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.
Files changed (44) hide show
  1. betterdb_agent_memory-0.1.0/.gitignore +8 -0
  2. betterdb_agent_memory-0.1.0/PKG-INFO +135 -0
  3. betterdb_agent_memory-0.1.0/README.md +117 -0
  4. betterdb_agent_memory-0.1.0/RELEASE_NOTES.md +55 -0
  5. betterdb_agent_memory-0.1.0/betterdb_agent_memory/__init__.py +88 -0
  6. betterdb_agent_memory-0.1.0/betterdb_agent_memory/_num.py +51 -0
  7. betterdb_agent_memory-0.1.0/betterdb_agent_memory/agent_memory.py +79 -0
  8. betterdb_agent_memory-0.1.0/betterdb_agent_memory/build_memory_index.py +60 -0
  9. betterdb_agent_memory-0.1.0/betterdb_agent_memory/build_memory_record.py +68 -0
  10. betterdb_agent_memory-0.1.0/betterdb_agent_memory/build_recall_query.py +65 -0
  11. betterdb_agent_memory-0.1.0/betterdb_agent_memory/composite_score.py +35 -0
  12. betterdb_agent_memory-0.1.0/betterdb_agent_memory/discovery.py +164 -0
  13. betterdb_agent_memory-0.1.0/betterdb_agent_memory/memory_store.py +964 -0
  14. betterdb_agent_memory-0.1.0/betterdb_agent_memory/parse_memory_item.py +34 -0
  15. betterdb_agent_memory-0.1.0/betterdb_agent_memory/select_evictions.py +54 -0
  16. betterdb_agent_memory-0.1.0/betterdb_agent_memory/telemetry.py +164 -0
  17. betterdb_agent_memory-0.1.0/betterdb_agent_memory/types.py +132 -0
  18. betterdb_agent_memory-0.1.0/pyproject.toml +42 -0
  19. betterdb_agent_memory-0.1.0/tests/__init__.py +0 -0
  20. betterdb_agent_memory-0.1.0/tests/conftest.py +144 -0
  21. betterdb_agent_memory-0.1.0/tests/test_agent_memory.py +258 -0
  22. betterdb_agent_memory-0.1.0/tests/test_build_memory_index.py +0 -0
  23. betterdb_agent_memory-0.1.0/tests/test_build_memory_record.py +72 -0
  24. betterdb_agent_memory-0.1.0/tests/test_build_recall_query.py +0 -0
  25. betterdb_agent_memory-0.1.0/tests/test_composite_score.py +59 -0
  26. betterdb_agent_memory-0.1.0/tests/test_discovery.py +186 -0
  27. betterdb_agent_memory-0.1.0/tests/test_index.py +17 -0
  28. betterdb_agent_memory-0.1.0/tests/test_memory_store_config.py +0 -0
  29. betterdb_agent_memory-0.1.0/tests/test_memory_store_consolidate.py +194 -0
  30. betterdb_agent_memory-0.1.0/tests/test_memory_store_discovery.py +40 -0
  31. betterdb_agent_memory-0.1.0/tests/test_memory_store_ensure_index.py +95 -0
  32. betterdb_agent_memory-0.1.0/tests/test_memory_store_eviction.py +0 -0
  33. betterdb_agent_memory-0.1.0/tests/test_memory_store_forget.py +121 -0
  34. betterdb_agent_memory-0.1.0/tests/test_memory_store_get.py +50 -0
  35. betterdb_agent_memory-0.1.0/tests/test_memory_store_list.py +63 -0
  36. betterdb_agent_memory-0.1.0/tests/test_memory_store_optional_embed_fn.py +22 -0
  37. betterdb_agent_memory-0.1.0/tests/test_memory_store_recall.py +147 -0
  38. betterdb_agent_memory-0.1.0/tests/test_memory_store_recall_by_vector.py +82 -0
  39. betterdb_agent_memory-0.1.0/tests/test_memory_store_reinforce.py +90 -0
  40. betterdb_agent_memory-0.1.0/tests/test_memory_store_remember.py +88 -0
  41. betterdb_agent_memory-0.1.0/tests/test_memory_store_spans.py +86 -0
  42. betterdb_agent_memory-0.1.0/tests/test_memory_store_stats.py +36 -0
  43. betterdb_agent_memory-0.1.0/tests/test_memory_store_telemetry.py +172 -0
  44. betterdb_agent_memory-0.1.0/tests/test_select_evictions.py +66 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ dist/
6
+ *.egg-info/
7
+ .ruff_cache/
8
+ .pytest_cache/
@@ -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)