diff --git a/app/services/flows/order_flow.py b/app/services/flows/order_flow.py index 1f4d314..323ec3d 100644 --- a/app/services/flows/order_flow.py +++ b/app/services/flows/order_flow.py @@ -53,50 +53,6 @@ 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) @@ -434,7 +390,6 @@ 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/app/services/orchestration/message_planner.py b/app/services/orchestration/message_planner.py index 6dc93f5..53a77dd 100644 --- a/app/services/orchestration/message_planner.py +++ b/app/services/orchestration/message_planner.py @@ -40,6 +40,8 @@ class MessagePlanner: "Regras:\n" "- Se houver mais de um pedido operacional, separe em itens distintos em ordem de aparicao.\n" "- Se nao houver pedido operacional, use domain='general' com a mensagem inteira.\n" + "- Para pedidos de compra com faixa de preco ou orcamento (ex.: '70 mil', 'ate 50 mil', 'R$ 45000'), preencha generic_memory.orcamento_max.\n" + "- Para pedidos com tipo de carro (ex.: suv, sedan, hatch, pickup), preencha generic_memory.perfil_veiculo.\n" "- Mantenha cada message curta e fiel ao texto do usuario.\n\n" f"Contexto: user_id={user_id if user_id is not None else 'anonimo'}\n" f"Mensagem do usuario: {message}" @@ -114,6 +116,10 @@ class MessagePlanner: ' "order_cancel": false\n' " }\n" "}\n\n" + "Regras adicionais:\n" + "- Para pedidos de compra com faixa de preco ou orcamento (ex.: '70 mil', 'ate 50 mil', 'R$ 45000'), preencha generic_memory.orcamento_max.\n" + "- Para pedidos com tipo de carro (ex.: suv, sedan, hatch, pickup), preencha generic_memory.perfil_veiculo.\n" + "- Nao deixe generic_memory.orcamento_max vazio quando a mensagem expressar claramente o teto de compra.\n\n" f"Contexto: {user_context}\n" f"Mensagem do usuario: {message}" ) @@ -149,6 +155,8 @@ class MessagePlanner: "- 'intent' deve refletir a intencao principal do turno.\n" "- 'action' deve ser uma das acoes do contrato.\n" "- 'entities' deve manter as secoes generic_memory, review_fields, review_management_fields, order_fields e cancel_order_fields.\n" + "- Em pedidos de compra com faixa de preco ou orcamento (ex.: '70 mil', 'ate 50 mil', 'R$ 45000'), preencha entities.generic_memory.orcamento_max.\n" + "- Em pedidos com tipo de carro (ex.: suv, sedan, hatch, pickup), preencha entities.generic_memory.perfil_veiculo.\n" "- Se faltar dado para continuar um fluxo, use action='ask_missing_fields' e preencha 'missing_fields' e 'response_to_user'.\n" "- Se o usuario estiver escolhendo entre pedidos enfileirados (ex.: '1', '2', 'o segundo'), preencha 'selection_index' com base zero.\n" "- Se for necessaria uma tool de orquestracao, use action compativel e preencha 'tool_name' e 'tool_arguments' quando apropriado.\n" diff --git a/app/services/orchestration/orquestrador_service.py b/app/services/orchestration/orquestrador_service.py index 4fa1be4..1490040 100644 --- a/app/services/orchestration/orquestrador_service.py +++ b/app/services/orchestration/orquestrador_service.py @@ -136,20 +136,19 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin): message=routing_message, user_id=user_id, ) + llm_extracted_entities = await self._extract_entities_with_llm( + message=routing_message, + user_id=user_id, + ) + extracted_entities = self._merge_extracted_entities( + extracted_entities, + llm_extracted_entities, + ) if self._has_useful_turn_decision(turn_decision): extracted_entities = self._merge_extracted_entities( extracted_entities, self._extracted_entities_from_turn_decision(turn_decision), ) - else: - llm_extracted_entities = await self._extract_entities_with_llm( - message=routing_message, - user_id=user_id, - ) - extracted_entities = self._merge_extracted_entities( - extracted_entities, - llm_extracted_entities, - ) self._capture_generic_memory( user_id=user_id, llm_generic_fields=extracted_entities.get("generic_memory", {}), diff --git a/tests/test_conversation_adjustments.py b/tests/test_conversation_adjustments.py index a495384..b78dd59 100644 --- a/tests/test_conversation_adjustments.py +++ b/tests/test_conversation_adjustments.py @@ -361,38 +361,6 @@ 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={