zelin-bot / src /learning.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* 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}%)`);
}