From b19713401fb8e3a12bcc79d67f9a6a6e5a3de2fe Mon Sep 17 00:00:00 2001 From: yaleMemVerge Date: Sun, 4 Jan 2026 21:41:17 +0800 Subject: [PATCH] feat: Add memmachine toolset This plugin implements the seamless integration between FastGPT and MemMachine --- modules/tool/packages/memmachine/DESIGN.md | 32 ++++ modules/tool/packages/memmachine/README.md | 89 +++++++++++ .../memmachine/children/search/config.ts | 93 ++++++++++++ .../memmachine/children/search/index.ts | 10 ++ .../children/search/src/contextTemplate.ts | 19 +++ .../memmachine/children/search/src/index.ts | 61 ++++++++ .../children/search/src/renderTemplate.ts | 74 +++++++++ .../children/search/test/index.test.ts | 140 ++++++++++++++++++ .../memmachine/children/store/config.ts | 109 ++++++++++++++ .../memmachine/children/store/index.ts | 10 ++ .../memmachine/children/store/src/index.ts | 90 +++++++++++ .../children/store/test/index.test.ts | 81 ++++++++++ modules/tool/packages/memmachine/config.ts | 31 ++++ modules/tool/packages/memmachine/index.ts | 6 + modules/tool/packages/memmachine/logo.svg | 11 ++ modules/tool/packages/memmachine/package.json | 17 +++ 16 files changed, 873 insertions(+) create mode 100644 modules/tool/packages/memmachine/DESIGN.md create mode 100644 modules/tool/packages/memmachine/README.md create mode 100644 modules/tool/packages/memmachine/children/search/config.ts create mode 100644 modules/tool/packages/memmachine/children/search/index.ts create mode 100644 modules/tool/packages/memmachine/children/search/src/contextTemplate.ts create mode 100644 modules/tool/packages/memmachine/children/search/src/index.ts create mode 100644 modules/tool/packages/memmachine/children/search/src/renderTemplate.ts create mode 100644 modules/tool/packages/memmachine/children/search/test/index.test.ts create mode 100644 modules/tool/packages/memmachine/children/store/config.ts create mode 100644 modules/tool/packages/memmachine/children/store/index.ts create mode 100644 modules/tool/packages/memmachine/children/store/src/index.ts create mode 100644 modules/tool/packages/memmachine/children/store/test/index.test.ts create mode 100644 modules/tool/packages/memmachine/config.ts create mode 100644 modules/tool/packages/memmachine/index.ts create mode 100644 modules/tool/packages/memmachine/logo.svg create mode 100644 modules/tool/packages/memmachine/package.json diff --git a/modules/tool/packages/memmachine/DESIGN.md b/modules/tool/packages/memmachine/DESIGN.md new file mode 100644 index 00000000..f3ce3a30 --- /dev/null +++ b/modules/tool/packages/memmachine/DESIGN.md @@ -0,0 +1,32 @@ +# MemMachine 系统工具 + +## 参考信息 + +本工具集用于处理 MemMachine 相关操作。 + +### 参考文档 + +- [MemMachine 文档](https://docs.memmachine.ai/) +- [MemMachine API 文档](https://api.memmachine.ai/docs) + +### 测试密钥环境变量名 + +无需环境变量配置 + +密钥获取链接:https://console.memmachine.ai/ + +### 工具集/子工具列表 + +一个MemMachine工具集,包含以下工具: + +#### 1. 存储记忆(store) +- **功能**:将用户的记忆内容通过 MemMachine API 存储到 MemMachine 服务端。 +- **API**:`POST {baseUrl}/memories` +- **输入**:组织 ID, 项目 ID, 记忆类型, 记忆消息, 消息发送者, 消息接收者, 消息创建时间, 消息角色, 附加属性 +- **输出**:新的记忆 ID + +#### 2. 搜索记忆(search) +- **功能**:根据关键词,通过 MemMachine API 检索已存储的记忆内容。 +- **API**:`POST {baseUrl}/memories/search` +- **输入**:组织 ID, 项目 ID, 记忆类型, 搜索内容,最大返回数量,过滤条件,上下文模板 +- **输出**:记忆上下文 diff --git a/modules/tool/packages/memmachine/README.md b/modules/tool/packages/memmachine/README.md new file mode 100644 index 00000000..a6d198eb --- /dev/null +++ b/modules/tool/packages/memmachine/README.md @@ -0,0 +1,89 @@ +# MemMachine 工具集 + +MemMachine 是一个面向高级 AI 智能体的开源记忆层,使 AI 应用能够学习、存储和回溯历史会话中的数据与偏好,提升后续交互的智能性与个性化体验。 + +## 密钥获取 + +1. 访问 [MemMachine 在线平台](https://console.memmachine.ai/) 并注册账号。 +2. 登录平台后,前往 [API Keys 页面](https://console.memmachine.ai/api-keys) 获取 API 密钥。 +3. 密钥格式为:`mm-xxxxxxxxxxxxx`(以 `mm-` 开头)。 + +## 功能 + +### 记忆存储 + +将关于用户或对话的重要新信息存入记忆。 + +**主要特性:** + +- 存储用户各类信息内容 +- 基于已存信息智能总结用户偏好等特征 + +**参数配置** + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| orgId | string | universal | 组织的唯一标识符 | +| projectId | string | universal | 项目的唯一标识符 | +| types | multipleSelect | ['episodic', 'semantic'] | 记忆类型,留空则添加到全部类型 | +| content | string | - | **必填** 要存储的记忆内容 | +| producer | string | user | 记忆内容的发送者 | +| producedFor | string | - | 记忆内容的接收者 | +| timestamp | string | - | 记忆内容的创建时间(ISO 8601格式) | +| role | string | - | 记忆内容在对话中的角色 | +| metadata | string | - | 附加的记忆内容属性(JSON格式) | + +**输出格式** +```json +{ + "memoryId": "新的记忆 ID" +} +``` + +### 记忆搜索 + +检索用户的相关上下文、记忆或画像信息。 + +**主要特性:** + +- 支持自然语言查询,查找相关记忆内容 +- 可跨会话回溯,获取全面的历史信息 + +**参数配置** + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| orgId | string | universal | 组织的唯一标识符 | +| projectId | string | universal | 项目的唯一标识符 | +| types | multipleSelect | ['episodic', 'semantic'] | 记忆类型,留空则搜索全部类型 | +| query | string | - | **必填** 记忆检索的自然语言查询 | +| limit | number | 10 | **必填** 搜索结果数量上限 | +| filter | string | - | 过滤记忆的条件 | +| contextTemplate | string | 默认模板 | 构建记忆上下文的模板 | + +**输出格式** +```json +{ + "memoryContext": "记忆上下文" +} +``` + +## 开发和测试 + +```bash +# 安装依赖 +bun install + +# 运行测试 +bun run test + +# 构建项目 +bun run build:runtime +``` + +## 支持与反馈 + +如有问题或建议,请通过以下方式联系: + +- [GitHub Issues](https://github.com/MemMachine/MemMachine/issues) +- [MemMachine 文档](https://docs.memmachine.ai/) diff --git a/modules/tool/packages/memmachine/children/search/config.ts b/modules/tool/packages/memmachine/children/search/config.ts new file mode 100644 index 00000000..75706873 --- /dev/null +++ b/modules/tool/packages/memmachine/children/search/config.ts @@ -0,0 +1,93 @@ +import { defineTool } from '@tool/type'; +import { FlowNodeInputTypeEnum, WorkflowIOValueTypeEnum } from '@tool/type/fastgpt'; +import { contextTemplate } from './src/contextTemplate'; + +export default defineTool({ + name: { + 'zh-CN': '搜索记忆', + en: 'Search Memory' + }, + description: { + 'zh-CN': '用于通过 MemMachine API 搜索记忆。', + en: 'Used to search memory via the MemMachine API.' + }, + toolDescription: 'Searches memory efficiently via the MemMachine API.', + versionList: [ + { + value: '0.1.0', + description: 'Default version', + inputs: [ + { + key: 'orgId', + label: '组织 ID', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + selectedTypeIndex: 1, + valueType: WorkflowIOValueTypeEnum.string, + description: '可选,指定搜索记忆的组织 ID' + }, + { + key: 'projectId', + label: '项目 ID', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + selectedTypeIndex: 1, + valueType: WorkflowIOValueTypeEnum.string, + description: '可选,指定搜索记忆的项目 ID' + }, + { + key: 'types', + label: '记忆类型', + renderTypeList: [FlowNodeInputTypeEnum.multipleSelect, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.arrayString, + list: [ + { label: '情节记忆', value: 'episodic' }, + { label: '语义记忆', value: 'semantic' } + ], + defaultValue: ['episodic', 'semantic'], + description: '指定记忆的类型。未指定时将搜索所有类型的记忆。' + }, + { + key: 'query', + label: '搜索内容', + renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: true, + description: '用于语义记忆检索的自然语言查询。应为对所需信息的描述性字符串。' + }, + { + key: 'limit', + label: '最大返回数量', + renderTypeList: [FlowNodeInputTypeEnum.numberInput, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.number, + defaultValue: 10, + required: true, + description: '指定搜索结果中要返回的最大记忆条目数量。' + }, + { + key: 'filter', + label: '过滤条件', + renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + placeholder: '例如:metadata.session_id=session_123 AND metadata.user_id=user_456', + description: + "可选,指定用于过滤记忆的条件。应用于记忆的附加属性,支持简单查询语法(如 'metadata.user_id=123')来实现精确匹配,多个条件可通过 AND 组合。" + }, + { + key: 'contextTemplate', + label: '上下文模板', + renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + defaultValue: contextTemplate, + description: '用于构建记忆上下文的模板,支持占位符替换。' + } + ], + outputs: [ + { + valueType: WorkflowIOValueTypeEnum.string, + key: 'memoryContext', + label: '记忆上下文', + description: '记忆上下文' + } + ] + } + ] +}); diff --git a/modules/tool/packages/memmachine/children/search/index.ts b/modules/tool/packages/memmachine/children/search/index.ts new file mode 100644 index 00000000..d698ed48 --- /dev/null +++ b/modules/tool/packages/memmachine/children/search/index.ts @@ -0,0 +1,10 @@ +import config from './config'; +import { InputType, OutputType, tool as toolCb } from './src'; +import { exportTool } from '@tool/utils/tool'; + +export default exportTool({ + toolCb, + InputType, + OutputType, + config +}); diff --git a/modules/tool/packages/memmachine/children/search/src/contextTemplate.ts b/modules/tool/packages/memmachine/children/search/src/contextTemplate.ts new file mode 100644 index 00000000..12e4c930 --- /dev/null +++ b/modules/tool/packages/memmachine/children/search/src/contextTemplate.ts @@ -0,0 +1,19 @@ +export const contextTemplate = `# 记忆上下文 + +**使用说明**: +以语义记忆为用户的核心事实依据。 +结合短期记忆和事件摘要,获取近期上下文与交互概览。 +查阅长期记忆,洞察历史模式与深层信息。 + +## 用户画像(语义记忆) +{{semanticMemory}} + +## 当前上下文(短期记忆) +{{shortTermMemory}} + +## 事件摘要 +{{episodeSummary}} + +## 历史上下文(长期记忆) +{{longTermMemory}} +`; diff --git a/modules/tool/packages/memmachine/children/search/src/index.ts b/modules/tool/packages/memmachine/children/search/src/index.ts new file mode 100644 index 00000000..21c339de --- /dev/null +++ b/modules/tool/packages/memmachine/children/search/src/index.ts @@ -0,0 +1,61 @@ +import { z } from 'zod'; +import { renderTemplate } from './renderTemplate'; + +export const InputType = z.object({ + baseUrl: z.preprocess( + (v) => (typeof v === 'string' && v.trim() === '' ? undefined : v), + z.string().default('https://api.memmachine.ai/v2') + ), + apiKey: z.string().nonempty(), + orgId: z.string().optional(), + projectId: z.string().optional(), + types: z.array(z.string()).default(['episodic', 'semantic']), + query: z.string().nonempty(), + limit: z.number().default(10), + filter: z.string().default(''), + contextTemplate: z.string().default('') +}); + +export const OutputType = z.object({ + memoryContext: z.string() +}); + +export async function tool({ + baseUrl, + apiKey, + orgId, + projectId, + types, + query, + limit, + filter, + contextTemplate +}: z.infer): Promise> { + // 请求数据 + const response = await fetch(`${baseUrl}/memories/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify({ + org_id: orgId, + project_id: projectId, + types, + top_k: limit, + query, + filter + }) + }); + + if (!response.ok) { + return Promise.reject({ + error: `MemMachine API Error: ${response.status} ${response.statusText}` + }); + } + + const data = await response.json(); + return { + memoryContext: renderTemplate(contextTemplate, data?.content || {}) + }; +} diff --git a/modules/tool/packages/memmachine/children/search/src/renderTemplate.ts b/modules/tool/packages/memmachine/children/search/src/renderTemplate.ts new file mode 100644 index 00000000..c17fbbde --- /dev/null +++ b/modules/tool/packages/memmachine/children/search/src/renderTemplate.ts @@ -0,0 +1,74 @@ +interface EpisodeItem { + content?: string; + producer_id?: string; + produced_for_id?: string; +} + +interface SemanticFeature { + tag?: string; + feature_name?: string; + value?: string; +} + +export function renderTemplate(template: string, content: Record): string { + // semantic memory + const semanticMemory = content.semantic_memory || []; + + // episodic memory + const episodicMemory = content.episodic_memory || {}; + const shortTermMemory = episodicMemory.short_term_memory?.episodes || []; + const episodeSummary = episodicMemory.short_term_memory?.episode_summary || []; + const longTermMemory = episodicMemory.long_term_memory?.episodes || []; + + let result = template; + result = result.replace('{{semanticMemory}}', _formatSemanticMemory(semanticMemory)); + result = result.replace('{{shortTermMemory}}', _formatEpisodicMemory(shortTermMemory)); + result = result.replace('{{episodeSummary}}', _formatEpisodeSummary(episodeSummary)); + result = result.replace('{{longTermMemory}}', _formatEpisodicMemory(longTermMemory)); + + return result; +} + +function _formatSemanticMemory(semanticMemory?: SemanticFeature[]): string { + if (!Array.isArray(semanticMemory) || semanticMemory.length === 0) { + return '*No semantic features available*'; + } + + return semanticMemory + .map((feature) => { + const tag = feature?.tag?.trim() || 'General'; + const featureName = feature?.feature_name?.trim() || 'property'; + const value = feature?.value?.trim() || ''; + return value ? `- **${tag}** / ${featureName}: ${value}` : `- **${tag}** / ${featureName}`; + }) + .join('\n'); +} + +function _formatEpisodicMemory(episodes?: EpisodeItem[]): string { + if (!Array.isArray(episodes) || episodes.length === 0) { + return '*No memories available*'; + } + + return episodes + .map((episode) => { + const content = episode?.content?.trim() || ''; + const producer = episode?.producer_id?.trim() || 'Unknown'; + const producedFor = episode?.produced_for_id?.trim() || 'Unknown'; + return content + ? `- **${producer}** → ${producedFor}: ${content}` + : `- **${producer}** → ${producedFor}`; + }) + .join('\n'); +} + +function _formatEpisodeSummary(episodeSummary?: string[]): string { + if (!Array.isArray(episodeSummary) || !episodeSummary.some((s) => s?.trim())) { + return '*No episode summaries available*'; + } + + return episodeSummary + .map((summary) => summary?.trim()) + .filter((summary) => !!summary) + .map((summary) => `> ${summary}`) + .join('\n\n'); +} diff --git a/modules/tool/packages/memmachine/children/search/test/index.test.ts b/modules/tool/packages/memmachine/children/search/test/index.test.ts new file mode 100644 index 00000000..bce52c8c --- /dev/null +++ b/modules/tool/packages/memmachine/children/search/test/index.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it, vi } from 'vitest'; +import { tool } from '../src'; +import { contextTemplate } from '../src/contextTemplate'; + +const mockSearchResult = { + content: { + episodic_memory: { + long_term_memory: { + episodes: [ + { + content: 'In 2020, the user showed interest in machine learning.', + producer_id: 'user-123', + producer_role: 'user', + produced_for_id: 'agent-456', + episode_type: 'message', + metadata: null, + created_at: '2024-01-15T10:00:00Z', + uid: '1' + } + ] + }, + short_term_memory: { + episodes: [ + { + content: 'User asked about AI advancements.', + producer_id: 'user-123', + producer_role: 'user', + produced_for_id: 'agent-456', + episode_type: 'message', + metadata: null, + created_at: '2024-06-01T12:00:00Z', + uid: '3' + } + ], + episode_summary: ['User is interested in AI.'] + } + }, + semantic_memory: [ + { + set_id: 'mem_session_universal/universal', + category: 'profile', + tag: 'Interest', + feature_name: 'Topic', + value: 'Artificial Intelligence', + metadata: { + citations: null, + id: 2, + other: null + } + } + ] + } +}; + +describe('Memmachine Search Tool', () => { + it('should return search results on success', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockSearchResult + }) + ); + await expect( + tool({ + apiKey: 'test-api-key', + query: 'Tell me about AI', + contextTemplate: contextTemplate + } as any) + ).resolves.toMatchObject({ + memoryContext: expect.stringContaining('User is interested in AI.') + }); + }); + + it('should handle empty search results', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({}) + }) + ); + await expect( + tool({ + apiKey: 'test-api-key', + query: 'Tell me about AI', + contextTemplate: '' + } as any) + ).resolves.toMatchObject({ + memoryContext: '' + }); + }); + + it('should handle empty memory sections', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + content: { + episodic_memory: { + long_term_memory: { episodes: [] }, + short_term_memory: { episodes: [{}], episode_summary: [] } + }, + semantic_memory: [{}] + } + }) + }) + ); + await expect( + tool({ + apiKey: 'test-api-key', + query: 'Tell me about AI', + contextTemplate: contextTemplate + } as any) + ).resolves.toMatchObject({ + memoryContext: expect.stringContaining('Unknown') + }); + }); + + it('should handle API error response', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: false, + status: 403, + statusText: 'Forbidden' + }) + ); + await expect( + tool({ + apiKey: 'test-api-key', + query: 'Tell me about AI', + contextTemplate: contextTemplate + } as any) + ).rejects.toMatchObject({ + error: 'MemMachine API Error: 403 Forbidden' + }); + }); +}); diff --git a/modules/tool/packages/memmachine/children/store/config.ts b/modules/tool/packages/memmachine/children/store/config.ts new file mode 100644 index 00000000..49c7a5f3 --- /dev/null +++ b/modules/tool/packages/memmachine/children/store/config.ts @@ -0,0 +1,109 @@ +import { defineTool } from '@tool/type'; +import { FlowNodeInputTypeEnum, WorkflowIOValueTypeEnum } from '@tool/type/fastgpt'; + +export default defineTool({ + name: { + 'zh-CN': '存储记忆', + en: 'Store Memory' + }, + description: { + 'zh-CN': '用于通过 MemMachine API 存储记忆。', + en: 'Used to store memory via the MemMachine API.' + }, + toolDescription: 'Stores memory efficiently via the MemMachine API.', + versionList: [ + { + value: '0.1.0', + description: 'Default version', + inputs: [ + { + key: 'orgId', + label: '组织 ID', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + selectedTypeIndex: 1, + valueType: WorkflowIOValueTypeEnum.string, + description: '可选,指定存储记忆的组织 ID' + }, + { + key: 'projectId', + label: '项目 ID', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + selectedTypeIndex: 1, + valueType: WorkflowIOValueTypeEnum.string, + description: '可选,指定存储记忆的项目 ID' + }, + { + key: 'types', + label: '记忆类型', + renderTypeList: [FlowNodeInputTypeEnum.multipleSelect, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.arrayString, + list: [ + { label: '情节记忆', value: 'episodic' }, + { label: '语义记忆', value: 'semantic' } + ], + defaultValue: ['episodic', 'semantic'], + description: '指定记忆的类型。未指定时将存储为所有类型的记忆。' + }, + { + key: 'content', + label: '记忆消息', + renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: true, + description: '要存储的记忆内容' + }, + { + key: 'producer', + label: '消息发送者', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + selectedTypeIndex: 1, + valueType: WorkflowIOValueTypeEnum.string, + description: '可选,记忆内容的发送者' + }, + { + key: 'producedFor', + label: '消息接收者', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + description: '可选,记忆内容的接收者' + }, + { + key: 'timestamp', + label: '消息创建时间(ISO 8601格式)', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + description: '可选,记忆内容的创建时间,格式如 2023-10-05T14:48:00.000Z' + }, + { + key: 'role', + label: '消息角色', + renderTypeList: [FlowNodeInputTypeEnum.select, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + list: [ + { label: '用户', value: 'user' }, + { label: '助手', value: 'assistant' }, + { label: '系统', value: 'system' } + ], + defaultValue: 'user', + description: '可选,记忆内容在对话中的角色' + }, + { + key: 'metadata', + label: '附加属性(JSON格式)', + renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + placeholder: '例如:{"session_id":"session_123", "user_id":"user_456"}', + description: '可选,附加的记忆内容属性,需为有效的 JSON 格式字符串' + } + ], + outputs: [ + { + valueType: WorkflowIOValueTypeEnum.string, + key: 'memoryId', + label: '新的记忆 ID', + description: '返回创建的记忆 ID' + } + ] + } + ] +}); diff --git a/modules/tool/packages/memmachine/children/store/index.ts b/modules/tool/packages/memmachine/children/store/index.ts new file mode 100644 index 00000000..d698ed48 --- /dev/null +++ b/modules/tool/packages/memmachine/children/store/index.ts @@ -0,0 +1,10 @@ +import config from './config'; +import { InputType, OutputType, tool as toolCb } from './src'; +import { exportTool } from '@tool/utils/tool'; + +export default exportTool({ + toolCb, + InputType, + OutputType, + config +}); diff --git a/modules/tool/packages/memmachine/children/store/src/index.ts b/modules/tool/packages/memmachine/children/store/src/index.ts new file mode 100644 index 00000000..b69938dc --- /dev/null +++ b/modules/tool/packages/memmachine/children/store/src/index.ts @@ -0,0 +1,90 @@ +import { getErrText } from '@tool/utils/err'; +import { z } from 'zod'; + +export const InputType = z.object({ + baseUrl: z.preprocess( + (v) => (typeof v === 'string' && v.trim() === '' ? undefined : v), + z.string().default('https://api.memmachine.ai/v2') + ), + apiKey: z.string().nonempty(), + orgId: z.string().optional(), + projectId: z.string().optional(), + types: z.array(z.string()).default(['episodic', 'semantic']), + content: z.string().nonempty(), + producer: z.string().optional(), + producedFor: z.string().optional(), + timestamp: z.string().optional(), + role: z.string().optional(), + metadata: z.string().optional() +}); + +export const OutputType = z.object({ + memoryId: z.string() +}); + +export async function tool({ + baseUrl, + apiKey, + orgId, + projectId, + types, + content, + producer, + producedFor, + timestamp, + role, + metadata +}: z.infer): Promise> { + let metadataObj: Record | undefined; + if (metadata) { + try { + metadataObj = JSON.parse(metadata); + } catch (e) { + return Promise.reject({ error: `Invalid JSON format for metadata: ${getErrText(e)}` }); + } + } + + let timestampISO: string | undefined; + if (timestamp) { + try { + timestampISO = new Date(timestamp).toISOString(); + } catch (e) { + return Promise.reject({ error: `Invalid format for timestamp: ${getErrText(e)}` }); + } + } + + // 请求数据 + const response = await fetch(`${baseUrl}/memories`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify({ + org_id: orgId, + project_id: projectId, + types, + messages: [ + { + content, + producer, + produced_for: producedFor, + timestamp: timestampISO || new Date().toISOString(), + role, + metadata: metadataObj || {} + } + ] + }) + }); + + if (!response.ok) { + return Promise.reject({ + error: `MemMachine API Error: ${response.status} ${response.statusText}` + }); + } + + const data = await response.json(); + return { + memoryId: data?.results?.[0]?.uid ?? '' + }; +} diff --git a/modules/tool/packages/memmachine/children/store/test/index.test.ts b/modules/tool/packages/memmachine/children/store/test/index.test.ts new file mode 100644 index 00000000..e69ef220 --- /dev/null +++ b/modules/tool/packages/memmachine/children/store/test/index.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from 'vitest'; +import { tool } from '../src'; + +describe('Memmachine Store Tool', () => { + it('should return memoryId on success', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ results: [{ uid: '123' }] }) + }) + ); + await expect( + tool({ + apiKey: 'test-api-key', + orgId: 'org-123', + projectId: 'proj-123', + types: ['episodic'], + content: 'test content', + producer: 'user-123', + producedFor: 'agent-123', + timestamp: '2024-01-01T00:00:00Z', + role: 'user', + metadata: '{"key":"value"}' + } as any) + ).resolves.toEqual({ memoryId: '123' }); + }); + + it('should handle empty memory results', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ results: [] }) + }) + ); + await expect( + tool({ + apiKey: 'test-api-key', + content: 'test content' + } as any) + ).resolves.toEqual({ memoryId: '' }); + }); + + it('should handle API error response', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: false, + status: 400, + statusText: 'Bad Request' + }) + ); + await expect( + tool({ + apiKey: 'test-api-key', + content: 'test content' + } as any) + ).rejects.toEqual({ error: 'MemMachine API Error: 400 Bad Request' }); + }); + + it('should handle invalid metadata JSON', async () => { + await expect( + tool({ + apiKey: 'test-api-key', + content: 'test content', + metadata: 'invalid-json' + } as any) + ).rejects.toMatchObject({ error: expect.stringMatching(/Invalid JSON format for metadata/) }); + }); + + it('should handle invalid timestamp format', async () => { + await expect( + tool({ + apiKey: 'test-api-key', + content: 'test content', + timestamp: 'invalid-timestamp' + } as any) + ).rejects.toMatchObject({ error: expect.stringMatching(/Invalid format for timestamp/) }); + }); +}); diff --git a/modules/tool/packages/memmachine/config.ts b/modules/tool/packages/memmachine/config.ts new file mode 100644 index 00000000..0ea05a3b --- /dev/null +++ b/modules/tool/packages/memmachine/config.ts @@ -0,0 +1,31 @@ +import { defineToolSet } from '@tool/type'; +import { ToolTagEnum } from '@tool/type/tags'; + +export default defineToolSet({ + name: { + 'zh-CN': 'MemMachine', + en: 'MemMachine' + }, + tags: [ToolTagEnum.enum.tools], + courseUrl: 'https://docs.memmachine.ai/getting_started/introduction', + description: { + 'zh-CN': '这是 MemMachine 工具集,支持通过 MemMachine API 进行记忆的存储与搜索。', + en: 'This is the MemMachine tool set, which supports memory storage and search via the MemMachine API.' + }, + toolDescription: 'Enables efficient memory storage and search via the MemMachine API.', + secretInputConfig: [ + { + key: 'baseUrl', + label: 'Base URL(Saas服务不需要填写)', + description: '例如:https://api.memmachine.ai/v2,http://127.0.0.1:8080/api/v2', + inputType: 'input' + }, + { + key: 'apiKey', + label: 'API Key', + description: '可以在 https://console.memmachine.ai 获取', + required: true, + inputType: 'secret' + } + ] +}); diff --git a/modules/tool/packages/memmachine/index.ts b/modules/tool/packages/memmachine/index.ts new file mode 100644 index 00000000..e6357f9b --- /dev/null +++ b/modules/tool/packages/memmachine/index.ts @@ -0,0 +1,6 @@ +import config from './config'; +import { exportToolSet } from '@tool/utils/tool'; + +export default exportToolSet({ + config +}); diff --git a/modules/tool/packages/memmachine/logo.svg b/modules/tool/packages/memmachine/logo.svg new file mode 100644 index 00000000..24cdb4ad --- /dev/null +++ b/modules/tool/packages/memmachine/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/modules/tool/packages/memmachine/package.json b/modules/tool/packages/memmachine/package.json new file mode 100644 index 00000000..55146bef --- /dev/null +++ b/modules/tool/packages/memmachine/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fastgpt-plugins/tool-memmachine", + "module": "index.ts", + "type": "module", + "scripts": { + "build": "bun ../../../../scripts/build.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "zod": "^3.25.76" + } +}