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.
Files changed (50) hide show
  1. {agentverify-0.1.0 → agentverify-0.2.0}/PKG-INFO +204 -13
  2. {agentverify-0.1.0 → agentverify-0.2.0}/README.md +202 -12
  3. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/__init__.py +12 -0
  4. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/io.py +3 -1
  5. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/recorder.py +74 -13
  6. agentverify-0.2.0/agentverify/cassette/sanitize.py +120 -0
  7. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/errors.py +31 -0
  8. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/fixtures.py +9 -0
  9. agentverify-0.2.0/agentverify/frameworks/__init__.py +11 -0
  10. agentverify-0.2.0/agentverify/frameworks/langchain.py +92 -0
  11. agentverify-0.2.0/agentverify/frameworks/langgraph.py +100 -0
  12. agentverify-0.2.0/agentverify/frameworks/openai_agents.py +113 -0
  13. agentverify-0.2.0/agentverify/frameworks/strands.py +93 -0
  14. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/plugin.py +7 -1
  15. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/PKG-INFO +204 -13
  16. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/SOURCES.txt +8 -1
  17. {agentverify-0.1.0 → agentverify-0.2.0}/pyproject.toml +2 -1
  18. {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_errors.py +44 -0
  19. {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_fixtures.py +41 -0
  20. {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_plugin.py +15 -7
  21. agentverify-0.2.0/tests/test_recorder.py +522 -0
  22. agentverify-0.2.0/tests/test_sanitize.py +220 -0
  23. agentverify-0.1.0/tests/test_recorder.py +0 -276
  24. {agentverify-0.1.0 → agentverify-0.2.0}/LICENSE +0 -0
  25. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/assertions.py +0 -0
  26. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/__init__.py +0 -0
  27. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/__init__.py +0 -0
  28. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/anthropic.py +0 -0
  29. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/base.py +0 -0
  30. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/bedrock.py +0 -0
  31. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/gemini.py +0 -0
  32. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/litellm.py +0 -0
  33. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/cassette/adapters/openai.py +0 -0
  34. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/markers.py +0 -0
  35. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/matchers.py +0 -0
  36. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/models.py +0 -0
  37. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify/py.typed +0 -0
  38. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/dependency_links.txt +0 -0
  39. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/entry_points.txt +0 -0
  40. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/requires.txt +0 -0
  41. {agentverify-0.1.0 → agentverify-0.2.0}/agentverify.egg-info/top_level.txt +0 -0
  42. {agentverify-0.1.0 → agentverify-0.2.0}/setup.cfg +0 -0
  43. {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_assertions.py +0 -0
  44. {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_base_adapter.py +0 -0
  45. {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_cassette_io.py +0 -0
  46. {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_init.py +0 -0
  47. {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_markers.py +0 -0
  48. {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_matchers.py +0 -0
  49. {agentverify-0.1.0 → agentverify-0.2.0}/tests/test_models.py +0 -0
  50. {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.1.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 — write a small converter function instead. See [Building from Your Framework](#building-from-your-framework) below.
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, or use a framework-specific converter (see [examples/](examples/) for Strands Agents and LangChain converters).
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
- Every agent framework has its own output format. To use agentverify, you need a small converter function (~20–50 lines) that maps your framework's output to the dict schema above. Here's the general pattern:
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
- **⚠️ Important: Cassette replay uses sequential matching** — responses are returned in recorded order without verifying request content. If your agent's prompts, tools, or model change, the cassette will still replay but may return stale/incorrect responses silently. **Delete and re-record cassettes after significant agent changes.** Request content matching is planned for a future release (see [Roadmap](#roadmap)).
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
- - Be mindful not to include sensitive data (API keys, PII, confidential prompts) in cassette files checked into version control.
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
- agentverify is framework-agnostic. Build an `ExecutionResult` from any agent framework's output using a converter function. The [`examples/`](examples/) directory includes ready-to-use converters:
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. Built-in framework adapters are planned for a future release (see [Roadmap](#roadmap)).
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 — write a small converter function instead. See [Building from Your Framework](#building-from-your-framework) below.
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, or use a framework-specific converter (see [examples/](examples/) for Strands Agents and LangChain converters).
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
- Every agent framework has its own output format. To use agentverify, you need a small converter function (~20–50 lines) that maps your framework's output to the dict schema above. Here's the general pattern:
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
- **⚠️ Important: Cassette replay uses sequential matching** — responses are returned in recorded order without verifying request content. If your agent's prompts, tools, or model change, the cassette will still replay but may return stale/incorrect responses silently. **Delete and re-record cassettes after significant agent changes.** Request content matching is planned for a future release (see [Roadmap](#roadmap)).
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
- - Be mindful not to include sensitive data (API keys, PII, confidential prompts) in cassette files checked into version control.
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
- agentverify is framework-agnostic. Build an `ExecutionResult` from any agent framework's output using a converter function. The [`examples/`](examples/) directory includes ready-to-use converters:
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. Built-in framework adapters are planned for a future release (see [Roadmap](#roadmap)).
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 = "0.1.0"
25
+ _AGENTVERIFY_VERSION = _pkg_version("agentverify")
24
26
 
25
27
  _YAML_EXTENSIONS = {".yaml", ".yml"}
26
28
  _JSON_EXTENSIONS = {".json"}