concierge-graph 3.8.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.
- agents/__init__.py +14 -0
- agents/revisor_critico.py +610 -0
- concierge_graph-3.8.2.dist-info/METADATA +327 -0
- concierge_graph-3.8.2.dist-info/RECORD +37 -0
- concierge_graph-3.8.2.dist-info/WHEEL +5 -0
- concierge_graph-3.8.2.dist-info/entry_points.txt +3 -0
- concierge_graph-3.8.2.dist-info/licenses/LICENSE +21 -0
- concierge_graph-3.8.2.dist-info/top_level.txt +8 -0
- core/__init__.py +23 -0
- core/config.py +192 -0
- core/hybrid_search.py +288 -0
- core/memory_extractor.py +245 -0
- core/middleware.py +723 -0
- core/probabilistic_retriever.py +99 -0
- core/project_index.py +316 -0
- core/vector_backend.py +471 -0
- ingestion/__init__.py +25 -0
- ingestion/crawler.py +722 -0
- ingestion/orchestrator.py +920 -0
- ingestion/parser.py +984 -0
- ingestion/summarizer.py +948 -0
- interface/__init__.py +16 -0
- interface/action_hooks.py +261 -0
- interface/cli.py +391 -0
- interface/mcp_server.py +1737 -0
- main.py +281 -0
- scripts/bootstrap_core_memory.py +93 -0
- services/__init__.py +13 -0
- services/janitor.py +762 -0
- storage/__init__.py +31 -0
- storage/base_backend.py +241 -0
- storage/connection.py +310 -0
- storage/logic.py +703 -0
- storage/schema.py +390 -0
- storage/semantic_logic.py +125 -0
- storage/store.py +802 -0
- storage/vector_store.py +759 -0
storage/store.py
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
"""
|
|
2
|
+
storage/store.py - Grafo Concierge v3.8.0 (Absolute Solidity)
|
|
3
|
+
|
|
4
|
+
Unified Facade that composes:
|
|
5
|
+
- ConnectionManager (connection.py) -> WAL, busy_timeout, serialized queue
|
|
6
|
+
- SchemaManager (schema.py) -> DDL, CHECK constraints, FTS5 triggers
|
|
7
|
+
- GraphLogic (logic.py) -> decay, centrality, recency, FTS5
|
|
8
|
+
|
|
9
|
+
API consistent with MCP v3.8 Tools:
|
|
10
|
+
concierge_resume -> get_project / get_project_stats
|
|
11
|
+
concierge_commit -> create_commit + touch_node_commit
|
|
12
|
+
concierge_search -> fts_search / hybrid_search_score_batch
|
|
13
|
+
concierge_mine -> create_node / find_node_by_hash
|
|
14
|
+
concierge_register -> create_project
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import sqlite3
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Callable, Optional, TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
from storage.connection import ConnectionManager
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from core.config import ConciergeConfig
|
|
30
|
+
from storage.schema import SchemaManager, VALID_NODE_TYPES, VALID_PRIVACY_LEVELS, VALID_STATUSES
|
|
31
|
+
from storage.logic import GraphLogic, TrajectoryNotFoundError, InvalidTransitionError
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger("grafo-concierge.store")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Facade exceptions
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
class ProjectNotFoundError(Exception):
|
|
41
|
+
"""Project not found by UUID or folder_name."""
|
|
42
|
+
|
|
43
|
+
class NodeNotFoundError(Exception):
|
|
44
|
+
"""Node not found by the provided ID."""
|
|
45
|
+
|
|
46
|
+
class CommitValidationError(Exception):
|
|
47
|
+
"""Required fields missing in the commit."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# SqliteStore — Unified Facade
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
class SqliteStore:
|
|
55
|
+
"""SQLite persistence facade for Grafo Concierge v3.8.
|
|
56
|
+
|
|
57
|
+
In __init__, the schema is verified and applied automatically.
|
|
58
|
+
The end user only interacts with this class.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
db_path: Path to the .db (default: <project_root>/data/concierge.db).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, db_path: str | None = None, config: Optional["ConciergeConfig"] = None) -> None:
|
|
65
|
+
if db_path is None:
|
|
66
|
+
db_path = str(Path(__file__).parent.parent.resolve() / "data" / "concierge.db")
|
|
67
|
+
resolved = str(Path(db_path).expanduser().absolute())
|
|
68
|
+
Path(resolved).parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
# 1. Schema FIRST (applies DDL + FTS5 + triggers before any persistent connection)
|
|
71
|
+
# The temporary connection is opened and closed here, without competing with the queue.
|
|
72
|
+
self._boot_schema(resolved)
|
|
73
|
+
|
|
74
|
+
# 2. Connections (WAL + busy_timeout + serialized queue) - starts AFTER schema is ready
|
|
75
|
+
self._conn_mgr = ConnectionManager(resolved)
|
|
76
|
+
self._conn_mgr.start()
|
|
77
|
+
|
|
78
|
+
# 3. Intelligence (centrality, recency, decay, FTS5, CTE)
|
|
79
|
+
# Now passes ConciergeConfig so that weights obey user config
|
|
80
|
+
self._logic = GraphLogic(self._conn_mgr, config=config)
|
|
81
|
+
|
|
82
|
+
logger.info("SqliteStore initialized: %s", resolved)
|
|
83
|
+
|
|
84
|
+
def _boot_schema(self, db_path: str) -> None:
|
|
85
|
+
"""Opens temporary connection to apply the schema idempotently."""
|
|
86
|
+
conn = sqlite3.connect(db_path)
|
|
87
|
+
try:
|
|
88
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
89
|
+
conn.execute("PRAGMA busy_timeout=5000;")
|
|
90
|
+
conn.execute("PRAGMA foreign_keys=ON;")
|
|
91
|
+
mgr = SchemaManager(conn)
|
|
92
|
+
mgr.apply_full_schema()
|
|
93
|
+
|
|
94
|
+
# Verification log
|
|
95
|
+
tables = mgr.verify_tables_exist()
|
|
96
|
+
missing = [t for t, exists in tables.items() if not exists]
|
|
97
|
+
if missing:
|
|
98
|
+
logger.error("Missing tables after boot: %s", missing)
|
|
99
|
+
raise RuntimeError(f"Incomplete schema: {missing}")
|
|
100
|
+
|
|
101
|
+
triggers = mgr.verify_triggers_exist()
|
|
102
|
+
missing_t = [t for t, exists in triggers.items() if not exists]
|
|
103
|
+
if missing_t:
|
|
104
|
+
logger.error("Missing FTS5 triggers: %s", missing_t)
|
|
105
|
+
raise RuntimeError(f"Missing triggers: {missing_t}")
|
|
106
|
+
|
|
107
|
+
logger.info("Schema v%s verified - all tables and triggers OK.", SchemaManager.SCHEMA_VERSION)
|
|
108
|
+
finally:
|
|
109
|
+
conn.close()
|
|
110
|
+
|
|
111
|
+
def close(self) -> None:
|
|
112
|
+
"""Shuts down the write queue and read connections."""
|
|
113
|
+
self._conn_mgr.close()
|
|
114
|
+
logger.info("SqliteStore closed.")
|
|
115
|
+
|
|
116
|
+
def write_callback(self, fn: Callable, *args: Any, **kwargs: Any) -> Any:
|
|
117
|
+
"""Delegates a write operation to the ConnectionManager's serialized queue.
|
|
118
|
+
|
|
119
|
+
This method protects the encapsulation of the ConnectionManager and its
|
|
120
|
+
internal write queue.
|
|
121
|
+
"""
|
|
122
|
+
return self._conn_mgr.write(fn, *args, **kwargs)
|
|
123
|
+
|
|
124
|
+
# ===================================================================
|
|
125
|
+
# PROJECTS
|
|
126
|
+
# ===================================================================
|
|
127
|
+
|
|
128
|
+
def create_project(
|
|
129
|
+
self, uuid: str, folder_name: str, primary_wing: str = "geral",
|
|
130
|
+
privacy_level: str = "PUBLIC", summary: Optional[str] = None,
|
|
131
|
+
) -> dict:
|
|
132
|
+
"""Registers a new project (aligned with concierge_register)."""
|
|
133
|
+
if privacy_level not in VALID_PRIVACY_LEVELS:
|
|
134
|
+
raise ValueError(f"Invalid privacy_level: '{privacy_level}'. Accepted: {sorted(VALID_PRIVACY_LEVELS)}")
|
|
135
|
+
|
|
136
|
+
def _do(conn, u, fn, pw, pl, s):
|
|
137
|
+
conn.execute(
|
|
138
|
+
"INSERT INTO projects (uuid, folder_name, primary_wing, privacy_level, summary) VALUES (?,?,?,?,?)",
|
|
139
|
+
(u, fn, pw, pl, s))
|
|
140
|
+
return {"uuid": u, "folder_name": fn, "primary_wing": pw, "privacy_level": pl}
|
|
141
|
+
|
|
142
|
+
return self._conn_mgr.write(_do, uuid, folder_name, primary_wing, privacy_level, summary)
|
|
143
|
+
|
|
144
|
+
def get_project(self, project_id: str) -> dict:
|
|
145
|
+
"""Retrieves a project by UUID or folder_name."""
|
|
146
|
+
with self._conn_mgr.read() as conn:
|
|
147
|
+
row = conn.execute(
|
|
148
|
+
"SELECT * FROM projects WHERE uuid = ? OR folder_name = ?",
|
|
149
|
+
(project_id, project_id)).fetchone()
|
|
150
|
+
if not row:
|
|
151
|
+
raise ProjectNotFoundError(f"Project not found: {project_id}")
|
|
152
|
+
return dict(row)
|
|
153
|
+
|
|
154
|
+
def update_project(self, uuid: str, **fields: Any) -> None:
|
|
155
|
+
"""Updates allowed fields of a project."""
|
|
156
|
+
allowed = {"folder_name", "primary_wing", "privacy_level", "summary"}
|
|
157
|
+
updates = {k: v for k, v in fields.items() if k in allowed}
|
|
158
|
+
if not updates:
|
|
159
|
+
return
|
|
160
|
+
if "privacy_level" in updates and updates["privacy_level"] not in VALID_PRIVACY_LEVELS:
|
|
161
|
+
raise ValueError(f"Invalid privacy_level: {updates['privacy_level']}")
|
|
162
|
+
updates["updated_at"] = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
|
163
|
+
set_clause = ", ".join(f"{k} = ?" for k in updates)
|
|
164
|
+
vals = list(updates.values()) + [uuid]
|
|
165
|
+
|
|
166
|
+
def _do(conn, sc, v):
|
|
167
|
+
conn.execute(f"UPDATE projects SET {sc} WHERE uuid = ?", v)
|
|
168
|
+
self._conn_mgr.write(_do, set_clause, vals)
|
|
169
|
+
|
|
170
|
+
def delete_project(self, uuid: str) -> None:
|
|
171
|
+
"""Removes a project and all cascaded data."""
|
|
172
|
+
def _do(conn, u):
|
|
173
|
+
conn.execute("DELETE FROM projects WHERE uuid = ?", (u,))
|
|
174
|
+
self._conn_mgr.write(_do, uuid)
|
|
175
|
+
|
|
176
|
+
def list_projects(self) -> list[dict]:
|
|
177
|
+
"""Lists all projects sorted by updated_at DESC."""
|
|
178
|
+
with self._conn_mgr.read() as conn:
|
|
179
|
+
rows = conn.execute("SELECT * FROM projects ORDER BY updated_at DESC").fetchall()
|
|
180
|
+
return [dict(r) for r in rows]
|
|
181
|
+
|
|
182
|
+
# ===================================================================
|
|
183
|
+
# NODES
|
|
184
|
+
# ===================================================================
|
|
185
|
+
|
|
186
|
+
def create_node(
|
|
187
|
+
self, project_uuid: str, label: str, summary: Optional[str] = None,
|
|
188
|
+
node_type: str = "FACT", type_: str = "file",
|
|
189
|
+
tags: Optional[list[str]] = None, file_hash: Optional[str] = None,
|
|
190
|
+
status: str = "ACTIVE", content: Optional[str] = None,
|
|
191
|
+
valid_from_commit: Optional[str] = None,
|
|
192
|
+
valid_to_commit: Optional[str] = None,
|
|
193
|
+
) -> int:
|
|
194
|
+
"""Creates a node in the graph (aligned with concierge_mine).
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
valid_from_commit: SHA of the commit where the node becomes valid (optional).
|
|
198
|
+
valid_to_commit: SHA of the commit where the node ceases to be valid (optional).
|
|
199
|
+
"""
|
|
200
|
+
if node_type not in VALID_NODE_TYPES:
|
|
201
|
+
raise ValueError(f"Invalid node_type: '{node_type}'. Accepted: {sorted(VALID_NODE_TYPES)}")
|
|
202
|
+
if status not in VALID_STATUSES:
|
|
203
|
+
raise ValueError(f"Invalid status: '{status}'. Accepted: {sorted(VALID_STATUSES)}")
|
|
204
|
+
|
|
205
|
+
tags_json = json.dumps(tags, ensure_ascii=False) if tags else None
|
|
206
|
+
|
|
207
|
+
def _do(conn, pu, lb, sm, nt, tp, tg, fh, st, ct, vfc, vtc):
|
|
208
|
+
cur = conn.execute(
|
|
209
|
+
"""INSERT INTO nodes
|
|
210
|
+
(project_uuid, label, summary, node_type, type, tags, file_hash, status, content,
|
|
211
|
+
valid_from_commit, valid_to_commit)
|
|
212
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
|
213
|
+
(pu, lb, sm, nt, tp, tg, fh, st, ct, vfc, vtc))
|
|
214
|
+
return cur.lastrowid
|
|
215
|
+
return self._conn_mgr.write(
|
|
216
|
+
_do, project_uuid, label, summary, node_type, type_, tags_json,
|
|
217
|
+
file_hash, status, content, valid_from_commit, valid_to_commit,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def get_node(self, node_id: int) -> dict:
|
|
221
|
+
"""Returns a node by ID."""
|
|
222
|
+
with self._conn_mgr.read() as conn:
|
|
223
|
+
row = conn.execute("SELECT * FROM nodes WHERE id = ?", (node_id,)).fetchone()
|
|
224
|
+
if not row:
|
|
225
|
+
raise NodeNotFoundError(f"Node not found: {node_id}")
|
|
226
|
+
result = dict(row)
|
|
227
|
+
if result.get("tags"):
|
|
228
|
+
try:
|
|
229
|
+
result["tags"] = json.loads(result["tags"])
|
|
230
|
+
except (json.JSONDecodeError, TypeError):
|
|
231
|
+
pass
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
def get_nodes_by_project(
|
|
235
|
+
self, project_uuid: str, node_type: Optional[str] = None, status: Optional[str] = None,
|
|
236
|
+
) -> list[dict]:
|
|
237
|
+
"""Lists nodes of a project with optional filters."""
|
|
238
|
+
sql = "SELECT * FROM nodes WHERE project_uuid = ?"
|
|
239
|
+
params: list[Any] = [project_uuid]
|
|
240
|
+
if node_type:
|
|
241
|
+
sql += " AND node_type = ?"
|
|
242
|
+
params.append(node_type)
|
|
243
|
+
if status:
|
|
244
|
+
sql += " AND status = ?"
|
|
245
|
+
params.append(status)
|
|
246
|
+
with self._conn_mgr.read() as conn:
|
|
247
|
+
rows = conn.execute(sql, params).fetchall()
|
|
248
|
+
results = []
|
|
249
|
+
for r in rows:
|
|
250
|
+
d = dict(r)
|
|
251
|
+
if d.get("tags"):
|
|
252
|
+
try:
|
|
253
|
+
d["tags"] = json.loads(d["tags"])
|
|
254
|
+
except (json.JSONDecodeError, TypeError):
|
|
255
|
+
pass
|
|
256
|
+
results.append(d)
|
|
257
|
+
return results
|
|
258
|
+
|
|
259
|
+
def get_lightweight_topology(self, project_uuid: Optional[str] = None) -> dict[str, list[dict]]:
|
|
260
|
+
"""Returns the complete and lightweight topology (nodes and edges) from the database.
|
|
261
|
+
|
|
262
|
+
Avoids loading text summaries (summary) or embeddings for bandwidth optimization.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
project_uuid: Optional UUID to filter the nodes and edges of that project.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Dict with structure:
|
|
269
|
+
{
|
|
270
|
+
"nodes": [{"node_id": int, "name": str, "node_type": str}],
|
|
271
|
+
"edges": [{"source": int, "target": int, "edge_type": str}]
|
|
272
|
+
}
|
|
273
|
+
"""
|
|
274
|
+
if project_uuid:
|
|
275
|
+
nodes_sql = "SELECT id AS node_id, label AS name, node_type FROM nodes WHERE project_uuid = ? AND status = 'ACTIVE'"
|
|
276
|
+
nodes_params = [project_uuid]
|
|
277
|
+
|
|
278
|
+
edges_sql = """
|
|
279
|
+
SELECT e.source_id AS source, e.target_id AS target, e.relation_type AS edge_type
|
|
280
|
+
FROM edges e
|
|
281
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
282
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
283
|
+
WHERE n1.project_uuid = ? AND n2.project_uuid = ?
|
|
284
|
+
AND n1.status = 'ACTIVE' AND n2.status = 'ACTIVE'
|
|
285
|
+
"""
|
|
286
|
+
edges_params = [project_uuid, project_uuid]
|
|
287
|
+
else:
|
|
288
|
+
nodes_sql = "SELECT id AS node_id, label AS name, node_type FROM nodes WHERE status = 'ACTIVE'"
|
|
289
|
+
nodes_params = []
|
|
290
|
+
|
|
291
|
+
edges_sql = "SELECT source_id AS source, target_id AS target, relation_type AS edge_type FROM edges"
|
|
292
|
+
edges_params = []
|
|
293
|
+
|
|
294
|
+
with self._conn_mgr.read() as conn:
|
|
295
|
+
node_rows = conn.execute(nodes_sql, nodes_params).fetchall()
|
|
296
|
+
edge_rows = conn.execute(edges_sql, edges_params).fetchall()
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
"nodes": [dict(r) for r in node_rows],
|
|
300
|
+
"edges": [dict(r) for r in edge_rows]
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def update_node(self, node_id: int, **fields: Any) -> None:
|
|
305
|
+
"""Updates allowed fields of a node (includes temporal fields)."""
|
|
306
|
+
allowed = {"label", "summary", "node_type", "type", "tags", "file_hash",
|
|
307
|
+
"last_accessed", "last_commit_at", "status",
|
|
308
|
+
"valid_from_commit", "valid_to_commit"}
|
|
309
|
+
updates = {k: v for k, v in fields.items() if k in allowed}
|
|
310
|
+
if not updates:
|
|
311
|
+
return
|
|
312
|
+
if "node_type" in updates and updates["node_type"] not in VALID_NODE_TYPES:
|
|
313
|
+
raise ValueError(f"Invalid node_type: {updates['node_type']}")
|
|
314
|
+
if "status" in updates and updates["status"] not in VALID_STATUSES:
|
|
315
|
+
raise ValueError(f"Invalid status: {updates['status']}")
|
|
316
|
+
if "tags" in updates and isinstance(updates["tags"], list):
|
|
317
|
+
updates["tags"] = json.dumps(updates["tags"], ensure_ascii=False)
|
|
318
|
+
set_clause = ", ".join(f"{k} = ?" for k in updates)
|
|
319
|
+
vals = list(updates.values()) + [node_id]
|
|
320
|
+
|
|
321
|
+
def _do(conn, sc, v):
|
|
322
|
+
conn.execute(f"UPDATE nodes SET {sc} WHERE id = ?", v)
|
|
323
|
+
self._conn_mgr.write(_do, set_clause, vals)
|
|
324
|
+
|
|
325
|
+
def delete_node(self, node_id: int) -> None:
|
|
326
|
+
"""Removes a node and its edges (CASCADE)."""
|
|
327
|
+
def _do(conn, nid):
|
|
328
|
+
conn.execute("DELETE FROM nodes WHERE id = ?", (nid,))
|
|
329
|
+
self._conn_mgr.write(_do, node_id)
|
|
330
|
+
|
|
331
|
+
def find_node_by_hash(self, project_uuid: str, file_hash: str) -> Optional[dict]:
|
|
332
|
+
"""Searches node by SHA256 hash (delta update check in concierge_mine)."""
|
|
333
|
+
with self._conn_mgr.read() as conn:
|
|
334
|
+
row = conn.execute(
|
|
335
|
+
"SELECT * FROM nodes WHERE project_uuid = ? AND file_hash = ?",
|
|
336
|
+
(project_uuid, file_hash)).fetchone()
|
|
337
|
+
return dict(row) if row else None
|
|
338
|
+
|
|
339
|
+
def touch_node_commit(self, node_id: int) -> None:
|
|
340
|
+
"""Updates last_commit_at to now (used in commit_memory)."""
|
|
341
|
+
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
|
342
|
+
self.update_node(node_id, last_commit_at=now)
|
|
343
|
+
|
|
344
|
+
def update_nodes_file_hash_bulk(self, updates: list[tuple[int, str]]) -> int:
|
|
345
|
+
"""Updates file_hash of multiple nodes in a single transaction.
|
|
346
|
+
|
|
347
|
+
Used by Delta Cache to bind cached nodes to the new file hash,
|
|
348
|
+
preventing Garbage Collection from marking them as orphans.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
updates: List of tuples (node_id, new_file_hash).
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Number of successfully updated nodes.
|
|
355
|
+
"""
|
|
356
|
+
if not updates:
|
|
357
|
+
return 0
|
|
358
|
+
|
|
359
|
+
def _do(conn, upd):
|
|
360
|
+
for node_id, new_hash in upd:
|
|
361
|
+
conn.execute(
|
|
362
|
+
"UPDATE nodes SET file_hash = ? WHERE id = ?",
|
|
363
|
+
(new_hash, node_id),
|
|
364
|
+
)
|
|
365
|
+
self._conn_mgr.write(_do, updates)
|
|
366
|
+
return len(updates)
|
|
367
|
+
|
|
368
|
+
def cleanup_obsolete_nodes(self, project_uuid: str, relative_path: str, current_file_hash: str) -> None:
|
|
369
|
+
"""Removes orphan nodes from a modified file (chunks that ceased to exist).
|
|
370
|
+
|
|
371
|
+
When modifying a file, unchanged chunks are cached and new ones are
|
|
372
|
+
inserted (both with the new file_hash). Old chunks of the same file
|
|
373
|
+
that were not reused keep the old hash and are removed here.
|
|
374
|
+
"""
|
|
375
|
+
def _do(conn):
|
|
376
|
+
prefix = f"{relative_path}::"
|
|
377
|
+
conn.execute(
|
|
378
|
+
"DELETE FROM nodes WHERE project_uuid = ? AND (label = ? OR label LIKE ?) AND (file_hash IS NULL OR file_hash != ?)",
|
|
379
|
+
(project_uuid, relative_path, prefix + "%", current_file_hash)
|
|
380
|
+
)
|
|
381
|
+
self._conn_mgr.write(_do)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def create_nodes_and_edges_bulk(
|
|
385
|
+
self,
|
|
386
|
+
nodes_to_create: list[dict],
|
|
387
|
+
edges_to_create: list[dict]
|
|
388
|
+
) -> list[int]:
|
|
389
|
+
"""Creates multiple nodes and edges in a single SQLite transaction (WAL-friendly).
|
|
390
|
+
|
|
391
|
+
Supports temporal fields and confidence_tag in each node/edge dict.
|
|
392
|
+
"""
|
|
393
|
+
def _do(conn) -> list[int]:
|
|
394
|
+
node_ids = []
|
|
395
|
+
# Insert nodes
|
|
396
|
+
for n in nodes_to_create:
|
|
397
|
+
if n.get("node_type", "FACT") not in VALID_NODE_TYPES:
|
|
398
|
+
raise ValueError(f"Invalid node_type in bulk: {n.get('node_type')}")
|
|
399
|
+
if n.get("status", "ACTIVE") not in VALID_STATUSES:
|
|
400
|
+
raise ValueError(f"Invalid status in bulk: {n.get('status')}")
|
|
401
|
+
|
|
402
|
+
tags_json = json.dumps(n.get("tags"), ensure_ascii=False) if n.get("tags") else None
|
|
403
|
+
cur = conn.execute(
|
|
404
|
+
"""INSERT INTO nodes
|
|
405
|
+
(project_uuid, label, summary, content, node_type, type, tags, file_hash, status,
|
|
406
|
+
valid_from_commit, valid_to_commit)
|
|
407
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
|
408
|
+
(n["project_uuid"], n["label"], n.get("summary"), n.get("content"),
|
|
409
|
+
n.get("node_type", "FACT"), n.get("type", "file"), tags_json,
|
|
410
|
+
n.get("file_hash"), n.get("status", "ACTIVE"),
|
|
411
|
+
n.get("valid_from_commit"), n.get("valid_to_commit"))
|
|
412
|
+
)
|
|
413
|
+
node_ids.append(cur.lastrowid)
|
|
414
|
+
|
|
415
|
+
# Insert edges
|
|
416
|
+
for e in edges_to_create:
|
|
417
|
+
src_id = e["source_id"]
|
|
418
|
+
tgt_id = e["target_id"]
|
|
419
|
+
|
|
420
|
+
# Resolution of index references if passed as idx_0, idx_1 etc.
|
|
421
|
+
if isinstance(src_id, str) and src_id.startswith("idx_"):
|
|
422
|
+
idx = int(src_id.split("_")[1])
|
|
423
|
+
src_id = node_ids[idx]
|
|
424
|
+
if isinstance(tgt_id, str) and tgt_id.startswith("idx_"):
|
|
425
|
+
idx = int(tgt_id.split("_")[1])
|
|
426
|
+
tgt_id = node_ids[idx]
|
|
427
|
+
|
|
428
|
+
conn.execute(
|
|
429
|
+
"""INSERT OR REPLACE INTO edges
|
|
430
|
+
(source_id, target_id, relation_type, weight,
|
|
431
|
+
valid_from_commit, valid_to_commit, confidence_tag)
|
|
432
|
+
VALUES (?,?,?,?,?,?,?)""",
|
|
433
|
+
(src_id, tgt_id, e.get("relation_type", "depends_on"), e.get("weight", 1.0),
|
|
434
|
+
e.get("valid_from_commit"), e.get("valid_to_commit"),
|
|
435
|
+
e.get("confidence_tag", "EXTRACTED"))
|
|
436
|
+
)
|
|
437
|
+
return node_ids
|
|
438
|
+
|
|
439
|
+
return self._conn_mgr.write(_do)
|
|
440
|
+
|
|
441
|
+
# ===================================================================
|
|
442
|
+
# EDGES
|
|
443
|
+
# ===================================================================
|
|
444
|
+
|
|
445
|
+
def create_edge(
|
|
446
|
+
self, source_id: int, target_id: int,
|
|
447
|
+
relation_type: str = "depends_on", weight: float = 1.0,
|
|
448
|
+
valid_from_commit: Optional[str] = None,
|
|
449
|
+
valid_to_commit: Optional[str] = None,
|
|
450
|
+
confidence_tag: str = "EXTRACTED",
|
|
451
|
+
) -> None:
|
|
452
|
+
"""Creates or updates an edge between two nodes.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
valid_from_commit: Commit SHA in which the edge becomes valid.
|
|
456
|
+
valid_to_commit: Commit SHA in which the edge ceases to be valid.
|
|
457
|
+
confidence_tag: Confidence degree of the relation ('EXTRACTED'|'INFERRED'|'AMBIGUOUS').
|
|
458
|
+
"""
|
|
459
|
+
def _do(conn, s, t, r, w, vfc, vtc, ct):
|
|
460
|
+
conn.execute(
|
|
461
|
+
"""INSERT OR REPLACE INTO edges
|
|
462
|
+
(source_id, target_id, relation_type, weight,
|
|
463
|
+
valid_from_commit, valid_to_commit, confidence_tag)
|
|
464
|
+
VALUES (?,?,?,?,?,?,?)""",
|
|
465
|
+
(s, t, r, w, vfc, vtc, ct))
|
|
466
|
+
self._conn_mgr.write(
|
|
467
|
+
_do, source_id, target_id, relation_type, weight,
|
|
468
|
+
valid_from_commit, valid_to_commit, confidence_tag,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
def get_edges_from(self, node_id: int) -> list[dict]:
|
|
472
|
+
"""Edges coming out of a node (source → targets)."""
|
|
473
|
+
with self._conn_mgr.read() as conn:
|
|
474
|
+
rows = conn.execute("SELECT * FROM edges WHERE source_id = ?", (node_id,)).fetchall()
|
|
475
|
+
return [dict(r) for r in rows]
|
|
476
|
+
|
|
477
|
+
def get_edges_to(self, node_id: int) -> list[dict]:
|
|
478
|
+
"""Edges arriving at a node (sources → target)."""
|
|
479
|
+
with self._conn_mgr.read() as conn:
|
|
480
|
+
rows = conn.execute("SELECT * FROM edges WHERE target_id = ?", (node_id,)).fetchall()
|
|
481
|
+
return [dict(r) for r in rows]
|
|
482
|
+
|
|
483
|
+
def get_in_degree(self, node_id: int) -> int:
|
|
484
|
+
"""Counts incoming edges (in-degree) of a node."""
|
|
485
|
+
with self._conn_mgr.read() as conn:
|
|
486
|
+
row = conn.execute("SELECT COUNT(*) AS c FROM edges WHERE target_id = ?", (node_id,)).fetchone()
|
|
487
|
+
return row["c"] if row else 0
|
|
488
|
+
|
|
489
|
+
def delete_edge(self, source_id: int, target_id: int) -> None:
|
|
490
|
+
"""Removes an edge."""
|
|
491
|
+
def _do(conn, s, t):
|
|
492
|
+
conn.execute("DELETE FROM edges WHERE source_id = ? AND target_id = ?", (s, t))
|
|
493
|
+
self._conn_mgr.write(_do, source_id, target_id)
|
|
494
|
+
|
|
495
|
+
# ===================================================================
|
|
496
|
+
# TRAJECTORIES
|
|
497
|
+
# ===================================================================
|
|
498
|
+
|
|
499
|
+
def create_trajectory(
|
|
500
|
+
self, project_uuid: str, prompt_origem: str, tentativa_execucao: str,
|
|
501
|
+
erro_encontrado: Optional[str] = None, solucao_aplicada: Optional[str] = None,
|
|
502
|
+
status: str = "ACTIVE",
|
|
503
|
+
) -> int:
|
|
504
|
+
"""Registers an episodic trajectory (Learning Loop)."""
|
|
505
|
+
if status not in VALID_STATUSES:
|
|
506
|
+
raise ValueError(f"Invalid status: '{status}'")
|
|
507
|
+
|
|
508
|
+
def _do(conn, pu, po, te, ee, sa, st):
|
|
509
|
+
cur = conn.execute(
|
|
510
|
+
"""INSERT INTO trajectories
|
|
511
|
+
(project_uuid, prompt_origem, tentativa_execucao, erro_encontrado, solucao_aplicada, status)
|
|
512
|
+
VALUES (?,?,?,?,?,?)""",
|
|
513
|
+
(pu, po, te, ee, sa, st))
|
|
514
|
+
return cur.lastrowid
|
|
515
|
+
return self._conn_mgr.write(_do, project_uuid, prompt_origem, tentativa_execucao,
|
|
516
|
+
erro_encontrado, solucao_aplicada, status)
|
|
517
|
+
|
|
518
|
+
def get_trajectories(self, project_uuid: str, status: Optional[str] = None) -> list[dict]:
|
|
519
|
+
"""Lists trajectories of a project with optional status filter."""
|
|
520
|
+
sql = "SELECT * FROM trajectories WHERE project_uuid = ?"
|
|
521
|
+
params: list[Any] = [project_uuid]
|
|
522
|
+
if status:
|
|
523
|
+
sql += " AND status = ?"
|
|
524
|
+
params.append(status)
|
|
525
|
+
sql += " ORDER BY created_at DESC"
|
|
526
|
+
with self._conn_mgr.read() as conn:
|
|
527
|
+
rows = conn.execute(sql, params).fetchall()
|
|
528
|
+
return [dict(r) for r in rows]
|
|
529
|
+
|
|
530
|
+
def decay_trajectory(self, trajectory_id: int, new_status: str) -> bool:
|
|
531
|
+
"""Delegates to GraphLogic (state machine with validation)."""
|
|
532
|
+
return self._logic.decay_trajectory(trajectory_id, new_status)
|
|
533
|
+
|
|
534
|
+
# ===================================================================
|
|
535
|
+
# COMMIT LOG (aligned with concierge_commit)
|
|
536
|
+
# ===================================================================
|
|
537
|
+
|
|
538
|
+
def create_commit(
|
|
539
|
+
self, project_uuid: str, phase: str, technical_changes: str,
|
|
540
|
+
updated_pointers: list[str], revisor_approved: bool = False,
|
|
541
|
+
partial_audit: bool = False,
|
|
542
|
+
) -> int:
|
|
543
|
+
"""Registers an audited memory commit.
|
|
544
|
+
|
|
545
|
+
Mandatory validation: technical_changes and updated_pointers cannot be empty.
|
|
546
|
+
"""
|
|
547
|
+
if not technical_changes:
|
|
548
|
+
raise CommitValidationError("technical_changes is mandatory and cannot be empty.")
|
|
549
|
+
if not updated_pointers:
|
|
550
|
+
raise CommitValidationError("updated_pointers is mandatory and cannot be empty.")
|
|
551
|
+
pointers_json = json.dumps(updated_pointers, ensure_ascii=False)
|
|
552
|
+
|
|
553
|
+
def _do(conn, pu, ph, tc, up, ra, pa):
|
|
554
|
+
cur = conn.execute(
|
|
555
|
+
"""INSERT INTO commit_log
|
|
556
|
+
(project_uuid, phase, technical_changes, updated_pointers, revisor_approved, partial_audit)
|
|
557
|
+
VALUES (?,?,?,?,?,?)""",
|
|
558
|
+
(pu, ph, tc, up, int(ra), int(pa)))
|
|
559
|
+
return cur.lastrowid
|
|
560
|
+
return self._conn_mgr.write(_do, project_uuid, phase, technical_changes,
|
|
561
|
+
pointers_json, revisor_approved, partial_audit)
|
|
562
|
+
|
|
563
|
+
def get_recent_commits(self, project_uuid: str, limit: int = 5) -> list[dict]:
|
|
564
|
+
"""Returns the N most recent commits of a project."""
|
|
565
|
+
with self._conn_mgr.read() as conn:
|
|
566
|
+
rows = conn.execute(
|
|
567
|
+
"SELECT * FROM commit_log WHERE project_uuid = ? ORDER BY created_at DESC LIMIT ?",
|
|
568
|
+
(project_uuid, limit)).fetchall()
|
|
569
|
+
results = []
|
|
570
|
+
for r in rows:
|
|
571
|
+
d = dict(r)
|
|
572
|
+
if d.get("updated_pointers"):
|
|
573
|
+
try:
|
|
574
|
+
d["updated_pointers"] = json.loads(d["updated_pointers"])
|
|
575
|
+
except (json.JSONDecodeError, TypeError):
|
|
576
|
+
pass
|
|
577
|
+
results.append(d)
|
|
578
|
+
return results
|
|
579
|
+
|
|
580
|
+
# ===================================================================
|
|
581
|
+
# REFERENCE WINGS
|
|
582
|
+
# ===================================================================
|
|
583
|
+
|
|
584
|
+
def add_reference_wing(self, project_uuid: str, wing_name: str) -> None:
|
|
585
|
+
"""Adds a Reference Wing to the project."""
|
|
586
|
+
def _do(conn, pu, wn):
|
|
587
|
+
conn.execute("INSERT OR IGNORE INTO reference_wings (project_uuid, wing_name) VALUES (?,?)", (pu, wn))
|
|
588
|
+
self._conn_mgr.write(_do, project_uuid, wing_name)
|
|
589
|
+
|
|
590
|
+
def get_reference_wings(self, project_uuid: str) -> list[str]:
|
|
591
|
+
"""Lists Reference Wings of a project."""
|
|
592
|
+
with self._conn_mgr.read() as conn:
|
|
593
|
+
rows = conn.execute("SELECT wing_name FROM reference_wings WHERE project_uuid = ?", (project_uuid,)).fetchall()
|
|
594
|
+
return [r["wing_name"] for r in rows]
|
|
595
|
+
|
|
596
|
+
def remove_reference_wing(self, project_uuid: str, wing_name: str) -> None:
|
|
597
|
+
"""Removes a Reference Wing."""
|
|
598
|
+
def _do(conn, pu, wn):
|
|
599
|
+
conn.execute("DELETE FROM reference_wings WHERE project_uuid = ? AND wing_name = ?", (pu, wn))
|
|
600
|
+
self._conn_mgr.write(_do, project_uuid, wing_name)
|
|
601
|
+
|
|
602
|
+
# ===================================================================
|
|
603
|
+
# INTELLIGENCE (delegated to GraphLogic)
|
|
604
|
+
# ===================================================================
|
|
605
|
+
|
|
606
|
+
def compute_centrality(self, node_id: int) -> float:
|
|
607
|
+
"""Normalized centrality: min(in_degree/10, 1.0)."""
|
|
608
|
+
return self._logic.compute_centrality(node_id)
|
|
609
|
+
|
|
610
|
+
def compute_recency_score(self, node_id: int) -> float:
|
|
611
|
+
"""Recency score: max(e^(-λ×t), 0.01)."""
|
|
612
|
+
return self._logic.compute_recency_score(node_id)
|
|
613
|
+
|
|
614
|
+
def fts_search(self, query: str, project_uuid: Optional[str] = None,
|
|
615
|
+
node_type: Optional[str] = None, limit: int = 20) -> list[dict]:
|
|
616
|
+
"""FTS5 search with normalized BM25 (concierge_search - frequency component)."""
|
|
617
|
+
return self._logic.fts_search(query, project_uuid, node_type, limit)
|
|
618
|
+
|
|
619
|
+
def hybrid_search_score(self, node_id: int, vector_score: float, fts_score: float) -> dict:
|
|
620
|
+
"""Individual hybrid score: 0.50×vet + 0.25×fts + 0.25×max(rec,cent)."""
|
|
621
|
+
return self._logic.hybrid_search_score(node_id, vector_score, fts_score)
|
|
622
|
+
|
|
623
|
+
def hybrid_search_score_batch(self, candidates: list[dict]) -> list[dict]:
|
|
624
|
+
"""Hybrid score in batch — used by concierge_search."""
|
|
625
|
+
return self._logic.hybrid_search_score_batch(candidates)
|
|
626
|
+
|
|
627
|
+
def fts_rebuild(self) -> None:
|
|
628
|
+
"""Rebuilds the FTS5 index (post massive concierge_mine)."""
|
|
629
|
+
self._logic.fts_rebuild()
|
|
630
|
+
|
|
631
|
+
def get_dependency_tree(self, start_node_id: int, max_depth: int = 10) -> list[dict]:
|
|
632
|
+
"""CTE: dependency tree with anti-loop protection."""
|
|
633
|
+
return self._logic.get_dependency_tree(start_node_id, max_depth)
|
|
634
|
+
|
|
635
|
+
def get_reverse_dependency_tree(self, start_node_id: int, max_depth: int = 10) -> list[dict]:
|
|
636
|
+
"""Reverse CTE: who depends on this node."""
|
|
637
|
+
return self._logic.get_reverse_dependency_tree(start_node_id, max_depth)
|
|
638
|
+
|
|
639
|
+
def get_project_stats(self, project_uuid: str) -> dict:
|
|
640
|
+
"""Complete statistics of a project."""
|
|
641
|
+
return self._logic.get_project_stats(project_uuid)
|
|
642
|
+
|
|
643
|
+
def get_last_commit_phase(self, project_uuid: str) -> Optional[str]:
|
|
644
|
+
"""Phase of the most recent commit."""
|
|
645
|
+
return self._logic.get_last_commit_phase(project_uuid)
|
|
646
|
+
|
|
647
|
+
def bulk_decay_stale_trajectories(self, project_uuid: str, stale_threshold_days: int = 30) -> int:
|
|
648
|
+
"""Mass decay for the Background Janitor."""
|
|
649
|
+
return self._logic.bulk_decay_stale_trajectories(project_uuid, stale_threshold_days)
|
|
650
|
+
|
|
651
|
+
def search_symbols(self, query: str, project_uuid: Optional[str] = None, limit: int = 50) -> list[dict]:
|
|
652
|
+
"""Searches for indexed classes, methods, and functions using FTS5."""
|
|
653
|
+
safe_query = query.replace('"', '""')
|
|
654
|
+
sql = """
|
|
655
|
+
SELECT n.id, n.label, n.type, n.project_uuid, n.file_hash
|
|
656
|
+
FROM nodes_fts f
|
|
657
|
+
JOIN nodes n ON n.id = f.rowid
|
|
658
|
+
WHERE nodes_fts MATCH ? AND n.type IN ('class', 'function', 'method')
|
|
659
|
+
ORDER BY CASE n.type WHEN 'class' THEN 1 WHEN 'function' THEN 2 WHEN 'method' THEN 3 ELSE 4 END, n.id
|
|
660
|
+
"""
|
|
661
|
+
params: list[Any] = [f'"{safe_query}"']
|
|
662
|
+
if project_uuid:
|
|
663
|
+
sql += " AND n.project_uuid = ?"
|
|
664
|
+
params.append(project_uuid)
|
|
665
|
+
sql += " LIMIT ?"
|
|
666
|
+
params.append(limit)
|
|
667
|
+
return self._conn_mgr.execute_raw_read(sql, tuple(params))
|
|
668
|
+
|
|
669
|
+
def get_callers(self, symbol_id: int) -> list[dict]:
|
|
670
|
+
"""Returns all nodes that call the specified symbol."""
|
|
671
|
+
sql = """
|
|
672
|
+
SELECT n.id, n.label, n.type, n.project_uuid
|
|
673
|
+
FROM edges e
|
|
674
|
+
JOIN nodes n ON e.source_id = n.id
|
|
675
|
+
WHERE e.target_id = ? AND e.relation_type = 'calls'
|
|
676
|
+
"""
|
|
677
|
+
return self._conn_mgr.execute_raw_read(sql, (symbol_id,))
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
# ===================================================================
|
|
681
|
+
# ENCAPSULATION — Public API for JanitorService (Patch 2)
|
|
682
|
+
# ===================================================================
|
|
683
|
+
|
|
684
|
+
def is_write_queue_empty(self) -> bool:
|
|
685
|
+
"""Returns True if the write queue (SerializedWriteQueue) has no pending jobs.
|
|
686
|
+
|
|
687
|
+
Replaces direct access to _conn_mgr._write_queue._queue.empty() in Janitor.
|
|
688
|
+
"""
|
|
689
|
+
return self._conn_mgr.is_write_queue_empty()
|
|
690
|
+
|
|
691
|
+
def execute_read_sql(self, sql: str, params: tuple = ()) -> list[dict]:
|
|
692
|
+
"""Executes arbitrary read SQL and returns a list of dicts.
|
|
693
|
+
|
|
694
|
+
Allows JanitorService to make complex queries (WITH RECURSIVE,
|
|
695
|
+
JOIN with FTS5, etc.) without needing to access self._conn_mgr directly.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
sql: SQL query for reading (SELECT / WITH RECURSIVE).
|
|
699
|
+
params: Positional parameters for the query.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
List of dictionaries with the results.
|
|
703
|
+
"""
|
|
704
|
+
return self._conn_mgr.execute_raw_read(sql, params)
|
|
705
|
+
|
|
706
|
+
# ===================================================================
|
|
707
|
+
# USER CORE MEMORY — Complete CRUD (Patch 1)
|
|
708
|
+
# ===================================================================
|
|
709
|
+
|
|
710
|
+
_VALID_SCOPE_TYPES: frozenset[str] = frozenset({"user", "session", "agent", "org"})
|
|
711
|
+
|
|
712
|
+
def set_core_memory(
|
|
713
|
+
self,
|
|
714
|
+
scope_type: str,
|
|
715
|
+
scope_id: str,
|
|
716
|
+
block_label: str,
|
|
717
|
+
content: str,
|
|
718
|
+
) -> int:
|
|
719
|
+
"""Writes or updates a block of user/session core memory.
|
|
720
|
+
|
|
721
|
+
Uses INSERT OR REPLACE to ensure block_label is unique per scope.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
scope_type: Scope type — 'user', 'session', 'agent' or 'org'.
|
|
725
|
+
scope_id: Scope identifier (e.g. user UUID, session UUID).
|
|
726
|
+
block_label: Label of the memory block (e.g. 'preferred_language').
|
|
727
|
+
content: Content of the memory block.
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
The id of the inserted or replaced record.
|
|
731
|
+
|
|
732
|
+
Raises:
|
|
733
|
+
ValueError: If scope_type is invalid or required fields are empty.
|
|
734
|
+
"""
|
|
735
|
+
if scope_type not in self._VALID_SCOPE_TYPES:
|
|
736
|
+
raise ValueError(f"Invalid scope_type: '{scope_type}'. Accepted: {sorted(self._VALID_SCOPE_TYPES)}")
|
|
737
|
+
if not scope_id or not scope_id.strip():
|
|
738
|
+
raise ValueError("scope_id cannot be empty.")
|
|
739
|
+
if not block_label or not block_label.strip():
|
|
740
|
+
raise ValueError("block_label cannot be empty.")
|
|
741
|
+
|
|
742
|
+
def _write(conn: "sqlite3.Connection") -> int:
|
|
743
|
+
cursor = conn.execute(
|
|
744
|
+
"""INSERT OR REPLACE INTO user_core_memory
|
|
745
|
+
(scope_type, scope_id, block_label, content, updated_at)
|
|
746
|
+
VALUES (?, ?, ?, ?, datetime('now'))""",
|
|
747
|
+
(scope_type, scope_id.strip(), block_label.strip(), content),
|
|
748
|
+
)
|
|
749
|
+
return cursor.lastrowid
|
|
750
|
+
|
|
751
|
+
return self._conn_mgr.write(_write)
|
|
752
|
+
|
|
753
|
+
def get_core_memory(
|
|
754
|
+
self,
|
|
755
|
+
scope_type: str,
|
|
756
|
+
scope_id: str,
|
|
757
|
+
block_label: str,
|
|
758
|
+
) -> Optional[dict]:
|
|
759
|
+
"""Returns a specific core memory block, or None if it doesn't exist.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
scope_type: Scope type.
|
|
763
|
+
scope_id: Scope identifier.
|
|
764
|
+
block_label: Label of the memory block.
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
Dict with the record columns, or None.
|
|
768
|
+
"""
|
|
769
|
+
if scope_type not in self._VALID_SCOPE_TYPES:
|
|
770
|
+
raise ValueError(f"Invalid scope_type: '{scope_type}'.")
|
|
771
|
+
rows = self._conn_mgr.execute_raw_read(
|
|
772
|
+
"""SELECT id, scope_type, scope_id, block_label, content, updated_at
|
|
773
|
+
FROM user_core_memory
|
|
774
|
+
WHERE scope_type = ? AND scope_id = ? AND block_label = ?
|
|
775
|
+
LIMIT 1""",
|
|
776
|
+
(scope_type, scope_id.strip(), block_label.strip()),
|
|
777
|
+
)
|
|
778
|
+
return rows[0] if rows else None
|
|
779
|
+
|
|
780
|
+
def list_core_memory_blocks(
|
|
781
|
+
self,
|
|
782
|
+
scope_type: str,
|
|
783
|
+
scope_id: str,
|
|
784
|
+
) -> list[dict]:
|
|
785
|
+
"""Returns all core memory blocks for a scope.
|
|
786
|
+
|
|
787
|
+
Args:
|
|
788
|
+
scope_type: Scope type.
|
|
789
|
+
scope_id: Scope identifier.
|
|
790
|
+
|
|
791
|
+
Returns:
|
|
792
|
+
List of dicts (can be empty).
|
|
793
|
+
"""
|
|
794
|
+
if scope_type not in self._VALID_SCOPE_TYPES:
|
|
795
|
+
raise ValueError(f"Invalid scope_type: '{scope_type}'.")
|
|
796
|
+
return self._conn_mgr.execute_raw_read(
|
|
797
|
+
"""SELECT id, scope_type, scope_id, block_label, content, updated_at
|
|
798
|
+
FROM user_core_memory
|
|
799
|
+
WHERE scope_type = ? AND scope_id = ?
|
|
800
|
+
ORDER BY block_label ASC""",
|
|
801
|
+
(scope_type, scope_id.strip()),
|
|
802
|
+
)
|