mirror of
https://github.com/microsoft/fara.git
synced 2026-08-21 17:50:45 +08:00
Three changes in one PR: 1. Remove webeval's dependency on ``autogen-core`` / ``autogen-ext``. All chat completion clients, message types, and the graceful-retry layer now live under ``webeval/src/webeval/oai_clients/`` — self-contained wrappers around openai / azure-identity. Install no longer needs the autogen submodule; just ``pip install -e .[vllm]`` then ``cd webeval; pip install -e .``. 2. Incorporate the initial (now-stale) WebTailBench benchmark into the codebase. ``webeval/src/webeval/benchmarks/webtailbench/`` + ``webeval/scripts/webtailbench.py``. Loader auto-downloads ``WebTailBench-v1-rubrics.tsv`` from ``huggingface.co/datasets/microsoft/WebTailBench`` and threads each task's published ``precomputed_rubric`` through to the verifier so rubrics never get regenerated. 3. Release the Universal Verifier (``MMRubricAgent``) as the official judge for WebTailBench. Multimodal, rubric-grounded, two-model ensemble (``gpt-5.2`` + ``o4-mini``) with per-criterion scoring, outcome verification, ambiguity / invalid-task classification, and first-point-of-failure analysis. ``webeval/scripts/verify_trajectories.py`` is a stand-alone parallel runner that re-scores any directory of webeval-shaped trajectories without touching the solver. Documentation: repo-root README ``Updates`` section + Reproducibility CLI block; ``webeval/README.md`` documents the Trajectory / FinalAnswer schema, the ``<no_answer>`` semantics, and per-benchmark score-file shape. Tests: 18 passing, 1 skipped (opt-in HF download). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
240 lines
8.8 KiB
Python
240 lines
8.8 KiB
Python
"""Run WebTailBench scored by the Universal Verifier (MMRubricAgent).
|
|
|
|
Example:
|
|
# Single split ("flights") with 1 process, using the Foundry endpoint
|
|
# configs for both the solver model and the judge (gpt-5.2):
|
|
python scripts/webtailbench.py \
|
|
--model_endpoint ../endpoint_configs/fara_foundry \
|
|
--eval_oai_config ../endpoint_configs/gpt5.2_prod \
|
|
--judge_eval_model gpt-5 \
|
|
--split flights \
|
|
--processes 1 --subsample 0.05 --max_rounds 30
|
|
"""
|
|
|
|
from webeval.systems.websurfer import WebSurferSystem
|
|
from webeval.benchmarks import WebTailBenchBenchmark
|
|
from pathlib import Path
|
|
import numpy as np
|
|
import os
|
|
import logging
|
|
import mlflow
|
|
from eval_exp import EvalExp, ModelReference, get_foundry_endpoint_configs
|
|
from webeval.oai_clients.graceful_client import GracefulRetryClient
|
|
from webeval.eval_result import EvalResult, Stage
|
|
from arg_parsing import get_eval_args
|
|
|
|
|
|
class Callback:
|
|
def __init__(self):
|
|
self.scores = []
|
|
|
|
def __call__(self, result: EvalResult, mlflow_facade, mlflow_run_id: str):
|
|
if result.stage == Stage.EVALUATED:
|
|
self.scores.append(result.score)
|
|
mlflow_facade.log_metric(
|
|
"score", np.mean(self.scores or [0]), run_id=mlflow_run_id
|
|
)
|
|
|
|
|
|
def add_webtailbench_args(parser):
|
|
parser.add_argument(
|
|
"--split",
|
|
type=str,
|
|
default=None,
|
|
help=(
|
|
"WebTailBench category to filter on (e.g. flights, hotels, "
|
|
"shopping, restaurants, activities, ticketing, real-estate, "
|
|
"jobs, shopping_list, comparison_shopping, compositional_tasks). "
|
|
"Omit to evaluate all categories."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--include_refusals",
|
|
action="store_true",
|
|
help="Also load WebTailBench-Refusals.tsv (the 111 harmful-task split).",
|
|
)
|
|
parser.add_argument(
|
|
"--judge_eval_model",
|
|
type=str,
|
|
default="gpt-5",
|
|
help=(
|
|
"Which judge LLM to pick from --eval_oai_config (default: gpt-5; "
|
|
"pass '*' to use all available configs)."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--judge_o4_eval_model",
|
|
type=str,
|
|
default=None,
|
|
help=(
|
|
"Optional separate o4-mini judge endpoint. Defaults to the "
|
|
"gpt-5 judge if unset. Expects --eval_oai_config to include "
|
|
"o4-mini configs."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--rubric_score_threshold",
|
|
type=float,
|
|
default=0.8,
|
|
help="Rubric pass threshold (default 0.8).",
|
|
)
|
|
parser.add_argument(
|
|
"--majority_vote_instances",
|
|
type=int,
|
|
default=1,
|
|
help="Odd number of MMRubric instances for majority voting (default 1).",
|
|
)
|
|
parser.add_argument(
|
|
"--success",
|
|
choices=("outcome", "process", "both"),
|
|
default="outcome",
|
|
help=(
|
|
"Which Universal Verifier signal counts as 'success' for the "
|
|
"top-line score. 'outcome' (default) reports the binary "
|
|
"outcome_success field — this is the metric Fara-7B numbers "
|
|
"in the README are reported against. 'process' reports "
|
|
"rubric_is_success (rubric_score >= --rubric_score_threshold) "
|
|
"— more lenient, expect slightly higher numbers. 'both' "
|
|
"requires outcome_success AND process pass."
|
|
),
|
|
)
|
|
|
|
|
|
def main():
|
|
args = get_eval_args(add_webtailbench_args)
|
|
|
|
if args.browserbase:
|
|
assert os.environ.get("BROWSERBASE_API_KEY"), (
|
|
"BROWSERBASE_API_KEY environment variable must be set to use browserbase"
|
|
)
|
|
assert os.environ.get("BROWSERBASE_PROJECT_ID"), (
|
|
"BROWSERBASE_PROJECT_ID environment variable must be set to use browserbase"
|
|
)
|
|
|
|
experiment = EvalExp(ws=None, user=args.user, seed=args.seed)
|
|
|
|
with experiment.start_run() as run:
|
|
model_ref = ModelReference(
|
|
args.model_url,
|
|
args.model_port,
|
|
args.device_id,
|
|
args.web_surfer_kwargs.get("max_n_images", 3),
|
|
args.gpt_solver_model_name,
|
|
args.dtype,
|
|
args.enforce_eager,
|
|
use_external_endpoint=bool(args.model_endpoint),
|
|
)
|
|
|
|
logger = logging.getLogger("webtailbench-eval")
|
|
logger.setLevel(logging.INFO)
|
|
|
|
mlflow.log_param("max_rounds", args.max_rounds)
|
|
if args.web_surfer_model_type == "gpt_solver":
|
|
mlflow.log_param(
|
|
"web_surfer_model_type",
|
|
f"{args.web_surfer_model_type}/{args.gpt_solver_model_name}",
|
|
)
|
|
else:
|
|
mlflow.log_param("web_surfer_model_type", args.web_surfer_model_type)
|
|
mlflow.log_param("fn_call_template", args.fn_call_template)
|
|
|
|
if args.model_endpoint:
|
|
websurfer_client_cfg = get_foundry_endpoint_configs(args.model_endpoint)
|
|
logger.info(
|
|
f"Loaded {len(websurfer_client_cfg)} external endpoint config(s) "
|
|
f"from {args.model_endpoint}"
|
|
)
|
|
model_ref.model_url_to_log = websurfer_client_cfg[0]["base_url"]
|
|
model_ref.model_to_log = websurfer_client_cfg[0]["base_url"]
|
|
mlflow.log_param("using_external_endpoint", True)
|
|
mlflow.log_param(
|
|
"endpoint_config_path",
|
|
",".join([x["base_url"] for x in websurfer_client_cfg]),
|
|
)
|
|
else:
|
|
websurfer_client_cfg = {
|
|
"api_key": "NONE",
|
|
"model": "gpt-4o-mini-2024-07-18",
|
|
"base_url": f"http://0.0.0.0:{args.model_port}/v1",
|
|
}
|
|
if args.web_surfer_client_cfg is not None:
|
|
websurfer_client_cfg = args.web_surfer_client_cfg
|
|
if args.web_surfer_kwargs:
|
|
mlflow.log_params(
|
|
{f"web_surfer_kwargs.{k}": v for k, v in args.web_surfer_kwargs.items()}
|
|
)
|
|
|
|
system = WebSurferSystem(
|
|
system_name="WebSurfer",
|
|
web_surfer_model_type=args.web_surfer_model_type,
|
|
max_rounds=args.max_rounds,
|
|
websurfer_client_cfg=websurfer_client_cfg,
|
|
start_on_target_url=True,
|
|
browserbase=args.browserbase,
|
|
web_surfer_kwargs=args.web_surfer_kwargs,
|
|
gpt_solver_model_name=args.gpt_solver_model_name,
|
|
fn_call_template=args.fn_call_template,
|
|
step_budgets=args.step_budgets,
|
|
)
|
|
|
|
mlflow.log_param("eval_oai_config", args.eval_oai_config)
|
|
mlflow.log_param("judge_eval_model", args.judge_eval_model)
|
|
|
|
data_dir = Path(__file__).resolve().parent.parent / "data" / "webtailbench"
|
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# The MMRubricAgent uses two clients: a gpt-5 family one and an
|
|
# o4-mini one. We load them from the same --eval_oai_config dir
|
|
# using the GracefulRetryClient model-filter.
|
|
gpt5_client = GracefulRetryClient.from_path(
|
|
args.eval_oai_config, logger=logger, eval_model=args.judge_eval_model
|
|
)
|
|
o4_target = args.judge_o4_eval_model or args.judge_eval_model
|
|
try:
|
|
o4mini_client = GracefulRetryClient.from_path(
|
|
args.eval_oai_config, logger=logger, eval_model=o4_target
|
|
)
|
|
except ValueError:
|
|
# Fall back to gpt5_client when eval_oai_config has no o4 configs.
|
|
logger.warning(
|
|
f"No configs matched '{o4_target}' in {args.eval_oai_config}; "
|
|
f"reusing the gpt-5 judge as the o4-mini judge."
|
|
)
|
|
o4mini_client = gpt5_client
|
|
|
|
benchmark = WebTailBenchBenchmark(
|
|
data_dir=data_dir,
|
|
gpt5_client=gpt5_client,
|
|
o4mini_client=o4mini_client,
|
|
include_refusals=args.include_refusals,
|
|
redo_eval=args.redo_eval,
|
|
rubric_score_threshold=args.rubric_score_threshold,
|
|
majority_vote_instances=args.majority_vote_instances,
|
|
success_criterion=args.success,
|
|
)
|
|
mlflow.log_param("success_criterion", args.success)
|
|
mlflow.log_param("rubric_score_threshold", args.rubric_score_threshold)
|
|
|
|
mlflow.log_param("subsample", args.subsample)
|
|
mlflow.log_param("processes", args.processes)
|
|
mlflow.log_param("split", args.split)
|
|
mlflow.log_param("max_error_task_retries", args.max_error_task_retries)
|
|
experiment.run(
|
|
model_ref=model_ref,
|
|
system=system,
|
|
benchmark=benchmark,
|
|
out_url=args.out_url,
|
|
subsample=args.subsample,
|
|
redo_eval=args.redo_eval,
|
|
run_id=args.run_id,
|
|
split=args.split,
|
|
processes=args.processes,
|
|
callbacks=[Callback()],
|
|
eval_only=args.eval_only,
|
|
max_error_task_retries=args.max_error_task_retries,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|