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.
Files changed (45) hide show
  1. {understudy-0.3.0 → understudy-0.4.0}/PKG-INFO +98 -26
  2. {understudy-0.3.0 → understudy-0.4.0}/README.md +89 -24
  3. {understudy-0.3.0 → understudy-0.4.0}/pyproject.toml +4 -2
  4. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/__init__.py +23 -7
  5. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/adk/__init__.py +30 -10
  6. understudy-0.4.0/src/understudy/check.py +296 -0
  7. understudy-0.4.0/src/understudy/cli.py +837 -0
  8. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/http/__init__.py +0 -6
  9. understudy-0.4.0/src/understudy/judges.py +214 -0
  10. understudy-0.4.0/src/understudy/langgraph/__init__.py +162 -0
  11. understudy-0.4.0/src/understudy/langgraph/tools.py +100 -0
  12. understudy-0.4.0/src/understudy/metrics/__init__.py +16 -0
  13. understudy-0.4.0/src/understudy/metrics/builtins.py +112 -0
  14. understudy-0.4.0/src/understudy/metrics/registry.py +90 -0
  15. understudy-0.4.0/src/understudy/metrics/templates/clarity.jinja +13 -0
  16. understudy-0.4.0/src/understudy/metrics/templates/policy_compliance.jinja +17 -0
  17. understudy-0.4.0/src/understudy/metrics/templates/tone_empathy.jinja +13 -0
  18. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/mocks.py +3 -5
  19. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/models.py +4 -2
  20. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/reports.py +74 -69
  21. understudy-0.4.0/src/understudy/runner.py +337 -0
  22. understudy-0.4.0/src/understudy/server/__init__.py +142 -0
  23. understudy-0.4.0/src/understudy/server/models.py +123 -0
  24. understudy-0.4.0/src/understudy/server/sessions.py +192 -0
  25. understudy-0.4.0/src/understudy/server/ui_simulator.py +160 -0
  26. understudy-0.4.0/src/understudy/storage.py +490 -0
  27. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/suite.py +24 -7
  28. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/templates/base.html +1 -5
  29. understudy-0.4.0/src/understudy/templates/index.html +271 -0
  30. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/templates/run_detail.html +53 -1
  31. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/trace.py +49 -20
  32. understudy-0.3.0/src/understudy/check.py +0 -138
  33. understudy-0.3.0/src/understudy/cli.py +0 -379
  34. understudy-0.3.0/src/understudy/judges.py +0 -105
  35. understudy-0.3.0/src/understudy/loaders/__init__.py +0 -0
  36. understudy-0.3.0/src/understudy/personas/__init__.py +0 -0
  37. understudy-0.3.0/src/understudy/runner.py +0 -183
  38. understudy-0.3.0/src/understudy/storage.py +0 -211
  39. understudy-0.3.0/src/understudy/templates/index.html +0 -116
  40. understudy-0.3.0/src/understudy/templates/metrics.html +0 -128
  41. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/compare.py +0 -0
  42. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/prompts/__init__.py +0 -0
  43. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/prompts/rubrics.py +0 -0
  44. {understudy-0.3.0 → understudy-0.4.0}/src/understudy/simulator.py +0 -0
  45. {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.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
  [![PyPI version](https://badge.fury.io/py/understudy.svg)](https://badge.fury.io/py/understudy)
49
- [![Downloads](https://pepy.tech/badge/understudy)](https://pepy.tech/project/understudy)
56
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/understudy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/project/understudy)
50
57
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
51
58
  [![Documentation](https://github.com/gojiplus/understudy/actions/workflows/docs.yml/badge.svg)](https://gojiplus.github.io/understudy/)
52
59
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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, handoffs, and terminal states, and then evaluates behavior with deterministic checks, optional LLM judges, and run reports.
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, terminal state).
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
- - [Test file](https://github.com/gojiplus/understudy/blob/main/example/test_returns.py) — pytest assertions against traces
71
- - [Sample report](https://htmlpreview.github.io/?https://github.com/gojiplus/understudy/blob/main/example/sample_report.html) — HTML output from `understudy report`
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
- allowed_terminal_states:
130
- - return_created
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, check
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.terminal_state == "return_created"
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 with Tags
159
+ ## Suites and Batch Runs
153
160
 
154
- Run multiple scenes and tag runs for comparison:
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
- # Tag runs for later comparison
163
- results = suite.run(app, mocks=mocks, storage=storage, tags={"version": "v1"})
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
- # List runs (shows tags)
171
- understudy list
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
- # Show run details
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 HTML reports
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** - percentage of scenes that passed all expectations
226
- - **Avg turns** - average conversation length
227
- - **Tool usage** - distribution of tool calls across runs
228
- - **Terminal states** - breakdown of how conversations ended
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
  [![PyPI version](https://badge.fury.io/py/understudy.svg)](https://badge.fury.io/py/understudy)
4
- [![Downloads](https://pepy.tech/badge/understudy)](https://pepy.tech/project/understudy)
4
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/understudy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/project/understudy)
5
5
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
6
6
  [![Documentation](https://github.com/gojiplus/understudy/actions/workflows/docs.yml/badge.svg)](https://gojiplus.github.io/understudy/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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, handoffs, and terminal states, and then evaluates behavior with deterministic checks, optional LLM judges, and run reports.
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, terminal state).
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
- - [Test file](https://github.com/gojiplus/understudy/blob/main/example/test_returns.py) — pytest assertions against traces
26
- - [Sample report](https://htmlpreview.github.io/?https://github.com/gojiplus/understudy/blob/main/example/sample_report.html) — HTML output from `understudy report`
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
- allowed_terminal_states:
85
- - return_created
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, check
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.terminal_state == "return_created"
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 with Tags
107
+ ## Suites and Batch Runs
108
108
 
109
- Run multiple scenes and tag runs for comparison:
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
- # Tag runs for later comparison
118
- results = suite.run(app, mocks=mocks, storage=storage, tags={"version": "v1"})
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
- # List runs (shows tags)
126
- understudy list
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
- # Show run details
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 HTML reports
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** - percentage of scenes that passed all expectations
181
- - **Avg turns** - average conversation length
182
- - **Tool usage** - distribution of tool calls across runs
183
- - **Terminal states** - breakdown of how conversations ended
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.3.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
- # runner
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
- # check for terminal state markers
212
- # convention: agent emits "TERMINAL_STATE: <state>"
213
- if "TERMINAL_STATE:" in text:
214
- state = text.split("TERMINAL_STATE:")[-1].strip()
215
- terminal_state = state.split()[0].strip()
216
- logger.debug("Terminal state: %s", terminal_state)
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
- response = AgentResponse(
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."""