evals

package
v0.9.13 Latest Latest
Warning

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

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

Documentation

Overview

Package evals provides observers and validation helpers for evaluating completed agent traces.

Tracing itself lives in chainguard.dev/driftlessaf/agents/agenttrace; this package layers evaluation on top. An evaluation is an ObservableTraceCallback — a function receiving an Observer and a completed agenttrace.Trace[T] — that reports outcomes through the Observer (Fail, Log, Grade, Increment, Total).

Observers

  • NamespacedObserver: hierarchical namespaces ("accuracy", "reliability", ...) built from a factory function
  • ResultCollector: wraps another Observer and collects failure messages and Grades for later inspection
  • MetricsObserver: publishes evaluation counts, failures, and grades as Prometheus metrics
  • testevals.New / testevals.NewPrefix (subpackage): adapt *testing.T to the Observer interface

Validation helpers

Ready-made ObservableTraceCallbacks cover common checks:

// Tool call counts
evals.ExactToolCalls[string](2)
// (also MinimumNToolCalls, RangeToolCalls, NoToolCalls)

// Tool usage constraints
evals.RequiredToolCalls[string]([]string{"search", "analyze"})
evals.OnlyToolCalls[string]("search", "analyze")

// No trace-level errors
evals.NoErrors[string]()

// Custom validation of the trace result
evals.ResultValidator[string](func(result string) error { ... })

Wiring evaluations into a tracer

Inject binds an Observer to an ObservableTraceCallback, producing an agenttrace.TraceCallback that runs when a trace completes:

obs := evals.NewNamespacedObserver(func(name string) evals.Observer {
	return customObserver(name)
})
tracer := agenttrace.ByCode[string](
	evals.Inject[string](obs.Child("tool-calls"), evals.ExactToolCalls[string](1)),
	evals.Inject[string](obs.Child("reliability"), evals.NoErrors[string]()),
)
ctx = agenttrace.WithTracer[string](ctx, tracer)

BuildCallbacks and BuildTracer do the same for a map of named evaluations, namespacing each entry under the observer.

Reporting

The report subpackage turns a NamespacedObserver[*ResultCollector] tree into markdown evaluation reports.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildCallbacks

func BuildCallbacks[T any, O Observer](observer *NamespacedObserver[O], evalMap map[string]ObservableTraceCallback[T]) []agenttrace.TraceCallback[T]

BuildCallbacks creates a list of TraceCallbacks from a namespaced observer and evaluation map. This helper injects each evaluation function with a child observer to create TraceCallbacks that can be used with ByCode or other tracers.

func BuildTracer

func BuildTracer[T any, O Observer](observer *NamespacedObserver[O], evalMap map[string]ObservableTraceCallback[T]) agenttrace.Tracer[T]

BuildTracer creates a ByCode tracer from a namespaced observer and evaluation map. This helper consolidates the common pattern of setting up comprehensive evaluation tracers by injecting each evaluation function with a child observer and building a ByCode tracer from the resulting callbacks.

func Inject

func Inject[T any](obs Observer, callback ObservableTraceCallback[T]) agenttrace.TraceCallback[T]

Inject creates a TraceCallback by injecting an Observer implementation into an ObservableTraceCallback

Types

type Grade

type Grade struct {
	Score     float64
	Reasoning string
}

Grade represents a grade with score and reasoning

type MetricsObserver

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

MetricsObserver implements Observer interface with Prometheus metrics

func NewMetricsObserver

func NewMetricsObserver[T any](namespace string) *MetricsObserver

NewMetricsObserver creates a metrics observer for the given tracer type and namespace

func (*MetricsObserver) Fail

func (m *MetricsObserver) Fail(msg string)

Fail implements Observer.Fail

func (*MetricsObserver) Grade

func (m *MetricsObserver) Grade(score float64, reasoning string)

Grade implements Observer.Grade

func (*MetricsObserver) Increment

func (m *MetricsObserver) Increment()

Increment implements Observer.Increment

func (*MetricsObserver) Log

func (m *MetricsObserver) Log(msg string)

Log implements Observer.Log (no-op for metrics observer)

func (*MetricsObserver) Total

func (m *MetricsObserver) Total() int64

Total implements Observer.Total

type NamespacedObserver

type NamespacedObserver[T Observer] struct {
	// contains filtered or unexported fields
}

NamespacedObserver provides hierarchical namespacing for Observer instances

func NewNamespacedObserver

func NewNamespacedObserver[T Observer](factory func(string) T) *NamespacedObserver[T]

NewNamespacedObserver creates a new root NamespacedObserver with the given factory function

func (*NamespacedObserver[T]) Child

func (n *NamespacedObserver[T]) Child(name string) *NamespacedObserver[T]

Child returns the child namespace with the given name, creating it if necessary

func (*NamespacedObserver[T]) Fail

func (n *NamespacedObserver[T]) Fail(msg string)

Fail delegates to the inner Observer instance

func (*NamespacedObserver[T]) Grade

func (n *NamespacedObserver[T]) Grade(score float64, reasoning string)

Grade delegates to the inner Observer instance

func (*NamespacedObserver[T]) Increment

func (n *NamespacedObserver[T]) Increment()

Increment delegates to the inner Observer instance

func (*NamespacedObserver[T]) Log

func (n *NamespacedObserver[T]) Log(msg string)

Log delegates to the inner Observer instance

func (*NamespacedObserver[T]) Total

func (n *NamespacedObserver[T]) Total() int64

Total delegates to the inner Observer instance

func (*NamespacedObserver[T]) Walk

func (n *NamespacedObserver[T]) Walk(visitor func(string, T))

Walk traverses the observer tree in depth-first order, calling the visitor function on the current node first, then on all children in sorted order by name

type ObservableTraceCallback

type ObservableTraceCallback[T any] func(Observer, *agenttrace.Trace[T])

ObservableTraceCallback is a function that receives an Observer interface and completed traces

func ExactToolCalls

func ExactToolCalls[T any](n int) ObservableTraceCallback[T]

ExactToolCalls returns an ObservableTraceCallback that validates the trace has exactly n tool calls.

Example

ExampleExactToolCalls shows how to validate exact tool call counts

// Create a mock observer
obs := &mockObserver{}

// Use ExactToolCalls to validate exactly 2 tool calls
evalCallback := evals.ExactToolCalls[string](2)

// Create tracer with the evaluation
tracer := agenttrace.ByCode[string](evals.Inject(obs, evalCallback))

// Create trace with exactly 2 tool calls
ctx := agenttrace.WithTracer[string](context.Background(), tracer)
trace, done := agenttrace.StartTrace[string](ctx, "Analyze logs")

// Add exactly 2 tool calls
tc1 := trace.StartToolCall("tc1", "read_logs", nil)
tc1.Complete("log data", nil)

tc2 := trace.StartToolCall("tc2", "analyze", nil)
tc2.Complete("analysis done", nil)

// Complete trace via done callback (triggers evaluation)
done("Analysis complete", nil)

if len(obs.failures) == 0 {
	fmt.Println("Validation passed: exactly 2 tool calls")
}
Output:
Validation passed: exactly 2 tool calls

func MinimumNToolCalls

func MinimumNToolCalls[T any](n int) ObservableTraceCallback[T]

MinimumNToolCalls returns an ObservableTraceCallback that validates the trace has at least n tool calls.

func NoErrors

func NoErrors[T any](ignore ...func(error) bool) ObservableTraceCallback[T]

NoErrors returns an ObservableTraceCallback that validates no tool calls resulted in errors. Optional ignore functions can be provided to filter out expected errors (e.g., file not found). If any ignore function returns true for a given error, that error is skipped.

A suspended trace (Trace.Suspended) never fails: suspension is an intentional mid-run halt awaiting an out-of-band signal, not a failure, and the resumed run is graded on its own trace.

Example

ExampleNoErrors shows how to validate no errors occurred

// Create a mock observer
obs := &mockObserver{}

evalCallback := evals.NoErrors[string]()

// Create tracer with the evaluation
tracer := agenttrace.ByCode[string](evals.Inject(obs, evalCallback))

// Create successful trace with no errors
ctx := agenttrace.WithTracer[string](context.Background(), tracer)
trace, done := agenttrace.StartTrace[string](ctx, "Read and analyze")

// Add successful tool calls
tc1 := trace.StartToolCall("tc1", "read_logs", nil)
tc1.Complete("log content", nil)

tc2 := trace.StartToolCall("tc2", "analyze", nil)
tc2.Complete("analysis complete", nil)

// Complete trace successfully via done callback (triggers evaluation)
done("Processing complete", nil)

if len(obs.failures) == 0 {
	fmt.Println("No errors found")
}
Output:
No errors found

func NoToolCalls

func NoToolCalls[T any]() ObservableTraceCallback[T]

NoToolCalls returns an ObservableTraceCallback that validates the trace has no tool calls.

func OnlyToolCalls

func OnlyToolCalls[T any](toolNames ...string) ObservableTraceCallback[T]

OnlyToolCalls returns an ObservableTraceCallback that validates the trace only uses the specified tool names.

func RangeToolCalls

func RangeToolCalls[T any](min, max int) ObservableTraceCallback[T]

RangeToolCalls returns an ObservableTraceCallback that validates the trace has between min and max tool calls (inclusive).

func RequiredToolCalls

func RequiredToolCalls[T any](toolNames []string) ObservableTraceCallback[T]

RequiredToolCalls returns an ObservableTraceCallback that validates the trace uses all of the specified tool names at least once.

Example

ExampleRequiredToolCalls shows how to ensure specific tools are called

// Create a mock observer
obs := &mockObserver{}

// Require both read_logs and analyze to be called
evalCallback := evals.RequiredToolCalls[string]([]string{"read_logs", "analyze"})

// Create tracer with the evaluation
tracer := agenttrace.ByCode[string](evals.Inject(obs, evalCallback))

// Create trace and add required tools (plus extra)
ctx := agenttrace.WithTracer[string](context.Background(), tracer)
trace, done := agenttrace.StartTrace[string](ctx, "Process data")

// Add required tools
tc1 := trace.StartToolCall("tc1", "read_logs", nil)
tc1.Complete("log data", nil)

tc2 := trace.StartToolCall("tc2", "analyze", nil)
tc2.Complete("analysis done", nil)

// Add extra tool (should be fine)
tc3 := trace.StartToolCall("tc3", "summarize", nil)
tc3.Complete("summary", nil)

// Complete trace via done callback (triggers evaluation)
done("Processing complete", nil)

if len(obs.failures) == 0 {
	fmt.Println("All required tools were called")
}
Output:
All required tools were called

func ResultValidator

func ResultValidator[T any](validator func(result T) error) ObservableTraceCallback[T]

ResultValidator returns an ObservableTraceCallback that validates the result using a custom validator. The validator is only called if the result is non-nil. T should typically be a pointer type like *MyStruct.

type Observer

type Observer interface {
	// Fail marks the evaluation as failed with the given message
	// Should be called at most once per Trace evaluation
	Fail(string)
	// Log logs a message
	// Can be called multiple times per Trace evaluation
	Log(string)
	// Grade assigns a rating (0.0-1.0) with reasoning to the trace result
	// Should be called at most once per Trace evaluation
	Grade(score float64, reasoning string)
	// Increment is called each time a trace is evaluated
	Increment()
	// Total returns the number of observed instances
	Total() int64
}

Observer defines an interface for observing and controlling evaluation execution

type ResultCollector

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

ResultCollector wraps an Observer to collect failure messages and grades

Example
package main

import (
	"context"
	"fmt"
	"sync"

	"chainguard.dev/driftlessaf/agents/agenttrace"
	"chainguard.dev/driftlessaf/agents/evals"
)

// exampleObserver implements Observer for examples with thread-safety
type exampleObserver struct {
	failures []string
	logs     []string
	count    int64
	mu       sync.Mutex
}

func (m *exampleObserver) Fail(msg string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.failures = append(m.failures, msg)
}

func (m *exampleObserver) Log(msg string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.logs = append(m.logs, msg)
}

func (m *exampleObserver) Grade(score float64, reasoning string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.logs = append(m.logs, fmt.Sprintf("Grade: %.2f - %s", score, reasoning))
}

func (m *exampleObserver) Increment() {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.count++
}

func (m *exampleObserver) Total() int64 {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.count
}

func main() {
	// Create a mock observer to demonstrate the pattern
	baseObs := &exampleObserver{}

	// Wrap it with a result collector
	collector := evals.NewResultCollector(baseObs)

	// Define an evaluation callback that validates tool calls
	callback := func(o evals.Observer, trace *agenttrace.Trace[string]) {
		o.Log("Analyzing trace")

		if len(trace.ToolCalls) != 1 {
			o.Fail("Expected exactly 1 tool call")
		}

		if trace.Error != nil {
			o.Fail("Unexpected error: " + trace.Error.Error())
		}

		// Give the trace a grade
		o.Grade(0.85, "Good tool usage")
	}

	// Create tracer with the collector
	tracer := agenttrace.ByCode[string](evals.Inject(collector, callback))

	// Create a trace that will trigger the evaluation
	ctx := agenttrace.WithTracer[string](context.Background(), tracer)
	trace, done := agenttrace.StartTrace[string](ctx, "Process data")

	// Add a tool call
	tc := trace.StartToolCall("tc1", "data-processor", map[string]any{
		"input": "some data",
	})
	tc.Complete("processed", nil)

	// Complete the trace via done callback (this triggers the evaluation)
	done("Processing complete", nil)

	// Check collected results
	failures := collector.Failures()
	grades := collector.Grades()

	fmt.Printf("Failures: %d\n", len(failures))
	fmt.Printf("Grades: %d (score: %.2f)\n", len(grades), grades[0].Score)
}
Output:
Failures: 0
Grades: 1 (score: 0.85)
Example (WithNamespacedObserver)
package main

import (
	"context"
	"errors"
	"fmt"
	"sync"

	"chainguard.dev/driftlessaf/agents/agenttrace"
	"chainguard.dev/driftlessaf/agents/evals"
)

// exampleObserver implements Observer for examples with thread-safety
type exampleObserver struct {
	failures []string
	logs     []string
	count    int64
	mu       sync.Mutex
}

func (m *exampleObserver) Fail(msg string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.failures = append(m.failures, msg)
}

func (m *exampleObserver) Log(msg string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.logs = append(m.logs, msg)
}

func (m *exampleObserver) Grade(score float64, reasoning string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.logs = append(m.logs, fmt.Sprintf("Grade: %.2f - %s", score, reasoning))
}

func (m *exampleObserver) Increment() {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.count++
}

func (m *exampleObserver) Total() int64 {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.count
}

func main() {
	// Create a namespaced observer using mock observers
	namespacedObs := evals.NewNamespacedObserver(func(name string) evals.Observer {
		return &exampleObserver{}
	})

	// Create result collectors for different namespaces
	toolCollector := evals.NewResultCollector(namespacedObs.Child("tools"))
	errorCollector := evals.NewResultCollector(namespacedObs.Child("errors"))

	// Define evaluations for tool calls
	toolEval := func(o evals.Observer, trace *agenttrace.Trace[string]) {
		for _, tc := range trace.ToolCalls {
			if tc.Error != nil {
				o.Fail(fmt.Sprintf("Tool %s failed: %v", tc.Name, tc.Error))
			}
		}
	}

	// Define evaluations for trace errors
	errorEval := func(o evals.Observer, trace *agenttrace.Trace[string]) {
		if trace.Error != nil {
			o.Fail("Trace error: " + trace.Error.Error())
		}
	}

	// Create tracer with multiple collectors
	tracer := agenttrace.ByCode[string](
		evals.Inject(toolCollector, toolEval),
		evals.Inject(errorCollector, errorEval),
	)

	// Create a trace with a failing tool call
	ctx := agenttrace.WithTracer[string](context.Background(), tracer)
	trace, done := agenttrace.StartTrace[string](ctx, "Complex analysis")

	tc := trace.StartToolCall("tc1", "analyzer", nil)
	tc.Complete(nil, errors.New("analysis failed"))

	// Complete the trace via done callback (this triggers both evaluations)
	done("Analysis complete", nil)

	// Check failures by category
	toolFailures := toolCollector.Failures()
	errorFailures := errorCollector.Failures()

	fmt.Printf("Tool failures: %d\n", len(toolFailures))
	fmt.Printf("Error failures: %d\n", len(errorFailures))
}
Output:
Tool failures: 1
Error failures: 0

func NewResultCollector

func NewResultCollector(inner Observer) *ResultCollector

NewResultCollector creates a new ResultCollector that wraps the given Observer

func (*ResultCollector) Fail

func (r *ResultCollector) Fail(msg string)

Fail logs the failure message and stores it in the failures list

func (*ResultCollector) Failures

func (r *ResultCollector) Failures() []string

Failures returns a copy of all collected failure messages

func (*ResultCollector) Grade

func (r *ResultCollector) Grade(score float64, reasoning string)

Grade passes through to the inner observer and stores the grade

func (*ResultCollector) Grades

func (r *ResultCollector) Grades() []Grade

Grades returns a copy of all collected grades

func (*ResultCollector) Increment

func (r *ResultCollector) Increment()

Increment passes through to the inner observer

func (*ResultCollector) Log

func (r *ResultCollector) Log(msg string)

Log passes through to the inner observer

func (*ResultCollector) Total

func (r *ResultCollector) Total() int64

Total passes through to the inner observer

Directories

Path Synopsis
Package report provides report generation functionality for evaluation results.
Package report provides report generation functionality for evaluation results.
Package testevals provides a testing.T adapter for the evals framework.
Package testevals provides a testing.T adapter for the evals framework.

Jump to

Keyboard shortcuts

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