17 KiB
Sprint M2 e M3 — Próximos passos do motor newwhats e plugin Alemão
Contexto: este documento congela o estado em 2026-04-26 após conclusão das Sprints Fase A, Fase B e M1 do motor. Serve para retomar o trabalho sem precisar reler todo o histórico.
📍 Estado atual (o que já está em produção)
Fluxo end-to-end de uma mensagem WhatsApp
WhatsApp ──► motor newwhats (PID 30387, porta 8008)
│
└─► webhook → alemão (PID 28386, porta 4001)
│
├─► [Fase A] smart-router
│ ├─ FAQ determinístico (horário, endereço, entrega, pagamento, menu)
│ ├─ Lookup direto: cotação #N, pedido #N, PED-XXX, "meus pedidos"
│ └─ ~80% das msgs respondidas SEM LLM (0 tokens)
│
└─► [Fase B] se smart-router miss:
├─ context-builder enxuto (intencoes, contexto_temporal, consulta_mencionada, produtos_buscados, carrinho_abandonado)
├─ systemExtra com header [DADOS_REAIS] (grounding rule)
├─ brain do alemão filtrado por tags (core + intent)
└─ chama motor /api/ext/v1/secretaria/ask
│
▼
[M1] motor ProtocolEngine
├─ brain compacto 2018 chars (template OpenAI)
├─ injeta DATA + PROTOCOLO + brain + contextData + systemExtra + summary
├─ callAI com max_tokens=250 + cache_control Anthropic
├─ telemetria gravada em sec_messages.usage_tokens
└─ sumariza a cada 10 trocas com modelo barato
Configuração de chaves (importante)
| Onde | O que |
|---|---|
Dragonfly/Redis key plugin:config:secretaria |
OpenAI, Gemini, Anthropic, default_provider — fonte da verdade |
PostgreSQL sec_agents (motor) |
Agente "Ana", provider=openai, model=gpt-4o-mini, max_tokens=250 |
PostgreSQL sec_brain_nodes (motor) |
6 nodes template OpenAI (~2000 chars) |
PostgreSQL sector_brain_nodes (alemão, sector_id=1) |
10 nodes filtráveis por tag (~3400 chars total) |
.env do motor |
NÃO é fonte de OPENAI_API_KEY — é Redis. O que está em .env é ignorado pelo plugin secretaria |
Tabelas críticas
Alemão (alemaoconveniencias DB):
sector_brain_nodes— brain do bot por intentnw_router_metrics— telemetria local vs LLMnw_auto_replies— log de respostas (smart-router e LLM)nw_event_logs— eventos do motor recebidos
Motor (newwhats DB):
sec_agents— agentes IAsec_brain_nodes— brain do motor (comusage_tokens,provider_used,model_usedagora)sec_messages— histórico de conversa por protocolosec_conversations— protocolos abertossec_brain_nodes_backup_2026_04_26— backup pré-M1
Métricas de economia já medidas
| Cenário | Tokens antes | Tokens depois | Δ |
|---|---|---|---|
| "em que fase esta a cotação #1" | ~2000 | 0 (smart-router) | −100% |
| "qual o horário?" | ~2000 | 0 (smart-router) | −100% |
| "oi tudo bem?" (LLM) — 1ª chamada | ~2000 | 1041 input + 16 output | −47% |
| "oi tudo bem?" (LLM) — 2ª chamada na mesma conversa | ~2000 | 46 efetivos (1024 cache) | −97% |
🚀 Sprint M2 — Function Calling Externo (callback HTTP)
Objetivo
Permitir que plugins satélites (como o Alemão) registrem tools customizadas no motor. O LLM decide quando chamar; o motor faz HTTP callback no plugin para executar.
Por quê
- Tokens −60% adicional: hoje injetamos
consulta_mencionada,produtos_buscados, etc. upfront no contexto. Com tools, só vai quando o LLM decide pedir - Tools versionadas no plugin: cada satélite define suas tools sem mexer no motor
- Reuso do smart-router: as funções já existem em
/sistema-nuvem/plugins/newwhats/smart-router.js(tryLocalReply) — viram a implementação das tools - Multi-tenant nato: motor vira gateway LLM puro, plugins ficam responsáveis pelos seus dados
Tools a implementar (lado Alemão)
| Tool | Endpoint | Argumentos | Retorno |
|---|---|---|---|
consultar_pedido |
POST /api/ai-tools/consultar_pedido |
{ id_or_protocolo: string } |
{ status, fase, pagamento, entrega, total } |
buscar_produto |
POST /api/ai-tools/buscar_produto |
{ query: string, max?: number } |
[{ nome, preco, sku, estoque }] |
meus_pedidos |
POST /api/ai-tools/meus_pedidos |
{ cpf?: string, telefone?: string } |
[{ protocolo, status, data, total }] |
info_loja |
POST /api/ai-tools/info_loja |
{ topico: 'horario'|'endereco'|'entrega'|'pagamento' } |
{ texto: string } |
criar_carrinho |
POST /api/ai-tools/criar_carrinho |
{ telefone, itens: [{ sku, qtd }] } |
{ cotacao_id, total } |
confirmar_recebimento |
POST /api/ai-tools/confirmar_recebimento |
{ protocolo } |
{ ok: bool } |
Trabalho no motor (plugins/secretaria/)
-
Schema novo em
sec_agentsousec_external_tools:CREATE TABLE sec_external_tools ( id UUID PRIMARY KEY, agent_id UUID REFERENCES sec_agents(id), tenant_id TEXT NOT NULL, name VARCHAR(80) NOT NULL, description TEXT NOT NULL, parameters JSONB NOT NULL, -- JSON Schema para args callback_url TEXT NOT NULL, auth_header_name VARCHAR(100) DEFAULT 'x-nw-key', auth_value TEXT, -- integration_key do plugin timeout_ms INT DEFAULT 5000, enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW() ); -
API admin do motor para CRUD de tools externas:
GET /api/secretaria/agents/:id/external-toolsPOST /api/secretaria/agents/:id/external-toolsDELETE /api/secretaria/external-tools/:id
-
Adicionar
external_httpnotools.ts(resolveTools): quando tool é externa, gera ToolDef comexecuteque faz fetch paracallback_url -
Bridge no ProtocolEngine.chat():
const externalTools = await this.db('sec_external_tools') .where({ agent_id: agent.id, enabled: true }) const externalToolDefs = externalTools.map(t => ({ name: t.name, description: t.description, parameters: t.parameters, execute: async (args, ctx) => { const res = await fetch(t.callback_url, { method: 'POST', headers: { 'Content-Type': 'application/json', [t.auth_header_name]: t.auth_value }, body: JSON.stringify({ args, chatId: ctx.extChatId, conversationId: ctx.conversationId }) }) return res.json() } })) -
API ext do motor:
POST /api/ext/v1/sec/toolspara o plugin satélite registrar tools com sua integration_key
Trabalho no Alemão (sistema-nuvem/)
-
Novo arquivo
plugins/newwhats/ai-tools.jscom handlers que reutilizam smart-router:router.post('/api/ai-tools/consultar_pedido', verifyMotorAuth, async (req, res) => { const { id_or_protocolo } = req.body.args const data = await lookupCotacaoOuPedido(parseIntent(id_or_protocolo)) res.json(data ?? { error: 'não encontrado' }) }) -
Boot do motor (alemão): registrar suas tools no startup
-
Remover injection de
consulta_mencionada,produtos_buscadosdo context-builder (vira on-demand) -
Manter smart-router para o caminho de 0 tokens (resposta direta sem LLM)
Critérios de sucesso M2
- Tools registradas no motor via API
- LLM consegue chamar
consultar_pedidoquando usuário pergunta sobre pedido - Latência adicional do callback < 200ms
- Tokens médio por chamada cai de ~1000 para ~400
- Smart-router continua respondendo o caso óbvio antes de chegar ao LLM
Estimativa
- Motor: 4-6h (schema + API + bridge no ProtocolEngine)
- Alemão: 3-4h (handlers + integração)
- Testes + ajustes: 2h
- Total: ~10h de trabalho focado
🎤 Sprint M3 — Whisper + Vision (mídia)
Objetivo
Aceitar áudio e imagem do cliente. Brasileiro manda muito áudio no WhatsApp; ignorar isso é deixar metade da experiência na mesa.
Whisper — transcrição de áudio
Onde: motor newwhats, hook em MessageHandler quando msg.type === 'AUDIO'.
Fluxo:
WhatsApp Audio → motor baixa via Baileys → ffmpeg converte para mp3
│
▼
OpenAI Whisper (whisper-1) com `language: pt`
│
▼
texto entra no fluxo normal de message.new
│
▼
webhook envia ao alemão como TEXT
Custo: $0.006/minuto. Áudio típico de WhatsApp 30s = 0.003 = R 0,015. Insignificante.
Implementação:
// plugins/secretaria ou novo plugin "transcribe"
async function transcribeAudio(filePath: string, openaiKey: string): Promise<string> {
const form = new FormData()
form.append('file', fs.createReadStream(filePath))
form.append('model', 'whisper-1')
form.append('language', 'pt')
const res = await fetch('https://api.openai.com/v1/audio/transcriptions', {
method: 'POST',
headers: { Authorization: `Bearer ${openaiKey}` },
body: form,
})
const data = await res.json()
return data.text
}
Onde plugar no motor: MessageHandler.processIncoming() — antes de salvar messages, se for AUDIO, transcrever e usar o texto como body (manter referência ao áudio original em mediaUrl).
Vision — entender imagens
Casos de uso reais Alemão:
- Comprovante de Pix → IA confirma valor + horário, marca pedido como pago
- Foto de produto ("é esse aqui que vocês têm?") → IA identifica produto
- Foto de receita → fora do escopo desta loja, mas vale para futuros tenants
Como:
- gpt-4o-mini aceita visão nativamente (
messages[].contentcomimage_url) - Encoda imagem como base64 ou URL pública
- Envia junto com a mensagem do usuário
Custo: vision adiciona ~85 tokens por imagem 512x512. ~R$ 0,001 por img. Trivial.
Implementação no motor (callProvider OpenAI):
// Quando há anexo de imagem, mensagem do user vira:
{
role: 'user',
content: [
{ type: 'text', text: 'foto que ele mandou' },
{ type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64}` } }
]
}
Boas práticas que devem entrar nesta sprint
-
Detecção de comprovante Pix (regex em texto após OCR ou direto via vision):
- Valor "R$ X,XX"
- Data/hora
- "Comprovante", "Pix enviado", "Transação"
- Quando detectar → tool
confirmar_pagamento(protocolo, valor)(parte da M2)
-
Prevenção de áudio infinito: limitar duração max (60s? 90s?) — áudio de 5min vira cobrança e ruído.
-
Salvar transcrição junto com o áudio em
messages.transcript_textpara auditoria.
Critérios de sucesso M3
- Áudio recebido vira texto e flui normalmente
- Imagem com comprovante Pix gera transcrição da info chave (valor, data)
- Custo médio por mensagem com mídia < R$ 0,02
- Mídia salva em storage (S3 ou local) com referência
Estimativa
- Whisper: 3-4h (download via Baileys, ffmpeg, integração)
- Vision: 2-3h (passar image_url no payload do callProvider)
- Hooks no MessageHandler: 2h
- Testes: 2h
- Total: ~10h
💡 Outras melhorias mapeadas (backlog)
Robustez (Sprint M4)
- Circuit breaker por provider: se OpenAI falhar 5x em 1min, pular esse provider por 30s
- Streaming response: usar
stream: trueda API, enviar chunks pro WhatsApp (latência percebida menor) - Persist mensagens fechadas: hoje
finalizeProtocoldeletasec_messages— mover para arquivo histórico em vez de apagar - Health check do motor: endpoint
/healthque valida providers (chave válida? respondem?) e expõe para monitoramento
Operação
- Painel admin para editar brain_nodes (alemão e motor): hoje só via SQL ou Dragonfly. Deve haver UI no admin-app
- Versão do prompt: tabela
sec_brain_node_versionspara rollback rápido se uma mudança quebrar conversas - A/B testing de prompts: 50% dos chats com brain v1, 50% com v2, medir qualidade
- Dashboard de tokens no admin-app: gráfico diário de custo por tenant/agente
- Limpeza de código morto:
chatbot.service.ts+chatbot.repository.ts(Gemini standalone, sem uso). Toca 4 arquivos: MessageHandler, WhatsAppConnectionManager, chatbot.routes, chatbot.repository
Inteligência (Sprint M5+)
- RAG com pgvector: embeddings dos brain_nodes e produtos. Busca semântica em vez de tag-matching. Próximo nível de relevância
- Memória persistente por cliente (mem0-style): "Bruno sempre pede entrega à noite", "Cliente PJ fatura sempre dia 10"
- Detector de intenção com modelo pequeno local (ex:
paraphrase-multilingual-MiniLMvia onnxruntime): roteamento sem LLM - Multi-agent: separar Aninha (atendimento) de outras personas (financeiro, suporte) com roteamento por contexto
Segurança / compliance
- Rate limiting por chat: além do anti-loop atual (8 replies/h), proteção contra abuso
- PII redaction: antes de enviar ao LLM, mascarar CPF/cartão se aparecer no histórico
- Audit trail: logar prompts completos enviados ao LLM (criptografado, retention configurável) para casos de incidente
- Termo de uso de IA: notificar cliente que está falando com IA na primeira mensagem
UX
- Resposta proativa: notificar cliente sem ele perguntar (status pedido mudou, promo do produto que costuma comprar)
- Carrinho via WhatsApp: usuário adiciona itens conversando, IA confirma e finaliza pedido
- Reativação inteligente: cliente sumiu há 30 dias com carrinho aberto → mensagem personalizada
- Multi-idioma automático: detectar idioma do cliente e responder no mesmo (ES, EN, PT)
Telemetria / observabilidade
- OpenTelemetry traces em cada chamada LLM (provider, latência, tokens, sucesso)
- Métricas Prometheus expostas em
/metrics - Alertas: provider down, custo diário > X, taxa de erro > Y
- Replays: poder "rerodar" uma conversa com prompt novo para ver se a resposta seria melhor
🛠️ Estado dos arquivos modificados (commits implícitos)
Alemão (/home/deploy/projetos/alemaoconveniencias.local/sistema-nuvem/)
plugins/newwhats/smart-router.js— NEW (Fase A)plugins/newwhats/context-builder.js— modificado (Fases A, B)plugins/newwhats/webhook-receiver.js— modificado (smart-router integration, brain filter)routes/apiCotacoes.js— modificado (endpoint sem-resposta)jobs/monitorMsgsSemResposta.js— NEW (sprint anterior)
Motor (/home/deploy/projetos/newwhats.local/)
plugins/secretaria/brain.ts— refatorado (M1: cache, max_tokens, telemetria, summarize barato)plugins/secretaria/tools.ts— modificado (campo_telemetryno ToolContext)
DB
alemao.sector_brain_nodes— colunatags TEXT[]adicionada, 10 nodes reescritosalemao.nw_router_metrics— tabela novanewwhats.sec_brain_nodes— 6 nodes reescritos (backup emsec_brain_nodes_backup_2026_04_26)newwhats.sec_messages— colunasusage_tokens(jsonb),provider_used,model_usednewwhats.sec_agents— colunamax_tokens(default 250)
Redis
plugin:config:secretaria—default_providermudou degeminiparaopenai,openai_keyatualizada
📌 Como retomar
-
Verificar serviços rodando:
ss -tlnp | grep -E "8008|4001" # 8008 = motor, 4001 = alemão -
Verificar telemetria:
SELECT provider_used, COUNT(*), AVG((usage_tokens->>'total')::int) AS avg_tokens FROM sec_messages WHERE role='assistant' AND usage_tokens IS NOT NULL GROUP BY 1; -
Decidir próxima sprint:
- M2 = mais ROI (tokens caem outros 60%, qualidade sobe)
- M3 = mais UX (cliente brasileiro adora áudio/imagem)
- M4 = robustez para escala (faça quando tiver volume)
-
Sequência sugerida: M2 → M3 → M4 → M5 (RAG)
Documento gerado em 2026-04-26 após Sprint M1 concluída e validada end-to-end.