alma-memory 0.5.0__py3-none-any.whl → 0.7.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.
- alma/__init__.py +296 -194
- alma/compression/__init__.py +33 -0
- alma/compression/pipeline.py +980 -0
- alma/confidence/__init__.py +47 -47
- alma/confidence/engine.py +540 -540
- alma/confidence/types.py +351 -351
- alma/config/loader.py +157 -157
- alma/consolidation/__init__.py +23 -23
- alma/consolidation/engine.py +678 -678
- alma/consolidation/prompts.py +84 -84
- alma/core.py +1189 -322
- alma/domains/__init__.py +30 -30
- alma/domains/factory.py +359 -359
- alma/domains/schemas.py +448 -448
- alma/domains/types.py +272 -272
- alma/events/__init__.py +75 -75
- alma/events/emitter.py +285 -284
- alma/events/storage_mixin.py +246 -246
- alma/events/types.py +126 -126
- alma/events/webhook.py +425 -425
- alma/exceptions.py +49 -49
- alma/extraction/__init__.py +31 -31
- alma/extraction/auto_learner.py +265 -264
- alma/extraction/extractor.py +420 -420
- alma/graph/__init__.py +106 -81
- alma/graph/backends/__init__.py +32 -18
- alma/graph/backends/kuzu.py +624 -0
- alma/graph/backends/memgraph.py +432 -0
- alma/graph/backends/memory.py +236 -236
- alma/graph/backends/neo4j.py +417 -417
- alma/graph/base.py +159 -159
- alma/graph/extraction.py +198 -198
- alma/graph/store.py +860 -860
- alma/harness/__init__.py +35 -35
- alma/harness/base.py +386 -386
- alma/harness/domains.py +705 -705
- alma/initializer/__init__.py +37 -37
- alma/initializer/initializer.py +418 -418
- alma/initializer/types.py +250 -250
- alma/integration/__init__.py +62 -62
- alma/integration/claude_agents.py +444 -432
- alma/integration/helena.py +423 -423
- alma/integration/victor.py +471 -471
- alma/learning/__init__.py +101 -86
- alma/learning/decay.py +878 -0
- alma/learning/forgetting.py +1446 -1446
- alma/learning/heuristic_extractor.py +390 -390
- alma/learning/protocols.py +374 -374
- alma/learning/validation.py +346 -346
- alma/mcp/__init__.py +123 -45
- alma/mcp/__main__.py +156 -156
- alma/mcp/resources.py +122 -122
- alma/mcp/server.py +955 -591
- alma/mcp/tools.py +3254 -511
- alma/observability/__init__.py +91 -0
- alma/observability/config.py +302 -0
- alma/observability/guidelines.py +170 -0
- alma/observability/logging.py +424 -0
- alma/observability/metrics.py +583 -0
- alma/observability/tracing.py +440 -0
- alma/progress/__init__.py +21 -21
- alma/progress/tracker.py +607 -607
- alma/progress/types.py +250 -250
- alma/retrieval/__init__.py +134 -53
- alma/retrieval/budget.py +525 -0
- alma/retrieval/cache.py +1304 -1061
- alma/retrieval/embeddings.py +202 -202
- alma/retrieval/engine.py +850 -366
- alma/retrieval/modes.py +365 -0
- alma/retrieval/progressive.py +560 -0
- alma/retrieval/scoring.py +344 -344
- alma/retrieval/trust_scoring.py +637 -0
- alma/retrieval/verification.py +797 -0
- alma/session/__init__.py +19 -19
- alma/session/manager.py +442 -399
- alma/session/types.py +288 -288
- alma/storage/__init__.py +101 -61
- alma/storage/archive.py +233 -0
- alma/storage/azure_cosmos.py +1259 -1048
- alma/storage/base.py +1083 -525
- alma/storage/chroma.py +1443 -1443
- alma/storage/constants.py +103 -0
- alma/storage/file_based.py +614 -619
- 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/migrations/versions/v1_1_0_workflow_context.py +551 -0
- alma/storage/pinecone.py +1080 -1080
- alma/storage/postgresql.py +1948 -1452
- alma/storage/qdrant.py +1306 -1306
- alma/storage/sqlite_local.py +3041 -1358
- alma/testing/__init__.py +46 -0
- alma/testing/factories.py +301 -0
- alma/testing/mocks.py +389 -0
- alma/types.py +292 -264
- alma/utils/__init__.py +19 -0
- alma/utils/tokenizer.py +521 -0
- alma/workflow/__init__.py +83 -0
- alma/workflow/artifacts.py +170 -0
- alma/workflow/checkpoint.py +311 -0
- alma/workflow/context.py +228 -0
- alma/workflow/outcomes.py +189 -0
- alma/workflow/reducers.py +393 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/METADATA +244 -72
- alma_memory-0.7.0.dist-info/RECORD +112 -0
- alma_memory-0.5.0.dist-info/RECORD +0 -76
- {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
alma/storage/postgresql.py
CHANGED
|
@@ -1,1452 +1,1948 @@
|
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
self.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
""")
|
|
222
|
-
conn.execute(f"""
|
|
223
|
-
CREATE INDEX IF NOT EXISTS
|
|
224
|
-
ON {self.schema}.
|
|
225
|
-
""")
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
conn.execute(f"""
|
|
286
|
-
CREATE
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
json.dumps(
|
|
428
|
-
self._embedding_to_db(
|
|
429
|
-
),
|
|
430
|
-
)
|
|
431
|
-
conn.commit()
|
|
432
|
-
|
|
433
|
-
logger.debug(f"Saved
|
|
434
|
-
return
|
|
435
|
-
|
|
436
|
-
def
|
|
437
|
-
"""Save
|
|
438
|
-
with self._get_connection() as conn:
|
|
439
|
-
conn.execute(
|
|
440
|
-
f"""
|
|
441
|
-
INSERT INTO {self.schema}.
|
|
442
|
-
(id,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
),
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
)
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
"""
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
query =
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
""
|
|
877
|
-
params
|
|
878
|
-
|
|
879
|
-
if
|
|
880
|
-
query += "
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
else:
|
|
1052
|
-
query
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
self
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
cursor
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
self
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
conn.commit()
|
|
1228
|
-
return cursor.rowcount > 0
|
|
1229
|
-
|
|
1230
|
-
def
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
conn.
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
def
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
self
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
return
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
or datetime.now(timezone.utc),
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
or
|
|
1443
|
-
|
|
1444
|
-
or
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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
|
+
v0.6.0 adds workflow context support:
|
|
13
|
+
- Checkpoint tables for crash recovery
|
|
14
|
+
- WorkflowOutcome tables for learning from workflows
|
|
15
|
+
- ArtifactRef tables for linking external files
|
|
16
|
+
- scope_filter parameter for workflow-scoped queries
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
# numpy is optional - only needed for fallback similarity when pgvector unavailable
|
|
27
|
+
try:
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
NUMPY_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
np = None # type: ignore
|
|
33
|
+
NUMPY_AVAILABLE = False
|
|
34
|
+
|
|
35
|
+
from alma.storage.base import StorageBackend
|
|
36
|
+
from alma.storage.constants import POSTGRESQL_TABLE_NAMES, MemoryType
|
|
37
|
+
from alma.types import (
|
|
38
|
+
AntiPattern,
|
|
39
|
+
DomainKnowledge,
|
|
40
|
+
Heuristic,
|
|
41
|
+
Outcome,
|
|
42
|
+
UserPreference,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from alma.workflow import ArtifactRef, Checkpoint, WorkflowOutcome
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
# Try to import psycopg (v3) with connection pooling
|
|
51
|
+
try:
|
|
52
|
+
from psycopg.rows import dict_row
|
|
53
|
+
from psycopg_pool import ConnectionPool
|
|
54
|
+
|
|
55
|
+
PSYCOPG_AVAILABLE = True
|
|
56
|
+
except ImportError:
|
|
57
|
+
PSYCOPG_AVAILABLE = False
|
|
58
|
+
logger.warning(
|
|
59
|
+
"psycopg not installed. Install with: pip install 'alma-memory[postgres]'"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PostgreSQLStorage(StorageBackend):
|
|
64
|
+
"""
|
|
65
|
+
PostgreSQL storage backend with pgvector support.
|
|
66
|
+
|
|
67
|
+
Uses native PostgreSQL vector operations for efficient similarity search.
|
|
68
|
+
Falls back to application-level cosine similarity if pgvector is not installed.
|
|
69
|
+
|
|
70
|
+
Database schema (uses canonical memory type names with alma_ prefix):
|
|
71
|
+
- alma_heuristics: id, agent, project_id, condition, strategy, ...
|
|
72
|
+
- alma_outcomes: id, agent, project_id, task_type, ...
|
|
73
|
+
- alma_preferences: id, user_id, category, preference, ...
|
|
74
|
+
- alma_domain_knowledge: id, agent, project_id, domain, fact, ...
|
|
75
|
+
- alma_anti_patterns: id, agent, project_id, pattern, ...
|
|
76
|
+
|
|
77
|
+
Vector search:
|
|
78
|
+
- Uses pgvector extension if available
|
|
79
|
+
- Embeddings stored as VECTOR type with cosine distance operator (<=>)
|
|
80
|
+
|
|
81
|
+
Table names are derived from alma.storage.constants.POSTGRESQL_TABLE_NAMES
|
|
82
|
+
for consistency across all storage backends.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
# Table names from constants for consistent naming
|
|
86
|
+
TABLE_NAMES = POSTGRESQL_TABLE_NAMES
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
host: str,
|
|
91
|
+
port: int,
|
|
92
|
+
database: str,
|
|
93
|
+
user: str,
|
|
94
|
+
password: str,
|
|
95
|
+
embedding_dim: int = 384,
|
|
96
|
+
pool_size: int = 10,
|
|
97
|
+
schema: str = "public",
|
|
98
|
+
ssl_mode: str = "prefer",
|
|
99
|
+
auto_migrate: bool = True,
|
|
100
|
+
):
|
|
101
|
+
"""
|
|
102
|
+
Initialize PostgreSQL storage.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
host: Database host
|
|
106
|
+
port: Database port
|
|
107
|
+
database: Database name
|
|
108
|
+
user: Database user
|
|
109
|
+
password: Database password
|
|
110
|
+
embedding_dim: Dimension of embedding vectors
|
|
111
|
+
pool_size: Connection pool size
|
|
112
|
+
schema: Database schema (default: public)
|
|
113
|
+
ssl_mode: SSL mode (disable, allow, prefer, require, verify-ca, verify-full)
|
|
114
|
+
auto_migrate: If True, automatically apply pending migrations on startup
|
|
115
|
+
"""
|
|
116
|
+
if not PSYCOPG_AVAILABLE:
|
|
117
|
+
raise ImportError(
|
|
118
|
+
"psycopg not installed. Install with: pip install 'alma-memory[postgres]'"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self.embedding_dim = embedding_dim
|
|
122
|
+
self.schema = schema
|
|
123
|
+
self._pgvector_available = False
|
|
124
|
+
|
|
125
|
+
# Migration support (lazy-loaded)
|
|
126
|
+
self._migration_runner = None
|
|
127
|
+
self._version_store = None
|
|
128
|
+
|
|
129
|
+
# Build connection string
|
|
130
|
+
conninfo = (
|
|
131
|
+
f"host={host} port={port} dbname={database} "
|
|
132
|
+
f"user={user} password={password} sslmode={ssl_mode}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Create connection pool
|
|
136
|
+
self._pool = ConnectionPool(
|
|
137
|
+
conninfo=conninfo,
|
|
138
|
+
min_size=1,
|
|
139
|
+
max_size=pool_size,
|
|
140
|
+
kwargs={"row_factory": dict_row},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Initialize database
|
|
144
|
+
self._init_database()
|
|
145
|
+
|
|
146
|
+
# Auto-migrate if enabled
|
|
147
|
+
if auto_migrate:
|
|
148
|
+
self._ensure_migrated()
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_config(cls, config: Dict[str, Any]) -> "PostgreSQLStorage":
|
|
152
|
+
"""Create instance from configuration."""
|
|
153
|
+
pg_config = config.get("postgres", {})
|
|
154
|
+
|
|
155
|
+
# Support environment variable expansion
|
|
156
|
+
def get_value(key: str, default: Any = None) -> Any:
|
|
157
|
+
value = pg_config.get(key, default)
|
|
158
|
+
if (
|
|
159
|
+
isinstance(value, str)
|
|
160
|
+
and value.startswith("${")
|
|
161
|
+
and value.endswith("}")
|
|
162
|
+
):
|
|
163
|
+
env_var = value[2:-1]
|
|
164
|
+
return os.environ.get(env_var, default)
|
|
165
|
+
return value
|
|
166
|
+
|
|
167
|
+
return cls(
|
|
168
|
+
host=get_value("host", "localhost"),
|
|
169
|
+
port=int(get_value("port", 5432)),
|
|
170
|
+
database=get_value("database", "alma_memory"),
|
|
171
|
+
user=get_value("user", "postgres"),
|
|
172
|
+
password=get_value("password", ""),
|
|
173
|
+
embedding_dim=int(config.get("embedding_dim", 384)),
|
|
174
|
+
pool_size=int(get_value("pool_size", 10)),
|
|
175
|
+
schema=get_value("schema", "public"),
|
|
176
|
+
ssl_mode=get_value("ssl_mode", "prefer"),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
@contextmanager
|
|
180
|
+
def _get_connection(self):
|
|
181
|
+
"""Get database connection from pool."""
|
|
182
|
+
with self._pool.connection() as conn:
|
|
183
|
+
yield conn
|
|
184
|
+
|
|
185
|
+
def _init_database(self):
|
|
186
|
+
"""Initialize database schema and pgvector extension."""
|
|
187
|
+
with self._get_connection() as conn:
|
|
188
|
+
# Try to enable pgvector extension
|
|
189
|
+
try:
|
|
190
|
+
conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
|
191
|
+
conn.commit()
|
|
192
|
+
self._pgvector_available = True
|
|
193
|
+
logger.info("pgvector extension enabled")
|
|
194
|
+
except Exception as e:
|
|
195
|
+
conn.rollback() # Important: rollback to clear aborted transaction
|
|
196
|
+
logger.warning(f"pgvector not available: {e}. Using fallback search.")
|
|
197
|
+
self._pgvector_available = False
|
|
198
|
+
|
|
199
|
+
# Create tables
|
|
200
|
+
vector_type = (
|
|
201
|
+
f"VECTOR({self.embedding_dim})" if self._pgvector_available else "BYTEA"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Heuristics table
|
|
205
|
+
heuristics_table = self.TABLE_NAMES[MemoryType.HEURISTICS]
|
|
206
|
+
conn.execute(f"""
|
|
207
|
+
CREATE TABLE IF NOT EXISTS {self.schema}.{heuristics_table} (
|
|
208
|
+
id TEXT PRIMARY KEY,
|
|
209
|
+
agent TEXT NOT NULL,
|
|
210
|
+
project_id TEXT NOT NULL,
|
|
211
|
+
condition TEXT NOT NULL,
|
|
212
|
+
strategy TEXT NOT NULL,
|
|
213
|
+
confidence REAL DEFAULT 0.0,
|
|
214
|
+
occurrence_count INTEGER DEFAULT 0,
|
|
215
|
+
success_count INTEGER DEFAULT 0,
|
|
216
|
+
last_validated TIMESTAMPTZ,
|
|
217
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
218
|
+
metadata JSONB,
|
|
219
|
+
embedding {vector_type}
|
|
220
|
+
)
|
|
221
|
+
""")
|
|
222
|
+
conn.execute(f"""
|
|
223
|
+
CREATE INDEX IF NOT EXISTS idx_heuristics_project_agent
|
|
224
|
+
ON {self.schema}.{heuristics_table}(project_id, agent)
|
|
225
|
+
""")
|
|
226
|
+
# Confidence index for efficient filtering by confidence score
|
|
227
|
+
conn.execute(f"""
|
|
228
|
+
CREATE INDEX IF NOT EXISTS idx_heuristics_confidence
|
|
229
|
+
ON {self.schema}.{heuristics_table}(project_id, confidence DESC)
|
|
230
|
+
""")
|
|
231
|
+
|
|
232
|
+
# Outcomes table
|
|
233
|
+
outcomes_table = self.TABLE_NAMES[MemoryType.OUTCOMES]
|
|
234
|
+
conn.execute(f"""
|
|
235
|
+
CREATE TABLE IF NOT EXISTS {self.schema}.{outcomes_table} (
|
|
236
|
+
id TEXT PRIMARY KEY,
|
|
237
|
+
agent TEXT NOT NULL,
|
|
238
|
+
project_id TEXT NOT NULL,
|
|
239
|
+
task_type TEXT,
|
|
240
|
+
task_description TEXT NOT NULL,
|
|
241
|
+
success BOOLEAN DEFAULT FALSE,
|
|
242
|
+
strategy_used TEXT,
|
|
243
|
+
duration_ms INTEGER,
|
|
244
|
+
error_message TEXT,
|
|
245
|
+
user_feedback TEXT,
|
|
246
|
+
timestamp TIMESTAMPTZ DEFAULT NOW(),
|
|
247
|
+
metadata JSONB,
|
|
248
|
+
embedding {vector_type}
|
|
249
|
+
)
|
|
250
|
+
""")
|
|
251
|
+
conn.execute(f"""
|
|
252
|
+
CREATE INDEX IF NOT EXISTS idx_outcomes_project_agent
|
|
253
|
+
ON {self.schema}.{outcomes_table}(project_id, agent)
|
|
254
|
+
""")
|
|
255
|
+
conn.execute(f"""
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_outcomes_task_type
|
|
257
|
+
ON {self.schema}.{outcomes_table}(project_id, agent, task_type)
|
|
258
|
+
""")
|
|
259
|
+
conn.execute(f"""
|
|
260
|
+
CREATE INDEX IF NOT EXISTS idx_outcomes_timestamp
|
|
261
|
+
ON {self.schema}.{outcomes_table}(project_id, timestamp DESC)
|
|
262
|
+
""")
|
|
263
|
+
|
|
264
|
+
# User preferences table
|
|
265
|
+
preferences_table = self.TABLE_NAMES[MemoryType.PREFERENCES]
|
|
266
|
+
conn.execute(f"""
|
|
267
|
+
CREATE TABLE IF NOT EXISTS {self.schema}.{preferences_table} (
|
|
268
|
+
id TEXT PRIMARY KEY,
|
|
269
|
+
user_id TEXT NOT NULL,
|
|
270
|
+
category TEXT,
|
|
271
|
+
preference TEXT NOT NULL,
|
|
272
|
+
source TEXT,
|
|
273
|
+
confidence REAL DEFAULT 1.0,
|
|
274
|
+
timestamp TIMESTAMPTZ DEFAULT NOW(),
|
|
275
|
+
metadata JSONB
|
|
276
|
+
)
|
|
277
|
+
""")
|
|
278
|
+
conn.execute(f"""
|
|
279
|
+
CREATE INDEX IF NOT EXISTS idx_preferences_user
|
|
280
|
+
ON {self.schema}.{preferences_table}(user_id)
|
|
281
|
+
""")
|
|
282
|
+
|
|
283
|
+
# Domain knowledge table
|
|
284
|
+
domain_knowledge_table = self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]
|
|
285
|
+
conn.execute(f"""
|
|
286
|
+
CREATE TABLE IF NOT EXISTS {self.schema}.{domain_knowledge_table} (
|
|
287
|
+
id TEXT PRIMARY KEY,
|
|
288
|
+
agent TEXT NOT NULL,
|
|
289
|
+
project_id TEXT NOT NULL,
|
|
290
|
+
domain TEXT,
|
|
291
|
+
fact TEXT NOT NULL,
|
|
292
|
+
source TEXT,
|
|
293
|
+
confidence REAL DEFAULT 1.0,
|
|
294
|
+
last_verified TIMESTAMPTZ DEFAULT NOW(),
|
|
295
|
+
metadata JSONB,
|
|
296
|
+
embedding {vector_type}
|
|
297
|
+
)
|
|
298
|
+
""")
|
|
299
|
+
conn.execute(f"""
|
|
300
|
+
CREATE INDEX IF NOT EXISTS idx_domain_knowledge_project_agent
|
|
301
|
+
ON {self.schema}.{domain_knowledge_table}(project_id, agent)
|
|
302
|
+
""")
|
|
303
|
+
# Confidence index for efficient filtering by confidence score
|
|
304
|
+
conn.execute(f"""
|
|
305
|
+
CREATE INDEX IF NOT EXISTS idx_domain_knowledge_confidence
|
|
306
|
+
ON {self.schema}.{domain_knowledge_table}(project_id, confidence DESC)
|
|
307
|
+
""")
|
|
308
|
+
|
|
309
|
+
# Anti-patterns table
|
|
310
|
+
anti_patterns_table = self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]
|
|
311
|
+
conn.execute(f"""
|
|
312
|
+
CREATE TABLE IF NOT EXISTS {self.schema}.{anti_patterns_table} (
|
|
313
|
+
id TEXT PRIMARY KEY,
|
|
314
|
+
agent TEXT NOT NULL,
|
|
315
|
+
project_id TEXT NOT NULL,
|
|
316
|
+
pattern TEXT NOT NULL,
|
|
317
|
+
why_bad TEXT,
|
|
318
|
+
better_alternative TEXT,
|
|
319
|
+
occurrence_count INTEGER DEFAULT 1,
|
|
320
|
+
last_seen TIMESTAMPTZ DEFAULT NOW(),
|
|
321
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
322
|
+
metadata JSONB,
|
|
323
|
+
embedding {vector_type}
|
|
324
|
+
)
|
|
325
|
+
""")
|
|
326
|
+
conn.execute(f"""
|
|
327
|
+
CREATE INDEX IF NOT EXISTS idx_anti_patterns_project_agent
|
|
328
|
+
ON {self.schema}.{anti_patterns_table}(project_id, agent)
|
|
329
|
+
""")
|
|
330
|
+
|
|
331
|
+
# Create vector indexes if pgvector available
|
|
332
|
+
# Using HNSW instead of IVFFlat because HNSW can be built on empty tables
|
|
333
|
+
# IVFFlat requires existing data to build, which causes silent failures on fresh databases
|
|
334
|
+
if self._pgvector_available:
|
|
335
|
+
# Vector-enabled tables use canonical memory type names
|
|
336
|
+
vector_tables = [
|
|
337
|
+
self.TABLE_NAMES[mt] for mt in MemoryType.VECTOR_ENABLED
|
|
338
|
+
]
|
|
339
|
+
for table in vector_tables:
|
|
340
|
+
try:
|
|
341
|
+
conn.execute(f"""
|
|
342
|
+
CREATE INDEX IF NOT EXISTS idx_{table}_embedding
|
|
343
|
+
ON {self.schema}.{table}
|
|
344
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
345
|
+
WITH (m = 16, ef_construction = 64)
|
|
346
|
+
""")
|
|
347
|
+
except Exception as e:
|
|
348
|
+
logger.warning(f"Failed to create HNSW index for {table}: {e}")
|
|
349
|
+
|
|
350
|
+
conn.commit()
|
|
351
|
+
|
|
352
|
+
def _embedding_to_db(self, embedding: Optional[List[float]]) -> Any:
|
|
353
|
+
"""Convert embedding to database format."""
|
|
354
|
+
if embedding is None:
|
|
355
|
+
return None
|
|
356
|
+
if self._pgvector_available:
|
|
357
|
+
# pgvector expects string format: '[1.0, 2.0, 3.0]'
|
|
358
|
+
return f"[{','.join(str(x) for x in embedding)}]"
|
|
359
|
+
else:
|
|
360
|
+
# Store as bytes (requires numpy)
|
|
361
|
+
if not NUMPY_AVAILABLE:
|
|
362
|
+
raise ImportError("numpy required for non-pgvector embedding storage")
|
|
363
|
+
return np.array(embedding, dtype=np.float32).tobytes()
|
|
364
|
+
|
|
365
|
+
def _embedding_from_db(self, value: Any) -> Optional[List[float]]:
|
|
366
|
+
"""Convert embedding from database format."""
|
|
367
|
+
if value is None:
|
|
368
|
+
return None
|
|
369
|
+
if self._pgvector_available:
|
|
370
|
+
# pgvector returns as string or list
|
|
371
|
+
if isinstance(value, str):
|
|
372
|
+
value = value.strip("[]")
|
|
373
|
+
return [float(x) for x in value.split(",")]
|
|
374
|
+
return list(value)
|
|
375
|
+
else:
|
|
376
|
+
# Stored as bytes (requires numpy)
|
|
377
|
+
if not NUMPY_AVAILABLE or np is None:
|
|
378
|
+
return None
|
|
379
|
+
return np.frombuffer(value, dtype=np.float32).tolist()
|
|
380
|
+
|
|
381
|
+
def _cosine_similarity(self, a: List[float], b: List[float]) -> float:
|
|
382
|
+
"""Compute cosine similarity between two vectors."""
|
|
383
|
+
if not NUMPY_AVAILABLE or np is None:
|
|
384
|
+
# Fallback to pure Python
|
|
385
|
+
dot = sum(x * y for x, y in zip(a, b, strict=False))
|
|
386
|
+
norm_a = sum(x * x for x in a) ** 0.5
|
|
387
|
+
norm_b = sum(x * x for x in b) ** 0.5
|
|
388
|
+
return dot / (norm_a * norm_b) if norm_a and norm_b else 0.0
|
|
389
|
+
a_arr = np.array(a)
|
|
390
|
+
b_arr = np.array(b)
|
|
391
|
+
return float(
|
|
392
|
+
np.dot(a_arr, b_arr) / (np.linalg.norm(a_arr) * np.linalg.norm(b_arr))
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# ==================== WRITE OPERATIONS ====================
|
|
396
|
+
|
|
397
|
+
def save_heuristic(self, heuristic: Heuristic) -> str:
|
|
398
|
+
"""Save a heuristic."""
|
|
399
|
+
with self._get_connection() as conn:
|
|
400
|
+
conn.execute(
|
|
401
|
+
f"""
|
|
402
|
+
INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
|
|
403
|
+
(id, agent, project_id, condition, strategy, confidence,
|
|
404
|
+
occurrence_count, success_count, last_validated, created_at, metadata, embedding)
|
|
405
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
406
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
407
|
+
condition = EXCLUDED.condition,
|
|
408
|
+
strategy = EXCLUDED.strategy,
|
|
409
|
+
confidence = EXCLUDED.confidence,
|
|
410
|
+
occurrence_count = EXCLUDED.occurrence_count,
|
|
411
|
+
success_count = EXCLUDED.success_count,
|
|
412
|
+
last_validated = EXCLUDED.last_validated,
|
|
413
|
+
metadata = EXCLUDED.metadata,
|
|
414
|
+
embedding = EXCLUDED.embedding
|
|
415
|
+
""",
|
|
416
|
+
(
|
|
417
|
+
heuristic.id,
|
|
418
|
+
heuristic.agent,
|
|
419
|
+
heuristic.project_id,
|
|
420
|
+
heuristic.condition,
|
|
421
|
+
heuristic.strategy,
|
|
422
|
+
heuristic.confidence,
|
|
423
|
+
heuristic.occurrence_count,
|
|
424
|
+
heuristic.success_count,
|
|
425
|
+
heuristic.last_validated,
|
|
426
|
+
heuristic.created_at,
|
|
427
|
+
json.dumps(heuristic.metadata) if heuristic.metadata else None,
|
|
428
|
+
self._embedding_to_db(heuristic.embedding),
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
conn.commit()
|
|
432
|
+
|
|
433
|
+
logger.debug(f"Saved heuristic: {heuristic.id}")
|
|
434
|
+
return heuristic.id
|
|
435
|
+
|
|
436
|
+
def save_outcome(self, outcome: Outcome) -> str:
|
|
437
|
+
"""Save an outcome."""
|
|
438
|
+
with self._get_connection() as conn:
|
|
439
|
+
conn.execute(
|
|
440
|
+
f"""
|
|
441
|
+
INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
|
|
442
|
+
(id, agent, project_id, task_type, task_description, success,
|
|
443
|
+
strategy_used, duration_ms, error_message, user_feedback, timestamp, metadata, embedding)
|
|
444
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
445
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
446
|
+
task_description = EXCLUDED.task_description,
|
|
447
|
+
success = EXCLUDED.success,
|
|
448
|
+
strategy_used = EXCLUDED.strategy_used,
|
|
449
|
+
duration_ms = EXCLUDED.duration_ms,
|
|
450
|
+
error_message = EXCLUDED.error_message,
|
|
451
|
+
user_feedback = EXCLUDED.user_feedback,
|
|
452
|
+
metadata = EXCLUDED.metadata,
|
|
453
|
+
embedding = EXCLUDED.embedding
|
|
454
|
+
""",
|
|
455
|
+
(
|
|
456
|
+
outcome.id,
|
|
457
|
+
outcome.agent,
|
|
458
|
+
outcome.project_id,
|
|
459
|
+
outcome.task_type,
|
|
460
|
+
outcome.task_description,
|
|
461
|
+
outcome.success,
|
|
462
|
+
outcome.strategy_used,
|
|
463
|
+
outcome.duration_ms,
|
|
464
|
+
outcome.error_message,
|
|
465
|
+
outcome.user_feedback,
|
|
466
|
+
outcome.timestamp,
|
|
467
|
+
json.dumps(outcome.metadata) if outcome.metadata else None,
|
|
468
|
+
self._embedding_to_db(outcome.embedding),
|
|
469
|
+
),
|
|
470
|
+
)
|
|
471
|
+
conn.commit()
|
|
472
|
+
|
|
473
|
+
logger.debug(f"Saved outcome: {outcome.id}")
|
|
474
|
+
return outcome.id
|
|
475
|
+
|
|
476
|
+
def save_user_preference(self, preference: UserPreference) -> str:
|
|
477
|
+
"""Save a user preference."""
|
|
478
|
+
with self._get_connection() as conn:
|
|
479
|
+
conn.execute(
|
|
480
|
+
f"""
|
|
481
|
+
INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.PREFERENCES]}
|
|
482
|
+
(id, user_id, category, preference, source, confidence, timestamp, metadata)
|
|
483
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
|
484
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
485
|
+
preference = EXCLUDED.preference,
|
|
486
|
+
source = EXCLUDED.source,
|
|
487
|
+
confidence = EXCLUDED.confidence,
|
|
488
|
+
metadata = EXCLUDED.metadata
|
|
489
|
+
""",
|
|
490
|
+
(
|
|
491
|
+
preference.id,
|
|
492
|
+
preference.user_id,
|
|
493
|
+
preference.category,
|
|
494
|
+
preference.preference,
|
|
495
|
+
preference.source,
|
|
496
|
+
preference.confidence,
|
|
497
|
+
preference.timestamp,
|
|
498
|
+
json.dumps(preference.metadata) if preference.metadata else None,
|
|
499
|
+
),
|
|
500
|
+
)
|
|
501
|
+
conn.commit()
|
|
502
|
+
|
|
503
|
+
logger.debug(f"Saved preference: {preference.id}")
|
|
504
|
+
return preference.id
|
|
505
|
+
|
|
506
|
+
def save_domain_knowledge(self, knowledge: DomainKnowledge) -> str:
|
|
507
|
+
"""Save domain knowledge."""
|
|
508
|
+
with self._get_connection() as conn:
|
|
509
|
+
conn.execute(
|
|
510
|
+
f"""
|
|
511
|
+
INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
|
|
512
|
+
(id, agent, project_id, domain, fact, source, confidence, last_verified, metadata, embedding)
|
|
513
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
514
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
515
|
+
fact = EXCLUDED.fact,
|
|
516
|
+
source = EXCLUDED.source,
|
|
517
|
+
confidence = EXCLUDED.confidence,
|
|
518
|
+
last_verified = EXCLUDED.last_verified,
|
|
519
|
+
metadata = EXCLUDED.metadata,
|
|
520
|
+
embedding = EXCLUDED.embedding
|
|
521
|
+
""",
|
|
522
|
+
(
|
|
523
|
+
knowledge.id,
|
|
524
|
+
knowledge.agent,
|
|
525
|
+
knowledge.project_id,
|
|
526
|
+
knowledge.domain,
|
|
527
|
+
knowledge.fact,
|
|
528
|
+
knowledge.source,
|
|
529
|
+
knowledge.confidence,
|
|
530
|
+
knowledge.last_verified,
|
|
531
|
+
json.dumps(knowledge.metadata) if knowledge.metadata else None,
|
|
532
|
+
self._embedding_to_db(knowledge.embedding),
|
|
533
|
+
),
|
|
534
|
+
)
|
|
535
|
+
conn.commit()
|
|
536
|
+
|
|
537
|
+
logger.debug(f"Saved domain knowledge: {knowledge.id}")
|
|
538
|
+
return knowledge.id
|
|
539
|
+
|
|
540
|
+
def save_anti_pattern(self, anti_pattern: AntiPattern) -> str:
|
|
541
|
+
"""Save an anti-pattern."""
|
|
542
|
+
with self._get_connection() as conn:
|
|
543
|
+
conn.execute(
|
|
544
|
+
f"""
|
|
545
|
+
INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]}
|
|
546
|
+
(id, agent, project_id, pattern, why_bad, better_alternative,
|
|
547
|
+
occurrence_count, last_seen, created_at, metadata, embedding)
|
|
548
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
549
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
550
|
+
pattern = EXCLUDED.pattern,
|
|
551
|
+
why_bad = EXCLUDED.why_bad,
|
|
552
|
+
better_alternative = EXCLUDED.better_alternative,
|
|
553
|
+
occurrence_count = EXCLUDED.occurrence_count,
|
|
554
|
+
last_seen = EXCLUDED.last_seen,
|
|
555
|
+
metadata = EXCLUDED.metadata,
|
|
556
|
+
embedding = EXCLUDED.embedding
|
|
557
|
+
""",
|
|
558
|
+
(
|
|
559
|
+
anti_pattern.id,
|
|
560
|
+
anti_pattern.agent,
|
|
561
|
+
anti_pattern.project_id,
|
|
562
|
+
anti_pattern.pattern,
|
|
563
|
+
anti_pattern.why_bad,
|
|
564
|
+
anti_pattern.better_alternative,
|
|
565
|
+
anti_pattern.occurrence_count,
|
|
566
|
+
anti_pattern.last_seen,
|
|
567
|
+
anti_pattern.created_at,
|
|
568
|
+
(
|
|
569
|
+
json.dumps(anti_pattern.metadata)
|
|
570
|
+
if anti_pattern.metadata
|
|
571
|
+
else None
|
|
572
|
+
),
|
|
573
|
+
self._embedding_to_db(anti_pattern.embedding),
|
|
574
|
+
),
|
|
575
|
+
)
|
|
576
|
+
conn.commit()
|
|
577
|
+
|
|
578
|
+
logger.debug(f"Saved anti-pattern: {anti_pattern.id}")
|
|
579
|
+
return anti_pattern.id
|
|
580
|
+
|
|
581
|
+
# ==================== BATCH WRITE OPERATIONS ====================
|
|
582
|
+
|
|
583
|
+
def save_heuristics(self, heuristics: List[Heuristic]) -> List[str]:
|
|
584
|
+
"""Save multiple heuristics in a batch using executemany."""
|
|
585
|
+
if not heuristics:
|
|
586
|
+
return []
|
|
587
|
+
|
|
588
|
+
with self._get_connection() as conn:
|
|
589
|
+
conn.executemany(
|
|
590
|
+
f"""
|
|
591
|
+
INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
|
|
592
|
+
(id, agent, project_id, condition, strategy, confidence,
|
|
593
|
+
occurrence_count, success_count, last_validated, created_at, metadata, embedding)
|
|
594
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
595
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
596
|
+
condition = EXCLUDED.condition,
|
|
597
|
+
strategy = EXCLUDED.strategy,
|
|
598
|
+
confidence = EXCLUDED.confidence,
|
|
599
|
+
occurrence_count = EXCLUDED.occurrence_count,
|
|
600
|
+
success_count = EXCLUDED.success_count,
|
|
601
|
+
last_validated = EXCLUDED.last_validated,
|
|
602
|
+
metadata = EXCLUDED.metadata,
|
|
603
|
+
embedding = EXCLUDED.embedding
|
|
604
|
+
""",
|
|
605
|
+
[
|
|
606
|
+
(
|
|
607
|
+
h.id,
|
|
608
|
+
h.agent,
|
|
609
|
+
h.project_id,
|
|
610
|
+
h.condition,
|
|
611
|
+
h.strategy,
|
|
612
|
+
h.confidence,
|
|
613
|
+
h.occurrence_count,
|
|
614
|
+
h.success_count,
|
|
615
|
+
h.last_validated,
|
|
616
|
+
h.created_at,
|
|
617
|
+
json.dumps(h.metadata) if h.metadata else None,
|
|
618
|
+
self._embedding_to_db(h.embedding),
|
|
619
|
+
)
|
|
620
|
+
for h in heuristics
|
|
621
|
+
],
|
|
622
|
+
)
|
|
623
|
+
conn.commit()
|
|
624
|
+
|
|
625
|
+
logger.debug(f"Batch saved {len(heuristics)} heuristics")
|
|
626
|
+
return [h.id for h in heuristics]
|
|
627
|
+
|
|
628
|
+
def save_outcomes(self, outcomes: List[Outcome]) -> List[str]:
|
|
629
|
+
"""Save multiple outcomes in a batch using executemany."""
|
|
630
|
+
if not outcomes:
|
|
631
|
+
return []
|
|
632
|
+
|
|
633
|
+
with self._get_connection() as conn:
|
|
634
|
+
conn.executemany(
|
|
635
|
+
f"""
|
|
636
|
+
INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
|
|
637
|
+
(id, agent, project_id, task_type, task_description, success,
|
|
638
|
+
strategy_used, duration_ms, error_message, user_feedback, timestamp, metadata, embedding)
|
|
639
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
640
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
641
|
+
task_description = EXCLUDED.task_description,
|
|
642
|
+
success = EXCLUDED.success,
|
|
643
|
+
strategy_used = EXCLUDED.strategy_used,
|
|
644
|
+
duration_ms = EXCLUDED.duration_ms,
|
|
645
|
+
error_message = EXCLUDED.error_message,
|
|
646
|
+
user_feedback = EXCLUDED.user_feedback,
|
|
647
|
+
metadata = EXCLUDED.metadata,
|
|
648
|
+
embedding = EXCLUDED.embedding
|
|
649
|
+
""",
|
|
650
|
+
[
|
|
651
|
+
(
|
|
652
|
+
o.id,
|
|
653
|
+
o.agent,
|
|
654
|
+
o.project_id,
|
|
655
|
+
o.task_type,
|
|
656
|
+
o.task_description,
|
|
657
|
+
o.success,
|
|
658
|
+
o.strategy_used,
|
|
659
|
+
o.duration_ms,
|
|
660
|
+
o.error_message,
|
|
661
|
+
o.user_feedback,
|
|
662
|
+
o.timestamp,
|
|
663
|
+
json.dumps(o.metadata) if o.metadata else None,
|
|
664
|
+
self._embedding_to_db(o.embedding),
|
|
665
|
+
)
|
|
666
|
+
for o in outcomes
|
|
667
|
+
],
|
|
668
|
+
)
|
|
669
|
+
conn.commit()
|
|
670
|
+
|
|
671
|
+
logger.debug(f"Batch saved {len(outcomes)} outcomes")
|
|
672
|
+
return [o.id for o in outcomes]
|
|
673
|
+
|
|
674
|
+
def save_domain_knowledge_batch(
|
|
675
|
+
self, knowledge_items: List[DomainKnowledge]
|
|
676
|
+
) -> List[str]:
|
|
677
|
+
"""Save multiple domain knowledge items in a batch using executemany."""
|
|
678
|
+
if not knowledge_items:
|
|
679
|
+
return []
|
|
680
|
+
|
|
681
|
+
with self._get_connection() as conn:
|
|
682
|
+
conn.executemany(
|
|
683
|
+
f"""
|
|
684
|
+
INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
|
|
685
|
+
(id, agent, project_id, domain, fact, source, confidence, last_verified, metadata, embedding)
|
|
686
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
687
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
688
|
+
fact = EXCLUDED.fact,
|
|
689
|
+
source = EXCLUDED.source,
|
|
690
|
+
confidence = EXCLUDED.confidence,
|
|
691
|
+
last_verified = EXCLUDED.last_verified,
|
|
692
|
+
metadata = EXCLUDED.metadata,
|
|
693
|
+
embedding = EXCLUDED.embedding
|
|
694
|
+
""",
|
|
695
|
+
[
|
|
696
|
+
(
|
|
697
|
+
k.id,
|
|
698
|
+
k.agent,
|
|
699
|
+
k.project_id,
|
|
700
|
+
k.domain,
|
|
701
|
+
k.fact,
|
|
702
|
+
k.source,
|
|
703
|
+
k.confidence,
|
|
704
|
+
k.last_verified,
|
|
705
|
+
json.dumps(k.metadata) if k.metadata else None,
|
|
706
|
+
self._embedding_to_db(k.embedding),
|
|
707
|
+
)
|
|
708
|
+
for k in knowledge_items
|
|
709
|
+
],
|
|
710
|
+
)
|
|
711
|
+
conn.commit()
|
|
712
|
+
|
|
713
|
+
logger.debug(f"Batch saved {len(knowledge_items)} domain knowledge items")
|
|
714
|
+
return [k.id for k in knowledge_items]
|
|
715
|
+
|
|
716
|
+
# ==================== READ OPERATIONS ====================
|
|
717
|
+
|
|
718
|
+
def get_heuristics(
|
|
719
|
+
self,
|
|
720
|
+
project_id: str,
|
|
721
|
+
agent: Optional[str] = None,
|
|
722
|
+
embedding: Optional[List[float]] = None,
|
|
723
|
+
top_k: int = 5,
|
|
724
|
+
min_confidence: float = 0.0,
|
|
725
|
+
) -> List[Heuristic]:
|
|
726
|
+
"""Get heuristics with optional vector search."""
|
|
727
|
+
with self._get_connection() as conn:
|
|
728
|
+
if embedding and self._pgvector_available:
|
|
729
|
+
# Use pgvector similarity search
|
|
730
|
+
query = f"""
|
|
731
|
+
SELECT *, 1 - (embedding <=> %s::vector) as similarity
|
|
732
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
|
|
733
|
+
WHERE project_id = %s AND confidence >= %s
|
|
734
|
+
"""
|
|
735
|
+
params: List[Any] = [
|
|
736
|
+
self._embedding_to_db(embedding),
|
|
737
|
+
project_id,
|
|
738
|
+
min_confidence,
|
|
739
|
+
]
|
|
740
|
+
|
|
741
|
+
if agent:
|
|
742
|
+
query += " AND agent = %s"
|
|
743
|
+
params.append(agent)
|
|
744
|
+
|
|
745
|
+
query += " ORDER BY similarity DESC LIMIT %s"
|
|
746
|
+
params.append(top_k)
|
|
747
|
+
else:
|
|
748
|
+
# Standard query
|
|
749
|
+
query = f"""
|
|
750
|
+
SELECT *
|
|
751
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
|
|
752
|
+
WHERE project_id = %s AND confidence >= %s
|
|
753
|
+
"""
|
|
754
|
+
params = [project_id, min_confidence]
|
|
755
|
+
|
|
756
|
+
if agent:
|
|
757
|
+
query += " AND agent = %s"
|
|
758
|
+
params.append(agent)
|
|
759
|
+
|
|
760
|
+
query += " ORDER BY confidence DESC LIMIT %s"
|
|
761
|
+
params.append(top_k)
|
|
762
|
+
|
|
763
|
+
cursor = conn.execute(query, params)
|
|
764
|
+
rows = cursor.fetchall()
|
|
765
|
+
|
|
766
|
+
results = [self._row_to_heuristic(row) for row in rows]
|
|
767
|
+
|
|
768
|
+
# If embedding provided but pgvector not available, do app-level filtering
|
|
769
|
+
if embedding and not self._pgvector_available and results:
|
|
770
|
+
results = self._filter_by_similarity(results, embedding, top_k, "embedding")
|
|
771
|
+
|
|
772
|
+
return results
|
|
773
|
+
|
|
774
|
+
def get_outcomes(
|
|
775
|
+
self,
|
|
776
|
+
project_id: str,
|
|
777
|
+
agent: Optional[str] = None,
|
|
778
|
+
task_type: Optional[str] = None,
|
|
779
|
+
embedding: Optional[List[float]] = None,
|
|
780
|
+
top_k: int = 5,
|
|
781
|
+
success_only: bool = False,
|
|
782
|
+
) -> List[Outcome]:
|
|
783
|
+
"""Get outcomes with optional vector search."""
|
|
784
|
+
with self._get_connection() as conn:
|
|
785
|
+
if embedding and self._pgvector_available:
|
|
786
|
+
query = f"""
|
|
787
|
+
SELECT *, 1 - (embedding <=> %s::vector) as similarity
|
|
788
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
|
|
789
|
+
WHERE project_id = %s
|
|
790
|
+
"""
|
|
791
|
+
params: List[Any] = [self._embedding_to_db(embedding), project_id]
|
|
792
|
+
else:
|
|
793
|
+
query = f"""
|
|
794
|
+
SELECT *
|
|
795
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
|
|
796
|
+
WHERE project_id = %s
|
|
797
|
+
"""
|
|
798
|
+
params = [project_id]
|
|
799
|
+
|
|
800
|
+
if agent:
|
|
801
|
+
query += " AND agent = %s"
|
|
802
|
+
params.append(agent)
|
|
803
|
+
|
|
804
|
+
if task_type:
|
|
805
|
+
query += " AND task_type = %s"
|
|
806
|
+
params.append(task_type)
|
|
807
|
+
|
|
808
|
+
if success_only:
|
|
809
|
+
query += " AND success = TRUE"
|
|
810
|
+
|
|
811
|
+
if embedding and self._pgvector_available:
|
|
812
|
+
query += " ORDER BY similarity DESC LIMIT %s"
|
|
813
|
+
else:
|
|
814
|
+
query += " ORDER BY timestamp DESC LIMIT %s"
|
|
815
|
+
params.append(top_k)
|
|
816
|
+
|
|
817
|
+
cursor = conn.execute(query, params)
|
|
818
|
+
rows = cursor.fetchall()
|
|
819
|
+
|
|
820
|
+
results = [self._row_to_outcome(row) for row in rows]
|
|
821
|
+
|
|
822
|
+
if embedding and not self._pgvector_available and results:
|
|
823
|
+
results = self._filter_by_similarity(results, embedding, top_k, "embedding")
|
|
824
|
+
|
|
825
|
+
return results
|
|
826
|
+
|
|
827
|
+
def get_user_preferences(
|
|
828
|
+
self,
|
|
829
|
+
user_id: str,
|
|
830
|
+
category: Optional[str] = None,
|
|
831
|
+
) -> List[UserPreference]:
|
|
832
|
+
"""Get user preferences."""
|
|
833
|
+
with self._get_connection() as conn:
|
|
834
|
+
query = f"SELECT * FROM {self.schema}.{self.TABLE_NAMES[MemoryType.PREFERENCES]} WHERE user_id = %s"
|
|
835
|
+
params: List[Any] = [user_id]
|
|
836
|
+
|
|
837
|
+
if category:
|
|
838
|
+
query += " AND category = %s"
|
|
839
|
+
params.append(category)
|
|
840
|
+
|
|
841
|
+
cursor = conn.execute(query, params)
|
|
842
|
+
rows = cursor.fetchall()
|
|
843
|
+
|
|
844
|
+
return [self._row_to_preference(row) for row in rows]
|
|
845
|
+
|
|
846
|
+
def get_domain_knowledge(
|
|
847
|
+
self,
|
|
848
|
+
project_id: str,
|
|
849
|
+
agent: Optional[str] = None,
|
|
850
|
+
domain: Optional[str] = None,
|
|
851
|
+
embedding: Optional[List[float]] = None,
|
|
852
|
+
top_k: int = 5,
|
|
853
|
+
) -> List[DomainKnowledge]:
|
|
854
|
+
"""Get domain knowledge with optional vector search."""
|
|
855
|
+
with self._get_connection() as conn:
|
|
856
|
+
if embedding and self._pgvector_available:
|
|
857
|
+
query = f"""
|
|
858
|
+
SELECT *, 1 - (embedding <=> %s::vector) as similarity
|
|
859
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
|
|
860
|
+
WHERE project_id = %s
|
|
861
|
+
"""
|
|
862
|
+
params: List[Any] = [self._embedding_to_db(embedding), project_id]
|
|
863
|
+
else:
|
|
864
|
+
query = f"""
|
|
865
|
+
SELECT *
|
|
866
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
|
|
867
|
+
WHERE project_id = %s
|
|
868
|
+
"""
|
|
869
|
+
params = [project_id]
|
|
870
|
+
|
|
871
|
+
if agent:
|
|
872
|
+
query += " AND agent = %s"
|
|
873
|
+
params.append(agent)
|
|
874
|
+
|
|
875
|
+
if domain:
|
|
876
|
+
query += " AND domain = %s"
|
|
877
|
+
params.append(domain)
|
|
878
|
+
|
|
879
|
+
if embedding and self._pgvector_available:
|
|
880
|
+
query += " ORDER BY similarity DESC LIMIT %s"
|
|
881
|
+
else:
|
|
882
|
+
query += " ORDER BY confidence DESC LIMIT %s"
|
|
883
|
+
params.append(top_k)
|
|
884
|
+
|
|
885
|
+
cursor = conn.execute(query, params)
|
|
886
|
+
rows = cursor.fetchall()
|
|
887
|
+
|
|
888
|
+
results = [self._row_to_domain_knowledge(row) for row in rows]
|
|
889
|
+
|
|
890
|
+
if embedding and not self._pgvector_available and results:
|
|
891
|
+
results = self._filter_by_similarity(results, embedding, top_k, "embedding")
|
|
892
|
+
|
|
893
|
+
return results
|
|
894
|
+
|
|
895
|
+
def get_anti_patterns(
|
|
896
|
+
self,
|
|
897
|
+
project_id: str,
|
|
898
|
+
agent: Optional[str] = None,
|
|
899
|
+
embedding: Optional[List[float]] = None,
|
|
900
|
+
top_k: int = 5,
|
|
901
|
+
) -> List[AntiPattern]:
|
|
902
|
+
"""Get anti-patterns with optional vector search."""
|
|
903
|
+
with self._get_connection() as conn:
|
|
904
|
+
if embedding and self._pgvector_available:
|
|
905
|
+
query = f"""
|
|
906
|
+
SELECT *, 1 - (embedding <=> %s::vector) as similarity
|
|
907
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]}
|
|
908
|
+
WHERE project_id = %s
|
|
909
|
+
"""
|
|
910
|
+
params: List[Any] = [self._embedding_to_db(embedding), project_id]
|
|
911
|
+
else:
|
|
912
|
+
query = f"""
|
|
913
|
+
SELECT *
|
|
914
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]}
|
|
915
|
+
WHERE project_id = %s
|
|
916
|
+
"""
|
|
917
|
+
params = [project_id]
|
|
918
|
+
|
|
919
|
+
if agent:
|
|
920
|
+
query += " AND agent = %s"
|
|
921
|
+
params.append(agent)
|
|
922
|
+
|
|
923
|
+
if embedding and self._pgvector_available:
|
|
924
|
+
query += " ORDER BY similarity DESC LIMIT %s"
|
|
925
|
+
else:
|
|
926
|
+
query += " ORDER BY occurrence_count DESC LIMIT %s"
|
|
927
|
+
params.append(top_k)
|
|
928
|
+
|
|
929
|
+
cursor = conn.execute(query, params)
|
|
930
|
+
rows = cursor.fetchall()
|
|
931
|
+
|
|
932
|
+
results = [self._row_to_anti_pattern(row) for row in rows]
|
|
933
|
+
|
|
934
|
+
if embedding and not self._pgvector_available and results:
|
|
935
|
+
results = self._filter_by_similarity(results, embedding, top_k, "embedding")
|
|
936
|
+
|
|
937
|
+
return results
|
|
938
|
+
|
|
939
|
+
def _filter_by_similarity(
|
|
940
|
+
self,
|
|
941
|
+
items: List[Any],
|
|
942
|
+
query_embedding: List[float],
|
|
943
|
+
top_k: int,
|
|
944
|
+
embedding_attr: str,
|
|
945
|
+
) -> List[Any]:
|
|
946
|
+
"""Filter items by cosine similarity (fallback when pgvector unavailable)."""
|
|
947
|
+
scored = []
|
|
948
|
+
for item in items:
|
|
949
|
+
item_embedding = getattr(item, embedding_attr, None)
|
|
950
|
+
if item_embedding:
|
|
951
|
+
similarity = self._cosine_similarity(query_embedding, item_embedding)
|
|
952
|
+
scored.append((item, similarity))
|
|
953
|
+
else:
|
|
954
|
+
scored.append((item, 0.0))
|
|
955
|
+
|
|
956
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
957
|
+
return [item for item, _ in scored[:top_k]]
|
|
958
|
+
|
|
959
|
+
# ==================== MULTI-AGENT MEMORY SHARING ====================
|
|
960
|
+
|
|
961
|
+
def get_heuristics_for_agents(
|
|
962
|
+
self,
|
|
963
|
+
project_id: str,
|
|
964
|
+
agents: List[str],
|
|
965
|
+
embedding: Optional[List[float]] = None,
|
|
966
|
+
top_k: int = 5,
|
|
967
|
+
min_confidence: float = 0.0,
|
|
968
|
+
) -> List[Heuristic]:
|
|
969
|
+
"""Get heuristics from multiple agents using optimized ANY query."""
|
|
970
|
+
if not agents:
|
|
971
|
+
return []
|
|
972
|
+
|
|
973
|
+
with self._get_connection() as conn:
|
|
974
|
+
if embedding and self._pgvector_available:
|
|
975
|
+
query = f"""
|
|
976
|
+
SELECT *, 1 - (embedding <=> %s::vector) as similarity
|
|
977
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
|
|
978
|
+
WHERE project_id = %s AND confidence >= %s AND agent = ANY(%s)
|
|
979
|
+
ORDER BY similarity DESC LIMIT %s
|
|
980
|
+
"""
|
|
981
|
+
params: List[Any] = [
|
|
982
|
+
self._embedding_to_db(embedding),
|
|
983
|
+
project_id,
|
|
984
|
+
min_confidence,
|
|
985
|
+
agents,
|
|
986
|
+
top_k * len(agents),
|
|
987
|
+
]
|
|
988
|
+
else:
|
|
989
|
+
query = f"""
|
|
990
|
+
SELECT *
|
|
991
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
|
|
992
|
+
WHERE project_id = %s AND confidence >= %s AND agent = ANY(%s)
|
|
993
|
+
ORDER BY confidence DESC LIMIT %s
|
|
994
|
+
"""
|
|
995
|
+
params = [project_id, min_confidence, agents, top_k * len(agents)]
|
|
996
|
+
|
|
997
|
+
cursor = conn.execute(query, params)
|
|
998
|
+
rows = cursor.fetchall()
|
|
999
|
+
|
|
1000
|
+
results = [self._row_to_heuristic(row) for row in rows]
|
|
1001
|
+
|
|
1002
|
+
if embedding and not self._pgvector_available and results:
|
|
1003
|
+
results = self._filter_by_similarity(
|
|
1004
|
+
results, embedding, top_k * len(agents), "embedding"
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
return results
|
|
1008
|
+
|
|
1009
|
+
def get_outcomes_for_agents(
|
|
1010
|
+
self,
|
|
1011
|
+
project_id: str,
|
|
1012
|
+
agents: List[str],
|
|
1013
|
+
task_type: Optional[str] = None,
|
|
1014
|
+
embedding: Optional[List[float]] = None,
|
|
1015
|
+
top_k: int = 5,
|
|
1016
|
+
success_only: bool = False,
|
|
1017
|
+
) -> List[Outcome]:
|
|
1018
|
+
"""Get outcomes from multiple agents using optimized ANY query."""
|
|
1019
|
+
if not agents:
|
|
1020
|
+
return []
|
|
1021
|
+
|
|
1022
|
+
with self._get_connection() as conn:
|
|
1023
|
+
if embedding and self._pgvector_available:
|
|
1024
|
+
query = f"""
|
|
1025
|
+
SELECT *, 1 - (embedding <=> %s::vector) as similarity
|
|
1026
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
|
|
1027
|
+
WHERE project_id = %s AND agent = ANY(%s)
|
|
1028
|
+
"""
|
|
1029
|
+
params: List[Any] = [
|
|
1030
|
+
self._embedding_to_db(embedding),
|
|
1031
|
+
project_id,
|
|
1032
|
+
agents,
|
|
1033
|
+
]
|
|
1034
|
+
else:
|
|
1035
|
+
query = f"""
|
|
1036
|
+
SELECT *
|
|
1037
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
|
|
1038
|
+
WHERE project_id = %s AND agent = ANY(%s)
|
|
1039
|
+
"""
|
|
1040
|
+
params = [project_id, agents]
|
|
1041
|
+
|
|
1042
|
+
if task_type:
|
|
1043
|
+
query += " AND task_type = %s"
|
|
1044
|
+
params.append(task_type)
|
|
1045
|
+
|
|
1046
|
+
if success_only:
|
|
1047
|
+
query += " AND success = TRUE"
|
|
1048
|
+
|
|
1049
|
+
if embedding and self._pgvector_available:
|
|
1050
|
+
query += " ORDER BY similarity DESC LIMIT %s"
|
|
1051
|
+
else:
|
|
1052
|
+
query += " ORDER BY timestamp DESC LIMIT %s"
|
|
1053
|
+
params.append(top_k * len(agents))
|
|
1054
|
+
|
|
1055
|
+
cursor = conn.execute(query, params)
|
|
1056
|
+
rows = cursor.fetchall()
|
|
1057
|
+
|
|
1058
|
+
results = [self._row_to_outcome(row) for row in rows]
|
|
1059
|
+
|
|
1060
|
+
if embedding and not self._pgvector_available and results:
|
|
1061
|
+
results = self._filter_by_similarity(
|
|
1062
|
+
results, embedding, top_k * len(agents), "embedding"
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
return results
|
|
1066
|
+
|
|
1067
|
+
def get_domain_knowledge_for_agents(
|
|
1068
|
+
self,
|
|
1069
|
+
project_id: str,
|
|
1070
|
+
agents: List[str],
|
|
1071
|
+
domain: Optional[str] = None,
|
|
1072
|
+
embedding: Optional[List[float]] = None,
|
|
1073
|
+
top_k: int = 5,
|
|
1074
|
+
) -> List[DomainKnowledge]:
|
|
1075
|
+
"""Get domain knowledge from multiple agents using optimized ANY query."""
|
|
1076
|
+
if not agents:
|
|
1077
|
+
return []
|
|
1078
|
+
|
|
1079
|
+
with self._get_connection() as conn:
|
|
1080
|
+
if embedding and self._pgvector_available:
|
|
1081
|
+
query = f"""
|
|
1082
|
+
SELECT *, 1 - (embedding <=> %s::vector) as similarity
|
|
1083
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
|
|
1084
|
+
WHERE project_id = %s AND agent = ANY(%s)
|
|
1085
|
+
"""
|
|
1086
|
+
params: List[Any] = [
|
|
1087
|
+
self._embedding_to_db(embedding),
|
|
1088
|
+
project_id,
|
|
1089
|
+
agents,
|
|
1090
|
+
]
|
|
1091
|
+
else:
|
|
1092
|
+
query = f"""
|
|
1093
|
+
SELECT *
|
|
1094
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
|
|
1095
|
+
WHERE project_id = %s AND agent = ANY(%s)
|
|
1096
|
+
"""
|
|
1097
|
+
params = [project_id, agents]
|
|
1098
|
+
|
|
1099
|
+
if domain:
|
|
1100
|
+
query += " AND domain = %s"
|
|
1101
|
+
params.append(domain)
|
|
1102
|
+
|
|
1103
|
+
if embedding and self._pgvector_available:
|
|
1104
|
+
query += " ORDER BY similarity DESC LIMIT %s"
|
|
1105
|
+
else:
|
|
1106
|
+
query += " ORDER BY confidence DESC LIMIT %s"
|
|
1107
|
+
params.append(top_k * len(agents))
|
|
1108
|
+
|
|
1109
|
+
cursor = conn.execute(query, params)
|
|
1110
|
+
rows = cursor.fetchall()
|
|
1111
|
+
|
|
1112
|
+
results = [self._row_to_domain_knowledge(row) for row in rows]
|
|
1113
|
+
|
|
1114
|
+
if embedding and not self._pgvector_available and results:
|
|
1115
|
+
results = self._filter_by_similarity(
|
|
1116
|
+
results, embedding, top_k * len(agents), "embedding"
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
return results
|
|
1120
|
+
|
|
1121
|
+
def get_anti_patterns_for_agents(
|
|
1122
|
+
self,
|
|
1123
|
+
project_id: str,
|
|
1124
|
+
agents: List[str],
|
|
1125
|
+
embedding: Optional[List[float]] = None,
|
|
1126
|
+
top_k: int = 5,
|
|
1127
|
+
) -> List[AntiPattern]:
|
|
1128
|
+
"""Get anti-patterns from multiple agents using optimized ANY query."""
|
|
1129
|
+
if not agents:
|
|
1130
|
+
return []
|
|
1131
|
+
|
|
1132
|
+
with self._get_connection() as conn:
|
|
1133
|
+
if embedding and self._pgvector_available:
|
|
1134
|
+
query = f"""
|
|
1135
|
+
SELECT *, 1 - (embedding <=> %s::vector) as similarity
|
|
1136
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]}
|
|
1137
|
+
WHERE project_id = %s AND agent = ANY(%s)
|
|
1138
|
+
"""
|
|
1139
|
+
params: List[Any] = [
|
|
1140
|
+
self._embedding_to_db(embedding),
|
|
1141
|
+
project_id,
|
|
1142
|
+
agents,
|
|
1143
|
+
]
|
|
1144
|
+
else:
|
|
1145
|
+
query = f"""
|
|
1146
|
+
SELECT *
|
|
1147
|
+
FROM {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]}
|
|
1148
|
+
WHERE project_id = %s AND agent = ANY(%s)
|
|
1149
|
+
"""
|
|
1150
|
+
params = [project_id, agents]
|
|
1151
|
+
|
|
1152
|
+
if embedding and self._pgvector_available:
|
|
1153
|
+
query += " ORDER BY similarity DESC LIMIT %s"
|
|
1154
|
+
else:
|
|
1155
|
+
query += " ORDER BY occurrence_count DESC LIMIT %s"
|
|
1156
|
+
params.append(top_k * len(agents))
|
|
1157
|
+
|
|
1158
|
+
cursor = conn.execute(query, params)
|
|
1159
|
+
rows = cursor.fetchall()
|
|
1160
|
+
|
|
1161
|
+
results = [self._row_to_anti_pattern(row) for row in rows]
|
|
1162
|
+
|
|
1163
|
+
if embedding and not self._pgvector_available and results:
|
|
1164
|
+
results = self._filter_by_similarity(
|
|
1165
|
+
results, embedding, top_k * len(agents), "embedding"
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
return results
|
|
1169
|
+
|
|
1170
|
+
# ==================== UPDATE OPERATIONS ====================
|
|
1171
|
+
|
|
1172
|
+
def update_heuristic(
|
|
1173
|
+
self,
|
|
1174
|
+
heuristic_id: str,
|
|
1175
|
+
updates: Dict[str, Any],
|
|
1176
|
+
) -> bool:
|
|
1177
|
+
"""Update a heuristic's fields."""
|
|
1178
|
+
if not updates:
|
|
1179
|
+
return False
|
|
1180
|
+
|
|
1181
|
+
set_clauses = []
|
|
1182
|
+
params = []
|
|
1183
|
+
for key, value in updates.items():
|
|
1184
|
+
if key == "metadata" and value:
|
|
1185
|
+
value = json.dumps(value)
|
|
1186
|
+
set_clauses.append(f"{key} = %s")
|
|
1187
|
+
params.append(value)
|
|
1188
|
+
|
|
1189
|
+
params.append(heuristic_id)
|
|
1190
|
+
|
|
1191
|
+
with self._get_connection() as conn:
|
|
1192
|
+
cursor = conn.execute(
|
|
1193
|
+
f"UPDATE {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]} SET {', '.join(set_clauses)} WHERE id = %s",
|
|
1194
|
+
params,
|
|
1195
|
+
)
|
|
1196
|
+
conn.commit()
|
|
1197
|
+
return cursor.rowcount > 0
|
|
1198
|
+
|
|
1199
|
+
def increment_heuristic_occurrence(
|
|
1200
|
+
self,
|
|
1201
|
+
heuristic_id: str,
|
|
1202
|
+
success: bool,
|
|
1203
|
+
) -> bool:
|
|
1204
|
+
"""Increment heuristic occurrence count."""
|
|
1205
|
+
with self._get_connection() as conn:
|
|
1206
|
+
if success:
|
|
1207
|
+
cursor = conn.execute(
|
|
1208
|
+
f"""
|
|
1209
|
+
UPDATE {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
|
|
1210
|
+
SET occurrence_count = occurrence_count + 1,
|
|
1211
|
+
success_count = success_count + 1,
|
|
1212
|
+
last_validated = %s
|
|
1213
|
+
WHERE id = %s
|
|
1214
|
+
""",
|
|
1215
|
+
(datetime.now(timezone.utc), heuristic_id),
|
|
1216
|
+
)
|
|
1217
|
+
else:
|
|
1218
|
+
cursor = conn.execute(
|
|
1219
|
+
f"""
|
|
1220
|
+
UPDATE {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
|
|
1221
|
+
SET occurrence_count = occurrence_count + 1,
|
|
1222
|
+
last_validated = %s
|
|
1223
|
+
WHERE id = %s
|
|
1224
|
+
""",
|
|
1225
|
+
(datetime.now(timezone.utc), heuristic_id),
|
|
1226
|
+
)
|
|
1227
|
+
conn.commit()
|
|
1228
|
+
return cursor.rowcount > 0
|
|
1229
|
+
|
|
1230
|
+
def update_heuristic_confidence(
|
|
1231
|
+
self,
|
|
1232
|
+
heuristic_id: str,
|
|
1233
|
+
new_confidence: float,
|
|
1234
|
+
) -> bool:
|
|
1235
|
+
"""Update confidence score for a heuristic."""
|
|
1236
|
+
with self._get_connection() as conn:
|
|
1237
|
+
cursor = conn.execute(
|
|
1238
|
+
f"UPDATE {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]} SET confidence = %s WHERE id = %s",
|
|
1239
|
+
(new_confidence, heuristic_id),
|
|
1240
|
+
)
|
|
1241
|
+
conn.commit()
|
|
1242
|
+
return cursor.rowcount > 0
|
|
1243
|
+
|
|
1244
|
+
def update_knowledge_confidence(
|
|
1245
|
+
self,
|
|
1246
|
+
knowledge_id: str,
|
|
1247
|
+
new_confidence: float,
|
|
1248
|
+
) -> bool:
|
|
1249
|
+
"""Update confidence score for domain knowledge."""
|
|
1250
|
+
with self._get_connection() as conn:
|
|
1251
|
+
cursor = conn.execute(
|
|
1252
|
+
f"UPDATE {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]} SET confidence = %s WHERE id = %s",
|
|
1253
|
+
(new_confidence, knowledge_id),
|
|
1254
|
+
)
|
|
1255
|
+
conn.commit()
|
|
1256
|
+
return cursor.rowcount > 0
|
|
1257
|
+
|
|
1258
|
+
# ==================== DELETE OPERATIONS ====================
|
|
1259
|
+
|
|
1260
|
+
def delete_heuristic(self, heuristic_id: str) -> bool:
|
|
1261
|
+
"""Delete a heuristic by ID."""
|
|
1262
|
+
with self._get_connection() as conn:
|
|
1263
|
+
cursor = conn.execute(
|
|
1264
|
+
f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]} WHERE id = %s",
|
|
1265
|
+
(heuristic_id,),
|
|
1266
|
+
)
|
|
1267
|
+
conn.commit()
|
|
1268
|
+
return cursor.rowcount > 0
|
|
1269
|
+
|
|
1270
|
+
def delete_outcome(self, outcome_id: str) -> bool:
|
|
1271
|
+
"""Delete an outcome by ID."""
|
|
1272
|
+
with self._get_connection() as conn:
|
|
1273
|
+
cursor = conn.execute(
|
|
1274
|
+
f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]} WHERE id = %s",
|
|
1275
|
+
(outcome_id,),
|
|
1276
|
+
)
|
|
1277
|
+
conn.commit()
|
|
1278
|
+
return cursor.rowcount > 0
|
|
1279
|
+
|
|
1280
|
+
def delete_domain_knowledge(self, knowledge_id: str) -> bool:
|
|
1281
|
+
"""Delete domain knowledge by ID."""
|
|
1282
|
+
with self._get_connection() as conn:
|
|
1283
|
+
cursor = conn.execute(
|
|
1284
|
+
f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]} WHERE id = %s",
|
|
1285
|
+
(knowledge_id,),
|
|
1286
|
+
)
|
|
1287
|
+
conn.commit()
|
|
1288
|
+
return cursor.rowcount > 0
|
|
1289
|
+
|
|
1290
|
+
def delete_anti_pattern(self, anti_pattern_id: str) -> bool:
|
|
1291
|
+
"""Delete an anti-pattern by ID."""
|
|
1292
|
+
with self._get_connection() as conn:
|
|
1293
|
+
cursor = conn.execute(
|
|
1294
|
+
f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]} WHERE id = %s",
|
|
1295
|
+
(anti_pattern_id,),
|
|
1296
|
+
)
|
|
1297
|
+
conn.commit()
|
|
1298
|
+
return cursor.rowcount > 0
|
|
1299
|
+
|
|
1300
|
+
def delete_outcomes_older_than(
|
|
1301
|
+
self,
|
|
1302
|
+
project_id: str,
|
|
1303
|
+
older_than: datetime,
|
|
1304
|
+
agent: Optional[str] = None,
|
|
1305
|
+
) -> int:
|
|
1306
|
+
"""Delete old outcomes."""
|
|
1307
|
+
with self._get_connection() as conn:
|
|
1308
|
+
query = f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]} WHERE project_id = %s AND timestamp < %s"
|
|
1309
|
+
params: List[Any] = [project_id, older_than]
|
|
1310
|
+
|
|
1311
|
+
if agent:
|
|
1312
|
+
query += " AND agent = %s"
|
|
1313
|
+
params.append(agent)
|
|
1314
|
+
|
|
1315
|
+
cursor = conn.execute(query, params)
|
|
1316
|
+
conn.commit()
|
|
1317
|
+
deleted = cursor.rowcount
|
|
1318
|
+
|
|
1319
|
+
logger.info(f"Deleted {deleted} old outcomes")
|
|
1320
|
+
return deleted
|
|
1321
|
+
|
|
1322
|
+
def delete_low_confidence_heuristics(
|
|
1323
|
+
self,
|
|
1324
|
+
project_id: str,
|
|
1325
|
+
below_confidence: float,
|
|
1326
|
+
agent: Optional[str] = None,
|
|
1327
|
+
) -> int:
|
|
1328
|
+
"""Delete low-confidence heuristics."""
|
|
1329
|
+
with self._get_connection() as conn:
|
|
1330
|
+
query = f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]} WHERE project_id = %s AND confidence < %s"
|
|
1331
|
+
params: List[Any] = [project_id, below_confidence]
|
|
1332
|
+
|
|
1333
|
+
if agent:
|
|
1334
|
+
query += " AND agent = %s"
|
|
1335
|
+
params.append(agent)
|
|
1336
|
+
|
|
1337
|
+
cursor = conn.execute(query, params)
|
|
1338
|
+
conn.commit()
|
|
1339
|
+
deleted = cursor.rowcount
|
|
1340
|
+
|
|
1341
|
+
logger.info(f"Deleted {deleted} low-confidence heuristics")
|
|
1342
|
+
return deleted
|
|
1343
|
+
|
|
1344
|
+
# ==================== STATS ====================
|
|
1345
|
+
|
|
1346
|
+
def get_stats(
|
|
1347
|
+
self,
|
|
1348
|
+
project_id: str,
|
|
1349
|
+
agent: Optional[str] = None,
|
|
1350
|
+
) -> Dict[str, Any]:
|
|
1351
|
+
"""Get memory statistics."""
|
|
1352
|
+
stats = {
|
|
1353
|
+
"project_id": project_id,
|
|
1354
|
+
"agent": agent,
|
|
1355
|
+
"storage_type": "postgresql",
|
|
1356
|
+
"pgvector_available": self._pgvector_available,
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
with self._get_connection() as conn:
|
|
1360
|
+
# Use canonical memory types for stats
|
|
1361
|
+
for memory_type in MemoryType.ALL:
|
|
1362
|
+
table = self.TABLE_NAMES[memory_type]
|
|
1363
|
+
if memory_type == MemoryType.PREFERENCES:
|
|
1364
|
+
# Preferences don't have project_id
|
|
1365
|
+
cursor = conn.execute(
|
|
1366
|
+
f"SELECT COUNT(*) as count FROM {self.schema}.{table}"
|
|
1367
|
+
)
|
|
1368
|
+
row = cursor.fetchone()
|
|
1369
|
+
stats[f"{memory_type}_count"] = row["count"] if row else 0
|
|
1370
|
+
else:
|
|
1371
|
+
query = f"SELECT COUNT(*) as count FROM {self.schema}.{table} WHERE project_id = %s"
|
|
1372
|
+
params: List[Any] = [project_id]
|
|
1373
|
+
if agent:
|
|
1374
|
+
query += " AND agent = %s"
|
|
1375
|
+
params.append(agent)
|
|
1376
|
+
cursor = conn.execute(query, params)
|
|
1377
|
+
row = cursor.fetchone()
|
|
1378
|
+
stats[f"{memory_type}_count"] = row["count"] if row else 0
|
|
1379
|
+
|
|
1380
|
+
stats["total_count"] = sum(
|
|
1381
|
+
stats.get(k, 0) for k in stats if k.endswith("_count")
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
return stats
|
|
1385
|
+
|
|
1386
|
+
# ==================== HELPERS ====================
|
|
1387
|
+
|
|
1388
|
+
def _parse_datetime(self, value: Any) -> Optional[datetime]:
|
|
1389
|
+
"""Parse datetime from database value."""
|
|
1390
|
+
if value is None:
|
|
1391
|
+
return None
|
|
1392
|
+
if isinstance(value, datetime):
|
|
1393
|
+
return value
|
|
1394
|
+
try:
|
|
1395
|
+
return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
1396
|
+
except (ValueError, AttributeError):
|
|
1397
|
+
return None
|
|
1398
|
+
|
|
1399
|
+
def _row_to_heuristic(self, row: Dict[str, Any]) -> Heuristic:
|
|
1400
|
+
"""Convert database row to Heuristic."""
|
|
1401
|
+
return Heuristic(
|
|
1402
|
+
id=row["id"],
|
|
1403
|
+
agent=row["agent"],
|
|
1404
|
+
project_id=row["project_id"],
|
|
1405
|
+
condition=row["condition"],
|
|
1406
|
+
strategy=row["strategy"],
|
|
1407
|
+
confidence=row["confidence"] or 0.0,
|
|
1408
|
+
occurrence_count=row["occurrence_count"] or 0,
|
|
1409
|
+
success_count=row["success_count"] or 0,
|
|
1410
|
+
last_validated=self._parse_datetime(row["last_validated"])
|
|
1411
|
+
or datetime.now(timezone.utc),
|
|
1412
|
+
created_at=self._parse_datetime(row["created_at"])
|
|
1413
|
+
or datetime.now(timezone.utc),
|
|
1414
|
+
embedding=self._embedding_from_db(row.get("embedding")),
|
|
1415
|
+
metadata=row["metadata"] if row["metadata"] else {},
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
def _row_to_outcome(self, row: Dict[str, Any]) -> Outcome:
|
|
1419
|
+
"""Convert database row to Outcome."""
|
|
1420
|
+
return Outcome(
|
|
1421
|
+
id=row["id"],
|
|
1422
|
+
agent=row["agent"],
|
|
1423
|
+
project_id=row["project_id"],
|
|
1424
|
+
task_type=row["task_type"] or "general",
|
|
1425
|
+
task_description=row["task_description"],
|
|
1426
|
+
success=bool(row["success"]),
|
|
1427
|
+
strategy_used=row["strategy_used"] or "",
|
|
1428
|
+
duration_ms=row["duration_ms"],
|
|
1429
|
+
error_message=row["error_message"],
|
|
1430
|
+
user_feedback=row["user_feedback"],
|
|
1431
|
+
timestamp=self._parse_datetime(row["timestamp"])
|
|
1432
|
+
or datetime.now(timezone.utc),
|
|
1433
|
+
embedding=self._embedding_from_db(row.get("embedding")),
|
|
1434
|
+
metadata=row["metadata"] if row["metadata"] else {},
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
def _row_to_preference(self, row: Dict[str, Any]) -> UserPreference:
|
|
1438
|
+
"""Convert database row to UserPreference."""
|
|
1439
|
+
return UserPreference(
|
|
1440
|
+
id=row["id"],
|
|
1441
|
+
user_id=row["user_id"],
|
|
1442
|
+
category=row["category"] or "general",
|
|
1443
|
+
preference=row["preference"],
|
|
1444
|
+
source=row["source"] or "unknown",
|
|
1445
|
+
confidence=row["confidence"] or 1.0,
|
|
1446
|
+
timestamp=self._parse_datetime(row["timestamp"])
|
|
1447
|
+
or datetime.now(timezone.utc),
|
|
1448
|
+
metadata=row["metadata"] if row["metadata"] else {},
|
|
1449
|
+
)
|
|
1450
|
+
|
|
1451
|
+
def _row_to_domain_knowledge(self, row: Dict[str, Any]) -> DomainKnowledge:
|
|
1452
|
+
"""Convert database row to DomainKnowledge."""
|
|
1453
|
+
return DomainKnowledge(
|
|
1454
|
+
id=row["id"],
|
|
1455
|
+
agent=row["agent"],
|
|
1456
|
+
project_id=row["project_id"],
|
|
1457
|
+
domain=row["domain"] or "general",
|
|
1458
|
+
fact=row["fact"],
|
|
1459
|
+
source=row["source"] or "unknown",
|
|
1460
|
+
confidence=row["confidence"] or 1.0,
|
|
1461
|
+
last_verified=self._parse_datetime(row["last_verified"])
|
|
1462
|
+
or datetime.now(timezone.utc),
|
|
1463
|
+
embedding=self._embedding_from_db(row.get("embedding")),
|
|
1464
|
+
metadata=row["metadata"] if row["metadata"] else {},
|
|
1465
|
+
)
|
|
1466
|
+
|
|
1467
|
+
def _row_to_anti_pattern(self, row: Dict[str, Any]) -> AntiPattern:
|
|
1468
|
+
"""Convert database row to AntiPattern."""
|
|
1469
|
+
return AntiPattern(
|
|
1470
|
+
id=row["id"],
|
|
1471
|
+
agent=row["agent"],
|
|
1472
|
+
project_id=row["project_id"],
|
|
1473
|
+
pattern=row["pattern"],
|
|
1474
|
+
why_bad=row["why_bad"] or "",
|
|
1475
|
+
better_alternative=row["better_alternative"] or "",
|
|
1476
|
+
occurrence_count=row["occurrence_count"] or 1,
|
|
1477
|
+
last_seen=self._parse_datetime(row["last_seen"])
|
|
1478
|
+
or datetime.now(timezone.utc),
|
|
1479
|
+
created_at=self._parse_datetime(row["created_at"])
|
|
1480
|
+
or datetime.now(timezone.utc),
|
|
1481
|
+
embedding=self._embedding_from_db(row.get("embedding")),
|
|
1482
|
+
metadata=row["metadata"] if row["metadata"] else {},
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
def close(self):
|
|
1486
|
+
"""Close connection pool."""
|
|
1487
|
+
if self._pool:
|
|
1488
|
+
self._pool.close()
|
|
1489
|
+
|
|
1490
|
+
# ==================== MIGRATION SUPPORT ====================
|
|
1491
|
+
|
|
1492
|
+
def _get_version_store(self):
|
|
1493
|
+
"""Get or create the version store."""
|
|
1494
|
+
if self._version_store is None:
|
|
1495
|
+
from alma.storage.migrations.version_stores import PostgreSQLVersionStore
|
|
1496
|
+
|
|
1497
|
+
self._version_store = PostgreSQLVersionStore(self._pool, self.schema)
|
|
1498
|
+
return self._version_store
|
|
1499
|
+
|
|
1500
|
+
def _get_migration_runner(self):
|
|
1501
|
+
"""Get or create the migration runner."""
|
|
1502
|
+
if self._migration_runner is None:
|
|
1503
|
+
from alma.storage.migrations.runner import MigrationRunner
|
|
1504
|
+
from alma.storage.migrations.versions import v1_0_0 # noqa: F401
|
|
1505
|
+
|
|
1506
|
+
self._migration_runner = MigrationRunner(
|
|
1507
|
+
version_store=self._get_version_store(),
|
|
1508
|
+
backend="postgresql",
|
|
1509
|
+
)
|
|
1510
|
+
return self._migration_runner
|
|
1511
|
+
|
|
1512
|
+
def _ensure_migrated(self) -> None:
|
|
1513
|
+
"""Ensure database is migrated to latest version."""
|
|
1514
|
+
runner = self._get_migration_runner()
|
|
1515
|
+
if runner.needs_migration():
|
|
1516
|
+
with self._get_connection() as conn:
|
|
1517
|
+
applied = runner.migrate(conn)
|
|
1518
|
+
if applied:
|
|
1519
|
+
logger.info(f"Applied {len(applied)} migrations: {applied}")
|
|
1520
|
+
|
|
1521
|
+
def get_schema_version(self) -> Optional[str]:
|
|
1522
|
+
"""Get the current schema version."""
|
|
1523
|
+
return self._get_version_store().get_current_version()
|
|
1524
|
+
|
|
1525
|
+
def get_migration_status(self) -> Dict[str, Any]:
|
|
1526
|
+
"""Get migration status information."""
|
|
1527
|
+
runner = self._get_migration_runner()
|
|
1528
|
+
status = runner.get_status()
|
|
1529
|
+
status["migration_supported"] = True
|
|
1530
|
+
return status
|
|
1531
|
+
|
|
1532
|
+
def migrate(
|
|
1533
|
+
self,
|
|
1534
|
+
target_version: Optional[str] = None,
|
|
1535
|
+
dry_run: bool = False,
|
|
1536
|
+
) -> List[str]:
|
|
1537
|
+
"""
|
|
1538
|
+
Apply pending schema migrations.
|
|
1539
|
+
|
|
1540
|
+
Args:
|
|
1541
|
+
target_version: Optional target version (applies all if not specified)
|
|
1542
|
+
dry_run: If True, show what would be done without making changes
|
|
1543
|
+
|
|
1544
|
+
Returns:
|
|
1545
|
+
List of applied migration versions
|
|
1546
|
+
"""
|
|
1547
|
+
runner = self._get_migration_runner()
|
|
1548
|
+
with self._get_connection() as conn:
|
|
1549
|
+
return runner.migrate(conn, target_version=target_version, dry_run=dry_run)
|
|
1550
|
+
|
|
1551
|
+
def rollback(
|
|
1552
|
+
self,
|
|
1553
|
+
target_version: str,
|
|
1554
|
+
dry_run: bool = False,
|
|
1555
|
+
) -> List[str]:
|
|
1556
|
+
"""
|
|
1557
|
+
Roll back schema to a previous version.
|
|
1558
|
+
|
|
1559
|
+
Args:
|
|
1560
|
+
target_version: Version to roll back to
|
|
1561
|
+
dry_run: If True, show what would be done without making changes
|
|
1562
|
+
|
|
1563
|
+
Returns:
|
|
1564
|
+
List of rolled back migration versions
|
|
1565
|
+
"""
|
|
1566
|
+
runner = self._get_migration_runner()
|
|
1567
|
+
with self._get_connection() as conn:
|
|
1568
|
+
return runner.rollback(conn, target_version=target_version, dry_run=dry_run)
|
|
1569
|
+
|
|
1570
|
+
# ==================== CHECKPOINT OPERATIONS (v0.6.0+) ====================
|
|
1571
|
+
|
|
1572
|
+
def save_checkpoint(self, checkpoint: "Checkpoint") -> str:
|
|
1573
|
+
"""Save a workflow checkpoint."""
|
|
1574
|
+
with self._get_connection() as conn:
|
|
1575
|
+
conn.execute(
|
|
1576
|
+
f"""
|
|
1577
|
+
INSERT INTO {self.schema}.alma_checkpoints
|
|
1578
|
+
(id, run_id, node_id, state_json, state_hash, sequence_number,
|
|
1579
|
+
branch_id, parent_checkpoint_id, metadata, created_at)
|
|
1580
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
1581
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
1582
|
+
state_json = EXCLUDED.state_json,
|
|
1583
|
+
state_hash = EXCLUDED.state_hash,
|
|
1584
|
+
sequence_number = EXCLUDED.sequence_number,
|
|
1585
|
+
metadata = EXCLUDED.metadata
|
|
1586
|
+
""",
|
|
1587
|
+
(
|
|
1588
|
+
checkpoint.id,
|
|
1589
|
+
checkpoint.run_id,
|
|
1590
|
+
checkpoint.node_id,
|
|
1591
|
+
json.dumps(checkpoint.state),
|
|
1592
|
+
checkpoint.state_hash,
|
|
1593
|
+
checkpoint.sequence_number,
|
|
1594
|
+
checkpoint.branch_id,
|
|
1595
|
+
checkpoint.parent_checkpoint_id,
|
|
1596
|
+
json.dumps(checkpoint.metadata) if checkpoint.metadata else None,
|
|
1597
|
+
checkpoint.created_at,
|
|
1598
|
+
),
|
|
1599
|
+
)
|
|
1600
|
+
conn.commit()
|
|
1601
|
+
|
|
1602
|
+
logger.debug(f"Saved checkpoint: {checkpoint.id}")
|
|
1603
|
+
return checkpoint.id
|
|
1604
|
+
|
|
1605
|
+
def get_checkpoint(self, checkpoint_id: str) -> Optional["Checkpoint"]:
|
|
1606
|
+
"""Get a checkpoint by ID."""
|
|
1607
|
+
with self._get_connection() as conn:
|
|
1608
|
+
cursor = conn.execute(
|
|
1609
|
+
f"SELECT * FROM {self.schema}.alma_checkpoints WHERE id = %s",
|
|
1610
|
+
(checkpoint_id,),
|
|
1611
|
+
)
|
|
1612
|
+
row = cursor.fetchone()
|
|
1613
|
+
|
|
1614
|
+
if row is None:
|
|
1615
|
+
return None
|
|
1616
|
+
return self._row_to_checkpoint(row)
|
|
1617
|
+
|
|
1618
|
+
def get_latest_checkpoint(
|
|
1619
|
+
self,
|
|
1620
|
+
run_id: str,
|
|
1621
|
+
branch_id: Optional[str] = None,
|
|
1622
|
+
) -> Optional["Checkpoint"]:
|
|
1623
|
+
"""Get the most recent checkpoint for a workflow run."""
|
|
1624
|
+
with self._get_connection() as conn:
|
|
1625
|
+
if branch_id is not None:
|
|
1626
|
+
cursor = conn.execute(
|
|
1627
|
+
f"""
|
|
1628
|
+
SELECT * FROM {self.schema}.alma_checkpoints
|
|
1629
|
+
WHERE run_id = %s AND branch_id = %s
|
|
1630
|
+
ORDER BY sequence_number DESC LIMIT 1
|
|
1631
|
+
""",
|
|
1632
|
+
(run_id, branch_id),
|
|
1633
|
+
)
|
|
1634
|
+
else:
|
|
1635
|
+
cursor = conn.execute(
|
|
1636
|
+
f"""
|
|
1637
|
+
SELECT * FROM {self.schema}.alma_checkpoints
|
|
1638
|
+
WHERE run_id = %s
|
|
1639
|
+
ORDER BY sequence_number DESC LIMIT 1
|
|
1640
|
+
""",
|
|
1641
|
+
(run_id,),
|
|
1642
|
+
)
|
|
1643
|
+
row = cursor.fetchone()
|
|
1644
|
+
|
|
1645
|
+
if row is None:
|
|
1646
|
+
return None
|
|
1647
|
+
return self._row_to_checkpoint(row)
|
|
1648
|
+
|
|
1649
|
+
def get_checkpoints_for_run(
|
|
1650
|
+
self,
|
|
1651
|
+
run_id: str,
|
|
1652
|
+
branch_id: Optional[str] = None,
|
|
1653
|
+
limit: int = 100,
|
|
1654
|
+
) -> List["Checkpoint"]:
|
|
1655
|
+
"""Get all checkpoints for a workflow run."""
|
|
1656
|
+
with self._get_connection() as conn:
|
|
1657
|
+
if branch_id is not None:
|
|
1658
|
+
cursor = conn.execute(
|
|
1659
|
+
f"""
|
|
1660
|
+
SELECT * FROM {self.schema}.alma_checkpoints
|
|
1661
|
+
WHERE run_id = %s AND branch_id = %s
|
|
1662
|
+
ORDER BY sequence_number ASC LIMIT %s
|
|
1663
|
+
""",
|
|
1664
|
+
(run_id, branch_id, limit),
|
|
1665
|
+
)
|
|
1666
|
+
else:
|
|
1667
|
+
cursor = conn.execute(
|
|
1668
|
+
f"""
|
|
1669
|
+
SELECT * FROM {self.schema}.alma_checkpoints
|
|
1670
|
+
WHERE run_id = %s
|
|
1671
|
+
ORDER BY sequence_number ASC LIMIT %s
|
|
1672
|
+
""",
|
|
1673
|
+
(run_id, limit),
|
|
1674
|
+
)
|
|
1675
|
+
rows = cursor.fetchall()
|
|
1676
|
+
|
|
1677
|
+
return [self._row_to_checkpoint(row) for row in rows]
|
|
1678
|
+
|
|
1679
|
+
def cleanup_checkpoints(
|
|
1680
|
+
self,
|
|
1681
|
+
run_id: str,
|
|
1682
|
+
keep_latest: int = 1,
|
|
1683
|
+
) -> int:
|
|
1684
|
+
"""Clean up old checkpoints for a completed run."""
|
|
1685
|
+
with self._get_connection() as conn:
|
|
1686
|
+
# Delete all but the latest N checkpoints
|
|
1687
|
+
cursor = conn.execute(
|
|
1688
|
+
f"""
|
|
1689
|
+
DELETE FROM {self.schema}.alma_checkpoints
|
|
1690
|
+
WHERE run_id = %s AND id NOT IN (
|
|
1691
|
+
SELECT id FROM {self.schema}.alma_checkpoints
|
|
1692
|
+
WHERE run_id = %s
|
|
1693
|
+
ORDER BY sequence_number DESC
|
|
1694
|
+
LIMIT %s
|
|
1695
|
+
)
|
|
1696
|
+
""",
|
|
1697
|
+
(run_id, run_id, keep_latest),
|
|
1698
|
+
)
|
|
1699
|
+
conn.commit()
|
|
1700
|
+
deleted = cursor.rowcount
|
|
1701
|
+
|
|
1702
|
+
logger.info(f"Cleaned up {deleted} checkpoints for run {run_id}")
|
|
1703
|
+
return deleted
|
|
1704
|
+
|
|
1705
|
+
def _row_to_checkpoint(self, row: Dict[str, Any]) -> "Checkpoint":
|
|
1706
|
+
"""Convert database row to Checkpoint."""
|
|
1707
|
+
from alma.workflow import Checkpoint
|
|
1708
|
+
|
|
1709
|
+
return Checkpoint(
|
|
1710
|
+
id=row["id"],
|
|
1711
|
+
run_id=row["run_id"],
|
|
1712
|
+
node_id=row["node_id"],
|
|
1713
|
+
state=json.loads(row["state_json"]) if row["state_json"] else {},
|
|
1714
|
+
sequence_number=row["sequence_number"] or 0,
|
|
1715
|
+
branch_id=row["branch_id"],
|
|
1716
|
+
parent_checkpoint_id=row["parent_checkpoint_id"],
|
|
1717
|
+
state_hash=row["state_hash"] or "",
|
|
1718
|
+
metadata=row["metadata"] if row["metadata"] else {},
|
|
1719
|
+
created_at=self._parse_datetime(row["created_at"])
|
|
1720
|
+
or datetime.now(timezone.utc),
|
|
1721
|
+
)
|
|
1722
|
+
|
|
1723
|
+
# ==================== WORKFLOW OUTCOME OPERATIONS (v0.6.0+) ====================
|
|
1724
|
+
|
|
1725
|
+
def save_workflow_outcome(self, outcome: "WorkflowOutcome") -> str:
|
|
1726
|
+
"""Save a workflow outcome."""
|
|
1727
|
+
with self._get_connection() as conn:
|
|
1728
|
+
conn.execute(
|
|
1729
|
+
f"""
|
|
1730
|
+
INSERT INTO {self.schema}.alma_workflow_outcomes
|
|
1731
|
+
(id, tenant_id, workflow_id, run_id, agent, project_id, result,
|
|
1732
|
+
summary, strategies_used, successful_patterns, failed_patterns,
|
|
1733
|
+
extracted_heuristics, extracted_anti_patterns, duration_seconds,
|
|
1734
|
+
node_count, error_message, metadata, embedding, created_at)
|
|
1735
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
1736
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
1737
|
+
result = EXCLUDED.result,
|
|
1738
|
+
summary = EXCLUDED.summary,
|
|
1739
|
+
strategies_used = EXCLUDED.strategies_used,
|
|
1740
|
+
successful_patterns = EXCLUDED.successful_patterns,
|
|
1741
|
+
failed_patterns = EXCLUDED.failed_patterns,
|
|
1742
|
+
extracted_heuristics = EXCLUDED.extracted_heuristics,
|
|
1743
|
+
extracted_anti_patterns = EXCLUDED.extracted_anti_patterns,
|
|
1744
|
+
duration_seconds = EXCLUDED.duration_seconds,
|
|
1745
|
+
node_count = EXCLUDED.node_count,
|
|
1746
|
+
error_message = EXCLUDED.error_message,
|
|
1747
|
+
metadata = EXCLUDED.metadata,
|
|
1748
|
+
embedding = EXCLUDED.embedding
|
|
1749
|
+
""",
|
|
1750
|
+
(
|
|
1751
|
+
outcome.id,
|
|
1752
|
+
outcome.tenant_id,
|
|
1753
|
+
outcome.workflow_id,
|
|
1754
|
+
outcome.run_id,
|
|
1755
|
+
outcome.agent,
|
|
1756
|
+
outcome.project_id,
|
|
1757
|
+
outcome.result.value,
|
|
1758
|
+
outcome.summary,
|
|
1759
|
+
outcome.strategies_used,
|
|
1760
|
+
outcome.successful_patterns,
|
|
1761
|
+
outcome.failed_patterns,
|
|
1762
|
+
outcome.extracted_heuristics,
|
|
1763
|
+
outcome.extracted_anti_patterns,
|
|
1764
|
+
outcome.duration_seconds,
|
|
1765
|
+
outcome.node_count,
|
|
1766
|
+
outcome.error_message,
|
|
1767
|
+
outcome.metadata,
|
|
1768
|
+
self._embedding_to_db(outcome.embedding),
|
|
1769
|
+
outcome.created_at,
|
|
1770
|
+
),
|
|
1771
|
+
)
|
|
1772
|
+
conn.commit()
|
|
1773
|
+
|
|
1774
|
+
logger.debug(f"Saved workflow outcome: {outcome.id}")
|
|
1775
|
+
return outcome.id
|
|
1776
|
+
|
|
1777
|
+
def get_workflow_outcome(self, outcome_id: str) -> Optional["WorkflowOutcome"]:
|
|
1778
|
+
"""Get a workflow outcome by ID."""
|
|
1779
|
+
with self._get_connection() as conn:
|
|
1780
|
+
cursor = conn.execute(
|
|
1781
|
+
f"SELECT * FROM {self.schema}.alma_workflow_outcomes WHERE id = %s",
|
|
1782
|
+
(outcome_id,),
|
|
1783
|
+
)
|
|
1784
|
+
row = cursor.fetchone()
|
|
1785
|
+
|
|
1786
|
+
if row is None:
|
|
1787
|
+
return None
|
|
1788
|
+
return self._row_to_workflow_outcome(row)
|
|
1789
|
+
|
|
1790
|
+
def get_workflow_outcomes(
|
|
1791
|
+
self,
|
|
1792
|
+
project_id: str,
|
|
1793
|
+
agent: Optional[str] = None,
|
|
1794
|
+
workflow_id: Optional[str] = None,
|
|
1795
|
+
embedding: Optional[List[float]] = None,
|
|
1796
|
+
top_k: int = 10,
|
|
1797
|
+
scope_filter: Optional[Dict[str, Any]] = None,
|
|
1798
|
+
) -> List["WorkflowOutcome"]:
|
|
1799
|
+
"""Get workflow outcomes with optional filtering."""
|
|
1800
|
+
with self._get_connection() as conn:
|
|
1801
|
+
if embedding and self._pgvector_available:
|
|
1802
|
+
query = f"""
|
|
1803
|
+
SELECT *, 1 - (embedding <=> %s::vector) as similarity
|
|
1804
|
+
FROM {self.schema}.alma_workflow_outcomes
|
|
1805
|
+
WHERE project_id = %s
|
|
1806
|
+
"""
|
|
1807
|
+
params: List[Any] = [self._embedding_to_db(embedding), project_id]
|
|
1808
|
+
else:
|
|
1809
|
+
query = f"""
|
|
1810
|
+
SELECT *
|
|
1811
|
+
FROM {self.schema}.alma_workflow_outcomes
|
|
1812
|
+
WHERE project_id = %s
|
|
1813
|
+
"""
|
|
1814
|
+
params = [project_id]
|
|
1815
|
+
|
|
1816
|
+
if agent:
|
|
1817
|
+
query += " AND agent = %s"
|
|
1818
|
+
params.append(agent)
|
|
1819
|
+
|
|
1820
|
+
if workflow_id:
|
|
1821
|
+
query += " AND workflow_id = %s"
|
|
1822
|
+
params.append(workflow_id)
|
|
1823
|
+
|
|
1824
|
+
# Apply scope filter
|
|
1825
|
+
if scope_filter:
|
|
1826
|
+
if scope_filter.get("tenant_id"):
|
|
1827
|
+
query += " AND tenant_id = %s"
|
|
1828
|
+
params.append(scope_filter["tenant_id"])
|
|
1829
|
+
if scope_filter.get("workflow_id"):
|
|
1830
|
+
query += " AND workflow_id = %s"
|
|
1831
|
+
params.append(scope_filter["workflow_id"])
|
|
1832
|
+
if scope_filter.get("run_id"):
|
|
1833
|
+
query += " AND run_id = %s"
|
|
1834
|
+
params.append(scope_filter["run_id"])
|
|
1835
|
+
|
|
1836
|
+
if embedding and self._pgvector_available:
|
|
1837
|
+
query += " ORDER BY similarity DESC LIMIT %s"
|
|
1838
|
+
else:
|
|
1839
|
+
query += " ORDER BY created_at DESC LIMIT %s"
|
|
1840
|
+
params.append(top_k)
|
|
1841
|
+
|
|
1842
|
+
cursor = conn.execute(query, params)
|
|
1843
|
+
rows = cursor.fetchall()
|
|
1844
|
+
|
|
1845
|
+
return [self._row_to_workflow_outcome(row) for row in rows]
|
|
1846
|
+
|
|
1847
|
+
def _row_to_workflow_outcome(self, row: Dict[str, Any]) -> "WorkflowOutcome":
|
|
1848
|
+
"""Convert database row to WorkflowOutcome."""
|
|
1849
|
+
from alma.workflow import WorkflowOutcome, WorkflowResult
|
|
1850
|
+
|
|
1851
|
+
return WorkflowOutcome(
|
|
1852
|
+
id=row["id"],
|
|
1853
|
+
tenant_id=row["tenant_id"],
|
|
1854
|
+
workflow_id=row["workflow_id"],
|
|
1855
|
+
run_id=row["run_id"],
|
|
1856
|
+
agent=row["agent"],
|
|
1857
|
+
project_id=row["project_id"],
|
|
1858
|
+
result=WorkflowResult(row["result"]),
|
|
1859
|
+
summary=row["summary"] or "",
|
|
1860
|
+
strategies_used=row["strategies_used"] or [],
|
|
1861
|
+
successful_patterns=row["successful_patterns"] or [],
|
|
1862
|
+
failed_patterns=row["failed_patterns"] or [],
|
|
1863
|
+
extracted_heuristics=row["extracted_heuristics"] or [],
|
|
1864
|
+
extracted_anti_patterns=row["extracted_anti_patterns"] or [],
|
|
1865
|
+
duration_seconds=row["duration_seconds"],
|
|
1866
|
+
node_count=row["node_count"],
|
|
1867
|
+
error_message=row["error_message"],
|
|
1868
|
+
embedding=self._embedding_from_db(row.get("embedding")),
|
|
1869
|
+
metadata=row["metadata"] if row["metadata"] else {},
|
|
1870
|
+
created_at=self._parse_datetime(row["created_at"])
|
|
1871
|
+
or datetime.now(timezone.utc),
|
|
1872
|
+
)
|
|
1873
|
+
|
|
1874
|
+
# ==================== ARTIFACT LINK OPERATIONS (v0.6.0+) ====================
|
|
1875
|
+
|
|
1876
|
+
def save_artifact_link(self, artifact_ref: "ArtifactRef") -> str:
|
|
1877
|
+
"""Save an artifact reference linked to a memory."""
|
|
1878
|
+
with self._get_connection() as conn:
|
|
1879
|
+
conn.execute(
|
|
1880
|
+
f"""
|
|
1881
|
+
INSERT INTO {self.schema}.alma_artifact_links
|
|
1882
|
+
(id, memory_id, artifact_type, storage_url, filename,
|
|
1883
|
+
mime_type, size_bytes, checksum, metadata, created_at)
|
|
1884
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
1885
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
1886
|
+
storage_url = EXCLUDED.storage_url,
|
|
1887
|
+
filename = EXCLUDED.filename,
|
|
1888
|
+
mime_type = EXCLUDED.mime_type,
|
|
1889
|
+
size_bytes = EXCLUDED.size_bytes,
|
|
1890
|
+
checksum = EXCLUDED.checksum,
|
|
1891
|
+
metadata = EXCLUDED.metadata
|
|
1892
|
+
""",
|
|
1893
|
+
(
|
|
1894
|
+
artifact_ref.id,
|
|
1895
|
+
artifact_ref.memory_id,
|
|
1896
|
+
artifact_ref.artifact_type.value,
|
|
1897
|
+
artifact_ref.storage_url,
|
|
1898
|
+
artifact_ref.filename,
|
|
1899
|
+
artifact_ref.mime_type,
|
|
1900
|
+
artifact_ref.size_bytes,
|
|
1901
|
+
artifact_ref.checksum,
|
|
1902
|
+
artifact_ref.metadata,
|
|
1903
|
+
artifact_ref.created_at,
|
|
1904
|
+
),
|
|
1905
|
+
)
|
|
1906
|
+
conn.commit()
|
|
1907
|
+
|
|
1908
|
+
logger.debug(f"Saved artifact link: {artifact_ref.id}")
|
|
1909
|
+
return artifact_ref.id
|
|
1910
|
+
|
|
1911
|
+
def get_artifact_links(self, memory_id: str) -> List["ArtifactRef"]:
|
|
1912
|
+
"""Get all artifact references linked to a memory."""
|
|
1913
|
+
with self._get_connection() as conn:
|
|
1914
|
+
cursor = conn.execute(
|
|
1915
|
+
f"SELECT * FROM {self.schema}.alma_artifact_links WHERE memory_id = %s",
|
|
1916
|
+
(memory_id,),
|
|
1917
|
+
)
|
|
1918
|
+
rows = cursor.fetchall()
|
|
1919
|
+
|
|
1920
|
+
return [self._row_to_artifact_ref(row) for row in rows]
|
|
1921
|
+
|
|
1922
|
+
def delete_artifact_link(self, artifact_id: str) -> bool:
|
|
1923
|
+
"""Delete an artifact reference."""
|
|
1924
|
+
with self._get_connection() as conn:
|
|
1925
|
+
cursor = conn.execute(
|
|
1926
|
+
f"DELETE FROM {self.schema}.alma_artifact_links WHERE id = %s",
|
|
1927
|
+
(artifact_id,),
|
|
1928
|
+
)
|
|
1929
|
+
conn.commit()
|
|
1930
|
+
return cursor.rowcount > 0
|
|
1931
|
+
|
|
1932
|
+
def _row_to_artifact_ref(self, row: Dict[str, Any]) -> "ArtifactRef":
|
|
1933
|
+
"""Convert database row to ArtifactRef."""
|
|
1934
|
+
from alma.workflow import ArtifactRef, ArtifactType
|
|
1935
|
+
|
|
1936
|
+
return ArtifactRef(
|
|
1937
|
+
id=row["id"],
|
|
1938
|
+
memory_id=row["memory_id"],
|
|
1939
|
+
artifact_type=ArtifactType(row["artifact_type"]),
|
|
1940
|
+
storage_url=row["storage_url"],
|
|
1941
|
+
filename=row["filename"],
|
|
1942
|
+
mime_type=row["mime_type"],
|
|
1943
|
+
size_bytes=row["size_bytes"],
|
|
1944
|
+
checksum=row["checksum"],
|
|
1945
|
+
metadata=row["metadata"] if row["metadata"] else {},
|
|
1946
|
+
created_at=self._parse_datetime(row["created_at"])
|
|
1947
|
+
or datetime.now(timezone.utc),
|
|
1948
|
+
)
|