Spaces:
Paused
Paused
| /** | |
| * reflexion.js β Reflexion Loop para Zelin | |
| * ========================================== | |
| * Basado en: Shinn et al., "Reflexion: Language Agents with Verbal RL" (NeurIPS 2023) | |
| * Resultado real: 88% HumanEval vs 67% GPT-4 usando solo auto-correcciΓ³n. | |
| * | |
| * ARQUITECTURA (3 roles separados): | |
| * Actor β genera la respuesta (proveedor principal) | |
| * Evaluador β puntΓΊa si es buena (proveedor DIFERENTE β evita confirmation bias) | |
| * Reflector β genera crΓtica verbal especΓfica (local-ai o proveedor diferente) | |
| * | |
| * LΓMITES DE SEGURIDAD: | |
| * - MAX_ROUNDS = 3 (nunca mΓ‘s β se convierte en loop destructivo) | |
| * - El Evaluador no tiene acceso a herramientas (solo opina) | |
| * - Si falla 3 rondas β escalar al owner, no seguir intentando | |
| */ | |
| import { callAI, callAIBackground } from './ai.js'; | |
| import { isLocalAIReady, ollamaChatDirect } from './local-ai.js'; | |
| import * as db from './db.js'; | |
| const MAX_ROUNDS = 3; // Cap duro β investigaciΓ³n recomienda 1-3 | |
| const REFLEXION_PROVIDERS = { | |
| actor : ['pollinations', 'groq', 'mistral', 'cerebrasLarge'], | |
| critic : ['mistral', 'groqFast', 'cerebras', 'pollinations'], // diferente al actor | |
| }; | |
| // ββ Memoria episΓ³dica: guarda pares (error, soluciΓ³n) ββββββββββββββββββββββββββ | |
| const episodicMemory = []; // [{ problem, critique, solution, ts, success }] | |
| const MAX_EPISODIC = 50; | |
| function storeEpisode(problem, critique, solution, success) { | |
| episodicMemory.push({ problem: problem.slice(0, 300), critique, solution: solution.slice(0, 500), success, ts: Date.now() }); | |
| if (episodicMemory.length > MAX_EPISODIC) episodicMemory.shift(); | |
| } | |
| function retrieveRelevantEpisode(problem) { | |
| // BΓΊsqueda textual simple β buscar problema parecido en los ΓΊltimos 20 | |
| const recent = episodicMemory.slice(-20).filter(e => e.success); | |
| const words = problem.toLowerCase().split(/\s+/).slice(0, 5); | |
| return recent.find(e => words.some(w => e.problem.toLowerCase().includes(w))); | |
| } | |
| // ββ Evaluador externo (crΓtico) ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CLAVE: usa proveedor DIFERENTE al actor para evitar confirmation bias | |
| async function evaluate(response, originalQuestion, context = '') { | |
| const evalPrompt = [ | |
| { | |
| role : 'system', | |
| content: `Eres un evaluador crΓtico de respuestas de un bot de Discord (TomateSMP, servidor Minecraft). | |
| EvalΓΊa esta respuesta de forma ESTRICTA y OBJETIVA. | |
| Criterios: relevancia, precisiΓ³n, completitud, tono apropiado para Discord, ausencia de alucinaciones. | |
| Responde SOLO JSON: | |
| { | |
| "score": 0-10, | |
| "pass": true/false, | |
| "specific_errors": ["error 1 concreto", "error 2"], | |
| "what_to_fix": "instrucciΓ³n especΓfica de cΓ³mo mejorar (mΓ‘x 100 palabras)" | |
| } | |
| SΓ© duro: score < 7 = no pasa. Un "no sΓ©" honesto pasa mejor que una respuesta inventada.`, | |
| }, | |
| { | |
| role : 'user', | |
| content: `Pregunta original: "${originalQuestion}"\n${context ? `Contexto: ${context}\n` : ''}Respuesta a evaluar: "${response}"`, | |
| }, | |
| ]; | |
| // Intentar con local-ai primero (gratis, diferente del actor) | |
| if (isLocalAIReady()) { | |
| try { | |
| const raw = await ollamaChatDirect(evalPrompt, 200, 8000); | |
| return JSON.parse(raw.replace(/```json|```/g, '').trim()); | |
| } catch {} | |
| } | |
| // Fallback: usar proveedor API diferente | |
| try { | |
| const raw = await callAI(evalPrompt, 'fast', 200, originalQuestion); | |
| return JSON.parse(raw.replace(/```json|```/g, '').trim()); | |
| } catch { | |
| return { score: 7, pass: true }; // si falla el evaluador, no bloquear | |
| } | |
| } | |
| // ββ Generador de crΓtica verbal especΓfica (Reflector) βββββββββββββββββββββββββ | |
| async function reflect(response, evalResult, originalQuestion, previousAttempts) { | |
| const attemptsText = previousAttempts.map((a, i) => | |
| `Intento ${i+1}: "${a.response.slice(0, 200)}" β Errores: ${a.errors.join(', ')}` | |
| ).join('\n'); | |
| const reflectPrompt = [ | |
| { | |
| role : 'system', | |
| content: `Eres un reflexion coach. Analiza por quΓ© esta respuesta fallΓ³ y genera instrucciones CONCRETAS para el siguiente intento. | |
| NO generes la respuesta correcta β solo las instrucciones para mejorarla. | |
| Basado en investigaciΓ³n de Reflexion (Shinn et al.): la crΓtica debe ser especΓfica, accionable y evitar los mismos errores.`, | |
| }, | |
| { | |
| role : 'user', | |
| content: `Pregunta: "${originalQuestion}" | |
| Respuesta fallida: "${response}" | |
| Errores especΓficos: ${evalResult.specific_errors?.join(', ') ?? 'calidad baja'} | |
| QuΓ© mejorar: ${evalResult.what_to_fix ?? 'desconocido'} | |
| ${attemptsText ? `\nIntentos previos:\n${attemptsText}` : ''} | |
| Genera instrucciones especΓficas para el siguiente intento (mΓ‘x 150 palabras):`, | |
| }, | |
| ]; | |
| try { | |
| if (isLocalAIReady()) { | |
| return await ollamaChatDirect(reflectPrompt, 200, 8000); | |
| } | |
| return await callAIBackground(reflectPrompt, 'reasoning', 200); | |
| } catch { | |
| return evalResult.what_to_fix ?? 'Mejora la respuesta siendo mΓ‘s preciso y relevante.'; | |
| } | |
| } | |
| // ββ REFLEXION LOOP principal βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export async function reflexionGenerate(messages, taskType, userMessage, systemPrompt) { | |
| const attempts = []; | |
| let bestResponse = null; | |
| let bestScore = 0; | |
| // Verificar memoria episΓ³dica β ΒΏhay un caso similar resuelto antes? | |
| const pastEpisode = retrieveRelevantEpisode(userMessage); | |
| let reflexionHint = pastEpisode | |
| ? `\n\n[Memoria episΓ³dica]: En situaciΓ³n similar, la soluciΓ³n exitosa fue: "${pastEpisode.solution.slice(0, 200)}"` | |
| : ''; | |
| for (let round = 0; round < MAX_ROUNDS; round++) { | |
| // ββ ACTOR: generar respuesta ββββββββββββββββββββββββββββββββββββββββββββββ | |
| const enrichedMessages = reflexionHint | |
| ? [messages[0] | |
| ? { ...messages[0], content: (messages[0].content ?? '') + reflexionHint } | |
| : { role: 'system', content: reflexionHint }, | |
| ...messages.slice(1)] | |
| : messages; | |
| let response; | |
| try { | |
| response = await callAI(enrichedMessages, taskType, null, userMessage); | |
| } catch (e) { | |
| console.warn(`[Reflexion] Actor fallΓ³ en ronda ${round+1}:`, e.message); | |
| break; | |
| } | |
| // ββ EVALUADOR: puntuar con modelo diferente βββββββββββββββββββββββββββββββ | |
| const evalResult = await evaluate(response, userMessage); | |
| console.log(`[Reflexion] Ronda ${round+1}: score=${evalResult.score}/10 pass=${evalResult.pass}`); | |
| attempts.push({ response, score: evalResult.score, errors: evalResult.specific_errors ?? [] }); | |
| if (evalResult.score > bestScore) { | |
| bestScore = evalResult.score; | |
| bestResponse = response; | |
| } | |
| // Si pasa β devolver directamente | |
| if (evalResult.pass) { | |
| storeEpisode(userMessage, 'pasΓ³ evaluaciΓ³n', response, true); | |
| console.log(`[Reflexion] β PasΓ³ en ronda ${round+1}`); | |
| return { response, rounds: round + 1, passed: true }; | |
| } | |
| // Si es la ΓΊltima ronda β devolver la mejor disponible | |
| if (round === MAX_ROUNDS - 1) { | |
| console.warn(`[Reflexion] β οΈ Agotadas ${MAX_ROUNDS} rondas β devolviendo mejor respuesta (score=${bestScore})`); | |
| storeEpisode(userMessage, evalResult.what_to_fix ?? 'sin correcciΓ³n', bestResponse ?? response, false); | |
| return { response: bestResponse ?? response, rounds: MAX_ROUNDS, passed: false, escalate: true }; | |
| } | |
| // ββ REFLECTOR: generar crΓtica verbal para el siguiente intento βββββββββββ | |
| const critique = await reflect(response, evalResult, userMessage, attempts.slice(-2)); | |
| reflexionHint = `\n\n[Auto-correcciΓ³n ronda ${round+2}]: ${critique}`; | |
| console.log(`[Reflexion] π Ronda ${round+2} con critique: ${critique.slice(0, 80)}...`); | |
| } | |
| return { response: bestResponse ?? '', rounds: MAX_ROUNDS, passed: false }; | |
| } | |
| // ββ Usar Reflexion para mejorar respuestas de alta importancia βββββββββββββββββ | |
| // Solo activar cuando la tarea lo justifica (no para saludos simples) | |
| export function shouldUseReflexion(taskType, messageLength) { | |
| if (taskType === 'fast') return false; // saludos, respuestas rΓ‘pidas | |
| if (taskType === 'reasoning') return true; // preguntas complejas β siempre | |
| if (taskType === 'code') return true; // cΓ³digo β siempre | |
| if (messageLength > 200) return true; // pregunta larga = compleja | |
| return false; | |
| } | |
| export function getEpisodicStats() { | |
| return { stored: episodicMemory.length, successes: episodicMemory.filter(e => e.success).length }; | |
| } | |