alma-memory 0.4.0__py3-none-any.whl → 0.5.1__py3-none-any.whl
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.
- alma/__init__.py +121 -45
- alma/confidence/__init__.py +1 -1
- alma/confidence/engine.py +92 -58
- alma/confidence/types.py +34 -14
- alma/config/loader.py +3 -2
- alma/consolidation/__init__.py +23 -0
- alma/consolidation/engine.py +678 -0
- alma/consolidation/prompts.py +84 -0
- alma/core.py +136 -28
- alma/domains/__init__.py +6 -6
- alma/domains/factory.py +12 -9
- alma/domains/schemas.py +17 -3
- alma/domains/types.py +8 -4
- alma/events/__init__.py +75 -0
- alma/events/emitter.py +284 -0
- alma/events/storage_mixin.py +246 -0
- alma/events/types.py +126 -0
- alma/events/webhook.py +425 -0
- alma/exceptions.py +49 -0
- alma/extraction/__init__.py +31 -0
- alma/extraction/auto_learner.py +265 -0
- alma/extraction/extractor.py +420 -0
- alma/graph/__init__.py +106 -0
- alma/graph/backends/__init__.py +32 -0
- alma/graph/backends/kuzu.py +624 -0
- alma/graph/backends/memgraph.py +432 -0
- alma/graph/backends/memory.py +236 -0
- alma/graph/backends/neo4j.py +417 -0
- alma/graph/base.py +159 -0
- alma/graph/extraction.py +198 -0
- alma/graph/store.py +860 -0
- alma/harness/__init__.py +4 -4
- alma/harness/base.py +18 -9
- alma/harness/domains.py +27 -11
- alma/initializer/__init__.py +1 -1
- alma/initializer/initializer.py +51 -43
- alma/initializer/types.py +25 -17
- alma/integration/__init__.py +9 -9
- alma/integration/claude_agents.py +32 -20
- alma/integration/helena.py +32 -22
- alma/integration/victor.py +57 -33
- alma/learning/__init__.py +27 -27
- alma/learning/forgetting.py +198 -148
- alma/learning/heuristic_extractor.py +40 -24
- alma/learning/protocols.py +65 -17
- alma/learning/validation.py +7 -2
- alma/mcp/__init__.py +4 -4
- alma/mcp/__main__.py +2 -1
- alma/mcp/resources.py +17 -16
- alma/mcp/server.py +102 -44
- alma/mcp/tools.py +180 -45
- alma/observability/__init__.py +84 -0
- alma/observability/config.py +302 -0
- alma/observability/logging.py +424 -0
- alma/observability/metrics.py +583 -0
- alma/observability/tracing.py +440 -0
- alma/progress/__init__.py +3 -3
- alma/progress/tracker.py +26 -20
- alma/progress/types.py +8 -12
- alma/py.typed +0 -0
- alma/retrieval/__init__.py +11 -11
- alma/retrieval/cache.py +20 -21
- alma/retrieval/embeddings.py +4 -4
- alma/retrieval/engine.py +179 -39
- alma/retrieval/scoring.py +73 -63
- alma/session/__init__.py +2 -2
- alma/session/manager.py +5 -5
- alma/session/types.py +5 -4
- alma/storage/__init__.py +70 -0
- alma/storage/azure_cosmos.py +414 -133
- alma/storage/base.py +215 -4
- alma/storage/chroma.py +1443 -0
- alma/storage/constants.py +103 -0
- alma/storage/file_based.py +59 -28
- alma/storage/migrations/__init__.py +21 -0
- alma/storage/migrations/base.py +321 -0
- alma/storage/migrations/runner.py +323 -0
- alma/storage/migrations/version_stores.py +337 -0
- alma/storage/migrations/versions/__init__.py +11 -0
- alma/storage/migrations/versions/v1_0_0.py +373 -0
- alma/storage/pinecone.py +1080 -0
- alma/storage/postgresql.py +1559 -0
- alma/storage/qdrant.py +1306 -0
- alma/storage/sqlite_local.py +504 -60
- alma/testing/__init__.py +46 -0
- alma/testing/factories.py +301 -0
- alma/testing/mocks.py +389 -0
- alma/types.py +62 -14
- alma_memory-0.5.1.dist-info/METADATA +939 -0
- alma_memory-0.5.1.dist-info/RECORD +93 -0
- {alma_memory-0.4.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +1 -1
- alma_memory-0.4.0.dist-info/METADATA +0 -488
- alma_memory-0.4.0.dist-info/RECORD +0 -52
- {alma_memory-0.4.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
alma/storage/sqlite_local.py
CHANGED
|
@@ -6,28 +6,31 @@ This is the recommended backend for local development and testing.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
-
import sqlite3
|
|
10
9
|
import logging
|
|
11
|
-
import
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from datetime import datetime, timezone
|
|
14
|
-
from typing import Optional, List, Dict, Any, Tuple
|
|
10
|
+
import sqlite3
|
|
15
11
|
from contextlib import contextmanager
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
16
15
|
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from alma.storage.base import StorageBackend
|
|
19
|
+
from alma.storage.constants import SQLITE_TABLE_NAMES, MemoryType
|
|
17
20
|
from alma.types import (
|
|
21
|
+
AntiPattern,
|
|
22
|
+
DomainKnowledge,
|
|
18
23
|
Heuristic,
|
|
19
24
|
Outcome,
|
|
20
25
|
UserPreference,
|
|
21
|
-
DomainKnowledge,
|
|
22
|
-
AntiPattern,
|
|
23
26
|
)
|
|
24
|
-
from alma.storage.base import StorageBackend
|
|
25
27
|
|
|
26
28
|
logger = logging.getLogger(__name__)
|
|
27
29
|
|
|
28
30
|
# Try to import FAISS, fall back to numpy-based search if not available
|
|
29
31
|
try:
|
|
30
32
|
import faiss
|
|
33
|
+
|
|
31
34
|
FAISS_AVAILABLE = True
|
|
32
35
|
except ImportError:
|
|
33
36
|
FAISS_AVAILABLE = False
|
|
@@ -54,6 +57,7 @@ class SQLiteStorage(StorageBackend):
|
|
|
54
57
|
self,
|
|
55
58
|
db_path: Path,
|
|
56
59
|
embedding_dim: int = 384, # Default for all-MiniLM-L6-v2
|
|
60
|
+
auto_migrate: bool = True,
|
|
57
61
|
):
|
|
58
62
|
"""
|
|
59
63
|
Initialize SQLite storage.
|
|
@@ -61,19 +65,29 @@ class SQLiteStorage(StorageBackend):
|
|
|
61
65
|
Args:
|
|
62
66
|
db_path: Path to SQLite database file
|
|
63
67
|
embedding_dim: Dimension of embedding vectors
|
|
68
|
+
auto_migrate: If True, automatically apply pending migrations on startup
|
|
64
69
|
"""
|
|
65
70
|
self.db_path = Path(db_path)
|
|
66
71
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
72
|
self.embedding_dim = embedding_dim
|
|
68
73
|
|
|
74
|
+
# Migration support (lazy-loaded)
|
|
75
|
+
self._migration_runner = None
|
|
76
|
+
self._version_store = None
|
|
77
|
+
|
|
69
78
|
# Initialize database
|
|
70
79
|
self._init_database()
|
|
71
80
|
|
|
72
81
|
# Initialize FAISS indices (one per memory type)
|
|
73
82
|
self._indices: Dict[str, Any] = {}
|
|
74
83
|
self._id_maps: Dict[str, List[str]] = {} # memory_type -> [memory_ids]
|
|
84
|
+
self._index_dirty: Dict[str, bool] = {} # Track which indexes need rebuilding
|
|
75
85
|
self._load_faiss_indices()
|
|
76
86
|
|
|
87
|
+
# Auto-migrate if enabled
|
|
88
|
+
if auto_migrate:
|
|
89
|
+
self._ensure_migrated()
|
|
90
|
+
|
|
77
91
|
@classmethod
|
|
78
92
|
def from_config(cls, config: Dict[str, Any]) -> "SQLiteStorage":
|
|
79
93
|
"""Create instance from configuration."""
|
|
@@ -149,6 +163,10 @@ class SQLiteStorage(StorageBackend):
|
|
|
149
163
|
"CREATE INDEX IF NOT EXISTS idx_outcomes_task_type "
|
|
150
164
|
"ON outcomes(project_id, agent, task_type)"
|
|
151
165
|
)
|
|
166
|
+
cursor.execute(
|
|
167
|
+
"CREATE INDEX IF NOT EXISTS idx_outcomes_timestamp "
|
|
168
|
+
"ON outcomes(project_id, timestamp)"
|
|
169
|
+
)
|
|
152
170
|
|
|
153
171
|
# User preferences table
|
|
154
172
|
cursor.execute("""
|
|
@@ -222,9 +240,14 @@ class SQLiteStorage(StorageBackend):
|
|
|
222
240
|
"ON embeddings(memory_type)"
|
|
223
241
|
)
|
|
224
242
|
|
|
225
|
-
def _load_faiss_indices(self):
|
|
226
|
-
"""Load or create FAISS indices for
|
|
227
|
-
|
|
243
|
+
def _load_faiss_indices(self, memory_types: Optional[List[str]] = None):
|
|
244
|
+
"""Load or create FAISS indices for specified memory types.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
memory_types: List of memory types to load. If None, loads all types.
|
|
248
|
+
"""
|
|
249
|
+
if memory_types is None:
|
|
250
|
+
memory_types = list(MemoryType.VECTOR_ENABLED)
|
|
228
251
|
|
|
229
252
|
for memory_type in memory_types:
|
|
230
253
|
if FAISS_AVAILABLE:
|
|
@@ -235,6 +258,7 @@ class SQLiteStorage(StorageBackend):
|
|
|
235
258
|
self._indices[memory_type] = []
|
|
236
259
|
|
|
237
260
|
self._id_maps[memory_type] = []
|
|
261
|
+
self._index_dirty[memory_type] = False # Mark as fresh after rebuild
|
|
238
262
|
|
|
239
263
|
# Load existing embeddings
|
|
240
264
|
with self._get_connection() as conn:
|
|
@@ -257,6 +281,19 @@ class SQLiteStorage(StorageBackend):
|
|
|
257
281
|
else:
|
|
258
282
|
self._indices[memory_type].append(embedding)
|
|
259
283
|
|
|
284
|
+
def _ensure_index_fresh(self, memory_type: str) -> None:
|
|
285
|
+
"""Rebuild index for a memory type if it has been marked dirty.
|
|
286
|
+
|
|
287
|
+
This implements lazy rebuilding - indexes are only rebuilt when
|
|
288
|
+
actually needed for search, not immediately on every delete.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
memory_type: The type of memory index to check/rebuild.
|
|
292
|
+
"""
|
|
293
|
+
if self._index_dirty.get(memory_type, False):
|
|
294
|
+
logger.debug(f"Rebuilding dirty index for {memory_type}")
|
|
295
|
+
self._load_faiss_indices([memory_type])
|
|
296
|
+
|
|
260
297
|
def _add_to_index(
|
|
261
298
|
self,
|
|
262
299
|
memory_type: str,
|
|
@@ -296,6 +333,9 @@ class SQLiteStorage(StorageBackend):
|
|
|
296
333
|
top_k: int,
|
|
297
334
|
) -> List[Tuple[str, float]]:
|
|
298
335
|
"""Search FAISS index for similar embeddings."""
|
|
336
|
+
# Ensure index is up-to-date before searching (lazy rebuild)
|
|
337
|
+
self._ensure_index_fresh(memory_type)
|
|
338
|
+
|
|
299
339
|
if not self._id_maps[memory_type]:
|
|
300
340
|
return []
|
|
301
341
|
|
|
@@ -304,10 +344,12 @@ class SQLiteStorage(StorageBackend):
|
|
|
304
344
|
if FAISS_AVAILABLE:
|
|
305
345
|
# Normalize for cosine similarity (IndexFlatIP)
|
|
306
346
|
faiss.normalize_L2(query)
|
|
307
|
-
scores, indices = self._indices[memory_type].search(
|
|
347
|
+
scores, indices = self._indices[memory_type].search(
|
|
348
|
+
query, min(top_k, len(self._id_maps[memory_type]))
|
|
349
|
+
)
|
|
308
350
|
|
|
309
351
|
results = []
|
|
310
|
-
for score, idx in zip(scores[0], indices[0]):
|
|
352
|
+
for score, idx in zip(scores[0], indices[0], strict=False):
|
|
311
353
|
if idx >= 0 and idx < len(self._id_maps[memory_type]):
|
|
312
354
|
results.append((self._id_maps[memory_type][idx], float(score)))
|
|
313
355
|
return results
|
|
@@ -354,14 +396,18 @@ class SQLiteStorage(StorageBackend):
|
|
|
354
396
|
heuristic.confidence,
|
|
355
397
|
heuristic.occurrence_count,
|
|
356
398
|
heuristic.success_count,
|
|
357
|
-
|
|
399
|
+
(
|
|
400
|
+
heuristic.last_validated.isoformat()
|
|
401
|
+
if heuristic.last_validated
|
|
402
|
+
else None
|
|
403
|
+
),
|
|
358
404
|
heuristic.created_at.isoformat() if heuristic.created_at else None,
|
|
359
405
|
json.dumps(heuristic.metadata) if heuristic.metadata else None,
|
|
360
406
|
),
|
|
361
407
|
)
|
|
362
408
|
|
|
363
409
|
# Add embedding to index
|
|
364
|
-
self._add_to_index(
|
|
410
|
+
self._add_to_index(MemoryType.HEURISTICS, heuristic.id, heuristic.embedding)
|
|
365
411
|
logger.debug(f"Saved heuristic: {heuristic.id}")
|
|
366
412
|
return heuristic.id
|
|
367
413
|
|
|
@@ -393,7 +439,7 @@ class SQLiteStorage(StorageBackend):
|
|
|
393
439
|
)
|
|
394
440
|
|
|
395
441
|
# Add embedding to index
|
|
396
|
-
self._add_to_index(
|
|
442
|
+
self._add_to_index(MemoryType.OUTCOMES, outcome.id, outcome.embedding)
|
|
397
443
|
logger.debug(f"Saved outcome: {outcome.id}")
|
|
398
444
|
return outcome.id
|
|
399
445
|
|
|
@@ -439,13 +485,19 @@ class SQLiteStorage(StorageBackend):
|
|
|
439
485
|
knowledge.fact,
|
|
440
486
|
knowledge.source,
|
|
441
487
|
knowledge.confidence,
|
|
442
|
-
|
|
488
|
+
(
|
|
489
|
+
knowledge.last_verified.isoformat()
|
|
490
|
+
if knowledge.last_verified
|
|
491
|
+
else None
|
|
492
|
+
),
|
|
443
493
|
json.dumps(knowledge.metadata) if knowledge.metadata else None,
|
|
444
494
|
),
|
|
445
495
|
)
|
|
446
496
|
|
|
447
497
|
# Add embedding to index
|
|
448
|
-
self._add_to_index(
|
|
498
|
+
self._add_to_index(
|
|
499
|
+
MemoryType.DOMAIN_KNOWLEDGE, knowledge.id, knowledge.embedding
|
|
500
|
+
)
|
|
449
501
|
logger.debug(f"Saved domain knowledge: {knowledge.id}")
|
|
450
502
|
return knowledge.id
|
|
451
503
|
|
|
@@ -468,17 +520,150 @@ class SQLiteStorage(StorageBackend):
|
|
|
468
520
|
anti_pattern.why_bad,
|
|
469
521
|
anti_pattern.better_alternative,
|
|
470
522
|
anti_pattern.occurrence_count,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
523
|
+
(
|
|
524
|
+
anti_pattern.last_seen.isoformat()
|
|
525
|
+
if anti_pattern.last_seen
|
|
526
|
+
else None
|
|
527
|
+
),
|
|
528
|
+
(
|
|
529
|
+
anti_pattern.created_at.isoformat()
|
|
530
|
+
if anti_pattern.created_at
|
|
531
|
+
else None
|
|
532
|
+
),
|
|
533
|
+
(
|
|
534
|
+
json.dumps(anti_pattern.metadata)
|
|
535
|
+
if anti_pattern.metadata
|
|
536
|
+
else None
|
|
537
|
+
),
|
|
474
538
|
),
|
|
475
539
|
)
|
|
476
540
|
|
|
477
541
|
# Add embedding to index
|
|
478
|
-
self._add_to_index(
|
|
542
|
+
self._add_to_index(
|
|
543
|
+
MemoryType.ANTI_PATTERNS, anti_pattern.id, anti_pattern.embedding
|
|
544
|
+
)
|
|
479
545
|
logger.debug(f"Saved anti-pattern: {anti_pattern.id}")
|
|
480
546
|
return anti_pattern.id
|
|
481
547
|
|
|
548
|
+
# ==================== BATCH WRITE OPERATIONS ====================
|
|
549
|
+
|
|
550
|
+
def save_heuristics(self, heuristics: List[Heuristic]) -> List[str]:
|
|
551
|
+
"""Save multiple heuristics in a batch using executemany."""
|
|
552
|
+
if not heuristics:
|
|
553
|
+
return []
|
|
554
|
+
|
|
555
|
+
with self._get_connection() as conn:
|
|
556
|
+
cursor = conn.cursor()
|
|
557
|
+
cursor.executemany(
|
|
558
|
+
"""
|
|
559
|
+
INSERT OR REPLACE INTO heuristics
|
|
560
|
+
(id, agent, project_id, condition, strategy, confidence,
|
|
561
|
+
occurrence_count, success_count, last_validated, created_at, metadata)
|
|
562
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
563
|
+
""",
|
|
564
|
+
[
|
|
565
|
+
(
|
|
566
|
+
h.id,
|
|
567
|
+
h.agent,
|
|
568
|
+
h.project_id,
|
|
569
|
+
h.condition,
|
|
570
|
+
h.strategy,
|
|
571
|
+
h.confidence,
|
|
572
|
+
h.occurrence_count,
|
|
573
|
+
h.success_count,
|
|
574
|
+
h.last_validated.isoformat() if h.last_validated else None,
|
|
575
|
+
h.created_at.isoformat() if h.created_at else None,
|
|
576
|
+
json.dumps(h.metadata) if h.metadata else None,
|
|
577
|
+
)
|
|
578
|
+
for h in heuristics
|
|
579
|
+
],
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# Add embeddings to index
|
|
583
|
+
for h in heuristics:
|
|
584
|
+
self._add_to_index(MemoryType.HEURISTICS, h.id, h.embedding)
|
|
585
|
+
|
|
586
|
+
logger.debug(f"Batch saved {len(heuristics)} heuristics")
|
|
587
|
+
return [h.id for h in heuristics]
|
|
588
|
+
|
|
589
|
+
def save_outcomes(self, outcomes: List[Outcome]) -> List[str]:
|
|
590
|
+
"""Save multiple outcomes in a batch using executemany."""
|
|
591
|
+
if not outcomes:
|
|
592
|
+
return []
|
|
593
|
+
|
|
594
|
+
with self._get_connection() as conn:
|
|
595
|
+
cursor = conn.cursor()
|
|
596
|
+
cursor.executemany(
|
|
597
|
+
"""
|
|
598
|
+
INSERT OR REPLACE INTO outcomes
|
|
599
|
+
(id, agent, project_id, task_type, task_description, success,
|
|
600
|
+
strategy_used, duration_ms, error_message, user_feedback, timestamp, metadata)
|
|
601
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
602
|
+
""",
|
|
603
|
+
[
|
|
604
|
+
(
|
|
605
|
+
o.id,
|
|
606
|
+
o.agent,
|
|
607
|
+
o.project_id,
|
|
608
|
+
o.task_type,
|
|
609
|
+
o.task_description,
|
|
610
|
+
1 if o.success else 0,
|
|
611
|
+
o.strategy_used,
|
|
612
|
+
o.duration_ms,
|
|
613
|
+
o.error_message,
|
|
614
|
+
o.user_feedback,
|
|
615
|
+
o.timestamp.isoformat() if o.timestamp else None,
|
|
616
|
+
json.dumps(o.metadata) if o.metadata else None,
|
|
617
|
+
)
|
|
618
|
+
for o in outcomes
|
|
619
|
+
],
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# Add embeddings to index
|
|
623
|
+
for o in outcomes:
|
|
624
|
+
self._add_to_index(MemoryType.OUTCOMES, o.id, o.embedding)
|
|
625
|
+
|
|
626
|
+
logger.debug(f"Batch saved {len(outcomes)} outcomes")
|
|
627
|
+
return [o.id for o in outcomes]
|
|
628
|
+
|
|
629
|
+
def save_domain_knowledge_batch(
|
|
630
|
+
self, knowledge_items: List[DomainKnowledge]
|
|
631
|
+
) -> List[str]:
|
|
632
|
+
"""Save multiple domain knowledge items in a batch using executemany."""
|
|
633
|
+
if not knowledge_items:
|
|
634
|
+
return []
|
|
635
|
+
|
|
636
|
+
with self._get_connection() as conn:
|
|
637
|
+
cursor = conn.cursor()
|
|
638
|
+
cursor.executemany(
|
|
639
|
+
"""
|
|
640
|
+
INSERT OR REPLACE INTO domain_knowledge
|
|
641
|
+
(id, agent, project_id, domain, fact, source, confidence, last_verified, metadata)
|
|
642
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
643
|
+
""",
|
|
644
|
+
[
|
|
645
|
+
(
|
|
646
|
+
k.id,
|
|
647
|
+
k.agent,
|
|
648
|
+
k.project_id,
|
|
649
|
+
k.domain,
|
|
650
|
+
k.fact,
|
|
651
|
+
k.source,
|
|
652
|
+
k.confidence,
|
|
653
|
+
k.last_verified.isoformat() if k.last_verified else None,
|
|
654
|
+
json.dumps(k.metadata) if k.metadata else None,
|
|
655
|
+
)
|
|
656
|
+
for k in knowledge_items
|
|
657
|
+
],
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
# Add embeddings to index
|
|
661
|
+
for k in knowledge_items:
|
|
662
|
+
self._add_to_index(MemoryType.DOMAIN_KNOWLEDGE, k.id, k.embedding)
|
|
663
|
+
|
|
664
|
+
logger.debug(f"Batch saved {len(knowledge_items)} domain knowledge items")
|
|
665
|
+
return [k.id for k in knowledge_items]
|
|
666
|
+
|
|
482
667
|
# ==================== READ OPERATIONS ====================
|
|
483
668
|
|
|
484
669
|
def get_heuristics(
|
|
@@ -493,7 +678,9 @@ class SQLiteStorage(StorageBackend):
|
|
|
493
678
|
# If embedding provided, use vector search to get candidate IDs
|
|
494
679
|
candidate_ids = None
|
|
495
680
|
if embedding:
|
|
496
|
-
search_results = self._search_index(
|
|
681
|
+
search_results = self._search_index(
|
|
682
|
+
MemoryType.HEURISTICS, embedding, top_k * 2
|
|
683
|
+
)
|
|
497
684
|
candidate_ids = [id for id, _ in search_results]
|
|
498
685
|
|
|
499
686
|
with self._get_connection() as conn:
|
|
@@ -531,7 +718,9 @@ class SQLiteStorage(StorageBackend):
|
|
|
531
718
|
"""Get outcomes with optional vector search."""
|
|
532
719
|
candidate_ids = None
|
|
533
720
|
if embedding:
|
|
534
|
-
search_results = self._search_index(
|
|
721
|
+
search_results = self._search_index(
|
|
722
|
+
MemoryType.OUTCOMES, embedding, top_k * 2
|
|
723
|
+
)
|
|
535
724
|
candidate_ids = [id for id, _ in search_results]
|
|
536
725
|
|
|
537
726
|
with self._get_connection() as conn:
|
|
@@ -596,7 +785,9 @@ class SQLiteStorage(StorageBackend):
|
|
|
596
785
|
"""Get domain knowledge with optional vector search."""
|
|
597
786
|
candidate_ids = None
|
|
598
787
|
if embedding:
|
|
599
|
-
search_results = self._search_index(
|
|
788
|
+
search_results = self._search_index(
|
|
789
|
+
MemoryType.DOMAIN_KNOWLEDGE, embedding, top_k * 2
|
|
790
|
+
)
|
|
600
791
|
candidate_ids = [id for id, _ in search_results]
|
|
601
792
|
|
|
602
793
|
with self._get_connection() as conn:
|
|
@@ -636,7 +827,9 @@ class SQLiteStorage(StorageBackend):
|
|
|
636
827
|
"""Get anti-patterns with optional vector search."""
|
|
637
828
|
candidate_ids = None
|
|
638
829
|
if embedding:
|
|
639
|
-
search_results = self._search_index(
|
|
830
|
+
search_results = self._search_index(
|
|
831
|
+
MemoryType.ANTI_PATTERNS, embedding, top_k * 2
|
|
832
|
+
)
|
|
640
833
|
candidate_ids = [id for id, _ in search_results]
|
|
641
834
|
|
|
642
835
|
with self._get_connection() as conn:
|
|
@@ -662,6 +855,175 @@ class SQLiteStorage(StorageBackend):
|
|
|
662
855
|
|
|
663
856
|
return [self._row_to_anti_pattern(row) for row in rows]
|
|
664
857
|
|
|
858
|
+
# ==================== MULTI-AGENT MEMORY SHARING ====================
|
|
859
|
+
|
|
860
|
+
def get_heuristics_for_agents(
|
|
861
|
+
self,
|
|
862
|
+
project_id: str,
|
|
863
|
+
agents: List[str],
|
|
864
|
+
embedding: Optional[List[float]] = None,
|
|
865
|
+
top_k: int = 5,
|
|
866
|
+
min_confidence: float = 0.0,
|
|
867
|
+
) -> List[Heuristic]:
|
|
868
|
+
"""Get heuristics from multiple agents using optimized IN query."""
|
|
869
|
+
if not agents:
|
|
870
|
+
return []
|
|
871
|
+
|
|
872
|
+
candidate_ids = None
|
|
873
|
+
if embedding:
|
|
874
|
+
search_results = self._search_index(
|
|
875
|
+
MemoryType.HEURISTICS, embedding, top_k * 2 * len(agents)
|
|
876
|
+
)
|
|
877
|
+
candidate_ids = [id for id, _ in search_results]
|
|
878
|
+
|
|
879
|
+
with self._get_connection() as conn:
|
|
880
|
+
cursor = conn.cursor()
|
|
881
|
+
|
|
882
|
+
placeholders = ",".join("?" * len(agents))
|
|
883
|
+
query = f"SELECT * FROM heuristics WHERE project_id = ? AND confidence >= ? AND agent IN ({placeholders})"
|
|
884
|
+
params: List[Any] = [project_id, min_confidence] + list(agents)
|
|
885
|
+
|
|
886
|
+
if candidate_ids is not None:
|
|
887
|
+
id_placeholders = ",".join("?" * len(candidate_ids))
|
|
888
|
+
query += f" AND id IN ({id_placeholders})"
|
|
889
|
+
params.extend(candidate_ids)
|
|
890
|
+
|
|
891
|
+
query += " ORDER BY confidence DESC LIMIT ?"
|
|
892
|
+
params.append(top_k * len(agents))
|
|
893
|
+
|
|
894
|
+
cursor.execute(query, params)
|
|
895
|
+
rows = cursor.fetchall()
|
|
896
|
+
|
|
897
|
+
return [self._row_to_heuristic(row) for row in rows]
|
|
898
|
+
|
|
899
|
+
def get_outcomes_for_agents(
|
|
900
|
+
self,
|
|
901
|
+
project_id: str,
|
|
902
|
+
agents: List[str],
|
|
903
|
+
task_type: Optional[str] = None,
|
|
904
|
+
embedding: Optional[List[float]] = None,
|
|
905
|
+
top_k: int = 5,
|
|
906
|
+
success_only: bool = False,
|
|
907
|
+
) -> List[Outcome]:
|
|
908
|
+
"""Get outcomes from multiple agents using optimized IN query."""
|
|
909
|
+
if not agents:
|
|
910
|
+
return []
|
|
911
|
+
|
|
912
|
+
candidate_ids = None
|
|
913
|
+
if embedding:
|
|
914
|
+
search_results = self._search_index(
|
|
915
|
+
MemoryType.OUTCOMES, embedding, top_k * 2 * len(agents)
|
|
916
|
+
)
|
|
917
|
+
candidate_ids = [id for id, _ in search_results]
|
|
918
|
+
|
|
919
|
+
with self._get_connection() as conn:
|
|
920
|
+
cursor = conn.cursor()
|
|
921
|
+
|
|
922
|
+
placeholders = ",".join("?" * len(agents))
|
|
923
|
+
query = f"SELECT * FROM outcomes WHERE project_id = ? AND agent IN ({placeholders})"
|
|
924
|
+
params: List[Any] = [project_id] + list(agents)
|
|
925
|
+
|
|
926
|
+
if task_type:
|
|
927
|
+
query += " AND task_type = ?"
|
|
928
|
+
params.append(task_type)
|
|
929
|
+
|
|
930
|
+
if success_only:
|
|
931
|
+
query += " AND success = 1"
|
|
932
|
+
|
|
933
|
+
if candidate_ids is not None:
|
|
934
|
+
id_placeholders = ",".join("?" * len(candidate_ids))
|
|
935
|
+
query += f" AND id IN ({id_placeholders})"
|
|
936
|
+
params.extend(candidate_ids)
|
|
937
|
+
|
|
938
|
+
query += " ORDER BY timestamp DESC LIMIT ?"
|
|
939
|
+
params.append(top_k * len(agents))
|
|
940
|
+
|
|
941
|
+
cursor.execute(query, params)
|
|
942
|
+
rows = cursor.fetchall()
|
|
943
|
+
|
|
944
|
+
return [self._row_to_outcome(row) for row in rows]
|
|
945
|
+
|
|
946
|
+
def get_domain_knowledge_for_agents(
|
|
947
|
+
self,
|
|
948
|
+
project_id: str,
|
|
949
|
+
agents: List[str],
|
|
950
|
+
domain: Optional[str] = None,
|
|
951
|
+
embedding: Optional[List[float]] = None,
|
|
952
|
+
top_k: int = 5,
|
|
953
|
+
) -> List[DomainKnowledge]:
|
|
954
|
+
"""Get domain knowledge from multiple agents using optimized IN query."""
|
|
955
|
+
if not agents:
|
|
956
|
+
return []
|
|
957
|
+
|
|
958
|
+
candidate_ids = None
|
|
959
|
+
if embedding:
|
|
960
|
+
search_results = self._search_index(
|
|
961
|
+
MemoryType.DOMAIN_KNOWLEDGE, embedding, top_k * 2 * len(agents)
|
|
962
|
+
)
|
|
963
|
+
candidate_ids = [id for id, _ in search_results]
|
|
964
|
+
|
|
965
|
+
with self._get_connection() as conn:
|
|
966
|
+
cursor = conn.cursor()
|
|
967
|
+
|
|
968
|
+
placeholders = ",".join("?" * len(agents))
|
|
969
|
+
query = f"SELECT * FROM domain_knowledge WHERE project_id = ? AND agent IN ({placeholders})"
|
|
970
|
+
params: List[Any] = [project_id] + list(agents)
|
|
971
|
+
|
|
972
|
+
if domain:
|
|
973
|
+
query += " AND domain = ?"
|
|
974
|
+
params.append(domain)
|
|
975
|
+
|
|
976
|
+
if candidate_ids is not None:
|
|
977
|
+
id_placeholders = ",".join("?" * len(candidate_ids))
|
|
978
|
+
query += f" AND id IN ({id_placeholders})"
|
|
979
|
+
params.extend(candidate_ids)
|
|
980
|
+
|
|
981
|
+
query += " ORDER BY confidence DESC LIMIT ?"
|
|
982
|
+
params.append(top_k * len(agents))
|
|
983
|
+
|
|
984
|
+
cursor.execute(query, params)
|
|
985
|
+
rows = cursor.fetchall()
|
|
986
|
+
|
|
987
|
+
return [self._row_to_domain_knowledge(row) for row in rows]
|
|
988
|
+
|
|
989
|
+
def get_anti_patterns_for_agents(
|
|
990
|
+
self,
|
|
991
|
+
project_id: str,
|
|
992
|
+
agents: List[str],
|
|
993
|
+
embedding: Optional[List[float]] = None,
|
|
994
|
+
top_k: int = 5,
|
|
995
|
+
) -> List[AntiPattern]:
|
|
996
|
+
"""Get anti-patterns from multiple agents using optimized IN query."""
|
|
997
|
+
if not agents:
|
|
998
|
+
return []
|
|
999
|
+
|
|
1000
|
+
candidate_ids = None
|
|
1001
|
+
if embedding:
|
|
1002
|
+
search_results = self._search_index(
|
|
1003
|
+
MemoryType.ANTI_PATTERNS, embedding, top_k * 2 * len(agents)
|
|
1004
|
+
)
|
|
1005
|
+
candidate_ids = [id for id, _ in search_results]
|
|
1006
|
+
|
|
1007
|
+
with self._get_connection() as conn:
|
|
1008
|
+
cursor = conn.cursor()
|
|
1009
|
+
|
|
1010
|
+
placeholders = ",".join("?" * len(agents))
|
|
1011
|
+
query = f"SELECT * FROM anti_patterns WHERE project_id = ? AND agent IN ({placeholders})"
|
|
1012
|
+
params: List[Any] = [project_id] + list(agents)
|
|
1013
|
+
|
|
1014
|
+
if candidate_ids is not None:
|
|
1015
|
+
id_placeholders = ",".join("?" * len(candidate_ids))
|
|
1016
|
+
query += f" AND id IN ({id_placeholders})"
|
|
1017
|
+
params.extend(candidate_ids)
|
|
1018
|
+
|
|
1019
|
+
query += " ORDER BY occurrence_count DESC LIMIT ?"
|
|
1020
|
+
params.append(top_k * len(agents))
|
|
1021
|
+
|
|
1022
|
+
cursor.execute(query, params)
|
|
1023
|
+
rows = cursor.fetchall()
|
|
1024
|
+
|
|
1025
|
+
return [self._row_to_anti_pattern(row) for row in rows]
|
|
1026
|
+
|
|
665
1027
|
# ==================== UPDATE OPERATIONS ====================
|
|
666
1028
|
|
|
667
1029
|
def update_heuristic(
|
|
@@ -792,19 +1154,22 @@ class SQLiteStorage(StorageBackend):
|
|
|
792
1154
|
with self._get_connection() as conn:
|
|
793
1155
|
cursor = conn.cursor()
|
|
794
1156
|
|
|
795
|
-
|
|
796
|
-
for
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1157
|
+
# Use canonical memory types for stats
|
|
1158
|
+
for memory_type in MemoryType.ALL:
|
|
1159
|
+
if memory_type == MemoryType.PREFERENCES:
|
|
1160
|
+
# Preferences don't have project_id
|
|
1161
|
+
cursor.execute(
|
|
1162
|
+
f"SELECT COUNT(*) FROM {SQLITE_TABLE_NAMES[memory_type]}"
|
|
1163
|
+
)
|
|
1164
|
+
stats[f"{memory_type}_count"] = cursor.fetchone()[0]
|
|
1165
|
+
else:
|
|
1166
|
+
query = f"SELECT COUNT(*) FROM {SQLITE_TABLE_NAMES[memory_type]} WHERE project_id = ?"
|
|
1167
|
+
params: List[Any] = [project_id]
|
|
1168
|
+
if agent:
|
|
1169
|
+
query += " AND agent = ?"
|
|
1170
|
+
params.append(agent)
|
|
1171
|
+
cursor.execute(query, params)
|
|
1172
|
+
stats[f"{memory_type}_count"] = cursor.fetchone()[0]
|
|
808
1173
|
|
|
809
1174
|
# Embedding counts
|
|
810
1175
|
cursor.execute("SELECT COUNT(*) FROM embeddings")
|
|
@@ -944,17 +1309,16 @@ class SQLiteStorage(StorageBackend):
|
|
|
944
1309
|
with self._get_connection() as conn:
|
|
945
1310
|
# Also remove from embedding index
|
|
946
1311
|
conn.execute(
|
|
947
|
-
"DELETE FROM embeddings WHERE memory_type =
|
|
948
|
-
(heuristic_id
|
|
1312
|
+
"DELETE FROM embeddings WHERE memory_type = ? AND memory_id = ?",
|
|
1313
|
+
(MemoryType.HEURISTICS, heuristic_id),
|
|
949
1314
|
)
|
|
950
1315
|
cursor = conn.execute(
|
|
951
|
-
"DELETE FROM
|
|
1316
|
+
f"DELETE FROM {SQLITE_TABLE_NAMES[MemoryType.HEURISTICS]} WHERE id = ?",
|
|
952
1317
|
(heuristic_id,),
|
|
953
1318
|
)
|
|
954
1319
|
if cursor.rowcount > 0:
|
|
955
|
-
#
|
|
956
|
-
|
|
957
|
-
self._load_faiss_indices()
|
|
1320
|
+
# Mark index as dirty for lazy rebuild on next search
|
|
1321
|
+
self._index_dirty[MemoryType.HEURISTICS] = True
|
|
958
1322
|
return True
|
|
959
1323
|
return False
|
|
960
1324
|
|
|
@@ -963,16 +1327,16 @@ class SQLiteStorage(StorageBackend):
|
|
|
963
1327
|
with self._get_connection() as conn:
|
|
964
1328
|
# Also remove from embedding index
|
|
965
1329
|
conn.execute(
|
|
966
|
-
"DELETE FROM embeddings WHERE memory_type =
|
|
967
|
-
(outcome_id
|
|
1330
|
+
"DELETE FROM embeddings WHERE memory_type = ? AND memory_id = ?",
|
|
1331
|
+
(MemoryType.OUTCOMES, outcome_id),
|
|
968
1332
|
)
|
|
969
1333
|
cursor = conn.execute(
|
|
970
|
-
"DELETE FROM
|
|
1334
|
+
f"DELETE FROM {SQLITE_TABLE_NAMES[MemoryType.OUTCOMES]} WHERE id = ?",
|
|
971
1335
|
(outcome_id,),
|
|
972
1336
|
)
|
|
973
1337
|
if cursor.rowcount > 0:
|
|
974
|
-
|
|
975
|
-
|
|
1338
|
+
# Mark index as dirty for lazy rebuild on next search
|
|
1339
|
+
self._index_dirty[MemoryType.OUTCOMES] = True
|
|
976
1340
|
return True
|
|
977
1341
|
return False
|
|
978
1342
|
|
|
@@ -981,16 +1345,16 @@ class SQLiteStorage(StorageBackend):
|
|
|
981
1345
|
with self._get_connection() as conn:
|
|
982
1346
|
# Also remove from embedding index
|
|
983
1347
|
conn.execute(
|
|
984
|
-
"DELETE FROM embeddings WHERE memory_type =
|
|
985
|
-
(knowledge_id
|
|
1348
|
+
"DELETE FROM embeddings WHERE memory_type = ? AND memory_id = ?",
|
|
1349
|
+
(MemoryType.DOMAIN_KNOWLEDGE, knowledge_id),
|
|
986
1350
|
)
|
|
987
1351
|
cursor = conn.execute(
|
|
988
|
-
"DELETE FROM
|
|
1352
|
+
f"DELETE FROM {SQLITE_TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]} WHERE id = ?",
|
|
989
1353
|
(knowledge_id,),
|
|
990
1354
|
)
|
|
991
1355
|
if cursor.rowcount > 0:
|
|
992
|
-
|
|
993
|
-
|
|
1356
|
+
# Mark index as dirty for lazy rebuild on next search
|
|
1357
|
+
self._index_dirty[MemoryType.DOMAIN_KNOWLEDGE] = True
|
|
994
1358
|
return True
|
|
995
1359
|
return False
|
|
996
1360
|
|
|
@@ -999,15 +1363,95 @@ class SQLiteStorage(StorageBackend):
|
|
|
999
1363
|
with self._get_connection() as conn:
|
|
1000
1364
|
# Also remove from embedding index
|
|
1001
1365
|
conn.execute(
|
|
1002
|
-
"DELETE FROM embeddings WHERE memory_type =
|
|
1003
|
-
(anti_pattern_id
|
|
1366
|
+
"DELETE FROM embeddings WHERE memory_type = ? AND memory_id = ?",
|
|
1367
|
+
(MemoryType.ANTI_PATTERNS, anti_pattern_id),
|
|
1004
1368
|
)
|
|
1005
1369
|
cursor = conn.execute(
|
|
1006
|
-
"DELETE FROM
|
|
1370
|
+
f"DELETE FROM {SQLITE_TABLE_NAMES[MemoryType.ANTI_PATTERNS]} WHERE id = ?",
|
|
1007
1371
|
(anti_pattern_id,),
|
|
1008
1372
|
)
|
|
1009
1373
|
if cursor.rowcount > 0:
|
|
1010
|
-
|
|
1011
|
-
|
|
1374
|
+
# Mark index as dirty for lazy rebuild on next search
|
|
1375
|
+
self._index_dirty[MemoryType.ANTI_PATTERNS] = True
|
|
1012
1376
|
return True
|
|
1013
1377
|
return False
|
|
1378
|
+
|
|
1379
|
+
# ==================== MIGRATION SUPPORT ====================
|
|
1380
|
+
|
|
1381
|
+
def _get_version_store(self):
|
|
1382
|
+
"""Get or create the version store."""
|
|
1383
|
+
if self._version_store is None:
|
|
1384
|
+
from alma.storage.migrations.version_stores import SQLiteVersionStore
|
|
1385
|
+
|
|
1386
|
+
self._version_store = SQLiteVersionStore(self.db_path)
|
|
1387
|
+
return self._version_store
|
|
1388
|
+
|
|
1389
|
+
def _get_migration_runner(self):
|
|
1390
|
+
"""Get or create the migration runner."""
|
|
1391
|
+
if self._migration_runner is None:
|
|
1392
|
+
from alma.storage.migrations.runner import MigrationRunner
|
|
1393
|
+
from alma.storage.migrations.versions import v1_0_0 # noqa: F401
|
|
1394
|
+
|
|
1395
|
+
self._migration_runner = MigrationRunner(
|
|
1396
|
+
version_store=self._get_version_store(),
|
|
1397
|
+
backend="sqlite",
|
|
1398
|
+
)
|
|
1399
|
+
return self._migration_runner
|
|
1400
|
+
|
|
1401
|
+
def _ensure_migrated(self) -> None:
|
|
1402
|
+
"""Ensure database is migrated to latest version."""
|
|
1403
|
+
runner = self._get_migration_runner()
|
|
1404
|
+
if runner.needs_migration():
|
|
1405
|
+
with self._get_connection() as conn:
|
|
1406
|
+
applied = runner.migrate(conn)
|
|
1407
|
+
if applied:
|
|
1408
|
+
logger.info(f"Applied {len(applied)} migrations: {applied}")
|
|
1409
|
+
|
|
1410
|
+
def get_schema_version(self) -> Optional[str]:
|
|
1411
|
+
"""Get the current schema version."""
|
|
1412
|
+
return self._get_version_store().get_current_version()
|
|
1413
|
+
|
|
1414
|
+
def get_migration_status(self) -> Dict[str, Any]:
|
|
1415
|
+
"""Get migration status information."""
|
|
1416
|
+
runner = self._get_migration_runner()
|
|
1417
|
+
status = runner.get_status()
|
|
1418
|
+
status["migration_supported"] = True
|
|
1419
|
+
return status
|
|
1420
|
+
|
|
1421
|
+
def migrate(
|
|
1422
|
+
self,
|
|
1423
|
+
target_version: Optional[str] = None,
|
|
1424
|
+
dry_run: bool = False,
|
|
1425
|
+
) -> List[str]:
|
|
1426
|
+
"""
|
|
1427
|
+
Apply pending schema migrations.
|
|
1428
|
+
|
|
1429
|
+
Args:
|
|
1430
|
+
target_version: Optional target version (applies all if not specified)
|
|
1431
|
+
dry_run: If True, show what would be done without making changes
|
|
1432
|
+
|
|
1433
|
+
Returns:
|
|
1434
|
+
List of applied migration versions
|
|
1435
|
+
"""
|
|
1436
|
+
runner = self._get_migration_runner()
|
|
1437
|
+
with self._get_connection() as conn:
|
|
1438
|
+
return runner.migrate(conn, target_version=target_version, dry_run=dry_run)
|
|
1439
|
+
|
|
1440
|
+
def rollback(
|
|
1441
|
+
self,
|
|
1442
|
+
target_version: str,
|
|
1443
|
+
dry_run: bool = False,
|
|
1444
|
+
) -> List[str]:
|
|
1445
|
+
"""
|
|
1446
|
+
Roll back schema to a previous version.
|
|
1447
|
+
|
|
1448
|
+
Args:
|
|
1449
|
+
target_version: Version to roll back to
|
|
1450
|
+
dry_run: If True, show what would be done without making changes
|
|
1451
|
+
|
|
1452
|
+
Returns:
|
|
1453
|
+
List of rolled back migration versions
|
|
1454
|
+
"""
|
|
1455
|
+
runner = self._get_migration_runner()
|
|
1456
|
+
with self._get_connection() as conn:
|
|
1457
|
+
return runner.rollback(conn, target_version=target_version, dry_run=dry_run)
|