import time import logging import json from src.agent.state.agent_state import AgentState, update_state_metadata from src.agent.state.steps import GraphStep from src.agent.state.step_helpers import set_current_step from src.components.clients.speech_analytics_client import SpeechAnalyticsClient from src.components.clients.exceptions.speech_exceptions import SpeechClientError from src.utils.observer import trace_node, score_current_trace from src.utils.ics_collector import build_ic_payload, build_noc_api_metadata, build_noc_llm_metadata from src.compat.framework_observer import event from src.providers.llm_provider import classification_llm, chat_llm_with_usage, LLM_ENDPOINT from src.core.config import settings from src.core.prompt_manager import get_prompt from src.agent.local_prompts.speech_history_analysis import speech_history_similarity_pt logger = logging.getLogger(__name__) _NO_SIMILAR_MESSAGE = "Não foram encontradas reclamações anteriores similares à reclamação atual" _HISTORY_UNAVAILABLE_MESSAGE = "Indisponível SPEECH para Histórico de Reclamação - realizar consulta manualmente" _HISTORY_DETAIL_KEYS = ( "reclamacao_resumo", "causa_raiz", "descortesia_cliente", "motivo_reclamacao", "submotivo_reclamacao", "sentimento_cliente", "solucao_proposta_cliente", ) _NULL_SPEECH_DATA = { "reclamacao_resumo": None, "causa_raiz": None, "descortesia_cliente": None, "motivo_reclamacao": None, "submotivo_reclamacao": None, "sentimento_cliente": None, "solucao_proposta_cliente": None, "analise_agente": None, } def _map_prediction_response(response: dict) -> dict: """ Map the raw Prediction API response to the internal speech_analytics schema. """ variables = response.get("variables", {}) return { "reclamacao_resumo": response.get("resume") or variables.get("resume"), "causa_raiz": variables.get("causa_raiz"), "descortesia_cliente": variables.get("descortesia_cliente"), "motivo_reclamacao": variables.get("motivo"), "submotivo_reclamacao": variables.get("submotivo"), "sentimento_cliente": variables.get("sentimento_cliente"), "solucao_proposta_cliente": variables.get("solucao_proposta_cliente"), } def _build_related_history( raw_history: list, current_complaint_id: str | None, llm_scores: list, threshold: int, ) -> list: """ Merge LLM similarity scores with raw history items and filter by threshold. Each returned item has: protocolo, data_reclamacao, similaridade_pct, reasoning, plus the 7 detail keys (same as reclamacao_atual). """ if not raw_history or not llm_scores: return [] history_by_protocol = {str(item.get("protocolo")): item for item in raw_history} related: list[dict] = [] for score in llm_scores: protocolo = str(score.get("protocolo")) if score.get("protocolo") is not None else None if not protocolo or protocolo == str(current_complaint_id or ""): continue try: similaridade_pct = int(score.get("similaridade_pct", 0)) except (TypeError, ValueError): continue if similaridade_pct < threshold: continue raw_item = history_by_protocol.get(protocolo) if not raw_item: continue merged = { "protocolo": raw_item.get("protocolo"), "data_reclamacao": raw_item.get("data_reclamacao"), "similaridade_pct": similaridade_pct, "reasoning": (score.get("reasoning") or "").strip(), } for key in _HISTORY_DETAIL_KEYS: merged[key] = raw_item.get(key) related.append(merged) related.sort(key=lambda x: x["similaridade_pct"], reverse=True) return related def _build_analise_agente(related_items: list) -> str: """ Build the analise_agente string from related items: one line per item with 'Protocolo X (Y%): '. Empty list -> negative spec message. """ if not related_items: return _NO_SIMILAR_MESSAGE lines = [] for item in related_items: reasoning = (item.get("reasoning") or "").strip() if not reasoning: reasoning = "—" lines.append(f"Protocolo {item.get('protocolo')} ({item.get('similaridade_pct')}%): {reasoning}") return "\n".join(lines) async def _score_history_with_llm( current_id: str | None, current_complaint_description: str, history_list: list, session_id: str = "", state: dict | None = None, ) -> list: """ Calls the LLM to score similarity between the current complaint description and each historical item. Returns the LLM-parsed JSON list. Items below SPEECH_SIMILARITY_THRESHOLD will be dropped later by _build_related_history. """ if not current_complaint_description or not history_list: return [] clean_history = [ item for item in history_list if str(item.get("protocolo")) != str(current_id or "") ] if not clean_history: logger.info("No historical items left after deduplication.") return [] llm = classification_llm prompt_template = get_prompt("speech_history_similarity_pt", speech_history_similarity_pt) history_payload = [ { "protocolo": item.get("protocolo"), "motivo_reclamacao": item.get("motivo_reclamacao"), "submotivo_reclamacao": item.get("submotivo_reclamacao"), "causa_raiz": item.get("causa_raiz"), "reclamacao_resumo": item.get("reclamacao_resumo"), } for item in clean_history ] message = prompt_template.format( current_complaint_description=current_complaint_description, history_json=json.dumps(history_payload, ensure_ascii=False), ) logger.info(f"Calling LLM for history similarity filtering. Items: {len(clean_history)}") # LLM generation telemetry is recorded by agent_framework.llm.providers.