/** * ============================================ * πŸ” analysis.js β€” AnΓ‘lisis de Mensajes * ============================================ * Context Weaver: detecta preguntas implΓ­citas * Proactive Helper: decide si Zelin debe intervenir * Conflict Detection: detecta tensiΓ³n antes de escalar */ import { callAI } from './ai.js'; import { buildProactivePrompt, buildConflictPrompt } from './prompt.js'; import { sanitizeOutput } from './security.js'; import { detectConflictSignals, detectSentiment } from './moderation.js'; import * as memory from './memory.js'; import { readConfig } from './utils.js'; const config = readConfig(); // Cooldowns por canal para evitar intervenciones masivas const proactiveCooldown = new Map(); // channelId β†’ lastProactiveAt const conflictCooldown = new Map(); // channelId β†’ lastConflictAt const unansweredQuestions = new Map(); // channelId β†’ { question, timestamp, userId } // Limpiar preguntas sin responder despuΓ©s de 15 minutos setInterval(() => { const cutoff = Date.now() - 15 * 60 * 1000; for (const [ch, q] of unansweredQuestions.entries()) { if (q.timestamp < cutoff) unansweredQuestions.delete(ch); } }, 5 * 60 * 1000); // ── Context Weaver ──────────────────────────────────────────────────────────── /** * Detecta si un mensaje es una pregunta implΓ­cita sin respuesta dirigida a nadie. * Ej: "alguien sabe cΓ³mo entrar desde bedrock?" sin mencionar a nadie */ export function detectImplicitQuestion(content) { const lower = content.toLowerCase(); const questionPatterns = [ /\balguien\s+(sabe|puede|tiene|conoce)/i, /\b(cΓ³mo|como)\s+(entro|conecto|hago|consigo|llego)/i, /\b(dΓ³nde|donde)\s+(estΓ‘|queda|hay)/i, /\b(cuΓ‘ndo|cuando)\s+(es|serΓ‘|hay|abre)/i, /\b(cuΓ‘l|cual)\s+(es|son|fue)/i, /\?$/, /\bno\s+(sΓ©|se)\s+(cΓ³mo|como|quΓ©|que)/i, /\bme\s+(pueden|podΓ©is|podeis)\s+(ayudar|decir|explicar)/i, /\btengo\s+(una\s+)?(pregunta|duda)/i, ]; for (const p of questionPatterns) { if (p.test(lower)) return true; } return false; } /** * Registra una pregunta sin responder para monitoreo proactivo. */ export function registerUnansweredQuestion(channelId, content, userId) { unansweredQuestions.set(channelId, { question : content, userId, timestamp: Date.now(), }); } /** * Verifica si hay una pregunta sin respuesta que lleva mΓ‘s de N segundos. */ export function checkUnansweredQuestion(channelId, thresholdMs = 90000) { const q = unansweredQuestions.get(channelId); if (!q) return null; if (Date.now() - q.timestamp > thresholdMs) return q; return null; } export function markQuestionAnswered(channelId) { unansweredQuestions.delete(channelId); } // ── Proactive Helper ────────────────────────────────────────────────────────── /** * Decide si Zelin debe responder proactivamente. * Llama a la IA solo si supera el umbral de confianza inicial. * * @returns {string|null} Respuesta de Zelin o null si no debe intervenir */ export async function evaluateProactiveResponse(channelId, question, userId) { // Cooldown de 3 minutos por canal const lastProactive = proactiveCooldown.get(channelId) ?? 0; if (Date.now() - lastProactive < 180000) return null; const channelCtx = await memory.getChannelContext(channelId); try { const raw = await callAI([ { role: 'system', content: 'Eres Zelin. Responde SOLO JSON vΓ‘lido.' }, { role: 'user', content: buildProactivePrompt(channelCtx, question) }, ], 'fast', 200); const clean = raw.replace(/```json|```/g, '').trim(); const result = JSON.parse(clean); if (!result.shouldRespond || result.confidence < (config.behavior?.proactiveThreshold ?? 0.65)) { return null; } proactiveCooldown.set(channelId, Date.now()); markQuestionAnswered(channelId); return sanitizeOutput(result.response ?? ''); } catch { return null; } } // ── Conflict Mediator ───────────────────────────────────────────────────────── // Seguimiento de tensiΓ³n por canal const channelTension = new Map(); // channelId β†’ { score, messages: [] } /** * Actualiza el nivel de tensiΓ³n de un canal con un nuevo mensaje. * @returns {boolean} true si la tensiΓ³n supera el umbral y hay que mediar */ export function updateChannelTension(channelId, content, username) { const score = detectConflictSignals(content); if (!channelTension.has(channelId)) { channelTension.set(channelId, { score: 0, messages: [], lastUpdate: Date.now() }); } const tension = channelTension.get(channelId); // Aplicar decay basado en tiempo transcurrido desde ΓΊltimo mensaje conflictivo const msSinceLast = Date.now() - (tension.lastUpdate ?? Date.now()); const decayAmount = Math.floor(msSinceLast / 30000) * 0.1; // 0.1 cada 30s tension.score = Math.max(0, tension.score - decayAmount); tension.score = Math.min(tension.score + score, 1.0); tension.lastUpdate = Date.now(); tension.messages.push(`[${username}]: ${content}`); if (tension.messages.length > 10) tension.messages.shift(); return tension.score >= (config.behavior?.conflictThreshold ?? 0.6); } /** * Genera una respuesta mediadora para un canal en conflicto. * @returns {string|null} */ export async function mediateConflict(channelId) { const lastMediation = conflictCooldown.get(channelId) ?? 0; if (Date.now() - lastMediation < 300000) return null; // 5 min cooldown const tension = channelTension.get(channelId); if (!tension) return null; const participants = [...new Set( tension.messages.map(m => m.match(/^\[([^\]]+)\]/)?.[1]).filter(Boolean) )]; try { const response = await callAI([ { role: 'system', content: 'Eres Zelin mediando un conflicto. SΓ© neutral, breve (1-2 lΓ­neas) y en el estilo de TomateSMP.' }, { role: 'user', content: buildConflictPrompt(tension.messages.join('\n'), participants) }, ], 'reasoning', 150); conflictCooldown.set(channelId, Date.now()); // Reducir tensiΓ³n tras mediar tension.score = Math.max(0, tension.score - 0.4); return sanitizeOutput(response); } catch { return null; } } export function resetChannelTension(channelId) { channelTension.delete(channelId); } // ── Knowledge Graph Builder ─────────────────────────────────────────────────── // Grafo simple en memoria (se persiste en Turso via zelin_memory) let knowledgeGraph = {}; let _kgUpdateCount = 0; // contador separado, no se serializa export async function loadKnowledgeGraph() { const { memGet } = await import('./db.js'); knowledgeGraph = await memGet('knowledge.graph') ?? {}; } export async function updateKnowledgeGraph(entity, type, connections = []) { if (!knowledgeGraph[entity]) { knowledgeGraph[entity] = { type, connections: [], mentions: 0 }; } knowledgeGraph[entity].mentions++; for (const conn of connections) { if (!knowledgeGraph[entity].connections.includes(conn)) { knowledgeGraph[entity].connections.push(conn); } } // Persistir cada 50 actualizaciones β€” sin _updateCount en el objeto serializado _kgUpdateCount = (_kgUpdateCount + 1); if (_kgUpdateCount % 50 === 0) { const { memSet } = await import('./db.js'); const toSave = { ...knowledgeGraph }; delete toSave._updateCount; // por si viene de versiΓ³n vieja await memSet('knowledge.graph', toSave, 'knowledge'); } } export function queryKnowledgeGraph(query) { const lower = query.toLowerCase(); const results = []; for (const [entity, data] of Object.entries(knowledgeGraph)) { if (entity.startsWith('_') || !data || typeof data !== 'object') continue; if (entity.toLowerCase().includes(lower)) { results.push({ entity, ...data }); } } return results.sort((a, b) => b.mentions - a.mentions).slice(0, 5); }