mirror of
https://github.com/hoshikawa2/compass_backoffice.git
synced 2026-07-09 22:04:20 +00:00
169 lines
5.9 KiB
Python
169 lines
5.9 KiB
Python
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%): <reasoning>'. 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.
|