agentprdiff 0.1.0__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.
- agentprdiff/__init__.py +81 -0
- agentprdiff/cli.py +124 -0
- agentprdiff/core.py +217 -0
- agentprdiff/differ.py +161 -0
- agentprdiff/graders/__init__.py +38 -0
- agentprdiff/graders/deterministic.py +186 -0
- agentprdiff/graders/semantic.py +180 -0
- agentprdiff/loader.py +47 -0
- agentprdiff/reporters.py +127 -0
- agentprdiff/runner.py +130 -0
- agentprdiff/store.py +80 -0
- agentprdiff-0.1.0.dist-info/METADATA +200 -0
- agentprdiff-0.1.0.dist-info/RECORD +16 -0
- agentprdiff-0.1.0.dist-info/WHEEL +4 -0
- agentprdiff-0.1.0.dist-info/entry_points.txt +2 -0
- agentprdiff-0.1.0.dist-info/licenses/LICENSE +21 -0
agentprdiff/store.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Baseline storage.
|
|
2
|
+
|
|
3
|
+
Baselines live in `.agentprdiff/baselines/<suite>/<case>.json` relative to the
|
|
4
|
+
project root. They are designed to be checked into git — reviewers should be
|
|
5
|
+
able to see them in pull requests and argue about changes.
|
|
6
|
+
|
|
7
|
+
Runs (every execution of `agentprdiff check`) are written under
|
|
8
|
+
`.agentprdiff/runs/` and are *not* checked in.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .core import Trace
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaselineStore:
|
|
21
|
+
"""Filesystem-backed store for baseline traces."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, root: Path | str = ".agentprdiff") -> None:
|
|
24
|
+
self.root = Path(root)
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------------ paths
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def baselines_dir(self) -> Path:
|
|
30
|
+
return self.root / "baselines"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def runs_dir(self) -> Path:
|
|
34
|
+
return self.root / "runs"
|
|
35
|
+
|
|
36
|
+
def baseline_path(self, suite_name: str, case_name: str) -> Path:
|
|
37
|
+
return self.baselines_dir / _safe(suite_name) / f"{_safe(case_name)}.json"
|
|
38
|
+
|
|
39
|
+
def run_path(self, run_id: str, suite_name: str, case_name: str) -> Path:
|
|
40
|
+
return self.runs_dir / run_id / _safe(suite_name) / f"{_safe(case_name)}.json"
|
|
41
|
+
|
|
42
|
+
# ------------------------------------------------------------------ io
|
|
43
|
+
|
|
44
|
+
def save_baseline(self, trace: Trace) -> Path:
|
|
45
|
+
path = self.baseline_path(trace.suite_name, trace.case_name)
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
path.write_text(_dump_trace(trace), encoding="utf-8")
|
|
48
|
+
return path
|
|
49
|
+
|
|
50
|
+
def load_baseline(self, suite_name: str, case_name: str) -> Trace | None:
|
|
51
|
+
path = self.baseline_path(suite_name, case_name)
|
|
52
|
+
if not path.exists():
|
|
53
|
+
return None
|
|
54
|
+
return Trace.model_validate_json(path.read_text(encoding="utf-8"))
|
|
55
|
+
|
|
56
|
+
def save_run_trace(self, run_id: str, trace: Trace) -> Path:
|
|
57
|
+
path = self.run_path(run_id, trace.suite_name, trace.case_name)
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
path.write_text(_dump_trace(trace), encoding="utf-8")
|
|
60
|
+
return path
|
|
61
|
+
|
|
62
|
+
def ensure_initialized(self) -> None:
|
|
63
|
+
self.baselines_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
self.runs_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
gitignore = self.root / ".gitignore"
|
|
66
|
+
if not gitignore.exists():
|
|
67
|
+
gitignore.write_text("# Committed: baselines/\n# Not committed: runs/\nruns/\n")
|
|
68
|
+
|
|
69
|
+
def fresh_run_id(self) -> str:
|
|
70
|
+
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _safe(name: str) -> str:
|
|
74
|
+
"""Make a case/suite name safe for use as a filename component."""
|
|
75
|
+
return "".join(c if c.isalnum() or c in "-_." else "_" for c in name) or "_"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _dump_trace(trace: Trace) -> str:
|
|
79
|
+
# pretty-printed JSON so git diffs are readable.
|
|
80
|
+
return json.dumps(trace.model_dump(mode="json"), indent=2, sort_keys=False) + "\n"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentprdiff
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Guard your LLM agents in CI. Snapshot tests that catch behavioral regressions when models, prompts, or vendors change.
|
|
5
|
+
Project-URL: Homepage, https://github.com/vnageshwaran-de/agentprdiff
|
|
6
|
+
Project-URL: Documentation, https://github.com/vnageshwaran-de/agentprdiff#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/vnageshwaran-de/agentprdiff/issues
|
|
8
|
+
Project-URL: Repository, https://github.com/vnageshwaran-de/agentprdiff
|
|
9
|
+
Author-email: Vinoth Nageshwaran <vnageshwaran@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,ci,evaluation,llm,observability,regression-testing,snapshot-testing
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: click>=8.1
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: rich>=13.0
|
|
27
|
+
Provides-Extra: anthropic
|
|
28
|
+
Requires-Dist: anthropic>=0.30; extra == 'anthropic'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
34
|
+
Provides-Extra: openai
|
|
35
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# agentprdiff
|
|
39
|
+
|
|
40
|
+
**Guard your LLM agents in CI.** Snapshot tests that catch behavioral regressions when models, prompts, or vendors change.
|
|
41
|
+
|
|
42
|
+
> You upgraded Claude. You tweaked a system prompt. You swapped `gpt-4o` for `gpt-4o-mini` in the cheap path. Which of your agent's behaviors just changed? `agentprdiff` tells you — before the PR merges.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install agentprdiff
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
[](https://github.com/vnageshwaran-de/agentprdiff/actions/workflows/ci.yml)
|
|
49
|
+
[](https://pypi.org/project/agentprdiff/)
|
|
50
|
+
[](https://pypi.org/project/agentprdiff/)
|
|
51
|
+
[](./LICENSE)
|
|
52
|
+
|
|
53
|
+
## Why
|
|
54
|
+
|
|
55
|
+
Unit tests assume determinism. Agents aren't deterministic, but they do have *behaviors you rely on* — a specific tool gets called, a refund amount is quoted, a latency budget is respected, a safety guardrail fires. When a model or prompt changes, those behaviors drift. Today most teams find out in production.
|
|
56
|
+
|
|
57
|
+
`agentprdiff` turns those behaviors into versioned, diffable baselines you check into git, and a CI command that fails the build when they regress.
|
|
58
|
+
|
|
59
|
+
It is **not** a framework. Your agent stays exactly the way it is. `agentprdiff` records what it did, lets you assert what should be true about what it did, and compares runs across time.
|
|
60
|
+
|
|
61
|
+
## 10-line hello world
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# suite.py
|
|
65
|
+
from agentprdiff import case, suite
|
|
66
|
+
from agentprdiff.graders import contains, tool_called, latency_lt_ms, semantic
|
|
67
|
+
from my_agent import run # your agent — unchanged
|
|
68
|
+
|
|
69
|
+
support = suite(
|
|
70
|
+
name="customer_support",
|
|
71
|
+
agent=run,
|
|
72
|
+
cases=[
|
|
73
|
+
case(
|
|
74
|
+
name="refund_happy_path",
|
|
75
|
+
input="I want a refund for order #1234",
|
|
76
|
+
expect=[
|
|
77
|
+
contains("refund"),
|
|
78
|
+
tool_called("lookup_order"),
|
|
79
|
+
semantic("agent acknowledges the refund and explains the timeline"),
|
|
80
|
+
latency_lt_ms(10_000),
|
|
81
|
+
],
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
agentprdiff init
|
|
89
|
+
agentprdiff record suite.py # save this run as the baseline
|
|
90
|
+
agentprdiff check suite.py # in CI: diff vs baseline, exit 1 on regression
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
That's the whole product. Four CLI commands. One Python file. Zero framework lock-in.
|
|
94
|
+
|
|
95
|
+
## What's in the box
|
|
96
|
+
|
|
97
|
+
- **Case + Suite model** — tiny, opinionated, no magic.
|
|
98
|
+
- **10 batteries-included graders** — `contains`, `contains_any`, `regex_match`, `tool_called`, `tool_sequence`, `no_tool_called`, `output_length_lt`, `latency_lt_ms`, `cost_lt_usd`, `semantic` (LLM-as-judge with pluggable backend).
|
|
99
|
+
- **Baseline store** — JSON files under `.agentprdiff/baselines/`, meant to be **committed**. Reviewers see trace changes in pull requests.
|
|
100
|
+
- **Diff engine** — per-case `TraceDelta` with assertion pass/fail changes, cost delta, latency delta, tool-sequence changes, and a unified output diff.
|
|
101
|
+
- **CI-ready CLI** — exit 1 on regression, `--json-out` for artifact archiving, Rich-formatted terminal output.
|
|
102
|
+
- **Zero SDK lock-in** — works with OpenAI, Anthropic, Gemini, Bedrock, LangChain, LangGraph, LlamaIndex, Vercel AI SDK, custom wrappers — if you can wrap your agent in a function, `agentprdiff` can test it.
|
|
103
|
+
|
|
104
|
+
## How it compares
|
|
105
|
+
|
|
106
|
+
| | Unit tests | LLM-as-judge eval | `agentprdiff` |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| Deterministic pass/fail | yes | no | **yes** (when assertions are deterministic) |
|
|
109
|
+
| Catches behavioral drift | no | yes | **yes** |
|
|
110
|
+
| Runs in CI on every PR | yes | too expensive | **yes** |
|
|
111
|
+
| Human-readable diff of what changed | n/a | rare | **yes** |
|
|
112
|
+
| Works without API keys | yes | no | **yes** (deterministic graders + fake judge) |
|
|
113
|
+
|
|
114
|
+
The value is in the combination: deterministic assertions for the 80% of behaviors you can encode as rules ("this tool was called", "this word appeared", "cost stayed under $0.02"), plus a semantic grader for the 20% that need a judge — with a fake-judge fallback so your CI stays green and free when API keys aren't available.
|
|
115
|
+
|
|
116
|
+
## The workflow
|
|
117
|
+
|
|
118
|
+
1. Write a `Suite` alongside your agent code.
|
|
119
|
+
2. Run `agentprdiff record` once on a known-good version. Commit the resulting `.agentprdiff/baselines/` directory.
|
|
120
|
+
3. In CI, on every PR, run `agentprdiff check`. If any assertion regresses, or cost/latency budgets are breached, the job fails.
|
|
121
|
+
4. When behavior intentionally changes, the PR author re-runs `agentprdiff record`, commits the new baseline, and explains the change in the PR description. Reviewers see the before/after in the diff.
|
|
122
|
+
|
|
123
|
+
This is the same loop as Jest snapshot tests or VCR cassettes — applied to LLM agents.
|
|
124
|
+
|
|
125
|
+
## Instrumenting your agent
|
|
126
|
+
|
|
127
|
+
`agentprdiff` doesn't monkey-patch anything. Your agent returns `(output, Trace)`:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from agentprdiff import Trace, LLMCall, ToolCall
|
|
131
|
+
|
|
132
|
+
def my_agent(query: str) -> tuple[str, Trace]:
|
|
133
|
+
trace = Trace(suite_name="", case_name="", input=query)
|
|
134
|
+
|
|
135
|
+
# ... call your model, record what happened ...
|
|
136
|
+
trace.record_llm_call(LLMCall(
|
|
137
|
+
provider="anthropic",
|
|
138
|
+
model="claude-sonnet-4-6",
|
|
139
|
+
prompt_tokens=120, completion_tokens=80,
|
|
140
|
+
cost_usd=0.0012, latency_ms=340,
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
# ... call a tool, record what happened ...
|
|
144
|
+
trace.record_tool_call(ToolCall(name="lookup_order", arguments={"id": "1234"}))
|
|
145
|
+
|
|
146
|
+
return final_output, trace
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Agents that return just an output still work — `agentprdiff` wraps them and captures wall-clock latency. You can backfill richer instrumentation incrementally, assertion by assertion.
|
|
150
|
+
|
|
151
|
+
## CI integration
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
# .github/workflows/agents.yml
|
|
155
|
+
name: agent-regression
|
|
156
|
+
on: [pull_request]
|
|
157
|
+
jobs:
|
|
158
|
+
agentprdiff:
|
|
159
|
+
runs-on: ubuntu-latest
|
|
160
|
+
steps:
|
|
161
|
+
- uses: actions/checkout@v4
|
|
162
|
+
- uses: actions/setup-python@v5
|
|
163
|
+
with: { python-version: "3.11" }
|
|
164
|
+
- run: pip install -e ".[dev]"
|
|
165
|
+
- run: agentprdiff check suites/*.py --json-out artifacts/agentprdiff.json
|
|
166
|
+
- uses: actions/upload-artifact@v4
|
|
167
|
+
if: always()
|
|
168
|
+
with: { name: agentprdiff, path: artifacts/ }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
See [`docs/ci-integration.md`](./docs/ci-integration.md) for GitLab, CircleCI, and Buildkite.
|
|
172
|
+
|
|
173
|
+
## Quickstart
|
|
174
|
+
|
|
175
|
+
A runnable end-to-end demo, no API keys needed:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
git clone https://github.com/vnageshwaran-de/agentprdiff
|
|
179
|
+
cd agentprdiff
|
|
180
|
+
pip install -e ".[dev]"
|
|
181
|
+
|
|
182
|
+
cd examples/quickstart
|
|
183
|
+
agentprdiff init
|
|
184
|
+
agentprdiff record suite.py
|
|
185
|
+
agentprdiff check suite.py # exit 0
|
|
186
|
+
|
|
187
|
+
# now break the agent and watch agentprdiff catch it
|
|
188
|
+
sed -i "s/refund/noundr/g" agent.py
|
|
189
|
+
agentprdiff check suite.py # exit 1; see the diff
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Status
|
|
193
|
+
|
|
194
|
+
`agentprdiff` is **alpha** (0.1.0). The core model and CLI are stable; provider-specific SDK wrappers and a LangChain/LangGraph integration are on the 0.2 roadmap. See [`CHANGELOG.md`](./CHANGELOG.md).
|
|
195
|
+
|
|
196
|
+
Feedback, bug reports, and PRs extremely welcome. Open an issue or @ me.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT. See [`LICENSE`](./LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
agentprdiff/__init__.py,sha256=34NHm7JajVi0YyzTjaNpbDuni0rsyQ3R_Naz0O2GIVU,1720
|
|
2
|
+
agentprdiff/cli.py,sha256=dkkbJiLhil8f7Qe6RvU_MPMd-Gi3h9Ga__kzjlKGfnA,4101
|
|
3
|
+
agentprdiff/core.py,sha256=2LcWti8XX8s7WVjDoYfeUmZHzhUu7AHp9JTemaPVPao,7018
|
|
4
|
+
agentprdiff/differ.py,sha256=kgzXbJy7H34I3a5glGPI807sWWWO9zE_AwZEXWx9nHs,5101
|
|
5
|
+
agentprdiff/loader.py,sha256=SwjYMDoTXAL0qjSQFCS9F-HP5vk4hhM_E0t5hqNb2mA,1600
|
|
6
|
+
agentprdiff/reporters.py,sha256=3dCY5Dwk8uFOUSIa7nqOLXFKfgdyW8wslJyifHJGYus,4725
|
|
7
|
+
agentprdiff/runner.py,sha256=6Brn-pwEPd3YjdxQWh0mEQdsJQIx3voDwpUWoYPKH6Q,4263
|
|
8
|
+
agentprdiff/store.py,sha256=xpjItWLBUAOFCiSs1nKvGc2WFFdvivsvb1lELqnUmkk,2923
|
|
9
|
+
agentprdiff/graders/__init__.py,sha256=ZOzt1LYzs7OWqQq6ihIKsfcNsym3I7vxUFeBVLlZutg,885
|
|
10
|
+
agentprdiff/graders/deterministic.py,sha256=s68gkI0oF_VjwOf_sj_GsI1G1Sn33w9MKeimjKwHR4c,5898
|
|
11
|
+
agentprdiff/graders/semantic.py,sha256=iXsFxIQmZA2QHKC8QVrnoXr0NE7CeZxyaGhkXa8Ctog,6385
|
|
12
|
+
agentprdiff-0.1.0.dist-info/METADATA,sha256=mICljoPWmqeFpNidPO9X9VKtymqLsyL-Nyz2d4C5dL0,8602
|
|
13
|
+
agentprdiff-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
14
|
+
agentprdiff-0.1.0.dist-info/entry_points.txt,sha256=mx0R_SosATqHzQASbuZWmWwf3ROmco5wm8kfk7PI6SA,53
|
|
15
|
+
agentprdiff-0.1.0.dist-info/licenses/LICENSE,sha256=UXQ7F5LfH7qhdqxz8U8YPGFl7dUT2A4_SWc9JFMW_yI,1075
|
|
16
|
+
agentprdiff-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vinoth Nageshwaran
|
|
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.
|