Packages
nous
0.12.6
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/evaluators/fuzzy_match.ex
defmodule Nous.Eval.Evaluators.FuzzyMatch do
@moduledoc """
Evaluator that uses string similarity for matching.
Uses Levenshtein distance to calculate similarity between strings.
## Configuration
* `:threshold` - Minimum similarity (0.0 to 1.0, default: 0.8)
* `:normalize` - Normalize strings before comparison (default: true)
* `:case_insensitive` - Ignore case (default: true)
## Examples
TestCase.new(
id: "fuzzy",
input: "What is the capital of France?",
expected: "Paris is the capital of France",
eval_type: :fuzzy_match,
eval_config: %{threshold: 0.7}
)
"""
@behaviour Nous.Eval.Evaluator
@impl true
def evaluate(actual, expected, config) do
threshold = Map.get(config, :threshold, 0.8)
# Handle map with :output key from runner, or raw string
actual_str =
case actual do
%{output: output} when is_binary(output) -> normalize(output, config)
%{output: nil} -> ""
str when is_binary(str) -> normalize(str, config)
_ -> ""
end
expected_str = normalize(to_string(expected), config)
similarity = calculate_similarity(actual_str, expected_str)
if similarity >= threshold do
%{
score: similarity,
passed: true,
reason: nil,
details: %{
similarity: Float.round(similarity, 4),
threshold: threshold,
actual: actual_str,
expected: expected_str
}
}
else
%{
score: similarity,
passed: false,
reason: "Similarity #{Float.round(similarity, 2)} below threshold #{threshold}",
details: %{
similarity: Float.round(similarity, 4),
threshold: threshold,
actual: actual_str,
expected: expected_str
}
}
end
end
@impl true
def name, do: "Fuzzy Match"
@doc """
Calculate similarity between two strings using Levenshtein distance.
Returns a value between 0.0 (completely different) and 1.0 (identical).
"""
@spec calculate_similarity(String.t(), String.t()) :: float()
def calculate_similarity("", ""), do: 1.0
def calculate_similarity("", _), do: 0.0
def calculate_similarity(_, ""), do: 0.0
def calculate_similarity(s1, s2) do
distance = levenshtein_distance(s1, s2)
max_len = max(String.length(s1), String.length(s2))
1.0 - distance / max_len
end
@doc """
Calculate the Levenshtein distance between two strings.
"""
@spec levenshtein_distance(String.t(), String.t()) :: non_neg_integer()
def levenshtein_distance(s1, s2) do
s1_chars = String.graphemes(s1)
s2_chars = String.graphemes(s2)
s2_len = length(s2_chars)
# Initialize first row
row = Enum.to_list(0..s2_len)
# Process each character in s1
{final_row, _} =
Enum.reduce(Enum.with_index(s1_chars), {row, 0}, fn {c1, i}, {prev_row, _} ->
# Start with deletion cost
first = i + 1
# Process each character in s2
{new_row, _} =
Enum.reduce(Enum.with_index(s2_chars), {[first], first}, fn {c2, j}, {acc, prev_diag} ->
prev = Enum.at(prev_row, j + 1)
current = hd(acc)
cost = if c1 == c2, do: 0, else: 1
min_val =
Enum.min([
prev + 1,
current + 1,
prev_diag + cost
])
{[min_val | acc], Enum.at(prev_row, j)}
end)
{Enum.reverse(new_row), i + 1}
end)
List.last(final_row)
end
defp normalize(str, config) do
str
|> String.trim()
|> maybe_downcase(config)
|> maybe_normalize_whitespace(config)
end
defp maybe_downcase(str, config) do
if Map.get(config, :case_insensitive, true), do: String.downcase(str), else: str
end
defp maybe_normalize_whitespace(str, config) do
if Map.get(config, :normalize, true) do
str
|> String.replace(~r/\s+/, " ")
|> String.trim()
else
str
end
end
end