use eval_core::{Agent, EvalCase, EvalError, Expectation, RunArtifacts, ToolCall, run_suite};
use serde_json::json;
struct CalculatorAgent;
impl CalculatorAgent {
fn parse_arithmetic(instruction: &str) -> Option<(&'static str, f64, f64, f64)> {
fn numbers_in(s: &str) -> Vec<f64> {
let mut nums = Vec::new();
let mut cur = String::new();
for ch in s.chars() {
if ch.is_ascii_digit() || (ch == '.' && !cur.contains('.')) {
cur.push(ch);
} else if !cur.is_empty() {
if let Ok(n) = cur.parse::<f64>() {
nums.push(n);
}
cur.clear();
}
}
if let Ok(n) = cur.parse::<f64>() {
nums.push(n); }
nums
}
fn extract_first_number(s: &str) -> Option<f64> {
numbers_in(s).into_iter().next()
}
fn extract_last_number(s: &str) -> Option<f64> {
numbers_in(s).into_iter().last()
}
for (sym, name) in [("+", "add"), ("-", "sub"), ("*", "mul"), ("/", "div")] {
if let Some((lhs, rhs)) = instruction.split_once(sym) {
let a = extract_last_number(lhs)?;
let b = extract_first_number(rhs)?;
let result = match name {
"add" => a + b,
"sub" => a - b,
"mul" => a * b,
_ => a / b,
};
return Some((name, a, b, result));
}
}
None
}
}
impl Agent for CalculatorAgent {
fn run(&self, instruction: &str) -> Result<RunArtifacts, EvalError> {
match Self::parse_arithmetic(instruction) {
Some((op, a, b, result)) => Ok(RunArtifacts::new()
.with_tool_calls(vec![ToolCall::new(
"calculator",
json!({ "op": op, "a": a, "b": b }),
)])
.with_final_text(format!("The answer is {result}."))),
None => Ok(RunArtifacts::new().with_final_text(format!(
"Hello! I can do math, but I can't help with: {instruction}"
))),
}
}
}
fn main() {
let cases: Vec<EvalCase<(), Expectation>> = vec![
EvalCase {
name: "adds-two-numbers".to_owned(),
instruction: "what is 2 + 2?".to_owned(),
setup: (),
expect: vec![
Expectation::CalledToolWith {
tool: "calculator".to_owned(),
args: json!({ "op": "add" }),
},
Expectation::FinalNumberEquals {
value: 4.0,
tolerance: 0.0,
},
],
},
EvalCase {
name: "divides-with-tolerance".to_owned(),
instruction: "compute 10 / 3".to_owned(),
setup: (),
expect: vec![
Expectation::CalledTool {
tool: "calculator".to_owned(),
},
Expectation::FinalNumberEquals {
value: 3.333,
tolerance: 0.01,
},
Expectation::FinalTextContains {
text: "the answer".to_owned(),
case_insensitive: true,
},
],
},
EvalCase {
name: "no-tools-for-chitchat".to_owned(),
instruction: "hello there".to_owned(),
setup: (),
expect: vec![Expectation::NoToolCalls],
},
EvalCase {
name: "wrong-op-expectation-fails".to_owned(),
instruction: "what is 2 + 2?".to_owned(),
setup: (),
expect: vec![Expectation::CalledToolWith {
tool: "calculator".to_owned(),
args: json!({ "op": "sub" }),
}],
},
];
let report = run_calculator_suite(&cases);
println!("{report}");
assert_eq!(report.total(), 4, "ran all four cases");
assert_eq!(report.passed(), 3, "exactly three cases pass");
assert!(report.outcomes[0].passed, "adds-two-numbers passes");
assert!(report.outcomes[1].passed, "divides-with-tolerance passes");
assert!(report.outcomes[2].passed, "no-tools-for-chitchat passes");
assert!(!report.outcomes[3].passed, "wrong-op-expectation fails");
assert!(
report.outcomes[0].tool_calls[0].starts_with("calculator("),
"tool call display string derived from the structured ToolCall: {:?}",
report.outcomes[0].tool_calls
);
println!("\ncalculator example OK: 3 passed, 1 failed as expected");
}
fn run_calculator_suite(cases: &[EvalCase<(), Expectation>]) -> eval_core::report::EvalReport {
use eval_core::{RunMeta, run_suite_with_meta};
match std::env::var("EVALFORGE_PROJECT_ID") {
Ok(project_id) if !project_id.is_empty() => {
let meta = RunMeta::new(0.0, "example: calculator", "")
.upload_from_env(project_id)
.upload_model("calculator-example");
run_suite_with_meta(&CalculatorAgent, cases, meta)
}
_ => run_suite(&CalculatorAgent, cases),
}
}