Files
shishirpatil--gorilla/berkeley-function-call-leaderboard/bfcl_eval/__main__.py
T
wehub-resource-sync bbfc60cd69
Publish BFCL to PyPI / build_and_publish (push) Waiting to run
Update API Zoo Data / send-updates (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:37:27 +08:00

339 lines
11 KiB
Python

import csv
from datetime import datetime
import os
from types import SimpleNamespace
from typing import List, Optional
import typer
from importlib.metadata import version as _version
from bfcl_eval._llm_response_generation import main as generation_main
from bfcl_eval.constants.category_mapping import TEST_COLLECTION_MAPPING
from bfcl_eval.constants.eval_config import (
DOTENV_PATH,
PROJECT_ROOT,
RESULT_PATH,
SCORE_PATH,
)
from bfcl_eval.constants.model_config import MODEL_CONFIG_MAPPING
from bfcl_eval.eval_checker.eval_runner import main as evaluation_main
from dotenv import load_dotenv
from tabulate import tabulate
class ExecutionOrderGroup(typer.core.TyperGroup):
def list_commands(self, ctx):
return [
"models",
"test-categories",
"generate",
"results",
"evaluate",
"scores",
"version",
]
cli = typer.Typer(
context_settings=dict(help_option_names=["-h", "--help"]),
no_args_is_help=True,
cls=ExecutionOrderGroup,
)
def handle_multiple_input(input_str):
"""
Input is like 'a,b,c,d', we need to transform it to ['a', 'b', 'c', 'd'] because that's the expected format in the actual main funciton
"""
if input_str is None:
"""
Cannot return None here, as typer will check the length of the return value and len(None) will raise an error
But when default is None, an empty list will be internally converted to None, and so the pipeline still works as expected
```
if default_value is None and len(value) == 0:
return None
```
"""
return []
return [item.strip() for item in ",".join(input_str).split(",") if item.strip()]
@cli.command()
def version():
"""
Show the bfcl version. PyPI versions are in development, please rely on the commit hash for reproducibility.
"""
print(f"bfcl version: {_version('bfcl')} \nNote: pypi versions are in development, please rely on the commit hash for reproducibility.")
@cli.command()
def test_categories():
"""
List available test categories.
"""
table = tabulate(
[
(category, "\n".join(test for test in tests))
for category, tests in TEST_COLLECTION_MAPPING.items()
],
headers=["Test category", "Test names"],
tablefmt="grid",
)
print(table)
@cli.command()
def models():
"""
List available models.
"""
table = tabulate(
[[model] for model in MODEL_CONFIG_MAPPING.keys()],
tablefmt="plain",
colalign=("left",),
)
print(table)
@cli.command()
def generate(
model: List[str] = typer.Option(
["gorilla-openfunctions-v2"],
help="A list of model names to generate the llm response. Use commas to separate multiple models.",
callback=handle_multiple_input
),
test_category: List[str] = typer.Option(
["all"],
help="A list of test categories to run the evaluation on. Use commas to separate multiple test categories.",
callback=handle_multiple_input
),
temperature: float = typer.Option(
0.001, help="The temperature parameter for the model."
),
include_input_log: bool = typer.Option(
False,
"--include-input-log",
help="Include the fully-transformed input to the model inference endpoint in the inference log; only relevant for debugging input integrity and format.",
),
exclude_state_log: bool = typer.Option(
False,
"--exclude-state-log",
help="Exclude info about the state of each API system after each turn in the inference log; only relevant for multi-turn categories.",
),
num_gpus: int = typer.Option(1, help="The number of GPUs to use."),
num_threads: Optional[int] = typer.Option(None, help="The number of threads to use."),
gpu_memory_utilization: float = typer.Option(0.9, help="The GPU memory utilization."),
backend: str = typer.Option("sglang", help="The backend to use for the model."),
skip_server_setup: bool = typer.Option(
False,
"--skip-server-setup",
help="Skip vLLM/SGLang server setup and use existing endpoint specified by the LOCAL_SERVER_ENDPOINT and LOCAL_SERVER_PORT environment variables.",
),
local_model_path: Optional[str] = typer.Option(
None,
"--local-model-path",
help="Specify the path to a local directory containing the model's config/tokenizer/weights for fully offline inference. Use this only if the model weights are stored in a location other than the default HF_HOME directory.",
),
result_dir: str = typer.Option(
RESULT_PATH,
"--result-dir",
help="Path to the folder where output files will be stored; Path should be relative to the `berkeley-function-call-leaderboard` root folder",
),
allow_overwrite: bool = typer.Option(
False,
"--allow-overwrite",
"-o",
help="Allow overwriting existing results for regeneration.",
),
run_ids: bool = typer.Option(
False,
"--run-ids",
help="If true, also run the test entry mentioned in the test_case_ids_to_generate.json file, in addition to the --test_category argument.",
),
enable_lora: bool = typer.Option(
False,
"--enable-lora",
help="Enable LoRA for vLLM backend.",
),
max_lora_rank: Optional[int] = typer.Option(
None,
"--max-lora-rank",
help="Specify the maximum LoRA rank for vLLM backend.",
),
lora_modules: Optional[List[str]] = typer.Option(
None,
"--lora-modules",
help='Specify the path to the LoRA modules for vLLM backend in name="path" format. Can be specified multiple times.',
),
):
"""
Generate the LLM response for one or more models on a test-category (same as openfunctions_evaluation.py).
"""
args = SimpleNamespace(
model=model,
test_category=test_category,
temperature=temperature,
include_input_log=include_input_log,
exclude_state_log=exclude_state_log,
num_gpus=num_gpus,
num_threads=num_threads,
gpu_memory_utilization=gpu_memory_utilization,
backend=backend,
skip_server_setup=skip_server_setup,
local_model_path=local_model_path,
result_dir=result_dir,
allow_overwrite=allow_overwrite,
run_ids=run_ids,
enable_lora=enable_lora,
max_lora_rank=max_lora_rank,
lora_modules=lora_modules,
)
load_dotenv(dotenv_path=DOTENV_PATH, verbose=True, override=True) # Load the .env file
generation_main(args)
@cli.command()
def results(
result_dir: str = typer.Option(
None,
"--result-dir",
help="Relative path to the model response folder, if different from the default; Path should be relative to the `berkeley-function-call-leaderboard` root folder",
),
):
"""
List the results available for evaluation.
"""
def display_name(name: str):
"""
Undo the / -> _ transformation if it happened.
Args:
name (str): The name of the model in the result directory.
Returns:
str: The original name of the model.
"""
if name not in MODEL_CONFIG_MAPPING:
candidate = name.replace("_", "/")
if candidate in MODEL_CONFIG_MAPPING:
return candidate
print(f"Unknown model name: {name}")
return name
if result_dir is None:
result_dir = RESULT_PATH
else:
result_dir = (PROJECT_ROOT / result_dir).resolve()
results_data = []
for dir in result_dir.iterdir():
# Check if it is a directory and not a file
if not dir.is_dir():
continue
results_data.append(
(
display_name(dir.name),
datetime.fromtimestamp(dir.stat().st_ctime).strftime("%Y-%m-%d %H:%M:%S"),
)
)
print(
tabulate(
results_data,
headers=["Model name", "Creation time"],
tablefmt="pretty",
)
)
@cli.command()
def evaluate(
model: List[str] = typer.Option(
None,
help="A list of model names to evaluate.",
callback=handle_multiple_input
),
test_category: List[str] = typer.Option(
["all"],
help="A list of test categories to run the evaluation on.",
callback=handle_multiple_input
),
result_dir: str = typer.Option(
None,
"--result-dir",
help="Relative path to the model response folder, if different from the default; Path should be relative to the `berkeley-function-call-leaderboard` root folder",
),
score_dir: str = typer.Option(
None,
"--score-dir",
help="Relative path to the evaluation score folder, if different from the default; Path should be relative to the `berkeley-function-call-leaderboard` root folder",
),
partial_eval: bool = typer.Option(
False,
"--partial-eval",
help="Run evaluation on a partial set of benchmark entries (eg. entries present in the model result files) without raising for missing IDs.",
),
):
"""
Evaluate results from run of one or more models on a test-category (same as eval_runner.py).
"""
load_dotenv(dotenv_path=DOTENV_PATH, verbose=True, override=True) # Load the .env file
evaluation_main(model, test_category, result_dir, score_dir, partial_eval)
@cli.command()
def scores(
score_dir: str = typer.Option(
None,
"--score-dir",
help="Relative path to the evaluation score folder, if different from the default; Path should be relative to the `berkeley-function-call-leaderboard` root folder",
),
):
"""
Display the leaderboard.
"""
def truncate(text, length=22):
return (text[:length] + "...") if len(text) > length else text
if score_dir is None:
score_dir = SCORE_PATH
else:
score_dir = (PROJECT_ROOT / score_dir).resolve()
# files = ["./score/data_non_live.csv", "./score/data_live.csv", "./score/data_overall.csv"]
file = score_dir / "data_overall.csv"
selected_columns = [
"Rank",
"Model",
"Overall Acc",
"Non-Live AST Acc",
"Non-Live Exec Acc",
"Live Acc",
"Multi Turn Acc",
"Relevance Detection",
"Irrelevance Detection",
]
if file.exists():
with open(file, newline="") as csvfile:
reader = csv.reader(csvfile)
headers = next(reader) # Read the header row
column_indices = [headers.index(col) for col in selected_columns]
data = [
[row[i] for i in column_indices] for row in reader
] # Read the rest of the data
selected_columns = selected_columns[:-2] + [
"Relevance",
"Irrelevance",
] # Shorten the column names
print(tabulate(data, headers=selected_columns, tablefmt="grid"))
else:
print(f"\nFile {file} not found.\n")
if __name__ == "__main__":
cli()