amrita_plugin_memory 0.1.4__tar.gz → 0.1.6__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.4 → amrita_plugin_memory-0.1.6}/PKG-INFO +1 -1
  2. amrita_plugin_memory-0.1.6/amrita_plugin_memory/matchers.py +196 -0
  3. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/amrita_plugin_memory.egg-info/PKG-INFO +1 -1
  4. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/pyproject.toml +1 -1
  5. amrita_plugin_memory-0.1.4/amrita_plugin_memory/matchers.py +0 -170
  6. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/LICENSE +0 -0
  7. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/README.md +0 -0
  8. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/amrita_plugin_memory/__init__.py +0 -0
  9. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/amrita_plugin_memory/config.py +0 -0
  10. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/amrita_plugin_memory/embed.py +0 -0
  11. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/amrita_plugin_memory/tools.py +0 -0
  12. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/amrita_plugin_memory/vector.py +0 -0
  13. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/amrita_plugin_memory.egg-info/SOURCES.txt +0 -0
  14. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/amrita_plugin_memory.egg-info/dependency_links.txt +0 -0
  15. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/amrita_plugin_memory.egg-info/requires.txt +0 -0
  16. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/amrita_plugin_memory.egg-info/top_level.txt +0 -0
  17. {amrita_plugin_memory-0.1.4 → amrita_plugin_memory-0.1.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amrita_plugin_memory
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Amrita长期记忆插件
5
5
  Requires-Python: <3.14,>=3.10
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1,196 @@
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.exception import FinishedException
7
+ from nonebot.matcher import Matcher
8
+ from nonebot.params import CommandArg
9
+
10
+ from .vector import AsyncUserMemory, get_db_conn
11
+
12
+ memory_cmd = on_command(
13
+ "memory",
14
+ aliases={"记忆"},
15
+ state=MatcherData(
16
+ name="记忆管理",
17
+ description="查看/搜索/删除长期记忆",
18
+ usage="/memory (list|search|delete) [参数]",
19
+ ).model_dump(),
20
+ )
21
+
22
+
23
+ def _label(scope: str) -> str:
24
+ return "👥群共享" if scope == "group" else "👤个人"
25
+
26
+
27
+ @memory_cmd.handle()
28
+ async def _(
29
+ matcher: Matcher,
30
+ event: MessageEvent,
31
+ args: Message = CommandArg(),
32
+ ):
33
+ args_list = args.extract_plain_text().strip().split(maxsplit=1)
34
+ sub = args_list[0].lower() if args_list else ""
35
+ rest = args_list[1] if len(args_list) > 1 else ""
36
+
37
+ ope = AsyncUserMemory(get_db_conn())
38
+ await ope.init()
39
+
40
+ if isinstance(event, GroupMessageEvent):
41
+ available_scopes = {"group", "user"}
42
+ partitions = {
43
+ "group": f"group_{event.group_id}",
44
+ "user": f"user_{event.user_id}",
45
+ }
46
+ role = event.sender.role if event.sender else "member"
47
+ else:
48
+ available_scopes = {"user"}
49
+ partitions = {"user": f"user_{event.user_id}"}
50
+ role = None
51
+
52
+ # 解析子命令: /memory [scope] [action] [args...]
53
+ if sub in available_scopes:
54
+ scope = sub
55
+ action = rest.split(maxsplit=1)[0] if rest else ""
56
+ action_args = rest.split(maxsplit=1)[1] if rest and " " in rest else ""
57
+ elif sub in ("list", "search", "delete"):
58
+ # 老格式兼容 /memory list 等 → 默认以个人范围执行
59
+ scope = "user"
60
+ action = sub
61
+ action_args = rest
62
+ else:
63
+ scope = None
64
+ action = ""
65
+ action_args = ""
66
+
67
+ pid = partitions.get(scope) if scope else None
68
+
69
+ if scope and pid and action in ("", "list"):
70
+ await _handle_list(matcher, ope, pid, scope)
71
+ elif scope and pid and action == "search":
72
+ await _handle_search(matcher, ope, pid, scope, action_args)
73
+ elif scope and pid and action == "delete":
74
+ await _handle_delete(matcher, ope, pid, scope, role, action_args)
75
+ else:
76
+ hint = (
77
+ "用法:\n"
78
+ "/memory user list — 列出个人记忆\n"
79
+ "/memory user search <关键词> — 搜索个人记忆\n"
80
+ "/memory user delete <ID> — 删除个人记忆"
81
+ )
82
+ if "group" in available_scopes:
83
+ hint += (
84
+ "\n"
85
+ "/memory group list — 列出群共享记忆\n"
86
+ "/memory group search <关键词> — 搜索群共享记忆\n"
87
+ "/memory group delete <ID> — 删除群共享记忆(仅管理/群主)"
88
+ )
89
+ await matcher.finish(hint)
90
+
91
+
92
+ async def _handle_list(
93
+ matcher: Matcher,
94
+ ope: AsyncUserMemory,
95
+ partition_id: str,
96
+ scope: str,
97
+ ):
98
+ try:
99
+ result = await ope.get_all_notes(
100
+ partition_id, include=["metadatas", "documents"]
101
+ )
102
+ if not result or not result.get("ids"):
103
+ await matcher.finish(f"{_label(scope)} 暂无记忆")
104
+
105
+ ids = result["ids"]
106
+ documents = result.get("documents") or [""] * len(ids)
107
+ metadatas = result.get("metadatas") or [{}] * len(ids)
108
+
109
+ lines = [f"📋 {_label(scope)} 记忆列表(共 {len(ids)} 条):"]
110
+ for i, doc_id in enumerate(ids):
111
+ meta = metadatas[i] if i < len(metadatas) else {}
112
+ doc = documents[i] if i < len(documents) else ""
113
+ preview = doc[:50] + "..." if len(doc) > 50 else doc
114
+ lines.append(
115
+ f"[{meta.get('importance', '-')}] {doc_id[:8]}… "
116
+ f"{preview} | tag: {meta.get('tags', '-')}"
117
+ )
118
+
119
+ await matcher.finish("\n".join(lines))
120
+ except FinishedException:
121
+ return
122
+ except Exception as e:
123
+ await matcher.finish(f"列出记忆失败: {e}")
124
+
125
+
126
+ async def _handle_search(
127
+ matcher: Matcher,
128
+ ope: AsyncUserMemory,
129
+ partition_id: str,
130
+ scope: str,
131
+ query: str,
132
+ ):
133
+ if not query:
134
+ await matcher.finish("请提供搜索关键词,例如: /memory user search 喜欢什么")
135
+
136
+ try:
137
+ res = await ope.query_notes(partition_id, query, top_k=10)
138
+ if not res or not res.get("ids") or not res["ids"]:
139
+ await matcher.finish(f"{_label(scope)} 未找到与「{query}」相关的记忆")
140
+
141
+ flat_ids = res["ids"][0]
142
+ if not flat_ids:
143
+ await matcher.finish(f"{_label(scope)} 未找到与「{query}」相关的记忆")
144
+
145
+ raw_docs = res.get("documents")
146
+ flat_docs = raw_docs[0] if raw_docs else []
147
+ raw_metas = res.get("metadatas")
148
+ flat_metas = raw_metas[0] if raw_metas else []
149
+ raw_dist = res.get("distances")
150
+ distances: list[float] = raw_dist[0] if raw_dist else [] # type: ignore[assignment]
151
+
152
+ lines = [f"🔍 {_label(scope)} 搜索「{query}」结果:"]
153
+ for i, doc_id in enumerate(flat_ids):
154
+ meta = flat_metas[i] if i < len(flat_metas) else {}
155
+ doc = flat_docs[i] if i < len(flat_docs) else ""
156
+ dist = distances[i] if i < len(distances) else 0.0
157
+ preview = doc[:50] + "..." if len(doc) > 50 else doc
158
+ lines.append(
159
+ f"[{meta.get('importance', '-')}] {doc_id[:8]}… "
160
+ f"{preview} | tag: {meta.get('tags', '-')} | score: {dist:.3f}"
161
+ )
162
+
163
+ await matcher.finish("\n".join(lines))
164
+ except FinishedException:
165
+ return
166
+ except Exception as e:
167
+ await matcher.finish(f"搜索记忆失败: {e}")
168
+
169
+
170
+ async def _handle_delete(
171
+ matcher: Matcher,
172
+ ope: AsyncUserMemory,
173
+ partition_id: str,
174
+ scope: str,
175
+ role: str | None,
176
+ doc_id: str,
177
+ ):
178
+ if not doc_id:
179
+ await matcher.finish("请提供要删除的记忆 ID")
180
+
181
+ try:
182
+ result = await ope.get_all_notes(partition_id, include=["metadatas"])
183
+ if doc_id not in (result.get("ids") or []):
184
+ await matcher.finish(f"{_label(scope)} 未找到记忆: {doc_id}")
185
+
186
+ if scope == "group" and role == "member":
187
+ await matcher.finish(
188
+ f"❌ 无权删除{_label(scope)}记忆,仅管理员和群主可删除"
189
+ )
190
+
191
+ await ope.delete_note(partition_id, doc_id)
192
+ await matcher.finish(f"✅ 已删除 {_label(scope)} 记忆: {doc_id[:8]}…")
193
+ except FinishedException:
194
+ return
195
+ except Exception as e:
196
+ await matcher.finish(f"删除记忆失败: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amrita_plugin_memory
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Amrita长期记忆插件
5
5
  Requires-Python: <3.14,>=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "amrita_plugin_memory"
3
3
  description = "Amrita长期记忆插件"
4
- version = "0.1.4"
4
+ version = "0.1.6"
5
5
  dependencies = [
6
6
  "aiohttp>=3.13.3",
7
7
  "aiologic>=0.16.0",
@@ -1,170 +0,0 @@
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}")