384 lines
16 KiB
Markdown
384 lines
16 KiB
Markdown
# 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
|
||
```javascript
|
||
// 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
|
||
```javascript
|
||
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)
|
||
```javascript
|
||
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
|
||
```javascript
|
||
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:**
|
||
```javascript
|
||
// webhook-receiver.js — handleMessageNew
|
||
async function handleMessageNew(data) {
|
||
await execute(`INSERT INTO nw_event_logs ...`)
|
||
// FIM — nunca responde
|
||
}
|
||
```
|
||
|
||
**Com padrão do boilerplate:**
|
||
```javascript
|
||
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-knowledge` para 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`:
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
// 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:
|
||
```javascript
|
||
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.**
|