DEV Community

Cover image for Do “200 OK” à entrega real: o que aprendi investigando mensagens no Engenhoso AI

Do “200 OK” à entrega real: o que aprendi investigando mensagens no Engenhoso AI

Durante o desenvolvimento do Engenhoso AI, encontrei um daqueles problemas que parecem simples quando descritos em uma frase:

uma mensagem era enviada, o sistema aparentemente funcionava, mas o destinatário nem sempre recebia a resposta como esperado.

À primeira vista, a pergunta parecia óbvia:

onde estava o erro?

Mas sistemas distribuídos raramente respondem bem a perguntas excessivamente simples.

Uma mensagem que atravessa WhatsApp, webhook, aplicação, modelo de inteligência artificial e novamente a infraestrutura de mensageria não percorre um único caminho.

Ela atravessa uma cadeia de acontecimentos.

E foi justamente essa cadeia que precisei aprender a observar.


O primeiro indício: POST /webhook 200 OK

Nos logs do serviço que hospeda o webhook do Engenhoso AI, as requisições apareciam normalmente:

POST /webhook HTTP/1.1 200 OK

Em um primeiro momento, isso parecia significar que tudo estava funcionando.

Mas havia um problema conceitual nessa interpretação.

Um 200 OK apenas informava que determinada requisição havia sido recebida e tratada pelo endpoint.

Ele não respondia às perguntas seguintes:

  • era uma mensagem enviada pelo usuário?
  • era apenas uma atualização de status da Meta?
  • a aplicação chamou a OpenAI?
  • a resposta foi gerada?
  • a Meta aceitou a resposta para envio?
  • a mensagem chegou ao telefone do destinatário?
  • ela foi lida?

Foi então que uma distinção aparentemente simples se tornou central para o diagnóstico:

Webhook recebeu ≠ IA respondeu ≠ Meta aceitou ≠ mensagem foi entregue ≠ mensagem foi lida

Em software, duas etapas consecutivas podem parecer uma coisa só quando não temos instrumentos para observá-las.


O teste com comandos locais

O Engenhoso AI possui alguns comandos locais que não dependem de geração por inteligência artificial.

Um deles é AJUDA.

Durante os testes, esse comando funcionou.

A mensagem chegou ao webhook, foi reconhecida e o menu foi devolvido ao usuário.

Isso permitiu retirar alguns suspeitos da investigação.

Se AJUDA funcionava, então pelo menos parte deste caminho estava operacional:

WhatsApp → Meta → Webhook → Aplicação → Meta → WhatsApp

Mas uma pergunta aberta dependia de uma etapa adicional:

Aplicação → OpenAI → Aplicação

Era necessário investigar essa fronteira separadamente.


O contador da API também pode enganar

Durante parte da investigação, o painel da API permaneceu mostrando o mesmo número de solicitações.

Isso parecia indicar que as perguntas não estavam chegando à OpenAI.

Mais tarde, porém, o contador mudou.

As solicitações estavam sendo contabilizadas.

O que havia ocorrido era uma diferença temporal entre o acontecimento dentro da aplicação e a atualização que eu observava no painel.

Foi um aprendizado importante:

um painel de uso é uma ferramenta de observação, mas não deve ser confundido automaticamente com um log em tempo real da aplicação.

Quando investigamos um sistema distribuído, precisamos saber exatamente o que cada instrumento mede.


Descobrindo a chamada real da OpenAI

Em vez de continuar formulando hipóteses a partir dos painéis, fui ao código.

A aplicação utiliza a Responses API e uma chamada semelhante a:

response = await self._client.responses.create(
    model=self._model,
    instructions=...,
    input=text,
    reasoning={"effort": OPENAI_REASONING_EFFORT},
    max_output_tokens=self._max_output_tokens,
    store=False,
)
Enter fullscreen mode Exit fullscreen mode

Algumas informações começaram então a ficar objetivas.

O projeto estava configurado para utilizar um modelo definido por variável de ambiente.

O esforço adicional de raciocínio estava desativado:

OPENAI_REASONING_EFFORT = "none"
Enter fullscreen mode Exit fullscreen mode

Também encontrei:

max_retries=0
Enter fullscreen mode Exit fullscreen mode

Ou seja, o cliente não estava realizando sucessivas tentativas automáticas capazes de explicar sozinho uma espera de vários minutos.

Mais importante ainda: a aplicação possuía limites explícitos de tempo.


Timeout não era apenas uma hipótese

O projeto já possuía uma arquitetura de deadlines.

Na configuração de referência apareciam, entre outros, estes valores:

  • OPENAI_TIMEOUT_SECONDS=12
  • OPENAI_WEBHOOK_TOTAL_TIMEOUT_SECONDS=15

E o código realmente propagava um prazo para a geração.

De maneira simplificada:

generation_task = asyncio.create_task(
    _run_openai_generation(...)
)

generation_timed_out = not await _wait_for_openai_task(
    generation_task,
    deadline=openai_deadline,
)
Enter fullscreen mode Exit fullscreen mode

Quando o limite era ultrapassado, o Engenhoso AI selecionava uma resposta de fallback.

Isso explicava uma mensagem que apareceu em um dos testes:

Desculpe, não consegui gerar uma resposta agora. Tente novamente em alguns instantes.

Poucos minutos depois, a mesma pergunta recebeu uma resposta normal.

O problema era, portanto, intermitente.

E já não fazia sentido atribuir automaticamente vários minutos de espera a uma única chamada indefinida para o modelo.


O envio à Meta também tinha prazo

Continuei seguindo o código.

A própria etapa de envio da resposta possuía um deadline:

remaining_delivery_seconds = delivery_deadline - loop.time()

if remaining_delivery_seconds <= 0:
    return
Enter fullscreen mode Exit fullscreen mode

E a chamada era executada dentro de uma janela controlada:

async with asyncio.timeout_at(meta_deadline):
    await _send_started_reply(...)
Enter fullscreen mode Exit fullscreen mode

Isso trouxe outra conclusão importante.

O projeto já havia sido construído para tentar impedir que uma operação permanecesse indefinidamente presa em uma etapa externa.

Mas ainda faltava uma informação fundamental:

o que acontecia depois que a Meta aceitava uma mensagem?


O ponto cego: statuses

A API do WhatsApp envia callbacks relacionados ao estado das mensagens.

Entre eles podem aparecer estados como:

  • sent
  • delivered
  • read
  • failed

O Engenhoso AI já possuía um teste que verificava que uma atualização de status deveria ser ignorada como mensagem de usuário:

def test_atualizacao_de_status_e_ignorada() -> None:
    ...
    assert response.status_code == 200
    assert fake.sent == []
Enter fullscreen mode Exit fullscreen mode

Isso estava correto.

Um callback de status não deve provocar uma nova resposta automática.

O problema era outro:

ele também desaparecia do ponto de vista da observabilidade.

O código extraía:

messages = _extract_messages(payload, settings)
Enter fullscreen mode Exit fullscreen mode

Mas um callback poderia conter, por exemplo:

{
  "statuses": [
    {
      "status": "delivered"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

em vez de uma nova mensagem.

Nesse caso, o webhook respondia 200 OK, mas eu não conseguia saber pelo log se aquele evento representava sent, delivered, read ou até failed.

Foi aí que percebi que o problema não era apenas corrigir alguma coisa.

Era tornar o sistema capaz de explicar o que estava acontecendo.


Criando observabilidade sem registrar a conversa

Foi criada uma função independente para extrair apenas os estados técnicos:

def _extract_status_updates(payload):
    ...
Enter fullscreen mode Exit fullscreen mode

Conceitualmente, ela percorre:

entry → changes → field = messages → value → statuses

e identifica estados como sent, delivered, read e failed.

Mas havia outra preocupação.

Observabilidade não deveria significar registrar indiscriminadamente informações dos usuários.

Por isso, o message_id original não precisava aparecer nos logs.

Foi utilizada uma referência derivada:

message_ref = (
    hashlib.sha256(message_id.encode("utf-8")).hexdigest()[:12]
    if message_id
    else "unknown"
)
Enter fullscreen mode Exit fullscreen mode

Os registros podem então assumir formas semelhantes a:

whatsapp_status status=sent ref=8a92e740fb21 timestamp=...

whatsapp_status status=delivered ref=8a92e740fb21 timestamp=...

whatsapp_status status=read ref=8a92e740fb21 timestamp=...

A mesma referência permite correlacionar os eventos sem registrar:

  • o texto da conversa;
  • o telefone;
  • o nome do usuário;
  • o identificador original da mensagem.

Para mim, esse ponto é particularmente importante.

Observabilidade não precisa significar abandono da privacidade.


Testar antes de integrar

Antes de conectar essa informação ao fluxo real, acrescentei um teste específico para os estados sent, delivered, read e failed.

A sequência de desenvolvimento foi deliberadamente simples:

criar extrator → criar teste → ligar ao log → Pull Request → Merge → Deploy

A alteração foi desenvolvida em uma branch separada:

fix/whatsapp-status-observability

Depois passou por Pull Request antes de chegar à main.

Esse processo pode parecer mais trabalhoso do que editar diretamente a produção.

Mas existe uma diferença entre fazer uma mudança e construir um histórico compreensível de mudanças.

Quanto mais o projeto cresce, mais essa diferença importa.


A infraestrutura também entrou no experimento

Durante a investigação, o webhook ainda estava executando em uma instância gratuita do serviço de hospedagem.

Decidi migrá-lo para uma instância paga de entrada.

No momento da mudança, a configuração selecionada foi:

  • Starter
  • 0,5 CPU
  • 512 MB de RAM

A decisão não foi tomada porque eu tivesse demonstrado que a hospedagem era a causa de todas as falhas.

Essa distinção é importante.

A migração foi uma forma de retirar uma variável da investigação e dar ao webhook uma infraestrutura mais adequada ao seu papel atual.

Depois do deploy, o serviço voltou ao estado Live, enquanto o health check continuou respondendo 200 OK.

A partir desse momento, o comportamento posterior poderia ser comparado com os registros anteriores à migração.

Não é correto afirmar:

“o plano pago resolveu o problema”

apenas porque uma mensagem posterior funcionou.

É necessário observar uma série de interações.

Essa diferença entre coincidência temporal e evidência causal é um aprendizado que considero importante também fora da programação.


O que mudou na maneira de diagnosticar

Antes dessa investigação, uma pergunta poderia ser formulada assim:

Por que a pessoa não recebeu a mensagem?

Agora ela pode ser dividida em perguntas melhores:

  1. O webhook recebeu o evento?
  2. Era uma mensagem ou um callback de status?
  3. A aplicação decidiu chamar a IA?
  4. A geração terminou ou ocorreu timeout?
  5. Foi utilizado fallback?
  6. A resposta foi enviada para a Meta?
  7. A Meta marcou como sent?
  8. Foi marcada como delivered?
  9. Foi marcada como read?
  10. Houve failed?

Quanto melhor a pergunta técnica, menor a necessidade de adivinhar.


Uma arquitetura que começou a se tornar observável

Hoje consigo representar o fluxo de forma mais clara:

Usuário → WhatsApp → Meta → Webhook Engenhoso AI

A partir do webhook, a aplicação pode seguir por um comando local ou por geração de IA.

Quando existe geração:

Webhook → OpenAI → resposta gerada → Meta

Depois disso, os callbacks ajudam a observar o que ocorreu com a mensagem:

Meta → sentdeliveredread

ou, quando algo não ocorre como esperado:

Meta → failed

O sistema não ficou apenas mais fácil de depurar.

Ele ficou mais capaz de contar a própria história operacional.


O aprendizado maior

Quando comecei essa investigação, eu procurava um erro.

No decorrer do processo, percebi que havia uma questão anterior ao erro:

eu possuía instrumentos suficientes para saber onde ele estava?

Nem sempre a próxima melhoria de um sistema é uma nova funcionalidade.

Às vezes, o que falta é capacidade de observação.

Um 200 OK pode ser verdadeiro e, ao mesmo tempo, insuficiente.

A chamada da IA pode ter acontecido e o painel ainda não ter atualizado.

A Meta pode ter aceitado uma mensagem sem que isso signifique que ela já chegou ao destinatário.

São estados diferentes.

E aprender a respeitar essas diferenças talvez seja uma das mudanças mais importantes quando um projeto deixa de ser apenas um experimento e começa a se comportar como um sistema real.

O Engenhoso AI continua sendo, para mim, um ambiente de aprendizagem.

Cada dificuldade técnica revela não apenas alguma coisa sobre Python, APIs, webhooks ou inteligência artificial.

Ela revela também uma forma de pensar:

antes de corrigir aquilo que não compreendemos, precisamos construir meios para enxergá-lo.


Transparência sobre o processo

Este texto nasceu de uma experiência real de desenvolvimento do projeto Engenhoso AI.

Ferramentas de inteligência artificial foram utilizadas como apoio na investigação técnica, organização das hipóteses, leitura do código e revisão do texto.

As decisões sobre arquitetura, testes, alterações no projeto, interpretação dos resultados e publicação permanecem sob responsabilidade do autor.


Engenhoso AI
Aprender, construir, observar e compreender.

Top comments (0)