@ -29,6 +29,9 @@ TELEGRAM_MESSAGE_SAFE_LIMIT = 3800
TELEGRAM_MAX_CONCURRENT_CHATS = 8
TELEGRAM_MAX_CONCURRENT_CHATS = 8
TELEGRAM_IDEMPOTENCY_BUCKET = " telegram_processed_messages "
TELEGRAM_IDEMPOTENCY_BUCKET = " telegram_processed_messages "
TELEGRAM_IDEMPOTENCY_CACHE_LIMIT = 100
TELEGRAM_IDEMPOTENCY_CACHE_LIMIT = 100
TELEGRAM_RUNTIME_BUCKET = " telegram_runtime_state "
TELEGRAM_RUNTIME_OWNER_ID = 0
TELEGRAM_RUNTIME_CURSOR_TTL_DAYS = 30
def _split_telegram_text ( text : str , limit : int = TELEGRAM_MESSAGE_SAFE_LIMIT ) - > List [ str ] :
def _split_telegram_text ( text : str , limit : int = TELEGRAM_MESSAGE_SAFE_LIMIT ) - > List [ str ] :
@ -244,6 +247,33 @@ class TelegramSatelliteService:
} ,
} ,
)
)
def _get_runtime_state ( self ) - > dict :
entry = self . state . get_entry ( TELEGRAM_RUNTIME_BUCKET , TELEGRAM_RUNTIME_OWNER_ID )
return entry if isinstance ( entry , dict ) else { }
def _persist_last_processed_update_id ( self , update_id : int ) - > None :
if update_id < 0 :
return
entry = self . _get_runtime_state ( )
current_last_update_id = entry . get ( " last_update_id " )
if isinstance ( current_last_update_id , int ) and current_last_update_id > = update_id :
self . _last_update_id = max ( self . _last_update_id , current_last_update_id )
return
now = utc_now ( ) . replace ( microsecond = 0 )
expires_at = now + timedelta ( days = TELEGRAM_RUNTIME_CURSOR_TTL_DAYS )
self . state . set_entry (
TELEGRAM_RUNTIME_BUCKET ,
TELEGRAM_RUNTIME_OWNER_ID ,
{
" last_update_id " : update_id ,
" updated_at " : now ,
" expires_at " : expires_at ,
} ,
)
self . _last_update_id = max ( self . _last_update_id , update_id )
async def _schedule_update_processing (
async def _schedule_update_processing (
self ,
self ,
session : aiohttp . ClientSession ,
session : aiohttp . ClientSession ,
@ -328,9 +358,16 @@ class TelegramSatelliteService:
async def _initialize_offset ( self , session : aiohttp . ClientSession ) - > int | None :
async def _initialize_offset ( self , session : aiohttp . ClientSession ) - > int | None :
"""
"""
Descarta backlog pendente no startup para evitar respostas repetidas apos restart .
Retoma o polling a partir do ultimo update persistido .
Retorna o offset inicial seguro para o loop princip al.
Sem cursor salvo , faz um bootstrap conservador e registra o ponto inici al.
"""
"""
runtime_state = self . _get_runtime_state ( )
last_update_id = runtime_state . get ( " last_update_id " )
if isinstance ( last_update_id , int ) and last_update_id > = 0 :
self . _last_update_id = last_update_id
logger . info ( " Retomando polling do Telegram a partir do update_id persistido %s . " , last_update_id )
return last_update_id + 1
payload : Dict [ str , Any ] = {
payload : Dict [ str , Any ] = {
" timeout " : 0 ,
" timeout " : 0 ,
" limit " : 100 ,
" limit " : 100 ,
@ -350,8 +387,11 @@ class TelegramSatelliteService:
if last_id < 0 :
if last_id < 0 :
return None
return None
self . _last_update_id = last_id
self . _persist_last_processed_update_id ( last_id )
logger . info ( " Startup com backlog descartado: %s update(s) anteriores ignorados. " , len ( updates ) )
logger . info (
" Bootstrap inicial do Telegram sem cursor persistido: %s update(s) anteriores ignorados. " ,
len ( updates ) ,
)
return last_id + 1
return last_id + 1
async def _get_updates (
async def _get_updates (
@ -421,6 +461,9 @@ class TelegramSatelliteService:
answer = " Nao consegui processar sua solicitacao agora. Tente novamente em instantes. "
answer = " Nao consegui processar sua solicitacao agora. Tente novamente em instantes. "
self . _store_processed_update ( update = update , answer = answer )
self . _store_processed_update ( update = update , answer = answer )
update_id = update . get ( " update_id " )
if isinstance ( update_id , int ) :
self . _persist_last_processed_update_id ( update_id )
await self . _send_message ( session = session , chat_id = chat_id , text = answer )
await self . _send_message ( session = session , chat_id = chat_id , text = answer )
async def _send_message (
async def _send_message (
@ -449,6 +492,34 @@ class TelegramSatelliteService:
image_attachments : List [ Dict [ str , Any ] ] | None = None ,
image_attachments : List [ Dict [ str , Any ] ] | None = None ,
) - > str :
) - > str :
""" Encaminha mensagem ao orquestrador com usuario identificado e retorna resposta. """
""" Encaminha mensagem ao orquestrador com usuario identificado e retorna resposta. """
message_text = text
if image_attachments :
image_message = await self . _build_orchestration_message_from_image (
caption = text ,
image_attachments = image_attachments ,
)
if self . _is_image_analysis_failure_message ( image_message ) :
return image_message
message_text = image_message
return await asyncio . to_thread (
self . _run_blocking_orchestration_turn ,
message_text = message_text ,
sender = sender ,
chat_id = chat_id ,
)
def _run_blocking_orchestration_turn (
self ,
* ,
message_text : str ,
sender : Dict [ str , Any ] ,
chat_id : int ,
) - > str :
"""
Executa o turno do orquestrador fora do loop async principal .
Isso isola sessoes SQLAlchemy sincronas e outras operacoes bloqueantes .
"""
tools_db = SessionLocal ( )
tools_db = SessionLocal ( )
mock_db = SessionMockLocal ( )
mock_db = SessionMockLocal ( )
try :
try :
@ -466,18 +537,11 @@ class TelegramSatelliteService:
username = username ,
username = username ,
)
)
message_text = text
service = OrquestradorService (
if image_attachments :
tools_db ,
image_message = await self . _build_orchestration_message_from_image (
state_repository = self . state ,
caption = text ,
image_attachments = image_attachments ,
)
)
if self . _is_image_analysis_failure_message ( image_message ) :
return asyncio . run ( service . handle_message ( message = message_text , user_id = user . id ) )
return image_message
message_text = image_message
service = OrquestradorService ( tools_db )
return await service . handle_message ( message = message_text , user_id = user . id )
finally :
finally :
tools_db . close ( )
tools_db . close ( )
mock_db . close ( )
mock_db . close ( )