Fluxo de mensagem¶
Do webhook à resposta¶
O entry point público é webhook_handler.app.lambda_handler, configurado em WebhookHandlerFunction no template.yaml. GET realiza o handshake Meta; POST lê o corpo bruto, valida a assinatura quando aplicável e traduz o envelope por core.integrations.whatsapp.adapter. O resultado interno contém telefone, tipo, conteúdo, instância, horário e ID do provider.
sequenceDiagram
participant WA as WhatsApp
participant WH as Webhook
participant DB as DynamoDB
participant Q as SQS FIFO
participant OR as Orchestrator
participant AI as AgentInvoker / OpenAI
WA->>WH: POST envelope Meta
WH->>WH: Validar assinatura e traduzir todas as mensagens
WH->>DB: Registrar receipts + latest por telefone
WH->>DB: Classificar texto como agent_routed
WH->>Q: phone_number + message_id
WH->>DB: Registrar dispatched_at
WH-->>WA: 202 queued
Q->>OR: Wake-up do telefone
OR->>DB: Ler pending_message_ids em ordem atômica
OR->>DB: Resolver ACTIVE, sessão e histórico
OR->>AI: Prompt + histórico da sessão + packages
AI-->>OR: JSON + mensagem
OR->>DB: Verificar latest, ownership e persistir ação
OR->>WA: Enviar texto permitido
OR->>DB: Marcar receipt completed_at
OR-->>Q: batchItemFailures vazio
O 202 confirma que a mensagem foi encaminhada, não que a OpenAI respondeu ou o WhatsApp entregou o texto. Callbacks Meta que não geram mensagens inbound não passam por esse fluxo de IA; consulte WhatsApp.
Classificação antes da fila¶
_handle_payload() aplica esta precedência, após deduplicação:
| Ordem | Condição | Resultado |
|---|---|---|
| 1 | /encerrar-debug reconhecido |
Invalida turnos pendentes, fecha sessão pelo helper de debug, registra e responde |
| 2 | Remetente na whitelist efetiva | Comandos de operador ou ajuda; pode emitir link de acesso |
| 3 | Sessão HANDOFF_PENDING ou HUMAN |
Append condicional no inbox real; nenhuma IA |
| 4 | Conteúdo não textual enquanto bot controla | Mensagem fixa pedindo texto; não chama IA |
| 5 | Texto controlado pelo bot | mark_agent_routed, SQS FIFO e mark_dispatched |
O claim try_claim_sync() impede que dois handlers concorrentes executem a mesma rota síncrona. Se o append do inbox perde uma corrida de estado e não era duplicata, o webhook libera o claim, recarrega o roteamento uma vez e pode devolver 409 conversation_state_changed. Uma falha no envio da confirmação de fila humana pode devolver 503 human_inbox_ack_failed, permitindo recuperação.
Dados ao longo do caminho¶
| Etapa | Contrato | Identificador preservado |
|---|---|---|
| Meta → adapter | entry[].changes[].value.messages[] |
messages[].id |
| Registro | InboundTurn(phone_number, message_id, content, received_at, instance) |
ID do provider; UUID local se ausente |
| SQS | JSON com phone_number, message_id |
Corpo não contém o texto completo |
| Log inbound | Message.inbound_text(...) |
message_id e provider_message_id |
| IA | AgentInvoker.invoke(...) |
Histórico filtrado por session_id |
| Ação | ParsedLLMResponse → ActionResult |
Código s, metadados e atualizações |
| Entrega | ProcessOutcome(reply, state_after, superseded) |
Log outbound associado ao conjunto inbound |
Exemplo sintético da mensagem SQS:
{"phone_number":"<TELEFONE_FICTICIO>","message_id":"wamid.EXEMPLO_001"}
O texto real é recuperado do receipt no DynamoDB. Um evento SQS para a mensagem B pode drenar A e B na ordem do registro atômico, independentemente da ordem em que chamadas de envio SQS chegaram.
Mensagens rápidas e contexto¶
Se A e B chegam antes de A concluir, A continua registrada no histórico, mas seu resultado pode ser marcado superseded. A execução de B recebe o histórico da mesma sessão, incluindo o que o cliente escreveu em A; não deve repetir uma resposta antiga. O marcador agent_routed_at impede que mídia, comandos ou inbox humano sejam drenados acidentalmente como texto de IA.
O fluxo atual não espera uma janela de agregação. O buffer legado ainda concatena textos com \n quando o consumidor recebe registros de implantação anterior; isso é compatibilidade, não o comportamento do POST atual.
Persistência versus entrega¶
O caso de uso retorna o texto e grava a resposta; a Lambda envia pelo WhatsAppClient. Se o envio falha, o registro permanece pendente e aparece em batchItemFailures. Na próxima tentativa, _pending_outbound_reply() procura resposta existente e reenvia sem nova chamada da IA. A validade é novamente comparada com a última entrada, sessão e estado. Respostas antigas não são reproduzidas depois de mudança de sessão ou takeover humano.
Não há confirmação transacional entre DynamoDB e Meta: uma queda após envio e antes de marcar conclusão pode exigir diagnóstico de duplicidade. IDs estáveis, logs e checagens reduzem o risco, mas não são garantia de entrega exatamente uma vez.
Diagnóstico e testes¶
Siga provider_message_id → receipt → session_id → log outbound, evitando expor conteúdo pessoal. Eventos úteis: webhook_received, orchestrator_event, immediate_turns_done, process_immediate_turn_failed, buffer_already_flushed e logs de envio WhatsApp.
tests/unit/lambdas/test_webhook_handler.py cobre assinatura, lote, roteamento, duplicação e atendimento humano. tests/unit/lambdas/test_orchestrator.py cobre ordem FIFO independente do wake-up, supersessão, reenvio sem segunda chamada LLM e takeover. Para mudanças, preserve os casos test_fifo_wakeup_processes_registered_order_even_when_newer_event_arrives_first e test_failed_current_immediate_turn_replays_without_second_llm_call.
Veja também contratos, persistência e atendimento humano.