epimneme 0.7.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 (101) hide show
  1. epimneme-0.7.0/.dockerignore +10 -0
  2. epimneme-0.7.0/.env.example +14 -0
  3. epimneme-0.7.0/.github/workflows/ci.yml +98 -0
  4. epimneme-0.7.0/.gitignore +43 -0
  5. epimneme-0.7.0/AI_TOOLBOX.md +275 -0
  6. epimneme-0.7.0/ARCHITECTURE.md +208 -0
  7. epimneme-0.7.0/CHANGELOG.md +34 -0
  8. epimneme-0.7.0/CONTRIBUTING.md +64 -0
  9. epimneme-0.7.0/Dockerfile +41 -0
  10. epimneme-0.7.0/ENGRAM2_HIGH_RECALL_SPEC.md +390 -0
  11. epimneme-0.7.0/LICENSE +21 -0
  12. epimneme-0.7.0/Makefile +105 -0
  13. epimneme-0.7.0/NOTICE +29 -0
  14. epimneme-0.7.0/PKG-INFO +242 -0
  15. epimneme-0.7.0/README.md +211 -0
  16. epimneme-0.7.0/SECURITY.md +37 -0
  17. epimneme-0.7.0/benchmarks/BENCHMARK_RESULTS.md +503 -0
  18. epimneme-0.7.0/benchmarks/DATA.md +59 -0
  19. epimneme-0.7.0/benchmarks/__init__.py +0 -0
  20. epimneme-0.7.0/benchmarks/ablation_beam.sh +190 -0
  21. epimneme-0.7.0/benchmarks/analyze_pref_misses.py +55 -0
  22. epimneme-0.7.0/benchmarks/beam_bench.py +568 -0
  23. epimneme-0.7.0/benchmarks/beir_bench.py +502 -0
  24. epimneme-0.7.0/benchmarks/compare_pref.py +37 -0
  25. epimneme-0.7.0/benchmarks/compare_runs.py +137 -0
  26. epimneme-0.7.0/benchmarks/convomem_bench.py +804 -0
  27. epimneme-0.7.0/benchmarks/convomem_diagnose.py +177 -0
  28. epimneme-0.7.0/benchmarks/data/test_beam_fixture.json +110 -0
  29. epimneme-0.7.0/benchmarks/data/test_lme_fixture.json +48 -0
  30. epimneme-0.7.0/benchmarks/data/test_locomo_fixture.json +50 -0
  31. epimneme-0.7.0/benchmarks/download_data.sh +37 -0
  32. epimneme-0.7.0/benchmarks/epimneme_client.py +209 -0
  33. epimneme-0.7.0/benchmarks/lme_e2e_bench.py +760 -0
  34. epimneme-0.7.0/benchmarks/lme_report.py +95 -0
  35. epimneme-0.7.0/benchmarks/locomo_bench.py +452 -0
  36. epimneme-0.7.0/benchmarks/longmemeval_bench.py +590 -0
  37. epimneme-0.7.0/benchmarks/mabench_bench.py +506 -0
  38. epimneme-0.7.0/benchmarks/metrics.py +86 -0
  39. epimneme-0.7.0/benchmarks/requirements.txt +2 -0
  40. epimneme-0.7.0/benchmarks/run_bench.sh +166 -0
  41. epimneme-0.7.0/docker-compose.example.yml +94 -0
  42. epimneme-0.7.0/pyproject.toml +58 -0
  43. epimneme-0.7.0/scripts/backup.sh +46 -0
  44. epimneme-0.7.0/scripts/restore.sh +43 -0
  45. epimneme-0.7.0/src/epimneme/__init__.py +3 -0
  46. epimneme-0.7.0/src/epimneme/activity.py +171 -0
  47. epimneme-0.7.0/src/epimneme/auth.py +190 -0
  48. epimneme-0.7.0/src/epimneme/backup.py +684 -0
  49. epimneme-0.7.0/src/epimneme/bulk_import.py +496 -0
  50. epimneme-0.7.0/src/epimneme/core/__init__.py +1 -0
  51. epimneme-0.7.0/src/epimneme/core/config.py +249 -0
  52. epimneme-0.7.0/src/epimneme/core/models.py +450 -0
  53. epimneme-0.7.0/src/epimneme/dashboard.py +11 -0
  54. epimneme-0.7.0/src/epimneme/dateutil.py +134 -0
  55. epimneme-0.7.0/src/epimneme/decay.py +82 -0
  56. epimneme-0.7.0/src/epimneme/dedup.py +127 -0
  57. epimneme-0.7.0/src/epimneme/fusion.py +1185 -0
  58. epimneme-0.7.0/src/epimneme/logging.py +40 -0
  59. epimneme-0.7.0/src/epimneme/manage.py +269 -0
  60. epimneme-0.7.0/src/epimneme/manager.py +1140 -0
  61. epimneme-0.7.0/src/epimneme/maxsim.py +194 -0
  62. epimneme-0.7.0/src/epimneme/migrations/001_migrate_pseudo_entities.py +65 -0
  63. epimneme-0.7.0/src/epimneme/migrations/002_add_decay_versioning_dedup.py +54 -0
  64. epimneme-0.7.0/src/epimneme/migrations/003_add_pinned_field.py +23 -0
  65. epimneme-0.7.0/src/epimneme/migrations/004_add_project_persistent.py +23 -0
  66. epimneme-0.7.0/src/epimneme/migrations/005_add_session_ordinal.py +55 -0
  67. epimneme-0.7.0/src/epimneme/migrations/__init__.py +5 -0
  68. epimneme-0.7.0/src/epimneme/migrations/runner.py +102 -0
  69. epimneme-0.7.0/src/epimneme/ratelimit.py +122 -0
  70. epimneme-0.7.0/src/epimneme/reflection.py +361 -0
  71. epimneme-0.7.0/src/epimneme/rerank.py +142 -0
  72. epimneme-0.7.0/src/epimneme/scheduler.py +166 -0
  73. epimneme-0.7.0/src/epimneme/server.py +1792 -0
  74. epimneme-0.7.0/src/epimneme/skills.py +466 -0
  75. epimneme-0.7.0/src/epimneme/stores/__init__.py +1 -0
  76. epimneme-0.7.0/src/epimneme/stores/postgresql.py +2247 -0
  77. epimneme-0.7.0/src/epimneme/templates/dashboard.html +1803 -0
  78. epimneme-0.7.0/src/epimneme/textlog.py +62 -0
  79. epimneme-0.7.0/tests/__init__.py +0 -0
  80. epimneme-0.7.0/tests/conftest.py +187 -0
  81. epimneme-0.7.0/tests/test_activity.py +237 -0
  82. epimneme-0.7.0/tests/test_auth.py +98 -0
  83. epimneme-0.7.0/tests/test_backup.py +966 -0
  84. epimneme-0.7.0/tests/test_bulk_import.py +197 -0
  85. epimneme-0.7.0/tests/test_config.py +178 -0
  86. epimneme-0.7.0/tests/test_decay.py +179 -0
  87. epimneme-0.7.0/tests/test_dedup.py +182 -0
  88. epimneme-0.7.0/tests/test_fusion.py +227 -0
  89. epimneme-0.7.0/tests/test_hardening.py +177 -0
  90. epimneme-0.7.0/tests/test_integration.py +552 -0
  91. epimneme-0.7.0/tests/test_logging.py +109 -0
  92. epimneme-0.7.0/tests/test_manager.py +547 -0
  93. epimneme-0.7.0/tests/test_migrations.py +87 -0
  94. epimneme-0.7.0/tests/test_models.py +160 -0
  95. epimneme-0.7.0/tests/test_pagination.py +119 -0
  96. epimneme-0.7.0/tests/test_ratelimit.py +87 -0
  97. epimneme-0.7.0/tests/test_reflection.py +474 -0
  98. epimneme-0.7.0/tests/test_rerank.py +88 -0
  99. epimneme-0.7.0/tests/test_scheduler.py +236 -0
  100. epimneme-0.7.0/tests/test_server.py +420 -0
  101. epimneme-0.7.0/tests/test_skills.py +199 -0
@@ -0,0 +1,10 @@
1
+ db-data/
2
+ tests/
3
+ scripts/
4
+ *.bak
5
+ *.pyc
6
+ __pycache__/
7
+ .git/
8
+ .pytest_cache/
9
+ .env
10
+ *.egg-info/
@@ -0,0 +1,14 @@
1
+ # Engram environment variables — copy to .env and edit
2
+ # ------------------------------------------------------------------
3
+ # REQUIRED: PostgreSQL password (must not be the literal "engram")
4
+ ENGRAM_PG_PASSWORD=change_me_to_a_strong_random_string
5
+
6
+ # Domain suffix used by the Traefik labels in docker-compose.yml
7
+ # (e.g. DOMAINNAME=example.com produces engram.example.com)
8
+ DOMAINNAME=example.com
9
+
10
+ # Timezone (see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
11
+ TZ=UTC
12
+
13
+ # Optional: set to "1" to allow running with the default password (local dev only)
14
+ # ENGRAM_DEMO_MODE=0
@@ -0,0 +1,98 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ env:
10
+ PYTHON_VERSION: "3.12"
11
+
12
+ jobs:
13
+ lint:
14
+ name: Lint
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ env.PYTHON_VERSION }}
22
+
23
+ - name: Install ruff
24
+ run: pip install ruff
25
+
26
+ - name: Ruff check
27
+ run: ruff check src/ tests/
28
+
29
+ - name: Ruff format check
30
+ run: ruff format --check src/ tests/
31
+
32
+ test:
33
+ name: Test
34
+ runs-on: ubuntu-latest
35
+ needs: lint
36
+
37
+ services:
38
+ postgres:
39
+ image: pgvector/pgvector:pg16
40
+ env:
41
+ POSTGRES_USER: engram
42
+ POSTGRES_PASSWORD: engram_test
43
+ POSTGRES_DB: engram_test
44
+ ports:
45
+ - 5432:5432
46
+ options: >-
47
+ --health-cmd "pg_isready -U engram"
48
+ --health-interval 5s
49
+ --health-timeout 5s
50
+ --health-retries 10
51
+
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+
55
+ - uses: actions/setup-python@v5
56
+ with:
57
+ python-version: ${{ env.PYTHON_VERSION }}
58
+
59
+ - name: Install dependencies
60
+ run: |
61
+ pip install --upgrade pip
62
+ pip install .[dev]
63
+
64
+ - name: Run unit tests
65
+ run: pytest tests/ -x -v --tb=short
66
+
67
+ - name: Run unit tests with coverage
68
+ run: |
69
+ pip install pytest-cov
70
+ pytest tests/ --cov=engram --cov-report=term-missing --cov-report=xml
71
+
72
+ - name: Upload coverage
73
+ if: always()
74
+ uses: actions/upload-artifact@v4
75
+ with:
76
+ name: coverage-report
77
+ path: coverage.xml
78
+
79
+ docker:
80
+ name: Docker Build
81
+ runs-on: ubuntu-latest
82
+ needs: test
83
+ if: github.ref == 'refs/heads/main'
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+
87
+ - name: Build Docker image
88
+ run: docker build -t engram:ci .
89
+
90
+ - name: Smoke test container
91
+ run: |
92
+ docker run -d --name engram-smoke \
93
+ -e ENGRAM_PG_HOST=host.docker.internal \
94
+ -e ENGRAM_EMBEDDINGS_ENABLED=0 \
95
+ engram:ci || true
96
+ sleep 3
97
+ docker logs engram-smoke 2>&1 | head -20
98
+ docker rm -f engram-smoke || true
@@ -0,0 +1,43 @@
1
+ # Database volumes
2
+ db-data/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+
17
+ # IDE
18
+ .vscode/
19
+ .idea/
20
+
21
+ # Local deployment overrides (copy from docker-compose.example.yml)
22
+ docker-compose.yml
23
+ docker-compose.override.yml
24
+ .env
25
+
26
+ # Backups
27
+ backups/
28
+
29
+ # Activity logs
30
+ logs/
31
+
32
+ # Benchmark data — fetched by benchmarks/download_data.sh
33
+ benchmarks/data/**
34
+ !benchmarks/data/
35
+ !benchmarks/data/test_*_fixture.json
36
+
37
+ # Benchmark run logs, raw result files, and tuning sweeps
38
+ benchmarks/*.log
39
+ benchmarks/results_*.json
40
+ benchmarks/results_*.jsonl
41
+ benchmarks/results_*.tsv
42
+ benchmarks/near_miss_analysis*.txt
43
+ benchmarks/sweep*
@@ -0,0 +1,275 @@
1
+ # Engram AI Toolbox
2
+
3
+ Reference for agents running benchmarks, tuning config, and interpreting results.
4
+
5
+ ---
6
+
7
+ ## Running Benchmarks
8
+
9
+ Always use the wrapper script — it captures git hash, container config, and version automatically:
10
+
11
+ ```bash
12
+ cd /docker/appdata/engram
13
+
14
+ # LongMemEval (500q, ~40min with MiniLM, ~55hr with BGE-large)
15
+ nohup ./benchmarks/run_bench.sh lme <version_tag> > /dev/null 2>&1 &
16
+
17
+ # LME with pre-staged data (~2min query-only — see Pre-Staging section below)
18
+ nohup ./benchmarks/run_bench.sh lme <version_tag> --skip-ingest > /dev/null 2>&1 &
19
+
20
+ # LoCoMo (10 conversations, ~3min; ~30sec with --skip-ingest)
21
+ nohup ./benchmarks/run_bench.sh locomo <version_tag> > /dev/null 2>&1 &
22
+
23
+ # MABench FactConsolidation (~30sec)
24
+ ./benchmarks/run_bench.sh mabench <version_tag>
25
+
26
+ # BEAM 100K (~10min; ~2min with --skip-ingest)
27
+ nohup ./benchmarks/run_bench.sh beam <version_tag> --split 100K > /dev/null 2>&1 &
28
+
29
+ # ConvoMem (sample=50 per category, ~20min)
30
+ nohup ./benchmarks/run_bench.sh convomem <version_tag> > /dev/null 2>&1 &
31
+
32
+ # BEIR SciFact (~46min full; ~46min query-only with --skip-ingest)
33
+ # Full corpus: 5,183 docs; 300 test queries; metric: nDCG@10
34
+ nohup ./benchmarks/run_bench.sh beir <version_tag> --dataset scifact --no-cleanup > /dev/null 2>&1 &
35
+
36
+ # BEIR skip-ingest (corpus already staged as _beir_scifact project)
37
+ nohup ./benchmarks/run_bench.sh beir <version_tag> --dataset scifact --skip-ingest > /dev/null 2>&1 &
38
+ ```
39
+
40
+ Logs go to `/tmp/bench_<bench>_<tag>.log`. Results go to `benchmarks/results_engram_<bench>_<tag>_<timestamp>.*`.
41
+
42
+ All runs are appended to `benchmarks/results_history.tsv` for easy comparison.
43
+
44
+ ---
45
+
46
+ ## Pre-Staging Benchmark Data (Fast Iteration)
47
+
48
+ Ingest dominates benchmark runtime. Once staged, data lives in isolated projects with zero
49
+ leakage between questions/conversations. Subsequent runs skip ingest and go straight to queries.
50
+
51
+ | Benchmark | Normal runtime | Query-only runtime | Project pattern |
52
+ |---|---|---|---|
53
+ | LME (500q) | ~40min | ~2min | `_lme_bench_<question_id>` |
54
+ | LoCoMo (10 convs) | ~3min | ~30sec | `_locomo_bench_<sample_id>` |
55
+ | BEAM 100K | ~10min | ~2min | `_beam_bench_<conv_id>` |
56
+ | MABench | ~30sec | — | not worth staging |
57
+ | ConvoMem | ~20min | ❌ | uses timestamp in project name, can't match |
58
+ | BEIR SciFact | ~50min (ingest ~4min, queries ~46min) | ~46min | `_beir_scifact` |
59
+
60
+ **Workflow:**
61
+
62
+ ```bash
63
+ cd /docker/appdata/engram
64
+
65
+ # Step 1: Stage each benchmark once (~40min for LME, then done).
66
+ # Run this after any model/chunk-size change that invalidates existing embeddings.
67
+ python3 -u benchmarks/longmemeval_bench.py \
68
+ benchmarks/data/longmemeval_s_cleaned.json \
69
+ --granularity turn-pair --workers 4 \
70
+ --engram-url http://192.168.90.45:8000 --no-cleanup > /tmp/lme_prestage.log 2>&1 &
71
+
72
+ python3 -u benchmarks/locomo_bench.py \
73
+ benchmarks/data/locomo10.json \
74
+ --granularity dialog --engram-url http://192.168.90.45:8000 --no-cleanup
75
+
76
+ python3 -u benchmarks/beam_bench.py \
77
+ --split 100K --engram-url http://192.168.90.45:8000 --no-cleanup
78
+
79
+ # Step 2: All subsequent runs — just add --skip-ingest.
80
+ # --skip-ingest checks which projects already exist, auto-disables cleanup.
81
+ nohup ./benchmarks/run_bench.sh lme <version_tag> --skip-ingest > /dev/null 2>&1 &
82
+ nohup ./benchmarks/run_bench.sh locomo <version_tag> --skip-ingest > /dev/null 2>&1 &
83
+ nohup ./benchmarks/run_bench.sh beam <version_tag> --skip-ingest > /dev/null 2>&1 &
84
+ ```
85
+
86
+ **When to re-stage:**
87
+ - After changing `ENGRAM_EMBEDDING_MODEL` or `ENGRAM_CHUNK_SIZE` (old embeddings are stale)
88
+ - After a schema migration that drops the memories table
89
+ - To re-stage: delete staged projects first (`clear_all` or drop+recreate DB), then re-run Step 1
90
+
91
+ **Note:** `run_bench.sh` passes `--engram-url http://192.168.90.45:8000` and all extra args automatically.
92
+
93
+ ---
94
+
95
+ ## Checking Progress
96
+
97
+ ```bash
98
+ tail -5 /tmp/bench_lme_<tag>.log
99
+ tail -5 /tmp/bench_locomo_<tag>.log
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Scoring Results
105
+
106
+ ```bash
107
+ # Quick LME scorecard (compare multiple versions)
108
+ python3 << 'EOF'
109
+ import json, glob
110
+
111
+ def score(path):
112
+ r = [json.loads(l) for l in open(path) if l.strip()]
113
+ s = lambda k: sum(1 for x in r if x['retrieval_results']['metrics']['session'].get(k, 0) >= 1.0)
114
+ n = len(r)
115
+ return n, s('recall_any@1'), s('recall_any@5'), s('recall_any@10'), \
116
+ sum(1 for x in r if x['retrieval_results']['metrics']['session'].get('recall_any@10', 0) < 1.0)
117
+
118
+ for path in sorted(glob.glob('benchmarks/results_engram_lme_*.jsonl')):
119
+ n, h1, h5, h10, miss = score(path)
120
+ tag = path.split('/')[-1]
121
+ print(f"{tag}: R@1={h1}/{n}={h1/n:.4f} R@5={h5/n:.4f} R@10={h10/n:.4f} misses={miss}")
122
+ EOF
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Key Config Variables (engram2.yml)
128
+
129
+ | Variable | Current | Notes |
130
+ |---|---|---|
131
+ | `ENGRAM_EMBEDDING_MODEL` | all-MiniLM-L6-v2 | Switch to BGE-large for higher quality |
132
+ | `ENGRAM_EMBEDDING_DIM` | 384 | Must match model (BGE-large=1024) |
133
+ | `ENGRAM_CHUNK_SIZE` | 1000 | Max chars per chunk (~1150 safe limit for MiniLM) |
134
+ | `ENGRAM_CHUNK_OVERLAP` | 300 | Higher = better boundary coverage, more chunks |
135
+ | `ENGRAM_HNSW_EF_SEARCH` | 200 | Higher = better recall, slower query |
136
+ | `ENGRAM_LLM_RERANK_TOP_N` | 20 | Candidates sent to LLM reranker |
137
+
138
+ ### Pure-Math Recall Improvement Knobs (no new models)
139
+
140
+ | Variable | Default | Notes |
141
+ |---|---|---|
142
+ | `ENGRAM_BM25_SIGNAL_ENABLED` | `1` | In-process BM25 as additional RRF signal |
143
+ | `ENGRAM_BM25_SIGNAL_WEIGHT` | `0.5` | RRF weight for BM25 ranked list |
144
+ | `ENGRAM_ENTITY_SIGNAL_ENABLED` | `1` | Proper nouns + numbers overlap as RRF signal |
145
+ | `ENGRAM_ENTITY_SIGNAL_WEIGHT` | `0.3` | RRF weight for entity-overlap list |
146
+ | `ENGRAM_DATE_SIGNAL_WEIGHT` | `0.6` | RRF weight for date-proximity list (temporal queries) |
147
+ | `ENGRAM_RECENCY_SIGNAL_WEIGHT` | `0.2` | RRF weight for session-recency list (recency queries) |
148
+ | `ENGRAM_TURN_PAIR_SIGNAL_WEIGHT` | `0.15` | RRF weight for turn-pair-completeness list |
149
+ | `ENGRAM_MAXSIM_ENABLED` | `0` | Token-level MaxSim rerank (ColBERT-style, same model) |
150
+ | `ENGRAM_MAXSIM_TOP_N` | `20` | Candidates to rerank with MaxSim |
151
+ | `ENGRAM_MAXSIM_CACHE_SIZE` | `2048` | LRU doc-embedding cache entries |
152
+ | `ENGRAM_PRF_ENABLED` | `0` | Pseudo-relevance feedback (Rocchio, vague queries only) |
153
+ | `ENGRAM_PRF_TOP_K` | `5` | Top-K results used for PRF term extraction |
154
+ | `ENGRAM_PRF_N_TERMS` | `8` | Max expansion terms appended to FTS re-query |
155
+ | `ENGRAM_PRF_FTS_WEIGHT` | `0.3` | RRF weight for PRF result list |
156
+ | `ENGRAM_TIEBREAK_ENABLED` | `1` | Gap-aware tiebreaker (fires when top-2 gap ≤ eps) |
157
+ | `ENGRAM_TIEBREAK_EPS` | `0.005` | Score-gap threshold to trigger tiebreaker |
158
+ | `ENGRAM_MMR_ENABLED` | `1` | Session MMR diversification (counting queries only) |
159
+ | `ENGRAM_MMR_LAMBDA` | `0.7` | Relevance weight in MMR (0=diversity, 1=relevance) |
160
+ | `ENGRAM_MMR_SESSION_CAP` | `2` | Max chunks per session_id in output |
161
+ | `ENGRAM_TEMPORAL_HARD_FILTER` | `0` | Pre-filter candidates to target-date window |
162
+ | `ENGRAM_TEMPORAL_HARD_FILTER_SIGMA` | `3.5` | Window half-size in days |
163
+
164
+ Compose file: `/docker/compose/dev/engram2.yml`
165
+
166
+ After changing config, rebuild and restart:
167
+ ```bash
168
+ cd /docker/compose/dev
169
+ docker compose -f engram2.yml --env-file /docker/compose/.env up -d --build
170
+ # Wait ~10s then verify
171
+ curl -s http://192.168.90.45:8000/health
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Benchmark Version History
177
+
178
+ | Version | Model | chunk/overlap/ef | LME R@1 | LoCoMo avg | BEAM 100K | BEIR SciFact nDCG@10 | Notes |
179
+ |---|---|---|---|---|---|---|---|
180
+ | v100/v110 | MiniLM | 800/200/ef50 | 0.846 | 0.906 | — | — | Baseline |
181
+ | v120 | BGE-large | 800/200/ef50 | 0.860 | — | 0.3274 | — | +7 LME R@1, worse LoCoMo |
182
+ | v301 | MiniLM | 900/100/ef50 | 0.852 | 0.763 | — | — | TF cap=2 fix |
183
+ | v302 | MiniLM | 1000/300/ef200 | 0.852 | 0.763 | 0.3917 | — | Plateau, best pre-v4 |
184
+ | **v402** | MiniLM | 1000/300/ef200 | **0.856** | **0.785** | **0.4167** | **0.6569** | Multi-signal RRF + MMR gate fix |
185
+
186
+ **BEIR SciFact v402 (2026-05-12):** nDCG@10=0.6569, Recall@100=0.9350, Precision@10=0.0887, Any-hit@100=94.0% (282/300)
187
+ For context: BM25 baseline ≈ 0.665, SentenceBERT ≈ 0.664, DPR ≈ 0.318 — Engram is at BM25 level on this domain-specific IR task.
188
+
189
+ **v402 signals (all enabled by default):**
190
+ - Phase A: BM25 (w=0.5) + entity overlap (w=0.3) fused via RRF
191
+ - Phase B: MaxSim rerank (disabled by default, `ENGRAM_MAXSIM_ENABLED=1`)
192
+ - Phase C: PRF expansion (disabled by default, `ENGRAM_PRF_ENABLED=1`)
193
+ - Phase D: Date-proximity Gaussian boost (w=0.6), session recency (w=0.2), turn-pair (w=0.15)
194
+ - Phase E: MMR diversification (enabled, gate: fires for non-counting queries)
195
+ - Phase F: Gap-aware tiebreaker
196
+
197
+ **v402 clean ablation (BEAM 100K, skip-ingest on staged data):**
198
+ | Variant | avg_recall | Δ vs v402 |
199
+ |---|---|---|
200
+ | v402 (all on) | 0.4167 | baseline |
201
+ | abl-all-off | 0.3917 | −0.025 |
202
+ | abl-no-bm25 | 0.4148 | −0.002 |
203
+ | abl-no-entity | 0.4165 | −0.000 |
204
+ | abl-no-date | 0.4167 | 0.000 |
205
+ | abl-no-recency-turnpair | 0.4181 | +0.001 (recency/turn-pair mildly hurt BEAM) |
206
+ | abl-no-tiebreak-mmr | 0.4043 | −0.012 (tiebreak+MMR are critical) |
207
+
208
+ **Current best**: v402 (git: `public-prep` branch, commit `25d6016`)
209
+
210
+ ---
211
+
212
+ ## Known Hard Misses (LME v402)
213
+
214
+ 15 questions that fail R@1 consistently:
215
+ - **single-session-preference** (18/30 misses): soft/subjective queries ("Can you suggest accessories for my setup?") — correct session is in top-10 in 13/18 cases but ranking noise prevents rank-1. Small n=30 makes improvements unreliable.
216
+ - **temporal-reasoning** (~22/133 misses, R@1=0.835): time-dependent queries
217
+ - **multi-session** (~21/133 misses, R@1=0.842): 14 are close-rank misses (answer at rank 2-3); often generic sharegpt/ultrachat content outranks personal conversation turns via BM25
218
+
219
+ **Preference boost notes**: Broadening `apply_preference_signal_boost` to fire on advice-seeking queries regardless of vagueness HURT preference (-1 hit) because non-topic preference language in other sessions gets incorrectly boosted. The vague-query gate is intentional.
220
+
221
+ ---
222
+
223
+ ## End-to-End LME (Retrieval + Generation)
224
+
225
+ `lme_e2e_bench.py` loads retrieval results from a prior LME run, feeds top-K chunks to
226
+ Gemma4:31b via Ollama, and evaluates generated answers against gold.
227
+
228
+ ```bash
229
+ cd /docker/appdata/engram
230
+
231
+ # Full 500q E2E (top-10 chunks → Gemma4, ~55min)
232
+ python3 benchmarks/lme_e2e_bench.py \
233
+ --retrieval-results benchmarks/results_engram_lme_v402-mmr-fix_20260512_1106.jsonl \
234
+ --out benchmarks/results_engram_lme_e2e_v402.jsonl
235
+
236
+ # Judge pass on misses (adds LLM rescoring of substring-match failures)
237
+ python3 benchmarks/lme_e2e_bench.py \
238
+ --retrieval-results benchmarks/results_engram_lme_v402-mmr-fix_20260512_1106.jsonl \
239
+ --judge --out benchmarks/results_engram_lme_e2e_v402_judged.jsonl
240
+ ```
241
+
242
+ **v402 E2E results (retrieval + Gemma4:31b, top-K=10, corrected scoring):**
243
+
244
+ | Type | N | Exact | Smart-Norm | Judge | **Final** | Retrieval R@1 |
245
+ |---|---|---|---|---|---|---|
246
+ | single-session-user | 70 | 0.800 | +2 | +4 | **0.843** | 0.957 |
247
+ | knowledge-update | 78 | 0.654 | +5 | +2 | **0.821** | 0.990 |
248
+ | single-session-assistant | 56 | 0.268 | +1 | +3 | **0.429** | 0.929 |
249
+ | multi-session | 133 | 0.293 | +10 | +15 | **0.451** | 0.985 |
250
+ | temporal-reasoning | 133 | 0.195 | +5 | +25 | **0.429** | 0.962 |
251
+ | single-session-preference | 30 | 0.000 | +1 | +6 | **0.067** | 0.933 |
252
+ | **TOTAL** | **500** | **0.374** | **+24** | **+55** | **0.532** | **0.856** |
253
+
254
+ **Key finding:** 174/313 misses (56%) are "Unknown" responses — the LLM cannot extract the
255
+ answer from the provided chunks even though retrieval is excellent. This is an extraction
256
+ problem, not a retrieval problem. The assistant (50% Unknown) and preference (73% Unknown)
257
+ categories are hardest; user-facts (84.3%) and knowledge-updates (82.1%) are our strong suit.
258
+
259
+ **Judge details:** Gemma4:31b acting as LLM judge with leniency (paraphrases, number formats,
260
+ minor wording). 55/115 judgeable misses rescued. "Unknown" responses skipped (can't judge).
261
+
262
+ ---
263
+
264
+ ## Important Code Locations
265
+
266
+ | File | What's there |
267
+ |---|---|
268
+ | `src/engram/rerank.py` | TF cap (`_TF_CAP = 2`), keyword reranker |
269
+ | `src/engram/manager.py` | `apply_turn_pair_boost()`, embedding prefix handling |
270
+ | `src/engram/config.py` | All config defaults |
271
+ | `benchmarks/run_bench.sh` | Benchmark launcher (use this!) |
272
+ | `benchmarks/lme_e2e_bench.py` | End-to-end LME (retrieval + generation + judge) |
273
+ | `benchmarks/results_history.tsv` | One-line summary of every run |
274
+ | `/docker/compose/dev/engram2.yml` | Container config (model, chunk size, etc.) |
275
+ | `/tmp/bench_<bench>_<tag>.log` | Live benchmark log |
@@ -0,0 +1,208 @@
1
+ # Architecture
2
+
3
+ Engram is a persistent memory service for AI coding agents. It exposes three surfaces:
4
+
5
+ 1. **MCP over SSE** at `/sse` + `/messages` — for MCP-compatible clients (VS Code, Cursor, Claude Desktop, n8n).
6
+ 2. **REST API** at `/api/*` — for scripts, automation, and non-MCP clients.
7
+ 3. **Web dashboard** at `/` — for humans, intended to sit behind SSO/OAuth at your reverse proxy.
8
+
9
+ Everything runs as a single FastAPI process against a single PostgreSQL database with the `pgvector` extension.
10
+
11
+ ---
12
+
13
+ ## Request Lifecycle
14
+
15
+ ```
16
+ client ──► (reverse proxy / OAuth) ──► FastAPI ──► AuthMiddleware
17
+
18
+ ┌────────────┴───────────┐
19
+ ▼ ▼
20
+ MemoryManager FastMCP (SSE)
21
+ │ │
22
+ └────────┬───────────────┘
23
+
24
+ PostgreSQL + pgvector
25
+ ```
26
+
27
+ ### Authentication
28
+
29
+ Two paths, resolved by `auth.get_auth`:
30
+
31
+ 1. **Bearer token** (`Authorization: Bearer engram_…`) — validated against the `api_keys` table in constant time. Keys are project-scoped (`agent`) or global (`admin`).
32
+ 2. **OAuth passthrough** — the reverse proxy sets `X-Forwarded-User` after its own auth. Trusted as admin.
33
+
34
+ `ENGRAM_DEMO_MODE=1` short-circuits both with an unauthenticated admin identity for local development.
35
+
36
+ MCP requests reuse the same resolver via `get_mcp_auth(ctx)`, which reads the `Authorization` header from the SSE handshake.
37
+
38
+ ### Project scoping
39
+
40
+ Every tenant-facing operation calls `AuthContext.enforce_project_access(project_name)`. Agents can "claim" a project name that doesn't yet exist — this makes self-service onboarding possible without an admin interventions step.
41
+
42
+ ---
43
+
44
+ ## Storage Layer (`stores/postgresql.py`)
45
+
46
+ A single `PostgresStore` class wraps a `psycopg` async connection pool and owns all SQL. Tables:
47
+
48
+ | Table | Purpose |
49
+ |---|---|
50
+ | `projects` | Tenant namespaces. `persistent_memories` flag exempts all memories from decay/GC. |
51
+ | `sessions` | Agent working sessions — task, summary, handoff notes. |
52
+ | `memories` | The core table. 20+ columns: kind, content, subject, `embedding vector(384)`, `simhash bigint`, decay state, version chain, `pinned`, `obsolete`, tags (JSONB), `content_tsv` (tsvector). |
53
+ | `memory_access` | Access log used by the decay model. |
54
+ | `entities` | Graph nodes — file, module, concept, tool, person, library, config, command. |
55
+ | `relationships` | Directed graph edges. |
56
+ | `memory_entities` | Link table associating memories with the entities they mention. |
57
+ | `api_keys` | Hashed keys only. Prefix is visible for identification. |
58
+ | `schema_migrations` | Applied migration versions. |
59
+
60
+ ### Indexes
61
+
62
+ - `memories.embedding` — HNSW, `vector_cosine_ops`, `m=16, ef_construction=64`.
63
+ - `memories.content_tsv` — GiST for full-text search.
64
+ - `memories.content` — trigram GIN for fuzzy matching.
65
+ - Multicolumn covering indexes on `(project_id, kind)`, `(from_entity)`, `(to_entity)`.
66
+
67
+ ### Migrations
68
+
69
+ `migrations/runner.py` applies numbered Python migration modules in order and records each in `schema_migrations`. Current migrations:
70
+
71
+ 1. Entity tracking bootstrap (pseudo-entity cleanup)
72
+ 2. Decay fields, versioning, simhash
73
+ 3. Pinned memories
74
+ 4. Per-project persistent flag
75
+
76
+ ---
77
+
78
+ ## MemoryManager (`manager.py`)
79
+
80
+ The unified API consumed by both the REST handlers and MCP tools.
81
+
82
+ ### `remember()` pipeline
83
+
84
+ 1. **SimHash dedup** — O(1) Hamming distance against recent memories in the same project. Configurable threshold (default 3 bits).
85
+ 2. **Embedding** — generated in a thread-pool (non-blocking async).
86
+ 3. **Semantic dedup** — cosine distance against existing embeddings above `semantic_dedup_threshold` (default 0.92).
87
+ 4. **Conflict surfacing** — find fact/decision memories with similarity ≥ 0.80. Returned to the caller so the agent can explicitly supersede or reconcile.
88
+ 5. **Store** — with version-chain pointers if `supersedes` was supplied.
89
+
90
+ ### `recall()`
91
+
92
+ Runs **two retrieval passes in parallel**, then fuses:
93
+
94
+ - **Semantic** — pgvector `<=>` cosine search, HNSW-accelerated.
95
+ - **Keyword** — `ts_rank` against `content_tsv` plus trigram similarity.
96
+
97
+ Ranks are fused via **Reciprocal Rank Fusion** with configurable weights (`rrf_vector_weight`, `rrf_keyword_weight`). Decay-based score boost (range `[0.3, 1.2]`) is applied last.
98
+
99
+ Top-5 results have their decay state updated on access (`update_decay_on_access`).
100
+
101
+ ### Knowledge graph
102
+
103
+ `entity_explore()` runs a recursive CTE over `relationships`, configurable depth and direction (`incoming`/`outgoing`/`both`). Returns entities + the memories linked to them.
104
+
105
+ ---
106
+
107
+ ## Decay Model (`decay.py`)
108
+
109
+ FSRS-inspired power-law retrievability:
110
+
111
+ - `R = exp(-t / S)` where `S = base_stability * (1 + storage_strength)`.
112
+ - `update_on_access` boosts `S` with diminishing returns: `S_new = S + growth_factor / (1 + 0.5 * S)`.
113
+ - At query time, `decay_score_boost` returns a ranking multiplier in `[0.3, 1.2]`.
114
+
115
+ Pinned memories, memories in persistent projects, and memories of kind `decision` or `procedure` are exempt from GC.
116
+
117
+ ---
118
+
119
+ ## Reflection (`reflection.py`, `scheduler.py`)
120
+
121
+ An async background job runs every `ENGRAM_REFLECTION_INTERVAL_HOURS` (default 24). Three phases:
122
+
123
+ 1. **GC** — mark obsolete any memory with retrievability < `ENGRAM_REFLECTION_GC_THRESHOLD` (default 0.05), older than `ENGRAM_REFLECTION_GC_MIN_AGE_DAYS` (default 7), and not in an exempt class.
124
+ 2. **Consolidation** — cluster high-similarity memories (threshold 0.88), merge into a single summary memory, mark originals obsolete with a pointer.
125
+ 3. **Conflict resolution** — find fact/decision pairs with similarity ≥ 0.85 and age gap ≥ 7 days; obsolete the older one.
126
+
127
+ Limits (`reflection_max_consolidations` = 10) prevent churn. The scheduler exposes `/api/admin/reflection/run` for manual triggering.
128
+
129
+ ---
130
+
131
+ ## Activity & Logging
132
+
133
+ - `activity.py` — in-memory ring buffer (~2000 events). Event types: `WRITE`, `RECALL`, `SESSION`, `ENTITY`, `REFLECT`, `DEDUP`, `FORGET`, `CONFLICT`.
134
+ - `textlog.py` — best-effort persistent log tailed by the dashboard.
135
+ - `logging.py` — optional JSON log formatter (`LOG_FORMAT=json`).
136
+
137
+ ---
138
+
139
+ ## Backup (`backup.py`)
140
+
141
+ JSON archive format v2 including all tables except `api_keys` and `schema_migrations`. Embeddings serialize as JSON arrays. Backward-compatible v1→v2 upgrade. Restore supports `merge` (upsert) or `clean` (truncate first). Auto-rotation keeps `N` recent plus any younger than `X` days.
142
+
143
+ ---
144
+
145
+ ## Rate Limiting (`ratelimit.py`)
146
+
147
+ Starlette middleware, per-IP token bucket.
148
+
149
+ - Defaults: burst 30, refill 120/min.
150
+ - Exempt paths: `/health`, `/sse`, `/messages`.
151
+ - Respects `X-Forwarded-For` when behind a proxy.
152
+ - Returns `429` with `Retry-After`.
153
+
154
+ ---
155
+
156
+ ## Configuration
157
+
158
+ All via environment variables. Defaults live in [`src/engram/core/config.py`](src/engram/core/config.py).
159
+
160
+ ### Required
161
+
162
+ | Variable | Notes |
163
+ |---|---|
164
+ | `ENGRAM_PG_PASSWORD` | Must not be literal `engram`. Server refuses to start otherwise unless `ENGRAM_DEMO_MODE=1`. |
165
+
166
+ ### PostgreSQL
167
+
168
+ `ENGRAM_PG_HOST`, `ENGRAM_PG_PORT`, `ENGRAM_PG_USER`, `ENGRAM_PG_DATABASE`, `ENGRAM_PG_POOL_TIMEOUT`.
169
+
170
+ ### Embeddings & retrieval
171
+
172
+ `ENGRAM_EMBEDDING_MODEL`, `ENGRAM_EMBEDDING_DIM`, `ENGRAM_RRF_VECTOR_WEIGHT`, `ENGRAM_RRF_KEYWORD_WEIGHT`, `ENGRAM_RRF_OVERFETCH`.
173
+
174
+ ### Decay
175
+
176
+ `ENGRAM_DECAY_STABILITY`, `ENGRAM_DECAY_GROWTH`.
177
+
178
+ ### Deduplication
179
+
180
+ `ENGRAM_DEDUP_ENABLED`, `ENGRAM_DEDUP_THRESHOLD`, `ENGRAM_SEMANTIC_DEDUP_ENABLED`, `ENGRAM_SEMANTIC_DEDUP_THRESHOLD`.
181
+
182
+ ### Reflection
183
+
184
+ `ENGRAM_REFLECTION_ENABLED`, `ENGRAM_REFLECTION_INTERVAL_HOURS`, `ENGRAM_REFLECTION_GC_THRESHOLD`, `ENGRAM_REFLECTION_GC_MIN_AGE_DAYS`, `ENGRAM_REFLECTION_CONSOLIDATION_SIM`, `ENGRAM_REFLECTION_MIN_CLUSTER`, `ENGRAM_REFLECTION_MAX_CONSOLIDATIONS`, `ENGRAM_REFLECTION_CONFLICT_SIM`, `ENGRAM_REFLECTION_CONFLICT_AGE_GAP`.
185
+
186
+ ### Backup
187
+
188
+ `ENGRAM_BACKUP_DIR`, `ENGRAM_BACKUP_KEEP_LAST`, `ENGRAM_BACKUP_KEEP_DAYS`.
189
+
190
+ ### Network / auth
191
+
192
+ `ENGRAM_ALLOWED_HOSTS`, `ENGRAM_CORS_ORIGINS`, `ENGRAM_DEMO_MODE`, `ENGRAM_RATE_LIMIT_RPM`, `ENGRAM_RATE_LIMIT_BURST`.
193
+
194
+ ### Logging
195
+
196
+ `LOG_LEVEL`, `LOG_FORMAT`.
197
+
198
+ ### Bulk import (path traversal guard)
199
+
200
+ `ENGRAM_IMPORT_ALLOWED_DIRS` — comma-separated list of base directories under which `/api/bulk/import` is allowed to read. Default `/app`.
201
+
202
+ ---
203
+
204
+ ## Known Limitations
205
+
206
+ - Embeddings are currently synchronous per-memory at write time (batched via a thread-pool executor). For very high-throughput workloads, consider pre-batching client-side.
207
+ - HNSW index quality depends on insert order; a periodic `REINDEX` can help after heavy ingestion. See `engram-manage re-embed` for the supported rebuild path.
208
+ - The activity ring buffer is per-process; in a multi-replica deployment, consume the text log instead.
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [Unreleased]
6
+
7
+ ### Added
8
+ - Apache 2.0 license, `NOTICE`, `CONTRIBUTING.md`, `SECURITY.md`, `ARCHITECTURE.md`.
9
+ - Startup guard: the server refuses to start if `ENGRAM_PG_PASSWORD` is unset or equal to the default `engram`, unless `ENGRAM_DEMO_MODE=1`.
10
+ - `.env.example` with documented minimum settings.
11
+ - OCI image labels on the Dockerfile.
12
+
13
+ ### Changed
14
+ - `benchmarks/*.py` defaults now point at `http://localhost:8000` instead of a hard-coded LAN address.
15
+ - README rewritten for a public audience; architectural deep-dive moved to `ARCHITECTURE.md`.
16
+ - CORS middleware now disables credentials when `allow_origins=["*"]` (matches browser spec).
17
+
18
+ ### Removed
19
+ - Internal-only `TECHNICAL_REVIEW.md` (content preserved in `ARCHITECTURE.md`).
20
+ - Private domain and hostnames from documentation.
21
+
22
+ ## [0.7.0] — 2026-04
23
+
24
+ Historical releases tracked via git history. Highlights:
25
+
26
+ - LongMemEval + LoCoMo benchmark harness, adaptive keyword weight for vague queries.
27
+ - Reciprocal-Rank Fusion hybrid search, preference boosting, entity-aware dedup.
28
+ - Persistent-project flag, pinning, versioning, SimHash dedup, FSRS-inspired decay.
29
+ - Reflection / compaction scheduler (GC, consolidation, conflict resolution).
30
+ - Dashboard (activity stream, graph viz, backup/restore), REST project claiming.
31
+ - Pagination, structured JSON logging, CI pipeline.
32
+ - API key cycling, hard-forget endpoint, 403 hints.
33
+ - Backup rotation, integration tests.
34
+ - Initial PostgreSQL + pgvector rewrite (replacing DuckDB + LanceDB + Kuzu).