new feature: Multi-Intent

This commit is contained in:
2026-09-10 16:12:08 -03:00
parent 2a563840c1
commit 82d96f2dbf
302 changed files with 2070 additions and 28 deletions

View File

@@ -0,0 +1,75 @@
# Implementação Multi-Intent
## Entregas
- Planejador determinístico no framework, limitado às intents, agents e tools
cadastrados no `routing.yaml`.
- Priorização automática pela natureza das tools em `tool_policies.yaml`, sem
política por combinação.
- Persistência padrão em `pending_topics` e histórico em `handled_topics`.
- Tratamento de confirmação composta (`sim e ...`) sem provocar troca de intent
durante uma transação aberta.
- Composição das mensagens controladas das operações secundárias antes dos
guardrails de saída.
- Tratamento do cancelamento de Tamboro combinado com segunda via ou assunto
realmente fora do escopo.
- Desambiguação de cobranças repetidas por seleção relativa, incluindo
`as duas mais caras`, alimentando o batch já suportado pelo workflow de
cancelamento.
- Guias em português e inglês em `agent_framework_oci/docs/features`.
- Integração propagada às dez variantes de backend existentes em
`agent_framework_oci/Tuning-Performance`, sem criar um exemplo artificial.
- Script idempotente `agent_framework_oci/scripts/sync_pending_topics_to_tuning.py`
para manter as cópias sincronizadas.
## Testes adicionados
- `agent_framework_oci/tests/unit/test_multi_intent_planner.py`
- `tests/migration/test_multi_intent_contas.py`
- `agent_framework_oci/tests/unit/test_tuning_pending_topics_sync.py`
## Evidências recuperadas do Contas antigo
O documento antigo `docs/PENDING_TOPICS_CAMADA.md` descrevia explicitamente:
- cancelamento simples;
- cancelamento + pergunta conceitual sobre outro serviço;
- cancelamento + solicitação fora do contexto, como alteração de CNPJ;
- drenagem de tópicos pendentes antes da confirmação ou finalização;
- manutenção do tópico para retry quando não fosse produzida resposta útil.
Esses comportamentos foram usados como referência. A segunda via não é tratada
como fora de escopo quando a versão atual do Contas possui uma intent/tool para
ela; nesse caso ela é executada como consulta secundária. Somente uma solicitação
sem capability registrada vira `off_context`.
## Resultado da validação
O recorte de 23 testes do planejador e das integrações transacionais afetadas
passa integralmente. A execução ampla de `agent_framework_oci/tests/unit` e
`tests/migration` resultou em 946 testes aprovados e 33 falhas já presentes em
áreas fora desta alteração (principalmente checkpoints, runtime legado de
workflows, guardrails e paridade histórica).
## Limite de execução
Consultas secundárias conhecidas são executadas e consolidadas. Assuntos sem
capability são tratados como `off_context`. Uma segunda ação mutável permanece
pendente e requer confirmação própria ou um workflow transacional coordenador.
## Correção de isolamento da operação secundária
O teste real `cancelar Tamboro + segunda via` revelou que o agent secundário
herdava o snapshot operacional da transação primária. Isso fazia o
`FaturasAgent` reutilizar `selected_tool_call=cancelar_vas_avulso` e repetir a
confirmação do cancelamento. O estado filho agora limpa integralmente transação,
tool selecionada/pendente, workflow, parâmetros ausentes, resultados MCP,
`next_state`, `workflow_id` e usa a cláusula secundária também como
`sanitized_input`. O teste `test_pending_topics_isolation.py` protege essa regra.
Uma segunda execução real mostrou outro campo de roteamento que precisava ser
isolado: `mcp_tools` permanecia com a allowlist da operação primária. O runtime
usa esse campo de primeiro nível como fonte de verdade, portanto o
`FaturasAgent` ainda selecionava o cancelamento. O estado filho agora sobrescreve
`domain`, `mcp_tools` e `available_mcp_tools` exclusivamente com a capability do
tópico secundário.

View File

@@ -185,6 +185,7 @@ O framework deve concentrar os motores reutilizáveis:
- Telemetria Langfuse/OpenTelemetry.
- Analytics IC/NOC/GRL.
- MCP Tool Router.
- Planejamento multi-intent e fila persistente de `pending_topics`.
- Cache.
- RAG genérico.
@@ -230,6 +231,8 @@ Input Guardrails
↓
Enterprise Router ou Supervisor
↓
Planejamento multi-intent / pending_topics
↓
Agente especializado
↓
MCP Tool Router / RAG / Cache / LLM
@@ -11486,6 +11489,8 @@ O conteúdo desta pasta deve ser tratado como uma extensão adicional do framewo
| Um exemplo antigo não compila | documentação histórica versus API atual | [Validação README x Código](docs/developer/pt/VALIDATION_README_ALIGNMENT.md) |
| Preciso criar um agente novo do zero | fluxo completo | [`README.md`](README.md) |
| Preciso saber onde colocar uma nova feature | arquitetura e boundaries | [Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md) |
| Duas solicitações na mesma mensagem acionam apenas uma intent | decomposição multi-intent, `pending_topics`, allowlist de tools | [Planejamento Multi-Intent](docs/features/pt-BR/16_multi_intent_planning.md) |
| O agent secundário repete a transação ou confirmação primária | isolamento de `active_transaction`, `mcp_tools` e estado operacional | [Planejamento Multi-Intent](docs/features/pt-BR/16_multi_intent_planning.md) |
### 34. Funcionalidades Avançadas
@@ -11561,9 +11566,31 @@ O conteúdo desta pasta deve ser tratado como uma extensão adicional do framewo
**Use quando:** um `COER`/guardrail de entrada gera mensagem genérica, `route=blocked` carrega resultados antigos ou há dúvida sobre a precedência entre input guardrails, routing e tools.
### Guias de features implementadas
Os manuais `01` a `12` acima organizam os assuntos por área de desenvolvimento. Os guias abaixo documentam cada feature executável individualmente:
1. [Autenticação](docs/features/pt-BR/01_authentication.md)
2. [Workflow Transacional Determinístico](docs/features/pt-BR/02_deterministic_transactional_workflow.md)
3. [Composição por LLM Solicitada pelo Domínio](docs/features/pt-BR/03_domain_requested_llm_composition.md)
4. [RAG Solicitado pelo Domínio](docs/features/pt-BR/04_domain_requested_rag.md)
5. [Memória de Longo Prazo](docs/features/pt-BR/05_long_term_memory.md)
6. [Regressão Offline de Workflow](docs/features/pt-BR/06_offline_workflow_regression.md)
7. [Pause e Resume de Workflow](docs/features/pt-BR/07_pause_resume_workflow.md)
8. [Route Stickiness](docs/features/pt-BR/08_route_stickiness.md)
9. [Replay em Interrupções de Voz](docs/features/pt-BR/09_voice_interruption_replay.md)
10. [Recuperação de Erro em Workflow](docs/features/pt-BR/10_workflow_error_recovery.md)
11. [Clarificação](docs/features/pt-BR/11_clarification.md)
12. [Idempotência Durável](docs/features/pt-BR/12_durable_idempotency.md)
13. [Estados Transacionais Dinâmicos](docs/features/pt-BR/13_dynamic_transaction_states.md)
14. [Replay Após Finalização](docs/features/pt-BR/14_post_finalization_replay.md)
15. [Guardrails de Retrieval e Tools](docs/features/pt-BR/15_retrieval_tool_guardrails.md)
16. [Planejamento Multi-Intent e Pending Topics](docs/features/pt-BR/16_multi_intent_planning.md)
O [índice de features em português](docs/features/pt-BR/README.md) deve ser atualizado sempre que um novo guia for adicionado.
### Tutorial principal
[`README.md`](README.md) continua sendo a referência para o passo a passo completo:
`arquitetura → configuração → criação do agente → registro → estado → routing → tools → MCP → identidade → execução → testes → gateways → memória → RAG`.

View File

@@ -126,7 +126,7 @@ Details the evaluation and certification framework, including evaluation archite
Defines the platform operational readiness and SRE model, including managed components, health checks, readiness, SLOs, metrics, dashboards, alerts, runbooks, incident management, capacity planning, and production checklists.
### [IMPORTANT: Disclaimer Auth and Security](specs/Disclaimer%20Auth%20and%20Security_EN.md)
### [IMPORTANT: Disclaimer Auth and Security](specs/Disclaimer_Auth_and_Security_EN.md)
Oracle Cloud Infrastructure Security Best Practice Recommendations.
@@ -186,6 +186,7 @@ The framework should concentrate the reusable engines:
- Langfuse/OpenTelemetry Telemetry.
- Analytics IC/NOC/GRL.
- MCP Tool Router.
- Multi-intent planning and the persistent `pending_topics` queue.
- Cache.
- Generic RAG.
@@ -231,6 +232,8 @@ Input Guardrails
↓
Enterprise Router or Supervisor
↓
Multi-intent planning / pending_topics
↓
Specialized agent
↓
MCP Tool Router / RAG / Cache / LLM
@@ -11388,6 +11391,8 @@ The content of this folder should be treated as an additional framework extensio
| Logs/traces do not correlate the same agent | labels, IDs, observability mapping | [Observability](docs/developer/en/11_observability_persistence_and_operational_readiness.md) |
| Historical example no longer compiles | stale docs vs current API | [README Alignment Validation](docs/developer/en/VALIDATION_README_ALIGNMENT.md) |
| Need to create a new agent from scratch | complete flow | [`README_en.md`](README_en.md) |
| Two requests in one message trigger only one intent | multi-intent decomposition, `pending_topics`, tool allowlist | [Multi-Intent Planning](docs/features/en/16_multi_intent_planning.md) |
| A secondary agent repeats the primary transaction or confirmation | isolation of `active_transaction`, `mcp_tools`, and operational state | [Multi-Intent Planning](docs/features/en/16_multi_intent_planning.md) |
### 34. Advanced Features
@@ -11463,6 +11468,29 @@ The content of this folder should be treated as an additional framework extensio
**Use it when:** `COER`/input guardrails generate generic messages, `route=blocked` carries stale results, or you need to reason about precedence between input guardrails, routing, and tools.
### Implemented feature guides
The `01` through `12` manuals above organize documentation by development area. The guides below document each executable feature individually:
1. [Authentication](docs/features/en/01_authentication.md)
2. [Deterministic Transactional Workflow](docs/features/en/02_deterministic_transactional_workflow.md)
3. [Domain-Requested LLM Composition](docs/features/en/03_domain_requested_llm_composition.md)
4. [Domain-Requested RAG](docs/features/en/04_domain_requested_rag.md)
5. [Long-Term Memory](docs/features/en/05_long_term_memory.md)
6. [Offline Workflow Regression](docs/features/en/06_offline_workflow_regression.md)
7. [Pause and Resume Workflow](docs/features/en/07_pause_resume_workflow.md)
8. [Route Stickiness](docs/features/en/08_route_stickiness.md)
9. [Voice Interruption Replay](docs/features/en/09_voice_interruption_replay.md)
10. [Workflow Error Recovery](docs/features/en/10_workflow_error_recovery.md)
11. [Clarification](docs/features/en/11_clarification.md)
12. [Durable Idempotency](docs/features/en/12_durable_idempotency.md)
13. [Dynamic Transaction States](docs/features/en/13_dynamic_transaction_states.md)
14. [Post-Finalization Replay](docs/features/en/14_post_finalization_replay.md)
15. [Retrieval and Tool Guardrails](docs/features/en/15_retrieval_tool_guardrails.md)
16. [Multi-Intent Planning and Pending Topics](docs/features/en/16_multi_intent_planning.md)
The [English feature index](docs/features/en/README.md) must be updated whenever a new feature guide is added.
### Main tutorial
[`README_en.md`](README_en.md) remains the complete step-by-step guide.

View File

@@ -35,6 +35,8 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -409,6 +410,10 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else []
),
}
async def billing_agent(self, state):
@@ -547,14 +552,28 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
@@ -623,6 +642,8 @@ class AgentWorkflow:
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}

View File

@@ -35,6 +35,8 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -409,6 +410,10 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else []
),
}
async def billing_agent(self, state):
@@ -547,14 +552,28 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
@@ -623,6 +642,8 @@ class AgentWorkflow:
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}

View File

@@ -35,6 +35,8 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -407,6 +408,10 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else []
),
}
async def billing_agent(self, state):
@@ -545,14 +550,28 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
@@ -621,6 +640,8 @@ class AgentWorkflow:
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}

View File

@@ -35,6 +35,8 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -409,6 +410,10 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else []
),
}
async def billing_agent(self, state):
@@ -547,14 +552,28 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
@@ -623,6 +642,8 @@ class AgentWorkflow:
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}

View File

@@ -35,6 +35,8 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -407,6 +408,10 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else []
),
}
async def billing_agent(self, state):
@@ -545,14 +550,28 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
@@ -621,6 +640,8 @@ class AgentWorkflow:
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}

View File

@@ -4,7 +4,7 @@ Este template suporta confirmação transacional em duas camadas: primeiro um pa
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.
Exemplo: após `Você confirma o cancelamento do serviço adicional selecionado?`, 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`.

View File

@@ -35,6 +35,8 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -407,6 +408,10 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else []
),
}
async def billing_agent(self, state):
@@ -545,14 +550,28 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
@@ -621,6 +640,8 @@ class AgentWorkflow:
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}

View File

@@ -4,7 +4,7 @@ Este template suporta confirmação transacional em duas camadas: primeiro um pa
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.
Exemplo: após `Você confirma o cancelamento do serviço adicional selecionado?`, 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`.

View File

@@ -35,6 +35,8 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -407,6 +408,10 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else []
),
}
async def billing_agent(self, state):
@@ -545,14 +550,28 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
@@ -621,6 +640,8 @@ class AgentWorkflow:
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}

View File

@@ -35,6 +35,8 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -407,6 +408,10 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else []
),
}
async def billing_agent(self, state):
@@ -545,14 +550,28 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
@@ -621,6 +640,8 @@ class AgentWorkflow:
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}

View File

@@ -38,6 +38,8 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -409,6 +410,10 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else []
),
}
async def billing_agent(self, state):
@@ -547,14 +552,28 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
@@ -623,6 +642,8 @@ class AgentWorkflow:
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}

View File

@@ -39,6 +39,8 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -409,6 +410,10 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else []
),
}
async def billing_agent(self, state):
@@ -547,14 +552,28 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
@@ -623,6 +642,8 @@ class AgentWorkflow:
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}

View File

@@ -0,0 +1,27 @@
# Multi-Intent Planning
`MultiIntentPlanner` recognizes independent requests in one utterance and
builds a plan constrained to the intents, agents, and tools declared in
`config/routing.yaml`. It does not execute operations or let the LLM invent a
sequence: the router selects the primary operation and the domain workflow
continues to own the transaction.
## Configuration and execution
No additional configuration is required. The planner uses the intents already
declared in `routing.yaml`, selects the single transactional operation from
`tool_policies.yaml`, and persists the remaining requests as `pending_topics`.
Known read-only topics execute through their own agent. A second mutation stays
pending for its own confirmation, while an unknown clause becomes off-context.
The router resolves clauses from registered intent keywords and routes the
transactional operation first. The graph drains read-only pending topics before
output supervision. During an open transaction,
`yes, and send me the invoice` confirms the transaction and records the
secondary request instead of causing an intent shift.
For multiple side-effecting actions, implement a transactional coordinator
that persists the plan, confirms it, executes idempotently, and records partial
failure or compensation. See `tests/unit/test_multi_intent_planner.py`. No
special Tuning-Performance template is needed because the behavior is native.

View File

@@ -17,3 +17,4 @@ Documentation for the main `agent_framework_oci` features.
- [Dynamic Transaction States](13_dynamic_transaction_states.md)
- [Post Finalization Replay](14_post_finalization_replay.md)
- [Retrieval / Tool Guardrails](15_retrieval_tool_guardrails.md)
- [Multi-Intent Planning](16_multi_intent_planning.md)

View File

@@ -0,0 +1,46 @@
# Planejamento Multi-Intent
O `MultiIntentPlanner` reconhece solicitações independentes na mesma mensagem e
cria um plano limitado às intents, agents e tools declarados em
`config/routing.yaml`. O planejador não executa operações nem permite que o LLM
invente uma sequência: o roteador escolhe a operação primária e o workflow do
domínio continua responsável pela transação.
## Configuração
Não existe configuração adicional. O planejador usa as intents do `routing.yaml`
e lê `tool_policies.yaml` para priorizar automaticamente a única operação
transacional. Pedidos secundários são persistidos em `pending_topics`.
- intent conhecida e read-only: executada pelo agent correspondente;
- intent conhecida e transacional: mantida pendente para confirmação própria;
- trecho sem capability correspondente: respondido como `off_context`.
Consultas secundárias com `execute` podem ser executadas pelo agent correspondente
e consolidadas. Uma segunda mutação é marcada como `defer`, pois exige um
workflow transacional coordenador.
## Fluxo de execução
1. O `EnterpriseRouter` separa a entrada em cláusulas e resolve cada uma pelas
keywords das intents cadastradas.
2. `tool_policies.yaml` identifica a operação transacional e a torna primária.
3. A primeira operação é roteada normalmente para seu agent/workflow.
4. O `agent_graph` compõe as mensagens públicas antes do supervisor de saída.
5. Durante uma confirmação, uma frase como `sim e me manda a segunda via`
confirma a transação aberta e registra a solicitação secundária, sem causar
troca indevida de intent.
## Operações mutáveis
Para executar mais de uma ação com efeito colateral, implemente um workflow
transacional coordenador. Ele deve persistir o plano, pedir confirmação quando
necessário, executar cada passo com idempotência e registrar sucesso parcial,
falha e compensação. O planejador fornece o contrato; a política de negócio e a
orquestração permanecem no domínio.
## Referência executável
Veja os testes genéricos em `tests/unit/test_multi_intent_planner.py`. Como a
feature é padrão do runtime, não existe uma cópia especial do
`agent_template_backend` em `Tuning-Performance`.

View File

@@ -17,3 +17,4 @@ Documentação das principais features do `agent_framework_oci`.
- [Estados Transacionais Dinâmicos](13_dynamic_transaction_states.md)
- [Replay Após Finalização](14_post_finalization_replay.md)
- [Guardrails de Retrieval e Tools](15_retrieval_tool_guardrails.md)
- [Planejamento Multi-Intent](16_multi_intent_planning.md)

View File

@@ -201,6 +201,8 @@ src/agent_framework/routing/config_loader.py
src/agent_framework/routing/continuity.py
src/agent_framework/routing/enterprise_router.py
src/agent_framework/routing/models.py
src/agent_framework/routing/multi_intent.py
src/agent_framework/routing/pending_topics.py
src/agent_framework/runtime/__init__.py
src/agent_framework/runtime/agent_runtime.py
src/agent_framework/runtime/transaction_input.py

Some files were not shown because too many files have changed in this diff Show More