limitless_analyze_speaker
Analyze conversations with a specific person to measure speaking time, identify discussion topics, track interaction patterns, and gain relationship insights from recorded audio.
Instructions
Detailed analytics for conversations with a specific person including speaking time, topics, interaction patterns, and relationship insights.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| participant_name | Yes | Name of the person to analyze conversations with. | |
| time_expression | No | Time range like 'this week', 'past month' (defaults to 'past month'). | |
| timezone | No | IANA timezone for calculations. |
Implementation Reference
- src/server.ts:824-848 (registration)Tool registration for 'limitless_analyze_speaker' with description, input schema, and handler function that calls SpeakerAnalyticsEngine.analyzeConversationWithserver.tool("limitless_analyze_speaker", "Detailed analytics for conversations with a specific person including speaking time, topics, interaction patterns, and relationship insights.", SpeakerAnalyticsArgsSchema, async (args, _extra) => { try { let timeRange = undefined; if (args.time_expression) { const parser = new NaturalTimeParser({ timezone: args.timezone }); timeRange = parser.parseTimeExpression(args.time_expression); } const analytics = await SpeakerAnalyticsEngine.analyzeConversationWith( limitlessApiKey, args.participant_name, timeRange ); const resultText = `Speaker analytics for ${args.participant_name}:\n\n${JSON.stringify(analytics, null, 2)}`; return { content: [{ type: "text", text: resultText }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error analyzing speaker: ${errorMessage}` }], isError: true }; } } );
- src/server.ts:252-256 (schema)Zod schema definition for tool input arguments: participant_name (required), time_expression (optional), timezone (optional).const SpeakerAnalyticsArgsSchema = { participant_name: z.string().describe("Name of the person to analyze conversations with."), time_expression: z.string().optional().describe("Time range like 'this week', 'past month' (defaults to 'past month')."), timezone: z.string().optional().describe("IANA timezone for calculations."), };
- src/search-and-analytics.ts:431-482 (handler)Main handler logic in SpeakerAnalyticsEngine.analyzeConversationWith: fetches lifelogs, filters those containing the participant, computes speaking time, conversation count, topics, time distribution, and recent interactions.*/ static async analyzeConversationWith( apiKey: string, participantName: string, timeRange?: TimeRange ): Promise<SpeakerAnalytics> { // Fetch relevant lifelogs const lifelogParams: any = { 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 }; }
- src/search-and-analytics.ts:426-614 (helper)SpeakerAnalyticsEngine class containing the core analysis logic and supporting helper methods for participant conversation analytics.export class SpeakerAnalyticsEngine { /** * Generate comprehensive analytics for conversations with a specific person */ static async analyzeConversationWith( apiKey: string, participantName: string, timeRange?: TimeRange ): Promise<SpeakerAnalytics> { // Fetch relevant lifelogs const lifelogParams: any = { 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 }; } private static createEmptyAnalytics(participantName: string): SpeakerAnalytics { return { participant: participantName, totalSpeakingTime: 0, conversationCount: 0, averageConversationLength: 0, topTopics: [], timeDistribution: [], recentInteractions: [] }; } private static calculateSpeakingTime(lifelogs: Lifelog[], participantName: string): number { 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; } private static extractTopicsWithParticipant(lifelogs: Lifelog[], participantName: string): string[] { const topicCounts = new Map<string, number>(); 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); } private static analyzeTimeDistribution(lifelogs: Lifelog[], participantName: string): Array<{hour: number, duration: number}> { 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); } private static getRecentInteractions(lifelogs: Lifelog[], participantName: string): Array<{date: string, duration: number, topics: string[]}> { const interactions: Array<{date: string, duration: number, topics: string[]}> = []; // Group by date const dateGroups = new Map<string, Lifelog[]>(); 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<string>(); 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); } }