Files
INDENTIFICAR-TEXTO-EM-IMAGEM/docs/superpowers/specs/2026-08-20-orcamento-exames-imagem-design.md
T
EricandClaude Sonnet 5 be145fad95 docs: confirma pipeline de audio validado com execucao real (#476)
Execucao 476 (2026-08-21 17:31) confirma o fluxo de audio ponta-a-ponta:
download via bmToken, transcricao Whisper correta, e resposta do AI Agent
com o valor certo do exame perguntado.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 14:32:15 -03:00

22 KiB

Orçamento automático de exames a partir de foto (GChat/Z-PRO + n8n + IA)

  • Data: 2026-08-20
  • Status: Aprovado para implementação
  • Instância n8n alvo: https://devn8n.gtigrupo.com.br

Objetivo

Quando um cliente manda, pelo WhatsApp (canal Z-PRO/GChat), uma foto de um pedido médico de exames, o sistema deve:

  1. Identificar os exames escritos/impressos na imagem.
  2. Cruzar esses exames com uma tabela de preços mantida no prompt da IA.
  3. Montar um orçamento formatado para WhatsApp (itens + valores + total).
  4. Enviar esse orçamento automaticamente de volta ao cliente, via API do Z-PRO.

Este é um teste ponta-a-ponta: o objetivo imediato é validar o fluxo técnico completo (recebimento da imagem → IA de visão → resposta automática), usando uma lista de exames de exemplo. A lista real de exames/valores é trocada depois, diretamente no prompt do agente.

Contexto / descobertas

  • A documentação disponível na pasta do projeto (Z-PRO v4.x.x.x.postman_collection.json) cobre apenas a API externa de saída do Z-PRO (criar ticket, enviar mensagem, listar contatos etc.) — não documenta o formato do webhook de entrada que o Z-PRO dispara quando um cliente manda uma mensagem.
  • A conta n8n já tem vários workflows em produção que integram GChat/Z-PRO com agentes de IA para bots de agendamento (ex.: Agendamento - GChat - Z-PRO (clínica exemplo), id yG2mi6ECBpPv4Hmn). Neles, o Z-PRO chama o webhook do n8n com um body como:
    { "step": "process", "protocol": "...", "entrada": "texto do cliente", "contactName": "...", "number": "55..." }
    
    Nenhum desses fluxos existentes trata imagem — todos são só texto. Portanto o nome exato do campo que carrega a foto (mediaUrl, file, midia, etc.) é desconhecido até o primeiro teste real com uma imagem de verdade.
  • Existe um sub-workflow reutilizável para envio de texto pelo Z-PRO: MCP Server - Mensageria ZPRO - enviar_texto (id Y4ndG7GuIUrmVRxV), que recebe { number, body, externalKey, isClosed }, monta o corpo e faz POST para https://apigchat.gtigrupo.com.br/v2/api/external/{apiId}/ usando a credencial HTTP Header Auth "ZPRO API Token - Canal QR - MC AGENDAMENTO". Este fluxo novo vai reaproveitar esse mesmo sub-workflow para enviar a resposta, em vez de duplicar a chamada HTTP.
  • O node AI Agent do n8n (@n8n/n8n-nodes-langchain.agent) tem a opção passthroughBinaryImages (padrão true), que repassa automaticamente imagens binárias do item de entrada para o modelo de chat como mensagem de imagem — não é necessário um node de OCR separado.

Decisões (confirmadas com o usuário)

Decisão Escolha
Como responder ao cliente Assíncrono: o workflow processa a imagem e chama a API do Z-PRO (SendMessageAPIText, via sub-workflow existente) para enviar a resposta. O webhook de entrada responde rápido (200 OK) e não fica esperando a IA.
Envio automático vs. revisão humana Automático — a IA monta e já envia o orçamento, sem aprovação humana no meio.
Lista de exames/valores Lista de exemplo (placeholder) embutida no prompt do agente para este teste. Lista real é trocada depois, editando o texto do system prompt do node AI Agent no n8n.
Exame identificado na imagem mas fora da lista de preços O orçamento traz os itens reconhecidos normalmente (com valor) e avisa, para os não reconhecidos, que um atendente vai confirmar o valor à parte. Nunca bloqueia o envio do orçamento inteiro por causa de um item não reconhecido.
Canal/token do Z-PRO usado para enviar a resposta Reaproveita o canal/credencial já cadastrados no n8n para "Canal QR - MC AGENDAMENTO" (mesmo usado pelo bot de agendamento), via o sub-workflow enviar_texto existente.
Workflow Novo workflow dedicado (não mexe no workflow de agendamento existente).
Credenciais (IA, Z-PRO) Configuradas como credenciais nativas do n8n (não hardcoded nos nodes), para poderem ser trocadas depois sem editar o workflow.

Arquitetura

Novo workflow: Orçamento de Exames - GChat - Z-PRO (IA Visão)

Webhook (Z-PRO)
   → Responder 200 OK imediatamente (ack rápido, não espera a IA)
   → Extrair campos do webhook (número, protocolo/ticket, referência da imagem)
   → IF: é mensagem de imagem?
        ├─ não → encerra (não faz nada; este fluxo só trata imagem)
        └─ sim → Baixar imagem (HTTP Request, binário)
                → AI Agent (visão) — system prompt com tabela de exames/valores
                     + OpenAI Chat Model (gpt-4o, com credencial própria)
                → Montar corpo da resposta (body/number/externalKey/isClosed)
                → Execute Workflow: "MCP Server - Mensageria ZPRO - enviar_texto"
   (erro em qualquer etapa crítica → mensagem padrão de fallback, enviada pelo
   mesmo sub-workflow: "Recebemos sua solicitação, nossa equipe vai te
   responder em breve.")

Componentes

  1. Webhook (trigger) — novo path dedicado (ex.: orcamento-exames-zpro), responseMode: lastNode, responde rápido com um ack simples. Este endpoint precisa ser configurado, do lado do Z-PRO, como destino das mensagens do canal-alvo (mesmo mecanismo já usado para plugar os bots de agendamento — o usuário já sabe fazer isso).

  2. Extrair campos do webhook — node Set/Code que lê number, protocol (ou equivalente) e a referência da imagem do body recebido. Os nomes de campo para a imagem são a melhor suposição inicial e serão corrigidos após o primeiro teste real (ver seção "Suposições em aberto").

  3. IF é imagem? — segue adiante só se a mensagem recebida for do tipo imagem; qualquer outro tipo (texto, áudio, documento) encerra o fluxo sem ação, já que este workflow trata exclusivamente pedidos de exame por foto.

  4. Baixar imagem — HTTP Request que baixa o arquivo da imagem como binário (assumindo que o Z-PRO entrega uma URL pública de mídia; se o payload real trouxer a imagem em base64 em vez de URL, este node é trocado por uma conversão base64 → binário).

  5. AI Agent + OpenAI Chat Model (gpt-4o) — recebe a imagem baixada como binário (repassada automaticamente ao modelo via passthroughBinaryImages). System prompt contém:

    • Persona: assistente de orçamentos de exames da clínica, respondendo por WhatsApp.
    • Tabela de exames + valores (placeholder nesta fase).
    • Instruções: ler os exames pedidos na imagem (impressos ou manuscritos), casar com a tabela (tolerando abreviações/sinônimos comuns), montar resposta formatada para WhatsApp com itens + valores + total: para exames não encontrados na tabela, listar separadamente avisando que um atendente vai confirmar o valor. Saída deve ser só o texto final a enviar ao cliente (sem comentários extras, sem markdown incompatível com WhatsApp).
    • Credencial de API key da OpenAI própria deste node (independente da usada nos bots de agendamento), para poder ser trocada sem afetar outros fluxos.
  6. Montar corpo da resposta — Set/Code node que monta { number, body: <texto da IA>, externalKey: <único por execução>, isClosed: false }. externalKey gerado por execução (ex.: orcamento-{{ $execution.id }}) para respeitar a exigência do Z-PRO de chave única por ação.

  7. Execute Workflow: enviar_texto — chama o sub-workflow existente MCP Server - Mensageria ZPRO - enviar_texto (id Y4ndG7GuIUrmVRxV), reaproveitando a credencial e a lógica de classificação de erro já existentes.

Tratamento de erro

  • Falha ao baixar a imagem ou erro na chamada da IA → segue para o mesmo passo de envio (enviar_texto), mas com uma mensagem padrão de fallback em vez do orçamento, avisando que a equipe vai responder manualmente.
  • Falha no próprio envio (API do Z-PRO fora do ar, token inválido etc.) → fica registrada na execução do n8n (o sub-workflow já classifica o tipo de erro); não há novo retry automático nesta primeira versão.

Suposições em aberto (validar no primeiro teste real)

  • Nome do campo da imagem no webhook — assumimos algo como mediaUrl (por analogia com a API de saída do Z-PRO, que usa esse nome para anexar arquivos). Pode ser diferente; corrigido após ver o payload real de uma mensagem de imagem.
  • Formato da mídia — assumimos URL pública, não base64 inline. Se for base64, o node "Baixar imagem" é substituído por uma conversão local.
  • Mecanismo de entrega do webhook para imagens — os exemplos existentes nessa conta só cobrem texto (via um passo "Webhook" dentro de um ChatFlow do Z-PRO). Se imagens não passarem pelo mesmo tipo de passo, pode ser necessário configurar algo diferente no painel do Z-PRO — isso será resolvido junto com o usuário durante o teste.

Plano de teste

  1. Criar o workflow no n8n (inativo) e obter a URL de teste do webhook.
  2. Usuário configura essa URL como destino de mensagens do canal-alvo no painel do Z-PRO.
  3. Usuário envia uma foto real de pedido de exames para o número de teste.
  4. Inspecionar a execução no n8n para confirmar o payload real recebido e ajustar o node "Extrair campos do webhook" (e "Baixar imagem", se necessário).
  5. Validar que a resposta com o orçamento (lista de exemplo) chega corretamente no WhatsApp do cliente de teste.
  6. Trocar a lista de exemplo pela lista real de exames/valores no prompt do AI Agent.

Revisão 1 (2026-08-21) — fluxo conversacional (texto + imagem)

Depois do primeiro teste real, o usuário apontou que um atendimento real não chega só como uma foto isolada: o cliente pode mandar texto antes/depois, ou digitar os nomes dos exames em vez de (ou além de) mandar foto, tudo em mensagens separadas. O desenho original tratava qualquer mensagem sem imagem como "não se aplica" e encerrava sem responder — o que quebra esse caso real.

Mudanças no workflow, alinhadas ao mesmo padrão já usado pelo bot de agendamento desta conta (Agendamento - GChat - Z-PRO (clínica exemplo)):

  • O node "É mensagem de imagem?" deixou de ser um filtro que encerra o fluxo — agora só decide se baixa a imagem antes do agente ou não. As duas saídas (com ou sem imagem) chegam no AI Agent; nenhuma mensagem é mais ignorada.
  • "Extrair campos do webhook" agora também extrai o texto da mensagem (entrada), no mesmo nome de campo usado pelos bots de agendamento que já funcionam nessa conta.
  • Adicionada memória de conversa por sessão (node Memória da Sessão (Postgres), reaproveitando a credencial Postgres Agendamento - (clínica exemplo), com sessionKey prefixado (orcamento-<protocolo>) para não colidir com a memória do bot de agendamento). Isso é necessário porque cada mensagem do WhatsApp chega como uma execução nova e separada do workflow — sem memória persistente, o agente esqueceria o que o cliente já tinha mandado numa mensagem anterior.
  • O system prompt do AI Agent foi reescrito para ser conversacional: se o cliente ainda não mandou nem foto nem lista de exames (nesta mensagem ou antes, na mesma conversa), o agente pede isso educadamente em vez de inventar um orçamento vazio; assim que houver pelo menos um exame identificável (por imagem, texto, ou o histórico da conversa), ele monta o orçamento normalmente.
  • O envio da resposta (via sub-workflow enviar_texto) passou a acontecer sempre, em toda mensagem — porque agora toda mensagem gera uma resposta do agente (seja um pedido de mais informação, seja o orçamento final), não só quando uma imagem é reconhecida.

Descoberta durante o teste: payload do webhook chegou vazio

Os dois testes reais feitos pelo usuário mostraram o webhook recebendo body: {} (content-length 0, user-agent axios/1.19.0) — ou seja, o Z-PRO não mandou protocol/entrada/contactName/number como os bots de agendamento recebem. Causa raiz confirmada pelo usuário: no ChatFlow do Z-PRO, o passo "Webhook Avançado" do fluxo de orçamento (Menu Orçamento / Menu Orçamento 2) estava com o campo Body vazio, enquanto o do fluxo de agendamento (Menu Agendamento / Menu Agendamento 2, que já funciona) tem:

{
  "step": "process",
  "entrada": "{{entrada}}",
  "number": "{{phoneNumber}}",
  "contactName": "{{name}}",
  "protocol": "{{protocol}}"
}

Ação do usuário: copiar o mesmo Body (com essas mesmas variáveis) para os passos "Webhook Avançado" de Menu Orçamento e Menu Orçamento 2. Os nomes de campo já batem com o que o node "Extrair campos do webhook" espera (body.protocol, body.number, body.contactName, body.entrada) — não precisou ajustar nada do lado do n8n para esses campos.

Descoberta 2: o Z-PRO captura a resposta HTTP e reenvia ao cliente

Nas telas do ChatFlow, o passo "Webhook Avançado" tem uma seção "Resposta da API (captura e roteamento)": ele salva o corpo da resposta HTTP do nosso webhook numa variável (n8n1) e o passo seguinte ("Mensagem") manda {{n8n1}} de volta ao cliente pelo WhatsApp. Ou seja, o Z-PRO não usa a API de envio de mensagens para essa resposta — ele repassa direto o que o webhook devolver na hora. Como o "Caminho no JSON" fica vazio, o corpo precisa ser texto puro, não JSON.

Isso invalida a decisão original de "envio assíncrono via API do Z-PRO": o workflow foi ajustado para responder de forma síncrona, com responseMode: responseNode no Webhook e um node "Respond to Webhook" (respondWith: text) devolvendo o texto final direto na resposta HTTP. O node "Enviar orçamento (Z-PRO)" (chamada assíncrona ao sub-workflow enviar_texto) foi removido — mantê-lo mandaria a mensagem duas vezes.

Descoberta 3: memória de conversa

A credencial Postgres reaproveitada do bot de agendamento (Postgres Agendamento - (clínica exemplo)) é de uso exclusivo daquele fluxo — o usuário pediu para não usá-la aqui sem autorização explícita. A memória de sessão agora usa o node nativo Simple Memory (memoryBufferWindow, sem credencial), com sessionKey prefixado (orcamento-<protocolo ou telefone>) para isolar as conversas de orçamento das de agendamento.

Descoberta 4: ChatFlow do Z-PRO não tem nenhum caminho pra imagem (confirmado)

Investigação exaustiva, todas as vias fechadas:

  • Seletor de variáveis do passo "Webhook Avançado": só campos de contato/sessão (Nome, Saudação, Protocolo, Email, Telefone, Kanban, Atendente, Email do atendente, Primeiro/Sobrenome, Empresa) — nenhuma variável de mídia.
  • Consulta à API do Z-PRO (showticketchatbot, buscando o ticket pelo número) — tentativa abandonada: no teste real, retornou um ticket fechado, de mais de um mês atrás, sobre um assunto não relacionado. Essa API não busca de forma confiável "a conversa/mensagem atual".
  • Passo "n8n" (interação alternativa ao "Webhook"): mesma lista de variáveis de contato/sessão, sem corpo customizável — não ajuda.
  • Passo "Mídia": é o oposto do que se imaginava — serve pra o bot enviar um arquivo pro cliente (seleciona arquivo + legenda), não pra capturar o que o cliente manda.
  • "Tipo da variável" na captura de resposta (seção "Variável (opcional)" dos passos "Mensagem"/"n8n"): só permite capturar texto livre — sem opção de mídia/arquivo.

Conclusão: o construtor de ChatFlow do Z-PRO, na versão em uso, não tem nenhum mecanismo para repassar uma imagem recebida do cliente para um webhook externo. Não é uma configuração faltando — é uma limitação da ferramenta nesse ponto. Corrigido o prompt do AI Agent pra não prometer mais "mande uma foto" ao cliente (antes disso ele pedia foto mesmo sem nunca conseguir processar nenhuma).

Descoberta 5: "Webhook do Canal" resolve a imagem (solução final)

O Z-PRO tem, nas configurações do canal, uma opção "Webhook do Canal" independente do ChatFlow: Habilitar Webhook do Canal + URL de destino + toggle Mensagens recebidas (dispara a cada mensagem recebida pelo canal, "em paralelo ao webhook geral do tenant"). Payload real capturado:

{
  "method": "message",
  "msg": {
    "from": "5575...",
    "id": "wamid....",
    "timestamp": "...",
    "type": "image",
    "image": { "mime_type": "image/jpeg", "id": "...", "url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/?..." }
  },
  "ticket": {
    "protocol": "...", "id": 469774, "status": "pending",
    "contact": { "name": "...", "number": "..." },
    "whatsapp": { "bmToken": "EAA..." }
  }
}

Pra mensagem de texto, msg vem como { "type": "text", "text": { "body": "..." } } em vez de image. O workflow foi reconstruído em cima desse webhook:

  • Novo endpoint: orcamento-exames-canal-zpro (path completamente separado do antigo orcamento-exames-zpro, que foi removido).
  • Campos extraídos: sessionId (ticket.protocol), telefone (msg.from), nomeContato (ticket.contact.name), entrada (msg.text.body), imagemUrl (msg.image.url), tipoMidia (msg.type), mediaToken (ticket.whatsapp.bmToken).
  • A URL da imagem é do WhatsApp/Meta (lookaside.fbsbx.com) e exige autenticação — o node "Baixar imagem" manda Authorization: Bearer {{ mediaToken }} usando o bmToken do próprio payload.
  • Como esse webhook é uma notificação assíncrona (não é capturado e reenviado pelo Z-PRO como no mecanismo do ChatFlow), o envio da resposta voltou a ser feito via API, reaproveitando o sub-workflow enviar_texto (mesma lógica da primeira versão do desenho).
  • Importante: esse webhook dispara em paralelo a qualquer coisa que o ChatFlow também dispare para a mesma mensagem. Por isso as interações "Webhook Avançado" e "Mensagem" dos passos Menu Orçamento / Menu Orçamento 2 no ChatFlow precisam ser removidas (ação do usuário, fora do n8n) — senão o cliente recebe duas respostas pra cada mensagem.
  • O prompt do AI Agent voltou a aceitar/pedir foto normalmente, já que a imagem agora chega de verdade.

Teste ponta-a-ponta validado

Com uma imagem real de teste, o fluxo completo funcionou: download autenticado da mídia (44.9 kB, jpg) → IA de visão leu a imagem e identificou 2 exames da tabela + 1 fora da tabela (avisando que precisa de confirmação) → resposta enviada com sucesso pelo Z-PRO. Texto puro também validado (cliente manda "oi" → IA pede pra digitar/mandar os exames).

Descoberta 6: canal/API errados no envio (corrigido)

O envio estava reaproveitando o sub-workflow enviar_texto (Y4ndG7GuIUrmVRxV), que pertence a outro projeto (bot de agendamento) e usa um ApiID/canal diferente — por isso as respostas saíam por um número errado (recebendo num número, respondendo por outro). O usuário criou um ApiID e token dedicados a este projeto. Correção aplicada:

  • Nova credencial httpHeaderAuth "ZPRO API Token - Orçamento de Exames" (nome deliberadamente diferente de "ZPRO API Token - Canal QR - MC AGENDAMENTO", pra não confundir entre projetos), header Authorization: Bearer <token>.
  • O envio deixou de chamar o sub-workflow compartilhado — agora é uma chamada HTTP direta (POST) dentro deste próprio workflow, isolada, usando a URL https://apigchat.gtigrupo.com.br/v2/api/external/<ApiID deste projeto>/ e a credencial acima.
  • Isso mantém este projeto completamente independente do de agendamento — nenhum node ou credencial é compartilhado entre os dois.

Descoberta 7: filtro por fluxo/fila (o bot respondia a qualquer mensagem)

Como o "Webhook do Canal" dispara pra toda mensagem do número (não só pra quem está no menu de Orçamento), o bot estava respondendo pra qualquer coisa — cliente mandando foto do CPF, pedindo agendamento, etc. — sempre pedindo exames. Isso não reflete o atendimento real: o cliente manda mensagem, recebe o menu de filas, escolhe "Orçamento", e só a partir daí a automação deve agir.

Correção: o payload do webhook traz ticket.stepChatFlow, identificando em qual passo do ChatFlow o ticket está parado. Os passos Menu Orçamento e Menu Orçamento 2 (os únicos dois pontos do ChatFlow que levam a essa automação) têm ids fixos: node-1787312722889 e node-1787312980617. Adicionado o node "Está no fluxo de Orçamento?", logo após extrair os campos: só segue adiante (pro resto do pipeline) se stepChatFlow for um desses dois; qualquer outra coisa cai num NoOp e é ignorada — sem resposta nenhuma, deixando pra fila/fluxo correspondente cuidar.

Risco conhecido: esse filtro depende dos ids de step atuais do ChatFlow. Se o Menu Orçamento/Menu Orçamento 2 forem recriados (não só editados) no editor do Z-PRO, os ids mudam e o filtro precisa ser atualizado no node Está no fluxo de Orçamento?.

Descoberta 8: pipeline de áudio confirmado ponta-a-ponta

Adicionado suporte a mensagens de áudio: Baixar áudio (HTTP Request com o bmToken) → Transcrever áudio (OpenAI node, resource=audio, operation=transcribe, Whisper whisper-1) → Preparar texto do áudio (Set node que popula entrada com a transcrição) → segue pro AI Agent - Orçamento normalmente.

Confirmado via execução real #476 (2026-08-21 17:31): cliente mandou um áudio de 5s, o Whisper transcreveu corretamente "Eu não tenho foto não, quero saber o valor da Hemograma.", e o AI Agent respondeu certo "Olá! O Hemograma custa R$ 35,00.". Pipeline de texto, imagem, PDF e áudio todos validados com execuções reais.

Ainda em aberto

  • Remover as interações antigas "Webhook Avançado"/"Mensagem" dos passos Menu Orçamento/Menu Orçamento 2 no ChatFlow (apontavam pro endpoint antigo, que não existe mais no n8n — não causam mais duplicidade, mas ficam como chamada morta até serem removidas).

Fora de escopo (por enquanto)

  • Fila/aprovação humana antes do envio (usuário optou por envio automático).
  • Fonte de preços fora do prompt (planilha/banco) — pode ser considerado no futuro se a lista crescer muito, mas não faz parte deste teste.
  • Retry automático em caso de falha de envio.