/** * learning.js β€” Sistema de Aprendizaje Continuo de Zelin * ======================================================== * No necesita GPU ni modelos externos. * Aprende de: * 1. Reacciones a sus mensajes (πŸ‘ / emojis positivos vs ❌) * 2. Si alguien responde a su mensaje (engagement) * 3. Si alguien ignora su mensaje o dice "cΓ‘llate" * 4. QuΓ© respuestas del owner reciben "πŸ‘" / "bien" / "perfecto" * * Genera un "style profile" que se inyecta al prompt para que * Zelin mejore su estilo con el tiempo de forma autΓ³noma. */ import * as db from './db.js'; // ── Registro de mensajes de Zelin para tracking ─────────────────────────────── // messageId β†’ { channelId, type, content, timestamp, context } const zelinMsgRegistry = new Map(); // en memoria, se limpia sola const POSITIVE_EMOJIS = new Set(['πŸ‘','βœ…','❀️','πŸ”₯','πŸ˜‚','🀣','πŸ’―','πŸ‘Œ','🫑','πŸ₯²','❀','xd','πŸ‘']); const NEGATIVE_EMOJIS = new Set(['πŸ‘Ž','❌','🀑','πŸ’€','πŸ™„','πŸ˜’','🫀']); // ── Registrar mensaje de Zelin ───────────────────────────────────────────────── export function trackZelinMessage(message, type = 'response', context = '') { if (!message?.id) return; zelinMsgRegistry.set(message.id, { channelId : message.channelId, type, // 'response' | 'autonomous' | 'revival' | 'spontaneous' content : message.content?.substring(0, 200), timestamp : Date.now(), context : context.substring(0, 100), reactions : { positive: 0, negative: 0 }, replies : 0, ignored : false, }); // Limpiar registry de mensajes mΓ‘s de 6h if (zelinMsgRegistry.size > 200) { const cutoff = Date.now() - 6 * 60 * 60 * 1000; for (const [id, data] of zelinMsgRegistry) { if (data.timestamp < cutoff) zelinMsgRegistry.delete(id); } } } // ── Procesar reacciΓ³n a mensaje de Zelin ────────────────────────────────────── export async function processReaction(messageId, emoji, isAdd) { const msg = zelinMsgRegistry.get(messageId); if (!msg) return; // no era de Zelin const emojiName = emoji?.name ?? emoji ?? ''; const isPositive = POSITIVE_EMOJIS.has(emojiName) || [...POSITIVE_EMOJIS].some(e => emojiName.includes(e)); const isNegative = NEGATIVE_EMOJIS.has(emojiName) || [...NEGATIVE_EMOJIS].some(e => emojiName.includes(e)); if (!isPositive && !isNegative) return; const delta = isAdd ? 1 : -1; if (isPositive) msg.reactions.positive = Math.max(0, msg.reactions.positive + delta); if (isNegative) msg.reactions.negative = Math.max(0, msg.reactions.negative + delta); // Guardar feedback en DB para aprendizaje persistente const score = msg.reactions.positive - msg.reactions.negative * 2; await saveFeedback(messageId, msg, score); } // ── Registrar reply a mensaje de Zelin (engagement positivo) ───────────────── export function processReply(referencedMessageId) { const msg = zelinMsgRegistry.get(referencedMessageId); if (!msg) return; msg.replies++; } // ── Marcar mensaje ignorado (canal muerto tras hablar Zelin) ────────────────── export async function markIgnored(messageId) { const msg = zelinMsgRegistry.get(messageId); if (!msg) return; msg.ignored = true; await saveFeedback(messageId, msg, -1); } // ── Guardar feedback en Turso ───────────────────────────────────────────────── async function saveFeedback(messageId, msg, score) { const key = `learning.feedback.${messageId}`; await db.memSet(key, { type : msg.type, content : msg.content, context : msg.context, score, reactions : msg.reactions, replies : msg.replies, ignored : msg.ignored, timestamp : new Date().toISOString(), }, 'learning').catch(() => {}); } // ── Generar style profile desde el historial de feedback ───────────────────── export async function getStyleProfile() { try { const r = await db.db.execute({ sql : `SELECT value FROM zelin_memory WHERE category = 'learning' AND key LIKE 'learning.feedback.%' ORDER BY key DESC LIMIT 200`, args: [], }); if (!r.rows.length) return null; const items = r.rows.map(row => { try { return typeof row.value === 'string' ? JSON.parse(row.value) : row.value; } catch { return null; } }).filter(Boolean); // Separar por tipo const byType = {}; for (const item of items) { if (!byType[item.type]) byType[item.type] = { positive: [], negative: [] }; if (item.score > 0) byType[item.type].positive.push(item); if (item.score < 0) byType[item.type].negative.push(item); } // Calcular mΓ©tricas const avgScore = items.reduce((s, i) => s + (i.score ?? 0), 0) / items.length; const engagementRate = items.filter(i => i.replies > 0).length / items.length; const ignoreRate = items.filter(i => i.ignored).length / items.length; // Encontrar patrones en mensajes positivos vs negativos const goodExamples = items.filter(i => i.score >= 2).slice(0, 5).map(i => i.content).filter(Boolean); const badExamples = items.filter(i => i.score <= -2).slice(0, 3).map(i => i.content).filter(Boolean); return { avgScore : Math.round(avgScore * 100) / 100, engagementRate : Math.round(engagementRate * 100), ignoreRate : Math.round(ignoreRate * 100), totalFeedback : items.length, goodExamples, badExamples, spontaneousOK : (byType.spontaneous?.positive?.length ?? 0) > (byType.spontaneous?.negative?.length ?? 0), revivalOK : (byType.revival?.positive?.length ?? 0) > (byType.revival?.negative?.length ?? 0), }; } catch { return null; } } // ── Generar bloque de instrucciones para el prompt ──────────────────────────── export async function getLearningPromptBlock() { const profile = await getStyleProfile(); if (!profile || profile.totalFeedback < 10) return ''; // Necesita datos suficientes let block = '\n\n## LO QUE HAS APRENDIDO DE TUS CONVERSACIONES\n'; if (profile.ignoreRate > 40) { block += '- Tus mensajes espontΓ‘neos ΓΊltimamente no generan mucha respuesta. Habla menos y solo cuando tengas algo bueno.\n'; } if (profile.engagementRate > 50) { block += '- La gente responde bastante a tus mensajes, estΓ‘s en buen camino.\n'; } if (profile.avgScore > 1) { block += '- El servidor reacciona bien a tu estilo actual. Mantenlo.\n'; } if (profile.avgScore < -0.5) { block += '- Últimamente tus mensajes no estΓ‘n siendo bien recibidos. SΓ© mΓ‘s conciso y directo.\n'; } if (profile.goodExamples.length) { block += `- Ejemplos de mensajes que funcionaron bien: ${profile.goodExamples.map(e => `"${e.substring(0, 60)}"`).join(' | ')}\n`; } if (profile.badExamples.length) { block += `- Evitar este tipo de mensajes (no funcionaron): ${profile.badExamples.map(e => `"${e.substring(0, 40)}"`).join(' | ')}\n`; } if (!profile.spontaneousOK) { block += '- Reduce los mensajes espontΓ‘neos β€” los recientes no han tenido buena recepciΓ³n.\n'; } return block; } // ── AnΓ‘lisis periΓ³dico: detectar patrones cada 6h ──────────────────────────── export async function runPeriodicAnalysis() { const profile = await getStyleProfile(); if (!profile) return; // Actualizar umbral de espontaneidad segΓΊn el ignoreRate // Si se ignoran mucho β†’ subir cooldown dinΓ‘micamente const dynamicCooldownKey = 'brain.spontaneous_cooldown_ms'; let cooldownMs = 15 * 60 * 1000; // 15min base if (profile.ignoreRate > 60) cooldownMs = 40 * 60 * 1000; else if (profile.ignoreRate > 40) cooldownMs = 25 * 60 * 1000; else if (profile.ignoreRate < 20 && profile.engagementRate > 50) cooldownMs = 10 * 60 * 1000; await db.memSet(dynamicCooldownKey, cooldownMs, 'brain').catch(() => {}); console.log(`[Learning] Cooldown espontΓ‘neo ajustado a ${Math.round(cooldownMs/60000)}min (ignore:${profile.ignoreRate}% engage:${profile.engagementRate}%)`); }