{
  "name": "Oliveira \u2014 Bot WhatsApp IA (INSS / IR / Precat\u00f3rio / D\u00edvidas)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "oliveira-whatsapp",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "b0000001-0001-4000-8000-000000000001",
      "name": "Webhook \u2014 Entrada WhatsApp",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        400
      ],
      "notes": "Configure este webhook na Evolution API como destino das mensagens recebidas. Endpoint: /webhook/oliveira-whatsapp"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Parse do payload Evolution API\nconst body = $input.first().json;\n\n// Ignora mensagens enviadas pelo bot ou de grupos\nconst data = body.data || {};\nconst key = data.key || {};\nif (key.fromMe === true) return [];\nif ((key.remoteJid || '').includes('@g.us')) return [];\n\n// Extrai campos\nconst telefone = (key.remoteJid || '').replace('@s.whatsapp.net', '');\nconst pushName  = data.pushName || 'Cliente';\nconst primeiroNome = pushName.split(' ')[0];\n\n// Suporte a texto simples e mensagem de lista/bot\u00e3o\nconst msg = data.message || {};\nconst texto = (\n  msg.conversation ||\n  msg.extendedTextMessage?.text ||\n  msg.listResponseMessage?.singleSelectReply?.selectedRowId ||\n  msg.buttonsResponseMessage?.selectedDisplayText ||\n  ''\n).trim();\n\nif (!texto) return []; // ignora \u00e1udio, imagem, etc sem legenda\n\nreturn [{ json: { telefone, pushName, primeiroNome, texto, timestamp: Date.now() } }];"
      },
      "id": "b0000001-0002-4000-8000-000000000002",
      "name": "Code \u2014 Extrair Mensagem",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        400
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Upsert lead pelo telefone e retorna dados\nINSERT INTO leads (nome, whatsapp, area_interesse, status)\nVALUES ('{{ $json.pushName }}', '{{ $json.telefone }}', 'Outro', 'Em Atendimento')\nON CONFLICT (whatsapp) DO UPDATE SET\n  nome = CASE WHEN leads.nome = 'Cliente' THEN EXCLUDED.nome ELSE leads.nome END,\n  status = 'Em Atendimento',\n  data_atualizacao = NOW()\nRETURNING id, nome, whatsapp, area_interesse::text AS area;",
        "options": {}
      },
      "id": "b0000001-0003-4000-8000-000000000003",
      "name": "Postgres \u2014 Upsert Lead",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        680,
        400
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Busca ou cria sess\u00e3o ativa\nINSERT INTO sessoes (lead_id, tipo, historico_json, data_inicio)\nSELECT\n  (SELECT id FROM leads WHERE whatsapp = '{{ $('Code \u2014 Extrair Mensagem').first().json.telefone }}'),\n  'WhatsApp',\n  '[]'::jsonb,\n  NOW()\nWHERE NOT EXISTS (\n  SELECT 1 FROM sessoes\n  WHERE lead_id = (SELECT id FROM leads WHERE whatsapp = '{{ $('Code \u2014 Extrair Mensagem').first().json.telefone }}')\n  AND data_fim IS NULL\n  AND data_inicio > NOW() - INTERVAL '4 hours'\n)\nRETURNING id;\n\n-- Retorna hist\u00f3rico da sess\u00e3o ativa\nSELECT s.id AS sessao_id, s.historico_json\nFROM sessoes s\nJOIN leads l ON l.id = s.lead_id\nWHERE l.whatsapp = '{{ $('Code \u2014 Extrair Mensagem').first().json.telefone }}'\n  AND s.data_fim IS NULL\n  AND s.data_inicio > NOW() - INTERVAL '4 hours'\nORDER BY s.data_inicio DESC\nLIMIT 1;",
        "options": {}
      },
      "id": "b0000001-0004-4000-8000-000000000004",
      "name": "Postgres \u2014 Sess\u00e3o",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        900,
        400
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Busca conhecimento jur\u00eddico relevante por palavras-chave (fallback sem embedding)\n-- Para busca vetorial real, configure a chamada de embedding antes deste n\u00f3\nSELECT titulo, conteudo, area\nFROM conhecimento_juridico\nWHERE\n  conteudo ILIKE '%{{ $('Code \u2014 Extrair Mensagem').first().json.texto.replace(/'/g, \"''\").substring(0, 50) }}%'\n  OR area::text ILIKE ANY(ARRAY[\n    CASE WHEN '{{ $('Code \u2014 Extrair Mensagem').first().json.texto }}' ILIKE '%inss%' OR '{{ $('Code \u2014 Extrair Mensagem').first().json.texto }}' ILIKE '%aposentadoria%' OR '{{ $('Code \u2014 Extrair Mensagem').first().json.texto }}' ILIKE '%benef\u00edcio%' OR '{{ $('Code \u2014 Extrair Mensagem').first().json.texto }}' ILIKE '%previd\u00eancia%' THEN '%INSS%' ELSE NULL END,\n    CASE WHEN '{{ $('Code \u2014 Extrair Mensagem').first().json.texto }}' ILIKE '%d\u00edvida%' OR '{{ $('Code \u2014 Extrair Mensagem').first().json.texto }}' ILIKE '%d\u00e9bito%' OR '{{ $('Code \u2014 Extrair Mensagem').first().json.texto }}' ILIKE '%cobran\u00e7a%' OR '{{ $('Code \u2014 Extrair Mensagem').first().json.texto }}' ILIKE '%negativado%' THEN '%D\u00edvidas%' ELSE NULL END\n  ])\nLIMIT 3;",
        "options": {}
      },
      "id": "b0000001-0005-4000-8000-000000000005",
      "name": "Postgres \u2014 Buscar Conhecimento",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1120,
        400
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "notes": "Busca b\u00e1sica por keyword. Para RAG vetorial completo, adicione um n\u00f3 antes que chama a API de embedding (Gemini/OpenAI) e use a query: SELECT titulo, conteudo FROM conhecimento_juridico ORDER BY embedding <=> $1::vector LIMIT 3"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Monta o contexto completo para a IA\nconst msg     = $('Code \u2014 Extrair Mensagem').first().json;\nconst sessao  = $('Postgres \u2014 Sess\u00e3o').first()?.json || {};\nconst conhecs = $('Postgres \u2014 Buscar Conhecimento').all().map(i => i.json);\n\n// Hist\u00f3rico de conversa (\u00faltimas 10 trocas)\nlet historico = [];\ntry {\n  historico = JSON.parse(sessao.historico_json || '[]').slice(-10);\n} catch(e) {}\n\n// Base de conhecimento encontrada\nlet contextoCon = '';\nif (conhecs.length > 0) {\n  contextoCon = '\\n\\n\ud83d\udcda BASE DE CONHECIMENTO RELEVANTE:\\n';\n  conhecs.forEach(k => {\n    contextoCon += `\\n--- ${k.titulo} ---\\n${k.conteudo}\\n`;\n  });\n}\n\nconst systemPrompt = `Voc\u00ea \u00e9 o assistente virtual da Oliveira Assessoria Jur\u00eddica.\nSeu nome \u00e9 OLI. Voc\u00ea \u00e9 especialista em assessoria (n\u00e3o advocacia) nas seguintes \u00e1reas:\n\n1. **INSS e Previd\u00eancia Social**\n   - Aposentadoria por idade (homem: 65 anos / mulher: 62 anos + m\u00ednimo de contribui\u00e7\u00f5es)\n   - Aposentadoria por tempo de contribui\u00e7\u00e3o (regras de transi\u00e7\u00e3o p\u00f3s-reforma 2019)\n   - Aux\u00edlio-doen\u00e7a (incapacidade tempor\u00e1ria, 15 dias de car\u00eancia)\n   - BPC/LOAS (renda per capita < 1/4 do sal\u00e1rio m\u00ednimo, pessoa com defici\u00eancia ou idoso +65)\n   - Sal\u00e1rio-maternidade (CLT: 120 dias; rural e contribuinte individual: 120 dias com 10 contribui\u00e7\u00f5es)\n   - Pens\u00e3o por morte, aux\u00edlio-acidente\n   - Como consultar o CNIS (extrato do INSS): meu.inss.gov.br\n   - Prazo para recurso: 30 dias ap\u00f3s notifica\u00e7\u00e3o\n\n2. **Precat\u00f3rios**\n   - O que \u00e9: d\u00edvida judicial do governo (Uni\u00e3o, Estado, Munic\u00edpio) reconhecida por senten\u00e7a\n   - Como verificar: consultaprecatorio.cnj.jus.br ou site do TRF/TJ da regi\u00e3o\n   - Precat\u00f3rios alimentares t\u00eam prioridade de pagamento\n   - RPV (Requisi\u00e7\u00e3o de Pequeno Valor): valores menores, pagos mais r\u00e1pido\n   - Cess\u00e3o de precat\u00f3rio: \u00e9 poss\u00edvel vender o direito a terceiros com des\u00e1gio\n   - Prazo constitucional: at\u00e9 31/12 do exerc\u00edcio seguinte ao que foi inclu\u00eddo no or\u00e7amento\n\n3. **Imposto de Renda**\n   - Obrigado a declarar (ano-base 2024): rendimentos > R$ 30.639,90 ou bens > R$ 800.000\n   - Documentos: informe de rendimentos, extrato de investimentos, recibos de despesas m\u00e9dicas/educa\u00e7\u00e3o, comprovante de im\u00f3veis/ve\u00edculos\n   - Dedu\u00e7\u00f5es: dependentes (R$ 2.275,08/ano), educa\u00e7\u00e3o (limite R$ 3.561,50), sa\u00fade (ilimitado com nota fiscal)\n   - Malha fina: diverg\u00eancia de dados, omiss\u00e3o de rendimentos, dedu\u00e7\u00f5es sem comprova\u00e7\u00e3o\n   - Restitui\u00e7\u00e3o: depende do imposto pago na fonte e das dedu\u00e7\u00f5es; prioridade idosos, deficientes, professores\n   - Prazo de entrega: geralmente at\u00e9 30/04 do ano seguinte\n   - Multa por atraso: m\u00ednimo R$ 165,74, m\u00e1ximo 20% do imposto devido\n\n4. **Negocia\u00e7\u00e3o de D\u00edvidas**\n   - Prescri\u00e7\u00e3o: d\u00edvidas banc\u00e1rias = 5 anos (CC Art. 206, \u00a75\u00ba, I); CDC = 5 anos; trabalhista = 2 anos\n   - Direitos do devedor: CDC Art. 42 pro\u00edbe cobran\u00e7a vexat\u00f3ria, ass\u00e9dio, amea\u00e7a\n   - Negativa\u00e7\u00e3o indevida gera dano moral (STJ S\u00famula 385)\n   - Serasa/SPC: credor tem at\u00e9 5 dias para comunicar devedor antes de negativar\n   - Acordos: descontos de at\u00e9 90% s\u00e3o poss\u00edveis em d\u00edvidas antigas\n   - FGTS digitado pode ser usado para quitar d\u00edvidas (Lei 14.181/2021)\n\nREGRAS IMPORTANTES:\n- Voc\u00ea faz ASSESSORIA, n\u00e3o advocacia. N\u00e3o represente o cliente em ju\u00edzo.\n- Para a\u00e7\u00f5es judiciais, oriente a buscar um advogado.\n- Seja OBJETIVO e CLARO. Use linguagem simples, sem juridiqu\u00eas.\n- Use emojis com modera\u00e7\u00e3o para ser mais amig\u00e1vel.\n- Ao identificar a \u00e1rea do cliente, fa\u00e7a perguntas pontuais para coletar as informa\u00e7\u00f5es necess\u00e1rias.\n- Ao ter informa\u00e7\u00f5es suficientes, entregue um PLANO DE A\u00c7\u00c3O numerado e claro.\n- Oriente sempre sobre os pr\u00f3ximos passos pr\u00e1ticos.\n- Se n\u00e3o souber algo com certeza, diga que vai verificar e pe\u00e7a para o cliente aguardar contato da equipe.\n- Limite respostas a no m\u00e1ximo 350 palavras para n\u00e3o sobrecarregar o WhatsApp.\n${contextoCon}`;\n\nconst messages = [\n  { role: 'system', content: systemPrompt },\n  ...historico,\n  { role: 'user', content: msg.texto }\n];\n\nreturn [{ json: {\n  messages,\n  sessaoId: sessao.sessao_id || null,\n  telefone: msg.telefone,\n  textoUsuario: msg.texto,\n  historico\n}}];"
      },
      "id": "b0000001-0006-4000-8000-000000000006",
      "name": "Code \u2014 Montar Contexto IA",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1340,
        400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer SUA_OPENAI_KEY"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"gpt-4o-mini\",\n  \"messages\": {{ JSON.stringify($json.messages) }},\n  \"max_tokens\": 700,\n  \"temperature\": 0.4\n}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "b0000001-0007-4000-8000-000000000007",
      "name": "HTTP \u2014 Chamar OpenAI",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1560,
        400
      ],
      "notes": "Substitua SUA_OPENAI_KEY pela sua chave da OpenAI. Pode trocar por Claude (Anthropic) ou qualquer API compat\u00edvel com o formato OpenAI Chat Completions."
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Extrai a resposta da IA e atualiza o hist\u00f3rico\nconst ctx = $('Code \u2014 Montar Contexto IA').first().json;\nconst aiResp = $input.first().json;\n\nconst respostaIA = aiResp.choices?.[0]?.message?.content || 'Desculpe, tive um problema t\u00e9cnico. Nossa equipe vai te contatar em breve!';\n\n// Atualiza hist\u00f3rico (mant\u00e9m m\u00e1ximo 20 mensagens = 10 trocas)\nconst historicoAtualizado = [\n  ...ctx.historico,\n  { role: 'user', content: ctx.textoUsuario },\n  { role: 'assistant', content: respostaIA }\n].slice(-20);\n\nreturn [{ json: {\n  telefone: ctx.telefone,\n  sessaoId: ctx.sessaoId,\n  respostaIA,\n  historicoAtualizado\n}}];"
      },
      "id": "b0000001-0008-4000-8000-000000000008",
      "name": "Code \u2014 Extrair Resposta IA",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1780,
        400
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE sessoes SET\n  historico_json = '{{ $json.historicoAtualizado.map(m => JSON.stringify(m)).join(',') }}'::jsonb,\n  resumo_ia = '{{ $json.respostaIA.replace(/'/g, \"''\").substring(0, 500) }}'\nWHERE id = {{ $json.sessaoId || 0 }};",
        "options": {}
      },
      "id": "b0000001-0009-4000-8000-000000000009",
      "name": "Postgres \u2014 Salvar Hist\u00f3rico",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        2000,
        400
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "notes": "Salva o hist\u00f3rico da conversa na tabela sessoes para manter contexto entre mensagens"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://SUA_EVOLUTION_API/message/sendText/NOME_DA_INSTANCIA",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "SUA_EVOLUTION_APIKEY"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "number",
              "value": "={{ $json.telefone }}"
            },
            {
              "name": "text",
              "value": "={{ $json.respostaIA }}"
            }
          ]
        },
        "options": {
          "timeout": 15000
        }
      },
      "id": "b0000001-0010-4000-8000-000000000010",
      "name": "HTTP \u2014 Enviar Resposta WhatsApp",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2220,
        400
      ],
      "notes": "Substitua SUA_EVOLUTION_API e SUA_APIKEY_EVOLUTION"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={ \"ok\": true }",
        "options": {
          "responseCode": 200
        }
      },
      "id": "b0000001-0011-4000-8000-000000000011",
      "name": "Resposta \u2014 200 OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2440,
        400
      ]
    }
  ],
  "connections": {
    "Webhook \u2014 Entrada WhatsApp": {
      "main": [
        [
          {
            "node": "Code \u2014 Extrair Mensagem",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Extrair Mensagem": {
      "main": [
        [
          {
            "node": "Postgres \u2014 Upsert Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres \u2014 Upsert Lead": {
      "main": [
        [
          {
            "node": "Postgres \u2014 Sess\u00e3o",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres \u2014 Sess\u00e3o": {
      "main": [
        [
          {
            "node": "Postgres \u2014 Buscar Conhecimento",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres \u2014 Buscar Conhecimento": {
      "main": [
        [
          {
            "node": "Code \u2014 Montar Contexto IA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Montar Contexto IA": {
      "main": [
        [
          {
            "node": "HTTP \u2014 Chamar OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP \u2014 Chamar OpenAI": {
      "main": [
        [
          {
            "node": "Code \u2014 Extrair Resposta IA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Extrair Resposta IA": {
      "main": [
        [
          {
            "node": "Postgres \u2014 Salvar Hist\u00f3rico",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres \u2014 Salvar Hist\u00f3rico": {
      "main": [
        [
          {
            "node": "HTTP \u2014 Enviar Resposta WhatsApp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP \u2014 Enviar Resposta WhatsApp": {
      "main": [
        [
          {
            "node": "Resposta \u2014 200 OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "id": "oliveira-workflow-02",
  "meta": {
    "description": "Bot WhatsApp com IA para atendimento de INSS, Precat\u00f3rio, Imposto de Renda e Negocia\u00e7\u00e3o de D\u00edvidas. Mant\u00e9m hist\u00f3rico de conversa por sess\u00e3o. Usa OpenAI GPT-4o-mini + base de conhecimento jur\u00eddico do banco.",
    "fluxo": [
      "1. Evolution API envia mensagem recebida para o webhook",
      "2. Extrai texto, telefone e nome do cliente",
      "3. Upsert lead no banco de dados",
      "4. Busca sess\u00e3o ativa (\u00faltimas 4 horas) ou cria nova",
      "5. Busca conhecimento jur\u00eddico relevante no banco",
      "6. Monta contexto com system prompt especializado + hist\u00f3rico + base de conhecimento",
      "7. Chama OpenAI GPT-4o-mini",
      "8. Salva resposta no hist\u00f3rico da sess\u00e3o",
      "9. Envia resposta de volta ao WhatsApp via Evolution API"
    ],
    "configurar": [
      "1. Credencial PostgreSQL: Settings > Credentials > New > Postgres",
      "2. OpenAI Key: substitua SUA_OPENAI_KEY (ou use Claude com endpoint da Anthropic)",
      "3. Evolution API URL: substitua SUA_EVOLUTION_API",
      "4. Evolution API Key: substitua SUA_APIKEY_EVOLUTION",
      "5. Nome da inst\u00e2ncia: ajuste 'oliveira' no endpoint de envio",
      "6. No Evolution API: configure o webhook para apontar para /webhook/oliveira-whatsapp",
      "7. Para RAG vetorial real: adicione n\u00f3 de embedding antes do 'Buscar Conhecimento'"
    ],
    "trocarModeloIA": {
      "claude": "URL: https://api.anthropic.com/v1/messages | Header: x-api-key: SUA_KEY | Body: {model: 'claude-haiku-4-5-20251001', max_tokens: 700, system: messages[0].content, messages: messages.slice(1)}",
      "gemini": "URL: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=SUA_KEY"
    }
  }
}