Packages
nous
0.13.3
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/nous.eval.ex
defmodule Mix.Tasks.Nous.Eval do
@moduledoc """
Run evaluation suites for Nous agents.
## Usage
# Run all suites from default directory (test/eval/suites)
mix nous.eval
# Run a specific suite file
mix nous.eval --suite test/eval/suites/basic.yaml
# Run from a different directory
mix nous.eval --dir priv/eval
# Filter by tags
mix nous.eval --tags basic,tool
# Exclude tags
mix nous.eval --exclude slow,stress
# Override model
mix nous.eval --model lmstudio:ministral-3-14b-reasoning
# Set parallelism
mix nous.eval --parallel 4
# Output format
mix nous.eval --format json
mix nous.eval --format json --output results.json
# Verbose mode
mix nous.eval --verbose
## Options
* `--suite` - Path to a specific suite file (YAML)
* `--dir` - Directory containing suite files (default: test/eval/suites)
* `--tags` - Only run test cases with these tags (comma-separated)
* `--exclude` - Exclude test cases with these tags (comma-separated)
* `--model` - Override default model for all tests
* `--parallel` - Number of concurrent tests (default: 1)
* `--timeout` - Default timeout in ms (default: 30000)
* `--format` - Output format: console, json, markdown (default: console)
* `--output` - Output file path (for json/markdown formats)
* `--verbose` - Show detailed output including passed tests
* `--retry` - Number of retries for failed tests (default: 0)
## Configuration
You can also configure defaults in your config:
config :nous, Nous.Eval,
default_model: "lmstudio:ministral-3-14b-reasoning",
default_timeout: 30_000,
parallelism: 4
"""
use Mix.Task
@shortdoc "Run Nous agent evaluation suites"
@switches [
suite: :string,
dir: :string,
tags: :string,
exclude: :string,
model: :string,
parallel: :integer,
timeout: :integer,
format: :string,
output: :string,
verbose: :boolean,
retry: :integer
]
@impl Mix.Task
def run(args) do
# Start the application
{:ok, _} = Application.ensure_all_started(:nous)
{opts, _remaining, _invalid} = OptionParser.parse(args, switches: @switches)
# Load suites
suites = load_suites(opts)
if suites == [] do
Mix.shell().error("No evaluation suites found!")
exit({:shutdown, 1})
end
# Build run options
run_opts = build_run_opts(opts)
# Run evaluations
results = run_suites(suites, run_opts, opts)
# Output results
output_results(results, opts)
# Exit with appropriate code
all_passed = Enum.all?(results, fn {_, result} -> result.pass_rate == 1.0 end)
unless all_passed do
exit({:shutdown, 1})
end
end
defp load_suites(opts) do
cond do
opts[:suite] ->
# Load specific suite
case Nous.Eval.Suite.from_yaml(opts[:suite]) do
{:ok, suite} ->
[suite]
{:error, reason} ->
Mix.shell().error("Failed to load suite: #{inspect(reason)}")
[]
end
true ->
# Load from directory
dir = opts[:dir] || "test/eval/suites"
if File.dir?(dir) do
case Nous.Eval.Suite.from_directory(dir) do
{:ok, suites} ->
suites
{:error, reason} ->
Mix.shell().error("Failed to load suites: #{inspect(reason)}")
[]
end
else
Mix.shell().info("Creating evaluation directory: #{dir}")
File.mkdir_p!(dir)
[]
end
end
end
defp build_run_opts(opts) do
run_opts = []
run_opts =
if opts[:tags] do
tags = opts[:tags] |> String.split(",") |> Enum.map(&String.to_atom/1)
Keyword.put(run_opts, :tags, tags)
else
run_opts
end
run_opts =
if opts[:exclude] do
tags = opts[:exclude] |> String.split(",") |> Enum.map(&String.to_atom/1)
Keyword.put(run_opts, :exclude_tags, tags)
else
run_opts
end
run_opts =
if opts[:model] do
Keyword.put(run_opts, :model, opts[:model])
else
run_opts
end
run_opts =
if opts[:parallel] do
Keyword.put(run_opts, :parallelism, opts[:parallel])
else
run_opts
end
run_opts =
if opts[:timeout] do
Keyword.put(run_opts, :timeout, opts[:timeout])
else
run_opts
end
run_opts =
if opts[:retry] do
Keyword.put(run_opts, :retry_failed, opts[:retry])
else
run_opts
end
run_opts
end
defp run_suites(suites, run_opts, opts) do
verbose = opts[:verbose] || false
Enum.map(suites, fn suite ->
if verbose do
Mix.shell().info("\nRunning suite: #{suite.name}")
Mix.shell().info("Test cases: #{Nous.Eval.Suite.count(suite)}")
end
case Nous.Eval.run(suite, run_opts) do
{:ok, result} ->
{suite.name, result}
{:error, reason} ->
Mix.shell().error("Suite #{suite.name} failed: #{inspect(reason)}")
{suite.name, nil}
end
end)
|> Enum.reject(fn {_, result} -> is_nil(result) end)
end
defp output_results(results, opts) do
format = opts[:format] || "console"
output_path = opts[:output]
verbose = opts[:verbose] || false
case format do
"console" ->
Enum.each(results, fn {_name, result} ->
if verbose do
Nous.Eval.Reporter.print_detailed(result)
else
Nous.Eval.Reporter.print(result)
end
end)
"json" ->
json_output =
results
|> Enum.map(fn {name, result} -> {name, Nous.Eval.Reporter.Json.to_map(result)} end)
|> Enum.into(%{})
|> Map.put(:generated_at, DateTime.to_iso8601(DateTime.utc_now()))
|> Nous.JSON.pretty_encode!()
if output_path do
File.write!(output_path, json_output)
Mix.shell().info("Results written to: #{output_path}")
else
IO.puts(json_output)
end
"markdown" ->
md_output =
Enum.map_join(results, "\n\n---\n\n", fn {_name, result} ->
Nous.Eval.Reporter.to_markdown(result)
end)
if output_path do
File.write!(output_path, md_output)
Mix.shell().info("Results written to: #{output_path}")
else
IO.puts(md_output)
end
_ ->
Mix.shell().error("Unknown format: #{format}")
end
end
end