mirror of
https://github.com/hoshikawa2/first_contas.git
synced 2026-07-09 10:14:20 +00:00
219 lines
8.7 KiB
Python
219 lines
8.7 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from app.agents.runtime import AgentRuntimeMixin
|
|
|
|
|
|
_CONTAS_TOOLS_BY_INTENT: dict[str, list[str]] = {
|
|
"billing_invoice_explanation": ["consultar_fatura", "consultar_pagamentos"],
|
|
"billing_payment_status": ["consultar_pagamentos", "consultar_fatura"],
|
|
"billing_second_copy": ["consultar_fatura", "recuperar_pdf_fatura"],
|
|
"billing_contestation": ["consultar_fatura", "consultar_pagamentos", "consultar_status_sr"],
|
|
"service_plan_information": ["consultar_plano", "listar_servicos"],
|
|
"vas_services_information": ["listar_servicos", "consultar_plano", "consultar_status_sr"],
|
|
"billing_protocol_status": ["consultar_status_sr"],
|
|
"billing_protocol_register": ["registrar_protocolo"],
|
|
"vas_cancel_or_block": ["listar_servicos", "cancelar_vas", "bloquear_vas", "registrar_protocolo"],
|
|
"send_billing_sms": ["enviar_sms"],
|
|
"tracking_activity": ["registrar_tracking"],
|
|
"generic_billing_rag": [],
|
|
}
|
|
|
|
|
|
class ContasAgent(AgentRuntimeMixin):
|
|
"""Agent Contas FIRST migrado para o agent_framework_oci.
|
|
|
|
Esta classe mantém a lógica específica de negócio apenas como prompt e
|
|
seleção declarativa de intents/tools. Guardrails, judges, RAG, memória,
|
|
cache MCP, Identity/BusinessContext, telemetria IC/NOC/GRL e chamada MCP são
|
|
executados pelos componentes nativos do framework.
|
|
"""
|
|
|
|
name = "contas_agent"
|
|
|
|
def __init__(
|
|
self,
|
|
llm,
|
|
telemetry=None,
|
|
tool_router=None,
|
|
rag_service=None,
|
|
cache=None,
|
|
settings=None,
|
|
observer=None,
|
|
memory=None,
|
|
summary_memory=None,
|
|
):
|
|
self.llm = llm
|
|
self.telemetry = telemetry
|
|
self.tool_router = tool_router
|
|
self.rag_service = rag_service
|
|
self.cache = cache
|
|
self.settings = settings
|
|
self.observer = observer
|
|
self.memory = memory
|
|
self.summary_memory = summary_memory
|
|
|
|
async def run(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
state = self.normalize_tools_by_intent(
|
|
state,
|
|
default_tools_by_intent=_CONTAS_TOOLS_BY_INTENT,
|
|
default_intent="billing_invoice_explanation",
|
|
route=self.name,
|
|
)
|
|
|
|
await self._emit_ic(
|
|
"IC.CONTAS_AGENT_STARTED",
|
|
state,
|
|
{
|
|
"framework_native": True,
|
|
"uses_native_guardrails": True,
|
|
"uses_native_rag": True,
|
|
"uses_native_mcp_router": True,
|
|
"legacy_business_layer": False,
|
|
},
|
|
component="agent.contas.start",
|
|
)
|
|
|
|
runtime = self.get_runtime_context(state)
|
|
missing = []
|
|
if not runtime.pick("customer_key", "msisdn", "customer_id"):
|
|
missing.append("customer_key/msisdn")
|
|
if missing:
|
|
await self._emit_noc(
|
|
"NOC.CONTAS_IDENTITY_INCOMPLETE",
|
|
state,
|
|
{"missing": missing, "business_context": runtime.business_context},
|
|
component="agent.contas.identity",
|
|
)
|
|
|
|
await self.prepare_memory_context(state)
|
|
|
|
rag_context, rag_metadata = await self._retrieve_rag_context(state)
|
|
await self._emit_ic(
|
|
"IC.CONTAS_RAG_CONTEXT_RETRIEVED",
|
|
state,
|
|
rag_metadata,
|
|
component="agent.contas.rag",
|
|
)
|
|
|
|
mcp_results = await self.execute_tools_for_intent(
|
|
state,
|
|
aliases={
|
|
"customer_key": ["msisdn", "customer_id", "ani", "from"],
|
|
"contract_key": ["invoice_id", "current_invoice_number", "asset_id"],
|
|
"interaction_key": ["ura_call_id", "message_id", "call_id"],
|
|
"resource_key": ["asset_id", "product_id"],
|
|
"account_key": ["billing_account_id", "account_id"],
|
|
},
|
|
)
|
|
mcp_results = self._normalize_mcp_diagnostics(mcp_results)
|
|
state["mcp_results"] = mcp_results
|
|
|
|
await self._emit_ic(
|
|
"IC.CONTAS_MCP_CONTEXT_COLLECTED",
|
|
state,
|
|
{
|
|
"tool_count": len(state.get("mcp_tools") or []),
|
|
"result_count": len(mcp_results),
|
|
"ok_count": sum(1 for r in mcp_results if r.get("ok")),
|
|
},
|
|
component="agent.contas.mcp",
|
|
)
|
|
|
|
system_prompt = """
|
|
Você é o Agent Contas FIRST migrado para o agent_framework_oci.
|
|
|
|
Regras de arquitetura:
|
|
- Use os resultados MCP recebidos do framework como fonte operacional.
|
|
- Use o contexto RAG nativo como fonte de política/conhecimento.
|
|
- Não invente dados de fatura, pagamento, plano, VAS, protocolo ou contestação.
|
|
- Não crie regras de negócio paralelas; quando faltar evidência, diga exatamente o que falta.
|
|
- Mantenha a resposta curta, auditável e adequada para atendimento de telecom.
|
|
- Para ações sensíveis como contestação/cancelamento/bloqueio, explique o próximo passo e solicite confirmação quando a tool/política exigir.
|
|
|
|
Coberturas vindas do agent_contas_first reaproveitadas como comportamento, não como camada legada:
|
|
- explicação de fatura e divergência de valor;
|
|
- consulta de pagamentos e segunda via;
|
|
- plano/serviços/VAS;
|
|
- contestação e fluxos com confirmação;
|
|
- uso de protocolo apenas quando existir ferramenta/evidência.
|
|
""".strip()
|
|
|
|
messages = self.build_messages(
|
|
state,
|
|
system_prompt=system_prompt,
|
|
mcp_results=mcp_results,
|
|
rag_context=rag_context,
|
|
rag_metadata=rag_metadata,
|
|
extra_sections={
|
|
"Diretriz de migração": "Máximo uso do framework; legado apenas como referência funcional.",
|
|
"Intents suportadas": sorted(_CONTAS_TOOLS_BY_INTENT.keys()),
|
|
},
|
|
)
|
|
|
|
try:
|
|
answer = await self._invoke_llm_cached(state, self.name, messages)
|
|
except Exception as exc:
|
|
await self._emit_noc(
|
|
"NOC.CONTAS_LLM_FAILED",
|
|
state,
|
|
{"error": str(exc)},
|
|
component="agent.contas.llm",
|
|
)
|
|
answer = self.build_llm_fallback_answer(state, mcp_results, agent_label="ContasAgent")
|
|
|
|
await self._emit_ic(
|
|
"IC.CONTAS_AGENT_COMPLETED",
|
|
state,
|
|
{
|
|
"answer_chars": len(str(answer or "")),
|
|
"mcp_result_count": len(mcp_results),
|
|
"rag_document_count": rag_metadata.get("document_count"),
|
|
},
|
|
component="agent.contas.completed",
|
|
)
|
|
|
|
return {
|
|
"answer": str(answer or ""),
|
|
"mcp_results": mcp_results,
|
|
"rag_metadata": rag_metadata,
|
|
"active_agent": self.name,
|
|
"route": self.name,
|
|
"next_state": self._next_state_for_intent(state),
|
|
}
|
|
|
|
|
|
def _normalize_mcp_diagnostics(self, results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
"""Preserva evidência MCP mesmo quando versões antigas do router retornam erro vazio.
|
|
|
|
A integração correta agora retorna metadata rica do MCP server legado.
|
|
Esta proteção evita que o prompt/telemetria recebam `ok=False` sem
|
|
diagnóstico quando o ToolRouter instalado for anterior ou quando o
|
|
servidor MCP antigo ainda estiver rodando em outra janela.
|
|
"""
|
|
normalized: list[dict[str, Any]] = []
|
|
for item in results or []:
|
|
if not isinstance(item, dict):
|
|
normalized.append({"ok": False, "error": f"Resultado MCP inválido: {type(item).__name__}", "metadata": {"exception_type": "InvalidMCPResult"}})
|
|
continue
|
|
current = dict(item)
|
|
metadata = current.get("metadata")
|
|
if not isinstance(metadata, dict):
|
|
metadata = {}
|
|
if current.get("ok") is False and not str(current.get("error") or "").strip():
|
|
current["error"] = "MCP retornou ok=False sem mensagem de erro; confirme se o legacy_tim_mcp v7 está em execução e reinicie o backend."
|
|
metadata.setdefault("exception_type", "EmptyMCPError")
|
|
metadata.setdefault("diagnostic_hint", "O servidor MCP antigo ou o ToolRouter instalado pode estar descartando error/metadata.")
|
|
current["metadata"] = metadata
|
|
normalized.append(current)
|
|
return normalized
|
|
|
|
def _next_state_for_intent(self, state: dict[str, Any]) -> str:
|
|
intent = state.get("intent") or ""
|
|
if intent in {"billing_contestation", "billing_second_copy"}:
|
|
return "WAITING_BILLING_CONFIRMATION"
|
|
if intent in {"service_plan_information", "vas_services_information"}:
|
|
return "WAITING_SERVICE_CONFIRMATION"
|
|
return "CONTAS_ACTIVE"
|