local Binding = require("evalframe.model.binding")
local std = require("evalframe.std")
local M = {}
local function normalize_response(raw, elapsed_ms)
if type(raw) == "string" then
return { text = raw, latency_ms = elapsed_ms }
end
if type(raw) ~= "table" then
return {
text = "",
latency_ms = elapsed_ms,
error = string.format("provider returned %s (expected string or table)", type(raw)),
}
end
local resp = {}
for k, v in pairs(raw) do
resp[k] = v
end
resp.text = type(raw.text) == "string" and raw.text or ""
resp.latency_ms = raw.latency_ms or elapsed_ms
return resp
end
local function call_provider(provider, case)
local start = std.time()
local ok, raw = pcall(provider, case.input)
local elapsed = (std.time() - start) * 1000
if not ok then
return {
text = "",
latency_ms = elapsed,
error = tostring(raw),
}
end
return normalize_response(raw, elapsed)
end
local function evaluate_binding(binding, response, case)
local raw_grade, err = binding.grader.check(response, case)
if err then
return {
grader = binding.grader.name,
score = 0.0,
grade = nil,
error = err,
weight = binding.weight,
}
end
local score = binding.scorer.score(raw_grade)
local warning
if raw_grade ~= nil and raw_grade ~= false and score == 0.0 then
local gt = type(raw_grade)
if gt == "string" or (gt == "table") then
warning = string.format(
"grader '%s' returned %s but scorer produced 0.0 (type mismatch?)",
binding.grader.name, gt
)
end
end
return {
grader = binding.grader.name,
score = score,
grade = raw_grade,
weight = binding.weight,
warning = warning,
}
end
local function eval_case(bindings, case, provider)
local response = call_provider(provider, case)
local grades = {}
local total_score = 0.0
local total_weight = 0.0
for _, b in ipairs(bindings) do
local result = evaluate_binding(b, response, case)
grades[#grades + 1] = result
total_score = total_score + result.score * result.weight
total_weight = total_weight + result.weight
end
local weighted_score = total_weight > 0 and (total_score / total_weight) or 0.0
return {
case = case,
response = response,
grades = grades,
score = weighted_score,
passed = weighted_score >= 1.0 - 1e-6,
}
end
function M.run(bindings, cases, provider)
if type(provider) ~= "function" then
error("runner.run: provider must be a function", 2)
end
local results = {}
for _, case in ipairs(cases) do
results[#results + 1] = eval_case(bindings, case, provider)
end
return results
end
return M