16 KiB
Análise: Boilerplate Secretária Virtual × NewWhats v2.2
Análise técnica real — comparando o boilerplate enviado com a arquitetura atual do motor NewWhats e identificando o que cada Frente v2.2 ganha (ou não) com esse padrão.
TL;DR — Veredicto em 3 linhas
O boilerplate implementa Tool Use (IA chama funções em tempo real). O motor atual implementa Knowledge Node (dados pré-carregados no system prompt). São complementares, não concorrentes — e o motor está faltando o Tool Use.
1. O que o boilerplate faz certo
✅ Segurança: patientId vem da sessão, nunca da IA
// patientId sempre da sessão — a IA nunca pode sobrescrever isso
async function executeTool(toolName, toolInput, patientId) { ... }
// No endpoint:
const patient = req.session.patient // ← fonte da verdade
const { reply } = await runAgentLoop(messages, patient)
Impacto: A IA pode pedir "cancele o agendamento 999" de outro paciente, mas o
WHERE patient_id = ? garante que só o paciente da sessão é afetado.
Isso é o padrão correto de segurança com tool use.
✅ IA nunca toca o banco — executa via função intermediária
A IA solicita { name: 'cancel_appointment', input: { appointment_id: 42 } }.
O servidor executa. A IA nunca tem credenciais, nunca faz queries diretas.
✅ Agent loop: multi-step tool chains
while (true) {
const response = await anthropic.messages.create({ tools: TOOLS, messages })
if (response.stop_reason === 'end_turn') return reply
if (response.stop_reason === 'tool_use') {
// executa todas as tools solicitadas
// devolve resultados e volta ao loop
messages.push({ role: 'user', content: toolResults })
}
}
Isso permite: list_doctors → apresenta ao usuário → check_availability → create_appointment
em uma única conversa, sem o usuário perceber que são 3 chamadas de API.
✅ Confirmação antes de ação destrutiva
O system prompt exige confirmação explícita antes de cancelar.
E o cancel_appointment re-verifica WHERE patient_id = ? como segunda camada de segurança.
✅ System prompt como "contrato" da IA
As regras absolutas são claras, específicas e mensuráveis:
- "NUNCA confirma agendamento sem check_availability"
- "NUNCA discute temas fora do escopo"
- "NUNCA pede CPF ou dados bancários"
2. O que está faltando / gaps do boilerplate
❌ Zero prompt caching
Cada request reconstrói o system prompt + histórico do zero.
Com Claude, o system prompt idêntico entre requests ativa cache automático de prefix.
O boilerplate não usa cache_control: { type: "ephemeral" } nos blocos de sistema.
Custo real: Um system prompt de 800 tokens × 100 conversas/dia = 80.000 tokens de input pagos inteiramente toda vez. Com cache, ~90% vira cache hit (5× mais barato).
❌ Histórico em RAM (express-session em memória)
req.session.messages = updatedMessages.slice(-50)
- Reiniciar o servidor = perder todas as conversas ativas
- Não escala horizontalmente (sem Redis/banco)
- Slice(-50) é bruto — 50 mensagens podem ser 40.000 tokens se forem longas
O motor faz certo: persiste em sec_conversations + sec_messages no banco.
❌ Sem fallback de provider
Se a API key da Anthropic falhar → a secretária morre.
O motor tem a chain openai → gemini → anthropic → ollama.
❌ Sem limite de iterações no agent loop
while (true) { ... } // pode rodar para sempre
Se a IA entrar em loop (chama tool → recebe erro → chama tool de novo), nunca sai.
Falta: if (iteration > MAX_ITER) throw new Error('Loop de ferramentas excedido')
❌ Sem multi-tenant
Uma instância, um banco de dados de clínica. Não serve para SaaS.
O motor tem tenantId em todas as queries e usa ext_chat_id = {tenantId}:{chatId}.
❌ Sem sumarização automática
Histórico só cresce (limitado por slice bruto). O motor sumariza a cada 10 trocas e descarta mensagens antigas, mantendo o contexto leve.
❌ Sem webhook — só polling request/response
O boilerplate assume que o humano digita no browser. Para WhatsApp, o fluxo é invertido: o motor recebe webhook e precisa chamar o runAgentLoop de forma assíncrona, depois enviar a resposta proativamente.
3. A diferença arquitetural central
BOILERPLATE MOTOR ATUAL (newwhats)
───────────────────────────── ──────────────────────────────
Pattern: Tool Use (runtime) Pattern: Knowledge Node (RAG)
IA decide quando buscar dados → Dados pré-carregados no prompt
Queries em tempo real por conv → sync-knowledge.js roda periodicamente
+1-3 chamadas API por mensagem → Contexto maior, 0 chamadas extras
Dados sempre frescos → Dados podem ter até 1h de delay
Conhece dados do CLIENTE → Conhece catálogo GERAL da empresa
O que cada abordagem responde bem
| Pergunta do cliente | Knowledge Node | Tool Use |
|---|---|---|
| "Quais planos do clube odonto?" | ✅ (sync-knowledge) | funciona, mas desnecessário |
| "Quais parceiros aceitam meu plano?" | ✅ (sync-knowledge) | funciona |
| "Tenho promoção ativa?" | ✅ (sync-knowledge) | funciona |
| "Quais são meus números da rifa?" | ❌ não sabe | ✅ lookup por telefone |
| "Meu plano está ativo?" | ❌ não sabe | ✅ lookup por CPF |
| "Minha candidatura foi aprovada?" | ❌ não sabe | ✅ lookup por CPF |
| "Quando é meu próximo vencimento?" | ❌ não sabe | ✅ lookup por CPF |
Conclusão: O motor conhece o catálogo do Alemão, mas é cego para dados individuais do cliente. O boilerplate mostra exatamente o que falta.
4. Mapeamento por Frente v2.2
Frente 1 — Gerenciamento de Números ← não usa o boilerplate
O boilerplate é para chat de clínica, não para gerenciar instâncias WhatsApp.
Porém o padrão de segurança (phone from webhook, never from AI) é o mesmo princípio
que usaremos na auto-resposta: o número de origem vem do evento, nunca da IA.
Status: em implementação (modais AddNumber/EditNumber já adicionados no WhatsAppInbox.tsx).
Frente 2 — Auto-resposta ← o boilerplate É o que falta
O runAgentLoop + executeTool é exatamente o núcleo que o webhook-receiver precisa.
Hoje:
// webhook-receiver.js — handleMessageNew
async function handleMessageNew(data) {
await execute(`INSERT INTO nw_event_logs ...`)
// FIM — nunca responde
}
Com padrão do boilerplate:
async function handleMessageNew(data) {
await execute(`INSERT INTO nw_event_logs ...`)
const msg = data.message
if (msg.fromMe) return // nunca responder a si mesmo
// 1. Verifica role do número (notificacoes = nunca auto-responde)
const numInfo = await getNwNumber(data.instanceId)
if (!numInfo || numInfo.role === 'notificacoes') return
if (auto_reply_mode === 'off') return
// 2. Identifica cliente pelo telefone
const phone = data.chatId.replace('@s.whatsapp.net', '')
const clientCtx = await fetch(`http://localhost:3000/nw/cliente?telefone=${phone}`)
// 3. Busca/cria conversa no motor — usa ext_chat_id = chatId
const conv = await motorFetch('POST', '/secretaria/ask', {
chatId: data.chatId,
message: msg.body,
contactName: msg.pushName ?? phone,
})
// motor.brain.chat() já executa o agent loop internamente
// 4. Envia resposta via motor
await motorFetch('POST', `/inbox/${data.chatId}/send`, { text: conv.reply }, useIntegKey)
}
Mas há um problema crítico detectado:
O /secretaria/ask do motor chama brain.chat() que é somente texto —
não tem tool use. A IA não pode buscar dados do cliente em tempo real.
Quando o cliente perguntar "meus números da rifa" a Ana vai responder "acesse o site" porque não tem como saber.
Frente 3 — Sync Knowledge ← complementar ao boilerplate
O boilerplate usa o banco direto (tool list_doctors).
O sync-knowledge.js usa o banco para construir o system prompt.
São estratégias diferentes para o mesmo problema:
Boilerplate approach: sync-knowledge approach:
list_doctors como tool → médicos/parceiros no knowledge node
resultado por request → resultado atualizado de hora em hora
+tokens de tool call → +tokens no system prompt
sempre atualizado → atraso de até 1h
Para o Alemão, o melhor é:
sync-knowledgepara catálogo (planos, parceiros, promoções, vagas) ← já feito- tool use para dados do cliente (tickets, memberships, candidaturas) ← falta
Status: botão de sync no admin ainda não implementado (simples, próximo passo).
Frente 4 — Escalação ← o boilerplate mostra o anti-padrão
O boilerplate usa system prompt para definir regras de escalação:
"NUNCA realize ações destrutivas sem confirmação"
O motor já tem o nó escalation com o mesmo conceito, porém mais estruturado.
O que falta no motor (e o boilerplate não resolve) é o lado operacional:
detectar [ESCALAR] na resposta da IA e notificar o humano no painel.
O boilerplate não implementa escalação porque assume atendimento 100% humano de qualquer forma (login no browser = humano já presente). Para WhatsApp assíncrono, escalação é crítica.
Frente 5 — Métricas ← o boilerplate tem zero
Apenas console.log dos tool calls. Sem persistência, sem dashboard.
O nw_event_logs já é a base certa para métricas.
Quando a auto-resposta for implementada, adicionar nw_auto_replies resolve as métricas.
5. O gap mais crítico identificado: Motor sem Tool Use
O motor atual em brain.ts:
private async callAI(...): Promise<string> { ... }
// Retorna sempre string — sem suporte a tool_use
Todos os providers são chamados sem tools: []. O motor nunca vai receber
stop_reason: 'tool_use' porque nunca passa ferramentas para a IA.
Para a Ana ser realmente útil individualmente, o motor precisa evoluir:
// Hoje: brain.chat() → string
const reply = await brain.chat(convId, message)
// Precisaria: brain.chat() com tool loop (como o boilerplate)
interface Tool { name: string; description: string; input_schema: object }
interface ToolExecutor { (name: string, input: any, context: ChatContext): Promise<any> }
async chat(convId: string, message: string, tools?: Tool[], executor?: ToolExecutor): Promise<string>
As ferramentas que a Ana precisaria para o Alemão:
const ALEMAO_TOOLS = [
{
name: 'buscar_cliente',
description: 'Busca dados do cliente pelo telefone. Retorna tickets da rifa, clubes ativos e candidaturas.',
input_schema: { type: 'object', properties: { telefone: { type: 'string' } }, required: ['telefone'] }
},
{
name: 'buscar_clube',
description: 'Retorna plano ativo do cliente, dependentes e parceiros credenciados.',
input_schema: { type: 'object', properties: { cpf: { type: 'string' } }, required: ['cpf'] }
},
{
name: 'buscar_candidaturas',
description: 'Retorna candidaturas do cliente com status atual.',
input_schema: { type: 'object', properties: { cpf: { type: 'string' } }, required: ['cpf'] }
},
]
// Executor: chama as rotas /nw/* já existentes no satélite
Essas rotas /nw/* já existem no satélite. O problema é que a Ana não consegue chamá-las.
6. Resumo executivo — O que fazer com isso
Aproveitar do boilerplate para v2.2
| O quê | Onde aplicar | Prioridade |
|---|---|---|
| Agent loop (while + tool_use) | webhook-receiver.js (Frente 2) | 🔴 Alta |
| Security pattern (phone from webhook only) | webhook-receiver.js (Frente 2) | 🔴 Alta |
| Max iterations guard | webhook-receiver.js anti-loop | 🔴 Alta |
| Tool definitions ALEMAO_TOOLS | motor brain.ts (evolução futura) | 🟡 Média |
| Confirmação antes de ação destrutiva | já no nó escalation do motor | ✅ Já existe |
NÃO aproveitar do boilerplate
| O quê | Motivo |
|---|---|
| express-session em memória | motor já usa banco (correto) |
| Histórico slice(-50) | motor já sumariza (correto) |
| Single provider | motor já tem fallback chain (correto) |
| MySQL | projeto usa PostgreSQL |
| Sem multi-tenant | motor já tem tenantId (correto) |
Sequência correta para Frente 2
1. webhook-receiver.js: verificar role do número (notificacoes = skip)
2. webhook-receiver.js: chamar POST /api/secretaria/ask (motor)
→ motor já faz: lookup/cria conversa, chama brain.chat()
→ brain.chat() responde com texto (sem tool use por ora)
3. webhook-receiver.js: enviar reply via POST /api/ext/v1/inbox/:chatId/send
4. Resultado: Ana responde perguntas gerais (catálogo) mas não dados individuais
5. (Evolução): adicionar tool use no brain.ts
→ Ana passa a responder "seus tickets são 003, 047, 112"
→ Usando as rotas /nw/* já existentes como tool executors
A Frente 2 básica (sem tool use) já resolve 70% dos casos de uso. A evolução com tool use resolve os 30% restantes (dados pessoais do cliente).
7. Arquitetura alvo — Sistema completo
┌─────────────────────────────────────┐
│ WhatsApp (cliente) │
└──────────────┬──────────────────────┘
│ mensagem
▼
┌─────────────────────────────────────┐
│ Motor NewWhats (webhook) │
│ POST /api/webhook/newwhats │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ webhook-receiver.js │
│ 1. verifica role do número │
│ 2. verifica auto_reply_mode │
│ 3. cooldown / anti-loop │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ POST /api/secretaria/ask │
│ Motor: cria/retoma conversa │
│ brain.chat(convId, message) │
└──────────────┬──────────────────────┘
│ [futuro: tool use]
┌───────────────────────►│◄───────────────────────┐
│ │ │
┌─────────▼──────────┐ ┌────────▼──────────┐ ┌────────▼──────────┐
│ Knowledge Nodes │ │ /nw/cliente │ │ /nw/clube │
│ (sync-knowledge) │ │ /nw/candidaturas │ │ /nw/sorteio │
│ planos, parceiros │ │ /nw/vagas │ │ /nw/promocoes │
│ promoções, vagas │ │ (dados pessoais) │ │ (dados pessoais) │
└────────────────────┘ └───────────────────┘ └───────────────────┘
(RAG) (Tool Use) (Tool Use)
dados estáticos dados individuais dados individuais
O boilerplate mostra como construir a metade direita do diagrama. O motor já tem a metade esquerda funcionando. Frente 2 une as duas.