Fluxo de Mensagens¶
Arquitetura de Messaging¶
flowchart TD
APP["unified_bot.py<br/>(FastAPI app)"]
APP --> WA["WhatsApp Adapter"]
APP --> WC["Webchat Adapter"]
APP --> TG["Telegram Adapter"]
WA --> AF["Adapter Factory<br/>(runtime/messaging/factory)"]
WC --> AF
TG --> AF
AF --> CP["ConversationProcessor<br/>(runtime/messaging/processor)"]
CP --> AT["Audio Transcriber<br/>(audio/transcriber.py)"]
AT --> LG["LoopGuard<br/>(anti-loop bot)"]
LG --> SS["Session Service<br/>+ Memory Service"]
SS --> RN["ADK Runner<br/>(root_agent)"]
Message Types¶
O enum MessageType define os tipos de mensagem suportados:
| Valor | Enum | Descrição |
|---|---|---|
"text" |
MessageType.TEXT |
Mensagem de texto simples |
"image" |
MessageType.IMAGE |
Mensagem com imagem |
"file" |
MessageType.FILE |
Arquivo/documento (PDF, DOCX, CSV, etc.) |
"audio" |
MessageType.AUDIO |
Mensagem de áudio |
"video" |
MessageType.VIDEO |
Mensagem de vídeo |
IncomingMessage¶
@dataclass
class IncomingMessage:
platform: str
user_id: str
channel_id: str
thread_id: str
text: str
message_type: MessageType = MessageType.TEXT
is_direct_message: bool = False
metadata: Dict[str, Any] = None
Metadata por tipo de mensagem¶
Áudio (MessageType.AUDIO):
- audio_data: bytes — Áudio bruto em bytes
- audio_mime_type: str — MIME type (ex: "audio/ogg", "audio/mp4")
Arquivo (MessageType.FILE):
- file_data: bytes — Arquivo bruto em bytes
- file_mime_type: str — MIME type (ex: "application/pdf", "text/csv")
- file_name: str — Nome original do arquivo
OutgoingMessage¶
@dataclass
class OutgoingMessage:
text: str
channel_id: str
thread_id: Optional[str] = None
message_type: MessageType = MessageType.TEXT
metadata: Dict[str, Any] = None
MessagingAdapter Interface¶
Cada adapter implementa a interface MessagingAdapter:
class MessagingAdapter(ABC):
def __init__(self, config: Dict[str, Any]):
self.config = config
self._validate_config()
@abstractmethod
def _validate_config(self) -> None: ...
@abstractmethod
async def setup(self) -> None: ...
@abstractmethod
async def parse_webhook_request(self, request: Any) -> Optional[IncomingMessage]: ...
@abstractmethod
async def send_message(self, message: OutgoingMessage) -> bool: ...
@abstractmethod
async def send_typing_indicator(
self, channel_id: str, thread_id: Optional[str] = None
) -> None: ...
@abstractmethod
async def update_message(
self, message_id: str, new_text: str, channel_id: str
) -> bool: ...
@abstractmethod
async def delete_message(
self, message_id: str, channel_id: str
) -> bool: ...
@abstractmethod
def format_text(self, text: str) -> str: ...
@property
@abstractmethod
def platform_name(self) -> str: ...
@property
@abstractmethod
def webhook_path(self) -> str: ...
async def is_bot_message(self, message_data: Any) -> bool:
return False # default — pode ser sobrescrito
async def cleanup(self) -> None:
pass # default — pode ser sobrescrito
Adapters Disponíveis¶
whatsapp_official_adapter.py— WhatsApp Official APIwhatsapp_evolution_adapter.py— Evolution APIwebchat_adapter.py— Webchat HTTPtelegram_adapter.py— Telegram Bot APIslack_adapter.py— Slacksse_adapter.py— Server-Sent Events
Audio Support¶
O fluxo de áudio é processado em duas etapas:
1. Detecção pelo Adapter¶
O adapter detecta que a mensagem recebida contém áudio e preenche:
- message_type = MessageType.AUDIO
- metadata["audio_data"] — bytes do áudio
- metadata["audio_mime_type"] — MIME type (ex: "audio/ogg")
2. Transcrição no Processor¶
No ConversationProcessor.process_message(), etapa 1b (antes de enviar ao ADK):
if message.message_type == MessageType.AUDIO and not message.text:
audio_data = message.metadata.get("audio_data")
audio_mime = message.metadata.get("audio_mime_type", "audio/ogg")
if audio_data:
from .audio.transcriber import transcribe_audio as _transcribe
transcript = await _transcribe(audio_data, audio_mime)
if transcript:
message.text = transcript
Regra: texto > áudio — se a mensagem contém tanto texto quanto áudio, o áudio é ignorado e apenas o texto é usado. A transcrição só acontece quando MessageType.AUDIO E message.text está vazio.
Transcriber (runtime/messaging/audio/transcriber.py)¶
A transcrição usa o modelo Gemini 2.5 Flash (configurável via env AUDIO_TRANSCRIPTION_MODEL) através do cliente Vertex AI:
client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)
response = client.models.generate_content(
model=MODEL,
contents=[
Part.from_bytes(data=audio_bytes, mime_type=mime_type),
Part(text="Transcreva este áudio para texto. ..."),
],
config=GenerateContentConfig(temperature=0.0),
)
- Tamanho máximo de áudio: 10 MB (env
AUDIO_MAX_BYTES) - Idioma: detecção automática via heurística de caracteres acentuados (pt-BR, en-US, es-ES)
- Projeto GCP: env
GOOGLE_CLOUD_PROJECT(default:ifriend-platform) - Região: env
GOOGLE_CLOUD_LOCATION(default:us-central1)
File/Document Support¶
1. Detecção pelo Adapter¶
O adapter detecta que a mensagem contém um arquivo/documento e preenche:
- message_type = MessageType.FILE
- metadata["file_data"] — bytes do arquivo
- metadata["file_mime_type"] — MIME type (ex: "application/pdf")
- metadata["file_name"] — Nome original do arquivo
2. Processamento no Processor¶
No ConversationProcessor.process_message(), etapa 2:
# Se tem arquivo, adiciona Part.from_bytes()
file_data = message.metadata.get("file_data") if message.message_type == MessageType.FILE else None
if file_data:
file_mime = message.metadata.get("file_mime_type", "application/octet-stream")
content_parts.append(types.Part.from_bytes(data=file_data, mime_type=file_mime))
# Texto do usuário (pode ser prompt sobre o arquivo)
user_text = message.text or ""
if not user_text and file_data:
user_text = "Analise este arquivo."
content_parts.append(types.Part.from_text(text=user_text))
Arquivo + texto: o arquivo pode coexistir com texto — o texto se torna o prompt sobre o arquivo (ex: "Resuma este PDF", "O que está nesta planilha?").
Modelo: Gemini 2.5 Flash processa nativamente PDF, imagens, CSV, DOCX e outros formatos via Part.from_bytes(), sem necessidade de processamento externo.
Loop Guard¶
Proteção contra loops infinitos quando outro agente IA conversa com a iFriend.
4 Camadas de Proteção¶
| Camada | Nome | Trigger | Ação |
|---|---|---|---|
| 1 | Message ID Dedup | message_id duplicado |
Ignora silenciosamente |
| 2 | Circuit Breaker | >5 msgs em 10s | Cooldown de 30s |
| 3 | Repetição Textual | ≥3 msgs idênticas | Trip circuit breaker |
| 4 | Hard Turn Limit | >200 turnos/sessão | Escala para humano |