Skip to content

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 API
  • whatsapp_evolution_adapter.py — Evolution API
  • webchat_adapter.py — Webchat HTTP
  • telegram_adapter.py — Telegram Bot API
  • slack_adapter.py — Slack
  • sse_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