discovery-by-llm/eval/baseline.py

114 lines
4.1 KiB
Python

"""Record bare Spotify search results and render side-by-side comparisons."""
import json
from pathlib import Path
import httpx
from scenario import Scenario
TOKEN_URL = "https://accounts.spotify.com/api/token"
SEARCH_URL = "https://api.spotify.com/v1/search"
async def record_baselines(
scenarios: tuple[Scenario, ...],
client_id: str,
client_secret: str,
snapshot_root: Path,
) -> None:
"""Snapshot the top ten results from bare searches for every query."""
snapshot_root.mkdir(parents=True, exist_ok=True)
async with httpx.AsyncClient(timeout=20.0) as client:
token_response = await client.post(
TOKEN_URL,
data={"grant_type": "client_credentials"},
auth=(client_id, client_secret),
)
token_response.raise_for_status()
access_token = token_response.json()["access_token"]
for scenario in scenarios:
response = await client.get(
SEARCH_URL,
params={"q": scenario.query, "type": "track", "limit": 10},
headers={"Authorization": f"Bearer {access_token}"},
)
response.raise_for_status()
tracks = response.json().get("tracks", {}).get("items", [])[:10]
_write_json(
snapshot_root / f"{scenario.key}.json",
{"key": scenario.key, "query": scenario.query, "tracks": tracks},
)
def write_comparison(
scenarios: tuple[Scenario, ...],
snapshot_root: Path,
report_root: Path,
top_n: int,
) -> Path:
"""Write a markdown comparison for scenarios with both result arms."""
report_root.mkdir(parents=True, exist_ok=True)
lines = ["# Pipeline and bare search comparison", ""]
compared = 0
for scenario in scenarios:
baseline_path = snapshot_root / f"{scenario.key}.json"
pipeline_path = report_root / f"{scenario.key}.json"
if not baseline_path.exists() or not pipeline_path.exists():
continue
baseline = json.loads(baseline_path.read_text(encoding="ascii"))
pipeline = json.loads(pipeline_path.read_text(encoding="ascii"))
lines.extend(_comparison_section(scenario, pipeline, baseline, top_n))
compared += 1
if compared == 0:
lines.extend(["No scenario has results from both arms yet.", ""])
output_path = report_root / "baseline-comparison.md"
output_path.write_text("\n".join(lines), encoding="utf-8")
return output_path
def _comparison_section(
scenario: Scenario,
pipeline: object,
baseline: object,
top_n: int,
) -> list[str]:
pipeline_tracks = pipeline.get("tracks", []) if isinstance(pipeline, dict) else []
baseline_tracks = baseline.get("tracks", []) if isinstance(baseline, dict) else []
lines = [f"## {scenario.chip}", "", "| Rank | Pipeline | Bare search |", "| ---: | --- | --- |"]
for index in range(top_n):
pipeline_label = _pipeline_label(pipeline_tracks, index)
baseline_label = _spotify_label(baseline_tracks, index)
lines.append(f"| {index + 1} | {pipeline_label} | {baseline_label} |")
lines.append("")
return lines
def _pipeline_label(tracks: object, index: int) -> str:
if not isinstance(tracks, list) or index >= len(tracks):
return ""
event = tracks[index]
track = event.get("track") if isinstance(event, dict) else None
return _track_label(track, "title")
def _spotify_label(tracks: object, index: int) -> str:
if not isinstance(tracks, list) or index >= len(tracks):
return ""
return _track_label(tracks[index], "name")
def _track_label(track: object, title_key: str) -> str:
if not isinstance(track, dict):
return ""
artists = track.get("artists", [])
names = [
artist.get("name", "") if isinstance(artist, dict) else str(artist) for artist in artists
]
return f"{track.get(title_key, '')} by {', '.join(names)}"
def _write_json(path: Path, payload: object) -> None:
path.write_text(
json.dumps(payload, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="ascii",
)