Spaces:
Running
Running
File size: 3,639 Bytes
b694417 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | 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 };
|