@babav/knowledge-core-client 0.22.5 → 0.23.1

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 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;
@@ -433,11 +443,10 @@ export declare class KnowledgeCoreClient extends HttpBase {
433
443
  limit?: number;
434
444
  cursor?: string;
435
445
  }) => Promise<Page<Document>>;
436
- /** Ingest a document ASYNCHRONOUSLY — resolves with the created document at
437
- * status `pending` (HTTP 202); track via documents.get() polling to
438
- * `indexed`/`failed`, or the tenant result webhook. Auto-routes by size: files
439
- * over ~30 MB go via the signed-URL upload path (Cloud Run caps requests at
440
- * ~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. */
441
450
  ingestDocument: (id: UUID, a: {
442
451
  file: FileData;
443
452
  filename: string;
@@ -472,6 +481,21 @@ export declare class KnowledgeCoreClient extends HttpBase {
472
481
  folder_id?: UUID;
473
482
  custom_metadata?: Record<string, unknown>;
474
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[]>;
475
499
  };
476
500
  parse(a: {
477
501
  file: FileData;
@@ -509,6 +533,13 @@ export declare class KnowledgeCoreClient extends HttpBase {
509
533
  }) => Promise<Document>;
510
534
  contentUrl: (id: UUID, disposition?: "inline" | "attachment") => Promise<ContentUrl>;
511
535
  delete: (id: UUID) => Promise<void>;
536
+ /** Delete MANY documents (by id) from one corpus in ONE call (202) — collapses N deletes into
537
+ * one request. IDs not in the corpus are skipped. Completion observable via documents.events /
538
+ * documents.get() → 404. Returns { deleting: ids, count }. */
539
+ deleteMany: (corpusId: UUID, documentIds: UUID[]) => Promise<{
540
+ deleting: UUID[];
541
+ count: number;
542
+ }>;
512
543
  /** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). Snapshot on
513
544
  * connect, then a delta per transition, then `complete` when nothing is in-flight. Abort via
514
545
  * the signal. 501 (onError) if the environment has no doc-status bus — fall back to polling. */
package/dist/index.js CHANGED
@@ -226,24 +226,11 @@ export class KnowledgeCoreClient extends HttpBase {
226
226
  countDocuments: (id) => this.request("GET", `/v1/corpora/${id}/documents/count`),
227
227
  /** Documents in the corpus whose filename contains `q` (case-insensitive), paginated. */
228
228
  searchDocuments: (id, q, opts) => this.request("GET", `/v1/corpora/${id}/documents/search`, { query: { q, ...opts } }),
229
- /** Ingest a document ASYNCHRONOUSLY — resolves with the created document at
230
- * status `pending` (HTTP 202); track via documents.get() polling to
231
- * `indexed`/`failed`, or the tenant result webhook. Auto-routes by size: files
232
- * over ~30 MB go via the signed-URL upload path (Cloud Run caps requests at
233
- * ~32 MB), smaller files via one multipart request. One method, any size. */
234
- ingestDocument: (id, a) => {
235
- if (fileSize(a.file) > MULTIPART_MAX_BYTES)
236
- return this.corpora.uploadDocument(id, a);
237
- const fd = new FormData();
238
- fd.append("file", toBlob(a.file, a.content_type), a.filename);
239
- if (a.custom_metadata)
240
- fd.append("custom_metadata", JSON.stringify(a.custom_metadata));
241
- if (a.visibility)
242
- fd.append("visibility", a.visibility);
243
- if (a.folder_id)
244
- fd.append("folder_id", a.folder_id);
245
- return this.request("POST", `/v1/corpora/${id}/documents`, { body: fd });
246
- },
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),
247
234
  /** Mint a signed PUT URL to upload a large file straight to GCS (bypasses the
248
235
  * ~32 MB request limit). PUT the bytes to upload_url, then ingestFromUpload. */
249
236
  uploadUrl: (id, a) => this.request("POST", `/v1/corpora/${id}/documents/upload-url`, { json: a }),
@@ -267,6 +254,33 @@ export class KnowledgeCoreClient extends HttpBase {
267
254
  visibility: a.visibility, folder_id: a.folder_id, custom_metadata: a.custom_metadata,
268
255
  });
269
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
+ },
270
284
  };
271
285
  // --- parsing (utility: file -> text, stores nothing) ---
272
286
  parse(a) {
@@ -299,6 +313,10 @@ export class KnowledgeCoreClient extends HttpBase {
299
313
  update: (id, b) => this.request("PATCH", `/v1/documents/${id}`, { json: b }),
300
314
  contentUrl: (id, disposition = "inline") => this.request("GET", `/v1/documents/${id}/content-url`, { query: { disposition } }),
301
315
  delete: (id) => this.request("DELETE", `/v1/documents/${id}`),
316
+ /** Delete MANY documents (by id) from one corpus in ONE call (202) — collapses N deletes into
317
+ * one request. IDs not in the corpus are skipped. Completion observable via documents.events /
318
+ * documents.get() → 404. Returns { deleting: ids, count }. */
319
+ deleteMany: (corpusId, documentIds) => this.request("POST", `/v1/corpora/${corpusId}/documents/batch-delete`, { json: { document_ids: documentIds } }),
302
320
  /** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). Snapshot on
303
321
  * connect, then a delta per transition, then `complete` when nothing is in-flight. Abort via
304
322
  * the signal. 501 (onError) if the environment has no doc-status bus — fall back to polling. */
@@ -376,6 +394,15 @@ export class AdminClient extends HttpBase {
376
394
  // ---------------------------------------------------------------------------
377
395
  // helpers
378
396
  // ---------------------------------------------------------------------------
397
+ /** Run fn(0..n-1) with bounded concurrency (used by uploadMany for the direct-to-GCS PUTs). */
398
+ async function poolMap(n, concurrency, fn) {
399
+ let next = 0;
400
+ const workers = Array.from({ length: Math.max(1, Math.min(concurrency, n)) }, async () => {
401
+ for (let i = next++; i < n; i = next++)
402
+ await fn(i);
403
+ });
404
+ await Promise.all(workers);
405
+ }
379
406
  function dispatchSse(frame, h) {
380
407
  let event = "message";
381
408
  const dataLines = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babav/knowledge-core-client",
3
- "version": "0.22.5",
3
+ "version": "0.23.1",
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;
@@ -596,20 +601,12 @@ export class KnowledgeCoreClient extends HttpBase {
596
601
  /** Documents in the corpus whose filename contains `q` (case-insensitive), paginated. */
597
602
  searchDocuments: (id: UUID, q: string, opts?: { limit?: number; cursor?: string }) =>
598
603
  this.request<Page<Document>>("GET", `/v1/corpora/${id}/documents/search`, { query: { q, ...opts } }),
599
- /** Ingest a document ASYNCHRONOUSLY — resolves with the created document at
600
- * status `pending` (HTTP 202); track via documents.get() polling to
601
- * `indexed`/`failed`, or the tenant result webhook. Auto-routes by size: files
602
- * over ~30 MB go via the signed-URL upload path (Cloud Run caps requests at
603
- * ~32 MB), smaller files via one multipart request. One method, any size. */
604
- ingestDocument: (id: UUID, a: { file: FileData; filename: string; content_type?: string; visibility?: Visibility; folder_id?: UUID; custom_metadata?: Record<string, unknown> }) => {
605
- if (fileSize(a.file) > MULTIPART_MAX_BYTES) return this.corpora.uploadDocument(id, a);
606
- const fd = new FormData();
607
- fd.append("file", toBlob(a.file, a.content_type), a.filename);
608
- if (a.custom_metadata) fd.append("custom_metadata", JSON.stringify(a.custom_metadata));
609
- if (a.visibility) fd.append("visibility", a.visibility);
610
- if (a.folder_id) fd.append("folder_id", a.folder_id);
611
- return this.request<Document>("POST", `/v1/corpora/${id}/documents`, { body: fd });
612
- },
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),
613
610
  /** Mint a signed PUT URL to upload a large file straight to GCS (bypasses the
614
611
  * ~32 MB request limit). PUT the bytes to upload_url, then ingestFromUpload. */
615
612
  uploadUrl: (id: UUID, a: { filename: string; content_type?: string }) =>
@@ -635,6 +632,41 @@ export class KnowledgeCoreClient extends HttpBase {
635
632
  visibility: a.visibility, folder_id: a.folder_id, custom_metadata: a.custom_metadata,
636
633
  });
637
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
+ },
638
670
  };
639
671
 
640
672
  // --- parsing (utility: file -> text, stores nothing) ---
@@ -677,6 +709,12 @@ export class KnowledgeCoreClient extends HttpBase {
677
709
  contentUrl: (id: UUID, disposition: "inline" | "attachment" = "inline") =>
678
710
  this.request<ContentUrl>("GET", `/v1/documents/${id}/content-url`, { query: { disposition } }),
679
711
  delete: (id: UUID) => this.request<void>("DELETE", `/v1/documents/${id}`),
712
+ /** Delete MANY documents (by id) from one corpus in ONE call (202) — collapses N deletes into
713
+ * one request. IDs not in the corpus are skipped. Completion observable via documents.events /
714
+ * documents.get() → 404. Returns { deleting: ids, count }. */
715
+ deleteMany: (corpusId: UUID, documentIds: UUID[]) =>
716
+ this.request<{ deleting: UUID[]; count: number }>(
717
+ "POST", `/v1/corpora/${corpusId}/documents/batch-delete`, { json: { document_ids: documentIds } }),
680
718
  /** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). Snapshot on
681
719
  * connect, then a delta per transition, then `complete` when nothing is in-flight. Abort via
682
720
  * the signal. 501 (onError) if the environment has no doc-status bus — fall back to polling. */
@@ -772,6 +810,15 @@ export class AdminClient extends HttpBase {
772
810
  // ---------------------------------------------------------------------------
773
811
  // helpers
774
812
  // ---------------------------------------------------------------------------
813
+ /** Run fn(0..n-1) with bounded concurrency (used by uploadMany for the direct-to-GCS PUTs). */
814
+ async function poolMap(n: number, concurrency: number, fn: (i: number) => Promise<void>): Promise<void> {
815
+ let next = 0;
816
+ const workers = Array.from({ length: Math.max(1, Math.min(concurrency, n)) }, async () => {
817
+ for (let i = next++; i < n; i = next++) await fn(i);
818
+ });
819
+ await Promise.all(workers);
820
+ }
821
+
775
822
  function dispatchSse(frame: string, h: StreamHandlers): void {
776
823
  let event = "message";
777
824
  const dataLines: string[] = [];