basic-memory 0.12.2__py3-none-any.whl → 0.13.0__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 +2 -1
- basic_memory/alembic/env.py +1 -1
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -6
- basic_memory/api/app.py +43 -13
- basic_memory/api/routers/__init__.py +4 -2
- basic_memory/api/routers/directory_router.py +63 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +139 -37
- basic_memory/api/routers/management_router.py +78 -0
- basic_memory/api/routers/memory_router.py +6 -62
- basic_memory/api/routers/project_router.py +234 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/search_router.py +3 -21
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +20 -21
- basic_memory/cli/commands/__init__.py +2 -1
- basic_memory/cli/commands/auth.py +136 -0
- basic_memory/cli/commands/db.py +3 -3
- basic_memory/cli/commands/import_chatgpt.py +31 -207
- basic_memory/cli/commands/import_claude_conversations.py +16 -142
- basic_memory/cli/commands/import_claude_projects.py +33 -143
- basic_memory/cli/commands/import_memory_json.py +26 -83
- basic_memory/cli/commands/mcp.py +71 -18
- basic_memory/cli/commands/project.py +102 -70
- basic_memory/cli/commands/status.py +19 -9
- basic_memory/cli/commands/sync.py +44 -58
- basic_memory/cli/commands/tool.py +6 -6
- basic_memory/cli/main.py +1 -5
- basic_memory/config.py +143 -87
- basic_memory/db.py +6 -4
- basic_memory/deps.py +227 -30
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +222 -0
- basic_memory/importers/claude_conversations_importer.py +172 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +93 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +5 -2
- basic_memory/mcp/auth_provider.py +270 -0
- basic_memory/mcp/external_auth_provider.py +321 -0
- basic_memory/mcp/project_session.py +103 -0
- basic_memory/mcp/prompts/__init__.py +2 -0
- basic_memory/mcp/prompts/continue_conversation.py +18 -68
- basic_memory/mcp/prompts/recent_activity.py +20 -4
- basic_memory/mcp/prompts/search.py +14 -140
- basic_memory/mcp/prompts/sync_status.py +116 -0
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/{tools → resources}/project_info.py +6 -2
- basic_memory/mcp/server.py +86 -13
- basic_memory/mcp/supabase_auth_provider.py +463 -0
- basic_memory/mcp/tools/__init__.py +24 -0
- basic_memory/mcp/tools/build_context.py +43 -8
- basic_memory/mcp/tools/canvas.py +17 -3
- basic_memory/mcp/tools/delete_note.py +168 -5
- basic_memory/mcp/tools/edit_note.py +303 -0
- basic_memory/mcp/tools/list_directory.py +154 -0
- basic_memory/mcp/tools/move_note.py +299 -0
- basic_memory/mcp/tools/project_management.py +332 -0
- basic_memory/mcp/tools/read_content.py +15 -6
- basic_memory/mcp/tools/read_note.py +28 -9
- basic_memory/mcp/tools/recent_activity.py +47 -16
- basic_memory/mcp/tools/search.py +189 -8
- basic_memory/mcp/tools/sync_status.py +254 -0
- basic_memory/mcp/tools/utils.py +184 -12
- basic_memory/mcp/tools/view_note.py +66 -0
- basic_memory/mcp/tools/write_note.py +24 -17
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +16 -4
- basic_memory/models/project.py +78 -0
- basic_memory/models/search.py +8 -5
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +8 -3
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +3 -2
- basic_memory/repository/project_repository.py +85 -0
- basic_memory/repository/relation_repository.py +8 -2
- basic_memory/repository/repository.py +107 -15
- basic_memory/repository/search_repository.py +192 -54
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/base.py +33 -5
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +34 -0
- basic_memory/schemas/memory.py +84 -13
- basic_memory/schemas/project_info.py +112 -2
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/search.py +1 -1
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +208 -95
- basic_memory/services/directory_service.py +167 -0
- basic_memory/services/entity_service.py +399 -6
- basic_memory/services/exceptions.py +6 -0
- basic_memory/services/file_service.py +14 -15
- basic_memory/services/initialization.py +170 -66
- basic_memory/services/link_resolver.py +35 -12
- basic_memory/services/migration_service.py +168 -0
- basic_memory/services/project_service.py +671 -0
- basic_memory/services/search_service.py +77 -2
- basic_memory/services/sync_status_service.py +181 -0
- basic_memory/sync/background_sync.py +25 -0
- basic_memory/sync/sync_service.py +102 -21
- basic_memory/sync/watch_service.py +63 -39
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +67 -17
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/METADATA +26 -4
- basic_memory-0.13.0.dist-info/RECORD +138 -0
- basic_memory/api/routers/project_info_router.py +0 -274
- basic_memory/mcp/main.py +0 -24
- basic_memory-0.12.2.dist-info/RECORD +0 -100
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/licenses/LICENSE +0 -0
basic_memory/schemas/base.py
CHANGED
|
@@ -13,7 +13,7 @@ Key Concepts:
|
|
|
13
13
|
|
|
14
14
|
import mimetypes
|
|
15
15
|
import re
|
|
16
|
-
from datetime import datetime
|
|
16
|
+
from datetime import datetime, time
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import List, Optional, Annotated, Dict
|
|
19
19
|
|
|
@@ -46,15 +46,43 @@ def to_snake_case(name: str) -> str:
|
|
|
46
46
|
return s2.lower()
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def parse_timeframe(timeframe: str) -> datetime:
|
|
50
|
+
"""Parse timeframe with special handling for 'today' and other natural language expressions.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
timeframe: Natural language timeframe like 'today', '1d', '1 week ago', etc.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
datetime: The parsed datetime for the start of the timeframe
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
parse_timeframe('today') -> 2025-06-05 00:00:00 (start of today)
|
|
60
|
+
parse_timeframe('1d') -> 2025-06-04 14:50:00 (24 hours ago)
|
|
61
|
+
parse_timeframe('1 week ago') -> 2025-05-29 14:50:00 (1 week ago)
|
|
62
|
+
"""
|
|
63
|
+
if timeframe.lower() == "today":
|
|
64
|
+
# Return start of today (00:00:00)
|
|
65
|
+
return datetime.combine(datetime.now().date(), time.min)
|
|
66
|
+
else:
|
|
67
|
+
# Use dateparser for other formats
|
|
68
|
+
parsed = parse(timeframe)
|
|
69
|
+
if not parsed:
|
|
70
|
+
raise ValueError(f"Could not parse timeframe: {timeframe}")
|
|
71
|
+
return parsed
|
|
72
|
+
|
|
73
|
+
|
|
49
74
|
def validate_timeframe(timeframe: str) -> str:
|
|
50
75
|
"""Convert human readable timeframes to a duration relative to the current time."""
|
|
51
76
|
if not isinstance(timeframe, str):
|
|
52
77
|
raise ValueError("Timeframe must be a string")
|
|
53
78
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
if
|
|
57
|
-
|
|
79
|
+
# Preserve special timeframe strings that need custom handling
|
|
80
|
+
special_timeframes = ["today"]
|
|
81
|
+
if timeframe.lower() in special_timeframes:
|
|
82
|
+
return timeframe.lower()
|
|
83
|
+
|
|
84
|
+
# Parse relative time expression using our enhanced parser
|
|
85
|
+
parsed = parse_timeframe(timeframe)
|
|
58
86
|
|
|
59
87
|
# Convert to duration
|
|
60
88
|
now = datetime.now()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Schemas for directory tree operations."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import List, Optional, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DirectoryNode(BaseModel):
|
|
10
|
+
"""Directory node in file system."""
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
file_path: Optional[str] = None # Original path without leading slash (matches DB)
|
|
14
|
+
directory_path: str # Path with leading slash for directory navigation
|
|
15
|
+
type: Literal["directory", "file"]
|
|
16
|
+
children: List["DirectoryNode"] = [] # Default to empty list
|
|
17
|
+
title: Optional[str] = None
|
|
18
|
+
permalink: Optional[str] = None
|
|
19
|
+
entity_id: Optional[int] = None
|
|
20
|
+
entity_type: Optional[str] = None
|
|
21
|
+
content_type: Optional[str] = None
|
|
22
|
+
updated_at: Optional[datetime] = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def has_children(self) -> bool:
|
|
26
|
+
return bool(self.children)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Support for recursive model
|
|
30
|
+
DirectoryNode.model_rebuild()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Schemas for import services."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ImportResult(BaseModel):
|
|
9
|
+
"""Common import result schema."""
|
|
10
|
+
|
|
11
|
+
import_count: Dict[str, int]
|
|
12
|
+
success: bool
|
|
13
|
+
error_message: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChatImportResult(ImportResult):
|
|
17
|
+
"""Result schema for chat imports."""
|
|
18
|
+
|
|
19
|
+
conversations: int = 0
|
|
20
|
+
messages: int = 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProjectImportResult(ImportResult):
|
|
24
|
+
"""Result schema for project imports."""
|
|
25
|
+
|
|
26
|
+
documents: int = 0
|
|
27
|
+
prompts: int = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EntityImportResult(ImportResult):
|
|
31
|
+
"""Result schema for entity imports."""
|
|
32
|
+
|
|
33
|
+
entities: int = 0
|
|
34
|
+
relations: int = 0
|
basic_memory/schemas/memory.py
CHANGED
|
@@ -9,8 +9,44 @@ from pydantic import BaseModel, Field, BeforeValidator, TypeAdapter
|
|
|
9
9
|
from basic_memory.schemas.search import SearchItemType
|
|
10
10
|
|
|
11
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
|
+
if not path or not path.strip():
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
# Check for invalid protocol schemes within the path first (more specific)
|
|
33
|
+
if "://" in path:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
# Check for double slashes (except at the beginning for absolute paths)
|
|
37
|
+
if "//" in path:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
# Check for invalid characters (excluding * which is used for pattern matching)
|
|
41
|
+
invalid_chars = {"<", ">", '"', "|", "?"}
|
|
42
|
+
if any(char in path for char in invalid_chars):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
12
48
|
def normalize_memory_url(url: str | None) -> str:
|
|
13
|
-
"""Normalize a MemoryUrl string.
|
|
49
|
+
"""Normalize a MemoryUrl string with validation.
|
|
14
50
|
|
|
15
51
|
Args:
|
|
16
52
|
url: A path like "specs/search" or "memory://specs/search"
|
|
@@ -18,22 +54,43 @@ def normalize_memory_url(url: str | None) -> str:
|
|
|
18
54
|
Returns:
|
|
19
55
|
Normalized URL starting with memory://
|
|
20
56
|
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: If the URL path is malformed
|
|
59
|
+
|
|
21
60
|
Examples:
|
|
22
61
|
>>> normalize_memory_url("specs/search")
|
|
23
62
|
'memory://specs/search'
|
|
24
63
|
>>> normalize_memory_url("memory://specs/search")
|
|
25
64
|
'memory://specs/search'
|
|
65
|
+
>>> normalize_memory_url("memory//test")
|
|
66
|
+
Traceback (most recent call last):
|
|
67
|
+
...
|
|
68
|
+
ValueError: Invalid memory URL path: 'memory//test' contains double slashes
|
|
26
69
|
"""
|
|
27
70
|
if not url:
|
|
28
71
|
return ""
|
|
29
72
|
|
|
30
73
|
clean_path = url.removeprefix("memory://")
|
|
74
|
+
|
|
75
|
+
# Validate the extracted path
|
|
76
|
+
if not validate_memory_url_path(clean_path):
|
|
77
|
+
# Provide specific error messages for common issues
|
|
78
|
+
if "://" in clean_path:
|
|
79
|
+
raise ValueError(f"Invalid memory URL path: '{clean_path}' contains protocol scheme")
|
|
80
|
+
elif "//" in clean_path:
|
|
81
|
+
raise ValueError(f"Invalid memory URL path: '{clean_path}' contains double slashes")
|
|
82
|
+
elif not clean_path.strip():
|
|
83
|
+
raise ValueError("Memory URL path cannot be empty or whitespace")
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"Invalid memory URL path: '{clean_path}' contains invalid characters")
|
|
86
|
+
|
|
31
87
|
return f"memory://{clean_path}"
|
|
32
88
|
|
|
33
89
|
|
|
34
90
|
MemoryUrl = Annotated[
|
|
35
91
|
str,
|
|
36
92
|
BeforeValidator(str.strip), # Clean whitespace
|
|
93
|
+
BeforeValidator(normalize_memory_url), # Validate and normalize the URL
|
|
37
94
|
MinLen(1),
|
|
38
95
|
MaxLen(2028),
|
|
39
96
|
]
|
|
@@ -100,27 +157,41 @@ class MemoryMetadata(BaseModel):
|
|
|
100
157
|
uri: Optional[str] = None
|
|
101
158
|
types: Optional[List[SearchItemType]] = None
|
|
102
159
|
depth: int
|
|
103
|
-
timeframe: str
|
|
160
|
+
timeframe: Optional[str] = None
|
|
104
161
|
generated_at: datetime
|
|
105
|
-
|
|
106
|
-
|
|
162
|
+
primary_count: Optional[int] = None # Changed field name
|
|
163
|
+
related_count: Optional[int] = None # Changed field name
|
|
164
|
+
total_results: Optional[int] = None # For backward compatibility
|
|
165
|
+
total_relations: Optional[int] = None
|
|
166
|
+
total_observations: Optional[int] = None
|
|
107
167
|
|
|
108
168
|
|
|
109
|
-
class
|
|
110
|
-
"""
|
|
169
|
+
class ContextResult(BaseModel):
|
|
170
|
+
"""Context result containing a primary item with its observations and related items."""
|
|
171
|
+
|
|
172
|
+
primary_result: EntitySummary | RelationSummary | ObservationSummary = Field(
|
|
173
|
+
description="Primary item"
|
|
174
|
+
)
|
|
111
175
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
description="results directly matching URI"
|
|
176
|
+
observations: Sequence[ObservationSummary] = Field(
|
|
177
|
+
description="Observations belonging to this entity", default_factory=list
|
|
115
178
|
)
|
|
116
179
|
|
|
117
|
-
# Related entities
|
|
118
180
|
related_results: Sequence[EntitySummary | RelationSummary | ObservationSummary] = Field(
|
|
119
|
-
description="
|
|
181
|
+
description="Related items", default_factory=list
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class GraphContext(BaseModel):
|
|
186
|
+
"""Complete context response."""
|
|
187
|
+
|
|
188
|
+
# hierarchical results
|
|
189
|
+
results: Sequence[ContextResult] = Field(
|
|
190
|
+
description="Hierarchical results with related items nested", default_factory=list
|
|
120
191
|
)
|
|
121
192
|
|
|
122
193
|
# Context metadata
|
|
123
194
|
metadata: MemoryMetadata
|
|
124
195
|
|
|
125
|
-
page: int =
|
|
126
|
-
page_size: int =
|
|
196
|
+
page: Optional[int] = None
|
|
197
|
+
page_size: Optional[int] = None
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Schema for project info response."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from typing import Dict, List, Optional, Any
|
|
5
6
|
|
|
@@ -75,14 +76,24 @@ class SystemStatus(BaseModel):
|
|
|
75
76
|
timestamp: datetime = Field(description="Timestamp when the information was collected")
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
class ProjectDetail(BaseModel):
|
|
80
|
+
"""Detailed information about a project."""
|
|
81
|
+
|
|
82
|
+
path: str = Field(description="Path to the project directory")
|
|
83
|
+
active: bool = Field(description="Whether the project is active")
|
|
84
|
+
id: Optional[int] = Field(description="Database ID of the project if available")
|
|
85
|
+
is_default: bool = Field(description="Whether this is the default project")
|
|
86
|
+
permalink: str = Field(description="URL-friendly identifier for the project")
|
|
87
|
+
|
|
88
|
+
|
|
78
89
|
class ProjectInfoResponse(BaseModel):
|
|
79
90
|
"""Response for the project_info tool."""
|
|
80
91
|
|
|
81
92
|
# Project configuration
|
|
82
93
|
project_name: str = Field(description="Name of the current project")
|
|
83
94
|
project_path: str = Field(description="Path to the current project files")
|
|
84
|
-
available_projects: Dict[str, str] = Field(
|
|
85
|
-
description="Map of configured project names to
|
|
95
|
+
available_projects: Dict[str, Dict[str, Any]] = Field(
|
|
96
|
+
description="Map of configured project names to detailed project information"
|
|
86
97
|
)
|
|
87
98
|
default_project: str = Field(description="Name of the default project")
|
|
88
99
|
|
|
@@ -94,3 +105,102 @@ class ProjectInfoResponse(BaseModel):
|
|
|
94
105
|
|
|
95
106
|
# System status
|
|
96
107
|
system: SystemStatus = Field(description="System and service status information")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ProjectInfoRequest(BaseModel):
|
|
111
|
+
"""Request model for switching projects."""
|
|
112
|
+
|
|
113
|
+
name: str = Field(..., description="Name of the project to switch to")
|
|
114
|
+
path: str = Field(..., description="Path to the project directory")
|
|
115
|
+
set_default: bool = Field(..., description="Set the project as the default")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class WatchEvent(BaseModel):
|
|
119
|
+
timestamp: datetime
|
|
120
|
+
path: str
|
|
121
|
+
action: str # new, delete, etc
|
|
122
|
+
status: str # success, error
|
|
123
|
+
checksum: Optional[str]
|
|
124
|
+
error: Optional[str] = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class WatchServiceState(BaseModel):
|
|
128
|
+
# Service status
|
|
129
|
+
running: bool = False
|
|
130
|
+
start_time: datetime = datetime.now() # Use directly with Pydantic model
|
|
131
|
+
pid: int = os.getpid() # Use directly with Pydantic model
|
|
132
|
+
|
|
133
|
+
# Stats
|
|
134
|
+
error_count: int = 0
|
|
135
|
+
last_error: Optional[datetime] = None
|
|
136
|
+
last_scan: Optional[datetime] = None
|
|
137
|
+
|
|
138
|
+
# File counts
|
|
139
|
+
synced_files: int = 0
|
|
140
|
+
|
|
141
|
+
# Recent activity
|
|
142
|
+
recent_events: List[WatchEvent] = [] # Use directly with Pydantic model
|
|
143
|
+
|
|
144
|
+
def add_event(
|
|
145
|
+
self,
|
|
146
|
+
path: str,
|
|
147
|
+
action: str,
|
|
148
|
+
status: str,
|
|
149
|
+
checksum: Optional[str] = None,
|
|
150
|
+
error: Optional[str] = None,
|
|
151
|
+
) -> WatchEvent: # pragma: no cover
|
|
152
|
+
event = WatchEvent(
|
|
153
|
+
timestamp=datetime.now(),
|
|
154
|
+
path=path,
|
|
155
|
+
action=action,
|
|
156
|
+
status=status,
|
|
157
|
+
checksum=checksum,
|
|
158
|
+
error=error,
|
|
159
|
+
)
|
|
160
|
+
self.recent_events.insert(0, event)
|
|
161
|
+
self.recent_events = self.recent_events[:100] # Keep last 100
|
|
162
|
+
return event
|
|
163
|
+
|
|
164
|
+
def record_error(self, error: str): # pragma: no cover
|
|
165
|
+
self.error_count += 1
|
|
166
|
+
self.add_event(path="", action="sync", status="error", error=error)
|
|
167
|
+
self.last_error = datetime.now()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ProjectWatchStatus(BaseModel):
|
|
171
|
+
"""Project with its watch status."""
|
|
172
|
+
|
|
173
|
+
name: str = Field(..., description="Name of the project")
|
|
174
|
+
path: str = Field(..., description="Path to the project")
|
|
175
|
+
watch_status: Optional[WatchServiceState] = Field(
|
|
176
|
+
None, description="Watch status information for the project"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ProjectItem(BaseModel):
|
|
181
|
+
"""Simple representation of a project."""
|
|
182
|
+
|
|
183
|
+
name: str
|
|
184
|
+
path: str
|
|
185
|
+
is_default: bool = False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ProjectList(BaseModel):
|
|
189
|
+
"""Response model for listing projects."""
|
|
190
|
+
|
|
191
|
+
projects: List[ProjectItem]
|
|
192
|
+
default_project: str
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ProjectStatusResponse(BaseModel):
|
|
196
|
+
"""Response model for switching projects."""
|
|
197
|
+
|
|
198
|
+
message: str = Field(..., description="Status message about the project switch")
|
|
199
|
+
status: str = Field(..., description="Status of the switch (success or error)")
|
|
200
|
+
default: bool = Field(..., description="True if the project was set as the default")
|
|
201
|
+
old_project: Optional[ProjectItem] = Field(
|
|
202
|
+
None, description="Information about the project being switched from"
|
|
203
|
+
)
|
|
204
|
+
new_project: Optional[ProjectItem] = Field(
|
|
205
|
+
None, description="Information about the project being switched to"
|
|
206
|
+
)
|
|
@@ -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/search.py
CHANGED
|
@@ -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"]
|