pytest-agent-check 0.0.1__py3-none-any.whl
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.
- pytest_agent_check-0.0.1.dist-info/METADATA +390 -0
- pytest_agent_check-0.0.1.dist-info/RECORD +14 -0
- pytest_agent_check-0.0.1.dist-info/WHEEL +5 -0
- pytest_agent_check-0.0.1.dist-info/entry_points.txt +5 -0
- pytest_agent_check-0.0.1.dist-info/licenses/LICENSE +21 -0
- pytest_agent_check-0.0.1.dist-info/top_level.txt +1 -0
- pytest_agent_eval/__init__.py +30 -0
- pytest_agent_eval/api.py +188 -0
- pytest_agent_eval/cassette.py +246 -0
- pytest_agent_eval/cli.py +161 -0
- pytest_agent_eval/comparator.py +129 -0
- pytest_agent_eval/models.py +173 -0
- pytest_agent_eval/plugin.py +108 -0
- pytest_agent_eval/reporter.py +138 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-agent-check
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A pytest plugin for evaluating and testing AI agents — record, replay, and assert agent behavior
|
|
5
|
+
Author-email: Xianpeng Shen <shenxianpeng@example.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shenxianpeng/pytest-agent-check
|
|
8
|
+
Project-URL: Repository, https://github.com/shenxianpeng/pytest-agent-check
|
|
9
|
+
Project-URL: Issues, https://github.com/shenxianpeng/pytest-agent-check/issues
|
|
10
|
+
Keywords: pytest,plugin,agent,AI,evaluation,testing,LLM
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Framework :: Pytest
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pytest>=8.0
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
|
+
Requires-Dist: deepdiff>=7.0
|
|
28
|
+
Requires-Dist: rich>=13.0
|
|
29
|
+
Provides-Extra: semantic
|
|
30
|
+
Requires-Dist: sentence-transformers>=2.2; extra == "semantic"
|
|
31
|
+
Provides-Extra: html
|
|
32
|
+
Requires-Dist: jinja2>=3.0; extra == "html"
|
|
33
|
+
Provides-Extra: cli
|
|
34
|
+
Requires-Dist: typer>=0.9; extra == "cli"
|
|
35
|
+
Provides-Extra: all
|
|
36
|
+
Requires-Dist: pytest-agent-check[cli,html,semantic]; extra == "all"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
<h1 align="center">pytest-agent-check</h1>
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<em>A pytest plugin for evaluating and testing AI agents — record, replay, and assert agent behaviour with confidence.</em>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<a href="https://pypi.org/project/pytest-agent-check/">
|
|
47
|
+
<img src="https://img.shields.io/pypi/v/pytest-agent-check" alt="PyPI">
|
|
48
|
+
</a>
|
|
49
|
+
<a href="https://pypi.org/project/pytest-agent-check/">
|
|
50
|
+
<img src="https://img.shields.io/pypi/pyversions/pytest-agent-check" alt="Python versions">
|
|
51
|
+
</a>
|
|
52
|
+
<a href="https://github.com/shenxianpeng/pytest-agent-check/actions/workflows/ci.yml">
|
|
53
|
+
<img src="https://github.com/shenxianpeng/pytest-agent-check/actions/workflows/ci.yml/badge.svg" alt="CI">
|
|
54
|
+
</a>
|
|
55
|
+
<a href="https://opensource.org/licenses/MIT">
|
|
56
|
+
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT">
|
|
57
|
+
</a>
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Overview
|
|
63
|
+
|
|
64
|
+
**pytest-agent-check** brings the familiar **snapshot testing** and **VCR-like cassette** patterns to AI agent evaluation.
|
|
65
|
+
|
|
66
|
+
If you've ever wished you could test your agent as confidently as you test your REST API — with deterministic assertions, offline replay, and a clear diff when something changes — this plugin is for you.
|
|
67
|
+
|
|
68
|
+
### Why?
|
|
69
|
+
|
|
70
|
+
AI agents are **non-deterministic** and **expensive to run**. You can't just ``assert result == expected`` when the output is natural language. And every test run that calls a real LLM costs money, time, and patience.
|
|
71
|
+
|
|
72
|
+
This plugin solves both problems:
|
|
73
|
+
|
|
74
|
+
1. **Cassette record/replay** — Run tests once in *record mode* to capture all tool calls and outputs as a "cassette" (YAML file). Subsequent runs replay from the cassette: fast, offline, deterministic, free.
|
|
75
|
+
2. **Fluent assertion API** — Assert on the *structure* of tool-call sequences (exact matching) and the *intent* of natural-language output (semantic matching, coming soon).
|
|
76
|
+
3. **Change detection** — When agent behaviour drifts, get a structured diff showing exactly which tool calls changed, were added, or were removed.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Installation
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install pytest-agent-check
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Optional extras:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install pytest-agent-check[semantic] # sentence-transformers for semantic similarity
|
|
90
|
+
pip install pytest-agent-check[cli] # typer-based `agent-eval` CLI
|
|
91
|
+
pip install pytest-agent-check[html] # jinja2 for HTML reports
|
|
92
|
+
pip install pytest-agent-check[all] # everything above
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Requirements
|
|
96
|
+
|
|
97
|
+
- Python 3.10+
|
|
98
|
+
- pytest 8.0+
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Quick Start
|
|
103
|
+
|
|
104
|
+
### 1. Write an agent test
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# test_support_agent.py
|
|
108
|
+
import pytest
|
|
109
|
+
from pytest_agent_eval import agent_test, expect_tools, expect_output
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.fixture
|
|
113
|
+
def agent():
|
|
114
|
+
"""Provide your agent under test."""
|
|
115
|
+
from my_agent import create_agent
|
|
116
|
+
return create_agent()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@agent_test(agent="support-agent", cassette="refund_flow")
|
|
120
|
+
def test_refund_request(agent, cassette):
|
|
121
|
+
"""Agent should process refunds: lookup → eligibility → issue."""
|
|
122
|
+
result = cassette.run(agent.run, "I want a refund for order ORD-001")
|
|
123
|
+
|
|
124
|
+
expect_tools(result).called("lookup_order") \
|
|
125
|
+
.then("check_refund_eligibility") \
|
|
126
|
+
.then("issue_refund")
|
|
127
|
+
|
|
128
|
+
expect_output(result).matches_intent(
|
|
129
|
+
"confirms refund is approved and provides arrival time"
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 2. Record cassettes (first run)
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
agent-eval record test_support_agent.py
|
|
137
|
+
# ─ or ─
|
|
138
|
+
pytest test_support_agent.py --cassette-mode=record -v
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This runs your **real agent**, captures all tool calls and outputs, and saves them to ``.cassettes/refund_flow.yaml``.
|
|
142
|
+
|
|
143
|
+
### 3. Run tests in replay mode
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Requires the CLI extra: pip install pytest-agent-check[cli]
|
|
147
|
+
agent-eval run test_support_agent.py
|
|
148
|
+
# ─ or ─
|
|
149
|
+
pytest test_support_agent.py --cassette-mode=replay -v
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Now tests run **offline** — no API calls, no LLM latency, no cost. The recorded data is returned instantly.
|
|
153
|
+
|
|
154
|
+
### 4. Update baselines when behaviour changes
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
agent-eval update test_support_agent.py
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Re-records all cassettes. Commit the updated YAML files alongside your code changes.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Core Concepts
|
|
165
|
+
|
|
166
|
+
### Cassette (`.cassettes/`)
|
|
167
|
+
|
|
168
|
+
A cassette is a YAML file that records everything your agent did during a test:
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
agent_name: support-agent
|
|
172
|
+
test_name: refund_flow
|
|
173
|
+
interactions:
|
|
174
|
+
- input: "I want a refund for order ORD-001"
|
|
175
|
+
tool_calls:
|
|
176
|
+
- name: lookup_order
|
|
177
|
+
arguments:
|
|
178
|
+
order_id: ORD-001
|
|
179
|
+
result:
|
|
180
|
+
status: delivered
|
|
181
|
+
amount: 49.99
|
|
182
|
+
- name: check_refund_eligibility
|
|
183
|
+
arguments:
|
|
184
|
+
order_id: ORD-001
|
|
185
|
+
result:
|
|
186
|
+
eligible: true
|
|
187
|
+
refund_amount: 49.99
|
|
188
|
+
- name: issue_refund
|
|
189
|
+
arguments:
|
|
190
|
+
order_id: ORD-001
|
|
191
|
+
amount: 49.99
|
|
192
|
+
result:
|
|
193
|
+
refund_id: RF-001
|
|
194
|
+
status: approved
|
|
195
|
+
output: "Your refund of $49.99 has been approved... 3-5 business days."
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Cassettes are the **baseline** you compare against. Treat them like snapshot files — commit them to version control.
|
|
199
|
+
|
|
200
|
+
### Operation modes
|
|
201
|
+
|
|
202
|
+
| Mode | Description |
|
|
203
|
+
|------------|-----------------------------------------------------------------------------|
|
|
204
|
+
| `record` | Run the real agent, save all interactions to a cassette. |
|
|
205
|
+
| `replay` | Return recorded data from the cassette. Fail if cassette doesn't exist. |
|
|
206
|
+
| `auto` | Replay if cassette exists, otherwise record. **Default.** |
|
|
207
|
+
|
|
208
|
+
### How the `cassette.run()` method works
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
result = cassette.run(agent_func, user_input, agent_name="my-agent")
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- **Record mode**: calls `agent_func(user_input)`, records the returned `tool_calls` and `output`, stores them internally.
|
|
215
|
+
- **Replay mode**: loads the cassette for this test, returns the recorded `tool_calls` and `output` without calling `agent_func`.
|
|
216
|
+
- Both modes return a dict with `tool_calls`, `output`, `input`, and `_replayed` (bool).
|
|
217
|
+
|
|
218
|
+
The result dict is what you pass to `expect_tools()` and `expect_output()`.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Assertion API
|
|
223
|
+
|
|
224
|
+
### `expect_tools(result).called(name).then(name)...`
|
|
225
|
+
|
|
226
|
+
Fluent assertions on the **exact sequence** of tool calls.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
expect_tools(result).called("lookup_order") \
|
|
230
|
+
.then("check_refund_eligibility") \
|
|
231
|
+
.then("issue_refund")
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Properties:
|
|
235
|
+
- `.total` — total number of tool calls
|
|
236
|
+
- `.remaining` — how many haven't been asserted yet
|
|
237
|
+
|
|
238
|
+
Raises `AssertionError` on mismatch.
|
|
239
|
+
|
|
240
|
+
### `expect_output(result).matches_intent(description)`
|
|
241
|
+
|
|
242
|
+
Validates the natural-language output is non-empty and documents the expected intent.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
expect_output(result).matches_intent("confirms refund is approved and provides arrival time")
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
In the MVP this is a non-emptiness check. Future releases will add:
|
|
249
|
+
- `.semantic_similarity(threshold=0.85)` — embedding-based similarity
|
|
250
|
+
- `.judged_by("gpt-4o-mini", rubric=...)` — LLM-as-judge evaluation
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## CLI Reference
|
|
255
|
+
|
|
256
|
+
When ``typer`` is installed (``pip install pytest-agent-check[cli]``):
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
agent-eval record [<test-path>] # Record cassettes (real agent runs)
|
|
260
|
+
agent-eval run [<test-path>] # Replay from cassettes (offline)
|
|
261
|
+
agent-eval update [<test-path>] # Re-record (update) baselines
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Options:
|
|
265
|
+
- ``--cassette-dir`` / ``-d`` — Cassette directory (default: ``.cassettes``)
|
|
266
|
+
- ``--verbose`` / ``--quiet`` — Pytest verbosity
|
|
267
|
+
- ``--pytest-args`` — Extra pytest arguments (repeatable, e.g. ``-pytest -x -pytest -k test_refund``)
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Configuration
|
|
272
|
+
|
|
273
|
+
All configuration is via pytest command-line flags:
|
|
274
|
+
|
|
275
|
+
| Flag | Default | Description |
|
|
276
|
+
|--------------------|---------------|--------------------------------------------------|
|
|
277
|
+
| `--cassette-dir` | `.cassettes` | Directory for cassette YAML files |
|
|
278
|
+
| `--cassette-mode` | `auto` | `record`, `replay`, or `auto` |
|
|
279
|
+
| `--agent-eval-report` | off | Print structured eval report after test run |
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## CI / GitHub Actions Integration
|
|
284
|
+
|
|
285
|
+
**Record mode** is for local development. In CI, always use **replay mode** for deterministic, fast, and cost-free testing.
|
|
286
|
+
|
|
287
|
+
```yaml
|
|
288
|
+
# .github/workflows/ci.yml
|
|
289
|
+
name: CI
|
|
290
|
+
on: [pull_request]
|
|
291
|
+
jobs:
|
|
292
|
+
agent-tests:
|
|
293
|
+
runs-on: ubuntu-latest
|
|
294
|
+
strategy:
|
|
295
|
+
matrix:
|
|
296
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
297
|
+
steps:
|
|
298
|
+
- uses: actions/checkout@v7.0.1
|
|
299
|
+
with:
|
|
300
|
+
fetch-depth: 0 # needed for setuptools-scm
|
|
301
|
+
- uses: actions/setup-python@v7.0.0
|
|
302
|
+
with:
|
|
303
|
+
python-version: ${{ matrix.python-version }}
|
|
304
|
+
- run: pip install pytest-agent-check[cli]
|
|
305
|
+
- run: pytest tests/ --cassette-mode=replay -v
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
To update baselines (e.g., after intentional agent changes):
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
pip install pytest-agent-check[cli]
|
|
312
|
+
agent-eval update tests/
|
|
313
|
+
git add .cassettes/
|
|
314
|
+
git commit -m "chore: update agent evaluation baselines"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Project Architecture
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
pytest-agent-check/
|
|
323
|
+
├── pytest_agent_eval/
|
|
324
|
+
│ ├── __init__.py # Public API exports
|
|
325
|
+
│ ├── api.py # @agent_test, expect_tools, expect_output
|
|
326
|
+
│ ├── cassette.py # CassetteManager, CassetteContext (record/replay)
|
|
327
|
+
│ ├── comparator.py # Tool-call sequence comparison (deepdiff)
|
|
328
|
+
│ ├── plugin.py # Pytest plugin (markers, options, fixtures)
|
|
329
|
+
│ ├── reporter.py # Terminal output with rich formatting
|
|
330
|
+
│ ├── cli.py # agent-eval CLI (record/run/update)
|
|
331
|
+
│ └── models.py # Data models (ToolCall, Interaction, Cassette, etc.)
|
|
332
|
+
├── tests/ # Unit and integration tests
|
|
333
|
+
├── examples/ # Runnable example tests
|
|
334
|
+
└── pyproject.toml # Project metadata & dependencies
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Roadmap
|
|
338
|
+
|
|
339
|
+
| Phase | Feature |
|
|
340
|
+
|-------|--------------------------------------------------------------|
|
|
341
|
+
| MVP | ✅ pytest plugin skeleton |
|
|
342
|
+
| | ✅ `@agent_test` decorator & marker |
|
|
343
|
+
| | ✅ `expect_tools` fluent assertion |
|
|
344
|
+
| | ✅ `expect_output` basic intent check |
|
|
345
|
+
| | ✅ Cassette record/replay (YAML-based) |
|
|
346
|
+
| | ✅ Tool-call diff comparison (deepdiff) |
|
|
347
|
+
| | ✅ Terminal reporter (rich) |
|
|
348
|
+
| | ✅ CLI (agent-eval record/run/update) |
|
|
349
|
+
| Next | 🔲 Semantic similarity (`expect_output().semantic_similarity()`) |
|
|
350
|
+
| | 🔲 LLM-as-judge evaluation (`expect_output().judged_by()`) |
|
|
351
|
+
| | 🔲 Multi-turn interaction support |
|
|
352
|
+
| | 🔲 HTML / PR comment report format |
|
|
353
|
+
| | 🔲 GitHub Action for automated PR commenting |
|
|
354
|
+
| | 🔲 LangChain / OpenAI SDK adapters (auto-interception) |
|
|
355
|
+
| | 🔲 MCP / A2A protocol support |
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Development
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
git clone https://github.com/shenxianpeng/pytest-agent-check.git
|
|
363
|
+
cd pytest-agent-check
|
|
364
|
+
python -m venv venv && source venv/bin/activate
|
|
365
|
+
pip install -e ".[dev]"
|
|
366
|
+
pytest
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Running tests
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
pytest tests/ -v # unit tests only
|
|
373
|
+
pytest examples/ --cassette-mode=record # integration tests (record)
|
|
374
|
+
pytest examples/ --cassette-mode=replay # integration tests (replay)
|
|
375
|
+
agent-eval run tests/ # via CLI
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## License
|
|
381
|
+
|
|
382
|
+
MIT — see [LICENSE](LICENSE).
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Contributing
|
|
387
|
+
|
|
388
|
+
Contributions are welcome! Please open an issue or pull request on [GitHub](https://github.com/shenxianpeng/pytest-agent-check).
|
|
389
|
+
|
|
390
|
+
When adding features, include tests and update the documentation. Every PR should pass `pytest tests/ --cassette-mode=replay`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
pytest_agent_check-0.0.1.dist-info/licenses/LICENSE,sha256=naE4MjnMNhwEWhlsEWmTREcCAiaiPVqDRxapVJYn4rM,1070
|
|
2
|
+
pytest_agent_eval/__init__.py,sha256=A0ZykDsaeKMn3kKHMa4iGdhL9hMROxvobSjEodk6c3U,867
|
|
3
|
+
pytest_agent_eval/api.py,sha256=0KNfiukSls0-d50VPaQ0W42t2qp5czYURjeLTjY0h-k,6246
|
|
4
|
+
pytest_agent_eval/cassette.py,sha256=YobwYPTHge-8Vofqs0eXPwnemN1MeXtgPn5z-EiWzEY,8181
|
|
5
|
+
pytest_agent_eval/cli.py,sha256=2fevB7OHmJZWL4YIRI_rliV6Wr67F5oRtimJx0UYK68,4998
|
|
6
|
+
pytest_agent_eval/comparator.py,sha256=d-ND6i3UVK_5N0sHD9bD22lw505PERySLAwr1YP7fkY,4032
|
|
7
|
+
pytest_agent_eval/models.py,sha256=-MY8VMwPKheANAsMaRgI_SjGI8rhL5JkYUjQCJn21Oo,6241
|
|
8
|
+
pytest_agent_eval/plugin.py,sha256=kyZVENitIMx9k-RGaIfp4fjsgGRX1eIgLy8pYGHZ5gc,4037
|
|
9
|
+
pytest_agent_eval/reporter.py,sha256=BWMZEZ149EhpHq20XV_2VhXW0KgMYZGEoje81Re5cFQ,4787
|
|
10
|
+
pytest_agent_check-0.0.1.dist-info/METADATA,sha256=ogGMscoNWfwLfZXwUE17H2FNzarXCzSuN2eRW0S7kbQ,13522
|
|
11
|
+
pytest_agent_check-0.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
12
|
+
pytest_agent_check-0.0.1.dist-info/entry_points.txt,sha256=LyaPJ3_3SdUTVyQelEuEo8Azg4B1KcnuS27PPXvXYm4,115
|
|
13
|
+
pytest_agent_check-0.0.1.dist-info/top_level.txt,sha256=-pjdrPHR20R5QdJV1Ja-_u-xqogh0Tss5ZFJEin8Ql4,18
|
|
14
|
+
pytest_agent_check-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Xianpeng Shen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest_agent_eval
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pytest-agent-check — A pytest plugin for evaluating and testing AI agents.
|
|
3
|
+
|
|
4
|
+
This plugin provides:
|
|
5
|
+
- ``@agent_test`` decorator for marking agent evaluation tests
|
|
6
|
+
- ``expect_tools`` / ``expect_output`` fluent assertion API
|
|
7
|
+
- Cassette-based record/replay for deterministic, offline testing
|
|
8
|
+
- Structured comparison of tool-call sequences
|
|
9
|
+
- Terminal and HTML diff reporting
|
|
10
|
+
- CLI tool for managing test lifecycles
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .api import agent_test, expect_output, expect_tools
|
|
16
|
+
from .cassette import CassetteContext, CassetteManager
|
|
17
|
+
from .models import Cassette, ComparisonResult, Interaction, ToolCall, ToolCallDiff
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"agent_test",
|
|
21
|
+
"expect_tools",
|
|
22
|
+
"expect_output",
|
|
23
|
+
"ToolCall",
|
|
24
|
+
"Interaction",
|
|
25
|
+
"Cassette",
|
|
26
|
+
"ComparisonResult",
|
|
27
|
+
"ToolCallDiff",
|
|
28
|
+
"CassetteManager",
|
|
29
|
+
"CassetteContext",
|
|
30
|
+
]
|
pytest_agent_eval/api.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public API — the user-facing decorators and assertion helpers.
|
|
3
|
+
|
|
4
|
+
This is what users ``from pytest_agent_eval import`` in their test files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
# ──────────────────────────────────────────────────────────────
|
|
14
|
+
# agent_test decorator
|
|
15
|
+
# ──────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def agent_test(
|
|
19
|
+
*,
|
|
20
|
+
agent: str,
|
|
21
|
+
cassette: str | None = None,
|
|
22
|
+
) -> Callable[[Callable], Callable]:
|
|
23
|
+
"""
|
|
24
|
+
Mark a function as an agent evaluation test.
|
|
25
|
+
|
|
26
|
+
This is a thin wrapper around ``@pytest.mark.agent_test`` that
|
|
27
|
+
provides a cleaner user-facing syntax.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
agent: Logical name of the agent under test.
|
|
31
|
+
cassette: (optional) Explicit cassette name. If omitted,
|
|
32
|
+
the test function name is used.
|
|
33
|
+
|
|
34
|
+
Example::
|
|
35
|
+
|
|
36
|
+
from pytest_agent_eval import agent_test, expect_tools
|
|
37
|
+
|
|
38
|
+
@agent_test(agent="my-support-agent")
|
|
39
|
+
def test_refund_request(agent, cassette):
|
|
40
|
+
result = cassette.run(agent, "I want a refund")
|
|
41
|
+
expect_tools(result).called("lookup_order").then("issue_refund")
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def decorator(func: Callable) -> Callable:
|
|
46
|
+
kwargs: dict[str, Any] = {"agent": agent}
|
|
47
|
+
if cassette is not None:
|
|
48
|
+
kwargs["cassette"] = cassette
|
|
49
|
+
return pytest.mark.agent_test(**kwargs)(func)
|
|
50
|
+
|
|
51
|
+
return decorator
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ──────────────────────────────────────────────────────────────
|
|
55
|
+
# Fluent assertion helpers
|
|
56
|
+
# ──────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ExpectToolsResult:
|
|
60
|
+
"""
|
|
61
|
+
Fluent assertion on a sequence of tool calls.
|
|
62
|
+
|
|
63
|
+
Constructed via :func:`expect_tools` — users should not instantiate
|
|
64
|
+
this directly.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, result: dict[str, Any]) -> None:
|
|
68
|
+
self._tool_calls: list[dict[str, Any]] = result.get("tool_calls", [])
|
|
69
|
+
self._index: int = 0
|
|
70
|
+
|
|
71
|
+
def called(self, tool_name: str) -> ExpectToolsResult:
|
|
72
|
+
"""
|
|
73
|
+
Assert that the **next** tool call has *tool_name*.
|
|
74
|
+
|
|
75
|
+
Returns ``self`` so calls can be chained with :meth:`then`.
|
|
76
|
+
"""
|
|
77
|
+
if self._index >= len(self._tool_calls):
|
|
78
|
+
raise AssertionError(
|
|
79
|
+
f"Expected tool call '{tool_name}' at position "
|
|
80
|
+
f"{self._index + 1}, but only "
|
|
81
|
+
f"{len(self._tool_calls)} tool call(s) were made."
|
|
82
|
+
)
|
|
83
|
+
actual = self._tool_calls[self._index].get("name", "")
|
|
84
|
+
if actual != tool_name:
|
|
85
|
+
raise AssertionError(
|
|
86
|
+
f"Tool call mismatch at position {self._index + 1}:\n"
|
|
87
|
+
f" Expected: '{tool_name}'\n"
|
|
88
|
+
f" Actual: '{actual}'"
|
|
89
|
+
)
|
|
90
|
+
self._index += 1
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def then(self, tool_name: str) -> ExpectToolsResult:
|
|
94
|
+
"""Alias for :meth:`called` — enables fluent chaining."""
|
|
95
|
+
return self.called(tool_name)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def total(self) -> int:
|
|
99
|
+
"""Return the total number of tool calls made."""
|
|
100
|
+
return len(self._tool_calls)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def remaining(self) -> int:
|
|
104
|
+
"""Return how many tool calls have not been asserted yet."""
|
|
105
|
+
return len(self._tool_calls) - self._index
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ExpectOutputResult:
|
|
109
|
+
"""
|
|
110
|
+
Fluent assertion on the natural-language output of an agent.
|
|
111
|
+
|
|
112
|
+
Constructed via :func:`expect_output`.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, result: dict[str, Any]) -> None:
|
|
116
|
+
self._output: str = result.get("output", "")
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def text(self) -> str:
|
|
120
|
+
"""The raw output text."""
|
|
121
|
+
return self._output
|
|
122
|
+
|
|
123
|
+
def matches_intent(self, description: str) -> ExpectOutputResult:
|
|
124
|
+
"""
|
|
125
|
+
Assert the output is non-empty and plausibly matches an intent.
|
|
126
|
+
|
|
127
|
+
In the MVP this is a basic non-emptiness check. Future versions
|
|
128
|
+
will use embedding similarity or an LLM-as-judge for semantic
|
|
129
|
+
equivalence.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
description: Human-readable description of the expected intent.
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
if not self._output:
|
|
136
|
+
raise AssertionError(
|
|
137
|
+
"Expected non-empty agent output, but got an empty string."
|
|
138
|
+
)
|
|
139
|
+
# For MVP we accept any non-empty output as "passing".
|
|
140
|
+
# The description parameter documents intent for future semantic checks.
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def semantic_similarity(
|
|
144
|
+
self,
|
|
145
|
+
threshold: float = 0.85,
|
|
146
|
+
reference: str | None = None,
|
|
147
|
+
) -> ExpectOutputResult:
|
|
148
|
+
"""
|
|
149
|
+
Assert that the output exceeds a semantic similarity threshold.
|
|
150
|
+
|
|
151
|
+
Requires ``sentence-transformers`` (install with
|
|
152
|
+
``pip install pytest-agent-check[semantic]``).
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
threshold: Minimum cosine similarity (0-1).
|
|
156
|
+
reference: Reference text to compare against. If ``None``,
|
|
157
|
+
the cassette's recorded output is used.
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
# Stub — full implementation requires the optional dependency.
|
|
161
|
+
# For now we just accept any non-empty output.
|
|
162
|
+
if not self._output:
|
|
163
|
+
raise AssertionError(
|
|
164
|
+
"Expected non-empty output for semantic similarity check."
|
|
165
|
+
)
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def expect_tools(result: dict[str, Any]) -> ExpectToolsResult:
|
|
170
|
+
"""
|
|
171
|
+
Start fluent assertion on the tool-call sequence of *result*.
|
|
172
|
+
|
|
173
|
+
Usage::
|
|
174
|
+
|
|
175
|
+
expect_tools(result).called("lookup_order").then("issue_refund")
|
|
176
|
+
"""
|
|
177
|
+
return ExpectToolsResult(result)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def expect_output(result: dict[str, Any]) -> ExpectOutputResult:
|
|
181
|
+
"""
|
|
182
|
+
Start fluent assertion on the natural-language output of *result*.
|
|
183
|
+
|
|
184
|
+
Usage::
|
|
185
|
+
|
|
186
|
+
expect_output(result).matches_intent("confirms refund and arrival time")
|
|
187
|
+
"""
|
|
188
|
+
return ExpectOutputResult(result)
|