betterdb-agent-memory 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.
- betterdb_agent_memory-0.1.0/.gitignore +8 -0
- betterdb_agent_memory-0.1.0/PKG-INFO +135 -0
- betterdb_agent_memory-0.1.0/README.md +117 -0
- betterdb_agent_memory-0.1.0/RELEASE_NOTES.md +55 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/__init__.py +88 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/_num.py +51 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/agent_memory.py +79 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/build_memory_index.py +60 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/build_memory_record.py +68 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/build_recall_query.py +65 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/composite_score.py +35 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/discovery.py +164 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/memory_store.py +964 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/parse_memory_item.py +34 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/select_evictions.py +54 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/telemetry.py +164 -0
- betterdb_agent_memory-0.1.0/betterdb_agent_memory/types.py +132 -0
- betterdb_agent_memory-0.1.0/pyproject.toml +42 -0
- betterdb_agent_memory-0.1.0/tests/__init__.py +0 -0
- betterdb_agent_memory-0.1.0/tests/conftest.py +144 -0
- betterdb_agent_memory-0.1.0/tests/test_agent_memory.py +258 -0
- betterdb_agent_memory-0.1.0/tests/test_build_memory_index.py +0 -0
- betterdb_agent_memory-0.1.0/tests/test_build_memory_record.py +72 -0
- betterdb_agent_memory-0.1.0/tests/test_build_recall_query.py +0 -0
- betterdb_agent_memory-0.1.0/tests/test_composite_score.py +59 -0
- betterdb_agent_memory-0.1.0/tests/test_discovery.py +186 -0
- betterdb_agent_memory-0.1.0/tests/test_index.py +17 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_config.py +0 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_consolidate.py +194 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_discovery.py +40 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_ensure_index.py +95 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_eviction.py +0 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_forget.py +121 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_get.py +50 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_list.py +63 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_optional_embed_fn.py +22 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_recall.py +147 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_recall_by_vector.py +82 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_reinforce.py +90 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_remember.py +88 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_spans.py +86 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_stats.py +36 -0
- betterdb_agent_memory-0.1.0/tests/test_memory_store_telemetry.py +172 -0
- betterdb_agent_memory-0.1.0/tests/test_select_evictions.py +66 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: betterdb-agent-memory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Long-term memory tier for AI agents backed by Valkey Search: semantic recall with recency/importance ranking, scoped capacity eviction, and consolidation. Pairs with @betterdb/agent-cache.
|
|
5
|
+
Project-URL: Repository, https://github.com/BetterDB-inc/monitor
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: agent,llm,memory,opentelemetry,prometheus,redis,valkey,vector-search
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: betterdb-agent-cache>=0.7.0
|
|
10
|
+
Requires-Dist: betterdb-valkey-search-kit>=0.1.0
|
|
11
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
12
|
+
Requires-Dist: prometheus-client>=0.19.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# @betterdb/agent-memory (Python)
|
|
20
|
+
|
|
21
|
+
`betterdb-agent-memory` is the long-term memory tier for AI agents, backed by
|
|
22
|
+
[Valkey Search](https://valkey.io/topics/search/). It is the Python port of
|
|
23
|
+
[`@betterdb/agent-memory`](https://www.npmjs.com/package/@betterdb/agent-memory)
|
|
24
|
+
and pairs with [`betterdb-agent-cache`](https://pypi.org/project/betterdb-agent-cache/)
|
|
25
|
+
(the short-term llm/tool/session cache tiers).
|
|
26
|
+
|
|
27
|
+
Where the cache tiers are exact-match and ephemeral, the memory tier is
|
|
28
|
+
semantic and durable: it embeds content, stores it in an HNSW vector index, and
|
|
29
|
+
recalls it by meaning with a composite score that blends **similarity**,
|
|
30
|
+
**recency** (half-life decay), and **importance**.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Semantic recall** — KNN vector search with a tunable composite score.
|
|
35
|
+
- **Scoping** — memories carry `thread_id` / `agent_id` / `namespace` / `tags`;
|
|
36
|
+
recall, forget, and consolidation all filter by scope.
|
|
37
|
+
- **Reinforcement** — recalled memories bump `last_accessed_at` + `access_count`,
|
|
38
|
+
so frequently-used memories stay recallable.
|
|
39
|
+
- **Capacity eviction** — `max_items_per_scope` evicts the lowest-scoring
|
|
40
|
+
memories (importance + recency) once a scope exceeds its cap.
|
|
41
|
+
- **Consolidation** — fold a set of older/low-importance memories into a single
|
|
42
|
+
summary memory.
|
|
43
|
+
- **Live config** — re-read `recall.threshold` / weights / `halfLifeSeconds` /
|
|
44
|
+
`maxItemsPerScope` from a Valkey hash without a restart.
|
|
45
|
+
- **Observability** — OpenTelemetry spans + Prometheus metrics.
|
|
46
|
+
- **Discovery** — registers a marker so BetterDB Monitor can enumerate the tier.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install betterdb-agent-memory
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You also need a Valkey server with the Search module loaded (e.g.
|
|
55
|
+
`valkey/valkey-bundle`) and the [`valkey`](https://pypi.org/project/valkey/)
|
|
56
|
+
async client.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import valkey.asyncio as valkey
|
|
62
|
+
from betterdb_agent_memory import AgentMemory, AgentMemoryOptions
|
|
63
|
+
|
|
64
|
+
async def embed(text: str) -> list[float]:
|
|
65
|
+
# Replace with a real embedding model (OpenAI, sentence-transformers, ...).
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
async def main() -> None:
|
|
69
|
+
client = valkey.Valkey(host="localhost", port=6379)
|
|
70
|
+
agent = AgentMemory(AgentMemoryOptions(client=client, embed_fn=embed))
|
|
71
|
+
await agent.initialize()
|
|
72
|
+
|
|
73
|
+
await agent.memory.remember(
|
|
74
|
+
"User prefers dark mode and concise answers.",
|
|
75
|
+
importance=0.8,
|
|
76
|
+
tags=["preference", "ui"],
|
|
77
|
+
thread_id="t1",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
hits = await agent.memory.recall("what UI settings does the user like?", thread_id="t1")
|
|
81
|
+
for hit in hits:
|
|
82
|
+
print(hit.score, hit.item.content)
|
|
83
|
+
|
|
84
|
+
# Short-term cache tiers remain available:
|
|
85
|
+
# agent.llm, agent.tool, agent.session
|
|
86
|
+
|
|
87
|
+
await agent.close()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Using the memory tier standalone
|
|
91
|
+
|
|
92
|
+
If you only need the memory tier, construct `MemoryStore` directly:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from betterdb_agent_memory import MemoryStore
|
|
96
|
+
|
|
97
|
+
store = MemoryStore(client=client, name="myapp", embed_fn=embed)
|
|
98
|
+
await store.ensure_index()
|
|
99
|
+
await store.remember("hello", thread_id="t1")
|
|
100
|
+
hits = await store.recall("hi", thread_id="t1")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## API
|
|
104
|
+
|
|
105
|
+
### `MemoryStore`
|
|
106
|
+
|
|
107
|
+
- `await ensure_index()` — create the `{name}:mem:idx` HNSW index if absent.
|
|
108
|
+
- `await remember(content, *, importance=None, tags=None, source=None, ttl=None, thread_id=None, agent_id=None, namespace=None) -> str`
|
|
109
|
+
- `await recall(query, *, k=None, threshold=None, tags=None, weights=None, reinforce=None, thread_id=None, agent_id=None, namespace=None) -> list[MemoryHit]`
|
|
110
|
+
- `await forget(id) -> bool`
|
|
111
|
+
- `await forget_by_scope(*, thread_id=None, agent_id=None, namespace=None, tags=None) -> int`
|
|
112
|
+
- `await consolidate(*, summarize, older_than_seconds=None, max_importance=None, delete_sources=None, summary_importance=None, tags=None, thread_id=None, agent_id=None, namespace=None) -> ConsolidateResult`
|
|
113
|
+
- `current_config() -> MemoryConfigSnapshot`
|
|
114
|
+
- `await refresh_config()`
|
|
115
|
+
- `await ensure_discovery_ready()`
|
|
116
|
+
- `await close()`
|
|
117
|
+
|
|
118
|
+
### `AgentMemory`
|
|
119
|
+
|
|
120
|
+
The batteries-included facade: an `AgentCache` (llm/tool/session) plus a
|
|
121
|
+
`MemoryStore` sharing one client and name. `initialize()` creates the index and
|
|
122
|
+
readies discovery for both tiers; `close()` tears both down.
|
|
123
|
+
|
|
124
|
+
## Scoring
|
|
125
|
+
|
|
126
|
+
`composite_score = w.similarity * similarity + w.recency * recency + w.importance * importance`
|
|
127
|
+
|
|
128
|
+
where `similarity = 1 - distance / 2` (cosine distance → 0..1) and `recency`
|
|
129
|
+
decays with a true half-life (`0.5` at one `half_life_seconds`). Default weights
|
|
130
|
+
are `{similarity: 0.6, recency: 0.25, importance: 0.15}`, default threshold
|
|
131
|
+
`0.25`, default half-life 7 days.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @betterdb/agent-memory (Python)
|
|
2
|
+
|
|
3
|
+
`betterdb-agent-memory` is the long-term memory tier for AI agents, backed by
|
|
4
|
+
[Valkey Search](https://valkey.io/topics/search/). It is the Python port of
|
|
5
|
+
[`@betterdb/agent-memory`](https://www.npmjs.com/package/@betterdb/agent-memory)
|
|
6
|
+
and pairs with [`betterdb-agent-cache`](https://pypi.org/project/betterdb-agent-cache/)
|
|
7
|
+
(the short-term llm/tool/session cache tiers).
|
|
8
|
+
|
|
9
|
+
Where the cache tiers are exact-match and ephemeral, the memory tier is
|
|
10
|
+
semantic and durable: it embeds content, stores it in an HNSW vector index, and
|
|
11
|
+
recalls it by meaning with a composite score that blends **similarity**,
|
|
12
|
+
**recency** (half-life decay), and **importance**.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **Semantic recall** — KNN vector search with a tunable composite score.
|
|
17
|
+
- **Scoping** — memories carry `thread_id` / `agent_id` / `namespace` / `tags`;
|
|
18
|
+
recall, forget, and consolidation all filter by scope.
|
|
19
|
+
- **Reinforcement** — recalled memories bump `last_accessed_at` + `access_count`,
|
|
20
|
+
so frequently-used memories stay recallable.
|
|
21
|
+
- **Capacity eviction** — `max_items_per_scope` evicts the lowest-scoring
|
|
22
|
+
memories (importance + recency) once a scope exceeds its cap.
|
|
23
|
+
- **Consolidation** — fold a set of older/low-importance memories into a single
|
|
24
|
+
summary memory.
|
|
25
|
+
- **Live config** — re-read `recall.threshold` / weights / `halfLifeSeconds` /
|
|
26
|
+
`maxItemsPerScope` from a Valkey hash without a restart.
|
|
27
|
+
- **Observability** — OpenTelemetry spans + Prometheus metrics.
|
|
28
|
+
- **Discovery** — registers a marker so BetterDB Monitor can enumerate the tier.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install betterdb-agent-memory
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
You also need a Valkey server with the Search module loaded (e.g.
|
|
37
|
+
`valkey/valkey-bundle`) and the [`valkey`](https://pypi.org/project/valkey/)
|
|
38
|
+
async client.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import valkey.asyncio as valkey
|
|
44
|
+
from betterdb_agent_memory import AgentMemory, AgentMemoryOptions
|
|
45
|
+
|
|
46
|
+
async def embed(text: str) -> list[float]:
|
|
47
|
+
# Replace with a real embedding model (OpenAI, sentence-transformers, ...).
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
async def main() -> None:
|
|
51
|
+
client = valkey.Valkey(host="localhost", port=6379)
|
|
52
|
+
agent = AgentMemory(AgentMemoryOptions(client=client, embed_fn=embed))
|
|
53
|
+
await agent.initialize()
|
|
54
|
+
|
|
55
|
+
await agent.memory.remember(
|
|
56
|
+
"User prefers dark mode and concise answers.",
|
|
57
|
+
importance=0.8,
|
|
58
|
+
tags=["preference", "ui"],
|
|
59
|
+
thread_id="t1",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
hits = await agent.memory.recall("what UI settings does the user like?", thread_id="t1")
|
|
63
|
+
for hit in hits:
|
|
64
|
+
print(hit.score, hit.item.content)
|
|
65
|
+
|
|
66
|
+
# Short-term cache tiers remain available:
|
|
67
|
+
# agent.llm, agent.tool, agent.session
|
|
68
|
+
|
|
69
|
+
await agent.close()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Using the memory tier standalone
|
|
73
|
+
|
|
74
|
+
If you only need the memory tier, construct `MemoryStore` directly:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from betterdb_agent_memory import MemoryStore
|
|
78
|
+
|
|
79
|
+
store = MemoryStore(client=client, name="myapp", embed_fn=embed)
|
|
80
|
+
await store.ensure_index()
|
|
81
|
+
await store.remember("hello", thread_id="t1")
|
|
82
|
+
hits = await store.recall("hi", thread_id="t1")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
### `MemoryStore`
|
|
88
|
+
|
|
89
|
+
- `await ensure_index()` — create the `{name}:mem:idx` HNSW index if absent.
|
|
90
|
+
- `await remember(content, *, importance=None, tags=None, source=None, ttl=None, thread_id=None, agent_id=None, namespace=None) -> str`
|
|
91
|
+
- `await recall(query, *, k=None, threshold=None, tags=None, weights=None, reinforce=None, thread_id=None, agent_id=None, namespace=None) -> list[MemoryHit]`
|
|
92
|
+
- `await forget(id) -> bool`
|
|
93
|
+
- `await forget_by_scope(*, thread_id=None, agent_id=None, namespace=None, tags=None) -> int`
|
|
94
|
+
- `await consolidate(*, summarize, older_than_seconds=None, max_importance=None, delete_sources=None, summary_importance=None, tags=None, thread_id=None, agent_id=None, namespace=None) -> ConsolidateResult`
|
|
95
|
+
- `current_config() -> MemoryConfigSnapshot`
|
|
96
|
+
- `await refresh_config()`
|
|
97
|
+
- `await ensure_discovery_ready()`
|
|
98
|
+
- `await close()`
|
|
99
|
+
|
|
100
|
+
### `AgentMemory`
|
|
101
|
+
|
|
102
|
+
The batteries-included facade: an `AgentCache` (llm/tool/session) plus a
|
|
103
|
+
`MemoryStore` sharing one client and name. `initialize()` creates the index and
|
|
104
|
+
readies discovery for both tiers; `close()` tears both down.
|
|
105
|
+
|
|
106
|
+
## Scoring
|
|
107
|
+
|
|
108
|
+
`composite_score = w.similarity * similarity + w.recency * recency + w.importance * importance`
|
|
109
|
+
|
|
110
|
+
where `similarity = 1 - distance / 2` (cosine distance → 0..1) and `recency`
|
|
111
|
+
decays with a true half-life (`0.5` at one `half_life_seconds`). Default weights
|
|
112
|
+
are `{similarity: 0.6, recency: 0.25, importance: 0.15}`, default threshold
|
|
113
|
+
`0.25`, default half-life 7 days.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# betterdb-agent-memory v0.1.0
|
|
2
|
+
|
|
3
|
+
Python port of `@betterdb/agent-memory`. Long-term memory tier for AI agents
|
|
4
|
+
backed by Valkey Search — semantic recall with recency/importance ranking,
|
|
5
|
+
scoped capacity eviction, and consolidation. Pairs with `betterdb-agent-cache`.
|
|
6
|
+
|
|
7
|
+
Requires Valkey 8+ with the **valkey-search** module (vector index support).
|
|
8
|
+
Works with ElastiCache for Valkey, Memorystore for Valkey, and MemoryDB.
|
|
9
|
+
|
|
10
|
+
Built on [`betterdb-valkey-search-kit`](https://pypi.org/project/betterdb-valkey-search-kit/)
|
|
11
|
+
and [`betterdb-agent-cache`](https://pypi.org/project/betterdb-agent-cache/).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pip install betterdb-agent-memory
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## What's included
|
|
24
|
+
|
|
25
|
+
### MemoryStore (long-term tier)
|
|
26
|
+
|
|
27
|
+
| Method | Description |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `ensure_index()` | Create or attach to the memory vector index |
|
|
30
|
+
| `remember(...)` | Persist a memory with embedding, scope, tags, and importance |
|
|
31
|
+
| `recall(...)` | Semantic recall ranked by similarity, recency, and importance |
|
|
32
|
+
| `recall_by_vector(...)` | KNN recall from a precomputed vector |
|
|
33
|
+
| `reinforce(id)` | Bump importance / recency on an existing memory |
|
|
34
|
+
| `forget(...)` | Delete memories by id or filter |
|
|
35
|
+
| `consolidate(...)` | Merge and summarize related memories |
|
|
36
|
+
| `get(id)` / `list(...)` | Read-only fetch and scoped, paginated listing |
|
|
37
|
+
| `stats()` | Doc count, evictions, and live config |
|
|
38
|
+
|
|
39
|
+
Scoped capacity eviction, live config refresh, and discovery are built in.
|
|
40
|
+
|
|
41
|
+
### AgentMemory facade
|
|
42
|
+
|
|
43
|
+
Convenience facade over `betterdb-agent-cache` combining the exact-match cache
|
|
44
|
+
tier with the long-term memory tier.
|
|
45
|
+
|
|
46
|
+
### Observability
|
|
47
|
+
|
|
48
|
+
- OpenTelemetry spans on every memory operation
|
|
49
|
+
- Prometheus metrics for recall latency and eviction counts
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Full changelog
|
|
54
|
+
|
|
55
|
+
See the repository history for detailed changes.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""betterdb-agent-memory: long-term vector memory tier for AI agents on Valkey.
|
|
2
|
+
|
|
3
|
+
Re-exports everything from ``betterdb-agent-cache`` (the short-term cache tiers)
|
|
4
|
+
alongside the memory tier so the facade is a single import.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import betterdb_agent_cache as _agent_cache
|
|
10
|
+
from betterdb_agent_cache import * # noqa: F401,F403
|
|
11
|
+
|
|
12
|
+
from .agent_memory import AgentMemory, AgentMemoryOptions
|
|
13
|
+
from .build_recall_query import MATCH_ALL_MEMORY_QUERY
|
|
14
|
+
from .composite_score import (
|
|
15
|
+
composite_score,
|
|
16
|
+
recency_decay,
|
|
17
|
+
similarity_from_distance,
|
|
18
|
+
)
|
|
19
|
+
from .discovery import MEMORY_CACHE_TYPE, MEMORY_CAPABILITIES, MemoryDiscovery
|
|
20
|
+
from .memory_store import MemoryStore
|
|
21
|
+
from .telemetry import (
|
|
22
|
+
DEFAULT_METRICS_PREFIX,
|
|
23
|
+
DEFAULT_TRACER_NAME,
|
|
24
|
+
MemoryMetrics,
|
|
25
|
+
MemoryTelemetry,
|
|
26
|
+
MemoryTelemetryOptions,
|
|
27
|
+
create_memory_telemetry,
|
|
28
|
+
)
|
|
29
|
+
from .types import (
|
|
30
|
+
AgentMemoryConfig,
|
|
31
|
+
AgentMemoryRecallConfig,
|
|
32
|
+
ConsolidateResult,
|
|
33
|
+
EmbedFn,
|
|
34
|
+
MemoryConfigRefreshConfig,
|
|
35
|
+
MemoryConfigSnapshot,
|
|
36
|
+
MemoryDiscoveryConfig,
|
|
37
|
+
MemoryHit,
|
|
38
|
+
MemoryItem,
|
|
39
|
+
MemoryListOptions,
|
|
40
|
+
MemoryListResult,
|
|
41
|
+
MemoryScope,
|
|
42
|
+
MemoryStats,
|
|
43
|
+
MemoryStoreClient,
|
|
44
|
+
RecallWeights,
|
|
45
|
+
SummarizeFn,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
# Memory tier
|
|
50
|
+
"AgentMemory",
|
|
51
|
+
"AgentMemoryOptions",
|
|
52
|
+
"AgentMemoryConfig",
|
|
53
|
+
"AgentMemoryRecallConfig",
|
|
54
|
+
"MemoryStore",
|
|
55
|
+
"MemoryDiscovery",
|
|
56
|
+
"MEMORY_CACHE_TYPE",
|
|
57
|
+
"MEMORY_CAPABILITIES",
|
|
58
|
+
# Telemetry
|
|
59
|
+
"create_memory_telemetry",
|
|
60
|
+
"DEFAULT_METRICS_PREFIX",
|
|
61
|
+
"DEFAULT_TRACER_NAME",
|
|
62
|
+
"MemoryTelemetry",
|
|
63
|
+
"MemoryTelemetryOptions",
|
|
64
|
+
"MemoryMetrics",
|
|
65
|
+
# Scoring
|
|
66
|
+
"composite_score",
|
|
67
|
+
"similarity_from_distance",
|
|
68
|
+
"recency_decay",
|
|
69
|
+
# Types
|
|
70
|
+
"EmbedFn",
|
|
71
|
+
"MemoryStoreClient",
|
|
72
|
+
"MemoryScope",
|
|
73
|
+
"MemoryItem",
|
|
74
|
+
"MemoryHit",
|
|
75
|
+
"MemoryListOptions",
|
|
76
|
+
"MemoryListResult",
|
|
77
|
+
"MemoryStats",
|
|
78
|
+
"ConsolidateResult",
|
|
79
|
+
"SummarizeFn",
|
|
80
|
+
"RecallWeights",
|
|
81
|
+
"MemoryConfigSnapshot",
|
|
82
|
+
"MemoryDiscoveryConfig",
|
|
83
|
+
"MemoryConfigRefreshConfig",
|
|
84
|
+
"MATCH_ALL_MEMORY_QUERY",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
# Surface everything agent-cache exports so consumers need only one import.
|
|
88
|
+
__all__ += list(getattr(_agent_cache, "__all__", []))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
_FLOAT_RE = re.compile(r"^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?")
|
|
7
|
+
_INT_RE = re.compile(r"^[+-]?\d+")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _coerce_str(value: object) -> str:
|
|
11
|
+
"""Coerce a valkey reply value to text the way JS ``String()`` would.
|
|
12
|
+
|
|
13
|
+
valkey-py can hand back ``bytes`` for raw replies, so decode those instead of
|
|
14
|
+
stringifying them (``str(b'1')`` is ``"b'1'"``, which would parse as NaN).
|
|
15
|
+
"""
|
|
16
|
+
if isinstance(value, bytes):
|
|
17
|
+
return value.decode()
|
|
18
|
+
return str(value)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def js_number(value: object) -> float:
|
|
22
|
+
"""Mimic JavaScript ``Number(value)`` for the string inputs we see.
|
|
23
|
+
|
|
24
|
+
Empty/whitespace-only strings become ``0`` (as in JS); unparseable strings
|
|
25
|
+
become ``NaN``. ``None`` becomes ``NaN``.
|
|
26
|
+
"""
|
|
27
|
+
if value is None:
|
|
28
|
+
return math.nan
|
|
29
|
+
text = _coerce_str(value).strip()
|
|
30
|
+
if text == "":
|
|
31
|
+
return 0.0
|
|
32
|
+
try:
|
|
33
|
+
return float(text)
|
|
34
|
+
except ValueError:
|
|
35
|
+
return math.nan
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_float(value: object) -> float:
|
|
39
|
+
"""Mimic JavaScript ``parseFloat``: parse the leading numeric portion, else NaN."""
|
|
40
|
+
if value is None:
|
|
41
|
+
return math.nan
|
|
42
|
+
match = _FLOAT_RE.match(_coerce_str(value).strip())
|
|
43
|
+
return float(match.group()) if match else math.nan
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_int(value: object) -> float:
|
|
47
|
+
"""Mimic JavaScript ``parseInt(value, 10)``: parse the leading integer, else NaN."""
|
|
48
|
+
if value is None:
|
|
49
|
+
return math.nan
|
|
50
|
+
match = _INT_RE.match(_coerce_str(value).strip())
|
|
51
|
+
return int(match.group()) if match else math.nan
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from betterdb_agent_cache import AgentCache, AgentCacheOptions
|
|
7
|
+
|
|
8
|
+
from .memory_store import MemoryStore
|
|
9
|
+
from .telemetry import MemoryTelemetryOptions
|
|
10
|
+
from .types import AgentMemoryConfig, EmbedFn, _empty_memory_config
|
|
11
|
+
|
|
12
|
+
DEFAULT_NAME = "betterdb_ac"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(kw_only=True)
|
|
16
|
+
class AgentMemoryOptions(AgentCacheOptions):
|
|
17
|
+
"""Options for the batteries-included :class:`AgentMemory` facade.
|
|
18
|
+
|
|
19
|
+
Extends :class:`AgentCacheOptions` (the three short-term cache tiers) with
|
|
20
|
+
the long-term memory tier: an ``embed_fn`` to vectorize content plus an
|
|
21
|
+
optional ``memory`` sub-config.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
embed_fn: EmbedFn
|
|
25
|
+
memory: AgentMemoryConfig = field(default_factory=_empty_memory_config)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AgentMemory:
|
|
29
|
+
"""Agent cache (llm/tool/session) plus a long-term :class:`MemoryStore` tier."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, options: AgentMemoryOptions) -> None:
|
|
32
|
+
if not callable(getattr(options, "embed_fn", None)):
|
|
33
|
+
raise ValueError("AgentMemory requires an embed_fn to back the memory tier")
|
|
34
|
+
|
|
35
|
+
# The name lives on the shared options object and defaults identically in
|
|
36
|
+
# both tiers, so the cache and memory key prefixes can never drift apart.
|
|
37
|
+
name = options.name
|
|
38
|
+
self._cache = AgentCache(options)
|
|
39
|
+
self.llm = self._cache.llm
|
|
40
|
+
self.tool = self._cache.tool
|
|
41
|
+
self.session = self._cache.session
|
|
42
|
+
|
|
43
|
+
memory = options.memory
|
|
44
|
+
registry = options.telemetry.registry
|
|
45
|
+
self.memory = MemoryStore(
|
|
46
|
+
client=options.client,
|
|
47
|
+
name=name,
|
|
48
|
+
embed_fn=options.embed_fn,
|
|
49
|
+
default_threshold=memory.default_threshold,
|
|
50
|
+
weights=memory.recall.weights if memory.recall is not None else None,
|
|
51
|
+
half_life_seconds=(
|
|
52
|
+
memory.recall.half_life_seconds if memory.recall is not None else None
|
|
53
|
+
),
|
|
54
|
+
max_items_per_scope=memory.max_items_per_scope,
|
|
55
|
+
# The facade is the batteries-included product: discover the memory
|
|
56
|
+
# tier alongside the cache tiers by default, unless explicitly disabled.
|
|
57
|
+
discovery=memory.discovery,
|
|
58
|
+
config_refresh=memory.config_refresh,
|
|
59
|
+
telemetry=MemoryTelemetryOptions(registry=registry) if registry else None,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def initialize(self) -> None:
|
|
63
|
+
# Create the memory index before discovery so a freshly constructed facade
|
|
64
|
+
# is immediately usable for remember/recall without the caller hand-rolling
|
|
65
|
+
# the FT index. A create failure surfaces — the tier is unusable without it.
|
|
66
|
+
await self.memory.ensure_index()
|
|
67
|
+
# Surface a discovery name-collision from either tier instead of swallowing it.
|
|
68
|
+
await asyncio.gather(
|
|
69
|
+
self._cache.ensure_discovery_ready(),
|
|
70
|
+
self.memory.ensure_discovery_ready(),
|
|
71
|
+
self.memory.ensure_config_refresh_started(),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
async def close(self) -> None:
|
|
75
|
+
# Tear down both tiers even if one fails, so timers and heartbeats can't leak.
|
|
76
|
+
try:
|
|
77
|
+
await self.memory.close()
|
|
78
|
+
finally:
|
|
79
|
+
await self._cache.shutdown()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .build_recall_query import VECTOR_FIELD
|
|
4
|
+
|
|
5
|
+
MEMORY_INDEX_ALGORITHM = "HNSW"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def memory_index_name(name: str) -> str:
|
|
9
|
+
return f"{name}:mem:idx"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def memory_key_prefix(name: str) -> str:
|
|
13
|
+
return f"{name}:mem:"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_memory_index_args(name: str, dims: int) -> list[str]:
|
|
17
|
+
if not isinstance(dims, int) or isinstance(dims, bool) or dims <= 0:
|
|
18
|
+
raise ValueError(f"memory index dimension must be a positive integer, got: {dims}")
|
|
19
|
+
return [
|
|
20
|
+
memory_index_name(name),
|
|
21
|
+
"ON",
|
|
22
|
+
"HASH",
|
|
23
|
+
"PREFIX",
|
|
24
|
+
"1",
|
|
25
|
+
memory_key_prefix(name),
|
|
26
|
+
"SCHEMA",
|
|
27
|
+
VECTOR_FIELD,
|
|
28
|
+
"VECTOR",
|
|
29
|
+
MEMORY_INDEX_ALGORITHM,
|
|
30
|
+
"6",
|
|
31
|
+
"TYPE",
|
|
32
|
+
"FLOAT32",
|
|
33
|
+
"DIM",
|
|
34
|
+
str(dims),
|
|
35
|
+
"DISTANCE_METRIC",
|
|
36
|
+
"COSINE",
|
|
37
|
+
"threadId",
|
|
38
|
+
"TAG",
|
|
39
|
+
"agentId",
|
|
40
|
+
"TAG",
|
|
41
|
+
"namespace",
|
|
42
|
+
"TAG",
|
|
43
|
+
"tags",
|
|
44
|
+
"TAG",
|
|
45
|
+
"SEPARATOR",
|
|
46
|
+
",",
|
|
47
|
+
"source",
|
|
48
|
+
"TAG",
|
|
49
|
+
"importance",
|
|
50
|
+
"NUMERIC",
|
|
51
|
+
"created_at",
|
|
52
|
+
"NUMERIC",
|
|
53
|
+
"SORTABLE",
|
|
54
|
+
"last_accessed_at",
|
|
55
|
+
"NUMERIC",
|
|
56
|
+
"access_count",
|
|
57
|
+
"NUMERIC",
|
|
58
|
+
"content",
|
|
59
|
+
"TEXT",
|
|
60
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from betterdb_valkey_search_kit import encode_float32
|
|
7
|
+
|
|
8
|
+
DEFAULT_IMPORTANCE = 0.5
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class MemoryWrite:
|
|
13
|
+
key: str
|
|
14
|
+
fields: list[str | bytes]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_memory_record(
|
|
18
|
+
name: str,
|
|
19
|
+
id: str,
|
|
20
|
+
content: str,
|
|
21
|
+
vector: list[float],
|
|
22
|
+
*,
|
|
23
|
+
importance: float | None = None,
|
|
24
|
+
tags: list[str] | None = None,
|
|
25
|
+
source: str | None = None,
|
|
26
|
+
thread_id: str | None = None,
|
|
27
|
+
agent_id: str | None = None,
|
|
28
|
+
namespace: str | None = None,
|
|
29
|
+
now: int,
|
|
30
|
+
) -> MemoryWrite:
|
|
31
|
+
imp = importance if importance is not None else DEFAULT_IMPORTANCE
|
|
32
|
+
if not isinstance(imp, (int, float)) or not math.isfinite(imp) or imp < 0 or imp > 1:
|
|
33
|
+
raise ValueError(f"importance must be a finite number in [0, 1], got: {importance}")
|
|
34
|
+
|
|
35
|
+
fields: list[str | bytes] = [
|
|
36
|
+
"content",
|
|
37
|
+
content,
|
|
38
|
+
"vector",
|
|
39
|
+
encode_float32(vector),
|
|
40
|
+
"importance",
|
|
41
|
+
str(imp),
|
|
42
|
+
"created_at",
|
|
43
|
+
str(now),
|
|
44
|
+
"last_accessed_at",
|
|
45
|
+
str(now),
|
|
46
|
+
"access_count",
|
|
47
|
+
"0",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
tag_list = tags if tags is not None else []
|
|
51
|
+
for tag in tag_list:
|
|
52
|
+
if "," in tag:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Tag '{tag}' must not contain a comma; tags are stored comma-separated"
|
|
55
|
+
)
|
|
56
|
+
if len(tag_list) > 0:
|
|
57
|
+
fields.extend(["tags", ",".join(tag_list)])
|
|
58
|
+
|
|
59
|
+
if thread_id is not None:
|
|
60
|
+
fields.extend(["threadId", thread_id])
|
|
61
|
+
if agent_id is not None:
|
|
62
|
+
fields.extend(["agentId", agent_id])
|
|
63
|
+
if namespace is not None:
|
|
64
|
+
fields.extend(["namespace", namespace])
|
|
65
|
+
if source is not None:
|
|
66
|
+
fields.extend(["source", source])
|
|
67
|
+
|
|
68
|
+
return MemoryWrite(key=f"{name}:mem:{id}", fields=fields)
|