new feature: Multi-Intent

This commit is contained in:
2026-09-10 16:12:04 -03:00
parent d535ec2a16
commit a5f24dac3f
223 changed files with 1121 additions and 21 deletions

View File

@@ -0,0 +1,66 @@
from agent_framework.routing.models import IntentDefinition
from agent_framework.routing.multi_intent import MultiIntentPlanner
def _planner():
return MultiIntentPlanner([
IntentDefinition(name="cancel", agent="orders", priority=10, keywords=["cancelar pedido"], mcp_tools=["cancelar_pedido"]),
IntentDefinition(name="invoice", agent="billing", priority=20, keywords=["segunda via"], mcp_tools=["consultar_fatura"]),
], {
"enabled": True,
}, transactional_tools={"cancelar_pedido"})
def test_builds_validated_plan_and_keeps_primary_first():
plan = _planner().plan("quero cancelar pedido e preciso da segunda via")
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
assert plan.operations[1].disposition == "execute"
assert plan.operations[1].tools == ["consultar_fatura"]
def test_confirmation_with_secondary_does_not_lose_confirmation():
plan = _planner().plan_confirmation_secondary(
"sim e preciso da segunda via", primary_intent="cancel", primary_agent="orders"
)
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
assert MultiIntentPlanner.public_messages(plan) == []
def test_transactional_intent_is_primary_even_when_mentioned_second():
plan = _planner().plan("preciso da segunda via e quero cancelar pedido")
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
def test_does_not_plan_single_intent_or_invent_unknown_intent():
assert _planner().plan("quero cancelar pedido") is None
assert _planner().plan("quero cancelar pedido e saber o clima") is None
def test_known_primary_with_actionable_unknown_creates_off_context_topic():
plan = _planner().plan("quero cancelar pedido e alterar meu CNPJ")
assert plan is not None
assert plan.operations[1].intent == "off_context"
assert plan.operations[1].disposition == "unsupported"
def test_confirmation_with_unknown_secondary_keeps_confirmation():
plan = _planner().plan_confirmation_secondary(
"sim e altere meu CNPJ", primary_intent="cancel", primary_agent="orders"
)
assert plan is not None
assert plan.operations[0].intent == "cancel"
assert plan.operations[1].intent == "off_context"
def test_single_word_keyword_does_not_match_a_longer_word_prefix():
planner = MultiIntentPlanner([
IntentDefinition(name="cancel", agent="orders", priority=10, keywords=["cancela"]),
IntentDefinition(name="invoice", agent="billing", priority=20, keywords=["segunda via"]),
])
plan = planner.plan("sim, pode cancelar e preciso da segunda via")
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"]

View File

@@ -0,0 +1,53 @@
import pytest
from agent_framework.routing.pending_topics import drain_pending_topics
@pytest.mark.asyncio
async def test_secondary_topic_does_not_inherit_primary_transaction_state():
captured = {}
async def billing_agent(state):
captured.update(state)
return {"answer": "Aqui está a sua segunda via."}
state = {
"answer": "Você confirma o cancelamento do Tamboro?",
"sanitized_input": "quero cancelar Tamboro e quero minha segunda via",
"confirmation_required": True,
"active_transaction": {"tool_name": "cancelar_vas_avulso"},
"transaction_status": "AWAITING_CONFIRMATION",
"selected_tool_call": {"tool_name": "cancelar_vas_avulso"},
"pending_tool_call": {"tool_name": "cancelar_vas_avulso"},
"pending_domain_workflow": {"workflow_name": "cancelamento_vas_avulso"},
"missing_parameters": ["subject"],
"next_state": "WAITING_CONTESTACAO_CONFIRMATION",
"mcp_results": [{"tool_name": "consultar_vas"}],
"pending_topics": [{
"operation_id": "op-2",
"intent": "contas_invoice_query",
"agent": "faturas_agent",
"domain": "telecom_contas",
"tools": ["consultar_faturas"],
"source_text": "quero minha segunda via",
"disposition": "execute",
"status": "pending",
}],
}
result = await drain_pending_topics(
state, state["answer"], {"faturas_agent": billing_agent}
)
assert captured["active_transaction"] is None
assert captured["selected_tool_call"] == {}
assert captured["pending_tool_call"] == {}
assert captured["pending_domain_workflow"] is None
assert captured["next_state"] is None
assert captured["sanitized_input"] == "quero minha segunda via"
assert captured["mcp_results"] == []
assert captured["mcp_tools"] == ["consultar_faturas"]
assert captured["available_mcp_tools"] == ["consultar_faturas"]
assert captured["domain"] == "telecom_contas"
assert result.answer.startswith("Aqui está a sua segunda via.")
assert result.pending_topics == []

View File

@@ -0,0 +1,39 @@
from pathlib import Path
import re
from urllib.parse import unquote
ROOT = Path(__file__).resolve().parents[2]
def _local_links(path: Path) -> list[str]:
text = path.read_text(encoding="utf-8")
return [
target for target in re.findall(r"\[[^]]*\]\(([^)]+)\)", text)
if not target.startswith(("http://", "https://", "#", "mailto:"))
]
def test_main_readmes_have_no_broken_local_links():
for name in ("README.md", "README_en.md"):
readme = ROOT / name
for target in _local_links(readme):
relative = unquote(target.split("#", 1)[0])
assert not relative or (readme.parent / relative).exists(), (name, target)
def test_every_feature_guide_is_listed_in_corresponding_main_readme():
pairs = (("pt-BR", "README.md"), ("en", "README_en.md"))
for language, readme_name in pairs:
readme = (ROOT / readme_name).read_text(encoding="utf-8")
guides = sorted((ROOT / "docs" / "features" / language).glob("[0-9][0-9]_*.md"))
assert guides
for guide in guides:
expected = f"docs/features/{language}/{guide.name}"
assert expected in readme, (readme_name, expected)
def test_portuguese_and_english_feature_catalogs_are_symmetric():
pt = {path.name for path in (ROOT / "docs" / "features" / "pt-BR").glob("*.md")}
en = {path.name for path in (ROOT / "docs" / "features" / "en").glob("*.md")}
assert pt == en

View File

@@ -0,0 +1,38 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_all_tuning_backend_graphs_support_pending_topics():
graphs = sorted((ROOT / "Tuning-Performance").glob("**/app/workflows/agent_graph.py"))
assert graphs
for path in graphs:
source = path.read_text(encoding="utf-8")
assert "from agent_framework.routing.pending_topics import drain_pending_topics" in source, path
assert '"pending_topics": drained.pending_topics' in source, path
assert '"handled_topics": drained.handled_topics' in source, path
def test_all_tuning_backend_states_expose_pending_topics():
states = sorted((ROOT / "Tuning-Performance").glob("**/app/state.py"))
assert states
for path in states:
source = path.read_text(encoding="utf-8")
assert "pending_topics: list[dict[str, Any]]" in source, path
assert "handled_topics: list[dict[str, Any]]" in source, path
def test_canonical_templates_delegate_drain_to_framework_helper():
for template in ("agent_template_backend", "agent_template_backend_day_zero"):
graph = ROOT / "templates" / template / "app" / "workflows" / "agent_graph.py"
state = ROOT / "templates" / template / "app" / "state.py"
graph_source = graph.read_text(encoding="utf-8")
state_source = state.read_text(encoding="utf-8")
assert "from agent_framework.routing.pending_topics import drain_pending_topics" in graph_source
assert "drained = await drain_pending_topics(" in graph_source
assert '"pending_topics": drained.pending_topics' in graph_source
assert '"handled_topics": drained.handled_topics' in graph_source
assert "MultiIntentPlanner.public_messages" not in graph_source
assert "pending_topics: list[dict[str, Any]]" in state_source
assert "handled_topics: list[dict[str, Any]]" in state_source