import { ApiError, client, getBaseUrl, getUserId } from './client'; import type { CreateKnowledgeBaseRequest, CreateKnowledgeBaseResponse, KbMiddlewareParametersSchemaResponse, KnowledgeBaseView, ListKbEmbeddingModelsResponse, ListKnowledgeBasesResponse, ListKnowledgeDocumentsResponse, ListKnowledgeDocumentStatusResponse, ListSupportedContentTypesResponse, SearchKnowledgeBaseRequest, SearchKnowledgeBaseResponse, UpdateKnowledgeBaseRequest, UploadKnowledgeDocumentResponse, } from './types'; /** * Callback invoked while bytes are pushed across the wire. * * - `loaded` — bytes already sent. * - `total` — total bytes (may be 0 when the browser cannot compute it, * e.g. for chunked encodings). */ export interface UploadProgress { loaded: number; total: number; } export interface UploadDocumentOptions { /** Fired with byte-level progress while the body is streamed. */ onProgress?: (progress: UploadProgress) => void; /** * Caller-supplied abort signal. Aborting before the server has * responded rejects the returned promise with a `DOMException` of * `name === "AbortError"`; aborting after a response has come back * is a no-op. */ signal?: AbortSignal; } /** * XHR-based upload — `fetch` does not surface byte-level send * progress in any current browser, so multipart uploads that drive a * progress UI have to fall back to XMLHttpRequest. */ function uploadDocumentXhr( knowledgeBaseId: string, file: File, options: UploadDocumentOptions = {}, ): Promise { const { onProgress, signal } = options; const formData = new FormData(); formData.append('file', file); return new Promise((resolve, reject) => { if (signal?.aborted) { reject(new DOMException('Aborted', 'AbortError')); return; } const xhr = new XMLHttpRequest(); const url = new URL(`/knowledge_bases/${knowledgeBaseId}/documents`, getBaseUrl()); xhr.open('POST', url.toString(), true); xhr.setRequestHeader('X-User-ID', getUserId()); const onAbort = () => xhr.abort(); signal?.addEventListener('abort', onAbort, { once: true }); const cleanup = () => signal?.removeEventListener('abort', onAbort); if (xhr.upload && onProgress) { xhr.upload.onprogress = (e) => { onProgress({ loaded: e.loaded, total: e.lengthComputable ? e.total : 0, }); }; } xhr.onload = () => { cleanup(); if (xhr.status >= 200 && xhr.status < 300) { try { resolve(JSON.parse(xhr.responseText) as UploadKnowledgeDocumentResponse); } catch (e) { reject(e); } return; } let detail = xhr.responseText || xhr.statusText; try { const json = JSON.parse(xhr.responseText) as { detail?: unknown; }; if (typeof json.detail === 'string') detail = json.detail; else if (json.detail !== undefined) detail = JSON.stringify(json.detail); } catch { // keep raw text } reject(new ApiError(xhr.status, detail)); }; xhr.onerror = () => { cleanup(); reject(new ApiError(0, 'Network error')); }; xhr.onabort = () => { cleanup(); reject(new DOMException('Aborted', 'AbortError')); }; xhr.send(formData); }); } /** * Client for the `/knowledge_bases` router. */ export const knowledgeBaseApi = { list: () => client.get('/knowledge_bases/'), listEmbeddingModels: () => client.get('/knowledge_bases/embedding_models'), /** Fetch the JSON Schema describing the KB middleware's tunable params. */ middlewareParametersSchema: () => client.get( '/knowledge_bases/middleware/parameters_schema', ), /** List the union of media types + extensions every parser accepts. */ supportedContentTypes: () => client.get('/knowledge_bases/supported_content_types'), create: (body: CreateKnowledgeBaseRequest) => client.post('/knowledge_bases/', body), update: (knowledgeBaseId: string, body: UpdateKnowledgeBaseRequest) => client.patch(`/knowledge_bases/${knowledgeBaseId}`, body), delete: (knowledgeBaseId: string) => client.delete(`/knowledge_bases/${knowledgeBaseId}`), /** List every document registered against a knowledge base. */ listDocuments: (knowledgeBaseId: string) => client.get(`/knowledge_bases/${knowledgeBaseId}/documents`), /** * Batch-query lifecycle status for a list of documents. * * Missing ids are silently omitted by the server, so the response * may be shorter than the input. An empty `ids` short-circuits * locally — the backend treats an empty list as a 200 with * `items: []`, but skipping the round-trip is friendlier to the * polling loop. */ getDocumentStatus: (knowledgeBaseId: string, ids: string[]) => { if (ids.length === 0) { return Promise.resolve({ items: [], }); } return client.get( `/knowledge_bases/${knowledgeBaseId}/documents/status`, { ids: ids.join(',') }, ); }, uploadDocument: (knowledgeBaseId: string, file: File, options?: UploadDocumentOptions) => uploadDocumentXhr(knowledgeBaseId, file, options), deleteDocument: (knowledgeBaseId: string, documentId: string) => client.delete(`/knowledge_bases/${knowledgeBaseId}/documents/${documentId}`), search: (knowledgeBaseId: string, body: SearchKnowledgeBaseRequest) => client.post( `/knowledge_bases/${knowledgeBaseId}/search`, body, ), };