Granis87 commited on
Commit
7c8b011
·
verified ·
1 Parent(s): c3a3710

Upload folder using huggingface_hub

Browse files
REVIEW_v4.5.0-beta.md ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MnemoCore v4.5.0-beta — Code Review
2
+ **Reviewer:** Omega (GLM-5)
3
+ **Datum:** 2026-02-20 07:45 CET
4
+ **Scope:** Full kodbas, fokus på query/store-flödet
5
+
6
+ ---
7
+
8
+ ## 🚨 KRITISKA PROBLEMER (Blockers)
9
+
10
+ ### 1. **Query Returnerar 0 Resultat** 🔴 BLOCKER
11
+ **Symptom:** `POST /query` returnerar tom lista även efter framgångsrik `POST /store`
12
+
13
+ **Root Cause Analysis:**
14
+
15
+ #### 1.1 HNSW Index Manager — Position Mapping Bug
16
+ **Fil:** `hnsw_index.py:221-236`
17
+
18
+ ```python
19
+ def _position_to_node_id(self, position: int) -> Optional[str]:
20
+ """Map HNSW sequential position back to node_id."""
21
+ if not hasattr(self, "_position_map"):
22
+ object.__setattr__(self, "_position_map", {})
23
+ pm: Dict[int, str] = self._position_map
24
+
25
+ # Rebuild position map if needed (after index rebuild)
26
+ if len(pm) < len(self._id_map):
27
+ pm.clear()
28
+ for pos, (fid, nid) in enumerate(
29
+ sorted(self._id_map.items(), key=lambda x: x[0])
30
+ ):
31
+ pm[pos] = nid
32
+
33
+ return pm.get(position)
34
+ ```
35
+
36
+ **PROBLEM:** Position map bygger på `sorted(_id_map.items(), key=lambda x: x[0])` vilket sorterar efter FAISS ID (int), **inte** efter insättningsordning. HNSW returnerar positioner baserat på insättningsordning, men mappningen är inkonsekvent.
37
+
38
+ **Fix:**
39
+ ```python
40
+ # Behåll insättningsordning separat
41
+ def add(self, node_id: str, hdv_data: np.ndarray) -> None:
42
+ # ... existing code ...
43
+ self._insertion_order.append(node_id) # NY
44
+
45
+ def _position_to_node_id(self, position: int) -> Optional[str]:
46
+ if position < len(self._insertion_order):
47
+ return self._insertion_order[position]
48
+ return None
49
+ ```
50
+
51
+ ---
52
+
53
+ #### 1.2 TextEncoder — Token Normalization Inkonsekvens
54
+ **Fil:** `binary_hdv.py:339-342`
55
+
56
+ ```python
57
+ def encode(self, text: str) -> BinaryHDV:
58
+ tokens = text.lower().split() # <-- BARA whitespace split
59
+ if not tokens:
60
+ return BinaryHDV.random(self.dimension)
61
+ ```
62
+
63
+ **PROBLEM:** Query-text vs lagrad text kan ha olika tokenisering:
64
+ - `"Hello World"` → tokens: `["hello", "world"]`
65
+ - `"Hello, World!"` → tokens: `["hello,", "world!"]` ← olika token!
66
+
67
+ **Fix:**
68
+ ```python
69
+ import re
70
+
71
+ def encode(self, text: str) -> BinaryHDV:
72
+ # Konsekvent tokenisering
73
+ tokens = re.findall(r'\b\w+\b', text.lower())
74
+ if not tokens:
75
+ return BinaryHDV.random(self.dimension)
76
+ ```
77
+
78
+ ---
79
+
80
+ #### 1.3 HNSW Upgrade Threshold Race Condition
81
+ **Fil:** `hnsw_index.py:87-117`
82
+
83
+ ```python
84
+ def _maybe_upgrade_to_hnsw(self) -> None:
85
+ if len(self._id_map) < FLAT_THRESHOLD: # 256
86
+ return
87
+
88
+ # ... existing code ...
89
+ existing: List[Tuple[int, np.ndarray]] = []
90
+ for fid, node_id in self._id_map.items():
91
+ if node_id in self._vector_cache:
92
+ existing.append((fid, self._vector_cache[node_id]))
93
+ ```
94
+
95
+ **PROBLEM:** `_vector_cache` används bara vid HNSW-upgrade, men vid normal flat-index-användning cachas inte vektorer. Vid upgrade saknas data.
96
+
97
+ **Fix:** Alltid cacha vektorer:
98
+ ```python
99
+ def add(self, node_id: str, hdv_data: np.ndarray) -> None:
100
+ # ... existing code ...
101
+ self._vector_cache[node_id] = hdv_data.copy() # ALLTID, inte bara HNSW
102
+ ```
103
+
104
+ ---
105
+
106
+ ### 2. **Qdrant Vector Unpacking Mismatch** 🔴 HIGH
107
+ **Fil:** `tier_manager.py:387-392` + `qdrant_store.py`
108
+
109
+ ```python
110
+ # Vid save till Qdrant (tier_manager.py):
111
+ bits = np.unpackbits(node.hdv.data)
112
+ vector = bits.astype(float).tolist() # 16,384 floats
113
+
114
+ # Vid search från Qdrant (qdrant_store.py):
115
+ arr = np.array(vec_data) > 0.5
116
+ packed = np.packbits(arr.astype(np.uint8))
117
+ ```
118
+
119
+ **PROBLEM:** Qdrant använder COSINE distance för HOT och MANHATTAN för WARM, men BinaryHDV använder HAMMING distance. Similarity scores kan vara inkompatibla.
120
+
121
+ **Konfiguration (`config.yaml`):**
122
+ ```yaml
123
+ qdrant:
124
+ collection_hot:
125
+ distance: COSINE # ← Fel för binary vectors!
126
+ collection_warm:
127
+ distance: MANHATTAN # ← Också suboptimalt
128
+ ```
129
+
130
+ **Fix:** Använd `Distance.DOT` för binary vectors med normaliserad similarity.
131
+
132
+ ---
133
+
134
+ ### 3. **FAISS Binary HNSW — Inte Fullt Implementerat** 🔴 HIGH
135
+ **Fil:** `hnsw_index.py:59-66`
136
+
137
+ ```python
138
+ def _build_hnsw_index(self, existing_nodes: Optional[List[Tuple[int, np.ndarray]]] = None) -> None:
139
+ hnsw = faiss.IndexBinaryHNSW(self.dimension, self.m)
140
+ hnsw.hnsw.efConstruction = self.ef_construction
141
+ hnsw.hnsw.efSearch = self.ef_search
142
+ ```
143
+
144
+ **PROBLEM:** `IndexBinaryHNSW` saknar `IndexIDMap`-stöd. Koden försöker hantera detta med `_position_map`, men detta är skört vid:
145
+ - Delete + re-add
146
+ - Concurrent access
147
+ - Index rebuilds
148
+
149
+ **Risk:** Position mapping kan bli desynkroniserad → query returnerar fel IDs eller inga resultat.
150
+
151
+ ---
152
+
153
+ ## ⚠️ HÖGA RISKER (High Priority)
154
+
155
+ ### 4. **Demotion Race Condition** 🟠
156
+ **Fil:** `tier_manager.py:175-220`
157
+
158
+ ```python
159
+ async def get_memory(self, node_id: str) -> Optional[MemoryNode]:
160
+ demote_candidate = None
161
+ result_node = None
162
+
163
+ async with self.lock:
164
+ if node_id in self.hot:
165
+ node = self.hot[node_id]
166
+ node.access()
167
+
168
+ if self._should_demote(node):
169
+ node.tier = "warm" # Markerar som warm
170
+ demote_candidate = node
171
+
172
+ result_node = node
173
+
174
+ # I/O OUTSIDE LOCK — gap där annan tråd kan försöka access
175
+ if demote_candidate:
176
+ await self._save_to_warm(demote_candidate) # Kan misslyckas
177
+
178
+ async with self.lock:
179
+ if demote_candidate.id in self.hot:
180
+ del self.hot[demote_candidate.id] # Nu borta
181
+ ```
182
+
183
+ **PROBLEM:** Tidsfönster mellan "mark as warm" och "delete from hot" där:
184
+ - `get_memory()` kan returnera samma node twice
185
+ - Query kan missa noden under övergången
186
+
187
+ ---
188
+
189
+ ### 5. **Subconscious AI — Infinite Loop Risk** 🟠
190
+ **Fil:** `subconscious_ai.py` (inte granskad fullt, men config visar risk)
191
+
192
+ ```yaml
193
+ subconscious_ai:
194
+ enabled: false # BETA - bra att den är avstängd
195
+ pulse_interval_seconds: 120
196
+ rate_limit_per_hour: 50
197
+ max_memories_per_cycle: 10
198
+ ```
199
+
200
+ **Risk:** Om `micro_self_improvement_enabled: true` kan systemet gå in i självförbättringsspiraler.
201
+
202
+ ---
203
+
204
+ ### 6. **Memory Leak i _vector_cache** 🟠
205
+ **Fil:** `hnsw_index.py:107`
206
+
207
+ ```python
208
+ @property
209
+ def _vector_cache(self) -> Dict[str, np.ndarray]:
210
+ if not hasattr(self, "_vcache"):
211
+ object.__setattr__(self, "_vcache", {})
212
+ return self._vcache
213
+ ```
214
+
215
+ **PROBLEM:** `_vector_cache` växer obegränsat. Ingen cleanup vid delete eller consolidation.
216
+
217
+ **Fix:**
218
+ ```python
219
+ def remove(self, node_id: str) -> None:
220
+ # ... existing code ...
221
+ self._vector_cache.pop(node_id, None) # Finns redan, men verifiera
222
+ ```
223
+
224
+ ---
225
+
226
+ ## 📊 PRESTANDA & SKALBARHET
227
+
228
+ ### 7. **O(N) Linear Search Fallback** 🟡
229
+ **Fil:** `tier_manager.py:902+`
230
+
231
+ När HNSW inte är tillgängligt (FAISS ej installerat), faller systemet tillbaka till:
232
+
233
+ ```python
234
+ def _linear_search_hot(self, query_vec: BinaryHDV, top_k: int) -> List[Tuple[str, float]]:
235
+ # Inte visad i filen, men nämnd som fallback
236
+ ```
237
+
238
+ **Prestandaimpakt:**
239
+ - 2,000 memories (HOT max): ~4ms
240
+ - 10,000 memories: ~20ms
241
+ - 100,000 memories: ~200ms ← Ej acceptabelt för real-time query
242
+
243
+ ---
244
+
245
+ ### 8. **Qdrant Batch Operations Saknas** 🟡
246
+ **Fil:** `qdrant_store.py`
247
+
248
+ ```python
249
+ async def upsert(self, collection: str, points: List[models.PointStruct]):
250
+ await qdrant_breaker.call(
251
+ self.client.upsert, collection_name=collection, points=points
252
+ )
253
+ ```
254
+
255
+ **PROBLEM:** Consolidation (`consolidate_warm_to_cold`) gör en-at-a-time deletes istället för batch:
256
+
257
+ ```python
258
+ # tier_manager.py:750
259
+ if ids_to_delete:
260
+ await self.qdrant.delete(collection, ids_to_delete) # Bra!
261
+ ```
262
+
263
+ Men `list_warm()` och `search()` saknar pagination-optimering.
264
+
265
+ ---
266
+
267
+ ## 🏗️ ARKITEKTUR & DESIGN
268
+
269
+ ### 9. **Dependency Injection — Halvvägs** 🟡
270
+ **Status:** Singeltons borttagna, men inte fullt DI
271
+
272
+ **Gott:**
273
+ - `HAIMEngine(config=..., tier_manager=...)` stöder injection
274
+ - `Container` pattern i `container.py`
275
+
276
+ **Dåligt:**
277
+ - `get_config()` är fortfarande global
278
+ - `BinaryHDV.random()` använder global `np.random`
279
+
280
+ **Rekommendation:**
281
+ ```python
282
+ class BinaryHDV:
283
+ def __init__(self, data: np.ndarray, dimension: int, rng: Optional[np.random.Generator] = None):
284
+ self._rng = rng or np.random.default_rng()
285
+ ```
286
+
287
+ ---
288
+
289
+ ### 10. **Error Handling — Inkonsekvent** 🟡
290
+ **Filer:** Spridda
291
+
292
+ Vissa funktioner returnerar `None`:
293
+ ```python
294
+ async def get_memory(self, node_id: str) -> Optional[MemoryNode]:
295
+ # Returnerar None om ej hittad
296
+ ```
297
+
298
+ Andra kastar exceptions:
299
+ ```python
300
+ async def delete_memory(self, node_id: str):
301
+ if not node:
302
+ raise MemoryNotFoundError(node_id)
303
+ ```
304
+
305
+ **Rekommendation:** Konsekvent mönster:
306
+ - `get_*` → return `Optional[T]` (None = not found)
307
+ - `*_or_raise` → raise exception
308
+ - `delete_*` → return `bool` (deleted or not)
309
+
310
+ ---
311
+
312
+ ## 🔒 SÄKERHET & ROBUSTHET
313
+
314
+ ### 11. **API Key i Env Var — Bra** ✅
315
+ **Fil:** `api/main.py:81`
316
+
317
+ ```python
318
+ security = config.security if config else None
319
+ expected_key = (security.api_key if security else None) or os.getenv("HAIM_API_KEY", "")
320
+ ```
321
+
322
+ **Gott:** API key måste sättas explicit, fallback till env var.
323
+
324
+ ---
325
+
326
+ ### 12. **Rate Limiting — Implementerat** ✅
327
+ **Fil:** `api/middleware.py`
328
+
329
+ ```python
330
+ class QueryRateLimiter(RateLimiter):
331
+ def __init__(self):
332
+ super().__init__(requests=500, window_seconds=60) # 500/min
333
+ ```
334
+
335
+ **Gott:** Separate limits för store/query/concept/analogy.
336
+
337
+ ---
338
+
339
+ ### 13. **Input Validation — Svag** 🟡
340
+ **Fil:** `api/models.py`
341
+
342
+ ```python
343
+ class StoreRequest(BaseModel):
344
+ content: str = Field(..., min_length=1, max_length=100000)
345
+ metadata: Optional[Dict[str, Any]] = None
346
+ ```
347
+
348
+ **PROBLEM:** Ingen validering av `metadata`-innehåll. Kan innehålla:
349
+ - Ogiltiga UTF-8 characters
350
+ - Recursive structures
351
+ - Sensitive data leaks
352
+
353
+ **Fix:**
354
+ ```python
355
+ from pydantic import field_validator
356
+
357
+ class StoreRequest(BaseModel):
358
+ @field_validator('metadata')
359
+ @classmethod
360
+ def validate_metadata(cls, v):
361
+ if v and len(str(v)) > 10000: # Max 10KB metadata
362
+ raise ValueError('Metadata too large')
363
+ return v
364
+ ```
365
+
366
+ ---
367
+
368
+ ## 📝 KODKVALITET
369
+
370
+ ### 14. **Test Coverage — 39 Passing** ✅
371
+ **Fil:** `test_regression_output.txt`
372
+
373
+ ```
374
+ 39 passed, 5 warnings in 3.47s
375
+ ```
376
+
377
+ **Gott:** Alla tester passerar. Men:
378
+ - Inga tester för HNSW upgrade path
379
+ - Inga tester för concurrent access
380
+ - Inga tester för Qdrant integration (kräver live Qdrant)
381
+
382
+ ---
383
+
384
+ ### 15. **Documentation — Komplett** ✅
385
+ **Filer:** `README.md` (43KB), `CHANGELOG.md`, inline docs
386
+
387
+ **Gott:** Dokumentation är omfattande och uppdaterad.
388
+
389
+ ---
390
+
391
+ ## 🎯 PRIORITERAD FIX-LISTA
392
+
393
+ | Prioritet | Problem | Fil | Estimerad tid |
394
+ |-----------|---------|-----|---------------|
395
+ | 🔴 P0 | Position mapping bug | `hnsw_index.py` | 2h |
396
+ | 🔴 P0 | Token normalization | `binary_hdv.py` | 30min |
397
+ | 🔴 P0 | Vector cache vid upgrade | `hnsw_index.py` | 1h |
398
+ | 🟠 P1 | Qdrant distance mismatch | `config.yaml` + `qdrant_store.py` | 2h |
399
+ | 🟠 P1 | Demotion race condition | `tier_manager.py` | 3h |
400
+ | 🟡 P2 | Linear search fallback | `tier_manager.py` | 4h |
401
+ | 🟡 P2 | Memory leak _vector_cache | `hnsw_index.py` | 30min |
402
+
403
+ ---
404
+
405
+ ## 🔧 REKOMMENDERAD ACTION PLAN
406
+
407
+ ### Fas 1: Query Fix (Dag 1)
408
+ 1. **Fixa `_position_to_node_id()`** — Använd insättningsordning istället för sorted IDs
409
+ 2. **Fixa `TextEncoder.encode()`** — Konsekvent tokenisering med regex
410
+ 3. **Alltid cacha vektorer** — Ta bort conditional `_vector_cache`
411
+
412
+ ### Fas 2: Qdrant Alignment (Dag 2)
413
+ 1. **Ändra distance metric** — `Distance.DOT` för binary vectors
414
+ 2. **Verifiera vector unpacking** — Säkerställ 16,384 → 2,048 byte mapping
415
+
416
+ ### Fas 3: Hardening (Dag 3)
417
+ 1. **Lägg till HNSW upgrade tester**
418
+ 2. **Fixa demotion race condition**
419
+ 3. **Input validation för metadata**
420
+
421
+ ---
422
+
423
+ ## 📋 SUMMARY
424
+
425
+ **Total kod:** ~25,000 LOC (src/)
426
+ **Tester:** 39 passing
427
+ **Kritiska buggar:** 3
428
+ **Höga risker:** 4
429
+ **Medel risker:** 5
430
+
431
+ **Verdict:** v4.5.0-beta är **inte production-ready**. Query-flödet har 3 kritiska buggar som förhindrar korrekt retrieval. Arkitekturen är solid, men implementationen av HNSW/index-mapping behöver omskrivas.
432
+
433
+ **Rekommendation:**
434
+ 1. Omedelbart fixa P0-issues (4-5 timmars arbete)
435
+ 2. Kör regression tests
436
+ 3. Deploy till staging för validering
437
+ 4. Sätt Opus 4.6 + Gemini 3.1 på Fas 1-3
438
+
439
+ ---
440
+
441
+ *Review genererad av Omega (GLM-5) för Robin Granberg*
442
+ *Senast uppdaterad: 2026-02-20 07:45 CET*
ROADMAP.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MnemoCore Roadmap
2
+
3
+ **Open Source Infrastructure for Persistent Cognitive Memory**
4
+
5
+ Version: 4.5.0-beta | Updated: 2026-02-20
6
+
7
+ ---
8
+
9
+ ## Vision
10
+
11
+ MnemoCore provides the foundational memory layer for cognitive AI systems —
12
+ a production-ready, self-hosted alternative to cloud-dependent memory solutions.
13
+
14
+ ---
15
+
16
+ ## Current Status (v4.5.0-beta)
17
+
18
+ | Component | Status |
19
+ |-----------|--------|
20
+ | Binary HDV Engine | ✅ Stable |
21
+ | Tiered Storage (HOT/WARM/COLD) | ✅ Functional |
22
+ | HNSW Index | ✅ Working |
23
+ | Query/Store API | ✅ Operational |
24
+ | Qdrant Integration | ✅ Available |
25
+ | MCP Server | 🟡 Beta |
26
+ | PyPI Distribution | 🟡 Pending |
27
+
28
+ ---
29
+
30
+ ## Phase 5: Production Hardening
31
+
32
+ **Goal:** Battle-tested, enterprise-ready release
33
+
34
+ ### 5.1 Stability & Testing
35
+ - [ ] Increase test coverage to 80%+
36
+ - [ ] Add integration tests for Qdrant backend
37
+ - [ ] Stress test with 100k+ memories
38
+ - [ ] Add chaos engineering tests (network failures, disk full)
39
+
40
+ ### 5.2 Performance Optimization
41
+ - [ ] Benchmark query latency at scale
42
+ - [ ] Optimize HNSW index rebuild time
43
+ - [ ] Add batch operation endpoints
44
+ - [ ] Profile and reduce memory footprint
45
+
46
+ ### 5.3 Developer Experience
47
+ - [ ] Complete API documentation (OpenAPI spec)
48
+ - [ ] Add usage examples for common patterns
49
+ - [ ] Create quickstart guide
50
+ - [ ] Add Jupyter notebook tutorials
51
+
52
+ ### 5.4 Operations
53
+ - [ ] Docker Compose production config
54
+ - [ ] Kubernetes Helm chart
55
+ - [ ] Prometheus metrics endpoint
56
+ - [ ] Health check hardening
57
+
58
+ **ETA:** 2-3 weeks
59
+
60
+ ---
61
+
62
+ ## Phase 6: Feature Expansion
63
+
64
+ **Goal:** More cognitive capabilities
65
+
66
+ ### 6.1 Advanced Retrieval
67
+ - [ ] Temporal queries ("memories from last week")
68
+ - [ ] Multi-hop associative recall
69
+ - [ ] Contextual ranking (personalized relevance)
70
+ - [ ] Negation queries ("NOT about project X")
71
+
72
+ ### 6.2 Memory Enrichment
73
+ - [ ] Auto-tagging via LLM
74
+ - [ ] Entity extraction (names, dates, concepts)
75
+ - [ ] Sentiment scoring
76
+ - [ ] Importance classification
77
+
78
+ ### 6.3 Multi-Modal Support
79
+ - [ ] Image embedding storage
80
+ - [ ] Audio transcript indexing
81
+ - [ ] Document chunk management
82
+
83
+ **ETA:** 4-6 weeks
84
+
85
+ ---
86
+
87
+ ## Phase 7: Ecosystem
88
+
89
+ **Goal:** Easy integration with existing AI stacks
90
+
91
+ ### 7.1 Integrations
92
+ - [ ] LangChain memory adapter
93
+ - [ ] LlamaIndex integration
94
+ - [ ] OpenAI Assistants API compatible
95
+ - [ ] Claude MCP protocol
96
+
97
+ ### 7.2 SDKs
98
+ - [ ] Python SDK (official)
99
+ - [ ] TypeScript/JavaScript SDK
100
+ - [ ] Go SDK
101
+ - [ ] Rust SDK
102
+
103
+ ### 7.3 Community
104
+ - [ ] Discord/Slack community
105
+ - [ ] Contributing guide
106
+ - [ ] Feature request process
107
+ - [ ] Regular release cadence
108
+
109
+ **ETA:** 8-12 weeks
110
+
111
+ ---
112
+
113
+ ## Long-Term Vision (Phase 8+)
114
+
115
+ ### Research Directions
116
+ - [ ] Hierarchical memory (episodic → semantic → procedural)
117
+ - [ ] Forgetting curves with spaced repetition
118
+ - [ ] Dream consolidation during idle cycles
119
+ - [ ] Meta-learning from usage patterns
120
+
121
+ ### Platform
122
+ - [ ] Managed cloud offering (optional)
123
+ - [ ] Multi-tenant support
124
+ - [ ] Federation across nodes
125
+ - [ ] Privacy-preserving memory sharing
126
+
127
+ ---
128
+
129
+ ## Release Schedule
130
+
131
+ | Version | Target | Focus |
132
+ |---------|--------|-------|
133
+ | v4.5.0 | Current | Beta stabilization |
134
+ | v5.0.0 | +2 weeks | Production ready |
135
+ | v5.1.0 | +4 weeks | Performance + DX |
136
+ | v6.0.0 | +6 weeks | Feature expansion |
137
+ | v7.0.0 | +10 weeks | Ecosystem |
138
+
139
+ ---
140
+
141
+ ## Contributing
142
+
143
+ MnemoCore is open source under MIT license.
144
+
145
+ - **GitHub:** https://github.com/RobinALG87/MnemoCore-Infrastructure-for-Persistent-Cognitive-Memory
146
+ - **PyPI:** `pip install mnemocore`
147
+ - **Issues:** Use GitHub Issues for bugs and feature requests
148
+ - **PRs:** Welcome! See CONTRIBUTING.md
149
+
150
+ ---
151
+
152
+ *Roadmap maintained by Robin Granberg & Omega*
config.yaml CHANGED
@@ -13,7 +13,7 @@ haim:
13
  # Memory tier thresholds
14
  tiers:
15
  hot:
16
- max_memories: 2000
17
  ltp_threshold_min: 0.7
18
  eviction_policy: "lru"
19
 
@@ -135,12 +135,12 @@ haim:
135
  # =========================================================================
136
  subconscious_ai:
137
  # BETA FEATURE - Must be explicitly enabled
138
- enabled: false
139
  beta_mode: true
140
 
141
  # Model configuration
142
  model_provider: "ollama" # ollama | lm_studio | openai_api | anthropic_api
143
- model_name: "phi3.5:3.8b"
144
  model_url: "http://localhost:11434"
145
  # api_key: null # For API providers
146
  # api_base_url: null
@@ -152,16 +152,16 @@ haim:
152
 
153
  # Resource management
154
  max_cpu_percent: 30.0
155
- cycle_timeout_seconds: 30
156
  rate_limit_per_hour: 50
157
 
158
  # Operations
159
  memory_sorting_enabled: true
160
  enhanced_dreaming_enabled: true
161
- micro_self_improvement_enabled: false # Initially disabled
162
 
163
  # Safety
164
- dry_run: true
165
  log_all_decisions: true
166
  audit_trail_path: "./data/subconscious_audit.jsonl"
167
  max_memories_per_cycle: 10
 
13
  # Memory tier thresholds
14
  tiers:
15
  hot:
16
+ max_memories: 3000
17
  ltp_threshold_min: 0.7
18
  eviction_policy: "lru"
19
 
 
135
  # =========================================================================
136
  subconscious_ai:
137
  # BETA FEATURE - Must be explicitly enabled
138
+ enabled: true
139
  beta_mode: true
140
 
141
  # Model configuration
142
  model_provider: "ollama" # ollama | lm_studio | openai_api | anthropic_api
143
+ model_name: "phi3.5:latest"
144
  model_url: "http://localhost:11434"
145
  # api_key: null # For API providers
146
  # api_base_url: null
 
152
 
153
  # Resource management
154
  max_cpu_percent: 30.0
155
+ cycle_timeout_seconds: 120
156
  rate_limit_per_hour: 50
157
 
158
  # Operations
159
  memory_sorting_enabled: true
160
  enhanced_dreaming_enabled: true
161
+ micro_self_improvement_enabled: true # Initially disabled
162
 
163
  # Safety
164
+ dry_run: false
165
  log_all_decisions: true
166
  audit_trail_path: "./data/subconscious_audit.jsonl"
167
  max_memories_per_cycle: 10
data/subconscious_audit.jsonl CHANGED
The diff for this file is too large to render. See raw diff
 
docs/PATTERN_LEARNER_SPEC.md ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MnemoCore Pattern Learner — Specification Draft
2
+
3
+ **Version:** 0.1-draft
4
+ **Date:** 2026-02-20
5
+ **Status:** Draft for Review
6
+ **Author:** Omega (GLM-5) for Robin Granberg
7
+
8
+ ---
9
+
10
+ ## Executive Summary
11
+
12
+ Pattern Learner är en MnemoCore-modul som lär sig från användarinteraktioner **utan att lagra persondata**. Den extraherar statistiska mönster, topic clustering och kvalitetsmetrics som kan användas för att förbättra chatbot-performance över tid.
13
+
14
+ **Key principle:** Learn patterns, forget people.
15
+
16
+ ---
17
+
18
+ ## Problem Statement
19
+
20
+ ### Healthcare Chatbot Challenges
21
+
22
+ | Utmaning | Konsekvens |
23
+ |----------|------------|
24
+ | GDPR/HIPAA compliance | Kan inte lagra konversationer |
25
+ | Multitenancy | Data får inte läcka mellan kliniker |
26
+ | Quality improvement | Behöver veta vad som fungerar |
27
+ | Knowledge gaps | Behöver identifiera vad som saknas i docs |
28
+
29
+ ### Current Solutions (Limitations)
30
+
31
+ - **Stateless RAG:** Ingen inlärning alls
32
+ - **Full memory:** GDPR-risk, sekretessproblem
33
+ - **Manual analytics:** Tidskrävande, inte real-time
34
+
35
+ ---
36
+
37
+ ## Solution: Pattern Learner
38
+
39
+ ### Core Concept
40
+
41
+ ```
42
+ User Query ──► Anonymize ──► Extract Pattern ──► Aggregate
43
+
44
+ └── PII removed before storage
45
+ ```
46
+
47
+ **What IS stored:**
48
+ - Topic clusters (anonymized)
49
+ - Query frequency distributions
50
+ - Response quality aggregates
51
+ - Knowledge gap indicators
52
+
53
+ **What is NOT stored:**
54
+ - User identities
55
+ - Clinic associations
56
+ - Patient data
57
+ - Raw conversations
58
+
59
+ ---
60
+
61
+ ## Architecture
62
+
63
+ ### High-Level Design
64
+
65
+ ```
66
+ ┌─────────────────────────────────────────────────────────────┐
67
+ │ Pattern Learner Module │
68
+ ├─────────────────────────────────────────────────────────────┤
69
+ │ │
70
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
71
+ │ │ Anonymizer │───►│Topic Extractor│───►│ Aggregator │ │
72
+ │ └──────────────┘ └──────────────┘ └──────────────┘ │
73
+ │ │ │ │ │
74
+ │ │ ▼ ▼ │
75
+ │ │ ┌──────────────┐ ┌──────────────┐ │
76
+ │ │ │Topic Embedder│ │ Stats Store │ │
77
+ │ │ │ (MnemoCore) │ │ (Encrypted) │ │
78
+ │ │ └──────────────┘ └──────────────┘ │
79
+ │ │ │ │ │
80
+ │ └───────────────────┴────────────────────┘ │
81
+ │ │ │
82
+ │ ▼ │
83
+ │ ┌──────────────┐ │
84
+ │ │ Insights API│ │
85
+ │ └──────────────┘ │
86
+ │ │
87
+ └─────────────────────────────────────────────────────────────┘
88
+ ```
89
+
90
+ ### Components
91
+
92
+ #### 1. Anonymizer
93
+
94
+ **Purpose:** Remove all PII before processing
95
+
96
+ **Methods:**
97
+ - Named Entity Recognition (NER) for person names
98
+ - Pattern matching for phone numbers, addresses
99
+ - Clinic/organization detection
100
+ - Session ID hashing
101
+
102
+ ```python
103
+ class Anonymizer:
104
+ """Remove PII from queries before pattern extraction"""
105
+
106
+ def __init__(self):
107
+ self.ner_model = load_ner_model("sv") # Swedish
108
+ self.patterns = {
109
+ "phone": r"\+?\d{1,3}[\s-]?\d{2,4}[\s-]?\d{2,4}[\s-]?\d{2,4}",
110
+ "email": r"[\w\.-]+@[\w\.-]+\.\w+",
111
+ "personal_number": r"\d{6,8}[-\s]?\d{4}",
112
+ }
113
+
114
+ def anonymize(self, text: str) -> str:
115
+ """Remove all PII from text"""
116
+
117
+ # 1. NER for names
118
+ entities = self.ner_model.extract(text)
119
+ for entity in entities:
120
+ if entity.type in ["PER", "ORG"]:
121
+ text = text.replace(entity.text, "[ANON]")
122
+
123
+ # 2. Pattern matching
124
+ for pattern_type, pattern in self.patterns.items():
125
+ text = re.sub(pattern, f"[{pattern_type.upper()}]", text)
126
+
127
+ # 3. Remove clinic names (configurable blacklist)
128
+ for clinic_name in self.clinic_blacklist:
129
+ text = text.replace(clinic_name, "[KLINIK]")
130
+
131
+ return text
132
+ ```
133
+
134
+ ---
135
+
136
+ #### 2. Topic Extractor
137
+
138
+ **Purpose:** Extract semantic topics from anonymized queries
139
+
140
+ **Methods:**
141
+ - Keyword extraction (TF-IDF)
142
+ - Topic modeling (LDA, BERTopic)
143
+ - Embedding-based clustering
144
+
145
+ ```python
146
+ class TopicExtractor:
147
+ """Extract topics from anonymized queries"""
148
+
149
+ def __init__(self, mnemocore_engine):
150
+ self.engine = mnemocore_engine
151
+ self.topic_threshold = 0.5
152
+
153
+ async def extract_topics(self, query: str) -> List[str]:
154
+ """Extract topics from anonymized query"""
155
+
156
+ # 1. Get keywords
157
+ keywords = self._extract_keywords(query)
158
+
159
+ # 2. Find similar topics in MnemoCore
160
+ similar = await self.engine.query(query, top_k=5)
161
+
162
+ # 3. Cluster into topics
163
+ topics = []
164
+ for memory_id, similarity in similar:
165
+ if similarity > self.topic_threshold:
166
+ memory = await self.engine.get_memory(memory_id)
167
+ topics.extend(memory.metadata.get("topics", []))
168
+
169
+ # 4. Deduplicate
170
+ return list(set(topics + keywords))
171
+
172
+ def _extract_keywords(self, text: str) -> List[str]:
173
+ """Extract keywords using TF-IDF"""
174
+ # Simple implementation
175
+ words = text.lower().split()
176
+ return [w for w in words if len(w) > 3 and w not in STOPWORDS_SV]
177
+ ```
178
+
179
+ ---
180
+
181
+ #### 3. Aggregator
182
+
183
+ **Purpose:** Store statistical patterns without PII
184
+
185
+ **Data structures:**
186
+
187
+ ```python
188
+ @dataclass
189
+ class TopicStats:
190
+ """Statistics for a topic"""
191
+ topic: str
192
+ count: int = 0
193
+ first_seen: datetime = None
194
+ last_seen: datetime = None
195
+ trend: float = 0.0 # Recent increase/decrease
196
+
197
+ @dataclass
198
+ class ResponseQuality:
199
+ """Aggregated response quality (no individual ratings)"""
200
+ response_signature: str # Hash of response template
201
+ avg_rating: float = 0.5
202
+ sample_count: int = 0
203
+ last_updated: datetime = None
204
+
205
+ @dataclass
206
+ class KnowledgeGap:
207
+ """Topics with no good answers"""
208
+ topic: str
209
+ query_count: int = 0
210
+ failure_rate: float = 1.0 # % of queries that got "I don't know"
211
+ suggested_action: str = "" # "add documentation", "improve answer"
212
+ ```
213
+
214
+ **Storage:**
215
+
216
+ ```python
217
+ class PatternStore:
218
+ """Store patterns (encrypted, no PII)"""
219
+
220
+ def __init__(self, encryption_key: bytes):
221
+ self.key = encryption_key
222
+ self.topics: Dict[str, TopicStats] = {}
223
+ self.qualities: Dict[str, ResponseQuality] = {}
224
+ self.gaps: Dict[str, KnowledgeGap] = {}
225
+
226
+ def record_topic(self, topic: str):
227
+ """Record that a topic was queried"""
228
+ if topic not in self.topics:
229
+ self.topics[topic] = TopicStats(
230
+ topic=topic,
231
+ first_seen=datetime.utcnow()
232
+ )
233
+
234
+ stats = self.topics[topic]
235
+ stats.count += 1
236
+ stats.last_seen = datetime.utcnow()
237
+
238
+ def record_quality(self, response_sig: str, rating: int):
239
+ """Record response quality (aggregated)"""
240
+ if response_sig not in self.qualities:
241
+ self.qualities[response_sig] = ResponseQuality(
242
+ response_signature=response_sig
243
+ )
244
+
245
+ q = self.qualities[response_sig]
246
+ # Exponential moving average
247
+ q.avg_rating = 0.9 * q.avg_rating + 0.1 * (rating / 5.0)
248
+ q.sample_count += 1
249
+ q.last_updated = datetime.utcnow()
250
+
251
+ def record_gap(self, topic: str, had_answer: bool):
252
+ """Record knowledge gap"""
253
+ if topic not in self.gaps:
254
+ self.gaps[topic] = KnowledgeGap(topic=topic)
255
+
256
+ gap = self.gaps[topic]
257
+ gap.query_count += 1
258
+ if not had_answer:
259
+ gap.failure_rate = (gap.failure_rate * (gap.query_count - 1) + 1) / gap.query_count
260
+ else:
261
+ gap.failure_rate = (gap.failure_rate * (gap.query_count - 1)) / gap.query_count
262
+ ```
263
+
264
+ ---
265
+
266
+ #### 4. Insights API
267
+
268
+ **Purpose:** Provide actionable insights to admins/developers
269
+
270
+ **Endpoints:**
271
+
272
+ ```python
273
+ # GET /insights/topics?top_k=10
274
+ {
275
+ "topics": [
276
+ {"topic": "implantat", "count": 1250, "trend": 0.15},
277
+ {"topic": "rotfyllning", "count": 980, "trend": -0.02},
278
+ {"topic": "priser", "count": 850, "trend": 0.30}
279
+ ],
280
+ "period": "30d"
281
+ }
282
+
283
+ # GET /insights/gaps
284
+ {
285
+ "knowledge_gaps": [
286
+ {
287
+ "topic": "tandreglering vuxna",
288
+ "query_count": 145,
289
+ "failure_rate": 0.85,
290
+ "suggested_action": "add documentation"
291
+ },
292
+ {
293
+ "topic": "akut tandvård",
294
+ "query_count": 89,
295
+ "failure_rate": 0.72,
296
+ "suggested_action": "improve answer"
297
+ }
298
+ ]
299
+ }
300
+
301
+ # GET /insights/quality
302
+ {
303
+ "top_responses": [
304
+ {"signature": "abc123", "avg_rating": 4.8, "sample_count": 520},
305
+ {"signature": "def456", "avg_rating": 4.5, "sample_count": 340}
306
+ ],
307
+ "worst_responses": [
308
+ {"signature": "xyz789", "avg_rating": 2.1, "sample_count": 45}
309
+ ]
310
+ }
311
+ ```
312
+
313
+ ---
314
+
315
+ ## MnemoCore Integration
316
+
317
+ ### Usage Pattern
318
+
319
+ ```python
320
+ from mnemocore import HAIMEngine
321
+ from mnemocore.pattern_learner import PatternLearner
322
+
323
+ # Initialize MnemoCore (stores topic embeddings)
324
+ engine = HAIMEngine(dimension=16384)
325
+ await engine.initialize()
326
+
327
+ # Initialize Pattern Learner
328
+ learner = PatternLearner(
329
+ engine=engine,
330
+ encryption_key=get_encryption_key(),
331
+ anonymizer=Anonymizer()
332
+ )
333
+
334
+ # Process a query (automatic learning)
335
+ async def handle_query(user_query: str, tenant_id: str):
336
+ # 1. Anonymize
337
+ anon_query = learner.anonymize(user_query)
338
+
339
+ # 2. Extract patterns (no PII)
340
+ topics = await learner.extract_topics(anon_query)
341
+
342
+ # 3. Record topic usage
343
+ for topic in topics:
344
+ learner.record_topic(topic)
345
+
346
+ # 4. Get answer from RAG
347
+ answer = await rag_lookup(anon_query)
348
+
349
+ # 5. Record if we had an answer
350
+ learner.record_gap(
351
+ topic=topics[0] if topics else "unknown",
352
+ had_answer=(answer is not None)
353
+ )
354
+
355
+ return answer
356
+
357
+ # Get insights (admin only)
358
+ async def get_dashboard():
359
+ top_topics = learner.get_top_topics(10)
360
+ gaps = learner.get_knowledge_gaps()
361
+ quality = learner.get_response_quality()
362
+
363
+ return {
364
+ "popular_topics": top_topics,
365
+ "needs_documentation": gaps,
366
+ "response_performance": quality
367
+ }
368
+ ```
369
+
370
+ ---
371
+
372
+ ## GDPR Compliance
373
+
374
+ ### Data Minimization
375
+
376
+ | Data Type | Stored? | Justification |
377
+ |-----------|---------|---------------|
378
+ | Raw queries | ❌ | PII risk |
379
+ | User IDs | ❌ | Not needed |
380
+ | Session IDs | ❌ | Not needed |
381
+ | Clinic IDs | ❌ | Not needed |
382
+ | **Topic labels** | ✅ | Anonymized |
383
+ | **Topic counts** | ✅ | Statistical |
384
+ | **Quality scores** | ✅ | Aggregated |
385
+ | **Gap indicators** | ✅ | Anonymized |
386
+
387
+ ### Right to Erasure (GDPR Art 17)
388
+
389
+ Since no PII is stored, right to erasure is **automatically satisfied**.
390
+
391
+ ### Data Retention
392
+
393
+ ```python
394
+ # Configurable retention
395
+ retention_policy = {
396
+ "topic_stats": "365d", # Keep for 1 year
397
+ "quality_scores": "90d", # Keep for 3 months
398
+ "gap_indicators": "30d", # Refresh monthly
399
+ }
400
+
401
+ # Automatic cleanup
402
+ async def cleanup_old_patterns():
403
+ cutoff = datetime.utcnow() - timedelta(days=retention_policy["topic_stats"])
404
+ for topic, stats in learner.topics.items():
405
+ if stats.last_seen < cutoff:
406
+ del learner.topics[topic]
407
+ ```
408
+
409
+ ---
410
+
411
+ ## Security Considerations
412
+
413
+ ### Encryption
414
+
415
+ - All pattern data encrypted at rest (AES-256)
416
+ - Encryption keys managed via HSM or Azure Key Vault
417
+ - Per-tenant encryption optional (for multi-tenant isolation)
418
+
419
+ ### Access Control
420
+
421
+ ```python
422
+ # Insights API requires admin role
423
+ @app.get("/insights/topics")
424
+ @require_role("admin")
425
+ async def get_topics():
426
+ return learner.get_top_topics(10)
427
+ ```
428
+
429
+ ### Audit Logging
430
+
431
+ ```python
432
+ # Log all pattern access (not the patterns themselves)
433
+ async def log_access(user_id: str, endpoint: str, timestamp: datetime):
434
+ await audit_log.store({
435
+ "user_id": user_id,
436
+ "endpoint": endpoint,
437
+ "timestamp": timestamp.isoformat(),
438
+ # No pattern data logged
439
+ })
440
+ ```
441
+
442
+ ---
443
+
444
+ ## Implementation Roadmap
445
+
446
+ ### Phase 1: MVP (2 weeks)
447
+
448
+ - [ ] Anonymizer with Swedish NER
449
+ - [ ] Basic topic extraction (keywords)
450
+ - [ ] Topic counter (no MnemoCore yet)
451
+ - [ ] Simple insights API
452
+
453
+ ### Phase 2: MnemoCore Integration (2 weeks)
454
+
455
+ - [ ] Topic embedding storage in MnemoCore
456
+ - [ ] Semantic topic clustering
457
+ - [ ] Gap detection using similarity search
458
+
459
+ ### Phase 3: Quality Metrics (2 weeks)
460
+
461
+ - [ ] Response quality tracking
462
+ - [ ] Feedback integration
463
+ - [ ] Quality dashboard
464
+
465
+ ### Phase 4: Production Hardening (2 weeks)
466
+
467
+ - [ ] Encryption at rest
468
+ - [ ] Access control
469
+ - [ ] Audit logging
470
+ - [ ] Performance optimization
471
+
472
+ ---
473
+
474
+ ## Business Value
475
+
476
+ ### For Healthcare Organizations
477
+
478
+ | Value | Metric |
479
+ |-------|--------|
480
+ | **Documentation gaps** | Know what to add to knowledge base |
481
+ | **Popular topics** | Prioritize documentation efforts |
482
+ | **Response quality** | Improve user satisfaction |
483
+ | **Trend analysis** | Identify emerging needs |
484
+
485
+ ### For Opus Dental (Competitive Advantage)
486
+
487
+ | Advantage | Value |
488
+ |-----------|-------|
489
+ | **Continuous improvement** | Chatbot gets smarter without storing PII |
490
+ | **Customer insights** | Know what clinics need |
491
+ | **Compliance by design** | GDPR-safe from day 1 |
492
+ | **Unique selling point** | "Learning chatbot" vs competitors |
493
+
494
+ ---
495
+
496
+ ## Technical Requirements
497
+
498
+ ### Dependencies
499
+
500
+ ```
501
+ mnemocore>=4.5.0
502
+ spacy[sv]>=3.7.0 # Swedish NER
503
+ numpy>=1.24.0
504
+ cryptography>=41.0.0 # Encryption
505
+ ```
506
+
507
+ ### Infrastructure
508
+
509
+ - MnemoCore instance (can be shared or per-tenant)
510
+ - Encrypted storage (Azure SQL, PostgreSQL with TDE)
511
+ - Optional: Azure Key Vault for key management
512
+
513
+ ### Performance
514
+
515
+ - Topic extraction: <50ms per query
516
+ - Insights API: <200ms
517
+ - Storage: ~1KB per unique topic (highly efficient)
518
+
519
+ ---
520
+
521
+ ## Open Questions
522
+
523
+ 1. **Topic granularity:** How specific should topics be? "Implantat" vs "Implantat pris" vs "Implantat komplikationer"
524
+
525
+ 2. **Trend detection:** What time window for trend analysis? 7d? 30d?
526
+
527
+ 3. **Multi-language:** Support for Finnish/Norwegian in addition to Swedish?
528
+
529
+ 4. **Tenant isolation:** Should patterns be shared across tenants (anonymized) or kept separate?
530
+
531
+ 5. **Feedback mechanism:** How to collect ratings? Thumbs up/down? 1-5 stars?
532
+
533
+ ---
534
+
535
+ ## Conclusion
536
+
537
+ Pattern Learner enables **continuous improvement** of healthcare chatbots **without GDPR risk**. It learns what users ask about, which answers work, and where documentation is missing — all without storing any personal data.
538
+
539
+ **Key innovation:** Transform "memory" into "patterns" — compliance-safe learning.
540
+
541
+ ---
542
+
543
+ ## Next Steps
544
+
545
+ 1. Review this spec
546
+ 2. Decide on open questions
547
+ 3. Prioritize MVP features
548
+ 4. Start implementation
549
+
550
+ ---
551
+
552
+ *Draft by Omega (GLM-5) for Robin Granberg*
553
+ *2026-02-20*
mnemocore_verify.py CHANGED
@@ -42,7 +42,7 @@ def setup_test_env():
42
  @pytest.mark.asyncio
43
  async def test_text_encoder_normalization():
44
  """Verify BUG-02: Text normalization fixes identical string variances"""
45
- encoder = TextEncoder(dimension=1024)
46
  hdv1 = encoder.encode("Hello World")
47
  hdv2 = encoder.encode("hello, world!")
48
 
@@ -51,14 +51,14 @@ async def test_text_encoder_normalization():
51
  def test_hnsw_singleton():
52
  """Verify BUG-08: HNSWIndexManager is a thread-safe singleton"""
53
  HNSWIndexManager._instance = None
54
- idx1 = HNSWIndexManager(dimension=1024)
55
- idx2 = HNSWIndexManager(dimension=1024)
56
  assert idx1 is idx2, "HNSWIndexManager is not a singleton"
57
 
58
  def test_hnsw_index_add_search():
59
  """Verify BUG-01 & BUG-03: Vector cache lost / Position mapping"""
60
  HNSWIndexManager._instance = None
61
- idx = HNSWIndexManager(dimension=1024)
62
 
63
  # Optional cleanup if it's reused
64
  idx._id_map = []
@@ -66,8 +66,8 @@ def test_hnsw_index_add_search():
66
  if idx._index:
67
  idx._index.reset()
68
 
69
- vec1 = BinaryHDV.random(1024)
70
- vec2 = BinaryHDV.random(1024)
71
 
72
  idx.add("test_node_1", vec1.data)
73
  idx.add("test_node_2", vec2.data)
 
42
  @pytest.mark.asyncio
43
  async def test_text_encoder_normalization():
44
  """Verify BUG-02: Text normalization fixes identical string variances"""
45
+ encoder = TextEncoder(dimension=16384)
46
  hdv1 = encoder.encode("Hello World")
47
  hdv2 = encoder.encode("hello, world!")
48
 
 
51
  def test_hnsw_singleton():
52
  """Verify BUG-08: HNSWIndexManager is a thread-safe singleton"""
53
  HNSWIndexManager._instance = None
54
+ idx1 = HNSWIndexManager(dimension=16384)
55
+ idx2 = HNSWIndexManager(dimension=16384)
56
  assert idx1 is idx2, "HNSWIndexManager is not a singleton"
57
 
58
  def test_hnsw_index_add_search():
59
  """Verify BUG-01 & BUG-03: Vector cache lost / Position mapping"""
60
  HNSWIndexManager._instance = None
61
+ idx = HNSWIndexManager(dimension=16384)
62
 
63
  # Optional cleanup if it's reused
64
  idx._id_map = []
 
66
  if idx._index:
67
  idx._index.reset()
68
 
69
+ vec1 = BinaryHDV.random(16384)
70
+ vec2 = BinaryHDV.random(16384)
71
 
72
  idx.add("test_node_1", vec1.data)
73
  idx.add("test_node_2", vec2.data)
src/mnemocore/api/main.py CHANGED
@@ -153,7 +153,7 @@ async def lifespan(app: FastAPI):
153
  from mnemocore.core.tier_manager import TierManager
154
  tier_manager = TierManager(config=config, qdrant_store=container.qdrant_store)
155
  engine = HAIMEngine(
156
- persist_path="./data/memory.jsonl",
157
  config=config,
158
  tier_manager=tier_manager,
159
  working_memory=container.working_memory,
@@ -574,6 +574,37 @@ async def get_stats(engine: HAIMEngine = Depends(get_engine)):
574
  return await engine.get_stats()
575
 
576
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  # Rate limit info endpoint
578
  @app.get("/rate-limits")
579
  async def get_rate_limits():
 
153
  from mnemocore.core.tier_manager import TierManager
154
  tier_manager = TierManager(config=config, qdrant_store=container.qdrant_store)
155
  engine = HAIMEngine(
156
+ persist_path=config.paths.memory_file,
157
  config=config,
158
  tier_manager=tier_manager,
159
  working_memory=container.working_memory,
 
574
  return await engine.get_stats()
575
 
576
 
577
+ # ─────────────────────────────────────────────────────────────────────────────
578
+ # Maintenance Endpoints
579
+ # ─────────────────────────────────────────────────────────────────────────────
580
+
581
+ @app.post("/maintenance/cleanup", dependencies=[Depends(get_api_key)])
582
+ async def cleanup_maintenance(threshold: float = 0.1, engine: HAIMEngine = Depends(get_engine)):
583
+ """Remove decayed synapses and stale index nodes."""
584
+ await engine.cleanup_decay(threshold=threshold)
585
+ return {"ok": True, "message": f"Synapse cleanup triggered with threshold {threshold}"}
586
+
587
+
588
+ @app.post("/maintenance/consolidate", dependencies=[Depends(get_api_key)])
589
+ async def consolidate_maintenance(engine: HAIMEngine = Depends(get_engine)):
590
+ """Trigger manual semantic consolidation pulse."""
591
+ if not engine._semantic_worker:
592
+ raise HTTPException(status_code=503, detail="Consolidation worker not initialized")
593
+
594
+ stats = await engine._semantic_worker.run_once()
595
+ return {"ok": True, "stats": stats}
596
+
597
+
598
+ @app.post("/maintenance/sweep", dependencies=[Depends(get_api_key)])
599
+ async def sweep_maintenance(engine: HAIMEngine = Depends(get_engine)):
600
+ """Trigger manual immunology sweep."""
601
+ if not engine._immunology:
602
+ raise HTTPException(status_code=503, detail="Immunology loop not initialized")
603
+
604
+ stats = await engine._immunology.sweep()
605
+ return {"ok": True, "stats": stats}
606
+
607
+
608
  # Rate limit info endpoint
609
  @app.get("/rate-limits")
610
  async def get_rate_limits():
src/mnemocore/core/binary_hdv.py CHANGED
@@ -404,9 +404,8 @@ class TextEncoder:
404
  Each token is bound with its position via XOR(token, permute(position_marker, i)).
405
  All position-bound tokens are bundled via majority vote.
406
  """
407
- # BUG-02 Fix: strip punctuation and normalize spaces
408
- normalized = re.sub(r'[^\w\s]', '', text).lower()
409
- tokens = normalized.split()
410
  if not tokens:
411
  return BinaryHDV.random(self.dimension)
412
 
 
404
  Each token is bound with its position via XOR(token, permute(position_marker, i)).
405
  All position-bound tokens are bundled via majority vote.
406
  """
407
+ # Improved Tokenization: consistent alphanumeric extraction
408
+ tokens = re.findall(r'\b\w+\b', text.lower())
 
409
  if not tokens:
410
  return BinaryHDV.random(self.dimension)
411
 
src/mnemocore/core/hnsw_index.py CHANGED
@@ -108,10 +108,7 @@ class HNSWIndexManager:
108
  self.VECTOR_PATH = data_dir / "mnemocore_hnsw_vectors.npy"
109
 
110
  if FAISS_AVAILABLE:
111
- if self.INDEX_PATH.exists() and self.IDMAP_PATH.exists() and self.VECTOR_PATH.exists():
112
- self._load()
113
- else:
114
- self._build_flat_index()
115
 
116
  self._initialized = True
117
 
@@ -195,7 +192,6 @@ class HNSWIndexManager:
195
  return
196
 
197
  self._maybe_upgrade_to_hnsw()
198
- self._save()
199
 
200
  def remove(self, node_id: str) -> None:
201
  """
@@ -234,7 +230,6 @@ class HNSWIndexManager:
234
  self._vector_store = compact_vecs
235
  self._stale_count = 0
236
 
237
- self._save()
238
  except ValueError:
239
  pass
240
 
@@ -251,7 +246,7 @@ class HNSWIndexManager:
251
  return []
252
 
253
  # Fetch more to account for deleted (None) entries
254
- k = min(top_k + self._stale_count, len(self._id_map))
255
  if k <= 0:
256
  return []
257
 
 
108
  self.VECTOR_PATH = data_dir / "mnemocore_hnsw_vectors.npy"
109
 
110
  if FAISS_AVAILABLE:
111
+ self._build_flat_index()
 
 
 
112
 
113
  self._initialized = True
114
 
 
192
  return
193
 
194
  self._maybe_upgrade_to_hnsw()
 
195
 
196
  def remove(self, node_id: str) -> None:
197
  """
 
230
  self._vector_store = compact_vecs
231
  self._stale_count = 0
232
 
 
233
  except ValueError:
234
  pass
235
 
 
246
  return []
247
 
248
  # Fetch more to account for deleted (None) entries
249
+ k = min(top_k + self._stale_count + 50, len(self._id_map))
250
  if k <= 0:
251
  return []
252
 
src/mnemocore/core/qdrant_store.py CHANGED
@@ -9,6 +9,7 @@ Phase 4.3: Temporal Recall - supports time-based filtering and indexing.
9
  from typing import List, Any, Optional, Tuple, Dict
10
  from datetime import datetime
11
  import asyncio
 
12
 
13
  from qdrant_client import AsyncQdrantClient, models
14
  from loguru import logger
@@ -93,39 +94,36 @@ class QdrantStore:
93
  )
94
  )
95
 
96
- # Create HOT collection (optimized for latency)
97
- if not await self.client.collection_exists(self.collection_hot):
98
- logger.info(f"Creating HOT collection: {self.collection_hot}")
99
- await self.client.create_collection(
100
- collection_name=self.collection_hot,
101
- vectors_config=models.VectorParams(
102
- size=self.dim,
103
- distance=models.Distance.DOT,
104
- on_disk=False
105
- ),
106
- quantization_config=quantization_config,
107
- hnsw_config=models.HnswConfigDiff(
108
- m=self.hnsw_m,
109
- ef_construct=self.hnsw_ef_construct,
110
- on_disk=False
111
- )
112
- )
113
 
114
- # Create WARM collection (optimized for scale/disk)
115
- if not await self.client.collection_exists(self.collection_warm):
116
- logger.info(f"Creating WARM collection: {self.collection_warm}")
117
  await self.client.create_collection(
118
- collection_name=self.collection_warm,
119
  vectors_config=models.VectorParams(
120
  size=self.dim,
121
  distance=models.Distance.DOT,
122
- on_disk=True
123
  ),
124
  quantization_config=quantization_config,
125
  hnsw_config=models.HnswConfigDiff(
126
  m=self.hnsw_m,
127
  ef_construct=self.hnsw_ef_construct,
128
- on_disk=True
129
  )
130
  )
131
 
@@ -173,24 +171,15 @@ class QdrantStore:
173
  ) -> List[models.ScoredPoint]:
174
  """
175
  Async semantic search.
176
-
177
- Args:
178
- collection: Collection name to search.
179
- query_vector: Query embedding vector.
180
- limit: Maximum number of results.
181
- score_threshold: Minimum similarity score.
182
- time_range: Optional (start, end) datetime tuple for temporal filtering.
183
- Phase 4.3: Enables "memories from last 48 hours" queries.
184
-
185
- Returns:
186
- List of scored points (empty list on errors).
187
-
188
- Note:
189
- This method returns an empty list on errors rather than raising,
190
- as search failures should not crash the calling code.
191
  """
192
  try:
 
 
 
 
 
193
  must_conditions = []
 
194
  if time_range:
195
  start_ts = int(time_range[0].timestamp())
196
  end_ts = int(time_range[1].timestamp())
@@ -227,15 +216,32 @@ class QdrantStore:
227
  )
228
  )
229
 
230
- return await qdrant_breaker.call(
231
  self.client.query_points,
232
  collection_name=collection,
233
- query=query_vector,
234
  limit=limit,
235
- score_threshold=score_threshold,
236
  query_filter=query_filter,
237
  search_params=search_params,
238
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  except CircuitOpenError:
240
  logger.warning(f"Qdrant search blocked for {collection}: circuit breaker open")
241
  return []
 
9
  from typing import List, Any, Optional, Tuple, Dict
10
  from datetime import datetime
11
  import asyncio
12
+ import numpy as np
13
 
14
  from qdrant_client import AsyncQdrantClient, models
15
  from loguru import logger
 
94
  )
95
  )
96
 
97
+ for collection_name, on_disk in [
98
+ (self.collection_hot, False),
99
+ (self.collection_warm, True)
100
+ ]:
101
+ if await self.client.collection_exists(collection_name):
102
+ # Check for distance mismatch (Phase 4.5 alignment)
103
+ info = await self.client.get_collection(collection_name)
104
+ current_distance = info.config.params.vectors.distance
105
+ if current_distance != models.Distance.DOT:
106
+ logger.warning(
107
+ f"Collection {collection_name} has distance {current_distance}, "
108
+ f"but DOT is required. Recreating collection."
109
+ )
110
+ await self.client.delete_collection(collection_name)
111
+ else:
112
+ continue
 
113
 
114
+ logger.info(f"Creating collection: {collection_name} (DOT)")
 
 
115
  await self.client.create_collection(
116
+ collection_name=collection_name,
117
  vectors_config=models.VectorParams(
118
  size=self.dim,
119
  distance=models.Distance.DOT,
120
+ on_disk=on_disk
121
  ),
122
  quantization_config=quantization_config,
123
  hnsw_config=models.HnswConfigDiff(
124
  m=self.hnsw_m,
125
  ef_construct=self.hnsw_ef_construct,
126
+ on_disk=on_disk
127
  )
128
  )
129
 
 
171
  ) -> List[models.ScoredPoint]:
172
  """
173
  Async semantic search.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  """
175
  try:
176
+ # Transform query to bipolar if it's (0, 1) (Phase 4.5)
177
+ q_vec = np.array(query_vector)
178
+ if np.all((q_vec == 0) | (q_vec == 1)):
179
+ q_vec = q_vec * 2.0 - 1.0
180
+
181
  must_conditions = []
182
+ query_filter = None
183
  if time_range:
184
  start_ts = int(time_range[0].timestamp())
185
  end_ts = int(time_range[1].timestamp())
 
216
  )
217
  )
218
 
219
+ response = await qdrant_breaker.call(
220
  self.client.query_points,
221
  collection_name=collection,
222
+ query=q_vec.tolist(),
223
  limit=limit,
 
224
  query_filter=query_filter,
225
  search_params=search_params,
226
  )
227
+
228
+ # Normalize scores to [0, 1] range (Phase 4.5)
229
+ # For Bipolar DOT, score is in range [-D, D].
230
+ # Similarity = (score + D) / (2 * D) = 0.5 + (score / 2D)
231
+ normalized_points = []
232
+ for hit in response.points:
233
+ sim = 0.5 + (hit.score / (2.0 * self.dim))
234
+
235
+ normalized_points.append(
236
+ models.ScoredPoint(
237
+ id=hit.id,
238
+ version=hit.version,
239
+ score=float(np.clip(sim, 0.0, 1.0)),
240
+ payload=hit.payload,
241
+ vector=hit.vector
242
+ )
243
+ )
244
+ return normalized_points
245
  except CircuitOpenError:
246
  logger.warning(f"Qdrant search blocked for {collection}: circuit breaker open")
247
  return []
src/mnemocore/core/subconscious_ai.py CHANGED
@@ -545,17 +545,19 @@ class SubconsciousAIWorker:
545
 
546
  # Build prompt for categorization
547
  memories_text = "\n".join([
548
- f"[{i+1}] {m.content[:200]}"
549
  for i, m in enumerate(unsorted[:5])
550
  ])
551
 
552
- prompt = f"""Categorize these memories into 2-3 broad categories and suggest tags for each.
 
 
553
 
554
- Memories:
555
- {memories_text}
556
 
557
- Return JSON format:
558
- {{"categories": ["cat1", "cat2"], "memory_tags": {{"1": ["tag1"], "2": ["tag2"]}}}}"""
559
 
560
  response = await self._model_client.generate(prompt, max_tokens=512)
561
  output = {"raw_response": response}
@@ -618,11 +620,12 @@ Return JSON format:
618
  """
619
  t_start = time.monotonic()
620
 
621
- # Find memories with low LTP (weak connections)
622
- recent = await self.engine.tier_manager.get_hot_recent(20)
 
623
  weak_memories = [
624
  m for m in recent
625
- if m.ltp_strength < 0.5 and not m.metadata.get("dream_analyzed")
626
  ][:self.cfg.max_memories_per_cycle]
627
 
628
  if not weak_memories:
@@ -638,17 +641,18 @@ Return JSON format:
638
 
639
  # Build prompt for semantic bridging
640
  memories_text = "\n".join([
641
- f"[{i+1}] {m.content[:150]} (LTP: {m.ltp_strength:.2f})"
642
  for i, m in enumerate(weak_memories[:5])
643
  ])
644
 
645
- prompt = f"""Analyze these memories and suggest semantic connections or bridging concepts.
 
646
 
647
- Memories:
648
- {memories_text}
649
 
650
- For each memory, suggest 2-3 keywords or concepts that could connect it to related memories.
651
- Return JSON: {{"bridges": {{"1": ["concept1", "concept2"], "2": ["concept3"]}}}}"""
652
 
653
  response = await self._model_client.generate(prompt, max_tokens=512)
654
  output = {"raw_response": response}
 
545
 
546
  # Build prompt for categorization
547
  memories_text = "\n".join([
548
+ f"ID {i+1}: {m.content[:200]}"
549
  for i, m in enumerate(unsorted[:5])
550
  ])
551
 
552
+ prompt = f"""You are a memory sorting agent for Veristate Systems.
553
+ Categorize these 5 memories into exactly 2 categories from this set: [Market Dynamics, Structural Integrity, Human Entropy, Digital Ascension, Strategic Intent].
554
+ Output ONLY a valid JSON object. No explanation. No markdown.
555
 
556
+ Format:
557
+ {{"categories": ["category1", "category2"], "memory_tags": {{"1": ["tagA"], "2": ["tagB"], "3": ["tagC"], "4": ["tagD"], "5": ["tagE"]}}}}
558
 
559
+ Memories:
560
+ {memories_text}"""
561
 
562
  response = await self._model_client.generate(prompt, max_tokens=512)
563
  output = {"raw_response": response}
 
620
  """
621
  t_start = time.monotonic()
622
 
623
+ # Find memories with low LTP (weak connections) or unanalyzed
624
+ recent = await self.engine.tier_manager.get_hot_recent(50)
625
+ # Phase 4.5 Tuning: Allow dreaming on nodes with LTP <= 0.5 (new nodes)
626
  weak_memories = [
627
  m for m in recent
628
+ if m.ltp_strength <= 0.5 and not m.metadata.get("dream_analyzed")
629
  ][:self.cfg.max_memories_per_cycle]
630
 
631
  if not weak_memories:
 
641
 
642
  # Build prompt for semantic bridging
643
  memories_text = "\n".join([
644
+ f"ID {i+1}: {m.content[:150]}"
645
  for i, m in enumerate(weak_memories[:5])
646
  ])
647
 
648
+ prompt = f"""You are a semantic analysis agent. Suggest connection keywords for these 5 memories.
649
+ Output ONLY a valid JSON object. No explanation. No markdown.
650
 
651
+ Format:
652
+ {{"bridges": {{"1": ["keyword1"], "2": ["keyword2"], "3": ["keyword3"], "4": ["keyword4"], "5": ["keyword5"]}}}}
653
 
654
+ Memories:
655
+ {memories_text}"""
656
 
657
  response = await self._model_client.generate(prompt, max_tokens=512)
658
  output = {"raw_response": response}
src/mnemocore/core/tier_manager.py CHANGED
@@ -177,6 +177,10 @@ class TierManager:
177
  """Add a new memory node. New memories are always HOT initially."""
178
  node.tier = "hot"
179
 
 
 
 
 
180
  # Phase 1: Add to HOT tier under lock (no I/O)
181
  victim_to_evict = None
182
  async with self.lock:
@@ -444,9 +448,9 @@ class TierManager:
444
  try:
445
  from qdrant_client import models
446
 
447
- # Unpack binary vector for Qdrant storage
448
  bits = np.unpackbits(node.hdv.data)
449
- vector = bits.astype(float).tolist()
450
 
451
  point = models.PointStruct(
452
  id=node.id,
 
177
  """Add a new memory node. New memories are always HOT initially."""
178
  node.tier = "hot"
179
 
180
+ # Delta 67.4: Ensure mutual exclusion.
181
+ # If the node exists in WARM, remove it before adding to HOT.
182
+ await self._delete_from_warm(node.id)
183
+
184
  # Phase 1: Add to HOT tier under lock (no I/O)
185
  victim_to_evict = None
186
  async with self.lock:
 
448
  try:
449
  from qdrant_client import models
450
 
451
+ # Unpack binary vector for Qdrant storage (Bipolar Phase 4.5)
452
  bits = np.unpackbits(node.hdv.data)
453
+ vector = (bits.astype(float) * 2.0 - 1.0).tolist()
454
 
455
  point = models.PointStruct(
456
  id=node.id,
sync_qdrant.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import asyncio
3
+ import json
4
+ import os
5
+ from mnemocore.core.engine import HAIMEngine
6
+ from mnemocore.core.config import get_config
7
+ from mnemocore.core.container import build_container
8
+ from mnemocore.core.tier_manager import TierManager
9
+
10
+ async def sync_qdrant():
11
+ config = get_config()
12
+ container = build_container(config)
13
+
14
+ # Initialize TierManager with Qdrant
15
+ tier_manager = TierManager(config=config, qdrant_store=container.qdrant_store)
16
+
17
+ engine = HAIMEngine(
18
+ config=config,
19
+ tier_manager=tier_manager,
20
+ working_memory=container.working_memory,
21
+ episodic_store=container.episodic_store,
22
+ semantic_store=container.semantic_store
23
+ )
24
+ await engine.initialize()
25
+
26
+ print(f"Engine initialized. Memories in HOT: {len(engine.tier_manager.hot)}")
27
+
28
+ # Force sync from memory.jsonl if HOT is empty
29
+ if len(engine.tier_manager.hot) == 0:
30
+ print("HOT is empty, reloading from legacy log...")
31
+ await engine._load_legacy_if_needed()
32
+ print(f"Memories after reload: {len(engine.tier_manager.hot)}")
33
+
34
+ # Consolidation will move them to WARM (Qdrant)
35
+ # But we can also just call _save_to_warm manually for all nodes
36
+ print("Syncing nodes to Qdrant...")
37
+ count = 0
38
+ for node_id, node in list(engine.tier_manager.hot.items()):
39
+ await engine.tier_manager._save_to_warm(node)
40
+ count += 1
41
+ if count % 100 == 0:
42
+ print(f"Synced {count} nodes...")
43
+
44
+ print(f"Total synced: {count}")
45
+ await engine.close()
46
+
47
+ if __name__ == "__main__":
48
+ import sys
49
+ sys.path.append(os.path.join(os.getcwd(), "src"))
50
+ asyncio.run(sync_qdrant())
test_qdrant_scores.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import asyncio
3
+ import numpy as np
4
+ from mnemocore.core.qdrant_store import QdrantStore
5
+ from mnemocore.core.config import get_config
6
+
7
+ async def test_qdrant_scores():
8
+ config = get_config()
9
+ store = QdrantStore(
10
+ url=config.qdrant.url,
11
+ api_key=None,
12
+ dimensionality=config.dimensionality
13
+ )
14
+
15
+ print(f"Ensuring collections (Migration Check)...")
16
+ await store.ensure_collections()
17
+
18
+ print(f"Searching {config.qdrant.collection_warm}...")
19
+ try:
20
+ info = await store.get_collection_info(config.qdrant.collection_warm)
21
+ print(f"Collection Info: {info}")
22
+
23
+ # Get one point first to have a valid vector
24
+ scroll_res = await store.scroll(config.qdrant.collection_warm, limit=1, with_vectors=True)
25
+ points = scroll_res[0]
26
+
27
+ if not points:
28
+ print("No points found in collection.")
29
+ return
30
+
31
+ target_vec = points[0].vector
32
+ target_id = points[0].id
33
+ print(f"Target Point: ID={target_id}")
34
+
35
+ # Test basic search without search_params
36
+ response = await store.client.query_points(
37
+ collection_name=config.qdrant.collection_warm,
38
+ query=target_vec,
39
+ limit=3
40
+ )
41
+ hits = response.points
42
+ print(f"Basic Search Hits count: {len(hits)}")
43
+ for i, hit in enumerate(hits):
44
+ print(f"Hit {i}: ID={hit.id}, Score={hit.score}")
45
+
46
+ hits = await store.search(config.qdrant.collection_warm, target_vec, limit=3)
47
+ print(f"Store Search Hits count: {len(hits)}")
48
+ for i, hit in enumerate(hits):
49
+ print(f"Hit {i}: ID={hit.id}, Score={hit.score}")
50
+ except Exception as e:
51
+ import traceback
52
+ traceback.print_exc()
53
+ print(f"Error: {e}")
54
+ finally:
55
+ await store.close()
56
+
57
+ if __name__ == "__main__":
58
+ import os
59
+ import sys
60
+ sys.path.append(os.path.join(os.getcwd(), "src"))
61
+ asyncio.run(test_qdrant_scores())