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.
Files changed (171) hide show
  1. basic_memory/__init__.py +7 -0
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +185 -0
  4. basic_memory/alembic/migrations.py +24 -0
  5. basic_memory/alembic/script.py.mako +26 -0
  6. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  7. basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
  8. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  9. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  10. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  11. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  12. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  13. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  14. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  15. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  16. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  17. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  18. basic_memory/api/__init__.py +5 -0
  19. basic_memory/api/app.py +131 -0
  20. basic_memory/api/routers/__init__.py +11 -0
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +318 -0
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +90 -0
  26. basic_memory/api/routers/project_router.py +448 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +249 -0
  29. basic_memory/api/routers/search_router.py +36 -0
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +182 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +413 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +342 -0
  39. basic_memory/api/v2/routers/prompt_router.py +270 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/__init__.py +1 -0
  43. basic_memory/cli/app.py +84 -0
  44. basic_memory/cli/auth.py +277 -0
  45. basic_memory/cli/commands/__init__.py +18 -0
  46. basic_memory/cli/commands/cloud/__init__.py +6 -0
  47. basic_memory/cli/commands/cloud/api_client.py +112 -0
  48. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  49. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  50. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  51. basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
  52. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  53. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  54. basic_memory/cli/commands/cloud/upload.py +233 -0
  55. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  56. basic_memory/cli/commands/command_utils.py +77 -0
  57. basic_memory/cli/commands/db.py +44 -0
  58. basic_memory/cli/commands/format.py +198 -0
  59. basic_memory/cli/commands/import_chatgpt.py +84 -0
  60. basic_memory/cli/commands/import_claude_conversations.py +87 -0
  61. basic_memory/cli/commands/import_claude_projects.py +86 -0
  62. basic_memory/cli/commands/import_memory_json.py +87 -0
  63. basic_memory/cli/commands/mcp.py +76 -0
  64. basic_memory/cli/commands/project.py +889 -0
  65. basic_memory/cli/commands/status.py +174 -0
  66. basic_memory/cli/commands/telemetry.py +81 -0
  67. basic_memory/cli/commands/tool.py +341 -0
  68. basic_memory/cli/main.py +28 -0
  69. basic_memory/config.py +616 -0
  70. basic_memory/db.py +394 -0
  71. basic_memory/deps.py +705 -0
  72. basic_memory/file_utils.py +478 -0
  73. basic_memory/ignore_utils.py +297 -0
  74. basic_memory/importers/__init__.py +27 -0
  75. basic_memory/importers/base.py +79 -0
  76. basic_memory/importers/chatgpt_importer.py +232 -0
  77. basic_memory/importers/claude_conversations_importer.py +180 -0
  78. basic_memory/importers/claude_projects_importer.py +148 -0
  79. basic_memory/importers/memory_json_importer.py +108 -0
  80. basic_memory/importers/utils.py +61 -0
  81. basic_memory/markdown/__init__.py +21 -0
  82. basic_memory/markdown/entity_parser.py +279 -0
  83. basic_memory/markdown/markdown_processor.py +160 -0
  84. basic_memory/markdown/plugins.py +242 -0
  85. basic_memory/markdown/schemas.py +70 -0
  86. basic_memory/markdown/utils.py +117 -0
  87. basic_memory/mcp/__init__.py +1 -0
  88. basic_memory/mcp/async_client.py +139 -0
  89. basic_memory/mcp/project_context.py +141 -0
  90. basic_memory/mcp/prompts/__init__.py +19 -0
  91. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  92. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  93. basic_memory/mcp/prompts/recent_activity.py +188 -0
  94. basic_memory/mcp/prompts/search.py +57 -0
  95. basic_memory/mcp/prompts/utils.py +162 -0
  96. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  97. basic_memory/mcp/resources/project_info.py +71 -0
  98. basic_memory/mcp/server.py +81 -0
  99. basic_memory/mcp/tools/__init__.py +48 -0
  100. basic_memory/mcp/tools/build_context.py +120 -0
  101. basic_memory/mcp/tools/canvas.py +152 -0
  102. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  103. basic_memory/mcp/tools/delete_note.py +242 -0
  104. basic_memory/mcp/tools/edit_note.py +324 -0
  105. basic_memory/mcp/tools/list_directory.py +168 -0
  106. basic_memory/mcp/tools/move_note.py +551 -0
  107. basic_memory/mcp/tools/project_management.py +201 -0
  108. basic_memory/mcp/tools/read_content.py +281 -0
  109. basic_memory/mcp/tools/read_note.py +267 -0
  110. basic_memory/mcp/tools/recent_activity.py +534 -0
  111. basic_memory/mcp/tools/search.py +385 -0
  112. basic_memory/mcp/tools/utils.py +540 -0
  113. basic_memory/mcp/tools/view_note.py +78 -0
  114. basic_memory/mcp/tools/write_note.py +230 -0
  115. basic_memory/models/__init__.py +15 -0
  116. basic_memory/models/base.py +10 -0
  117. basic_memory/models/knowledge.py +226 -0
  118. basic_memory/models/project.py +87 -0
  119. basic_memory/models/search.py +85 -0
  120. basic_memory/repository/__init__.py +11 -0
  121. basic_memory/repository/entity_repository.py +503 -0
  122. basic_memory/repository/observation_repository.py +73 -0
  123. basic_memory/repository/postgres_search_repository.py +379 -0
  124. basic_memory/repository/project_info_repository.py +10 -0
  125. basic_memory/repository/project_repository.py +128 -0
  126. basic_memory/repository/relation_repository.py +146 -0
  127. basic_memory/repository/repository.py +385 -0
  128. basic_memory/repository/search_index_row.py +95 -0
  129. basic_memory/repository/search_repository.py +94 -0
  130. basic_memory/repository/search_repository_base.py +241 -0
  131. basic_memory/repository/sqlite_search_repository.py +439 -0
  132. basic_memory/schemas/__init__.py +86 -0
  133. basic_memory/schemas/base.py +297 -0
  134. basic_memory/schemas/cloud.py +50 -0
  135. basic_memory/schemas/delete.py +37 -0
  136. basic_memory/schemas/directory.py +30 -0
  137. basic_memory/schemas/importer.py +35 -0
  138. basic_memory/schemas/memory.py +285 -0
  139. basic_memory/schemas/project_info.py +212 -0
  140. basic_memory/schemas/prompt.py +90 -0
  141. basic_memory/schemas/request.py +112 -0
  142. basic_memory/schemas/response.py +229 -0
  143. basic_memory/schemas/search.py +117 -0
  144. basic_memory/schemas/sync_report.py +72 -0
  145. basic_memory/schemas/v2/__init__.py +27 -0
  146. basic_memory/schemas/v2/entity.py +129 -0
  147. basic_memory/schemas/v2/resource.py +46 -0
  148. basic_memory/services/__init__.py +8 -0
  149. basic_memory/services/context_service.py +601 -0
  150. basic_memory/services/directory_service.py +308 -0
  151. basic_memory/services/entity_service.py +864 -0
  152. basic_memory/services/exceptions.py +37 -0
  153. basic_memory/services/file_service.py +541 -0
  154. basic_memory/services/initialization.py +216 -0
  155. basic_memory/services/link_resolver.py +121 -0
  156. basic_memory/services/project_service.py +880 -0
  157. basic_memory/services/search_service.py +404 -0
  158. basic_memory/services/service.py +15 -0
  159. basic_memory/sync/__init__.py +6 -0
  160. basic_memory/sync/background_sync.py +26 -0
  161. basic_memory/sync/sync_service.py +1259 -0
  162. basic_memory/sync/watch_service.py +510 -0
  163. basic_memory/telemetry.py +249 -0
  164. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  165. basic_memory/templates/prompts/search.hbs +101 -0
  166. basic_memory/utils.py +468 -0
  167. basic_memory-0.17.1.dist-info/METADATA +617 -0
  168. basic_memory-0.17.1.dist-info/RECORD +171 -0
  169. basic_memory-0.17.1.dist-info/WHEEL +4 -0
  170. basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
  171. 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