prismlang 0.1.0__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.
- prismlang/__init__.py +88 -0
- prismlang/checkpointer.py +547 -0
- prismlang/config.py +17 -0
- prismlang/encoder.py +129 -0
- prismlang/envelope.py +18 -0
- prismlang/exceptions.py +158 -0
- prismlang/middleware.py +102 -0
- prismlang/projector.py +136 -0
- prismlang/state.py +29 -0
- prismlang/taxonomy.py +100 -0
- prismlang/translator.py +84 -0
- prismlang-0.1.0.dist-info/METADATA +481 -0
- prismlang-0.1.0.dist-info/RECORD +15 -0
- prismlang-0.1.0.dist-info/WHEEL +4 -0
- prismlang-0.1.0.dist-info/licenses/LICENSE +94 -0
prismlang/__init__.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""PrismLang — Deterministic Vector Language Protocol for Multi-Agent AI Orchestration.
|
|
2
|
+
|
|
3
|
+
Public API
|
|
4
|
+
----------
|
|
5
|
+
from prismlang import (
|
|
6
|
+
PrismState,
|
|
7
|
+
PrismEnvelope,
|
|
8
|
+
PrismProjector,
|
|
9
|
+
TaxonomyConfig,
|
|
10
|
+
Category,
|
|
11
|
+
prism_node,
|
|
12
|
+
BoundaryTranslator,
|
|
13
|
+
JsonFileCheckpointer,
|
|
14
|
+
PostgresCheckpointer,
|
|
15
|
+
)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .config import DEFAULT_ALPHA, DEFAULT_K, EMBED_DIM
|
|
19
|
+
from .envelope import PrismEnvelope
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
PrismLangError,
|
|
22
|
+
EncoderError,
|
|
23
|
+
ModelDownloadError,
|
|
24
|
+
ModelNotFoundError,
|
|
25
|
+
TokenizerNotFoundError,
|
|
26
|
+
TaxonomyError,
|
|
27
|
+
DuplicateCategoryError,
|
|
28
|
+
UnknownCategoryError,
|
|
29
|
+
EmptyTaxonomyError,
|
|
30
|
+
ProjectionError,
|
|
31
|
+
ZeroVectorError,
|
|
32
|
+
DimensionMismatchError,
|
|
33
|
+
CheckpointerError,
|
|
34
|
+
CheckpointerConnectionError,
|
|
35
|
+
CheckpointerSchemaError,
|
|
36
|
+
TenantError,
|
|
37
|
+
MissingTenantError,
|
|
38
|
+
)
|
|
39
|
+
from .state import PrismState
|
|
40
|
+
from .taxonomy import Category, TaxonomyConfig
|
|
41
|
+
from .projector import PrismProjector
|
|
42
|
+
from .middleware import prism_node, async_prism_node
|
|
43
|
+
from .translator import BoundaryTranslator
|
|
44
|
+
from .checkpointer import (
|
|
45
|
+
JsonFileCheckpointer,
|
|
46
|
+
PostgresCheckpointer,
|
|
47
|
+
AsyncJsonFileCheckpointer,
|
|
48
|
+
AsyncPostgresCheckpointer,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__version__ = "0.1.0"
|
|
52
|
+
__author__ = "Amin Parva / Insight IT Solutions LLC"
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"PrismState",
|
|
56
|
+
"PrismEnvelope",
|
|
57
|
+
"PrismProjector",
|
|
58
|
+
"TaxonomyConfig",
|
|
59
|
+
"Category",
|
|
60
|
+
"prism_node",
|
|
61
|
+
"BoundaryTranslator",
|
|
62
|
+
"JsonFileCheckpointer",
|
|
63
|
+
"PostgresCheckpointer",
|
|
64
|
+
"AsyncJsonFileCheckpointer",
|
|
65
|
+
"AsyncPostgresCheckpointer",
|
|
66
|
+
"async_prism_node",
|
|
67
|
+
"DEFAULT_ALPHA",
|
|
68
|
+
"DEFAULT_K",
|
|
69
|
+
"EMBED_DIM",
|
|
70
|
+
# Exceptions
|
|
71
|
+
"PrismLangError",
|
|
72
|
+
"EncoderError",
|
|
73
|
+
"ModelDownloadError",
|
|
74
|
+
"ModelNotFoundError",
|
|
75
|
+
"TokenizerNotFoundError",
|
|
76
|
+
"TaxonomyError",
|
|
77
|
+
"DuplicateCategoryError",
|
|
78
|
+
"UnknownCategoryError",
|
|
79
|
+
"EmptyTaxonomyError",
|
|
80
|
+
"ProjectionError",
|
|
81
|
+
"ZeroVectorError",
|
|
82
|
+
"DimensionMismatchError",
|
|
83
|
+
"CheckpointerError",
|
|
84
|
+
"CheckpointerConnectionError",
|
|
85
|
+
"CheckpointerSchemaError",
|
|
86
|
+
"TenantError",
|
|
87
|
+
"MissingTenantError",
|
|
88
|
+
]
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""PrismLang checkpointers for persisting PrismState across graph runs.
|
|
2
|
+
|
|
3
|
+
Two backends are provided:
|
|
4
|
+
|
|
5
|
+
JsonFileCheckpointer — zero extra dependencies, writes one JSON file per
|
|
6
|
+
checkpoint under a local directory. Good for dev/testing.
|
|
7
|
+
|
|
8
|
+
PostgresCheckpointer — stores envelope sequences in a JSONB column. Requires
|
|
9
|
+
psycopg2-binary. Can share the same Postgres instance as
|
|
10
|
+
PrismRAG by pointing DATABASE_URL at the same DSN.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import uuid
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, Iterator, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver, Checkpoint, CheckpointMetadata
|
|
22
|
+
from langgraph.checkpoint.base import CheckpointTuple
|
|
23
|
+
|
|
24
|
+
from .exceptions import CheckpointerConnectionError, CheckpointerSchemaError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ------------------------------------------------------------------ #
|
|
28
|
+
# JSON file backend #
|
|
29
|
+
# ------------------------------------------------------------------ #
|
|
30
|
+
|
|
31
|
+
class JsonFileCheckpointer(BaseCheckpointSaver):
|
|
32
|
+
"""Persists checkpoints as JSON files under a local directory.
|
|
33
|
+
|
|
34
|
+
Directory layout:
|
|
35
|
+
{root}/{thread_id}/{checkpoint_id}.json
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, root: str = ".prismlang_checkpoints") -> None:
|
|
39
|
+
super().__init__()
|
|
40
|
+
self.root = Path(root)
|
|
41
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
def _path(self, thread_id: str, checkpoint_id: str) -> Path:
|
|
44
|
+
thread_dir = self.root / thread_id
|
|
45
|
+
thread_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
return thread_dir / f"{checkpoint_id}.json"
|
|
47
|
+
|
|
48
|
+
def get_tuple(self, config: Dict[str, Any]) -> Optional[CheckpointTuple]:
|
|
49
|
+
thread_id = config["configurable"]["thread_id"]
|
|
50
|
+
checkpoint_id = config["configurable"].get("checkpoint_id")
|
|
51
|
+
|
|
52
|
+
if checkpoint_id:
|
|
53
|
+
p = self._path(thread_id, checkpoint_id)
|
|
54
|
+
if not p.exists():
|
|
55
|
+
return None
|
|
56
|
+
data = json.loads(p.read_text())
|
|
57
|
+
else:
|
|
58
|
+
# Return the latest checkpoint for this thread
|
|
59
|
+
thread_dir = self.root / thread_id
|
|
60
|
+
if not thread_dir.exists():
|
|
61
|
+
return None
|
|
62
|
+
files = sorted(thread_dir.glob("*.json"))
|
|
63
|
+
if not files:
|
|
64
|
+
return None
|
|
65
|
+
data = json.loads(files[-1].read_text())
|
|
66
|
+
|
|
67
|
+
return CheckpointTuple(
|
|
68
|
+
config=config,
|
|
69
|
+
checkpoint=data["checkpoint"],
|
|
70
|
+
metadata=data.get("metadata", {}),
|
|
71
|
+
parent_config=data.get("parent_config"),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def list(
|
|
75
|
+
self,
|
|
76
|
+
config: Optional[Dict[str, Any]],
|
|
77
|
+
*,
|
|
78
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
79
|
+
before: Optional[Dict[str, Any]] = None,
|
|
80
|
+
limit: Optional[int] = None,
|
|
81
|
+
) -> Iterator[CheckpointTuple]:
|
|
82
|
+
thread_id = (config or {}).get("configurable", {}).get("thread_id", "")
|
|
83
|
+
thread_dir = self.root / thread_id
|
|
84
|
+
if not thread_dir.exists():
|
|
85
|
+
return
|
|
86
|
+
files = sorted(thread_dir.glob("*.json"), reverse=True)
|
|
87
|
+
count = 0
|
|
88
|
+
for f in files:
|
|
89
|
+
if limit and count >= limit:
|
|
90
|
+
break
|
|
91
|
+
data = json.loads(f.read_text())
|
|
92
|
+
yield CheckpointTuple(
|
|
93
|
+
config={"configurable": {"thread_id": thread_id, "checkpoint_id": f.stem}},
|
|
94
|
+
checkpoint=data["checkpoint"],
|
|
95
|
+
metadata=data.get("metadata", {}),
|
|
96
|
+
parent_config=data.get("parent_config"),
|
|
97
|
+
)
|
|
98
|
+
count += 1
|
|
99
|
+
|
|
100
|
+
def put(
|
|
101
|
+
self,
|
|
102
|
+
config: Dict[str, Any],
|
|
103
|
+
checkpoint: Checkpoint,
|
|
104
|
+
metadata: CheckpointMetadata,
|
|
105
|
+
new_versions: Dict[str, Any],
|
|
106
|
+
) -> Dict[str, Any]:
|
|
107
|
+
thread_id = config["configurable"]["thread_id"]
|
|
108
|
+
checkpoint_id = checkpoint["id"]
|
|
109
|
+
p = self._path(thread_id, checkpoint_id)
|
|
110
|
+
p.write_text(
|
|
111
|
+
json.dumps(
|
|
112
|
+
{
|
|
113
|
+
"checkpoint": checkpoint,
|
|
114
|
+
"metadata": metadata,
|
|
115
|
+
"parent_config": config,
|
|
116
|
+
},
|
|
117
|
+
default=str,
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
return {**config, "configurable": {**config["configurable"], "checkpoint_id": checkpoint_id}}
|
|
121
|
+
|
|
122
|
+
def put_writes(
|
|
123
|
+
self,
|
|
124
|
+
config: Dict[str, Any],
|
|
125
|
+
writes: list,
|
|
126
|
+
task_id: str,
|
|
127
|
+
) -> None:
|
|
128
|
+
# For this simple backend writes are committed atomically in put()
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------ #
|
|
133
|
+
# PostgreSQL backend #
|
|
134
|
+
# ------------------------------------------------------------------ #
|
|
135
|
+
|
|
136
|
+
_CREATE_TABLE = """
|
|
137
|
+
CREATE TABLE IF NOT EXISTS prismlang_checkpoint (
|
|
138
|
+
thread_id TEXT NOT NULL,
|
|
139
|
+
checkpoint_id TEXT NOT NULL,
|
|
140
|
+
tenant_id TEXT,
|
|
141
|
+
checkpoint JSONB NOT NULL,
|
|
142
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
143
|
+
parent_config JSONB,
|
|
144
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
145
|
+
PRIMARY KEY (thread_id, checkpoint_id)
|
|
146
|
+
);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS ix_prismlang_cp_thread
|
|
148
|
+
ON prismlang_checkpoint (thread_id, created_at DESC);
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PostgresCheckpointer(BaseCheckpointSaver):
|
|
153
|
+
"""Persists PrismState checkpoints in a PostgreSQL JSONB column.
|
|
154
|
+
|
|
155
|
+
Compatible with PrismRAG's database — point DATABASE_URL at the same DSN.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
dsn: PostgreSQL DSN string, e.g. "postgresql://user:pass@localhost/db".
|
|
159
|
+
Falls back to the DATABASE_URL environment variable.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(self, dsn: Optional[str] = None) -> None:
|
|
163
|
+
super().__init__()
|
|
164
|
+
try:
|
|
165
|
+
import psycopg2
|
|
166
|
+
except ImportError as exc:
|
|
167
|
+
raise ImportError(
|
|
168
|
+
"psycopg2-binary is required for PostgresCheckpointer. "
|
|
169
|
+
"Install it with: pip install psycopg2-binary"
|
|
170
|
+
) from exc
|
|
171
|
+
|
|
172
|
+
self._dsn = dsn or os.environ.get("DATABASE_URL")
|
|
173
|
+
if not self._dsn:
|
|
174
|
+
raise CheckpointerConnectionError(
|
|
175
|
+
"postgresql", cause=ValueError("No DSN provided. Pass dsn= or set the DATABASE_URL environment variable.")
|
|
176
|
+
)
|
|
177
|
+
self._ensure_schema()
|
|
178
|
+
|
|
179
|
+
def _connect(self):
|
|
180
|
+
import psycopg2
|
|
181
|
+
try:
|
|
182
|
+
return psycopg2.connect(self._dsn)
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
raise CheckpointerConnectionError("postgresql", cause=exc) from exc
|
|
185
|
+
|
|
186
|
+
def _ensure_schema(self) -> None:
|
|
187
|
+
try:
|
|
188
|
+
conn = self._connect()
|
|
189
|
+
with conn.cursor() as cur:
|
|
190
|
+
cur.execute(_CREATE_TABLE)
|
|
191
|
+
conn.commit()
|
|
192
|
+
conn.close()
|
|
193
|
+
except CheckpointerConnectionError:
|
|
194
|
+
raise
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
raise CheckpointerSchemaError(
|
|
197
|
+
f"Failed to create prismlang_checkpoint schema: {exc}"
|
|
198
|
+
) from exc
|
|
199
|
+
|
|
200
|
+
def get_tuple(self, config: Dict[str, Any]) -> Optional[CheckpointTuple]:
|
|
201
|
+
thread_id = config["configurable"]["thread_id"]
|
|
202
|
+
checkpoint_id = config["configurable"].get("checkpoint_id")
|
|
203
|
+
|
|
204
|
+
conn = self._connect()
|
|
205
|
+
try:
|
|
206
|
+
with conn.cursor() as cur:
|
|
207
|
+
if checkpoint_id:
|
|
208
|
+
cur.execute(
|
|
209
|
+
"SELECT checkpoint, metadata, parent_config FROM prismlang_checkpoint "
|
|
210
|
+
"WHERE thread_id=%s AND checkpoint_id=%s",
|
|
211
|
+
(thread_id, checkpoint_id),
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
cur.execute(
|
|
215
|
+
"SELECT checkpoint, metadata, parent_config FROM prismlang_checkpoint "
|
|
216
|
+
"WHERE thread_id=%s ORDER BY created_at DESC LIMIT 1",
|
|
217
|
+
(thread_id,),
|
|
218
|
+
)
|
|
219
|
+
row = cur.fetchone()
|
|
220
|
+
finally:
|
|
221
|
+
conn.close()
|
|
222
|
+
|
|
223
|
+
if not row:
|
|
224
|
+
return None
|
|
225
|
+
checkpoint, metadata, parent_config = row
|
|
226
|
+
return CheckpointTuple(
|
|
227
|
+
config=config,
|
|
228
|
+
checkpoint=checkpoint,
|
|
229
|
+
metadata=metadata or {},
|
|
230
|
+
parent_config=parent_config,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def list(
|
|
234
|
+
self,
|
|
235
|
+
config: Optional[Dict[str, Any]],
|
|
236
|
+
*,
|
|
237
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
238
|
+
before: Optional[Dict[str, Any]] = None,
|
|
239
|
+
limit: Optional[int] = None,
|
|
240
|
+
) -> Iterator[CheckpointTuple]:
|
|
241
|
+
thread_id = (config or {}).get("configurable", {}).get("thread_id", "")
|
|
242
|
+
q = (
|
|
243
|
+
"SELECT thread_id, checkpoint_id, checkpoint, metadata, parent_config "
|
|
244
|
+
"FROM prismlang_checkpoint WHERE thread_id=%s ORDER BY created_at DESC"
|
|
245
|
+
)
|
|
246
|
+
params = [thread_id]
|
|
247
|
+
if limit:
|
|
248
|
+
q += " LIMIT %s"
|
|
249
|
+
params.append(limit)
|
|
250
|
+
conn = self._connect()
|
|
251
|
+
try:
|
|
252
|
+
with conn.cursor() as cur:
|
|
253
|
+
cur.execute(q, params)
|
|
254
|
+
for row in cur.fetchall():
|
|
255
|
+
tid, cid, checkpoint, metadata, parent_config = row
|
|
256
|
+
yield CheckpointTuple(
|
|
257
|
+
config={"configurable": {"thread_id": tid, "checkpoint_id": cid}},
|
|
258
|
+
checkpoint=checkpoint,
|
|
259
|
+
metadata=metadata or {},
|
|
260
|
+
parent_config=parent_config,
|
|
261
|
+
)
|
|
262
|
+
finally:
|
|
263
|
+
conn.close()
|
|
264
|
+
|
|
265
|
+
def put(
|
|
266
|
+
self,
|
|
267
|
+
config: Dict[str, Any],
|
|
268
|
+
checkpoint: Checkpoint,
|
|
269
|
+
metadata: CheckpointMetadata,
|
|
270
|
+
new_versions: Dict[str, Any],
|
|
271
|
+
) -> Dict[str, Any]:
|
|
272
|
+
thread_id = config["configurable"]["thread_id"]
|
|
273
|
+
checkpoint_id = checkpoint["id"]
|
|
274
|
+
tenant_id = checkpoint.get("channel_values", {}).get("tenant_id")
|
|
275
|
+
conn = self._connect()
|
|
276
|
+
try:
|
|
277
|
+
with conn.cursor() as cur:
|
|
278
|
+
cur.execute(
|
|
279
|
+
"""
|
|
280
|
+
INSERT INTO prismlang_checkpoint
|
|
281
|
+
(thread_id, checkpoint_id, tenant_id, checkpoint, metadata, parent_config)
|
|
282
|
+
VALUES (%s, %s, %s, %s::jsonb, %s::jsonb, %s::jsonb)
|
|
283
|
+
ON CONFLICT (thread_id, checkpoint_id) DO UPDATE
|
|
284
|
+
SET checkpoint = EXCLUDED.checkpoint,
|
|
285
|
+
metadata = EXCLUDED.metadata
|
|
286
|
+
""",
|
|
287
|
+
(
|
|
288
|
+
thread_id,
|
|
289
|
+
checkpoint_id,
|
|
290
|
+
tenant_id,
|
|
291
|
+
json.dumps(checkpoint, default=str),
|
|
292
|
+
json.dumps(dict(metadata), default=str),
|
|
293
|
+
json.dumps(config, default=str),
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
conn.commit()
|
|
297
|
+
finally:
|
|
298
|
+
conn.close()
|
|
299
|
+
return {**config, "configurable": {**config["configurable"], "checkpoint_id": checkpoint_id}}
|
|
300
|
+
|
|
301
|
+
def put_writes(
|
|
302
|
+
self,
|
|
303
|
+
config: Dict[str, Any],
|
|
304
|
+
writes: list,
|
|
305
|
+
task_id: str,
|
|
306
|
+
) -> None:
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ------------------------------------------------------------------ #
|
|
311
|
+
# Async JSON file backend #
|
|
312
|
+
# ------------------------------------------------------------------ #
|
|
313
|
+
|
|
314
|
+
class AsyncJsonFileCheckpointer(BaseCheckpointSaver):
|
|
315
|
+
"""Async variant of JsonFileCheckpointer using aiofiles.
|
|
316
|
+
|
|
317
|
+
Requires: pip install prismlang[async-files]
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
def __init__(self, root: str = ".prismlang_checkpoints") -> None:
|
|
321
|
+
super().__init__()
|
|
322
|
+
self.root = Path(root)
|
|
323
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
|
|
325
|
+
def _path(self, thread_id: str, checkpoint_id: str) -> Path:
|
|
326
|
+
thread_dir = self.root / thread_id
|
|
327
|
+
thread_dir.mkdir(parents=True, exist_ok=True)
|
|
328
|
+
return thread_dir / f"{checkpoint_id}.json"
|
|
329
|
+
|
|
330
|
+
def get_tuple(self, config: Dict[str, Any]) -> Optional[CheckpointTuple]:
|
|
331
|
+
import asyncio
|
|
332
|
+
loop = asyncio.new_event_loop()
|
|
333
|
+
try:
|
|
334
|
+
return loop.run_until_complete(self.aget_tuple(config))
|
|
335
|
+
finally:
|
|
336
|
+
loop.close()
|
|
337
|
+
|
|
338
|
+
async def aget_tuple(self, config: Dict[str, Any]) -> Optional[CheckpointTuple]:
|
|
339
|
+
try:
|
|
340
|
+
import aiofiles
|
|
341
|
+
except ImportError as exc:
|
|
342
|
+
raise ImportError(
|
|
343
|
+
"aiofiles is required for AsyncJsonFileCheckpointer. "
|
|
344
|
+
"Install with: pip install 'prismlang[async-files]'"
|
|
345
|
+
) from exc
|
|
346
|
+
|
|
347
|
+
thread_id = config["configurable"]["thread_id"]
|
|
348
|
+
checkpoint_id = config["configurable"].get("checkpoint_id")
|
|
349
|
+
|
|
350
|
+
if checkpoint_id:
|
|
351
|
+
p = self._path(thread_id, checkpoint_id)
|
|
352
|
+
if not p.exists():
|
|
353
|
+
return None
|
|
354
|
+
async with aiofiles.open(p) as f:
|
|
355
|
+
data = json.loads(await f.read())
|
|
356
|
+
else:
|
|
357
|
+
thread_dir = self.root / thread_id
|
|
358
|
+
if not thread_dir.exists():
|
|
359
|
+
return None
|
|
360
|
+
files = sorted(thread_dir.glob("*.json"))
|
|
361
|
+
if not files:
|
|
362
|
+
return None
|
|
363
|
+
async with aiofiles.open(files[-1]) as f:
|
|
364
|
+
data = json.loads(await f.read())
|
|
365
|
+
|
|
366
|
+
return CheckpointTuple(
|
|
367
|
+
config=config,
|
|
368
|
+
checkpoint=data["checkpoint"],
|
|
369
|
+
metadata=data.get("metadata", {}),
|
|
370
|
+
parent_config=data.get("parent_config"),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def list(self, config, *, filter=None, before=None, limit=None):
|
|
374
|
+
thread_id = (config or {}).get("configurable", {}).get("thread_id", "")
|
|
375
|
+
thread_dir = self.root / thread_id
|
|
376
|
+
if not thread_dir.exists():
|
|
377
|
+
return
|
|
378
|
+
files = sorted(thread_dir.glob("*.json"), reverse=True)
|
|
379
|
+
count = 0
|
|
380
|
+
for f in files:
|
|
381
|
+
if limit and count >= limit:
|
|
382
|
+
break
|
|
383
|
+
data = json.loads(f.read_text())
|
|
384
|
+
yield CheckpointTuple(
|
|
385
|
+
config={"configurable": {"thread_id": thread_id, "checkpoint_id": f.stem}},
|
|
386
|
+
checkpoint=data["checkpoint"],
|
|
387
|
+
metadata=data.get("metadata", {}),
|
|
388
|
+
parent_config=data.get("parent_config"),
|
|
389
|
+
)
|
|
390
|
+
count += 1
|
|
391
|
+
|
|
392
|
+
async def aput(
|
|
393
|
+
self,
|
|
394
|
+
config: Dict[str, Any],
|
|
395
|
+
checkpoint: Checkpoint,
|
|
396
|
+
metadata: CheckpointMetadata,
|
|
397
|
+
new_versions: Dict[str, Any],
|
|
398
|
+
) -> Dict[str, Any]:
|
|
399
|
+
try:
|
|
400
|
+
import aiofiles
|
|
401
|
+
except ImportError as exc:
|
|
402
|
+
raise ImportError(
|
|
403
|
+
"aiofiles is required for AsyncJsonFileCheckpointer. "
|
|
404
|
+
"Install with: pip install 'prismlang[async-files]'"
|
|
405
|
+
) from exc
|
|
406
|
+
|
|
407
|
+
thread_id = config["configurable"]["thread_id"]
|
|
408
|
+
checkpoint_id = checkpoint["id"]
|
|
409
|
+
p = self._path(thread_id, checkpoint_id)
|
|
410
|
+
content = json.dumps(
|
|
411
|
+
{"checkpoint": checkpoint, "metadata": metadata, "parent_config": config},
|
|
412
|
+
default=str,
|
|
413
|
+
)
|
|
414
|
+
async with aiofiles.open(p, "w") as f:
|
|
415
|
+
await f.write(content)
|
|
416
|
+
return {**config, "configurable": {**config["configurable"], "checkpoint_id": checkpoint_id}}
|
|
417
|
+
|
|
418
|
+
def put(self, config, checkpoint, metadata, new_versions):
|
|
419
|
+
import asyncio
|
|
420
|
+
loop = asyncio.new_event_loop()
|
|
421
|
+
try:
|
|
422
|
+
return loop.run_until_complete(self.aput(config, checkpoint, metadata, new_versions))
|
|
423
|
+
finally:
|
|
424
|
+
loop.close()
|
|
425
|
+
|
|
426
|
+
def put_writes(self, config, writes, task_id):
|
|
427
|
+
pass
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ------------------------------------------------------------------ #
|
|
431
|
+
# Async PostgreSQL backend #
|
|
432
|
+
# ------------------------------------------------------------------ #
|
|
433
|
+
|
|
434
|
+
class AsyncPostgresCheckpointer(BaseCheckpointSaver):
|
|
435
|
+
"""Async variant of PostgresCheckpointer using asyncpg.
|
|
436
|
+
|
|
437
|
+
Requires: pip install prismlang[async-postgres]
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
def __init__(self, dsn: Optional[str] = None) -> None:
|
|
441
|
+
super().__init__()
|
|
442
|
+
self._dsn = dsn or os.environ.get("DATABASE_URL")
|
|
443
|
+
if not self._dsn:
|
|
444
|
+
raise CheckpointerConnectionError(
|
|
445
|
+
"asyncpg", cause=ValueError("No DSN provided. Pass dsn= or set DATABASE_URL.")
|
|
446
|
+
)
|
|
447
|
+
self._pool = None
|
|
448
|
+
|
|
449
|
+
async def _get_pool(self):
|
|
450
|
+
if self._pool is None:
|
|
451
|
+
try:
|
|
452
|
+
import asyncpg
|
|
453
|
+
except ImportError as exc:
|
|
454
|
+
raise ImportError(
|
|
455
|
+
"asyncpg is required for AsyncPostgresCheckpointer. "
|
|
456
|
+
"Install with: pip install 'prismlang[async-postgres]'"
|
|
457
|
+
) from exc
|
|
458
|
+
try:
|
|
459
|
+
import asyncpg
|
|
460
|
+
self._pool = await asyncpg.create_pool(self._dsn)
|
|
461
|
+
async with self._pool.acquire() as conn:
|
|
462
|
+
await conn.execute(_CREATE_TABLE)
|
|
463
|
+
except Exception as exc:
|
|
464
|
+
raise CheckpointerConnectionError("asyncpg", cause=exc) from exc
|
|
465
|
+
return self._pool
|
|
466
|
+
|
|
467
|
+
async def aget_tuple(self, config: Dict[str, Any]) -> Optional[CheckpointTuple]:
|
|
468
|
+
pool = await self._get_pool()
|
|
469
|
+
thread_id = config["configurable"]["thread_id"]
|
|
470
|
+
checkpoint_id = config["configurable"].get("checkpoint_id")
|
|
471
|
+
|
|
472
|
+
async with pool.acquire() as conn:
|
|
473
|
+
if checkpoint_id:
|
|
474
|
+
row = await conn.fetchrow(
|
|
475
|
+
"SELECT checkpoint, metadata, parent_config FROM prismlang_checkpoint "
|
|
476
|
+
"WHERE thread_id=$1 AND checkpoint_id=$2",
|
|
477
|
+
thread_id, checkpoint_id,
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
row = await conn.fetchrow(
|
|
481
|
+
"SELECT checkpoint, metadata, parent_config FROM prismlang_checkpoint "
|
|
482
|
+
"WHERE thread_id=$1 ORDER BY created_at DESC LIMIT 1",
|
|
483
|
+
thread_id,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if not row:
|
|
487
|
+
return None
|
|
488
|
+
return CheckpointTuple(
|
|
489
|
+
config=config,
|
|
490
|
+
checkpoint=json.loads(row["checkpoint"]),
|
|
491
|
+
metadata=json.loads(row["metadata"] or "{}"),
|
|
492
|
+
parent_config=json.loads(row["parent_config"]) if row["parent_config"] else None,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def get_tuple(self, config):
|
|
496
|
+
import asyncio
|
|
497
|
+
loop = asyncio.new_event_loop()
|
|
498
|
+
try:
|
|
499
|
+
return loop.run_until_complete(self.aget_tuple(config))
|
|
500
|
+
finally:
|
|
501
|
+
loop.close()
|
|
502
|
+
|
|
503
|
+
def list(self, config, *, filter=None, before=None, limit=None):
|
|
504
|
+
return iter([]) # use alist() for async listing
|
|
505
|
+
|
|
506
|
+
async def aput(
|
|
507
|
+
self,
|
|
508
|
+
config: Dict[str, Any],
|
|
509
|
+
checkpoint: Checkpoint,
|
|
510
|
+
metadata: CheckpointMetadata,
|
|
511
|
+
new_versions: Dict[str, Any],
|
|
512
|
+
) -> Dict[str, Any]:
|
|
513
|
+
pool = await self._get_pool()
|
|
514
|
+
thread_id = config["configurable"]["thread_id"]
|
|
515
|
+
checkpoint_id = checkpoint["id"]
|
|
516
|
+
tenant_id = checkpoint.get("channel_values", {}).get("tenant_id")
|
|
517
|
+
|
|
518
|
+
async with pool.acquire() as conn:
|
|
519
|
+
await conn.execute(
|
|
520
|
+
"""
|
|
521
|
+
INSERT INTO prismlang_checkpoint
|
|
522
|
+
(thread_id, checkpoint_id, tenant_id, checkpoint, metadata, parent_config)
|
|
523
|
+
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, $6::jsonb)
|
|
524
|
+
ON CONFLICT (thread_id, checkpoint_id) DO UPDATE
|
|
525
|
+
SET checkpoint = EXCLUDED.checkpoint,
|
|
526
|
+
metadata = EXCLUDED.metadata
|
|
527
|
+
""",
|
|
528
|
+
thread_id,
|
|
529
|
+
checkpoint_id,
|
|
530
|
+
tenant_id,
|
|
531
|
+
json.dumps(checkpoint, default=str),
|
|
532
|
+
json.dumps(dict(metadata), default=str),
|
|
533
|
+
json.dumps(config, default=str),
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
return {**config, "configurable": {**config["configurable"], "checkpoint_id": checkpoint_id}}
|
|
537
|
+
|
|
538
|
+
def put(self, config, checkpoint, metadata, new_versions):
|
|
539
|
+
import asyncio
|
|
540
|
+
loop = asyncio.new_event_loop()
|
|
541
|
+
try:
|
|
542
|
+
return loop.run_until_complete(self.aput(config, checkpoint, metadata, new_versions))
|
|
543
|
+
finally:
|
|
544
|
+
loop.close()
|
|
545
|
+
|
|
546
|
+
def put_writes(self, config, writes, task_id):
|
|
547
|
+
pass
|
prismlang/config.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Global constants for PrismLang."""
|
|
2
|
+
|
|
3
|
+
# ONNX encoder output dimension (all-MiniLM-L6-v2)
|
|
4
|
+
EMBED_DIM: int = 384
|
|
5
|
+
|
|
6
|
+
# Default Johnson-Lindenstrauss target dimension
|
|
7
|
+
DEFAULT_K: int = 64
|
|
8
|
+
|
|
9
|
+
# Spherical blend weight — how strongly the category direction pulls the vector
|
|
10
|
+
DEFAULT_ALPHA: float = 0.3
|
|
11
|
+
|
|
12
|
+
# HuggingFace model repo for the ONNX encoder
|
|
13
|
+
HF_MODEL_REPO: str = "sentence-transformers/all-MiniLM-L6-v2"
|
|
14
|
+
|
|
15
|
+
# Local cache directory for downloaded models
|
|
16
|
+
import os
|
|
17
|
+
MODEL_CACHE_DIR: str = os.path.join(os.path.expanduser("~"), ".prismlang", "models")
|