diff --git a/apps/backend/agents/memory_manager.py b/apps/backend/agents/memory_manager.py index a1ea6a5c..8571fe61 100644 --- a/apps/backend/agents/memory_manager.py +++ b/apps/backend/agents/memory_manager.py @@ -10,6 +10,7 @@ Handles session memory storage using dual-layer approach: import logging from pathlib import Path +from core.sentry import capture_exception from debug import ( debug, debug_detailed, @@ -116,12 +117,15 @@ async def get_graphiti_context( memory = None try: - # Use centralized helper for GraphitiMemory instantiation - memory = get_graphiti_memory(spec_dir, project_dir) + # 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_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", "") @@ -228,6 +232,15 @@ async def get_graphiti_context( 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) @@ -324,21 +337,16 @@ async def save_session_memory( memory = None try: - # Use centralized helper for GraphitiMemory instantiation - memory = get_graphiti_memory(spec_dir, project_dir) + # 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") - # Continue to file-based fallback - else: - if is_debug_enabled(): - debug_detailed( + debug( "memory", - "GraphitiMemory instance created", - is_enabled=memory.is_enabled, - group_id=getattr(memory, "group_id", "unknown"), + "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...") @@ -393,6 +401,17 @@ async def save_session_memory( 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: @@ -438,6 +457,17 @@ async def save_session_memory( 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" diff --git a/apps/backend/agents/tools_pkg/tools/memory.py b/apps/backend/agents/tools_pkg/tools/memory.py index 4a9868a1..3181ab90 100644 --- a/apps/backend/agents/tools_pkg/tools/memory.py +++ b/apps/backend/agents/tools_pkg/tools/memory.py @@ -52,7 +52,7 @@ async def _save_to_graphiti_async( # The helper handles enablement checks internally from memory.graphiti_helpers import get_graphiti_memory - memory = get_graphiti_memory(spec_dir, project_dir) + memory = await get_graphiti_memory(spec_dir, project_dir) if memory is None: return False diff --git a/apps/backend/integrations/graphiti/queries_pkg/client.py b/apps/backend/integrations/graphiti/queries_pkg/client.py index 3808d9d5..3852e05b 100644 --- a/apps/backend/integrations/graphiti/queries_pkg/client.py +++ b/apps/backend/integrations/graphiti/queries_pkg/client.py @@ -9,6 +9,7 @@ import logging import sys from datetime import datetime, timezone +from core.sentry import capture_exception from graphiti_config import GraphitiConfig, GraphitiState logger = logging.getLogger(__name__) @@ -133,9 +134,23 @@ class GraphitiClient: ) except ProviderNotInstalled as e: logger.warning(f"LLM provider packages not installed: {e}") + capture_exception( + e, + error_type="ProviderNotInstalled", + provider_type="llm", + llm_provider=self.config.llm_provider, + embedder_provider=self.config.embedder_provider, + ) return False except ProviderError as e: logger.warning(f"LLM provider configuration error: {e}") + capture_exception( + e, + error_type="ProviderError", + provider_type="llm", + llm_provider=self.config.llm_provider, + embedder_provider=self.config.embedder_provider, + ) return False try: @@ -145,9 +160,23 @@ class GraphitiClient: ) except ProviderNotInstalled as e: logger.warning(f"Embedder provider packages not installed: {e}") + capture_exception( + e, + error_type="ProviderNotInstalled", + provider_type="embedder", + llm_provider=self.config.llm_provider, + embedder_provider=self.config.embedder_provider, + ) return False except ProviderError as e: logger.warning(f"Embedder provider configuration error: {e}") + capture_exception( + e, + error_type="ProviderError", + provider_type="embedder", + llm_provider=self.config.llm_provider, + embedder_provider=self.config.embedder_provider, + ) return False # Apply LadybugDB monkeypatch to use it via graphiti's KuzuDriver @@ -173,15 +202,36 @@ class GraphitiClient: logger.warning( f"Failed to initialize LadybugDB driver at {db_path}: {e}" ) + capture_exception( + e, + error_type=type(e).__name__, + db_path=str(db_path), + llm_provider=self.config.llm_provider, + embedder_provider=self.config.embedder_provider, + ) return False except Exception as e: logger.warning( f"Unexpected error initializing LadybugDB driver at {db_path}: {e}" ) + capture_exception( + e, + error_type=type(e).__name__, + db_path=str(db_path), + llm_provider=self.config.llm_provider, + embedder_provider=self.config.embedder_provider, + ) return False logger.info(f"Initialized LadybugDB driver (patched) at: {db_path}") except ImportError as e: logger.warning(f"KuzuDriver not available: {e}") + capture_exception( + e, + error_type="ImportError", + component="kuzu_driver_patched", + llm_provider=self.config.llm_provider, + embedder_provider=self.config.embedder_provider, + ) return False # Initialize Graphiti with the custom providers @@ -216,10 +266,23 @@ class GraphitiClient: f"Graphiti packages not installed: {e}. " "Install with: pip install real_ladybug graphiti-core" ) + capture_exception( + e, + error_type="ImportError", + component="graphiti_core", + llm_provider=self.config.llm_provider, + embedder_provider=self.config.embedder_provider, + ) return False except Exception as e: logger.warning(f"Failed to initialize Graphiti client: {e}") + capture_exception( + e, + error_type=type(e).__name__, + llm_provider=self.config.llm_provider, + embedder_provider=self.config.embedder_provider, + ) return False async def close(self) -> None: diff --git a/apps/backend/integrations/graphiti/queries_pkg/graphiti.py b/apps/backend/integrations/graphiti/queries_pkg/graphiti.py index 0ebd98d9..ef104358 100644 --- a/apps/backend/integrations/graphiti/queries_pkg/graphiti.py +++ b/apps/backend/integrations/graphiti/queries_pkg/graphiti.py @@ -13,6 +13,7 @@ import logging from datetime import datetime, timezone from pathlib import Path +from core.sentry import capture_exception from graphiti_config import GraphitiConfig, GraphitiState from .client import GraphitiClient @@ -196,6 +197,13 @@ class GraphitiMemory: except Exception as e: logger.warning(f"Failed to initialize Graphiti: {e}") self._record_error(f"Initialization failed: {e}") + capture_exception( + e, + component="graphiti", + operation="initialize", + group_id=self.group_id, + group_id_mode=self.group_id_mode, + ) self._available = False return False @@ -220,14 +228,25 @@ class GraphitiMemory: if not await self._ensure_initialized(): return False - result = await self._queries.add_session_insight(session_num, insights) + try: + result = await self._queries.add_session_insight(session_num, insights) - if result and self.state: - self.state.last_session = session_num - self.state.episode_count += 1 - self.state.save(self.spec_dir) + if result and self.state: + self.state.last_session = session_num + self.state.episode_count += 1 + self.state.save(self.spec_dir) - return result + return result + except Exception as e: + logger.warning(f"Failed to save session insights: {e}") + self._record_error(f"save_session_insights failed: {e}") + capture_exception( + e, + component="graphiti", + operation="save_session_insights", + session_num=session_num, + ) + return False async def save_codebase_discoveries( self, @@ -237,39 +256,69 @@ class GraphitiMemory: if not await self._ensure_initialized(): return False - result = await self._queries.add_codebase_discoveries(discoveries) + try: + result = await self._queries.add_codebase_discoveries(discoveries) - if result and self.state: - self.state.episode_count += 1 - self.state.save(self.spec_dir) + if result and self.state: + self.state.episode_count += 1 + self.state.save(self.spec_dir) - return result + return result + except Exception as e: + logger.warning(f"Failed to save codebase discoveries: {e}") + self._record_error(f"save_codebase_discoveries failed: {e}") + capture_exception( + e, + component="graphiti", + operation="save_codebase_discoveries", + ) + return False async def save_pattern(self, pattern: str) -> bool: """Save a code pattern to the knowledge graph.""" if not await self._ensure_initialized(): return False - result = await self._queries.add_pattern(pattern) + try: + result = await self._queries.add_pattern(pattern) - if result and self.state: - self.state.episode_count += 1 - self.state.save(self.spec_dir) + if result and self.state: + self.state.episode_count += 1 + self.state.save(self.spec_dir) - return result + return result + except Exception as e: + logger.warning(f"Failed to save pattern: {e}") + self._record_error(f"save_pattern failed: {e}") + capture_exception( + e, + component="graphiti", + operation="save_pattern", + ) + return False async def save_gotcha(self, gotcha: str) -> bool: """Save a gotcha (pitfall) to the knowledge graph.""" if not await self._ensure_initialized(): return False - result = await self._queries.add_gotcha(gotcha) + try: + result = await self._queries.add_gotcha(gotcha) - if result and self.state: - self.state.episode_count += 1 - self.state.save(self.spec_dir) + if result and self.state: + self.state.episode_count += 1 + self.state.save(self.spec_dir) - return result + return result + except Exception as e: + logger.warning(f"Failed to save gotcha: {e}") + self._record_error(f"save_gotcha failed: {e}") + capture_exception( + e, + component="graphiti", + operation="save_gotcha", + ) + return False async def save_task_outcome( self, @@ -282,28 +331,49 @@ class GraphitiMemory: if not await self._ensure_initialized(): return False - result = await self._queries.add_task_outcome( - task_id, success, outcome, metadata - ) + try: + result = await self._queries.add_task_outcome( + task_id, success, outcome, metadata + ) - if result and self.state: - self.state.episode_count += 1 - self.state.save(self.spec_dir) + if result and self.state: + self.state.episode_count += 1 + self.state.save(self.spec_dir) - return result + return result + except Exception as e: + logger.warning(f"Failed to save task outcome: {e}") + self._record_error(f"save_task_outcome failed: {e}") + capture_exception( + e, + component="graphiti", + operation="save_task_outcome", + task_id=task_id, + ) + return False async def save_structured_insights(self, insights: dict) -> bool: """Save extracted insights as multiple focused episodes.""" if not await self._ensure_initialized(): return False - result = await self._queries.add_structured_insights(insights) + try: + result = await self._queries.add_structured_insights(insights) - if result and self.state: - # Episode count updated in queries module - pass + if result and self.state: + # Episode count updated in queries module + pass - return result + return result + except Exception as e: + logger.warning(f"Failed to save structured insights: {e}") + self._record_error(f"save_structured_insights failed: {e}") + capture_exception( + e, + component="graphiti", + operation="save_structured_insights", + ) + return False # Delegate methods to search module @@ -317,9 +387,19 @@ class GraphitiMemory: if not await self._ensure_initialized(): return [] - return await self._search.get_relevant_context( - query, num_results, include_project_context - ) + try: + return await self._search.get_relevant_context( + query, num_results, include_project_context + ) + except Exception as e: + logger.warning(f"Failed to get relevant context: {e}") + self._record_error(f"get_relevant_context failed: {e}") + capture_exception( + e, + component="graphiti", + operation="get_relevant_context", + ) + return [] async def get_session_history( self, @@ -330,7 +410,17 @@ class GraphitiMemory: if not await self._ensure_initialized(): return [] - return await self._search.get_session_history(limit, spec_only) + try: + return await self._search.get_session_history(limit, spec_only) + except Exception as e: + logger.warning(f"Failed to get session history: {e}") + self._record_error(f"get_session_history failed: {e}") + capture_exception( + e, + component="graphiti", + operation="get_session_history", + ) + return [] async def get_similar_task_outcomes( self, @@ -341,7 +431,17 @@ class GraphitiMemory: if not await self._ensure_initialized(): return [] - return await self._search.get_similar_task_outcomes(task_description, limit) + try: + return await self._search.get_similar_task_outcomes(task_description, limit) + except Exception as e: + logger.warning(f"Failed to get similar task outcomes: {e}") + self._record_error(f"get_similar_task_outcomes failed: {e}") + capture_exception( + e, + component="graphiti", + operation="get_similar_task_outcomes", + ) + return [] async def get_patterns_and_gotchas( self, @@ -367,9 +467,19 @@ class GraphitiMemory: if not await self._ensure_initialized(): return [], [] - return await self._search.get_patterns_and_gotchas( - query, num_results, min_score - ) + try: + return await self._search.get_patterns_and_gotchas( + query, num_results, min_score + ) + except Exception as e: + logger.warning(f"Failed to get patterns and gotchas: {e}") + self._record_error(f"get_patterns_and_gotchas failed: {e}") + capture_exception( + e, + component="graphiti", + operation="get_patterns_and_gotchas", + ) + return [], [] # Status and utility methods diff --git a/apps/backend/integrations/graphiti/queries_pkg/queries.py b/apps/backend/integrations/graphiti/queries_pkg/queries.py index 3ec5db70..cf67cf6b 100644 --- a/apps/backend/integrations/graphiti/queries_pkg/queries.py +++ b/apps/backend/integrations/graphiti/queries_pkg/queries.py @@ -8,6 +8,8 @@ import json import logging from datetime import datetime, timezone +from core.sentry import capture_exception + from .schema import ( EPISODE_TYPE_CODEBASE_DISCOVERY, EPISODE_TYPE_GOTCHA, @@ -82,6 +84,13 @@ class GraphitiQueries: except Exception as e: logger.warning(f"Failed to save session insights: {e}") + capture_exception( + e, + operation="add_session_insight", + group_id=self.group_id, + spec_id=self.spec_context_id, + session_number=session_num, + ) return False async def add_codebase_discoveries( @@ -124,6 +133,13 @@ class GraphitiQueries: except Exception as e: logger.warning(f"Failed to save codebase discoveries: {e}") + capture_exception( + e, + operation="add_codebase_discoveries", + group_id=self.group_id, + spec_id=self.spec_context_id, + discovery_count=len(discoveries), + ) return False async def add_pattern(self, pattern: str) -> bool: @@ -160,6 +176,13 @@ class GraphitiQueries: except Exception as e: logger.warning(f"Failed to save pattern: {e}") + capture_exception( + e, + operation="add_pattern", + group_id=self.group_id, + spec_id=self.spec_context_id, + content_summary=pattern[:100] if pattern else "", + ) return False async def add_gotcha(self, gotcha: str) -> bool: @@ -196,6 +219,13 @@ class GraphitiQueries: except Exception as e: logger.warning(f"Failed to save gotcha: {e}") + capture_exception( + e, + operation="add_gotcha", + group_id=self.group_id, + spec_id=self.spec_context_id, + content_summary=gotcha[:100] if gotcha else "", + ) return False async def add_task_outcome( @@ -245,6 +275,15 @@ class GraphitiQueries: except Exception as e: logger.warning(f"Failed to save task outcome: {e}") + capture_exception( + e, + operation="add_task_outcome", + group_id=self.group_id, + spec_id=self.spec_context_id, + task_id=task_id, + success=success, + content_summary=outcome[:100] if outcome else "", + ) return False async def add_structured_insights(self, insights: dict) -> bool: @@ -459,4 +498,26 @@ class GraphitiQueries: except Exception as e: logger.warning(f"Failed to save structured insights: {e}") + # Build content summary of insight types + insight_types = [] + if insights.get("file_insights"): + insight_types.append(f"files:{len(insights['file_insights'])}") + if insights.get("patterns_discovered"): + insight_types.append(f"patterns:{len(insights['patterns_discovered'])}") + if insights.get("gotchas_discovered"): + insight_types.append(f"gotchas:{len(insights['gotchas_discovered'])}") + if insights.get("approach_outcome"): + insight_types.append("outcome:1") + if insights.get("recommendations"): + insight_types.append( + f"recommendations:{len(insights['recommendations'])}" + ) + + capture_exception( + e, + operation="add_structured_insights", + group_id=self.group_id, + spec_id=self.spec_context_id, + content_summary=", ".join(insight_types) if insight_types else "empty", + ) return False diff --git a/apps/backend/integrations/graphiti/queries_pkg/search.py b/apps/backend/integrations/graphiti/queries_pkg/search.py index f8c96e2e..155e2409 100644 --- a/apps/backend/integrations/graphiti/queries_pkg/search.py +++ b/apps/backend/integrations/graphiti/queries_pkg/search.py @@ -9,6 +9,8 @@ import json import logging from pathlib import Path +from core.sentry import capture_exception + from .schema import ( EPISODE_TYPE_GOTCHA, EPISODE_TYPE_PATTERN, @@ -120,6 +122,12 @@ class GraphitiSearch: except Exception as e: logger.warning(f"Failed to search context: {e}") + capture_exception( + e, + query_summary=query[:100] if query else "", + group_id=self.group_id, + operation="get_relevant_context", + ) return [] async def get_session_history( @@ -174,6 +182,11 @@ class GraphitiSearch: except Exception as e: logger.warning(f"Failed to get session history: {e}") + capture_exception( + e, + group_id=self.group_id, + operation="get_session_history", + ) return [] async def get_similar_task_outcomes( @@ -227,6 +240,12 @@ class GraphitiSearch: except Exception as e: logger.warning(f"Failed to get similar task outcomes: {e}") + capture_exception( + e, + query_summary=task_description[:100] if task_description else "", + group_id=self.group_id, + operation="get_similar_task_outcomes", + ) return [] async def get_patterns_and_gotchas( @@ -337,4 +356,10 @@ class GraphitiSearch: except Exception as e: logger.warning(f"Failed to get patterns/gotchas: {e}") + capture_exception( + e, + query_summary=query[:100] if query else "", + group_id=self.group_id, + operation="get_patterns_and_gotchas", + ) return [], [] diff --git a/apps/backend/memory/codebase_map.py b/apps/backend/memory/codebase_map.py index 689d5203..4d108b0c 100644 --- a/apps/backend/memory/codebase_map.py +++ b/apps/backend/memory/codebase_map.py @@ -64,9 +64,10 @@ def update_codebase_map(spec_dir: Path, discoveries: dict[str, str]) -> None: # Also save to Graphiti if enabled if is_graphiti_memory_enabled() and discoveries: try: - graphiti = get_graphiti_memory(spec_dir) + graphiti = run_async(get_graphiti_memory(spec_dir)) if graphiti: run_async(graphiti.save_codebase_discoveries(discoveries)) + run_async(graphiti.close()) logger.info("Codebase discoveries also saved to Graphiti") except Exception as e: logger.warning(f"Graphiti codebase save failed: {e}") diff --git a/apps/backend/memory/graphiti_helpers.py b/apps/backend/memory/graphiti_helpers.py index 17029662..11a1a075 100644 --- a/apps/backend/memory/graphiti_helpers.py +++ b/apps/backend/memory/graphiti_helpers.py @@ -12,6 +12,8 @@ import logging from pathlib import Path from typing import TYPE_CHECKING, Any +from core.sentry import capture_exception + logger = logging.getLogger(__name__) if TYPE_CHECKING: @@ -37,18 +39,22 @@ def is_graphiti_memory_enabled() -> bool: return False -def get_graphiti_memory( +async def get_graphiti_memory( spec_dir: Path, project_dir: Path | None = None ) -> "GraphitiMemory | None": """ - Get a GraphitiMemory instance if available. + Get an initialized GraphitiMemory instance if available. Args: spec_dir: Spec directory project_dir: Project root directory (defaults to spec_dir.parent.parent) Returns: - GraphitiMemory instance or None if not available + Initialized GraphitiMemory instance or None if not available + + Note: + This function is async and calls initialize() on the memory instance + before returning, following the GitHub pattern for proper initialization. """ if not is_graphiti_memory_enabled(): return None @@ -59,29 +65,54 @@ def get_graphiti_memory( if project_dir is None: project_dir = spec_dir.parent.parent # Use project-wide shared memory for cross-spec learning - return GraphitiMemory(spec_dir, project_dir, group_id_mode=GroupIdMode.PROJECT) + memory = GraphitiMemory( + spec_dir, project_dir, group_id_mode=GroupIdMode.PROJECT + ) + + # Initialize the memory instance (following GitHub pattern) + await memory.initialize() + + return memory except ImportError: return None + except Exception as e: + logger.warning(f"Failed to initialize Graphiti memory: {e}") + capture_exception( + e, + function="get_graphiti_memory", + spec_dir=str(spec_dir), + project_dir=str(project_dir) if project_dir else None, + ) + return None def run_async(coro): """ Run an async coroutine synchronously. - Handles the case where we're already in an event loop. + NOTE: This should only be called from synchronous code. For async callers, + use the async function directly with await to ensure proper execution. Args: coro: Async coroutine to run Returns: - Result of the coroutine or a Future if already in event loop + Result of the coroutine, or None if already in an async context """ try: - loop = asyncio.get_running_loop() - # Already in an event loop - create a task - return asyncio.ensure_future(coro) + asyncio.get_running_loop() + # Already in an async context - caller should use await directly + # Log a warning and return None to avoid returning a Future that + # callers would incorrectly try to use as the actual result + logger.warning( + "run_async called from async context. " + "Use await directly for proper execution." + ) + # Close the coroutine to avoid "coroutine was never awaited" warning + coro.close() + return None except RuntimeError: - # No event loop running - create one + # No event loop running - safe to create one return asyncio.run(coro) @@ -105,7 +136,7 @@ async def save_to_graphiti_async( Returns: True if save succeeded, False otherwise """ - graphiti = get_graphiti_memory(spec_dir, project_dir) + graphiti = await get_graphiti_memory(spec_dir, project_dir) if graphiti is None: return False @@ -130,13 +161,27 @@ async def save_to_graphiti_async( except Exception as e: logger.warning(f"Failed to save to Graphiti: {e}") + capture_exception( + e, + function="save_to_graphiti_async", + spec_dir=str(spec_dir), + session_num=session_num, + project_dir=str(project_dir) if project_dir else None, + ) return False finally: # Always close the graphiti connection (swallow exceptions to avoid overriding) if graphiti is not None: try: await graphiti.close() - except Exception as e: + except Exception as close_error: logger.debug( "Failed to close Graphiti memory connection", exc_info=True ) + capture_exception( + close_error, + function="save_to_graphiti_async", + context="closing_connection", + spec_dir=str(spec_dir), + session_num=session_num, + ) diff --git a/apps/backend/memory/patterns.py b/apps/backend/memory/patterns.py index 208d63f4..2de3c0c1 100644 --- a/apps/backend/memory/patterns.py +++ b/apps/backend/memory/patterns.py @@ -57,9 +57,10 @@ def append_gotcha(spec_dir: Path, gotcha: str) -> None: # Also save to Graphiti if enabled if is_graphiti_memory_enabled(): try: - graphiti = get_graphiti_memory(spec_dir) + graphiti = run_async(get_graphiti_memory(spec_dir)) if graphiti: run_async(graphiti.save_gotcha(gotcha_stripped)) + run_async(graphiti.close()) except Exception as e: logger.warning(f"Graphiti gotcha save failed: {e}") @@ -133,9 +134,10 @@ def append_pattern(spec_dir: Path, pattern: str) -> None: # Also save to Graphiti if enabled if is_graphiti_memory_enabled(): try: - graphiti = get_graphiti_memory(spec_dir) + graphiti = run_async(get_graphiti_memory(spec_dir)) if graphiti: run_async(graphiti.save_pattern(pattern_stripped)) + run_async(graphiti.close()) except Exception as e: logger.warning(f"Graphiti pattern save failed: {e}") diff --git a/apps/backend/scripts/test_memory_save.py b/apps/backend/scripts/test_memory_save.py new file mode 100644 index 00000000..52715671 --- /dev/null +++ b/apps/backend/scripts/test_memory_save.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +""" +Memory Save Verification Script +================================ + +Tests the memory save functionality with Graphiti enabled. +Run with DEBUG=true SENTRY_DEV=true to verify Sentry events. + +Usage: + cd apps/backend + DEBUG=true python scripts/test_memory_save.py + + # With Sentry enabled (for Sentry event verification): + DEBUG=true SENTRY_DEV=true python scripts/test_memory_save.py +""" + +import asyncio +import logging +import os +import sys +import tempfile +from pathlib import Path + +# Add the backend directory to the path so we can import modules +SCRIPT_DIR = Path(__file__).resolve().parent +BACKEND_DIR = SCRIPT_DIR.parent +if str(BACKEND_DIR) not in sys.path: + sys.path.insert(0, str(BACKEND_DIR)) + +# Configure logging +logging.basicConfig( + level=logging.DEBUG if os.environ.get("DEBUG") else logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +async def test_memory_imports(): + """Test that all memory-related imports work correctly.""" + print("\n=== Testing Memory System Imports ===") + + errors = [] + + # Test memory_manager imports + try: + from agents.memory_manager import ( + debug_memory_system_status, + get_graphiti_context, + save_session_memory, + ) + + print("[OK] agents.memory_manager imports successful") + except ImportError as e: + errors.append(f"agents.memory_manager: {e}") + print(f"[FAIL] agents.memory_manager: {e}") + + # Test graphiti_helpers imports + try: + from memory.graphiti_helpers import ( + get_graphiti_memory, + is_graphiti_memory_enabled, + save_to_graphiti_async, + ) + + print("[OK] memory.graphiti_helpers imports successful") + except ImportError as e: + errors.append(f"memory.graphiti_helpers: {e}") + print(f"[FAIL] memory.graphiti_helpers: {e}") + + # Test graphiti_config imports + try: + from graphiti_config import ( + get_graphiti_status, + is_graphiti_enabled, + ) + + print("[OK] graphiti_config imports successful") + except ImportError as e: + errors.append(f"graphiti_config: {e}") + print(f"[FAIL] graphiti_config: {e}") + + # Test sentry imports + try: + from core.sentry import ( + capture_exception, + capture_message, + init_sentry, + ) + from core.sentry import ( + is_enabled as sentry_is_enabled, + ) + + print("[OK] core.sentry imports successful") + except ImportError as e: + errors.append(f"core.sentry: {e}") + print(f"[FAIL] core.sentry: {e}") + + # Test graphiti queries_pkg imports + try: + from integrations.graphiti.queries_pkg.client import GraphitiClient + from integrations.graphiti.queries_pkg.graphiti import GraphitiMemory + from integrations.graphiti.queries_pkg.queries import GraphitiQueries + from integrations.graphiti.queries_pkg.search import GraphitiSearch + + print("[OK] integrations.graphiti.queries_pkg imports successful") + except ImportError as e: + errors.append(f"integrations.graphiti.queries_pkg: {e}") + print(f"[FAIL] integrations.graphiti.queries_pkg: {e}") + + if errors: + print(f"\n[FAIL] {len(errors)} import error(s) found") + return False + else: + print("\n[OK] All imports successful") + return True + + +async def test_graphiti_status(): + """Test Graphiti configuration status.""" + print("\n=== Testing Graphiti Status ===") + + try: + from graphiti_config import get_graphiti_status, is_graphiti_enabled + + enabled = is_graphiti_enabled() + status = get_graphiti_status() + + print(f"Graphiti Enabled: {enabled}") + print(f"Graphiti Available: {status.get('available')}") + print(f" Host: {status.get('host')}") + print(f" Port: {status.get('port')}") + print(f" Database: {status.get('database')}") + print(f" LLM Provider: {status.get('llm_provider')}") + print(f" Embedder Provider: {status.get('embedder_provider')}") + + if not status.get("available"): + print(f" Reason: {status.get('reason')}") + print(f" Errors: {status.get('errors')}") + + return enabled + except Exception as e: + print(f"[FAIL] Error checking Graphiti status: {e}") + return False + + +async def test_sentry_status(): + """Test Sentry configuration status. + + Returns True if: + - Sentry is enabled and ready, OR + - Sentry is properly disabled due to configuration (no DSN, not dev mode, SDK not installed) + + Only returns False if there's an unexpected error. + """ + print("\n=== Testing Sentry Status ===") + + try: + from core.sentry import init_sentry, is_enabled, is_initialized + + # Check if SENTRY_DEV is set + sentry_dev = os.environ.get("SENTRY_DEV", "").lower() in ("true", "1", "yes") + sentry_dsn = os.environ.get("SENTRY_DSN", "") + + print(f"SENTRY_DSN set: {bool(sentry_dsn)}") + print(f"SENTRY_DEV: {sentry_dev}") + + # Initialize Sentry + init_sentry(component="memory-test") + + print(f"Sentry Initialized: {is_initialized()}") + print(f"Sentry Enabled: {is_enabled()}") + + if is_enabled(): + print("[OK] Sentry is enabled and ready to capture events") + else: + # Sentry being disabled is OK - it just means configuration requires it + if not sentry_dsn: + print("[INFO] Sentry disabled - no SENTRY_DSN configured (expected)") + elif not sentry_dev: + print( + "[INFO] Sentry disabled in dev mode - set SENTRY_DEV=true to enable" + ) + else: + print("[INFO] Sentry disabled - sentry-sdk may not be installed") + print( + "[OK] Sentry integration configured correctly (disabled by configuration)" + ) + + # Return True even if disabled - we're testing that the integration works, + # not that Sentry is necessarily enabled + return True + except Exception as e: + print(f"[FAIL] Error checking Sentry status: {e}") + return False + + +async def test_memory_save_flow(): + """Test the memory save flow end-to-end.""" + print("\n=== Testing Memory Save Flow ===") + + # Create temporary directories for testing + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = Path(tmp_dir) + spec_dir = tmp_path / "test_spec" + spec_dir.mkdir(parents=True) + project_dir = tmp_path / "project" + project_dir.mkdir(parents=True) + + print(f"Test spec_dir: {spec_dir}") + print(f"Test project_dir: {project_dir}") + + try: + from agents.memory_manager import save_session_memory + + # Test memory save with sample data + subtask_id = "test-subtask-1" + session_num = 1 + success = True + subtasks_completed = ["test-subtask-1"] + discoveries = { + "files_understood": {"test.py": "Test file for memory verification"}, + "patterns_found": ["Test pattern: Always verify imports"], + "gotchas_encountered": ["Test gotcha: Check async/await usage"], + } + + print("\nSaving test session memory...") + print(f" subtask_id: {subtask_id}") + print(f" session_num: {session_num}") + print(f" success: {success}") + + result, storage_type = await save_session_memory( + spec_dir=spec_dir, + project_dir=project_dir, + subtask_id=subtask_id, + session_num=session_num, + success=success, + subtasks_completed=subtasks_completed, + discoveries=discoveries, + ) + + print("\nMemory Save Result:") + print(f" Success: {result}") + print(f" Storage Type: {storage_type}") + + if result: + print(f"[OK] Memory save succeeded using {storage_type} storage") + + # Verify file was created if file-based + if storage_type == "file": + memory_file = ( + spec_dir + / "memory" + / "session_insights" + / f"session_{session_num:03d}.json" + ) + if memory_file.exists(): + print(f"[OK] Memory file created: {memory_file}") + else: + print(f"[WARN] Memory file not found: {memory_file}") + + return True + else: + print("[FAIL] Memory save failed") + return False + + except Exception as e: + print(f"[FAIL] Error during memory save test: {e}") + import traceback + + traceback.print_exc() + + # Test Sentry capture + try: + from core.sentry import capture_exception, is_enabled + + if is_enabled(): + capture_exception( + e, + operation="test_memory_save", + context="verification_script", + ) + print("[INFO] Exception captured to Sentry") + except Exception: + pass + + return False + + +async def test_sentry_capture(): + """Test that Sentry capture works (only if Sentry is enabled).""" + print("\n=== Testing Sentry Capture ===") + + try: + from core.sentry import ( + capture_exception, + capture_message, + is_enabled, + ) + + if not is_enabled(): + print("[SKIP] Sentry not enabled - skipping capture test") + print(" Set SENTRY_DSN and SENTRY_DEV=true to test Sentry capture") + return True + + # Test capture_message + print("Sending test message to Sentry...") + capture_message( + "Memory save verification script test message", + level="info", + test_type="verification", + component="memory-test", + ) + print("[OK] Test message sent to Sentry") + + # Test capture_exception + print("Sending test exception to Sentry...") + try: + raise ValueError("Test exception for memory save verification") + except Exception as e: + capture_exception( + e, + operation="test_exception", + context="verification_script", + ) + print("[OK] Test exception sent to Sentry") + + print("\n[INFO] Check your Sentry dashboard for the test events") + return True + + except Exception as e: + print(f"[FAIL] Error testing Sentry capture: {e}") + return False + + +async def main(): + """Run all memory save verification tests.""" + print("=" * 60) + print("Memory Save Verification Script") + print("=" * 60) + print(f"DEBUG: {os.environ.get('DEBUG', 'not set')}") + print(f"SENTRY_DEV: {os.environ.get('SENTRY_DEV', 'not set')}") + print(f"GRAPHITI_ENABLED: {os.environ.get('GRAPHITI_ENABLED', 'not set')}") + + results = {} + + # Run tests + results["imports"] = await test_memory_imports() + results["graphiti_status"] = await test_graphiti_status() + results["sentry_status"] = await test_sentry_status() + results["memory_save"] = await test_memory_save_flow() + results["sentry_capture"] = await test_sentry_capture() + + # Summary + print("\n" + "=" * 60) + print("Test Summary") + print("=" * 60) + + passed = 0 + failed = 0 + for test_name, result in results.items(): + status = "[OK]" if result else "[FAIL]" + print(f" {status} {test_name}") + if result: + passed += 1 + else: + failed += 1 + + print(f"\nTotal: {passed} passed, {failed} failed") + + if failed > 0: + print("\n[FAIL] Some tests failed") + sys.exit(1) + else: + print("\n[OK] All tests passed") + sys.exit(0) + + +if __name__ == "__main__": + asyncio.run(main())