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}%)`);
}