Building a Custom Benchmark¶
This tutorial walks through building a complete benchmark from scratch. It goes beyond the Quickstart by showing a realistic multi-file layout, a typed solution class, a checker that uses that type, and how to expose the checker as an LLM tool.
The running example is the Sum of Three Cubes problem: given an integer k,
find integers
x, y, z such that x³ + y³ + z³ = k. Verification is trivial
arithmetic, but finding solutions can require large search spaces — a good fit
for APE.
What you need to implement¶
Every APE benchmark requires three mandatory pieces and one that is almost always needed in practice:
| Piece | Description |
|---|---|
| Dataset (required) | Input file (.csv / .jsonl) and a DataModel schema describing one row |
| Prompt (required) | Mustache template rendered once per data item and sent to the LLM |
| Checker (required) | Deterministic verifier — returns Passed or Failed for a given solution |
| Adapter (almost always) | Parser that turns the LLM's raw text into the type the checker expects |
The adapter is technically optional only if your checker accepts a plain string — in practice you will always write one.
Prerequisites¶
- APE installed (
pip install ape-framework) - A LangChain chat model provider installed and an API key configured
APE works with any LangChain chat model. Install the package for the provider you want to use and export the corresponding API key:
| Provider | Package | Environment variable |
|---|---|---|
| Mistral | langchain-mistralai |
MISTRAL_API_KEY |
| OpenAI | langchain-openai |
OPENAI_API_KEY |
| Anthropic | langchain-anthropic |
ANTHROPIC_API_KEY |
langchain-google-genai |
GOOGLE_API_KEY |
For example, to use Mistral:
The [model] section of config.toml tells APE which class and model name to
use:
Replace provider with the fully qualified class name from the package you
installed
(e.g. langchain_openai.ChatOpenAI) and name with the model
identifier your provider expects.
1. Create the problem folder¶
APE has no required directory structure, but a consistent layout makes problems easy to navigate. Create a folder for your problem:
Everything below is placed inside this folder. All paths in config.toml will
be relative to it.
2. Define the dataset¶
Create data/schema.py with the schema for one row of your dataset:
DataModel is a Pydantic model. id_field() marks k as the row identifier —
APE uses it for logging, saving progress, and resume. You can add as many
additional fields as your dataset provides; they are available in the prompt
template.
Create data/__init__.py so the package is importable:
Now create data/problems.csv with the problems you want to evaluate:
APE supports both .csv and .jsonl as input formats.
3. Write the prompt template¶
Create prompts/base.md. APE renders this as a mustache template once per data
item, substituting {{ field_name }} with the value from the schema:
## Task — Sum of Three Cubes
Let k be {{ k }}. Find integers x, y, z such that
x³ + y³ + z³ = k
## Response format
Output exactly `TRIPLE(x, y, z)` where x, y, z are your integers.
Do not include any explanation.
4. Define the solution type¶
The checker needs a structured object, not a raw string. Create a solution class
in checker.py:
class TripleSolution:
def __init__(self, x: int, y: int, z: int) -> None:
self.x = x
self.y = y
self.z = z
def __str__(self) -> str:
return f"({self.x}, {self.y}, {self.z})"
Using a dedicated class (rather than a plain tuple) makes the checker signature self-documenting and lets you add methods later without changing the interface.
5. Write the adapter¶
The adapter is a function that parses the LLM's raw text response into a
TripleSolution. Create adapter.py:
import re
from checker import TripleSolution
def soc_adapter(raw: str) -> TripleSolution:
matches = re.findall(r"TRIPLE\((-?\d+),\s*(-?\d+),\s*(-?\d+)\)", raw)
if not matches:
raise ValueError(f"No triple found in response: {raw!r}")
return TripleSolution(*map(int, matches[-1]))
Key points:
- Raise
ValueError(or any exception) when parsing fails. APE catches it and, if configured, shows the LLM a reformat prompt and retries. - Taking the last match handles responses where the model revises itself before giving a final answer.
- Negative integers must be matched explicitly — note the
-?in the regex.
6. Write the checker¶
Complete checker.py by adding the SOCChecker class below TripleSolution.
The full file now looks like this:
import time
from datetime import timedelta
from typing import override
from ape.checker import Checker, CheckResult, Failed, Passed
from data import SOCSchema
class TripleSolution:
def __init__(self, x: int, y: int, z: int) -> None:
self.x = x
self.y = y
self.z = z
def __str__(self) -> str:
return f"({self.x}, {self.y}, {self.z})"
class SOCChecker(Checker[SOCSchema, TripleSolution]):
@override
def check(self, data: SOCSchema, solution: TripleSolution) -> CheckResult:
start = time.perf_counter()
correct = solution.x**3 + solution.y**3 + solution.z**3 == data.k
runtime = timedelta(seconds=time.perf_counter() - start)
if not correct:
return Failed(reason="Incorrect sum of cubes", runtime=runtime)
return Passed(runtime=runtime)
Checker[DataT, SolutionT] is generic. The type parameters document exactly
what the checker expects, which APE uses for validation. Passed and Failed
both accept an optional runtime and metadata dict.
7. Wire it all together¶
Create config.toml:
[data]
path = "data/problems.csv"
schema = "data.SOCSchema"
[prompt]
template = "prompts/base.md"
[model]
provider = "langchain_mistralai.ChatMistralAI"
name = "ministral-8b-2512"
[adapter]
callable = "adapter.soc_adapter"
[solver]
reformat_prompt = "That answer did not parse. Reply with ONLY 'TRIPLE(x, y, z)'."
[checker]
callable = "checker.SOCChecker"
APE resolves dotted module paths (data.SOCSchema, adapter.soc_adapter,
checker.SOCChecker) relative to the folder containing config.toml. No
package installation is needed — just put the files in the same directory.
8. Run the benchmark¶
APE validates the configuration, starts a live CLI dashboard, and writes results
to output/ when done. See Outputs & Results for
what each file contains.
9. Expose the checker as an LLM tool (optional)¶
For hard search problems the LLM benefits from being able to verify candidate triples mid-session, before committing to a final answer. APE can expose any checker as a LangChain tool with two config keys and a small factory function.
Create tool.py:
import json
from langchain_core.tools import BaseTool, tool
def soc_checker_tool() -> BaseTool:
"""Build a LangChain tool that checks x³ + y³ + z³ = k."""
@tool
def check_triple(x: int, y: int, z: int, k: int) -> str:
"""Check whether (x, y, z) solves x³ + y³ + z³ = k.
Args:
x: first integer
y: second integer
z: third integer
k: target integer
"""
if x**3 + y**3 + z**3 == k:
return json.dumps({"status": "success", "result": "OK"})
return json.dumps({
"status": "success",
"result": f"Incorrect (got {x**3 + y**3 + z**3}, expected {k})",
})
return check_triple
Then add two keys to the [checker] section in config.toml:
[checker]
callable = "checker.SOCChecker"
expose_as_tool = true
checker_tool = "tool.soc_checker_tool"
The factory (soc_checker_tool) is called with the same keyword arguments as
the checker constructor, so a single set of [checker] keys configures both.
The LLM can now call check_triple between reasoning steps.
Note
The tool factory must be a zero-argument callable when expose_as_tool is
true and no extra checker kwargs are set. If your checker takes
constructor arguments (e.g. enforce_strict = true), add them as plain
TOML keys in [checker] — APE passes them to both the checker and the
factory.
Final folder layout¶
sum-of-cubes/
config.toml
adapter.py
checker.py
tool.py # only if expose_as_tool = true
prompts/
base.md
data/
__init__.py
schema.py
problems.csv
output/ # written by APE, gitignore this
Next steps¶
- Add more prompt templates and switch between them with
[prompt] template. - Scale up with parallel workers — see Advanced LLM Run.
- Run on a slice of the dataset with
start_id/end_idin[data]. - Attach computation tools (Julia, SessionNotebook) — also in Advanced LLM Run.