agentverify 0.1.0__tar.gz → 0.2.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.1.0 → agentverify-0.2.0}/PKG-INFO +204 -13
- {agentverify-0.1.0 → agentverify-0.2.0}/README.md +202 -12
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/__init__.py +12 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/io.py +3 -1
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/recorder.py +74 -13
- agentverify-0.2.0/agentverify/cassette/sanitize.py +120 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/errors.py +31 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/fixtures.py +9 -0
- agentverify-0.2.0/agentverify/frameworks/__init__.py +11 -0
- agentverify-0.2.0/agentverify/frameworks/langchain.py +92 -0
- agentverify-0.2.0/agentverify/frameworks/langgraph.py +100 -0
- agentverify-0.2.0/agentverify/frameworks/openai_agents.py +113 -0
- agentverify-0.2.0/agentverify/frameworks/strands.py +93 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/plugin.py +7 -1
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/PKG-INFO +204 -13
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/SOURCES.txt +8 -1
- {agentverify-0.1.0 → agentverify-0.2.0}/pyproject.toml +2 -1
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_errors.py +44 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_fixtures.py +41 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_plugin.py +15 -7
- agentverify-0.2.0/tests/test_recorder.py +522 -0
- agentverify-0.2.0/tests/test_sanitize.py +220 -0
- agentverify-0.1.0/tests/test_recorder.py +0 -276
- {agentverify-0.1.0 → agentverify-0.2.0}/LICENSE +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/assertions.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/__init__.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/__init__.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/anthropic.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/base.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/bedrock.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/gemini.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/litellm.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/openai.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/markers.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/matchers.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/models.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/py.typed +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/dependency_links.txt +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/entry_points.txt +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/requires.txt +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/top_level.txt +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/setup.cfg +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_assertions.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_base_adapter.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_cassette_io.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_init.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_markers.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_matchers.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_models.py +0 -0
- {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_properties.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentverify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Deterministic testing for AI agents. Assert tool calls, not vibes.
|
|
5
5
|
Author: simukappu
|
|
6
6
|
License: MIT
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
22
|
Classifier: Topic :: Software Development :: Testing
|
|
22
23
|
Requires-Python: >=3.10
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
@@ -110,7 +111,7 @@ def test_output():
|
|
|
110
111
|
assert_final_output(result, contains="Tokyo")
|
|
111
112
|
```
|
|
112
113
|
|
|
113
|
-
> **Using a real agent framework?** You don't need to build dicts by hand —
|
|
114
|
+
> **Using a real agent framework?** You don't need to build dicts by hand — use a built-in adapter instead. See [Framework Integration](#framework-integration) below.
|
|
114
115
|
|
|
115
116
|
```
|
|
116
117
|
$ pytest test_agent.py -v
|
|
@@ -120,11 +121,95 @@ test_agent.py::test_safety PASSED
|
|
|
120
121
|
test_agent.py::test_output PASSED
|
|
121
122
|
```
|
|
122
123
|
|
|
124
|
+
## Real-World Example — Testing the Strands Weather Forecaster
|
|
125
|
+
|
|
126
|
+
Here's how you'd test the [Strands Weather Forecaster](https://strandsagents.com/docs/examples/python/weather_forecaster/) — an official Strands Agents sample that uses the `http_request` tool to fetch weather data from the National Weather Service API.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import pytest
|
|
130
|
+
from agentverify import (
|
|
131
|
+
assert_tool_calls, assert_cost, assert_no_tool_call,
|
|
132
|
+
assert_final_output, assert_all, ToolCall, ANY, OrderMode,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Import the weather agent from the Strands sample
|
|
136
|
+
# (see https://strandsagents.com/docs/examples/python/weather_forecaster/)
|
|
137
|
+
from weather_agent import weather_agent
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.mark.agentverify
|
|
141
|
+
def test_weather_agent_tool_sequence(cassette):
|
|
142
|
+
"""The agent should call http_request twice: location lookup, then forecast."""
|
|
143
|
+
with cassette("weather_seattle.yaml", provider="bedrock") as rec:
|
|
144
|
+
weather_agent("What's the weather in Seattle?")
|
|
145
|
+
|
|
146
|
+
result = rec.to_execution_result()
|
|
147
|
+
assert_tool_calls(
|
|
148
|
+
result,
|
|
149
|
+
expected=[
|
|
150
|
+
ToolCall("http_request", {"method": "GET", "url": ANY}),
|
|
151
|
+
ToolCall("http_request", {"method": "GET", "url": ANY}),
|
|
152
|
+
],
|
|
153
|
+
order=OrderMode.IN_ORDER,
|
|
154
|
+
partial_args=True,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.mark.agentverify
|
|
159
|
+
def test_weather_agent_safety(cassette):
|
|
160
|
+
"""The agent should only read data — no writes, no dangerous tools."""
|
|
161
|
+
with cassette("weather_seattle.yaml", provider="bedrock") as rec:
|
|
162
|
+
weather_agent("What's the weather in Seattle?")
|
|
163
|
+
|
|
164
|
+
result = rec.to_execution_result()
|
|
165
|
+
assert_no_tool_call(
|
|
166
|
+
result,
|
|
167
|
+
forbidden_tools=["write_file", "execute_command", "delete_file"],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.agentverify
|
|
172
|
+
def test_weather_agent_all(cassette):
|
|
173
|
+
"""Run all assertions at once — collect all failures."""
|
|
174
|
+
with cassette("weather_seattle.yaml", provider="bedrock") as rec:
|
|
175
|
+
weather_agent("What's the weather in Seattle?")
|
|
176
|
+
|
|
177
|
+
result = rec.to_execution_result()
|
|
178
|
+
assert_all(
|
|
179
|
+
result,
|
|
180
|
+
lambda r: assert_tool_calls(r, expected=[
|
|
181
|
+
ToolCall("http_request", {"method": "GET", "url": ANY}),
|
|
182
|
+
], order=OrderMode.IN_ORDER, partial_args=True),
|
|
183
|
+
lambda r: assert_cost(r, max_tokens=15000),
|
|
184
|
+
lambda r: assert_no_tool_call(r, forbidden_tools=["write_file"]),
|
|
185
|
+
lambda r: assert_final_output(r, contains="Seattle"),
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Or skip cassettes entirely and use the built-in adapter with a live agent:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from agentverify.frameworks.strands import from_strands
|
|
193
|
+
def test_weather_agent_live():
|
|
194
|
+
"""Test with a live LLM call (requires API credentials)."""
|
|
195
|
+
result = weather_agent("What's the weather in Seattle?")
|
|
196
|
+
er = from_strands(result)
|
|
197
|
+
|
|
198
|
+
assert_tool_calls(er, expected=[
|
|
199
|
+
ToolCall("http_request", {"method": "GET", "url": ANY}),
|
|
200
|
+
ToolCall("http_request", {"method": "GET", "url": ANY}),
|
|
201
|
+
], order=OrderMode.IN_ORDER, partial_args=True)
|
|
202
|
+
|
|
203
|
+
assert_cost(er, max_tokens=15000)
|
|
204
|
+
assert_no_tool_call(er, forbidden_tools=["write_file", "execute_command"])
|
|
205
|
+
assert_final_output(er, contains="Seattle")
|
|
206
|
+
```
|
|
207
|
+
|
|
123
208
|
## 3 Steps to Test Your Agent
|
|
124
209
|
|
|
125
210
|
### Step 1: Build an ExecutionResult
|
|
126
211
|
|
|
127
|
-
Build an `ExecutionResult` from your agent's output. You can construct one from a dict,
|
|
212
|
+
Build an `ExecutionResult` from your agent's output. You can construct one from a dict, use a [built-in adapter](#framework-integration), or write a small converter for your framework.
|
|
128
213
|
|
|
129
214
|
```python
|
|
130
215
|
from agentverify import ExecutionResult
|
|
@@ -153,7 +238,16 @@ You can also use `ExecutionResult.from_json(json_string)` to parse from a JSON s
|
|
|
153
238
|
|
|
154
239
|
#### Building from Your Framework
|
|
155
240
|
|
|
156
|
-
|
|
241
|
+
For frameworks with built-in adapter support (Strands Agents, LangChain, LangGraph, OpenAI Agents SDK), use the adapter directly:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from agentverify.frameworks.strands import from_strands
|
|
245
|
+
|
|
246
|
+
result = agent("Analyze the files")
|
|
247
|
+
execution_result = from_strands(result)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
For other frameworks, write a small converter function (~20–50 lines) that maps your framework's output to the dict schema above. See [`examples/strands-file-organizer/converter.py`](examples/strands-file-organizer/converter.py) and [`examples/langchain-issue-triage/converter.py`](examples/langchain-issue-triage/converter.py) for reference implementations. Here's the general pattern:
|
|
157
251
|
|
|
158
252
|
```python
|
|
159
253
|
from agentverify import ExecutionResult, ToolCall, TokenUsage
|
|
@@ -180,8 +274,6 @@ def my_framework_to_execution_result(agent_output) -> ExecutionResult:
|
|
|
180
274
|
)
|
|
181
275
|
```
|
|
182
276
|
|
|
183
|
-
See [`examples/strands-file-organizer/converter.py`](examples/strands-file-organizer/converter.py) and [`examples/langchain-issue-triage/converter.py`](examples/langchain-issue-triage/converter.py) for complete, production-ready converters. Built-in framework adapters are planned for a future release (see [Roadmap](#roadmap)).
|
|
184
|
-
|
|
185
277
|
### Step 2: Assert
|
|
186
278
|
|
|
187
279
|
```python
|
|
@@ -208,7 +300,7 @@ assert_final_output(result, contains="Tokyo")
|
|
|
208
300
|
|
|
209
301
|
Record real LLM API calls once. Replay them in CI forever — zero cost, deterministic.
|
|
210
302
|
|
|
211
|
-
|
|
303
|
+
Cassette replay verifies request content by default — if your model or tools change, replay raises `CassetteRequestMismatchError` instead of silently returning stale responses. Re-record cassettes with `--cassette-mode=record` after significant agent changes. See [Request Matching](#request-matching-stale-cassette-detection) for details.
|
|
212
304
|
|
|
213
305
|
The `cassette` fixture is a pytest fixture provided by the agentverify 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.
|
|
214
306
|
|
|
@@ -261,10 +353,61 @@ pytest --cassette-mode=record
|
|
|
261
353
|
pytest
|
|
262
354
|
```
|
|
263
355
|
|
|
356
|
+
### Request Matching (Stale Cassette Detection)
|
|
357
|
+
|
|
358
|
+
Cassette replay verifies request content by default. Each replay request is compared against the recorded request:
|
|
359
|
+
- **Model name**: must match exactly (empty model in either side skips the check)
|
|
360
|
+
- **Tool names**: the sorted list of tool names must match (empty tools in either side skips the check)
|
|
361
|
+
|
|
362
|
+
A `CassetteRequestMismatchError` is raised on mismatch with a clear message indicating which field differs and suggesting re-recording.
|
|
363
|
+
|
|
364
|
+
To disable matching (e.g., during migration):
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# CLI: disable for all tests
|
|
368
|
+
pytest --no-cassette-match-requests
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
# Per-test: disable in the cassette factory call
|
|
373
|
+
with cassette("my_test.yaml", provider="openai", match_requests=False) as rec:
|
|
374
|
+
run_my_agent("What's the weather?")
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Cassette Sanitization
|
|
378
|
+
|
|
379
|
+
Cassette files are sanitized by default when recording. API keys, tokens, and other sensitive data are automatically redacted before saving to disk.
|
|
380
|
+
|
|
381
|
+
Built-in patterns cover:
|
|
382
|
+
- OpenAI API keys (`sk-...`)
|
|
383
|
+
- Anthropic API keys (`sk-ant-...`)
|
|
384
|
+
- AWS access keys (`AKIA...`)
|
|
385
|
+
- Bearer tokens (`Bearer ...`)
|
|
386
|
+
|
|
387
|
+
Add custom patterns:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
from agentverify import SanitizePattern
|
|
391
|
+
|
|
392
|
+
custom_patterns = [
|
|
393
|
+
SanitizePattern(name="internal_token", pattern=r"tok-[a-f0-9]{32}", replacement="tok-***REDACTED***"),
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
with cassette("test.yaml", provider="openai", sanitize=custom_patterns) as rec:
|
|
397
|
+
run_my_agent("Do something")
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Disable sanitization (not recommended):
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
with cassette("test.yaml", provider="openai", sanitize=False) as rec:
|
|
404
|
+
run_my_agent("Do something")
|
|
405
|
+
```
|
|
406
|
+
|
|
264
407
|
**Other limitations:**
|
|
265
408
|
|
|
266
409
|
- `total_cost_usd` is not populated from cassettes. Use `assert_cost(max_tokens=...)` for cassette-based budget checks, or set `total_cost_usd` manually in your `ExecutionResult`.
|
|
267
|
-
-
|
|
410
|
+
- Cassette sanitization covers common API key patterns by default. Add custom `SanitizePattern` objects for application-specific secrets (internal tokens, database credentials, etc.).
|
|
268
411
|
|
|
269
412
|
## Assertion Modes
|
|
270
413
|
|
|
@@ -327,14 +470,59 @@ assert_final_output(result, matches=r"\d+°C")
|
|
|
327
470
|
|
|
328
471
|
## Framework Integration
|
|
329
472
|
|
|
330
|
-
|
|
473
|
+
### Built-in Adapters
|
|
474
|
+
|
|
475
|
+
agentverify ships with built-in adapters for popular agent frameworks. No converter function needed — just import and call:
|
|
476
|
+
|
|
477
|
+
```python
|
|
478
|
+
# Strands Agents
|
|
479
|
+
from agentverify.frameworks.strands import from_strands
|
|
480
|
+
|
|
481
|
+
result = agent("Analyze the files")
|
|
482
|
+
execution_result = from_strands(result)
|
|
483
|
+
|
|
484
|
+
# LangChain
|
|
485
|
+
from agentverify.frameworks.langchain import from_langchain
|
|
486
|
+
|
|
487
|
+
result = agent_executor.invoke({"input": "Triage the issues"})
|
|
488
|
+
execution_result = from_langchain(result)
|
|
489
|
+
|
|
490
|
+
# LangGraph
|
|
491
|
+
from agentverify.frameworks.langgraph import from_langgraph
|
|
492
|
+
|
|
493
|
+
result = agent.invoke({"messages": [{"role": "user", "content": "Hello"}]})
|
|
494
|
+
execution_result = from_langgraph(result)
|
|
495
|
+
|
|
496
|
+
# OpenAI Agents SDK
|
|
497
|
+
from agentverify.frameworks.openai_agents import from_openai_agents
|
|
498
|
+
|
|
499
|
+
result = await Runner.run(agent, "What's the weather?")
|
|
500
|
+
execution_result = from_openai_agents(result)
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
| Framework | Adapter | Input |
|
|
504
|
+
|---|---|---|
|
|
505
|
+
| [Strands Agents](https://github.com/strands-agents/sdk-python) | `from agentverify.frameworks.strands import from_strands` | `AgentResult` |
|
|
506
|
+
| [LangChain](https://github.com/langchain-ai/langchain) | `from agentverify.frameworks.langchain import from_langchain` | `AgentExecutor.invoke()` dict |
|
|
507
|
+
| [LangGraph](https://github.com/langchain-ai/langgraph) | `from agentverify.frameworks.langgraph import from_langgraph` | `create_react_agent` result dict |
|
|
508
|
+
| [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) | `from agentverify.frameworks.openai_agents import from_openai_agents` | `RunResult` |
|
|
509
|
+
|
|
510
|
+
For LangChain, pass the conversation history `messages` list as a second argument to capture token usage:
|
|
511
|
+
|
|
512
|
+
```python
|
|
513
|
+
execution_result = from_langchain(result, messages=memory.chat_memory.messages)
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Custom Converters
|
|
517
|
+
|
|
518
|
+
For other frameworks, agentverify is framework-agnostic. Build an `ExecutionResult` from any agent framework's output using a converter function. The [`examples/`](examples/) directory includes reference converters:
|
|
331
519
|
|
|
332
520
|
| Framework | Converter | Description |
|
|
333
521
|
|---|---|---|
|
|
334
522
|
| [Strands Agents](https://github.com/strands-agents/sdk-python) | [`strands-file-organizer/converter.py`](examples/strands-file-organizer/converter.py) | Converts `AgentResult` → `ExecutionResult` |
|
|
335
523
|
| [LangChain](https://github.com/langchain-ai/langchain) | [`langchain-issue-triage/converter.py`](examples/langchain-issue-triage/converter.py) | Converts `AgentExecutor` output → `ExecutionResult` |
|
|
336
524
|
|
|
337
|
-
These converters are small (~50 lines) and easy to adapt for your own framework.
|
|
525
|
+
These converters are small (~50 lines) and easy to adapt for your own framework.
|
|
338
526
|
|
|
339
527
|
## Supported LLM Providers
|
|
340
528
|
|
|
@@ -420,6 +608,7 @@ The [`examples/`](examples/) directory contains end-to-end examples with real ag
|
|
|
420
608
|
|
|
421
609
|
| Example | Framework | Description |
|
|
422
610
|
|---|---|---|
|
|
611
|
+
| [`strands-weather-forecaster`](examples/strands-weather-forecaster/) | Strands Agents + Bedrock | Fetches weather via HTTP, verifies tool sequence and safety. Matches the [Real-World Example](#real-world-example--testing-the-strands-weather-forecaster) in this README |
|
|
423
612
|
| [`strands-file-organizer`](examples/strands-file-organizer/) | Strands Agents + Bedrock | Scans a directory via Filesystem MCP, suggests organization. Read-only safety verified |
|
|
424
613
|
| [`langchain-issue-triage`](examples/langchain-issue-triage/) | LangChain + OpenAI | Triages GitHub issues via GitHub MCP. Label and priority suggestions |
|
|
425
614
|
| [`mcp-server`](examples/mcp-server/) | — | Mock GitHub MCP server for token-free testing |
|
|
@@ -458,11 +647,13 @@ See each example's README for agent execution instructions and recording mode de
|
|
|
458
647
|
|
|
459
648
|
## Roadmap
|
|
460
649
|
|
|
461
|
-
- Agent framework adapters — extract `ExecutionResult` directly from Strands Agents, LangChain, and others without writing a converter
|
|
650
|
+
- ~~Agent framework adapters — extract `ExecutionResult` directly from Strands Agents, LangChain, and others without writing a converter~~ ✅ Shipped
|
|
651
|
+
- Framework adapters for Google ADK and CrewAI — pending async support and stable tool-call APIs from these frameworks
|
|
652
|
+
- Responses API cassette adapter — record/replay for OpenAI Agents SDK (Responses API) with end-to-end example
|
|
462
653
|
- Tool mocking/stubbing — test agent routing logic without calling real tools
|
|
463
654
|
- Async support — first-class `asyncio` testing for async agents and tools
|
|
464
|
-
- Cassette request matching — verify request content during replay to detect stale cassettes
|
|
465
|
-
- Cassette sanitization — automatic masking of API keys and sensitive data in recorded cassettes
|
|
655
|
+
- ~~Cassette request matching — verify request content during replay to detect stale cassettes~~ ✅ Shipped
|
|
656
|
+
- ~~Cassette sanitization — automatic masking of API keys and sensitive data in recorded cassettes~~ ✅ Shipped
|
|
466
657
|
- Cost estimation from tokens — auto-calculate `total_cost_usd` from token usage and model pricing
|
|
467
658
|
- YAML/JSON test case definitions — declarative test cases for non-Python CI pipelines
|
|
468
659
|
- CLI test runner — run agent tests without pytest
|
|
@@ -63,7 +63,7 @@ def test_output():
|
|
|
63
63
|
assert_final_output(result, contains="Tokyo")
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
> **Using a real agent framework?** You don't need to build dicts by hand —
|
|
66
|
+
> **Using a real agent framework?** You don't need to build dicts by hand — use a built-in adapter instead. See [Framework Integration](#framework-integration) below.
|
|
67
67
|
|
|
68
68
|
```
|
|
69
69
|
$ pytest test_agent.py -v
|
|
@@ -73,11 +73,95 @@ test_agent.py::test_safety PASSED
|
|
|
73
73
|
test_agent.py::test_output PASSED
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
## Real-World Example — Testing the Strands Weather Forecaster
|
|
77
|
+
|
|
78
|
+
Here's how you'd test the [Strands Weather Forecaster](https://strandsagents.com/docs/examples/python/weather_forecaster/) — an official Strands Agents sample that uses the `http_request` tool to fetch weather data from the National Weather Service API.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import pytest
|
|
82
|
+
from agentverify import (
|
|
83
|
+
assert_tool_calls, assert_cost, assert_no_tool_call,
|
|
84
|
+
assert_final_output, assert_all, ToolCall, ANY, OrderMode,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Import the weather agent from the Strands sample
|
|
88
|
+
# (see https://strandsagents.com/docs/examples/python/weather_forecaster/)
|
|
89
|
+
from weather_agent import weather_agent
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.agentverify
|
|
93
|
+
def test_weather_agent_tool_sequence(cassette):
|
|
94
|
+
"""The agent should call http_request twice: location lookup, then forecast."""
|
|
95
|
+
with cassette("weather_seattle.yaml", provider="bedrock") as rec:
|
|
96
|
+
weather_agent("What's the weather in Seattle?")
|
|
97
|
+
|
|
98
|
+
result = rec.to_execution_result()
|
|
99
|
+
assert_tool_calls(
|
|
100
|
+
result,
|
|
101
|
+
expected=[
|
|
102
|
+
ToolCall("http_request", {"method": "GET", "url": ANY}),
|
|
103
|
+
ToolCall("http_request", {"method": "GET", "url": ANY}),
|
|
104
|
+
],
|
|
105
|
+
order=OrderMode.IN_ORDER,
|
|
106
|
+
partial_args=True,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.mark.agentverify
|
|
111
|
+
def test_weather_agent_safety(cassette):
|
|
112
|
+
"""The agent should only read data — no writes, no dangerous tools."""
|
|
113
|
+
with cassette("weather_seattle.yaml", provider="bedrock") as rec:
|
|
114
|
+
weather_agent("What's the weather in Seattle?")
|
|
115
|
+
|
|
116
|
+
result = rec.to_execution_result()
|
|
117
|
+
assert_no_tool_call(
|
|
118
|
+
result,
|
|
119
|
+
forbidden_tools=["write_file", "execute_command", "delete_file"],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.mark.agentverify
|
|
124
|
+
def test_weather_agent_all(cassette):
|
|
125
|
+
"""Run all assertions at once — collect all failures."""
|
|
126
|
+
with cassette("weather_seattle.yaml", provider="bedrock") as rec:
|
|
127
|
+
weather_agent("What's the weather in Seattle?")
|
|
128
|
+
|
|
129
|
+
result = rec.to_execution_result()
|
|
130
|
+
assert_all(
|
|
131
|
+
result,
|
|
132
|
+
lambda r: assert_tool_calls(r, expected=[
|
|
133
|
+
ToolCall("http_request", {"method": "GET", "url": ANY}),
|
|
134
|
+
], order=OrderMode.IN_ORDER, partial_args=True),
|
|
135
|
+
lambda r: assert_cost(r, max_tokens=15000),
|
|
136
|
+
lambda r: assert_no_tool_call(r, forbidden_tools=["write_file"]),
|
|
137
|
+
lambda r: assert_final_output(r, contains="Seattle"),
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Or skip cassettes entirely and use the built-in adapter with a live agent:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from agentverify.frameworks.strands import from_strands
|
|
145
|
+
def test_weather_agent_live():
|
|
146
|
+
"""Test with a live LLM call (requires API credentials)."""
|
|
147
|
+
result = weather_agent("What's the weather in Seattle?")
|
|
148
|
+
er = from_strands(result)
|
|
149
|
+
|
|
150
|
+
assert_tool_calls(er, expected=[
|
|
151
|
+
ToolCall("http_request", {"method": "GET", "url": ANY}),
|
|
152
|
+
ToolCall("http_request", {"method": "GET", "url": ANY}),
|
|
153
|
+
], order=OrderMode.IN_ORDER, partial_args=True)
|
|
154
|
+
|
|
155
|
+
assert_cost(er, max_tokens=15000)
|
|
156
|
+
assert_no_tool_call(er, forbidden_tools=["write_file", "execute_command"])
|
|
157
|
+
assert_final_output(er, contains="Seattle")
|
|
158
|
+
```
|
|
159
|
+
|
|
76
160
|
## 3 Steps to Test Your Agent
|
|
77
161
|
|
|
78
162
|
### Step 1: Build an ExecutionResult
|
|
79
163
|
|
|
80
|
-
Build an `ExecutionResult` from your agent's output. You can construct one from a dict,
|
|
164
|
+
Build an `ExecutionResult` from your agent's output. You can construct one from a dict, use a [built-in adapter](#framework-integration), or write a small converter for your framework.
|
|
81
165
|
|
|
82
166
|
```python
|
|
83
167
|
from agentverify import ExecutionResult
|
|
@@ -106,7 +190,16 @@ You can also use `ExecutionResult.from_json(json_string)` to parse from a JSON s
|
|
|
106
190
|
|
|
107
191
|
#### Building from Your Framework
|
|
108
192
|
|
|
109
|
-
|
|
193
|
+
For frameworks with built-in adapter support (Strands Agents, LangChain, LangGraph, OpenAI Agents SDK), use the adapter directly:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from agentverify.frameworks.strands import from_strands
|
|
197
|
+
|
|
198
|
+
result = agent("Analyze the files")
|
|
199
|
+
execution_result = from_strands(result)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
For other frameworks, write a small converter function (~20–50 lines) that maps your framework's output to the dict schema above. See [`examples/strands-file-organizer/converter.py`](examples/strands-file-organizer/converter.py) and [`examples/langchain-issue-triage/converter.py`](examples/langchain-issue-triage/converter.py) for reference implementations. Here's the general pattern:
|
|
110
203
|
|
|
111
204
|
```python
|
|
112
205
|
from agentverify import ExecutionResult, ToolCall, TokenUsage
|
|
@@ -133,8 +226,6 @@ def my_framework_to_execution_result(agent_output) -> ExecutionResult:
|
|
|
133
226
|
)
|
|
134
227
|
```
|
|
135
228
|
|
|
136
|
-
See [`examples/strands-file-organizer/converter.py`](examples/strands-file-organizer/converter.py) and [`examples/langchain-issue-triage/converter.py`](examples/langchain-issue-triage/converter.py) for complete, production-ready converters. Built-in framework adapters are planned for a future release (see [Roadmap](#roadmap)).
|
|
137
|
-
|
|
138
229
|
### Step 2: Assert
|
|
139
230
|
|
|
140
231
|
```python
|
|
@@ -161,7 +252,7 @@ assert_final_output(result, contains="Tokyo")
|
|
|
161
252
|
|
|
162
253
|
Record real LLM API calls once. Replay them in CI forever — zero cost, deterministic.
|
|
163
254
|
|
|
164
|
-
|
|
255
|
+
Cassette replay verifies request content by default — if your model or tools change, replay raises `CassetteRequestMismatchError` instead of silently returning stale responses. Re-record cassettes with `--cassette-mode=record` after significant agent changes. See [Request Matching](#request-matching-stale-cassette-detection) for details.
|
|
165
256
|
|
|
166
257
|
The `cassette` fixture is a pytest fixture provided by the agentverify 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.
|
|
167
258
|
|
|
@@ -214,10 +305,61 @@ pytest --cassette-mode=record
|
|
|
214
305
|
pytest
|
|
215
306
|
```
|
|
216
307
|
|
|
308
|
+
### Request Matching (Stale Cassette Detection)
|
|
309
|
+
|
|
310
|
+
Cassette replay verifies request content by default. Each replay request is compared against the recorded request:
|
|
311
|
+
- **Model name**: must match exactly (empty model in either side skips the check)
|
|
312
|
+
- **Tool names**: the sorted list of tool names must match (empty tools in either side skips the check)
|
|
313
|
+
|
|
314
|
+
A `CassetteRequestMismatchError` is raised on mismatch with a clear message indicating which field differs and suggesting re-recording.
|
|
315
|
+
|
|
316
|
+
To disable matching (e.g., during migration):
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# CLI: disable for all tests
|
|
320
|
+
pytest --no-cassette-match-requests
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
# Per-test: disable in the cassette factory call
|
|
325
|
+
with cassette("my_test.yaml", provider="openai", match_requests=False) as rec:
|
|
326
|
+
run_my_agent("What's the weather?")
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Cassette Sanitization
|
|
330
|
+
|
|
331
|
+
Cassette files are sanitized by default when recording. API keys, tokens, and other sensitive data are automatically redacted before saving to disk.
|
|
332
|
+
|
|
333
|
+
Built-in patterns cover:
|
|
334
|
+
- OpenAI API keys (`sk-...`)
|
|
335
|
+
- Anthropic API keys (`sk-ant-...`)
|
|
336
|
+
- AWS access keys (`AKIA...`)
|
|
337
|
+
- Bearer tokens (`Bearer ...`)
|
|
338
|
+
|
|
339
|
+
Add custom patterns:
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from agentverify import SanitizePattern
|
|
343
|
+
|
|
344
|
+
custom_patterns = [
|
|
345
|
+
SanitizePattern(name="internal_token", pattern=r"tok-[a-f0-9]{32}", replacement="tok-***REDACTED***"),
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
with cassette("test.yaml", provider="openai", sanitize=custom_patterns) as rec:
|
|
349
|
+
run_my_agent("Do something")
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Disable sanitization (not recommended):
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
with cassette("test.yaml", provider="openai", sanitize=False) as rec:
|
|
356
|
+
run_my_agent("Do something")
|
|
357
|
+
```
|
|
358
|
+
|
|
217
359
|
**Other limitations:**
|
|
218
360
|
|
|
219
361
|
- `total_cost_usd` is not populated from cassettes. Use `assert_cost(max_tokens=...)` for cassette-based budget checks, or set `total_cost_usd` manually in your `ExecutionResult`.
|
|
220
|
-
-
|
|
362
|
+
- Cassette sanitization covers common API key patterns by default. Add custom `SanitizePattern` objects for application-specific secrets (internal tokens, database credentials, etc.).
|
|
221
363
|
|
|
222
364
|
## Assertion Modes
|
|
223
365
|
|
|
@@ -280,14 +422,59 @@ assert_final_output(result, matches=r"\d+°C")
|
|
|
280
422
|
|
|
281
423
|
## Framework Integration
|
|
282
424
|
|
|
283
|
-
|
|
425
|
+
### Built-in Adapters
|
|
426
|
+
|
|
427
|
+
agentverify ships with built-in adapters for popular agent frameworks. No converter function needed — just import and call:
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
# Strands Agents
|
|
431
|
+
from agentverify.frameworks.strands import from_strands
|
|
432
|
+
|
|
433
|
+
result = agent("Analyze the files")
|
|
434
|
+
execution_result = from_strands(result)
|
|
435
|
+
|
|
436
|
+
# LangChain
|
|
437
|
+
from agentverify.frameworks.langchain import from_langchain
|
|
438
|
+
|
|
439
|
+
result = agent_executor.invoke({"input": "Triage the issues"})
|
|
440
|
+
execution_result = from_langchain(result)
|
|
441
|
+
|
|
442
|
+
# LangGraph
|
|
443
|
+
from agentverify.frameworks.langgraph import from_langgraph
|
|
444
|
+
|
|
445
|
+
result = agent.invoke({"messages": [{"role": "user", "content": "Hello"}]})
|
|
446
|
+
execution_result = from_langgraph(result)
|
|
447
|
+
|
|
448
|
+
# OpenAI Agents SDK
|
|
449
|
+
from agentverify.frameworks.openai_agents import from_openai_agents
|
|
450
|
+
|
|
451
|
+
result = await Runner.run(agent, "What's the weather?")
|
|
452
|
+
execution_result = from_openai_agents(result)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
| Framework | Adapter | Input |
|
|
456
|
+
|---|---|---|
|
|
457
|
+
| [Strands Agents](https://github.com/strands-agents/sdk-python) | `from agentverify.frameworks.strands import from_strands` | `AgentResult` |
|
|
458
|
+
| [LangChain](https://github.com/langchain-ai/langchain) | `from agentverify.frameworks.langchain import from_langchain` | `AgentExecutor.invoke()` dict |
|
|
459
|
+
| [LangGraph](https://github.com/langchain-ai/langgraph) | `from agentverify.frameworks.langgraph import from_langgraph` | `create_react_agent` result dict |
|
|
460
|
+
| [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) | `from agentverify.frameworks.openai_agents import from_openai_agents` | `RunResult` |
|
|
461
|
+
|
|
462
|
+
For LangChain, pass the conversation history `messages` list as a second argument to capture token usage:
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
execution_result = from_langchain(result, messages=memory.chat_memory.messages)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Custom Converters
|
|
469
|
+
|
|
470
|
+
For other frameworks, agentverify is framework-agnostic. Build an `ExecutionResult` from any agent framework's output using a converter function. The [`examples/`](examples/) directory includes reference converters:
|
|
284
471
|
|
|
285
472
|
| Framework | Converter | Description |
|
|
286
473
|
|---|---|---|
|
|
287
474
|
| [Strands Agents](https://github.com/strands-agents/sdk-python) | [`strands-file-organizer/converter.py`](examples/strands-file-organizer/converter.py) | Converts `AgentResult` → `ExecutionResult` |
|
|
288
475
|
| [LangChain](https://github.com/langchain-ai/langchain) | [`langchain-issue-triage/converter.py`](examples/langchain-issue-triage/converter.py) | Converts `AgentExecutor` output → `ExecutionResult` |
|
|
289
476
|
|
|
290
|
-
These converters are small (~50 lines) and easy to adapt for your own framework.
|
|
477
|
+
These converters are small (~50 lines) and easy to adapt for your own framework.
|
|
291
478
|
|
|
292
479
|
## Supported LLM Providers
|
|
293
480
|
|
|
@@ -373,6 +560,7 @@ The [`examples/`](examples/) directory contains end-to-end examples with real ag
|
|
|
373
560
|
|
|
374
561
|
| Example | Framework | Description |
|
|
375
562
|
|---|---|---|
|
|
563
|
+
| [`strands-weather-forecaster`](examples/strands-weather-forecaster/) | Strands Agents + Bedrock | Fetches weather via HTTP, verifies tool sequence and safety. Matches the [Real-World Example](#real-world-example--testing-the-strands-weather-forecaster) in this README |
|
|
376
564
|
| [`strands-file-organizer`](examples/strands-file-organizer/) | Strands Agents + Bedrock | Scans a directory via Filesystem MCP, suggests organization. Read-only safety verified |
|
|
377
565
|
| [`langchain-issue-triage`](examples/langchain-issue-triage/) | LangChain + OpenAI | Triages GitHub issues via GitHub MCP. Label and priority suggestions |
|
|
378
566
|
| [`mcp-server`](examples/mcp-server/) | — | Mock GitHub MCP server for token-free testing |
|
|
@@ -411,11 +599,13 @@ See each example's README for agent execution instructions and recording mode de
|
|
|
411
599
|
|
|
412
600
|
## Roadmap
|
|
413
601
|
|
|
414
|
-
- Agent framework adapters — extract `ExecutionResult` directly from Strands Agents, LangChain, and others without writing a converter
|
|
602
|
+
- ~~Agent framework adapters — extract `ExecutionResult` directly from Strands Agents, LangChain, and others without writing a converter~~ ✅ Shipped
|
|
603
|
+
- Framework adapters for Google ADK and CrewAI — pending async support and stable tool-call APIs from these frameworks
|
|
604
|
+
- Responses API cassette adapter — record/replay for OpenAI Agents SDK (Responses API) with end-to-end example
|
|
415
605
|
- Tool mocking/stubbing — test agent routing logic without calling real tools
|
|
416
606
|
- Async support — first-class `asyncio` testing for async agents and tools
|
|
417
|
-
- Cassette request matching — verify request content during replay to detect stale cassettes
|
|
418
|
-
- Cassette sanitization — automatic masking of API keys and sensitive data in recorded cassettes
|
|
607
|
+
- ~~Cassette request matching — verify request content during replay to detect stale cassettes~~ ✅ Shipped
|
|
608
|
+
- ~~Cassette sanitization — automatic masking of API keys and sensitive data in recorded cassettes~~ ✅ Shipped
|
|
419
609
|
- Cost estimation from tokens — auto-calculate `total_cost_usd` from token usage and model pricing
|
|
420
610
|
- YAML/JSON test case definitions — declarative test cases for non-Python CI pipelines
|
|
421
611
|
- CLI test runner — run agent tests without pytest
|
|
@@ -9,6 +9,13 @@ Public API re-exports for convenient access::
|
|
|
9
9
|
assert_final_output, assert_all,
|
|
10
10
|
LLMCassetteRecorder, CassetteMode,
|
|
11
11
|
)
|
|
12
|
+
|
|
13
|
+
Framework adapters (no converter needed)::
|
|
14
|
+
|
|
15
|
+
from agentverify.frameworks.strands import from_strands
|
|
16
|
+
from agentverify.frameworks.langchain import from_langchain
|
|
17
|
+
from agentverify.frameworks.langgraph import from_langgraph
|
|
18
|
+
from agentverify.frameworks.openai_agents import from_openai_agents
|
|
12
19
|
"""
|
|
13
20
|
|
|
14
21
|
from agentverify.assertions import (
|
|
@@ -19,6 +26,8 @@ from agentverify.assertions import (
|
|
|
19
26
|
assert_tool_calls,
|
|
20
27
|
)
|
|
21
28
|
from agentverify.cassette.recorder import CassetteMode, LLMCassetteRecorder
|
|
29
|
+
from agentverify.cassette.sanitize import DEFAULT_PATTERNS, SanitizePattern
|
|
30
|
+
from agentverify.errors import CassetteRequestMismatchError
|
|
22
31
|
from agentverify.matchers import ANY, OrderMode
|
|
23
32
|
from agentverify.models import ExecutionResult, TokenUsage, ToolCall
|
|
24
33
|
|
|
@@ -39,4 +48,7 @@ __all__ = [
|
|
|
39
48
|
# Cassette
|
|
40
49
|
"LLMCassetteRecorder",
|
|
41
50
|
"CassetteMode",
|
|
51
|
+
"CassetteRequestMismatchError",
|
|
52
|
+
"SanitizePattern",
|
|
53
|
+
"DEFAULT_PATTERNS",
|
|
42
54
|
]
|
|
@@ -19,8 +19,10 @@ from typing import Any
|
|
|
19
19
|
from agentverify.cassette.adapters.base import NormalizedRequest, NormalizedResponse
|
|
20
20
|
from agentverify.models import TokenUsage
|
|
21
21
|
|
|
22
|
+
from importlib.metadata import version as _pkg_version
|
|
23
|
+
|
|
22
24
|
#: Library version stamped into cassette metadata.
|
|
23
|
-
_AGENTVERIFY_VERSION = "
|
|
25
|
+
_AGENTVERIFY_VERSION = _pkg_version("agentverify")
|
|
24
26
|
|
|
25
27
|
_YAML_EXTENSIONS = {".yaml", ".yml"}
|
|
26
28
|
_JSON_EXTENSIONS = {".json"}
|