adjustments: route stickness usefull tokens only

This commit is contained in:
2026-08-20 13:14:57 -03:00
parent d271031198
commit 6e43cc0158
49 changed files with 185 additions and 211 deletions

View File

@@ -0,0 +1,47 @@
# Release Notes — Generic Deterministic Intent Shift v15
## Problema corrigido
A Route Stickiness podia preservar a intent anterior quando a nova mensagem correspondia a uma intent configurada no `routing.yaml`, mas a frase do usuário omitia conectores curtos presentes na keyword configurada.
Exemplo real de configuração:
- keyword: `qual é o meu plano`
- mensagem: `qual o meu plano`
A classificação determinística não reconhecia a nova intent e a continuity acabava mantendo a intent anterior.
## Correção
O `EnterpriseRouter` continua usando, nesta ordem:
1. match exato;
2. sequência completa de tokens com palavras inseridas (`ordered_tokens`);
3. sequência de tokens informativos tolerando a omissão de conectores curtos presentes na keyword (`ordered_content_tokens`).
A terceira estratégia ignora, somente no lado da keyword, tokens de até dois caracteres e exige pelo menos dois tokens informativos. Não há nomes de intents, agentes, domínios ou verbos de negócio hardcoded.
Assim, a solução é dirigida integralmente pelas intents carregadas do `routing.yaml` da aplicação.
## Precedência sobre Route Stickiness
Quando o candidato determinístico encontrado é diferente da intent ativa, ele preempta a stickiness e retorna:
- `route_stickiness_preempted: true`
- `previous_agent`
- `previous_intent`
- `keyword_match_strategy`
A continuity LLM não é chamada nesse caminho.
## Casos cobertos
### Mesmo agente, nova intent
`retail_order_tracking` -> `quero cancelar meu pedido` -> `retail_order_cancel`
### Mesmo agente, tools diferentes
`contas_invoice_query` -> `qual o meu plano` -> `contas_plan_information`
Mesmo que ambas as intents usem `faturas_agent`, as tools mudam de `consultar_faturas` para `consultar_plano`.

View File

@@ -181,6 +181,45 @@ class EnterpriseRouter:
pos = found pos = found
return True return True
@classmethod
def _ordered_content_keyword_match(cls, keyword: str, text: str, *, max_gap: int = 4) -> bool:
"""Match determinístico tolerante à omissão de conectores curtos.
Alguns ``routing.yaml`` usam frases naturais como ``qual é o meu plano``.
A mesma intenção pode chegar como ``qual o meu plano``. O matcher legado
falhava porque exigia também o token ``e`` (resultado da normalização de
``é``). Aqui tokens de até dois caracteres são tratados como conectores
opcionais *apenas no lado da keyword*. Os tokens informativos continuam
obrigatórios, em ordem e próximos entre si.
A heurística é propositalmente linguística-neutra e não contém nomes de
intents, agentes, domínios ou listas de verbos de negócio. Assim funciona
com qualquer configuração carregada pelo ``routing.yaml`` sem LLM extra.
"""
wanted_all = cls._keyword_tokens(keyword)
actual = cls._keyword_tokens(text)
if len(wanted_all) < 2 or not actual:
return False
wanted = [token for token in wanted_all if len(token) > 2]
# Exigimos pelo menos dois tokens informativos para não transformar
# keywords curtas em matches amplos demais.
if len(wanted) < 2 or len(wanted) == len(wanted_all):
return False
pos = -1
for token in wanted:
found = None
upper = min(len(actual), pos + max_gap + 2)
for idx in range(pos + 1, upper):
if actual[idx] == token:
found = idx
break
if found is None:
return False
pos = found
return True
def _route_by_keyword(self, text: str) -> RouteDecision | None: def _route_by_keyword(self, text: str) -> RouteDecision | None:
normalized = text.casefold() normalized = text.casefold()
matches: list[tuple[int, int, int, IntentDefinition, str, str]] = [] matches: list[tuple[int, int, int, IntentDefinition, str, str]] = []
@@ -195,10 +234,17 @@ class EnterpriseRouter:
strategy = "exact" strategy = "exact"
elif self._ordered_keyword_match(kw, text): elif self._ordered_keyword_match(kw, text):
strategy = "ordered_tokens" strategy = "ordered_tokens"
elif self._ordered_content_keyword_match(kw, text):
strategy = "ordered_content_tokens"
if strategy: if strategy:
# menor priority vence; exact vence fuzzy; keyword maior desempata # menor priority vence; estratégias mais estritas vencem as relaxadas;
strategy_rank = 0 if strategy == "exact" else 1 # keyword maior desempata dentro da mesma prioridade/estratégia.
strategy_rank = {
"exact": 0,
"ordered_tokens": 1,
"ordered_content_tokens": 2,
}[strategy]
matches.append((intent.priority, strategy_rank, -len(kw), intent, kw, strategy)) matches.append((intent.priority, strategy_rank, -len(kw), intent, kw, strategy))
if not matches: if not matches:
return None return None
@@ -208,11 +254,19 @@ class EnterpriseRouter:
route=intent.agent, route=intent.agent,
agent=intent.agent, agent=intent.agent,
intent=intent.name, intent=intent.name,
confidence=0.85 if strategy == "exact" else 0.82, confidence={
"exact": 0.85,
"ordered_tokens": 0.82,
"ordered_content_tokens": 0.80,
}[strategy],
reason=( reason=(
f"Keyword '{kw}' correspondeu à intent '{intent.name}'." f"Keyword '{kw}' correspondeu à intent '{intent.name}'."
if strategy == "exact" if strategy == "exact"
else f"Sequência de tokens da keyword '{kw}' correspondeu à intent '{intent.name}'." else (
f"Sequência de tokens da keyword '{kw}' correspondeu à intent '{intent.name}'."
if strategy == "ordered_tokens"
else f"Tokens informativos da keyword '{kw}' corresponderam à intent '{intent.name}'."
)
), ),
method="keyword", method="keyword",
metadata={"matched_keyword": kw, "keyword_match_strategy": strategy}, metadata={"matched_keyword": kw, "keyword_match_strategy": strategy},

View File

@@ -1,207 +0,0 @@
###############################################################################
# AI AGENT PLATFORM - CONFIGURAÇÃO ÚNICA
# Este arquivo é lido por Pydantic Settings no framework e no backend template.
###############################################################################
APP_NAME=ai-agent-template
APP_ENV=local
LOG_LEVEL=INFO
API_HOST=0.0.0.0
API_PORT=8000
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
###############################################################################
# LLM - OCI Generative AI como provider principal
###############################################################################
# Opções: mock, oci_openai, oci_sdk, openai_compatible
LLM_PROVIDER=oci_sdk
LLM_TEMPERATURE=0.2
LLM_MAX_TOKENS=2048
LLM_TIMEOUT_SECONDS=120
# OCI OpenAI-compatible endpoint
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
OCI_GENAI_MODEL=openai.gpt-4.1
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
OCI_GENAI_PROJECT_OCID=
#OCI_GENAI_BASE_URL=https://pegruagntaiatenddev.pe.inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com
#OCI_GENAI_MODEL=openai.gpt-4.1
#OCI_GENAI_API_KEY=
#OCI_GENAI_PROJECT_OCID=
# OCI_AUTH_MODE=config_file|instance_principal|resource_principal
OCI_AUTH_MODE=config_file
# OCI SDK / signer / profiles
OCI_CONFIG_FILE=~/.oci/config
OCI_PROFILE=LATINOAMERICA-Chicago
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
OCI_REGION=us-chicago-1
###############################################################################
# Persistência
###############################################################################
# Opções: memory, autonomous, mongodb
SESSION_REPOSITORY_PROVIDER=autonomous
MEMORY_REPOSITORY_PROVIDER=autonomous
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
# Autonomous Database
ADB_USER=admin
ADB_PASSWORD=Moniquinha19721972
ADB_DSN=oradb23ai_high
ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
ADB_WALLET_PASSWORD=Moniquinha1972
ADB_TABLE_PREFIX=AGENTFW
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
MONGODB_DATABASE=agent_platform
# Redis
REDIS_URL=redis://localhost:6379/0
ENABLE_REDIS_CACHE=false
###############################################################################
# RAG / Vector / Graph
###############################################################################
VECTOR_STORE_PROVIDER=autonomous
GRAPH_STORE_PROVIDER=autonomous
RAG_TOP_K=5
EMBEDDING_PROVIDER=oci
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
###############################################################################
# Observabilidade
###############################################################################
ENABLE_LANGFUSE=true
# Opcional: verbose, compact
LANGFUSE_TRACE_MODE=compact
# Nome customizado do trace pai, ex.: backoffice.checklist.workflow ou backoffice.emulador.workflow
LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES=AGA.,NOC., IC.
LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion
LANGFUSE_IGNORE_HEALTHCHECKS=true
LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics
LANGFUSE_PUBLIC_KEY=pk-lf-4a1e3921-5158-4fd3-a16d-7a77549fb312
LANGFUSE_SECRET_KEY=sk-lf-efc6fd59-c5ec-4858-b6ec-4aa129734915
LANGFUSE_HOST=http://localhost:3005
ENABLE_OTEL=false
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_SERVICE_NAME=ai-agent-template
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false
###############################################################################
# Analytics / Observer corporativo
###############################################################################
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
ENABLE_ANALYTICS=false
# Providers aceitos: oci_streaming,pubsub,noop
ANALYTICS_PROVIDERS=oci_streaming
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
AGENT_PUBSUB_TOPIC=
GCP_PUBSUB_TOPIC_PATH=
GCP_PROJECT_ID=
GCP_PUBSUB_TOPIC=
GCP_PUBSUB_TIMEOUT_SECONDS=30
# Credencial GCP segue padrão Google:
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
###############################################################################
# OCI Streaming
###############################################################################
ENABLE_OCI_STREAMING=false
OCI_STREAM_ENDPOINT=
OCI_STREAM_OCID=
OCI_STREAM_PARTITION_KEY=agent-events
###############################################################################
# Guardrails, Judges, Supervisor
###############################################################################
ENABLE_INPUT_GUARDRAILS=true
ENABLE_OUTPUT_GUARDRAILS=true
ENABLE_JUDGES=true
ENABLE_SUPERVISOR=true
ENABLE_OUTPUT_SUPERVISOR=true
ENABLE_PARALLEL_GUARDRAILS=true
GUARDRAILS_FAIL_FAST=true
OUTPUT_SUPERVISOR_MAX_RETRIES=3
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
JUDGES_CONFIG_PATH=./config/judges.yaml
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
###############################################################################
# Gateway de canais
###############################################################################
DEFAULT_CHANNEL=web
# embedded = backend may parse simple/native channel payloads.
# external = backend only accepts GatewayRequest normalized by an external Channel Gateway.
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
ENABLE_VOICE_ADAPTER=true
ENABLE_WHATSAPP_ADAPTER=true
ENABLE_TEXT_ADAPTER=true
#################################################
# ENTERPRISE ROUTING
#################################################
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
ROUTING_CONFIG_PATH=./config/routing.yaml
# true = usa LLM para classificar quando keywords/estado não resolverem.
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
ENABLE_LLM_ROUTER=true
# Semantic route stickiness (optional).
# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE.
# There are no regexes or deterministic language rules.
ENABLE_ROUTE_STICKINESS=true
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
ROUTE_STICKINESS_HISTORY_TURNS=2
ROUTE_STICKINESS_MAX_TOKENS=80
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
###############################################################################
# MCP / Tools
###############################################################################
ENABLE_MCP_TOOLS=true
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
TOOLS_CONFIG_PATH=./config/tools.yaml
MCP_TOOL_TIMEOUT_SECONDS=30
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
ROUTING_MODE=router
# Usage/cost accounting
USAGE_REPOSITORY_PROVIDER=autonomous
IDENTITY_CONFIG_PATH=./config/identity.yaml
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
# -----------------------------------------------------------------------------
# ConversationSummaryMemory / compressão de contexto conversacional
# -----------------------------------------------------------------------------
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
MEMORY_CONTEXT_STRATEGY=summary
MEMORY_HISTORY_LIMIT=80
MEMORY_RECENT_MESSAGES_LIMIT=8
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
MEMORY_MAX_SUMMARY_CHARS=6000
MEMORY_SUMMARY_USE_LLM=true
MEMORY_INJECT_RECENT_MESSAGES=true
MEMORY_INJECT_SUMMARY=true
###############################################################################
# LONG-TERM MEMORY
###############################################################################
ENABLE_LONG_TERM_MEMORY=true
LONG_TERM_MEMORY_PROVIDER=sqlite
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY
# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
LONG_TERM_MEMORY_AUTO_EXTRACT=true
LONG_TERM_MEMORY_INJECT_CONTEXT=true

View File

@@ -158,3 +158,83 @@ def test_ordered_keyword_match_is_conservative():
assert EnterpriseRouter._ordered_keyword_match("cancelar pedido", "quero cancelar meu pedido") is True assert EnterpriseRouter._ordered_keyword_match("cancelar pedido", "quero cancelar meu pedido") is True
assert EnterpriseRouter._ordered_keyword_match("cancelar pedido", "quero pedido e talvez cancelar depois") is False assert EnterpriseRouter._ordered_keyword_match("cancelar pedido", "quero pedido e talvez cancelar depois") is False
assert EnterpriseRouter._ordered_keyword_match("pedido", "meu pedido") is False assert EnterpriseRouter._ordered_keyword_match("pedido", "meu pedido") is False
def test_ordered_content_keyword_match_tolerates_omitted_short_connectors():
assert EnterpriseRouter._ordered_content_keyword_match(
"qual é o meu plano", "qual o meu plano"
) is True
assert EnterpriseRouter._ordered_content_keyword_match(
"qual é o meu plano", "quero ver minha fatura"
) is False
@pytest.mark.asyncio
async def test_loaded_domain_style_plan_shift_preempts_continuity_without_llm():
"""Regression: invoice -> plan must work from configured keywords, even same agent.
This mirrors applications such as Contas where two intents may be handled by
the same agent but require different MCP tools. The routing core must not
know application-specific intent or agent names.
"""
from agent_framework.routing.models import IntentDefinition
class _Continuity:
async def evaluate(self, state, *, intents):
raise AssertionError("continuity LLM must not run for explicit configured intent shift")
router = object.__new__(EnterpriseRouter)
router.state_policies = []
router.intents = [
IntentDefinition(
name="domain_plan_information",
domain="customer_domain",
agent="customer_agent",
description="informações de plano",
priority=142,
mcp_tools=["consultar_plano"],
keywords=["qual é o meu plano", "plano contratado"],
examples=[],
enabled=True,
),
IntentDefinition(
name="domain_invoice_query",
domain="customer_domain",
agent="customer_agent",
description="consulta de fatura",
priority=140,
mcp_tools=["consultar_faturas"],
keywords=["quero minha fatura", "fatura atual"],
examples=[],
enabled=True,
),
]
router.continuity = _Continuity()
router.enable_llm_router = False
router.llm = None
router.telemetry = None
router.fallback_agent = "customer_agent"
message = "qual o meu plano"
decision = await router.route({
"user_text": message,
"sanitized_input": message,
"active_agent": "customer_agent",
"intent": "domain_invoice_query",
"route_decision": {
"route": "customer_agent",
"agent": "customer_agent",
"intent": "domain_invoice_query",
"domain": "customer_domain",
"mcp_tools": ["consultar_faturas"],
},
"context": {"session": {}},
})
assert decision.intent == "domain_plan_information"
assert decision.agent == "customer_agent"
assert decision.mcp_tools == ["consultar_plano"]
assert decision.method == "keyword"
assert decision.metadata["route_stickiness_preempted"] is True
assert decision.metadata["previous_intent"] == "domain_invoice_query"
assert decision.metadata["keyword_match_strategy"] == "ordered_content_tokens"