basic-memory 0.6.0__py3-none-any.whl → 0.8.0__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.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (70) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +23 -1
  4. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  5. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  6. basic_memory/api/app.py +0 -4
  7. basic_memory/api/routers/knowledge_router.py +1 -9
  8. basic_memory/api/routers/memory_router.py +41 -25
  9. basic_memory/api/routers/resource_router.py +119 -12
  10. basic_memory/api/routers/search_router.py +17 -9
  11. basic_memory/cli/app.py +0 -2
  12. basic_memory/cli/commands/db.py +11 -8
  13. basic_memory/cli/commands/import_chatgpt.py +31 -27
  14. basic_memory/cli/commands/import_claude_conversations.py +29 -27
  15. basic_memory/cli/commands/import_claude_projects.py +30 -29
  16. basic_memory/cli/commands/import_memory_json.py +28 -26
  17. basic_memory/cli/commands/status.py +16 -26
  18. basic_memory/cli/commands/sync.py +11 -12
  19. basic_memory/cli/commands/tools.py +180 -0
  20. basic_memory/cli/main.py +1 -1
  21. basic_memory/config.py +16 -2
  22. basic_memory/db.py +1 -0
  23. basic_memory/deps.py +5 -1
  24. basic_memory/file_utils.py +6 -4
  25. basic_memory/markdown/entity_parser.py +3 -3
  26. basic_memory/mcp/async_client.py +1 -1
  27. basic_memory/mcp/main.py +25 -0
  28. basic_memory/mcp/prompts/__init__.py +15 -0
  29. basic_memory/mcp/prompts/ai_assistant_guide.py +28 -0
  30. basic_memory/mcp/prompts/continue_conversation.py +172 -0
  31. basic_memory/mcp/prompts/json_canvas_spec.py +25 -0
  32. basic_memory/mcp/prompts/recent_activity.py +46 -0
  33. basic_memory/mcp/prompts/search.py +127 -0
  34. basic_memory/mcp/prompts/utils.py +98 -0
  35. basic_memory/mcp/server.py +3 -7
  36. basic_memory/mcp/tools/__init__.py +6 -4
  37. basic_memory/mcp/tools/canvas.py +99 -0
  38. basic_memory/mcp/tools/knowledge.py +26 -14
  39. basic_memory/mcp/tools/memory.py +57 -31
  40. basic_memory/mcp/tools/notes.py +65 -72
  41. basic_memory/mcp/tools/resource.py +192 -0
  42. basic_memory/mcp/tools/search.py +13 -4
  43. basic_memory/mcp/tools/utils.py +2 -1
  44. basic_memory/models/knowledge.py +27 -11
  45. basic_memory/repository/repository.py +1 -1
  46. basic_memory/repository/search_repository.py +17 -4
  47. basic_memory/schemas/__init__.py +0 -11
  48. basic_memory/schemas/base.py +4 -1
  49. basic_memory/schemas/memory.py +14 -2
  50. basic_memory/schemas/request.py +1 -1
  51. basic_memory/schemas/search.py +4 -1
  52. basic_memory/services/context_service.py +14 -6
  53. basic_memory/services/entity_service.py +19 -12
  54. basic_memory/services/file_service.py +69 -2
  55. basic_memory/services/link_resolver.py +12 -9
  56. basic_memory/services/search_service.py +59 -13
  57. basic_memory/sync/__init__.py +3 -2
  58. basic_memory/sync/sync_service.py +287 -107
  59. basic_memory/sync/watch_service.py +125 -129
  60. basic_memory/utils.py +27 -15
  61. {basic_memory-0.6.0.dist-info → basic_memory-0.8.0.dist-info}/METADATA +3 -2
  62. basic_memory-0.8.0.dist-info/RECORD +91 -0
  63. basic_memory/alembic/README +0 -1
  64. basic_memory/schemas/discovery.py +0 -28
  65. basic_memory/sync/file_change_scanner.py +0 -158
  66. basic_memory/sync/utils.py +0 -31
  67. basic_memory-0.6.0.dist-info/RECORD +0 -81
  68. {basic_memory-0.6.0.dist-info → basic_memory-0.8.0.dist-info}/WHEEL +0 -0
  69. {basic_memory-0.6.0.dist-info → basic_memory-0.8.0.dist-info}/entry_points.txt +0 -0
  70. {basic_memory-0.6.0.dist-info → basic_memory-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,127 @@
1
+ """Search prompts for Basic Memory MCP server.
2
+
3
+ These prompts help users search and explore their knowledge base.
4
+ """
5
+
6
+ from textwrap import dedent
7
+ from typing import Annotated, Optional
8
+
9
+ from loguru import logger
10
+ import logfire
11
+ from pydantic import Field
12
+
13
+ from basic_memory.mcp.server import mcp
14
+ from basic_memory.mcp.tools.search import search as search_tool
15
+ from basic_memory.schemas.search import SearchQuery, SearchResponse
16
+ from basic_memory.schemas.base import TimeFrame
17
+
18
+
19
+ @mcp.prompt(
20
+ name="search",
21
+ description="Search across all content in basic-memory",
22
+ )
23
+ async def search_prompt(
24
+ query: str,
25
+ timeframe: Annotated[
26
+ Optional[TimeFrame],
27
+ Field(description="How far back to search (e.g. '1d', '1 week')"),
28
+ ] = None,
29
+ ) -> str:
30
+ """Search across all content in basic-memory.
31
+
32
+ This prompt helps search for content in the knowledge base and
33
+ provides helpful context about the results.
34
+
35
+ Args:
36
+ query: The search text to look for
37
+ timeframe: Optional timeframe to limit results (e.g. '1d', '1 week')
38
+
39
+ Returns:
40
+ Formatted search results with context
41
+ """
42
+ with logfire.span("Searching knowledge base", query=query, timeframe=timeframe): # pyright: ignore
43
+ logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
44
+
45
+ search_results = await search_tool(SearchQuery(text=query, after_date=timeframe))
46
+ return format_search_results(query, search_results, timeframe)
47
+
48
+
49
+ def format_search_results(
50
+ query: str, results: SearchResponse, timeframe: Optional[TimeFrame] = None
51
+ ) -> str:
52
+ """Format search results into a helpful summary.
53
+
54
+ Args:
55
+ query: The search query
56
+ results: Search results object
57
+ timeframe: How far back results were searched
58
+
59
+ Returns:
60
+ Formatted search results summary
61
+ """
62
+ if not results.results:
63
+ return dedent(f"""
64
+ # Search Results for: "{query}"
65
+
66
+ I couldn't find any results for this query.
67
+
68
+ ## Suggestions
69
+ - Try a different search term
70
+ - Broaden your search criteria
71
+ - Check recent activity with `recent_activity(timeframe="1w")`
72
+ - Create new content with `write_note(...)`
73
+ """)
74
+
75
+ # Start building our summary with header
76
+ time_info = f" (after {timeframe})" if timeframe else ""
77
+ summary = dedent(f"""
78
+ # Search Results for: "{query}"{time_info}
79
+
80
+ This is a memory search session.
81
+ Please use the available basic-memory tools to gather relevant context before responding.
82
+ I found {len(results.results)} results that match your query.
83
+
84
+ Here are the most relevant results:
85
+ """)
86
+
87
+ # Add each search result
88
+ for i, result in enumerate(results.results[:5]): # Limit to top 5 results
89
+ summary += dedent(f"""
90
+ ## {i + 1}. {result.title}
91
+ - **Type**: {result.type}
92
+ """)
93
+
94
+ # Add creation date if available in metadata
95
+ if hasattr(result, "metadata") and result.metadata and "created_at" in result.metadata:
96
+ created_at = result.metadata["created_at"]
97
+ if hasattr(created_at, "strftime"):
98
+ summary += f"- **Created**: {created_at.strftime('%Y-%m-%d %H:%M')}\n"
99
+ elif isinstance(created_at, str):
100
+ summary += f"- **Created**: {created_at}\n"
101
+
102
+ # Add score and excerpt
103
+ summary += f"- **Relevance Score**: {result.score:.2f}\n"
104
+ # Add excerpt if available in metadata
105
+ if hasattr(result, "metadata") and result.metadata and "excerpt" in result.metadata:
106
+ summary += f"- **Excerpt**: {result.metadata['excerpt']}\n"
107
+
108
+ # Add permalink for retrieving content
109
+ if hasattr(result, "permalink") and result.permalink:
110
+ summary += dedent(f"""
111
+
112
+ You can view this content with: `read_note("{result.permalink}")`
113
+ Or explore its context with: `build_context("memory://{result.permalink}")`
114
+ """)
115
+
116
+ # Add next steps
117
+ summary += dedent(f"""
118
+ ## Next Steps
119
+
120
+ You can:
121
+ - Refine your search: `search("{query} AND additional_term")`
122
+ - Exclude terms: `search("{query} NOT exclude_term")`
123
+ - View more results: `search("{query}", after_date=None)`
124
+ - Check recent activity: `recent_activity()`
125
+ """)
126
+
127
+ return summary
@@ -0,0 +1,98 @@
1
+ """Utility functions for formatting prompt responses.
2
+
3
+ These utilities help format data from various tools into consistent,
4
+ user-friendly markdown summaries.
5
+ """
6
+
7
+ from basic_memory.schemas.memory import GraphContext
8
+
9
+
10
+ def format_context_summary(header: str, context: GraphContext) -> str:
11
+ """Format GraphContext as a helpful markdown summary.
12
+
13
+ This creates a user-friendly markdown response that explains the context
14
+ and provides guidance on how to explore further.
15
+
16
+ Args:
17
+ header: The title to use for the summary
18
+ context: The GraphContext object to format
19
+
20
+ Returns:
21
+ Formatted markdown string with the context summary
22
+ """
23
+ summary = []
24
+
25
+ # Extract URI for reference
26
+ uri = context.metadata.uri or "a/permalink-value"
27
+
28
+ # Add header
29
+ summary.append(f"{header}")
30
+ summary.append("")
31
+
32
+ # Primary document section
33
+ if context.primary_results:
34
+ summary.append(f"## Primary Documents ({len(context.primary_results)})")
35
+
36
+ for primary in context.primary_results:
37
+ summary.append(f"### {primary.title}")
38
+ summary.append(f"- **Type**: {primary.type}")
39
+ summary.append(f"- **Path**: {primary.file_path}")
40
+ summary.append(f"- **Created**: {primary.created_at.strftime('%Y-%m-%d %H:%M')}")
41
+ summary.append("")
42
+ summary.append(
43
+ f'To view this document\'s content: `read_note("{primary.permalink}")` or `read_note("{primary.title}")` '
44
+ )
45
+ summary.append("")
46
+ else:
47
+ summary.append("\nNo primary documents found.")
48
+
49
+ # Related documents section
50
+ if context.related_results:
51
+ summary.append(f"## Related Documents ({len(context.related_results)})")
52
+
53
+ # Group by relation type for better organization
54
+ relation_types = {}
55
+ for rel in context.related_results:
56
+ if hasattr(rel, "relation_type"):
57
+ rel_type = rel.relation_type # pyright: ignore
58
+ if rel_type not in relation_types:
59
+ relation_types[rel_type] = []
60
+ relation_types[rel_type].append(rel)
61
+
62
+ # Display relations grouped by type
63
+ for rel_type, relations in relation_types.items():
64
+ summary.append(f"### {rel_type.replace('_', ' ').title()} ({len(relations)})")
65
+
66
+ for rel in relations:
67
+ if hasattr(rel, "to_id") and rel.to_id:
68
+ summary.append(f"- **{rel.to_id}**")
69
+ summary.append(f' - View document: `read_note("{rel.to_id}")` ')
70
+ summary.append(
71
+ f' - Explore connections: `build_context("memory://{rel.to_id}")` '
72
+ )
73
+ else:
74
+ summary.append(f"- **Unresolved relation**: {rel.permalink}")
75
+ summary.append("")
76
+
77
+ # Next steps section
78
+ summary.append("## Next Steps")
79
+ summary.append("Here are some ways to explore further:")
80
+
81
+ search_term = uri.split("/")[-1]
82
+ summary.append(f'- **Search related topics**: `search({{"text": "{search_term}"}})`')
83
+
84
+ summary.append('- **Check recent changes**: `recent_activity(timeframe="3 days")`')
85
+ summary.append(f'- **Explore all relations**: `build_context("memory://{uri}/*")`')
86
+
87
+ # Tips section
88
+ summary.append("")
89
+ summary.append("## Tips")
90
+ summary.append(
91
+ f'- For more specific context, increase depth: `build_context("memory://{uri}", depth=2)`'
92
+ )
93
+ summary.append(
94
+ "- You can follow specific relation types using patterns like: `memory://document/relation-type/*`"
95
+ )
96
+ summary.append("- Look for connected documents by checking relations between them")
97
+
98
+ return "\n".join(summary)
@@ -1,15 +1,11 @@
1
1
  """Enhanced FastMCP server instance for Basic Memory."""
2
2
 
3
3
  from mcp.server.fastmcp import FastMCP
4
-
5
- from basic_memory.utils import setup_logging
4
+ from mcp.server.fastmcp.utilities.logging import configure_logging
6
5
 
7
6
  # mcp console logging
8
- # configure_logging(level='INFO')
9
-
7
+ configure_logging(level="INFO")
10
8
 
11
- # start our out file logging
12
- setup_logging(log_file=".basic-memory/basic-memory.log")
13
9
 
14
10
  # Create the shared server instance
15
- mcp = FastMCP("Basic Memory")
11
+ mcp = FastMCP("Basic Memory")
@@ -6,11 +6,11 @@ all tools with the MCP server.
6
6
  """
7
7
 
8
8
  # Import tools to register them with MCP
9
+ from basic_memory.mcp.tools.resource import read_resource
9
10
  from basic_memory.mcp.tools.memory import build_context, recent_activity
10
-
11
- # from basic_memory.mcp.tools.ai_edit import ai_edit
12
11
  from basic_memory.mcp.tools.notes import read_note, write_note
13
12
  from basic_memory.mcp.tools.search import search
13
+ from basic_memory.mcp.tools.canvas import canvas
14
14
 
15
15
  from basic_memory.mcp.tools.knowledge import (
16
16
  delete_entities,
@@ -31,6 +31,8 @@ __all__ = [
31
31
  # notes
32
32
  "read_note",
33
33
  "write_note",
34
- # file edit
35
- # "ai_edit",
34
+ # files
35
+ "read_resource",
36
+ # canvas
37
+ "canvas",
36
38
  ]
@@ -0,0 +1,99 @@
1
+ """Canvas creation tool for Basic Memory MCP server.
2
+
3
+ This tool creates Obsidian canvas files (.canvas) using the JSON Canvas 1.0 spec.
4
+ """
5
+
6
+ import json
7
+ from typing import Dict, List, Any
8
+
9
+ import logfire
10
+ from loguru import logger
11
+
12
+ from basic_memory.mcp.async_client import client
13
+ from basic_memory.mcp.server import mcp
14
+ from basic_memory.mcp.tools.utils import call_put
15
+
16
+
17
+ @mcp.tool(
18
+ description="Create an Obsidian canvas file to visualize concepts and connections.",
19
+ )
20
+ async def canvas(
21
+ nodes: List[Dict[str, Any]],
22
+ edges: List[Dict[str, Any]],
23
+ title: str,
24
+ folder: str,
25
+ ) -> str:
26
+ """Create an Obsidian canvas file with the provided nodes and edges.
27
+
28
+ This tool creates a .canvas file compatible with Obsidian's Canvas feature,
29
+ allowing visualization of relationships between concepts or documents.
30
+
31
+ For the full JSON Canvas 1.0 specification, see the 'spec://canvas' resource.
32
+
33
+ Args:
34
+ nodes: List of node objects following JSON Canvas 1.0 spec
35
+ edges: List of edge objects following JSON Canvas 1.0 spec
36
+ title: The title of the canvas (will be saved as title.canvas)
37
+ folder: The folder where the file should be saved
38
+
39
+ Returns:
40
+ A summary of the created canvas file
41
+
42
+ Important Notes:
43
+ - When referencing files, use the exact file path as shown in Obsidian
44
+ Example: "folder/Document Name.md" (not permalink format)
45
+ - For file nodes, the "file" attribute must reference an existing file
46
+ - Nodes require id, type, x, y, width, height properties
47
+ - Edges require id, fromNode, toNode properties
48
+ - Position nodes in a logical layout (x,y coordinates in pixels)
49
+ - Use color attributes ("1"-"6" or hex) for visual organization
50
+
51
+ Basic Structure:
52
+ ```json
53
+ {
54
+ "nodes": [
55
+ {
56
+ "id": "node1",
57
+ "type": "file", // Options: "file", "text", "link", "group"
58
+ "file": "folder/Document.md",
59
+ "x": 0,
60
+ "y": 0,
61
+ "width": 400,
62
+ "height": 300
63
+ }
64
+ ],
65
+ "edges": [
66
+ {
67
+ "id": "edge1",
68
+ "fromNode": "node1",
69
+ "toNode": "node2",
70
+ "label": "connects to"
71
+ }
72
+ ]
73
+ }
74
+ ```
75
+ """
76
+ with logfire.span("Creating canvas", folder=folder, title=title): # type: ignore
77
+ # Ensure path has .canvas extension
78
+ file_title = title if title.endswith(".canvas") else f"{title}.canvas"
79
+ file_path = f"{folder}/{file_title}"
80
+
81
+ # Create canvas data structure
82
+ canvas_data = {"nodes": nodes, "edges": edges}
83
+
84
+ # Convert to JSON
85
+ canvas_json = json.dumps(canvas_data, indent=2)
86
+
87
+ # Write the file using the resource API
88
+ logger.info(f"Creating canvas file: {file_path}")
89
+ response = await call_put(client, f"/resource/{file_path}", json=canvas_json)
90
+
91
+ # Parse response
92
+ result = response.json()
93
+ logger.debug(result)
94
+
95
+ # Build summary
96
+ action = "Created" if response.status_code == 201 else "Updated"
97
+ summary = [f"# {action}: {file_path}", "\nThe canvas is ready to open in Obsidian."]
98
+
99
+ return "\n".join(summary)
@@ -1,8 +1,10 @@
1
1
  """Knowledge graph management tools for Basic Memory MCP server."""
2
2
 
3
+ import logfire
4
+
3
5
  from basic_memory.mcp.server import mcp
4
6
  from basic_memory.mcp.tools.utils import call_get, call_post
5
- from basic_memory.schemas.base import Permalink
7
+ from basic_memory.schemas.memory import memory_url_path
6
8
  from basic_memory.schemas.request import (
7
9
  GetEntitiesRequest,
8
10
  )
@@ -16,15 +18,17 @@ from basic_memory.mcp.async_client import client
16
18
  @mcp.tool(
17
19
  description="Get complete information about a specific entity including observations and relations",
18
20
  )
19
- async def get_entity(permalink: Permalink) -> EntityResponse:
21
+ async def get_entity(identifier: str) -> EntityResponse:
20
22
  """Get a specific entity info by its permalink.
21
23
 
22
24
  Args:
23
- permalink: Path identifier for the entity
25
+ identifier: Path identifier for the entity
24
26
  """
25
- url = f"/knowledge/entities/{permalink}"
26
- response = await call_get(client, url)
27
- return EntityResponse.model_validate(response.json())
27
+ with logfire.span("Getting entity", permalink=identifier): # pyright: ignore [reportGeneralTypeIssues]
28
+ permalink = memory_url_path(identifier)
29
+ url = f"/knowledge/entities/{permalink}"
30
+ response = await call_get(client, url)
31
+ return EntityResponse.model_validate(response.json())
28
32
 
29
33
 
30
34
  @mcp.tool(
@@ -39,11 +43,16 @@ async def get_entities(request: GetEntitiesRequest) -> EntityListResponse:
39
43
  Returns:
40
44
  EntityListResponse containing complete details for each requested entity
41
45
  """
42
- url = "/knowledge/entities"
43
- response = await call_get(
44
- client, url, params=[("permalink", permalink) for permalink in request.permalinks]
45
- )
46
- return EntityListResponse.model_validate(response.json())
46
+ with logfire.span("Getting multiple entities", permalink_count=len(request.permalinks)): # pyright: ignore [reportGeneralTypeIssues]
47
+ url = "/knowledge/entities"
48
+ response = await call_get(
49
+ client,
50
+ url,
51
+ params=[
52
+ ("permalink", memory_url_path(identifier)) for identifier in request.permalinks
53
+ ],
54
+ )
55
+ return EntityListResponse.model_validate(response.json())
47
56
 
48
57
 
49
58
  @mcp.tool(
@@ -51,6 +60,9 @@ async def get_entities(request: GetEntitiesRequest) -> EntityListResponse:
51
60
  )
52
61
  async def delete_entities(request: DeleteEntitiesRequest) -> DeleteEntitiesResponse:
53
62
  """Delete entities from the knowledge graph."""
54
- url = "/knowledge/entities/delete"
55
- response = await call_post(client, url, json=request.model_dump())
56
- return DeleteEntitiesResponse.model_validate(response.json())
63
+ with logfire.span("Deleting entities", permalink_count=len(request.permalinks)): # pyright: ignore [reportGeneralTypeIssues]
64
+ url = "/knowledge/entities/delete"
65
+
66
+ request.permalinks = [memory_url_path(permlink) for permlink in request.permalinks]
67
+ response = await call_post(client, url, json=request.model_dump())
68
+ return DeleteEntitiesResponse.model_validate(response.json())
@@ -1,8 +1,9 @@
1
1
  """Discussion context tools for Basic Memory MCP server."""
2
2
 
3
- from typing import Optional, Literal, List
3
+ from typing import Optional, List
4
4
 
5
5
  from loguru import logger
6
+ import logfire
6
7
 
7
8
  from basic_memory.mcp.async_client import client
8
9
  from basic_memory.mcp.server import mcp
@@ -14,6 +15,7 @@ from basic_memory.schemas.memory import (
14
15
  normalize_memory_url,
15
16
  )
16
17
  from basic_memory.schemas.base import TimeFrame
18
+ from basic_memory.schemas.search import SearchItemType
17
19
 
18
20
 
19
21
  @mcp.tool(
@@ -32,7 +34,9 @@ async def build_context(
32
34
  url: MemoryUrl,
33
35
  depth: Optional[int] = 1,
34
36
  timeframe: Optional[TimeFrame] = "7d",
35
- max_results: int = 10,
37
+ page: int = 1,
38
+ page_size: int = 10,
39
+ max_related: int = 10,
36
40
  ) -> GraphContext:
37
41
  """Get context needed to continue a discussion.
38
42
 
@@ -44,7 +48,9 @@ async def build_context(
44
48
  url: memory:// URI pointing to discussion content (e.g. memory://specs/search)
45
49
  depth: How many relation hops to traverse (1-3 recommended for performance)
46
50
  timeframe: How far back to look. Supports natural language like "2 days ago", "last week"
47
- max_results: Maximum number of results to return (default: 10)
51
+ page: Page number of results to return (default: 1)
52
+ page_size: Number of results to return per page (default: 10)
53
+ max_related: Maximum number of related results to return (default: 10)
48
54
 
49
55
  Returns:
50
56
  GraphContext containing:
@@ -65,14 +71,21 @@ async def build_context(
65
71
  # Research the history of a feature
66
72
  build_context("memory://features/knowledge-graph", timeframe="3 months ago")
67
73
  """
68
- logger.info(f"Building context from {url}")
69
- url = normalize_memory_url(url)
70
- response = await call_get(
71
- client,
72
- f"/memory/{memory_url_path(url)}",
73
- params={"depth": depth, "timeframe": timeframe, "max_results": max_results},
74
- )
75
- return GraphContext.model_validate(response.json())
74
+ with logfire.span("Building context", url=url, depth=depth, timeframe=timeframe): # pyright: ignore [reportGeneralTypeIssues]
75
+ logger.info(f"Building context from {url}")
76
+ url = normalize_memory_url(url)
77
+ response = await call_get(
78
+ client,
79
+ f"/memory/{memory_url_path(url)}",
80
+ params={
81
+ "depth": depth,
82
+ "timeframe": timeframe,
83
+ "page": page,
84
+ "page_size": page_size,
85
+ "max_related": max_related,
86
+ },
87
+ )
88
+ return GraphContext.model_validate(response.json())
76
89
 
77
90
 
78
91
  @mcp.tool(
@@ -88,10 +101,12 @@ async def build_context(
88
101
  """,
89
102
  )
90
103
  async def recent_activity(
91
- type: List[Literal["entity", "observation", "relation"]] = [],
104
+ type: Optional[List[SearchItemType]] = None,
92
105
  depth: Optional[int] = 1,
93
106
  timeframe: Optional[TimeFrame] = "7d",
94
- max_results: int = 10,
107
+ page: int = 1,
108
+ page_size: int = 10,
109
+ max_related: int = 10,
95
110
  ) -> GraphContext:
96
111
  """Get recent activity across the knowledge base.
97
112
 
@@ -106,7 +121,9 @@ async def recent_activity(
106
121
  - Relative: "2 days ago", "last week", "yesterday"
107
122
  - Points in time: "2024-01-01", "January 1st"
108
123
  - Standard format: "7d", "24h"
109
- max_results: Maximum number of results to return (default: 10)
124
+ page: Page number of results to return (default: 1)
125
+ page_size: Number of results to return per page (default: 10)
126
+ max_related: Maximum number of related results to return (default: 10)
110
127
 
111
128
  Returns:
112
129
  GraphContext containing:
@@ -132,20 +149,29 @@ async def recent_activity(
132
149
  - For focused queries, consider using build_context with a specific URI
133
150
  - Max timeframe is 1 year in the past
134
151
  """
135
- logger.info(
136
- f"Getting recent activity from {type}, depth={depth}, timeframe={timeframe}, max_results={max_results}"
137
- )
138
- params = {
139
- "depth": depth,
140
- "timeframe": timeframe,
141
- "max_results": max_results,
142
- }
143
- if type:
144
- params["type"] = type
145
-
146
- response = await call_get(
147
- client,
148
- "/memory/recent",
149
- params=params,
150
- )
151
- return GraphContext.model_validate(response.json())
152
+ with logfire.span("Getting recent activity", type=type, depth=depth, timeframe=timeframe): # pyright: ignore [reportGeneralTypeIssues]
153
+ logger.info(
154
+ f"Getting recent activity from {type}, depth={depth}, timeframe={timeframe}, page={page}, page_size={page_size}, max_related={max_related}"
155
+ )
156
+ params = {
157
+ "page": page,
158
+ "page_size": page_size,
159
+ "max_related": max_related,
160
+ }
161
+ if depth:
162
+ params["depth"] = depth
163
+ if timeframe:
164
+ params["timeframe"] = timeframe # pyright: ignore
165
+
166
+ # send enum values if we have an enum, else send string value
167
+ if type:
168
+ params["type"] = [ # pyright: ignore
169
+ type.value if isinstance(type, SearchItemType) else type for type in type
170
+ ]
171
+
172
+ response = await call_get(
173
+ client,
174
+ "/memory/recent",
175
+ params=params,
176
+ )
177
+ return GraphContext.model_validate(response.json())