Skip to main content
Glama

ACE MCP Server

curator.tsโ€ข14.7 kB
/** * Curator component for the ACE framework. * * The Curator synthesizes insights into delta operations that can be * applied to the playbook to improve future interactions. */ import { LLMProvider } from '../llm/provider'; import { Playbook } from './playbook'; import { ACEError } from '../utils/errors'; import { logger } from '../utils/logger'; import { Insight, CurationResult, DeltaOperation, Bullet } from './types'; import { CURATOR_SYNTHESIS_PROMPT, CURATOR_DEDUP_PROMPT } from './prompts'; import { v4 as uuidv4 } from 'uuid'; /** * Error thrown by Curator operations. */ export class CuratorError extends ACEError { constructor(message: string, public readonly originalError?: Error) { super(message); this.name = 'CuratorError'; Object.setPrototypeOf(this, CuratorError.prototype); } } /** * Options for curation process. */ export interface CurationOptions { /** Minimum confidence threshold for insights */ minConfidence?: number; /** Temperature for LLM generation */ temperature?: number; /** Maximum tokens for response */ maxTokens?: number; /** Specific model to use */ model?: string; /** Enable deduplication checking */ enableDeduplication?: boolean; /** Similarity threshold for deduplication */ dedupThreshold?: number; } /** * Deduplication assessment result. */ interface DeduplicationAssessment { assessment: 'DUPLICATE' | 'SIMILAR' | 'UNIQUE'; related_bullets: string[]; recommendation: 'merge' | 'update' | 'keep_separate' | 'discard'; reasoning: string; } /** * Curator synthesizes insights into actionable delta operations. * * Features: * - Insight filtering and validation * - Deduplication detection using embeddings * - Delta operation generation * - Bullet merging and updating logic * - Comprehensive curation summaries */ export class Curator { constructor( private llmProvider: LLMProvider, private playbook: Playbook ) { logger.info('Curator initialized', { provider: this.llmProvider.name, bulletCount: this.playbook.getBulletCount() }); } /** * Curate insights into delta operations. */ async curate(insights: Insight[], options: CurationOptions = {}): Promise<CurationResult> { if (!insights || insights.length === 0) { return { operations: [], summary: 'No insights provided for curation', statistics: { adds: 0, updates: 0, deletes: 0 } }; } const startTime = Date.now(); try { logger.debug('Starting curation', { insightCount: insights.length, minConfidence: options.minConfidence ?? 0.5 }); // Filter insights by confidence const filteredInsights = this.filterInsights(insights, options.minConfidence ?? 0.5); if (filteredInsights.length === 0) { return { operations: [], summary: 'No insights met the confidence threshold', statistics: { adds: 0, updates: 0, deletes: 0 } }; } // Generate delta operations const operations = await this.generateDeltaOperations(filteredInsights, options); // Create summary const summary = this.createSummary(operations, filteredInsights); // Calculate statistics const statistics = this.calculateStatistics(operations); const duration = Date.now() - startTime; logger.info('Curation completed', { duration, insightCount: filteredInsights.length, operationCount: operations.length, statistics }); return { operations, summary, statistics }; } catch (error) { const duration = Date.now() - startTime; logger.error('Curation failed', { duration, insightCount: insights.length, error: (error as Error).message }); if (error instanceof ACEError) { throw error; } throw new CuratorError( `Failed to curate insights: ${(error as Error).message}`, error as Error ); } } /** * Filter insights by confidence threshold. */ private filterInsights(insights: Insight[], minConfidence: number): Insight[] { return insights.filter(insight => { const confidence = insight.confidence || 0; return confidence >= minConfidence; }); } /** * Generate delta operations from insights. */ private async generateDeltaOperations(insights: Insight[], options: CurationOptions): Promise<DeltaOperation[]> { const operations: DeltaOperation[] = []; // Get current bullets for context const currentBullets = this.playbook.getBullets(); // Build prompt with current bullets and insights const prompt = this.buildSynthesisPrompt(currentBullets, insights); // Get LLM response const response = await this.llmProvider.chat([ { role: 'user', content: prompt } ], { temperature: options.temperature || 0.3, maxTokens: options.maxTokens || 2000, model: options.model }); // Parse operations from response const parsedOperations = this.parseOperations(response); // Process each operation with deduplication if enabled for (const operation of parsedOperations) { if (operation.type === 'ADD' && options.enableDeduplication !== false) { const processedOp = await this.processAddOperation(operation, options); if (processedOp) { operations.push(processedOp); } } else { operations.push(operation); } } return operations; } /** * Build synthesis prompt with current bullets and insights. */ private buildSynthesisPrompt(currentBullets: Bullet[], insights: Insight[]): string { const bulletsText = currentBullets.length > 0 ? currentBullets.map(b => `${b.id}: [${b.section}] ${b.content}`).join('\n') : 'No bullets currently in playbook'; const insightsText = JSON.stringify(insights, null, 2); return CURATOR_SYNTHESIS_PROMPT .replace('{current_bullets}', bulletsText) .replace('{insights}', insightsText); } /** * Parse operations from LLM response. */ private parseOperations(response: string): DeltaOperation[] { try { const parsed = JSON.parse(response); if (parsed.operations && Array.isArray(parsed.operations)) { return parsed.operations.map((op: any) => this.validateOperation(op)); } } catch (error) { logger.warn('Failed to parse operations JSON, attempting text extraction', { error: (error as Error).message }); } // Fallback: extract operations from text return this.extractOperationsFromText(response); } /** * Validate and normalize an operation. */ private validateOperation(op: any): DeltaOperation { const operation: DeltaOperation = { type: op.type }; if (op.type === 'ADD' && op.bullet) { operation.bullet = { id: op.bullet.id || uuidv4(), section: op.bullet.section || 'General', content: op.bullet.content || '', metadata: { created: new Date(), helpful_count: 0, harmful_count: 0, ...op.bullet.metadata } }; } else if (op.type === 'UPDATE' && op.bulletId) { operation.bulletId = op.bulletId; operation.updates = op.updates || {}; } else if (op.type === 'DELETE' && op.bulletId) { operation.bulletId = op.bulletId; } return operation; } /** * Extract operations from text response (fallback). */ private extractOperationsFromText(response: string): DeltaOperation[] { const operations: DeltaOperation[] = []; // Simple text parsing - look for operation patterns const lines = response.split('\n'); for (const line of lines) { const trimmed = line.trim().toLowerCase(); if (trimmed.includes('add') && trimmed.includes('bullet')) { // Extract ADD operation const contentMatch = line.match(/["']([^"']+)["']/); if (contentMatch) { operations.push({ type: 'ADD', bullet: { id: uuidv4(), section: 'General', content: contentMatch[1], metadata: { created: new Date(), helpful_count: 0, harmful_count: 0 } } }); } } } return operations; } /** * Process ADD operation with deduplication checking. */ private async processAddOperation( operation: DeltaOperation, options: CurationOptions ): Promise<DeltaOperation | null> { if (!operation.bullet) { return operation; } const newBulletContent = operation.bullet.content; // Check for duplicates using embeddings if available if (options.enableDeduplication && this.llmProvider.embed) { try { const embedding = await this.llmProvider.embed(newBulletContent); const threshold = options.dedupThreshold || 0.85; const similarBullets = this.playbook.findSimilarBullets(embedding, threshold); if (similarBullets.length > 0) { // Get deduplication assessment const assessment = await this.assessDeduplication(newBulletContent, similarBullets, options); return this.handleDeduplication(operation, assessment, similarBullets); } } catch (error) { logger.warn('Deduplication check failed, proceeding with add', { error: (error as Error).message }); } } return operation; } /** * Assess deduplication for a new bullet. */ private async assessDeduplication( newBulletContent: string, existingBullets: Bullet[], options: CurationOptions ): Promise<DeduplicationAssessment> { const existingBulletsText = existingBullets .map(b => `${b.id}: ${b.content}`) .join('\n'); const prompt = CURATOR_DEDUP_PROMPT .replace('{new_bullet_content}', newBulletContent) .replace('{existing_bullets}', existingBulletsText); try { const response = await this.llmProvider.chat([ { role: 'user', content: prompt } ], { temperature: options.temperature || 0.2, maxTokens: options.maxTokens || 500, model: options.model }); const assessment = JSON.parse(response); return { assessment: assessment.assessment || 'UNIQUE', related_bullets: assessment.related_bullets || [], recommendation: assessment.recommendation || 'keep_separate', reasoning: assessment.reasoning || 'No reasoning provided' }; } catch (error) { logger.warn('Deduplication assessment failed, treating as unique', { error: (error as Error).message }); return { assessment: 'UNIQUE', related_bullets: [], recommendation: 'keep_separate', reasoning: 'Assessment failed, treating as unique' }; } } /** * Handle deduplication based on assessment. */ private handleDeduplication( operation: DeltaOperation, assessment: DeduplicationAssessment, similarBullets: Bullet[] ): DeltaOperation | null { switch (assessment.recommendation) { case 'discard': logger.debug('Discarding duplicate bullet', { content: operation.bullet?.content, reasoning: assessment.reasoning }); return null; case 'merge': // Convert to UPDATE operation for the most similar bullet if (similarBullets.length > 0) { const targetBullet = similarBullets[0]; return { type: 'UPDATE', bulletId: targetBullet.id, updates: { content: `${targetBullet.content}. ${operation.bullet?.content}` } }; } break; case 'update': // Convert to UPDATE operation if (similarBullets.length > 0) { const targetBullet = similarBullets[0]; return { type: 'UPDATE', bulletId: targetBullet.id, updates: { content: operation.bullet?.content } }; } break; case 'keep_separate': default: // Keep as ADD operation return operation; } return operation; } /** * Create a human-readable summary of the curation. */ private createSummary(operations: DeltaOperation[], insights: Insight[]): string { const stats = this.calculateStatistics(operations); let summary = `Processed ${insights.length} insights and generated ${operations.length} operations: `; const parts: string[] = []; if (stats.adds > 0) { parts.push(`${stats.adds} new bullet${stats.adds > 1 ? 's' : ''}`); } if (stats.updates > 0) { parts.push(`${stats.updates} update${stats.updates > 1 ? 's' : ''}`); } if (stats.deletes > 0) { parts.push(`${stats.deletes} deletion${stats.deletes > 1 ? 's' : ''}`); } if (parts.length === 0) { summary += 'no changes recommended'; } else { summary += parts.join(', '); } summary += '.'; return summary; } /** * Calculate statistics for operations. */ private calculateStatistics(operations: DeltaOperation[]): { adds: number; updates: number; deletes: number } { return operations.reduce( (stats, op) => { switch (op.type) { case 'ADD': stats.adds++; break; case 'UPDATE': stats.updates++; break; case 'DELETE': stats.deletes++; break; } return stats; }, { adds: 0, updates: 0, deletes: 0 } ); } /** * Test the curator with sample insights. */ async test(): Promise<boolean> { try { const testInsights: Insight[] = [ { observation: 'Code was hard to read', lesson: 'Use clear variable names', suggested_bullet: 'Always use descriptive variable names', confidence: 0.8, section: 'Code Quality' } ]; const result = await this.curate(testInsights, { minConfidence: 0.5, enableDeduplication: false }); return result.operations.length >= 0; // Should at least return empty array } catch (error) { logger.error('Curator test failed', { error: (error as Error).message }); return false; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Angry-Robot-Deals/ace-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server