Spaces:
Paused
Paused
File size: 8,515 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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | /**
* 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}%)`);
}
|