Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
51f559d
feat(bfcl): add BFCL v3 test infrastructure
vschandramourya Feb 24, 2026
0900457
feat(bfcl): add BFCL pytest test classes
vschandramourya Feb 24, 2026
6c5bc3a
refactor(bfcl): address review feedback
vschandramourya Feb 24, 2026
c091618
remove bfcl logs from being tracked
vschandramourya Feb 24, 2026
6adf41f
Remove the baseline comparision code
vschandramourya Feb 24, 2026
5d9d32d
add pytest marker for GPU-dependent workers
vschandramourya Feb 24, 2026
de0bfdc
Replace module-level _all_cases with pytest_generate_tests for lazy l…
vschandramourya Feb 24, 2026
7c52ebd
fix(bfcl): thread-safe result collection and proper session lifecycle…
vschandramourya Feb 25, 2026
4cb86fc
fix(bfcl): replace set_run_dir with get_or_create_run_dir avoiding em…
vschandramourya Feb 25, 2026
4561812
lint fix
vschandramourya Feb 25, 2026
764fd7e
test(bfcl): improve data downloading and loader fail-fast behavior
vschandramourya Mar 20, 2026
ea384a9
test(bfcl): tighten evaluator validation
vschandramourya Mar 20, 2026
88b1a98
fix(bfcl): fix session teardown issue
vschandramourya Mar 20, 2026
b1a4318
fix(bfcl): resolve mypy issues in evaluator
vschandramourya Mar 20, 2026
44f12b5
test(e2e)mark standalone BFCL suite as e2e
vschandramourya Mar 20, 2026
056ad64
fix(e2e) keep all ignored files at one place
vschandramourya Mar 24, 2026
e52b7ab
fix(e2e) correct the file imports and fail gracefully
vschandramourya Mar 24, 2026
17f3b98
refactor(bfcl): move response helpers from test file into bfcl package
vschandramourya Mar 27, 2026
d6e0244
Merge branch 'main' into feat/bfcl-test-infrastructure
vschandramourya Mar 27, 2026
6ef5353
Merge branch 'main' into feat/bfcl-test-infrastructure
vschandramourya Mar 27, 2026
29bc201
refactor(bfcl): convert case dict to BFCLCase dataclass
vschandramourya Mar 30, 2026
f4a1df0
Merge branch 'main' into feat/bfcl-test-infrastructure
vschandramourya Apr 1, 2026
5981cf3
Update e2e_test/bfcl/converter.py
vschandramourya Apr 7, 2026
ecb0361
Merge branch 'main' into feat/bfcl-test-infrastructure
vschandramourya Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,11 @@ clients/java/target/
clients/java/build.sbt
clients/java/gradle.properties
openapitools.json

# BFCL test data (downloaded from HuggingFace at runtime)
e2e_test/bfcl/data/*.json
!e2e_test/bfcl/data/.gitkeep

# BFCL test logs (generated per run)
e2e_test/bfcl_logs/*
!e2e_test/bfcl_logs/.gitkeep
25 changes: 25 additions & 0 deletions e2e_test/bfcl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""BFCL (Berkeley Function Calling Leaderboard) test infrastructure.

Provides data loading, evaluation, and per-test logging for open-source
BFCL v3 test cases run against the SMG gateway.
"""

from .converter import bfcl_to_openai_tools
from .evaluator import BFCLEvaluator, extract_tool_calls, log_file_for_summary
from .loader import (
BFCL_CATEGORIES,
BFCLCase,
MissingBFCLAnswerFileError,
load_bfcl_category,
)

__all__ = [
"BFCL_CATEGORIES",
"BFCLCase",
"BFCLEvaluator",
"MissingBFCLAnswerFileError",
"bfcl_to_openai_tools",
"extract_tool_calls",
"load_bfcl_category",
"log_file_for_summary",
]
47 changes: 47 additions & 0 deletions e2e_test/bfcl/converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""BFCL-to-OpenAI format converter.

Converts BFCL function definitions to OpenAI-compatible tool calling format,
handling BFCL-specific schema quirks (non-standard JSON Schema types).
"""

from __future__ import annotations


def _fix_parameter_type(params: dict) -> dict:
"""Convert BFCL's non-standard types to valid JSON Schema types recursively."""
result = dict(params)
ptype = result.get("type")
if ptype == "dict":
result["type"] = "object"
elif ptype == "float":
result["type"] = "number"
Comment thread
vschandramourya marked this conversation as resolved.
elif ptype == "int":
result["type"] = "integer"
elif ptype in ("list", "tuple"):
result["type"] = "array"
props = result.get("properties")
if isinstance(props, dict):
result["properties"] = {
k: _fix_parameter_type(v) if isinstance(v, dict) else v for k, v in props.items()
}
items = result.get("items")
if isinstance(items, dict):
result["items"] = _fix_parameter_type(items)
return result


def bfcl_to_openai_tools(bfcl_functions: list[dict]) -> list[dict]:
"""Convert BFCL function definitions to OpenAI tools format.

Handles the BFCL-specific quirks:
- parameters.type "dict" → "object"
- parameters.type "float" → "number"
- Wraps in {"type": "function", "function": ...}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: The docstring still only mentions dict → object and float → number, but the implementation now also handles int → integer and list/tuple → array. Worth updating the docstring to reflect the complete set of conversions.

Suggested change
- Wraps in {"type": "function", "function": ...}
"""Convert BFCL function definitions to OpenAI tools format.
Handles the BFCL-specific quirks:
- parameters.type "dict" → "object"
- parameters.type "float" → "number"
- parameters.type "int" → "integer"
- parameters.type "list"/"tuple" → "array"
- Wraps in {"type": "function", "function": ...}
"""

"""
tools = []
for fn in bfcl_functions:
fixed_fn = dict(fn)
if "parameters" in fixed_fn:
fixed_fn["parameters"] = _fix_parameter_type(fixed_fn["parameters"])
tools.append({"type": "function", "function": fixed_fn})
return tools
Empty file added e2e_test/bfcl/data/.gitkeep
Empty file.
83 changes: 83 additions & 0 deletions e2e_test/bfcl/download_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Download BFCL v3 open-source test data from HuggingFace.

Usage:
python e2e_test/bfcl/download_data.py
"""

from __future__ import annotations

import os
import shutil
import urllib.request
from pathlib import Path

HF_BASE = (
"https://huggingface.co/datasets/gorilla-llm/Berkeley-Function-Calling-Leaderboard/resolve/main"
)

DATA_DIR = Path(__file__).parent / "data"
TIMEOUT_SECONDS = 60

FILES = [
"BFCL_v3_simple.json",
"BFCL_v3_multiple.json",
"BFCL_v3_parallel.json",
"BFCL_v3_parallel_multiple.json",
"BFCL_v3_irrelevance.json",
]

ANSWER_FILES = [
("possible_answer/BFCL_v3_simple.json", "BFCL_v3_simple_answer.json"),
("possible_answer/BFCL_v3_multiple.json", "BFCL_v3_multiple_answer.json"),
("possible_answer/BFCL_v3_parallel.json", "BFCL_v3_parallel_answer.json"),
(
"possible_answer/BFCL_v3_parallel_multiple.json",
"BFCL_v3_parallel_multiple_answer.json",
),
]


def _count_lines(path: Path) -> int:
with path.open(encoding="utf-8") as handle:
return sum(1 for _ in handle)


def _download_one(remote_path: str, local_name: str, *, force: bool = False) -> None:
url = f"{HF_BASE}/{remote_path}"
dest = DATA_DIR / local_name
if dest.exists() and not force:
print(f"Skipping {local_name} (already downloaded)")
print(f" -> {dest.name} ({_count_lines(dest)} entries)")
return

print(f"Downloading {local_name}...")
tmp = dest.with_suffix(dest.suffix + ".tmp")
try:
with urllib.request.urlopen(url, timeout=TIMEOUT_SECONDS) as resp, tmp.open("wb") as out:
shutil.copyfileobj(resp, out)
out.flush()
os.fsync(out.fileno())
tmp.replace(dest)
finally:
if tmp.exists():
tmp.unlink()

lines = _count_lines(dest)
print(f" → {dest.name} ({lines} entries)")


def download(*, force_redownload: bool = False) -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)

for fname in FILES:
_download_one(fname, fname, force=force_redownload)

for remote_path, local_name in ANSWER_FILES:
_download_one(remote_path, local_name, force=force_redownload)

print("\nDone. All BFCL v3 data downloaded.")


if __name__ == "__main__":
download()
Loading
Loading