evalforge-agents 0.1.0__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 (42) hide show
  1. evalforge_agents-0.1.0/LICENSE +21 -0
  2. evalforge_agents-0.1.0/PKG-INFO +384 -0
  3. evalforge_agents-0.1.0/README.md +347 -0
  4. evalforge_agents-0.1.0/pyproject.toml +73 -0
  5. evalforge_agents-0.1.0/setup.cfg +4 -0
  6. evalforge_agents-0.1.0/src/evalforge_agents/__init__.py +3 -0
  7. evalforge_agents-0.1.0/src/evalforge_agents/adapters/__init__.py +5 -0
  8. evalforge_agents-0.1.0/src/evalforge_agents/adapters/base.py +13 -0
  9. evalforge_agents-0.1.0/src/evalforge_agents/adapters/command.py +68 -0
  10. evalforge_agents-0.1.0/src/evalforge_agents/adapters/http.py +52 -0
  11. evalforge_agents-0.1.0/src/evalforge_agents/api.py +185 -0
  12. evalforge_agents-0.1.0/src/evalforge_agents/cli.py +340 -0
  13. evalforge_agents-0.1.0/src/evalforge_agents/config.py +13 -0
  14. evalforge_agents-0.1.0/src/evalforge_agents/db.py +237 -0
  15. evalforge_agents-0.1.0/src/evalforge_agents/graders/__init__.py +19 -0
  16. evalforge_agents-0.1.0/src/evalforge_agents/graders/base.py +13 -0
  17. evalforge_agents-0.1.0/src/evalforge_agents/graders/deterministic.py +174 -0
  18. evalforge_agents-0.1.0/src/evalforge_agents/graders/human.py +55 -0
  19. evalforge_agents-0.1.0/src/evalforge_agents/graders/llm_judge.py +94 -0
  20. evalforge_agents-0.1.0/src/evalforge_agents/logging_utils.py +89 -0
  21. evalforge_agents-0.1.0/src/evalforge_agents/models.py +164 -0
  22. evalforge_agents-0.1.0/src/evalforge_agents/regression.py +55 -0
  23. evalforge_agents-0.1.0/src/evalforge_agents/runner.py +135 -0
  24. evalforge_agents-0.1.0/src/evalforge_agents/storage.py +71 -0
  25. evalforge_agents-0.1.0/src/evalforge_agents.egg-info/PKG-INFO +384 -0
  26. evalforge_agents-0.1.0/src/evalforge_agents.egg-info/SOURCES.txt +40 -0
  27. evalforge_agents-0.1.0/src/evalforge_agents.egg-info/dependency_links.txt +1 -0
  28. evalforge_agents-0.1.0/src/evalforge_agents.egg-info/entry_points.txt +2 -0
  29. evalforge_agents-0.1.0/src/evalforge_agents.egg-info/requires.txt +18 -0
  30. evalforge_agents-0.1.0/src/evalforge_agents.egg-info/top_level.txt +1 -0
  31. evalforge_agents-0.1.0/tests/test_adapters.py +154 -0
  32. evalforge_agents-0.1.0/tests/test_api.py +73 -0
  33. evalforge_agents-0.1.0/tests/test_api_security.py +102 -0
  34. evalforge_agents-0.1.0/tests/test_cli.py +313 -0
  35. evalforge_agents-0.1.0/tests/test_db.py +183 -0
  36. evalforge_agents-0.1.0/tests/test_graders.py +22 -0
  37. evalforge_agents-0.1.0/tests/test_human_grader.py +19 -0
  38. evalforge_agents-0.1.0/tests/test_logging_utils.py +121 -0
  39. evalforge_agents-0.1.0/tests/test_regression.py +39 -0
  40. evalforge_agents-0.1.0/tests/test_runner.py +29 -0
  41. evalforge_agents-0.1.0/tests/test_storage.py +94 -0
  42. evalforge_agents-0.1.0/tests/test_suite_version.py +48 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abhilash Reddy Somigari
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,384 @@
1
+ Metadata-Version: 2.4
2
+ Name: evalforge-agents
3
+ Version: 0.1.0
4
+ Summary: CI/CD evaluation infrastructure for AI agents
5
+ Author: EvalForge Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/AbhilashSomigari/evalforge
8
+ Project-URL: Repository, https://github.com/AbhilashSomigari/evalforge
9
+ Project-URL: Issues, https://github.com/AbhilashSomigari/evalforge/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: pydantic<3,>=2.8
21
+ Requires-Dist: PyYAML<7,>=6
22
+ Requires-Dist: typer<1,>=0.12
23
+ Requires-Dist: rich<15,>=13
24
+ Requires-Dist: fastapi<1,>=0.115
25
+ Requires-Dist: uvicorn<1,>=0.30
26
+ Requires-Dist: httpx<1,>=0.27
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest<10,>=8; extra == "dev"
29
+ Requires-Dist: pytest-asyncio<1,>=0.24; extra == "dev"
30
+ Requires-Dist: ruff<0.17,>=0.16.10; extra == "dev"
31
+ Requires-Dist: httpx2<3,>=2; extra == "dev"
32
+ Requires-Dist: mypy<2,>=1.11; extra == "dev"
33
+ Requires-Dist: types-PyYAML<7,>=6; extra == "dev"
34
+ Provides-Extra: postgres
35
+ Requires-Dist: psycopg[binary,pool]<4,>=3.1; extra == "postgres"
36
+ Dynamic: license-file
37
+
38
+ # EvalForge — CI/CD for AI Agents
39
+
40
+ EvalForge is an infrastructure-first evaluation harness for **tool-using, multi-step AI agents**. It turns agent behavior into repeatable tests that can run locally or in CI and block a deployment when quality regresses.
41
+
42
+ > Change a prompt, model, tool schema, or RAG pipeline → run scenarios → capture trajectories → grade outcomes → compare against a baseline → enforce gates.
43
+
44
+ ## Why this exists
45
+
46
+ Normal unit tests are necessary but insufficient for agents. Agents are non-deterministic, make multiple model calls, use tools, retrieve context, and mutate environments. EvalForge treats the **trajectory and outcome** as first-class test artifacts instead of evaluating only the final string.
47
+
48
+ ## v0.1 capabilities
49
+
50
+ - YAML evaluation suites and reusable scenarios
51
+ - Multiple trials per task + concurrent execution
52
+ - Language-agnostic command adapter (JSON over stdin/stdout)
53
+ - HTTP adapter for already-running agents
54
+ - Full trajectory/tool-call capture
55
+ - Deterministic graders: exact match, contains, regex, similarity, JSON contract
56
+ - Tool sequence and tool argument correctness
57
+ - Citation-based RAG groundedness proxy
58
+ - Known-hallucination regression guard
59
+ - Optional OpenAI-compatible LLM-as-judge grader
60
+ - Human-score ingestion that participates in the same pass/fail pipeline
61
+ - Task success, tool correctness, hallucination rate, latency, token and cost aggregation
62
+ - Baseline-vs-candidate regression comparison
63
+ - Prompt/model A/B runs over the exact same suite
64
+ - Absolute and regression-based CI gates
65
+ - Trajectory replay / re-grading without paying for another agent run
66
+ - FastAPI run-inspection API
67
+ - Docker + GitHub Actions example
68
+
69
+ ## Architecture
70
+
71
+ ```text
72
+ ┌────────────────────────┐
73
+ │ YAML Evaluation Suite │
74
+ │ tasks · trials · gates │
75
+ └────────────┬───────────┘
76
+ │
77
+ ┌───────────▼───────────┐
78
+ │ Eval Runner │
79
+ │ concurrency · retries │
80
+ └───────┬────────┬──────┘
81
+ │ │
82
+ ┌──────────────▼─┐ ┌─▼──────────────┐
83
+ │ Agent Adapter │ │ Recorded Replay │
84
+ │ command / HTTP │ │ no model call │
85
+ └────────┬────────┘ └──────┬─────────┘
86
+ │ │
87
+ trajectory + output + usage
88
+ │ │
89
+ ┌─────▼────────────────────▼──┐
90
+ │ Graders │
91
+ │ code · tool · RAG · LLM │
92
+ └──────────────┬──────────────┘
93
+ │
94
+ ┌───────▼────────┐
95
+ │ Metrics + Runs │
96
+ └───────┬────────┘
97
+ │
98
+ ┌──────────▼──────────┐
99
+ │ Regression CI Gates │
100
+ │ PASS / BLOCK │
101
+ └─────────────────────┘
102
+ ```
103
+
104
+ ## Run it
105
+
106
+ ```bash
107
+ python -m venv .venv
108
+ source .venv/bin/activate
109
+ make install # hash-pinned deps from requirements/dev.lock, then the package itself
110
+ pytest
111
+
112
+ evalforge run \
113
+ --suite examples/suites/customer_support.yaml \
114
+ --agent "python examples/agents/demo_agent.py" \
115
+ --out runs/demo.json
116
+ ```
117
+
118
+ A failing gate exits with code `2`, which is what blocks a CI job. Invalid
119
+ input — a malformed suite, an unknown gate metric, an unreadable run file —
120
+ exits with code `1` and a one-line error, before any agent is called where
121
+ possible.
122
+
123
+ Dependencies are pinned with hashes in `requirements/*.lock` (runtime, dev,
124
+ dev+postgres), used by CI and the Docker image. After changing dependencies in
125
+ `pyproject.toml`, regenerate them with `make lock` (needs
126
+ [uv](https://docs.astral.sh/uv/)).
127
+
128
+ ## Agent protocol
129
+
130
+ EvalForge deliberately does **not** force a LangChain/LlamaIndex/custom-agent dependency. For the command adapter, your executable receives one `TaskSpec` JSON object on stdin and returns one `AgentOutput` JSON object on stdout:
131
+
132
+ ```json
133
+ {
134
+ "output": "Refund submitted. Source: KB-REFUND-1",
135
+ "trajectory": [
136
+ {"type": "message", "role": "user", "content": "Refund order R200"},
137
+ {"type": "tool_call", "tool_call": {"name": "lookup_order", "arguments": {"order_id": "R200"}}}
138
+ ],
139
+ "tool_calls": [
140
+ {"name": "lookup_order", "arguments": {"order_id": "R200"}}
141
+ ],
142
+ "usage": {
143
+ "input_tokens": 100,
144
+ "output_tokens": 35,
145
+ "cost_usd": 0.012
146
+ },
147
+ "model": "your-model"
148
+ }
149
+ ```
150
+
151
+ That adapter boundary lets the same suite evaluate Python, TypeScript, Java, Go, MCP-based agents, or remote services.
152
+
153
+ ## Suite example
154
+
155
+ ```yaml
156
+ name: checkout-agent
157
+ trials_per_task: 3
158
+ concurrency: 8
159
+
160
+ gates:
161
+ - metric: task_success
162
+ op: ">="
163
+ value: 0.90
164
+ - metric: avg_cost_usd
165
+ op: "<="
166
+ value: 0.15
167
+ - metric: task_success
168
+ op: ">="
169
+ max_regression: 0.03
170
+
171
+ tasks:
172
+ - id: refund-order
173
+ input: Refund order R200.
174
+ expected_tools: [lookup_order, issue_refund]
175
+ graders:
176
+ - type: tool_sequence
177
+ name: tool_correctness
178
+ pass_threshold: 1.0
179
+ ```
180
+
181
+ For a regression gate, provide a baseline. A quality metric with `op: ">="` is allowed to drop by at most `max_regression`; a cost/latency metric with `op: "<="` is allowed to rise by at most that amount. A regression gate with no baseline to compare against **fails** (shown as `no baseline` in the gates table) — it can't pass without one.
182
+
183
+ ```bash
184
+ evalforge run --suite examples/suites/customer_support.yaml --agent "..." \
185
+ --out runs/pr-184.json --baseline-branch main
186
+ ```
187
+
188
+ `--baseline-branch main` auto-resolves the baseline to the latest run of this
189
+ suite on that branch — no more hand-tracking `runs/main.json` yourself. It
190
+ looks in Postgres if `EVALFORGE_DATABASE_URL` is set, otherwise in `--out`'s
191
+ directory. `GITHUB_HEAD_REF`/`GITHUB_REF_NAME` set this automatically in
192
+ GitHub Actions; set `GIT_BRANCH` yourself elsewhere. Use `--baseline
193
+ <path>` instead for a fixed file, same as before — the two are mutually
194
+ exclusive. Either way, the run prints which baseline it resolved (run id,
195
+ branch, commit, timestamp), and regression gates show the baseline value and
196
+ delta next to each result. Run files in that directory that can't be read
197
+ (corrupt, or from an older `schema_version`) are skipped with a logged
198
+ warning rather than silently. If the baseline came from a suite definition that's
199
+ since changed (a task, grader, or gate edited — not just its `description`
200
+ or `concurrency`), you'll get a warning that the comparison may not be
201
+ meaningful, since `evalforge` tracks a content hash (`suite_version`) of
202
+ every run's suite on the `EvalRun` itself.
203
+
204
+ **In CI, `--baseline-branch` needs storage that outlives the job.** Each CI
205
+ job starts with an empty workspace, so without Postgres the `--out`
206
+ directory never contains an earlier run and every regression gate fails with
207
+ `no baseline`. Either set `EVALFORGE_DATABASE_URL` (as a secret) to a
208
+ Postgres instance your CI can reach, or restore previous run files into the
209
+ `--out` directory before running (e.g. `actions/cache` or downloading the
210
+ main branch's last `evalforge-run` artifact).
211
+
212
+ ## Replay a trajectory
213
+
214
+ When you improve a grader or rubric, re-score an existing run without spending tokens again:
215
+
216
+ ```bash
217
+ evalforge replay \
218
+ --suite examples/suites/customer_support.yaml \
219
+ --run runs/demo.json \
220
+ --out runs/replayed.json
221
+ ```
222
+
223
+ ## Compare two runs
224
+
225
+ ```bash
226
+ evalforge compare --baseline runs/main.json --candidate runs/pr-184.json
227
+ ```
228
+
229
+ ## Prompt/model A/B test
230
+
231
+ Run the same task bank against two commands:
232
+
233
+ ```bash
234
+ evalforge ab \
235
+ --suite examples/suites/customer_support.yaml \
236
+ --agent-a "python agent_prompt_v11.py" \
237
+ --agent-b "python agent_prompt_v12.py"
238
+ ```
239
+
240
+ EvalForge stores both run artifacts and prints metric deltas. The two commands can represent different prompts, models, tool schemas, retrieval strategies, or entire agent implementations.
241
+
242
+ ## Human grading
243
+
244
+ Human scores can be imported without changing the run pipeline:
245
+
246
+ ```yaml
247
+ - type: human
248
+ name: human_quality
249
+ pass_threshold: 0.8
250
+ config:
251
+ file: reviews/human_scores.json
252
+ ```
253
+
254
+ ```json
255
+ {
256
+ "refund": {"score": 0.95, "reason": "Correct and policy-compliant"},
257
+ "order-status": 1.0
258
+ }
259
+ ```
260
+
261
+ ## Inspect runs over HTTP
262
+
263
+ ```bash
264
+ evalforge serve --host 0.0.0.0 --port 8000
265
+ # GET /health
266
+ # GET /runs?limit=20&offset=0&suite=customer-support-regression
267
+ # GET /runs/{key}
268
+ ```
269
+
270
+ Interactive FastAPI docs are available at `/docs`.
271
+
272
+ By default runs are read back from the local `runs/` directory (`{key}` is a
273
+ filename). Set `EVALFORGE_DATABASE_URL` to persist runs to Postgres instead —
274
+ `evalforge run`/`ab`/`replay` write to it in addition to the local JSON file,
275
+ and `/runs`/`/runs/{key}` (`{key}` becomes the run id) query it directly with
276
+ real pagination and suite filtering instead of parsing every file in `runs/`
277
+ on every request:
278
+
279
+ ```bash
280
+ export EVALFORGE_DATABASE_URL=postgresql://evalforge:evalforge@localhost:5432/evalforge
281
+ pip install -e ".[postgres]"
282
+ ```
283
+
284
+ Database connections are pooled (`EVALFORGE_DB_POOL_MAX_SIZE`, default 10).
285
+ Runs stored under a different `schema_version` are treated the same way in
286
+ both backends: skipped (with a logged warning) when listing or resolving a
287
+ baseline, and a `409` with the reason from `/runs/{key}`.
288
+
289
+ `docker-compose.yml` runs a Postgres container alongside the API and wires
290
+ this automatically. It requires a `.env` file next to it:
291
+
292
+ ```bash
293
+ cp .env.example .env # then set POSTGRES_PASSWORD and EVALFORGE_API_KEY (e.g. openssl rand -hex 32)
294
+ docker compose up --build
295
+ ```
296
+
297
+ Compose refuses to start without both values, and publishes the API on
298
+ `127.0.0.1:8000` only; put a TLS-terminating reverse proxy in front of it to
299
+ expose it further. (If a `evalforge-db` volume already exists from an older
300
+ setup, Postgres keeps the password it was initialized with — change it in
301
+ the database, or remove the volume, to match `.env`.)
302
+
303
+ ### Securing the API
304
+
305
+ The API is **open by default** — fine for `localhost`, not for anywhere else.
306
+ Before exposing it beyond your own machine:
307
+
308
+ ```bash
309
+ export EVALFORGE_API_KEY=some-long-random-value # required Authorization: Bearer <key> on /runs*
310
+ export EVALFORGE_CORS_ORIGINS=https://your-dashboard.example # comma-separated allowlist; unset = no cross-origin reads
311
+ export EVALFORGE_RATE_LIMIT_PER_MINUTE=60 # per client IP; see below
312
+ ```
313
+
314
+ The API key is compared in constant time. The rate limit is counted in
315
+ Postgres when `EVALFORGE_DATABASE_URL` is set (a fixed one-minute window
316
+ shared by every replica); without a database it falls back to an in-memory
317
+ counter per process. Behind a reverse proxy every request arrives from the
318
+ proxy's IP, so rate-limit there (or by forwarded client IP at the proxy)
319
+ rather than relying on this per-IP limit alone.
320
+
321
+ `/health` never requires the API key, so container/orchestrator health checks
322
+ keep working unauthenticated. Every request gets an `X-Request-ID` (echoed
323
+ back if the caller sends one), and the API and runner emit one JSON log line
324
+ per request/run to stderr — `evalforge run` keeps its human-readable table on
325
+ stdout, so scripts piping stdout are unaffected.
326
+
327
+ ## LLM-as-judge
328
+
329
+ Add this grader to a task or suite:
330
+
331
+ ```yaml
332
+ - type: llm_judge
333
+ name: policy_quality
334
+ pass_threshold: 0.8
335
+ config:
336
+ model: your-judge-model
337
+ rubric: >-
338
+ Score whether the answer follows the refund policy, cites evidence,
339
+ and avoids claiming actions that did not occur.
340
+ ```
341
+
342
+ Then set:
343
+
344
+ ```bash
345
+ export EVALFORGE_JUDGE_API_KEY=...
346
+ export EVALFORGE_JUDGE_BASE_URL=https://api.openai.com/v1
347
+ export EVALFORGE_JUDGE_MODEL=...
348
+ ```
349
+
350
+ The implementation is OpenAI-compatible so it can also point to compatible gateways/local inference servers.
351
+
352
+ ## Releasing
353
+
354
+ The package is published to PyPI as **`evalforge-agents`** (import it as
355
+ `evalforge_agents`; the CLI command is `evalforge`). Releases go through
356
+ `.github/workflows/release.yml` with PyPI Trusted Publishing, so no API token
357
+ is stored anywhere.
358
+
359
+ One-time setup:
360
+
361
+ 1. On PyPI → *Your account* → *Publishing* → *Add a new pending publisher*:
362
+ project `evalforge-agents`, owner `AbhilashSomigari`, repository
363
+ `evalforge`, workflow `release.yml`, environment `pypi`.
364
+ 2. Optional but recommended: in the GitHub repo → *Settings* →
365
+ *Environments* → `pypi`, add yourself as a required reviewer so every
366
+ release waits for a manual approval.
367
+
368
+ Each release: bump `version` in `pyproject.toml`, commit, then push a
369
+ matching tag — the workflow refuses a tag that doesn't match:
370
+
371
+ ```bash
372
+ git tag v0.1.0 && git push origin v0.1.0
373
+ ```
374
+
375
+ It re-runs lint, type checks and tests, builds the sdist and wheel, checks
376
+ them with `twine check --strict`, then uploads.
377
+
378
+ ## What comes next
379
+
380
+ The next platform milestones are: distributed workers; human review queues; richer LLM/NLI groundedness graders; environment snapshots and state-based outcome grading; provider-native OpenAI/Anthropic adapters; statistical confidence intervals; flaky-eval detection; prompt/model matrix experiments; GitHub PR annotations; and an observability UI built on the same run schema.
381
+
382
+ ## Design principle
383
+
384
+ **The run artifact is the product.** The UI is only one consumer. Every trial stores the task, outcome-facing output, tool calls, trajectory, grader results, latency, usage and cost so the same evidence can drive CI, debugging, dashboards, human review, and future graders.