mirror of
https://github.com/hoshikawa2/compass_backoffice.git
synced 2026-07-10 06:14:19 +00:00
231 lines
8.4 KiB
Python
231 lines
8.4 KiB
Python
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
|
|
)
|
|
)
|