hermes-hy-memory 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermes-hy-memory
3
+ Version: 0.1.2
4
+ Summary: HY Memory provider plugin for Hermes Agent (native, 100% passive injection)
5
+ Author: alvinfei
6
+ License: MIT
7
+ Keywords: hermes,memory,llm,agent,plugin
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: hy-memory>=1.2.17
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7.0; extra == "dev"
21
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
22
+
23
+ # HY Memory Provider for Hermes
24
+
25
+ Hermes Agent 的原生 Memory Provider 插件 —— **第一梯队 100% 被动注入**。
26
+
27
+ ## 工作原理
28
+
29
+ Hermes 框架在每次 LLM 调用前自动触发 `prefetch(query)`,本 Provider:
30
+ 1. 用 query 在 HY Memory 里搜(chat 链路三路召回)
31
+ 2. 把命中结果按 layer 格式化(`§ [profile]` / `§ [intent]` / `§`)
32
+ 3. Hermes 把这块文本注入 system prompt
33
+
34
+ **用户无需任何额外操作**,记忆自动工作。
35
+
36
+ 每轮对话结束 Hermes 调 `sync_turn(user, assistant)`,Provider 提交到内部线程池异步写入,不阻塞主流程。
37
+
38
+ ## 安装
39
+
40
+ > SDK 包 `hy-memory` 已发布到**公网 PyPI**(pypi.org),直接 `pip install` 即可,无需任何额外源或凭证。
41
+
42
+ > **chroma 后端需 `sqlite3 >= 3.35`**(默认 `MEMORY_VECTOR_STORE=chroma`)。系统 sqlite 过低
43
+ > (旧版 CentOS/Linux 会报 `unsupported version of sqlite3`)时:`pip install pysqlite3-binary`
44
+ > 并在进程入口 swap(`import sys; sys.modules["sqlite3"]=__import__("pysqlite3")`),
45
+ > 或改用 `MEMORY_VECTOR_STORE=qdrant` / `faiss`。
46
+
47
+ ### 方式 A:通过 `hermes memory setup`(推荐)
48
+
49
+ ```bash
50
+ hermes memory setup hy-memory
51
+ ```
52
+
53
+ Hermes 会按 `plugin.yaml` 的 `pip_dependencies` 自动安装 `hy-memory`,并启动配置向导询问 `HY_MEMORY_USER_ID` / `HY_MEMORY_AGENT_ID` / mode。
54
+
55
+ ### 方式 B:手动 pip install
56
+
57
+ ```bash
58
+ # 装这个 plugin(自动拉 hy-memory 作为依赖)
59
+ pip install hermes-hy-memory
60
+
61
+ # 让 Hermes 启用
62
+ hermes config set memory.provider hy-memory
63
+ ```
64
+
65
+ ### 方式 C:folder-drop(开发调试)
66
+
67
+ ```bash
68
+ cp -r plugins/native/hermes ~/.hermes/plugins/memory/hy-memory
69
+ pip install "hy-memory>=1.2.17"
70
+ ```
71
+
72
+ ### 内网安装(腾讯内部,可选)
73
+
74
+ 腾讯内网用内部包 `hy-memory-internal`(与公网 `hy-memory` 同源,import 同为 `hy_memory`),
75
+ 通过腾讯镜像源安装,无需凭证:
76
+
77
+ ```bash
78
+ pip install "hy-memory-internal>=0.1.5.14" --index-url https://mirrors.tencent.com/pypi/simple/
79
+ ```
80
+
81
+ ## 配置
82
+
83
+ ### 环境变量
84
+
85
+ | 变量 | 必填 | 默认 | 说明 |
86
+ |---|---|---|---|
87
+ | `HY_MEMORY_USER_ID` | ✅ | — | 一级隔离 key(你的记忆命名空间) |
88
+ | `HY_MEMORY_AGENT_ID` | ❌ | `hermes` | 二级隔离 key |
89
+ | `HY_MEMORY_MODE` | ❌ | `pro` | 处理模式:`lite` / `pro` / `ultra` |
90
+ | `HY_MEMORY_PREFETCH_MAX_CHARS` | ❌ | `2000` | prefetch 注入文本总字符数上限 |
91
+ | `HY_MEMORY_SYNC_WORKERS` | ❌ | `2` | sync_turn 后台线程池大小 |
92
+ | `HY_MEMORY_SHUTDOWN_GRACE_SEC` | ❌ | `10` | shutdown 等 in-flight 写入的最长秒数 |
93
+ | `OPENAI_API_KEY`(或对应 LLM/embedder key) | ✅* | — | `pro`/`ultra` 必须;`lite` 仅 embed 用得到 |
94
+
95
+ *HY Memory SDK 自身的 LLM/Embedder 配置(详见 `hy-memory-internal` 文档)。
96
+
97
+ ### `~/.hermes/config.yaml`
98
+
99
+ ```yaml
100
+ memory:
101
+ provider: hy-memory
102
+ ```
103
+
104
+ 环境变量优先级最高,可在 `~/.hermes/.env` 里持久化(`hermes memory setup` 自动写)。
105
+
106
+ ## 处理模式(`HY_MEMORY_MODE`)
107
+
108
+ | 模式 | 写入流程 | 速度 | 召回质量 |
109
+ |---|---|---|---|
110
+ | `lite` | 纯 embedding 写入(不调 LLM) | 最快 | 仅向量相似 |
111
+ | `pro`(默认) | LLM 抽取事实/身份 + reconcile 演化 | 中 | 含画像 + 事实层 |
112
+ | `ultra` | pro + System2 异步认知(Schema/Intention) | 最完整 | + 跨域归纳 + 前瞻意图 |
113
+
114
+ > ⚠️ **`lite` 模式不适用于 Hermes 被动注入 / 检索。** lite 只做 embedding、不经 LLM 抽取,
115
+ > 记忆停留在 `L1_RAW` 层;而 SDK 的 `list` / `search` 会过滤掉 `L1_RAW`,因此 lite 写入的记忆
116
+ > **无法被 `prefetch` 召回**(表现为写入成功但检索恒为空)。Hermes 集成请使用 `pro`(默认)或
117
+ > `ultra`;`lite` 仅适合“只写、不依赖语义召回”的特殊场景。
118
+
119
+ ## CLI
120
+
121
+ `pip install` 后可用 `hermes-hy-memory` 子命令做体检和手动操作(脱离 Hermes 主进程):
122
+
123
+ ```bash
124
+ # 体检:env 齐 / Client 能否构造 / list 能否跑通(只读)
125
+ hermes-hy-memory doctor
126
+
127
+ # 手动写入
128
+ hermes-hy-memory add "我喜欢 K-Pop 但更喜欢 Jazz"
129
+
130
+ # 手动搜索
131
+ hermes-hy-memory search "音乐口味" --limit 5
132
+
133
+ # 列出最近 20 条
134
+ hermes-hy-memory list
135
+
136
+ # 跨用户测试(覆盖 env)
137
+ hermes-hy-memory search "x" --user-id other_user --agent-id test
138
+ ```
139
+
140
+ 被 Hermes 主 CLI 加载后也可:
141
+
142
+ ```bash
143
+ hermes hy-memory doctor
144
+ ```
145
+
146
+ ## Hooks
147
+
148
+ | Hook | 触发时机 | 行为 |
149
+ |---|---|---|
150
+ | `prefetch(query)` | 每次 LLM 调用前 | 搜记忆 → 注入 system prompt |
151
+ | `sync_turn(user, ast)` | 每轮对话结束 | 提交线程池异步写入 |
152
+ | `on_session_end(msgs)` | 会话结束 | 等 in-flight 完成 + 尾部补提取 |
153
+ | `on_pre_compress(msgs)` | 上下文压缩前 | 同 `on_session_end`,保住即将被裁的内容 |
154
+ | `on_memory_write(action, target, content)` | Hermes 内置 memory 命令 | `add` 同步到 HY Memory;`delete` 跳过(target ID 不互通) |
155
+
156
+ ## Tools(LLM 主动调用,可选)
157
+
158
+ | Tool | 用途 |
159
+ |---|---|
160
+ | `memory_search(query, limit)` | 搜索记忆 |
161
+ | `memory_add(content)` | 写入一条记忆 |
162
+ | `memory_delete(memory_id)` | 删除一条记忆 |
163
+ | `memory_list(limit)` | 列出当前 user/agent 的记忆 |
164
+
165
+ 即使关掉 tools,prefetch 的被动注入也保证每次 LLM 调用都能看到相关记忆。
166
+
167
+ ## 故障排查
168
+
169
+ ### `provider not initialized` 或所有 hook 静默 no-op
170
+
171
+ 跑一遍体检:
172
+ ```bash
173
+ hermes-hy-memory doctor
174
+ ```
175
+
176
+ 常见原因:
177
+ 1. `HY_MEMORY_USER_ID` 没设
178
+ 2. embed/LLM key 没设(`OPENAI_API_KEY` 等)→ `HyMemoryClient(mode="pro")` 构造失败
179
+ 3. SDK 没装:`pip install hy-memory`(内网:`pip install hy-memory-internal --index-url https://mirrors.tencent.com/pypi/simple/`)
180
+
181
+ ### prefetch 没注入任何记忆
182
+
183
+ - 库里**真没相关记忆** —— 用 `hermes-hy-memory list` 确认
184
+ - query 太短(< 3 字符)或在跳过列表里(`ok`/`好的`/`thanks` 等)—— 设计如此
185
+ - 中文 query 命中率低?SDK ≥ 0.1.5.11.dev2 已修过 BM25 中文分词不对称的 bug,确认版本
186
+
187
+ ### sync_turn 似乎没写入
188
+
189
+ - 默认 daemon 线程,主进程退出会被 kill。生产部署用 `hermes` daemon 模式
190
+ - 提高 `HY_MEMORY_SHUTDOWN_GRACE_SEC` 让 shutdown 多等几秒
191
+ - 检查日志 `[hermes] sync_turn failed: ...`
192
+
193
+ ### `cross-loop` 错误(多 client 场景)
194
+
195
+ 如果你的 Hermes 部署同时跑了多个 `HyMemoryClient` 实例(比如多租户 server),需要 SDK ≥ 0.1.5.11.dev2 + 用 `SharedRuntime`:
196
+
197
+ ```python
198
+ from hy_memory import HyMemoryClient, SharedRuntime
199
+ runtime = await SharedRuntime.create(base_config)
200
+ client = HyMemoryClient(cfg, runtime=runtime)
201
+ ```
202
+
203
+ 本 plugin 单 Hermes 进程默认走 solo mode 的 client,不需要这个。
204
+
205
+ ## 与 Mem0 风格的对比
206
+
207
+ [Mem0 的 Hermes 集成](https://github.com/mem0ai/mem0)依赖 **TypeScript SDK**,本插件用 **Python SDK**。HY Memory 提供 lite/pro/ultra 三档处理深度(lite 不调 LLM,pro 标准抽取,ultra 加 System2 认知);Mem0 是单档 LLM 抽取。
208
+
209
+ ## 开发
210
+
211
+ ```bash
212
+ cd plugins/native/hermes
213
+ python -m pytest tests/ -v
214
+ ```
215
+
216
+ 测试 mock 掉 `HyMemoryClient`,无外部依赖(不需要 OPENAI_API_KEY、不需要起 Qdrant)。
217
+
218
+ ## 版本
219
+
220
+ | Plugin | SDK 依赖 | 备注 |
221
+ |---|---|---|
222
+ | 0.1.2 | `hy-memory>=1.2.17`(内网 `hy-memory-internal>=0.1.5.14`) | 发布到公网 PyPI(`hermes-hy-memory`);channel-dict flatten 修复 |
223
+ | 0.1.0 | `hy-memory-internal>=0.1.5.11.dev2` | 含 SharedRuntime / BM25 中文修复 / Chroma keyword_search / stdout 日志 |
@@ -0,0 +1,201 @@
1
+ # HY Memory Provider for Hermes
2
+
3
+ Hermes Agent 的原生 Memory Provider 插件 —— **第一梯队 100% 被动注入**。
4
+
5
+ ## 工作原理
6
+
7
+ Hermes 框架在每次 LLM 调用前自动触发 `prefetch(query)`,本 Provider:
8
+ 1. 用 query 在 HY Memory 里搜(chat 链路三路召回)
9
+ 2. 把命中结果按 layer 格式化(`§ [profile]` / `§ [intent]` / `§`)
10
+ 3. Hermes 把这块文本注入 system prompt
11
+
12
+ **用户无需任何额外操作**,记忆自动工作。
13
+
14
+ 每轮对话结束 Hermes 调 `sync_turn(user, assistant)`,Provider 提交到内部线程池异步写入,不阻塞主流程。
15
+
16
+ ## 安装
17
+
18
+ > SDK 包 `hy-memory` 已发布到**公网 PyPI**(pypi.org),直接 `pip install` 即可,无需任何额外源或凭证。
19
+
20
+ > **chroma 后端需 `sqlite3 >= 3.35`**(默认 `MEMORY_VECTOR_STORE=chroma`)。系统 sqlite 过低
21
+ > (旧版 CentOS/Linux 会报 `unsupported version of sqlite3`)时:`pip install pysqlite3-binary`
22
+ > 并在进程入口 swap(`import sys; sys.modules["sqlite3"]=__import__("pysqlite3")`),
23
+ > 或改用 `MEMORY_VECTOR_STORE=qdrant` / `faiss`。
24
+
25
+ ### 方式 A:通过 `hermes memory setup`(推荐)
26
+
27
+ ```bash
28
+ hermes memory setup hy-memory
29
+ ```
30
+
31
+ Hermes 会按 `plugin.yaml` 的 `pip_dependencies` 自动安装 `hy-memory`,并启动配置向导询问 `HY_MEMORY_USER_ID` / `HY_MEMORY_AGENT_ID` / mode。
32
+
33
+ ### 方式 B:手动 pip install
34
+
35
+ ```bash
36
+ # 装这个 plugin(自动拉 hy-memory 作为依赖)
37
+ pip install hermes-hy-memory
38
+
39
+ # 让 Hermes 启用
40
+ hermes config set memory.provider hy-memory
41
+ ```
42
+
43
+ ### 方式 C:folder-drop(开发调试)
44
+
45
+ ```bash
46
+ cp -r plugins/native/hermes ~/.hermes/plugins/memory/hy-memory
47
+ pip install "hy-memory>=1.2.17"
48
+ ```
49
+
50
+ ### 内网安装(腾讯内部,可选)
51
+
52
+ 腾讯内网用内部包 `hy-memory-internal`(与公网 `hy-memory` 同源,import 同为 `hy_memory`),
53
+ 通过腾讯镜像源安装,无需凭证:
54
+
55
+ ```bash
56
+ pip install "hy-memory-internal>=0.1.5.14" --index-url https://mirrors.tencent.com/pypi/simple/
57
+ ```
58
+
59
+ ## 配置
60
+
61
+ ### 环境变量
62
+
63
+ | 变量 | 必填 | 默认 | 说明 |
64
+ |---|---|---|---|
65
+ | `HY_MEMORY_USER_ID` | ✅ | — | 一级隔离 key(你的记忆命名空间) |
66
+ | `HY_MEMORY_AGENT_ID` | ❌ | `hermes` | 二级隔离 key |
67
+ | `HY_MEMORY_MODE` | ❌ | `pro` | 处理模式:`lite` / `pro` / `ultra` |
68
+ | `HY_MEMORY_PREFETCH_MAX_CHARS` | ❌ | `2000` | prefetch 注入文本总字符数上限 |
69
+ | `HY_MEMORY_SYNC_WORKERS` | ❌ | `2` | sync_turn 后台线程池大小 |
70
+ | `HY_MEMORY_SHUTDOWN_GRACE_SEC` | ❌ | `10` | shutdown 等 in-flight 写入的最长秒数 |
71
+ | `OPENAI_API_KEY`(或对应 LLM/embedder key) | ✅* | — | `pro`/`ultra` 必须;`lite` 仅 embed 用得到 |
72
+
73
+ *HY Memory SDK 自身的 LLM/Embedder 配置(详见 `hy-memory-internal` 文档)。
74
+
75
+ ### `~/.hermes/config.yaml`
76
+
77
+ ```yaml
78
+ memory:
79
+ provider: hy-memory
80
+ ```
81
+
82
+ 环境变量优先级最高,可在 `~/.hermes/.env` 里持久化(`hermes memory setup` 自动写)。
83
+
84
+ ## 处理模式(`HY_MEMORY_MODE`)
85
+
86
+ | 模式 | 写入流程 | 速度 | 召回质量 |
87
+ |---|---|---|---|
88
+ | `lite` | 纯 embedding 写入(不调 LLM) | 最快 | 仅向量相似 |
89
+ | `pro`(默认) | LLM 抽取事实/身份 + reconcile 演化 | 中 | 含画像 + 事实层 |
90
+ | `ultra` | pro + System2 异步认知(Schema/Intention) | 最完整 | + 跨域归纳 + 前瞻意图 |
91
+
92
+ > ⚠️ **`lite` 模式不适用于 Hermes 被动注入 / 检索。** lite 只做 embedding、不经 LLM 抽取,
93
+ > 记忆停留在 `L1_RAW` 层;而 SDK 的 `list` / `search` 会过滤掉 `L1_RAW`,因此 lite 写入的记忆
94
+ > **无法被 `prefetch` 召回**(表现为写入成功但检索恒为空)。Hermes 集成请使用 `pro`(默认)或
95
+ > `ultra`;`lite` 仅适合“只写、不依赖语义召回”的特殊场景。
96
+
97
+ ## CLI
98
+
99
+ `pip install` 后可用 `hermes-hy-memory` 子命令做体检和手动操作(脱离 Hermes 主进程):
100
+
101
+ ```bash
102
+ # 体检:env 齐 / Client 能否构造 / list 能否跑通(只读)
103
+ hermes-hy-memory doctor
104
+
105
+ # 手动写入
106
+ hermes-hy-memory add "我喜欢 K-Pop 但更喜欢 Jazz"
107
+
108
+ # 手动搜索
109
+ hermes-hy-memory search "音乐口味" --limit 5
110
+
111
+ # 列出最近 20 条
112
+ hermes-hy-memory list
113
+
114
+ # 跨用户测试(覆盖 env)
115
+ hermes-hy-memory search "x" --user-id other_user --agent-id test
116
+ ```
117
+
118
+ 被 Hermes 主 CLI 加载后也可:
119
+
120
+ ```bash
121
+ hermes hy-memory doctor
122
+ ```
123
+
124
+ ## Hooks
125
+
126
+ | Hook | 触发时机 | 行为 |
127
+ |---|---|---|
128
+ | `prefetch(query)` | 每次 LLM 调用前 | 搜记忆 → 注入 system prompt |
129
+ | `sync_turn(user, ast)` | 每轮对话结束 | 提交线程池异步写入 |
130
+ | `on_session_end(msgs)` | 会话结束 | 等 in-flight 完成 + 尾部补提取 |
131
+ | `on_pre_compress(msgs)` | 上下文压缩前 | 同 `on_session_end`,保住即将被裁的内容 |
132
+ | `on_memory_write(action, target, content)` | Hermes 内置 memory 命令 | `add` 同步到 HY Memory;`delete` 跳过(target ID 不互通) |
133
+
134
+ ## Tools(LLM 主动调用,可选)
135
+
136
+ | Tool | 用途 |
137
+ |---|---|
138
+ | `memory_search(query, limit)` | 搜索记忆 |
139
+ | `memory_add(content)` | 写入一条记忆 |
140
+ | `memory_delete(memory_id)` | 删除一条记忆 |
141
+ | `memory_list(limit)` | 列出当前 user/agent 的记忆 |
142
+
143
+ 即使关掉 tools,prefetch 的被动注入也保证每次 LLM 调用都能看到相关记忆。
144
+
145
+ ## 故障排查
146
+
147
+ ### `provider not initialized` 或所有 hook 静默 no-op
148
+
149
+ 跑一遍体检:
150
+ ```bash
151
+ hermes-hy-memory doctor
152
+ ```
153
+
154
+ 常见原因:
155
+ 1. `HY_MEMORY_USER_ID` 没设
156
+ 2. embed/LLM key 没设(`OPENAI_API_KEY` 等)→ `HyMemoryClient(mode="pro")` 构造失败
157
+ 3. SDK 没装:`pip install hy-memory`(内网:`pip install hy-memory-internal --index-url https://mirrors.tencent.com/pypi/simple/`)
158
+
159
+ ### prefetch 没注入任何记忆
160
+
161
+ - 库里**真没相关记忆** —— 用 `hermes-hy-memory list` 确认
162
+ - query 太短(< 3 字符)或在跳过列表里(`ok`/`好的`/`thanks` 等)—— 设计如此
163
+ - 中文 query 命中率低?SDK ≥ 0.1.5.11.dev2 已修过 BM25 中文分词不对称的 bug,确认版本
164
+
165
+ ### sync_turn 似乎没写入
166
+
167
+ - 默认 daemon 线程,主进程退出会被 kill。生产部署用 `hermes` daemon 模式
168
+ - 提高 `HY_MEMORY_SHUTDOWN_GRACE_SEC` 让 shutdown 多等几秒
169
+ - 检查日志 `[hermes] sync_turn failed: ...`
170
+
171
+ ### `cross-loop` 错误(多 client 场景)
172
+
173
+ 如果你的 Hermes 部署同时跑了多个 `HyMemoryClient` 实例(比如多租户 server),需要 SDK ≥ 0.1.5.11.dev2 + 用 `SharedRuntime`:
174
+
175
+ ```python
176
+ from hy_memory import HyMemoryClient, SharedRuntime
177
+ runtime = await SharedRuntime.create(base_config)
178
+ client = HyMemoryClient(cfg, runtime=runtime)
179
+ ```
180
+
181
+ 本 plugin 单 Hermes 进程默认走 solo mode 的 client,不需要这个。
182
+
183
+ ## 与 Mem0 风格的对比
184
+
185
+ [Mem0 的 Hermes 集成](https://github.com/mem0ai/mem0)依赖 **TypeScript SDK**,本插件用 **Python SDK**。HY Memory 提供 lite/pro/ultra 三档处理深度(lite 不调 LLM,pro 标准抽取,ultra 加 System2 认知);Mem0 是单档 LLM 抽取。
186
+
187
+ ## 开发
188
+
189
+ ```bash
190
+ cd plugins/native/hermes
191
+ python -m pytest tests/ -v
192
+ ```
193
+
194
+ 测试 mock 掉 `HyMemoryClient`,无外部依赖(不需要 OPENAI_API_KEY、不需要起 Qdrant)。
195
+
196
+ ## 版本
197
+
198
+ | Plugin | SDK 依赖 | 备注 |
199
+ |---|---|---|
200
+ | 0.1.2 | `hy-memory>=1.2.17`(内网 `hy-memory-internal>=0.1.5.14`) | 发布到公网 PyPI(`hermes-hy-memory`);channel-dict flatten 修复 |
201
+ | 0.1.0 | `hy-memory-internal>=0.1.5.11.dev2` | 含 SharedRuntime / BM25 中文修复 / Chroma keyword_search / stdout 日志 |
@@ -0,0 +1,15 @@
1
+ """
2
+ HY Memory Provider for Hermes Agent
3
+
4
+ 第一梯队原生插件 — 实现 Hermes MemoryProvider 接口,
5
+ 每次请求自动 prefetch 相关记忆注入 system prompt(100% 被动注入)。
6
+
7
+ 安装:pip install hy-mem-internal
8
+ 配置:~/.hermes/config.yaml → memory.provider: hy-memory
9
+ """
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ from .provider import HyMemoryProvider, register
14
+
15
+ __all__ = ["HyMemoryProvider", "register"]
@@ -0,0 +1,259 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ HY Memory CLI for Hermes — `hermes hy-memory <subcommand>`
4
+
5
+ 子命令:
6
+ doctor 连通性 + 配置体检(不写不删,只读)
7
+ add <text> 手动写入一条记忆
8
+ search <query> 手动搜索
9
+ list 列出最近 N 条记忆
10
+
11
+ Hermes 在 plugin 加载时调用 register_cli(subparser) 把以上 subcommand
12
+ 挂到 hermes 主 CLI 上;只有当 provider 已配置(HY_MEMORY_USER_ID 设置)
13
+ 时才激活。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import sys
22
+ from typing import Optional
23
+
24
+
25
+ def register_cli(subparser: argparse._SubParsersAction) -> None:
26
+ """
27
+ Hermes 插件 CLI 注册入口。
28
+
29
+ 会被 hermes 主 CLI 在加载插件时调用:
30
+ ` hermes hy-memory doctor / add / search / list `
31
+ """
32
+ p = subparser.add_parser(
33
+ "hy-memory",
34
+ help="HY Memory plugin CLI (doctor / add / search / list)",
35
+ )
36
+ sub = p.add_subparsers(dest="hy_memory_cmd", required=True)
37
+
38
+ p_doctor = sub.add_parser("doctor", help="Health check (read-only diagnostic)")
39
+ p_doctor.set_defaults(func=_cmd_doctor)
40
+
41
+ p_add = sub.add_parser("add", help="Manually add a memory")
42
+ p_add.add_argument("text", help="Memory content")
43
+ p_add.add_argument("--user-id", help="Override HY_MEMORY_USER_ID")
44
+ p_add.add_argument("--agent-id", help="Override HY_MEMORY_AGENT_ID")
45
+ p_add.set_defaults(func=_cmd_add)
46
+
47
+ p_search = sub.add_parser("search", help="Search memories")
48
+ p_search.add_argument("query", help="Search query")
49
+ p_search.add_argument("--limit", type=int, default=10)
50
+ p_search.add_argument("--user-id", help="Override HY_MEMORY_USER_ID")
51
+ p_search.add_argument("--agent-id", help="Override HY_MEMORY_AGENT_ID")
52
+ p_search.set_defaults(func=_cmd_search)
53
+
54
+ p_list = sub.add_parser("list", help="List recent memories")
55
+ p_list.add_argument("--limit", type=int, default=20)
56
+ p_list.add_argument("--user-id", help="Override HY_MEMORY_USER_ID")
57
+ p_list.add_argument("--agent-id", help="Override HY_MEMORY_AGENT_ID")
58
+ p_list.set_defaults(func=_cmd_list)
59
+
60
+
61
+ # ================================================================
62
+ # Helpers
63
+ # ================================================================
64
+
65
+ def _resolve_ids(args) -> tuple[str, str, str]:
66
+ """从 args + env 解析 user_id / agent_id / mode(顺序:CLI flag > env)"""
67
+ user_id = (getattr(args, "user_id", None) or os.environ.get("HY_MEMORY_USER_ID", "")).strip()
68
+ agent_id = (getattr(args, "agent_id", None) or os.environ.get("HY_MEMORY_AGENT_ID", "hermes")).strip() or "hermes"
69
+ mode = os.environ.get("HY_MEMORY_MODE", "pro").strip() or "pro"
70
+ return user_id, agent_id, mode
71
+
72
+
73
+ def _make_client(mode: str):
74
+ try:
75
+ from hy_memory import HyMemoryClient
76
+ except ImportError as e:
77
+ print(f"ERROR: hy-memory SDK not installed: {e}", file=sys.stderr)
78
+ print("Hint: pip install hy-memory (内网: pip install hy-memory-internal --index-url https://mirrors.tencent.com/pypi/simple/)", file=sys.stderr)
79
+ sys.exit(2)
80
+ try:
81
+ return HyMemoryClient(mode=mode)
82
+ except Exception as e:
83
+ print(f"ERROR: HyMemoryClient init failed: {e}", file=sys.stderr)
84
+ sys.exit(2)
85
+
86
+
87
+ def _print_kv(label: str, value, ok: Optional[bool] = None) -> None:
88
+ """[ok]/[fail]/[--] label: value"""
89
+ if ok is True:
90
+ prefix = " ✓ "
91
+ elif ok is False:
92
+ prefix = " ✗ "
93
+ else:
94
+ prefix = " "
95
+ print(f"{prefix}{label}: {value}")
96
+
97
+
98
+ # ================================================================
99
+ # Subcommands
100
+ # ================================================================
101
+
102
+ def _cmd_doctor(args) -> int:
103
+ """Health check — 完全只读。报:env 是否齐 / Client 能否构造 / list 能否跑通。"""
104
+ print("HY Memory Provider — Doctor")
105
+ print("=" * 60)
106
+
107
+ user_id, agent_id, mode = _resolve_ids(args)
108
+
109
+ # 1) Env vars
110
+ print("\n[1/3] Environment")
111
+ _print_kv("HY_MEMORY_USER_ID", user_id or "(unset)", ok=bool(user_id))
112
+ _print_kv("HY_MEMORY_AGENT_ID", agent_id, ok=True)
113
+ _print_kv("HY_MEMORY_MODE", mode, ok=mode in ("lite", "pro", "ultra"))
114
+
115
+ embed_keys = [
116
+ "OPENAI_API_KEY",
117
+ "MEMORY_EMBEDDER_API_KEY",
118
+ "AZURE_OPENAI_API_KEY",
119
+ ]
120
+ has_embed = any(os.environ.get(k) for k in embed_keys)
121
+ _print_kv(
122
+ "Embedder API key",
123
+ "set" if has_embed else "(none of " + "/".join(embed_keys) + " set)",
124
+ ok=has_embed,
125
+ )
126
+
127
+ if not user_id:
128
+ print("\nFAIL: HY_MEMORY_USER_ID is required.", file=sys.stderr)
129
+ return 2
130
+
131
+ # 2) Client init
132
+ print("\n[2/3] Client init")
133
+ try:
134
+ from hy_memory import HyMemoryClient
135
+ client = HyMemoryClient(mode=mode)
136
+ _print_kv("HyMemoryClient", "ready", ok=True)
137
+ except Exception as e:
138
+ _print_kv("HyMemoryClient", f"FAIL: {e}", ok=False)
139
+ return 2
140
+
141
+ # 3) Probe list
142
+ print("\n[3/3] List probe")
143
+ try:
144
+ result = client.list_memories(user_id=user_id, agent_id=agent_id, limit=1)
145
+ vdb = result.get("vdb", {}) or {}
146
+ total = vdb.get("total", 0)
147
+ _print_kv(f"list_memories(user={user_id}, agent={agent_id})", f"total={total}", ok=True)
148
+ except Exception as e:
149
+ _print_kv("list_memories probe", f"FAIL: {e}", ok=False)
150
+ client.close()
151
+ return 2
152
+
153
+ client.close()
154
+ print("\nAll checks passed.")
155
+ return 0
156
+
157
+
158
+ def _cmd_add(args) -> int:
159
+ user_id, agent_id, mode = _resolve_ids(args)
160
+ if not user_id:
161
+ print("ERROR: HY_MEMORY_USER_ID required", file=sys.stderr)
162
+ return 2
163
+
164
+ client = _make_client(mode)
165
+ try:
166
+ result = client.add(
167
+ args.text,
168
+ user_id=user_id,
169
+ agent_id=agent_id,
170
+ session_id="hermes_cli",
171
+ )
172
+ print(json.dumps(result, ensure_ascii=False, indent=2))
173
+ return 0 if result.get("success") else 1
174
+ finally:
175
+ client.close()
176
+
177
+
178
+ def _flatten_memories(memories):
179
+ """把 search() 返回统一拍平成 list。
180
+
181
+ SDK search() chat 路径返回按通道分组的 dict
182
+ {'profile': [...], 'proactive': [...], 'normal': [...]};旧契约为扁平 list。
183
+ 三路 layer 互斥,无需去重;顺序 profile→proactive→normal。
184
+ """
185
+ if isinstance(memories, dict):
186
+ out = []
187
+ for ch in ("profile", "proactive", "normal"):
188
+ out.extend(memories.get(ch) or [])
189
+ return out
190
+ return memories or []
191
+
192
+
193
+ def _cmd_search(args) -> int:
194
+ user_id, agent_id, mode = _resolve_ids(args)
195
+ if not user_id:
196
+ print("ERROR: HY_MEMORY_USER_ID required", file=sys.stderr)
197
+ return 2
198
+
199
+ client = _make_client(mode)
200
+ try:
201
+ result = client.search(
202
+ args.query,
203
+ user_ids=[user_id],
204
+ agent_ids=[agent_id],
205
+ limit=args.limit,
206
+ )
207
+ memories = _flatten_memories(result.get("memories"))
208
+ print(f"Found {len(memories)} memories for query={args.query!r}")
209
+ for i, m in enumerate(memories, 1):
210
+ score = m.get("score", 0)
211
+ layer = m.get("layer", "")
212
+ content = m.get("content", "")
213
+ print(f"\n[{i}] score={score:.3f} layer={layer}")
214
+ print(f" {content}")
215
+ return 0
216
+ finally:
217
+ client.close()
218
+
219
+
220
+ def _cmd_list(args) -> int:
221
+ user_id, agent_id, mode = _resolve_ids(args)
222
+ if not user_id:
223
+ print("ERROR: HY_MEMORY_USER_ID required", file=sys.stderr)
224
+ return 2
225
+
226
+ client = _make_client(mode)
227
+ try:
228
+ result = client.list_memories(
229
+ user_id=user_id, agent_id=agent_id, limit=args.limit,
230
+ )
231
+ vdb = result.get("vdb", {}) or {}
232
+ memories = vdb.get("memories") or []
233
+ total = vdb.get("total", 0)
234
+ print(f"Listing {len(memories)}/{total} memories (user={user_id} agent={agent_id})")
235
+ for i, m in enumerate(memories, 1):
236
+ mid = m.get("memory_id", "")
237
+ layer = m.get("layer", "")
238
+ content = m.get("content", "")
239
+ print(f"\n[{i}] {mid} layer={layer}")
240
+ print(f" {content}")
241
+ return 0
242
+ finally:
243
+ client.close()
244
+
245
+
246
+ # ================================================================
247
+ # Standalone entry — 当 plugin 不通过 hermes 加载时也能直接 python -m
248
+ # ================================================================
249
+
250
+ def _main_standalone() -> int:
251
+ parser = argparse.ArgumentParser(prog="hermes-hy-memory")
252
+ sub = parser.add_subparsers(dest="cmd", required=True)
253
+ register_cli(sub)
254
+ args = parser.parse_args()
255
+ return args.func(args)
256
+
257
+
258
+ if __name__ == "__main__":
259
+ sys.exit(_main_standalone())