Skip to main content

eval_core/
lib.rs

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