new feature: Multi-Intent

This commit is contained in:
2026-09-10 22:55:50 -03:00
parent 05410adf09
commit 3f884d028a
198 changed files with 566 additions and 45 deletions

View File

@@ -1,3 +1,9 @@
import json
from types import SimpleNamespace
import pytest
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.models import IntentDefinition
from agent_framework.routing.multi_intent import MultiIntentPlanner
@@ -64,3 +70,114 @@ def test_single_word_keyword_does_not_match_a_longer_word_prefix():
assert plan is not None
assert all(item.intent != "cancel" for item in plan.operations)
assert [item.intent for item in plan.operations] == ["invoice", "off_context"]
def test_semantic_fallback_hydrates_ownership_from_configured_intents():
plan = _planner().plan_from_semantic({
"confidence": 0.92,
"operations": [
{"intent": "invoice", "source_text": "me encaminhe o boleto"},
{"intent": "cancel", "source_text": "não quero mais o pedido"},
],
})
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
assert plan.operations[0].agent == "orders"
assert plan.operations[0].tools == ["cancelar_pedido"]
def test_semantic_fallback_rejects_invented_intent_and_low_confidence():
assert _planner().plan_from_semantic({
"confidence": 0.99,
"operations": [
{"intent": "cancel", "source_text": "não quero mais o pedido"},
{"intent": "invented", "source_text": "faça outra coisa"},
],
}) is None
assert _planner().plan_from_semantic({
"confidence": 0.2,
"operations": [
{"intent": "cancel", "source_text": "não quero mais o pedido"},
{"intent": "invoice", "source_text": "me encaminhe o boleto"},
],
}) is None
def test_semantic_confirmation_preserves_primary_transaction():
plan = _planner().plan_confirmation_from_semantic(
{
"confidence": 0.9,
"operations": [
{"intent": "invoice", "source_text": "também gostaria do boleto"},
],
},
primary_intent="cancel",
primary_agent="orders",
)
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
@pytest.mark.asyncio
async def test_router_uses_semantic_multi_intent_only_after_deterministic_miss(tmp_path):
routing = tmp_path / "routing.yaml"
routing.write_text("""
router:
fallback_agent: billing
intents:
- name: cancel
agent: orders
description: Encerrar uma encomenda existente
keywords: [cancelar pedido]
mcp_tools: [cancelar_pedido]
- name: invoice
agent: billing
description: Obter documento de cobrança ou boleto
keywords: [segunda via]
mcp_tools: [consultar_fatura]
""", encoding="utf-8")
policies = tmp_path / "tool_policies.yaml"
policies.write_text("""
defaults:
operation_type: read_only
tool_policies:
cancelar_pedido:
operation_type: transactional
require_confirmation: true
""", encoding="utf-8")
class SemanticMultiIntentLLM:
calls = 0
async def ainvoke(self, messages, **kwargs):
self.calls += 1
assert kwargs["generation_name"] == "llm.router.multi_intent"
return json.dumps({
"confidence": 0.94,
"operations": [
{"intent": "cancel", "source_text": "encerrar minha encomenda"},
{"intent": "invoice", "source_text": "encaminhe o boleto"},
],
})
llm = SemanticMultiIntentLLM()
router = EnterpriseRouter(SimpleNamespace(
ROUTING_CONFIG_PATH=str(routing),
TOOL_POLICIES_PATH=str(policies),
ENABLE_LLM_ROUTER=True,
ENABLE_ROUTE_STICKINESS=False,
), llm=llm)
decision = await router.route({
"user_text": "quero encerrar minha encomenda e encaminhe o boleto",
"context": {},
})
assert llm.calls == 1
assert decision.method == "llm"
assert decision.intent == "cancel"
assert decision.agent == "orders"
assert decision.metadata["multi_intent_classifier"] == "llm"
assert [
item["intent"]
for item in decision.metadata["multi_intent_plan"]["operations"]
] == ["cancel", "invoice"]