shwetangisingh's picture
Stop blocking /chat on evals; let the UI poll for them
c09a7e7
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;
}
}