545 lines
14 KiB
TypeScript
545 lines
14 KiB
TypeScript
/**
|
|
* PromptLooper typed API client.
|
|
*
|
|
* - JWT token stored in memory (never localStorage) for security.
|
|
* - Automatic Authorization header injection.
|
|
* - Typed wrapper functions for every API endpoint group.
|
|
* - WebSocket connection helper for real-time updates.
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types — mirrors backend Pydantic schemas
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface ProjectCreate {
|
|
name: string;
|
|
description?: string | null;
|
|
}
|
|
|
|
export interface ProjectUpdate {
|
|
name?: string | null;
|
|
description?: string | null;
|
|
}
|
|
|
|
export interface ProjectResponse {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
owner_id: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface ProjectListResponse {
|
|
items: ProjectResponse[];
|
|
total: number;
|
|
}
|
|
|
|
export interface ExperimentCreate {
|
|
name: string;
|
|
description?: string | null;
|
|
sample_data?: Record<string, unknown> | null;
|
|
pipeline_stages?: Record<string, unknown> | null;
|
|
scoring_config?: Record<string, unknown> | null;
|
|
parameter_space?: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface ExperimentUpdate {
|
|
name?: string | null;
|
|
description?: string | null;
|
|
sample_data?: Record<string, unknown> | null;
|
|
pipeline_stages?: Record<string, unknown> | null;
|
|
scoring_config?: Record<string, unknown> | null;
|
|
parameter_space?: Record<string, unknown> | null;
|
|
status?: string | null;
|
|
}
|
|
|
|
export interface ExperimentResponse {
|
|
id: string;
|
|
project_id: string;
|
|
name: string;
|
|
description: string | null;
|
|
sample_data: Record<string, unknown> | null;
|
|
pipeline_stages: Record<string, unknown> | null;
|
|
scoring_config: Record<string, unknown> | null;
|
|
parameter_space: Record<string, unknown> | null;
|
|
status: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface ExperimentListResponse {
|
|
items: ExperimentResponse[];
|
|
total: number;
|
|
}
|
|
|
|
export interface RunResponse {
|
|
id: string;
|
|
experiment_id: string;
|
|
config_hash: string;
|
|
config: Record<string, unknown>;
|
|
status: string;
|
|
started_at: string | null;
|
|
completed_at: string | null;
|
|
duration_ms: number | null;
|
|
tokens_in: number | null;
|
|
tokens_out: number | null;
|
|
cost_estimate: number | null;
|
|
}
|
|
|
|
export interface RunListResponse {
|
|
items: RunResponse[];
|
|
total: number;
|
|
}
|
|
|
|
export interface StageResultResponse {
|
|
id: string;
|
|
run_id: string;
|
|
stage_index: number;
|
|
prompt_sent: string;
|
|
response_raw: string;
|
|
model_used: string;
|
|
parameters: Record<string, unknown> | null;
|
|
tokens_in: number | null;
|
|
tokens_out: number | null;
|
|
latency_ms: number | null;
|
|
}
|
|
|
|
export interface ScoreResponse {
|
|
id: string;
|
|
run_id: string;
|
|
scorer_name: string;
|
|
value: number;
|
|
scorer_metadata: Record<string, unknown> | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface RunDetailResponse extends RunResponse {
|
|
stage_results: StageResultResponse[];
|
|
scores: ScoreResponse[];
|
|
}
|
|
|
|
export interface ScoreInput {
|
|
scorer_name: string;
|
|
value: number;
|
|
metadata?: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface EndpointCreate {
|
|
name: string;
|
|
url: string;
|
|
api_key?: string | null;
|
|
default_model?: string | null;
|
|
}
|
|
|
|
export interface EndpointUpdate {
|
|
name?: string | null;
|
|
url?: string | null;
|
|
api_key?: string | null;
|
|
default_model?: string | null;
|
|
}
|
|
|
|
export interface EndpointResponse {
|
|
id: string;
|
|
name: string;
|
|
url: string;
|
|
default_model: string | null;
|
|
}
|
|
|
|
export interface EndpointListResponse {
|
|
items: EndpointResponse[];
|
|
total: number;
|
|
}
|
|
|
|
export interface WebhookCreate {
|
|
event_type: string;
|
|
url: string;
|
|
headers?: Record<string, string> | null;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
export interface WebhookUpdate {
|
|
event_type?: string | null;
|
|
url?: string | null;
|
|
headers?: Record<string, string> | null;
|
|
is_active?: boolean | null;
|
|
}
|
|
|
|
export interface WebhookResponse {
|
|
id: string;
|
|
event_type: string;
|
|
url: string;
|
|
headers: Record<string, string> | null;
|
|
is_active: boolean;
|
|
}
|
|
|
|
export interface WebhookListResponse {
|
|
items: WebhookResponse[];
|
|
total: number;
|
|
}
|
|
|
|
export interface SetupRequest {
|
|
username: string;
|
|
password: string;
|
|
}
|
|
|
|
export interface LoginRequest {
|
|
username: string;
|
|
password: string;
|
|
}
|
|
|
|
export interface TokenResponse {
|
|
access_token: string;
|
|
token_type: string;
|
|
}
|
|
|
|
export interface UserResponse {
|
|
id: string;
|
|
username: string;
|
|
is_admin: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface HealthResponse {
|
|
status: string;
|
|
database: boolean;
|
|
redis: boolean;
|
|
}
|
|
|
|
export interface ExportRunRow {
|
|
run_id: string;
|
|
experiment_id: string;
|
|
config_hash: string;
|
|
config: Record<string, unknown>;
|
|
status: string;
|
|
duration_ms: number | null;
|
|
tokens_in: number | null;
|
|
tokens_out: number | null;
|
|
cost_estimate: number | null;
|
|
scores: Record<string, number>;
|
|
}
|
|
|
|
export interface ExportResponse {
|
|
experiment_id: string;
|
|
experiment_name: string;
|
|
rows: ExportRunRow[];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API Error
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public statusText: string,
|
|
public body: unknown,
|
|
) {
|
|
super(`API ${status}: ${statusText}`);
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Token management (in-memory only)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let _accessToken: string | null = null;
|
|
|
|
export function setToken(token: string | null): void {
|
|
_accessToken = token;
|
|
}
|
|
|
|
export function getToken(): string | null {
|
|
return _accessToken;
|
|
}
|
|
|
|
export function clearToken(): void {
|
|
_accessToken = null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Base fetch wrapper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const BASE_URL = ""; // Uses Vite proxy in dev; same origin in prod
|
|
|
|
async function request<T>(
|
|
path: string,
|
|
options: RequestInit = {},
|
|
): Promise<T> {
|
|
const headers: Record<string, string> = {
|
|
...(options.headers as Record<string, string> | undefined),
|
|
};
|
|
|
|
// Inject auth header
|
|
if (_accessToken) {
|
|
headers["Authorization"] = `Bearer ${_accessToken}`;
|
|
}
|
|
|
|
// Default content-type for requests with bodies
|
|
if (options.body && !headers["Content-Type"]) {
|
|
headers["Content-Type"] = "application/json";
|
|
}
|
|
|
|
const response = await fetch(`${BASE_URL}${path}`, {
|
|
...options,
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let body: unknown;
|
|
try {
|
|
body = await response.json();
|
|
} catch {
|
|
body = await response.text();
|
|
}
|
|
throw new ApiError(response.status, response.statusText, body);
|
|
}
|
|
|
|
// 204 No Content
|
|
if (response.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
function get<T>(path: string): Promise<T> {
|
|
return request<T>(path, { method: "GET" });
|
|
}
|
|
|
|
function post<T>(path: string, body?: unknown): Promise<T> {
|
|
return request<T>(path, {
|
|
method: "POST",
|
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
});
|
|
}
|
|
|
|
function put<T>(path: string, body?: unknown): Promise<T> {
|
|
return request<T>(path, {
|
|
method: "PUT",
|
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
});
|
|
}
|
|
|
|
function del<T>(path: string): Promise<T> {
|
|
return request<T>(path, { method: "DELETE" });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Health
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const health = {
|
|
check: () => get<HealthResponse>("/health"),
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Auth
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const auth = {
|
|
setup: (data: SetupRequest) =>
|
|
post<TokenResponse>("/api/auth/setup", data),
|
|
|
|
login: async (data: LoginRequest): Promise<TokenResponse> => {
|
|
const resp = await post<TokenResponse>("/api/auth/login", data);
|
|
setToken(resp.access_token);
|
|
return resp;
|
|
},
|
|
|
|
me: () => get<UserResponse>("/api/auth/me"),
|
|
|
|
logout: () => {
|
|
clearToken();
|
|
},
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Projects
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const projects = {
|
|
list: () => get<ProjectListResponse>("/api/projects/"),
|
|
|
|
create: (data: ProjectCreate) =>
|
|
post<ProjectResponse>("/api/projects/", data),
|
|
|
|
get: (id: string) => get<ProjectResponse>(`/api/projects/${id}`),
|
|
|
|
update: (id: string, data: ProjectUpdate) =>
|
|
put<ProjectResponse>(`/api/projects/${id}`, data),
|
|
|
|
delete: (id: string) => del<void>(`/api/projects/${id}`),
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Experiments
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const experiments = {
|
|
list: () => get<ExperimentListResponse>("/api/experiments/"),
|
|
|
|
create: (data: ExperimentCreate) =>
|
|
post<ExperimentResponse>("/api/experiments/", data),
|
|
|
|
get: (id: string) => get<ExperimentResponse>(`/api/experiments/${id}`),
|
|
|
|
update: (id: string, data: ExperimentUpdate) =>
|
|
put<ExperimentResponse>(`/api/experiments/${id}`, data),
|
|
|
|
delete: (id: string) => del<void>(`/api/experiments/${id}`),
|
|
|
|
startSweep: (id: string) =>
|
|
post<void>(`/api/experiments/${id}/sweep`),
|
|
|
|
pause: (id: string) =>
|
|
post<void>(`/api/experiments/${id}/pause`),
|
|
|
|
resume: (id: string) =>
|
|
post<void>(`/api/experiments/${id}/resume`),
|
|
|
|
stop: (id: string) =>
|
|
post<void>(`/api/experiments/${id}/stop`),
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Runs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const runs = {
|
|
list: (experimentId: string) =>
|
|
get<RunListResponse>(`/api/runs/experiments/${experimentId}/runs`),
|
|
|
|
get: (runId: string) =>
|
|
get<RunDetailResponse>(`/api/runs/${runId}`),
|
|
|
|
create: (data: Record<string, unknown>) =>
|
|
post<RunResponse>("/api/runs/", data),
|
|
|
|
score: (runId: string, data: ScoreInput) =>
|
|
post<ScoreResponse>(`/api/runs/${runId}/score`, data),
|
|
|
|
leaderboard: (experimentId: string) =>
|
|
get<RunListResponse>(
|
|
`/api/runs/experiments/${experimentId}/leaderboard`,
|
|
),
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Endpoints (LLM targets)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const endpoints = {
|
|
list: () => get<EndpointListResponse>("/api/endpoints/"),
|
|
|
|
create: (data: EndpointCreate) =>
|
|
post<EndpointResponse>("/api/endpoints/", data),
|
|
|
|
update: (id: string, data: EndpointUpdate) =>
|
|
put<EndpointResponse>(`/api/endpoints/${id}`, data),
|
|
|
|
delete: (id: string) => del<void>(`/api/endpoints/${id}`),
|
|
|
|
test: (id: string) =>
|
|
post<Record<string, unknown>>(`/api/endpoints/${id}/test`),
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Export
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const exportApi = {
|
|
best: (experimentId: string) =>
|
|
get<Record<string, unknown>>(
|
|
`/api/export/experiments/${experimentId}/best`,
|
|
),
|
|
|
|
env: (experimentId: string) =>
|
|
get<string>(`/api/export/experiments/${experimentId}/env`),
|
|
|
|
yaml: (experimentId: string) =>
|
|
get<string>(`/api/export/experiments/${experimentId}/yaml`),
|
|
|
|
report: (experimentId: string) =>
|
|
get<string>(`/api/export/experiments/${experimentId}/report`),
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Webhooks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const webhooks = {
|
|
list: () => get<WebhookListResponse>("/api/webhooks/"),
|
|
|
|
create: (data: WebhookCreate) =>
|
|
post<WebhookResponse>("/api/webhooks/", data),
|
|
|
|
delete: (id: string) => del<void>(`/api/webhooks/${id}`),
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Admin
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const admin = {
|
|
getSettings: () =>
|
|
get<Record<string, unknown>>("/api/admin/settings"),
|
|
|
|
updateSettings: (data: Record<string, unknown>) =>
|
|
put<Record<string, unknown>>("/api/admin/settings", data),
|
|
|
|
getStats: () => get<Record<string, unknown>>("/api/admin/stats"),
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// WebSocket helper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type WsMessageHandler = (data: unknown) => void;
|
|
|
|
export interface WsConnection {
|
|
send: (data: unknown) => void;
|
|
close: () => void;
|
|
}
|
|
|
|
/**
|
|
* Connect to the real-time WebSocket endpoint.
|
|
*
|
|
* @param onMessage Called for each incoming message.
|
|
* @param onClose Optional callback when connection closes.
|
|
* @returns Object with `send()` and `close()` methods.
|
|
*/
|
|
export function connectWebSocket(
|
|
onMessage: WsMessageHandler,
|
|
onClose?: () => void,
|
|
): WsConnection {
|
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
|
const ws = new WebSocket(wsUrl);
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const data: unknown = JSON.parse(event.data as string);
|
|
onMessage(data);
|
|
} catch {
|
|
onMessage(event.data);
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
onClose?.();
|
|
};
|
|
|
|
return {
|
|
send: (data: unknown) => {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(data));
|
|
}
|
|
},
|
|
close: () => {
|
|
ws.close();
|
|
},
|
|
};
|
|
}
|