Snapshot: gti-scheduling-mcp (Z-PRO + Klingo adapters) e workflow n8n de agendamento funcionando de ponta a ponta
- gti-scheduling-mcp: MCP server com ZProAdapter e KlingoAdapter (agendar() completo pros dois) - Workflow n8n 'Agendamento - GChat - Z-PRO': webhook -> AI Agent -> MCP -> Z-PRO, testado com sucesso real - Documentacao: specs, plano, runbook de problemas reais e export do workflow Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
# Runbook — Workflow de Agendamento no n8n (GChat/Z-PRO + gti-scheduling-mcp)
|
||||
|
||||
> Registra problemas reais encontrados ao montar o workflow
|
||||
> `Agendamento - GChat - Z-PRO (clínica exemplo)` (id `yG2mi6ECBpPv4Hmn`, servidor
|
||||
> `srvdevn8ngti` / `devn8n.gtigrupo.com.br`), e como foram resolvidos — para não
|
||||
> repetir o mesmo processo de descoberta ao duplicar este workflow para uma nova
|
||||
> clínica. Ver também `docs/superpowers/specs/2026-08-10-n8n-agendamento-workflow-design.md`
|
||||
> (design) e `docs/superpowers/plans/2026-08-10-n8n-agendamento-workflow.md` (plano).
|
||||
|
||||
## 1. Node MCP: qual usar e como instalar
|
||||
|
||||
- O node **oficial** do n8n (`MCP Client Tool`, `@n8n/n8n-nodes-langchain`) só
|
||||
conecta em MCP remoto via HTTP Streamable/SSE — **não** roda comando local
|
||||
(stdio). Para rodar o `gti-scheduling-mcp` como subprocesso local, use o
|
||||
pacote **comunitário** `n8n-nodes-mcp` (autor `nerding-io`).
|
||||
- Instalação: **Settings → Community Nodes → Install a community node** →
|
||||
`n8n-nodes-mcp`.
|
||||
- Ao conectar como tool de um AI Agent, o **tipo do node no JSON do workflow**
|
||||
precisa ser `n8n-nodes-mcp.mcpClientTool` (não `n8n-nodes-mcp.mcpClient`) —
|
||||
é uma variante "Tool" que o n8n gera automaticamente para nodes marcados
|
||||
como usáveis em AI Agents. Usar `mcpClient` na conexão `ai_tool` falha a
|
||||
validação com "cannot output ai_tool connections".
|
||||
- É necessário setar a variável de ambiente do **processo do n8n**
|
||||
(não é configurável pela UI):
|
||||
```
|
||||
N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true
|
||||
```
|
||||
Nesse servidor, o n8n roda via **systemd** (`n8n.service`), com
|
||||
`EnvironmentFile=/opt/n8n.env`. A variável foi adicionada nesse arquivo,
|
||||
seguido de `systemctl restart n8n`. (Se o n8n rodasse em Docker, seria em
|
||||
`environment:` no `docker-compose.yml`.)
|
||||
|
||||
## 2. Credencial MCP (STDIO) — dois bugs reais
|
||||
|
||||
Tipo de credencial: `mcpClientApi` ("MCP Client (STDIO) API"), campos:
|
||||
`command`, `args`, `environments`.
|
||||
|
||||
- **Bug 1 — `command: "node"` sozinho falhou** com "Connection closed". O
|
||||
serviço systemd tem um `PATH` mais restrito que um shell interativo. Fix:
|
||||
usar o **caminho absoluto** (`which node` no servidor → `/usr/bin/node`).
|
||||
|
||||
- **Bug 2 — o campo "Environments" é um `<input>` de uma linha só (mascarado
|
||||
como senha) na UI do n8n.** O código real do node
|
||||
(`nodes/McpClient/McpClient.node.ts` no pacote `n8n-nodes-mcp`) espera o
|
||||
formato `NOME=VALOR` **separado por quebra de linha real** (`split('\n')`),
|
||||
mas navegadores removem quebras de linha ao digitar OU colar em campos de
|
||||
uma linha — então múltiplas variáveis nunca conseguem ser inseridas
|
||||
corretamente pela UI (tentamos: uma por linha, vírgula, espaço — todas
|
||||
colapsam a mesma forma, e a primeira variável acaba recebendo o resto do
|
||||
texto como valor, quebrando a validação).
|
||||
|
||||
**Fix**: criar/recriar a credencial via **API REST do n8n**, que aceita o
|
||||
JSON com `\n` de verdade (não passa pelo campo quebrado da UI):
|
||||
|
||||
```bash
|
||||
# 1. Gerar uma API key: Settings → n8n API → Create an API key
|
||||
# 2. DELETE na credencial antiga (se existir), depois POST na nova:
|
||||
curl -X POST "https://devn8n.gtigrupo.com.br/api/v1/credentials" \
|
||||
-H "X-N8N-API-KEY: <chave>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Client (STDIO) account",
|
||||
"type": "mcpClientApi",
|
||||
"data": {
|
||||
"command": "/usr/bin/node",
|
||||
"args": "/opt/gti-scheduling-mcp/dist/index.js",
|
||||
"environments": "SCHEDULING_PROVIDER=zpro\nZPRO_BASE_URL=https://...\nZPRO_API_TOKEN=...\nZPRO_API_ID=..."
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Sintoma quando esse bug está presente: o log do n8n
|
||||
(`journalctl -u n8n -f`) mostra exatamente a mensagem de erro do
|
||||
`gti-scheduling-mcp` (`Falha ao carregar a configuração: SCHEDULING_PROVIDER
|
||||
deve ser "zpro" ou "klingo".`), seguida de `MCP error -32000: Connection
|
||||
closed` — o processo sobe e cai na validação de config antes de completar o
|
||||
handshake stdio.
|
||||
|
||||
## 3. Deploy do `gti-scheduling-mcp` no servidor do n8n
|
||||
|
||||
O `node dist/index.js` executado pela credencial precisa estar no **mesmo
|
||||
host** onde o n8n roda (o node comunitário spawna o processo localmente).
|
||||
Processo usado (sem repositório git configurado ainda neste projeto):
|
||||
`tar` local (excluindo `node_modules`/`dist`) → `scp` pro servidor → `npm
|
||||
install && npm run build` lá. Publicado em `/opt/gti-scheduling-mcp`.
|
||||
|
||||
## 4. Modelo de IA (OpenAI Chat Model node) — gotchas de configuração
|
||||
|
||||
Mesmo usando um provider "compatível com OpenAI" (ex.: Z.ai/GLM), o node
|
||||
`OpenAI Chat Model` (`@n8n/n8n-nodes-langchain.lmChatOpenAi`, v1.3) tem
|
||||
comportamento por padrão que quebra com endpoints não-OpenAI:
|
||||
|
||||
- **`responsesApiEnabled` é `true` por padrão** nessa versão — usa a
|
||||
"Responses API" da OpenAI (`/responses`), um endpoint proprietário que
|
||||
providers OpenAI-compatíveis geralmente não implementam (eles só têm
|
||||
`/chat/completions`). Sintoma: `404 Not Found` /
|
||||
`MODEL_NOT_FOUND`. **Fix**: setar `responsesApiEnabled: false`
|
||||
explicitamente.
|
||||
- **Campo `model`** é um `resourceLocator` — precisa de `{mode: "id", value:
|
||||
"nome-do-modelo"}` explícito. Omitir `mode` pode fazer o n8n tentar listar
|
||||
modelos via uma chamada de API (`GET /models`) que o provider alternativo
|
||||
pode não suportar do mesmo jeito, gerando erro parecido com o anterior.
|
||||
- **Base URL da credencial**: o campo correto na credencial `openAiApi` é
|
||||
`url` (aparece como "Base URL" na UI, geralmente atrás de "Add Option").
|
||||
|
||||
## 5. Expressões em sub-nodes de IA (Memory/Tools) dentro do AI Agent
|
||||
|
||||
Dentro de nodes conectados via `ai_memory`/`ai_tool`/`ai_languageModel`
|
||||
(sub-nodes do AI Agent), referenciar um node upstream explicitamente
|
||||
(`={{ $('Normalizar entrada').item.json.sessionId }}`) falhou com "Key
|
||||
parameter is empty" — mesmo com o dado presente no item que chegou no AI
|
||||
Agent. **Fix**: usar `={{ $json.sessionId }}` (referência ao item atual, sem
|
||||
nome de node) — é o padrão que o próprio n8n usa por padrão nesse campo, e
|
||||
funciona porque sub-nodes de IA herdam o item do node pai (AI Agent) na
|
||||
execução.
|
||||
|
||||
## 6. Contrato do webhook do Z-PRO/GChat (verificado, não documentado
|
||||
oficialmente em lugar nenhum)
|
||||
|
||||
- Uma execução do workflow **por mensagem** do cliente — o Z-PRO não
|
||||
enfileira mensagens seguintes se o workflow não responder imediatamente.
|
||||
- Corpo recebido: `{step, entrada, number, contactName, protocol}` — `step`
|
||||
deve ser `"process"`; `protocol` é o identificador do **atendimento**
|
||||
(não do telefone).
|
||||
- Resposta esperada: **texto puro**, nunca JSON — node `Respond to Webhook`
|
||||
em modo `text`.
|
||||
|
||||
## 7. Z.ai/GLM como modelo temporário — limitação real de tool-calling
|
||||
|
||||
Usado como substituto temporário enquanto a chave da OpenAI não chegava.
|
||||
Dois endpoints testados:
|
||||
|
||||
- **`https://api.z.ai/api/coding/paas/v4`** ("GLM Coding Plan"): funciona
|
||||
bem para chat simples, mas é **restrito a ferramentas oficialmente
|
||||
suportadas** (Claude Code, Goose, etc. — ver
|
||||
`https://docs.z.ai/devpack/faq`). Ao anexar uma tool MCP genérica ao AI
|
||||
Agent, o modelo passou a devolver respostas **vazias silenciosamente**
|
||||
(sem erro) em qualquer mensagem, mesmo sem chamar a tool. Confirmado
|
||||
desabilitando a tool: a conversa voltou a funcionar normalmente.
|
||||
- **`https://api.z.ai/api/paas/v4/`** (endpoint geral, mesma conta): suporta
|
||||
tool-calling arbitrário segundo a documentação, mas apresentou timeout
|
||||
consistente nesta conta/plano (possivelmente saldo separado do "Coding
|
||||
Plan").
|
||||
- **Conclusão**: Z.ai/GLM não é uma substituição direta e confiável da
|
||||
OpenAI para um AI Agent com tools neste projeto. Recomendado usar a chave
|
||||
real da OpenAI (ou outro provider com suporte robusto e testado de
|
||||
function-calling) assim que disponível — só trocar a credencial e o
|
||||
`model`/`responsesApiEnabled` de volta, sem tocar no resto do workflow.
|
||||
|
||||
## 8. Tratamento de erro adicionado
|
||||
|
||||
`Normalizar entrada` e `AI Agent` têm `onError: "continueErrorOutput"`,
|
||||
ambos roteados para um node `Set` ("Erro - Resposta padrão") com uma
|
||||
mensagem fixa amigável, que alimenta o `Respond to Webhook`. Isso garante
|
||||
que o Z-PRO **sempre** recebe alguma resposta, mesmo se a normalização
|
||||
lançar uma exceção ou o AI Agent falhar (timeout, erro de modelo, etc.) —
|
||||
testado com um timeout real do endpoint geral do Z.ai, confirmado
|
||||
funcionando.
|
||||
|
||||
## 9. Bug crítico — `toolParameters` do node MCP nunca era preenchido pelo modelo
|
||||
|
||||
**Sintoma**: toda chamada de `agendar` falhava com
|
||||
`MCP error -32602: Input validation error: Invalid arguments for tool
|
||||
agendar: Required at clinicId, Required at startAt, Required at endAt,
|
||||
Required at patient.` — mesmo com o prompt do sistema explicando o formato
|
||||
esperado em detalhe.
|
||||
|
||||
**Causa raiz**: o node `n8n-nodes-mcp` (operação `executeTool`) tem um
|
||||
campo próprio **`toolParameters`** (JSON, obrigatório, valor padrão
|
||||
`'{}'`) que carrega os argumentos reais enviados à tool. Ele **não é
|
||||
preenchido automaticamente** só por a tool estar conectada a um AI Agent —
|
||||
é preciso configurá-lo explicitamente com o helper `$fromAI()` do n8n, que
|
||||
é o mecanismo real usado para "deixar o modelo decidir este valor em cada
|
||||
chamada". Sem isso, o campo fica travado no `{}` padrão, e nenhum valor
|
||||
que o modelo "pensa" que está enviando chega de fato ao MCP.
|
||||
|
||||
**Fix**: configurar `toolParameters` assim (mistura valores fixos vindos
|
||||
do próprio workflow com `$fromAI()` só para o que o modelo precisa mesmo
|
||||
decidir):
|
||||
|
||||
```
|
||||
={{ JSON.stringify({
|
||||
clinicId: $('Normalizar entrada').item.json.clinicId,
|
||||
startAt: $fromAI('startAt', 'Data e hora de início, ISO 8601 completo', 'string'),
|
||||
endAt: $fromAI('endAt', 'Data e hora de término, ISO 8601 completo', 'string'),
|
||||
patient: {
|
||||
name: $fromAI('patientName', 'Nome completo confirmado do paciente', 'string'),
|
||||
phone: $('Normalizar entrada').item.json.telefone
|
||||
}
|
||||
}) }}
|
||||
```
|
||||
|
||||
## 10. Bug crítico — `systemMessage` do AI Agent nunca era avaliado como expressão
|
||||
|
||||
**Sintoma**: um agendamento de teste foi criado com o campo de telefone
|
||||
contendo literalmente o texto `{{ $('Normalizar entrada').item.json.telefone
|
||||
}}` em vez de um número de telefone real.
|
||||
|
||||
**Causa raiz**: o campo `parameters.options.systemMessage` do node AI Agent
|
||||
só é avaliado como expressão do n8n se o **valor inteiro do campo** começar
|
||||
com `=`. Sem esse prefixo, tudo — inclusive trechos `{{ ... }}` escritos
|
||||
"como exemplo" dentro do texto — é enviado ao modelo como **texto literal**,
|
||||
e o modelo, sem saber que aquilo deveria virar um valor real, às vezes
|
||||
copia o placeholder ao pé da letra.
|
||||
|
||||
**Fix**: sempre prefixar o campo inteiro com `=` quando ele precisar
|
||||
referenciar dados reais do workflow (`={{ $('Normalizar entrada')... }}`).
|
||||
|
||||
**Consequência que isso causou (transparência total)**: dois agendamentos
|
||||
de teste fictícios ficaram duplicados no Z-PRO (`Consulta - TESTE-N8N
|
||||
Paciente Final`, mesma data/hora) antes desse bug ser identificado — uma
|
||||
chamada que o agente reportou como "erro" na verdade teve sucesso
|
||||
silenciosamente no lado do Z-PRO. Os testes fictícios foram apagados
|
||||
manualmente depois. Isso reforça por que a disciplina de "uma execução por
|
||||
vez, conferir visualmente antes de tentar de novo" existe em todos os
|
||||
testes reais deste projeto — mesmo seguindo ela, um bug de configuração
|
||||
ainda pode gerar um resultado inesperado, e só a conferência visual pega
|
||||
isso.
|
||||
|
||||
## 11. Bug — modelo não sabe a data de hoje
|
||||
|
||||
**Sintoma**: pedido de agendamento para "amanhã" resultou em uma data no
|
||||
passado (2023) sendo enviada ao Z-PRO.
|
||||
|
||||
**Causa raiz**: modelos de linguagem não têm acesso à data/hora real do
|
||||
momento da conversa — sem essa informação no prompt, o modelo "chuta" uma
|
||||
data (geralmente relacionada à data de corte do treinamento).
|
||||
|
||||
**Fix**: incluir a data/hora atual no `systemMessage` via expressão do
|
||||
próprio n8n:
|
||||
```
|
||||
A data de hoje é {{ $now.setZone('America/Sao_Paulo').toFormat('yyyy-MM-dd (cccc)') }}
|
||||
```
|
||||
|
||||
## 12. Bug — horário criado 3h adiantado/atrasado (fuso horário)
|
||||
|
||||
**Sintoma**: cliente pediu "sexta às 10h", o agendamento foi criado em produção
|
||||
às 7h — exatamente 3h a menos (o offset de America/Sao_Paulo, UTC-3).
|
||||
|
||||
**Causa raiz**: o exemplo de formato dado ao modelo usava sufixo `Z` (UTC)
|
||||
— ex. `2026-04-20T18:00:00.000Z`. O modelo tratou o horário local que o
|
||||
cliente pediu (10h) como se já fosse UTC, sem converter, e o Z-PRO (que
|
||||
aparentemente armazena/exibe em UTC) mostrou 10h UTC como 7h local.
|
||||
|
||||
**Fix**: instruir o modelo a **nunca usar `Z`/UTC** e sempre escrever o
|
||||
mesmo número de hora que o cliente pediu, com offset `-03:00` explícito
|
||||
(ex.: cliente pediu 10h → `2026-08-14T10:00:00-03:00`). Isso evita
|
||||
qualquer aritmética de fuso horário pelo modelo — ele só copia o número
|
||||
que o cliente já disse, sem calcular nada. Ajustado tanto na descrição do
|
||||
`$fromAI('startAt', ...)`/`$fromAI('endAt', ...)` quanto numa regra
|
||||
explícita no `systemMessage`.
|
||||
|
||||
**Se isso acontecer nas próximas clínicas**: o offset `-03:00` só está
|
||||
certo para fuso de Brasília. Se uma clínica futura estiver em outro fuso,
|
||||
ajustar o offset (e o `$now.setZone(...)` do prompt) de acordo.
|
||||
|
||||
## Status final (10/08/2026)
|
||||
|
||||
Testado de ponta a ponta com sucesso, com a chave real da OpenAI
|
||||
(`gpt-4o-mini`): telefone correto, data correta, um único agendamento por
|
||||
teste, confirmado visualmente no Z-PRO. Workflow deixado **desativado**
|
||||
propositalmente — só ativar quando o Z-PRO de uma clínica real for
|
||||
configurado para apontar pra esse webhook.
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"name": "Agendamento - GChat - Z-PRO (clínica exemplo)",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "agendamento-zpro-clinica-exemplo",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "webhook-zpro",
|
||||
"name": "Webhook Z-PRO",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2.1,
|
||||
"position": [-208, -176]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "text",
|
||||
"responseBody": "={{ $json.output }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "respond-stub",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.5,
|
||||
"position": [224, 48]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "const body = $input.item.json.body || {};\n\n// Configuração fixa desta cópia do workflow. Ajustar por clínica ao\n// duplicar este workflow para uma nova instância.\nconst clinicConfig = {\n provider: \"zpro\",\n clinicId: \"clinica-exemplo\",\n};\n\nif (body.step !== \"process\") {\n return {\n json: {\n earlyReply: true,\n sessionId: body.protocol || \"\",\n mensagem: \"\",\n nomeContato: \"\",\n telefone: \"\",\n provider: clinicConfig.provider,\n clinicId: clinicConfig.clinicId,\n },\n };\n}\n\nreturn {\n json: {\n earlyReply: false,\n sessionId: body.protocol || \"\",\n mensagem: String(body.entrada || \"\").trim(),\n nomeContato: body.contactName || \"\",\n telefone: body.number || \"\",\n provider: clinicConfig.provider,\n clinicId: clinicConfig.clinicId,\n },\n};"
|
||||
},
|
||||
"id": "normalizar-entrada",
|
||||
"name": "Normalizar entrada",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [-16, -176],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"combinator": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cond-early-reply",
|
||||
"leftValue": "={{ $json.earlyReply }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "if-early-reply",
|
||||
"name": "É step=process?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.3,
|
||||
"position": [240, -336]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"model": { "mode": "id", "value": "gpt-4o-mini" },
|
||||
"builtInTools": {},
|
||||
"options": {}
|
||||
},
|
||||
"id": "openai-chat-model",
|
||||
"name": "OpenAI Chat Model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.3,
|
||||
"position": [512, -64],
|
||||
"credentials": {
|
||||
"openAiApi": { "id": "PRECISA_RECRIAR_NO_SERVIDOR_NOVO", "name": "GPT" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"sessionIdType": "customKey",
|
||||
"sessionKey": "={{ $json.sessionId }}",
|
||||
"contextWindowLength": 20
|
||||
},
|
||||
"id": "memoria-sessao",
|
||||
"name": "Memória da Sessão",
|
||||
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
|
||||
"typeVersion": 1.4,
|
||||
"position": [672, -96]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.mensagem }}",
|
||||
"options": {
|
||||
"systemMessage": "=Você é o assistente de agendamento da clínica, conversando por WhatsApp. A data de hoje é {{ $now.setZone('America/Sao_Paulo').toFormat('yyyy-MM-dd (cccc)') }}, fuso America/Sao_Paulo — use isso pra calcular qualquer data relativa (\"amanhã\", \"sexta que vem\", etc.), nunca assuma outro ano. clinicId e telefone do paciente já são preenchidos automaticamente pelo sistema — você só precisa decidir data/hora e confirmar o nome completo.\n\nRegras:\n1. O nome de contato do WhatsApp é \"{{ $('Normalizar entrada').item.json.nomeContato }}\" e o telefone é \"{{ $('Normalizar entrada').item.json.telefone }}\" — mas SEMPRE confirme o nome completo do paciente com o cliente antes de agendar, mesmo que já tenha o nome de contato.\n2. Pergunte a data e o horário desejados para a consulta, e opcionalmente o motivo. O horário que o cliente disser (ex.: \"10h\") é sempre horário local de Brasília (America/Sao_Paulo, UTC-3) — use esse mesmo número de hora ao montar startAt/endAt, com offset -03:00 (nunca converta pra UTC/Z, nunca some ou subtraia horas).\n3. Nunca invente informação que o cliente não deu.\n4. Responda sempre em português, em tom cordial e objetivo, adequado para WhatsApp (mensagens curtas).\n5. Quando tiver o nome completo confirmado e a data/hora desejada, você pode agendar de verdade.\n6. NUNCA agende mais de uma vez na mesma conversa — se o cliente já confirmou um agendamento nesta conversa, não agende de novo; informe que o agendamento já foi feito.\n7. Se o agendamento falhar, explique o problema ao cliente em linguagem simples e pergunte se ele quer tentar de novo com outros dados, ou avise que vai encaminhar para um atendente."
|
||||
}
|
||||
},
|
||||
"id": "agente-agendamento",
|
||||
"name": "AI Agent",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"position": [560, -320],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeTool",
|
||||
"toolName": "agendar",
|
||||
"toolParameters": "={{ JSON.stringify({ clinicId: $('Normalizar entrada').item.json.clinicId, startAt: $fromAI('startAt', 'Data e hora de inicio da consulta, no horario local pedido pelo cliente, em ISO 8601 com offset -03:00 (America/Sao_Paulo) -- NUNCA use Z/UTC. Exemplo: cliente pediu 10h -> 2026-08-14T10:00:00-03:00', 'string'), endAt: $fromAI('endAt', 'Data e hora de termino da consulta, mesmo formato do startAt (offset -03:00, nunca Z/UTC), geralmente 30 minutos apos o startAt', 'string'), patient: { name: $fromAI('patientName', 'Nome completo do paciente, ja confirmado com o cliente na conversa', 'string'), phone: $('Normalizar entrada').item.json.telefone } }) }}"
|
||||
},
|
||||
"id": "mcp-tool-agendar",
|
||||
"name": "MCP - gti-scheduling-mcp",
|
||||
"type": "n8n-nodes-mcp.mcpClientTool",
|
||||
"typeVersion": 1,
|
||||
"position": [1024, -128],
|
||||
"credentials": {
|
||||
"mcpClientApi": { "id": "PRECISA_RECRIAR_NO_SERVIDOR_NOVO", "name": "Client (STDIO) account" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "erro-output",
|
||||
"name": "output",
|
||||
"type": "string",
|
||||
"value": "Desculpe, tive um problema técnico agora e não consegui continuar. Pode tentar de novo em alguns minutos, ou aguardar que alguém da nossa equipe vai te atender."
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "erro-resposta-padrao",
|
||||
"name": "Erro - Resposta padrão",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.5,
|
||||
"position": [240, -176]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook Z-PRO": {
|
||||
"main": [[{ "node": "Normalizar entrada", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Normalizar entrada": {
|
||||
"main": [
|
||||
[{ "node": "É step=process?", "type": "main", "index": 0 }],
|
||||
[{ "node": "Erro - Resposta padrão", "type": "main", "index": 0 }]
|
||||
]
|
||||
},
|
||||
"OpenAI Chat Model": {
|
||||
"ai_languageModel": [[{ "node": "AI Agent", "type": "ai_languageModel", "index": 0 }]]
|
||||
},
|
||||
"Memória da Sessão": {
|
||||
"ai_memory": [[{ "node": "AI Agent", "type": "ai_memory", "index": 0 }]]
|
||||
},
|
||||
"É step=process?": {
|
||||
"main": [
|
||||
[],
|
||||
[{ "node": "AI Agent", "type": "main", "index": 0 }]
|
||||
]
|
||||
},
|
||||
"AI Agent": {
|
||||
"main": [
|
||||
[{ "node": "Respond to Webhook", "type": "main", "index": 0 }],
|
||||
[{ "node": "Erro - Resposta padrão", "type": "main", "index": 0 }]
|
||||
]
|
||||
},
|
||||
"MCP - gti-scheduling-mcp": {
|
||||
"ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]]
|
||||
},
|
||||
"Erro - Resposta padrão": {
|
||||
"main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
# Workflow n8n de Agendamento (GChat/Z-PRO → gti-scheduling-mcp) 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.
|
||||
|
||||
**Goal:** Construir, no n8n (`https://devn8n.gtigrupo.com.br`), o primeiro workflow que recebe mensagens de clientes via webhook do Z-PRO/GChat, conduz a conversa com um AI Agent para capturar os dados do agendamento, e chama a tool `agendar` do `gti-scheduling-mcp` (via `ZProAdapter`) para realizar o agendamento de fato.
|
||||
|
||||
**Architecture:** Webhook (Z-PRO) → node `Code` de normalização/guarda de `step` → `AI Agent` (OpenAI + memória de sessão por `protocol` + tool MCP `agendar`) → `Respond to Webhook` (texto puro). Construído incrementalmente: cada task adiciona uma camada e é validada isoladamente antes da próxima.
|
||||
|
||||
**Tech Stack:** n8n (self-hosted, `devn8n.gtigrupo.com.br`), node `n8n-nodes-mcp` (community, transporte stdio), `gti-scheduling-mcp` (já implementado, sem alterações de código nesta etapa), OpenAI Chat Model.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Nenhuma alteração de código/arquitetura no `gti-scheduling-mcp` é necessária ou permitida nesta etapa — ele já expõe a tool `agendar` exatamente como o workflow precisa consumir.
|
||||
- O webhook precisa **sempre responder de imediato**, uma execução por mensagem — nunca segurar a resposta esperando mais mensagens (Z-PRO descarta mensagens se isso acontecer).
|
||||
- A resposta ao Z-PRO é **sempre texto puro**, nunca JSON.
|
||||
- Nenhuma credencial (token do Z-PRO, API key da OpenAI) é inserida por quem executa este plano de forma automática — todas vêm de Credentials do n8n, criadas manualmente pelo usuário na UI.
|
||||
- `sessionId` da memória do agente = `protocol` do payload do Z-PRO (nunca o telefone — o mesmo telefone pode ter vários atendimentos/protocolos).
|
||||
- Escopo: só `agendar()` via `ZProAdapter`. Nada de `consultar_horarios`, cancelamento, remarcação ou múltiplas clínicas nesta etapa (ver spec, seção "Fora de escopo").
|
||||
|
||||
Spec de referência: `docs/superpowers/specs/2026-08-10-n8n-agendamento-workflow-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Pré-requisitos (ação manual do usuário — fora do escopo de execução automática)
|
||||
|
||||
Estes itens **não podem ser feitos pelas tasks abaixo** (exigem acesso à infraestrutura do n8n/credenciais reais) e precisam estar prontos antes da Task 5:
|
||||
|
||||
1. **Instalar o node comunitário `n8n-nodes-mcp`** no n8n de vocês, seguindo
|
||||
`https://docs.n8n.io/integrations/community-nodes/installation/`.
|
||||
2. **Definir a variável de ambiente `N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true`**
|
||||
no processo do n8n (ex.: no `docker-compose.yml` do serviço n8n) e reiniciar o n8n — sem isso, o node comunitário não pode ser usado como tool dentro do AI Agent.
|
||||
3. **Criar uma Credential do tipo OpenAI** no n8n (Settings → Credentials → OpenAI API), com uma API key válida.
|
||||
4. **Criar uma Credential do tipo "MCP Client (Command Line) API"** (fornecida pelo node `n8n-nodes-mcp` após instalado), com:
|
||||
- **Command**: `node`
|
||||
- **Arguments**: caminho absoluto para `dist/index.js` do `gti-scheduling-mcp` no servidor onde o n8n roda (ex.: `/opt/gti-scheduling-mcp/dist/index.js`) — depende de onde o projeto for implantado, que é uma decisão de infraestrutura fora do escopo deste plano.
|
||||
- **Environment Variables** (uma por linha, formato `NOME=valor`):
|
||||
```
|
||||
SCHEDULING_PROVIDER=zpro
|
||||
ZPRO_BASE_URL=<url real da clínica>
|
||||
ZPRO_API_TOKEN=<token real da clínica>
|
||||
ZPRO_API_ID=<ApiID real da clínica — o GUID, não o número do registro>
|
||||
```
|
||||
|
||||
Sem os 4 itens acima, a Task 5 (conectar a tool MCP ao Agent) não pode ser validada de ponta a ponta — as Tasks 1 a 4 não dependem deles.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Build do `gti-scheduling-mcp` e localização do `dist/index.js`
|
||||
|
||||
**Files:**
|
||||
- Nenhum arquivo novo — apenas build do projeto existente.
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: caminho absoluto de `dist/index.js`, usado nas Tasks 5 e no pré-requisito 4 acima.
|
||||
|
||||
- [ ] **Step 1: Rodar o build**
|
||||
|
||||
```bash
|
||||
cd "D:\PROJETOS\KLINGO-CLINICAL-SYSLIFE-GCHAT-AGENDAMENTO\gti-scheduling-mcp"
|
||||
npm run build
|
||||
```
|
||||
|
||||
Esperado: `tsc -p tsconfig.json` sem erros, e a pasta `dist/` criada/atualizada.
|
||||
|
||||
- [ ] **Step 2: Confirmar que `dist/index.js` existe e roda**
|
||||
|
||||
```bash
|
||||
node "D:\PROJETOS\KLINGO-CLINICAL-SYSLIFE-GCHAT-AGENDAMENTO\gti-scheduling-mcp\dist\index.js" --help 2>&1 | head -5
|
||||
```
|
||||
|
||||
Esperado: o processo inicia (mesmo que não reconheça `--help`, não deve dar erro de módulo não encontrado). Se o servidor esperar variáveis de ambiente obrigatórias e falhar por causa disso, isso confirma que o build está correto (o processo rodou, só não tinha config — comportamento esperado sem env vars).
|
||||
|
||||
Anote o caminho absoluto de `dist/index.js` — será necessário nos pré-requisitos e na Task 5.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Esqueleto base do webhook (Webhook → Respond to Webhook)
|
||||
|
||||
**Files:** Nenhum arquivo local — cria o workflow no n8n via `n8n_create_workflow`.
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `workflowId` (retornado pela chamada) — usado em todas as tasks seguintes.
|
||||
|
||||
- [ ] **Step 1: Criar o workflow com dois nodes ligados diretamente**
|
||||
|
||||
Chame a tool `n8n_create_workflow` com:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Agendamento - GChat - Z-PRO (clínica exemplo)",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "webhook-zpro",
|
||||
"name": "Webhook Z-PRO",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2.1,
|
||||
"position": [0, 0],
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "agendamento-zpro-clinica-exemplo",
|
||||
"responseMode": "responseNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "respond-stub",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.5,
|
||||
"position": [300, 0],
|
||||
"parameters": {
|
||||
"respondWith": "text",
|
||||
"responseBody": "teste ok"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook Z-PRO": {
|
||||
"main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Guarde o `id` do workflow retornado — chamado de `<workflowId>` no restante deste plano.
|
||||
|
||||
- [ ] **Step 2: Validar o workflow**
|
||||
|
||||
Chame `n8n_validate_workflow` com `{"id": "<workflowId>"}`. Esperado: sem erros (pode haver avisos sobre o workflow estar inativo — normal, só ativamos na Task 6).
|
||||
|
||||
- [ ] **Step 3: Ativar temporariamente e testar a resposta em texto puro**
|
||||
|
||||
Ative o workflow (`n8n_update_partial_workflow` com operação `{"type": "activateWorkflow"}`), pegue a URL de produção do webhook (visível no node Webhook Z-PRO / na resposta da criação), e chame:
|
||||
|
||||
```bash
|
||||
curl -i -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"step":"process","entrada":"oi","number":"5511999999999","contactName":"Teste","protocol":"protocolo-teste-1"}'
|
||||
```
|
||||
|
||||
Esperado: HTTP 200, corpo **exatamente** `teste ok` (texto puro, sem aspas de JSON ao redor).
|
||||
|
||||
- [ ] **Step 4: Desativar o workflow de novo**
|
||||
|
||||
`n8n_update_partial_workflow` com `{"type": "deactivateWorkflow"}` — mantemos desativado até a Task 6, para não responder de verdade a mensagens reais enquanto ainda está incompleto.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Node de normalização + guarda de `step`
|
||||
|
||||
**Files:** Nenhum arquivo local — edita o workflow `<workflowId>` via `n8n_update_partial_workflow`.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `<workflowId>` da Task 2.
|
||||
- Produces: campos `sessionId`, `mensagem`, `nomeContato`, `telefone`, `provider`, `clinicId`, `earlyReply` — consumidos pela Task 4 (AI Agent) e pelo `If` desta task.
|
||||
|
||||
- [ ] **Step 1: Adicionar o node `Code` de normalização**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"id": "normalizar-entrada",
|
||||
"name": "Normalizar entrada",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [150, -120],
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"language": "javaScript",
|
||||
"jsCode": "const body = $input.item.json.body || {};\n\n// Configuração fixa desta cópia do workflow. Ajustar por clínica ao\n// duplicar este workflow para uma nova instância.\nconst clinicConfig = {\n provider: \"zpro\",\n clinicId: \"clinica-exemplo\",\n};\n\nif (body.step !== \"process\") {\n return {\n json: {\n earlyReply: true,\n sessionId: body.protocol || \"\",\n mensagem: \"\",\n nomeContato: \"\",\n telefone: \"\",\n provider: clinicConfig.provider,\n clinicId: clinicConfig.clinicId,\n },\n };\n}\n\nreturn {\n json: {\n earlyReply: false,\n sessionId: body.protocol || \"\",\n mensagem: String(body.entrada || \"\").trim(),\n nomeContato: body.contactName || \"\",\n telefone: body.number || \"\",\n provider: clinicConfig.provider,\n clinicId: clinicConfig.clinicId,\n },\n};"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Adicionar o node `If` que decide entre resposta antecipada e fluxo normal**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"id": "if-early-reply",
|
||||
"name": "É step=process?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.3,
|
||||
"position": [400, -120],
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" },
|
||||
"combinator": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cond-early-reply",
|
||||
"leftValue": "={{ $json.earlyReply }}",
|
||||
"rightValue": true,
|
||||
"operator": { "type": "boolean", "operation": "true", "singleValue": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Rewire — Webhook → Normalizar entrada → If**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "type": "removeConnection", "source": "Webhook Z-PRO", "target": "Respond to Webhook" },
|
||||
{ "type": "addConnection", "source": "Webhook Z-PRO", "target": "Normalizar entrada" },
|
||||
{ "type": "addConnection", "source": "Normalizar entrada", "target": "É step=process?" }
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Ligar a saída TRUE do If (índice 0) direto ao Respond to Webhook existente, com texto vazio**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "type": "patchNodeField", "nodeName": "Respond to Webhook", "fieldPath": "parameters.responseBody", "patches": [{ "find": "teste ok", "replace": "={{ $json.mensagem || '' }}" }] },
|
||||
{ "type": "addConnection", "source": "É step=process?", "sourceOutput": 0, "target": "Respond to Webhook" }
|
||||
]
|
||||
```
|
||||
|
||||
A saída FALSE (índice 1) do `If` fica sem conexão por enquanto — será ligada ao AI Agent na Task 4.
|
||||
|
||||
- [ ] **Step 5: Validar**
|
||||
|
||||
`n8n_validate_workflow` com `{"id": "<workflowId>"}`. Esperado: sem erros. Pode haver aviso de node com saída não conectada (`É step=process?` saída FALSE) — esperado nesta task, resolvido na Task 4.
|
||||
|
||||
- [ ] **Step 6: Testar os dois caminhos**
|
||||
|
||||
Ative o workflow, chame o webhook duas vezes:
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"step":"outro","entrada":"x","number":"1","contactName":"y","protocol":"p1"}'
|
||||
```
|
||||
|
||||
Esperado: corpo vazio (early reply, `step` diferente de `process`).
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"step":"process","entrada":"Olá, quero marcar uma consulta","number":"5511999999999","contactName":"Fulano","protocol":"p2"}'
|
||||
```
|
||||
|
||||
Esperado: corpo = `Olá, quero marcar uma consulta` (o `Respond to Webhook` está ecoando `mensagem` só para confirmar que os campos normalizados chegaram certos — será substituído pela resposta do Agent na Task 4).
|
||||
|
||||
Desative o workflow ao final (`deactivateWorkflow`), como na Task 2.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: AI Agent com memória de sessão (sem tool ainda)
|
||||
|
||||
**Files:** Nenhum arquivo local — edita `<workflowId>`.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `earlyReply` (falso), `sessionId`, `mensagem`, `nomeContato`, `telefone` da Task 3.
|
||||
- Produces: texto de resposta do Agent, consumido pelo `Respond to Webhook`.
|
||||
|
||||
- [ ] **Step 1: Adicionar o node OpenAI Chat Model**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"id": "openai-chat-model",
|
||||
"name": "OpenAI Chat Model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1,
|
||||
"position": [650, 100],
|
||||
"parameters": {
|
||||
"model": { "value": "gpt-4o-mini" },
|
||||
"options": {}
|
||||
},
|
||||
"credentials": {
|
||||
"openAiApi": { "id": "<ID da Credential OpenAI criada no pré-requisito 3>", "name": "OpenAI account" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Substitua `<ID da Credential OpenAI criada no pré-requisito 3>` pelo ID real da credencial (visível na UI do n8n, em Settings → Credentials, ou via `n8n_manage_credentials` se disponível). Se preferir, este campo pode ser deixado para o usuário selecionar manualmente na UI do n8n após a criação do node — não bloqueia os steps seguintes.
|
||||
|
||||
- [ ] **Step 2: Adicionar o node Simple Memory**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"id": "memoria-sessao",
|
||||
"name": "Memória da Sessão",
|
||||
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
|
||||
"typeVersion": 1.4,
|
||||
"position": [650, 250],
|
||||
"parameters": {
|
||||
"sessionIdType": "customKey",
|
||||
"sessionKey": "={{ $('Normalizar entrada').item.json.sessionId }}",
|
||||
"contextWindowLength": 20
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Adicionar o node AI Agent**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"id": "agente-agendamento",
|
||||
"name": "AI Agent",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"position": [650, -120],
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.mensagem }}",
|
||||
"options": {
|
||||
"systemMessage": "Você é o assistente de agendamento da clínica, conversando por WhatsApp. Regras:\n1. O nome de contato do WhatsApp é \"{{ $('Normalizar entrada').item.json.nomeContato }}\" e o telefone é \"{{ $('Normalizar entrada').item.json.telefone }}\" — mas SEMPRE confirme o nome completo do paciente com o cliente antes de agendar, mesmo que já tenha o nome de contato.\n2. Pergunte a data e o horário desejados para a consulta, e opcionalmente o motivo.\n3. Nunca invente informação que o cliente não deu.\n4. Responda sempre em português, em tom cordial e objetivo, adequado para WhatsApp (mensagens curtas).\n5. Quando tiver o nome completo confirmado e a data/hora desejada, você pode agendar de verdade.\n6. NUNCA agende mais de uma vez na mesma conversa — se o cliente já confirmou um agendamento nesta conversa, não agende de novo; informe que o agendamento já foi feito.\n7. Se o agendamento falhar, explique o problema ao cliente em linguagem simples e pergunte se ele quer tentar de novo com outros dados, ou avise que vai encaminhar para um atendente."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Conectar Chat Model e Memory ao Agent, e a saída FALSE do If ao Agent**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "type": "addConnection", "source": "OpenAI Chat Model", "sourceOutput": "ai_languageModel", "target": "AI Agent", "targetInput": "ai_languageModel" },
|
||||
{ "type": "addConnection", "source": "Memória da Sessão", "sourceOutput": "ai_memory", "target": "AI Agent", "targetInput": "ai_memory" },
|
||||
{ "type": "addConnection", "source": "É step=process?", "sourceOutput": 1, "target": "AI Agent" },
|
||||
{ "type": "addConnection", "source": "AI Agent", "target": "Respond to Webhook" }
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Ajustar o Respond to Webhook para usar a saída do Agent**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "patchNodeField",
|
||||
"nodeName": "Respond to Webhook",
|
||||
"fieldPath": "parameters.responseBody",
|
||||
"patches": [{ "find": "={{ $json.mensagem || '' }}", "replace": "={{ $json.output }}" }]
|
||||
}
|
||||
```
|
||||
|
||||
(`$json.output` é o campo de texto padrão que o node AI Agent produz.)
|
||||
|
||||
- [ ] **Step 6: Validar**
|
||||
|
||||
`n8n_validate_workflow` com `{"id": "<workflowId>"}`. Esperado: sem erros. Se aparecer erro sobre a credencial OpenAI ausente/inválida, ajuste o `credentials.openAiApi.id` (Step 1) ou peça para o usuário selecionar a credencial manualmente na UI antes de prosseguir.
|
||||
|
||||
- [ ] **Step 7: Testar a memória de sessão**
|
||||
|
||||
Ative o workflow. Envie duas mensagens com o **mesmo** `protocol`:
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"step":"process","entrada":"Meu nome completo é João da Silva Teste","number":"5511999999999","contactName":"João","protocol":"p-memoria-1"}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"step":"process","entrada":"Qual é o meu nome completo que eu te falei?","number":"5511999999999","contactName":"João","protocol":"p-memoria-1"}'
|
||||
```
|
||||
|
||||
Esperado: a segunda resposta menciona "João da Silva Teste" — confirma que a memória por `sessionId=protocol` está funcionando entre execuções separadas do webhook.
|
||||
|
||||
Desative o workflow ao final.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Conectar a tool MCP `agendar` ao Agent
|
||||
|
||||
**Pré-requisito:** os 4 itens da seção "Pré-requisitos" no topo deste plano precisam estar prontos (node comunitário instalado, env var setada e n8n reiniciado, credenciais OpenAI e MCP criadas).
|
||||
|
||||
**Files:** Nenhum arquivo local — edita `<workflowId>`.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: credencial MCP stdio criada no pré-requisito 4.
|
||||
- Produces: capacidade do Agent de chamar `agendar` de verdade.
|
||||
|
||||
- [ ] **Step 1: Adicionar o node MCP Client Tool**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"id": "mcp-tool-agendar",
|
||||
"name": "MCP - gti-scheduling-mcp",
|
||||
"type": "n8n-nodes-mcp.mcpClientTool",
|
||||
"typeVersion": 1,
|
||||
"position": [900, 250],
|
||||
"parameters": {
|
||||
"connectionType": "cmd",
|
||||
"include": "selected",
|
||||
"includeTools": ["agendar"]
|
||||
},
|
||||
"credentials": {
|
||||
"mcpClientCmdApi": { "id": "<ID da Credential MCP stdio criada no pré-requisito 4>", "name": "gti-scheduling-mcp (stdio)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Os nomes exatos do tipo de credencial (`mcpClientCmdApi`) e da propriedade `connectionType` podem variar conforme a versão instalada do pacote `n8n-nodes-mcp` — se a validação do Step 3 apontar um nome de campo diferente, ajuste conforme o que a própria UI do n8n mostrar ao adicionar o node manualmente (Settings do node → aba Credentials).
|
||||
|
||||
- [ ] **Step 2: Conectar o node como tool do Agent**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "MCP - gti-scheduling-mcp",
|
||||
"sourceOutput": "ai_tool",
|
||||
"target": "AI Agent",
|
||||
"targetInput": "ai_tool"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Validar**
|
||||
|
||||
`n8n_validate_workflow` com `{"id": "<workflowId>", "options": {"profile": "ai-friendly"}}`. Corrija qualquer erro de nome de campo/credencial apontado (ver nota no Step 1).
|
||||
|
||||
- [ ] **Step 4: Teste manual controlado, com dados fictícios, seguindo a mesma disciplina de segurança contra duplicidade do teste do `ZProAdapter`**
|
||||
|
||||
Ative o workflow. Envie, com um `protocol` novo e único:
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://devn8n.gtigrupo.com.br/webhook/agendamento-zpro-clinica-exemplo" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"step":"process","entrada":"Oi, quero marcar uma consulta pra amanhã às 14h, meu nome completo é TESTE-N8N Paciente","number":"5511900000001","contactName":"Teste N8N","protocol":"p-e2e-1"}'
|
||||
```
|
||||
|
||||
Continue a conversa (repetindo a chamada com o mesmo `protocol`, mudando só `entrada`) até o Agent confirmar o agendamento. **Antes de cada nova tentativa, se algo parecer ter falhado, confira visualmente no Z-PRO se o agendamento já foi criado** (mesma regra usada no teste manual do `ZProAdapter`) — não repita mensagens de confirmação "às cegas".
|
||||
|
||||
Confirme visualmente no Z-PRO que o agendamento fictício foi criado.
|
||||
|
||||
- [ ] **Step 5: Desativar o workflow ao final do teste**
|
||||
|
||||
`n8n_update_partial_workflow` com `{"type": "deactivateWorkflow"}` — só reativar definitivamente depois que o usuário revisar o resultado (Task 6).
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Revisão final e ativação
|
||||
|
||||
**Files:** Nenhum arquivo local.
|
||||
|
||||
- [ ] **Step 1: Validação final completa**
|
||||
|
||||
`n8n_validate_workflow` com `{"id": "<workflowId>", "options": {"profile": "strict"}}`. Resolver qualquer erro/aviso restante.
|
||||
|
||||
- [ ] **Step 2: Apresentar o resumo ao usuário**
|
||||
|
||||
Antes de ativar definitivamente, mostrar ao usuário: URL do webhook de produção, nome do workflow, e pedir confirmação de que o Z-PRO já está configurado para apontar o fluxo de agendamento desta clínica para essa URL.
|
||||
|
||||
- [ ] **Step 3: Ativar**
|
||||
|
||||
Só ativar (`activateWorkflow`) depois da confirmação explícita do usuário no Step 2 — ativar aqui significa que mensagens reais de clientes passam a ser processadas.
|
||||
|
||||
---
|
||||
|
||||
## Observação sobre "commit"
|
||||
|
||||
Nem `D:\PROJETOS\KLINGO-CLINICAL-SYSLIFE-GCHAT-AGENDAMENTO` nem `gti-scheduling-mcp` são repositórios git — não há passo de `git commit` neste plano. O estado do workflow vive inteiramente no n8n; cada task é validada via `n8n_validate_workflow` e teste funcional (chamada real ao webhook), no lugar de testes automatizados de código.
|
||||
@@ -0,0 +1,164 @@
|
||||
# Design — Workflow n8n de Agendamento via GChat/Z-PRO + gti-scheduling-mcp
|
||||
|
||||
**Data:** 2026-08-10
|
||||
**Status:** Aprovado pelo usuário, aguardando plano de implementação.
|
||||
|
||||
## Objetivo
|
||||
|
||||
Construir o primeiro workflow n8n que recebe mensagens de clientes via
|
||||
GChat (webhook do Z-PRO), conduz a conversa com um agente de IA para
|
||||
capturar os dados necessários e realiza o agendamento de fato, chamando a
|
||||
tool `agendar` do `gti-scheduling-mcp` (que por sua vez delega ao Adapter
|
||||
do sistema da clínica — Z-PRO neste MVP).
|
||||
|
||||
Escopo do MVP: **só agendamento** (`agendar`). Consulta de disponibilidade,
|
||||
cancelamento e remarcação ficam fora desta etapa.
|
||||
|
||||
## Contexto verificado (não inventado)
|
||||
|
||||
Estes fatos vêm de um workflow real já em produção no n8n de referência
|
||||
("Enforcer - GLPI"/fluxo de suporte via Z-PRO, código do node `1 - Decide`
|
||||
compartilhado pelo usuário) e de consultas diretas ao n8n
|
||||
(`devn8n.gtigrupo.com.br`) e ao registro de nodes via `n8n-mcp`:
|
||||
|
||||
- O Z-PRO chama o webhook do n8n **uma vez por mensagem do cliente** — não
|
||||
agrupa/enfileira mensagens.
|
||||
- O corpo da requisição (`req = $json.body`) tem os campos: `step`
|
||||
(esperado `"process"`), `entrada` (texto da mensagem do cliente),
|
||||
`number` (telefone), `contactName`, `protocol` (identificador único do
|
||||
**atendimento**, não do telefone — o mesmo cliente pode ter vários
|
||||
atendimentos/protocolos ao longo do tempo).
|
||||
- **Se o workflow segurar a resposta** (esperando mais mensagens antes de
|
||||
responder), o Z-PRO **descarta** as mensagens seguintes do cliente
|
||||
(confirmado em teste real: 5 mensagens em sequência → 1 execução no
|
||||
n8n). O workflow precisa responder a cada execução, sempre.
|
||||
- A resposta ao Z-PRO é **texto puro** (não JSON), via node `Respond to
|
||||
Webhook` em modo texto.
|
||||
- O node oficial `MCP Client Tool` (`@n8n/n8n-nodes-langchain`) só suporta
|
||||
transporte HTTP Streamable/SSE — **não** suporta rodar um comando local
|
||||
via stdio.
|
||||
- O node comunitário `n8n-nodes-mcp` (`mcpClient`/`mcpClientTool`) suporta
|
||||
conexão via linha de comando (stdio) — é o caminho escolhido para não
|
||||
precisar alterar o transporte do `gti-scheduling-mcp` (que hoje só
|
||||
implementa `StdioServerTransport`).
|
||||
- O `gti-scheduling-mcp` (`ZProAdapter`/`KlingoAdapter`) já está pronto e
|
||||
testado para `agendar()` — nenhuma mudança de arquitetura nele é
|
||||
necessária para este workflow.
|
||||
|
||||
## Arquitetura
|
||||
|
||||
```
|
||||
[Cliente WhatsApp]
|
||||
│
|
||||
▼
|
||||
[Z-PRO/GChat] ──webhook──▶ [n8n: Webhook] → [Normalizar entrada] → [AI Agent] → [Respond to Webhook]
|
||||
│ ▲
|
||||
(tool call) │ │ (memória de sessão)
|
||||
▼ │
|
||||
[MCP Client Tool] [Simple Memory]
|
||||
(stdio, agendar) (sessionId = protocol)
|
||||
│
|
||||
▼
|
||||
[gti-scheduling-mcp: tool "agendar"]
|
||||
│
|
||||
▼
|
||||
[AdapterFactory → ZProAdapter/KlingoAdapter]
|
||||
│
|
||||
▼
|
||||
[Sistema real da clínica: Z-PRO/Klingo]
|
||||
```
|
||||
|
||||
O `gti-scheduling-mcp` não sofre nenhuma alteração de arquitetura para
|
||||
este workflow — o n8n é só mais um cliente MCP, como qualquer outro.
|
||||
|
||||
## Componentes (nós do n8n)
|
||||
|
||||
1. **Webhook (Z-PRO)** — `POST`, recebe `{step, entrada, number,
|
||||
contactName, protocol}`.
|
||||
2. **Normalizar entrada** (`Set`/`Code`) —
|
||||
- Valida `step === "process"`; caso contrário, responde com um texto
|
||||
mínimo e encerra (mesma defesa do fluxo de referência).
|
||||
- Monta as variáveis usadas a seguir: `sessionId = protocol`,
|
||||
`mensagem = entrada`, `nomeContato = contactName`, `telefone =
|
||||
number`.
|
||||
- **Nó de configuração da clínica** (fixo por cópia do workflow, não
|
||||
dinâmico em runtime): `provider` (ex.: `"zpro"`), `clinicId`, e
|
||||
referência de qual Credential do n8n / config do MCP Client Tool
|
||||
usar para esta clínica. Cada clínica nova = uma cópia deste workflow
|
||||
com esses valores ajustados e seu próprio webhook cadastrado no
|
||||
Z-PRO — não há branching dinâmico de provider dentro de uma mesma
|
||||
execução.
|
||||
3. **AI Agent** (`@n8n/n8n-nodes-langchain.agent`) —
|
||||
- Chat Model: OpenAI (credencial a configurar no n8n).
|
||||
- System prompt cobre: confirmar o nome completo do paciente (mesmo já
|
||||
tendo `contactName`), perguntar data/horário desejado, motivo
|
||||
(opcional), nunca chamar `agendar` mais de uma vez por atendimento,
|
||||
responder sempre em português, tom cordial, nunca inventar dado que
|
||||
o cliente não informou.
|
||||
- **Memória**: `Simple Memory` (`memoryBufferWindow`), `sessionId =
|
||||
protocol`. Trade-off aceito para o MVP: essa memória vive na RAM do
|
||||
processo do n8n — um restart do n8n no meio de um atendimento perde
|
||||
o histórico da conversa. Evolução futura, se necessário: Postgres ou
|
||||
Redis Chat Memory (durável, mas exige infraestrutura adicional).
|
||||
- **Tool**: `MCP Client Tool` (via `n8n-nodes-mcp`, transporte stdio),
|
||||
expondo a tool `agendar` do `gti-scheduling-mcp` (comando: `node
|
||||
dist/index.js`, variáveis de ambiente = credenciais da clínica,
|
||||
vindas de uma Credential do n8n — nunca hardcoded no node/JSON do
|
||||
workflow).
|
||||
4. **Respond to Webhook** — modo texto simples; devolve só a resposta em
|
||||
linguagem natural produzida pelo AI Agent.
|
||||
|
||||
## Tratamento de erro
|
||||
|
||||
Quando `agendar` falha (`VALIDATION_ERROR`, `AUTHENTICATION_ERROR`,
|
||||
`NOT_FOUND`, `ZPRO_API_ERROR`/`KLINGO_API_ERROR`, timeout, etc.), o MCP já
|
||||
devolve uma resposta estruturada (`success:false` + código/mensagem) — o
|
||||
AI Agent recebe isso como resultado da tool e decide, em linguagem
|
||||
natural, como comunicar isso ao cliente (pedir os dados de novo, avisar
|
||||
que vai encaminhar para um atendente humano, etc.), dentro do que o
|
||||
system prompt permitir. Não há tratamento hard-coded por código de erro
|
||||
neste workflow — isso fica deliberadamente a cargo do agente, para manter
|
||||
o MVP simples.
|
||||
|
||||
## Risco de agendamento duplicado (mitigação no nível do workflow)
|
||||
|
||||
O `ZProAdapter`/`KlingoAdapter` já trata qualquer HTTP 2xx do provider
|
||||
como sucesso operacional (sem risco de duplicidade por reinterpretação de
|
||||
resposta ambígua — ver `docs/zpro-api-mapping.md`). O risco adicional
|
||||
neste workflow é de outra natureza: o **próprio agente** decidir chamar a
|
||||
tool `agendar` mais de uma vez na mesma conversa (ex.: o cliente confirma
|
||||
duas vezes, ou o agente se confunde). Mitigação adotada para o MVP:
|
||||
instrução explícita no system prompt para nunca chamar `agendar` mais de
|
||||
uma vez por atendimento (`protocol`). Isto é uma mitigação por prompt, não
|
||||
uma garantia determinística — se isso se mostrar insuficiente em uso real,
|
||||
a evolução natural é adicionar uma marcação de "já agendado" no estado da
|
||||
sessão, verificada por código antes de permitir uma nova chamada da tool.
|
||||
|
||||
## Fora de escopo (explicitamente adiado)
|
||||
|
||||
- Consulta de disponibilidade (`consultar_horarios`) — não implementada
|
||||
para nenhum provider neste MVP (Z-PRO não suporta; Klingo tem lacuna de
|
||||
mapeamento de resposta).
|
||||
- Cancelamento/remarcação de agendamento.
|
||||
- Transferência automática de fila/atendimento humano no Z-PRO após o
|
||||
agendamento (padrão existente no fluxo de referência via
|
||||
`updateticketinfo`) — pode ser adicionado depois, não é necessário para
|
||||
o MVP funcionar.
|
||||
- Suporte a múltiplas clínicas com providers diferentes atendidas por uma
|
||||
única execução/webhook compartilhado — cada clínica é uma cópia
|
||||
dedicada do workflow.
|
||||
- Memória de conversa durável entre restarts do n8n (Postgres/Redis Chat
|
||||
Memory).
|
||||
|
||||
## Testes
|
||||
|
||||
Este workflow vive no n8n, fora do repositório `gti-scheduling-mcp` — não
|
||||
há testes automatizados de código aqui. A validação é funcional/manual:
|
||||
|
||||
- Testar o fluxo completo com uma conversa real (ou simulada via chamada
|
||||
direta ao webhook) contra o Z-PRO, com dados fictícios, seguindo a mesma
|
||||
disciplina de segurança contra duplicidade já usada nos testes manuais
|
||||
do `ZProAdapter` (execução única, verificação visual no Z-PRO antes de
|
||||
qualquer nova tentativa).
|
||||
- Confirmar que o `Respond to Webhook` devolve texto puro, sem envelope
|
||||
JSON, compatível com o que o Z-PRO espera exibir ao cliente.
|
||||
Reference in New Issue
Block a user