f1-temporal-bench 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- f1_temporal_bench-0.1.0/.github/workflows/ci.yml +19 -0
- f1_temporal_bench-0.1.0/.github/workflows/release.yml +27 -0
- f1_temporal_bench-0.1.0/.gitignore +7 -0
- f1_temporal_bench-0.1.0/LICENSE +21 -0
- f1_temporal_bench-0.1.0/PKG-INFO +68 -0
- f1_temporal_bench-0.1.0/README.md +45 -0
- f1_temporal_bench-0.1.0/data/questions.jsonl +3 -0
- f1_temporal_bench-0.1.0/pyproject.toml +33 -0
- f1_temporal_bench-0.1.0/src/f1_temporal_bench/__init__.py +1 -0
- f1_temporal_bench-0.1.0/src/f1_temporal_bench/cli.py +77 -0
- f1_temporal_bench-0.1.0/src/f1_temporal_bench/dataset.py +60 -0
- f1_temporal_bench-0.1.0/src/f1_temporal_bench/eval.py +71 -0
- f1_temporal_bench-0.1.0/src/f1_temporal_bench/models.py +30 -0
- f1_temporal_bench-0.1.0/tests/test_eval.py +29 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.11"
|
|
17
|
+
- run: pip install -e ".[dev]"
|
|
18
|
+
- run: pytest
|
|
19
|
+
- run: f1-temporal-bench validate
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # needed for PyPI trusted publishing
|
|
13
|
+
contents: write # needed to create the GitHub release
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.11"
|
|
19
|
+
- run: pip install build
|
|
20
|
+
- run: python -m build
|
|
21
|
+
- name: Publish to PyPI
|
|
22
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
23
|
+
- name: Create GitHub Release
|
|
24
|
+
uses: softprops/action-gh-release@v2
|
|
25
|
+
with:
|
|
26
|
+
files: dist/*
|
|
27
|
+
generate_release_notes: true
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 YOUR_NAME
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: f1-temporal-bench
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A benchmark for evaluating temporal knowledge and hallucination in language models, using Formula 1 as a fast-changing ground truth domain.
|
|
5
|
+
Project-URL: Homepage, https://github.com/YOUR_USERNAME/f1-temporal-bench
|
|
6
|
+
Project-URL: Issues, https://github.com/YOUR_USERNAME/f1-temporal-bench/issues
|
|
7
|
+
Author: Your Name
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: benchmark,formula1,huggingface,llm-evaluation,nlp
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: click>=8.1
|
|
13
|
+
Requires-Dist: huggingface-hub>=0.24
|
|
14
|
+
Requires-Dist: requests>=2.31
|
|
15
|
+
Requires-Dist: rich>=13.7
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
19
|
+
Provides-Extra: local-models
|
|
20
|
+
Requires-Dist: torch>=2.2; extra == 'local-models'
|
|
21
|
+
Requires-Dist: transformers>=4.40; extra == 'local-models'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# F1 Temporal Knowledge Benchmark
|
|
25
|
+
|
|
26
|
+
A benchmark for measuring how well language models track fast-changing,
|
|
27
|
+
verifiable facts — using Formula 1 standings, results, and records as
|
|
28
|
+
ground truth. F1 facts change every ~2 weeks during the season, making
|
|
29
|
+
this domain a natural, continuously-refreshed test of temporal knowledge
|
|
30
|
+
and hallucination in LLMs.
|
|
31
|
+
|
|
32
|
+
## Why F1?
|
|
33
|
+
|
|
34
|
+
Most knowledge benchmarks go stale the moment a model is trained. F1
|
|
35
|
+
gives us a domain where "true as of last week" and "true as of last year"
|
|
36
|
+
are meaningfully different — a clean way to measure whether a model
|
|
37
|
+
knows what it knows, or confidently guesses.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
\`\`\`bash
|
|
42
|
+
pip install f1-temporal-bench
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
\`\`\`bash
|
|
48
|
+
f1-temporal-bench validate
|
|
49
|
+
f1-temporal-bench run --model meta-llama/Llama-3.2-1B-Instruct
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
## Metrics
|
|
53
|
+
|
|
54
|
+
- **Accuracy** — exact/alias match against ground truth
|
|
55
|
+
- **Confidently wrong rate** — model gives a specific, wrong answer
|
|
56
|
+
instead of hedging
|
|
57
|
+
- **Refusal rate** — model declines to answer / says it doesn't know
|
|
58
|
+
|
|
59
|
+
## Contributing questions
|
|
60
|
+
|
|
61
|
+
Add new rows to \`data/questions.jsonl\` after each race weekend. Run
|
|
62
|
+
\`f1-temporal-bench validate\` before committing.
|
|
63
|
+
|
|
64
|
+
## Roadmap
|
|
65
|
+
|
|
66
|
+
- [ ] Automated dataset updates via GitHub Actions after each race
|
|
67
|
+
- [ ] Multi-model leaderboard published to GitHub Pages
|
|
68
|
+
- [ ] Dataset published on the Hugging Face Hub with versioned season snapshots
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# F1 Temporal Knowledge Benchmark
|
|
2
|
+
|
|
3
|
+
A benchmark for measuring how well language models track fast-changing,
|
|
4
|
+
verifiable facts — using Formula 1 standings, results, and records as
|
|
5
|
+
ground truth. F1 facts change every ~2 weeks during the season, making
|
|
6
|
+
this domain a natural, continuously-refreshed test of temporal knowledge
|
|
7
|
+
and hallucination in LLMs.
|
|
8
|
+
|
|
9
|
+
## Why F1?
|
|
10
|
+
|
|
11
|
+
Most knowledge benchmarks go stale the moment a model is trained. F1
|
|
12
|
+
gives us a domain where "true as of last week" and "true as of last year"
|
|
13
|
+
are meaningfully different — a clean way to measure whether a model
|
|
14
|
+
knows what it knows, or confidently guesses.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
\`\`\`bash
|
|
19
|
+
pip install f1-temporal-bench
|
|
20
|
+
\`\`\`
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
\`\`\`bash
|
|
25
|
+
f1-temporal-bench validate
|
|
26
|
+
f1-temporal-bench run --model meta-llama/Llama-3.2-1B-Instruct
|
|
27
|
+
\`\`\`
|
|
28
|
+
|
|
29
|
+
## Metrics
|
|
30
|
+
|
|
31
|
+
- **Accuracy** — exact/alias match against ground truth
|
|
32
|
+
- **Confidently wrong rate** — model gives a specific, wrong answer
|
|
33
|
+
instead of hedging
|
|
34
|
+
- **Refusal rate** — model declines to answer / says it doesn't know
|
|
35
|
+
|
|
36
|
+
## Contributing questions
|
|
37
|
+
|
|
38
|
+
Add new rows to \`data/questions.jsonl\` after each race weekend. Run
|
|
39
|
+
\`f1-temporal-bench validate\` before committing.
|
|
40
|
+
|
|
41
|
+
## Roadmap
|
|
42
|
+
|
|
43
|
+
- [ ] Automated dataset updates via GitHub Actions after each race
|
|
44
|
+
- [ ] Multi-model leaderboard published to GitHub Pages
|
|
45
|
+
- [ ] Dataset published on the Hugging Face Hub with versioned season snapshots
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"id": "2024-r08-wdc-leader", "date": "2024-05-27", "question": "Who was leading the F1 drivers' championship after round 8 of the 2024 season?", "answer": "Max Verstappen", "aliases": ["Verstappen"], "category": "wdc_standings", "season": 2024, "round": 8}
|
|
2
|
+
{"id": "2023-constructors-champion", "date": "2023-11-26", "question": "Which team won the F1 constructors' championship in 2023?", "answer": "Red Bull Racing", "aliases": ["Red Bull"], "category": "constructors_champion", "season": 2023, "round": null}
|
|
3
|
+
{"id": "2021-drivers-champion", "date": "2021-12-12", "question": "Who won the F1 drivers' championship in 2021?", "answer": "Max Verstappen", "aliases": ["Verstappen"], "category": "wdc_champion", "season": 2021, "round": null}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "f1-temporal-bench"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A benchmark for evaluating temporal knowledge and hallucination in language models, using Formula 1 as a fast-changing ground truth domain."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Your Name" }]
|
|
13
|
+
keywords = ["nlp", "llm-evaluation", "benchmark", "formula1", "huggingface"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"click>=8.1",
|
|
16
|
+
"huggingface_hub>=0.24",
|
|
17
|
+
"requests>=2.31",
|
|
18
|
+
"rich>=13.7",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
local-models = ["transformers>=4.40", "torch>=2.2"]
|
|
23
|
+
dev = ["pytest>=8.0", "ruff>=0.5"]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
f1-temporal-bench = "f1_temporal_bench.cli:main"
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/YOUR_USERNAME/f1-temporal-bench"
|
|
30
|
+
Issues = "https://github.com/YOUR_USERNAME/f1-temporal-bench/issues"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/f1_temporal_bench"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Command-line interface: f1-temporal-bench"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from .dataset import load_dataset, validate_dataset
|
|
11
|
+
from .eval import run_eval
|
|
12
|
+
from .models import query_hf_inference, query_local_model
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group()
|
|
18
|
+
def main():
|
|
19
|
+
"""F1 Temporal Knowledge Benchmark CLI."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@main.command()
|
|
24
|
+
@click.option("--path", default="data/questions.jsonl", help="Path to dataset.")
|
|
25
|
+
def validate(path):
|
|
26
|
+
"""Validate the dataset file for schema errors."""
|
|
27
|
+
ok, errors = validate_dataset(path)
|
|
28
|
+
if ok:
|
|
29
|
+
console.print(f"[green]Dataset valid: {path}[/green]")
|
|
30
|
+
else:
|
|
31
|
+
console.print(f"[red]{len(errors)} error(s) found:[/red]")
|
|
32
|
+
for e in errors:
|
|
33
|
+
console.print(f" - {e}")
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@main.command()
|
|
38
|
+
@click.option("--model", required=True, help="HF model id, e.g. meta-llama/Llama-3.2-1B-Instruct")
|
|
39
|
+
@click.option("--dataset", default="data/questions.jsonl", help="Path to dataset.")
|
|
40
|
+
@click.option("--local", is_flag=True, help="Run the model locally instead of via HF Inference API.")
|
|
41
|
+
@click.option("--output", default=None, help="Optional path to write JSON results.")
|
|
42
|
+
def run(model, dataset, local, output):
|
|
43
|
+
"""Evaluate a model against the dataset."""
|
|
44
|
+
questions = load_dataset(dataset)
|
|
45
|
+
console.print(f"Loaded {len(questions)} questions. Evaluating [bold]{model}[/bold]...")
|
|
46
|
+
|
|
47
|
+
query_fn = (
|
|
48
|
+
(lambda q: query_local_model(model, q))
|
|
49
|
+
if local
|
|
50
|
+
else (lambda q: query_hf_inference(model, q))
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
result, per_question = run_eval(query_fn, questions)
|
|
54
|
+
|
|
55
|
+
table = Table(title=f"Results: {model}")
|
|
56
|
+
table.add_column("Metric")
|
|
57
|
+
table.add_column("Value")
|
|
58
|
+
table.add_row("Total questions", str(result.total))
|
|
59
|
+
table.add_row("Accuracy", f"{result.accuracy:.1%}")
|
|
60
|
+
table.add_row("Confidently wrong", f"{result.confidently_wrong_rate:.1%}")
|
|
61
|
+
table.add_row("Refusal rate", f"{result.refusal_rate:.1%}")
|
|
62
|
+
console.print(table)
|
|
63
|
+
|
|
64
|
+
if output:
|
|
65
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
66
|
+
json.dump({
|
|
67
|
+
"model": model,
|
|
68
|
+
"accuracy": result.accuracy,
|
|
69
|
+
"confidently_wrong_rate": result.confidently_wrong_rate,
|
|
70
|
+
"refusal_rate": result.refusal_rate,
|
|
71
|
+
"details": per_question,
|
|
72
|
+
}, f, indent=2)
|
|
73
|
+
console.print(f"Saved results to {output}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Load and validate the F1 temporal knowledge dataset."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Question:
|
|
10
|
+
id: str
|
|
11
|
+
date: str # ISO date the fact became true (e.g. race date)
|
|
12
|
+
question: str
|
|
13
|
+
answer: str
|
|
14
|
+
aliases: list[str] = field(default_factory=list)
|
|
15
|
+
category: str = "general"
|
|
16
|
+
season: int | None = None
|
|
17
|
+
round: int | None = None
|
|
18
|
+
|
|
19
|
+
def matches(self, model_answer: str) -> bool:
|
|
20
|
+
"""Case-insensitive containment match against answer + aliases."""
|
|
21
|
+
candidates = [self.answer] + self.aliases
|
|
22
|
+
model_answer_lower = model_answer.lower()
|
|
23
|
+
return any(c.lower() in model_answer_lower for c in candidates)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_dataset(path: str | Path) -> list[Question]:
|
|
27
|
+
path = Path(path)
|
|
28
|
+
questions = []
|
|
29
|
+
with path.open(encoding="utf-8") as f:
|
|
30
|
+
for line_num, line in enumerate(f, start=1):
|
|
31
|
+
line = line.strip()
|
|
32
|
+
if not line:
|
|
33
|
+
continue
|
|
34
|
+
try:
|
|
35
|
+
row = json.loads(line)
|
|
36
|
+
except json.JSONDecodeError as e:
|
|
37
|
+
raise ValueError(f"Invalid JSON on line {line_num}: {e}") from e
|
|
38
|
+
questions.append(Question(**row))
|
|
39
|
+
return questions
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_dataset(path: str | Path) -> tuple[bool, list[str]]:
|
|
43
|
+
"""Returns (is_valid, list_of_errors)."""
|
|
44
|
+
errors = []
|
|
45
|
+
try:
|
|
46
|
+
questions = load_dataset(path)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
return False, [str(e)]
|
|
49
|
+
|
|
50
|
+
seen_ids = set()
|
|
51
|
+
for q in questions:
|
|
52
|
+
if q.id in seen_ids:
|
|
53
|
+
errors.append(f"Duplicate id: {q.id}")
|
|
54
|
+
seen_ids.add(q.id)
|
|
55
|
+
if not q.question.strip():
|
|
56
|
+
errors.append(f"{q.id}: empty question")
|
|
57
|
+
if not q.answer.strip():
|
|
58
|
+
errors.append(f"{q.id}: empty answer")
|
|
59
|
+
|
|
60
|
+
return len(errors) == 0, errors
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Run a model over the dataset and score it."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .dataset import Question
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
REFUSAL_PHRASES = [
|
|
9
|
+
"i don't know", "i'm not sure", "as of my knowledge",
|
|
10
|
+
"i cannot provide", "i am unable", "no information",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class EvalResult:
|
|
16
|
+
total: int
|
|
17
|
+
correct: int
|
|
18
|
+
confidently_wrong: int
|
|
19
|
+
refused: int
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def accuracy(self) -> float:
|
|
23
|
+
return self.correct / self.total if self.total else 0.0
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def confidently_wrong_rate(self) -> float:
|
|
27
|
+
return self.confidently_wrong / self.total if self.total else 0.0
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def refusal_rate(self) -> float:
|
|
31
|
+
return self.refused / self.total if self.total else 0.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_refusal(model_answer: str) -> bool:
|
|
35
|
+
lower = model_answer.lower()
|
|
36
|
+
return any(phrase in lower for phrase in REFUSAL_PHRASES)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_eval(query_fn, questions: list[Question]) -> tuple[EvalResult, list[dict]]:
|
|
40
|
+
"""query_fn: callable(question_text: str) -> str"""
|
|
41
|
+
correct = confidently_wrong = refused = 0
|
|
42
|
+
per_question = []
|
|
43
|
+
|
|
44
|
+
for q in questions:
|
|
45
|
+
model_answer = query_fn(q.question)
|
|
46
|
+
refusal = is_refusal(model_answer)
|
|
47
|
+
correct_flag = q.matches(model_answer)
|
|
48
|
+
|
|
49
|
+
if correct_flag:
|
|
50
|
+
correct += 1
|
|
51
|
+
elif refusal:
|
|
52
|
+
refused += 1
|
|
53
|
+
else:
|
|
54
|
+
confidently_wrong += 1
|
|
55
|
+
|
|
56
|
+
per_question.append({
|
|
57
|
+
"id": q.id,
|
|
58
|
+
"question": q.question,
|
|
59
|
+
"expected": q.answer,
|
|
60
|
+
"model_answer": model_answer,
|
|
61
|
+
"correct": correct_flag,
|
|
62
|
+
"refused": refusal,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
result = EvalResult(
|
|
66
|
+
total=len(questions),
|
|
67
|
+
correct=correct,
|
|
68
|
+
confidently_wrong=confidently_wrong,
|
|
69
|
+
refused=refused,
|
|
70
|
+
)
|
|
71
|
+
return result, per_question
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Thin wrapper around HF Inference API / local transformers pipeline."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from huggingface_hub import InferenceClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def query_hf_inference(model_id: str, question: str, token: str | None = None) -> str:
|
|
8
|
+
"""Query a model via the Hugging Face Inference API."""
|
|
9
|
+
token = token or os.environ.get("HF_TOKEN")
|
|
10
|
+
client = InferenceClient(model=model_id, token=token)
|
|
11
|
+
prompt = (
|
|
12
|
+
f"Answer the following question in one short phrase, "
|
|
13
|
+
f"with no explanation.\nQuestion: {question}\nAnswer:"
|
|
14
|
+
)
|
|
15
|
+
response = client.text_generation(prompt, max_new_tokens=30)
|
|
16
|
+
return response.strip()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def query_local_model(model_id: str, question: str) -> str:
|
|
20
|
+
"""Query a model loaded locally via transformers (requires the
|
|
21
|
+
'local-models' extra: pip install f1-temporal-bench[local-models])."""
|
|
22
|
+
from transformers import pipeline
|
|
23
|
+
|
|
24
|
+
generator = pipeline("text-generation", model=model_id)
|
|
25
|
+
prompt = (
|
|
26
|
+
f"Answer the following question in one short phrase, "
|
|
27
|
+
f"with no explanation.\nQuestion: {question}\nAnswer:"
|
|
28
|
+
)
|
|
29
|
+
result = generator(prompt, max_new_tokens=30, do_sample=False)
|
|
30
|
+
return result[0]["generated_text"][len(prompt):].strip()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from f1_temporal_bench.dataset import Question
|
|
2
|
+
from f1_temporal_bench.eval import is_refusal, run_eval
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_question_matches():
|
|
6
|
+
q = Question(id="t1", date="2024-01-01", question="Who won?", answer="Max Verstappen", aliases=["Verstappen"])
|
|
7
|
+
assert q.matches("The winner was Max Verstappen.")
|
|
8
|
+
assert q.matches("It was Verstappen.")
|
|
9
|
+
assert not q.matches("Lewis Hamilton won.")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_is_refusal():
|
|
13
|
+
assert is_refusal("I don't know the answer to that.")
|
|
14
|
+
assert not is_refusal("Max Verstappen")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_run_eval_scores_correctly():
|
|
18
|
+
questions = [
|
|
19
|
+
Question(id="t1", date="2024-01-01", question="Q1", answer="Alpha"),
|
|
20
|
+
Question(id="t2", date="2024-01-01", question="Q2", answer="Beta"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
fake_answers = {"Q1": "Alpha", "Q2": "I'm not sure"}
|
|
24
|
+
result, _ = run_eval(lambda q: fake_answers[q], questions)
|
|
25
|
+
|
|
26
|
+
assert result.total == 2
|
|
27
|
+
assert result.correct == 1
|
|
28
|
+
assert result.refused == 1
|
|
29
|
+
assert result.confidently_wrong == 0
|