feat: auto-create edges on memory_store in MCP server
Automatically find and link related memories after every store operation, removing reliance on Claude manually creating edges. Uses recall + type heuristics to choose edge types (SOLVES, BUILDS_ON, RELATED_TO) and returns created edge IDs in the store response for optional review. Key design choices: - Keyword-only fallback requires tag overlap to prevent spurious edges - Similarity threshold (0.4) filters before max-edge cap (3) - Edge description arrow matches actual from/to direction - Failures never break the store operation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e87c7598d1
commit
194990d424
110
mcp_server.py
110
mcp_server.py
@ -20,6 +20,23 @@ from common import resolve_graph_path, list_graphs
|
|||||||
|
|
||||||
SYNC_SCRIPT = Path(__file__).parent / "scripts" / "memory-git-sync.sh"
|
SYNC_SCRIPT = Path(__file__).parent / "scripts" / "memory-git-sync.sh"
|
||||||
|
|
||||||
|
# Auto-edge heuristics: (new_type, match_type) -> (rel_type, direction)
|
||||||
|
# direction "ab" = new->match, "ba" = match->new
|
||||||
|
AUTO_EDGE_HEURISTICS = {
|
||||||
|
("fix", "problem"): ("SOLVES", "ab"),
|
||||||
|
("solution", "problem"): ("SOLVES", "ab"),
|
||||||
|
("solution", "error"): ("SOLVES", "ab"),
|
||||||
|
("fix", "error"): ("SOLVES", "ab"),
|
||||||
|
("insight", "solution"): ("BUILDS_ON", "ab"),
|
||||||
|
("insight", "decision"): ("BUILDS_ON", "ab"),
|
||||||
|
("decision", "solution"): ("BUILDS_ON", "ab"),
|
||||||
|
("fix", "solution"): ("BUILDS_ON", "ab"),
|
||||||
|
("code_pattern", "solution"): ("BUILDS_ON", "ab"),
|
||||||
|
("procedure", "workflow"): ("BUILDS_ON", "ab"),
|
||||||
|
}
|
||||||
|
AUTO_EDGE_SIMILARITY_THRESHOLD = 0.4
|
||||||
|
AUTO_EDGE_MAX = 3
|
||||||
|
|
||||||
_clients: Dict[str, CognitiveMemoryClient] = {}
|
_clients: Dict[str, CognitiveMemoryClient] = {}
|
||||||
|
|
||||||
|
|
||||||
@ -527,6 +544,97 @@ def create_tools() -> list:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _auto_create_edges(
|
||||||
|
client: CognitiveMemoryClient,
|
||||||
|
memory_id: str,
|
||||||
|
title: str,
|
||||||
|
mem_type: str,
|
||||||
|
tags: Optional[list] = None,
|
||||||
|
) -> list:
|
||||||
|
"""Auto-create edges between a newly stored memory and related existing memories.
|
||||||
|
|
||||||
|
Uses recall to find similar memories and heuristic type-pairs to choose
|
||||||
|
relationship types. Returns list of created edge info dicts.
|
||||||
|
Never raises — failures are silently swallowed so store always succeeds.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Build recall query from title + tags for better precision (#4)
|
||||||
|
query = title
|
||||||
|
if tags:
|
||||||
|
query = f"{title} {' '.join(tags)}"
|
||||||
|
results = client.recall(query, limit=5, semantic=True)
|
||||||
|
|
||||||
|
# Filter out the just-stored memory
|
||||||
|
results = [r for r in results if r.get("id") != memory_id]
|
||||||
|
|
||||||
|
# Filter by similarity threshold before slicing to AUTO_EDGE_MAX (#5)
|
||||||
|
# When embeddings are absent, keyword-only results lack "similarity" —
|
||||||
|
# require at least a tag overlap to avoid spurious edges (#1)
|
||||||
|
filtered = []
|
||||||
|
new_tags = set(tags or [])
|
||||||
|
for result in results:
|
||||||
|
similarity = result.get("similarity")
|
||||||
|
if similarity is not None:
|
||||||
|
# Semantic path: use similarity threshold
|
||||||
|
if similarity < AUTO_EDGE_SIMILARITY_THRESHOLD:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Keyword-only path: require at least one shared tag
|
||||||
|
match_tags = set(result.get("tags") or [])
|
||||||
|
if not new_tags or not (new_tags & match_tags):
|
||||||
|
continue
|
||||||
|
filtered.append(result)
|
||||||
|
|
||||||
|
created_edges = []
|
||||||
|
for result in filtered[:AUTO_EDGE_MAX]:
|
||||||
|
match_type = result.get("type", "")
|
||||||
|
match_id = result["id"]
|
||||||
|
match_title = result.get("title", "")
|
||||||
|
|
||||||
|
# Look up heuristic for (new_type, match_type) in both orderings
|
||||||
|
key_ab = (mem_type, match_type)
|
||||||
|
key_ba = (match_type, mem_type)
|
||||||
|
|
||||||
|
if key_ab in AUTO_EDGE_HEURISTICS:
|
||||||
|
rel_type, direction = AUTO_EDGE_HEURISTICS[key_ab]
|
||||||
|
from_id = memory_id if direction == "ab" else match_id
|
||||||
|
to_id = match_id if direction == "ab" else memory_id
|
||||||
|
elif key_ba in AUTO_EDGE_HEURISTICS:
|
||||||
|
rel_type, direction = AUTO_EDGE_HEURISTICS[key_ba]
|
||||||
|
# Reverse: if heuristic says ab for (match, new), then match->new
|
||||||
|
from_id = match_id if direction == "ab" else memory_id
|
||||||
|
to_id = memory_id if direction == "ab" else match_id
|
||||||
|
else:
|
||||||
|
rel_type = "RELATED_TO"
|
||||||
|
from_id = memory_id
|
||||||
|
to_id = match_id
|
||||||
|
|
||||||
|
# Build description matching actual edge direction (#2)
|
||||||
|
from_title = title if from_id == memory_id else match_title
|
||||||
|
to_title = match_title if to_id == match_id else title
|
||||||
|
desc = f"Auto-edge: {from_title} → {to_title}"
|
||||||
|
edge_id = client.relate(
|
||||||
|
from_id=from_id,
|
||||||
|
to_id=to_id,
|
||||||
|
rel_type=rel_type,
|
||||||
|
description=desc,
|
||||||
|
)
|
||||||
|
|
||||||
|
if edge_id: # Empty string means duplicate
|
||||||
|
created_edges.append(
|
||||||
|
{
|
||||||
|
"edge_id": edge_id,
|
||||||
|
"rel_type": rel_type,
|
||||||
|
"linked_memory_id": match_id,
|
||||||
|
"linked_title": match_title,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return created_edges
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def handle_tool_call(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
def handle_tool_call(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Dispatch MCP tool calls to the CognitiveMemoryClient."""
|
"""Dispatch MCP tool calls to the CognitiveMemoryClient."""
|
||||||
|
|
||||||
@ -561,12 +669,14 @@ def handle_tool_call(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any
|
|||||||
tags=tags,
|
tags=tags,
|
||||||
)
|
)
|
||||||
episode_logged = True
|
episode_logged = True
|
||||||
|
auto_edges = _auto_create_edges(client, memory_id, title, mem_type, tags)
|
||||||
_trigger_git_sync()
|
_trigger_git_sync()
|
||||||
return ok(
|
return ok(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"memory_id": memory_id,
|
"memory_id": memory_id,
|
||||||
"episode_logged": episode_logged,
|
"episode_logged": episode_logged,
|
||||||
|
"auto_edges": auto_edges,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user