memgov 0.1.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.
- memgov-0.1.0/LICENSE +21 -0
- memgov-0.1.0/PKG-INFO +108 -0
- memgov-0.1.0/README.md +82 -0
- memgov-0.1.0/memgov/__init__.py +40 -0
- memgov-0.1.0/memgov/__main__.py +4 -0
- memgov-0.1.0/memgov/cli.py +42 -0
- memgov-0.1.0/memgov/decay.py +45 -0
- memgov-0.1.0/memgov/governor.py +137 -0
- memgov-0.1.0/memgov/models.py +52 -0
- memgov-0.1.0/memgov/store.py +88 -0
- memgov-0.1.0/memgov.egg-info/PKG-INFO +108 -0
- memgov-0.1.0/memgov.egg-info/SOURCES.txt +18 -0
- memgov-0.1.0/memgov.egg-info/dependency_links.txt +1 -0
- memgov-0.1.0/memgov.egg-info/entry_points.txt +2 -0
- memgov-0.1.0/memgov.egg-info/requires.txt +4 -0
- memgov-0.1.0/memgov.egg-info/top_level.txt +1 -0
- memgov-0.1.0/pyproject.toml +39 -0
- memgov-0.1.0/setup.cfg +4 -0
- memgov-0.1.0/tests/test_decay.py +56 -0
- memgov-0.1.0/tests/test_governor.py +74 -0
memgov-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wangkevin2100-cell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
memgov-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memgov
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-agnostic governance layer for AI agent memory: reuse-aware decay, confidence-gated recall injection, and memory-health metrics. Governs the memory you already have (mem0/Zep/vector DB) instead of storing it.
|
|
5
|
+
Author: wangkevin2100-cell
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/wangkevin2100-cell/memgov
|
|
8
|
+
Project-URL: Repository, https://github.com/wangkevin2100-cell/memgov
|
|
9
|
+
Keywords: llm,ai-agents,memory,rag,decay,context,governance,mem0,zep
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# memgov
|
|
28
|
+
|
|
29
|
+
> Provider-agnostic **governance layer** for AI agent memory. It doesn't store
|
|
30
|
+
> your memory — it governs the memory you already have (mem0 / Zep / a raw
|
|
31
|
+
> vector DB): decays it by recency × reuse, gates recall injection behind a
|
|
32
|
+
> confidence threshold so stale junk never reaches the prompt, and surfaces
|
|
33
|
+
> memory-health metrics. Local-first (SQLite), zero required deps, BYO store.
|
|
34
|
+
|
|
35
|
+
## The problem
|
|
36
|
+
|
|
37
|
+
The 2025–2026 memory stores (mem0, Zep, LangMem, Letta) solved *write* and
|
|
38
|
+
*retrieve*. But after a quarter of accumulation, teams hit the second-order
|
|
39
|
+
problem: **memory useful in week 1 is actively harmful in week 12, and nothing
|
|
40
|
+
ages it out.** Stores answer "what do I remember?" — none answer "what should I
|
|
41
|
+
have *forgotten*, and what's safe to inject right now?" That's governance, and
|
|
42
|
+
it's missing.
|
|
43
|
+
|
|
44
|
+
memgov fills that lane:
|
|
45
|
+
|
|
46
|
+
- **Reuse-aware decay** — a tunable `recency × reuse` score, not a crude global TTL.
|
|
47
|
+
- **Confidence-gated recall** — always search, but only *inject* when retrieval
|
|
48
|
+
confidence clears a threshold and the memory isn't stale.
|
|
49
|
+
- **Memory health** — `active / fading / stale` ratios so silent degradation
|
|
50
|
+
becomes observable *before* answers get bad.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install memgov # zero required deps
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Python 3.11+.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from memgov import Governor, Candidate
|
|
64
|
+
|
|
65
|
+
gov = Governor() # local SQLite sidecar
|
|
66
|
+
|
|
67
|
+
# register memories your store already holds, keyed by the store's native id
|
|
68
|
+
gov.track("mem-1", agent_id="bot")
|
|
69
|
+
gov.reinforce("mem-1", agent_id="bot") # user restated it -> reinforce
|
|
70
|
+
|
|
71
|
+
gov.recompute(agent_id="bot") # recompute decay scores + states
|
|
72
|
+
|
|
73
|
+
# on recall: your store returns candidates; memgov gates what actually injects
|
|
74
|
+
candidates = [Candidate("mem-1", confidence=0.82),
|
|
75
|
+
Candidate("mem-9", confidence=0.31)] # low-confidence
|
|
76
|
+
inject = gov.gate(candidates, agent_id="bot") # only the worthy ones
|
|
77
|
+
print([c.mem_id for c in inject]) # -> ['mem-1']
|
|
78
|
+
|
|
79
|
+
print(gov.health(agent_id="bot")) # active/fading/stale snapshot
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## How it works
|
|
83
|
+
|
|
84
|
+
- **Sidecar metadata** keyed by your store's native `mem_id` (memgov never holds
|
|
85
|
+
the memory text — it tracks the signals decay needs).
|
|
86
|
+
- **`track / touch / reinforce`** record usage signals; `reinforce` (user
|
|
87
|
+
restated/confirmed) is the strongest keep signal.
|
|
88
|
+
- **`recompute`** rescans an agent's memories and assigns `decay_score` (0..1)
|
|
89
|
+
and a `state`: `active | fading | stale | pinned`.
|
|
90
|
+
- **`gate`** is the core: it receives recall candidates and passes only those
|
|
91
|
+
with confidence ≥ threshold **and** not `stale`. `pin` protects a memory from
|
|
92
|
+
ever decaying.
|
|
93
|
+
|
|
94
|
+
## See it (no deps, no API key)
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python examples/demo_governance.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Status
|
|
101
|
+
|
|
102
|
+
Early MVP (v0.1). Implemented: decay scorer, gate middleware, health metrics,
|
|
103
|
+
SQLite sidecar, CLI. Planned: store adapters (mem0/Zep), MCP-server option,
|
|
104
|
+
embedding-confidence helpers, health dashboard.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT.
|
memgov-0.1.0/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# memgov
|
|
2
|
+
|
|
3
|
+
> Provider-agnostic **governance layer** for AI agent memory. It doesn't store
|
|
4
|
+
> your memory — it governs the memory you already have (mem0 / Zep / a raw
|
|
5
|
+
> vector DB): decays it by recency × reuse, gates recall injection behind a
|
|
6
|
+
> confidence threshold so stale junk never reaches the prompt, and surfaces
|
|
7
|
+
> memory-health metrics. Local-first (SQLite), zero required deps, BYO store.
|
|
8
|
+
|
|
9
|
+
## The problem
|
|
10
|
+
|
|
11
|
+
The 2025–2026 memory stores (mem0, Zep, LangMem, Letta) solved *write* and
|
|
12
|
+
*retrieve*. But after a quarter of accumulation, teams hit the second-order
|
|
13
|
+
problem: **memory useful in week 1 is actively harmful in week 12, and nothing
|
|
14
|
+
ages it out.** Stores answer "what do I remember?" — none answer "what should I
|
|
15
|
+
have *forgotten*, and what's safe to inject right now?" That's governance, and
|
|
16
|
+
it's missing.
|
|
17
|
+
|
|
18
|
+
memgov fills that lane:
|
|
19
|
+
|
|
20
|
+
- **Reuse-aware decay** — a tunable `recency × reuse` score, not a crude global TTL.
|
|
21
|
+
- **Confidence-gated recall** — always search, but only *inject* when retrieval
|
|
22
|
+
confidence clears a threshold and the memory isn't stale.
|
|
23
|
+
- **Memory health** — `active / fading / stale` ratios so silent degradation
|
|
24
|
+
becomes observable *before* answers get bad.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install memgov # zero required deps
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Python 3.11+.
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from memgov import Governor, Candidate
|
|
38
|
+
|
|
39
|
+
gov = Governor() # local SQLite sidecar
|
|
40
|
+
|
|
41
|
+
# register memories your store already holds, keyed by the store's native id
|
|
42
|
+
gov.track("mem-1", agent_id="bot")
|
|
43
|
+
gov.reinforce("mem-1", agent_id="bot") # user restated it -> reinforce
|
|
44
|
+
|
|
45
|
+
gov.recompute(agent_id="bot") # recompute decay scores + states
|
|
46
|
+
|
|
47
|
+
# on recall: your store returns candidates; memgov gates what actually injects
|
|
48
|
+
candidates = [Candidate("mem-1", confidence=0.82),
|
|
49
|
+
Candidate("mem-9", confidence=0.31)] # low-confidence
|
|
50
|
+
inject = gov.gate(candidates, agent_id="bot") # only the worthy ones
|
|
51
|
+
print([c.mem_id for c in inject]) # -> ['mem-1']
|
|
52
|
+
|
|
53
|
+
print(gov.health(agent_id="bot")) # active/fading/stale snapshot
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## How it works
|
|
57
|
+
|
|
58
|
+
- **Sidecar metadata** keyed by your store's native `mem_id` (memgov never holds
|
|
59
|
+
the memory text — it tracks the signals decay needs).
|
|
60
|
+
- **`track / touch / reinforce`** record usage signals; `reinforce` (user
|
|
61
|
+
restated/confirmed) is the strongest keep signal.
|
|
62
|
+
- **`recompute`** rescans an agent's memories and assigns `decay_score` (0..1)
|
|
63
|
+
and a `state`: `active | fading | stale | pinned`.
|
|
64
|
+
- **`gate`** is the core: it receives recall candidates and passes only those
|
|
65
|
+
with confidence ≥ threshold **and** not `stale`. `pin` protects a memory from
|
|
66
|
+
ever decaying.
|
|
67
|
+
|
|
68
|
+
## See it (no deps, no API key)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python examples/demo_governance.py
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Status
|
|
75
|
+
|
|
76
|
+
Early MVP (v0.1). Implemented: decay scorer, gate middleware, health metrics,
|
|
77
|
+
SQLite sidecar, CLI. Planned: store adapters (mem0/Zep), MCP-server option,
|
|
78
|
+
embedding-confidence helpers, health dashboard.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""memgov — provider-agnostic governance layer for AI agent memory.
|
|
2
|
+
|
|
3
|
+
不存储记忆,而是治理你已有的记忆(mem0 / Zep / 向量库):
|
|
4
|
+
- 按 recency × reuse 衰减,让陈旧记忆自然淡出
|
|
5
|
+
- 召回时按置信度门控注入,陈旧垃圾不进 prompt
|
|
6
|
+
- 出"记忆健康度"指标,让静默退化变可观测
|
|
7
|
+
|
|
8
|
+
Quick start::
|
|
9
|
+
|
|
10
|
+
from memgov import Governor, Candidate
|
|
11
|
+
|
|
12
|
+
gov = Governor()
|
|
13
|
+
gov.track("mem-1", agent_id="bot") # 登记一条记忆进治理层
|
|
14
|
+
gov.reinforce("mem-1", agent_id="bot") # 用户重申 -> 强化保留
|
|
15
|
+
|
|
16
|
+
gov.recompute(agent_id="bot") # 重算衰减分 + 状态
|
|
17
|
+
# 召回时门控:只放行置信度够且没陈旧的
|
|
18
|
+
keep = gov.gate([Candidate("mem-1", confidence=0.8)], agent_id="bot")
|
|
19
|
+
print(gov.health(agent_id="bot"))
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from .decay import decay_score, recency_factor, reuse_bonus
|
|
24
|
+
from .governor import Governor
|
|
25
|
+
from .models import Candidate, HealthReport, MemoryMeta, MemoryState
|
|
26
|
+
from .store import MetaStore
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Governor",
|
|
32
|
+
"MetaStore",
|
|
33
|
+
"MemoryMeta",
|
|
34
|
+
"MemoryState",
|
|
35
|
+
"Candidate",
|
|
36
|
+
"HealthReport",
|
|
37
|
+
"decay_score",
|
|
38
|
+
"recency_factor",
|
|
39
|
+
"reuse_bonus",
|
|
40
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""memgov CLI:查看记忆健康度、重算衰减。
|
|
2
|
+
|
|
3
|
+
用法:
|
|
4
|
+
memgov health [--agent NAME] [--db PATH] 查看健康度
|
|
5
|
+
memgov recompute [--agent NAME] [--db PATH] 重算衰减分+状态
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from .governor import Governor
|
|
13
|
+
from .store import MetaStore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str] | None = None) -> int:
|
|
17
|
+
p = argparse.ArgumentParser(prog="memgov")
|
|
18
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
19
|
+
for name in ("health", "recompute"):
|
|
20
|
+
sp = sub.add_parser(name)
|
|
21
|
+
sp.add_argument("--agent", default="default")
|
|
22
|
+
sp.add_argument("--db", default=".memgov/memgov.db")
|
|
23
|
+
|
|
24
|
+
args = p.parse_args(argv)
|
|
25
|
+
gov = Governor(store=MetaStore(args.db))
|
|
26
|
+
|
|
27
|
+
if args.cmd == "recompute":
|
|
28
|
+
n = gov.recompute(agent_id=args.agent)
|
|
29
|
+
print(f"recomputed {n} memories for agent '{args.agent}'")
|
|
30
|
+
return 0
|
|
31
|
+
if args.cmd == "health":
|
|
32
|
+
h = gov.health(agent_id=args.agent)
|
|
33
|
+
print(f"agent={h.agent_id} total={h.total} active={h.active} "
|
|
34
|
+
f"fading={h.fading} stale={h.stale} pinned={h.pinned} "
|
|
35
|
+
f"stale_ratio={h.stale_ratio:.2f} avg_score={h.avg_decay_score:.3f}")
|
|
36
|
+
return 0
|
|
37
|
+
p.print_help()
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
sys.exit(main())
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""衰减评分:recency(时间新近)× reuse(复用/强化)。
|
|
2
|
+
|
|
3
|
+
这是 memgov 的核心——决定一条记忆"现在还该不该算数"。纯函数,无副作用,
|
|
4
|
+
便于属性测试。返回 [0,1] 的分数:越高 = 越该保留并参与注入。
|
|
5
|
+
|
|
6
|
+
设计:
|
|
7
|
+
- recency:自上次访问起按半衰期指数衰减。越久没碰,分越低。
|
|
8
|
+
- reuse:访问/强化越多,越该保留——给一个有上限的加成(防止被刷爆)。
|
|
9
|
+
- pinned 由上层处理(钉住的不走衰减)。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
|
|
15
|
+
# 默认参数(可调)
|
|
16
|
+
DEFAULT_HALF_LIFE_DAYS = 14.0 # 14 天没访问,recency 减半
|
|
17
|
+
REINFORCE_BONUS = 0.15 # 每次强化的加成
|
|
18
|
+
MAX_REUSE_BONUS = 0.5 # reuse 加成上限
|
|
19
|
+
_SECONDS_PER_DAY = 86400.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def recency_factor(age_seconds: float, half_life_days: float = DEFAULT_HALF_LIFE_DAYS) -> float:
|
|
23
|
+
"""自上次访问的时间衰减因子,∈ (0, 1]。age=0 -> 1.0,越久越接近 0。"""
|
|
24
|
+
if age_seconds <= 0:
|
|
25
|
+
return 1.0
|
|
26
|
+
half_life_seconds = max(half_life_days, 1e-9) * _SECONDS_PER_DAY
|
|
27
|
+
return math.pow(0.5, age_seconds / half_life_seconds)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def reuse_bonus(access_count: int, reinforce_count: int) -> float:
|
|
31
|
+
"""复用加成,∈ [0, MAX_REUSE_BONUS]。强化比普通访问权重高。"""
|
|
32
|
+
raw = REINFORCE_BONUS * reinforce_count + 0.02 * max(access_count, 0)
|
|
33
|
+
return min(max(raw, 0.0), MAX_REUSE_BONUS)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decay_score(
|
|
37
|
+
age_seconds: float,
|
|
38
|
+
access_count: int = 0,
|
|
39
|
+
reinforce_count: int = 0,
|
|
40
|
+
half_life_days: float = DEFAULT_HALF_LIFE_DAYS,
|
|
41
|
+
) -> float:
|
|
42
|
+
"""综合衰减分,∈ [0,1]。recency 为基底,reuse 在其上加成后截断到 1。"""
|
|
43
|
+
base = recency_factor(age_seconds, half_life_days)
|
|
44
|
+
score = base + reuse_bonus(access_count, reinforce_count) * base
|
|
45
|
+
return min(max(score, 0.0), 1.0)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Governor:记忆治理中间件主类。
|
|
2
|
+
|
|
3
|
+
套在你已有的记忆存储前面,做三件事(spec 的三大核心):
|
|
4
|
+
1. 衰减治理:track/touch/reinforce 记录信号,recompute 按 recency×reuse 重算分数+状态
|
|
5
|
+
2. 召回门控:gate() 总是接收存储召回的候选,但只放行"置信度够 + 没陈旧"的注入
|
|
6
|
+
3. 健康度:health() 出 active/fading/stale 比例,让"静默退化"变可观测
|
|
7
|
+
它不替你存记忆,只治理你已有的记忆。
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from .decay import DEFAULT_HALF_LIFE_DAYS, decay_score
|
|
14
|
+
from .models import Candidate, HealthReport, MemoryMeta, MemoryState
|
|
15
|
+
from .store import MetaStore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Governor:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
store: MetaStore | None = None,
|
|
22
|
+
*,
|
|
23
|
+
half_life_days: float = DEFAULT_HALF_LIFE_DAYS,
|
|
24
|
+
active_threshold: float = 0.5, # >= 视为 active
|
|
25
|
+
fading_threshold: float = 0.2, # >= 视为 fading,否则 stale
|
|
26
|
+
min_confidence: float = 0.6, # 召回注入的默认置信度门槛
|
|
27
|
+
):
|
|
28
|
+
self.store = store or MetaStore()
|
|
29
|
+
self.half_life_days = half_life_days
|
|
30
|
+
self.active_threshold = active_threshold
|
|
31
|
+
self.fading_threshold = fading_threshold
|
|
32
|
+
self.min_confidence = min_confidence
|
|
33
|
+
|
|
34
|
+
# ---- 记录治理信号 ----
|
|
35
|
+
def track(self, mem_id: str, *, agent_id: str = "default",
|
|
36
|
+
source: str = "user_msg", now: float | None = None) -> MemoryMeta:
|
|
37
|
+
"""登记一条新记忆进治理层。"""
|
|
38
|
+
t = now if now is not None else time.time()
|
|
39
|
+
m = MemoryMeta(mem_id=mem_id, agent_id=agent_id, created_utc=t,
|
|
40
|
+
last_access_utc=t, access_count=0, reinforce_count=0,
|
|
41
|
+
decay_score=1.0, state=MemoryState.ACTIVE,
|
|
42
|
+
source=source, last_eval_utc=t)
|
|
43
|
+
self.store.upsert(m)
|
|
44
|
+
return m
|
|
45
|
+
|
|
46
|
+
def touch(self, mem_id: str, *, agent_id: str = "default",
|
|
47
|
+
now: float | None = None) -> None:
|
|
48
|
+
"""记忆被召回/读取——更新访问时间和计数。"""
|
|
49
|
+
m = self.store.get(mem_id, agent_id)
|
|
50
|
+
if m is None:
|
|
51
|
+
m = self.track(mem_id, agent_id=agent_id, now=now)
|
|
52
|
+
m.last_access_utc = now if now is not None else time.time()
|
|
53
|
+
m.access_count += 1
|
|
54
|
+
self.store.upsert(m)
|
|
55
|
+
|
|
56
|
+
def reinforce(self, mem_id: str, *, agent_id: str = "default",
|
|
57
|
+
now: float | None = None) -> None:
|
|
58
|
+
"""记忆被显式强化(用户重申/确认)——最强的保留信号。"""
|
|
59
|
+
m = self.store.get(mem_id, agent_id)
|
|
60
|
+
if m is None:
|
|
61
|
+
m = self.track(mem_id, agent_id=agent_id, now=now)
|
|
62
|
+
m.reinforce_count += 1
|
|
63
|
+
m.last_access_utc = now if now is not None else time.time()
|
|
64
|
+
self.store.upsert(m)
|
|
65
|
+
|
|
66
|
+
def pin(self, mem_id: str, *, agent_id: str = "default") -> None:
|
|
67
|
+
m = self.store.get(mem_id, agent_id)
|
|
68
|
+
if m:
|
|
69
|
+
m.state = MemoryState.PINNED
|
|
70
|
+
self.store.upsert(m)
|
|
71
|
+
|
|
72
|
+
# ---- 重算衰减 ----
|
|
73
|
+
def recompute(self, *, agent_id: str = "default", now: float | None = None) -> int:
|
|
74
|
+
"""重算某 agent 所有记忆的 decay_score 和 state。返回处理条数。"""
|
|
75
|
+
t = now if now is not None else time.time()
|
|
76
|
+
metas = self.store.list_for_agent(agent_id)
|
|
77
|
+
for m in metas:
|
|
78
|
+
if m.state == MemoryState.PINNED:
|
|
79
|
+
continue
|
|
80
|
+
age = max(t - m.last_access_utc, 0.0)
|
|
81
|
+
m.decay_score = decay_score(age, m.access_count, m.reinforce_count,
|
|
82
|
+
self.half_life_days)
|
|
83
|
+
m.state = self._state_for(m.decay_score)
|
|
84
|
+
m.last_eval_utc = t
|
|
85
|
+
self.store.upsert(m)
|
|
86
|
+
return len(metas)
|
|
87
|
+
|
|
88
|
+
def _state_for(self, score: float) -> MemoryState:
|
|
89
|
+
if score >= self.active_threshold:
|
|
90
|
+
return MemoryState.ACTIVE
|
|
91
|
+
if score >= self.fading_threshold:
|
|
92
|
+
return MemoryState.FADING
|
|
93
|
+
return MemoryState.STALE
|
|
94
|
+
|
|
95
|
+
# ---- 召回门控(核心价值)----
|
|
96
|
+
def gate(self, candidates: list[Candidate], *, agent_id: str = "default",
|
|
97
|
+
min_confidence: float | None = None,
|
|
98
|
+
include_fading: bool = True) -> list[Candidate]:
|
|
99
|
+
"""门控召回注入:总是接收候选,但只放行该注入的。
|
|
100
|
+
|
|
101
|
+
规则:置信度 >= 门槛 且 状态不是 stale(fading 可选放行)。
|
|
102
|
+
chosen 的会顺便 touch(被注入即被使用)。
|
|
103
|
+
"""
|
|
104
|
+
thr = min_confidence if min_confidence is not None else self.min_confidence
|
|
105
|
+
chosen: list[Candidate] = []
|
|
106
|
+
for c in candidates:
|
|
107
|
+
if c.confidence < thr:
|
|
108
|
+
continue
|
|
109
|
+
m = self.store.get(c.mem_id, agent_id)
|
|
110
|
+
state = m.state if m else MemoryState.ACTIVE
|
|
111
|
+
if state == MemoryState.STALE:
|
|
112
|
+
continue
|
|
113
|
+
if state == MemoryState.FADING and not include_fading:
|
|
114
|
+
continue
|
|
115
|
+
chosen.append(c)
|
|
116
|
+
for c in chosen:
|
|
117
|
+
self.touch(c.mem_id, agent_id=agent_id)
|
|
118
|
+
return chosen
|
|
119
|
+
|
|
120
|
+
# ---- 健康度 ----
|
|
121
|
+
def health(self, *, agent_id: str = "default") -> HealthReport:
|
|
122
|
+
metas = self.store.list_for_agent(agent_id)
|
|
123
|
+
rep = HealthReport(agent_id=agent_id, total=len(metas))
|
|
124
|
+
if not metas:
|
|
125
|
+
return rep
|
|
126
|
+
for m in metas:
|
|
127
|
+
if m.state == MemoryState.ACTIVE:
|
|
128
|
+
rep.active += 1
|
|
129
|
+
elif m.state == MemoryState.FADING:
|
|
130
|
+
rep.fading += 1
|
|
131
|
+
elif m.state == MemoryState.STALE:
|
|
132
|
+
rep.stale += 1
|
|
133
|
+
elif m.state == MemoryState.PINNED:
|
|
134
|
+
rep.pinned += 1
|
|
135
|
+
rep.stale_ratio = rep.stale / rep.total
|
|
136
|
+
rep.avg_decay_score = sum(m.decay_score for m in metas) / rep.total
|
|
137
|
+
return rep
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""数据模型:每条被治理记忆的 sidecar 元数据。
|
|
2
|
+
|
|
3
|
+
memgov 不存储记忆本身——它在你已有的记忆存储(mem0/Zep/向量库)旁边,
|
|
4
|
+
按存储原生的 mem_id 镜像一份"治理信号",用来算衰减、决定注入、出健康度。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MemoryState(str, Enum):
|
|
13
|
+
ACTIVE = "active" # 活跃,正常参与召回注入
|
|
14
|
+
FADING = "fading" # 在衰退,注入需更高置信度
|
|
15
|
+
STALE = "stale" # 陈旧,默认不注入(除非显式)
|
|
16
|
+
PINNED = "pinned" # 钉住,永不衰减/永不 stale
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class MemoryMeta:
|
|
21
|
+
"""一条记忆的治理元数据(对应 spec 的 sidecar schema)。"""
|
|
22
|
+
mem_id: str # 存储原生 id
|
|
23
|
+
agent_id: str = "default"
|
|
24
|
+
created_utc: float = 0.0
|
|
25
|
+
last_access_utc: float = 0.0
|
|
26
|
+
access_count: int = 0 # 被召回/读取次数
|
|
27
|
+
reinforce_count: int = 0 # 被显式强化(用户重申/确认)次数
|
|
28
|
+
decay_score: float = 1.0 # 0..1,越高越该保留
|
|
29
|
+
state: MemoryState = MemoryState.ACTIVE
|
|
30
|
+
source: str = "user_msg" # user_msg | tool_result | summary
|
|
31
|
+
last_eval_utc: float = 0.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Candidate:
|
|
36
|
+
"""一次召回返回的候选记忆:存储给的 id + 相似度/置信度。"""
|
|
37
|
+
mem_id: str
|
|
38
|
+
confidence: float # 召回置信度(如向量相似度 0..1)
|
|
39
|
+
text: str = ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class HealthReport:
|
|
44
|
+
"""记忆健康度快照。"""
|
|
45
|
+
agent_id: str
|
|
46
|
+
total: int = 0
|
|
47
|
+
active: int = 0
|
|
48
|
+
fading: int = 0
|
|
49
|
+
stale: int = 0
|
|
50
|
+
pinned: int = 0
|
|
51
|
+
stale_ratio: float = 0.0 # stale / total,越高越该清理
|
|
52
|
+
avg_decay_score: float = 0.0
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""SQLite 持久化:记忆治理元数据的 sidecar 存储。本地优先,零外部依赖。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sqlite3
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .models import MemoryMeta, MemoryState
|
|
8
|
+
|
|
9
|
+
_SCHEMA = """
|
|
10
|
+
CREATE TABLE IF NOT EXISTS memory_meta (
|
|
11
|
+
mem_id TEXT NOT NULL,
|
|
12
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
13
|
+
created_utc REAL DEFAULT 0,
|
|
14
|
+
last_access_utc REAL DEFAULT 0,
|
|
15
|
+
access_count INTEGER DEFAULT 0,
|
|
16
|
+
reinforce_count INTEGER DEFAULT 0,
|
|
17
|
+
decay_score REAL DEFAULT 1.0,
|
|
18
|
+
state TEXT DEFAULT 'active',
|
|
19
|
+
source TEXT DEFAULT 'user_msg',
|
|
20
|
+
last_eval_utc REAL DEFAULT 0,
|
|
21
|
+
PRIMARY KEY (agent_id, mem_id)
|
|
22
|
+
);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_meta_agent_state ON memory_meta(agent_id, state);
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MetaStore:
|
|
28
|
+
"""治理元数据的 SQLite 存储。"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, db_path: str | Path = ".memgov/memgov.db"):
|
|
31
|
+
self.db_path = Path(db_path)
|
|
32
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
self._init()
|
|
34
|
+
|
|
35
|
+
def _conn(self) -> sqlite3.Connection:
|
|
36
|
+
c = sqlite3.connect(str(self.db_path))
|
|
37
|
+
c.row_factory = sqlite3.Row
|
|
38
|
+
return c
|
|
39
|
+
|
|
40
|
+
def _init(self) -> None:
|
|
41
|
+
with self._conn() as c:
|
|
42
|
+
c.executescript(_SCHEMA)
|
|
43
|
+
|
|
44
|
+
def upsert(self, m: MemoryMeta) -> None:
|
|
45
|
+
with self._conn() as c:
|
|
46
|
+
c.execute(
|
|
47
|
+
"""INSERT INTO memory_meta
|
|
48
|
+
(mem_id, agent_id, created_utc, last_access_utc, access_count,
|
|
49
|
+
reinforce_count, decay_score, state, source, last_eval_utc)
|
|
50
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
51
|
+
ON CONFLICT(agent_id, mem_id) DO UPDATE SET
|
|
52
|
+
created_utc=excluded.created_utc,
|
|
53
|
+
last_access_utc=excluded.last_access_utc,
|
|
54
|
+
access_count=excluded.access_count,
|
|
55
|
+
reinforce_count=excluded.reinforce_count,
|
|
56
|
+
decay_score=excluded.decay_score,
|
|
57
|
+
state=excluded.state,
|
|
58
|
+
source=excluded.source,
|
|
59
|
+
last_eval_utc=excluded.last_eval_utc""",
|
|
60
|
+
(m.mem_id, m.agent_id, m.created_utc, m.last_access_utc,
|
|
61
|
+
m.access_count, m.reinforce_count, m.decay_score,
|
|
62
|
+
m.state.value, m.source, m.last_eval_utc),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def get(self, mem_id: str, agent_id: str = "default") -> MemoryMeta | None:
|
|
66
|
+
with self._conn() as c:
|
|
67
|
+
r = c.execute(
|
|
68
|
+
"SELECT * FROM memory_meta WHERE agent_id=? AND mem_id=?",
|
|
69
|
+
(agent_id, mem_id),
|
|
70
|
+
).fetchone()
|
|
71
|
+
return _row_to_meta(r) if r else None
|
|
72
|
+
|
|
73
|
+
def list_for_agent(self, agent_id: str = "default") -> list[MemoryMeta]:
|
|
74
|
+
with self._conn() as c:
|
|
75
|
+
rows = c.execute(
|
|
76
|
+
"SELECT * FROM memory_meta WHERE agent_id=?", (agent_id,)
|
|
77
|
+
).fetchall()
|
|
78
|
+
return [_row_to_meta(r) for r in rows]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _row_to_meta(r: sqlite3.Row) -> MemoryMeta:
|
|
82
|
+
return MemoryMeta(
|
|
83
|
+
mem_id=r["mem_id"], agent_id=r["agent_id"],
|
|
84
|
+
created_utc=r["created_utc"], last_access_utc=r["last_access_utc"],
|
|
85
|
+
access_count=r["access_count"], reinforce_count=r["reinforce_count"],
|
|
86
|
+
decay_score=r["decay_score"], state=MemoryState(r["state"]),
|
|
87
|
+
source=r["source"], last_eval_utc=r["last_eval_utc"],
|
|
88
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memgov
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-agnostic governance layer for AI agent memory: reuse-aware decay, confidence-gated recall injection, and memory-health metrics. Governs the memory you already have (mem0/Zep/vector DB) instead of storing it.
|
|
5
|
+
Author: wangkevin2100-cell
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/wangkevin2100-cell/memgov
|
|
8
|
+
Project-URL: Repository, https://github.com/wangkevin2100-cell/memgov
|
|
9
|
+
Keywords: llm,ai-agents,memory,rag,decay,context,governance,mem0,zep
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# memgov
|
|
28
|
+
|
|
29
|
+
> Provider-agnostic **governance layer** for AI agent memory. It doesn't store
|
|
30
|
+
> your memory — it governs the memory you already have (mem0 / Zep / a raw
|
|
31
|
+
> vector DB): decays it by recency × reuse, gates recall injection behind a
|
|
32
|
+
> confidence threshold so stale junk never reaches the prompt, and surfaces
|
|
33
|
+
> memory-health metrics. Local-first (SQLite), zero required deps, BYO store.
|
|
34
|
+
|
|
35
|
+
## The problem
|
|
36
|
+
|
|
37
|
+
The 2025–2026 memory stores (mem0, Zep, LangMem, Letta) solved *write* and
|
|
38
|
+
*retrieve*. But after a quarter of accumulation, teams hit the second-order
|
|
39
|
+
problem: **memory useful in week 1 is actively harmful in week 12, and nothing
|
|
40
|
+
ages it out.** Stores answer "what do I remember?" — none answer "what should I
|
|
41
|
+
have *forgotten*, and what's safe to inject right now?" That's governance, and
|
|
42
|
+
it's missing.
|
|
43
|
+
|
|
44
|
+
memgov fills that lane:
|
|
45
|
+
|
|
46
|
+
- **Reuse-aware decay** — a tunable `recency × reuse` score, not a crude global TTL.
|
|
47
|
+
- **Confidence-gated recall** — always search, but only *inject* when retrieval
|
|
48
|
+
confidence clears a threshold and the memory isn't stale.
|
|
49
|
+
- **Memory health** — `active / fading / stale` ratios so silent degradation
|
|
50
|
+
becomes observable *before* answers get bad.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install memgov # zero required deps
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Python 3.11+.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from memgov import Governor, Candidate
|
|
64
|
+
|
|
65
|
+
gov = Governor() # local SQLite sidecar
|
|
66
|
+
|
|
67
|
+
# register memories your store already holds, keyed by the store's native id
|
|
68
|
+
gov.track("mem-1", agent_id="bot")
|
|
69
|
+
gov.reinforce("mem-1", agent_id="bot") # user restated it -> reinforce
|
|
70
|
+
|
|
71
|
+
gov.recompute(agent_id="bot") # recompute decay scores + states
|
|
72
|
+
|
|
73
|
+
# on recall: your store returns candidates; memgov gates what actually injects
|
|
74
|
+
candidates = [Candidate("mem-1", confidence=0.82),
|
|
75
|
+
Candidate("mem-9", confidence=0.31)] # low-confidence
|
|
76
|
+
inject = gov.gate(candidates, agent_id="bot") # only the worthy ones
|
|
77
|
+
print([c.mem_id for c in inject]) # -> ['mem-1']
|
|
78
|
+
|
|
79
|
+
print(gov.health(agent_id="bot")) # active/fading/stale snapshot
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## How it works
|
|
83
|
+
|
|
84
|
+
- **Sidecar metadata** keyed by your store's native `mem_id` (memgov never holds
|
|
85
|
+
the memory text — it tracks the signals decay needs).
|
|
86
|
+
- **`track / touch / reinforce`** record usage signals; `reinforce` (user
|
|
87
|
+
restated/confirmed) is the strongest keep signal.
|
|
88
|
+
- **`recompute`** rescans an agent's memories and assigns `decay_score` (0..1)
|
|
89
|
+
and a `state`: `active | fading | stale | pinned`.
|
|
90
|
+
- **`gate`** is the core: it receives recall candidates and passes only those
|
|
91
|
+
with confidence ≥ threshold **and** not `stale`. `pin` protects a memory from
|
|
92
|
+
ever decaying.
|
|
93
|
+
|
|
94
|
+
## See it (no deps, no API key)
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python examples/demo_governance.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Status
|
|
101
|
+
|
|
102
|
+
Early MVP (v0.1). Implemented: decay scorer, gate middleware, health metrics,
|
|
103
|
+
SQLite sidecar, CLI. Planned: store adapters (mem0/Zep), MCP-server option,
|
|
104
|
+
embedding-confidence helpers, health dashboard.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
memgov/__init__.py
|
|
5
|
+
memgov/__main__.py
|
|
6
|
+
memgov/cli.py
|
|
7
|
+
memgov/decay.py
|
|
8
|
+
memgov/governor.py
|
|
9
|
+
memgov/models.py
|
|
10
|
+
memgov/store.py
|
|
11
|
+
memgov.egg-info/PKG-INFO
|
|
12
|
+
memgov.egg-info/SOURCES.txt
|
|
13
|
+
memgov.egg-info/dependency_links.txt
|
|
14
|
+
memgov.egg-info/entry_points.txt
|
|
15
|
+
memgov.egg-info/requires.txt
|
|
16
|
+
memgov.egg-info/top_level.txt
|
|
17
|
+
tests/test_decay.py
|
|
18
|
+
tests/test_governor.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
memgov
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "memgov"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Provider-agnostic governance layer for AI agent memory: reuse-aware decay, confidence-gated recall injection, and memory-health metrics. Governs the memory you already have (mem0/Zep/vector DB) instead of storing it."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "wangkevin2100-cell" }]
|
|
9
|
+
keywords = ["llm", "ai-agents", "memory", "rag", "decay", "context", "governance", "mem0", "zep"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["pytest>=8.0", "hypothesis>=6.0"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
memgov = "memgov.cli:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/wangkevin2100-cell/memgov"
|
|
31
|
+
Repository = "https://github.com/wangkevin2100-cell/memgov"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["setuptools>=68"]
|
|
35
|
+
build-backend = "setuptools.build_meta"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["."]
|
|
39
|
+
include = ["memgov*"]
|
memgov-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""decay 评分的单元测试 + 正确性属性。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from hypothesis import given, settings
|
|
5
|
+
from hypothesis import strategies as st
|
|
6
|
+
|
|
7
|
+
from memgov.decay import decay_score, recency_factor, reuse_bonus
|
|
8
|
+
|
|
9
|
+
_DAY = 86400.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Feature: memgov, Property: decay_score 永远落在 [0, 1]
|
|
13
|
+
@settings(max_examples=200)
|
|
14
|
+
@given(
|
|
15
|
+
age=st.floats(min_value=0, max_value=10_000 * _DAY, allow_nan=False),
|
|
16
|
+
acc=st.integers(min_value=0, max_value=10_000),
|
|
17
|
+
rein=st.integers(min_value=0, max_value=10_000),
|
|
18
|
+
)
|
|
19
|
+
def test_score_in_unit_interval(age, acc, rein):
|
|
20
|
+
s = decay_score(age, acc, rein)
|
|
21
|
+
assert 0.0 <= s <= 1.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Feature: memgov, Property: 其它不变时,越久没访问分越低(recency 单调)
|
|
25
|
+
@settings(max_examples=100)
|
|
26
|
+
@given(
|
|
27
|
+
age1=st.floats(min_value=0, max_value=100 * _DAY),
|
|
28
|
+
extra=st.floats(min_value=1.0, max_value=100 * _DAY),
|
|
29
|
+
)
|
|
30
|
+
def test_recency_monotonic(age1, extra):
|
|
31
|
+
assert recency_factor(age1) >= recency_factor(age1 + extra) - 1e-12
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Feature: memgov, Property: 强化越多,保留分不降(reuse 单调不减)
|
|
35
|
+
@settings(max_examples=100)
|
|
36
|
+
@given(
|
|
37
|
+
age=st.floats(min_value=0, max_value=30 * _DAY),
|
|
38
|
+
rein=st.integers(min_value=0, max_value=50),
|
|
39
|
+
)
|
|
40
|
+
def test_reinforce_does_not_decrease_score(age, rein):
|
|
41
|
+
s_low = decay_score(age, 0, rein)
|
|
42
|
+
s_high = decay_score(age, 0, rein + 1)
|
|
43
|
+
assert s_high >= s_low - 1e-12
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_fresh_memory_scores_high():
|
|
47
|
+
assert decay_score(0) == 1.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_old_unused_memory_decays():
|
|
51
|
+
# 一年没碰、没复用 -> 应该很低
|
|
52
|
+
assert decay_score(365 * _DAY) < 0.05
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_reuse_bonus_capped():
|
|
56
|
+
assert reuse_bonus(10_000, 10_000) <= 0.5
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Governor 中间件测试:衰减治理、召回门控、健康度。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from memgov import Candidate, Governor, MemoryState
|
|
7
|
+
from memgov.store import MetaStore
|
|
8
|
+
|
|
9
|
+
_DAY = 86400.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def gov(tmp_path):
|
|
14
|
+
return Governor(store=MetaStore(tmp_path / "t.db"))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_track_and_get(gov):
|
|
18
|
+
gov.track("m1", agent_id="bot", now=1000.0)
|
|
19
|
+
m = gov.store.get("m1", "bot")
|
|
20
|
+
assert m is not None and m.state == MemoryState.ACTIVE
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_unused_memory_goes_stale_after_recompute(gov):
|
|
24
|
+
gov.track("old", agent_id="bot", now=0.0)
|
|
25
|
+
# 一年后重算,没访问没强化 -> stale
|
|
26
|
+
gov.recompute(agent_id="bot", now=365 * _DAY)
|
|
27
|
+
assert gov.store.get("old", "bot").state == MemoryState.STALE
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_reinforced_memory_stays_active(gov):
|
|
31
|
+
gov.track("keep", agent_id="bot", now=0.0)
|
|
32
|
+
for _ in range(5):
|
|
33
|
+
gov.reinforce("keep", agent_id="bot", now=300 * _DAY)
|
|
34
|
+
gov.recompute(agent_id="bot", now=300 * _DAY)
|
|
35
|
+
assert gov.store.get("keep", "bot").state != MemoryState.STALE
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_pinned_never_decays(gov):
|
|
39
|
+
gov.track("pin", agent_id="bot", now=0.0)
|
|
40
|
+
gov.pin("pin", agent_id="bot")
|
|
41
|
+
gov.recompute(agent_id="bot", now=10_000 * _DAY)
|
|
42
|
+
assert gov.store.get("pin", "bot").state == MemoryState.PINNED
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# 核心价值:召回门控
|
|
46
|
+
def test_gate_blocks_low_confidence(gov):
|
|
47
|
+
gov.track("m1", agent_id="bot", now=1000.0)
|
|
48
|
+
kept = gov.gate([Candidate("m1", confidence=0.3)], agent_id="bot",
|
|
49
|
+
min_confidence=0.6)
|
|
50
|
+
assert kept == [] # 置信度不够,不注入
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_gate_blocks_stale(gov):
|
|
54
|
+
gov.track("s1", agent_id="bot", now=0.0)
|
|
55
|
+
gov.recompute(agent_id="bot", now=365 * _DAY) # 变 stale
|
|
56
|
+
kept = gov.gate([Candidate("s1", confidence=0.99)], agent_id="bot")
|
|
57
|
+
assert kept == [] # 置信度高但已陈旧,仍不注入
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_gate_passes_good_candidate(gov):
|
|
61
|
+
gov.track("good", agent_id="bot", now=1000.0)
|
|
62
|
+
gov.recompute(agent_id="bot", now=1000.0)
|
|
63
|
+
kept = gov.gate([Candidate("good", confidence=0.9)], agent_id="bot")
|
|
64
|
+
assert [c.mem_id for c in kept] == ["good"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_health_counts(gov):
|
|
68
|
+
gov.track("a", agent_id="bot", now=0.0)
|
|
69
|
+
gov.track("b", agent_id="bot", now=0.0)
|
|
70
|
+
gov.reinforce("a", agent_id="bot", now=300 * _DAY)
|
|
71
|
+
gov.recompute(agent_id="bot", now=300 * _DAY)
|
|
72
|
+
rep = gov.health(agent_id="bot")
|
|
73
|
+
assert rep.total == 2
|
|
74
|
+
assert rep.active + rep.fading + rep.stale + rep.pinned == 2
|