alma-memory 0.3.0__py3-none-any.whl → 0.5.0__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.
Files changed (77) hide show
  1. alma/__init__.py +99 -29
  2. alma/confidence/__init__.py +47 -0
  3. alma/confidence/engine.py +540 -0
  4. alma/confidence/types.py +351 -0
  5. alma/config/loader.py +3 -2
  6. alma/consolidation/__init__.py +23 -0
  7. alma/consolidation/engine.py +678 -0
  8. alma/consolidation/prompts.py +84 -0
  9. alma/core.py +15 -15
  10. alma/domains/__init__.py +6 -6
  11. alma/domains/factory.py +12 -9
  12. alma/domains/schemas.py +17 -3
  13. alma/domains/types.py +8 -4
  14. alma/events/__init__.py +75 -0
  15. alma/events/emitter.py +284 -0
  16. alma/events/storage_mixin.py +246 -0
  17. alma/events/types.py +126 -0
  18. alma/events/webhook.py +425 -0
  19. alma/exceptions.py +49 -0
  20. alma/extraction/__init__.py +31 -0
  21. alma/extraction/auto_learner.py +264 -0
  22. alma/extraction/extractor.py +420 -0
  23. alma/graph/__init__.py +81 -0
  24. alma/graph/backends/__init__.py +18 -0
  25. alma/graph/backends/memory.py +236 -0
  26. alma/graph/backends/neo4j.py +417 -0
  27. alma/graph/base.py +159 -0
  28. alma/graph/extraction.py +198 -0
  29. alma/graph/store.py +860 -0
  30. alma/harness/__init__.py +4 -4
  31. alma/harness/base.py +18 -9
  32. alma/harness/domains.py +27 -11
  33. alma/initializer/__init__.py +37 -0
  34. alma/initializer/initializer.py +418 -0
  35. alma/initializer/types.py +250 -0
  36. alma/integration/__init__.py +9 -9
  37. alma/integration/claude_agents.py +10 -10
  38. alma/integration/helena.py +32 -22
  39. alma/integration/victor.py +57 -33
  40. alma/learning/__init__.py +27 -27
  41. alma/learning/forgetting.py +198 -148
  42. alma/learning/heuristic_extractor.py +40 -24
  43. alma/learning/protocols.py +62 -14
  44. alma/learning/validation.py +7 -2
  45. alma/mcp/__init__.py +4 -4
  46. alma/mcp/__main__.py +2 -1
  47. alma/mcp/resources.py +17 -16
  48. alma/mcp/server.py +102 -44
  49. alma/mcp/tools.py +174 -37
  50. alma/progress/__init__.py +3 -3
  51. alma/progress/tracker.py +26 -20
  52. alma/progress/types.py +8 -12
  53. alma/py.typed +0 -0
  54. alma/retrieval/__init__.py +11 -11
  55. alma/retrieval/cache.py +20 -21
  56. alma/retrieval/embeddings.py +4 -4
  57. alma/retrieval/engine.py +114 -35
  58. alma/retrieval/scoring.py +73 -63
  59. alma/session/__init__.py +2 -2
  60. alma/session/manager.py +5 -5
  61. alma/session/types.py +5 -4
  62. alma/storage/__init__.py +41 -0
  63. alma/storage/azure_cosmos.py +107 -31
  64. alma/storage/base.py +157 -4
  65. alma/storage/chroma.py +1443 -0
  66. alma/storage/file_based.py +56 -20
  67. alma/storage/pinecone.py +1080 -0
  68. alma/storage/postgresql.py +1452 -0
  69. alma/storage/qdrant.py +1306 -0
  70. alma/storage/sqlite_local.py +376 -31
  71. alma/types.py +62 -14
  72. alma_memory-0.5.0.dist-info/METADATA +905 -0
  73. alma_memory-0.5.0.dist-info/RECORD +76 -0
  74. {alma_memory-0.3.0.dist-info → alma_memory-0.5.0.dist-info}/WHEEL +1 -1
  75. alma_memory-0.3.0.dist-info/METADATA +0 -438
  76. alma_memory-0.3.0.dist-info/RECORD +0 -46
  77. {alma_memory-0.3.0.dist-info → alma_memory-0.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1452 @@
1
+ """
2
+ ALMA PostgreSQL Storage Backend.
3
+
4
+ Production-ready storage using PostgreSQL with pgvector extension for
5
+ native vector similarity search. Supports connection pooling.
6
+
7
+ Recommended for:
8
+ - Customer deployments (Azure PostgreSQL, AWS RDS, etc.)
9
+ - Self-hosted production environments
10
+ - High-availability requirements
11
+ """
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ from contextlib import contextmanager
17
+ from datetime import datetime, timezone
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ # numpy is optional - only needed for fallback similarity when pgvector unavailable
21
+ try:
22
+ import numpy as np
23
+
24
+ NUMPY_AVAILABLE = True
25
+ except ImportError:
26
+ np = None # type: ignore
27
+ NUMPY_AVAILABLE = False
28
+
29
+ from alma.storage.base import StorageBackend
30
+ from alma.types import (
31
+ AntiPattern,
32
+ DomainKnowledge,
33
+ Heuristic,
34
+ Outcome,
35
+ UserPreference,
36
+ )
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ # Try to import psycopg (v3) with connection pooling
41
+ try:
42
+ from psycopg.rows import dict_row
43
+ from psycopg_pool import ConnectionPool
44
+
45
+ PSYCOPG_AVAILABLE = True
46
+ except ImportError:
47
+ PSYCOPG_AVAILABLE = False
48
+ logger.warning(
49
+ "psycopg not installed. Install with: pip install 'alma-memory[postgres]'"
50
+ )
51
+
52
+
53
+ class PostgreSQLStorage(StorageBackend):
54
+ """
55
+ PostgreSQL storage backend with pgvector support.
56
+
57
+ Uses native PostgreSQL vector operations for efficient similarity search.
58
+ Falls back to application-level cosine similarity if pgvector is not installed.
59
+
60
+ Database schema:
61
+ - alma_heuristics: id, agent, project_id, condition, strategy, ...
62
+ - alma_outcomes: id, agent, project_id, task_type, ...
63
+ - alma_preferences: id, user_id, category, preference, ...
64
+ - alma_domain_knowledge: id, agent, project_id, domain, fact, ...
65
+ - alma_anti_patterns: id, agent, project_id, pattern, ...
66
+
67
+ Vector search:
68
+ - Uses pgvector extension if available
69
+ - Embeddings stored as VECTOR type with cosine distance operator (<=>)
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ host: str,
75
+ port: int,
76
+ database: str,
77
+ user: str,
78
+ password: str,
79
+ embedding_dim: int = 384,
80
+ pool_size: int = 10,
81
+ schema: str = "public",
82
+ ssl_mode: str = "prefer",
83
+ ):
84
+ """
85
+ Initialize PostgreSQL storage.
86
+
87
+ Args:
88
+ host: Database host
89
+ port: Database port
90
+ database: Database name
91
+ user: Database user
92
+ password: Database password
93
+ embedding_dim: Dimension of embedding vectors
94
+ pool_size: Connection pool size
95
+ schema: Database schema (default: public)
96
+ ssl_mode: SSL mode (disable, allow, prefer, require, verify-ca, verify-full)
97
+ """
98
+ if not PSYCOPG_AVAILABLE:
99
+ raise ImportError(
100
+ "psycopg not installed. Install with: pip install 'alma-memory[postgres]'"
101
+ )
102
+
103
+ self.embedding_dim = embedding_dim
104
+ self.schema = schema
105
+ self._pgvector_available = False
106
+
107
+ # Build connection string
108
+ conninfo = (
109
+ f"host={host} port={port} dbname={database} "
110
+ f"user={user} password={password} sslmode={ssl_mode}"
111
+ )
112
+
113
+ # Create connection pool
114
+ self._pool = ConnectionPool(
115
+ conninfo=conninfo,
116
+ min_size=1,
117
+ max_size=pool_size,
118
+ kwargs={"row_factory": dict_row},
119
+ )
120
+
121
+ # Initialize database
122
+ self._init_database()
123
+
124
+ @classmethod
125
+ def from_config(cls, config: Dict[str, Any]) -> "PostgreSQLStorage":
126
+ """Create instance from configuration."""
127
+ pg_config = config.get("postgres", {})
128
+
129
+ # Support environment variable expansion
130
+ def get_value(key: str, default: Any = None) -> Any:
131
+ value = pg_config.get(key, default)
132
+ if (
133
+ isinstance(value, str)
134
+ and value.startswith("${")
135
+ and value.endswith("}")
136
+ ):
137
+ env_var = value[2:-1]
138
+ return os.environ.get(env_var, default)
139
+ return value
140
+
141
+ return cls(
142
+ host=get_value("host", "localhost"),
143
+ port=int(get_value("port", 5432)),
144
+ database=get_value("database", "alma_memory"),
145
+ user=get_value("user", "postgres"),
146
+ password=get_value("password", ""),
147
+ embedding_dim=int(config.get("embedding_dim", 384)),
148
+ pool_size=int(get_value("pool_size", 10)),
149
+ schema=get_value("schema", "public"),
150
+ ssl_mode=get_value("ssl_mode", "prefer"),
151
+ )
152
+
153
+ @contextmanager
154
+ def _get_connection(self):
155
+ """Get database connection from pool."""
156
+ with self._pool.connection() as conn:
157
+ yield conn
158
+
159
+ def _init_database(self):
160
+ """Initialize database schema and pgvector extension."""
161
+ with self._get_connection() as conn:
162
+ # Try to enable pgvector extension
163
+ try:
164
+ conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
165
+ conn.commit()
166
+ self._pgvector_available = True
167
+ logger.info("pgvector extension enabled")
168
+ except Exception as e:
169
+ conn.rollback() # Important: rollback to clear aborted transaction
170
+ logger.warning(f"pgvector not available: {e}. Using fallback search.")
171
+ self._pgvector_available = False
172
+
173
+ # Create tables
174
+ vector_type = (
175
+ f"VECTOR({self.embedding_dim})" if self._pgvector_available else "BYTEA"
176
+ )
177
+
178
+ # Heuristics table
179
+ conn.execute(f"""
180
+ CREATE TABLE IF NOT EXISTS {self.schema}.alma_heuristics (
181
+ id TEXT PRIMARY KEY,
182
+ agent TEXT NOT NULL,
183
+ project_id TEXT NOT NULL,
184
+ condition TEXT NOT NULL,
185
+ strategy TEXT NOT NULL,
186
+ confidence REAL DEFAULT 0.0,
187
+ occurrence_count INTEGER DEFAULT 0,
188
+ success_count INTEGER DEFAULT 0,
189
+ last_validated TIMESTAMPTZ,
190
+ created_at TIMESTAMPTZ DEFAULT NOW(),
191
+ metadata JSONB,
192
+ embedding {vector_type}
193
+ )
194
+ """)
195
+ conn.execute(f"""
196
+ CREATE INDEX IF NOT EXISTS idx_heuristics_project_agent
197
+ ON {self.schema}.alma_heuristics(project_id, agent)
198
+ """)
199
+
200
+ # Outcomes table
201
+ conn.execute(f"""
202
+ CREATE TABLE IF NOT EXISTS {self.schema}.alma_outcomes (
203
+ id TEXT PRIMARY KEY,
204
+ agent TEXT NOT NULL,
205
+ project_id TEXT NOT NULL,
206
+ task_type TEXT,
207
+ task_description TEXT NOT NULL,
208
+ success BOOLEAN DEFAULT FALSE,
209
+ strategy_used TEXT,
210
+ duration_ms INTEGER,
211
+ error_message TEXT,
212
+ user_feedback TEXT,
213
+ timestamp TIMESTAMPTZ DEFAULT NOW(),
214
+ metadata JSONB,
215
+ embedding {vector_type}
216
+ )
217
+ """)
218
+ conn.execute(f"""
219
+ CREATE INDEX IF NOT EXISTS idx_outcomes_project_agent
220
+ ON {self.schema}.alma_outcomes(project_id, agent)
221
+ """)
222
+ conn.execute(f"""
223
+ CREATE INDEX IF NOT EXISTS idx_outcomes_task_type
224
+ ON {self.schema}.alma_outcomes(project_id, agent, task_type)
225
+ """)
226
+ conn.execute(f"""
227
+ CREATE INDEX IF NOT EXISTS idx_outcomes_timestamp
228
+ ON {self.schema}.alma_outcomes(project_id, timestamp DESC)
229
+ """)
230
+
231
+ # User preferences table
232
+ conn.execute(f"""
233
+ CREATE TABLE IF NOT EXISTS {self.schema}.alma_preferences (
234
+ id TEXT PRIMARY KEY,
235
+ user_id TEXT NOT NULL,
236
+ category TEXT,
237
+ preference TEXT NOT NULL,
238
+ source TEXT,
239
+ confidence REAL DEFAULT 1.0,
240
+ timestamp TIMESTAMPTZ DEFAULT NOW(),
241
+ metadata JSONB
242
+ )
243
+ """)
244
+ conn.execute(f"""
245
+ CREATE INDEX IF NOT EXISTS idx_preferences_user
246
+ ON {self.schema}.alma_preferences(user_id)
247
+ """)
248
+
249
+ # Domain knowledge table
250
+ conn.execute(f"""
251
+ CREATE TABLE IF NOT EXISTS {self.schema}.alma_domain_knowledge (
252
+ id TEXT PRIMARY KEY,
253
+ agent TEXT NOT NULL,
254
+ project_id TEXT NOT NULL,
255
+ domain TEXT,
256
+ fact TEXT NOT NULL,
257
+ source TEXT,
258
+ confidence REAL DEFAULT 1.0,
259
+ last_verified TIMESTAMPTZ DEFAULT NOW(),
260
+ metadata JSONB,
261
+ embedding {vector_type}
262
+ )
263
+ """)
264
+ conn.execute(f"""
265
+ CREATE INDEX IF NOT EXISTS idx_domain_knowledge_project_agent
266
+ ON {self.schema}.alma_domain_knowledge(project_id, agent)
267
+ """)
268
+
269
+ # Anti-patterns table
270
+ conn.execute(f"""
271
+ CREATE TABLE IF NOT EXISTS {self.schema}.alma_anti_patterns (
272
+ id TEXT PRIMARY KEY,
273
+ agent TEXT NOT NULL,
274
+ project_id TEXT NOT NULL,
275
+ pattern TEXT NOT NULL,
276
+ why_bad TEXT,
277
+ better_alternative TEXT,
278
+ occurrence_count INTEGER DEFAULT 1,
279
+ last_seen TIMESTAMPTZ DEFAULT NOW(),
280
+ created_at TIMESTAMPTZ DEFAULT NOW(),
281
+ metadata JSONB,
282
+ embedding {vector_type}
283
+ )
284
+ """)
285
+ conn.execute(f"""
286
+ CREATE INDEX IF NOT EXISTS idx_anti_patterns_project_agent
287
+ ON {self.schema}.alma_anti_patterns(project_id, agent)
288
+ """)
289
+
290
+ # Create vector indexes if pgvector available
291
+ # Using HNSW instead of IVFFlat because HNSW can be built on empty tables
292
+ # IVFFlat requires existing data to build, which causes silent failures on fresh databases
293
+ if self._pgvector_available:
294
+ for table in [
295
+ "alma_heuristics",
296
+ "alma_outcomes",
297
+ "alma_domain_knowledge",
298
+ "alma_anti_patterns",
299
+ ]:
300
+ try:
301
+ conn.execute(f"""
302
+ CREATE INDEX IF NOT EXISTS idx_{table}_embedding
303
+ ON {self.schema}.{table}
304
+ USING hnsw (embedding vector_cosine_ops)
305
+ WITH (m = 16, ef_construction = 64)
306
+ """)
307
+ except Exception as e:
308
+ logger.warning(f"Failed to create HNSW index for {table}: {e}")
309
+
310
+ conn.commit()
311
+
312
+ def _embedding_to_db(self, embedding: Optional[List[float]]) -> Any:
313
+ """Convert embedding to database format."""
314
+ if embedding is None:
315
+ return None
316
+ if self._pgvector_available:
317
+ # pgvector expects string format: '[1.0, 2.0, 3.0]'
318
+ return f"[{','.join(str(x) for x in embedding)}]"
319
+ else:
320
+ # Store as bytes (requires numpy)
321
+ if not NUMPY_AVAILABLE:
322
+ raise ImportError("numpy required for non-pgvector embedding storage")
323
+ return np.array(embedding, dtype=np.float32).tobytes()
324
+
325
+ def _embedding_from_db(self, value: Any) -> Optional[List[float]]:
326
+ """Convert embedding from database format."""
327
+ if value is None:
328
+ return None
329
+ if self._pgvector_available:
330
+ # pgvector returns as string or list
331
+ if isinstance(value, str):
332
+ value = value.strip("[]")
333
+ return [float(x) for x in value.split(",")]
334
+ return list(value)
335
+ else:
336
+ # Stored as bytes (requires numpy)
337
+ if not NUMPY_AVAILABLE or np is None:
338
+ return None
339
+ return np.frombuffer(value, dtype=np.float32).tolist()
340
+
341
+ def _cosine_similarity(self, a: List[float], b: List[float]) -> float:
342
+ """Compute cosine similarity between two vectors."""
343
+ if not NUMPY_AVAILABLE or np is None:
344
+ # Fallback to pure Python
345
+ dot = sum(x * y for x, y in zip(a, b, strict=False))
346
+ norm_a = sum(x * x for x in a) ** 0.5
347
+ norm_b = sum(x * x for x in b) ** 0.5
348
+ return dot / (norm_a * norm_b) if norm_a and norm_b else 0.0
349
+ a_arr = np.array(a)
350
+ b_arr = np.array(b)
351
+ return float(
352
+ np.dot(a_arr, b_arr) / (np.linalg.norm(a_arr) * np.linalg.norm(b_arr))
353
+ )
354
+
355
+ # ==================== WRITE OPERATIONS ====================
356
+
357
+ def save_heuristic(self, heuristic: Heuristic) -> str:
358
+ """Save a heuristic."""
359
+ with self._get_connection() as conn:
360
+ conn.execute(
361
+ f"""
362
+ INSERT INTO {self.schema}.alma_heuristics
363
+ (id, agent, project_id, condition, strategy, confidence,
364
+ occurrence_count, success_count, last_validated, created_at, metadata, embedding)
365
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
366
+ ON CONFLICT (id) DO UPDATE SET
367
+ condition = EXCLUDED.condition,
368
+ strategy = EXCLUDED.strategy,
369
+ confidence = EXCLUDED.confidence,
370
+ occurrence_count = EXCLUDED.occurrence_count,
371
+ success_count = EXCLUDED.success_count,
372
+ last_validated = EXCLUDED.last_validated,
373
+ metadata = EXCLUDED.metadata,
374
+ embedding = EXCLUDED.embedding
375
+ """,
376
+ (
377
+ heuristic.id,
378
+ heuristic.agent,
379
+ heuristic.project_id,
380
+ heuristic.condition,
381
+ heuristic.strategy,
382
+ heuristic.confidence,
383
+ heuristic.occurrence_count,
384
+ heuristic.success_count,
385
+ heuristic.last_validated,
386
+ heuristic.created_at,
387
+ json.dumps(heuristic.metadata) if heuristic.metadata else None,
388
+ self._embedding_to_db(heuristic.embedding),
389
+ ),
390
+ )
391
+ conn.commit()
392
+
393
+ logger.debug(f"Saved heuristic: {heuristic.id}")
394
+ return heuristic.id
395
+
396
+ def save_outcome(self, outcome: Outcome) -> str:
397
+ """Save an outcome."""
398
+ with self._get_connection() as conn:
399
+ conn.execute(
400
+ f"""
401
+ INSERT INTO {self.schema}.alma_outcomes
402
+ (id, agent, project_id, task_type, task_description, success,
403
+ strategy_used, duration_ms, error_message, user_feedback, timestamp, metadata, embedding)
404
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
405
+ ON CONFLICT (id) DO UPDATE SET
406
+ task_description = EXCLUDED.task_description,
407
+ success = EXCLUDED.success,
408
+ strategy_used = EXCLUDED.strategy_used,
409
+ duration_ms = EXCLUDED.duration_ms,
410
+ error_message = EXCLUDED.error_message,
411
+ user_feedback = EXCLUDED.user_feedback,
412
+ metadata = EXCLUDED.metadata,
413
+ embedding = EXCLUDED.embedding
414
+ """,
415
+ (
416
+ outcome.id,
417
+ outcome.agent,
418
+ outcome.project_id,
419
+ outcome.task_type,
420
+ outcome.task_description,
421
+ outcome.success,
422
+ outcome.strategy_used,
423
+ outcome.duration_ms,
424
+ outcome.error_message,
425
+ outcome.user_feedback,
426
+ outcome.timestamp,
427
+ json.dumps(outcome.metadata) if outcome.metadata else None,
428
+ self._embedding_to_db(outcome.embedding),
429
+ ),
430
+ )
431
+ conn.commit()
432
+
433
+ logger.debug(f"Saved outcome: {outcome.id}")
434
+ return outcome.id
435
+
436
+ def save_user_preference(self, preference: UserPreference) -> str:
437
+ """Save a user preference."""
438
+ with self._get_connection() as conn:
439
+ conn.execute(
440
+ f"""
441
+ INSERT INTO {self.schema}.alma_preferences
442
+ (id, user_id, category, preference, source, confidence, timestamp, metadata)
443
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
444
+ ON CONFLICT (id) DO UPDATE SET
445
+ preference = EXCLUDED.preference,
446
+ source = EXCLUDED.source,
447
+ confidence = EXCLUDED.confidence,
448
+ metadata = EXCLUDED.metadata
449
+ """,
450
+ (
451
+ preference.id,
452
+ preference.user_id,
453
+ preference.category,
454
+ preference.preference,
455
+ preference.source,
456
+ preference.confidence,
457
+ preference.timestamp,
458
+ json.dumps(preference.metadata) if preference.metadata else None,
459
+ ),
460
+ )
461
+ conn.commit()
462
+
463
+ logger.debug(f"Saved preference: {preference.id}")
464
+ return preference.id
465
+
466
+ def save_domain_knowledge(self, knowledge: DomainKnowledge) -> str:
467
+ """Save domain knowledge."""
468
+ with self._get_connection() as conn:
469
+ conn.execute(
470
+ f"""
471
+ INSERT INTO {self.schema}.alma_domain_knowledge
472
+ (id, agent, project_id, domain, fact, source, confidence, last_verified, metadata, embedding)
473
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
474
+ ON CONFLICT (id) DO UPDATE SET
475
+ fact = EXCLUDED.fact,
476
+ source = EXCLUDED.source,
477
+ confidence = EXCLUDED.confidence,
478
+ last_verified = EXCLUDED.last_verified,
479
+ metadata = EXCLUDED.metadata,
480
+ embedding = EXCLUDED.embedding
481
+ """,
482
+ (
483
+ knowledge.id,
484
+ knowledge.agent,
485
+ knowledge.project_id,
486
+ knowledge.domain,
487
+ knowledge.fact,
488
+ knowledge.source,
489
+ knowledge.confidence,
490
+ knowledge.last_verified,
491
+ json.dumps(knowledge.metadata) if knowledge.metadata else None,
492
+ self._embedding_to_db(knowledge.embedding),
493
+ ),
494
+ )
495
+ conn.commit()
496
+
497
+ logger.debug(f"Saved domain knowledge: {knowledge.id}")
498
+ return knowledge.id
499
+
500
+ def save_anti_pattern(self, anti_pattern: AntiPattern) -> str:
501
+ """Save an anti-pattern."""
502
+ with self._get_connection() as conn:
503
+ conn.execute(
504
+ f"""
505
+ INSERT INTO {self.schema}.alma_anti_patterns
506
+ (id, agent, project_id, pattern, why_bad, better_alternative,
507
+ occurrence_count, last_seen, created_at, metadata, embedding)
508
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
509
+ ON CONFLICT (id) DO UPDATE SET
510
+ pattern = EXCLUDED.pattern,
511
+ why_bad = EXCLUDED.why_bad,
512
+ better_alternative = EXCLUDED.better_alternative,
513
+ occurrence_count = EXCLUDED.occurrence_count,
514
+ last_seen = EXCLUDED.last_seen,
515
+ metadata = EXCLUDED.metadata,
516
+ embedding = EXCLUDED.embedding
517
+ """,
518
+ (
519
+ anti_pattern.id,
520
+ anti_pattern.agent,
521
+ anti_pattern.project_id,
522
+ anti_pattern.pattern,
523
+ anti_pattern.why_bad,
524
+ anti_pattern.better_alternative,
525
+ anti_pattern.occurrence_count,
526
+ anti_pattern.last_seen,
527
+ anti_pattern.created_at,
528
+ (
529
+ json.dumps(anti_pattern.metadata)
530
+ if anti_pattern.metadata
531
+ else None
532
+ ),
533
+ self._embedding_to_db(anti_pattern.embedding),
534
+ ),
535
+ )
536
+ conn.commit()
537
+
538
+ logger.debug(f"Saved anti-pattern: {anti_pattern.id}")
539
+ return anti_pattern.id
540
+
541
+ # ==================== BATCH WRITE OPERATIONS ====================
542
+
543
+ def save_heuristics(self, heuristics: List[Heuristic]) -> List[str]:
544
+ """Save multiple heuristics in a batch using executemany."""
545
+ if not heuristics:
546
+ return []
547
+
548
+ with self._get_connection() as conn:
549
+ conn.executemany(
550
+ f"""
551
+ INSERT INTO {self.schema}.alma_heuristics
552
+ (id, agent, project_id, condition, strategy, confidence,
553
+ occurrence_count, success_count, last_validated, created_at, metadata, embedding)
554
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
555
+ ON CONFLICT (id) DO UPDATE SET
556
+ condition = EXCLUDED.condition,
557
+ strategy = EXCLUDED.strategy,
558
+ confidence = EXCLUDED.confidence,
559
+ occurrence_count = EXCLUDED.occurrence_count,
560
+ success_count = EXCLUDED.success_count,
561
+ last_validated = EXCLUDED.last_validated,
562
+ metadata = EXCLUDED.metadata,
563
+ embedding = EXCLUDED.embedding
564
+ """,
565
+ [
566
+ (
567
+ h.id,
568
+ h.agent,
569
+ h.project_id,
570
+ h.condition,
571
+ h.strategy,
572
+ h.confidence,
573
+ h.occurrence_count,
574
+ h.success_count,
575
+ h.last_validated,
576
+ h.created_at,
577
+ json.dumps(h.metadata) if h.metadata else None,
578
+ self._embedding_to_db(h.embedding),
579
+ )
580
+ for h in heuristics
581
+ ],
582
+ )
583
+ conn.commit()
584
+
585
+ logger.debug(f"Batch saved {len(heuristics)} heuristics")
586
+ return [h.id for h in heuristics]
587
+
588
+ def save_outcomes(self, outcomes: List[Outcome]) -> List[str]:
589
+ """Save multiple outcomes in a batch using executemany."""
590
+ if not outcomes:
591
+ return []
592
+
593
+ with self._get_connection() as conn:
594
+ conn.executemany(
595
+ f"""
596
+ INSERT INTO {self.schema}.alma_outcomes
597
+ (id, agent, project_id, task_type, task_description, success,
598
+ strategy_used, duration_ms, error_message, user_feedback, timestamp, metadata, embedding)
599
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
600
+ ON CONFLICT (id) DO UPDATE SET
601
+ task_description = EXCLUDED.task_description,
602
+ success = EXCLUDED.success,
603
+ strategy_used = EXCLUDED.strategy_used,
604
+ duration_ms = EXCLUDED.duration_ms,
605
+ error_message = EXCLUDED.error_message,
606
+ user_feedback = EXCLUDED.user_feedback,
607
+ metadata = EXCLUDED.metadata,
608
+ embedding = EXCLUDED.embedding
609
+ """,
610
+ [
611
+ (
612
+ o.id,
613
+ o.agent,
614
+ o.project_id,
615
+ o.task_type,
616
+ o.task_description,
617
+ o.success,
618
+ o.strategy_used,
619
+ o.duration_ms,
620
+ o.error_message,
621
+ o.user_feedback,
622
+ o.timestamp,
623
+ json.dumps(o.metadata) if o.metadata else None,
624
+ self._embedding_to_db(o.embedding),
625
+ )
626
+ for o in outcomes
627
+ ],
628
+ )
629
+ conn.commit()
630
+
631
+ logger.debug(f"Batch saved {len(outcomes)} outcomes")
632
+ return [o.id for o in outcomes]
633
+
634
+ def save_domain_knowledge_batch(
635
+ self, knowledge_items: List[DomainKnowledge]
636
+ ) -> List[str]:
637
+ """Save multiple domain knowledge items in a batch using executemany."""
638
+ if not knowledge_items:
639
+ return []
640
+
641
+ with self._get_connection() as conn:
642
+ conn.executemany(
643
+ f"""
644
+ INSERT INTO {self.schema}.alma_domain_knowledge
645
+ (id, agent, project_id, domain, fact, source, confidence, last_verified, metadata, embedding)
646
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
647
+ ON CONFLICT (id) DO UPDATE SET
648
+ fact = EXCLUDED.fact,
649
+ source = EXCLUDED.source,
650
+ confidence = EXCLUDED.confidence,
651
+ last_verified = EXCLUDED.last_verified,
652
+ metadata = EXCLUDED.metadata,
653
+ embedding = EXCLUDED.embedding
654
+ """,
655
+ [
656
+ (
657
+ k.id,
658
+ k.agent,
659
+ k.project_id,
660
+ k.domain,
661
+ k.fact,
662
+ k.source,
663
+ k.confidence,
664
+ k.last_verified,
665
+ json.dumps(k.metadata) if k.metadata else None,
666
+ self._embedding_to_db(k.embedding),
667
+ )
668
+ for k in knowledge_items
669
+ ],
670
+ )
671
+ conn.commit()
672
+
673
+ logger.debug(f"Batch saved {len(knowledge_items)} domain knowledge items")
674
+ return [k.id for k in knowledge_items]
675
+
676
+ # ==================== READ OPERATIONS ====================
677
+
678
+ def get_heuristics(
679
+ self,
680
+ project_id: str,
681
+ agent: Optional[str] = None,
682
+ embedding: Optional[List[float]] = None,
683
+ top_k: int = 5,
684
+ min_confidence: float = 0.0,
685
+ ) -> List[Heuristic]:
686
+ """Get heuristics with optional vector search."""
687
+ with self._get_connection() as conn:
688
+ if embedding and self._pgvector_available:
689
+ # Use pgvector similarity search
690
+ query = f"""
691
+ SELECT *, 1 - (embedding <=> %s::vector) as similarity
692
+ FROM {self.schema}.alma_heuristics
693
+ WHERE project_id = %s AND confidence >= %s
694
+ """
695
+ params: List[Any] = [
696
+ self._embedding_to_db(embedding),
697
+ project_id,
698
+ min_confidence,
699
+ ]
700
+
701
+ if agent:
702
+ query += " AND agent = %s"
703
+ params.append(agent)
704
+
705
+ query += " ORDER BY similarity DESC LIMIT %s"
706
+ params.append(top_k)
707
+ else:
708
+ # Standard query
709
+ query = f"""
710
+ SELECT *
711
+ FROM {self.schema}.alma_heuristics
712
+ WHERE project_id = %s AND confidence >= %s
713
+ """
714
+ params = [project_id, min_confidence]
715
+
716
+ if agent:
717
+ query += " AND agent = %s"
718
+ params.append(agent)
719
+
720
+ query += " ORDER BY confidence DESC LIMIT %s"
721
+ params.append(top_k)
722
+
723
+ cursor = conn.execute(query, params)
724
+ rows = cursor.fetchall()
725
+
726
+ results = [self._row_to_heuristic(row) for row in rows]
727
+
728
+ # If embedding provided but pgvector not available, do app-level filtering
729
+ if embedding and not self._pgvector_available and results:
730
+ results = self._filter_by_similarity(results, embedding, top_k, "embedding")
731
+
732
+ return results
733
+
734
+ def get_outcomes(
735
+ self,
736
+ project_id: str,
737
+ agent: Optional[str] = None,
738
+ task_type: Optional[str] = None,
739
+ embedding: Optional[List[float]] = None,
740
+ top_k: int = 5,
741
+ success_only: bool = False,
742
+ ) -> List[Outcome]:
743
+ """Get outcomes with optional vector search."""
744
+ with self._get_connection() as conn:
745
+ if embedding and self._pgvector_available:
746
+ query = f"""
747
+ SELECT *, 1 - (embedding <=> %s::vector) as similarity
748
+ FROM {self.schema}.alma_outcomes
749
+ WHERE project_id = %s
750
+ """
751
+ params: List[Any] = [self._embedding_to_db(embedding), project_id]
752
+ else:
753
+ query = f"""
754
+ SELECT *
755
+ FROM {self.schema}.alma_outcomes
756
+ WHERE project_id = %s
757
+ """
758
+ params = [project_id]
759
+
760
+ if agent:
761
+ query += " AND agent = %s"
762
+ params.append(agent)
763
+
764
+ if task_type:
765
+ query += " AND task_type = %s"
766
+ params.append(task_type)
767
+
768
+ if success_only:
769
+ query += " AND success = TRUE"
770
+
771
+ if embedding and self._pgvector_available:
772
+ query += " ORDER BY similarity DESC LIMIT %s"
773
+ else:
774
+ query += " ORDER BY timestamp DESC LIMIT %s"
775
+ params.append(top_k)
776
+
777
+ cursor = conn.execute(query, params)
778
+ rows = cursor.fetchall()
779
+
780
+ results = [self._row_to_outcome(row) for row in rows]
781
+
782
+ if embedding and not self._pgvector_available and results:
783
+ results = self._filter_by_similarity(results, embedding, top_k, "embedding")
784
+
785
+ return results
786
+
787
+ def get_user_preferences(
788
+ self,
789
+ user_id: str,
790
+ category: Optional[str] = None,
791
+ ) -> List[UserPreference]:
792
+ """Get user preferences."""
793
+ with self._get_connection() as conn:
794
+ query = f"SELECT * FROM {self.schema}.alma_preferences WHERE user_id = %s"
795
+ params: List[Any] = [user_id]
796
+
797
+ if category:
798
+ query += " AND category = %s"
799
+ params.append(category)
800
+
801
+ cursor = conn.execute(query, params)
802
+ rows = cursor.fetchall()
803
+
804
+ return [self._row_to_preference(row) for row in rows]
805
+
806
+ def get_domain_knowledge(
807
+ self,
808
+ project_id: str,
809
+ agent: Optional[str] = None,
810
+ domain: Optional[str] = None,
811
+ embedding: Optional[List[float]] = None,
812
+ top_k: int = 5,
813
+ ) -> List[DomainKnowledge]:
814
+ """Get domain knowledge with optional vector search."""
815
+ with self._get_connection() as conn:
816
+ if embedding and self._pgvector_available:
817
+ query = f"""
818
+ SELECT *, 1 - (embedding <=> %s::vector) as similarity
819
+ FROM {self.schema}.alma_domain_knowledge
820
+ WHERE project_id = %s
821
+ """
822
+ params: List[Any] = [self._embedding_to_db(embedding), project_id]
823
+ else:
824
+ query = f"""
825
+ SELECT *
826
+ FROM {self.schema}.alma_domain_knowledge
827
+ WHERE project_id = %s
828
+ """
829
+ params = [project_id]
830
+
831
+ if agent:
832
+ query += " AND agent = %s"
833
+ params.append(agent)
834
+
835
+ if domain:
836
+ query += " AND domain = %s"
837
+ params.append(domain)
838
+
839
+ if embedding and self._pgvector_available:
840
+ query += " ORDER BY similarity DESC LIMIT %s"
841
+ else:
842
+ query += " ORDER BY confidence DESC LIMIT %s"
843
+ params.append(top_k)
844
+
845
+ cursor = conn.execute(query, params)
846
+ rows = cursor.fetchall()
847
+
848
+ results = [self._row_to_domain_knowledge(row) for row in rows]
849
+
850
+ if embedding and not self._pgvector_available and results:
851
+ results = self._filter_by_similarity(results, embedding, top_k, "embedding")
852
+
853
+ return results
854
+
855
+ def get_anti_patterns(
856
+ self,
857
+ project_id: str,
858
+ agent: Optional[str] = None,
859
+ embedding: Optional[List[float]] = None,
860
+ top_k: int = 5,
861
+ ) -> List[AntiPattern]:
862
+ """Get anti-patterns with optional vector search."""
863
+ with self._get_connection() as conn:
864
+ if embedding and self._pgvector_available:
865
+ query = f"""
866
+ SELECT *, 1 - (embedding <=> %s::vector) as similarity
867
+ FROM {self.schema}.alma_anti_patterns
868
+ WHERE project_id = %s
869
+ """
870
+ params: List[Any] = [self._embedding_to_db(embedding), project_id]
871
+ else:
872
+ query = f"""
873
+ SELECT *
874
+ FROM {self.schema}.alma_anti_patterns
875
+ WHERE project_id = %s
876
+ """
877
+ params = [project_id]
878
+
879
+ if agent:
880
+ query += " AND agent = %s"
881
+ params.append(agent)
882
+
883
+ if embedding and self._pgvector_available:
884
+ query += " ORDER BY similarity DESC LIMIT %s"
885
+ else:
886
+ query += " ORDER BY occurrence_count DESC LIMIT %s"
887
+ params.append(top_k)
888
+
889
+ cursor = conn.execute(query, params)
890
+ rows = cursor.fetchall()
891
+
892
+ results = [self._row_to_anti_pattern(row) for row in rows]
893
+
894
+ if embedding and not self._pgvector_available and results:
895
+ results = self._filter_by_similarity(results, embedding, top_k, "embedding")
896
+
897
+ return results
898
+
899
+ def _filter_by_similarity(
900
+ self,
901
+ items: List[Any],
902
+ query_embedding: List[float],
903
+ top_k: int,
904
+ embedding_attr: str,
905
+ ) -> List[Any]:
906
+ """Filter items by cosine similarity (fallback when pgvector unavailable)."""
907
+ scored = []
908
+ for item in items:
909
+ item_embedding = getattr(item, embedding_attr, None)
910
+ if item_embedding:
911
+ similarity = self._cosine_similarity(query_embedding, item_embedding)
912
+ scored.append((item, similarity))
913
+ else:
914
+ scored.append((item, 0.0))
915
+
916
+ scored.sort(key=lambda x: x[1], reverse=True)
917
+ return [item for item, _ in scored[:top_k]]
918
+
919
+ # ==================== MULTI-AGENT MEMORY SHARING ====================
920
+
921
+ def get_heuristics_for_agents(
922
+ self,
923
+ project_id: str,
924
+ agents: List[str],
925
+ embedding: Optional[List[float]] = None,
926
+ top_k: int = 5,
927
+ min_confidence: float = 0.0,
928
+ ) -> List[Heuristic]:
929
+ """Get heuristics from multiple agents using optimized ANY query."""
930
+ if not agents:
931
+ return []
932
+
933
+ with self._get_connection() as conn:
934
+ if embedding and self._pgvector_available:
935
+ query = f"""
936
+ SELECT *, 1 - (embedding <=> %s::vector) as similarity
937
+ FROM {self.schema}.alma_heuristics
938
+ WHERE project_id = %s AND confidence >= %s AND agent = ANY(%s)
939
+ ORDER BY similarity DESC LIMIT %s
940
+ """
941
+ params: List[Any] = [
942
+ self._embedding_to_db(embedding),
943
+ project_id,
944
+ min_confidence,
945
+ agents,
946
+ top_k * len(agents),
947
+ ]
948
+ else:
949
+ query = f"""
950
+ SELECT *
951
+ FROM {self.schema}.alma_heuristics
952
+ WHERE project_id = %s AND confidence >= %s AND agent = ANY(%s)
953
+ ORDER BY confidence DESC LIMIT %s
954
+ """
955
+ params = [project_id, min_confidence, agents, top_k * len(agents)]
956
+
957
+ cursor = conn.execute(query, params)
958
+ rows = cursor.fetchall()
959
+
960
+ results = [self._row_to_heuristic(row) for row in rows]
961
+
962
+ if embedding and not self._pgvector_available and results:
963
+ results = self._filter_by_similarity(
964
+ results, embedding, top_k * len(agents), "embedding"
965
+ )
966
+
967
+ return results
968
+
969
+ def get_outcomes_for_agents(
970
+ self,
971
+ project_id: str,
972
+ agents: List[str],
973
+ task_type: Optional[str] = None,
974
+ embedding: Optional[List[float]] = None,
975
+ top_k: int = 5,
976
+ success_only: bool = False,
977
+ ) -> List[Outcome]:
978
+ """Get outcomes from multiple agents using optimized ANY query."""
979
+ if not agents:
980
+ return []
981
+
982
+ with self._get_connection() as conn:
983
+ if embedding and self._pgvector_available:
984
+ query = f"""
985
+ SELECT *, 1 - (embedding <=> %s::vector) as similarity
986
+ FROM {self.schema}.alma_outcomes
987
+ WHERE project_id = %s AND agent = ANY(%s)
988
+ """
989
+ params: List[Any] = [
990
+ self._embedding_to_db(embedding),
991
+ project_id,
992
+ agents,
993
+ ]
994
+ else:
995
+ query = f"""
996
+ SELECT *
997
+ FROM {self.schema}.alma_outcomes
998
+ WHERE project_id = %s AND agent = ANY(%s)
999
+ """
1000
+ params = [project_id, agents]
1001
+
1002
+ if task_type:
1003
+ query += " AND task_type = %s"
1004
+ params.append(task_type)
1005
+
1006
+ if success_only:
1007
+ query += " AND success = TRUE"
1008
+
1009
+ if embedding and self._pgvector_available:
1010
+ query += " ORDER BY similarity DESC LIMIT %s"
1011
+ else:
1012
+ query += " ORDER BY timestamp DESC LIMIT %s"
1013
+ params.append(top_k * len(agents))
1014
+
1015
+ cursor = conn.execute(query, params)
1016
+ rows = cursor.fetchall()
1017
+
1018
+ results = [self._row_to_outcome(row) for row in rows]
1019
+
1020
+ if embedding and not self._pgvector_available and results:
1021
+ results = self._filter_by_similarity(
1022
+ results, embedding, top_k * len(agents), "embedding"
1023
+ )
1024
+
1025
+ return results
1026
+
1027
+ def get_domain_knowledge_for_agents(
1028
+ self,
1029
+ project_id: str,
1030
+ agents: List[str],
1031
+ domain: Optional[str] = None,
1032
+ embedding: Optional[List[float]] = None,
1033
+ top_k: int = 5,
1034
+ ) -> List[DomainKnowledge]:
1035
+ """Get domain knowledge from multiple agents using optimized ANY query."""
1036
+ if not agents:
1037
+ return []
1038
+
1039
+ with self._get_connection() as conn:
1040
+ if embedding and self._pgvector_available:
1041
+ query = f"""
1042
+ SELECT *, 1 - (embedding <=> %s::vector) as similarity
1043
+ FROM {self.schema}.alma_domain_knowledge
1044
+ WHERE project_id = %s AND agent = ANY(%s)
1045
+ """
1046
+ params: List[Any] = [
1047
+ self._embedding_to_db(embedding),
1048
+ project_id,
1049
+ agents,
1050
+ ]
1051
+ else:
1052
+ query = f"""
1053
+ SELECT *
1054
+ FROM {self.schema}.alma_domain_knowledge
1055
+ WHERE project_id = %s AND agent = ANY(%s)
1056
+ """
1057
+ params = [project_id, agents]
1058
+
1059
+ if domain:
1060
+ query += " AND domain = %s"
1061
+ params.append(domain)
1062
+
1063
+ if embedding and self._pgvector_available:
1064
+ query += " ORDER BY similarity DESC LIMIT %s"
1065
+ else:
1066
+ query += " ORDER BY confidence DESC LIMIT %s"
1067
+ params.append(top_k * len(agents))
1068
+
1069
+ cursor = conn.execute(query, params)
1070
+ rows = cursor.fetchall()
1071
+
1072
+ results = [self._row_to_domain_knowledge(row) for row in rows]
1073
+
1074
+ if embedding and not self._pgvector_available and results:
1075
+ results = self._filter_by_similarity(
1076
+ results, embedding, top_k * len(agents), "embedding"
1077
+ )
1078
+
1079
+ return results
1080
+
1081
+ def get_anti_patterns_for_agents(
1082
+ self,
1083
+ project_id: str,
1084
+ agents: List[str],
1085
+ embedding: Optional[List[float]] = None,
1086
+ top_k: int = 5,
1087
+ ) -> List[AntiPattern]:
1088
+ """Get anti-patterns from multiple agents using optimized ANY query."""
1089
+ if not agents:
1090
+ return []
1091
+
1092
+ with self._get_connection() as conn:
1093
+ if embedding and self._pgvector_available:
1094
+ query = f"""
1095
+ SELECT *, 1 - (embedding <=> %s::vector) as similarity
1096
+ FROM {self.schema}.alma_anti_patterns
1097
+ WHERE project_id = %s AND agent = ANY(%s)
1098
+ """
1099
+ params: List[Any] = [
1100
+ self._embedding_to_db(embedding),
1101
+ project_id,
1102
+ agents,
1103
+ ]
1104
+ else:
1105
+ query = f"""
1106
+ SELECT *
1107
+ FROM {self.schema}.alma_anti_patterns
1108
+ WHERE project_id = %s AND agent = ANY(%s)
1109
+ """
1110
+ params = [project_id, agents]
1111
+
1112
+ if embedding and self._pgvector_available:
1113
+ query += " ORDER BY similarity DESC LIMIT %s"
1114
+ else:
1115
+ query += " ORDER BY occurrence_count DESC LIMIT %s"
1116
+ params.append(top_k * len(agents))
1117
+
1118
+ cursor = conn.execute(query, params)
1119
+ rows = cursor.fetchall()
1120
+
1121
+ results = [self._row_to_anti_pattern(row) for row in rows]
1122
+
1123
+ if embedding and not self._pgvector_available and results:
1124
+ results = self._filter_by_similarity(
1125
+ results, embedding, top_k * len(agents), "embedding"
1126
+ )
1127
+
1128
+ return results
1129
+
1130
+ # ==================== UPDATE OPERATIONS ====================
1131
+
1132
+ def update_heuristic(
1133
+ self,
1134
+ heuristic_id: str,
1135
+ updates: Dict[str, Any],
1136
+ ) -> bool:
1137
+ """Update a heuristic's fields."""
1138
+ if not updates:
1139
+ return False
1140
+
1141
+ set_clauses = []
1142
+ params = []
1143
+ for key, value in updates.items():
1144
+ if key == "metadata" and value:
1145
+ value = json.dumps(value)
1146
+ set_clauses.append(f"{key} = %s")
1147
+ params.append(value)
1148
+
1149
+ params.append(heuristic_id)
1150
+
1151
+ with self._get_connection() as conn:
1152
+ cursor = conn.execute(
1153
+ f"UPDATE {self.schema}.alma_heuristics SET {', '.join(set_clauses)} WHERE id = %s",
1154
+ params,
1155
+ )
1156
+ conn.commit()
1157
+ return cursor.rowcount > 0
1158
+
1159
+ def increment_heuristic_occurrence(
1160
+ self,
1161
+ heuristic_id: str,
1162
+ success: bool,
1163
+ ) -> bool:
1164
+ """Increment heuristic occurrence count."""
1165
+ with self._get_connection() as conn:
1166
+ if success:
1167
+ cursor = conn.execute(
1168
+ f"""
1169
+ UPDATE {self.schema}.alma_heuristics
1170
+ SET occurrence_count = occurrence_count + 1,
1171
+ success_count = success_count + 1,
1172
+ last_validated = %s
1173
+ WHERE id = %s
1174
+ """,
1175
+ (datetime.now(timezone.utc), heuristic_id),
1176
+ )
1177
+ else:
1178
+ cursor = conn.execute(
1179
+ f"""
1180
+ UPDATE {self.schema}.alma_heuristics
1181
+ SET occurrence_count = occurrence_count + 1,
1182
+ last_validated = %s
1183
+ WHERE id = %s
1184
+ """,
1185
+ (datetime.now(timezone.utc), heuristic_id),
1186
+ )
1187
+ conn.commit()
1188
+ return cursor.rowcount > 0
1189
+
1190
+ def update_heuristic_confidence(
1191
+ self,
1192
+ heuristic_id: str,
1193
+ new_confidence: float,
1194
+ ) -> bool:
1195
+ """Update confidence score for a heuristic."""
1196
+ with self._get_connection() as conn:
1197
+ cursor = conn.execute(
1198
+ f"UPDATE {self.schema}.alma_heuristics SET confidence = %s WHERE id = %s",
1199
+ (new_confidence, heuristic_id),
1200
+ )
1201
+ conn.commit()
1202
+ return cursor.rowcount > 0
1203
+
1204
+ def update_knowledge_confidence(
1205
+ self,
1206
+ knowledge_id: str,
1207
+ new_confidence: float,
1208
+ ) -> bool:
1209
+ """Update confidence score for domain knowledge."""
1210
+ with self._get_connection() as conn:
1211
+ cursor = conn.execute(
1212
+ f"UPDATE {self.schema}.alma_domain_knowledge SET confidence = %s WHERE id = %s",
1213
+ (new_confidence, knowledge_id),
1214
+ )
1215
+ conn.commit()
1216
+ return cursor.rowcount > 0
1217
+
1218
+ # ==================== DELETE OPERATIONS ====================
1219
+
1220
+ def delete_heuristic(self, heuristic_id: str) -> bool:
1221
+ """Delete a heuristic by ID."""
1222
+ with self._get_connection() as conn:
1223
+ cursor = conn.execute(
1224
+ f"DELETE FROM {self.schema}.alma_heuristics WHERE id = %s",
1225
+ (heuristic_id,),
1226
+ )
1227
+ conn.commit()
1228
+ return cursor.rowcount > 0
1229
+
1230
+ def delete_outcome(self, outcome_id: str) -> bool:
1231
+ """Delete an outcome by ID."""
1232
+ with self._get_connection() as conn:
1233
+ cursor = conn.execute(
1234
+ f"DELETE FROM {self.schema}.alma_outcomes WHERE id = %s",
1235
+ (outcome_id,),
1236
+ )
1237
+ conn.commit()
1238
+ return cursor.rowcount > 0
1239
+
1240
+ def delete_domain_knowledge(self, knowledge_id: str) -> bool:
1241
+ """Delete domain knowledge by ID."""
1242
+ with self._get_connection() as conn:
1243
+ cursor = conn.execute(
1244
+ f"DELETE FROM {self.schema}.alma_domain_knowledge WHERE id = %s",
1245
+ (knowledge_id,),
1246
+ )
1247
+ conn.commit()
1248
+ return cursor.rowcount > 0
1249
+
1250
+ def delete_anti_pattern(self, anti_pattern_id: str) -> bool:
1251
+ """Delete an anti-pattern by ID."""
1252
+ with self._get_connection() as conn:
1253
+ cursor = conn.execute(
1254
+ f"DELETE FROM {self.schema}.alma_anti_patterns WHERE id = %s",
1255
+ (anti_pattern_id,),
1256
+ )
1257
+ conn.commit()
1258
+ return cursor.rowcount > 0
1259
+
1260
+ def delete_outcomes_older_than(
1261
+ self,
1262
+ project_id: str,
1263
+ older_than: datetime,
1264
+ agent: Optional[str] = None,
1265
+ ) -> int:
1266
+ """Delete old outcomes."""
1267
+ with self._get_connection() as conn:
1268
+ query = f"DELETE FROM {self.schema}.alma_outcomes WHERE project_id = %s AND timestamp < %s"
1269
+ params: List[Any] = [project_id, older_than]
1270
+
1271
+ if agent:
1272
+ query += " AND agent = %s"
1273
+ params.append(agent)
1274
+
1275
+ cursor = conn.execute(query, params)
1276
+ conn.commit()
1277
+ deleted = cursor.rowcount
1278
+
1279
+ logger.info(f"Deleted {deleted} old outcomes")
1280
+ return deleted
1281
+
1282
+ def delete_low_confidence_heuristics(
1283
+ self,
1284
+ project_id: str,
1285
+ below_confidence: float,
1286
+ agent: Optional[str] = None,
1287
+ ) -> int:
1288
+ """Delete low-confidence heuristics."""
1289
+ with self._get_connection() as conn:
1290
+ query = f"DELETE FROM {self.schema}.alma_heuristics WHERE project_id = %s AND confidence < %s"
1291
+ params: List[Any] = [project_id, below_confidence]
1292
+
1293
+ if agent:
1294
+ query += " AND agent = %s"
1295
+ params.append(agent)
1296
+
1297
+ cursor = conn.execute(query, params)
1298
+ conn.commit()
1299
+ deleted = cursor.rowcount
1300
+
1301
+ logger.info(f"Deleted {deleted} low-confidence heuristics")
1302
+ return deleted
1303
+
1304
+ # ==================== STATS ====================
1305
+
1306
+ def get_stats(
1307
+ self,
1308
+ project_id: str,
1309
+ agent: Optional[str] = None,
1310
+ ) -> Dict[str, Any]:
1311
+ """Get memory statistics."""
1312
+ stats = {
1313
+ "project_id": project_id,
1314
+ "agent": agent,
1315
+ "storage_type": "postgresql",
1316
+ "pgvector_available": self._pgvector_available,
1317
+ }
1318
+
1319
+ with self._get_connection() as conn:
1320
+ tables = [
1321
+ ("heuristics", "alma_heuristics"),
1322
+ ("outcomes", "alma_outcomes"),
1323
+ ("domain_knowledge", "alma_domain_knowledge"),
1324
+ ("anti_patterns", "alma_anti_patterns"),
1325
+ ]
1326
+
1327
+ for stat_name, table in tables:
1328
+ query = f"SELECT COUNT(*) as count FROM {self.schema}.{table} WHERE project_id = %s"
1329
+ params: List[Any] = [project_id]
1330
+ if agent:
1331
+ query += " AND agent = %s"
1332
+ params.append(agent)
1333
+ cursor = conn.execute(query, params)
1334
+ row = cursor.fetchone()
1335
+ stats[f"{stat_name}_count"] = row["count"] if row else 0
1336
+
1337
+ # Preferences don't have project_id
1338
+ cursor = conn.execute(
1339
+ f"SELECT COUNT(*) as count FROM {self.schema}.alma_preferences"
1340
+ )
1341
+ row = cursor.fetchone()
1342
+ stats["preferences_count"] = row["count"] if row else 0
1343
+
1344
+ stats["total_count"] = sum(
1345
+ stats.get(k, 0) for k in stats if k.endswith("_count")
1346
+ )
1347
+
1348
+ return stats
1349
+
1350
+ # ==================== HELPERS ====================
1351
+
1352
+ def _parse_datetime(self, value: Any) -> Optional[datetime]:
1353
+ """Parse datetime from database value."""
1354
+ if value is None:
1355
+ return None
1356
+ if isinstance(value, datetime):
1357
+ return value
1358
+ try:
1359
+ return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
1360
+ except (ValueError, AttributeError):
1361
+ return None
1362
+
1363
+ def _row_to_heuristic(self, row: Dict[str, Any]) -> Heuristic:
1364
+ """Convert database row to Heuristic."""
1365
+ return Heuristic(
1366
+ id=row["id"],
1367
+ agent=row["agent"],
1368
+ project_id=row["project_id"],
1369
+ condition=row["condition"],
1370
+ strategy=row["strategy"],
1371
+ confidence=row["confidence"] or 0.0,
1372
+ occurrence_count=row["occurrence_count"] or 0,
1373
+ success_count=row["success_count"] or 0,
1374
+ last_validated=self._parse_datetime(row["last_validated"])
1375
+ or datetime.now(timezone.utc),
1376
+ created_at=self._parse_datetime(row["created_at"])
1377
+ or datetime.now(timezone.utc),
1378
+ embedding=self._embedding_from_db(row.get("embedding")),
1379
+ metadata=row["metadata"] if row["metadata"] else {},
1380
+ )
1381
+
1382
+ def _row_to_outcome(self, row: Dict[str, Any]) -> Outcome:
1383
+ """Convert database row to Outcome."""
1384
+ return Outcome(
1385
+ id=row["id"],
1386
+ agent=row["agent"],
1387
+ project_id=row["project_id"],
1388
+ task_type=row["task_type"] or "general",
1389
+ task_description=row["task_description"],
1390
+ success=bool(row["success"]),
1391
+ strategy_used=row["strategy_used"] or "",
1392
+ duration_ms=row["duration_ms"],
1393
+ error_message=row["error_message"],
1394
+ user_feedback=row["user_feedback"],
1395
+ timestamp=self._parse_datetime(row["timestamp"])
1396
+ or datetime.now(timezone.utc),
1397
+ embedding=self._embedding_from_db(row.get("embedding")),
1398
+ metadata=row["metadata"] if row["metadata"] else {},
1399
+ )
1400
+
1401
+ def _row_to_preference(self, row: Dict[str, Any]) -> UserPreference:
1402
+ """Convert database row to UserPreference."""
1403
+ return UserPreference(
1404
+ id=row["id"],
1405
+ user_id=row["user_id"],
1406
+ category=row["category"] or "general",
1407
+ preference=row["preference"],
1408
+ source=row["source"] or "unknown",
1409
+ confidence=row["confidence"] or 1.0,
1410
+ timestamp=self._parse_datetime(row["timestamp"])
1411
+ or datetime.now(timezone.utc),
1412
+ metadata=row["metadata"] if row["metadata"] else {},
1413
+ )
1414
+
1415
+ def _row_to_domain_knowledge(self, row: Dict[str, Any]) -> DomainKnowledge:
1416
+ """Convert database row to DomainKnowledge."""
1417
+ return DomainKnowledge(
1418
+ id=row["id"],
1419
+ agent=row["agent"],
1420
+ project_id=row["project_id"],
1421
+ domain=row["domain"] or "general",
1422
+ fact=row["fact"],
1423
+ source=row["source"] or "unknown",
1424
+ confidence=row["confidence"] or 1.0,
1425
+ last_verified=self._parse_datetime(row["last_verified"])
1426
+ or datetime.now(timezone.utc),
1427
+ embedding=self._embedding_from_db(row.get("embedding")),
1428
+ metadata=row["metadata"] if row["metadata"] else {},
1429
+ )
1430
+
1431
+ def _row_to_anti_pattern(self, row: Dict[str, Any]) -> AntiPattern:
1432
+ """Convert database row to AntiPattern."""
1433
+ return AntiPattern(
1434
+ id=row["id"],
1435
+ agent=row["agent"],
1436
+ project_id=row["project_id"],
1437
+ pattern=row["pattern"],
1438
+ why_bad=row["why_bad"] or "",
1439
+ better_alternative=row["better_alternative"] or "",
1440
+ occurrence_count=row["occurrence_count"] or 1,
1441
+ last_seen=self._parse_datetime(row["last_seen"])
1442
+ or datetime.now(timezone.utc),
1443
+ created_at=self._parse_datetime(row["created_at"])
1444
+ or datetime.now(timezone.utc),
1445
+ embedding=self._embedding_from_db(row.get("embedding")),
1446
+ metadata=row["metadata"] if row["metadata"] else {},
1447
+ )
1448
+
1449
+ def close(self):
1450
+ """Close connection pool."""
1451
+ if self._pool:
1452
+ self._pool.close()