Spaces:
Paused
Paused
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 };
|