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.
- memorygraph/__init__.py +50 -0
- memorygraph/__main__.py +12 -0
- memorygraph/advanced_tools.py +509 -0
- memorygraph/analytics/__init__.py +46 -0
- memorygraph/analytics/advanced_queries.py +727 -0
- memorygraph/backends/__init__.py +21 -0
- memorygraph/backends/base.py +179 -0
- memorygraph/backends/cloud.py +75 -0
- memorygraph/backends/cloud_backend.py +858 -0
- memorygraph/backends/factory.py +577 -0
- memorygraph/backends/falkordb_backend.py +749 -0
- memorygraph/backends/falkordblite_backend.py +746 -0
- memorygraph/backends/ladybugdb_backend.py +242 -0
- memorygraph/backends/memgraph_backend.py +327 -0
- memorygraph/backends/neo4j_backend.py +298 -0
- memorygraph/backends/sqlite_fallback.py +463 -0
- memorygraph/backends/turso.py +448 -0
- memorygraph/cli.py +743 -0
- memorygraph/cloud_database.py +297 -0
- memorygraph/config.py +295 -0
- memorygraph/database.py +933 -0
- memorygraph/graph_analytics.py +631 -0
- memorygraph/integration/__init__.py +69 -0
- memorygraph/integration/context_capture.py +426 -0
- memorygraph/integration/project_analysis.py +583 -0
- memorygraph/integration/workflow_tracking.py +492 -0
- memorygraph/intelligence/__init__.py +59 -0
- memorygraph/intelligence/context_retrieval.py +447 -0
- memorygraph/intelligence/entity_extraction.py +386 -0
- memorygraph/intelligence/pattern_recognition.py +420 -0
- memorygraph/intelligence/temporal.py +374 -0
- memorygraph/migration/__init__.py +27 -0
- memorygraph/migration/manager.py +579 -0
- memorygraph/migration/models.py +142 -0
- memorygraph/migration/scripts/__init__.py +17 -0
- memorygraph/migration/scripts/bitemporal_migration.py +595 -0
- memorygraph/migration/scripts/multitenancy_migration.py +452 -0
- memorygraph/migration_tools_module.py +146 -0
- memorygraph/models.py +684 -0
- memorygraph/proactive/__init__.py +46 -0
- memorygraph/proactive/outcome_learning.py +444 -0
- memorygraph/proactive/predictive.py +410 -0
- memorygraph/proactive/session_briefing.py +399 -0
- memorygraph/relationships.py +668 -0
- memorygraph/server.py +883 -0
- memorygraph/sqlite_database.py +1876 -0
- memorygraph/tools/__init__.py +59 -0
- memorygraph/tools/activity_tools.py +262 -0
- memorygraph/tools/memory_tools.py +315 -0
- memorygraph/tools/migration_tools.py +181 -0
- memorygraph/tools/relationship_tools.py +147 -0
- memorygraph/tools/search_tools.py +406 -0
- memorygraph/tools/temporal_tools.py +339 -0
- memorygraph/utils/__init__.py +10 -0
- memorygraph/utils/context_extractor.py +429 -0
- memorygraph/utils/error_handling.py +151 -0
- memorygraph/utils/export_import.py +425 -0
- memorygraph/utils/graph_algorithms.py +200 -0
- memorygraph/utils/pagination.py +149 -0
- memorygraph/utils/project_detection.py +133 -0
- memorygraphmcp-0.11.7.dist-info/METADATA +970 -0
- memorygraphmcp-0.11.7.dist-info/RECORD +65 -0
- memorygraphmcp-0.11.7.dist-info/WHEEL +4 -0
- memorygraphmcp-0.11.7.dist-info/entry_points.txt +2 -0
- 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
|