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