This example demonstrates how to write custom graders and extractors using Python decorators.
- Creating custom extractors with the
@extractordecorator to evaluate agent memory operations - Creating custom graders with the
@graderdecorator for domain-specific validation - Inspecting tool calls within agent trajectories (not just final responses)
- Validating internal agent behavior beyond message content
Custom extractors and graders allow you to evaluate any part of the agent's behavior. In this example, we check if the agent correctly stores fruit preferences in memory by extracting memory_insert tool calls and validating their arguments.
Start your local Letta server:
letta serverThen run the evaluation:
cd examples/custom-tool-grader-and-extractor
letta-evals run suite.yamlSet these environment variables:
export LETTA_API_KEY=your-api-key
export LETTA_PROJECT_ID=your-project-idUpdate base_url in suite.yaml:
target:
base_url: https://api.letta.com/Then run the evaluation as above.
The custom extractor inspects the agent's trajectory to find memory_insert tool calls:
@extractor
def memory_insert_extractor(trajectory: List[List[LettaMessageUnion]], config: dict) -> str:
"""Extract memory_insert tool calls from trajectory."""
for turn in trajectory:
for message in turn:
if isinstance(message, ToolCallMessage) and message.tool_call.name == "memory_insert":
return message.tool_call.arguments
return "{}"Key points:
- The
@extractordecorator registers this function with letta-evals trajectoryis a list of conversation turns, each containing messages- We look for
ToolCallMessagewith the specific tool name - Returns the tool call arguments as a JSON string for grading
The custom grader validates the extracted memory insert arguments:
@grader
def grade_fruit_preference(sample: Sample, submission: str) -> GradeResult:
"""Grade if the fruit preference was correctly stored in memory."""
try:
args = json.loads(submission)
except json.JSONDecodeError:
return GradeResult(score=0.0, rationale="No valid memory_insert tool call found")
# check label is user_fruit_preferences
label = args.get("label", "")
if label != "user_fruit_preferences":
return GradeResult(
score=0.0, rationale=f"Wrong memory block label: expected 'user_fruit_preferences', got '{label}'"
)
# check fruit name is in new_str
fruit = sample.ground_truth.lower()
new_str = args.get("new_str", "").lower()
if fruit not in new_str:
return GradeResult(score=0.0, rationale=f"Fruit '{fruit}' not found in new_str")
return GradeResult(score=1.0, rationale="Fruit preference correctly stored")Key points:
- The
@graderdecorator registers this function with letta-evals - Takes
sample(containing ground truth) andsubmission(extractor output) - Returns
GradeResultwithscore(0.0-1.0) andrationale(explanation) - Validates both the memory block label and that the fruit name appears in the content
In suite.yaml, reference custom functions using the file:function syntax:
graders:
preference_check:
kind: tool
function: custom_evaluators.py:grade_fruit_preference
extractor: custom_evaluators.py:memory_insert_extractor