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.
- basic_memory/__init__.py +5 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +27 -3
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- 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/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/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/api/app.py +63 -31
- 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 +165 -28
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +28 -67
- basic_memory/api/routers/project_router.py +406 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +219 -14
- basic_memory/api/routers/search_router.py +21 -13
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +52 -1
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/__init__.py +13 -2
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
- basic_memory/cli/commands/cloud/upload.py +233 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +51 -0
- basic_memory/cli/commands/db.py +26 -7
- basic_memory/cli/commands/import_chatgpt.py +83 -0
- basic_memory/cli/commands/import_claude_conversations.py +86 -0
- basic_memory/cli/commands/import_claude_projects.py +85 -0
- basic_memory/cli/commands/import_memory_json.py +35 -92
- basic_memory/cli/commands/mcp.py +84 -10
- basic_memory/cli/commands/project.py +876 -0
- basic_memory/cli/commands/status.py +47 -30
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +13 -6
- basic_memory/config.py +481 -22
- basic_memory/db.py +192 -32
- basic_memory/deps.py +252 -22
- basic_memory/file_utils.py +113 -58
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +232 -0
- basic_memory/importers/claude_conversations_importer.py +177 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +108 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +143 -23
- basic_memory/markdown/markdown_processor.py +3 -3
- basic_memory/markdown/plugins.py +39 -21
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/markdown/utils.py +28 -13
- basic_memory/mcp/async_client.py +134 -4
- basic_memory/mcp/project_context.py +141 -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 +7 -13
- basic_memory/mcp/tools/__init__.py +33 -21
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +130 -0
- basic_memory/mcp/tools/chatgpt_tools.py +187 -0
- basic_memory/mcp/tools/delete_note.py +225 -0
- basic_memory/mcp/tools/edit_note.py +320 -0
- basic_memory/mcp/tools/list_directory.py +167 -0
- basic_memory/mcp/tools/move_note.py +545 -0
- basic_memory/mcp/tools/project_management.py +200 -0
- basic_memory/mcp/tools/read_content.py +271 -0
- basic_memory/mcp/tools/read_note.py +255 -0
- basic_memory/mcp/tools/recent_activity.py +534 -0
- basic_memory/mcp/tools/search.py +369 -14
- basic_memory/mcp/tools/utils.py +374 -16
- basic_memory/mcp/tools/view_note.py +77 -0
- basic_memory/mcp/tools/write_note.py +207 -0
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +67 -15
- basic_memory/models/project.py +87 -0
- basic_memory/models/search.py +10 -6
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +229 -7
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +103 -0
- basic_memory/repository/relation_repository.py +21 -2
- basic_memory/repository/repository.py +147 -29
- basic_memory/repository/search_repository.py +437 -59
- basic_memory/schemas/__init__.py +22 -9
- basic_memory/schemas/base.py +97 -8
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +188 -23
- basic_memory/schemas/project_info.py +211 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +57 -3
- basic_memory/schemas/response.py +9 -1
- basic_memory/schemas/search.py +33 -35
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +251 -106
- basic_memory/services/directory_service.py +295 -0
- basic_memory/services/entity_service.py +595 -60
- basic_memory/services/exceptions.py +21 -0
- basic_memory/services/file_service.py +284 -30
- basic_memory/services/initialization.py +191 -0
- basic_memory/services/link_resolver.py +50 -56
- basic_memory/services/project_service.py +863 -0
- basic_memory/services/search_service.py +172 -34
- basic_memory/sync/__init__.py +3 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1176 -96
- basic_memory/sync/watch_service.py +412 -135
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +388 -28
- basic_memory-0.16.1.dist-info/METADATA +493 -0
- basic_memory-0.16.1.dist-info/RECORD +148 -0
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/sync.py +0 -203
- basic_memory/mcp/tools/knowledge.py +0 -56
- basic_memory/mcp/tools/memory.py +0 -151
- basic_memory/mcp/tools/notes.py +0 -122
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -34
- basic_memory-0.2.12.dist-info/METADATA +0 -291
- basic_memory-0.2.12.dist-info/RECORD +0 -78
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Service for building rich context from the knowledge graph."""
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from typing import List, Optional, Tuple
|
|
6
6
|
|
|
@@ -8,9 +8,11 @@ from loguru import logger
|
|
|
8
8
|
from sqlalchemy import text
|
|
9
9
|
|
|
10
10
|
from basic_memory.repository.entity_repository import EntityRepository
|
|
11
|
-
from basic_memory.repository.
|
|
11
|
+
from basic_memory.repository.observation_repository import ObservationRepository
|
|
12
|
+
from basic_memory.repository.search_repository import SearchRepository, SearchIndexRow
|
|
12
13
|
from basic_memory.schemas.memory import MemoryUrl, memory_url_path
|
|
13
14
|
from basic_memory.schemas.search import SearchItemType
|
|
15
|
+
from basic_memory.utils import generate_permalink
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
@dataclass
|
|
@@ -31,6 +33,38 @@ class ContextResultRow:
|
|
|
31
33
|
entity_id: Optional[int] = None
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
@dataclass
|
|
37
|
+
class ContextResultItem:
|
|
38
|
+
"""A hierarchical result containing a primary item with its observations and related items."""
|
|
39
|
+
|
|
40
|
+
primary_result: ContextResultRow | SearchIndexRow
|
|
41
|
+
observations: List[ContextResultRow] = field(default_factory=list)
|
|
42
|
+
related_results: List[ContextResultRow] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ContextMetadata:
|
|
47
|
+
"""Metadata about a context result."""
|
|
48
|
+
|
|
49
|
+
uri: Optional[str] = None
|
|
50
|
+
types: Optional[List[SearchItemType]] = None
|
|
51
|
+
depth: int = 1
|
|
52
|
+
timeframe: Optional[str] = None
|
|
53
|
+
generated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
54
|
+
primary_count: int = 0
|
|
55
|
+
related_count: int = 0
|
|
56
|
+
total_observations: int = 0
|
|
57
|
+
total_relations: int = 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ContextResult:
|
|
62
|
+
"""Complete context result with metadata."""
|
|
63
|
+
|
|
64
|
+
results: List[ContextResultItem] = field(default_factory=list)
|
|
65
|
+
metadata: ContextMetadata = field(default_factory=ContextMetadata)
|
|
66
|
+
|
|
67
|
+
|
|
34
68
|
class ContextService:
|
|
35
69
|
"""Service for building rich context from memory:// URIs.
|
|
36
70
|
|
|
@@ -44,9 +78,11 @@ class ContextService:
|
|
|
44
78
|
self,
|
|
45
79
|
search_repository: SearchRepository,
|
|
46
80
|
entity_repository: EntityRepository,
|
|
81
|
+
observation_repository: ObservationRepository,
|
|
47
82
|
):
|
|
48
83
|
self.search_repository = search_repository
|
|
49
84
|
self.entity_repository = entity_repository
|
|
85
|
+
self.observation_repository = observation_repository
|
|
50
86
|
|
|
51
87
|
async def build_context(
|
|
52
88
|
self,
|
|
@@ -54,27 +90,46 @@ class ContextService:
|
|
|
54
90
|
types: Optional[List[SearchItemType]] = None,
|
|
55
91
|
depth: int = 1,
|
|
56
92
|
since: Optional[datetime] = None,
|
|
57
|
-
|
|
58
|
-
|
|
93
|
+
limit=10,
|
|
94
|
+
offset=0,
|
|
95
|
+
max_related: int = 10,
|
|
96
|
+
include_observations: bool = True,
|
|
97
|
+
) -> ContextResult:
|
|
59
98
|
"""Build rich context from a memory:// URI."""
|
|
60
99
|
logger.debug(
|
|
61
|
-
f"Building context for URI: '{memory_url}' depth: '{depth}' since: '{since}'
|
|
100
|
+
f"Building context for URI: '{memory_url}' depth: '{depth}' since: '{since}' limit: '{limit}' offset: '{offset}' max_related: '{max_related}'"
|
|
62
101
|
)
|
|
63
102
|
|
|
103
|
+
normalized_path: Optional[str] = None
|
|
64
104
|
if memory_url:
|
|
65
105
|
path = memory_url_path(memory_url)
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
logger.debug(f"Pattern search for '{path}'")
|
|
69
|
-
primary = await self.search_repository.search(permalink_match=path)
|
|
106
|
+
# Check for wildcards before normalization
|
|
107
|
+
has_wildcard = "*" in path
|
|
70
108
|
|
|
71
|
-
|
|
109
|
+
if has_wildcard:
|
|
110
|
+
# For wildcard patterns, normalize each segment separately to preserve the *
|
|
111
|
+
parts = path.split("*")
|
|
112
|
+
normalized_parts = [
|
|
113
|
+
generate_permalink(part, split_extension=False) if part else ""
|
|
114
|
+
for part in parts
|
|
115
|
+
]
|
|
116
|
+
normalized_path = "*".join(normalized_parts)
|
|
117
|
+
logger.debug(f"Pattern search for '{normalized_path}'")
|
|
118
|
+
primary = await self.search_repository.search(
|
|
119
|
+
permalink_match=normalized_path, limit=limit, offset=offset
|
|
120
|
+
)
|
|
72
121
|
else:
|
|
73
|
-
|
|
74
|
-
|
|
122
|
+
# For exact paths, normalize the whole thing
|
|
123
|
+
normalized_path = generate_permalink(path, split_extension=False)
|
|
124
|
+
logger.debug(f"Direct lookup for '{normalized_path}'")
|
|
125
|
+
primary = await self.search_repository.search(
|
|
126
|
+
permalink=normalized_path, limit=limit, offset=offset
|
|
127
|
+
)
|
|
75
128
|
else:
|
|
76
129
|
logger.debug(f"Build context for '{types}'")
|
|
77
|
-
primary = await self.search_repository.search(
|
|
130
|
+
primary = await self.search_repository.search(
|
|
131
|
+
search_item_types=types, after_date=since, limit=limit, offset=offset
|
|
132
|
+
)
|
|
78
133
|
|
|
79
134
|
# Get type_id pairs for traversal
|
|
80
135
|
|
|
@@ -83,27 +138,81 @@ class ContextService:
|
|
|
83
138
|
|
|
84
139
|
# Find related content
|
|
85
140
|
related = await self.find_related(
|
|
86
|
-
type_id_pairs, max_depth=depth, since=since, max_results=
|
|
141
|
+
type_id_pairs, max_depth=depth, since=since, max_results=max_related
|
|
87
142
|
)
|
|
88
143
|
logger.debug(f"Found {len(related)} related results")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
144
|
+
|
|
145
|
+
# Collect entity IDs from primary and related results
|
|
146
|
+
entity_ids = []
|
|
147
|
+
for result in primary:
|
|
148
|
+
if result.type == SearchItemType.ENTITY.value:
|
|
149
|
+
entity_ids.append(result.id)
|
|
150
|
+
|
|
151
|
+
for result in related:
|
|
152
|
+
if result.type == SearchItemType.ENTITY.value:
|
|
153
|
+
entity_ids.append(result.id)
|
|
154
|
+
|
|
155
|
+
# Fetch observations for all entities if requested
|
|
156
|
+
observations_by_entity = {}
|
|
157
|
+
if include_observations and entity_ids:
|
|
158
|
+
# Use our observation repository to get observations for all entities at once
|
|
159
|
+
observations_by_entity = await self.observation_repository.find_by_entities(entity_ids)
|
|
160
|
+
logger.debug(f"Found observations for {len(observations_by_entity)} entities")
|
|
161
|
+
|
|
162
|
+
# Create metadata dataclass
|
|
163
|
+
metadata = ContextMetadata(
|
|
164
|
+
uri=normalized_path if memory_url else None,
|
|
165
|
+
types=types,
|
|
166
|
+
depth=depth,
|
|
167
|
+
timeframe=since.isoformat() if since else None,
|
|
168
|
+
primary_count=len(primary),
|
|
169
|
+
related_count=len(related),
|
|
170
|
+
total_observations=sum(len(obs) for obs in observations_by_entity.values()),
|
|
171
|
+
total_relations=sum(1 for r in related if r.type == SearchItemType.RELATION),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Build context results list directly with ContextResultItem objects
|
|
175
|
+
context_results = []
|
|
176
|
+
|
|
177
|
+
# For each primary result
|
|
178
|
+
for primary_item in primary:
|
|
179
|
+
# Find all related items with this primary item as root
|
|
180
|
+
related_to_primary = [r for r in related if r.root_id == primary_item.id]
|
|
181
|
+
|
|
182
|
+
# Get observations for this item if it's an entity
|
|
183
|
+
item_observations = []
|
|
184
|
+
if primary_item.type == SearchItemType.ENTITY.value and include_observations:
|
|
185
|
+
# Convert Observation models to ContextResultRows
|
|
186
|
+
for obs in observations_by_entity.get(primary_item.id, []):
|
|
187
|
+
item_observations.append(
|
|
188
|
+
ContextResultRow(
|
|
189
|
+
type="observation",
|
|
190
|
+
id=obs.id,
|
|
191
|
+
title=f"{obs.category}: {obs.content[:50]}...",
|
|
192
|
+
permalink=generate_permalink(
|
|
193
|
+
f"{primary_item.permalink}/observations/{obs.category}/{obs.content}"
|
|
194
|
+
),
|
|
195
|
+
file_path=primary_item.file_path,
|
|
196
|
+
content=obs.content,
|
|
197
|
+
category=obs.category,
|
|
198
|
+
entity_id=primary_item.id,
|
|
199
|
+
depth=0,
|
|
200
|
+
root_id=primary_item.id,
|
|
201
|
+
created_at=primary_item.created_at, # created_at time from entity
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Create ContextResultItem directly
|
|
206
|
+
context_item = ContextResultItem(
|
|
207
|
+
primary_result=primary_item,
|
|
208
|
+
observations=item_observations,
|
|
209
|
+
related_results=related_to_primary,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
context_results.append(context_item)
|
|
213
|
+
|
|
214
|
+
# Return the structured ContextResult
|
|
215
|
+
return ContextResult(results=context_results, metadata=metadata)
|
|
107
216
|
|
|
108
217
|
async def find_related(
|
|
109
218
|
self,
|
|
@@ -116,7 +225,6 @@ class ContextService:
|
|
|
116
225
|
|
|
117
226
|
Uses recursive CTE to find:
|
|
118
227
|
- Connected entities
|
|
119
|
-
- Their observations
|
|
120
228
|
- Relations that connect them
|
|
121
229
|
|
|
122
230
|
Note on depth:
|
|
@@ -130,105 +238,144 @@ class ContextService:
|
|
|
130
238
|
if not type_id_pairs:
|
|
131
239
|
return []
|
|
132
240
|
|
|
133
|
-
|
|
241
|
+
# Extract entity IDs from type_id_pairs for the optimized query
|
|
242
|
+
entity_ids = [i for t, i in type_id_pairs if t == "entity"]
|
|
243
|
+
|
|
244
|
+
if not entity_ids:
|
|
245
|
+
logger.debug("No entity IDs found in type_id_pairs")
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
logger.debug(
|
|
249
|
+
f"Finding connected items for {len(entity_ids)} entities with depth {max_depth}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Build the VALUES clause for entity IDs
|
|
253
|
+
entity_id_values = ", ".join([str(i) for i in entity_ids])
|
|
134
254
|
|
|
135
|
-
#
|
|
255
|
+
# For compatibility with the old query, we still need this for filtering
|
|
136
256
|
values = ", ".join([f"('{t}', {i})" for t, i in type_id_pairs])
|
|
137
257
|
|
|
138
|
-
# Parameters for bindings
|
|
139
|
-
params = {
|
|
258
|
+
# Parameters for bindings - include project_id for security filtering
|
|
259
|
+
params = {
|
|
260
|
+
"max_depth": max_depth,
|
|
261
|
+
"max_results": max_results,
|
|
262
|
+
"project_id": self.search_repository.project_id,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Build date and timeframe filters conditionally based on since parameter
|
|
140
266
|
if since:
|
|
141
267
|
params["since_date"] = since.isoformat() # pyright: ignore
|
|
268
|
+
date_filter = "AND e.created_at >= :since_date"
|
|
269
|
+
relation_date_filter = "AND e_from.created_at >= :since_date"
|
|
270
|
+
timeframe_condition = "AND eg.relation_date >= :since_date"
|
|
271
|
+
else:
|
|
272
|
+
date_filter = ""
|
|
273
|
+
relation_date_filter = ""
|
|
274
|
+
timeframe_condition = ""
|
|
142
275
|
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
related_date_filter = "AND e.created_at >= :since_date" if since else ""
|
|
276
|
+
# Add project filtering for security - ensure all entities and relations belong to the same project
|
|
277
|
+
project_filter = "AND e.project_id = :project_id"
|
|
278
|
+
relation_project_filter = "AND e_from.project_id = :project_id"
|
|
147
279
|
|
|
280
|
+
# Use a CTE that operates directly on entity and relation tables
|
|
281
|
+
# This avoids the overhead of the search_index virtual table
|
|
148
282
|
query = text(f"""
|
|
149
|
-
WITH RECURSIVE
|
|
150
|
-
-- Base case: seed
|
|
283
|
+
WITH RECURSIVE entity_graph AS (
|
|
284
|
+
-- Base case: seed entities
|
|
151
285
|
SELECT
|
|
152
|
-
id,
|
|
153
|
-
type,
|
|
154
|
-
title,
|
|
155
|
-
permalink,
|
|
156
|
-
file_path,
|
|
157
|
-
from_id,
|
|
158
|
-
to_id,
|
|
159
|
-
relation_type,
|
|
160
|
-
content,
|
|
161
|
-
category,
|
|
162
|
-
entity_id,
|
|
286
|
+
e.id,
|
|
287
|
+
'entity' as type,
|
|
288
|
+
e.title,
|
|
289
|
+
e.permalink,
|
|
290
|
+
e.file_path,
|
|
291
|
+
NULL as from_id,
|
|
292
|
+
NULL as to_id,
|
|
293
|
+
NULL as relation_type,
|
|
294
|
+
NULL as content,
|
|
295
|
+
NULL as category,
|
|
296
|
+
NULL as entity_id,
|
|
163
297
|
0 as depth,
|
|
164
|
-
id as root_id,
|
|
165
|
-
created_at,
|
|
166
|
-
created_at as relation_date,
|
|
298
|
+
e.id as root_id,
|
|
299
|
+
e.created_at,
|
|
300
|
+
e.created_at as relation_date,
|
|
167
301
|
0 as is_incoming
|
|
168
|
-
FROM
|
|
169
|
-
WHERE
|
|
302
|
+
FROM entity e
|
|
303
|
+
WHERE e.id IN ({entity_id_values})
|
|
170
304
|
{date_filter}
|
|
305
|
+
{project_filter}
|
|
171
306
|
|
|
172
|
-
UNION ALL
|
|
307
|
+
UNION ALL
|
|
173
308
|
|
|
174
|
-
-- Get relations from current entities
|
|
175
|
-
SELECT
|
|
309
|
+
-- Get relations from current entities
|
|
310
|
+
SELECT
|
|
176
311
|
r.id,
|
|
177
|
-
|
|
178
|
-
r.title,
|
|
179
|
-
|
|
180
|
-
|
|
312
|
+
'relation' as type,
|
|
313
|
+
r.relation_type || ': ' || r.to_name as title,
|
|
314
|
+
-- Relation model doesn't have permalink column - we'll generate it at runtime
|
|
315
|
+
'' as permalink,
|
|
316
|
+
e_from.file_path,
|
|
181
317
|
r.from_id,
|
|
182
318
|
r.to_id,
|
|
183
319
|
r.relation_type,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
CASE WHEN r.from_id =
|
|
192
|
-
FROM
|
|
193
|
-
JOIN
|
|
194
|
-
|
|
195
|
-
r.
|
|
196
|
-
(r.from_id = cg.id OR r.to_id = cg.id)
|
|
197
|
-
{r1_date_filter}
|
|
320
|
+
NULL as content,
|
|
321
|
+
NULL as category,
|
|
322
|
+
NULL as entity_id,
|
|
323
|
+
eg.depth + 1,
|
|
324
|
+
eg.root_id,
|
|
325
|
+
e_from.created_at, -- Use the from_entity's created_at since relation has no timestamp
|
|
326
|
+
e_from.created_at as relation_date,
|
|
327
|
+
CASE WHEN r.from_id = eg.id THEN 0 ELSE 1 END as is_incoming
|
|
328
|
+
FROM entity_graph eg
|
|
329
|
+
JOIN relation r ON (
|
|
330
|
+
eg.type = 'entity' AND
|
|
331
|
+
(r.from_id = eg.id OR r.to_id = eg.id)
|
|
198
332
|
)
|
|
199
|
-
|
|
333
|
+
JOIN entity e_from ON (
|
|
334
|
+
r.from_id = e_from.id
|
|
335
|
+
{relation_date_filter}
|
|
336
|
+
{relation_project_filter}
|
|
337
|
+
)
|
|
338
|
+
LEFT JOIN entity e_to ON (r.to_id = e_to.id)
|
|
339
|
+
WHERE eg.depth < :max_depth
|
|
340
|
+
-- Ensure to_entity (if exists) also belongs to same project
|
|
341
|
+
AND (r.to_id IS NULL OR e_to.project_id = :project_id)
|
|
200
342
|
|
|
201
343
|
UNION ALL
|
|
202
344
|
|
|
203
345
|
-- Get entities connected by relations
|
|
204
|
-
SELECT
|
|
346
|
+
SELECT
|
|
205
347
|
e.id,
|
|
206
|
-
|
|
348
|
+
'entity' as type,
|
|
207
349
|
e.title,
|
|
208
|
-
|
|
350
|
+
CASE
|
|
351
|
+
WHEN e.permalink IS NULL THEN ''
|
|
352
|
+
ELSE e.permalink
|
|
353
|
+
END as permalink,
|
|
209
354
|
e.file_path,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
355
|
+
NULL as from_id,
|
|
356
|
+
NULL as to_id,
|
|
357
|
+
NULL as relation_type,
|
|
358
|
+
NULL as content,
|
|
359
|
+
NULL as category,
|
|
360
|
+
NULL as entity_id,
|
|
361
|
+
eg.depth + 1,
|
|
362
|
+
eg.root_id,
|
|
218
363
|
e.created_at,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
FROM
|
|
222
|
-
JOIN
|
|
223
|
-
|
|
224
|
-
e.type = 'entity' AND
|
|
364
|
+
eg.relation_date,
|
|
365
|
+
eg.is_incoming
|
|
366
|
+
FROM entity_graph eg
|
|
367
|
+
JOIN entity e ON (
|
|
368
|
+
eg.type = 'relation' AND
|
|
225
369
|
e.id = CASE
|
|
226
|
-
WHEN
|
|
227
|
-
ELSE
|
|
370
|
+
WHEN eg.is_incoming = 0 THEN eg.to_id
|
|
371
|
+
ELSE eg.from_id
|
|
228
372
|
END
|
|
229
|
-
{
|
|
373
|
+
{date_filter}
|
|
374
|
+
{project_filter}
|
|
230
375
|
)
|
|
231
|
-
WHERE
|
|
376
|
+
WHERE eg.depth < :max_depth
|
|
377
|
+
-- Only include entities connected by relations within timeframe if specified
|
|
378
|
+
{timeframe_condition}
|
|
232
379
|
)
|
|
233
380
|
SELECT DISTINCT
|
|
234
381
|
type,
|
|
@@ -245,12 +392,10 @@ class ContextService:
|
|
|
245
392
|
MIN(depth) as depth,
|
|
246
393
|
root_id,
|
|
247
394
|
created_at
|
|
248
|
-
FROM
|
|
395
|
+
FROM entity_graph
|
|
249
396
|
WHERE (type, id) NOT IN ({values})
|
|
250
397
|
GROUP BY
|
|
251
|
-
type, id
|
|
252
|
-
relation_type, category, entity_id,
|
|
253
|
-
root_id, created_at
|
|
398
|
+
type, id
|
|
254
399
|
ORDER BY depth, type, id
|
|
255
400
|
LIMIT :max_results
|
|
256
401
|
""")
|