Spaces:
Sleeping
Sleeping
| """ | |
| benchmarks/bfcl.py | |
| βββββββββββββββββββ | |
| Berkeley Function-Calling Leaderboard (BFCL) β local eval adapter. | |
| What it tests: Can the model call functions with correct names, | |
| argument names, and argument values given a user request + schema? | |
| Dataset: gorilla-llm/Berkeley-Function-Calling-Leaderboard on HF Hub | |
| OR local JSONL at cfg["data_path"] | |
| Scoring: Exact-match on function name + fuzzy-match on arguments. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import re | |
| from typing import Any | |
| from slm_evals.benchmarks.base import BaseBenchmark | |
| SYSTEM_PROMPT = """\ | |
| You are a helpful assistant with access to the following functions. | |
| To call a function, respond ONLY with a JSON object in this exact format: | |
| {"name": "<function_name>", "arguments": {<key>: <value>, ...}} | |
| Do not add any explanation or text outside the JSON. | |
| """ | |
| class BFCLBenchmark(BaseBenchmark): | |
| """ | |
| BFCL function-calling benchmark. | |
| Config keys (in benchmark_overrides.bfcl): | |
| data_path β local JSONL file (optional; falls back to HF Hub) | |
| categories β list of categories to filter on (optional) | |
| strict β bool, require perfect arg match (default: False) | |
| """ | |
| name = "bfcl" | |
| # ββ Dataset βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_dataset(self) -> list[dict]: | |
| data_path = self.cfg.get("data_path") | |
| if data_path: | |
| return self._load_local(data_path) | |
| else: | |
| return self._load_from_hub() | |
| def _load_local(self, path: str) -> list[dict]: | |
| samples = [] | |
| with open(path) as f: | |
| for line in f: | |
| line = line.strip() | |
| if line: | |
| samples.append(json.loads(line)) | |
| return samples | |
| def _load_from_hub(self) -> list[dict]: | |
| """ | |
| Pulls the BFCL dataset from HF Hub. | |
| Requires: pip install datasets | |
| """ | |
| try: | |
| from datasets import load_dataset | |
| except ImportError: | |
| raise ImportError("pip install datasets (required to fetch BFCL from Hub)") | |
| ds = load_dataset( | |
| "gorilla-llm/Berkeley-Function-Calling-Leaderboard", | |
| split="train", | |
| trust_remote_code=True, | |
| ) | |
| return list(ds) | |
| # ββ Prompt ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_prompt(self, sample: dict) -> str: | |
| functions_block = json.dumps(sample.get("function", []), indent=2) | |
| user_query = sample.get("question", sample.get("input", "")) | |
| return ( | |
| f"{SYSTEM_PROMPT}\n" | |
| f"Available functions:\n{functions_block}\n\n" | |
| f"User: {user_query}\n" | |
| f"Assistant:" | |
| ) | |
| # ββ Evaluation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def evaluate_sample(self, sample: dict, prediction: str) -> dict: | |
| """ | |
| 1. Parse model output as JSON. | |
| 2. Check function name matches ground truth. | |
| 3. Check arguments (strict = exact keys+values, else fuzzy). | |
| """ | |
| ground_truth = sample.get("ground_truth", {}) | |
| if isinstance(ground_truth, str): | |
| try: | |
| ground_truth = json.loads(ground_truth) | |
| except json.JSONDecodeError: | |
| ground_truth = {} | |
| # ββ Parse prediction ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| parsed = self._extract_json(prediction) | |
| if parsed is None: | |
| return { | |
| "passed": False, | |
| "score": 0.0, | |
| "note": "Could not parse JSON from model output", | |
| } | |
| # ββ Check function name βββββββββββββββββββββββββββββββββββββββββββββββ | |
| expected_name = ( | |
| ground_truth.get("name") | |
| or ground_truth.get("function_name") | |
| or "" | |
| ) | |
| predicted_name = parsed.get("name", "") | |
| name_ok = predicted_name.strip() == expected_name.strip() | |
| if not name_ok: | |
| return { | |
| "passed": False, | |
| "score": 0.2, # partial credit: valid JSON produced | |
| "note": f"Wrong fn name: got '{predicted_name}', expected '{expected_name}'", | |
| } | |
| # ββ Check arguments βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| expected_args = ground_truth.get("arguments", ground_truth.get("args", {})) | |
| predicted_args = parsed.get("arguments", parsed.get("args", {})) | |
| strict = self.cfg.get("strict", False) | |
| arg_score = self._score_args(expected_args, predicted_args, strict=strict) | |
| passed = arg_score >= (1.0 if strict else 0.8) | |
| return { | |
| "passed": passed, | |
| "score": round((0.5 + 0.5 * arg_score), 3), # name correct = 0.5, args = 0.5 | |
| "note": f"arg_match={arg_score:.2f}", | |
| } | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _extract_json(text: str) -> dict | None: | |
| """Extract the first JSON object from free-form model output.""" | |
| # Try direct parse first | |
| try: | |
| return json.loads(text.strip()) | |
| except json.JSONDecodeError: | |
| pass | |
| # Regex fallback: find first {...} block | |
| match = re.search(r"\{.*\}", text, re.DOTALL) | |
| if match: | |
| try: | |
| return json.loads(match.group()) | |
| except json.JSONDecodeError: | |
| pass | |
| return None | |
| def _score_args( | |
| expected: dict, | |
| predicted: dict, | |
| strict: bool = False, | |
| ) -> float: | |
| """Return 0β1 argument match score.""" | |
| if not expected: | |
| return 1.0 if not predicted else 0.8 | |
| if strict: | |
| return 1.0 if expected == predicted else 0.0 | |
| # Fuzzy: what fraction of expected keys are correctly predicted? | |
| hits = 0 | |
| for key, exp_val in expected.items(): | |
| pred_val = predicted.get(key) | |
| if pred_val is None: | |
| continue | |
| if str(pred_val).strip().lower() == str(exp_val).strip().lower(): | |
| hits += 1 | |
| elif str(exp_val).strip().lower() in str(pred_val).strip().lower(): | |
| hits += 0.5 # partial credit | |
| return hits / len(expected) | |