claude-memory-agent 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +206 -206
  2. package/agent_card.py +186 -0
  3. package/bin/cli.js +327 -185
  4. package/bin/lib/banner.js +39 -0
  5. package/bin/lib/environment.js +166 -0
  6. package/bin/lib/installer.js +291 -0
  7. package/bin/lib/models.js +95 -0
  8. package/bin/lib/steps/advanced.js +101 -0
  9. package/bin/lib/steps/confirm.js +87 -0
  10. package/bin/lib/steps/model.js +57 -0
  11. package/bin/lib/steps/provider.js +65 -0
  12. package/bin/lib/steps/scope.js +59 -0
  13. package/bin/lib/steps/server.js +74 -0
  14. package/bin/lib/ui.js +75 -0
  15. package/bin/onboarding.js +164 -0
  16. package/bin/postinstall.js +35 -270
  17. package/config.py +103 -4
  18. package/dashboard.html +4902 -2689
  19. package/hooks/extract_memories.py +439 -0
  20. package/hooks/grounding-hook.py +422 -348
  21. package/hooks/pre_compact_hook.py +76 -0
  22. package/hooks/session_end.py +293 -192
  23. package/hooks/session_end_hook.py +149 -0
  24. package/hooks/session_start.py +227 -227
  25. package/hooks/stop_hook.py +372 -0
  26. package/install.py +972 -902
  27. package/main.py +5240 -2859
  28. package/mcp_server.py +451 -0
  29. package/package.json +58 -47
  30. package/requirements.txt +12 -8
  31. package/services/__init__.py +50 -50
  32. package/services/adaptive_ranker.py +272 -0
  33. package/services/agent_catalog.json +153 -0
  34. package/services/agent_registry.py +245 -730
  35. package/services/claude_md_sync.py +320 -4
  36. package/services/consolidation.py +417 -0
  37. package/services/curator.py +1606 -0
  38. package/services/database.py +4118 -2485
  39. package/services/embedding_pipeline.py +262 -0
  40. package/services/embeddings.py +493 -85
  41. package/services/memory_decay.py +408 -0
  42. package/services/native_memory_paths.py +86 -0
  43. package/services/native_memory_sync.py +496 -0
  44. package/services/response_manager.py +183 -0
  45. package/services/terminal_ui.py +199 -0
  46. package/services/tier_manager.py +235 -0
  47. package/services/websocket.py +26 -6
  48. package/skills/__init__.py +21 -1
  49. package/skills/confidence_tracker.py +441 -0
  50. package/skills/context.py +675 -0
  51. package/skills/curator.py +348 -0
  52. package/skills/search.py +444 -213
  53. package/skills/session_review.py +605 -0
  54. package/skills/store.py +484 -179
  55. package/terminal_dashboard.py +474 -0
  56. package/update_system.py +829 -817
  57. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  58. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  59. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  60. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  61. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  62. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  63. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  64. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  65. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  66. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  67. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  68. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  69. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  70. package/services/__pycache__/database.cpython-312.pyc +0 -0
  71. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  72. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  73. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  74. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  75. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  76. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  77. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  78. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  81. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  82. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  83. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  84. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  85. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  86. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  87. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  88. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  89. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  90. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  91. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  92. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  93. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  94. package/test_automation.py +0 -221
  95. package/test_complete.py +0 -338
  96. package/test_full.py +0 -322
  97. package/verify_db.py +0 -134
package/skills/search.py CHANGED
@@ -1,213 +1,444 @@
1
- """Semantic search skill with context filtering and fallback support."""
2
- from typing import Dict, Any, Optional, List
3
- from services.database import DatabaseService
4
- from services.embeddings import EmbeddingService
5
-
6
-
7
- async def semantic_search(
8
- db: DatabaseService,
9
- embeddings: EmbeddingService,
10
- query: str,
11
- limit: int = 10,
12
- memory_type: Optional[str] = None,
13
- session_id: Optional[str] = None,
14
- project_path: Optional[str] = None,
15
- agent_type: Optional[str] = None,
16
- success_only: bool = False,
17
- threshold: float = 0.5
18
- ) -> Dict[str, Any]:
19
- """
20
- Search memories using semantic similarity with context filters.
21
-
22
- Includes automatic fallback to keyword search when Ollama is unavailable.
23
-
24
- Args:
25
- db: Database service instance
26
- embeddings: Embedding service instance
27
- query: Search query text
28
- limit: Maximum number of results
29
- memory_type: Filter by type (session, decision, code, chunk, error)
30
- session_id: Filter by session ID
31
- project_path: Filter by project
32
- agent_type: Filter by agent that created the memory
33
- success_only: Only return memories marked as successful
34
- threshold: Minimum similarity threshold (0-1)
35
-
36
- Returns:
37
- Dict with search results ranked by similarity * importance
38
- """
39
- # Generate embedding for the query (may return None if Ollama unavailable)
40
- query_embedding = await embeddings.generate_embedding(query)
41
-
42
- # Determine search method based on embedding availability
43
- search_method = "semantic"
44
- results = []
45
-
46
- if query_embedding is not None:
47
- # Use semantic search with embeddings
48
- results = await db.search_similar(
49
- embedding=query_embedding,
50
- limit=limit,
51
- memory_type=memory_type,
52
- session_id=session_id,
53
- project_path=project_path,
54
- agent_type=agent_type,
55
- success_only=success_only,
56
- threshold=threshold
57
- )
58
- else:
59
- # Fallback to keyword search
60
- search_method = "keyword"
61
- results = await db.keyword_search(
62
- query=query,
63
- limit=limit,
64
- memory_type=memory_type,
65
- session_id=session_id,
66
- project_path=project_path,
67
- agent_type=agent_type,
68
- success_only=success_only
69
- )
70
-
71
- return {
72
- "success": True,
73
- "query": query,
74
- "results": results,
75
- "count": len(results),
76
- "search_method": search_method,
77
- "degraded_mode": embeddings.is_degraded(),
78
- "filters": {
79
- "type": memory_type,
80
- "project": project_path,
81
- "agent": agent_type,
82
- "success_only": success_only
83
- },
84
- "threshold": threshold if search_method == "semantic" else None
85
- }
86
-
87
-
88
- async def search_patterns(
89
- db: DatabaseService,
90
- embeddings: EmbeddingService,
91
- query: str,
92
- limit: int = 5,
93
- problem_type: Optional[str] = None,
94
- threshold: float = 0.5
95
- ) -> Dict[str, Any]:
96
- """
97
- Search for reusable solution patterns.
98
-
99
- Includes fallback to keyword search when Ollama is unavailable.
100
-
101
- Args:
102
- db: Database service instance
103
- embeddings: Embedding service instance
104
- query: Problem description or search query
105
- limit: Maximum number of results
106
- problem_type: Filter by problem type
107
- threshold: Minimum similarity threshold
108
-
109
- Returns:
110
- Dict with patterns ranked by similarity * success_rate
111
- """
112
- query_embedding = await embeddings.generate_embedding(query)
113
-
114
- search_method = "semantic"
115
- results = []
116
-
117
- if query_embedding is not None:
118
- results = await db.search_patterns(
119
- embedding=query_embedding,
120
- limit=limit,
121
- problem_type=problem_type,
122
- threshold=threshold
123
- )
124
- else:
125
- # Fallback: keyword search on patterns table
126
- search_method = "keyword"
127
- results = await db.keyword_search_patterns(
128
- query=query,
129
- limit=limit,
130
- problem_type=problem_type
131
- )
132
-
133
- return {
134
- "success": True,
135
- "query": query,
136
- "patterns": results,
137
- "count": len(results),
138
- "search_method": search_method,
139
- "degraded_mode": embeddings.is_degraded(),
140
- "problem_type": problem_type
141
- }
142
-
143
-
144
- async def get_project_context(
145
- db: DatabaseService,
146
- embeddings: EmbeddingService,
147
- project_path: str,
148
- query: Optional[str] = None,
149
- limit: int = 10
150
- ) -> Dict[str, Any]:
151
- """
152
- Get all relevant context for a project.
153
-
154
- Includes fallback to keyword search when Ollama is unavailable.
155
-
156
- Args:
157
- db: Database service instance
158
- embeddings: Embedding service instance
159
- project_path: Path to the project
160
- query: Optional query to filter relevant memories
161
- limit: Max memories to return
162
-
163
- Returns:
164
- Dict with project info and relevant memories
165
- """
166
- # Get project info
167
- project = await db.get_project(project_path)
168
-
169
- # Get recent decisions for this project
170
- decisions = await db.get_memories_by_type(
171
- memory_type="decision",
172
- project_path=project_path,
173
- limit=limit
174
- )
175
-
176
- # Get patterns used in this project
177
- patterns = await db.get_memories_by_type(
178
- memory_type="code",
179
- project_path=project_path,
180
- limit=limit
181
- )
182
-
183
- # If query provided, search for relevant memories
184
- relevant = []
185
- search_method = None
186
- if query:
187
- query_embedding = await embeddings.generate_embedding(query)
188
- if query_embedding is not None:
189
- search_method = "semantic"
190
- relevant = await db.search_similar(
191
- embedding=query_embedding,
192
- project_path=project_path,
193
- limit=limit,
194
- threshold=0.4
195
- )
196
- else:
197
- # Fallback to keyword search
198
- search_method = "keyword"
199
- relevant = await db.keyword_search(
200
- query=query,
201
- project_path=project_path,
202
- limit=limit
203
- )
204
-
205
- return {
206
- "success": True,
207
- "project": project,
208
- "decisions": decisions,
209
- "code_patterns": patterns,
210
- "relevant_to_query": relevant if query else None,
211
- "search_method": search_method,
212
- "degraded_mode": embeddings.is_degraded()
213
- }
1
+ """Semantic search skill with context filtering and fallback support."""
2
+ import logging
3
+ from typing import Dict, Any, Optional, List
4
+ from services.database import DatabaseService
5
+ from services.embeddings import EmbeddingService
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ async def _enrich_with_graph_context(db: DatabaseService, results: list, max_related_items: int = 2) -> list:
11
+ """
12
+ Enrich search results with relationship context using batch queries.
13
+
14
+ Uses batch DB queries (IN clauses) instead of per-result sequential queries,
15
+ providing 5-10x speedup on the enrichment phase for typical 10-result sets.
16
+
17
+ Args:
18
+ db: Database service instance
19
+ results: List of search results to enrich
20
+ max_related_items: Max related items per relationship type (caps response size)
21
+
22
+ Returns:
23
+ List of enriched results with graph context added
24
+ """
25
+ import asyncio
26
+
27
+ if not results:
28
+ return results
29
+
30
+ # Collect all memory IDs that need enrichment
31
+ all_ids = [r.get('id') for r in results if r.get('id')]
32
+ if not all_ids:
33
+ return results
34
+
35
+ # Categorize IDs by type for targeted batch queries
36
+ error_ids = [r['id'] for r in results if r.get('id') and r.get('type') == 'error']
37
+ decision_ids = [r['id'] for r in results if r.get('id') and r.get('type') == 'decision']
38
+ causal_ids = [r['id'] for r in results if r.get('id') and r.get('type') in ('error', 'decision', 'code')]
39
+
40
+ # Run all batch queries in parallel
41
+ batch_tasks = {}
42
+
43
+ # Batch: fixes for errors (incoming 'fixes' relationships)
44
+ if error_ids:
45
+ batch_tasks['fixes'] = db.get_related_memories_batch(error_ids, 'fixes', direction='incoming')
46
+
47
+ # Batch: supports for decisions (incoming 'supports' relationships)
48
+ if decision_ids:
49
+ batch_tasks['supports'] = db.get_related_memories_batch(decision_ids, 'supports', direction='incoming')
50
+ batch_tasks['caused'] = db.get_related_memories_batch(decision_ids, 'caused_by', direction='outgoing')
51
+
52
+ # Batch: contradictions for all results
53
+ batch_tasks['contradictions'] = db.find_contradictions_batch(all_ids)
54
+
55
+ # Causal chains still need per-ID traversal (recursive), but run in parallel
56
+ causal_futures = {}
57
+ for mid in causal_ids:
58
+ causal_futures[mid] = db.get_causal_chain(mid, max_depth=3)
59
+
60
+ # Gather all batch results
61
+ batch_results = {}
62
+ try:
63
+ if batch_tasks:
64
+ keys = list(batch_tasks.keys())
65
+ values = await asyncio.gather(*batch_tasks.values(), return_exceptions=True)
66
+ for k, v in zip(keys, values):
67
+ batch_results[k] = v if not isinstance(v, Exception) else {}
68
+ except Exception as e:
69
+ logger.warning(f"Batch graph enrichment failed: {e}")
70
+ batch_results = {}
71
+
72
+ # Gather causal chain results in parallel
73
+ causal_results = {}
74
+ if causal_futures:
75
+ try:
76
+ keys = list(causal_futures.keys())
77
+ values = await asyncio.gather(*causal_futures.values(), return_exceptions=True)
78
+ for k, v in zip(keys, values):
79
+ causal_results[k] = v if not isinstance(v, Exception) else None
80
+ except Exception as e:
81
+ logger.debug(f"Causal chain batch failed: {e}")
82
+
83
+ # Assemble enriched results
84
+ enriched = []
85
+ fixes_map = batch_results.get('fixes', {})
86
+ supports_map = batch_results.get('supports', {})
87
+ caused_map = batch_results.get('caused', {})
88
+ contradictions_map = batch_results.get('contradictions', {})
89
+
90
+ for result in results:
91
+ memory_id = result.get('id')
92
+ if not memory_id:
93
+ enriched.append(result)
94
+ continue
95
+
96
+ enriched_result = dict(result)
97
+ memory_type = result.get('type', '')
98
+
99
+ # Errors: known fixes
100
+ if memory_type == 'error' and memory_id in fixes_map:
101
+ fixes = fixes_map[memory_id]
102
+ if fixes:
103
+ enriched_result['known_fixes'] = [
104
+ {'id': f['id'], 'content': f['content'][:200], 'outcome': f.get('outcome')}
105
+ for f in fixes[:max_related_items]
106
+ ]
107
+
108
+ # Decisions: rationale and consequences
109
+ if memory_type == 'decision':
110
+ supports = supports_map.get(memory_id, [])
111
+ if supports:
112
+ enriched_result['rationale'] = [
113
+ {'id': s['id'], 'content': s['content'][:200]}
114
+ for s in supports[:max_related_items]
115
+ ]
116
+ caused = caused_map.get(memory_id, [])
117
+ if caused:
118
+ enriched_result['consequences'] = [
119
+ {'id': c['id'], 'content': c['content'][:200]}
120
+ for c in caused[:max_related_items]
121
+ ]
122
+
123
+ # All types: contradictions
124
+ contradictions = contradictions_map.get(memory_id, [])
125
+ if contradictions:
126
+ enriched_result['contradictions'] = [
127
+ {'id': c['id'], 'content': c['content'][:200]}
128
+ for c in contradictions[:max_related_items]
129
+ ]
130
+
131
+ # Causal chains
132
+ if memory_type in ('error', 'decision', 'code') and memory_id in causal_results:
133
+ chain = causal_results[memory_id]
134
+ if chain and (chain.get('causes') or chain.get('fixes') or chain.get('root_causes')):
135
+ enriched_result['causal_chain'] = chain
136
+
137
+ enriched.append(enriched_result)
138
+
139
+ return enriched
140
+
141
+
142
+ async def semantic_search(
143
+ db: DatabaseService,
144
+ embeddings: EmbeddingService,
145
+ query: str,
146
+ limit: int = 10,
147
+ memory_type: Optional[str] = None,
148
+ session_id: Optional[str] = None,
149
+ project_path: Optional[str] = None,
150
+ agent_type: Optional[str] = None,
151
+ success_only: bool = False,
152
+ threshold: float = 0.5,
153
+ # Outcome spectrum filters
154
+ include_failed: bool = False,
155
+ include_superseded: bool = False,
156
+ include_unreliable: bool = False,
157
+ outcome_status: Optional[str] = None,
158
+ # Context-aware search
159
+ current_context: Optional[Dict[str, Any]] = None,
160
+ auto_detect_context: bool = True,
161
+ # Graph enrichment
162
+ include_graph: bool = True,
163
+ # Adaptive ranking
164
+ temperature: Optional[float] = None
165
+ ) -> Dict[str, Any]:
166
+ """
167
+ Search memories using semantic similarity with context filters.
168
+
169
+ Includes automatic fallback to keyword search when Ollama is unavailable.
170
+
171
+ Outcome-aware search behavior:
172
+ - 'success' memories rank highest (1.5x boost)
173
+ - 'partial' memories shown with warning (1.0x - no penalty)
174
+ - 'failed' memories excluded by default (use include_failed=True to show)
175
+ - 'superseded' memories excluded and replaced with their superseding memory
176
+ - 'pending' memories shown normally (1.0x)
177
+ - Unreliable memories (failure_count >= 3) excluded by default (use include_unreliable=True)
178
+
179
+ Context-aware search:
180
+ - If current_context provided, memories that worked in similar contexts get +0.2 boost
181
+ - Memories that failed in similar contexts get -0.2 penalty
182
+ - If auto_detect_context=True and project_path provided, context is auto-detected
183
+
184
+ Args:
185
+ db: Database service instance
186
+ embeddings: Embedding service instance
187
+ query: Search query text
188
+ limit: Maximum number of results
189
+ memory_type: Filter by type (session, decision, code, chunk, error)
190
+ session_id: Filter by session ID
191
+ project_path: Filter by project
192
+ agent_type: Filter by agent that created the memory
193
+ success_only: Only return memories marked as successful (legacy)
194
+ threshold: Minimum similarity threshold (0-1)
195
+ include_failed: Include memories with outcome_status='failed' (default False)
196
+ include_superseded: Include memories with outcome_status='superseded' (default False)
197
+ include_unreliable: Include memories with failure_count >= 3 (default False)
198
+ outcome_status: Filter by specific outcome status
199
+ current_context: Context dict with project_type, tech_stack, file_patterns
200
+ auto_detect_context: If True and project_path provided, auto-detect context
201
+ include_graph: Enrich results with graph context (fixes, rationale, contradictions)
202
+
203
+ Returns:
204
+ Dict with search results ranked by: (similarity * 0.7) + (confidence * 0.3) + context_adjustment
205
+ Each result includes outcome_status, outcome_warning, outcome_boost, context_adjustment,
206
+ and context_recommendation fields.
207
+ """
208
+ # Auto-detect context from project_path if enabled
209
+ detected_context = None
210
+ if auto_detect_context and project_path and not current_context:
211
+ try:
212
+ from skills.context import detect_project_context
213
+ detected_context = detect_project_context(project_path)
214
+ except Exception:
215
+ pass
216
+
217
+ # Use provided context or detected context
218
+ search_context = current_context or detected_context
219
+
220
+ # Generate embedding for the query (may return None if Ollama unavailable)
221
+ query_embedding = await embeddings.generate_embedding(query)
222
+
223
+ # Determine search method based on embedding availability
224
+ search_method = "semantic"
225
+ results = []
226
+
227
+ if query_embedding is not None:
228
+ # Use semantic search with embeddings
229
+ results = await db.search_similar(
230
+ embedding=query_embedding,
231
+ limit=limit,
232
+ memory_type=memory_type,
233
+ session_id=session_id,
234
+ project_path=project_path,
235
+ agent_type=agent_type,
236
+ success_only=success_only,
237
+ threshold=threshold,
238
+ include_failed=include_failed,
239
+ include_superseded=include_superseded,
240
+ include_unreliable=include_unreliable,
241
+ outcome_status=outcome_status,
242
+ current_context=search_context,
243
+ query_text=query,
244
+ temperature=temperature
245
+ )
246
+ else:
247
+ # Fallback to keyword search
248
+ search_method = "keyword"
249
+ results = await db.keyword_search(
250
+ query=query,
251
+ limit=limit,
252
+ memory_type=memory_type,
253
+ session_id=session_id,
254
+ project_path=project_path,
255
+ agent_type=agent_type,
256
+ success_only=success_only,
257
+ include_failed=include_failed,
258
+ include_superseded=include_superseded,
259
+ include_unreliable=include_unreliable,
260
+ outcome_status=outcome_status
261
+ )
262
+
263
+ # Enrich with graph context if requested
264
+ if include_graph:
265
+ try:
266
+ results = await _enrich_with_graph_context(db, results)
267
+ except Exception as e:
268
+ logger.warning(f"Failed to enrich with graph context: {e}")
269
+ # Continue with unenriched results
270
+
271
+ # Promote accessed memories if their tier should be higher (fire-and-forget)
272
+ if results:
273
+ try:
274
+ from services.tier_manager import TierManager
275
+ tier_mgr = TierManager(db)
276
+ for r in results:
277
+ mem_id = r.get('id')
278
+ if mem_id:
279
+ await tier_mgr.promote_on_access(mem_id)
280
+ except Exception as e:
281
+ logger.debug(f"Tier promotion on access failed (non-fatal): {e}")
282
+
283
+ return {
284
+ "success": True,
285
+ "query": query,
286
+ "results": results,
287
+ "count": len(results),
288
+ "search_method": search_method,
289
+ "degraded_mode": embeddings.is_degraded(),
290
+ "filters": {
291
+ "type": memory_type,
292
+ "project": project_path,
293
+ "agent": agent_type,
294
+ "success_only": success_only,
295
+ "include_failed": include_failed,
296
+ "include_superseded": include_superseded,
297
+ "include_unreliable": include_unreliable,
298
+ "outcome_status": outcome_status,
299
+ "include_graph": include_graph
300
+ },
301
+ "context_aware": search_context is not None,
302
+ "detected_context": detected_context,
303
+ "threshold": threshold if search_method == "semantic" else None,
304
+ "temperature": temperature
305
+ }
306
+
307
+
308
+ async def search_patterns(
309
+ db: DatabaseService,
310
+ embeddings: EmbeddingService,
311
+ query: str,
312
+ limit: int = 5,
313
+ problem_type: Optional[str] = None,
314
+ threshold: float = 0.5
315
+ ) -> Dict[str, Any]:
316
+ """
317
+ Search for reusable solution patterns.
318
+
319
+ Includes fallback to keyword search when Ollama is unavailable.
320
+
321
+ Args:
322
+ db: Database service instance
323
+ embeddings: Embedding service instance
324
+ query: Problem description or search query
325
+ limit: Maximum number of results
326
+ problem_type: Filter by problem type
327
+ threshold: Minimum similarity threshold
328
+
329
+ Returns:
330
+ Dict with patterns ranked by similarity * success_rate
331
+ """
332
+ query_embedding = await embeddings.generate_embedding(query)
333
+
334
+ search_method = "semantic"
335
+ results = []
336
+
337
+ if query_embedding is not None:
338
+ results = await db.search_patterns(
339
+ embedding=query_embedding,
340
+ limit=limit,
341
+ problem_type=problem_type,
342
+ threshold=threshold
343
+ )
344
+ else:
345
+ # Fallback: keyword search on patterns table
346
+ search_method = "keyword"
347
+ results = await db.keyword_search_patterns(
348
+ query=query,
349
+ limit=limit,
350
+ problem_type=problem_type
351
+ )
352
+
353
+ return {
354
+ "success": True,
355
+ "query": query,
356
+ "patterns": results,
357
+ "count": len(results),
358
+ "search_method": search_method,
359
+ "degraded_mode": embeddings.is_degraded(),
360
+ "problem_type": problem_type
361
+ }
362
+
363
+
364
+ async def get_project_context(
365
+ db: DatabaseService,
366
+ embeddings: EmbeddingService,
367
+ project_path: str,
368
+ query: Optional[str] = None,
369
+ limit: int = 5
370
+ ) -> Dict[str, Any]:
371
+ """
372
+ Get all relevant context for a project.
373
+
374
+ Includes fallback to keyword search when Ollama is unavailable.
375
+
376
+ Args:
377
+ db: Database service instance
378
+ embeddings: Embedding service instance
379
+ project_path: Path to the project
380
+ query: Optional query to filter relevant memories
381
+ limit: Max memories to return (default reduced to 5 for response size)
382
+
383
+ Returns:
384
+ Dict with project info and relevant memories
385
+ """
386
+ # Get project info
387
+ project = await db.get_project(project_path)
388
+
389
+ # Get recent decisions for this project
390
+ decisions = await db.get_memories_by_type(
391
+ memory_type="decision",
392
+ project_path=project_path,
393
+ limit=limit
394
+ )
395
+
396
+ # Get patterns used in this project
397
+ patterns = await db.get_memories_by_type(
398
+ memory_type="code",
399
+ project_path=project_path,
400
+ limit=limit
401
+ )
402
+
403
+ # If query provided, search for relevant memories
404
+ relevant = []
405
+ search_method = None
406
+ if query:
407
+ query_embedding = await embeddings.generate_embedding(query)
408
+ if query_embedding is not None:
409
+ search_method = "semantic"
410
+ relevant = await db.search_similar(
411
+ embedding=query_embedding,
412
+ project_path=project_path,
413
+ limit=limit,
414
+ threshold=0.4
415
+ )
416
+ else:
417
+ # Fallback to keyword search
418
+ search_method = "keyword"
419
+ relevant = await db.keyword_search(
420
+ query=query,
421
+ project_path=project_path,
422
+ limit=limit
423
+ )
424
+
425
+ # Enrich all result lists with graph context (relationships, contradictions, causal chains)
426
+ try:
427
+ if decisions:
428
+ decisions = await _enrich_with_graph_context(db, decisions)
429
+ if patterns:
430
+ patterns = await _enrich_with_graph_context(db, patterns)
431
+ if relevant:
432
+ relevant = await _enrich_with_graph_context(db, relevant)
433
+ except Exception as e:
434
+ logger.warning(f"Failed to enrich project context with graph: {e}")
435
+
436
+ return {
437
+ "success": True,
438
+ "project": project,
439
+ "decisions": decisions,
440
+ "code_patterns": patterns,
441
+ "relevant_to_query": relevant if query else None,
442
+ "search_method": search_method,
443
+ "degraded_mode": embeddings.is_degraded()
444
+ }