basic-memory 0.7.0__py3-none-any.whl → 0.17.4__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.
- basic_memory/__init__.py +5 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +130 -20
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
- basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
- basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
- basic_memory/api/app.py +87 -20
- basic_memory/api/container.py +133 -0
- basic_memory/api/routers/__init__.py +4 -1
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +180 -23
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +9 -64
- basic_memory/api/routers/project_router.py +460 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +136 -11
- basic_memory/api/routers/search_router.py +5 -5
- basic_memory/api/routers/utils.py +169 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +181 -0
- basic_memory/api/v2/routers/knowledge_router.py +427 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +359 -0
- basic_memory/api/v2/routers/prompt_router.py +269 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/app.py +80 -10
- basic_memory/cli/auth.py +300 -0
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +127 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
- basic_memory/cli/commands/cloud/upload.py +240 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +99 -0
- basic_memory/cli/commands/db.py +87 -12
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +47 -223
- basic_memory/cli/commands/import_claude_conversations.py +48 -171
- basic_memory/cli/commands/import_claude_projects.py +53 -160
- basic_memory/cli/commands/import_memory_json.py +55 -111
- basic_memory/cli/commands/mcp.py +67 -11
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +52 -34
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/container.py +84 -0
- basic_memory/cli/main.py +14 -6
- basic_memory/config.py +580 -26
- basic_memory/db.py +285 -28
- basic_memory/deps/__init__.py +293 -0
- basic_memory/deps/config.py +26 -0
- basic_memory/deps/db.py +56 -0
- basic_memory/deps/importers.py +200 -0
- basic_memory/deps/projects.py +238 -0
- basic_memory/deps/repositories.py +179 -0
- basic_memory/deps/services.py +480 -0
- basic_memory/deps.py +16 -185
- basic_memory/file_utils.py +318 -54
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +100 -0
- basic_memory/importers/chatgpt_importer.py +245 -0
- basic_memory/importers/claude_conversations_importer.py +192 -0
- basic_memory/importers/claude_projects_importer.py +184 -0
- basic_memory/importers/memory_json_importer.py +128 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/entity_parser.py +182 -23
- basic_memory/markdown/markdown_processor.py +70 -7
- basic_memory/markdown/plugins.py +43 -23
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/markdown/utils.py +38 -14
- basic_memory/mcp/async_client.py +135 -4
- basic_memory/mcp/clients/__init__.py +28 -0
- basic_memory/mcp/clients/directory.py +70 -0
- basic_memory/mcp/clients/knowledge.py +176 -0
- basic_memory/mcp/clients/memory.py +120 -0
- basic_memory/mcp/clients/project.py +89 -0
- basic_memory/mcp/clients/resource.py +71 -0
- basic_memory/mcp/clients/search.py +65 -0
- basic_memory/mcp/container.py +110 -0
- basic_memory/mcp/project_context.py +155 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +61 -9
- basic_memory/mcp/tools/__init__.py +33 -21
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +152 -0
- basic_memory/mcp/tools/chatgpt_tools.py +190 -0
- basic_memory/mcp/tools/delete_note.py +249 -0
- basic_memory/mcp/tools/edit_note.py +325 -0
- basic_memory/mcp/tools/list_directory.py +157 -0
- basic_memory/mcp/tools/move_note.py +549 -0
- basic_memory/mcp/tools/project_management.py +204 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +265 -0
- basic_memory/mcp/tools/recent_activity.py +528 -0
- basic_memory/mcp/tools/search.py +377 -24
- basic_memory/mcp/tools/utils.py +402 -16
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +82 -17
- basic_memory/models/project.py +93 -0
- basic_memory/models/search.py +68 -8
- basic_memory/project_resolver.py +222 -0
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +437 -8
- basic_memory/repository/observation_repository.py +36 -3
- basic_memory/repository/postgres_search_repository.py +451 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +140 -0
- basic_memory/repository/relation_repository.py +79 -4
- basic_memory/repository/repository.py +148 -29
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +79 -268
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +437 -0
- basic_memory/runtime.py +61 -0
- basic_memory/schemas/__init__.py +22 -9
- basic_memory/schemas/base.py +131 -12
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/directory.py +31 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +194 -25
- basic_memory/schemas/project_info.py +213 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/response.py +85 -28
- basic_memory/schemas/search.py +36 -35
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +133 -0
- basic_memory/schemas/v2/resource.py +47 -0
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +451 -138
- basic_memory/services/directory_service.py +310 -0
- basic_memory/services/entity_service.py +636 -71
- basic_memory/services/exceptions.py +21 -0
- basic_memory/services/file_service.py +402 -33
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +50 -56
- basic_memory/services/project_service.py +888 -0
- basic_memory/services/search_service.py +232 -37
- basic_memory/sync/__init__.py +4 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/coordinator.py +160 -0
- basic_memory/sync/sync_service.py +1200 -109
- basic_memory/sync/watch_service.py +432 -135
- basic_memory/telemetry.py +249 -0
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +407 -54
- basic_memory-0.17.4.dist-info/METADATA +617 -0
- basic_memory-0.17.4.dist-info/RECORD +193 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/sync.py +0 -206
- basic_memory/cli/commands/tools.py +0 -157
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -170
- basic_memory/mcp/tools/notes.py +0 -202
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -31
- basic_memory-0.7.0.dist-info/METADATA +0 -378
- basic_memory-0.7.0.dist-info/RECORD +0 -82
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
external_id: str # UUID string for API references (required after migration)
|
|
178
|
+
name: str
|
|
179
|
+
path: str
|
|
180
|
+
is_default: bool = False
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def permalink(self) -> str: # pragma: no cover
|
|
184
|
+
return generate_permalink(self.name)
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def home(self) -> Path: # pragma: no cover
|
|
188
|
+
return Path(self.path).expanduser()
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def project_url(self) -> str: # pragma: no cover
|
|
192
|
+
return f"/{generate_permalink(self.name)}"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ProjectList(BaseModel):
|
|
196
|
+
"""Response model for listing projects."""
|
|
197
|
+
|
|
198
|
+
projects: List[ProjectItem]
|
|
199
|
+
default_project: str
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class ProjectStatusResponse(BaseModel):
|
|
203
|
+
"""Response model for switching projects."""
|
|
204
|
+
|
|
205
|
+
message: str = Field(..., description="Status message about the project switch")
|
|
206
|
+
status: str = Field(..., description="Status of the switch (success or error)")
|
|
207
|
+
default: bool = Field(..., description="True if the project was set as the default")
|
|
208
|
+
old_project: Optional[ProjectItem] = Field(
|
|
209
|
+
None, description="Information about the project being switched from"
|
|
210
|
+
)
|
|
211
|
+
new_project: Optional[ProjectItem] = Field(
|
|
212
|
+
None, description="Information about the project being switched to"
|
|
213
|
+
)
|
|
@@ -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
|
+
)
|
basic_memory/schemas/request.py
CHANGED
|
@@ -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,
|
|
@@ -56,3 +56,57 @@ class GetEntitiesRequest(BaseModel):
|
|
|
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()
|
basic_memory/schemas/response.py
CHANGED
|
@@ -14,7 +14,7 @@ Key Features:
|
|
|
14
14
|
from datetime import datetime
|
|
15
15
|
from typing import List, Optional, Dict
|
|
16
16
|
|
|
17
|
-
from pydantic import BaseModel, ConfigDict, Field,
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
18
18
|
|
|
19
19
|
from basic_memory.schemas.base import Relation, Permalink, EntityType, ContentType, Observation
|
|
20
20
|
|
|
@@ -64,32 +64,89 @@ class RelationResponse(Relation, SQLAlchemyModel):
|
|
|
64
64
|
|
|
65
65
|
permalink: Permalink
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
67
|
+
# Override base Relation fields to allow Optional values
|
|
68
|
+
from_id: Optional[Permalink] = Field(default=None) # pyright: ignore[reportIncompatibleVariableOverride]
|
|
69
|
+
to_id: Optional[Permalink] = Field(default=None) # pyright: ignore[reportIncompatibleVariableOverride]
|
|
70
|
+
to_name: Optional[str] = Field(default=None)
|
|
71
|
+
|
|
72
|
+
@model_validator(mode="before")
|
|
73
|
+
@classmethod
|
|
74
|
+
def resolve_entity_references(cls, data):
|
|
75
|
+
"""Resolve from_id and to_id from joined entities, falling back to file_path.
|
|
76
|
+
|
|
77
|
+
When loading from SQLAlchemy models, the from_entity and to_entity relationships
|
|
78
|
+
are joined. We extract the permalink from these entities, falling back to
|
|
79
|
+
file_path when permalink is None.
|
|
80
|
+
|
|
81
|
+
We use file_path directly (not converted to permalink format) because if the
|
|
82
|
+
entity doesn't have a permalink, the system won't be able to find it by a
|
|
83
|
+
generated one anyway. Using the actual file_path preserves the real identifier.
|
|
84
|
+
"""
|
|
85
|
+
# Handle dict input (e.g., from API or tests)
|
|
86
|
+
if isinstance(data, dict):
|
|
87
|
+
from_entity = data.get("from_entity")
|
|
88
|
+
to_entity = data.get("to_entity")
|
|
89
|
+
|
|
90
|
+
# Resolve from_id: prefer permalink, fall back to file_path
|
|
91
|
+
if from_entity and isinstance(from_entity, dict):
|
|
92
|
+
permalink = from_entity.get("permalink")
|
|
93
|
+
if permalink:
|
|
94
|
+
data["from_id"] = permalink
|
|
95
|
+
elif from_entity.get("file_path"):
|
|
96
|
+
data["from_id"] = from_entity["file_path"]
|
|
97
|
+
|
|
98
|
+
# Resolve to_id: prefer permalink, fall back to file_path
|
|
99
|
+
if to_entity and isinstance(to_entity, dict):
|
|
100
|
+
permalink = to_entity.get("permalink")
|
|
101
|
+
if permalink:
|
|
102
|
+
data["to_id"] = permalink
|
|
103
|
+
elif to_entity.get("file_path"):
|
|
104
|
+
data["to_id"] = to_entity["file_path"]
|
|
105
|
+
|
|
106
|
+
# Also resolve to_name from entity title
|
|
107
|
+
if to_entity.get("title") and not data.get("to_name"):
|
|
108
|
+
data["to_name"] = to_entity["title"]
|
|
109
|
+
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
# Handle SQLAlchemy model input (from_attributes=True)
|
|
113
|
+
# Access attributes directly from the ORM model
|
|
114
|
+
from_entity = getattr(data, "from_entity", None)
|
|
115
|
+
to_entity = getattr(data, "to_entity", None)
|
|
116
|
+
|
|
117
|
+
# Build a dict from the model's attributes
|
|
118
|
+
result = {}
|
|
119
|
+
|
|
120
|
+
# Copy base fields
|
|
121
|
+
for field in ["permalink", "relation_type", "context", "to_name"]:
|
|
122
|
+
if hasattr(data, field):
|
|
123
|
+
result[field] = getattr(data, field)
|
|
124
|
+
|
|
125
|
+
# Resolve from_id: prefer permalink, fall back to file_path
|
|
126
|
+
if from_entity:
|
|
127
|
+
permalink = getattr(from_entity, "permalink", None)
|
|
128
|
+
file_path = getattr(from_entity, "file_path", None)
|
|
129
|
+
if permalink:
|
|
130
|
+
result["from_id"] = permalink
|
|
131
|
+
elif file_path:
|
|
132
|
+
result["from_id"] = file_path
|
|
133
|
+
|
|
134
|
+
# Resolve to_id: prefer permalink, fall back to file_path
|
|
135
|
+
if to_entity:
|
|
136
|
+
permalink = getattr(to_entity, "permalink", None)
|
|
137
|
+
file_path = getattr(to_entity, "file_path", None)
|
|
138
|
+
if permalink:
|
|
139
|
+
result["to_id"] = permalink
|
|
140
|
+
elif file_path:
|
|
141
|
+
result["to_id"] = file_path
|
|
142
|
+
|
|
143
|
+
# Also resolve to_name from entity title if not set
|
|
144
|
+
if not result.get("to_name"):
|
|
145
|
+
title = getattr(to_entity, "title", None)
|
|
146
|
+
if title:
|
|
147
|
+
result["to_name"] = title
|
|
148
|
+
|
|
149
|
+
return result
|
|
93
150
|
|
|
94
151
|
|
|
95
152
|
class EntityResponse(SQLAlchemyModel):
|
|
@@ -131,7 +188,7 @@ class EntityResponse(SQLAlchemyModel):
|
|
|
131
188
|
}
|
|
132
189
|
"""
|
|
133
190
|
|
|
134
|
-
permalink: Permalink
|
|
191
|
+
permalink: Optional[Permalink]
|
|
135
192
|
title: str
|
|
136
193
|
file_path: str
|
|
137
194
|
entity_type: EntityType
|
basic_memory/schemas/search.py
CHANGED
|
@@ -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 #
|
|
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[
|
|
45
|
-
entity_types: Optional[List[
|
|
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,60 +65,53 @@ 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
|
-
|
|
90
|
+
title: str
|
|
71
91
|
type: SearchItemType
|
|
72
92
|
score: float
|
|
73
|
-
|
|
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
|
|
|
100
|
+
# IDs for v2 API consistency
|
|
101
|
+
entity_id: Optional[int] = None # Entity ID (always present for entities)
|
|
102
|
+
observation_id: Optional[int] = None # Observation ID (for observation results)
|
|
103
|
+
relation_id: Optional[int] = None # Relation ID (for relation results)
|
|
104
|
+
|
|
78
105
|
# Type-specific fields
|
|
79
|
-
entity_id: Optional[int] = None # For observations
|
|
80
106
|
category: Optional[str] = None # For observations
|
|
81
|
-
|
|
82
|
-
|
|
107
|
+
from_entity: Optional[Permalink] = None # For relations
|
|
108
|
+
to_entity: Optional[Permalink] = None # For relations
|
|
83
109
|
relation_type: Optional[str] = None # For relations
|
|
84
110
|
|
|
85
111
|
|
|
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
112
|
class SearchResponse(BaseModel):
|
|
102
113
|
"""Wrapper for search results."""
|
|
103
114
|
|
|
104
115
|
results: List[SearchResult]
|
|
105
116
|
current_page: int
|
|
106
117
|
page_size: int
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
# Schema for future advanced search endpoint
|
|
110
|
-
class AdvancedSearchQuery(BaseModel):
|
|
111
|
-
"""Advanced full-text search with explicit FTS5 syntax."""
|
|
112
|
-
|
|
113
|
-
query: str # Raw FTS5 query (e.g., "foo AND bar")
|
|
114
|
-
types: Optional[List[SearchItemType]] = None
|
|
115
|
-
entity_types: Optional[List[str]] = None
|
|
116
|
-
after_date: Optional[Union[datetime, str]] = None
|