basic-memory 0.4.2__tar.gz → 0.4.3__tar.gz
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-0.4.2 → basic_memory-0.4.3}/CHANGELOG.md +10 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/PKG-INFO +1 -1
- {basic_memory-0.4.2 → basic_memory-0.4.3}/pyproject.toml +1 -1
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/__init__.py +1 -1
- basic_memory-0.4.3/src/basic_memory/api/routers/resource_router.py +118 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/commands/import_chatgpt.py +5 -1
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/db.py +8 -4
- basic_memory-0.4.3/src/basic_memory/mcp/tools/notes.py +190 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/repository/search_repository.py +49 -23
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/schemas/response.py +8 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/services/entity_service.py +5 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/services/link_resolver.py +8 -7
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/services/search_service.py +1 -1
- basic_memory-0.4.3/tests/api/test_resource_router.py +229 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/conftest.py +0 -21
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/schemas/test_schemas.py +16 -4
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/services/test_link_resolver.py +26 -29
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/services/test_search_service.py +29 -11
- {basic_memory-0.4.2 → basic_memory-0.4.3}/uv.lock +1 -1
- basic_memory-0.4.2/src/basic_memory/api/routers/resource_router.py +0 -34
- basic_memory-0.4.2/src/basic_memory/mcp/tools/notes.py +0 -125
- basic_memory-0.4.2/tests/api/test_resource_router.py +0 -91
- {basic_memory-0.4.2 → basic_memory-0.4.3}/.github/workflows/pr-title.yml +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/.github/workflows/release.yml +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/.github/workflows/test.yml +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/.gitignore +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/.python-version +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/CITATION.cff +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/CODE_OF_CONDUCT.md +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/CONTRIBUTING.md +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/LICENSE +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/Makefile +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/README.md +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/basic-memory.md +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/installer/Basic.icns +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/installer/README.md +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/installer/icon.svg +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/installer/installer.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/installer/make_icons.sh +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/installer/setup.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/memory.json +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/scripts/install.sh +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/alembic/README +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/alembic/env.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/alembic/migrations.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/alembic/script.py.mako +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/api/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/api/app.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/api/routers/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/api/routers/knowledge_router.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/api/routers/memory_router.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/api/routers/search_router.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/app.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/commands/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/commands/db.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/commands/import_claude_conversations.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/commands/import_claude_projects.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/commands/import_memory_json.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/commands/mcp.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/commands/status.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/commands/sync.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/cli/main.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/config.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/deps.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/file_utils.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/markdown/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/markdown/entity_parser.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/markdown/markdown_processor.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/markdown/plugins.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/markdown/schemas.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/markdown/utils.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/mcp/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/mcp/async_client.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/mcp/server.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/mcp/tools/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/mcp/tools/knowledge.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/mcp/tools/memory.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/mcp/tools/search.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/mcp/tools/utils.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/models/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/models/base.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/models/knowledge.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/models/search.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/repository/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/repository/entity_repository.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/repository/observation_repository.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/repository/relation_repository.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/repository/repository.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/schemas/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/schemas/base.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/schemas/delete.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/schemas/discovery.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/schemas/memory.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/schemas/request.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/schemas/search.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/services/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/services/context_service.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/services/exceptions.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/services/file_service.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/services/service.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/sync/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/sync/file_change_scanner.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/sync/sync_service.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/sync/utils.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/sync/watch_service.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/src/basic_memory/utils.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/api/conftest.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/api/test_knowledge_router.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/api/test_memory_router.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/api/test_search_router.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/cli/test_import_chatgpt.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/cli/test_import_claude_conversations.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/cli/test_import_claude_projects.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/cli/test_import_memory_json.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/cli/test_status.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/cli/test_sync.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/edit_file_test.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/markdown/__init__.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/markdown/test_entity_parser.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/markdown/test_markdown_plugins.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/markdown/test_markdown_processor.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/markdown/test_observation_edge_cases.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/markdown/test_parser_edge_cases.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/markdown/test_relation_edge_cases.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/markdown/test_task_detection.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/mcp/conftest.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/mcp/test_tool_get_entity.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/mcp/test_tool_knowledge.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/mcp/test_tool_memory.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/mcp/test_tool_notes.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/mcp/test_tool_search.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/mcp/test_tool_utils.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/repository/test_entity_repository.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/repository/test_observation_repository.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/repository/test_relation_repository.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/repository/test_repository.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/schemas/test_memory_url.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/schemas/test_search.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/services/test_context_service.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/services/test_entity_service.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/services/test_file_service.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/sync/test_file_change_scanner.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/sync/test_sync_service.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/sync/test_watch_service.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/test_basic_memory.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/utils/test_file_utils.py +0 -0
- {basic_memory-0.4.2 → basic_memory-0.4.3}/tests/utils/test_permalink_formatting.py +0 -0
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.4.3 (2025-02-18)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- Re do enhanced read note format ([#10](https://github.com/basicmachines-co/basic-memory/pull/10),
|
|
9
|
+
[`39bd5ca`](https://github.com/basicmachines-co/basic-memory/commit/39bd5ca08fd057220b95a8b5d82c5e73a1f5722b))
|
|
10
|
+
|
|
11
|
+
Co-authored-by: phernandez <phernandez@basicmachines.co>
|
|
12
|
+
|
|
13
|
+
|
|
4
14
|
## v0.4.2 (2025-02-17)
|
|
5
15
|
|
|
6
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-memory
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Local-first knowledge management combining Zettelkasten with knowledge graphs
|
|
5
5
|
Project-URL: Homepage, https://github.com/basicmachines-co/basic-memory
|
|
6
6
|
Project-URL: Repository, https://github.com/basicmachines-co/basic-memory
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Routes for getting entity content."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
|
7
|
+
from fastapi.responses import FileResponse
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from basic_memory.deps import (
|
|
11
|
+
ProjectConfigDep,
|
|
12
|
+
LinkResolverDep,
|
|
13
|
+
SearchServiceDep,
|
|
14
|
+
EntityServiceDep,
|
|
15
|
+
FileServiceDep,
|
|
16
|
+
)
|
|
17
|
+
from basic_memory.repository.search_repository import SearchIndexRow
|
|
18
|
+
from basic_memory.schemas.memory import normalize_memory_url
|
|
19
|
+
from basic_memory.schemas.search import SearchQuery, SearchItemType
|
|
20
|
+
|
|
21
|
+
router = APIRouter(prefix="/resource", tags=["resources"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_entity_ids(item: SearchIndexRow) -> list[int]:
|
|
25
|
+
match item.type:
|
|
26
|
+
case SearchItemType.ENTITY:
|
|
27
|
+
return [item.id]
|
|
28
|
+
case SearchItemType.OBSERVATION:
|
|
29
|
+
return [item.entity_id] # pyright: ignore [reportReturnType]
|
|
30
|
+
case SearchItemType.RELATION:
|
|
31
|
+
from_entity = item.from_id
|
|
32
|
+
to_entity = item.to_id # pyright: ignore [reportReturnType]
|
|
33
|
+
return [from_entity, to_entity] if to_entity else [from_entity] # pyright: ignore [reportReturnType]
|
|
34
|
+
case _:
|
|
35
|
+
raise ValueError(f"Unexpected type: {item.type}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.get("/{identifier:path}")
|
|
39
|
+
async def get_resource_content(
|
|
40
|
+
config: ProjectConfigDep,
|
|
41
|
+
link_resolver: LinkResolverDep,
|
|
42
|
+
search_service: SearchServiceDep,
|
|
43
|
+
entity_service: EntityServiceDep,
|
|
44
|
+
file_service: FileServiceDep,
|
|
45
|
+
background_tasks: BackgroundTasks,
|
|
46
|
+
identifier: str,
|
|
47
|
+
) -> FileResponse:
|
|
48
|
+
"""Get resource content by identifier: name or permalink."""
|
|
49
|
+
logger.debug(f"Getting content for: {identifier}")
|
|
50
|
+
|
|
51
|
+
# Find single entity by permalink
|
|
52
|
+
entity = await link_resolver.resolve_link(identifier)
|
|
53
|
+
results = [entity] if entity else []
|
|
54
|
+
|
|
55
|
+
# search using the identifier as a permalink
|
|
56
|
+
if not results:
|
|
57
|
+
# if the identifier contains a wildcard, use GLOB search
|
|
58
|
+
query = (
|
|
59
|
+
SearchQuery(permalink_match=identifier)
|
|
60
|
+
if "*" in identifier
|
|
61
|
+
else SearchQuery(permalink=identifier)
|
|
62
|
+
)
|
|
63
|
+
search_results = await search_service.search(query)
|
|
64
|
+
if not search_results:
|
|
65
|
+
raise HTTPException(status_code=404, detail=f"Resource not found: {identifier}")
|
|
66
|
+
|
|
67
|
+
# get the entities related to the search results
|
|
68
|
+
entity_ids = [id for result in search_results for id in get_entity_ids(result)]
|
|
69
|
+
results = await entity_service.get_entities_by_id(entity_ids)
|
|
70
|
+
|
|
71
|
+
# return single response
|
|
72
|
+
if len(results) == 1:
|
|
73
|
+
entity = results[0]
|
|
74
|
+
file_path = Path(f"{config.home}/{entity.file_path}")
|
|
75
|
+
if not file_path.exists():
|
|
76
|
+
raise HTTPException(
|
|
77
|
+
status_code=404,
|
|
78
|
+
detail=f"File not found: {file_path}",
|
|
79
|
+
)
|
|
80
|
+
return FileResponse(path=file_path)
|
|
81
|
+
|
|
82
|
+
# for multiple files, initialize a temporary file for writing the results
|
|
83
|
+
with tempfile.NamedTemporaryFile(delete=False, mode="w", suffix=".md") as tmp_file:
|
|
84
|
+
temp_file_path = tmp_file.name
|
|
85
|
+
|
|
86
|
+
for result in results:
|
|
87
|
+
# Read content for each entity
|
|
88
|
+
content = await file_service.read_entity_content(result)
|
|
89
|
+
memory_url = normalize_memory_url(result.permalink)
|
|
90
|
+
modified_date = result.updated_at.isoformat()
|
|
91
|
+
assert result.checksum
|
|
92
|
+
checksum = result.checksum[:8]
|
|
93
|
+
|
|
94
|
+
# Prepare the delimited content
|
|
95
|
+
response_content = f"--- {memory_url} {modified_date} {checksum}\n"
|
|
96
|
+
response_content += f"\n{content}\n"
|
|
97
|
+
response_content += "\n"
|
|
98
|
+
|
|
99
|
+
# Write content directly to the temporary file in append mode
|
|
100
|
+
tmp_file.write(response_content)
|
|
101
|
+
|
|
102
|
+
# Ensure all content is written to disk
|
|
103
|
+
tmp_file.flush()
|
|
104
|
+
|
|
105
|
+
# Schedule the temporary file to be deleted after the response
|
|
106
|
+
background_tasks.add_task(cleanup_temp_file, temp_file_path)
|
|
107
|
+
|
|
108
|
+
# Return the file response
|
|
109
|
+
return FileResponse(path=temp_file_path)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def cleanup_temp_file(file_path: str):
|
|
113
|
+
"""Delete the temporary file."""
|
|
114
|
+
try:
|
|
115
|
+
Path(file_path).unlink() # Deletes the file
|
|
116
|
+
logger.debug(f"Temporary file deleted: {file_path}")
|
|
117
|
+
except Exception as e: # pragma: no cover
|
|
118
|
+
logger.error(f"Error deleting temporary file {file_path}: {e}")
|
|
@@ -69,7 +69,11 @@ def traverse_messages(
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def format_chat_markdown(
|
|
72
|
-
title: str,
|
|
72
|
+
title: str,
|
|
73
|
+
mapping: Dict[str, Any],
|
|
74
|
+
root_id: Optional[str],
|
|
75
|
+
created_at: float,
|
|
76
|
+
modified_at: float,
|
|
73
77
|
) -> str:
|
|
74
78
|
"""Format chat as clean markdown."""
|
|
75
79
|
|
|
@@ -140,11 +140,15 @@ async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.F
|
|
|
140
140
|
|
|
141
141
|
# Set required Alembic config options programmatically
|
|
142
142
|
config.set_main_option("script_location", str(alembic_dir))
|
|
143
|
-
config.set_main_option(
|
|
144
|
-
|
|
143
|
+
config.set_main_option(
|
|
144
|
+
"file_template",
|
|
145
|
+
"%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s",
|
|
146
|
+
)
|
|
145
147
|
config.set_main_option("timezone", "UTC")
|
|
146
148
|
config.set_main_option("revision_environment", "false")
|
|
147
|
-
config.set_main_option(
|
|
149
|
+
config.set_main_option(
|
|
150
|
+
"sqlalchemy.url", DatabaseType.get_db_url(app_config.database_path, database_type)
|
|
151
|
+
)
|
|
148
152
|
|
|
149
153
|
command.upgrade(config, "head")
|
|
150
154
|
logger.info("Migrations completed successfully")
|
|
@@ -153,4 +157,4 @@ async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.F
|
|
|
153
157
|
await SearchRepository(session_maker).init_search_index()
|
|
154
158
|
except Exception as e: # pragma: no cover
|
|
155
159
|
logger.error(f"Error running migrations: {e}")
|
|
156
|
-
raise
|
|
160
|
+
raise
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Note management tools for Basic Memory MCP server.
|
|
2
|
+
|
|
3
|
+
These tools provide a natural interface for working with markdown notes
|
|
4
|
+
while leveraging the underlying knowledge graph structure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from basic_memory.mcp.server import mcp
|
|
12
|
+
from basic_memory.mcp.async_client import client
|
|
13
|
+
from basic_memory.schemas import EntityResponse, DeleteEntitiesResponse
|
|
14
|
+
from basic_memory.schemas.base import Entity
|
|
15
|
+
from basic_memory.mcp.tools.utils import call_get, call_put, call_delete
|
|
16
|
+
from basic_memory.schemas.memory import memory_url_path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.tool(
|
|
20
|
+
description="Create or update a markdown note. Returns the permalink for referencing.",
|
|
21
|
+
)
|
|
22
|
+
async def write_note(
|
|
23
|
+
title: str,
|
|
24
|
+
content: str,
|
|
25
|
+
folder: str,
|
|
26
|
+
tags: Optional[List[str]] = None,
|
|
27
|
+
verbose: bool = False,
|
|
28
|
+
) -> EntityResponse | str:
|
|
29
|
+
"""Write a markdown note to the knowledge base.
|
|
30
|
+
|
|
31
|
+
The content can include semantic observations and relations using markdown syntax.
|
|
32
|
+
Relations can be specified either explicitly or through inline wiki-style links:
|
|
33
|
+
|
|
34
|
+
Observations format:
|
|
35
|
+
`- [category] Observation text #tag1 #tag2 (optional context)`
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
`- [design] Files are the source of truth #architecture (All state comes from files)`
|
|
39
|
+
`- [tech] Using SQLite for storage #implementation`
|
|
40
|
+
`- [note] Need to add error handling #todo`
|
|
41
|
+
|
|
42
|
+
Relations format:
|
|
43
|
+
- Explicit: `- relation_type [[Entity]] (optional context)`
|
|
44
|
+
- Inline: Any `[[Entity]]` reference creates a relation
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
`- depends_on [[Content Parser]] (Need for semantic extraction)`
|
|
48
|
+
`- implements [[Search Spec]] (Initial implementation)`
|
|
49
|
+
`- This feature extends [[Base Design]] and uses [[Core Utils]]`
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
title: The title of the note
|
|
53
|
+
content: Markdown content for the note, can include observations and relations
|
|
54
|
+
folder: the folder where the file should be saved
|
|
55
|
+
tags: Optional list of tags to categorize the note
|
|
56
|
+
verbose: If True, returns full EntityResponse with semantic info
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
If verbose=False: Permalink that can be used to reference the note
|
|
60
|
+
If verbose=True: EntityResponse with full semantic details
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
# Note with both explicit and inline relations
|
|
64
|
+
write_note(
|
|
65
|
+
title="Search Implementation",
|
|
66
|
+
content="# Search Component\\n\\n"
|
|
67
|
+
"Implementation of the search feature, building on [[Core Search]].\\n\\n"
|
|
68
|
+
"## Observations\\n"
|
|
69
|
+
"- [tech] Using FTS5 for full-text search #implementation\\n"
|
|
70
|
+
"- [design] Need pagination support #todo\\n\\n"
|
|
71
|
+
"## Relations\\n"
|
|
72
|
+
"- implements [[Search Spec]]\\n"
|
|
73
|
+
"- depends_on [[Database Schema]]",
|
|
74
|
+
folder="docs/components"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Note with tags
|
|
78
|
+
write_note(
|
|
79
|
+
title="Error Handling Design",
|
|
80
|
+
content="# Error Handling\\n\\n"
|
|
81
|
+
"This design builds on [[Reliability Design]].\\n\\n"
|
|
82
|
+
"## Approach\\n"
|
|
83
|
+
"- [design] Use error codes #architecture\\n"
|
|
84
|
+
"- [tech] Implement retry logic #implementation\\n\\n"
|
|
85
|
+
"## Relations\\n"
|
|
86
|
+
"- extends [[Base Error Handling]]",
|
|
87
|
+
folder="docs/design",
|
|
88
|
+
tags=["architecture", "reliability"]
|
|
89
|
+
)
|
|
90
|
+
"""
|
|
91
|
+
logger.info(f"Writing note folder:'{folder}' title: '{title}'")
|
|
92
|
+
|
|
93
|
+
# Create the entity request
|
|
94
|
+
metadata = {"tags": [f"#{tag}" for tag in tags]} if tags else None
|
|
95
|
+
entity = Entity(
|
|
96
|
+
title=title,
|
|
97
|
+
folder=folder,
|
|
98
|
+
entity_type="note",
|
|
99
|
+
content_type="text/markdown",
|
|
100
|
+
content=content,
|
|
101
|
+
entity_metadata=metadata,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Use existing knowledge tool
|
|
105
|
+
logger.info(f"Creating {entity.permalink}")
|
|
106
|
+
url = f"/knowledge/entities/{entity.permalink}"
|
|
107
|
+
response = await call_put(client, url, json=entity.model_dump())
|
|
108
|
+
result = EntityResponse.model_validate(response.json())
|
|
109
|
+
return result if verbose else result.permalink
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@mcp.tool(description="Read note content by title, permalink, relation, or pattern")
|
|
113
|
+
async def read_note(identifier: str) -> str:
|
|
114
|
+
"""Get note content in unified diff format.
|
|
115
|
+
|
|
116
|
+
The content is returned in a unified diff inspired format:
|
|
117
|
+
```
|
|
118
|
+
--- memory://docs/example 2025-01-31T19:32:49 7d9f1c8b
|
|
119
|
+
<document content>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Multiple documents (from relations or pattern matches) are separated by
|
|
123
|
+
additional headers.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
identifier: Can be one of:
|
|
127
|
+
- Note title ("Project Planning")
|
|
128
|
+
- Note permalink ("docs/example")
|
|
129
|
+
- Relation path ("docs/example/depends-on/other-doc")
|
|
130
|
+
- Pattern match ("docs/*-architecture")
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Document content in unified diff format. For single documents, returns
|
|
134
|
+
just that document's content. For relations or pattern matches, returns
|
|
135
|
+
multiple documents separated by unified diff headers.
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
# Single document
|
|
139
|
+
content = await read_note("Project Planning")
|
|
140
|
+
|
|
141
|
+
# Read by permalink
|
|
142
|
+
content = await read_note("docs/architecture/file-first")
|
|
143
|
+
|
|
144
|
+
# Follow relation
|
|
145
|
+
content = await read_note("docs/architecture/depends-on/docs/content-parser")
|
|
146
|
+
|
|
147
|
+
# Pattern matching
|
|
148
|
+
content = await read_note("docs/*-architecture") # All architecture docs
|
|
149
|
+
content = await read_note("docs/*/implements/*") # Find implementations
|
|
150
|
+
|
|
151
|
+
Output format:
|
|
152
|
+
```
|
|
153
|
+
--- memory://docs/example 2025-01-31T19:32:49 7d9f1c8b
|
|
154
|
+
<first document content>
|
|
155
|
+
|
|
156
|
+
--- memory://docs/other 2025-01-30T15:45:22 a1b2c3d4
|
|
157
|
+
<second document content>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The headers include:
|
|
161
|
+
- Full memory:// URI for the document
|
|
162
|
+
- Last modified timestamp
|
|
163
|
+
- Content checksum
|
|
164
|
+
"""
|
|
165
|
+
logger.info(f"Reading note {identifier}")
|
|
166
|
+
url = memory_url_path(identifier)
|
|
167
|
+
response = await call_get(client, f"/resource/{url}")
|
|
168
|
+
return response.text
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@mcp.tool(description="Delete a note by title or permalink")
|
|
172
|
+
async def delete_note(identifier: str) -> bool:
|
|
173
|
+
"""Delete a note from the knowledge base.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
identifier: Note title or permalink
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
True if note was deleted, False otherwise
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
# Delete by title
|
|
183
|
+
delete_note("Meeting Notes: Project Planning")
|
|
184
|
+
|
|
185
|
+
# Delete by permalink
|
|
186
|
+
delete_note("notes/project-planning")
|
|
187
|
+
"""
|
|
188
|
+
response = await call_delete(client, f"/knowledge/entities/{identifier}")
|
|
189
|
+
result = DeleteEntitiesResponse.model_validate(response.json())
|
|
190
|
+
return result.deleted
|
|
@@ -68,24 +68,40 @@ class SearchRepository:
|
|
|
68
68
|
|
|
69
69
|
async def init_search_index(self):
|
|
70
70
|
"""Create or recreate the search index."""
|
|
71
|
-
|
|
72
71
|
logger.info("Initializing search index")
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
try:
|
|
73
|
+
async with db.scoped_session(self.session_maker) as session:
|
|
74
|
+
await session.execute(CREATE_SEARCH_INDEX)
|
|
75
|
+
await session.commit()
|
|
76
|
+
except Exception as e: # pragma: no cover
|
|
77
|
+
logger.error(f"Error initializing search index: {e}")
|
|
78
|
+
raise e
|
|
79
|
+
|
|
80
|
+
def _prepare_search_term(self, term: str, is_prefix: bool = True) -> str:
|
|
81
|
+
"""Prepare a search term for FTS5 query.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
term: The search term to prepare
|
|
85
|
+
is_prefix: Whether to add prefix search capability (* suffix)
|
|
86
|
+
|
|
87
|
+
For FTS5:
|
|
88
|
+
- Special characters and phrases need to be quoted
|
|
89
|
+
- Terms with spaces or special chars need quotes
|
|
80
90
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
if "*" in term:
|
|
92
|
+
return term
|
|
93
|
+
|
|
94
|
+
# List of special characters that need quoting (excluding *)
|
|
95
|
+
special_chars = ["/", "-", ".", " ", "(", ")", "[", "]", '"', "'"]
|
|
83
96
|
|
|
84
97
|
# Check if term contains any special characters
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
needs_quotes = any(c in term for c in special_chars)
|
|
99
|
+
|
|
100
|
+
if needs_quotes:
|
|
101
|
+
# If the term already contains quotes, escape them and add a wildcard
|
|
87
102
|
term = term.replace('"', '""')
|
|
88
|
-
|
|
103
|
+
term = f'"{term}"*'
|
|
104
|
+
|
|
89
105
|
return term
|
|
90
106
|
|
|
91
107
|
async def search(
|
|
@@ -106,14 +122,14 @@ class SearchRepository:
|
|
|
106
122
|
|
|
107
123
|
# Handle text search for title and content
|
|
108
124
|
if search_text:
|
|
109
|
-
search_text = self.
|
|
110
|
-
params["text"] =
|
|
125
|
+
search_text = self._prepare_search_term(search_text.strip())
|
|
126
|
+
params["text"] = search_text
|
|
111
127
|
conditions.append("(title MATCH :text OR content MATCH :text)")
|
|
112
128
|
|
|
113
129
|
# Handle title match search
|
|
114
130
|
if title:
|
|
115
|
-
title_text = self.
|
|
116
|
-
params["text"] =
|
|
131
|
+
title_text = self._prepare_search_term(title.strip())
|
|
132
|
+
params["text"] = title_text
|
|
117
133
|
conditions.append("title MATCH :text")
|
|
118
134
|
|
|
119
135
|
# Handle permalink exact search
|
|
@@ -123,8 +139,15 @@ class SearchRepository:
|
|
|
123
139
|
|
|
124
140
|
# Handle permalink match search, supports *
|
|
125
141
|
if permalink_match:
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
# Clean and prepare permalink for FTS5 GLOB match
|
|
143
|
+
permalink_text = self._prepare_search_term(
|
|
144
|
+
permalink_match.lower().strip(), is_prefix=False
|
|
145
|
+
)
|
|
146
|
+
params["permalink"] = permalink_text
|
|
147
|
+
if "*" in permalink_match:
|
|
148
|
+
conditions.append("permalink GLOB :permalink")
|
|
149
|
+
else:
|
|
150
|
+
conditions.append("permalink MATCH :permalink")
|
|
128
151
|
|
|
129
152
|
# Handle type filter
|
|
130
153
|
if types:
|
|
@@ -173,7 +196,7 @@ class SearchRepository:
|
|
|
173
196
|
LIMIT :limit
|
|
174
197
|
"""
|
|
175
198
|
|
|
176
|
-
|
|
199
|
+
logger.debug(f"Search {sql} params: {params}")
|
|
177
200
|
async with db.scoped_session(self.session_maker) as session:
|
|
178
201
|
result = await session.execute(text(sql), params)
|
|
179
202
|
rows = result.fetchall()
|
|
@@ -199,8 +222,11 @@ class SearchRepository:
|
|
|
199
222
|
for row in rows
|
|
200
223
|
]
|
|
201
224
|
|
|
202
|
-
|
|
203
|
-
|
|
225
|
+
logger.debug(f"Found {len(results)} search results")
|
|
226
|
+
for r in results:
|
|
227
|
+
logger.debug(
|
|
228
|
+
f"Search result: type:{r.type} title: {r.title} permalink: {r.permalink} score: {r.score}"
|
|
229
|
+
)
|
|
204
230
|
|
|
205
231
|
return results
|
|
206
232
|
|
|
@@ -233,7 +259,7 @@ class SearchRepository:
|
|
|
233
259
|
"""),
|
|
234
260
|
search_index_row.to_insert(),
|
|
235
261
|
)
|
|
236
|
-
logger.debug(f"indexed
|
|
262
|
+
logger.debug(f"indexed row {search_index_row}")
|
|
237
263
|
await session.commit()
|
|
238
264
|
|
|
239
265
|
async def delete_by_permalink(self, permalink: str):
|
|
@@ -11,6 +11,7 @@ Key Features:
|
|
|
11
11
|
4. Bulk operations return all affected items
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
from datetime import datetime
|
|
14
15
|
from typing import List, Optional, Dict
|
|
15
16
|
|
|
16
17
|
from pydantic import BaseModel, ConfigDict, Field, AliasPath, AliasChoices
|
|
@@ -43,6 +44,8 @@ class ObservationResponse(Observation, SQLAlchemyModel):
|
|
|
43
44
|
}
|
|
44
45
|
"""
|
|
45
46
|
|
|
47
|
+
permalink: Permalink
|
|
48
|
+
|
|
46
49
|
|
|
47
50
|
class RelationResponse(Relation, SQLAlchemyModel):
|
|
48
51
|
"""Response schema for relation operations.
|
|
@@ -59,6 +62,8 @@ class RelationResponse(Relation, SQLAlchemyModel):
|
|
|
59
62
|
}
|
|
60
63
|
"""
|
|
61
64
|
|
|
65
|
+
permalink: Permalink
|
|
66
|
+
|
|
62
67
|
from_id: Permalink = Field(
|
|
63
68
|
# use the permalink from the associated Entity
|
|
64
69
|
# or the from_id value
|
|
@@ -131,9 +136,12 @@ class EntityResponse(SQLAlchemyModel):
|
|
|
131
136
|
file_path: str
|
|
132
137
|
entity_type: EntityType
|
|
133
138
|
entity_metadata: Optional[Dict] = None
|
|
139
|
+
checksum: Optional[str] = None
|
|
134
140
|
content_type: ContentType
|
|
135
141
|
observations: List[ObservationResponse] = []
|
|
136
142
|
relations: List[RelationResponse] = []
|
|
143
|
+
created_at: datetime
|
|
144
|
+
updated_at: datetime
|
|
137
145
|
|
|
138
146
|
|
|
139
147
|
class EntityListResponse(SQLAlchemyModel):
|
|
@@ -185,6 +185,11 @@ class EntityService(BaseService[EntityModel]):
|
|
|
185
185
|
raise EntityNotFoundError(f"Entity not found: {permalink}")
|
|
186
186
|
return db_entity
|
|
187
187
|
|
|
188
|
+
async def get_entities_by_id(self, ids: List[int]) -> Sequence[EntityModel]:
|
|
189
|
+
"""Get specific entities and their relationships."""
|
|
190
|
+
logger.debug(f"Getting entities: {ids}")
|
|
191
|
+
return await self.repository.find_by_ids(ids)
|
|
192
|
+
|
|
188
193
|
async def get_entities_by_permalinks(self, permalinks: List[str]) -> Sequence[EntityModel]:
|
|
189
194
|
"""Get specific nodes and their relationships."""
|
|
190
195
|
logger.debug(f"Getting entities permalinks: {permalinks}")
|
|
@@ -16,9 +16,10 @@ class LinkResolver:
|
|
|
16
16
|
|
|
17
17
|
Uses a combination of exact matching and search-based resolution:
|
|
18
18
|
1. Try exact permalink match (fastest)
|
|
19
|
-
2. Try
|
|
20
|
-
3.
|
|
21
|
-
4.
|
|
19
|
+
2. Try permalink pattern match (for wildcards)
|
|
20
|
+
3. Try exact title match
|
|
21
|
+
4. Fall back to search for fuzzy matching
|
|
22
|
+
5. Generate new permalink if no match found
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
25
|
def __init__(self, entity_repository: EntityRepository, search_service: SearchService):
|
|
@@ -45,8 +46,8 @@ class LinkResolver:
|
|
|
45
46
|
logger.debug(f"Found title match: {entity.title}")
|
|
46
47
|
return entity
|
|
47
48
|
|
|
48
|
-
if use_search:
|
|
49
|
-
# 3. Fall back to search for fuzzy matching on title
|
|
49
|
+
if use_search and "*" not in clean_text:
|
|
50
|
+
# 3. Fall back to search for fuzzy matching on title
|
|
50
51
|
results = await self.search_service.search(
|
|
51
52
|
query=SearchQuery(title=clean_text, types=[SearchItemType.ENTITY]),
|
|
52
53
|
)
|
|
@@ -59,7 +60,7 @@ class LinkResolver:
|
|
|
59
60
|
)
|
|
60
61
|
return await self.entity_repository.get_by_permalink(best_match.permalink)
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
# if we couldn't find anything then return None
|
|
63
64
|
return None
|
|
64
65
|
|
|
65
66
|
def _normalize_link_text(self, link_text: str) -> Tuple[str, Optional[str]]:
|
|
@@ -87,7 +88,7 @@ class LinkResolver:
|
|
|
87
88
|
|
|
88
89
|
return text, alias
|
|
89
90
|
|
|
90
|
-
def _select_best_match(self, search_text: str, results: List[SearchIndexRow]) ->
|
|
91
|
+
def _select_best_match(self, search_text: str, results: List[SearchIndexRow]) -> SearchIndexRow:
|
|
91
92
|
"""Select best match from search results.
|
|
92
93
|
|
|
93
94
|
Uses multiple criteria:
|