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 };