mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
@@ -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(
|
||||
@@ -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) {
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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',
|
||||
@@ -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: {
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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(', ')}`)
|
||||
}
|
||||
@@ -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 = Number(params.limit)
|
||||
|
||||
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,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
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -28,50 +28,9 @@ async function makeNoisePng(width: number, height: number): Promise<Buffer> {
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
// Both tests do real sharp work (encode + metadata read) that can exceed the
|
||||
// default 10s timeout when CI runs them alongside thousands of other tests.
|
||||
const SHARP_TEST_TIMEOUT_MS = 30_000
|
||||
|
||||
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)
|
||||
|
||||
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: smallPng.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(smallPng)
|
||||
},
|
||||
SHARP_TEST_TIMEOUT_MS
|
||||
)
|
||||
|
||||
it(
|
||||
'downscales oversized images into attachments that fit the read limit',
|
||||
async () => {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @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('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(
|
||||
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',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,16 @@
|
||||
import { ADD_MEMORY_OUTPUT_PROPERTIES } from '@/tools/mem0/types'
|
||||
import {
|
||||
ADD_MEMORY_OUTPUT_PROPERTIES,
|
||||
type Mem0AddMemoriesParams,
|
||||
type Mem0AddMemoriesResponse,
|
||||
} from '@/tools/mem0/types'
|
||||
import { parseMem0Messages } from '@/tools/mem0/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Add Memories Tool
|
||||
* @see https://docs.mem0.ai/api-reference/memory/add-memories
|
||||
*/
|
||||
export const mem0AddMemoriesTool: ToolConfig = {
|
||||
export const mem0AddMemoriesTool: ToolConfig<Mem0AddMemoriesParams, Mem0AddMemoriesResponse> = {
|
||||
id: 'mem0_add_memories',
|
||||
name: 'Add Memories',
|
||||
description: 'Add memories to Mem0 for persistent storage and retrieval',
|
||||
@@ -34,107 +39,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')
|
||||
}
|
||||
const messages = parseMem0Messages(params.messages)
|
||||
return {
|
||||
messages,
|
||||
user_id: params.userId.trim(),
|
||||
}
|
||||
|
||||
// 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<string, any> = {
|
||||
messages: messagesArray,
|
||||
version: 'v2',
|
||||
user_id: params.userId,
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
transformResponse: async (response): Promise<Mem0AddMemoriesResponse> => {
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @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
|
||||
page?: number
|
||||
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',
|
||||
page: 3,
|
||||
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: 3,
|
||||
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'])
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,24 @@
|
||||
import { MEMORY_OUTPUT_PROPERTIES } from '@/tools/mem0/types'
|
||||
import { MEMORY_OUTPUT_PROPERTIES, type Mem0GetMemoriesParams } from '@/tools/mem0/types'
|
||||
import { isRecord } from '@/tools/mem0/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
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<Mem0GetMemoriesParams> = {
|
||||
id: 'mem0_get_memories',
|
||||
name: 'Get Memories',
|
||||
description: 'Retrieve memories from Mem0 by ID or filter criteria',
|
||||
@@ -43,6 +56,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,
|
||||
@@ -52,73 +72,63 @@ export const mem0GetMemoriesTool: ToolConfig = {
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: Record<string, any>) => {
|
||||
// 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<string, any>) => {
|
||||
// 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<string, unknown> = {
|
||||
user_id: params.userId?.trim(),
|
||||
}
|
||||
if (params.startDate || params.endDate) {
|
||||
const dateFilter: Record<string, any> = {}
|
||||
|
||||
const dateFilter: Record<string, unknown> = {}
|
||||
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<string, any> = {
|
||||
return {
|
||||
filters,
|
||||
page: Number(params.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 +149,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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,3 +3,5 @@ import { mem0GetMemoriesTool } from '@/tools/mem0/get_memories'
|
||||
import { mem0SearchMemoriesTool } from '@/tools/mem0/search_memories'
|
||||
|
||||
export { mem0AddMemoriesTool, mem0SearchMemoriesTool, mem0GetMemoriesTool }
|
||||
export * from '@/tools/mem0/types'
|
||||
export * from '@/tools/mem0/utils'
|
||||
|
||||
@@ -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'],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,29 @@
|
||||
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 { isRecord, type JsonRecord } from '@/tools/mem0/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
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<any, Mem0Response> = {
|
||||
export const mem0SearchMemoriesTool: ToolConfig<Mem0SearchMemoriesParams, Mem0Response> = {
|
||||
id: 'mem0_search_memories',
|
||||
name: 'Search Memories',
|
||||
description: 'Search for memories in Mem0 using semantic search',
|
||||
@@ -41,71 +58,46 @@ export const mem0SearchMemoriesTool: ToolConfig<any, Mem0Response> = {
|
||||
},
|
||||
|
||||
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<string, any> = {
|
||||
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<Mem0Response> => {
|
||||
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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
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
|
||||
page?: number
|
||||
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 +43,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<string, OutputProperty>
|
||||
|
||||
@@ -30,7 +63,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 +99,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<string, OutputProperty>
|
||||
|
||||
/**
|
||||
@@ -139,7 +160,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 +169,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
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import type { Mem0Message } from '@/tools/mem0/types'
|
||||
|
||||
export type JsonRecord = Record<string, unknown>
|
||||
|
||||
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 &&
|
||||
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
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -35,12 +35,8 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
|
||||
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<any, MemoryResponse> = {
|
||||
|
||||
transformResponse: async (response): Promise<MemoryResponse> => {
|
||||
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<any, MemoryResponse> = {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
memories,
|
||||
message: `Found ${memories.length} memories`,
|
||||
memories: [memory],
|
||||
message: 'Found 1 memory',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user