🐛 fix(orchestration): normalizar aliases do contrato de decisao do modelo

- aceitar aliases de intent retornados pelo Vertex como place_order e create_order
- converter aliases de campos faltantes como modelo_carro para vehicle_id no fluxo de compra
- redirecionar decisoes de compra quase-validas para collect_order_create em vez de cair no fallback
- cobrir com testes os formatos divergentes retornados localmente e no servidor
main
parent e274dc9017
commit 6fe92a0ae1

@ -14,6 +14,31 @@ logger = logging.getLogger(__name__)
# Essa classe concentra normalizacao tecnica e coercoes estruturadas.
# A semantica conversacional idealmente vem do modelo, nao daqui.
class EntityNormalizer:
_TURN_INTENT_ALIASES = {
"create_order": "order_create",
"place_order": "order_create",
"new_order": "order_create",
"buy_car": "order_create",
"purchase_car": "order_create",
"cancel_order": "order_cancel",
"list_inventory": "inventory_search",
"search_inventory": "inventory_search",
"clear_conversation": "conversation_reset",
}
_TURN_ACTION_ALIASES = {
"collect_order": "collect_order_create",
"collect_review": "collect_review_schedule",
"cancel_flow": "cancel_active_flow",
"reset_context": "clear_context",
}
_ORDER_MISSING_FIELD_ALIASES = {
"modelo_carro": "vehicle_id",
"modelo_do_carro": "vehicle_id",
"modelo_veiculo": "vehicle_id",
"veiculo": "vehicle_id",
"carro": "vehicle_id",
}
def empty_turn_decision(self) -> dict:
return TurnDecision().model_dump()
@ -103,6 +128,7 @@ class EntityNormalizer:
def coerce_turn_decision(self, payload) -> dict:
if not isinstance(payload, dict):
return self.empty_turn_decision()
payload = self._normalize_turn_decision_payload(payload)
try:
model = TurnDecision.model_validate(payload)
except ValidationError:
@ -122,6 +148,61 @@ class EntityNormalizer:
dumped["missing_fields"] = [str(field) for field in dumped.get("missing_fields") or [] if str(field).strip()]
return dumped
def _normalize_turn_decision_payload(self, payload: dict) -> dict:
normalized = dict(payload)
raw_intent = self.normalize_text(str(normalized.get("intent") or "")).replace("-", "_").replace(" ", "_")
if raw_intent in self._TURN_INTENT_ALIASES:
normalized["intent"] = self._TURN_INTENT_ALIASES[raw_intent]
raw_action = self.normalize_text(str(normalized.get("action") or "")).replace("-", "_").replace(" ", "_")
if raw_action in self._TURN_ACTION_ALIASES:
normalized["action"] = self._TURN_ACTION_ALIASES[raw_action]
missing_fields = normalized.get("missing_fields")
if isinstance(missing_fields, list):
normalized["missing_fields"] = self._normalize_turn_missing_fields(missing_fields)
entities = normalized.get("entities")
if isinstance(entities, dict):
normalized["entities"] = dict(entities)
if self._should_route_order_alias_to_collection(normalized):
normalized["action"] = "collect_order_create"
normalized["missing_fields"] = []
normalized["response_to_user"] = None
return normalized
def _normalize_turn_missing_fields(self, missing_fields: list) -> list[str]:
normalized_fields: list[str] = []
for field in missing_fields:
candidate = self.normalize_text(str(field or "")).replace("-", "_").replace(" ", "_")
canonical = self._ORDER_MISSING_FIELD_ALIASES.get(candidate, candidate)
if canonical and canonical not in normalized_fields:
normalized_fields.append(canonical)
return normalized_fields
def _should_route_order_alias_to_collection(self, payload: dict) -> bool:
if payload.get("intent") != "order_create":
return False
if payload.get("action") != "ask_missing_fields":
return False
missing_fields = payload.get("missing_fields") or []
if not missing_fields or any(field != "vehicle_id" for field in missing_fields):
return False
entities = payload.get("entities")
if not isinstance(entities, dict):
return False
order_fields = entities.get("order_fields")
if not isinstance(order_fields, dict):
return False
if order_fields.get("vehicle_id"):
return False
return True
def normalize_text(self, text: str) -> str:
return technical_normalizer.normalize_text(text)

@ -173,6 +173,63 @@ class TurnDecisionContractTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(decision["action"], "answer_user")
self.assertEqual(decision["entities"]["order_fields"], {})
def test_coerce_turn_decision_maps_order_aliases_from_model(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "place_order",
"domain": "sales",
"action": "answer_user",
"entities": {
"generic_memory": {"orcamento_max": "70000", "cpf": "12345678909"},
"review_fields": {},
"review_management_fields": {},
"order_fields": {},
"cancel_order_fields": {},
},
"missing_fields": [],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": None,
}
)
self.assertEqual(decision["intent"], "order_create")
self.assertEqual(decision["domain"], "sales")
self.assertEqual(decision["action"], "answer_user")
self.assertEqual(decision["entities"]["generic_memory"]["orcamento_max"], 70000)
self.assertEqual(decision["entities"]["generic_memory"]["cpf"], "12345678909")
def test_coerce_turn_decision_converts_vehicle_alias_missing_field_into_order_collection(self):
normalizer = EntityNormalizer()
decision = normalizer.coerce_turn_decision(
{
"intent": "create_order",
"domain": "sales",
"action": "ask_missing_fields",
"entities": {
"generic_memory": {"orcamento_max": 70000},
"review_fields": {},
"review_management_fields": {},
"order_fields": {"cpf": "12345678909"},
"cancel_order_fields": {},
},
"missing_fields": ["modelo_carro"],
"selection_index": None,
"tool_name": None,
"tool_arguments": {},
"response_to_user": "Certo! Para qual modelo de carro voce gostaria de um orcamento de 70 mil?",
}
)
self.assertEqual(decision["intent"], "order_create")
self.assertEqual(decision["action"], "collect_order_create")
self.assertEqual(decision["missing_fields"], [])
self.assertIsNone(decision["response_to_user"])
def test_coerce_turn_decision_rejects_missing_fields_without_response_payload(self):
normalizer = EntityNormalizer()

Loading…
Cancel
Save