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.
@@ -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,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
@@ -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