basic-memory 0.10.1__py3-none-any.whl → 0.12.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/api/app.py +42 -3
- basic_memory/cli/app.py +0 -7
- basic_memory/cli/commands/db.py +15 -2
- basic_memory/cli/commands/mcp.py +8 -1
- basic_memory/cli/commands/sync.py +1 -0
- basic_memory/cli/commands/tool.py +30 -17
- basic_memory/cli/main.py +16 -7
- basic_memory/config.py +71 -12
- basic_memory/db.py +3 -1
- basic_memory/file_utils.py +3 -0
- basic_memory/markdown/entity_parser.py +16 -7
- basic_memory/markdown/utils.py +21 -13
- basic_memory/mcp/prompts/continue_conversation.py +7 -7
- basic_memory/mcp/prompts/search.py +6 -6
- basic_memory/mcp/resources/ai_assistant_guide.md +5 -5
- basic_memory/mcp/server.py +2 -2
- basic_memory/mcp/tools/__init__.py +2 -2
- basic_memory/mcp/tools/read_note.py +3 -4
- basic_memory/mcp/tools/search.py +64 -28
- basic_memory/mcp/tools/write_note.py +3 -1
- basic_memory/repository/search_repository.py +11 -11
- basic_memory/schemas/search.py +2 -2
- basic_memory/services/context_service.py +1 -1
- basic_memory/services/entity_service.py +10 -10
- basic_memory/services/link_resolver.py +8 -1
- basic_memory/services/search_service.py +3 -23
- basic_memory/sync/sync_service.py +60 -23
- basic_memory/utils.py +10 -2
- {basic_memory-0.10.1.dist-info → basic_memory-0.12.0.dist-info}/METADATA +44 -6
- {basic_memory-0.10.1.dist-info → basic_memory-0.12.0.dist-info}/RECORD +34 -34
- {basic_memory-0.10.1.dist-info → basic_memory-0.12.0.dist-info}/entry_points.txt +1 -0
- {basic_memory-0.10.1.dist-info → basic_memory-0.12.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.10.1.dist-info → basic_memory-0.12.0.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py
CHANGED
basic_memory/api/app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""FastAPI application for basic-memory knowledge graph API."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
from contextlib import asynccontextmanager
|
|
4
5
|
|
|
5
6
|
from fastapi import FastAPI, HTTPException
|
|
@@ -7,16 +8,54 @@ from fastapi.exception_handlers import http_exception_handler
|
|
|
7
8
|
from loguru import logger
|
|
8
9
|
|
|
9
10
|
from basic_memory import db
|
|
10
|
-
from basic_memory.
|
|
11
|
-
from basic_memory.
|
|
11
|
+
from basic_memory.api.routers import knowledge, memory, project_info, resource, search
|
|
12
|
+
from basic_memory.config import config as project_config
|
|
13
|
+
from basic_memory.config import config_manager
|
|
14
|
+
from basic_memory.sync import SyncService, WatchService
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def run_background_sync(sync_service: SyncService, watch_service: WatchService): # pragma: no cover
|
|
18
|
+
logger.info(f"Starting watch service to sync file changes in dir: {project_config.home}")
|
|
19
|
+
# full sync
|
|
20
|
+
await sync_service.sync(project_config.home, show_progress=False)
|
|
21
|
+
|
|
22
|
+
# watch changes
|
|
23
|
+
await watch_service.run()
|
|
12
24
|
|
|
13
25
|
|
|
14
26
|
@asynccontextmanager
|
|
15
27
|
async def lifespan(app: FastAPI): # pragma: no cover
|
|
16
28
|
"""Lifecycle manager for the FastAPI app."""
|
|
17
|
-
await db.run_migrations(
|
|
29
|
+
await db.run_migrations(project_config)
|
|
30
|
+
|
|
31
|
+
# app config
|
|
32
|
+
basic_memory_config = config_manager.load_config()
|
|
33
|
+
logger.info(f"Sync changes enabled: {basic_memory_config.sync_changes}")
|
|
34
|
+
logger.info(f"Update permalinks on move enabled: {basic_memory_config.update_permalinks_on_move}")
|
|
35
|
+
|
|
36
|
+
watch_task = None
|
|
37
|
+
if basic_memory_config.sync_changes:
|
|
38
|
+
# import after migrations have run
|
|
39
|
+
from basic_memory.cli.commands.sync import get_sync_service
|
|
40
|
+
|
|
41
|
+
sync_service = await get_sync_service()
|
|
42
|
+
watch_service = WatchService(
|
|
43
|
+
sync_service=sync_service,
|
|
44
|
+
file_service=sync_service.entity_service.file_service,
|
|
45
|
+
config=project_config,
|
|
46
|
+
)
|
|
47
|
+
watch_task = asyncio.create_task(run_background_sync(sync_service, watch_service))
|
|
48
|
+
else:
|
|
49
|
+
logger.info("Sync changes disabled. Skipping watch service.")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# proceed with startup
|
|
18
53
|
yield
|
|
54
|
+
|
|
19
55
|
logger.info("Shutting down Basic Memory API")
|
|
56
|
+
if watch_task:
|
|
57
|
+
watch_task.cancel()
|
|
58
|
+
|
|
20
59
|
await db.shutdown_db()
|
|
21
60
|
|
|
22
61
|
|
basic_memory/cli/app.py
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
from typing import Optional
|
|
3
2
|
|
|
4
3
|
import typer
|
|
5
4
|
|
|
6
|
-
from basic_memory import db
|
|
7
|
-
from basic_memory.config import config
|
|
8
|
-
|
|
9
5
|
|
|
10
6
|
def version_callback(value: bool) -> None:
|
|
11
7
|
"""Show version and exit."""
|
|
@@ -58,9 +54,6 @@ def app_callback(
|
|
|
58
54
|
config = new_config
|
|
59
55
|
|
|
60
56
|
|
|
61
|
-
# Run database migrations
|
|
62
|
-
asyncio.run(db.run_migrations(config))
|
|
63
|
-
|
|
64
57
|
# Register sub-command groups
|
|
65
58
|
import_app = typer.Typer(help="Import data from various sources")
|
|
66
59
|
app.add_typer(import_app, name="import")
|
basic_memory/cli/commands/db.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Database management commands."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
3
5
|
import typer
|
|
4
6
|
from loguru import logger
|
|
5
7
|
|
|
6
|
-
from basic_memory
|
|
8
|
+
from basic_memory import db
|
|
7
9
|
from basic_memory.cli.app import app
|
|
10
|
+
from basic_memory.config import config
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
@app.command()
|
|
@@ -14,7 +17,17 @@ def reset(
|
|
|
14
17
|
"""Reset database (drop all tables and recreate)."""
|
|
15
18
|
if typer.confirm("This will delete all data in your db. Are you sure?"):
|
|
16
19
|
logger.info("Resetting database...")
|
|
17
|
-
|
|
20
|
+
# Get database path
|
|
21
|
+
db_path = config.database_path
|
|
22
|
+
|
|
23
|
+
# Delete the database file if it exists
|
|
24
|
+
if db_path.exists():
|
|
25
|
+
db_path.unlink()
|
|
26
|
+
logger.info(f"Database file deleted: {db_path}")
|
|
27
|
+
|
|
28
|
+
# Create a new empty database
|
|
29
|
+
asyncio.run(db.run_migrations(config))
|
|
30
|
+
logger.info("Database reset complete")
|
|
18
31
|
|
|
19
32
|
if reindex:
|
|
20
33
|
# Import and run sync
|
basic_memory/cli/commands/mcp.py
CHANGED
|
@@ -4,7 +4,7 @@ from loguru import logger
|
|
|
4
4
|
|
|
5
5
|
import basic_memory
|
|
6
6
|
from basic_memory.cli.app import app
|
|
7
|
-
from basic_memory.config import config
|
|
7
|
+
from basic_memory.config import config, config_manager
|
|
8
8
|
|
|
9
9
|
# Import mcp instance
|
|
10
10
|
from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
|
|
@@ -19,8 +19,15 @@ def mcp(): # pragma: no cover
|
|
|
19
19
|
home_dir = config.home
|
|
20
20
|
project_name = config.project
|
|
21
21
|
|
|
22
|
+
# app config
|
|
23
|
+
basic_memory_config = config_manager.load_config()
|
|
24
|
+
|
|
22
25
|
logger.info(f"Starting Basic Memory MCP server {basic_memory.__version__}")
|
|
23
26
|
logger.info(f"Project: {project_name}")
|
|
24
27
|
logger.info(f"Project directory: {home_dir}")
|
|
28
|
+
logger.info(f"Sync changes enabled: {basic_memory_config.sync_changes}")
|
|
29
|
+
logger.info(
|
|
30
|
+
f"Update permalinks on move enabled: {basic_memory_config.update_permalinks_on_move}"
|
|
31
|
+
)
|
|
25
32
|
|
|
26
33
|
mcp_server.run()
|
|
@@ -2,31 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import sys
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Annotated, List, Optional
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
from loguru import logger
|
|
9
9
|
from rich import print as rprint
|
|
10
10
|
|
|
11
11
|
from basic_memory.cli.app import app
|
|
12
|
-
from basic_memory.mcp.tools import build_context as mcp_build_context
|
|
13
|
-
from basic_memory.mcp.tools import read_note as mcp_read_note
|
|
14
|
-
from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
|
|
15
|
-
from basic_memory.mcp.tools import search as mcp_search
|
|
16
|
-
from basic_memory.mcp.tools import write_note as mcp_write_note
|
|
17
12
|
|
|
18
13
|
# Import prompts
|
|
19
14
|
from basic_memory.mcp.prompts.continue_conversation import (
|
|
20
15
|
continue_conversation as mcp_continue_conversation,
|
|
21
16
|
)
|
|
22
|
-
|
|
23
17
|
from basic_memory.mcp.prompts.recent_activity import (
|
|
24
18
|
recent_activity_prompt as recent_activity_prompt,
|
|
25
19
|
)
|
|
26
|
-
|
|
20
|
+
from basic_memory.mcp.tools import build_context as mcp_build_context
|
|
21
|
+
from basic_memory.mcp.tools import read_note as mcp_read_note
|
|
22
|
+
from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
|
|
23
|
+
from basic_memory.mcp.tools import search_notes as mcp_search
|
|
24
|
+
from basic_memory.mcp.tools import write_note as mcp_write_note
|
|
27
25
|
from basic_memory.schemas.base import TimeFrame
|
|
28
26
|
from basic_memory.schemas.memory import MemoryUrl
|
|
29
|
-
from basic_memory.schemas.search import
|
|
27
|
+
from basic_memory.schemas.search import SearchItemType
|
|
30
28
|
|
|
31
29
|
tool_app = typer.Typer()
|
|
32
30
|
app.add_typer(tool_app, name="tool", help="Access to MCP tools via CLI")
|
|
@@ -180,8 +178,8 @@ def recent_activity(
|
|
|
180
178
|
raise
|
|
181
179
|
|
|
182
180
|
|
|
183
|
-
@tool_app.command()
|
|
184
|
-
def
|
|
181
|
+
@tool_app.command("search-notes")
|
|
182
|
+
def search_notes(
|
|
185
183
|
query: str,
|
|
186
184
|
permalink: Annotated[bool, typer.Option("--permalink", help="Search permalink values")] = False,
|
|
187
185
|
title: Annotated[bool, typer.Option("--title", help="Search title values")] = False,
|
|
@@ -198,13 +196,28 @@ def search(
|
|
|
198
196
|
raise typer.Abort()
|
|
199
197
|
|
|
200
198
|
try:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
199
|
+
if permalink and title: # pragma: no cover
|
|
200
|
+
typer.echo(
|
|
201
|
+
"Use either --permalink or --title, not both. Exiting.",
|
|
202
|
+
err=True,
|
|
203
|
+
)
|
|
204
|
+
raise typer.Exit(1)
|
|
205
|
+
|
|
206
|
+
# set search type
|
|
207
|
+
search_type = ("permalink" if permalink else None,)
|
|
208
|
+
search_type = ("permalink_match" if permalink and "*" in query else None,)
|
|
209
|
+
search_type = ("title" if title else None,)
|
|
210
|
+
search_type = "text" if search_type is None else search_type
|
|
211
|
+
|
|
212
|
+
results = asyncio.run(
|
|
213
|
+
mcp_search(
|
|
214
|
+
query,
|
|
215
|
+
search_type=search_type,
|
|
216
|
+
page=page,
|
|
217
|
+
after_date=after_date,
|
|
218
|
+
page_size=page_size,
|
|
219
|
+
)
|
|
206
220
|
)
|
|
207
|
-
results = asyncio.run(mcp_search(query=search_query, page=page, page_size=page_size))
|
|
208
221
|
# Use json module for more controlled serialization
|
|
209
222
|
import json
|
|
210
223
|
|
basic_memory/cli/main.py
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
"""Main CLI entry point for basic-memory.""" # pragma: no cover
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
4
5
|
import typer
|
|
5
6
|
|
|
7
|
+
from basic_memory.cli.app import app # pragma: no cover
|
|
8
|
+
|
|
6
9
|
# Register commands
|
|
7
10
|
from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
|
|
8
|
-
status,
|
|
9
|
-
sync,
|
|
10
11
|
db,
|
|
11
|
-
|
|
12
|
-
mcp,
|
|
12
|
+
import_chatgpt,
|
|
13
13
|
import_claude_conversations,
|
|
14
14
|
import_claude_projects,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import_memory_json,
|
|
16
|
+
mcp,
|
|
17
17
|
project,
|
|
18
|
+
status,
|
|
19
|
+
sync,
|
|
20
|
+
tool,
|
|
18
21
|
)
|
|
22
|
+
from basic_memory.config import config
|
|
23
|
+
from basic_memory.db import run_migrations as db_run_migrations
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
# Version command
|
|
@@ -55,4 +60,8 @@ def main(
|
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
if __name__ == "__main__": # pragma: no cover
|
|
63
|
+
# Run database migrations
|
|
64
|
+
asyncio.run(db_run_migrations(config))
|
|
65
|
+
|
|
66
|
+
# start the app
|
|
58
67
|
app()
|
basic_memory/config.py
CHANGED
|
@@ -5,12 +5,13 @@ import os
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Dict, Literal, Optional
|
|
7
7
|
|
|
8
|
-
import basic_memory
|
|
9
|
-
from basic_memory.utils import setup_logging
|
|
10
8
|
from loguru import logger
|
|
11
9
|
from pydantic import Field, field_validator
|
|
12
10
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
11
|
|
|
12
|
+
import basic_memory
|
|
13
|
+
from basic_memory.utils import setup_logging
|
|
14
|
+
|
|
14
15
|
DATABASE_NAME = "memory.db"
|
|
15
16
|
DATA_DIR_NAME = ".basic-memory"
|
|
16
17
|
CONFIG_FILE_NAME = "config.json"
|
|
@@ -37,7 +38,11 @@ class ProjectConfig(BaseSettings):
|
|
|
37
38
|
default=500, description="Milliseconds to wait after changes before syncing", gt=0
|
|
38
39
|
)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
# update permalinks on move
|
|
42
|
+
update_permalinks_on_move: bool = Field(
|
|
43
|
+
default=False,
|
|
44
|
+
description="Whether to update permalinks when files are moved or renamed. default (False)",
|
|
45
|
+
)
|
|
41
46
|
|
|
42
47
|
model_config = SettingsConfigDict(
|
|
43
48
|
env_prefix="BASIC_MEMORY_",
|
|
@@ -76,6 +81,18 @@ class BasicMemoryConfig(BaseSettings):
|
|
|
76
81
|
description="Name of the default project to use",
|
|
77
82
|
)
|
|
78
83
|
|
|
84
|
+
log_level: str = "INFO"
|
|
85
|
+
|
|
86
|
+
update_permalinks_on_move: bool = Field(
|
|
87
|
+
default=False,
|
|
88
|
+
description="Whether to update permalinks when files are moved or renamed. default (False)",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
sync_changes: bool = Field(
|
|
92
|
+
default=True,
|
|
93
|
+
description="Whether to sync changes in real time. default (True)",
|
|
94
|
+
)
|
|
95
|
+
|
|
79
96
|
model_config = SettingsConfigDict(
|
|
80
97
|
env_prefix="BASIC_MEMORY_",
|
|
81
98
|
extra="ignore",
|
|
@@ -193,9 +210,14 @@ def get_project_config(project_name: Optional[str] = None) -> ProjectConfig:
|
|
|
193
210
|
"BASIC_MEMORY_PROJECT", project_name or config_manager.default_project
|
|
194
211
|
)
|
|
195
212
|
|
|
213
|
+
update_permalinks_on_move = config_manager.load_config().update_permalinks_on_move
|
|
196
214
|
try:
|
|
197
215
|
project_path = config_manager.get_project_path(actual_project_name)
|
|
198
|
-
return ProjectConfig(
|
|
216
|
+
return ProjectConfig(
|
|
217
|
+
home=project_path,
|
|
218
|
+
project=actual_project_name,
|
|
219
|
+
update_permalinks_on_move=update_permalinks_on_move,
|
|
220
|
+
)
|
|
199
221
|
except ValueError: # pragma: no cover
|
|
200
222
|
logger.warning(f"Project '{actual_project_name}' not found, using default")
|
|
201
223
|
project_path = config_manager.get_project_path(config_manager.default_project)
|
|
@@ -213,11 +235,48 @@ user_home = Path.home()
|
|
|
213
235
|
log_dir = user_home / DATA_DIR_NAME
|
|
214
236
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
215
237
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
|
|
239
|
+
def get_process_name(): # pragma: no cover
|
|
240
|
+
"""
|
|
241
|
+
get the type of process for logging
|
|
242
|
+
"""
|
|
243
|
+
import sys
|
|
244
|
+
|
|
245
|
+
if "sync" in sys.argv:
|
|
246
|
+
return "sync"
|
|
247
|
+
elif "mcp" in sys.argv:
|
|
248
|
+
return "mcp"
|
|
249
|
+
elif "cli" in sys.argv:
|
|
250
|
+
return "cli"
|
|
251
|
+
else:
|
|
252
|
+
return "api"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
process_name = get_process_name()
|
|
256
|
+
|
|
257
|
+
# Global flag to track if logging has been set up
|
|
258
|
+
_LOGGING_SETUP = False
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def setup_basic_memory_logging(): # pragma: no cover
|
|
262
|
+
"""Set up logging for basic-memory, ensuring it only happens once."""
|
|
263
|
+
global _LOGGING_SETUP
|
|
264
|
+
if _LOGGING_SETUP:
|
|
265
|
+
# We can't log before logging is set up
|
|
266
|
+
# print("Skipping duplicate logging setup")
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
setup_logging(
|
|
270
|
+
env=config.env,
|
|
271
|
+
home_dir=user_home, # Use user home for logs
|
|
272
|
+
log_level=config_manager.load_config().log_level,
|
|
273
|
+
log_file=f"{DATA_DIR_NAME}/basic-memory-{process_name}.log",
|
|
274
|
+
console=False,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
logger.info(f"Starting Basic Memory {basic_memory.__version__} (Project: {config.project})")
|
|
278
|
+
_LOGGING_SETUP = True
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Set up logging
|
|
282
|
+
setup_basic_memory_logging()
|
basic_memory/db.py
CHANGED
|
@@ -146,7 +146,9 @@ async def engine_session_factory(
|
|
|
146
146
|
_session_maker = None
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
async def run_migrations(
|
|
149
|
+
async def run_migrations(
|
|
150
|
+
app_config: ProjectConfig, database_type=DatabaseType.FILESYSTEM
|
|
151
|
+
): # pragma: no cover
|
|
150
152
|
"""Run any pending alembic migrations."""
|
|
151
153
|
logger.info("Running database migrations...")
|
|
152
154
|
try:
|
basic_memory/file_utils.py
CHANGED
|
@@ -104,6 +104,9 @@ def has_frontmatter(content: str) -> bool:
|
|
|
104
104
|
Returns:
|
|
105
105
|
True if content has valid frontmatter markers (---), False otherwise
|
|
106
106
|
"""
|
|
107
|
+
if not content:
|
|
108
|
+
return False
|
|
109
|
+
|
|
107
110
|
content = content.strip()
|
|
108
111
|
if not content.startswith("---"):
|
|
109
112
|
return False
|
|
@@ -92,27 +92,36 @@ class EntityParser:
|
|
|
92
92
|
async def parse_file(self, path: Path | str) -> EntityMarkdown:
|
|
93
93
|
"""Parse markdown file into EntityMarkdown."""
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
# TODO move to api endpoint to check if absolute path was requested
|
|
96
|
+
# Check if the path is already absolute
|
|
97
|
+
if (
|
|
98
|
+
isinstance(path, Path)
|
|
99
|
+
and path.is_absolute()
|
|
100
|
+
or (isinstance(path, str) and Path(path).is_absolute())
|
|
101
|
+
):
|
|
102
|
+
absolute_path = Path(path)
|
|
103
|
+
else:
|
|
104
|
+
absolute_path = self.base_path / path
|
|
105
|
+
|
|
96
106
|
# Parse frontmatter and content using python-frontmatter
|
|
97
|
-
|
|
107
|
+
file_content = absolute_path.read_text()
|
|
108
|
+
return await self.parse_file_content(absolute_path, file_content)
|
|
98
109
|
|
|
110
|
+
async def parse_file_content(self, absolute_path, file_content):
|
|
111
|
+
post = frontmatter.loads(file_content)
|
|
99
112
|
# Extract file stat info
|
|
100
113
|
file_stats = absolute_path.stat()
|
|
101
|
-
|
|
102
114
|
metadata = post.metadata
|
|
103
|
-
metadata["title"] = post.metadata.get("title", absolute_path.
|
|
115
|
+
metadata["title"] = post.metadata.get("title", absolute_path.stem)
|
|
104
116
|
metadata["type"] = post.metadata.get("type", "note")
|
|
105
117
|
tags = parse_tags(post.metadata.get("tags", [])) # pyright: ignore
|
|
106
118
|
if tags:
|
|
107
119
|
metadata["tags"] = tags
|
|
108
|
-
|
|
109
120
|
# frontmatter
|
|
110
121
|
entity_frontmatter = EntityFrontmatter(
|
|
111
122
|
metadata=post.metadata,
|
|
112
123
|
)
|
|
113
|
-
|
|
114
124
|
entity_content = parse(post.content)
|
|
115
|
-
|
|
116
125
|
return EntityMarkdown(
|
|
117
126
|
frontmatter=entity_frontmatter,
|
|
118
127
|
content=post.content,
|
basic_memory/markdown/utils.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""Utilities for converting between markdown and entity models."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Any, Optional
|
|
5
5
|
|
|
6
6
|
from frontmatter import Post
|
|
7
7
|
|
|
8
|
-
from basic_memory.file_utils import has_frontmatter, remove_frontmatter
|
|
8
|
+
from basic_memory.file_utils import has_frontmatter, remove_frontmatter, parse_frontmatter
|
|
9
9
|
from basic_memory.markdown import EntityMarkdown
|
|
10
|
-
from basic_memory.models import Entity
|
|
11
|
-
from basic_memory.
|
|
10
|
+
from basic_memory.models import Entity
|
|
11
|
+
from basic_memory.models import Observation as ObservationModel
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def entity_model_from_markdown(
|
|
@@ -32,16 +32,13 @@ def entity_model_from_markdown(
|
|
|
32
32
|
if not markdown.created or not markdown.modified: # pragma: no cover
|
|
33
33
|
raise ValueError("Both created and modified dates are required in markdown")
|
|
34
34
|
|
|
35
|
-
# Generate permalink if not provided
|
|
36
|
-
permalink = markdown.frontmatter.permalink or generate_permalink(file_path)
|
|
37
|
-
|
|
38
35
|
# Create or update entity
|
|
39
36
|
model = entity or Entity()
|
|
40
37
|
|
|
41
38
|
# Update basic fields
|
|
42
39
|
model.title = markdown.frontmatter.title
|
|
43
40
|
model.entity_type = markdown.frontmatter.type
|
|
44
|
-
model.permalink = permalink
|
|
41
|
+
model.permalink = markdown.frontmatter.permalink
|
|
45
42
|
model.file_path = str(file_path)
|
|
46
43
|
model.content_type = "text/markdown"
|
|
47
44
|
model.created_at = markdown.created
|
|
@@ -77,22 +74,33 @@ async def schema_to_markdown(schema: Any) -> Post:
|
|
|
77
74
|
"""
|
|
78
75
|
# Extract content and metadata
|
|
79
76
|
content = schema.content or ""
|
|
80
|
-
|
|
77
|
+
entity_metadata = dict(schema.entity_metadata or {})
|
|
81
78
|
|
|
82
79
|
# if the content contains frontmatter, remove it and merge
|
|
83
80
|
if has_frontmatter(content):
|
|
81
|
+
content_frontmatter = parse_frontmatter(content)
|
|
84
82
|
content = remove_frontmatter(content)
|
|
85
83
|
|
|
84
|
+
# Merge content frontmatter with entity metadata
|
|
85
|
+
# (entity_metadata takes precedence for conflicts)
|
|
86
|
+
content_frontmatter.update(entity_metadata)
|
|
87
|
+
entity_metadata = content_frontmatter
|
|
88
|
+
|
|
86
89
|
# Remove special fields for ordered frontmatter
|
|
87
90
|
for field in ["type", "title", "permalink"]:
|
|
88
|
-
|
|
91
|
+
entity_metadata.pop(field, None)
|
|
89
92
|
|
|
90
|
-
# Create Post with ordered
|
|
93
|
+
# Create Post with fields ordered by insert order
|
|
91
94
|
post = Post(
|
|
92
95
|
content,
|
|
93
96
|
title=schema.title,
|
|
94
97
|
type=schema.entity_type,
|
|
95
|
-
permalink=schema.permalink,
|
|
96
|
-
**frontmatter_metadata,
|
|
97
98
|
)
|
|
99
|
+
# set the permalink if passed in
|
|
100
|
+
if schema.permalink:
|
|
101
|
+
post.metadata["permalink"] = schema.permalink
|
|
102
|
+
|
|
103
|
+
if entity_metadata:
|
|
104
|
+
post.metadata.update(entity_metadata)
|
|
105
|
+
|
|
98
106
|
return post
|
|
@@ -5,19 +5,19 @@ providing context from previous interactions to maintain continuity.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from textwrap import dedent
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Annotated, Optional
|
|
9
9
|
|
|
10
10
|
from loguru import logger
|
|
11
11
|
from pydantic import Field
|
|
12
12
|
|
|
13
|
-
from basic_memory.mcp.prompts.utils import
|
|
13
|
+
from basic_memory.mcp.prompts.utils import PromptContext, PromptContextItem, format_prompt_context
|
|
14
14
|
from basic_memory.mcp.server import mcp
|
|
15
15
|
from basic_memory.mcp.tools.build_context import build_context
|
|
16
16
|
from basic_memory.mcp.tools.recent_activity import recent_activity
|
|
17
|
-
from basic_memory.mcp.tools.search import
|
|
17
|
+
from basic_memory.mcp.tools.search import search_notes
|
|
18
18
|
from basic_memory.schemas.base import TimeFrame
|
|
19
19
|
from basic_memory.schemas.memory import GraphContext
|
|
20
|
-
from basic_memory.schemas.search import
|
|
20
|
+
from basic_memory.schemas.search import SearchItemType
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@mcp.prompt(
|
|
@@ -47,8 +47,8 @@ async def continue_conversation(
|
|
|
47
47
|
|
|
48
48
|
# If topic provided, search for it
|
|
49
49
|
if topic:
|
|
50
|
-
search_results = await
|
|
51
|
-
|
|
50
|
+
search_results = await search_notes(
|
|
51
|
+
query=topic, after_date=timeframe, entity_types=[SearchItemType.ENTITY]
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
# Build context from results
|
|
@@ -93,7 +93,7 @@ async def continue_conversation(
|
|
|
93
93
|
## Next Steps
|
|
94
94
|
|
|
95
95
|
You can:
|
|
96
|
-
- Explore more with: `
|
|
96
|
+
- Explore more with: `search_notes({{"text": "{topic}"}})`
|
|
97
97
|
- See what's changed: `recent_activity(timeframe="{timeframe or "7d"}")`
|
|
98
98
|
- **Record new learnings or decisions from this conversation:** `write_note(title="[Create a meaningful title]", content="[Content with observations and relations]")`
|
|
99
99
|
|
|
@@ -10,9 +10,9 @@ from loguru import logger
|
|
|
10
10
|
from pydantic import Field
|
|
11
11
|
|
|
12
12
|
from basic_memory.mcp.server import mcp
|
|
13
|
-
from basic_memory.mcp.tools.search import
|
|
13
|
+
from basic_memory.mcp.tools.search import search_notes as search_tool
|
|
14
14
|
from basic_memory.schemas.base import TimeFrame
|
|
15
|
-
from basic_memory.schemas.search import
|
|
15
|
+
from basic_memory.schemas.search import SearchResponse
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@mcp.prompt(
|
|
@@ -40,7 +40,7 @@ async def search_prompt(
|
|
|
40
40
|
"""
|
|
41
41
|
logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
|
|
42
42
|
|
|
43
|
-
search_results = await search_tool(
|
|
43
|
+
search_results = await search_tool(query=query, after_date=timeframe)
|
|
44
44
|
return format_search_results(query, search_results, timeframe)
|
|
45
45
|
|
|
46
46
|
|
|
@@ -144,9 +144,9 @@ def format_search_results(
|
|
|
144
144
|
## Next Steps
|
|
145
145
|
|
|
146
146
|
You can:
|
|
147
|
-
- Refine your search: `
|
|
148
|
-
- Exclude terms: `
|
|
149
|
-
- View more results: `
|
|
147
|
+
- Refine your search: `search_notes("{query} AND additional_term")`
|
|
148
|
+
- Exclude terms: `search_notes("{query} NOT exclude_term")`
|
|
149
|
+
- View more results: `search_notes("{query}", after_date=None)`
|
|
150
150
|
- Check recent activity: `recent_activity()`
|
|
151
151
|
|
|
152
152
|
## Synthesize and Capture Knowledge
|
|
@@ -49,7 +49,7 @@ content = await read_note("specs/search-design") # By path
|
|
|
49
49
|
content = await read_note("memory://specs/search") # By memory URL
|
|
50
50
|
|
|
51
51
|
# Searching for knowledge
|
|
52
|
-
results = await
|
|
52
|
+
results = await search_notes(
|
|
53
53
|
query="authentication system", # Text to search for
|
|
54
54
|
page=1, # Optional: Pagination
|
|
55
55
|
page_size=10 # Optional: Results per page
|
|
@@ -154,7 +154,7 @@ Users will interact with Basic Memory in patterns like:
|
|
|
154
154
|
Human: "What were our decisions about auth?"
|
|
155
155
|
|
|
156
156
|
You: Let me find that information for you.
|
|
157
|
-
[Use
|
|
157
|
+
[Use search_notes() to find relevant notes]
|
|
158
158
|
[Then build_context() to understand connections]
|
|
159
159
|
```
|
|
160
160
|
|
|
@@ -263,7 +263,7 @@ When creating relations, you can:
|
|
|
263
263
|
# Example workflow for creating notes with effective relations
|
|
264
264
|
async def create_note_with_effective_relations():
|
|
265
265
|
# Search for existing entities to reference
|
|
266
|
-
search_results = await
|
|
266
|
+
search_results = await search_notes("travel")
|
|
267
267
|
existing_entities = [result.title for result in search_results.primary_results]
|
|
268
268
|
|
|
269
269
|
# Check if specific entities exist
|
|
@@ -335,7 +335,7 @@ Common issues to watch for:
|
|
|
335
335
|
content = await read_note("Document")
|
|
336
336
|
except:
|
|
337
337
|
# Try search instead
|
|
338
|
-
results = await
|
|
338
|
+
results = await search_notes("Document")
|
|
339
339
|
if results and results.primary_results:
|
|
340
340
|
# Found something similar
|
|
341
341
|
content = await read_note(results.primary_results[0].permalink)
|
|
@@ -381,7 +381,7 @@ Common issues to watch for:
|
|
|
381
381
|
- **Create deliberate relations**: Connect each note to at least 2-3 related entities
|
|
382
382
|
- **Use existing entities**: Before creating a new relation, search for existing entities
|
|
383
383
|
- **Verify wikilinks**: When referencing `[[Entity]]`, use exact titles of existing notes
|
|
384
|
-
- **Check accuracy**: Use `
|
|
384
|
+
- **Check accuracy**: Use `search_notes()` or `recent_activity()` to confirm entity titles
|
|
385
385
|
- **Use precise relation types**: Choose specific relation types that convey meaning (e.g., "implements" instead
|
|
386
386
|
of "relates_to")
|
|
387
387
|
- **Consider bidirectional relations**: When appropriate, create inverse relations in both entities
|