-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmemory_manager.py
More file actions
1565 lines (1382 loc) · 55.3 KB
/
Copy pathmemory_manager.py
File metadata and controls
1565 lines (1382 loc) · 55.3 KB
1
2
3
4
5
6
7
8
9
10
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
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
223
224
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
286
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
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
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
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
873
874
875
876
877
878
879
880
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
"""
Memory Management for Agent System
===================================
Handles session memory storage using dual-layer approach:
- PRIMARY: Graphiti (when enabled) - semantic search, cross-session context
- FALLBACK: File-based memory - zero dependencies, always available
"""
import json
import logging
from pathlib import Path
from typing import TYPE_CHECKING
from core.sentry import capture_exception
if TYPE_CHECKING:
from agents.session_context import SessionContext
from debug import (
debug,
debug_detailed,
debug_error,
debug_section,
debug_success,
debug_warning,
is_debug_enabled,
)
from integrations.graphiti.config import get_graphiti_status, is_graphiti_enabled
# Import from parent memory package
# Now safe since this module is named memory_manager (not memory)
from memory import save_session_insights as save_file_based_memory
from memory.graphiti_helpers import get_graphiti_memory
from memory.patterns import (
save_detected_patterns_from_errors,
save_detected_patterns_from_naming,
save_detected_patterns_from_organization,
)
logger = logging.getLogger(__name__)
async def get_session_context(
spec_dir: Path,
project_dir: Path,
) -> "SessionContext | None":
"""
Get SessionContext instance for managing conversation history in Graphiti.
This provides access to session context storage and retrieval for:
- Persisting conversation history across restarts
- Tracking code references
- Optimizing context window
Args:
spec_dir: Spec directory
project_dir: Project root directory
Returns:
SessionContext instance or None if initialization fails
"""
try:
from agents.session_context import SessionContext
# Create SessionContext instance
session_context = SessionContext(
spec_dir=spec_dir,
project_dir=project_dir,
)
# Initialize Graphiti connection
if await session_context.initialize():
debug_success(
"memory",
"SessionContext initialized",
spec_dir=str(spec_dir),
)
return session_context
else:
debug_warning(
"memory",
"SessionContext initialization failed - Graphiti not available",
)
return None
except Exception as e:
debug_error(
"memory",
f"Failed to create SessionContext: {e}",
)
logger.warning(f"Failed to create SessionContext: {e}")
capture_exception(
e,
operation="get_session_context",
spec_dir=str(spec_dir),
)
return None
def debug_memory_system_status() -> None:
"""
Print memory system status for debugging.
Called at startup when DEBUG=true to show memory configuration.
"""
if not is_debug_enabled():
return
debug_section("memory", "Memory System Status")
# Get Graphiti status
graphiti_status = get_graphiti_status()
debug(
"memory",
"Memory system configuration",
primary_system="Graphiti"
if graphiti_status.get("available")
else "File-based (fallback)",
graphiti_enabled=graphiti_status.get("enabled"),
graphiti_available=graphiti_status.get("available"),
)
if graphiti_status.get("enabled"):
debug_detailed(
"memory",
"Graphiti configuration",
host=graphiti_status.get("host"),
port=graphiti_status.get("port"),
database=graphiti_status.get("database"),
llm_provider=graphiti_status.get("llm_provider"),
embedder_provider=graphiti_status.get("embedder_provider"),
)
if not graphiti_status.get("available"):
debug_warning(
"memory",
"Graphiti not available",
reason=graphiti_status.get("reason"),
errors=graphiti_status.get("errors"),
)
debug("memory", "Will use file-based memory as fallback")
else:
debug_success("memory", "Graphiti ready as PRIMARY memory system")
else:
debug(
"memory",
"Graphiti disabled, using file-based memory only",
note="Set GRAPHITI_ENABLED=true to enable Graphiti",
)
async def get_pattern_suggestions(
spec_dir: Path,
project_dir: Path,
query: str,
categories: list[str] | None = None,
num_results: int = 5,
min_score: float = 0.5,
) -> str | None:
"""
Retrieve pattern suggestions from Graphiti for the current task.
This searches the knowledge graph for relevant code patterns that match
the task query, returning categorized patterns with confidence scores.
Args:
spec_dir: Spec directory
project_dir: Project root directory
query: Task description or search query
categories: Optional list of pattern categories to filter by
num_results: Maximum number of patterns to return (default: 5)
min_score: Minimum relevance score 0.0-1.0 (default: 0.5)
Returns:
Formatted pattern suggestions string or None if unavailable
"""
if is_debug_enabled():
debug(
"memory",
"Retrieving pattern suggestions",
query=query[:100],
categories=categories,
num_results=num_results,
)
if not is_graphiti_enabled():
if is_debug_enabled():
debug("memory", "Graphiti not enabled, skipping pattern suggestions")
return None
memory = None
try:
# Get GraphitiMemory instance
memory = await get_graphiti_memory(spec_dir, project_dir)
if memory is None:
if is_debug_enabled():
debug_warning(
"memory", "GraphitiMemory not available for pattern suggestions"
)
return None
# Import pattern suggester
from integrations.graphiti.pattern_suggester import suggest_patterns
# Get group_id and spec_context_id from memory
group_id = memory.group_id
spec_context_id = memory.spec_context_id
if is_debug_enabled():
debug_detailed(
"memory",
"Searching for pattern suggestions",
query=query[:200],
group_id=group_id,
categories=categories,
)
# Get pattern suggestions
patterns = await suggest_patterns(
client=memory.client,
group_id=group_id,
spec_context_id=spec_context_id,
query=query,
categories=categories,
num_results=num_results,
min_score=min_score,
include_project_context=True,
project_dir=project_dir,
)
if is_debug_enabled():
debug(
"memory",
"Pattern suggestion retrieval complete",
patterns_found=len(patterns) if patterns else 0,
)
if not patterns:
if is_debug_enabled():
debug("memory", "No pattern suggestions found")
return None
# Format the patterns
sections = ["## Pattern Suggestions\n"]
sections.append("_Relevant code patterns from previous implementations:_\n")
# Group patterns by category
by_category: dict[str, list[dict]] = {}
for pattern in patterns:
category = pattern.get("category", "uncategorized")
if category not in by_category:
by_category[category] = []
by_category[category].append(pattern)
# Format each category
for category, category_patterns in by_category.items():
sections.append(f"### {category.replace('-', ' ').title()}\n")
for p in category_patterns:
pattern_text = p.get("pattern", "")
reasoning = p.get("reasoning", "")
confidence = p.get("confidence", 0.0)
score = p.get("score", 0.0)
spec_id = p.get("spec_id", "")
sections.append(f"- **Pattern**: {pattern_text}\n")
if reasoning:
sections.append(f" _Reasoning_: {reasoning}\n")
sections.append(
f" _Confidence_: {confidence:.2f} | _Relevance_: {score:.2f}"
)
if spec_id:
sections.append(f" | _From_: {spec_id}")
sections.append("\n")
formatted = "".join(sections)
if is_debug_enabled():
debug_success(
"memory",
"Pattern suggestions formatted",
categories=len(by_category),
total_patterns=len(patterns),
)
return formatted
except Exception as e:
if is_debug_enabled():
debug_error("memory", "Failed to get pattern suggestions", error=str(e))
logger.warning(f"Failed to get pattern suggestions: {e}")
capture_exception(
e,
query_summary=query[:100] if query else "",
spec_dir=str(spec_dir),
project_dir=str(project_dir),
operation="get_pattern_suggestions",
)
return None
finally:
# Close memory connection if we opened it
if memory is not None:
try:
await memory.close()
except Exception:
pass
async def get_failure_patterns(
spec_dir: Path,
project_dir: Path,
query: str,
failure_types: list[str] | None = None,
num_results: int = 5,
min_score: float = 0.5,
) -> str | None:
"""
Retrieve failure patterns from Graphiti for the current task.
This searches the knowledge graph for relevant root cause analyses
from past failures, returning categorized patterns with recommendations.
Args:
spec_dir: Spec directory
project_dir: Project root directory
query: Task description or error message to search for
failure_types: Optional list of failure types to filter by
("qa_rejection", "build_error", "test_failure")
num_results: Maximum number of patterns to return (default: 5)
min_score: Minimum relevance score 0.0-1.0 (default: 0.5)
Returns:
Formatted failure pattern suggestions string or None if unavailable
"""
if is_debug_enabled():
debug(
"memory",
"Retrieving failure patterns",
query=query[:100],
failure_types=failure_types,
num_results=num_results,
)
if not is_graphiti_enabled():
if is_debug_enabled():
debug("memory", "Graphiti not enabled, skipping failure pattern retrieval")
return None
memory = None
try:
# Get GraphitiMemory instance
memory = await get_graphiti_memory(spec_dir, project_dir)
if memory is None:
if is_debug_enabled():
debug_warning(
"memory", "GraphitiMemory not available for failure patterns"
)
return None
# Import schema constants
from integrations.graphiti.queries_pkg.schema import EPISODE_TYPE_ROOT_CAUSE
if is_debug_enabled():
debug_detailed(
"memory",
"Searching for failure patterns",
query=query[:200],
group_id=memory.group_id,
failure_types=failure_types,
)
# Search for root cause episodes
search_query = f"root cause failure {query}"
client = memory.client
if client is None:
if is_debug_enabled():
debug_warning("memory", "No client available on memory instance")
return None
results = await client.graphiti.search(
query=search_query,
group_ids=[memory.group_id],
num_results=num_results * 2, # Get extra results for filtering
)
if is_debug_enabled():
debug(
"memory",
"Failure pattern search complete",
raw_results=len(results) if results else 0,
)
if not results:
if is_debug_enabled():
debug("memory", "No failure patterns found")
return None
# Parse and filter results
failure_patterns = []
for result in results:
content = (
getattr(result, "content", None)
or getattr(result, "fact", None)
or (result.get("content") if isinstance(result, dict) else None)
)
score = getattr(result, "score", None)
if score is None and isinstance(result, dict):
score = result.get("score", 0.0)
if score is None:
score = 0.0
if score < min_score:
continue
if content:
try:
data = json.loads(content) if isinstance(content, str) else content
# Ensure data is a dict
if not isinstance(data, dict):
continue
# Verify it's a root cause episode
if data.get("type") != EPISODE_TYPE_ROOT_CAUSE:
continue
# Filter by failure type if specified
if failure_types and data.get("failure_type") not in failure_types:
continue
# Extract failure pattern data
pattern = {
"failure_type": data.get("failure_type", "unknown"),
"category": data.get("category", "unknown"),
"description": data.get("description", ""),
"affected_files": data.get("affected_files", []),
"confidence": data.get("confidence", 0.0),
"recommendations": data.get("recommendations", []),
"is_recurring": data.get("is_recurring", False),
"score": score,
"spec_id": data.get("spec_id", ""),
}
failure_patterns.append(pattern)
if len(failure_patterns) >= num_results:
break
except (json.JSONDecodeError, AttributeError, KeyError) as e:
if is_debug_enabled():
debug_warning(
"memory",
"Failed to parse failure pattern result",
error=str(e),
)
continue
if is_debug_enabled():
debug(
"memory",
"Failure pattern parsing complete",
patterns_found=len(failure_patterns),
)
if not failure_patterns:
if is_debug_enabled():
debug("memory", "No relevant failure patterns after filtering")
return None
# Format the failure patterns
sections = ["## Failure Pattern Analysis\n"]
sections.append("_Similar failures from past builds (learn from history):_\n")
# Group patterns by failure type and category
by_type: dict[str, list[dict]] = {}
for pattern in failure_patterns:
failure_type = pattern.get("failure_type", "unknown")
if failure_type not in by_type:
by_type[failure_type] = []
by_type[failure_type].append(pattern)
# Format each failure type
for failure_type, type_patterns in by_type.items():
sections.append(f"### {failure_type.replace('_', ' ').title()}\n")
# Group by category within type
by_category: dict[str, list[dict]] = {}
for p in type_patterns:
category = p.get("category", "uncategorized")
if category not in by_category:
by_category[category] = []
by_category[category].append(p)
for category, category_patterns in by_category.items():
sections.append(f"#### {category.replace('_', ' ').title()}\n")
for p in category_patterns:
description = p.get("description", "")
confidence = p.get("confidence", 0.0)
score = p.get("score", 0.0)
recommendations = p.get("recommendations", [])
is_recurring = p.get("is_recurring", False)
affected_files = p.get("affected_files", [])
spec_id = p.get("spec_id", "")
sections.append(f"- **Root Cause**: {description}\n")
if affected_files:
files_str = ", ".join(affected_files[:3])
if len(affected_files) > 3:
files_str += f" (+{len(affected_files) - 3} more)"
sections.append(f" _Affected Files_: {files_str}\n")
if recommendations:
sections.append(" _Recommendations_:\n")
for rec in recommendations[:3]: # Limit to top 3
sections.append(f" • {rec}\n")
sections.append(
f" _Confidence_: {confidence:.2f} | _Relevance_: {score:.2f}"
)
if is_recurring:
sections.append(" | ⚠️ _RECURRING ISSUE_")
if spec_id:
sections.append(f" | _From_: {spec_id}")
sections.append("\n")
formatted = "".join(sections)
if is_debug_enabled():
debug_success(
"memory",
"Failure patterns formatted",
types=len(by_type),
total_patterns=len(failure_patterns),
)
return formatted
except Exception as e:
if is_debug_enabled():
debug_error("memory", "Failed to get failure patterns", error=str(e))
logger.warning(f"Failed to get failure patterns: {e}")
capture_exception(
e,
query_summary=query[:100] if query else "",
spec_dir=str(spec_dir),
project_dir=str(project_dir),
operation="get_failure_patterns",
)
return None
finally:
# Close memory connection if we opened it
if memory is not None:
try:
await memory.close()
except Exception:
pass
async def get_graphiti_context(
spec_dir: Path,
project_dir: Path,
subtask: dict,
) -> str | None:
"""
Retrieve relevant context from Graphiti for the current subtask.
This searches the knowledge graph for context relevant to the subtask's
task description, returning past insights, patterns, and gotchas.
Args:
spec_dir: Spec directory
project_dir: Project root directory
subtask: The current subtask being worked on
Returns:
Formatted context string or None if unavailable
"""
if is_debug_enabled():
debug(
"memory",
"Retrieving Graphiti context for subtask",
subtask_id=subtask.get("id", "unknown"),
subtask_desc=subtask.get("description", "")[:100],
)
if not is_graphiti_enabled():
if is_debug_enabled():
debug("memory", "Graphiti not enabled, skipping context retrieval")
return None
memory = None
try:
# Use centralized helper for GraphitiMemory instantiation (async)
memory = await get_graphiti_memory(spec_dir, project_dir)
if memory is None:
if is_debug_enabled():
debug_warning(
"memory", "GraphitiMemory not available for context retrieval"
)
return None
# Build search query from subtask description
subtask_desc = subtask.get("description", "")
subtask_id = subtask.get("id", "")
query = f"{subtask_desc} {subtask_id}".strip()
if not query:
if is_debug_enabled():
debug_warning("memory", "Empty query, skipping context retrieval")
return None
if is_debug_enabled():
debug_detailed(
"memory",
"Searching Graphiti knowledge graph",
query=query[:200],
num_results=5,
)
# Get relevant context
context_items = await memory.get_relevant_context(query, num_results=5)
# Get patterns and gotchas specifically (THE FIX for learning loop!)
# This retrieves PATTERN and GOTCHA episode types for cross-session learning
patterns, gotchas = await memory.get_patterns_and_gotchas(
query, num_results=3, min_score=0.5
)
# Also get recent session history
session_history = await memory.get_session_history(limit=3)
if is_debug_enabled():
debug(
"memory",
"Graphiti context retrieval complete",
context_items_found=len(context_items) if context_items else 0,
patterns_found=len(patterns) if patterns else 0,
gotchas_found=len(gotchas) if gotchas else 0,
session_history_found=len(session_history) if session_history else 0,
)
if not context_items and not session_history and not patterns and not gotchas:
if is_debug_enabled():
debug("memory", "No relevant context found in Graphiti")
return None
# Format the context
sections = ["## Graphiti Memory Context\n"]
sections.append("_Retrieved from knowledge graph for this subtask:_\n")
if context_items:
sections.append("### Relevant Knowledge\n")
for item in context_items:
content = item.get("content", "")[:500] # Truncate
item_type = item.get("type", "unknown")
sections.append(f"- **[{item_type}]** {content}\n")
# Add patterns section (cross-session learning)
if patterns:
sections.append("### Learned Patterns\n")
sections.append("_Patterns discovered in previous sessions:_\n")
for p in patterns:
pattern_text = p.get("pattern", "")
applies_to = p.get("applies_to", "")
if applies_to:
sections.append(
f"- **Pattern**: {pattern_text}\n _Applies to:_ {applies_to}\n"
)
else:
sections.append(f"- **Pattern**: {pattern_text}\n")
# Add gotchas section (cross-session learning)
if gotchas:
sections.append("### Known Gotchas\n")
sections.append("_Pitfalls to avoid:_\n")
for g in gotchas:
gotcha_text = g.get("gotcha", "")
solution = g.get("solution", "")
if solution:
sections.append(
f"- **Gotcha**: {gotcha_text}\n _Solution:_ {solution}\n"
)
else:
sections.append(f"- **Gotcha**: {gotcha_text}\n")
if session_history:
sections.append("### Recent Session Insights\n")
for session in session_history[:2]: # Only show last 2
session_num = session.get("session_number", "?")
recommendations = session.get("recommendations_for_next_session", [])
if recommendations:
sections.append(f"**Session {session_num} recommendations:**")
for rec in recommendations[:3]: # Limit to 3
sections.append(f"- {rec}")
sections.append("")
if is_debug_enabled():
debug_success(
"memory", "Graphiti context formatted", total_sections=len(sections)
)
return "\n".join(sections)
except Exception as e:
logger.warning(f"Failed to get Graphiti context: {e}")
if is_debug_enabled():
debug_error("memory", "Graphiti context retrieval failed", error=str(e))
# Capture exception to Sentry with full context
capture_exception(
e,
operation="get_graphiti_context",
subtask_id=subtask.get("id", "unknown"),
subtask_desc=subtask.get("description", "")[:200],
spec_dir=str(spec_dir),
project_dir=str(project_dir),
)
return None
finally:
# Always close the memory connection (swallow exceptions to avoid overriding)
if memory is not None:
try:
await memory.close()
except Exception:
logger.debug(
"Failed to close Graphiti memory connection", exc_info=True
)
async def save_session_memory(
spec_dir: Path,
project_dir: Path,
subtask_id: str,
session_num: int,
success: bool,
subtasks_completed: list[str],
discoveries: dict | None = None,
) -> tuple[bool, str]:
"""
Save session insights to memory.
Memory Strategy:
- PRIMARY: Graphiti (when enabled) - provides semantic search, cross-session context
- FALLBACK: File-based (when Graphiti is disabled) - zero dependencies, always works
This is called after each session to persist learnings.
Args:
spec_dir: Spec directory
project_dir: Project root directory
subtask_id: The subtask that was worked on
session_num: Current session number
success: Whether the subtask was completed successfully
subtasks_completed: List of subtask IDs completed this session
discoveries: Optional dict with file discoveries, patterns, gotchas
Returns:
Tuple of (success, storage_type) where storage_type is "graphiti" or "file"
"""
# Debug: Log memory save start
if is_debug_enabled():
debug_section("memory", f"Saving Session {session_num} Memory")
debug(
"memory",
"Memory save initiated",
subtask_id=subtask_id,
session_num=session_num,
success=success,
subtasks_completed=subtasks_completed,
spec_dir=str(spec_dir),
)
# Build insights structure (same format for both storage systems)
insights = {
"subtasks_completed": subtasks_completed,
"discoveries": discoveries
or {
"files_understood": {},
"patterns_found": [],
"gotchas_encountered": [],
},
"what_worked": [f"Implemented subtask: {subtask_id}"] if success else [],
"what_failed": [] if success else [f"Failed to complete subtask: {subtask_id}"],
"recommendations_for_next_session": [],
}
if is_debug_enabled():
debug_detailed("memory", "Insights structure built", insights=insights)
# Check Graphiti status for debugging
graphiti_enabled = is_graphiti_enabled()
if is_debug_enabled():
graphiti_status = get_graphiti_status()
debug(
"memory",
"Graphiti status check",
enabled=graphiti_status.get("enabled"),
available=graphiti_status.get("available"),
host=graphiti_status.get("host"),
port=graphiti_status.get("port"),
database=graphiti_status.get("database"),
llm_provider=graphiti_status.get("llm_provider"),
embedder_provider=graphiti_status.get("embedder_provider"),
reason=graphiti_status.get("reason") or "OK",
)
# PRIMARY: Try Graphiti if enabled
if graphiti_enabled:
if is_debug_enabled():
debug("memory", "Attempting PRIMARY storage: Graphiti")
memory = None
try:
# Use centralized helper for GraphitiMemory instantiation (async)
memory = await get_graphiti_memory(spec_dir, project_dir)
if memory is None:
if is_debug_enabled():
debug_warning("memory", "GraphitiMemory not available")
debug(
"memory",
"get_graphiti_memory() returned None - this usually means Graphiti is disabled or provider config is invalid",
)
# Continue to file-based fallback
if memory is not None and memory.is_enabled:
if is_debug_enabled():
debug("memory", "Saving to Graphiti...")
# Use structured insights if we have rich extracted data
if discoveries and discoveries.get("file_insights"):
# Rich insights from insight_extractor
if is_debug_enabled():
debug(
"memory",
"Using save_structured_insights (rich data available)",
)
result = await memory.save_structured_insights(discoveries)
else:
# Fallback to basic session insights
result = await memory.save_session_insights(session_num, insights)
if result:
logger.info(
f"Session {session_num} insights saved to Graphiti (primary)"
)
if is_debug_enabled():
debug_success(
"memory",
f"Session {session_num} saved to Graphiti (PRIMARY)",
storage_type="graphiti",
subtasks_saved=len(subtasks_completed),
)
return True, "graphiti"
else:
logger.warning(
"Graphiti save returned False, falling back to file-based"
)
if is_debug_enabled():
debug_warning(
"memory", "Graphiti save returned False, using FALLBACK"
)
elif memory is None:
if is_debug_enabled():
debug_warning(
"memory", "GraphitiMemory not available, using FALLBACK"
)
else:
# memory is not None but memory.is_enabled is False
logger.warning(
"GraphitiMemory.is_enabled=False, falling back to file-based"
)
if is_debug_enabled():
debug_warning("memory", "GraphitiMemory disabled, using FALLBACK")
except Exception as e:
logger.warning(f"Graphiti save failed: {e}, falling back to file-based")
if is_debug_enabled():
debug_error("memory", "Graphiti save failed", error=str(e))
# Capture exception to Sentry with full context
capture_exception(
e,
operation="save_session_memory_graphiti",
subtask_id=subtask_id,
session_num=session_num,
success=success,
subtasks_completed=subtasks_completed,
spec_dir=str(spec_dir),
project_dir=str(project_dir),
)
finally:
# Always close the memory connection (swallow exceptions to avoid overriding)
if memory is not None:
try:
await memory.close()
except Exception as e:
logger.debug(
"Failed to close Graphiti memory connection", exc_info=e
)
else:
if is_debug_enabled():
debug("memory", "Graphiti not enabled, skipping to FALLBACK")
# FALLBACK: File-based memory (when Graphiti is disabled or fails)
if is_debug_enabled():
debug("memory", "Attempting FALLBACK storage: File-based")
try:
memory_dir = spec_dir / "memory" / "session_insights"
if is_debug_enabled():
debug_detailed(
"memory",
"File-based memory path",
memory_dir=str(memory_dir),
session_file=f"session_{session_num:03d}.json",
)
save_file_based_memory(spec_dir, session_num, insights)
logger.info(
f"Session {session_num} insights saved to file-based memory (fallback)"
)
if is_debug_enabled():
debug_success(
"memory",
f"Session {session_num} saved to file-based (FALLBACK)",
storage_type="file",
file_path=str(memory_dir / f"session_{session_num:03d}.json"),
subtasks_saved=len(subtasks_completed),
)
return True, "file"
except Exception as e:
logger.error(f"File-based memory save also failed: {e}")
if is_debug_enabled():
debug_error("memory", "File-based memory save FAILED", error=str(e))
# Capture exception to Sentry with full context
capture_exception(
e,
operation="save_session_memory_file",
subtask_id=subtask_id,
session_num=session_num,
success=success,
subtasks_completed=subtasks_completed,
spec_dir=str(spec_dir),
project_dir=str(project_dir),
)
return False, "none"
async def save_feedback(
spec_dir: Path,
project_dir: Path,
feedback_type: str,
task_description: str,
agent_type: str,
context: dict | None = None,
rating: int | None = None,
) -> bool:
"""
Save user feedback (accept/reject/modify) to memory and update preferences.
This is the primary feedback collection function that tracks all user
interactions with agent outputs and updates the preference profile to
enable adaptive behavior.
Args:
spec_dir: Spec directory
project_dir: Project root directory
feedback_type: Type of feedback ("accepted", "rejected", "modified")
task_description: Description of the task that was evaluated
agent_type: Type of agent that produced the output (planner, coder, qa_reviewer, etc.)
context: Optional additional context about the feedback
For "modified": should include what was changed
For "rejected": should include why it was rejected
rating: Optional rating (1-5 for stars, or 0/1 for thumbs down/up)
Returns:
True if saved successfully, False otherwise
"""
if not is_graphiti_enabled():
if is_debug_enabled():
debug("memory", "Graphiti not enabled, skipping feedback save")
return False
memory = None
try:
memory = await get_graphiti_memory(spec_dir, project_dir)
if memory is None:
if is_debug_enabled():
debug_warning("memory", "GraphitiMemory not available for feedback")
return False
if is_debug_enabled():
debug_data = {
"feedback_type": feedback_type,
"agent_type": agent_type,
"task": task_description[:100],
}
if rating is not None:
debug_data["rating"] = rating
debug(
"memory",