amrita_plugin_memory 0.1.2__tar.gz → 0.1.4__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.
- amrita_plugin_memory-0.1.4/PKG-INFO +187 -0
- amrita_plugin_memory-0.1.4/README.md +172 -0
- amrita_plugin_memory-0.1.4/amrita_plugin_memory/__init__.py +8 -0
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory/config.py +7 -1
- amrita_plugin_memory-0.1.4/amrita_plugin_memory/matchers.py +170 -0
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory/tools.py +28 -8
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory/vector.py +3 -3
- amrita_plugin_memory-0.1.4/amrita_plugin_memory.egg-info/PKG-INFO +187 -0
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory.egg-info/SOURCES.txt +1 -0
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/pyproject.toml +1 -1
- amrita_plugin_memory-0.1.2/PKG-INFO +0 -187
- amrita_plugin_memory-0.1.2/README.md +0 -172
- amrita_plugin_memory-0.1.2/amrita_plugin_memory/__init__.py +0 -7
- amrita_plugin_memory-0.1.2/amrita_plugin_memory.egg-info/PKG-INFO +0 -187
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/LICENSE +0 -0
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory/embed.py +0 -0
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory.egg-info/dependency_links.txt +0 -0
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory.egg-info/requires.txt +0 -0
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory.egg-info/top_level.txt +0 -0
- {amrita_plugin_memory-0.1.2 → amrita_plugin_memory-0.1.4}/setup.cfg +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: amrita_plugin_memory
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Amrita长期记忆插件
|
|
5
|
+
Requires-Python: <3.14,>=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: aiohttp>=3.13.3
|
|
9
|
+
Requires-Dist: aiologic>=0.16.0
|
|
10
|
+
Requires-Dist: amrita-core>=0.8.5
|
|
11
|
+
Requires-Dist: amrita>=1.3
|
|
12
|
+
Requires-Dist: chromadb>=1.5.8
|
|
13
|
+
Requires-Dist: nonebot-plugin-localstore>=0.7.4
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# amrita_plugin_memory
|
|
17
|
+
|
|
18
|
+
Amrita 长期记忆插件 —— 基于 ChromaDB 向量数据库的智能记忆管理系统,让 AI 助手能够持久化地记住关于用户的重要信息。
|
|
19
|
+
|
|
20
|
+
## 技术栈
|
|
21
|
+
|
|
22
|
+
| 组件 | 技术 |
|
|
23
|
+
| ---------- | ------------------------------------------------------- |
|
|
24
|
+
| 后端框架 | Python 3.10+ / NoneBot2 / Amrita Core |
|
|
25
|
+
| 向量数据库 | ChromaDB(支持本地 PersistentClient 与远程 HttpClient) |
|
|
26
|
+
| 嵌入模型 | OpenAI Embedding API / Ollama Embedding API |
|
|
27
|
+
| 配置管理 | Pydantic + TOML |
|
|
28
|
+
| 异步处理 | asyncio + aiologic + aiohttp |
|
|
29
|
+
| 代码质量 | Ruff + Pyright |
|
|
30
|
+
|
|
31
|
+
## 架构概览
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
amrita_plugin_memory/
|
|
35
|
+
├── __init__.py # 插件入口,注册依赖
|
|
36
|
+
├── config.py # 配置模型(ConfigFile / EnvConfig / DataManager)
|
|
37
|
+
├── embed.py # Ollama 嵌入适配器
|
|
38
|
+
├── vector.py # ChromaDB 异步封装(WrappedClientAPI / AsyncUserMemory)
|
|
39
|
+
└── tools.py # 工具函数定义与注册(write/read/update/delete/list)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **config.py**: 定义配置文件(过期天数、记忆上限)和环境变量(数据库类型、嵌入模型地址等),通过 Pydantic 模型校验。
|
|
43
|
+
- **embed.py**: 实现 Ollama 协议的嵌入适配器,通过 HTTP API 调用 `/api/embed` 端点。
|
|
44
|
+
- **vector.py**: 对 ChromaDB 原生 API 进行 `asyncio.to_thread` 封装,提供异步的集合管理与记忆 CRUD。使用 `aiologic.Lock` 按用户粒度加锁,保证并发安全。
|
|
45
|
+
- **tools.py**: 定义 5 个 Function Calling 工具(`write_memory` / `read_memory` / `update_memory` / `delete_memory` / `list_memory`),注册到 Amrita 的工具系统中。通过 `scope` 参数支持群共享记忆与用户专属记忆的分区隔离。
|
|
46
|
+
|
|
47
|
+
## 功能特点
|
|
48
|
+
|
|
49
|
+
- **长期记忆存储**:将用户偏好、项目信息等持久化存入 ChromaDB 向量数据库
|
|
50
|
+
- **群/用户记忆隔离**:LLM 可通过 `scope` 参数选择存入群共享记忆(群内所有人可见)或用户专属记忆(仅自己可见)
|
|
51
|
+
- **语义检索**:基于嵌入向量的语义相似度检索,支持中英文关键词
|
|
52
|
+
- **智能标签**:支持使用自定义标签对记忆进行分类(如 `preference`、`project`)
|
|
53
|
+
- **重要性分级**:三级重要程度(`low` / `medium` / `high`),支持按级别过滤检索
|
|
54
|
+
- **完整 CRUD**:支持记忆的创建、查询、更新、删除和列表查看
|
|
55
|
+
- **并发安全**:按用户 ID 粒度加锁,确保同一用户的操作串行化
|
|
56
|
+
- **多租户支持**:通过 ChromaDB remote 模式支持远程向量数据库
|
|
57
|
+
|
|
58
|
+
## 工具函数
|
|
59
|
+
|
|
60
|
+
### 1. `write_memory` —— 写入记忆
|
|
61
|
+
|
|
62
|
+
将当前用户(或群组)的重要信息存入长期记忆。
|
|
63
|
+
|
|
64
|
+
| 参数 | 类型 | 必需 | 描述 |
|
|
65
|
+
| ------------ | ------------------------------- | ---- | ----------------------------------------------------- |
|
|
66
|
+
| `content` | string | ✅ | 记忆内容,简洁明了 |
|
|
67
|
+
| `tags` | string | ✅ | 分类标签,如 `preference`、`project` |
|
|
68
|
+
| `importance` | `"low"` / `"medium"` / `"high"` | ✅ | 重要性等级 |
|
|
69
|
+
| `scope` | `"group"` / `"user"` | ✅ | `group`=群共享,`user`=个人专属(私聊不可用 `group`) |
|
|
70
|
+
|
|
71
|
+
### 2. `read_memory` —— 检索记忆
|
|
72
|
+
|
|
73
|
+
从记忆库中按语义相似度检索用户相关信息。
|
|
74
|
+
|
|
75
|
+
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|
|
76
|
+
| ------------ | ------------------------------- | ---- | ------ | -------------------------- |
|
|
77
|
+
| `query` | string | ✅ | — | 关键词字符串,空格分隔 |
|
|
78
|
+
| `top_k` | integer | ❌ | `5` | 返回结果数量 |
|
|
79
|
+
| `importance` | `"low"` / `"medium"` / `"high"` | ❌ | — | 按重要性过滤 |
|
|
80
|
+
| `scope` | `"group"` / `"user"` | ✅ | — | 检索范围:群共享或个人专属 |
|
|
81
|
+
|
|
82
|
+
### 3. `update_memory` —— 更新记忆
|
|
83
|
+
|
|
84
|
+
更新指定 ID 的记忆内容、标签或重要性。
|
|
85
|
+
|
|
86
|
+
| 参数 | 类型 | 必需 | 描述 |
|
|
87
|
+
| ------------ | ------------------------------- | ---- | --------------- |
|
|
88
|
+
| `id` | string | ✅ | 要更新的记忆 ID |
|
|
89
|
+
| `scope` | `"group"` / `"user"` | ✅ | 记忆所属范围 |
|
|
90
|
+
| `content` | string | ❌ | 新的记忆内容 |
|
|
91
|
+
| `tags` | string | ❌ | 新的标签 |
|
|
92
|
+
| `importance` | `"low"` / `"medium"` / `"high"` | ❌ | 新的重要性等级 |
|
|
93
|
+
|
|
94
|
+
### 4. `delete_memory` —— 删除记忆
|
|
95
|
+
|
|
96
|
+
删除指定 ID 的记忆。
|
|
97
|
+
|
|
98
|
+
| 参数 | 类型 | 必需 | 描述 |
|
|
99
|
+
| ------- | -------------------- | ---- | --------------- |
|
|
100
|
+
| `id` | string | ✅ | 要删除的记忆 ID |
|
|
101
|
+
| `scope` | `"group"` / `"user"` | ✅ | 记忆所属范围 |
|
|
102
|
+
|
|
103
|
+
### 5. `list_memory` —— 列出记忆
|
|
104
|
+
|
|
105
|
+
列出当前用户的所有记忆(返回 scope、标签和 ID)。
|
|
106
|
+
|
|
107
|
+
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|
|
108
|
+
| ------- | -------------------- | ---- | ------ | -------------------------- |
|
|
109
|
+
| `limit` | integer | ❌ | `5` | 返回数量限制 |
|
|
110
|
+
| `scope` | `"group"` / `"user"` | ✅ | — | 列出范围:群共享或个人专属 |
|
|
111
|
+
|
|
112
|
+
## 配置选项
|
|
113
|
+
|
|
114
|
+
### 文件配置(`config/amrita_plugin_memory/config.toml`)
|
|
115
|
+
|
|
116
|
+
| 配置项 | 类型 | 默认值 | 描述 |
|
|
117
|
+
| -------------------------- | ---- | ------ | ---------------------- |
|
|
118
|
+
| `short_term_expiry_days` | int | `3` | 短期记忆过期天数 |
|
|
119
|
+
| `long_term_expiry_days` | int | `30` | 长期记忆过期天数 |
|
|
120
|
+
| `permanent_expiry_days` | int | `365` | 永久记忆过期天数 |
|
|
121
|
+
| `per_session_memory_limit` | int | `50` | 每个会话的记忆数量上限 |
|
|
122
|
+
|
|
123
|
+
### 环境变量(`.env` 或系统环境变量)
|
|
124
|
+
|
|
125
|
+
| 变量 | 类型 | 默认值 | 描述 |
|
|
126
|
+
| -------------------------- | ------------------------- | ------------------------ | ------------------------- |
|
|
127
|
+
| `VECTOR_DB_TYPE` | `local` / `remote` | `local` | ChromaDB 数据库类型 |
|
|
128
|
+
| `VECTOR_DB_SERVER` | string | `127.0.0.1` | 远程 ChromaDB 地址 |
|
|
129
|
+
| `VECTOR_DB_PORT` | int | `8000` | 远程 ChromaDB 端口 |
|
|
130
|
+
| `VECTOR_DB_SERVER_SSL` | bool | `false` | 是否启用 SSL |
|
|
131
|
+
| `VECTOR_DB_REMOTE_HEADERS` | dict | `{}` | 远程请求头 |
|
|
132
|
+
| `VECTOR_DB_TENANT` | string | `default` | 租户名称 |
|
|
133
|
+
| `VECTOR_DB_DATABASE` | string | `default` | 数据库名称 |
|
|
134
|
+
| `EMBEDDING_MODEL_URL` | string | `http://127.0.0.1:11434` | 嵌入模型地址 |
|
|
135
|
+
| `EMBEDDING_MODEL_NAME` | string | `auto` | 嵌入模型名称 |
|
|
136
|
+
| `EMBEDDING_PROCTOL` | `openai` / `ollama-embed` | `ollama-embed` | 嵌入模型协议 |
|
|
137
|
+
| `EMBEDDING_MODEL_API_KEY` | string | (空) | 嵌入模型 API 密钥(可选) |
|
|
138
|
+
|
|
139
|
+
## 安装与使用
|
|
140
|
+
|
|
141
|
+
### 前置条件
|
|
142
|
+
|
|
143
|
+
- Python 3.10 ~ 3.13
|
|
144
|
+
- 已部署的 [AmritaBot](https://github.com/AmritaBot) 实例
|
|
145
|
+
- (本地模式)无需额外组件
|
|
146
|
+
- (远程模式)已运行的 ChromaDB 服务端
|
|
147
|
+
- 已运行的嵌入模型服务(如 Ollama)
|
|
148
|
+
|
|
149
|
+
### 安装
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
ambot plugin install amrita_plugin_memory
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 配置
|
|
156
|
+
|
|
157
|
+
插件安装后会自动生成配置文件。你可以通过以下方式配置:
|
|
158
|
+
|
|
159
|
+
1. **WebUI 配置**:在 Amrita 的 Web 管理界面中直接修改
|
|
160
|
+
2. **手动编辑**:修改 `config/amrita_plugin_memory/config.toml`
|
|
161
|
+
3. **环境变量**:通过 `.env` 文件或系统环境变量设置数据库和嵌入模型参数
|
|
162
|
+
|
|
163
|
+
### 验证
|
|
164
|
+
|
|
165
|
+
启动后查看日志,确认向量数据库连接成功:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
正在尝试向量数据库连接...
|
|
169
|
+
当前向量数据库类型: local
|
|
170
|
+
创建了向量数据库客户端!
|
|
171
|
+
向量数据库版本:x.x.x
|
|
172
|
+
完成。
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## 开发
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# 安装开发依赖
|
|
179
|
+
uv sync
|
|
180
|
+
|
|
181
|
+
# 代码格式化与检查
|
|
182
|
+
ruff format .
|
|
183
|
+
ruff check .
|
|
184
|
+
|
|
185
|
+
# 类型检查
|
|
186
|
+
pyright
|
|
187
|
+
```
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# amrita_plugin_memory
|
|
2
|
+
|
|
3
|
+
Amrita 长期记忆插件 —— 基于 ChromaDB 向量数据库的智能记忆管理系统,让 AI 助手能够持久化地记住关于用户的重要信息。
|
|
4
|
+
|
|
5
|
+
## 技术栈
|
|
6
|
+
|
|
7
|
+
| 组件 | 技术 |
|
|
8
|
+
| ---------- | ------------------------------------------------------- |
|
|
9
|
+
| 后端框架 | Python 3.10+ / NoneBot2 / Amrita Core |
|
|
10
|
+
| 向量数据库 | ChromaDB(支持本地 PersistentClient 与远程 HttpClient) |
|
|
11
|
+
| 嵌入模型 | OpenAI Embedding API / Ollama Embedding API |
|
|
12
|
+
| 配置管理 | Pydantic + TOML |
|
|
13
|
+
| 异步处理 | asyncio + aiologic + aiohttp |
|
|
14
|
+
| 代码质量 | Ruff + Pyright |
|
|
15
|
+
|
|
16
|
+
## 架构概览
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
amrita_plugin_memory/
|
|
20
|
+
├── __init__.py # 插件入口,注册依赖
|
|
21
|
+
├── config.py # 配置模型(ConfigFile / EnvConfig / DataManager)
|
|
22
|
+
├── embed.py # Ollama 嵌入适配器
|
|
23
|
+
├── vector.py # ChromaDB 异步封装(WrappedClientAPI / AsyncUserMemory)
|
|
24
|
+
└── tools.py # 工具函数定义与注册(write/read/update/delete/list)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- **config.py**: 定义配置文件(过期天数、记忆上限)和环境变量(数据库类型、嵌入模型地址等),通过 Pydantic 模型校验。
|
|
28
|
+
- **embed.py**: 实现 Ollama 协议的嵌入适配器,通过 HTTP API 调用 `/api/embed` 端点。
|
|
29
|
+
- **vector.py**: 对 ChromaDB 原生 API 进行 `asyncio.to_thread` 封装,提供异步的集合管理与记忆 CRUD。使用 `aiologic.Lock` 按用户粒度加锁,保证并发安全。
|
|
30
|
+
- **tools.py**: 定义 5 个 Function Calling 工具(`write_memory` / `read_memory` / `update_memory` / `delete_memory` / `list_memory`),注册到 Amrita 的工具系统中。通过 `scope` 参数支持群共享记忆与用户专属记忆的分区隔离。
|
|
31
|
+
|
|
32
|
+
## 功能特点
|
|
33
|
+
|
|
34
|
+
- **长期记忆存储**:将用户偏好、项目信息等持久化存入 ChromaDB 向量数据库
|
|
35
|
+
- **群/用户记忆隔离**:LLM 可通过 `scope` 参数选择存入群共享记忆(群内所有人可见)或用户专属记忆(仅自己可见)
|
|
36
|
+
- **语义检索**:基于嵌入向量的语义相似度检索,支持中英文关键词
|
|
37
|
+
- **智能标签**:支持使用自定义标签对记忆进行分类(如 `preference`、`project`)
|
|
38
|
+
- **重要性分级**:三级重要程度(`low` / `medium` / `high`),支持按级别过滤检索
|
|
39
|
+
- **完整 CRUD**:支持记忆的创建、查询、更新、删除和列表查看
|
|
40
|
+
- **并发安全**:按用户 ID 粒度加锁,确保同一用户的操作串行化
|
|
41
|
+
- **多租户支持**:通过 ChromaDB remote 模式支持远程向量数据库
|
|
42
|
+
|
|
43
|
+
## 工具函数
|
|
44
|
+
|
|
45
|
+
### 1. `write_memory` —— 写入记忆
|
|
46
|
+
|
|
47
|
+
将当前用户(或群组)的重要信息存入长期记忆。
|
|
48
|
+
|
|
49
|
+
| 参数 | 类型 | 必需 | 描述 |
|
|
50
|
+
| ------------ | ------------------------------- | ---- | ----------------------------------------------------- |
|
|
51
|
+
| `content` | string | ✅ | 记忆内容,简洁明了 |
|
|
52
|
+
| `tags` | string | ✅ | 分类标签,如 `preference`、`project` |
|
|
53
|
+
| `importance` | `"low"` / `"medium"` / `"high"` | ✅ | 重要性等级 |
|
|
54
|
+
| `scope` | `"group"` / `"user"` | ✅ | `group`=群共享,`user`=个人专属(私聊不可用 `group`) |
|
|
55
|
+
|
|
56
|
+
### 2. `read_memory` —— 检索记忆
|
|
57
|
+
|
|
58
|
+
从记忆库中按语义相似度检索用户相关信息。
|
|
59
|
+
|
|
60
|
+
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|
|
61
|
+
| ------------ | ------------------------------- | ---- | ------ | -------------------------- |
|
|
62
|
+
| `query` | string | ✅ | — | 关键词字符串,空格分隔 |
|
|
63
|
+
| `top_k` | integer | ❌ | `5` | 返回结果数量 |
|
|
64
|
+
| `importance` | `"low"` / `"medium"` / `"high"` | ❌ | — | 按重要性过滤 |
|
|
65
|
+
| `scope` | `"group"` / `"user"` | ✅ | — | 检索范围:群共享或个人专属 |
|
|
66
|
+
|
|
67
|
+
### 3. `update_memory` —— 更新记忆
|
|
68
|
+
|
|
69
|
+
更新指定 ID 的记忆内容、标签或重要性。
|
|
70
|
+
|
|
71
|
+
| 参数 | 类型 | 必需 | 描述 |
|
|
72
|
+
| ------------ | ------------------------------- | ---- | --------------- |
|
|
73
|
+
| `id` | string | ✅ | 要更新的记忆 ID |
|
|
74
|
+
| `scope` | `"group"` / `"user"` | ✅ | 记忆所属范围 |
|
|
75
|
+
| `content` | string | ❌ | 新的记忆内容 |
|
|
76
|
+
| `tags` | string | ❌ | 新的标签 |
|
|
77
|
+
| `importance` | `"low"` / `"medium"` / `"high"` | ❌ | 新的重要性等级 |
|
|
78
|
+
|
|
79
|
+
### 4. `delete_memory` —— 删除记忆
|
|
80
|
+
|
|
81
|
+
删除指定 ID 的记忆。
|
|
82
|
+
|
|
83
|
+
| 参数 | 类型 | 必需 | 描述 |
|
|
84
|
+
| ------- | -------------------- | ---- | --------------- |
|
|
85
|
+
| `id` | string | ✅ | 要删除的记忆 ID |
|
|
86
|
+
| `scope` | `"group"` / `"user"` | ✅ | 记忆所属范围 |
|
|
87
|
+
|
|
88
|
+
### 5. `list_memory` —— 列出记忆
|
|
89
|
+
|
|
90
|
+
列出当前用户的所有记忆(返回 scope、标签和 ID)。
|
|
91
|
+
|
|
92
|
+
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|
|
93
|
+
| ------- | -------------------- | ---- | ------ | -------------------------- |
|
|
94
|
+
| `limit` | integer | ❌ | `5` | 返回数量限制 |
|
|
95
|
+
| `scope` | `"group"` / `"user"` | ✅ | — | 列出范围:群共享或个人专属 |
|
|
96
|
+
|
|
97
|
+
## 配置选项
|
|
98
|
+
|
|
99
|
+
### 文件配置(`config/amrita_plugin_memory/config.toml`)
|
|
100
|
+
|
|
101
|
+
| 配置项 | 类型 | 默认值 | 描述 |
|
|
102
|
+
| -------------------------- | ---- | ------ | ---------------------- |
|
|
103
|
+
| `short_term_expiry_days` | int | `3` | 短期记忆过期天数 |
|
|
104
|
+
| `long_term_expiry_days` | int | `30` | 长期记忆过期天数 |
|
|
105
|
+
| `permanent_expiry_days` | int | `365` | 永久记忆过期天数 |
|
|
106
|
+
| `per_session_memory_limit` | int | `50` | 每个会话的记忆数量上限 |
|
|
107
|
+
|
|
108
|
+
### 环境变量(`.env` 或系统环境变量)
|
|
109
|
+
|
|
110
|
+
| 变量 | 类型 | 默认值 | 描述 |
|
|
111
|
+
| -------------------------- | ------------------------- | ------------------------ | ------------------------- |
|
|
112
|
+
| `VECTOR_DB_TYPE` | `local` / `remote` | `local` | ChromaDB 数据库类型 |
|
|
113
|
+
| `VECTOR_DB_SERVER` | string | `127.0.0.1` | 远程 ChromaDB 地址 |
|
|
114
|
+
| `VECTOR_DB_PORT` | int | `8000` | 远程 ChromaDB 端口 |
|
|
115
|
+
| `VECTOR_DB_SERVER_SSL` | bool | `false` | 是否启用 SSL |
|
|
116
|
+
| `VECTOR_DB_REMOTE_HEADERS` | dict | `{}` | 远程请求头 |
|
|
117
|
+
| `VECTOR_DB_TENANT` | string | `default` | 租户名称 |
|
|
118
|
+
| `VECTOR_DB_DATABASE` | string | `default` | 数据库名称 |
|
|
119
|
+
| `EMBEDDING_MODEL_URL` | string | `http://127.0.0.1:11434` | 嵌入模型地址 |
|
|
120
|
+
| `EMBEDDING_MODEL_NAME` | string | `auto` | 嵌入模型名称 |
|
|
121
|
+
| `EMBEDDING_PROCTOL` | `openai` / `ollama-embed` | `ollama-embed` | 嵌入模型协议 |
|
|
122
|
+
| `EMBEDDING_MODEL_API_KEY` | string | (空) | 嵌入模型 API 密钥(可选) |
|
|
123
|
+
|
|
124
|
+
## 安装与使用
|
|
125
|
+
|
|
126
|
+
### 前置条件
|
|
127
|
+
|
|
128
|
+
- Python 3.10 ~ 3.13
|
|
129
|
+
- 已部署的 [AmritaBot](https://github.com/AmritaBot) 实例
|
|
130
|
+
- (本地模式)无需额外组件
|
|
131
|
+
- (远程模式)已运行的 ChromaDB 服务端
|
|
132
|
+
- 已运行的嵌入模型服务(如 Ollama)
|
|
133
|
+
|
|
134
|
+
### 安装
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
ambot plugin install amrita_plugin_memory
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 配置
|
|
141
|
+
|
|
142
|
+
插件安装后会自动生成配置文件。你可以通过以下方式配置:
|
|
143
|
+
|
|
144
|
+
1. **WebUI 配置**:在 Amrita 的 Web 管理界面中直接修改
|
|
145
|
+
2. **手动编辑**:修改 `config/amrita_plugin_memory/config.toml`
|
|
146
|
+
3. **环境变量**:通过 `.env` 文件或系统环境变量设置数据库和嵌入模型参数
|
|
147
|
+
|
|
148
|
+
### 验证
|
|
149
|
+
|
|
150
|
+
启动后查看日志,确认向量数据库连接成功:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
正在尝试向量数据库连接...
|
|
154
|
+
当前向量数据库类型: local
|
|
155
|
+
创建了向量数据库客户端!
|
|
156
|
+
向量数据库版本:x.x.x
|
|
157
|
+
完成。
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 开发
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# 安装开发依赖
|
|
164
|
+
uv sync
|
|
165
|
+
|
|
166
|
+
# 代码格式化与检查
|
|
167
|
+
ruff format .
|
|
168
|
+
ruff check .
|
|
169
|
+
|
|
170
|
+
# 类型检查
|
|
171
|
+
pyright
|
|
172
|
+
```
|
|
@@ -65,6 +65,9 @@ class EnvConfig(BaseModel):
|
|
|
65
65
|
embedding_proctol: Literal["openai", "ollama-embed"] = Field(
|
|
66
66
|
default="ollama-embed", description="Embedding模型协议"
|
|
67
67
|
)
|
|
68
|
+
embedding_model_api_key: str = Field(
|
|
69
|
+
default="", description="Embedding模型API密钥(可选,默认为空)"
|
|
70
|
+
)
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
class DataManager(BaseDataManager[ConfigFile]):
|
|
@@ -79,7 +82,10 @@ async def setup():
|
|
|
79
82
|
|
|
80
83
|
def build_preset() -> ModelPreset:
|
|
81
84
|
return ModelPreset(
|
|
82
|
-
model=env_config.embedding_model_name,
|
|
85
|
+
model=env_config.embedding_model_name,
|
|
86
|
+
base_url=env_config.embedding_model_url,
|
|
87
|
+
api_key=env_config.embedding_model_api_key,
|
|
88
|
+
protocol=env_config.embedding_proctol,
|
|
83
89
|
)
|
|
84
90
|
|
|
85
91
|
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""NoneBot2 命令:/memory"""
|
|
2
|
+
|
|
3
|
+
from amrita.plugins.menu.models import MatcherData
|
|
4
|
+
from nonebot import on_command
|
|
5
|
+
from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent
|
|
6
|
+
from nonebot.matcher import Matcher
|
|
7
|
+
from nonebot.params import CommandArg
|
|
8
|
+
|
|
9
|
+
from .vector import AsyncUserMemory, get_db_conn
|
|
10
|
+
|
|
11
|
+
memory_cmd = on_command(
|
|
12
|
+
"memory",
|
|
13
|
+
aliases={"记忆"},
|
|
14
|
+
state=MatcherData(
|
|
15
|
+
name="记忆管理",
|
|
16
|
+
description="查看/搜索/删除长期记忆",
|
|
17
|
+
usage="/memory (list|search|delete) [参数]",
|
|
18
|
+
).model_dump(),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _label(scope: str) -> str:
|
|
23
|
+
return "👥群共享" if scope == "group" else "👤个人"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@memory_cmd.handle()
|
|
27
|
+
async def _(
|
|
28
|
+
matcher: Matcher,
|
|
29
|
+
event: MessageEvent,
|
|
30
|
+
args: Message = CommandArg(),
|
|
31
|
+
):
|
|
32
|
+
args_list = args.extract_plain_text().strip().split(maxsplit=1)
|
|
33
|
+
sub = args_list[0].lower() if args_list else ""
|
|
34
|
+
rest = args_list[1] if len(args_list) > 1 else ""
|
|
35
|
+
|
|
36
|
+
ope = AsyncUserMemory(get_db_conn())
|
|
37
|
+
await ope.init()
|
|
38
|
+
|
|
39
|
+
if isinstance(event, GroupMessageEvent):
|
|
40
|
+
partitions = [
|
|
41
|
+
("group", f"group_{event.group_id}"),
|
|
42
|
+
("user", f"user_{event.user_id}"),
|
|
43
|
+
]
|
|
44
|
+
role = event.sender.role if event.sender else "member"
|
|
45
|
+
else:
|
|
46
|
+
partitions = [("user", f"user_{event.user_id}")]
|
|
47
|
+
role = None
|
|
48
|
+
|
|
49
|
+
if sub == "" or sub == "list":
|
|
50
|
+
await _handle_list(matcher, ope, partitions)
|
|
51
|
+
elif sub == "search":
|
|
52
|
+
await _handle_search(matcher, ope, partitions, rest)
|
|
53
|
+
elif sub == "delete":
|
|
54
|
+
await _handle_delete(matcher, ope, partitions, role, rest)
|
|
55
|
+
else:
|
|
56
|
+
await matcher.finish(
|
|
57
|
+
"用法:\n"
|
|
58
|
+
"/memory — 列出记忆\n"
|
|
59
|
+
"/memory search <关键词> — 搜索记忆\n"
|
|
60
|
+
"/memory delete <ID> — 删除记忆"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def _handle_list(
|
|
65
|
+
matcher: Matcher,
|
|
66
|
+
ope: AsyncUserMemory,
|
|
67
|
+
partitions: list[tuple[str, str]],
|
|
68
|
+
):
|
|
69
|
+
try:
|
|
70
|
+
all_lines = ["📋 记忆列表:"]
|
|
71
|
+
total = 0
|
|
72
|
+
for scope, pid in partitions:
|
|
73
|
+
result = await ope.get_all_notes(pid, include=["metadatas", "documents"])
|
|
74
|
+
if not result or not result.get("ids"):
|
|
75
|
+
all_lines.append(f" {_label(scope)}: (无)")
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
ids = result["ids"]
|
|
79
|
+
documents = result.get("documents") or [""] * len(ids)
|
|
80
|
+
metadatas = result.get("metadatas") or [{}] * len(ids)
|
|
81
|
+
total += len(ids)
|
|
82
|
+
|
|
83
|
+
all_lines.append(f" {_label(scope)} ({len(ids)} 条):")
|
|
84
|
+
for i, doc_id in enumerate(ids):
|
|
85
|
+
meta = metadatas[i] if i < len(metadatas) else {}
|
|
86
|
+
doc = documents[i] if i < len(documents) else ""
|
|
87
|
+
preview = doc[:50] + "..." if len(doc) > 50 else doc
|
|
88
|
+
all_lines.append(
|
|
89
|
+
f" [{meta.get('importance', '-')}] {doc_id[:8]}… "
|
|
90
|
+
f"{preview} | tag: {meta.get('tags', '-')}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
all_lines[0] = f"📋 记忆列表(共 {total} 条):"
|
|
94
|
+
await matcher.finish("\n".join(all_lines))
|
|
95
|
+
except Exception as e:
|
|
96
|
+
await matcher.finish(f"列出记忆失败: {e}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def _handle_search(
|
|
100
|
+
matcher: Matcher,
|
|
101
|
+
ope: AsyncUserMemory,
|
|
102
|
+
partitions: list[tuple[str, str]],
|
|
103
|
+
query: str,
|
|
104
|
+
):
|
|
105
|
+
if not query:
|
|
106
|
+
await matcher.finish("请提供搜索关键词,例如: /memory search 喜欢什么")
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
all_lines = [f"🔍 搜索「{query}」结果:"]
|
|
110
|
+
has_result = False
|
|
111
|
+
for scope, pid in partitions:
|
|
112
|
+
res = await ope.query_notes(pid, query, top_k=5)
|
|
113
|
+
if not res or not res.get("ids") or not res["ids"]:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
flat_ids = res["ids"][0]
|
|
117
|
+
if not flat_ids:
|
|
118
|
+
continue
|
|
119
|
+
has_result = True
|
|
120
|
+
|
|
121
|
+
raw_docs = res.get("documents")
|
|
122
|
+
flat_docs = raw_docs[0] if raw_docs else []
|
|
123
|
+
raw_metas = res.get("metadatas")
|
|
124
|
+
flat_metas = raw_metas[0] if raw_metas else []
|
|
125
|
+
raw_dist = res.get("distances")
|
|
126
|
+
distances: list[float] = raw_dist[0] if raw_dist else [] # type: ignore[assignment]
|
|
127
|
+
|
|
128
|
+
all_lines.append(f" {_label(scope)}:")
|
|
129
|
+
for i, doc_id in enumerate(flat_ids):
|
|
130
|
+
meta = flat_metas[i] if i < len(flat_metas) else {}
|
|
131
|
+
doc = flat_docs[i] if i < len(flat_docs) else ""
|
|
132
|
+
dist = distances[i] if i < len(distances) else 0.0
|
|
133
|
+
preview = doc[:50] + "..." if len(doc) > 50 else doc
|
|
134
|
+
all_lines.append(
|
|
135
|
+
f" [{meta.get('importance', '-')}] {doc_id[:8]}… "
|
|
136
|
+
f"{preview} | tag: {meta.get('tags', '-')} | score: {dist:.3f}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if not has_result:
|
|
140
|
+
all_lines.append(f" 未找到与「{query}」相关的记忆")
|
|
141
|
+
|
|
142
|
+
await matcher.finish("\n".join(all_lines))
|
|
143
|
+
except Exception as e:
|
|
144
|
+
await matcher.finish(f"搜索记忆失败: {e}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def _handle_delete(
|
|
148
|
+
matcher: Matcher,
|
|
149
|
+
ope: AsyncUserMemory,
|
|
150
|
+
partitions: list[tuple[str, str]],
|
|
151
|
+
role: str | None,
|
|
152
|
+
doc_id: str,
|
|
153
|
+
):
|
|
154
|
+
if not doc_id:
|
|
155
|
+
await matcher.finish("请提供要删除的记忆 ID")
|
|
156
|
+
|
|
157
|
+
# 先找到记忆属于哪个分区
|
|
158
|
+
for scope, pid in partitions:
|
|
159
|
+
result = await ope.get_all_notes(pid, include=["metadatas"])
|
|
160
|
+
if doc_id in (result.get("ids") or []):
|
|
161
|
+
# 群共享记忆只有 admin/owner 能删
|
|
162
|
+
if scope == "group" and role == "member":
|
|
163
|
+
await matcher.finish(
|
|
164
|
+
f"❌ 无权删除群共享记忆({_label(scope)}),仅管理员和群主可删除"
|
|
165
|
+
)
|
|
166
|
+
await ope.delete_note(pid, doc_id)
|
|
167
|
+
await matcher.finish(f"✅ 已删除 {_label(scope)} 记忆: {doc_id[:8]}…")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
await matcher.finish(f"未找到记忆: {doc_id}")
|
|
@@ -13,6 +13,7 @@ from amrita_core import (
|
|
|
13
13
|
from chromadb import QueryResult
|
|
14
14
|
from nonebot import logger
|
|
15
15
|
from nonebot.adapters.onebot.v11 import Event as OB11Event
|
|
16
|
+
from nonebot.adapters.onebot.v11 import GroupMessageEvent
|
|
16
17
|
|
|
17
18
|
from .config import DataManager
|
|
18
19
|
from .vector import AsyncUserMemory, MemoryMetadata, get_db_conn
|
|
@@ -30,16 +31,15 @@ def _resolve_scope_id(ctx: ToolContext, scope: str) -> str:
|
|
|
30
31
|
"""根据 scope 返回对应的分区 key
|
|
31
32
|
|
|
32
33
|
- scope="group": 群共享记忆,返回 f"group_{group_id}"
|
|
33
|
-
- scope="user": 用户专属记忆,返回
|
|
34
|
+
- scope="user": 用户专属记忆,返回 f"user_{user_id}"(群聊私聊互通)
|
|
34
35
|
"""
|
|
35
|
-
event = _get_event(ctx)
|
|
36
|
+
event: OB11Event = _get_event(ctx)
|
|
36
37
|
if scope == "group":
|
|
37
|
-
|
|
38
|
-
if group_id is None:
|
|
38
|
+
if not isinstance(event, GroupMessageEvent):
|
|
39
39
|
raise ValueError("当前不在群聊中,无法使用群共享记忆")
|
|
40
|
-
return f"group_{group_id}"
|
|
40
|
+
return f"group_{event.group_id}"
|
|
41
41
|
elif scope == "user":
|
|
42
|
-
return event.
|
|
42
|
+
return f"user_{event.user_id}" # type: ignore
|
|
43
43
|
else:
|
|
44
44
|
raise ValueError(f"无效的 scope: {scope}")
|
|
45
45
|
|
|
@@ -61,6 +61,14 @@ def _err(message: str) -> str:
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def _check_required(ctx: ToolContext, tool_name: str, *params: str) -> str | None:
|
|
65
|
+
"""校验必填参数,缺失时返回错误消息;全部通过返回 None"""
|
|
66
|
+
for p in params:
|
|
67
|
+
if p not in ctx.data or ctx.data[p] is None:
|
|
68
|
+
return _err(f"调用 {tool_name} 工具必须带有 {p} 参数")
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
64
72
|
# 公共参数:scope
|
|
65
73
|
_SCOPE_PROP = FunctionPropertySchema(
|
|
66
74
|
type="string",
|
|
@@ -183,6 +191,10 @@ LIST_MEMORY_FUN = FunctionDefinitionSchema(
|
|
|
183
191
|
|
|
184
192
|
@on_tools(WRITE_MEMORY_FUN, custom_run=True, strict=True)
|
|
185
193
|
async def w(ctx: ToolContext) -> str:
|
|
194
|
+
if err := _check_required(
|
|
195
|
+
ctx, "write_memory", "content", "tags", "importance", "scope"
|
|
196
|
+
):
|
|
197
|
+
return err
|
|
186
198
|
scope: str = ctx.data["scope"]
|
|
187
199
|
try:
|
|
188
200
|
partition_id = _resolve_scope_id(ctx, scope)
|
|
@@ -205,13 +217,15 @@ async def w(ctx: ToolContext) -> str:
|
|
|
205
217
|
scope=cast(Scope, scope),
|
|
206
218
|
)
|
|
207
219
|
await ope.add_note(partition_id, ctx.data["content"], metadata=meta)
|
|
208
|
-
dmp = meta.model_dump()
|
|
220
|
+
dmp = meta.model_dump(mode="json")
|
|
209
221
|
dmp["status"] = "success"
|
|
210
222
|
return json.dumps(dmp, ensure_ascii=False, indent=4)
|
|
211
223
|
|
|
212
224
|
|
|
213
225
|
@on_tools(READ_MEMORY_FUN, custom_run=True, strict=True)
|
|
214
226
|
async def r(ctx: ToolContext) -> str:
|
|
227
|
+
if err := _check_required(ctx, "read_memory", "query", "scope"):
|
|
228
|
+
return err
|
|
215
229
|
scope: str = ctx.data["scope"]
|
|
216
230
|
try:
|
|
217
231
|
partition_id = _resolve_scope_id(ctx, scope)
|
|
@@ -226,7 +240,7 @@ async def r(ctx: ToolContext) -> str:
|
|
|
226
240
|
res: QueryResult = await ope.query_notes(
|
|
227
241
|
partition_id,
|
|
228
242
|
ctx.data["query"],
|
|
229
|
-
top_k=ctx.data
|
|
243
|
+
top_k=ctx.data.get("top_k", 5),
|
|
230
244
|
importance=ctx.data.get("importance"),
|
|
231
245
|
)
|
|
232
246
|
return json.dumps(res, ensure_ascii=False, indent=4)
|
|
@@ -237,6 +251,8 @@ async def r(ctx: ToolContext) -> str:
|
|
|
237
251
|
|
|
238
252
|
@on_tools(UPDATE_FUN, custom_run=True, strict=True)
|
|
239
253
|
async def update_memory(ctx: ToolContext) -> str:
|
|
254
|
+
if err := _check_required(ctx, "update_memory", "id", "scope"):
|
|
255
|
+
return err
|
|
240
256
|
scope: str = ctx.data["scope"]
|
|
241
257
|
try:
|
|
242
258
|
partition_id = _resolve_scope_id(ctx, scope)
|
|
@@ -307,6 +323,8 @@ async def update_memory(ctx: ToolContext) -> str:
|
|
|
307
323
|
|
|
308
324
|
@on_tools(DELETE_FUN, custom_run=True, strict=True)
|
|
309
325
|
async def delete_memory(ctx: ToolContext) -> str:
|
|
326
|
+
if err := _check_required(ctx, "delete_memory", "id", "scope"):
|
|
327
|
+
return err
|
|
310
328
|
scope: str = ctx.data["scope"]
|
|
311
329
|
try:
|
|
312
330
|
partition_id = _resolve_scope_id(ctx, scope)
|
|
@@ -337,6 +355,8 @@ async def delete_memory(ctx: ToolContext) -> str:
|
|
|
337
355
|
|
|
338
356
|
@on_tools(LIST_MEMORY_FUN, custom_run=True, strict=True)
|
|
339
357
|
async def list_memory(ctx: ToolContext) -> str:
|
|
358
|
+
if err := _check_required(ctx, "list_memory", "scope"):
|
|
359
|
+
return err
|
|
340
360
|
scope: str = ctx.data["scope"]
|
|
341
361
|
try:
|
|
342
362
|
partition_id = _resolve_scope_id(ctx, scope)
|