@babav/knowledge-core-client 0.22.4 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +64 -5
- package/dist/index.js +103 -18
- package/package.json +1 -1
- package/src/index.ts +133 -14
package/dist/index.d.ts
CHANGED
|
@@ -214,6 +214,16 @@ export interface UploadUrl {
|
|
|
214
214
|
gcs_uri: string;
|
|
215
215
|
expires_in: number;
|
|
216
216
|
}
|
|
217
|
+
/** Batch of signed PUT URLs (corpora.uploadMany drives this). `items` is in request order. */
|
|
218
|
+
export interface BatchUploadUrls {
|
|
219
|
+
items: Array<{
|
|
220
|
+
filename: string;
|
|
221
|
+
document_id: UUID;
|
|
222
|
+
upload_url: string;
|
|
223
|
+
gcs_uri: string;
|
|
224
|
+
}>;
|
|
225
|
+
expires_in: number;
|
|
226
|
+
}
|
|
217
227
|
export interface Conversation {
|
|
218
228
|
id: UUID;
|
|
219
229
|
title: string | null;
|
|
@@ -359,12 +369,41 @@ export interface StreamHandlers {
|
|
|
359
369
|
/** Catch-all for any event (incl. unknown ones). */
|
|
360
370
|
onEvent?: (event: string, data: unknown) => void;
|
|
361
371
|
}
|
|
372
|
+
/** A live document status transition pushed over the ingestion-status SSE stream
|
|
373
|
+
* (documents.events / folders.events). Replaces polling GET /documents/count. */
|
|
374
|
+
export interface DocumentStatusEvent {
|
|
375
|
+
document_id: string;
|
|
376
|
+
corpus_id?: string | null;
|
|
377
|
+
folder_id?: string | null;
|
|
378
|
+
status: string;
|
|
379
|
+
filename?: string | null;
|
|
380
|
+
chunk_count?: number | null;
|
|
381
|
+
error?: string | null;
|
|
382
|
+
counts?: DocumentCount;
|
|
383
|
+
}
|
|
384
|
+
export interface DocumentEventsSnapshot {
|
|
385
|
+
documents: DocumentStatusEvent[];
|
|
386
|
+
counts: DocumentCount;
|
|
387
|
+
}
|
|
388
|
+
export interface DocumentEventHandlers {
|
|
389
|
+
/** Current state on connect (one query) — render the list from this. */
|
|
390
|
+
onSnapshot?: (s: DocumentEventsSnapshot) => void;
|
|
391
|
+
/** A single document's status changed (the moment it happened). Update that row + counts. */
|
|
392
|
+
onDocument?: (e: DocumentStatusEvent) => void;
|
|
393
|
+
/** Nothing is in-flight anymore — the stream is about to close. Authoritative final counts. */
|
|
394
|
+
onComplete?: (counts: DocumentCount) => void;
|
|
395
|
+
onError?: (err: unknown) => void;
|
|
396
|
+
}
|
|
362
397
|
export declare class KnowledgeCoreClient extends HttpBase {
|
|
363
398
|
/** @param opts.apiKey a TENANT key, supplied by the caller. */
|
|
364
399
|
constructor(opts: ClientOptions);
|
|
365
400
|
query(agentId: UUID, body: QueryRequest): Promise<QueryResponse>;
|
|
366
401
|
/** Streaming query (SSE). Resolves when the stream ends. */
|
|
367
402
|
queryStream(agentId: UUID, body: QueryRequest, handlers: StreamHandlers, signal?: AbortSignal): Promise<void>;
|
|
403
|
+
/** Shared SSE reader for the document-status streams (documents.events / folders.events).
|
|
404
|
+
* Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
|
|
405
|
+
* cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
|
|
406
|
+
_streamDocEvents(path: string, handlers: DocumentEventHandlers, signal?: AbortSignal): Promise<void>;
|
|
368
407
|
retrieve(body: {
|
|
369
408
|
query: string;
|
|
370
409
|
corpus_ids: UUID[];
|
|
@@ -404,11 +443,10 @@ export declare class KnowledgeCoreClient extends HttpBase {
|
|
|
404
443
|
limit?: number;
|
|
405
444
|
cursor?: string;
|
|
406
445
|
}) => Promise<Page<Document>>;
|
|
407
|
-
/** Ingest
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* ~32 MB), smaller files via one multipart request. One method, any size. */
|
|
446
|
+
/** Ingest ONE document ASYNCHRONOUSLY — resolves with the created document at status
|
|
447
|
+
* `pending` (HTTP 202); track via documents.events()/get() or the tenant webhook.
|
|
448
|
+
* ALWAYS uploads the bytes straight to GCS via a signed URL (uploadUrl → PUT → ingestFromUpload),
|
|
449
|
+
* regardless of size — so there is NO request-size cap, ever. One method, any size. */
|
|
412
450
|
ingestDocument: (id: UUID, a: {
|
|
413
451
|
file: FileData;
|
|
414
452
|
filename: string;
|
|
@@ -443,6 +481,21 @@ export declare class KnowledgeCoreClient extends HttpBase {
|
|
|
443
481
|
folder_id?: UUID;
|
|
444
482
|
custom_metadata?: Record<string, unknown>;
|
|
445
483
|
}) => Promise<Document>;
|
|
484
|
+
/** Ingest MANY files in ONE go — any size, no 429, no client backoff. Mints all signed URLs in
|
|
485
|
+
* one request, PUTs the bytes straight to GCS (bounded concurrency; never touches the KC), then
|
|
486
|
+
* registers the whole batch in one request (N pending docs + N Cloud Tasks). `opts`
|
|
487
|
+
* (folder_id / visibility / custom_metadata — the latter carries `owner`) applies to ALL files.
|
|
488
|
+
* Resolves with the created `pending` documents. */
|
|
489
|
+
uploadMany: (id: UUID, files: Array<{
|
|
490
|
+
file: FileData;
|
|
491
|
+
filename: string;
|
|
492
|
+
content_type?: string;
|
|
493
|
+
}>, opts?: {
|
|
494
|
+
folder_id?: UUID;
|
|
495
|
+
visibility?: Visibility;
|
|
496
|
+
custom_metadata?: Record<string, unknown>;
|
|
497
|
+
concurrency?: number;
|
|
498
|
+
}) => Promise<Document[]>;
|
|
446
499
|
};
|
|
447
500
|
parse(a: {
|
|
448
501
|
file: FileData;
|
|
@@ -466,6 +519,8 @@ export declare class KnowledgeCoreClient extends HttpBase {
|
|
|
466
519
|
limit?: number;
|
|
467
520
|
cursor?: string;
|
|
468
521
|
}) => Promise<Page<Document>>;
|
|
522
|
+
/** Live ingestion-status stream for ONE folder (same contract as documents.events). */
|
|
523
|
+
events: (folderId: UUID, handlers: DocumentEventHandlers, signal?: AbortSignal) => Promise<void>;
|
|
469
524
|
};
|
|
470
525
|
documents: {
|
|
471
526
|
get: (id: UUID) => Promise<Document>;
|
|
@@ -478,6 +533,10 @@ export declare class KnowledgeCoreClient extends HttpBase {
|
|
|
478
533
|
}) => Promise<Document>;
|
|
479
534
|
contentUrl: (id: UUID, disposition?: "inline" | "attachment") => Promise<ContentUrl>;
|
|
480
535
|
delete: (id: UUID) => Promise<void>;
|
|
536
|
+
/** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). Snapshot on
|
|
537
|
+
* connect, then a delta per transition, then `complete` when nothing is in-flight. Abort via
|
|
538
|
+
* the signal. 501 (onError) if the environment has no doc-status bus — fall back to polling. */
|
|
539
|
+
events: (corpusId: UUID, handlers: DocumentEventHandlers, signal?: AbortSignal) => Promise<void>;
|
|
481
540
|
};
|
|
482
541
|
conversations: {
|
|
483
542
|
create: (b?: {
|
package/dist/index.js
CHANGED
|
@@ -176,6 +176,38 @@ export class KnowledgeCoreClient extends HttpBase {
|
|
|
176
176
|
if (buf.trim())
|
|
177
177
|
dispatchSse(buf, handlers);
|
|
178
178
|
}
|
|
179
|
+
/** Shared SSE reader for the document-status streams (documents.events / folders.events).
|
|
180
|
+
* Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
|
|
181
|
+
* cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
|
|
182
|
+
async _streamDocEvents(path, handlers, signal) {
|
|
183
|
+
const res = await this.raw("GET", path, { signal });
|
|
184
|
+
if (!res.ok || !res.body) {
|
|
185
|
+
const t = await res.text();
|
|
186
|
+
const err = new KnowledgeCoreError(res.status, safeJson(t), path);
|
|
187
|
+
if (handlers.onError) {
|
|
188
|
+
handlers.onError(err);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
const reader = res.body.getReader();
|
|
194
|
+
const decoder = new TextDecoder();
|
|
195
|
+
let buf = "";
|
|
196
|
+
for (;;) {
|
|
197
|
+
const { value, done } = await reader.read();
|
|
198
|
+
if (done)
|
|
199
|
+
break;
|
|
200
|
+
buf += decoder.decode(value, { stream: true });
|
|
201
|
+
let idx;
|
|
202
|
+
while ((idx = buf.indexOf("\n\n")) !== -1) {
|
|
203
|
+
const frame = buf.slice(0, idx);
|
|
204
|
+
buf = buf.slice(idx + 2);
|
|
205
|
+
dispatchDocEvent(frame, handlers);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (buf.trim())
|
|
209
|
+
dispatchDocEvent(buf, handlers);
|
|
210
|
+
}
|
|
179
211
|
// --- retrieve (cross-corpus primitive, no generation) ---
|
|
180
212
|
retrieve(body) {
|
|
181
213
|
return this.request("POST", "/v1/retrieve", { json: body });
|
|
@@ -194,24 +226,11 @@ export class KnowledgeCoreClient extends HttpBase {
|
|
|
194
226
|
countDocuments: (id) => this.request("GET", `/v1/corpora/${id}/documents/count`),
|
|
195
227
|
/** Documents in the corpus whose filename contains `q` (case-insensitive), paginated. */
|
|
196
228
|
searchDocuments: (id, q, opts) => this.request("GET", `/v1/corpora/${id}/documents/search`, { query: { q, ...opts } }),
|
|
197
|
-
/** Ingest
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
|
|
202
|
-
ingestDocument: (id, a) => {
|
|
203
|
-
if (fileSize(a.file) > MULTIPART_MAX_BYTES)
|
|
204
|
-
return this.corpora.uploadDocument(id, a);
|
|
205
|
-
const fd = new FormData();
|
|
206
|
-
fd.append("file", toBlob(a.file, a.content_type), a.filename);
|
|
207
|
-
if (a.custom_metadata)
|
|
208
|
-
fd.append("custom_metadata", JSON.stringify(a.custom_metadata));
|
|
209
|
-
if (a.visibility)
|
|
210
|
-
fd.append("visibility", a.visibility);
|
|
211
|
-
if (a.folder_id)
|
|
212
|
-
fd.append("folder_id", a.folder_id);
|
|
213
|
-
return this.request("POST", `/v1/corpora/${id}/documents`, { body: fd });
|
|
214
|
-
},
|
|
229
|
+
/** Ingest ONE document ASYNCHRONOUSLY — resolves with the created document at status
|
|
230
|
+
* `pending` (HTTP 202); track via documents.events()/get() or the tenant webhook.
|
|
231
|
+
* ALWAYS uploads the bytes straight to GCS via a signed URL (uploadUrl → PUT → ingestFromUpload),
|
|
232
|
+
* regardless of size — so there is NO request-size cap, ever. One method, any size. */
|
|
233
|
+
ingestDocument: (id, a) => this.corpora.uploadDocument(id, a),
|
|
215
234
|
/** Mint a signed PUT URL to upload a large file straight to GCS (bypasses the
|
|
216
235
|
* ~32 MB request limit). PUT the bytes to upload_url, then ingestFromUpload. */
|
|
217
236
|
uploadUrl: (id, a) => this.request("POST", `/v1/corpora/${id}/documents/upload-url`, { json: a }),
|
|
@@ -235,6 +254,33 @@ export class KnowledgeCoreClient extends HttpBase {
|
|
|
235
254
|
visibility: a.visibility, folder_id: a.folder_id, custom_metadata: a.custom_metadata,
|
|
236
255
|
});
|
|
237
256
|
},
|
|
257
|
+
/** Ingest MANY files in ONE go — any size, no 429, no client backoff. Mints all signed URLs in
|
|
258
|
+
* one request, PUTs the bytes straight to GCS (bounded concurrency; never touches the KC), then
|
|
259
|
+
* registers the whole batch in one request (N pending docs + N Cloud Tasks). `opts`
|
|
260
|
+
* (folder_id / visibility / custom_metadata — the latter carries `owner`) applies to ALL files.
|
|
261
|
+
* Resolves with the created `pending` documents. */
|
|
262
|
+
uploadMany: async (id, files, opts) => {
|
|
263
|
+
if (files.length === 0)
|
|
264
|
+
return [];
|
|
265
|
+
const urls = await this.request("POST", `/v1/corpora/${id}/documents/upload-urls`, { json: { items: files.map((f) => ({ filename: f.filename, content_type: f.content_type })) } });
|
|
266
|
+
const items = urls.items; // same order as the request
|
|
267
|
+
await poolMap(files.length, opts?.concurrency ?? 6, async (i) => {
|
|
268
|
+
const f = files[i];
|
|
269
|
+
const it = items[i];
|
|
270
|
+
const put = await this._fetch(it.upload_url, {
|
|
271
|
+
method: "PUT",
|
|
272
|
+
body: toBlob(f.file, f.content_type),
|
|
273
|
+
headers: f.content_type ? { "content-type": f.content_type } : {},
|
|
274
|
+
});
|
|
275
|
+
if (!put.ok)
|
|
276
|
+
throw new KnowledgeCoreError(put.status, safeJson(await put.text()), it.upload_url);
|
|
277
|
+
});
|
|
278
|
+
const res = await this.request("POST", `/v1/corpora/${id}/documents/batch`, { json: {
|
|
279
|
+
folder_id: opts?.folder_id, visibility: opts?.visibility, custom_metadata: opts?.custom_metadata,
|
|
280
|
+
items: items.map((it) => ({ document_id: it.document_id, filename: it.filename })),
|
|
281
|
+
} });
|
|
282
|
+
return res.documents;
|
|
283
|
+
},
|
|
238
284
|
};
|
|
239
285
|
// --- parsing (utility: file -> text, stores nothing) ---
|
|
240
286
|
parse(a) {
|
|
@@ -255,6 +301,8 @@ export class KnowledgeCoreClient extends HttpBase {
|
|
|
255
301
|
countDocuments: (folderId) => this.request("GET", `/v1/folders/${folderId}/documents/count`),
|
|
256
302
|
/** Documents in the folder whose filename contains `q` (case-insensitive), paginated. */
|
|
257
303
|
searchDocuments: (folderId, q, opts) => this.request("GET", `/v1/folders/${folderId}/documents/search`, { query: { q, ...opts } }),
|
|
304
|
+
/** Live ingestion-status stream for ONE folder (same contract as documents.events). */
|
|
305
|
+
events: (folderId, handlers, signal) => this._streamDocEvents(`/v1/folders/${folderId}/documents/events`, handlers, signal),
|
|
258
306
|
};
|
|
259
307
|
// --- documents ---
|
|
260
308
|
documents = {
|
|
@@ -265,6 +313,10 @@ export class KnowledgeCoreClient extends HttpBase {
|
|
|
265
313
|
update: (id, b) => this.request("PATCH", `/v1/documents/${id}`, { json: b }),
|
|
266
314
|
contentUrl: (id, disposition = "inline") => this.request("GET", `/v1/documents/${id}/content-url`, { query: { disposition } }),
|
|
267
315
|
delete: (id) => this.request("DELETE", `/v1/documents/${id}`),
|
|
316
|
+
/** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). Snapshot on
|
|
317
|
+
* connect, then a delta per transition, then `complete` when nothing is in-flight. Abort via
|
|
318
|
+
* the signal. 501 (onError) if the environment has no doc-status bus — fall back to polling. */
|
|
319
|
+
events: (corpusId, handlers, signal) => this._streamDocEvents(`/v1/corpora/${corpusId}/documents/events`, handlers, signal),
|
|
268
320
|
};
|
|
269
321
|
// --- conversations ---
|
|
270
322
|
conversations = {
|
|
@@ -338,6 +390,15 @@ export class AdminClient extends HttpBase {
|
|
|
338
390
|
// ---------------------------------------------------------------------------
|
|
339
391
|
// helpers
|
|
340
392
|
// ---------------------------------------------------------------------------
|
|
393
|
+
/** Run fn(0..n-1) with bounded concurrency (used by uploadMany for the direct-to-GCS PUTs). */
|
|
394
|
+
async function poolMap(n, concurrency, fn) {
|
|
395
|
+
let next = 0;
|
|
396
|
+
const workers = Array.from({ length: Math.max(1, Math.min(concurrency, n)) }, async () => {
|
|
397
|
+
for (let i = next++; i < n; i = next++)
|
|
398
|
+
await fn(i);
|
|
399
|
+
});
|
|
400
|
+
await Promise.all(workers);
|
|
401
|
+
}
|
|
341
402
|
function dispatchSse(frame, h) {
|
|
342
403
|
let event = "message";
|
|
343
404
|
const dataLines = [];
|
|
@@ -384,3 +445,27 @@ function dispatchSse(frame, h) {
|
|
|
384
445
|
break;
|
|
385
446
|
}
|
|
386
447
|
}
|
|
448
|
+
function dispatchDocEvent(frame, h) {
|
|
449
|
+
let event = "message";
|
|
450
|
+
const dataLines = [];
|
|
451
|
+
for (const line of frame.split("\n")) {
|
|
452
|
+
if (line.startsWith("event:"))
|
|
453
|
+
event = line.slice(6).trim();
|
|
454
|
+
else if (line.startsWith("data:"))
|
|
455
|
+
dataLines.push(line.slice(5).trim());
|
|
456
|
+
}
|
|
457
|
+
if (dataLines.length === 0)
|
|
458
|
+
return; // heartbeat / comment frame
|
|
459
|
+
const data = safeJson(dataLines.join("\n"));
|
|
460
|
+
switch (event) {
|
|
461
|
+
case "snapshot":
|
|
462
|
+
h.onSnapshot?.(data);
|
|
463
|
+
break;
|
|
464
|
+
case "document":
|
|
465
|
+
h.onDocument?.(data);
|
|
466
|
+
break;
|
|
467
|
+
case "complete":
|
|
468
|
+
h.onComplete?.(data);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@babav/knowledge-core-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "TypeScript client for the Babav Knowledge Core API (Deno + Node 18+, zero deps). Includes the babav.visual grammar TYPES at the ./visual subpath (types only; all visual rendering is server-side).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/index.ts
CHANGED
|
@@ -235,6 +235,11 @@ export interface UploadUrl {
|
|
|
235
235
|
gcs_uri: string;
|
|
236
236
|
expires_in: number;
|
|
237
237
|
}
|
|
238
|
+
/** Batch of signed PUT URLs (corpora.uploadMany drives this). `items` is in request order. */
|
|
239
|
+
export interface BatchUploadUrls {
|
|
240
|
+
items: Array<{ filename: string; document_id: UUID; upload_url: string; gcs_uri: string }>;
|
|
241
|
+
expires_in: number;
|
|
242
|
+
}
|
|
238
243
|
export interface Conversation {
|
|
239
244
|
id: UUID;
|
|
240
245
|
title: string | null;
|
|
@@ -480,6 +485,32 @@ export interface StreamHandlers {
|
|
|
480
485
|
onEvent?: (event: string, data: unknown) => void;
|
|
481
486
|
}
|
|
482
487
|
|
|
488
|
+
/** A live document status transition pushed over the ingestion-status SSE stream
|
|
489
|
+
* (documents.events / folders.events). Replaces polling GET /documents/count. */
|
|
490
|
+
export interface DocumentStatusEvent {
|
|
491
|
+
document_id: string;
|
|
492
|
+
corpus_id?: string | null;
|
|
493
|
+
folder_id?: string | null;
|
|
494
|
+
status: string; // pending | ingesting | indexed | failed | deleting
|
|
495
|
+
filename?: string | null;
|
|
496
|
+
chunk_count?: number | null;
|
|
497
|
+
error?: string | null;
|
|
498
|
+
counts?: DocumentCount; // running aggregate, included on each `document` delta
|
|
499
|
+
}
|
|
500
|
+
export interface DocumentEventsSnapshot {
|
|
501
|
+
documents: DocumentStatusEvent[]; // current state of every doc in the corpus/folder
|
|
502
|
+
counts: DocumentCount;
|
|
503
|
+
}
|
|
504
|
+
export interface DocumentEventHandlers {
|
|
505
|
+
/** Current state on connect (one query) — render the list from this. */
|
|
506
|
+
onSnapshot?: (s: DocumentEventsSnapshot) => void;
|
|
507
|
+
/** A single document's status changed (the moment it happened). Update that row + counts. */
|
|
508
|
+
onDocument?: (e: DocumentStatusEvent) => void;
|
|
509
|
+
/** Nothing is in-flight anymore — the stream is about to close. Authoritative final counts. */
|
|
510
|
+
onComplete?: (counts: DocumentCount) => void;
|
|
511
|
+
onError?: (err: unknown) => void;
|
|
512
|
+
}
|
|
513
|
+
|
|
483
514
|
// ---------------------------------------------------------------------------
|
|
484
515
|
// Tenant client (data ops) — use a TENANT key
|
|
485
516
|
// ---------------------------------------------------------------------------
|
|
@@ -518,6 +549,34 @@ export class KnowledgeCoreClient extends HttpBase {
|
|
|
518
549
|
if (buf.trim()) dispatchSse(buf, handlers);
|
|
519
550
|
}
|
|
520
551
|
|
|
552
|
+
/** Shared SSE reader for the document-status streams (documents.events / folders.events).
|
|
553
|
+
* Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
|
|
554
|
+
* cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
|
|
555
|
+
async _streamDocEvents(path: string, handlers: DocumentEventHandlers, signal?: AbortSignal): Promise<void> {
|
|
556
|
+
const res = await this.raw("GET", path, { signal });
|
|
557
|
+
if (!res.ok || !res.body) {
|
|
558
|
+
const t = await res.text();
|
|
559
|
+
const err = new KnowledgeCoreError(res.status, safeJson(t), path);
|
|
560
|
+
if (handlers.onError) { handlers.onError(err); return; }
|
|
561
|
+
throw err;
|
|
562
|
+
}
|
|
563
|
+
const reader = res.body.getReader();
|
|
564
|
+
const decoder = new TextDecoder();
|
|
565
|
+
let buf = "";
|
|
566
|
+
for (;;) {
|
|
567
|
+
const { value, done } = await reader.read();
|
|
568
|
+
if (done) break;
|
|
569
|
+
buf += decoder.decode(value, { stream: true });
|
|
570
|
+
let idx: number;
|
|
571
|
+
while ((idx = buf.indexOf("\n\n")) !== -1) {
|
|
572
|
+
const frame = buf.slice(0, idx);
|
|
573
|
+
buf = buf.slice(idx + 2);
|
|
574
|
+
dispatchDocEvent(frame, handlers);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (buf.trim()) dispatchDocEvent(buf, handlers);
|
|
578
|
+
}
|
|
579
|
+
|
|
521
580
|
// --- retrieve (cross-corpus primitive, no generation) ---
|
|
522
581
|
retrieve(body: { query: string; corpus_ids: UUID[]; top_k_retrieved_chunks?: number; top_k_reranked_chunks?: number; rerank?: boolean; instruction?: string; filter?: MetadataFilter; }): Promise<{ retrieval_contents: RetrievalContent[] }> {
|
|
523
582
|
return this.request("POST", "/v1/retrieve", { json: body });
|
|
@@ -542,20 +601,12 @@ export class KnowledgeCoreClient extends HttpBase {
|
|
|
542
601
|
/** Documents in the corpus whose filename contains `q` (case-insensitive), paginated. */
|
|
543
602
|
searchDocuments: (id: UUID, q: string, opts?: { limit?: number; cursor?: string }) =>
|
|
544
603
|
this.request<Page<Document>>("GET", `/v1/corpora/${id}/documents/search`, { query: { q, ...opts } }),
|
|
545
|
-
/** Ingest
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (fileSize(a.file) > MULTIPART_MAX_BYTES) return this.corpora.uploadDocument(id, a);
|
|
552
|
-
const fd = new FormData();
|
|
553
|
-
fd.append("file", toBlob(a.file, a.content_type), a.filename);
|
|
554
|
-
if (a.custom_metadata) fd.append("custom_metadata", JSON.stringify(a.custom_metadata));
|
|
555
|
-
if (a.visibility) fd.append("visibility", a.visibility);
|
|
556
|
-
if (a.folder_id) fd.append("folder_id", a.folder_id);
|
|
557
|
-
return this.request<Document>("POST", `/v1/corpora/${id}/documents`, { body: fd });
|
|
558
|
-
},
|
|
604
|
+
/** Ingest ONE document ASYNCHRONOUSLY — resolves with the created document at status
|
|
605
|
+
* `pending` (HTTP 202); track via documents.events()/get() or the tenant webhook.
|
|
606
|
+
* ALWAYS uploads the bytes straight to GCS via a signed URL (uploadUrl → PUT → ingestFromUpload),
|
|
607
|
+
* regardless of size — so there is NO request-size cap, ever. One method, any size. */
|
|
608
|
+
ingestDocument: (id: UUID, a: { file: FileData; filename: string; content_type?: string; visibility?: Visibility; folder_id?: UUID; custom_metadata?: Record<string, unknown> }) =>
|
|
609
|
+
this.corpora.uploadDocument(id, a),
|
|
559
610
|
/** Mint a signed PUT URL to upload a large file straight to GCS (bypasses the
|
|
560
611
|
* ~32 MB request limit). PUT the bytes to upload_url, then ingestFromUpload. */
|
|
561
612
|
uploadUrl: (id: UUID, a: { filename: string; content_type?: string }) =>
|
|
@@ -581,6 +632,41 @@ export class KnowledgeCoreClient extends HttpBase {
|
|
|
581
632
|
visibility: a.visibility, folder_id: a.folder_id, custom_metadata: a.custom_metadata,
|
|
582
633
|
});
|
|
583
634
|
},
|
|
635
|
+
/** Ingest MANY files in ONE go — any size, no 429, no client backoff. Mints all signed URLs in
|
|
636
|
+
* one request, PUTs the bytes straight to GCS (bounded concurrency; never touches the KC), then
|
|
637
|
+
* registers the whole batch in one request (N pending docs + N Cloud Tasks). `opts`
|
|
638
|
+
* (folder_id / visibility / custom_metadata — the latter carries `owner`) applies to ALL files.
|
|
639
|
+
* Resolves with the created `pending` documents. */
|
|
640
|
+
uploadMany: async (
|
|
641
|
+
id: UUID,
|
|
642
|
+
files: Array<{ file: FileData; filename: string; content_type?: string }>,
|
|
643
|
+
opts?: { folder_id?: UUID; visibility?: Visibility; custom_metadata?: Record<string, unknown>; concurrency?: number },
|
|
644
|
+
): Promise<Document[]> => {
|
|
645
|
+
if (files.length === 0) return [];
|
|
646
|
+
const urls = await this.request<BatchUploadUrls>(
|
|
647
|
+
"POST", `/v1/corpora/${id}/documents/upload-urls`,
|
|
648
|
+
{ json: { items: files.map((f) => ({ filename: f.filename, content_type: f.content_type })) } },
|
|
649
|
+
);
|
|
650
|
+
const items = urls.items; // same order as the request
|
|
651
|
+
await poolMap(files.length, opts?.concurrency ?? 6, async (i) => {
|
|
652
|
+
const f = files[i]!;
|
|
653
|
+
const it = items[i]!;
|
|
654
|
+
const put = await this._fetch(it.upload_url, {
|
|
655
|
+
method: "PUT",
|
|
656
|
+
body: toBlob(f.file, f.content_type),
|
|
657
|
+
headers: f.content_type ? { "content-type": f.content_type } : {},
|
|
658
|
+
});
|
|
659
|
+
if (!put.ok) throw new KnowledgeCoreError(put.status, safeJson(await put.text()), it.upload_url);
|
|
660
|
+
});
|
|
661
|
+
const res = await this.request<{ documents: Document[] }>(
|
|
662
|
+
"POST", `/v1/corpora/${id}/documents/batch`,
|
|
663
|
+
{ json: {
|
|
664
|
+
folder_id: opts?.folder_id, visibility: opts?.visibility, custom_metadata: opts?.custom_metadata,
|
|
665
|
+
items: items.map((it) => ({ document_id: it.document_id, filename: it.filename })),
|
|
666
|
+
} },
|
|
667
|
+
);
|
|
668
|
+
return res.documents;
|
|
669
|
+
},
|
|
584
670
|
};
|
|
585
671
|
|
|
586
672
|
// --- parsing (utility: file -> text, stores nothing) ---
|
|
@@ -607,6 +693,9 @@ export class KnowledgeCoreClient extends HttpBase {
|
|
|
607
693
|
/** Documents in the folder whose filename contains `q` (case-insensitive), paginated. */
|
|
608
694
|
searchDocuments: (folderId: UUID, q: string, opts?: { limit?: number; cursor?: string }) =>
|
|
609
695
|
this.request<Page<Document>>("GET", `/v1/folders/${folderId}/documents/search`, { query: { q, ...opts } }),
|
|
696
|
+
/** Live ingestion-status stream for ONE folder (same contract as documents.events). */
|
|
697
|
+
events: (folderId: UUID, handlers: DocumentEventHandlers, signal?: AbortSignal) =>
|
|
698
|
+
this._streamDocEvents(`/v1/folders/${folderId}/documents/events`, handlers, signal),
|
|
610
699
|
};
|
|
611
700
|
|
|
612
701
|
// --- documents ---
|
|
@@ -620,6 +709,11 @@ export class KnowledgeCoreClient extends HttpBase {
|
|
|
620
709
|
contentUrl: (id: UUID, disposition: "inline" | "attachment" = "inline") =>
|
|
621
710
|
this.request<ContentUrl>("GET", `/v1/documents/${id}/content-url`, { query: { disposition } }),
|
|
622
711
|
delete: (id: UUID) => this.request<void>("DELETE", `/v1/documents/${id}`),
|
|
712
|
+
/** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). Snapshot on
|
|
713
|
+
* connect, then a delta per transition, then `complete` when nothing is in-flight. Abort via
|
|
714
|
+
* the signal. 501 (onError) if the environment has no doc-status bus — fall back to polling. */
|
|
715
|
+
events: (corpusId: UUID, handlers: DocumentEventHandlers, signal?: AbortSignal) =>
|
|
716
|
+
this._streamDocEvents(`/v1/corpora/${corpusId}/documents/events`, handlers, signal),
|
|
623
717
|
};
|
|
624
718
|
|
|
625
719
|
// --- conversations ---
|
|
@@ -710,6 +804,15 @@ export class AdminClient extends HttpBase {
|
|
|
710
804
|
// ---------------------------------------------------------------------------
|
|
711
805
|
// helpers
|
|
712
806
|
// ---------------------------------------------------------------------------
|
|
807
|
+
/** Run fn(0..n-1) with bounded concurrency (used by uploadMany for the direct-to-GCS PUTs). */
|
|
808
|
+
async function poolMap(n: number, concurrency: number, fn: (i: number) => Promise<void>): Promise<void> {
|
|
809
|
+
let next = 0;
|
|
810
|
+
const workers = Array.from({ length: Math.max(1, Math.min(concurrency, n)) }, async () => {
|
|
811
|
+
for (let i = next++; i < n; i = next++) await fn(i);
|
|
812
|
+
});
|
|
813
|
+
await Promise.all(workers);
|
|
814
|
+
}
|
|
815
|
+
|
|
713
816
|
function dispatchSse(frame: string, h: StreamHandlers): void {
|
|
714
817
|
let event = "message";
|
|
715
818
|
const dataLines: string[] = [];
|
|
@@ -733,3 +836,19 @@ function dispatchSse(frame: string, h: StreamHandlers): void {
|
|
|
733
836
|
case "done": h.onDone?.(); break;
|
|
734
837
|
}
|
|
735
838
|
}
|
|
839
|
+
|
|
840
|
+
function dispatchDocEvent(frame: string, h: DocumentEventHandlers): void {
|
|
841
|
+
let event = "message";
|
|
842
|
+
const dataLines: string[] = [];
|
|
843
|
+
for (const line of frame.split("\n")) {
|
|
844
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
845
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
|
|
846
|
+
}
|
|
847
|
+
if (dataLines.length === 0) return; // heartbeat / comment frame
|
|
848
|
+
const data = safeJson(dataLines.join("\n"));
|
|
849
|
+
switch (event) {
|
|
850
|
+
case "snapshot": h.onSnapshot?.(data as DocumentEventsSnapshot); break;
|
|
851
|
+
case "document": h.onDocument?.(data as DocumentStatusEvent); break;
|
|
852
|
+
case "complete": h.onComplete?.(data as DocumentCount); break;
|
|
853
|
+
}
|
|
854
|
+
}
|