1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! `eval-core` — **pytest/jest, but for LLM agents**: a batteries-included agent testing framework
//! where a test case is a *prompt* and the assertions are built-in checks on what the agent DID —
//! which tools it called, with which parameters, and what it finally said or computed — scored over
//! the universal [`RunArtifacts`].
//!
//! Prompts in, assertions on behavior out; bring your own harness. For the common case a host
//! implements ONE method ([`Agent::run`]), authors [`expect::Expectation`] predicates (in RON or
//! inline), and calls [`run_suite`] — no `World`, no `Setup`, no [`Scorer`] impl. It is
//! game-agnostic, so it doubles as a generic result/metric data model plus a self-contained HTML
//! comparison report (a small "Weights & Biases for evals").
//!
//! ## Quickstart
//!
//! Implement [`Agent::run`] over your harness (run one prompt, return what the agent did via the
//! `with_*` builders + [`ToolCall::new`]), author [`Expectation`] cases, and call [`run_suite`]:
//!
//! ```
//! use eval_core::{run_suite, Agent, EvalCase, EvalError, Expectation, RunArtifacts, ToolCall};
//! use serde_json::json;
//!
//! // A toy agent (no real LLM): for an "add" prompt it emits a calculator tool call and ends with
//! // the sum; for anything else it just greets, making no tool call.
//! struct MyAgent;
//! impl Agent for MyAgent {
//! fn run(&self, instruction: &str) -> Result<RunArtifacts, EvalError> {
//! if instruction.contains("add") {
//! Ok(RunArtifacts::new()
//! .with_tool_calls(vec![ToolCall::new(
//! "calculator",
//! json!({ "op": "add", "a": 2, "b": 2 }),
//! )])
//! .with_final_text("The answer is 4."))
//! } else {
//! Ok(RunArtifacts::new().with_final_text("Hello!"))
//! }
//! }
//! }
//!
//! let cases: Vec<EvalCase<(), Expectation>> = vec![
//! EvalCase {
//! name: "adds-two-numbers".to_owned(),
//! instruction: "please add 2 and 2".to_owned(),
//! setup: (), // no `setup` on the easy path — it is `()`
//! expect: vec![
//! Expectation::CalledToolWith {
//! tool: "calculator".to_owned(),
//! args: json!({ "op": "add" }),
//! },
//! Expectation::FinalNumberEquals { value: 4.0, tolerance: 0.0 },
//! ],
//! },
//! EvalCase {
//! name: "no-tools-for-chitchat".to_owned(),
//! instruction: "hello there".to_owned(),
//! setup: (),
//! expect: vec![Expectation::NoToolCalls],
//! },
//! ];
//!
//! let report = run_suite(&MyAgent, &cases);
//! assert_eq!(report.total(), 2);
//! assert_eq!(report.passed(), 2); // both cases pass
//! // `println!("{report}")` prints the human-readable summary table.
//! ```
//!
//! In practice cases are usually authored as RON and loaded with [`load_cases`]:
//!
//! ```ron
//! (
//! name: "adds-two-numbers",
//! instruction: "what is 2 + 2?",
//! expect: [
//! CalledToolWith(tool: "calculator", args: { "op": "add" }),
//! FinalNumberEquals(value: 4.0),
//! ],
//! )
//! ```
//!
//! `eval-core` also ships a ready-to-run [`baseline()`] suite (arithmetic / language / tool-use, 18
//! cases) you can hand straight to [`run_suite`], and [`baseline_files`] to dump it as a template.
//! See `examples/calculator.rs` for a complete, dependency-free agent-framework example, and the
//! crate `README.md` for the full assertion catalog.
//!
//! ## Isolation guarantee
//!
//! This crate depends only on small, well-scoped third-party crates (`serde`, `serde_json`, `ron`,
//! `regex`, `thiserror`, `anyhow`, `tracing`, `chrono` (local timestamps on auto-persisted runs),
//! `include_dir` to embed the shipped baseline suite, and `ureq` (a small blocking HTTP client with
//! rustls TLS, used to upload finished runs to the EvalForge dashboard)). It has ZERO dependency on any
//! host engine/game crate, so it can be lifted into a standalone public repository unchanged. The
//! dependency arrow points one way: a host harness depends on `eval-core`, never the reverse.
//!
//! ## Modules
//!
//! - [`report`] — the result/metric data model: [`report::RunRecord`], [`report::EvalReport`],
//! [`report::CaseOutcome`], with a readable `Display` summary and the aggregate statistics
//! (accuracy, latency percentiles, token totals).
//! - [`report_html`] — the self-contained HTML report generator ([`report_html::generate_report`]):
//! loads persisted [`report::RunRecord`]s from a directory and writes a single offline `report.html`.
//! - [`persist`] — automatic run persistence ([`persist::save_and_report`] / [`persist::save_record`]):
//! write a run as a JSON [`report::RunRecord`] and regenerate `report.html`. Driven automatically when
//! a [`RunMeta`] carries a [`persist::Persist`] target (see [`RunMeta::persist_to`]).
//! - [`upload`] — automatic upload of a finished run to the EvalForge API (evalforge.ai), configured at
//! runtime with a project id + API key via [`RunMeta::upload_to`] / [`RunMeta::upload_from_env`] (env
//! `EVALFORGE_API_KEY`); reuses the same [`report::RunRecord`] as the request body.
//! - [`case`] — the generic, RON-authored case container [`EvalCase`] + the fail-loud [`load_cases`]
//! loader (and [`parse_cases_from_str`] for one-or-many cases per file), both generic over the host's
//! `Setup`/`Expect` types.
//! - [`baseline`](mod@baseline) — a shipped, ready-to-run baseline capability suite
//! ([`baseline()`](baseline()) / [`baseline_files`]): basic arithmetic / language / tool-use checks,
//! embedded into the crate, that a user runs against their agent in one call or copies as a template.
//! - [`harness`] — the [`Harness`] trait (the thing being benchmarked), the easy-path [`Agent`] trait,
//! [`RunArtifacts`] (what one run produced, minus scoring), and the structured [`harness::ToolCall`].
//! - [`expect`] — the built-in assertion library [`expect::Expectation`] (tool use / text / math /
//! health checks over [`RunArtifacts`]), serde/RON-authored.
//! - [`scorer`] — the [`Scorer`] trait (score one expectation against the post-run world + artifacts)
//! and the batteries-included [`BuiltinScorer`].
//! - [`runner`] — the generic engine: [`run_eval`] / [`run_eval_with_meta`] tie a [`Harness`] + a
//! [`Scorer`] over a shared world; [`run_suite`] / [`run_suite_with_meta`] are the easy path
//! ([`Agent`] + [`BuiltinScorer`]). Both run every case, time each, isolate panics, and assemble an
//! [`report::EvalReport`].
//! - [`error`] — the public [`EvalError`] surfaced by [`load_cases`] and [`Agent::run`].
//!
//! ## Advanced — the full path (custom world)
//!
//! When scoring needs post-run WORLD state, implement [`Harness`] over your agent + world, implement
//! [`Scorer`] over the same world, and call [`run_eval`]. See `examples/minimal.rs`.
pub use ;
pub use ;
pub use EvalError;
pub use Expectation;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;