Skip to content

Commit c3870e1

Browse files
committed
fix(memories): get memory tool, mem0 integration update
1 parent 31cfb74 commit c3870e1

13 files changed

Lines changed: 626 additions & 284 deletions

File tree

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

Lines changed: 8 additions & 2 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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
})

apps/sim/blocks/blocks/mem0.ts

Lines changed: 60 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,46 @@
1+
import { toError } from '@sim/utils/errors'
12
import { Mem0Icon } from '@/components/icons'
23
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
3-
import type { Mem0Response } from '@/tools/mem0/types'
4+
import type { Mem0Message, Mem0Response } from '@/tools/mem0/types'
5+
6+
function isMem0Message(value: unknown): value is Mem0Message {
7+
return (
8+
Boolean(value) &&
9+
typeof value === 'object' &&
10+
'role' in value &&
11+
'content' in value &&
12+
(value.role === 'user' || value.role === 'assistant') &&
13+
typeof value.content === 'string' &&
14+
value.content.length > 0
15+
)
16+
}
17+
18+
function parseMem0Messages(value: unknown): Mem0Message[] {
19+
if (!value) {
20+
throw new Error('Messages are required for add operation')
21+
}
22+
23+
let messages: unknown
24+
try {
25+
messages = typeof value === 'string' ? JSON.parse(value) : value
26+
} catch (error) {
27+
throw new Error(`Messages must be valid JSON: ${toError(error).message}`)
28+
}
29+
30+
if (!Array.isArray(messages) || messages.length === 0) {
31+
throw new Error('Messages must be a non-empty array')
32+
}
33+
34+
const validMessages: Mem0Message[] = []
35+
for (const message of messages) {
36+
if (!isMem0Message(message)) {
37+
throw new Error('Each message must have role user or assistant and non-empty content')
38+
}
39+
validMessages.push(message)
40+
}
41+
42+
return validMessages
43+
}
444

545
export const Mem0Block: BlockConfig<Mem0Response> = {
646
type: 'mem0',
@@ -32,7 +72,6 @@ export const Mem0Block: BlockConfig<Mem0Response> = {
3272
title: 'User ID',
3373
type: 'short-input',
3474
placeholder: 'Enter user identifier',
35-
value: () => 'userid', // Default to the working user ID from curl example
3675
required: true,
3776
},
3877
{
@@ -77,6 +116,7 @@ export const Mem0Block: BlockConfig<Mem0Response> = {
77116
field: 'operation',
78117
value: 'get',
79118
},
119+
mode: 'advanced',
80120
wandConfig: {
81121
enabled: true,
82122
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
100140
field: 'operation',
101141
value: 'get',
102142
},
143+
mode: 'advanced',
103144
wandConfig: {
104145
enabled: true,
105146
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
134175
field: 'operation',
135176
value: ['search', 'get'],
136177
},
178+
mode: 'advanced',
137179
},
138180
],
139181
tools: {
@@ -153,16 +195,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
153195
}
154196
},
155197
params: (params: Record<string, any>) => {
156-
// Create detailed error information for any missing required fields
157198
const errors: string[] = []
199+
const operation = params.operation || 'add'
158200

159-
// Validate required API key for all operations
160201
if (!params.apiKey) {
161202
errors.push('API Key is required')
162203
}
163204

164-
// For search operation, validate required fields
165-
if (params.operation === 'search') {
205+
if (operation === 'search') {
166206
if (!params.query || params.query.trim() === '') {
167207
errors.push('Search Query is required')
168208
}
@@ -172,27 +212,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
172212
}
173213
}
174214

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-
215+
if (operation === 'add') {
190216
if (!params.userId) {
191217
errors.push('User ID is required')
192218
}
193219
}
194220

195-
// Throw error if any required fields are missing
196221
if (errors.length > 0) {
197222
throw new Error(`Mem0 Block Error: ${errors.join(', ')}`)
198223
}
@@ -201,63 +226,22 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
201226
apiKey: params.apiKey,
202227
}
203228

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

207-
// Add version if specified
208-
if (params.version) result.version = params.version
209-
210231
if (params.limit) result.limit = params.limit
211232

212-
const operation = params.operation || 'add'
213-
214-
// Process operation-specific parameters
215233
switch (operation) {
216234
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-
}
235+
try {
236+
result.messages = parseMem0Messages(params.messages)
237+
} catch (error) {
238+
throw new Error(`Mem0 Block Error: ${toError(error).message}`)
254239
}
255240
break
256241
case 'search':
257242
if (params.query) {
258243
result.query = params.query
259244

260-
// Check if we have at least one identifier for search
261245
if (!params.userId) {
262246
errors.push('Search requires a User ID')
263247
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
267251
throw new Error('Mem0 Block Error: Search requires a query parameter')
268252
}
269253

270-
// Include limit if specified
271254
if (params.limit) {
272255
result.limit = Number(params.limit)
273256
}
@@ -277,7 +260,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
277260
result.memoryId = params.memoryId
278261
}
279262

280-
// Add date range filtering for v2 get memories
281263
if (params.startDate) {
282264
result.startDate = params.startDate
283265
}
@@ -296,7 +278,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
296278
operation: { type: 'string', description: 'Operation to perform' },
297279
apiKey: { type: 'string', description: 'Mem0 API key' },
298280
userId: { type: 'string', description: 'User identifier' },
299-
version: { type: 'string', description: 'API version' },
300281
messages: { type: 'json', description: 'Message data array' },
301282
query: { type: 'string', description: 'Search query' },
302283
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
305286
limit: { type: 'number', description: 'Result limit' },
306287
},
307288
outputs: {
308-
ids: { type: 'json', description: 'Memory identifiers' },
309-
memories: { type: 'json', description: 'Memory data' },
310-
searchResults: { type: 'json', description: 'Search results' },
289+
ids: { type: 'json', description: 'Memory identifiers returned by search or get operations' },
290+
memories: { type: 'json', description: 'Memory records returned by get operations' },
291+
searchResults: { type: 'json', description: 'Ranked memory records returned by search' },
292+
message: { type: 'string', description: 'Add operation status message' },
293+
status: { type: 'string', description: 'Add operation processing status' },
294+
event_id: { type: 'string', description: 'Add operation event ID for status polling' },
295+
count: { type: 'number', description: 'Total memory count for get operations' },
296+
next: { type: 'string', description: 'Next page URL for get operations' },
297+
previous: { type: 'string', description: 'Previous page URL for get operations' },
311298
},
312299
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { mem0AddMemoriesTool } from '@/tools/mem0/add_memories'
6+
import type { Mem0AddMemoriesParams } from '@/tools/mem0/types'
7+
8+
describe('mem0AddMemoriesTool', () => {
9+
const buildBody = mem0AddMemoriesTool.request.body!
10+
const transformResponse = mem0AddMemoriesTool.transformResponse!
11+
12+
it('uses the v3 add memories endpoint', () => {
13+
expect(mem0AddMemoriesTool.request.url).toBe('https://api.mem0.ai/v3/memories/add/')
14+
expect(mem0AddMemoriesTool.request.method).toBe('POST')
15+
})
16+
17+
it('builds the documented add memories request body', () => {
18+
const body = buildBody({
19+
apiKey: 'test-key',
20+
userId: ' alice ',
21+
messages: [{ role: 'user', content: 'I like Sim.' }],
22+
})
23+
24+
expect(body).toEqual({
25+
messages: [{ role: 'user', content: 'I like Sim.' }],
26+
user_id: 'alice',
27+
})
28+
})
29+
30+
it('accepts JSON string messages from the block code input', () => {
31+
const params: Mem0AddMemoriesParams = {
32+
apiKey: 'test-key',
33+
userId: 'alice',
34+
messages: JSON.stringify([{ role: 'assistant', content: 'I will remember that.' }]),
35+
}
36+
37+
expect(buildBody(params)).toEqual({
38+
messages: [{ role: 'assistant', content: 'I will remember that.' }],
39+
user_id: 'alice',
40+
})
41+
})
42+
43+
it('extracts queued processing fields from v3 responses', async () => {
44+
const result = await transformResponse(
45+
new Response(
46+
JSON.stringify({
47+
message: 'Memory processing has been queued for background execution',
48+
status: 'PENDING',
49+
event_id: 'evt-123',
50+
})
51+
)
52+
)
53+
54+
expect(result).toEqual({
55+
success: true,
56+
output: {
57+
message: 'Memory processing has been queued for background execution',
58+
status: 'PENDING',
59+
event_id: 'evt-123',
60+
},
61+
})
62+
})
63+
})

0 commit comments

Comments
 (0)