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.
Binary file not shown.
@@ -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
|
||||
@@ -11,9 +11,18 @@ from agent_framework.runtime.transaction_parameters import reconcile_transaction
|
||||
class _RouterLLM:
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[-1]["content"] if isinstance(messages[-1], dict) else str(messages[-1])
|
||||
low = prompt.lower()
|
||||
if kwargs.get("generation_name") == "llm.transaction_parameter_relevance":
|
||||
if "isso mesmo, pode cancelar" in low or "motivo é que desisti" in low or "motivo e que desisti" in low:
|
||||
return json.dumps({"relevance": "RELEVANT", "confidence": 0.99, "reason": "responde ao contexto solicitado"})
|
||||
return json.dumps({"relevance": "NOT_RELEVANT", "confidence": 0.99, "reason": "não responde ao parâmetro pendente"})
|
||||
if kwargs.get("profile_name") == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
|
||||
return json.dumps({"subject": None, "valor": None})
|
||||
if "nao quero cancelar. quero apenas verificar os meus servicos contratados" in prompt.lower():
|
||||
if (
|
||||
"nao quero cancelar. quero apenas verificar os meus servicos contratados" in prompt.lower()
|
||||
or "nao quero cancelar. quero apenas ver meus servicos contratados" in prompt.lower()
|
||||
or "nao quero mais cancelar. quero apenas ver os servicos que eu contratei" in prompt.lower()
|
||||
):
|
||||
return json.dumps({
|
||||
"decision": "ABANDON",
|
||||
"intent": "product_services_information",
|
||||
@@ -179,6 +188,43 @@ async def test_explicit_abandonment_precedes_awaiting_confirmation_and_routes_ne
|
||||
assert not decision.metadata.get("transaction_turn_consumed")
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_explicit_abandonment_precedes_generic_state_lock_without_transaction_status(tmp_path):
|
||||
state = _awaiting_confirmation_state(
|
||||
"nao quero cancelar. quero apenas ver meus servicos contratados"
|
||||
)
|
||||
# Reproduce the host/checkpoint shape observed in production: the domain
|
||||
# WAITING_* state is restored, but transaction_status is absent/stale.
|
||||
state.pop("transaction_status", None)
|
||||
decision = await _router(tmp_path).route(state)
|
||||
assert decision.intent == "product_services_information"
|
||||
assert decision.agent == "product_agent"
|
||||
assert decision.metadata["transaction_interruption"] == "explicit_abandonment"
|
||||
assert decision.metadata["interrupted_state"] == "WAITING_CONTESTACAO_CONFIRMATION"
|
||||
assert not decision.metadata.get("transaction_turn_consumed")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_explicit_abandonment_precedes_state_lock_with_only_domain_state_preserved(tmp_path):
|
||||
state = _awaiting_confirmation_state(
|
||||
"nao quero mais cancelar. quero apenas ver os servicos que eu contratei"
|
||||
)
|
||||
# Production checkpoint shape: only the domain WAITING_* lock survives.
|
||||
# No transaction_status, active_transaction or legacy pending tool latch.
|
||||
state.pop("transaction_status", None)
|
||||
state.pop("active_transaction", None)
|
||||
state.pop("pending_tool_call", None)
|
||||
state.pop("selected_tool_call", None)
|
||||
decision = await _router(tmp_path).route(state)
|
||||
assert decision.intent == "product_services_information"
|
||||
assert decision.agent == "product_agent"
|
||||
assert decision.metadata["transaction_interruption"] == "explicit_abandonment"
|
||||
assert decision.metadata["interrupted_state"] == "WAITING_CONTESTACAO_CONFIRMATION"
|
||||
assert not decision.metadata.get("transaction_turn_consumed")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plain_negative_confirmation_remains_confirmation_not_abandon(tmp_path):
|
||||
decision = await _router(tmp_path).route(_awaiting_confirmation_state("não"))
|
||||
@@ -191,12 +237,30 @@ async def test_plain_negative_confirmation_remains_confirmation_not_abandon(tmp_
|
||||
class _MisleadingParameterRouterLLM(_RouterLLM):
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[-1]["content"] if isinstance(messages[-1], dict) else str(messages[-1])
|
||||
# Simulate the real failure mode: the parameter extractor hallucinates a
|
||||
# usable value from an abandonment utterance. ABANDON must win before
|
||||
# parameter extraction is allowed to consume the turn.
|
||||
generation = kwargs.get("generation_name")
|
||||
low = prompt.lower()
|
||||
# Relevance is a semantic gate before extraction. The extractor below is
|
||||
# intentionally misleading to prove that NOT_RELEVANT prevents a false
|
||||
# parameter value from stealing an abandonment turn.
|
||||
if generation == "llm.transaction_parameter_relevance":
|
||||
if "motivo é que desisti" in low or "motivo e que desisti" in low:
|
||||
return json.dumps({"relevance": "RELEVANT", "confidence": 0.99, "reason": "responde ao reason"})
|
||||
if "nao quero mais devolver" in low or "não quero mais devolver" in low or "nao quero mais cancelar" in low:
|
||||
return json.dumps({"relevance": "NOT_RELEVANT", "confidence": 0.99, "reason": "abandona a ação"})
|
||||
return json.dumps({"relevance": "NOT_RELEVANT", "confidence": 0.95, "reason": "não responde ao parâmetro"})
|
||||
if kwargs.get("profile_name") == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
|
||||
if "nao quero mais cancelar" in prompt.lower() or "não quero mais cancelar" in prompt.lower():
|
||||
if "motivo é que desisti" in low or "motivo e que desisti" in low:
|
||||
return json.dumps({"valor": "desisti"})
|
||||
if "nao quero mais cancelar" in low or "não quero mais cancelar" in low:
|
||||
return json.dumps({"valor": 14.99})
|
||||
if "nao quero mais devolver" in low or "não quero mais devolver" in low:
|
||||
return json.dumps({
|
||||
"decision": "ABANDON",
|
||||
"intent": "product_services_information",
|
||||
"agent": "product_agent",
|
||||
"confidence": 0.99,
|
||||
"reason": "abandono explícito da devolução com novo objetivo",
|
||||
})
|
||||
return await super().ainvoke(messages, **kwargs)
|
||||
|
||||
|
||||
@@ -207,17 +271,43 @@ def _router_with_misleading_parameter_extractor(tmp_path):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_explicit_abandonment_precedes_misleading_parameter_extraction(tmp_path):
|
||||
async def test_parameter_relevance_precedes_abandonment(tmp_path):
|
||||
state = _active_state("motivo é que desisti")
|
||||
state["missing_parameters"] = ["valor"]
|
||||
state["active_transaction"]["parameter_schema"] = {"valor": {"type": "string", "description": "motivo informado pelo cliente"}}
|
||||
decision = await _router_with_misleading_parameter_extractor(tmp_path).route(state)
|
||||
assert decision.intent == "state:COLLECTING_PARAMETERS"
|
||||
assert decision.metadata["transaction_turn_consumed"] is True
|
||||
assert decision.metadata["transaction_parameter_relevant"] is True
|
||||
assert decision.metadata["transaction_parameter_values"] == {"valor": "desisti"}
|
||||
assert "transaction_interruption" not in decision.metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_not_relevant_prevents_misleading_extractor_and_allows_abandon(tmp_path):
|
||||
decision = await _router_with_misleading_parameter_extractor(tmp_path).route(
|
||||
_active_state("esquece, nao quero mais cancelar")
|
||||
_active_state("nao quero mais cancelar")
|
||||
)
|
||||
assert decision.intent == "state:TRANSACTION_ABANDONED"
|
||||
assert decision.agent == "contestacao_agent"
|
||||
assert decision.mcp_tools == []
|
||||
assert decision.metadata["transaction_interruption"] == "explicit_abandonment"
|
||||
assert decision.metadata["transaction_parameter_relevant"] is False
|
||||
assert not decision.metadata.get("transaction_turn_consumed")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_abandon_plus_new_goal_after_parameter_relevance_fails(tmp_path):
|
||||
decision = await _router_with_misleading_parameter_extractor(tmp_path).route(
|
||||
_active_state("nao quero mais devolver. quero apenas ver os meus servicos contratados")
|
||||
)
|
||||
assert decision.intent == "product_services_information"
|
||||
assert decision.agent == "product_agent"
|
||||
assert decision.metadata["transaction_interruption"] == "explicit_abandonment"
|
||||
assert decision.metadata["transaction_parameter_relevant"] is False
|
||||
|
||||
|
||||
|
||||
class _PolicyRouter:
|
||||
def __init__(self):
|
||||
self.registry = SimpleNamespace(
|
||||
@@ -449,3 +539,79 @@ async def test_runtime_abandon_cancels_only_active_transaction_without_rearming_
|
||||
assert state["tool_policy_result"]["action"] == "cancelled_by_explicit_abandonment"
|
||||
# ABANDON encerra a ação ativa; não apaga outras ações pendentes por efeito colateral.
|
||||
assert state["pending_topics"] == [{"operation_id": "op-x", "intent": "other_pending_intent"}]
|
||||
|
||||
class _ReasonParameterRouterLLM(_RouterLLM):
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[-1]["content"] if isinstance(messages[-1], dict) else str(messages[-1])
|
||||
lowered = prompt.lower()
|
||||
if kwargs.get("generation_name") == "llm.transaction_parameter_relevance":
|
||||
if "motivo é que desisti" in lowered or "motivo e que desisti" in lowered:
|
||||
return json.dumps({"relevance": "RELEVANT", "confidence": 0.99, "reason": "responde ao motivo"})
|
||||
if "nao quero mais devolver o pedido" in lowered or "não quero mais devolver o pedido" in lowered:
|
||||
return json.dumps({"relevance": "NOT_RELEVANT", "confidence": 0.99, "reason": "abandona a devolução"})
|
||||
if kwargs.get("profile_name") == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
|
||||
if "motivo é que desisti" in lowered or "motivo e que desisti" in lowered:
|
||||
return json.dumps({"reason": "desisti"})
|
||||
if "nao quero mais devolver o pedido" in lowered or "não quero mais devolver o pedido" in lowered:
|
||||
return json.dumps({
|
||||
"decision": "ABANDON",
|
||||
"intent": None,
|
||||
"agent": None,
|
||||
"confidence": 0.99,
|
||||
"reason": "abandono explícito da devolução",
|
||||
})
|
||||
return await super().ainvoke(messages, **kwargs)
|
||||
|
||||
|
||||
def _reason_collecting_state(message):
|
||||
return {
|
||||
"user_text": message,
|
||||
"sanitized_input": message,
|
||||
"next_state": "COLLECTING_ORDERS_PARAMETERS",
|
||||
"transaction_status": "COLLECTING_PARAMETERS",
|
||||
"missing_parameters": ["reason"],
|
||||
"intent": "state:COLLECTING_PARAMETERS",
|
||||
"active_agent": "orders_agent",
|
||||
"active_transaction": {
|
||||
"tool_name": "solicitar_devolucao",
|
||||
"arguments": {"order_id": "PED-1001"},
|
||||
"status": "COLLECTING_PARAMETERS",
|
||||
"started_from_intent": "retail_support_exchange_return",
|
||||
"parameter_schema": {"reason": {"type": "string", "description": "motivo da devolução"}},
|
||||
"tool_description": "Solicita devolução de pedido informando o motivo",
|
||||
},
|
||||
"history": [
|
||||
{"role": "user", "content": "quero devolver o pedido PED-1001"},
|
||||
{"role": "assistant", "content": "Qual o motivo da devolução?"},
|
||||
{"role": "user", "content": message},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parameter_value_desisti_is_not_abandon_when_reason_is_expected(tmp_path):
|
||||
router = _router(tmp_path)
|
||||
router.llm = _ReasonParameterRouterLLM()
|
||||
decision = await router.route(_reason_collecting_state("motivo é que desisti"))
|
||||
assert decision.intent == "state:COLLECTING_PARAMETERS"
|
||||
assert decision.agent == "contestacao_agent" or decision.agent == "orders_agent"
|
||||
assert decision.metadata["transaction_turn_consumed"] is True
|
||||
assert decision.metadata["transaction_parameter_values"] == {"reason": "desisti"}
|
||||
assert "transaction_interruption" not in decision.metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_action_scoped_abandon_still_preempts_reason_extraction(tmp_path):
|
||||
router = _router(tmp_path)
|
||||
router.llm = _ReasonParameterRouterLLM()
|
||||
# The action is explicitly abandoned; this must not be consumed as reason.
|
||||
decision = await router.route(_reason_collecting_state("nao quero mais devolver o pedido"))
|
||||
assert decision.metadata["transaction_interruption"] == "explicit_abandonment"
|
||||
|
||||
|
||||
def test_abandonment_has_no_lexical_gate_in_router_source():
|
||||
from pathlib import Path
|
||||
source = Path("libs/agent_framework/src/agent_framework/routing/enterprise_router.py").read_text(encoding="utf-8")
|
||||
assert "_looks_like_explicit_abandonment" not in source
|
||||
assert "_looks_like_action_abandonment" not in source
|
||||
assert "cues = (" not in source
|
||||
|
||||
@@ -301,7 +301,7 @@ intents:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_incompatible_intent_shift_runs_only_when_parameter_extractor_does_not_consume(tmp_path):
|
||||
"""A real new goal still shifts, but only after parameter extraction declines it."""
|
||||
"""A real new goal shifts after semantic parameter-relevance declines it."""
|
||||
routing = tmp_path / "routing.yaml"
|
||||
routing.write_text(
|
||||
"""
|
||||
@@ -331,6 +331,8 @@ intents:
|
||||
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[-1]["content"] if isinstance(messages[-1], dict) else str(messages[-1])
|
||||
if kwargs.get("generation_name") == "llm.transaction_parameter_relevance":
|
||||
return json.dumps({"relevance": "NOT_RELEVANT", "confidence": 0.99, "reason": "test relevance"})
|
||||
if kwargs.get("profile_name") == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
|
||||
self.extraction_calls += 1
|
||||
# The extractor must not convert a clearly new request into the
|
||||
@@ -374,13 +376,13 @@ intents:
|
||||
assert decision.intent == "retail_order_cancel"
|
||||
assert decision.agent == "orders_agent"
|
||||
assert decision.metadata["transaction_interruption"] == "intent_shift"
|
||||
assert llm.shift_calls == 1
|
||||
assert llm.extraction_calls == 1
|
||||
assert llm.shift_calls == 2 # ABANDON probe + SHIFT classification
|
||||
assert llm.extraction_calls == 0 # NOT_RELEVANT skips parameter extraction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_shift_without_keyword_runs_after_parameter_extractor_declines(tmp_path):
|
||||
"""Semantic SHIFT remains available when no pending parameter is consumed."""
|
||||
"""Semantic SHIFT remains available after parameter relevance is NOT_RELEVANT."""
|
||||
routing = tmp_path / "routing.yaml"
|
||||
routing.write_text(
|
||||
"""
|
||||
@@ -410,6 +412,8 @@ intents:
|
||||
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[-1]["content"] if isinstance(messages[-1], dict) else str(messages[-1])
|
||||
if kwargs.get("generation_name") == "llm.transaction_parameter_relevance":
|
||||
return json.dumps({"relevance": "NOT_RELEVANT", "confidence": 0.99, "reason": "test relevance"})
|
||||
if kwargs.get("profile_name") == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
|
||||
self.extraction_calls += 1
|
||||
return json.dumps({"reason": None})
|
||||
@@ -452,8 +456,8 @@ intents:
|
||||
assert decision.agent == "orders_agent"
|
||||
assert decision.metadata["transaction_interruption"] == "intent_shift"
|
||||
assert decision.metadata["interruption_source"] == "semantic_classifier"
|
||||
assert llm.shift_calls == 1
|
||||
assert llm.extraction_calls == 1
|
||||
assert llm.shift_calls == 2 # ABANDON probe + SHIFT classification
|
||||
assert llm.extraction_calls == 0 # NOT_RELEVANT skips parameter extraction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -487,6 +491,8 @@ intents:
|
||||
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[-1]["content"] if isinstance(messages[-1], dict) else str(messages[-1])
|
||||
if kwargs.get("generation_name") == "llm.transaction_parameter_relevance":
|
||||
return json.dumps({"relevance": "RELEVANT", "confidence": 0.99, "reason": "test relevance"})
|
||||
if kwargs.get("profile_name") == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
|
||||
self.extraction_calls += 1
|
||||
assert "Tamboro Mensal" in prompt
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user