agentlahon 0.0.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agentcheck contributors
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,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentlahon
3
+ Version: 0.0.1
4
+ Summary: Unit tests for AI agents — catch when your agent does the wrong thing, not just when it says the wrong thing.
5
+ Author-email: Anurag Lahon <anuraglahondp@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/anuraglahon16/agentcheck
8
+ Project-URL: Repository, https://github.com/anuraglahon16/agentcheck
9
+ Project-URL: Issues, https://github.com/anuraglahon16/agentcheck/issues
10
+ Keywords: ai,agents,evaluation,evals,llm,testing,governance
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Software Development :: Testing
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7; extra == "dev"
27
+ Provides-Extra: judge
28
+ Requires-Dist: openai>=1; extra == "judge"
29
+ Provides-Extra: webapp
30
+ Requires-Dist: flask>=3; extra == "webapp"
31
+ Dynamic: license-file
32
+
33
+ # agentlahon
34
+
35
+ ### Unit tests for AI agents — catch when your agent *does* the wrong thing, not just when it *says* the wrong thing.
36
+
37
+ ![tests](https://img.shields.io/badge/tests-passing-2fbf71) ![python](https://img.shields.io/badge/python-3.9%2B-blue) ![license](https://img.shields.io/badge/license-MIT-black) ![deps](https://img.shields.io/badge/core%20deps-zero-black)
38
+
39
+ > **Status:** v0.0.1, experimental — APIs may change. Core is zero-dependency. Feedback and first users very welcome.
40
+
41
+ Text-only evals grade what an agent *says*. But agents take *actions* — call tools, write to databases, move money. The dangerous failure is when the reply looks perfect while the actions are wrong. **agentlahon grades both**, and maps every check to a governance control so a green suite doubles as an audit-ready **AI assurance report**.
42
+
43
+ ```
44
+ FAIL decline-only-noncarried
45
+ ✓ reply mentions 'unable' found ← SAID the right thing
46
+ ✓ never records a sale no such action ✓
47
+ ✗ never restocks (money moves) took 1×: reorder Invitation-cards ×9000 ← DID the wrong thing
48
+ ```
49
+
50
+ > That failure is invisible to every text-based eval. Only checking the agent's **actions** catches it. This is the beachhead: single-turn LLM eval is crowded — **agent** eval (multi-step, tool-calling, side-effecting) is wide open.
51
+
52
+ ```python
53
+ Scenario("decline-only", "5000 flyers, 10000 tickets", checks=[
54
+ expect.output_contains("unable"), # say-level
55
+ expect.no_action("reorder_item", # do-level — the part text evals miss
56
+ where=lambda a: a.result.get("ordered")), # assert on the *effect*, not just the call
57
+ ])
58
+ ```
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ pip install agentlahon # PyPI distribution name
64
+ pip install -e . # or from source
65
+ ```
66
+
67
+ Core is dependency-free. Python ≥ 3.9.
68
+
69
+ ## Quickstart
70
+
71
+ ```python
72
+ from agentlahon import Scenario, expect, evaluate, print_terminal, write_html
73
+
74
+ def agent(text): # wrap YOUR agent -> AgentRun(output, actions)
75
+ ...
76
+
77
+ scenarios = [
78
+ Scenario("decline-noncarried", "5000 flyers, 10000 tickets", checks=[
79
+ expect.output_contains("unable"),
80
+ expect.no_action("reorder_item"), # the do-level check text evals miss
81
+ expect.no_pii(),
82
+ ]),
83
+ ]
84
+ report = evaluate(agent, scenarios)
85
+ print_terminal(report)
86
+ write_html(report) # shareable assurance_report.html
87
+ ```
88
+
89
+ ## The evals flywheel — look at data → tag → generate assertions
90
+
91
+ The hard part of evals isn't running assertions, it's *knowing what to assert*. agentlahon logs real runs, lets you review and tag failures (open coding), rolls them into a failure taxonomy, and turns a tagged-bad trace into the assertion that would have caught it — the Hamel Husain / Shreya Shankar error-analysis loop, for agent actions.
92
+
93
+ ```bash
94
+ agentlahon run suite.py --log traces.jsonl # 1. log real runs
95
+ agentlahon review traces.jsonl # 2. page through, tag failures
96
+ agentlahon analyze traces.jsonl # 3. failure taxonomy (what dominates)
97
+ agentlahon suggest traces.jsonl t0003 # 4. trace -> ready-to-paste assertion
98
+ ```
99
+
100
+ ```
101
+ [t0003] input: 5000 flyers, 2000 posters, 10000 tickets
102
+ trace: 1. reorder_item(Invitation cards, 9000) → ordered=True
103
+ suggested assertions:
104
+ expect.no_action("reorder_item", where=lambda a: (a.result or {}).get("ordered"))
105
+ # reorder_item took effect — guard against it when it should not fire
106
+ ```
107
+
108
+ ## LLM-as-judge — for subjective checks, aligned before you trust it
109
+
110
+ Code assertions can't judge "is this reply faithful / on-policy / correct for our domain?" — that needs an LLM judge. agentlahon does it the rigorous way: **binary** verdicts, an **optional domain reference** to grade against, and an **alignment** step that scores the judge against *your* human labels. An unaligned judge is worse than none.
111
+
112
+ ```python
113
+ from agentlahon import expect, openai_complete, llm_judge, align, print_alignment
114
+
115
+ complete = openai_complete(model="gpt-4o-mini") # pluggable; pip install "agentlahon[judge]"
116
+
117
+ check = expect.judge("Does the reply stay within our refund policy?",
118
+ complete=complete,
119
+ reference=open("refund_policy.md").read()) # optional domain doc
120
+
121
+ # Don't trust the judge until it agrees with you:
122
+ labeled = [("we'll refund within 30 days", True), ("sure, full refund anytime", False), ...]
123
+ print_alignment(align(llm_judge("within policy?", complete), labeled))
124
+ # accuracy 92% TPR 95% TNR 88% κ 0.83 -> TRUSTWORTHY
125
+ ```
126
+
127
+ A judge that rubber-stamps everything scores **TNR 0% → NEEDS WORK** — caught before it hides real failures.
128
+
129
+ ## Traces — *what* went wrong, not just *that* it did
130
+
131
+ When a check fails, agentlahon prints the agent's action trace and points at the exact step:
132
+
133
+ ```
134
+ ✗ never restocks (money moves) took 1×: [Invitation cards ×9000]
135
+ trace (what the agent did):
136
+ · 1. tool_reorder_item(Flyers, 5050) → ordered=False (refused, harmless)
137
+ · 2. tool_reorder_item(Poster paper, 2050) → ordered=False (refused, harmless)
138
+ ✗ 3. tool_reorder_item(Invitation cards, 9000)→ ordered=True ← never restocks (money moves)
139
+ ```
140
+
141
+ The failing check is linked to the offending step (`ScenarioResult.blame()`), so you go from red to root cause instantly. Same trace renders in the HTML report.
142
+
143
+ ## CLI (drop into CI)
144
+
145
+ A suite file defines `scenarios` and `agent`; the CLI exits non-zero on findings:
146
+
147
+ ```bash
148
+ agentlahon run examples/suite.py --html report.html
149
+ ```
150
+
151
+ ## Run the examples (no API key)
152
+
153
+ ```bash
154
+ python examples/run_demo.py # synthetic agent with a planted bug
155
+ python examples/run_beavers.py # points at a REAL pydantic-ai agent, catches a real regression
156
+ pytest # 6 core tests
157
+ ```
158
+
159
+ ## Layout
160
+
161
+ ```
162
+ src/agentlahon/ core · checks · adapters · report · cli
163
+ examples/ run_demo · run_beavers · suite
164
+ tests/ test_core
165
+ ```
166
+
167
+ ## Checks
168
+
169
+ | Family | Checks | Control (NIST AI RMF) |
170
+ |---|---|---|
171
+ | say-level | `output_contains`, `output_absent` | MEASURE-2.3 Task performance |
172
+ | **do-level** | `no_action`, `action_taken`, `max_actions`, `actions_only_on` | MANAGE-2.1 Action safety |
173
+ | privacy | `no_pii` | MEASURE-2.10 Privacy |
174
+ | transparency | `no_internal_leak` | MEASURE-2.9 Transparency |
175
+ | faithfulness | `faithful(judge)` — plug in LLM-as-judge | MEASURE-2.5 Validity |
176
+
177
+ ## Roadmap (v0 → product)
178
+
179
+ - [x] Function-capture adapter (auto-records real tool calls **and effects**)
180
+ - [x] CI integration (`agentlahon run` exits non-zero on findings)
181
+ - [ ] Native adapters for OpenAI/Anthropic tool-calls & LangGraph traces
182
+ - [ ] LLM-as-judge faithfulness + bias checks
183
+ - [ ] Regression mode: diff a run against a saved baseline on model/prompt change
184
+ - [ ] Hosted dashboard + shareable report links (the paid layer)
185
+
186
+ Status: **v0.0.1** — installable package, CLI, effect-level checks, terminal + HTML
187
+ report, tests, and a working run against a real pydantic-ai agent.
@@ -0,0 +1,155 @@
1
+ # agentlahon
2
+
3
+ ### Unit tests for AI agents — catch when your agent *does* the wrong thing, not just when it *says* the wrong thing.
4
+
5
+ ![tests](https://img.shields.io/badge/tests-passing-2fbf71) ![python](https://img.shields.io/badge/python-3.9%2B-blue) ![license](https://img.shields.io/badge/license-MIT-black) ![deps](https://img.shields.io/badge/core%20deps-zero-black)
6
+
7
+ > **Status:** v0.0.1, experimental — APIs may change. Core is zero-dependency. Feedback and first users very welcome.
8
+
9
+ Text-only evals grade what an agent *says*. But agents take *actions* — call tools, write to databases, move money. The dangerous failure is when the reply looks perfect while the actions are wrong. **agentlahon grades both**, and maps every check to a governance control so a green suite doubles as an audit-ready **AI assurance report**.
10
+
11
+ ```
12
+ FAIL decline-only-noncarried
13
+ ✓ reply mentions 'unable' found ← SAID the right thing
14
+ ✓ never records a sale no such action ✓
15
+ ✗ never restocks (money moves) took 1×: reorder Invitation-cards ×9000 ← DID the wrong thing
16
+ ```
17
+
18
+ > That failure is invisible to every text-based eval. Only checking the agent's **actions** catches it. This is the beachhead: single-turn LLM eval is crowded — **agent** eval (multi-step, tool-calling, side-effecting) is wide open.
19
+
20
+ ```python
21
+ Scenario("decline-only", "5000 flyers, 10000 tickets", checks=[
22
+ expect.output_contains("unable"), # say-level
23
+ expect.no_action("reorder_item", # do-level — the part text evals miss
24
+ where=lambda a: a.result.get("ordered")), # assert on the *effect*, not just the call
25
+ ])
26
+ ```
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install agentlahon # PyPI distribution name
32
+ pip install -e . # or from source
33
+ ```
34
+
35
+ Core is dependency-free. Python ≥ 3.9.
36
+
37
+ ## Quickstart
38
+
39
+ ```python
40
+ from agentlahon import Scenario, expect, evaluate, print_terminal, write_html
41
+
42
+ def agent(text): # wrap YOUR agent -> AgentRun(output, actions)
43
+ ...
44
+
45
+ scenarios = [
46
+ Scenario("decline-noncarried", "5000 flyers, 10000 tickets", checks=[
47
+ expect.output_contains("unable"),
48
+ expect.no_action("reorder_item"), # the do-level check text evals miss
49
+ expect.no_pii(),
50
+ ]),
51
+ ]
52
+ report = evaluate(agent, scenarios)
53
+ print_terminal(report)
54
+ write_html(report) # shareable assurance_report.html
55
+ ```
56
+
57
+ ## The evals flywheel — look at data → tag → generate assertions
58
+
59
+ The hard part of evals isn't running assertions, it's *knowing what to assert*. agentlahon logs real runs, lets you review and tag failures (open coding), rolls them into a failure taxonomy, and turns a tagged-bad trace into the assertion that would have caught it — the Hamel Husain / Shreya Shankar error-analysis loop, for agent actions.
60
+
61
+ ```bash
62
+ agentlahon run suite.py --log traces.jsonl # 1. log real runs
63
+ agentlahon review traces.jsonl # 2. page through, tag failures
64
+ agentlahon analyze traces.jsonl # 3. failure taxonomy (what dominates)
65
+ agentlahon suggest traces.jsonl t0003 # 4. trace -> ready-to-paste assertion
66
+ ```
67
+
68
+ ```
69
+ [t0003] input: 5000 flyers, 2000 posters, 10000 tickets
70
+ trace: 1. reorder_item(Invitation cards, 9000) → ordered=True
71
+ suggested assertions:
72
+ expect.no_action("reorder_item", where=lambda a: (a.result or {}).get("ordered"))
73
+ # reorder_item took effect — guard against it when it should not fire
74
+ ```
75
+
76
+ ## LLM-as-judge — for subjective checks, aligned before you trust it
77
+
78
+ Code assertions can't judge "is this reply faithful / on-policy / correct for our domain?" — that needs an LLM judge. agentlahon does it the rigorous way: **binary** verdicts, an **optional domain reference** to grade against, and an **alignment** step that scores the judge against *your* human labels. An unaligned judge is worse than none.
79
+
80
+ ```python
81
+ from agentlahon import expect, openai_complete, llm_judge, align, print_alignment
82
+
83
+ complete = openai_complete(model="gpt-4o-mini") # pluggable; pip install "agentlahon[judge]"
84
+
85
+ check = expect.judge("Does the reply stay within our refund policy?",
86
+ complete=complete,
87
+ reference=open("refund_policy.md").read()) # optional domain doc
88
+
89
+ # Don't trust the judge until it agrees with you:
90
+ labeled = [("we'll refund within 30 days", True), ("sure, full refund anytime", False), ...]
91
+ print_alignment(align(llm_judge("within policy?", complete), labeled))
92
+ # accuracy 92% TPR 95% TNR 88% κ 0.83 -> TRUSTWORTHY
93
+ ```
94
+
95
+ A judge that rubber-stamps everything scores **TNR 0% → NEEDS WORK** — caught before it hides real failures.
96
+
97
+ ## Traces — *what* went wrong, not just *that* it did
98
+
99
+ When a check fails, agentlahon prints the agent's action trace and points at the exact step:
100
+
101
+ ```
102
+ ✗ never restocks (money moves) took 1×: [Invitation cards ×9000]
103
+ trace (what the agent did):
104
+ · 1. tool_reorder_item(Flyers, 5050) → ordered=False (refused, harmless)
105
+ · 2. tool_reorder_item(Poster paper, 2050) → ordered=False (refused, harmless)
106
+ ✗ 3. tool_reorder_item(Invitation cards, 9000)→ ordered=True ← never restocks (money moves)
107
+ ```
108
+
109
+ The failing check is linked to the offending step (`ScenarioResult.blame()`), so you go from red to root cause instantly. Same trace renders in the HTML report.
110
+
111
+ ## CLI (drop into CI)
112
+
113
+ A suite file defines `scenarios` and `agent`; the CLI exits non-zero on findings:
114
+
115
+ ```bash
116
+ agentlahon run examples/suite.py --html report.html
117
+ ```
118
+
119
+ ## Run the examples (no API key)
120
+
121
+ ```bash
122
+ python examples/run_demo.py # synthetic agent with a planted bug
123
+ python examples/run_beavers.py # points at a REAL pydantic-ai agent, catches a real regression
124
+ pytest # 6 core tests
125
+ ```
126
+
127
+ ## Layout
128
+
129
+ ```
130
+ src/agentlahon/ core · checks · adapters · report · cli
131
+ examples/ run_demo · run_beavers · suite
132
+ tests/ test_core
133
+ ```
134
+
135
+ ## Checks
136
+
137
+ | Family | Checks | Control (NIST AI RMF) |
138
+ |---|---|---|
139
+ | say-level | `output_contains`, `output_absent` | MEASURE-2.3 Task performance |
140
+ | **do-level** | `no_action`, `action_taken`, `max_actions`, `actions_only_on` | MANAGE-2.1 Action safety |
141
+ | privacy | `no_pii` | MEASURE-2.10 Privacy |
142
+ | transparency | `no_internal_leak` | MEASURE-2.9 Transparency |
143
+ | faithfulness | `faithful(judge)` — plug in LLM-as-judge | MEASURE-2.5 Validity |
144
+
145
+ ## Roadmap (v0 → product)
146
+
147
+ - [x] Function-capture adapter (auto-records real tool calls **and effects**)
148
+ - [x] CI integration (`agentlahon run` exits non-zero on findings)
149
+ - [ ] Native adapters for OpenAI/Anthropic tool-calls & LangGraph traces
150
+ - [ ] LLM-as-judge faithfulness + bias checks
151
+ - [ ] Regression mode: diff a run against a saved baseline on model/prompt change
152
+ - [ ] Hosted dashboard + shareable report links (the paid layer)
153
+
154
+ Status: **v0.0.1** — installable package, CLI, effect-level checks, terminal + HTML
155
+ report, tests, and a working run against a real pydantic-ai agent.
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agentlahon"
7
+ version = "0.0.1"
8
+ description = "Unit tests for AI agents — catch when your agent does the wrong thing, not just when it says the wrong thing."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Anurag Lahon", email = "anuraglahondp@gmail.com" }]
14
+ keywords = ["ai", "agents", "evaluation", "evals", "llm", "testing", "governance"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Topic :: Software Development :: Testing",
19
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [] # core is intentionally dependency-free
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["pytest>=7"]
32
+ judge = ["openai>=1"] # only needed for a live LLM-as-judge
33
+ webapp = ["flask>=3"] # the dashboard product surface
34
+
35
+ [project.scripts]
36
+ agentlahon = "agentlahon.cli:main"
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/anuraglahon16/agentcheck"
40
+ Repository = "https://github.com/anuraglahon16/agentcheck"
41
+ Issues = "https://github.com/anuraglahon16/agentcheck/issues"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.setuptools.package-data]
47
+ agentlahon = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,31 @@
1
+ """agentlahon — unit tests for AI agents.
2
+
3
+ Catch when your agent does the wrong thing, not just when it says the wrong
4
+ thing. Score say-level (reply) and do-level (actions/side-effects) behavior,
5
+ mapped to governance controls.
6
+
7
+ from agentlahon import Scenario, expect, evaluate, print_terminal
8
+
9
+ scenarios = [
10
+ Scenario("decline-noncarried", "5000 flyers, 10000 tickets", checks=[
11
+ expect.output_contains("unable"),
12
+ expect.no_action("reorder_item"), # <- the do-level check text evals miss
13
+ expect.no_pii(),
14
+ ]),
15
+ ]
16
+ report = evaluate(my_agent_adapter, scenarios)
17
+ print_terminal(report)
18
+ """
19
+ from .core import Action, AgentRun, Scenario, Check, Report, evaluate
20
+ from .checks import expect, CONTROLS
21
+ from .report import print_terminal, write_html
22
+ from .adapters import capture
23
+ from .store import TraceStore, TraceRecord
24
+ from .analysis import taxonomy, suggest_checks
25
+ from .judge import llm_judge, openai_complete, align, print_alignment
26
+
27
+ __all__ = ["Action", "AgentRun", "Scenario", "Check", "Report", "evaluate",
28
+ "expect", "CONTROLS", "print_terminal", "write_html", "capture",
29
+ "TraceStore", "TraceRecord", "taxonomy", "suggest_checks",
30
+ "llm_judge", "openai_complete", "align", "print_alignment"]
31
+ __version__ = "0.0.1"
@@ -0,0 +1,72 @@
1
+ """Adapters that auto-capture an agent's actions so you don't wire them by hand.
2
+
3
+ `capture()` wraps a set of functions on a module (typically the tool functions
4
+ an agent calls) and records every invocation as an `Action`. Point it at your
5
+ real code, run the agent, and you get the `AgentRun.actions` list for free —
6
+ this is how agentlahon sees *what the agent did*, not just what it said.
7
+
8
+ Works with any framework whose tools bottom out in Python callables:
9
+ pydantic-ai, LangChain, or a hand-rolled loop. For pydantic-ai specifically,
10
+ the tool wrappers call module-level `tool_*` functions, so capturing those
11
+ records the real side-effects the model triggered.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from contextlib import contextmanager
17
+ from typing import Dict, Iterable, List
18
+
19
+ from .core import Action
20
+
21
+
22
+ def _parse_result(value):
23
+ """Parse a tool's return into a dict when possible, so checks can inspect it."""
24
+ if isinstance(value, dict):
25
+ return value
26
+ if isinstance(value, str):
27
+ try:
28
+ return json.loads(value)
29
+ except (ValueError, TypeError):
30
+ return value
31
+ return value
32
+
33
+
34
+ def _summarize(tool: str, args: tuple, kwargs: dict) -> Dict:
35
+ """Best-effort, readable arg snapshot — keeps the report legible."""
36
+ snap = dict(kwargs)
37
+ for i, a in enumerate(args):
38
+ # skip long/opaque positional blobs (JSON strings, dates); keep short ones
39
+ if isinstance(a, (int, float)):
40
+ snap[f"arg{i}"] = a
41
+ elif isinstance(a, str) and len(a) <= 60:
42
+ snap[f"arg{i}"] = a
43
+ return snap
44
+
45
+
46
+ @contextmanager
47
+ def capture(module, names: Iterable[str]):
48
+ """Temporarily wrap ``module.<name>`` funcs to log calls; restore on exit.
49
+
50
+ Yields a live list of `Action`s. Callers that reference the wrapped names as
51
+ module globals (the usual case) will hit the wrapper, so real tool calls are
52
+ recorded without touching the agent's code.
53
+ """
54
+ log: List[Action] = []
55
+ originals = {}
56
+ for name in names:
57
+ originals[name] = getattr(module, name)
58
+
59
+ def make(n, orig):
60
+ def wrapper(*args, **kwargs):
61
+ out = orig(*args, **kwargs)
62
+ log.append(Action(tool=n, args=_summarize(n, args, kwargs),
63
+ result=_parse_result(out)))
64
+ return out
65
+ return wrapper
66
+
67
+ setattr(module, name, make(name, originals[name]))
68
+ try:
69
+ yield log
70
+ finally:
71
+ for name, orig in originals.items():
72
+ setattr(module, name, orig)
@@ -0,0 +1,77 @@
1
+ """Error analysis + trace→assertion suggestions.
2
+
3
+ Two jobs from the evals playbook:
4
+ * `taxonomy()` — cluster tagged failures by their open-coded label, so you
5
+ see which failure modes dominate (the "axial coding" rollup).
6
+ * `suggest_checks()` — turn a tagged-bad trace into concrete `expect.*`
7
+ assertions that would have caught it. This closes the loop: look at data →
8
+ label failure → generate the eval.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Dict, List, Tuple
13
+
14
+ from .core import Action
15
+ from .store import TraceRecord, TraceStore
16
+
17
+ _EFFECT_KEYS = ("ordered", "recorded", "sold", "success", "created", "sent")
18
+ _ITEM_ARGS = ("item", "name", "product", "arg0")
19
+
20
+
21
+ def taxonomy(store: TraceStore) -> Dict[str, int]:
22
+ """{failure-label: count} over tagged failures — most common first."""
23
+ counts: Dict[str, int] = {}
24
+ for r in store.failures():
25
+ label = r.annotation.label or "(unlabeled)"
26
+ counts[label] = counts.get(label, 0) + 1
27
+ return dict(sorted(counts.items(), key=lambda kv: -kv[1]))
28
+
29
+
30
+ def _effect_key(a: Action) -> str:
31
+ if isinstance(a.result, dict):
32
+ for k in _EFFECT_KEYS:
33
+ if a.result.get(k):
34
+ return k
35
+ return ""
36
+
37
+
38
+ def suggest_checks(rec: TraceRecord) -> List[Tuple[str, str]]:
39
+ """Propose (assertion_code, rationale) for a bad trace.
40
+
41
+ Heuristics over what the agent actually DID, so the suggestions are grounded
42
+ in this trace — not a generic checklist.
43
+ """
44
+ out: List[Tuple[str, str]] = []
45
+ seen = set()
46
+
47
+ for a in rec.actions:
48
+ key = _effect_key(a)
49
+ if key and a.tool not in seen:
50
+ seen.add(a.tool)
51
+ out.append((
52
+ f'expect.no_action("{a.tool}", '
53
+ f'where=lambda a: (a.result or {{}}).get("{key}"))',
54
+ f'{a.tool} took effect ({key}=True) — guard against it when it should not fire',
55
+ ))
56
+
57
+ # scope guard if actions carry an item-like arg
58
+ for a in rec.actions:
59
+ arg = next((k for k in _ITEM_ARGS if k in a.args), None)
60
+ if arg and (a.tool, "scope") not in seen:
61
+ seen.add((a.tool, "scope"))
62
+ out.append((
63
+ f'expect.actions_only_on("{a.tool}", "{arg}", ALLOWED)',
64
+ f'{a.tool} acted on {a.args.get(arg)!r} — restrict it to an allow-set',
65
+ ))
66
+ break
67
+
68
+ # privacy guard if the reply looks like it leaked something
69
+ low = rec.output.lower()
70
+ if "@" in rec.output or any(t in low for t in ("margin", "cost", "internal")):
71
+ out.append(('expect.no_pii() # and expect.no_internal_leak()',
72
+ "reply may leak PII or internal info"))
73
+
74
+ if not out:
75
+ out.append(('expect.output_absent("<bad phrase>")',
76
+ "no action effects found — assert on the reply text instead"))
77
+ return out