basic-memory 0.2.12__py3-none-any.whl → 0.16.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.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (149) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +27 -3
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  6. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  7. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  8. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  9. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  10. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  11. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
  12. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  13. basic_memory/api/app.py +63 -31
  14. basic_memory/api/routers/__init__.py +4 -1
  15. basic_memory/api/routers/directory_router.py +84 -0
  16. basic_memory/api/routers/importer_router.py +152 -0
  17. basic_memory/api/routers/knowledge_router.py +165 -28
  18. basic_memory/api/routers/management_router.py +80 -0
  19. basic_memory/api/routers/memory_router.py +28 -67
  20. basic_memory/api/routers/project_router.py +406 -0
  21. basic_memory/api/routers/prompt_router.py +260 -0
  22. basic_memory/api/routers/resource_router.py +219 -14
  23. basic_memory/api/routers/search_router.py +21 -13
  24. basic_memory/api/routers/utils.py +130 -0
  25. basic_memory/api/template_loader.py +292 -0
  26. basic_memory/cli/app.py +52 -1
  27. basic_memory/cli/auth.py +277 -0
  28. basic_memory/cli/commands/__init__.py +13 -2
  29. basic_memory/cli/commands/cloud/__init__.py +6 -0
  30. basic_memory/cli/commands/cloud/api_client.py +112 -0
  31. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  32. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  33. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  34. basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
  35. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  36. basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
  37. basic_memory/cli/commands/cloud/upload.py +233 -0
  38. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  39. basic_memory/cli/commands/command_utils.py +51 -0
  40. basic_memory/cli/commands/db.py +26 -7
  41. basic_memory/cli/commands/import_chatgpt.py +83 -0
  42. basic_memory/cli/commands/import_claude_conversations.py +86 -0
  43. basic_memory/cli/commands/import_claude_projects.py +85 -0
  44. basic_memory/cli/commands/import_memory_json.py +35 -92
  45. basic_memory/cli/commands/mcp.py +84 -10
  46. basic_memory/cli/commands/project.py +876 -0
  47. basic_memory/cli/commands/status.py +47 -30
  48. basic_memory/cli/commands/tool.py +341 -0
  49. basic_memory/cli/main.py +13 -6
  50. basic_memory/config.py +481 -22
  51. basic_memory/db.py +192 -32
  52. basic_memory/deps.py +252 -22
  53. basic_memory/file_utils.py +113 -58
  54. basic_memory/ignore_utils.py +297 -0
  55. basic_memory/importers/__init__.py +27 -0
  56. basic_memory/importers/base.py +79 -0
  57. basic_memory/importers/chatgpt_importer.py +232 -0
  58. basic_memory/importers/claude_conversations_importer.py +177 -0
  59. basic_memory/importers/claude_projects_importer.py +148 -0
  60. basic_memory/importers/memory_json_importer.py +108 -0
  61. basic_memory/importers/utils.py +58 -0
  62. basic_memory/markdown/entity_parser.py +143 -23
  63. basic_memory/markdown/markdown_processor.py +3 -3
  64. basic_memory/markdown/plugins.py +39 -21
  65. basic_memory/markdown/schemas.py +1 -1
  66. basic_memory/markdown/utils.py +28 -13
  67. basic_memory/mcp/async_client.py +134 -4
  68. basic_memory/mcp/project_context.py +141 -0
  69. basic_memory/mcp/prompts/__init__.py +19 -0
  70. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  71. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  72. basic_memory/mcp/prompts/recent_activity.py +188 -0
  73. basic_memory/mcp/prompts/search.py +57 -0
  74. basic_memory/mcp/prompts/utils.py +162 -0
  75. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  76. basic_memory/mcp/resources/project_info.py +71 -0
  77. basic_memory/mcp/server.py +7 -13
  78. basic_memory/mcp/tools/__init__.py +33 -21
  79. basic_memory/mcp/tools/build_context.py +120 -0
  80. basic_memory/mcp/tools/canvas.py +130 -0
  81. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  82. basic_memory/mcp/tools/delete_note.py +225 -0
  83. basic_memory/mcp/tools/edit_note.py +320 -0
  84. basic_memory/mcp/tools/list_directory.py +167 -0
  85. basic_memory/mcp/tools/move_note.py +545 -0
  86. basic_memory/mcp/tools/project_management.py +200 -0
  87. basic_memory/mcp/tools/read_content.py +271 -0
  88. basic_memory/mcp/tools/read_note.py +255 -0
  89. basic_memory/mcp/tools/recent_activity.py +534 -0
  90. basic_memory/mcp/tools/search.py +369 -14
  91. basic_memory/mcp/tools/utils.py +374 -16
  92. basic_memory/mcp/tools/view_note.py +77 -0
  93. basic_memory/mcp/tools/write_note.py +207 -0
  94. basic_memory/models/__init__.py +3 -2
  95. basic_memory/models/knowledge.py +67 -15
  96. basic_memory/models/project.py +87 -0
  97. basic_memory/models/search.py +10 -6
  98. basic_memory/repository/__init__.py +2 -0
  99. basic_memory/repository/entity_repository.py +229 -7
  100. basic_memory/repository/observation_repository.py +35 -3
  101. basic_memory/repository/project_info_repository.py +10 -0
  102. basic_memory/repository/project_repository.py +103 -0
  103. basic_memory/repository/relation_repository.py +21 -2
  104. basic_memory/repository/repository.py +147 -29
  105. basic_memory/repository/search_repository.py +437 -59
  106. basic_memory/schemas/__init__.py +22 -9
  107. basic_memory/schemas/base.py +97 -8
  108. basic_memory/schemas/cloud.py +50 -0
  109. basic_memory/schemas/directory.py +30 -0
  110. basic_memory/schemas/importer.py +35 -0
  111. basic_memory/schemas/memory.py +188 -23
  112. basic_memory/schemas/project_info.py +211 -0
  113. basic_memory/schemas/prompt.py +90 -0
  114. basic_memory/schemas/request.py +57 -3
  115. basic_memory/schemas/response.py +9 -1
  116. basic_memory/schemas/search.py +33 -35
  117. basic_memory/schemas/sync_report.py +72 -0
  118. basic_memory/services/__init__.py +2 -1
  119. basic_memory/services/context_service.py +251 -106
  120. basic_memory/services/directory_service.py +295 -0
  121. basic_memory/services/entity_service.py +595 -60
  122. basic_memory/services/exceptions.py +21 -0
  123. basic_memory/services/file_service.py +284 -30
  124. basic_memory/services/initialization.py +191 -0
  125. basic_memory/services/link_resolver.py +50 -56
  126. basic_memory/services/project_service.py +863 -0
  127. basic_memory/services/search_service.py +172 -34
  128. basic_memory/sync/__init__.py +3 -2
  129. basic_memory/sync/background_sync.py +26 -0
  130. basic_memory/sync/sync_service.py +1176 -96
  131. basic_memory/sync/watch_service.py +412 -135
  132. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  133. basic_memory/templates/prompts/search.hbs +101 -0
  134. basic_memory/utils.py +388 -28
  135. basic_memory-0.16.1.dist-info/METADATA +493 -0
  136. basic_memory-0.16.1.dist-info/RECORD +148 -0
  137. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
  138. basic_memory/alembic/README +0 -1
  139. basic_memory/cli/commands/sync.py +0 -203
  140. basic_memory/mcp/tools/knowledge.py +0 -56
  141. basic_memory/mcp/tools/memory.py +0 -151
  142. basic_memory/mcp/tools/notes.py +0 -122
  143. basic_memory/schemas/discovery.py +0 -28
  144. basic_memory/sync/file_change_scanner.py +0 -158
  145. basic_memory/sync/utils.py +0 -34
  146. basic_memory-0.2.12.dist-info/METADATA +0 -291
  147. basic_memory-0.2.12.dist-info/RECORD +0 -78
  148. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
  149. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,211 @@
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
+ name: str
177
+ path: str
178
+ is_default: bool = False
179
+
180
+ @property
181
+ def permalink(self) -> str: # pragma: no cover
182
+ return generate_permalink(self.name)
183
+
184
+ @property
185
+ def home(self) -> Path: # pragma: no cover
186
+ return Path(self.path).expanduser()
187
+
188
+ @property
189
+ def project_url(self) -> str: # pragma: no cover
190
+ return f"/{generate_permalink(self.name)}"
191
+
192
+
193
+ class ProjectList(BaseModel):
194
+ """Response model for listing projects."""
195
+
196
+ projects: List[ProjectItem]
197
+ default_project: str
198
+
199
+
200
+ class ProjectStatusResponse(BaseModel):
201
+ """Response model for switching projects."""
202
+
203
+ message: str = Field(..., description="Status message about the project switch")
204
+ status: str = Field(..., description="Status of the switch (success or error)")
205
+ default: bool = Field(..., description="True if the project was set as the default")
206
+ old_project: Optional[ProjectItem] = Field(
207
+ None, description="Information about the project being switched from"
208
+ )
209
+ new_project: Optional[ProjectItem] = Field(
210
+ None, description="Information about the project being switched to"
211
+ )
@@ -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
+ )
@@ -1,9 +1,9 @@
1
1
  """Request schemas for interacting with the knowledge graph."""
2
2
 
3
- from typing import List, Optional, Annotated
3
+ from typing import List, Optional, Annotated, Literal
4
4
  from annotated_types import MaxLen, MinLen
5
5
 
6
- from pydantic import BaseModel
6
+ from pydantic import BaseModel, field_validator
7
7
 
8
8
  from basic_memory.schemas.base import (
9
9
  Relation,
@@ -51,8 +51,62 @@ class GetEntitiesRequest(BaseModel):
51
51
  discovered through search.
52
52
  """
53
53
 
54
- permalinks: Annotated[List[Permalink], MinLen(1)]
54
+ permalinks: Annotated[List[Permalink], MinLen(1), MaxLen(10)]
55
55
 
56
56
 
57
57
  class CreateRelationsRequest(BaseModel):
58
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()
@@ -11,6 +11,7 @@ Key Features:
11
11
  4. Bulk operations return all affected items
12
12
  """
13
13
 
14
+ from datetime import datetime
14
15
  from typing import List, Optional, Dict
15
16
 
16
17
  from pydantic import BaseModel, ConfigDict, Field, AliasPath, AliasChoices
@@ -43,6 +44,8 @@ class ObservationResponse(Observation, SQLAlchemyModel):
43
44
  }
44
45
  """
45
46
 
47
+ permalink: Permalink
48
+
46
49
 
47
50
  class RelationResponse(Relation, SQLAlchemyModel):
48
51
  """Response schema for relation operations.
@@ -59,6 +62,8 @@ class RelationResponse(Relation, SQLAlchemyModel):
59
62
  }
60
63
  """
61
64
 
65
+ permalink: Permalink
66
+
62
67
  from_id: Permalink = Field(
63
68
  # use the permalink from the associated Entity
64
69
  # or the from_id value
@@ -126,14 +131,17 @@ class EntityResponse(SQLAlchemyModel):
126
131
  }
127
132
  """
128
133
 
129
- permalink: Permalink
134
+ permalink: Optional[Permalink]
130
135
  title: str
131
136
  file_path: str
132
137
  entity_type: EntityType
133
138
  entity_metadata: Optional[Dict] = None
139
+ checksum: Optional[str] = None
134
140
  content_type: ContentType
135
141
  observations: List[ObservationResponse] = []
136
142
  relations: List[RelationResponse] = []
143
+ created_at: datetime
144
+ updated_at: datetime
137
145
 
138
146
 
139
147
  class EntityListResponse(SQLAlchemyModel):
@@ -11,6 +11,8 @@ from datetime import datetime
11
11
  from enum import Enum
12
12
  from pydantic import BaseModel, field_validator
13
13
 
14
+ from basic_memory.schemas.base import Permalink
15
+
14
16
 
15
17
  class SearchItemType(str, Enum):
16
18
  """Types of searchable items."""
@@ -26,23 +28,29 @@ class SearchQuery(BaseModel):
26
28
  Use ONE of these primary search modes:
27
29
  - permalink: Exact permalink match
28
30
  - permalink_match: Path pattern with *
29
- - text: Full-text search of title/content
31
+ - text: Full-text search of title/content (supports boolean operators: AND, OR, NOT)
30
32
 
31
33
  Optionally filter results by:
32
34
  - types: Limit to specific item types
33
35
  - entity_types: Limit to specific entity types
34
36
  - after_date: Only items after date
37
+
38
+ Boolean search examples:
39
+ - "python AND flask" - Find items with both terms
40
+ - "python OR django" - Find items with either term
41
+ - "python NOT django" - Find items with python but not django
42
+ - "(python OR flask) AND web" - Use parentheses for grouping
35
43
  """
36
44
 
37
45
  # Primary search modes (use ONE of these)
38
46
  permalink: Optional[str] = None # Exact permalink match
39
- permalink_match: Optional[str] = None # Exact permalink match
40
- text: Optional[str] = None # Full-text search
47
+ permalink_match: Optional[str] = None # Glob permalink match
48
+ text: Optional[str] = None # Full-text search (now supports boolean operators)
41
49
  title: Optional[str] = None # title only search
42
50
 
43
51
  # Optional filters
44
- types: Optional[List[SearchItemType]] = None # Filter by item type
45
- entity_types: Optional[List[str]] = None # Filter by entity type
52
+ types: Optional[List[str]] = None # Filter by type
53
+ entity_types: Optional[List[SearchItemType]] = None # Filter by entity type
46
54
  after_date: Optional[Union[datetime, str]] = None # Time-based filter
47
55
 
48
56
  @field_validator("after_date")
@@ -57,58 +65,48 @@ class SearchQuery(BaseModel):
57
65
  return (
58
66
  self.permalink is None
59
67
  and self.permalink_match is None
68
+ and self.title is None
60
69
  and self.text is None
61
70
  and self.after_date is None
62
71
  and self.types is None
63
72
  and self.entity_types is None
64
73
  )
65
74
 
75
+ def has_boolean_operators(self) -> bool:
76
+ """Check if the text query contains boolean operators (AND, OR, NOT)."""
77
+ if not self.text: # pragma: no cover
78
+ return False
79
+
80
+ # Check for common boolean operators with correct word boundaries
81
+ # to avoid matching substrings like "GRAND" containing "AND"
82
+ boolean_patterns = [" AND ", " OR ", " NOT ", "(", ")"]
83
+ text = f" {self.text} " # Add spaces to ensure we match word boundaries
84
+ return any(pattern in text for pattern in boolean_patterns)
85
+
66
86
 
67
87
  class SearchResult(BaseModel):
68
88
  """Search result with score and metadata."""
69
89
 
70
- id: int
90
+ title: str
71
91
  type: SearchItemType
72
92
  score: float
73
- permalink: str
93
+ entity: Optional[Permalink] = None
94
+ permalink: Optional[str]
95
+ content: Optional[str] = None
74
96
  file_path: str
75
97
 
76
98
  metadata: Optional[dict] = None
77
99
 
78
100
  # Type-specific fields
79
- entity_id: Optional[int] = None # For observations
80
101
  category: Optional[str] = None # For observations
81
- from_id: Optional[int] = None # For relations
82
- to_id: Optional[int] = None # For relations
102
+ from_entity: Optional[Permalink] = None # For relations
103
+ to_entity: Optional[Permalink] = None # For relations
83
104
  relation_type: Optional[str] = None # For relations
84
105
 
85
106
 
86
- class RelatedResult(BaseModel):
87
- type: SearchItemType
88
- id: int
89
- title: str
90
- permalink: str
91
- depth: int
92
- root_id: int
93
- created_at: datetime
94
- from_id: Optional[int] = None
95
- to_id: Optional[int] = None
96
- relation_type: Optional[str] = None
97
- category: Optional[str] = None
98
- entity_id: Optional[int] = None
99
-
100
-
101
107
  class SearchResponse(BaseModel):
102
108
  """Wrapper for search results."""
103
109
 
104
110
  results: List[SearchResult]
105
-
106
-
107
- # Schema for future advanced search endpoint
108
- class AdvancedSearchQuery(BaseModel):
109
- """Advanced full-text search with explicit FTS5 syntax."""
110
-
111
- query: str # Raw FTS5 query (e.g., "foo AND bar")
112
- types: Optional[List[SearchItemType]] = None
113
- entity_types: Optional[List[str]] = None
114
- after_date: Optional[Union[datetime, str]] = None
111
+ current_page: int
112
+ page_size: int
@@ -0,0 +1,72 @@
1
+ """Pydantic schemas for sync report responses."""
2
+
3
+ from datetime import datetime
4
+ from typing import TYPE_CHECKING, Dict, List, Set
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ # avoid cirular imports
9
+ if TYPE_CHECKING:
10
+ from basic_memory.sync.sync_service import SyncReport
11
+
12
+
13
+ class SkippedFileResponse(BaseModel):
14
+ """Information about a file that was skipped due to repeated failures."""
15
+
16
+ path: str = Field(description="File path relative to project root")
17
+ reason: str = Field(description="Error message from last failure")
18
+ failure_count: int = Field(description="Number of consecutive failures")
19
+ first_failed: datetime = Field(description="Timestamp of first failure")
20
+
21
+ model_config = {"from_attributes": True}
22
+
23
+
24
+ class SyncReportResponse(BaseModel):
25
+ """Report of file changes found compared to database state.
26
+
27
+ Used for API responses when scanning or syncing files.
28
+ """
29
+
30
+ new: Set[str] = Field(default_factory=set, description="Files on disk but not in database")
31
+ modified: Set[str] = Field(default_factory=set, description="Files with different checksums")
32
+ deleted: Set[str] = Field(default_factory=set, description="Files in database but not on disk")
33
+ moves: Dict[str, str] = Field(
34
+ default_factory=dict, description="Files moved (old_path -> new_path)"
35
+ )
36
+ checksums: Dict[str, str] = Field(
37
+ default_factory=dict, description="Current file checksums (path -> checksum)"
38
+ )
39
+ skipped_files: List[SkippedFileResponse] = Field(
40
+ default_factory=list, description="Files skipped due to repeated failures"
41
+ )
42
+ total: int = Field(description="Total number of changes")
43
+
44
+ @classmethod
45
+ def from_sync_report(cls, report: "SyncReport") -> "SyncReportResponse":
46
+ """Convert SyncReport dataclass to Pydantic model.
47
+
48
+ Args:
49
+ report: SyncReport dataclass from sync service
50
+
51
+ Returns:
52
+ SyncReportResponse with same data
53
+ """
54
+ return cls(
55
+ new=report.new,
56
+ modified=report.modified,
57
+ deleted=report.deleted,
58
+ moves=report.moves,
59
+ checksums=report.checksums,
60
+ skipped_files=[
61
+ SkippedFileResponse(
62
+ path=skipped.path,
63
+ reason=skipped.reason,
64
+ failure_count=skipped.failure_count,
65
+ first_failed=skipped.first_failed,
66
+ )
67
+ for skipped in report.skipped_files
68
+ ],
69
+ total=report.total,
70
+ )
71
+
72
+ model_config = {"from_attributes": True}
@@ -3,5 +3,6 @@
3
3
  from .service import BaseService
4
4
  from .file_service import FileService
5
5
  from .entity_service import EntityService
6
+ from .project_service import ProjectService
6
7
 
7
- __all__ = ["BaseService", "FileService", "EntityService"]
8
+ __all__ = ["BaseService", "FileService", "EntityService", "ProjectService"]