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/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
+ [![CI](https://github.com/vnageshwaran-de/agentprdiff/actions/workflows/ci.yml/badge.svg)](https://github.com/vnageshwaran-de/agentprdiff/actions/workflows/ci.yml)
49
+ [![PyPI](https://img.shields.io/pypi/v/agentprdiff.svg)](https://pypi.org/project/agentprdiff/)
50
+ [![Python](https://img.shields.io/pypi/pyversions/agentprdiff.svg)](https://pypi.org/project/agentprdiff/)
51
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agentprdiff = agentprdiff.cli:main
@@ -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.