You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(memory): pluggable semantic embeddings via go-vector v1.3.0 HTTPEmbedder
Replace the hardwired RandomProjections usage across all memory similarity
paths with a textEmbedder seam. The default stays RP (local, zero-cost,
unchanged behavior); a new memory.embedding config section routes episode
recall, episode dedup, the non-LLM ranker, and fact merge-on-write through
any OpenAI-compatible embeddings API (Ollama, llama.cpp, OpenAI, ...) for
real semantic similarity instead of bag-of-words overlap.
- internal/memory/embedder.go: EmbeddingConfig + textEmbedder interface;
rpTextEmbedder (corpus-fitted, bigram-featurized) and httpTextEmbedder
(stateless, raw text, text->vector cache, batch misses-only refits).
- episode_index.go: index owns a textEmbedder; persisted vectors carry an
embedding-space fingerprint (episodes_index_meta.json) so switching
provider/model/dims rebuilds automatically while legacy RP gobs keep
loading; failed rebuilds back off 30s so a down backend is never re-hit
on every loop turn.
- episodes.go: embedder factory on EpisodeStore; findDuplicate and the
embedder ranker fail safe (no dedup / recency order) on embed errors.
- merge.go: MergeDetector classifies over the configured embedder; embed
failures degrade to "nobody" (add without merge) instead of misclassifying.
- config: memory.embedding overlay in resolveMemory + NewMemoryManager;
${ENV_VAR} expansion for base_url/api_key; 10s default timeout.
- docs: CONFIG.md field reference + MEMORY.md mechanics/privacy notes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/CONFIG.md
+42-1Lines changed: 42 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -206,7 +206,15 @@ The `memory` section controls the persistent memory system (see [docs/MEMORY.md]
206
206
"auto_approve_episodes": false,
207
207
"episode_dedup_threshold": 0.92,
208
208
"max_episodes": 500,
209
-
"episode_ttl_days": 0
209
+
"episode_ttl_days": 0,
210
+
"embedding": {
211
+
"provider": "http",
212
+
"base_url": "http://localhost:11434/v1",
213
+
"model": "nomic-embed-text",
214
+
"api_key": "${OPENAI_API_KEY}",
215
+
"dims": 0,
216
+
"timeout_seconds": 10
217
+
}
210
218
}
211
219
}
212
220
```
@@ -231,6 +239,39 @@ The `memory` section controls the persistent memory system (see [docs/MEMORY.md]
231
239
|`episode_dedup_threshold`| 0.92 | Cosine similarity above which a newly written episode is treated as a near-duplicate of an existing one and **replaces** it (newest wins). An untrusted episode never replaces a trusted/approved one. `0` disables dedup. |
232
240
|`max_episodes`| 500 | Maximum number of stored episodes. On each write, episodes beyond this count are evicted oldest-first (both the summary file and the index entry). `0` disables the cap. |
233
241
|`episode_ttl_days`| 0 | Evict episodes older than this many days. `0` (default) disables TTL-based eviction. |
242
+
|`embedding`|*(unset)*| Semantic embedding backend for episode recall, episode dedup, the non-LLM episode ranker, and fact merge-on-write. Unset = local RandomProjections (lexical bag-of-words — fast, zero-cost, but no real semantics). See below. |
243
+
244
+
### `embedding` — real semantic embeddings (optional)
245
+
246
+
By default every similarity computation in memory uses go-vector
247
+
**RandomProjections**: a local, zero-dependency bag-of-words embedder. It is
248
+
fast but purely lexical — *"fixed the auth bug"* and *"repaired login issue"*
249
+
share no tokens and score ~0. Setting `embedding.provider` to `"http"` routes
250
+
all of those paths through any **OpenAI-compatible embeddings API** instead
|`provider`|`"rp"`|`"rp"` = local RandomProjections; `"http"` = OpenAI-compatible embeddings API. An `"http"` config missing `base_url` or `model` silently falls back to `"rp"` so memory keeps working. |
257
+
|`base_url`| — | API root, e.g. `http://localhost:11434/v1` (Ollama) or `https://api.openai.com/v1`. `${ENV_VAR}` expansion supported. |
258
+
|`model`| — | Embedding model name, e.g. `nomic-embed-text`, `text-embedding-3-small`. |
259
+
|`api_key`| — | Sent as `Authorization: Bearer <key>` when set. `${ENV_VAR}` expansion supported — keep secrets out of config files. |
260
+
|`dims`| 0 | Expected vector dimensionality; `0` infers it from the first response (recommended). |
Copy file name to clipboardExpand all lines: docs/MEMORY.md
+44-2Lines changed: 44 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -90,7 +90,7 @@ RP.embed(newEntry) → cos similarity vs each existing entry
90
90
91
91
This saves ~80% of LLM calls on memory writes.
92
92
93
-
**Implementation:**`internal/memory/merge.go` imports `github.com/BackendStack21/go-vector/pkg/vector` for `RandomProjections` and `Cosine`. The RP embedder is fit on existing facts when the detector is created, and re-fit whenever facts change.
93
+
**Implementation:**`internal/memory/merge.go` imports `github.com/BackendStack21/go-vector/pkg/vector` for `RandomProjections` and `Cosine`. The embedder is fit on existing facts when the detector is created, and re-fit whenever facts change. With `memory.embedding` configured (see *Pluggable Embeddings* below), the same classification runs over real semantic vectors from an OpenAI-compatible API instead of RP.
94
94
95
95
### Durability & Statefulness
96
96
@@ -207,11 +207,53 @@ The episode index (`episodes/index.json`) is cached in memory after the first re
207
207
208
208
### Search Ranking
209
209
210
-
Episode search uses **RandomProjections** (go-vector) for semantic similarity by default:
210
+
Episode search uses **RandomProjections** (go-vector) for similarity by default:
211
211
212
212
1. Fit RP embedder on episode summaries + query (64 dims, ~1ms)
213
213
2. Embed each summary and the query into 64-dimensional vectors
214
214
3. Score by cosine similarity between query vector and each summary vector
215
215
4. Return top-3 results sorted by score
216
216
217
217
This is zero LLM calls per search, ~1ms per search. Set `llm_search: true` in config to switch to LLM-based ranking (uses SimpleCall to rank episodes by relevance — higher quality, higher latency + token cost).
218
+
219
+
### Pluggable Embeddings (`memory.embedding`)
220
+
221
+
RandomProjections is lexical: two texts only score as similar when they share
222
+
vocabulary. *"fixed the auth bug"* vs *"repaired login issue"* → cosine ≈ 0,
223
+
so recall misses semantically related episodes. The `memory.embedding` config
224
+
section replaces RP with a real embedding model behind any OpenAI-compatible
225
+
API (Ollama, llama.cpp server, LM Studio, vLLM, OpenAI, …):
226
+
227
+
```json
228
+
{
229
+
"memory": {
230
+
"embedding": {
231
+
"provider": "http",
232
+
"base_url": "http://localhost:11434/v1",
233
+
"model": "nomic-embed-text"
234
+
}
235
+
}
236
+
}
237
+
```
238
+
239
+
One config switches **every** similarity path: per-turn episode recall, the
0 commit comments