amrita_plugin_memory 0.5.0__tar.gz → 0.6.0__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.5.0 → amrita_plugin_memory-0.6.0}/PKG-INFO +58 -7
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/README.md +57 -6
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/__init__.py +2 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/config.py +36 -5
- amrita_plugin_memory-0.6.0/amrita_plugin_memory/decay.py +184 -0
- amrita_plugin_memory-0.6.0/amrita_plugin_memory/permissions.py +68 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/_state.py +1 -1
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/hooks.py +16 -7
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/knowledge.py +46 -11
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/runner.py +42 -18
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/schemas.py +17 -1
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/tools.py +239 -37
- amrita_plugin_memory-0.6.0/amrita_plugin_memory/tasks.py +47 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/vector.py +132 -13
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory.egg-info/PKG-INFO +58 -7
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory.egg-info/SOURCES.txt +3 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/pyproject.toml +1 -1
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/LICENSE +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/cli.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/embed.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/embedding.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/keys.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/matchers.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/memo.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/migrations/21f55abc2b90_init.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/migrations/6004d221a7de_state.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/models.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/__init__.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/backend.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/consts.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/types.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/tools.py +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory.egg-info/dependency_links.txt +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory.egg-info/entry_points.txt +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory.egg-info/requires.txt +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory.egg-info/top_level.txt +0 -0
- {amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amrita_plugin_memory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Amrita长期记忆插件
|
|
5
5
|
Requires-Python: <3.14,>=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -254,6 +254,7 @@ LLM 读当前 memo → 融合新事实 → 输出新全文 → 服务端校验
|
|
|
254
254
|
|
|
255
255
|
每条记忆带 `user_id` / `scope` / `tags` / `importance` / `created_at`。
|
|
256
256
|
`importance` 分 `low` / `medium` / `high`,可在检索时作为过滤条件。
|
|
257
|
+
检索时额外回填 `decay_score`(时间退化得分),不参与过滤。
|
|
257
258
|
|
|
258
259
|
### 检索
|
|
259
260
|
|
|
@@ -267,6 +268,32 @@ LLM 读当前 memo → 融合新事实 → 输出新全文 → 服务端校验
|
|
|
267
268
|
> 距离是原始度量而非归一化相似度:`l2` 空间下其取值范围取决于向量是否归一化,
|
|
268
269
|
> 因此不要跨模型比较绝对值,只用于同一查询内的相对排序。
|
|
269
270
|
|
|
271
|
+
### 时间记忆退化(软衰减 + 分层清理)
|
|
272
|
+
|
|
273
|
+
记忆不是永久等权 —— 越旧权重越低,超龄则逐级退化直至清理。分两段实现:
|
|
274
|
+
|
|
275
|
+
**软衰减(检索期)**:`query_notes` 先多取 `overfetch` 倍候选,再按
|
|
276
|
+
|
|
277
|
+
$$\text{score} = \frac{1}{1 + d} \times \max\left(0.5^{\,\text{age} / \text{half-life}},\ \text{floor}\right)$$
|
|
278
|
+
|
|
279
|
+
重排后截回 `top_k`。半衰期随 `importance` 放大:`low` 为 `half_life_days`,
|
|
280
|
+
`medium` 为 `1+boost` 倍,`high` 为 `1+2·boost` 倍。得分回填到元数据的
|
|
281
|
+
`decay_score` 字段,便于排查。这一步**只重排、不删数据**。
|
|
282
|
+
|
|
283
|
+
**硬分层(清理期)**:每条记忆按 `importance` 有一条寿命阈值 ——
|
|
284
|
+
`low` = `short_term_days`、`high` = `long_term_days`、`medium` 取两者几何中值。
|
|
285
|
+
后台任务按 `sweep_interval_hours` 扫描,超龄先 `high → medium → low` 逐级降级,
|
|
286
|
+
降到 `low` 仍超龄才删除。降级只改 `importance`,不重新嵌入。
|
|
287
|
+
|
|
288
|
+
| 动作 | 触发条件 | 效果 |
|
|
289
|
+
| -------- | ------------------ | ------------------- |
|
|
290
|
+
| `keep` | `age ≤ 当前层阈值` | 不动 |
|
|
291
|
+
| `demote` | 超龄且非 `low` | `importance` 降一级 |
|
|
292
|
+
| `purge` | 超龄且已是 `low` | 删除该条 |
|
|
293
|
+
|
|
294
|
+
`created_at` 缺失或不可解析时按 `keep` 处理,避免误删。
|
|
295
|
+
软衰减与分层清理都受 `[decay].enabled` 总开关控制。
|
|
296
|
+
|
|
270
297
|
---
|
|
271
298
|
|
|
272
299
|
## L3 离线巩固循环(DMN)
|
|
@@ -401,11 +428,16 @@ flowchart TD
|
|
|
401
428
|
| `knowledge_search` | query, top_k? |
|
|
402
429
|
| `knowledge_suggest` | action, title, summary, body, reason |
|
|
403
430
|
|
|
431
|
+
知识库的三个只读工具 `knowledge_list` / `knowledge_read` / `knowledge_search`
|
|
432
|
+
需要 LP 权限节点 `memory.knowledge`;`lp.admin` 与 `.env` 里的超级用户始终持有。
|
|
433
|
+
未授权时工具返回错误 JSON,不返回任何知识内容。
|
|
434
|
+
|
|
404
435
|
### 巩固循环工具(`rethinking/tools.py`,共 20 个)
|
|
405
436
|
|
|
406
437
|
记忆与 session/画像工具注册在隔离的 `_SUBCONSCIOUS_TOOLS` 上,不污染全局工具管理器。
|
|
407
|
-
知识库的 `list`/`read`/`search`
|
|
408
|
-
`create`/`update`/`delete`
|
|
438
|
+
知识库的 `list`/`read`/`search` 双重注册:循环侧不受限,表层(对话 LLM)走
|
|
439
|
+
`memory.knowledge` 权限校验;`create`/`update`/`delete` 仅循环可用,
|
|
440
|
+
表层通过 `knowledge_suggest` 提交建议。
|
|
409
441
|
|
|
410
442
|
| 工具 | 用途 |
|
|
411
443
|
| -------------------------------- | -------------------------------------- |
|
|
@@ -541,11 +573,22 @@ flowchart TD
|
|
|
541
573
|
|
|
542
574
|
```toml
|
|
543
575
|
# L2 记忆
|
|
544
|
-
short_term_expiry_days = 7
|
|
545
|
-
long_term_expiry_days = 90
|
|
546
576
|
per_session_memory_limit = 50
|
|
547
577
|
memo_max_chars = 1000
|
|
548
578
|
|
|
579
|
+
# L2 时间记忆退化(软衰减 + 分层清理)
|
|
580
|
+
[decay]
|
|
581
|
+
enabled = true
|
|
582
|
+
short_term_days = 7
|
|
583
|
+
long_term_days = 90
|
|
584
|
+
half_life_days = 30.0
|
|
585
|
+
importance_boost = 1.0
|
|
586
|
+
overfetch = 3
|
|
587
|
+
score_floor = 0.05
|
|
588
|
+
sweep_enabled = true
|
|
589
|
+
sweep_interval_hours = 24
|
|
590
|
+
sweep_batch_limit = 5000
|
|
591
|
+
|
|
549
592
|
# L3 离线巩固循环
|
|
550
593
|
[subconscious]
|
|
551
594
|
enabled = false
|
|
@@ -571,8 +614,16 @@ enable_knowledge = true
|
|
|
571
614
|
|
|
572
615
|
| 字段 | 默认值 | 说明 |
|
|
573
616
|
| ---------------------------- | ------- | ------------------------------------------- |
|
|
574
|
-
| `
|
|
575
|
-
| `
|
|
617
|
+
| `decay.enabled` | `true` | 是否启用时间记忆退化(软衰减 + 分层清理) |
|
|
618
|
+
| `decay.short_term_days` | `7` | `low` 层寿命阈值(天) |
|
|
619
|
+
| `decay.long_term_days` | `90` | `high` 层寿命阈值(天),`medium` 取几何中值 |
|
|
620
|
+
| `decay.half_life_days` | `30.0` | 软衰减基础半衰期(天),`low` 为基准 |
|
|
621
|
+
| `decay.importance_boost` | `1.0` | `high` 半衰期为 `1+2·boost` 倍 |
|
|
622
|
+
| `decay.overfetch` | `3` | 检索候选放大倍数 |
|
|
623
|
+
| `decay.score_floor` | `0.05` | 衰减因子下限,避免旧记忆权重归零 |
|
|
624
|
+
| `decay.sweep_enabled` | `true` | 是否启用分层清理 |
|
|
625
|
+
| `decay.sweep_interval_hours` | `24` | 分层清理周期(小时) |
|
|
626
|
+
| `decay.sweep_batch_limit` | `5000` | 单次清理最多扫描的记忆条数 |
|
|
576
627
|
| `per_session_memory_limit` | `50` | 单个分区(uni_id)的记忆条数上限 |
|
|
577
628
|
| `memo_max_chars` | `1000` | L1 备忘录字数硬上限 |
|
|
578
629
|
| `enabled` | `false` | 是否启用离线巩固循环 |
|
|
@@ -235,6 +235,7 @@ LLM 读当前 memo → 融合新事实 → 输出新全文 → 服务端校验
|
|
|
235
235
|
|
|
236
236
|
每条记忆带 `user_id` / `scope` / `tags` / `importance` / `created_at`。
|
|
237
237
|
`importance` 分 `low` / `medium` / `high`,可在检索时作为过滤条件。
|
|
238
|
+
检索时额外回填 `decay_score`(时间退化得分),不参与过滤。
|
|
238
239
|
|
|
239
240
|
### 检索
|
|
240
241
|
|
|
@@ -248,6 +249,32 @@ LLM 读当前 memo → 融合新事实 → 输出新全文 → 服务端校验
|
|
|
248
249
|
> 距离是原始度量而非归一化相似度:`l2` 空间下其取值范围取决于向量是否归一化,
|
|
249
250
|
> 因此不要跨模型比较绝对值,只用于同一查询内的相对排序。
|
|
250
251
|
|
|
252
|
+
### 时间记忆退化(软衰减 + 分层清理)
|
|
253
|
+
|
|
254
|
+
记忆不是永久等权 —— 越旧权重越低,超龄则逐级退化直至清理。分两段实现:
|
|
255
|
+
|
|
256
|
+
**软衰减(检索期)**:`query_notes` 先多取 `overfetch` 倍候选,再按
|
|
257
|
+
|
|
258
|
+
$$\text{score} = \frac{1}{1 + d} \times \max\left(0.5^{\,\text{age} / \text{half-life}},\ \text{floor}\right)$$
|
|
259
|
+
|
|
260
|
+
重排后截回 `top_k`。半衰期随 `importance` 放大:`low` 为 `half_life_days`,
|
|
261
|
+
`medium` 为 `1+boost` 倍,`high` 为 `1+2·boost` 倍。得分回填到元数据的
|
|
262
|
+
`decay_score` 字段,便于排查。这一步**只重排、不删数据**。
|
|
263
|
+
|
|
264
|
+
**硬分层(清理期)**:每条记忆按 `importance` 有一条寿命阈值 ——
|
|
265
|
+
`low` = `short_term_days`、`high` = `long_term_days`、`medium` 取两者几何中值。
|
|
266
|
+
后台任务按 `sweep_interval_hours` 扫描,超龄先 `high → medium → low` 逐级降级,
|
|
267
|
+
降到 `low` 仍超龄才删除。降级只改 `importance`,不重新嵌入。
|
|
268
|
+
|
|
269
|
+
| 动作 | 触发条件 | 效果 |
|
|
270
|
+
| -------- | ------------------ | ------------------- |
|
|
271
|
+
| `keep` | `age ≤ 当前层阈值` | 不动 |
|
|
272
|
+
| `demote` | 超龄且非 `low` | `importance` 降一级 |
|
|
273
|
+
| `purge` | 超龄且已是 `low` | 删除该条 |
|
|
274
|
+
|
|
275
|
+
`created_at` 缺失或不可解析时按 `keep` 处理,避免误删。
|
|
276
|
+
软衰减与分层清理都受 `[decay].enabled` 总开关控制。
|
|
277
|
+
|
|
251
278
|
---
|
|
252
279
|
|
|
253
280
|
## L3 离线巩固循环(DMN)
|
|
@@ -382,11 +409,16 @@ flowchart TD
|
|
|
382
409
|
| `knowledge_search` | query, top_k? |
|
|
383
410
|
| `knowledge_suggest` | action, title, summary, body, reason |
|
|
384
411
|
|
|
412
|
+
知识库的三个只读工具 `knowledge_list` / `knowledge_read` / `knowledge_search`
|
|
413
|
+
需要 LP 权限节点 `memory.knowledge`;`lp.admin` 与 `.env` 里的超级用户始终持有。
|
|
414
|
+
未授权时工具返回错误 JSON,不返回任何知识内容。
|
|
415
|
+
|
|
385
416
|
### 巩固循环工具(`rethinking/tools.py`,共 20 个)
|
|
386
417
|
|
|
387
418
|
记忆与 session/画像工具注册在隔离的 `_SUBCONSCIOUS_TOOLS` 上,不污染全局工具管理器。
|
|
388
|
-
知识库的 `list`/`read`/`search`
|
|
389
|
-
`create`/`update`/`delete`
|
|
419
|
+
知识库的 `list`/`read`/`search` 双重注册:循环侧不受限,表层(对话 LLM)走
|
|
420
|
+
`memory.knowledge` 权限校验;`create`/`update`/`delete` 仅循环可用,
|
|
421
|
+
表层通过 `knowledge_suggest` 提交建议。
|
|
390
422
|
|
|
391
423
|
| 工具 | 用途 |
|
|
392
424
|
| -------------------------------- | -------------------------------------- |
|
|
@@ -522,11 +554,22 @@ flowchart TD
|
|
|
522
554
|
|
|
523
555
|
```toml
|
|
524
556
|
# L2 记忆
|
|
525
|
-
short_term_expiry_days = 7
|
|
526
|
-
long_term_expiry_days = 90
|
|
527
557
|
per_session_memory_limit = 50
|
|
528
558
|
memo_max_chars = 1000
|
|
529
559
|
|
|
560
|
+
# L2 时间记忆退化(软衰减 + 分层清理)
|
|
561
|
+
[decay]
|
|
562
|
+
enabled = true
|
|
563
|
+
short_term_days = 7
|
|
564
|
+
long_term_days = 90
|
|
565
|
+
half_life_days = 30.0
|
|
566
|
+
importance_boost = 1.0
|
|
567
|
+
overfetch = 3
|
|
568
|
+
score_floor = 0.05
|
|
569
|
+
sweep_enabled = true
|
|
570
|
+
sweep_interval_hours = 24
|
|
571
|
+
sweep_batch_limit = 5000
|
|
572
|
+
|
|
530
573
|
# L3 离线巩固循环
|
|
531
574
|
[subconscious]
|
|
532
575
|
enabled = false
|
|
@@ -552,8 +595,16 @@ enable_knowledge = true
|
|
|
552
595
|
|
|
553
596
|
| 字段 | 默认值 | 说明 |
|
|
554
597
|
| ---------------------------- | ------- | ------------------------------------------- |
|
|
555
|
-
| `
|
|
556
|
-
| `
|
|
598
|
+
| `decay.enabled` | `true` | 是否启用时间记忆退化(软衰减 + 分层清理) |
|
|
599
|
+
| `decay.short_term_days` | `7` | `low` 层寿命阈值(天) |
|
|
600
|
+
| `decay.long_term_days` | `90` | `high` 层寿命阈值(天),`medium` 取几何中值 |
|
|
601
|
+
| `decay.half_life_days` | `30.0` | 软衰减基础半衰期(天),`low` 为基准 |
|
|
602
|
+
| `decay.importance_boost` | `1.0` | `high` 半衰期为 `1+2·boost` 倍 |
|
|
603
|
+
| `decay.overfetch` | `3` | 检索候选放大倍数 |
|
|
604
|
+
| `decay.score_floor` | `0.05` | 衰减因子下限,避免旧记忆权重归零 |
|
|
605
|
+
| `decay.sweep_enabled` | `true` | 是否启用分层清理 |
|
|
606
|
+
| `decay.sweep_interval_hours` | `24` | 分层清理周期(小时) |
|
|
607
|
+
| `decay.sweep_batch_limit` | `5000` | 单次清理最多扫描的记忆条数 |
|
|
557
608
|
| `per_session_memory_limit` | `50` | 单个分区(uni_id)的记忆条数上限 |
|
|
558
609
|
| `memo_max_chars` | `1000` | L1 备忘录字数硬上限 |
|
|
559
610
|
| `enabled` | `false` | 是否启用离线巩固循环 |
|
|
@@ -91,14 +91,45 @@ class SubconsciousConfig(BaseModel):
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
class DecayConfig(BaseModel):
|
|
95
|
+
"""时间记忆退化配置:软衰减(检索重排)+ 硬分层(降级 / 清理)。"""
|
|
96
|
+
|
|
97
|
+
enabled: bool = Field(default=True, description="是否启用时间记忆退化")
|
|
98
|
+
short_term_days: int = Field(
|
|
99
|
+
default=7, ge=1, description="短期记忆保留天数(low 层寿命)"
|
|
100
|
+
)
|
|
101
|
+
long_term_days: int = Field(
|
|
102
|
+
default=90, ge=1, description="长期记忆保留天数(high 层寿命)"
|
|
103
|
+
)
|
|
104
|
+
half_life_days: float = Field(
|
|
105
|
+
default=30.0, ge=1.0, description="软衰减基础半衰期(天),low 为基准"
|
|
106
|
+
)
|
|
107
|
+
importance_boost: float = Field(
|
|
108
|
+
default=1.0,
|
|
109
|
+
ge=0.0,
|
|
110
|
+
le=5.0,
|
|
111
|
+
description="importance 对半衰期的放大系数,high 为 (1+2*boost) 倍",
|
|
112
|
+
)
|
|
113
|
+
overfetch: int = Field(
|
|
114
|
+
default=3, ge=1, le=10, description="软衰减检索的候选放大倍数"
|
|
115
|
+
)
|
|
116
|
+
score_floor: float = Field(
|
|
117
|
+
default=0.05, ge=0.0, le=1.0, description="衰减因子下限,避免旧记忆权重归零"
|
|
118
|
+
)
|
|
119
|
+
sweep_enabled: bool = Field(default=True, description="是否启用分层清理")
|
|
120
|
+
sweep_interval_hours: int = Field(
|
|
121
|
+
default=24, ge=1, description="分层清理周期(小时)"
|
|
122
|
+
)
|
|
123
|
+
sweep_batch_limit: int = Field(
|
|
124
|
+
default=5000, ge=100, description="单次清理最多扫描的记忆条数"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
94
128
|
class ConfigFile(BaseModel):
|
|
95
129
|
"""配置文件"""
|
|
96
130
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
long_term_expiry_days: int = Field(
|
|
101
|
-
default=90, description="长期记忆的过期天数,默认为90天"
|
|
131
|
+
decay: DecayConfig = Field(
|
|
132
|
+
default_factory=DecayConfig, description="时间记忆退化配置"
|
|
102
133
|
)
|
|
103
134
|
per_session_memory_limit: int = Field(
|
|
104
135
|
default=100, description="每个会话的记忆数量限制,默认为50条"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""时间记忆退化:软衰减(检索重排)与硬分层(降级 / 清理)。
|
|
2
|
+
|
|
3
|
+
本模块只放纯逻辑,不碰 I/O,便于单独验证。
|
|
4
|
+
- 软衰减:检索时按 created_at 的年龄压低得分,不删数据。
|
|
5
|
+
- 硬分层:按 importance 决定寿命阈值,超龄先降级、降到 low 再清理。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
Importance = Literal["low", "medium", "high"]
|
|
16
|
+
|
|
17
|
+
#: importance 的序,越大寿命越长
|
|
18
|
+
IMPORTANCE_RANK: dict[str, int] = {"low": 0, "medium": 1, "high": 2}
|
|
19
|
+
#: 降级链,走到尽头即清理
|
|
20
|
+
DEMOTE_CHAIN: dict[str, str] = {"high": "medium", "medium": "low"}
|
|
21
|
+
|
|
22
|
+
_SECONDS_PER_DAY = 86400.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_created_at(value: Any) -> datetime | None:
|
|
26
|
+
"""解析元数据里的 created_at,统一成 aware UTC;不可解析返回 None。"""
|
|
27
|
+
if isinstance(value, datetime):
|
|
28
|
+
dt = value
|
|
29
|
+
elif isinstance(value, str) and value:
|
|
30
|
+
try:
|
|
31
|
+
dt = datetime.fromisoformat(value)
|
|
32
|
+
except ValueError:
|
|
33
|
+
return None
|
|
34
|
+
else:
|
|
35
|
+
return None
|
|
36
|
+
if dt.tzinfo is None:
|
|
37
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
38
|
+
return dt.astimezone(timezone.utc)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def age_days(created_at: datetime, now: datetime) -> float:
|
|
42
|
+
"""年龄(天),未来时间按 0 处理。"""
|
|
43
|
+
return max((now - created_at).total_seconds() / _SECONDS_PER_DAY, 0.0)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def importance_half_life(base_days: float, importance: str, boost: float) -> float:
|
|
47
|
+
"""importance 越高,有效半衰期越长;low 为基准。"""
|
|
48
|
+
return base_days * (1.0 + boost * IMPORTANCE_RANK.get(importance, 1))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def decay_factor(age: float, half_life: float, floor: float) -> float:
|
|
52
|
+
"""指数衰减因子,下限为 floor,避免旧记忆权重归零。"""
|
|
53
|
+
if half_life <= 0:
|
|
54
|
+
return 1.0
|
|
55
|
+
return max(0.5 ** (age / half_life), floor)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def similarity_from_distance(distance: float) -> float:
|
|
59
|
+
"""把 Chroma 距离映射到 (0,1],对 l2 / cosine 都单调。"""
|
|
60
|
+
try:
|
|
61
|
+
d = float(distance)
|
|
62
|
+
except (TypeError, ValueError):
|
|
63
|
+
d = 0.0
|
|
64
|
+
return 1.0 / (1.0 + max(d, 0.0))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def decay_score(
|
|
68
|
+
similarity: float,
|
|
69
|
+
age: float,
|
|
70
|
+
importance: str,
|
|
71
|
+
base_half_life: float,
|
|
72
|
+
boost: float,
|
|
73
|
+
floor: float,
|
|
74
|
+
) -> float:
|
|
75
|
+
"""相似度 × 时间衰减因子。"""
|
|
76
|
+
half_life = importance_half_life(base_half_life, importance, boost)
|
|
77
|
+
return similarity * decay_factor(age, half_life, floor)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def tier_days(importance: str, short_days: float, long_days: float) -> float:
|
|
81
|
+
"""该 importance 层的寿命阈值(天)。medium 取 short/long 的几何中值。
|
|
82
|
+
|
|
83
|
+
两个配置项写反时按大小重排,保证 low ≤ medium ≤ high 单调,
|
|
84
|
+
否则 high 会比 medium 先过期。
|
|
85
|
+
"""
|
|
86
|
+
low_days, high_days = sorted((float(short_days), float(long_days)))
|
|
87
|
+
if importance == "low":
|
|
88
|
+
return low_days
|
|
89
|
+
if importance == "high":
|
|
90
|
+
return high_days
|
|
91
|
+
if high_days > low_days:
|
|
92
|
+
return math.sqrt(low_days * high_days)
|
|
93
|
+
return low_days
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def plan_action(
|
|
97
|
+
metadata: dict, now: datetime, short_days: float, long_days: float
|
|
98
|
+
) -> tuple[str, str | None]:
|
|
99
|
+
"""给出单条记忆的退化动作。
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
(动作, 新 importance),动作 ∈ {"keep", "demote", "purge"}。
|
|
103
|
+
仅 demote 会带新的 importance。
|
|
104
|
+
"""
|
|
105
|
+
created = parse_created_at(metadata.get("created_at"))
|
|
106
|
+
if created is None:
|
|
107
|
+
return "keep", None
|
|
108
|
+
importance = str(metadata.get("importance", "medium"))
|
|
109
|
+
if importance not in IMPORTANCE_RANK:
|
|
110
|
+
importance = "medium"
|
|
111
|
+
if age_days(created, now) <= tier_days(importance, short_days, long_days):
|
|
112
|
+
return "keep", None
|
|
113
|
+
nxt = DEMOTE_CHAIN.get(importance)
|
|
114
|
+
if nxt is None:
|
|
115
|
+
return "purge", None
|
|
116
|
+
return "demote", nxt
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _inner_list(value: Any, size: int) -> list:
|
|
120
|
+
"""取 QueryResult 里 list-of-list 的内层;兼容 numpy 数组;缺失时补 None。"""
|
|
121
|
+
if hasattr(value, "tolist"): # chroma 的 embeddings 可能是 ndarray
|
|
122
|
+
value = value.tolist()
|
|
123
|
+
if isinstance(value, list) and value and isinstance(value[0], list):
|
|
124
|
+
inner = list(value[0])
|
|
125
|
+
else:
|
|
126
|
+
inner = []
|
|
127
|
+
if len(inner) < size:
|
|
128
|
+
inner.extend([None] * (size - len(inner)))
|
|
129
|
+
return inner
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def rerank_query_result(
|
|
133
|
+
result: Mapping[str, Any],
|
|
134
|
+
top_k: int,
|
|
135
|
+
base_half_life: float,
|
|
136
|
+
boost: float,
|
|
137
|
+
floor: float,
|
|
138
|
+
now: datetime | None = None,
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
"""按衰减得分重排 QueryResult,并截断到 top_k;meta 里回填 decay_score。"""
|
|
141
|
+
now = now or datetime.now(timezone.utc)
|
|
142
|
+
ids_nested = result.get("ids")
|
|
143
|
+
if not ids_nested or not ids_nested[0]:
|
|
144
|
+
return dict(result)
|
|
145
|
+
ids = list(ids_nested[0])
|
|
146
|
+
size = len(ids)
|
|
147
|
+
metas = _inner_list(result.get("metadatas"), size)
|
|
148
|
+
dists = _inner_list(result.get("distances"), size)
|
|
149
|
+
|
|
150
|
+
scores: list[float] = []
|
|
151
|
+
for i in range(size):
|
|
152
|
+
meta = metas[i] if isinstance(metas[i], dict) else {}
|
|
153
|
+
created = parse_created_at(meta.get("created_at"))
|
|
154
|
+
age = age_days(created, now) if created else 0.0
|
|
155
|
+
similarity = similarity_from_distance(dists[i])
|
|
156
|
+
scores.append(
|
|
157
|
+
decay_score(
|
|
158
|
+
similarity,
|
|
159
|
+
age,
|
|
160
|
+
str(meta.get("importance", "medium")),
|
|
161
|
+
base_half_life,
|
|
162
|
+
boost,
|
|
163
|
+
floor,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
order = sorted(range(size), key=lambda i: scores[i], reverse=True)[:top_k]
|
|
168
|
+
|
|
169
|
+
out = dict(result)
|
|
170
|
+
for key in ("ids", "documents", "metadatas", "distances", "embeddings"):
|
|
171
|
+
value = result.get(key)
|
|
172
|
+
if value is None:
|
|
173
|
+
continue
|
|
174
|
+
inner = _inner_list(value, size)
|
|
175
|
+
out[key] = [[inner[i] for i in order]]
|
|
176
|
+
|
|
177
|
+
if result.get("metadatas") is not None:
|
|
178
|
+
reordered = []
|
|
179
|
+
for i in order:
|
|
180
|
+
meta = dict(metas[i]) if isinstance(metas[i], dict) else {}
|
|
181
|
+
meta["decay_score"] = round(scores[i], 4)
|
|
182
|
+
reordered.append(meta)
|
|
183
|
+
out["metadatas"] = [reordered]
|
|
184
|
+
return out
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""知识库读取的权限校验。
|
|
2
|
+
|
|
3
|
+
会话侧(对话 LLM)可以读全局知识库,但需要 LP 节点 ``memory.knowledge``;
|
|
4
|
+
``lp.admin`` 与 ``.env`` 里的超级用户始终持有该权限。
|
|
5
|
+
潜意识循环内部调用不受此限制。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from nonebot import get_driver, logger
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from nonebot.adapters.onebot.v11 import Event
|
|
16
|
+
|
|
17
|
+
#: 读取知识库所需的权限节点
|
|
18
|
+
KNOWLEDGE_NODE = "memory.knowledge"
|
|
19
|
+
#: 管理员节点,始终持有 KNOWLEDGE_NODE
|
|
20
|
+
ADMIN_NODE = "lp.admin"
|
|
21
|
+
|
|
22
|
+
_ENV_ADMINS = get_driver().config.superusers
|
|
23
|
+
|
|
24
|
+
_lp_warned = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _has_node(event: Event, node: str) -> bool:
|
|
28
|
+
"""查询 LP 节点;LP 不可用时按无权限处理。"""
|
|
29
|
+
global _lp_warned
|
|
30
|
+
try:
|
|
31
|
+
from amrita.plugins.perm.API.rules import any_has_permission
|
|
32
|
+
except Exception:
|
|
33
|
+
if not _lp_warned:
|
|
34
|
+
_lp_warned = True
|
|
35
|
+
logger.warning("[Memory] LitePerm 不可用,知识库读取仅对 .env 超级用户开放")
|
|
36
|
+
return False
|
|
37
|
+
try:
|
|
38
|
+
return bool(await any_has_permission(node)(event))
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.opt(exception=e, colors=True, raw=True).warning(
|
|
41
|
+
f"[Memory] 权限节点 {node} 校验失败: {e}"
|
|
42
|
+
)
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def can_read_knowledge(event: Event | None) -> bool:
|
|
47
|
+
"""是否允许读取全局知识库:超级用户 / lp.admin / memory.knowledge。"""
|
|
48
|
+
if event is None:
|
|
49
|
+
return False
|
|
50
|
+
if event.get_user_id() in _ENV_ADMINS:
|
|
51
|
+
return True
|
|
52
|
+
if await _has_node(event, ADMIN_NODE):
|
|
53
|
+
return True
|
|
54
|
+
return await _has_node(event, KNOWLEDGE_NODE)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_event_from_tool_ctx(ctx: Any) -> Event | None:
|
|
58
|
+
"""从 ToolContext 里取出触发本次调用的原始事件。"""
|
|
59
|
+
try:
|
|
60
|
+
from amrita.plugins.chat.runtime import try_get_amrita_ctx
|
|
61
|
+
|
|
62
|
+
amrita_ctx = try_get_amrita_ctx(ctx.ctx.chat_object)
|
|
63
|
+
if amrita_ctx is None:
|
|
64
|
+
return None
|
|
65
|
+
return amrita_ctx["event"]
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.debug(f"[Memory] 取事件失败: {e}")
|
|
68
|
+
return None
|
{amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/_state.py
RENAMED
|
@@ -41,7 +41,7 @@ def set_runner(r: SubconsciousRunner | None) -> None:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def get_target_user_id() -> str:
|
|
44
|
-
return _runner.
|
|
44
|
+
return _runner.target_user_id if _runner is not None else ""
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def get_pending() -> list[PendingMsg]:
|
{amrita_plugin_memory-0.5.0 → amrita_plugin_memory-0.6.0}/amrita_plugin_memory/rethinking/hooks.py
RENAMED
|
@@ -11,7 +11,6 @@ from . import _state
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
async def _setup_subconscious_hook() -> None:
|
|
14
|
-
|
|
15
14
|
pre_chat_hook = on_precompletion(priority=10, block=False)
|
|
16
15
|
|
|
17
16
|
@pre_chat_hook.handle()
|
|
@@ -22,10 +21,16 @@ async def _setup_subconscious_hook() -> None:
|
|
|
22
21
|
amrita_ctx = try_get_amrita_ctx(event.chat_object)
|
|
23
22
|
if amrita_ctx is None:
|
|
24
23
|
return
|
|
25
|
-
if str(amrita_ctx["event"].user_id) != runner.
|
|
24
|
+
if str(amrita_ctx["event"].user_id) != runner.target_user_id:
|
|
26
25
|
return
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
try:
|
|
28
|
+
await runner.cancel_and_reschedule()
|
|
29
|
+
except Exception as e:
|
|
30
|
+
# 调度失败不能影响用户这条消息的处理
|
|
31
|
+
logger.opt(exception=e, colors=True, raw=True).error(
|
|
32
|
+
f"[Subconscious] Reschedule failed: {e}"
|
|
33
|
+
)
|
|
29
34
|
|
|
30
35
|
pending = _state.get_pending()
|
|
31
36
|
if pending and runner._config.allow_send_to_user:
|
|
@@ -53,13 +58,17 @@ async def _subconscious_startup() -> None:
|
|
|
53
58
|
if not cfg.enabled or not cfg.target_user_id:
|
|
54
59
|
logger.debug("[Subconscious] Not enabled or no target user, skip")
|
|
55
60
|
return
|
|
56
|
-
runner
|
|
57
|
-
_state.set_runner(runner)
|
|
58
|
-
await runner.start()
|
|
61
|
+
# 先挂触发钩子:钩子失败就没有触发器,此时不能启动 runner,否则 DMN 静默死掉
|
|
59
62
|
try:
|
|
60
63
|
await _setup_subconscious_hook()
|
|
61
64
|
except Exception as e:
|
|
62
|
-
logger.
|
|
65
|
+
logger.opt(exception=e, colors=True, raw=True).error(
|
|
66
|
+
f"[Subconscious] Hook setup failed, DMN will NOT start: {e}"
|
|
67
|
+
)
|
|
68
|
+
return
|
|
69
|
+
runner = SubconsciousRunner(cfg)
|
|
70
|
+
_state.set_runner(runner)
|
|
71
|
+
await runner.start()
|
|
63
72
|
|
|
64
73
|
|
|
65
74
|
@get_driver().on_shutdown
|