Current section

Files

Jump to
nous lib nous eval evaluator.ex
Raw

lib/nous/eval/evaluator.ex

defmodule Nous.Eval.Evaluator do
@moduledoc """
Behaviour for evaluating agent outputs against expected results.
Evaluators determine whether an agent's output matches expectations and
provide a score indicating the quality of the match.
## Built-in Evaluators
- `Nous.Eval.Evaluators.ExactMatch` - Exact string match
- `Nous.Eval.Evaluators.FuzzyMatch` - Similarity-based match
- `Nous.Eval.Evaluators.Contains` - Check for substrings
- `Nous.Eval.Evaluators.ToolUsage` - Verify tool calls
- `Nous.Eval.Evaluators.Schema` - Validate structured output
- `Nous.Eval.Evaluators.LLMJudge` - LLM-based evaluation
## Custom Evaluators
defmodule MyEvaluator do
@behaviour Nous.Eval.Evaluator
@impl true
def evaluate(actual, expected, config) do
# Your evaluation logic
if my_check(actual, expected) do
%{score: 1.0, passed: true, reason: nil, details: %{}}
else
%{score: 0.0, passed: false, reason: "Did not match", details: %{}}
end
end
end
## Result Format
Evaluators must return a map with:
* `:score` - Float from 0.0 to 1.0
* `:passed` - Boolean indicating pass/fail
* `:reason` - String explaining failure (or nil)
* `:details` - Map with additional details
"""
@type score :: float()
@type result :: %{
score: score(),
passed: boolean(),
reason: String.t() | nil,
details: map()
}
@doc """
Evaluate an actual output against expected output.
## Parameters
* `actual` - The actual output from the agent
* `expected` - The expected output
* `config` - Configuration map for the evaluator
## Returns
A result map with score, passed status, and details.
"""
@callback evaluate(actual :: term(), expected :: term(), config :: map()) :: result()
@doc """
Optional: Name of the evaluator for display purposes.
"""
@callback name() :: String.t()
@optional_callbacks [name: 0]
@doc """
Get the evaluator module for an eval_type.
"""
@spec get_evaluator(atom()) :: module() | nil
def get_evaluator(:exact_match), do: Nous.Eval.Evaluators.ExactMatch
def get_evaluator(:fuzzy_match), do: Nous.Eval.Evaluators.FuzzyMatch
def get_evaluator(:contains), do: Nous.Eval.Evaluators.Contains
def get_evaluator(:tool_usage), do: Nous.Eval.Evaluators.ToolUsage
def get_evaluator(:schema), do: Nous.Eval.Evaluators.Schema
def get_evaluator(:llm_judge), do: Nous.Eval.Evaluators.LLMJudge
def get_evaluator(:custom), do: nil
def get_evaluator(_), do: nil
@doc """
Run evaluation using the appropriate evaluator.
"""
@spec run(atom(), term(), term(), map()) :: result()
def run(:custom, actual, expected, config) do
case Map.get(config, :evaluator) do
nil ->
%{
score: 0.0,
passed: false,
reason: "Custom evaluator not specified in config",
details: %{}
}
evaluator when is_atom(evaluator) ->
evaluator.evaluate(actual, expected, config)
end
end
def run(eval_type, actual, expected, config) do
case get_evaluator(eval_type) do
nil ->
%{
score: 0.0,
passed: false,
reason: "Unknown eval_type: #{inspect(eval_type)}",
details: %{}
}
evaluator when is_atom(evaluator) and not is_nil(evaluator) ->
apply(evaluator, :evaluate, [actual, expected, config])
end
end
@doc """
Create a passing result helper.
"""
@spec pass(map()) :: result()
def pass(details \\ %{}) do
%{score: 1.0, passed: true, reason: nil, details: details}
end
@doc """
Create a failing result helper.
"""
@spec fail(String.t(), map()) :: result()
def fail(reason, details \\ %{}) do
%{score: 0.0, passed: false, reason: reason, details: details}
end
@doc """
Create a partial match result helper.
"""
@spec partial(float(), String.t() | nil, map()) :: result()
def partial(score, reason \\ nil, details \\ %{}) do
%{score: score, passed: score >= 0.5, reason: reason, details: details}
end
end