// Typed fetch wrappers over the Flask backend. Each function mirrors // a Pydantic schema in backend/schemas.py. Keep these in sync: if you // add a route on the backend, add a wrapper here. export type PartnerInfo = { key: string display_name: string difficulty: "easy" | "medium" | "hard" | "human-like" description: string tags: string[] } export type EnvInfo = { env_name: string display_name: string modes: string[] default_partner_key: string overview: string stats: Record accent: "blue" | "yellow" | "green" | "red" | string ready: boolean num_partners: number } export type NewGameResponse = { session_id: string state: Record score: Record } export type StepResponse = { state: Record score: Record reward: number done: boolean info: Record partner_acted: boolean events: Record[] } async function jsonFetch(url: string, init?: RequestInit): Promise { let resp: Response try { resp = await fetch(url, { ...init, headers: { "Content-Type": "application/json", ...(init?.headers ?? {}), }, }) } catch (cause) { // Pure network failure: DNS down, CORS block, Vite proxy has // no upstream to talk to, etc. fetch throws a TypeError with // "Failed to fetch" which is cryptic, so we unwrap it. throw new ApiError( "network_error", "Could not reach the backend. Start it with `bash benchmark_ui/dev.sh`.", 0, ) } if (!resp.ok) { // The backend (when reached) always returns JSON errors. // If we get non-JSON here, it's usually Vite's own 502 HTML // surfaced because Flask isn't listening on :5174. let parsed: { error?: string; detail?: string } | null = null try { parsed = await resp.json() } catch { /* non-JSON response */ } if (parsed) { throw new ApiError( parsed.error ?? `HTTP ${resp.status}`, parsed.detail ?? resp.statusText, resp.status, ) } throw new ApiError( `HTTP ${resp.status}`, resp.status === 502 ? "Vite proxy got a 502: the Flask backend on :5174 isn't responding." : resp.statusText || "non-JSON error response", resp.status, ) } return resp.json() as Promise } export class ApiError extends Error { constructor( public readonly code: string, public readonly detail: string, public readonly status: number, ) { super(detail ? `${code}: ${detail}` : code) } } export type BuiltinEgo = { key: string display_name: string description: string tags: string[] } export type UploadedPartnerInfo = { checkpoint_id: string agent_name: string actor_type: string | null aggregate_score: number | null created_at: number | null checkpoint_sha256: string | null } export type HeldoutPartnerDescriptor = { key: string display_name: string difficulty: string description: string tags: string[] normalize_bounds: [number, number] | null } export type PerPartnerScore = { key: string display_name: string mean: number std: number ci_low: number ci_high: number normalized_mean: number normalized_ci_low: number normalized_ci_high: number n_episodes: number mean_steps: number } export type AggregateScore = { method: string mean: number ci_low: number ci_high: number num_partners: number } export type LeaderboardEntry = { id: string env: string version: string agent_name: string aggregate_score: number aggregate: AggregateScore per_partner: PerPartnerScore[] ego_kind: "builtin" | "upload" builtin_key: string | null checkpoint_sha256: string | null num_episodes: number eval_seed: number notes: string | null created_at: number wall_clock_seconds: number } export type DemoFrame = { state: Record score: Record step: number actor: number | null action: number | null reward: number event: Record | null } export type DemoResponse = { frames: DemoFrame[] total_reward: number final_score: Record done: boolean num_frames: number agent_a_id: string agent_b_id: string } export type StudyStateResponse = { study_id: string current_game_index: number total_games: number num_warmup: number is_warmup: boolean session_id: string state: Record score: Record partner_key: string } export type StudyStepResponse = StudyStateResponse & { session_complete: boolean completion_code: string | null game_just_advanced: boolean prev_game_index: number | null } export type JobStatus = { id: string kind: string env: string status: "pending" | "running" | "done" | "error" progress: { completed: number; total: number; current: string | null } error: string | null created_at: number updated_at: number started_at: number | null finished_at: number | null } export const api = { envs: () => jsonFetch<{ envs: EnvInfo[] }>("/api/envs"), partners: (env: string) => jsonFetch<{ partners: PartnerInfo[] }>(`/api/${env}/partners`), heldout: (env: string, version = "v1") => jsonFetch<{ env: string; version: string; partners: HeldoutPartnerDescriptor[] }>( `/api/${env}/heldout?version=${encodeURIComponent(version)}`, ), heldoutVersions: (env: string) => jsonFetch<{ versions: string[] }>(`/api/${env}/heldout/versions`), egos: (env: string) => jsonFetch<{ egos: BuiltinEgo[] }>(`/api/${env}/egos`), uploadedPartners: (env: string) => jsonFetch<{ uploaded: UploadedPartnerInfo[] }>(`/api/${env}/uploaded_partners`), demoUpload: async (env: string, file: File, meta: { agent_name: string actor_type: "mlp" | "s5" | "rnn" arch_params: Record ckpt_key?: string idx?: number }) => { const fd = new FormData() fd.append("payload", JSON.stringify(meta)) fd.append("checkpoint", file) const r = await fetch(`/api/${env}/demo/upload`, { method: "POST", body: fd }) if (!r.ok) throw new Error(`HTTP ${r.status}: ${(await r.text()).slice(0, 300)}`) return (await r.json()) as { checkpoint_id: string; entry_id: string; ok: boolean } }, newGame: (env: string, partnerKey: string, seed?: number, envKwargs?: Record) => jsonFetch(`/api/${env}/play/new`, { method: "POST", body: JSON.stringify({ partner_key: partnerKey, seed: seed ?? null, env_kwargs: envKwargs ?? {}, }), }), step: (env: string, sessionId: string, action: Record) => jsonFetch(`/api/${env}/play/step`, { method: "POST", body: JSON.stringify({ session_id: sessionId, action }), }), saveTrajectory: (env: string, sessionId: string, agentName: string) => jsonFetch<{ trajectory_id: string }>(`/api/${env}/play/save`, { method: "POST", body: JSON.stringify({ session_id: sessionId, agent_name: agentName }), }), demo: (env: string, payload: { agent_a_id: string agent_b_id: string seed?: number max_steps?: number }) => jsonFetch(`/api/${env}/play/demo`, { method: "POST", body: JSON.stringify(payload), }), studyStart: (env: string, prolific: { prolific_pid?: string study_id?: string prolific_session_id?: string data_source?: string }) => jsonFetch(`/api/${env}/study/start`, { method: "POST", body: JSON.stringify(prolific), }), studyStep: (env: string, studyId: string, action: Record) => jsonFetch(`/api/${env}/study/step`, { method: "POST", body: JSON.stringify({ study_id: studyId, action }), }), studySave: (env: string, studyId: string, agentName: string) => jsonFetch<{ trajectory_ids: string[]; count: number }>(`/api/${env}/study/save`, { method: "POST", body: JSON.stringify({ study_id: studyId, agent_name: agentName }), }), submit: (env: string, payload: { agent_name?: string version?: string num_episodes?: number eval_seed?: number ego_kind?: "builtin" | "upload" builtin_key?: string | null notes?: string | null }) => jsonFetch<{ job_id: string }>(`/api/${env}/submit`, { method: "POST", body: JSON.stringify(payload), }), jobStatus: (env: string, jobId: string) => jsonFetch(`/api/${env}/submit/status/${jobId}`), jobResult: (env: string, jobId: string) => jsonFetch<{ entry_id: string per_partner: PerPartnerScore[] aggregate: AggregateScore num_episodes: number wall_clock_seconds: number }>(`/api/${env}/submit/result/${jobId}`), leaderboard: (env: string, version = "v1") => jsonFetch<{ env: string; version: string; entries: LeaderboardEntry[] }>( `/api/${env}/leaderboard?version=${encodeURIComponent(version)}`, ), healthz: () => jsonFetch<{ ok: boolean; envs: string[] }>("/api/healthz"), }