Files
clube67_newwhats.local/newwhats.clube67.com/newwhats.local/plugins/secretaria/embeddings.js
T
VPS 4 Deploy Agent f51785bace fix(secretaria): embed() cai para Gemini se OpenAI falhar (não derruba RAG)
Antes, com openai_key presente, qualquer falha (ex.: 429 insufficient_quota)
retornava null sem tentar o Gemini. Agora tenta OpenAI e, em falha, usa Gemini.
Aviso documentado: OpenAI (1536d) e Gemini (768d) são incompatíveis — trocar de
provider exige reindexar (sync-knowledge + memória de contato).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 15:23:45 +02:00

127 lines
4.4 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.chunkText = chunkText;
exports.hashText = hashText;
exports.cosineSimilarity = cosineSimilarity;
exports.embed = embed;
exports.hasEmbeddingKey = hasEmbeddingKey;
/**
* RAG sem pgvector — embeddings + similaridade calculados na aplicação.
*
* Os vetores são gerados via OpenAI (text-embedding-3-small) ou Gemini
* (text-embedding-004), conforme a chave configurada no plugin, e guardados
* como JSON em coluna text. A busca por similaridade (cosseno) roda em Node.
* Para a base pequena da secretária (dezenas/centenas de chunks) é rápido e
* dispensa a extensão pgvector.
*/
const crypto_1 = __importDefault(require("crypto"));
/** Quebra o texto em chunks (~maxLen chars), preferindo limites de parágrafo. */
function chunkText(text, maxLen = 600) {
const paras = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
const merged = [];
let buf = '';
for (const p of paras) {
if (buf && (buf.length + 2 + p.length) > maxLen) {
merged.push(buf);
buf = p;
}
else {
buf = buf ? `${buf}\n\n${p}` : p;
}
}
if (buf)
merged.push(buf);
// Fatia parágrafos gigantes que excedam bastante o limite.
const out = [];
for (const c of merged) {
if (c.length <= maxLen * 1.5) {
out.push(c);
continue;
}
for (let i = 0; i < c.length; i += maxLen)
out.push(c.slice(i, i + maxLen));
}
return out.filter(Boolean);
}
function hashText(text) {
return crypto_1.default.createHash('md5').update(text).digest('hex');
}
function cosineSimilarity(a, b) {
const len = Math.min(a.length, b.length);
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < len; i++) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
if (!na || !nb)
return 0;
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
/**
* Gera o embedding de um texto. Usa OpenAI se houver openai_key, senão Gemini.
* Retorna null em qualquer falha (o chamador deve cair no fallback).
*/
async function embed(text, cfg) {
const input = (text ?? '').slice(0, 8000);
if (!input.trim())
return null;
const openaiKey = cfg?.openai_key;
const geminiKey = cfg?.gemini_key;
// Preferência: OpenAI; em QUALQUER falha (ex.: 429 insufficient_quota), cai para
// o Gemini — assim uma chave OpenAI sem créditos não derruba o RAG.
// ⚠️ OpenAI (1536-dim) e Gemini (768-dim) geram espaços vetoriais incompatíveis.
// Ao trocar de provider é preciso REINDEXAR (sync-knowledge + memória de contato),
// senão o cosseno mistura dimensões e o retrieval fica sem sentido.
if (openaiKey) {
const v = await embedOpenAI(input, openaiKey);
if (v)
return v;
}
if (geminiKey) {
const v = await embedGemini(input, geminiKey);
if (v)
return v;
}
return null;
}
async function embedOpenAI(input, apiKey) {
try {
const res = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({ model: 'text-embedding-3-small', input }),
});
if (!res.ok)
return null;
const data = await res.json();
return data?.data?.[0]?.embedding ?? null;
}
catch {
return null;
}
}
async function embedGemini(input, apiKey) {
try {
const url = `https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key=${apiKey}`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'models/text-embedding-004', content: { parts: [{ text: input }] } }),
});
if (!res.ok)
return null;
const data = await res.json();
return data?.embedding?.values ?? null;
}
catch {
return null;
}
}
function hasEmbeddingKey(cfg) {
return !!(cfg?.openai_key || cfg?.gemini_key);
}
//# sourceMappingURL=embeddings.js.map