feat: initial project structure (Model Project) - Backend + Multi-Frontend + Docker
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
# Análise de Velocidade — Secretária Virtual (WhatsApp + PDF)
|
||||
|
||||
> **Data:** 2026-05-05 | **Atualizado:** 2026-05-05 (fixes aplicados — seção 9)
|
||||
> **Contexto:** A lentidão nas respostas não é só inconveniência de UX —
|
||||
> ela aumenta diretamente o risco de **alucinações** da IA. Veja seção 1.
|
||||
|
||||
---
|
||||
|
||||
## 1. Por que velocidade afeta alucinações
|
||||
|
||||
Quando a resposta demora, três caminhos de degradação se abrem:
|
||||
|
||||
| Causa | Mecanismo | Consequência |
|
||||
|---|---|---|
|
||||
| Timeout na extração do PDF | Sistema cai no fallback → LLM não recebe o conteúdo real | IA inventa dados que não estão no documento |
|
||||
| Fallback de provider (Gemini → OpenAI → Claude) | Cada falha consume tempo; o 3º provider recebe um prompt diferente | Inconsistência de persona e tom |
|
||||
| Sumarização na 5ª mensagem | 2 chamadas LLM encadeadas; se a 2ª expira, o resumo some | Próximas respostas sem contexto da conversa |
|
||||
| Tool calling em loop (até 5 iterações) | Cada rodada = +2–4s; se a última expira, a resposta é truncada | Resposta cortada ou genérica sem dados reais |
|
||||
| Fetch de mídia chegando vazio | `extractPdfText` retorna null → `effectiveBody` fica vazio | IA não sabe o que o cliente enviou e inventa |
|
||||
|
||||
**Regra geral:** quanto mais o sistema espera por dados externos (LLM, DB, motor), maior a janela para timeout e maior a chance de o LLM responder com contexto incompleto.
|
||||
|
||||
---
|
||||
|
||||
## 2. Arquitetura do fluxo (visão atual)
|
||||
|
||||
```
|
||||
WhatsApp
|
||||
└─► Motor (newwhats.local:8008)
|
||||
├─► setImmediate: downloadAndPersistMedia() ← assíncrono
|
||||
└─► HTTP POST webhook → satélite (sistema-nuvem:4001)
|
||||
│
|
||||
├── 1. Log em nw_event_logs (DB)
|
||||
├── 2. getPluginConfig() ← DB query TODA mensagem
|
||||
├── 3. [PDF] fetchMediaBuffer() ← retry 4x, até 6.5s
|
||||
├── 4. [PDF] extractPdfText() ← Gemini → OpenAI → Claude
|
||||
├── 5. Follow-up detector (DB)
|
||||
├── 6. Number role lookup ← HTTP ao motor (toda msg)
|
||||
├── 7. Cooldown check (DB)
|
||||
├── 8. Anti-loop check (DB)
|
||||
├── 9. Smart-router (DB)
|
||||
└── 10. autoReply()
|
||||
├── buildContext() (DB, sequencial)
|
||||
├── resolvePersona() (DB, sequencial)
|
||||
├── brainRows injection (DB, sequencial)
|
||||
└── POST /api/ext/v1/secretaria/ask
|
||||
└─► Motor: brain.chat()
|
||||
├── sec_conversations (DB)
|
||||
├── sec_agents (DB, sequencial)
|
||||
├── buildSystemPrompt()
|
||||
│ ├── sec_brain_nodes (DB)
|
||||
│ └── getCalendarContext() (DB, dentro do loop)
|
||||
├── sec_messages (DB)
|
||||
├── sec_messages INSERT user
|
||||
├── sec_brain_nodes persona (DB, repetido!)
|
||||
├── LLM call com tools (até 5 iterações)
|
||||
├── sec_messages INSERT assistant
|
||||
├── sec_messages COUNT
|
||||
├── [se 5ª/10ª msg] summarize() ← outro LLM!
|
||||
└── sec_conversations UPDATE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Gargalos identificados
|
||||
|
||||
### 🔴 CRÍTICO — `fetchMediaBuffer` (até 6,5s de espera pura)
|
||||
|
||||
**Arquivo:** `sistema-nuvem/plugins/newwhats/media-transcribe.js:25–26`
|
||||
|
||||
```js
|
||||
const DOWNLOAD_RETRIES = 4
|
||||
const DOWNLOAD_BACKOFF = [500, 1000, 2000, 3000] // soma: 6.5s
|
||||
```
|
||||
|
||||
**Causa raiz:** O motor dispara o webhook e o download de mídia **simultaneamente**
|
||||
via `setImmediate` (`MessageHandler.ts:547`). O satélite começa a pedir o arquivo
|
||||
com apenas 500ms de espera, mas o motor geralmente leva 800ms–2s para concluir
|
||||
o download do WhatsApp.
|
||||
|
||||
Resultado prático: **3 das 4 tentativas são 404**. O sistema espera
|
||||
`500 + 1000 + 2000 = 3500ms` antes de conseguir o arquivo — mesmo quando o
|
||||
download terminou cedo.
|
||||
|
||||
**Impacto em alucinações:** se todas as retries falharem, `extractPdfText` retorna
|
||||
`null`, o webhook-receiver aborta (`return`) e a mensagem é **ignorada silenciosamente**.
|
||||
O cliente fica sem resposta; se mandar outra mensagem cobrando, o contexto do PDF
|
||||
se perde e a IA responde sem os dados reais do documento.
|
||||
|
||||
---
|
||||
|
||||
### 🔴 CRÍTICO — `extractPdfText` é sequencial ao `context-builder`
|
||||
|
||||
**Arquivo:** `sistema-nuvem/plugins/newwhats/webhook-receiver.js` (fluxo de DOCUMENT)
|
||||
|
||||
```js
|
||||
// Atual — sequencial:
|
||||
const pdfText = await extractPdfText(...) // 2–8s (download + LLM)
|
||||
// ... só depois:
|
||||
const ctx = await buildContext(...) // ~200ms — poderia rodar em paralelo
|
||||
const persona = await resolvePersona(...) // ~100ms — poderia rodar em paralelo
|
||||
const brainRows = await dbq(...) // ~100ms — poderia rodar em paralelo
|
||||
```
|
||||
|
||||
`buildContext`, `resolvePersona` e `brainRows` não dependem do texto do PDF.
|
||||
Os ~400ms deles poderiam ser absorvidos dentro do tempo de `extractPdfText`.
|
||||
|
||||
---
|
||||
|
||||
### 🟠 ALTO — `getPluginConfig()` faz query ao banco em toda mensagem
|
||||
|
||||
**Arquivo:** `sistema-nuvem/plugins/newwhats/webhook-receiver.js:44` e `:207`
|
||||
|
||||
```js
|
||||
const cfg = await getPluginConfig() // SELECT key, value FROM plugin_configs WHERE plugin_id = 'newwhats'
|
||||
```
|
||||
|
||||
Disparado em **toda mensagem recebida**, incluindo as que serão descartadas
|
||||
(fromMe, grupos, etc.). A config muda raramente (admin altera via painel),
|
||||
mas o sistema trata como volátil.
|
||||
|
||||
**Impacto:** ~50–150ms por mensagem + carga desnecessária no banco.
|
||||
|
||||
---
|
||||
|
||||
### 🟠 ALTO — Number role lookup via HTTP ao motor (toda mensagem)
|
||||
|
||||
**Arquivo:** `sistema-nuvem/plugins/newwhats/webhook-receiver.js` linha ~350
|
||||
|
||||
```js
|
||||
const numRes = await fetch(`${motorUrl}/api/ext/v1/secretaria/numbers`, {
|
||||
headers: { 'x-nw-key': integKey },
|
||||
})
|
||||
```
|
||||
|
||||
HTTP síncrono para o motor em cada mensagem para verificar se o número tem
|
||||
`role === 'notificacoes'`. Os números não mudam com frequência.
|
||||
|
||||
**Impacto:** ~100–300ms por mensagem (rede local, mas ainda é round-trip HTTP).
|
||||
|
||||
---
|
||||
|
||||
### 🟠 ALTO — Queries sequenciais no `brain.chat()` do motor
|
||||
|
||||
**Arquivo:** `newwhats.local/plugins/secretaria/brain.ts:35–64`
|
||||
|
||||
```ts
|
||||
// Sequencial (atual):
|
||||
const conversation = await db('sec_conversations')... // query 1
|
||||
const agent = await db('sec_agents')... // query 2
|
||||
// dentro de buildSystemPrompt():
|
||||
const nodes = await db('sec_brain_nodes')... // query 3
|
||||
const calCtx = await this.getCalendarContext() // query 4 (dentro do loop de nodes)
|
||||
// depois:
|
||||
const personaNode = await db('sec_brain_nodes')... // query 5 — repete a mesma tabela!
|
||||
const recentMessages = await db('sec_messages')... // query 6
|
||||
```
|
||||
|
||||
Seis queries sequenciais antes de qualquer chamada ao LLM. `agent` depende de
|
||||
`conversation.agent_id`, mas as demais podem ser paralelizadas após a primeira
|
||||
consulta.
|
||||
|
||||
A query da `personaNode` (linha 62) busca na mesma tabela `sec_brain_nodes`
|
||||
que `buildSystemPrompt` já consultou na linha 158 — **query duplicada**.
|
||||
|
||||
**Impacto:** 200–400ms de overhead de banco antes de cada chamada ao LLM.
|
||||
|
||||
---
|
||||
|
||||
### 🟠 ALTO — `config.get('secretaria')` chamado 2× por requisição com tools
|
||||
|
||||
**Arquivo:** `brain.ts:333` (dentro de `callAI`) e `brain.ts:589` (dentro de `callAIWithTools`)
|
||||
|
||||
```ts
|
||||
// callAI():
|
||||
const cfg = await this.config.get('secretaria') // leitura 1
|
||||
|
||||
// callAIWithTools() — chamado antes de callAI em fluxos com tools:
|
||||
const cfg = await this.config.get('secretaria') // leitura 2 — sem cache
|
||||
```
|
||||
|
||||
Se `PluginConfigStore.get()` faz I/O (DB ou arquivo), o custo dobra em toda
|
||||
requisição que usa tool calling.
|
||||
|
||||
---
|
||||
|
||||
### 🟡 MÉDIO — Tool calling ativo para todas as mensagens
|
||||
|
||||
**Arquivo:** `sistema-nuvem/plugins/newwhats/webhook-receiver.js` linha ~450
|
||||
|
||||
```js
|
||||
tools: ['listar_horarios', 'agendar_horario', 'escalar_humano', 'encerrar_protocolo', 'human_api'],
|
||||
```
|
||||
|
||||
Todas as conversas enviam as definições completas de 5 tools ao LLM, mesmo para
|
||||
mensagens que jamais precisarão de agendamento (ex: "qual o preço da cerveja?").
|
||||
|
||||
Overhead duplo:
|
||||
1. **Tokens extras no prompt** → resposta mais lenta
|
||||
2. **Possíveis tool loops desnecessários** → `MAX_ITER = 5` iterações de LLM
|
||||
|
||||
**Impacto em alucinações:** o modelo pode invocar `listar_horarios` quando o cliente
|
||||
está só perguntando o preço de um produto, gerando uma resposta fora de contexto.
|
||||
|
||||
---
|
||||
|
||||
### 🟡 MÉDIO — Sumarização na 5ª e a cada 10ª mensagem (LLM extra)
|
||||
|
||||
**Arquivo:** `brain.ts:132`
|
||||
|
||||
```ts
|
||||
if (totalMsgs > 0 && (totalMsgs % 10 === 0 || totalMsgs === 5)) {
|
||||
summary = await this.summarize(agent, recentMessages, userMessage, response)
|
||||
}
|
||||
```
|
||||
|
||||
Na 5ª mensagem da conversa, há **duas chamadas LLM encadeadas**: a resposta ao
|
||||
cliente + o resumo. O resumo usa modelo barato (haiku/flash), mas ainda adiciona
|
||||
1–2s à latência percebida na primeira resposta "longa".
|
||||
|
||||
**Impacto em alucinações:** se o modelo de resumo alucinar no sumário, todas as
|
||||
respostas seguintes recebem um contexto corrompido na seção
|
||||
`=== ESTADO ATUAL DA CONVERSA ===`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Latências estimadas (cenário PDF)
|
||||
|
||||
| Etapa | Tempo atual | Com todos os fixes |
|
||||
|---|---|---|
|
||||
| `fetchMediaBuffer` (worst case com retries) | 3.500–6.500ms | 1.200–3.000ms |
|
||||
| Extração PDF via Gemini/OpenAI | 1.500–3.000ms | 1.500–3.000ms |
|
||||
| `getPluginConfig` + number lookup | 200–450ms | ~0ms (cache) |
|
||||
| `buildContext` + persona + brain (sequencial) | 400–600ms | ~0ms (paralelo com PDF) |
|
||||
| DB queries no `brain.chat()` (sequencial) | 200–400ms | ~80ms (paralelo) |
|
||||
| LLM response da secretaria (sem tools) | 2.000–4.000ms | 2.000–4.000ms |
|
||||
| LLM response (com tool call, 1 iteração) | 4.000–8.000ms | 4.000–8.000ms |
|
||||
| **Total estimado (sem tools)** | **8–15s** | **4.5–8s** |
|
||||
| **Total estimado (com tool call)** | **12–22s** | **7–14s** |
|
||||
|
||||
---
|
||||
|
||||
## 5. Correções recomendadas
|
||||
|
||||
### Fix 1 — Backoff do fetchMediaBuffer (trivial, impacto imediato)
|
||||
|
||||
**Arquivo:** `media-transcribe.js:25–26`
|
||||
|
||||
```js
|
||||
// ANTES:
|
||||
const DOWNLOAD_RETRIES = 4
|
||||
const DOWNLOAD_BACKOFF = [500, 1000, 2000, 3000]
|
||||
|
||||
// DEPOIS:
|
||||
const DOWNLOAD_RETRIES = 3
|
||||
const DOWNLOAD_BACKOFF = [1200, 2000, 3000]
|
||||
```
|
||||
|
||||
Aguarda 1.2s na primeira tentativa (cobre ~90% dos casos onde o motor já terminou)
|
||||
e reduz o número de retries desnecessários de 3 para 2.
|
||||
|
||||
---
|
||||
|
||||
### Fix 2 — Cache em memória para `getPluginConfig`
|
||||
|
||||
**Arquivo:** `webhook-receiver.js` (substituir função existente)
|
||||
|
||||
```js
|
||||
let _cfgCache = null
|
||||
let _cfgExpiry = 0
|
||||
|
||||
async function getPluginConfig() {
|
||||
if (_cfgCache && Date.now() < _cfgExpiry) return _cfgCache
|
||||
const rows = await query(`SELECT key, value FROM plugin_configs WHERE plugin_id = 'newwhats'`)
|
||||
_cfgCache = Object.fromEntries(rows.map(r => [r.key, r.value]))
|
||||
_cfgExpiry = Date.now() + 60_000 // 60s — config muda raramente
|
||||
return _cfgCache
|
||||
}
|
||||
|
||||
// Expor para o painel forçar invalidação após salvar config:
|
||||
function invalidatePluginConfigCache() { _cfgCache = null }
|
||||
module.exports = { createWebhookReceiver, invalidatePluginConfigCache }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Fix 3 — Cache em memória para number lookup
|
||||
|
||||
**Arquivo:** `webhook-receiver.js` (dentro de `handleMessageNew`)
|
||||
|
||||
```js
|
||||
const _numberCache = new Map() // instanceId → { role, active, ts }
|
||||
const NUMBER_CACHE_TTL = 5 * 60_000 // 5 min
|
||||
|
||||
async function getNumberRole(motorUrl, integKey, instanceId) {
|
||||
const cached = _numberCache.get(instanceId)
|
||||
if (cached && Date.now() - cached.ts < NUMBER_CACHE_TTL) return cached
|
||||
try {
|
||||
const res = await fetch(`${motorUrl}/api/ext/v1/secretaria/numbers`, {
|
||||
headers: { 'x-nw-key': integKey },
|
||||
})
|
||||
if (!res.ok) return null
|
||||
const nums = await res.json()
|
||||
const info = Array.isArray(nums) ? nums.find(n => n.instance_id === instanceId) : null
|
||||
const entry = { role: info?.role ?? null, active: info?.active ?? true, ts: Date.now() }
|
||||
_numberCache.set(instanceId, entry)
|
||||
return entry
|
||||
} catch { return null }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Fix 4 — Extração PDF em paralelo com context-builder
|
||||
|
||||
**Arquivo:** `webhook-receiver.js` (bloco DOCUMENT dentro de `handleMessageNew`)
|
||||
|
||||
```js
|
||||
// ANTES (sequencial):
|
||||
const pdfText = await extractPdfText({ motorUrl, integKey, messageId: msg.id, captionFromUser: body || null })
|
||||
if (!pdfText) { ... return }
|
||||
effectiveBody = body ? `${pdfText}\nMensagem do cliente: ${body}` : pdfText
|
||||
// ... depois autoReply() → buildContext() → resolvePersona() → brainRows
|
||||
|
||||
// DEPOIS — extração em paralelo com preparação do contexto:
|
||||
const [pdfText, ctxPrefetch] = await Promise.all([
|
||||
extractPdfText({ motorUrl, integKey, messageId: msg.id, captionFromUser: body || null }),
|
||||
buildContext(phone, 1, body || '', chatId).catch(() => null),
|
||||
])
|
||||
if (!pdfText) { ... return }
|
||||
effectiveBody = body ? `${pdfText}\nMensagem do cliente: ${body}` : pdfText
|
||||
// Passar ctxPrefetch diretamente para autoReply() — evita reconstruir
|
||||
```
|
||||
|
||||
> Requer refatorar `autoReply()` para aceitar `ctxPrefetch` opcional.
|
||||
|
||||
---
|
||||
|
||||
### Fix 5 — Paralelizar queries no `brain.chat()`
|
||||
|
||||
**Arquivo:** `newwhats.local/plugins/secretaria/brain.ts:35–65`
|
||||
|
||||
```ts
|
||||
// ANTES (6 queries sequenciais):
|
||||
const conversation = await this.db('sec_conversations').where({ id: conversationId }).first()
|
||||
const agent = await this.db('sec_agents').where({ id: conversation.agent_id }).first()
|
||||
const nodes = await this.db('sec_brain_nodes').where(...).orderBy('sort_order')
|
||||
const recentMessages = await this.db('sec_messages')...
|
||||
const personaNode = await this.db('sec_brain_nodes').where({ type: 'persona' })... // duplicado!
|
||||
|
||||
// DEPOIS — 2 fases:
|
||||
// Fase 1: busca conversa (necessária para agent_id)
|
||||
const conversation = await this.db('sec_conversations').where({ id: conversationId }).first()
|
||||
if (!conversation) throw new Error('Conversa não encontrada')
|
||||
|
||||
// Fase 2: tudo que depende de agent_id em paralelo
|
||||
const contextWindow = 8 // valor padrão antes de ter o agent
|
||||
const [agent, nodes, recentMessages] = await Promise.all([
|
||||
this.db('sec_agents').where({ id: conversation.agent_id }).first(),
|
||||
this.db('sec_brain_nodes').where({ agent_id: conversation.agent_id, active: true }).orderBy('sort_order'),
|
||||
this.db('sec_messages')
|
||||
.where({ conversation_id: conversationId })
|
||||
.orderBy('created_at', 'desc')
|
||||
.limit(contextWindow)
|
||||
.then((rows: any[]) => rows.reverse()),
|
||||
])
|
||||
|
||||
// personaNode é subset de nodes — extrair do resultado já carregado:
|
||||
const personaNode = (nodes as any[]).find(n => n.type === 'persona')
|
||||
// Elimina a query duplicada da linha 62!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Fix 6 — Tools seletivos por contexto da mensagem
|
||||
|
||||
**Arquivo:** `webhook-receiver.js` (dentro de `autoReply`)
|
||||
|
||||
```js
|
||||
// ANTES: sempre envia todas as tools
|
||||
tools: ['listar_horarios', 'agendar_horario', 'escalar_humano', 'encerrar_protocolo', 'human_api'],
|
||||
|
||||
// DEPOIS: tools mínimas + scheduling só quando o contexto pede
|
||||
const needsScheduling = /\b(agend|hor[áa]rio|consulta|marcar|agendar|reservar|data|dia\s+dispon)\b/i.test(effectiveBody)
|
||||
const tools = [
|
||||
...(needsScheduling ? ['listar_horarios', 'agendar_horario'] : []),
|
||||
'escalar_humano',
|
||||
'encerrar_protocolo',
|
||||
'human_api',
|
||||
]
|
||||
```
|
||||
|
||||
Reduz tokens no prompt e elimina tool calls espúrios em conversas que não
|
||||
envolvem agendamento.
|
||||
|
||||
---
|
||||
|
||||
## 6. Relação entre cada gargalo e risco de alucinação
|
||||
|
||||
| Gargalo | Risco de alucinação | Motivo |
|
||||
|---|---|---|
|
||||
| fetchMediaBuffer falha total | **ALTO** | LLM não recebe o PDF; inventa conteúdo |
|
||||
| Fallback Gemini → OpenAI → Claude | **MÉDIO** | Persona/instrução diferente por provider |
|
||||
| context-builder sequencial | **BAIXO** | Atraso apenas, contexto chega completo |
|
||||
| summarize() corrompido | **ALTO** | Todas as próximas respostas usam resumo errado |
|
||||
| tool calls em loop (5 iter.) | **MÉDIO** | Resposta truncada ou sem dados da tool |
|
||||
| config.get() lento | **BAIXO** | Atraso, não afeta contexto diretamente |
|
||||
| Timeout geral >15s | **ALTO** | Cliente manda nova msg → IA responde sem contexto do PDF |
|
||||
|
||||
---
|
||||
|
||||
## 7. Priorização de implementação
|
||||
|
||||
| # | Fix | Esforço | Impacto | Alucinação |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Backoff `fetchMediaBuffer` | ⭐ trivial | 2–3s ganhos | Evita PDF perdido |
|
||||
| 2 | Cache `getPluginConfig` | ⭐ trivial | 150ms/msg | Não direto |
|
||||
| 3 | Cache number lookup | ⭐ trivial | 200ms/msg | Não direto |
|
||||
| 4 | Queries paralelas em `brain.chat()` | ⭐⭐ médio | 200ms/req | Não direto |
|
||||
| 5 | PDF + context-builder em paralelo | ⭐⭐ médio | 400ms/PDF | Não direto |
|
||||
| 6 | Tools seletivos | ⭐⭐ médio | 1–4s em msgs simples | Reduz tool calls errados |
|
||||
| 7 | Eliminar query duplicada `personaNode` | ⭐ trivial | 80ms/req | Não direto |
|
||||
|
||||
---
|
||||
|
||||
## 8. Arquivos-chave para as correções
|
||||
|
||||
| Arquivo | Fixes aplicáveis |
|
||||
|---|---|
|
||||
| `sistema-nuvem/plugins/newwhats/media-transcribe.js` | Fix 1 (backoff) |
|
||||
| `sistema-nuvem/plugins/newwhats/webhook-receiver.js` | Fix 2, 3, 4, 6 |
|
||||
| `newwhats.local/plugins/secretaria/brain.ts` | Fix 5, 7 |
|
||||
|
||||
---
|
||||
|
||||
## 9. Causa raiz do "30+ segundos para Oi!" — Diagnóstico e fixes aplicados
|
||||
|
||||
### Causa 1 — `fetch` sem timeout (PRINCIPAL)
|
||||
|
||||
**Arquivo:** `brain.ts` — todas as chamadas a OpenAI, Anthropic, Gemini, Ollama
|
||||
|
||||
```ts
|
||||
// ANTES — sem timeout: se a API travar, a requisição espera para sempre
|
||||
const res = await fetch('https://api.openai.com/v1/chat/completions', { ... })
|
||||
|
||||
// DEPOIS — máximo 25s; se ultrapassar, lança AbortError → fallback chain
|
||||
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
signal: AbortSignal.timeout(25_000),
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
**✅ APLICADO** em 7 pontos: `callProvider` (OpenAI, Anthropic, Gemini, Ollama) +
|
||||
`openAIToolLoop` + `anthropicToolLoop` + `geminiToolLoop`.
|
||||
|
||||
**Impacto em alucinações:** sem timeout, qualquer travamento da API deixava o LLM
|
||||
sem resposta enquanto o cliente enviava novas mensagens, criando contexto corrompido.
|
||||
|
||||
---
|
||||
|
||||
### Causa 2 — Gemini 429 → espera de até 30s
|
||||
|
||||
**Arquivo:** `brain.ts:458`
|
||||
|
||||
```ts
|
||||
// ANTES: espera 5s (default) ou até 30s (se API informar retry-in: 30s)
|
||||
const waitMs = match ? Math.min(Math.ceil(...) * 1000, 30_000) : 5_000
|
||||
|
||||
// DEPOIS: espera máximo 8s, default 1.5s — Gemini com quota cai rápido ao próximo provider
|
||||
const waitMs = match ? Math.min(Math.ceil(...) * 1000, 8_000) : 1_500
|
||||
```
|
||||
|
||||
**✅ APLICADO.**
|
||||
|
||||
Antes: Gemini em quota → espera 5s → retry → falha → tenta OpenAI → 7-10s só no Gemini.
|
||||
Depois: Gemini em quota → espera 1.5s → retry → falha → tenta OpenAI → 3-5s.
|
||||
|
||||
---
|
||||
|
||||
### Causa 3 — API key auth faz query Prisma em CADA requisição ao motor
|
||||
|
||||
**Arquivo:** `plugins/ext-api/backend/apikey-auth.ts`
|
||||
|
||||
Cada mensagem do satélite dispara **3 requisições HTTP ao motor**:
|
||||
1. `GET /api/ext/v1/secretaria/numbers` (verificar role)
|
||||
2. `POST /api/ext/v1/secretaria/ask` (chamar brain)
|
||||
3. `POST /api/ext/v1/inbox/:chatId/send` (enviar resposta)
|
||||
|
||||
Cada uma passava pelo middleware `apiKeyAuth` que fazia `prisma.apiKey.findUnique()`
|
||||
— **3 queries ao PostgreSQL por mensagem**, sem cache.
|
||||
|
||||
```ts
|
||||
// DEPOIS — cache em memória de 5 minutos por chave
|
||||
const cached = keyCache.get(key)
|
||||
if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) {
|
||||
req.extTenantId = cached.tenantId
|
||||
next(); return
|
||||
}
|
||||
// Só vai ao banco se cache expirou
|
||||
```
|
||||
|
||||
**✅ APLICADO** — elimina ~3 × 50ms = 150ms de queries por mensagem.
|
||||
|
||||
---
|
||||
|
||||
### Causa 4 — `getPluginConfig()` e number lookup sem cache
|
||||
|
||||
**Arquivo:** `webhook-receiver.js`
|
||||
|
||||
```js
|
||||
// ANTES: query ao banco em TODA mensagem recebida
|
||||
async function getPluginConfig() {
|
||||
const rows = await query(`SELECT key, value FROM plugin_configs WHERE plugin_id = 'newwhats'`)
|
||||
...
|
||||
}
|
||||
|
||||
// DEPOIS: cache de 60s em memória
|
||||
if (_cfgCache && Date.now() < _cfgExpiry) return _cfgCache
|
||||
```
|
||||
|
||||
Igualmente, o lookup de `role` do número fazia HTTP ao motor em cada mensagem.
|
||||
Substituído por `getNumberRole()` com cache de 5 minutos.
|
||||
|
||||
**✅ APLICADO** — elimina ~200-500ms por mensagem.
|
||||
|
||||
---
|
||||
|
||||
### Resumo dos fixes aplicados em 2026-05-05
|
||||
|
||||
| Fix | Arquivo(s) modificado(s) | Ganho estimado |
|
||||
|---|---|---|
|
||||
| `AbortSignal.timeout(25_000)` em todos os fetches LLM | `brain.ts` + `brain.js` | Elimina hangs de 30-∞s |
|
||||
| Gemini retry wait: 5s→1.5s, max 30s→8s | `brain.ts` + `brain.js` | -3.5s em quota errors |
|
||||
| Cache de API key (5min) | `apikey-auth.ts` + `apikey-auth.js` | -150ms/msg |
|
||||
| Cache de `getPluginConfig` (60s) | `webhook-receiver.js` | -100ms/msg |
|
||||
| Cache de number role (5min) | `webhook-receiver.js` | -200ms/msg |
|
||||
| **Total mensagem simples (ex: "Oi!")** | | **-4 a 34s** |
|
||||
|
||||
### Pendentes (próximos fixes)
|
||||
|
||||
| Fix | Arquivo | Ganho |
|
||||
|---|---|---|
|
||||
| Backoff `fetchMediaBuffer`: `[1200, 2000, 3000]` | `media-transcribe.js` | -3s/PDF |
|
||||
| Queries paralelas em `brain.chat()` | `brain.ts` | -200ms/req |
|
||||
| Eliminar query duplicada `personaNode` | `brain.ts` | -80ms/req |
|
||||
| Tools seletivos por contexto | `webhook-receiver.js` | -1-4s em msgs simples |
|
||||
| PDF + context-builder em paralelo | `webhook-receiver.js` | -400ms/PDF |
|
||||
Reference in New Issue
Block a user