pdm-memory 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdm_memory/__init__.py +22 -0
- pdm_memory/auth/__init__.py +4 -0
- pdm_memory/auth/jwt_handler.py +119 -0
- pdm_memory/bench.py +5 -0
- pdm_memory/core/__init__.py +1 -0
- pdm_memory/core/math.py +329 -0
- pdm_memory/core/retrieval.py +362 -0
- pdm_memory/core/signature.py +257 -0
- pdm_memory/ingest/__init__.py +6 -0
- pdm_memory/ingest/auto_signature.py +202 -0
- pdm_memory/ingest/batch.py +164 -0
- pdm_memory/ingest/ingester.py +272 -0
- pdm_memory/integrations/__init__.py +21 -0
- pdm_memory/integrations/anthropic_adapter.py +179 -0
- pdm_memory/integrations/context_manager.py +134 -0
- pdm_memory/integrations/gemini_adapter.py +215 -0
- pdm_memory/integrations/groq_adapter.py +171 -0
- pdm_memory/integrations/ollama_adapter.py +184 -0
- pdm_memory/integrations/openai_adapter.py +191 -0
- pdm_memory/memory.py +552 -0
- pdm_memory/storage/__init__.py +5 -0
- pdm_memory/storage/base.py +123 -0
- pdm_memory/storage/cloud_driver.py +271 -0
- pdm_memory/storage/sqlite_driver.py +331 -0
- pdm_memory/sync.py +145 -0
- pdm_memory/tools/__init__.py +1 -0
- pdm_memory/tools/bench.py +294 -0
- pdm_memory/tools/cli.py +207 -0
- pdm_memory-0.1.2.dist-info/METADATA +485 -0
- pdm_memory-0.1.2.dist-info/RECORD +34 -0
- pdm_memory-0.1.2.dist-info/WHEEL +5 -0
- pdm_memory-0.1.2.dist-info/entry_points.txt +2 -0
- pdm_memory-0.1.2.dist-info/licenses/LICENSE +50 -0
- pdm_memory-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cloud Storage Driver — Task 2.1
|
|
3
|
+
|
|
4
|
+
Implements BaseStorage by making HTTP requests to the AZUS Companion API
|
|
5
|
+
instead of reading/writing a local file.
|
|
6
|
+
|
|
7
|
+
Requires: httpx (listed as a main dependency in pyproject.toml).
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from pdm_memory.storage.cloud_driver import CloudDriver
|
|
11
|
+
from pdm_memory.auth import JWTAuth
|
|
12
|
+
|
|
13
|
+
auth = JWTAuth(token="eyJ...")
|
|
14
|
+
driver = CloudDriver(auth=auth, base_url="https://api.azus.ai")
|
|
15
|
+
|
|
16
|
+
# Or via Memory:
|
|
17
|
+
mem = Memory(store="cloud", token="eyJ...", cloud_url="https://api.azus.ai")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
from typing import List, Optional
|
|
25
|
+
|
|
26
|
+
from pdm_memory.auth.jwt_handler import JWTAuth
|
|
27
|
+
from pdm_memory.core.signature import DrawerInfo, SignatureRecord
|
|
28
|
+
from pdm_memory.storage.base import BaseStorage
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# Default AZUS Companion API base URL
|
|
33
|
+
DEFAULT_CLOUD_URL = "https://api.azus.ai"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CloudDriver(BaseStorage):
|
|
37
|
+
"""
|
|
38
|
+
HTTP-backed PDM storage driver targeting the AZUS Companion API.
|
|
39
|
+
|
|
40
|
+
All network calls use httpx (sync). Timeout is configurable.
|
|
41
|
+
On 401 responses, automatically attempts token refresh if JWTAuth
|
|
42
|
+
was constructed with a refresh_token.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
auth: JWTAuth instance carrying the user's access token.
|
|
46
|
+
base_url: Base URL of the AZUS Companion API.
|
|
47
|
+
timeout: Request timeout in seconds.
|
|
48
|
+
user: Username to scope all requests (overrides token claim).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
auth: JWTAuth,
|
|
54
|
+
base_url: str = DEFAULT_CLOUD_URL,
|
|
55
|
+
timeout: float = 15.0,
|
|
56
|
+
user: str = "default",
|
|
57
|
+
) -> None:
|
|
58
|
+
try:
|
|
59
|
+
import httpx # noqa: F401 — guard for optional dep
|
|
60
|
+
except ImportError:
|
|
61
|
+
raise ImportError(
|
|
62
|
+
"httpx is required for cloud mode. Install it: pip install httpx"
|
|
63
|
+
)
|
|
64
|
+
self._auth = auth
|
|
65
|
+
self._base_url = base_url.rstrip("/")
|
|
66
|
+
self._timeout = timeout
|
|
67
|
+
self._user = user
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
# BaseStorage
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def save(self, sig: SignatureRecord) -> str:
|
|
74
|
+
"""POST /api/v1/pdm/ingest — create a new signature in the cloud."""
|
|
75
|
+
payload = self._record_to_payload(sig)
|
|
76
|
+
resp = self._post("/api/v1/pdm/ingest", payload)
|
|
77
|
+
data = resp.json()
|
|
78
|
+
# Cloud may assign a different ID
|
|
79
|
+
cloud_id = data.get("id") or data.get("signature_id") or sig.id
|
|
80
|
+
logger.debug("[PDM-Cloud] Saved signature %s", cloud_id)
|
|
81
|
+
return str(cloud_id)
|
|
82
|
+
|
|
83
|
+
def get(self, memory_id: str, user: str = "default") -> Optional[SignatureRecord]:
|
|
84
|
+
"""GET /api/v1/pdm/signatures/<id>/"""
|
|
85
|
+
try:
|
|
86
|
+
resp = self._get(f"/api/v1/pdm/signatures/{memory_id}/")
|
|
87
|
+
return self._payload_to_record(resp.json())
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.warning("[PDM-Cloud] get(%s) failed: %s", memory_id, e)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def update(self, memory_id: str, **fields) -> None:
|
|
93
|
+
"""PATCH /api/v1/pdm/signatures/<id>/"""
|
|
94
|
+
try:
|
|
95
|
+
self._patch(f"/api/v1/pdm/signatures/{memory_id}/", fields)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.warning("[PDM-Cloud] update(%s) failed: %s", memory_id, e)
|
|
98
|
+
|
|
99
|
+
def delete(self, memory_id: str, user: str = "default") -> None:
|
|
100
|
+
"""DELETE /api/v1/pdm/signatures/<id>/"""
|
|
101
|
+
try:
|
|
102
|
+
import httpx
|
|
103
|
+
self._auth.ensure_fresh()
|
|
104
|
+
resp = httpx.delete(
|
|
105
|
+
f"{self._base_url}/api/v1/pdm/signatures/{memory_id}/",
|
|
106
|
+
headers=self._auth.headers(),
|
|
107
|
+
timeout=self._timeout,
|
|
108
|
+
)
|
|
109
|
+
resp.raise_for_status()
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.warning("[PDM-Cloud] delete(%s) failed: %s", memory_id, e)
|
|
112
|
+
|
|
113
|
+
def list(
|
|
114
|
+
self,
|
|
115
|
+
user: str = "default",
|
|
116
|
+
limit: int = 100,
|
|
117
|
+
min_pressure: float = 0.0,
|
|
118
|
+
drawer: Optional[str] = None,
|
|
119
|
+
) -> List[SignatureRecord]:
|
|
120
|
+
"""GET /api/v1/pdm/retrieve — list signatures with filters."""
|
|
121
|
+
params: dict = {"limit": limit, "min_pressure": min_pressure}
|
|
122
|
+
if drawer:
|
|
123
|
+
params["drawer"] = drawer
|
|
124
|
+
try:
|
|
125
|
+
resp = self._get("/api/v1/pdm/retrieve", params=params)
|
|
126
|
+
data = resp.json()
|
|
127
|
+
items = data if isinstance(data, list) else data.get("results", [])
|
|
128
|
+
return [self._payload_to_record(item) for item in items]
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.warning("[PDM-Cloud] list() failed: %s", e)
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
def list_drawers(self, user: str = "default") -> List[DrawerInfo]:
|
|
134
|
+
"""GET /api/v1/pdm/drawers"""
|
|
135
|
+
try:
|
|
136
|
+
resp = self._get("/api/v1/pdm/drawers")
|
|
137
|
+
items = resp.json()
|
|
138
|
+
if not isinstance(items, list):
|
|
139
|
+
items = items.get("results", [])
|
|
140
|
+
return [
|
|
141
|
+
DrawerInfo(
|
|
142
|
+
domain=item.get("domain", ""),
|
|
143
|
+
signature_count=item.get("signature_count", 0),
|
|
144
|
+
avg_pressure=float(item.get("avg_pressure", 0.0)),
|
|
145
|
+
description=item.get("description", ""),
|
|
146
|
+
)
|
|
147
|
+
for item in items
|
|
148
|
+
]
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.warning("[PDM-Cloud] list_drawers() failed: %s", e)
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
# HTTP helpers
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def _post(self, path: str, payload: dict):
|
|
158
|
+
import httpx
|
|
159
|
+
self._auth.ensure_fresh()
|
|
160
|
+
resp = httpx.post(
|
|
161
|
+
f"{self._base_url}{path}",
|
|
162
|
+
json=payload,
|
|
163
|
+
headers=self._auth.headers(),
|
|
164
|
+
timeout=self._timeout,
|
|
165
|
+
)
|
|
166
|
+
self._handle_auth_retry(resp, "POST", path, payload)
|
|
167
|
+
resp.raise_for_status()
|
|
168
|
+
return resp
|
|
169
|
+
|
|
170
|
+
def _get(self, path: str, params: Optional[dict] = None):
|
|
171
|
+
import httpx
|
|
172
|
+
self._auth.ensure_fresh()
|
|
173
|
+
resp = httpx.get(
|
|
174
|
+
f"{self._base_url}{path}",
|
|
175
|
+
params=params,
|
|
176
|
+
headers=self._auth.headers(),
|
|
177
|
+
timeout=self._timeout,
|
|
178
|
+
)
|
|
179
|
+
self._handle_auth_retry(resp, "GET", path)
|
|
180
|
+
resp.raise_for_status()
|
|
181
|
+
return resp
|
|
182
|
+
|
|
183
|
+
def _patch(self, path: str, payload: dict):
|
|
184
|
+
import httpx
|
|
185
|
+
self._auth.ensure_fresh()
|
|
186
|
+
resp = httpx.patch(
|
|
187
|
+
f"{self._base_url}{path}",
|
|
188
|
+
json=payload,
|
|
189
|
+
headers=self._auth.headers(),
|
|
190
|
+
timeout=self._timeout,
|
|
191
|
+
)
|
|
192
|
+
resp.raise_for_status()
|
|
193
|
+
return resp
|
|
194
|
+
|
|
195
|
+
def _handle_auth_retry(self, resp, method: str, path: str, payload=None) -> None:
|
|
196
|
+
"""On 401, attempt one token refresh and retry."""
|
|
197
|
+
import httpx
|
|
198
|
+
if resp.status_code == 401:
|
|
199
|
+
if self._auth.refresh():
|
|
200
|
+
if method == "POST":
|
|
201
|
+
resp2 = httpx.post(
|
|
202
|
+
f"{self._base_url}{path}",
|
|
203
|
+
json=payload,
|
|
204
|
+
headers=self._auth.headers(),
|
|
205
|
+
timeout=self._timeout,
|
|
206
|
+
)
|
|
207
|
+
resp.__dict__.update(resp2.__dict__)
|
|
208
|
+
elif method == "GET":
|
|
209
|
+
resp2 = httpx.get(
|
|
210
|
+
f"{self._base_url}{path}",
|
|
211
|
+
headers=self._auth.headers(),
|
|
212
|
+
timeout=self._timeout,
|
|
213
|
+
)
|
|
214
|
+
resp.__dict__.update(resp2.__dict__)
|
|
215
|
+
|
|
216
|
+
# ------------------------------------------------------------------
|
|
217
|
+
# Serialisation helpers
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _record_to_payload(sig: SignatureRecord) -> dict:
|
|
222
|
+
return {
|
|
223
|
+
"compressed_fact": sig.compressed_fact,
|
|
224
|
+
"source": sig.source,
|
|
225
|
+
"p_magnitude": sig.p_magnitude,
|
|
226
|
+
"t_persistence": sig.t_persistence,
|
|
227
|
+
"phase_privilege": sig.phase_privilege,
|
|
228
|
+
"intent_tags": sig.intent_tags,
|
|
229
|
+
"question_regime": sig.question_regime,
|
|
230
|
+
"drawer_domain": sig.drawer_domain,
|
|
231
|
+
"metadata": sig.metadata,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _payload_to_record(data: dict) -> SignatureRecord:
|
|
236
|
+
from datetime import datetime
|
|
237
|
+
tags = data.get("intent_tags") or []
|
|
238
|
+
if isinstance(tags, str):
|
|
239
|
+
try:
|
|
240
|
+
tags = json.loads(tags)
|
|
241
|
+
except Exception:
|
|
242
|
+
tags = []
|
|
243
|
+
|
|
244
|
+
def _dt(val):
|
|
245
|
+
if not val:
|
|
246
|
+
return None
|
|
247
|
+
try:
|
|
248
|
+
return datetime.fromisoformat(str(val))
|
|
249
|
+
except Exception:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
return SignatureRecord(
|
|
253
|
+
id=str(data.get("id", "")),
|
|
254
|
+
user=str(data.get("user", "default")),
|
|
255
|
+
compressed_fact=data.get("compressed_fact", ""),
|
|
256
|
+
source=data.get("source", "chat"),
|
|
257
|
+
p_magnitude=float(data.get("p_magnitude", 50.0)),
|
|
258
|
+
t_persistence=float(data.get("t_persistence", 30.0)),
|
|
259
|
+
phase_privilege=float(data.get("phase_privilege", 1.0)),
|
|
260
|
+
effective_spike=data.get("effective_spike"),
|
|
261
|
+
intent_tags=tags,
|
|
262
|
+
question_regime=data.get("question_regime", "neutral"),
|
|
263
|
+
domain=data.get("domain", "insight"),
|
|
264
|
+
drawer_domain=data.get("drawer_domain") or data.get("drawer", "general"),
|
|
265
|
+
retrieval_count=int(data.get("retrieval_count", 0)),
|
|
266
|
+
last_retrieved=_dt(data.get("last_retrieved")),
|
|
267
|
+
created_at=_dt(data.get("created_at")),
|
|
268
|
+
validation_prediction_total=int(data.get("validation_prediction_total", 0)),
|
|
269
|
+
validation_prediction_correct=int(data.get("validation_prediction_correct", 0)),
|
|
270
|
+
metadata=data.get("metadata") or {},
|
|
271
|
+
)
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite Storage Driver — Task 1.2
|
|
3
|
+
|
|
4
|
+
Zero external dependencies beyond Python stdlib.
|
|
5
|
+
Stores all PDM signatures in a single local .db file.
|
|
6
|
+
|
|
7
|
+
Privacy guarantee: raw compressed_fact is stored by default.
|
|
8
|
+
If store_raw=False, only the SHA-256 hash of the text is stored
|
|
9
|
+
(the memory is opaque even to someone with file access).
|
|
10
|
+
|
|
11
|
+
Schema is auto-created on first run.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
driver = SQLiteDriver("./my_app.db")
|
|
15
|
+
memory_id = driver.save(sig)
|
|
16
|
+
hits = driver.list(user="alice", min_pressure=50)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import sqlite3
|
|
25
|
+
import threading
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from typing import List, Optional
|
|
28
|
+
|
|
29
|
+
from pdm_memory.core.signature import DrawerInfo, SignatureRecord
|
|
30
|
+
from pdm_memory.storage.base import BaseStorage
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
_SCHEMA = """
|
|
35
|
+
CREATE TABLE IF NOT EXISTS pdm_signatures (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
user TEXT NOT NULL DEFAULT 'default',
|
|
38
|
+
compressed_fact TEXT NOT NULL,
|
|
39
|
+
compressed_fact_hash TEXT NOT NULL,
|
|
40
|
+
source TEXT NOT NULL DEFAULT 'chat',
|
|
41
|
+
p_magnitude REAL NOT NULL DEFAULT 50.0,
|
|
42
|
+
t_persistence REAL NOT NULL DEFAULT 30.0,
|
|
43
|
+
phase_privilege REAL NOT NULL DEFAULT 1.0,
|
|
44
|
+
effective_spike REAL,
|
|
45
|
+
intent_tags TEXT NOT NULL DEFAULT '[]',
|
|
46
|
+
question_regime TEXT NOT NULL DEFAULT 'neutral',
|
|
47
|
+
domain TEXT NOT NULL DEFAULT 'insight',
|
|
48
|
+
drawer_domain TEXT NOT NULL DEFAULT 'general',
|
|
49
|
+
retrieval_count INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
last_retrieved TEXT,
|
|
51
|
+
created_at TEXT NOT NULL,
|
|
52
|
+
validation_prediction_total INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
validation_prediction_correct INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
decay_rate REAL NOT NULL DEFAULT 0.9,
|
|
55
|
+
t_deadline TEXT,
|
|
56
|
+
urgency_rate REAL NOT NULL DEFAULT 2.0,
|
|
57
|
+
metadata TEXT NOT NULL DEFAULT '{}'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_pdm_user_pressure
|
|
61
|
+
ON pdm_signatures (user, p_magnitude DESC);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_pdm_user_drawer
|
|
64
|
+
ON pdm_signatures (user, drawer_domain);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS pdm_drawers (
|
|
67
|
+
domain TEXT NOT NULL,
|
|
68
|
+
user TEXT NOT NULL DEFAULT 'default',
|
|
69
|
+
description TEXT NOT NULL DEFAULT '',
|
|
70
|
+
PRIMARY KEY (domain, user)
|
|
71
|
+
);
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _now_iso() -> str:
|
|
76
|
+
return datetime.now(tz=timezone.utc).isoformat()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _parse_dt(s: Optional[str]) -> Optional[datetime]:
|
|
80
|
+
if not s:
|
|
81
|
+
return None
|
|
82
|
+
try:
|
|
83
|
+
return datetime.fromisoformat(s)
|
|
84
|
+
except Exception:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SQLiteDriver(BaseStorage):
|
|
89
|
+
"""
|
|
90
|
+
SQLite-backed PDM storage. Thread-safe (one connection per thread).
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
db_path: Path to the .db file. Created automatically.
|
|
94
|
+
store_raw: If False, only the SHA-256 hash of compressed_fact is
|
|
95
|
+
stored — the text itself never touches disk.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, db_path: str = "./pdm_memory.db", store_raw: bool = True) -> None:
|
|
99
|
+
self.db_path = db_path
|
|
100
|
+
self.store_raw = store_raw
|
|
101
|
+
self._local = threading.local()
|
|
102
|
+
# Initialise schema on startup
|
|
103
|
+
conn = self._conn()
|
|
104
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
105
|
+
conn.executescript(_SCHEMA)
|
|
106
|
+
conn.commit()
|
|
107
|
+
logger.debug("[PDM-SQLite] Opened %s (store_raw=%s)", db_path, store_raw)
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Connection management (one connection per thread)
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _conn(self) -> sqlite3.Connection:
|
|
114
|
+
if not hasattr(self._local, "conn") or self._local.conn is None:
|
|
115
|
+
self._local.conn = sqlite3.connect(
|
|
116
|
+
self.db_path,
|
|
117
|
+
detect_types=sqlite3.PARSE_DECLTYPES,
|
|
118
|
+
check_same_thread=False,
|
|
119
|
+
)
|
|
120
|
+
self._local.conn.row_factory = sqlite3.Row
|
|
121
|
+
# Optimise connection performance pragmas
|
|
122
|
+
self._local.conn.execute("PRAGMA synchronous=NORMAL")
|
|
123
|
+
self._local.conn.execute("PRAGMA temp_store=MEMORY")
|
|
124
|
+
return self._local.conn
|
|
125
|
+
|
|
126
|
+
def close(self) -> None:
|
|
127
|
+
if hasattr(self._local, "conn") and self._local.conn:
|
|
128
|
+
self._local.conn.close()
|
|
129
|
+
self._local.conn = None
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# BaseStorage implementation
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def save(self, sig: SignatureRecord) -> str:
|
|
136
|
+
"""Insert a new signature. Returns sig.id."""
|
|
137
|
+
text = sig.compressed_fact or ""
|
|
138
|
+
text_hash = hashlib.sha256(text.encode()).hexdigest()
|
|
139
|
+
stored_text = text if self.store_raw else f"[HASH:{text_hash}]"
|
|
140
|
+
|
|
141
|
+
conn = self._conn()
|
|
142
|
+
conn.execute(
|
|
143
|
+
"""
|
|
144
|
+
INSERT OR REPLACE INTO pdm_signatures (
|
|
145
|
+
id, user, compressed_fact, compressed_fact_hash, source,
|
|
146
|
+
p_magnitude, t_persistence, phase_privilege, effective_spike,
|
|
147
|
+
intent_tags, question_regime, domain, drawer_domain,
|
|
148
|
+
retrieval_count, last_retrieved, created_at,
|
|
149
|
+
validation_prediction_total, validation_prediction_correct,
|
|
150
|
+
decay_rate, t_deadline, urgency_rate, metadata
|
|
151
|
+
) VALUES (
|
|
152
|
+
?, ?, ?, ?, ?,
|
|
153
|
+
?, ?, ?, ?,
|
|
154
|
+
?, ?, ?, ?,
|
|
155
|
+
?, ?, ?,
|
|
156
|
+
?, ?,
|
|
157
|
+
?, ?, ?, ?
|
|
158
|
+
)
|
|
159
|
+
""",
|
|
160
|
+
(
|
|
161
|
+
sig.id,
|
|
162
|
+
sig.user,
|
|
163
|
+
stored_text,
|
|
164
|
+
text_hash,
|
|
165
|
+
sig.source,
|
|
166
|
+
sig.p_magnitude,
|
|
167
|
+
sig.t_persistence,
|
|
168
|
+
sig.phase_privilege,
|
|
169
|
+
sig.effective_spike,
|
|
170
|
+
json.dumps(sig.intent_tags),
|
|
171
|
+
sig.question_regime,
|
|
172
|
+
sig.domain,
|
|
173
|
+
sig.drawer_domain,
|
|
174
|
+
sig.retrieval_count,
|
|
175
|
+
sig.last_retrieved.isoformat() if sig.last_retrieved else None,
|
|
176
|
+
(sig.created_at or datetime.now(tz=timezone.utc)).isoformat(),
|
|
177
|
+
sig.validation_prediction_total,
|
|
178
|
+
sig.validation_prediction_correct,
|
|
179
|
+
sig.decay_rate,
|
|
180
|
+
sig.t_deadline.isoformat() if sig.t_deadline else None,
|
|
181
|
+
sig.urgency_rate,
|
|
182
|
+
json.dumps(sig.metadata),
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
# Upsert drawer record
|
|
186
|
+
conn.execute(
|
|
187
|
+
"INSERT OR IGNORE INTO pdm_drawers (domain, user, description) VALUES (?, ?, ?)",
|
|
188
|
+
(sig.drawer_domain, sig.user, ""),
|
|
189
|
+
)
|
|
190
|
+
conn.commit()
|
|
191
|
+
logger.debug("[PDM-SQLite] Saved signature %s (P=%.1f)", sig.id, sig.p_magnitude)
|
|
192
|
+
return sig.id
|
|
193
|
+
|
|
194
|
+
def get(self, memory_id: str, user: str = "default") -> Optional[SignatureRecord]:
|
|
195
|
+
row = self._conn().execute(
|
|
196
|
+
"SELECT * FROM pdm_signatures WHERE id = ? AND user = ?",
|
|
197
|
+
(memory_id, user),
|
|
198
|
+
).fetchone()
|
|
199
|
+
return self._row_to_record(row) if row else None
|
|
200
|
+
|
|
201
|
+
def update(self, memory_id: str, **fields) -> None:
|
|
202
|
+
if not fields:
|
|
203
|
+
return
|
|
204
|
+
# Serialise JSON fields
|
|
205
|
+
if "intent_tags" in fields:
|
|
206
|
+
fields["intent_tags"] = json.dumps(fields["intent_tags"])
|
|
207
|
+
if "metadata" in fields:
|
|
208
|
+
fields["metadata"] = json.dumps(fields["metadata"])
|
|
209
|
+
if "last_retrieved" in fields and isinstance(fields["last_retrieved"], datetime):
|
|
210
|
+
fields["last_retrieved"] = fields["last_retrieved"].isoformat()
|
|
211
|
+
|
|
212
|
+
set_clause = ", ".join(f"{k} = ?" for k in fields)
|
|
213
|
+
values = list(fields.values()) + [memory_id]
|
|
214
|
+
self._conn().execute(
|
|
215
|
+
f"UPDATE pdm_signatures SET {set_clause} WHERE id = ?",
|
|
216
|
+
values,
|
|
217
|
+
)
|
|
218
|
+
self._conn().commit()
|
|
219
|
+
|
|
220
|
+
def update_batch(self, updates: List[tuple[str, dict]]) -> None:
|
|
221
|
+
if not updates:
|
|
222
|
+
return
|
|
223
|
+
conn = self._conn()
|
|
224
|
+
for memory_id, fields in updates:
|
|
225
|
+
if not fields:
|
|
226
|
+
continue
|
|
227
|
+
fields_copy = dict(fields)
|
|
228
|
+
if "intent_tags" in fields_copy:
|
|
229
|
+
fields_copy["intent_tags"] = json.dumps(fields_copy["intent_tags"])
|
|
230
|
+
if "metadata" in fields_copy:
|
|
231
|
+
fields_copy["metadata"] = json.dumps(fields_copy["metadata"])
|
|
232
|
+
if "last_retrieved" in fields_copy and isinstance(fields_copy["last_retrieved"], datetime):
|
|
233
|
+
fields_copy["last_retrieved"] = fields_copy["last_retrieved"].isoformat()
|
|
234
|
+
|
|
235
|
+
set_clause = ", ".join(f"{k} = ?" for k in fields_copy)
|
|
236
|
+
values = list(fields_copy.values()) + [memory_id]
|
|
237
|
+
conn.execute(
|
|
238
|
+
f"UPDATE pdm_signatures SET {set_clause} WHERE id = ?",
|
|
239
|
+
values,
|
|
240
|
+
)
|
|
241
|
+
conn.commit()
|
|
242
|
+
|
|
243
|
+
def delete(self, memory_id: str, user: str = "default") -> None:
|
|
244
|
+
self._conn().execute(
|
|
245
|
+
"DELETE FROM pdm_signatures WHERE id = ? AND user = ?",
|
|
246
|
+
(memory_id, user),
|
|
247
|
+
)
|
|
248
|
+
self._conn().commit()
|
|
249
|
+
|
|
250
|
+
def list(
|
|
251
|
+
self,
|
|
252
|
+
user: str = "default",
|
|
253
|
+
limit: int = 100,
|
|
254
|
+
min_pressure: float = 0.0,
|
|
255
|
+
drawer: Optional[str] = None,
|
|
256
|
+
) -> List[SignatureRecord]:
|
|
257
|
+
query = (
|
|
258
|
+
"SELECT * FROM pdm_signatures WHERE user = ? AND p_magnitude >= ?"
|
|
259
|
+
)
|
|
260
|
+
params: list = [user, min_pressure]
|
|
261
|
+
if drawer:
|
|
262
|
+
query += " AND drawer_domain = ?"
|
|
263
|
+
params.append(drawer)
|
|
264
|
+
query += " ORDER BY p_magnitude DESC LIMIT ?"
|
|
265
|
+
params.append(limit)
|
|
266
|
+
|
|
267
|
+
rows = self._conn().execute(query, params).fetchall()
|
|
268
|
+
return [self._row_to_record(r) for r in rows]
|
|
269
|
+
|
|
270
|
+
def list_drawers(self, user: str = "default") -> List[DrawerInfo]:
|
|
271
|
+
rows = self._conn().execute(
|
|
272
|
+
"""
|
|
273
|
+
SELECT
|
|
274
|
+
d.domain,
|
|
275
|
+
d.description,
|
|
276
|
+
COUNT(s.id) AS sig_count,
|
|
277
|
+
AVG(s.p_magnitude) AS avg_pressure
|
|
278
|
+
FROM pdm_drawers d
|
|
279
|
+
LEFT JOIN pdm_signatures s
|
|
280
|
+
ON s.drawer_domain = d.domain AND s.user = d.user
|
|
281
|
+
WHERE d.user = ?
|
|
282
|
+
GROUP BY d.domain, d.description
|
|
283
|
+
ORDER BY d.domain
|
|
284
|
+
""",
|
|
285
|
+
(user,),
|
|
286
|
+
).fetchall()
|
|
287
|
+
return [
|
|
288
|
+
DrawerInfo(
|
|
289
|
+
domain=r["domain"],
|
|
290
|
+
signature_count=r["sig_count"] or 0,
|
|
291
|
+
avg_pressure=round(r["avg_pressure"] or 0.0, 2),
|
|
292
|
+
description=r["description"] or "",
|
|
293
|
+
)
|
|
294
|
+
for r in rows
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
def count(self, user: str = "default") -> int:
|
|
298
|
+
row = self._conn().execute(
|
|
299
|
+
"SELECT COUNT(*) FROM pdm_signatures WHERE user = ?", (user,)
|
|
300
|
+
).fetchone()
|
|
301
|
+
return row[0] if row else 0
|
|
302
|
+
|
|
303
|
+
# ------------------------------------------------------------------
|
|
304
|
+
# Private helpers
|
|
305
|
+
# ------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def _row_to_record(row: sqlite3.Row) -> SignatureRecord:
|
|
309
|
+
return SignatureRecord(
|
|
310
|
+
id=row["id"],
|
|
311
|
+
user=row["user"],
|
|
312
|
+
compressed_fact=row["compressed_fact"],
|
|
313
|
+
source=row["source"],
|
|
314
|
+
p_magnitude=row["p_magnitude"],
|
|
315
|
+
t_persistence=row["t_persistence"],
|
|
316
|
+
phase_privilege=row["phase_privilege"],
|
|
317
|
+
effective_spike=row["effective_spike"],
|
|
318
|
+
intent_tags=json.loads(row["intent_tags"] or "[]"),
|
|
319
|
+
question_regime=row["question_regime"],
|
|
320
|
+
domain=row["domain"],
|
|
321
|
+
drawer_domain=row["drawer_domain"],
|
|
322
|
+
retrieval_count=row["retrieval_count"],
|
|
323
|
+
last_retrieved=_parse_dt(row["last_retrieved"]),
|
|
324
|
+
created_at=_parse_dt(row["created_at"]),
|
|
325
|
+
validation_prediction_total=row["validation_prediction_total"],
|
|
326
|
+
validation_prediction_correct=row["validation_prediction_correct"],
|
|
327
|
+
decay_rate=row["decay_rate"],
|
|
328
|
+
t_deadline=_parse_dt(row["t_deadline"]),
|
|
329
|
+
urgency_rate=row["urgency_rate"],
|
|
330
|
+
metadata=json.loads(row["metadata"] or "{}"),
|
|
331
|
+
)
|