memor-cli 0.2.0__tar.gz → 0.4.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.
- {memor_cli-0.2.0/memor_cli.egg-info → memor_cli-0.4.0}/PKG-INFO +43 -16
- {memor_cli-0.2.0 → memor_cli-0.4.0}/README.md +42 -15
- memor_cli-0.4.0/memor/__init__.py +1 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/daemon.py +24 -2
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/dashboard/server.py +5 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/dashboard/static/index.html +64 -5
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/distill/distiller.py +5 -2
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/distill/extractive.py +15 -5
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/eval/runner.py +3 -2
- memor_cli-0.4.0/memor/feedback.py +129 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/hook_server.py +51 -2
- memor_cli-0.4.0/memor/query_complexity.py +92 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/recall.py +35 -3
- memor_cli-0.4.0/memor/retrieve/retriever.py +122 -0
- memor_cli-0.4.0/memor/session_context.py +42 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/store/sqlite_store.py +156 -5
- memor_cli-0.4.0/memor/turn_metrics.py +90 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0/memor_cli.egg-info}/PKG-INFO +43 -16
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor_cli.egg-info/SOURCES.txt +9 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/pyproject.toml +1 -1
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_dashboard.py +3 -3
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_eval_ablation.py +5 -1
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_feedback.py +14 -0
- memor_cli-0.4.0/tests/test_hook_server.py +96 -0
- memor_cli-0.4.0/tests/test_hybrid_retrieval.py +213 -0
- memor_cli-0.4.0/tests/test_quality_gate.py +141 -0
- memor_cli-0.4.0/tests/test_query_complexity.py +82 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_recall_core.py +63 -3
- memor_cli-0.4.0/tests/test_semantic_feedback.py +103 -0
- memor_cli-0.4.0/tests/test_session_context.py +61 -0
- memor_cli-0.4.0/tests/test_turn_metrics.py +122 -0
- memor_cli-0.2.0/memor/__init__.py +0 -1
- memor_cli-0.2.0/memor/feedback.py +0 -96
- memor_cli-0.2.0/memor/retrieve/retriever.py +0 -78
- memor_cli-0.2.0/tests/test_hook_server.py +0 -42
- {memor_cli-0.2.0 → memor_cli-0.4.0}/LICENSE +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/cli.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/dashboard/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/distill/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/embed/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/embed/api.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/embed/fake.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/embed/local.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/eval/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/eval/baselines/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/eval/baselines/base.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/eval/baselines/claude_mem.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/eval/baselines/graphiti.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/eval/dataset.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/eval/embed_benchmark.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/eval/judge.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/eval/metrics.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/hook_cli.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/ingest/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/ingest/claude_code.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/ingest/documents.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/interfaces.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/llm/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/llm/anthropic.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/llm/base.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/llm/openai_compat.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/project.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/redact.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/retrieve/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/service.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/store/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/tokencount.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor/types.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor_cli.egg-info/dependency_links.txt +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor_cli.egg-info/entry_points.txt +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor_cli.egg-info/requires.txt +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/memor_cli.egg-info/top_level.txt +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/setup.cfg +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_cli_smoke.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_daemon.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_dataset_builder.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_dimension_safety.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_distiller.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_embed.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_embed_benchmark.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_eval_runner.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_external_baselines.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_extractive.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_hook.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_ingest_claude_code.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_ingest_documents.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_install_hook.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_interfaces.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_judge.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_metrics.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_noise_filter.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_project_resolver.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_redact.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_retriever.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_service.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_skill_recall.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_store.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_supersession.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_tokencount.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.4.0}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memor-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Measured memory for coding agents. Fire and forget — no API keys needed.
|
|
5
5
|
Author-email: Nimit Bhandari <nimitbhandari17@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -46,7 +46,7 @@ Dynamic: license-file
|
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
[](LICENSE)
|
|
49
|
-
[]()
|
|
50
50
|
[]()
|
|
51
51
|
[](https://pypi.org/project/memor-cli/)
|
|
52
52
|
|
|
@@ -65,7 +65,10 @@ pipx install memor-cli
|
|
|
65
65
|
# Install the Claude Code hook + download embedding model (~60MB)
|
|
66
66
|
memor install-hook
|
|
67
67
|
|
|
68
|
-
# Start
|
|
68
|
+
# Start as a background service (macOS/Linux)
|
|
69
|
+
memor service install
|
|
70
|
+
|
|
71
|
+
# Or run in the foreground
|
|
69
72
|
memor daemon
|
|
70
73
|
```
|
|
71
74
|
|
|
@@ -92,7 +95,13 @@ memor dashboard
|
|
|
92
95
|
Embed query locally (model2vec, ~2ms)
|
|
93
96
|
|
|
|
94
97
|
v
|
|
95
|
-
Hybrid
|
|
98
|
+
Hybrid retrieval: dense vectors + lexical BM25, fused (RRF)
|
|
99
|
+
|
|
|
100
|
+
v
|
|
101
|
+
Relevance gate drops off-topic matches (inject nothing if nothing fits)
|
|
102
|
+
|
|
|
103
|
+
v
|
|
104
|
+
Rank: similarity + recency + kind weight + quality
|
|
96
105
|
|
|
|
97
106
|
v
|
|
98
107
|
Inject relevant context into prompt
|
|
@@ -104,20 +113,31 @@ memor dashboard
|
|
|
104
113
|
|
|
105
114
|
**Two background processes:**
|
|
106
115
|
|
|
107
|
-
1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback, compacts duplicates. All local.
|
|
116
|
+
1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback, compacts duplicates, tracks session-level token usage. All local.
|
|
108
117
|
2. **Hook** — fires on every prompt, recalls relevant memories, injects them as context. Sub-15ms.
|
|
109
118
|
|
|
110
119
|
**No API keys required.** Embeddings run locally via [model2vec](https://github.com/MinishLab/model2vec) (potion-base-8M, 256-dim). Vectors stored in [sqlite-vec](https://github.com/asg017/sqlite-vec). Everything runs on your machine.
|
|
111
120
|
|
|
112
121
|
---
|
|
113
122
|
|
|
114
|
-
## Hybrid
|
|
123
|
+
## Hybrid Retrieval
|
|
124
|
+
|
|
125
|
+
Memor retrieves over two channels and fuses them, so it catches both semantic matches and exact terms:
|
|
126
|
+
|
|
127
|
+
- **Dense** — local vector similarity (model2vec) for semantic recall.
|
|
128
|
+
- **Lexical** — SQLite FTS5 / BM25 over the raw text, to recover exact identifiers, error strings, and API names that static embeddings blur together.
|
|
129
|
+
|
|
130
|
+
The two rankings are combined with **Reciprocal Rank Fusion (RRF)**. A **relevance gate** drops anti-correlated (off-topic) candidates *before* ranking, so an unrelated prompt injects nothing rather than the least-bad guess. The lexical channel only activates when the dense channel finds the query on-topic, preventing generic words from pulling in noise.
|
|
131
|
+
|
|
132
|
+
> Tunable via `MEMOR_MIN_SIMILARITY` (relevance floor, default 0.0) and `MEMOR_MAX_TOKENS` (injection budget, default 1500).
|
|
133
|
+
|
|
134
|
+
## Scoring
|
|
115
135
|
|
|
116
|
-
|
|
136
|
+
Surviving candidates are ranked by four signals:
|
|
117
137
|
|
|
118
138
|
| Signal | Weight | How it works |
|
|
119
139
|
|---|---|---|
|
|
120
|
-
| **Semantic similarity** | 50% |
|
|
140
|
+
| **Semantic similarity** | 50% | Dense + lexical relevance, fused via RRF |
|
|
121
141
|
| **Recency** | 25% | Exponential decay with 14-day half-life — recent decisions rank higher |
|
|
122
142
|
| **Kind weight** | 15% | Distilled memories (1.3x) rank above raw session chunks (1.0x) |
|
|
123
143
|
| **Quality** | 10% | Bayesian score from implicit feedback — memories the agent actually uses rank higher |
|
|
@@ -147,10 +167,11 @@ Memories are automatically classified as `decision`, `bugfix`, `lesson`, `snippe
|
|
|
147
167
|
memor dashboard
|
|
148
168
|
```
|
|
149
169
|
|
|
150
|
-
|
|
151
|
-
- **
|
|
152
|
-
- **
|
|
153
|
-
- **
|
|
170
|
+
Dark fintech-inspired UI showing:
|
|
171
|
+
- **Hero metrics** — total memories, recall count, avg latency, coverage — with sparkline bars
|
|
172
|
+
- **Daily recall activity** — stacked bar chart of hits vs misses over time
|
|
173
|
+
- **Session efficiency** — real token savings measured from API usage data (avg tokens/turn with vs without recall)
|
|
174
|
+
- **Per-project breakdown** — artifact counts, token totals, last activity
|
|
154
175
|
- **Recent recalls** — every hook event with scores, latency, and status
|
|
155
176
|
|
|
156
177
|
---
|
|
@@ -162,6 +183,11 @@ memor help Print the full manual
|
|
|
162
183
|
memor install-hook Install Claude Code hook + download model
|
|
163
184
|
memor daemon Auto-ingest + distill (background watcher)
|
|
164
185
|
memor dashboard Web dashboard on localhost:8420
|
|
186
|
+
memor version Print installed version
|
|
187
|
+
memor service install Run daemon as background service (launchd/systemd)
|
|
188
|
+
memor service stop Stop the background service
|
|
189
|
+
memor service uninstall Remove the background service
|
|
190
|
+
memor service status Check if the service is running
|
|
165
191
|
memor query <text> Search memories from the CLI
|
|
166
192
|
memor reingest Wipe DB and re-ingest everything
|
|
167
193
|
memor reingest --project <name> Re-ingest only one project
|
|
@@ -193,16 +219,17 @@ memor/
|
|
|
193
219
|
+-- feedback.py Implicit feedback analyzer (usage detection)
|
|
194
220
|
|
|
|
195
221
|
+-- retrieve/
|
|
196
|
-
| +-- retriever.py Hybrid
|
|
222
|
+
| +-- retriever.py Hybrid retrieval (dense + BM25, RRF) + relevance gate + scoring
|
|
197
223
|
|
|
|
198
224
|
+-- store/
|
|
199
|
-
| +-- sqlite_store.py SQLite + sqlite-vec (WAL mode, dimension safety)
|
|
225
|
+
| +-- sqlite_store.py SQLite + sqlite-vec + FTS5 (WAL mode, dimension safety)
|
|
200
226
|
|
|
|
201
227
|
+-- embed/
|
|
202
228
|
| +-- local.py model2vec (potion-base-8M, 256-dim, ~60MB)
|
|
203
229
|
| +-- api.py OpenAI-compatible embedding API (optional)
|
|
204
230
|
| +-- fake.py Deterministic SHA-256 embedder (tests)
|
|
205
231
|
|
|
|
232
|
+
+-- service.py Background service management (launchd/systemd)
|
|
206
233
|
+-- dashboard/
|
|
207
234
|
| +-- server.py FastAPI dashboard backend
|
|
208
235
|
| +-- static/index.html Self-contained dashboard (no CDN deps)
|
|
@@ -227,7 +254,7 @@ skill/recall.py Standalone recall script
|
|
|
227
254
|
**Nothing leaves your machine.** In the default configuration:
|
|
228
255
|
|
|
229
256
|
- **No telemetry, no analytics, no phone-home.** Zero outbound network calls.
|
|
230
|
-
- **Embeddings run locally** via model2vec
|
|
257
|
+
- **Embeddings run locally** via model2vec static token embeddings — no inference runtime, no GPU (one-time model download from HuggingFace — no user data sent).
|
|
231
258
|
- **Hook transport is a Unix socket** (`~/.memor/hook.sock`), not a network port.
|
|
232
259
|
- **Dashboard binds localhost only.**
|
|
233
260
|
|
|
@@ -263,7 +290,7 @@ cd memor-ai
|
|
|
263
290
|
python3 -m venv .venv && source .venv/bin/activate
|
|
264
291
|
pip install -e ".[dev]"
|
|
265
292
|
|
|
266
|
-
pytest #
|
|
293
|
+
pytest # 215 tests
|
|
267
294
|
```
|
|
268
295
|
|
|
269
296
|
---
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[]()
|
|
13
13
|
[]()
|
|
14
14
|
[](https://pypi.org/project/memor-cli/)
|
|
15
15
|
|
|
@@ -28,7 +28,10 @@ pipx install memor-cli
|
|
|
28
28
|
# Install the Claude Code hook + download embedding model (~60MB)
|
|
29
29
|
memor install-hook
|
|
30
30
|
|
|
31
|
-
# Start
|
|
31
|
+
# Start as a background service (macOS/Linux)
|
|
32
|
+
memor service install
|
|
33
|
+
|
|
34
|
+
# Or run in the foreground
|
|
32
35
|
memor daemon
|
|
33
36
|
```
|
|
34
37
|
|
|
@@ -55,7 +58,13 @@ memor dashboard
|
|
|
55
58
|
Embed query locally (model2vec, ~2ms)
|
|
56
59
|
|
|
|
57
60
|
v
|
|
58
|
-
Hybrid
|
|
61
|
+
Hybrid retrieval: dense vectors + lexical BM25, fused (RRF)
|
|
62
|
+
|
|
|
63
|
+
v
|
|
64
|
+
Relevance gate drops off-topic matches (inject nothing if nothing fits)
|
|
65
|
+
|
|
|
66
|
+
v
|
|
67
|
+
Rank: similarity + recency + kind weight + quality
|
|
59
68
|
|
|
|
60
69
|
v
|
|
61
70
|
Inject relevant context into prompt
|
|
@@ -67,20 +76,31 @@ memor dashboard
|
|
|
67
76
|
|
|
68
77
|
**Two background processes:**
|
|
69
78
|
|
|
70
|
-
1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback, compacts duplicates. All local.
|
|
79
|
+
1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback, compacts duplicates, tracks session-level token usage. All local.
|
|
71
80
|
2. **Hook** — fires on every prompt, recalls relevant memories, injects them as context. Sub-15ms.
|
|
72
81
|
|
|
73
82
|
**No API keys required.** Embeddings run locally via [model2vec](https://github.com/MinishLab/model2vec) (potion-base-8M, 256-dim). Vectors stored in [sqlite-vec](https://github.com/asg017/sqlite-vec). Everything runs on your machine.
|
|
74
83
|
|
|
75
84
|
---
|
|
76
85
|
|
|
77
|
-
## Hybrid
|
|
86
|
+
## Hybrid Retrieval
|
|
87
|
+
|
|
88
|
+
Memor retrieves over two channels and fuses them, so it catches both semantic matches and exact terms:
|
|
89
|
+
|
|
90
|
+
- **Dense** — local vector similarity (model2vec) for semantic recall.
|
|
91
|
+
- **Lexical** — SQLite FTS5 / BM25 over the raw text, to recover exact identifiers, error strings, and API names that static embeddings blur together.
|
|
92
|
+
|
|
93
|
+
The two rankings are combined with **Reciprocal Rank Fusion (RRF)**. A **relevance gate** drops anti-correlated (off-topic) candidates *before* ranking, so an unrelated prompt injects nothing rather than the least-bad guess. The lexical channel only activates when the dense channel finds the query on-topic, preventing generic words from pulling in noise.
|
|
94
|
+
|
|
95
|
+
> Tunable via `MEMOR_MIN_SIMILARITY` (relevance floor, default 0.0) and `MEMOR_MAX_TOKENS` (injection budget, default 1500).
|
|
96
|
+
|
|
97
|
+
## Scoring
|
|
78
98
|
|
|
79
|
-
|
|
99
|
+
Surviving candidates are ranked by four signals:
|
|
80
100
|
|
|
81
101
|
| Signal | Weight | How it works |
|
|
82
102
|
|---|---|---|
|
|
83
|
-
| **Semantic similarity** | 50% |
|
|
103
|
+
| **Semantic similarity** | 50% | Dense + lexical relevance, fused via RRF |
|
|
84
104
|
| **Recency** | 25% | Exponential decay with 14-day half-life — recent decisions rank higher |
|
|
85
105
|
| **Kind weight** | 15% | Distilled memories (1.3x) rank above raw session chunks (1.0x) |
|
|
86
106
|
| **Quality** | 10% | Bayesian score from implicit feedback — memories the agent actually uses rank higher |
|
|
@@ -110,10 +130,11 @@ Memories are automatically classified as `decision`, `bugfix`, `lesson`, `snippe
|
|
|
110
130
|
memor dashboard
|
|
111
131
|
```
|
|
112
132
|
|
|
113
|
-
|
|
114
|
-
- **
|
|
115
|
-
- **
|
|
116
|
-
- **
|
|
133
|
+
Dark fintech-inspired UI showing:
|
|
134
|
+
- **Hero metrics** — total memories, recall count, avg latency, coverage — with sparkline bars
|
|
135
|
+
- **Daily recall activity** — stacked bar chart of hits vs misses over time
|
|
136
|
+
- **Session efficiency** — real token savings measured from API usage data (avg tokens/turn with vs without recall)
|
|
137
|
+
- **Per-project breakdown** — artifact counts, token totals, last activity
|
|
117
138
|
- **Recent recalls** — every hook event with scores, latency, and status
|
|
118
139
|
|
|
119
140
|
---
|
|
@@ -125,6 +146,11 @@ memor help Print the full manual
|
|
|
125
146
|
memor install-hook Install Claude Code hook + download model
|
|
126
147
|
memor daemon Auto-ingest + distill (background watcher)
|
|
127
148
|
memor dashboard Web dashboard on localhost:8420
|
|
149
|
+
memor version Print installed version
|
|
150
|
+
memor service install Run daemon as background service (launchd/systemd)
|
|
151
|
+
memor service stop Stop the background service
|
|
152
|
+
memor service uninstall Remove the background service
|
|
153
|
+
memor service status Check if the service is running
|
|
128
154
|
memor query <text> Search memories from the CLI
|
|
129
155
|
memor reingest Wipe DB and re-ingest everything
|
|
130
156
|
memor reingest --project <name> Re-ingest only one project
|
|
@@ -156,16 +182,17 @@ memor/
|
|
|
156
182
|
+-- feedback.py Implicit feedback analyzer (usage detection)
|
|
157
183
|
|
|
|
158
184
|
+-- retrieve/
|
|
159
|
-
| +-- retriever.py Hybrid
|
|
185
|
+
| +-- retriever.py Hybrid retrieval (dense + BM25, RRF) + relevance gate + scoring
|
|
160
186
|
|
|
|
161
187
|
+-- store/
|
|
162
|
-
| +-- sqlite_store.py SQLite + sqlite-vec (WAL mode, dimension safety)
|
|
188
|
+
| +-- sqlite_store.py SQLite + sqlite-vec + FTS5 (WAL mode, dimension safety)
|
|
163
189
|
|
|
|
164
190
|
+-- embed/
|
|
165
191
|
| +-- local.py model2vec (potion-base-8M, 256-dim, ~60MB)
|
|
166
192
|
| +-- api.py OpenAI-compatible embedding API (optional)
|
|
167
193
|
| +-- fake.py Deterministic SHA-256 embedder (tests)
|
|
168
194
|
|
|
|
195
|
+
+-- service.py Background service management (launchd/systemd)
|
|
169
196
|
+-- dashboard/
|
|
170
197
|
| +-- server.py FastAPI dashboard backend
|
|
171
198
|
| +-- static/index.html Self-contained dashboard (no CDN deps)
|
|
@@ -190,7 +217,7 @@ skill/recall.py Standalone recall script
|
|
|
190
217
|
**Nothing leaves your machine.** In the default configuration:
|
|
191
218
|
|
|
192
219
|
- **No telemetry, no analytics, no phone-home.** Zero outbound network calls.
|
|
193
|
-
- **Embeddings run locally** via model2vec
|
|
220
|
+
- **Embeddings run locally** via model2vec static token embeddings — no inference runtime, no GPU (one-time model download from HuggingFace — no user data sent).
|
|
194
221
|
- **Hook transport is a Unix socket** (`~/.memor/hook.sock`), not a network port.
|
|
195
222
|
- **Dashboard binds localhost only.**
|
|
196
223
|
|
|
@@ -226,7 +253,7 @@ cd memor-ai
|
|
|
226
253
|
python3 -m venv .venv && source .venv/bin/activate
|
|
227
254
|
pip install -e ".[dev]"
|
|
228
255
|
|
|
229
|
-
pytest #
|
|
256
|
+
pytest # 215 tests
|
|
230
257
|
```
|
|
231
258
|
|
|
232
259
|
---
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -146,7 +146,7 @@ def distill_new_sessions(
|
|
|
146
146
|
return distilled
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
COMPACT_SIM_THRESHOLD = 0.
|
|
149
|
+
COMPACT_SIM_THRESHOLD = 0.85
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
def compact_memories(store: SqliteStore, embedder) -> int:
|
|
@@ -239,12 +239,34 @@ def run_poll_cycle(
|
|
|
239
239
|
for path, project in pending:
|
|
240
240
|
session_id = path.stem
|
|
241
241
|
try:
|
|
242
|
-
used = analyze_session_feedback(store, session_id, path)
|
|
242
|
+
used = analyze_session_feedback(store, session_id, path, embedder=embedder)
|
|
243
243
|
if used > 0:
|
|
244
244
|
print(f" feedback: {used} memories confirmed used in {session_id[:12]}...")
|
|
245
245
|
except Exception:
|
|
246
246
|
pass
|
|
247
247
|
|
|
248
|
+
# Turn-level metrics: parse tool calls per turn, correlate with recalls
|
|
249
|
+
if new_ingested:
|
|
250
|
+
from memor.turn_metrics import parse_turn_metrics, correlate_with_recalls
|
|
251
|
+
for path, project in pending:
|
|
252
|
+
session_id = path.stem
|
|
253
|
+
try:
|
|
254
|
+
metrics = parse_turn_metrics(path, session_id)
|
|
255
|
+
if metrics:
|
|
256
|
+
metrics = correlate_with_recalls(metrics, store, session_id)
|
|
257
|
+
store.save_turn_metrics(session_id, project, metrics)
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
# Soft quality decay: unused memories lose quality over time
|
|
262
|
+
if new_ingested:
|
|
263
|
+
try:
|
|
264
|
+
decayed = store.decay_quality(stale_days=14, factor=0.5, deactivate_floor=0.03)
|
|
265
|
+
if decayed > 0:
|
|
266
|
+
print(f" decayed quality for {decayed} stale memories")
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
248
270
|
# Compact near-duplicate memories (run occasionally, not every cycle)
|
|
249
271
|
if new_ingested:
|
|
250
272
|
try:
|
|
@@ -134,6 +134,11 @@ def create_app(db_path: str | None = None) -> FastAPI:
|
|
|
134
134
|
""", (f"-{days}",)).fetchall()
|
|
135
135
|
return [dict(r) for r in rows]
|
|
136
136
|
|
|
137
|
+
@app.get("/api/roi")
|
|
138
|
+
def roi(project: str | None = Query(None)):
|
|
139
|
+
store = _store()
|
|
140
|
+
return store.get_token_roi(project=project)
|
|
141
|
+
|
|
137
142
|
@app.get("/api/health")
|
|
138
143
|
def health():
|
|
139
144
|
store = _store()
|
|
@@ -410,9 +410,9 @@
|
|
|
410
410
|
<div class="side-stat-sub" id="e-avg-input-sub">across all sessions</div>
|
|
411
411
|
</div>
|
|
412
412
|
<div class="side-stat">
|
|
413
|
-
<div class="side-stat-label">Recall
|
|
414
|
-
<div class="side-stat-value" id="e-
|
|
415
|
-
<div class="side-stat-sub">% of prompts
|
|
413
|
+
<div class="side-stat-label">Recall Coverage</div>
|
|
414
|
+
<div class="side-stat-value" id="e-coverage">–</div>
|
|
415
|
+
<div class="side-stat-sub">% of prompts where context was injected</div>
|
|
416
416
|
</div>
|
|
417
417
|
<div class="side-stat">
|
|
418
418
|
<div class="side-stat-label">Avg Latency</div>
|
|
@@ -426,6 +426,32 @@
|
|
|
426
426
|
</div>
|
|
427
427
|
</div>
|
|
428
428
|
</div>
|
|
429
|
+
<div class="chart-card">
|
|
430
|
+
<div class="chart-header">
|
|
431
|
+
<div class="chart-title">Token ROI</div>
|
|
432
|
+
</div>
|
|
433
|
+
<div id="roi-banner" style="display:none;background:var(--ok-dim);border:1px solid rgba(61,214,140,0.2);border-radius:var(--radius-sm);padding:12px 14px;margin-bottom:14px;">
|
|
434
|
+
<div style="font-size:22px;font-weight:700;color:var(--ok);letter-spacing:-0.5px;" id="roi-value">–</div>
|
|
435
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:2px;" id="roi-desc">fewer tool calls when Memor injects context</div>
|
|
436
|
+
</div>
|
|
437
|
+
<div class="side-stats" id="roi-side">
|
|
438
|
+
<div class="side-stat">
|
|
439
|
+
<div class="side-stat-label">Avg Tools / Turn (with recall)</div>
|
|
440
|
+
<div class="side-stat-value" id="roi-tools-with">–</div>
|
|
441
|
+
<div class="side-stat-sub">when Memor injected context</div>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="side-stat">
|
|
444
|
+
<div class="side-stat-label">Avg Tools / Turn (without)</div>
|
|
445
|
+
<div class="side-stat-value" id="roi-tools-without">–</div>
|
|
446
|
+
<div class="side-stat-sub">when no context was injected</div>
|
|
447
|
+
</div>
|
|
448
|
+
<div class="side-stat">
|
|
449
|
+
<div class="side-stat-label">Turns Measured</div>
|
|
450
|
+
<div class="side-stat-value" id="roi-turns">–</div>
|
|
451
|
+
<div class="side-stat-sub" id="roi-turns-sub">with vs. without recall</div>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
429
455
|
</div>
|
|
430
456
|
</section>
|
|
431
457
|
|
|
@@ -581,7 +607,7 @@
|
|
|
581
607
|
|
|
582
608
|
/* ── Efficiency ────────────────────────────────────────── */
|
|
583
609
|
function renderEfficiency(data) {
|
|
584
|
-
document.getElementById('e-
|
|
610
|
+
document.getElementById('e-coverage').textContent = fmtPct(data.coverage * 100);
|
|
585
611
|
}
|
|
586
612
|
|
|
587
613
|
function renderSessionEfficiency(data) {
|
|
@@ -781,6 +807,38 @@
|
|
|
781
807
|
msg.innerHTML = 'Status: <strong>' + esc(status) + '</strong>. Run <code>memor help</code> for guidance.';
|
|
782
808
|
}
|
|
783
809
|
|
|
810
|
+
/* ── ROI renderer ─────────────────────────────────────── */
|
|
811
|
+
function renderROI(data) {
|
|
812
|
+
document.getElementById('roi-tools-with').textContent = data.avg_tools_with_recall;
|
|
813
|
+
document.getElementById('roi-tools-without').textContent = data.avg_tools_without_recall;
|
|
814
|
+
document.getElementById('roi-turns').textContent =
|
|
815
|
+
(data.turns_with_recall + data.turns_without_recall).toLocaleString();
|
|
816
|
+
document.getElementById('roi-turns-sub').textContent =
|
|
817
|
+
data.turns_with_recall + ' with recall · ' + data.turns_without_recall + ' without';
|
|
818
|
+
|
|
819
|
+
var banner = document.getElementById('roi-banner');
|
|
820
|
+
var roiValue = document.getElementById('roi-value');
|
|
821
|
+
var roiDesc = document.getElementById('roi-desc');
|
|
822
|
+
banner.style.display = 'none';
|
|
823
|
+
banner.style.background = 'var(--ok-dim)';
|
|
824
|
+
banner.style.borderColor = 'rgba(61,214,140,0.2)';
|
|
825
|
+
roiValue.style.color = 'var(--ok)';
|
|
826
|
+
roiValue.textContent = '–';
|
|
827
|
+
roiDesc.textContent = 'fewer tool calls when Memor injects context';
|
|
828
|
+
|
|
829
|
+
if (data.tool_call_reduction_pct > 0 && data.turns_with_recall >= 5 && data.turns_without_recall >= 5) {
|
|
830
|
+
roiValue.textContent = data.tool_call_reduction_pct + '% fewer';
|
|
831
|
+
banner.style.display = 'block';
|
|
832
|
+
} else if (data.tool_call_reduction_pct < 0 && data.turns_with_recall >= 5) {
|
|
833
|
+
roiValue.textContent = Math.abs(data.tool_call_reduction_pct) + '% more';
|
|
834
|
+
roiValue.style.color = 'var(--warn)';
|
|
835
|
+
roiDesc.textContent = 'tool calls with recall — investigating...';
|
|
836
|
+
banner.style.display = 'block';
|
|
837
|
+
banner.style.background = 'var(--warn-dim)';
|
|
838
|
+
banner.style.borderColor = 'rgba(232,147,32,0.2)';
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
784
842
|
/* ── Data loaders ──────────────────────────────────────── */
|
|
785
843
|
async function loadSummary() { try { renderSummary(await api('/api/summary')); } catch(e) { console.warn('summary',e); } }
|
|
786
844
|
async function loadProjects() { try { renderProjects(await api('/api/projects')); } catch(e) { console.warn('projects',e); } }
|
|
@@ -788,6 +846,7 @@
|
|
|
788
846
|
async function loadSessionEfficiency() { try { renderSessionEfficiency(await api('/api/session-efficiency')); } catch(e) { console.warn('session-efficiency',e); } }
|
|
789
847
|
async function loadHealth() { try { renderHealth(await api('/api/health')); } catch(e) { console.warn('health',e); } }
|
|
790
848
|
async function loadTrend() { try { renderTrend(await api('/api/recall-trend?days=30')); } catch(e) { console.warn('trend',e); } }
|
|
849
|
+
async function loadROI() { try { renderROI(await api('/api/roi')); } catch(e) { console.warn('roi',e); } }
|
|
791
850
|
async function loadRecalls() {
|
|
792
851
|
try {
|
|
793
852
|
var url = '/api/recalls?limit=50' + (projectFilter ? '&project=' + encodeURIComponent(projectFilter) : '');
|
|
@@ -796,7 +855,7 @@
|
|
|
796
855
|
}
|
|
797
856
|
|
|
798
857
|
async function refresh() {
|
|
799
|
-
await Promise.allSettled([loadSummary(), loadProjects(), loadEfficiency(), loadSessionEfficiency(), loadHealth(), loadTrend(), loadRecalls()]);
|
|
858
|
+
await Promise.allSettled([loadSummary(), loadProjects(), loadEfficiency(), loadSessionEfficiency(), loadHealth(), loadTrend(), loadRecalls(), loadROI()]);
|
|
800
859
|
document.getElementById('last-updated').textContent = new Date().toLocaleTimeString();
|
|
801
860
|
|
|
802
861
|
renderMiniBars('mb-chunks', null);
|
|
@@ -99,11 +99,14 @@ class ExtractiveDistiller:
|
|
|
99
99
|
def distill_session(
|
|
100
100
|
self, session_id: str, chunks: list[Artifact], project: str
|
|
101
101
|
) -> list[str]:
|
|
102
|
-
from memor.distill.extractive import classify_chunk
|
|
102
|
+
from memor.distill.extractive import classify_chunk, score_chunks, MIN_MEMORY_SIGNAL
|
|
103
103
|
key_chunks = extract_key_chunks(chunks, self.embedder)
|
|
104
|
+
scores = score_chunks(key_chunks)
|
|
104
105
|
created = max((c.created_at for c in chunks), default=0.0)
|
|
105
106
|
new_ids: list[str] = []
|
|
106
|
-
for c in key_chunks:
|
|
107
|
+
for c, score in zip(key_chunks, scores):
|
|
108
|
+
if score < MIN_MEMORY_SIGNAL:
|
|
109
|
+
continue
|
|
107
110
|
mem_type = classify_chunk(c.text)
|
|
108
111
|
mid = _store_memory(self.store, self.embedder, c.text, mem_type,
|
|
109
112
|
session_id, project, created, [c])
|
|
@@ -132,13 +132,13 @@ def _cluster_select(chunks: list[Artifact], embedder, max_clusters: int) -> list
|
|
|
132
132
|
return selected
|
|
133
133
|
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
MIN_MEMORY_SIGNAL = 0.3
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def score_chunks(chunks: list[Artifact]) -> list[float]:
|
|
139
|
+
"""Score each chunk by TF-IDF + heuristic. Returns parallel list of scores."""
|
|
139
140
|
if not chunks:
|
|
140
141
|
return []
|
|
141
|
-
# Score each chunk: TF-IDF + heuristic
|
|
142
142
|
tfidf = _tfidf_scores(chunks)
|
|
143
143
|
max_tf = max(tfidf) or 1.0
|
|
144
144
|
combined = []
|
|
@@ -148,6 +148,16 @@ def extract_key_chunks(
|
|
|
148
148
|
combined.append(-999.0)
|
|
149
149
|
continue
|
|
150
150
|
combined.append((tfidf[i] / max_tf) + h)
|
|
151
|
+
return combined
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def extract_key_chunks(
|
|
155
|
+
chunks: list[Artifact], embedder, *, max_extracts: int = MAX_EXTRACTS
|
|
156
|
+
) -> list[Artifact]:
|
|
157
|
+
"""Select the highest-signal chunks from a session. Pure local, no LLM."""
|
|
158
|
+
if not chunks:
|
|
159
|
+
return []
|
|
160
|
+
combined = score_chunks(chunks)
|
|
151
161
|
# Pre-filter: drop anything scored below 0
|
|
152
162
|
viable_idx = [i for i, s in enumerate(combined) if s > 0]
|
|
153
163
|
if not viable_idx:
|
|
@@ -72,7 +72,8 @@ def run_ablation(*, query, project, relevant_ids, store, embedder, k=8):
|
|
|
72
72
|
"ndcg@k": ndcg_at_k(ids, relevant_ids, k)}
|
|
73
73
|
return out
|
|
74
74
|
|
|
75
|
-
def run_contradiction_eval(*, query, project, stale_id, current_id, store, embedder,
|
|
76
|
-
|
|
75
|
+
def run_contradiction_eval(*, query, project, stale_id, current_id, store, embedder,
|
|
76
|
+
k=8, min_similarity=0.0):
|
|
77
|
+
r = Retriever(store, embedder, k=k, edge_expand=True, min_similarity=min_similarity)
|
|
77
78
|
ids = [h.artifact.id for h in r.query(query, Scope(project=project)).hits]
|
|
78
79
|
return (current_id in ids) and (stale_id not in ids)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Feedback analyzer — detects whether recalled memories were used by the agent.
|
|
2
|
+
|
|
3
|
+
After a session ends, cross-references recall_log with the transcript to see
|
|
4
|
+
if the agent's responses referenced recalled content. Updates memory_quality
|
|
5
|
+
scores accordingly.
|
|
6
|
+
|
|
7
|
+
Two matching strategies:
|
|
8
|
+
1. N-gram overlap (fast, catches verbatim reuse)
|
|
9
|
+
2. Semantic similarity via embeddings (catches paraphrased reuse)
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from memor.store.sqlite_store import SqliteStore
|
|
16
|
+
|
|
17
|
+
_NGRAM_SIZE = 3
|
|
18
|
+
_MIN_WORDS = 4
|
|
19
|
+
_MATCH_RATIO = 0.10
|
|
20
|
+
_SEMANTIC_SIM_THRESHOLD = 0.45
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _extract_assistant_texts(transcript_path: Path) -> list[str]:
|
|
24
|
+
texts = []
|
|
25
|
+
for line in transcript_path.read_text().splitlines():
|
|
26
|
+
line = line.strip()
|
|
27
|
+
if not line:
|
|
28
|
+
continue
|
|
29
|
+
try:
|
|
30
|
+
rec = json.loads(line)
|
|
31
|
+
except json.JSONDecodeError:
|
|
32
|
+
continue
|
|
33
|
+
if rec.get("type") != "assistant":
|
|
34
|
+
continue
|
|
35
|
+
msg = rec.get("message", {})
|
|
36
|
+
content = msg.get("content", "")
|
|
37
|
+
if isinstance(content, str):
|
|
38
|
+
texts.append(content.lower())
|
|
39
|
+
elif isinstance(content, list):
|
|
40
|
+
for block in content:
|
|
41
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
42
|
+
texts.append(block.get("text", "").lower())
|
|
43
|
+
return texts
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _text_was_used(memory_text: str, assistant_texts: list[str]) -> bool:
|
|
47
|
+
words = memory_text.lower().split()
|
|
48
|
+
if len(words) < _MIN_WORDS:
|
|
49
|
+
return False
|
|
50
|
+
ngrams = []
|
|
51
|
+
for i in range(len(words) - _NGRAM_SIZE + 1):
|
|
52
|
+
ngrams.append(" ".join(words[i:i + _NGRAM_SIZE]))
|
|
53
|
+
if not ngrams:
|
|
54
|
+
return False
|
|
55
|
+
matches = 0
|
|
56
|
+
for phrase in ngrams:
|
|
57
|
+
for text in assistant_texts:
|
|
58
|
+
if phrase in text:
|
|
59
|
+
matches += 1
|
|
60
|
+
break
|
|
61
|
+
return matches >= max(1, math.ceil(len(ngrams) * _MATCH_RATIO))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cosine(a: list[float], b: list[float]) -> float:
|
|
65
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
66
|
+
na = math.sqrt(sum(x * x for x in a))
|
|
67
|
+
nb = math.sqrt(sum(x * x for x in b))
|
|
68
|
+
return dot / (na * nb) if na and nb else 0.0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _semantic_match(memory_text: str, response_text: str, embedder) -> bool:
|
|
72
|
+
"""Check if memory content appears in the response via embedding similarity.
|
|
73
|
+
Catches paraphrased reuse that n-gram matching misses."""
|
|
74
|
+
if len(memory_text.split()) < _MIN_WORDS:
|
|
75
|
+
return False
|
|
76
|
+
vecs = embedder.embed([memory_text, response_text])
|
|
77
|
+
return _cosine(vecs[0], vecs[1]) >= _SEMANTIC_SIM_THRESHOLD
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def analyze_session_feedback(
|
|
81
|
+
store: SqliteStore, session_id: str, transcript_path: Path,
|
|
82
|
+
*, embedder=None,
|
|
83
|
+
) -> int:
|
|
84
|
+
recalled_ids = set()
|
|
85
|
+
rows = store.db.execute("""
|
|
86
|
+
SELECT q.artifact_id FROM memory_quality q
|
|
87
|
+
JOIN artifacts a ON a.id = q.artifact_id
|
|
88
|
+
WHERE a.active = 1
|
|
89
|
+
AND a.project = (
|
|
90
|
+
SELECT project FROM recall_log
|
|
91
|
+
WHERE session_id = ? AND hits_count > 0
|
|
92
|
+
LIMIT 1
|
|
93
|
+
)
|
|
94
|
+
AND q.last_recalled >= (
|
|
95
|
+
SELECT MIN(timestamp) FROM recall_log
|
|
96
|
+
WHERE session_id = ? AND hits_count > 0
|
|
97
|
+
)
|
|
98
|
+
AND q.last_recalled <= (
|
|
99
|
+
SELECT MAX(timestamp) FROM recall_log
|
|
100
|
+
WHERE session_id = ? AND hits_count > 0
|
|
101
|
+
) + 5
|
|
102
|
+
""", (session_id, session_id, session_id)).fetchall()
|
|
103
|
+
for row in rows:
|
|
104
|
+
recalled_ids.add(row["artifact_id"])
|
|
105
|
+
|
|
106
|
+
if not recalled_ids:
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
assistant_texts = _extract_assistant_texts(transcript_path)
|
|
110
|
+
if not assistant_texts:
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
used_ids = []
|
|
114
|
+
combined_response = " ".join(assistant_texts) if embedder else ""
|
|
115
|
+
for aid in recalled_ids:
|
|
116
|
+
art = store.db.execute(
|
|
117
|
+
"SELECT text FROM artifacts WHERE id=?", (aid,)
|
|
118
|
+
).fetchone()
|
|
119
|
+
if not art:
|
|
120
|
+
continue
|
|
121
|
+
if _text_was_used(art["text"], assistant_texts):
|
|
122
|
+
used_ids.append(aid)
|
|
123
|
+
elif embedder and _semantic_match(art["text"], combined_response, embedder):
|
|
124
|
+
used_ids.append(aid)
|
|
125
|
+
|
|
126
|
+
if used_ids:
|
|
127
|
+
store.record_usage(used_ids)
|
|
128
|
+
|
|
129
|
+
return len(used_ids)
|