Coverage for session_mgmt_mcp/tools/memory_tools.py: 0.00%
234 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-01 05:22 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-01 05:22 -0700
1#!/usr/bin/env python3
2"""Memory and reflection management MCP tools.
4This module provides tools for storing, searching, and managing reflections and conversation memories.
5"""
7from datetime import datetime
9from session_mgmt_mcp.utils.logging import get_session_logger
11logger = get_session_logger()
13# Lazy loading for optional dependencies
14_reflection_db = None
15_reflection_tools_available = None
18async def _get_reflection_database():
19 """Get reflection database instance with lazy loading."""
20 global _reflection_db, _reflection_tools_available
22 if _reflection_tools_available is False:
23 msg = "Reflection tools not available"
24 raise ImportError(msg)
26 if _reflection_db is None:
27 try:
28 from session_mgmt_mcp.reflection_tools import ReflectionDatabase
30 _reflection_db = ReflectionDatabase()
31 _reflection_tools_available = True
32 except ImportError as e:
33 _reflection_tools_available = False
34 msg = f"Reflection tools not available. Install dependencies: {e}"
35 raise ImportError(
36 msg,
37 )
39 return _reflection_db
42def _check_reflection_tools_available() -> bool:
43 """Check if reflection tools are available."""
44 global _reflection_tools_available
46 if _reflection_tools_available is None:
47 try:
48 # Check if reflection_tools module is importable
49 import importlib.util
51 spec = importlib.util.find_spec("session_mgmt_mcp.reflection_tools")
52 _reflection_tools_available = spec is not None
53 except ImportError:
54 _reflection_tools_available = False
56 return _reflection_tools_available
59# Tool implementations
60async def _store_reflection_impl(content: str, tags: list[str] | None = None) -> str:
61 """Implementation for store_reflection tool."""
62 if not _check_reflection_tools_available():
63 return "❌ Reflection tools not available. Install dependencies: uv sync --extra embeddings"
65 try:
66 db = await _get_reflection_database()
67 success = await db.store_reflection(content, tags=tags or [])
69 if success:
70 output = []
71 output.append("💾 Reflection stored successfully!")
72 output.append(
73 f"📝 Content: {content[:100]}{'...' if len(content) > 100 else ''}",
74 )
75 if tags:
76 output.append(f"🏷️ Tags: {', '.join(tags)}")
77 output.append(f"📅 Stored: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
79 logger.info("Reflection stored", content_length=len(content), tags=tags)
80 return "\n".join(output)
81 return "❌ Failed to store reflection"
83 except Exception as e:
84 logger.exception("Error storing reflection", error=str(e))
85 return f"❌ Error storing reflection: {e}"
88async def _quick_search_impl(
89 query: str,
90 min_score: float = 0.7,
91 project: str | None = None,
92) -> str:
93 """Implementation for quick_search tool."""
94 if not _check_reflection_tools_available():
95 return "❌ Reflection tools not available. Install dependencies: uv sync --extra embeddings"
97 try:
98 db = await _get_reflection_database()
99 results = await db.search_reflections(
100 query=query,
101 project=project,
102 limit=1,
103 min_score=min_score,
104 )
106 output = []
107 output.append(f"🔍 Quick search for: '{query}'")
109 if results:
110 result = results[0]
111 output.append("📊 Found results (showing top 1)")
112 output.append(
113 f"📝 {result['content'][:150]}{'...' if len(result['content']) > 150 else ''}",
114 )
115 if result.get("project"):
116 output.append(f"📁 Project: {result['project']}")
117 if result.get("score"):
118 output.append(f"⭐ Relevance: {result['score']:.2f}")
119 output.append(f"📅 Date: {result.get('timestamp', 'Unknown')}")
120 else:
121 output.append("🔍 No results found")
122 output.append("💡 Try adjusting your search terms or lowering min_score")
124 logger.info("Quick search performed", query=query, results_count=len(results))
125 return "\n".join(output)
127 except Exception as e:
128 logger.exception("Error in quick search", error=str(e), query=query)
129 return f"❌ Search error: {e}"
132async def _search_summary_impl(
133 query: str,
134 min_score: float = 0.7,
135 project: str | None = None,
136) -> str:
137 """Implementation for search_summary tool."""
138 if not _check_reflection_tools_available():
139 return "❌ Reflection tools not available. Install dependencies: uv sync --extra embeddings"
141 try:
142 db = await _get_reflection_database()
143 results = await db.search_reflections(
144 query=query,
145 project=project,
146 limit=20,
147 min_score=min_score,
148 )
150 output = []
151 output.append(f"📊 Search Summary for: '{query}'")
152 output.append("=" * 50)
154 if results:
155 output.append(f"📈 Total results: {len(results)}")
157 # Project distribution
158 projects = {}
159 for result in results:
160 proj = result.get("project", "Unknown")
161 projects[proj] = projects.get(proj, 0) + 1
163 if len(projects) > 1:
164 output.append("📁 Project distribution:")
165 for proj, count in sorted(
166 projects.items(),
167 key=lambda x: x[1],
168 reverse=True,
169 ):
170 output.append(f" • {proj}: {count} results")
172 # Time distribution
173 timestamps = [r.get("timestamp") for r in results if r.get("timestamp")]
174 if timestamps:
175 output.append(f"📅 Time range: {len(timestamps)} results with dates")
177 # Average relevance score
178 scores = [r.get("score", 0) for r in results if r.get("score")]
179 if scores:
180 avg_score = sum(scores) / len(scores)
181 output.append(f"⭐ Average relevance: {avg_score:.2f}")
183 # Common themes
184 all_content = " ".join([r["content"] for r in results])
185 words = all_content.lower().split()
186 word_freq = {}
187 for word in words:
188 if len(word) > 4:
189 word_freq[word] = word_freq.get(word, 0) + 1
191 if word_freq:
192 top_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[
193 :5
194 ]
195 output.append("🔤 Common themes:")
196 for word, freq in top_words:
197 output.append(f" • {word}: {freq} mentions")
199 else:
200 output.append("🔍 No results found")
201 output.append(
202 "💡 Try different search terms or lower the min_score threshold",
203 )
205 logger.info("Search summary generated", query=query, results_count=len(results))
206 return "\n".join(output)
208 except Exception as e:
209 logger.exception("Error generating search summary", error=str(e), query=query)
210 return f"❌ Search summary error: {e}"
213async def _search_by_file_impl(
214 file_path: str,
215 limit: int = 10,
216 project: str | None = None,
217) -> str:
218 """Implementation for search_by_file tool."""
219 if not _check_reflection_tools_available():
220 return "❌ Reflection tools not available. Install dependencies: uv sync --extra embeddings"
222 try:
223 db = await _get_reflection_database()
224 results = await db.search_reflections(
225 query=file_path,
226 project=project,
227 limit=limit,
228 )
230 output = []
231 output.append(f"📁 Searching conversations about: {file_path}")
232 output.append("=" * 50)
234 if results:
235 output.append(f"📈 Found {len(results)} relevant conversations:")
237 for i, result in enumerate(results, 1):
238 output.append(
239 f"\n{i}. 📝 {result['content'][:200]}{'...' if len(result['content']) > 200 else ''}",
240 )
241 if result.get("project"):
242 output.append(f" 📁 Project: {result['project']}")
243 if result.get("score"):
244 output.append(f" ⭐ Relevance: {result['score']:.2f}")
245 if result.get("timestamp"):
246 output.append(f" 📅 Date: {result['timestamp']}")
247 else:
248 output.append("🔍 No conversations found about this file")
249 output.append(
250 "💡 The file might not have been discussed in previous sessions",
251 )
253 logger.info(
254 "File search performed",
255 file_path=file_path,
256 results_count=len(results),
257 )
258 return "\n".join(output)
260 except Exception as e:
261 logger.exception("Error searching by file", error=str(e), file_path=file_path)
262 return f"❌ File search error: {e}"
265async def _search_by_concept_impl(
266 concept: str,
267 include_files: bool = True,
268 limit: int = 10,
269 project: str | None = None,
270) -> str:
271 """Implementation for search_by_concept tool."""
272 if not _check_reflection_tools_available():
273 return "❌ Reflection tools not available. Install dependencies: uv sync --extra embeddings"
275 try:
276 db = await _get_reflection_database()
277 results = await db.search_reflections(
278 query=concept,
279 project=project,
280 limit=limit,
281 )
283 output = []
284 output.append(f"🧠 Searching for concept: '{concept}'")
285 output.append("=" * 50)
287 if results:
288 output.append(f"📈 Found {len(results)} related conversations:")
290 for i, result in enumerate(results, 1):
291 output.append(
292 f"\n{i}. 📝 {result['content'][:250]}{'...' if len(result['content']) > 250 else ''}",
293 )
294 if result.get("project"):
295 output.append(f" 📁 Project: {result['project']}")
296 if result.get("score"):
297 output.append(f" ⭐ Relevance: {result['score']:.2f}")
298 if result.get("timestamp"):
299 output.append(f" 📅 Date: {result['timestamp']}")
301 if include_files and result.get("files"):
302 files = result["files"][:3]
303 if files:
304 output.append(f" 📄 Files: {', '.join(files)}")
305 else:
306 output.append("🔍 No conversations found about this concept")
307 output.append("💡 Try related terms or broader concepts")
309 logger.info(
310 "Concept search performed",
311 concept=concept,
312 results_count=len(results),
313 )
314 return "\n".join(output)
316 except Exception as e:
317 logger.exception("Error searching by concept", error=str(e), concept=concept)
318 return f"❌ Concept search error: {e}"
321async def _reflection_stats_impl() -> str:
322 """Implementation for reflection_stats tool."""
323 if not _check_reflection_tools_available():
324 return "❌ Reflection tools not available. Install dependencies: uv sync --extra embeddings"
326 try:
327 db = await _get_reflection_database()
328 stats = await db.get_reflection_stats()
330 output = []
331 output.append("📊 Reflection Database Statistics")
332 output.append("=" * 40)
334 if stats:
335 output.append(f"📈 Total reflections: {stats.get('total_reflections', 0)}")
336 output.append(f"📁 Projects: {stats.get('projects', 0)}")
338 date_range = stats.get("date_range")
339 if date_range:
340 output.append(
341 f"📅 Date range: {date_range.get('start')} to {date_range.get('end')}",
342 )
344 recent_activity = stats.get("recent_activity", [])
345 if recent_activity:
346 output.append("\n🕐 Recent activity:")
347 for activity in recent_activity[:5]:
348 output.append(f" • {activity}")
350 # Database health info
351 output.append(
352 f"\n🏥 Database health: {'✅ Healthy' if stats.get('total_reflections', 0) > 0 else '⚠️ Empty'}",
353 )
355 else:
356 output.append("📊 No statistics available")
357 output.append("💡 Database may be empty or inaccessible")
359 logger.info("Reflection stats retrieved")
360 return "\n".join(output)
362 except Exception as e:
363 logger.exception("Error getting reflection stats", error=str(e))
364 return f"❌ Stats error: {e}"
367async def _reset_reflection_database_impl() -> str:
368 """Implementation for reset_reflection_database tool."""
369 if not _check_reflection_tools_available():
370 return "❌ Reflection tools not available. Install dependencies: uv sync --extra embeddings"
372 try:
373 global _reflection_db
375 # Close existing connection if any
376 if _reflection_db and hasattr(_reflection_db, "conn") and _reflection_db.conn:
377 try:
378 _reflection_db.conn.close()
379 except Exception as e:
380 logger.warning(f"Error closing old connection: {e}")
382 # Reset the global instance
383 _reflection_db = None
385 # Try to create a new connection
386 await _get_reflection_database()
388 output = []
389 output.append("🔄 Reflection database connection reset")
390 output.append("✅ New connection established successfully")
391 output.append("💡 Database locks should be resolved")
393 logger.info("Reflection database reset successfully")
394 return "\n".join(output)
396 except Exception as e:
397 logger.exception("Error resetting reflection database", error=str(e))
398 return f"❌ Reset error: {e}"
401def register_memory_tools(mcp_server) -> None:
402 """Register all memory management tools with the MCP server."""
404 @mcp_server.tool()
405 async def store_reflection(content: str, tags: list[str] | None = None) -> str:
406 """Store an important insight or reflection for future reference."""
407 return await _store_reflection_impl(content, tags)
409 @mcp_server.tool()
410 async def quick_search(
411 query: str,
412 min_score: float = 0.7,
413 project: str | None = None,
414 ) -> str:
415 """Quick search that returns only the count and top result for fast overview."""
416 return await _quick_search_impl(query, min_score, project)
418 @mcp_server.tool()
419 async def search_summary(
420 query: str,
421 min_score: float = 0.7,
422 project: str | None = None,
423 ) -> str:
424 """Get aggregated insights from search results without individual result details."""
425 return await _search_summary_impl(query, min_score, project)
427 @mcp_server.tool()
428 async def search_by_file(
429 file_path: str,
430 limit: int = 10,
431 project: str | None = None,
432 ) -> str:
433 """Search for conversations that analyzed a specific file."""
434 return await _search_by_file_impl(file_path, limit, project)
436 @mcp_server.tool()
437 async def search_by_concept(
438 concept: str,
439 include_files: bool = True,
440 limit: int = 10,
441 project: str | None = None,
442 ) -> str:
443 """Search for conversations about a specific development concept."""
444 return await _search_by_concept_impl(concept, include_files, limit, project)
446 @mcp_server.tool()
447 async def reflection_stats() -> str:
448 """Get statistics about the reflection database."""
449 return await _reflection_stats_impl()
451 @mcp_server.tool()
452 async def reset_reflection_database() -> str:
453 """Reset the reflection database connection to fix lock issues."""
454 return await _reset_reflection_database_impl()