Download src/lib/api.ts from meljendy/proteinea: direct link, hf CLI and curl.
- Browser
- Download file 16.5 kB
-
https://huggingface.co/spaces/meljendy/proteinea/resolve/main/src/lib/api.ts
- Command line
-
hf download hf://spaces/meljendy/proteinea/src/lib/api.ts
-
curl -L -o api.ts https://huggingface.co/spaces/meljendy/proteinea/resolve/main/src/lib/api.ts
16.5 kB
| // Same-origin client for the Next.js API routes (projects, tasks, messages) + legacy benchmark/model/campaign shims. | |
| import type { | |
| Artifact, | |
| Benchmark, | |
| ChatMessage, | |
| DatasetSearchResponse, | |
| DatasetSearchResult, | |
| ModelEntry, | |
| ProjectRecord, | |
| ResourceKind, | |
| ResourceRef, | |
| Skill, | |
| SyncStatusEntry, | |
| TaskRecord, | |
| } from "./types"; | |
| import { BENCHMARKS, MODELS } from "./data"; | |
| // Legacy backend base — some deployments host the agent backend on a separate | |
| // origin. New task/project/artifact routes always live same-origin under the | |
| // Next.js app; only the legacy catalog endpoints (benchmarks, models, campaigns) | |
| // respect this base. | |
| const LEGACY_BASE = process.env.NEXT_PUBLIC_API_URL || ""; | |
| /** True when running without Tailscale (local dev). In production, Tailscale | |
| * Serve injects identity headers at the reverse-proxy layer so the JS side | |
| * doesn't need to add anything. In dev mode we pass X-Dev-User so the backend | |
| * can still identify the caller. */ | |
| const IS_DEV = | |
| typeof window !== "undefined" && | |
| (window.location.hostname === "localhost" || | |
| window.location.hostname === "127.0.0.1"); | |
| /** Fetch with a 4-second timeout so the UI doesn't hang. */ | |
| async function fetchJSON<T>(path: string, init?: RequestInit): Promise<T> { | |
| return fetchJSONAt(path, init); | |
| } | |
| async function fetchJSONAt<T>(path: string, init?: RequestInit, base = ""): Promise<T> { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 4000); | |
| const devHeaders: Record<string, string> = IS_DEV | |
| ? { "X-Dev-User": "dev@proteinea.local" } | |
| : {}; | |
| try { | |
| const res = await fetch(`${base}${path}`, { | |
| ...init, | |
| signal: controller.signal, | |
| headers: { "Content-Type": "application/json", ...devHeaders, ...init?.headers }, | |
| }); | |
| if (!res.ok) throw new Error(`API ${res.status}`); | |
| return (await res.json()) as T; | |
| } finally { | |
| clearTimeout(timeout); | |
| } | |
| } | |
| // ---- Projects ---- | |
| export async function listProjects(): Promise<{ projects: ProjectRecord[] }> { | |
| return fetchJSON<{ projects: ProjectRecord[] }>("/api/projects"); | |
| } | |
| export async function ensureDefaultProject(): Promise<ProjectRecord> { | |
| const { projects } = await listProjects(); | |
| if (projects.length === 0) { | |
| throw new Error("No projects returned from /api/projects"); | |
| } | |
| return projects[0]; | |
| } | |
| // ---- Tasks ---- | |
| export async function listTasks(projectId: string): Promise<{ tasks: TaskRecord[] }> { | |
| return fetchJSON<{ tasks: TaskRecord[] }>( | |
| `/api/projects/${encodeURIComponent(projectId)}/tasks`, | |
| ); | |
| } | |
| export async function createTask( | |
| projectId: string, | |
| firstMessage: string, | |
| ): Promise<TaskRecord> { | |
| const { task } = await fetchJSON<{ task: TaskRecord }>( | |
| `/api/projects/${encodeURIComponent(projectId)}/tasks`, | |
| { | |
| method: "POST", | |
| body: JSON.stringify({ firstMessage }), | |
| }, | |
| ); | |
| return task; | |
| } | |
| export async function getTask(taskId: string): Promise<TaskRecord | null> { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 4000); | |
| try { | |
| const res = await fetch(`/api/tasks/${encodeURIComponent(taskId)}`, { | |
| signal: controller.signal, | |
| headers: { "Content-Type": "application/json" }, | |
| }); | |
| if (res.status === 404) return null; | |
| if (!res.ok) throw new Error(`API ${res.status}`); | |
| const data = (await res.json()) as { task: TaskRecord }; | |
| return data.task; | |
| } finally { | |
| clearTimeout(timeout); | |
| } | |
| } | |
| export async function appendMessage( | |
| taskId: string, | |
| message: ChatMessage, | |
| ): Promise<TaskRecord> { | |
| const { task } = await fetchJSON<{ task: TaskRecord }>( | |
| `/api/tasks/${encodeURIComponent(taskId)}/messages`, | |
| { | |
| method: "POST", | |
| body: JSON.stringify({ message }), | |
| }, | |
| ); | |
| return task; | |
| } | |
| export async function patchTask( | |
| taskId: string, | |
| patch: Partial<TaskRecord>, | |
| ): Promise<TaskRecord> { | |
| const body: Record<string, unknown> = {}; | |
| if (patch.title !== undefined) body.title = patch.title; | |
| if (patch.messages !== undefined) body.messages = patch.messages; | |
| if (patch.artifacts !== undefined) body.artifacts = patch.artifacts; | |
| const { task } = await fetchJSON<{ task: TaskRecord }>( | |
| `/api/tasks/${encodeURIComponent(taskId)}`, | |
| { | |
| method: "PATCH", | |
| body: JSON.stringify(body), | |
| }, | |
| ); | |
| return task; | |
| } | |
| // ---- Skills ---- | |
| export async function listSkillsAPI(): Promise<{ skills: Skill[] }> { | |
| return fetchJSON<{ skills: Skill[] }>("/api/skills"); | |
| } | |
| // ---- Jobs ---- | |
| export interface JobSubmitResult { | |
| campaign_job_id: string; | |
| hub_task_execution_id: string; | |
| provider: string; | |
| hub_response: Record<string, unknown>; | |
| } | |
| export interface BackendArtifact { | |
| id: string; | |
| hub_output_key: string; | |
| phylo_tags?: string[]; | |
| scientist_note?: string | null; | |
| } | |
| export interface JobStatusResult { | |
| campaign_job_id: string; | |
| campaign_id: string; | |
| role: string; | |
| submitted_by: string; | |
| hub_task_execution_id: string; | |
| hub: Record<string, unknown>; | |
| artifacts: BackendArtifact[]; | |
| } | |
| export interface CostEstimate { | |
| estimated_usd: number; | |
| gpu_type: string; | |
| minutes_used: number; | |
| cost_alert: boolean; | |
| } | |
| /** Submit a tool run job. FormData must include campaign_id, endpoint_name, | |
| * call_params (JSON string), and input_file. */ | |
| export async function submitJobAPI( | |
| campaignId: string, | |
| formData: FormData, | |
| ): Promise<JobSubmitResult> { | |
| formData.set("campaign_id", campaignId); | |
| const controller = new AbortController(); | |
| // Allow 60s for job submission (file upload may be large). | |
| const timeout = setTimeout(() => controller.abort(), 60_000); | |
| const devHeaders: Record<string, string> = IS_DEV | |
| ? { "X-Dev-User": "dev@proteinea.local" } | |
| : {}; | |
| try { | |
| const res = await fetch("/api/jobs/submit", { | |
| method: "POST", | |
| body: formData, | |
| signal: controller.signal, | |
| headers: devHeaders, | |
| // Do NOT set Content-Type — the browser sets the multipart boundary. | |
| }); | |
| if (!res.ok) throw new Error(`API ${res.status}`); | |
| return (await res.json()) as JobSubmitResult; | |
| } finally { | |
| clearTimeout(timeout); | |
| } | |
| } | |
| /** Poll job status. */ | |
| export async function getJobStatusAPI( | |
| jobId: string, | |
| ): Promise<JobStatusResult> { | |
| return fetchJSON<JobStatusResult>(`/api/jobs/${encodeURIComponent(jobId)}`); | |
| } | |
| /** Pre-submit cost estimate for a tool. */ | |
| export async function estimateToolCostAPI( | |
| toolName: string, | |
| gpuType?: string, | |
| ): Promise<CostEstimate> { | |
| return fetchJSON<CostEstimate>( | |
| `/api/tools/${encodeURIComponent(toolName)}/estimate`, | |
| { | |
| method: "POST", | |
| body: JSON.stringify(gpuType ? { gpu_type: gpuType } : {}), | |
| }, | |
| ); | |
| } | |
| // ---- Resources ---- | |
| export async function listResourcesAPI(params?: { | |
| type?: ResourceKind; | |
| q?: string; | |
| }): Promise<{ resources: ResourceRef[]; counts: Record<string, number> }> { | |
| const qs = new URLSearchParams(); | |
| if (params?.type) qs.set("type", params.type); | |
| if (params?.q) qs.set("q", params.q); | |
| const suffix = qs.toString() ? `?${qs.toString()}` : ""; | |
| return fetchJSON<{ resources: ResourceRef[]; counts: Record<string, number> }>( | |
| `/api/resources${suffix}`, | |
| ); | |
| } | |
| // ---- Datasets ---- | |
| export async function searchDatasetsAPI( | |
| query: Record<string, unknown>, | |
| ): Promise<DatasetSearchResponse> { | |
| return fetchJSON<DatasetSearchResponse>("/api/datasets/search", { | |
| method: "POST", | |
| body: JSON.stringify(query), | |
| }); | |
| } | |
| export async function searchDatasetSourceAPI( | |
| source: string, | |
| query: Record<string, unknown>, | |
| ): Promise<DatasetSearchResult[]> { | |
| return fetchJSON<DatasetSearchResult[]>( | |
| `/api/datasets/${encodeURIComponent(source)}/search`, | |
| { | |
| method: "POST", | |
| body: JSON.stringify(query), | |
| }, | |
| ); | |
| } | |
| export async function importDatasetEntriesAPI( | |
| entries: { source: string; entry_id: string }[], | |
| ): Promise<{ artifacts: Artifact[]; errors: Record<string, string> }> { | |
| return fetchJSON<{ artifacts: Artifact[]; errors: Record<string, string> }>( | |
| "/api/datasets/import", | |
| { | |
| method: "POST", | |
| body: JSON.stringify({ entries }), | |
| }, | |
| ); | |
| } | |
| export async function getDatasetSyncStatusAPI(): Promise<SyncStatusEntry[]> { | |
| return fetchJSON<SyncStatusEntry[]>("/api/datasets/sync/status"); | |
| } | |
| // ---- Context Enrichment ---- | |
| export interface EnrichmentResult { | |
| entities: Record<string, string[]>; | |
| context: Record<string, unknown>; | |
| enriched_prompt: string; | |
| } | |
| export async function enrichContextAPI( | |
| message: string, | |
| maxResultsPerSource = 5, | |
| ): Promise<EnrichmentResult> { | |
| return fetchJSON<EnrichmentResult>("/api/context/enrich", { | |
| method: "POST", | |
| body: JSON.stringify({ message, max_results_per_source: maxResultsPerSource }), | |
| }); | |
| } | |
| // ---- Legacy catalog endpoints (no server routes yet; fall back to embedded JSON) ---- | |
| export async function listBenchmarks(): Promise<{ benchmarks: Benchmark[]; count: number }> { | |
| try { | |
| return await fetchJSONAt<{ benchmarks: Benchmark[]; count: number }>("/api/benchmarks", undefined, LEGACY_BASE); | |
| } catch { | |
| return { benchmarks: BENCHMARKS, count: BENCHMARKS.length }; | |
| } | |
| } | |
| export async function listModels(params?: { | |
| capability?: string; | |
| }): Promise<{ models: ModelEntry[]; count: number }> { | |
| try { | |
| const qs = params?.capability ? `?capability=${encodeURIComponent(params.capability)}` : ""; | |
| return await fetchJSONAt<{ models: ModelEntry[]; count: number }>(`/api/models${qs}`, undefined, LEGACY_BASE); | |
| } catch { | |
| let models = MODELS; | |
| if (params?.capability) { | |
| const cap = params.capability; | |
| models = models.filter((m) => m.capabilities.includes(cap)); | |
| } | |
| return { models, count: models.length }; | |
| } | |
| } | |
| interface CampaignEntry { | |
| campaign_id: string; | |
| conversation_id: string; | |
| active_jobs: Record<string, string>; | |
| } | |
| export async function listCampaigns(): Promise<{ campaigns: CampaignEntry[]; count: number }> { | |
| try { | |
| return await fetchJSONAt<{ campaigns: CampaignEntry[]; count: number }>("/api/campaigns", undefined, LEGACY_BASE); | |
| } catch { | |
| return { campaigns: [], count: 0 }; | |
| } | |
| } | |
| // ---- Phylo Backend (Innovation Hub) ---- | |
| const PHYLO_BACKEND = process.env.NEXT_PUBLIC_PHYLO_BACKEND_URL || "http://127.0.0.1:8601"; | |
| /** GET /api/campaigns — list all design campaigns from Phylo backend */ | |
| export async function listPhyloCampaigns(): Promise<PhyloCampaign[]> { | |
| return fetchJSONAt<PhyloCampaign[]>("/api/campaigns", undefined, PHYLO_BACKEND); | |
| } | |
| export interface PhyloCampaign { | |
| id: string; | |
| name: string; | |
| target: string; | |
| modality: string; | |
| goal: string; | |
| constraints?: Record<string, unknown>; | |
| created_by: string; | |
| created_at?: string; | |
| } | |
| /** POST /api/campaigns — create a new design campaign */ | |
| export async function createPhyloCampaign(params: { | |
| name: string; | |
| target: string; | |
| modality: string; | |
| goal: string; | |
| constraints?: Record<string, unknown>; | |
| }): Promise<{ id: string; name: string; target: string; [k: string]: unknown }> { | |
| return fetchJSONAt<{ id: string; name: string; target: string }>( | |
| "/api/campaigns", | |
| { method: "POST", body: JSON.stringify(params) }, | |
| PHYLO_BACKEND, | |
| ); | |
| } | |
| /** POST /api/campaigns/{id}/jobs — submit a job (multipart form) */ | |
| export async function submitPhyloJob( | |
| campaignId: string, | |
| formData: FormData, | |
| ): Promise<{ | |
| campaign_job_id: string; | |
| hub_task_execution_id: string; | |
| provider: string; | |
| hub_response: unknown; | |
| }> { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 120_000); // jobs can take time | |
| try { | |
| const res = await fetch( | |
| `${PHYLO_BACKEND}/api/campaigns/${encodeURIComponent(campaignId)}/jobs`, | |
| { method: "POST", body: formData, signal: controller.signal }, | |
| ); | |
| if (!res.ok) throw new Error(`Phylo API ${res.status}: ${await res.text()}`); | |
| return (await res.json()) as { | |
| campaign_job_id: string; | |
| hub_task_execution_id: string; | |
| provider: string; | |
| hub_response: unknown; | |
| }; | |
| } finally { | |
| clearTimeout(timeout); | |
| } | |
| } | |
| /** GET /api/jobs/{id} — poll job status */ | |
| export async function getPhyloJobStatus(jobId: string): Promise<{ | |
| hub: { status: string; output_data?: unknown; cost_estimate?: number; [k: string]: unknown }; | |
| artifacts: { name: string; url: string; mime?: string; bytes?: number }[]; | |
| }> { | |
| return fetchJSONAt( | |
| `/api/jobs/${encodeURIComponent(jobId)}`, | |
| undefined, | |
| PHYLO_BACKEND, | |
| ); | |
| } | |
| /** GET /api/tools — list all Hub tools */ | |
| export async function listPhyloTools(): Promise< | |
| { endpoint_name: string; provider: string; [k: string]: unknown }[] | |
| > { | |
| return fetchJSONAt< | |
| { endpoint_name: string; provider: string; [k: string]: unknown }[] | |
| >("/api/tools", undefined, PHYLO_BACKEND); | |
| } | |
| /** POST /api/tools/{name}/estimate — cost estimate for a tool run */ | |
| export async function estimateToolCost( | |
| endpointName: string, | |
| gpuType = "A100", | |
| ): Promise<{ | |
| estimated_usd: number; | |
| gpu_type: string; | |
| minutes_used: number; | |
| cost_alert?: string; | |
| }> { | |
| return fetchJSONAt( | |
| `/api/tools/${encodeURIComponent(endpointName)}/estimate`, | |
| { method: "POST", body: JSON.stringify({ gpu_type: gpuType }) }, | |
| PHYLO_BACKEND, | |
| ); | |
| } | |
| // ---- Legacy conversation shims (route into the new task system) ---- | |
| /** Create a conversation. Tries the Phylo backend first (creates a task | |
| * in the local Next.js system, then uses the task ID for the stream). | |
| * If NEXT_PUBLIC_API_URL is set, falls back to legacy backend. */ | |
| export async function createConversation(): Promise<{ conversation_id: string }> { | |
| // Always create a local task for persistence | |
| try { | |
| const project = await ensureDefaultProject(); | |
| const task = await createTask(project.id, "(new conversation)"); | |
| return { conversation_id: task.id }; | |
| } catch { | |
| // Task system unavailable — generate a temp ID (demo mode will activate) | |
| return { conversation_id: `temp-${Date.now()}` }; | |
| } | |
| } | |
| /** Stream URL — points to the Phylo backend's agentic SSE endpoint. | |
| * This is where Claude processes the message and calls Hub tools. */ | |
| export function getStreamUrl(conversationId: string): string { | |
| return `${PHYLO_BACKEND}/api/tasks/${encodeURIComponent(conversationId)}/messages/stream`; | |
| } | |
| // ---- File Upload ---- | |
| export async function uploadFiles( | |
| taskId: string, | |
| files: File[], | |
| note?: string, | |
| ): Promise<{ artifacts: Artifact[]; message: ChatMessage }> { | |
| const form = new FormData(); | |
| for (const file of files) { | |
| form.append("files", file); | |
| } | |
| if (note) form.append("note", note); | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 60_000); | |
| const devHeaders: Record<string, string> = IS_DEV | |
| ? { "X-Dev-User": "dev@proteinea.local" } | |
| : {}; | |
| try { | |
| const res = await fetch( | |
| `/api/tasks/${encodeURIComponent(taskId)}/upload`, | |
| { | |
| method: "POST", | |
| body: form, | |
| signal: controller.signal, | |
| headers: devHeaders, | |
| }, | |
| ); | |
| if (!res.ok) throw new Error(`API ${res.status}`); | |
| return (await res.json()) as { artifacts: Artifact[]; message: ChatMessage }; | |
| } finally { | |
| clearTimeout(timeout); | |
| } | |
| } | |
| // ---- Knowledge Bases ---- | |
| export interface KnowledgeBaseEntry { | |
| id: string; | |
| name: string; | |
| description: string; | |
| entryCount: number; | |
| source: "embedded" | "dataset" | "custom"; | |
| } | |
| export async function listKnowledgeBases(): Promise<{ knowledgeBases: KnowledgeBaseEntry[] }> { | |
| return fetchJSON<{ knowledgeBases: KnowledgeBaseEntry[] }>("/api/knowledge-bases"); | |
| } | |
| // ---- Task Skills ---- | |
| export async function listTaskSkills(taskId: string): Promise<{ skills: unknown[]; taskId: string }> { | |
| return fetchJSON<{ skills: unknown[]; taskId: string }>( | |
| `/api/tasks/${encodeURIComponent(taskId)}/skills`, | |
| ); | |
| } | |
| export interface DatasetListEntry { | |
| id: string; | |
| name: string; | |
| source: string; | |
| type: "structure" | "sequence" | "gene" | "benchmark" | "mixed"; | |
| description: string; | |
| entryCount: number; | |
| lastUpdated: string | null; | |
| sizeBytes: number; | |
| } | |
| export async function listDatasetsAPI(params?: { | |
| q?: string; | |
| type?: string; | |
| }): Promise<{ datasets: DatasetListEntry[]; count: number }> { | |
| const qs = new URLSearchParams(); | |
| if (params?.q) qs.set("q", params.q); | |
| if (params?.type) qs.set("type", params.type); | |
| const suffix = qs.toString() ? `?${qs.toString()}` : ""; | |
| return fetchJSON<{ datasets: DatasetListEntry[]; count: number }>( | |
| `/api/datasets/list${suffix}`, | |
| ); | |
| } | |