elfmem 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.
- elfmem-0.1.0/.github/workflows/ci.yml +39 -0
- elfmem-0.1.0/.github/workflows/publish.yml +60 -0
- elfmem-0.1.0/.gitignore +10 -0
- elfmem-0.1.0/.python-version +1 -0
- elfmem-0.1.0/PKG-INFO +410 -0
- elfmem-0.1.0/QUICKSTART.md +229 -0
- elfmem-0.1.0/README.md +374 -0
- elfmem-0.1.0/SIMULATION_OVERVIEW.md +407 -0
- elfmem-0.1.0/START_HERE.md +236 -0
- elfmem-0.1.0/alembic/env.py +63 -0
- elfmem-0.1.0/alembic/script.py.mako +27 -0
- elfmem-0.1.0/alembic.ini +41 -0
- elfmem-0.1.0/docs/amgs_architecture.md +749 -0
- elfmem-0.1.0/docs/amgs_instructions.md +227 -0
- elfmem-0.1.0/docs/coding_principles.md +364 -0
- elfmem-0.1.0/docs/notes.md +275 -0
- elfmem-0.1.0/docs/plans/step_03_schema_storage.md +767 -0
- elfmem-0.1.0/docs/plans/step_04_mock_adapters.md +346 -0
- elfmem-0.1.0/docs/plans/step_05_learn_consolidate.md +682 -0
- elfmem-0.1.0/docs/plans/step_06_recall_frame.md +785 -0
- elfmem-0.1.0/docs/plans/step_07_curate.md +385 -0
- elfmem-0.1.0/docs/plans/step_08_real_adapters.md +416 -0
- elfmem-0.1.0/docs/plans/step_09_config_factory.md +446 -0
- elfmem-0.1.0/docs/prompt_ab_testing.md +638 -0
- elfmem-0.1.0/docs/prompt_team_01.md +18 -0
- elfmem-0.1.0/docs/testing_principles.md +133 -0
- elfmem-0.1.0/pyproject.toml +68 -0
- elfmem-0.1.0/sim/EXPLORATIONS.md +1146 -0
- elfmem-0.1.0/sim/README.md +278 -0
- elfmem-0.1.0/sim/explorations/001_basic_decay.md +177 -0
- elfmem-0.1.0/sim/explorations/002_confidence_trap.md +213 -0
- elfmem-0.1.0/sim/explorations/003_scoring_walkthrough.md +244 -0
- elfmem-0.1.0/sim/explorations/004_self_interest_model.md +395 -0
- elfmem-0.1.0/sim/explorations/005_decay_sophistication.md +326 -0
- elfmem-0.1.0/sim/explorations/006_self_as_system_prompt.md +751 -0
- elfmem-0.1.0/sim/explorations/007_constitutional_self.md +878 -0
- elfmem-0.1.0/sim/explorations/008_lifecycle_operations.md +895 -0
- elfmem-0.1.0/sim/explorations/009_near_duplicates_and_curate_scheduling.md +568 -0
- elfmem-0.1.0/sim/explorations/010_block_anatomy.md +488 -0
- elfmem-0.1.0/sim/explorations/011_identifying_self_blocks.md +551 -0
- elfmem-0.1.0/sim/explorations/012_self_tag_assignment.md +588 -0
- elfmem-0.1.0/sim/explorations/013_edges.md +643 -0
- elfmem-0.1.0/sim/explorations/014_edge_types.md +369 -0
- elfmem-0.1.0/sim/explorations/015_context_frames_api.md +596 -0
- elfmem-0.1.0/sim/explorations/016_custom_frames.md +428 -0
- elfmem-0.1.0/sim/explorations/017_storage_layer.md +595 -0
- elfmem-0.1.0/sim/explorations/018_duckdb_vs_sqlite.md +347 -0
- elfmem-0.1.0/sim/explorations/019_database_tooling.md +795 -0
- elfmem-0.1.0/sim/explorations/020_graph_layer.md +532 -0
- elfmem-0.1.0/sim/explorations/021_hybrid_retrieval.md +449 -0
- elfmem-0.1.0/sim/explorations/022_layer_model.md +557 -0
- elfmem-0.1.0/sim/explorations/023_agent_usage.md +625 -0
- elfmem-0.1.0/sim/explorations/024_system_refinement.md +954 -0
- elfmem-0.1.0/sim/explorations/025_llm_gateway.md +854 -0
- elfmem-0.1.0/sim/explorations/026_prompt_overrides.md +626 -0
- elfmem-0.1.0/sim/explorations/027_implementation_priority.md +595 -0
- elfmem-0.1.0/sim/explorations/_template.md +47 -0
- elfmem-0.1.0/sim/playgrounds/README.md +110 -0
- elfmem-0.1.0/sim/playgrounds/decay/decay.md +482 -0
- elfmem-0.1.0/sim/playgrounds/frames/frames.md +518 -0
- elfmem-0.1.0/sim/playgrounds/graph/graph.md +428 -0
- elfmem-0.1.0/sim/playgrounds/lifecycle/lifecycle.md +509 -0
- elfmem-0.1.0/sim/playgrounds/retrieval/retrieval.md +391 -0
- elfmem-0.1.0/sim/playgrounds/scoring/scoring.md +528 -0
- elfmem-0.1.0/src/elfmem/__init__.py +5 -0
- elfmem-0.1.0/src/elfmem/adapters/__init__.py +0 -0
- elfmem-0.1.0/src/elfmem/adapters/litellm.py +171 -0
- elfmem-0.1.0/src/elfmem/adapters/mock.py +172 -0
- elfmem-0.1.0/src/elfmem/adapters/models.py +38 -0
- elfmem-0.1.0/src/elfmem/api.py +367 -0
- elfmem-0.1.0/src/elfmem/config.py +187 -0
- elfmem-0.1.0/src/elfmem/context/__init__.py +0 -0
- elfmem-0.1.0/src/elfmem/context/contradiction.py +65 -0
- elfmem-0.1.0/src/elfmem/context/frames.py +123 -0
- elfmem-0.1.0/src/elfmem/context/rendering.py +94 -0
- elfmem-0.1.0/src/elfmem/db/__init__.py +6 -0
- elfmem-0.1.0/src/elfmem/db/engine.py +72 -0
- elfmem-0.1.0/src/elfmem/db/models.py +113 -0
- elfmem-0.1.0/src/elfmem/db/queries.py +630 -0
- elfmem-0.1.0/src/elfmem/memory/__init__.py +0 -0
- elfmem-0.1.0/src/elfmem/memory/blocks.py +42 -0
- elfmem-0.1.0/src/elfmem/memory/dedup.py +66 -0
- elfmem-0.1.0/src/elfmem/memory/graph.py +85 -0
- elfmem-0.1.0/src/elfmem/memory/retrieval.py +212 -0
- elfmem-0.1.0/src/elfmem/operations/__init__.py +0 -0
- elfmem-0.1.0/src/elfmem/operations/consolidate.py +190 -0
- elfmem-0.1.0/src/elfmem/operations/curate.py +144 -0
- elfmem-0.1.0/src/elfmem/operations/learn.py +53 -0
- elfmem-0.1.0/src/elfmem/operations/recall.py +139 -0
- elfmem-0.1.0/src/elfmem/ports/__init__.py +0 -0
- elfmem-0.1.0/src/elfmem/ports/services.py +39 -0
- elfmem-0.1.0/src/elfmem/prompts.py +79 -0
- elfmem-0.1.0/src/elfmem/py.typed +0 -0
- elfmem-0.1.0/src/elfmem/scoring.py +145 -0
- elfmem-0.1.0/src/elfmem/session.py +87 -0
- elfmem-0.1.0/src/elfmem/types.py +93 -0
- elfmem-0.1.0/tests/__init__.py +0 -0
- elfmem-0.1.0/tests/conftest.py +44 -0
- elfmem-0.1.0/tests/test_curate.py +470 -0
- elfmem-0.1.0/tests/test_lifecycle.py +505 -0
- elfmem-0.1.0/tests/test_mock_adapters.py +417 -0
- elfmem-0.1.0/tests/test_retrieval.py +471 -0
- elfmem-0.1.0/tests/test_scoring.py +256 -0
- elfmem-0.1.0/tests/test_storage.py +621 -0
- elfmem-0.1.0/uv.lock +2299 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v5
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
27
|
+
run: uv python install ${{ matrix.python-version }}
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: uv sync --extra dev
|
|
31
|
+
|
|
32
|
+
- name: Lint (ruff)
|
|
33
|
+
run: uv run ruff check src/ tests/
|
|
34
|
+
|
|
35
|
+
- name: Type-check (mypy)
|
|
36
|
+
run: uv run mypy --ignore-missing-imports src/elfmem/
|
|
37
|
+
|
|
38
|
+
- name: Test (pytest)
|
|
39
|
+
run: uv run pytest -q
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build distribution
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v5
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
|
|
24
|
+
- name: Set up Python
|
|
25
|
+
run: uv python install 3.11
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies and run tests
|
|
28
|
+
run: |
|
|
29
|
+
uv sync --extra dev
|
|
30
|
+
uv run pytest -q
|
|
31
|
+
|
|
32
|
+
- name: Build package
|
|
33
|
+
run: uv build
|
|
34
|
+
|
|
35
|
+
- name: Upload distribution artifact
|
|
36
|
+
uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
publish:
|
|
42
|
+
name: Publish to PyPI
|
|
43
|
+
needs: build
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
environment: pypi
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write # required for OIDC trusted publishing
|
|
48
|
+
|
|
49
|
+
steps:
|
|
50
|
+
- name: Install uv
|
|
51
|
+
uses: astral-sh/setup-uv@v5
|
|
52
|
+
|
|
53
|
+
- name: Download distribution artifact
|
|
54
|
+
uses: actions/download-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: dist
|
|
57
|
+
path: dist/
|
|
58
|
+
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
run: uv publish --trusted-publishing always
|
elfmem-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
elfmem-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: elfmem
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-aware adaptive memory for LLM agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/emson/elfmem
|
|
6
|
+
Project-URL: Repository, https://github.com/emson/elfmem
|
|
7
|
+
Project-URL: Issues, https://github.com/emson/elfmem/issues
|
|
8
|
+
Author: emson
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,llm,memory,rag,sqlite
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: aiosqlite>=0.19
|
|
21
|
+
Requires-Dist: alembic>=1.13
|
|
22
|
+
Requires-Dist: greenlet>=3.3.2
|
|
23
|
+
Requires-Dist: instructor>=1.2
|
|
24
|
+
Requires-Dist: litellm>=1.30
|
|
25
|
+
Requires-Dist: numpy>=1.26
|
|
26
|
+
Requires-Dist: pydantic>=2.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
34
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# elfmem
|
|
38
|
+
|
|
39
|
+
**Adaptive, self-aware memory for LLM agents.**
|
|
40
|
+
|
|
41
|
+
elfmem gives your LLM agent a memory that grows, evolves, and forgets — just like a human's. Knowledge that gets used survives; knowledge that doesn't fades away. Identity persists across sessions. Context is always relevant.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import asyncio
|
|
45
|
+
from elfmem import MemorySystem
|
|
46
|
+
|
|
47
|
+
async def main():
|
|
48
|
+
system = await MemorySystem.from_config("agent.db", {
|
|
49
|
+
"llm": {"model": "claude-sonnet-4-6"},
|
|
50
|
+
"embeddings": {"model": "text-embedding-3-small", "dimensions": 1536},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
async with system.session():
|
|
54
|
+
# Teach the agent something
|
|
55
|
+
await system.learn("Use Celery with Redis for background tasks in Django.")
|
|
56
|
+
await system.learn("I always explain my reasoning before giving recommendations.")
|
|
57
|
+
|
|
58
|
+
# Retrieve relevant context for a prompt
|
|
59
|
+
identity = await system.frame("self") # Who am I?
|
|
60
|
+
context = await system.frame("attention", # What do I know about this?
|
|
61
|
+
query="background job processing")
|
|
62
|
+
|
|
63
|
+
print(identity.text) # Agent identity, values, style
|
|
64
|
+
print(context.text) # Relevant knowledge, ranked by importance
|
|
65
|
+
|
|
66
|
+
asyncio.run(main())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- **Adaptive decay** — Knowledge survives when reinforced through use, fades when ignored. Session-aware clock means your agent's memory doesn't decay over weekends.
|
|
72
|
+
- **SELF frame** — Persistent agent identity. Values, style, and constraints survive across sessions with near-permanent decay rates.
|
|
73
|
+
- **Hybrid retrieval** — 4-stage pipeline: pre-filter, vector search, graph expansion, composite scoring. Finds knowledge that's relevant *and* important.
|
|
74
|
+
- **Knowledge graph** — Semantic edges between memory blocks. Co-retrieved knowledge strengthens connections. Graph expansion recovers related-but-not-similar context.
|
|
75
|
+
- **Contradiction detection** — LLM-powered detection of conflicting knowledge. Newer, higher-confidence blocks win.
|
|
76
|
+
- **Near-duplicate resolution** — Detects when new knowledge updates existing knowledge. Old block archived, new block inherits history.
|
|
77
|
+
- **Zero infrastructure** — SQLite backend. No Redis, no Postgres, no vector database. One file, fully portable.
|
|
78
|
+
- **Any LLM provider** — LiteLLM backend supports 100+ providers. Switch from OpenAI to Anthropic to local Ollama with a config change.
|
|
79
|
+
|
|
80
|
+
## Installation
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv add elfmem
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or with pip:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install elfmem
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Requires Python 3.11+.
|
|
93
|
+
|
|
94
|
+
## How It Works
|
|
95
|
+
|
|
96
|
+
### The Lifecycle
|
|
97
|
+
|
|
98
|
+
Every piece of knowledge follows the same path:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
learn() → Instant ingestion. Content-hash dedup. No API calls.
|
|
102
|
+
consolidate() → Batch processing. Embeddings, self-alignment scoring,
|
|
103
|
+
tag inference, near-duplicate detection, graph edges.
|
|
104
|
+
recall() → 4-stage hybrid retrieval. Reinforces returned blocks.
|
|
105
|
+
curate() → Maintenance. Archives decayed blocks, prunes weak edges,
|
|
106
|
+
reinforces top-scoring knowledge.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Three Frames
|
|
110
|
+
|
|
111
|
+
Frames are pre-configured retrieval pipelines optimized for different contexts:
|
|
112
|
+
|
|
113
|
+
| Frame | Purpose | Scoring Priority | Use Case |
|
|
114
|
+
|-------|---------|-----------------|----------|
|
|
115
|
+
| **SELF** | Agent identity | Confidence, reinforcement, centrality | System prompt injection |
|
|
116
|
+
| **ATTENTION** | Query-relevant knowledge | Similarity, recency | RAG-style retrieval |
|
|
117
|
+
| **TASK** | Goal-oriented context | Balanced across all signals | Task planning |
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# Identity context — cached, no embedding needed
|
|
121
|
+
self_ctx = await system.frame("self")
|
|
122
|
+
|
|
123
|
+
# Knowledge retrieval — hybrid pipeline with graph expansion
|
|
124
|
+
attn_ctx = await system.frame("attention", query="async error handling")
|
|
125
|
+
|
|
126
|
+
# Task context — balanced scoring, goal blocks guaranteed
|
|
127
|
+
task_ctx = await system.frame("task", query="refactor the API layer")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Decay Tiers
|
|
131
|
+
|
|
132
|
+
Knowledge decays at different rates based on its nature:
|
|
133
|
+
|
|
134
|
+
| Tier | Half-life | Use Case |
|
|
135
|
+
|------|-----------|----------|
|
|
136
|
+
| Permanent | ~80,000 hours | Constitutional beliefs, core identity |
|
|
137
|
+
| Durable | ~693 hours | Stable preferences, learned values |
|
|
138
|
+
| Standard | ~69 hours | General knowledge |
|
|
139
|
+
| Ephemeral | ~14 hours | Session observations, temporary facts |
|
|
140
|
+
|
|
141
|
+
Decay is **session-aware**: the clock only ticks during active use. Your agent's memory doesn't degrade over holidays or downtime.
|
|
142
|
+
|
|
143
|
+
### Composite Scoring
|
|
144
|
+
|
|
145
|
+
Every block is scored across five dimensions:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
Score = w_similarity * cosine_similarity(query, block)
|
|
149
|
+
+ w_confidence * block.confidence
|
|
150
|
+
+ w_recency * exp(-lambda * hours_since_reinforced)
|
|
151
|
+
+ w_centrality * normalized_weighted_degree(block)
|
|
152
|
+
+ w_reinforcement * log(1 + count) / log(1 + max_count)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Each frame uses different weights. SELF emphasizes confidence and reinforcement. ATTENTION emphasizes similarity and recency.
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
### Minimal (defaults)
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
system = await MemorySystem.from_config("agent.db")
|
|
163
|
+
# Uses claude-sonnet-4-6 for LLM, text-embedding-3-small for embeddings
|
|
164
|
+
# Requires ANTHROPIC_API_KEY environment variable
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### YAML config file
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
# elfmem.yaml
|
|
171
|
+
llm:
|
|
172
|
+
model: "claude-sonnet-4-6"
|
|
173
|
+
contradiction_model: "claude-opus-4-6" # higher precision for contradictions
|
|
174
|
+
|
|
175
|
+
embeddings:
|
|
176
|
+
model: "text-embedding-3-small"
|
|
177
|
+
dimensions: 1536
|
|
178
|
+
|
|
179
|
+
memory:
|
|
180
|
+
inbox_threshold: 10
|
|
181
|
+
curate_interval_hours: 40
|
|
182
|
+
self_alignment_threshold: 0.70
|
|
183
|
+
prune_threshold: 0.05
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
system = await MemorySystem.from_config("agent.db", "elfmem.yaml")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Local models (no API key)
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
llm:
|
|
194
|
+
model: "ollama/llama3.2"
|
|
195
|
+
base_url: "http://localhost:11434"
|
|
196
|
+
|
|
197
|
+
embeddings:
|
|
198
|
+
model: "ollama/nomic-embed-text"
|
|
199
|
+
dimensions: 768
|
|
200
|
+
base_url: "http://localhost:11434"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Environment variables
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
207
|
+
# or
|
|
208
|
+
export OPENAI_API_KEY=sk-...
|
|
209
|
+
# or any provider LiteLLM supports
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
API keys are read by LiteLLM from standard environment variables. They never appear in config files.
|
|
213
|
+
|
|
214
|
+
## Agent Integration Pattern
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
async def run_turn(system, user_message):
|
|
218
|
+
# 1. Assemble context
|
|
219
|
+
self_ctx = await system.frame("self")
|
|
220
|
+
attn_ctx = await system.frame("attention", query=user_message)
|
|
221
|
+
|
|
222
|
+
# 2. Build prompt with memory context
|
|
223
|
+
prompt = f"""
|
|
224
|
+
{self_ctx.text}
|
|
225
|
+
|
|
226
|
+
{attn_ctx.text}
|
|
227
|
+
|
|
228
|
+
User: {user_message}
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
# 3. Generate response
|
|
232
|
+
response = await llm.complete(prompt)
|
|
233
|
+
|
|
234
|
+
# 4. Learn from the interaction
|
|
235
|
+
if worth_remembering(response):
|
|
236
|
+
await system.learn(extract_knowledge(response))
|
|
237
|
+
|
|
238
|
+
return response
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## API Reference
|
|
242
|
+
|
|
243
|
+
### MemorySystem
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
# Factory
|
|
247
|
+
system = await MemorySystem.from_config(db_path, config=None)
|
|
248
|
+
|
|
249
|
+
# Session management (required)
|
|
250
|
+
async with system.session():
|
|
251
|
+
...
|
|
252
|
+
|
|
253
|
+
# Write
|
|
254
|
+
result = await system.learn(content, tags=None, category="knowledge")
|
|
255
|
+
|
|
256
|
+
# Read
|
|
257
|
+
frame_result = await system.frame(name, query=None, top_k=5)
|
|
258
|
+
blocks = await system.recall(name, query=None, top_k=5) # raw, no side effects
|
|
259
|
+
|
|
260
|
+
# Maintenance (usually automatic)
|
|
261
|
+
await system.consolidate() # process inbox → active
|
|
262
|
+
await system.curate() # archive decayed, prune edges, reinforce top-N
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Return Types
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
LearnResult(block_id, status) # "created" | "duplicate_rejected"
|
|
269
|
+
FrameResult(text, blocks, frame_name) # rendered text + scored blocks
|
|
270
|
+
ConsolidateResult(processed, promoted, deduplicated, edges_created)
|
|
271
|
+
CurateResult(archived, edges_pruned, reinforced)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Custom Prompts
|
|
275
|
+
|
|
276
|
+
Override the LLM prompts for domain-specific agents:
|
|
277
|
+
|
|
278
|
+
```yaml
|
|
279
|
+
prompts:
|
|
280
|
+
self_alignment: |
|
|
281
|
+
You are evaluating a memory block for a medical AI assistant...
|
|
282
|
+
{self_context}
|
|
283
|
+
{block}
|
|
284
|
+
Respond: {"score": <float>}
|
|
285
|
+
|
|
286
|
+
valid_self_tags:
|
|
287
|
+
- "self/constitutional"
|
|
288
|
+
- "self/domain/oncology"
|
|
289
|
+
- "self/regulatory/hipaa"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Custom Adapters
|
|
293
|
+
|
|
294
|
+
For full control, implement the port protocols directly:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from elfmem.ports.services import LLMService, EmbeddingService
|
|
298
|
+
|
|
299
|
+
class MyLLMService:
|
|
300
|
+
async def score_self_alignment(self, block: str, self_context: str) -> float: ...
|
|
301
|
+
async def infer_self_tags(self, block: str, self_context: str) -> list[str]: ...
|
|
302
|
+
async def detect_contradiction(self, block_a: str, block_b: str) -> float: ...
|
|
303
|
+
|
|
304
|
+
system = MemorySystem(engine, llm_service=MyLLMService(), embedding_service=MyEmbedder())
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Architecture
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
src/elfmem/
|
|
311
|
+
├── api.py # MemorySystem — public API
|
|
312
|
+
├── config.py # ElfmemConfig — Pydantic configuration
|
|
313
|
+
├── scoring.py # Composite scoring formula (frozen)
|
|
314
|
+
├── types.py # Domain types — shared vocabulary
|
|
315
|
+
├── prompts.py # LLM prompt templates
|
|
316
|
+
├── session.py # Session lifecycle, active hours tracking
|
|
317
|
+
├── ports/
|
|
318
|
+
│ └── services.py # LLMService + EmbeddingService protocols
|
|
319
|
+
├── adapters/
|
|
320
|
+
│ ├── mock.py # Deterministic mocks for testing
|
|
321
|
+
│ ├── litellm.py # Real adapters (LiteLLM + instructor)
|
|
322
|
+
│ └── models.py # Pydantic response models
|
|
323
|
+
├── db/
|
|
324
|
+
│ ├── models.py # SQLAlchemy Core table definitions
|
|
325
|
+
│ ├── engine.py # Async engine factory
|
|
326
|
+
│ └── queries.py # All database operations
|
|
327
|
+
├── memory/
|
|
328
|
+
│ ├── blocks.py # Block state, content hashing, decay tiers
|
|
329
|
+
│ ├── dedup.py # Near-duplicate detection and resolution
|
|
330
|
+
│ ├── graph.py # Centrality, expansion, edge reinforcement
|
|
331
|
+
│ └── retrieval.py # 4-stage hybrid retrieval pipeline
|
|
332
|
+
├── context/
|
|
333
|
+
│ ├── frames.py # Frame definitions, registry, cache
|
|
334
|
+
│ ├── rendering.py # Blocks → rendered text
|
|
335
|
+
│ └── contradiction.py # Contradiction suppression
|
|
336
|
+
└── operations/
|
|
337
|
+
├── learn.py # learn() — fast-path ingestion
|
|
338
|
+
├── consolidate.py # consolidate() — batch promotion
|
|
339
|
+
├── recall.py # recall() — retrieval + reinforcement
|
|
340
|
+
└── curate.py # curate() — maintenance
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Four layers, clear boundaries:**
|
|
344
|
+
|
|
345
|
+
| Layer | Responsibility | Side Effects |
|
|
346
|
+
|-------|---------------|-------------|
|
|
347
|
+
| **Storage** (db/) | Tables, queries, engine | Database writes |
|
|
348
|
+
| **Memory** (memory/) | Blocks, dedup, graph, retrieval | None (pure) |
|
|
349
|
+
| **Context** (context/) | Frames, rendering, contradictions | None (pure) |
|
|
350
|
+
| **Operations** (operations/) | Orchestration, lifecycle | All side effects |
|
|
351
|
+
|
|
352
|
+
## Development
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Clone
|
|
356
|
+
git clone https://github.com/emson/elfmem.git
|
|
357
|
+
cd elfmem
|
|
358
|
+
|
|
359
|
+
# Install with dev dependencies
|
|
360
|
+
uv sync --extra dev
|
|
361
|
+
|
|
362
|
+
# Run tests (no API key needed — uses deterministic mocks)
|
|
363
|
+
uv run pytest
|
|
364
|
+
|
|
365
|
+
# Type checking
|
|
366
|
+
uv run mypy --ignore-missing-imports src/elfmem/
|
|
367
|
+
|
|
368
|
+
# Lint
|
|
369
|
+
uv run ruff check src/ tests/
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Testing Philosophy
|
|
373
|
+
|
|
374
|
+
All tests run against deterministic mock services. No API keys, no network calls, fully reproducible. The mock embedding service produces hash-seeded vectors — same input always gives the same embedding. The mock LLM service returns configurable scores and tags via substring matching.
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
from elfmem.adapters.mock import make_mock_llm, make_mock_embedding
|
|
378
|
+
|
|
379
|
+
# Control exactly what the LLM returns
|
|
380
|
+
llm = make_mock_llm(
|
|
381
|
+
alignment_overrides={"identity": 0.95},
|
|
382
|
+
tag_overrides={"identity": ["self/value"]},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Control similarity between specific texts
|
|
386
|
+
embedding = make_mock_embedding(
|
|
387
|
+
similarity_overrides={
|
|
388
|
+
frozenset({"cats are great", "dogs are great"}): 0.85,
|
|
389
|
+
},
|
|
390
|
+
)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Design Decisions
|
|
394
|
+
|
|
395
|
+
| Decision | Rationale |
|
|
396
|
+
|----------|-----------|
|
|
397
|
+
| SQLAlchemy Core, not ORM | Bulk updates, embedding BLOBs, N+1 centrality queries |
|
|
398
|
+
| Session-aware decay, not wall-clock | Knowledge survives holidays and downtime |
|
|
399
|
+
| Soft bias for identity, not hard gates | Everything is learned; self-aligned knowledge just survives longer |
|
|
400
|
+
| Retrieval is pure; reinforcement is separate | Clean separation of read path and side effects |
|
|
401
|
+
| LiteLLM as unified backend | One adapter for 100+ providers; switch with config |
|
|
402
|
+
| Mock-first testing | All logic verified without API keys; adapters are thin wrappers |
|
|
403
|
+
|
|
404
|
+
## License
|
|
405
|
+
|
|
406
|
+
MIT
|
|
407
|
+
|
|
408
|
+
## Acknowledgements
|
|
409
|
+
|
|
410
|
+
elfmem was designed through 26 structured explorations and 6 subsystem playgrounds, building mathematical confidence in every architectural decision before writing code. The complete design documentation is in `sim/explorations/`.
|