understudy 0.3.0__tar.gz → 0.4.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.
- {understudy-0.3.0 → understudy-0.4.0}/PKG-INFO +98 -26
- {understudy-0.3.0 → understudy-0.4.0}/README.md +89 -24
- {understudy-0.3.0 → understudy-0.4.0}/pyproject.toml +4 -2
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/__init__.py +23 -7
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/adk/__init__.py +30 -10
- understudy-0.4.0/src/understudy/check.py +296 -0
- understudy-0.4.0/src/understudy/cli.py +837 -0
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/http/__init__.py +0 -6
- understudy-0.4.0/src/understudy/judges.py +214 -0
- understudy-0.4.0/src/understudy/langgraph/__init__.py +162 -0
- understudy-0.4.0/src/understudy/langgraph/tools.py +100 -0
- understudy-0.4.0/src/understudy/metrics/__init__.py +16 -0
- understudy-0.4.0/src/understudy/metrics/builtins.py +112 -0
- understudy-0.4.0/src/understudy/metrics/registry.py +90 -0
- understudy-0.4.0/src/understudy/metrics/templates/clarity.jinja +13 -0
- understudy-0.4.0/src/understudy/metrics/templates/policy_compliance.jinja +17 -0
- understudy-0.4.0/src/understudy/metrics/templates/tone_empathy.jinja +13 -0
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/mocks.py +3 -5
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/models.py +4 -2
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/reports.py +74 -69
- understudy-0.4.0/src/understudy/runner.py +337 -0
- understudy-0.4.0/src/understudy/server/__init__.py +142 -0
- understudy-0.4.0/src/understudy/server/models.py +123 -0
- understudy-0.4.0/src/understudy/server/sessions.py +192 -0
- understudy-0.4.0/src/understudy/server/ui_simulator.py +160 -0
- understudy-0.4.0/src/understudy/storage.py +490 -0
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/suite.py +24 -7
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/templates/base.html +1 -5
- understudy-0.4.0/src/understudy/templates/index.html +271 -0
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/templates/run_detail.html +53 -1
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/trace.py +49 -20
- understudy-0.3.0/src/understudy/check.py +0 -138
- understudy-0.3.0/src/understudy/cli.py +0 -379
- understudy-0.3.0/src/understudy/judges.py +0 -105
- understudy-0.3.0/src/understudy/loaders/__init__.py +0 -0
- understudy-0.3.0/src/understudy/personas/__init__.py +0 -0
- understudy-0.3.0/src/understudy/runner.py +0 -183
- understudy-0.3.0/src/understudy/storage.py +0 -211
- understudy-0.3.0/src/understudy/templates/index.html +0 -116
- understudy-0.3.0/src/understudy/templates/metrics.html +0 -128
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/compare.py +0 -0
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/prompts/__init__.py +0 -0
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/prompts/rubrics.py +0 -0
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/simulator.py +0 -0
- {understudy-0.3.0 → understudy-0.4.0}/src/understudy/templates/comparison.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: understudy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Simulation and trace-based evaluation for agentic systems
|
|
5
5
|
Keywords: agent,evaluation,simulation,testing,llm
|
|
6
6
|
Author: Gaurav Sood
|
|
@@ -17,7 +17,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
17
17
|
Requires-Dist: litellm>=1.50
|
|
18
18
|
Requires-Dist: google-adk>=1.0 ; extra == 'adk'
|
|
19
19
|
Requires-Dist: python-dotenv>=1.0 ; extra == 'adk'
|
|
20
|
-
Requires-Dist: understudy[adk,http,reports] ; extra == 'all'
|
|
20
|
+
Requires-Dist: understudy[adk,http,langgraph,server,reports] ; extra == 'all'
|
|
21
21
|
Requires-Dist: understudy[all,docs] ; extra == 'dev'
|
|
22
22
|
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
23
23
|
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
|
|
@@ -28,8 +28,13 @@ Requires-Dist: furo>=2024.0 ; extra == 'docs'
|
|
|
28
28
|
Requires-Dist: myst-parser>=3.0 ; extra == 'docs'
|
|
29
29
|
Requires-Dist: sphinx-autodoc-typehints>=2.0 ; extra == 'docs'
|
|
30
30
|
Requires-Dist: httpx>=0.27 ; extra == 'http'
|
|
31
|
+
Requires-Dist: langgraph>=0.2 ; extra == 'langgraph'
|
|
32
|
+
Requires-Dist: langchain-core>=0.3 ; extra == 'langgraph'
|
|
33
|
+
Requires-Dist: langchain-openai>=0.3 ; extra == 'langgraph'
|
|
31
34
|
Requires-Dist: jinja2>=3.0 ; extra == 'reports'
|
|
32
35
|
Requires-Dist: click>=8.0 ; extra == 'reports'
|
|
36
|
+
Requires-Dist: fastapi>=0.115 ; extra == 'server'
|
|
37
|
+
Requires-Dist: uvicorn>=0.32 ; extra == 'server'
|
|
33
38
|
Requires-Python: >=3.12
|
|
34
39
|
Project-URL: Homepage, https://github.com/gojiplus/understudy
|
|
35
40
|
Project-URL: Documentation, https://gojiplus.github.io/understudy
|
|
@@ -40,19 +45,21 @@ Provides-Extra: all
|
|
|
40
45
|
Provides-Extra: dev
|
|
41
46
|
Provides-Extra: docs
|
|
42
47
|
Provides-Extra: http
|
|
48
|
+
Provides-Extra: langgraph
|
|
43
49
|
Provides-Extra: reports
|
|
50
|
+
Provides-Extra: server
|
|
44
51
|
Description-Content-Type: text/markdown
|
|
45
52
|
|
|
46
53
|
## understudy: Scenario Testing for AI Agents
|
|
47
54
|
|
|
48
55
|
[](https://badge.fury.io/py/understudy)
|
|
49
|
-
[](https://pepy.tech/project/understudy)
|
|
56
|
+
[](https://pepy.tech/project/understudy)
|
|
50
57
|
[](https://www.python.org/downloads/)
|
|
51
58
|
[](https://gojiplus.github.io/understudy/)
|
|
52
59
|
[](https://opensource.org/licenses/MIT)
|
|
53
60
|
|
|
54
61
|
|
|
55
|
-
Understudy is a scenario-driven testing framework for AI agents that simulates realistic multi-turn users, runs those scenes against an agent through a simple app adapter, records a structured execution trace of messages, tool calls,
|
|
62
|
+
Understudy is a scenario-driven testing framework for AI agents that simulates realistic multi-turn users, runs those scenes against an agent through a simple app adapter, records a structured execution trace of messages, tool calls, and handoffs, and then evaluates behavior with deterministic checks, optional LLM judges, and run reports.
|
|
56
63
|
|
|
57
64
|
## How It Works
|
|
58
65
|
|
|
@@ -63,12 +70,12 @@ Testing with understudy is **4 steps**:
|
|
|
63
70
|
3. **Write scenes** — YAML files defining what the simulated user wants and what you expect
|
|
64
71
|
4. **Run and assert** — Execute simulations, check traces, generate reports
|
|
65
72
|
|
|
66
|
-
The key insight: **assert against the trace, not the prose**. Don't check what the agent said—check what it did (tool calls
|
|
73
|
+
The key insight: **assert against the trace, not the prose**. Don't check what the agent said—check what it did (tool calls).
|
|
67
74
|
|
|
68
75
|
**See real examples:**
|
|
69
76
|
- [Example scene](https://github.com/gojiplus/understudy/blob/main/example/scenes/return_eligible_backpack.yaml) — YAML defining a test scenario
|
|
70
|
-
- [
|
|
71
|
-
- [
|
|
77
|
+
- [ADK test file](https://github.com/gojiplus/understudy/blob/main/example/adk/test_returns.py) — pytest assertions against traces
|
|
78
|
+
- [LangGraph test file](https://github.com/gojiplus/understudy/blob/main/example/langgraph/test_returns.py) — same tests, different framework
|
|
72
79
|
|
|
73
80
|
## Installation
|
|
74
81
|
|
|
@@ -126,21 +133,21 @@ expectations:
|
|
|
126
133
|
required_tools:
|
|
127
134
|
- lookup_order
|
|
128
135
|
- create_return
|
|
129
|
-
|
|
130
|
-
-
|
|
136
|
+
forbidden_tools:
|
|
137
|
+
- issue_refund
|
|
131
138
|
```
|
|
132
139
|
|
|
133
140
|
### 4. Run simulation
|
|
134
141
|
|
|
135
142
|
```python
|
|
136
|
-
from understudy import Scene, run
|
|
143
|
+
from understudy import Scene, run
|
|
137
144
|
|
|
138
145
|
scene = Scene.from_file("scenes/return_backpack.yaml")
|
|
139
146
|
trace = run(app, scene, mocks=mocks)
|
|
140
147
|
|
|
141
148
|
assert trace.called("lookup_order")
|
|
142
149
|
assert trace.called("create_return")
|
|
143
|
-
assert trace.
|
|
150
|
+
assert not trace.called("issue_refund")
|
|
144
151
|
```
|
|
145
152
|
|
|
146
153
|
Or with pytest (define `app` and `mocks` fixtures in conftest.py):
|
|
@@ -149,9 +156,9 @@ Or with pytest (define `app` and `mocks` fixtures in conftest.py):
|
|
|
149
156
|
pytest test_returns.py -v
|
|
150
157
|
```
|
|
151
158
|
|
|
152
|
-
## Suites
|
|
159
|
+
## Suites and Batch Runs
|
|
153
160
|
|
|
154
|
-
Run multiple scenes
|
|
161
|
+
Run multiple scenes with multiple simulations per scene:
|
|
155
162
|
|
|
156
163
|
```python
|
|
157
164
|
from understudy import Suite, RunStorage
|
|
@@ -159,33 +166,98 @@ from understudy import Suite, RunStorage
|
|
|
159
166
|
suite = Suite.from_directory("scenes/")
|
|
160
167
|
storage = RunStorage()
|
|
161
168
|
|
|
162
|
-
#
|
|
163
|
-
results = suite.run(
|
|
169
|
+
# Run each scene 3 times and tag for comparison
|
|
170
|
+
results = suite.run(
|
|
171
|
+
app,
|
|
172
|
+
mocks=mocks,
|
|
173
|
+
storage=storage,
|
|
174
|
+
n_sims=3,
|
|
175
|
+
tags={"version": "v1"},
|
|
176
|
+
)
|
|
164
177
|
print(f"{results.pass_count}/{len(results.results)} passed")
|
|
165
178
|
```
|
|
166
179
|
|
|
180
|
+
## Simulation and Evaluation
|
|
181
|
+
|
|
182
|
+
Understudy separates simulation (generating traces) from evaluation (checking traces). Use together or separately:
|
|
183
|
+
|
|
184
|
+
### Combined (most common)
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
understudy run \
|
|
188
|
+
--app mymodule:agent_app \
|
|
189
|
+
--scene ./scenes/ \
|
|
190
|
+
--n-sims 3 \
|
|
191
|
+
--junit results.xml
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Separate workflows
|
|
195
|
+
|
|
196
|
+
Generate traces only:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
understudy simulate \
|
|
200
|
+
--app mymodule:agent_app \
|
|
201
|
+
--scenes ./scenes/ \
|
|
202
|
+
--output ./traces/ \
|
|
203
|
+
--n-sims 3
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Evaluate existing traces:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
understudy evaluate \
|
|
210
|
+
--traces ./traces/ \
|
|
211
|
+
--output ./results/ \
|
|
212
|
+
--junit results.xml
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Python API:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from understudy import simulate_batch, evaluate_batch
|
|
219
|
+
|
|
220
|
+
# Generate traces
|
|
221
|
+
traces = simulate_batch(
|
|
222
|
+
app=agent_app,
|
|
223
|
+
scenes="./scenes/",
|
|
224
|
+
n_sims=3,
|
|
225
|
+
output="./traces/",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Evaluate later
|
|
229
|
+
results = evaluate_batch(
|
|
230
|
+
traces="./traces/",
|
|
231
|
+
output="./results/",
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
167
235
|
## CLI Commands
|
|
168
236
|
|
|
169
237
|
```bash
|
|
170
|
-
#
|
|
171
|
-
understudy
|
|
238
|
+
# Run simulations
|
|
239
|
+
understudy run --app mymodule:app --scene ./scenes/
|
|
240
|
+
understudy simulate --app mymodule:app --scenes ./scenes/
|
|
241
|
+
understudy evaluate --traces ./traces/
|
|
172
242
|
|
|
173
|
-
#
|
|
243
|
+
# View results
|
|
244
|
+
understudy list
|
|
174
245
|
understudy show <run_id>
|
|
175
|
-
|
|
176
|
-
# Aggregate metrics
|
|
177
246
|
understudy summary
|
|
178
247
|
|
|
179
248
|
# Compare runs by tag
|
|
180
249
|
understudy compare --tag version --before v1 --after v2
|
|
181
250
|
|
|
182
|
-
# Generate
|
|
251
|
+
# Generate reports
|
|
183
252
|
understudy report -o report.html
|
|
184
253
|
understudy compare --tag version --before v1 --after v2 --html comparison.html
|
|
185
254
|
|
|
186
255
|
# Interactive browser
|
|
187
256
|
understudy serve --port 8080
|
|
188
257
|
|
|
258
|
+
# HTTP simulator server (for browser/UI testing)
|
|
259
|
+
understudy serve-api --port 8000
|
|
260
|
+
|
|
189
261
|
# Cleanup
|
|
190
262
|
understudy delete <run_id>
|
|
191
263
|
understudy clear
|
|
@@ -222,11 +294,10 @@ from understudy.judges import (
|
|
|
222
294
|
## Report Contents
|
|
223
295
|
|
|
224
296
|
The `understudy summary` command shows:
|
|
225
|
-
- **Pass rate**
|
|
226
|
-
- **Avg turns**
|
|
227
|
-
- **Tool usage**
|
|
228
|
-
- **
|
|
229
|
-
- **Agents** - which agents were invoked
|
|
297
|
+
- **Pass rate** — percentage of scenes that passed all expectations
|
|
298
|
+
- **Avg turns** — average conversation length
|
|
299
|
+
- **Tool usage** — distribution of tool calls across runs
|
|
300
|
+
- **Agents** — which agents were invoked
|
|
230
301
|
|
|
231
302
|
The HTML report (`understudy report`) includes:
|
|
232
303
|
- All metrics above
|
|
@@ -241,6 +312,7 @@ See the [full documentation](https://gojiplus.github.io/understudy) for:
|
|
|
241
312
|
- [Installation guide](https://gojiplus.github.io/understudy/installation.html)
|
|
242
313
|
- [Writing scenes](https://gojiplus.github.io/understudy/tutorial/scenes.html)
|
|
243
314
|
- [ADK integration](https://gojiplus.github.io/understudy/adk-integration.html)
|
|
315
|
+
- [LangGraph integration](https://gojiplus.github.io/understudy/langgraph-integration.html)
|
|
244
316
|
- [HTTP client for deployed agents](https://gojiplus.github.io/understudy/tutorial/http.html)
|
|
245
317
|
- [API reference](https://gojiplus.github.io/understudy/api/index.html)
|
|
246
318
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
## understudy: Scenario Testing for AI Agents
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/py/understudy)
|
|
4
|
-
[](https://pepy.tech/project/understudy)
|
|
4
|
+
[](https://pepy.tech/project/understudy)
|
|
5
5
|
[](https://www.python.org/downloads/)
|
|
6
6
|
[](https://gojiplus.github.io/understudy/)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
Understudy is a scenario-driven testing framework for AI agents that simulates realistic multi-turn users, runs those scenes against an agent through a simple app adapter, records a structured execution trace of messages, tool calls,
|
|
10
|
+
Understudy is a scenario-driven testing framework for AI agents that simulates realistic multi-turn users, runs those scenes against an agent through a simple app adapter, records a structured execution trace of messages, tool calls, and handoffs, and then evaluates behavior with deterministic checks, optional LLM judges, and run reports.
|
|
11
11
|
|
|
12
12
|
## How It Works
|
|
13
13
|
|
|
@@ -18,12 +18,12 @@ Testing with understudy is **4 steps**:
|
|
|
18
18
|
3. **Write scenes** — YAML files defining what the simulated user wants and what you expect
|
|
19
19
|
4. **Run and assert** — Execute simulations, check traces, generate reports
|
|
20
20
|
|
|
21
|
-
The key insight: **assert against the trace, not the prose**. Don't check what the agent said—check what it did (tool calls
|
|
21
|
+
The key insight: **assert against the trace, not the prose**. Don't check what the agent said—check what it did (tool calls).
|
|
22
22
|
|
|
23
23
|
**See real examples:**
|
|
24
24
|
- [Example scene](https://github.com/gojiplus/understudy/blob/main/example/scenes/return_eligible_backpack.yaml) — YAML defining a test scenario
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
25
|
+
- [ADK test file](https://github.com/gojiplus/understudy/blob/main/example/adk/test_returns.py) — pytest assertions against traces
|
|
26
|
+
- [LangGraph test file](https://github.com/gojiplus/understudy/blob/main/example/langgraph/test_returns.py) — same tests, different framework
|
|
27
27
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
@@ -81,21 +81,21 @@ expectations:
|
|
|
81
81
|
required_tools:
|
|
82
82
|
- lookup_order
|
|
83
83
|
- create_return
|
|
84
|
-
|
|
85
|
-
-
|
|
84
|
+
forbidden_tools:
|
|
85
|
+
- issue_refund
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
### 4. Run simulation
|
|
89
89
|
|
|
90
90
|
```python
|
|
91
|
-
from understudy import Scene, run
|
|
91
|
+
from understudy import Scene, run
|
|
92
92
|
|
|
93
93
|
scene = Scene.from_file("scenes/return_backpack.yaml")
|
|
94
94
|
trace = run(app, scene, mocks=mocks)
|
|
95
95
|
|
|
96
96
|
assert trace.called("lookup_order")
|
|
97
97
|
assert trace.called("create_return")
|
|
98
|
-
assert trace.
|
|
98
|
+
assert not trace.called("issue_refund")
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
Or with pytest (define `app` and `mocks` fixtures in conftest.py):
|
|
@@ -104,9 +104,9 @@ Or with pytest (define `app` and `mocks` fixtures in conftest.py):
|
|
|
104
104
|
pytest test_returns.py -v
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
## Suites
|
|
107
|
+
## Suites and Batch Runs
|
|
108
108
|
|
|
109
|
-
Run multiple scenes
|
|
109
|
+
Run multiple scenes with multiple simulations per scene:
|
|
110
110
|
|
|
111
111
|
```python
|
|
112
112
|
from understudy import Suite, RunStorage
|
|
@@ -114,33 +114,98 @@ from understudy import Suite, RunStorage
|
|
|
114
114
|
suite = Suite.from_directory("scenes/")
|
|
115
115
|
storage = RunStorage()
|
|
116
116
|
|
|
117
|
-
#
|
|
118
|
-
results = suite.run(
|
|
117
|
+
# Run each scene 3 times and tag for comparison
|
|
118
|
+
results = suite.run(
|
|
119
|
+
app,
|
|
120
|
+
mocks=mocks,
|
|
121
|
+
storage=storage,
|
|
122
|
+
n_sims=3,
|
|
123
|
+
tags={"version": "v1"},
|
|
124
|
+
)
|
|
119
125
|
print(f"{results.pass_count}/{len(results.results)} passed")
|
|
120
126
|
```
|
|
121
127
|
|
|
128
|
+
## Simulation and Evaluation
|
|
129
|
+
|
|
130
|
+
Understudy separates simulation (generating traces) from evaluation (checking traces). Use together or separately:
|
|
131
|
+
|
|
132
|
+
### Combined (most common)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
understudy run \
|
|
136
|
+
--app mymodule:agent_app \
|
|
137
|
+
--scene ./scenes/ \
|
|
138
|
+
--n-sims 3 \
|
|
139
|
+
--junit results.xml
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Separate workflows
|
|
143
|
+
|
|
144
|
+
Generate traces only:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
understudy simulate \
|
|
148
|
+
--app mymodule:agent_app \
|
|
149
|
+
--scenes ./scenes/ \
|
|
150
|
+
--output ./traces/ \
|
|
151
|
+
--n-sims 3
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Evaluate existing traces:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
understudy evaluate \
|
|
158
|
+
--traces ./traces/ \
|
|
159
|
+
--output ./results/ \
|
|
160
|
+
--junit results.xml
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Python API:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from understudy import simulate_batch, evaluate_batch
|
|
167
|
+
|
|
168
|
+
# Generate traces
|
|
169
|
+
traces = simulate_batch(
|
|
170
|
+
app=agent_app,
|
|
171
|
+
scenes="./scenes/",
|
|
172
|
+
n_sims=3,
|
|
173
|
+
output="./traces/",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Evaluate later
|
|
177
|
+
results = evaluate_batch(
|
|
178
|
+
traces="./traces/",
|
|
179
|
+
output="./results/",
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
122
183
|
## CLI Commands
|
|
123
184
|
|
|
124
185
|
```bash
|
|
125
|
-
#
|
|
126
|
-
understudy
|
|
186
|
+
# Run simulations
|
|
187
|
+
understudy run --app mymodule:app --scene ./scenes/
|
|
188
|
+
understudy simulate --app mymodule:app --scenes ./scenes/
|
|
189
|
+
understudy evaluate --traces ./traces/
|
|
127
190
|
|
|
128
|
-
#
|
|
191
|
+
# View results
|
|
192
|
+
understudy list
|
|
129
193
|
understudy show <run_id>
|
|
130
|
-
|
|
131
|
-
# Aggregate metrics
|
|
132
194
|
understudy summary
|
|
133
195
|
|
|
134
196
|
# Compare runs by tag
|
|
135
197
|
understudy compare --tag version --before v1 --after v2
|
|
136
198
|
|
|
137
|
-
# Generate
|
|
199
|
+
# Generate reports
|
|
138
200
|
understudy report -o report.html
|
|
139
201
|
understudy compare --tag version --before v1 --after v2 --html comparison.html
|
|
140
202
|
|
|
141
203
|
# Interactive browser
|
|
142
204
|
understudy serve --port 8080
|
|
143
205
|
|
|
206
|
+
# HTTP simulator server (for browser/UI testing)
|
|
207
|
+
understudy serve-api --port 8000
|
|
208
|
+
|
|
144
209
|
# Cleanup
|
|
145
210
|
understudy delete <run_id>
|
|
146
211
|
understudy clear
|
|
@@ -177,11 +242,10 @@ from understudy.judges import (
|
|
|
177
242
|
## Report Contents
|
|
178
243
|
|
|
179
244
|
The `understudy summary` command shows:
|
|
180
|
-
- **Pass rate**
|
|
181
|
-
- **Avg turns**
|
|
182
|
-
- **Tool usage**
|
|
183
|
-
- **
|
|
184
|
-
- **Agents** - which agents were invoked
|
|
245
|
+
- **Pass rate** — percentage of scenes that passed all expectations
|
|
246
|
+
- **Avg turns** — average conversation length
|
|
247
|
+
- **Tool usage** — distribution of tool calls across runs
|
|
248
|
+
- **Agents** — which agents were invoked
|
|
185
249
|
|
|
186
250
|
The HTML report (`understudy report`) includes:
|
|
187
251
|
- All metrics above
|
|
@@ -196,6 +260,7 @@ See the [full documentation](https://gojiplus.github.io/understudy) for:
|
|
|
196
260
|
- [Installation guide](https://gojiplus.github.io/understudy/installation.html)
|
|
197
261
|
- [Writing scenes](https://gojiplus.github.io/understudy/tutorial/scenes.html)
|
|
198
262
|
- [ADK integration](https://gojiplus.github.io/understudy/adk-integration.html)
|
|
263
|
+
- [LangGraph integration](https://gojiplus.github.io/understudy/langgraph-integration.html)
|
|
199
264
|
- [HTTP client for deployed agents](https://gojiplus.github.io/understudy/tutorial/http.html)
|
|
200
265
|
- [API reference](https://gojiplus.github.io/understudy/api/index.html)
|
|
201
266
|
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "understudy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Simulation and trace-based evaluation for agentic systems"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -31,6 +31,8 @@ dependencies = [
|
|
|
31
31
|
[project.optional-dependencies]
|
|
32
32
|
adk = ["google-adk>=1.0", "python-dotenv>=1.0"]
|
|
33
33
|
http = ["httpx>=0.27"]
|
|
34
|
+
langgraph = ["langgraph>=0.2", "langchain-core>=0.3", "langchain-openai>=0.3"]
|
|
35
|
+
server = ["fastapi>=0.115", "uvicorn>=0.32"]
|
|
34
36
|
reports = ["jinja2>=3.0", "click>=8.0"]
|
|
35
37
|
docs = [
|
|
36
38
|
"sphinx>=7.0",
|
|
@@ -38,7 +40,7 @@ docs = [
|
|
|
38
40
|
"myst-parser>=3.0",
|
|
39
41
|
"sphinx-autodoc-typehints>=2.0",
|
|
40
42
|
]
|
|
41
|
-
all = ["understudy[adk,http,reports]"]
|
|
43
|
+
all = ["understudy[adk,http,langgraph,server,reports]"]
|
|
42
44
|
dev = [
|
|
43
45
|
"understudy[all,docs]",
|
|
44
46
|
"pytest>=8.0",
|
|
@@ -7,9 +7,10 @@ not by reading the script, but by inspecting what actually happened.
|
|
|
7
7
|
|
|
8
8
|
from importlib.metadata import version
|
|
9
9
|
|
|
10
|
-
from .check import CheckItem, CheckResult, check
|
|
10
|
+
from .check import CheckItem, CheckResult, EvaluationResult, check, evaluate, evaluate_batch
|
|
11
11
|
from .compare import ComparisonResult, SceneComparison, compare_runs
|
|
12
|
-
from .judges import Judge, JudgeResult
|
|
12
|
+
from .judges import FailureAnalysis, FailureAnalyzer, Judge, JudgeResult
|
|
13
|
+
from .metrics import MetricRegistry, MetricResult
|
|
13
14
|
from .mocks import MockToolkit, ToolError
|
|
14
15
|
from .models import Expectations, Persona, PersonaPreset, Scene
|
|
15
16
|
from .prompts import (
|
|
@@ -21,11 +22,11 @@ from .prompts import (
|
|
|
21
22
|
TONE_EMPATHY,
|
|
22
23
|
TOOL_USAGE_CORRECTNESS,
|
|
23
24
|
)
|
|
24
|
-
from .runner import AgentApp, AgentResponse, LiteLLMBackend, run
|
|
25
|
+
from .runner import AgentApp, AgentResponse, LiteLLMBackend, run, simulate, simulate_batch
|
|
25
26
|
from .simulator import Simulator, SimulatorBackend
|
|
26
|
-
from .storage import RunStorage
|
|
27
|
+
from .storage import EvaluationStorage, RunStorage, TraceStorage
|
|
27
28
|
from .suite import SceneResult, Suite, SuiteResults
|
|
28
|
-
from .trace import AgentTransfer, ToolCall, Trace, Turn
|
|
29
|
+
from .trace import AgentTransfer, StateSnapshot, ToolCall, Trace, TraceMetrics, Turn, TurnMetrics
|
|
29
30
|
|
|
30
31
|
__version__ = version("understudy")
|
|
31
32
|
|
|
@@ -40,21 +41,34 @@ __all__ = [
|
|
|
40
41
|
"Turn",
|
|
41
42
|
"ToolCall",
|
|
42
43
|
"AgentTransfer",
|
|
43
|
-
|
|
44
|
+
"TraceMetrics",
|
|
45
|
+
"TurnMetrics",
|
|
46
|
+
"StateSnapshot",
|
|
47
|
+
# metrics
|
|
48
|
+
"MetricRegistry",
|
|
49
|
+
"MetricResult",
|
|
50
|
+
# runner / simulate
|
|
44
51
|
"run",
|
|
52
|
+
"simulate",
|
|
53
|
+
"simulate_batch",
|
|
45
54
|
"AgentApp",
|
|
46
55
|
"AgentResponse",
|
|
47
56
|
"LiteLLMBackend",
|
|
48
|
-
# check
|
|
57
|
+
# check / evaluate
|
|
49
58
|
"check",
|
|
59
|
+
"evaluate",
|
|
60
|
+
"evaluate_batch",
|
|
50
61
|
"CheckResult",
|
|
51
62
|
"CheckItem",
|
|
63
|
+
"EvaluationResult",
|
|
52
64
|
# suite
|
|
53
65
|
"Suite",
|
|
54
66
|
"SuiteResults",
|
|
55
67
|
"SceneResult",
|
|
56
68
|
# storage
|
|
57
69
|
"RunStorage",
|
|
70
|
+
"TraceStorage",
|
|
71
|
+
"EvaluationStorage",
|
|
58
72
|
# compare
|
|
59
73
|
"compare_runs",
|
|
60
74
|
"ComparisonResult",
|
|
@@ -62,6 +76,8 @@ __all__ = [
|
|
|
62
76
|
# judges
|
|
63
77
|
"Judge",
|
|
64
78
|
"JudgeResult",
|
|
79
|
+
"FailureAnalyzer",
|
|
80
|
+
"FailureAnalysis",
|
|
65
81
|
# mocks
|
|
66
82
|
"MockToolkit",
|
|
67
83
|
"ToolError",
|
|
@@ -147,6 +147,9 @@ class ADKApp(AgentApp):
|
|
|
147
147
|
agent_text_parts: list[str] = []
|
|
148
148
|
terminal_state: str | None = None
|
|
149
149
|
current_agent_name = self._current_agent
|
|
150
|
+
input_tokens = 0
|
|
151
|
+
output_tokens = 0
|
|
152
|
+
thinking_tokens = 0
|
|
150
153
|
|
|
151
154
|
async for event in self._runner.run_async(
|
|
152
155
|
user_id="understudy_user",
|
|
@@ -208,23 +211,40 @@ class ADKApp(AgentApp):
|
|
|
208
211
|
if text:
|
|
209
212
|
agent_text_parts.append(text)
|
|
210
213
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
# capture token usage from usage_metadata
|
|
215
|
+
if hasattr(event, "usage_metadata") and event.usage_metadata:
|
|
216
|
+
usage = event.usage_metadata
|
|
217
|
+
input_tokens += getattr(usage, "prompt_token_count", 0) or 0
|
|
218
|
+
output_tokens += getattr(usage, "candidates_token_count", 0) or 0
|
|
219
|
+
thinking_tokens += getattr(usage, "thoughts_token_count", 0) or 0
|
|
220
|
+
|
|
221
|
+
# detect if agent signaled conversation end via escalate action
|
|
222
|
+
if (
|
|
223
|
+
hasattr(event, "actions")
|
|
224
|
+
and event.actions
|
|
225
|
+
and getattr(event.actions, "escalate", False)
|
|
226
|
+
):
|
|
227
|
+
terminal_state = "agent_ended"
|
|
228
|
+
logger.debug("Agent escalated (ended conversation)")
|
|
217
229
|
|
|
218
230
|
self._current_agent = current_agent_name
|
|
219
231
|
|
|
220
|
-
|
|
232
|
+
# capture state snapshot from session
|
|
233
|
+
state_snapshot = None
|
|
234
|
+
if self._session and hasattr(self._session, "state"):
|
|
235
|
+
state_snapshot = dict(self._session.state) if self._session.state else None
|
|
236
|
+
|
|
237
|
+
return AgentResponse(
|
|
221
238
|
content=" ".join(agent_text_parts),
|
|
222
239
|
tool_calls=tool_calls,
|
|
223
240
|
terminal_state=terminal_state,
|
|
241
|
+
agent_name=current_agent_name,
|
|
242
|
+
agent_transfers=list(self._agent_transfers),
|
|
243
|
+
input_tokens=input_tokens,
|
|
244
|
+
output_tokens=output_tokens,
|
|
245
|
+
thinking_tokens=thinking_tokens,
|
|
246
|
+
state_snapshot=state_snapshot,
|
|
224
247
|
)
|
|
225
|
-
response.agent_name = current_agent_name
|
|
226
|
-
response.agent_transfers = list(self._agent_transfers)
|
|
227
|
-
return response
|
|
228
248
|
|
|
229
249
|
def stop(self) -> None:
|
|
230
250
|
"""Clean up the ADK session."""
|