/** * 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 }; }