mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-09-07 10:13:46 +00:00
Ajustes conforme relatorio de testes 2026-08-27
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -95,7 +95,7 @@ class BillingAgent(AgentRuntimeMixin):
|
||||
state,
|
||||
system_prompt=apply_agent_profile_prompt(
|
||||
state,
|
||||
"Você é um agente especialista em faturas.\n\nUse dados de tools/MCP e RAG autorizados como fonte de verdade para responder somente à solicitação atual.\nNunca exponha identificadores técnicos ou de identidade presentes no estado, contexto ou MCP, incluindo customer_key, contract_key, account_key, resource_key, session_key, customer_id, document, message_id, ura_call_id ou MSISDN/telefone completo.\nNão transforme nomes internos de campos em rótulos para o cliente, como “contract_key”, “customer_key” ou “MSISDN”.\nPara consultas informativas de fatura, apresente somente dados de negócio necessários, como valor, vencimento, situação e itens cobrados.\nNão acrescente canais, telefones, códigos USSD, URLs, aplicativos, lojas, relatórios adicionais, procedimentos alternativos ou próximos passos que não tenham sido explicitamente retornados pela tool/RAG e solicitados pelo usuário.\nNão ofereça espontaneamente outras ações ou detalhamentos.\nSe uma tool retornar BLOCKED, OUT_OF_SCOPE, NOT_ALLOWED, FAILED ou outro resultado terminal, explique somente o motivo retornado, não declare sucesso, não invente alternativa e encerre a resposta.",
|
||||
"Você é um agente especialista em faturas. Responda com clareza, objetividade e sem sugerir ações não solicitadas. Use dados MCP quando disponíveis.",
|
||||
),
|
||||
mcp_results=tool_context,
|
||||
rag_context=rag_context,
|
||||
|
||||
@@ -95,7 +95,7 @@ class OrdersAgent(AgentRuntimeMixin):
|
||||
state,
|
||||
system_prompt=apply_agent_profile_prompt(
|
||||
state,
|
||||
"Você é um agente de pedidos de varejo.\n\nUse dados de tools/MCP e RAG autorizados como fonte de verdade e responda somente à solicitação atual.\nNunca exponha identificadores técnicos ou de identidade presentes no estado, contexto ou MCP, incluindo customer_key, contract_key, account_key, resource_key, session_key, customer_id, document, message_id, ura_call_id ou telefone completo.\nNão transforme nomes internos de campos em rótulos para o cliente.\nNão declare sucesso, alteração, troca, cancelamento ou qualquer mutação se a tool não tiver confirmado a execução.\nNão acrescente canais, procedimentos, ofertas ou próximos passos não solicitados.\nSe uma tool retornar BLOCKED, OUT_OF_SCOPE, NOT_ALLOWED, FAILED ou outro resultado terminal, explique somente o motivo retornado, não invente alternativa e encerre a resposta.",
|
||||
"Você é um agente de pedidos de varejo. Use dados de tools quando disponíveis.",
|
||||
),
|
||||
mcp_results=tool_context,
|
||||
rag_context=rag_context,
|
||||
|
||||
@@ -110,7 +110,7 @@ class ProductAgent(AgentRuntimeMixin):
|
||||
state,
|
||||
system_prompt=apply_agent_profile_prompt(
|
||||
state,
|
||||
"Você é um agente especialista em produtos, planos e serviços.\n\nUse dados de tools/MCP e RAG autorizados como fonte de verdade e responda somente à solicitação atual.\nNunca exponha identificadores técnicos ou de identidade presentes no estado, contexto ou MCP, incluindo customer_key, contract_key, account_key, resource_key, session_key, customer_id, document, message_id, ura_call_id ou MSISDN/telefone completo.\nNão transforme nomes internos de campos em rótulos para o cliente.\nEm consultas meramente informativas, não exponha flags ou capacidades transacionais internas como can.cancel e não informe espontaneamente que algo pode ser cancelado, alterado, contratado, removido ou trocado. Só mencione capacidade transacional quando o usuário tiver solicitado essa ação.\nNão faça oferta proativa e não execute nem simule mutações sem a confirmação exigida pelo framework.\nNão acrescente canais, procedimentos ou próximos passos não solicitados.\nSe uma tool retornar BLOCKED, OUT_OF_SCOPE, NOT_ALLOWED, FAILED ou outro resultado terminal, explique somente o motivo retornado, não declare sucesso, não invente alternativa e encerre a resposta.",
|
||||
"Você é um agente especialista em produtos, planos e serviços. Explique sem fazer oferta proativa e sem executar ações sem confirmação. Use dados MCP quando disponíveis.",
|
||||
),
|
||||
mcp_results=tool_context,
|
||||
rag_context=rag_context,
|
||||
|
||||
@@ -95,7 +95,7 @@ class SupportAgent(AgentRuntimeMixin):
|
||||
state,
|
||||
system_prompt=apply_agent_profile_prompt(
|
||||
state,
|
||||
"Você é um agente de suporte de varejo para troca, devolução e garantia.\n\nUse dados de tools/MCP e RAG autorizados como fonte de verdade e responda somente à solicitação atual.\nNunca exponha identificadores técnicos ou de identidade presentes no estado, contexto ou MCP, incluindo customer_key, contract_key, account_key, resource_key, session_key, customer_id, document, message_id, ura_call_id ou telefone completo.\nNão transforme nomes internos de campos em rótulos para o cliente.\nNão declare sucesso nem simule troca, devolução, garantia ou outra mutação se a tool não tiver confirmado a execução.\nNão acrescente canais, procedimentos, ofertas ou próximos passos não solicitados.\nSe uma tool retornar BLOCKED, OUT_OF_SCOPE, NOT_ALLOWED, FAILED ou outro resultado terminal, explique somente o motivo retornado, não invente alternativa e encerre a resposta.",
|
||||
"Você é um agente de suporte de varejo para troca, devolução e garantia.",
|
||||
),
|
||||
mcp_results=tool_context,
|
||||
rag_context=rag_context,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,42 +13,7 @@ def _money_brl(value: Any) -> str:
|
||||
|
||||
|
||||
def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||
"""Renderiza somente campos de negócio seguros da fatura.
|
||||
|
||||
Identificadores técnicos/PII presentes no payload MCP (por exemplo msisdn,
|
||||
customer_id, document e business keys) não devem ser propagados ao usuário.
|
||||
"""
|
||||
lines = [f"[{agent_label}] Dados da sua fatura:"]
|
||||
total = result.get("valor_total")
|
||||
vencimento = result.get("vencimento")
|
||||
status = result.get("status")
|
||||
if total is not None:
|
||||
lines.append(f"Valor total: R$ {_money_brl(total)}.")
|
||||
if vencimento not in (None, ""):
|
||||
lines.append(f"Vencimento: {vencimento}.")
|
||||
if status not in (None, ""):
|
||||
lines.append(f"Situação: {status}.")
|
||||
|
||||
items = result.get("itens") or []
|
||||
rendered_items: list[str] = []
|
||||
if isinstance(items, list):
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
description = item.get("descricao") or item.get("nome")
|
||||
value = item.get("valor")
|
||||
if description in (None, ""):
|
||||
continue
|
||||
if value is None:
|
||||
rendered_items.append(str(description))
|
||||
else:
|
||||
rendered_items.append(f"{description}: R$ {_money_brl(value)}")
|
||||
if rendered_items:
|
||||
lines.append("Itens: " + "; ".join(rendered_items) + ".")
|
||||
|
||||
# Se não houver nenhum campo de negócio seguro além do cabeçalho, deixe a
|
||||
# composição pela LLM/guardrails em vez de despejar o payload bruto.
|
||||
return " ".join(lines) if len(lines) > 1 else None
|
||||
return f"[{agent_label}] Fatura consultada: {result}."
|
||||
|
||||
|
||||
def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -160,7 +160,7 @@ class AgentWorkflow:
|
||||
builder.add_conditional_edges(
|
||||
"input_guardrails",
|
||||
self._after_input_guardrails,
|
||||
{"blocked": "persist", "continue": "load_long_term_memory"},
|
||||
{"blocked": "output_guardrails", "continue": "load_long_term_memory"},
|
||||
)
|
||||
builder.add_edge("load_long_term_memory", "routing_decision")
|
||||
builder.add_conditional_edges(
|
||||
@@ -186,7 +186,11 @@ class AgentWorkflow:
|
||||
builder.add_edge("end_session", "output_supervisor")
|
||||
builder.add_edge("supervisor_agent", "output_supervisor")
|
||||
builder.add_edge("output_supervisor", "output_guardrails")
|
||||
builder.add_edge("output_guardrails", "judge")
|
||||
builder.add_conditional_edges(
|
||||
"output_guardrails",
|
||||
lambda s: "blocked" if s.get("blocked") else "continue",
|
||||
{"blocked": "persist", "continue": "judge"},
|
||||
)
|
||||
builder.add_edge("judge", "supervisor_review")
|
||||
builder.add_edge("supervisor_review", "persist_long_term_memory")
|
||||
builder.add_edge("persist_long_term_memory", "persist")
|
||||
@@ -197,6 +201,28 @@ class AgentWorkflow:
|
||||
def _after_input_guardrails(self, state):
|
||||
return "blocked" if state.get("blocked") else "continue"
|
||||
|
||||
@staticmethod
|
||||
def _input_guardrail_user_message(decisions, state, sanitized_text):
|
||||
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
|
||||
first = blocked[0] if blocked else None
|
||||
code = str(getattr(first, "code", "") or "").upper()
|
||||
if code == "COER":
|
||||
return (
|
||||
"Não consegui entender sua última mensagem porque ela parece "
|
||||
"incompleta ou ambígua. Pode reformular ou completar o que você quis dizer?"
|
||||
)
|
||||
if code == "INPUT_SIZE":
|
||||
return "Sua mensagem ficou muito longa para eu processar de uma vez. Pode resumir ou dividir em partes?"
|
||||
if code == "DLEX_IN":
|
||||
return "Não posso usar essa informação da forma solicitada. Reformule o pedido sem incluir dados ou conteúdo restrito."
|
||||
if code == "PINJ":
|
||||
return "Não posso seguir instruções que tentem alterar as regras do atendimento. Posso continuar ajudando com a sua solicitação."
|
||||
if code == "TOX":
|
||||
return "Não consegui prosseguir com essa mensagem. Pode reformular o pedido para continuarmos o atendimento?"
|
||||
if code == "CMP":
|
||||
return "Não posso prosseguir com essa solicitação dessa forma. Posso ajudar com uma alternativa permitida."
|
||||
return "Não consegui processar essa mensagem. Pode reformular para eu continuar o atendimento?"
|
||||
|
||||
async def input_guardrails(self, state):
|
||||
if state.get("session_ended") is True:
|
||||
answer = str(getattr(
|
||||
@@ -281,12 +307,32 @@ class AgentWorkflow:
|
||||
component="workflow.input_guardrails.final",
|
||||
)
|
||||
if any(not d.allowed for d in decisions):
|
||||
# Input blocks stop routing/tools. Keep the internal reason in telemetry,
|
||||
# create a safe user-facing message, then send it through output guardrails.
|
||||
user_message = self._input_guardrail_user_message(decisions, state, sanitized)
|
||||
return {
|
||||
"sanitized_input": sanitized,
|
||||
"answer": "Não consegui seguir com essa mensagem por regra de segurança.",
|
||||
"final_answer": "Não consegui seguir com essa mensagem por regra de segurança.",
|
||||
"answer": user_message,
|
||||
"final_answer": None,
|
||||
"guardrail_decisions": [d.model_dump() for d in decisions],
|
||||
"route": "blocked",
|
||||
"intent": "input_guardrail_blocked",
|
||||
"route_decision": {
|
||||
"route": "blocked",
|
||||
"agent": None,
|
||||
"intent": "input_guardrail_blocked",
|
||||
"confidence": 1.0,
|
||||
"reason": "Entrada interrompida por guardrail antes do roteamento.",
|
||||
"method": "guardrail",
|
||||
"next_state": state.get("next_state"),
|
||||
"handoff": False,
|
||||
"metadata": {},
|
||||
"domain": state.get("domain"),
|
||||
"mcp_tools": [],
|
||||
},
|
||||
"mcp_tools": [],
|
||||
"mcp_results": [],
|
||||
"judge_results": [],
|
||||
"blocked": True,
|
||||
}
|
||||
return {
|
||||
@@ -494,119 +540,6 @@ class AgentWorkflow:
|
||||
"next_state": "SESSION_ENDED",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _output_guardrail_context(state: dict) -> dict:
|
||||
"""Monta o contexto operacional do turno para os guardrails de saída.
|
||||
|
||||
Mantém evidências/protocolos necessários aos rails, mas impede que uma
|
||||
transação encerrada ou semanticamente interrompida governe o novo turno.
|
||||
O histórico completo permanece no state/checkpoint para auditoria.
|
||||
"""
|
||||
ctx = dict(state.get("context", {}) or {})
|
||||
mcp_results = state.get("mcp_results") or []
|
||||
ctx["evidence"] = mcp_results or ctx.get("evidence")
|
||||
ctx["tool_result"] = mcp_results or ctx.get("tool_result")
|
||||
ctx["tool_executed"] = any(isinstance(r, dict) and r.get("ok") for r in mcp_results)
|
||||
|
||||
history = list(state.get("history") or [])
|
||||
current_user_text = str(state.get("user_text") or "").strip()
|
||||
if current_user_text:
|
||||
if (
|
||||
not history
|
||||
or not isinstance(history[-1], dict)
|
||||
or str(history[-1].get("content") or "") != current_user_text
|
||||
or str(history[-1].get("role") or "") != "user"
|
||||
):
|
||||
history.append({"role": "user", "content": current_user_text})
|
||||
|
||||
route_decision = state.get("route_decision") or {}
|
||||
route_metadata = route_decision.get("metadata") if isinstance(route_decision, dict) else {}
|
||||
route_metadata = route_metadata if isinstance(route_metadata, dict) else {}
|
||||
pre_validation = state.get("transaction_pre_validation") or {}
|
||||
pre_validation = pre_validation if isinstance(pre_validation, dict) else {}
|
||||
tx_status = str(
|
||||
state.get("transaction_status") or pre_validation.get("status") or ""
|
||||
).strip().upper()
|
||||
terminal_tx = bool(pre_validation.get("terminal")) or tx_status in {
|
||||
"COMPLETED", "FAILED", "CANCELLED", "BLOCKED", "OUT_OF_SCOPE"
|
||||
}
|
||||
semantic_intent_shift = (
|
||||
str(route_metadata.get("transaction_interruption") or "").strip().lower()
|
||||
== "intent_shift"
|
||||
)
|
||||
stickiness_intent_shift = bool(route_metadata.get("route_stickiness_preempted"))
|
||||
should_isolate_history = semantic_intent_shift or (terminal_tx and stickiness_intent_shift)
|
||||
|
||||
current_route = str(
|
||||
state.get("route")
|
||||
or (route_decision.get("route") if isinstance(route_decision, dict) else "")
|
||||
or ""
|
||||
).strip()
|
||||
current_intent = str(
|
||||
state.get("intent")
|
||||
or (route_decision.get("intent") if isinstance(route_decision, dict) else "")
|
||||
or ""
|
||||
).strip()
|
||||
ctx["current_user_message"] = current_user_text
|
||||
ctx["current_route"] = current_route
|
||||
ctx["current_intent"] = current_intent
|
||||
|
||||
if should_isolate_history:
|
||||
operational_history = (
|
||||
[{"role": "user", "content": current_user_text}]
|
||||
if current_user_text else []
|
||||
)
|
||||
ctx["historical_transaction_ignored"] = True
|
||||
ctx["historical_transaction_status"] = tx_status or (
|
||||
"INTERRUPTED" if semantic_intent_shift else "TERMINAL"
|
||||
)
|
||||
if semantic_intent_shift:
|
||||
ctx["historical_transaction_interruption"] = "intent_shift"
|
||||
for stale_key in (
|
||||
"transaction_pre_validation",
|
||||
"transaction_status",
|
||||
"active_transaction",
|
||||
"transaction",
|
||||
):
|
||||
ctx.pop(stale_key, None)
|
||||
else:
|
||||
operational_history = history
|
||||
|
||||
ctx["conversation_history"] = operational_history
|
||||
ctx["history_texts"] = [
|
||||
str(item.get("content") or "")
|
||||
for item in operational_history
|
||||
if isinstance(item, dict) and item.get("content") not in (None, "")
|
||||
]
|
||||
|
||||
protocols: list[str] = []
|
||||
seen: set[str] = set()
|
||||
protocol_keys = {
|
||||
"protocol_number", "protocolo_id", "interactionProtocol",
|
||||
"protocolNumber", "finalizacao_protocol",
|
||||
}
|
||||
|
||||
def walk(value):
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
if key in protocol_keys and item not in (None, ""):
|
||||
text = str(item).strip()
|
||||
if text and text not in seen:
|
||||
seen.add(text)
|
||||
protocols.append(text)
|
||||
elif isinstance(item, (dict, list, tuple)):
|
||||
walk(item)
|
||||
elif isinstance(value, (list, tuple)):
|
||||
for item in value:
|
||||
walk(item)
|
||||
|
||||
walk(mcp_results)
|
||||
if protocols:
|
||||
ctx["expected_protocols"] = protocols
|
||||
ctx["requer_protocolo"] = True
|
||||
ctx.setdefault("tipo_fluxo", "ajuste")
|
||||
return ctx
|
||||
|
||||
async def output_supervisor(self, state):
|
||||
"""Valida a resposta candidata com o OutputSupervisor corporativo.
|
||||
|
||||
@@ -622,15 +555,15 @@ class AgentWorkflow:
|
||||
}
|
||||
|
||||
candidate = state.get("answer") or ""
|
||||
context = self._output_guardrail_context(state)
|
||||
context.update({
|
||||
context = {
|
||||
**(state.get("context") or {}),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"route": state.get("route"),
|
||||
"intent": state.get("intent"),
|
||||
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
|
||||
})
|
||||
}
|
||||
async with self.telemetry.span(
|
||||
"workflow.output_supervisor",
|
||||
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||
@@ -716,7 +649,7 @@ class AgentWorkflow:
|
||||
component="workflow.output_guardrails.start",
|
||||
)
|
||||
final, decisions = await self.guardrails.run_output(
|
||||
state["answer"], self._output_guardrail_context(state)
|
||||
state["answer"], state.get("context", {})
|
||||
)
|
||||
for _decision in decisions:
|
||||
await self.guardrail_telemetry.evaluated("output", _decision)
|
||||
|
||||
Reference in New Issue
Block a user