242 lines
8.8 KiB
Python
242 lines
8.8 KiB
Python
from app.agents.prompting import apply_agent_profile_prompt
|
|
from app.agents.contas_prompting import load_domain_prompt
|
|
from app.agents.runtime import AgentRuntimeMixin
|
|
import re
|
|
import unicodedata
|
|
|
|
|
|
class FaturasAgent(AgentRuntimeMixin):
|
|
name = "faturas_agent"
|
|
|
|
def __init__(
|
|
self,
|
|
llm,
|
|
telemetry=None,
|
|
tool_router=None,
|
|
rag_service=None,
|
|
cache=None,
|
|
settings=None,
|
|
observer=None,
|
|
memory=None,
|
|
summary_memory=None,
|
|
guardrail_pipeline=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
|
|
self.guardrail_pipeline = guardrail_pipeline
|
|
|
|
|
|
@staticmethod
|
|
def _normalize_text(value):
|
|
text = unicodedata.normalize("NFKD", str(value or ""))
|
|
return "".join(ch for ch in text if not unicodedata.combining(ch)).lower().strip()
|
|
|
|
@classmethod
|
|
def _is_live_internet_balance_request(cls, state):
|
|
text = cls._normalize_text(
|
|
state.get("sanitized_input") or state.get("user_text") or state.get("input") or ""
|
|
)
|
|
if not text:
|
|
return False
|
|
|
|
internet = bool(re.search(r"\b(internet|dados moveis|dados|franquia)\b", text))
|
|
live_balance = bool(
|
|
re.search(
|
|
r"\b(quanto.*(?:resta|restou|tenho|sobrou|disponivel)|"
|
|
r"saldo|restante|restam|ainda tenho|ainda posso usar|consumo atual)\b",
|
|
text,
|
|
)
|
|
)
|
|
return internet and live_balance
|
|
|
|
@classmethod
|
|
def _has_live_internet_balance_evidence(cls, tool_context):
|
|
# Não bloqueia uma integração futura que passe a devolver o saldo em tempo real.
|
|
evidence_keys = {
|
|
"saldo_internet",
|
|
"internet_balance",
|
|
"remaining_data",
|
|
"remaining_data_mb",
|
|
"remaining_data_gb",
|
|
"saldo_dados",
|
|
"franquia_disponivel",
|
|
"consumo_atual",
|
|
"current_usage",
|
|
}
|
|
|
|
def walk(value):
|
|
if isinstance(value, dict):
|
|
for key, nested in value.items():
|
|
if cls._normalize_text(key).replace(" ", "_") in evidence_keys and nested not in (None, "", [], {}):
|
|
return True
|
|
if walk(nested):
|
|
return True
|
|
elif isinstance(value, list):
|
|
return any(walk(item) for item in value)
|
|
return False
|
|
|
|
for item in tool_context or []:
|
|
if not isinstance(item, dict) or item.get("ok") is False:
|
|
continue
|
|
if walk(item.get("result")):
|
|
return True
|
|
return False
|
|
|
|
@classmethod
|
|
def _live_internet_balance_guidance(cls, state, tool_context):
|
|
if not cls._is_live_internet_balance_request(state):
|
|
return None
|
|
if cls._has_live_internet_balance_evidence(tool_context):
|
|
return None
|
|
return (
|
|
"Não consigo consultar o saldo de internet em tempo real por aqui. "
|
|
"Para ver quanto ainda resta neste mês, consulte o app Meu TIM, "
|
|
"onde você acompanha o consumo atual da sua franquia."
|
|
)
|
|
|
|
@staticmethod
|
|
def _handoff_patch_from_tool_context(tool_context):
|
|
for item in tool_context or []:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
data = item.get("result")
|
|
if not isinstance(data, dict):
|
|
continue
|
|
nested = data.get("result")
|
|
if isinstance(nested, dict) and nested.get("session_control"):
|
|
data = nested
|
|
if str(data.get("session_control") or "").upper() != "HUMAN_HANDOFF":
|
|
continue
|
|
return {
|
|
"session_control": "HUMAN_HANDOFF",
|
|
"human_handoff_requested": True,
|
|
"session_ended": True,
|
|
"terminal_status": str(data.get("terminal_status") or "human_handoff"),
|
|
"handoff_reason": str(data.get("handoff_reason") or ""),
|
|
}
|
|
return {}
|
|
|
|
async def run(self, state):
|
|
await self._emit_ic(
|
|
"IC.FATURAS_AGENT_STARTED",
|
|
state,
|
|
{"business_component": "faturas"},
|
|
component="agent.faturas.start",
|
|
)
|
|
|
|
tool_context = await self._collect_tool_context(state)
|
|
if tool_context:
|
|
await self._emit_ic(
|
|
"IC.FATURAS_MCP_CONTEXT_COLLECTED",
|
|
state,
|
|
{"tool_result_count": len(tool_context)},
|
|
component="agent.faturas.mcp",
|
|
)
|
|
|
|
state["mcp_results"] = tool_context
|
|
handoff_patch = self._handoff_patch_from_tool_context(tool_context)
|
|
clarification_message = self.transaction_clarification_message(state)
|
|
if clarification_message:
|
|
return {
|
|
"answer": f"[{self.__class__.__name__}] {clarification_message}",
|
|
"next_state": state.get("next_state") or "COLLECTING_PARAMETERS",
|
|
"mcp_results": tool_context,
|
|
**self.transaction_state_patch(state),
|
|
**handoff_patch,
|
|
}
|
|
|
|
confirmation_message = self.transaction_confirmation_message(state)
|
|
if confirmation_message:
|
|
result = {
|
|
"answer": f"[{self.__class__.__name__}] {confirmation_message}",
|
|
"next_state": state.get("next_state"),
|
|
"mcp_results": tool_context,
|
|
**self.transaction_state_patch(state),
|
|
**handoff_patch,
|
|
}
|
|
return result
|
|
|
|
live_balance_guidance = self._live_internet_balance_guidance(state, tool_context)
|
|
if live_balance_guidance:
|
|
return {
|
|
"answer": f"[FaturasAgent] {live_balance_guidance}",
|
|
"next_state": state.get("next_state") or "ACTIVE",
|
|
"mcp_results": tool_context,
|
|
"rag": {"enabled": False, "skipped": True, "reason": "live_internet_balance_not_available"},
|
|
**self.transaction_state_patch(state),
|
|
**handoff_patch,
|
|
}
|
|
|
|
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="FaturasAgent")
|
|
if direct_answer:
|
|
return {
|
|
"answer": direct_answer,
|
|
"next_state": state.get("next_state") or "ACTIVE",
|
|
"mcp_results": tool_context,
|
|
"rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"},
|
|
**self.transaction_state_patch(state),
|
|
**handoff_patch,
|
|
}
|
|
|
|
rag_context, rag_metadata = await self._retrieve_rag_context(state)
|
|
if rag_metadata.get("enabled"):
|
|
await self._emit_ic(
|
|
"IC.FATURAS_RAG_CONTEXT_RETRIEVED",
|
|
state,
|
|
{
|
|
"document_count": rag_metadata.get("document_count"),
|
|
"graph_neighbors": rag_metadata.get("graph_neighbors"),
|
|
"latency_ms": rag_metadata.get("latency_ms"),
|
|
},
|
|
component="agent.faturas.rag",
|
|
)
|
|
|
|
# Prepara ConversationSummaryMemory antes de montar o prompt.
|
|
# O build_messages() do framework injeta resumo + últimas mensagens quando habilitado.
|
|
await self.prepare_memory_context(state)
|
|
|
|
messages = self.build_messages(
|
|
state,
|
|
system_prompt=apply_agent_profile_prompt(
|
|
state,
|
|
load_domain_prompt("billing"),
|
|
),
|
|
mcp_results=tool_context,
|
|
rag_context=rag_context,
|
|
rag_metadata=rag_metadata,
|
|
)
|
|
|
|
answer = await self._invoke_llm_cached(state, "FaturasAgent", messages)
|
|
result = {
|
|
"answer": f"[FaturasAgent] {answer}",
|
|
"next_state": "FATURAS_ACTIVE",
|
|
"mcp_results": tool_context,
|
|
"rag": rag_metadata,
|
|
"memory_context_metadata": state.get("memory_context_metadata"),
|
|
**self.transaction_state_patch(state),
|
|
**handoff_patch,
|
|
}
|
|
|
|
await self._emit_ic(
|
|
"IC.FATURAS_AGENT_COMPLETED",
|
|
state,
|
|
{
|
|
"answer_chars": len(result.get("answer") or ""),
|
|
"has_mcp_results": bool(tool_context),
|
|
"rag_enabled": bool(rag_metadata.get("enabled")),
|
|
"memory_context": state.get("memory_context_metadata"),
|
|
},
|
|
component="agent.faturas.completed",
|
|
)
|
|
return result
|
|
|
|
async def _collect_tool_context(self, state):
|
|
return await self._collect_mcp_context(state)
|