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