bugfix reconciliation extractor
This commit is contained in:
@@ -42,3 +42,316 @@ async def test_contextual_reentry_separates_current_claim_from_prior_context_for
|
||||
assert "Cobrança Tamboro Mensal" in llm.prompt
|
||||
assert "user_message: é a de quatorze e noventa e nove" in llm.prompt
|
||||
assert "Não trate texto do contexto como uma nova afirmação do cliente" in llm.prompt
|
||||
|
||||
class _TwoPassContextLLM:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[-1]["content"]
|
||||
self.calls.append(prompt)
|
||||
# First pass: current utterance provides only the amount.
|
||||
if "user_message: é a de vinte e cinco e cinquenta" in prompt:
|
||||
return json.dumps({"subject": None, "valor": 25.50}, ensure_ascii=False)
|
||||
# Second bounded pass: previous USER utterance provides the missing entity.
|
||||
if "user_message: não reconheço esse Tamboro Mensal na minha fatura" in prompt:
|
||||
return json.dumps({"subject": "Tamboro Mensal"}, ensure_ascii=False)
|
||||
return "{}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contextual_reentry_second_pass_recovers_only_missing_candidate_from_prior_user_turn():
|
||||
from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
|
||||
|
||||
class Runtime(AgentRuntimeMixin):
|
||||
pass
|
||||
|
||||
runtime = Runtime()
|
||||
runtime.llm = _TwoPassContextLLM()
|
||||
state = {
|
||||
"route_decision": {
|
||||
"metadata": {
|
||||
"contextual_reentry": True,
|
||||
"original_input": "é a de vinte e cinco e cinquenta",
|
||||
"relevant_conversation_context": (
|
||||
"user: não reconheço esse Tamboro Mensal na minha fatura\n"
|
||||
"assistant: identifiquei duas cobranças de Tamboro Mensal"
|
||||
),
|
||||
}
|
||||
},
|
||||
"sanitized_input": "é a de vinte e cinco e cinquenta",
|
||||
}
|
||||
out = await runtime._extract_transaction_parameters(
|
||||
state=state,
|
||||
tool_name="contestar_cobranca",
|
||||
missing_parameters=["subject", "valor"],
|
||||
known_arguments={},
|
||||
)
|
||||
assert float(out["valor"]) == 25.5
|
||||
assert out["subject"] == "Tamboro Mensal"
|
||||
assert len(runtime.llm.calls) >= 2
|
||||
|
||||
class _CoherentTemporalLLM:
|
||||
"""Simulates the structured decisions expected from temporal reconciliation."""
|
||||
def __init__(self):
|
||||
self.prompts = []
|
||||
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[-1]["content"]
|
||||
self.prompts.append(prompt)
|
||||
low = prompt.lower()
|
||||
# Product A + value X established.
|
||||
if "user_message: na verdade é tim fashion mensal" in low:
|
||||
return json.dumps({"fields": {
|
||||
"subject": {"decision": "resolved", "value": "TIM Fashion Mensal", "source": "current"},
|
||||
# Previous 14.99 was tied to the former product and must not be transplanted.
|
||||
"valor": {"decision": "clear", "value": None, "source": "history:1"},
|
||||
}}, ensure_ascii=False)
|
||||
if "user_message: na verdade é produto inexistente" in low:
|
||||
return json.dumps({"fields": {
|
||||
# Keep the newest candidate so authoritative validation can reject it.
|
||||
"subject": {"decision": "resolved", "value": "Produto Inexistente", "source": "current"},
|
||||
"valor": {"decision": "clear", "value": None, "source": "history:1"},
|
||||
}}, ensure_ascii=False)
|
||||
if "user_message: desculpa, era tamboro mensal mesmo" in low:
|
||||
return json.dumps({"fields": {
|
||||
"subject": {"decision": "resolved", "value": "Tamboro Mensal", "source": "current"},
|
||||
# Full temporal text makes the old amount coherent again with the restored product.
|
||||
"valor": {"decision": "resolved", "value": 14.99, "source": "history:2"},
|
||||
}}, ensure_ascii=False)
|
||||
return json.dumps({"fields": {
|
||||
"subject": {"decision": "preserve", "value": None, "source": "state"},
|
||||
"valor": {"decision": "preserve", "value": None, "source": "state"},
|
||||
}}, ensure_ascii=False)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_reconciliation_product_change_rechecks_old_value_as_part_of_coherent_set():
|
||||
from agent_framework.runtime.transaction_parameters import reconcile_transaction_parameters
|
||||
|
||||
llm = _CoherentTemporalLLM()
|
||||
out = await reconcile_transaction_parameters(
|
||||
llm,
|
||||
text="na verdade é TIM Fashion Mensal",
|
||||
conversational_context=(
|
||||
"history:1: user: é R$ 14,99\n"
|
||||
"history:2: user: quero contestar Tamboro Mensal"
|
||||
),
|
||||
tool_name="contestar_cobranca",
|
||||
parameter_names=["subject", "valor"],
|
||||
known_arguments={"subject": "Tamboro Mensal", "valor": 14.99},
|
||||
parameter_schema={
|
||||
"subject": {"type": "string", "description": "Nome do serviço, produto, item ou cobrança objeto da contestação."},
|
||||
"valor": {"type": "number", "description": "Valor monetário associado ao item objeto da contestação."},
|
||||
},
|
||||
tool_description="Contesta uma cobrança após validação.",
|
||||
)
|
||||
assert out["values"] == {"subject": "TIM Fashion Mensal"}
|
||||
assert out["clear_fields"] == ["valor"]
|
||||
assert out["provenance"]["subject"] == "current"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_reconciliation_invalid_new_product_does_not_erase_history_or_fall_back_silently():
|
||||
from agent_framework.runtime.transaction_parameters import reconcile_transaction_parameters
|
||||
|
||||
llm = _CoherentTemporalLLM()
|
||||
history = (
|
||||
"history:1: user: é R$ 14,99\n"
|
||||
"history:2: user: quero contestar Tamboro Mensal"
|
||||
)
|
||||
out = await reconcile_transaction_parameters(
|
||||
llm,
|
||||
text="na verdade é Produto Inexistente",
|
||||
conversational_context=history,
|
||||
tool_name="contestar_cobranca",
|
||||
parameter_names=["subject", "valor"],
|
||||
known_arguments={"subject": "Tamboro Mensal", "valor": 14.99},
|
||||
parameter_schema={
|
||||
"subject": {"type": "string", "description": "Nome do serviço, produto, item ou cobrança objeto da contestação."},
|
||||
"valor": {"type": "number", "description": "Valor monetário associado ao item objeto da contestação."},
|
||||
},
|
||||
tool_description="Contesta uma cobrança após validação.",
|
||||
)
|
||||
# The newest candidate stays visible for pre-validation; the old product is not silently restored.
|
||||
assert out["values"] == {"subject": "Produto Inexistente"}
|
||||
assert "valor" in out["clear_fields"]
|
||||
assert "Tamboro Mensal" in llm.prompts[-1] # history remains available to future reconciliation.
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_reconciliation_can_restore_coherent_old_value_when_product_is_explicitly_restored():
|
||||
from agent_framework.runtime.transaction_parameters import reconcile_transaction_parameters
|
||||
|
||||
llm = _CoherentTemporalLLM()
|
||||
out = await reconcile_transaction_parameters(
|
||||
llm,
|
||||
text="desculpa, era Tamboro Mensal mesmo",
|
||||
conversational_context=(
|
||||
"history:1: user: na verdade é Produto Inexistente\n"
|
||||
"history:2: user: é R$ 14,99\n"
|
||||
"history:3: user: quero contestar Tamboro Mensal"
|
||||
),
|
||||
tool_name="contestar_cobranca",
|
||||
parameter_names=["subject", "valor"],
|
||||
known_arguments={},
|
||||
parameter_schema={
|
||||
"subject": {"type": "string", "description": "Nome do serviço, produto, item ou cobrança objeto da contestação."},
|
||||
"valor": {"type": "number", "description": "Valor monetário associado ao item objeto da contestação."},
|
||||
},
|
||||
tool_description="Contesta uma cobrança após validação.",
|
||||
)
|
||||
assert out["values"] == {"subject": "Tamboro Mensal", "valor": 14.99}
|
||||
assert out["provenance"] == {"subject": "current", "valor": "history:2"}
|
||||
|
||||
|
||||
def test_temporal_context_priority_places_assistant_before_older_user_history():
|
||||
from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
|
||||
|
||||
context = (
|
||||
"user: tem uma cobrança aqui que eu não reconheço\n"
|
||||
"assistant: Cobrança Tamboro Mensal no valor de R$ 14,99; TIM Fashion Mensal no valor de R$ 10,00."
|
||||
)
|
||||
prioritized = AgentRuntimeMixin._transaction_context_priority_view(context)
|
||||
|
||||
assert "priority_3_previous_assistant_tool_or_evidence_context:" in prioritized
|
||||
assert "priority_4_previous_user_utterances:" in prioritized
|
||||
assert prioritized.index("assistant: Cobrança Tamboro Mensal") < prioritized.index("user: tem uma cobrança")
|
||||
|
||||
|
||||
class _AssistantRelationshipReconcilerLLM:
|
||||
def __init__(self):
|
||||
self.prompt = ""
|
||||
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
self.prompt = messages[-1]["content"]
|
||||
# Simula o comportamento desejado: a fala atual dá o valor, enquanto a
|
||||
# relação item->valor está numa resposta anterior do assistant.
|
||||
assert "user_message: é a de quatorze e noventa e nove" in self.prompt
|
||||
assert "priority_3_previous_assistant_tool_or_evidence_context:" in self.prompt
|
||||
assert "assistant: Cobrança Tamboro Mensal no valor de R$ 14,99" in self.prompt
|
||||
assert "anchor_relation_candidates:" in self.prompt
|
||||
assert "anchor[valor=14.99]" in self.prompt
|
||||
return json.dumps({"fields": {
|
||||
"subject": {"decision": "resolved", "value": "Tamboro Mensal", "source": "history:1"},
|
||||
"valor": {"decision": "resolved", "value": 14.99, "source": "current"},
|
||||
}}, ensure_ascii=False)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_reconciler_can_use_grounded_assistant_relationship_after_current_only_extraction_is_incomplete():
|
||||
from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
|
||||
|
||||
class Runtime(AgentRuntimeMixin):
|
||||
pass
|
||||
|
||||
runtime = Runtime()
|
||||
runtime.llm = _AssistantRelationshipReconcilerLLM()
|
||||
runtime.tool_router = type("TR", (), {
|
||||
"registry": type("REG", (), {
|
||||
"get_tool": staticmethod(lambda name: type("CFG", (), {
|
||||
"requires": ["subject", "valor"],
|
||||
"args_schema": {
|
||||
"subject": {"type": "string", "description": "Nome do item ou cobrança a contestar."},
|
||||
"valor": {"type": "number", "description": "Valor monetário do item a contestar."},
|
||||
},
|
||||
"description": "Contesta uma cobrança após validação.",
|
||||
})())
|
||||
})(),
|
||||
"resolve_execution_policy": staticmethod(lambda name, arguments=None: {
|
||||
"operation_type": "transactional",
|
||||
"requires": ["subject", "valor"],
|
||||
}),
|
||||
})()
|
||||
|
||||
state = {
|
||||
"sanitized_input": "é a de quatorze e noventa e nove",
|
||||
"user_text": "é a de quatorze e noventa e nove",
|
||||
"route_decision": {"metadata": {
|
||||
"contextual_reentry": True,
|
||||
"original_input": "é a de quatorze e noventa e nove",
|
||||
"relevant_conversation_context": (
|
||||
"user: tem uma cobrança aqui que eu não reconheço\n"
|
||||
"assistant: Cobrança Tamboro Mensal no valor de R$ 14,99; TIM Fashion Mensal no valor de R$ 10,00."
|
||||
),
|
||||
}},
|
||||
}
|
||||
|
||||
# O happy path corrente só consegue o valor; o subject fica faltando.
|
||||
class _CurrentOnlyLLM:
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
return json.dumps({"valor": 14.99}, ensure_ascii=False)
|
||||
|
||||
original_llm = runtime.llm
|
||||
runtime.llm = _CurrentOnlyLLM()
|
||||
current = await runtime._extract_transaction_parameters_current_only(
|
||||
state,
|
||||
tool_name="contestar_cobranca",
|
||||
missing_parameters=["subject", "valor"],
|
||||
known_arguments={},
|
||||
)
|
||||
assert current == {"valor": 14.99}
|
||||
|
||||
runtime.llm = original_llm
|
||||
reconciled = await runtime._reconcile_transaction_parameters(
|
||||
state,
|
||||
tool_name="contestar_cobranca",
|
||||
parameter_names=["subject", "valor"],
|
||||
known_arguments=current,
|
||||
)
|
||||
assert reconciled["values"]["subject"] == "Tamboro Mensal"
|
||||
assert float(reconciled["values"]["valor"]) == 14.99
|
||||
assert "(3) respostas anteriores do assistente" in runtime.llm.prompt
|
||||
|
||||
class _UnifiedAnchorScanLLM:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[-1]["content"]
|
||||
self.calls.append(prompt)
|
||||
assert "UMA ÚNICA VARREDURA TEMPORAL" in prompt
|
||||
assert "known_parameters: {\"valor\": 14.99}" in prompt
|
||||
assert "missing_parameters: [\"subject\", \"data\"]" in prompt
|
||||
assert "anchor_relation_candidates:" in prompt
|
||||
assert "anchor[valor=14.99]" in prompt
|
||||
assert "Tamboro Mensal no valor de R$ 14,99" in prompt
|
||||
return json.dumps({"fields": {
|
||||
"subject": {"decision": "resolved", "value": "Tamboro Mensal", "source": "history:1"},
|
||||
"valor": {"decision": "preserve", "value": None, "source": "state"},
|
||||
"data": {"decision": "resolved", "value": "01/11/2025", "source": "history:1"},
|
||||
}}, ensure_ascii=False)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_reconciler_single_scan_uses_all_known_fields_as_anchors_and_resolves_all_missing_fields():
|
||||
from agent_framework.runtime.transaction_parameters import reconcile_transaction_parameters
|
||||
|
||||
llm = _UnifiedAnchorScanLLM()
|
||||
result = await reconcile_transaction_parameters(
|
||||
llm,
|
||||
text="é a de quatorze e noventa e nove",
|
||||
tool_name="contestar_cobranca",
|
||||
parameter_names=["subject", "valor", "data"],
|
||||
known_arguments={"valor": 14.99},
|
||||
parameter_schema={
|
||||
"subject": {"type": "string", "description": "Nome do item ou cobrança a contestar."},
|
||||
"valor": {"type": "number", "description": "Valor monetário do item a contestar."},
|
||||
"data": {"type": "string", "description": "Data da cobrança selecionada."},
|
||||
},
|
||||
tool_description="Contesta uma cobrança após validação.",
|
||||
conversational_context=(
|
||||
"priority_3_previous_assistant_tool_or_evidence_context:\n"
|
||||
"history:1: assistant: Cobrança Tamboro Mensal no valor de R$ 14,99 no dia 01/11/2025 * "
|
||||
"TIM Fashion Mensal no valor de R$ 10,00 no dia 01/11/2025.\n"
|
||||
"priority_4_previous_user_utterances:\n"
|
||||
"history:2: user: tem uma cobrança aqui que eu não reconheço"
|
||||
),
|
||||
)
|
||||
|
||||
assert len(llm.calls) == 1
|
||||
assert result["values"]["subject"] == "Tamboro Mensal"
|
||||
assert float(result["values"]["valor"]) == 14.99
|
||||
assert result["values"]["data"] == "01/11/2025"
|
||||
assert result["provenance"]["subject"] == "history:1"
|
||||
|
||||
|
||||
@@ -643,3 +643,98 @@ intents: []
|
||||
decision = await router.route(state)
|
||||
assert decision.metadata["transaction_confirmation_decision"] == "confirm"
|
||||
assert decision.metadata["transaction_confirmation_source"] == "deterministic"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collecting_preserves_resolved_required_fields_and_only_extracts_currently_missing():
|
||||
class Runtime(_Runtime):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pending_seen = None
|
||||
|
||||
async def _extract_transaction_parameters(
|
||||
self, state, *, tool_name, missing_parameters, known_arguments=None
|
||||
):
|
||||
self.pending_seen = list(missing_parameters)
|
||||
# Simulate the value extracted for the only missing field. A resolved
|
||||
# required field must not be offered back to the semantic extractor.
|
||||
return {"reason": "desisti da compra"}
|
||||
|
||||
runtime = Runtime()
|
||||
state = {
|
||||
"user_text": "desisti da compra",
|
||||
"sanitized_input": "desisti da compra",
|
||||
"transaction_status": "COLLECTING_PARAMETERS",
|
||||
"missing_parameters": ["reason"],
|
||||
"mcp_tools": ["solicitar_devolucao"],
|
||||
"active_transaction": {
|
||||
"tool_name": "solicitar_devolucao",
|
||||
"arguments": {"order_id": "PED-1001"},
|
||||
"status": "COLLECTING_PARAMETERS",
|
||||
"parameter_schema": {"order_id": "string", "reason": "string"},
|
||||
"tool_description": "Abre uma solicitação de devolução de pedido.",
|
||||
},
|
||||
"selected_tool_call": {
|
||||
"tool_name": "solicitar_devolucao",
|
||||
"arguments": {"order_id": "PED-1001"},
|
||||
},
|
||||
"route_decision": {"metadata": {}},
|
||||
"route": "support_agent",
|
||||
"intent": "state:COLLECTING_SUPPORT_PARAMETERS",
|
||||
}
|
||||
|
||||
result = await runtime.execute_tools_for_intent(state)
|
||||
assert runtime.pending_seen == ["reason"]
|
||||
assert state["pending_tool_call"]["arguments"]["order_id"] == "PED-1001"
|
||||
assert state["pending_tool_call"]["arguments"]["reason"] == "desisti da compra"
|
||||
assert result[-1]["awaiting_confirmation"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_correction_of_missing_numeric_value_does_not_reopen_resolved_text_entity():
|
||||
class Runtime(_Runtime):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pending_seen = None
|
||||
|
||||
async def _extract_transaction_parameters(
|
||||
self, state, *, tool_name, missing_parameters, known_arguments=None
|
||||
):
|
||||
self.pending_seen = list(missing_parameters)
|
||||
# If subject were incorrectly offered again, a semantic extractor
|
||||
# could reinterpret the monetary phrase as an entity. The runtime
|
||||
# contract must expose only the missing numeric field here.
|
||||
out = {"valor": 19.99}
|
||||
if "subject" in missing_parameters:
|
||||
out["subject"] = "cobrança de R$ 19,99"
|
||||
return out
|
||||
|
||||
runtime = Runtime()
|
||||
state = {
|
||||
"user_text": "desculpa é a de dezenove e noventa e nove",
|
||||
"sanitized_input": "desculpa é a de dezenove e noventa e nove",
|
||||
"transaction_status": "COLLECTING_PARAMETERS",
|
||||
"missing_parameters": ["valor"],
|
||||
"mcp_tools": ["solicitar_devolucao"],
|
||||
"active_transaction": {
|
||||
"tool_name": "solicitar_devolucao",
|
||||
"arguments": {"order_id": "PED-1001"},
|
||||
"status": "COLLECTING_PARAMETERS",
|
||||
"parameter_schema": {"order_id": "string", "reason": "string", "valor": "number"},
|
||||
},
|
||||
"selected_tool_call": {
|
||||
"tool_name": "solicitar_devolucao",
|
||||
"arguments": {"order_id": "PED-1001"},
|
||||
},
|
||||
"route_decision": {"metadata": {}},
|
||||
"route": "support_agent",
|
||||
"intent": "state:COLLECTING_SUPPORT_PARAMETERS",
|
||||
}
|
||||
# This synthetic tool doesn't require valor, so exercise the extraction
|
||||
# contract directly with the same resolved-text/missing-number shape.
|
||||
out = await runtime._extract_transaction_parameters(
|
||||
state,
|
||||
tool_name="solicitar_devolucao",
|
||||
missing_parameters=["valor"],
|
||||
known_arguments={"subject": "Tamboro Mensal"},
|
||||
)
|
||||
assert runtime.pending_seen == ["valor"]
|
||||
assert out == {"valor": 19.99}
|
||||
|
||||
@@ -1283,3 +1283,94 @@ async def test_prevalidation_parameter_message_is_used_once_without_changing_tra
|
||||
# One-shot: subsequent clarification falls back to the normal prompt.
|
||||
assert runtime.transaction_clarification_message(state) != expected
|
||||
assert "transaction_parameter_message_override" not in state
|
||||
|
||||
class _TemporalFallbackRuntime(AgentRuntimeMixin):
|
||||
def __init__(self):
|
||||
from types import SimpleNamespace
|
||||
self.calls = []
|
||||
self.llm = None
|
||||
self.tool_router = SimpleNamespace(
|
||||
registry=SimpleNamespace(
|
||||
get_tool=lambda name: SimpleNamespace(
|
||||
requires=["subject"],
|
||||
args_schema={"subject": {"type": "string", "description": "Nome do serviço ou VAS alvo do cancelamento."}},
|
||||
description="Cancela o serviço VAS selecionado pelo cliente.",
|
||||
confirmation_required=True,
|
||||
tool_type="transactional",
|
||||
)
|
||||
),
|
||||
resolve_execution_policy=lambda name, arguments=None: {
|
||||
"operation_type": "transactional",
|
||||
"require_confirmation": True,
|
||||
"requires": ["subject"],
|
||||
"pre_validation": {"enabled": True, "tool": "validar_vas_subject", "fail_open": False},
|
||||
},
|
||||
)
|
||||
|
||||
async def _extract_transaction_parameters_current_only(self, state, *, tool_name, missing_parameters, known_arguments=None):
|
||||
# Simula o collector tradicional interpretando literalmente a fala atual.
|
||||
return {"subject": "dezenove e noventa e nove"}
|
||||
|
||||
async def _reconcile_transaction_parameters(self, state, *, tool_name, parameter_names, known_arguments=None):
|
||||
# Simula o reconciliador temporal usando tools.yaml + contexto newest->oldest.
|
||||
return {
|
||||
"values": {"subject": "Tamboro Mensal"},
|
||||
"decisions": {"subject": "resolved"},
|
||||
"provenance": {"subject": "history:2"},
|
||||
"clear_fields": [],
|
||||
}
|
||||
|
||||
async def _call_mcp_tool(self, tool_name, arguments, state):
|
||||
self.calls.append((tool_name, dict(arguments)))
|
||||
if tool_name == "validar_vas_subject":
|
||||
subject = arguments.get("subject")
|
||||
if subject == "Tamboro Mensal":
|
||||
return {"ok": True, "tool_name": tool_name, "result": {"eligible": True, "status": "ELIGIBLE"}}
|
||||
return {"ok": True, "tool_name": tool_name, "result": {
|
||||
"eligible": False,
|
||||
"status": "NEEDS_PARAMETER",
|
||||
"parameter": "subject",
|
||||
"reason": "subject_not_resolved",
|
||||
}}
|
||||
return {"ok": True, "tool_name": tool_name, "result": {"status": "OK"}}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_reconciler_is_fallback_after_traditional_candidate_fails_prevalidation():
|
||||
runtime = _TemporalFallbackRuntime()
|
||||
state = {
|
||||
"user_text": "é a de dezenove e noventa e nove",
|
||||
"sanitized_input": "é a de dezenove e noventa e nove",
|
||||
"route": "contestacao_agent",
|
||||
"intent": "state:COLLECTING_CONTESTACAO_PARAMETERS",
|
||||
"transaction_status": "COLLECTING_PARAMETERS",
|
||||
"selected_tool_call": {"tool_name": "cancelar_vas_avulso", "arguments": {}},
|
||||
"active_transaction": {
|
||||
"transaction_id": "tx-1",
|
||||
"tool_name": "cancelar_vas_avulso",
|
||||
"arguments": {},
|
||||
"status": "COLLECTING_PARAMETERS",
|
||||
"started_from_intent": "contas_vas_cancel",
|
||||
"requires": ["subject"],
|
||||
"parameter_schema": {"subject": {"type": "string", "description": "Nome do serviço ou VAS alvo do cancelamento."}},
|
||||
"tool_description": "Cancela o serviço VAS selecionado pelo cliente.",
|
||||
"parameter_conversational_context": (
|
||||
"user: não reconheço esse Tamboro Mensal na minha fatura\n"
|
||||
"assistant: identifiquei Tamboro Mensal por R$ 14,99 e R$ 19,99"
|
||||
),
|
||||
},
|
||||
"context": {},
|
||||
}
|
||||
|
||||
result = await runtime.execute_tools_for_intent(state, tools=[])
|
||||
|
||||
validations = [(name, args.get("subject")) for name, args in runtime.calls if name == "validar_vas_subject"]
|
||||
assert validations == [
|
||||
("validar_vas_subject", "dezenove e noventa e nove"),
|
||||
("validar_vas_subject", "Tamboro Mensal"),
|
||||
]
|
||||
assert state["transaction_parameter_collection"]["mode"] == "current_turn"
|
||||
assert state["transaction_parameter_reconciliation"]["trigger"] == "prevalidation_needs_parameter"
|
||||
assert state["pending_tool_call"]["arguments"]["subject"] == "Tamboro Mensal"
|
||||
assert state["transaction_status"] == "AWAITING_CONFIRMATION"
|
||||
assert result[-1]["awaiting_confirmation"] is True
|
||||
|
||||
Reference in New Issue
Block a user