UNPKG

ai-memory-sdk

Version:

TypeScript SDK for using Letta subagents for pluggable memory management

248 lines (214 loc) 7.31 kB
import { LettaClient } from '@letta-ai/letta-client'; import { formatMessages } from './prompt-formatter'; import { MessageCreate } from './schemas'; export interface MemoryConfig { lettaApiKey?: string; } export class Memory { private lettaClient: LettaClient; constructor(config: MemoryConfig = {}) { const apiKey = config.lettaApiKey || process.env.LETTA_API_KEY; this.lettaClient = new LettaClient({ token: apiKey }); } private async createSleeptimeAgent(name: string, tags: string[]): Promise<string> { const agentState = await this.lettaClient.agents.create({ name, model: 'openai/gpt-4', agentType: 'sleeptime_agent', initialMessageSequence: [], tags, }); return agentState.id; } private async getMatchingAgent(tags: string[]) { const agents = await this.lettaClient.agents.list({ tags, matchAllTags: true, }); return agents.length > 0 ? agents[0] : null; } private async createContextBlock( agentId: string, label: string, description: string, charLimit: number = 10000, value: string = '' ): Promise<string> { const block = await this.lettaClient.blocks.create({ label, description, limit: charLimit, value, }); // Attach block to agent await this.lettaClient.agents.blocks.attach(agentId, block.id!); return block.id!; } private async listContextBlocks(agentId: string) { return await this.lettaClient.agents.blocks.list(agentId); } private async deleteContextBlock(agentId: string, blockId: string): Promise<void> { await this.lettaClient.agents.blocks.detach(agentId, blockId); await this.lettaClient.blocks.delete(blockId); } private async deleteAgent(agentId: string): Promise<void> { await this.lettaClient.agents.delete(agentId); } private async learnMessages(agentId: string, messages: Record<string, any>[], skipVectorStorage: boolean = true): Promise<string> { const messageCreates = messages.map(msg => ({ content: msg.content, role: msg.role, name: msg.name, metadata: msg.metadata } as MessageCreate)); const formattedMessages = formatMessages(messageCreates); console.log('FORMATTED MESSAGES', formattedMessages); console.log('AGENT ID', agentId); const lettaRun = await this.lettaClient.agents.messages.createAsync(agentId, { messages: formattedMessages as any, }); // Insert into archival in parallel if not skipping vector storage if (!skipVectorStorage) { const tasks = messages.map(message => this.lettaClient.agents.passages.create(agentId, { text: message.content, tags: [message.role], }) ); await Promise.all(tasks); } return lettaRun.id!; } private formatBlock(block: any): string { return `<${block.label} description="${block.description}">${block.value}</${block.label}>`; } private async getRunStatus(runId: string): Promise<string> { const run = await this.lettaClient.runs.retrieve(runId); if (!run) { throw new Error(`Run ${runId} not found`); } return run.status!; } async waitForRun(runId: string): Promise<void> { while (await this.getRunStatus(runId) !== 'completed') { await new Promise(resolve => setTimeout(resolve, 1000)); } } async initializeUserMemory( userId: string, options: { userContextBlockPrompt?: string; userContextBlockCharLimit?: number; userContextBlockValue?: string; summaryBlockPrompt?: string; summaryBlockCharLimit?: number; reset?: boolean; } = {} ): Promise<string> { const { userContextBlockPrompt = 'Details about the human user you are speaking to.', userContextBlockCharLimit = 10000, userContextBlockValue = '', summaryBlockPrompt = 'A short (1-2 sentences) running summary of the conversation.', summaryBlockCharLimit = 1000, reset = false, } = options; // Check if agent already exists const existingAgent = await this.getMatchingAgent([userId]); if (existingAgent) { if (reset) { await this.deleteAgent(existingAgent.id); } else { throw new Error( `Agent ${existingAgent.id} already exists for user ${userId}. Cannot re-initialize memory unless deleted.` ); } } // Create the agent const agentId = await this.createSleeptimeAgent( `subconscious_agent_user_${userId}`, [userId] ); // Create context blocks await this.createContextBlock( agentId, 'human', userContextBlockPrompt, userContextBlockCharLimit, userContextBlockValue ); await this.createContextBlock( agentId, 'summary', summaryBlockPrompt, summaryBlockCharLimit, '' ); // Attach a single archival memory (workaround) await this.lettaClient.agents.passages.create(agentId, { text: `Initialized memory for user ${userId}`, }); return agentId; } async addMessages(userId: string, messages: Record<string, any>[], skipVectorStorage: boolean = true): Promise<string> { let agentId: string; const agent = await this.getMatchingAgent([userId]); if (agent) { agentId = agent.id; } else { agentId = await this.initializeUserMemory(userId); } return await this.learnMessages(agentId, messages, skipVectorStorage); } async addFiles(files: Record<string, any>[]): Promise<never> { throw new Error('Not implemented'); } async getUserMemory(userId: string, promptFormatted: boolean = false): Promise<string | null> { const agent = await this.getMatchingAgent([userId]); if (agent) { const block = await this.lettaClient.agents.blocks.retrieve(agent.id, 'human'); if (promptFormatted) { return this.formatBlock(block); } return block.value; } return null; } async getSummary(userId: string, promptFormatted: boolean = false): Promise<string | null> { const agent = await this.getMatchingAgent([userId]); if (agent) { const block = await this.lettaClient.agents.blocks.retrieve(agent.id, 'summary'); if (promptFormatted) { return `<conversation_summary>${block.value}</conversation_summary>`; } return block.value; } return null; } async getMemoryAgentId(userId: string): Promise<string | null> { const agent = await this.getMatchingAgent([userId]); if (agent) { return agent.id; } return null; } async deleteUser(userId: string): Promise<void> { const agent = await this.getMatchingAgent([userId]); if (agent) { // Deleting the agent also deletes associated messages/blocks await this.lettaClient.agents.delete(agent.id); console.log(`Deleted agent ${agent.id} for user ${userId}`); } } async search(userId: string, query: string): Promise<string[]> { const agent = await this.getMatchingAgent([userId]); if (agent) { const response = await this.lettaClient.agents.passages.search(agent.id, { query, tags: ['user'], }); return response.results.map(result => result.content); } return []; } }