🐛 fix(review): estabilizar follow-ups curtos e remarcacao incremental no Telegram

- consome follow-ups curtos de revisao aberta antes do LLM para preservar data, horario e rascunhos ativos entre mensagens

- melhora a extracao de modelo a partir de resumos curtos e aceita respostas isoladas como Onix quando esse eh o ultimo campo faltante

- faz a remarcacao aceitar amanha 11h ou amanha seguido de 11h sem cair em um novo agendamento de revisao

- prioriza review_reschedule, review_cancel e review_list sobre respostas livres do modelo e amplia a cobertura de regressao da orquestracao
main
parent 71592c544e
commit 9b6b2a643b

@ -145,6 +145,77 @@ class ReviewFlowMixin:
return "general"
return str(context.get("active_domain") or "general").strip().lower()
def _clean_review_model_candidate(self, raw_model: str | None) -> str | None:
text = str(raw_model or "").strip(" ,.;:-")
if not text:
return None
text = re.sub(r"\s+", " ", text)
text = re.sub(r"\be\b$", "", text).strip(" ,.;:-")
if not text:
return None
stop_terms = {
"amanha",
"hoje",
"revisao",
"agendar",
"marcar",
"cancelar",
"pedido",
"sim",
"nao",
"ok",
"pode",
}
lowered = text.lower()
if lowered in stop_terms:
return None
if any(term in lowered for term in {"agendar revisao", "marcar revisao", "cancelar revisao"}):
return None
if not re.search(r"[a-z]", lowered):
return None
if len(text.split()) > 4:
return None
return text.title()
def _extract_review_model_from_message(self, normalized_message: str) -> str | None:
explicit_match = re.search(
r"(?:modelo do meu carro (?:e|eh)?|meu carro (?:e|eh)?|carro (?:e|eh)?|veiculo (?:e|eh)?)\s+([a-z0-9][a-z0-9\s-]{1,30})",
normalized_message,
flags=re.IGNORECASE,
)
if explicit_match:
raw_model = explicit_match.group(1)
raw_model = re.split(r"\b(?:ele e|ele eh|ano|placa|km|quilometragem|data|amanha|hoje)\b", raw_model, maxsplit=1)[0]
return self._clean_review_model_candidate(raw_model)
has_year = bool(re.search(r"(?<!\d)(19\d{2}|20\d{2}|2100)(?!\d)", normalized_message))
has_km = bool(re.search(r"(?<!\d)(\d{1,3}(?:[.\s]\d{3})+|\d{2,6})\s*km\b", normalized_message, flags=re.IGNORECASE))
has_review_history = any(
term in normalized_message
for term in {
"nunca fiz revisao",
"nao fiz revisao",
"nunca revisei",
"ja fiz revisao",
"fiz revisao",
"ja revisei",
}
)
token_count = len([token for token in normalized_message.split() if token])
if not (has_year or has_km or has_review_history or token_count <= 3):
return None
raw_model = normalized_message
raw_model = re.split(r"(?<!\d)(?:19\d{2}|20\d{2}|2100)(?!\d)", raw_model, maxsplit=1)[0]
raw_model = re.split(r"(?<!\d)(?:\d{1,3}(?:[.\s]\d{3})+|\d{2,6})\s*km\b", raw_model, maxsplit=1, flags=re.IGNORECASE)[0]
raw_model = re.split(
r"\b(?:placa|quilometragem|data|amanha|hoje|nunca fiz revisao|nao fiz revisao|nunca revisei|ja fiz revisao|fiz revisao|ja revisei|na concessionaria|concessionaria)\b",
raw_model,
maxsplit=1,
)[0]
raw_model = re.sub(r"^(?:modelo(?: do meu carro)?|meu carro|carro|veiculo)\s+(?:e|eh)?\s+", "", raw_model).strip()
return self._clean_review_model_candidate(raw_model)
def _supplement_review_fields_from_message(self, message: str, payload: dict) -> None:
if not isinstance(payload, dict):
return
@ -185,18 +256,9 @@ class ReviewFlowMixin:
payload["ano"] = int(year_match.group(1))
if "modelo" not in payload:
model_match = re.search(
r"(?:modelo do meu carro (?:e|eh)?|meu carro (?:e|eh)?|carro (?:e|eh)?|veiculo (?:e|eh)?)\s+([a-z0-9][a-z0-9\s-]{1,30})",
normalized_message,
flags=re.IGNORECASE,
)
if model_match:
raw_model = model_match.group(1)
raw_model = re.split(r"\b(?:ele e|ele eh|ano|placa|km|quilometragem|data|amanha|hoje)\b", raw_model, maxsplit=1)[0]
raw_model = raw_model.strip(" ,.;:-")
raw_model = re.sub(r"\be\b$", "", raw_model).strip(" ,.;:-")
if raw_model:
payload["modelo"] = raw_model.title()
extracted_model = self._extract_review_model_from_message(normalized_message)
if extracted_model:
payload["modelo"] = extracted_model
def _extract_review_date_only_text(self, message: str) -> str | None:
text = self.normalizer.normalize_datetime_connector(message)
@ -241,6 +303,33 @@ class ReviewFlowMixin:
return
payload["data_hora_base"] = date_only
def _extract_review_management_datetime_from_message(self, message: str) -> str | None:
return self.normalizer.normalize_review_datetime_text(
message,
now_provider=self._review_now,
)
def _merge_review_management_base_date_with_time(self, message: str, payload: dict) -> None:
if not isinstance(payload, dict):
return
if payload.get("nova_data_hora") or not payload.get("nova_data_hora_base"):
return
time_text = self.normalizer.extract_hhmm_from_text(message)
if not time_text:
return
payload["nova_data_hora"] = f"{payload['nova_data_hora_base']} {time_text}"
payload.pop("nova_data_hora_base", None)
def _store_review_management_base_date_from_message(self, message: str, payload: dict) -> None:
if not isinstance(payload, dict):
return
if payload.get("nova_data_hora") or payload.get("nova_data_hora_base"):
return
date_only = self._extract_review_date_only_text(message)
if not date_only:
return
payload["nova_data_hora_base"] = date_only
def _is_review_temporal_follow_up(self, message: str, payload: dict | None) -> bool:
if not isinstance(payload, dict):
return False
@ -396,6 +485,10 @@ class ReviewFlowMixin:
extracted["protocolo"] = inferred_protocol
action = draft.get("action", "cancel")
if action == "reschedule" and "nova_data_hora" not in extracted:
normalized_new_datetime = self._extract_review_management_datetime_from_message(message)
if normalized_new_datetime:
extracted["nova_data_hora"] = normalized_new_datetime
if (
action == "cancel"
and "motivo" not in extracted
@ -407,6 +500,10 @@ class ReviewFlowMixin:
extracted["motivo"] = free_text
draft["payload"].update(extracted)
if action == "reschedule":
self._merge_review_management_base_date_with_time(message=message, payload=draft["payload"])
if "nova_data_hora" not in draft["payload"]:
self._store_review_management_base_date_from_message(message=message, payload=draft["payload"])
draft["expires_at"] = utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES)
self._set_review_flow_entry(
"pending_review_management_drafts",
@ -419,6 +516,11 @@ class ReviewFlowMixin:
if action == "reschedule":
missing = [field for field in ("protocolo", "nova_data_hora") if field not in draft["payload"]]
if missing:
if missing == ["nova_data_hora"] and draft["payload"].get("nova_data_hora_base"):
return (
f"Perfeito. Tenho a data {draft['payload']['nova_data_hora_base']}. "
"Agora me informe o horario desejado para a revisao."
)
return self._render_missing_review_reschedule_fields_prompt(missing)
try:
tool_result = await self.tool_executor.execute(

@ -112,6 +112,13 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin):
)
if active_sales_follow_up:
return active_sales_follow_up
active_review_follow_up = await self._try_handle_active_review_follow_up(
message=message,
user_id=user_id,
finish=finish,
)
if active_review_follow_up:
return active_review_follow_up
# Faz uma leitura inicial do turno para ajudar a policy
# com fila, troca de contexto e comandos globais.
early_turn_decision = await self._extract_turn_decision_with_llm(
@ -231,6 +238,10 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin):
user_id=user_id,
message=routing_message,
)
should_prioritize_review_management = self._should_prioritize_review_management(
turn_decision=turn_decision,
user_id=user_id,
)
domain_hint = self._domain_from_turn_decision(turn_decision)
if domain_hint == "general":
domain_hint = self._domain_from_intents(extracted_entities.get("intents", {}))
@ -268,6 +279,7 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin):
decision_action == "ask_missing_fields"
and decision_response
and not should_prioritize_review_flow
and not should_prioritize_review_management
and not should_prioritize_order_flow
):
return await finish(decision_response, queue_notice=queue_notice)
@ -275,6 +287,7 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin):
decision_action == "answer_user"
and decision_response
and not should_prioritize_review_flow
and not should_prioritize_review_management
and not should_prioritize_order_flow
):
return await finish(decision_response, queue_notice=queue_notice)
@ -642,6 +655,86 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin):
return None
async def _try_handle_active_review_follow_up(
self,
message: str,
user_id: int | None,
finish,
) -> str | None:
if user_id is None:
return None
context = self._get_user_context(user_id)
if not isinstance(context, dict):
return None
if str(context.get("active_domain") or "").strip().lower() != "review":
return None
if (
self._has_explicit_order_request(message)
or self._has_order_listing_request(message)
or self._has_stock_listing_request(message)
):
return None
pending_management_draft = self.state.get_entry(
"pending_review_management_drafts",
user_id,
expire=True,
)
if pending_management_draft:
management_action = "review_reschedule"
if str(pending_management_draft.get("action") or "").strip().lower() == "cancel":
management_action = "review_cancel"
response = await self._try_handle_review_management(
message=message,
user_id=user_id,
extracted_fields={},
intents={},
turn_decision={
"intent": management_action,
"domain": "review",
"action": "answer_user",
},
)
if response:
return await finish(response)
pending_review_confirmation = self.state.get_entry(
"pending_review_confirmations",
user_id,
expire=True,
)
if pending_review_confirmation:
response = await self._try_confirm_pending_review(
message=message,
user_id=user_id,
extracted_review_fields={},
)
if response:
return await finish(response)
has_open_review_schedule = bool(
self.state.get_entry("pending_review_drafts", user_id, expire=True)
or self.state.get_entry("pending_review_reuse_confirmations", user_id, expire=True)
)
if not has_open_review_schedule:
return None
response = await self._try_collect_and_schedule_review(
message=message,
user_id=user_id,
extracted_fields={},
intents={},
turn_decision={
"intent": "review_schedule",
"domain": "review",
"action": "collect_review_schedule",
},
)
if not response:
return None
return await finish(response)
async def _try_execute_business_tool_from_turn_decision(
self,
message: str,
@ -1318,6 +1411,21 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin):
)
return any(term in normalized_message for term in shift_terms)
def _should_prioritize_review_management(
self,
turn_decision: dict | None,
user_id: int | None = None,
) -> bool:
has_open_management_draft = bool(
user_id is not None
and self.state.get_entry("pending_review_management_drafts", user_id, expire=True)
)
if has_open_management_draft:
return True
decision_intent = str((turn_decision or {}).get("intent") or "").strip().lower()
return decision_intent in {"review_list", "review_cancel", "review_reschedule"}
def _should_prioritize_review_flow(
self,
turn_decision: dict | None,

@ -1584,6 +1584,73 @@ class ReviewFlowDraftTests(unittest.IsolatedAsyncioTestCase):
self.assertFalse(arguments.get("revisao_previa_concessionaria"))
self.assertIn("REV-TESTE-123", response)
async def test_review_flow_extracts_short_vehicle_summary_from_free_text(self):
state = FakeState(
entries={
"pending_review_drafts": {
21: {
"payload": {"placa": "ABC1269", "data_hora": "13/03/2026 16:00"},
"expires_at": utc_now() + timedelta(minutes=30),
}
}
}
)
registry = FakeRegistry()
flow = ReviewFlowHarness(state=state, registry=registry)
response = await flow._try_collect_and_schedule_review(
message="Onix 2024, 12000 km, nao fiz revisao na concessionaria",
user_id=21,
extracted_fields={},
intents={},
turn_decision={"intent": "review_schedule", "domain": "review", "action": "answer_user"},
)
self.assertIsNone(state.get_entry("pending_review_drafts", 21))
self.assertEqual(registry.calls[0][0], "agendar_revisao")
_, arguments, tool_user_id = registry.calls[0]
self.assertEqual(tool_user_id, 21)
self.assertEqual(arguments.get("modelo"), "Onix")
self.assertEqual(arguments.get("ano"), 2024)
self.assertEqual(arguments.get("km"), 12000)
self.assertFalse(arguments.get("revisao_previa_concessionaria"))
self.assertIn("REV-TESTE-123", response)
async def test_review_flow_accepts_bare_model_when_it_is_last_missing_field(self):
state = FakeState(
entries={
"pending_review_drafts": {
21: {
"payload": {
"placa": "ABC1269",
"data_hora": "13/03/2026 16:00",
"ano": 2024,
"km": 12000,
"revisao_previa_concessionaria": False,
},
"expires_at": utc_now() + timedelta(minutes=30),
}
}
}
)
registry = FakeRegistry()
flow = ReviewFlowHarness(state=state, registry=registry)
response = await flow._try_collect_and_schedule_review(
message="Onix",
user_id=21,
extracted_fields={},
intents={},
turn_decision={"intent": "review_schedule", "domain": "review", "action": "answer_user"},
)
self.assertIsNone(state.get_entry("pending_review_drafts", 21))
self.assertEqual(registry.calls[0][0], "agendar_revisao")
_, arguments, tool_user_id = registry.calls[0]
self.assertEqual(tool_user_id, 21)
self.assertEqual(arguments.get("modelo"), "Onix")
self.assertIn("REV-TESTE-123", response)
async def test_review_flow_keeps_plate_and_datetime_across_incremental_messages(self):
fixed_now = lambda: datetime(2026, 3, 12, 9, 0)
state = FakeState()
@ -2199,6 +2266,78 @@ class ReviewFlowDraftTests(unittest.IsolatedAsyncioTestCase):
self.assertIn("cancelar_agendamento_revisao", response)
self.assertIn("REV-20260313-F754AF27", response)
async def test_review_management_reschedule_consumes_relative_datetime_follow_up(self):
fixed_now = lambda: datetime(2026, 3, 12, 9, 0)
state = FakeState(
entries={
"pending_review_management_drafts": {
21: {
"action": "reschedule",
"payload": {"protocolo": "REV-20260313-F754AF27"},
"expires_at": utc_now() + timedelta(minutes=30),
}
}
}
)
registry = FakeRegistry()
flow = ReviewFlowHarness(state=state, registry=registry, review_now_provider=fixed_now)
response = await flow._try_handle_review_management(
message="amanha 11h",
user_id=21,
extracted_fields={},
intents={},
turn_decision={"intent": "review_reschedule", "domain": "review", "action": "answer_user"},
)
self.assertIsNone(state.get_entry("pending_review_management_drafts", 21))
self.assertEqual(registry.calls[0][0], "editar_data_revisao")
self.assertEqual(registry.calls[0][1]["protocolo"], "REV-20260313-F754AF27")
self.assertEqual(registry.calls[0][1]["nova_data_hora"], "13/03/2026 11:00")
self.assertIn("13/03/2026 11:00", response)
async def test_review_management_reschedule_date_only_then_time_follow_up(self):
fixed_now = lambda: datetime(2026, 3, 12, 9, 0)
state = FakeState(
entries={
"pending_review_management_drafts": {
21: {
"action": "reschedule",
"payload": {"protocolo": "REV-20260313-F754AF27"},
"expires_at": utc_now() + timedelta(minutes=30),
}
}
}
)
registry = FakeRegistry()
flow = ReviewFlowHarness(state=state, registry=registry, review_now_provider=fixed_now)
first_response = await flow._try_handle_review_management(
message="amanha",
user_id=21,
extracted_fields={},
intents={},
turn_decision={"intent": "review_reschedule", "domain": "review", "action": "answer_user"},
)
draft = state.get_entry("pending_review_management_drafts", 21)
self.assertIsNotNone(draft)
self.assertEqual(draft["payload"].get("nova_data_hora_base"), "13/03/2026")
self.assertIn("Perfeito. Tenho a data 13/03/2026.", first_response)
second_response = await flow._try_handle_review_management(
message="11h",
user_id=21,
extracted_fields={},
intents={},
turn_decision={"intent": "review_reschedule", "domain": "review", "action": "answer_user"},
)
self.assertIsNone(state.get_entry("pending_review_management_drafts", 21))
self.assertEqual(registry.calls[0][0], "editar_data_revisao")
self.assertEqual(registry.calls[0][1]["nova_data_hora"], "13/03/2026 11:00")
self.assertIn("13/03/2026 11:00", second_response)
async def test_review_management_infers_listing_intent_from_agendamentos_message(self):
state = FakeState()
registry = FakeRegistry()

@ -1312,6 +1312,140 @@ class TurnDecisionContractTests(unittest.IsolatedAsyncioTestCase):
self.assertTrue(prioritized)
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={
@ -1832,6 +1966,199 @@ class TurnDecisionContractTests(unittest.IsolatedAsyncioTestCase):
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={

Loading…
Cancel
Save