floe-ai 0.1.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.
- floe_ai-0.1.0/LICENSE +21 -0
- floe_ai-0.1.0/PKG-INFO +408 -0
- floe_ai-0.1.0/README.md +381 -0
- floe_ai-0.1.0/floe/__init__.py +77 -0
- floe_ai-0.1.0/floe/cli.py +1349 -0
- floe_ai-0.1.0/floe/core/auth.py +58 -0
- floe_ai-0.1.0/floe/core/engine.py +883 -0
- floe_ai-0.1.0/floe/core/enrichment.py +143 -0
- floe_ai-0.1.0/floe/core/floe.py +27 -0
- floe_ai-0.1.0/floe/core/loader.py +129 -0
- floe_ai-0.1.0/floe/core/pressure.py +794 -0
- floe_ai-0.1.0/floe/core/replay_log.py +299 -0
- floe_ai-0.1.0/floe/core/setup.py +25 -0
- floe_ai-0.1.0/floe/core/span.py +85 -0
- floe_ai-0.1.0/floe/core/spans.py +250 -0
- floe_ai-0.1.0/floe/core/tool_registry.py +235 -0
- floe_ai-0.1.0/floe/core/trace.py +0 -0
- floe_ai-0.1.0/floe/core/tracer.py +0 -0
- floe_ai-0.1.0/floe/exporters/__init__.py +0 -0
- floe_ai-0.1.0/floe/exporters/json_exporter.py +75 -0
- floe_ai-0.1.0/floe/server/__init__.py +0 -0
- floe_ai-0.1.0/floe/server/app.py +535 -0
- floe_ai-0.1.0/floe/server/static/app.js +1323 -0
- floe_ai-0.1.0/floe/server/static/index.html +73 -0
- floe_ai-0.1.0/floe/server/static/style.css +456 -0
- floe_ai-0.1.0/floe/tools.py +85 -0
- floe_ai-0.1.0/floe_ai.egg-info/PKG-INFO +408 -0
- floe_ai-0.1.0/floe_ai.egg-info/SOURCES.txt +32 -0
- floe_ai-0.1.0/floe_ai.egg-info/dependency_links.txt +1 -0
- floe_ai-0.1.0/floe_ai.egg-info/entry_points.txt +2 -0
- floe_ai-0.1.0/floe_ai.egg-info/requires.txt +18 -0
- floe_ai-0.1.0/floe_ai.egg-info/top_level.txt +1 -0
- floe_ai-0.1.0/pyproject.toml +51 -0
- floe_ai-0.1.0/setup.cfg +4 -0
floe_ai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dhruv Vootkuri and Kevin Geng
|
|
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.
|
floe_ai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: floe-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Optimize & observe your agent infrastructure.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: opentelemetry-api>=1.40.0
|
|
10
|
+
Requires-Dist: opentelemetry-sdk>=1.40.0
|
|
11
|
+
Requires-Dist: opentelemetry-instrumentation-openai>=0.53.3
|
|
12
|
+
Requires-Dist: opentelemetry-instrumentation-langchain>=0.53.3
|
|
13
|
+
Requires-Dist: openai>=2.0.0
|
|
14
|
+
Requires-Dist: litellm>=1.80.0
|
|
15
|
+
Requires-Dist: click>=8.1
|
|
16
|
+
Requires-Dist: blessed>=1.20
|
|
17
|
+
Requires-Dist: fastapi>=0.115
|
|
18
|
+
Requires-Dist: uvicorn>=0.30
|
|
19
|
+
Requires-Dist: pydantic>=2.7
|
|
20
|
+
Requires-Dist: python-dotenv>=1.0
|
|
21
|
+
Provides-Extra: demo
|
|
22
|
+
Requires-Dist: langchain>=1.2; extra == "demo"
|
|
23
|
+
Requires-Dist: langchain-openai>=1.1; extra == "demo"
|
|
24
|
+
Requires-Dist: langchain-core>=1.2; extra == "demo"
|
|
25
|
+
Requires-Dist: langgraph>=1.1; extra == "demo"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# Replay
|
|
29
|
+
|
|
30
|
+
**Git for agent execution.** Capture an AI agent run as a tree of spans, then fork any step, change the inputs, and rerun from that point forward to see what would have happened — in seconds, instead of re-running the whole agent from scratch.
|
|
31
|
+
|
|
32
|
+
When a multi-step agent fails, you don't want to rerun the world. You want to fork the trace at the broken step, apply a fix, and observe the new downstream behavior. Replay does exactly that.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
### Requirements
|
|
39
|
+
|
|
40
|
+
| | |
|
|
41
|
+
|---|---|
|
|
42
|
+
| **Python** | 3.10 or newer (`python3 --version` to check) |
|
|
43
|
+
| **OS** | macOS, Linux, or Windows |
|
|
44
|
+
| **OpenAI API key** | Only for *capturing* new traces and for replays/pressure tests — browsing traces already on disk needs no key |
|
|
45
|
+
| **Node.js** | Not required. Only for the marketing site in `app/` — the Replay Console dashboard is pure Python |
|
|
46
|
+
|
|
47
|
+
If you don't have Python 3.10+: [python.org/downloads](https://www.python.org/downloads/), or `brew install python@3.11` on macOS.
|
|
48
|
+
|
|
49
|
+
### Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/dhruv-vootkuri/Replay.git
|
|
53
|
+
cd Replay
|
|
54
|
+
|
|
55
|
+
python3 -m venv venv
|
|
56
|
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
|
57
|
+
|
|
58
|
+
pip install -e ".[demo]"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That's it. `pip install -e ".[demo]"` installs the engine, the web console, and the LangChain packages needed to run the included demo agent, and puts a `replay` command on your PATH.
|
|
62
|
+
|
|
63
|
+
Leave off `[demo]` (`pip install -e .`) if you only want to explore and replay traces and won't be capturing new ones from a LangChain agent.
|
|
64
|
+
|
|
65
|
+
> Prefer the exact versions this was built against? `pip install -r requirements.txt` instead, then `pip install -e . --no-deps`.
|
|
66
|
+
|
|
67
|
+
### Verify it worked
|
|
68
|
+
|
|
69
|
+
The repo ships with real captured traces, so you can confirm everything works before writing a line of code or spending a cent:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
replay list
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
4 trace(s) found:
|
|
77
|
+
|
|
78
|
+
a7f3521dceaf7e37...
|
|
79
|
+
18 spans • 3 LLM calls • 5 tool calls • 5.3s
|
|
80
|
+
...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then open the dashboard:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
replay serve # → http://localhost:7823
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Browse the traces, open any span, and read the replay log — all without an API key.
|
|
90
|
+
|
|
91
|
+
### Add your API key
|
|
92
|
+
|
|
93
|
+
Needed once you want to capture new traces or run replays and pressure tests (both make real model calls):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cp .env.example .env
|
|
97
|
+
# edit .env and set OPENAI_API_KEY=sk-...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or export it directly:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
export OPENAI_API_KEY=sk-... # Windows: set OPENAI_API_KEY=sk-...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Capture your first trace
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python demo_agent.py # runs the included trip-planner agent
|
|
110
|
+
replay list # your new trace is at the bottom
|
|
111
|
+
replay explore <trace_id> # arrow keys to navigate, Enter to fork
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Troubleshooting
|
|
115
|
+
|
|
116
|
+
| Symptom | Fix |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `replay: command not found` | The venv isn't active. `source venv/bin/activate` (Windows: `venv\Scripts\activate`) |
|
|
119
|
+
| `ModuleNotFoundError: No module named 'replay'` | Same cause — activate the venv, or re-run `pip install -e .` |
|
|
120
|
+
| `ModuleNotFoundError: langchain` when running `demo_agent.py` | Installed without the extra. Run `pip install -e ".[demo]"` |
|
|
121
|
+
| `AuthenticationError` / 401 from OpenAI | `OPENAI_API_KEY` isn't set or is invalid. See "Add your API key" above |
|
|
122
|
+
| `Address already in use` on `replay serve` | Another process has the port. Use `replay serve --port 8080` |
|
|
123
|
+
| `No traces found` | You're not in the repo root, or pointing elsewhere. `cd` to the repo, or pass `--dir path/to/traces` |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Features
|
|
128
|
+
|
|
129
|
+
### One-line instrumentation
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import replay
|
|
133
|
+
replay.init()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
That's the whole setup. Replay attaches to OpenTelemetry and auto-instruments anything OTel knows about.
|
|
137
|
+
|
|
138
|
+
### Framework-agnostic capture
|
|
139
|
+
|
|
140
|
+
Built on OpenTelemetry, so it captures runs from anything OTel can see:
|
|
141
|
+
- **OpenAI** Python SDK (auto-instrumented)
|
|
142
|
+
- **LangChain / LangGraph** (auto-instrumented, including `create_agent`)
|
|
143
|
+
- **LlamaIndex**, raw OTel spans, and any other OTel-instrumented framework
|
|
144
|
+
- Tool calls, LLM calls, agent workflows, custom spans
|
|
145
|
+
|
|
146
|
+
### Fork-and-replay engine
|
|
147
|
+
|
|
148
|
+
Pick any span in any captured trace and fork it:
|
|
149
|
+
|
|
150
|
+
1. Ancestors of the fork point are kept as **cached** (not re-executed)
|
|
151
|
+
2. The fork span gets your **attribute overrides** applied — your changes are ground truth, no re-execution of that step
|
|
152
|
+
3. **Downstream LLM calls** are re-run with updated context (new tool results threaded in via `tool_call_id`, new messages rebuilt from `replay.messages_json`)
|
|
153
|
+
4. **Downstream tool calls** consult the tool registry to decide whether to run for real, use an alternative, accept a manually provided output, or skip
|
|
154
|
+
5. The replay is saved as `{original_trace_id}.replay.{replay_trace_id}.json` so you can diff it against the original
|
|
155
|
+
|
|
156
|
+
### Tool registry with pause/resume
|
|
157
|
+
|
|
158
|
+
Declare your tools so replay knows how to handle them when they're hit downstream of a fork:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
@replay.tool(safe=True) # safe to run for real during replay
|
|
162
|
+
def get_weather(city: str): ...
|
|
163
|
+
|
|
164
|
+
@replay.tool(safe=False) # has real-world side effects — replay will pause
|
|
165
|
+
def send_email(to: str, body: str): ...
|
|
166
|
+
|
|
167
|
+
@replay.tool(replay_fn=mock_db) # provide an alternative impl just for replay
|
|
168
|
+
def query_prod_db(sql: str): ...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
When a tool span is hit during replay, the engine consults this decision hierarchy:
|
|
172
|
+
|
|
173
|
+
1. **Saved CLI preference** → run automatically with the user's prior choice
|
|
174
|
+
2. **No preference** → pause and prompt the user:
|
|
175
|
+
- **run** — execute the real tool (only if `safe=True`)
|
|
176
|
+
- **alternative** — call the registered `replay_fn` (only if defined)
|
|
177
|
+
- **provide** — manually type the expected output
|
|
178
|
+
- **skip** — reuse the cached output from the original trace
|
|
179
|
+
- **stop** — abort the replay
|
|
180
|
+
|
|
181
|
+
Decisions can be saved to `.replay/tool_preferences.json` for future runs.
|
|
182
|
+
|
|
183
|
+
### Interactive terminal explorer
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
replay explore <trace_id>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
A `blessed`-based TUI that renders the trace as a navigable tree. Arrow keys to move, Enter to fork at the highlighted span. Inputs are shown inline; you edit them in place and the replay runs against your edits. Forkable spans (LLM and tool) are marked with a green diamond.
|
|
190
|
+
|
|
191
|
+
### Replay Console — the live dashboard
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
replay serve # opens http://localhost:7823
|
|
195
|
+
replay serve --port 8080
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Everything the CLI does, live in a browser. Five views over the same traces directory:
|
|
199
|
+
|
|
200
|
+
| View | What's in it |
|
|
201
|
+
|---|---|
|
|
202
|
+
| **Overview** | Traces, spans, LLM calls, tool calls, tokens, replays and pressure-check pass rate at a glance, plus span-mix and tool-usage charts and the most recent activity |
|
|
203
|
+
| **Traces** | The trace list and a span tree per trace — open any span for its inputs, output, model and tokens, and fork it in place. Also shows every replay that trace has produced. |
|
|
204
|
+
| **Pressure tests** | Pick a system prompt, edit it, and replay it against every stored trace at once (see below) |
|
|
205
|
+
| **Replay log** | Every replay ever played — what changed, what the new answer was, and a full span-by-span diff against the original |
|
|
206
|
+
| **Tools** | The tool registry: which tools can run for real during a replay, and their saved preferences (editable) |
|
|
207
|
+
|
|
208
|
+
The console loads `.replay/tool_sources.py` the same way `explore` does, so replays started from the browser can run your tools for real.
|
|
209
|
+
|
|
210
|
+
### Pressure tests — one prompt against every trace
|
|
211
|
+
|
|
212
|
+
The workflow this exists for: you fork one trace, edit the system prompt, like the new answer — and now you need to know whether that edit breaks anything the agent has *already* been asked.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# from the console: Traces → any trace → "Pressure test this prompt against all traces"
|
|
216
|
+
# or from a fork result: "Pressure test this prompt against all traces"
|
|
217
|
+
|
|
218
|
+
# from the CLI:
|
|
219
|
+
replay prompts # see the distinct system prompts
|
|
220
|
+
replay pressure --set-prompt "You are a terse assistant. Always use the tools."
|
|
221
|
+
replay pressure --from-prompt 0 --trace a7f35 --contains Tokyo
|
|
222
|
+
replay pressure-log # every run
|
|
223
|
+
replay pressure-show <run_id> # one run's results
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Each selected trace is forked at its **entry LLM call** with the candidate prompt swapped in (or added, if the trace had none), then the whole agent loop is replayed — real model calls, real tool calls — and graded against the original run:
|
|
227
|
+
|
|
228
|
+
| Check | Fails when |
|
|
229
|
+
|---|---|
|
|
230
|
+
| **Produced an answer** | the replay ended without a final answer |
|
|
231
|
+
| **No errored spans** | any replayed span came back with an error status |
|
|
232
|
+
| **Tool usage preserved** | the agent stopped calling a tool the original called — the classic way a prompt edit quietly turns a grounded agent into a guessing one |
|
|
233
|
+
| **Grounded facts survive** | a tool result that was quoted in the original answer is missing from the new one |
|
|
234
|
+
| **Must contain / must not contain / matches pattern** | your own assertions on the new answer |
|
|
235
|
+
| **Answer still similar** *(warning)* | word overlap with the original answer drops below a threshold |
|
|
236
|
+
| **No extra reasoning steps** *(warning)* | the replay takes more LLM calls than the original, past a tolerance |
|
|
237
|
+
|
|
238
|
+
Nothing here asks a model to judge a model — every check is something you could verify by hand. Blocking checks fail the run; warnings flag it without failing. Runs stream results live, can be cancelled mid-flight, and are stored under `traces/pressure/` so the history survives a restart. Every replay they produce also lands in the replay log, tagged with the run it came from.
|
|
239
|
+
|
|
240
|
+
### Auto-explore on exit
|
|
241
|
+
|
|
242
|
+
Set the `REPLAY` env var and the explorer opens automatically when your script finishes:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
REPLAY=1 python my_agent.py
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
No code changes needed beyond `replay.init()`.
|
|
249
|
+
|
|
250
|
+
### Programmatic exploration
|
|
251
|
+
|
|
252
|
+
Open the explorer from inside your own process — useful when you want registered tools to actually execute during fork (CLI invocations are a separate process and can't reach back into your tool functions):
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
import replay
|
|
256
|
+
replay.init()
|
|
257
|
+
run_my_agent()
|
|
258
|
+
replay.explore() # open the latest trace
|
|
259
|
+
replay.explore("f6caa") # specific trace by partial ID
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Run-and-explore in one command
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
replay run my_agent.py
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Executes your script in the current process (so `@replay.tool` registrations stay live), captures the trace, and drops you straight into the explorer when it finishes.
|
|
269
|
+
|
|
270
|
+
### Diff replays against originals
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
replay diff <replay_id>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Side-by-side `before` / `after` comparison for every changed span: forked attributes, re-run LLM outputs, and which spans were cached vs. re-executed.
|
|
277
|
+
|
|
278
|
+
### Span enrichment
|
|
279
|
+
|
|
280
|
+
At export time, replay adds three attributes to every span so the engine never has to reconstruct data from flattened OTel attributes:
|
|
281
|
+
|
|
282
|
+
- `replay.messages_json` — full messages array on LLM spans
|
|
283
|
+
- `replay.tool_call_map_json` — `tool_call_id` → `tool_name` mapping
|
|
284
|
+
- `replay.tool_result` — unwrapped tool result (LangChain wraps results in JSON blobs; this is the clean version)
|
|
285
|
+
|
|
286
|
+
### Saved tool sources
|
|
287
|
+
|
|
288
|
+
The first time you run an agent script, replay snapshots your `@replay.tool` function definitions to `.replay/tool_sources.py`. That means you can `replay explore` later **without re-running the agent**, and the explorer can still execute your tools for real.
|
|
289
|
+
|
|
290
|
+
If your tool implementations change, refresh the snapshot:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
replay explore <trace_id> --reload-tools my_agent.py
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## CLI reference
|
|
299
|
+
|
|
300
|
+
| Command | What it does |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `replay list` | List all captured traces with span counts and durations |
|
|
303
|
+
| `replay show <trace_id>` | Render a trace as an indented timeline with inputs/outputs/tokens |
|
|
304
|
+
| `replay ids <trace_id>` | Same as `show`, but prints span IDs (useful before forking) |
|
|
305
|
+
| `replay fork <trace_id> <span_id> --set attr=value [--set ...]` | Fork at a span, override attributes, rerun downstream |
|
|
306
|
+
| `replay diff <replay_id>` | Compare a replay against its original trace |
|
|
307
|
+
| `replay explore <trace_id>` | Interactive TUI for navigating and forking |
|
|
308
|
+
| `replay run <script.py>` | Run an agent script and explore the resulting trace |
|
|
309
|
+
| `replay serve [--port N]` | Start the Replay Console — the web dashboard |
|
|
310
|
+
| `replay replays [--trace <id>]` | The replay log — every replay ever played, newest first |
|
|
311
|
+
| `replay prompts` | Every distinct system prompt across your traces, and which traces use it |
|
|
312
|
+
| `replay pressure --set-prompt "…"` | Pressure test a candidate system prompt against every trace |
|
|
313
|
+
| `replay pressure-log` | List every pressure run |
|
|
314
|
+
| `replay pressure-show <run_id>` | Full per-trace results of one pressure run |
|
|
315
|
+
|
|
316
|
+
All `<trace_id>`, `<span_id>`, `<replay_id>` and `<run_id>` arguments accept partial prefixes.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Quickstart
|
|
321
|
+
|
|
322
|
+
Assumes you've done the [Install](#install) above and your venv is active.
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
# my_agent.py
|
|
326
|
+
import replay
|
|
327
|
+
from langchain.agents import create_agent
|
|
328
|
+
# ...
|
|
329
|
+
|
|
330
|
+
replay.init() # one line
|
|
331
|
+
|
|
332
|
+
@replay.tool(safe=True)
|
|
333
|
+
def get_weather(city: str) -> str:
|
|
334
|
+
return f"It's sunny in {city}."
|
|
335
|
+
|
|
336
|
+
agent = create_agent(model="gpt-4", tools=[get_weather])
|
|
337
|
+
agent.invoke({"messages": [{"role": "user", "content": "Weather in Paris?"}]})
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
export OPENAI_API_KEY=sk-... # or put it in .env
|
|
342
|
+
python my_agent.py
|
|
343
|
+
replay list # see the captured trace
|
|
344
|
+
replay explore <trace_id> # fork at any step
|
|
345
|
+
replay serve # or browse everything at localhost:7823
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Architecture
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
replay/
|
|
354
|
+
__init__.py # replay.init(), replay.explore()
|
|
355
|
+
tools.py # @replay.tool decorator
|
|
356
|
+
cli.py # CLI commands (list, show, ids, fork, diff, explore,
|
|
357
|
+
# run, serve, replays, prompts, pressure*)
|
|
358
|
+
core/
|
|
359
|
+
setup.py # TracerProvider + OTel auto-instrumentation
|
|
360
|
+
engine.py # the replay engine — fork, cascade, downstream rerun
|
|
361
|
+
loader.py # reads traces from disk
|
|
362
|
+
enrichment.py # adds replay.* attributes at export time
|
|
363
|
+
tool_registry.py # tool declarations, preferences, pause decisions
|
|
364
|
+
spans.py # shared span classification/extraction (one source of truth
|
|
365
|
+
# for the CLI, server, replay log and pressure suite)
|
|
366
|
+
replay_log.py # indexes every played replay + builds original-vs-replay diffs
|
|
367
|
+
pressure.py # pressure suite — checks, run storage, threaded runner
|
|
368
|
+
span.py # span helpers
|
|
369
|
+
trace.py # trace helpers
|
|
370
|
+
tracer.py # tracer wrappers
|
|
371
|
+
exporters/
|
|
372
|
+
json_exporter.py # writes spans to local JSON files
|
|
373
|
+
server/
|
|
374
|
+
app.py # FastAPI API behind the Replay Console
|
|
375
|
+
static/ # dashboard frontend (index.html, app.js, style.css)
|
|
376
|
+
traces/ # captured traces (JSON)
|
|
377
|
+
*.replay.*.json # every replay played, the replay log reads these
|
|
378
|
+
pressure/*.json # one file per pressure run — config, results, verdict
|
|
379
|
+
.replay/
|
|
380
|
+
tool_preferences.json # saved CLI tool decisions
|
|
381
|
+
tool_sources.py # snapshotted @replay.tool sources
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Key design decisions:
|
|
385
|
+
- **Enrichment at export time, not via span processor** — some OTel implementations don't allow setting attributes after a span ends; the exporter is the only safe place
|
|
386
|
+
- **`tool_call_id_map` is built from all spans before the fork point**, not just ancestors, because in LangGraph the decision LLM span is a sibling of the tool spans, not an ancestor
|
|
387
|
+
- **Span type detection uses attributes, not span names**, so the engine works for any OTel-instrumented framework rather than being LangChain-specific
|
|
388
|
+
- **A pressure test is just a fork with a policy** — it reuses the same engine path as a manual system-prompt fork, so anything the console can replay by hand, the suite can replay across every trace
|
|
389
|
+
- **Pressure checks are deterministic** — no LLM-as-judge. Every verdict traces back to something you could check yourself, which is what makes a red result actionable
|
|
390
|
+
- **The replay files are the log** — nothing separate to keep in sync; `replay_log.py` indexes the `*.replay.*.json` the engine already writes
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Tech stack
|
|
395
|
+
|
|
396
|
+
Python 3.11 · OpenTelemetry (api, sdk) · `opentelemetry-instrumentation-openai` · `opentelemetry-instrumentation-langchain` · OpenAI Python SDK · LangChain 1.2 · `click` (CLI) · `blessed` (TUI) · `fastapi` + `uvicorn` (web UI)
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Status & limitations
|
|
401
|
+
|
|
402
|
+
- Tool **"run for real"** during replay only works when the explorer runs in the same process as your `@replay.tool` registrations (use `replay.explore()` programmatically, or `replay run my_agent.py`)
|
|
403
|
+
- JSON file storage — fine for local dev; production usage will want a real database
|
|
404
|
+
- Tested primarily with LangChain/LangGraph + OpenAI; other OTel-instrumented frameworks should work but have less coverage
|
|
405
|
+
- Streaming LLM responses are not yet handled
|
|
406
|
+
- Installs from source (`pip install -e .`) — not published to PyPI yet
|
|
407
|
+
- A pressure run costs one full agent replay per trace — real model calls, real spend. Start with a few traces before pointing it at everything.
|
|
408
|
+
- Pressure runs execute in the server/CLI process with a thread pool; there is no queue or worker, so a very large trace set is better split across runs
|