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

1#!/usr/bin/env python3 

2"""Memory and reflection management MCP tools. 

3 

4This module provides tools for storing, searching, and managing reflections and conversation memories. 

5""" 

6 

7from datetime import datetime 

8 

9from session_mgmt_mcp.utils.logging import get_session_logger 

10 

11logger = get_session_logger() 

12 

13# Lazy loading for optional dependencies 

14_reflection_db = None 

15_reflection_tools_available = None 

16 

17 

18async def _get_reflection_database(): 

19 """Get reflection database instance with lazy loading.""" 

20 global _reflection_db, _reflection_tools_available 

21 

22 if _reflection_tools_available is False: 

23 msg = "Reflection tools not available" 

24 raise ImportError(msg) 

25 

26 if _reflection_db is None: 

27 try: 

28 from session_mgmt_mcp.reflection_tools import ReflectionDatabase 

29 

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 ) 

38 

39 return _reflection_db 

40 

41 

42def _check_reflection_tools_available() -> bool: 

43 """Check if reflection tools are available.""" 

44 global _reflection_tools_available 

45 

46 if _reflection_tools_available is None: 

47 try: 

48 # Check if reflection_tools module is importable 

49 import importlib.util 

50 

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 

55 

56 return _reflection_tools_available 

57 

58 

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" 

64 

65 try: 

66 db = await _get_reflection_database() 

67 success = await db.store_reflection(content, tags=tags or []) 

68 

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')}") 

78 

79 logger.info("Reflection stored", content_length=len(content), tags=tags) 

80 return "\n".join(output) 

81 return "❌ Failed to store reflection" 

82 

83 except Exception as e: 

84 logger.exception("Error storing reflection", error=str(e)) 

85 return f"❌ Error storing reflection: {e}" 

86 

87 

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" 

96 

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 ) 

105 

106 output = [] 

107 output.append(f"🔍 Quick search for: '{query}'") 

108 

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") 

123 

124 logger.info("Quick search performed", query=query, results_count=len(results)) 

125 return "\n".join(output) 

126 

127 except Exception as e: 

128 logger.exception("Error in quick search", error=str(e), query=query) 

129 return f"❌ Search error: {e}" 

130 

131 

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" 

140 

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 ) 

149 

150 output = [] 

151 output.append(f"📊 Search Summary for: '{query}'") 

152 output.append("=" * 50) 

153 

154 if results: 

155 output.append(f"📈 Total results: {len(results)}") 

156 

157 # Project distribution 

158 projects = {} 

159 for result in results: 

160 proj = result.get("project", "Unknown") 

161 projects[proj] = projects.get(proj, 0) + 1 

162 

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") 

171 

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") 

176 

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}") 

182 

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 

190 

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") 

198 

199 else: 

200 output.append("🔍 No results found") 

201 output.append( 

202 "💡 Try different search terms or lower the min_score threshold", 

203 ) 

204 

205 logger.info("Search summary generated", query=query, results_count=len(results)) 

206 return "\n".join(output) 

207 

208 except Exception as e: 

209 logger.exception("Error generating search summary", error=str(e), query=query) 

210 return f"❌ Search summary error: {e}" 

211 

212 

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" 

221 

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 ) 

229 

230 output = [] 

231 output.append(f"📁 Searching conversations about: {file_path}") 

232 output.append("=" * 50) 

233 

234 if results: 

235 output.append(f"📈 Found {len(results)} relevant conversations:") 

236 

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 ) 

252 

253 logger.info( 

254 "File search performed", 

255 file_path=file_path, 

256 results_count=len(results), 

257 ) 

258 return "\n".join(output) 

259 

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}" 

263 

264 

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" 

274 

275 try: 

276 db = await _get_reflection_database() 

277 results = await db.search_reflections( 

278 query=concept, 

279 project=project, 

280 limit=limit, 

281 ) 

282 

283 output = [] 

284 output.append(f"🧠 Searching for concept: '{concept}'") 

285 output.append("=" * 50) 

286 

287 if results: 

288 output.append(f"📈 Found {len(results)} related conversations:") 

289 

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']}") 

300 

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") 

308 

309 logger.info( 

310 "Concept search performed", 

311 concept=concept, 

312 results_count=len(results), 

313 ) 

314 return "\n".join(output) 

315 

316 except Exception as e: 

317 logger.exception("Error searching by concept", error=str(e), concept=concept) 

318 return f"❌ Concept search error: {e}" 

319 

320 

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" 

325 

326 try: 

327 db = await _get_reflection_database() 

328 stats = await db.get_reflection_stats() 

329 

330 output = [] 

331 output.append("📊 Reflection Database Statistics") 

332 output.append("=" * 40) 

333 

334 if stats: 

335 output.append(f"📈 Total reflections: {stats.get('total_reflections', 0)}") 

336 output.append(f"📁 Projects: {stats.get('projects', 0)}") 

337 

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 ) 

343 

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}") 

349 

350 # Database health info 

351 output.append( 

352 f"\n🏥 Database health: {'✅ Healthy' if stats.get('total_reflections', 0) > 0 else '⚠️ Empty'}", 

353 ) 

354 

355 else: 

356 output.append("📊 No statistics available") 

357 output.append("💡 Database may be empty or inaccessible") 

358 

359 logger.info("Reflection stats retrieved") 

360 return "\n".join(output) 

361 

362 except Exception as e: 

363 logger.exception("Error getting reflection stats", error=str(e)) 

364 return f"❌ Stats error: {e}" 

365 

366 

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" 

371 

372 try: 

373 global _reflection_db 

374 

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}") 

381 

382 # Reset the global instance 

383 _reflection_db = None 

384 

385 # Try to create a new connection 

386 await _get_reflection_database() 

387 

388 output = [] 

389 output.append("🔄 Reflection database connection reset") 

390 output.append("✅ New connection established successfully") 

391 output.append("💡 Database locks should be resolved") 

392 

393 logger.info("Reflection database reset successfully") 

394 return "\n".join(output) 

395 

396 except Exception as e: 

397 logger.exception("Error resetting reflection database", error=str(e)) 

398 return f"❌ Reset error: {e}" 

399 

400 

401def register_memory_tools(mcp_server) -> None: 

402 """Register all memory management tools with the MCP server.""" 

403 

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) 

408 

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) 

417 

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) 

426 

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) 

435 

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) 

445 

446 @mcp_server.tool() 

447 async def reflection_stats() -> str: 

448 """Get statistics about the reflection database.""" 

449 return await _reflection_stats_impl() 

450 

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()