UNPKG

199bio-mcp-limitless-server

Version:

Scientifically Validated Speech Vitality Index for Limitless Pendant. Empirically validated metrics from 2,500+ conversation segments with transparent reliability assessment. Peer-reviewed methodology for clinical research applications.

442 lines 18.7 kB
import { getLifelogs } from "./limitless-client.js"; import { MeetingDetector, ActionItemExtractor, NaturalTimeParser } from "./advanced-features.js"; export class AdvancedSearch { /** * Comprehensive search across all lifelogs with intelligent context and relevance scoring */ static async searchConversationsAbout(apiKey, searchTerm, options = {}) { const { includeContext = true, contextLines = 2, maxResults = 20, minRelevanceScore = 0.3, searchInSpeaker, searchInContentType, timeRange } = options; // Fetch all relevant lifelogs const lifelogParams = {}; if (timeRange) { lifelogParams.start = timeRange.start; lifelogParams.end = timeRange.end; lifelogParams.timezone = timeRange.timezone; } // Use a large limit to search through more history lifelogParams.limit = 1000; lifelogParams.includeMarkdown = true; lifelogParams.includeHeadings = true; const allLifelogs = await getLifelogs(apiKey, lifelogParams); const results = []; const searchTermLower = searchTerm.toLowerCase(); for (const lifelog of allLifelogs) { const matchingNodes = this.findRelevantNodes(lifelog, searchTermLower, searchInSpeaker, searchInContentType); if (matchingNodes.length === 0) continue; // Calculate relevance score const relevanceScore = this.calculateRelevanceScore(matchingNodes, searchTermLower); if (relevanceScore < minRelevanceScore) continue; // Get context if requested let contextBefore = []; let contextAfter = []; if (includeContext && lifelog.contents) { const { before, after } = this.extractContext(lifelog.contents, matchingNodes, contextLines); contextBefore = before; contextAfter = after; } results.push({ lifelog, relevantNodes: matchingNodes, contextBefore, contextAfter, relevanceScore, summary: this.generateSearchSummary(lifelog, matchingNodes, searchTerm) }); } // Sort by relevance and return top results return results .sort((a, b) => b.relevanceScore - a.relevanceScore) .slice(0, maxResults); } static findRelevantNodes(lifelog, searchTermLower, searchInSpeaker, searchInContentType) { if (!lifelog.contents) return []; return lifelog.contents.filter(node => { // Content match const hasContentMatch = node.content?.toLowerCase().includes(searchTermLower); // Speaker filter if (searchInSpeaker && node.speakerName !== searchInSpeaker) { return false; } // Content type filter if (searchInContentType && !searchInContentType.includes(node.type)) { return false; } return hasContentMatch; }); } static calculateRelevanceScore(nodes, searchTerm) { let score = 0; let totalContent = 0; for (const node of nodes) { if (!node.content) continue; const content = node.content.toLowerCase(); totalContent += content.length; // Exact phrase match gets higher score if (content.includes(searchTerm)) { score += 10; } // Word matches const searchWords = searchTerm.split(' '); for (const word of searchWords) { const wordMatches = (content.match(new RegExp(word, 'g')) || []).length; score += wordMatches * 2; } // Bonus for headings if (node.type.startsWith('heading')) { score *= 1.5; } // Bonus for user speech if (node.speakerIdentifier === 'user') { score *= 1.2; } } // Normalize by content length to avoid bias toward longer content return Math.min(1.0, score / Math.max(100, totalContent / 10)); } static extractContext(allNodes, relevantNodes, contextLines) { const relevantIndices = relevantNodes.map(node => allNodes.indexOf(node)); const minIndex = Math.min(...relevantIndices); const maxIndex = Math.max(...relevantIndices); const before = allNodes.slice(Math.max(0, minIndex - contextLines), minIndex); const after = allNodes.slice(maxIndex + 1, Math.min(allNodes.length, maxIndex + 1 + contextLines)); return { before, after }; } static generateSearchSummary(lifelog, relevantNodes, searchTerm) { const time = new Date(lifelog.startTime).toLocaleString(); const nodeCount = relevantNodes.length; const speakers = new Set(relevantNodes.map(n => n.speakerName).filter(Boolean)); let summary = `Found ${nodeCount} reference${nodeCount !== 1 ? 's' : ''} to "${searchTerm}" at ${time}`; if (speakers.size > 0) { summary += ` (speakers: ${Array.from(speakers).join(', ')})`; } return summary; } } // ============================================================================= // DAILY SUMMARY GENERATOR // ============================================================================= export class DailySummaryGenerator { /** * Generate comprehensive daily summary with insights and analytics */ static async generateDailySummary(apiKey, date, timezone) { // Get the full day range for the specified date const parser = new NaturalTimeParser({ timezone }); const targetDate = new Date(date + 'T00:00:00'); const timeRange = { start: date + ' 00:00:00', end: date + ' 23:59:59', timezone: timezone || parser['timezone'] }; // Fetch all lifelogs for the day const lifelogs = await getLifelogs(apiKey, { date, timezone, limit: 1000, includeMarkdown: true, includeHeadings: true, direction: 'asc' }); if (lifelogs.length === 0) { return this.createEmptyDailySummary(date, timezone || 'UTC'); } // Detect meetings const meetings = MeetingDetector.detectMeetings(lifelogs); // Extract all action items const allActionItems = []; for (const lifelog of lifelogs) { if (lifelog.contents) { const items = ActionItemExtractor.extractFromNodes(lifelog.contents, lifelog.id); allActionItems.push(...items); } } // Calculate metrics const totalRecordingTime = this.calculateTotalRecordingTime(lifelogs); const totalSpeakingTime = this.calculateTotalSpeakingTime(lifelogs); const topParticipants = this.getTopParticipants(meetings); const keyTopics = this.extractKeyTopics(lifelogs); // Generate insights const insights = this.generateInsights(lifelogs, meetings); return { date, timezone: timezone || 'UTC', meetings, totalRecordingTime, totalSpeakingTime, topParticipants, keyTopics, actionItems: allActionItems, insights }; } static createEmptyDailySummary(date, timezone) { return { date, timezone, meetings: [], totalRecordingTime: 0, totalSpeakingTime: 0, topParticipants: [], keyTopics: [], actionItems: [], insights: { mostProductiveHours: [], longestMeeting: null, mostFrequentParticipant: null, topicsDiscussed: 0 } }; } static calculateTotalRecordingTime(lifelogs) { return lifelogs.reduce((total, lifelog) => { const duration = new Date(lifelog.endTime).getTime() - new Date(lifelog.startTime).getTime(); return total + duration; }, 0); } static calculateTotalSpeakingTime(lifelogs) { let totalSpeaking = 0; for (const lifelog of lifelogs) { if (lifelog.contents) { for (const node of lifelog.contents) { if (node.startOffsetMs !== undefined && node.endOffsetMs !== undefined) { totalSpeaking += node.endOffsetMs - node.startOffsetMs; } } } } return totalSpeaking; } static getTopParticipants(meetings) { const participantMap = new Map(); for (const meeting of meetings) { for (const participant of meeting.participants) { const existing = participantMap.get(participant.name) || { name: participant.name, identifier: participant.identifier, speakingDuration: 0, messageCount: 0 }; existing.speakingDuration += participant.speakingDuration; existing.messageCount += participant.messageCount; participantMap.set(participant.name, existing); } } return Array.from(participantMap.values()) .sort((a, b) => b.speakingDuration - a.speakingDuration) .slice(0, 5); } static extractKeyTopics(lifelogs) { const topicCounts = new Map(); for (const lifelog of lifelogs) { if (lifelog.contents) { for (const node of lifelog.contents) { if ((node.type === 'heading1' || node.type === 'heading2') && node.content) { const topic = node.content.trim(); topicCounts.set(topic, (topicCounts.get(topic) || 0) + 1); } } } } return Array.from(topicCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([topic]) => topic); } static generateInsights(lifelogs, meetings) { // Most productive hours based on meeting count and duration const hourlyActivity = this.analyzeHourlyActivity(lifelogs); const mostProductiveHours = hourlyActivity .sort((a, b) => b.activity - a.activity) .slice(0, 3) .map(h => `${h.hour}:00`); // Longest meeting const longestMeeting = meetings.reduce((longest, current) => { return (!longest || current.duration > longest.duration) ? current : longest; }, null); // Most frequent participant const participantCounts = new Map(); for (const meeting of meetings) { for (const participant of meeting.participants) { if (participant.identifier !== 'user') { participantCounts.set(participant.name, (participantCounts.get(participant.name) || 0) + 1); } } } const mostFrequentParticipant = participantCounts.size > 0 ? Array.from(participantCounts.entries()).sort((a, b) => b[1] - a[1])[0][0] : null; return { mostProductiveHours, longestMeeting, mostFrequentParticipant, topicsDiscussed: new Set(meetings.flatMap(m => m.mainTopics)).size }; } static analyzeHourlyActivity(lifelogs) { const hourlyData = new Array(24).fill(0).map((_, hour) => ({ hour, activity: 0 })); for (const lifelog of lifelogs) { const startHour = new Date(lifelog.startTime).getHours(); const duration = new Date(lifelog.endTime).getTime() - new Date(lifelog.startTime).getTime(); hourlyData[startHour].activity += duration; } return hourlyData; } } // ============================================================================= // SPEAKER ANALYTICS // ============================================================================= export class SpeakerAnalyticsEngine { /** * Generate comprehensive analytics for conversations with a specific person */ static async analyzeConversationWith(apiKey, participantName, timeRange) { // Fetch relevant lifelogs const lifelogParams = { limit: 1000, includeMarkdown: true, includeHeadings: true }; if (timeRange) { lifelogParams.start = timeRange.start; lifelogParams.end = timeRange.end; lifelogParams.timezone = timeRange.timezone; } const allLifelogs = await getLifelogs(apiKey, lifelogParams); // Filter lifelogs that contain the participant const relevantLifelogs = allLifelogs.filter(lifelog => lifelog.contents?.some(node => node.speakerName === participantName || (node.content && node.content.toLowerCase().includes(participantName.toLowerCase())))); if (relevantLifelogs.length === 0) { return this.createEmptyAnalytics(participantName); } // Calculate metrics const totalSpeakingTime = this.calculateSpeakingTime(relevantLifelogs, participantName); const conversationCount = relevantLifelogs.length; const averageConversationLength = totalSpeakingTime / conversationCount; const topTopics = this.extractTopicsWithParticipant(relevantLifelogs, participantName); const timeDistribution = this.analyzeTimeDistribution(relevantLifelogs, participantName); const recentInteractions = this.getRecentInteractions(relevantLifelogs, participantName); return { participant: participantName, totalSpeakingTime, conversationCount, averageConversationLength, topTopics, timeDistribution, recentInteractions }; } static createEmptyAnalytics(participantName) { return { participant: participantName, totalSpeakingTime: 0, conversationCount: 0, averageConversationLength: 0, topTopics: [], timeDistribution: [], recentInteractions: [] }; } static calculateSpeakingTime(lifelogs, participantName) { let totalTime = 0; for (const lifelog of lifelogs) { if (!lifelog.contents) continue; for (const node of lifelog.contents) { if (node.speakerName === participantName && node.startOffsetMs !== undefined && node.endOffsetMs !== undefined) { totalTime += node.endOffsetMs - node.startOffsetMs; } } } return totalTime; } static extractTopicsWithParticipant(lifelogs, participantName) { const topicCounts = new Map(); for (const lifelog of lifelogs) { if (!lifelog.contents) continue; // Check if participant is in this lifelog const hasParticipant = lifelog.contents.some(node => node.speakerName === participantName); if (hasParticipant) { // Extract topics from this lifelog for (const node of lifelog.contents) { if ((node.type === 'heading1' || node.type === 'heading2') && node.content) { const topic = node.content.trim(); topicCounts.set(topic, (topicCounts.get(topic) || 0) + 1); } } } } return Array.from(topicCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([topic]) => topic); } static analyzeTimeDistribution(lifelogs, participantName) { const hourlyData = new Array(24).fill(0).map((_, hour) => ({ hour, duration: 0 })); for (const lifelog of lifelogs) { if (!lifelog.contents) continue; const startHour = new Date(lifelog.startTime).getHours(); // Calculate duration with this participant let participantDuration = 0; for (const node of lifelog.contents) { if (node.speakerName === participantName && node.startOffsetMs !== undefined && node.endOffsetMs !== undefined) { participantDuration += node.endOffsetMs - node.startOffsetMs; } } hourlyData[startHour].duration += participantDuration; } return hourlyData.filter(h => h.duration > 0); } static getRecentInteractions(lifelogs, participantName) { const interactions = []; // Group by date const dateGroups = new Map(); for (const lifelog of lifelogs) { const date = lifelog.startTime.split('T')[0]; if (!dateGroups.has(date)) { dateGroups.set(date, []); } dateGroups.get(date).push(lifelog); } // Process each date for (const [date, dayLifelogs] of dateGroups) { let totalDuration = 0; const topics = new Set(); for (const lifelog of dayLifelogs) { if (!lifelog.contents) continue; // Check for participant and extract data for (const node of lifelog.contents) { if (node.speakerName === participantName) { if (node.startOffsetMs !== undefined && node.endOffsetMs !== undefined) { totalDuration += node.endOffsetMs - node.startOffsetMs; } } if ((node.type === 'heading1' || node.type === 'heading2') && node.content) { topics.add(node.content.trim()); } } } if (totalDuration > 0) { interactions.push({ date, duration: totalDuration, topics: Array.from(topics) }); } } return interactions .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) .slice(0, 10); } } //# sourceMappingURL=search-and-analytics.js.map