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:
@@ -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,
|
||||
|
||||
@@ -95,7 +95,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,
|
||||
|
||||
@@ -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.
@@ -159,7 +159,7 @@ class AgentWorkflow:
|
||||
builder.add_conditional_edges(
|
||||
"input_guardrails",
|
||||
self._after_input_guardrails,
|
||||
{"blocked": "persist", "continue": "routing_decision"},
|
||||
{"blocked": "output_guardrails", "continue": "routing_decision"},
|
||||
)
|
||||
builder.add_conditional_edges(
|
||||
"routing_decision",
|
||||
@@ -195,6 +195,31 @@ 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):
|
||||
# Keep the technical guardrail reason in telemetry, but expose only a
|
||||
# safe, actionable message to the end user. The message is intentionally
|
||||
# routed through output_guardrails before persistence/delivery.
|
||||
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(
|
||||
@@ -279,12 +304,33 @@ class AgentWorkflow:
|
||||
component="workflow.input_guardrails.final",
|
||||
)
|
||||
if any(not d.allowed for d in decisions):
|
||||
# A blocking input guardrail stops the turn before routing/tools.
|
||||
# Clear turn-local routing/tool state so stale data from a prior
|
||||
# turn cannot appear as if it was executed after the block.
|
||||
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 {
|
||||
@@ -492,119 +538,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.
|
||||
|
||||
@@ -620,15 +553,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"),
|
||||
@@ -714,7 +647,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)
|
||||
|
||||
@@ -7,6 +7,35 @@ router:
|
||||
confidence_threshold: 0.65
|
||||
allow_handoff: true
|
||||
|
||||
transaction_confirmation:
|
||||
# Explicit yes/no stays deterministic. Only inconclusive replies use this LLM fallback.
|
||||
semantic_fallback:
|
||||
enabled: true
|
||||
allowed_values: [SIM, NAO, CONTINUAR]
|
||||
confirm_values: [SIM]
|
||||
reject_values: [NAO]
|
||||
continue_values: [CONTINUAR]
|
||||
include_relevant_context: true
|
||||
profile_name: router
|
||||
prompt: |
|
||||
Você classifica a resposta do cliente a uma confirmação transacional pendente.
|
||||
Considere a pergunta pendente, somente o histórico recente relacionado ao mesmo tema e a fala atual.
|
||||
Não execute a ação e não invente fatos.
|
||||
|
||||
Classes permitidas: {{ allowed_values }}
|
||||
- SIM: confirmação/aceite inequívoco, inclusive equivalentes como "isso mesmo", "pode confirmar", "é isso" quando o contexto tornar o aceite claro.
|
||||
- NAO: recusa/cancelamento inequívoco da ação pendente.
|
||||
- CONTINUAR: qualquer resposta que não confirme nem rejeite inequivocamente, incluindo pergunta adicional, correção, novo dado, ambiguidade ou possível mudança de assunto.
|
||||
|
||||
Pergunta pendente:
|
||||
{{ pending_prompt }}
|
||||
|
||||
Histórico relevante:
|
||||
{{ relevant_conversation_context }}
|
||||
|
||||
Resposta atual do cliente:
|
||||
{{ user_input }}
|
||||
|
||||
state_policies:
|
||||
- state: WAITING_BILLING_CONFIRMATION
|
||||
agent: billing_agent
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Confirmação Transacional Semântica
|
||||
|
||||
Este template suporta confirmação transacional em duas camadas: primeiro um parser determinístico para `sim`/`não` e equivalentes explícitos; somente quando ele não consegue decidir, o framework usa um classificador semântico configurado em `config/routing.yaml`.
|
||||
|
||||
A configuração `router.transaction_confirmation.semantic_fallback` usa três classes: `SIM`, `NAO` e `CONTINUAR`. O prompt pode usar `{{ pending_prompt }}`, `{{ relevant_conversation_context }}`, `{{ user_input }}` e `{{ allowed_values }}`. O histórico injetado é apenas contexto de interpretação; não substitui validação de negócio ou evidência MCP.
|
||||
|
||||
Exemplo: após `Você confirma o cancelamento do serviço Tamboro Mensal?`, a frase `isso mesmo, pode confirmar` pode ser classificada como `SIM`. Já `mas qual é o valor?` deve ser `CONTINUAR`, portanto não executa a ação por confirmação.
|
||||
|
||||
Entradas explícitas já suportadas continuam no caminho determinístico e não geram custo adicional de LLM. Em observabilidade, o fallback usa `transaction.confirmation.semantic_classifier` e o `route_decision.metadata` informa `transaction_confirmation_source: semantic`.
|
||||
|
||||
Consulte `docs/developer/pt/03_transaction_workflows_and_state.md` do framework para o contrato completo e exemplos.
|
||||
Reference in New Issue
Block a user