| | |
| | import { z } from 'zod'; |
| | import { tool } from '@langchain/core/tools'; |
| | import { Tools } from 'librechat-data-provider'; |
| | import { logger } from '@librechat/data-schemas'; |
| | import { Run, Providers, GraphEvents } from '@librechat/agents'; |
| | import type { |
| | OpenAIClientOptions, |
| | StreamEventData, |
| | ToolEndCallback, |
| | ClientOptions, |
| | EventHandler, |
| | ToolEndData, |
| | LLMConfig, |
| | } from '@librechat/agents'; |
| | import type { TAttachment, MemoryArtifact } from 'librechat-data-provider'; |
| | import type { ObjectId, MemoryMethods } from '@librechat/data-schemas'; |
| | import type { BaseMessage, ToolMessage } from '@langchain/core/messages'; |
| | import type { Response as ServerResponse } from 'express'; |
| | import { Tokenizer } from '~/utils'; |
| |
|
| | type RequiredMemoryMethods = Pick< |
| | MemoryMethods, |
| | 'setMemory' | 'deleteMemory' | 'getFormattedMemories' |
| | >; |
| |
|
| | type ToolEndMetadata = Record<string, unknown> & { |
| | run_id?: string; |
| | thread_id?: string; |
| | }; |
| |
|
| | export interface MemoryConfig { |
| | validKeys?: string[]; |
| | instructions?: string; |
| | llmConfig?: Partial<LLMConfig>; |
| | tokenLimit?: number; |
| | } |
| |
|
| | export const memoryInstructions = |
| | 'The system automatically stores important user information and can update or delete memories based on user requests, enabling dynamic memory management.'; |
| |
|
| | const getDefaultInstructions = ( |
| | validKeys?: string[], |
| | tokenLimit?: number, |
| | ) => `Use the \`set_memory\` tool to save important information about the user, but ONLY when the user has requested you to remember something. |
| | |
| | The \`delete_memory\` tool should only be used in two scenarios: |
| | 1. When the user explicitly asks to forget or remove specific information |
| | 2. When updating existing memories, use the \`set_memory\` tool instead of deleting and re-adding the memory. |
| | |
| | 1. ONLY use memory tools when the user requests memory actions with phrases like: |
| | - "Remember [that] [I]..." |
| | - "Don't forget [that] [I]..." |
| | - "Please remember..." |
| | - "Store this..." |
| | - "Forget [that] [I]..." |
| | - "Delete the memory about..." |
| | |
| | 2. NEVER store information just because the user mentioned it in conversation. |
| | |
| | 3. NEVER use memory tools when the user asks you to use other tools or invoke tools in general. |
| | |
| | 4. Memory tools are ONLY for memory requests, not for general tool usage. |
| | |
| | 5. If the user doesn't ask you to remember or forget something, DO NOT use any memory tools. |
| | |
| | ${validKeys && validKeys.length > 0 ? `\nVALID KEYS: ${validKeys.join(', ')}` : ''} |
| | |
| | ${tokenLimit ? `\nTOKEN LIMIT: Maximum ${tokenLimit} tokens per memory value.` : ''} |
| | |
| | When in doubt, and the user hasn't asked to remember or forget anything, END THE TURN IMMEDIATELY.`; |
| |
|
| | |
| | |
| | |
| | export const createMemoryTool = ({ |
| | userId, |
| | setMemory, |
| | validKeys, |
| | tokenLimit, |
| | totalTokens = 0, |
| | }: { |
| | userId: string | ObjectId; |
| | setMemory: MemoryMethods['setMemory']; |
| | validKeys?: string[]; |
| | tokenLimit?: number; |
| | totalTokens?: number; |
| | }) => { |
| | const remainingTokens = tokenLimit ? tokenLimit - totalTokens : Infinity; |
| | const isOverflowing = tokenLimit ? remainingTokens <= 0 : false; |
| |
|
| | return tool( |
| | async ({ key, value }) => { |
| | try { |
| | if (validKeys && validKeys.length > 0 && !validKeys.includes(key)) { |
| | logger.warn( |
| | `Memory Agent failed to set memory: Invalid key "${key}". Must be one of: ${validKeys.join( |
| | ', ', |
| | )}`, |
| | ); |
| | return [`Invalid key "${key}". Must be one of: ${validKeys.join(', ')}`, undefined]; |
| | } |
| |
|
| | const tokenCount = Tokenizer.getTokenCount(value, 'o200k_base'); |
| |
|
| | if (isOverflowing) { |
| | const errorArtifact: Record<Tools.memory, MemoryArtifact> = { |
| | [Tools.memory]: { |
| | key: 'system', |
| | type: 'error', |
| | value: JSON.stringify({ |
| | errorType: 'already_exceeded', |
| | tokenCount: Math.abs(remainingTokens), |
| | totalTokens: totalTokens, |
| | tokenLimit: tokenLimit!, |
| | }), |
| | tokenCount: totalTokens, |
| | }, |
| | }; |
| | return [`Memory storage exceeded. Cannot save new memories.`, errorArtifact]; |
| | } |
| |
|
| | if (tokenLimit) { |
| | const newTotalTokens = totalTokens + tokenCount; |
| | const newRemainingTokens = tokenLimit - newTotalTokens; |
| |
|
| | if (newRemainingTokens < 0) { |
| | const errorArtifact: Record<Tools.memory, MemoryArtifact> = { |
| | [Tools.memory]: { |
| | key: 'system', |
| | type: 'error', |
| | value: JSON.stringify({ |
| | errorType: 'would_exceed', |
| | tokenCount: Math.abs(newRemainingTokens), |
| | totalTokens: newTotalTokens, |
| | tokenLimit, |
| | }), |
| | tokenCount: totalTokens, |
| | }, |
| | }; |
| | return [`Memory storage would exceed limit. Cannot save this memory.`, errorArtifact]; |
| | } |
| | } |
| |
|
| | const artifact: Record<Tools.memory, MemoryArtifact> = { |
| | [Tools.memory]: { |
| | key, |
| | value, |
| | tokenCount, |
| | type: 'update', |
| | }, |
| | }; |
| |
|
| | const result = await setMemory({ userId, key, value, tokenCount }); |
| | if (result.ok) { |
| | logger.debug(`Memory set for key "${key}" (${tokenCount} tokens) for user "${userId}"`); |
| | return [`Memory set for key "${key}" (${tokenCount} tokens)`, artifact]; |
| | } |
| | logger.warn(`Failed to set memory for key "${key}" for user "${userId}"`); |
| | return [`Failed to set memory for key "${key}"`, undefined]; |
| | } catch (error) { |
| | logger.error('Memory Agent failed to set memory', error); |
| | return [`Error setting memory for key "${key}"`, undefined]; |
| | } |
| | }, |
| | { |
| | name: 'set_memory', |
| | description: 'Saves important information about the user into memory.', |
| | responseFormat: 'content_and_artifact', |
| | schema: z.object({ |
| | key: z |
| | .string() |
| | .describe( |
| | validKeys && validKeys.length > 0 |
| | ? `The key of the memory value. Must be one of: ${validKeys.join(', ')}` |
| | : 'The key identifier for this memory', |
| | ), |
| | value: z |
| | .string() |
| | .describe( |
| | 'Value MUST be a complete sentence that fully describes relevant user information.', |
| | ), |
| | }), |
| | }, |
| | ); |
| | }; |
| |
|
| | |
| | |
| | |
| | const createDeleteMemoryTool = ({ |
| | userId, |
| | deleteMemory, |
| | validKeys, |
| | }: { |
| | userId: string | ObjectId; |
| | deleteMemory: MemoryMethods['deleteMemory']; |
| | validKeys?: string[]; |
| | }) => { |
| | return tool( |
| | async ({ key }) => { |
| | try { |
| | if (validKeys && validKeys.length > 0 && !validKeys.includes(key)) { |
| | logger.warn( |
| | `Memory Agent failed to delete memory: Invalid key "${key}". Must be one of: ${validKeys.join( |
| | ', ', |
| | )}`, |
| | ); |
| | return [`Invalid key "${key}". Must be one of: ${validKeys.join(', ')}`, undefined]; |
| | } |
| |
|
| | const artifact: Record<Tools.memory, MemoryArtifact> = { |
| | [Tools.memory]: { |
| | key, |
| | type: 'delete', |
| | }, |
| | }; |
| |
|
| | const result = await deleteMemory({ userId, key }); |
| | if (result.ok) { |
| | logger.debug(`Memory deleted for key "${key}" for user "${userId}"`); |
| | return [`Memory deleted for key "${key}"`, artifact]; |
| | } |
| | logger.warn(`Failed to delete memory for key "${key}" for user "${userId}"`); |
| | return [`Failed to delete memory for key "${key}"`, undefined]; |
| | } catch (error) { |
| | logger.error('Memory Agent failed to delete memory', error); |
| | return [`Error deleting memory for key "${key}"`, undefined]; |
| | } |
| | }, |
| | { |
| | name: 'delete_memory', |
| | description: |
| | 'Deletes specific memory data about the user using the provided key. For updating existing memories, use the `set_memory` tool instead', |
| | responseFormat: 'content_and_artifact', |
| | schema: z.object({ |
| | key: z |
| | .string() |
| | .describe( |
| | validKeys && validKeys.length > 0 |
| | ? `The key of the memory to delete. Must be one of: ${validKeys.join(', ')}` |
| | : 'The key identifier of the memory to delete', |
| | ), |
| | }), |
| | }, |
| | ); |
| | }; |
| | export class BasicToolEndHandler implements EventHandler { |
| | private callback?: ToolEndCallback; |
| | constructor(callback?: ToolEndCallback) { |
| | this.callback = callback; |
| | } |
| | handle( |
| | event: string, |
| | data: StreamEventData | undefined, |
| | metadata?: Record<string, unknown>, |
| | ): void { |
| | if (!metadata) { |
| | console.warn(`Graph or metadata not found in ${event} event`); |
| | return; |
| | } |
| | const toolEndData = data as ToolEndData | undefined; |
| | if (!toolEndData?.output) { |
| | console.warn('No output found in tool_end event'); |
| | return; |
| | } |
| | this.callback?.(toolEndData, metadata); |
| | } |
| | } |
| |
|
| | export async function processMemory({ |
| | res, |
| | userId, |
| | setMemory, |
| | deleteMemory, |
| | messages, |
| | memory, |
| | messageId, |
| | conversationId, |
| | validKeys, |
| | instructions, |
| | llmConfig, |
| | tokenLimit, |
| | totalTokens = 0, |
| | }: { |
| | res: ServerResponse; |
| | setMemory: MemoryMethods['setMemory']; |
| | deleteMemory: MemoryMethods['deleteMemory']; |
| | userId: string | ObjectId; |
| | memory: string; |
| | messageId: string; |
| | conversationId: string; |
| | messages: BaseMessage[]; |
| | validKeys?: string[]; |
| | instructions: string; |
| | tokenLimit?: number; |
| | totalTokens?: number; |
| | llmConfig?: Partial<LLMConfig>; |
| | }): Promise<(TAttachment | null)[] | undefined> { |
| | try { |
| | const memoryTool = createMemoryTool({ |
| | userId, |
| | tokenLimit, |
| | setMemory, |
| | validKeys, |
| | totalTokens, |
| | }); |
| | const deleteMemoryTool = createDeleteMemoryTool({ |
| | userId, |
| | validKeys, |
| | deleteMemory, |
| | }); |
| |
|
| | const currentMemoryTokens = totalTokens; |
| |
|
| | let memoryStatus = `# Existing memory:\n${memory ?? 'No existing memories'}`; |
| |
|
| | if (tokenLimit) { |
| | const remainingTokens = tokenLimit - currentMemoryTokens; |
| | memoryStatus = `# Memory Status: |
| | Current memory usage: ${currentMemoryTokens} tokens |
| | Token limit: ${tokenLimit} tokens |
| | Remaining capacity: ${remainingTokens} tokens |
| | |
| | # Existing memory: |
| | ${memory ?? 'No existing memories'}`; |
| | } |
| |
|
| | const defaultLLMConfig: LLMConfig = { |
| | provider: Providers.OPENAI, |
| | model: 'gpt-4.1-mini', |
| | temperature: 0.4, |
| | streaming: false, |
| | disableStreaming: true, |
| | }; |
| |
|
| | const finalLLMConfig: ClientOptions = { |
| | ...defaultLLMConfig, |
| | ...llmConfig, |
| | |
| | |
| | |
| | streaming: false, |
| | disableStreaming: true, |
| | }; |
| |
|
| | |
| | if ('model' in finalLLMConfig && /\bgpt-[5-9](?:\.\d+)?\b/i.test(finalLLMConfig.model ?? '')) { |
| | |
| | delete finalLLMConfig.temperature; |
| |
|
| | |
| | if ('maxTokens' in finalLLMConfig && finalLLMConfig.maxTokens != null) { |
| | const modelKwargs = (finalLLMConfig as OpenAIClientOptions).modelKwargs ?? {}; |
| | const paramName = |
| | (finalLLMConfig as OpenAIClientOptions).useResponsesApi === true |
| | ? 'max_output_tokens' |
| | : 'max_completion_tokens'; |
| | modelKwargs[paramName] = finalLLMConfig.maxTokens; |
| | delete finalLLMConfig.maxTokens; |
| | (finalLLMConfig as OpenAIClientOptions).modelKwargs = modelKwargs; |
| | } |
| | } |
| |
|
| | const artifactPromises: Promise<TAttachment | null>[] = []; |
| | const memoryCallback = createMemoryCallback({ res, artifactPromises }); |
| | const customHandlers = { |
| | [GraphEvents.TOOL_END]: new BasicToolEndHandler(memoryCallback), |
| | }; |
| |
|
| | const run = await Run.create({ |
| | runId: messageId, |
| | graphConfig: { |
| | type: 'standard', |
| | llmConfig: finalLLMConfig, |
| | tools: [memoryTool, deleteMemoryTool], |
| | instructions, |
| | additional_instructions: memoryStatus, |
| | toolEnd: true, |
| | }, |
| | customHandlers, |
| | returnContent: true, |
| | }); |
| |
|
| | const config = { |
| | runName: 'MemoryRun', |
| | configurable: { |
| | user_id: userId, |
| | thread_id: conversationId, |
| | provider: llmConfig?.provider, |
| | }, |
| | streamMode: 'values', |
| | recursionLimit: 3, |
| | version: 'v2', |
| | } as const; |
| |
|
| | const inputs = { |
| | messages, |
| | }; |
| | const content = await run.processStream(inputs, config); |
| | if (content) { |
| | logger.debug('Memory Agent processed memory successfully', content); |
| | } else { |
| | logger.warn('Memory Agent processed memory but returned no content'); |
| | } |
| | return await Promise.all(artifactPromises); |
| | } catch (error) { |
| | logger.error('Memory Agent failed to process memory', error); |
| | } |
| | } |
| |
|
| | export async function createMemoryProcessor({ |
| | res, |
| | userId, |
| | messageId, |
| | memoryMethods, |
| | conversationId, |
| | config = {}, |
| | }: { |
| | res: ServerResponse; |
| | messageId: string; |
| | conversationId: string; |
| | userId: string | ObjectId; |
| | memoryMethods: RequiredMemoryMethods; |
| | config?: MemoryConfig; |
| | }): Promise<[string, (messages: BaseMessage[]) => Promise<(TAttachment | null)[] | undefined>]> { |
| | const { validKeys, instructions, llmConfig, tokenLimit } = config; |
| | const finalInstructions = instructions || getDefaultInstructions(validKeys, tokenLimit); |
| |
|
| | const { withKeys, withoutKeys, totalTokens } = await memoryMethods.getFormattedMemories({ |
| | userId, |
| | }); |
| |
|
| | return [ |
| | withoutKeys, |
| | async function (messages: BaseMessage[]): Promise<(TAttachment | null)[] | undefined> { |
| | try { |
| | return await processMemory({ |
| | res, |
| | userId, |
| | messages, |
| | validKeys, |
| | llmConfig, |
| | messageId, |
| | tokenLimit, |
| | conversationId, |
| | memory: withKeys, |
| | totalTokens: totalTokens || 0, |
| | instructions: finalInstructions, |
| | setMemory: memoryMethods.setMemory, |
| | deleteMemory: memoryMethods.deleteMemory, |
| | }); |
| | } catch (error) { |
| | logger.error('Memory Agent failed to process memory', error); |
| | } |
| | }, |
| | ]; |
| | } |
| |
|
| | async function handleMemoryArtifact({ |
| | res, |
| | data, |
| | metadata, |
| | }: { |
| | res: ServerResponse; |
| | data: ToolEndData; |
| | metadata?: ToolEndMetadata; |
| | }) { |
| | const output = data?.output as ToolMessage | undefined; |
| | if (!output) { |
| | return null; |
| | } |
| |
|
| | if (!output.artifact) { |
| | return null; |
| | } |
| |
|
| | const memoryArtifact = output.artifact[Tools.memory] as MemoryArtifact | undefined; |
| | if (!memoryArtifact) { |
| | return null; |
| | } |
| |
|
| | const attachment: Partial<TAttachment> = { |
| | type: Tools.memory, |
| | toolCallId: output.tool_call_id, |
| | messageId: metadata?.run_id ?? '', |
| | conversationId: metadata?.thread_id ?? '', |
| | [Tools.memory]: memoryArtifact, |
| | }; |
| | if (!res.headersSent) { |
| | return attachment; |
| | } |
| | res.write(`event: attachment\ndata: ${JSON.stringify(attachment)}\n\n`); |
| | return attachment; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function createMemoryCallback({ |
| | res, |
| | artifactPromises, |
| | }: { |
| | res: ServerResponse; |
| | artifactPromises: Promise<Partial<TAttachment> | null>[]; |
| | }): ToolEndCallback { |
| | return async (data: ToolEndData, metadata?: Record<string, unknown>) => { |
| | const output = data?.output as ToolMessage | undefined; |
| | const memoryArtifact = output?.artifact?.[Tools.memory] as MemoryArtifact; |
| | if (memoryArtifact == null) { |
| | return; |
| | } |
| | artifactPromises.push( |
| | handleMemoryArtifact({ res, data, metadata }).catch((error) => { |
| | logger.error('Error processing memory artifact content:', error); |
| | return null; |
| | }), |
| | ); |
| | }; |
| | } |
| |
|