Spaces:
Sleeping
Sleeping
| import type { | |
| ChatRequest, | |
| ChatResponse, | |
| EvalScores, | |
| Persona, | |
| TurnaroundRequest, | |
| } from "../types"; | |
| const API_BASE = ""; | |
| export async function fetchUsers(): Promise<Persona[]> { | |
| const res = await fetch(`${API_BASE}/users`); | |
| if (!res.ok) throw new Error(`API error: ${res.status}`); | |
| const data = await res.json(); | |
| return data.users; | |
| } | |
| export async function sendChat(req: ChatRequest): Promise<ChatResponse> { | |
| const res = await fetch(`${API_BASE}/chat`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(req), | |
| }); | |
| if (!res.ok) throw new Error(`API error: ${res.status}`); | |
| return res.json(); | |
| } | |
| export async function sendTurnaround( | |
| req: TurnaroundRequest | |
| ): Promise<ChatResponse> { | |
| const res = await fetch(`${API_BASE}/chat/turnaround`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(req), | |
| }); | |
| if (!res.ok) throw new Error(`API error: ${res.status}`); | |
| return res.json(); | |
| } | |
| export async function resetSession(userId: string): Promise<void> { | |
| const res = await fetch( | |
| `${API_BASE}/session/reset?user_id=${encodeURIComponent(userId)}`, | |
| { method: "POST" } | |
| ); | |
| if (!res.ok) throw new Error(`API error: ${res.status}`); | |
| } | |
| export type StreamEvent = | |
| | { type: "candidate_start"; idx: number; strategy: string; grounded_buckets: string[] } | |
| | { type: "token"; idx: number; delta: string } | |
| | { type: "candidate_done"; idx: number; text: string } | |
| | { type: "candidate_error"; idx: number; error: string } | |
| | { type: "complete"; response: ChatResponse } | |
| | { type: "error"; message: string }; | |
| async function readSSE( | |
| res: Response, | |
| onEvent: (evt: StreamEvent) => void, | |
| ): Promise<void> { | |
| if (!res.body) throw new Error("no response body"); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ""; | |
| const emitFrame = (frame: string) => { | |
| const line = frame.split("\n").find((l) => l.startsWith("data:")); | |
| if (!line) return; | |
| const json = line.slice(5).trim(); | |
| if (!json) return; | |
| try { | |
| onEvent(JSON.parse(json) as StreamEvent); | |
| } catch (e) { | |
| console.warn("SSE parse failed", e, json.slice(0, 200)); | |
| } | |
| }; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| // SSE frames are separated by blank lines. | |
| const parts = buffer.split("\n\n"); | |
| buffer = parts.pop() ?? ""; | |
| for (const part of parts) emitFrame(part); | |
| } | |
| // Server closed cleanly but the final frame didn't end with \n\n — | |
| // emit whatever remains so the terminal event isn't dropped. | |
| if (buffer.trim()) emitFrame(buffer); | |
| } | |
| export async function streamChat( | |
| req: ChatRequest, | |
| onEvent: (evt: StreamEvent) => void, | |
| ): Promise<void> { | |
| const res = await fetch(`${API_BASE}/chat/stream`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(req), | |
| }); | |
| if (!res.ok) throw new Error(`API error: ${res.status}`); | |
| await readSSE(res, onEvent); | |
| } | |
| export async function streamRegenerate( | |
| args: { user_id: string; turn_id: number; rejected_texts: string[] }, | |
| onEvent: (evt: StreamEvent) => void, | |
| ): Promise<void> { | |
| const res = await fetch(`${API_BASE}/chat/regenerate/stream`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(args), | |
| }); | |
| if (!res.ok) throw new Error(`API error: ${res.status}`); | |
| await readSSE(res, onEvent); | |
| } | |
| export async function sendRegenerate(args: { | |
| user_id: string; | |
| turn_id: number; | |
| rejected_texts: string[]; | |
| }): Promise<ChatResponse> { | |
| const res = await fetch(`${API_BASE}/chat/regenerate`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(args), | |
| }); | |
| if (!res.ok) throw new Error(`API error: ${res.status}`); | |
| return res.json(); | |
| } | |
| export async function sendPick(args: { | |
| run_id: string; | |
| user_id: string; | |
| picked_idx: number; | |
| }): Promise<void> { | |
| const res = await fetch(`${API_BASE}/chat/pick`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(args), | |
| }); | |
| if (!res.ok) throw new Error(`API error: ${res.status}`); | |
| } | |
| export type EvalsStatus = "pending" | "ready" | "failed" | "unknown"; | |
| export interface EvalsFetchResult { | |
| status: EvalsStatus; | |
| run_id: string; | |
| eval_scores: EvalScores | null; | |
| } | |
| export async function fetchEvals(runId: string): Promise<EvalsFetchResult> { | |
| const res = await fetch(`${API_BASE}/evals/${encodeURIComponent(runId)}`); | |
| if (!res.ok) throw new Error(`API error: ${res.status}`); | |
| return res.json(); | |
| } | |
| export async function pollEvals( | |
| runId: string, | |
| opts: { | |
| initialDelayMs?: number; | |
| maxDelayMs?: number; | |
| timeoutMs?: number; | |
| signal?: AbortSignal; | |
| } = {} | |
| ): Promise<EvalScores | null> { | |
| const maxDelayMs = opts.maxDelayMs ?? 2000; | |
| const timeoutMs = opts.timeoutMs ?? 20000; | |
| let delay = opts.initialDelayMs ?? 300; | |
| const start = performance.now(); | |
| // Track consecutive "unknown" responses so transient race conditions (poll | |
| // racing the server picking up the new run_id) don't immediately give up. | |
| let unknownStreak = 0; | |
| while (performance.now() - start < timeoutMs) { | |
| if (opts.signal?.aborted) return null; | |
| try { | |
| const r = await fetchEvals(runId); | |
| if (r.status === "ready") return r.eval_scores; | |
| if (r.status === "failed") return null; | |
| if (r.status === "unknown") { | |
| unknownStreak += 1; | |
| if (unknownStreak >= 3) return null; | |
| } else { | |
| unknownStreak = 0; | |
| } | |
| } catch (e) { | |
| console.warn("pollEvals: transient error", e); | |
| } | |
| await new Promise((res) => setTimeout(res, delay)); | |
| delay = Math.min(delay * 2, maxDelayMs); | |
| } | |
| return null; | |
| } | |
| export async function submitRating(args: { | |
| run_id: string; | |
| user_id: string; | |
| authenticity: number; | |
| rater_id?: string; | |
| notes?: string; | |
| }): Promise<void> { | |
| const res = await fetch(`${API_BASE}/feedback/rating`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(args), | |
| }); | |
| if (!res.ok) throw new Error(`API error: ${res.status}`); | |
| } | |
| export async function checkHealth(): Promise<boolean> { | |
| try { | |
| const res = await fetch(`${API_BASE}/health`); | |
| if (!res.ok) return false; | |
| const data = await res.json(); | |
| return data.models_ready === true; | |
| } catch { | |
| return false; | |
| } | |
| } | |