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.
- agentlahon-0.0.1/LICENSE +21 -0
- agentlahon-0.0.1/PKG-INFO +187 -0
- agentlahon-0.0.1/README.md +155 -0
- agentlahon-0.0.1/pyproject.toml +47 -0
- agentlahon-0.0.1/setup.cfg +4 -0
- agentlahon-0.0.1/src/agentlahon/__init__.py +31 -0
- agentlahon-0.0.1/src/agentlahon/adapters.py +72 -0
- agentlahon-0.0.1/src/agentlahon/analysis.py +77 -0
- agentlahon-0.0.1/src/agentlahon/checks.py +127 -0
- agentlahon-0.0.1/src/agentlahon/cli.py +160 -0
- agentlahon-0.0.1/src/agentlahon/core.py +146 -0
- agentlahon-0.0.1/src/agentlahon/judge.py +139 -0
- agentlahon-0.0.1/src/agentlahon/py.typed +0 -0
- agentlahon-0.0.1/src/agentlahon/report.py +144 -0
- agentlahon-0.0.1/src/agentlahon/store.py +95 -0
- agentlahon-0.0.1/src/agentlahon.egg-info/PKG-INFO +187 -0
- agentlahon-0.0.1/src/agentlahon.egg-info/SOURCES.txt +22 -0
- agentlahon-0.0.1/src/agentlahon.egg-info/dependency_links.txt +1 -0
- agentlahon-0.0.1/src/agentlahon.egg-info/entry_points.txt +2 -0
- agentlahon-0.0.1/src/agentlahon.egg-info/requires.txt +9 -0
- agentlahon-0.0.1/src/agentlahon.egg-info/top_level.txt +1 -0
- agentlahon-0.0.1/tests/test_core.py +56 -0
- agentlahon-0.0.1/tests/test_judge.py +57 -0
- agentlahon-0.0.1/tests/test_store.py +56 -0
agentlahon-0.0.1/LICENSE
ADDED
|
@@ -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
|
+
   
|
|
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
|
+
   
|
|
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,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
|