Comprehensive Evaluation Framework¶
Use Draive's evaluation primitives to score model outputs consistently and keep quality criteria transparent. This guide walks through evaluators, scenarios, suites, and supporting patterns for building end-to-end evaluation flows.
Evaluator Basics¶
- Evaluators are async callables decorated with
@evaluatorthat return anEvaluationScoreor a compatible score value (float,bool, or a level name). - Thresholds determine whether an evaluation passes; named levels are easier to reason about than
raw floats:
"none"(0.0),"poor"(0.2),"fair"(0.4),"good"(0.6),"excellent"(0.8),"perfect"(1.0). The default threshold is1- the strictest one. EvaluatorResultcarriesscore,threshold,metaand derivespassed(score >= threshold).EvaluatorResult.performanceis reported as a percentage and can exceed 100 when a score comfortably beats its threshold.- An evaluator that raises is not propagated: the failure is logged and turned into a
0.0score with the exception recorded in the result metadata.
Working with EvaluationScore¶
from draive.evaluation import EvaluationScore
score_from_float = EvaluationScore.of(0.85)
score_from_label = EvaluationScore.of("good")
score_from_boolean = EvaluationScore.of(True)
score_with_meta = EvaluationScore.of(0.85, meta={"comment": "minor omissions"})
EvaluationScore holds only value and meta - comments and any other explanation belong in
meta, conventionally under the "comment" key.
Defining an evaluator¶
from draive.evaluation import evaluator
@evaluator(name="length_check", threshold="excellent")
async def check_response_length(value: str, min_length: int = 100) -> float:
actual_length = len(value)
if actual_length >= min_length:
return 1.0
return actual_length / min_length
# Prepared evaluators freeze arguments for reuse
strict_length_check = check_response_length.prepared(min_length=200)
result = await strict_length_check("This is a test response...")
assert result.passed # True when score >= excellent (0.8)
Combining Evaluators with Scenarios¶
Use evaluator_scenario to bundle related evaluators and evaluate to execute them together.
from collections.abc import Sequence
from draive.evaluation import evaluate, evaluator_scenario, EvaluatorResult
@evaluator_scenario(name="quality_checks")
async def evaluate_response_quality(value: str, context: str) -> Sequence[EvaluatorResult]:
return await evaluate(
value,
check_response_length.prepared(),
check_sentiment.prepared(),
check_relevance.prepared(context=context),
check_grammar.prepared(),
)
The scenario definition returns a sequence of EvaluatorResult, while calling the scenario returns
an EvaluatorScenarioResult exposing scenario, results, passed, performance and
report(...). Scenarios and plain evaluators can be mixed in one evaluate(...) call and in one
suite definition.
evaluate can run evaluators concurrently. Limit concurrency when evaluators hit rate-limited
services.
async def evaluate_response_quality_parallel(value: str, context: str) -> Sequence[EvaluatorResult]:
return await evaluate(
value,
check_response_length.prepared(),
check_sentiment.prepared(),
check_relevance.prepared(context=context),
check_grammar.prepared(),
concurrent_tasks=2,
)
Evaluator Suites for Regression Testing¶
Suites persist test cases, run them in bulk, and expose reporting helpers.
from collections.abc import Sequence
from pathlib import Path
from draive import State
from draive.evaluation import evaluator_suite
from draive.evaluation import EvaluatorResult
class QATestCase(State, serializable=True):
question: str
expected_topics: list[str]
min_length: int = 100
@evaluator_suite(
QATestCase,
name="qa_validation",
storage=Path("./test_cases.json"),
concurrent_evaluations=5,
)
async def qa_test_suite(parameters: QATestCase) -> Sequence[EvaluatorResult]:
answer = await generate_answer(parameters.question)
return [
await check_response_length(answer, parameters.min_length),
await check_topic_coverage(answer, parameters.expected_topics),
await check_factual_accuracy(answer, parameters.question),
]
await qa_test_suite.add_case(
QATestCase(
question="What is machine learning?",
expected_topics=["algorithms", "data", "training"],
min_length=150,
)
)
all_cases = await qa_test_suite.cases()
full_results = await qa_test_suite()
sample_results = await qa_test_suite(5)
partial_results = await qa_test_suite(0.3)
specific_results = await qa_test_suite(["case-1", "case-2"])
report = full_results.report(detailed=True, include_passed=False)
Case selection follows the argument type: None (or no argument) runs every stored case, an int
runs that many random cases, a float in (0, 1] runs that fraction, and a sequence selects
specific cases by identifier, by parameters, or by EvaluatorSuiteCase instances. An unknown
identifier raises ValueError.
storage accepts a Path/str JSON file, a sequence of EvaluatorSuiteCase values for in-memory
cases, or a custom EvaluatorSuiteCasesStorage implementation; omitting it starts with empty
in-memory storage. File storage expects the file to exist - create it ([] for no cases) before the
first run. with_storage(...), with_name(...), with_meta(...), with_state(...) and
with_concurrent_evaluations(...) produce reconfigured copies of a suite, and prepared(...) binds
extra arguments of the definition.
Composing and Transforming Evaluators¶
from draive.evaluation import Evaluator
conservative_eval = Evaluator.lowest(
evaluator1.prepared(),
evaluator2.prepared(),
evaluator3.prepared(),
)
optimistic_eval = Evaluator.highest(
evaluator1.prepared(),
evaluator2.prepared(),
)
mean_eval = Evaluator.average(
evaluator1.prepared(),
evaluator2.prepared(),
threshold="good", # required - it replaces the combined thresholds
)
lowest and highest compare performance (score relative to each threshold) and return the
winning EvaluatorResult as is; average returns a new result named average carrying the mean
score. All three accept concurrent_tasks (2 by default).
# Transform inputs before delegation
field_evaluator = my_evaluator.contra_map(MyModel._.attribute)
normalized = my_evaluator.contra_map(lambda data: data["response"].strip().lower())
Reference-Based Scoring¶
When you have ground-truth ratings, score the evaluator itself: Evaluator.referenced(...) runs the
wrapped evaluation and replaces its score with how well that score conforms to an accepted window
pulled from the evaluated value. This keeps the ground truth in the value (e.g. a field of the suite
case parameters) instead of adding another argument.
from draive import State
from draive.evaluation import EvaluationReference, evaluator
class ReviewCase(State, serializable=True):
content: str
expected: EvaluationReference
@evaluator(name="review_quality", threshold="good")
async def review_quality(case: ReviewCase) -> float:
return await score_content(case.content)
# resolve the window per value - callable, attribute path, or a constant reference
referenced = review_quality.referenced(reference=ReviewCase._.expected)
nominal = review_quality.referenced(
reference=EvaluationReference(lower=0.6, upper=1.0),
weighting="nominal",
)
EvaluationReference describes the accepted window:
EvaluationReference.of("good")- exact single point (0.6).EvaluationReference.of("good", tolerance=0.2)-±20%of the target, so[0.48, 0.72].EvaluationReference(lower=0.6, upper=1.0)- explicit bounds.- A bare score value (
"good",0.6,True) is accepted anywhere a reference is expected and treated as an exact single-point window.
weighting controls the falloff outside the window: "quadratic" (default) grants partial credit
that decays with the squared distance to the nearest bound, "nominal" scores a miss as 0.0. The
result keeps the original evaluator name and threshold and records predicted_score,
predicted_level, reference_lower, reference_upper, within_reference and reference_weighting
in its metadata. reference_conformance(score, reference, weighting=...) exposes the same math
directly.
Rater Agreement¶
To compare an automatic evaluator against human labels, use Cohen's kappa. The functional variants
work on level labels, while cohen_kappa_evaluator accepts score values, EvaluationScore or
EvaluatorResult instances on both sides and bins them into levels.
from draive.evaluation import cohen_kappa, quadratic_weighted_kappa
from draive.evaluators import cohen_kappa_evaluator
nominal = cohen_kappa(["good", "excellent"], ["good", "good"])
ordinal = quadratic_weighted_kappa(["good", "excellent"], ["good", "good"])
agreement = await cohen_kappa_evaluator(
automatic_results, # Sequence of scores, EvaluationScore or EvaluatorResult
reference=human_labels,
weighting="quadratic", # or "nominal"
)
The evaluator score is the selected kappa clamped to [0, 1], with both variants plus
exact_agreement, sample_count and weighting reported in metadata. The tools/evals/ suite in
the repository uses exactly this evaluator to verify the shipped evaluators against labeled
baselines.
Evaluating Conversation Context¶
Every built-in content evaluator has a *_context_evaluator twin operating on a ModelContext - the
sequence of ModelInput/ModelOutput elements produced by a conversation - so multi-turn behaviour
can be judged as a whole. Their reference-style arguments are optional: when omitted the evaluator
judges the outputs on their own (for example internal consistency instead of consistency with a
reference). tool_usage_context_evaluator is context-only and checks the tool calls recorded in the
timeline.
Stateful Evaluation with Haiway¶
from haiway import State, ctx
from draive.evaluation import evaluator
class EvaluationConfig(State):
strict_mode: bool = False
max_retries: int = 3
@evaluator(threshold="perfect", state=[EvaluationConfig(strict_mode=True)])
async def strict_evaluator(value: str) -> float:
config = ctx.state(EvaluationConfig)
if config.strict_mode:
# Apply stricter logic
return await evaluate_strict(value)
return await evaluate_lenient(value)
Threshold Strategy¶
from draive.evaluators import (
coherence_evaluator,
completeness_evaluator,
consistency_evaluator,
creativity_evaluator,
factual_accuracy_evaluator,
forbidden_keywords_evaluator,
groundedness_evaluator,
helpfulness_evaluator,
readability_evaluator,
required_keywords_evaluator,
safety_evaluator,
similarity_evaluator,
tone_style_evaluator,
)
safety_check = safety_evaluator.with_threshold("perfect")
consistency_check = consistency_evaluator.with_threshold("perfect")
forbidden_check = forbidden_keywords_evaluator.with_threshold("perfect")
helpfulness_check = helpfulness_evaluator.with_threshold("excellent")
factual_accuracy_check = factual_accuracy_evaluator.with_threshold("excellent")
tone_style_check = tone_style_evaluator.with_threshold("excellent")
completeness_check = completeness_evaluator.with_threshold("good")
creativity_check = creativity_evaluator.with_threshold("good")
readability_check = readability_evaluator.with_threshold("good")
similarity_check = similarity_evaluator.with_threshold("fair")
keyword_check = required_keywords_evaluator.with_threshold("fair")
precise_check = factual_accuracy_evaluator.with_threshold(0.85)
Threshold guidelines
- Safety & compliance: use
"perfect"; violations are unacceptable. - Core quality: use
"excellent"for user-facing content. - Supportive signals: use
"good"or lower when outcomes are subjective.
Rich Metadata¶
from datetime import datetime
from draive.evaluation import EvaluationScore, evaluator
@evaluator
async def evaluate_with_context(response: str) -> EvaluationScore:
score, issues = await analyze_response(response)
return EvaluationScore.of(
score,
meta={
"timestamp": datetime.now().isoformat(),
"issues_found": issues,
"evaluation_model": "gpt-5.5",
"confidence": 0.85,
},
)
Generating Test Cases¶
generate_cases asks a model to synthesize new case parameters of the suite's own parameters type.
All arguments are keyword-only; examples defaults to the currently available cases and persist
controls whether the extended set is written back to storage.
examples = [
QATestCase(
question="What is machine learning?",
expected_topics=["algorithms", "data", "training"],
min_length=150,
),
QATestCase(
question="Explain overfitting",
expected_topics=["generalization", "validation"],
),
]
cases = await qa_test_suite.generate_cases(
count=20,
examples=examples,
guidelines="""
Generate diverse test cases covering:
- Different complexity levels
- Various input formats
- Edge cases and error conditions
- Performance boundaries
""",
persist=True,
)
Generation runs a model call per case, so it has to happen inside a scope providing a model. The returned sequence contains only the newly generated cases; they are appended to the suite's cases either way.
Summary¶
- Flexible scoring with normalized values and named levels
- Composable evaluators with thresholds and metadata
- Scenario grouping for related checks
- Suite management with persistent storage and generation tools
- Reporting helpers for insight into failures and regressions
- Reference windows and Cohen's kappa for validating evaluators against ground truth
- Concurrent execution to balance latency and throughput
Use evaluators for quick checks, scenarios for logical groupings, and suites for comprehensive regression coverage backed by persistent cases and automated generation.