Spaces:
Paused
Paused
File size: 6,039 Bytes
ee826ee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | /**
* semantic-cache.js β Cache SemΓ‘ntico con Embeddings
* ====================================================
* Usa Gemini text-embedding-004 (gratis) para convertir preguntas
* en vectores y comparar por similitud coseno.
* "cuΓ‘les son las reglas" y "dime las reglas del server" β mismo hit.
* Reduce hasta 73% de llamadas a la API en trΓ‘fico repetitivo.
*/
import { readConfig } from './utils.js';
const config = readConfig();
// Rotar keys de Gemini β si una falla pasa a la siguiente
const _failedGeminiKeys = new Set();
// Registro de funciΓ³n de embedding local β se registra desde local-ai.js al estar listo
// Evita importaciΓ³n circular entre semantic-cache β local-ai
let _localEmbedFn = null;
export function registerLocalEmbedFn(fn) {
_localEmbedFn = fn;
console.log('[SemanticCache] FunciΓ³n de embedding local registrada β
');
}
function getGeminiKey() {
const keys = [
...(config.ai?.gemini?.keys ?? []),
config.ai?.gemini?.apiKey,
].filter(Boolean);
// Devolver primera key que no haya fallado
for (const k of keys) {
if (!_failedGeminiKeys.has(k)) return k;
}
// Si todas fallaron, resetear y reintentar con la primera
_failedGeminiKeys.clear();
return keys[0] ?? null;
}
function markGeminiKeyFailed(key) {
if (key) _failedGeminiKeys.add(key);
// Si todas las keys fallaron, deshabilitar embeddings para no spamear logs
const allKeys = [
...(config.ai?.gemini?.keys ?? []),
config.ai?.gemini?.apiKey,
].filter(Boolean);
if (_failedGeminiKeys.size >= allKeys.length) {
_embeddingDisabled = true;
console.log('[SemanticCache] Todas las keys Gemini fallaron β usando cache exacto (sin embeddings)');
}
}
// ββ Coseno puro en JS (sin librerΓas) ββββββββββββββββββββββββββββββββββββββββ
function cosineSimilarity(a, b) {
if (!a?.length || !b?.length || a.length !== b.length) return 0;
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom === 0 ? 0 : dot / denom;
}
// ββ Embedding via Gemini API ββββββββββββββββββββββββββββββββββββββββββββββββββ
const _embedCache = new Map(); // texto β embedding (evita re-embedear lo mismo)
let _embeddingDisabled = false; // si todas las keys fallan, deshabilitar silenciosamente
export async function embed(text) {
const key = text.slice(0, 200);
if (_embedCache.has(key)) return _embedCache.get(key);
let vector = null;
// 1. Intentar con funciΓ³n de embedding local registrada (evita import circular)
if (_localEmbedFn) {
try {
vector = await _localEmbedFn(text);
} catch { /* local no disponible */ }
}
// 2. Fallback: Gemini API si el local no estΓ‘ listo
if (!vector && !_embeddingDisabled) {
const geminiKey = getGeminiKey();
if (geminiKey) {
try {
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key=${geminiKey}`,
{
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify({ model: 'models/text-embedding-004', content: { parts: [{ text }] } }),
signal : AbortSignal.timeout(8000),
}
);
if (!res.ok) {
markGeminiKeyFailed(geminiKey);
} else {
vector = (await res.json()).embedding?.values ?? null;
}
} catch {}
}
}
if (!vector?.length) throw new Error('Sin vector de embedding disponible');
_embedCache.set(key, vector);
if (_embedCache.size > 2000) _embedCache.delete(_embedCache.keys().next().value);
return vector;
}
// ββ Semantic Cache ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export class SemanticCache {
constructor(threshold = 0.88) {
this.entries = []; // { embedding, response, ts, ttl, query }
this.threshold = threshold;
this.hits = 0;
this.misses = 0;
this.errors = 0;
this.maxSize = 500;
}
async get(query) {
try {
const now = Date.now();
// Limpiar expirados
this.entries = this.entries.filter(e => now - e.ts < e.ttl);
const qEmb = await embed(query);
for (const e of this.entries) {
const sim = cosineSimilarity(qEmb, e.embedding);
if (sim >= this.threshold) {
this.hits++;
console.log(`[SemanticCache] HIT (sim=${sim.toFixed(3)}): "${query.slice(0,60)}"`);
return e.response;
}
}
this.misses++;
return null;
} catch (e) {
this.errors++;
return null; // fallo silencioso β usar cache normal
}
}
async set(query, response, ttlMs = 3_600_000) {
try {
const embedding = await embed(query);
this.entries.push({ embedding, response, ts: Date.now(), ttl: ttlMs, query: query.slice(0, 100) });
if (this.entries.length > this.maxSize) this.entries.shift();
} catch { /* silencioso */ }
}
// TTL semΓ‘ntico segΓΊn tipo de pregunta
getTTL(query) {
const q = query.toLowerCase();
if (/regla|norma|info|servidor|comandos|plugin/.test(q)) return 86_400_000; // 24h
if (/online|jugadores|tps|lag|estado/.test(q)) return 30_000; // 30s
return 3_600_000; // 1h default
}
stats() {
const total = this.hits + this.misses;
return {
hits : this.hits,
misses : this.misses,
hitRate : total > 0 ? ((this.hits / total) * 100).toFixed(1) + '%' : '0%',
entries : this.entries.length,
threshold: this.threshold,
};
}
}
export const semanticCache = new SemanticCache(0.88);
|