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.
- basic_memory/__init__.py +1 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +23 -1
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/api/app.py +0 -4
- basic_memory/api/routers/knowledge_router.py +1 -9
- basic_memory/api/routers/memory_router.py +41 -25
- basic_memory/api/routers/resource_router.py +119 -12
- basic_memory/api/routers/search_router.py +17 -9
- basic_memory/cli/app.py +0 -2
- basic_memory/cli/commands/db.py +11 -8
- basic_memory/cli/commands/import_chatgpt.py +31 -27
- basic_memory/cli/commands/import_claude_conversations.py +29 -27
- basic_memory/cli/commands/import_claude_projects.py +30 -29
- basic_memory/cli/commands/import_memory_json.py +28 -26
- basic_memory/cli/commands/status.py +16 -26
- basic_memory/cli/commands/sync.py +11 -12
- basic_memory/cli/commands/tools.py +180 -0
- basic_memory/cli/main.py +1 -1
- basic_memory/config.py +16 -2
- basic_memory/db.py +1 -0
- basic_memory/deps.py +5 -1
- basic_memory/file_utils.py +6 -4
- basic_memory/markdown/entity_parser.py +3 -3
- basic_memory/mcp/async_client.py +1 -1
- basic_memory/mcp/main.py +25 -0
- basic_memory/mcp/prompts/__init__.py +15 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +28 -0
- basic_memory/mcp/prompts/continue_conversation.py +172 -0
- basic_memory/mcp/prompts/json_canvas_spec.py +25 -0
- basic_memory/mcp/prompts/recent_activity.py +46 -0
- basic_memory/mcp/prompts/search.py +127 -0
- basic_memory/mcp/prompts/utils.py +98 -0
- basic_memory/mcp/server.py +3 -7
- basic_memory/mcp/tools/__init__.py +6 -4
- basic_memory/mcp/tools/canvas.py +99 -0
- basic_memory/mcp/tools/knowledge.py +26 -14
- basic_memory/mcp/tools/memory.py +57 -31
- basic_memory/mcp/tools/notes.py +65 -72
- basic_memory/mcp/tools/resource.py +192 -0
- basic_memory/mcp/tools/search.py +13 -4
- basic_memory/mcp/tools/utils.py +2 -1
- basic_memory/models/knowledge.py +27 -11
- basic_memory/repository/repository.py +1 -1
- basic_memory/repository/search_repository.py +17 -4
- basic_memory/schemas/__init__.py +0 -11
- basic_memory/schemas/base.py +4 -1
- basic_memory/schemas/memory.py +14 -2
- basic_memory/schemas/request.py +1 -1
- basic_memory/schemas/search.py +4 -1
- basic_memory/services/context_service.py +14 -6
- basic_memory/services/entity_service.py +19 -12
- basic_memory/services/file_service.py +69 -2
- basic_memory/services/link_resolver.py +12 -9
- basic_memory/services/search_service.py +59 -13
- basic_memory/sync/__init__.py +3 -2
- basic_memory/sync/sync_service.py +287 -107
- basic_memory/sync/watch_service.py +125 -129
- basic_memory/utils.py +27 -15
- {basic_memory-0.6.0.dist-info → basic_memory-0.8.0.dist-info}/METADATA +3 -2
- basic_memory-0.8.0.dist-info/RECORD +91 -0
- basic_memory/alembic/README +0 -1
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -31
- basic_memory-0.6.0.dist-info/RECORD +0 -81
- {basic_memory-0.6.0.dist-info → basic_memory-0.8.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.6.0.dist-info → basic_memory-0.8.0.dist-info}/entry_points.txt +0 -0
- {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)
|
basic_memory/mcp/server.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
35
|
-
|
|
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.
|
|
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(
|
|
21
|
+
async def get_entity(identifier: str) -> EntityResponse:
|
|
20
22
|
"""Get a specific entity info by its permalink.
|
|
21
23
|
|
|
22
24
|
Args:
|
|
23
|
-
|
|
25
|
+
identifier: Path identifier for the entity
|
|
24
26
|
"""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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())
|
basic_memory/mcp/tools/memory.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Discussion context tools for Basic Memory MCP server."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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[
|
|
104
|
+
type: Optional[List[SearchItemType]] = None,
|
|
92
105
|
depth: Optional[int] = 1,
|
|
93
106
|
timeframe: Optional[TimeFrame] = "7d",
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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())
|