Files
INDENTIFICAR-TEXTO-EM-IMAGEM/docs/superpowers/plans/2026-08-20-orcamento-exames-imagem.md
T

23 KiB
Raw Blame History

Orçamento Automático de Exames por Foto (GChat/Z-PRO + n8n) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Nota sobre esta plan: o "código" aqui não é um arquivo-fonte — é um workflow do n8n, construído via chamadas às ferramentas MCP n8n-mcp (já conectadas nesta sessão, instância https://devn8n.gtigrupo.com.br). O equivalente a "rodar o teste" é chamar mcp__n8n-mcp__n8n_validate_workflow e ler os erros; o equivalente a "commit" é salvar um snapshot do JSON do workflow no repositório e commitar. A Task 6 exige uma ação real do usuário (mandar uma foto de teste no WhatsApp) e não pode ser concluída sem ele — por isso a execução recomendada desta plan é inline, nesta mesma sessão, não subagent-driven.

Goal: Criar, no n8n, um workflow que recebe (via webhook do Z-PRO/GChat) uma foto de pedido de exames, usa um AI Agent com visão para identificar os exames e cruzá-los com uma tabela de preços no prompt, e envia automaticamente um orçamento formatado de volta ao cliente pela API do Z-PRO.

Architecture: Webhook (ack imediato) → extrai campos do payload → filtra só mensagens de imagem → baixa a imagem → AI Agent (gpt-4o com visão, tabela de preços no system prompt) → monta o corpo da resposta → envia via sub-workflow já existente MCP Server - Mensageria ZPRO - enviar_texto (id Y4ndG7GuIUrmVRxV). Erros em qualquer etapa caem numa mensagem de fallback, que também é enviada pelo mesmo sub-workflow.

Tech Stack: n8n (self-hosted, devn8n.gtigrupo.com.br), n8n-mcp (ferramentas mcp__n8n-mcp__*), node @n8n/n8n-nodes-langchain.agent + lmChatOpenAi (OpenAI gpt-4o), API externa do Z-PRO v4.

Global Constraints

  • Envio do orçamento é automático, sem aprovação humana no meio (decisão da spec).
  • externalKey deve ser único por envio (exigência da API do Z-PRO — não pode repetir).
  • isClosed do ticket fica false por padrão (o ticket permanece aberto após o envio).
  • A resposta final da IA deve ser somente o texto a mandar ao cliente — sem markdown incompatível com WhatsApp (nada de **, #, etc.), sem comentários extras.
  • Nunca deixar de orçar por causa de um único exame não reconhecido — orça o que reconhecer e avisa do resto (decisão da spec).
  • Reaproveitar o sub-workflow existente MCP Server - Mensageria ZPRO - enviar_texto (id Y4ndG7GuIUrmVRxV) para o envio — não duplicar a chamada HTTP ao Z-PRO.
  • A credencial de IA usada neste workflow deve ser própria (não a credencial GPT compartilhada com os bots de agendamento), para poder ser trocada sem afetar outros fluxos.
  • Instância n8n alvo: https://devn8n.gtigrupo.com.br, acessível nesta sessão via as ferramentas mcp__n8n-mcp__*.
  • Nome do workflow: Orçamento de Exames - GChat - Z-PRO (IA Visão).
  • Nomes de campo do payload de entrada de imagem são desconhecidos até o teste real (ver Task 6) — o workflow é construído com suposições defensivas (múltiplos nomes de campo candidatos) e corrigido depois de ver o payload real.

Task 1: Esqueleto do workflow (Webhook com ack imediato)

Files:

  • Cria (no n8n, via MCP): workflow Orçamento de Exames - GChat - Z-PRO (IA Visão).
  • Create: n8n/orcamento-exames-imagem.workflow.json (snapshot local do workflow, para ficar versionado no repo).

Interfaces:

  • Produces: node Webhook - Foto Exame (id webhook-orcamento), que entrega o payload recebido do Z-PRO em $json.body. Path do webhook: orcamento-exames-zpro. Tarefas seguintes conectam a partir deste node.

  • Step 1: Criar o workflow com o node de Webhook

Chame mcp__n8n-mcp__n8n_create_workflow com:

{
  "name": "Orçamento de Exames - GChat - Z-PRO (IA Visão)",
  "nodes": [
    {
      "id": "webhook-orcamento",
      "name": "Webhook - Foto Exame",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [-600, 0],
      "parameters": {
        "httpMethod": "POST",
        "path": "orcamento-exames-zpro",
        "responseMode": "onReceived",
        "options": {}
      }
    }
  ],
  "connections": {},
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveManualExecutions": true,
    "saveExecutionProgress": true,
    "binaryMode": "separate"
  }
}

Guarde o id do workflow retornado — todas as tasks seguintes usam esse id.

  • Step 2: Validar

Chame mcp__n8n-mcp__n8n_validate_workflow com { "id": "<id retornado>" }. Esperado: 0 errors (um workflow só com um webhook, sem mais nada conectado, pode gerar warnings do tipo "nenhum node depois do trigger" — isso é esperado nesta etapa e será resolvido nas próximas tasks; o que importa aqui é não ter errors).

  • Step 3: Salvar snapshot local e commitar

Chame mcp__n8n-mcp__n8n_get_workflow com o id, pegue o campo data, e escreva o JSON formatado (indentado) em n8n/orcamento-exames-imagem.workflow.json (crie a pasta n8n/ se não existir).

git add n8n/orcamento-exames-imagem.workflow.json
git commit -m "feat(n8n): cria esqueleto do workflow de orçamento de exames (webhook)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"

Task 2: Extrair campos do webhook e filtrar só mensagens de imagem

Files:

  • Modifica (no n8n): workflow criado na Task 1 (via n8n_update_partial_workflow).
  • Modify: n8n/orcamento-exames-imagem.workflow.json (snapshot).

Interfaces:

  • Consumes: node Webhook - Foto Exame (Task 1), payload em $json.body.

  • Produces:

    • node Extrair campos do webhook (id extrair-campos), que expõe telefone, nomeContato, sessionId, imagemUrl, tipoMidia no item de saída — tasks seguintes leem esses campos via $('Extrair campos do webhook').item.json.<campo>.
    • node É mensagem de imagem? (id if-e-imagem), branch verdadeiro (index 0) ainda sem destino (conectado na Task 3), branch falso (index 1) → node Não é imagem - encerra (id noop-nao-imagem).
  • Step 1: Adicionar os três nodes e as conexões

Chame mcp__n8n-mcp__n8n_update_partial_workflow com id do workflow e:

{
  "operations": [
    {
      "type": "addNode",
      "node": {
        "id": "extrair-campos",
        "name": "Extrair campos do webhook",
        "type": "n8n-nodes-base.set",
        "typeVersion": 3.4,
        "position": [-360, 0],
        "parameters": {
          "assignments": {
            "assignments": [
              { "id": "campo-sessionid", "name": "sessionId", "type": "string", "value": "={{ $json.body.protocol }}" },
              { "id": "campo-telefone", "name": "telefone", "type": "string", "value": "={{ $json.body.number }}" },
              { "id": "campo-nomecontato", "name": "nomeContato", "type": "string", "value": "={{ $json.body.contactName }}" },
              { "id": "campo-imagemurl", "name": "imagemUrl", "type": "string", "value": "={{ $json.body.mediaUrl || $json.body.fileUrl || $json.body.file || '' }}" },
              { "id": "campo-tipomidia", "name": "tipoMidia", "type": "string", "value": "={{ $json.body.mediaType || $json.body.type || '' }}" }
            ]
          },
          "options": {}
        }
      }
    },
    {
      "type": "addNode",
      "node": {
        "id": "if-e-imagem",
        "name": "É mensagem de imagem?",
        "type": "n8n-nodes-base.if",
        "typeVersion": 2.3,
        "position": [-120, 0],
        "parameters": {
          "conditions": {
            "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
            "combinator": "and",
            "conditions": [
              {
                "id": "cond-tem-imagem",
                "leftValue": "={{ $json.imagemUrl }}",
                "rightValue": "",
                "operator": { "type": "string", "operation": "notEmpty" }
              }
            ]
          },
          "options": {}
        }
      }
    },
    {
      "type": "addNode",
      "node": {
        "id": "noop-nao-imagem",
        "name": "Não é imagem - encerra",
        "type": "n8n-nodes-base.noOp",
        "typeVersion": 1,
        "position": [120, 160],
        "parameters": {}
      }
    },
    { "type": "addConnection", "source": "Webhook - Foto Exame", "target": "Extrair campos do webhook" },
    { "type": "addConnection", "source": "Extrair campos do webhook", "target": "É mensagem de imagem?" },
    { "type": "addConnection", "source": "É mensagem de imagem?", "target": "Não é imagem - encerra", "sourceOutput": 1 }
  ]
}
  • Step 2: Validar

Chame mcp__n8n-mcp__n8n_validate_workflow com o id. Se a condição do IF acusar erro de operador (notEmpty não reconhecido nessa versão do n8n), ajuste com n8n_update_partial_workflow usando patchNodeField no node if-e-imagem, campo parameters.conditions.conditions[0].operator, testando { "type": "string", "operation": "exists" } como alternativa. Warnings sobre o branch verdadeiro do IF sem conexão são esperados até a Task 3 — não é erro.

  • Step 3: Salvar snapshot local e commitar

Repita o Step 3 da Task 1 (buscar workflow atualizado, sobrescrever n8n/orcamento-exames-imagem.workflow.json, git add + git commit -m "feat(n8n): extrai campos do webhook e filtra mensagens de imagem").


Task 3: Baixar a imagem

Files:

  • Modifica (no n8n): workflow (via n8n_update_partial_workflow).
  • Modify: n8n/orcamento-exames-imagem.workflow.json.

Interfaces:

  • Consumes: node É mensagem de imagem? (Task 2), branch verdadeiro; campo $json.imagemUrl.

  • Produces: node Baixar imagem (id baixar-imagem), que entrega o arquivo como dado binário (propriedade binária padrão data) para a Task 4. Em erro, usa onError: continueErrorOutput — a saída de erro (index 1) fica pendente de conexão até a Task 5.

  • Step 1: Adicionar o node e conectar

{
  "operations": [
    {
      "type": "addNode",
      "node": {
        "id": "baixar-imagem",
        "name": "Baixar imagem",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.4,
        "position": [120, -120],
        "onError": "continueErrorOutput",
        "parameters": {
          "method": "GET",
          "url": "={{ $json.imagemUrl }}",
          "options": {
            "response": { "response": { "responseFormat": "file" } },
            "timeout": 20000
          }
        }
      }
    },
    { "type": "addConnection", "source": "É mensagem de imagem?", "target": "Baixar imagem", "sourceOutput": 0 }
  ]
}
  • Step 2: Validar

mcp__n8n-mcp__n8n_validate_workflow com o id. Esperado: 0 errors.

  • Step 3: Salvar snapshot local e commitar

Mesmo procedimento das tasks anteriores. Mensagem de commit: "feat(n8n): baixa a imagem recebida do cliente".


Task 4: Credencial de IA + AI Agent com visão

Files:

  • Cria (no n8n): credencial OpenAI - Orçamento de Exames (via n8n_manage_credentials).
  • Modifica (no n8n): workflow (via n8n_update_partial_workflow).
  • Modify: n8n/orcamento-exames-imagem.workflow.json.

Interfaces:

  • Consumes: node Baixar imagem (Task 3) — o binário da imagem é repassado automaticamente ao Agent (passthroughBinaryImages: true).

  • Produces: node AI Agent - Orçamento (id ai-agent-orcamento), cuja saída de sucesso tem $json.output (texto do orçamento) e cuja saída de erro (index 1, onError: continueErrorOutput) fica pendente de conexão até a Task 5.

  • Step 1: Criar a credencial de OpenAI (placeholder)

Chame mcp__n8n-mcp__n8n_manage_credentials com:

{
  "action": "create",
  "name": "OpenAI - Orçamento de Exames",
  "type": "openAiApi",
  "data": { "apiKey": "COLOQUE_SUA_CHAVE_AQUI" }
}

Guarde o id retornado. Depois, abra essa credencial na interface do n8n (Credentials → OpenAI - Orçamento de Exames) e substitua COLOQUE_SUA_CHAVE_AQUI pela chave real da OpenAI — assim a chave nunca precisa ser digitada aqui na conversa, e pode ser trocada a qualquer momento sem tocar no workflow.

  • Step 2: Adicionar o OpenAI Chat Model e o AI Agent
{
  "operations": [
    {
      "type": "addNode",
      "node": {
        "id": "openai-chat-model-orcamento",
        "name": "OpenAI Chat Model - Orçamento",
        "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
        "typeVersion": 1.3,
        "position": [360, -280],
        "parameters": {
          "model": { "mode": "id", "value": "gpt-4o" },
          "options": {}
        },
        "credentials": {
          "openAiApi": { "id": "<id da credencial criada no Step 1>", "name": "OpenAI - Orçamento de Exames" }
        }
      }
    },
    {
      "type": "addNode",
      "node": {
        "id": "ai-agent-orcamento",
        "name": "AI Agent - Orçamento",
        "type": "@n8n/n8n-nodes-langchain.agent",
        "typeVersion": 3.1,
        "position": [360, -120],
        "onError": "continueErrorOutput",
        "parameters": {
          "promptType": "define",
          "text": "Segue a imagem do pedido de exames do cliente. Monte o orçamento conforme as instruções do system prompt.",
          "options": {
            "systemMessage": "Você é o assistente de orçamentos de exames de uma clínica, respondendo automaticamente por WhatsApp.\n\nTABELA DE EXAMES E VALORES (lista de exemplo — será substituída pela lista real depois):\n- Hemograma completo — R$ 35,00\n- Glicemia de jejum — R$ 20,00\n- Colesterol total e frações — R$ 45,00\n- Triglicerídeos — R$ 25,00\n- TSH (hormônio tireoidiano) — R$ 40,00\n- Exame de urina (EAS) — R$ 30,00\n- Ultrassonografia abdominal — R$ 150,00\n- Raio-X de tórax — R$ 90,00\n- Eletrocardiograma (ECG) — R$ 80,00\n- Papanicolau — R$ 60,00\n\nINSTRUÇÕES:\n1. Leia a imagem anexada, que é a foto de um pedido médico de exames (pode estar manuscrita ou impressa).\n2. Identifique cada exame solicitado na imagem.\n3. Para cada exame identificado que corresponde (mesmo com sinônimo, abreviação comum ou pequena diferença de grafia) a um item da tabela acima, inclua na resposta o nome do exame e o valor.\n4. Para exames identificados na imagem que NÃO correspondem a nenhum item da tabela, ou que você não conseguiu ler com confiança, liste-os separadamente avisando que um atendente vai confirmar o valor.\n5. Nunca deixe de responder por causa de um item não reconhecido — sempre orce o que for possível reconhecer.\n6. Calcule e mostre o valor total somando apenas os itens com preço confirmado na tabela.\n7. Responda em português, tom cordial e objetivo, formatado para WhatsApp: texto simples, sem markdown (nada de ** ou #), pode usar emojis com moderação e quebras de linha para organizar.\n8. Sua resposta deve conter SOMENTE o texto final a ser enviado ao cliente — nada de comentários extras, explicações do seu raciocínio, ou texto fora do orçamento.",
            "passthroughBinaryImages": true
          }
        }
      }
    },
    { "type": "addConnection", "source": "Baixar imagem", "target": "AI Agent - Orçamento", "sourceOutput": 0 },
    { "type": "addConnection", "source": "OpenAI Chat Model - Orçamento", "target": "AI Agent - Orçamento", "targetInput": "ai_languageModel" }
  ]
}
  • Step 3: Validar

mcp__n8n-mcp__n8n_validate_workflow com o id. Se houver erro sobre o formato do model ou da credencial, ajuste com patchNodeField conforme a mensagem retornada.

  • Step 4: Salvar snapshot local e commitar

Mesmo procedimento. Mensagem de commit: "feat(n8n): adiciona AI Agent com visao e tabela de precos no prompt".

Não inclua a chave real da OpenAI em nenhum arquivo commitado — o snapshot do workflow não contém segredos (credenciais ficam referenciadas só por id/name, o n8n nunca devolve o valor da chave nas consultas via API).


Task 5: Montar a resposta e enviar (com fallback de erro)

Files:

  • Modifica (no n8n): workflow (via n8n_update_partial_workflow).
  • Modify: n8n/orcamento-exames-imagem.workflow.json.

Interfaces:

  • Consumes:

    • node AI Agent - Orçamento (Task 4), saída de sucesso ($json.output) e saída de erro.
    • node Baixar imagem (Task 3), saída de erro.
    • node Extrair campos do webhook (Task 2), para reobter telefone independente do caminho (sucesso ou erro).
  • Produces: envio da resposta via sub-workflow existente MCP Server - Mensageria ZPRO - enviar_texto (id Y4ndG7GuIUrmVRxV) — fim do fluxo principal.

  • Step 1: Adicionar "Montar corpo da resposta" e "Enviar via Z-PRO", e ligar os três caminhos (sucesso + 2 erros)

{
  "operations": [
    {
      "type": "addNode",
      "node": {
        "id": "montar-corpo-resposta",
        "name": "Montar corpo da resposta",
        "type": "n8n-nodes-base.set",
        "typeVersion": 3.4,
        "position": [640, -120],
        "parameters": {
          "assignments": {
            "assignments": [
              { "id": "resp-number", "name": "number", "type": "string", "value": "={{ $('Extrair campos do webhook').item.json.telefone }}" },
              { "id": "resp-body", "name": "body", "type": "string", "value": "={{ $json.output ? $json.output : 'Recebemos sua solicitação de exames! Nossa equipe vai confirmar os valores e te retornar em breve.' }}" },
              { "id": "resp-externalkey", "name": "externalKey", "type": "string", "value": "={{ 'orcamento-' + $execution.id }}" },
              { "id": "resp-isclosed", "name": "isClosed", "type": "boolean", "value": false }
            ]
          },
          "options": {}
        }
      }
    },
    {
      "type": "addNode",
      "node": {
        "id": "enviar-orcamento-zpro",
        "name": "Enviar orçamento (Z-PRO)",
        "type": "n8n-nodes-base.executeWorkflow",
        "typeVersion": 1.3,
        "position": [900, -120],
        "parameters": {
          "workflowId": { "__rl": true, "mode": "id", "value": "Y4ndG7GuIUrmVRxV", "cachedResultName": "MCP Server - Mensageria ZPRO - enviar_texto" },
          "workflowInputs": {
            "mappingMode": "defineBelow",
            "value": {
              "number": "={{ $json.number }}",
              "body": "={{ $json.body }}",
              "externalKey": "={{ $json.externalKey }}",
              "isClosed": "={{ $json.isClosed }}"
            },
            "matchingColumns": [],
            "schema": [
              { "id": "number", "displayName": "number", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" },
              { "id": "body", "displayName": "body", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" },
              { "id": "externalKey", "displayName": "externalKey", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "string" },
              { "id": "isClosed", "displayName": "isClosed", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "type": "boolean" }
            ],
            "attemptToConvertTypes": false,
            "convertFieldsToString": false
          }
        }
      }
    },
    { "type": "addConnection", "source": "AI Agent - Orçamento", "target": "Montar corpo da resposta", "sourceOutput": 0 },
    { "type": "addConnection", "source": "AI Agent - Orçamento", "target": "Montar corpo da resposta", "sourceOutput": 1 },
    { "type": "addConnection", "source": "Baixar imagem", "target": "Montar corpo da resposta", "sourceOutput": 1 },
    { "type": "addConnection", "source": "Montar corpo da resposta", "target": "Enviar orçamento (Z-PRO)" }
  ]
}
  • Step 2: Validar

mcp__n8n-mcp__n8n_validate_workflow com o id, options.profile: "strict". Se o formato do workflowId (resourceLocator) não for reconhecido, ajuste com patchNodeField conforme a mensagem de erro retornada — o objetivo é 0 errors no final desta task (o workflow inteiro, ponta a ponta, deve validar limpo aqui).

  • Step 3: Salvar snapshot local e commitar

Mesmo procedimento. Mensagem de commit: "feat(n8n): monta resposta com fallback de erro e envia via sub-workflow Z-PRO".


Task 6: Teste real e ajuste dos campos do webhook

Esta task depende de uma ação do usuário (mandar uma foto real pelo WhatsApp) e não pode ser concluída sem ele — é o próprio objetivo de "testarmos" da spec.

  • Step 1: Obter a URL de teste do webhook

A URL de teste é https://devn8n.gtigrupo.com.br/webhook-test/orcamento-exames-zpro (POST). A URL de produção, usada depois de ativar o workflow, é https://devn8n.gtigrupo.com.br/webhook/orcamento-exames-zpro.

  • Step 2: Pedir para o usuário configurar o destino no Z-PRO

Peça para o usuário apontar esse endpoint como destino de mensagens do canal-alvo, no painel do Z-PRO (mesmo mecanismo que ele já usa para os outros bots dessa conta).

  • Step 3: Pedir um envio de teste

Peça para o usuário mandar, pelo WhatsApp, uma foto de um pedido de exames (pode ser uma foto de exemplo qualquer com nomes de exames legíveis) para o número configurado.

  • Step 4: Inspecionar a execução

Chame mcp__n8n-mcp__n8n_executions (list, filtrando pelo workflowId) para achar a execução mais recente, e leia o body recebido pelo node Webhook - Foto Exame.

  • Step 5: Corrigir os nomes de campo, se necessário

Compare o payload real com as suposições do node Extrair campos do webhook (Task 2: body.mediaUrl/body.fileUrl/body.file para a imagem, body.mediaType/body.type pro tipo). Se o campo real tiver outro nome, ajuste com:

{
  "operations": [
    {
      "type": "patchNodeField",
      "nodeId": "extrair-campos",
      "fieldPath": "parameters.assignments.assignments",
      "patches": [
        { "find": "$json.body.mediaUrl || $json.body.fileUrl || $json.body.file", "replace": "$json.body.<nome_real_do_campo>" }
      ]
    }
  ]
}
  • Step 6: Repetir o teste até a resposta chegar certa no WhatsApp

Repita os Steps 3–5 até o cliente de teste receber, no WhatsApp, o orçamento formatado com a lista de exemplo.

  • Step 7: Salvar snapshot final, commitar e ativar

Salve o JSON final em n8n/orcamento-exames-imagem.workflow.json, git commit -m "fix(n8n): ajusta campos do webhook conforme payload real do Z-PRO". Depois, com o usuário confirmando que está tudo certo, ative o workflow:

{ "operations": [ { "type": "activateWorkflow" } ] }

(via mcp__n8n-mcp__n8n_update_partial_workflow, mesmo id).

  • Step 8: Lembrar o usuário de trocar a lista de exemplo

Avise o usuário que a tabela de exames/valores no system message do node AI Agent - Orçamento ainda é a lista de exemplo (Task 4) — ele deve trocá-la pela lista real de exames/valores da clínica antes de usar em produção com clientes de verdade.