memorygraphMCP 0.11.7__py3-none-any.whl

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 (65) hide show
  1. memorygraph/__init__.py +50 -0
  2. memorygraph/__main__.py +12 -0
  3. memorygraph/advanced_tools.py +509 -0
  4. memorygraph/analytics/__init__.py +46 -0
  5. memorygraph/analytics/advanced_queries.py +727 -0
  6. memorygraph/backends/__init__.py +21 -0
  7. memorygraph/backends/base.py +179 -0
  8. memorygraph/backends/cloud.py +75 -0
  9. memorygraph/backends/cloud_backend.py +858 -0
  10. memorygraph/backends/factory.py +577 -0
  11. memorygraph/backends/falkordb_backend.py +749 -0
  12. memorygraph/backends/falkordblite_backend.py +746 -0
  13. memorygraph/backends/ladybugdb_backend.py +242 -0
  14. memorygraph/backends/memgraph_backend.py +327 -0
  15. memorygraph/backends/neo4j_backend.py +298 -0
  16. memorygraph/backends/sqlite_fallback.py +463 -0
  17. memorygraph/backends/turso.py +448 -0
  18. memorygraph/cli.py +743 -0
  19. memorygraph/cloud_database.py +297 -0
  20. memorygraph/config.py +295 -0
  21. memorygraph/database.py +933 -0
  22. memorygraph/graph_analytics.py +631 -0
  23. memorygraph/integration/__init__.py +69 -0
  24. memorygraph/integration/context_capture.py +426 -0
  25. memorygraph/integration/project_analysis.py +583 -0
  26. memorygraph/integration/workflow_tracking.py +492 -0
  27. memorygraph/intelligence/__init__.py +59 -0
  28. memorygraph/intelligence/context_retrieval.py +447 -0
  29. memorygraph/intelligence/entity_extraction.py +386 -0
  30. memorygraph/intelligence/pattern_recognition.py +420 -0
  31. memorygraph/intelligence/temporal.py +374 -0
  32. memorygraph/migration/__init__.py +27 -0
  33. memorygraph/migration/manager.py +579 -0
  34. memorygraph/migration/models.py +142 -0
  35. memorygraph/migration/scripts/__init__.py +17 -0
  36. memorygraph/migration/scripts/bitemporal_migration.py +595 -0
  37. memorygraph/migration/scripts/multitenancy_migration.py +452 -0
  38. memorygraph/migration_tools_module.py +146 -0
  39. memorygraph/models.py +684 -0
  40. memorygraph/proactive/__init__.py +46 -0
  41. memorygraph/proactive/outcome_learning.py +444 -0
  42. memorygraph/proactive/predictive.py +410 -0
  43. memorygraph/proactive/session_briefing.py +399 -0
  44. memorygraph/relationships.py +668 -0
  45. memorygraph/server.py +883 -0
  46. memorygraph/sqlite_database.py +1876 -0
  47. memorygraph/tools/__init__.py +59 -0
  48. memorygraph/tools/activity_tools.py +262 -0
  49. memorygraph/tools/memory_tools.py +315 -0
  50. memorygraph/tools/migration_tools.py +181 -0
  51. memorygraph/tools/relationship_tools.py +147 -0
  52. memorygraph/tools/search_tools.py +406 -0
  53. memorygraph/tools/temporal_tools.py +339 -0
  54. memorygraph/utils/__init__.py +10 -0
  55. memorygraph/utils/context_extractor.py +429 -0
  56. memorygraph/utils/error_handling.py +151 -0
  57. memorygraph/utils/export_import.py +425 -0
  58. memorygraph/utils/graph_algorithms.py +200 -0
  59. memorygraph/utils/pagination.py +149 -0
  60. memorygraph/utils/project_detection.py +133 -0
  61. memorygraphmcp-0.11.7.dist-info/METADATA +970 -0
  62. memorygraphmcp-0.11.7.dist-info/RECORD +65 -0
  63. memorygraphmcp-0.11.7.dist-info/WHEEL +4 -0
  64. memorygraphmcp-0.11.7.dist-info/entry_points.txt +2 -0
  65. memorygraphmcp-0.11.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,727 @@
1
+ """
2
+ Advanced Analytics Queries for Claude Code Memory Server.
3
+
4
+ Provides sophisticated graph analytics:
5
+ - Graph visualization data (D3/vis.js compatible)
6
+ - Solution similarity matching
7
+ - Solution effectiveness prediction
8
+ - Learning path recommendations
9
+ - Knowledge gap identification
10
+ - Memory ROI tracking
11
+
12
+ Phase 7 Implementation - Advanced Query & Analytics
13
+ """
14
+
15
+ from datetime import datetime, timedelta
16
+ from typing import List, Dict, Optional, Any, Tuple
17
+ import logging
18
+ from collections import defaultdict
19
+
20
+ from pydantic import BaseModel, Field, ConfigDict
21
+
22
+ from ..backends.base import GraphBackend
23
+ from ..models import Memory, MemoryType, RelationshipType
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class GraphNode(BaseModel):
29
+ """Node in visualization graph."""
30
+
31
+ id: str
32
+ label: str
33
+ type: str
34
+ group: int = 0 # For coloring
35
+ value: float = 1.0 # Node size
36
+ title: Optional[str] = None # Hover text
37
+
38
+
39
+ class GraphEdge(BaseModel):
40
+ """Edge in visualization graph."""
41
+
42
+ model_config = ConfigDict(
43
+ populate_by_name=True
44
+ )
45
+
46
+ from_: str = Field(..., alias="from")
47
+ to: str
48
+ type: str
49
+ value: float = 1.0 # Edge width/weight
50
+ title: Optional[str] = None # Hover text
51
+
52
+
53
+ class GraphVisualizationData(BaseModel):
54
+ """
55
+ Graph visualization data compatible with D3.js and vis.js.
56
+
57
+ Can be directly consumed by visualization libraries.
58
+ """
59
+
60
+ nodes: List[GraphNode] = Field(default_factory=list)
61
+ edges: List[GraphEdge] = Field(default_factory=list)
62
+ metadata: Dict[str, Any] = Field(default_factory=dict)
63
+
64
+
65
+ class SimilarSolution(BaseModel):
66
+ """Similar solution to a given solution."""
67
+
68
+ solution_id: str
69
+ title: str
70
+ description: str
71
+ similarity_score: float = Field(..., ge=0.0, le=1.0)
72
+ shared_entities: List[str] = Field(default_factory=list)
73
+ shared_tags: List[str] = Field(default_factory=list)
74
+ effectiveness: Optional[float] = None
75
+
76
+
77
+ class LearningPath(BaseModel):
78
+ """Recommended learning path for a topic."""
79
+
80
+ path_id: str
81
+ topic: str
82
+ steps: List[Dict[str, str]] = Field(default_factory=list)
83
+ total_memories: int = 0
84
+ estimated_value: float = 0.0
85
+
86
+
87
+ class KnowledgeGap(BaseModel):
88
+ """Identified knowledge gap."""
89
+
90
+ gap_id: str
91
+ topic: str
92
+ description: str
93
+ severity: str = "medium" # low, medium, high
94
+ related_memories: int = 0
95
+ suggestions: List[str] = Field(default_factory=list)
96
+
97
+
98
+ class MemoryROI(BaseModel):
99
+ """Memory return on investment tracking."""
100
+
101
+ memory_id: str
102
+ title: str
103
+ creation_date: datetime
104
+ times_accessed: int = 0
105
+ times_helpful: int = 0
106
+ success_rate: float = 0.0
107
+ value_score: float = 0.0
108
+ last_used: Optional[datetime] = None
109
+
110
+
111
+ async def get_memory_graph_visualization(
112
+ backend: GraphBackend,
113
+ center_memory_id: Optional[str] = None,
114
+ depth: int = 2,
115
+ max_nodes: int = 100,
116
+ include_types: Optional[List[str]] = None,
117
+ ) -> GraphVisualizationData:
118
+ """
119
+ Get graph visualization data centered on a memory or full graph.
120
+
121
+ Args:
122
+ backend: Database backend
123
+ center_memory_id: Optional center memory (None = full graph)
124
+ depth: Depth to traverse from center
125
+ max_nodes: Maximum nodes to return
126
+ include_types: Filter to specific memory types
127
+
128
+ Returns:
129
+ GraphVisualizationData for D3/vis.js
130
+
131
+ Example:
132
+ >>> viz_data = await get_memory_graph_visualization(backend, "mem_123", depth=2)
133
+ >>> # Use viz_data.nodes and viz_data.edges in your visualization
134
+ """
135
+ logger.info(f"Generating graph visualization: center={center_memory_id}, depth={depth}")
136
+
137
+ visualization = GraphVisualizationData()
138
+
139
+ # Set metadata early so it's available even if query fails
140
+ visualization.metadata = {
141
+ "node_count": 0,
142
+ "edge_count": 0,
143
+ "center_id": center_memory_id,
144
+ "depth": depth,
145
+ "generated_at": datetime.now().isoformat(),
146
+ }
147
+
148
+ if center_memory_id:
149
+ # Get subgraph around center
150
+ query = """
151
+ MATCH path = (center:Memory {id: $center_id})-[*1..$depth]-(m:Memory)
152
+ WITH center, m, relationships(path) as rels
153
+ OPTIONAL MATCH (m)-[r]-(other:Memory)
154
+ WHERE other IN collect(center) + collect(m)
155
+ RETURN DISTINCT
156
+ collect(DISTINCT center) + collect(DISTINCT m) as memories,
157
+ collect(DISTINCT r) as relationships
158
+ LIMIT 1
159
+ """
160
+
161
+ params = {"center_id": center_memory_id, "depth": depth}
162
+ else:
163
+ # Get full graph (limited)
164
+ type_filter = ""
165
+ if include_types:
166
+ type_filter = "WHERE m.type IN $types"
167
+
168
+ query = f"""
169
+ MATCH (m:Memory)
170
+ {type_filter}
171
+ WITH m
172
+ LIMIT $max_nodes
173
+ OPTIONAL MATCH (m)-[r]-(other:Memory)
174
+ WHERE other.id IN [m2 IN collect(m) | m2.id]
175
+ RETURN collect(DISTINCT m) as memories,
176
+ collect(DISTINCT r) as relationships
177
+ """
178
+
179
+ params = {"max_nodes": max_nodes}
180
+ if include_types:
181
+ params["types"] = include_types
182
+
183
+ try:
184
+ results = await backend.execute_query(query, params)
185
+
186
+ if not results:
187
+ return visualization
188
+
189
+ memories = results[0].get("memories", [])
190
+ relationships = results[0].get("relationships", [])
191
+
192
+ # Create nodes
193
+ type_groups = {
194
+ "problem": 0,
195
+ "solution": 1,
196
+ "code_pattern": 2,
197
+ "task": 3,
198
+ "project": 4,
199
+ }
200
+
201
+ for mem in memories[:max_nodes]:
202
+ mem_type = mem.get("type", "general")
203
+ node = GraphNode(
204
+ id=mem["id"],
205
+ label=mem.get("title", "Untitled")[:50],
206
+ type=mem_type,
207
+ group=type_groups.get(mem_type, 5),
208
+ value=mem.get("importance", 0.5) * 10,
209
+ title=f"{mem_type}: {mem.get('title', 'Untitled')}",
210
+ )
211
+ visualization.nodes.append(node)
212
+
213
+ # Create edges
214
+ for rel in relationships:
215
+ edge = GraphEdge(**{
216
+ "from": rel["from_id"],
217
+ "to": rel["to_id"],
218
+ "type": rel.get("type", "RELATED_TO"),
219
+ "value": rel.get("strength", 0.5) * 5,
220
+ "title": f"{rel.get('type', 'RELATED_TO')} (strength: {rel.get('strength', 0.5):.2f})",
221
+ })
222
+ visualization.edges.append(edge)
223
+
224
+ # Update metadata with actual counts
225
+ visualization.metadata.update({
226
+ "node_count": len(visualization.nodes),
227
+ "edge_count": len(visualization.edges),
228
+ })
229
+
230
+ logger.info(f"Generated visualization: {len(visualization.nodes)} nodes, {len(visualization.edges)} edges")
231
+
232
+ except Exception as e:
233
+ logger.error(f"Error generating visualization: {e}")
234
+
235
+ return visualization
236
+
237
+
238
+ async def analyze_solution_similarity(
239
+ backend: GraphBackend,
240
+ solution_id: str,
241
+ top_k: int = 5,
242
+ min_similarity: float = 0.3,
243
+ ) -> List[SimilarSolution]:
244
+ """
245
+ Find solutions similar to a given solution.
246
+
247
+ Uses shared entities, tags, and problem types to calculate similarity.
248
+
249
+ Args:
250
+ backend: Database backend
251
+ solution_id: Solution to find similar solutions for
252
+ top_k: Number of similar solutions to return
253
+ min_similarity: Minimum similarity threshold
254
+
255
+ Returns:
256
+ List of similar solutions ranked by similarity
257
+
258
+ Example:
259
+ >>> similar = await analyze_solution_similarity(backend, "solution_123")
260
+ >>> for sol in similar:
261
+ ... print(f"{sol.title}: {sol.similarity_score:.2f}")
262
+ """
263
+ logger.info(f"Analyzing similarity for solution {solution_id}")
264
+
265
+ # Get entities and tags for target solution
266
+ target_query = """
267
+ MATCH (s:Memory {id: $solution_id})
268
+ OPTIONAL MATCH (s)-[:MENTIONS]->(e:Entity)
269
+ RETURN s.tags as tags, collect(e.text) as entities
270
+ """
271
+
272
+ try:
273
+ result = await backend.execute_query(target_query, {"solution_id": solution_id})
274
+
275
+ if not result:
276
+ return []
277
+
278
+ target_tags = set(result[0].get("tags", []))
279
+ target_entities = set(result[0].get("entities", []))
280
+
281
+ # Find similar solutions
282
+ similarity_query = """
283
+ MATCH (other:Memory)
284
+ WHERE other.id <> $solution_id
285
+ AND other.type IN ['solution', 'fix']
286
+ OPTIONAL MATCH (other)-[:MENTIONS]->(e:Entity)
287
+ WITH other,
288
+ other.tags as tags,
289
+ collect(e.text) as entities
290
+ RETURN other.id as id, other.title as title,
291
+ other.content as content, tags, entities,
292
+ other.effectiveness as effectiveness
293
+ LIMIT 50
294
+ """
295
+
296
+ results = await backend.execute_query(
297
+ similarity_query,
298
+ {"solution_id": solution_id}
299
+ )
300
+
301
+ similar_solutions = []
302
+
303
+ for record in results:
304
+ other_tags = set(record.get("tags", []))
305
+ other_entities = set(record.get("entities", []))
306
+
307
+ # Calculate similarity
308
+ # Jaccard similarity for entities and tags
309
+ shared_entities = target_entities & other_entities
310
+ shared_tags = target_tags & other_tags
311
+
312
+ entity_similarity = (
313
+ len(shared_entities) / len(target_entities | other_entities)
314
+ if target_entities or other_entities else 0.0
315
+ )
316
+
317
+ tag_similarity = (
318
+ len(shared_tags) / len(target_tags | other_tags)
319
+ if target_tags or other_tags else 0.0
320
+ )
321
+
322
+ # Weighted combination
323
+ similarity = (entity_similarity * 0.6) + (tag_similarity * 0.4)
324
+
325
+ if similarity >= min_similarity:
326
+ similar_solutions.append(SimilarSolution(
327
+ solution_id=record["id"],
328
+ title=record["title"],
329
+ description=record["content"][:200],
330
+ similarity_score=similarity,
331
+ shared_entities=list(shared_entities),
332
+ shared_tags=list(shared_tags),
333
+ effectiveness=record.get("effectiveness"),
334
+ ))
335
+
336
+ # Sort by similarity
337
+ similar_solutions.sort(key=lambda s: s.similarity_score, reverse=True)
338
+
339
+ logger.info(f"Found {len(similar_solutions)} similar solutions")
340
+ return similar_solutions[:top_k]
341
+
342
+ except Exception as e:
343
+ logger.error(f"Error analyzing similarity: {e}")
344
+ return []
345
+
346
+
347
+ async def predict_solution_effectiveness(
348
+ backend: GraphBackend,
349
+ problem_description: str,
350
+ solution_id: str,
351
+ ) -> float:
352
+ """
353
+ Predict how effective a solution will be for a problem.
354
+
355
+ Based on:
356
+ - Solution's historical effectiveness
357
+ - Similarity to successful past uses
358
+ - Entity matches with problem
359
+
360
+ Args:
361
+ backend: Database backend
362
+ problem_description: Description of the problem
363
+ solution_id: Solution being considered
364
+
365
+ Returns:
366
+ Predicted effectiveness score (0.0 to 1.0)
367
+
368
+ Example:
369
+ >>> score = await predict_solution_effectiveness(
370
+ ... backend,
371
+ ... "Authentication failing with JWT",
372
+ ... "solution_456"
373
+ ... )
374
+ >>> print(f"Predicted effectiveness: {score:.2%}")
375
+ """
376
+ logger.info(f"Predicting effectiveness of solution {solution_id}")
377
+
378
+ # Get solution's historical effectiveness
379
+ solution_query = """
380
+ MATCH (s:Memory {id: $solution_id})
381
+ OPTIONAL MATCH (s)-[:RESULTED_IN]->(o:Outcome)
382
+ RETURN s.effectiveness as base_effectiveness,
383
+ s.confidence as confidence,
384
+ count(o) as outcomes,
385
+ sum(CASE WHEN o.success THEN 1 ELSE 0 END) as successes
386
+ """
387
+
388
+ try:
389
+ result = await backend.execute_query(
390
+ solution_query,
391
+ {"solution_id": solution_id}
392
+ )
393
+
394
+ if not result:
395
+ return 0.5 # Default
396
+
397
+ record = result[0]
398
+ base_effectiveness = record.get("base_effectiveness", 0.5)
399
+ confidence = record.get("confidence", 0.5)
400
+
401
+ # If high confidence, trust the historical score
402
+ if confidence > 0.7:
403
+ return base_effectiveness
404
+
405
+ # Otherwise, blend with entity matching
406
+ # (This would ideally use embeddings, but we'll use entity matching)
407
+
408
+ # Extract entities from problem description
409
+ from ..intelligence.entity_extraction import extract_entities
410
+
411
+ entities = extract_entities(problem_description)
412
+ entity_texts = [e.text for e in entities]
413
+
414
+ if not entity_texts:
415
+ return base_effectiveness
416
+
417
+ # Check if solution has been used with similar entities
418
+ entity_match_query = """
419
+ MATCH (s:Memory {id: $solution_id})-[:MENTIONS]->(e:Entity)
420
+ WHERE e.text IN $entity_texts
421
+ RETURN count(DISTINCT e) as matched_entities
422
+ """
423
+
424
+ entity_result = await backend.execute_query(
425
+ entity_match_query,
426
+ {"solution_id": solution_id, "entity_texts": entity_texts}
427
+ )
428
+
429
+ matched_count = entity_result[0]["matched_entities"] if entity_result else 0
430
+ entity_match_score = min(matched_count / len(entity_texts), 1.0)
431
+
432
+ # Blend scores
433
+ predicted = (base_effectiveness * 0.7) + (entity_match_score * 0.3)
434
+
435
+ logger.info(f"Predicted effectiveness: {predicted:.2f} (base: {base_effectiveness:.2f}, match: {entity_match_score:.2f})")
436
+ return predicted
437
+
438
+ except Exception as e:
439
+ logger.error(f"Error predicting effectiveness: {e}")
440
+ return 0.5
441
+
442
+
443
+ async def recommend_learning_paths(
444
+ backend: GraphBackend,
445
+ topic: str,
446
+ max_paths: int = 3,
447
+ ) -> List[LearningPath]:
448
+ """
449
+ Recommend learning paths for a topic.
450
+
451
+ Analyzes memory relationships to suggest learning sequences.
452
+
453
+ Args:
454
+ backend: Database backend
455
+ topic: Topic to learn about
456
+ max_paths: Maximum number of paths to return
457
+
458
+ Returns:
459
+ List of recommended learning paths
460
+
461
+ Example:
462
+ >>> paths = await recommend_learning_paths(backend, "React authentication")
463
+ >>> for path in paths:
464
+ ... print(f"Path {path.path_id}: {len(path.steps)} steps")
465
+ """
466
+ logger.info(f"Recommending learning paths for topic: {topic}")
467
+
468
+ # Find memories related to topic
469
+ topic_query = """
470
+ MATCH (m:Memory)
471
+ WHERE m.content CONTAINS $topic
472
+ OR any(tag IN m.tags WHERE tag CONTAINS $topic_lower)
473
+ OR m.title CONTAINS $topic
474
+ WITH m
475
+ LIMIT 20
476
+ MATCH path = (m)-[:BUILDS_ON|GENERALIZES|SPECIALIZES*1..3]-(related:Memory)
477
+ RETURN m, collect(DISTINCT related) as related_memories,
478
+ length(path) as path_length
479
+ ORDER BY path_length DESC
480
+ LIMIT $max_paths
481
+ """
482
+
483
+ try:
484
+ results = await backend.execute_query(
485
+ topic_query,
486
+ {
487
+ "topic": topic,
488
+ "topic_lower": topic.lower(),
489
+ "max_paths": max_paths,
490
+ }
491
+ )
492
+
493
+ paths = []
494
+
495
+ for idx, record in enumerate(results):
496
+ start_memory = record["m"]
497
+ related = record.get("related_memories", [])
498
+
499
+ steps = [
500
+ {
501
+ "memory_id": start_memory["id"],
502
+ "title": start_memory["title"],
503
+ "type": start_memory["type"],
504
+ "step": 1,
505
+ }
506
+ ]
507
+
508
+ # Add related memories as subsequent steps
509
+ for step_idx, mem in enumerate(related[:5], start=2):
510
+ steps.append({
511
+ "memory_id": mem["id"],
512
+ "title": mem["title"],
513
+ "type": mem["type"],
514
+ "step": step_idx,
515
+ })
516
+
517
+ # Calculate value based on effectiveness
518
+ total_effectiveness = sum(
519
+ mem.get("effectiveness", 0.5)
520
+ for mem in [start_memory] + related[:5]
521
+ )
522
+ avg_effectiveness = total_effectiveness / len(steps)
523
+
524
+ paths.append(LearningPath(
525
+ path_id=f"path_{idx + 1}",
526
+ topic=topic,
527
+ steps=steps,
528
+ total_memories=len(steps),
529
+ estimated_value=avg_effectiveness,
530
+ ))
531
+
532
+ logger.info(f"Recommended {len(paths)} learning paths")
533
+ return paths
534
+
535
+ except Exception as e:
536
+ logger.error(f"Error recommending learning paths: {e}")
537
+ return []
538
+
539
+
540
+ async def identify_knowledge_gaps(
541
+ backend: GraphBackend,
542
+ project: Optional[str] = None,
543
+ min_gap_severity: str = "low",
544
+ ) -> List[KnowledgeGap]:
545
+ """
546
+ Identify knowledge gaps in the memory graph.
547
+
548
+ Looks for:
549
+ - Problems without solutions
550
+ - Sparse areas of the graph
551
+ - Technologies mentioned but not documented
552
+
553
+ Args:
554
+ backend: Database backend
555
+ project: Optional project filter
556
+ min_gap_severity: Minimum severity ("low", "medium", "high")
557
+
558
+ Returns:
559
+ List of identified knowledge gaps
560
+
561
+ Example:
562
+ >>> gaps = await identify_knowledge_gaps(backend, project="my-app")
563
+ >>> for gap in gaps:
564
+ ... print(f"{gap.severity.upper()}: {gap.topic}")
565
+ """
566
+ logger.info(f"Identifying knowledge gaps for project: {project}")
567
+
568
+ gaps = []
569
+
570
+ # Find problems without solutions
571
+ unsolved_query = """
572
+ MATCH (p:Memory {type: 'problem'})
573
+ WHERE NOT EXISTS {
574
+ MATCH (p)<-[:SOLVES|ADDRESSES]-(:Memory)
575
+ }
576
+ """
577
+
578
+ if project:
579
+ unsolved_query += """
580
+ AND (p.context CONTAINS $project)
581
+ """
582
+
583
+ unsolved_query += """
584
+ RETURN p.id as id, p.title as title, p.tags as tags,
585
+ p.created_at as created_at
586
+ ORDER BY p.created_at DESC
587
+ LIMIT 10
588
+ """
589
+
590
+ try:
591
+ params = {"project": project} if project else {}
592
+ results = await backend.execute_query(unsolved_query, params)
593
+
594
+ for record in results:
595
+ age_days = (datetime.now() - datetime.fromisoformat(record["created_at"])).days
596
+
597
+ # Severity based on age
598
+ if age_days > 30:
599
+ severity = "high"
600
+ elif age_days > 7:
601
+ severity = "medium"
602
+ else:
603
+ severity = "low"
604
+
605
+ gaps.append(KnowledgeGap(
606
+ gap_id=record["id"],
607
+ topic=record["title"],
608
+ description=f"Unsolved problem ({age_days} days old)",
609
+ severity=severity,
610
+ related_memories=0,
611
+ suggestions=["Create a solution memory", "Link to existing solutions"],
612
+ ))
613
+
614
+ except Exception as e:
615
+ logger.error(f"Error finding unsolved problems: {e}")
616
+
617
+ # Find sparse entities (mentioned but no dedicated memories)
618
+ sparse_query = """
619
+ MATCH (e:Entity)<-[:MENTIONS]-(m:Memory)
620
+ WITH e, count(m) as mention_count
621
+ WHERE mention_count <= 2
622
+ AND mention_count > 0
623
+ RETURN e.text as entity, e.entity_type as type, mention_count
624
+ LIMIT 10
625
+ """
626
+
627
+ try:
628
+ results = await backend.execute_query(sparse_query, {})
629
+
630
+ for record in results:
631
+ gaps.append(KnowledgeGap(
632
+ gap_id=f"sparse_{record['entity']}",
633
+ topic=record["entity"],
634
+ description=f"Technology/concept mentioned only {record['mention_count']} time(s)",
635
+ severity="low",
636
+ related_memories=record["mention_count"],
637
+ suggestions=[f"Create documentation for {record['entity']}", "Add code patterns"],
638
+ ))
639
+
640
+ except Exception as e:
641
+ logger.error(f"Error finding sparse entities: {e}")
642
+
643
+ # Filter by severity
644
+ severity_levels = {"low": 0, "medium": 1, "high": 2}
645
+ threshold = severity_levels.get(min_gap_severity, 1)
646
+
647
+ filtered_gaps = [
648
+ gap for gap in gaps
649
+ if severity_levels.get(gap.severity, 0) >= threshold
650
+ ]
651
+
652
+ logger.info(f"Identified {len(filtered_gaps)} knowledge gaps")
653
+ return filtered_gaps
654
+
655
+
656
+ async def track_memory_roi(
657
+ backend: GraphBackend,
658
+ memory_id: str,
659
+ ) -> Optional[MemoryROI]:
660
+ """
661
+ Track return on investment for a memory.
662
+
663
+ Calculates value based on:
664
+ - How often accessed
665
+ - Success rate when used
666
+ - Time since creation
667
+
668
+ Args:
669
+ backend: Database backend
670
+ memory_id: Memory to track
671
+
672
+ Returns:
673
+ MemoryROI metrics, or None if not found
674
+
675
+ Example:
676
+ >>> roi = await track_memory_roi(backend, "solution_789")
677
+ >>> print(f"ROI: {roi.value_score:.2f} (used {roi.times_accessed} times)")
678
+ """
679
+ logger.info(f"Tracking ROI for memory {memory_id}")
680
+
681
+ roi_query = """
682
+ MATCH (m:Memory {id: $memory_id})
683
+ OPTIONAL MATCH (m)-[:RESULTED_IN]->(o:Outcome)
684
+ RETURN m.id as id, m.title as title, m.created_at as created_at,
685
+ m.usage_count as usage_count, m.last_accessed as last_accessed,
686
+ count(o) as total_outcomes,
687
+ sum(CASE WHEN o.success THEN 1 ELSE 0 END) as successful_outcomes
688
+ """
689
+
690
+ try:
691
+ result = await backend.execute_query(roi_query, {"memory_id": memory_id})
692
+
693
+ if not result:
694
+ return None
695
+
696
+ record = result[0]
697
+ usage_count = record.get("usage_count", 0)
698
+ total_outcomes = record.get("total_outcomes", 0)
699
+ successful_outcomes = record.get("successful_outcomes", 0)
700
+
701
+ success_rate = (
702
+ successful_outcomes / total_outcomes
703
+ if total_outcomes > 0 else 0.0
704
+ )
705
+
706
+ # Value score combines usage and success
707
+ # High usage + high success = high value
708
+ usage_score = min(usage_count / 10.0, 1.0) # Cap at 10 uses
709
+ value_score = (usage_score * 0.5) + (success_rate * 0.5)
710
+
711
+ return MemoryROI(
712
+ memory_id=record["id"],
713
+ title=record["title"],
714
+ creation_date=datetime.fromisoformat(record["created_at"]),
715
+ times_accessed=usage_count,
716
+ times_helpful=successful_outcomes,
717
+ success_rate=success_rate,
718
+ value_score=value_score,
719
+ last_used=(
720
+ datetime.fromisoformat(record["last_accessed"])
721
+ if record.get("last_accessed") else None
722
+ ),
723
+ )
724
+
725
+ except Exception as e:
726
+ logger.error(f"Error tracking ROI: {e}")
727
+ return None