# 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 intent - `nw_router_metrics` — telemetria local vs LLM - `nw_auto_replies` — log de respostas (smart-router e LLM) - `nw_event_logs` — eventos do motor recebidos **Motor (`newwhats` DB):** - `sec_agents` — agentes IA - `sec_brain_nodes` — brain do motor (com `usage_tokens`, `provider_used`, `model_used` agora) - `sec_messages` — histórico de conversa por protocolo - `sec_conversations` — protocolos abertos - `sec_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/`) 1. **Schema novo** em `sec_agents` ou `sec_external_tools`: ```sql 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() ); ``` 2. **API admin do motor** para CRUD de tools externas: - `GET /api/secretaria/agents/:id/external-tools` - `POST /api/secretaria/agents/:id/external-tools` - `DELETE /api/secretaria/external-tools/:id` 3. **Adicionar `external_http` no `tools.ts`** (resolveTools): quando tool é externa, gera ToolDef com `execute` que faz fetch para `callback_url` 4. **Bridge no ProtocolEngine.chat()**: ```ts 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() } })) ``` 5. **API ext do motor**: `POST /api/ext/v1/sec/tools` para o plugin satélite registrar tools com sua integration_key ### Trabalho no Alemão (`sistema-nuvem/`) 1. **Novo arquivo** `plugins/newwhats/ai-tools.js` com handlers que reutilizam smart-router: ```js 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' }) }) ``` 2. **Boot do motor** (alemão): registrar suas tools no startup 3. **Remover injection de** `consulta_mencionada`, `produtos_buscados` do context-builder (vira on-demand) 4. **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_pedido` quando 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:** ```ts // plugins/secretaria ou novo plugin "transcribe" async function transcribeAudio(filePath: string, openaiKey: string): Promise { 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:** 1. **Comprovante de Pix** → IA confirma valor + horário, marca pedido como pago 2. **Foto de produto** ("é esse aqui que vocês têm?") → IA identifica produto 3. **Foto de receita** → fora do escopo desta loja, mas vale para futuros tenants **Como:** - gpt-4o-mini aceita visão nativamente (`messages[].content` com `image_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):** ```ts // 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 1. **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) 2. **Prevenção de áudio infinito**: limitar duração max (60s? 90s?) — áudio de 5min vira cobrança e ruído. 3. **Salvar transcrição** junto com o áudio em `messages.transcript_text` para 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: true` da API, enviar chunks pro WhatsApp (latência percebida menor) - [ ] **Persist mensagens fechadas**: hoje `finalizeProtocol` deleta `sec_messages` — mover para arquivo histórico em vez de apagar - [ ] **Health check do motor**: endpoint `/health` que 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_versions` para 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-MiniLM` via 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 `_telemetry` no ToolContext) ### DB - `alemao.sector_brain_nodes` — coluna `tags TEXT[]` adicionada, 10 nodes reescritos - `alemao.nw_router_metrics` — tabela nova - `newwhats.sec_brain_nodes` — 6 nodes reescritos (backup em `sec_brain_nodes_backup_2026_04_26`) - `newwhats.sec_messages` — colunas `usage_tokens` (jsonb), `provider_used`, `model_used` - `newwhats.sec_agents` — coluna `max_tokens` (default 250) ### Redis - `plugin:config:secretaria` — `default_provider` mudou de `gemini` para `openai`, `openai_key` atualizada --- ## 📌 Como retomar 1. **Verificar serviços rodando:** ```bash ss -tlnp | grep -E "8008|4001" # 8008 = motor, 4001 = alemão ``` 2. **Verificar telemetria:** ```sql 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; ``` 3. **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) 4. **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.*