basic-memory 0.8.0__py3-none-any.whl → 0.9.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/migrations.py +4 -9
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
- basic_memory/api/app.py +9 -6
- basic_memory/api/routers/__init__.py +2 -1
- basic_memory/api/routers/knowledge_router.py +30 -4
- basic_memory/api/routers/memory_router.py +3 -2
- basic_memory/api/routers/project_info_router.py +275 -0
- basic_memory/api/routers/search_router.py +22 -4
- basic_memory/cli/app.py +54 -3
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/db.py +9 -13
- basic_memory/cli/commands/import_chatgpt.py +26 -30
- basic_memory/cli/commands/import_claude_conversations.py +27 -29
- basic_memory/cli/commands/import_claude_projects.py +29 -31
- basic_memory/cli/commands/import_memory_json.py +26 -28
- basic_memory/cli/commands/mcp.py +7 -1
- basic_memory/cli/commands/project.py +119 -0
- basic_memory/cli/commands/project_info.py +167 -0
- basic_memory/cli/commands/status.py +7 -9
- basic_memory/cli/commands/sync.py +54 -9
- basic_memory/cli/commands/{tools.py → tool.py} +92 -19
- basic_memory/cli/main.py +40 -1
- basic_memory/config.py +155 -7
- basic_memory/db.py +19 -4
- basic_memory/deps.py +10 -3
- basic_memory/file_utils.py +32 -16
- basic_memory/markdown/utils.py +5 -0
- basic_memory/mcp/main.py +1 -2
- basic_memory/mcp/prompts/__init__.py +6 -2
- basic_memory/mcp/prompts/ai_assistant_guide.py +6 -8
- basic_memory/mcp/prompts/continue_conversation.py +65 -126
- basic_memory/mcp/prompts/recent_activity.py +55 -13
- basic_memory/mcp/prompts/search.py +72 -17
- basic_memory/mcp/prompts/utils.py +139 -82
- basic_memory/mcp/server.py +1 -1
- basic_memory/mcp/tools/__init__.py +11 -22
- basic_memory/mcp/tools/build_context.py +85 -0
- basic_memory/mcp/tools/canvas.py +17 -19
- basic_memory/mcp/tools/delete_note.py +28 -0
- basic_memory/mcp/tools/project_info.py +51 -0
- basic_memory/mcp/tools/{resource.py → read_content.py} +42 -5
- basic_memory/mcp/tools/read_note.py +190 -0
- basic_memory/mcp/tools/recent_activity.py +100 -0
- basic_memory/mcp/tools/search.py +56 -17
- basic_memory/mcp/tools/utils.py +245 -17
- basic_memory/mcp/tools/write_note.py +124 -0
- basic_memory/models/search.py +2 -1
- basic_memory/repository/entity_repository.py +3 -2
- basic_memory/repository/project_info_repository.py +9 -0
- basic_memory/repository/repository.py +23 -6
- basic_memory/repository/search_repository.py +33 -10
- basic_memory/schemas/__init__.py +12 -0
- basic_memory/schemas/memory.py +3 -2
- basic_memory/schemas/project_info.py +96 -0
- basic_memory/schemas/search.py +27 -32
- basic_memory/services/context_service.py +3 -3
- basic_memory/services/entity_service.py +8 -2
- basic_memory/services/file_service.py +105 -53
- basic_memory/services/link_resolver.py +5 -45
- basic_memory/services/search_service.py +45 -16
- basic_memory/sync/sync_service.py +274 -39
- basic_memory/sync/watch_service.py +160 -30
- basic_memory/utils.py +40 -40
- basic_memory-0.9.0.dist-info/METADATA +736 -0
- basic_memory-0.9.0.dist-info/RECORD +99 -0
- basic_memory/mcp/prompts/json_canvas_spec.py +0 -25
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -177
- basic_memory/mcp/tools/notes.py +0 -201
- basic_memory-0.8.0.dist-info/METADATA +0 -379
- basic_memory-0.8.0.dist-info/RECORD +0 -91
- {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.8.0.dist-info → basic_memory-0.9.0.dist-info}/licenses/LICENSE +0 -0
basic_memory/file_utils.py
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Any, Dict, Union
|
|
6
6
|
|
|
7
7
|
import yaml
|
|
8
8
|
from loguru import logger
|
|
9
9
|
|
|
10
|
+
from basic_memory.utils import FilePath
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class FileError(Exception):
|
|
12
14
|
"""Base exception for file operations."""
|
|
@@ -48,42 +50,47 @@ async def compute_checksum(content: Union[str, bytes]) -> str:
|
|
|
48
50
|
raise FileError(f"Failed to compute checksum: {e}")
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
async def ensure_directory(path:
|
|
53
|
+
async def ensure_directory(path: FilePath) -> None:
|
|
52
54
|
"""
|
|
53
55
|
Ensure directory exists, creating if necessary.
|
|
54
56
|
|
|
55
57
|
Args:
|
|
56
|
-
path: Directory path to ensure
|
|
58
|
+
path: Directory path to ensure (Path or string)
|
|
57
59
|
|
|
58
60
|
Raises:
|
|
59
61
|
FileWriteError: If directory creation fails
|
|
60
62
|
"""
|
|
61
63
|
try:
|
|
62
|
-
|
|
64
|
+
# Convert string to Path if needed
|
|
65
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
66
|
+
path_obj.mkdir(parents=True, exist_ok=True)
|
|
63
67
|
except Exception as e: # pragma: no cover
|
|
64
|
-
logger.error(
|
|
68
|
+
logger.error("Failed to create directory", path=str(path), error=str(e))
|
|
65
69
|
raise FileWriteError(f"Failed to create directory {path}: {e}")
|
|
66
70
|
|
|
67
71
|
|
|
68
|
-
async def write_file_atomic(path:
|
|
72
|
+
async def write_file_atomic(path: FilePath, content: str) -> None:
|
|
69
73
|
"""
|
|
70
74
|
Write file with atomic operation using temporary file.
|
|
71
75
|
|
|
72
76
|
Args:
|
|
73
|
-
path: Target file path
|
|
77
|
+
path: Target file path (Path or string)
|
|
74
78
|
content: Content to write
|
|
75
79
|
|
|
76
80
|
Raises:
|
|
77
81
|
FileWriteError: If write operation fails
|
|
78
82
|
"""
|
|
79
|
-
|
|
83
|
+
# Convert string to Path if needed
|
|
84
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
85
|
+
temp_path = path_obj.with_suffix(".tmp")
|
|
86
|
+
|
|
80
87
|
try:
|
|
81
88
|
temp_path.write_text(content)
|
|
82
|
-
temp_path.replace(
|
|
83
|
-
logger.debug(
|
|
89
|
+
temp_path.replace(path_obj)
|
|
90
|
+
logger.debug("Wrote file atomically", path=str(path_obj), content_length=len(content))
|
|
84
91
|
except Exception as e: # pragma: no cover
|
|
85
92
|
temp_path.unlink(missing_ok=True)
|
|
86
|
-
logger.error(
|
|
93
|
+
logger.error("Failed to write file", path=str(path_obj), error=str(e))
|
|
87
94
|
raise FileWriteError(f"Failed to write file {path}: {e}")
|
|
88
95
|
|
|
89
96
|
|
|
@@ -173,7 +180,7 @@ def remove_frontmatter(content: str) -> str:
|
|
|
173
180
|
return parts[2].strip()
|
|
174
181
|
|
|
175
182
|
|
|
176
|
-
async def update_frontmatter(path:
|
|
183
|
+
async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
|
|
177
184
|
"""Update frontmatter fields in a file while preserving all content.
|
|
178
185
|
|
|
179
186
|
Only modifies the frontmatter section, leaving all content untouched.
|
|
@@ -181,7 +188,7 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
|
|
181
188
|
Returns checksum of updated file.
|
|
182
189
|
|
|
183
190
|
Args:
|
|
184
|
-
path: Path to markdown file
|
|
191
|
+
path: Path to markdown file (Path or string)
|
|
185
192
|
updates: Dict of frontmatter fields to update
|
|
186
193
|
|
|
187
194
|
Returns:
|
|
@@ -192,8 +199,11 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
|
|
192
199
|
ParseError: If frontmatter parsing fails
|
|
193
200
|
"""
|
|
194
201
|
try:
|
|
202
|
+
# Convert string to Path if needed
|
|
203
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
204
|
+
|
|
195
205
|
# Read current content
|
|
196
|
-
content =
|
|
206
|
+
content = path_obj.read_text()
|
|
197
207
|
|
|
198
208
|
# Parse current frontmatter
|
|
199
209
|
current_fm = {}
|
|
@@ -208,9 +218,15 @@ async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
|
|
208
218
|
yaml_fm = yaml.dump(new_fm, sort_keys=False)
|
|
209
219
|
final_content = f"---\n{yaml_fm}---\n\n{content.strip()}"
|
|
210
220
|
|
|
211
|
-
|
|
221
|
+
logger.debug("Updating frontmatter", path=str(path_obj), update_keys=list(updates.keys()))
|
|
222
|
+
|
|
223
|
+
await write_file_atomic(path_obj, final_content)
|
|
212
224
|
return await compute_checksum(final_content)
|
|
213
225
|
|
|
214
226
|
except Exception as e: # pragma: no cover
|
|
215
|
-
logger.error(
|
|
227
|
+
logger.error(
|
|
228
|
+
"Failed to update frontmatter",
|
|
229
|
+
path=str(path) if isinstance(path, (str, Path)) else "<unknown>",
|
|
230
|
+
error=str(e),
|
|
231
|
+
)
|
|
216
232
|
raise FileError(f"Failed to update frontmatter: {e}")
|
basic_memory/markdown/utils.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import Optional, Any
|
|
|
5
5
|
|
|
6
6
|
from frontmatter import Post
|
|
7
7
|
|
|
8
|
+
from basic_memory.file_utils import has_frontmatter, remove_frontmatter
|
|
8
9
|
from basic_memory.markdown import EntityMarkdown
|
|
9
10
|
from basic_memory.models import Entity, Observation as ObservationModel
|
|
10
11
|
from basic_memory.utils import generate_permalink
|
|
@@ -78,6 +79,10 @@ async def schema_to_markdown(schema: Any) -> Post:
|
|
|
78
79
|
content = schema.content or ""
|
|
79
80
|
frontmatter_metadata = dict(schema.entity_metadata or {})
|
|
80
81
|
|
|
82
|
+
# if the content contains frontmatter, remove it and merge
|
|
83
|
+
if has_frontmatter(content):
|
|
84
|
+
content = remove_frontmatter(content)
|
|
85
|
+
|
|
81
86
|
# Remove special fields for ordered frontmatter
|
|
82
87
|
for field in ["type", "title", "permalink"]:
|
|
83
88
|
frontmatter_metadata.pop(field, None)
|
basic_memory/mcp/main.py
CHANGED
|
@@ -17,9 +17,8 @@ import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
|
|
|
17
17
|
import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
if __name__ == "__main__": # pragma: no cover
|
|
22
21
|
home_dir = config.home
|
|
23
22
|
logger.info("Starting Basic Memory MCP server")
|
|
24
23
|
logger.info(f"Home directory: {home_dir}")
|
|
25
|
-
mcp.run()
|
|
24
|
+
mcp.run()
|
|
@@ -10,6 +10,10 @@ from basic_memory.mcp.prompts import continue_conversation
|
|
|
10
10
|
from basic_memory.mcp.prompts import recent_activity
|
|
11
11
|
from basic_memory.mcp.prompts import search
|
|
12
12
|
from basic_memory.mcp.prompts import ai_assistant_guide
|
|
13
|
-
from basic_memory.mcp.prompts import json_canvas_spec
|
|
14
13
|
|
|
15
|
-
__all__ = [
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ai_assistant_guide",
|
|
16
|
+
"continue_conversation",
|
|
17
|
+
"recent_activity",
|
|
18
|
+
"search",
|
|
19
|
+
]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
import logfire
|
|
4
3
|
from loguru import logger
|
|
5
4
|
|
|
6
5
|
from basic_memory.mcp.server import mcp
|
|
@@ -8,7 +7,7 @@ from basic_memory.mcp.server import mcp
|
|
|
8
7
|
|
|
9
8
|
@mcp.resource(
|
|
10
9
|
uri="memory://ai_assistant_guide",
|
|
11
|
-
name="
|
|
10
|
+
name="ai assistant guide",
|
|
12
11
|
description="Give an AI assistant guidance on how to use Basic Memory tools effectively",
|
|
13
12
|
)
|
|
14
13
|
def ai_assistant_guide() -> str:
|
|
@@ -20,9 +19,8 @@ def ai_assistant_guide() -> str:
|
|
|
20
19
|
Returns:
|
|
21
20
|
A focused guide on Basic Memory usage.
|
|
22
21
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return content
|
|
22
|
+
logger.info("Loading AI assistant guide resource")
|
|
23
|
+
guide_doc = Path(__file__).parent.parent.parent.parent.parent / "static" / "ai_assistant_guide.md"
|
|
24
|
+
content = guide_doc.read_text()
|
|
25
|
+
logger.info(f"Loaded AI assistant guide ({len(content)} chars)")
|
|
26
|
+
return content
|
|
@@ -5,22 +5,23 @@ providing context from previous interactions to maintain continuity.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from textwrap import dedent
|
|
8
|
-
from typing import Optional,
|
|
8
|
+
from typing import Optional, Annotated
|
|
9
9
|
|
|
10
10
|
from loguru import logger
|
|
11
|
-
import logfire
|
|
12
11
|
from pydantic import Field
|
|
13
12
|
|
|
13
|
+
from basic_memory.mcp.prompts.utils import format_prompt_context, PromptContext, PromptContextItem
|
|
14
14
|
from basic_memory.mcp.server import mcp
|
|
15
|
-
from basic_memory.mcp.tools.
|
|
15
|
+
from basic_memory.mcp.tools.build_context import build_context
|
|
16
|
+
from basic_memory.mcp.tools.recent_activity import recent_activity
|
|
16
17
|
from basic_memory.mcp.tools.search import search
|
|
17
18
|
from basic_memory.schemas.base import TimeFrame
|
|
18
19
|
from basic_memory.schemas.memory import GraphContext
|
|
19
|
-
from basic_memory.schemas.search import SearchQuery
|
|
20
|
+
from basic_memory.schemas.search import SearchQuery, SearchItemType
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
@mcp.prompt(
|
|
23
|
-
name="
|
|
24
|
+
name="Continue Conversation",
|
|
24
25
|
description="Continue a previous conversation",
|
|
25
26
|
)
|
|
26
27
|
async def continue_conversation(
|
|
@@ -42,131 +43,69 @@ async def continue_conversation(
|
|
|
42
43
|
Returns:
|
|
43
44
|
Context from previous sessions on this topic
|
|
44
45
|
"""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
logger.info(f"Continuing session, topic: {topic}, timeframe: {timeframe}")
|
|
47
|
+
|
|
48
|
+
# If topic provided, search for it
|
|
49
|
+
if topic:
|
|
50
|
+
search_results = await search(
|
|
51
|
+
SearchQuery(text=topic, after_date=timeframe, types=[SearchItemType.ENTITY])
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Build context from results
|
|
55
|
+
contexts = []
|
|
56
|
+
for result in search_results.results:
|
|
57
|
+
if hasattr(result, "permalink") and result.permalink:
|
|
58
|
+
context: GraphContext = await build_context(f"memory://{result.permalink}")
|
|
59
|
+
if context.primary_results:
|
|
60
|
+
contexts.append(
|
|
61
|
+
PromptContextItem(
|
|
62
|
+
primary_results=context.primary_results[:1], # pyright: ignore
|
|
63
|
+
related_results=context.related_results[:3], # pyright: ignore
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# get context for the top 3 results
|
|
68
|
+
prompt_context = format_prompt_context(
|
|
69
|
+
PromptContext(topic=topic, timeframe=timeframe, results=contexts) # pyright: ignore
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
else:
|
|
61
73
|
# If no topic, get recent activity
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return dedent(f"""
|
|
81
|
-
# Continuing conversation on: {topic}
|
|
82
|
-
|
|
83
|
-
This is a memory retrieval session.
|
|
84
|
-
Please use the available basic-memory tools to gather relevant context before responding.
|
|
85
|
-
Start by executing one of the suggested commands below to retrieve content.
|
|
86
|
-
|
|
87
|
-
I couldn't find any recent work specifically on this topic.
|
|
88
|
-
|
|
89
|
-
## Suggestions
|
|
90
|
-
- Try a different search term
|
|
91
|
-
- Check recent activity with `recent_activity(timeframe="1w")`
|
|
92
|
-
- Start a new topic with `write_note(...)`
|
|
93
|
-
""")
|
|
94
|
-
|
|
95
|
-
# Start building our summary with header
|
|
96
|
-
summary = dedent(f"""
|
|
97
|
-
# Continuing conversation on: {topic}
|
|
98
|
-
|
|
99
|
-
This is a memory retrieval session.
|
|
100
|
-
Please use the available basic-memory tools to gather relevant context before responding.
|
|
101
|
-
Start by executing one of the suggested commands below to retrieve content.
|
|
102
|
-
|
|
103
|
-
Here's what I found about the previous conversation:
|
|
104
|
-
""")
|
|
105
|
-
|
|
106
|
-
# Track what we've added to avoid duplicates
|
|
107
|
-
added_permalinks = set()
|
|
108
|
-
sections = []
|
|
109
|
-
|
|
110
|
-
# Process each context
|
|
111
|
-
for context in contexts:
|
|
112
|
-
# Add primary results
|
|
113
|
-
for primary in context.primary_results:
|
|
114
|
-
if hasattr(primary, "permalink") and primary.permalink not in added_permalinks:
|
|
115
|
-
added_permalinks.add(primary.permalink)
|
|
116
|
-
|
|
117
|
-
section = dedent(f"""
|
|
118
|
-
## {primary.title}
|
|
119
|
-
- **Type**: {primary.type}
|
|
120
|
-
""")
|
|
121
|
-
|
|
122
|
-
# Add creation date if available
|
|
123
|
-
if hasattr(primary, "created_at"):
|
|
124
|
-
section += f"- **Created**: {primary.created_at.strftime('%Y-%m-%d %H:%M')}\n"
|
|
125
|
-
|
|
126
|
-
section += dedent(f"""
|
|
127
|
-
|
|
128
|
-
You can read this document with: `read_note("{primary.permalink}")`
|
|
129
|
-
""")
|
|
130
|
-
|
|
131
|
-
# Add related documents if available
|
|
132
|
-
related_by_type = {}
|
|
133
|
-
if context.related_results:
|
|
134
|
-
for related in context.related_results:
|
|
135
|
-
if hasattr(related, "relation_type") and related.relation_type: # pyright: ignore
|
|
136
|
-
if related.relation_type not in related_by_type: # pyright: ignore
|
|
137
|
-
related_by_type[related.relation_type] = [] # pyright: ignore
|
|
138
|
-
related_by_type[related.relation_type].append(related) # pyright: ignore
|
|
139
|
-
|
|
140
|
-
if related_by_type:
|
|
141
|
-
section += dedent("""
|
|
142
|
-
### Related Documents
|
|
143
|
-
""")
|
|
144
|
-
for rel_type, relations in related_by_type.items():
|
|
145
|
-
display_type = rel_type.replace("_", " ").title()
|
|
146
|
-
section += f"- **{display_type}**:\n"
|
|
147
|
-
for rel in relations[:3]: # Limit to avoid overwhelming
|
|
148
|
-
if hasattr(rel, "to_id") and rel.to_id:
|
|
149
|
-
section += f" - `{rel.to_id}`\n"
|
|
150
|
-
|
|
151
|
-
sections.append(section)
|
|
152
|
-
|
|
153
|
-
# Add all sections
|
|
154
|
-
summary += "\n".join(sections)
|
|
155
|
-
|
|
156
|
-
# Add next steps
|
|
74
|
+
timeframe = timeframe or "7d"
|
|
75
|
+
recent: GraphContext = await recent_activity(
|
|
76
|
+
timeframe=timeframe, type=[SearchItemType.ENTITY]
|
|
77
|
+
)
|
|
78
|
+
prompt_context = format_prompt_context(
|
|
79
|
+
PromptContext(
|
|
80
|
+
topic=f"Recent Activity from ({timeframe})",
|
|
81
|
+
timeframe=timeframe,
|
|
82
|
+
results=[
|
|
83
|
+
PromptContextItem(
|
|
84
|
+
primary_results=recent.primary_results[:5], # pyright: ignore
|
|
85
|
+
related_results=recent.related_results[:2], # pyright: ignore
|
|
86
|
+
)
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Add next steps with strong encouragement to write
|
|
157
92
|
next_steps = dedent(f"""
|
|
158
93
|
## Next Steps
|
|
159
|
-
|
|
94
|
+
|
|
160
95
|
You can:
|
|
161
96
|
- Explore more with: `search({{"text": "{topic}"}})`
|
|
162
|
-
- See what's changed: `recent_activity(timeframe="{timeframe}")`
|
|
97
|
+
- See what's changed: `recent_activity(timeframe="{timeframe or "7d"}")`
|
|
98
|
+
- **Record new learnings or decisions from this conversation:** `write_note(title="[Create a meaningful title]", content="[Content with observations and relations]")`
|
|
99
|
+
|
|
100
|
+
## Knowledge Capture Recommendation
|
|
101
|
+
|
|
102
|
+
As you continue this conversation, **actively look for opportunities to:**
|
|
103
|
+
1. Record key information, decisions, or insights that emerge
|
|
104
|
+
2. Link new knowledge to existing topics
|
|
105
|
+
3. Suggest capturing important context when appropriate
|
|
106
|
+
4. Create forward references to topics that might be created later
|
|
107
|
+
|
|
108
|
+
Remember that capturing knowledge during conversations is one of the most valuable aspects of Basic Memory.
|
|
163
109
|
""")
|
|
164
110
|
|
|
165
|
-
|
|
166
|
-
if added_permalinks:
|
|
167
|
-
first_permalink = next(iter(added_permalinks))
|
|
168
|
-
next_steps += dedent(f"""
|
|
169
|
-
- Continue the conversation: `build_context("memory://{first_permalink}")`
|
|
170
|
-
""")
|
|
171
|
-
|
|
172
|
-
return summary + next_steps
|
|
111
|
+
return prompt_context + next_steps
|
|
@@ -3,27 +3,27 @@
|
|
|
3
3
|
These prompts help users see what has changed in their knowledge base recently.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import Annotated
|
|
6
|
+
from typing import Annotated
|
|
7
7
|
|
|
8
8
|
from loguru import logger
|
|
9
|
-
import logfire
|
|
10
9
|
from pydantic import Field
|
|
11
10
|
|
|
12
|
-
from basic_memory.mcp.prompts.utils import
|
|
11
|
+
from basic_memory.mcp.prompts.utils import format_prompt_context, PromptContext, PromptContextItem
|
|
13
12
|
from basic_memory.mcp.server import mcp
|
|
14
|
-
from basic_memory.mcp.tools.
|
|
13
|
+
from basic_memory.mcp.tools.recent_activity import recent_activity
|
|
15
14
|
from basic_memory.schemas.base import TimeFrame
|
|
15
|
+
from basic_memory.schemas.search import SearchItemType
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@mcp.prompt(
|
|
19
|
-
name="
|
|
19
|
+
name="Share Recent Activity",
|
|
20
20
|
description="Get recent activity from across the knowledge base",
|
|
21
21
|
)
|
|
22
22
|
async def recent_activity_prompt(
|
|
23
23
|
timeframe: Annotated[
|
|
24
|
-
|
|
24
|
+
TimeFrame,
|
|
25
25
|
Field(description="How far back to look for activity (e.g. '1d', '1 week')"),
|
|
26
|
-
] =
|
|
26
|
+
] = "7d",
|
|
27
27
|
) -> str:
|
|
28
28
|
"""Get recent activity from across the knowledge base.
|
|
29
29
|
|
|
@@ -36,11 +36,53 @@ async def recent_activity_prompt(
|
|
|
36
36
|
Returns:
|
|
37
37
|
Formatted summary of recent activity
|
|
38
38
|
"""
|
|
39
|
-
|
|
40
|
-
logger.info(f"Getting recent activity, timeframe: {timeframe}")
|
|
39
|
+
logger.info(f"Getting recent activity, timeframe: {timeframe}")
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
recent = await recent_activity(timeframe=timeframe, type=[SearchItemType.ENTITY])
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
prompt_context = format_prompt_context(
|
|
44
|
+
PromptContext(
|
|
45
|
+
topic=f"Recent Activity from ({timeframe})",
|
|
46
|
+
timeframe=timeframe,
|
|
47
|
+
results=[
|
|
48
|
+
PromptContextItem(
|
|
49
|
+
primary_results=recent.primary_results[:5],
|
|
50
|
+
related_results=recent.related_results[:2],
|
|
51
|
+
)
|
|
52
|
+
],
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Add suggestions for summarizing recent activity
|
|
57
|
+
capture_suggestions = f"""
|
|
58
|
+
## Opportunity to Capture Activity Summary
|
|
59
|
+
|
|
60
|
+
Consider creating a summary note of recent activity:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
await write_note(
|
|
64
|
+
title="Activity Summary {timeframe}",
|
|
65
|
+
content='''
|
|
66
|
+
# Activity Summary for {timeframe}
|
|
67
|
+
|
|
68
|
+
## Overview
|
|
69
|
+
[Summary of key changes and developments over this period]
|
|
70
|
+
|
|
71
|
+
## Key Updates
|
|
72
|
+
[List main updates and their significance]
|
|
73
|
+
|
|
74
|
+
## Observations
|
|
75
|
+
- [trend] [Observation about patterns in recent activity]
|
|
76
|
+
- [insight] [Connection between different activities]
|
|
77
|
+
|
|
78
|
+
## Relations
|
|
79
|
+
- summarizes [[{recent.primary_results[0].title if recent.primary_results else "Recent Topic"}]]
|
|
80
|
+
- relates_to [[Project Overview]]
|
|
81
|
+
'''
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Summarizing periodic activity helps create high-level insights and connections between topics.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
return prompt_context + capture_suggestions
|
|
@@ -7,17 +7,16 @@ from textwrap import dedent
|
|
|
7
7
|
from typing import Annotated, Optional
|
|
8
8
|
|
|
9
9
|
from loguru import logger
|
|
10
|
-
import logfire
|
|
11
10
|
from pydantic import Field
|
|
12
11
|
|
|
13
12
|
from basic_memory.mcp.server import mcp
|
|
14
13
|
from basic_memory.mcp.tools.search import search as search_tool
|
|
15
|
-
from basic_memory.schemas.search import SearchQuery, SearchResponse
|
|
16
14
|
from basic_memory.schemas.base import TimeFrame
|
|
15
|
+
from basic_memory.schemas.search import SearchQuery, SearchResponse
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
@mcp.prompt(
|
|
20
|
-
name="
|
|
19
|
+
name="Search Knowledge Base",
|
|
21
20
|
description="Search across all content in basic-memory",
|
|
22
21
|
)
|
|
23
22
|
async def search_prompt(
|
|
@@ -39,11 +38,10 @@ async def search_prompt(
|
|
|
39
38
|
Returns:
|
|
40
39
|
Formatted search results with context
|
|
41
40
|
"""
|
|
42
|
-
|
|
43
|
-
logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
|
|
41
|
+
logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
search_results = await search_tool(SearchQuery(text=query, after_date=timeframe))
|
|
44
|
+
return format_search_results(query, search_results, timeframe)
|
|
47
45
|
|
|
48
46
|
|
|
49
47
|
def format_search_results(
|
|
@@ -65,11 +63,33 @@ def format_search_results(
|
|
|
65
63
|
|
|
66
64
|
I couldn't find any results for this query.
|
|
67
65
|
|
|
68
|
-
##
|
|
66
|
+
## Opportunity to Capture Knowledge!
|
|
67
|
+
|
|
68
|
+
This is an excellent opportunity to create new knowledge on this topic. Consider:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
await write_note(
|
|
72
|
+
title="{query.capitalize()}",
|
|
73
|
+
content=f'''
|
|
74
|
+
# {query.capitalize()}
|
|
75
|
+
|
|
76
|
+
## Overview
|
|
77
|
+
[Summary of what we've discussed about {query}]
|
|
78
|
+
|
|
79
|
+
## Observations
|
|
80
|
+
- [category] [First observation about {query}]
|
|
81
|
+
- [category] [Second observation about {query}]
|
|
82
|
+
|
|
83
|
+
## Relations
|
|
84
|
+
- relates_to [[Other Relevant Topic]]
|
|
85
|
+
'''
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Other Suggestions
|
|
69
90
|
- Try a different search term
|
|
70
91
|
- Broaden your search criteria
|
|
71
92
|
- Check recent activity with `recent_activity(timeframe="1w")`
|
|
72
|
-
- Create new content with `write_note(...)`
|
|
73
93
|
""")
|
|
74
94
|
|
|
75
95
|
# Start building our summary with header
|
|
@@ -88,32 +108,38 @@ def format_search_results(
|
|
|
88
108
|
for i, result in enumerate(results.results[:5]): # Limit to top 5 results
|
|
89
109
|
summary += dedent(f"""
|
|
90
110
|
## {i + 1}. {result.title}
|
|
91
|
-
- **Type**: {result.type}
|
|
111
|
+
- **Type**: {result.type.value}
|
|
92
112
|
""")
|
|
93
113
|
|
|
94
114
|
# Add creation date if available in metadata
|
|
95
|
-
if
|
|
115
|
+
if result.metadata and "created_at" in result.metadata:
|
|
96
116
|
created_at = result.metadata["created_at"]
|
|
97
117
|
if hasattr(created_at, "strftime"):
|
|
98
|
-
summary +=
|
|
118
|
+
summary += (
|
|
119
|
+
f"- **Created**: {created_at.strftime('%Y-%m-%d %H:%M')}\n" # pragma: no cover
|
|
120
|
+
)
|
|
99
121
|
elif isinstance(created_at, str):
|
|
100
122
|
summary += f"- **Created**: {created_at}\n"
|
|
101
123
|
|
|
102
124
|
# Add score and excerpt
|
|
103
125
|
summary += f"- **Relevance Score**: {result.score:.2f}\n"
|
|
126
|
+
|
|
104
127
|
# Add excerpt if available in metadata
|
|
105
|
-
if
|
|
106
|
-
summary += f"- **Excerpt
|
|
128
|
+
if result.content:
|
|
129
|
+
summary += f"- **Excerpt**:\n{result.content}\n"
|
|
107
130
|
|
|
108
131
|
# Add permalink for retrieving content
|
|
109
|
-
if
|
|
132
|
+
if result.permalink:
|
|
110
133
|
summary += dedent(f"""
|
|
111
|
-
|
|
112
134
|
You can view this content with: `read_note("{result.permalink}")`
|
|
113
135
|
Or explore its context with: `build_context("memory://{result.permalink}")`
|
|
114
136
|
""")
|
|
137
|
+
else:
|
|
138
|
+
summary += dedent(f"""
|
|
139
|
+
You can view this file with: `read_file("{result.file_path}")`
|
|
140
|
+
""") # pragma: no cover
|
|
115
141
|
|
|
116
|
-
# Add next steps
|
|
142
|
+
# Add next steps with strong write encouragement
|
|
117
143
|
summary += dedent(f"""
|
|
118
144
|
## Next Steps
|
|
119
145
|
|
|
@@ -122,6 +148,35 @@ def format_search_results(
|
|
|
122
148
|
- Exclude terms: `search("{query} NOT exclude_term")`
|
|
123
149
|
- View more results: `search("{query}", after_date=None)`
|
|
124
150
|
- Check recent activity: `recent_activity()`
|
|
151
|
+
|
|
152
|
+
## Synthesize and Capture Knowledge
|
|
153
|
+
|
|
154
|
+
Consider creating a new note that synthesizes what you've learned:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
await write_note(
|
|
158
|
+
title="Synthesis of {query.capitalize()} Information",
|
|
159
|
+
content='''
|
|
160
|
+
# Synthesis of {query.capitalize()} Information
|
|
161
|
+
|
|
162
|
+
## Overview
|
|
163
|
+
[Synthesis of the search results and your conversation]
|
|
164
|
+
|
|
165
|
+
## Key Insights
|
|
166
|
+
[Summary of main points learned from these results]
|
|
167
|
+
|
|
168
|
+
## Observations
|
|
169
|
+
- [insight] [Important observation from search results]
|
|
170
|
+
- [connection] [How this connects to other topics]
|
|
171
|
+
|
|
172
|
+
## Relations
|
|
173
|
+
- relates_to [[{results.results[0].title if results.results else "Related Topic"}]]
|
|
174
|
+
- extends [[Another Relevant Topic]]
|
|
175
|
+
'''
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Remember that capturing synthesized knowledge is one of the most valuable features of Basic Memory.
|
|
125
180
|
""")
|
|
126
181
|
|
|
127
182
|
return summary
|