Skip to content

Commit 7921449

Browse files
fix(memories): get memory tool, mem0 integration update (#4415)
* fix(memories): get memory tool, mem0 integration update * timeout test * address comments * address more comments * remove flaky test
1 parent 71ebe81 commit 7921449

16 files changed

Lines changed: 691 additions & 334 deletions

File tree

apps/sim/app/api/memory/[id]/route.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
22
import { memory } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { and, eq } from 'drizzle-orm'
4+
import { and, eq, isNull } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import {
77
agentMemoryDataSchemaContract,
@@ -75,7 +75,13 @@ export const GET = withRouteHandler(async (request: NextRequest, context: Memory
7575
const memories = await db
7676
.select()
7777
.from(memory)
78-
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
78+
.where(
79+
and(
80+
eq(memory.key, id),
81+
eq(memory.workspaceId, validatedWorkspaceId),
82+
isNull(memory.deletedAt)
83+
)
84+
)
7985
.orderBy(memory.createdAt)
8086
.limit(1)
8187

@@ -125,7 +131,13 @@ export const DELETE = withRouteHandler(
125131
const existingMemory = await db
126132
.select({ id: memory.id })
127133
.from(memory)
128-
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
134+
.where(
135+
and(
136+
eq(memory.key, id),
137+
eq(memory.workspaceId, validatedWorkspaceId),
138+
isNull(memory.deletedAt)
139+
)
140+
)
129141
.limit(1)
130142

131143
if (existingMemory.length === 0) {
@@ -134,7 +146,13 @@ export const DELETE = withRouteHandler(
134146

135147
await db
136148
.delete(memory)
137-
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
149+
.where(
150+
and(
151+
eq(memory.key, id),
152+
eq(memory.workspaceId, validatedWorkspaceId),
153+
isNull(memory.deletedAt)
154+
)
155+
)
138156

139157
logger.info(`[${requestId}] Memory deleted: ${id} for workspace: ${validatedWorkspaceId}`)
140158
return NextResponse.json(
@@ -177,7 +195,13 @@ export const PUT = withRouteHandler(async (request: NextRequest, context: Memory
177195
const existingMemories = await db
178196
.select()
179197
.from(memory)
180-
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
198+
.where(
199+
and(
200+
eq(memory.key, id),
201+
eq(memory.workspaceId, validatedWorkspaceId),
202+
isNull(memory.deletedAt)
203+
)
204+
)
181205
.limit(1)
182206

183207
if (existingMemories.length === 0) {
@@ -196,12 +220,24 @@ export const PUT = withRouteHandler(async (request: NextRequest, context: Memory
196220
await db
197221
.update(memory)
198222
.set({ data: validatedData, updatedAt: now })
199-
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
223+
.where(
224+
and(
225+
eq(memory.key, id),
226+
eq(memory.workspaceId, validatedWorkspaceId),
227+
isNull(memory.deletedAt)
228+
)
229+
)
200230

201231
const updatedMemories = await db
202232
.select()
203233
.from(memory)
204-
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
234+
.where(
235+
and(
236+
eq(memory.key, id),
237+
eq(memory.workspaceId, validatedWorkspaceId),
238+
isNull(memory.deletedAt)
239+
)
240+
)
205241
.limit(1)
206242

207243
const mem = updatedMemories[0]

apps/sim/app/api/memory/route.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,13 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => {
293293

294294
const result = await db
295295
.delete(memory)
296-
.where(and(eq(memory.key, conversationId), eq(memory.workspaceId, workspaceId)))
296+
.where(
297+
and(
298+
eq(memory.key, conversationId),
299+
eq(memory.workspaceId, workspaceId),
300+
isNull(memory.deletedAt)
301+
)
302+
)
297303
.returning({ id: memory.id })
298304

299305
const deletedCount = result.length
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { Mem0Block } from '@/blocks/blocks/mem0'
6+
7+
describe('Mem0Block', () => {
8+
const buildParams = Mem0Block.tools.config.params!
9+
10+
it('parses JSON string messages for add operations', () => {
11+
const params = buildParams({
12+
operation: 'add',
13+
apiKey: 'test-key',
14+
userId: 'alice',
15+
messages: JSON.stringify([{ role: 'user', content: 'I like Sim.' }]),
16+
})
17+
18+
expect(params).toEqual({
19+
apiKey: 'test-key',
20+
userId: 'alice',
21+
messages: [{ role: 'user', content: 'I like Sim.' }],
22+
})
23+
})
24+
25+
it('rejects unsupported message roles before execution', () => {
26+
expect(() =>
27+
buildParams({
28+
operation: 'add',
29+
apiKey: 'test-key',
30+
userId: 'alice',
31+
messages: JSON.stringify([{ role: 'system', content: 'Remember this.' }]),
32+
})
33+
).toThrow('Each message must have role user or assistant and non-empty content')
34+
})
35+
36+
it('passes pagination params for get operations', () => {
37+
const params = buildParams({
38+
operation: 'get',
39+
apiKey: 'test-key',
40+
userId: 'alice',
41+
page: '2',
42+
limit: '25',
43+
})
44+
45+
expect(params).toEqual({
46+
apiKey: 'test-key',
47+
userId: 'alice',
48+
page: 2,
49+
limit: 25,
50+
})
51+
})
52+
})

apps/sim/blocks/blocks/mem0.ts

Lines changed: 38 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { toError } from '@sim/utils/errors'
12
import { Mem0Icon } from '@/components/icons'
23
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
34
import type { Mem0Response } from '@/tools/mem0/types'
5+
import { parseMem0Messages } from '@/tools/mem0/utils'
46

57
export const Mem0Block: BlockConfig<Mem0Response> = {
68
type: 'mem0',
@@ -32,7 +34,6 @@ export const Mem0Block: BlockConfig<Mem0Response> = {
3234
title: 'User ID',
3335
type: 'short-input',
3436
placeholder: 'Enter user identifier',
35-
value: () => 'userid', // Default to the working user ID from curl example
3637
required: true,
3738
},
3839
{
@@ -77,6 +78,7 @@ export const Mem0Block: BlockConfig<Mem0Response> = {
7778
field: 'operation',
7879
value: 'get',
7980
},
81+
mode: 'advanced',
8082
wandConfig: {
8183
enabled: true,
8284
prompt: `Generate a date in YYYY-MM-DD format based on the user's description.
@@ -100,6 +102,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
100102
field: 'operation',
101103
value: 'get',
102104
},
105+
mode: 'advanced',
103106
wandConfig: {
104107
enabled: true,
105108
prompt: `Generate a date in YYYY-MM-DD format based on the user's description.
@@ -122,6 +125,17 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
122125
password: true,
123126
required: true,
124127
},
128+
{
129+
id: 'page',
130+
title: 'Page',
131+
type: 'short-input',
132+
placeholder: '1',
133+
condition: {
134+
field: 'operation',
135+
value: 'get',
136+
},
137+
mode: 'advanced',
138+
},
125139
{
126140
id: 'limit',
127141
title: 'Result Limit',
@@ -134,6 +148,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
134148
field: 'operation',
135149
value: ['search', 'get'],
136150
},
151+
mode: 'advanced',
137152
},
138153
],
139154
tools: {
@@ -153,16 +168,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
153168
}
154169
},
155170
params: (params: Record<string, any>) => {
156-
// Create detailed error information for any missing required fields
157171
const errors: string[] = []
172+
const operation = params.operation || 'add'
158173

159-
// Validate required API key for all operations
160174
if (!params.apiKey) {
161175
errors.push('API Key is required')
162176
}
163177

164-
// For search operation, validate required fields
165-
if (params.operation === 'search') {
178+
if (operation === 'search') {
166179
if (!params.query || params.query.trim() === '') {
167180
errors.push('Search Query is required')
168181
}
@@ -172,27 +185,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
172185
}
173186
}
174187

175-
// For add operation, validate required fields
176-
if (params.operation === 'add') {
177-
if (!params.messages) {
178-
errors.push('Messages are required for add operation')
179-
} else if (!Array.isArray(params.messages) || params.messages.length === 0) {
180-
errors.push('Messages must be a non-empty array')
181-
} else {
182-
for (const msg of params.messages) {
183-
if (!msg.role || !msg.content) {
184-
errors.push("Each message must have 'role' and 'content' properties")
185-
break
186-
}
187-
}
188-
}
189-
188+
if (operation === 'add') {
190189
if (!params.userId) {
191190
errors.push('User ID is required')
192191
}
193192
}
194193

195-
// Throw error if any required fields are missing
196194
if (errors.length > 0) {
197195
throw new Error(`Mem0 Block Error: ${errors.join(', ')}`)
198196
}
@@ -201,63 +199,22 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
201199
apiKey: params.apiKey,
202200
}
203201

204-
// Add any identifiers that are present
205202
if (params.userId) result.userId = params.userId
206203

207-
// Add version if specified
208-
if (params.version) result.version = params.version
209-
210-
if (params.limit) result.limit = params.limit
211-
212-
const operation = params.operation || 'add'
204+
if (params.limit) result.limit = Number(params.limit)
213205

214-
// Process operation-specific parameters
215206
switch (operation) {
216207
case 'add':
217-
if (params.messages) {
218-
try {
219-
// Ensure messages are properly formatted
220-
const messagesArray =
221-
typeof params.messages === 'string'
222-
? JSON.parse(params.messages)
223-
: params.messages
224-
225-
// Validate message structure
226-
if (Array.isArray(messagesArray) && messagesArray.length > 0) {
227-
let validMessages = true
228-
for (const msg of messagesArray) {
229-
if (!msg.role || !msg.content) {
230-
validMessages = false
231-
break
232-
}
233-
}
234-
if (validMessages) {
235-
result.messages = messagesArray
236-
} else {
237-
// Consistent with other error handling - collect in errors array
238-
errors.push('Invalid message format - each message must have role and content')
239-
throw new Error(
240-
'Mem0 Block Error: Invalid message format - each message must have role and content'
241-
)
242-
}
243-
} else {
244-
// Consistent with other error handling
245-
errors.push('Messages must be a non-empty array')
246-
throw new Error('Mem0 Block Error: Messages must be a non-empty array')
247-
}
248-
} catch (e: any) {
249-
if (!errors.includes('Messages must be valid JSON')) {
250-
errors.push('Messages must be valid JSON')
251-
}
252-
throw new Error(`Mem0 Block Error: ${e.message || 'Messages must be valid JSON'}`)
253-
}
208+
try {
209+
result.messages = parseMem0Messages(params.messages)
210+
} catch (error) {
211+
throw new Error(`Mem0 Block Error: ${toError(error).message}`)
254212
}
255213
break
256214
case 'search':
257215
if (params.query) {
258216
result.query = params.query
259217

260-
// Check if we have at least one identifier for search
261218
if (!params.userId) {
262219
errors.push('Search requires a User ID')
263220
throw new Error('Mem0 Block Error: Search requires a User ID')
@@ -267,17 +224,16 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
267224
throw new Error('Mem0 Block Error: Search requires a query parameter')
268225
}
269226

270-
// Include limit if specified
271-
if (params.limit) {
272-
result.limit = Number(params.limit)
273-
}
274227
break
275228
case 'get':
276229
if (params.memoryId) {
277230
result.memoryId = params.memoryId
278231
}
279232

280-
// Add date range filtering for v2 get memories
233+
if (params.page) {
234+
result.page = Number(params.page)
235+
}
236+
281237
if (params.startDate) {
282238
result.startDate = params.startDate
283239
}
@@ -296,17 +252,23 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
296252
operation: { type: 'string', description: 'Operation to perform' },
297253
apiKey: { type: 'string', description: 'Mem0 API key' },
298254
userId: { type: 'string', description: 'User identifier' },
299-
version: { type: 'string', description: 'API version' },
300255
messages: { type: 'json', description: 'Message data array' },
301256
query: { type: 'string', description: 'Search query' },
302257
memoryId: { type: 'string', description: 'Memory identifier' },
303258
startDate: { type: 'string', description: 'Start date filter' },
304259
endDate: { type: 'string', description: 'End date filter' },
260+
page: { type: 'number', description: 'Page number for paginated get results' },
305261
limit: { type: 'number', description: 'Result limit' },
306262
},
307263
outputs: {
308-
ids: { type: 'json', description: 'Memory identifiers' },
309-
memories: { type: 'json', description: 'Memory data' },
310-
searchResults: { type: 'json', description: 'Search results' },
264+
ids: { type: 'json', description: 'Memory identifiers returned by search or get operations' },
265+
memories: { type: 'json', description: 'Memory records returned by get operations' },
266+
searchResults: { type: 'json', description: 'Ranked memory records returned by search' },
267+
message: { type: 'string', description: 'Add operation status message' },
268+
status: { type: 'string', description: 'Add operation processing status' },
269+
event_id: { type: 'string', description: 'Add operation event ID for status polling' },
270+
count: { type: 'number', description: 'Total memory count for get operations' },
271+
next: { type: 'string', description: 'Next page URL for get operations' },
272+
previous: { type: 'string', description: 'Previous page URL for get operations' },
311273
},
312274
}

0 commit comments

Comments
 (0)