import json import logging from src.agent.state.agent_state import AgentState from src.agent.state.steps import GraphStep from typing import Any, Optional, Tuple from enum import Enum from fastapi import status from fastapi.responses import JSONResponse from src.api.schemas.anatel_schemas import TicketResponseEvent, Processing, FieldsToUpdate, KeyType from src.agent.state.agent_state import get_last_ai_message from src.utils.external_response_builder import build_external_response logger = logging.getLogger(__name__) def create_error_response( status_code: int, correlation_id: str, final_response: str, parsed_response: dict = None, metadata: dict = None, ) -> JSONResponse: """Standardized error envelope used by agent routes.""" if isinstance(parsed_response, dict): error_payload = { "title": parsed_response.get("title", "execution error"), "status": parsed_response.get("status", status_code), "correlation_id": correlation_id, "detail": parsed_response.get("detail", {"messages": []}), } else: error_payload = { "title": "system error" if status_code >= 500 else "validation error", "status": status_code, "correlation_id": correlation_id, "detail": { "messages": [{"code": "EXECUTION_ERROR", "text": str(final_response)}] }, } if metadata: error_payload["metadata"] = metadata return JSONResponse(status_code=status_code, content=error_payload) _SR_STATUSES = [ "opened_reclassification_sr", "opened_treatment_sr", "opened_cancelation_sr", "opened_forwarding_sr" ] def build_ticket_tags(event_context: dict, complaint_context: dict) -> list[str]: """Builds Langfuse tags from ticket event fields, used before graph execution.""" case_type = event_context.get("caseType", "unknown") service = complaint_context.get("service", "unknown") modality = complaint_context.get("modality", "unknown") action_type = complaint_context.get("actionType", "unknown") gov_br_seal = event_context.get("customer", {}).get("govBrSeal") or False return [ f"caseType: {case_type.value.capitalize() if isinstance(case_type, Enum) else case_type}", f"service: {service}", f"actionType: {action_type.capitalize()}", f"govBrSeal: {str(gov_br_seal).lower()}", ] def resolve_outcome_tag(current_step: str, error_info: Optional[dict]) -> str: """Returns the outcome tag based on graph execution result, used after graph execution.""" if current_step == GraphStep.VALIDATION_FAILED: return "outcome:validation_failed" if error_info: return "outcome:failed" return "outcome:completed" def extract_response_payload(state: AgentState) -> Tuple[str, Any]: """ Standardizes the extraction of the final response from agent state. Returns a tuple of (raw_string, parsed_json_or_string). """ final_response = state.get("final_response") if not final_response: last_ai_msg = get_last_ai_message(state) final_response = last_ai_msg.content if last_ai_msg else "No response generated" parsed = None try: parsed = json.loads(final_response) except Exception: parsed = final_response return final_response, parsed def get_http_status_code(state: AgentState, parsed_response: Any) -> int: """ Determines the appropriate HTTP status code based on state and response content. """ current_step = state.get("current_step", "unknown") error_state = state.get("error") is_error = bool(error_state) or current_step == GraphStep.VALIDATION_FAILED status_code = status.HTTP_200_OK # Try to extract status code from parsed response if it's a dict if isinstance(parsed_response, dict) and "status" in parsed_response: try: resp_status = int(parsed_response.get("status")) if resp_status >= 400: is_error = True status_code = resp_status except (ValueError, TypeError): pass if is_error and status_code == status.HTTP_200_OK: is_val_err = (current_step == GraphStep.VALIDATION_FAILED or (error_state and error_state.get("type") == "ValidationError")) status_code = status.HTTP_400_BAD_REQUEST if is_val_err else status.HTTP_500_INTERNAL_SERVER_ERROR return status_code def build_cms_response_event(state: AgentState, correlation_id: str) -> TicketResponseEvent: """ Unified logic to build the TicketResponseEvent for both API and Background Worker. """ context = state.get("metadata", {}).get("request_context", {}) sr_data = context.get("siebel_sr_data", {}) sr_number = sr_data.get("interactionProtocol", "N/A") decision = context.get("siebel_action", "done") current_step = state.get("current_step", "unknown") status_mapping = { "reclassificar": "opened_reclassification_sr", "tratamento": "opened_treatment_sr", "cancelar": "opened_cancelation_sr", "reencaminhar": "opened_forwarding_sr" } if current_step == GraphStep.VALIDATION_FAILED or sr_number == "N/A": cms_status = "failed" else: cms_status = status_mapping.get(decision.lower() if isinstance(decision, str) else "done", "done") # Determine response action if cms_status in _SR_STATUSES: action = "update" elif sr_number != "N/A": action = "response" else: action = "atg" # Build fieldsToUpdate for reclassification fields_to_update = None if cms_status == "opened_reclassification_sr": fields_to_update = [ FieldsToUpdate(keyType=KeyType.STRING, keyDesc="Motivo/Problema da reclamação a ser corrigido", keyName="motive"), FieldsToUpdate(keyType=KeyType.STRING, keyDesc="Modalidade/Assunto da reclamação a ser corrigida", keyName="modality") ] # Build enhanced metadata metadata = { "siebel_action": decision, } for key in ("canceling_decision", "forwarding_decision", "reclassification_decision", "treatment_decision"): if context.get(key): metadata[key] = context.get(key) # Add knowledge base enrichment data if available relevant_docs = context.get("relevant_documents") if relevant_docs: kb_payload = { "query": relevant_docs.get("query"), "documents": relevant_docs.get("documents") or [], } if relevant_docs.get("message"): kb_payload["message"] = relevant_docs["message"] if relevant_docs.get("postprocessing_id_procs_map"): kb_payload["postprocessing_id_procs_map"] = relevant_docs["postprocessing_id_procs_map"] metadata["relevant_documents"] = kb_payload # Add speech enrichment data if available speech = context.get("speech_analytics") if speech: atual_keys = ( "reclamacao_resumo", "causa_raiz", "descortesia_cliente", "motivo_reclamacao", "submotivo_reclamacao", "sentimento_cliente", "solucao_proposta_cliente", ) related_keys = ("protocolo", "data_reclamacao", "similaridade_pct") + atual_keys historico_relacionado = [ {k: item.get(k) for k in related_keys} for item in (speech.get("historico_relacionado") or []) ] metadata["speech_retrieved_data"] = { "reclamacao_atual": {k: speech.get(k) for k in atual_keys}, "historico": { "relacionado": historico_relacionado or [], "analise_agente": speech.get("analise_agente"), }, } # Include execution markers metadata.update({ "iteration_count": state.get("iteration_count", 0), "error": state.get("error") }) case_response = build_external_response(context, decision.lower() if isinstance(decision, str) else "") return TicketResponseEvent( transactionId=correlation_id, processing=Processing( status=cms_status, current_step=current_step, action=action, note=state.get("processing_notes") or None, crmProtocol=sr_number if sr_number != "N/A" else None, fieldsToUpdate=fields_to_update, case_response=case_response, metadata=metadata ) )