mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-11-06 03:14:12 +00:00
new feature: transaction interruption for same intent replacement
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
164
tests/test_same_intent_transaction_replacement.py
Normal file
164
tests/test_same_intent_transaction_replacement.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_framework.routing.enterprise_router import EnterpriseRouter
|
||||
from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
|
||||
|
||||
|
||||
class _ReplacementLLM:
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
system = str(messages[0].get("content") if isinstance(messages[0], dict) else messages[0]).lower()
|
||||
user = str(messages[-1].get("content") if isinstance(messages[-1], dict) else messages[-1]).lower()
|
||||
|
||||
# First probe is abandonment-only and must not steal a normal same-intent replacement.
|
||||
if "somente se o usuário desistiu explicitamente" in system:
|
||||
if "esquece" in user:
|
||||
return json.dumps({
|
||||
"decision": "ABANDON",
|
||||
"intent": "contas_vas_cancel",
|
||||
"agent": "contestacao_agent",
|
||||
"confidence": 0.99,
|
||||
"reason": "abandona a instância anterior e inicia novo cancelamento",
|
||||
})
|
||||
return json.dumps({
|
||||
"decision": "CONTINUE",
|
||||
"intent": None,
|
||||
"agent": None,
|
||||
"confidence": 0.99,
|
||||
"reason": "não houve abandono explícito",
|
||||
})
|
||||
|
||||
if "também tim fashion" in user or "tambem tim fashion" in user:
|
||||
return json.dumps({
|
||||
"decision": "CONTINUE",
|
||||
"intent": "contas_vas_cancel",
|
||||
"agent": "contestacao_agent",
|
||||
"confidence": 0.99,
|
||||
"reason": "adição de item à mesma operação, não substituição",
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
"decision": "REPLACE",
|
||||
"intent": "contas_vas_cancel",
|
||||
"agent": "contestacao_agent",
|
||||
"confidence": 0.99,
|
||||
"reason": "mesma intenção, novo alvo transacional",
|
||||
})
|
||||
|
||||
|
||||
def _router(tmp_path):
|
||||
routing = tmp_path / "routing.yaml"
|
||||
routing.write_text(
|
||||
"""
|
||||
router:
|
||||
fallback_agent: contestacao_agent
|
||||
confidence_threshold: 0.70
|
||||
state_policies:
|
||||
- state: WAITING_CONTESTACAO_CONFIRMATION
|
||||
agent: contestacao_agent
|
||||
intents:
|
||||
- name: contas_vas_cancel
|
||||
domain: telecom_contas
|
||||
agent: contestacao_agent
|
||||
priority: 20
|
||||
keywords: [cancelar]
|
||||
mcp_tools: [consultar_vas, cancelar_vas_avulso]
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return EnterpriseRouter(
|
||||
SimpleNamespace(
|
||||
ROUTING_CONFIG_PATH=str(routing),
|
||||
ENABLE_LLM_ROUTER=True,
|
||||
ENABLE_ROUTE_STICKINESS=False,
|
||||
),
|
||||
llm=_ReplacementLLM(),
|
||||
)
|
||||
|
||||
|
||||
def _state(message):
|
||||
return {
|
||||
"user_text": message,
|
||||
"sanitized_input": message,
|
||||
"next_state": "WAITING_CONTESTACAO_CONFIRMATION",
|
||||
"transaction_status": "AWAITING_CONFIRMATION",
|
||||
"intent": "state:WAITING_CONTESTACAO_CONFIRMATION",
|
||||
"active_agent": "contestacao_agent",
|
||||
"active_transaction": {
|
||||
"tool_name": "cancelar_vas_avulso",
|
||||
"arguments": {"subject": ["Tamboro Mensal", "Paramount+"]},
|
||||
"status": "AWAITING_CONFIRMATION",
|
||||
"started_from_intent": "contas_vas_cancel",
|
||||
},
|
||||
"pending_tool_call": {
|
||||
"tool_name": "cancelar_vas_avulso",
|
||||
"arguments": {"subject": ["Tamboro Mensal", "Paramount+"]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_same_intent_new_target_emits_replacement_without_new_state(tmp_path):
|
||||
decision = await _router(tmp_path).route(_state("quero cancelar TIM Fashion"))
|
||||
|
||||
assert decision.intent == "contas_vas_cancel"
|
||||
assert decision.agent == "contestacao_agent"
|
||||
assert decision.next_state is None
|
||||
assert decision.metadata["transaction_interruption"] == "same_intent_replacement"
|
||||
assert decision.metadata["interrupted_intent"] == "contas_vas_cancel"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_same_intent_additive_request_keeps_current_transaction(tmp_path):
|
||||
decision = await _router(tmp_path).route(_state("quero cancelar também TIM Fashion"))
|
||||
|
||||
assert decision.method == "state"
|
||||
assert decision.intent == "state:WAITING_CONTESTACAO_CONFIRMATION"
|
||||
assert "transaction_interruption" not in (decision.metadata or {})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_explicit_abandonment_with_same_intent_new_target_can_preempt_state_lock(tmp_path):
|
||||
decision = await _router(tmp_path).route(_state("esquece, quero cancelar TIM Fashion"))
|
||||
|
||||
assert decision.intent == "contas_vas_cancel"
|
||||
assert decision.agent == "contestacao_agent"
|
||||
assert decision.metadata["transaction_interruption"] == "explicit_abandonment"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_cancels_old_instance_for_same_intent_replacement():
|
||||
runtime = AgentRuntimeMixin()
|
||||
state = {
|
||||
"sanitized_input": "quero cancelar TIM Fashion",
|
||||
"user_text": "quero cancelar TIM Fashion",
|
||||
"route_decision": {
|
||||
"metadata": {"transaction_interruption": "same_intent_replacement"}
|
||||
},
|
||||
"active_transaction": {
|
||||
"tool_name": "cancelar_vas_avulso",
|
||||
"arguments": {"subject": ["Tamboro Mensal", "Paramount+"]},
|
||||
"status": "AWAITING_CONFIRMATION",
|
||||
"started_from_intent": "contas_vas_cancel",
|
||||
},
|
||||
"pending_tool_call": {
|
||||
"tool_name": "cancelar_vas_avulso",
|
||||
"arguments": {"subject": ["Tamboro Mensal", "Paramount+"]},
|
||||
},
|
||||
"selected_tool_call": {
|
||||
"tool_name": "cancelar_vas_avulso",
|
||||
"arguments": {"subject": ["Tamboro Mensal", "Paramount+"]},
|
||||
},
|
||||
"transaction_status": "AWAITING_CONFIRMATION",
|
||||
"mcp_tools": [],
|
||||
}
|
||||
|
||||
await runtime.execute_tools_for_intent(state, tools=[])
|
||||
|
||||
assert state.get("active_transaction") is None
|
||||
assert state.get("pending_tool_call") in ({}, None)
|
||||
assert state.get("selected_tool_call") in ({}, None)
|
||||
assert state["tool_policy_result"]["action"] == "cancelled_by_same_intent_replacement"
|
||||
assert state["operational_context_reset"] is True
|
||||
Reference in New Issue
Block a user