eval

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package eval replays recorded incident cases through the investigation loop and scores whether the agent identifies the root cause — a reproducible RCA benchmark (cf. ITBench). A case records the evidence each tool returns, so the eval measures the model+loop's reasoning over fixed evidence, independent of a live cluster.

Index

Constants

This section is empty.

Variables

View Source
var Rubric = []Dimension{
	{"root_cause", 3},
	{"evidence", 3},
	{"solution", 3},
	{"description", 3},
	{"calibration", 2},
}

Rubric is the RCA-quality grading rubric (matches the design spec §5).

Functions

func GateError

func GateError(c Campaign, failUnder float64) error

GateError returns a non-nil error when the campaign pass-rate is below failUnder (which is only enforced when failUnder > 0). The message names the cases that did not reach RCA and any flaky cases, so CI logs explain the failure.

func RubricMax

func RubricMax() int

RubricMax is the maximum total score across all dimensions.

func WriteCase

func WriteCase(dir string, c Case) error

WriteCase writes a Case as <dir>/<name>.yaml.

Types

type Call

type Call struct {
	Name   string
	Args   string
	Output string
	Err    string
}

Call is one recorded tool invocation during a live investigation.

type Campaign

type Campaign struct {
	N          int
	Aggregates []CaseAggregate
}

Campaign is the aggregate of a multi-repeat replay run.

func (Campaign) FlakyNames

func (c Campaign) FlakyNames() []string

FlakyNames lists cases whose repeats disagreed too much to trust.

func (Campaign) JSON

func (c Campaign) JSON() ([]byte, error)

JSON renders the campaign as an indented report for CI artifacts.

func (Campaign) PassRate

func (c Campaign) PassRate() float64

PassRate is the fraction of cases that reached RCA (0 for empty).

func (Campaign) ReachedCases

func (c Campaign) ReachedCases() int

ReachedCases counts cases whose pass-rate met the k-of-n bar.

type Case

type Case struct {
	Name     string            `yaml:"name"`
	Prompt   string            `yaml:"prompt"` // the incident description (seeds the loop)
	Tools    map[string]string `yaml:"tools"`  // tool name -> recorded evidence the tool returns
	Expected Expected          `yaml:"expected"`
	// GroundTruth is optional live-scenario ground truth carried into replay. When
	// present it unlocks the richer scoring the model-comparison benchmark reports:
	// data-source coverage (expected_sources) and blind LLM-judge rubric grading
	// (root_cause / expected_action). Absent ⇒ keyword-only scoring, as before.
	GroundTruth *GroundTruth `yaml:"ground_truth,omitempty"`
}

Case is one replayable incident.

func Load

func Load(dir string) ([]Case, error)

Load reads every *.yaml / *.yml case in dir.

func RecordedCase

func RecordedCase(scn Scenario, calls []Call) Case

RecordedCase converts a live run's recorded tool calls into a replayable Case (the existing examples/eval format). v1 keeps the LAST output per tool: the replay staticTool returns one fixed output per tool regardless of args, so multi-call tools are flattened. submit_findings is excluded (it is the model's own output, not evidence).

type CaseAggregate

type CaseAggregate struct {
	Name        string
	Runs        int
	PassRate    float64  // fraction of repeats whose Result.Pass is true
	Reached     bool     // PassRate >= evalMinPassRate
	Flaky       bool     // PassRate in (1-evalMinPassRate, evalMinPassRate): runs disagree
	Confidence  float64  // median confidence over repeats
	Missing     []string // union of missing keywords/entities across repeats
	OverClaimed []string // union of over-claimed distractors across repeats
}

CaseAggregate is the k-of-n verdict for one case over N replay repeats.

type CompareCaseRow added in v0.3.0

type CompareCaseRow struct {
	Name     string  `json:"name"`
	Runs     int     `json:"runs"`
	PassRate float64 `json:"pass_rate"`
	Reached  bool    `json:"reached"`
	Flaky    bool    `json:"flaky"`
	Coverage float64 `json:"coverage"` // median coverage ratio over the case's runs
}

CompareCaseRow is one case's k-of-n verdict for one model, in the comparison.

type CompareSpec added in v0.3.0

type CompareSpec struct {
	Judge  *JudgeSpec   `yaml:"judge,omitempty"`
	Models []ModelEntry `yaml:"models"`
}

CompareSpec is the multi-model benchmark description: the model entries to benchmark against the replay suite, and (optionally) the judge that grades every entry. Keeping the judge in the spec makes a published comparison self-describing — the judge disclosure travels with the results.

func LoadCompareSpec added in v0.3.0

func LoadCompareSpec(path string) (CompareSpec, error)

LoadCompareSpec reads and validates a comparison spec. Unknown keys are rejected so a typo (e.g. "pricess") fails loudly instead of silently skewing a published benchmark.

type ComparedCase added in v0.3.0

type ComparedCase struct {
	Name string
	Runs []ComparedRun
}

ComparedCase is all N runs of one case for one model entry.

type ComparedRun added in v0.3.0

type ComparedRun struct {
	Result   Result
	Coverage Coverage
	Verdict  Verdict
	Graded   bool
}

ComparedRun is one replay run of one case for one model entry: the deterministic keyword score, the tool-call coverage, and (when the case carries ground truth and a judge is set) the judge's rubric verdict.

type ComparisonReport added in v0.3.0

type ComparisonReport struct {
	At     string            `json:"at"`
	N      int               `json:"n"`     // runs per case
	Judge  string            `json:"judge"` // judge model disclosure, e.g. "anthropic/claude-…"
	Models []ModelComparison `json:"models"`
}

ComparisonReport is the aggregate of a multi-model benchmark run: one row per model entry, in spec order (comparisons should not reorder by score), plus the disclosure a published benchmark needs — N runs and the judge model identity.

func NewComparisonReport added in v0.3.0

func NewComparisonReport(at string, n int, judge string, models []ModelComparison) ComparisonReport

NewComparisonReport builds a report from already-aggregated model rows, preserving the given (spec) order.

func ParseComparisonReport added in v0.3.0

func ParseComparisonReport(b []byte) (ComparisonReport, error)

ParseComparisonReport reads a report back from its JSON form (baseline diffs, tests).

func (ComparisonReport) JSON added in v0.3.0

func (r ComparisonReport) JSON() ([]byte, error)

JSON renders the deterministic machine-readable comparison report.

func (ComparisonReport) Markdown added in v0.3.0

func (r ComparisonReport) Markdown() string

Markdown renders the human comparison report: a per-model summary table (rubric breakdown, pass rate, coverage, confident-wrong, tokens, optional cost) followed by a per-case pass-rate matrix. Deterministic: models keep spec order, cases sort by name.

type ComparisonRunner added in v0.3.0

type ComparisonRunner struct {
	Model providers.ModelProvider // the entry under test (wrap with CountingModel for token totals)
	Judge Judge                   // fixed across entries; nil skips rubric grading
	Log   *slog.Logger
}

ComparisonRunner benchmarks one model entry over the replay cases. It mirrors the replay Runner (static tools, same loop) but additionally records tool calls for coverage and grades every run with a fixed judge, so entries can be compared on the full rubric — not only keyword pass/fail.

func (*ComparisonRunner) RunCases added in v0.3.0

func (cr *ComparisonRunner) RunCases(ctx context.Context, cases []Case, n int) []ComparedCase

RunCases replays every case n times against the entry's model.

type CountingModel added in v0.3.0

type CountingModel struct {
	Inner providers.ModelProvider
	// contains filtered or unexported fields
}

CountingModel wraps a ModelProvider and sums the provider-reported token usage across completions. The loop only logs/meters each response's Usage, so this wrapper is what turns per-response usage into a per-benchmark total.

func (*CountingModel) Complete added in v0.3.0

Complete delegates to Inner and accumulates the response usage on success.

func (*CountingModel) Total added in v0.3.0

func (c *CountingModel) Total() providers.Usage

Total returns the usage accumulated so far.

type Coverage

type Coverage struct {
	Touched     []string // mandatory source groups actually exercised
	Missing     []string // mandatory groups never touched
	Bonus       []string // optional groups touched
	CrossSignal bool     // >=2 distinct source groups exercised
	ToolErrors  []string // distinct tool names that returned an error
	Ratio       float64  // |touched| / |expected|  (1.0 when no expected sources)
}

Coverage is the deterministic data-source coverage result for one run.

func ScoreCoverage

func ScoreCoverage(expected, optional []string, calls []Call) Coverage

ScoreCoverage computes coverage of the mandatory expected sources from the recorded calls. optional sources count as Bonus and never affect Ratio.

type Dimension

type Dimension struct {
	Key string
	Max int
}

Dimension is one rubric axis and its max score.

type Expected

type Expected struct {
	MustContain       []string `yaml:"must_contain"`        // keywords that must appear in the findings (recall, over full findings text)
	MinConfidence     float64  `yaml:"min_confidence"`      // confidence floor (0 = no floor)
	RootCauseEntities []string `yaml:"root_cause_entities"` // entities that MUST be named as the cause (entity recall, over claim text)
	Distractors       []string `yaml:"distractors"`         // plausible-but-wrong entities that must NOT be blamed (over-claim/FP); only evaluated when root_cause_entities is non-empty
}

Expected is the RCA scoring spec for a case.

type GroundTruth

type GroundTruth struct {
	RootCause       string   `yaml:"root_cause"`
	ExpectedSources []string `yaml:"expected_sources"` // MANDATORY data-source groups -> coverage gate
	OptionalSources []string `yaml:"optional_sources"` // bonus if touched, never gates
	ExpectedAction  string   `yaml:"expected_action"`
	MustReachRoot   bool     `yaml:"must_reach_root"`
}

GroundTruth is the human-authored truth a scenario is graded against.

type Judge

type Judge interface {
	Grade(ctx context.Context, scn Scenario, inv providers.Investigation) (Verdict, error)
}

Judge grades an investigation against a scenario's ground truth.

type JudgeSpec added in v0.3.0

type JudgeSpec struct {
	Provider  string `yaml:"provider"`
	BaseURL   string `yaml:"base_url"`
	Model     string `yaml:"model"`
	APIKeyEnv string `yaml:"api_key_env"`
}

JudgeSpec identifies the (single, fixed) judge model used for every entry. Grading is blind: the judge never sees which entry produced an investigation.

type LiveReport

type LiveReport struct {
	At      string       `json:"at"`
	N       int          `json:"n"`   // runs per scenario (live mode); 0 in replay mode
	Ran     int          `json:"ran"` // scenarios actually investigated (not skipped)
	Passed  int          `json:"passed"`
	Skipped int          `json:"skipped"`
	Results []LiveResult `json:"results"`
}

LiveReport is the serializable output of one live-fire campaign run.

func NewLiveReport

func NewLiveReport(at string, n int, results []LiveResult) LiveReport

NewLiveReport tallies results into a report.

func (LiveReport) JSON

func (rep LiveReport) JSON() []byte

JSON is the machine-readable sibling of the markdown report.

func (LiveReport) Markdown

func (rep LiveReport) Markdown() string

Markdown renders the human report: summary, per-scenario table, coverage heatmap.

func (LiveReport) RegressionsVS

func (rep LiveReport) RegressionsVS(prev LiveReport) []string

RegressionsVS returns scenarios that passed in prev but fail/skip now.

type LiveResult

type LiveResult struct {
	Scenario       string
	Skipped        bool
	SkipReason     string
	Runs           []RunOutcome
	CoverageRatio  float64        // median
	DimMedian      map[string]int // median per rubric dimension
	DimVariance    map[string]float64
	ConfidentWrong bool     // any run confident-wrong
	Flaky          bool     // root_cause scores vary too much across runs to trust
	ToolErrors     []string // union across runs
	Pass           bool
}

LiveResult aggregates the N runs of one scenario.

type LiveRunner

type LiveRunner struct {
	Model     providers.ModelProvider
	BaseTools []investigate.Tool
	Judge     Judge
	Steps     StepRunner
	Log       *slog.Logger
	N         int                    // runs per scenario (default 1 if 0)
	OnRecord  func(Scenario, []Call) // optional: persist the run's calls (replay corpus)
	Recall    *investigate.Recall    // optional; when set, runOnce takes the instant-recall short-circuit (production path). nil ⇒ no recall.
}

LiveRunner runs scenarios against real tools, grading coverage + RCA quality. BaseTools and Model are the LIVE tools/model (built by cmd/lore via buildModelAndTools); Judge uses a separate, stronger model.

func (*LiveRunner) RunScenario

func (lr *LiveRunner) RunScenario(ctx context.Context, scn Scenario) LiveResult

RunScenario runs setup (or precheck), N investigations, judging each, then always tears down. Pass gate: at least evalMinPassRate of runs reach root_cause >= evalRootCauseBar, coverage median == 1.0, no confident-wrong run, and root_cause variance within evalMaxRootCauseVariance (not flaky).

type ModelComparison added in v0.3.0

type ModelComparison struct {
	Name     string `json:"name"` // the entry's report label
	Provider string `json:"provider"`
	Model    string `json:"model"`
	Effort   string `json:"effort,omitempty"`

	// Aggregate scores.
	PassRate       float64            `json:"pass_rate"`               // fraction of cases that reached the k-of-n bar
	Reached        int                `json:"reached"`                 // cases whose pass-rate met the k-of-n bar
	Total          int                `json:"total"`                   // cases run
	RubricMedian   map[string]float64 `json:"rubric_median,omitempty"` // per-dimension median over graded runs; nil when ungraded
	GradedRuns     int                `json:"graded_runs"`             // runs the judge graded (0 ⇒ rubric omitted)
	CoverageMedian float64            `json:"coverage_median"`         // median coverage ratio over all runs
	ConfidentWrong int                `json:"confident_wrong"`         // graded runs flagged confident-and-wrong

	// Usage + cost.
	InputTokens  int      `json:"input_tokens"`
	OutputTokens int      `json:"output_tokens"`
	CostUSD      *float64 `json:"cost_usd,omitempty"` // present only when the entry supplied prices

	Cases []CompareCaseRow `json:"cases"`
}

ModelComparison is one model entry's aggregated result across all cases.

func AggregateModel added in v0.3.0

func AggregateModel(entry ModelEntry, cases []ComparedCase, usage providers.Usage) ModelComparison

AggregateModel folds a model entry's per-case replay runs into one comparison row. Per-case pass uses the same k-of-n bar as the single-run campaign; rubric medians and confident-wrong are computed over every graded run; coverage is the median over every run; cost is computed only when the entry supplied prices. Case rows come out sorted by name so two reports diff cleanly.

type ModelEntry added in v0.3.0

type ModelEntry struct {
	Name      string `yaml:"name"`     // report label; must be unique
	Provider  string `yaml:"provider"` // "openai" (default) | "anthropic" | "gemini"
	BaseURL   string `yaml:"base_url"`
	Model     string `yaml:"model"`
	APIKeyEnv string `yaml:"api_key_env"` // env var holding the API key (empty = keyless)
	// Effort is the OpenAI-compatible reasoning_effort request field (e.g. "low",
	// "medium", "high"). Only valid for the openai provider; other providers reject it.
	Effort string `yaml:"effort,omitempty"`
	// Prices enables the estimated-cost column; omit it to omit the column.
	Prices *Prices `yaml:"prices,omitempty"`
}

ModelEntry is one model configuration to benchmark.

type ModelJudge

type ModelJudge struct {
	Model providers.ModelProvider
}

ModelJudge grades with an LLM (use a stronger model than the one under test).

func (ModelJudge) Grade

Grade builds a blind grading prompt and parses the JSON verdict.

type Prices added in v0.3.0

type Prices struct {
	InputUSD  float64 `yaml:"input_usd" json:"input_usd"`
	OutputUSD float64 `yaml:"output_usd" json:"output_usd"`
}

Prices is optional per-million-token (MTok) pricing for cost estimation.

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder collects tool calls made during one investigation run. Safe for concurrent use (the loop is sequential today, but tools may fan out later).

func (*Recorder) Calls

func (r *Recorder) Calls() []Call

Calls returns a copy of the recorded calls in order.

type Report

type Report struct {
	Results []Result
}

Report aggregates case results.

func (Report) Passed

func (r Report) Passed() int

Passed counts cases whose root cause was identified.

func (Report) RCARate

func (r Report) RCARate() float64

RCARate is the fraction of cases whose root cause was identified.

type Result

type Result struct {
	Name        string
	Pass        bool
	Confidence  float64
	Missing     []string // expected keywords/entities not found (or an error note); includes "over-claimed: <e>" markers
	OverClaimed []string // distractor entities the investigation wrongly blamed (over-claim/FP)
}

Result is the score for one case.

func Score

func Score(name string, inv providers.Investigation, exp Expected) Result

Score reports whether the investigation identifies the expected root cause. Keyword recall (must_contain) is matched over the full findings text. Entity scoring — recall over root_cause_entities and an over-claim penalty over distractors — is matched over the CLAIM text only (what was blamed), and engages only when root_cause_entities is set. A case passes when nothing is missing, no distractor was blamed, and confidence meets the floor.

type RunOutcome

type RunOutcome struct {
	Investigation providers.Investigation
	Coverage      Coverage
	Verdict       Verdict
}

RunOutcome is one of the N runs of a scenario.

type Runner

type Runner struct {
	Model providers.ModelProvider
	Log   *slog.Logger
}

Runner replays cases through the investigation loop with a given model.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, cases []Case) Report

Run replays every case and scores it.

func (*Runner) RunN

func (r *Runner) RunN(ctx context.Context, cases []Case, n int) Campaign

RunN replays every case n times and returns the aggregated campaign.

type Scenario

type Scenario struct {
	ID          string      `yaml:"id"`
	Category    string      `yaml:"category"` // what-changed | saturation | network | cloud | dependency | cert | dns | storage | instant-recall
	Description string      `yaml:"description"`
	Invasive    bool        `yaml:"invasive"` // true => has setup/teardown; false => natural failure
	Precheck    string      `yaml:"precheck"` // optional shell; non-zero exit => SKIP (natural scenarios)
	Setup       []string    `yaml:"setup"`    // shell steps (kubectl/flux) to induce the fault
	Trigger     Trigger     `yaml:"trigger"`
	GroundTruth GroundTruth `yaml:"ground_truth"`
	Teardown    []string    `yaml:"teardown"` // shell steps to revert; always run
}

Scenario is one live-fire eval case: an induced-or-natural failure, how to trigger an investigation, and the ground truth to grade against. It is a superset of Case (which is the recorded/replay form this produces).

func LoadScenarios

func LoadScenarios(dir string) ([]Scenario, error)

LoadScenarios reads every *.yaml / *.yml scenario in dir.

type StepRunner

type StepRunner interface {
	Run(ctx context.Context, step string) error
}

StepRunner executes a scenario's shell setup/teardown/precheck steps. The real implementation shells out (kubectl/flux); tests use a fake.

type Trigger

type Trigger struct {
	Mode      string `yaml:"mode"`      // "cli" (default) | "webhook"
	Symptom   string `yaml:"symptom"`   // free-text incident description
	Namespace string `yaml:"namespace"` // affected namespace (optional)
}

Trigger describes how the investigation is started.

type Verdict

type Verdict struct {
	Scores         map[string]int `json:"scores"`
	ConfidentWrong bool           `json:"confident_wrong"`
	Rationale      string         `json:"rationale"`
}

Verdict is the judge's structured grade for one investigation.

func (Verdict) Total

func (v Verdict) Total() int

Total sums the dimension scores.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL