You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
orquestrador/tests/test_turn_decision_contract.py

5804 lines
244 KiB
Python

import os
import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
os.environ.setdefault("DEBUG", "false")
from datetime import datetime, timedelta
from app.core.time_utils import utc_now
from app.db.mock_database import MockBase
from app.db.mock_models import RentalContract, RentalPayment, RentalVehicle
from app.services.integrations.events import ORDER_CREATED_EVENT
from app.services.orchestration.conversation_policy import ConversationPolicy
from app.services.orchestration.entity_normalizer import EntityNormalizer
from app.services.orchestration.message_planner import MessagePlanner
from app.services.orchestration.orquestrador_service import OrquestradorService
from app.services.orchestration.tool_executor import ToolExecutor
class FakeLLM:
def __init__(self, responses):
self.responses = list(responses)
self.calls = 0
async def generate_response(self, message: str, tools):
self.calls += 1
if self.responses:
return self.responses.pop(0)
return {"response": "", "tool_call": None}
class FakeState:
def __init__(self, entries=None, contexts=None):
self.entries = entries or {}
self.contexts = contexts or {}
def get_entry(self, bucket: str, user_id: int | None, *, expire: bool = False):
if user_id is None:
return None
return self.entries.get(bucket, {}).get(user_id)
def set_entry(self, bucket: str, user_id: int | None, value: dict):
if user_id is None:
return
self.entries.setdefault(bucket, {})[user_id] = value
def pop_entry(self, bucket: str, user_id: int | None):
if user_id is None:
return None
return self.entries.get(bucket, {}).pop(user_id, None)
def get_user_context(self, user_id: int | None):
if user_id is None:
return None
return self.contexts.get(user_id)
def save_user_context(self, user_id: int | None, context: dict):
if user_id is None:
return
self.contexts[user_id] = context
class FakeToolExecutor:
def __init__(self, result=None):
self.result = result or {"ok": True}
self.calls = []
async def execute(self, tool_name: str, arguments: dict, user_id: int | None = None):
self.calls.append((tool_name, arguments, user_id))
if tool_name == "consultar_estoque" and arguments.get("preco_max") and float(arguments["preco_max"]) > 50000:
return [
{"id": 7, "modelo": "Hyundai HB20 2022", "categoria": "hatch", "preco": 54500.0},
{"id": 8, "modelo": "Chevrolet Onix 2023", "categoria": "hatch", "preco": 58900.0},
]
return self.result
def coerce_http_error(self, exc):
detail = exc.detail if isinstance(exc.detail, dict) else {}
return {
"code": detail.get("code", "tool_error"),
"message": detail.get("message", str(exc)),
"retryable": bool(detail.get("retryable", False)),
"field": detail.get("field"),
}
class StaticToolRegistry:
def __init__(self, result=None):
self.result = result if result is not None else {"ok": True}
self.calls = []
async def execute(self, tool_name: str, arguments: dict, user_id: int | None = None):
self.calls.append((tool_name, arguments, user_id))
return self.result
class FakePolicyService:
def __init__(self, state):
self.state = state
self.normalizer = EntityNormalizer()
def _get_user_context(self, user_id: int | None):
if user_id is None:
return None
return self.state.contexts.get(user_id)
def _save_user_context(self, user_id: int | None, context: dict | None) -> None:
if user_id is None or not isinstance(context, dict):
return
self.state.save_user_context(user_id, context)
def _new_tab_memory(self, user_id: int | None):
return {}
def _coerce_extraction_contract(self, payload):
return payload if isinstance(payload, dict) else self.normalizer.empty_extraction_payload()
def _is_affirmative_message(self, text: str) -> bool:
normalized = self.normalizer.normalize_text(text).strip().rstrip(".!?,;:")
return normalized in {"sim", "pode", "ok", "confirmo", "aceito", "fechado", "pode sim"}
def _is_negative_message(self, text: str) -> bool:
normalized = self.normalizer.normalize_text(text).strip().rstrip(".!?,;:")
return normalized in {"nao", "nao quero"} or normalized.startswith("nao")
def _clear_user_conversation_state(self, user_id: int | None) -> None:
context = self._get_user_context(user_id)
if context:
context["pending_order_selection"] = None
async def handle_message(self, message: str, user_id: int | None = None) -> str:
return f"handled:{message}"
def _render_missing_review_fields_prompt(self, missing_fields: list[str]) -> str:
return "missing review"
def _render_missing_review_reschedule_fields_prompt(self, missing_fields: list[str]) -> str:
return "missing review reschedule"
def _render_missing_review_cancel_fields_prompt(self, missing_fields: list[str]) -> str:
return "missing review cancel"
def _render_review_reuse_question(self) -> str:
return "reuse review?"
def _render_missing_order_fields_prompt(self, missing_fields: list[str]) -> str:
return "missing order"
def _render_missing_cancel_order_fields_prompt(self, missing_fields: list[str]) -> str:
return "missing cancel order"
class TurnDecisionContractTests(unittest.IsolatedAsyncioTestCase):
async def test_extract_turn_decision_retries_once_and_returns_structured_payload(self):
llm = FakeLLM(
[
{"response": "nao eh json", "tool_call": None},
{
"response": """
{
"intent": "review_schedule",
"domain": "review",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {},
"review_fields": {"placa": "abc1234", "data_hora": "10/03/2026 às 09:00"},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {}
},
"missing_fields": ["modelo", "ano", "km"],
"tool_name": null,
"tool_arguments": {},
"response_to_user": "Preciso do modelo, ano e quilometragem."
}
""",
"tool_call": None,
},
]
)
planner = MessagePlanner(llm=llm, normalizer=EntityNormalizer())
decision = await planner.extract_turn_decision("Quero agendar revisão amanhã às 09:00", user_id=7)
self.assertEqual(llm.calls, 2)
self.assertEqual(decision["intent"], "review_schedule")
self.assertEqual(decision["domain"], "review")
self.assertEqual(decision["action"], "ask_missing_fields")
self.assertEqual(decision["entities"]["review_fields"]["placa"], "ABC1234")
self.assertEqual(decision["entities"]["review_fields"]["data_hora"], "10/03/2026 às 09:00")
self.assertEqual(decision["missing_fields"], ["modelo", "ano", "km"])
async def test_extract_turn_bundle_retries_once_and_returns_structured_payload(self):
llm = FakeLLM(
[
{"response": "nao eh json", "tool_call": None},
{
"response": """
{
"turn_decision": {
"intent": "order_create",
"domain": "sales",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {}
},
"missing_fields": ["modelo_veiculo"],
"tool_name": null,
"tool_arguments": {},
"response_to_user": "Qual veículo você quer comprar?"
},
"message_plan": {
"orders": [
{
"domain": "sales",
"message": "Quero comprar um carro até 70 mil",
"entities": {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {"order_create": true}
}
}
]
}
}
""",
"tool_call": None,
},
]
)
planner = MessagePlanner(llm=llm, normalizer=EntityNormalizer())
bundle = await planner.extract_turn_bundle("Quero comprar um carro até 70 mil", user_id=7)
self.assertEqual(llm.calls, 2)
self.assertTrue(bundle["has_turn_decision"])
self.assertTrue(bundle["has_message_plan"])
self.assertEqual(bundle["turn_decision"]["intent"], "order_create")
self.assertEqual(bundle["turn_decision"]["domain"], "sales")
self.assertEqual(bundle["turn_decision"]["entities"]["generic_memory"]["orcamento_max"], 70000)
self.assertEqual(bundle["message_plan"]["orders"][0]["domain"], "sales")
self.assertEqual(bundle["message_plan"]["orders"][0]["message"], "Quero comprar um carro até 70 mil")
async def test_extract_turn_bundle_returns_partial_payload_without_retry_when_first_response_is_useful(self):
llm = FakeLLM(
[
{
"response": """
{
"turn_decision": {
"intent": "order_create",
"domain": "sales",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {}
},
"missing_fields": ["modelo_veiculo"],
"tool_name": null,
"tool_arguments": {},
"response_to_user": "Qual veiculo voce quer comprar?"
},
"message_plan": {}
}
""",
"tool_call": None,
},
{"response": "nao deveria chamar de novo", "tool_call": None},
]
)
planner = MessagePlanner(llm=llm, normalizer=EntityNormalizer())
bundle = await planner.extract_turn_bundle("Quero comprar um carro ate 70 mil", user_id=7)
self.assertEqual(llm.calls, 1)
self.assertTrue(bundle["has_turn_decision"])
self.assertFalse(bundle["has_message_plan"])
self.assertEqual(bundle["turn_decision"]["intent"], "order_create")
self.assertEqual(bundle["turn_decision"]["entities"]["generic_memory"]["orcamento_max"], 70000)
def test_parse_json_object_accepts_python_style_dict_with_trailing_commas(self):
normalizer = EntityNormalizer()
payload = normalizer.parse_json_object(
"""
```json
{
'intent': 'review_schedule',
'domain': 'review',
'action': 'answer_user',
}
```
"""
)
self.assertEqual(payload["intent"], "review_schedule")
self.assertEqual(payload["domain"], "review")
self.assertEqual(payload["action"], "answer_user")
def test_coerce_turn_decision_maps_top_level_aliases_and_embedded_intents(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"domain": "service",
"action": "answer",
"response": "Certo, vou seguir com a revisao.",
"selected_index": "2",
"entities": {
"generic_memory": {"cpf": "12345678909"},
"review_fields": {"placa": "abc1234"},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {"review_schedule": True},
},
}
)
self.assertEqual(decision["intent"], "review_schedule")
self.assertEqual(decision["domain"], "review")
self.assertEqual(decision["action"], "answer_user")
self.assertEqual(decision["response_to_user"], "Certo, vou seguir com a revisao.")
self.assertEqual(decision["selection_index"], 2)
self.assertEqual(decision["entities"]["review_fields"]["placa"], "ABC1234")
self.assertEqual(decision["entities"]["generic_memory"]["cpf"], "12345678909")
def test_coerce_turn_decision_rejects_invalid_shape_with_fallback(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "valor_invalido",
"domain": "sales",
"action": "call_tool",
"entities": [],
}
)
self.assertEqual(decision["intent"], "general")
self.assertEqual(decision["domain"], "general")
self.assertEqual(decision["action"], "answer_user")
self.assertEqual(decision["entities"]["order_fields"], {})
def test_coerce_turn_decision_maps_order_aliases_from_model(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "place_order",
"domain": "sales",
"action": "answer_user",
"entities": {
"generic_memory": {"orcamento_max": "70000", "cpf": "12345678909"},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
}
)
self.assertEqual(decision["intent"], "order_create")
self.assertEqual(decision["domain"], "sales")
self.assertEqual(decision["action"], "answer_user")
self.assertEqual(decision["entities"]["generic_memory"]["orcamento_max"], 70000)
self.assertEqual(decision["entities"]["generic_memory"]["cpf"], "12345678909")
def test_coerce_turn_decision_converts_vehicle_alias_missing_field_into_order_collection(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "create_order",
"domain": "sales",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {"cpf": "12345678909"},
"cancel_order_fields": {},
},
"missing_fields": ["modelo_carro"],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": "Certo! Para qual modelo de carro voce gostaria de um orcamento de 70 mil?",
}
)
self.assertEqual(decision["intent"], "order_create")
self.assertEqual(decision["action"], "collect_order_create")
self.assertEqual(decision["missing_fields"], [])
self.assertIsNone(decision["response_to_user"])
def test_coerce_turn_decision_normalizes_cancel_order_tool_argument_aliases(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "order_cancel",
"domain": "sales",
"action": "call_tool",
"tool_name": "cancelar_pedido",
"tool_arguments": {
"order_id": "PED-20260310113756-DC1540",
"reason": "desisti da compra",
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"response_to_user": None,
}
)
self.assertEqual(decision["tool_arguments"]["numero_pedido"], "PED-20260310113756-DC1540")
self.assertEqual(decision["tool_arguments"]["motivo"], "desisti da compra")
def test_coerce_turn_decision_normalizes_order_tool_name_alias_and_downgrades_incomplete_call(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "order_create",
"domain": "sales",
"action": "call_tool",
"tool_name": "fazer_pedido",
"tool_arguments": {
"modelo": "Onix 2024",
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"response_to_user": None,
}
)
self.assertEqual(decision["action"], "collect_order_create")
self.assertIsNone(decision["tool_name"])
self.assertEqual(decision["tool_arguments"], {})
self.assertEqual(decision["entities"]["order_fields"]["modelo_veiculo"], "Onix 2024")
def test_coerce_turn_decision_normalizes_review_tool_name_alias(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "review_schedule",
"domain": "review",
"action": "call_tool",
"tool_name": "marcar_revisao",
"tool_arguments": {
"placa": "ABC1234",
"data_hora": "19/03/2026 09:00",
"modelo": "Corolla",
"ano": 2020,
"km": 30000,
"revisao_previa_concessionaria": True,
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"response_to_user": None,
}
)
self.assertEqual(decision["tool_name"], "agendar_revisao")
self.assertEqual(decision["tool_arguments"]["placa"], "ABC1234")
def test_coerce_turn_decision_normalizes_legacy_review_vehicle_tool_alias(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "review_schedule",
"domain": "review",
"action": "call_tool",
"tool_name": "agendar_revisao_veiculo",
"tool_arguments": {
"placa_veiculo": "ABC1234",
"data_agendamento": "tomorrow",
"horario_agendamento": "14:00",
"modelo_veiculo": "Onix",
"ano_veiculo": 2024,
"quilometragem": 12000,
"revisao_previa": False,
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"response_to_user": None,
}
)
self.assertEqual(decision["tool_name"], "agendar_revisao")
self.assertEqual(decision["tool_arguments"]["placa"], "ABC1234")
self.assertEqual(decision["tool_arguments"]["modelo"], "Onix")
self.assertEqual(decision["tool_arguments"]["ano"], 2024)
self.assertEqual(decision["tool_arguments"]["km"], 12000)
self.assertFalse(decision["tool_arguments"]["revisao_previa_concessionaria"])
self.assertTrue(decision["tool_arguments"]["data_hora"].endswith("14:00"))
def test_coerce_turn_decision_normalizes_review_schedule_tool_argument_aliases(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "review_schedule",
"domain": "review",
"action": "call_tool",
"tool_name": "agendar_revisao",
"tool_arguments": {
"placa_veiculo": "ABC1234",
"data": "20/03/2026 09:00",
"modelo_veiculo": "Corolla",
"ano_veiculo": 2020,
"quilometragem": 30000,
"revisao_previa": True,
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"response_to_user": None,
}
)
self.assertEqual(decision["tool_arguments"]["placa"], "ABC1234")
self.assertEqual(decision["tool_arguments"]["data_hora"], "20/03/2026 09:00")
self.assertEqual(decision["tool_arguments"]["modelo"], "Corolla")
self.assertEqual(decision["tool_arguments"]["ano"], 2020)
self.assertEqual(decision["tool_arguments"]["km"], 30000)
self.assertTrue(decision["tool_arguments"]["revisao_previa_concessionaria"])
def test_coerce_turn_decision_downgrades_incomplete_review_schedule_tool_call_to_collection(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "review_schedule",
"domain": "review",
"action": "call_tool",
"tool_name": "agendar_revisao",
"tool_arguments": {
"placa_veiculo": "ABC1234",
"modelo_veiculo": "Corolla",
"ano_veiculo": 2020,
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"response_to_user": None,
}
)
self.assertEqual(decision["action"], "collect_review_schedule")
self.assertIsNone(decision["tool_name"])
self.assertEqual(decision["tool_arguments"], {})
self.assertEqual(decision["entities"]["review_fields"]["placa"], "ABC1234")
self.assertEqual(decision["entities"]["review_fields"]["modelo"], "Corolla")
self.assertEqual(decision["entities"]["review_fields"]["ano"], 2020)
def test_coerce_turn_decision_downgrades_incomplete_review_schedule_tool_call_even_with_general_domain(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "review_schedule",
"domain": "general",
"action": "call_tool",
"tool_name": "agendar_revisao",
"tool_arguments": {
"placa_veiculo": "ABC1234",
"modelo_veiculo": "Corolla",
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"response_to_user": None,
}
)
self.assertEqual(decision["action"], "collect_review_schedule")
self.assertIsNone(decision["tool_name"])
self.assertEqual(decision["tool_arguments"], {})
self.assertEqual(decision["entities"]["review_fields"]["placa"], "ABC1234")
self.assertEqual(decision["entities"]["review_fields"]["modelo"], "Corolla")
def test_coerce_turn_decision_normalizes_review_listing_tool_arguments(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "review_list",
"domain": "review",
"action": "call_tool",
"tool_name": "listar_agendamentos",
"tool_arguments": {
"vehicle_plate": "abc1234",
"schedule_status": "agendado",
"limit": 10,
"tipo": "revisao",
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"response_to_user": None,
}
)
self.assertEqual(decision["tool_name"], "listar_agendamentos_revisao")
self.assertEqual(
decision["tool_arguments"],
{"placa": "ABC1234", "status": "agendado", "limite": 10},
)
def test_coerce_turn_decision_normalizes_review_management_tool_name_alias(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "review_cancel",
"domain": "review",
"action": "call_tool",
"tool_name": "cancelar_agendamento",
"tool_arguments": {
"protocolo": "REV-20260313-F754AF27",
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"response_to_user": None,
}
)
self.assertEqual(decision["tool_name"], "cancelar_agendamento_revisao")
def test_coerce_turn_decision_downgrades_incomplete_cancel_order_tool_call_to_collection(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "order_cancel",
"domain": "sales",
"action": "call_tool",
"tool_name": "cancelar_pedido",
"tool_arguments": {
"order_id": "PED-20260310124202-5EF4E9",
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"response_to_user": None,
}
)
self.assertEqual(decision["action"], "collect_order_cancel")
self.assertIsNone(decision["tool_name"])
self.assertEqual(decision["tool_arguments"], {})
self.assertEqual(decision["entities"]["cancel_order_fields"]["numero_pedido"], "PED-20260310124202-5EF4E9")
def test_coerce_turn_decision_downgrades_missing_response_ask_missing_fields_to_collection(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "review_schedule",
"domain": "review",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {},
"review_fields": {"placa": "ABC1234"},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": ["data e hora", "modelo"],
"tool_name": None,
"tool_arguments": {},
"response_to_user": "",
}
)
self.assertEqual(decision["intent"], "review_schedule")
self.assertEqual(decision["action"], "collect_review_schedule")
self.assertEqual(decision["missing_fields"], ["data_hora", "modelo"])
self.assertEqual(decision["entities"]["review_fields"]["placa"], "ABC1234")
def test_coerce_turn_decision_downgrades_call_tool_without_tool_name_to_cancel_order_collection(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "order_cancel",
"domain": "sales",
"action": "call_tool",
"arguments": {
"order_id": "PED-20260310124202-5EF4E9",
"reason": "desisti da compra",
},
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
}
)
self.assertEqual(decision["intent"], "order_cancel")
self.assertEqual(decision["action"], "collect_order_cancel")
self.assertIsNone(decision["tool_name"])
self.assertEqual(decision["tool_arguments"], {})
self.assertEqual(decision["entities"]["cancel_order_fields"]["numero_pedido"], "PED-20260310124202-5EF4E9")
self.assertEqual(decision["entities"]["cancel_order_fields"]["motivo"], "desisti da compra")
def test_turn_decision_entities_do_not_rebuild_legacy_intents(self):
service = OrquestradorService.__new__(OrquestradorService)
service.normalizer = EntityNormalizer()
extracted = service._extracted_entities_from_turn_decision(
{
"intent": "order_create",
"domain": "sales",
"action": "collect_order_create",
"entities": {
"generic_memory": {"cpf": "12345678909"},
"review_fields": {},
"review_management_fields": {},
"order_fields": {"vehicle_id": 1},
"cancel_order_fields": {},
},
}
)
self.assertEqual(extracted["intents"], {})
self.assertEqual(extracted["order_fields"]["vehicle_id"], 1)
def test_turn_decision_entity_merge_preserves_generic_memory_from_previous_extraction(self):
service = OrquestradorService.__new__(OrquestradorService)
service.normalizer = EntityNormalizer()
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
merged = service._merge_extracted_entities(
{
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {"cpf": "12345678909"},
"cancel_order_fields": {},
"intents": {},
},
{
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
},
)
self.assertEqual(merged["generic_memory"]["orcamento_max"], 70000)
self.assertEqual(merged["order_fields"]["cpf"], "12345678909")
def test_capture_successful_review_tool_side_effects_store_last_review_package_for_direct_tool_call(self):
state = FakeState(
contexts={
7: {
"active_domain": "review",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
service._capture_successful_tool_side_effects(
tool_name="agendar_revisao",
arguments={
"placa": "ABC1463",
"data_hora": "28/04/2026 15:00",
"modelo": "Civic",
"ano": 2024,
"km": 30000,
"revisao_previa_concessionaria": True,
},
tool_result={
"protocolo": "REV-TESTE-123",
"placa": "ABC1463",
"data_hora": "28/04/2026 15:00",
},
user_id=7,
)
cached = state.get_entry("last_review_packages", 7)
self.assertIsNotNone(cached)
self.assertEqual(cached["payload"]["placa"], "ABC1463")
self.assertEqual(cached["payload"]["modelo"], "Civic")
self.assertTrue(cached["payload"]["revisao_previa_concessionaria"])
async def test_execute_tool_with_trace_normalizes_direct_review_tool_alias_and_merges_open_draft(self):
state = FakeState(
entries={
"pending_review_drafts": {
7: {
"payload": {
"placa": "ABC1463",
"modelo": "Civic",
"ano": 2024,
"km": 30000,
"revisao_previa_concessionaria": False,
},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
7: {
"active_domain": "review",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.tool_executor = FakeToolExecutor(result={"protocolo": "REV-TESTE-123"})
service._log_turn_event = lambda *args, **kwargs: None
result = await service._execute_tool_with_trace(
tool_name="agendar_revisao_veiculo",
arguments={
"data_agendamento": "tomorrow",
"horario_agendamento": "14:00",
},
user_id=7,
)
self.assertEqual(result["protocolo"], "REV-TESTE-123")
self.assertEqual(service.tool_executor.calls[0][0], "agendar_revisao")
self.assertEqual(service.tool_executor.calls[0][2], 7)
self.assertEqual(service.tool_executor.calls[0][1]["placa"], "ABC1463")
self.assertEqual(service.tool_executor.calls[0][1]["modelo"], "Civic")
self.assertEqual(service.tool_executor.calls[0][1]["ano"], 2024)
self.assertEqual(service.tool_executor.calls[0][1]["km"], 30000)
self.assertFalse(service.tool_executor.calls[0][1]["revisao_previa_concessionaria"])
self.assertTrue(service.tool_executor.calls[0][1]["data_hora"].endswith("14:00"))
def test_capture_tool_result_context_stores_pending_stock_selection_entry(self):
state = FakeState(
contexts={
5: {
"active_domain": "sales",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
service._capture_tool_result_context(
tool_name="consultar_estoque",
tool_result=[
{"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True},
{"id": 11, "modelo": "Toyota Corolla 2024", "categoria": "suv", "preco": 76087.0},
],
user_id=5,
)
cached = state.get_entry("pending_stock_selections", 5)
self.assertIsNotNone(cached)
self.assertEqual(cached["payload"][0]["id"], 15)
self.assertEqual(cached["payload"][1]["modelo"], "Toyota Corolla 2024")
self.assertTrue(cached["payload"][0]["budget_relaxed"])
self.assertFalse(cached["payload"][1]["budget_relaxed"])
def test_entity_merge_can_enrich_message_plan_with_full_extraction(self):
service = OrquestradorService.__new__(OrquestradorService)
service.normalizer = EntityNormalizer()
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
merged = service._merge_extracted_entities(
{
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {"cpf": "12345678909"},
"cancel_order_fields": {},
"intents": {},
},
{
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
},
)
self.assertEqual(merged["generic_memory"]["orcamento_max"], 70000)
self.assertEqual(merged["order_fields"]["cpf"], "12345678909")
async def test_missing_sales_search_context_uses_deterministic_enrichment_before_llm(self):
service = OrquestradorService.__new__(OrquestradorService)
service.normalizer = EntityNormalizer()
service._log_turn_event = lambda *args, **kwargs: None
async def should_not_run(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar LLM extra para filtros de compra explicitos")
service._extract_sales_search_context_with_llm = should_not_run
result = await service._extract_missing_sales_search_context_with_llm(
message="Quero comprar um carro hatch de 70 mil, meu CPF e 12345678909",
user_id=7,
turn_decision={"domain": "sales", "intent": "order_create", "action": "collect_order_create"},
extracted_entities={
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {"cpf": "12345678909"},
"cancel_order_fields": {},
"intents": {},
},
)
self.assertEqual(result["orcamento_max"], 70000)
self.assertEqual(result["perfil_veiculo"], ["hatch"])
async def test_orchestration_control_router_is_skipped_when_turn_decision_is_already_operational(self):
service = OrquestradorService.__new__(OrquestradorService)
service.normalizer = EntityNormalizer()
async def should_not_run(**kwargs):
raise AssertionError("nao deveria consultar orchestration_router quando a decisao ja for operacional")
service._call_llm_with_trace = should_not_run
async def finish(response: str, queue_notice: str | None = None) -> str:
return response
result = await service._try_execute_orchestration_control_tool(
message="Quero comprar um carro ate 70 mil",
user_id=7,
turn_decision={"intent": "order_create", "domain": "sales", "action": "ask_missing_fields"},
extracted_entities={},
queue_notice=None,
finish=finish,
)
self.assertIsNone(result)
async def test_try_execute_orchestration_control_tool_returns_business_tool_call_for_reuse(self):
service = OrquestradorService.__new__(OrquestradorService)
service.normalizer = EntityNormalizer()
service.policy = SimpleNamespace(should_defer_flow_cancellation_control=lambda **kwargs: False)
class DummyRegistry:
def get_tools(self):
return []
service.registry = DummyRegistry()
service._build_router_prompt = lambda user_message, user_id=None: user_message
service._is_low_value_response = lambda text: False
service._has_open_flow = lambda user_id, domain: False
service._get_user_context = lambda user_id: {}
async def fake_call_llm_with_trace(**kwargs):
return {
"response": "",
"tool_call": {
"name": "consultar_estoque",
"arguments": {"preco_max": 80000.0, "limite": 5},
},
}
service._call_llm_with_trace = fake_call_llm_with_trace
async def finish(response: str, queue_notice: str | None = None) -> str:
return response
result = await service._try_execute_orchestration_control_tool(
message="Quero ver carros ate 80000 reais",
user_id=7,
turn_decision={},
extracted_entities={},
queue_notice=None,
finish=finish,
)
self.assertIsInstance(result, dict)
self.assertEqual(result["source"], "orchestration_router")
self.assertEqual(result["llm_result"]["tool_call"]["name"], "consultar_estoque")
async def test_turn_decision_call_tool_executes_without_router(self):
service = OrquestradorService.__new__(OrquestradorService)
service.tool_executor = FakeToolExecutor(result={"numero_pedido": "PED-1", "status": "Ativo"})
service.llm = FakeLLM([])
service._capture_review_confirmation_suggestion = lambda **kwargs: None
service._capture_tool_result_context = lambda **kwargs: None
service._should_use_deterministic_response = lambda tool_name: True
service._fallback_format_tool_result = lambda tool_name, tool_result: f"{tool_name}:{tool_result['numero_pedido']}"
async def fake_render_tool_response_with_fallback(**kwargs):
return f"{kwargs['tool_name']}:{kwargs['tool_result']['numero_pedido']}"
service._render_tool_response_with_fallback = fake_render_tool_response_with_fallback
service._http_exception_detail = lambda exc: str(exc)
service._is_low_value_response = lambda text: False
async def finish(response: str, queue_notice: str | None = None) -> str:
return response if not queue_notice else f"{queue_notice}\n{response}"
response = await service._try_execute_business_tool_from_turn_decision(
message="quero fechar o pedido",
user_id=7,
turn_decision={
"action": "call_tool",
"tool_name": "realizar_pedido",
"tool_arguments": {"cpf": "12345678909", "vehicle_id": 1},
},
queue_notice=None,
finish=finish,
)
self.assertEqual(
service.tool_executor.calls,
[("realizar_pedido", {"cpf": "12345678909", "vehicle_id": 1}, 7)],
)
self.assertEqual(response, "realizar_pedido:PED-1")
self.assertEqual(service.llm.calls, 0)
async def test_turn_decision_cancel_order_uses_deterministic_response_without_result_llm(self):
service = OrquestradorService.__new__(OrquestradorService)
service.tool_executor = FakeToolExecutor(result={"numero_pedido": "PED-1", "status": "Cancelado", "motivo": "desisti"})
service.llm = FakeLLM([])
service._capture_review_confirmation_suggestion = lambda **kwargs: None
service._capture_tool_result_context = lambda **kwargs: None
service._should_use_deterministic_response = lambda tool_name: tool_name == "cancelar_pedido"
service._fallback_format_tool_result = lambda tool_name, tool_result: (
f"Pedido {tool_result['numero_pedido']} atualizado.\nStatus: {tool_result['status']}"
)
service._http_exception_detail = lambda exc: str(exc)
service._is_low_value_response = lambda text: False
async def finish(response: str, queue_notice: str | None = None) -> str:
return response if not queue_notice else f"{queue_notice}\n{response}"
response = await service._try_execute_business_tool_from_turn_decision(
message="cancelar pedido",
user_id=7,
turn_decision={
"action": "call_tool",
"tool_name": "cancelar_pedido",
"tool_arguments": {"numero_pedido": "PED-1", "motivo": "desisti"},
},
queue_notice=None,
finish=finish,
)
self.assertEqual(response, "Pedido PED-1 atualizado.\nStatus: Cancelado")
self.assertEqual(service.llm.calls, 0)
async def test_turn_decision_rental_fleet_listing_uses_deterministic_response_without_result_llm(self):
registry = StaticToolRegistry(
result=[
{"id": 1, "placa": "RAA1A01", "modelo": "Chevrolet Tracker", "categoria": "suv", "ano": 2024, "valor_diaria": 219.9, "status": "disponivel"},
{"id": 2, "placa": "RAA1A02", "modelo": "Fiat Pulse", "categoria": "suv", "ano": 2024, "valor_diaria": 189.9, "status": "disponivel"},
]
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = FakeState()
service.normalizer = EntityNormalizer()
service.tool_executor = ToolExecutor(registry=registry)
service.llm = FakeLLM([])
service._capture_review_confirmation_suggestion = lambda **kwargs: None
service._capture_tool_result_context = lambda **kwargs: None
service._capture_tool_invocation_trace = lambda **kwargs: None
service._log_turn_event = lambda *args, **kwargs: None
async def fake_render_tool_response_with_fallback(**kwargs):
raise AssertionError("nao deveria usar llm para listagem de locacao")
service._render_tool_response_with_fallback = fake_render_tool_response_with_fallback
service._http_exception_detail = lambda exc: str(exc)
service._is_low_value_response = lambda text: False
async def finish(response: str, queue_notice: str | None = None) -> str:
return response if not queue_notice else f"{queue_notice}\n{response}"
response = await service._try_execute_business_tool_from_turn_decision(
message="quais carros estao disponiveis para aluguel",
user_id=7,
turn_decision={
"action": "call_tool",
"tool_name": "consultar_frota_aluguel",
"tool_arguments": {"valor_diaria_max": 220},
},
queue_notice=None,
finish=finish,
)
self.assertEqual(registry.calls[0][0], "consultar_frota_aluguel")
self.assertEqual(registry.calls[0][2], 7)
self.assertIn("Encontrei 2 veiculo(s) para locacao:", response)
self.assertIn("RAA1A01", response)
self.assertIn("numero da lista, a placa ou o modelo", response)
self.assertEqual(service.llm.calls, 0)
async def test_turn_decision_rental_fleet_listing_seeds_pending_draft_from_message(self):
registry = StaticToolRegistry(
result=[
{"id": 1, "placa": "RAA1A01", "modelo": "Chevrolet Tracker", "categoria": "hatch", "ano": 2024, "valor_diaria": 219.9, "status": "disponivel"},
{"id": 2, "placa": "RAA1A02", "modelo": "Fiat Pulse", "categoria": "hatch", "ano": 2024, "valor_diaria": 189.9, "status": "disponivel"},
]
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = FakeState(
contexts={
7: {
"active_domain": "general",
"active_task": None,
"generic_memory": {},
"shared_memory": {},
"collected_slots": {},
"flow_snapshots": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": None,
}
}
)
service.normalizer = EntityNormalizer()
service.tool_executor = ToolExecutor(registry=registry)
service.llm = FakeLLM([])
service._rental_now_provider = lambda: datetime(2026, 3, 19, 9, 0)
service._capture_review_confirmation_suggestion = lambda **kwargs: None
service._capture_tool_invocation_trace = lambda **kwargs: None
service._log_turn_event = lambda *args, **kwargs: None
async def fake_render_tool_response_with_fallback(**kwargs):
raise AssertionError("nao deveria usar llm para listagem de locacao")
service._render_tool_response_with_fallback = fake_render_tool_response_with_fallback
service._http_exception_detail = lambda exc: str(exc)
service._is_low_value_response = lambda text: False
async def finish(response: str, queue_notice: str | None = None) -> str:
return response if not queue_notice else f"{queue_notice}\n{response}"
response = await service._try_execute_business_tool_from_turn_decision(
message="Quero alugar um hatch amanha 10h ate depois de amanha 10h",
user_id=7,
turn_decision={
"action": "call_tool",
"tool_name": "consultar_frota_aluguel",
"tool_arguments": {"status": "disponivel"},
},
queue_notice=None,
finish=finish,
)
self.assertIn("veiculo(s) para locacao", response)
draft = service.state.get_entry("pending_rental_drafts", 7)
self.assertIsNotNone(draft)
self.assertEqual(draft["payload"]["categoria"], "hatch")
self.assertEqual(draft["payload"]["data_inicio"], "20/03/2026 10:00")
self.assertEqual(draft["payload"]["data_fim_prevista"], "21/03/2026 10:00")
pending_selection = service.state.get_entry("pending_rental_selections", 7)
self.assertIsNotNone(pending_selection)
self.assertEqual(pending_selection["search_payload"]["categoria"], "hatch")
self.assertEqual(pending_selection["search_payload"]["data_inicio"], "20/03/2026 10:00")
self.assertEqual(pending_selection["search_payload"]["data_fim_prevista"], "21/03/2026 10:00")
context = service.state.get_user_context(7)
self.assertEqual(context["active_domain"], "rental")
self.assertEqual(context["active_task"], "rental_create")
self.assertEqual(context["last_rental_search_payload"]["categoria"], "hatch")
self.assertEqual(context["last_rental_search_payload"]["data_inicio"], "20/03/2026 10:00")
self.assertEqual(context["last_rental_search_payload"]["data_fim_prevista"], "21/03/2026 10:00")
async def test_confirm_pending_review_clears_open_review_draft_after_suggested_time_success(self):
state = FakeState(
entries={
"pending_review_drafts": {
7: {
"payload": {
"placa": "ABC1C23",
"modelo": "Onix",
"ano": 2024,
"km": 20000,
"revisao_previa_concessionaria": False,
},
"expires_at": utc_now() + timedelta(minutes=15),
}
},
"pending_review_confirmations": {
7: {
"payload": {
"placa": "ABC1C23",
"data_hora": "14/03/2026 16:30",
"modelo": "Onix",
"ano": 2024,
"km": 20000,
"revisao_previa_concessionaria": False,
},
"expires_at": utc_now() + timedelta(minutes=15),
}
},
},
contexts={
7: {
"active_domain": "review",
"active_task": "review_schedule",
"generic_memory": {"placa": "ABC1C23"},
"shared_memory": {"placa": "ABC1C23"},
"collected_slots": {
"review_schedule": {
"placa": "ABC1C23",
"modelo": "Onix",
"ano": 2024,
}
},
"flow_snapshots": {
"review_schedule": {
"payload": {
"placa": "ABC1C23",
"modelo": "Onix",
"ano": 2024,
},
"expires_at": utc_now() + timedelta(minutes=15),
},
"review_confirmation": {
"payload": {
"placa": "ABC1C23",
"data_hora": "14/03/2026 16:30",
},
"expires_at": utc_now() + timedelta(minutes=15),
},
},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
},
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.tool_executor = FakeToolExecutor(
result={
"protocolo": "REV-TESTE-999",
"placa": "ABC1C23",
"data_hora": "14/03/2026 16:30",
"valor_revisao": 728.0,
}
)
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
service._http_exception_detail = lambda exc: str(exc)
service._fallback_format_tool_result = lambda tool_name, tool_result: (
f"Revisao agendada com sucesso.\nProtocolo: {tool_result['protocolo']}"
)
response = await service._try_confirm_pending_review(
message="sim",
user_id=7,
extracted_review_fields={},
)
self.assertIn("REV-TESTE-999", response)
self.assertIsNone(state.get_entry("pending_review_confirmations", 7))
self.assertIsNone(state.get_entry("pending_review_drafts", 7))
self.assertIsNotNone(state.get_entry("last_review_packages", 7))
context = state.get_user_context(7)
self.assertEqual(context["active_task"], None)
self.assertEqual(context["collected_slots"], {})
self.assertEqual(context["flow_snapshots"], {})
async def test_try_confirm_pending_review_executes_pending_reschedule_confirmation(self):
state = FakeState(
entries={
"pending_review_confirmations": {
7: {
"tool_name": "editar_data_revisao",
"payload": {
"protocolo": "REV-TESTE-321",
"nova_data_hora": "14/03/2026 16:30",
},
"expires_at": utc_now() + timedelta(minutes=15),
}
},
"pending_review_management_drafts": {
7: {
"action": "reschedule",
"payload": {"protocolo": "REV-TESTE-321"},
"expires_at": utc_now() + timedelta(minutes=15),
}
},
},
contexts={
7: {
"active_domain": "review",
"active_task": "review_management",
"generic_memory": {},
"shared_memory": {},
"collected_slots": {
"review_management": {"protocolo": "REV-TESTE-321"},
},
"flow_snapshots": {
"review_management": {
"payload": {"protocolo": "REV-TESTE-321"},
"expires_at": utc_now() + timedelta(minutes=15),
},
"review_confirmation": {
"payload": {
"protocolo": "REV-TESTE-321",
"nova_data_hora": "14/03/2026 16:30",
},
"expires_at": utc_now() + timedelta(minutes=15),
},
},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
},
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.tool_executor = FakeToolExecutor(
result={
"protocolo": "REV-TESTE-321",
"placa": "ABC1C23",
"data_hora": "14/03/2026 16:30",
"status": "Remarcado",
}
)
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
service._http_exception_detail = lambda exc: str(exc)
service._fallback_format_tool_result = lambda tool_name, tool_result: (
f"Agendamento atualizado.\nProtocolo: {tool_result['protocolo']}"
)
response = await service._try_confirm_pending_review(
message="sim",
user_id=7,
extracted_review_fields={},
)
self.assertIn("REV-TESTE-321", response)
self.assertEqual(
service.tool_executor.calls,
[("editar_data_revisao", {"protocolo": "REV-TESTE-321", "nova_data_hora": "14/03/2026 16:30"}, 7)],
)
self.assertIsNone(state.get_entry("pending_review_confirmations", 7))
self.assertIsNone(state.get_entry("pending_review_management_drafts", 7))
self.assertIsNone(state.get_entry("last_review_packages", 7))
context = state.get_user_context(7)
self.assertEqual(context["active_task"], None)
self.assertEqual(context["collected_slots"], {})
self.assertEqual(context["flow_snapshots"], {})
async def test_empty_stock_search_suggests_nearby_options(self):
service = OrquestradorService.__new__(OrquestradorService)
service.normalizer = EntityNormalizer()
service.tool_executor = FakeToolExecutor(result=[])
service._get_user_context = lambda user_id: {
"generic_memory": {},
"shared_memory": {},
"last_stock_results": [],
"selected_vehicle": None,
}
service._capture_tool_result_context = lambda tool_name, tool_result, user_id: None
service._normalize_positive_number = service.normalizer.normalize_positive_number
response = await service._maybe_build_stock_suggestion_response(
tool_name="consultar_estoque",
arguments={"preco_max": 50000, "limite": 5},
tool_result=[],
user_id=5,
)
self.assertIn("Nao encontrei veiculos ate R$ 50.000.", response)
self.assertIn("Hyundai HB20 2022", response)
self.assertIn("Se quiser, responda com o numero da lista ou com o modelo.", response)
async def test_turn_decision_answer_user_can_short_circuit_router(self):
decision = {
"intent": "general",
"domain": "general",
"action": "answer_user",
"response_to_user": "Resposta direta do contrato.",
}
self.assertEqual(str(decision.get("action") or ""), "answer_user")
self.assertEqual(str(decision.get("response_to_user") or "").strip(), "Resposta direta do contrato.")
async def test_handle_message_persists_completed_turn_history(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
history_calls = []
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service.history_service = SimpleNamespace(record_turn=lambda **kwargs: history_calls.append(kwargs))
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "general",
"domain": "general",
"action": "answer_user",
"entities": service.normalizer.empty_extraction_payload(),
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": "Resposta direta do contrato.",
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_message_plan(message: str, user_id: int | None):
return {
"orders": [
{
"domain": "general",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
service._extract_message_plan_with_llm = fake_extract_message_plan
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
async def fake_extract_entities(message: str, user_id: int | None):
return service.normalizer.empty_extraction_payload()
service._extract_entities_with_llm = fake_extract_entities
async def fake_extract_missing_sales_search_context_with_llm(**kwargs):
return {}
service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm
service._domain_from_intents = lambda intents: "general"
service._handle_context_switch = lambda **kwargs: None
service._update_active_domain = lambda **kwargs: None
async def fake_try_execute_orchestration_control_tool(**kwargs):
return None
service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool
async def fake_try_execute_business_tool_from_turn_decision(**kwargs):
return None
service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision
async def fake_try_handle_review_management(**kwargs):
return None
service._try_handle_review_management = fake_try_handle_review_management
async def fake_try_confirm_pending_review(**kwargs):
return None
service._try_confirm_pending_review = fake_try_confirm_pending_review
async def fake_try_collect_and_schedule_review(**kwargs):
return None
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
async def fake_try_collect_and_cancel_order(**kwargs):
return None
service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order
async def fake_try_handle_order_listing(**kwargs):
return None
service._try_handle_order_listing = fake_try_handle_order_listing
async def fake_try_collect_and_create_order(**kwargs):
return None
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message(
"ola",
user_id=1,
)
self.assertEqual(response, "Resposta direta do contrato.")
self.assertEqual(len(history_calls), 1)
self.assertEqual(history_calls[0]["user_message"], "ola")
self.assertEqual(history_calls[0]["assistant_response"], "Resposta direta do contrato.")
self.assertEqual(history_calls[0]["turn_status"], "completed")
self.assertEqual(history_calls[0]["intent"], "general")
async def test_handle_message_restores_outer_turn_trace_after_nested_call(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
history_calls = []
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service.history_service = SimpleNamespace(record_turn=lambda **kwargs: history_calls.append(kwargs))
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._upsert_user_context = lambda user_id: None
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
if base_response == "resposta externa":
nested_response = await service.handle_message("mensagem interna", user_id=user_id)
return f"{base_response}\n{nested_response}"
return base_response
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "general",
"domain": "general",
"action": "answer_user",
"entities": service.normalizer.empty_extraction_payload(),
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": "resposta interna" if message == "mensagem interna" else "resposta externa",
}
async def fake_extract_message_plan(message: str, user_id: int | None):
return {"orders": [{"domain": "general", "message": message}]}
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._extract_turn_decision_with_llm = fake_extract_turn_decision
service._extract_message_plan_with_llm = fake_extract_message_plan
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
async def fake_try_continue_queued_order(**kwargs):
return None
async def fake_try_execute_orchestration_control_tool(**kwargs):
return None
async def fake_try_execute_business_tool_from_turn_decision(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
service._try_continue_queued_order = fake_try_continue_queued_order
service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool
service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision
service._handle_context_switch = lambda **kwargs: None
service._update_active_domain = lambda **kwargs: None
async def fake_extract_entities_with_llm(message: str, user_id: int | None):
return service.normalizer.empty_extraction_payload()
async def fake_extract_missing_sales_search_context_with_llm(**kwargs):
return {}
service._extract_entities_with_llm = fake_extract_entities_with_llm
service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm
service._domain_from_intents = lambda intents: "general"
response = await service.handle_message("mensagem externa", user_id=1)
self.assertEqual(response, "resposta externa\nresposta interna")
self.assertEqual(len(history_calls), 2)
self.assertEqual(
{call["user_message"] for call in history_calls},
{"mensagem externa", "mensagem interna"},
)
self.assertEqual(
{call["assistant_response"] for call in history_calls},
{"resposta externa\nresposta interna", "resposta interna"},
)
self.assertEqual(len({call["request_id"] for call in history_calls}), 2)
async def test_handle_message_persists_failed_turn_history(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
history_calls = []
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service.history_service = SimpleNamespace(record_turn=lambda **kwargs: history_calls.append(kwargs))
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
raise RuntimeError("falha controlada no turno")
service._extract_turn_decision_with_llm = fake_extract_turn_decision
with self.assertRaises(RuntimeError):
await service.handle_message(
"ola",
user_id=1,
)
self.assertEqual(len(history_calls), 1)
self.assertEqual(history_calls[0]["user_message"], "ola")
self.assertIsNone(history_calls[0]["assistant_response"])
self.assertEqual(history_calls[0]["turn_status"], "failed")
self.assertIn("RuntimeError", history_calls[0]["error_detail"])
self.assertIn("falha controlada no turno", history_calls[0]["error_detail"])
def test_log_turn_event_masks_sensitive_payload(self):
service = OrquestradorService.__new__(OrquestradorService)
service._turn_trace = {
"request_id": "req-1",
"conversation_id": "user:7",
}
with patch("app.services.orchestration.orquestrador_service.logger.info") as logger_info:
service._log_turn_event(
"tool_completed",
message="Meu cpf 12345678909 e a placa ABC1D23.",
arguments={
"cpf": "12345678909",
"placa": "ABC1D23",
"external_id": "987654321",
"identificador_comprovante": "NSU123",
},
result={
"placa": "ABC1D23",
"identificador_comprovante": "NSU123",
},
)
self.assertTrue(logger_info.called)
payload = logger_info.call_args.args[2]
payload_text = str(payload)
self.assertNotIn("12345678909", payload_text)
self.assertNotIn("ABC1D23", payload_text)
self.assertNotIn("987654321", payload_text)
self.assertNotIn("NSU123", payload_text)
self.assertIn("***.***.***-09", payload_text)
self.assertIn("ABC***3", payload_text)
self.assertIn("******321", payload_text)
self.assertIn("***123", payload_text)
async def test_handle_message_prioritizes_order_flow_over_model_answer_for_purchase_intent(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "order_create",
"domain": "sales",
"action": "answer_user",
"entities": {
"generic_memory": {"cpf": "12345678909", "orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": "Certo! Para te ajudar a encontrar o carro ideal dentro do seu orcamento.",
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_message_plan(message: str, user_id: int | None):
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
service._extract_message_plan_with_llm = fake_extract_message_plan
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
async def fake_extract_entities(message: str, user_id: int | None):
return {
"generic_memory": {"cpf": "12345678909", "orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
}
service._extract_entities_with_llm = fake_extract_entities
async def fake_extract_missing_sales_search_context_with_llm(**kwargs):
return {}
service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm
service._domain_from_intents = lambda intents: "general"
service._handle_context_switch = lambda **kwargs: None
service._update_active_domain = lambda **kwargs: None
async def fake_try_execute_orchestration_control_tool(**kwargs):
return None
service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool
async def fake_try_execute_business_tool_from_turn_decision(**kwargs):
return None
service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision
async def fake_try_handle_review_management(**kwargs):
return None
service._try_handle_review_management = fake_try_handle_review_management
async def fake_try_confirm_pending_review(**kwargs):
return None
service._try_confirm_pending_review = fake_try_confirm_pending_review
async def fake_try_collect_and_schedule_review(**kwargs):
return None
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
async def fake_try_collect_and_cancel_order(**kwargs):
return None
service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order
async def fake_try_collect_and_create_order(**kwargs):
return "Encontrei 2 veiculo(s):\n1. Hyundai HB20 2022"
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message(
"Quero comprar um carro de 70 mil, meu CPF e 12345678909",
user_id=1,
)
self.assertIn("Encontrei 2 veiculo(s):", response)
async def test_handle_message_prioritizes_order_flow_for_explicit_order_request_without_extracted_fields(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "general",
"domain": "general",
"action": "answer_user",
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": "Claro! Voce gostaria de fazer um pedido de um veiculo ou agendar uma revisao?",
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_message_plan(message: str, user_id: int | None):
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
service._extract_message_plan_with_llm = fake_extract_message_plan
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
async def fake_extract_entities(message: str, user_id: int | None):
return {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
}
service._extract_entities_with_llm = fake_extract_entities
async def fake_extract_missing_sales_search_context_with_llm(**kwargs):
return {}
service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm
service._domain_from_intents = lambda intents: "general"
service._handle_context_switch = lambda **kwargs: None
service._update_active_domain = lambda **kwargs: None
async def fake_try_execute_orchestration_control_tool(**kwargs):
return None
service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool
async def fake_try_execute_business_tool_from_turn_decision(**kwargs):
return None
service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision
async def fake_try_handle_review_management(**kwargs):
return None
service._try_handle_review_management = fake_try_handle_review_management
async def fake_try_confirm_pending_review(**kwargs):
return None
service._try_confirm_pending_review = fake_try_confirm_pending_review
async def fake_try_collect_and_schedule_review(**kwargs):
return None
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
async def fake_try_collect_and_cancel_order(**kwargs):
return None
service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order
async def fake_try_handle_order_listing(**kwargs):
return None
service._try_handle_order_listing = fake_try_handle_order_listing
async def fake_try_collect_and_create_order(**kwargs):
return (
"Para seguir com o pedido, me diga qual carro voce procura.\n"
"Se preferir, posso listar opcoes por faixa de preco, modelo ou tipo de carro."
)
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message(
"Quero fazer um pedido",
user_id=1,
)
self.assertIn("Para seguir com o pedido", response)
self.assertNotIn("Qual veiculo voce gostaria de pedir", response)
def test_should_prioritize_review_flow_when_review_draft_is_open(self):
state = FakeState(
entries={
"pending_review_drafts": {
1: {
"payload": {"placa": "ABC1269"},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
prioritized = service._should_prioritize_review_flow(
turn_decision={"intent": "general", "domain": "general", "action": "answer_user"},
extracted_entities={
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
},
user_id=1,
)
self.assertTrue(prioritized)
def test_should_prioritize_review_flow_when_active_domain_is_review(self):
state = FakeState(
contexts={
1: {
"active_domain": "review",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"expires_at": utc_now() + timedelta(minutes=15),
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
prioritized = service._should_prioritize_review_flow(
turn_decision={"intent": "general", "domain": "general", "action": "answer_user"},
extracted_entities={
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
},
user_id=1,
)
self.assertTrue(prioritized)
def test_should_prioritize_review_flow_for_review_schedule_intent_without_prefilled_fields(self):
service = OrquestradorService.__new__(OrquestradorService)
service.state = FakeState()
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: None
prioritized = service._should_prioritize_review_flow(
turn_decision={"intent": "review_schedule", "domain": "review", "action": "ask_missing_fields"},
extracted_entities={
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
},
user_id=1,
)
self.assertTrue(prioritized)
async def test_review_schedule_direct_flow_captures_email_side_effects_after_success(self):
state = FakeState(
entries={
"pending_review_drafts": {
1: {
"payload": {
"placa": "ABC1D23",
"data_hora": "2026-03-25T10:00:00",
"modelo": "Onix",
"ano": 2022,
"km": 35000,
"revisao_previa_concessionaria": False,
},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "review",
"active_task": "review_schedule",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"expires_at": utc_now() + timedelta(minutes=15),
}
},
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.tool_executor = FakeToolExecutor(
result={
"protocolo": "REV-20260325-084279F3",
"placa": "ABC1D23",
"data_hora": "2026-03-25T10:00:00",
"modelo": "Onix",
"ano": 2022,
"km": 35000,
"valor_revisao": 906.0,
"status": "agendado",
"user_id": 1,
}
)
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
service._try_prefill_review_fields_from_memory = lambda user_id, payload: None
service._store_last_review_package = lambda user_id, payload: None
service._log_review_flow_source = lambda **kwargs: None
service._fallback_format_tool_result = lambda tool_name, tool_result: "Revisao agendada com sucesso."
captured = []
service._capture_successful_tool_side_effects = lambda **kwargs: captured.append(kwargs)
response = await service._try_collect_and_schedule_review(
message="ok",
user_id=1,
extracted_fields={},
intents={},
turn_decision={"intent": "review_schedule", "domain": "review", "action": "collect_review_schedule"},
)
self.assertEqual(response, "Revisao agendada com sucesso.")
self.assertEqual(len(captured), 1)
self.assertEqual(captured[0]["tool_name"], "agendar_revisao")
self.assertEqual(captured[0]["user_id"], 1)
self.assertEqual(captured[0]["tool_result"]["protocolo"], "REV-20260325-084279F3")
async def test_handle_message_prioritizes_review_management_over_model_answer_for_reschedule_intent(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "review_reschedule",
"domain": "review",
"action": "answer_user",
"entities": service.normalizer.empty_extraction_payload(),
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": "Claro, para qual data e horario voce gostaria de remarcar?",
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_message_plan(message: str, user_id: int | None):
return {
"orders": [
{
"domain": "review",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
service._extract_message_plan_with_llm = fake_extract_message_plan
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
async def fake_extract_entities(message: str, user_id: int | None):
return service.normalizer.empty_extraction_payload()
service._extract_entities_with_llm = fake_extract_entities
async def fake_extract_missing_sales_search_context_with_llm(**kwargs):
return {}
service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm
service._domain_from_intents = lambda intents: "general"
service._handle_context_switch = lambda **kwargs: None
service._update_active_domain = lambda **kwargs: None
async def fake_try_execute_orchestration_control_tool(**kwargs):
return None
service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool
async def fake_try_execute_business_tool_from_turn_decision(**kwargs):
return None
service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision
async def fake_try_handle_review_management(**kwargs):
return "Para remarcar sua revisao, preciso dos dados abaixo:\n- a nova data e hora desejada para a revisao"
service._try_handle_review_management = fake_try_handle_review_management
async def fake_try_confirm_pending_review(**kwargs):
return None
service._try_confirm_pending_review = fake_try_confirm_pending_review
async def fake_try_collect_and_schedule_review(**kwargs):
return None
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
async def fake_try_collect_and_cancel_order(**kwargs):
return None
service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order
async def fake_try_handle_order_listing(**kwargs):
return None
service._try_handle_order_listing = fake_try_handle_order_listing
async def fake_try_collect_and_create_order(**kwargs):
return None
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message(
"quero remarcar o meu agendamento REV-20260317-54E9D3CB",
user_id=1,
)
self.assertIn("a nova data e hora desejada", response)
async def test_handle_message_prioritizes_review_flow_over_model_answer_for_followup(self):
state = FakeState(
entries={
"pending_review_drafts": {
1: {
"payload": {"placa": "ABC1269"},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "review",
"generic_memory": {"placa": "ABC1269"},
"shared_memory": {"placa": "ABC1269"},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
},
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "general",
"domain": "general",
"action": "answer_user",
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": "Para que tipo de compromisso voce gostaria de marcar amanha as 16 horas?",
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_message_plan(message: str, user_id: int | None):
return {
"orders": [
{
"domain": "review",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
service._extract_message_plan_with_llm = fake_extract_message_plan
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
async def fake_extract_entities(message: str, user_id: int | None):
return {
"generic_memory": {},
"review_fields": {"data_hora": "13/03/2026 16:00"},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
}
service._extract_entities_with_llm = fake_extract_entities
async def fake_extract_missing_sales_search_context_with_llm(**kwargs):
return {}
service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm
service._domain_from_intents = lambda intents: "general"
service._handle_context_switch = lambda **kwargs: None
service._update_active_domain = lambda **kwargs: None
async def fake_try_execute_orchestration_control_tool(**kwargs):
return None
service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool
async def fake_try_execute_business_tool_from_turn_decision(**kwargs):
return None
service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision
async def fake_try_handle_review_management(**kwargs):
return None
service._try_handle_review_management = fake_try_handle_review_management
async def fake_try_confirm_pending_review(**kwargs):
return None
service._try_confirm_pending_review = fake_try_confirm_pending_review
async def fake_try_collect_and_schedule_review(**kwargs):
return "Para agendar sua revisao, preciso dos dados abaixo:\n- o modelo do veiculo"
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
async def fake_try_collect_and_cancel_order(**kwargs):
return None
service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order
async def fake_try_handle_order_listing(**kwargs):
return None
service._try_handle_order_listing = fake_try_handle_order_listing
async def fake_try_collect_and_create_order(**kwargs):
return None
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message(
"Eu gostaria de marcar amanha as 16 horas",
user_id=1,
)
self.assertIn("o modelo do veiculo", response)
async def test_handle_message_skips_inventory_tool_and_uses_order_flow_for_list_selection_follow_up(self):
state = FakeState(
contexts={
1: {
"active_domain": "sales",
"generic_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]},
"shared_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [
{"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0},
{"id": 11, "modelo": "Toyota Corolla 2024", "categoria": "suv", "preco": 76087.0},
],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "inventory_search",
"domain": "sales",
"action": "call_tool",
"entities": {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"selection_index": None,
"tool_name": "consultar_estoque",
"tool_arguments": {"preco_max": 70000, "categoria": "suv"},
"response_to_user": "",
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_message_plan(message: str, user_id: int | None):
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
service._extract_message_plan_with_llm = fake_extract_message_plan
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
async def fake_extract_entities(message: str, user_id: int | None):
return {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
}
service._extract_entities_with_llm = fake_extract_entities
async def fake_extract_missing_sales_search_context_with_llm(**kwargs):
return {}
service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm
service._domain_from_intents = lambda intents: "general"
service._handle_context_switch = lambda **kwargs: None
service._update_active_domain = lambda **kwargs: None
async def fake_try_execute_orchestration_control_tool(**kwargs):
return None
service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool
async def fake_try_execute_business_tool_from_turn_decision(**kwargs):
return "nao deveria chamar tool"
service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision
async def fake_try_handle_review_management(**kwargs):
return None
service._try_handle_review_management = fake_try_handle_review_management
async def fake_try_confirm_pending_review(**kwargs):
return None
service._try_confirm_pending_review = fake_try_confirm_pending_review
async def fake_try_collect_and_schedule_review(**kwargs):
return None
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
async def fake_try_collect_and_cancel_order(**kwargs):
return None
service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order
async def fake_try_handle_order_listing(**kwargs):
return None
service._try_handle_order_listing = fake_try_handle_order_listing
async def fake_try_collect_and_create_order(**kwargs):
return "Para realizar o pedido, preciso dos dados abaixo:\n- o CPF do cliente"
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message(
"quero a opcao 1",
user_id=1,
)
self.assertIn("CPF do cliente", response)
async def test_handle_message_short_circuits_llm_when_pending_stock_selection_matches_list_choice(self):
state = FakeState(
entries={
"pending_stock_selections": {
1: {
"payload": [
{"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0},
{"id": 11, "modelo": "Toyota Corolla 2024", "categoria": "suv", "preco": 76087.0},
],
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "sales",
"generic_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]},
"shared_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar o LLM para selecao pendente de estoque")
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_try_collect_and_create_order(**kwargs):
return "Para realizar o pedido, preciso dos dados abaixo:\n- o CPF do cliente"
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message(
"quero a opcao 1",
user_id=1,
)
self.assertIn("CPF do cliente", response)
async def test_handle_message_short_circuits_llm_when_pending_rental_selection_matches_list_choice(self):
state = FakeState(
entries={
"pending_rental_selections": {
1: {
"payload": [
{"id": 1, "placa": "RAA1A01", "modelo": "Chevrolet Tracker", "categoria": "suv", "ano": 2024, "valor_diaria": 219.9, "status": "disponivel"},
{"id": 2, "placa": "RAA1A02", "modelo": "Fiat Pulse", "categoria": "suv", "ano": 2024, "valor_diaria": 189.9, "status": "disponivel"},
],
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "rental",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar o LLM para selecao pendente de locacao")
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_try_collect_and_open_rental(**kwargs):
return "Para abrir a locacao, preciso dos dados abaixo:\n- a data e hora de inicio da locacao"
service._try_collect_and_open_rental = fake_try_collect_and_open_rental
response = await service.handle_message(
"1",
user_id=1,
)
self.assertIn("inicio da locacao", response)
async def test_handle_message_keeps_rental_create_flow_when_user_informs_due_date_with_devolucao_label(self):
state = FakeState(
entries={
"pending_rental_drafts": {
1: {
"payload": {
"rental_vehicle_id": 3,
"placa": "RAA1A02",
"data_inicio": "19/03/2026 10:00",
},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "rental",
"active_task": "rental_create",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": {"id": 3, "placa": "RAA1A02", "modelo": "Fiat Pulse"},
"last_rental_contract": {
"contrato_numero": "LOC-20260319-33CD6567",
"placa": "RAA1A02",
},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar o LLM durante follow-up ativo de locacao")
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_try_collect_and_open_rental(**kwargs):
self.assertEqual(kwargs["message"], "devolucao 21/03/2026 10:00")
return "locacao aberta"
service._try_collect_and_open_rental = fake_try_collect_and_open_rental
response = await service.handle_message(
"devolucao 21/03/2026 10:00",
user_id=1,
)
self.assertEqual(response, "locacao aberta")
async def test_handle_message_short_circuits_for_rental_return_using_last_contract(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": None,
"last_rental_contract": {
"contrato_numero": "LOC-20260318-FE69BCF0",
"placa": "RAA1A12",
},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service.tool_executor = FakeToolExecutor(
result={
"contrato_numero": "LOC-20260318-FE69BCF0",
"placa": "RAA1A12",
"modelo_veiculo": "Peugeot 208",
"data_devolucao": "2026-03-18T15:46:00",
"valor_final": 449.7,
}
)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._fallback_format_tool_result = lambda tool_name, tool_result: "devolucao ok"
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "general",
"domain": "general",
"action": "answer_user",
"entities": service.normalizer.empty_extraction_payload(),
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_message_plan(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar o planner para devolucao deterministica de aluguel")
service._extract_message_plan_with_llm = fake_extract_message_plan
response = await service.handle_message(
"devolver a placa RAA1A12",
user_id=1,
)
self.assertTrue(response.startswith("devolucao ok"))
self.assertIn("Se quiser, posso te enviar esse resumo por e-mail.", response)
self.assertEqual(
service.tool_executor.calls,
[
(
"registrar_devolucao_aluguel",
{"placa": "RAA1A12", "contrato_numero": "LOC-20260318-FE69BCF0"},
1,
)
],
)
async def test_handle_message_short_circuits_for_rental_payment_receipt_text(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": None,
"last_rental_contract": {
"contrato_numero": "LOC-20260318-FE69BCF0",
"placa": "RAA1A12",
},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service.tool_executor = FakeToolExecutor(
result={
"protocolo": "ALP-20260318-ABCD1234",
"contrato_numero": "LOC-20260318-FE69BCF0",
"placa": "RAA1A12",
"valor": 449.7,
"data_pagamento": "2026-03-18T15:47:00",
}
)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._fallback_format_tool_result = lambda tool_name, tool_result: "pagamento ok"
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "general",
"domain": "general",
"action": "answer_user",
"entities": service.normalizer.empty_extraction_payload(),
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_message_plan(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar o planner para pagamento deterministico de aluguel")
service._extract_message_plan_with_llm = fake_extract_message_plan
response = await service.handle_message(
"Registrar pagamento de aluguel: valor 449,70; data_pagamento 18/03/2026 15:47; favorecido Locadora XPTO; identificador_comprovante NSU123.",
user_id=1,
)
self.assertTrue(response.startswith("pagamento ok"))
self.assertIn("Se quiser, posso te enviar esse resumo por e-mail.", response)
self.assertEqual(
service.tool_executor.calls,
[
(
"registrar_pagamento_aluguel",
{
"valor": 449.7,
"data_pagamento": "18/03/2026 15:47",
"favorecido": "Locadora XPTO",
"identificador_comprovante": "NSU123",
"contrato_numero": "LOC-20260318-FE69BCF0",
"placa": "RAA1A12",
},
1,
)
],
)
async def test_handle_message_short_circuits_for_rental_payment_receipt_text_with_angle_brackets(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": None,
"last_rental_contract": {
"contrato_numero": "LOC-20260318-4B85490F",
"placa": "RAA1A22",
},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service.tool_executor = FakeToolExecutor(
result={
"protocolo": "ALP-20260318-ABCD1234",
"contrato_numero": "LOC-20260318-4B85490F",
"placa": "RAA1A22",
"valor": 479.7,
"data_pagamento": "2026-03-18T16:10:00",
}
)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._fallback_format_tool_result = lambda tool_name, tool_result: "pagamento ok"
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "general",
"domain": "general",
"action": "answer_user",
"entities": service.normalizer.empty_extraction_payload(),
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_message_plan(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar o planner para pagamento deterministico de aluguel")
service._extract_message_plan_with_llm = fake_extract_message_plan
response = await service.handle_message(
"[imagem recebida no telegram]\nDados extraidos da imagem: Registrar pagamento de aluguel: contrato <LOC-20260318-4B85490F>; placa <nao informada>; valor <R$ 479,70>; data_pagamento <18/03/2026 16:10>; favorecido <Locadora XPTO>; identificador_comprovante <NSU123456>; observacoes <pagamento da locacao>.",
user_id=1,
)
self.assertTrue(response.startswith("pagamento ok"))
self.assertIn("Se quiser, posso te enviar esse resumo por e-mail.", response)
self.assertEqual(
service.tool_executor.calls,
[
(
"registrar_pagamento_aluguel",
{
"contrato_numero": "LOC-20260318-4B85490F",
"valor": 479.7,
"data_pagamento": "18/03/2026 16:10",
"favorecido": "Locadora XPTO",
"identificador_comprovante": "NSU123456",
"observacoes": "pagamento da locacao",
"placa": "RAA1A22",
},
1,
)
],
)
async def test_handle_message_short_circuits_for_current_rental_info_question(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": None,
"last_rental_contract": {
"contrato_numero": "LOC-20260323-CAEECA1C",
"placa": "RAA1A02",
"modelo_veiculo": "Fiat Pulse",
"data_inicio": "2026-03-19T10:00:00",
"data_fim_prevista": "2026-03-21T10:00:00",
"valor_diaria": 189.9,
"valor_previsto": 379.8,
"status": "ativa",
"status_pagamento": "registrado",
"data_pagamento": "2026-03-23T15:47:00",
"valor_pagamento": 379.8,
},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_try_handle_pending_stock_selection_follow_up(**kwargs):
return None
async def fake_try_handle_active_sales_follow_up(**kwargs):
return None
async def fake_try_handle_pending_rental_selection_follow_up(**kwargs):
return None
async def fake_try_handle_active_rental_follow_up(**kwargs):
return None
async def fake_try_handle_active_review_follow_up(**kwargs):
return None
service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up
service._try_handle_active_sales_follow_up = fake_try_handle_active_sales_follow_up
service._try_handle_pending_rental_selection_follow_up = fake_try_handle_pending_rental_selection_follow_up
service._try_handle_active_rental_follow_up = fake_try_handle_active_rental_follow_up
service._try_handle_active_review_follow_up = fake_try_handle_active_review_follow_up
async def fake_extract_turn_decision(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar o LLM para consulta informativa do aluguel atual")
service._extract_turn_decision_with_llm = fake_extract_turn_decision
response = await service.handle_message(
"qual a data de devolucao do meu aluguel?",
user_id=1,
)
self.assertIn("A devolucao prevista do seu aluguel e 21/03/2026 10:00.", response)
self.assertIn("Contrato: LOC-20260323-CAEECA1C", response)
self.assertIn("Veiculo: Fiat Pulse", response)
async def test_handle_message_rehydrates_current_rental_info_from_db_after_restart(self):
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
MockBase.metadata.create_all(bind=engine)
self.addCleanup(engine.dispose)
db = SessionLocal()
try:
vehicle = RentalVehicle(
placa="RAA1A02",
modelo="Fiat Pulse",
categoria="suv",
ano=2024,
valor_diaria=189.9,
status="disponivel",
)
db.add(vehicle)
db.commit()
db.refresh(vehicle)
contract = RentalContract(
contrato_numero="LOC-20260323-CAEECA1C",
user_id=1,
rental_vehicle_id=vehicle.id,
placa=vehicle.placa,
modelo_veiculo=vehicle.modelo,
categoria=vehicle.categoria,
data_inicio=datetime(2026, 3, 19, 10, 0),
data_fim_prevista=datetime(2026, 3, 21, 10, 0),
data_devolucao=None,
valor_diaria=189.9,
valor_previsto=379.8,
valor_final=None,
status="ativa",
)
db.add(contract)
db.commit()
db.refresh(contract)
payment = RentalPayment(
protocolo="ALP-20260323-0B41DD0D",
user_id=1,
rental_contract_id=contract.id,
contrato_numero=contract.contrato_numero,
placa=contract.placa,
valor=379.8,
data_pagamento=datetime(2026, 3, 23, 15, 47),
favorecido="Locadora XPTO",
identificador_comprovante="NSU123456",
observacoes="pagamento da locacao",
)
db.add(payment)
db.commit()
finally:
db.close()
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_try_handle_pending_stock_selection_follow_up(**kwargs):
return None
async def fake_try_handle_active_sales_follow_up(**kwargs):
return None
async def fake_try_handle_pending_rental_selection_follow_up(**kwargs):
return None
async def fake_try_handle_active_rental_follow_up(**kwargs):
return None
async def fake_try_handle_active_review_follow_up(**kwargs):
return None
service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up
service._try_handle_active_sales_follow_up = fake_try_handle_active_sales_follow_up
service._try_handle_pending_rental_selection_follow_up = fake_try_handle_pending_rental_selection_follow_up
service._try_handle_active_rental_follow_up = fake_try_handle_active_rental_follow_up
service._try_handle_active_review_follow_up = fake_try_handle_active_review_follow_up
async def fake_extract_turn_decision(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar o LLM para consulta informativa do aluguel apos restart")
service._extract_turn_decision_with_llm = fake_extract_turn_decision
with patch("app.services.flows.rental_flow_support.SessionMockLocal", SessionLocal):
response = await service.handle_message(
"qual a data de devolucao do meu aluguel?",
user_id=1,
)
self.assertIn("A devolucao prevista do seu aluguel e 21/03/2026 10:00.", response)
self.assertIn("Contrato: LOC-20260323-CAEECA1C", response)
self.assertIn("Veiculo: Fiat Pulse", response)
snapshot = state.get_user_context(1)["last_rental_contract"]
self.assertEqual(snapshot["contrato_numero"], "LOC-20260323-CAEECA1C")
self.assertEqual(snapshot["status_pagamento"], "registrado")
self.assertEqual(snapshot["data_fim_prevista"], "2026-03-21T10:00:00")
def test_store_last_rental_contract_preserves_contract_snapshot_after_payment_update(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
service._store_last_rental_contract(
user_id=1,
payload={
"contrato_numero": "LOC-20260323-CAEECA1C",
"placa": "RAA1A02",
"modelo_veiculo": "Fiat Pulse",
"data_inicio": "2026-03-19T10:00:00",
"data_fim_prevista": "2026-03-21T10:00:00",
"valor_diaria": 189.9,
"valor_previsto": 379.8,
"status": "ativa",
},
)
service._store_last_rental_contract(
user_id=1,
payload={
"contrato_numero": "LOC-20260323-CAEECA1C",
"placa": "RAA1A02",
"valor": 379.8,
"data_pagamento": "2026-03-23T15:47:00",
"favorecido": "Locadora XPTO",
"status": "registrado",
},
)
snapshot = state.get_user_context(1)["last_rental_contract"]
self.assertEqual(snapshot["modelo_veiculo"], "Fiat Pulse")
self.assertEqual(snapshot["data_fim_prevista"], "2026-03-21T10:00:00")
self.assertEqual(snapshot["status"], "ativa")
self.assertEqual(snapshot["status_pagamento"], "registrado")
self.assertEqual(snapshot["data_pagamento"], "2026-03-23T15:47:00")
self.assertEqual(snapshot["valor_pagamento"], 379.8)
def test_has_rental_return_management_request_ignores_return_question_even_with_last_contract(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": None,
"last_rental_contract": {
"contrato_numero": "LOC-20260318-FE69BCF0",
"placa": "RAA1A12",
},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
self.assertFalse(
service._has_rental_return_management_request(
"qual a data de devolucao do meu aluguel?",
user_id=1,
)
)
def test_has_rental_payment_request_requires_current_rental_reference(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
"last_rental_results": [],
"selected_rental_vehicle": None,
"last_rental_contract": {
"contrato_numero": "LOC-20260318-FE69BCF0",
"placa": "RAA1A12",
},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
self.assertFalse(service._has_rental_payment_request("segue comprovante pix de R$ 500", user_id=1))
self.assertTrue(service._has_rental_payment_request("segue comprovante do aluguel de R$ 500", user_id=1))
async def test_handle_message_keeps_sales_flow_when_cpf_follow_up_is_misclassified_as_review(self):
state = FakeState(
entries={
"pending_order_drafts": {
1: {
"payload": {"vehicle_id": 15, "modelo_veiculo": "Volkswagen T-Cross 2022", "valor_veiculo": 73224.0},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "sales",
"generic_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]},
"shared_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [
{"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True},
],
"selected_vehicle": {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_extract_turn_decision(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar o LLM para continuar um fluxo de venda aberto")
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_message_plan(message: str, user_id: int | None):
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
service._extract_message_plan_with_llm = fake_extract_message_plan
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
async def fake_extract_entities(message: str, user_id: int | None):
return {
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
}
service._extract_entities_with_llm = fake_extract_entities
async def fake_extract_missing_sales_search_context_with_llm(**kwargs):
return {}
service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm
service._domain_from_intents = lambda intents: "general"
service._update_active_domain = lambda **kwargs: None
async def fake_try_execute_orchestration_control_tool(**kwargs):
return None
service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool
async def fake_try_execute_business_tool_from_turn_decision(**kwargs):
return "nao deveria executar tool planejada"
service._try_execute_business_tool_from_turn_decision = fake_try_execute_business_tool_from_turn_decision
async def fake_try_handle_review_management(**kwargs):
return None
service._try_handle_review_management = fake_try_handle_review_management
async def fake_try_confirm_pending_review(**kwargs):
return None
service._try_confirm_pending_review = fake_try_confirm_pending_review
async def fake_try_collect_and_schedule_review(**kwargs):
return None
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
async def fake_try_collect_and_cancel_order(**kwargs):
return None
service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order
async def fake_try_handle_order_listing(**kwargs):
return None
service._try_handle_order_listing = fake_try_handle_order_listing
async def fake_try_collect_and_create_order(**kwargs):
return "Pedido criado com sucesso."
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message(
"12345678909",
user_id=1,
)
self.assertEqual(response, "Pedido criado com sucesso.")
async def test_handle_message_short_circuits_active_review_time_follow_up_before_llm(self):
state = FakeState(
entries={
"pending_review_drafts": {
1: {
"payload": {
"placa": "ABC1234",
"data_hora_base": "17/03/2026",
},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "review",
"generic_memory": {"placa": "ABC1234"},
"shared_memory": {"placa": "ABC1234"},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_try_handle_pending_stock_selection_follow_up(**kwargs):
return None
service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up
async def fake_extract_turn_decision(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar o LLM para um follow-up temporal de revisao com draft aberto")
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_try_collect_and_schedule_review(**kwargs):
self.assertEqual(kwargs["turn_decision"]["intent"], "review_schedule")
return "Para agendar sua revisao, preciso dos dados abaixo:\n- o modelo do veiculo"
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
async def fake_try_handle_review_management(**kwargs):
return None
service._try_handle_review_management = fake_try_handle_review_management
async def fake_try_confirm_pending_review(**kwargs):
return None
service._try_confirm_pending_review = fake_try_confirm_pending_review
response = await service.handle_message(
"15h",
user_id=1,
)
self.assertIn("o modelo do veiculo", response)
async def test_handle_message_allows_explicit_sales_shift_before_active_review_follow_up_short_circuit(self):
state = FakeState(
entries={
"pending_review_drafts": {
1: {
"payload": {
"placa": "ABC1234",
"data_hora_base": "17/03/2026",
},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "review",
"generic_memory": {"placa": "ABC1234"},
"shared_memory": {"placa": "ABC1234"},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_try_handle_pending_stock_selection_follow_up(**kwargs):
return None
service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "order_create",
"domain": "sales",
"action": "collect_order_create",
"entities": service.normalizer.empty_extraction_payload(),
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": "",
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_extract_message_plan(message: str, user_id: int | None):
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
service._extract_message_plan_with_llm = fake_extract_message_plan
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
async def fake_extract_entities(message: str, user_id: int | None):
return service.normalizer.empty_extraction_payload()
service._extract_entities_with_llm = fake_extract_entities
async def fake_extract_missing_sales_search_context_with_llm(**kwargs):
return {}
service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm
service._domain_from_intents = lambda intents: "general"
service._update_active_domain = lambda **kwargs: None
service._handle_context_switch = lambda **kwargs: "Entendi que voce quer sair de agendamento de revisao e ir para compra de veiculo. Tem certeza?"
async def fake_try_collect_and_schedule_review(**kwargs):
raise AssertionError("nao deveria consumir uma solicitacao explicita de compra como follow-up de revisao")
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
response = await service.handle_message(
"quero comprar um carro ate 80 mil",
user_id=1,
)
self.assertEqual(
response,
"Entendi que voce quer sair de agendamento de revisao e ir para compra de veiculo. Tem certeza?",
)
async def test_handle_message_prioritizes_pending_switch_confirmation_before_sales_follow_up(self):
state = FakeState(
entries={
"pending_stock_selections": {
1: {
"payload": [
{"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0},
],
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "sales",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": {
"source_domain": "sales",
"target_domain": "review",
"expires_at": utc_now() + timedelta(minutes=15),
},
"last_stock_results": [
{"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0},
],
"selected_vehicle": {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
service._is_order_selection_reset_message = lambda message: False
async def fake_try_handle_pending_stock_selection_follow_up(**kwargs):
raise AssertionError("nao deveria entrar no follow-up de estoque antes de confirmar a troca de contexto")
service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up
async def fake_try_handle_active_sales_follow_up(**kwargs):
raise AssertionError("nao deveria entrar no follow-up de vendas antes de confirmar a troca de contexto")
service._try_handle_active_sales_follow_up = fake_try_handle_active_sales_follow_up
response = await service.handle_message(
"sim",
user_id=1,
)
self.assertEqual(
response,
"Certo, contexto anterior encerrado. Vamos seguir com agendamento de revisao.\n"
"Pode me informar a placa ou, se preferir, ja mandar placa, data/hora, modelo, ano, km e se ja fez revisao.",
)
self.assertEqual(state.get_user_context(1)["active_domain"], "review")
self.assertIsNone(state.get_user_context(1).get("pending_switch"))
self.assertIsNone(state.get_entry("pending_stock_selections", 1))
async def test_handle_message_prioritizes_immediate_reset_before_active_sales_follow_up(self):
state = FakeState(
contexts={
1: {
"active_domain": "sales",
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
service._is_order_selection_reset_message = lambda message: True
async def fake_try_handle_immediate_context_reset(**kwargs):
return "Contexto da conversa limpo. Podemos recomecar do zero."
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_handle_pending_stock_selection_follow_up(**kwargs):
raise AssertionError("nao deveria entrar no follow-up de estoque antes do reset")
service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up
async def fake_try_handle_active_sales_follow_up(**kwargs):
raise AssertionError("nao deveria entrar no follow-up de vendas antes do reset")
service._try_handle_active_sales_follow_up = fake_try_handle_active_sales_follow_up
response = await service.handle_message(
"esqueca tudo e vamos recomecar",
user_id=1,
)
self.assertEqual(response, "Contexto da conversa limpo. Podemos recomecar do zero.")
async def test_try_handle_immediate_context_reset_treats_vamos_recomecar_suffix_as_pure_reset(self):
state = FakeState(
contexts={
1: {
"active_domain": "sales",
"generic_memory": {"orcamento_max": 65000},
"shared_memory": {"orcamento_max": 65000},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [{"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0}],
"selected_vehicle": {"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0},
"last_rental_contract": {
"contrato_numero": "LOC-20260319-33CD6567",
"placa": "RAA1A02",
},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def finish(response: str, queue_notice: str | None = None):
return response
async def fake_handle_message(message: str, user_id: int | None = None):
raise AssertionError("nao deveria reprocessar 'vamos recomecar' como mensagem de negocio")
service.handle_message = fake_handle_message
response = await service._try_handle_immediate_context_reset(
message="esqueca tudo e vamos recomecar",
user_id=1,
turn_decision={"action": "clear_context"},
finish=finish,
)
self.assertEqual(response, "Contexto da conversa limpo. Podemos recomecar do zero.")
self.assertEqual(state.get_user_context(1)["active_domain"], "general")
self.assertEqual(state.get_user_context(1)["generic_memory"], {})
self.assertIsNone(state.get_user_context(1).get("last_rental_contract"))
async def test_active_sales_follow_up_ignores_order_listing_request_with_open_order_draft(self):
state = FakeState(
entries={
"pending_order_drafts": {
1: {
"payload": {
"cpf": "12345678909",
"vehicle_id": 15,
"modelo_veiculo": "Volkswagen T-Cross 2022",
"valor_veiculo": 73224.0,
},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "sales",
"generic_memory": {"cpf": "12345678909"},
"shared_memory": {"cpf": "12345678909"},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [
{"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True},
],
"selected_vehicle": {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_try_collect_and_create_order(**kwargs):
raise AssertionError("nao deveria consumir listagem de pedidos como continuacao de order_create")
service._try_collect_and_create_order = fake_try_collect_and_create_order
async def finish(response: str, queue_notice: str | None = None):
return response
response = await service._try_handle_active_sales_follow_up(
message="Liste os meus pedidos",
user_id=1,
finish=finish,
)
self.assertIsNone(response)
self.assertIsNotNone(state.get_entry("pending_order_drafts", 1))
async def test_active_sales_follow_up_prioritizes_cancel_order_over_stale_order_create_draft(self):
state = FakeState(
entries={
"pending_order_drafts": {
1: {
"payload": {
"cpf": "12345678909",
"vehicle_id": 15,
"modelo_veiculo": "Volkswagen T-Cross 2022",
"valor_veiculo": 73224.0,
},
"expires_at": utc_now() + timedelta(minutes=15),
}
},
"pending_cancel_order_drafts": {
1: {
"payload": {"numero_pedido": "PED-20260312110556-BBA37F"},
"expires_at": utc_now() + timedelta(minutes=15),
}
},
},
contexts={
1: {
"active_domain": "sales",
"active_task": "order_cancel",
"generic_memory": {"cpf": "12345678909"},
"shared_memory": {"cpf": "12345678909"},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
},
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_try_collect_and_create_order(**kwargs):
raise AssertionError("nao deveria priorizar order_create enquanto order_cancel aguarda motivo")
async def fake_try_collect_and_cancel_order(**kwargs):
self.assertEqual(kwargs["message"], "desisti da compra")
self.assertEqual(kwargs["user_id"], 1)
return "Pedido PED-20260312110556-BBA37F atualizado.\nStatus: Cancelado"
service._try_collect_and_create_order = fake_try_collect_and_create_order
service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order
async def finish(response: str, queue_notice: str | None = None):
return response
response = await service._try_handle_active_sales_follow_up(
message="desisti da compra",
user_id=1,
finish=finish,
)
self.assertIn("Pedido PED-20260312110556-BBA37F atualizado.", response)
self.assertIn("Status: Cancelado", response)
async def test_active_sales_follow_up_allows_new_budget_search_to_reset_open_order_draft(self):
state = FakeState(
entries={
"pending_order_drafts": {
1: {
"payload": {
"cpf": "12345678909",
"vehicle_id": 15,
"modelo_veiculo": "Volkswagen T-Cross 2022",
"valor_veiculo": 73224.0,
},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "sales",
"generic_memory": {"cpf": "12345678909", "orcamento_max": 70000, "perfil_veiculo": ["suv"]},
"shared_memory": {"cpf": "12345678909", "orcamento_max": 70000, "perfil_veiculo": ["suv"]},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [
{"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True},
],
"selected_vehicle": {"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0, "budget_relaxed": True},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_try_collect_and_create_order(**kwargs):
raise AssertionError("nao deveria consumir nova busca de estoque no atalho de venda ativa")
service._try_collect_and_create_order = fake_try_collect_and_create_order
async def finish(response: str, queue_notice: str | None = None):
return response
response = await service._try_handle_active_sales_follow_up(
message="agora quero comprar um carro de ate 60 mil",
user_id=1,
finish=finish,
)
self.assertIsNone(response)
self.assertIsNone(state.get_entry("pending_order_drafts", 1))
self.assertIsNone(state.get_user_context(1)["selected_vehicle"])
async def test_orchestration_control_ignores_cancel_flow_tool_for_fresh_sales_request_without_open_flow(self):
state = FakeState(
contexts={
1: {
"active_domain": "sales",
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
class DummyRegistry:
def get_tools(self):
return []
class DummyExecutor:
async def execute(self, tool_name, arguments, user_id=None):
raise AssertionError("nao deveria executar cancelar_fluxo_atual para nova busca operacional")
service.registry = DummyRegistry()
service.tool_executor = DummyExecutor()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
service._has_open_flow = lambda user_id, domain: service.policy.has_open_flow(user_id=user_id, domain=domain)
service._is_low_value_response = lambda text: False
async def fake_call_llm_with_trace(**kwargs):
return {
"response": "",
"tool_call": {
"name": "cancelar_fluxo_atual",
"arguments": {"motivo": "cliente mudou de ideia e iniciou nova busca"},
},
}
service._call_llm_with_trace = fake_call_llm_with_trace
async def finish(response: str, queue_notice: str | None = None):
return response
response = await service._try_execute_orchestration_control_tool(
message="agora eu quero comprar um carro de ate 70 mil",
user_id=1,
turn_decision={},
extracted_entities={},
queue_notice=None,
finish=finish,
)
self.assertIsNone(response)
def test_should_prioritize_order_flow_when_cancel_draft_is_open(self):
state = FakeState(
entries={
"pending_cancel_order_drafts": {
1: {
"payload": {"numero_pedido": "PED-202603101204814-6ED33A"},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
prioritized = service._should_prioritize_order_flow(
turn_decision={"intent": "general", "domain": "general", "action": "answer_user"},
extracted_entities={
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
},
user_id=1,
)
self.assertTrue(prioritized)
def test_should_prioritize_order_flow_when_message_selects_vehicle_from_last_stock_results(self):
state = FakeState(
contexts={
1: {
"active_domain": "sales",
"generic_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]},
"shared_memory": {"orcamento_max": 70000, "perfil_veiculo": ["suv"]},
"last_stock_results": [
{"id": 15, "modelo": "Volkswagen T-Cross 2022", "categoria": "suv", "preco": 73224.0},
{"id": 11, "modelo": "Toyota Corolla 2024", "categoria": "suv", "preco": 76087.0},
],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
prioritized = service._should_prioritize_order_flow(
turn_decision={"intent": "inventory_search", "domain": "sales", "action": "call_tool"},
extracted_entities={
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
},
user_id=1,
message="quero a opcao 1",
)
self.assertTrue(prioritized)
def test_should_prioritize_order_flow_for_explicit_order_request_without_entities(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
prioritized = service._should_prioritize_order_flow(
turn_decision={"intent": "order_create", "domain": "sales", "action": "answer_user"},
extracted_entities={
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
},
user_id=1,
message="Quero fazer um pedido",
)
self.assertTrue(prioritized)
def test_should_prioritize_order_flow_for_explicit_order_request_even_when_model_returns_general(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
prioritized = service._should_prioritize_order_flow(
turn_decision={"intent": "general", "domain": "general", "action": "answer_user"},
extracted_entities={
"generic_memory": {},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
},
user_id=1,
message="Quero fazer um pedido",
)
self.assertTrue(prioritized)
def test_normalize_tool_name_maps_trade_in_alias_and_arguments(self):
normalizer = EntityNormalizer()
tool_name = normalizer.normalize_tool_name("avaliar_troca_veiculo")
arguments = normalizer.normalize_tool_arguments(
tool_name,
{"modelo": "Onix", "ano": 2020, "quilometragem": 45000},
)
self.assertEqual(tool_name, "avaliar_veiculo_troca")
self.assertEqual(arguments, {"modelo": "Onix", "ano": 2020, "km": 45000})
async def test_pending_order_selection_prefers_turn_decision_domain(self):
state = FakeState(
contexts={
9: {
"pending_order_selection": {
"orders": [
{"domain": "review", "message": "agendar revisao", "memory_seed": {}},
{"domain": "sales", "message": "fazer pedido", "memory_seed": {}},
],
"expires_at": utc_now() + timedelta(minutes=15),
},
"order_queue": [],
"active_domain": "general",
"generic_memory": {},
}
}
)
policy = ConversationPolicy(service=FakePolicyService(state))
response = await policy.try_resolve_pending_order_selection(
message="quero comprar",
user_id=9,
turn_decision={"domain": "sales", "intent": "order_create", "action": "collect_order_create"},
)
self.assertIn("Vou comecar por: Venda: fazer pedido", response)
async def test_pending_order_selection_prefers_turn_decision_selection_index(self):
state = FakeState(
contexts={
9: {
"pending_order_selection": {
"orders": [
{"domain": "review", "message": "agendar revisao", "memory_seed": {}},
{"domain": "sales", "message": "fazer pedido", "memory_seed": {}},
],
"expires_at": utc_now() + timedelta(minutes=15),
},
"order_queue": [],
"active_domain": "general",
"generic_memory": {},
}
}
)
policy = ConversationPolicy(service=FakePolicyService(state))
response = await policy.try_resolve_pending_order_selection(
message="esse",
user_id=9,
turn_decision={"domain": "general", "intent": "general", "action": "answer_user", "selection_index": 1},
)
self.assertIn("Vou comecar por: Venda: fazer pedido", response)
async def test_pending_order_selection_promotes_new_operational_request_before_previous_options(self):
state = FakeState(
contexts={
9: {
"pending_order_selection": {
"orders": [
{"domain": "sales", "message": "compra", "seed_message": "quero comprar um veiculo", "memory_seed": {}},
{"domain": "review", "message": "revisao", "seed_message": "quero agendar revisao", "memory_seed": {}},
{"domain": "rental", "message": "aluguel", "seed_message": "quero alugar um carro", "memory_seed": {}},
],
"expires_at": utc_now() + timedelta(minutes=15),
},
"order_queue": [],
"active_domain": "general",
"generic_memory": {},
}
}
)
service = FakePolicyService(state)
policy = ConversationPolicy(service=service)
response = await policy.try_resolve_pending_order_selection(
message="quais pedidos eu tenho?",
user_id=9,
turn_decision={"domain": "sales", "intent": "order_list", "action": "call_tool", "tool_name": "listar_pedidos"},
)
self.assertIsNone(response)
context = state.get_user_context(9)
self.assertIsNone(context["pending_order_selection"])
self.assertEqual([item["domain"] for item in context["order_queue"]], ["sales", "review", "rental"])
self.assertEqual(context["order_queue"][0]["message"], "quero comprar um veiculo")
async def test_pending_order_selection_skips_duplicate_base_task_when_new_request_is_more_specific(self):
state = FakeState(
contexts={
9: {
"pending_order_selection": {
"orders": [
{"domain": "sales", "message": "compra", "seed_message": "quero comprar um veiculo", "memory_seed": {}},
{"domain": "review", "message": "revisao", "seed_message": "quero agendar revisao", "memory_seed": {}},
{"domain": "rental", "message": "aluguel", "seed_message": "quero alugar um carro", "memory_seed": {}},
],
"expires_at": utc_now() + timedelta(minutes=15),
},
"order_queue": [],
"active_domain": "general",
"generic_memory": {},
}
}
)
service = FakePolicyService(state)
policy = ConversationPolicy(service=service)
response = await policy.try_resolve_pending_order_selection(
message="quero comprar um suv ate 95 mil",
user_id=9,
turn_decision={"domain": "sales", "intent": "order_create", "action": "collect_order_create"},
)
self.assertIn("Perfeito. Vou comecar por: Venda: compra", response)
self.assertIn("handled:quero comprar um suv ate 95 mil", response)
context = state.get_user_context(9)
self.assertIsNone(context["pending_order_selection"])
self.assertEqual(context["active_domain"], "sales")
self.assertEqual([item["domain"] for item in context["order_queue"]], ["review", "rental"])
async def test_try_continue_queue_prefers_turn_decision_action(self):
state = FakeState(
contexts={
9: {
"pending_switch": {
"target_domain": "sales",
"queued_message": "fazer pedido",
"memory_seed": {"cpf": "12345678909"},
"expires_at": utc_now() + timedelta(minutes=15),
},
"active_domain": "general",
"generic_memory": {},
"pending_order_selection": None,
}
}
)
service = FakePolicyService(state)
policy = ConversationPolicy(service=service)
policy.apply_domain_switch = lambda user_id, target_domain: service._get_user_context(user_id).update(
{"active_domain": target_domain, "pending_switch": None}
)
response = await policy.try_continue_queued_order(
message="ok",
user_id=9,
turn_decision={"action": "continue_queue", "intent": "queue_continue", "domain": "sales"},
)
self.assertIn("Agora, sobre a compra do veiculo:", response)
def test_handle_context_switch_prefers_turn_decision_domain_confirmation(self):
state = FakeState(
contexts={
9: {
"pending_switch": {
"target_domain": "review",
"expires_at": utc_now() + timedelta(minutes=15),
},
"active_domain": "sales",
"generic_memory": {},
"pending_order_selection": None,
}
}
)
service = FakePolicyService(state)
policy = ConversationPolicy(service=service)
policy.apply_domain_switch = lambda user_id, target_domain: service._get_user_context(user_id).update(
{"active_domain": target_domain, "pending_switch": None}
)
response = policy.handle_context_switch(
message="quero revisar",
user_id=9,
target_domain_hint="review",
turn_decision={"domain": "review", "intent": "review_schedule", "action": "collect_review_schedule"},
)
self.assertEqual(
response,
"Certo, contexto anterior encerrado. Vamos seguir com agendamento de revisao.\n"
"Pode me informar a placa ou, se preferir, ja mandar placa, data/hora, modelo, ano, km e se ja fez revisao.",
)
def test_prepare_message_for_single_order_defers_explicit_domain_switch_with_open_flow(self):
state = FakeState(
entries={
"pending_review_drafts": {
9: {
"payload": {"placa": "ABC1234"},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
9: {
"active_domain": "review",
"generic_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
}
},
)
service = FakePolicyService(state)
policy = ConversationPolicy(service=service)
routed_message, queue_notice, early_response = policy.prepare_message_for_single_order(
message="quero comprar um carro de ate 62 mil",
user_id=9,
routing_plan={"orders": [{"domain": "sales", "message": "quero comprar um carro de ate 62 mil"}]},
)
self.assertEqual(routed_message, "quero comprar um carro de ate 62 mil")
self.assertIsNone(queue_notice)
self.assertIsNone(early_response)
self.assertEqual(service._get_user_context(9).get("order_queue"), [])
def test_prepare_message_for_single_order_requests_clarification_for_three_actionable_domains(self):
state = FakeState(
contexts={
9: {
"active_domain": "general",
"generic_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
}
},
)
service = FakePolicyService(state)
policy = ConversationPolicy(service=service)
routed_message, queue_notice, early_response = policy.prepare_message_for_single_order(
message="oi, pode me ajudar com compra, revisao e aluguel?",
user_id=9,
routing_plan={
"orders": [
{"domain": "sales", "message": "compra"},
{"domain": "review", "message": "revisao"},
]
},
)
self.assertEqual(routed_message, "oi, pode me ajudar com compra, revisao e aluguel?")
self.assertIsNone(queue_notice)
self.assertIn("Identifiquei 3 acoes", early_response)
self.assertIn("3. Locacao: aluguel", early_response)
pending = state.get_user_context(9)["pending_order_selection"]
self.assertEqual(len(pending["orders"]), 3)
def test_prepare_message_for_single_order_counts_only_orders_effectively_queued(self):
state = FakeState(
entries={
"pending_review_drafts": {
9: {
"payload": {"placa": "ABC1234"},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
9: {
"active_domain": "review",
"generic_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
}
},
)
service = FakePolicyService(state)
policy = ConversationPolicy(service=service)
routed_message, queue_notice, early_response = policy.prepare_message_for_single_order(
message="quero continuar a revisao e tambem ver aluguel",
user_id=9,
routing_plan={
"orders": [
{"domain": "review", "message": "quero continuar a revisao"},
{"domain": "general", "message": "oi"},
{"domain": "rental", "message": "quero ver aluguel"},
]
},
)
self.assertEqual(routed_message, "quero continuar a revisao e tambem ver aluguel")
self.assertIn("Anotei mais 1 pedido", early_response)
self.assertEqual(len(state.get_user_context(9)["order_queue"]), 1)
self.assertEqual(state.get_user_context(9)["order_queue"][0]["domain"], "rental")
self.assertEqual(state.get_user_context(9)["order_queue"][0]["message"], "quero ver aluguel")
async def test_pending_order_selection_uses_canonical_seed_message_for_selected_domain(self):
state = FakeState(
contexts={
9: {
"active_domain": "general",
"generic_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
}
}
)
service = FakePolicyService(state)
policy = ConversationPolicy(service=service)
policy.store_pending_order_selection(
user_id=9,
orders=[
{"domain": "sales", "message": "compra", "entities": service.normalizer.empty_extraction_payload()},
{"domain": "review", "message": "revisao", "entities": service.normalizer.empty_extraction_payload()},
{"domain": "rental", "message": "aluguel", "entities": service.normalizer.empty_extraction_payload()},
],
)
response = await policy.try_resolve_pending_order_selection(message="1", user_id=9)
self.assertIn("Perfeito. Vou comecar por: Venda: compra", response)
self.assertIn("handled:quero comprar um veiculo", response)
context = state.get_user_context(9)
self.assertEqual(context["active_domain"], "sales")
self.assertEqual([item["domain"] for item in context["order_queue"]], ["review", "rental"])
async def test_tool_continuar_proximo_pedido_reports_empty_queue(self):
state = FakeState(
contexts={
1: {
"active_domain": "sales",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
response = await service._tool_continuar_proximo_pedido(user_id=1)
self.assertEqual(response, "Nao ha pedidos pendentes na fila para continuar.")
async def test_tool_cancelar_fluxo_atual_preserves_queue(self):
state = FakeState(
entries={
"pending_order_drafts": {
1: {
"payload": {"vehicle_id": 7},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "sales",
"active_task": "order_create",
"generic_memory": {"orcamento_max": 70000},
"shared_memory": {"orcamento_max": 70000},
"order_queue": [{"domain": "review", "message": "agendar revisao"}],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [{"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0}],
"selected_vehicle": {"id": 7, "modelo": "Fiat Argo 2020", "categoria": "suv", "preco": 61857.0},
"last_rental_contract": {
"contrato_numero": "LOC-20260319-33CD6567",
"placa": "RAA1A02",
},
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
result = await service._tool_cancelar_fluxo_atual(user_id=1)
self.assertEqual(result["message"], "Fluxo atual de compra de veiculo cancelado.")
self.assertIsNone(state.get_entry("pending_order_drafts", 1))
self.assertEqual(state.get_user_context(1)["order_queue"], [{"domain": "review", "message": "agendar revisao"}])
async def test_tool_descartar_pedidos_pendentes_preserves_active_flow(self):
state = FakeState(
entries={
"pending_order_drafts": {
1: {
"payload": {"vehicle_id": 7},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "sales",
"active_task": "order_create",
"generic_memory": {"orcamento_max": 70000},
"shared_memory": {"orcamento_max": 70000},
"order_queue": [{"domain": "review", "message": "agendar revisao"}],
"pending_order_selection": {
"orders": [
{"domain": "sales", "message": "comprar carro"},
{"domain": "review", "message": "remarcar revisao"},
],
"expires_at": utc_now() + timedelta(minutes=15),
},
"pending_switch": {
"target_domain": "review",
"queued_message": "agendar revisao",
"expires_at": utc_now() + timedelta(minutes=15),
},
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
result = await service._tool_descartar_pedidos_pendentes(user_id=1)
self.assertEqual(result["message"], "Descartei 4 pedidos pendentes da fila.")
self.assertIsNotNone(state.get_entry("pending_order_drafts", 1))
self.assertEqual(state.get_user_context(1)["order_queue"], [])
self.assertIsNone(state.get_user_context(1)["pending_order_selection"])
self.assertIsNone(state.get_user_context(1)["pending_switch"])
async def test_handle_message_prioritizes_trade_in_evaluation_over_freeform_answer(self):
state = FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service.policy = ConversationPolicy(service=service)
service.tool_executor = FakeToolExecutor(
result={
"modelo": "Onix",
"ano": 2020,
"km": 45000,
"valor_estimado_troca": 65432.0,
}
)
service._empty_extraction_payload = service.normalizer.empty_extraction_payload
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._fallback_format_tool_result = lambda tool_name, tool_result: (
"Estimativa de troca concluida.\n"
f"Veiculo: {tool_result['modelo']} {tool_result['ano']}\n"
f"Quilometragem: {tool_result['km']} km\n"
f"Valor estimado: R$ {tool_result['valor_estimado_troca']:.2f}"
)
service._http_exception_detail = lambda exc: str(exc)
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._upsert_user_context = lambda user_id: None
async def fake_try_handle_pending_stock_selection_follow_up(**kwargs):
return None
service._try_handle_pending_stock_selection_follow_up = fake_try_handle_pending_stock_selection_follow_up
async def fake_try_handle_active_sales_follow_up(**kwargs):
return None
service._try_handle_active_sales_follow_up = fake_try_handle_active_sales_follow_up
async def fake_try_handle_active_review_follow_up(**kwargs):
return None
service._try_handle_active_review_follow_up = fake_try_handle_active_review_follow_up
async def fake_try_handle_immediate_context_reset(**kwargs):
return None
service._try_handle_immediate_context_reset = fake_try_handle_immediate_context_reset
async def fake_try_resolve_pending_order_selection(**kwargs):
return None
service._try_resolve_pending_order_selection = fake_try_resolve_pending_order_selection
async def fake_try_continue_queued_order(**kwargs):
return None
service._try_continue_queued_order = fake_try_continue_queued_order
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "general",
"domain": "general",
"action": "answer_user",
"entities": service.normalizer.empty_extraction_payload(),
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": "Legal! Para fazer a avaliacao, preciso da versao e da placa.",
}
service._extract_turn_decision_with_llm = fake_extract_turn_decision
async def fake_extract_message_plan(message: str, user_id: int | None):
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
service._extract_message_plan_with_llm = fake_extract_message_plan
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
async def fake_extract_entities(message: str, user_id: int | None):
return service.normalizer.empty_extraction_payload()
service._extract_entities_with_llm = fake_extract_entities
async def fake_extract_missing_sales_search_context_with_llm(**kwargs):
return {}
service._extract_missing_sales_search_context_with_llm = fake_extract_missing_sales_search_context_with_llm
service._domain_from_intents = lambda intents: "general"
service._handle_context_switch = lambda **kwargs: None
service._update_active_domain = lambda **kwargs: None
async def fake_try_execute_orchestration_control_tool(**kwargs):
return None
service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool
async def fake_try_handle_review_management(**kwargs):
raise AssertionError("nao deveria entrar em gerenciamento de revisao para avaliacao de troca")
service._try_handle_review_management = fake_try_handle_review_management
async def fake_try_confirm_pending_review(**kwargs):
raise AssertionError("nao deveria entrar em confirmacao de revisao para avaliacao de troca")
service._try_confirm_pending_review = fake_try_confirm_pending_review
async def fake_try_collect_and_schedule_review(**kwargs):
raise AssertionError("nao deveria entrar em agendamento de revisao para avaliacao de troca")
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
async def fake_try_collect_and_cancel_order(**kwargs):
raise AssertionError("nao deveria entrar em cancelamento de pedido para avaliacao de troca")
service._try_collect_and_cancel_order = fake_try_collect_and_cancel_order
async def fake_try_handle_order_listing(**kwargs):
raise AssertionError("nao deveria entrar em listagem de pedidos para avaliacao de troca")
service._try_handle_order_listing = fake_try_handle_order_listing
async def fake_try_collect_and_create_order(**kwargs):
raise AssertionError("nao deveria entrar em compra para avaliacao de troca")
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message(
"Quero avaliar meu carro para troca: Onix 2020, 45000 km",
user_id=1,
)
self.assertIn("Estimativa de troca concluida", response)
self.assertEqual(
service.tool_executor.calls,
[("avaliar_veiculo_troca", {"modelo": "Onix", "ano": 2020, "km": 45000}, 1)],
)
async def test_active_review_follow_up_ignores_trade_in_request(self):
state = FakeState(
entries={
"pending_review_drafts": {
1: {
"payload": {"placa": "ABC1234"},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
},
contexts={
1: {
"active_domain": "review",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = state
service.normalizer = EntityNormalizer()
service._get_user_context = lambda user_id: state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: state.save_user_context(user_id, context)
async def fake_try_collect_and_schedule_review(**kwargs):
raise AssertionError("nao deveria consumir avaliacao de troca como follow-up de revisao")
service._try_collect_and_schedule_review = fake_try_collect_and_schedule_review
async def finish(response: str, queue_notice: str | None = None):
return response
response = await service._try_handle_active_review_follow_up(
message="Quero avaliar meu carro para troca: Onix 2020, 45000 km",
user_id=1,
finish=finish,
)
self.assertIsNone(response)
class OrquestradorLatencyOptimizationTests(unittest.IsolatedAsyncioTestCase):
def _build_service(self, state=None):
default_state = state or FakeState(
contexts={
1: {
"active_domain": "general",
"generic_memory": {},
"shared_memory": {},
"order_queue": [],
"pending_order_selection": None,
"pending_switch": None,
"last_stock_results": [],
"selected_vehicle": None,
}
}
)
service = OrquestradorService.__new__(OrquestradorService)
service.state = default_state
service.normalizer = EntityNormalizer()
service.history_service = SimpleNamespace(record_turn=lambda **kwargs: None)
service._get_user_context = lambda user_id: default_state.get_user_context(user_id)
service._save_user_context = lambda user_id, context: default_state.save_user_context(user_id, context)
service._log_turn_event = lambda *args, **kwargs: None
service._compose_order_aware_response = lambda response, user_id, queue_notice=None: response
service._append_email_capture_prompt_if_needed = lambda response, user_id: response
service._finalize_turn_history = lambda **kwargs: None
service._upsert_user_context = lambda user_id: None
service._ensure_user_email_routes = lambda user_id: None
service._capture_turn_decision_trace = lambda turn_decision: None
service._capture_generic_memory = lambda **kwargs: None
service._domain_from_intents = lambda intents: "general"
service._handle_context_switch = lambda **kwargs: None
service._update_active_domain = lambda **kwargs: None
async def fake_maybe_auto_advance_next_order(base_response: str, user_id: int | None):
return base_response
async def fake_none(**kwargs):
return None
service._maybe_auto_advance_next_order = fake_maybe_auto_advance_next_order
service._try_handle_pending_email_capture_message = fake_none
service._try_handle_pending_stock_selection_follow_up = fake_none
service._try_handle_active_sales_follow_up = fake_none
service._try_handle_pending_rental_selection_follow_up = fake_none
service._try_handle_active_rental_follow_up = fake_none
service._try_handle_active_review_follow_up = fake_none
service._try_handle_current_rental_info_request = fake_none
service._try_handle_immediate_context_reset = fake_none
service._try_resolve_pending_order_selection = fake_none
service._try_continue_queued_order = fake_none
service._try_handle_deterministic_rental_management = fake_none
service._try_execute_orchestration_control_tool = fake_none
service._try_handle_trade_in_evaluation = fake_none
service._try_execute_business_tool_from_turn_decision = fake_none
service._try_handle_review_management = fake_none
service._try_confirm_pending_review = fake_none
service._try_collect_and_schedule_review = fake_none
service._try_collect_and_cancel_order = fake_none
service._try_handle_order_listing = fake_none
service._try_collect_and_open_rental = fake_none
service._extract_missing_sales_search_context_with_llm = fake_none
service._prepare_message_for_single_order = lambda message, user_id, routing_plan=None: (message, None, None)
service._resolve_entities_for_message_plan = lambda message_plan, routed_message: service.normalizer.empty_extraction_payload()
return service
async def test_handle_message_uses_deterministic_rental_bootstrap_before_llm(self):
service = self._build_service()
rental_calls = []
async def fake_try_collect_and_open_rental(**kwargs):
rental_calls.append(kwargs)
return "Fluxo de locacao continuado."
async def should_not_run(*args, **kwargs):
raise AssertionError("nao deveria consultar o LLM para aluguel explicito")
service._try_collect_and_open_rental = fake_try_collect_and_open_rental
service._extract_turn_bundle_with_llm = should_not_run
service._extract_turn_decision_with_llm = should_not_run
service._extract_message_plan_with_llm = should_not_run
service._extract_entities_with_llm = should_not_run
response = await service.handle_message(
"Quero alugar um hatch amanha 10h ate depois de amanha 10h",
user_id=1,
)
self.assertEqual(response, "Fluxo de locacao continuado.")
self.assertEqual(len(rental_calls), 1)
self.assertEqual(rental_calls[0]["message"], "Quero alugar um hatch amanha 10h ate depois de amanha 10h")
self.assertEqual(rental_calls[0]["turn_decision"]["intent"], "rental_create")
self.assertEqual(rental_calls[0]["turn_decision"]["domain"], "rental")
async def test_handle_message_keeps_message_plan_but_skips_entity_extraction_when_turn_decision_is_enough(self):
service = self._build_service()
planner_calls = []
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "order_create",
"domain": "sales",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": ["modelo_veiculo"],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
}
async def fake_extract_message_plan(message: str, user_id: int | None):
planner_calls.append((message, user_id))
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
async def should_not_run_entities(message: str, user_id: int | None):
raise AssertionError("extracao dedicada nao deveria rodar quando a decisao ja trouxe entidades")
async def fake_try_collect_and_create_order(**kwargs):
return "Fluxo de venda continuado."
service._extract_turn_decision_with_llm = fake_extract_turn_decision
service._extract_message_plan_with_llm = fake_extract_message_plan
service._extract_entities_with_llm = should_not_run_entities
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message("quero comprar um carro ate 70 mil", user_id=1)
self.assertEqual(len(planner_calls), 1)
self.assertEqual(response, "Fluxo de venda continuado.")
async def test_handle_message_skips_entity_extraction_when_trade_in_tool_arguments_are_already_present(self):
service = self._build_service()
planner_calls = []
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "general",
"domain": "sales",
"action": "call_tool",
"entities": service.normalizer.empty_extraction_payload(),
"missing_fields": [],
"selection_index": None,
"tool_name": "avaliar_veiculo_troca",
"tool_arguments": {"modelo": "Onix", "ano": 2020, "km": 45000},
"response_to_user": None,
}
async def fake_extract_message_plan(message: str, user_id: int | None):
planner_calls.append((message, user_id))
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
async def should_not_run_entities(message: str, user_id: int | None):
raise AssertionError("extracao dedicada nao deveria rodar quando a decisao de troca ja trouxe tool_arguments completos")
async def fake_try_handle_trade_in_evaluation(**kwargs):
extracted_entities = kwargs.get("extracted_entities") or {}
review_fields = extracted_entities.get("review_fields") or {}
self.assertEqual(review_fields.get("modelo"), "Onix")
self.assertEqual(review_fields.get("ano"), 2020)
self.assertEqual(review_fields.get("km"), 45000)
return "Estimativa de troca concluida."
service._extract_turn_decision_with_llm = fake_extract_turn_decision
service._extract_message_plan_with_llm = fake_extract_message_plan
service._extract_entities_with_llm = should_not_run_entities
service._try_handle_trade_in_evaluation = fake_try_handle_trade_in_evaluation
response = await service.handle_message("Quero avaliar meu carro para troca: Onix 2020, 45000 km", user_id=1)
self.assertEqual(len(planner_calls), 1)
self.assertEqual(response, "Estimativa de troca concluida.")
async def test_handle_message_runs_entity_extraction_when_turn_decision_entities_are_empty(self):
service = self._build_service()
planner_calls = []
entity_calls = []
async def fake_extract_turn_decision(message: str, user_id: int | None):
return {
"intent": "order_create",
"domain": "sales",
"action": "ask_missing_fields",
"entities": service.normalizer.empty_extraction_payload(),
"missing_fields": ["modelo_veiculo"],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
}
async def fake_extract_message_plan(message: str, user_id: int | None):
planner_calls.append((message, user_id))
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
async def fake_extract_entities(message: str, user_id: int | None):
entity_calls.append((message, user_id))
return {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
}
async def fake_try_collect_and_create_order(**kwargs):
return "Fluxo de venda continuado."
service._extract_turn_decision_with_llm = fake_extract_turn_decision
service._extract_message_plan_with_llm = fake_extract_message_plan
service._extract_entities_with_llm = fake_extract_entities
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message("quero comprar um carro ate 70 mil", user_id=1)
self.assertEqual(len(planner_calls), 1)
self.assertEqual(len(entity_calls), 1)
self.assertEqual(response, "Fluxo de venda continuado.")
async def test_handle_message_reuses_orchestration_router_tool_call_without_second_router(self):
service = self._build_service()
async def fake_extract_turn_bundle(message: str, user_id: int | None):
return {
"turn_decision": {
"intent": "general",
"domain": "general",
"action": "answer_user",
"entities": {
"generic_memory": {"orcamento_max": 80000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
},
"message_plan": {
"orders": [
{
"domain": "general",
"message": message,
"entities": {
"generic_memory": {"orcamento_max": 80000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
"intents": {},
},
}
]
},
"has_turn_decision": True,
"has_message_plan": True,
}
async def should_not_run_router(**kwargs):
raise AssertionError("nao deveria consultar o router quando o orchestration_router ja trouxe tool_call reutilizavel")
async def fake_try_execute_orchestration_control_tool(**kwargs):
return {
"source": "orchestration_router",
"llm_result": {
"response": "",
"tool_call": {
"name": "consultar_estoque",
"arguments": {"preco_max": 80000.0, "limite": 5},
},
},
}
async def fake_execute_tool_with_trace(tool_name, arguments, user_id=None):
return [
{"id": 1, "modelo": "Toyota Corolla 2020", "categoria": "hatch", "preco": 39809.0},
]
async def fake_maybe_build_stock_suggestion_response(**kwargs):
return "Estoque reutilizado do primeiro router."
service._extract_turn_bundle_with_llm = fake_extract_turn_bundle
service._try_execute_orchestration_control_tool = fake_try_execute_orchestration_control_tool
service._call_llm_with_trace = should_not_run_router
service._execute_tool_with_trace = fake_execute_tool_with_trace
service._maybe_build_stock_suggestion_response = fake_maybe_build_stock_suggestion_response
service._capture_successful_tool_side_effects = lambda **kwargs: None
class DummyRegistry:
def get_tools(self):
return []
service.registry = DummyRegistry()
response = await service.handle_message("Quero ver carros ate 80000 reais", user_id=1)
self.assertEqual(response, "Estoque reutilizado do primeiro router.")
async def test_handle_message_uses_turn_bundle_when_available(self):
service = self._build_service()
bundle_calls = []
async def fake_extract_turn_bundle(message: str, user_id: int | None):
bundle_calls.append((message, user_id))
return {
"turn_decision": {
"intent": "order_create",
"domain": "sales",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": ["modelo_veiculo"],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
},
"message_plan": {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
},
"has_turn_decision": True,
"has_message_plan": True,
}
async def should_not_run_turn_decision(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar turn_decision legado quando o bundle estiver completo")
async def should_not_run_message_plan(message: str, user_id: int | None):
raise AssertionError("nao deveria consultar message_plan legado quando o bundle estiver completo")
async def fake_try_collect_and_create_order(**kwargs):
return "Fluxo de venda continuado."
service._extract_turn_bundle_with_llm = fake_extract_turn_bundle
service._extract_turn_decision_with_llm = should_not_run_turn_decision
service._extract_message_plan_with_llm = should_not_run_message_plan
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message("quero comprar um carro ate 70 mil", user_id=1)
self.assertEqual(len(bundle_calls), 1)
self.assertEqual(response, "Fluxo de venda continuado.")
async def test_handle_message_reuses_partial_bundle_turn_decision_when_message_plan_is_missing(self):
service = self._build_service()
turn_decision_calls = []
message_plan_calls = []
async def fake_extract_turn_bundle(message: str, user_id: int | None):
return {
"turn_decision": {
"intent": "order_create",
"domain": "sales",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": ["modelo_veiculo"],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
},
"message_plan": service.normalizer.empty_message_plan(message),
"has_turn_decision": True,
"has_message_plan": False,
}
async def should_not_run_turn_decision(message: str, user_id: int | None):
turn_decision_calls.append((message, user_id))
raise AssertionError("nao deveria consultar turn_decision legado quando o bundle ja trouxe decisao util")
async def fake_extract_message_plan(message: str, user_id: int | None):
message_plan_calls.append((message, user_id))
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
async def fake_try_collect_and_create_order(**kwargs):
return "Fluxo de venda continuado."
service._extract_turn_bundle_with_llm = fake_extract_turn_bundle
service._extract_turn_decision_with_llm = should_not_run_turn_decision
service._extract_message_plan_with_llm = fake_extract_message_plan
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message("quero comprar um carro ate 70 mil", user_id=1)
self.assertEqual(len(turn_decision_calls), 0)
self.assertEqual(len(message_plan_calls), 1)
self.assertEqual(response, "Fluxo de venda continuado.")
async def test_handle_message_reuses_partial_bundle_message_plan_when_turn_decision_is_missing(self):
service = self._build_service()
turn_decision_calls = []
message_plan_calls = []
async def fake_extract_turn_bundle(message: str, user_id: int | None):
return {
"turn_decision": service.normalizer.empty_turn_decision(),
"message_plan": {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
},
"has_turn_decision": False,
"has_message_plan": True,
}
async def fake_extract_turn_decision(message: str, user_id: int | None):
turn_decision_calls.append((message, user_id))
return {
"intent": "order_create",
"domain": "sales",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": ["modelo_veiculo"],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
}
async def should_not_run_message_plan(message: str, user_id: int | None):
message_plan_calls.append((message, user_id))
raise AssertionError("nao deveria consultar message_plan legado quando o bundle ja trouxe plano util")
async def fake_try_collect_and_create_order(**kwargs):
return "Fluxo de venda continuado."
service._extract_turn_bundle_with_llm = fake_extract_turn_bundle
service._extract_turn_decision_with_llm = fake_extract_turn_decision
service._extract_message_plan_with_llm = should_not_run_message_plan
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message("quero comprar um carro ate 70 mil", user_id=1)
self.assertEqual(len(turn_decision_calls), 1)
self.assertEqual(len(message_plan_calls), 0)
self.assertEqual(response, "Fluxo de venda continuado.")
async def test_handle_message_falls_back_to_legacy_turn_decision_and_message_plan_when_bundle_is_incomplete(self):
service = self._build_service()
bundle_calls = []
turn_decision_calls = []
message_plan_calls = []
async def fake_extract_turn_bundle(message: str, user_id: int | None):
bundle_calls.append((message, user_id))
return {
"turn_decision": service.normalizer.empty_turn_decision(),
"message_plan": service.normalizer.empty_message_plan(message),
"has_turn_decision": True,
"has_message_plan": False,
}
async def fake_extract_turn_decision(message: str, user_id: int | None):
turn_decision_calls.append((message, user_id))
return {
"intent": "order_create",
"domain": "sales",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": ["modelo_veiculo"],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
}
async def fake_extract_message_plan(message: str, user_id: int | None):
message_plan_calls.append((message, user_id))
return {
"orders": [
{
"domain": "sales",
"message": message,
"entities": service.normalizer.empty_extraction_payload(),
}
]
}
async def fake_try_collect_and_create_order(**kwargs):
return "Fluxo de venda continuado."
service._extract_turn_bundle_with_llm = fake_extract_turn_bundle
service._extract_turn_decision_with_llm = fake_extract_turn_decision
service._extract_message_plan_with_llm = fake_extract_message_plan
service._try_collect_and_create_order = fake_try_collect_and_create_order
response = await service.handle_message("quero comprar um carro ate 70 mil", user_id=1)
self.assertEqual(len(bundle_calls), 1)
self.assertEqual(len(turn_decision_calls), 1)
self.assertEqual(len(message_plan_calls), 1)
self.assertEqual(response, "Fluxo de venda continuado.")
class OrquestradorEmailCaptureTests(unittest.IsolatedAsyncioTestCase):
def _build_service(self, state=None):
service = OrquestradorService.__new__(OrquestradorService)
service.state = state or FakeState()
service.normalizer = EntityNormalizer()
service._turn_trace = {"request_id": "req-1"}
return service
def test_stage_email_capture_request_and_prompt_for_current_turn(self):
service = self._build_service()
service._get_saved_user_email = lambda user_id: None
service._stage_email_capture_request(
tool_name="realizar_pedido",
tool_result={"numero_pedido": "PED-1"},
user_id=7,
)
pending = service.state.get_entry("pending_email_capture_requests", 7)
self.assertIsNotNone(pending)
self.assertEqual(pending["event_type"], ORDER_CREATED_EVENT)
self.assertEqual(pending["payload"]["numero_pedido"], "PED-1")
self.assertEqual(pending["payload"]["user_id"], 7)
response = service._append_email_capture_prompt_if_needed(
response="Pedido criado com sucesso.",
user_id=7,
)
self.assertIn("Se quiser, posso te enviar esse resumo por e-mail.", response)
async def test_pending_email_capture_decline_clears_request(self):
state = FakeState(
entries={
"pending_email_capture_requests": {
7: {
"request_id": "req-1",
"event_type": ORDER_CREATED_EVENT,
"payload": {"numero_pedido": "PED-1", "user_id": 7},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
}
)
service = self._build_service(state=state)
response = await service._try_handle_pending_email_capture_message(
message="prefiro nao informar",
user_id=7,
)
self.assertEqual(response, "Tudo bem. Nao vou enviar este resumo por e-mail.")
self.assertIsNone(state.get_entry("pending_email_capture_requests", 7))
def test_ensure_user_email_routes_syncs_global_routes_only_once(self):
service = self._build_service()
service._get_user_record = lambda user_id: SimpleNamespace(
id=user_id,
email="cliente@example.com",
name="Cliente Teste",
)
with patch(
"app.services.orchestration.orquestrador_service.sync_user_email_integration_routes"
) as sync_routes_mock:
service._ensure_user_email_routes(user_id=7)
service._ensure_user_email_routes(user_id=7)
sync_routes_mock.assert_called_once_with(
user_id=7,
recipient_email="cliente@example.com",
recipient_name="Cliente Teste",
)
async def test_pending_email_capture_success_saves_email_and_reemits_event(self):
state = FakeState(
entries={
"pending_email_capture_requests": {
7: {
"request_id": "req-1",
"event_type": ORDER_CREATED_EVENT,
"payload": {"numero_pedido": "PED-1", "user_id": 7},
"expires_at": utc_now() + timedelta(minutes=15),
}
}
}
)
service = self._build_service(state=state)
saved = {}
ensured_routes = []
def fake_save_user_email(user_id: int | None, email: str | None):
saved["user_id"] = user_id
saved["email"] = email
return SimpleNamespace(id=user_id, email=email, name="Cliente Teste")
service._save_user_email = fake_save_user_email
service._ensure_user_email_routes = lambda user_id: ensured_routes.append(user_id)
with patch(
"app.services.orchestration.orquestrador_service.emit_business_event",
new=AsyncMock(return_value=[{"status": "sent", "provider_message_id": "brevo-1"}]),
) as emit_business_event_mock:
response = await service._try_handle_pending_email_capture_message(
message="cliente@example.com",
user_id=7,
)
self.assertEqual(saved, {"user_id": 7, "email": "cliente@example.com"})
self.assertEqual(ensured_routes, [7])
emit_business_event_mock.assert_awaited_once_with(
event_type=ORDER_CREATED_EVENT,
payload={"numero_pedido": "PED-1", "user_id": 7},
)
self.assertIn("enviei este resumo por la", response)
self.assertIsNone(state.get_entry("pending_email_capture_requests", 7))
if __name__ == "__main__":
unittest.main()