🐛 fix(sales): extrair orcamento do pedido sem depender do llm

- 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
main
parent 5f229bd745
commit 0eb56f1f0a

@ -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"])

@ -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={

Loading…
Cancel
Save