Spaces:
Paused
Paused
File size: 8,972 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 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | /**
* 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 };
}
|