File size: 7,057 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
/**
 * ============================================================
 * react.js — ReAct Loop + Tool Registry
 * ============================================================
 * Patrón Reason → Act → Observe para autonomía real.
 *
 * Reglas de seguridad duras:
 * - NUNCA escribe directamente a /src/
 * - NUNCA hace git operations
 * - NUNCA ejecuta comandos shell arbitrarios
 * - Acciones destructivas SIEMPRE requieren aprobación
 *
 * Activar con reactLoop: true en config.features
 */

import path         from 'path';
import fs           from 'fs/promises';
import { fileURLToPath } from 'url';
import * as db      from './db.js';
import { callAI }   from './ai.js';
import { readConfig } from './utils.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname  = path.dirname(__filename);
const BASE_DIR   = path.dirname(__dirname);
const config     = readConfig();

let _client = null, _guild = null;
export function initReact(client, guild) { _client = client; _guild = guild; }

// ── Tool Registry ─────────────────────────────────────────────────────────────
const TOOLS = {
  get_server_stats: {
    description: 'Obtiene estadísticas del servidor Discord/Minecraft',
    params: {},
    async execute() {
      const stats = await db.getServerStats();
      return JSON.stringify(stats);
    },
  },

  lookup_player: {
    description: 'Busca información de un jugador por nombre o ID',
    params: { identifier: 'string — nombre, nick o Discord ID' },
    async execute({ identifier }) {
      const rows = await db.db.execute({
        sql : 'SELECT * FROM users WHERE username=? OR nickname=? OR user_id=? LIMIT 1',
        args: [identifier, identifier, identifier],
      });
      if (!rows.rows.length) return 'Jugador no encontrado: ' + identifier;
      const u = rows.rows[0];
      return JSON.stringify({ username: u.username, nickname: u.nickname, discordId: u.user_id, messages: u.message_count, active: u.is_active });
    },
  },

  send_discord_message: {
    description: 'Envía un mensaje a un canal de Discord por nombre o ID',
    params: { channel: 'string — nombre o ID del canal', content: 'string — mensaje a enviar' },
    async execute({ channel, content }) {
      if (!_guild) throw new Error('Sin guild disponible');
      const ch = _guild.channels.cache.find(c => c.id === channel || c.name === channel);
      if (!ch) throw new Error('Canal no encontrado: ' + channel);
      await ch.send(content.slice(0, 2000));
      return 'Mensaje enviado a #' + ch.name;
    },
  },

  read_own_code: {
    description: 'Lee un archivo de código de Zelin (solo lectura)',
    params: { file: 'string — nombre del archivo en src/ (ej: ai.js)' },
    readOnly: true,
    async execute({ file }) {
      const safe = path.resolve(path.join(BASE_DIR, 'src'), path.basename(file));
      if (!safe.startsWith(path.join(BASE_DIR, 'src'))) throw new Error('Acceso denegado');
      const content = await fs.readFile(safe, 'utf8');
      return content.slice(0, 3000) + (content.length > 3000 ? '\n[truncado...]' : '');
    },
  },

  suggest_code_improvement: {
    description: 'Guarda una sugerencia de mejora de código para revisión del owner. NUNCA edita código directamente.',
    params: { file: 'string', improvement: 'string', reason: 'string' },
    async execute({ file, improvement, reason }) {
      const sugDir = path.join(BASE_DIR, 'suggestions');
      await fs.mkdir(sugDir, { recursive: true });
      const fname   = Date.now() + '-' + path.basename(file) + '.md';
      const fpath   = path.join(sugDir, fname);
      const content = '# Sugerencia para ' + file + '\n\n**Razón:** ' + reason + '\n\n```js\n' + improvement + '\n```\n\n*Generado por Zelin el ' + new Date().toISOString() + '*';
      await fs.writeFile(fpath, content, 'utf8');
      await db.memSet('selfimprove.suggestion.' + Date.now(), { file, reason, improvement: improvement.slice(0, 500), createdAt: new Date().toISOString() }, 'selfimprove');
      return 'Sugerencia guardada en suggestions/' + fname;
    },
  },

  get_mod_history: {
    description: 'Obtiene el historial de moderación de un usuario',
    params: { userId: 'string — Discord ID del usuario' },
    async execute({ userId }) {
      const hist = await db.getUserModHistory(userId, 5).catch(() => []);
      if (!hist.length) return 'Sin historial de moderación para ' + userId;
      return hist.map(h => '[' + h.action + '] ' + h.reason + ' (' + h.created_at + ')').join('\n');
    },
  },

  ban_player: {
    description: 'Banea un jugador del servidor. REQUIERE aprobación humana.',
    params: { userId: 'string', reason: 'string', duration: 'string' },
    requiresApproval: true,
    async execute() { throw new Error('Requiere aprobación humana'); },
  },
};

// ── ReAct Loop ────────────────────────────────────────────────────────────────
export async function reactLoop(goal, maxSteps = 5, systemPrompt = '') {
  if (!config.features?.reactLoop) return null;

  const toolDefs = Object.entries(TOOLS).map(([name, t]) => ({ name, description: t.description, params: t.params }));
  const history  = [];
  const sys = (systemPrompt || 'Eres Zelin, asistente de TomateSMP.') +
    '\n\nHerramientas disponibles:\n' + JSON.stringify(toolDefs, null, 2) +
    '\n\nPara usar una herramienta responde SOLO con JSON:\n{"thought":"...","tool":"nombre","params":{...}}' +
    '\n\nPara respuesta final:\n{"thought":"...","final_answer":"..."}';

  for (let step = 0; step < maxSteps; step++) {
    let raw;
    try {
      raw = await callAI([{ role: 'system', content: sys }, { role: 'user', content: goal }, ...history], 'reasoning', null, goal);
    } catch (e) { return 'Error en ReAct loop: ' + e.message; }

    const clean = raw.replace(/```json|```/g, '').trim();
    let parsed;
    try { parsed = JSON.parse(clean); } catch { history.push({ role: 'assistant', content: raw }); continue; }

    history.push({ role: 'assistant', content: raw });
    if (parsed.final_answer) return parsed.final_answer;

    const tool = TOOLS[parsed.tool];
    if (!tool) { history.push({ role: 'user', content: 'Observation: Herramienta no existe: ' + parsed.tool }); continue; }

    if (tool.requiresApproval) {
      return '⚠️ Esta acción requiere aprobación de tomatitoo__: **' + parsed.tool + '** con params: ' + JSON.stringify(parsed.params);
    }

    let observation;
    try {
      observation = await tool.execute(parsed.params ?? {});
    } catch (e) {
      observation = 'Error ejecutando ' + parsed.tool + ': ' + e.message;
    }

    history.push({ role: 'user', content: 'Observation: ' + String(observation).slice(0, 1000) });
  }

  return 'Se alcanzó el límite de pasos (' + maxSteps + ') sin respuesta final.';
}

export { TOOLS };