/** * Conversation Summarizer Module * * Uses LLM to create intelligent summaries of conversations, * preserving key information while reducing token count. */ import ZAI from 'z-ai-web-dev-sdk'; import { TokenCounter, countTokens } from './token-counter'; export interface SummaryResult { summary: string; originalTokens: number; summaryTokens: number; compressionRatio: number; keyPoints: string[]; decisions: string[]; actionItems: string[]; } export interface SummarizerOptions { maxSummaryTokens?: number; preserveRecentMessages?: number; extractKeyPoints?: boolean; extractDecisions?: boolean; extractActionItems?: boolean; } export interface ConversationTurn { role: 'user' | 'assistant' | 'system'; content: string; timestamp?: Date; metadata?: Record; } const DEFAULT_OPTIONS: Required = { maxSummaryTokens: 1000, preserveRecentMessages: 3, extractKeyPoints: true, extractDecisions: true, extractActionItems: true }; /** * ConversationSummarizer - Creates intelligent summaries of conversations */ export class ConversationSummarizer { private zai: Awaited> | null = null; private tokenCounter: TokenCounter; private options: Required; constructor(options: SummarizerOptions = {}) { this.options = { ...DEFAULT_OPTIONS, ...options }; this.tokenCounter = new TokenCounter(); } /** * Initialize the summarizer (lazy load ZAI) */ private async init(): Promise { if (!this.zai) { this.zai = await ZAI.create(); } } /** * Summarize a conversation */ async summarize( messages: ConversationTurn[], options?: Partial ): Promise { await this.init(); const opts = { ...this.options, ...options }; const originalTokens = this.tokenCounter.countConversation(messages).total; // Format conversation for summarization const conversationText = this.formatConversationForSummary(messages); // Create the summarization prompt const prompt = this.buildSummarizationPrompt(conversationText, opts); // Get summary from LLM const response = await this.zai!.chat.completions.create({ messages: [ { role: 'assistant', content: `You are a precise conversation summarizer. Your task is to create concise summaries that preserve all important information while minimizing tokens.` }, { role: 'user', content: prompt } ], thinking: { type: 'disabled' } }); const summaryText = response.choices?.[0]?.message?.content || ''; // Parse the structured response const parsed = this.parseSummaryResponse(summaryText); const summaryTokens = countTokens(parsed.summary); return { summary: parsed.summary, originalTokens, summaryTokens, compressionRatio: originalTokens > 0 ? summaryTokens / originalTokens : 0, keyPoints: parsed.keyPoints, decisions: parsed.decisions, actionItems: parsed.actionItems }; } /** * Format conversation for summarization */ private formatConversationForSummary(messages: ConversationTurn[]): string { return messages.map(msg => { const timestamp = msg.timestamp?.toISOString() || ''; return `[${msg.role.toUpperCase()}]${timestamp ? ` (${timestamp})` : ''}: ${msg.content}`; }).join('\n\n'); } /** * Build the summarization prompt */ private buildSummarizationPrompt( conversationText: string, options: Required ): string { const sections: string[] = []; sections.push(`Please summarize the following conversation concisely.`); sections.push(`The summary should be under ${options.maxSummaryTokens} tokens.`); if (options.extractKeyPoints) { sections.push(`\nExtract KEY POINTS as a bullet list.`); } if (options.extractDecisions) { sections.push(`Extract any DECISIONS made as a bullet list.`); } if (options.extractActionItems) { sections.push(`Extract any ACTION ITEMS as a bullet list.`); } sections.push(`\nFormat your response as: ## SUMMARY [Your concise summary here] ## KEY POINTS - [Key point 1] - [Key point 2] ## DECISIONS - [Decision 1] - [Decision 2] ## ACTION ITEMS - [Action item 1] - [Action item 2] --- CONVERSATION: ${conversationText}`); return sections.join('\n'); } /** * Parse the structured summary response */ private parseSummaryResponse(text: string): { summary: string; keyPoints: string[]; decisions: string[]; actionItems: string[]; } { const sections = { summary: '', keyPoints: [] as string[], decisions: [] as string[], actionItems: [] as string[] }; // Extract summary const summaryMatch = text.match(/## SUMMARY\s*([\s\S]*?)(?=##|$)/i); if (summaryMatch) { sections.summary = summaryMatch[1].trim(); } // Extract key points const keyPointsMatch = text.match(/## KEY POINTS\s*([\s\S]*?)(?=##|$)/i); if (keyPointsMatch) { sections.keyPoints = this.extractBulletPoints(keyPointsMatch[1]); } // Extract decisions const decisionsMatch = text.match(/## DECISIONS\s*([\s\S]*?)(?=##|$)/i); if (decisionsMatch) { sections.decisions = this.extractBulletPoints(decisionsMatch[1]); } // Extract action items const actionItemsMatch = text.match(/## ACTION ITEMS\s*([\s\S]*?)(?=##|$)/i); if (actionItemsMatch) { sections.actionItems = this.extractBulletPoints(actionItemsMatch[1]); } return sections; } /** * Extract bullet points from text */ private extractBulletPoints(text: string): string[] { const lines = text.split('\n'); return lines .map(line => line.replace(/^[-*•]\s*/, '').trim()) .filter(line => line.length > 0); } /** * Create a rolling summary (for continuous conversations) */ async createRollingSummary( previousSummary: string, newMessages: ConversationTurn[] ): Promise { await this.init(); const prompt = `You are updating a conversation summary with new messages. PREVIOUS SUMMARY: ${previousSummary} NEW MESSAGES: ${this.formatConversationForSummary(newMessages)} Create an updated summary that integrates the new information with the previous summary. Keep the summary concise but comprehensive. Format your response as: ## SUMMARY [Your updated summary] ## KEY POINTS - [Updated key points] ## DECISIONS - [Updated decisions] ## ACTION ITEMS - [Updated action items]`; const response = await this.zai!.chat.completions.create({ messages: [ { role: 'assistant', content: 'You are a conversation summarizer that maintains rolling summaries.' }, { role: 'user', content: prompt } ], thinking: { type: 'disabled' } }); const summaryText = response.choices?.[0]?.message?.content || ''; const parsed = this.parseSummaryResponse(summaryText); return { summary: parsed.summary, originalTokens: countTokens(previousSummary) + this.tokenCounter.countConversation(newMessages).total, summaryTokens: countTokens(parsed.summary), compressionRatio: 0, keyPoints: parsed.keyPoints, decisions: parsed.decisions, actionItems: parsed.actionItems }; } /** * Create a topic-based summary (groups messages by topic) */ async createTopicSummary( messages: ConversationTurn[] ): Promise> { await this.init(); // First, identify topics const topicResponse = await this.zai!.chat.completions.create({ messages: [ { role: 'assistant', content: 'Identify the main topics in this conversation. Respond with a JSON array of topic names.' }, { role: 'user', content: this.formatConversationForSummary(messages) } ], thinking: { type: 'disabled' } }); let topics: string[] = []; try { const topicText = topicResponse.choices?.[0]?.message?.content || '[]'; topics = JSON.parse(topicText.match(/\[.*\]/s)?.[0] || '[]'); } catch { topics = ['General']; } // Create summaries for each topic const summaries = new Map(); for (const topic of topics) { const summary = await this.summarize(messages, { maxSummaryTokens: 500 }); summaries.set(topic, summary); } return summaries; } } // Singleton instance export const defaultSummarizer = new ConversationSummarizer();