@ -2,20 +2,24 @@ import asyncio
import logging
import logging
import os
import os
import tempfile
import tempfile
from datetime import timedelta
from typing import Any , Dict , List
from typing import Any , Dict , List
import aiohttp
import aiohttp
from fastapi import HTTPException
from fastapi import HTTPException
from app . core . settings import settings
from app . core . settings import settings
from app . core . time_utils import utc_now
from app . db . database import SessionLocal
from app . db . database import SessionLocal
from app . db . mock_database import SessionMockLocal
from app . db . mock_database import SessionMockLocal
from app . services . ai . llm_service import (
from app . services . ai . llm_service import (
IMAGE_ANALYSIS_BLOCKING_PREFIXES ,
IMAGE_ANALYSIS_BLOCKING_PREFIXES ,
LLMService ,
LLMService ,
)
)
from app . services . orchestration . conversation_state_repository import ConversationStateRepository
from app . services . orchestration . orquestrador_service import OrquestradorService
from app . services . orchestration . orquestrador_service import OrquestradorService
from app . services . orchestration . sensitive_data import mask_sensitive_payload
from app . services . orchestration . sensitive_data import mask_sensitive_payload
from app . services . orchestration . state_repository_factory import get_conversation_state_repository
from app . services . user . user_service import UserService
from app . services . user . user_service import UserService
@ -23,6 +27,8 @@ logger = logging.getLogger(__name__)
TELEGRAM_MESSAGE_SAFE_LIMIT = 3800
TELEGRAM_MESSAGE_SAFE_LIMIT = 3800
TELEGRAM_MAX_CONCURRENT_CHATS = 8
TELEGRAM_MAX_CONCURRENT_CHATS = 8
TELEGRAM_IDEMPOTENCY_BUCKET = " telegram_processed_messages "
TELEGRAM_IDEMPOTENCY_CACHE_LIMIT = 100
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 ] :
@ -125,12 +131,17 @@ class TelegramSatelliteService:
Processa mensagens direto no OrquestradorService e publica respostas no chat .
Processa mensagens direto no OrquestradorService e publica respostas no chat .
"""
"""
def __init__ ( self , token : str ) :
def __init__ (
self ,
token : str ,
state_repository : ConversationStateRepository | None = None ,
) :
""" Configura cliente Telegram com URL base e timeouts padrao. """
""" Configura cliente Telegram com URL base e timeouts padrao. """
self . base_url = f " https://api.telegram.org/bot { token } "
self . base_url = f " https://api.telegram.org/bot { token } "
self . file_base_url = f " https://api.telegram.org/file/bot { token } "
self . file_base_url = f " https://api.telegram.org/file/bot { token } "
self . polling_timeout = settings . telegram_polling_timeout
self . polling_timeout = settings . telegram_polling_timeout
self . request_timeout = settings . telegram_request_timeout
self . request_timeout = settings . telegram_request_timeout
self . state = state_repository or get_conversation_state_repository ( )
self . _last_update_id = - 1
self . _last_update_id = - 1
self . _chat_queues : dict [ int , asyncio . Queue [ Dict [ str , Any ] ] ] = { }
self . _chat_queues : dict [ int , asyncio . Queue [ Dict [ str , Any ] ] ] = { }
self . _chat_workers : dict [ int , asyncio . Task [ None ] ] = { }
self . _chat_workers : dict [ int , asyncio . Task [ None ] ] = { }
@ -167,6 +178,72 @@ class TelegramSatelliteService:
chat_id = chat . get ( " id " )
chat_id = chat . get ( " id " )
return chat_id if isinstance ( chat_id , int ) else None
return chat_id if isinstance ( chat_id , int ) else None
def _build_update_idempotency_key ( self , update : Dict [ str , Any ] ) - > str | None :
chat_id = self . _extract_chat_id ( update )
message = update . get ( " message " , { } )
message_id = message . get ( " message_id " )
if isinstance ( chat_id , int ) and isinstance ( message_id , int ) :
return f " telegram:message: { chat_id } : { message_id } "
update_id = update . get ( " update_id " )
if isinstance ( update_id , int ) :
return f " telegram:update: { update_id } "
return None
def _idempotency_owner_id ( self , update : Dict [ str , Any ] ) - > int | None :
chat_id = self . _extract_chat_id ( update )
if isinstance ( chat_id , int ) :
return chat_id
update_id = update . get ( " update_id " )
return update_id if isinstance ( update_id , int ) else None
def _get_processed_update ( self , update : Dict [ str , Any ] ) - > dict | None :
owner_id = self . _idempotency_owner_id ( update )
idempotency_key = self . _build_update_idempotency_key ( update )
if owner_id is None or not idempotency_key :
return None
entry = self . state . get_entry ( TELEGRAM_IDEMPOTENCY_BUCKET , owner_id , expire = True )
if not isinstance ( entry , dict ) :
return None
items = entry . get ( " items " )
if not isinstance ( items , dict ) :
return None
payload = items . get ( idempotency_key )
return payload if isinstance ( payload , dict ) else None
def _store_processed_update ( self , update : Dict [ str , Any ] , answer : str ) - > None :
owner_id = self . _idempotency_owner_id ( update )
idempotency_key = self . _build_update_idempotency_key ( update )
if owner_id is None or not idempotency_key :
return
now = utc_now ( ) . replace ( microsecond = 0 )
expires_at = now + timedelta ( minutes = settings . conversation_state_ttl_minutes )
entry = self . state . get_entry ( TELEGRAM_IDEMPOTENCY_BUCKET , owner_id , expire = True ) or { }
items = dict ( entry . get ( " items " ) or { } )
items [ idempotency_key ] = {
" answer " : str ( answer or " " ) ,
" processed_at " : now ,
}
if len ( items ) > TELEGRAM_IDEMPOTENCY_CACHE_LIMIT :
ordered = sorted (
items . items ( ) ,
key = lambda item : item [ 1 ] . get ( " processed_at " ) or now ,
reverse = True ,
)
items = dict ( ordered [ : TELEGRAM_IDEMPOTENCY_CACHE_LIMIT ] )
self . state . set_entry (
TELEGRAM_IDEMPOTENCY_BUCKET ,
owner_id ,
{
" items " : items ,
" expires_at " : expires_at ,
} ,
)
async def _schedule_update_processing (
async def _schedule_update_processing (
self ,
self ,
session : aiohttp . ClientSession ,
session : aiohttp . ClientSession ,
@ -310,9 +387,23 @@ class TelegramSatelliteService:
chat_id = chat . get ( " id " )
chat_id = chat . get ( " id " )
sender = message . get ( " from " , { } )
sender = message . get ( " from " , { } )
if not chat_id :
return
cached_update = self . _get_processed_update ( update )
if cached_update :
cached_answer = str ( cached_update . get ( " answer " ) or " " ) . strip ( )
if cached_answer :
logger . info (
" Reutilizando resposta em reentrega do Telegram. chat_id= %s update_key= %s " ,
chat_id ,
self . _build_update_idempotency_key ( update ) ,
)
await self . _send_message ( session = session , chat_id = chat_id , text = cached_answer )
return
image_attachments = await self . _extract_image_attachments ( session = session , message = message )
image_attachments = await self . _extract_image_attachments ( session = session , message = message )
if ( not text and not image_attachments ) or not chat_id :
if not text and not image_attachments :
return
return
try :
try :
@ -329,6 +420,7 @@ class TelegramSatelliteService:
logger . exception ( " Erro ao processar mensagem do Telegram. " )
logger . exception ( " Erro ao processar mensagem do Telegram. " )
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 )
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 (