agentverify 0.3.0__tar.gz → 0.5.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.
- {agentverify-0.3.0 → agentverify-0.5.0}/PKG-INFO +180 -84
- {agentverify-0.3.0 → agentverify-0.5.0}/README.md +178 -83
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/__init__.py +19 -2
- agentverify-0.5.0/agentverify/_step_builder.py +148 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/assertions.py +276 -70
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/cassette/adapters/anthropic.py +50 -24
- agentverify-0.5.0/agentverify/cassette/adapters/base.py +264 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/cassette/adapters/bedrock.py +42 -18
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/cassette/adapters/gemini.py +45 -17
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/cassette/adapters/litellm.py +36 -17
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/cassette/adapters/openai.py +86 -54
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/cassette/io.py +1 -3
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/cassette/recorder.py +36 -24
- agentverify-0.5.0/agentverify/cassette/sanitize.py +223 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/errors.py +85 -2
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/fixtures.py +4 -2
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/frameworks/__init__.py +1 -2
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/frameworks/langchain.py +8 -10
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/frameworks/langgraph.py +17 -11
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/frameworks/openai_agents.py +42 -18
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/frameworks/strands.py +15 -10
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/markers.py +1 -2
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/matchers.py +3 -6
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/mocking.py +12 -22
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/models.py +52 -38
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/plugin.py +1 -3
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/probe.py +14 -20
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify.egg-info/PKG-INFO +180 -84
- {agentverify-0.3.0 → agentverify-0.5.0}/pyproject.toml +2 -1
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_assertions.py +405 -36
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_errors.py +83 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_fixtures.py +12 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_init.py +13 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_mocking.py +30 -2
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_models.py +80 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_probe.py +48 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_properties.py +143 -2
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_recorder.py +125 -4
- agentverify-0.5.0/tests/test_sanitize.py +472 -0
- agentverify-0.5.0/tests/test_step_builder.py +175 -0
- agentverify-0.3.0/agentverify/_step_builder.py +0 -73
- agentverify-0.3.0/agentverify/cassette/adapters/base.py +0 -72
- agentverify-0.3.0/agentverify/cassette/sanitize.py +0 -120
- agentverify-0.3.0/tests/test_sanitize.py +0 -220
- agentverify-0.3.0/tests/test_step_builder.py +0 -84
- {agentverify-0.3.0 → agentverify-0.5.0}/LICENSE +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/cassette/__init__.py +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/cassette/adapters/__init__.py +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify/py.typed +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify.egg-info/SOURCES.txt +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify.egg-info/dependency_links.txt +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify.egg-info/entry_points.txt +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify.egg-info/requires.txt +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/agentverify.egg-info/top_level.txt +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/setup.cfg +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_base_adapter.py +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_cassette_io.py +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_markers.py +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_matchers.py +0 -0
- {agentverify-0.3.0 → agentverify-0.5.0}/tests/test_plugin.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentverify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Deterministic testing for AI agents. Assert agent actions, not vibes.
|
|
5
5
|
Author: simukappu
|
|
6
6
|
License: MIT
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
22
23
|
Classifier: Topic :: Software Development :: Testing
|
|
23
24
|
Requires-Python: >=3.10
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
@@ -56,25 +57,36 @@ Dynamic: license-file
|
|
|
56
57
|
|
|
57
58
|
**pytest for AI agents.** Assert agent actions, not vibes.
|
|
58
59
|
|
|
59
|
-
agentverify is a pytest plugin for deterministic testing of AI agent actions. Record real LLM calls once and replay them in CI with zero cost, or run against a live LLM with
|
|
60
|
+
agentverify is a pytest plugin for deterministic testing of AI agent actions. Record real LLM calls once and replay them in CI with zero cost, or run against a live LLM with flexible matchers that tolerate prompt variation. Either way, assert exactly what your agent did: which tools it called, what data flowed between steps, how much it cost, and whether it stayed within your safety rules.
|
|
61
|
+
|
|
62
|
+

|
|
60
63
|
|
|
61
64
|
Specifically:
|
|
62
65
|
|
|
63
|
-
- **Tool call sequences
|
|
64
|
-
- **Step-level execution
|
|
65
|
-
- **
|
|
66
|
-
- **
|
|
66
|
+
- **Tool call sequences**: exact order, subsequence, or set membership, with regex / wildcard argument matching
|
|
67
|
+
- **Step-level execution**: each LLM call is a step; assert what tool was called at step N, what the step's output was, and that step N's input references data produced by step M
|
|
68
|
+
- **Tool outcomes**: whether each tool call actually returned a usable result (vs. errored, timed out, or came back empty), what the result contained, and how many times a flaky tool was retried
|
|
69
|
+
- **Budgets**: token counts, cost in USD, end-to-end latency
|
|
70
|
+
- **Safety**: a list of tools that must never be called, no matter what the LLM decides
|
|
71
|
+
|
|
72
|
+
Built-in adapters cover LangChain, LangGraph, Strands Agents, and OpenAI Agents SDK. A small [custom converter](#custom-converters) plugs in anything else, including pure-Python ReAct loops, the Anthropic / OpenAI SDK directly, and frameworks not listed above. LLM providers supported: OpenAI, Amazon Bedrock, Google Gemini, Anthropic, and LiteLLM. Cassettes are human-readable YAML. Commit them to git, review in PRs, run in CI without an API key.
|
|
73
|
+
|
|
74
|
+
## Why I built this
|
|
67
75
|
|
|
68
|
-
|
|
76
|
+
When I started building agents that call tools in a loop, I ran into a failure mode that is easy to introduce and easy to miss: the agent calls the wrong tool, ignores a tool result, or quietly retries a failing call until the budget runs over. I wanted those caught in CI the way any other regression is. But the usual options did not fit. Mocking everything proves nothing, and running against a real LLM is unreliable, costs money, and needs an API key in CI.
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
What I wanted was boring. Run the agent once, record what it did, and assert on it in CI like any other function: the tool sequence, the arguments, the cost, the steps where one tool's output feeds the next. Review the recording in the PR next to the prompt change that produced it. No judge model, no scoring, no cloud account. agentverify is deliberately narrow, and the cassette-replay path is what keeps those tests in CI without a key or a bill.
|
|
79
|
+
|
|
80
|
+
This is probably for you if:
|
|
71
81
|
|
|
72
82
|
- You've shipped an agent that calls tools in a loop and need CI-safe regression tests for it
|
|
73
|
-
-
|
|
83
|
+
- You've hit bugs like "it ignored the tool result" or "it called the wrong tool"
|
|
74
84
|
- You want prompt and tool changes reviewed in PRs via a concrete diff, not an informal "looks fine locally" signoff
|
|
75
85
|
- You're building on MCP servers and need to pin down which tools get called with which arguments
|
|
76
86
|
- You want token, cost, and latency budgets enforced in CI, not caught only on the billing dashboard
|
|
77
87
|
|
|
88
|
+
This is the **regression-test layer** for agents: deterministic assertions on what the agent did. It's distinct from quality evaluation (LLM-as-judge scoring on open-ended output) and production trace observability, both of which are often used alongside agentverify rather than instead of it. See [How agentverify compares](#how-agentverify-compares) for when to reach for each.
|
|
89
|
+
|
|
78
90
|
## Install
|
|
79
91
|
|
|
80
92
|
```bash
|
|
@@ -83,7 +95,7 @@ pip install agentverify
|
|
|
83
95
|
|
|
84
96
|
## Quick Start
|
|
85
97
|
|
|
86
|
-
|
|
98
|
+
After [installing](#install), copy this into `test_agent.py` and run `pytest`. No LLM calls, no API keys, no cassettes needed. Just pure assertions on an `ExecutionResult`.
|
|
87
99
|
|
|
88
100
|
```python
|
|
89
101
|
from agentverify import (
|
|
@@ -118,8 +130,6 @@ def test_output():
|
|
|
118
130
|
assert_final_output(result, contains="Tokyo")
|
|
119
131
|
```
|
|
120
132
|
|
|
121
|
-
> **Using a real agent framework?** Walk through the [Real-World Examples](#real-world-examples) below for end-to-end tests with cassettes, or jump straight to the [Examples](#examples) table for the full list of runnable samples.
|
|
122
|
-
|
|
123
133
|
```
|
|
124
134
|
$ pytest test_agent.py -v
|
|
125
135
|
test_agent.py::test_tool_sequence PASSED
|
|
@@ -128,13 +138,15 @@ test_agent.py::test_safety PASSED
|
|
|
128
138
|
test_agent.py::test_output PASSED
|
|
129
139
|
```
|
|
130
140
|
|
|
141
|
+
> **Using a real agent framework?** Walk through the [Real-World Examples](#real-world-examples) below for in-depth walkthroughs with cassettes, or jump to the [Examples](#examples) table for a quick index of all runnable samples.
|
|
142
|
+
|
|
131
143
|
## Real-World Examples
|
|
132
144
|
|
|
133
|
-
End-to-end examples that showcase different agent patterns and testing angles. All run with pre-recorded cassettes
|
|
145
|
+
End-to-end examples that showcase different agent patterns and testing angles. All run with pre-recorded cassettes, no API keys needed.
|
|
134
146
|
|
|
135
147
|
### Strands Weather Forecaster — Two-Step ReAct
|
|
136
148
|
|
|
137
|
-
Test the [Strands Weather Forecaster](https://strandsagents.com/docs/examples/python/weather_forecaster/)
|
|
149
|
+
Test the [Strands Weather Forecaster](https://strandsagents.com/docs/examples/python/weather_forecaster/), an official Strands Agents sample. The agent uses the `http_request` tool to call the National Weather Service API, which returns the forecast URL for the requested location; the agent then calls that URL to get the actual forecast. Two steps, one piece of data flowing between them: the simplest shape of a ReAct loop.
|
|
138
150
|
|
|
139
151
|
```python
|
|
140
152
|
import pytest
|
|
@@ -158,7 +170,7 @@ def test_weather_agent(cassette):
|
|
|
158
170
|
result = rec.to_execution_result()
|
|
159
171
|
assert_all(
|
|
160
172
|
result,
|
|
161
|
-
# Two http_request calls with distinct URL shapes
|
|
173
|
+
# Two http_request calls with distinct URL shapes: location lookup then forecast
|
|
162
174
|
lambda r: assert_tool_calls(r, expected=[
|
|
163
175
|
ToolCall("http_request", {"method": "GET", "url": MATCHES(r"/points/")}),
|
|
164
176
|
ToolCall("http_request", {"method": "GET", "url": MATCHES(r"/forecast")}),
|
|
@@ -193,11 +205,11 @@ def test_weather_agent_steps(cassette):
|
|
|
193
205
|
assert_step_uses_result_from(result, step=1, depends_on=0)
|
|
194
206
|
```
|
|
195
207
|
|
|
196
|
-
`MATCHES(pattern)` is a regex matcher
|
|
208
|
+
`MATCHES(pattern)` is a regex matcher. See [Assertion Modes](#assertion-modes) for details on `ANY`, `MATCHES`, `OrderMode`, and `partial_args`. Step assertions are covered in [Step-Level Assertions](#step-level-assertions).
|
|
197
209
|
|
|
198
210
|
### OpenAI Agents SDK LLM as a Judge — Freezing a Probabilistic Refinement Loop
|
|
199
211
|
|
|
200
|
-
Test the [OpenAI Agents SDK `agent_patterns/llm_as_a_judge.py`](https://github.com/openai/openai-agents-python/blob/main/examples/agent_patterns/llm_as_a_judge.py) sample: a generator writes a story outline, an evaluator grades it with a structured `{feedback, score}` verdict, and the loop re-runs the generator with that feedback until the evaluator accepts the draft. Everything about this agent is probabilistic
|
|
212
|
+
Test the [OpenAI Agents SDK `agent_patterns/llm_as_a_judge.py`](https://github.com/openai/openai-agents-python/blob/main/examples/agent_patterns/llm_as_a_judge.py) sample: a generator writes a story outline, an evaluator grades it with a structured `{feedback, score}` verdict, and the loop re-runs the generator with that feedback until the evaluator accepts the draft. Everything about this agent is probabilistic: how many rounds it takes, what the feedback says, whether the rewrite incorporates it. Recording one run freezes all of it into a deterministic test.
|
|
201
213
|
|
|
202
214
|
The headline assertion is the feedback chain: round 2's generator must actually consume round 1's evaluator feedback, not silently regenerate the same outline.
|
|
203
215
|
|
|
@@ -221,12 +233,14 @@ def test_generator_consumes_evaluator_feedback(cassette):
|
|
|
221
233
|
assert_step_output(
|
|
222
234
|
result, step=1, matches=r'"score"\s*:\s*"(needs_improvement|fail)"'
|
|
223
235
|
)
|
|
224
|
-
# Step 2 (generator, round 2) must actually consume step 1's feedback
|
|
225
|
-
# catches "agent regenerated but ignored the judge" bugs.
|
|
236
|
+
# Step 2 (generator, round 2) must actually consume step 1's feedback.
|
|
237
|
+
# This catches "agent regenerated but ignored the judge" bugs.
|
|
226
238
|
assert_step_uses_result_from(result, step=2, depends_on=1)
|
|
227
239
|
```
|
|
228
240
|
|
|
229
|
-
`assert_step_uses_result_from`
|
|
241
|
+
`assert_step_uses_result_from` matches across JSON-serialization boundaries. The multi-line evaluator feedback is found inside the next step's user message even when newlines are escaped during serialization.
|
|
242
|
+
|
|
243
|
+
Because each `Runner.run` call is recorded individually and the workflow spans several runs, the test builds the `ExecutionResult` straight off the cassette recorder rather than the single-run `from_openai_agents` adapter. The cassette layer is the natural single source of truth when a test covers multiple agent runs.
|
|
230
244
|
|
|
231
245
|
See [`examples/openai-agents-llm-as-a-judge/`](examples/openai-agents-llm-as-a-judge/) for the full loop, the cassette, and the remaining flat + step-level assertions (budget, safety, pass-verdict reached).
|
|
232
246
|
|
|
@@ -234,27 +248,27 @@ See [`examples/openai-agents-llm-as-a-judge/`](examples/openai-agents-llm-as-a-j
|
|
|
234
248
|
|
|
235
249
|
Full-depth examples for the LangChain ecosystem live under [`examples/`](examples/):
|
|
236
250
|
|
|
237
|
-
- **[LangGraph multi-agent supervisor](examples/langgraph-multi-agent-supervisor/)
|
|
238
|
-
- **[LangChain GitHub issue triage](examples/langchain-issue-triage/)
|
|
251
|
+
- **[LangGraph multi-agent supervisor](examples/langgraph-multi-agent-supervisor/)**: a supervisor routes to a research expert and a math expert; each `add` in the math chain must consume the previous `add`'s result. `assert_step_uses_result_from` matches the numeric running total (`"231317.0"` produced as a string, consumed as `231317` int/float) across type boundaries, with digit-boundary checks that keep `"231"` from falsely appearing inside `1231`.
|
|
252
|
+
- **[LangChain GitHub issue triage](examples/langchain-issue-triage/)**: a `create_react_agent` drives the GitHub MCP server to list issues, read one, propose labels. The step-level tests verify that the `issue_number` passed to `get_issue` actually came from the preceding `list_issues` response, that the GitHub reads returned usable results (`assert_no_tool_errors`, `assert_tool_result_matches`), and that destructive tools like `close_issue` are never called.
|
|
239
253
|
|
|
240
|
-
Each example ships with its own pre-recorded cassette, a detailed `README.md`, and a full test file
|
|
254
|
+
Each example ships with its own pre-recorded cassette, a detailed `README.md`, and a full test file. Pick whichever framework fits your stack.
|
|
241
255
|
|
|
242
256
|
## Build an ExecutionResult
|
|
243
257
|
|
|
244
258
|
Every agentverify assertion takes an `ExecutionResult`. You can build one three ways:
|
|
245
259
|
|
|
246
|
-
1. **From a dict
|
|
247
|
-
2. **From a built-in adapter
|
|
248
|
-
3. **From a custom converter
|
|
260
|
+
1. **From a dict**: convenient for quick tests and fixtures, as in the [Quick Start](#quick-start).
|
|
261
|
+
2. **From a built-in adapter**: one-liner for LangChain, LangGraph, Strands Agents, OpenAI Agents SDK. See [Framework Integration](#framework-integration).
|
|
262
|
+
3. **From a custom converter**: for frameworks or pure-Python agents not covered by a built-in adapter, map your output to the schema below. See [`examples/custom-converter-python-agent/`](examples/custom-converter-python-agent/) for a complete walkthrough with an Anthropic SDK agent, including a pre-recorded cassette and step-level tests.
|
|
249
263
|
|
|
250
264
|
`ExecutionResult.from_dict()` accepts these keys:
|
|
251
265
|
|
|
252
266
|
| Key | Type | Description |
|
|
253
267
|
|---|---|---|
|
|
254
268
|
| `steps` | `list[dict]` | Each step dict has `index` (int), `source` (`"llm"` / `"probe"` / `"tool"`), `tool_calls` (list), `tool_results` (list), `output` (str or None), `input_context` (dict or None), `name` (str or None). See [Step-Level Assertions](#step-level-assertions) |
|
|
255
|
-
| `tool_calls` | `list[dict]` | **Shortcut for single-step cases
|
|
269
|
+
| `tool_calls` | `list[dict]` | **Shortcut for single-step cases**: when `steps` is absent, a flat `tool_calls` list is wrapped into a single synthetic step. Used throughout the [Quick Start](#quick-start). Each dict has `name` (str, required), `arguments` (dict, optional), `result` (any, optional) |
|
|
256
270
|
| `token_usage` | `dict` or `None` | `{"input_tokens": int, "output_tokens": int}` |
|
|
257
|
-
| `total_cost_usd` | `float` or `None` | Total cost in USD (must be set manually
|
|
271
|
+
| `total_cost_usd` | `float` or `None` | Total cost in USD (must be set manually, not auto-calculated from tokens or populated from cassettes) |
|
|
258
272
|
| `duration_ms` | `float` or `None` | Wall-clock duration in milliseconds (auto-populated by the `cassette` fixture and `MockLLM`) |
|
|
259
273
|
| `final_output` | `str` or `None` | The agent's final text response |
|
|
260
274
|
|
|
@@ -262,7 +276,7 @@ Every agentverify assertion takes an `ExecutionResult`. You can build one three
|
|
|
262
276
|
|
|
263
277
|
## Record & Replay with Cassettes
|
|
264
278
|
|
|
265
|
-
Record real LLM API calls once
|
|
279
|
+
Record real LLM API calls once, then replay them in CI forever. Zero cost, fully deterministic.
|
|
266
280
|
|
|
267
281
|
```python
|
|
268
282
|
import pytest
|
|
@@ -283,7 +297,7 @@ def test_weather_agent(cassette):
|
|
|
283
297
|
])
|
|
284
298
|
```
|
|
285
299
|
|
|
286
|
-
The `cassette` fixture is provided by the agentverify pytest plugin. It creates an `LLMCassetteRecorder` that intercepts LLM SDK calls (not HTTP
|
|
300
|
+
The `cassette` fixture is provided by the agentverify pytest plugin. It creates an `LLMCassetteRecorder` that intercepts LLM SDK calls (not HTTP; it patches the SDK's chat completion method directly). Use `@pytest.mark.agentverify` to mark your test, and call your agent code inside the `with cassette(...)` block. After the block exits, call `rec.to_execution_result()` to build the result for assertions.
|
|
287
301
|
|
|
288
302
|
Record the cassette once, then replay it forever:
|
|
289
303
|
|
|
@@ -297,7 +311,7 @@ pytest
|
|
|
297
311
|
|
|
298
312
|
Cassettes are human-readable YAML (or JSON). Commit them to git, review in PRs.
|
|
299
313
|
|
|
300
|
-
|
|
314
|
+
agentverify supports three cassette modes:
|
|
301
315
|
|
|
302
316
|
| Mode | Behavior |
|
|
303
317
|
|---|---|
|
|
@@ -332,25 +346,40 @@ Cassettes drift when your prompts, tool schemas, or model versions change. agent
|
|
|
332
346
|
|
|
333
347
|
- Re-record on major prompt rewrites or tool schema changes
|
|
334
348
|
- Pin model versions (`gpt-4o-2024-08-06`, not `gpt-4o`) in your agent code so cassettes don't silently shift underneath you
|
|
335
|
-
- Treat cassette diffs as part of the PR review
|
|
349
|
+
- Treat cassette diffs as part of the PR review. A YAML diff next to the prompt diff is a feature, not noise
|
|
336
350
|
- Use `MATCHES` and `partial_args=True` to assert on the stable parts of tool arguments, so small wording changes in LLM outputs don't cascade into test failures
|
|
337
351
|
|
|
352
|
+
### Calls Cassettes Cannot Record
|
|
353
|
+
|
|
354
|
+
Cassettes record non-streaming chat calls: OpenAI `chat.completions.create`, Anthropic `messages.create`, Gemini `models.generate_content`, Bedrock `Converse`, and LiteLLM `completion`. Inside a cassette or `MockLLM`, agentverify raises `CassetteUnsupportedCallError` instead of letting other LLM calls reach the real API:
|
|
355
|
+
- Streaming entry points (`stream=True`, `messages.stream()`, `generate_content_stream()`, Bedrock `ConverseStream`, OpenAI Responses `stream()`), in record and replay mode.
|
|
356
|
+
- In replay mode, every other request through an OpenAI, Anthropic, or Gemini SDK client, including the Responses API, `parse()`, embeddings, and the async Anthropic and Gemini clients. Bedrock operations that generate (`InvokeModel`, `InvokeAgent`, `RetrieveAndGenerate`, ...) and LiteLLM `acompletion`, `text_completion`, and `responses` raise too.
|
|
357
|
+
- In record mode, the entry points named above run with a warning. Other requests (beta resources, WebSocket APIs) run without one, and replay then fails at that call.
|
|
358
|
+
- With no cassette file (`AUTO` passthrough), nothing is saved and no guard applies.
|
|
359
|
+
|
|
360
|
+
Frameworks that stream by default need streaming turned off, e.g. `BedrockModel(streaming=False)` in Strands.
|
|
361
|
+
|
|
338
362
|
### Cassette Sanitization
|
|
339
363
|
|
|
340
|
-
|
|
364
|
+
Cassettes are sanitized by default when recording. Before saving, agentverify redacts:
|
|
365
|
+
- values that match a built-in key pattern, anywhere in the cassette
|
|
366
|
+
- call arguments and headers under a credential key, whatever their format: `api_key`, `authorization`, `x-api-key`, `cookie`, and names ending in `token`, `secret`, `password`, and similar (for example in `extra_headers`)
|
|
341
367
|
|
|
342
|
-
Built-in patterns cover:
|
|
343
|
-
- OpenAI API keys (`sk-...`)
|
|
344
|
-
-
|
|
345
|
-
-
|
|
368
|
+
Built-in patterns (`DEFAULT_PATTERNS`) cover:
|
|
369
|
+
- OpenAI and Anthropic API keys (`sk-...`, `sk-ant-...`)
|
|
370
|
+
- AWS access keys (`AKIA...`, `ASIA...`)
|
|
371
|
+
- Amazon Bedrock API keys (`ABSK...`, `bedrock-api-key-...`)
|
|
372
|
+
- Google API keys (`AIza...`)
|
|
373
|
+
- GitHub tokens (`ghp_...`, `github_pat_...`)
|
|
346
374
|
- Bearer tokens (`Bearer ...`)
|
|
347
375
|
|
|
348
|
-
|
|
376
|
+
A custom list replaces the built-in patterns, so extend `DEFAULT_PATTERNS` to keep them. Credential keys in call arguments are redacted either way:
|
|
349
377
|
|
|
350
378
|
```python
|
|
351
|
-
from agentverify import SanitizePattern
|
|
379
|
+
from agentverify import DEFAULT_PATTERNS, SanitizePattern
|
|
352
380
|
|
|
353
381
|
custom_patterns = [
|
|
382
|
+
*DEFAULT_PATTERNS,
|
|
354
383
|
SanitizePattern(name="internal_token", pattern=r"tok-[a-f0-9]{32}", replacement="tok-***REDACTED***"),
|
|
355
384
|
]
|
|
356
385
|
|
|
@@ -365,27 +394,27 @@ Pass `sanitize=False` to disable sanitization entirely (not recommended).
|
|
|
365
394
|
```python
|
|
366
395
|
from agentverify import assert_tool_calls, OrderMode, ToolCall, ANY, MATCHES
|
|
367
396
|
|
|
368
|
-
# Exact match
|
|
397
|
+
# Exact match: same tools, same order, same count (default)
|
|
369
398
|
assert_tool_calls(result, expected=[...])
|
|
370
399
|
|
|
371
|
-
# Subsequence
|
|
400
|
+
# Subsequence: these tools appeared in this order (other calls in between are OK)
|
|
372
401
|
assert_tool_calls(result, expected=[...], order=OrderMode.IN_ORDER)
|
|
373
402
|
|
|
374
|
-
# Set membership
|
|
403
|
+
# Set membership: these tools were called (order doesn't matter)
|
|
375
404
|
assert_tool_calls(result, expected=[...], order=OrderMode.ANY_ORDER)
|
|
376
405
|
|
|
377
|
-
# Partial args
|
|
406
|
+
# Partial args: only check the keys you care about
|
|
378
407
|
assert_tool_calls(result, expected=[
|
|
379
408
|
ToolCall("search", {"query": "Tokyo"}),
|
|
380
409
|
], partial_args=True)
|
|
381
410
|
|
|
382
|
-
# ANY wildcard
|
|
411
|
+
# ANY wildcard: ignore a specific argument value
|
|
383
412
|
assert_tool_calls(result, expected=[
|
|
384
413
|
ToolCall("get_weather", {"lat": ANY, "lon": ANY}),
|
|
385
414
|
])
|
|
386
415
|
|
|
387
|
-
# MATCHES regex
|
|
388
|
-
# (re.search semantics
|
|
416
|
+
# MATCHES regex: verify a string argument follows a pattern
|
|
417
|
+
# (re.search semantics; use ^/$ anchors for full match)
|
|
389
418
|
assert_tool_calls(result, expected=[
|
|
390
419
|
ToolCall("send_email", {"to": MATCHES(r"^[\w.+-]+@example\.com$")}),
|
|
391
420
|
ToolCall("log_event", {"event_id": MATCHES(r"^evt_[a-f0-9]{16}$")}),
|
|
@@ -408,7 +437,7 @@ assert_all(
|
|
|
408
437
|
|
|
409
438
|
### Cost Assertions
|
|
410
439
|
|
|
411
|
-
`assert_cost()` enforces token and dollar budgets on an agent run. Either bound is optional
|
|
440
|
+
`assert_cost()` enforces token and dollar budgets on an agent run. Either bound is optional. Pass `max_tokens` only, `max_cost_usd` only, or both together:
|
|
412
441
|
|
|
413
442
|
```python
|
|
414
443
|
from agentverify import assert_cost
|
|
@@ -419,11 +448,11 @@ assert_cost(result, max_tokens=500)
|
|
|
419
448
|
# Dollar budget only
|
|
420
449
|
assert_cost(result, max_cost_usd=0.01)
|
|
421
450
|
|
|
422
|
-
# Both
|
|
451
|
+
# Both: fail if either is exceeded
|
|
423
452
|
assert_cost(result, max_tokens=500, max_cost_usd=0.01)
|
|
424
453
|
```
|
|
425
454
|
|
|
426
|
-
`result.token_usage` is populated automatically by the `cassette` fixture, `MockLLM`, and every built-in framework adapter. `result.total_cost_usd` must be set manually (see [Build an ExecutionResult](#build-an-executionresult))
|
|
455
|
+
`result.token_usage` is populated automatically by the `cassette` fixture, `MockLLM`, and every built-in framework adapter. `result.total_cost_usd` must be set manually (see [Build an ExecutionResult](#build-an-executionresult)). agentverify does not compute dollar costs from tokens, since the pricing table would need per-model maintenance and get stale quickly.
|
|
427
456
|
|
|
428
457
|
### Latency Assertions
|
|
429
458
|
|
|
@@ -461,9 +490,16 @@ assert_final_output(result, equals="The weather in Tokyo is sunny, 22°C.")
|
|
|
461
490
|
assert_final_output(result, matches=r"\d+°C")
|
|
462
491
|
```
|
|
463
492
|
|
|
493
|
+
`contains` also accepts a list; every substring must appear. `case_sensitive=False` ignores case in `contains` only; for a case-insensitive regex, put `(?i)` in the pattern. `assert_step_output` and `assert_tool_result_matches` accept the same options:
|
|
494
|
+
|
|
495
|
+
```python
|
|
496
|
+
assert_final_output(result, contains=["Tokyo", "22°C"])
|
|
497
|
+
assert_final_output(result, contains="tokyo", case_sensitive=False)
|
|
498
|
+
```
|
|
499
|
+
|
|
464
500
|
### Strict Mode
|
|
465
501
|
|
|
466
|
-
`assert_cost()` and `assert_latency()` silently pass when the underlying data (`token_usage`, `total_cost_usd`, `duration_ms`) is `None
|
|
502
|
+
`assert_cost()` and `assert_latency()` silently pass when the underlying data (`token_usage`, `total_cost_usd`, `duration_ms`) is `None`. Useful during cassette replay where some data may be unavailable. Pass `strict=True` to require the data to be present:
|
|
467
503
|
|
|
468
504
|
```python
|
|
469
505
|
# Fails if token_usage or total_cost_usd is None
|
|
@@ -475,7 +511,7 @@ assert_latency(result, max_ms=3000, strict=True)
|
|
|
475
511
|
|
|
476
512
|
## Step-Level Assertions
|
|
477
513
|
|
|
478
|
-
For agents that make multiple LLM calls per execution (ReAct, Plan-and-Execute, multi-hop retrieval, workflow-style), agentverify exposes the full step structure
|
|
514
|
+
For agents that make multiple LLM calls per execution (ReAct, Plan-and-Execute, multi-hop retrieval, workflow-style), agentverify exposes the full step structure, not just a flat list of tool calls.
|
|
479
515
|
|
|
480
516
|
`ExecutionResult.steps` is the single source of truth; `result.tool_calls` remains as a derived flat view for simpler cases.
|
|
481
517
|
|
|
@@ -502,7 +538,7 @@ def test_plan_and_execute(cassette):
|
|
|
502
538
|
|
|
503
539
|
### Assert Step-to-Step Data Flow
|
|
504
540
|
|
|
505
|
-
`assert_step_uses_result_from()` verifies that one step's input references data produced by another
|
|
541
|
+
`assert_step_uses_result_from()` verifies that one step's input references data produced by another. Catches "agent ignored the tool result" bugs.
|
|
506
542
|
|
|
507
543
|
```python
|
|
508
544
|
from agentverify import assert_step_uses_result_from
|
|
@@ -516,13 +552,13 @@ assert_step_uses_result_from(result, step=2, depends_on=1, via="tool_result")
|
|
|
516
552
|
|
|
517
553
|
The check searches for any produced value (step M's `tool_results`, `tool_calls[*].result`, or `output`) inside step N's `input_context` or `tool_calls[*].arguments`. Strings are substring-matched; primitives (numbers, booleans) are matched structurally inside containers; JSON-encoded tool results are descended automatically.
|
|
518
554
|
|
|
519
|
-
To see this concretely with a different example: if step 0's `list_issues` tool returned `[{"number": 1}, {"number": 2}]` and step 1 called `get_issue(issue_number=2)`, the check finds that the number `2` produced by step 0 shows up in step 1's arguments
|
|
555
|
+
To see this concretely with a different example: if step 0's `list_issues` tool returned `[{"number": 1}, {"number": 2}]` and step 1 called `get_issue(issue_number=2)`, the check finds that the number `2` produced by step 0 shows up in step 1's arguments. Data flows correctly. This is exactly the pattern the [LangChain GitHub issue triage example](examples/langchain-issue-triage/) verifies.
|
|
520
556
|
|
|
521
|
-
|
|
557
|
+
This works on cassette replay. Cassette recorders don't record tool execution results directly, but agentverify backfills them from the next step's `input_context` so you can assert data flow without any special adapter setup.
|
|
522
558
|
|
|
523
559
|
### Workflow-Style Agents with `step_probe`
|
|
524
560
|
|
|
525
|
-
Many production agents aren't pure ReAct loops
|
|
561
|
+
Many production agents aren't pure ReAct loops. They mix LLM calls with caching, state management, validation, and conditional branches. `step_probe()` lets you mark logical step boundaries in your agent code so tests can assert on those non-LLM steps.
|
|
526
562
|
|
|
527
563
|
```python
|
|
528
564
|
# agent.py
|
|
@@ -551,13 +587,49 @@ def test_cache_miss_path():
|
|
|
551
587
|
assert_step_output(result, name="postprocess", equals="formatted")
|
|
552
588
|
```
|
|
553
589
|
|
|
554
|
-
**`step_probe` is a zero-cost no-op outside of test recording contexts
|
|
590
|
+
**`step_probe` is a zero-cost no-op outside of test recording contexts.** When no `LLMCassetteRecorder` or `MockLLM` is active, it's a pass-through context manager. Safe to leave in production code.
|
|
591
|
+
|
|
592
|
+
## Tool Result Assertions
|
|
593
|
+
|
|
594
|
+
Knowing the agent *called* the right tool is not the same as knowing the call *worked*. A tool can return a 5xx, time out, or hand back an empty body, and the LLM will often smooth that over in its final reply. These assertions check the result a tool handed back, not just which tool was called and with what arguments.
|
|
595
|
+
|
|
596
|
+
```python
|
|
597
|
+
from agentverify import (
|
|
598
|
+
assert_tool_invocation_succeeded,
|
|
599
|
+
assert_no_tool_errors,
|
|
600
|
+
assert_tool_result_matches,
|
|
601
|
+
assert_retry_count,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# Affirmative: the tool call(s) at this step returned a usable result
|
|
605
|
+
assert_tool_invocation_succeeded(result, step=1)
|
|
606
|
+
|
|
607
|
+
# Blanket safety net: no tool anywhere in the run errored
|
|
608
|
+
assert_no_tool_errors(result)
|
|
609
|
+
|
|
610
|
+
# The tool returned a well-shaped / non-empty result
|
|
611
|
+
assert_tool_result_matches(result, step=1, contains="temperature")
|
|
612
|
+
assert_tool_result_matches(result, step=1, matches=r'"temp":\s*\d+')
|
|
613
|
+
# Every substring must appear in the same tool result; case_sensitive=False ignores case
|
|
614
|
+
assert_tool_result_matches(result, step=1, contains=["temp", "unit"], case_sensitive=False)
|
|
615
|
+
|
|
616
|
+
# Budget on retries of a flaky tool (counted from consecutive repeat calls)
|
|
617
|
+
assert_retry_count(result, tool_name="http_request", max=2)
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
agentverify reads a per-result error signal that the built-in adapters populate from each framework's native tool-error convention (LangGraph `ToolMessage.status == "error"`, Strands `toolResult` status, OpenAI Agents SDK default failure and timeout messages, LangChain error-shaped observations), and that cassette replay reconstructs from the recorded tool messages.
|
|
621
|
+
|
|
622
|
+
The signal is three-valued: a result is a **known error**, a **known success**, or **unknown** (for example, a plain-string tool output on cassette replay where no structured status survived). `assert_tool_invocation_succeeded` and `assert_no_tool_errors` fail only on a *known* error; unknown and absent results pass, so the same test gives the same verdict whether it runs against a live agent or a replayed cassette. To require that a result is actually present and well-formed, use `assert_tool_result_matches`, which fails when the step has no tool result at all.
|
|
623
|
+
|
|
624
|
+
`assert_no_tool_errors` is a blanket gate, so reach for it only when every tool in the run is expected to succeed. If your agent's correct behaviour includes recovering from a failure (a 429 then a retry, a primary tool then a fallback), that trajectory contains an expected error, and the blanket gate would flag it. In that case assert success on the specific steps that must succeed with `assert_tool_invocation_succeeded(step=N)` instead.
|
|
625
|
+
|
|
626
|
+
If you build an `ExecutionResult` by hand or with a custom converter, set the signal yourself via the `tool_results_meta` field on each `Step`: a list positionally aligned with `tool_results`, where entry `i` is `{"is_error": True}`, `{"is_error": False}`, or `{}` (unknown). With `step_probe`, pass it through `handle.set_tool_result(result, is_error=True)`.
|
|
555
627
|
|
|
556
628
|
## Framework Integration
|
|
557
629
|
|
|
558
630
|
### Built-in Adapters
|
|
559
631
|
|
|
560
|
-
agentverify ships with built-in adapters for popular agent frameworks. No converter function needed
|
|
632
|
+
agentverify ships with built-in adapters for popular agent frameworks. No converter function needed. Just import and call:
|
|
561
633
|
|
|
562
634
|
| Framework | Adapter | Input |
|
|
563
635
|
|---|---|---|
|
|
@@ -593,22 +665,22 @@ execution_result = from_openai_agents(Runner.run_sync(agent, "your prompt"))
|
|
|
593
665
|
|
|
594
666
|
### Custom Converters
|
|
595
667
|
|
|
596
|
-
For other frameworks, build an `ExecutionResult` from your agent's output using a small converter function. See [`examples/custom-converter-python-agent/`](examples/custom-converter-python-agent/) for a complete reference
|
|
668
|
+
For other frameworks, build an `ExecutionResult` from your agent's output using a small converter function. See [`examples/custom-converter-python-agent/`](examples/custom-converter-python-agent/) for a complete reference: an Anthropic SDK ReAct agent paired with a ~80-line `converter.py`, a recorded cassette, and step-level tests.
|
|
597
669
|
|
|
598
670
|
## Supported LLM Providers
|
|
599
671
|
|
|
600
672
|
| Provider | Extra |
|
|
601
673
|
|---|---|
|
|
602
|
-
| OpenAI | `pip install agentverify[openai]` |
|
|
603
|
-
| Amazon Bedrock | `pip install agentverify[bedrock]` |
|
|
604
|
-
| Google Gemini | `pip install agentverify[gemini]` |
|
|
605
|
-
| Anthropic | `pip install agentverify[anthropic]` |
|
|
606
|
-
| LiteLLM | `pip install agentverify[litellm]` |
|
|
607
|
-
| All providers | `pip install agentverify[all]` |
|
|
674
|
+
| OpenAI | `pip install "agentverify[openai]"` |
|
|
675
|
+
| Amazon Bedrock | `pip install "agentverify[bedrock]"` |
|
|
676
|
+
| Google Gemini | `pip install "agentverify[gemini]"` |
|
|
677
|
+
| Anthropic | `pip install "agentverify[anthropic]"` |
|
|
678
|
+
| LiteLLM | `pip install "agentverify[litellm]"` |
|
|
679
|
+
| All providers | `pip install "agentverify[all]"` |
|
|
608
680
|
|
|
609
|
-
##
|
|
681
|
+
## LLM Mocking with MockLLM
|
|
610
682
|
|
|
611
|
-
`MockLLM` replays a list of predefined LLM responses that you define in code. Test your agent's routing logic without a cassette or a real LLM call
|
|
683
|
+
`MockLLM` replays a list of predefined LLM responses that you define in code. Test your agent's routing logic without a cassette or a real LLM call. Useful when you haven't recorded yet, or when you want to drive the agent through hypothetical responses (errors, edge cases) that would be awkward to elicit from a real model.
|
|
612
684
|
|
|
613
685
|
```python
|
|
614
686
|
import pytest
|
|
@@ -631,11 +703,11 @@ def test_agent_routes_to_weather_tool():
|
|
|
631
703
|
|
|
632
704
|
`mock_response()` accepts:
|
|
633
705
|
|
|
634
|
-
- `content
|
|
635
|
-
- `tool_calls
|
|
636
|
-
- `input_tokens` / `output_tokens
|
|
706
|
+
- `content`: the final text message the LLM should return.
|
|
707
|
+
- `tool_calls`: a list of `(name, arguments)` tuples or `{"name": ..., "arguments": ...}` dicts.
|
|
708
|
+
- `input_tokens` / `output_tokens`: optional token usage to report.
|
|
637
709
|
|
|
638
|
-
Provide one `mock_response(...)` per LLM call your agent is expected to make. If your agent makes more calls than you've queued, `MockLLM` raises `CassetteMissingRequestError` so under-specified tests fail loudly.
|
|
710
|
+
Provide one `mock_response(...)` per LLM call your agent is expected to make. If your agent makes more calls than you've queued, `MockLLM` raises [`CassetteMissingRequestError`](#error-messages) so under-specified tests fail loudly.
|
|
639
711
|
|
|
640
712
|
### When to Use MockLLM vs Cassettes
|
|
641
713
|
|
|
@@ -664,21 +736,21 @@ jobs:
|
|
|
664
736
|
- uses: actions/setup-python@v6
|
|
665
737
|
with:
|
|
666
738
|
python-version: "3.14"
|
|
667
|
-
# Shown separately here for clarity
|
|
739
|
+
# Shown separately here for clarity. In practice, list agentverify
|
|
668
740
|
# alongside the rest of your project's deps in requirements.txt.
|
|
669
741
|
- run: pip install "agentverify[all]"
|
|
670
742
|
- run: pip install -r requirements.txt # your project's deps
|
|
671
743
|
- run: pytest --tb=short -v
|
|
672
744
|
```
|
|
673
745
|
|
|
674
|
-
Cassette files in `tests/cassettes/` are replayed automatically
|
|
746
|
+
Cassette files in `tests/cassettes/` are replayed automatically. No API keys or secrets needed in CI.
|
|
675
747
|
|
|
676
748
|
## Error Messages
|
|
677
749
|
|
|
678
750
|
Clear, structured output when assertions fail:
|
|
679
751
|
|
|
680
752
|
```
|
|
681
|
-
ToolCallSequenceError: Tool call sequence mismatch at index 1
|
|
753
|
+
ToolCallSequenceError: Tool call sequence mismatch at index 1 (step 0)
|
|
682
754
|
|
|
683
755
|
Expected:
|
|
684
756
|
[0] get_location(city="Tokyo")
|
|
@@ -705,7 +777,7 @@ LatencyBudgetError: Latency budget exceeded
|
|
|
705
777
|
Exceeded by: 450.0 ms (15.0%)
|
|
706
778
|
```
|
|
707
779
|
|
|
708
|
-
Other error types follow the same pattern: `SafetyRuleViolationError`, `FinalOutputError`.
|
|
780
|
+
Other error types follow the same pattern: `SafetyRuleViolationError`, `FinalOutputError`, `CassetteRequestMismatchError` (stale cassette detected), `CassetteMissingRequestError` (cassette or MockLLM ran out of queued responses), and the step-level `StepIndexError`, `StepNameNotFoundError`, `StepNameAmbiguousError`, `StepOutputError`, `StepDependencyError`. All inherit from `AgentVerifyError`, which itself extends `AssertionError` so pytest treats them as normal test failures.
|
|
709
781
|
|
|
710
782
|
## Requirements
|
|
711
783
|
|
|
@@ -714,28 +786,52 @@ Other error types follow the same pattern: `SafetyRuleViolationError`, `FinalOut
|
|
|
714
786
|
|
|
715
787
|
## Examples
|
|
716
788
|
|
|
717
|
-
The [`examples/`](examples/) directory contains end-to-end examples with real agent frameworks and MCP servers. Each example ships with pre-recorded cassettes
|
|
789
|
+
The [`examples/`](examples/) directory contains end-to-end examples with real agent frameworks and MCP servers. Each example ships with pre-recorded cassettes, so tests run without any API keys.
|
|
718
790
|
|
|
719
791
|
| Example | Framework | Description |
|
|
720
792
|
|---|---|---|
|
|
721
|
-
| [`langchain-issue-triage`](examples/langchain-issue-triage/) | LangChain + OpenAI | Triages GitHub issues via GitHub MCP, step-level data flow for the discovered issue number |
|
|
793
|
+
| [`langchain-issue-triage`](examples/langchain-issue-triage/) | LangChain + OpenAI | Triages GitHub issues via GitHub MCP, step-level data flow for the discovered issue number, tool-result outcome checks |
|
|
722
794
|
| [`langgraph-multi-agent-supervisor`](examples/langgraph-multi-agent-supervisor/) | LangGraph + OpenAI | Research + math multi-agent handoff with running-total data flow |
|
|
723
795
|
| [`strands-weather-forecaster`](examples/strands-weather-forecaster/) | Strands Agents + Bedrock | Two-step ReAct that discovers a forecast URL, then calls it |
|
|
724
796
|
| [`openai-agents-llm-as-a-judge`](examples/openai-agents-llm-as-a-judge/) | OpenAI Agents SDK | Probabilistic generator ↔ evaluator refinement loop, frozen into a deterministic test with feedback-chain data flow |
|
|
725
|
-
| [`custom-converter-python-agent`](examples/custom-converter-python-agent/) | Anthropic SDK (no framework) | Reference for writing a custom converter
|
|
726
|
-
| [`mcp-server`](examples/mcp-server/) |
|
|
797
|
+
| [`custom-converter-python-agent`](examples/custom-converter-python-agent/) | Anthropic SDK (no framework) | Reference for writing a custom converter: a hand-rolled ReAct loop calling the Anthropic Messages API directly |
|
|
798
|
+
| [`mcp-server`](examples/mcp-server/) | (shared utility) | Mock GitHub MCP server for token-free testing |
|
|
727
799
|
|
|
728
800
|
See each example's README for setup and recording mode details.
|
|
729
801
|
|
|
802
|
+
## Benchmark
|
|
803
|
+
|
|
804
|
+
The [`benchmarks/execution-model-trajectory/`](benchmarks/execution-model-trajectory/) directory contains a comparison study that writes the same execution-trajectory assertion in three different assertion-execution-model styles (inline pytest via agentverify, separate-runner via [DeepEval](https://www.deepeval.com/docs/evaluation-llm-tracing) [`ToolCorrectnessMetric`](https://www.deepeval.com/docs/metrics-tool-correctness) + `@observe`, managed remote via [Amazon Bedrock AgentCore Evaluations Custom code-based evaluator](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/code-based-evaluators.html) on Lambda) over two agents (Strands single-agent ReAct and LangGraph multi-agent supervisor). Per-cell numbers (LOC, wall time, API calls, dollar cost, CI secrets, cold-start) are recorded under [`results/`](benchmarks/execution-model-trajectory/results/). See the bench's [`README.md`](benchmarks/execution-model-trajectory/README.md) for the operating manual and [`DESIGN.md`](benchmarks/execution-model-trajectory/DESIGN.md) for the methodology and explicit non-goals. The benchmark is a one-time exercise that refreshes when tooling versions change materially, not a continuous CI gate.
|
|
805
|
+
|
|
806
|
+
## How agentverify compares
|
|
807
|
+
|
|
808
|
+
Agent evaluation covers a few different jobs, and agentverify does one of them. This table is meant to help you pick the right tool for a given job, not to rank them. In practice they are often used together.
|
|
809
|
+
|
|
810
|
+
| Tool | Best at | Execution model |
|
|
811
|
+
|---|---|---|
|
|
812
|
+
| **agentverify** | Deterministic regression tests on what the agent did: tool calls, arguments, step-to-step data flow, cost, latency, safety | Inline pytest, in-process, no LLM call on replay, no cloud account |
|
|
813
|
+
| [DeepEval](https://github.com/confident-ai/deepeval) (Confident AI) | A large catalog of quality metrics (relevancy, faithfulness, and 40+ others), run locally or on a cloud platform | Separate test runner / cloud |
|
|
814
|
+
| [LangSmith](https://www.langchain.com/langsmith) (LangChain) | Agent tracing and dataset-based evaluation for the LangChain / LangGraph stack: trajectory matching and LLM-as-judge, with a pytest / vitest integration | Hosted platform (plus open-source evaluator packages) |
|
|
815
|
+
| Managed cloud eval services: [Amazon Bedrock AgentCore Evaluations](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/evaluations), [Google Vertex AI Gen AI evaluation](https://cloud.google.com/vertex-ai/generative-ai/docs/models/evaluation-overview), [Azure AI Foundry evaluators](https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-evaluators/agent-evaluators) | Hosted quality scoring (LLM-as-judge) plus some deterministic trajectory and metric checks, with datasets, dashboards, and reuse across CI and production | Managed, tied to a cloud account |
|
|
816
|
+
| [Scenario](https://github.com/langwatch/scenario) (LangWatch) | Multi-turn simulation of a user conversing with the agent | Simulation against a live LLM |
|
|
817
|
+
|
|
818
|
+
Most of these tools focus on output quality, often via LLM-as-judge, and several now add deterministic trajectory checks too. The line agentverify draws is narrower and lower in the stack: it asserts the **trajectory** (what tools ran, with what arguments, in what order, feeding what into what) inline in pytest, with no LLM in the loop on replay and no cloud account. Quality judgements ("was this a good answer?") need a judge model and fit better on a separate stage of the pipeline, usually against a live agent in staging rather than on every PR. The two are complementary: one gates the agent's actions cheaply on every commit, the other scores answer quality less often, where the judge cost is justified.
|
|
819
|
+
|
|
820
|
+
A rough guide:
|
|
821
|
+
|
|
822
|
+
- Want a broad catalog of quality metrics you can run locally? DeepEval has the widest catalog.
|
|
823
|
+
- Building entirely on the LangChain / LangGraph stack and want tracing plus evaluation in one place? LangSmith fits naturally.
|
|
824
|
+
- Already invested in a cloud agent platform (AWS, Google, or Azure) and want hosted quality scoring with datasets and dashboards? The managed eval services fit, and several now include deterministic trajectory checks too.
|
|
825
|
+
- Need to stress-test multi-turn conversations with a simulated user? Scenario is built for that.
|
|
826
|
+
- Want trajectory assertions inline in pytest, on every PR, with no API key, no secrets, and no cloud account to stand up? That is what agentverify is for.
|
|
827
|
+
|
|
730
828
|
## Roadmap
|
|
731
829
|
|
|
732
|
-
We want agentverify to become the way people regression-test agents in CI
|
|
830
|
+
We want agentverify to become the way people regression-test agents in CI: tests that live next to the agent code, in git, reviewed in PRs alongside the prompt and tool changes that affect them.
|
|
733
831
|
|
|
734
|
-
- Async support
|
|
735
|
-
- Responses API cassette adapter
|
|
736
|
-
- Framework adapters for Google ADK and CrewAI
|
|
737
|
-
- Cost estimation from tokens — auto-calculate `total_cost_usd` from token usage and model pricing
|
|
738
|
-
- Eval-framework bridges — compose ragas / deepeval quality scores with agentverify's action assertions in a single test report
|
|
832
|
+
- Async support: first-class `asyncio` testing for async agents and tools
|
|
833
|
+
- Responses API cassette adapter: record/replay for OpenAI Agents SDK (Responses API) with end-to-end example
|
|
834
|
+
- Framework adapters for Google ADK and CrewAI: pending async support and stable tool-call APIs from these frameworks
|
|
739
835
|
|
|
740
836
|
## Changelog
|
|
741
837
|
|