From 8a00b6a68e9069f59b409fcefe18dcc142b78aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vitor=20Hugo=20Belorio=20Sim=C3=A3o?= Date: Thu, 12 Mar 2026 18:32:02 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(policy):=20corrigir=20troca?= =?UTF-8?q?=20explicita=20de=20contexto=20com=20fluxo=20aberto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - impede que uma compra explicita durante revisao aberta seja enfileirada cedo demais com prompt pendente do fluxo atual - deixa a mudanca seguir para a confirmacao normal de context switch via pending_switch - adiciona teste para garantir que a policy nao devolve resposta antecipada nem fila automatica nesse cenario --- .../orchestration/conversation_policy.py | 8 ++-- tests/test_turn_decision_contract.py | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app/services/orchestration/conversation_policy.py b/app/services/orchestration/conversation_policy.py index 22d5205..35dc761 100644 --- a/app/services/orchestration/conversation_policy.py +++ b/app/services/orchestration/conversation_policy.py @@ -171,10 +171,10 @@ class ConversationPolicy: and inferred != active_domain and self.has_open_flow(user_id=user_id, domain=active_domain) ): - self.queue_order(user_id=user_id, domain=inferred, order_message=message) - queue_hint = self.render_queue_notice(1) - prompt = self.render_open_flow_prompt(user_id=user_id, domain=active_domain) - return message, None, f"{prompt}\n{queue_hint}" if queue_hint else prompt + # Para uma troca explicita de dominio com fluxo aberto, + # deixa a confirmacao de context switch acontecer no ponto + # normal da policy em vez de enfileirar cedo demais. + return message, None, None return message, None, None if self.has_open_flow(user_id=user_id, domain=active_domain): diff --git a/tests/test_turn_decision_contract.py b/tests/test_turn_decision_contract.py index 0d82ab1..05b606b 100644 --- a/tests/test_turn_decision_contract.py +++ b/tests/test_turn_decision_contract.py @@ -96,6 +96,9 @@ class FakePolicyService: 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"} @@ -1220,6 +1223,40 @@ class TurnDecisionContractTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(response, "Certo, contexto anterior encerrado. Vamos seguir com agendamento de 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": datetime.utcnow() + 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"), []) + if __name__ == "__main__": unittest.main()