@ -181,6 +181,16 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
if queued_followup :
return queued_followup
deterministic_rental_management = await self . _try_handle_deterministic_rental_management (
message = message ,
user_id = user_id ,
queue_notice = None ,
finish = finish ,
)
if deterministic_rental_management :
return deterministic_rental_management
message_plan = await self . _extract_message_plan_with_llm (
message = message ,
user_id = user_id ,
@ -741,6 +751,285 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
return await finish ( response )
return None
def _clean_extracted_rental_value ( self , value : str | None ) - > str | None :
text = str ( value or " " ) . strip ( " \t \r \n .;, " )
if not text :
return None
if re . fullmatch ( r " <[^>]*> " , text ) :
text = text [ 1 : - 1 ] . strip ( " \t \r \n .;, " )
if not text :
return None
normalized = self . _normalize_text ( text ) . strip ( )
if normalized in {
" n/a " ,
" na " ,
" nao informado " ,
" nao informada " ,
" nao identificado " ,
" nao identificada " ,
" desconhecido " ,
" desconhecida " ,
" sem informacao " ,
" null " ,
" none " ,
" ... " ,
} :
return None
return text
def _extract_rental_labeled_value ( self , text : str , labels : tuple [ str , . . . ] ) - > str | None :
if not labels :
return None
label_pattern = " | " . join ( re . escape ( label ) for label in labels )
match = re . search (
rf " (?:^|[ \ s; \ n])(?: { label_pattern } ) \ s*[:=]? \ s*(?P<value>[^; \ n]+) " ,
str ( text or " " ) ,
flags = re . IGNORECASE ,
)
if not match :
return None
return self . _clean_extracted_rental_value ( match . group ( " value " ) )
def _extract_rental_contract_number_from_text ( self , text : str ) - > str | None :
match = re . search ( r " \ bLOC-[A-Z0-9-]+ \ b " , str ( text or " " ) , flags = re . IGNORECASE )
if match :
return str ( match . group ( 0 ) ) . strip ( ) . upper ( )
labeled_value = self . _extract_rental_labeled_value ( text , ( " contrato_numero " , " contrato " ) )
if not labeled_value :
return None
labeled_match = re . search ( r " \ bLOC-[A-Z0-9-]+ \ b " , labeled_value , flags = re . IGNORECASE )
if labeled_match :
return str ( labeled_match . group ( 0 ) ) . strip ( ) . upper ( )
return None
def _extract_rental_plate_from_text ( self , text : str ) - > str | None :
labeled_value = self . _extract_rental_labeled_value ( text , ( " placa " , ) )
if labeled_value :
labeled_plate = self . _normalize_plate ( labeled_value )
if labeled_plate :
return labeled_plate
extracted : dict = { }
self . _try_capture_rental_fields_from_message ( message = text , payload = extracted )
return self . _normalize_plate ( extracted . get ( " placa " ) )
def _merge_last_rental_reference ( self , user_id : int | None , arguments : dict ) - > dict :
if not isinstance ( arguments , dict ) :
return { }
last_contract = self . _get_last_rental_contract ( user_id )
if not isinstance ( last_contract , dict ) :
return arguments
if not arguments . get ( " contrato_numero " ) and last_contract . get ( " contrato_numero " ) :
arguments [ " contrato_numero " ] = str ( last_contract [ " contrato_numero " ] )
if not arguments . get ( " placa " ) and last_contract . get ( " placa " ) :
arguments [ " placa " ] = str ( last_contract [ " placa " ] )
return arguments
def _has_rental_return_management_request ( self , message : str , user_id : int | None = None ) - > bool :
if not self . _has_rental_return_request ( message ) :
return False
normalized_message = self . _normalize_text ( message ) . strip ( )
return bool (
" aluguel " in normalized_message
or " locacao " in normalized_message
or self . _get_last_rental_contract ( user_id )
or self . _extract_rental_contract_number_from_text ( message )
or self . _extract_rental_plate_from_text ( message )
)
def _has_rental_payment_request ( self , message : str , user_id : int | None = None ) - > bool :
normalized_message = self . _normalize_text ( message ) . strip ( )
if " multa " in normalized_message :
return False
payment_terms = ( " pagamento " , " comprovante " , " pix " , " boleto " )
if not any ( term in normalized_message for term in payment_terms ) :
return False
return bool (
" aluguel " in normalized_message
or " locacao " in normalized_message
or self . _get_last_rental_contract ( user_id )
or self . _extract_rental_contract_number_from_text ( message )
)
def _has_rental_fine_request ( self , message : str , user_id : int | None = None ) - > bool :
normalized_message = self . _normalize_text ( message ) . strip ( )
if " multa " not in normalized_message :
return False
return bool (
" aluguel " in normalized_message
or " locacao " in normalized_message
or " auto_infracao " in normalized_message
or self . _get_last_rental_contract ( user_id )
or self . _extract_rental_contract_number_from_text ( message )
or self . _extract_rental_plate_from_text ( message )
)
def _is_deterministic_rental_management_candidate ( self , message : str , user_id : int | None ) - > bool :
has_policy = hasattr ( self , " policy " ) and getattr ( self , " policy " ) is not None
if has_policy and user_id is not None and (
self . _has_open_flow ( user_id , " sales " ) or self . _has_open_flow ( user_id , " review " )
) :
return False
return bool (
self . _has_rental_return_management_request ( message , user_id = user_id )
or self . _has_rental_payment_request ( message , user_id = user_id )
or self . _has_rental_fine_request ( message , user_id = user_id )
)
def _build_rental_return_arguments_from_message ( self , message : str , user_id : int | None ) - > dict :
arguments : dict = { }
contract_number = self . _extract_rental_contract_number_from_text ( message )
if contract_number :
arguments [ " contrato_numero " ] = contract_number
plate = self . _extract_rental_plate_from_text ( message )
if plate :
arguments [ " placa " ] = plate
date_text = self . _extract_rental_labeled_value ( message , ( " data_devolucao " , " data de devolucao " ) )
if not date_text :
datetimes = self . _extract_rental_datetimes_from_text ( message )
if datetimes :
date_text = datetimes [ - 1 ]
if date_text :
arguments [ " data_devolucao " ] = date_text
return self . _merge_last_rental_reference ( user_id = user_id , arguments = arguments )
def _build_rental_payment_arguments_from_message ( self , message : str , user_id : int | None ) - > dict :
arguments : dict = { }
contract_number = self . _extract_rental_contract_number_from_text ( message )
if contract_number :
arguments [ " contrato_numero " ] = contract_number
plate = self . _extract_rental_plate_from_text ( message )
if plate :
arguments [ " placa " ] = plate
amount_text = self . _extract_rental_labeled_value ( message , ( " valor_pago " , " valor " ) )
amount = self . _normalize_positive_number ( amount_text )
if amount is not None :
arguments [ " valor " ] = float ( amount )
payment_date = self . _extract_rental_labeled_value ( message , ( " data_pagamento " , " data do pagamento " ) )
if not payment_date :
datetimes = self . _extract_rental_datetimes_from_text ( message )
if datetimes :
payment_date = datetimes [ 0 ]
if payment_date :
arguments [ " data_pagamento " ] = payment_date
favorecido = self . _extract_rental_labeled_value ( message , ( " favorecido " , ) )
if favorecido :
arguments [ " favorecido " ] = favorecido
receipt_id = self . _extract_rental_labeled_value (
message ,
( " identificador_comprovante " , " identificador " , " nsu " ) ,
)
if receipt_id :
arguments [ " identificador_comprovante " ] = receipt_id
observations = self . _extract_rental_labeled_value ( message , ( " observacoes " , " observacao " ) )
if observations :
arguments [ " observacoes " ] = observations
return self . _merge_last_rental_reference ( user_id = user_id , arguments = arguments )
def _build_rental_fine_arguments_from_message ( self , message : str , user_id : int | None ) - > dict :
arguments : dict = { }
contract_number = self . _extract_rental_contract_number_from_text ( message )
if contract_number :
arguments [ " contrato_numero " ] = contract_number
plate = self . _extract_rental_plate_from_text ( message )
if plate :
arguments [ " placa " ] = plate
notice_number = self . _extract_rental_labeled_value (
message ,
( " auto_infracao " , " auto de infracao " , " auto da infracao " ) ,
)
if notice_number :
arguments [ " auto_infracao " ] = notice_number
issuing_agency = self . _extract_rental_labeled_value (
message ,
( " orgao_emissor " , " orgao emissor " ) ,
)
if issuing_agency :
arguments [ " orgao_emissor " ] = issuing_agency
amount_text = self . _extract_rental_labeled_value ( message , ( " valor " , ) )
amount = self . _normalize_positive_number ( amount_text )
if amount is not None :
arguments [ " valor " ] = float ( amount )
violation_date = self . _extract_rental_labeled_value ( message , ( " data_infracao " , " data da infracao " ) )
due_date = self . _extract_rental_labeled_value ( message , ( " vencimento " , " data_vencimento " , " data de vencimento " ) )
datetimes = self . _extract_rental_datetimes_from_text ( message )
if not violation_date and datetimes :
violation_date = datetimes [ 0 ]
if not due_date and len ( datetimes ) > = 2 :
due_date = datetimes [ 1 ]
if violation_date :
arguments [ " data_infracao " ] = violation_date
if due_date :
arguments [ " vencimento " ] = due_date
observations = self . _extract_rental_labeled_value ( message , ( " observacoes " , " observacao " ) )
if observations :
arguments [ " observacoes " ] = observations
return self . _merge_last_rental_reference ( user_id = user_id , arguments = arguments )
async def _try_handle_deterministic_rental_management (
self ,
message : str ,
user_id : int | None ,
queue_notice : str | None ,
finish ,
) - > str | None :
if user_id is None or not self . _is_deterministic_rental_management_candidate ( message , user_id = user_id ) :
return None
if self . _has_rental_return_management_request ( message , user_id = user_id ) :
tool_name = " registrar_devolucao_aluguel "
arguments = self . _build_rental_return_arguments_from_message ( message = message , user_id = user_id )
missing_response = None
elif self . _has_rental_fine_request ( message , user_id = user_id ) :
tool_name = " registrar_multa_aluguel "
arguments = self . _build_rental_fine_arguments_from_message ( message = message , user_id = user_id )
missing_response = None
if " valor " not in arguments :
missing_response = " Para registrar a multa de aluguel, preciso do valor informado no documento. "
elif self . _has_rental_payment_request ( message , user_id = user_id ) :
tool_name = " registrar_pagamento_aluguel "
arguments = self . _build_rental_payment_arguments_from_message ( message = message , user_id = user_id )
missing_response = None
if " valor " not in arguments :
missing_response = " Para registrar o pagamento do aluguel, preciso do valor informado no comprovante. "
else :
return None
if missing_response :
return await finish ( missing_response , queue_notice = queue_notice )
try :
tool_result = await self . _execute_tool_with_trace (
tool_name ,
arguments ,
user_id = user_id ,
)
except HTTPException as exc :
return await finish ( self . _http_exception_detail ( exc ) , queue_notice = queue_notice )
self . _capture_successful_tool_side_effects (
tool_name = tool_name ,
arguments = arguments ,
tool_result = tool_result ,
user_id = user_id ,
)
return await finish (
self . _fallback_format_tool_result ( tool_name , tool_result ) ,
queue_notice = queue_notice ,
)
async def _try_handle_active_sales_follow_up (
self ,
message : str ,
@ -1271,6 +1560,13 @@ class OrquestradorService(ReviewFlowMixin, OrderFlowMixin, RentalFlowMixin):
) - > None :
if tool_name == " agendar_revisao " and isinstance ( arguments , dict ) :
self . _store_last_review_package ( user_id = user_id , payload = arguments )
if tool_name in {
" abrir_locacao_aluguel " ,
" registrar_devolucao_aluguel " ,
" registrar_pagamento_aluguel " ,
" registrar_multa_aluguel " ,
} and isinstance ( tool_result , dict ) :
self . _store_last_rental_contract ( user_id = user_id , payload = tool_result )
self . _capture_tool_result_context (
tool_name = tool_name ,
tool_result = tool_result ,