1/** 2 * Conversation Context Manager 3 * Handles session tracking, entity memory, and contextual continuity 4 */ 5 6class ConversationContext { 7 constructor() { 8 this.sessionId = this.generateSessionId(); 9 this.conversationHistory = []; 10 this.entityMemory = new Map(); // Track mentioned entities 11 this.topicMemory = new Set(); // Track conversation topics 12 this.maxHistoryLength = 20; // Keep last 20 messages 13 this.maxContextLength = 8; // Include last 8 relevant messages in context 14 15 // Load existing session if available 16 this.loadSession(); 17 18 console.log(`[Context] Initialized session: ${this.sessionId}`); 19 } 20 21 /** 22 * Generate unique session ID 23 */ 24 generateSessionId() { 25 return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; 26 } 27 28 /** 29 * Add message to conversation history with entity extraction 30 */ 31 addMessage(role, content, metadata = {}) { 32 const message = { 33 role, 34 content, 35 timestamp: Date.now(), 36 entities: this.extractEntities(content), 37 topics: this.extractTopics(content), 38 metadata 39 }; 40 41 this.conversationHistory.push(message); 42 43 // Update entity and topic memory 44 this.updateEntityMemory(message.entities, message); 45 this.updateTopicMemory(message.topics); 46 47 // Trim history if too long 48 if (this.conversationHistory.length > this.maxHistoryLength) { 49 this.conversationHistory = this.conversationHistory.slice(-this.maxHistoryLength); 50 } 51 52 // Save to storage 53 this.saveSession(); 54 55 console.log(`[Context] Added ${role} message with ${message.entities.length} entities`); 56 return message; 57 } 58 59 /** 60 * Extract entities from text (people, places, organizations) 61 */ 62 extractEntities(text) { 63 // Handle undefined/null text 64 if (!text || typeof text !== 'string') { 65 return []; 66 } 67 68 const entities = []; 69 70 // Common patterns for entity extraction 71 const patterns = { 72 // People: Capitalized names (2+ words) 73 people: /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b/g, 74 // Places: Common location indicators 75 places: /\b(?:in|at|from|to)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\b/g, 76 // Organizations/Movies: Quoted or capitalized multi-word entities 77 organizations: /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3})\b/g 78 }; 79 80 // Extract people names 81 let match; 82 while ((match = patterns.people.exec(text)) !== null) { 83 const entity = match[1]; 84 if (this.isLikelyPersonName(entity)) { 85 entities.push({ 86 text: entity, 87 type: 'person', 88 confidence: 0.8 89 }); 90 } 91 } 92 93 // Extract places 94 patterns.places.lastIndex = 0; 95 while ((match = patterns.places.exec(text)) !== null) { 96 entities.push({ 97 text: match[1], 98 type: 'place', 99 confidence: 0.6 100 }); 101 } 102 103 // Remove duplicates 104 const uniqueEntities = entities.filter((entity, index, self) => 105 index === self.findIndex(e => e.text.toLowerCase() === entity.text.toLowerCase()) 106 ); 107 108 return uniqueEntities; 109 } 110 111 /** 112 * Check if text is likely a person name 113 */ 114 isLikelyPersonName(text) { 115 const commonWords = ['The', 'And', 'Or', 'But', 'For', 'With', 'About', 'Mission', 'Top', 'Gun']; 116 const words = text.split(' '); 117 118 // Skip if contains common non-name words 119 if (words.some(word => commonWords.includes(word))) { 120 return false; 121 } 122 123 // Must be 2-4 words 124 return words.length >= 2 && words.length <= 4; 125 } 126 127 /** 128 * Extract topics from text 129 */ 130 extractTopics(text) { 131 // Handle undefined/null text 132 if (!text || typeof text !== 'string') { 133 return []; 134 } 135 136 const topics = []; 137 const topicKeywords = { 138 'movies': ['film', 'movie', 'cinema', 'actor', 'actress', 'director', 'role'], 139 'marriage': ['marriage', 'married', 'wife', 'husband', 'divorce', 'relationship'], 140 'career': ['career', 'work', 'job', 'profession', 'achievement', 'award'], 141 'family': ['family', 'children', 'kids', 'parents', 'father', 'mother'], 142 'biography': ['born', 'birth', 'life', 'biography', 'background', 'early'] 143 }; 144 145 const lowerText = text.toLowerCase(); 146 147 for (const [topic, keywords] of Object.entries(topicKeywords)) { 148 if (keywords.some(keyword => lowerText.includes(keyword))) { 149 topics.push(topic); 150 } 151 } 152 153 return topics; 154 } 155 156 /** 157 * Update entity memory with new entities 158 */ 159 updateEntityMemory(entities, message) {
160 entities.forEach(entity => { 161 const key = entity.text.toLowerCase(); 162 163 if (!this.entityMemory.has(key)) { 164 this.entityMemory.set(key, { 165 text: entity.text, 166 type: entity.type, 167 firstMention: message.timestamp, 168 lastMention: message.timestamp, 169 mentionCount: 1, 170 contexts: [message.content.substring(0, 100)] 171 }); 172 } else { 173 const existing = this.entityMemory.get(key); 174 existing.lastMention = message.timestamp; 175 existing.mentionCount++; 176 existing.contexts.push(message.content.substring(0, 100)); 177 178 // Keep only last 3 contexts 179 if (existing.contexts.length > 3) { 180 existing.contexts = existing.contexts.slice(-3); 181 } 182 } 183 }); 184 } 185 186 /** 187 * Update topic memory 188 */ 189 updateTopicMemory(topics) { 190 topics.forEach(topic => this.topicMemory.add(topic)); 191 } 192 193 /** 194 * Get contextual information for a new query 195 */ 196 getContextForQuery(query) { 197 const queryEntities = this.extractEntities(query); 198 const queryTopics = this.extractTopics(query); 199 200 // Find relevant previous messages 201 const relevantMessages = this.findRelevantMessages(queryEntities, queryTopics); 202 203 // Build context summary 204 const context = { 205 sessionId: this.sessionId, 206 relevantHistory: relevantMessages, 207 mentionedEntities: this.getRelevantEntities(queryEntities), 208 activeTopics: Array.from(this.topicMemory), 209 conversationLength: this.conversationHistory.length 210 }; 211 212 console.log(`[Context] Generated context with ${relevantMessages.length} relevant messages`); 213 return context; 214 } 215 216 /** 217 * Find messages relevant to current query 218 */ 219 findRelevantMessages(queryEntities, queryTopics) { 220 const relevantMessages = []; 221 const entityTexts = queryEntities.map(e => e.text.toLowerCase()); 222 223 // Score messages by relevance 224 const scoredMessages = this.conversationHistory.map(message => { 225 let score = 0; 226 227 // Recent messages get higher scores 228 const age = Date.now() - message.timestamp; 229 const recencyScore = Math.max(0, 1 - (age / (30 * 60 * 1000))); // 30 minutes 230 score += recencyScore * 0.3; 231 232 // Entity overlap 233 const messageEntityTexts = message.entities.map(e => e.text.toLowerCase()); 234 const entityOverlap = entityTexts.filter(e => messageEntityTexts.includes(e)).length; 235 score += entityOverlap * 0.5; 236 237 // Topic overlap 238 const topicOverlap = queryTopics.filter(t => message.topics.includes(t)).length; 239 score += topicOverlap * 0.2; 240 241 return { message, score }; 242 }); 243 244 // Sort by score and take top messages 245 return scoredMessages 246 .sort((a, b) => b.score - a.score) 247 .slice(0, this.maxContextLength) 248 .map(item => item.message); 249 } 250 251 /** 252 * Get relevant entities for context 253 */ 254 getRelevantEntities(queryEntities) { 255 const relevant = []; 256 const queryTexts = queryEntities.map(e => e.text.toLowerCase()); 257 258 for (const [key, entity] of this.entityMemory.entries()) { 259 // Include if mentioned in query or recently mentioned 260 const recentlyMentioned = (Date.now() - entity.lastMention) < (10 * 60 * 1000); // 10 minutes 261 const mentionedInQuery = queryTexts.includes(key); 262 263 if (mentionedInQuery || recentlyMentioned) { 264 relevant.push(entity); 265 } 266 } 267 268 return relevant; 269 } 270 271 /** 272 * Resolve references in query (e.g., "his wife", "that movie") 273 */ 274 resolveReferences(query) { 275 let resolvedQuery = query; 276 277 // Only resolve references if we have recent conversation context (within last 2 messages) 278 if (this.conversationHistory.length < 2) { 279 console.log('[Context] No recent context for reference resolution'); 280 return resolvedQuery; 281 } 282 283 // Get the most recent user message to check relevance 284 const recentMessages = this.conversationHistory.slice(-3); 285 const lastUserMessage = recentMessages.reverse().find(msg => msg.role === 'user'); 286 287 if (!lastUserMessage || Date.now() - lastUserMessage.timestamp > 300000) { // 5 minutes 288 console.log('[Context] Last user message too old for reference resolution'); 289 return resolvedQuery; 290 } 291 292 // Simple reference resolution patterns with stricter validation 293 const references = { 294 'his wife': this.findRecentEntity('person', 'wife'), 295 'his wives': this.findRecentEntity('person', 'wife'), 296 'her husband': this.findRecentEntity('person', 'husband'), 297 'that movie': this.findRecentEntity('movie'), 298 'the film': this.findRecentEntity('movie'), 299 'that person': this.findRecentEntity('person') 300 }; 301 302 for (const [reference, entity] of Object.entries(references)) { 303 if (entity && resolvedQuery.toLowerCase().includes(reference)) { 304 // Additional validation: ensure the entity was mentioned recently 305 const timeSinceLastMention = Date.now() - entity.lastMention; 306 if (timeSinceLastMention > 300000) { // 5 minutes 307 console.log(`[Context] Entity "${entity.text}" too old for resolution (${timeSinceLastMention}ms ago)`); 308 continue; 309 } 310 311 // Check if the entity is contextually relevant to re
311cent conversation 312 const isRelevant = this.isEntityRelevantToRecentContext(entity, recentMessages); 313 if (!isRelevant) { 314 console.log(`[Context] Entity "${entity.text}" not relevant to recent context`); 315 continue; 316 } 317 318 resolvedQuery = resolvedQuery.replace( 319 new RegExp(reference, 'gi'), 320 entity.text 321 ); 322 console.log(`[Context] Resolved "${reference}" to "${entity.text}"`); 323 } 324 } 325 326 return resolvedQuery; 327 } 328 329 /** 330 * Find recent entity of specific type 331 */ 332 findRecentEntity(type, context = null) { 333 const candidates = Array.from(this.entityMemory.values()) 334 .filter(entity => { 335 // Basic type filter 336 if (entity.type !== type) return false; 337 338 // Time filter: only consider entities mentioned in last 5 minutes 339 const timeSinceLastMention = Date.now() - entity.lastMention; 340 if (timeSinceLastMention > 300000) return false; 341 342 // Frequency filter: entity should have been mentioned at least once recently 343 if (entity.mentionCount < 1) return false; 344 345 return true; 346 }) 347 .sort((a, b) => b.lastMention - a.lastMention); 348 349 if (context) { 350 // Look for entity mentioned in context with additional validation 351 const contextMatch = candidates.find(entity => { 352 const hasContextMatch = entity.contexts.some(ctx => 353 ctx.toLowerCase().includes(context.toLowerCase()) 354 ); 355 356 // Additional check: ensure the context is recent 357 const recentContexts = entity.contexts.slice(-2); // Last 2 contexts 358 const hasRecentContextMatch = recentContexts.some(ctx => 359 ctx.toLowerCase().includes(context.toLowerCase()) 360 ); 361 362 return hasContextMatch && hasRecentContextMatch; 363 }); 364 365 if (contextMatch) { 366 console.log(`[Context] Found contextual match for "${context}": ${contextMatch.text}`); 367 return contextMatch; 368 } 369 } 370 371 // Return most recent candidate, but only if it's very recent (within 2 minutes) 372 const mostRecent = candidates[0]; 373 if (mostRecent && (Date.now() - mostRecent.lastMention) < 120000) { 374 console.log(`[Context] Found recent entity: ${mostRecent.text}`); 375 return mostRecent; 376 } 377 378 console.log(`[Context] No suitable recent entity found for type "${type}" with context "${context}"`); 379 return null; 380 } 381 382 /** 383 * Check if entity is relevant to recent conversation context 384 */ 385 isEntityRelevantToRecentContext(entity, recentMessages) { 386 // Check if entity was mentioned in recent messages 387 const recentMentions = recentMessages.filter(msg => 388 msg.entities.some(e => e.text.toLowerCase() === entity.text.toLowerCase()) 389 ); 390 391 return recentMentions.length > 0; 392 } 393 394 /** 395 * Save session to localStorage 396 */ 397 saveSession() { 398 try { 399 const sessionData = { 400 sessionId: this.sessionId, 401 conversationHistory: this.conversationHistory, 402 entityMemory: Array.from(this.entityMemory.entries()), 403 topicMemory: Array.from(this.topicMemory), 404 timestamp: Date.now() 405 }; 406 407 localStorage.setItem(`wikiLive_session_${this.sessionId}`, JSON.stringify(sessionData)); 408 } catch (error) { 409 console.warn('[Context] Failed to save session:', error); 410 } 411 } 412 413 /** 414 * Load session from localStorage 415 */ 416 loadSession() { 417 try { 418 // Try to load the most recent session 419 const sessions = this.getAllSessions(); 420 if (sessions.length > 0) { 421 const latestSession = sessions[0]; 422 this.sessionId = latestSession.sessionId; 423 this.conversationHistory = latestSession.conversationHistory || []; 424 this.entityMemory = new Map(latestSession.entityMemory || []); 425 this.topicMemory = new Set(latestSession.topicMemory || []); 426
427 console.log(`[Context] Loaded session with ${this.conversationHistory.length} messages`); 428 } 429 } catch (error) { 430 console.warn('[Context] Failed to load session:', error); 431 } 432 } 433 434 /** 435 * Get all sessions sorted by timestamp 436 */ 437 getAllSessions() { 438 const sessions = []; 439 440 for (let i = 0; i < localStorage.length; i++) { 441 const key = localStorage.key(i); 442 if (key && key.startsWith('wikiLive_session_')) { 443 try { 444 const sessionData = JSON.parse(localStorage.getItem(key)); 445 sessions.push(sessionData); 446 } catch (error) { 447 console.warn('[Context] Failed to parse session:', key); 448 } 449 } 450 } 451 452 return sessions.sort((a, b) => b.timestamp - a.timestamp); 453 } 454 455 /** 456 * Clear old sessions (keep last 5) 457 */ 458 cleanupOldSessions() { 459 const sessions = this.getAllSessions(); 460 461 // Remove sessions older than 7 days or beyond the limit 462 const cutoffTime = Date.now() - (7 * 24 * 60 * 60 * 1000); // 7 days 463 const sessionsToRemove = sessions.slice(5).concat( 464 sessions.filter(s => s.timestamp < cutoffTime) 465 ); 466 467 sessionsToRemove.forEach(session => { 468 localStorage.removeItem(`wikiLive_session_${session.sessionId}`); 469 }); 470 471 if (sessionsToRemove.length > 0) { 472 console.log(`[Context] Cleaned up ${sessionsToRemove.length} old sessions`); 473 } 474 } 475 476 /** 477 * Start new conversation session 478 */ 479 startNewSession() { 480 this.sessionId = this.generateSessionId(); 481 this.conversationHistory = []; 482 this.entityMemory = new Map(); 483 this.topicMemory = new Set(); 484 485 console.log(`[Context] Started new session: ${this.sessionId}`); 486 } 487 488 /** 489 * Get conversation summary for display 490 */ 491 getConversationSummary() { 492 const entities = Array.from(this.entityMemory.values()) 493 .sort((a, b) => b.mentionCount - a.mentionCount) 494 .slice(0, 5); 495 496 return { 497 sessionId: this.sessionId, 498 messageCount: this.conversationHistory.length, 499 topEntities: entities, 500 activeTopics: Array.from(this.topicMemory), 501 sessionAge: this.conversationHistory.length > 0 ? 502 Date.now() - this.conversationHistory[0].timestamp : 0 503 }; 504 } 505 506 /** 507 * Clear stale context data to prevent carryover between sessions 508 */ 509 clearStaleContext() { 510 const now = Date.now(); 511 const staleThreshold = 600000; // 10 minutes 512 513 // Remove stale entities 514 const staleEntities = []; 515 for (const [key, entity] of this.entityMemory.entries()) { 516 if (now - entity.lastMention > staleThreshold) { 517 staleEntities.push(key); 518 } 519 } 520 521 staleEntities.forEach(key => { 522 console.log(`[Context] Removing stale entity: ${key}`); 523 this.entityMemory.delete(key); 524 }); 525 526 // Clear old conversation history (keep only last 10 messages) 527 if (this.conversationHistory.length > 10) { 528 this.conversationHistory = this.conversationHistory.slice(-10); 529 console.log('[Context] Trimmed conversation history to last 10 messages'); 530 } 531 532 // Clear old topics 533 this.topicMemory.clear(); 534 console.log('[Context] Cleared topic memory'); 535 536 // Save cleaned session 537 this.saveSession(); 538 } 539} 540 541// Global instance 542window.conversationContext = new ConversationContext(); 543 544// Export for use in other modules 545if (typeof module !== 'undefined' && module.exports) { 546 module.exports = ConversationContext; 547}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.