@frontera-sdk/knowledge 0.0.0-stage → 1.53.17

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.
@@ -0,0 +1,120 @@
1
+ import { FronteraError } from '@frontera-sdk/core/errors'
2
+
3
+ import type { KnowledgeClient } from './knowledge-client'
4
+ import { SETTLED_STATUSES, type DocumentStatus, type KnowledgeDocument, type UploadResult } from './types'
5
+
6
+ export type WaitOptions = {
7
+ /** The first pause between polls; each later pause is half as long again, up to `maxIntervalMs`. 1,500 ms by default. */
8
+ intervalMs?: number
9
+ /** 10,000 ms by default. */
10
+ maxIntervalMs?: number
11
+ /** 10 minutes by default. Past it the wait throws `KNOWLEDGE_WAIT_TIMEOUT` and leaves the documents as they are. */
12
+ timeoutMs?: number
13
+ signal?: AbortSignal
14
+ /** Every poll's answer, for progress; the next poll waits for it to finish. */
15
+ onPoll?: (documents: readonly KnowledgeDocument[]) => void | Promise<void>
16
+ /**
17
+ * Sends one document's file again, at most once per document. The wait calls
18
+ * it when an upload did not get its file stored: the document vanished before
19
+ * its malware check finished (the earlier upload of the same bytes it was
20
+ * answered with failed to store them), or it failed with `errorKind`
21
+ * `storage` (an upload cut short). Answer what the upload answers. Without
22
+ * it, such a document ends as it is: missing, or failed. A document that
23
+ * vanishes later was removed by someone, and is never sent again.
24
+ */
25
+ resend?: (documentId: string) => Promise<UploadResult>
26
+ }
27
+
28
+ export type WaitResult = {
29
+ /** The settled documents in the order asked; one sent again is the document its new upload answered. */
30
+ documents: KnowledgeDocument[]
31
+ /** The asked ids that are gone or that the caller cannot see, including one whose sending again failed. */
32
+ missing: string[]
33
+ }
34
+
35
+ /** An upload whose file was never stored: sending the same bytes again stores them in that document. */
36
+ const cutShort = (document: KnowledgeDocument) => document.status === 'failed' && document.errorKind === 'storage' && !document.searchable
37
+
38
+ /**
39
+ * What one poll says about a document, given the status the wait saw last
40
+ * (null before the first): still on its way, settled, gone and staying gone,
41
+ * or worth sending again. A document gone before its malware check finished
42
+ * lost its file with the upload it duplicated; one gone after was removed.
43
+ */
44
+ function pollVerdict(document: KnowledgeDocument | undefined, lastSeen: DocumentStatus | null): 'waiting' | 'settled' | 'gone' | 'send again' {
45
+ if (!document) return lastSeen === null || lastSeen === 'scanning' ? 'send again' : 'gone'
46
+ if (cutShort(document)) return 'send again'
47
+ return SETTLED_STATUSES.has(document.status) ? 'settled' : 'waiting'
48
+ }
49
+
50
+ type Slot = { asked: string; id: string; lastSeen: DocumentStatus | null; resent: boolean; end?: KnowledgeDocument | 'gone' }
51
+
52
+ /**
53
+ * Polls `POST /documents/status`, 200 ids a request, until every document is
54
+ * settled (`ready`, `failed` or `blocked`) or gone. It never throws for a
55
+ * failed or blocked document: read each one's `status`.
56
+ */
57
+ export async function waitUntilReady(knowledge: KnowledgeClient, ids: readonly string[], options: WaitOptions = {}): Promise<WaitResult> {
58
+ const { intervalMs = 1500, maxIntervalMs = 10_000, timeoutMs = 10 * 60_000, signal, onPoll, resend } = options
59
+ const slots: Slot[] = [...new Set(ids)].map((id) => ({ asked: id, id, lastSeen: null, resent: false }))
60
+ const deadline = Date.now() + timeoutMs
61
+ let pause = intervalMs
62
+ for (;;) {
63
+ throwIfAborted(signal)
64
+ const open = slots.filter((slot) => slot.end === undefined)
65
+ if (open.length === 0) break
66
+ const { documents } = await knowledge.documents.status(open.map((slot) => slot.id), signal)
67
+ await onPoll?.(documents)
68
+ const byId = new Map(documents.map((document) => [document.id, document]))
69
+ for (const slot of open) {
70
+ const document = byId.get(slot.id)
71
+ const verdict = pollVerdict(document, slot.lastSeen)
72
+ if (document) slot.lastSeen = document.status
73
+ if (verdict === 'send again' && resend && !slot.resent) {
74
+ slot.resent = true
75
+ try {
76
+ const again = await resend(slot.id)
77
+ slot.id = again.document.id
78
+ slot.lastSeen = again.document.status
79
+ } catch {
80
+ slot.end = 'gone'
81
+ }
82
+ } else if (verdict !== 'waiting') {
83
+ slot.end = document ?? 'gone'
84
+ }
85
+ }
86
+ if (slots.every((slot) => slot.end !== undefined)) break
87
+ if (Date.now() >= deadline) {
88
+ const waiting = slots.filter((slot) => slot.end === undefined)
89
+ throw new FronteraError(`${waiting.length} of ${slots.length} documents did not settle within ${timeoutMs} ms`, {
90
+ code: 'KNOWLEDGE_WAIT_TIMEOUT',
91
+ details: { waiting: waiting.map((slot) => ({ id: slot.id, status: slot.lastSeen })) },
92
+ })
93
+ }
94
+ await sleep(Math.min(pause, deadline - Date.now()), signal)
95
+ pause = Math.min(maxIntervalMs, pause * 1.5)
96
+ }
97
+ return {
98
+ documents: slots.flatMap((slot) => (slot.end && slot.end !== 'gone' ? [slot.end] : [])),
99
+ missing: slots.filter((slot) => slot.end === 'gone').map((slot) => slot.asked),
100
+ }
101
+ }
102
+
103
+ export function throwIfAborted(signal: AbortSignal | undefined): void {
104
+ if (signal?.aborted) throw new FronteraError('Knowledge wait was aborted', { code: 'ABORTED', details: { reason: signal.reason } })
105
+ }
106
+
107
+ /** Resolves after `ms`, or as soon as `signal` aborts. */
108
+ export function sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
109
+ return new Promise((resolve) => {
110
+ const timer = setTimeout(() => {
111
+ signal?.removeEventListener('abort', onAbort)
112
+ resolve()
113
+ }, Math.max(0, ms))
114
+ function onAbort() {
115
+ clearTimeout(timer)
116
+ resolve()
117
+ }
118
+ signal?.addEventListener('abort', onAbort, { once: true })
119
+ })
120
+ }
@@ -0,0 +1,588 @@
1
+ /**
2
+ * Generated from the service's Knowledge schemas by `bun run sdk:knowledge-wire`.
3
+ * Do not edit: a service test fails while this file differs from what the
4
+ * generator writes. Each `…Answer` is what one route answers; a date is its
5
+ * ISO string.
6
+ */
7
+
8
+ export type CapabilitiesAnswer = {
9
+ steward: boolean
10
+ canAssignStewards: boolean
11
+ canCreate: boolean
12
+ }
13
+
14
+ export type BasesAnswer = {
15
+ bases: Array<{
16
+ id: string
17
+ orgId: string
18
+ ownerWorkspaceId: string | null
19
+ name: string
20
+ description: string | null
21
+ createdBy: string
22
+ createdAt: string
23
+ updatedAt: string
24
+ canManage: boolean
25
+ sharing: {
26
+ allWorkspaces: boolean
27
+ workspaceIds: string[]
28
+ } | null
29
+ }>
30
+ }
31
+
32
+ export type BaseAnswer = {
33
+ base: {
34
+ id: string
35
+ orgId: string
36
+ ownerWorkspaceId: string | null
37
+ name: string
38
+ description: string | null
39
+ createdBy: string
40
+ createdAt: string
41
+ updatedAt: string
42
+ canManage: boolean
43
+ sharing: {
44
+ allWorkspaces: boolean
45
+ workspaceIds: string[]
46
+ } | null
47
+ }
48
+ }
49
+
50
+ export type CreatedBaseAnswer = {
51
+ base: {
52
+ id: string
53
+ orgId: string
54
+ ownerWorkspaceId: string | null
55
+ name: string
56
+ description: string | null
57
+ createdBy: string
58
+ createdAt: string
59
+ updatedAt: string
60
+ canManage: boolean
61
+ sharing: {
62
+ allWorkspaces: boolean
63
+ workspaceIds: string[]
64
+ } | null
65
+ }
66
+ }
67
+
68
+ export type DeletedBaseAnswer = {
69
+ base: {
70
+ id: string
71
+ documents: number
72
+ }
73
+ }
74
+
75
+ export type DocumentsAnswer = {
76
+ documents: Array<{
77
+ id: string
78
+ orgId: string
79
+ knowledgeBaseId: string | null
80
+ title: string
81
+ fileName: string | null
82
+ mimeType: string | null
83
+ byteSize: number | null
84
+ status: 'scanning' | 'pending' | 'processing' | 'ready' | 'failed' | 'blocked'
85
+ searchable: boolean
86
+ error: string | null
87
+ errorKind: string | null
88
+ viewUrl: string | null
89
+ createdBy: string | null
90
+ addedByKey: {
91
+ name: string | null
92
+ } | null
93
+ source: {
94
+ id: string
95
+ name: string
96
+ url: string | null
97
+ } | null
98
+ createdAt: string
99
+ updatedAt: string
100
+ }>
101
+ nextCursor: string | null
102
+ }
103
+
104
+ export type DocumentAnswer = {
105
+ document: {
106
+ id: string
107
+ orgId: string
108
+ knowledgeBaseId: string | null
109
+ title: string
110
+ fileName: string | null
111
+ mimeType: string | null
112
+ byteSize: number | null
113
+ status: 'scanning' | 'pending' | 'processing' | 'ready' | 'failed' | 'blocked'
114
+ searchable: boolean
115
+ error: string | null
116
+ errorKind: string | null
117
+ viewUrl: string | null
118
+ createdBy: string | null
119
+ addedByKey: {
120
+ name: string | null
121
+ } | null
122
+ source: {
123
+ id: string
124
+ name: string
125
+ url: string | null
126
+ } | null
127
+ createdAt: string
128
+ updatedAt: string
129
+ }
130
+ }
131
+
132
+ export type UploadedAnswer = {
133
+ document: {
134
+ id: string
135
+ orgId: string
136
+ knowledgeBaseId: string | null
137
+ title: string
138
+ fileName: string | null
139
+ mimeType: string | null
140
+ byteSize: number | null
141
+ status: 'scanning' | 'pending' | 'processing' | 'ready' | 'failed' | 'blocked'
142
+ searchable: boolean
143
+ error: string | null
144
+ errorKind: string | null
145
+ viewUrl: string | null
146
+ createdBy: string | null
147
+ addedByKey: {
148
+ name: string | null
149
+ } | null
150
+ source: {
151
+ id: string
152
+ name: string
153
+ url: string | null
154
+ } | null
155
+ createdAt: string
156
+ updatedAt: string
157
+ }
158
+ duplicate: boolean
159
+ }
160
+
161
+ export type DocumentDetailsAnswer = {
162
+ document: {
163
+ id: string
164
+ orgId: string
165
+ knowledgeBaseId: string | null
166
+ title: string
167
+ fileName: string | null
168
+ mimeType: string | null
169
+ byteSize: number | null
170
+ status: 'scanning' | 'pending' | 'processing' | 'ready' | 'failed' | 'blocked'
171
+ searchable: boolean
172
+ error: string | null
173
+ errorKind: string | null
174
+ viewUrl: string | null
175
+ createdBy: string | null
176
+ addedByKey: {
177
+ name: string | null
178
+ } | null
179
+ source: {
180
+ id: string
181
+ name: string
182
+ url: string | null
183
+ } | null
184
+ createdAt: string
185
+ updatedAt: string
186
+ knowledgeBase: {
187
+ id: string
188
+ name: string
189
+ } | null
190
+ }
191
+ }
192
+
193
+ export type StatusesAnswer = {
194
+ documents: Array<{
195
+ id: string
196
+ orgId: string
197
+ knowledgeBaseId: string | null
198
+ title: string
199
+ fileName: string | null
200
+ mimeType: string | null
201
+ byteSize: number | null
202
+ status: 'scanning' | 'pending' | 'processing' | 'ready' | 'failed' | 'blocked'
203
+ searchable: boolean
204
+ error: string | null
205
+ errorKind: string | null
206
+ viewUrl: string | null
207
+ createdBy: string | null
208
+ addedByKey: {
209
+ name: string | null
210
+ } | null
211
+ source: {
212
+ id: string
213
+ name: string
214
+ url: string | null
215
+ } | null
216
+ createdAt: string
217
+ updatedAt: string
218
+ }>
219
+ missing: string[]
220
+ }
221
+
222
+ export type DocumentTextAnswer = {
223
+ id: string
224
+ title: string
225
+ knowledgeBase: {
226
+ id: string
227
+ name: string
228
+ }
229
+ pageCount: number | null
230
+ pages: Array<{
231
+ page: number | null
232
+ text: string
233
+ }>
234
+ nextPage: number | null
235
+ cut: boolean
236
+ }
237
+
238
+ export type SearchAnswer = {
239
+ results: Array<{
240
+ documentId: string
241
+ title: string
242
+ knowledgeBase: {
243
+ id: string
244
+ name: string
245
+ } | null
246
+ page: number | null
247
+ snippet: string
248
+ score: number
249
+ viewUrl: string
250
+ openUrl: string
251
+ fileName: string | null
252
+ mimeType: string | null
253
+ updatedAt: string
254
+ documentUpdatedAt: string | null
255
+ addedBy: string | null
256
+ addedByKey: {
257
+ name: string | null
258
+ } | null
259
+ text?: string
260
+ passageId?: string
261
+ }>
262
+ total: number
263
+ facets: {
264
+ knowledgeBases: Array<{
265
+ id: string
266
+ name: string
267
+ count: number
268
+ }>
269
+ types: Array<{
270
+ type: string
271
+ count: number
272
+ }>
273
+ addedBy: Array<{
274
+ userId: string
275
+ count: number
276
+ }>
277
+ updated: Array<{
278
+ range: string
279
+ count: number
280
+ }>
281
+ }
282
+ mode: 'hybrid' | 'text'
283
+ correctedQuery?: string
284
+ timing: {
285
+ totalMs: number
286
+ embedMs: number
287
+ engineMs: number
288
+ recheckMs: number
289
+ queryVectorCached: boolean
290
+ }
291
+ }
292
+
293
+ export type AgentBasesAnswer = {
294
+ bases: Array<{
295
+ id: string
296
+ name: string
297
+ layer: 'workspace' | 'organization'
298
+ files: number
299
+ picked: boolean
300
+ unavailable: 'not_shared' | 'other_workspace' | null
301
+ }>
302
+ canChange: boolean
303
+ customerFacing: {
304
+ setUpForCustomers: boolean
305
+ channels: string[]
306
+ publicChat: boolean
307
+ embeddedChat: boolean
308
+ apps: string[]
309
+ voice: boolean
310
+ } | null
311
+ }
312
+
313
+ /** A source as a file or an API call carries it, and as whoever manages its base reads it back. */
314
+ export type SourceSpec = {
315
+ kind: 'api_pull'
316
+ name: string
317
+ schedule: 'manual' | 'hourly' | 'every_6_hours' | 'daily'
318
+ keepRemovedDays: 1 | 7 | 30
319
+ config: {
320
+ listUrl: string
321
+ itemsAt: string
322
+ paging: {
323
+ kind: 'link_header'
324
+ } | {
325
+ kind: 'next_url'
326
+ at: string
327
+ } | {
328
+ kind: 'page'
329
+ param: string
330
+ first: number
331
+ } | {
332
+ kind: 'cursor'
333
+ param: string
334
+ at: string
335
+ }
336
+ changes: {
337
+ kind: 'updated_since'
338
+ param: string
339
+ format: 'iso' | 'unix'
340
+ } | {
341
+ kind: 'newest_first'
342
+ } | {
343
+ kind: 'full'
344
+ }
345
+ fields: {
346
+ id: string
347
+ title: string
348
+ updatedAt: string
349
+ link?: string
350
+ content: {
351
+ kind: 'inline'
352
+ at: string
353
+ format: 'html' | 'markdown' | 'text'
354
+ } | {
355
+ kind: 'file'
356
+ at: string
357
+ }
358
+ deletedWhen?: {
359
+ at: string
360
+ equals: string | boolean
361
+ }
362
+ }
363
+ requestsPerSecond?: number
364
+ }
365
+ auth: {
366
+ kind: 'none'
367
+ } | {
368
+ kind: 'bearer'
369
+ } | {
370
+ kind: 'header'
371
+ name: string
372
+ } | {
373
+ kind: 'basic'
374
+ username: string
375
+ } | {
376
+ kind: 'oauth_client_credentials'
377
+ tokenUrl: string
378
+ clientId: string
379
+ scope?: string
380
+ }
381
+ }
382
+
383
+ /** What adding a source, or testing one, sends: its shape, with the schedule and retention optional, and the credential its sign-in needs. */
384
+ export type SourceInput = {
385
+ kind: 'api_pull'
386
+ name: string
387
+ schedule?: 'manual' | 'hourly' | 'every_6_hours' | 'daily'
388
+ keepRemovedDays?: 1 | 7 | 30
389
+ config: {
390
+ listUrl: string
391
+ itemsAt: string
392
+ paging: {
393
+ kind: 'link_header'
394
+ } | {
395
+ kind: 'next_url'
396
+ at: string
397
+ } | {
398
+ kind: 'page'
399
+ param: string
400
+ first: number
401
+ } | {
402
+ kind: 'cursor'
403
+ param: string
404
+ at: string
405
+ }
406
+ changes: {
407
+ kind: 'updated_since'
408
+ param: string
409
+ format: 'iso' | 'unix'
410
+ } | {
411
+ kind: 'newest_first'
412
+ } | {
413
+ kind: 'full'
414
+ }
415
+ fields: {
416
+ id: string
417
+ title: string
418
+ updatedAt: string
419
+ link?: string
420
+ content: {
421
+ kind: 'inline'
422
+ at: string
423
+ format: 'html' | 'markdown' | 'text'
424
+ } | {
425
+ kind: 'file'
426
+ at: string
427
+ }
428
+ deletedWhen?: {
429
+ at: string
430
+ equals: string | boolean
431
+ }
432
+ }
433
+ requestsPerSecond?: number
434
+ }
435
+ auth: {
436
+ kind: 'none'
437
+ } | {
438
+ kind: 'bearer'
439
+ } | {
440
+ kind: 'header'
441
+ name: string
442
+ } | {
443
+ kind: 'basic'
444
+ username: string
445
+ } | {
446
+ kind: 'oauth_client_credentials'
447
+ tokenUrl: string
448
+ clientId: string
449
+ scope?: string
450
+ }
451
+ credential?: string
452
+ }
453
+
454
+ /** A change to a source: only the parts given change. */
455
+ export type SourcePatch = {
456
+ name?: string
457
+ schedule?: 'manual' | 'hourly' | 'every_6_hours' | 'daily'
458
+ keepRemovedDays?: 1 | 7 | 30
459
+ config?: {
460
+ listUrl: string
461
+ itemsAt: string
462
+ paging: {
463
+ kind: 'link_header'
464
+ } | {
465
+ kind: 'next_url'
466
+ at: string
467
+ } | {
468
+ kind: 'page'
469
+ param: string
470
+ first: number
471
+ } | {
472
+ kind: 'cursor'
473
+ param: string
474
+ at: string
475
+ }
476
+ changes: {
477
+ kind: 'updated_since'
478
+ param: string
479
+ format: 'iso' | 'unix'
480
+ } | {
481
+ kind: 'newest_first'
482
+ } | {
483
+ kind: 'full'
484
+ }
485
+ fields: {
486
+ id: string
487
+ title: string
488
+ updatedAt: string
489
+ link?: string
490
+ content: {
491
+ kind: 'inline'
492
+ at: string
493
+ format: 'html' | 'markdown' | 'text'
494
+ } | {
495
+ kind: 'file'
496
+ at: string
497
+ }
498
+ deletedWhen?: {
499
+ at: string
500
+ equals: string | boolean
501
+ }
502
+ }
503
+ requestsPerSecond?: number
504
+ }
505
+ auth?: {
506
+ kind: 'none'
507
+ } | {
508
+ kind: 'bearer'
509
+ } | {
510
+ kind: 'header'
511
+ name: string
512
+ } | {
513
+ kind: 'basic'
514
+ username: string
515
+ } | {
516
+ kind: 'oauth_client_credentials'
517
+ tokenUrl: string
518
+ clientId: string
519
+ scope?: string
520
+ }
521
+ credential?: string
522
+ }
523
+
524
+ /** Where a source's list is, how to page and follow changes, and which field of an item holds what. */
525
+ export type PullConfig = {
526
+ listUrl: string
527
+ itemsAt: string
528
+ paging: {
529
+ kind: 'link_header'
530
+ } | {
531
+ kind: 'next_url'
532
+ at: string
533
+ } | {
534
+ kind: 'page'
535
+ param: string
536
+ first: number
537
+ } | {
538
+ kind: 'cursor'
539
+ param: string
540
+ at: string
541
+ }
542
+ changes: {
543
+ kind: 'updated_since'
544
+ param: string
545
+ format: 'iso' | 'unix'
546
+ } | {
547
+ kind: 'newest_first'
548
+ } | {
549
+ kind: 'full'
550
+ }
551
+ fields: {
552
+ id: string
553
+ title: string
554
+ updatedAt: string
555
+ link?: string
556
+ content: {
557
+ kind: 'inline'
558
+ at: string
559
+ format: 'html' | 'markdown' | 'text'
560
+ } | {
561
+ kind: 'file'
562
+ at: string
563
+ }
564
+ deletedWhen?: {
565
+ at: string
566
+ equals: string | boolean
567
+ }
568
+ }
569
+ requestsPerSecond?: number
570
+ }
571
+
572
+ /** How the platform signs in to a source; the credential itself travels beside it and is never read back. */
573
+ export type SourceAuth = {
574
+ kind: 'none'
575
+ } | {
576
+ kind: 'bearer'
577
+ } | {
578
+ kind: 'header'
579
+ name: string
580
+ } | {
581
+ kind: 'basic'
582
+ username: string
583
+ } | {
584
+ kind: 'oauth_client_credentials'
585
+ tokenUrl: string
586
+ clientId: string
587
+ scope?: string
588
+ }