basic-memory 0.17.1__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.
- basic_memory/__init__.py +7 -0
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +185 -0
- basic_memory/alembic/migrations.py +24 -0
- basic_memory/alembic/script.py.mako +26 -0
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
- basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/api/__init__.py +5 -0
- basic_memory/api/app.py +131 -0
- basic_memory/api/routers/__init__.py +11 -0
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +318 -0
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +90 -0
- basic_memory/api/routers/project_router.py +448 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +249 -0
- basic_memory/api/routers/search_router.py +36 -0
- basic_memory/api/routers/utils.py +169 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +182 -0
- basic_memory/api/v2/routers/knowledge_router.py +413 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +342 -0
- basic_memory/api/v2/routers/prompt_router.py +270 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/__init__.py +1 -0
- basic_memory/cli/app.py +84 -0
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/__init__.py +18 -0
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
- basic_memory/cli/commands/cloud/upload.py +233 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +77 -0
- basic_memory/cli/commands/db.py +44 -0
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +84 -0
- basic_memory/cli/commands/import_claude_conversations.py +87 -0
- basic_memory/cli/commands/import_claude_projects.py +86 -0
- basic_memory/cli/commands/import_memory_json.py +87 -0
- basic_memory/cli/commands/mcp.py +76 -0
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +174 -0
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +28 -0
- basic_memory/config.py +616 -0
- basic_memory/db.py +394 -0
- basic_memory/deps.py +705 -0
- basic_memory/file_utils.py +478 -0
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +232 -0
- basic_memory/importers/claude_conversations_importer.py +180 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +108 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/__init__.py +21 -0
- basic_memory/markdown/entity_parser.py +279 -0
- basic_memory/markdown/markdown_processor.py +160 -0
- basic_memory/markdown/plugins.py +242 -0
- basic_memory/markdown/schemas.py +70 -0
- basic_memory/markdown/utils.py +117 -0
- basic_memory/mcp/__init__.py +1 -0
- basic_memory/mcp/async_client.py +139 -0
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +81 -0
- basic_memory/mcp/tools/__init__.py +48 -0
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +152 -0
- basic_memory/mcp/tools/chatgpt_tools.py +190 -0
- basic_memory/mcp/tools/delete_note.py +242 -0
- basic_memory/mcp/tools/edit_note.py +324 -0
- basic_memory/mcp/tools/list_directory.py +168 -0
- basic_memory/mcp/tools/move_note.py +551 -0
- basic_memory/mcp/tools/project_management.py +201 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +267 -0
- basic_memory/mcp/tools/recent_activity.py +534 -0
- basic_memory/mcp/tools/search.py +385 -0
- basic_memory/mcp/tools/utils.py +540 -0
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +15 -0
- basic_memory/models/base.py +10 -0
- basic_memory/models/knowledge.py +226 -0
- basic_memory/models/project.py +87 -0
- basic_memory/models/search.py +85 -0
- basic_memory/repository/__init__.py +11 -0
- basic_memory/repository/entity_repository.py +503 -0
- basic_memory/repository/observation_repository.py +73 -0
- basic_memory/repository/postgres_search_repository.py +379 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +128 -0
- basic_memory/repository/relation_repository.py +146 -0
- basic_memory/repository/repository.py +385 -0
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +94 -0
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +439 -0
- basic_memory/schemas/__init__.py +86 -0
- basic_memory/schemas/base.py +297 -0
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/delete.py +37 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +285 -0
- basic_memory/schemas/project_info.py +212 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +112 -0
- basic_memory/schemas/response.py +229 -0
- basic_memory/schemas/search.py +117 -0
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +129 -0
- basic_memory/schemas/v2/resource.py +46 -0
- basic_memory/services/__init__.py +8 -0
- basic_memory/services/context_service.py +601 -0
- basic_memory/services/directory_service.py +308 -0
- basic_memory/services/entity_service.py +864 -0
- basic_memory/services/exceptions.py +37 -0
- basic_memory/services/file_service.py +541 -0
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +121 -0
- basic_memory/services/project_service.py +880 -0
- basic_memory/services/search_service.py +404 -0
- basic_memory/services/service.py +15 -0
- basic_memory/sync/__init__.py +6 -0
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1259 -0
- basic_memory/sync/watch_service.py +510 -0
- basic_memory/telemetry.py +249 -0
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +468 -0
- basic_memory-0.17.1.dist-info/METADATA +617 -0
- basic_memory-0.17.1.dist-info/RECORD +171 -0
- basic_memory-0.17.1.dist-info/WHEEL +4 -0
- basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
- basic_memory-0.17.1.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Knowledge graph schema exports.
|
|
2
|
+
|
|
3
|
+
This module exports all schema classes to simplify imports.
|
|
4
|
+
Rather than importing from individual schema files, you can
|
|
5
|
+
import everything from basic_memory.schemas.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Base types and models
|
|
9
|
+
from basic_memory.schemas.base import (
|
|
10
|
+
Observation,
|
|
11
|
+
EntityType,
|
|
12
|
+
RelationType,
|
|
13
|
+
Relation,
|
|
14
|
+
Entity,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Delete operation models
|
|
18
|
+
from basic_memory.schemas.delete import (
|
|
19
|
+
DeleteEntitiesRequest,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Request models
|
|
23
|
+
from basic_memory.schemas.request import (
|
|
24
|
+
SearchNodesRequest,
|
|
25
|
+
GetEntitiesRequest,
|
|
26
|
+
CreateRelationsRequest,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Response models
|
|
30
|
+
from basic_memory.schemas.response import (
|
|
31
|
+
SQLAlchemyModel,
|
|
32
|
+
ObservationResponse,
|
|
33
|
+
RelationResponse,
|
|
34
|
+
EntityResponse,
|
|
35
|
+
EntityListResponse,
|
|
36
|
+
SearchNodesResponse,
|
|
37
|
+
DeleteEntitiesResponse,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from basic_memory.schemas.project_info import (
|
|
41
|
+
ProjectStatistics,
|
|
42
|
+
ActivityMetrics,
|
|
43
|
+
SystemStatus,
|
|
44
|
+
ProjectInfoResponse,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
from basic_memory.schemas.directory import (
|
|
48
|
+
DirectoryNode,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from basic_memory.schemas.sync_report import (
|
|
52
|
+
SyncReportResponse,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# For convenient imports, export all models
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Base
|
|
58
|
+
"Observation",
|
|
59
|
+
"EntityType",
|
|
60
|
+
"RelationType",
|
|
61
|
+
"Relation",
|
|
62
|
+
"Entity",
|
|
63
|
+
# Requests
|
|
64
|
+
"SearchNodesRequest",
|
|
65
|
+
"GetEntitiesRequest",
|
|
66
|
+
"CreateRelationsRequest",
|
|
67
|
+
# Responses
|
|
68
|
+
"SQLAlchemyModel",
|
|
69
|
+
"ObservationResponse",
|
|
70
|
+
"RelationResponse",
|
|
71
|
+
"EntityResponse",
|
|
72
|
+
"EntityListResponse",
|
|
73
|
+
"SearchNodesResponse",
|
|
74
|
+
"DeleteEntitiesResponse",
|
|
75
|
+
# Delete Operations
|
|
76
|
+
"DeleteEntitiesRequest",
|
|
77
|
+
# Project Info
|
|
78
|
+
"ProjectStatistics",
|
|
79
|
+
"ActivityMetrics",
|
|
80
|
+
"SystemStatus",
|
|
81
|
+
"ProjectInfoResponse",
|
|
82
|
+
# Directory
|
|
83
|
+
"DirectoryNode",
|
|
84
|
+
# Sync
|
|
85
|
+
"SyncReportResponse",
|
|
86
|
+
]
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Core pydantic models for basic-memory entities, observations, and relations.
|
|
2
|
+
|
|
3
|
+
This module defines the foundational data structures for the knowledge graph system.
|
|
4
|
+
The graph consists of entities (nodes) connected by relations (edges), where each
|
|
5
|
+
entity can have multiple observations (facts) attached to it.
|
|
6
|
+
|
|
7
|
+
Key Concepts:
|
|
8
|
+
1. Entities are nodes storing factual observations
|
|
9
|
+
2. Relations are directed edges between entities using active voice verbs
|
|
10
|
+
3. Observations are atomic facts/notes about an entity
|
|
11
|
+
4. Everything is stored in both SQLite and markdown files
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import mimetypes
|
|
16
|
+
import re
|
|
17
|
+
from datetime import datetime, timedelta
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List, Optional, Annotated, Dict
|
|
20
|
+
|
|
21
|
+
from annotated_types import MinLen, MaxLen
|
|
22
|
+
from dateparser import parse
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, BeforeValidator, Field, model_validator, computed_field
|
|
25
|
+
|
|
26
|
+
from basic_memory.config import ConfigManager
|
|
27
|
+
from basic_memory.file_utils import sanitize_for_filename, sanitize_for_folder
|
|
28
|
+
from basic_memory.utils import generate_permalink
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def has_valid_file_extension(filename: str) -> bool:
|
|
32
|
+
"""Check if a filename has a valid file extension recognized by mimetypes.
|
|
33
|
+
|
|
34
|
+
This is used to determine whether to split the extension when processing
|
|
35
|
+
titles in kebab_filenames mode. Prevents treating periods in version numbers
|
|
36
|
+
or decimals as file extensions.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
filename: The filename to check
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
True if the filename has a recognized file extension, False otherwise
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
>>> has_valid_file_extension("document.md")
|
|
46
|
+
True
|
|
47
|
+
>>> has_valid_file_extension("Version 2.0.0")
|
|
48
|
+
False
|
|
49
|
+
>>> has_valid_file_extension("image.png")
|
|
50
|
+
True
|
|
51
|
+
"""
|
|
52
|
+
mime_type, _ = mimetypes.guess_type(filename)
|
|
53
|
+
return mime_type is not None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def to_snake_case(name: str) -> str:
|
|
57
|
+
"""Convert a string to snake_case.
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
BasicMemory -> basic_memory
|
|
61
|
+
Memory Service -> memory_service
|
|
62
|
+
memory-service -> memory_service
|
|
63
|
+
Memory_Service -> memory_service
|
|
64
|
+
"""
|
|
65
|
+
name = name.strip()
|
|
66
|
+
|
|
67
|
+
# Replace spaces and hyphens and . with underscores
|
|
68
|
+
s1 = re.sub(r"[\s\-\\.]", "_", name)
|
|
69
|
+
|
|
70
|
+
# Insert underscore between camelCase
|
|
71
|
+
s2 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1)
|
|
72
|
+
|
|
73
|
+
# Convert to lowercase
|
|
74
|
+
return s2.lower()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_timeframe(timeframe: str) -> datetime:
|
|
78
|
+
"""Parse timeframe with special handling for 'today' and other natural language expressions.
|
|
79
|
+
|
|
80
|
+
Enforces a minimum 1-day lookback to handle timezone differences in distributed deployments.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
timeframe: Natural language timeframe like 'today', '1d', '1 week ago', etc.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
datetime: The parsed datetime for the start of the timeframe, timezone-aware in local system timezone
|
|
87
|
+
Always returns at least 1 day ago to handle timezone differences.
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
parse_timeframe('today') -> 2025-06-04 14:50:00-07:00 (1 day ago, not start of today)
|
|
91
|
+
parse_timeframe('1h') -> 2025-06-04 14:50:00-07:00 (1 day ago, not 1 hour ago)
|
|
92
|
+
parse_timeframe('1d') -> 2025-06-04 14:50:00-07:00 (24 hours ago with local timezone)
|
|
93
|
+
parse_timeframe('1 week ago') -> 2025-05-29 14:50:00-07:00 (1 week ago with local timezone)
|
|
94
|
+
"""
|
|
95
|
+
if timeframe.lower() == "today":
|
|
96
|
+
# For "today", return 1 day ago to ensure we capture recent activity across timezones
|
|
97
|
+
# This handles the case where client and server are in different timezones
|
|
98
|
+
now = datetime.now()
|
|
99
|
+
one_day_ago = now - timedelta(days=1)
|
|
100
|
+
return one_day_ago.astimezone()
|
|
101
|
+
else:
|
|
102
|
+
# Use dateparser for other formats
|
|
103
|
+
parsed = parse(timeframe)
|
|
104
|
+
if not parsed:
|
|
105
|
+
raise ValueError(f"Could not parse timeframe: {timeframe}")
|
|
106
|
+
|
|
107
|
+
# If the parsed datetime is naive, make it timezone-aware in local system timezone
|
|
108
|
+
if parsed.tzinfo is None:
|
|
109
|
+
parsed = parsed.astimezone()
|
|
110
|
+
else:
|
|
111
|
+
parsed = parsed
|
|
112
|
+
|
|
113
|
+
# Enforce minimum 1-day lookback to handle timezone differences
|
|
114
|
+
# This ensures we don't miss recent activity due to client/server timezone mismatches
|
|
115
|
+
now = datetime.now().astimezone()
|
|
116
|
+
one_day_ago = now - timedelta(days=1)
|
|
117
|
+
|
|
118
|
+
# If the parsed time is more recent than 1 day ago, use 1 day ago instead
|
|
119
|
+
if parsed > one_day_ago:
|
|
120
|
+
return one_day_ago
|
|
121
|
+
else:
|
|
122
|
+
return parsed
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_timeframe(timeframe: str) -> str:
|
|
126
|
+
"""Convert human readable timeframes to a duration relative to the current time."""
|
|
127
|
+
if not isinstance(timeframe, str):
|
|
128
|
+
raise ValueError("Timeframe must be a string")
|
|
129
|
+
|
|
130
|
+
# Preserve special timeframe strings that need custom handling
|
|
131
|
+
special_timeframes = ["today"]
|
|
132
|
+
if timeframe.lower() in special_timeframes:
|
|
133
|
+
return timeframe.lower()
|
|
134
|
+
|
|
135
|
+
# Parse relative time expression using our enhanced parser
|
|
136
|
+
parsed = parse_timeframe(timeframe)
|
|
137
|
+
|
|
138
|
+
# Convert to duration
|
|
139
|
+
now = datetime.now().astimezone()
|
|
140
|
+
if parsed > now:
|
|
141
|
+
raise ValueError("Timeframe cannot be in the future")
|
|
142
|
+
|
|
143
|
+
# Could format the duration back to our standard format
|
|
144
|
+
days = (now - parsed).days
|
|
145
|
+
|
|
146
|
+
# Could enforce reasonable limits
|
|
147
|
+
if days > 365:
|
|
148
|
+
raise ValueError("Timeframe should be <= 1 year")
|
|
149
|
+
|
|
150
|
+
return f"{days}d"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
TimeFrame = Annotated[str, BeforeValidator(validate_timeframe)]
|
|
154
|
+
|
|
155
|
+
Permalink = Annotated[str, MinLen(1)]
|
|
156
|
+
"""Unique identifier in format '{path}/{normalized_name}'."""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
EntityType = Annotated[str, BeforeValidator(to_snake_case), MinLen(1), MaxLen(200)]
|
|
160
|
+
"""Classification of entity (e.g., 'person', 'project', 'concept'). """
|
|
161
|
+
|
|
162
|
+
ALLOWED_CONTENT_TYPES = {
|
|
163
|
+
"text/markdown",
|
|
164
|
+
"text/plain",
|
|
165
|
+
"application/pdf",
|
|
166
|
+
"image/jpeg",
|
|
167
|
+
"image/png",
|
|
168
|
+
"image/svg+xml",
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
ContentType = Annotated[
|
|
172
|
+
str,
|
|
173
|
+
BeforeValidator(str.lower),
|
|
174
|
+
Field(pattern=r"^[\w\-\+\.]+/[\w\-\+\.]+$"),
|
|
175
|
+
Field(json_schema_extra={"examples": list(ALLOWED_CONTENT_TYPES)}),
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
RelationType = Annotated[str, MinLen(1), MaxLen(200)]
|
|
180
|
+
"""Type of relationship between entities. Always use active voice present tense."""
|
|
181
|
+
|
|
182
|
+
ObservationStr = Annotated[
|
|
183
|
+
str,
|
|
184
|
+
BeforeValidator(str.strip), # Clean whitespace
|
|
185
|
+
MinLen(1), # Ensure non-empty after stripping
|
|
186
|
+
# No MaxLen - matches DB Text column which has no length restriction
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class Observation(BaseModel):
|
|
191
|
+
"""A single observation with category, content, and optional context."""
|
|
192
|
+
|
|
193
|
+
category: Optional[str] = None
|
|
194
|
+
content: ObservationStr
|
|
195
|
+
tags: Optional[List[str]] = Field(default_factory=list)
|
|
196
|
+
context: Optional[str] = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Relation(BaseModel):
|
|
200
|
+
"""Represents a directed edge between entities in the knowledge graph.
|
|
201
|
+
|
|
202
|
+
Relations are directed connections stored in active voice (e.g., "created", "depends_on").
|
|
203
|
+
The from_permalink represents the source or actor entity, while to_permalink represents the target
|
|
204
|
+
or recipient entity.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
from_id: Permalink
|
|
208
|
+
to_id: Permalink
|
|
209
|
+
relation_type: RelationType
|
|
210
|
+
context: Optional[str] = None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Entity(BaseModel):
|
|
214
|
+
"""Represents a node in our knowledge graph - could be a person, project, concept, etc.
|
|
215
|
+
|
|
216
|
+
Each entity has:
|
|
217
|
+
- A file path (e.g., "people/jane-doe.md")
|
|
218
|
+
- An entity type (for classification)
|
|
219
|
+
- A list of observations (facts/notes about the entity)
|
|
220
|
+
- Optional relations to other entities
|
|
221
|
+
- Optional description for high-level overview
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
# private field to override permalink
|
|
225
|
+
# Use empty string "" as sentinel to indicate permalinks are explicitly disabled
|
|
226
|
+
_permalink: Optional[str] = None
|
|
227
|
+
|
|
228
|
+
title: str
|
|
229
|
+
content: Optional[str] = None
|
|
230
|
+
folder: str
|
|
231
|
+
entity_type: EntityType = "note"
|
|
232
|
+
entity_metadata: Optional[Dict] = Field(default=None, description="Optional metadata")
|
|
233
|
+
content_type: ContentType = Field(
|
|
234
|
+
description="MIME type of the content (e.g. text/markdown, image/jpeg)",
|
|
235
|
+
examples=["text/markdown", "image/jpeg"],
|
|
236
|
+
default="text/markdown",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def __init__(self, **data):
|
|
240
|
+
data["folder"] = sanitize_for_folder(data.get("folder", ""))
|
|
241
|
+
super().__init__(**data)
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def safe_title(self) -> str:
|
|
245
|
+
"""
|
|
246
|
+
A sanitized version of the title, which is safe for use on the filesystem. For example,
|
|
247
|
+
a title of "Coupon Enable/Disable Feature" should create a the file as "Coupon Enable-Disable Feature.md"
|
|
248
|
+
instead of creating a file named "Disable Feature.md" beneath the "Coupon Enable" directory.
|
|
249
|
+
|
|
250
|
+
Replaces POSIX and/or Windows style slashes as well as a few other characters that are not safe for filenames.
|
|
251
|
+
If kebab_filenames is True, then behavior is consistent with transformation used when generating permalink
|
|
252
|
+
strings (e.g. "Coupon Enable/Disable Feature" -> "coupon-enable-disable-feature").
|
|
253
|
+
"""
|
|
254
|
+
fixed_title = sanitize_for_filename(self.title)
|
|
255
|
+
|
|
256
|
+
app_config = ConfigManager().config
|
|
257
|
+
use_kebab_case = app_config.kebab_filenames
|
|
258
|
+
|
|
259
|
+
if use_kebab_case:
|
|
260
|
+
# Convert to kebab-case: lowercase with hyphens, preserving periods in version numbers
|
|
261
|
+
# generate_permalink() uses mimetypes to detect real file extensions and only splits
|
|
262
|
+
# them off, avoiding misinterpreting periods in version numbers as extensions
|
|
263
|
+
has_extension = has_valid_file_extension(fixed_title)
|
|
264
|
+
fixed_title = generate_permalink(file_path=fixed_title, split_extension=has_extension)
|
|
265
|
+
|
|
266
|
+
return fixed_title
|
|
267
|
+
|
|
268
|
+
@computed_field
|
|
269
|
+
@property
|
|
270
|
+
def file_path(self) -> str:
|
|
271
|
+
"""Get the file path for this entity based on its permalink."""
|
|
272
|
+
safe_title = self.safe_title
|
|
273
|
+
if self.content_type == "text/markdown":
|
|
274
|
+
return (
|
|
275
|
+
os.path.join(self.folder, f"{safe_title}.md") if self.folder else f"{safe_title}.md"
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
return os.path.join(self.folder, safe_title) if self.folder else safe_title
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def permalink(self) -> Optional[Permalink]:
|
|
282
|
+
"""Get a url friendly path}."""
|
|
283
|
+
# Empty string is a sentinel value indicating permalinks are disabled
|
|
284
|
+
if self._permalink == "":
|
|
285
|
+
return None
|
|
286
|
+
return self._permalink or generate_permalink(self.file_path)
|
|
287
|
+
|
|
288
|
+
@model_validator(mode="after")
|
|
289
|
+
def infer_content_type(self) -> "Entity": # pragma: no cover
|
|
290
|
+
if not self.content_type:
|
|
291
|
+
path = Path(self.file_path)
|
|
292
|
+
if not path.exists():
|
|
293
|
+
self.content_type = "text/plain"
|
|
294
|
+
else:
|
|
295
|
+
mime_type, _ = mimetypes.guess_type(path.name)
|
|
296
|
+
self.content_type = mime_type or "text/plain"
|
|
297
|
+
return self
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Schemas for cloud-related API responses."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TenantMountInfo(BaseModel):
|
|
7
|
+
"""Response from /tenant/mount/info endpoint."""
|
|
8
|
+
|
|
9
|
+
tenant_id: str = Field(..., description="Unique identifier for the tenant")
|
|
10
|
+
bucket_name: str = Field(..., description="S3 bucket name for the tenant")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MountCredentials(BaseModel):
|
|
14
|
+
"""Response from /tenant/mount/credentials endpoint."""
|
|
15
|
+
|
|
16
|
+
access_key: str = Field(..., description="S3 access key for mount")
|
|
17
|
+
secret_key: str = Field(..., description="S3 secret key for mount")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CloudProject(BaseModel):
|
|
21
|
+
"""Representation of a cloud project."""
|
|
22
|
+
|
|
23
|
+
name: str = Field(..., description="Project name")
|
|
24
|
+
path: str = Field(..., description="Project path on cloud")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CloudProjectList(BaseModel):
|
|
28
|
+
"""Response from /proxy/projects/projects endpoint."""
|
|
29
|
+
|
|
30
|
+
projects: list[CloudProject] = Field(default_factory=list, description="List of cloud projects")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CloudProjectCreateRequest(BaseModel):
|
|
34
|
+
"""Request to create a new cloud project."""
|
|
35
|
+
|
|
36
|
+
name: str = Field(..., description="Project name")
|
|
37
|
+
path: str = Field(..., description="Project path (permalink)")
|
|
38
|
+
set_default: bool = Field(default=False, description="Set as default project")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CloudProjectCreateResponse(BaseModel):
|
|
42
|
+
"""Response from creating a cloud project."""
|
|
43
|
+
|
|
44
|
+
message: str = Field(..., description="Status message about the project creation")
|
|
45
|
+
status: str = Field(..., description="Status of the creation (success or error)")
|
|
46
|
+
default: bool = Field(..., description="True if the project was set as the default")
|
|
47
|
+
old_project: dict | None = Field(None, description="Information about the previous project")
|
|
48
|
+
new_project: dict | None = Field(
|
|
49
|
+
None, description="Information about the newly created project"
|
|
50
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Delete operation schemas for the knowledge graph.
|
|
2
|
+
|
|
3
|
+
This module defines the request schemas for removing entities, relations,
|
|
4
|
+
and observations from the knowledge graph. Each operation has specific
|
|
5
|
+
implications and safety considerations.
|
|
6
|
+
|
|
7
|
+
Deletion Hierarchy:
|
|
8
|
+
1. Entity deletion removes the entity and all its relations
|
|
9
|
+
2. Relation deletion only removes the connection between entities
|
|
10
|
+
3. Observation deletion preserves entity and relations
|
|
11
|
+
|
|
12
|
+
Key Considerations:
|
|
13
|
+
- All deletions are permanent
|
|
14
|
+
- Entity deletions cascade to relations
|
|
15
|
+
- Files are removed along with entities
|
|
16
|
+
- Operations are atomic - they fully succeed or fail
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import List, Annotated
|
|
20
|
+
|
|
21
|
+
from annotated_types import MinLen
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
from basic_memory.schemas.base import Permalink
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DeleteEntitiesRequest(BaseModel):
|
|
28
|
+
"""Delete one or more entities from the knowledge graph.
|
|
29
|
+
|
|
30
|
+
This operation:
|
|
31
|
+
1. Removes the entity from the database
|
|
32
|
+
2. Deletes all observations attached to the entity
|
|
33
|
+
3. Removes all relations where the entity is source or target
|
|
34
|
+
4. Deletes the corresponding markdown file
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
permalinks: Annotated[List[Permalink], MinLen(1)]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Schemas for directory tree operations."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import List, Optional, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DirectoryNode(BaseModel):
|
|
10
|
+
"""Directory node in file system."""
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
file_path: Optional[str] = None # Original path without leading slash (matches DB)
|
|
14
|
+
directory_path: str # Path with leading slash for directory navigation
|
|
15
|
+
type: Literal["directory", "file"]
|
|
16
|
+
children: List["DirectoryNode"] = [] # Default to empty list
|
|
17
|
+
title: Optional[str] = None
|
|
18
|
+
permalink: Optional[str] = None
|
|
19
|
+
entity_id: Optional[int] = None
|
|
20
|
+
entity_type: Optional[str] = None
|
|
21
|
+
content_type: Optional[str] = None
|
|
22
|
+
updated_at: Optional[datetime] = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def has_children(self) -> bool:
|
|
26
|
+
return bool(self.children)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Support for recursive model
|
|
30
|
+
DirectoryNode.model_rebuild()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Schemas for import services."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ImportResult(BaseModel):
|
|
9
|
+
"""Common import result schema."""
|
|
10
|
+
|
|
11
|
+
import_count: Dict[str, int]
|
|
12
|
+
success: bool
|
|
13
|
+
error_message: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChatImportResult(ImportResult):
|
|
17
|
+
"""Result schema for chat imports."""
|
|
18
|
+
|
|
19
|
+
conversations: int = 0
|
|
20
|
+
messages: int = 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProjectImportResult(ImportResult):
|
|
24
|
+
"""Result schema for project imports."""
|
|
25
|
+
|
|
26
|
+
documents: int = 0
|
|
27
|
+
prompts: int = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EntityImportResult(ImportResult):
|
|
31
|
+
"""Result schema for entity imports."""
|
|
32
|
+
|
|
33
|
+
entities: int = 0
|
|
34
|
+
relations: int = 0
|
|
35
|
+
skipped_entities: int = 0
|