fix(n8n): torna o fluxo conversacional (texto e/ou imagem, com memoria de sessao)

O fluxo anterior encerrava sem responder quando a mensagem nao tinha imagem,
o que nao reflete um atendimento real (cliente manda texto antes/depois, ou
digita os exames em vez de mandar foto). Agora toda mensagem chega ao AI
Agent (com ou sem imagem), com memoria de conversa via Postgres, seguindo o
mesmo padrao do bot de agendamento ja usado nessa conta.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Eric
2026-08-21 09:03:16 -03:00
co-authored by Claude Sonnet 5
parent e1ea3dee53
commit 772747d74e
2 changed files with 89 additions and 15 deletions
@@ -164,6 +164,53 @@ Webhook (Z-PRO)
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. Isso sugere que a URL foi configurada num campo de
webhook diferente do usado pelo ChatFlow desses bots (que é o que gera esse
formato JSON). Ainda em aberto — precisa confirmar com o usuário onde
exatamente a URL foi cadastrada no painel do Z-PRO, e ajustar pro mesmo
mecanismo usado pelos bots que já funcionam (ChatFlow → passo "Webhook").
## Fora de escopo (por enquanto)
- Fila/aprovação humana antes do envio (usuário optou por envio automático).
+42 -15
View File
@@ -1,7 +1,7 @@
{
"id": "6FU0jjPwEKDBrFSW",
"name": "Orçamento de Exames - GChat - Z-PRO (IA Visão)",
"active": false,
"active": true,
"nodes": [
{
"id": "webhook-orcamento",
@@ -51,6 +51,12 @@
"type": "string",
"value": "={{ $json.body.contactName }}"
},
{
"id": "campo-entrada",
"name": "entrada",
"type": "string",
"value": "={{ String($json.body.entrada || $json.body.text || '').trim() }}"
},
{
"id": "campo-imagemurl",
"name": "imagemUrl",
@@ -102,17 +108,6 @@
"options": {}
}
},
{
"id": "noop-nao-imagem",
"name": "Não é imagem - encerra",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
120,
160
],
"parameters": {}
},
{
"id": "baixar-imagem",
"name": "Baixar imagem",
@@ -170,9 +165,9 @@
],
"parameters": {
"promptType": "define",
"text": "Segue a imagem do pedido de exames do cliente. Monte o orçamento conforme as instruções do system prompt.",
"text": "={{ $('Extrair campos do webhook').item.json.entrada || 'O cliente enviou uma imagem (sem texto). Veja a imagem anexada, que é o pedido médico dele.' }}",
"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- Triglicérideos — 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 orçe 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.",
"systemMessage": "Você é o assistente de orçamentos de exames de uma clínica, atendendo automaticamente pelo WhatsApp, numa conversa com várias mensagens (não só uma).\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- Triglicérideos — 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\nCOMO CONVERSAR:\n1. O cliente pode mandar uma foto do pedido médico (com ou sem legenda), digitar os nomes dos exames, ou fazer as duas coisas em mensagens separadas, em qualquer ordem — use também o histórico da conversa (mensagens anteriores) pra juntar as informações, não só a mensagem atual. Uma imagem anexada a esta mensagem (quando houver) é o pedido médico do cliente.\n2. Se o cliente ainda não mandou, nem agora nem antes nesta conversa, nenhuma foto de pedido nem nomes de exames, NÃO invente orçamento — cumprimente rapidamente e peça pra ele mandar a foto do pedido médico ou digitar os nomes dos exames.\n3. Assim que tiver pelo menos um exame identificável (considerando a imagem atual, o texto atual E o histórico da conversa), identifique cada exame pedido.\n4. 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.\n5. Para exames identificados que NÃO correspondem a nenhum item da tabela, ou que você não conseguiu ler/entender com confiança, liste-os separadamente avisando que um atendente vai confirmar o valor.\n6. Nunca deixe de orçar por causa de um item não reconhecido — sempre orçe o que for possível reconhecer.\n7. Calcule e mostre o valor total somando apenas os itens com preço confirmado na tabela.\n8. Responda sempre 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.\n9. Sua resposta deve conter SOMENTE o texto a ser enviado ao cliente nesta mensagem — nada de comentários extras, explicações do seu raciocínio, ou texto fora da conversa com o cliente.",
"passthroughBinaryImages": true
}
},
@@ -286,6 +281,27 @@
"convertFieldsToString": false
}
}
},
{
"id": "memoria-sessao-orcamento",
"name": "Memória da Sessão (Postgres)",
"type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
"typeVersion": 1.4,
"position": [
360,
40
],
"parameters": {
"sessionIdType": "customKey",
"sessionKey": "={{ 'orcamento-' + ($('Extrair campos do webhook').item.json.sessionId || $('Extrair campos do webhook').item.json.telefone) }}",
"contextWindowLength": 20
},
"credentials": {
"postgres": {
"id": "xq3QAN3MZSOCVgdY",
"name": "Postgres Agendamento - (clínica exemplo)"
}
}
}
],
"connections": {
@@ -322,7 +338,7 @@
],
[
{
"node": "Não é imagem - encerra",
"node": "AI Agent - Orçamento",
"type": "main",
"index": 0
}
@@ -386,6 +402,17 @@
}
]
]
},
"Memória da Sessão (Postgres)": {
"ai_memory": [
[
{
"node": "AI Agent - Orçamento",
"type": "ai_memory",
"index": 0
}
]
]
}
},
"settings": {