amrita_plugin_memory 0.1.3__tar.gz → 0.1.5__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.
Files changed (17) hide show
  1. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/PKG-INFO +1 -1
  2. amrita_plugin_memory-0.1.5/amrita_plugin_memory/__init__.py +8 -0
  3. amrita_plugin_memory-0.1.5/amrita_plugin_memory/matchers.py +189 -0
  4. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/amrita_plugin_memory/tools.py +33 -13
  5. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/amrita_plugin_memory.egg-info/PKG-INFO +1 -1
  6. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/amrita_plugin_memory.egg-info/SOURCES.txt +1 -0
  7. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/pyproject.toml +1 -1
  8. amrita_plugin_memory-0.1.3/amrita_plugin_memory/__init__.py +0 -7
  9. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/LICENSE +0 -0
  10. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/README.md +0 -0
  11. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/amrita_plugin_memory/config.py +0 -0
  12. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/amrita_plugin_memory/embed.py +0 -0
  13. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/amrita_plugin_memory/vector.py +0 -0
  14. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/amrita_plugin_memory.egg-info/dependency_links.txt +0 -0
  15. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/amrita_plugin_memory.egg-info/requires.txt +0 -0
  16. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/amrita_plugin_memory.egg-info/top_level.txt +0 -0
  17. {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amrita_plugin_memory
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Amrita长期记忆插件
5
5
  Requires-Python: <3.14,>=3.10
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1,8 @@
1
+ from nonebot import require
2
+
3
+ require("nonebot_plugin_localstore")
4
+ require("amrita.plugins.chat")
5
+ require("amrita.plugins.menu")
6
+ from . import config, embed, matchers, tools, vector
7
+
8
+ __all__ = ["config", "embed", "matchers", "tools", "vector"]
@@ -0,0 +1,189 @@
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
+ available_scopes = {"group", "user"}
41
+ partitions = {
42
+ "group": f"group_{event.group_id}",
43
+ "user": f"user_{event.user_id}",
44
+ }
45
+ role = event.sender.role if event.sender else "member"
46
+ else:
47
+ available_scopes = {"user"}
48
+ partitions = {"user": f"user_{event.user_id}"}
49
+ role = None
50
+
51
+ # 解析子命令: /memory [scope] [action] [args...]
52
+ if sub in available_scopes:
53
+ scope = sub
54
+ action = rest.split(maxsplit=1)[0] if rest else ""
55
+ action_args = rest.split(maxsplit=1)[1] if rest and " " in rest else ""
56
+ elif sub in ("list", "search", "delete"):
57
+ # 老格式兼容 /memory list 等 → 默认以个人范围执行
58
+ scope = "user"
59
+ action = sub
60
+ action_args = rest
61
+ else:
62
+ scope = None
63
+ action = ""
64
+ action_args = ""
65
+
66
+ pid = partitions.get(scope) if scope else None
67
+
68
+ if scope and pid and action in ("", "list"):
69
+ await _handle_list(matcher, ope, pid, scope)
70
+ elif scope and pid and action == "search":
71
+ await _handle_search(matcher, ope, pid, scope, action_args)
72
+ elif scope and pid and action == "delete":
73
+ await _handle_delete(matcher, ope, pid, scope, role, action_args)
74
+ else:
75
+ hint = (
76
+ "用法:\n"
77
+ "/memory user list — 列出个人记忆\n"
78
+ "/memory user search <关键词> — 搜索个人记忆\n"
79
+ "/memory user delete <ID> — 删除个人记忆"
80
+ )
81
+ if "group" in available_scopes:
82
+ hint += (
83
+ "\n"
84
+ "/memory group list — 列出群共享记忆\n"
85
+ "/memory group search <关键词> — 搜索群共享记忆\n"
86
+ "/memory group delete <ID> — 删除群共享记忆(仅管理/群主)"
87
+ )
88
+ await matcher.finish(hint)
89
+
90
+
91
+ async def _handle_list(
92
+ matcher: Matcher,
93
+ ope: AsyncUserMemory,
94
+ partition_id: str,
95
+ scope: str,
96
+ ):
97
+ try:
98
+ result = await ope.get_all_notes(
99
+ partition_id, include=["metadatas", "documents"]
100
+ )
101
+ if not result or not result.get("ids"):
102
+ await matcher.finish(f"{_label(scope)} 暂无记忆")
103
+
104
+ ids = result["ids"]
105
+ documents = result.get("documents") or [""] * len(ids)
106
+ metadatas = result.get("metadatas") or [{}] * len(ids)
107
+
108
+ lines = [f"📋 {_label(scope)} 记忆列表(共 {len(ids)} 条):"]
109
+ for i, doc_id in enumerate(ids):
110
+ meta = metadatas[i] if i < len(metadatas) else {}
111
+ doc = documents[i] if i < len(documents) else ""
112
+ preview = doc[:50] + "..." if len(doc) > 50 else doc
113
+ lines.append(
114
+ f"[{meta.get('importance', '-')}] {doc_id[:8]}… "
115
+ f"{preview} | tag: {meta.get('tags', '-')}"
116
+ )
117
+
118
+ await matcher.finish("\n".join(lines))
119
+ except Exception as e:
120
+ await matcher.finish(f"列出记忆失败: {e}")
121
+
122
+
123
+ async def _handle_search(
124
+ matcher: Matcher,
125
+ ope: AsyncUserMemory,
126
+ partition_id: str,
127
+ scope: str,
128
+ query: str,
129
+ ):
130
+ if not query:
131
+ await matcher.finish("请提供搜索关键词,例如: /memory user search 喜欢什么")
132
+
133
+ try:
134
+ res = await ope.query_notes(partition_id, query, top_k=10)
135
+ if not res or not res.get("ids") or not res["ids"]:
136
+ await matcher.finish(f"{_label(scope)} 未找到与「{query}」相关的记忆")
137
+
138
+ flat_ids = res["ids"][0]
139
+ if not flat_ids:
140
+ await matcher.finish(f"{_label(scope)} 未找到与「{query}」相关的记忆")
141
+
142
+ raw_docs = res.get("documents")
143
+ flat_docs = raw_docs[0] if raw_docs else []
144
+ raw_metas = res.get("metadatas")
145
+ flat_metas = raw_metas[0] if raw_metas else []
146
+ raw_dist = res.get("distances")
147
+ distances: list[float] = raw_dist[0] if raw_dist else [] # type: ignore[assignment]
148
+
149
+ lines = [f"🔍 {_label(scope)} 搜索「{query}」结果:"]
150
+ for i, doc_id in enumerate(flat_ids):
151
+ meta = flat_metas[i] if i < len(flat_metas) else {}
152
+ doc = flat_docs[i] if i < len(flat_docs) else ""
153
+ dist = distances[i] if i < len(distances) else 0.0
154
+ preview = doc[:50] + "..." if len(doc) > 50 else doc
155
+ lines.append(
156
+ f"[{meta.get('importance', '-')}] {doc_id[:8]}… "
157
+ f"{preview} | tag: {meta.get('tags', '-')} | score: {dist:.3f}"
158
+ )
159
+
160
+ await matcher.finish("\n".join(lines))
161
+ except Exception as e:
162
+ await matcher.finish(f"搜索记忆失败: {e}")
163
+
164
+
165
+ async def _handle_delete(
166
+ matcher: Matcher,
167
+ ope: AsyncUserMemory,
168
+ partition_id: str,
169
+ scope: str,
170
+ role: str | None,
171
+ doc_id: str,
172
+ ):
173
+ if not doc_id:
174
+ await matcher.finish("请提供要删除的记忆 ID")
175
+
176
+ try:
177
+ result = await ope.get_all_notes(partition_id, include=["metadatas"])
178
+ if doc_id not in (result.get("ids") or []):
179
+ await matcher.finish(f"{_label(scope)} 未找到记忆: {doc_id}")
180
+
181
+ if scope == "group" and role == "member":
182
+ await matcher.finish(
183
+ f"❌ 无权删除{_label(scope)}记忆,仅管理员和群主可删除"
184
+ )
185
+
186
+ await ope.delete_note(partition_id, doc_id)
187
+ await matcher.finish(f"✅ 已删除 {_label(scope)} 记忆: {doc_id[:8]}…")
188
+ except Exception as e:
189
+ await matcher.finish(f"删除记忆失败: {e}")
@@ -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": 用户专属记忆,返回 event.get_session_id()
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
- group_id: int | None = getattr(event, "group_id", None)
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.get_session_id()
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,7 +191,11 @@ 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:
186
- scope: str = ctx.data.get("scope", "user")
194
+ if err := _check_required(
195
+ ctx, "write_memory", "content", "tags", "importance", "scope"
196
+ ):
197
+ return err
198
+ scope: str = ctx.data["scope"]
187
199
  try:
188
200
  partition_id = _resolve_scope_id(ctx, scope)
189
201
  except ValueError as e:
@@ -205,14 +217,16 @@ 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:
215
- scope: str = ctx.data.get("scope", "user")
227
+ if err := _check_required(ctx, "read_memory", "query", "scope"):
228
+ return err
229
+ scope: str = ctx.data["scope"]
216
230
  try:
217
231
  partition_id = _resolve_scope_id(ctx, scope)
218
232
  except ValueError as e:
@@ -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["top_k"],
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,7 +251,9 @@ 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:
240
- scope: str = ctx.data.get("scope", "user")
254
+ if err := _check_required(ctx, "update_memory", "id", "scope"):
255
+ return err
256
+ scope: str = ctx.data["scope"]
241
257
  try:
242
258
  partition_id = _resolve_scope_id(ctx, scope)
243
259
  except ValueError as e:
@@ -307,7 +323,9 @@ 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:
310
- scope: str = ctx.data.get("scope", "user")
326
+ if err := _check_required(ctx, "delete_memory", "id", "scope"):
327
+ return err
328
+ scope: str = ctx.data["scope"]
311
329
  try:
312
330
  partition_id = _resolve_scope_id(ctx, scope)
313
331
  except ValueError as e:
@@ -337,7 +355,9 @@ 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:
340
- scope: str = ctx.data.get("scope", "user")
358
+ if err := _check_required(ctx, "list_memory", "scope"):
359
+ return err
360
+ scope: str = ctx.data["scope"]
341
361
  try:
342
362
  partition_id = _resolve_scope_id(ctx, scope)
343
363
  except ValueError as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amrita_plugin_memory
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Amrita长期记忆插件
5
5
  Requires-Python: <3.14,>=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  amrita_plugin_memory/__init__.py
5
5
  amrita_plugin_memory/config.py
6
6
  amrita_plugin_memory/embed.py
7
+ amrita_plugin_memory/matchers.py
7
8
  amrita_plugin_memory/tools.py
8
9
  amrita_plugin_memory/vector.py
9
10
  amrita_plugin_memory.egg-info/PKG-INFO
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "amrita_plugin_memory"
3
3
  description = "Amrita长期记忆插件"
4
- version = "0.1.3"
4
+ version = "0.1.5"
5
5
  dependencies = [
6
6
  "aiohttp>=3.13.3",
7
7
  "aiologic>=0.16.0",
@@ -1,7 +0,0 @@
1
- from nonebot import require
2
-
3
- require("nonebot_plugin_localstore")
4
- require("amrita.plugins.chat")
5
- from . import config, embed, tools, vector
6
-
7
- __all__ = ["config", "embed", "tools", "vector"]