@ -7,37 +7,29 @@ from datetime import datetime, timedelta
from fastapi import HTTPException
from sqlalchemy . orm import Session
from app . services . orchestrator_config import (
CANCEL_ORDER_REQUIRED_FIELDS ,
from app . services . orchestration . orchestrator_config import (
DETERMINISTIC_RESPONSE_TOOLS ,
LOW_VALUE_RESPONSES ,
LAST_REVIEW_PACKAGE_TTL_MINUTES ,
ORDER_REQUIRED_FIELDS ,
PENDING_CANCEL_ORDER_DRAFT_TTL_MINUTES ,
PENDING_ORDER_DRAFT_TTL_MINUTES ,
PENDING_REVIEW_DRAFT_TTL_MINUTES ,
PENDING_REVIEW_TTL_MINUTES ,
REVIEW_REQUIRED_FIELDS ,
USER_CONTEXT_TTL_MINUTES ,
)
from app . services . llm_service import LLMService
from app . services . tool_registry import ToolRegistry
from app . services . orchestration . conversation_state_store import ConversationStateStore
from app . services . ai . llm_service import LLMService
from app . services . flows . order_flow import OrderFlowMixin
from app . services . orchestration . prompt_builders import (
build_force_tool_prompt ,
build_result_prompt ,
build_router_prompt ,
)
from app . services . flows . review_flow import ReviewFlowMixin
from app . services . orchestration . response_formatter import fallback_format_tool_result
from app . services . tools . tool_registry import ToolRegistry
logger = logging . getLogger ( __name__ )
class OrquestradorService :
USER_CONTEXTS : dict [ int , dict ] = { }
# Memoria temporaria de confirmacao quando a API sugere novo horario (conflito 409).
PENDING_REVIEW_CONFIRMATIONS : dict [ int , dict ] = { }
# Rascunho por usuario para juntar dados de revisao enviados em mensagens separadas.
PENDING_REVIEW_DRAFTS : dict [ int , dict ] = { }
PENDING_REVIEW_MANAGEMENT_DRAFTS : dict [ int , dict ] = { }
LAST_REVIEW_PACKAGES : dict [ int , dict ] = { }
PENDING_REVIEW_REUSE_CONFIRMATIONS : dict [ int , dict ] = { }
PENDING_ORDER_DRAFTS : dict [ int , dict ] = { }
PENDING_CANCEL_ORDER_DRAFTS : dict [ int , dict ] = { }
class OrquestradorService ( ReviewFlowMixin , OrderFlowMixin ) :
state = ConversationStateStore ( )
def __init__ ( self , db : Session ) :
""" Inicializa servicos de LLM e registro de tools para a sessao atual. """
@ -236,45 +228,23 @@ class OrquestradorService:
def _reset_pending_review_states ( self , user_id : int | None ) - > None :
if user_id is None :
return
self . PENDING_REVIEW_DRAFTS. pop ( user_id , None )
self . PENDING_REVIEW_CONFIRMATIONS. pop ( user_id , None )
self . PENDING_REVIEW_MANAGEMENT_DRAFTS. pop ( user_id , None )
self . PENDING_REVIEW_REUSE_CONFIRMATIONS. pop ( user_id , None )
self . state. pop_entry ( " pending_review_drafts " , user_id )
self . state. pop_entry ( " pending_review_confirmations " , user_id )
self . state. pop_entry ( " pending_review_management_drafts " , user_id )
self . state. pop_entry ( " pending_review_reuse_confirmations " , user_id )
def _reset_pending_order_states ( self , user_id : int | None ) - > None :
if user_id is None :
return
self . PENDING_ORDER_DRAFTS. pop ( user_id , None )
self . PENDING_CANCEL_ORDER_DRAFTS. pop ( user_id , None )
self . state. pop_entry ( " pending_order_drafts " , user_id )
self . state. pop_entry ( " pending_cancel_order_drafts " , user_id )
# Nessa função é onde eu configuro a memória volátil do sistema
def _upsert_user_context ( self , user_id : int | None ) - > None :
if user_id is None :
return
now = datetime . utcnow ( )
context = self . USER_CONTEXTS . get ( user_id )
if context and context [ " expires_at " ] > = now :
context [ " expires_at " ] = now + timedelta ( minutes = USER_CONTEXT_TTL_MINUTES )
return
self . USER_CONTEXTS [ user_id ] = {
" active_domain " : " general " ,
" generic_memory " : { } ,
" shared_memory " : { } ,
" order_queue " : [ ] ,
" pending_switch " : None ,
" expires_at " : now + timedelta ( minutes = USER_CONTEXT_TTL_MINUTES ) ,
}
self . state . upsert_user_context ( user_id = user_id , ttl_minutes = USER_CONTEXT_TTL_MINUTES )
def _get_user_context ( self , user_id : int | None ) - > dict | None :
if user_id is None :
return None
context = self . USER_CONTEXTS . get ( user_id )
if not context :
return None
if context [ " expires_at " ] < datetime . utcnow ( ) :
self . USER_CONTEXTS . pop ( user_id , None )
return None
return context
return self . state . get_user_context ( user_id )
def _extract_generic_memory_fields ( self , llm_generic_fields : dict | None = None ) - > dict :
extracted : dict = { }
@ -907,13 +877,13 @@ class OrquestradorService:
def _render_open_flow_prompt ( self , user_id : int | None , domain : str ) - > str :
if domain == " review " and user_id is not None :
draft = self . PENDING_REVIEW_DRAFTS. get ( user_id )
draft = self . state. get_entry ( " pending_review_drafts " , user_id , expire = True )
if draft :
missing = [ field for field in REVIEW_REQUIRED_FIELDS if field not in draft . get ( " payload " , { } ) ]
if missing :
return self . _render_missing_review_fields_prompt ( missing )
management_draft = self . PENDING_REVIEW_MANAGEMENT_DRAFTS. get ( user_id )
management_draft = self . state. get_entry ( " pending_review_management_drafts " , user_id , expire = True )
if management_draft :
action = management_draft . get ( " action " , " cancel " )
payload = management_draft . get ( " payload " , { } )
@ -926,19 +896,19 @@ class OrquestradorService:
if missing :
return self . _render_missing_review_cancel_fields_prompt ( missing )
pending = self . PENDING_REVIEW_CONFIRMATIONS. get ( user_id )
pending = self . state. get_entry ( " pending_review_confirmations " , user_id , expire = True )
if pending :
return " Antes de mudar de assunto, me confirme se podemos concluir seu agendamento de revisao. "
reuse_pending = self . PENDING_REVIEW_REUSE_CONFIRMATIONS. get ( user_id )
reuse_pending = self . state. get_entry ( " pending_review_reuse_confirmations " , user_id , expire = True )
if reuse_pending :
return self . _render_review_reuse_question ( )
if domain == " sales " and user_id is not None :
draft = self . PENDING_ORDER_DRAFTS. get ( user_id )
draft = self . state. get_entry ( " pending_order_drafts " , user_id , expire = True )
if draft :
missing = [ field for field in ORDER_REQUIRED_FIELDS if field not in draft . get ( " payload " , { } ) ]
if missing :
return self . _render_missing_order_fields_prompt ( missing )
cancel_draft = self . PENDING_CANCEL_ORDER_DRAFTS. get ( user_id )
cancel_draft = self . state. get_entry ( " pending_cancel_order_drafts " , user_id , expire = True )
if cancel_draft :
missing = [ field for field in CANCEL_ORDER_REQUIRED_FIELDS if field not in cancel_draft . get ( " payload " , { } ) ]
if missing :
@ -1043,15 +1013,15 @@ class OrquestradorService:
return False
if domain == " review " :
return bool (
self . PENDING_REVIEW_DRAFTS. get ( user_id )
or self . PENDING_REVIEW_CONFIRMATIONS. get ( user_id )
or self . PENDING_REVIEW_MANAGEMENT_DRAFTS. get ( user_id )
or self . PENDING_REVIEW_REUSE_CONFIRMATIONS. get ( user_id )
self . state. get_entry ( " pending_review_drafts " , user_id , expire = True )
or self . state. get_entry ( " pending_review_confirmations " , user_id , expire = True )
or self . state. get_entry ( " pending_review_management_drafts " , user_id , expire = True )
or self . state. get_entry ( " pending_review_reuse_confirmations " , user_id , expire = True )
)
if domain == " sales " :
return bool (
self . PENDING_ORDER_DRAFTS. get ( user_id )
or self . PENDING_CANCEL_ORDER_DRAFTS. get ( user_id )
self . state. get_entry ( " pending_order_drafts " , user_id , expire = True )
or self . state. get_entry ( " pending_cancel_order_drafts " , user_id , expire = True )
)
return False
@ -1158,522 +1128,6 @@ class OrquestradorService:
def _is_low_value_response ( self , text : str ) - > bool :
return text . strip ( ) . lower ( ) in LOW_VALUE_RESPONSES
async def _try_handle_review_management (
self ,
message : str ,
user_id : int | None ,
extracted_fields : dict | None = None ,
intents : dict | None = None ,
) - > str | None :
if user_id is None :
return None
normalized_intents = self . _normalize_intents ( intents )
draft = self . PENDING_REVIEW_MANAGEMENT_DRAFTS . get ( user_id )
if draft and draft [ " expires_at " ] < datetime . utcnow ( ) :
self . PENDING_REVIEW_MANAGEMENT_DRAFTS . pop ( user_id , None )
draft = None
has_list_intent = normalized_intents . get ( " review_list " , False )
has_cancel_intent = normalized_intents . get ( " review_cancel " , False )
has_reschedule_intent = normalized_intents . get ( " review_reschedule " , False )
if has_list_intent :
# Listagem e acao terminal; limpa rascunhos de revisao para evitar conflito de contexto.
self . _reset_pending_review_states ( user_id = user_id )
try :
tool_result = await self . registry . execute (
" listar_agendamentos_revisao " ,
{ " limite " : 20 } ,
user_id = user_id ,
)
except HTTPException as exc :
return self . _http_exception_detail ( exc )
return self . _fallback_format_tool_result ( " listar_agendamentos_revisao " , tool_result )
if not has_cancel_intent and not has_reschedule_intent and draft is None :
return None
if draft is None :
action = " reschedule " if has_reschedule_intent else " cancel "
draft = {
" action " : action ,
" payload " : { } ,
" expires_at " : datetime . utcnow ( ) + timedelta ( minutes = PENDING_REVIEW_DRAFT_TTL_MINUTES ) ,
}
else :
if has_reschedule_intent :
draft [ " action " ] = " reschedule "
elif has_cancel_intent :
draft [ " action " ] = " cancel "
extracted = self . _normalize_review_management_fields ( extracted_fields )
if " protocolo " not in extracted :
inferred_protocol = self . _extract_review_protocol_from_text ( message )
if inferred_protocol :
extracted [ " protocolo " ] = inferred_protocol
action = draft . get ( " action " , " cancel " )
if (
action == " cancel "
and " motivo " not in extracted
and draft [ " payload " ] . get ( " protocolo " )
and not has_cancel_intent
) :
free_text = str ( message or " " ) . strip ( )
if free_text and len ( free_text ) > = 4 and not self . _is_affirmative_message ( free_text ) :
extracted [ " motivo " ] = free_text
draft [ " payload " ] . update ( extracted )
draft [ " expires_at " ] = datetime . utcnow ( ) + timedelta ( minutes = PENDING_REVIEW_DRAFT_TTL_MINUTES )
self . PENDING_REVIEW_MANAGEMENT_DRAFTS [ user_id ] = draft
if action == " reschedule " :
missing = [ field for field in ( " protocolo " , " nova_data_hora " ) if field not in draft [ " payload " ] ]
if missing :
return self . _render_missing_review_reschedule_fields_prompt ( missing )
try :
tool_result = await self . registry . execute (
" editar_data_revisao " ,
{
" protocolo " : draft [ " payload " ] [ " protocolo " ] ,
" nova_data_hora " : draft [ " payload " ] [ " nova_data_hora " ] ,
} ,
user_id = user_id ,
)
except HTTPException as exc :
return self . _http_exception_detail ( exc )
finally :
self . PENDING_REVIEW_MANAGEMENT_DRAFTS . pop ( user_id , None )
return self . _fallback_format_tool_result ( " editar_data_revisao " , tool_result )
missing = [ field for field in ( " protocolo " , ) if field not in draft [ " payload " ] ]
if missing :
return self . _render_missing_review_cancel_fields_prompt ( missing )
try :
tool_result = await self . registry . execute (
" cancelar_agendamento_revisao " ,
{
" protocolo " : draft [ " payload " ] [ " protocolo " ] ,
" motivo " : draft [ " payload " ] . get ( " motivo " ) ,
} ,
user_id = user_id ,
)
except HTTPException as exc :
return self . _http_exception_detail ( exc )
finally :
self . PENDING_REVIEW_MANAGEMENT_DRAFTS . pop ( user_id , None )
return self . _fallback_format_tool_result ( " cancelar_agendamento_revisao " , tool_result )
def _render_missing_review_fields_prompt ( self , missing_fields : list [ str ] ) - > str :
labels = {
" placa " : " a placa do veiculo " ,
" data_hora " : " a data e hora desejada para a revisao " ,
" modelo " : " o modelo do veiculo " ,
" ano " : " o ano do veiculo " ,
" km " : " a quilometragem atual (km) " ,
" revisao_previa_concessionaria " : " se ja fez revisao na concessionaria (sim/nao) " ,
}
itens = [ f " - { labels [ field ] } " for field in missing_fields ]
return " Para agendar sua revisao, preciso dos dados abaixo: \n " + " \n " . join ( itens )
def _render_missing_review_cancel_fields_prompt ( self , missing_fields : list [ str ] ) - > str :
labels = {
" protocolo " : " o protocolo da revisao (ex.: REV-20260310-ABC12345) " ,
}
itens = [ f " - { labels [ field ] } " for field in missing_fields ]
return " Para cancelar o agendamento de revisao, preciso dos dados abaixo: \n " + " \n " . join ( itens )
def _render_missing_review_reschedule_fields_prompt ( self , missing_fields : list [ str ] ) - > str :
labels = {
" protocolo " : " o protocolo da revisao (ex.: REV-20260310-ABC12345) " ,
" nova_data_hora " : " a nova data e hora desejada para a revisao " ,
}
itens = [ f " - { labels [ field ] } " for field in missing_fields ]
return " Para remarcar sua revisao, preciso dos dados abaixo: \n " + " \n " . join ( itens )
def _render_review_reuse_question ( self ) - > str :
return (
" Deseja usar os mesmos dados do ultimo veiculo e informar so a data/hora da revisao? "
" (sim/nao) "
)
def _store_last_review_package ( self , user_id : int | None , payload : dict | None ) - > None :
if user_id is None or not isinstance ( payload , dict ) :
return
package = {
" placa " : payload . get ( " placa " ) ,
" modelo " : payload . get ( " modelo " ) ,
" ano " : payload . get ( " ano " ) ,
" km " : payload . get ( " km " ) ,
" revisao_previa_concessionaria " : payload . get ( " revisao_previa_concessionaria " ) ,
}
sanitized = { k : v for k , v in package . items ( ) if v is not None }
required = { " placa " , " modelo " , " ano " , " km " , " revisao_previa_concessionaria " }
if not required . issubset ( sanitized . keys ( ) ) :
return
self . LAST_REVIEW_PACKAGES [ user_id ] = {
" payload " : sanitized ,
" expires_at " : datetime . utcnow ( ) + timedelta ( minutes = LAST_REVIEW_PACKAGE_TTL_MINUTES ) ,
}
def _get_last_review_package ( self , user_id : int | None ) - > dict | None :
if user_id is None :
return None
cached = self . LAST_REVIEW_PACKAGES . get ( user_id )
if not cached :
return None
if cached [ " expires_at " ] < datetime . utcnow ( ) :
self . LAST_REVIEW_PACKAGES . pop ( user_id , None )
return None
payload = cached . get ( " payload " )
return dict ( payload ) if isinstance ( payload , dict ) else None
def _is_valid_cpf ( self , cpf : str ) - > bool :
digits = re . sub ( r " \ D " , " " , cpf or " " )
if len ( digits ) != 11 :
return False
if digits == digits [ 0 ] * 11 :
return False
numbers = [ int ( d ) for d in digits ]
sum_first = sum ( n * w for n , w in zip ( numbers [ : 9 ] , range ( 10 , 1 , - 1 ) ) )
first_digit = 11 - ( sum_first % 11 )
first_digit = 0 if first_digit > = 10 else first_digit
if first_digit != numbers [ 9 ] :
return False
sum_second = sum ( n * w for n , w in zip ( numbers [ : 10 ] , range ( 11 , 1 , - 1 ) ) )
second_digit = 11 - ( sum_second % 11 )
second_digit = 0 if second_digit > = 10 else second_digit
return second_digit == numbers [ 10 ]
def _try_prefill_order_value_from_memory ( self , user_id : int | None , payload : dict ) - > None :
# So preenche quando o usuario ainda nao informou valor explicitamente no fluxo atual.
if user_id is None or payload . get ( " valor_veiculo " ) is not None :
return
context = self . _get_user_context ( user_id )
if not context :
return
memory = context . get ( " generic_memory " , { } )
budget = memory . get ( " orcamento_max " )
if isinstance ( budget , ( int , float ) ) and budget > 0 :
# Reaproveita o orcamento capturado anteriormente como valor base do pedido.
payload [ " valor_veiculo " ] = float ( budget )
def _render_missing_order_fields_prompt ( self , missing_fields : list [ str ] ) - > str :
labels = {
" cpf " : " o CPF do cliente " ,
" valor_veiculo " : " o valor do veiculo (R$) " ,
}
itens = [ f " - { labels [ field ] } " for field in missing_fields ]
return " Para realizar o pedido, preciso dos dados abaixo: \n " + " \n " . join ( itens )
def _render_missing_cancel_order_fields_prompt ( self , missing_fields : list [ str ] ) - > str :
labels = {
" numero_pedido " : " o numero do pedido (ex.: PED-20260305123456-ABC123) " ,
" motivo " : " o motivo do cancelamento " ,
}
itens = [ f " - { labels [ field ] } " for field in missing_fields ]
return " Para cancelar o pedido, preciso dos dados abaixo: \n " + " \n " . join ( itens )
# Em vez de tentar entender tudo de uma vez, o bot mantem um "estado" do que ja sabe e vai perguntando apenas o que falta (os "slots" vazios) ate que a tarefa possa ser completada.
async def _try_collect_and_schedule_review (
self ,
message : str ,
user_id : int | None ,
extracted_fields : dict | None = None ,
intents : dict | None = None ,
) - > str | None :
if user_id is None :
return None
normalized_intents = self . _normalize_intents ( intents )
has_intent = normalized_intents . get ( " review_schedule " , False )
has_management_intent = (
normalized_intents . get ( " review_list " , False )
or normalized_intents . get ( " review_cancel " , False )
or normalized_intents . get ( " review_reschedule " , False )
)
# Nao inicia slot-filling quando a intencao atual nao e de agendamento.
if has_management_intent :
# Se o usuario mudou para gerenciamento de revisao, encerra
# qualquer coleta pendente de novo agendamento.
self . PENDING_REVIEW_DRAFTS . pop ( user_id , None )
self . PENDING_REVIEW_REUSE_CONFIRMATIONS . pop ( user_id , None )
return None
# Reaproveita rascunho anterior do usuario, se ainda estiver valido.
draft = self . PENDING_REVIEW_DRAFTS . get ( user_id )
if draft and draft [ " expires_at " ] < datetime . utcnow ( ) :
self . PENDING_REVIEW_DRAFTS . pop ( user_id , None )
draft = None
extracted = self . _normalize_review_fields ( extracted_fields )
pending_reuse = self . PENDING_REVIEW_REUSE_CONFIRMATIONS . get ( user_id )
if pending_reuse and pending_reuse [ " expires_at " ] < datetime . utcnow ( ) :
self . PENDING_REVIEW_REUSE_CONFIRMATIONS . pop ( user_id , None )
pending_reuse = None
if pending_reuse :
should_reuse = False
if self . _is_negative_message ( message ) :
self . PENDING_REVIEW_REUSE_CONFIRMATIONS . pop ( user_id , None )
pending_reuse = None
elif self . _is_affirmative_message ( message ) or " data_hora " in extracted :
should_reuse = True
else :
return self . _render_review_reuse_question ( )
if should_reuse :
seed_payload = dict ( pending_reuse . get ( " payload " ) or { } )
if draft is None :
draft = {
" payload " : seed_payload ,
" expires_at " : datetime . utcnow ( ) + timedelta ( minutes = PENDING_REVIEW_DRAFT_TTL_MINUTES ) ,
}
else :
for key , value in seed_payload . items ( ) :
draft [ " payload " ] . setdefault ( key , value )
self . PENDING_REVIEW_REUSE_CONFIRMATIONS . pop ( user_id , None )
pending_reuse = None
if " data_hora " not in extracted :
self . PENDING_REVIEW_DRAFTS [ user_id ] = draft
return " Perfeito. Me informe apenas a data e hora desejada para a revisao. "
if has_intent and draft is None and not extracted :
last_package = self . _get_last_review_package ( user_id = user_id )
if last_package :
self . PENDING_REVIEW_REUSE_CONFIRMATIONS [ user_id ] = {
" payload " : last_package ,
" expires_at " : datetime . utcnow ( ) + timedelta ( minutes = PENDING_REVIEW_DRAFT_TTL_MINUTES ) ,
}
return self . _render_review_reuse_question ( )
# Se houver rascunho de revisao, mas o usuario mudou para outra
# intencao operacional (ex.: compra/estoque), descarta o rascunho.
if (
draft
and not has_intent
and (
normalized_intents . get ( " order_create " , False )
or normalized_intents . get ( " order_cancel " , False )
)
and not extracted
) :
self . PENDING_REVIEW_DRAFTS . pop ( user_id , None )
return None
# Sem intencao de revisao e sem rascunho aberto: nao interfere no fluxo normal.
if not has_intent and draft is None :
return None
if draft is None :
draft = { " payload " : { } , " expires_at " : datetime . utcnow ( ) + timedelta ( minutes = PENDING_REVIEW_DRAFT_TTL_MINUTES ) }
# Merge incremental: apenas atualiza os campos detectados na mensagem atual.
draft [ " payload " ] . update ( extracted )
self . _try_prefill_review_fields_from_memory ( user_id = user_id , payload = draft [ " payload " ] )
# Se o usuario responder apenas "sim/nao" no follow-up, preenche o slot booleano.
if (
" revisao_previa_concessionaria " not in draft [ " payload " ]
and draft [ " payload " ]
and not extracted
) :
if self . _is_affirmative_message ( message ) :
draft [ " payload " ] [ " revisao_previa_concessionaria " ] = True
elif self . _is_negative_message ( message ) :
draft [ " payload " ] [ " revisao_previa_concessionaria " ] = False
draft [ " expires_at " ] = datetime . utcnow ( ) + timedelta ( minutes = PENDING_REVIEW_DRAFT_TTL_MINUTES )
self . PENDING_REVIEW_DRAFTS [ user_id ] = draft
# Enquanto faltar campo obrigatorio, responde de forma deterministica
# (sem depender do LLM para lembrar contexto).
missing = [ field for field in REVIEW_REQUIRED_FIELDS if field not in draft [ " payload " ] ]
if missing :
return self . _render_missing_review_fields_prompt ( missing )
try :
# Com payload completo, executa a tool de agendamento.
tool_result = await self . registry . execute (
" agendar_revisao " ,
draft [ " payload " ] ,
user_id = user_id ,
)
except HTTPException as exc :
# Se houver conflito com sugestao de horario, salva para confirmar com "pode/sim".
self . _capture_review_confirmation_suggestion (
tool_name = " agendar_revisao " ,
arguments = draft [ " payload " ] ,
exc = exc ,
user_id = user_id ,
)
return self . _http_exception_detail ( exc )
finally :
# Limpa o rascunho apos tentativa final para evitar estado sujo.
self . PENDING_REVIEW_DRAFTS . pop ( user_id , None )
self . _store_last_review_package ( user_id = user_id , payload = draft [ " payload " ] )
return self . _fallback_format_tool_result ( " agendar_revisao " , tool_result )
async def _try_collect_and_create_order (
self ,
message : str ,
user_id : int | None ,
extracted_fields : dict | None = None ,
intents : dict | None = None ,
) - > str | None :
if user_id is None :
return None
normalized_intents = self . _normalize_intents ( intents )
draft = self . PENDING_ORDER_DRAFTS . get ( user_id )
if draft and draft [ " expires_at " ] < datetime . utcnow ( ) :
self . PENDING_ORDER_DRAFTS . pop ( user_id , None )
draft = None
extracted = self . _normalize_order_fields ( extracted_fields )
has_intent = normalized_intents . get ( " order_create " , False )
if (
draft
and not has_intent
and (
normalized_intents . get ( " review_schedule " , False )
or normalized_intents . get ( " review_list " , False )
or normalized_intents . get ( " review_cancel " , False )
or normalized_intents . get ( " review_reschedule " , False )
or normalized_intents . get ( " order_cancel " , False )
)
and not extracted
) :
self . PENDING_ORDER_DRAFTS . pop ( user_id , None )
return None
if not has_intent and draft is None :
return None
if draft is None :
draft = {
" payload " : { } ,
" expires_at " : datetime . utcnow ( ) + timedelta ( minutes = PENDING_ORDER_DRAFT_TTL_MINUTES ) ,
}
draft [ " payload " ] . update ( extracted )
self . _try_prefill_order_value_from_memory ( user_id = user_id , payload = draft [ " payload " ] )
cpf_value = draft [ " payload " ] . get ( " cpf " )
if cpf_value and not self . _is_valid_cpf ( str ( cpf_value ) ) :
draft [ " payload " ] . pop ( " cpf " , None )
self . PENDING_ORDER_DRAFTS [ user_id ] = draft
return " Para seguir com o pedido, preciso de um CPF valido com 11 digitos. "
valor = draft [ " payload " ] . get ( " valor_veiculo " )
if valor is not None :
try :
parsed = float ( valor )
if parsed < = 0 :
draft [ " payload " ] . pop ( " valor_veiculo " , None )
else :
draft [ " payload " ] [ " valor_veiculo " ] = round ( parsed , 2 )
except ( TypeError , ValueError ) :
draft [ " payload " ] . pop ( " valor_veiculo " , None )
draft [ " expires_at " ] = datetime . utcnow ( ) + timedelta ( minutes = PENDING_ORDER_DRAFT_TTL_MINUTES )
self . PENDING_ORDER_DRAFTS [ user_id ] = draft
missing = [ field for field in ORDER_REQUIRED_FIELDS if field not in draft [ " payload " ] ]
if missing :
return self . _render_missing_order_fields_prompt ( missing )
try :
tool_result = await self . registry . execute (
" realizar_pedido " ,
draft [ " payload " ] ,
user_id = user_id ,
)
except HTTPException as exc :
return self . _http_exception_detail ( exc )
finally :
self . PENDING_ORDER_DRAFTS . pop ( user_id , None )
return self . _fallback_format_tool_result ( " realizar_pedido " , tool_result )
async def _try_collect_and_cancel_order (
self ,
message : str ,
user_id : int | None ,
extracted_fields : dict | None = None ,
intents : dict | None = None ,
) - > str | None :
if user_id is None :
return None
normalized_intents = self . _normalize_intents ( intents )
draft = self . PENDING_CANCEL_ORDER_DRAFTS . get ( user_id )
if draft and draft [ " expires_at " ] < datetime . utcnow ( ) :
self . PENDING_CANCEL_ORDER_DRAFTS . pop ( user_id , None )
draft = None
extracted = self . _normalize_cancel_order_fields ( extracted_fields )
has_intent = normalized_intents . get ( " order_cancel " , False )
if (
draft
and not has_intent
and (
normalized_intents . get ( " review_schedule " , False )
or normalized_intents . get ( " review_list " , False )
or normalized_intents . get ( " review_cancel " , False )
or normalized_intents . get ( " review_reschedule " , False )
or normalized_intents . get ( " order_create " , False )
)
and not extracted
) :
self . PENDING_CANCEL_ORDER_DRAFTS . pop ( user_id , None )
return None
if not has_intent and draft is None :
return None
if draft is None :
draft = {
" payload " : { } ,
" expires_at " : datetime . utcnow ( ) + timedelta ( minutes = PENDING_CANCEL_ORDER_DRAFT_TTL_MINUTES ) ,
}
if (
" motivo " not in extracted
and draft [ " payload " ] . get ( " numero_pedido " )
and not has_intent
) :
free_text = ( message or " " ) . strip ( )
if free_text and len ( free_text ) > = 4 :
extracted [ " motivo " ] = free_text
draft [ " payload " ] . update ( extracted )
draft [ " expires_at " ] = datetime . utcnow ( ) + timedelta ( minutes = PENDING_CANCEL_ORDER_DRAFT_TTL_MINUTES )
self . PENDING_CANCEL_ORDER_DRAFTS [ user_id ] = draft
missing = [ field for field in CANCEL_ORDER_REQUIRED_FIELDS if field not in draft [ " payload " ] ]
if missing :
return self . _render_missing_cancel_order_fields_prompt ( missing )
try :
tool_result = await self . registry . execute (
" cancelar_pedido " ,
draft [ " payload " ] ,
user_id = user_id ,
)
except HTTPException as exc :
return self . _http_exception_detail ( exc )
finally :
self . PENDING_CANCEL_ORDER_DRAFTS . pop ( user_id , None )
return self . _fallback_format_tool_result ( " cancelar_pedido " , tool_result )
def _is_affirmative_message ( self , text : str ) - > bool :
normalized = self . _normalize_text ( text ) . strip ( )
normalized = re . sub ( r " [.!?,;:]+$ " , " " , normalized )
@ -1723,10 +1177,10 @@ class OrquestradorService:
if not payload . get ( " placa " ) :
return
payload [ " data_hora " ] = suggested_iso
self . PENDING_REVIEW_CONFIRMATIONS[ user_id ] = {
self . state. set_entry ( " pending_review_confirmations " , user_id , {
" payload " : payload ,
" expires_at " : datetime . utcnow ( ) + timedelta ( minutes = PENDING_REVIEW_TTL_MINUTES ) ,
}
} )
async def _try_confirm_pending_review (
self ,
@ -1736,7 +1190,7 @@ class OrquestradorService:
) - > str | None :
if user_id is None :
return None
pending = self . PENDING_REVIEW_CONFIRMATIONS. get ( user_id )
pending = self . state. get_entry ( " pending_review_confirmations " , user_id , expire = True )
if not pending :
return None
@ -1749,7 +1203,7 @@ class OrquestradorService:
if not new_data_hora and time_only :
new_data_hora = self . _merge_date_with_time ( pending [ " payload " ] . get ( " data_hora " , " " ) , time_only )
if not new_data_hora :
self . PENDING_REVIEW_CONFIRMATIONS. pop ( user_id , None )
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 " ] )
@ -1761,7 +1215,7 @@ class OrquestradorService:
user_id = user_id ,
)
except HTTPException as exc :
self . PENDING_REVIEW_CONFIRMATIONS. pop ( user_id , None )
self . state. pop_entry ( " pending_review_confirmations " , user_id )
self . _capture_review_confirmation_suggestion (
tool_name = " agendar_revisao " ,
arguments = payload ,
@ -1770,16 +1224,12 @@ class OrquestradorService:
)
return self . _http_exception_detail ( exc )
self . PENDING_REVIEW_CONFIRMATIONS. pop ( user_id , None )
self . state. pop_entry ( " pending_review_confirmations " , user_id )
self . _store_last_review_package ( user_id = user_id , payload = payload )
return self . _fallback_format_tool_result ( " agendar_revisao " , tool_result )
if not self . _is_affirmative_message ( message ) :
return None
if pending [ " expires_at " ] < datetime . utcnow ( ) :
self . PENDING_REVIEW_CONFIRMATIONS . pop ( user_id , None )
return None
try :
tool_result = await self . registry . execute (
" agendar_revisao " ,
@ -1787,35 +1237,27 @@ class OrquestradorService:
user_id = user_id ,
)
except HTTPException as exc :
self . PENDING_REVIEW_CONFIRMATIONS. pop ( user_id , None )
self . state. pop_entry ( " pending_review_confirmations " , user_id )
return self . _http_exception_detail ( exc )
self . PENDING_REVIEW_CONFIRMATIONS. pop ( user_id , None )
self . state. pop_entry ( " pending_review_confirmations " , user_id )
self . _store_last_review_package ( user_id = user_id , payload = pending . get ( " payload " ) )
return self . _fallback_format_tool_result ( " agendar_revisao " , tool_result )
def _build_router_prompt ( self , user_message : str , user_id : int | None ) - > str :
user_context = f " Contexto de usuario autenticado: user_id= { user_id } . \n " if user_id else " "
conversation_context = self . _build_context_summary ( user_id = user_id )
return (
" Voce e um assistente de concessionaria. "
" Sempre que a solicitacao depender de dados operacionais (estoque, validacao de cliente, "
" avaliacao de troca, agendamento de revisao, realizacao ou cancelamento de pedido), use a tool correta. "
" Se faltar parametro obrigatorio para a tool, responda em texto pedindo apenas o que falta. \n \n "
f " { user_context } "
f " { conversation_context } \n "
f " Mensagem do usuario: { user_message } "
return build_router_prompt (
user_message = user_message ,
user_id = user_id ,
conversation_context = conversation_context ,
)
def _build_force_tool_prompt ( self , user_message : str , user_id : int | None ) - > str :
user_context = f " Contexto de usuario autenticado: user_id= { user_id } . \n " if user_id else " "
conversation_context = self . _build_context_summary ( user_id = user_id )
return (
" Reavalie a mensagem e priorize chamar tool se houver intencao operacional. "
" Use texto apenas quando faltar dado obrigatorio. \n \n "
f " { user_context } "
f " { conversation_context } \n "
f " Mensagem do usuario: { user_message } "
return build_force_tool_prompt (
user_message = user_message ,
user_id = user_id ,
conversation_context = conversation_context ,
)
def _build_result_prompt (
@ -1825,17 +1267,13 @@ class OrquestradorService:
tool_name : str ,
tool_result ,
) - > str :
user_context = f " Contexto de usuario autenticado: user_id= { user_id } . \n " if user_id else " "
conversation_context = self . _build_context_summary ( user_id = user_id )
return (
" Responda ao usuario de forma objetiva usando o resultado da tool abaixo. "
" Nao invente dados. Se a lista vier vazia, diga explicitamente que nao encontrou resultados. "
" Retorne texto puro sem markdown, sem asteriscos, sem emojis e com linhas curtas. \n \n "
f " { user_context } "
f " { conversation_context } \n "
f " Pergunta original: { user_message } \n "
f " Tool executada: { tool_name } \n "
f " Resultado da tool: { tool_result } "
return build_result_prompt (
user_message = user_message ,
user_id = user_id ,
tool_name = tool_name ,
tool_result = tool_result ,
conversation_context = conversation_context ,
)
def _http_exception_detail ( self , exc : HTTPException ) - > str :
@ -1844,134 +1282,5 @@ class OrquestradorService:
return detail
return " Nao foi possivel concluir a operacao solicitada. "
def _format_datetime_for_chat ( self , value : str ) - > str :
try :
dt = datetime . fromisoformat ( ( value or " " ) . replace ( " Z " , " +00:00 " ) )
return dt . strftime ( " %d / % m/ % Y % H: % M " )
except Exception :
return value or " N/A "
def _format_currency_br ( self , value ) - > str :
try :
number = float ( value )
formatted = f " { number : ,.2f } " . replace ( " , " , " X " ) . replace ( " . " , " , " ) . replace ( " X " , " . " )
return f " R$ { formatted } "
except Exception :
return " N/A "
def _fallback_format_tool_result ( self , tool_name : str , tool_result ) - > str :
if tool_name == " consultar_estoque " and isinstance ( tool_result , list ) :
if not tool_result :
return " Nao encontrei nenhum veiculo com os criterios informados. "
linhas = [ f " Encontrei { len ( tool_result ) } veiculo(s): " ]
for idx , item in enumerate ( tool_result [ : 10 ] , start = 1 ) :
modelo = item . get ( " modelo " , " N/A " )
categoria = item . get ( " categoria " , " N/A " )
preco = self . _format_currency_br ( item . get ( " preco " ) )
linhas . append ( f " { idx } . { modelo } ( { categoria } ) - { preco } " )
restantes = len ( tool_result ) - 10
if restantes > 0 :
linhas . append ( f " ... e mais { restantes } veiculo(s). " )
return " \n " . join ( linhas )
if tool_name == " cancelar_pedido " and isinstance ( tool_result , dict ) :
numero = tool_result . get ( " numero_pedido " , " N/A " )
status = tool_result . get ( " status " , " N/A " )
motivo = tool_result . get ( " motivo " )
linhas = [ f " Pedido { numero } atualizado. " , f " Status: { status } " ]
if motivo :
linhas . append ( f " Motivo: { motivo } " )
return " \n " . join ( linhas )
if tool_name == " realizar_pedido " and isinstance ( tool_result , dict ) :
numero = tool_result . get ( " numero_pedido " , " N/A " )
valor = self . _format_currency_br ( tool_result . get ( " valor_veiculo " ) )
return f " Pedido criado com sucesso. \n Numero: { numero } \n Valor: { valor } "
if tool_name == " agendar_revisao " and isinstance ( tool_result , dict ) :
placa = tool_result . get ( " placa " , " N/A " )
data_hora = self . _format_datetime_for_chat ( tool_result . get ( " data_hora " , " N/A " ) )
protocolo = tool_result . get ( " protocolo " , " N/A " )
valor = tool_result . get ( " valor_revisao " )
if isinstance ( valor , ( int , float ) ) :
return (
" Revisao agendada com sucesso. \n "
f " Protocolo: { protocolo } \n "
f " Placa: { placa } \n "
f " Data/Hora: { data_hora } \n "
f " Valor estimado: { self . _format_currency_br ( valor ) } "
)
return (
" Revisao agendada com sucesso. \n "
f " Protocolo: { protocolo } \n "
f " Placa: { placa } \n "
f " Data/Hora: { data_hora } "
)
if tool_name == " listar_agendamentos_revisao " and isinstance ( tool_result , list ) :
if not tool_result :
return " Nao encontrei agendamentos de revisao para sua conta. "
linhas = [ f " Voce tem { len ( tool_result ) } agendamento(s): " ]
for idx , item in enumerate ( tool_result [ : 12 ] , start = 1 ) :
protocolo = item . get ( " protocolo " , " N/A " )
placa = item . get ( " placa " , " N/A " )
data_hora = self . _format_datetime_for_chat ( item . get ( " data_hora " , " N/A " ) )
status = item . get ( " status " , " N/A " )
linhas . append ( f " { idx } ) Protocolo: { protocolo } " )
linhas . append ( f " Placa: { placa } " )
linhas . append ( f " Data/Hora: { data_hora } | Status: { status } " )
if idx < min ( len ( tool_result ) , 12 ) :
linhas . append ( " " )
restantes = len ( tool_result ) - 12
if restantes > 0 :
if linhas and linhas [ - 1 ] != " " :
linhas . append ( " " )
linhas . append ( f " ... e mais { restantes } agendamento(s). " )
return " \n " . join ( linhas )
if tool_name == " cancelar_agendamento_revisao " and isinstance ( tool_result , dict ) :
protocolo = tool_result . get ( " protocolo " , " N/A " )
status = tool_result . get ( " status " , " N/A " )
placa = tool_result . get ( " placa " , " N/A " )
data_hora = self . _format_datetime_for_chat ( tool_result . get ( " data_hora " , " N/A " ) )
return (
" Agendamento atualizado. \n "
f " Protocolo: { protocolo } \n "
f " Placa: { placa } \n "
f " Data/Hora: { data_hora } \n "
f " Status: { status } "
)
if tool_name == " editar_data_revisao " and isinstance ( tool_result , dict ) :
protocolo = tool_result . get ( " protocolo " , " N/A " )
placa = tool_result . get ( " placa " , " N/A " )
data_hora = self . _format_datetime_for_chat ( tool_result . get ( " data_hora " , " N/A " ) )
status = tool_result . get ( " status " , " N/A " )
return (
" Agendamento remarcado com sucesso. \n "
f " Protocolo: { protocolo } \n "
f " Placa: { placa } \n "
f " Nova data/hora: { data_hora } \n "
f " Status: { status } "
)
if tool_name == " validar_cliente_venda " and isinstance ( tool_result , dict ) :
aprovado = tool_result . get ( " aprovado " )
limite = self . _format_currency_br ( tool_result . get ( " limite_credito " ) )
score = tool_result . get ( " score " , " N/A " )
cpf = tool_result . get ( " cpf " , " N/A " )
if aprovado :
return (
" Cliente aprovado para financiamento. \n "
f " CPF: { cpf } \n "
f " Score: { score } \n "
f " Limite: { limite } "
)
return (
" Cliente nao aprovado para financiamento. \n "
f " CPF: { cpf } \n "
f " Score: { score } \n "
f " Limite: { limite } "
)
return " Operacao concluida com sucesso. "
return fallback_format_tool_result ( tool_name = tool_name , tool_result = tool_result )