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)