Complete Agent Pipeline System with Claude Code & OpenClaw Integration
- 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
This commit is contained in:
332
agent-system/core/summarizer.ts
Normal file
332
agent-system/core/summarizer.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user