Spaces:
Paused
Paused
| /** | |
| * 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}%)`); | |
| } | |