@simpleplatform/sdk 2.3.0 → 2.4.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/README.md +111 -0
- package/dist/ai.d.ts +138 -2
- package/dist/ai.js +88 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -308,6 +308,63 @@ yet; attach its handle through the workflow that owns the record.
|
|
|
308
308
|
|
|
309
309
|
## API Documentation
|
|
310
310
|
|
|
311
|
+
### Describing an Action
|
|
312
|
+
|
|
313
|
+
What an action is, and when to reach for it, is written where the code is — in
|
|
314
|
+
the JSDoc comment above the handler, the same comment that carries its
|
|
315
|
+
description:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
/**
|
|
319
|
+
* Close a duplicate lead and point it at the record that survives.
|
|
320
|
+
*
|
|
321
|
+
* The surviving lead keeps its activity; the duplicate is marked closed and
|
|
322
|
+
* linked to it, so a later report still reaches both records.
|
|
323
|
+
*
|
|
324
|
+
* @tool
|
|
325
|
+
* @shortdesc Close a duplicate lead, pointing it at the surviving record.
|
|
326
|
+
* @usewhen A lead is a duplicate of one already in the system.
|
|
327
|
+
* @usewhen Two leads share a contact and one should be retired.
|
|
328
|
+
*/
|
|
329
|
+
simple.Handle(async (request) => {
|
|
330
|
+
// ...
|
|
331
|
+
})
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
| Tag | Shape | What it says |
|
|
335
|
+
| --------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
336
|
+
| `@tool` | bare, no value | This action can be reached as a tool |
|
|
337
|
+
| `@shortdesc` | one line, up to 300 characters, written once | What this is, read in a listing of tools |
|
|
338
|
+
| `@usewhen` | one line, up to 100 characters, up to ten times | One occasion for reaching for this rather than something else |
|
|
339
|
+
| `@parallelsafe` | bare, no value, only alongside `@tool` | This tool changes no stored data and sends nothing, so it may run at the same time as the other parallel-safe calls next to it in a batch |
|
|
340
|
+
|
|
341
|
+
`@parallelsafe` is a claim you make about your own action; the platform does
|
|
342
|
+
not verify it. It changes only which calls of a batch may overlap — consecutive
|
|
343
|
+
parallel-safe calls may run at the same time, and every other call still runs
|
|
344
|
+
alone, in the order asked. It never changes whether a failed call is retried,
|
|
345
|
+
nor the platform's assumption that a failed call may have changed stored data.
|
|
346
|
+
The build refuses it without `@tool`, with a value, or written twice. Write it
|
|
347
|
+
only when the action is read-only:
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
/**
|
|
351
|
+
* Look up a lead's open activity.
|
|
352
|
+
*
|
|
353
|
+
* @tool
|
|
354
|
+
* @shortdesc Look up a lead's open activity.
|
|
355
|
+
* @usewhen A caller wants a lead's current open activity.
|
|
356
|
+
* @parallelsafe
|
|
357
|
+
*/
|
|
358
|
+
simple.Handle(async (request) => {
|
|
359
|
+
// reads only — no writes, no outbound calls
|
|
360
|
+
})
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The prose above the tags is the full description, and stays exactly as
|
|
364
|
+
written. Each tag is declared in `tsdoc.json` (`{"tagName": "@parallelsafe",
|
|
365
|
+
"syntaxKind": "modifier"}` alongside `@tool`, `@shortdesc` and `@usewhen`), so
|
|
366
|
+
an editor with TSDoc support recognizes it instead of flagging it as unknown.
|
|
367
|
+
|
|
311
368
|
### AI Module
|
|
312
369
|
|
|
313
370
|
The AI module provides powerful capabilities for working with unstructured data.
|
|
@@ -382,6 +439,60 @@ console.log(result.data.summary) // "Customer called regarding..."
|
|
|
382
439
|
console.log(result.data.participants) // ["Customer", "Support Agent"]
|
|
383
440
|
```
|
|
384
441
|
|
|
442
|
+
#### Transcribe PDF Pages
|
|
443
|
+
|
|
444
|
+
Read the pages of a PDF that have no usable text of their own — scans,
|
|
445
|
+
image-only exhibits — from their images. Each such page is transcribed once,
|
|
446
|
+
and it is the same transcription an `extract` or `summarize` that asks for text
|
|
447
|
+
is given in the page's place, so a quote on an image page can be checked
|
|
448
|
+
against the text the answer was built on. Pages with a readable text layer are
|
|
449
|
+
not returned.
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
import { transcribePages } from '@simpleplatform/sdk/ai'
|
|
453
|
+
|
|
454
|
+
const { data } = await transcribePages(
|
|
455
|
+
{ ...contract, first_page: 40, last_page: 52 },
|
|
456
|
+
{},
|
|
457
|
+
request.context
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
for (const page of data.pages) {
|
|
461
|
+
if ('error' in page) {
|
|
462
|
+
console.log(`page ${page.page} could not be read: ${page.error}`)
|
|
463
|
+
continue
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.log(page.page, page.text.includes(quote))
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Pages are numbered in the original document. Transcriptions are kept per
|
|
471
|
+
version of the file and page, so a page already read for an `extract` or
|
|
472
|
+
`summarize` that asked for text (`deliver_as: 'text'`) is not read again.
|
|
473
|
+
|
|
474
|
+
A page is not transcribed twice to check itself: that would be the same model
|
|
475
|
+
reading the same image again, doubling the cost of every scanned page without
|
|
476
|
+
adding independence. To check an answer independently, read the pages a second
|
|
477
|
+
way — as the document itself (`deliver_as: 'document'`) — and compare.
|
|
478
|
+
|
|
479
|
+
#### How Files Travelled
|
|
480
|
+
|
|
481
|
+
Every AI result says how each file it carried reached the model, in
|
|
482
|
+
`metadata.delivery`: `deliveredAs` (`'document'`, `'text'` or `'image'`), the
|
|
483
|
+
range it was cut to, the pages transcribed from their images, and — when text
|
|
484
|
+
was asked for and the document was sent instead — a `fallback` naming the pages
|
|
485
|
+
that could not be read and why.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
const result = await extract({ ...contract, deliver_as: 'text' }, { prompt, schema }, request.context)
|
|
489
|
+
|
|
490
|
+
for (const file of result.metadata.delivery ?? []) {
|
|
491
|
+
if (file.fallback)
|
|
492
|
+
console.log(`${file.filename} was read as a PDF: ${file.fallback.message}`)
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
385
496
|
### GraphQL Module
|
|
386
497
|
|
|
387
498
|
Execute type-safe database operations with GraphQL:
|
package/dist/ai.d.ts
CHANGED
|
@@ -88,8 +88,12 @@ export type JSONSchema = JSONSchemaArray | JSONSchemaBoolean | JSONSchemaNumber
|
|
|
88
88
|
* file,
|
|
89
89
|
* which only ever travels as text.
|
|
90
90
|
*
|
|
91
|
-
* A page
|
|
92
|
-
*
|
|
91
|
+
* A page with no usable text of its own — a scan, or text that reads as noise
|
|
92
|
+
* — is read from its image instead, and that text stands in its place, marked
|
|
93
|
+
* `[Transcribed from the page image.]` under its page label. Only if that
|
|
94
|
+
* reading fails are the pages asked for sent as a PDF, so an answer is never
|
|
95
|
+
* built on text with a hole in it. The answer's `metadata.delivery` says which
|
|
96
|
+
* happened.
|
|
93
97
|
*
|
|
94
98
|
* Pages asked for as text arrive numbered from 1, because by then they are a
|
|
95
99
|
* document of their own. A line above them says which pages of which document
|
|
@@ -203,6 +207,62 @@ export interface AITranscribeOptions extends Omit<AICommonOptions, 'prompt'> {
|
|
|
203
207
|
*/
|
|
204
208
|
summarize?: boolean;
|
|
205
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* A PDF whose pages are to be transcribed, and optionally which of them.
|
|
212
|
+
*
|
|
213
|
+
* `first_page` and `last_page` work as they do for any AI operation: counted
|
|
214
|
+
* from 1, both ends included, named together or not at all, and cut out first.
|
|
215
|
+
* `deliver_as` has no meaning here, because the pages are answered as text.
|
|
216
|
+
*/
|
|
217
|
+
export type AITranscribePagesInput = DocumentHandle & Omit<AIDocumentInput, 'deliver_as' | keyof DocumentHandle>;
|
|
218
|
+
/**
|
|
219
|
+
* Configuration options for `transcribePages`. There is no prompt and no
|
|
220
|
+
* reasoning switch: the instructions are the platform's, and every page is
|
|
221
|
+
* read at the platform's own accuracy-first effort.
|
|
222
|
+
*/
|
|
223
|
+
export interface AITranscribePagesOptions {
|
|
224
|
+
/**
|
|
225
|
+
* If true, runs the operation again rather than serving its kept answer.
|
|
226
|
+
* A page already read is still served from its kept read, which is tied to
|
|
227
|
+
* this version of the file and to the platform's instructions.
|
|
228
|
+
*/
|
|
229
|
+
regenerate?: boolean;
|
|
230
|
+
/** (Optional) The maximum time in milliseconds to wait for the operation. */
|
|
231
|
+
timeout?: number;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* One page's transcription, or why it has none. Pages are numbered in the
|
|
235
|
+
* original document.
|
|
236
|
+
*/
|
|
237
|
+
export type AIPageTranscription = {
|
|
238
|
+
/** Why the page could not be read. No partial text is given. */
|
|
239
|
+
error: string;
|
|
240
|
+
/** The page's number in the original document. */
|
|
241
|
+
page: number;
|
|
242
|
+
} | {
|
|
243
|
+
/** The page's number in the original document. */
|
|
244
|
+
page: number;
|
|
245
|
+
/**
|
|
246
|
+
* The page's text as read from its image: verbatim markdown, in reading
|
|
247
|
+
* order, tables as markdown tables, `[illegible]` where a word could not
|
|
248
|
+
* be read. An empty string is a read that found the page blank.
|
|
249
|
+
*/
|
|
250
|
+
text: string;
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* The response of `transcribePages`.
|
|
254
|
+
*/
|
|
255
|
+
export interface AITranscriptionResult {
|
|
256
|
+
/** Every page of the document, or of the range, that has no usable text layer. */
|
|
257
|
+
data: {
|
|
258
|
+
pages: AIPageTranscription[];
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* The operation's metadata. Its token counts are zero: each page read is
|
|
262
|
+
* charged where it was made, once, whichever operation asked for it.
|
|
263
|
+
*/
|
|
264
|
+
metadata: AIExecutionResult['metadata'];
|
|
265
|
+
}
|
|
206
266
|
/**
|
|
207
267
|
* Options for searching faces within the Face Engine.
|
|
208
268
|
*/
|
|
@@ -218,6 +278,34 @@ export interface AIFaceSearchOptions {
|
|
|
218
278
|
*/
|
|
219
279
|
similarityThreshold?: number;
|
|
220
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* How one file an AI operation carried reached the model. Page numbers are
|
|
283
|
+
* the original document's, whatever range was cut from it.
|
|
284
|
+
*/
|
|
285
|
+
export interface AIFileDelivery {
|
|
286
|
+
/** How the file travelled: as the document itself, as its text, or as an image. */
|
|
287
|
+
deliveredAs: 'document' | 'image' | 'text';
|
|
288
|
+
/**
|
|
289
|
+
* Present only when text was asked for and the document was sent instead:
|
|
290
|
+
* the pages whose images could not be read, and why.
|
|
291
|
+
*/
|
|
292
|
+
fallback?: {
|
|
293
|
+
/** What went wrong, page by page, in the platform's words. */
|
|
294
|
+
message: string;
|
|
295
|
+
/** The pages that could not be transcribed. */
|
|
296
|
+
pages: number[];
|
|
297
|
+
/** Why the document travelled instead of its text. */
|
|
298
|
+
reason: 'transcription_failed';
|
|
299
|
+
};
|
|
300
|
+
/** The file's name. */
|
|
301
|
+
filename: string;
|
|
302
|
+
/** The first page of the range the file was cut to, when one was named. */
|
|
303
|
+
firstPage?: number;
|
|
304
|
+
/** The last page of the range the file was cut to, when one was named. */
|
|
305
|
+
lastPage?: number;
|
|
306
|
+
/** The pages whose text was read from their images rather than their text layer. */
|
|
307
|
+
transcribedPages: number[];
|
|
308
|
+
}
|
|
221
309
|
/**
|
|
222
310
|
* The response structure from a successful AI `extract` or `summarize` operation.
|
|
223
311
|
*/
|
|
@@ -232,6 +320,12 @@ export interface AIExecutionResult {
|
|
|
232
320
|
* providing context to the user.
|
|
233
321
|
*/
|
|
234
322
|
metadata: {
|
|
323
|
+
/**
|
|
324
|
+
* How each file the operation carried reached the model, in input order.
|
|
325
|
+
* Absent when the operation carried no file, and on an answer kept from
|
|
326
|
+
* before the platform reported it.
|
|
327
|
+
*/
|
|
328
|
+
delivery?: AIFileDelivery[];
|
|
235
329
|
/** The number of tokens in the input prompt. */
|
|
236
330
|
inputTokens: number;
|
|
237
331
|
/** The number of tokens in the generated output. */
|
|
@@ -301,6 +395,48 @@ export declare function summarize(input: AIDocumentInput | DocumentHandle | obje
|
|
|
301
395
|
* // Returns: { language: "en", summary: "...", participants: ["Participant 1", "Participant 2"] }
|
|
302
396
|
*/
|
|
303
397
|
export declare function transcribe(input: DocumentHandle, options: AITranscribeOptions, context: Context): Promise<AIExecutionResult>;
|
|
398
|
+
/**
|
|
399
|
+
* Transcribes the pages of a PDF that have no usable text of their own — scans,
|
|
400
|
+
* image-only exhibits, text that reads as noise — from their images.
|
|
401
|
+
*
|
|
402
|
+
* Every such page is read once, and it is the same transcription an `extract`
|
|
403
|
+
* or `summarize` that asked for text is given in the page's place, so a caller
|
|
404
|
+
* can check that a quote on an image page is really there by looking for it in
|
|
405
|
+
* the text the answer was built on. Pages the platform reads as text are
|
|
406
|
+
* neither read nor returned.
|
|
407
|
+
*
|
|
408
|
+
* A page is not transcribed a second time to check the first: that would be
|
|
409
|
+
* the same model reading the same image again, doubling the cost of every
|
|
410
|
+
* scanned page without being independent of the first read. An independent
|
|
411
|
+
* check reads the pages a second way — as the document itself
|
|
412
|
+
* (`deliver_as: 'document'`) — and compares the answers.
|
|
413
|
+
*
|
|
414
|
+
* Transcriptions are kept per version of the file and page, and shared with
|
|
415
|
+
* every other AI operation: a page already read for an `extract` or
|
|
416
|
+
* `summarize` that asked for text is not read again.
|
|
417
|
+
*
|
|
418
|
+
* @param document The PDF, as a DocumentHandle. Add `first_page` and
|
|
419
|
+
* `last_page` to transcribe only those pages; they are counted from 1 and
|
|
420
|
+
* the answer numbers pages in the original document.
|
|
421
|
+
* @param options Optional `regenerate` and `timeout`.
|
|
422
|
+
* @param context The execution context provided by the host.
|
|
423
|
+
* @returns A promise that resolves to each page's transcription, or why it has none.
|
|
424
|
+
* @throws Will throw an error if the operation fails or the input is not a PDF.
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* const { data } = await transcribePages(
|
|
428
|
+
* { ...contract, first_page: 40, last_page: 52 },
|
|
429
|
+
* {},
|
|
430
|
+
* context,
|
|
431
|
+
* )
|
|
432
|
+
*
|
|
433
|
+
* for (const page of data.pages) {
|
|
434
|
+
* if ('error' in page)
|
|
435
|
+
* continue
|
|
436
|
+
* const quoted = page.text.includes(quote)
|
|
437
|
+
* }
|
|
438
|
+
*/
|
|
439
|
+
export declare function transcribePages(document: AITranscribePagesInput, options: AITranscribePagesOptions, context: Context): Promise<AITranscriptionResult>;
|
|
304
440
|
/**
|
|
305
441
|
* Enrolls a face for a subject. The face is associated with the given `subjectId`
|
|
306
442
|
* within the tenant's secure collection.
|
package/dist/ai.js
CHANGED
|
@@ -85,6 +85,7 @@ async function _executeAIOperation(operation, input, options, context) {
|
|
|
85
85
|
return {
|
|
86
86
|
data,
|
|
87
87
|
metadata: {
|
|
88
|
+
...(Array.isArray(metadata.delivery) && { delivery: metadata.delivery.map(_fileDelivery) }),
|
|
88
89
|
inputTokens: metadata.input_tokens,
|
|
89
90
|
outputTokens: metadata.output_tokens,
|
|
90
91
|
reasoning: metadata.reasoning,
|
|
@@ -92,6 +93,38 @@ async function _executeAIOperation(operation, input, options, context) {
|
|
|
92
93
|
},
|
|
93
94
|
};
|
|
94
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* One file's delivery as the platform reports it, in the SDK's own casing.
|
|
98
|
+
* A key the platform sends as `null` is left out rather than carried as one.
|
|
99
|
+
*
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
function _fileDelivery(file) {
|
|
103
|
+
return {
|
|
104
|
+
deliveredAs: file.delivered_as,
|
|
105
|
+
...(file.fallback && { fallback: file.fallback }),
|
|
106
|
+
filename: file.filename,
|
|
107
|
+
...(typeof file.first_page === 'number' && { firstPage: file.first_page }),
|
|
108
|
+
...(typeof file.last_page === 'number' && { lastPage: file.last_page }),
|
|
109
|
+
transcribedPages: Array.isArray(file.transcribed_pages) ? file.transcribed_pages : [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One page's transcription as the platform reports it: its text or its error,
|
|
114
|
+
* never both. An error wins, so no partial text is handed on, and a page that
|
|
115
|
+
* carries no text is reported as unread rather than as text that is not there.
|
|
116
|
+
*
|
|
117
|
+
* @internal
|
|
118
|
+
*/
|
|
119
|
+
function _pageTranscription(page) {
|
|
120
|
+
if (typeof page.text === 'string' && typeof page.error !== 'string') {
|
|
121
|
+
return { page: page.page, text: page.text };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
error: typeof page.error === 'string' ? page.error : 'no transcription was returned for this page',
|
|
125
|
+
page: page.page,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
95
128
|
// ============================================================================
|
|
96
129
|
// Public SDK Functions
|
|
97
130
|
// ============================================================================
|
|
@@ -284,6 +317,61 @@ export async function transcribe(input, options, context) {
|
|
|
284
317
|
schema,
|
|
285
318
|
}, context);
|
|
286
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Transcribes the pages of a PDF that have no usable text of their own — scans,
|
|
322
|
+
* image-only exhibits, text that reads as noise — from their images.
|
|
323
|
+
*
|
|
324
|
+
* Every such page is read once, and it is the same transcription an `extract`
|
|
325
|
+
* or `summarize` that asked for text is given in the page's place, so a caller
|
|
326
|
+
* can check that a quote on an image page is really there by looking for it in
|
|
327
|
+
* the text the answer was built on. Pages the platform reads as text are
|
|
328
|
+
* neither read nor returned.
|
|
329
|
+
*
|
|
330
|
+
* A page is not transcribed a second time to check the first: that would be
|
|
331
|
+
* the same model reading the same image again, doubling the cost of every
|
|
332
|
+
* scanned page without being independent of the first read. An independent
|
|
333
|
+
* check reads the pages a second way — as the document itself
|
|
334
|
+
* (`deliver_as: 'document'`) — and compares the answers.
|
|
335
|
+
*
|
|
336
|
+
* Transcriptions are kept per version of the file and page, and shared with
|
|
337
|
+
* every other AI operation: a page already read for an `extract` or
|
|
338
|
+
* `summarize` that asked for text is not read again.
|
|
339
|
+
*
|
|
340
|
+
* @param document The PDF, as a DocumentHandle. Add `first_page` and
|
|
341
|
+
* `last_page` to transcribe only those pages; they are counted from 1 and
|
|
342
|
+
* the answer numbers pages in the original document.
|
|
343
|
+
* @param options Optional `regenerate` and `timeout`.
|
|
344
|
+
* @param context The execution context provided by the host.
|
|
345
|
+
* @returns A promise that resolves to each page's transcription, or why it has none.
|
|
346
|
+
* @throws Will throw an error if the operation fails or the input is not a PDF.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* const { data } = await transcribePages(
|
|
350
|
+
* { ...contract, first_page: 40, last_page: 52 },
|
|
351
|
+
* {},
|
|
352
|
+
* context,
|
|
353
|
+
* )
|
|
354
|
+
*
|
|
355
|
+
* for (const page of data.pages) {
|
|
356
|
+
* if ('error' in page)
|
|
357
|
+
* continue
|
|
358
|
+
* const quoted = page.text.includes(quote)
|
|
359
|
+
* }
|
|
360
|
+
*/
|
|
361
|
+
export async function transcribePages(document, options, context) {
|
|
362
|
+
if (!document || typeof document !== 'object' || !document.file_hash) {
|
|
363
|
+
throw new Error('The `document` parameter must be a valid DocumentHandle for `transcribePages`.');
|
|
364
|
+
}
|
|
365
|
+
const mimeType = (document.mime_type || '').split(';')[0].trim().toLowerCase();
|
|
366
|
+
if (mimeType !== 'application/pdf') {
|
|
367
|
+
throw new Error('`transcribePages` reads the pages of a PDF.');
|
|
368
|
+
}
|
|
369
|
+
const result = await _executeAIOperation('transcribe', document, options, context);
|
|
370
|
+
return {
|
|
371
|
+
data: { pages: Array.isArray(result.data?.pages) ? result.data.pages.map(_pageTranscription) : [] },
|
|
372
|
+
metadata: result.metadata,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
287
375
|
// ============================================================================
|
|
288
376
|
// Face Recognition API
|
|
289
377
|
// ============================================================================
|