From c3870e1dd19357f6898c2c772411196b09056b0c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 2 May 2026 18:08:17 -0700 Subject: [PATCH 1/5] fix(memories): get memory tool, mem0 integration update --- apps/sim/app/api/memory/[id]/route.ts | 10 +- apps/sim/blocks/blocks/mem0.test.ts | 35 ++++++ apps/sim/blocks/blocks/mem0.ts | 133 +++++++++----------- apps/sim/tools/mem0/add_memories.test.ts | 63 ++++++++++ apps/sim/tools/mem0/add_memories.ts | 131 +++++++------------ apps/sim/tools/mem0/get_memories.test.ts | 120 ++++++++++++++++++ apps/sim/tools/mem0/get_memories.ts | 100 +++++++++------ apps/sim/tools/mem0/index.ts | 1 + apps/sim/tools/mem0/search_memories.test.ts | 72 +++++++++++ apps/sim/tools/mem0/search_memories.ts | 92 +++++++------- apps/sim/tools/mem0/types.ts | 74 ++++++----- apps/sim/tools/memory/get.test.ts | 65 ++++++++++ apps/sim/tools/memory/get.ts | 14 +-- 13 files changed, 626 insertions(+), 284 deletions(-) create mode 100644 apps/sim/blocks/blocks/mem0.test.ts create mode 100644 apps/sim/tools/mem0/add_memories.test.ts create mode 100644 apps/sim/tools/mem0/get_memories.test.ts create mode 100644 apps/sim/tools/mem0/search_memories.test.ts create mode 100644 apps/sim/tools/memory/get.test.ts diff --git a/apps/sim/app/api/memory/[id]/route.ts b/apps/sim/app/api/memory/[id]/route.ts index 20d0a71cd0a..6853182cfcf 100644 --- a/apps/sim/app/api/memory/[id]/route.ts +++ b/apps/sim/app/api/memory/[id]/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { memory } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq } from 'drizzle-orm' +import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { agentMemoryDataSchemaContract, @@ -75,7 +75,13 @@ export const GET = withRouteHandler(async (request: NextRequest, context: Memory const memories = await db .select() .from(memory) - .where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId))) + .where( + and( + eq(memory.key, id), + eq(memory.workspaceId, validatedWorkspaceId), + isNull(memory.deletedAt) + ) + ) .orderBy(memory.createdAt) .limit(1) diff --git a/apps/sim/blocks/blocks/mem0.test.ts b/apps/sim/blocks/blocks/mem0.test.ts new file mode 100644 index 00000000000..3e8b0cdd41d --- /dev/null +++ b/apps/sim/blocks/blocks/mem0.test.ts @@ -0,0 +1,35 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { Mem0Block } from '@/blocks/blocks/mem0' + +describe('Mem0Block', () => { + const buildParams = Mem0Block.tools.config.params! + + it('parses JSON string messages for add operations', () => { + const params = buildParams({ + operation: 'add', + apiKey: 'test-key', + userId: 'alice', + messages: JSON.stringify([{ role: 'user', content: 'I like Sim.' }]), + }) + + expect(params).toEqual({ + apiKey: 'test-key', + userId: 'alice', + messages: [{ role: 'user', content: 'I like Sim.' }], + }) + }) + + it('rejects unsupported message roles before execution', () => { + expect(() => + buildParams({ + operation: 'add', + apiKey: 'test-key', + userId: 'alice', + messages: JSON.stringify([{ role: 'system', content: 'Remember this.' }]), + }) + ).toThrow('Each message must have role user or assistant and non-empty content') + }) +}) diff --git a/apps/sim/blocks/blocks/mem0.ts b/apps/sim/blocks/blocks/mem0.ts index 8904e9593e2..e003408422f 100644 --- a/apps/sim/blocks/blocks/mem0.ts +++ b/apps/sim/blocks/blocks/mem0.ts @@ -1,6 +1,46 @@ +import { toError } from '@sim/utils/errors' import { Mem0Icon } from '@/components/icons' import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types' -import type { Mem0Response } from '@/tools/mem0/types' +import type { Mem0Message, Mem0Response } from '@/tools/mem0/types' + +function isMem0Message(value: unknown): value is Mem0Message { + return ( + Boolean(value) && + typeof value === 'object' && + 'role' in value && + 'content' in value && + (value.role === 'user' || value.role === 'assistant') && + typeof value.content === 'string' && + value.content.length > 0 + ) +} + +function parseMem0Messages(value: unknown): Mem0Message[] { + if (!value) { + throw new Error('Messages are required for add operation') + } + + let messages: unknown + try { + messages = typeof value === 'string' ? JSON.parse(value) : value + } catch (error) { + throw new Error(`Messages must be valid JSON: ${toError(error).message}`) + } + + if (!Array.isArray(messages) || messages.length === 0) { + throw new Error('Messages must be a non-empty array') + } + + const validMessages: Mem0Message[] = [] + for (const message of messages) { + if (!isMem0Message(message)) { + throw new Error('Each message must have role user or assistant and non-empty content') + } + validMessages.push(message) + } + + return validMessages +} export const Mem0Block: BlockConfig = { type: 'mem0', @@ -32,7 +72,6 @@ export const Mem0Block: BlockConfig = { title: 'User ID', type: 'short-input', placeholder: 'Enter user identifier', - value: () => 'userid', // Default to the working user ID from curl example required: true, }, { @@ -77,6 +116,7 @@ export const Mem0Block: BlockConfig = { field: 'operation', value: 'get', }, + mode: 'advanced', wandConfig: { enabled: true, prompt: `Generate a date in YYYY-MM-DD format based on the user's description. @@ -100,6 +140,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n field: 'operation', value: 'get', }, + mode: 'advanced', wandConfig: { enabled: true, prompt: `Generate a date in YYYY-MM-DD format based on the user's description. @@ -134,6 +175,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n field: 'operation', value: ['search', 'get'], }, + mode: 'advanced', }, ], tools: { @@ -153,16 +195,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n } }, params: (params: Record) => { - // Create detailed error information for any missing required fields const errors: string[] = [] + const operation = params.operation || 'add' - // Validate required API key for all operations if (!params.apiKey) { errors.push('API Key is required') } - // For search operation, validate required fields - if (params.operation === 'search') { + if (operation === 'search') { if (!params.query || params.query.trim() === '') { errors.push('Search Query is required') } @@ -172,27 +212,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n } } - // For add operation, validate required fields - if (params.operation === 'add') { - if (!params.messages) { - errors.push('Messages are required for add operation') - } else if (!Array.isArray(params.messages) || params.messages.length === 0) { - errors.push('Messages must be a non-empty array') - } else { - for (const msg of params.messages) { - if (!msg.role || !msg.content) { - errors.push("Each message must have 'role' and 'content' properties") - break - } - } - } - + if (operation === 'add') { if (!params.userId) { errors.push('User ID is required') } } - // Throw error if any required fields are missing if (errors.length > 0) { throw new Error(`Mem0 Block Error: ${errors.join(', ')}`) } @@ -201,63 +226,22 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n apiKey: params.apiKey, } - // Add any identifiers that are present if (params.userId) result.userId = params.userId - // Add version if specified - if (params.version) result.version = params.version - if (params.limit) result.limit = params.limit - const operation = params.operation || 'add' - - // Process operation-specific parameters switch (operation) { case 'add': - if (params.messages) { - try { - // Ensure messages are properly formatted - const messagesArray = - typeof params.messages === 'string' - ? JSON.parse(params.messages) - : params.messages - - // Validate message structure - if (Array.isArray(messagesArray) && messagesArray.length > 0) { - let validMessages = true - for (const msg of messagesArray) { - if (!msg.role || !msg.content) { - validMessages = false - break - } - } - if (validMessages) { - result.messages = messagesArray - } else { - // Consistent with other error handling - collect in errors array - errors.push('Invalid message format - each message must have role and content') - throw new Error( - 'Mem0 Block Error: Invalid message format - each message must have role and content' - ) - } - } else { - // Consistent with other error handling - errors.push('Messages must be a non-empty array') - throw new Error('Mem0 Block Error: Messages must be a non-empty array') - } - } catch (e: any) { - if (!errors.includes('Messages must be valid JSON')) { - errors.push('Messages must be valid JSON') - } - throw new Error(`Mem0 Block Error: ${e.message || 'Messages must be valid JSON'}`) - } + try { + result.messages = parseMem0Messages(params.messages) + } catch (error) { + throw new Error(`Mem0 Block Error: ${toError(error).message}`) } break case 'search': if (params.query) { result.query = params.query - // Check if we have at least one identifier for search if (!params.userId) { errors.push('Search requires a User ID') throw new Error('Mem0 Block Error: Search requires a User ID') @@ -267,7 +251,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n throw new Error('Mem0 Block Error: Search requires a query parameter') } - // Include limit if specified if (params.limit) { result.limit = Number(params.limit) } @@ -277,7 +260,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n result.memoryId = params.memoryId } - // Add date range filtering for v2 get memories if (params.startDate) { result.startDate = params.startDate } @@ -296,7 +278,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n operation: { type: 'string', description: 'Operation to perform' }, apiKey: { type: 'string', description: 'Mem0 API key' }, userId: { type: 'string', description: 'User identifier' }, - version: { type: 'string', description: 'API version' }, messages: { type: 'json', description: 'Message data array' }, query: { type: 'string', description: 'Search query' }, memoryId: { type: 'string', description: 'Memory identifier' }, @@ -305,8 +286,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n limit: { type: 'number', description: 'Result limit' }, }, outputs: { - ids: { type: 'json', description: 'Memory identifiers' }, - memories: { type: 'json', description: 'Memory data' }, - searchResults: { type: 'json', description: 'Search results' }, + ids: { type: 'json', description: 'Memory identifiers returned by search or get operations' }, + memories: { type: 'json', description: 'Memory records returned by get operations' }, + searchResults: { type: 'json', description: 'Ranked memory records returned by search' }, + message: { type: 'string', description: 'Add operation status message' }, + status: { type: 'string', description: 'Add operation processing status' }, + event_id: { type: 'string', description: 'Add operation event ID for status polling' }, + count: { type: 'number', description: 'Total memory count for get operations' }, + next: { type: 'string', description: 'Next page URL for get operations' }, + previous: { type: 'string', description: 'Previous page URL for get operations' }, }, } diff --git a/apps/sim/tools/mem0/add_memories.test.ts b/apps/sim/tools/mem0/add_memories.test.ts new file mode 100644 index 00000000000..8d1192bbe4b --- /dev/null +++ b/apps/sim/tools/mem0/add_memories.test.ts @@ -0,0 +1,63 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { mem0AddMemoriesTool } from '@/tools/mem0/add_memories' +import type { Mem0AddMemoriesParams } from '@/tools/mem0/types' + +describe('mem0AddMemoriesTool', () => { + const buildBody = mem0AddMemoriesTool.request.body! + const transformResponse = mem0AddMemoriesTool.transformResponse! + + it('uses the v3 add memories endpoint', () => { + expect(mem0AddMemoriesTool.request.url).toBe('https://api.mem0.ai/v3/memories/add/') + expect(mem0AddMemoriesTool.request.method).toBe('POST') + }) + + it('builds the documented add memories request body', () => { + const body = buildBody({ + apiKey: 'test-key', + userId: ' alice ', + messages: [{ role: 'user', content: 'I like Sim.' }], + }) + + expect(body).toEqual({ + messages: [{ role: 'user', content: 'I like Sim.' }], + user_id: 'alice', + }) + }) + + it('accepts JSON string messages from the block code input', () => { + const params: Mem0AddMemoriesParams = { + apiKey: 'test-key', + userId: 'alice', + messages: JSON.stringify([{ role: 'assistant', content: 'I will remember that.' }]), + } + + expect(buildBody(params)).toEqual({ + messages: [{ role: 'assistant', content: 'I will remember that.' }], + user_id: 'alice', + }) + }) + + it('extracts queued processing fields from v3 responses', async () => { + const result = await transformResponse( + new Response( + JSON.stringify({ + message: 'Memory processing has been queued for background execution', + status: 'PENDING', + event_id: 'evt-123', + }) + ) + ) + + expect(result).toEqual({ + success: true, + output: { + message: 'Memory processing has been queued for background execution', + status: 'PENDING', + event_id: 'evt-123', + }, + }) + }) +}) diff --git a/apps/sim/tools/mem0/add_memories.ts b/apps/sim/tools/mem0/add_memories.ts index 0616e5a2be5..ef05c17fb1f 100644 --- a/apps/sim/tools/mem0/add_memories.ts +++ b/apps/sim/tools/mem0/add_memories.ts @@ -1,11 +1,43 @@ -import { ADD_MEMORY_OUTPUT_PROPERTIES } from '@/tools/mem0/types' +import { toError } from '@sim/utils/errors' +import { + ADD_MEMORY_OUTPUT_PROPERTIES, + type Mem0AddMemoriesParams, + type Mem0AddMemoriesResponse, + type Mem0Message, +} from '@/tools/mem0/types' import type { ToolConfig } from '@/tools/types' +function parseMessages(messages: Mem0AddMemoriesParams['messages']): Mem0Message[] { + let parsed: unknown + try { + parsed = typeof messages === 'string' ? JSON.parse(messages) : messages + } catch (error) { + throw new Error(`Messages must be valid JSON: ${toError(error).message}`) + } + if (!Array.isArray(parsed) || parsed.length === 0) { + throw new Error('Messages must be a non-empty array') + } + + for (const message of parsed) { + if ( + !message || + typeof message !== 'object' || + (message.role !== 'user' && message.role !== 'assistant') || + typeof message.content !== 'string' || + message.content.length === 0 + ) { + throw new Error('Each message must have role user or assistant and non-empty content') + } + } + + return parsed +} + /** * Add Memories Tool * @see https://docs.mem0.ai/api-reference/memory/add-memories */ -export const mem0AddMemoriesTool: ToolConfig = { +export const mem0AddMemoriesTool: ToolConfig = { id: 'mem0_add_memories', name: 'Add Memories', description: 'Add memories to Mem0 for persistent storage and retrieval', @@ -34,107 +66,36 @@ export const mem0AddMemoriesTool: ToolConfig = { }, request: { - url: 'https://api.mem0.ai/v1/memories/', + url: 'https://api.mem0.ai/v3/memories/add/', method: 'POST', headers: (params) => ({ Authorization: `Token ${params.apiKey}`, 'Content-Type': 'application/json', }), body: (params) => { - // First, ensure messages is an array - let messagesArray = params.messages - if (typeof messagesArray === 'string') { - try { - messagesArray = JSON.parse(messagesArray) - } catch (_e) { - throw new Error('Messages must be a valid JSON array of objects with role and content') - } - } - - // Validate message format - if (!Array.isArray(messagesArray) || messagesArray.length === 0) { - throw new Error('Messages must be a non-empty array') - } - - for (const msg of messagesArray) { - if (!msg.role || !msg.content) { - throw new Error('Each message must have role and content properties') - } - } - - // Prepare request body - const body: Record = { - messages: messagesArray, - version: 'v2', - user_id: params.userId, + const messages = parseMessages(params.messages) + return { + messages, + user_id: params.userId.trim(), } - - return body }, }, - transformResponse: async (response) => { + transformResponse: async (response): Promise => { const data = await response.json() - - // If the API returns an empty array, this might be normal behavior on success - if (Array.isArray(data) && data.length === 0) { - return { - success: true, - output: { - memories: [], - }, - } - } - - // Handle array response with memory objects - if (Array.isArray(data) && data.length > 0) { - // Extract IDs for easy access - const memoryIds = data.map((memory) => memory.id) - - return { - success: true, - output: { - ids: memoryIds, - memories: data, - }, - } - } - - // Handle non-array responses (single memory object) - if (data && !Array.isArray(data) && data.id) { - return { - success: true, - output: { - ids: [data.id], - memories: [data], - }, - } - } - - // Default response format if none of the above match return { success: true, output: { - memories: Array.isArray(data) ? data : [data], + message: data.message ?? '', + status: data.status ?? '', + event_id: data.event_id ?? '', }, } }, outputs: { - ids: { - type: 'array', - description: 'Array of memory IDs that were created', - items: { - type: 'string', - }, - }, - memories: { - type: 'array', - description: 'Array of memory objects that were created', - items: { - type: 'object', - properties: ADD_MEMORY_OUTPUT_PROPERTIES, - }, - }, + message: ADD_MEMORY_OUTPUT_PROPERTIES.message, + status: ADD_MEMORY_OUTPUT_PROPERTIES.status, + event_id: ADD_MEMORY_OUTPUT_PROPERTIES.event_id, }, } diff --git a/apps/sim/tools/mem0/get_memories.test.ts b/apps/sim/tools/mem0/get_memories.test.ts new file mode 100644 index 00000000000..af4831cb7e2 --- /dev/null +++ b/apps/sim/tools/mem0/get_memories.test.ts @@ -0,0 +1,120 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { mem0GetMemoriesTool } from '@/tools/mem0/get_memories' + +interface Mem0GetParams { + apiKey: string + userId?: string + memoryId?: string + startDate?: string + endDate?: string + limit?: number +} + +describe('mem0GetMemoriesTool', () => { + const buildUrl = mem0GetMemoriesTool.request.url as (params: Mem0GetParams) => string + const buildMethod = mem0GetMemoriesTool.request.method as (params: Mem0GetParams) => string + const buildBody = mem0GetMemoriesTool.request.body! + const transformResponse = mem0GetMemoriesTool.transformResponse! + + it('uses scoped v3 list memories requests', () => { + const params = { + apiKey: 'test-key', + userId: 'user-123', + limit: 25, + } + + expect(buildUrl(params)).toBe('https://api.mem0.ai/v3/memories/') + expect(buildMethod(params)).toBe('POST') + expect(buildBody(params)).toEqual({ + filters: { + user_id: 'user-123', + }, + page: 1, + page_size: 25, + }) + }) + + it('keeps date filters inside the scoped filter object', () => { + const body = buildBody({ + apiKey: 'test-key', + userId: 'user-123', + startDate: '2026-01-01', + endDate: '2026-01-31', + }) + + expect(body).toEqual({ + filters: { + user_id: 'user-123', + created_at: { + gte: '2026-01-01', + lte: '2026-01-31', + }, + }, + page: 1, + page_size: 10, + }) + }) + + it('uses the single-memory endpoint for memoryId requests', () => { + const params = { + apiKey: 'test-key', + userId: 'user-123', + memoryId: 'mem/123', + } + + expect(buildUrl(params)).toBe('https://api.mem0.ai/v1/memories/mem%2F123/') + expect(buildMethod(params)).toBe('GET') + expect(buildBody(params)).toBeUndefined() + }) + + it('extracts memories from paginated v3 responses', async () => { + const result = await transformResponse( + new Response( + JSON.stringify({ + count: 2, + next: 'https://api.mem0.ai/v3/memories/?page=2&page_size=25', + previous: null, + results: [ + { id: 'mem-1', memory: 'First memory.', user_id: 'user-123' }, + { id: 'mem-2', memory: 'Second memory.', user_id: 'user-123' }, + ], + }) + ) + ) + + expect(result.output).toEqual({ + memories: [ + { id: 'mem-1', memory: 'First memory.', user_id: 'user-123' }, + { id: 'mem-2', memory: 'Second memory.', user_id: 'user-123' }, + ], + ids: ['mem-1', 'mem-2'], + count: 2, + next: 'https://api.mem0.ai/v3/memories/?page=2&page_size=25', + previous: null, + }) + }) + + it('extracts direct single memory responses without rewriting fields', async () => { + const result = await transformResponse( + new Response( + JSON.stringify({ + id: 'mem-1', + memory: 'Stored memory content.', + created_at: '2026-01-01T00:00:00Z', + }) + ) + ) + + expect(result.output.memories).toEqual([ + { + id: 'mem-1', + memory: 'Stored memory content.', + created_at: '2026-01-01T00:00:00Z', + }, + ]) + expect(result.output.ids).toEqual(['mem-1']) + }) +}) diff --git a/apps/sim/tools/mem0/get_memories.ts b/apps/sim/tools/mem0/get_memories.ts index c753a25fa02..c1cd6e9d59c 100644 --- a/apps/sim/tools/mem0/get_memories.ts +++ b/apps/sim/tools/mem0/get_memories.ts @@ -1,11 +1,28 @@ -import { MEMORY_OUTPUT_PROPERTIES } from '@/tools/mem0/types' +import { MEMORY_OUTPUT_PROPERTIES, type Mem0GetMemoriesParams } from '@/tools/mem0/types' import type { ToolConfig } from '@/tools/types' +type JsonRecord = Record + +const isRecord = (value: unknown): value is JsonRecord => + Boolean(value) && typeof value === 'object' && !Array.isArray(value) + +const getMemoriesFromResponse = (data: unknown): unknown[] => { + if (Array.isArray(data)) return data + if (!isRecord(data)) return [] + if (Array.isArray(data.results)) return data.results + if (isRecord(data.memory)) return [data.memory] + if (data.id) return [data] + return [] +} + +const getMemoryId = (memory: unknown): string | undefined => + isRecord(memory) && typeof memory.id === 'string' ? memory.id : undefined + /** * Get Memories Tool * @see https://docs.mem0.ai/api-reference/memory/get-memories */ -export const mem0GetMemoriesTool: ToolConfig = { +export const mem0GetMemoriesTool: ToolConfig = { id: 'mem0_get_memories', name: 'Get Memories', description: 'Retrieve memories from Mem0 by ID or filter criteria', @@ -52,73 +69,63 @@ export const mem0GetMemoriesTool: ToolConfig = { }, request: { - url: (params: Record) => { - // For a specific memory ID, use the get single memory endpoint - if (params.memoryId) { - // Dynamically set method to GET for memory ID requests - params.method = 'GET' - return `https://api.mem0.ai/v1/memories/${params.memoryId}/` + url: (params) => { + const memoryId = typeof params.memoryId === 'string' ? params.memoryId.trim() : undefined + if (memoryId) { + return `https://api.mem0.ai/v1/memories/${encodeURIComponent(memoryId)}/` } - // Otherwise use v2 memories endpoint with filters - return 'https://api.mem0.ai/v2/memories/' + return 'https://api.mem0.ai/v3/memories/' }, - method: 'POST', // Default to POST for filtering + method: (params) => + typeof params.memoryId === 'string' && params.memoryId.trim() ? 'GET' : 'POST', headers: (params) => ({ 'Content-Type': 'application/json', Authorization: `Token ${params.apiKey}`, }), - body: (params: Record) => { - // For specific memory ID, we'll use GET method instead and don't need a body - // But we still need to return an empty object to satisfy the type - if (params.memoryId) { - return {} + body: (params) => { + if (typeof params.memoryId === 'string' && params.memoryId.trim()) { + return undefined } - // Build filters array for AND condition - const andConditions = [] - - // Add user filter - andConditions.push({ user_id: params.userId }) - - // Add date range filter if provided + const filters: Record = { + user_id: params.userId?.trim(), + } if (params.startDate || params.endDate) { - const dateFilter: Record = {} - + const dateFilter: Record = {} if (params.startDate) { dateFilter.gte = params.startDate } - if (params.endDate) { dateFilter.lte = params.endDate } - - andConditions.push({ created_at: dateFilter }) + filters.created_at = dateFilter } - // Build final filters object - const body: Record = { + return { + filters, + page: 1, page_size: Number(params.limit || 10), } - - // Only add filters if we have any conditions - if (andConditions.length > 0) { - body.filters = { AND: andConditions } - } - - return body }, }, transformResponse: async (response: Response) => { const data = await response.json() - const memories = Array.isArray(data) ? data : [data] - const ids = memories.map((memory) => memory.id).filter(Boolean) + const memories = getMemoriesFromResponse(data) + const ids = memories.map(getMemoryId).filter((id): id is string => Boolean(id)) return { success: true, output: { memories, ids, + ...(isRecord(data) && typeof data.count === 'number' ? { count: data.count } : {}), + ...(isRecord(data) && (typeof data.next === 'string' || data.next === null) + ? { next: data.next } + : {}), + ...(isRecord(data) && (typeof data.previous === 'string' || data.previous === null) + ? { previous: data.previous } + : {}), }, } }, @@ -139,5 +146,20 @@ export const mem0GetMemoriesTool: ToolConfig = { type: 'string', }, }, + count: { + type: 'number', + description: 'Total number of memories matching the filters', + optional: true, + }, + next: { + type: 'string', + description: 'URL for the next page of results', + optional: true, + }, + previous: { + type: 'string', + description: 'URL for the previous page of results', + optional: true, + }, }, } diff --git a/apps/sim/tools/mem0/index.ts b/apps/sim/tools/mem0/index.ts index 01b33170816..c3c73532b4d 100644 --- a/apps/sim/tools/mem0/index.ts +++ b/apps/sim/tools/mem0/index.ts @@ -3,3 +3,4 @@ import { mem0GetMemoriesTool } from '@/tools/mem0/get_memories' import { mem0SearchMemoriesTool } from '@/tools/mem0/search_memories' export { mem0AddMemoriesTool, mem0SearchMemoriesTool, mem0GetMemoriesTool } +export * from '@/tools/mem0/types' diff --git a/apps/sim/tools/mem0/search_memories.test.ts b/apps/sim/tools/mem0/search_memories.test.ts new file mode 100644 index 00000000000..a7fec78dc08 --- /dev/null +++ b/apps/sim/tools/mem0/search_memories.test.ts @@ -0,0 +1,72 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { mem0SearchMemoriesTool } from '@/tools/mem0/search_memories' + +describe('mem0SearchMemoriesTool', () => { + const buildBody = mem0SearchMemoriesTool.request.body! + const transformResponse = mem0SearchMemoriesTool.transformResponse! + + it('uses the v3 search endpoint', () => { + expect(mem0SearchMemoriesTool.request.url).toBe('https://api.mem0.ai/v3/memories/search/') + expect(mem0SearchMemoriesTool.request.method).toBe('POST') + }) + + it('builds the documented search request body', () => { + const body = buildBody({ + apiKey: 'test-key', + userId: ' alice ', + query: 'where does the user live?', + limit: 20, + }) + + expect(body).toEqual({ + query: 'where does the user live?', + filters: { + user_id: 'alice', + }, + top_k: 20, + }) + }) + + it('extracts results from v3 response envelopes', async () => { + const result = await transformResponse( + new Response( + JSON.stringify({ + results: [ + { + id: 'mem-1', + memory: 'User lives in San Francisco.', + user_id: 'alice', + categories: ['location'], + score: 0.82, + created_at: '2026-01-15T10:30:00Z', + updated_at: '2026-01-15T10:30:00Z', + }, + ], + }) + ) + ) + + expect(result.output).toEqual({ + searchResults: [ + { + id: 'mem-1', + memory: 'User lives in San Francisco.', + user_id: 'alice', + agent_id: undefined, + app_id: undefined, + run_id: undefined, + hash: undefined, + metadata: undefined, + categories: ['location'], + created_at: '2026-01-15T10:30:00Z', + updated_at: '2026-01-15T10:30:00Z', + score: 0.82, + }, + ], + ids: ['mem-1'], + }) + }) +}) diff --git a/apps/sim/tools/mem0/search_memories.ts b/apps/sim/tools/mem0/search_memories.ts index c7fc4d3d7dd..1d3d24068fe 100644 --- a/apps/sim/tools/mem0/search_memories.ts +++ b/apps/sim/tools/mem0/search_memories.ts @@ -1,12 +1,33 @@ -import type { Mem0Response } from '@/tools/mem0/types' +import type { Mem0Response, Mem0SearchMemoriesParams } from '@/tools/mem0/types' import { SEARCH_RESULT_OUTPUT_PROPERTIES } from '@/tools/mem0/types' import type { ToolConfig } from '@/tools/types' +type JsonRecord = Record + +const isRecord = (value: unknown): value is JsonRecord => + Boolean(value) && typeof value === 'object' && !Array.isArray(value) + +const getSearchResults = (data: unknown): JsonRecord[] => { + if (!isRecord(data) || !Array.isArray(data.results)) return [] + return data.results.filter(isRecord) +} + +const getString = (value: unknown): string | undefined => + typeof value === 'string' ? value : undefined + +const getStringArray = (value: unknown): string[] | undefined => + Array.isArray(value) + ? value.filter((item): item is string => typeof item === 'string') + : undefined + +const getNumber = (value: unknown, fallback = 0): number => + typeof value === 'number' ? value : fallback + /** * Search Memories Tool * @see https://docs.mem0.ai/api-reference/memory/search-memories */ -export const mem0SearchMemoriesTool: ToolConfig = { +export const mem0SearchMemoriesTool: ToolConfig = { id: 'mem0_search_memories', name: 'Search Memories', description: 'Search for memories in Mem0 using semantic search', @@ -41,71 +62,46 @@ export const mem0SearchMemoriesTool: ToolConfig = { }, request: { - url: 'https://api.mem0.ai/v2/memories/search/', + url: 'https://api.mem0.ai/v3/memories/search/', method: 'POST', headers: (params) => ({ 'Content-Type': 'application/json', Authorization: `Token ${params.apiKey}`, }), body: (params) => { - // Create the request body with the format that the curl test confirms works - const body: Record = { - query: params.query || 'test', + return { + query: params.query, filters: { - user_id: params.userId, + user_id: params.userId.trim(), }, top_k: Number(params.limit || 10), } - - return body }, }, transformResponse: async (response): Promise => { const data = await response.json() - - if (!data || (Array.isArray(data) && data.length === 0)) { - return { - success: true, - output: { - searchResults: [], - ids: [], - }, - } - } - - if (Array.isArray(data)) { - const searchResults = data.map((item) => ({ - id: item.id, - memory: item.memory || '', - user_id: item.user_id, - agent_id: item.agent_id, - app_id: item.app_id, - run_id: item.run_id, - hash: item.hash, - metadata: item.metadata, - categories: item.categories, - created_at: item.created_at, - updated_at: item.updated_at, - score: item.score || 0, - })) - - const ids = data.map((item) => item.id).filter(Boolean) - - return { - success: true, - output: { - searchResults, - ids, - }, - } - } + const searchResults = getSearchResults(data).map((result) => ({ + id: getString(result.id) ?? '', + memory: getString(result.memory) ?? '', + user_id: getString(result.user_id), + agent_id: getString(result.agent_id), + app_id: getString(result.app_id), + run_id: getString(result.run_id), + hash: getString(result.hash), + metadata: isRecord(result.metadata) ? result.metadata : undefined, + categories: getStringArray(result.categories), + created_at: getString(result.created_at), + updated_at: getString(result.updated_at), + score: getNumber(result.score), + })) + const ids = searchResults.map((result) => result.id).filter(Boolean) return { success: true, output: { - searchResults: [], - ids: [], + searchResults, + ids, }, } }, diff --git a/apps/sim/tools/mem0/types.ts b/apps/sim/tools/mem0/types.ts index 156926d65a4..446cd8a8f25 100644 --- a/apps/sim/tools/mem0/types.ts +++ b/apps/sim/tools/mem0/types.ts @@ -1,5 +1,40 @@ import type { OutputProperty, ToolResponse } from '@/tools/types' +export interface Mem0Message { + role: 'user' | 'assistant' + content: string +} + +export interface Mem0AddMemoriesParams { + userId: string + messages: Mem0Message[] | string + apiKey: string +} + +export interface Mem0SearchMemoriesParams { + userId: string + query: string + limit?: number + apiKey: string +} + +export interface Mem0GetMemoriesParams { + userId?: string + memoryId?: string + startDate?: string + endDate?: string + limit?: number + apiKey: string +} + +export interface Mem0AddMemoriesResponse extends ToolResponse { + output: { + message: string + status: string + event_id: string + } +} + /** * Shared output property definitions for Mem0 API responses. * Based on official Mem0 REST API documentation. @@ -7,21 +42,18 @@ import type { OutputProperty, ToolResponse } from '@/tools/types' */ /** - * Output definition for memory objects returned by add operations. - * Add responses include event type indicating what operation was performed. + * Output definition for queued add-memory operations. * @see https://docs.mem0.ai/api-reference/memory/add-memories */ export const ADD_MEMORY_OUTPUT_PROPERTIES = { - id: { type: 'string', description: 'Unique identifier for the memory' }, - memory: { type: 'string', description: 'The content of the memory' }, - event: { + message: { type: 'string', description: 'Status message for the queued memory processing job' }, + status: { type: 'string', - description: 'Event type indicating operation performed (ADD, UPDATE, DELETE, NOOP)', + description: 'Processing status returned by Mem0', }, - metadata: { - type: 'json', - description: 'Custom metadata associated with the memory', - optional: true, + event_id: { + type: 'string', + description: 'Event ID for polling memory processing status', }, } as const satisfies Record @@ -30,7 +62,7 @@ export const ADD_MEMORY_OUTPUT_PROPERTIES = { */ export const ADD_MEMORY_OUTPUT: OutputProperty = { type: 'object', - description: 'Memory object returned from add operation with event type', + description: 'Queued memory processing job returned from add operation', properties: ADD_MEMORY_OUTPUT_PROPERTIES, } @@ -66,18 +98,6 @@ export const MEMORY_OUTPUT_PROPERTIES = { type: 'string', description: 'ISO 8601 timestamp when the memory was last updated', }, - owner: { type: 'string', description: 'Owner of the memory', optional: true }, - organization: { - type: 'string', - description: 'Organization associated with the memory', - optional: true, - }, - immutable: { type: 'boolean', description: 'Whether the memory can be modified', optional: true }, - expiration_date: { - type: 'string', - description: 'Expiration date after which memory is not retrieved', - optional: true, - }, } as const satisfies Record /** @@ -139,7 +159,6 @@ export interface Mem0Response extends ToolResponse { memories?: Array<{ id: string memory: string - event?: string user_id?: string agent_id?: string app_id?: string @@ -149,11 +168,10 @@ export interface Mem0Response extends ToolResponse { categories?: string[] created_at?: string updated_at?: string - owner?: string - organization?: string - immutable?: boolean - expiration_date?: string }> + count?: number + next?: string | null + previous?: string | null searchResults?: Array<{ id: string memory: string diff --git a/apps/sim/tools/memory/get.test.ts b/apps/sim/tools/memory/get.test.ts new file mode 100644 index 00000000000..0b7177d19d6 --- /dev/null +++ b/apps/sim/tools/memory/get.test.ts @@ -0,0 +1,65 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { memoryGetTool } from '@/tools/memory/get' + +interface MemoryGetParams { + _context?: { + workspaceId?: string + } + conversationId?: string + id?: string +} + +describe('memoryGetTool', () => { + const buildUrl = memoryGetTool.request.url as (params: MemoryGetParams) => string + const transformResponse = memoryGetTool.transformResponse! + + it('builds an exact memory lookup URL', () => { + const url = buildUrl({ + _context: { workspaceId: 'workspace-1' }, + conversationId: 'user-123', + }) + + expect(url).toBe('/api/memory/user-123?workspaceId=workspace-1') + expect(url).not.toContain('query=') + expect(url).not.toContain('limit=') + }) + + it('encodes legacy id values in the path', () => { + const url = buildUrl({ + _context: { workspaceId: 'workspace-1' }, + id: 'team/user 123', + }) + + expect(url).toBe('/api/memory/team%2Fuser%20123?workspaceId=workspace-1') + }) + + it('wraps the exact memory response as a single result', async () => { + const result = await transformResponse( + new Response( + JSON.stringify({ + success: true, + data: { + conversationId: 'user-123', + data: [{ role: 'user', content: 'Remember this' }], + }, + }) + ) + ) + + expect(result).toEqual({ + success: true, + output: { + memories: [ + { + conversationId: 'user-123', + data: [{ role: 'user', content: 'Remember this' }], + }, + ], + message: 'Found 1 memory', + }, + }) + }) +}) diff --git a/apps/sim/tools/memory/get.ts b/apps/sim/tools/memory/get.ts index 27125637613..a9eed715403 100644 --- a/apps/sim/tools/memory/get.ts +++ b/apps/sim/tools/memory/get.ts @@ -35,12 +35,8 @@ export const memoryGetTool: ToolConfig = { if (!conversationId) { throw new Error('conversationId or id is required') } - const query = conversationId - - const url = new URL('/api/memory', 'http://dummy') + const url = new URL(`/api/memory/${encodeURIComponent(conversationId)}`, 'http://dummy') url.searchParams.set('workspaceId', workspaceId) - url.searchParams.set('query', query) - url.searchParams.set('limit', '1000') return url.pathname + url.search }, @@ -52,9 +48,9 @@ export const memoryGetTool: ToolConfig = { transformResponse: async (response): Promise => { const result = await response.json() - const memories = result.data?.memories || [] + const memory = result.data - if (!Array.isArray(memories) || memories.length === 0) { + if (!memory) { return { success: true, output: { @@ -67,8 +63,8 @@ export const memoryGetTool: ToolConfig = { return { success: true, output: { - memories, - message: `Found ${memories.length} memories`, + memories: [memory], + message: 'Found 1 memory', }, } }, From 080ae8b347230363a13f2ffc3cadca2539117979 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 2 May 2026 18:18:31 -0700 Subject: [PATCH 2/5] timeout test --- apps/sim/lib/copilot/vfs/file-reader.test.ts | 22 +++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/apps/sim/lib/copilot/vfs/file-reader.test.ts b/apps/sim/lib/copilot/vfs/file-reader.test.ts index 1e202d77d5f..7471a0372b7 100644 --- a/apps/sim/lib/copilot/vfs/file-reader.test.ts +++ b/apps/sim/lib/copilot/vfs/file-reader.test.ts @@ -18,6 +18,10 @@ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ import { readFileRecord } from '@/lib/copilot/vfs/file-reader' const MAX_IMAGE_READ_BYTES = 5 * 1024 * 1024 +const TINY_PNG = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADUlEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', + 'base64' +) async function makeNoisePng(width: number, height: number): Promise { const sharp = (await import('sharp')).default @@ -30,19 +34,7 @@ async function makeNoisePng(width: number, height: number): Promise { describe('readFileRecord', () => { it('returns small images as attachments without resize note', async () => { - const sharp = (await import('sharp')).default - const smallPng = await sharp({ - create: { - width: 200, - height: 200, - channels: 3, - background: { r: 255, g: 0, b: 0 }, - }, - }) - .png() - .toBuffer() - - downloadWorkspaceFile.mockResolvedValue(smallPng) + downloadWorkspaceFile.mockResolvedValue(TINY_PNG) const result = await readFileRecord({ id: 'wf_small', @@ -50,7 +42,7 @@ describe('readFileRecord', () => { name: 'small.png', key: 'uploads/small.png', path: '/api/files/serve/uploads%2Fsmall.png?context=mothership', - size: smallPng.length, + size: TINY_PNG.length, type: 'image/png', uploadedBy: 'user_1', uploadedAt: new Date(), @@ -61,7 +53,7 @@ describe('readFileRecord', () => { expect(result?.attachment?.type).toBe('image') expect(result?.attachment?.source.media_type).toBe('image/png') expect(result?.content).not.toContain('resized for vision') - expect(Buffer.from(result?.attachment?.source.data ?? '', 'base64')).toEqual(smallPng) + expect(Buffer.from(result?.attachment?.source.data ?? '', 'base64')).toEqual(TINY_PNG) }) it('downscales oversized images into attachments that fit the read limit', async () => { From e0c04c27acf9b46fbe18e57ae4e822f1da362c9a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 2 May 2026 18:26:30 -0700 Subject: [PATCH 3/5] address comments --- apps/sim/app/api/memory/[id]/route.ts | 40 ++++++++++++++-- apps/sim/app/api/memory/route.ts | 8 +++- apps/sim/blocks/blocks/mem0.test.ts | 17 +++++++ apps/sim/blocks/blocks/mem0.ts | 60 ++++++++---------------- apps/sim/tools/mem0/add_memories.test.ts | 10 ++++ apps/sim/tools/mem0/add_memories.ts | 31 +----------- apps/sim/tools/mem0/get_memories.test.ts | 4 +- apps/sim/tools/mem0/get_memories.ts | 9 +++- apps/sim/tools/mem0/index.ts | 1 + apps/sim/tools/mem0/types.ts | 1 + apps/sim/tools/mem0/utils.ts | 37 +++++++++++++++ 11 files changed, 140 insertions(+), 78 deletions(-) create mode 100644 apps/sim/tools/mem0/utils.ts diff --git a/apps/sim/app/api/memory/[id]/route.ts b/apps/sim/app/api/memory/[id]/route.ts index 6853182cfcf..2928fd8b42c 100644 --- a/apps/sim/app/api/memory/[id]/route.ts +++ b/apps/sim/app/api/memory/[id]/route.ts @@ -131,7 +131,13 @@ export const DELETE = withRouteHandler( const existingMemory = await db .select({ id: memory.id }) .from(memory) - .where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId))) + .where( + and( + eq(memory.key, id), + eq(memory.workspaceId, validatedWorkspaceId), + isNull(memory.deletedAt) + ) + ) .limit(1) if (existingMemory.length === 0) { @@ -140,7 +146,13 @@ export const DELETE = withRouteHandler( await db .delete(memory) - .where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId))) + .where( + and( + eq(memory.key, id), + eq(memory.workspaceId, validatedWorkspaceId), + isNull(memory.deletedAt) + ) + ) logger.info(`[${requestId}] Memory deleted: ${id} for workspace: ${validatedWorkspaceId}`) return NextResponse.json( @@ -183,7 +195,13 @@ export const PUT = withRouteHandler(async (request: NextRequest, context: Memory const existingMemories = await db .select() .from(memory) - .where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId))) + .where( + and( + eq(memory.key, id), + eq(memory.workspaceId, validatedWorkspaceId), + isNull(memory.deletedAt) + ) + ) .limit(1) if (existingMemories.length === 0) { @@ -202,12 +220,24 @@ export const PUT = withRouteHandler(async (request: NextRequest, context: Memory await db .update(memory) .set({ data: validatedData, updatedAt: now }) - .where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId))) + .where( + and( + eq(memory.key, id), + eq(memory.workspaceId, validatedWorkspaceId), + isNull(memory.deletedAt) + ) + ) const updatedMemories = await db .select() .from(memory) - .where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId))) + .where( + and( + eq(memory.key, id), + eq(memory.workspaceId, validatedWorkspaceId), + isNull(memory.deletedAt) + ) + ) .limit(1) const mem = updatedMemories[0] diff --git a/apps/sim/app/api/memory/route.ts b/apps/sim/app/api/memory/route.ts index 4ad47e108b2..53b9340f3c6 100644 --- a/apps/sim/app/api/memory/route.ts +++ b/apps/sim/app/api/memory/route.ts @@ -293,7 +293,13 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => { const result = await db .delete(memory) - .where(and(eq(memory.key, conversationId), eq(memory.workspaceId, workspaceId))) + .where( + and( + eq(memory.key, conversationId), + eq(memory.workspaceId, workspaceId), + isNull(memory.deletedAt) + ) + ) .returning({ id: memory.id }) const deletedCount = result.length diff --git a/apps/sim/blocks/blocks/mem0.test.ts b/apps/sim/blocks/blocks/mem0.test.ts index 3e8b0cdd41d..c468054c338 100644 --- a/apps/sim/blocks/blocks/mem0.test.ts +++ b/apps/sim/blocks/blocks/mem0.test.ts @@ -32,4 +32,21 @@ describe('Mem0Block', () => { }) ).toThrow('Each message must have role user or assistant and non-empty content') }) + + it('passes pagination params for get operations', () => { + const params = buildParams({ + operation: 'get', + apiKey: 'test-key', + userId: 'alice', + page: '2', + limit: '25', + }) + + expect(params).toEqual({ + apiKey: 'test-key', + userId: 'alice', + page: 2, + limit: 25, + }) + }) }) diff --git a/apps/sim/blocks/blocks/mem0.ts b/apps/sim/blocks/blocks/mem0.ts index e003408422f..8aa9faf9080 100644 --- a/apps/sim/blocks/blocks/mem0.ts +++ b/apps/sim/blocks/blocks/mem0.ts @@ -1,46 +1,8 @@ import { toError } from '@sim/utils/errors' import { Mem0Icon } from '@/components/icons' import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types' -import type { Mem0Message, Mem0Response } from '@/tools/mem0/types' - -function isMem0Message(value: unknown): value is Mem0Message { - return ( - Boolean(value) && - typeof value === 'object' && - 'role' in value && - 'content' in value && - (value.role === 'user' || value.role === 'assistant') && - typeof value.content === 'string' && - value.content.length > 0 - ) -} - -function parseMem0Messages(value: unknown): Mem0Message[] { - if (!value) { - throw new Error('Messages are required for add operation') - } - - let messages: unknown - try { - messages = typeof value === 'string' ? JSON.parse(value) : value - } catch (error) { - throw new Error(`Messages must be valid JSON: ${toError(error).message}`) - } - - if (!Array.isArray(messages) || messages.length === 0) { - throw new Error('Messages must be a non-empty array') - } - - const validMessages: Mem0Message[] = [] - for (const message of messages) { - if (!isMem0Message(message)) { - throw new Error('Each message must have role user or assistant and non-empty content') - } - validMessages.push(message) - } - - return validMessages -} +import type { Mem0Response } from '@/tools/mem0/types' +import { parseMem0Messages } from '@/tools/mem0/utils' export const Mem0Block: BlockConfig = { type: 'mem0', @@ -163,6 +125,17 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n password: true, required: true, }, + { + id: 'page', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { + field: 'operation', + value: 'get', + }, + mode: 'advanced', + }, { id: 'limit', title: 'Result Limit', @@ -228,7 +201,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n if (params.userId) result.userId = params.userId - if (params.limit) result.limit = params.limit + if (params.limit) result.limit = Number(params.limit) switch (operation) { case 'add': @@ -260,6 +233,10 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n result.memoryId = params.memoryId } + if (params.page) { + result.page = Number(params.page) + } + if (params.startDate) { result.startDate = params.startDate } @@ -283,6 +260,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n memoryId: { type: 'string', description: 'Memory identifier' }, startDate: { type: 'string', description: 'Start date filter' }, endDate: { type: 'string', description: 'End date filter' }, + page: { type: 'number', description: 'Page number for paginated get results' }, limit: { type: 'number', description: 'Result limit' }, }, outputs: { diff --git a/apps/sim/tools/mem0/add_memories.test.ts b/apps/sim/tools/mem0/add_memories.test.ts index 8d1192bbe4b..372669977c5 100644 --- a/apps/sim/tools/mem0/add_memories.test.ts +++ b/apps/sim/tools/mem0/add_memories.test.ts @@ -40,6 +40,16 @@ describe('mem0AddMemoriesTool', () => { }) }) + it('rejects unsupported message roles before building the request body', () => { + expect(() => + buildBody({ + apiKey: 'test-key', + userId: 'alice', + messages: JSON.stringify([{ role: 'system', content: 'Remember this.' }]), + }) + ).toThrow('Each message must have role user or assistant and non-empty content') + }) + it('extracts queued processing fields from v3 responses', async () => { const result = await transformResponse( new Response( diff --git a/apps/sim/tools/mem0/add_memories.ts b/apps/sim/tools/mem0/add_memories.ts index ef05c17fb1f..fc7ca732026 100644 --- a/apps/sim/tools/mem0/add_memories.ts +++ b/apps/sim/tools/mem0/add_memories.ts @@ -1,38 +1,11 @@ -import { toError } from '@sim/utils/errors' import { ADD_MEMORY_OUTPUT_PROPERTIES, type Mem0AddMemoriesParams, type Mem0AddMemoriesResponse, - type Mem0Message, } from '@/tools/mem0/types' +import { parseMem0Messages } from '@/tools/mem0/utils' import type { ToolConfig } from '@/tools/types' -function parseMessages(messages: Mem0AddMemoriesParams['messages']): Mem0Message[] { - let parsed: unknown - try { - parsed = typeof messages === 'string' ? JSON.parse(messages) : messages - } catch (error) { - throw new Error(`Messages must be valid JSON: ${toError(error).message}`) - } - if (!Array.isArray(parsed) || parsed.length === 0) { - throw new Error('Messages must be a non-empty array') - } - - for (const message of parsed) { - if ( - !message || - typeof message !== 'object' || - (message.role !== 'user' && message.role !== 'assistant') || - typeof message.content !== 'string' || - message.content.length === 0 - ) { - throw new Error('Each message must have role user or assistant and non-empty content') - } - } - - return parsed -} - /** * Add Memories Tool * @see https://docs.mem0.ai/api-reference/memory/add-memories @@ -73,7 +46,7 @@ export const mem0AddMemoriesTool: ToolConfig { - const messages = parseMessages(params.messages) + const messages = parseMem0Messages(params.messages) return { messages, user_id: params.userId.trim(), diff --git a/apps/sim/tools/mem0/get_memories.test.ts b/apps/sim/tools/mem0/get_memories.test.ts index af4831cb7e2..26451a71d32 100644 --- a/apps/sim/tools/mem0/get_memories.test.ts +++ b/apps/sim/tools/mem0/get_memories.test.ts @@ -10,6 +10,7 @@ interface Mem0GetParams { memoryId?: string startDate?: string endDate?: string + page?: number limit?: number } @@ -23,6 +24,7 @@ describe('mem0GetMemoriesTool', () => { const params = { apiKey: 'test-key', userId: 'user-123', + page: 3, limit: 25, } @@ -32,7 +34,7 @@ describe('mem0GetMemoriesTool', () => { filters: { user_id: 'user-123', }, - page: 1, + page: 3, page_size: 25, }) }) diff --git a/apps/sim/tools/mem0/get_memories.ts b/apps/sim/tools/mem0/get_memories.ts index c1cd6e9d59c..83b47928c27 100644 --- a/apps/sim/tools/mem0/get_memories.ts +++ b/apps/sim/tools/mem0/get_memories.ts @@ -60,6 +60,13 @@ export const mem0GetMemoriesTool: ToolConfig = { visibility: 'user-or-llm', description: 'Maximum number of results to return (e.g., 10, 50, 100)', }, + page: { + type: 'number', + required: false, + default: 1, + visibility: 'user-or-llm', + description: 'Page number to retrieve for paginated list results', + }, apiKey: { type: 'string', required: true, @@ -103,7 +110,7 @@ export const mem0GetMemoriesTool: ToolConfig = { return { filters, - page: 1, + page: Number(params.page ?? 1), page_size: Number(params.limit || 10), } }, diff --git a/apps/sim/tools/mem0/index.ts b/apps/sim/tools/mem0/index.ts index c3c73532b4d..ba80251221c 100644 --- a/apps/sim/tools/mem0/index.ts +++ b/apps/sim/tools/mem0/index.ts @@ -4,3 +4,4 @@ import { mem0SearchMemoriesTool } from '@/tools/mem0/search_memories' export { mem0AddMemoriesTool, mem0SearchMemoriesTool, mem0GetMemoriesTool } export * from '@/tools/mem0/types' +export * from '@/tools/mem0/utils' diff --git a/apps/sim/tools/mem0/types.ts b/apps/sim/tools/mem0/types.ts index 446cd8a8f25..63f18988df9 100644 --- a/apps/sim/tools/mem0/types.ts +++ b/apps/sim/tools/mem0/types.ts @@ -23,6 +23,7 @@ export interface Mem0GetMemoriesParams { memoryId?: string startDate?: string endDate?: string + page?: number limit?: number apiKey: string } diff --git a/apps/sim/tools/mem0/utils.ts b/apps/sim/tools/mem0/utils.ts new file mode 100644 index 00000000000..b46bd3467a0 --- /dev/null +++ b/apps/sim/tools/mem0/utils.ts @@ -0,0 +1,37 @@ +import { toError } from '@sim/utils/errors' +import type { Mem0Message } from '@/tools/mem0/types' + +function isMem0Message(value: unknown): value is Mem0Message { + return ( + value !== null && + typeof value === 'object' && + 'role' in value && + 'content' in value && + (value.role === 'user' || value.role === 'assistant') && + typeof value.content === 'string' && + value.content.length > 0 + ) +} + +export function parseMem0Messages(value: unknown): Mem0Message[] { + let messages: unknown + try { + messages = typeof value === 'string' ? JSON.parse(value) : value + } catch (error) { + throw new Error(`Messages must be valid JSON: ${toError(error).message}`) + } + + if (!Array.isArray(messages) || messages.length === 0) { + throw new Error('Messages must be a non-empty array') + } + + const validMessages: Mem0Message[] = [] + for (const message of messages) { + if (!isMem0Message(message)) { + throw new Error('Each message must have role user or assistant and non-empty content') + } + validMessages.push(message) + } + + return validMessages +} From b1f31a719cb89e39c9b77846f3a1b46059a70dc5 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 2 May 2026 18:36:40 -0700 Subject: [PATCH 4/5] address more comments --- apps/sim/blocks/blocks/mem0.ts | 3 --- apps/sim/tools/mem0/get_memories.ts | 6 +----- apps/sim/tools/mem0/search_memories.ts | 6 +----- apps/sim/tools/mem0/utils.ts | 5 +++++ 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/apps/sim/blocks/blocks/mem0.ts b/apps/sim/blocks/blocks/mem0.ts index 8aa9faf9080..ce54ad31298 100644 --- a/apps/sim/blocks/blocks/mem0.ts +++ b/apps/sim/blocks/blocks/mem0.ts @@ -224,9 +224,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n throw new Error('Mem0 Block Error: Search requires a query parameter') } - if (params.limit) { - result.limit = Number(params.limit) - } break case 'get': if (params.memoryId) { diff --git a/apps/sim/tools/mem0/get_memories.ts b/apps/sim/tools/mem0/get_memories.ts index 83b47928c27..74d2addcc37 100644 --- a/apps/sim/tools/mem0/get_memories.ts +++ b/apps/sim/tools/mem0/get_memories.ts @@ -1,11 +1,7 @@ import { MEMORY_OUTPUT_PROPERTIES, type Mem0GetMemoriesParams } from '@/tools/mem0/types' +import { isRecord } from '@/tools/mem0/utils' import type { ToolConfig } from '@/tools/types' -type JsonRecord = Record - -const isRecord = (value: unknown): value is JsonRecord => - Boolean(value) && typeof value === 'object' && !Array.isArray(value) - const getMemoriesFromResponse = (data: unknown): unknown[] => { if (Array.isArray(data)) return data if (!isRecord(data)) return [] diff --git a/apps/sim/tools/mem0/search_memories.ts b/apps/sim/tools/mem0/search_memories.ts index 1d3d24068fe..5f8dde791f4 100644 --- a/apps/sim/tools/mem0/search_memories.ts +++ b/apps/sim/tools/mem0/search_memories.ts @@ -1,12 +1,8 @@ import type { Mem0Response, Mem0SearchMemoriesParams } from '@/tools/mem0/types' import { SEARCH_RESULT_OUTPUT_PROPERTIES } from '@/tools/mem0/types' +import { isRecord, type JsonRecord } from '@/tools/mem0/utils' import type { ToolConfig } from '@/tools/types' -type JsonRecord = Record - -const isRecord = (value: unknown): value is JsonRecord => - Boolean(value) && typeof value === 'object' && !Array.isArray(value) - const getSearchResults = (data: unknown): JsonRecord[] => { if (!isRecord(data) || !Array.isArray(data.results)) return [] return data.results.filter(isRecord) diff --git a/apps/sim/tools/mem0/utils.ts b/apps/sim/tools/mem0/utils.ts index b46bd3467a0..8c6d7cedac9 100644 --- a/apps/sim/tools/mem0/utils.ts +++ b/apps/sim/tools/mem0/utils.ts @@ -1,6 +1,11 @@ import { toError } from '@sim/utils/errors' import type { Mem0Message } from '@/tools/mem0/types' +export type JsonRecord = Record + +export const isRecord = (value: unknown): value is JsonRecord => + value !== null && typeof value === 'object' && !Array.isArray(value) + function isMem0Message(value: unknown): value is Mem0Message { return ( value !== null && From 21ef4aed57d3f219bca170a763c5af0141c96cab Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 2 May 2026 18:39:58 -0700 Subject: [PATCH 5/5] remove flaky test --- apps/sim/lib/copilot/vfs/file-reader.test.ts | 27 -------------------- 1 file changed, 27 deletions(-) diff --git a/apps/sim/lib/copilot/vfs/file-reader.test.ts b/apps/sim/lib/copilot/vfs/file-reader.test.ts index 10494be2b38..115ad959496 100644 --- a/apps/sim/lib/copilot/vfs/file-reader.test.ts +++ b/apps/sim/lib/copilot/vfs/file-reader.test.ts @@ -18,10 +18,6 @@ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ import { readFileRecord } from '@/lib/copilot/vfs/file-reader' const MAX_IMAGE_READ_BYTES = 5 * 1024 * 1024 -const TINY_PNG = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADUlEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', - 'base64' -) async function makeNoisePng(width: number, height: number): Promise { const sharp = (await import('sharp')).default @@ -35,29 +31,6 @@ async function makeNoisePng(width: number, height: number): Promise { const SHARP_TEST_TIMEOUT_MS = 30_000 describe('readFileRecord', () => { - it('returns small images as attachments without resize note', async () => { - downloadWorkspaceFile.mockResolvedValue(TINY_PNG) - - const result = await readFileRecord({ - id: 'wf_small', - workspaceId: 'ws_1', - name: 'small.png', - key: 'uploads/small.png', - path: '/api/files/serve/uploads%2Fsmall.png?context=mothership', - size: TINY_PNG.length, - type: 'image/png', - uploadedBy: 'user_1', - uploadedAt: new Date(), - deletedAt: null, - storageContext: 'mothership', - }) - - expect(result?.attachment?.type).toBe('image') - expect(result?.attachment?.source.media_type).toBe('image/png') - expect(result?.content).not.toContain('resized for vision') - expect(Buffer.from(result?.attachment?.source.data ?? '', 'base64')).toEqual(TINY_PNG) - }) - it( 'downscales oversized images into attachments that fit the read limit', async () => {