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