Spaces:
Paused
Paused
File size: 6,363 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 | /**
* rag.js β RAG para TomateSMP
* ============================
* Retrieval-Augmented Generation: indexa el conocimiento del servidor
* y recupera chunks relevantes para cada pregunta.
* Sin vectra ni Docker β array en memoria + Gemini embeddings.
* Se inicializa al arrancar con server_knowledge.js.
*/
import { embed } from './semantic-cache.js';
import { SERVER_KNOWLEDGE } from '../server_knowledge.js';
function cosineSim(a, b) {
if (!a?.length || !b?.length) return 0;
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < a.length; i++) { dot += a[i]*b[i]; na += a[i]**2; nb += b[i]**2; }
return Math.sqrt(na)*Math.sqrt(nb) === 0 ? 0 : dot/(Math.sqrt(na)*Math.sqrt(nb));
}
// ββ Chunker: divide el conocimiento en fragmentos manejables βββββββββββββββββ
function chunkText(text, maxChars = 400, overlap = 50) {
const sections = text.split(/\n## /);
const chunks = [];
for (const section of sections) {
if (!section.trim()) continue;
const sectionText = section.startsWith('##') ? section : '## ' + section;
// Si el section cabe entero, aΓ±adirlo tal cual
if (sectionText.length <= maxChars) {
chunks.push(sectionText.trim());
continue;
}
// Si es largo, dividir por pΓ‘rrafos
const paragraphs = sectionText.split('\n\n');
let current = '';
for (const para of paragraphs) {
if (current.length + para.length > maxChars && current.length > 0) {
chunks.push(current.trim());
// Overlap: incluir el final del chunk anterior
current = current.slice(-overlap) + '\n' + para;
} else {
current += '\n\n' + para;
}
}
if (current.trim()) chunks.push(current.trim());
}
return chunks.filter(c => c.length > 30);
}
// ββ Index en memoria ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const _index = []; // { text, embedding, category }
let _indexed = false;
let _indexing = false;
export async function initRAG() {
if (_indexed || _indexing) return;
if (!SERVER_KNOWLEDGE) { console.log('[RAG] server_knowledge.js vacΓo β RAG desactivado'); return; }
_indexing = true;
console.log('[RAG] Indexando conocimiento de TomateSMP...');
const chunks = chunkText(SERVER_KNOWLEDGE);
console.log(`[RAG] ${chunks.length} chunks a indexar...`);
// Probar si los embeddings de Gemini funcionan con un chunk de prueba
let embeddingsWorking = false;
try {
await embed(chunks[0].slice(0, 50));
embeddingsWorking = true;
console.log('[RAG] β
Embeddings disponibles (local o Gemini)');
} catch (e) {
console.warn('[RAG] Embeddings no disponibles aΓΊn (modelo cargando), usando bΓΊsqueda textual');
}
// Indexar chunks (con o sin embeddings)
const BATCH = 5;
for (let i = 0; i < chunks.length; i += BATCH) {
const batch = chunks.slice(i, i + BATCH);
await Promise.allSettled(batch.map(async text => {
if (embeddingsWorking) {
try {
const embedding = await embed(text);
_index.push({ text, embedding });
} catch {
_index.push({ text, embedding: null }); // fallback textual
}
} else {
_index.push({ text, embedding: null }); // solo textual
}
}));
if (embeddingsWorking && i + BATCH < chunks.length) {
await new Promise(r => setTimeout(r, 200));
}
}
_indexed = true;
_indexing = false;
const indexed = _index.filter(e => e.embedding).length;
console.log(`[RAG] β
Indexado: ${indexed}/${_index.length} chunks con embedding`);
}
// ββ Retrieval: obtener chunks mΓ‘s relevantes para una query ββββββββββββββββββ
export async function retrieveContext(query, topK = 3) {
if (!_indexed || _index.length === 0) return '';
try {
const queryEmb = await embed(query);
const scored = _index
.filter(e => e.embedding)
.map(e => ({ text: e.text, score: cosineSim(queryEmb, e.embedding) }))
.filter(e => e.score > 0.5)
.sort((a, b) => b.score - a.score)
.slice(0, topK);
if (!scored.length) return '';
return scored.map(e => e.text).join('\n\n---\n\n');
} catch {
// Fallback textual: buscar por keywords mΓΊltiples
const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 3);
const scored = _index.map(e => {
const t = e.text.toLowerCase();
const score = words.reduce((s, w) => s + (t.includes(w) ? 1 : 0), 0);
return { text: e.text, score };
}).filter(e => e.score > 0).sort((a, b) => b.score - a.score).slice(0, topK);
return scored.map(e => e.text).join('\n\n---\n\n');
}
}
// ββ Inyectar contexto RAG en el system prompt βββββββββββββββββββββββββββββββββ
// Devuelve solo el string de contexto RAG (sin el system prompt base)
// Γtil para paralelizar con otras operaciones
export async function getRAGSuffix(userMessage) {
if (!_indexed) return '';
const serverKeywords = /regla|comando|rango|plugin|crate|job|skill|habilidad|warp|server|servidor|pvp|koth|dungeon|trade|battlepass|tag|badge|insignia|economΓa|dinero|mobcoin|claim|protecciΓ³n|terreno|matrimonio|mascota|voice|voz/i;
if (!serverKeywords.test(userMessage)) return '';
const context = await retrieveContext(userMessage, 3);
if (!context) return '';
return `\n\n## INFORMACIΓN RELEVANTE DEL SERVIDOR (RAG)\n${context}`;
}
export async function injectRAGContext(systemPrompt, userMessage) {
if (!_indexed) return systemPrompt;
// Solo activar RAG si la pregunta parece ser sobre el servidor
const serverKeywords = /regla|comando|rango|plugin|crate|job|skill|habilidad|warp|server|servidor|pvp|koth|dungeon|trade|battlepass|tag|badge|insignia|economΓa|dinero|mobcoin|claim|protecciΓ³n|terreno|matrimonio|mascota|voice|voz/i;
if (!serverKeywords.test(userMessage)) return systemPrompt;
const context = await retrieveContext(userMessage, 3);
if (!context) return systemPrompt;
return systemPrompt + `\n\n## INFORMACIΓN RELEVANTE DEL SERVIDOR (RAG)\n${context}`;
}
export function ragStats() {
return { indexed: _indexed, chunks: _index.length, withEmbedding: _index.filter(e=>e.embedding).length };
}
|