Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 43 additions & 7 deletions apps/sim/app/api/memory/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
)
)
Comment thread
icecrasher321 marked this conversation as resolved.
.orderBy(memory.createdAt)
.limit(1)

Expand Down Expand Up @@ -125,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) {
Expand All @@ -134,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(
Expand Down Expand Up @@ -177,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) {
Expand All @@ -196,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]
Expand Down
8 changes: 7 additions & 1 deletion apps/sim/app/api/memory/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions apps/sim/blocks/blocks/mem0.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @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')
})

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,
})
})
})
114 changes: 38 additions & 76 deletions apps/sim/blocks/blocks/mem0.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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 { parseMem0Messages } from '@/tools/mem0/utils'

export const Mem0Block: BlockConfig<Mem0Response> = {
type: 'mem0',
Expand Down Expand Up @@ -32,7 +34,6 @@ export const Mem0Block: BlockConfig<Mem0Response> = {
title: 'User ID',
type: 'short-input',
placeholder: 'Enter user identifier',
value: () => 'userid', // Default to the working user ID from curl example
required: true,
},
{
Expand Down Expand Up @@ -77,6 +78,7 @@ export const Mem0Block: BlockConfig<Mem0Response> = {
field: 'operation',
value: 'get',
},
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate a date in YYYY-MM-DD format based on the user's description.
Expand All @@ -100,6 +102,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.
Expand All @@ -122,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',
Expand All @@ -134,6 +148,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
field: 'operation',
value: ['search', 'get'],
},
mode: 'advanced',
},
],
tools: {
Expand All @@ -153,16 +168,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
}
},
params: (params: Record<string, any>) => {
// 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')
}
Expand All @@ -172,27 +185,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(', ')}`)
}
Expand All @@ -201,63 +199,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'
if (params.limit) result.limit = Number(params.limit)

// 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) {
Comment thread
icecrasher321 marked this conversation as resolved.
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')
Expand All @@ -267,17 +224,16 @@ 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)
}
break
case 'get':
if (params.memoryId) {
result.memoryId = params.memoryId
}

// Add date range filtering for v2 get memories
if (params.page) {
result.page = Number(params.page)
}

if (params.startDate) {
result.startDate = params.startDate
}
Expand All @@ -296,17 +252,23 @@ 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' },
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: {
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' },
},
}
Loading
Loading