windlass 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.
- windlass/__init__.py +243 -0
- windlass/_version.py +7 -0
- windlass/agent/__init__.py +44 -0
- windlass/agent/builder.py +709 -0
- windlass/agent/checkpoint.py +330 -0
- windlass/agent/graph.py +410 -0
- windlass/agent/runtime.py +633 -0
- windlass/agent/supervisor.py +373 -0
- windlass/api.py +469 -0
- windlass/cli.py +389 -0
- windlass/core/__init__.py +145 -0
- windlass/core/cache.py +336 -0
- windlass/core/concurrency.py +341 -0
- windlass/core/config.py +461 -0
- windlass/core/container.py +383 -0
- windlass/core/exceptions.py +465 -0
- windlass/core/lazy.py +208 -0
- windlass/core/logging.py +204 -0
- windlass/core/registry.py +626 -0
- windlass/core/retry.py +288 -0
- windlass/core/text.py +505 -0
- windlass/core/types.py +671 -0
- windlass/core/vectors.py +306 -0
- windlass/interfaces/__init__.py +81 -0
- windlass/interfaces/base.py +191 -0
- windlass/interfaces/chunker.py +279 -0
- windlass/interfaces/embedding.py +259 -0
- windlass/interfaces/evaluator.py +264 -0
- windlass/interfaces/guardrail.py +289 -0
- windlass/interfaces/llm.py +410 -0
- windlass/interfaces/loader.py +349 -0
- windlass/interfaces/mcp.py +312 -0
- windlass/interfaces/memory.py +178 -0
- windlass/interfaces/preprocessor.py +195 -0
- windlass/interfaces/retriever.py +245 -0
- windlass/interfaces/tool.py +339 -0
- windlass/interfaces/tracer.py +366 -0
- windlass/interfaces/vectordb.py +340 -0
- windlass/providers/__init__.py +642 -0
- windlass/providers/chunkers/__init__.py +7 -0
- windlass/providers/chunkers/hierarchical.py +254 -0
- windlass/providers/chunkers/recursive.py +260 -0
- windlass/providers/chunkers/semantic.py +204 -0
- windlass/providers/chunkers/structural.py +323 -0
- windlass/providers/embeddings/__init__.py +7 -0
- windlass/providers/embeddings/hash.py +127 -0
- windlass/providers/embeddings/hf_inference.py +284 -0
- windlass/providers/embeddings/huggingface.py +187 -0
- windlass/providers/embeddings/openai.py +126 -0
- windlass/providers/evaluation/__init__.py +7 -0
- windlass/providers/evaluation/builtin.py +428 -0
- windlass/providers/evaluation/external.py +356 -0
- windlass/providers/guardrails/__init__.py +7 -0
- windlass/providers/guardrails/nemo.py +241 -0
- windlass/providers/guardrails/rules.py +241 -0
- windlass/providers/llm/__init__.py +9 -0
- windlass/providers/llm/anthropic.py +355 -0
- windlass/providers/llm/fake.py +271 -0
- windlass/providers/llm/gemini.py +291 -0
- windlass/providers/llm/groq.py +361 -0
- windlass/providers/llm/ollama.py +313 -0
- windlass/providers/llm/openai.py +424 -0
- windlass/providers/loaders/__init__.py +7 -0
- windlass/providers/loaders/media.py +296 -0
- windlass/providers/loaders/office.py +511 -0
- windlass/providers/loaders/text.py +507 -0
- windlass/providers/loaders/web.py +452 -0
- windlass/providers/mcp/__init__.py +7 -0
- windlass/providers/mcp/fastmcp.py +635 -0
- windlass/providers/memory/__init__.py +7 -0
- windlass/providers/memory/conversation.py +322 -0
- windlass/providers/memory/longterm.py +304 -0
- windlass/providers/observability/__init__.py +7 -0
- windlass/providers/observability/console.py +259 -0
- windlass/providers/observability/multi.py +160 -0
- windlass/providers/observability/platforms.py +424 -0
- windlass/providers/preprocessors/__init__.py +7 -0
- windlass/providers/preprocessors/clean.py +306 -0
- windlass/providers/preprocessors/dedup.py +321 -0
- windlass/providers/preprocessors/enrich.py +303 -0
- windlass/providers/preprocessors/privacy.py +307 -0
- windlass/providers/retrievers/__init__.py +7 -0
- windlass/providers/retrievers/bm25.py +365 -0
- windlass/providers/retrievers/contextual.py +326 -0
- windlass/providers/retrievers/hybrid.py +255 -0
- windlass/providers/retrievers/vector.py +247 -0
- windlass/providers/vectordb/__init__.py +7 -0
- windlass/providers/vectordb/chroma.py +382 -0
- windlass/providers/vectordb/faiss.py +445 -0
- windlass/providers/vectordb/memory.py +361 -0
- windlass/providers/vectordb/pinecone.py +442 -0
- windlass/py.typed +1 -0
- windlass/rag/__init__.py +34 -0
- windlass/rag/builder.py +739 -0
- windlass/rag/loading.py +250 -0
- windlass/rag/pipeline.py +854 -0
- windlass/testing.py +376 -0
- windlass/tools/__init__.py +458 -0
- windlass/tools/schema.py +417 -0
- windlass-0.1.0.dist-info/METADATA +679 -0
- windlass-0.1.0.dist-info/RECORD +104 -0
- windlass-0.1.0.dist-info/WHEEL +4 -0
- windlass-0.1.0.dist-info/entry_points.txt +2 -0
- windlass-0.1.0.dist-info/licenses/LICENSE +205 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Checkpointing — durable agent state.
|
|
2
|
+
|
|
3
|
+
An agent run is a sequence of model calls and tool executions. Checkpointing
|
|
4
|
+
snapshots that state after every step, which buys three things:
|
|
5
|
+
|
|
6
|
+
* **Resumability.** A run interrupted for human approval, or by a crash, picks
|
|
7
|
+
up where it stopped instead of starting over (and re-paying for every token
|
|
8
|
+
already spent).
|
|
9
|
+
* **Multi-turn conversations.** A ``thread_id`` maps to accumulated state, so
|
|
10
|
+
the next message continues the same run.
|
|
11
|
+
* **Time travel.** Every step is retained, so you can inspect exactly what the
|
|
12
|
+
agent believed at step 3 when debugging why step 4 went wrong.
|
|
13
|
+
|
|
14
|
+
Two backends ship: in-memory (fast, per-process) and SQLite (durable, shared
|
|
15
|
+
between processes, stdlib-only).
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> saver = MemoryCheckpointer()
|
|
19
|
+
>>> saver.put("thread-1", {"step": 1, "messages": []})
|
|
20
|
+
>>> saver.get("thread-1")["step"]
|
|
21
|
+
1
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import abc
|
|
27
|
+
import json
|
|
28
|
+
import sqlite3
|
|
29
|
+
import threading
|
|
30
|
+
import time
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from windlass.core.exceptions import SerializationError
|
|
35
|
+
from windlass.core.logging import get_logger
|
|
36
|
+
from windlass.core.registry import register
|
|
37
|
+
|
|
38
|
+
__all__ = ["Checkpointer", "MemoryCheckpointer", "SQLiteCheckpointer"]
|
|
39
|
+
|
|
40
|
+
_log = get_logger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Checkpointer(abc.ABC):
|
|
44
|
+
"""Stores and retrieves agent state by thread.
|
|
45
|
+
|
|
46
|
+
Implementations must be safe to use from multiple threads, since one agent
|
|
47
|
+
instance typically serves many concurrent conversations.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@abc.abstractmethod
|
|
51
|
+
def put(self, thread_id: str, state: dict[str, Any]) -> None:
|
|
52
|
+
"""Save a state snapshot.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
thread_id: Conversation / run identifier.
|
|
56
|
+
state: JSON-serialisable state.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
SerializationError: When the state cannot be stored.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
@abc.abstractmethod
|
|
63
|
+
def get(self, thread_id: str) -> dict[str, Any] | None:
|
|
64
|
+
"""Return the latest snapshot for a thread, or ``None``."""
|
|
65
|
+
|
|
66
|
+
@abc.abstractmethod
|
|
67
|
+
def history(self, thread_id: str, limit: int = 20) -> list[dict[str, Any]]:
|
|
68
|
+
"""Return recent snapshots, newest first.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
thread_id: Conversation identifier.
|
|
72
|
+
limit: Maximum snapshots to return.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Snapshots, newest first.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
@abc.abstractmethod
|
|
79
|
+
def delete(self, thread_id: str) -> None:
|
|
80
|
+
"""Discard every snapshot for a thread."""
|
|
81
|
+
|
|
82
|
+
def threads(self) -> list[str]:
|
|
83
|
+
"""Return the known thread ids."""
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@register.checkpointer(
|
|
88
|
+
"memory",
|
|
89
|
+
aliases=("inmemory", "default"),
|
|
90
|
+
description="In-process checkpoint store (no dependencies).",
|
|
91
|
+
)
|
|
92
|
+
class MemoryCheckpointer(Checkpointer):
|
|
93
|
+
"""Keeps checkpoints in a dict.
|
|
94
|
+
|
|
95
|
+
Fast and dependency-free, but per-process and lost on restart. Right for
|
|
96
|
+
tests, notebooks and single-process services; wrong for anything that needs
|
|
97
|
+
a resumed run to survive a deploy.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
max_history: Snapshots retained per thread. Older ones are dropped.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> saver = MemoryCheckpointer()
|
|
104
|
+
>>> saver.put("t", {"step": 1})
|
|
105
|
+
>>> saver.put("t", {"step": 2})
|
|
106
|
+
>>> saver.get("t")["step"], len(saver.history("t"))
|
|
107
|
+
(2, 2)
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, max_history: int = 50) -> None:
|
|
111
|
+
self.max_history = max_history
|
|
112
|
+
self._data: dict[str, list[dict[str, Any]]] = {}
|
|
113
|
+
self._lock = threading.RLock()
|
|
114
|
+
|
|
115
|
+
def put(self, thread_id: str, state: dict[str, Any]) -> None:
|
|
116
|
+
"""Append a snapshot, trimming the oldest beyond ``max_history``."""
|
|
117
|
+
with self._lock:
|
|
118
|
+
bucket = self._data.setdefault(thread_id, [])
|
|
119
|
+
bucket.append({**state, "_saved_at": time.time()})
|
|
120
|
+
if len(bucket) > self.max_history:
|
|
121
|
+
del bucket[: len(bucket) - self.max_history]
|
|
122
|
+
|
|
123
|
+
def get(self, thread_id: str) -> dict[str, Any] | None:
|
|
124
|
+
"""Return the newest snapshot, or ``None``."""
|
|
125
|
+
with self._lock:
|
|
126
|
+
bucket = self._data.get(thread_id)
|
|
127
|
+
return dict(bucket[-1]) if bucket else None
|
|
128
|
+
|
|
129
|
+
def history(self, thread_id: str, limit: int = 20) -> list[dict[str, Any]]:
|
|
130
|
+
"""Return recent snapshots, newest first."""
|
|
131
|
+
with self._lock:
|
|
132
|
+
bucket = self._data.get(thread_id, [])
|
|
133
|
+
return [dict(s) for s in reversed(bucket[-limit:])]
|
|
134
|
+
|
|
135
|
+
def delete(self, thread_id: str) -> None:
|
|
136
|
+
"""Discard a thread's snapshots."""
|
|
137
|
+
with self._lock:
|
|
138
|
+
self._data.pop(thread_id, None)
|
|
139
|
+
|
|
140
|
+
def threads(self) -> list[str]:
|
|
141
|
+
"""Return the known thread ids."""
|
|
142
|
+
with self._lock:
|
|
143
|
+
return sorted(self._data)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@register.checkpointer(
|
|
147
|
+
"sqlite",
|
|
148
|
+
aliases=("file", "durable"),
|
|
149
|
+
description="Durable checkpoint store backed by SQLite (stdlib only).",
|
|
150
|
+
)
|
|
151
|
+
class SQLiteCheckpointer(Checkpointer):
|
|
152
|
+
"""Persists checkpoints to a SQLite database.
|
|
153
|
+
|
|
154
|
+
Survives restarts and is shared between processes on one machine — enough
|
|
155
|
+
for a single-node deployment, and it needs nothing beyond the standard
|
|
156
|
+
library.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
path: Database file. Parent directories are created. ``":memory:"``
|
|
160
|
+
gives a transient database.
|
|
161
|
+
max_history: Snapshots retained per thread.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
SerializationError: When the database cannot be opened.
|
|
165
|
+
|
|
166
|
+
Note:
|
|
167
|
+
Each call opens its own connection. That costs a little per write and
|
|
168
|
+
buys thread safety without a connection pool — the right trade at the
|
|
169
|
+
rate an agent checkpoints.
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
>>> import tempfile, pathlib
|
|
173
|
+
>>> db = pathlib.Path(tempfile.mkdtemp()) / "state.db"
|
|
174
|
+
>>> saver = SQLiteCheckpointer(db)
|
|
175
|
+
>>> saver.put("t", {"step": 7})
|
|
176
|
+
>>> SQLiteCheckpointer(db).get("t")["step"]
|
|
177
|
+
7
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(
|
|
181
|
+
self, path: str | Path = ".windlass/checkpoints.db", max_history: int = 50
|
|
182
|
+
) -> None:
|
|
183
|
+
self.path = Path(path) if str(path) != ":memory:" else Path(":memory:")
|
|
184
|
+
self.max_history = max_history
|
|
185
|
+
self._lock = threading.RLock()
|
|
186
|
+
if str(self.path) != ":memory:":
|
|
187
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
self._shared: sqlite3.Connection | None = (
|
|
189
|
+
sqlite3.connect(":memory:", check_same_thread=False)
|
|
190
|
+
if str(self.path) == ":memory:"
|
|
191
|
+
else None
|
|
192
|
+
)
|
|
193
|
+
self._init()
|
|
194
|
+
|
|
195
|
+
def _connect(self) -> sqlite3.Connection:
|
|
196
|
+
"""Open (or reuse) a connection."""
|
|
197
|
+
if self._shared is not None:
|
|
198
|
+
return self._shared
|
|
199
|
+
try:
|
|
200
|
+
connection = sqlite3.connect(str(self.path), timeout=10.0)
|
|
201
|
+
connection.execute("PRAGMA journal_mode=WAL")
|
|
202
|
+
return connection
|
|
203
|
+
except sqlite3.Error as exc:
|
|
204
|
+
raise SerializationError(
|
|
205
|
+
f"Could not open the checkpoint database at {self.path}: {exc}",
|
|
206
|
+
hint="Check the directory exists and is writable.",
|
|
207
|
+
) from exc
|
|
208
|
+
|
|
209
|
+
def _init(self) -> None:
|
|
210
|
+
"""Create the schema if it does not exist."""
|
|
211
|
+
with self._lock:
|
|
212
|
+
connection = self._connect()
|
|
213
|
+
try:
|
|
214
|
+
connection.execute("""
|
|
215
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
216
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
217
|
+
thread_id TEXT NOT NULL,
|
|
218
|
+
state TEXT NOT NULL,
|
|
219
|
+
saved_at REAL NOT NULL
|
|
220
|
+
)
|
|
221
|
+
""")
|
|
222
|
+
connection.execute(
|
|
223
|
+
"CREATE INDEX IF NOT EXISTS idx_thread ON checkpoints(thread_id, id DESC)"
|
|
224
|
+
)
|
|
225
|
+
connection.commit()
|
|
226
|
+
finally:
|
|
227
|
+
if self._shared is None:
|
|
228
|
+
connection.close()
|
|
229
|
+
|
|
230
|
+
def put(self, thread_id: str, state: dict[str, Any]) -> None:
|
|
231
|
+
"""Insert a snapshot and prune old ones.
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
SerializationError: When the state is not JSON-serialisable or the
|
|
235
|
+
write fails.
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
payload = json.dumps(state, default=str)
|
|
239
|
+
except (TypeError, ValueError) as exc:
|
|
240
|
+
raise SerializationError(
|
|
241
|
+
f"Agent state for thread {thread_id!r} is not JSON-serialisable: {exc}",
|
|
242
|
+
hint="Keep custom objects out of agent state, or store an id instead.",
|
|
243
|
+
) from exc
|
|
244
|
+
|
|
245
|
+
with self._lock:
|
|
246
|
+
connection = self._connect()
|
|
247
|
+
try:
|
|
248
|
+
connection.execute(
|
|
249
|
+
"INSERT INTO checkpoints (thread_id, state, saved_at) VALUES (?, ?, ?)",
|
|
250
|
+
(thread_id, payload, time.time()),
|
|
251
|
+
)
|
|
252
|
+
connection.execute(
|
|
253
|
+
"""
|
|
254
|
+
DELETE FROM checkpoints
|
|
255
|
+
WHERE thread_id = ?
|
|
256
|
+
AND id NOT IN (
|
|
257
|
+
SELECT id FROM checkpoints
|
|
258
|
+
WHERE thread_id = ? ORDER BY id DESC LIMIT ?
|
|
259
|
+
)
|
|
260
|
+
""",
|
|
261
|
+
(thread_id, thread_id, self.max_history),
|
|
262
|
+
)
|
|
263
|
+
connection.commit()
|
|
264
|
+
except sqlite3.Error as exc:
|
|
265
|
+
raise SerializationError(f"Could not save the checkpoint: {exc}") from exc
|
|
266
|
+
finally:
|
|
267
|
+
if self._shared is None:
|
|
268
|
+
connection.close()
|
|
269
|
+
|
|
270
|
+
def get(self, thread_id: str) -> dict[str, Any] | None:
|
|
271
|
+
"""Return the newest snapshot, or ``None``."""
|
|
272
|
+
rows = self.history(thread_id, limit=1)
|
|
273
|
+
return rows[0] if rows else None
|
|
274
|
+
|
|
275
|
+
def history(self, thread_id: str, limit: int = 20) -> list[dict[str, Any]]:
|
|
276
|
+
"""Return recent snapshots, newest first."""
|
|
277
|
+
with self._lock:
|
|
278
|
+
connection = self._connect()
|
|
279
|
+
try:
|
|
280
|
+
cursor = connection.execute(
|
|
281
|
+
"SELECT state FROM checkpoints WHERE thread_id = ? ORDER BY id DESC LIMIT ?",
|
|
282
|
+
(thread_id, limit),
|
|
283
|
+
)
|
|
284
|
+
rows = cursor.fetchall()
|
|
285
|
+
except sqlite3.Error as exc:
|
|
286
|
+
_log.warning("Could not read checkpoints for %s: %s", thread_id, exc)
|
|
287
|
+
return []
|
|
288
|
+
finally:
|
|
289
|
+
if self._shared is None:
|
|
290
|
+
connection.close()
|
|
291
|
+
|
|
292
|
+
snapshots: list[dict[str, Any]] = []
|
|
293
|
+
for (payload,) in rows:
|
|
294
|
+
try:
|
|
295
|
+
snapshots.append(json.loads(payload))
|
|
296
|
+
except json.JSONDecodeError: # pragma: no cover - corrupt row
|
|
297
|
+
_log.warning("Skipping a corrupt checkpoint for thread %s.", thread_id)
|
|
298
|
+
return snapshots
|
|
299
|
+
|
|
300
|
+
def delete(self, thread_id: str) -> None:
|
|
301
|
+
"""Discard a thread's snapshots."""
|
|
302
|
+
with self._lock:
|
|
303
|
+
connection = self._connect()
|
|
304
|
+
try:
|
|
305
|
+
connection.execute("DELETE FROM checkpoints WHERE thread_id = ?", (thread_id,))
|
|
306
|
+
connection.commit()
|
|
307
|
+
except sqlite3.Error as exc: # pragma: no cover
|
|
308
|
+
_log.warning("Could not delete checkpoints for %s: %s", thread_id, exc)
|
|
309
|
+
finally:
|
|
310
|
+
if self._shared is None:
|
|
311
|
+
connection.close()
|
|
312
|
+
|
|
313
|
+
def threads(self) -> list[str]:
|
|
314
|
+
"""Return the known thread ids."""
|
|
315
|
+
with self._lock:
|
|
316
|
+
connection = self._connect()
|
|
317
|
+
try:
|
|
318
|
+
cursor = connection.execute("SELECT DISTINCT thread_id FROM checkpoints")
|
|
319
|
+
return sorted(row[0] for row in cursor.fetchall())
|
|
320
|
+
except sqlite3.Error: # pragma: no cover
|
|
321
|
+
return []
|
|
322
|
+
finally:
|
|
323
|
+
if self._shared is None:
|
|
324
|
+
connection.close()
|
|
325
|
+
|
|
326
|
+
def close(self) -> None:
|
|
327
|
+
"""Close the shared in-memory connection, if there is one."""
|
|
328
|
+
if self._shared is not None:
|
|
329
|
+
self._shared.close()
|
|
330
|
+
self._shared = None
|