"""Read graph tool - return entire knowledge graph."""
import time
from typing import Any
from ..context import db, mcp
from ..core.decay import calculate_score
from ..core.pagination import paginate_list, validate_pagination_params
from ..security.validators import validate_positive_int
from ..storage.models import MemoryStatus
@mcp.tool()
def read_graph(
status: str = "active",
include_scores: bool = True,
limit: int | None = None,
page: int | None = None,
page_size: int | None = None,
) -> dict[str, Any]:
"""
Read the entire knowledge graph of memories and relations.
Returns the complete graph structure including all memories (with decay scores),
all relations between memories, and statistics about the graph.
**Pagination:** Results are paginated to help you navigate large knowledge graphs.
Use `page` and `page_size` to retrieve specific portions of the graph.
If searching for specific memories or patterns, increment `page` to see more results.
Args:
status: Filter memories by status - "active", "promoted", "archived", or "all".
include_scores: Include decay scores and age in results.
limit: Maximum number of memories to return (1-10,000).
page: Page number to retrieve (1-indexed, default: 1).
page_size: Number of memories per page (default: 10, max: 100).
Returns:
Dictionary with paginated graph including:
- memories: List of memories for current page
- relations: All relations (not paginated, for graph structure)
- stats: Graph statistics
- pagination: Metadata (page, page_size, total_count, total_pages, has_more)
Examples:
# Get first page of active memories
read_graph(status="active", page=1, page_size=10)
# Get next page
read_graph(status="active", page=2, page_size=10)
# Larger page for overview
read_graph(status="active", page=1, page_size=50)
Raises:
ValueError: If status is invalid or limit is out of range.
"""
# Input validation
valid_statuses = {"active", "promoted", "archived", "all"}
if status not in valid_statuses:
raise ValueError(f"status must be one of {valid_statuses}, got: {status}")
if limit is not None:
limit = validate_positive_int(limit, "limit", min_value=1, max_value=10000)
# Only validate pagination if explicitly requested
pagination_requested = page is not None or page_size is not None
status_filter = None if status == "all" else MemoryStatus(status)
graph = db.get_knowledge_graph(status=status_filter)
if limit and limit > 0:
graph.memories = graph.memories[:limit]
graph.stats["limited_to"] = limit
now = int(time.time())
memories_data = []
for memory in graph.memories:
mem_data = {
"id": memory.id,
"content": memory.content,
"entities": memory.entities,
"tags": memory.meta.tags,
"created_at": memory.created_at,
"last_used": memory.last_used,
"use_count": memory.use_count,
"strength": memory.strength,
"status": memory.status.value,
}
if include_scores:
score = calculate_score(
use_count=memory.use_count,
last_used=memory.last_used,
strength=memory.strength,
now=now,
)
mem_data["score"] = round(score, 4)
mem_data["age_days"] = round((now - memory.created_at) / 86400, 1)
memories_data.append(mem_data)
relations_data = [
{
"id": rel.id,
"from": rel.from_memory_id,
"to": rel.to_memory_id,
"type": rel.relation_type,
"strength": round(rel.strength, 4),
"created_at": rel.created_at,
}
for rel in graph.relations
]
# Apply pagination only if requested
if pagination_requested:
# Validate and get non-None values
valid_page, valid_page_size = validate_pagination_params(page, page_size)
paginated_memories = paginate_list(
memories_data, page=valid_page, page_size=valid_page_size
)
return {
"success": True,
"memories": paginated_memories.items,
"relations": relations_data,
"stats": {
"total_memories": graph.stats["total_memories"],
"total_relations": graph.stats["total_relations"],
"avg_score": round(graph.stats["avg_score"], 4),
"avg_use_count": round(graph.stats["avg_use_count"], 2),
"status_filter": graph.stats["status_filter"],
},
"pagination": paginated_memories.to_dict(),
}
else:
# No pagination - return all memories
return {
"success": True,
"memories": memories_data,
"relations": relations_data,
"stats": {
"total_memories": graph.stats["total_memories"],
"total_relations": graph.stats["total_relations"],
"avg_score": round(graph.stats["avg_score"], 4),
"avg_use_count": round(graph.stats["avg_use_count"], 2),
"status_filter": graph.stats["status_filter"],
},
}