agentprdiff 0.2.2__tar.gz → 0.2.3__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 (25) hide show
  1. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/CHANGELOG.md +37 -0
  2. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/PKG-INFO +1 -1
  3. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/pyproject.toml +1 -1
  4. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/graders/semantic.py +53 -0
  5. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/reporters.py +23 -0
  6. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/scaffold.py +164 -13
  7. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/.gitignore +0 -0
  8. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/LICENSE +0 -0
  9. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/README.md +0 -0
  10. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/examples/quickstart/README.md +0 -0
  11. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/examples/regression-tour/README.md +0 -0
  12. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/__init__.py +0 -0
  13. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/adapters/__init__.py +0 -0
  14. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/adapters/anthropic.py +0 -0
  15. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/adapters/openai.py +0 -0
  16. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/adapters/pricing.py +0 -0
  17. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/cli.py +0 -0
  18. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/core.py +0 -0
  19. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/differ.py +0 -0
  20. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/filtering.py +0 -0
  21. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/graders/__init__.py +0 -0
  22. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/graders/deterministic.py +0 -0
  23. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/loader.py +0 -0
  24. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/runner.py +0 -0
  25. {agentprdiff-0.2.2 → agentprdiff-0.2.3}/src/agentprdiff/store.py +0 -0
@@ -8,6 +8,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.2.3] — 2026-04-28
12
+
13
+ ### Added
14
+
15
+ - **Semantic-judge banner in `check` and `review` output.** `TerminalReporter`
16
+ and `ReviewReporter` now print one line — `semantic judge: <description>`
17
+ — directly under the header whenever the suite contains at least one
18
+ `semantic(...)` grader. The description names the active backend
19
+ (`fake_judge`, `openai/<model>`, or `anthropic/<model>`) and the env-var
20
+ signal that selected it, with explicit `silent fallback` wording when
21
+ no judge is configured. Closes the most common adoption trap: shipping
22
+ suites whose semantic coverage is decorative because no key was set
23
+ and the runner stayed quiet about it. Suites without `semantic(...)`
24
+ are unaffected — no banner is printed. New helpers
25
+ `agentprdiff.graders.semantic.describe_default_judge()` and
26
+ `case_uses_semantic()` power the rendering and are reusable by
27
+ third-party tooling.
28
+ - **Scaffolded workflow YAML now flags judge-SDK installs explicitly.**
29
+ `_TPL_WORKFLOW` ships commented `pip install anthropic` /
30
+ `pip install openai` lines tied to the chosen `AGENTGUARD_JUDGE` mode,
31
+ with guidance that a missing SDK raises `ImportError` rather than
32
+ falling back silently. Pairs with the in-band judge banner for
33
+ end-to-end coverage of the silent-fake_judge trap.
34
+
35
+ ### Fixed
36
+
37
+ - Adoption checklist in `AGENTS.md` now requires the adopter to verify
38
+ the installed CLI supports the documented commands (`agentprdiff
39
+ check --help`) before writing run commands into the case dossier —
40
+ prevents the "source docs reference `--case` but the pinned wheel
41
+ predates it" confusion reported during 0.2.x adoption.
42
+ - New `Step 5b — decide and document the semantic-judge mode` mandates
43
+ a `## Semantic Judge Keys` section in `suites/README.md` and an
44
+ explicit `AGENTGUARD_JUDGE=<mode>` line in the workflow YAML, replacing
45
+ the implicit "first available key wins" precedence with a deliberate
46
+ declaration.
47
+
11
48
  ## [0.2.2] — 2026-04-28
12
49
 
13
50
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprdiff
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Guard your LLM agents in CI. Snapshot tests that catch behavioral regressions when models, prompts, or vendors change.
5
5
  Project-URL: Homepage, https://github.com/vnageshwaran-de/agentprdiff
6
6
  Project-URL: Documentation, https://github.com/vnageshwaran-de/agentprdiff#readme
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentprdiff"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Guard your LLM agents in CI. Snapshot tests that catch behavioral regressions when models, prompts, or vendors change."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -178,3 +178,56 @@ def _default_judge() -> Judge:
178
178
  if choice == "anthropic" or (not choice and os.environ.get("ANTHROPIC_API_KEY")):
179
179
  return anthropic_judge()
180
180
  return fake_judge
181
+
182
+
183
+ # Default model names mirrored from `openai_judge` and `anthropic_judge`. Kept
184
+ # in module scope so `describe_default_judge` reports the same string the
185
+ # default judge would actually use without instantiating the judge (which
186
+ # would import the SDK lazily).
187
+ _DEFAULT_OPENAI_MODEL = "gpt-4o-mini"
188
+ _DEFAULT_ANTHROPIC_MODEL = "claude-haiku-4-5-20251001"
189
+
190
+
191
+ def describe_default_judge() -> str:
192
+ """Return a one-line description of the currently-selected default judge.
193
+
194
+ Mirrors the env-var precedence in :func:`_default_judge` so the string is
195
+ a faithful preview of what `semantic(...)` graders without an explicit
196
+ `judge=` argument will use at runtime. Reporters print this once per run
197
+ so the silent fake_judge fallback (no key set, no AGENTGUARD_JUDGE) is
198
+ visible at the moment a suite executes — not buried in trace JSON.
199
+
200
+ Examples:
201
+ - ``"fake_judge (AGENTGUARD_JUDGE=fake)"``
202
+ - ``"openai/gpt-4o-mini (OPENAI_API_KEY set)"``
203
+ - ``"anthropic/claude-haiku-4-5-20251001 (AGENTGUARD_JUDGE=anthropic)"``
204
+ - ``"fake_judge (no AGENTGUARD_JUDGE, no OPENAI_API_KEY/ANTHROPIC_API_KEY — silent fallback)"``
205
+ """
206
+ choice = (os.environ.get("AGENTGUARD_JUDGE") or "").lower()
207
+ if choice == "fake":
208
+ return "fake_judge (AGENTGUARD_JUDGE=fake)"
209
+ if choice == "openai":
210
+ return f"openai/{_DEFAULT_OPENAI_MODEL} (AGENTGUARD_JUDGE=openai)"
211
+ if choice == "anthropic":
212
+ return (
213
+ f"anthropic/{_DEFAULT_ANTHROPIC_MODEL} (AGENTGUARD_JUDGE=anthropic)"
214
+ )
215
+ if not choice and os.environ.get("OPENAI_API_KEY"):
216
+ return f"openai/{_DEFAULT_OPENAI_MODEL} (OPENAI_API_KEY set)"
217
+ if not choice and os.environ.get("ANTHROPIC_API_KEY"):
218
+ return f"anthropic/{_DEFAULT_ANTHROPIC_MODEL} (ANTHROPIC_API_KEY set)"
219
+ return (
220
+ "fake_judge (no AGENTGUARD_JUDGE, no OPENAI_API_KEY/ANTHROPIC_API_KEY"
221
+ " — silent fallback)"
222
+ )
223
+
224
+
225
+ def case_uses_semantic(grader_results: list[GradeResult]) -> bool:
226
+ """Return True if any grader in `grader_results` was a `semantic()` grader.
227
+
228
+ Detection is based on the grader name format produced by :func:`semantic`
229
+ (``semantic(<rubric>)``). This is stable because the public grader name is
230
+ part of the user-visible grading contract; reporters and serializers
231
+ already depend on it.
232
+ """
233
+ return any(r.grader_name.startswith("semantic(") for r in grader_results)
@@ -17,6 +17,7 @@ from rich.table import Table
17
17
  from rich.text import Text
18
18
 
19
19
  from .differ import AssertionChange, TraceDelta
20
+ from .graders.semantic import case_uses_semantic, describe_default_judge
20
21
  from .runner import CaseReport, RunReport
21
22
 
22
23
 
@@ -35,6 +36,7 @@ class TerminalReporter:
35
36
  style="dim",
36
37
  )
37
38
  self.console.print(header)
39
+ _maybe_print_judge_banner(self.console, report)
38
40
 
39
41
  table = Table(show_header=True, header_style="bold", show_lines=False, expand=True)
40
42
  table.add_column("Case", style="bold")
@@ -130,6 +132,26 @@ def _format_delta(value: float, fmt: str) -> str:
130
132
  return f"[{color}]{text}[/{color}]"
131
133
 
132
134
 
135
+ def _maybe_print_judge_banner(console: Console, report: RunReport) -> None:
136
+ """Print "semantic judge: <mode>" once per run if any case used semantic().
137
+
138
+ Skipped silently when no case has a semantic grader — most suites don't
139
+ use them and the banner would be noise. When a suite *does* have semantic
140
+ coverage, the banner makes the silent fake_judge fallback (no key, no
141
+ AGENTGUARD_JUDGE) loud at the moment of execution rather than buried in
142
+ trace JSON. Coloured yellow when fake_judge would run so the warning is
143
+ visually distinct from real-judge runs.
144
+ """
145
+ if not any(case_uses_semantic(cr.grader_results) for cr in report.case_reports):
146
+ return
147
+ description = describe_default_judge()
148
+ style = "yellow" if description.startswith("fake_judge") else "dim"
149
+ line = Text()
150
+ line.append("semantic judge: ", style="dim")
151
+ line.append(description, style=style)
152
+ console.print(line)
153
+
154
+
133
155
  # ---------------------------------------------------------------------------
134
156
  # ReviewReporter — verbose per-case view for `agentprdiff review`.
135
157
  # ---------------------------------------------------------------------------
@@ -175,6 +197,7 @@ class ReviewReporter:
175
197
  style="dim",
176
198
  )
177
199
  self.console.print(header)
200
+ _maybe_print_judge_banner(self.console, report)
178
201
  self.console.print()
179
202
 
180
203
  def _render_footer(self, report: RunReport) -> None:
@@ -378,6 +378,36 @@ locally (rather than the keyword-matching fake fallback), also set
378
378
  [AGENTS.md → API keys](https://github.com/vnageshwaran-de/agentprdiff/blob/main/AGENTS.md#api-keys--what-to-set-where-and-how-to-ask-the-user-about-them)
379
379
  for the full picture, including CI secret setup.
380
380
 
381
+ ## Semantic Judge Keys
382
+
383
+ `semantic(...)` graders need an LLM judge to render verdicts. Without
384
+ one, agentprdiff silently falls back to `fake_judge` (keyword matching) —
385
+ the suite reports PASS even when no LLM ever ran. Make the judge mode
386
+ explicit so the absence-or-presence of LLM scoring is never ambiguous in
387
+ CI logs.
388
+
389
+ **This suite's mode:** TODO — fill in one of:
390
+
391
+ - `fake_judge` — free, keyword matching only. Acceptable when the rubric
392
+ reduces cleanly to keywords; brittle otherwise. Set
393
+ `AGENTGUARD_JUDGE=fake`.
394
+ - Real Anthropic judge — recommended for cost.
395
+ `AGENTGUARD_JUDGE=anthropic` plus `ANTHROPIC_API_KEY=...`.
396
+ - Real OpenAI judge — `AGENTGUARD_JUDGE=openai` plus `OPENAI_API_KEY=...`.
397
+ - `Not applicable` — this suite has no `semantic(...)` graders. Verify with
398
+ `grep -n "semantic(" suites/{name}.py`. State this explicitly anyway so
399
+ the next reviewer knows the absence of judge config is deliberate.
400
+
401
+ **Reproduce CI's mode locally:** export the same env vars CI sets and run:
402
+
403
+ ```bash
404
+ agentprdiff check suites/{name}.py --case <a_case_with_semantic>
405
+ ```
406
+
407
+ Confirm the trace shows non-zero `cost_usd` on the judge call (real
408
+ judges) or a flat zero (fake_judge). A real-judge config that traces zero
409
+ cost means the silent fallback bit you — re-check the env vars.
410
+
381
411
  ## Run locally
382
412
 
383
413
  ```bash
@@ -409,23 +439,110 @@ or meaningfully change a case.
409
439
  If a case fails in CI and you don't recognize the name, this file is the
410
440
  fastest path back to *what the case is pinning and why it matters*.
411
441
 
412
- ## How to run
442
+ ## Running the suite
443
+
444
+ Run every case from the repository root:
413
445
 
414
446
  ```bash
415
- agentprdiff check suites/{name}.py # full suite
416
- agentprdiff check suites/{name}.py --case happy_path # one case (substring or glob)
417
- agentprdiff check suites/{name}.py --list # discover case names
418
- agentprdiff check suites/{name}.py --skip slow # everything except a pattern
447
+ agentprdiff check suites/{name}.py
419
448
  ```
420
449
 
421
- After an *intentional* behavior change, re-record the baseline and explain
422
- the change in the PR description so reviewers see the before/after diff:
450
+ If the installed wheel in your virtualenv predates the `--case`, `--list`,
451
+ and `review` commands, use a local source checkout of agentprdiff next to
452
+ this repository. Substitute `.venv-agentprdiff/` with whatever virtualenv
453
+ your project actually uses:
423
454
 
424
455
  ```bash
425
- agentprdiff record suites/{name}.py
456
+ .venv-agentprdiff/bin/agentprdiff check suites/{name}.py
457
+ ```
458
+
459
+ ### List the available cases
460
+
461
+ ```bash
462
+ agentprdiff check suites/{name}.py --list
463
+ # or, with a local source checkout:
464
+ PYTHONPATH=../agentprdiff/src .venv-agentprdiff/bin/python -m agentprdiff.cli \\
465
+ check suites/{name}.py --list
466
+ ```
467
+
468
+ ### Run one case
469
+
470
+ Filter by substring or glob (case-insensitive). A filter that matches
471
+ nothing exits 2 — no silent zero-runs:
472
+
473
+ ```bash
474
+ agentprdiff check suites/{name}.py --case happy_path
475
+ agentprdiff check suites/{name}.py --case "*refund*"
476
+ agentprdiff check suites/{name}.py --skip slow # everything except a pattern
477
+ ```
478
+
479
+ ### Use `review` for verbose, exit-0 iteration
480
+
481
+ `review` runs the same comparison `check` does but renders one verbose
482
+ panel per case (input echo, every assertion's `was → now` verdict, cost /
483
+ latency / token deltas, tool-sequence diff, unified output diff) and
484
+ exits 0 even on regression — safe to wire into a watcher / `entr` / `fzf`
485
+ loop without your shell going red on every iteration:
486
+
487
+ ```bash
488
+ agentprdiff review suites/{name}.py --case happy_path
489
+ ```
490
+
491
+ Reach for `check` when you want the CI gate's exit semantics; reach for
492
+ `review` while you're working.
493
+
494
+ ## Seeing a regression locally
495
+
496
+ These are safe local experiments to confirm the suite would catch a real
497
+ behavior drift. Revert the edit after observing the failure — none of
498
+ these changes should be committed.
499
+
500
+ 1. **Change the system prompt** in your production agent module to a
501
+ different sentence, then run:
502
+
503
+ ```bash
504
+ agentprdiff check suites/{name}.py --case happy_path
505
+ ```
506
+
507
+ Expected result: the prompt-text grader (e.g. `system_prompt_is(...)`)
508
+ fails because the recorded LLM call no longer matches the baseline
509
+ contract.
510
+
511
+ 2. **Change a stub fixture** in `suites/_stubs.py` so it returns different
512
+ values from the case expects, then run:
513
+
514
+ ```bash
515
+ agentprdiff check suites/{name}.py --case happy_path
516
+ ```
517
+
518
+ Expected result: the entity / value graders fail and the trace diff
519
+ shows the output text changed.
520
+
521
+ 3. **Change an exception handler** in production code (e.g. so a failure
522
+ path returns a string where callers expect `None`), then run:
523
+
524
+ ```bash
525
+ agentprdiff check suites/{name}.py --case happy_path
526
+ ```
527
+
528
+ Expected result: the contract grader pinning the failure-mode return
529
+ value fails, proving callers would no longer receive the fallback
530
+ signal they depend on.
531
+
532
+ ## Updating the baseline after an intentional change
533
+
534
+ When a behavior change is intentional, re-record the baseline for only
535
+ the affected case and commit the resulting JSON diff so reviewers see the
536
+ before/after:
537
+
538
+ ```bash
539
+ agentprdiff record suites/{name}.py --case happy_path
426
540
  git add .agentprdiff/baselines/
427
541
  ```
428
542
 
543
+ Drop `--case` to re-record every case at once. Either form is safe to
544
+ re-run — `record` overwrites baselines in place, no accumulation.
545
+
429
546
  ## Cases
430
547
 
431
548
  ### `happy_path`
@@ -453,11 +570,19 @@ affected lines without re-reading the whole agent.
453
570
  One sentence; concrete and specific. ("Refunds silently fail" not "the
454
571
  agent misbehaves.")
455
572
 
573
+ **How to exercise this case in isolation.**
574
+
575
+ ```bash
576
+ agentprdiff check suites/{name}.py --case happy_path # CI-style, exit 1 on regression
577
+ agentprdiff review suites/{name}.py --case happy_path # verbose, exit 0
578
+ agentprdiff record suites/{name}.py --case happy_path # re-record after intentional change
579
+ ```
580
+
456
581
  ---
457
582
 
458
583
  <!--
459
584
  Template for additional cases. Copy-paste this block, change the heading,
460
- fill in the four sections.
585
+ fill in the five sections plus the run-commands block.
461
586
 
462
587
  ### `<case_name>`
463
588
 
@@ -475,6 +600,14 @@ fill in the four sections.
475
600
 
476
601
  **Application impact.**
477
602
 
603
+ **How to exercise this case in isolation.**
604
+
605
+ ```bash
606
+ agentprdiff check suites/{name}.py --case <case_name>
607
+ agentprdiff review suites/{name}.py --case <case_name>
608
+ agentprdiff record suites/{name}.py --case <case_name>
609
+ ```
610
+
478
611
  -->
479
612
  '''
480
613
 
@@ -505,16 +638,34 @@ jobs:
505
638
  - run: |
506
639
  python -m pip install --upgrade pip
507
640
  pip install -r requirements.txt agentprdiff
641
+ # Semantic-judge SDKs. The agentprdiff wheel imports these lazily,
642
+ # so installing only the one matching AGENTGUARD_JUDGE below keeps
643
+ # the CI environment lean. If your suite has no semantic() graders
644
+ # (see suites/README.md "Semantic Judge Keys"), drop both lines.
645
+ # Uncomment exactly one to match the chosen judge mode:
646
+ # pip install anthropic # if AGENTGUARD_JUDGE=anthropic
647
+ # pip install openai # if AGENTGUARD_JUDGE=openai (already installed if your agent uses OpenAI)
648
+ # Without the matching SDK, semantic() raises ImportError at first
649
+ # use and the case fails — louder than the silent fake_judge trap.
508
650
  - env:
509
651
  # TODO: match the env var your production agent reads.
510
652
  # Common names: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY,
511
653
  # or a project-specific name. Add the secret in:
512
654
  # Settings -> Secrets and variables -> Actions -> New repository secret.
513
655
  OPENAI_API_KEY: ${{{{ secrets.OPENAI_API_KEY }}}}
514
- # Optional: real semantic judge in CI. Without this, semantic()
515
- # graders fall back to fake_judge (keyword matching) see
516
- # AGENTS.md "API keys" section. Omit this line entirely to use
517
- # fake_judge and keep CI free.
656
+ # Semantic-judge mode (see AGENTS.md "Step 5b decide and document
657
+ # the semantic-judge mode"). Pick one and uncomment; do NOT rely on
658
+ # the implicit selection order it makes "which provider did I
659
+ # get?" ambiguous in CI logs. If your suite has no semantic()
660
+ # graders, leave all three commented and CI runs free.
661
+ #
662
+ # AGENTGUARD_JUDGE: fake # keyword matching, free
663
+ # AGENTGUARD_JUDGE: anthropic # real judge, cheaper; pair with ANTHROPIC_API_KEY below
664
+ # AGENTGUARD_JUDGE: openai # real judge; pair with OPENAI_API_KEY above
665
+ #
666
+ # Pair the chosen judge with its key. Without a key, semantic()
667
+ # falls back to fake_judge silently and you'll see PASS without
668
+ # the LLM judge ever running.
518
669
  ANTHROPIC_API_KEY: ${{{{ secrets.ANTHROPIC_API_KEY }}}}
519
670
  run: |
520
671
  if [ -z "${{OPENAI_API_KEY}}" ]; then
File without changes
File without changes
File without changes