Spaces:
Running
Running
| import type { | |
| Entity, | |
| ExtractResponse, | |
| Relation, | |
| TierInfo, | |
| TierName, | |
| } from "../types"; | |
| const REQUEST_TIMEOUT_MS = 20_000; | |
| const TIER_RETRY_DELAYS_MS = [300, 900] as const; | |
| interface ApiExtractResponse { | |
| tokens: string[]; | |
| entities: Entity[]; | |
| relations: Relation[]; | |
| } | |
| interface ApiTierResponse { | |
| tiers: TierInfo[]; | |
| } | |
| interface ApiErrorResponse { | |
| error?: { message?: string }; | |
| } | |
| interface RuntimeConfiguration { | |
| apiBaseUrl?: string; | |
| } | |
| class DeploymentConfigurationError extends Error {} | |
| function getApiBaseUrl(): string { | |
| const runtimeConfiguration = ( | |
| globalThis as typeof globalThis & { | |
| __TEXTMOSAIC_CONFIG__?: RuntimeConfiguration; | |
| } | |
| ).__TEXTMOSAIC_CONFIG__; | |
| const configuredUrl = | |
| runtimeConfiguration?.apiBaseUrl ?? import.meta.env.VITE_API_BASE_URL; | |
| if (configuredUrl) { | |
| return configuredUrl.replace(/\/$/, ""); | |
| } | |
| const hostname = globalThis.location?.hostname; | |
| if ( | |
| hostname === undefined || | |
| hostname === "localhost" || | |
| hostname === "127.0.0.1" || | |
| hostname === "::1" | |
| ) { | |
| return "http://127.0.0.1:7860"; | |
| } | |
| throw new DeploymentConfigurationError( | |
| "This deployment is missing VITE_API_BASE_URL. Set the frontend container environment variable to the public API URL.", | |
| ); | |
| } | |
| function toClientResponse(response: ApiExtractResponse): ExtractResponse { | |
| // This is the deliberate API boundary. All current fields are single words, | |
| // so the required snake_case-to-camelCase conversion is a no-op today. | |
| return { | |
| tokens: response.tokens, | |
| entities: response.entities.map((entity) => ({ ...entity })), | |
| relations: response.relations.map((relation) => ({ ...relation })), | |
| }; | |
| } | |
| async function request<T>(path: string, init?: RequestInit): Promise<T> { | |
| const apiBaseUrl = getApiBaseUrl(); | |
| const controller = new AbortController(); | |
| const timeout = globalThis.setTimeout( | |
| () => controller.abort(), | |
| REQUEST_TIMEOUT_MS, | |
| ); | |
| let response: Response; | |
| try { | |
| response = await fetch(`${apiBaseUrl}${path}`, { | |
| headers: { "Content-Type": "application/json", ...init?.headers }, | |
| signal: controller.signal, | |
| ...init, | |
| }); | |
| } catch (error) { | |
| if (error instanceof DOMException && error.name === "AbortError") { | |
| throw new Error("The extraction service did not respond in time."); | |
| } | |
| throw new Error("Unable to reach the extraction service."); | |
| } finally { | |
| globalThis.clearTimeout(timeout); | |
| } | |
| if (!response.ok) { | |
| const body = (await response.json().catch(() => ({}))) as ApiErrorResponse; | |
| throw new Error( | |
| body.error?.message ?? `Request failed with status ${response.status}.`, | |
| ); | |
| } | |
| return (await response.json()) as T; | |
| } | |
| export async function getTiers(): Promise<TierInfo[]> { | |
| let lastError: Error | undefined; | |
| for (const delay of [...TIER_RETRY_DELAYS_MS, 0]) { | |
| if (delay > 0) { | |
| await new Promise((resolve) => globalThis.setTimeout(resolve, delay)); | |
| } | |
| try { | |
| return (await request<ApiTierResponse>("/tiers")).tiers; | |
| } catch (error) { | |
| if (error instanceof DeploymentConfigurationError) { | |
| throw error; | |
| } | |
| lastError = | |
| error instanceof Error | |
| ? error | |
| : new Error("Unable to load model tiers."); | |
| } | |
| } | |
| throw lastError ?? new Error("Unable to load model tiers."); | |
| } | |
| export async function extractText( | |
| text: string, | |
| tier: TierName, | |
| ): Promise<ExtractResponse> { | |
| const response = await request<ApiExtractResponse>("/extract", { | |
| method: "POST", | |
| body: JSON.stringify({ text, tier }), | |
| }); | |
| return toClientResponse(response); | |
| } | |
| export { toClientResponse }; | |