@babav/knowledge-core-client 0.22.5 → 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 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;
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) {
@@ -376,6 +390,15 @@ export class AdminClient extends HttpBase {
376
390
  // ---------------------------------------------------------------------------
377
391
  // helpers
378
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
+ }
379
402
  function dispatchSse(frame, h) {
380
403
  let event = "message";
381
404
  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.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;
@@ -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) ---
@@ -772,6 +804,15 @@ export class AdminClient extends HttpBase {
772
804
  // ---------------------------------------------------------------------------
773
805
  // helpers
774
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
+
775
816
  function dispatchSse(frame: string, h: StreamHandlers): void {
776
817
  let event = "message";
777
818
  const dataLines: string[] = [];