Assistente de FAQ e catálogo com RAG híbrido (lexical + vetorial) via OpenRouter, Express e Vite.
flowchart TD
U[User message] --> Chat[POST /api/chat]
Chat --> Classify[OpenRouter call #1<br/>model: OPENROUTER_MODEL_CLASSIFY<br/>retorna FAQ/CATALOG/MIST/OTHER]
Classify --> Route{Intent}
Route -->|OTHER| NoSearch[Nenhuma busca<br/>contexto vazio]
Route -->|FAQ| FAQ[searchFaqsHybrid → merge]
Route -->|CATALOG| Catalog[searchCatalogHybrid → merge]
Route -->|MIST| FAQ
Route -->|MIST| Catalog
FAQ --> FAQCtx[Contexto de FAQs<br/>logToolPayload + logFaqHybridStats]
Catalog --> CatalogCtx[Contexto do catálogo<br/>logToolPayload + logHybridStats]
NoSearch --> FinalPrep
FAQCtx --> FinalPrep[Consolida contexto<br/>+ instrução responder]
CatalogCtx --> FinalPrep
FinalPrep --> Answer[OpenRouter call #2<br/>model: OPENROUTER_MODEL_ANSWER<br/>sem tools]
Answer --> Resp[Resposta final + debug]
- O payload do chat inclui
history(user/assistant) com as últimas interações; o backend limita o número de mensagens usadas no contexto viaCHAT_HISTORY_CONTEXT_LIMIT(default 6, máx. 20) e trunca cada uma a ~1200 caracteres para evitar estourar tokens. - Chamada #1 (classificação) instrui a IA a retornar apenas uma palavra (
FAQ,CATALOG,MIST,OTHER), registrando o modelo usado (OPENROUTER_MODEL_CLASSIFY,OPENROUTER_MODEL_CLASSIFY_FALLBACKou fallback padrão). - O backend decide quais buscas executar com base na intenção (
searchFaqsHybrid,searchCatalogHybrid, ambos ou nenhum), monta um único contexto e registra logs (classification=...,usedTools,llmCalls=0/1/2,logToolPayload,logFaqHybridStats,logHybridStats). - Chamada #2 (
OPENROUTER_MODEL_ANSWER) recebe somente o contexto consolidado como mensagenssysteme o texto do usuário; não usa tools e não pode mencionar a palavra de intenção. - O
debugda resposta inclui a intenção detectada, modelos usados (classify/answer), flags de banco, contagens de FAQs/itens,ragSource,usedToolsellmCallscoerente com as buscas disparadas.
CHAT_HISTORY_CONTEXT_LIMIT(default 6, máx. 20): número de mensagens recentes (user/assistant) que entram no contexto enviado ao LLM. A API aceita um arrayhistorye usa apenas as últimas mensagens nessa ordem, truncando cada conteúdo a ~1200 caracteres. Ajuste para balancear recall x custo de tokens.OPENROUTER_API_KEY: necessário para gerar embeddings; sem ele, as buscas híbridas caem para o fallback lexical.FAQ_HYBRID_ENABLED(default true): ativa busca híbrida (lexical + vetorial) de FAQs; definafalsepara forçar apenas lexical.FAQ_VECTOR_THRESHOLD(default -0.5): filtro de similaridade na busca vetorial de FAQs (menor = mais próximo).FAQ_VECTOR_WEIGHT/FAQ_LEXICAL_WEIGHT(default 6/4): pesos no merge quandoHYBRID_SEARCH_ENHANCED=true.FAQ_SNIPPET_LIMIT(default 220): limite de caracteres do snippet das FAQs usado no contexto/debug.HYBRID_SEARCH_ENHANCED(default false): habilita merge ponderado (FAQ e catálogo); sefalse, usa dedupe simples com prioridade vetorial.
- Rotas:
GET /webhooks/whatsappdevolvehub.challengequandohub.verify_tokenbate comWHATSAPP_VERIFY_TOKEN;POST /webhooks/whatsappprocessaentry[].changes[].value.messages[].text.body. - Envs obrigatórias:
WHATSAPP_VERIFY_TOKEN,WHATSAPP_APP_SECRET(para validarX-Hub-Signature-256com o corpo bruto),WHATSAPP_ACCESS_TOKEN,WHATSAPP_PHONE_NUMBER_ID; opcionalWHATSAPP_CHAT_BASE_URL(defaulthttp://localhost:${PORT}/api/chat). - Fluxo: usa
message.idpara idempotência em cache em memória (~15 minutos) e derivasessionIdcomowa:{from}; repassa o texto para/api/chate envia a resposta viaPOST https://graph.facebook.com/{version}/{phone_number_id}/messages(defaultv20.0). - Erros de assinatura retornam
401, payloads sem texto retornam200 { status: "ignored" }, duplicatas retornam{ status: "duplicate" }. - Teste rápido local: gere a assinatura com
printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WHATSAPP_APP_SECRET" | awk '{print \"sha256=\" $2}'e envie comcurl -X POST http://localhost:3000/webhooks/whatsapp -H "Content-Type: application/json" -H "X-Hub-Signature-256: $SIGNATURE" -d "$BODY".
- Híbrido direto:
curl -X POST http://localhost:3000/api/rag/search -H "Content-Type: application/json" -d '{"query":"adubo foliar","limit":5}' - Chat end-to-end: perguntar sobre um produto; o retorno inclui
debugcom flags do RAG. - Backfill FAQ embeddings:
npx tsx scripts/seedFaqEmbeddings.ts - Debug FAQ híbrido:
npx tsx scripts/debugFaqHybrid.ts "sua pergunta aqui"
- O backend expõe
GET /api/instructions(filtro opcional?scope=chat,catalog) ePUT /api/instructions/:slugpara atualizar o conteúdo versionado na tabelasystem_instructions. - A SPA mostra o painel diretamente nas páginas de Chat e Catálogo, reutilizando o componente
InstructionsPanelpara listar, editar e salvar instruções com React Query. - O prompt do chat passou a ser dividido em duas mensagens
system:buscar-dados(etapa 1, coleta de contexto) eresponder-usuario(etapa 2, formatação da resposta). Ambas são lidas do banco em ordem determinística e têm fallback codificado caso o registro seja removido. - Após atualizar
shared/schema.ts, rodenpm run db:pushpara criar/alterar a tabela e inserir os seeds (global-operating-principles,buscar-dados,responder-usuario,catalog-guidelines).
GET /api/catalog/import/templategera a planilha.xlsxcom cabeçalho fixo (Nome, Descrição, Categoria, Fabricante, Preço, Status, Tags) e duas linhas de exemplo.POST /api/catalog/importaceita apenas.xlsx(5MB, 500 linhas úteis) viamultipart/form-datacom campofile; valida cabeçalho, deduplica linhas por par nome+fabricante e aplica o schema existente do catálogo.- Em caso de erro, retorna
400com{ errors: [{ row, fields, message }] }sem inserir nada. Sucesso retorna{ created, durationMs, sampleIds }. - A página
/catalogotem a seção “Importar catálogo em lote” com download do template, upload arraste-e-solte e resumo de erros ou itens criados; ao concluir, a lista é atualizada automaticamente.