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,285 @@
1
+ """Schemas for memory context."""
2
+
3
+ from datetime import datetime
4
+ from typing import List, Optional, Annotated, Sequence, Literal, Union, Dict
5
+
6
+ from annotated_types import MinLen, MaxLen
7
+ from pydantic import BaseModel, Field, BeforeValidator, TypeAdapter, field_serializer
8
+
9
+ from basic_memory.schemas.search import SearchItemType
10
+
11
+
12
+ def validate_memory_url_path(path: str) -> bool:
13
+ """Validate that a memory URL path is well-formed.
14
+
15
+ Args:
16
+ path: The path part of a memory URL (without memory:// prefix)
17
+
18
+ Returns:
19
+ True if the path is valid, False otherwise
20
+
21
+ Examples:
22
+ >>> validate_memory_url_path("specs/search")
23
+ True
24
+ >>> validate_memory_url_path("memory//test") # Double slash
25
+ False
26
+ >>> validate_memory_url_path("invalid://test") # Contains protocol
27
+ False
28
+ """
29
+ # Empty paths are not valid
30
+ if not path or not path.strip():
31
+ return False
32
+
33
+ # Check for invalid protocol schemes within the path first (more specific)
34
+ if "://" in path:
35
+ return False
36
+
37
+ # Check for double slashes (except at the beginning for absolute paths)
38
+ if "//" in path:
39
+ return False
40
+
41
+ # Check for invalid characters (excluding * which is used for pattern matching)
42
+ invalid_chars = {"<", ">", '"', "|", "?"}
43
+ if any(char in path for char in invalid_chars):
44
+ return False
45
+
46
+ return True
47
+
48
+
49
+ def normalize_memory_url(url: str | None) -> str:
50
+ """Normalize a MemoryUrl string with validation.
51
+
52
+ Args:
53
+ url: A path like "specs/search" or "memory://specs/search"
54
+
55
+ Returns:
56
+ Normalized URL starting with memory://
57
+
58
+ Raises:
59
+ ValueError: If the URL path is malformed
60
+
61
+ Examples:
62
+ >>> normalize_memory_url("specs/search")
63
+ 'memory://specs/search'
64
+ >>> normalize_memory_url("memory://specs/search")
65
+ 'memory://specs/search'
66
+ >>> normalize_memory_url("memory//test")
67
+ Traceback (most recent call last):
68
+ ...
69
+ ValueError: Invalid memory URL path: 'memory//test' contains double slashes
70
+ """
71
+ if not url:
72
+ raise ValueError("Memory URL cannot be empty")
73
+
74
+ # Strip whitespace for consistency
75
+ url = url.strip()
76
+
77
+ if not url:
78
+ raise ValueError("Memory URL cannot be empty or whitespace")
79
+
80
+ clean_path = url.removeprefix("memory://")
81
+
82
+ # Validate the extracted path
83
+ if not validate_memory_url_path(clean_path):
84
+ # Provide specific error messages for common issues
85
+ if "://" in clean_path:
86
+ raise ValueError(f"Invalid memory URL path: '{clean_path}' contains protocol scheme")
87
+ elif "//" in clean_path:
88
+ raise ValueError(f"Invalid memory URL path: '{clean_path}' contains double slashes")
89
+ else:
90
+ raise ValueError(f"Invalid memory URL path: '{clean_path}' contains invalid characters")
91
+
92
+ return f"memory://{clean_path}"
93
+
94
+
95
+ MemoryUrl = Annotated[
96
+ str,
97
+ BeforeValidator(str.strip), # Clean whitespace
98
+ BeforeValidator(normalize_memory_url), # Validate and normalize the URL
99
+ MinLen(1),
100
+ MaxLen(2028),
101
+ ]
102
+
103
+ memory_url = TypeAdapter(MemoryUrl)
104
+
105
+
106
+ def memory_url_path(url: memory_url) -> str: # pyright: ignore
107
+ """
108
+ Returns the uri for a url value by removing the prefix "memory://" from a given MemoryUrl.
109
+
110
+ This function processes a given MemoryUrl by removing the "memory://"
111
+ prefix and returns the resulting string. If the provided url does not
112
+ begin with "memory://", the function will simply return the input url
113
+ unchanged.
114
+
115
+ :param url: A MemoryUrl object representing the URL with a "memory://" prefix.
116
+ :type url: MemoryUrl
117
+ :return: A string representing the URL with the "memory://" prefix removed.
118
+ :rtype: str
119
+ """
120
+ return url.removeprefix("memory://")
121
+
122
+
123
+ class EntitySummary(BaseModel):
124
+ """Simplified entity representation."""
125
+
126
+ type: Literal["entity"] = "entity"
127
+ entity_id: int # Database ID for v2 API consistency
128
+ permalink: Optional[str]
129
+ title: str
130
+ content: Optional[str] = None
131
+ file_path: str
132
+ created_at: Annotated[
133
+ datetime, Field(json_schema_extra={"type": "string", "format": "date-time"})
134
+ ]
135
+
136
+ @field_serializer("created_at")
137
+ def serialize_created_at(self, dt: datetime) -> str:
138
+ return dt.isoformat()
139
+
140
+
141
+ class RelationSummary(BaseModel):
142
+ """Simplified relation representation."""
143
+
144
+ type: Literal["relation"] = "relation"
145
+ relation_id: int # Database ID for v2 API consistency
146
+ entity_id: Optional[int] = None # ID of the entity this relation belongs to
147
+ title: str
148
+ file_path: str
149
+ permalink: str
150
+ relation_type: str
151
+ from_entity: Optional[str] = None
152
+ from_entity_id: Optional[int] = None # ID of source entity
153
+ to_entity: Optional[str] = None
154
+ to_entity_id: Optional[int] = None # ID of target entity
155
+ created_at: Annotated[
156
+ datetime, Field(json_schema_extra={"type": "string", "format": "date-time"})
157
+ ]
158
+
159
+ @field_serializer("created_at")
160
+ def serialize_created_at(self, dt: datetime) -> str:
161
+ return dt.isoformat()
162
+
163
+
164
+ class ObservationSummary(BaseModel):
165
+ """Simplified observation representation."""
166
+
167
+ type: Literal["observation"] = "observation"
168
+ observation_id: int # Database ID for v2 API consistency
169
+ entity_id: Optional[int] = None # ID of the entity this observation belongs to
170
+ title: str
171
+ file_path: str
172
+ permalink: str
173
+ category: str
174
+ content: str
175
+ created_at: Annotated[
176
+ datetime, Field(json_schema_extra={"type": "string", "format": "date-time"})
177
+ ]
178
+
179
+ @field_serializer("created_at")
180
+ def serialize_created_at(self, dt: datetime) -> str:
181
+ return dt.isoformat()
182
+
183
+
184
+ class MemoryMetadata(BaseModel):
185
+ """Simplified response metadata."""
186
+
187
+ uri: Optional[str] = None
188
+ types: Optional[List[SearchItemType]] = None
189
+ depth: int
190
+ timeframe: Optional[str] = None
191
+ generated_at: Annotated[
192
+ datetime, Field(json_schema_extra={"type": "string", "format": "date-time"})
193
+ ]
194
+ primary_count: Optional[int] = None # Changed field name
195
+ related_count: Optional[int] = None # Changed field name
196
+ total_results: Optional[int] = None # For backward compatibility
197
+ total_relations: Optional[int] = None
198
+ total_observations: Optional[int] = None
199
+
200
+ @field_serializer("generated_at")
201
+ def serialize_generated_at(self, dt: datetime) -> str:
202
+ return dt.isoformat()
203
+
204
+
205
+ class ContextResult(BaseModel):
206
+ """Context result containing a primary item with its observations and related items."""
207
+
208
+ primary_result: Annotated[
209
+ Union[EntitySummary, RelationSummary, ObservationSummary],
210
+ Field(discriminator="type", description="Primary item"),
211
+ ]
212
+
213
+ observations: Sequence[ObservationSummary] = Field(
214
+ description="Observations belonging to this entity", default_factory=list
215
+ )
216
+
217
+ related_results: Sequence[
218
+ Annotated[
219
+ Union[EntitySummary, RelationSummary, ObservationSummary], Field(discriminator="type")
220
+ ]
221
+ ] = Field(description="Related items", default_factory=list)
222
+
223
+
224
+ class GraphContext(BaseModel):
225
+ """Complete context response."""
226
+
227
+ # hierarchical results
228
+ results: Sequence[ContextResult] = Field(
229
+ description="Hierarchical results with related items nested", default_factory=list
230
+ )
231
+
232
+ # Context metadata
233
+ metadata: MemoryMetadata
234
+
235
+ page: Optional[int] = None
236
+ page_size: Optional[int] = None
237
+
238
+
239
+ class ActivityStats(BaseModel):
240
+ """Statistics about activity across all projects."""
241
+
242
+ total_projects: int
243
+ active_projects: int = Field(description="Projects with activity in timeframe")
244
+ most_active_project: Optional[str] = None
245
+ total_items: int = Field(description="Total items across all projects")
246
+ total_entities: int = 0
247
+ total_relations: int = 0
248
+ total_observations: int = 0
249
+
250
+
251
+ class ProjectActivity(BaseModel):
252
+ """Activity summary for a single project."""
253
+
254
+ project_name: str
255
+ project_path: str
256
+ activity: GraphContext = Field(description="The actual activity data for this project")
257
+ item_count: int = Field(description="Total items in this project's activity")
258
+ last_activity: Optional[
259
+ Annotated[datetime, Field(json_schema_extra={"type": "string", "format": "date-time"})]
260
+ ] = Field(default=None, description="Most recent activity timestamp")
261
+ active_folders: List[str] = Field(default_factory=list, description="Most active folders")
262
+
263
+ @field_serializer("last_activity")
264
+ def serialize_last_activity(self, dt: Optional[datetime]) -> Optional[str]:
265
+ return dt.isoformat() if dt else None
266
+
267
+
268
+ class ProjectActivitySummary(BaseModel):
269
+ """Summary of activity across all projects."""
270
+
271
+ projects: Dict[str, ProjectActivity] = Field(
272
+ description="Activity per project, keyed by project name"
273
+ )
274
+ summary: ActivityStats
275
+ timeframe: str = Field(description="The timeframe used for the query")
276
+ generated_at: Annotated[
277
+ datetime, Field(json_schema_extra={"type": "string", "format": "date-time"})
278
+ ]
279
+ guidance: Optional[str] = Field(
280
+ default=None, description="Assistant guidance for project selection and session management"
281
+ )
282
+
283
+ @field_serializer("generated_at")
284
+ def serialize_generated_at(self, dt: datetime) -> str:
285
+ return dt.isoformat()
@@ -0,0 +1,212 @@
1
+ """Schema for project info response."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional, Any
7
+
8
+ from pydantic import Field, BaseModel
9
+
10
+ from basic_memory.utils import generate_permalink
11
+
12
+
13
+ class ProjectStatistics(BaseModel):
14
+ """Statistics about the current project."""
15
+
16
+ # Basic counts
17
+ total_entities: int = Field(description="Total number of entities in the knowledge base")
18
+ total_observations: int = Field(description="Total number of observations across all entities")
19
+ total_relations: int = Field(description="Total number of relations between entities")
20
+ total_unresolved_relations: int = Field(
21
+ description="Number of relations with unresolved targets"
22
+ )
23
+
24
+ # Entity counts by type
25
+ entity_types: Dict[str, int] = Field(
26
+ description="Count of entities by type (e.g., note, conversation)"
27
+ )
28
+
29
+ # Observation counts by category
30
+ observation_categories: Dict[str, int] = Field(
31
+ description="Count of observations by category (e.g., tech, decision)"
32
+ )
33
+
34
+ # Relation counts by type
35
+ relation_types: Dict[str, int] = Field(
36
+ description="Count of relations by type (e.g., implements, relates_to)"
37
+ )
38
+
39
+ # Graph metrics
40
+ most_connected_entities: List[Dict[str, Any]] = Field(
41
+ description="Entities with the most relations, including their titles and permalinks"
42
+ )
43
+ isolated_entities: int = Field(description="Number of entities with no relations")
44
+
45
+
46
+ class ActivityMetrics(BaseModel):
47
+ """Activity metrics for the current project."""
48
+
49
+ # Recent activity
50
+ recently_created: List[Dict[str, Any]] = Field(
51
+ description="Recently created entities with timestamps"
52
+ )
53
+ recently_updated: List[Dict[str, Any]] = Field(
54
+ description="Recently updated entities with timestamps"
55
+ )
56
+
57
+ # Growth over time (last 6 months)
58
+ monthly_growth: Dict[str, Dict[str, int]] = Field(
59
+ description="Monthly growth statistics for entities, observations, and relations"
60
+ )
61
+
62
+
63
+ class SystemStatus(BaseModel):
64
+ """System status information."""
65
+
66
+ # Version information
67
+ version: str = Field(description="Basic Memory version")
68
+
69
+ # Database status
70
+ database_path: str = Field(description="Path to the SQLite database")
71
+ database_size: str = Field(description="Size of the database in human-readable format")
72
+
73
+ # Watch service status
74
+ watch_status: Optional[Dict[str, Any]] = Field(
75
+ default=None, description="Watch service status information (if running)"
76
+ )
77
+
78
+ # System information
79
+ timestamp: datetime = Field(description="Timestamp when the information was collected")
80
+
81
+
82
+ class ProjectInfoResponse(BaseModel):
83
+ """Response for the project_info tool."""
84
+
85
+ # Project configuration
86
+ project_name: str = Field(description="Name of the current project")
87
+ project_path: str = Field(description="Path to the current project files")
88
+ available_projects: Dict[str, Dict[str, Any]] = Field(
89
+ description="Map of configured project names to detailed project information"
90
+ )
91
+ default_project: str = Field(description="Name of the default project")
92
+
93
+ # Statistics
94
+ statistics: ProjectStatistics = Field(description="Statistics about the knowledge base")
95
+
96
+ # Activity metrics
97
+ activity: ActivityMetrics = Field(description="Activity and growth metrics")
98
+
99
+ # System status
100
+ system: SystemStatus = Field(description="System and service status information")
101
+
102
+
103
+ class ProjectInfoRequest(BaseModel):
104
+ """Request model for switching projects."""
105
+
106
+ name: str = Field(..., description="Name of the project to switch to")
107
+ path: str = Field(..., description="Path to the project directory")
108
+ set_default: bool = Field(..., description="Set the project as the default")
109
+
110
+
111
+ class WatchEvent(BaseModel):
112
+ timestamp: datetime
113
+ path: str
114
+ action: str # new, delete, etc
115
+ status: str # success, error
116
+ checksum: Optional[str]
117
+ error: Optional[str] = None
118
+
119
+
120
+ class WatchServiceState(BaseModel):
121
+ # Service status
122
+ running: bool = False
123
+ start_time: datetime = datetime.now() # Use directly with Pydantic model
124
+ pid: int = os.getpid() # Use directly with Pydantic model
125
+
126
+ # Stats
127
+ error_count: int = 0
128
+ last_error: Optional[datetime] = None
129
+ last_scan: Optional[datetime] = None
130
+
131
+ # File counts
132
+ synced_files: int = 0
133
+
134
+ # Recent activity
135
+ recent_events: List[WatchEvent] = [] # Use directly with Pydantic model
136
+
137
+ def add_event(
138
+ self,
139
+ path: str,
140
+ action: str,
141
+ status: str,
142
+ checksum: Optional[str] = None,
143
+ error: Optional[str] = None,
144
+ ) -> WatchEvent: # pragma: no cover
145
+ event = WatchEvent(
146
+ timestamp=datetime.now(),
147
+ path=path,
148
+ action=action,
149
+ status=status,
150
+ checksum=checksum,
151
+ error=error,
152
+ )
153
+ self.recent_events.insert(0, event)
154
+ self.recent_events = self.recent_events[:100] # Keep last 100
155
+ return event
156
+
157
+ def record_error(self, error: str): # pragma: no cover
158
+ self.error_count += 1
159
+ self.add_event(path="", action="sync", status="error", error=error)
160
+ self.last_error = datetime.now()
161
+
162
+
163
+ class ProjectWatchStatus(BaseModel):
164
+ """Project with its watch status."""
165
+
166
+ name: str = Field(..., description="Name of the project")
167
+ path: str = Field(..., description="Path to the project")
168
+ watch_status: Optional[WatchServiceState] = Field(
169
+ None, description="Watch status information for the project"
170
+ )
171
+
172
+
173
+ class ProjectItem(BaseModel):
174
+ """Simple representation of a project."""
175
+
176
+ id: int
177
+ name: str
178
+ path: str
179
+ is_default: bool = False
180
+
181
+ @property
182
+ def permalink(self) -> str: # pragma: no cover
183
+ return generate_permalink(self.name)
184
+
185
+ @property
186
+ def home(self) -> Path: # pragma: no cover
187
+ return Path(self.path).expanduser()
188
+
189
+ @property
190
+ def project_url(self) -> str: # pragma: no cover
191
+ return f"/{generate_permalink(self.name)}"
192
+
193
+
194
+ class ProjectList(BaseModel):
195
+ """Response model for listing projects."""
196
+
197
+ projects: List[ProjectItem]
198
+ default_project: str
199
+
200
+
201
+ class ProjectStatusResponse(BaseModel):
202
+ """Response model for switching projects."""
203
+
204
+ message: str = Field(..., description="Status message about the project switch")
205
+ status: str = Field(..., description="Status of the switch (success or error)")
206
+ default: bool = Field(..., description="True if the project was set as the default")
207
+ old_project: Optional[ProjectItem] = Field(
208
+ None, description="Information about the project being switched from"
209
+ )
210
+ new_project: Optional[ProjectItem] = Field(
211
+ None, description="Information about the project being switched to"
212
+ )
@@ -0,0 +1,90 @@
1
+ """Request and response schemas for prompt-related operations."""
2
+
3
+ from typing import Optional, List, Any, Dict
4
+ from pydantic import BaseModel, Field
5
+
6
+ from basic_memory.schemas.base import TimeFrame
7
+ from basic_memory.schemas.memory import EntitySummary, ObservationSummary, RelationSummary
8
+
9
+
10
+ class PromptContextItem(BaseModel):
11
+ """Container for primary and related results to render in a prompt."""
12
+
13
+ primary_results: List[EntitySummary]
14
+ related_results: List[EntitySummary | ObservationSummary | RelationSummary]
15
+
16
+
17
+ class ContinueConversationRequest(BaseModel):
18
+ """Request for generating a continue conversation prompt.
19
+
20
+ Used to provide context for continuing a conversation on a specific topic
21
+ or with recent activity from a given timeframe.
22
+ """
23
+
24
+ topic: Optional[str] = Field(None, description="Topic or keyword to search for")
25
+ timeframe: Optional[TimeFrame] = Field(
26
+ None, description="How far back to look for activity (e.g. '1d', '1 week')"
27
+ )
28
+ # Limit depth to max 2 for performance reasons - higher values cause significant slowdown
29
+ search_items_limit: int = Field(
30
+ 5,
31
+ description="Maximum number of search results to include in context (max 10)",
32
+ ge=1,
33
+ le=10,
34
+ )
35
+ depth: int = Field(
36
+ 1,
37
+ description="How many relationship 'hops' to follow when building context (max 5)",
38
+ ge=1,
39
+ le=5,
40
+ )
41
+ # Limit related items to prevent overloading the context
42
+ related_items_limit: int = Field(
43
+ 5, description="Maximum number of related items to include in context (max 10)", ge=1, le=10
44
+ )
45
+
46
+
47
+ class SearchPromptRequest(BaseModel):
48
+ """Request for generating a search results prompt.
49
+
50
+ Used to format search results into a prompt with context and suggestions.
51
+ """
52
+
53
+ query: str = Field(..., description="The search query text")
54
+ timeframe: Optional[TimeFrame] = Field(
55
+ None, description="Optional timeframe to limit results (e.g. '1d', '1 week')"
56
+ )
57
+
58
+
59
+ class PromptMetadata(BaseModel):
60
+ """Metadata about a prompt response.
61
+
62
+ Contains statistical information about the prompt generation process
63
+ and results, useful for debugging and UI display.
64
+ """
65
+
66
+ query: Optional[str] = Field(None, description="The original query or topic")
67
+ timeframe: Optional[str] = Field(None, description="The timeframe used for filtering")
68
+ search_count: int = Field(0, description="Number of search results found")
69
+ context_count: int = Field(0, description="Number of context items retrieved")
70
+ observation_count: int = Field(0, description="Total number of observations included")
71
+ relation_count: int = Field(0, description="Total number of relations included")
72
+ total_items: int = Field(0, description="Total number of all items included in the prompt")
73
+ search_limit: int = Field(0, description="Maximum search results requested")
74
+ context_depth: int = Field(0, description="Context depth used")
75
+ related_limit: int = Field(0, description="Maximum related items requested")
76
+ generated_at: str = Field(..., description="ISO timestamp when this prompt was generated")
77
+
78
+
79
+ class PromptResponse(BaseModel):
80
+ """Response containing the rendered prompt.
81
+
82
+ Includes both the rendered prompt text and the context that was used
83
+ to render it, for potential client-side use.
84
+ """
85
+
86
+ prompt: str = Field(..., description="The rendered prompt text")
87
+ context: Dict[str, Any] = Field(..., description="The context used to render the prompt")
88
+ metadata: PromptMetadata = Field(
89
+ ..., description="Metadata about the prompt generation process"
90
+ )
@@ -0,0 +1,112 @@
1
+ """Request schemas for interacting with the knowledge graph."""
2
+
3
+ from typing import List, Optional, Annotated, Literal
4
+ from annotated_types import MaxLen, MinLen
5
+
6
+ from pydantic import BaseModel, field_validator
7
+
8
+ from basic_memory.schemas.base import (
9
+ Relation,
10
+ Permalink,
11
+ )
12
+
13
+
14
+ class SearchNodesRequest(BaseModel):
15
+ """Search for entities in the knowledge graph.
16
+
17
+ The search looks across multiple fields:
18
+ - Entity title
19
+ - Entity types
20
+ - summary
21
+ - file content
22
+ - Observations
23
+
24
+ Features:
25
+ - Case-insensitive matching
26
+ - Partial word matches
27
+ - Returns full entity objects with relations
28
+ - Includes all matching entities
29
+ - If a category is specified, only entities with that category are returned
30
+
31
+ Example Queries:
32
+ - "memory" - Find entities related to memory systems
33
+ - "SQLite" - Find database-related components
34
+ - "test" - Find test-related entities
35
+ - "implementation" - Find concrete implementations
36
+ - "service" - Find service components
37
+
38
+ Note: Currently uses SQL ILIKE for matching. Wildcard (*) searches
39
+ and full-text search capabilities are planned for future versions.
40
+ """
41
+
42
+ query: Annotated[str, MinLen(1), MaxLen(200)]
43
+ category: Optional[str] = None
44
+
45
+
46
+ class GetEntitiesRequest(BaseModel):
47
+ """Retrieve specific entities by their IDs.
48
+
49
+ Used to load complete entity details including all observations
50
+ and relations. Particularly useful for following relations
51
+ discovered through search.
52
+ """
53
+
54
+ permalinks: Annotated[List[Permalink], MinLen(1), MaxLen(10)]
55
+
56
+
57
+ class CreateRelationsRequest(BaseModel):
58
+ relations: List[Relation]
59
+
60
+
61
+ class EditEntityRequest(BaseModel):
62
+ """Request schema for editing an existing entity's content.
63
+
64
+ This allows for targeted edits without requiring the full entity content.
65
+ Supports various operation types for different editing scenarios.
66
+ """
67
+
68
+ operation: Literal["append", "prepend", "find_replace", "replace_section"]
69
+ content: str
70
+ section: Optional[str] = None
71
+ find_text: Optional[str] = None
72
+ expected_replacements: int = 1
73
+
74
+ @field_validator("section")
75
+ @classmethod
76
+ def validate_section_for_replace_section(cls, v, info):
77
+ """Ensure section is provided for replace_section operation."""
78
+ if info.data.get("operation") == "replace_section" and not v:
79
+ raise ValueError("section parameter is required for replace_section operation")
80
+ return v
81
+
82
+ @field_validator("find_text")
83
+ @classmethod
84
+ def validate_find_text_for_find_replace(cls, v, info):
85
+ """Ensure find_text is provided for find_replace operation."""
86
+ if info.data.get("operation") == "find_replace" and not v:
87
+ raise ValueError("find_text parameter is required for find_replace operation")
88
+ return v
89
+
90
+
91
+ class MoveEntityRequest(BaseModel):
92
+ """Request schema for moving an entity to a new file location.
93
+
94
+ This allows moving notes to different paths while maintaining project
95
+ consistency and optionally updating permalinks based on configuration.
96
+ """
97
+
98
+ identifier: Annotated[str, MinLen(1), MaxLen(200)]
99
+ destination_path: Annotated[str, MinLen(1), MaxLen(500)]
100
+ project: Optional[str] = None
101
+
102
+ @field_validator("destination_path")
103
+ @classmethod
104
+ def validate_destination_path(cls, v):
105
+ """Ensure destination path is relative and valid."""
106
+ if v.startswith("/"):
107
+ raise ValueError("destination_path must be relative, not absolute")
108
+ if ".." in v:
109
+ raise ValueError("destination_path cannot contain '..' path components")
110
+ if not v.strip():
111
+ raise ValueError("destination_path cannot be empty or whitespace only")
112
+ return v.strip()