Spaces:
Sleeping
Sleeping
| """ | |
| Task #22 — Benchmark any model/provider against the ground truth oracle. | |
| Usage: | |
| python3 tests/benchmark.py --provider anthropic --model claude-haiku-4-5 | |
| python3 tests/benchmark.py --provider ollama --model llama3.2 | |
| python3 tests/benchmark.py --all | |
| Output: | |
| Ranked comparison table printed to stdout. | |
| Writes / updates tests/benchmark_results.json. | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import time | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| _env_file = Path(__file__).parent.parent / ".env" | |
| if _env_file.exists(): | |
| for _line in _env_file.read_text().splitlines(): | |
| _line = _line.strip() | |
| if _line and not _line.startswith("#") and "=" in _line: | |
| _k, _, _v = _line.partition("=") | |
| os.environ.setdefault(_k.strip(), _v.strip()) | |
| import anthropic | |
| from rapidfuzz import fuzz | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from pipeline.extract import MODELS, MODELS_OLLAMA, analyze_post | |
| ROOT = Path(__file__).parent.parent | |
| ORACLE = ROOT / "tests" / "fixtures" / "test_posts.json" | |
| RESULTS = ROOT / "tests" / "benchmark_results.json" | |
| RATE_LIMIT_DELAY = 0.3 # seconds between API calls | |
| # Scoring weights | |
| W_IS_PLACE = 0.40 | |
| W_NAME = 0.25 | |
| W_CITY = 0.15 | |
| W_COMPLETENESS = 0.20 | |
| # City aliases for lenient matching (lowercase) | |
| CITY_ALIASES: dict[str, set[str]] = { | |
| "san francisco": {"sf", "san francisco", "the city"}, | |
| "new york": {"ny", "new york", "new york city", "nyc"}, | |
| "los angeles": {"la", "los angeles"}, | |
| } | |
| def _city_match(pred: str, truth: str) -> bool: | |
| p, t = pred.lower().strip(), truth.lower().strip() | |
| if p == t: | |
| return True | |
| for canonical, aliases in CITY_ALIASES.items(): | |
| if t in aliases or t == canonical: | |
| if p in aliases or p == canonical: | |
| return True | |
| return False | |
| def _load_oracle() -> list[dict]: | |
| if not ORACLE.exists(): | |
| sys.exit(f"Oracle not found: {ORACLE}\nRun python3 tests/generate_ground_truth.py first.") | |
| with open(ORACLE, encoding="utf-8") as f: | |
| return json.load(f) | |
| def _load_results() -> dict: | |
| if RESULTS.exists(): | |
| with open(RESULTS) as f: | |
| return json.load(f) | |
| return {} | |
| def _save_results(data: dict) -> None: | |
| with open(RESULTS, "w") as f: | |
| json.dump(data, f, indent=2) | |
| def run_benchmark( | |
| provider: str, | |
| model: str, | |
| oracle: list[dict], | |
| ollama_url: str = "http://localhost:11434", | |
| ) -> dict: | |
| client = anthropic.Anthropic() if provider == "anthropic" else None | |
| is_place_correct = 0 | |
| name_scores: list[float] = [] | |
| city_correct = 0 | |
| completeness_scores: list[float] = [] | |
| place_posts = [p for p in oracle if p["ground_truth"]["is_place"]] | |
| print(f"\n Benchmarking {model} ({provider}) on {len(oracle)} posts …") | |
| for i, post in enumerate(oracle, 1): | |
| gt = post["ground_truth"] | |
| try: | |
| pred = analyze_post( | |
| client, | |
| post["caption"], | |
| hashtags=post.get("hashtags"), | |
| model=model, | |
| provider=provider, | |
| ollama_url=ollama_url, | |
| ) | |
| except Exception as exc: | |
| print(f" [{i:02d}] ERROR: {exc}") | |
| pred = None | |
| pred_is_place = pred is not None | |
| if pred_is_place == gt["is_place"]: | |
| is_place_correct += 1 | |
| if gt["is_place"] and pred is not None: | |
| # Name recall — fuzzy token sort ratio ≥ 80 counts as a match | |
| gt_name = gt.get("name", "UNKNOWN") | |
| pred_name = pred.get("name", "UNKNOWN") | |
| name_score = fuzz.token_sort_ratio(pred_name.lower(), gt_name.lower()) / 100.0 | |
| name_scores.append(name_score) | |
| # City recall | |
| gt_city = gt.get("city", "UNKNOWN") | |
| pred_city = pred.get("city", "UNKNOWN") | |
| city_correct += 1 if _city_match(pred_city, gt_city) else 0 | |
| # Completeness — % of place fields that are non-UNKNOWN | |
| fields = ("name", "city", "state", "country", "cuisine", "price_range", "highlight", "occasion") | |
| known = sum(1 for k in fields if pred.get(k, "UNKNOWN") != "UNKNOWN") | |
| completeness_scores.append(known / len(fields)) | |
| time.sleep(RATE_LIMIT_DELAY) | |
| n = len(oracle) | |
| n_place = len(place_posts) | |
| is_place_accuracy = round(is_place_correct / n * 100, 1) if n else 0 | |
| name_recall = round(sum(name_scores) / len(name_scores) * 100, 1) if name_scores else 0 | |
| city_recall = round(city_correct / n_place * 100, 1) if n_place else 0 | |
| field_completeness = round(sum(completeness_scores) / len(completeness_scores) * 100, 1) if completeness_scores else 0 | |
| composite = round( | |
| is_place_accuracy * W_IS_PLACE | |
| + name_recall * W_NAME | |
| + city_recall * W_CITY | |
| + field_completeness * W_COMPLETENESS, | |
| 1, | |
| ) | |
| pricing = MODELS.get(model) or MODELS_OLLAMA.get(model) or {"input": 0, "output": 0} | |
| # Rough cost per 100 posts: 200 input tokens + 70 output tokens each | |
| cost_per_100 = (200 * pricing["input"] + 70 * pricing["output"]) / 1_000_000 * 100 | |
| return { | |
| "provider": provider, | |
| "is_place_accuracy": is_place_accuracy, | |
| "name_recall": name_recall, | |
| "city_recall": city_recall, | |
| "field_completeness": field_completeness, | |
| "composite_score": composite, | |
| "cost_per_100": round(cost_per_100, 4), | |
| "run_at": datetime.now(timezone.utc).isoformat(), | |
| } | |
| def print_table(results: dict) -> None: | |
| if not results: | |
| print("No results yet.") | |
| return | |
| rows = sorted(results.items(), key=lambda kv: kv[1].get("composite_score", 0), reverse=True) | |
| header = f"{'Model':<25} {'Provider':<12} {'IsPlace':>7} {'Name':>7} {'City':>7} {'Complete':>9} {'Score':>7} {'$/100':>7}" | |
| print("\n" + "=" * len(header)) | |
| print(header) | |
| print("-" * len(header)) | |
| for model_name, r in rows: | |
| print( | |
| f"{model_name:<25} {r['provider']:<12} " | |
| f"{r.get('is_place_accuracy', 0):>6.1f}% {r['name_recall']:>6.1f}% " | |
| f"{r['city_recall']:>6.1f}% {r['field_completeness']:>8.1f}% " | |
| f"{r['composite_score']:>6.1f} {r['cost_per_100']:>7.4f}" | |
| ) | |
| print("=" * len(header)) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Benchmark model extraction quality.") | |
| parser.add_argument("--provider", choices=["anthropic", "ollama"]) | |
| parser.add_argument("--model") | |
| parser.add_argument("--all", action="store_true", help="Run all known models") | |
| parser.add_argument("--ollama-url", default="http://localhost:11434") | |
| args = parser.parse_args() | |
| oracle = _load_oracle() | |
| results = _load_results() | |
| targets: list[tuple[str, str]] = [] | |
| if args.all: | |
| targets += [("anthropic", m) for m in MODELS] | |
| targets += [("ollama", m) for m in MODELS_OLLAMA] | |
| elif args.provider and args.model: | |
| targets = [(args.provider, args.model)] | |
| else: | |
| parser.error("Provide --provider and --model, or use --all") | |
| for provider, model in targets: | |
| entry = run_benchmark(provider, model, oracle, ollama_url=args.ollama_url) | |
| results[model] = entry | |
| _save_results(results) | |
| print(f" → composite_score={entry['composite_score']}") | |
| print_table(results) | |
| print(f"\nResults written to {RESULTS.relative_to(ROOT)}") | |
| if __name__ == "__main__": | |
| main() | |