eval

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package eval is a small LLM-judged eval harness: a Suite of Cases (input + judge criteria), each dispatched through an agentloop.Loop and scored by a judge llm.Client on a 0-10 scale.

This is a port of studio-apps' core/evals — extracted the same way the rest of this module was, with two simplifications specific to agentloop rather than a multi-tenant platform:

  • No OwnerID scoping. The original's Store methods took an ownerID to enforce tenant isolation across products sharing one harness. agentloop has no tenancy concept baked into its own stores (see agentloop.SessionStore) — an application that needs scoping can add it to its own Store implementation.
  • No agents.Runner / llm.Registry indirection. The original dispatched cases through a Runner interface and looked up a judge client from a Registry, because the platform runs many named agents behind one harness. Here, Service just takes an agentloop.Loop directly — its Run method already has exactly the shape a Runner provided — and an llm.Client for judging. SessionStore.Get's documented auto-create-on-unknown-ID behavior means RunSuite needs no separate session-creation step: each case gets a fresh session ID, and the Loop's own SessionStore provisions it.

Service.RunSuite dispatches every Case in a Suite through the Loop, has the judge llm.Client rate each response against the case's criteria (either a single rubric with PassThreshold, or a per-criterion CriteriaItems breakdown scored and rolled up separately), and persists the run via Store. A case that errors (agent failure or judge failure) records the error inline rather than aborting the suite — RunSuite always finishes and returns a Run.

Store is a persistence seam, not an opinion: package evalmem ships an in-memory implementation for local dev and CI; back it with whatever your application already uses for anything that needs to survive a process restart.

Index

Constants

View Source
const (
	StatusRunning   = "running"
	StatusCompleted = "completed"
	StatusFailed    = "failed"
)

Run statuses persisted on the row.

View Source
const DefaultPassThreshold int32 = 7

DefaultPassThreshold is used when AddCase is called with passThreshold <= 0.

Variables

This section is empty.

Functions

This section is empty.

Types

type Case

type Case struct {
	ID            string
	SuiteID       string
	Name          string
	Input         string
	Criteria      string
	PassThreshold int32
	CriteriaItems []CriterionItem
	CreatedAt     time.Time
}

Case is one input + judge criteria + pass threshold.

CriteriaItems is the per-criterion rubric: when non-empty, the judge rates each item separately and the case passes only when every item meets its MinScore. The legacy Criteria + PassThreshold pair stays for cases authored before the rubric existed; RunSuite falls back to the single-rubric path when CriteriaItems is empty.

type CaseResult

type CaseResult struct {
	CaseID           string            `json:"case_id"`
	CaseName         string            `json:"case_name"`
	Response         string            `json:"response"`
	Score            int               `json:"score"`
	Rationale        string            `json:"rationale"`
	Passed           bool              `json:"passed"`
	Error            string            `json:"error,omitempty"`
	CriterionResults []CriterionResult `json:"criterion_results,omitempty"`
}

CaseResult is the per-case judgment recorded on a run. Error is set (and every other field left at its zero value except CaseID/CaseName) when the agent or judge call failed — a failing case does not abort the rest of the suite.

type CriterionItem

type CriterionItem struct {
	Label    string `json:"label"`
	MinScore int    `json:"min_score,omitempty"`
}

CriterionItem is one row in a case's per-criterion rubric. Label is the human-readable assertion the judge rates; MinScore is the 0-10 threshold the item must clear to count as passed. MinScore defaults to the case's PassThreshold when zero.

type CriterionResult

type CriterionResult struct {
	Label     string `json:"label"`
	Score     int    `json:"score"`
	MinScore  int    `json:"min_score"`
	Rationale string `json:"rationale"`
	Passed    bool   `json:"passed"`
}

CriterionResult is the judge's verdict for one rubric item on one case run. Recorded inside CaseResult.CriterionResults; empty when the case used the legacy single-rubric path.

type Run

type Run struct {
	ID         string
	SuiteID    string
	Status     string
	StartedAt  time.Time
	FinishedAt *time.Time
	Results    []CaseResult
	Summary    RunSummary
}

Run is one execution of a suite — header fields plus the per-case results.

type RunSummary

type RunSummary struct {
	Total  int `json:"total"`
	Passed int `json:"passed"`
	Failed int `json:"failed"`
}

RunSummary rolls up the per-case results.

type Service

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

Service is the eval-harness entry point. CRUD methods are thin pass-throughs to the Store; RunSuite is the real logic: dispatch each case through runner, have judge rate the response, persist the run.

func NewService

func NewService(store Store, runner agentloop.Loop, judge llm.Client, redactor *redact.Redactor) *Service

NewService wires the harness. store and runner are required and panic if nil — a misconfigured harness should fail at construction, not on the first RunSuite call. judge may be nil if the harness is only used for Suite/Case CRUD (e.g. an admin UI) and RunSuite is never called.

redactor, if non-nil, is applied to the agent's response before it reaches the judge prompt or CaseResult.Response — an eval case exercises the same capabilities production traffic does, so a case whose input happens to trigger secret(...) or a credential-echoing fetch() response would otherwise send that value on to a third-party judge LLM and persist it in the stored run. Pass nil for no redaction.

func (*Service) AddCase

func (s *Service) AddCase(ctx context.Context, suiteID, name, input, criteria string, passThreshold int32, items []CriterionItem) (Case, error)

AddCase defaults passThreshold to DefaultPassThreshold when <= 0, and defaults each item's MinScore to that same threshold when unset — a caller can leave most items at the case default and override only the strict or lenient ones.

func (*Service) CreateSuite

func (s *Service) CreateSuite(ctx context.Context, name, judgeModel string) (Suite, error)

func (*Service) DeleteCase

func (s *Service) DeleteCase(ctx context.Context, id string) error

func (*Service) DeleteSuite

func (s *Service) DeleteSuite(ctx context.Context, id string) error

func (*Service) GetRun

func (s *Service) GetRun(ctx context.Context, runID string) (Run, error)

func (*Service) ListCases

func (s *Service) ListCases(ctx context.Context, suiteID string) ([]Case, error)

func (*Service) ListRuns

func (s *Service) ListRuns(ctx context.Context, suiteID string) ([]Run, error)

func (*Service) ListSuites

func (s *Service) ListSuites(ctx context.Context) ([]Suite, error)

func (*Service) RunSuite

func (s *Service) RunSuite(ctx context.Context, suiteID string) (Run, error)

RunSuite dispatches every case in the suite through runner and has judge rate each response. Synchronous — returns the finished run. For long suites the caller should set a generous ctx timeout. Errors during a single case are surfaced inside that case's result (Error field) rather than aborting the suite, so the rest of the cases still run and RunSuite still returns a completed Run.

type Store

type Store interface {
	CreateSuite(ctx context.Context, name, judgeModel string) (Suite, error)
	ListSuites(ctx context.Context) ([]Suite, error)
	GetSuite(ctx context.Context, id string) (Suite, error)
	DeleteSuite(ctx context.Context, id string) error

	AddCase(ctx context.Context, suiteID, name, input, criteria string, passThreshold int32, items []CriterionItem) (Case, error)
	ListCases(ctx context.Context, suiteID string) ([]Case, error)
	DeleteCase(ctx context.Context, id string) error

	CreateRun(ctx context.Context, suiteID string) (Run, error)
	FinishRun(ctx context.Context, id, status string, results []CaseResult, summary RunSummary) error
	ListRuns(ctx context.Context, suiteID string) ([]Run, error)
	GetRun(ctx context.Context, id string) (Run, error)
}

Store is the persistence contract an application implements to back the harness with whatever it already uses — Postgres, SQLite, evalmem.InMemoryStore for local dev/CI. All methods return domain types, never a driver-specific row.

type Suite

type Suite struct {
	ID         string
	Name       string
	JudgeModel string // passed as llm.CompletionRequest.Model for every judge call; empty uses the judge client's default
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

Suite is one collection of Cases run against the Service's configured agentloop.Loop.

Jump to

Keyboard shortcuts

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