Tools¶
A tool is a small, deterministic function an agent can call during a question: a calculator, a date calculator, a grep over the corpus, a lookup in a table, or your own Python function. Tools are traced (every call is a tool step with its arguments, output, latency and cost), budgeted, safe to run on untrusted model output, and work in --mock mode with no network.
Why they are a benchmark axis: the same agent with tools: [] and with tools: [calculator, date_calc, corpus_grep] are two systems in one comparison, so the report shows what the tools bought (answer score on the questions that need them) and what they cost ($/Q, latency, error rate).
Using tools¶
Give a system that supports tools a tools: list. An entry is a built-in's name, or a mapping with the tool's options:
systems:
- type: agent_search # a system that accepts tools (see the list at the end of this page)
name: agent_with_tools
tools:
- calculator
- date_calc
- {name: corpus_grep, max_results: 10}
A system that does not use tools rejects a tools: list when the config loads, and an unknown tool name fails there too, with a did-you-mean hint.
Questions can declare the tools they need (requires_tools, see dataset-format.md); names are checked against the registry below. The run reports required_tool_used_rate for them.
Built-in tools¶
| Tool | What it does | Side effects | Tool options |
|---|---|---|---|
calculator |
Evaluate an arithmetic expression exactly. Use it for any calculation instead of doing the arithmetic yourself. | none | none |
list_documents |
List the documents in the collection: id, title and length, in order. Use it to see what exists before reading. | none | none |
read_document |
Read a window of a document's text by id. Long documents are read in several calls by moving start. |
none | none |
corpus_grep |
Search every document's lines for words or a pattern, like grep. Returns doc_id:line: text with a little context. Good for identifiers, names and exact figures. |
none | max_results |
lookup_table |
Look up rows of the CSV / JSON tables in the collection by column value and return chosen columns. Exact cell values instead of searching prose. | none | none |
date_calc |
Date arithmetic: the difference between two dates, a date plus or minus an offset, or the weekday of a date. today is a fixed date for the whole run. |
none | none |
search |
Search the document collection and return the best matching passages with their document ids. Cite those ids in the answer. | none | none |
calculator¶
Evaluate an arithmetic expression exactly. Use it for any calculation instead of doing the arithmetic yourself.
Arguments the model passes:
| Option | Type | Default | Description |
|---|---|---|---|
expression |
str |
required | Arithmetic to evaluate, e.g. (1200 - 950) / 950 * 100. Supports + - * / // % ** (or ^), parentheses, round(x, digits), min, max, sum, abs, pi and e. |
list_documents¶
List the documents in the collection: id, title and length, in order. Use it to see what exists before reading.
Arguments the model passes:
| Option | Type | Default | Description |
|---|---|---|---|
contains |
str |
unset | Only documents whose title or id contains this text (case-insensitive). |
limit |
int |
50 |
Most documents to list. |
offset |
int |
0 |
Documents to skip (to page through a long list). |
read_document¶
Read a window of a document's text by id. Long documents are read in several calls by moving start.
Arguments the model passes:
| Option | Type | Default | Description |
|---|---|---|---|
doc_id |
str |
required | Document id, as listed by list_documents or cited in search results. |
start |
int |
0 |
Character offset to start reading at. |
length |
int |
2000 |
Characters to read (at most 8000). |
corpus_grep¶
Search every document's lines for words or a pattern, like grep. Returns doc_id:line: text with a little context. Good for identifiers, names and exact figures.
Arguments the model passes:
| Option | Type | Default | Description |
|---|---|---|---|
pattern |
str |
required | What to look for. By default every word must appear on the same line (case-insensitive); with regex it is a regular expression. |
regex |
bool |
false |
Treat pattern as a regular expression (simple patterns only). |
case_sensitive |
bool |
false |
|
doc_id |
str |
unset | Search only this document. |
context_lines |
int |
1 |
Lines of context around each match. |
max_results |
int |
20 |
Most matching lines to return. |
Options (tools: [{name: corpus_grep, ...}]):
| Option | Type | Default | Description |
|---|---|---|---|
max_results |
int |
50 |
Hard cap on matching lines per call, whatever the model asks for. |
lookup_table¶
Look up rows of the CSV / JSON tables in the collection by column value and return chosen columns. Exact cell values instead of searching prose.
Arguments the model passes:
| Option | Type | Default | Description |
|---|---|---|---|
where |
dict |
required | Column values the row must have, e.g. {"product": "HarborShield"}. Compared ignoring case and surrounding spaces. |
select |
list |
unset | Columns to return (default: all of them). |
source |
str |
unset | Only rows of the table whose file path or title contains this text. |
limit |
int |
5 |
Most rows to return. |
date_calc¶
Date arithmetic: the difference between two dates, a date plus or minus an offset, or the weekday of a date. today is a fixed date for the whole run.
Arguments the model passes:
| Option | Type | Default | Description |
|---|---|---|---|
operation |
diff | add | weekday | today |
required | diff: days from date to other_date. add: date plus the given offsets. weekday: the weekday of date. today: the current date. |
date |
str |
unset | A date: YYYY-MM-DD, March 5, 2025, or today. The start for diff, the base for add (default today). |
other_date |
str |
unset | diff only: the end date. |
days |
int |
0 |
add only: days to add (negative to subtract). |
weeks |
int |
0 |
add only: weeks to add. |
months |
int |
0 |
add only: calendar months to add. |
years |
int |
0 |
add only: years to add. |
search¶
Search the document collection and return the best matching passages with their document ids. Cite those ids in the answer.
Arguments the model passes:
| Option | Type | Default | Description |
|---|---|---|---|
query |
str |
required | What to search for; a short natural-language query or the key terms. |
top_k |
int |
5 |
Passages to return. |
Run-wide rules (tools: section)¶
| Option | Type | Default | Description |
|---|---|---|---|
allow |
list |
empty | Tools with side effects (network, filesystem) that may be used; a tool with side effects that is not listed here is refused when the config loads. |
allow_network |
bool |
false |
Permit tools that declare side_effects: network (they must also be listed in allow). No network tool is built in. |
timeout_s |
float |
10.0 |
A tool call that takes longer returns an error result instead of hanging the question. |
max_output_chars |
int |
4000 |
Tool output longer than this is cut and flagged truncated, so one call cannot flood the model's context. |
evaluation.tools_now (default 2026-01-01) is what date_calc treats as today for the whole run.
Systems that accept a tools: list: agent_search.
Network tools (http_get, web_search) are deliberately not built in: a benchmark that depends on the live web is not reproducible. See Network tools.
Tool-using systems¶
Two built-in systems call tools (their options are in systems.md):
agent_searchis a function-calling agent. It always has asearchtool (the same hybrid or vector retrieval the plain systems use) plus the tools in itstools:list, decides when to search, compute or grep, and writes the cited answer itself. Retrieval is scored on the passages its tool calls returned: each call that returned passages (search hits, grep matches, a document window) is one ranking, and the rankings are merged with Reciprocal Rank Fusion.agent_mode: react_jsonreplaces native function calling with one JSON action per reply, for models that cannot call functions.grep_agentis index-free: no chunking, no embeddings, no vector store. Its only tools arelist_documents,corpus_grepandread_document. It shows whether your corpus needs retrieval infrastructure at all. Ingestion is free; the cost is paid per question, in agent turns, and it takes notools:list.
Tool sets are benchmark variants. configs/all.yaml runs agent_search twice, once with tools: [] (search only) and once with tools: [calculator, date_calc, corpus_grep], so every comparison shows what the tools bought (the required_tool_used_rate and answer score on questions that need them) against what they cost ($/Q, latency, steps).
Limits per question: max_steps (model turns), max_tool_calls, max_cost_usd, max_tokens. When one is reached the agent stops using tools and is asked to answer with what it found; that answer is flagged best_effort in the agent block of per_question_results.jsonl, and a cap (not the step limit) also sets budget_exhausted. The final answer call is always made, so a question can end slightly over a cost cap by that one call.
Your own tools¶
Any Python function with type hints is a tool. Point to it by package.module:function:
tools:
- {name: lookup_price, path: "my_pkg.tools:lookup_price"}
def lookup_price(product: str, quantity: int = 1) -> str:
"""Price of a product in the internal catalogue.""" # the first paragraph becomes the description
...
- The parameters' type hints become the JSON schema the model sees (
Annotated[str, Field(description="...")]adds a description to one parameter). Every parameter needs a hint;*args/**kwargsare not supported. - A parameter named
ctx(or annotatedToolContext) receives the per-question context (ctx.corpus,ctx.now,ctx.state) and is hidden from the model. - Return a
str(used as is), any JSON-serialisable value (serialised), or aToolResult(to setdata,cost_usdorerroryourself). - Options for a custom tool:
description(overrides the docstring),side_effects(nonedefault,network,filesystem),timeout_s. - A custom tool cannot reuse a built-in's name. Packages can also contribute tools to the registry (see extending.md), which makes them valid in
requires_toolstoo.
MCP tools¶
The tools of any Model Context Protocol server can be offered to an agent, so a benchmark can ask whether your agent does better with the tools you run in production. Install the SDK (pip install 'ragbench[mcp]') and list the server and the tools the agent may call:
systems:
- type: agent_search
name: agent_with_mcp
tools:
- type: mcp
server: {command: [python, examples/mcp_server.py]} # started on stdio; or {url: "https://mcp.example.com/mcp"} for a running server
allow: [convert_units, days_between] # required: only these are offered, by the server's own names
prefix: shop_ # optional: the agent sees shop_convert_units
cost_usd: {days_between: 0.001} # optional price per call, for every tool (a number) or per tool; MCP reports none
timeout_s: 10 # optional per-call timeout (default `tools.timeout_s`)
- The server's own description and argument schema are what the agent is told, read when the first system that lists the server is built. A name under
allowthat the server does not offer fails the run before any question, with a did-you-mean hint. The config check itself never starts a server. - One connection per server per run, shared by every question, system and route that names it, and closed when the process exits. Keep the server read-only and stateless: questions run concurrently, and a tool that changes state makes the comparison unfair.
- Servers are
command(stdio) orurl(Streamable HTTP).env(command) andheaders(url) accept${VAR}, filled from the environment when the server is started, so a secret is never written to a config or a run directory; errors name the server without them. - Permissions.
allowon the ref is the explicit list of callable tools, so they need not also appear undertools.allow. Acommandserver runs a process on this machine (reported as afilesystemside effect); aurlserver is a network tool and needstools.allow_network: true; the recommendation also lists it among the system's network tools. - Reporting. Every call is a
toolstep like any other (arguments, output, latency,tool_cost), andtool_usage.csvhas one row per allowed tool with its calls, errors, average latency and total cost; a tool offered and never called still gets a row. A tool whose structured result (or JSON text) is a list of objects withdoc_idis also credited to the trajectory metrics (avg_steps_to_evidence,redundant_search_rate). Retrieval scores are computed only from the built-insearch,corpus_grepandread_documentcalls. --mocknever starts a server. Each allowed tool is still offered, with a free-form argument schema, and answers[mock mcp] tool(args)at the configured price, so a config with MCP tools runs offline in CI. Measure the real server without--mock(setevaluation.max_cost_usd).
configs/mcp_example.yaml runs the agent with and without the example server examples/mcp_server.py.
Safety model¶
Tool arguments come from a language model, so a tool must be safe against whatever it sends.
- No
eval.calculatorparses withastand evaluates a whitelist (numbers,+ - * / // % **, parentheses,round,min,max,sum,abs,pi,e). Names, attributes, calls to anything else, strings, comprehensions and lambdas are rejected. Expressions are limited to 500 characters and 200 nodes, exponents to 1000 and integer results to about 3000 digits, so9**9**9is an error, not a hang. - Regular expressions are vetted, not just timed. Python's
reholds the interpreter while it backtracks, so no timeout can stop(a+)+$.corpus_greprefuses such patterns (a repeated group containing a repeat or an alternation, back-references, patterns over 200 characters) before matching, and only scans the first 2000 characters of a line. Withoutregex: truethe pattern is plain words. - Timeouts. A call that exceeds
tools.timeout_s(default 10 s) returns an error result; the question carries on. The abandoned thread is a daemon and cannot keep the process alive. - Exceptions never propagate. An exception, a malformed return value, an unknown tool name or bad arguments all become a
ToolResultwitherrorset, which the model sees as an observation and can recover from. - Bounded output. Output over
tools.max_output_chars(default 4000) is cut and flaggedtruncated, so one call cannot flood the model's context. - Permissions. Built-in tools only read the corpus given to them. A tool with side effects must declare
side_effectsand be listed undertools.allow; a network tool also needstools.allow_network: true. Otherwise the config does not load. (An MCP tool is allowed by theallow:list of its own entry.) - Determinism. "Today" is
evaluation.tools_now, fixed for the run, so date answers do not depend on when the benchmark runs. Each question gets its ownToolContext, so concurrent questions never share state.
Network tools¶
None ships with RAGBench, because results from the live web cannot be reproduced and a benchmark should not make requests nobody asked for. To use one, write it as a custom tool with side_effects: network, list it under tools.allow, and set tools.allow_network: true:
tools:
allow: [web_search]
allow_network: true
systems:
- type: agent_search
tools: [{name: web_search, path: "my_pkg.tools:web_search", side_effects: network, timeout_s: 20}]
Mock runs call your function too, so guard it yourself (for example by returning a canned result when an API key is missing) if you want --mock to stay offline.
Metrics¶
Every tool call is a tool step in per_question_results.jsonl, and its cost counts as tool_cost. For systems that have tools, metrics_summary.csv adds:
| Column | Meaning |
|---|---|
avg_tool_calls |
Mean tool calls per question. |
tool_error_rate |
Share of calls that returned an error (empty if no call was made). |
required_tool_used_rate |
Over the questions that declare requires_tools: the share where the system called every required tool at least once. |
tool_usage.csv has one row per system and tool: calls, errors, error_rate, truncated (outputs that hit the size cap), questions_using, avg_latency_ms and total_cost_usd. A tool that was offered but never called still gets a row with 0 calls.
An agent's budget can cap tool use with max_tool_calls (see AgentBudget in extending.md).