LLM-judged scorers (BinaryRubricScorer, LlmRater, GoalCompletionRate,
BehavioralMetrics, ParameterAnalysis, SkillsBestPractices) call
judge.generate(prompt) and parse the text back. By default the judge is
single-shot: it sees the prompt and produces an answer with no access to
external state.
Tool support lets a judge invoke registered tools mid-generation so it can
ground its answer in something the prompt alone can't tell it — for example,
fetching https://beam.apache.org/get-started/downloads/ to check that an
agent named the most-recent Apache Beam version.
Status: Gemini (
gcp_vertex_gemini) only. Claude SDK judge support is tracked as a follow-up.
Tools are opt-in per judge via a tools: list in the model config YAML.
Configs without the key keep the original single-shot codepath.
generator: gcp_vertex_gemini
vertex_model: gemini-2.5-pro
base_prompt: ""
execs_per_minute: 5
tools:
- fetch_urlUnknown tool names fail fast at GeminiGenerator.__init__ with a ValueError
that lists the available tools.
| Name | Purpose | Constraints |
|---|---|---|
fetch_url |
Fetch an HTTPS URL and return its body as text. HTML is stripped to plain text. | HTTPS only; SSRF guard against private/loopback/link-local/multicast/reserved IPs; 10s timeout; 50 KB body cap. |
fetch_url returns "Error: ..." strings on any failure (bad scheme, blocked
host, HTTP error, timeout) so the model can observe the failure and react
rather than crash the judge.
datasets/model_configs/gemini_2.5_pro_with_tools_model.yaml (new)
generator: gcp_vertex_gemini
vertex_model: gemini-2.5-pro
base_prompt: ""
execs_per_minute: 5
tools:
- fetch_urldatasets/your-eval/example_run_config.yaml
dataset_config: datasets/your-eval/scenarios.evalset.json
dataset_format: agent-format
orchestrator: agent
model_config: datasets/model_configs/gemini_cli_model.yaml
simulated_user_model_config: datasets/model_configs/gemini_2.5_pro_model.yaml
scorers:
binary_rubric_scorer:
model_config: datasets/model_configs/gemini_2.5_pro_with_tools_model.yaml
goal_completion:
model_config: datasets/model_configs/gemini_2.5_pro_model.yaml # no tools
trajectory_matcher: {}
turn_count: {}
reporting:
csv:
output_directory: 'results'datasets/your-eval/scenarios.evalset.json (rubric lives on the scenario)
{
"scenarios": [
{
"id": "beam-version-check",
"starting_prompt": "What is the most recent version of Apache Beam?",
"conversation_plan": "User wants the agent to look up the current Beam release.",
"binary_rubric": [
"Did the agent's final answer state the most-recent Apache Beam version listed on https://beam.apache.org/get-started/downloads/?"
],
"max_turns": 2
}
]
}- The run config's
scorers.binary_rubric_scorer.model_configis loaded viaget_generator(...)insideBinaryRubricScorer.__init__(evalbench/scorers/binaryrubricscorer.py). - That YAML has
generator: gcp_vertex_geminiplustools: [fetch_url], soGeminiGeneratorresolves the names throughevalbench/generators/models/tools/__init__.py::get_tools(...)and builds the nativetypes.Toolconfig once at construction. - When the scorer calls
self.model.generate(prompt), the judge enters the tool-calling loop. If the model emits afunction_call, the registered tool runs, its result is appended as aFunctionResponse, and the loop continues until the model returns a final text answer. - The loop is bounded at
MAX_TOOL_ITERATIONS = 5(evalbench/generators/models/gemini.py). On exhaust the judge returns the last text and logs a warning.
Per-scorer judges are independent, so you can give fetch_url only to the
rubric judge and leave latency-sensitive judges (behavioral_metrics,
parameter_analysis) on plain single-shot configs.
- HTTPS-only. Other schemes (
http,file, ...) are rejected before any network call. - SSRF guard.
fetch_urlresolves the hostname and rejects responses pointing at private / loopback / link-local / multicast / reserved address ranges (Python'sipaddressclassification). - Known follow-ups:
- Redirect SSRF.
urlopenfollows 3xx without re-validating each hop. A malicious site can return a 302 to a private IP and bypass the upfront check. Fix is a customHTTPRedirectHandlerthat re-runs the host check on every hop, or disabling redirects entirely. - DNS TOCTOU.
_is_blocked_hostresolves the name once;urlopenresolves it again. DNS rebinding can flip between the two. Hardening requires socket-level interception.
- Redirect SSRF.
-
Create
evalbench/generators/models/tools/<my_tool>.pyexporting aToolinstance:from .base import Tool def _impl(args: dict) -> str: return f"got args: {args}" MY_TOOL = Tool( name="my_tool", description="One-sentence summary the model will read.", input_schema={ "type": "object", "properties": {"x": {"type": "string"}}, "required": ["x"], }, fn=_impl, )
-
Register it in
evalbench/generators/models/tools/__init__.py:from .my_tool import MY_TOOL TOOL_REGISTRY = { FETCH_URL_TOOL.name: FETCH_URL_TOOL, MY_TOOL.name: MY_TOOL, }
-
Reference it in any judge model config:
tools: - fetch_url - my_tool
Tool implementations should return strings (errors as "Error: ..."), keep
side effects bounded, and avoid raising — exceptions are caught and converted
into "Error: <ExcType>: <message>" strings so the loop can continue, but a
well-behaved tool produces actionable error text itself.