zelin-bot / src /reflexion.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* 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 };
}