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.
Files changed (104) hide show
  1. windlass/__init__.py +243 -0
  2. windlass/_version.py +7 -0
  3. windlass/agent/__init__.py +44 -0
  4. windlass/agent/builder.py +709 -0
  5. windlass/agent/checkpoint.py +330 -0
  6. windlass/agent/graph.py +410 -0
  7. windlass/agent/runtime.py +633 -0
  8. windlass/agent/supervisor.py +373 -0
  9. windlass/api.py +469 -0
  10. windlass/cli.py +389 -0
  11. windlass/core/__init__.py +145 -0
  12. windlass/core/cache.py +336 -0
  13. windlass/core/concurrency.py +341 -0
  14. windlass/core/config.py +461 -0
  15. windlass/core/container.py +383 -0
  16. windlass/core/exceptions.py +465 -0
  17. windlass/core/lazy.py +208 -0
  18. windlass/core/logging.py +204 -0
  19. windlass/core/registry.py +626 -0
  20. windlass/core/retry.py +288 -0
  21. windlass/core/text.py +505 -0
  22. windlass/core/types.py +671 -0
  23. windlass/core/vectors.py +306 -0
  24. windlass/interfaces/__init__.py +81 -0
  25. windlass/interfaces/base.py +191 -0
  26. windlass/interfaces/chunker.py +279 -0
  27. windlass/interfaces/embedding.py +259 -0
  28. windlass/interfaces/evaluator.py +264 -0
  29. windlass/interfaces/guardrail.py +289 -0
  30. windlass/interfaces/llm.py +410 -0
  31. windlass/interfaces/loader.py +349 -0
  32. windlass/interfaces/mcp.py +312 -0
  33. windlass/interfaces/memory.py +178 -0
  34. windlass/interfaces/preprocessor.py +195 -0
  35. windlass/interfaces/retriever.py +245 -0
  36. windlass/interfaces/tool.py +339 -0
  37. windlass/interfaces/tracer.py +366 -0
  38. windlass/interfaces/vectordb.py +340 -0
  39. windlass/providers/__init__.py +642 -0
  40. windlass/providers/chunkers/__init__.py +7 -0
  41. windlass/providers/chunkers/hierarchical.py +254 -0
  42. windlass/providers/chunkers/recursive.py +260 -0
  43. windlass/providers/chunkers/semantic.py +204 -0
  44. windlass/providers/chunkers/structural.py +323 -0
  45. windlass/providers/embeddings/__init__.py +7 -0
  46. windlass/providers/embeddings/hash.py +127 -0
  47. windlass/providers/embeddings/hf_inference.py +284 -0
  48. windlass/providers/embeddings/huggingface.py +187 -0
  49. windlass/providers/embeddings/openai.py +126 -0
  50. windlass/providers/evaluation/__init__.py +7 -0
  51. windlass/providers/evaluation/builtin.py +428 -0
  52. windlass/providers/evaluation/external.py +356 -0
  53. windlass/providers/guardrails/__init__.py +7 -0
  54. windlass/providers/guardrails/nemo.py +241 -0
  55. windlass/providers/guardrails/rules.py +241 -0
  56. windlass/providers/llm/__init__.py +9 -0
  57. windlass/providers/llm/anthropic.py +355 -0
  58. windlass/providers/llm/fake.py +271 -0
  59. windlass/providers/llm/gemini.py +291 -0
  60. windlass/providers/llm/groq.py +361 -0
  61. windlass/providers/llm/ollama.py +313 -0
  62. windlass/providers/llm/openai.py +424 -0
  63. windlass/providers/loaders/__init__.py +7 -0
  64. windlass/providers/loaders/media.py +296 -0
  65. windlass/providers/loaders/office.py +511 -0
  66. windlass/providers/loaders/text.py +507 -0
  67. windlass/providers/loaders/web.py +452 -0
  68. windlass/providers/mcp/__init__.py +7 -0
  69. windlass/providers/mcp/fastmcp.py +635 -0
  70. windlass/providers/memory/__init__.py +7 -0
  71. windlass/providers/memory/conversation.py +322 -0
  72. windlass/providers/memory/longterm.py +304 -0
  73. windlass/providers/observability/__init__.py +7 -0
  74. windlass/providers/observability/console.py +259 -0
  75. windlass/providers/observability/multi.py +160 -0
  76. windlass/providers/observability/platforms.py +424 -0
  77. windlass/providers/preprocessors/__init__.py +7 -0
  78. windlass/providers/preprocessors/clean.py +306 -0
  79. windlass/providers/preprocessors/dedup.py +321 -0
  80. windlass/providers/preprocessors/enrich.py +303 -0
  81. windlass/providers/preprocessors/privacy.py +307 -0
  82. windlass/providers/retrievers/__init__.py +7 -0
  83. windlass/providers/retrievers/bm25.py +365 -0
  84. windlass/providers/retrievers/contextual.py +326 -0
  85. windlass/providers/retrievers/hybrid.py +255 -0
  86. windlass/providers/retrievers/vector.py +247 -0
  87. windlass/providers/vectordb/__init__.py +7 -0
  88. windlass/providers/vectordb/chroma.py +382 -0
  89. windlass/providers/vectordb/faiss.py +445 -0
  90. windlass/providers/vectordb/memory.py +361 -0
  91. windlass/providers/vectordb/pinecone.py +442 -0
  92. windlass/py.typed +1 -0
  93. windlass/rag/__init__.py +34 -0
  94. windlass/rag/builder.py +739 -0
  95. windlass/rag/loading.py +250 -0
  96. windlass/rag/pipeline.py +854 -0
  97. windlass/testing.py +376 -0
  98. windlass/tools/__init__.py +458 -0
  99. windlass/tools/schema.py +417 -0
  100. windlass-0.1.0.dist-info/METADATA +679 -0
  101. windlass-0.1.0.dist-info/RECORD +104 -0
  102. windlass-0.1.0.dist-info/WHEEL +4 -0
  103. windlass-0.1.0.dist-info/entry_points.txt +2 -0
  104. 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