UNPKG

contaigents

Version:

Modular AI Content Ecosystem with Audio Generation

430 lines (425 loc) 17.5 kB
import { BaseTool } from './BaseTool.js'; import { FileService } from '../fileService.js'; export class MemoryTool extends BaseTool { constructor(baseDir) { super(); this.maxMemoryEntries = 50; // Limit to prevent memory from growing too large this.fileService = new FileService(baseDir); this.memoryFile = '.chat_memory.txt'; } getName() { return 'memory'; } getDescription() { return 'Manage chat memory to maintain context across long conversations. Use this to store important information, track progress, and maintain continuity.'; } getParameters() { return [ { name: 'operation', type: 'string', description: 'Memory operation: read, append, update, clear, summarize', required: true }, { name: 'content', type: 'string', description: 'Content to store in memory (required for append/update operations)', required: false }, { name: 'max_entries', type: 'number', description: 'Maximum number of memory entries to keep (default: 50)', required: false } ]; } getAgentGuidance() { return `**Memory Tool Usage Guidelines:** Use the memory tool to maintain context across long conversations: 1. **After completing significant tasks**: Store key outcomes and decisions 2. **When starting new topics**: Read memory to understand previous context 3. **End-of-turn memory**: You can include memory calls with your final response - they won't trigger additional LLM turns 4. **Tool results are automatically stored**: Non-memory tool results are automatically saved to memory **Best Practices:** - Keep entries concise but informative - Include key decisions, completed tasks, and important context - Use present tense and be specific - Focus on information that will be useful in future conversations - Memory calls at the end of responses are processed without additional turns **Example Usage:** \`\`\`xml <tool_call name="memory" id="1"> <operation>append</operation> <content>User requested screenplay agent modification. Successfully updated PromptTemplates.ts to transform chat agent into cinematic screenplay creation assistant. Test completed - agent now creates story outlines, character profiles, and scene files with visual sketches.</content> </tool_call> \`\`\``; } async execute(parameters) { const validationError = this.validateParameters(parameters); if (validationError) { return validationError; } const { operation, content, max_entries } = parameters; if (max_entries && typeof max_entries === 'number') { this.maxMemoryEntries = max_entries; } try { switch (operation) { case 'read': return await this.readMemory(); case 'append': if (!content) { return { success: false, error: 'Content is required for append operation' }; } return await this.appendMemory(content); case 'update': if (!content) { return { success: false, error: 'Content is required for update operation' }; } return await this.updateMemory(content); case 'clear': return await this.clearMemory(); case 'summarize': return await this.summarizeMemory(); default: return { success: false, error: `Unknown operation: ${operation}. Available operations: read, append, update, clear, summarize` }; } } catch (error) { return { success: false, error: `Memory operation failed: ${error.message}` }; } } async readMemory() { try { const content = await this.fileService.readFile(this.memoryFile); const entries = this.parseMemoryContent(content); return { success: true, data: { entries, total_entries: entries.length, memory_file: this.memoryFile }, message: `Memory contains ${entries.length} entries` }; } catch (error) { // Memory file doesn't exist yet return { success: true, data: { entries: [], total_entries: 0, memory_file: this.memoryFile }, message: 'Memory is empty (no memory file found)' }; } } async appendMemory(content) { const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19); const newEntry = `[${timestamp}] ${content}`; try { // Read existing memory let existingContent = ''; try { existingContent = await this.fileService.readFile(this.memoryFile); } catch (error) { // File doesn't exist, start fresh } // Parse existing entries const entries = this.parseMemoryContent(existingContent); entries.push({ timestamp, content }); // Trim to max entries if needed const trimmedEntries = entries.slice(-this.maxMemoryEntries); // Write back to file const newContent = trimmedEntries .map(entry => `[${entry.timestamp}] ${entry.content}`) .join('\n'); await this.fileService.saveFile({ path: this.memoryFile, content: newContent }); return { success: true, data: { new_entry: newEntry, total_entries: trimmedEntries.length, trimmed: entries.length > this.maxMemoryEntries }, message: `Added memory entry. Total entries: ${trimmedEntries.length}${entries.length > this.maxMemoryEntries ? ' (trimmed older entries)' : ''}` }; } catch (error) { return { success: false, error: `Failed to append memory: ${error.message}` }; } } async updateMemory(content) { try { await this.fileService.saveFile({ path: this.memoryFile, content: content }); const entries = this.parseMemoryContent(content); return { success: true, data: { total_entries: entries.length, memory_file: this.memoryFile }, message: `Memory updated with ${entries.length} entries` }; } catch (error) { return { success: false, error: `Failed to update memory: ${error.message}` }; } } async clearMemory() { try { await this.fileService.saveFile({ path: this.memoryFile, content: '' }); return { success: true, data: { memory_file: this.memoryFile }, message: 'Memory cleared successfully' }; } catch (error) { return { success: false, error: `Failed to clear memory: ${error.message}` }; } } async summarizeMemory() { try { const content = await this.fileService.readFile(this.memoryFile); const entries = this.parseMemoryContent(content); if (entries.length <= 10) { return { success: true, data: { total_entries: entries.length, action: 'no_summarization_needed' }, message: 'Memory has few entries, no summarization needed' }; } // Keep recent entries and summarize older ones const recentEntries = entries.slice(-10); const olderEntries = entries.slice(0, -10); // Create a simple summary of older entries const summary = this.createSummary(olderEntries); const summaryEntry = { timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19), content: `SUMMARY: ${summary}` }; // Combine summary with recent entries const newEntries = [summaryEntry, ...recentEntries]; const newContent = newEntries .map(entry => `[${entry.timestamp}] ${entry.content}`) .join('\n'); await this.fileService.saveFile({ path: this.memoryFile, content: newContent }); return { success: true, data: { original_entries: entries.length, new_entries: newEntries.length, summarized_entries: olderEntries.length }, message: `Memory summarized: ${olderEntries.length} older entries condensed into summary, ${recentEntries.length} recent entries preserved` }; } catch (error) { return { success: false, error: `Failed to summarize memory: ${error.message}` }; } } parseMemoryContent(content) { if (!content.trim()) { return []; } return content .split('\n') .filter(line => line.trim()) .map(line => { const match = line.match(/^\[([^\]]+)\] (.+)$/); if (match) { return { timestamp: match[1], content: match[2] }; } // Fallback for malformed entries return { timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19), content: line }; }); } createSummary(entries) { // Simple summarization - extract key topics and actions const topics = new Set(); const actions = new Set(); entries.forEach(entry => { const content = entry.content.toLowerCase(); // Extract potential topics (simple keyword matching) if (content.includes('screenplay') || content.includes('script')) topics.add('screenplay development'); if (content.includes('agent') || content.includes('chat')) topics.add('chat agent configuration'); if (content.includes('test') || content.includes('demo')) topics.add('testing and validation'); if (content.includes('file') || content.includes('tool')) topics.add('file operations'); if (content.includes('memory') || content.includes('context')) topics.add('memory management'); // Extract actions if (content.includes('created') || content.includes('generated')) actions.add('content creation'); if (content.includes('modified') || content.includes('updated')) actions.add('system modifications'); if (content.includes('completed') || content.includes('finished')) actions.add('task completion'); }); const topicList = Array.from(topics).join(', '); const actionList = Array.from(actions).join(', '); return `Previous conversation covered: ${topicList}. Key actions: ${actionList}. (${entries.length} entries from ${entries[0]?.timestamp} to ${entries[entries.length - 1]?.timestamp})`; } /** * Get current memory content for system prompt inclusion */ async getMemoryForSystemPrompt() { try { const result = await this.readMemory(); if (result.success && result.data.entries.length > 0) { const entries = result.data.entries; const recentEntries = entries.slice(-10); // Only include recent entries in system prompt return recentEntries .map(entry => `[${entry.timestamp}] ${entry.content}`) .join('\n'); } return ''; } catch (error) { return ''; } } /** * Automatically store tool response in memory */ async storeToolResponse(toolName, toolResult, parameters) { try { const summary = this.summarizeToolResult(toolName, toolResult, parameters); await this.appendMemory(`TOOL_RESULT: ${toolName} - ${summary}`); } catch (error) { // Silently fail - memory storage shouldn't break the main flow console.error('Failed to store tool response in memory:', error); } } /** * Store conversation summary when trimming history */ async storeConversationSummary(messages) { try { const summary = this.summarizeConversation(messages); if (summary) { await this.appendMemory(`CONVERSATION_SUMMARY: ${summary}`); } } catch (error) { console.error('Failed to store conversation summary:', error); } } summarizeToolResult(toolName, toolResult, parameters) { const status = toolResult.success ? 'SUCCESS' : 'FAILED'; // Create concise summaries based on tool type switch (toolName) { case 'write_file': const operation = parameters?.operation_type || 'write'; const filePath = parameters?.file_path || 'unknown'; return `${status} - ${operation} file ${filePath}`; case 'read_file': const readPath = parameters?.file_path || 'unknown'; return `${status} - read file ${readPath}`; case 'execute_command': const command = parameters?.command || 'unknown command'; return `${status} - executed: ${command.substring(0, 50)}${command.length > 50 ? '...' : ''}`; case 'image_generation': return `${status} - generated image`; case 'audio_generation': return `${status} - generated audio`; default: return `${status} - ${toolResult.message || 'completed'}`; } } summarizeConversation(messages) { if (messages.length === 0) return ''; // Extract key topics and actions from conversation const userMessages = messages.filter(m => m.role === 'user'); const assistantMessages = messages.filter(m => m.role === 'assistant'); if (userMessages.length === 0) return ''; // Simple summarization - extract key topics const topics = new Set(); const actions = new Set(); [...userMessages, ...assistantMessages].forEach(msg => { const content = msg.content.toLowerCase(); // Extract topics if (content.includes('screenplay') || content.includes('script')) topics.add('screenplay'); if (content.includes('agent') || content.includes('chat')) topics.add('agent'); if (content.includes('memory') || content.includes('context')) topics.add('memory'); if (content.includes('file') || content.includes('tool')) topics.add('files'); if (content.includes('test') || content.includes('demo')) topics.add('testing'); // Extract actions if (content.includes('create') || content.includes('generate')) actions.add('creation'); if (content.includes('modify') || content.includes('update')) actions.add('modification'); if (content.includes('read') || content.includes('analyze')) actions.add('analysis'); }); const topicList = Array.from(topics).join(', '); const actionList = Array.from(actions).join(', '); const startTime = messages[0]?.timestamp ? new Date(messages[0].timestamp).toISOString().substring(0, 19) : 'unknown'; const endTime = messages[messages.length - 1]?.timestamp ? new Date(messages[messages.length - 1].timestamp).toISOString().substring(0, 19) : 'unknown'; return `Conversation (${startTime} to ${endTime}): Topics: ${topicList || 'general'}. Actions: ${actionList || 'discussion'}. (${messages.length} messages)`; } }