Skip to content

Commit e6cf0d3

Browse files
committed
feat(openviking-memory): add memory_recall tool, rename memory_save_turn, translate docs to English
Add recallMemories method to OpenVikingClient (delegates to searchMemories) Register memory_recall tool for system-triggered context injection Rename memory_save_conversation tool to memory_save_turn Rename saveConversation to saveTurn in client interface and implementation Translate all tool titles, descriptions, and parameter docs from Chinese to English Mark system-only tools (memory_recall, memory_save_turn) with empty activation keywords Update plugin-tools.ts constants to reference memory_recall and memory_save_turn Update Readme with memory_recall, memory_save_turn, session memory processing rules, and system-triggered tool usage notes
1 parent d59db36 commit e6cf0d3

4 files changed

Lines changed: 87 additions & 44 deletions

File tree

apps/stage-tamagotchi/src/main/services/airi/plugins/examples/openviking-memory/docs/Readme.zh-CN.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,21 @@
5151

5252
- **读取记忆**`memory_read`):根据 URI 读取一条记忆的完整内容
5353
- **保存记忆**`memory_save`):将重要信息保存到长期记忆
54-
- **保存对话记录**`memory_save_conversation`):将一轮对话(用户消息 + 助手回复 + 工具调用)保存到长期记忆
54+
- **保存对话记录**`memory_save_turn`):将一轮对话(用户消息 + 助手回复 + 工具调用)保存到长期记忆。此工具由系统自动触发,AI 助手不应主动调用。
55+
- **召回记忆**`memory_recall`):自动召回与当前对话相关的长期记忆,用于上下文注入。此工具由系统自动触发,AI 助手不应主动调用。
5556
- **删除记忆**`memory_delete`):删除指定的记忆条目
5657

58+
> `memory_recall``memory_save_turn` 为系统级工具,由框架在对话处理流程中自动调用,用于上下文注入和对话持久化。AI 助手不会在工具选择列表中看到它们,开发者也不应手动调用。
59+
60+
### Session 记忆处理
61+
62+
对话通过 `memory_save_turn` 保存到 Session 后,系统会按以下规则决定是否将对话内容转化为可检索的记忆:
63+
64+
- **短对话**(累计 pending tokens < 20,000):仅保存到 Session,不参与记忆搜索
65+
- **长对话**(累计 pending tokens ≥ 20,000):自动提交并做 embedding 处理,之后可通过记忆注入(`memory_recall`)或 Agent 主动搜索(`memory_search`)找到
66+
67+
例如,一个持续数小时的长对话(如聊了一整晚)达到阈值后会被 embedding,后续用户提问时系统会自动召回相关记忆片段作为上下文注入。
68+
5769
## 安装
5870

5971
### 前提条件
@@ -119,11 +131,12 @@
119131

120132
加载成功后,AIRI 将获得以下工具能力:
121133

122-
- `memory_search` — 搜索长期记忆
123-
- `memory_read` — 根据 URI 读取记忆完整内容
124-
- `memory_save` — 保存记忆
125-
- `memory_save_conversation` — 保存对话记录
126-
- `memory_delete` — 删除记忆
134+
- `memory_search` — 搜索长期记忆(Agent 主动调用)
135+
- `memory_read` — 根据 URI 读取记忆完整内容(Agent 主动调用)
136+
- `memory_save` — 保存记忆(Agent 主动调用)
137+
- `memory_save_turn` — 保存对话记录(系统自动触发)
138+
- `memory_recall` — 召回记忆(系统自动触发)
139+
- `memory_delete` — 删除记忆(Agent 主动调用)
127140

128141
## 开发
129142

apps/stage-tamagotchi/src/main/services/airi/plugins/examples/openviking-memory/src/index.ts

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,17 @@ export async function setupModules({ apis }: ContextInit): Promise<void> {
2727
await apis.tools.register({
2828
tool: {
2929
id: 'memory_search',
30-
title: '搜索记忆',
31-
description: '搜索长期记忆中的相关信息',
30+
title: 'Search Memory',
31+
description: 'Retrieve specific memory entries from long-term storage by contextual keywords, time references, or event descriptions. Use this when you need to recall factual details (what happened, how something was done), temporal information (specific dates, years, months, days, weekends, weeks, or relative timeframes like "last week" or "yesterday"), or scheduled events (appointments, arrangements, calendar entries, or planned activities). Returns matching memory entries with relevance scores and metadata.',
3232
activation: {
33-
keywords: ['记忆', '回忆', '搜索'],
33+
keywords: ['search', 'find', 'recall', 'look up', 'what', 'when', 'did', 'remember', 'tell me about'],
3434
patterns: [],
3535
},
3636
parameters: {
3737
type: 'object',
3838
properties: {
39-
query: { type: 'string', description: '搜索查询' },
40-
limit: { type: 'number', description: '返回结果数量', default: 5 },
39+
query: { type: 'string', description: 'Natural language search query describing the information to recall, including keywords, time references, event descriptions, or contextual details' },
40+
limit: { type: 'number', description: 'Maximum number of memory results to return', default: 5 },
4141
},
4242
required: ['query'],
4343
},
@@ -52,16 +52,16 @@ export async function setupModules({ apis }: ContextInit): Promise<void> {
5252
await apis.tools.register({
5353
tool: {
5454
id: 'memory_read',
55-
title: '读取记忆',
56-
description: '根据 URI 读取一条记忆的完整内容',
55+
title: 'Read Memory',
56+
description: 'Retrieve the full detailed content of a specific memory entry identified by its URI. Use this when you need to access complete context and details of a previously identified memory item, such as viewing the entire content found through memory_search. This provides the full text and metadata of a single memory entry, as opposed to performing a broader search across memories.',
5757
activation: {
58-
keywords: ['读取', '查看', '阅读'],
58+
keywords: ['read', 'show', 'view', 'get details', 'open', 'display', 'see'],
5959
patterns: [],
6060
},
6161
parameters: {
6262
type: 'object',
6363
properties: {
64-
uri: { type: 'string', description: '记忆的 URI(如 viking://user/default/...' },
64+
uri: { type: 'string', description: 'The URI of the memory entry to read (e.g. viking://user/default/...). Obtain this from memory_search results.' },
6565
},
6666
required: ['uri'],
6767
},
@@ -76,19 +76,19 @@ export async function setupModules({ apis }: ContextInit): Promise<void> {
7676
tool: {
7777
id: 'memory_save',
7878
title: '保存记忆',
79-
description: '保存一条重要信息到长期记忆',
79+
description: 'Store a piece of important information into long-term memory for future recall. Use this when you need to remember factual details, user preferences, key decisions, follow-up tasks, or any information that should be persisted beyond the current conversation. The saved content can later be retrieved using memory_search. Optionally attach tags for better organization and retrieval.',
8080
activation: {
81-
keywords: ['记住', '保存', '记忆'],
81+
keywords: ['save', 'remember', 'store', 'keep', 'record', 'note', 'add', 'learn'],
8282
patterns: [],
8383
},
8484
parameters: {
8585
type: 'object',
8686
properties: {
87-
content: { type: 'string', description: '要记忆的内容' },
87+
content: { type: 'string', description: 'The information content to store in long-term memory' },
8888
tags: {
8989
type: 'array',
9090
items: { type: 'string' },
91-
description: '标签',
91+
description: 'Optional tags for categorizing and organizing memory entries for easier retrieval',
9292
},
9393
},
9494
required: ['content'],
@@ -102,44 +102,69 @@ export async function setupModules({ apis }: ContextInit): Promise<void> {
102102

103103
await apis.tools.register({
104104
tool: {
105-
id: 'memory_save_conversation',
106-
title: '保存对话记录',
107-
description: '将一轮对话(用户消息 + 助手回复 + 工具调用)保存到长期记忆',
105+
id: 'memory_recall',
106+
title: 'Recall Memory',
107+
description: 'Automatically recall long-term memories relevant to the current conversation for context injection. NOTICE: This tool is triggered by the system only and should not be invoked by the AI assistant.',
108108
activation: {
109-
keywords: ['保存对话', '记录对话', '记住对话', '保存聊天', '保存'],
109+
keywords: [],
110110
patterns: [],
111111
},
112112
parameters: {
113113
type: 'object',
114114
properties: {
115-
sessionId: { type: 'string', description: '对话会话 ID,同一对话应使用相同 ID。首次不传会自动创建' },
116-
userMessage: { type: 'string', description: '用户消息' },
117-
assistantResponse: { type: 'string', description: '助手回复' },
118-
toolCalls: { type: 'object', description: '工具调用记录' },
119-
timestamp: { type: 'string', description: '时间戳' },
115+
query: { type: 'string', description: 'Search query describing the context to recall from long-term memory' },
116+
limit: { type: 'number', description: 'Maximum number of memory results to return', default: 5 },
117+
},
118+
required: ['query'],
119+
},
120+
},
121+
execute: async (input: unknown) => {
122+
const { query, limit } = input as { query: string, limit?: number }
123+
const results = await client!.recallMemories(query, limit)
124+
return { results }
125+
},
126+
})
127+
128+
await apis.tools.register({
129+
tool: {
130+
id: 'memory_save_turn',
131+
title: 'Save Conversation Turn',
132+
description: 'Save a conversation turn (user message + assistant response + tool calls) into long-term memory. NOTICE: This tool is triggered by the system only and should not be invoked by the AI assistant.',
133+
activation: {
134+
keywords: [],
135+
patterns: [],
136+
},
137+
parameters: {
138+
type: 'object',
139+
properties: {
140+
sessionId: { type: 'string', description: 'Conversation session ID. Use the same ID for the same conversation; auto-created if not provided' },
141+
userMessage: { type: 'string', description: 'The user message content' },
142+
assistantResponse: { type: 'string', description: 'The assistant response content' },
143+
toolCalls: { type: 'object', description: 'Record of tool calls made during the conversation turn' },
144+
timestamp: { type: 'string', description: 'ISO 8601 timestamp of the conversation turn' },
120145
},
121146
required: ['userMessage', 'assistantResponse', 'timestamp'],
122147
},
123148
},
124149
execute: async (input: unknown) => {
125150
const turn = input as { sessionId?: string, userMessage: string, assistantResponse: string, toolCalls?: unknown[], timestamp: string }
126-
return await client!.saveConversation(turn)
151+
return await client!.saveTurn(turn)
127152
},
128153
})
129154

130155
await apis.tools.register({
131156
tool: {
132157
id: 'memory_delete',
133-
title: '删除记忆',
134-
description: '删除一条记忆',
158+
title: 'Delete Memory',
159+
description: 'Permanently and irreversibly remove a specific memory entry from long-term storage. Use this only when you have explicit intent to discard particular information. This action cannot be undone — the deleted memory entry and all its associated data will be erased permanently.',
135160
activation: {
136-
keywords: ['删除', '移除', '清除'],
161+
keywords: ['delete', 'remove', 'erase', 'forget', 'discard', 'clear'],
137162
patterns: [],
138163
},
139164
parameters: {
140165
type: 'object',
141166
properties: {
142-
id: { type: 'string', description: '要删除的记忆 ID' },
167+
id: { type: 'string', description: 'The unique identifier of the memory entry to permanently delete' },
143168
},
144169
required: ['id'],
145170
},

apps/stage-tamagotchi/src/main/services/airi/plugins/examples/openviking-memory/src/openviking.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ export interface OpenVikingClientConfig {
3636

3737
export interface OpenVikingClient {
3838
searchMemories: (query: string, limit?: number) => Promise<Record<string, unknown>[]>
39+
recallMemories: (query: string, limit?: number) => Promise<Record<string, unknown>[]>
3940
readMemory: (uri: string) => Promise<{ uri: string, content: string }>
4041
addSessionMessage: (sessionId: string, role: string, content: string, createdAt?: string) => Promise<void>
4142
getSession: (sessionId: string) => Promise<SessionInfo>
4243
commitSession: (sessionId: string, keepRecentCount?: number) => Promise<void>
43-
saveConversation: (conversation: ConversationTurn) => Promise<ConversationSaveResult>
44+
saveTurn: (turn: ConversationTurn) => Promise<ConversationSaveResult>
4445
saveMemory: (content: string, tags?: string[]) => Promise<{ id: string }>
4546
deleteMemory: (id: string) => Promise<void>
4647
healthCheck: () => Promise<boolean>
@@ -132,6 +133,10 @@ export function createOpenVikingClient(config: OpenVikingClientConfig): OpenViki
132133
return data.result?.memories ?? []
133134
},
134135

136+
async recallMemories(query: string, limit = 5): Promise<Record<string, unknown>[]> {
137+
return await this.searchMemories(query, limit)
138+
},
139+
135140
async readMemory(uri: string): Promise<{ uri: string, content: string }> {
136141
const params = new URLSearchParams({ uri })
137142
const response = await apiFetch(`/api/v1/content/read?${params}`, {
@@ -199,11 +204,11 @@ export function createOpenVikingClient(config: OpenVikingClientConfig): OpenViki
199204
}
200205
},
201206

202-
async saveConversation(conversation: ConversationTurn): Promise<ConversationSaveResult> {
203-
const sessionId = conversation.sessionId || crypto.randomUUID()
207+
async saveTurn(turn: ConversationTurn): Promise<ConversationSaveResult> {
208+
const sessionId = turn.sessionId || crypto.randomUUID()
204209

205-
await this.addSessionMessage(sessionId, 'user', conversation.userMessage || '(empty)', conversation.timestamp)
206-
await this.addSessionMessage(sessionId, 'assistant', conversation.assistantResponse || '(empty)', conversation.timestamp)
210+
await this.addSessionMessage(sessionId, 'user', turn.userMessage || '(empty)', turn.timestamp)
211+
await this.addSessionMessage(sessionId, 'assistant', turn.assistantResponse || '(empty)', turn.timestamp)
207212

208213
let committed = false
209214
try {

apps/stage-tamagotchi/src/renderer/stores/plugin-tools.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { electronPluginList } from '../../shared/eventa/plugin/host'
1414
import { electronPluginInvokeTool, electronPluginListXsaiTools } from '../../shared/eventa/plugin/tools'
1515

1616
const PLUGIN_CONTEXT_ID_PREFIX = 'system:plugin:'
17-
const MEMORY_SAVE_CONVERSATION_TOOL = 'memory_save_conversation'
18-
const MEMORY_SEARCH_TOOL = 'memory_search'
17+
const MEMORY_SAVE_TURN = 'memory_save_turn'
18+
const MEMORY_RECALL_TOOL = 'memory_recall'
1919
const MEMORY_CONTEXT_TIMEOUT_MS = 3_000
2020

2121
function generateId(): string {
@@ -86,10 +86,10 @@ export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tool
8686
pluginsWithMemorySaveTool.clear()
8787
pluginsWithMemorySearchTool.clear()
8888
for (const tool of definitions.tools) {
89-
if (tool.name === MEMORY_SAVE_CONVERSATION_TOOL) {
89+
if (tool.name === MEMORY_SAVE_TURN) {
9090
pluginsWithMemorySaveTool.add(tool.ownerPluginId)
9191
}
92-
if (tool.name === MEMORY_SEARCH_TOOL) {
92+
if (tool.name === MEMORY_RECALL_TOOL) {
9393
pluginsWithMemorySearchTool.add(tool.ownerPluginId)
9494
}
9595
}
@@ -141,7 +141,7 @@ export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tool
141141
loadedPlugins.map(async (plugin) => {
142142
const result = await invokePluginTool({
143143
ownerPluginId: plugin.name,
144-
name: MEMORY_SEARCH_TOOL,
144+
name: MEMORY_RECALL_TOOL,
145145
input: { query: sendingMessage },
146146
}, { signal: AbortSignal.timeout(MEMORY_CONTEXT_TIMEOUT_MS) })
147147

@@ -220,7 +220,7 @@ export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tool
220220
for (const plugin of loadedPlugins) {
221221
invokePluginTool({
222222
ownerPluginId: plugin.name,
223-
name: MEMORY_SAVE_CONVERSATION_TOOL,
223+
name: MEMORY_SAVE_TURN,
224224
input: turn,
225225
}).catch((err: unknown) => console.warn(`[plugin-tools] failed to save turn to plugin "${plugin.name}"`, err))
226226
}

0 commit comments

Comments
 (0)