feat: initial project structure (Model Project) - Backend + Multi-Frontend + Docker

This commit is contained in:
VPS 4 Builder
2026-05-15 10:39:35 +02:00
commit 3beac0e0f2
288 changed files with 78761 additions and 0 deletions
+383
View File
@@ -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.**