differix 0.1.0a1__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/editor.py +455 -0
- agents/researcher.py +405 -0
- agents/reviewer.py +374 -0
- cli/model_cli.py +430 -0
- config.py +557 -0
- differix-0.1.0a1.dist-info/METADATA +123 -0
- differix-0.1.0a1.dist-info/RECORD +62 -0
- differix-0.1.0a1.dist-info/WHEEL +5 -0
- differix-0.1.0a1.dist-info/entry_points.txt +2 -0
- differix-0.1.0a1.dist-info/top_level.txt +6 -0
- main.py +2796 -0
- memory/__init__.py +59 -0
- memory/embeddings/__init__.py +29 -0
- memory/embeddings/factory.py +178 -0
- memory/embeddings/providers.py +259 -0
- memory/manager.py +186 -0
- memory/migrator.py +253 -0
- memory/ranker.py +203 -0
- memory/schemas.py +127 -0
- memory/storage.py +552 -0
- tools/agents_chat.py +152 -0
- tools/asset_extractor.py +545 -0
- tools/asset_manager.py +115 -0
- tools/atomic_changeset.py +264 -0
- tools/backup_manager.py +122 -0
- tools/changelog_engine.py +261 -0
- tools/coherence_checker.py +771 -0
- tools/compiler.py +531 -0
- tools/component_normalizer.py +130 -0
- tools/context_assembler.py +631 -0
- tools/contract_validator.py +478 -0
- tools/cost_predictor.py +141 -0
- tools/drift_guard.py +196 -0
- tools/file_manager.py +119 -0
- tools/handshake.py +351 -0
- tools/lesson_manager.py +570 -0
- tools/lesson_memory_bridge.py +398 -0
- tools/llm_client.py +103 -0
- tools/llm_selector.py +362 -0
- tools/model_ranker.py +535 -0
- tools/palette_extractor.py +1043 -0
- tools/plugin_manager.py +204 -0
- tools/project_context_gatherer.py +404 -0
- tools/project_scanner.py +41 -0
- tools/project_structure.py +244 -0
- tools/reasoning_logger.py +78 -0
- tools/reflection.py +452 -0
- tools/sandbox_utils.py +56 -0
- tools/security/__init__.py +0 -0
- tools/security/behavior_guard.py +83 -0
- tools/security/verification_firewall.py +113 -0
- tools/security_filter.py +213 -0
- tools/session_adr.py +311 -0
- tools/skill_manager.py +129 -0
- tools/state_manager.py +383 -0
- tools/structure_index.py +570 -0
- tools/test_schema_consistency.py +19 -0
- tools/verification_firewall.py +113 -0
- tools/visual_capture.py +65 -0
- tools/visual_diff.py +220 -0
- tools/visual_reviewer.py +79 -0
- tools/web_search.py +371 -0
memory/schemas.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data types, enums, and exceptions for the VegeX memory subsystem.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import enum
|
|
8
|
+
import math
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Any, Dict, List
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Exceptions
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
class MemoryStorageError(Exception):
|
|
20
|
+
"""Base exception for all memory storage operations."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DimensionMismatchError(MemoryStorageError):
|
|
24
|
+
def __init__(self, expected: int, received: int) -> None:
|
|
25
|
+
self.expected = expected
|
|
26
|
+
self.received = received
|
|
27
|
+
super().__init__(
|
|
28
|
+
f"Vector dimension mismatch: expected {expected}, received {received}."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MemoryNotFoundError(MemoryStorageError):
|
|
33
|
+
def __init__(self, node_id: str) -> None:
|
|
34
|
+
self.node_id = node_id
|
|
35
|
+
super().__init__(f"Memory node not found: {node_id}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MemoryValidationError(MemoryStorageError):
|
|
39
|
+
"""Raised when a MemoryNode violates schema-level invariants."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Enums
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
class MemoryScope(str, enum.Enum):
|
|
46
|
+
GLOBAL = "global"
|
|
47
|
+
PROJECT = "project"
|
|
48
|
+
LANGUAGE = "language"
|
|
49
|
+
DOMAIN = "domain"
|
|
50
|
+
FILE = "file"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Core model
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
class MemoryNode(BaseModel):
|
|
57
|
+
"""A single memory node / lesson with A-Score signals."""
|
|
58
|
+
|
|
59
|
+
node_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
60
|
+
vector: List[float] = Field(...)
|
|
61
|
+
content: str = Field(..., min_length=1)
|
|
62
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
base_trust: float = Field(default=0.3, ge=0.0, le=1.0)
|
|
65
|
+
trigger_count: int = Field(default=0, ge=0)
|
|
66
|
+
helpful_count: int = Field(default=0, ge=0)
|
|
67
|
+
violation_count: int = Field(default=0, ge=0)
|
|
68
|
+
is_critical: bool = Field(default=False)
|
|
69
|
+
|
|
70
|
+
scope_type: MemoryScope = Field(default=MemoryScope.GLOBAL)
|
|
71
|
+
scope_value: str = Field(default="*", min_length=1)
|
|
72
|
+
|
|
73
|
+
created_at: float = Field(default_factory=time.time)
|
|
74
|
+
last_used_at: float = Field(default_factory=time.time)
|
|
75
|
+
|
|
76
|
+
@field_validator("vector")
|
|
77
|
+
@classmethod
|
|
78
|
+
def _validate_vector(cls, v: List[float]) -> List[float]:
|
|
79
|
+
if not v:
|
|
80
|
+
raise MemoryValidationError("Vector cannot be empty.")
|
|
81
|
+
for idx, value in enumerate(v):
|
|
82
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
83
|
+
raise MemoryValidationError(
|
|
84
|
+
f"Vector element at index {idx} is not numeric: {value!r}"
|
|
85
|
+
)
|
|
86
|
+
if math.isnan(value) or math.isinf(value):
|
|
87
|
+
raise MemoryValidationError(
|
|
88
|
+
f"Vector contains NaN/Inf at index {idx}: {value!r}"
|
|
89
|
+
)
|
|
90
|
+
return [float(x) for x in v]
|
|
91
|
+
|
|
92
|
+
model_config = ConfigDict(frozen=False, extra="forbid")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Ranking weights (A-Score hyperparameters)
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
class RankingWeights(BaseModel):
|
|
99
|
+
alpha: float = Field(default=1.0, ge=0.0, le=5.0)
|
|
100
|
+
beta_protection: float = Field(default=2.0, ge=0.0, le=10.0)
|
|
101
|
+
lambda_base: float = Field(default=0.01, gt=0.0)
|
|
102
|
+
epsilon: float = Field(default=1e-5, ge=0.0)
|
|
103
|
+
|
|
104
|
+
model_config = ConfigDict(frozen=False, extra="forbid")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ScoredMemoryNode(BaseModel):
|
|
108
|
+
node: MemoryNode
|
|
109
|
+
score: float
|
|
110
|
+
similarity: float
|
|
111
|
+
scope_match: float
|
|
112
|
+
decay: float
|
|
113
|
+
jitter: float
|
|
114
|
+
|
|
115
|
+
model_config = ConfigDict(frozen=False, extra="forbid")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
__all__ = [
|
|
119
|
+
"MemoryNode",
|
|
120
|
+
"MemoryScope",
|
|
121
|
+
"RankingWeights",
|
|
122
|
+
"ScoredMemoryNode",
|
|
123
|
+
"MemoryStorageError",
|
|
124
|
+
"DimensionMismatchError",
|
|
125
|
+
"MemoryNotFoundError",
|
|
126
|
+
"MemoryValidationError",
|
|
127
|
+
]
|
memory/storage.py
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite-backed persistent storage for MemoryNode instances.
|
|
3
|
+
Source of truth for A-Score signals (B, T, H, V, is_critical, scope, timestamps).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import math
|
|
11
|
+
import sqlite3
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
16
|
+
|
|
17
|
+
from memory.schemas import (
|
|
18
|
+
DimensionMismatchError,
|
|
19
|
+
MemoryNode,
|
|
20
|
+
MemoryNotFoundError,
|
|
21
|
+
MemoryScope,
|
|
22
|
+
MemoryStorageError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SQLiteMemoryStorage:
|
|
29
|
+
"""Thread-safe SQLite storage for MemoryNode instances."""
|
|
30
|
+
|
|
31
|
+
_SCHEMA_VERSION = 2
|
|
32
|
+
|
|
33
|
+
_CREATE_TABLE = """
|
|
34
|
+
CREATE TABLE IF NOT EXISTS memory_nodes (
|
|
35
|
+
node_id TEXT PRIMARY KEY,
|
|
36
|
+
vector TEXT NOT NULL,
|
|
37
|
+
content TEXT NOT NULL,
|
|
38
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
39
|
+
base_trust REAL NOT NULL DEFAULT 0.3,
|
|
40
|
+
trigger_count INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
helpful_count INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
violation_count INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
is_critical INTEGER NOT NULL DEFAULT 0,
|
|
44
|
+
scope_type TEXT NOT NULL DEFAULT 'global',
|
|
45
|
+
scope_value TEXT NOT NULL DEFAULT '*',
|
|
46
|
+
created_at REAL NOT NULL,
|
|
47
|
+
last_used_at REAL NOT NULL
|
|
48
|
+
);
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
_CREATE_META_TABLE = """
|
|
52
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
53
|
+
key TEXT PRIMARY KEY,
|
|
54
|
+
value TEXT NOT NULL,
|
|
55
|
+
updated_at REAL NOT NULL
|
|
56
|
+
);
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
_CREATE_INDEXES = (
|
|
60
|
+
"CREATE INDEX IF NOT EXISTS idx_mn_created_at ON memory_nodes (created_at);",
|
|
61
|
+
"CREATE INDEX IF NOT EXISTS idx_mn_last_used ON memory_nodes (last_used_at);",
|
|
62
|
+
"CREATE INDEX IF NOT EXISTS idx_mn_scope ON memory_nodes (scope_type, scope_value);",
|
|
63
|
+
"CREATE INDEX IF NOT EXISTS idx_mn_critical ON memory_nodes (is_critical);",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_MIGRATION_COLUMNS: Tuple[Tuple[str, str], ...] = (
|
|
67
|
+
("base_trust", "REAL NOT NULL DEFAULT 0.3"),
|
|
68
|
+
("trigger_count", "INTEGER NOT NULL DEFAULT 0"),
|
|
69
|
+
("helpful_count", "INTEGER NOT NULL DEFAULT 0"),
|
|
70
|
+
("violation_count", "INTEGER NOT NULL DEFAULT 0"),
|
|
71
|
+
("is_critical", "INTEGER NOT NULL DEFAULT 0"),
|
|
72
|
+
("scope_type", "TEXT NOT NULL DEFAULT 'global'"),
|
|
73
|
+
("scope_value", "TEXT NOT NULL DEFAULT '*'"),
|
|
74
|
+
("last_used_at", "REAL NOT NULL DEFAULT 0"),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
_INSERT_SQL = """
|
|
78
|
+
INSERT INTO memory_nodes (
|
|
79
|
+
node_id, vector, content, metadata,
|
|
80
|
+
base_trust, trigger_count, helpful_count, violation_count, is_critical,
|
|
81
|
+
scope_type, scope_value, created_at, last_used_at
|
|
82
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
db_path: Union[str, Path] = ":memory:",
|
|
88
|
+
expected_dim: Optional[int] = None,
|
|
89
|
+
timeout: float = 30.0,
|
|
90
|
+
) -> None:
|
|
91
|
+
self.db_path = str(db_path)
|
|
92
|
+
self.expected_dim = expected_dim
|
|
93
|
+
self.timeout = timeout
|
|
94
|
+
self._lock = threading.RLock()
|
|
95
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
96
|
+
|
|
97
|
+
if self.db_path != ":memory:":
|
|
98
|
+
Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
self._initialize()
|
|
101
|
+
|
|
102
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
103
|
+
if self._conn is None:
|
|
104
|
+
try:
|
|
105
|
+
conn = sqlite3.connect(
|
|
106
|
+
self.db_path, timeout=self.timeout, check_same_thread=False
|
|
107
|
+
)
|
|
108
|
+
conn.row_factory = sqlite3.Row
|
|
109
|
+
if self.db_path != ":memory:":
|
|
110
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
111
|
+
conn.execute("PRAGMA synchronous=NORMAL;")
|
|
112
|
+
self._conn = conn
|
|
113
|
+
except sqlite3.Error as exc:
|
|
114
|
+
raise MemoryStorageError(
|
|
115
|
+
f"Failed to open SQLite database at {self.db_path}: {exc}"
|
|
116
|
+
) from exc
|
|
117
|
+
return self._conn
|
|
118
|
+
|
|
119
|
+
def _initialize(self) -> None:
|
|
120
|
+
try:
|
|
121
|
+
with self._lock:
|
|
122
|
+
conn = self._get_connection()
|
|
123
|
+
with conn:
|
|
124
|
+
conn.execute(self._CREATE_TABLE)
|
|
125
|
+
conn.execute(self._CREATE_META_TABLE)
|
|
126
|
+
for idx_sql in self._CREATE_INDEXES:
|
|
127
|
+
conn.execute(idx_sql)
|
|
128
|
+
self._ensure_schema(conn)
|
|
129
|
+
with conn:
|
|
130
|
+
conn.execute(f"PRAGMA user_version = {self._SCHEMA_VERSION};")
|
|
131
|
+
except sqlite3.Error as exc:
|
|
132
|
+
raise MemoryStorageError(
|
|
133
|
+
f"Failed to initialize memory storage schema: {exc}"
|
|
134
|
+
) from exc
|
|
135
|
+
|
|
136
|
+
def _ensure_schema(self, conn: sqlite3.Connection) -> None:
|
|
137
|
+
existing = {
|
|
138
|
+
row["name"]
|
|
139
|
+
for row in conn.execute("PRAGMA table_info(memory_nodes);").fetchall()
|
|
140
|
+
}
|
|
141
|
+
for column, definition in self._MIGRATION_COLUMNS:
|
|
142
|
+
if column in existing:
|
|
143
|
+
continue
|
|
144
|
+
try:
|
|
145
|
+
conn.execute(
|
|
146
|
+
f"ALTER TABLE memory_nodes ADD COLUMN {column} {definition};"
|
|
147
|
+
)
|
|
148
|
+
except sqlite3.Error as exc:
|
|
149
|
+
raise MemoryStorageError(
|
|
150
|
+
f"Failed to migrate column '{column}': {exc}"
|
|
151
|
+
) from exc
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------ #
|
|
154
|
+
# Config
|
|
155
|
+
# ------------------------------------------------------------------ #
|
|
156
|
+
def set_expected_dim(self, dim: Optional[int]) -> None:
|
|
157
|
+
"""Update the vector-dimension constraint. Used by the migrator."""
|
|
158
|
+
self.expected_dim = dim
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------ #
|
|
161
|
+
# Validation
|
|
162
|
+
# ------------------------------------------------------------------ #
|
|
163
|
+
def _validate_vector(self, vector: List[float]) -> None:
|
|
164
|
+
if not isinstance(vector, list) or not vector:
|
|
165
|
+
raise MemoryStorageError("Vector must be a non-empty list of floats.")
|
|
166
|
+
if self.expected_dim is not None and len(vector) != self.expected_dim:
|
|
167
|
+
raise DimensionMismatchError(self.expected_dim, len(vector))
|
|
168
|
+
for idx, value in enumerate(vector):
|
|
169
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
170
|
+
raise MemoryStorageError(
|
|
171
|
+
f"Vector element at index {idx} is not numeric: {value!r}"
|
|
172
|
+
)
|
|
173
|
+
if math.isnan(value) or math.isinf(value):
|
|
174
|
+
raise MemoryStorageError(
|
|
175
|
+
f"Vector contains NaN/Inf at index {idx}: {value!r}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _serialize_metadata(metadata: dict) -> str:
|
|
180
|
+
try:
|
|
181
|
+
return json.dumps(metadata or {})
|
|
182
|
+
except (TypeError, ValueError) as exc:
|
|
183
|
+
raise MemoryStorageError(
|
|
184
|
+
f"Metadata is not JSON-serializable: {exc}"
|
|
185
|
+
) from exc
|
|
186
|
+
|
|
187
|
+
def _row_to_node(self, row: sqlite3.Row) -> MemoryNode:
|
|
188
|
+
try:
|
|
189
|
+
vector = json.loads(row["vector"])
|
|
190
|
+
metadata = json.loads(row["metadata"]) if row["metadata"] else {}
|
|
191
|
+
scope_type = MemoryScope(row["scope_type"])
|
|
192
|
+
except (TypeError, ValueError) as exc:
|
|
193
|
+
raise MemoryStorageError(
|
|
194
|
+
f"Corrupted row for node_id={row['node_id']!r}: {exc}"
|
|
195
|
+
) from exc
|
|
196
|
+
|
|
197
|
+
return MemoryNode(
|
|
198
|
+
node_id=row["node_id"],
|
|
199
|
+
vector=vector,
|
|
200
|
+
content=row["content"],
|
|
201
|
+
metadata=metadata,
|
|
202
|
+
base_trust=float(row["base_trust"]),
|
|
203
|
+
trigger_count=int(row["trigger_count"]),
|
|
204
|
+
helpful_count=int(row["helpful_count"]),
|
|
205
|
+
violation_count=int(row["violation_count"]),
|
|
206
|
+
is_critical=bool(row["is_critical"]),
|
|
207
|
+
scope_type=scope_type,
|
|
208
|
+
scope_value=row["scope_value"],
|
|
209
|
+
created_at=float(row["created_at"]),
|
|
210
|
+
last_used_at=float(row["last_used_at"]),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _node_params(node: MemoryNode) -> Tuple:
|
|
215
|
+
return (
|
|
216
|
+
node.node_id,
|
|
217
|
+
json.dumps(node.vector),
|
|
218
|
+
node.content,
|
|
219
|
+
SQLiteMemoryStorage._serialize_metadata(node.metadata),
|
|
220
|
+
float(node.base_trust),
|
|
221
|
+
int(node.trigger_count),
|
|
222
|
+
int(node.helpful_count),
|
|
223
|
+
int(node.violation_count),
|
|
224
|
+
1 if node.is_critical else 0,
|
|
225
|
+
node.scope_type.value,
|
|
226
|
+
node.scope_value,
|
|
227
|
+
float(node.created_at),
|
|
228
|
+
float(node.last_used_at),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# ------------------------------------------------------------------ #
|
|
232
|
+
# Strict public API
|
|
233
|
+
# ------------------------------------------------------------------ #
|
|
234
|
+
def insert(self, node: MemoryNode) -> None:
|
|
235
|
+
if not isinstance(node, MemoryNode):
|
|
236
|
+
raise MemoryStorageError(
|
|
237
|
+
f"insert() expected MemoryNode, got {type(node).__name__}"
|
|
238
|
+
)
|
|
239
|
+
self._validate_vector(node.vector)
|
|
240
|
+
try:
|
|
241
|
+
with self._lock:
|
|
242
|
+
conn = self._get_connection()
|
|
243
|
+
with conn:
|
|
244
|
+
conn.execute(self._INSERT_SQL, self._node_params(node))
|
|
245
|
+
except sqlite3.IntegrityError as exc:
|
|
246
|
+
raise MemoryStorageError(
|
|
247
|
+
f"Memory node '{node.node_id}' already exists: {exc}"
|
|
248
|
+
) from exc
|
|
249
|
+
except sqlite3.Error as exc:
|
|
250
|
+
raise MemoryStorageError(
|
|
251
|
+
f"Failed to insert memory node '{node.node_id}': {exc}"
|
|
252
|
+
) from exc
|
|
253
|
+
|
|
254
|
+
def insert_batch(self, nodes: List[MemoryNode]) -> None:
|
|
255
|
+
if not nodes:
|
|
256
|
+
return
|
|
257
|
+
records = []
|
|
258
|
+
for node in nodes:
|
|
259
|
+
if not isinstance(node, MemoryNode):
|
|
260
|
+
raise MemoryStorageError(
|
|
261
|
+
f"insert_batch() expected MemoryNode, got {type(node).__name__}"
|
|
262
|
+
)
|
|
263
|
+
self._validate_vector(node.vector)
|
|
264
|
+
records.append(self._node_params(node))
|
|
265
|
+
try:
|
|
266
|
+
with self._lock:
|
|
267
|
+
conn = self._get_connection()
|
|
268
|
+
with conn:
|
|
269
|
+
conn.executemany(self._INSERT_SQL, records)
|
|
270
|
+
except sqlite3.IntegrityError as exc:
|
|
271
|
+
raise MemoryStorageError(
|
|
272
|
+
f"Batch insert failed due to constraint violation: {exc}"
|
|
273
|
+
) from exc
|
|
274
|
+
except sqlite3.Error as exc:
|
|
275
|
+
raise MemoryStorageError(f"Batch insert failed: {exc}") from exc
|
|
276
|
+
|
|
277
|
+
def replace_all(self, nodes: List[MemoryNode]) -> None:
|
|
278
|
+
"""
|
|
279
|
+
Atomically replace the entire node table with `nodes`.
|
|
280
|
+
Used by the migrator. Preserves nothing implicitly — the caller
|
|
281
|
+
must supply fully-formed nodes.
|
|
282
|
+
"""
|
|
283
|
+
records = []
|
|
284
|
+
for node in nodes:
|
|
285
|
+
if not isinstance(node, MemoryNode):
|
|
286
|
+
raise MemoryStorageError(
|
|
287
|
+
f"replace_all() expected MemoryNode, got {type(node).__name__}"
|
|
288
|
+
)
|
|
289
|
+
self._validate_vector(node.vector)
|
|
290
|
+
records.append(self._node_params(node))
|
|
291
|
+
try:
|
|
292
|
+
with self._lock:
|
|
293
|
+
conn = self._get_connection()
|
|
294
|
+
with conn:
|
|
295
|
+
conn.execute("DELETE FROM memory_nodes;")
|
|
296
|
+
if records:
|
|
297
|
+
conn.executemany(self._INSERT_SQL, records)
|
|
298
|
+
except sqlite3.Error as exc:
|
|
299
|
+
raise MemoryStorageError(
|
|
300
|
+
f"Atomic replace_all failed: {exc}"
|
|
301
|
+
) from exc
|
|
302
|
+
|
|
303
|
+
def get(self, node_id: str) -> MemoryNode:
|
|
304
|
+
if not isinstance(node_id, str) or not node_id:
|
|
305
|
+
raise MemoryStorageError("node_id must be a non-empty string.")
|
|
306
|
+
try:
|
|
307
|
+
with self._lock:
|
|
308
|
+
conn = self._get_connection()
|
|
309
|
+
row = conn.execute(
|
|
310
|
+
"SELECT * FROM memory_nodes WHERE node_id = ?;", (node_id,)
|
|
311
|
+
).fetchone()
|
|
312
|
+
except sqlite3.Error as exc:
|
|
313
|
+
raise MemoryStorageError(
|
|
314
|
+
f"Failed to retrieve memory node '{node_id}': {exc}"
|
|
315
|
+
) from exc
|
|
316
|
+
if row is None:
|
|
317
|
+
raise MemoryNotFoundError(node_id)
|
|
318
|
+
return self._row_to_node(row)
|
|
319
|
+
|
|
320
|
+
def query(self, vector: List[float], limit: int = 10) -> List[MemoryNode]:
|
|
321
|
+
if limit <= 0:
|
|
322
|
+
return []
|
|
323
|
+
self._validate_vector(vector)
|
|
324
|
+
try:
|
|
325
|
+
with self._lock:
|
|
326
|
+
conn = self._get_connection()
|
|
327
|
+
rows = conn.execute("SELECT * FROM memory_nodes;").fetchall()
|
|
328
|
+
except sqlite3.Error as exc:
|
|
329
|
+
raise MemoryStorageError(f"Failed to scan memory nodes: {exc}") from exc
|
|
330
|
+
|
|
331
|
+
query_norm = math.sqrt(sum(v * v for v in vector))
|
|
332
|
+
if query_norm == 0.0:
|
|
333
|
+
return []
|
|
334
|
+
|
|
335
|
+
scored: List[Tuple[float, MemoryNode]] = []
|
|
336
|
+
for row in rows:
|
|
337
|
+
node = self._row_to_node(row)
|
|
338
|
+
if len(node.vector) != len(vector):
|
|
339
|
+
continue
|
|
340
|
+
node_norm = math.sqrt(sum(v * v for v in node.vector))
|
|
341
|
+
if node_norm == 0.0:
|
|
342
|
+
continue
|
|
343
|
+
dot = sum(a * b for a, b in zip(vector, node.vector))
|
|
344
|
+
scored.append((dot / (query_norm * node_norm), node))
|
|
345
|
+
scored.sort(key=lambda pair: pair[0], reverse=True)
|
|
346
|
+
return [node for _, node in scored[:limit]]
|
|
347
|
+
|
|
348
|
+
def delete(self, node_id: str) -> bool:
|
|
349
|
+
try:
|
|
350
|
+
with self._lock:
|
|
351
|
+
conn = self._get_connection()
|
|
352
|
+
with conn:
|
|
353
|
+
cur = conn.execute(
|
|
354
|
+
"DELETE FROM memory_nodes WHERE node_id = ?;", (node_id,)
|
|
355
|
+
)
|
|
356
|
+
return cur.rowcount > 0
|
|
357
|
+
except sqlite3.Error as exc:
|
|
358
|
+
raise MemoryStorageError(
|
|
359
|
+
f"Failed to delete memory node '{node_id}': {exc}"
|
|
360
|
+
) from exc
|
|
361
|
+
|
|
362
|
+
# ------------------------------------------------------------------ #
|
|
363
|
+
# Extended API
|
|
364
|
+
# ------------------------------------------------------------------ #
|
|
365
|
+
def list_all(self) -> List[MemoryNode]:
|
|
366
|
+
try:
|
|
367
|
+
with self._lock:
|
|
368
|
+
conn = self._get_connection()
|
|
369
|
+
rows = conn.execute("SELECT * FROM memory_nodes;").fetchall()
|
|
370
|
+
except sqlite3.Error as exc:
|
|
371
|
+
raise MemoryStorageError(f"Failed to list memory nodes: {exc}") from exc
|
|
372
|
+
return [self._row_to_node(row) for row in rows]
|
|
373
|
+
|
|
374
|
+
def query_scoped(
|
|
375
|
+
self,
|
|
376
|
+
scope_type: Optional[MemoryScope] = None,
|
|
377
|
+
scope_value: Optional[str] = None,
|
|
378
|
+
limit: Optional[int] = None,
|
|
379
|
+
) -> List[MemoryNode]:
|
|
380
|
+
clauses: List[str] = []
|
|
381
|
+
params: List[object] = []
|
|
382
|
+
if scope_type is not None:
|
|
383
|
+
clauses.append("scope_type = ?")
|
|
384
|
+
params.append(scope_type.value)
|
|
385
|
+
if scope_value is not None:
|
|
386
|
+
clauses.append("scope_value = ?")
|
|
387
|
+
params.append(scope_value)
|
|
388
|
+
sql = "SELECT * FROM memory_nodes"
|
|
389
|
+
if clauses:
|
|
390
|
+
sql += " WHERE " + " AND ".join(clauses)
|
|
391
|
+
sql += " ORDER BY last_used_at DESC"
|
|
392
|
+
if limit is not None and limit > 0:
|
|
393
|
+
sql += f" LIMIT {int(limit)}"
|
|
394
|
+
try:
|
|
395
|
+
with self._lock:
|
|
396
|
+
conn = self._get_connection()
|
|
397
|
+
rows = conn.execute(sql, params).fetchall()
|
|
398
|
+
except sqlite3.Error as exc:
|
|
399
|
+
raise MemoryStorageError(f"Failed to query scoped nodes: {exc}") from exc
|
|
400
|
+
return [self._row_to_node(row) for row in rows]
|
|
401
|
+
|
|
402
|
+
# ------------------------------------------------------------------ #
|
|
403
|
+
# Metadata (schema_meta)
|
|
404
|
+
# ------------------------------------------------------------------ #
|
|
405
|
+
def get_meta(self, key: str) -> Optional[str]:
|
|
406
|
+
try:
|
|
407
|
+
with self._lock:
|
|
408
|
+
conn = self._get_connection()
|
|
409
|
+
row = conn.execute(
|
|
410
|
+
"SELECT value FROM schema_meta WHERE key = ?;", (key,)
|
|
411
|
+
).fetchone()
|
|
412
|
+
except sqlite3.Error as exc:
|
|
413
|
+
raise MemoryStorageError(
|
|
414
|
+
f"Failed to read schema_meta['{key}']: {exc}"
|
|
415
|
+
) from exc
|
|
416
|
+
return row["value"] if row else None
|
|
417
|
+
|
|
418
|
+
def set_meta(self, key: str, value: str) -> None:
|
|
419
|
+
try:
|
|
420
|
+
with self._lock:
|
|
421
|
+
conn = self._get_connection()
|
|
422
|
+
with conn:
|
|
423
|
+
conn.execute(
|
|
424
|
+
"""
|
|
425
|
+
INSERT INTO schema_meta (key, value, updated_at)
|
|
426
|
+
VALUES (?, ?, ?)
|
|
427
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
428
|
+
value = excluded.value,
|
|
429
|
+
updated_at = excluded.updated_at;
|
|
430
|
+
""",
|
|
431
|
+
(key, value, time.time()),
|
|
432
|
+
)
|
|
433
|
+
except sqlite3.Error as exc:
|
|
434
|
+
raise MemoryStorageError(
|
|
435
|
+
f"Failed to write schema_meta['{key}']: {exc}"
|
|
436
|
+
) from exc
|
|
437
|
+
|
|
438
|
+
# ------------------------------------------------------------------ #
|
|
439
|
+
# Feedback loop
|
|
440
|
+
# ------------------------------------------------------------------ #
|
|
441
|
+
def record_trigger(
|
|
442
|
+
self, node_ids: List[str], touched_at: Optional[float] = None
|
|
443
|
+
) -> int:
|
|
444
|
+
return self._apply_deltas(
|
|
445
|
+
node_ids, trigger_delta=1, helpful_delta=0,
|
|
446
|
+
violation_delta=0, touched_at=touched_at,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
def record_helpful(self, node_ids: List[str]) -> int:
|
|
450
|
+
return self._apply_deltas(
|
|
451
|
+
node_ids, trigger_delta=0, helpful_delta=1, violation_delta=0
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def record_violation(self, node_ids: List[str]) -> int:
|
|
455
|
+
return self._apply_deltas(
|
|
456
|
+
node_ids, trigger_delta=0, helpful_delta=0, violation_delta=1
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def _apply_deltas(
|
|
460
|
+
self,
|
|
461
|
+
node_ids: List[str],
|
|
462
|
+
trigger_delta: int,
|
|
463
|
+
helpful_delta: int,
|
|
464
|
+
violation_delta: int,
|
|
465
|
+
touched_at: Optional[float] = None,
|
|
466
|
+
) -> int:
|
|
467
|
+
if not node_ids:
|
|
468
|
+
return 0
|
|
469
|
+
ts = time.time() if touched_at is None else float(touched_at)
|
|
470
|
+
affected = 0
|
|
471
|
+
try:
|
|
472
|
+
with self._lock:
|
|
473
|
+
conn = self._get_connection()
|
|
474
|
+
with conn:
|
|
475
|
+
for node_id in node_ids:
|
|
476
|
+
cur = conn.execute(
|
|
477
|
+
"""
|
|
478
|
+
UPDATE memory_nodes
|
|
479
|
+
SET trigger_count = trigger_count + ?,
|
|
480
|
+
helpful_count = helpful_count + ?,
|
|
481
|
+
violation_count = violation_count + ?,
|
|
482
|
+
last_used_at = ?
|
|
483
|
+
WHERE node_id = ?;
|
|
484
|
+
""",
|
|
485
|
+
(trigger_delta, helpful_delta, violation_delta, ts, node_id),
|
|
486
|
+
)
|
|
487
|
+
affected += cur.rowcount
|
|
488
|
+
except sqlite3.Error as exc:
|
|
489
|
+
raise MemoryStorageError(
|
|
490
|
+
f"Failed to apply feedback deltas: {exc}"
|
|
491
|
+
) from exc
|
|
492
|
+
return affected
|
|
493
|
+
|
|
494
|
+
def promote(self, node_id: str, base_trust: float = 1.0) -> None:
|
|
495
|
+
if not 0.0 <= base_trust <= 1.0:
|
|
496
|
+
raise MemoryStorageError(
|
|
497
|
+
f"base_trust must be within [0.0, 1.0], got {base_trust!r}"
|
|
498
|
+
)
|
|
499
|
+
try:
|
|
500
|
+
with self._lock:
|
|
501
|
+
conn = self._get_connection()
|
|
502
|
+
with conn:
|
|
503
|
+
cur = conn.execute(
|
|
504
|
+
"""
|
|
505
|
+
UPDATE memory_nodes
|
|
506
|
+
SET base_trust = ?, is_critical = 1
|
|
507
|
+
WHERE node_id = ?;
|
|
508
|
+
""",
|
|
509
|
+
(float(base_trust), node_id),
|
|
510
|
+
)
|
|
511
|
+
if cur.rowcount == 0:
|
|
512
|
+
raise MemoryNotFoundError(node_id)
|
|
513
|
+
except MemoryNotFoundError:
|
|
514
|
+
raise
|
|
515
|
+
except sqlite3.Error as exc:
|
|
516
|
+
raise MemoryStorageError(
|
|
517
|
+
f"Failed to promote memory node '{node_id}': {exc}"
|
|
518
|
+
) from exc
|
|
519
|
+
|
|
520
|
+
def count(self) -> int:
|
|
521
|
+
try:
|
|
522
|
+
with self._lock:
|
|
523
|
+
conn = self._get_connection()
|
|
524
|
+
row = conn.execute("SELECT COUNT(*) FROM memory_nodes;").fetchone()
|
|
525
|
+
return int(row[0]) if row else 0
|
|
526
|
+
except sqlite3.Error as exc:
|
|
527
|
+
raise MemoryStorageError(f"Failed to count memory nodes: {exc}") from exc
|
|
528
|
+
|
|
529
|
+
def clear(self) -> None:
|
|
530
|
+
try:
|
|
531
|
+
with self._lock:
|
|
532
|
+
conn = self._get_connection()
|
|
533
|
+
with conn:
|
|
534
|
+
conn.execute("DELETE FROM memory_nodes;")
|
|
535
|
+
except sqlite3.Error as exc:
|
|
536
|
+
raise MemoryStorageError(f"Failed to clear memory store: {exc}") from exc
|
|
537
|
+
|
|
538
|
+
def close(self) -> None:
|
|
539
|
+
with self._lock:
|
|
540
|
+
if self._conn is not None:
|
|
541
|
+
try:
|
|
542
|
+
self._conn.close()
|
|
543
|
+
except sqlite3.Error:
|
|
544
|
+
pass
|
|
545
|
+
finally:
|
|
546
|
+
self._conn = None
|
|
547
|
+
|
|
548
|
+
def __enter__(self) -> "SQLiteMemoryStorage":
|
|
549
|
+
return self
|
|
550
|
+
|
|
551
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
552
|
+
self.close()
|