@ -1,4 +1,4 @@
import json
import json
import logging
import re
from datetime import datetime , timedelta
@ -47,6 +47,7 @@ from app.services.flows.review_flow import ReviewFlowMixin
from app . services . orchestration . tool_executor import ToolExecutor
from app . services . tools . tool_registry import ToolRegistry
from app . services . orchestration . response_formatter import format_currency_br , format_datetime_for_chat
from app . services . orchestration . technical_normalizer import extract_budget_from_text , normalize_vehicle_profile
logger = logging . getLogger ( __name__ )
@ -220,12 +221,36 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
)
if current_rental_info :
return current_rental_info
deterministic_rental_bootstrap = await self . _try_handle_deterministic_rental_bootstrap (
message = message ,
user_id = user_id ,
finish = finish ,
)
if deterministic_rental_bootstrap :
return deterministic_rental_bootstrap
# Faz uma leitura inicial do turno para ajudar a policy
# com fila, troca de contexto e comandos globais.
early_turn_decision = await self . _extract_turn_decision_with_llm (
message = message ,
user_id = user_id ,
)
use_turn_bundle = self . _should_attempt_turn_bundle (
message = message ,
early_turn_decision = early_turn_decision ,
)
turn_bundle = (
await self . _extract_turn_bundle_with_llm (
message = message ,
user_id = user_id ,
)
if use_turn_bundle
else None
)
bundle_has_message_plan = (
isinstance ( turn_bundle , dict )
and bool ( turn_bundle . get ( " has_message_plan " ) )
and isinstance ( turn_bundle . get ( " message_plan " ) , dict )
)
reset_override = await self . _try_handle_immediate_context_reset (
message = message ,
user_id = user_id ,
@ -259,10 +284,28 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
return deterministic_rental_management
message_plan = await self . _extract_message_plan_with_llm (
synthesized_message_plan = (
self . _synthesize_message_plan_from_turn_decision (
message = message ,
turn_decision = early_turn_decision ,
)
if not bundle_has_message_plan
and self . _can_synthesize_message_plan_from_turn_decision (
message = message ,
turn_decision = early_turn_decision ,
)
else None
)
message_plan = (
turn_bundle . get ( " message_plan " )
if bundle_has_message_plan
else synthesized_message_plan
if isinstance ( synthesized_message_plan , dict )
else await self . _extract_message_plan_with_llm (
message = message ,
user_id = user_id ,
)
)
routing_plan = {
" orders " : [
{
@ -299,6 +342,13 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
user_id = user_id ,
)
self . _capture_turn_decision_trace ( turn_decision )
decision_entities = self . _extracted_entities_from_turn_decision ( turn_decision )
if self . _has_useful_extraction ( decision_entities ) :
extracted_entities = self . _merge_extracted_entities (
extracted_entities ,
decision_entities ,
)
if not self . _has_useful_extraction ( extracted_entities ) :
llm_extracted_entities = await self . _extract_entities_with_llm (
message = routing_message ,
user_id = user_id ,
@ -307,10 +357,18 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
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 :
started_at = perf_counter ( )
self . _emit_turn_stage_metric (
" extract_entities_short_circuit " ,
started_at ,
has_message_plan_entities = self . _has_useful_extraction (
self . _resolve_entities_for_message_plan (
message_plan = message_plan ,
routed_message = routing_message ,
)
) ,
has_turn_decision_entities = self . _has_useful_extraction ( decision_entities ) ,
)
self . _capture_generic_memory (
user_id = user_id ,
@ -383,6 +441,7 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
self . _update_active_domain ( user_id = user_id , domain_hint = domain_hint )
reusable_router_result = None
orchestration_override = await self . _try_execute_orchestration_control_tool (
message = routing_message ,
user_id = user_id ,
@ -391,7 +450,12 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
queue_notice = queue_notice ,
finish = finish ,
)
if orchestration_override :
if isinstance ( orchestration_override , dict ) :
reusable_router_result = orchestration_override . get ( " llm_result " )
handled_response = orchestration_override . get ( " handled_response " )
if handled_response :
return handled_response
elif orchestration_override :
return orchestration_override
trade_in_response = await self . _try_handle_trade_in_evaluation (
@ -505,6 +569,8 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
tools = self . registry . get_tools ( )
llm_result = reusable_router_result
if not isinstance ( llm_result , dict ) :
llm_result = await self . _call_llm_with_trace (
operation = " router " ,
message = self . _build_router_prompt ( user_message = routing_message , user_id = user_id ) ,
@ -560,6 +626,11 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
tool_result = tool_result ,
user_id = user_id ,
)
if tool_name == " consultar_frota_aluguel " :
self . _seed_pending_rental_draft_from_message (
message = routing_message ,
user_id = user_id ,
)
if self . _should_use_deterministic_response ( tool_name ) :
return await finish (
@ -615,7 +686,7 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
extracted_entities : dict ,
queue_notice : str | None ,
finish ,
) - > str | None :
) - > str | dict | None :
decision = turn_decision or { }
decision_action = str ( decision . get ( " action " ) or " " ) . strip ( )
decision_tool_name = str ( decision . get ( " tool_name " ) or " " ) . strip ( )
@ -647,6 +718,9 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
queue_notice = queue_notice ,
)
if self . _should_skip_orchestration_control_router ( turn_decision = decision ) :
return None
tools = self . registry . get_tools ( )
llm_result = await self . _call_llm_with_trace (
operation = " orchestration_router " ,
@ -687,8 +761,12 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
)
and self . _is_low_value_response ( first_pass_text )
)
reusable_first_pass = self . _build_reusable_router_result_payload (
llm_result = llm_result ,
source = " orchestration_router " ,
)
if not should_force_tool :
return None
return reusable_first_pass
llm_result = await self . _call_llm_with_trace (
operation = " orchestration_force_tool " ,
@ -698,7 +776,10 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
forced_tool_call = llm_result . get ( " tool_call " ) or { }
forced_tool_name = forced_tool_call . get ( " name " )
if forced_tool_name not in ORCHESTRATION_CONTROL_TOOLS :
return None
return self . _build_reusable_router_result_payload (
llm_result = llm_result ,
source = " orchestration_force_tool " ,
)
if (
forced_tool_name == " cancelar_fluxo_atual "
and self . policy . should_defer_flow_cancellation_control ( message = message , user_id = user_id )
@ -1406,6 +1487,48 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
queue_notice = queue_notice ,
)
async def _try_handle_deterministic_rental_bootstrap (
self ,
message : str ,
user_id : int | None ,
finish ,
) - > str | None :
if user_id is None :
return None
if (
self . _has_rental_return_management_request ( message , user_id = user_id )
or self . _has_rental_payment_or_fine_request ( message )
) :
return None
if (
self . _has_explicit_order_request ( message )
or self . _has_stock_listing_request ( message )
or self . _has_order_listing_request ( message )
or self . _has_trade_in_evaluation_request ( message )
) :
return None
explicit_rental_request = self . _has_explicit_rental_request ( message )
rental_listing_request = self . _has_rental_listing_request ( message )
if not explicit_rental_request and not rental_listing_request :
return None
turn_decision = {
" intent " : " rental_create " if explicit_rental_request else " rental_list " ,
" domain " : " rental " ,
" action " : " collect_rental_create " if explicit_rental_request else " collect_rental_list " ,
}
response = await self . _try_collect_and_open_rental (
message = message ,
user_id = user_id ,
extracted_fields = { } ,
intents = { } ,
turn_decision = turn_decision ,
)
if not response :
return None
return await finish ( response )
async def _try_handle_active_sales_follow_up (
self ,
message : str ,
@ -1612,6 +1735,11 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
tool_result = tool_result ,
user_id = user_id ,
)
if tool_name == " consultar_frota_aluguel " :
self . _seed_pending_rental_draft_from_message (
message = message ,
user_id = user_id ,
)
if self . _should_use_deterministic_response ( tool_name ) :
return await finish (
@ -1686,7 +1814,9 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
# Nessa funcao eu configuro a memoria volatil do sistema
def _upsert_user_context ( self , user_id : int | None ) - > None :
started_at = perf_counter ( )
self . _context_manager . upsert_user_context ( user_id = user_id )
self . _emit_turn_stage_metric ( " upsert_user_context " , started_at )
def _get_user_context ( self , user_id : int | None ) - > dict | None :
return self . _context_manager . get_user_context ( user_id )
@ -1738,6 +1868,7 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
user = self . _get_user_record ( user_id = user_id )
if not user or not getattr ( user , " email " , None ) :
return
started_at = perf_counter ( )
try :
sync_user_email_integration_routes (
user_id = user . id ,
@ -1745,6 +1876,11 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
recipient_name = user . name ,
)
self . _user_profile_routes_ready = True
self . _emit_turn_stage_metric (
" ensure_user_email_routes " ,
started_at ,
synced_routes_count = 6 ,
)
except Exception :
logger . exception (
" Falha ao sincronizar rotas de email do usuario. " ,
@ -1952,17 +2088,104 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
def _coerce_extraction_contract ( self , payload ) - > dict :
return self . normalizer . coerce_extraction_contract ( payload )
async def _extract_turn_bundle_with_llm ( self , message : str , user_id : int | None ) - > dict | None :
planner = getattr ( self , " planner " , None )
if planner is None or not hasattr ( planner , " extract_turn_bundle " ) :
return None
started_at = perf_counter ( )
result = await planner . extract_turn_bundle ( message = message , user_id = user_id )
self . _emit_turn_stage_metric (
" extract_turn_bundle " ,
started_at ,
has_turn_decision = bool ( ( result or { } ) . get ( " has_turn_decision " ) ) ,
has_message_plan = bool ( ( result or { } ) . get ( " has_message_plan " ) ) ,
order_count = len ( ( ( ( result . get ( " message_plan " ) if isinstance ( result , dict ) else { } ) or { } ) . get ( " orders " ) or [ ] ) ) ,
)
return result if isinstance ( result , dict ) else None
async def _extract_message_plan_with_llm ( self , message : str , user_id : int | None ) - > dict :
return await self . planner . extract_message_plan ( message = message , user_id = user_id )
started_at = perf_counter ( )
result = await self . planner . extract_message_plan ( message = message , user_id = user_id )
self . _emit_turn_stage_metric (
" extract_message_plan " ,
started_at ,
order_count = len ( result . get ( " orders " ) or [ ] ) if isinstance ( result , dict ) else 0 ,
)
return result
async def _extract_routing_with_llm ( self , message : str , user_id : int | None ) - > dict :
return await self . planner . extract_routing ( message = message , user_id = user_id )
started_at = perf_counter ( )
result = await self . planner . extract_routing ( message = message , user_id = user_id )
self . _emit_turn_stage_metric (
" extract_routing " ,
started_at ,
order_count = len ( result . get ( " orders " ) or [ ] ) if isinstance ( result , dict ) else 0 ,
)
return result
async def _extract_entities_with_llm ( self , message : str , user_id : int | None ) - > dict :
return await self . planner . extract_entities ( message = message , user_id = user_id )
started_at = perf_counter ( )
result = await self . planner . extract_entities ( message = message , user_id = user_id )
self . _emit_turn_stage_metric (
" extract_entities " ,
started_at ,
has_generic_memory = bool ( ( result or { } ) . get ( " generic_memory " ) ) ,
review_field_keys = [
key
for key , value in ( ( result or { } ) . get ( " review_fields " ) or { } ) . items ( )
if value not in ( None , " " , [ ] , { } )
] ,
)
return result
async def _extract_sales_search_context_with_llm ( self , message : str , user_id : int | None ) - > dict :
return await self . planner . extract_sales_search_context ( message = message , user_id = user_id )
started_at = perf_counter ( )
result = await self . planner . extract_sales_search_context ( message = message , user_id = user_id )
self . _emit_turn_stage_metric (
" extract_sales_search_context " ,
started_at ,
has_budget = bool ( ( result or { } ) . get ( " orcamento_max " ) ) ,
profile_count = len ( ( result or { } ) . get ( " perfil_veiculo " ) or [ ] ) ,
)
return result
def _extract_sales_search_context_deterministically ( self , message : str ) - > dict :
started_at = perf_counter ( )
candidate = str ( message or " " ) . strip ( )
if not candidate :
return { }
extracted : dict [ str , object ] = { }
budget = extract_budget_from_text ( candidate )
if budget :
extracted [ " orcamento_max " ] = int ( round ( budget ) )
normalized_message = self . _normalize_text ( candidate )
raw_profiles : list [ str ] = [ ]
for pattern , canonical in (
( r " \ bsuv \ b " , " suv " ) ,
( r " \ bsedan \ b " , " sedan " ) ,
( r " \ bhatch \ b " , " hatch " ) ,
( r " \ bpickup \ b " , " pickup " ) ,
( r " \ bpicape \ b " , " pickup " ) ,
) :
if canonical in raw_profiles :
continue
if re . search ( pattern , normalized_message ) :
raw_profiles . append ( canonical )
profile = normalize_vehicle_profile ( raw_profiles )
if profile :
extracted [ " perfil_veiculo " ] = profile
if extracted :
self . _emit_turn_stage_metric (
" extract_sales_search_context_short_circuit " ,
started_at ,
source = " technical " ,
has_budget = bool ( extracted . get ( " orcamento_max " ) ) ,
profile_count = len ( extracted . get ( " perfil_veiculo " ) or [ ] ) ,
)
return extracted
async def _extract_missing_sales_search_context_with_llm (
self ,
@ -1974,7 +2197,9 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
decision = turn_decision or { }
decision_intent = str ( decision . get ( " intent " ) or " " ) . strip ( ) . lower ( )
decision_domain = str ( decision . get ( " domain " ) or " " ) . strip ( ) . lower ( )
if decision_domain != " sales " and decision_intent not in { " order_create " , " order_list " , " inventory_search " } :
if decision_intent not in { " order_create " , " inventory_search " } and decision_domain != " sales " :
return { }
if decision_intent not in { " order_create " , " inventory_search " } :
return { }
generic_memory = ( extracted_entities or { } ) . get ( " generic_memory " )
@ -1982,10 +2207,20 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
generic_memory = { }
if generic_memory . get ( " orcamento_max " ) or generic_memory . get ( " perfil_veiculo " ) :
return { }
return await self . _extract_sales_search_context_with_llm ( message = message , user_id = user_id )
return self . _extract_sales_search_context_deterministically ( message )
async def _extract_turn_decision_with_llm ( self , message : str , user_id : int | None ) - > dict :
return await self . planner . extract_turn_decision ( message = message , user_id = user_id )
started_at = perf_counter ( )
result = await self . planner . extract_turn_decision ( message = message , user_id = user_id )
self . _emit_turn_stage_metric (
" extract_turn_decision " ,
started_at ,
intent = str ( ( result or { } ) . get ( " intent " ) or " " ) ,
action = str ( ( result or { } ) . get ( " action " ) or " " ) ,
domain = str ( ( result or { } ) . get ( " domain " ) or " " ) ,
)
return result
async def _try_handle_immediate_context_reset (
self ,
@ -2042,6 +2277,14 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
def _has_useful_turn_decision ( self , turn_decision : dict | None ) - > bool :
if not isinstance ( turn_decision , dict ) :
return False
if str ( turn_decision . get ( " response_to_user " ) or " " ) . strip ( ) :
return True
if turn_decision . get ( " selection_index " ) is not None :
return True
if str ( turn_decision . get ( " tool_name " ) or " " ) . strip ( ) :
return True
if turn_decision . get ( " missing_fields " ) :
return True
if ( turn_decision . get ( " intent " ) or " general " ) != " general " :
return True
if ( turn_decision . get ( " action " ) or " answer_user " ) != " answer_user " :
@ -2049,11 +2292,140 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
entities = turn_decision . get ( " entities " )
return self . _has_useful_extraction ( self . _extracted_entities_from_turn_decision ( turn_decision ) ) if isinstance ( entities , dict ) else False
def _should_attempt_turn_bundle ( self , message : str , early_turn_decision : dict | None ) - > bool :
# O bundle ficou caro e instavel nas amostras atuais.
# Mantemos o caminho desabilitado por padrao e deixamos opt-in
# para cenarios/testes onde ainda queremos exercita-lo.
return False
def _can_synthesize_message_plan_from_turn_decision ( self , message : str , turn_decision : dict | None ) - > bool :
if not str ( message or " " ) . strip ( ) :
return False
if str ( ( turn_decision or { } ) . get ( " action " ) or " " ) . strip ( ) . lower ( ) != " call_tool " :
return False
normalized_tool_name = self . normalizer . normalize_tool_name ( ( turn_decision or { } ) . get ( " tool_name " ) )
if normalized_tool_name not in { " consultar_estoque " , " avaliar_veiculo_troca " } :
return False
if self . _has_message_plan_synthesis_conflict (
message = message ,
turn_decision = turn_decision ,
normalized_tool_name = normalized_tool_name ,
) :
return False
if normalized_tool_name == " consultar_estoque " :
return self . _has_stock_listing_request ( message , turn_decision = turn_decision )
return self . _has_trade_in_evaluation_request ( message , turn_decision = turn_decision )
def _has_message_plan_synthesis_conflict (
self ,
message : str ,
turn_decision : dict | None ,
normalized_tool_name : str ,
) - > bool :
normalized_message = self . _normalize_text ( message ) . strip ( )
if not normalized_message :
return False
seed_order = {
" domain " : " sales " ,
" message " : str ( message or " " ) . strip ( ) ,
" entities " : self . _empty_extraction_payload ( ) ,
}
augmented_orders = [ seed_order ]
if hasattr ( self , " policy " ) and self . policy is not None :
augmented_orders = self . policy . augment_actionable_orders_from_message (
message = message ,
extracted_orders = [ seed_order ] ,
)
actionable_domains = {
str ( order . get ( " domain " ) or " general " )
for order in augmented_orders
if isinstance ( order , dict )
}
if len ( actionable_domains & { " sales " , " review " , " rental " } ) > 1 :
return True
if self . _has_order_listing_request ( message = message , turn_decision = turn_decision ) :
return True
if normalized_tool_name == " consultar_estoque " :
return self . _has_trade_in_evaluation_request ( message , turn_decision = turn_decision )
return (
self . _has_stock_listing_request ( message = message , turn_decision = turn_decision )
or self . _has_explicit_order_request ( message )
)
def _synthesize_message_plan_from_turn_decision ( self , message : str , turn_decision : dict | None ) - > dict :
domain = self . _domain_from_turn_decision ( turn_decision )
normalized_tool_name = self . normalizer . normalize_tool_name ( ( turn_decision or { } ) . get ( " tool_name " ) )
if domain == " general " and normalized_tool_name in { " consultar_estoque " , " avaliar_veiculo_troca " } :
domain = " sales "
extracted_entities = self . _merge_extracted_entities (
self . _empty_extraction_payload ( ) ,
self . _extracted_entities_from_turn_decision ( turn_decision ) ,
)
return {
" orders " : [
{
" domain " : domain ,
" message " : str ( message or " " ) . strip ( ) ,
" entities " : extracted_entities ,
}
]
}
def _build_reusable_router_result_payload ( self , llm_result : dict | None , source : str ) - > dict | None :
if not isinstance ( llm_result , dict ) :
return None
tool_call = llm_result . get ( " tool_call " ) or { }
tool_name = str ( tool_call . get ( " name " ) or " " ) . strip ( )
if tool_name and tool_name not in ORCHESTRATION_CONTROL_TOOLS :
return { " llm_result " : llm_result , " source " : source }
response_text = str ( llm_result . get ( " response " ) or " " ) . strip ( )
if response_text and not self . _is_low_value_response ( response_text ) :
return { " llm_result " : llm_result , " source " : source }
return None
def _should_skip_orchestration_control_router ( self , turn_decision : dict | None ) - > bool :
decision = turn_decision or { }
decision_action = str ( decision . get ( " action " ) or " " ) . strip ( ) . lower ( )
decision_intent = str ( decision . get ( " intent " ) or " " ) . strip ( ) . lower ( )
decision_domain = str ( decision . get ( " domain " ) or " " ) . strip ( ) . lower ( )
normalized_tool_name = self . normalizer . normalize_tool_name ( decision . get ( " tool_name " ) )
if normalized_tool_name in ORCHESTRATION_CONTROL_TOOLS :
return False
if decision_action in { " clear_context " , " continue_queue " , " discard_queue " , " cancel_active_flow " } :
return False
if decision_action == " call_tool " and normalized_tool_name :
return True
if decision_intent in {
" order_create " ,
" inventory_search " ,
" order_list " ,
" order_cancel " ,
" review_schedule " ,
" review_list " ,
" review_cancel " ,
" review_reschedule " ,
} :
return True
return decision_domain in { " sales " , " review " } and decision_action in {
" ask_missing_fields " ,
" collect_review_schedule " ,
" collect_review_management " ,
" collect_order_create " ,
" collect_order_cancel " ,
}
def _extracted_entities_from_turn_decision ( self , turn_decision : dict | None ) - > dict :
entities = ( turn_decision or { } ) . get ( " entities " )
if not isinstance ( entities , dict ) :
entities = { }
return {
extracted = {
" generic_memory " : entities . get ( " generic_memory " , { } ) ,
" review_fields " : entities . get ( " review_fields " , { } ) ,
" review_management_fields " : entities . get ( " review_management_fields " , { } ) ,
@ -2062,6 +2434,43 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
" intents " : { } ,
}
normalized_tool_name = self . normalizer . normalize_tool_name ( ( turn_decision or { } ) . get ( " tool_name " ) )
raw_tool_arguments = ( turn_decision or { } ) . get ( " tool_arguments " )
if normalized_tool_name == " avaliar_veiculo_troca " and isinstance ( raw_tool_arguments , dict ) :
normalized_arguments = self . normalizer . normalize_tool_arguments (
" avaliar_veiculo_troca " ,
raw_tool_arguments ,
)
if normalized_arguments :
review_fields = extracted . get ( " review_fields " )
if not isinstance ( review_fields , dict ) :
review_fields = { }
for field in ( " modelo " , " ano " , " km " ) :
value = normalized_arguments . get ( field )
if value not in ( None , " " , [ ] , { } ) :
review_fields [ field ] = value
extracted [ " review_fields " ] = review_fields
if normalized_tool_name == " consultar_estoque " and isinstance ( raw_tool_arguments , dict ) :
normalized_arguments = self . normalizer . normalize_tool_arguments (
" consultar_estoque " ,
raw_tool_arguments ,
)
if normalized_arguments :
generic_memory = extracted . get ( " generic_memory " )
if not isinstance ( generic_memory , dict ) :
generic_memory = { }
budget = normalized_arguments . get ( " preco_max " )
if budget not in ( None , " " , [ ] , { } ) :
generic_memory [ " orcamento_max " ] = int ( round ( float ( budget ) ) )
category = str ( normalized_arguments . get ( " categoria " ) or " " ) . strip ( ) . lower ( )
if category :
existing_profiles = normalize_vehicle_profile ( generic_memory . get ( " perfil_veiculo " ) )
generic_memory [ " perfil_veiculo " ] = normalize_vehicle_profile ( [ * existing_profiles , category ] )
extracted [ " generic_memory " ] = generic_memory
return extracted
def _merge_extracted_entities ( self , base : dict | None , override : dict | None ) - > dict :
merged = self . _empty_extraction_payload ( )
for section in ( " generic_memory " , " review_fields " , " review_management_fields " , " order_fields " , " cancel_order_fields " ) :
@ -2643,17 +3052,20 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
exc : HTTPException ,
user_id : int | None ,
) - > None :
if tool_name != " agendar_revisao " or user_id is None or exc . status_code != 409 :
if tool_name not in { " agendar_revisao " , " editar_data_revisao " } or user_id is None or exc . status_code != 409 :
return
detail = exc . detail if isinstance ( exc . detail , dict ) else { }
suggested_iso = str ( detail . get ( " suggested_iso " ) or " " ) . strip ( )
if not suggested_iso :
return
payload = dict ( arguments or { } )
if not payload . get ( " placa " ) :
datetime_field = " nova_data_hora " if tool_name == " editar_data_revisao " else " data_hora "
required_field = " protocolo " if tool_name == " editar_data_revisao " else " placa "
if not payload . get ( required_field ) :
return
payload [ " data_hora " ] = suggested_iso
payload [ datetime_field ] = suggested_iso
self . state . set_entry ( " pending_review_confirmations " , user_id , {
" tool_name " : tool_name ,
" payload " : payload ,
" expires_at " : utc_now ( ) + timedelta ( minutes = PENDING_REVIEW_TTL_MINUTES ) ,
} )
@ -2773,6 +3185,14 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
def _format_turn_error ( self , exc : Exception ) - > str :
return self . _execution_manager . format_turn_error ( exc )
def _emit_turn_stage_metric ( self , operation : str , started_at : float , * * payload ) - > None :
self . _log_turn_event (
" turn_stage_completed " ,
operation = operation ,
elapsed_ms = round ( ( perf_counter ( ) - started_at ) * 1000 , 2 ) ,
* * payload ,
)
def _log_turn_event ( self , event : str , * * payload ) - > None :
self . _execution_manager . log_turn_event ( event , * * payload )
@ -2870,17 +3290,20 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
exc : HTTPException ,
user_id : int | None ,
) - > None :
if tool_name != " agendar_revisao " or user_id is None or exc . status_code != 409 :
if tool_name not in { " agendar_revisao " , " editar_data_revisao " } or user_id is None or exc . status_code != 409 :
return
detail = exc . detail if isinstance ( exc . detail , dict ) else { }
suggested_iso = str ( detail . get ( " suggested_iso " ) or " " ) . strip ( )
if not suggested_iso :
return
payload = dict ( arguments or { } )
if not payload . get ( " placa " ) :
datetime_field = " nova_data_hora " if tool_name == " editar_data_revisao " else " data_hora "
required_field = " protocolo " if tool_name == " editar_data_revisao " else " placa "
if not payload . get ( required_field ) :
return
payload [ " data_hora " ] = suggested_iso
payload [ datetime_field ] = suggested_iso
self . state . set_entry ( " pending_review_confirmations " , user_id , {
" tool_name " : tool_name ,
" payload " : payload ,
" expires_at " : utc_now ( ) + timedelta ( minutes = PENDING_REVIEW_TTL_MINUTES ) ,
} )
@ -2897,28 +3320,39 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
if not pending :
return None
pending_tool_name = str ( pending . get ( " tool_name " ) or " agendar_revisao " ) . strip ( ) or " agendar_revisao "
datetime_field = " nova_data_hora " if pending_tool_name == " editar_data_revisao " else " data_hora "
normalized_schedule_fields = self . _normalize_review_fields ( extracted_review_fields )
normalized_management_fields = self . _normalize_review_management_fields ( extracted_review_fields )
normalized_message_datetime = None if self . _is_affirmative_message ( message ) else self . _normalize_review_datetime_text ( message )
time_only = self . _extract_time_only ( message )
if self . _is_negative_message ( message ) or time_only :
extracted = self . _normalize_review_fields ( extracted_review_fields )
new_data_hora = extracted . get ( " data_hora " )
new_data_hora = (
normalized_management_fields . get ( " nova_data_hora " )
if pending_tool_name == " editar_data_revisao "
else normalized_schedule_fields . get ( " data_hora " )
)
if not new_data_hora and normalized_message_datetime :
new_data_hora = normalized_message_datetime
if not new_data_hora and time_only :
new_data_hora = self . _merge_date_with_time ( pending [ " payload " ] . get ( " data_hora " , " " ) , time_only )
new_data_hora = self . _merge_date_with_time ( pending [ " payload " ] . get ( datetime_field , " " ) , time_only )
if self . _is_negative_message ( message ) or time_only or ( new_data_hora and not self . _is_affirmative_message ( message ) ) :
if not new_data_hora :
self . state . pop_entry ( " pending_review_confirmations " , user_id )
return " Sem problema. Me informe a nova data e hora desejada para a revisao. "
payload = dict ( pending [ " payload " ] )
payload [ " data_hora " ] = new_data_hora
payload [ datetime_field ] = new_data_hora
try :
tool_result = await self . tool_executor . execute (
" agendar_revisao " ,
pending_tool_name ,
payload ,
user_id = user_id ,
)
except HTTPException as exc :
self . state . pop_entry ( " pending_review_confirmations " , user_id )
self . _capture_review_confirmation_suggestion (
tool_name = " agendar_revisao " ,
tool_name = pending_tool_name ,
arguments = payload ,
exc = exc ,
user_id = user_id ,
@ -2926,24 +3360,32 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
return self . _http_exception_detail ( exc )
self . _reset_pending_review_states ( user_id = user_id )
if pending_tool_name == " agendar_revisao " :
self . _store_last_review_package ( user_id = user_id , payload = payload )
return self . _fallback_format_tool_result ( " agendar_revisao " , tool_result )
return self . _fallback_format_tool_result ( pending_tool_name , tool_result )
if not self . _is_affirmative_message ( message ) :
return None
try :
tool_result = await self . tool_executor . execute (
" agendar_revisao " ,
pending_tool_name ,
pending [ " payload " ] ,
user_id = user_id ,
)
except HTTPException as exc :
self . state . pop_entry ( " pending_review_confirmations " , user_id )
self . _capture_review_confirmation_suggestion (
tool_name = pending_tool_name ,
arguments = pending . get ( " payload " ) or { } ,
exc = exc ,
user_id = user_id ,
)
return self . _http_exception_detail ( exc )
self . _reset_pending_review_states ( user_id = user_id )
if pending_tool_name == " agendar_revisao " :
self . _store_last_review_package ( user_id = user_id , payload = pending . get ( " payload " ) )
return self . _fallback_format_tool_result ( " agendar_revisao " , tool_result )
return self . _fallback_format_tool_result ( pending_tool_name , tool_result )
def _http_exception_detail ( self , exc : HTTPException ) - > str :
return self . _execution_manager . http_exception_detail ( exc )
@ -2953,3 +3395,5 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
tool_name = tool_name ,
tool_result = tool_result ,
)