Basic TaskEval Evaluation¶
This tutorial walks through evaluating free text outputs using TaskEval. You log any text or structured traces, attach evaluation criteria (templates, rubrics, or both), run the judge LLM, and inspect results. No question definition or answer generation is needed. For the underlying concepts, see TaskEval.
What you'll learn:
- Create a
TaskEvalinstance with tracking metadata - Log text output with
log()and structured traces withlog_trace() - Attach a correctness template with
add_template() - Attach a quality rubric with
add_rubric() - Configure
VerificationConfigwithparsing_only=True - Run evaluation and inspect
TaskEvalResult - Use
summary(),display(),get_summary_stats(), and field-level access - Export results to JSON and Markdown
from karenina.benchmark.task_eval import TaskEval
from karenina.schemas.config.models import ModelConfig
from karenina.schemas.verification.config import VerificationConfig
from karenina.schemas.entities import BaseAnswer
from pydantic import Field
task = TaskEval(task_id="quick-demo")
task.log("The primary pharmacological target of venetoclax is BCL2.")
class Answer(BaseAnswer):
identifies_bcl2: bool = Field(description="True if BCL2 is identified as the target.")
def ground_truth(self):
self.correct = {"identifies_bcl2": True}
def verify(self) -> bool:
return self.identifies_bcl2 == self.correct["identifies_bcl2"]
task.add_template(Answer)
config = VerificationConfig(
parsing_models=[
ModelConfig(
id="haiku",
model_name="claude-haiku-4-5",
model_provider="anthropic",
interface="langchain",
temperature=0.0,
)
],
parsing_only=True,
)
result = task.evaluate(config)
print(result.summary())
Native structured output failed for claude-haiku-4-5: No fixture for prompt hash 6e957fa65bac5898. Falling back to manual JSON parsing.
Native structured output failed for claude-haiku-4-5: No fixture for prompt hash b4d5af408aa2f5a4. Falling back to manual JSON parsing.
Native structured output failed for claude-haiku-4-5: No fixture for prompt hash a7aecefefb0eb89a. Falling back to manual JSON parsing.
Native structured output failed for claude-haiku-4-5: No fixture for prompt hash 57592f82b28b0a3b. Falling back to manual JSON parsing.
Abstention detection failed with non-retryable error: Structured output failed after 4 attempts. Last error: No fixture for prompt hash fcdfc28fec33a6b7
Native structured output failed for claude-haiku-4-5: No fixture for prompt hash 81cb7233c8f13bfb. Falling back to manual JSON parsing.
Retrying LLM call (attempt 1/3) after error: No fixture for prompt hash 502ac56f4dbf051f
Retrying LLM call (attempt 2/3) after error: No fixture for prompt hash 502ac56f4dbf051f
Native structured output failed for claude-haiku-4-5: No fixture for prompt hash bf3fedb555a70945. Falling back to manual JSON parsing.
Native structured output failed for claude-haiku-4-5: No fixture for prompt hash 61dbf229f41a7c62. Falling back to manual JSON parsing.
Native structured output failed for claude-haiku-4-5: No fixture for prompt hash 57a2d9a8bc255887. Falling back to manual JSON parsing.
Stage ParseTemplate set error: Parsing failed: No fixture for prompt hash 81cb7233c8f13bfb
Stage FinalizeResult set error: Parsing failed: No fixture for prompt hash 81cb7233c8f13bfb
0/1 traces passed
Create a TaskEval Instance¶
TaskEval is a recording and evaluation container. You log text into it, attach evaluation criteria, then run evaluation. Create one with an optional task ID and metadata for tracking.
from karenina.benchmark.task_eval import TaskEval
task = TaskEval(
task_id="agent-drug-target",
metadata={"model": "claude-haiku-4-5", "run_date": "2025-02-15"},
)
print(f"Created TaskEval: {task.task_id}")
Created TaskEval: agent-drug-target
task_id and metadata are optional tracking fields that appear in results and exports. They have no effect on evaluation behavior.
Constructor parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
task_id |
str \| None |
None |
Identifier for tracking |
metadata |
dict \| None |
None |
Arbitrary metadata dict |
merge_strategy |
"concatenate" \| "traces_only" |
"concatenate" |
How to combine logs before evaluation (see Merge Strategies) |
callable_registry |
dict[str, Callable] |
None |
Runtime overrides keyed by callable trait name (see Rubrics) |
Log Outputs¶
TaskEval accepts two kinds of logged output: plain text and structured Message traces. Multiple calls accumulate: each log() or log_trace() call appends to the TaskEval's log store. When evaluation runs, the merge strategy controls how these logs combine into the response that the judge LLM sees.
Text Logging¶
Use log() when you have a plain string output to evaluate. This is the simplest path: pass any text and TaskEval records it as potential answer content.
task.log(
"The approved drug target of venetoclax is BCL2 (B-cell lymphoma 2). "
"Venetoclax is a selective BCL2 inhibitor that works by displacing pro-apoptotic "
"proteins, triggering programmed cell death in cancer cells [1]."
)
print(f"Logged {len(task.global_logs)} event(s)")
Logged 1 event(s)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
text |
str |
(required) | Text content to log |
step_id |
str \| None |
None |
Step identifier for grouping (see Multi-Step Evaluation) |
target |
"global" \| "step" \| "both" |
"both" |
Where to store the log |
level |
"debug" \| "info" \| "warn" \| "error" |
"info" |
Log level |
tags |
list[str] \| None |
None |
Optional tags |
Trace Logging¶
Use log_trace() when the judge LLM needs to see the full conversation structure: user messages, assistant responses, tool calls, and tool results. This matters for rubric traits that assess tool usage, multi-turn reasoning, or conversation flow. Without trace structure, the judge only sees a flat string.
from karenina.ports.messages import Message
trace_demo = TaskEval(task_id="trace-demo")
trace_demo.log_trace(
[
Message.user("What is the target of venetoclax?"),
Message.assistant("The primary pharmacological target of venetoclax is BCL2."),
]
)
print(f"Logged {len(trace_demo.global_logs)} trace event(s)")
Logged 1 trace event(s)
Tool Call Traces¶
For agent workflows with tool use, include ToolUseContent blocks and Message.tool_result(). The judge LLM receives the full trace, so tool call context can influence both template parsing and rubric scoring.
from karenina.ports.messages import ToolUseContent
trace_demo.log_trace(
[
Message.user("What is the target of venetoclax?"),
Message.assistant(
"Let me search the database.",
tool_calls=[ToolUseContent(id="call_1", name="search_db", input={"query": "venetoclax target"})],
),
Message.tool_result(tool_use_id="call_1", content="BCL2 (B-cell lymphoma 2)"),
Message.assistant(
"Based on the search results, the primary pharmacological target of venetoclax is BCL2 (B-cell lymphoma 2)."
),
]
)
print(f"Total trace events: {len(trace_demo.global_logs)}")
Total trace events: 2
Parameters: Same as log(), except the first argument is messages: list[Message] | str. If a plain str is passed, it is automatically wrapped as Message.assistant(text).
When to Use Each¶
| Situation | Use | Why |
|---|---|---|
| Single text output (API response, summary, generated answer) | log() |
Simple, no conversation structure needed |
| Agent trace with tool calls | log_trace() |
Preserves tool use context for the judge |
| Multi-turn conversation | log_trace() |
Preserves turn structure and speaker roles |
| Mix of debug output and structured responses | Both | Text for notes, traces for evaluable content (use traces_only merge strategy) |
Merge Strategies¶
Before evaluation, TaskEval combines all logged outputs into a single response for the verification pipeline. The merge strategy controls how log() and log_trace() calls are combined.
concatenate (default)¶
Every log contributes. Text logs are wrapped as Message.assistant(text). Trace logs contribute their messages directly. All messages are serialized together in the order you logged them.
Use concatenate when all logged content is part of the response being evaluated.
concat_demo = TaskEval(task_id="concat-demo")
concat_demo.log("BCL2 is the primary target of venetoclax.")
concat_demo.log("It inhibits the anti-apoptotic protein, triggering cell death.")
print(f"Logged {len(concat_demo.global_logs)} events (both contribute with 'concatenate')")
Logged 2 events (both contribute with 'concatenate')
traces_only¶
Only logs that contain structured Message traces are used. Text-only log() calls are silently ignored. No error is raised if all logs are text (the pipeline receives an empty string).
Use traces_only when your text logs are debug output, status messages, or intermediate notes that should not reach the judge LLM.
traces_demo = TaskEval(task_id="traces-demo", merge_strategy="traces_only")
traces_demo.log("Debug: starting retrieval phase")
traces_demo.log_trace([Message.assistant("BCL2 is the primary target.")])
print(f"Total logs: {len(traces_demo.global_logs)}")
print("With traces_only: only the trace log reaches the judge")
print("The text log ('Debug: starting retrieval phase') is silently ignored")
Total logs: 2
With traces_only: only the trace log reaches the judge
The text log ('Debug: starting retrieval phase') is silently ignored
Setting the Strategy¶
Set at construction time via merge_strategy, or override per evaluate() call:
custom_strategy = TaskEval(task_id="custom-strategy", merge_strategy="traces_only")
print(f"Instance default: {custom_strategy.merge_strategy}")
# Override per call: task.evaluate(config, merge_strategy="concatenate")
Instance default: traces_only
The per-call override takes precedence over the instance default.
Attach Evaluation Criteria¶
Attach templates, rubrics, or both. TaskEval auto-detects the evaluation mode based on what you provide:
| You attach | Evaluation mode | What happens |
|---|---|---|
| Template only | template_only |
Judge parses response into template fields, verify() checks correctness |
| Rubric only | rubric_only |
Each trait scored independently (a synthetic minimal template is created internally) |
| Template + rubric | template_and_rubric |
Both: template verification and rubric scoring |
If you attach neither, evaluate() raises ValueError.
Templates (correctness)¶
Pass a BaseAnswer subclass directly via add_template(). The template's fields tell the judge LLM what to extract; verify() checks correctness programmatically.
from pydantic import Field
from karenina.schemas.entities import BaseAnswer
class Answer(BaseAnswer):
identifies_bcl2: bool = Field(
description=(
"True if the response identifies BCL2 (including Bcl-2, BCL-2, or "
"B-cell lymphoma 2) as the direct pharmacological target of venetoclax. "
"False if BCL2 is mentioned only as a pathway member or a different "
"protein is identified as the primary target."
)
)
def ground_truth(self):
self.correct = {"identifies_bcl2": True}
def verify(self) -> bool:
return self.identifies_bcl2 == self.correct["identifies_bcl2"]
task.add_template(Answer)
print("Attached answer template with 1 verification field")
Attached answer template with 1 verification field
See Answer Templates for field types, descriptions, and writing verify() methods.
Rubrics (quality)¶
Attach a rubric via add_rubric(). Each trait evaluates one dimension of the response independently. TaskEval supports all the same trait types as Benchmark.
from karenina.schemas.entities.rubric import LLMRubricTrait, RegexRubricTrait, Rubric
rubric = Rubric(
llm_traits=[
LLMRubricTrait(
name="conciseness",
kind="boolean",
description=(
"True if the response answers directly without unnecessary "
"elaboration. False if the response is verbose or includes "
"tangential information."
),
higher_is_better=True,
),
],
regex_traits=[
RegexRubricTrait(
name="has_citations",
pattern=r"\[\d+\]",
description="Response includes numbered citations in bracket notation.",
case_sensitive=False,
higher_is_better=True,
),
],
)
task.add_rubric(rubric)
print(f"Added rubric with {len(rubric.llm_traits)} LLM trait(s) and {len(rubric.regex_traits)} regex trait(s)")
Added rubric with 1 LLM trait(s) and 1 regex trait(s)
Other Trait Types¶
TaskEval also supports callable traits (custom Python functions via cloudpickle), metric traits (precision/recall/F1 from confusion matrices), and literal traits (exact expected values). See Rubrics for all five trait types and their setup.
Note: Trait names must be unique across all trait types within a rubric. If you add multiple rubrics (e.g., global + step-scoped), they are merged before evaluation, and duplicate trait names raise ValueError.
Configure and Evaluate¶
Create a VerificationConfig with parsing_only=True and call evaluate().
parsing_only=True tells the pipeline to skip answer generation (stage 2), since you already supplied the text via log(). The pipeline still runs all relevant evaluation stages: template parsing (stage 7), template verification (stage 8), rubric evaluation (stage 11), and finalization (stage 13).
from karenina.schemas.config.models import ModelConfig
from karenina.schemas.verification.config import VerificationConfig
config = VerificationConfig(
parsing_models=[
ModelConfig(
id="claude-haiku-4-5",
model_name="claude-haiku-4-5",
model_provider="anthropic",
interface="langchain",
temperature=0.0,
)
],
parsing_only=True,
)
result = task.evaluate(config)
print(f"Evaluation complete: {result.summary()}")
Evaluation complete: 1/1 template verifications passed | 2/2 rubric traits passed
Evaluation Scope¶
Without step_id, evaluate() runs global evaluation first, then auto-evaluates every step that has logged data. Pass step_id to evaluate only one step's logs and criteria:
# Evaluate only one step (shown in Multi-Step Evaluation):
# result = task.evaluate(config, step_id="retrieval")
print("Without step_id: evaluates global + all steps")
print("With step_id: evaluates only that step")
Without step_id: evaluates global + all steps With step_id: evaluates only that step
print(result.display())
════════════════════════════════════════════════════════════════════════════════
TASK EVALUATION RESULTS
════════════════════════════════════════════════════════════════════════════════
Task ID: agent-drug-target
Model: claude-haiku-4-5
Run Date: 2025-02-15
Timestamp: 2026-03-12 16:03:00
────────────────────────────────────────────────────────────
GLOBAL EVALUATION
────────────────────────────────────────────────────────────
Verification Results:
Question: answer
Status: ✓ PASSED
Output: "--- AI Message ---
The approved drug target of venetoclax is BCL2 (B-cell lymphoma 2). Venetoclax is a selective BCL2 inhibitor that works by displacing pro-apoptotic proteins, triggering programmed cell death in cancer cells [1]."
Rubric: conciseness=✓, has_citations=✓
════════════════════════════════════════════════════════════════════════════════
SUMMARY: 1/1 template verifications passed | 2/2 rubric traits passed
════════════════════════════════════════════════════════════════════════════════
print(result.summary())
1/1 template verifications passed | 2/2 rubric traits passed
Result Structure¶
print(f"task_id: {result.task_id}")
print(f"metadata: {result.metadata}")
print(f"global_eval: {'present' if result.global_eval else 'None'}")
print(f"per_step: {list(result.per_step.keys())}")
print(f"timestamp: {result.timestamp}")
task_id: agent-drug-target
metadata: {'model': 'claude-haiku-4-5', 'run_date': '2025-02-15'}
global_eval: present
per_step: []
timestamp: 2026-03-12T16:03:00.746759
global_eval contains evaluation results for globally-scoped logs and criteria. per_step maps step IDs to their respective StepEval objects.
Summary Statistics¶
Each StepEval contains verification_results: a dict mapping question IDs to lists of VerificationResult (one per replicate).
get_summary_stats() returns a dict with these keys:
| Key | Type | Description |
|---|---|---|
traces_total |
int |
Number of evaluated traces (question IDs) |
traces_passed |
int |
Traces where at least one replicate passed verification |
template_verification_total |
int |
Results where template verification was performed |
template_verification_passed |
int |
Of those, how many passed |
rubric_traits_total |
int |
Sum of all trait counts (LLM + regex + callable + metric) |
rubric_traits_passed |
int |
Traits with score True or > 0 |
success_rate |
float |
traces_passed / traces_total * 100, or 0.0 if no traces |
step = result.global_eval
stats = step.get_summary_stats()
for key, value in stats.items():
print(f" {key}: {value}")
traces_total: 1 traces_passed: 1 results_total: 1 template_verification_total: 1 template_verification_passed: 1 rubric_traits_total: 2 rubric_traits_passed: 2 success_rate: 100.0
Formatted Results¶
For a detailed text breakdown of each verification result, including the full response, individual trait scores, and any error messages:
print(step.format_verification_results())
Question: answer
Status: ✓ PASSED
Output: "--- AI Message ---
The approved drug target of venetoclax is BCL2 (B-cell lymphoma 2). Venetoclax is a selective BCL2 inhibitor that works by displacing pro-apoptotic proteins, triggering programmed cell death in cancer cells [1]."
Rubric: conciseness=✓, has_citations=✓
Accessing VerificationResult Fields¶
Each VerificationResult uses nested sub-objects for structured access:
for qid, vr_list in step.verification_results.items():
vr = vr_list[0] # first replicate
print(f"Question: {qid}")
print(f" Template pass: {vr.template.verify_result}")
if vr.rubric:
print(f" LLM traits: {vr.rubric.llm_trait_scores}")
print(f" Regex traits: {vr.rubric.regex_trait_scores}")
Question: answer
Template pass: True
LLM traits: {'conciseness': True}
Regex traits: {'has_citations': True}
Export¶
Export results as JSON (optionally including raw logs) or Markdown for downstream analysis or archiving:
json_str = result.export_json()
print(json_str[:300])
{
"task_id": "agent-drug-target",
"metadata": {
"model": "claude-haiku-4-5",
"run_date": "2025-02-15"
},
"timestamp": "2026-03-12T16:03:00.746759",
"summary": "1/1 template verifications passed | 2/2 rubric traits passed",
"global_evaluation": {
"verification_results": {
md_str = result.export_markdown()
print(md_str[:300])
# Task Evaluation Results **Task ID:** agent-drug-target **Model:** claude-haiku-4-5 **Run Date:** 2025-02-15 **Status:** ✅ All Passed ## Verification Results - **Traces Passed**: 1/1 - **Template Verifications Passed**: 1/1 - **Rubric Traits Passed**: 2/2 - **Success Rate**: 100.0% ### Trace: a
Next Steps¶
- Quality Assessment with Rubrics: Rubric-only evaluation with all trait types
- Multi-Step Agent Evaluation: Step-scoped evaluation for agent workflows
- TaskEval concept page: Object structure, pipeline integration, and merge strategies in depth
- Answer Templates: Writing
verify()methods and field descriptions - Rubrics: All five trait types (LLM, regex, callable, metric, literal)