loomcache 1.0.0__tar.gz
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.
- loomcache-1.0.0/.github/workflows/ci.yml +26 -0
- loomcache-1.0.0/.gitignore +16 -0
- loomcache-1.0.0/CHANGELOG.md +7 -0
- loomcache-1.0.0/CONTRIBUTING.md +36 -0
- loomcache-1.0.0/LICENSE +21 -0
- loomcache-1.0.0/PKG-INFO +302 -0
- loomcache-1.0.0/README.md +259 -0
- loomcache-1.0.0/examples/standalone_demo.py +111 -0
- loomcache-1.0.0/examples/with_anthropic.py +69 -0
- loomcache-1.0.0/examples/with_ollama.py +162 -0
- loomcache-1.0.0/loom/__init__.py +49 -0
- loomcache-1.0.0/loom/async_run.py +182 -0
- loomcache-1.0.0/loom/cache.py +114 -0
- loomcache-1.0.0/loom/cache_remote.py +143 -0
- loomcache-1.0.0/loom/cli.py +92 -0
- loomcache-1.0.0/loom/diff.py +58 -0
- loomcache-1.0.0/loom/hashing.py +83 -0
- loomcache-1.0.0/loom/langchain.py +133 -0
- loomcache-1.0.0/loom/run.py +250 -0
- loomcache-1.0.0/loom/trace.py +15 -0
- loomcache-1.0.0/loom/tracing.py +132 -0
- loomcache-1.0.0/loom/web.py +145 -0
- loomcache-1.0.0/pyproject.toml +61 -0
- loomcache-1.0.0/tests/test_async.py +73 -0
- loomcache-1.0.0/tests/test_cache.py +36 -0
- loomcache-1.0.0/tests/test_cache_remote.py +67 -0
- loomcache-1.0.0/tests/test_diff.py +68 -0
- loomcache-1.0.0/tests/test_hashing.py +56 -0
- loomcache-1.0.0/tests/test_langchain.py +54 -0
- loomcache-1.0.0/tests/test_run.py +157 -0
- loomcache-1.0.0/tests/test_web.py +50 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Install
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
- name: Run tests
|
|
24
|
+
run: pytest -v
|
|
25
|
+
- name: Run standalone demo (smoke test)
|
|
26
|
+
run: python examples/standalone_demo.py
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Contributing to Loom
|
|
2
|
+
|
|
3
|
+
Thanks for considering a contribution — this is a young project and
|
|
4
|
+
there's a lot of surface area to help with (see the Roadmap in the
|
|
5
|
+
README for ideas).
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/SanjayDey786/loomtrace.git
|
|
11
|
+
cd loomtrace
|
|
12
|
+
pip install -e ".[dev]"
|
|
13
|
+
pytest
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Guidelines
|
|
17
|
+
|
|
18
|
+
- Keep the core (`loom/`) dependency-free. Optional integrations
|
|
19
|
+
(Redis cache backend, LangChain adapter, etc.) should be optional
|
|
20
|
+
extras, not hard dependencies.
|
|
21
|
+
- Every new behavior needs a test in `tests/`. `pytest` should pass
|
|
22
|
+
with zero warnings before you open a PR.
|
|
23
|
+
- If you change hashing behavior (`loom/hashing.py`) or the
|
|
24
|
+
`Node`/`Run` schema (`loom/run.py`), call it out explicitly in your
|
|
25
|
+
PR description — it can silently invalidate everyone's existing
|
|
26
|
+
caches and saved run files, which is a breaking change even if no
|
|
27
|
+
public function signature changed.
|
|
28
|
+
- Favor small, focused PRs over large ones. If you want to work on
|
|
29
|
+
something roadmap-sized (remote cache backend, LangChain adapter,
|
|
30
|
+
UI), open an issue first to discuss the design.
|
|
31
|
+
|
|
32
|
+
## Reporting bugs / requesting features
|
|
33
|
+
|
|
34
|
+
Open a GitHub issue with a minimal reproduction where possible. For
|
|
35
|
+
bugs involving caching behavior specifically, include the two node
|
|
36
|
+
hashes you'd expect to be equal/different and why.
|
loomcache-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sanjay Dey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
loomcache-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: loomcache
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Content-addressed, deterministic execution engine for agent AI workflows.
|
|
5
|
+
Project-URL: Homepage, https://github.com/SanjayDey786/loomcache
|
|
6
|
+
Project-URL: Repository, https://github.com/SanjayDey786/loomcache
|
|
7
|
+
Project-URL: Issues, https://github.com/SanjayDey786/loomcache/issues
|
|
8
|
+
Author-email: Sanjay Dey <deysanjay30@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ai,caching,dag,llm,memoization,orchestration,reproducibility
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: all
|
|
25
|
+
Requires-Dist: boto3>=1.26.0; extra == 'all'
|
|
26
|
+
Requires-Dist: flask>=2.0.0; extra == 'all'
|
|
27
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'all'
|
|
28
|
+
Requires-Dist: redis>=4.5.0; extra == 'all'
|
|
29
|
+
Provides-Extra: anthropic
|
|
30
|
+
Requires-Dist: anthropic>=0.30.0; extra == 'anthropic'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
34
|
+
Provides-Extra: langchain
|
|
35
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
|
|
36
|
+
Provides-Extra: redis
|
|
37
|
+
Requires-Dist: redis>=4.5.0; extra == 'redis'
|
|
38
|
+
Provides-Extra: s3
|
|
39
|
+
Requires-Dist: boto3>=1.26.0; extra == 's3'
|
|
40
|
+
Provides-Extra: web
|
|
41
|
+
Requires-Dist: flask>=2.0.0; extra == 'web'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
[](https://pypi.org/project/loomcache/)
|
|
45
|
+
# Loom
|
|
46
|
+
|
|
47
|
+
**The smallest tool that gives any Python script content‑addressed caching, partial re‑execution, and node‑level diffing — without an orchestration platform.**
|
|
48
|
+
|
|
49
|
+
Loom treats every function call in your pipeline the way Bazel treats a build target or Git treats a commit: as a hashed, content‑addressed node in a dependency graph. Change one prompt buried deep in a pipeline, and Loom re‑runs **only** that step and everything downstream of it — not the whole pipeline.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Table of Contents
|
|
53
|
+
|
|
54
|
+
- [What Loom Solves](#what-loom-solves)
|
|
55
|
+
- [How It Works](#how-it-works)
|
|
56
|
+
- [Quick Start](#quick-start)
|
|
57
|
+
- [Key Features](#key-features)
|
|
58
|
+
- [Core Caching & Execution](#core-caching--execution)
|
|
59
|
+
- [Remote Cache Backends](#remote-cache-backends-s3--redis)
|
|
60
|
+
- [LangChain / LangGraph Adapter](#langchain--langgraph-adapter)
|
|
61
|
+
- [Web UI](#web-ui)
|
|
62
|
+
- [Async Steps & Concurrency](#async-steps--concurrency)
|
|
63
|
+
- [CLI Reference](#cli-reference)
|
|
64
|
+
- [Installation](#installation)
|
|
65
|
+
- [Comparison with Existing Tools](#comparison-with-existing-tools)
|
|
66
|
+
- [Limitations (Read This)](#limitations-read-this)
|
|
67
|
+
- [Roadmap Status](#roadmap-status)
|
|
68
|
+
- [Contributing & Tests](#contributing--tests)
|
|
69
|
+
- [License](#license)
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## What Loom Solves
|
|
74
|
+
|
|
75
|
+
If you’re hand‑rolling an agent script — not running it through a pipeline platform — you’ve likely faced these problems:
|
|
76
|
+
|
|
77
|
+
- Tweaking **one** prompt or **one** tool halfway through forces you to re‑run the **entire** pipeline — burning tokens, money, and wall‑clock time.
|
|
78
|
+
- When a pipeline’s output changes between two runs, you have no way to see **exactly where** the two runs diverged — you’re left diffing final text blobs and guessing.
|
|
79
|
+
- Debugging means adding print statements and re‑running the whole pipeline again, and again, and again.
|
|
80
|
+
|
|
81
|
+
**Loom solves this** by:
|
|
82
|
+
|
|
83
|
+
1. **Hashing** every step’s source code + arguments (and upstream node hashes) → deterministic cache keys.
|
|
84
|
+
2. **Caching** step outputs on disk (or S3/Redis) — identical calls return instantly.
|
|
85
|
+
3. **Recording** every run as a list of nodes, so you can `diff` two runs node‑by‑node.
|
|
86
|
+
4. **Forking** a run with a new input — only the changed step and its downstream steps re‑execute.
|
|
87
|
+
|
|
88
|
+
All of this works with **plain Python functions** – no special pipeline declarations, no extra infrastructure.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## How It Works
|
|
93
|
+
|
|
94
|
+
Every `@loom.step` call is hashed from:
|
|
95
|
+
|
|
96
|
+
1. **The source code of the step function itself** — so editing a prompt template inside the function body invalidates the cache automatically.
|
|
97
|
+
2. **Its arguments** — either their content hash, or, if an argument is itself the traced output of an upstream step, that step’s node hash. This turns a plain chain of Python function calls into a real hashable dependency graph — without any manual wiring.
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
user code Loom
|
|
101
|
+
--------- ----
|
|
102
|
+
@loom.step
|
|
103
|
+
def plan(q): ─────► 1. hash(source(plan) + q)
|
|
104
|
+
... 2. cache lookup
|
|
105
|
+
│
|
|
106
|
+
hit ◄──┴──► miss
|
|
107
|
+
│ │
|
|
108
|
+
return cached execute plan(q)
|
|
109
|
+
output cache + record
|
|
110
|
+
│ │
|
|
111
|
+
└─────┬──────┘
|
|
112
|
+
▼
|
|
113
|
+
tagged output (carries node hash)
|
|
114
|
+
│
|
|
115
|
+
passed into the next @loom.step call
|
|
116
|
+
▼
|
|
117
|
+
hash includes the UPSTREAM node hash
|
|
118
|
+
(so changing `plan` invalidates every-
|
|
119
|
+
thing downstream of it automatically)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Quick Start
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install loomtrace
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import loom
|
|
133
|
+
|
|
134
|
+
@loom.step
|
|
135
|
+
def plan(query: str) -> str:
|
|
136
|
+
return llm.call(f"Plan: {query}")
|
|
137
|
+
|
|
138
|
+
@loom.step
|
|
139
|
+
def execute(plan: str) -> str:
|
|
140
|
+
return tool.run(plan)
|
|
141
|
+
|
|
142
|
+
with loom.Run("research-agent") as run:
|
|
143
|
+
p = plan("find competitors of X")
|
|
144
|
+
result = execute(p)
|
|
145
|
+
|
|
146
|
+
run.save()
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Run that pipeline again unchanged — every step is served from cache in milliseconds. Change `plan`’s prompt or the input — only `plan` and its downstream steps re‑run.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Key Features
|
|
155
|
+
|
|
156
|
+
### Core Caching & Execution
|
|
157
|
+
|
|
158
|
+
* `@loom.step` – caches any function.
|
|
159
|
+
* `loom.Run(name)` – context manager; records every step call.
|
|
160
|
+
* `run.save()` / `Run.load(path)` – persist/restore runs as JSON.
|
|
161
|
+
* `run.fork(pipeline_fn, **kwargs)` – re‑run with new inputs; unchanged steps are cached.
|
|
162
|
+
* `loom.diff_runs(a, b)` / `loom.first_divergence(a, b)` – node‑by‑node diff.
|
|
163
|
+
* `loom.DiskCache(root=...)` – default local cache; subclass `loom.Cache` for other backends.
|
|
164
|
+
|
|
165
|
+
### Remote Cache Backends (S3 / Redis)
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from loom import S3Cache, RedisCache
|
|
169
|
+
|
|
170
|
+
# S3
|
|
171
|
+
cache = S3Cache(bucket="my-bucket", prefix="loom/")
|
|
172
|
+
|
|
173
|
+
# Redis
|
|
174
|
+
cache = RedisCache(url="redis://localhost:6379/0", key_prefix="loom:")
|
|
175
|
+
|
|
176
|
+
with loom.Run("pipeline", cache=cache) as run:
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### LangChain / LangGraph Adapter
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from loom.langchain import wrap_runnable
|
|
185
|
+
from langchain.chains import LLMChain
|
|
186
|
+
|
|
187
|
+
chain = LLMChain(...)
|
|
188
|
+
cached_chain = wrap_runnable(chain, name="my_chain")
|
|
189
|
+
|
|
190
|
+
with loom.Run("lc_run") as run:
|
|
191
|
+
result = cached_chain.invoke({"input": "Hello"})
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Web UI
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
loom web --runs-dir .loom_runs --port 5000
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Then open `http://localhost:5000` to browse runs, inspect nodes, and diff runs visually.
|
|
203
|
+
|
|
204
|
+
### Async Steps & Concurrency
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
import asyncio
|
|
208
|
+
import loom
|
|
209
|
+
|
|
210
|
+
@loom.async_step
|
|
211
|
+
async def fetch_data(query: str) -> str:
|
|
212
|
+
await asyncio.sleep(0.1)
|
|
213
|
+
return f"Data for {query}"
|
|
214
|
+
|
|
215
|
+
async def main():
|
|
216
|
+
async with loom.AsyncRun("async_demo") as run:
|
|
217
|
+
results = await loom.gather(
|
|
218
|
+
fetch_data("A"),
|
|
219
|
+
fetch_data("B")
|
|
220
|
+
)
|
|
221
|
+
run.save()
|
|
222
|
+
|
|
223
|
+
asyncio.run(main())
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## CLI Reference
|
|
230
|
+
|
|
231
|
+
| Command | Description |
|
|
232
|
+
| --- | --- |
|
|
233
|
+
| `loom show <run.json>` | List all nodes in a run |
|
|
234
|
+
| `loom diff <a.json> <b.json>` | Node‑by‑node diff |
|
|
235
|
+
| `loom stats <run.json>` | Cache hit rate and timing |
|
|
236
|
+
| `loom web` | Launch the web UI |
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Installation
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
pip install loomtrace
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Optional extras:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
pip install loomtrace[s3] # S3 support
|
|
251
|
+
pip install loomtrace[redis] # Redis support
|
|
252
|
+
pip install loomtrace[langchain] # LangChain adapter
|
|
253
|
+
pip install loomtrace[web] # Web UI (Flask)
|
|
254
|
+
pip install loomtrace[all] # all of the above
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Comparison with Existing Tools
|
|
261
|
+
|
|
262
|
+
> **Honest take**: content‑addressed step caching with automatic invalidation is not a new idea. ZenML and Dagster both already do it, well, in production. Loom is a **smaller, single‑purpose** version for standalone scripts.
|
|
263
|
+
|
|
264
|
+
| Tool | Category | What it does | Where it differs from Loom |
|
|
265
|
+
| --- | --- | --- | --- |
|
|
266
|
+
| **ZenML** | ML pipeline platform | Hashes step code, parameters, and artifacts; caches outputs; invalidates on code changes | Full platform: artifact store, stack config, UI, ML‑lifecycle features. You declare pipelines/steps in its framework. |
|
|
267
|
+
| **Dagster** | Data orchestrator | Op/asset memoization with version‑based cache keys; built‑in lineage and scheduling | Full platform — assets, sensors, a runtime you deploy, not a single importable decorator. |
|
|
268
|
+
| LangSmith / Langfuse / Helicone | LLM observability | Log, trace, and visualize LLM calls after the fact | Doesn’t cache or re‑execute — every re‑run still costs full price and time. |
|
|
269
|
+
| MLflow | Experiment tracking | Tracks metrics, params, and artifacts | Not content‑addressed caching; no automatic partial re‑execution. |
|
|
270
|
+
| DVC | Data/pipeline versioning | Content‑addressed, Git‑like caching for **file‑based** pipelines | Built around files and CLI pipeline stages, not live in‑process Python call graphs. |
|
|
271
|
+
| Bazel / Nix | Build systems | Content‑addressed, incremental builds | Not Python‑ or agent‑aware; infrastructure‑level, not a pip‑installable library. |
|
|
272
|
+
| **Loom** | Single‑purpose library | Same core idea (hash code + args, cache, invalidate on change) **+** node‑level diff, but as one dependency‑free decorator with no platform | Smaller, narrower — for standalone scripts. |
|
|
273
|
+
|
|
274
|
+
**The takeaway**: if you already use ZenML or Dagster, their caching is more mature — use it. Loom exists for the case: *“I have a standalone agent script, I don’t want to adopt an orchestration platform, and I want dependency edges inferred automatically from plain Python.”*
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Limitations (Read This)
|
|
279
|
+
|
|
280
|
+
* **Steps should be pure.** Caching assumes output depends only on declared inputs. Hidden side effects (global mutation, reading `time.time()`) won’t be tracked correctly.
|
|
281
|
+
* **Value tagging covers most types, not all.** `str`, `tuple`, `frozenset`, `list`, `dict`, `set`, and any object with `__dict__` are tagged directly. `int`, `float`, `bool` (fixed C layout) fall back to a transparent `TracedBox` wrapper – documented, not a silent failure.
|
|
282
|
+
* **`fork()` re‑invokes your pipeline function** – it does not resume from a checkpoint. Speed comes from cache hits, just like Bazel/DVC.
|
|
283
|
+
* **No distributed cache** – but the `Cache` interface is pluggable; `S3Cache` and `RedisCache` ship today.
|
|
284
|
+
* **Async is supported**, but parallel DAG execution is basic (`gather`). True multi‑branch concurrency is planned.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Contributing & Tests
|
|
289
|
+
|
|
290
|
+
Contributions welcome — see `CONTRIBUTING.md`. Run the test suite with:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
pip install -e ".[dev,all]"
|
|
294
|
+
pytest tests/
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
[](https://pypi.org/project/loomcache/)
|
|
2
|
+
# Loom
|
|
3
|
+
|
|
4
|
+
**The smallest tool that gives any Python script content‑addressed caching, partial re‑execution, and node‑level diffing — without an orchestration platform.**
|
|
5
|
+
|
|
6
|
+
Loom treats every function call in your pipeline the way Bazel treats a build target or Git treats a commit: as a hashed, content‑addressed node in a dependency graph. Change one prompt buried deep in a pipeline, and Loom re‑runs **only** that step and everything downstream of it — not the whole pipeline.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [What Loom Solves](#what-loom-solves)
|
|
12
|
+
- [How It Works](#how-it-works)
|
|
13
|
+
- [Quick Start](#quick-start)
|
|
14
|
+
- [Key Features](#key-features)
|
|
15
|
+
- [Core Caching & Execution](#core-caching--execution)
|
|
16
|
+
- [Remote Cache Backends](#remote-cache-backends-s3--redis)
|
|
17
|
+
- [LangChain / LangGraph Adapter](#langchain--langgraph-adapter)
|
|
18
|
+
- [Web UI](#web-ui)
|
|
19
|
+
- [Async Steps & Concurrency](#async-steps--concurrency)
|
|
20
|
+
- [CLI Reference](#cli-reference)
|
|
21
|
+
- [Installation](#installation)
|
|
22
|
+
- [Comparison with Existing Tools](#comparison-with-existing-tools)
|
|
23
|
+
- [Limitations (Read This)](#limitations-read-this)
|
|
24
|
+
- [Roadmap Status](#roadmap-status)
|
|
25
|
+
- [Contributing & Tests](#contributing--tests)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## What Loom Solves
|
|
31
|
+
|
|
32
|
+
If you’re hand‑rolling an agent script — not running it through a pipeline platform — you’ve likely faced these problems:
|
|
33
|
+
|
|
34
|
+
- Tweaking **one** prompt or **one** tool halfway through forces you to re‑run the **entire** pipeline — burning tokens, money, and wall‑clock time.
|
|
35
|
+
- When a pipeline’s output changes between two runs, you have no way to see **exactly where** the two runs diverged — you’re left diffing final text blobs and guessing.
|
|
36
|
+
- Debugging means adding print statements and re‑running the whole pipeline again, and again, and again.
|
|
37
|
+
|
|
38
|
+
**Loom solves this** by:
|
|
39
|
+
|
|
40
|
+
1. **Hashing** every step’s source code + arguments (and upstream node hashes) → deterministic cache keys.
|
|
41
|
+
2. **Caching** step outputs on disk (or S3/Redis) — identical calls return instantly.
|
|
42
|
+
3. **Recording** every run as a list of nodes, so you can `diff` two runs node‑by‑node.
|
|
43
|
+
4. **Forking** a run with a new input — only the changed step and its downstream steps re‑execute.
|
|
44
|
+
|
|
45
|
+
All of this works with **plain Python functions** – no special pipeline declarations, no extra infrastructure.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## How It Works
|
|
50
|
+
|
|
51
|
+
Every `@loom.step` call is hashed from:
|
|
52
|
+
|
|
53
|
+
1. **The source code of the step function itself** — so editing a prompt template inside the function body invalidates the cache automatically.
|
|
54
|
+
2. **Its arguments** — either their content hash, or, if an argument is itself the traced output of an upstream step, that step’s node hash. This turns a plain chain of Python function calls into a real hashable dependency graph — without any manual wiring.
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
user code Loom
|
|
58
|
+
--------- ----
|
|
59
|
+
@loom.step
|
|
60
|
+
def plan(q): ─────► 1. hash(source(plan) + q)
|
|
61
|
+
... 2. cache lookup
|
|
62
|
+
│
|
|
63
|
+
hit ◄──┴──► miss
|
|
64
|
+
│ │
|
|
65
|
+
return cached execute plan(q)
|
|
66
|
+
output cache + record
|
|
67
|
+
│ │
|
|
68
|
+
└─────┬──────┘
|
|
69
|
+
▼
|
|
70
|
+
tagged output (carries node hash)
|
|
71
|
+
│
|
|
72
|
+
passed into the next @loom.step call
|
|
73
|
+
▼
|
|
74
|
+
hash includes the UPSTREAM node hash
|
|
75
|
+
(so changing `plan` invalidates every-
|
|
76
|
+
thing downstream of it automatically)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install loomtrace
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import loom
|
|
90
|
+
|
|
91
|
+
@loom.step
|
|
92
|
+
def plan(query: str) -> str:
|
|
93
|
+
return llm.call(f"Plan: {query}")
|
|
94
|
+
|
|
95
|
+
@loom.step
|
|
96
|
+
def execute(plan: str) -> str:
|
|
97
|
+
return tool.run(plan)
|
|
98
|
+
|
|
99
|
+
with loom.Run("research-agent") as run:
|
|
100
|
+
p = plan("find competitors of X")
|
|
101
|
+
result = execute(p)
|
|
102
|
+
|
|
103
|
+
run.save()
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Run that pipeline again unchanged — every step is served from cache in milliseconds. Change `plan`’s prompt or the input — only `plan` and its downstream steps re‑run.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Key Features
|
|
112
|
+
|
|
113
|
+
### Core Caching & Execution
|
|
114
|
+
|
|
115
|
+
* `@loom.step` – caches any function.
|
|
116
|
+
* `loom.Run(name)` – context manager; records every step call.
|
|
117
|
+
* `run.save()` / `Run.load(path)` – persist/restore runs as JSON.
|
|
118
|
+
* `run.fork(pipeline_fn, **kwargs)` – re‑run with new inputs; unchanged steps are cached.
|
|
119
|
+
* `loom.diff_runs(a, b)` / `loom.first_divergence(a, b)` – node‑by‑node diff.
|
|
120
|
+
* `loom.DiskCache(root=...)` – default local cache; subclass `loom.Cache` for other backends.
|
|
121
|
+
|
|
122
|
+
### Remote Cache Backends (S3 / Redis)
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from loom import S3Cache, RedisCache
|
|
126
|
+
|
|
127
|
+
# S3
|
|
128
|
+
cache = S3Cache(bucket="my-bucket", prefix="loom/")
|
|
129
|
+
|
|
130
|
+
# Redis
|
|
131
|
+
cache = RedisCache(url="redis://localhost:6379/0", key_prefix="loom:")
|
|
132
|
+
|
|
133
|
+
with loom.Run("pipeline", cache=cache) as run:
|
|
134
|
+
...
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### LangChain / LangGraph Adapter
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from loom.langchain import wrap_runnable
|
|
142
|
+
from langchain.chains import LLMChain
|
|
143
|
+
|
|
144
|
+
chain = LLMChain(...)
|
|
145
|
+
cached_chain = wrap_runnable(chain, name="my_chain")
|
|
146
|
+
|
|
147
|
+
with loom.Run("lc_run") as run:
|
|
148
|
+
result = cached_chain.invoke({"input": "Hello"})
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Web UI
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
loom web --runs-dir .loom_runs --port 5000
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Then open `http://localhost:5000` to browse runs, inspect nodes, and diff runs visually.
|
|
160
|
+
|
|
161
|
+
### Async Steps & Concurrency
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
import asyncio
|
|
165
|
+
import loom
|
|
166
|
+
|
|
167
|
+
@loom.async_step
|
|
168
|
+
async def fetch_data(query: str) -> str:
|
|
169
|
+
await asyncio.sleep(0.1)
|
|
170
|
+
return f"Data for {query}"
|
|
171
|
+
|
|
172
|
+
async def main():
|
|
173
|
+
async with loom.AsyncRun("async_demo") as run:
|
|
174
|
+
results = await loom.gather(
|
|
175
|
+
fetch_data("A"),
|
|
176
|
+
fetch_data("B")
|
|
177
|
+
)
|
|
178
|
+
run.save()
|
|
179
|
+
|
|
180
|
+
asyncio.run(main())
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## CLI Reference
|
|
187
|
+
|
|
188
|
+
| Command | Description |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `loom show <run.json>` | List all nodes in a run |
|
|
191
|
+
| `loom diff <a.json> <b.json>` | Node‑by‑node diff |
|
|
192
|
+
| `loom stats <run.json>` | Cache hit rate and timing |
|
|
193
|
+
| `loom web` | Launch the web UI |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Installation
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pip install loomtrace
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Optional extras:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
pip install loomtrace[s3] # S3 support
|
|
208
|
+
pip install loomtrace[redis] # Redis support
|
|
209
|
+
pip install loomtrace[langchain] # LangChain adapter
|
|
210
|
+
pip install loomtrace[web] # Web UI (Flask)
|
|
211
|
+
pip install loomtrace[all] # all of the above
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Comparison with Existing Tools
|
|
218
|
+
|
|
219
|
+
> **Honest take**: content‑addressed step caching with automatic invalidation is not a new idea. ZenML and Dagster both already do it, well, in production. Loom is a **smaller, single‑purpose** version for standalone scripts.
|
|
220
|
+
|
|
221
|
+
| Tool | Category | What it does | Where it differs from Loom |
|
|
222
|
+
| --- | --- | --- | --- |
|
|
223
|
+
| **ZenML** | ML pipeline platform | Hashes step code, parameters, and artifacts; caches outputs; invalidates on code changes | Full platform: artifact store, stack config, UI, ML‑lifecycle features. You declare pipelines/steps in its framework. |
|
|
224
|
+
| **Dagster** | Data orchestrator | Op/asset memoization with version‑based cache keys; built‑in lineage and scheduling | Full platform — assets, sensors, a runtime you deploy, not a single importable decorator. |
|
|
225
|
+
| LangSmith / Langfuse / Helicone | LLM observability | Log, trace, and visualize LLM calls after the fact | Doesn’t cache or re‑execute — every re‑run still costs full price and time. |
|
|
226
|
+
| MLflow | Experiment tracking | Tracks metrics, params, and artifacts | Not content‑addressed caching; no automatic partial re‑execution. |
|
|
227
|
+
| DVC | Data/pipeline versioning | Content‑addressed, Git‑like caching for **file‑based** pipelines | Built around files and CLI pipeline stages, not live in‑process Python call graphs. |
|
|
228
|
+
| Bazel / Nix | Build systems | Content‑addressed, incremental builds | Not Python‑ or agent‑aware; infrastructure‑level, not a pip‑installable library. |
|
|
229
|
+
| **Loom** | Single‑purpose library | Same core idea (hash code + args, cache, invalidate on change) **+** node‑level diff, but as one dependency‑free decorator with no platform | Smaller, narrower — for standalone scripts. |
|
|
230
|
+
|
|
231
|
+
**The takeaway**: if you already use ZenML or Dagster, their caching is more mature — use it. Loom exists for the case: *“I have a standalone agent script, I don’t want to adopt an orchestration platform, and I want dependency edges inferred automatically from plain Python.”*
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Limitations (Read This)
|
|
236
|
+
|
|
237
|
+
* **Steps should be pure.** Caching assumes output depends only on declared inputs. Hidden side effects (global mutation, reading `time.time()`) won’t be tracked correctly.
|
|
238
|
+
* **Value tagging covers most types, not all.** `str`, `tuple`, `frozenset`, `list`, `dict`, `set`, and any object with `__dict__` are tagged directly. `int`, `float`, `bool` (fixed C layout) fall back to a transparent `TracedBox` wrapper – documented, not a silent failure.
|
|
239
|
+
* **`fork()` re‑invokes your pipeline function** – it does not resume from a checkpoint. Speed comes from cache hits, just like Bazel/DVC.
|
|
240
|
+
* **No distributed cache** – but the `Cache` interface is pluggable; `S3Cache` and `RedisCache` ship today.
|
|
241
|
+
* **Async is supported**, but parallel DAG execution is basic (`gather`). True multi‑branch concurrency is planned.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Contributing & Tests
|
|
246
|
+
|
|
247
|
+
Contributions welcome — see `CONTRIBUTING.md`. Run the test suite with:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
pip install -e ".[dev,all]"
|
|
251
|
+
pytest tests/
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT — see `LICENSE`.
|