@ -80,6 +80,8 @@ ACTIVE_TASK_LABELS = {
" rental_create " : " abertura de locacao " ,
" rental_create " : " abertura de locacao " ,
}
}
ACTIONABLE_ORDER_DOMAINS = { " review " , " sales " , " rental " }
# essa classe é responsável por controlar qual o assunto está ativo na conversa, se existe fluxo aberto, se o usuário mandou dois pedidos ao mesmo tempo...
# essa classe é responsável por controlar qual o assunto está ativo na conversa, se existe fluxo aberto, se o usuário mandou dois pedidos ao mesmo tempo...
class ConversationPolicy :
class ConversationPolicy :
def __init__ ( self , service : " OrquestradorService " ) :
def __init__ ( self , service : " OrquestradorService " ) :
@ -132,20 +134,21 @@ class ConversationPolicy:
domain : str ,
domain : str ,
order_message : str ,
order_message : str ,
memory_seed : dict | None = None ,
memory_seed : dict | None = None ,
) - > None :
) - > bool :
context = self . service . _get_user_context ( user_id )
context = self . service . _get_user_context ( user_id )
if not context or domain == " general " :
if not context or domain == " general " :
return
return False
queue = context . setdefault ( " order_queue " , [ ] )
queue = context . setdefault ( " order_queue " , [ ] )
queue . append (
queue . append (
{
{
" domain " : domain ,
" domain " : domain ,
" message " : ( order_message or " " ) . strip ( ) ,
" message " : self . build_order_execution_message ( domain , order_message ) ,
" memory_seed " : dict ( memory_seed or self . service . _new_tab_memory ( user_id = user_id ) ) ,
" memory_seed " : dict ( memory_seed or self . service . _new_tab_memory ( user_id = user_id ) ) ,
" created_at " : utc_now ( ) . isoformat ( ) ,
" created_at " : utc_now ( ) . isoformat ( ) ,
}
}
)
)
self . _save_context ( user_id = user_id , context = context )
self . _save_context ( user_id = user_id , context = context )
return True
# Transforma as entidades extraídas de um pedido em uma memória temporária pronta para usar quando esse pedido for processado.
# Transforma as entidades extraídas de um pedido em uma memória temporária pronta para usar quando esse pedido for processado.
@ -210,7 +213,7 @@ class ConversationPolicy:
if not isinstance ( item , dict ) :
if not isinstance ( item , dict ) :
continue
continue
domain = str ( item . get ( " domain " ) or " general " ) . strip ( ) . lower ( )
domain = str ( item . get ( " domain " ) or " general " ) . strip ( ) . lower ( )
if domain not in { " review " , " sales " , " general " } :
if domain not in ACTIONABLE_ORDER_DOMAINS | { " general " } :
domain = " general "
domain = " general "
segment = str ( item . get ( " message " ) or " " ) . strip ( )
segment = str ( item . get ( " message " ) or " " ) . strip ( )
if segment :
if segment :
@ -223,14 +226,19 @@ class ConversationPolicy:
)
)
if not extracted_orders :
if not extracted_orders :
extracted_orders = [ { " domain " : " general " , " message " : ( message or " " ) . strip ( ) } ]
extracted_orders = [ { " domain " : " general " , " message " : ( message or " " ) . strip ( ) } ]
extracted_orders = self . augment_actionable_orders_from_message (
message = message ,
extracted_orders = extracted_orders ,
)
actionable_orders = [ order for order in extracted_orders if order [ " domain " ] in ACTIONABLE_ORDER_DOMAINS ]
if (
if (
len ( extracted_orders ) == 2
len ( actionable_orders ) > = 2
and all ( order [ " domain " ] != " general " for order in extracted_orders )
and not self . has_open_flow ( user_id = user_id , domain = active_domain )
and not self . has_open_flow ( user_id = user_id , domain = active_domain )
) :
) :
self . store_pending_order_selection ( user_id = user_id , orders = extr acted _orders)
self . store_pending_order_selection ( user_id = user_id , orders = actionabl e_orders)
return message , None , self . render_order_selection_prompt ( extr acted _orders)
return message , None , self . render_order_selection_prompt ( actionabl e_orders)
if len ( extracted_orders ) < = 1 :
if len ( extracted_orders ) < = 1 :
inferred = extracted_orders [ 0 ] [ " domain " ]
inferred = extracted_orders [ 0 ] [ " domain " ]
@ -247,29 +255,33 @@ class ConversationPolicy:
if self . has_open_flow ( user_id = user_id , domain = active_domain ) :
if self . has_open_flow ( user_id = user_id , domain = active_domain ) :
queued_count = 0
queued_count = 0
for queued in extr acted _orders:
for queued in actionabl e_orders:
if queued [ " domain " ] != active_domain :
if queued [ " domain " ] != active_domain :
self . queue_order_with_memory_seed (
queued_count + = int (
user_id = user_id ,
self . queue_order_with_memory_seed (
domain = queued [ " domain " ] ,
user_id = user_id ,
order_message = queued [ " message " ] ,
domain = queued [ " domain " ] ,
memory_seed = self . build_order_memory_seed ( user_id = user_id , order = queued ) ,
order_message = queued [ " message " ] ,
memory_seed = self . build_order_memory_seed ( user_id = user_id , order = queued ) ,
)
)
)
queued_count + = 1
queue_hint = self . render_queue_notice ( queued_count )
queue_hint = self . render_queue_notice ( queued_count )
prompt = self . render_open_flow_prompt ( user_id = user_id , domain = active_domain )
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
return message , None , f " { prompt } \n { queue_hint } " if queue_hint else prompt
first = extracted_orders[ 0 ]
first = actionable_orders[ 0 ] if actionable_orders else extracted_orders[ 0 ]
queued_count = 0
queued_count = 0
for queued in extracted_orders [ 1 : ] :
for queued in actionable_orders :
self . queue_order_with_memory_seed (
if queued is first :
user_id = user_id ,
continue
domain = queued [ " domain " ] ,
queued_count + = int (
order_message = queued [ " message " ] ,
self . queue_order_with_memory_seed (
memory_seed = self . build_order_memory_seed ( user_id = user_id , order = queued ) ,
user_id = user_id ,
domain = queued [ " domain " ] ,
order_message = queued [ " message " ] ,
memory_seed = self . build_order_memory_seed ( user_id = user_id , order = queued ) ,
)
)
)
queued_count + = 1
context [ " active_domain " ] = first [ " domain " ]
context [ " active_domain " ] = first [ " domain " ]
context [ " generic_memory " ] = self . build_order_memory_seed ( user_id = user_id , order = first )
context [ " generic_memory " ] = self . build_order_memory_seed ( user_id = user_id , order = first )
self . _save_context ( user_id = user_id , context = context )
self . _save_context ( user_id = user_id , context = context )
@ -297,9 +309,10 @@ class ConversationPolicy:
{
{
" domain " : order [ " domain " ] ,
" domain " : order [ " domain " ] ,
" message " : order [ " message " ] ,
" message " : order [ " message " ] ,
" seed_message " : self . build_order_execution_message ( order [ " domain " ] , order [ " message " ] ) ,
" memory_seed " : self . build_order_memory_seed ( user_id = user_id , order = order ) ,
" memory_seed " : self . build_order_memory_seed ( user_id = user_id , order = order ) ,
}
}
for order in orders [ : 2 ]
for order in orders
] ,
] ,
" expires_at " : utc_now ( ) + timedelta ( minutes = PENDING_ORDER_SELECTION_TTL_MINUTES ) ,
" expires_at " : utc_now ( ) + timedelta ( minutes = PENDING_ORDER_SELECTION_TTL_MINUTES ) ,
}
}
@ -310,15 +323,68 @@ class ConversationPolicy:
def render_order_selection_prompt ( self , orders : list [ dict ] ) - > str :
def render_order_selection_prompt ( self , orders : list [ dict ] ) - > str :
if len ( orders ) < 2 :
if len ( orders ) < 2 :
return " Qual das acoes voce quer iniciar primeiro? "
return " Qual das acoes voce quer iniciar primeiro? "
first_label = self . describe_order_selection_option ( orders [ 0 ] )
enumerated_orders = " \n " . join (
second_label = self . describe_order_selection_option ( orders [ 1 ] )
f " { index } . { self . describe_order_selection_option ( order ) } "
for index , order in enumerate ( orders , start = 1 )
)
return (
return (
" Identifiquei duas acoes na sua mensagem: \n "
f " Identifiquei { len ( orders ) } acoes na sua mensagem: \n "
f " 1. { first_label } \n "
f " { enumerated_orders } \n "
f " 2. { second_label } \n "
" Qual delas voce quer iniciar primeiro? Se for indiferente, eu escolho. "
" Qual delas voce quer iniciar primeiro? Se for indiferente, eu escolho. "
)
)
def build_order_execution_message ( self , domain : str , order_message : str | None ) - > str :
raw_message = str ( order_message or " " ) . strip ( )
normalized = self . service . normalizer . normalize_text ( raw_message ) . strip ( )
if domain == " sales " and normalized in { " compra " , " comprar " , " venda " , " pedido " } :
return " quero comprar um veiculo "
if domain == " review " and normalized in { " revisao " , " agendamento " , " agendar " , " marcar revisao " } :
return " quero agendar revisao "
if domain == " rental " and normalized in { " aluguel " , " alugar " , " locacao " , " locar " } :
return " quero alugar um carro "
return raw_message
def augment_actionable_orders_from_message ( self , message : str , extracted_orders : list [ dict ] ) - > list [ dict ] :
normalized = self . service . normalizer . normalize_text ( message ) . strip ( )
if not normalized :
return extracted_orders
existing_domains = {
str ( order . get ( " domain " ) or " general " )
for order in extracted_orders
if isinstance ( order , dict )
}
domain_hints = (
( " sales " , { " compra " , " comprar " , " venda " , " pedido " } , " compra " ) ,
( " review " , { " revisao " , " agendamento " , " agendar " , " remarcar " } , " revisao " ) ,
( " rental " , { " aluguel " , " alugar " , " locacao " , " locar " } , " aluguel " ) ,
)
augmented = list ( extracted_orders )
for domain , terms , label in domain_hints :
if domain in existing_domains :
continue
if any ( term in normalized for term in terms ) :
augmented . append (
{
" domain " : domain ,
" message " : label ,
" entities " : self . service . normalizer . empty_extraction_payload ( ) ,
}
)
return augmented
def render_multi_order_clarification_prompt ( self , orders : list [ dict ] ) - > str :
if not orders :
return " Identifiquei mais de um assunto. Me diga qual voce quer iniciar primeiro. "
options = " \n " . join (
f " - { self . describe_order_selection_option ( order ) } "
for order in orders [ : 3 ]
)
return (
" Identifiquei mais de um assunto na sua mensagem: \n "
f " { options } \n "
" Para eu nao misturar os fluxos, me diga qual deles voce quer comecar primeiro. "
)
# Formata o rótulo do pedido para exibição.
# Formata o rótulo do pedido para exibição.
def describe_order_selection_option ( self , order : dict ) - > str :
def describe_order_selection_option ( self , order : dict ) - > str :
@ -429,6 +495,139 @@ class ConversationPolicy:
}
}
return self . contains_any_term ( normalized , operational_terms )
return self . contains_any_term ( normalized , operational_terms )
def is_explicit_pending_order_selection_message (
self ,
message : str ,
turn_decision : dict | None = None ,
) - > bool :
if self . _decision_selection_index ( turn_decision ) is not None :
return True
normalized = self . strip_choice_message ( self . service . normalizer . normalize_text ( message ) )
if not normalized :
return False
indifferent_tokens = {
" tanto faz " ,
" indiferente " ,
" qualquer um " ,
" qualquer uma " ,
" voce escolhe " ,
" pode escolher " ,
" fica a seu criterio " ,
}
if normalized in indifferent_tokens :
return True
if re . fullmatch ( r " (?:opcao|acao|pedido)? \ s*( \ d+) " , normalized ) :
return True
explicit_selection_messages = {
" compra " ,
" comprar " ,
" quero comprar " ,
" quero comprar um veiculo " ,
" venda " ,
" pedido " ,
" revisao " ,
" agendamento " ,
" agendar " ,
" agendar revisao " ,
" quero agendar revisao " ,
" aluguel " ,
" alugar " ,
" quero alugar " ,
" quero alugar um carro " ,
" locacao " ,
" locar " ,
}
return normalized in explicit_selection_messages
def derive_operational_task_key (
self ,
* ,
message : str ,
turn_decision : dict | None = None ,
fallback_domain : str | None = None ,
) - > str | None :
normalized = self . service . normalizer . normalize_text ( message ) . strip ( )
domain = self . _decision_domain ( turn_decision ) or str ( fallback_domain or " " ) . strip ( ) . lower ( )
intent = self . _decision_intent ( turn_decision )
tool_name = str ( ( turn_decision or { } ) . get ( " tool_name " ) or " " ) . strip ( ) . lower ( )
if domain == " sales " :
if intent == " order_list " or tool_name == " listar_pedidos " or " quais pedidos " in normalized :
return " sales:list "
if intent == " order_cancel " or tool_name == " cancelar_pedido " or ( " cancel " in normalized and " pedido " in normalized ) :
return " sales:cancel "
if tool_name == " avaliar_veiculo_troca " or ( " avali " in normalized and " troca " in normalized ) :
return " sales:trade_in "
if (
intent in { " order_create " , " inventory_search " }
or tool_name in { " consultar_estoque " , " realizar_pedido " }
or self . contains_any_term ( normalized , { " compra " , " comprar " , " venda " , " carro " , " veiculo " } )
) :
return " sales:create "
if domain == " review " :
if intent == " review_list " or tool_name == " listar_agendamentos_revisao " or " agendamentos " in normalized :
return " review:list "
if intent == " review_cancel " or ( " cancel " in normalized and " revis " in normalized ) :
return " review:cancel "
if intent == " review_reschedule " or " remarc " in normalized :
return " review:reschedule "
if (
intent == " review_schedule "
or tool_name == " agendar_revisao "
or self . contains_any_term ( normalized , { " revisao " , " agendar " , " agendamento " } )
) :
return " review:schedule "
if domain == " rental " :
if intent == " rental_list " or tool_name == " consultar_frota_aluguel " or " frota " in normalized :
return " rental:list "
if tool_name == " registrar_devolucao_aluguel " or " devol " in normalized :
return " rental:return "
if tool_name == " registrar_pagamento_aluguel " or " comprovante " in normalized or " pagamento " in normalized :
return " rental:payment "
if tool_name == " registrar_multa_aluguel " or " multa " in normalized :
return " rental:fine "
if (
intent == " rental_create "
or self . contains_any_term ( normalized , { " aluguel " , " alugar " , " locacao " , " locar " } )
) :
return " rental:create "
return None
def derive_pending_order_task_key ( self , order : dict ) - > str | None :
return self . derive_operational_task_key (
message = str ( order . get ( " seed_message " ) or order . get ( " message " ) or " " ) ,
fallback_domain = str ( order . get ( " domain " ) or " general " ) ,
)
def queue_pending_orders_for_later (
self ,
* ,
user_id : int | None ,
orders : list [ dict ] ,
skip_task_key : str | None = None ,
) - > int :
queued_count = 0
skipped_matching_task = False
for order in orders :
if skip_task_key and not skipped_matching_task and self . derive_pending_order_task_key ( order ) == skip_task_key :
skipped_matching_task = True
continue
queued_count + = int (
self . queue_order_with_memory_seed (
user_id = user_id ,
domain = order [ " domain " ] ,
order_message = order [ " message " ] ,
memory_seed = order . get ( " memory_seed " ) ,
)
)
return queued_count
# Distingue um comando global explicito de cancelamento do fluxo atual de um texto livre
# Distingue um comando global explicito de cancelamento do fluxo atual de um texto livre
# que deve ser consumido como dado do rascunho aberto.
# que deve ser consumido como dado do rascunho aberto.
@ -518,26 +717,36 @@ class ConversationPolicy:
}
}
if normalized in indifferent_tokens :
if normalized in indifferent_tokens :
return 0 , True
return 0 , True
numeric_match = re . fullmatch ( r " (?:opcao|acao|pedido)? \ s*( \ d+) " , normalized )
if numeric_match :
candidate = int ( numeric_match . group ( 1 ) ) - 1
if 0 < = candidate < len ( orders ) :
return candidate , False
if normalized in { " 1 " , " primeiro " , " primeira " , " opcao 1 " , " acao 1 " , " pedido 1 " } :
if normalized in { " 1 " , " primeiro " , " primeira " , " opcao 1 " , " acao 1 " , " pedido 1 " } :
return 0 , False
return 0 , False
if normalized in { " 2 " , " segundo " , " segunda " , " opcao 2 " , " acao 2 " , " pedido 2 " } :
if normalized in { " 2 " , " segundo " , " segunda " , " opcao 2 " , " acao 2 " , " pedido 2 " } :
return 1 , False
return 1 , False
if normalized in { " 3 " , " terceiro " , " terceira " , " opcao 3 " , " acao 3 " , " pedido 3 " } :
return ( 2 , False ) if len ( orders ) > = 3 else ( None , False )
decision_domain = self . _decision_domain ( turn_decision )
decision_domain = self . _decision_domain ( turn_decision )
if len ( orders ) > = 2 and decision_domain in { " review " , " sales " } :
if len ( orders ) > = 2 and decision_domain in ACTIONABLE_ORDER_DOMAINS :
matches = [ index for index , order in enumerate ( orders ) if order . get ( " domain " ) == decision_domain ]
matches = [ index for index , order in enumerate ( orders ) if order . get ( " domain " ) == decision_domain ]
if len ( matches ) == 1 :
if len ( matches ) == 1 :
return matches [ 0 ] , False
return matches [ 0 ] , False
review_matches = [ index for index , order in enumerate ( orders ) if order . get ( " domain " ) == " review " ]
review_matches = [ index for index , order in enumerate ( orders ) if order . get ( " domain " ) == " review " ]
sales_matches = [ index for index , order in enumerate ( orders ) if order . get ( " domain " ) == " sales " ]
sales_matches = [ index for index , order in enumerate ( orders ) if order . get ( " domain " ) == " sales " ]
rental_matches = [ index for index , order in enumerate ( orders ) if order . get ( " domain " ) == " rental " ]
has_review_signal = self . contains_any_term ( normalized , { " revisao " , " agendamento " , " agendar " , " remarcar " , " pos venda " } )
has_review_signal = self . contains_any_term ( normalized , { " revisao " , " agendamento " , " agendar " , " remarcar " , " pos venda " } )
has_sales_signal = self . contains_any_term ( normalized , { " venda " , " compra " , " comprar " , " pedido " , " cancelamento " , " cancelar " , " carro " , " veiculo " } )
has_sales_signal = self . contains_any_term ( normalized , { " venda " , " compra " , " comprar " , " pedido " , " cancelamento " , " cancelar " , " carro " , " veiculo " } )
has_rental_signal = self . contains_any_term ( normalized , { " aluguel " , " locacao " , " alugar " , " locar " , " devolucao " , " frota " } )
if len ( review_matches ) == 1 and has_review_signal and not has_sales_signal :
if len ( review_matches ) == 1 and has_review_signal and not has_sales_signal :
return review_matches [ 0 ] , False
return review_matches [ 0 ] , False
if len ( sales_matches ) == 1 and has_sales_signal and not has_review_signal :
if len ( sales_matches ) == 1 and has_sales_signal and not has_review_signal :
return sales_matches [ 0 ] , False
return sales_matches [ 0 ] , False
if len ( rental_matches ) == 1 and has_rental_signal and not has_review_signal and not has_sales_signal :
return rental_matches [ 0 ] , False
return None , False
return None , False
@ -572,26 +781,59 @@ class ConversationPolicy:
return " Tudo bem. Limpei o contexto atual. Pode me dizer o que voce quer fazer agora? "
return " Tudo bem. Limpei o contexto atual. Pode me dizer o que voce quer fazer agora? "
return await self . service . handle_message ( cleaned_message , user_id = user_id )
return await self . service . handle_message ( cleaned_message , user_id = user_id )
if (
self . looks_like_fresh_operational_request ( message , turn_decision = turn_decision )
and not self . is_explicit_pending_order_selection_message ( message , turn_decision = turn_decision )
) :
current_task_key = self . derive_operational_task_key (
message = message ,
turn_decision = turn_decision ,
)
matching_indexes = [
index
for index , order in enumerate ( orders )
if current_task_key and self . derive_pending_order_task_key ( order ) == current_task_key
]
if len ( matching_indexes ) == 1 :
selected_index = matching_indexes [ 0 ]
selected_order = orders [ selected_index ]
context [ " pending_order_selection " ] = None
self . queue_pending_orders_for_later (
user_id = user_id ,
orders = [ order for index , order in enumerate ( orders ) if index != selected_index ] ,
)
intro = f " Perfeito. Vou comecar por: { self . describe_order_selection_option ( selected_order ) } "
selected_memory = dict ( selected_order . get ( " memory_seed " ) or { } )
context [ " active_domain " ] = selected_order . get ( " domain " ) or context . get ( " active_domain " , " general " )
if selected_memory :
context [ " generic_memory " ] = selected_memory
self . _save_context ( user_id = user_id , context = context )
next_response = await self . service . handle_message ( message , user_id = user_id )
return f " { intro } \n { next_response } "
context [ " pending_order_selection " ] = None
self . _save_context ( user_id = user_id , context = context )
self . queue_pending_orders_for_later (
user_id = user_id ,
orders = orders ,
skip_task_key = current_task_key ,
)
return None
selected_index , auto_selected = self . detect_selected_order_index (
selected_index , auto_selected = self . detect_selected_order_index (
message = message ,
message = message ,
orders = orders ,
orders = orders ,
turn_decision = turn_decision ,
turn_decision = turn_decision ,
)
)
if selected_index is None :
if selected_index is None :
if self . looks_like_fresh_operational_request ( message , turn_decision = turn_decision ) :
context [ " pending_order_selection " ] = None
self . _save_context ( user_id = user_id , context = context )
return None
return self . render_order_selection_prompt ( orders )
return self . render_order_selection_prompt ( orders )
selected_order = orders [ selected_index ]
selected_order = orders [ selected_index ]
remaining_order = orders [ 1 - selected_index ]
context [ " pending_order_selection " ] = None
context [ " pending_order_selection " ] = None
self . queue_order_with_memory_seed (
self . queue_ pending_orders_for_later (
user_id = user_id ,
user_id = user_id ,
domain = remaining_order [ " domain " ] ,
orders = [ order for index , order in enumerate ( orders ) if index != selected_index ] ,
order_message = remaining_order [ " message " ] ,
memory_seed = remaining_order . get ( " memory_seed " ) ,
)
)
intro = (
intro = (
@ -600,10 +842,12 @@ class ConversationPolicy:
else f " Perfeito. Vou comecar por: { self . describe_order_selection_option ( selected_order ) } "
else f " Perfeito. Vou comecar por: { self . describe_order_selection_option ( selected_order ) } "
)
)
selected_memory = dict ( selected_order . get ( " memory_seed " ) or { } )
selected_memory = dict ( selected_order . get ( " memory_seed " ) or { } )
context [ " active_domain " ] = selected_order . get ( " domain " ) or context . get ( " active_domain " , " general " )
if selected_memory :
if selected_memory :
context [ " generic_memory " ] = selected_memory
context [ " generic_memory " ] = selected_memory
self . _save_context ( user_id = user_id , context = context )
self . _save_context ( user_id = user_id , context = context )
next_response = await self . service . handle_message ( str ( selected_order . get ( " message " ) or " " ) , user_id = user_id )
selected_message = str ( selected_order . get ( " seed_message " ) or selected_order . get ( " message " ) or " " )
next_response = await self . service . handle_message ( selected_message , user_id = user_id )
return f " { intro } \n { next_response } "
return f " { intro } \n { next_response } "