@ -1,3 +1,4 @@
import ast
import json
import json
import logging
import logging
import re
import re
@ -24,6 +25,7 @@ class EntityNormalizer:
" marcar_revisao " : " agendar_revisao " ,
" marcar_revisao " : " agendar_revisao " ,
" agendar revisao " : " agendar_revisao " ,
" agendar revisao " : " agendar_revisao " ,
" schedule_review " : " agendar_revisao " ,
" schedule_review " : " agendar_revisao " ,
" agendar_revisao_veiculo " : " agendar_revisao " ,
" list_reviews " : " listar_agendamentos_revisao " ,
" list_reviews " : " listar_agendamentos_revisao " ,
" listar_revisoes " : " listar_agendamentos_revisao " ,
" listar_revisoes " : " listar_agendamentos_revisao " ,
" listar_agendamentos " : " listar_agendamentos_revisao " ,
" listar_agendamentos " : " listar_agendamentos_revisao " ,
@ -57,6 +59,66 @@ class EntityNormalizer:
" cancel_flow " : " cancel_active_flow " ,
" cancel_flow " : " cancel_active_flow " ,
" reset_context " : " clear_context " ,
" reset_context " : " clear_context " ,
}
}
_TURN_DOMAIN_ALIASES = {
" service " : " review " ,
" services " : " review " ,
" post_sales " : " review " ,
" after_sales " : " review " ,
" purchase " : " sales " ,
" buy " : " sales " ,
" order " : " sales " ,
" orders " : " sales " ,
" conversation " : " general " ,
" chat " : " general " ,
}
_TURN_VALID_DOMAINS = { " review " , " sales " , " general " }
_TURN_VALID_INTENTS = {
" review_schedule " ,
" review_list " ,
" review_cancel " ,
" review_reschedule " ,
" order_create " ,
" order_list " ,
" order_cancel " ,
" inventory_search " ,
" conversation_reset " ,
" queue_continue " ,
" discard_queue " ,
" cancel_active_flow " ,
" general " ,
}
_TURN_VALID_ACTIONS = {
" collect_review_schedule " ,
" collect_review_management " ,
" collect_order_create " ,
" collect_order_cancel " ,
" ask_missing_fields " ,
" answer_user " ,
" call_tool " ,
" clear_context " ,
" continue_queue " ,
" discard_queue " ,
" cancel_active_flow " ,
}
_TURN_TOP_LEVEL_FIELD_ALIASES = {
" response " : " response_to_user " ,
" answer " : " response_to_user " ,
" reply " : " response_to_user " ,
" message " : " response_to_user " ,
" tool " : " tool_name " ,
" function_name " : " tool_name " ,
" function " : " tool_name " ,
" arguments " : " tool_arguments " ,
" args " : " tool_arguments " ,
" tool_args " : " tool_arguments " ,
" tool_parameters " : " tool_arguments " ,
" missing " : " missing_fields " ,
" required_fields " : " missing_fields " ,
" missing_data " : " missing_fields " ,
" selected_index " : " selection_index " ,
" choice_index " : " selection_index " ,
" selected_option_index " : " selection_index " ,
}
_ORDER_MISSING_FIELD_ALIASES = {
_ORDER_MISSING_FIELD_ALIASES = {
" modelo_carro " : " vehicle_id " ,
" modelo_carro " : " vehicle_id " ,
" modelo_do_carro " : " vehicle_id " ,
" modelo_do_carro " : " vehicle_id " ,
@ -64,6 +126,35 @@ class EntityNormalizer:
" veiculo " : " vehicle_id " ,
" veiculo " : " vehicle_id " ,
" carro " : " vehicle_id " ,
" carro " : " vehicle_id " ,
}
}
_TURN_MISSING_FIELD_ALIASES = {
* * _ORDER_MISSING_FIELD_ALIASES ,
" date " : " data_hora " ,
" date_time " : " data_hora " ,
" datetime " : " data_hora " ,
" data " : " data_hora " ,
" data_e_hora " : " data_hora " ,
" data_hora " : " data_hora " ,
" data_agendamento " : " data_hora " ,
" horario " : " data_hora " ,
" hora " : " data_hora " ,
" time " : " data_hora " ,
" modelo " : " modelo " ,
" modelo_veiculo " : " modelo " ,
" vehicle_model " : " modelo " ,
" ano_veiculo " : " ano " ,
" vehicle_year " : " ano " ,
" quilometragem " : " km " ,
" quilometragem_atual " : " km " ,
" vehicle_km " : " km " ,
" revisao_previa " : " revisao_previa_concessionaria " ,
" reviewed_before " : " revisao_previa_concessionaria " ,
" numero " : " numero_pedido " ,
" order_number " : " numero_pedido " ,
" order_id " : " numero_pedido " ,
" review_id " : " protocolo " ,
" schedule_id " : " protocolo " ,
" new_datetime " : " nova_data_hora " ,
}
_TOOL_ARGUMENT_ALIASES = {
_TOOL_ARGUMENT_ALIASES = {
" cancelar_pedido " : {
" cancelar_pedido " : {
" order_id " : " numero_pedido " ,
" order_id " : " numero_pedido " ,
@ -115,6 +206,10 @@ class EntityNormalizer:
" vehicle_km " : " km " ,
" vehicle_km " : " km " ,
" data " : " data_hora " ,
" data " : " data_hora " ,
" datetime " : " data_hora " ,
" datetime " : " data_hora " ,
" data_agendamento " : " data_agendamento " ,
" appointment_date " : " data_agendamento " ,
" horario_agendamento " : " horario_agendamento " ,
" appointment_time " : " horario_agendamento " ,
" reviewed_before " : " revisao_previa_concessionaria " ,
" reviewed_before " : " revisao_previa_concessionaria " ,
" revisao_previa " : " revisao_previa_concessionaria " ,
" revisao_previa " : " revisao_previa_concessionaria " ,
} ,
} ,
@ -197,21 +292,66 @@ class EntityNormalizer:
candidate = ( text or " " ) . strip ( )
candidate = ( text or " " ) . strip ( )
if not candidate :
if not candidate :
return None
return None
if candidate . startswith ( " ``` " ) :
candidate = re . sub ( r " ^```(?:json)? \ s* " , " " , candidate , flags = re . IGNORECASE )
candidates : list [ str ] = [ ]
candidate = re . sub ( r " \ s*```$ " , " " , candidate )
stripped_candidate = self . _strip_json_fence ( candidate )
try :
for option in ( stripped_candidate , candidate ) :
return json . loads ( candidate )
option = str ( option or " " ) . strip ( )
except json . JSONDecodeError :
if option and option not in candidates :
match = re . search ( r " \ { .* \ } " , candidate , flags = re . DOTALL )
candidates . append ( option )
if not match :
logger . warning ( " Extracao sem JSON valido no texto retornado. " )
match = re . search ( r " \ { .* \ } " , stripped_candidate or candidate , flags = re . DOTALL )
return None
if match :
clipped = match . group ( 0 ) . strip ( )
if clipped and clipped not in candidates :
candidates . append ( clipped )
for option in candidates :
parsed = self . _try_parse_json_candidate ( option )
if isinstance ( parsed , dict ) :
return parsed
logger . warning ( " Extracao sem JSON valido no texto retornado. " )
return None
def _strip_json_fence ( self , candidate : str ) - > str :
stripped = str ( candidate or " " ) . strip ( )
if stripped . startswith ( " ``` " ) :
stripped = re . sub ( r " ^```(?:json)? \ s* " , " " , stripped , flags = re . IGNORECASE )
stripped = re . sub ( r " \ s*```$ " , " " , stripped )
return stripped . strip ( )
def _try_parse_json_candidate ( self , candidate : str ) :
normalized = str ( candidate or " " ) . strip ( )
if not normalized :
return None
normalized = (
normalized . replace ( " \u201c " , ' " ' )
. replace ( " \u201d " , ' " ' )
. replace ( " \u2018 " , " ' " )
. replace ( " \u2019 " , " ' " )
. replace ( chr ( 0x201C ) , ' " ' )
. replace ( chr ( 0x201D ) , ' " ' )
. replace ( chr ( 0x2018 ) , " ' " )
. replace ( chr ( 0x2019 ) , " ' " )
)
variants = [ normalized ]
without_trailing_commas = re . sub ( r " ,( \ s*[} \ ]]) " , r " \ 1 " , normalized )
if without_trailing_commas != normalized :
variants . append ( without_trailing_commas )
for variant in variants :
try :
try :
return json . loads ( match . group ( 0 ) )
return json . loads ( variant )
except json . JSONDecodeError :
except json . JSONDecodeError :
logger . warning ( " Extracao com JSON invalido apos recorte. " )
pass
return None
try :
parsed = ast . literal_eval ( variant )
except ( ValueError , SyntaxError ) :
continue
if isinstance ( parsed , dict ) :
return parsed
return None
def coerce_turn_decision ( self , payload ) - > dict :
def coerce_turn_decision ( self , payload ) - > dict :
if not isinstance ( payload , dict ) :
if not isinstance ( payload , dict ) :
@ -219,8 +359,12 @@ class EntityNormalizer:
payload = self . _normalize_turn_decision_payload ( payload )
payload = self . _normalize_turn_decision_payload ( payload )
try :
try :
model = TurnDecision . model_validate ( payload )
model = TurnDecision . model_validate ( payload )
except ValidationError :
except ValidationError as exc :
logger . warning ( " Decisao de turno invalida; usando fallback estruturado. " )
details = " ; " . join (
f " { ' . ' . join ( str ( part ) for part in error . get ( ' loc ' , [ ] ) ) } : { error . get ( ' msg ' , ' erro ' ) } "
for error in exc . errors ( ) [ : 3 ]
)
logger . warning ( " Decisao de turno invalida; usando fallback estruturado. detalhes= %s " , details or " n/a " )
return self . empty_turn_decision ( )
return self . empty_turn_decision ( )
normalized_entities = {
normalized_entities = {
@ -237,30 +381,44 @@ class EntityNormalizer:
return dumped
return dumped
def _normalize_turn_decision_payload ( self , payload : dict ) - > dict :
def _normalize_turn_decision_payload ( self , payload : dict ) - > dict :
normalized = dict ( payload )
normalized = self . _unwrap_turn_decision_payload ( payload )
raw_intent = self . normalize_text ( str ( normalized . get ( " intent " ) or " " ) ) . replace ( " - " , " _ " ) . replace ( " " , " _ " )
tool_call = normalized . get ( " tool_call " )
if raw_intent in self . _TURN_INTENT_ALIASES :
if isinstance ( tool_call , dict ) :
normalized [ " intent " ] = self . _TURN_INTENT_ALIASES [ raw_intent ]
if " tool_name " not in normalized :
normalized [ " tool_name " ] = tool_call . get ( " tool_name " ) or tool_call . get ( " name " )
raw_action = self . normalize_text ( str ( normalized . get ( " action " ) or " " ) ) . replace ( " - " , " _ " ) . replace ( " " , " _ " )
if " tool_arguments " not in normalized :
if raw_action in self . _TURN_ACTION_ALIASES :
normalized [ " tool_arguments " ] = tool_call . get ( " tool_arguments " ) or tool_call . get ( " arguments " ) or { }
normalized [ " action " ] = self . _TURN_ACTION_ALIASES [ raw_action ]
for alias , canonical in self . _TURN_TOP_LEVEL_FIELD_ALIASES . items ( ) :
missing_fields = normalized . get ( " missing_fields " )
if canonical not in normalized and alias in normalized :
if isinstance ( missing_fields , list ) :
normalized [ canonical ] = normalized . get ( alias )
normalized [ " missing_fields " ] = self . _normalize_turn_missing_fields ( missing_fields )
normalized [ " domain " ] = self . _normalize_turn_domain ( normalized . get ( " domain " ) )
entities = normalized . get ( " entities " )
normalized [ " intent " ] = self . _normalize_turn_intent ( normalized . get ( " intent " ) )
if isinstance ( entities , dict ) :
normalized [ " action " ] = self . _normalize_turn_action ( normalized . get ( " action " ) )
normalized [ " entities " ] = dict ( entities )
normalized [ " response_to_user " ] = self . _normalize_turn_response ( normalized . get ( " response_to_user " ) )
normalized [ " selection_index " ] = self . _normalize_turn_selection_index ( normalized . get ( " selection_index " ) )
normalized [ " missing_fields " ] = self . _normalize_turn_missing_fields ( normalized . get ( " missing_fields " ) )
embedded_intents = self . _extract_turn_intents ( normalized )
normalized [ " entities " ] = self . _normalize_turn_entities ( normalized )
if normalized [ " intent " ] == " general " :
inferred_intent = self . _infer_primary_turn_intent ( embedded_intents )
if inferred_intent :
normalized [ " intent " ] = inferred_intent
if normalized [ " domain " ] == " general " :
normalized [ " domain " ] = self . _domain_from_turn_intent ( normalized . get ( " intent " ) )
tool_name = self . normalize_tool_name ( normalized . get ( " tool_name " ) )
tool_name = self . normalize_tool_name ( normalized . get ( " tool_name " ) )
if tool_name :
normalized [ " tool_name " ] = tool_name or None
normalized [ " tool_name " ] = tool_name
tool_arguments = normalized . get ( " tool_arguments " )
tool_arguments = normalized . get ( " tool_arguments " )
if tool_name and isinstance ( tool_arguments , dict ) :
if tool_name and isinstance ( tool_arguments , dict ) :
normalized [ " tool_arguments " ] = self . normalize_tool_arguments ( tool_name , tool_arguments )
normalized [ " tool_arguments " ] = self . normalize_tool_arguments ( tool_name , tool_arguments )
else :
normalized [ " tool_arguments " ] = tool_arguments if isinstance ( tool_arguments , dict ) else { }
normalized = self . _coerce_incomplete_action_to_collection ( normalized )
if self . _should_route_order_alias_to_collection ( normalized ) :
if self . _should_route_order_alias_to_collection ( normalized ) :
normalized [ " action " ] = " collect_order_create "
normalized [ " action " ] = " collect_order_create "
@ -269,15 +427,296 @@ class EntityNormalizer:
normalized = self . _coerce_incomplete_tool_call_to_collection ( normalized )
normalized = self . _coerce_incomplete_tool_call_to_collection ( normalized )
if (
normalized . get ( " intent " ) == " general "
and not normalized . get ( " tool_name " )
and not any ( ( normalized . get ( " entities " ) or { } ) . values ( ) )
) :
normalized [ " domain " ] = " general "
return {
" intent " : normalized . get ( " intent " ) or " general " ,
" domain " : normalized . get ( " domain " ) or " general " ,
" action " : normalized . get ( " action " ) or " answer_user " ,
" entities " : normalized . get ( " entities " ) if isinstance ( normalized . get ( " entities " ) , dict ) else self . empty_turn_decision ( ) [ " entities " ] ,
" missing_fields " : normalized . get ( " missing_fields " ) if isinstance ( normalized . get ( " missing_fields " ) , list ) else [ ] ,
" selection_index " : normalized . get ( " selection_index " ) ,
" tool_name " : normalized . get ( " tool_name " ) ,
" tool_arguments " : normalized . get ( " tool_arguments " ) if isinstance ( normalized . get ( " tool_arguments " ) , dict ) else { } ,
" response_to_user " : normalized . get ( " response_to_user " ) ,
}
def _unwrap_turn_decision_payload ( self , payload : dict ) - > dict :
normalized = dict ( payload )
if any ( key in normalized for key in ( " intent " , " domain " , " action " , " entities " , " tool_name " , " tool_arguments " ) ) :
return normalized
for key in ( " turn_decision " , " decision " , " payload " , " data " ) :
nested = normalized . get ( key )
if isinstance ( nested , dict ) :
return dict ( nested )
return normalized
return normalized
def _normalize_turn_missing_fields ( self , missing_fields : list ) - > list [ str ] :
def _normalize_turn_domain ( self , value ) - > str :
candidate = self . normalize_text ( str ( value or " " ) ) . replace ( " - " , " _ " ) . replace ( " " , " _ " )
candidate = self . _TURN_DOMAIN_ALIASES . get ( candidate , candidate )
return candidate if candidate in self . _TURN_VALID_DOMAINS else " general "
def _normalize_turn_intent ( self , value ) - > str :
candidate = self . normalize_text ( str ( value or " " ) ) . replace ( " - " , " _ " ) . replace ( " " , " _ " )
candidate = self . _TURN_INTENT_ALIASES . get ( candidate , candidate )
extra_aliases = {
" schedule_review " : " review_schedule " ,
" review_schedule_follow_up " : " review_schedule " ,
" review_schedule_followup " : " review_schedule " ,
" review_management " : " review_reschedule " ,
" reset " : " conversation_reset " ,
" restart_conversation " : " conversation_reset " ,
" continue_queue " : " queue_continue " ,
" next_order " : " queue_continue " ,
" cancel_current_flow " : " cancel_active_flow " ,
}
candidate = extra_aliases . get ( candidate , candidate )
return candidate if candidate in self . _TURN_VALID_INTENTS else " general "
def _normalize_turn_action ( self , value ) - > str :
candidate = self . normalize_text ( str ( value or " " ) ) . replace ( " - " , " _ " ) . replace ( " " , " _ " )
candidate = self . _TURN_ACTION_ALIASES . get ( candidate , candidate )
extra_aliases = {
" answer " : " answer_user " ,
" reply " : " answer_user " ,
" respond " : " answer_user " ,
" tool_call " : " call_tool " ,
" execute_tool " : " call_tool " ,
" use_tool " : " call_tool " ,
" ask_for_missing_fields " : " ask_missing_fields " ,
" request_missing_fields " : " ask_missing_fields " ,
" collect_missing_fields " : " ask_missing_fields " ,
" continue " : " continue_queue " ,
" next_order " : " continue_queue " ,
" discard " : " discard_queue " ,
" reset " : " clear_context " ,
" clear_conversation " : " clear_context " ,
}
candidate = extra_aliases . get ( candidate , candidate )
return candidate if candidate in self . _TURN_VALID_ACTIONS else " answer_user "
def _normalize_turn_response ( self , value ) - > str | None :
if value is None :
return None
if isinstance ( value , str ) :
stripped = value . strip ( )
return stripped or None
if isinstance ( value , ( int , float ) ) and not isinstance ( value , bool ) :
return str ( value )
return None
def _normalize_turn_selection_index ( self , value ) - > int | None :
if value in ( None , " " ) or isinstance ( value , bool ) :
return None
if isinstance ( value , ( int , float ) ) :
candidate = int ( value )
return candidate if candidate > = 0 else None
text = self . normalize_text ( str ( value or " " ) ) . strip ( )
ordinal_aliases = {
" primeiro " : 0 ,
" primeira " : 0 ,
" segundo " : 1 ,
" segunda " : 1 ,
" terceiro " : 2 ,
" terceira " : 2 ,
}
if text in ordinal_aliases :
return ordinal_aliases [ text ]
match = re . search ( r " \ d+ " , text )
if not match :
return None
candidate = int ( match . group ( 0 ) )
return candidate if candidate > = 0 else None
def _normalize_turn_entities ( self , payload : dict ) - > dict :
container = payload . get ( " entities " ) if isinstance ( payload . get ( " entities " ) , dict ) else { }
normalized_entities : dict [ str , dict ] = { }
for key in (
" generic_memory " ,
" review_fields " ,
" review_management_fields " ,
" order_fields " ,
" cancel_order_fields " ,
) :
merged : dict = { }
top_level_value = payload . get ( key )
if isinstance ( top_level_value , dict ) :
merged . update ( top_level_value )
nested_value = container . get ( key )
if isinstance ( nested_value , dict ) :
merged . update ( nested_value )
normalized_entities [ key ] = merged
return normalized_entities
def _extract_turn_intents ( self , payload : dict ) - > dict :
candidates : list = [ ]
if isinstance ( payload . get ( " intents " ) , dict ) :
candidates . append ( payload . get ( " intents " ) )
entities = payload . get ( " entities " ) if isinstance ( payload . get ( " entities " ) , dict ) else { }
if isinstance ( entities . get ( " intents " ) , dict ) :
candidates . append ( entities . get ( " intents " ) )
for candidate in candidates :
normalized = self . normalize_intents ( candidate )
if any ( normalized . values ( ) ) :
return normalized
return { }
def _infer_primary_turn_intent ( self , intents : dict ) - > str | None :
if not isinstance ( intents , dict ) :
return None
priority = (
" review_schedule " ,
" review_reschedule " ,
" review_cancel " ,
" review_list " ,
" order_create " ,
" order_cancel " ,
" order_list " ,
)
for key in priority :
if intents . get ( key ) :
return key
return None
def _domain_from_turn_intent ( self , intent : str | None ) - > str :
if intent in { " review_schedule " , " review_list " , " review_cancel " , " review_reschedule " } :
return " review "
if intent in { " order_create " , " order_list " , " order_cancel " , " inventory_search " , " queue_continue " , " discard_queue " , " cancel_active_flow " } :
return " sales "
return " general "
def _coerce_incomplete_action_to_collection ( self , payload : dict ) - > dict :
action = payload . get ( " action " )
collection_action = self . _infer_collection_action ( payload )
if action == " ask_missing_fields " and not payload . get ( " response_to_user " ) :
if collection_action :
payload [ " action " ] = collection_action
payload [ " response_to_user " ] = None
else :
payload [ " action " ] = " answer_user "
payload [ " missing_fields " ] = [ ]
return payload
if action == " call_tool " and not str ( payload . get ( " tool_name " ) or " " ) . strip ( ) :
if collection_action :
payload [ " action " ] = collection_action
payload = self . _merge_tool_arguments_into_collection_entities ( payload , collection_action )
else :
payload [ " action " ] = " answer_user "
payload [ " tool_name " ] = None
payload [ " tool_arguments " ] = { }
payload [ " response_to_user " ] = payload . get ( " response_to_user " )
return payload
return payload
def _infer_collection_action ( self , payload : dict ) - > str | None :
intent = str ( payload . get ( " intent " ) or " " ) . strip ( )
if intent == " review_schedule " :
return " collect_review_schedule "
if intent in { " review_cancel " , " review_reschedule " } :
return " collect_review_management "
if intent == " order_create " :
return " collect_order_create "
if intent == " order_cancel " :
return " collect_order_cancel "
entities = payload . get ( " entities " ) if isinstance ( payload . get ( " entities " ) , dict ) else { }
domain = str ( payload . get ( " domain " ) or " " ) . strip ( )
if domain == " review " :
if entities . get ( " review_management_fields " ) :
return " collect_review_management "
if entities . get ( " review_fields " ) :
return " collect_review_schedule "
if domain == " sales " :
if entities . get ( " cancel_order_fields " ) :
return " collect_order_cancel "
if entities . get ( " order_fields " ) or entities . get ( " generic_memory " ) :
return " collect_order_create "
return None
def _merge_tool_arguments_into_collection_entities ( self , payload : dict , collection_action : str ) - > dict :
entities = payload . get ( " entities " ) if isinstance ( payload . get ( " entities " ) , dict ) else { }
payload [ " entities " ] = entities
raw_arguments = payload . get ( " tool_arguments " ) if isinstance ( payload . get ( " tool_arguments " ) , dict ) else { }
intent = str ( payload . get ( " intent " ) or " " ) . strip ( )
if collection_action == " collect_order_cancel " :
normalized_arguments = self . normalize_tool_arguments ( " cancelar_pedido " , raw_arguments )
entities [ " cancel_order_fields " ] = self . normalize_cancel_order_fields (
{
* * ( entities . get ( " cancel_order_fields " ) or { } ) ,
* * normalized_arguments ,
}
)
return payload
if collection_action == " collect_order_create " :
normalized_arguments = self . normalize_tool_arguments ( " realizar_pedido " , raw_arguments )
entities [ " order_fields " ] = self . normalize_order_fields (
{
* * ( entities . get ( " order_fields " ) or { } ) ,
* * normalized_arguments ,
}
)
return payload
if collection_action == " collect_review_schedule " :
normalized_arguments = self . normalize_tool_arguments ( " agendar_revisao " , raw_arguments )
entities [ " review_fields " ] = self . normalize_review_fields (
{
* * ( entities . get ( " review_fields " ) or { } ) ,
* * normalized_arguments ,
}
)
return payload
if collection_action == " collect_review_management " :
tool_name = " editar_data_revisao " if intent == " review_reschedule " else " cancelar_agendamento_revisao "
normalized_arguments = self . normalize_tool_arguments ( tool_name , raw_arguments )
entities [ " review_management_fields " ] = self . normalize_review_management_fields (
{
* * ( entities . get ( " review_management_fields " ) or { } ) ,
* * normalized_arguments ,
}
)
return payload
return payload
def _normalize_turn_missing_fields ( self , missing_fields ) - > list [ str ] :
if missing_fields is None :
return [ ]
raw_fields = missing_fields if isinstance ( missing_fields , list ) else [ missing_fields ]
normalized_fields : list [ str ] = [ ]
normalized_fields : list [ str ] = [ ]
for field in missing_fields :
for field in raw_fields :
candidate = self . normalize_text ( str ( field or " " ) ) . replace ( " - " , " _ " ) . replace ( " " , " _ " )
if field in ( None , " " ) :
canonical = self . _ORDER_MISSING_FIELD_ALIASES . get ( candidate , candidate )
continue
if canonical and canonical not in normalized_fields :
text_value = str ( field or " " ) . strip ( )
normalized_fields . append ( canonical )
if not text_value :
continue
normalized_value = self . normalize_text ( text_value ) . replace ( " - " , " _ " ) . replace ( " " , " _ " )
segments = [ normalized_value ]
if normalized_value not in self . _TURN_MISSING_FIELD_ALIASES :
split_segments = [ segment for segment in re . split ( r " [,;/] " , normalized_value ) if segment ]
if len ( split_segments ) > 1 :
segments = split_segments
for segment in segments :
if segment not in self . _TURN_MISSING_FIELD_ALIASES and " _e_ " in segment :
for part in ( item for item in segment . split ( " _e_ " ) if item ) :
canonical = self . _TURN_MISSING_FIELD_ALIASES . get ( part , part )
if canonical and canonical not in normalized_fields :
normalized_fields . append ( canonical )
continue
canonical = self . _TURN_MISSING_FIELD_ALIASES . get ( segment , segment )
if canonical and canonical not in normalized_fields :
normalized_fields . append ( canonical )
return normalized_fields
return normalized_fields
def _should_route_order_alias_to_collection ( self , payload : dict ) - > bool :
def _should_route_order_alias_to_collection ( self , payload : dict ) - > bool :
@ -454,6 +893,14 @@ class EntityNormalizer:
return self . normalize_review_management_fields ( normalized_arguments )
return self . normalize_review_management_fields ( normalized_arguments )
if normalized_tool_name == " agendar_revisao " :
if normalized_tool_name == " agendar_revisao " :
schedule_date = str ( normalized_arguments . pop ( " data_agendamento " , " " ) or " " ) . strip ( )
schedule_time = str ( normalized_arguments . pop ( " horario_agendamento " , " " ) or " " ) . strip ( )
if " data_hora " not in normalized_arguments :
combined_datetime = self . normalize_review_datetime_text (
" " . join ( part for part in ( schedule_date , schedule_time ) if part )
)
if combined_datetime :
normalized_arguments [ " data_hora " ] = combined_datetime
return self . normalize_review_fields ( normalized_arguments )
return self . normalize_review_fields ( normalized_arguments )
return normalized_arguments
return normalized_arguments