amrita_plugin_memory 0.1.3__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.3 → amrita_plugin_memory-0.1.4}/PKG-INFO +1 -1
- amrita_plugin_memory-0.1.4/amrita_plugin_memory/__init__.py +8 -0
- amrita_plugin_memory-0.1.4/amrita_plugin_memory/matchers.py +170 -0
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory/tools.py +33 -13
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory.egg-info/PKG-INFO +1 -1
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory.egg-info/SOURCES.txt +1 -0
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/pyproject.toml +1 -1
- amrita_plugin_memory-0.1.3/amrita_plugin_memory/__init__.py +0 -7
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/LICENSE +0 -0
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/README.md +0 -0
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory/config.py +0 -0
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory/embed.py +0 -0
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory/vector.py +0 -0
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory.egg-info/dependency_links.txt +0 -0
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory.egg-info/requires.txt +0 -0
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory.egg-info/top_level.txt +0 -0
- {amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/setup.cfg +0 -0
|
@@ -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,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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{amrita_plugin_memory-0.1.3 → amrita_plugin_memory-0.1.4}/amrita_plugin_memory.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|