Spaces:
Paused
Paused
File size: 8,284 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 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | /**
* ============================================
* π 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);
}
|