omni-cortex 1.17.0__py3-none-any.whl → 1.17.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.
- omni_cortex/__init__.py +3 -0
- omni_cortex/categorization/__init__.py +9 -0
- omni_cortex/categorization/auto_tags.py +166 -0
- omni_cortex/categorization/auto_type.py +165 -0
- omni_cortex/config.py +141 -0
- omni_cortex/dashboard.py +232 -0
- omni_cortex/database/__init__.py +24 -0
- omni_cortex/database/connection.py +137 -0
- omni_cortex/database/migrations.py +210 -0
- omni_cortex/database/schema.py +212 -0
- omni_cortex/database/sync.py +421 -0
- omni_cortex/decay/__init__.py +7 -0
- omni_cortex/decay/importance.py +147 -0
- omni_cortex/embeddings/__init__.py +35 -0
- omni_cortex/embeddings/local.py +442 -0
- omni_cortex/models/__init__.py +20 -0
- omni_cortex/models/activity.py +265 -0
- omni_cortex/models/agent.py +144 -0
- omni_cortex/models/memory.py +395 -0
- omni_cortex/models/relationship.py +206 -0
- omni_cortex/models/session.py +290 -0
- omni_cortex/resources/__init__.py +1 -0
- omni_cortex/search/__init__.py +22 -0
- omni_cortex/search/hybrid.py +197 -0
- omni_cortex/search/keyword.py +204 -0
- omni_cortex/search/ranking.py +127 -0
- omni_cortex/search/semantic.py +232 -0
- omni_cortex/server.py +360 -0
- omni_cortex/setup.py +278 -0
- omni_cortex/tools/__init__.py +13 -0
- omni_cortex/tools/activities.py +453 -0
- omni_cortex/tools/memories.py +536 -0
- omni_cortex/tools/sessions.py +311 -0
- omni_cortex/tools/utilities.py +477 -0
- omni_cortex/utils/__init__.py +13 -0
- omni_cortex/utils/formatting.py +282 -0
- omni_cortex/utils/ids.py +72 -0
- omni_cortex/utils/timestamps.py +129 -0
- omni_cortex/utils/truncation.py +111 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/main.py +43 -13
- {omni_cortex-1.17.0.dist-info → omni_cortex-1.17.2.dist-info}/METADATA +1 -1
- omni_cortex-1.17.2.dist-info/RECORD +65 -0
- omni_cortex-1.17.0.dist-info/RECORD +0 -26
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/.env.example +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/backfill_summaries.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/chat_service.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/database.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/image_service.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/logging_config.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/models.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/project_config.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/prompt_security.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/security.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/uv.lock +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/hooks/post_tool_use.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/hooks/pre_tool_use.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/hooks/session_utils.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/hooks/stop.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
- {omni_cortex-1.17.0.data → omni_cortex-1.17.2.data}/data/share/omni-cortex/hooks/user_prompt.py +0 -0
- {omni_cortex-1.17.0.dist-info → omni_cortex-1.17.2.dist-info}/WHEEL +0 -0
- {omni_cortex-1.17.0.dist-info → omni_cortex-1.17.2.dist-info}/entry_points.txt +0 -0
- {omni_cortex-1.17.0.dist-info → omni_cortex-1.17.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""Memory model and CRUD operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from typing import Optional, Any
|
|
6
|
+
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
|
7
|
+
|
|
8
|
+
from ..utils.ids import generate_memory_id
|
|
9
|
+
from ..utils.timestamps import now_iso
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MemoryBase(BaseModel):
|
|
13
|
+
"""Base memory model."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(
|
|
16
|
+
str_strip_whitespace=True,
|
|
17
|
+
validate_assignment=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
content: str = Field(..., description="The memory content", min_length=1)
|
|
21
|
+
context: Optional[str] = Field(None, description="Additional context")
|
|
22
|
+
tags: Optional[list[str]] = Field(default_factory=list, description="Tags for categorization")
|
|
23
|
+
type: str = Field("general", description="Memory type")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MemoryCreate(MemoryBase):
|
|
27
|
+
"""Input model for creating a memory."""
|
|
28
|
+
|
|
29
|
+
importance: Optional[int] = Field(
|
|
30
|
+
None, description="Importance score 1-100", ge=1, le=100
|
|
31
|
+
)
|
|
32
|
+
related_activity_id: Optional[str] = Field(None, description="Related activity ID")
|
|
33
|
+
related_memory_ids: Optional[list[str]] = Field(
|
|
34
|
+
default_factory=list, description="Related memory IDs"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@field_validator("tags", mode="before")
|
|
38
|
+
@classmethod
|
|
39
|
+
def parse_tags(cls, v: Any) -> list[str]:
|
|
40
|
+
if v is None:
|
|
41
|
+
return []
|
|
42
|
+
if isinstance(v, str):
|
|
43
|
+
return [t.strip() for t in v.split(",") if t.strip()]
|
|
44
|
+
return list(v)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MemoryUpdate(BaseModel):
|
|
48
|
+
"""Input model for updating a memory."""
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(
|
|
51
|
+
str_strip_whitespace=True,
|
|
52
|
+
validate_assignment=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
content: Optional[str] = Field(None, description="New content")
|
|
56
|
+
context: Optional[str] = Field(None, description="New context")
|
|
57
|
+
tags: Optional[list[str]] = Field(None, description="Replace all tags")
|
|
58
|
+
add_tags: Optional[list[str]] = Field(None, description="Tags to add")
|
|
59
|
+
remove_tags: Optional[list[str]] = Field(None, description="Tags to remove")
|
|
60
|
+
status: Optional[str] = Field(None, description="New status")
|
|
61
|
+
importance: Optional[int] = Field(None, description="New importance", ge=1, le=100)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Memory(MemoryBase):
|
|
65
|
+
"""Full memory model from database."""
|
|
66
|
+
|
|
67
|
+
id: str
|
|
68
|
+
created_at: str
|
|
69
|
+
updated_at: str
|
|
70
|
+
last_accessed: str
|
|
71
|
+
last_verified: Optional[str] = None
|
|
72
|
+
access_count: int = 0
|
|
73
|
+
importance_score: float = 50.0
|
|
74
|
+
manual_importance: Optional[int] = None
|
|
75
|
+
status: str = "fresh"
|
|
76
|
+
source_session_id: Optional[str] = None
|
|
77
|
+
source_agent_id: Optional[str] = None
|
|
78
|
+
source_activity_id: Optional[str] = None
|
|
79
|
+
project_path: Optional[str] = None
|
|
80
|
+
file_context: Optional[list[str]] = None
|
|
81
|
+
has_embedding: bool = False
|
|
82
|
+
metadata: Optional[dict[str, Any]] = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def create_memory(
|
|
86
|
+
conn: sqlite3.Connection,
|
|
87
|
+
data: MemoryCreate,
|
|
88
|
+
project_path: Optional[str] = None,
|
|
89
|
+
session_id: Optional[str] = None,
|
|
90
|
+
agent_id: Optional[str] = None,
|
|
91
|
+
) -> Memory:
|
|
92
|
+
"""Create a new memory in the database.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
conn: Database connection
|
|
96
|
+
data: Memory creation data
|
|
97
|
+
project_path: Current project path
|
|
98
|
+
session_id: Current session ID
|
|
99
|
+
agent_id: Current agent ID
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Created memory object
|
|
103
|
+
"""
|
|
104
|
+
from ..categorization import detect_memory_type, suggest_tags
|
|
105
|
+
|
|
106
|
+
memory_id = generate_memory_id()
|
|
107
|
+
now = now_iso()
|
|
108
|
+
|
|
109
|
+
# Auto-detect type if not specified or is default
|
|
110
|
+
mem_type = data.type
|
|
111
|
+
if mem_type == "general":
|
|
112
|
+
mem_type = detect_memory_type(data.content, data.context)
|
|
113
|
+
|
|
114
|
+
# Auto-suggest tags and merge with provided
|
|
115
|
+
suggested = suggest_tags(data.content, data.context)
|
|
116
|
+
tags = list(set((data.tags or []) + suggested))
|
|
117
|
+
|
|
118
|
+
# Determine importance
|
|
119
|
+
importance = float(data.importance) if data.importance else 50.0
|
|
120
|
+
manual_importance = data.importance
|
|
121
|
+
|
|
122
|
+
cursor = conn.cursor()
|
|
123
|
+
cursor.execute(
|
|
124
|
+
"""
|
|
125
|
+
INSERT INTO memories (
|
|
126
|
+
id, content, type, tags, context,
|
|
127
|
+
created_at, updated_at, last_accessed,
|
|
128
|
+
access_count, importance_score, manual_importance, status,
|
|
129
|
+
source_session_id, source_agent_id, source_activity_id,
|
|
130
|
+
project_path, has_embedding
|
|
131
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
132
|
+
""",
|
|
133
|
+
(
|
|
134
|
+
memory_id,
|
|
135
|
+
data.content,
|
|
136
|
+
mem_type,
|
|
137
|
+
json.dumps(tags),
|
|
138
|
+
data.context,
|
|
139
|
+
now,
|
|
140
|
+
now,
|
|
141
|
+
now,
|
|
142
|
+
0,
|
|
143
|
+
importance,
|
|
144
|
+
manual_importance,
|
|
145
|
+
"fresh",
|
|
146
|
+
session_id,
|
|
147
|
+
agent_id,
|
|
148
|
+
data.related_activity_id,
|
|
149
|
+
project_path,
|
|
150
|
+
0,
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
conn.commit()
|
|
154
|
+
|
|
155
|
+
return Memory(
|
|
156
|
+
id=memory_id,
|
|
157
|
+
content=data.content,
|
|
158
|
+
type=mem_type,
|
|
159
|
+
tags=tags,
|
|
160
|
+
context=data.context,
|
|
161
|
+
created_at=now,
|
|
162
|
+
updated_at=now,
|
|
163
|
+
last_accessed=now,
|
|
164
|
+
access_count=0,
|
|
165
|
+
importance_score=importance,
|
|
166
|
+
manual_importance=manual_importance,
|
|
167
|
+
status="fresh",
|
|
168
|
+
source_session_id=session_id,
|
|
169
|
+
source_activity_id=data.related_activity_id,
|
|
170
|
+
project_path=project_path,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_memory(conn: sqlite3.Connection, memory_id: str) -> Optional[Memory]:
|
|
175
|
+
"""Get a memory by ID.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
conn: Database connection
|
|
179
|
+
memory_id: Memory ID
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Memory object or None if not found
|
|
183
|
+
"""
|
|
184
|
+
cursor = conn.cursor()
|
|
185
|
+
cursor.execute("SELECT * FROM memories WHERE id = ?", (memory_id,))
|
|
186
|
+
row = cursor.fetchone()
|
|
187
|
+
|
|
188
|
+
if not row:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
return _row_to_memory(row)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def update_memory(
|
|
195
|
+
conn: sqlite3.Connection,
|
|
196
|
+
memory_id: str,
|
|
197
|
+
data: MemoryUpdate,
|
|
198
|
+
) -> Optional[Memory]:
|
|
199
|
+
"""Update a memory.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
conn: Database connection
|
|
203
|
+
memory_id: Memory ID
|
|
204
|
+
data: Update data
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Updated memory or None if not found
|
|
208
|
+
"""
|
|
209
|
+
memory = get_memory(conn, memory_id)
|
|
210
|
+
if not memory:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
updates = []
|
|
214
|
+
params = []
|
|
215
|
+
|
|
216
|
+
if data.content is not None:
|
|
217
|
+
updates.append("content = ?")
|
|
218
|
+
params.append(data.content)
|
|
219
|
+
|
|
220
|
+
if data.context is not None:
|
|
221
|
+
updates.append("context = ?")
|
|
222
|
+
params.append(data.context)
|
|
223
|
+
|
|
224
|
+
if data.status is not None:
|
|
225
|
+
updates.append("status = ?")
|
|
226
|
+
params.append(data.status)
|
|
227
|
+
|
|
228
|
+
if data.importance is not None:
|
|
229
|
+
updates.append("manual_importance = ?")
|
|
230
|
+
params.append(data.importance)
|
|
231
|
+
updates.append("importance_score = ?")
|
|
232
|
+
params.append(float(data.importance))
|
|
233
|
+
|
|
234
|
+
# Handle tags
|
|
235
|
+
current_tags = memory.tags or []
|
|
236
|
+
new_tags = current_tags
|
|
237
|
+
|
|
238
|
+
if data.tags is not None:
|
|
239
|
+
new_tags = data.tags
|
|
240
|
+
else:
|
|
241
|
+
if data.add_tags:
|
|
242
|
+
new_tags = list(set(current_tags + data.add_tags))
|
|
243
|
+
if data.remove_tags:
|
|
244
|
+
new_tags = [t for t in new_tags if t not in data.remove_tags]
|
|
245
|
+
|
|
246
|
+
if new_tags != current_tags:
|
|
247
|
+
updates.append("tags = ?")
|
|
248
|
+
params.append(json.dumps(new_tags))
|
|
249
|
+
|
|
250
|
+
if updates:
|
|
251
|
+
updates.append("updated_at = ?")
|
|
252
|
+
params.append(now_iso())
|
|
253
|
+
params.append(memory_id)
|
|
254
|
+
|
|
255
|
+
cursor = conn.cursor()
|
|
256
|
+
cursor.execute(
|
|
257
|
+
f"UPDATE memories SET {', '.join(updates)} WHERE id = ?",
|
|
258
|
+
params,
|
|
259
|
+
)
|
|
260
|
+
conn.commit()
|
|
261
|
+
|
|
262
|
+
return get_memory(conn, memory_id)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def delete_memory(conn: sqlite3.Connection, memory_id: str) -> bool:
|
|
266
|
+
"""Delete a memory.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
conn: Database connection
|
|
270
|
+
memory_id: Memory ID
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
True if deleted, False if not found
|
|
274
|
+
"""
|
|
275
|
+
cursor = conn.cursor()
|
|
276
|
+
cursor.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
|
|
277
|
+
conn.commit()
|
|
278
|
+
return cursor.rowcount > 0
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def list_memories(
|
|
282
|
+
conn: sqlite3.Connection,
|
|
283
|
+
type_filter: Optional[str] = None,
|
|
284
|
+
tags_filter: Optional[list[str]] = None,
|
|
285
|
+
status_filter: Optional[str] = None,
|
|
286
|
+
sort_by: str = "last_accessed",
|
|
287
|
+
sort_order: str = "desc",
|
|
288
|
+
limit: int = 20,
|
|
289
|
+
offset: int = 0,
|
|
290
|
+
) -> tuple[list[Memory], int]:
|
|
291
|
+
"""List memories with filters.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Tuple of (memories list, total count)
|
|
295
|
+
"""
|
|
296
|
+
where_clauses = []
|
|
297
|
+
params: list[Any] = []
|
|
298
|
+
|
|
299
|
+
if type_filter:
|
|
300
|
+
where_clauses.append("type = ?")
|
|
301
|
+
params.append(type_filter)
|
|
302
|
+
|
|
303
|
+
if status_filter:
|
|
304
|
+
where_clauses.append("status = ?")
|
|
305
|
+
params.append(status_filter)
|
|
306
|
+
|
|
307
|
+
if tags_filter:
|
|
308
|
+
# Match any of the tags
|
|
309
|
+
tag_conditions = []
|
|
310
|
+
for tag in tags_filter:
|
|
311
|
+
tag_conditions.append("tags LIKE ?")
|
|
312
|
+
params.append(f'%"{tag}"%')
|
|
313
|
+
where_clauses.append(f"({' OR '.join(tag_conditions)})")
|
|
314
|
+
|
|
315
|
+
where_sql = ""
|
|
316
|
+
if where_clauses:
|
|
317
|
+
where_sql = "WHERE " + " AND ".join(where_clauses)
|
|
318
|
+
|
|
319
|
+
# Validate sort column
|
|
320
|
+
valid_sorts = ["last_accessed", "created_at", "importance_score", "access_count"]
|
|
321
|
+
if sort_by not in valid_sorts:
|
|
322
|
+
sort_by = "last_accessed"
|
|
323
|
+
|
|
324
|
+
order = "DESC" if sort_order.lower() == "desc" else "ASC"
|
|
325
|
+
|
|
326
|
+
# Get total count
|
|
327
|
+
cursor = conn.cursor()
|
|
328
|
+
cursor.execute(f"SELECT COUNT(*) FROM memories {where_sql}", params)
|
|
329
|
+
total = cursor.fetchone()[0]
|
|
330
|
+
|
|
331
|
+
# Get page
|
|
332
|
+
params_page = params + [limit, offset]
|
|
333
|
+
cursor.execute(
|
|
334
|
+
f"""
|
|
335
|
+
SELECT * FROM memories {where_sql}
|
|
336
|
+
ORDER BY {sort_by} {order}
|
|
337
|
+
LIMIT ? OFFSET ?
|
|
338
|
+
""",
|
|
339
|
+
params_page,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
memories = [_row_to_memory(row) for row in cursor.fetchall()]
|
|
343
|
+
return memories, total
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def touch_memory(conn: sqlite3.Connection, memory_id: str) -> None:
|
|
347
|
+
"""Update last_accessed and increment access_count."""
|
|
348
|
+
cursor = conn.cursor()
|
|
349
|
+
cursor.execute(
|
|
350
|
+
"""
|
|
351
|
+
UPDATE memories
|
|
352
|
+
SET last_accessed = ?, access_count = access_count + 1
|
|
353
|
+
WHERE id = ?
|
|
354
|
+
""",
|
|
355
|
+
(now_iso(), memory_id),
|
|
356
|
+
)
|
|
357
|
+
conn.commit()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _row_to_memory(row: sqlite3.Row) -> Memory:
|
|
361
|
+
"""Convert database row to Memory object."""
|
|
362
|
+
tags = row["tags"]
|
|
363
|
+
if tags and isinstance(tags, str):
|
|
364
|
+
tags = json.loads(tags)
|
|
365
|
+
|
|
366
|
+
file_context = row["file_context"]
|
|
367
|
+
if file_context and isinstance(file_context, str):
|
|
368
|
+
file_context = json.loads(file_context)
|
|
369
|
+
|
|
370
|
+
metadata = row["metadata"]
|
|
371
|
+
if metadata and isinstance(metadata, str):
|
|
372
|
+
metadata = json.loads(metadata)
|
|
373
|
+
|
|
374
|
+
return Memory(
|
|
375
|
+
id=row["id"],
|
|
376
|
+
content=row["content"],
|
|
377
|
+
type=row["type"],
|
|
378
|
+
tags=tags or [],
|
|
379
|
+
context=row["context"],
|
|
380
|
+
created_at=row["created_at"],
|
|
381
|
+
updated_at=row["updated_at"],
|
|
382
|
+
last_accessed=row["last_accessed"],
|
|
383
|
+
last_verified=row["last_verified"],
|
|
384
|
+
access_count=row["access_count"],
|
|
385
|
+
importance_score=row["importance_score"],
|
|
386
|
+
manual_importance=row["manual_importance"],
|
|
387
|
+
status=row["status"],
|
|
388
|
+
source_session_id=row["source_session_id"],
|
|
389
|
+
source_agent_id=row["source_agent_id"],
|
|
390
|
+
source_activity_id=row["source_activity_id"],
|
|
391
|
+
project_path=row["project_path"],
|
|
392
|
+
file_context=file_context,
|
|
393
|
+
has_embedding=bool(row["has_embedding"]),
|
|
394
|
+
metadata=metadata,
|
|
395
|
+
)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Memory relationship model and operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from typing import Optional, Any
|
|
6
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
7
|
+
|
|
8
|
+
from ..utils.ids import generate_relationship_id
|
|
9
|
+
from ..utils.timestamps import now_iso
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MemoryRelationship(BaseModel):
|
|
13
|
+
"""Memory relationship model."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(
|
|
16
|
+
str_strip_whitespace=True,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
id: str
|
|
20
|
+
source_memory_id: str
|
|
21
|
+
target_memory_id: str
|
|
22
|
+
relationship_type: str # related_to, supersedes, derived_from, contradicts
|
|
23
|
+
strength: float = 1.0
|
|
24
|
+
created_at: str
|
|
25
|
+
metadata: Optional[dict[str, Any]] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LinkMemoriesInput(BaseModel):
|
|
29
|
+
"""Input for linking two memories."""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(
|
|
32
|
+
str_strip_whitespace=True,
|
|
33
|
+
validate_assignment=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
source_id: str = Field(..., description="Source memory ID")
|
|
37
|
+
target_id: str = Field(..., description="Target memory ID")
|
|
38
|
+
relationship_type: str = Field(
|
|
39
|
+
...,
|
|
40
|
+
description="Relationship type: related_to, supersedes, derived_from, contradicts",
|
|
41
|
+
)
|
|
42
|
+
strength: float = Field(1.0, description="Relationship strength 0.0-1.0", ge=0.0, le=1.0)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
VALID_RELATIONSHIP_TYPES = ["related_to", "supersedes", "derived_from", "contradicts"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_relationship(
|
|
49
|
+
conn: sqlite3.Connection,
|
|
50
|
+
source_id: str,
|
|
51
|
+
target_id: str,
|
|
52
|
+
relationship_type: str,
|
|
53
|
+
strength: float = 1.0,
|
|
54
|
+
) -> Optional[MemoryRelationship]:
|
|
55
|
+
"""Create a relationship between two memories.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
conn: Database connection
|
|
59
|
+
source_id: Source memory ID
|
|
60
|
+
target_id: Target memory ID
|
|
61
|
+
relationship_type: Type of relationship
|
|
62
|
+
strength: Relationship strength
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Created relationship or None if memories don't exist
|
|
66
|
+
"""
|
|
67
|
+
if relationship_type not in VALID_RELATIONSHIP_TYPES:
|
|
68
|
+
raise ValueError(f"Invalid relationship type: {relationship_type}")
|
|
69
|
+
|
|
70
|
+
# Verify both memories exist
|
|
71
|
+
cursor = conn.cursor()
|
|
72
|
+
cursor.execute("SELECT id FROM memories WHERE id IN (?, ?)", (source_id, target_id))
|
|
73
|
+
found = [row[0] for row in cursor.fetchall()]
|
|
74
|
+
if len(found) != 2:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
rel_id = generate_relationship_id()
|
|
78
|
+
now = now_iso()
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
cursor.execute(
|
|
82
|
+
"""
|
|
83
|
+
INSERT INTO memory_relationships (
|
|
84
|
+
id, source_memory_id, target_memory_id,
|
|
85
|
+
relationship_type, strength, created_at
|
|
86
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
87
|
+
""",
|
|
88
|
+
(rel_id, source_id, target_id, relationship_type, strength, now),
|
|
89
|
+
)
|
|
90
|
+
conn.commit()
|
|
91
|
+
except sqlite3.IntegrityError:
|
|
92
|
+
# Relationship already exists
|
|
93
|
+
cursor.execute(
|
|
94
|
+
"""
|
|
95
|
+
SELECT * FROM memory_relationships
|
|
96
|
+
WHERE source_memory_id = ? AND target_memory_id = ? AND relationship_type = ?
|
|
97
|
+
""",
|
|
98
|
+
(source_id, target_id, relationship_type),
|
|
99
|
+
)
|
|
100
|
+
row = cursor.fetchone()
|
|
101
|
+
if row:
|
|
102
|
+
return _row_to_relationship(row)
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
return MemoryRelationship(
|
|
106
|
+
id=rel_id,
|
|
107
|
+
source_memory_id=source_id,
|
|
108
|
+
target_memory_id=target_id,
|
|
109
|
+
relationship_type=relationship_type,
|
|
110
|
+
strength=strength,
|
|
111
|
+
created_at=now,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_relationships(
|
|
116
|
+
conn: sqlite3.Connection,
|
|
117
|
+
memory_id: str,
|
|
118
|
+
as_source: bool = True,
|
|
119
|
+
as_target: bool = True,
|
|
120
|
+
) -> list[MemoryRelationship]:
|
|
121
|
+
"""Get all relationships for a memory.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
conn: Database connection
|
|
125
|
+
memory_id: Memory ID
|
|
126
|
+
as_source: Include relationships where memory is source
|
|
127
|
+
as_target: Include relationships where memory is target
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
List of relationships
|
|
131
|
+
"""
|
|
132
|
+
cursor = conn.cursor()
|
|
133
|
+
relationships = []
|
|
134
|
+
|
|
135
|
+
if as_source:
|
|
136
|
+
cursor.execute(
|
|
137
|
+
"SELECT * FROM memory_relationships WHERE source_memory_id = ?",
|
|
138
|
+
(memory_id,),
|
|
139
|
+
)
|
|
140
|
+
relationships.extend([_row_to_relationship(row) for row in cursor.fetchall()])
|
|
141
|
+
|
|
142
|
+
if as_target:
|
|
143
|
+
cursor.execute(
|
|
144
|
+
"SELECT * FROM memory_relationships WHERE target_memory_id = ?",
|
|
145
|
+
(memory_id,),
|
|
146
|
+
)
|
|
147
|
+
relationships.extend([_row_to_relationship(row) for row in cursor.fetchall()])
|
|
148
|
+
|
|
149
|
+
return relationships
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def delete_relationship(
|
|
153
|
+
conn: sqlite3.Connection,
|
|
154
|
+
source_id: str,
|
|
155
|
+
target_id: str,
|
|
156
|
+
relationship_type: Optional[str] = None,
|
|
157
|
+
) -> int:
|
|
158
|
+
"""Delete relationships between memories.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
conn: Database connection
|
|
162
|
+
source_id: Source memory ID
|
|
163
|
+
target_id: Target memory ID
|
|
164
|
+
relationship_type: Optional type filter
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Number of relationships deleted
|
|
168
|
+
"""
|
|
169
|
+
cursor = conn.cursor()
|
|
170
|
+
|
|
171
|
+
if relationship_type:
|
|
172
|
+
cursor.execute(
|
|
173
|
+
"""
|
|
174
|
+
DELETE FROM memory_relationships
|
|
175
|
+
WHERE source_memory_id = ? AND target_memory_id = ? AND relationship_type = ?
|
|
176
|
+
""",
|
|
177
|
+
(source_id, target_id, relationship_type),
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
cursor.execute(
|
|
181
|
+
"""
|
|
182
|
+
DELETE FROM memory_relationships
|
|
183
|
+
WHERE source_memory_id = ? AND target_memory_id = ?
|
|
184
|
+
""",
|
|
185
|
+
(source_id, target_id),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
conn.commit()
|
|
189
|
+
return cursor.rowcount
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _row_to_relationship(row: sqlite3.Row) -> MemoryRelationship:
|
|
193
|
+
"""Convert database row to MemoryRelationship object."""
|
|
194
|
+
metadata = row["metadata"]
|
|
195
|
+
if metadata and isinstance(metadata, str):
|
|
196
|
+
metadata = json.loads(metadata)
|
|
197
|
+
|
|
198
|
+
return MemoryRelationship(
|
|
199
|
+
id=row["id"],
|
|
200
|
+
source_memory_id=row["source_memory_id"],
|
|
201
|
+
target_memory_id=row["target_memory_id"],
|
|
202
|
+
relationship_type=row["relationship_type"],
|
|
203
|
+
strength=row["strength"],
|
|
204
|
+
created_at=row["created_at"],
|
|
205
|
+
metadata=metadata,
|
|
206
|
+
)
|