feat: initial project structure (Model Project) - Backend + Multi-Frontend + Docker
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
# 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.**
|
||||
Reference in New Issue
Block a user