Current section

Files

Jump to
aludel lib aludel evals assertion_evaluator.ex
Raw

lib/aludel/evals/assertion_evaluator.ex

defmodule Aludel.Evals.AssertionEvaluator do
@moduledoc false
alias Aludel.Evals.DeepCompare
@default_threshold 100.0
@spec evaluate(String.t(), map()) :: map()
def evaluate(output, %{"type" => "contains", "value" => value} = assertion) do
boolean_result(assertion, String.contains?(output, value))
end
def evaluate(output, %{"type" => "not_contains", "value" => value} = assertion) do
boolean_result(assertion, not String.contains?(output, value))
end
def evaluate(output, %{"type" => "regex", "value" => pattern} = assertion) do
passed =
case Regex.compile(pattern) do
{:ok, regex} -> Regex.match?(regex, output)
{:error, _reason} -> false
end
boolean_result(assertion, passed)
end
def evaluate(output, %{"type" => "exact_match", "value" => value} = assertion) do
boolean_result(assertion, output == value)
end
def evaluate(output, %{"type" => "json_field", "field" => field, "expected" => expected}) do
case decode_json_output(output) do
{:ok, json} ->
actual_value = get_in(json, String.split(field, "."))
passed = compare_json_values(actual_value, expected)
%{
"type" => "json_field",
"passed" => passed,
"score" => score_from_boolean(passed),
"value" => %{"field" => field, "expected" => expected},
"actual_value" => actual_value
}
{:error, _reason} ->
%{
"type" => "json_field",
"passed" => false,
"score" => 0.0,
"value" => %{"field" => field, "expected" => expected},
"actual_value" => nil
}
end
end
def evaluate(output, %{"type" => "json_deep_compare", "expected" => expected} = assertion) do
threshold = normalize_threshold(assertion)
case decode_json_output(output) do
{:ok, json} ->
score_details = DeepCompare.compare(json, expected)
score = DeepCompare.score(score_details)
passed = score >= threshold
%{
"type" => "json_deep_compare",
"passed" => passed,
"score" => score,
"value" => %{"expected" => expected, "threshold" => threshold},
"score_details" => score_details
}
{:error, _reason} ->
%{
"type" => "json_deep_compare",
"passed" => false,
"score" => 0.0,
"value" => %{"expected" => expected, "threshold" => threshold},
"score_details" => %{
"matches" => 0,
"total" => 0,
"field_scores" => %{},
"comparisons" => %{}
}
}
end
end
def evaluate(_output, assertion) do
%{
"type" => Map.get(assertion, "type"),
"passed" => false,
"score" => 0.0,
"value" => Map.get(assertion, "value")
}
end
@spec score_for_results([map()]) :: float() | nil
def score_for_results([]), do: nil
def score_for_results(results) do
scores =
results
|> Enum.map(&Map.get(&1, "score"))
|> Enum.filter(&is_number/1)
if scores == [] do
nil
else
Float.round(Enum.sum(scores) / length(scores), 1)
end
end
defp boolean_result(assertion, passed) do
%{
"type" => assertion["type"],
"passed" => passed,
"score" => score_from_boolean(passed),
"value" => assertion["value"]
}
end
defp score_from_boolean(true), do: 100.0
defp score_from_boolean(false), do: 0.0
defp normalize_threshold(%{"threshold" => threshold}) when is_integer(threshold),
do: threshold / 1
defp normalize_threshold(%{"threshold" => threshold}) when is_float(threshold), do: threshold
defp normalize_threshold(_assertion), do: @default_threshold
defp decode_json_output(output) do
output
|> String.trim()
|> String.replace(~r/^```json\s*/i, "")
|> String.replace(~r/^```\s*/, "")
|> String.replace(~r/```\s*$/, "")
|> String.trim()
|> Jason.decode()
end
defp compare_json_values(actual, expected) when is_map(actual) or is_list(actual) do
Jason.encode!(actual) == Jason.encode!(expected)
end
defp compare_json_values(actual, expected), do: actual == expected
end