From 0eb56f1f0a63a4bac924ace0e6d8cc9240cad2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vitor=20Hugo=20Belorio=20Sim=C3=A3o?= Date: Wed, 11 Mar 2026 11:05:40 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sales):=20extrair=20orcament?= =?UTF-8?q?o=20do=20pedido=20sem=20depender=20do=20llm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - adicionar fallback tecnico para capturar orcamento diretamente da mensagem de compra - inferir perfil de veiculo no fluxo de vendas quando a memoria generica vier incompleta - garantir a listagem automatica de estoque mesmo quando o modelo nao preencher orcamento_max - cobrir o cenario com teste focado de pedido sem hints estruturados do llm --- app/services/flows/order_flow.py | 45 ++++++++++++++++++++++++++ tests/test_conversation_adjustments.py | 35 ++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/app/services/flows/order_flow.py b/app/services/flows/order_flow.py index 323ec3d..1f4d314 100644 --- a/app/services/flows/order_flow.py +++ b/app/services/flows/order_flow.py @@ -53,6 +53,50 @@ class OrderFlowMixin: } return any(term in normalized for term in stock_terms) + def _extract_budget_hint_from_message(self, message: str) -> float | None: + normalized = self._normalize_text(message) + patterns = ( + r"(?:ate|até|de|por|valor|orcamento|orcamento de)\s*r?\$?\s*(\d{1,3}(?:[.,]\d{3})*(?:,\d+)?|\d+(?:[.,]\d+)?)\s*mil\b", + r"(?:ate|até|de|por|valor|orcamento|orcamento de)\s*r?\$?\s*(\d{4,6}(?:[.,]\d{2})?)\b", + ) + for pattern in patterns: + match = re.search(pattern, normalized) + if not match: + continue + extracted = self._normalize_positive_number(match.group(1) + (" mil" if "mil" in match.group(0) else "")) + if extracted: + return extracted + return None + + def _extract_vehicle_profile_hint_from_message(self, message: str) -> list[str]: + normalized = self._normalize_text(message) + allowed = [] + for marker in ("suv", "sedan", "hatch", "pickup"): + if marker in normalized: + allowed.append(marker) + return allowed + + def _capture_order_search_hints_from_message(self, user_id: int | None, message: str) -> None: + if user_id is None: + return + context = self._get_user_context(user_id) + if not isinstance(context, dict): + return + generic_memory = context.get("generic_memory") + if not isinstance(generic_memory, dict): + generic_memory = {} + context["generic_memory"] = generic_memory + + if "orcamento_max" not in generic_memory: + budget = self._extract_budget_hint_from_message(message) + if budget: + generic_memory["orcamento_max"] = int(round(budget)) + + if "perfil_veiculo" not in generic_memory or not generic_memory.get("perfil_veiculo"): + profile = self._extract_vehicle_profile_hint_from_message(message) + if profile: + generic_memory["perfil_veiculo"] = profile + def _is_valid_cpf(self, cpf: str) -> bool: return is_valid_cpf(cpf) @@ -390,6 +434,7 @@ class OrderFlowMixin: } draft["payload"].update(extracted) + self._capture_order_search_hints_from_message(user_id=user_id, message=message) self._try_prefill_order_cpf_from_memory(user_id=user_id, payload=draft["payload"]) self._try_prefill_order_vehicle_from_context(user_id=user_id, payload=draft["payload"]) diff --git a/tests/test_conversation_adjustments.py b/tests/test_conversation_adjustments.py index 23f1123..a495384 100644 --- a/tests/test_conversation_adjustments.py +++ b/tests/test_conversation_adjustments.py @@ -119,6 +119,9 @@ class OrderFlowHarness(OrderFlowMixin): def _normalize_text(self, text: str) -> str: return self.normalizer.normalize_text(text) + def _normalize_positive_number(self, value): + return self.normalizer.normalize_positive_number(value) + def _http_exception_detail(self, exc) -> str: return str(exc) @@ -358,6 +361,38 @@ class CreateOrderFlowWithVehicleTests(unittest.IsolatedAsyncioTestCase): self.assertIn("Encontrei 2 veiculo(s):", response) self.assertIn("Honda Civic 2021", response) + async def test_order_flow_extracts_budget_from_message_when_llm_does_not_fill_generic_memory(self): + state = FakeState( + contexts={ + 10: { + "generic_memory": {"cpf": "12345678909"}, + "last_stock_results": [], + "selected_vehicle": None, + } + } + ) + registry = FakeRegistry() + flow = OrderFlowHarness(state=state, registry=registry) + + async def fake_hydrate_mock_customer_from_cpf(cpf: str, user_id: int | None = None): + return {"cpf": cpf, "user_id": user_id} + + with patch( + "app.services.flows.order_flow.hydrate_mock_customer_from_cpf", + new=fake_hydrate_mock_customer_from_cpf, + ): + response = await flow._try_collect_and_create_order( + message="Quero comprar um carro de 70 mil, meu CPF e 12345678909", + user_id=10, + extracted_fields={"cpf": "12345678909"}, + intents={}, + turn_decision={"intent": "order_create", "domain": "sales", "action": "collect_order_create"}, + ) + + self.assertEqual(registry.calls[0][0], "consultar_estoque") + self.assertEqual(state.get_user_context(10)["generic_memory"]["orcamento_max"], 70000) + self.assertIn("Encontrei 2 veiculo(s):", response) + async def test_order_flow_lists_stock_from_budget_when_vehicle_is_missing(self): state = FakeState( entries={