basic-memory 0.1.1__py3-none-any.whl → 0.1.2__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 +1 -1
- basic_memory/alembic/README +1 -0
- basic_memory/alembic/env.py +75 -0
- basic_memory/alembic/migrations.py +29 -0
- basic_memory/alembic/script.py.mako +26 -0
- basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
- basic_memory/api/__init__.py +2 -1
- basic_memory/api/app.py +26 -24
- basic_memory/api/routers/knowledge_router.py +28 -26
- basic_memory/api/routers/memory_router.py +17 -11
- basic_memory/api/routers/search_router.py +6 -12
- basic_memory/cli/__init__.py +1 -1
- basic_memory/cli/app.py +0 -1
- basic_memory/cli/commands/__init__.py +3 -3
- basic_memory/cli/commands/db.py +25 -0
- basic_memory/cli/commands/import_memory_json.py +35 -31
- basic_memory/cli/commands/mcp.py +20 -0
- basic_memory/cli/commands/status.py +10 -6
- basic_memory/cli/commands/sync.py +5 -56
- basic_memory/cli/main.py +5 -38
- basic_memory/config.py +3 -3
- basic_memory/db.py +15 -22
- basic_memory/deps.py +3 -4
- basic_memory/file_utils.py +36 -35
- basic_memory/markdown/entity_parser.py +13 -30
- basic_memory/markdown/markdown_processor.py +7 -7
- basic_memory/markdown/plugins.py +109 -123
- basic_memory/markdown/schemas.py +7 -8
- basic_memory/markdown/utils.py +70 -121
- basic_memory/mcp/__init__.py +1 -1
- basic_memory/mcp/async_client.py +0 -2
- basic_memory/mcp/server.py +3 -27
- basic_memory/mcp/tools/__init__.py +5 -3
- basic_memory/mcp/tools/knowledge.py +2 -2
- basic_memory/mcp/tools/memory.py +8 -4
- basic_memory/mcp/tools/search.py +2 -1
- basic_memory/mcp/tools/utils.py +1 -1
- basic_memory/models/__init__.py +1 -2
- basic_memory/models/base.py +3 -3
- basic_memory/models/knowledge.py +23 -60
- basic_memory/models/search.py +1 -1
- basic_memory/repository/__init__.py +5 -3
- basic_memory/repository/entity_repository.py +34 -98
- basic_memory/repository/relation_repository.py +0 -7
- basic_memory/repository/repository.py +2 -39
- basic_memory/repository/search_repository.py +20 -25
- basic_memory/schemas/__init__.py +4 -4
- basic_memory/schemas/base.py +21 -62
- basic_memory/schemas/delete.py +2 -3
- basic_memory/schemas/discovery.py +4 -1
- basic_memory/schemas/memory.py +12 -13
- basic_memory/schemas/request.py +4 -23
- basic_memory/schemas/response.py +10 -9
- basic_memory/schemas/search.py +4 -7
- basic_memory/services/__init__.py +2 -7
- basic_memory/services/context_service.py +116 -110
- basic_memory/services/entity_service.py +25 -62
- basic_memory/services/exceptions.py +1 -0
- basic_memory/services/file_service.py +73 -109
- basic_memory/services/link_resolver.py +9 -9
- basic_memory/services/search_service.py +22 -15
- basic_memory/services/service.py +3 -24
- basic_memory/sync/__init__.py +2 -2
- basic_memory/sync/file_change_scanner.py +3 -7
- basic_memory/sync/sync_service.py +35 -40
- basic_memory/sync/utils.py +6 -38
- basic_memory/sync/watch_service.py +26 -5
- basic_memory/utils.py +42 -33
- {basic_memory-0.1.1.dist-info → basic_memory-0.1.2.dist-info}/METADATA +2 -7
- basic_memory-0.1.2.dist-info/RECORD +78 -0
- basic_memory/mcp/main.py +0 -21
- basic_memory/mcp/tools/ai_edit.py +0 -84
- basic_memory/services/database_service.py +0 -159
- basic_memory-0.1.1.dist-info/RECORD +0 -74
- {basic_memory-0.1.1.dist-info → basic_memory-0.1.2.dist-info}/WHEEL +0 -0
- {basic_memory-0.1.1.dist-info → basic_memory-0.1.2.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.1.1.dist-info → basic_memory-0.1.2.dist-info}/licenses/LICENSE +0 -0
basic_memory/schemas/memory.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""Schemas for memory context."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import List, Optional, Annotated, Sequence
|
|
5
5
|
|
|
6
|
-
import pydantic
|
|
7
6
|
from annotated_types import MinLen, MaxLen
|
|
8
|
-
from pydantic import BaseModel,
|
|
7
|
+
from pydantic import BaseModel, Field, BeforeValidator, TypeAdapter
|
|
9
8
|
|
|
10
9
|
from basic_memory.schemas.search import SearchItemType
|
|
11
10
|
|
|
@@ -32,19 +31,20 @@ def normalize_memory_url(url: str) -> str:
|
|
|
32
31
|
MemoryUrl = Annotated[
|
|
33
32
|
str,
|
|
34
33
|
BeforeValidator(str.strip), # Clean whitespace
|
|
35
|
-
MinLen(1),
|
|
36
|
-
MaxLen(2028),
|
|
34
|
+
MinLen(1),
|
|
35
|
+
MaxLen(2028),
|
|
37
36
|
]
|
|
38
37
|
|
|
39
|
-
memory_url = TypeAdapter(MemoryUrl)
|
|
38
|
+
memory_url = TypeAdapter(MemoryUrl)
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
|
|
41
|
+
def memory_url_path(url: memory_url) -> str: # pyright: ignore
|
|
42
42
|
"""
|
|
43
43
|
Returns the uri for a url value by removing the prefix "memory://" from a given MemoryUrl.
|
|
44
44
|
|
|
45
45
|
This function processes a given MemoryUrl by removing the "memory://"
|
|
46
|
-
prefix and returns the resulting string. If the provided url does not
|
|
47
|
-
begin with "memory://", the function will simply return the input url
|
|
46
|
+
prefix and returns the resulting string. If the provided url does not
|
|
47
|
+
begin with "memory://", the function will simply return the input url
|
|
48
48
|
unchanged.
|
|
49
49
|
|
|
50
50
|
:param url: A MemoryUrl object representing the URL with a "memory://" prefix.
|
|
@@ -55,7 +55,6 @@ def memory_url_path(url: memory_url) -> str:
|
|
|
55
55
|
return url.removeprefix("memory://")
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
|
|
59
58
|
class EntitySummary(BaseModel):
|
|
60
59
|
"""Simplified entity representation."""
|
|
61
60
|
|
|
@@ -101,14 +100,14 @@ class GraphContext(BaseModel):
|
|
|
101
100
|
"""Complete context response."""
|
|
102
101
|
|
|
103
102
|
# Direct matches
|
|
104
|
-
primary_results:
|
|
103
|
+
primary_results: Sequence[EntitySummary | RelationSummary | ObservationSummary] = Field(
|
|
105
104
|
description="results directly matching URI"
|
|
106
105
|
)
|
|
107
106
|
|
|
108
107
|
# Related entities
|
|
109
|
-
related_results:
|
|
108
|
+
related_results: Sequence[EntitySummary | RelationSummary | ObservationSummary] = Field(
|
|
110
109
|
description="related results"
|
|
111
110
|
)
|
|
112
111
|
|
|
113
112
|
# Context metadata
|
|
114
|
-
metadata: MemoryMetadata
|
|
113
|
+
metadata: MemoryMetadata
|
basic_memory/schemas/request.py
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
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
|
|
4
4
|
from annotated_types import MaxLen, MinLen
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
8
8
|
from basic_memory.schemas.base import (
|
|
9
|
-
Observation,
|
|
10
|
-
Entity,
|
|
11
9
|
Relation,
|
|
12
|
-
|
|
13
|
-
ObservationCategory,
|
|
14
|
-
EntityType,
|
|
10
|
+
Permalink,
|
|
15
11
|
)
|
|
16
12
|
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
14
|
class SearchNodesRequest(BaseModel):
|
|
22
15
|
"""Search for entities in the knowledge graph.
|
|
23
16
|
|
|
@@ -47,7 +40,7 @@ class SearchNodesRequest(BaseModel):
|
|
|
47
40
|
"""
|
|
48
41
|
|
|
49
42
|
query: Annotated[str, MinLen(1), MaxLen(200)]
|
|
50
|
-
category: Optional[
|
|
43
|
+
category: Optional[str] = None
|
|
51
44
|
|
|
52
45
|
|
|
53
46
|
class GetEntitiesRequest(BaseModel):
|
|
@@ -58,20 +51,8 @@ class GetEntitiesRequest(BaseModel):
|
|
|
58
51
|
discovered through search.
|
|
59
52
|
"""
|
|
60
53
|
|
|
61
|
-
permalinks: Annotated[List[
|
|
54
|
+
permalinks: Annotated[List[Permalink], MinLen(1)]
|
|
62
55
|
|
|
63
56
|
|
|
64
57
|
class CreateRelationsRequest(BaseModel):
|
|
65
58
|
relations: List[Relation]
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
## update
|
|
69
|
-
|
|
70
|
-
# TODO remove UpdateEntityRequest
|
|
71
|
-
class UpdateEntityRequest(BaseModel):
|
|
72
|
-
"""Request to update an existing entity."""
|
|
73
|
-
|
|
74
|
-
title: Optional[str] = None
|
|
75
|
-
entity_type: Optional[EntityType] = None
|
|
76
|
-
content: Optional[str] = None
|
|
77
|
-
entity_metadata: Optional[Dict[str, Any]] = None
|
basic_memory/schemas/response.py
CHANGED
|
@@ -15,7 +15,7 @@ from typing import List, Optional, Dict
|
|
|
15
15
|
|
|
16
16
|
from pydantic import BaseModel, ConfigDict, Field, AliasPath, AliasChoices
|
|
17
17
|
|
|
18
|
-
from basic_memory.schemas.base import Relation,
|
|
18
|
+
from basic_memory.schemas.base import Relation, Permalink, EntityType, ContentType, Observation
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class SQLAlchemyModel(BaseModel):
|
|
@@ -28,7 +28,7 @@ class SQLAlchemyModel(BaseModel):
|
|
|
28
28
|
|
|
29
29
|
model_config = ConfigDict(from_attributes=True)
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
class ObservationResponse(Observation, SQLAlchemyModel):
|
|
33
33
|
"""Schema for observation data returned from the service.
|
|
34
34
|
|
|
@@ -59,7 +59,7 @@ class RelationResponse(Relation, SQLAlchemyModel):
|
|
|
59
59
|
}
|
|
60
60
|
"""
|
|
61
61
|
|
|
62
|
-
from_id:
|
|
62
|
+
from_id: Permalink = Field(
|
|
63
63
|
# use the permalink from the associated Entity
|
|
64
64
|
# or the from_id value
|
|
65
65
|
validation_alias=AliasChoices(
|
|
@@ -67,25 +67,26 @@ class RelationResponse(Relation, SQLAlchemyModel):
|
|
|
67
67
|
"from_id",
|
|
68
68
|
)
|
|
69
69
|
)
|
|
70
|
-
to_id: Optional[
|
|
70
|
+
to_id: Optional[Permalink] = Field( # pyright: ignore
|
|
71
71
|
# use the permalink from the associated Entity
|
|
72
72
|
# or the to_id value
|
|
73
73
|
validation_alias=AliasChoices(
|
|
74
74
|
AliasPath("to_entity", "permalink"),
|
|
75
75
|
"to_id",
|
|
76
|
-
),
|
|
76
|
+
),
|
|
77
|
+
default=None,
|
|
77
78
|
)
|
|
78
|
-
to_name: Optional[
|
|
79
|
+
to_name: Optional[Permalink] = Field(
|
|
79
80
|
# use the permalink from the associated Entity
|
|
80
81
|
# or the to_id value
|
|
81
82
|
validation_alias=AliasChoices(
|
|
82
83
|
AliasPath("to_entity", "title"),
|
|
83
84
|
"to_name",
|
|
84
|
-
),
|
|
85
|
+
),
|
|
86
|
+
default=None,
|
|
85
87
|
)
|
|
86
88
|
|
|
87
89
|
|
|
88
|
-
|
|
89
90
|
class EntityResponse(SQLAlchemyModel):
|
|
90
91
|
"""Complete entity data returned from the service.
|
|
91
92
|
|
|
@@ -125,7 +126,7 @@ class EntityResponse(SQLAlchemyModel):
|
|
|
125
126
|
}
|
|
126
127
|
"""
|
|
127
128
|
|
|
128
|
-
permalink:
|
|
129
|
+
permalink: Permalink
|
|
129
130
|
title: str
|
|
130
131
|
file_path: str
|
|
131
132
|
entity_type: EntityType
|
basic_memory/schemas/search.py
CHANGED
|
@@ -49,8 +49,6 @@ class SearchQuery(BaseModel):
|
|
|
49
49
|
@classmethod
|
|
50
50
|
def validate_date(cls, v: Optional[Union[datetime, str]]) -> Optional[str]:
|
|
51
51
|
"""Convert datetime to ISO format if needed."""
|
|
52
|
-
if v is None:
|
|
53
|
-
return None
|
|
54
52
|
if isinstance(v, datetime):
|
|
55
53
|
return v.isoformat()
|
|
56
54
|
return v
|
|
@@ -71,12 +69,11 @@ class SearchResult(BaseModel):
|
|
|
71
69
|
|
|
72
70
|
id: int
|
|
73
71
|
type: SearchItemType
|
|
74
|
-
score:
|
|
75
|
-
|
|
72
|
+
score: float
|
|
73
|
+
permalink: str
|
|
74
|
+
file_path: str
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
permalink: Optional[str] = None
|
|
79
|
-
file_path: Optional[str] = None
|
|
76
|
+
metadata: Optional[dict] = None
|
|
80
77
|
|
|
81
78
|
# Type-specific fields
|
|
82
79
|
entity_id: Optional[int] = None # For observations
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
"""Services package."""
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
from .service import BaseService
|
|
4
4
|
from .file_service import FileService
|
|
5
5
|
from .entity_service import EntityService
|
|
6
6
|
|
|
7
|
-
__all__ = [
|
|
8
|
-
"BaseService",
|
|
9
|
-
"FileService",
|
|
10
|
-
"EntityService",
|
|
11
|
-
"DatabaseService"
|
|
12
|
-
]
|
|
7
|
+
__all__ = ["BaseService", "FileService", "EntityService"]
|
|
@@ -50,8 +50,8 @@ class ContextService:
|
|
|
50
50
|
|
|
51
51
|
async def build_context(
|
|
52
52
|
self,
|
|
53
|
-
memory_url: MemoryUrl = None,
|
|
54
|
-
types: List[SearchItemType] = None,
|
|
53
|
+
memory_url: Optional[MemoryUrl] = None,
|
|
54
|
+
types: Optional[List[SearchItemType]] = None,
|
|
55
55
|
depth: int = 1,
|
|
56
56
|
since: Optional[datetime] = None,
|
|
57
57
|
max_results: int = 10,
|
|
@@ -66,9 +66,7 @@ class ContextService:
|
|
|
66
66
|
# Pattern matching - use search
|
|
67
67
|
if "*" in path:
|
|
68
68
|
logger.debug(f"Pattern search for '{path}'")
|
|
69
|
-
primary = await self.search_repository.search(
|
|
70
|
-
permalink_match=path
|
|
71
|
-
)
|
|
69
|
+
primary = await self.search_repository.search(permalink_match=path)
|
|
72
70
|
|
|
73
71
|
# Direct lookup for exact path
|
|
74
72
|
else:
|
|
@@ -120,11 +118,19 @@ class ContextService:
|
|
|
120
118
|
- Connected entities
|
|
121
119
|
- Their observations
|
|
122
120
|
- Relations that connect them
|
|
121
|
+
|
|
122
|
+
Note on depth:
|
|
123
|
+
Each traversal step requires two depth levels - one to find the relation,
|
|
124
|
+
and another to follow that relation to an entity. So a max_depth of 4 allows
|
|
125
|
+
traversal through two entities (relation->entity->relation->entity), while reaching
|
|
126
|
+
an entity three steps away requires max_depth=6 (relation->entity->relation->entity->relation->entity).
|
|
123
127
|
"""
|
|
128
|
+
max_depth = max_depth * 2
|
|
129
|
+
|
|
124
130
|
if not type_id_pairs:
|
|
125
131
|
return []
|
|
126
132
|
|
|
127
|
-
logger.debug(f"Finding connected items for {
|
|
133
|
+
logger.debug(f"Finding connected items for {type_id_pairs} with depth {max_depth}")
|
|
128
134
|
|
|
129
135
|
# Build the VALUES clause directly since SQLite doesn't handle parameterized IN well
|
|
130
136
|
values = ", ".join([f"('{t}', {i})" for t, i in type_id_pairs])
|
|
@@ -132,7 +138,7 @@ class ContextService:
|
|
|
132
138
|
# Parameters for bindings
|
|
133
139
|
params = {"max_depth": max_depth, "max_results": max_results}
|
|
134
140
|
if since:
|
|
135
|
-
params["since_date"] = since.isoformat()
|
|
141
|
+
params["since_date"] = since.isoformat() # pyright: ignore
|
|
136
142
|
|
|
137
143
|
# Build date filter
|
|
138
144
|
date_filter = "AND base.created_at >= :since_date" if since else ""
|
|
@@ -140,113 +146,113 @@ class ContextService:
|
|
|
140
146
|
related_date_filter = "AND e.created_at >= :since_date" if since else ""
|
|
141
147
|
|
|
142
148
|
query = text(f"""
|
|
143
|
-
WITH RECURSIVE context_graph AS (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
149
|
+
WITH RECURSIVE context_graph AS (
|
|
150
|
+
-- Base case: seed items
|
|
151
|
+
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,
|
|
163
|
+
0 as depth,
|
|
164
|
+
id as root_id,
|
|
165
|
+
created_at,
|
|
166
|
+
created_at as relation_date,
|
|
167
|
+
0 as is_incoming
|
|
168
|
+
FROM search_index base
|
|
169
|
+
WHERE (base.type, base.id) IN ({values})
|
|
170
|
+
{date_filter}
|
|
165
171
|
|
|
166
|
-
|
|
172
|
+
UNION ALL -- Allow same paths at different depths
|
|
167
173
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
174
|
+
-- Get relations from current entities
|
|
175
|
+
SELECT DISTINCT
|
|
176
|
+
r.id,
|
|
177
|
+
r.type,
|
|
178
|
+
r.title,
|
|
179
|
+
r.permalink,
|
|
180
|
+
r.file_path,
|
|
181
|
+
r.from_id,
|
|
182
|
+
r.to_id,
|
|
183
|
+
r.relation_type,
|
|
184
|
+
r.content,
|
|
185
|
+
r.category,
|
|
186
|
+
r.entity_id,
|
|
187
|
+
cg.depth + 1,
|
|
188
|
+
cg.root_id,
|
|
189
|
+
r.created_at,
|
|
190
|
+
r.created_at as relation_date,
|
|
191
|
+
CASE WHEN r.from_id = cg.id THEN 0 ELSE 1 END as is_incoming
|
|
192
|
+
FROM context_graph cg
|
|
193
|
+
JOIN search_index r ON (
|
|
194
|
+
cg.type = 'entity' AND
|
|
195
|
+
r.type = 'relation' AND
|
|
196
|
+
(r.from_id = cg.id OR r.to_id = cg.id)
|
|
197
|
+
{r1_date_filter}
|
|
198
|
+
)
|
|
199
|
+
WHERE cg.depth < :max_depth
|
|
194
200
|
|
|
195
|
-
|
|
201
|
+
UNION ALL
|
|
196
202
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
SELECT DISTINCT
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
FROM context_graph
|
|
243
|
-
WHERE (type, id) NOT IN ({values})
|
|
244
|
-
GROUP BY
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
ORDER BY depth, type, id
|
|
249
|
-
LIMIT :max_results
|
|
203
|
+
-- Get entities connected by relations
|
|
204
|
+
SELECT DISTINCT
|
|
205
|
+
e.id,
|
|
206
|
+
e.type,
|
|
207
|
+
e.title,
|
|
208
|
+
e.permalink,
|
|
209
|
+
e.file_path,
|
|
210
|
+
e.from_id,
|
|
211
|
+
e.to_id,
|
|
212
|
+
e.relation_type,
|
|
213
|
+
e.content,
|
|
214
|
+
e.category,
|
|
215
|
+
e.entity_id,
|
|
216
|
+
cg.depth + 1, -- Increment depth for entities
|
|
217
|
+
cg.root_id,
|
|
218
|
+
e.created_at,
|
|
219
|
+
cg.relation_date,
|
|
220
|
+
cg.is_incoming
|
|
221
|
+
FROM context_graph cg
|
|
222
|
+
JOIN search_index e ON (
|
|
223
|
+
cg.type = 'relation' AND
|
|
224
|
+
e.type = 'entity' AND
|
|
225
|
+
e.id = CASE
|
|
226
|
+
WHEN cg.is_incoming = 0 THEN cg.to_id -- Fixed entity lookup
|
|
227
|
+
ELSE cg.from_id
|
|
228
|
+
END
|
|
229
|
+
{related_date_filter}
|
|
230
|
+
)
|
|
231
|
+
WHERE cg.depth < :max_depth
|
|
232
|
+
)
|
|
233
|
+
SELECT DISTINCT
|
|
234
|
+
type,
|
|
235
|
+
id,
|
|
236
|
+
title,
|
|
237
|
+
permalink,
|
|
238
|
+
file_path,
|
|
239
|
+
from_id,
|
|
240
|
+
to_id,
|
|
241
|
+
relation_type,
|
|
242
|
+
content,
|
|
243
|
+
category,
|
|
244
|
+
entity_id,
|
|
245
|
+
MIN(depth) as depth,
|
|
246
|
+
root_id,
|
|
247
|
+
created_at
|
|
248
|
+
FROM context_graph
|
|
249
|
+
WHERE (type, id) NOT IN ({values})
|
|
250
|
+
GROUP BY
|
|
251
|
+
type, id, title, permalink, from_id, to_id,
|
|
252
|
+
relation_type, category, entity_id,
|
|
253
|
+
root_id, created_at
|
|
254
|
+
ORDER BY depth, type, id
|
|
255
|
+
LIMIT :max_results
|
|
250
256
|
""")
|
|
251
257
|
|
|
252
258
|
result = await self.search_repository.execute_query(query, params=params)
|