Packages
nous
0.15.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/nous/eval/yaml_loader.ex
defmodule Nous.Eval.YamlLoader do
@moduledoc """
Loads evaluation suites and test cases from YAML files.
## Example YAML Format
name: my_suite
description: Test suite for weather agent
default_model: lmstudio:ministral-3-14b-reasoning
default_timeout: 30000
parallelism: 2
test_cases:
- id: basic_query
name: Basic Weather Query
input: "What's the weather in Tokyo?"
expected:
contains: ["weather", "Tokyo"]
eval_type: contains
tags: [basic, weather]
timeout: 20000
- id: exact_response
name: Math Test
input: "What is 2+2?"
expected: "4"
eval_type: exact_match
- id: quality_check
name: Explanation Quality
input: "Explain recursion"
expected:
criteria: "Is the explanation clear?"
eval_type: llm_judge
eval_config:
judge_model: lmstudio:ministral-3-14b-reasoning
"""
alias Nous.Eval.{Suite, TestCase}
@doc """
Load a suite from a YAML file.
"""
@spec load_suite(String.t()) :: {:ok, Suite.t()} | {:error, term()}
def load_suite(path) do
with {:ok, content} <- File.read(path),
{:ok, data} <- parse_yaml(content),
{:ok, suite} <- build_suite(data, path) do
{:ok, suite}
else
{:error, reason} ->
{:error, {:yaml_load_error, path, reason}}
end
end
@doc """
Load all suites from a directory.
"""
@spec load_directory(String.t()) :: {:ok, [Suite.t()]} | {:error, term()}
def load_directory(dir) do
pattern = Path.join(dir, "*.{yaml,yml}")
files = Path.wildcard(pattern)
if files == [] do
{:error, {:no_yaml_files, dir}}
else
results =
Enum.map(files, fn file ->
case load_suite(file) do
{:ok, suite} -> {:ok, suite}
{:error, reason} -> {:error, {file, reason}}
end
end)
errors = Enum.filter(results, &match?({:error, _}, &1))
if errors == [] do
suites = Enum.map(results, fn {:ok, suite} -> suite end)
{:ok, suites}
else
{:error, {:load_errors, errors}}
end
end
end
# Private implementation
defp parse_yaml(content) do
try do
data = YamlElixir.read_from_string!(content)
{:ok, data}
rescue
e in YamlElixir.ParsingError ->
{:error, {:yaml_parse_error, e.message}}
e ->
{:error, {:yaml_error, Exception.message(e)}}
end
end
defp build_suite(data, path) when is_map(data) do
name = Map.get(data, "name") || Path.basename(path, Path.extname(path))
# Parse test cases
test_cases_data = Map.get(data, "test_cases", [])
test_cases_result =
Enum.reduce_while(test_cases_data, {:ok, []}, fn tc_data, {:ok, acc} ->
case TestCase.from_map(tc_data) do
{:ok, tc} -> {:cont, {:ok, [tc | acc]}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
case test_cases_result do
{:ok, test_cases} ->
suite =
Suite.new(
name: name,
description: Map.get(data, "description"),
test_cases: Enum.reverse(test_cases),
default_model: Map.get(data, "default_model"),
default_instructions: Map.get(data, "default_instructions"),
default_timeout: Map.get(data, "default_timeout", 30_000),
parallelism: Map.get(data, "parallelism", 1),
retry_failed: Map.get(data, "retry_failed", 0),
metadata: Map.get(data, "metadata", %{})
)
{:ok, suite}
{:error, reason} ->
{:error, reason}
end
end
defp build_suite(data, _path) do
{:error, {:invalid_yaml_format, "Expected a map, got: #{inspect(data)}"}}
end
end