- Added Claude Code integration with full context compaction support - Added OpenClaw integration with deterministic pipeline support - Implemented parallel agent execution (4 projects x 3 roles pattern) - Added workspace isolation with permissions and quotas - Implemented Lobster-compatible YAML workflow parser - Added persistent memory store for cross-session context - Created comprehensive README with hero section This project was 100% autonomously built by Z.AI GLM-5
333 lines
8.6 KiB
TypeScript
333 lines
8.6 KiB
TypeScript
/**
|
|
* 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<string, unknown>;
|
|
}
|
|
|
|
const DEFAULT_OPTIONS: Required<SummarizerOptions> = {
|
|
maxSummaryTokens: 1000,
|
|
preserveRecentMessages: 3,
|
|
extractKeyPoints: true,
|
|
extractDecisions: true,
|
|
extractActionItems: true
|
|
};
|
|
|
|
/**
|
|
* ConversationSummarizer - Creates intelligent summaries of conversations
|
|
*/
|
|
export class ConversationSummarizer {
|
|
private zai: Awaited<ReturnType<typeof ZAI.create>> | null = null;
|
|
private tokenCounter: TokenCounter;
|
|
private options: Required<SummarizerOptions>;
|
|
|
|
constructor(options: SummarizerOptions = {}) {
|
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
this.tokenCounter = new TokenCounter();
|
|
}
|
|
|
|
/**
|
|
* Initialize the summarizer (lazy load ZAI)
|
|
*/
|
|
private async init(): Promise<void> {
|
|
if (!this.zai) {
|
|
this.zai = await ZAI.create();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Summarize a conversation
|
|
*/
|
|
async summarize(
|
|
messages: ConversationTurn[],
|
|
options?: Partial<SummarizerOptions>
|
|
): Promise<SummaryResult> {
|
|
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<SummarizerOptions>
|
|
): 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<SummaryResult> {
|
|
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<Map<string, SummaryResult>> {
|
|
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<string, SummaryResult>();
|
|
|
|
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();
|