@saplingai/sapling-js 1.0.39 → 1.0.52
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 +152 -1
- package/package.json +2 -2
- package/sapling/client.d.ts +405 -2
- package/sapling/client.js +1059 -4
package/sapling/client.js
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
3
3
|
|
|
4
|
+
// Value types an extract() field may declare; 'string' is the default.
|
|
5
|
+
const EXTRACT_FIELD_TYPES = ['string', 'number', 'integer', 'boolean', 'date', 'list'];
|
|
6
|
+
// Registers translate() accepts; the API rejects anything else with a 400.
|
|
7
|
+
const TRANSLATE_FORMALITIES = ['formal', 'informal'];
|
|
8
|
+
const SUMMARIZE_LENGTHS = ['short', 'medium', 'long'];
|
|
9
|
+
// Reading levels simplify() accepts; the API rejects anything else with a 400.
|
|
10
|
+
const SIMPLIFY_READING_LEVELS = ['plain', 'elementary', 'middle_school', 'high_school'];
|
|
11
|
+
|
|
12
|
+
// Shared by styleguide() and saveStyleguideRuleset(). Shape/type checks only —
|
|
13
|
+
// size limits (rule/description length) are server configuration and enforced
|
|
14
|
+
// there with a clear 400 (see classify()).
|
|
15
|
+
const validateStyleguideRules = (rules, method) => {
|
|
16
|
+
const isValidRule = (rule) => {
|
|
17
|
+
if (typeof rule === 'string') {
|
|
18
|
+
return rule.trim().length > 0;
|
|
19
|
+
}
|
|
20
|
+
if (rule == null || typeof rule !== 'object' || Array.isArray(rule)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (typeof rule.name !== 'string' || rule.name.trim().length === 0) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (rule.description !== undefined && typeof rule.description !== 'string') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
// Reject unknown keys so a typo like `{ name, descrption: '...' }` fails
|
|
30
|
+
// here rather than being forwarded to the API as a malformed rule (the
|
|
31
|
+
// classify()/extract() convention; the MCP object schema is `.strict()`).
|
|
32
|
+
const allowed = new Set(['name', 'description']);
|
|
33
|
+
if (Object.keys(rule).some((key) => !allowed.has(key))) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
!Array.isArray(rules)
|
|
41
|
+
|| rules.length < 1
|
|
42
|
+
|| rules.length > 20
|
|
43
|
+
// Array.from() materializes holes in sparse arrays as `undefined` (which
|
|
44
|
+
// isValidRule rejects); a bare `.every()` would skip them and let a hole
|
|
45
|
+
// through to be serialized as `null`.
|
|
46
|
+
|| !Array.from(rules).every(isValidRule)
|
|
47
|
+
) {
|
|
48
|
+
throw new TypeError(`${method}: rules must be an array of 1 to 20 non-empty strings or `
|
|
49
|
+
+ '{name, description?} objects (string name, optional string description).');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const names = rules.map(
|
|
53
|
+
(rule) => (typeof rule === 'string' ? rule : rule.name).trim().toLowerCase());
|
|
54
|
+
if (names.length !== new Set(names).size) {
|
|
55
|
+
throw new TypeError(`${method}: rule names must be unique (case-insensitive).`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
4
59
|
export class Client {
|
|
5
60
|
constructor(apiKey, options = {}) {
|
|
6
61
|
this.apiKey = apiKey;
|
|
@@ -13,7 +68,7 @@ export class Client {
|
|
|
13
68
|
}
|
|
14
69
|
|
|
15
70
|
axiosPromise(url, method, data) {
|
|
16
|
-
return new Promise((resolve) => {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
17
72
|
axios({
|
|
18
73
|
url,
|
|
19
74
|
method,
|
|
@@ -22,7 +77,8 @@ export class Client {
|
|
|
22
77
|
})
|
|
23
78
|
.then(function (response) {
|
|
24
79
|
resolve(response);
|
|
25
|
-
})
|
|
80
|
+
})
|
|
81
|
+
.catch(reject);
|
|
26
82
|
});
|
|
27
83
|
}
|
|
28
84
|
|
|
@@ -56,6 +112,11 @@ export class Client {
|
|
|
56
112
|
data.advanced_edits = options.advancedEdits;
|
|
57
113
|
}
|
|
58
114
|
|
|
115
|
+
// Omitted unless set; an explicit null is forwarded so the API rejects it.
|
|
116
|
+
if (options.includeErrorCategories !== undefined) {
|
|
117
|
+
data.include_error_categories = options.includeErrorCategories;
|
|
118
|
+
}
|
|
119
|
+
|
|
59
120
|
if (userDetails) {
|
|
60
121
|
const { userId, anonUser } = userDetails;
|
|
61
122
|
data.user_id = userId;
|
|
@@ -176,11 +237,304 @@ export class Client {
|
|
|
176
237
|
return this.axiosPromise(url, 'post', data);
|
|
177
238
|
}
|
|
178
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Structured data extraction: pull caller-named fields out of unstructured text.
|
|
242
|
+
*
|
|
243
|
+
* `classify()` answers "what kind of text is this"; `extract()` answers "what's in it".
|
|
244
|
+
* Pass 1–20 fields — plain names, or `{name, type, description, required}` objects to
|
|
245
|
+
* declare the value type and sharpen ambiguous fields. Returns `data` (a
|
|
246
|
+
* `{field name: value}` map with every requested field present and `null` where the text
|
|
247
|
+
* didn't state it), `fields` (one entry per requested field, in request order, each with
|
|
248
|
+
* the verbatim `evidence` span the value came from) and `missing` (the names the text
|
|
249
|
+
* didn't yield).
|
|
250
|
+
*
|
|
251
|
+
* Values are coerced to the declared type server-side and must be grounded in a quote
|
|
252
|
+
* from the text, so this is extraction, not inference: a value that isn't stated is
|
|
253
|
+
* reported missing rather than guessed. Finding nothing is a normal 200.
|
|
254
|
+
*
|
|
255
|
+
* @param {string|string[]} text - The document to extract from, plain text or HTML (the
|
|
256
|
+
* API caps it at 10,000 characters). Pass an array of 1–10 strings to extract from a
|
|
257
|
+
* batch in one request (sent as the API's `texts` parameter; combined length capped at
|
|
258
|
+
* 10,000 chars) — the response is then `{results: [...]}` with one
|
|
259
|
+
* single-response-shaped entry per input, in order.
|
|
260
|
+
* @param {Array<string|{name: string, type?: string, description?: string, required?: boolean}>} fields -
|
|
261
|
+
* The fields to pull out (1–20): field names, or objects. `type` is one of 'string'
|
|
262
|
+
* (default), 'number', 'integer', 'boolean', 'date' (returned as YYYY-MM-DD) or 'list'.
|
|
263
|
+
* Names must be unique case-insensitively; the API caps names at 50 characters and
|
|
264
|
+
* descriptions at 200.
|
|
265
|
+
* @param {Object} [options]
|
|
266
|
+
* @param {string} [options.context] - Optional guidance (the API caps it at 500
|
|
267
|
+
* characters): what the document is and how to read ambiguous fields, e.g.
|
|
268
|
+
* 'A vendor invoice'.
|
|
269
|
+
* @returns {Promise<AxiosResponse>}
|
|
270
|
+
*/
|
|
271
|
+
extract(text, fields, options = {}) {
|
|
272
|
+
const url = `${this.urlEndpoint}extract`;
|
|
273
|
+
|
|
274
|
+
// Shape/type checks only. Size limits (text length, field name/description/
|
|
275
|
+
// context length) are server configuration and enforced there with a clear
|
|
276
|
+
// 400 — mirroring them here would drift the day they change (and JS
|
|
277
|
+
// `length` counts UTF-16 units, not the characters the API counts).
|
|
278
|
+
// An array of texts is the batch form (sent as `texts`, one result per
|
|
279
|
+
// item); the 1–10 item bound is structural like the field count below.
|
|
280
|
+
const isBatch = Array.isArray(text);
|
|
281
|
+
if (isBatch) {
|
|
282
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
283
|
+
// undefined and rejected, rather than skipped.
|
|
284
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
285
|
+
for (const item of text) {
|
|
286
|
+
ok = ok && typeof item === 'string' && item.trim().length > 0;
|
|
287
|
+
}
|
|
288
|
+
if (!ok) {
|
|
289
|
+
throw new TypeError('extract: texts must be an array of 1 to 10 non-empty strings.');
|
|
290
|
+
}
|
|
291
|
+
} else if (typeof text !== 'string' || text.trim().length === 0) {
|
|
292
|
+
throw new TypeError('extract: text must be a non-empty string.');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const isValidField = (field) => {
|
|
296
|
+
if (typeof field === 'string') {
|
|
297
|
+
return field.trim().length > 0;
|
|
298
|
+
}
|
|
299
|
+
if (field == null || typeof field !== 'object' || Array.isArray(field)) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
if (typeof field.name !== 'string' || field.name.trim().length === 0) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
if (field.type !== undefined && !EXTRACT_FIELD_TYPES.includes(field.type)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (field.description !== undefined && typeof field.description !== 'string') {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
if (field.required !== undefined && typeof field.required !== 'boolean') {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
// Reject unknown keys so a typo like `{ name, typ: 'number' }` fails here
|
|
315
|
+
// rather than being forwarded to the API as a malformed field (the MCP
|
|
316
|
+
// schema is `.strict()` for the same reason).
|
|
317
|
+
const allowed = new Set(['name', 'type', 'description', 'required']);
|
|
318
|
+
if (Object.keys(field).some((key) => !allowed.has(key))) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
if (
|
|
325
|
+
!Array.isArray(fields)
|
|
326
|
+
|| fields.length < 1
|
|
327
|
+
|| fields.length > 20
|
|
328
|
+
// Array.from() materializes holes in sparse arrays as `undefined` (which
|
|
329
|
+
// isValidField rejects); a bare `.every()` would skip them and let a hole
|
|
330
|
+
// through to be serialized as `null`.
|
|
331
|
+
|| !Array.from(fields).every(isValidField)
|
|
332
|
+
) {
|
|
333
|
+
throw new TypeError('extract: fields must be an array of 1 to 20 non-empty strings or '
|
|
334
|
+
+ '{name, type?, description?, required?} objects (string name, type one of '
|
|
335
|
+
+ `${EXTRACT_FIELD_TYPES.join(', ')}, optional string description, optional boolean required).`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const names = fields.map((field) => (typeof field === 'string' ? field : field.name).toLowerCase());
|
|
339
|
+
if (names.length !== new Set(names).size) {
|
|
340
|
+
throw new TypeError('extract: field names must be unique (case-insensitive).');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let data = {
|
|
344
|
+
...(isBatch ? { texts: text } : { text }),
|
|
345
|
+
fields,
|
|
346
|
+
key: this.apiKey,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
350
|
+
// type rather than silently treated as "omitted", matching how a null
|
|
351
|
+
// `description` is rejected above.
|
|
352
|
+
if (options?.context !== undefined) {
|
|
353
|
+
if (typeof options.context !== 'string') {
|
|
354
|
+
throw new TypeError('extract: context must be a string.');
|
|
355
|
+
}
|
|
356
|
+
data.context = options.context;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return this.axiosPromise(url, 'post', data);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Translate text into a target language with an LLM.
|
|
364
|
+
*
|
|
365
|
+
* Line breaks, markup and placeholders are preserved in place, and the response
|
|
366
|
+
* reports the language the text was actually written in (detected — never assumed
|
|
367
|
+
* from the hint): `translation`, `source_lang`/`source_lang_name` (`'und'`/`null`
|
|
368
|
+
* when undetermined) and the normalized `target_lang`/`target_lang_name`.
|
|
369
|
+
*
|
|
370
|
+
* @param {string|string[]} text - The text to translate (the API caps it at
|
|
371
|
+
* 5,000 characters). Markup and whitespace are preserved, not stripped.
|
|
372
|
+
* Pass an array of 1-10 strings to translate a batch in one request (sent
|
|
373
|
+
* as the API's `texts` parameter; combined length up to 5,000 characters,
|
|
374
|
+
* same target/source/formality applied to every item). A batch response is
|
|
375
|
+
* `{results: [...]}` with one single-response-shaped entry per input, in
|
|
376
|
+
* order.
|
|
377
|
+
* @param {string} targetLang - Language to translate into: an ISO 639 code
|
|
378
|
+
* ('fr', 'zh-TW') or an English language name ('French'). Region variants
|
|
379
|
+
* zh-CN/zh-TW/zh-HK, pt-BR/pt-PT, en-GB/en-US, fr-CA and es-419 are supported
|
|
380
|
+
* as targets.
|
|
381
|
+
* @param {Object} [options]
|
|
382
|
+
* @param {string} [options.source_lang] - Optional hint for the source language,
|
|
383
|
+
* same forms as targetLang. The text itself wins if they disagree.
|
|
384
|
+
* @param {string} [options.formality] - Optional register: 'formal'
|
|
385
|
+
* (vous/Sie/usted) or 'informal' (tu/du/tú). Omit for the model default.
|
|
386
|
+
* @returns {Promise<AxiosResponse>}
|
|
387
|
+
*/
|
|
388
|
+
translate(text, targetLang, options = {}) {
|
|
389
|
+
const url = `${this.urlEndpoint}translate`;
|
|
390
|
+
|
|
391
|
+
// Shape/type checks only; the language vocabulary and length caps are
|
|
392
|
+
// server configuration enforced there with a clear 400 (mirroring the
|
|
393
|
+
// 176-language table here would drift the day it changes).
|
|
394
|
+
// An array of texts is the batch form (SAP-398, sent as `texts`, one
|
|
395
|
+
// result per item); the 1–10 item bound is structural like safety's.
|
|
396
|
+
const isBatch = Array.isArray(text);
|
|
397
|
+
if (isBatch) {
|
|
398
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
399
|
+
// undefined and rejected, rather than skipped.
|
|
400
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
401
|
+
if (ok) {
|
|
402
|
+
for (const item of text) {
|
|
403
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
404
|
+
ok = false;
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (!ok) {
|
|
410
|
+
throw new TypeError('translate: texts must be an array of 1 to 10 non-empty strings.');
|
|
411
|
+
}
|
|
412
|
+
} else if (typeof text !== 'string' || text.trim().length === 0) {
|
|
413
|
+
throw new TypeError('translate: text must be a non-empty string.');
|
|
414
|
+
}
|
|
415
|
+
if (typeof targetLang !== 'string' || targetLang.trim().length === 0) {
|
|
416
|
+
throw new TypeError('translate: targetLang must be a non-empty string (an ISO 639 '
|
|
417
|
+
+ 'code like "fr" or an English language name like "French").');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let data = {
|
|
421
|
+
...(isBatch ? { texts: text } : { text }),
|
|
422
|
+
target_lang: targetLang,
|
|
423
|
+
key: this.apiKey,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
|
|
427
|
+
// bad type rather than silently treated as "omitted" (same as extract's
|
|
428
|
+
// context handling).
|
|
429
|
+
if (options?.source_lang !== undefined) {
|
|
430
|
+
if (typeof options.source_lang !== 'string' || options.source_lang.trim().length === 0) {
|
|
431
|
+
throw new TypeError('translate: source_lang must be a non-empty string.');
|
|
432
|
+
}
|
|
433
|
+
data.source_lang = options.source_lang;
|
|
434
|
+
}
|
|
435
|
+
if (options?.formality !== undefined) {
|
|
436
|
+
if (!TRANSLATE_FORMALITIES.includes(options.formality)) {
|
|
437
|
+
throw new TypeError('translate: formality must be one of '
|
|
438
|
+
+ `${TRANSLATE_FORMALITIES.join(', ')}.`);
|
|
439
|
+
}
|
|
440
|
+
data.formality = options.formality;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return this.axiosPromise(url, 'post', data);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Summarize a document, webpage or email thread into a prose summary plus
|
|
448
|
+
* key points.
|
|
449
|
+
*
|
|
450
|
+
* Returns `summary` (the prose summary), `key_points` (an array of strings),
|
|
451
|
+
* `length` (the option the summary was generated with) and `result`
|
|
452
|
+
* (mirrors `summary` — the endpoint's original response key, kept for
|
|
453
|
+
* backward compatibility).
|
|
454
|
+
*
|
|
455
|
+
* @param {string|string[]} text - The document to summarize, plain text or
|
|
456
|
+
* HTML (tags are stripped; the API caps it at 20,000 characters). Pass an
|
|
457
|
+
* array of 1-10 strings to summarize a batch in one request (sent as the
|
|
458
|
+
* API's `texts` parameter; combined length up to 20,000 characters, same
|
|
459
|
+
* length option applied to every item). A batch response is
|
|
460
|
+
* `{results: [...]}` with one single-response-shaped entry per input, in
|
|
461
|
+
* order.
|
|
462
|
+
* @param {Object} [options]
|
|
463
|
+
* @param {string} [options.length] - Optional summary length: 'short',
|
|
464
|
+
* 'medium' (the default) or 'long'.
|
|
465
|
+
* @returns {Promise<AxiosResponse>}
|
|
466
|
+
*/
|
|
467
|
+
summarize(text, options = {}) {
|
|
468
|
+
const url = `${this.urlEndpoint}summarize`;
|
|
469
|
+
|
|
470
|
+
// An array of texts is the batch form (SAP-399, sent as `texts`, one
|
|
471
|
+
// result per item); the 1–10 item bound is structural like translate's.
|
|
472
|
+
const isBatch = Array.isArray(text);
|
|
473
|
+
if (isBatch) {
|
|
474
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
475
|
+
// undefined and rejected, rather than skipped.
|
|
476
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
477
|
+
if (ok) {
|
|
478
|
+
for (const item of text) {
|
|
479
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
480
|
+
ok = false;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (!ok) {
|
|
486
|
+
throw new TypeError('summarize: texts must be an array of 1 to 10 non-empty strings.');
|
|
487
|
+
}
|
|
488
|
+
} else if (typeof text !== 'string' || text.trim().length === 0) {
|
|
489
|
+
throw new TypeError('summarize: text must be a non-empty string.');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let data = {
|
|
493
|
+
...(isBatch ? { texts: text } : { text }),
|
|
494
|
+
key: this.apiKey,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
|
|
498
|
+
// bad type rather than silently treated as "omitted" (same as translate's
|
|
499
|
+
// formality handling).
|
|
500
|
+
if (options?.length !== undefined) {
|
|
501
|
+
if (!SUMMARIZE_LENGTHS.includes(options.length)) {
|
|
502
|
+
throw new TypeError('summarize: length must be one of '
|
|
503
|
+
+ `${SUMMARIZE_LENGTHS.join(', ')}.`);
|
|
504
|
+
}
|
|
505
|
+
data.length = options.length;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return this.axiosPromise(url, 'post', data);
|
|
509
|
+
}
|
|
510
|
+
|
|
179
511
|
langdetect(text, options = {}) {
|
|
180
512
|
const url = `${this.urlEndpoint}langdetect`;
|
|
181
513
|
|
|
514
|
+
// An array of texts is the batch form (SAP-397, sent as `texts`, one
|
|
515
|
+
// result per item); the 1–10 item bound is structural like classify's.
|
|
516
|
+
// The single (string) form is passed through unvalidated, as it always
|
|
517
|
+
// has been — the server answers a clear 400.
|
|
518
|
+
const isBatch = Array.isArray(text);
|
|
519
|
+
if (isBatch) {
|
|
520
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
521
|
+
// undefined and rejected, rather than skipped.
|
|
522
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
523
|
+
if (ok) {
|
|
524
|
+
for (const item of text) {
|
|
525
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
526
|
+
ok = false;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (!ok) {
|
|
532
|
+
throw new TypeError('langdetect: texts must be an array of 1 to 10 non-empty strings.');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
182
536
|
let data = {
|
|
183
|
-
text,
|
|
537
|
+
...(isBatch ? { texts: text } : { text }),
|
|
184
538
|
key: this.apiKey,
|
|
185
539
|
}
|
|
186
540
|
|
|
@@ -194,11 +548,318 @@ export class Client {
|
|
|
194
548
|
return this.axiosPromise(url, 'post', data);
|
|
195
549
|
}
|
|
196
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Zero-shot text classification: assign one (or several) of your own labels to a text.
|
|
553
|
+
*
|
|
554
|
+
* No training data needed — pass 2–20 candidate labels (plain names, or
|
|
555
|
+
* `{name, description}` objects; a short description per label sharpens borderline
|
|
556
|
+
* decisions). Returns `label` (best match, always one of your labels), `labels`
|
|
557
|
+
* (single-label: `[label]`; multi-label: every label with `score >= threshold`, possibly
|
|
558
|
+
* empty), `scores` (one `{label, score}` per input label, descending), a one-sentence
|
|
559
|
+
* `rationale` and the echoed `multi_label` mode.
|
|
560
|
+
*
|
|
561
|
+
* @param {string|string[]} text - Text to classify, plain text or HTML (the API caps it
|
|
562
|
+
* at 10,000 chars). Pass an array of 1–10 strings to classify a batch in one request
|
|
563
|
+
* (sent as the API's `texts` parameter; combined length capped at 10,000 chars) — the
|
|
564
|
+
* response is then `{results: [...]}` with one single-response-shaped entry per input,
|
|
565
|
+
* in order.
|
|
566
|
+
* @param {Array<string|{name: string, description?: string}>} labels - Candidate labels
|
|
567
|
+
* (2–20). Names unique case-insensitively; the API caps names at 50 and descriptions at 200 chars.
|
|
568
|
+
* @param {Object} [options]
|
|
569
|
+
* @param {boolean} [options.multiLabel] - true: zero, one or several labels may apply
|
|
570
|
+
* (independent 0–1 scores). Defaults to false: exactly one label (scores sum to ~1).
|
|
571
|
+
* @param {number} [options.threshold] - Multi-label cut-off on the per-label score (0–1,
|
|
572
|
+
* default 0.5). Ignored in single-label mode.
|
|
573
|
+
* @param {string} [options.context] - Optional guidance (max 500 chars): what the texts
|
|
574
|
+
* are and how to decide borderline cases, e.g. 'Support tickets for a SaaS billing product'.
|
|
575
|
+
* @returns {Promise<AxiosResponse>}
|
|
576
|
+
*/
|
|
577
|
+
classify(text, labels, options = {}) {
|
|
578
|
+
const url = `${this.urlEndpoint}classify`;
|
|
579
|
+
|
|
580
|
+
// Shape/type checks only. Size limits (text length, label/description/
|
|
581
|
+
// context length) are server configuration and enforced there with a
|
|
582
|
+
// clear 400 — mirroring them here would drift the day they change (and
|
|
583
|
+
// JS `length` counts UTF-16 units, not the characters the API counts).
|
|
584
|
+
// An array of texts is the batch form (sent as `texts`, one result per
|
|
585
|
+
// item); the 1–10 item bound is structural like the label count below.
|
|
586
|
+
const isBatch = Array.isArray(text);
|
|
587
|
+
if (isBatch) {
|
|
588
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
589
|
+
// undefined and rejected, rather than skipped.
|
|
590
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
591
|
+
for (const item of text) {
|
|
592
|
+
ok = ok && typeof item === 'string' && item.trim().length > 0;
|
|
593
|
+
}
|
|
594
|
+
if (!ok) {
|
|
595
|
+
throw new TypeError('classify: texts must be an array of 1 to 10 non-empty strings.');
|
|
596
|
+
}
|
|
597
|
+
} else if (typeof text !== 'string' || text.trim().length === 0) {
|
|
598
|
+
throw new TypeError('classify: text must be a non-empty string.');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const isValidLabel = (label) => {
|
|
602
|
+
if (typeof label === 'string') {
|
|
603
|
+
return label.trim().length > 0;
|
|
604
|
+
}
|
|
605
|
+
if (label == null || typeof label !== 'object' || Array.isArray(label)) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
if (typeof label.name !== 'string' || label.name.trim().length === 0) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
if (label.description !== undefined && typeof label.description !== 'string') {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
// Reject unknown keys so a typo like `{ name, descrption: '...' }` fails
|
|
615
|
+
// here rather than being forwarded to the API as a malformed label (the
|
|
616
|
+
// MCP object schema is `.strict()` and `extract()` does the same).
|
|
617
|
+
const allowed = new Set(['name', 'description']);
|
|
618
|
+
if (Object.keys(label).some((key) => !allowed.has(key))) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
return true;
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
if (
|
|
625
|
+
!Array.isArray(labels)
|
|
626
|
+
|| labels.length < 2
|
|
627
|
+
|| labels.length > 20
|
|
628
|
+
// Array.from() materializes holes in sparse arrays as `undefined` (which
|
|
629
|
+
// isValidLabel rejects); a bare `.every()` would skip them and let a hole
|
|
630
|
+
// through to be serialized as `null`.
|
|
631
|
+
|| !Array.from(labels).every(isValidLabel)
|
|
632
|
+
) {
|
|
633
|
+
throw new TypeError('classify: labels must be an array of 2 to 20 non-empty strings or '
|
|
634
|
+
+ '{name, description?} objects (string name, optional string description).');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const names = labels.map((label) => (typeof label === 'string' ? label : label.name).toLowerCase());
|
|
638
|
+
if (names.length !== new Set(names).size) {
|
|
639
|
+
throw new TypeError('classify: label names must be unique (case-insensitive).');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let data = {
|
|
643
|
+
...(isBatch ? { texts: text } : { text }),
|
|
644
|
+
labels,
|
|
645
|
+
key: this.apiKey,
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
649
|
+
// type rather than silently treated as "omitted", matching how a null
|
|
650
|
+
// `description` is rejected above.
|
|
651
|
+
if (options?.multiLabel !== undefined) {
|
|
652
|
+
if (typeof options.multiLabel !== 'boolean') {
|
|
653
|
+
throw new TypeError('classify: multiLabel must be a boolean.');
|
|
654
|
+
}
|
|
655
|
+
data.multi_label = options.multiLabel;
|
|
656
|
+
}
|
|
657
|
+
if (options?.threshold !== undefined) {
|
|
658
|
+
if (typeof options.threshold !== 'number' || !Number.isFinite(options.threshold)
|
|
659
|
+
|| options.threshold < 0 || options.threshold > 1) {
|
|
660
|
+
throw new TypeError('classify: threshold must be a finite number between 0 and 1.');
|
|
661
|
+
}
|
|
662
|
+
data.threshold = options.threshold;
|
|
663
|
+
}
|
|
664
|
+
if (options?.context !== undefined) {
|
|
665
|
+
if (typeof options.context !== 'string') {
|
|
666
|
+
throw new TypeError('classify: context must be a string.');
|
|
667
|
+
}
|
|
668
|
+
data.context = options.context;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return this.axiosPromise(url, 'post', data);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* SEO analysis and metadata generation for a page or article body (plain text or HTML).
|
|
676
|
+
*
|
|
677
|
+
* Always returns deterministic `stats` (word/sentence/paragraph counts, reading time,
|
|
678
|
+
* Flesch readability), `keywords` (count/density/in_first_100_words for each target
|
|
679
|
+
* keyword) and `top_terms` (most frequent unigrams/bigrams). By default also returns
|
|
680
|
+
* LLM-generated `suggestions` (titles, meta_descriptions, slug — null when no ASCII slug
|
|
681
|
+
* can be derived — and keywords) — pass
|
|
682
|
+
* `suggestions: false` for the free stats-only mode.
|
|
683
|
+
*
|
|
684
|
+
* @param {string} text - Page/article body, plain text or HTML (max 20,000 chars).
|
|
685
|
+
* @param {Object} [options]
|
|
686
|
+
* @param {string[]} [options.keywords] - Target keyword phrases, most important first
|
|
687
|
+
* (max 10, each 1–100 chars). Measured in the text and steered into the suggestions.
|
|
688
|
+
* @param {boolean} [options.suggestions] - Generate LLM suggestions. Defaults to true;
|
|
689
|
+
* false returns stats/keywords/top_terms only (unbilled).
|
|
690
|
+
* @param {string} [options.lang] - ISO 639-1 language code of the text (region subtag
|
|
691
|
+
* allowed, e.g. 'pt-BR'; or 'auto'). Defaults to 'en'. Only selects the readability
|
|
692
|
+
* formulas: supported for en/de/es/fr/it/nl/pl/ru, otherwise flesch_reading_ease /
|
|
693
|
+
* flesch_kincaid_grade are null.
|
|
694
|
+
* @returns {Promise<AxiosResponse>}
|
|
695
|
+
*/
|
|
696
|
+
seo(text, options = {}) {
|
|
697
|
+
const url = `${this.urlEndpoint}seo`;
|
|
698
|
+
|
|
699
|
+
// Shape/type checks only. Size limits (text length, keyword count/length)
|
|
700
|
+
// are server configuration and enforced there with a clear 400 — mirroring
|
|
701
|
+
// them here would drift the day they change (and JS `length` counts UTF-16
|
|
702
|
+
// units, not the characters the API counts).
|
|
703
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
704
|
+
throw new TypeError('seo: text must be a non-empty string.');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let data = {
|
|
708
|
+
text,
|
|
709
|
+
key: this.apiKey,
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
713
|
+
// type rather than silently treated as "omitted", matching classify().
|
|
714
|
+
if (options?.keywords !== undefined) {
|
|
715
|
+
if (!Array.isArray(options.keywords)
|
|
716
|
+
|| !Array.from(options.keywords).every((k) => typeof k === 'string' && k.trim().length > 0)) {
|
|
717
|
+
throw new TypeError('seo: keywords must be an array of non-empty strings.');
|
|
718
|
+
}
|
|
719
|
+
data.keywords = options.keywords;
|
|
720
|
+
}
|
|
721
|
+
if (options?.suggestions !== undefined) {
|
|
722
|
+
if (typeof options.suggestions !== 'boolean') {
|
|
723
|
+
throw new TypeError('seo: suggestions must be a boolean.');
|
|
724
|
+
}
|
|
725
|
+
data.suggestions = options.suggestions;
|
|
726
|
+
}
|
|
727
|
+
if (options?.lang !== undefined) {
|
|
728
|
+
if (typeof options.lang !== 'string') {
|
|
729
|
+
throw new TypeError('seo: lang must be a string.');
|
|
730
|
+
}
|
|
731
|
+
data.lang = options.lang;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return this.axiosPromise(url, 'post', data);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
quality(text, options = {}) {
|
|
738
|
+
const url = `${this.urlEndpoint}quality`;
|
|
739
|
+
|
|
740
|
+
// Shape/type checks only; size limits are server configuration (see seo()).
|
|
741
|
+
// An array of texts is the batch form (SAP-400, sent as `texts`, one
|
|
742
|
+
// result per item); the 1–10 item bound is structural like classify's.
|
|
743
|
+
const isBatch = Array.isArray(text);
|
|
744
|
+
if (isBatch) {
|
|
745
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
746
|
+
// undefined and rejected, rather than skipped.
|
|
747
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
748
|
+
if (ok) {
|
|
749
|
+
for (const item of text) {
|
|
750
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
751
|
+
ok = false;
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (!ok) {
|
|
757
|
+
throw new TypeError('quality: texts must be an array of 1 to 10 non-empty strings.');
|
|
758
|
+
}
|
|
759
|
+
} else if (typeof text !== 'string' || text.trim().length === 0) {
|
|
760
|
+
throw new TypeError('quality: text must be a non-empty string.');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
let data = {
|
|
764
|
+
...(isBatch ? { texts: text } : { text }),
|
|
765
|
+
key: this.apiKey,
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
769
|
+
// type rather than silently treated as "omitted", matching classify()/seo().
|
|
770
|
+
if (options?.sentence_scores !== undefined) {
|
|
771
|
+
if (typeof options.sentence_scores !== 'boolean') {
|
|
772
|
+
throw new TypeError('quality: sentence_scores must be a boolean.');
|
|
773
|
+
}
|
|
774
|
+
data.sentence_scores = options.sentence_scores;
|
|
775
|
+
}
|
|
776
|
+
if (options?.rubric !== undefined) {
|
|
777
|
+
if (typeof options.rubric !== 'boolean') {
|
|
778
|
+
throw new TypeError('quality: rubric must be a boolean.');
|
|
779
|
+
}
|
|
780
|
+
data.rubric = options.rubric;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return this.axiosPromise(url, 'post', data);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
safety(text, options = {}) {
|
|
787
|
+
const url = `${this.urlEndpoint}safety`;
|
|
788
|
+
|
|
789
|
+
// Shape/type checks only; size limits are server configuration (see seo()).
|
|
790
|
+
// An array of texts is the batch form (SAP-397, sent as `texts`, one
|
|
791
|
+
// result per item); the 1–10 item bound is structural like classify's.
|
|
792
|
+
const isBatch = Array.isArray(text);
|
|
793
|
+
if (isBatch) {
|
|
794
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
795
|
+
// undefined and rejected, rather than skipped.
|
|
796
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
797
|
+
if (ok) {
|
|
798
|
+
for (const item of text) {
|
|
799
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
800
|
+
ok = false;
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (!ok) {
|
|
806
|
+
throw new TypeError('safety: texts must be an array of 1 to 10 non-empty strings.');
|
|
807
|
+
}
|
|
808
|
+
} else if (typeof text !== 'string' || text.trim().length === 0) {
|
|
809
|
+
throw new TypeError('safety: text must be a non-empty string.');
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
let data = {
|
|
813
|
+
...(isBatch ? { texts: text } : { text }),
|
|
814
|
+
key: this.apiKey,
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
818
|
+
// type rather than silently treated as "omitted", matching classify().
|
|
819
|
+
if (options?.threshold !== undefined) {
|
|
820
|
+
if (typeof options.threshold !== 'number' || !Number.isFinite(options.threshold)
|
|
821
|
+
|| options.threshold < 0 || options.threshold > 1) {
|
|
822
|
+
throw new TypeError('safety: threshold must be a finite number between 0 and 1.');
|
|
823
|
+
}
|
|
824
|
+
data.threshold = options.threshold;
|
|
825
|
+
}
|
|
826
|
+
if (options?.spans !== undefined) {
|
|
827
|
+
if (typeof options.spans !== 'boolean') {
|
|
828
|
+
throw new TypeError('safety: spans must be a boolean.');
|
|
829
|
+
}
|
|
830
|
+
data.spans = options.spans;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return this.axiosPromise(url, 'post', data);
|
|
834
|
+
}
|
|
835
|
+
|
|
197
836
|
aidetect(text, options = {}) {
|
|
198
837
|
const url = `${this.urlEndpoint}aidetect`;
|
|
199
838
|
|
|
839
|
+
// An array of texts is the batch form (SAP-403, sent as `texts`, one
|
|
840
|
+
// result per item); the 1–10 item bound is structural like classify's.
|
|
841
|
+
// The single form keeps its historical no-validation behavior (the
|
|
842
|
+
// server 400s clearly on a bad `text`).
|
|
843
|
+
const isBatch = Array.isArray(text);
|
|
844
|
+
if (isBatch) {
|
|
845
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
846
|
+
// undefined and rejected, rather than skipped.
|
|
847
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
848
|
+
if (ok) {
|
|
849
|
+
for (const item of text) {
|
|
850
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
851
|
+
ok = false;
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (!ok) {
|
|
857
|
+
throw new TypeError('aidetect: texts must be an array of 1 to 10 non-empty strings.');
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
200
861
|
let data = {
|
|
201
|
-
text,
|
|
862
|
+
...(isBatch ? { texts: text } : { text }),
|
|
202
863
|
key: this.apiKey,
|
|
203
864
|
}
|
|
204
865
|
|
|
@@ -208,4 +869,398 @@ export class Client {
|
|
|
208
869
|
|
|
209
870
|
return this.axiosPromise(url, 'post', data);
|
|
210
871
|
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Check a text for compliance with style rules you supply (house style,
|
|
875
|
+
* brand voice, editorial guidelines). Only the listed rules are enforced.
|
|
876
|
+
*
|
|
877
|
+
* Returns `violations` (one `{rule, text, start, end, note, suggestion}`
|
|
878
|
+
* entry per violation, most important first — `start`/`end` index the
|
|
879
|
+
* submitted text and are null when the passage cannot be located), `rules`
|
|
880
|
+
* (the checked rule names) and `compliant` (true when no violations were
|
|
881
|
+
* found). The text is checked exactly as submitted — markup is NOT
|
|
882
|
+
* stripped, so offsets always index the string you sent.
|
|
883
|
+
*
|
|
884
|
+
* @param {string|string[]} text - The text to check (the API caps it at
|
|
885
|
+
* 10,000 characters), or an array of 1–10 texts for the batch form (sent
|
|
886
|
+
* as the API's `texts` parameter; combined length up to 10,000
|
|
887
|
+
* characters, same rules applied to every item). A batch response is
|
|
888
|
+
* `{results: [...]}` with one single-response-shaped entry per input, in
|
|
889
|
+
* order, each item's violation offsets indexing that item's own text.
|
|
890
|
+
* @param {Array<string|{name: string, description?: string}>|string} rules
|
|
891
|
+
* - 1 to 20 style rules: a rule name (max 80 chars) or a
|
|
892
|
+
* `{name, description}` object whose optional description (max 400
|
|
893
|
+
* chars) carries the nuance the model follows. Names must be unique
|
|
894
|
+
* case-insensitively. OR a string: the name of a saved ruleset (see
|
|
895
|
+
* saveStyleguideRuleset()) to check against, matched case-insensitively
|
|
896
|
+
* within the API key's account.
|
|
897
|
+
* @returns {Promise<AxiosResponse>}
|
|
898
|
+
*/
|
|
899
|
+
styleguide(text, rules) {
|
|
900
|
+
const url = `${this.urlEndpoint}styleguide`;
|
|
901
|
+
|
|
902
|
+
// Shape/type checks only. Size limits (text/rule/description length) are
|
|
903
|
+
// server configuration and enforced there with a clear 400 (see
|
|
904
|
+
// classify()).
|
|
905
|
+
// An array of texts is the batch form (SAP-412, sent as `texts`, one
|
|
906
|
+
// result per item); the 1–10 item bound is structural like classify's.
|
|
907
|
+
const isBatch = Array.isArray(text);
|
|
908
|
+
if (isBatch) {
|
|
909
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
910
|
+
// undefined and rejected, rather than skipped.
|
|
911
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
912
|
+
if (ok) {
|
|
913
|
+
for (const item of text) {
|
|
914
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
915
|
+
ok = false;
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (!ok) {
|
|
921
|
+
throw new TypeError('styleguide: texts must be an array of 1 to 10 non-empty strings.');
|
|
922
|
+
}
|
|
923
|
+
} else if (typeof text !== 'string' || text.trim().length === 0) {
|
|
924
|
+
throw new TypeError('styleguide: text must be a non-empty string.');
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// A string names a saved ruleset (see saveStyleguideRuleset()) and is
|
|
928
|
+
// sent as the API's `ruleset` parameter in place of inline `rules`.
|
|
929
|
+
const isSavedRuleset = typeof rules === 'string';
|
|
930
|
+
if (isSavedRuleset) {
|
|
931
|
+
if (rules.trim().length === 0) {
|
|
932
|
+
throw new TypeError('styleguide: ruleset name must be a non-empty string.');
|
|
933
|
+
}
|
|
934
|
+
} else {
|
|
935
|
+
validateStyleguideRules(rules, 'styleguide');
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
let data = {
|
|
939
|
+
...(isBatch ? { texts: text } : { text }),
|
|
940
|
+
...(isSavedRuleset ? { ruleset: rules } : { rules }),
|
|
941
|
+
key: this.apiKey,
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return this.axiosPromise(url, 'post', data);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* List the saved style-guide rulesets of this API key's account (its team,
|
|
949
|
+
* for team-scoped keys — a team's rulesets are shared by every caller of
|
|
950
|
+
* its keys). Requires a per-account API key.
|
|
951
|
+
*
|
|
952
|
+
* Returns `rulesets`: `{name, rules, created_at, updated_at}` entries whose
|
|
953
|
+
* `rules` are the normalized `{name, description}` objects the check
|
|
954
|
+
* endpoint applies.
|
|
955
|
+
*
|
|
956
|
+
* @returns {Promise<AxiosResponse>}
|
|
957
|
+
*/
|
|
958
|
+
styleguideRulesets() {
|
|
959
|
+
const url = `${this.urlEndpoint}styleguide/rulesets`;
|
|
960
|
+
// Not axiosPromise: this is a GET and the key rides the query string.
|
|
961
|
+
return axios({
|
|
962
|
+
url,
|
|
963
|
+
method: 'get',
|
|
964
|
+
params: { key: this.apiKey },
|
|
965
|
+
timeout: this.timeout,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Create — or replace, when the name already exists (matched
|
|
971
|
+
* case-insensitively) — a saved style-guide ruleset, so later
|
|
972
|
+
* `styleguide(text, name)` calls check against it by name. Owned by the
|
|
973
|
+
* API key's account (its team, for team-scoped keys). Requires a
|
|
974
|
+
* per-account API key.
|
|
975
|
+
*
|
|
976
|
+
* Returns `ruleset` (the saved `{name, rules, created_at, updated_at}`)
|
|
977
|
+
* and `created` (false when an existing ruleset was replaced).
|
|
978
|
+
*
|
|
979
|
+
* @param {string} name - The ruleset name (the API caps it at 100
|
|
980
|
+
* characters; surrounding/duplicate whitespace is normalized).
|
|
981
|
+
* @param {Array<string|{name: string, description?: string}>} rules - The
|
|
982
|
+
* rules to save; same shape and bounds as styleguide()'s inline rules.
|
|
983
|
+
* @returns {Promise<AxiosResponse>}
|
|
984
|
+
*/
|
|
985
|
+
saveStyleguideRuleset(name, rules) {
|
|
986
|
+
const url = `${this.urlEndpoint}styleguide/rulesets`;
|
|
987
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
988
|
+
throw new TypeError('saveStyleguideRuleset: name must be a non-empty string.');
|
|
989
|
+
}
|
|
990
|
+
validateStyleguideRules(rules, 'saveStyleguideRuleset');
|
|
991
|
+
|
|
992
|
+
let data = {
|
|
993
|
+
name,
|
|
994
|
+
rules,
|
|
995
|
+
key: this.apiKey,
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return this.axiosPromise(url, 'post', data);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Delete a saved style-guide ruleset by name (matched case-insensitively;
|
|
1003
|
+
* 404 when no such ruleset exists). Requires a per-account API key.
|
|
1004
|
+
*
|
|
1005
|
+
* @param {string} name - The ruleset name to delete.
|
|
1006
|
+
* @returns {Promise<AxiosResponse>}
|
|
1007
|
+
*/
|
|
1008
|
+
deleteStyleguideRuleset(name) {
|
|
1009
|
+
const url = `${this.urlEndpoint}styleguide/rulesets`;
|
|
1010
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
1011
|
+
throw new TypeError('deleteStyleguideRuleset: name must be a non-empty string.');
|
|
1012
|
+
}
|
|
1013
|
+
// The name rides the query string: some HTTP stacks drop DELETE bodies,
|
|
1014
|
+
// and the API accepts the query form for exactly that reason.
|
|
1015
|
+
return axios({
|
|
1016
|
+
url,
|
|
1017
|
+
method: 'delete',
|
|
1018
|
+
params: { key: this.apiKey, name },
|
|
1019
|
+
timeout: this.timeout,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Rewrite a text in plain language at a target reading level, keeping all
|
|
1025
|
+
* of its content, structure and language — nothing is summarized or
|
|
1026
|
+
* dropped.
|
|
1027
|
+
*
|
|
1028
|
+
* Returns `simplified` (the rewrite, structure preserved), the applied
|
|
1029
|
+
* `reading_level`, the `lang` the readability scores were computed for
|
|
1030
|
+
* (detected when not supplied) and, when that language supports scoring, a
|
|
1031
|
+
* `readability` block: `{before: {grade, ease}, after: {grade, ease}}`
|
|
1032
|
+
* Flesch-Kincaid scores, so the improvement is verifiable.
|
|
1033
|
+
*
|
|
1034
|
+
* @param {string|string[]} text - The text to simplify (the API caps it at
|
|
1035
|
+
* 5,000 characters), or an array of 1–10 texts for the batch form (sent
|
|
1036
|
+
* as the API's `texts` parameter; combined length up to 5,000
|
|
1037
|
+
* characters, same options applied to every item). A batch response is
|
|
1038
|
+
* `{results: [...]}` with one single-response-shaped entry per input, in
|
|
1039
|
+
* order; with `lang` omitted, each item's language (and readability
|
|
1040
|
+
* scores) is detected on its own. Sent to the model as submitted —
|
|
1041
|
+
* markup and line structure are preserved, not stripped.
|
|
1042
|
+
* @param {Object} [options]
|
|
1043
|
+
* @param {string} [options.reading_level] - Target audience: 'plain'
|
|
1044
|
+
* (plain-language style for a general audience, the API default),
|
|
1045
|
+
* 'elementary' (~US grade 3-5), 'middle_school' (~grade 6-8) or
|
|
1046
|
+
* 'high_school' (~grade 9-10).
|
|
1047
|
+
* @param {string[]} [options.preserve_terms] - Up to 20 terms (each up to
|
|
1048
|
+
* 100 characters) the rewrite must keep exactly as written — product
|
|
1049
|
+
* names, defined legal terms. A rewrite that drops one is refused
|
|
1050
|
+
* server-side rather than returned.
|
|
1051
|
+
* @param {string} [options.lang] - ISO 639-1 code used to pick the
|
|
1052
|
+
* readability formula for the before/after scores. Omit for
|
|
1053
|
+
* auto-detection (falls back to 'en'). The rewrite itself always stays
|
|
1054
|
+
* in the text's own language.
|
|
1055
|
+
* @returns {Promise<AxiosResponse>}
|
|
1056
|
+
*/
|
|
1057
|
+
simplify(text, options = {}) {
|
|
1058
|
+
const url = `${this.urlEndpoint}simplify`;
|
|
1059
|
+
|
|
1060
|
+
// Shape/type checks only. Size limits (text/term length) are server
|
|
1061
|
+
// configuration and enforced there with a clear 400 (see classify()).
|
|
1062
|
+
// An array of texts is the batch form (SAP-412, sent as `texts`, one
|
|
1063
|
+
// result per item); the 1–10 item bound is structural like classify's.
|
|
1064
|
+
const isBatch = Array.isArray(text);
|
|
1065
|
+
if (isBatch) {
|
|
1066
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
1067
|
+
// undefined and rejected, rather than skipped.
|
|
1068
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
1069
|
+
if (ok) {
|
|
1070
|
+
for (const item of text) {
|
|
1071
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
1072
|
+
ok = false;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (!ok) {
|
|
1078
|
+
throw new TypeError('simplify: texts must be an array of 1 to 10 non-empty strings.');
|
|
1079
|
+
}
|
|
1080
|
+
} else if (typeof text !== 'string' || text.trim().length === 0) {
|
|
1081
|
+
throw new TypeError('simplify: text must be a non-empty string.');
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
let data = {
|
|
1085
|
+
...(isBatch ? { texts: text } : { text }),
|
|
1086
|
+
key: this.apiKey,
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
|
|
1090
|
+
// bad type rather than silently treated as "omitted" (same as translate's
|
|
1091
|
+
// option handling).
|
|
1092
|
+
if (options?.reading_level !== undefined) {
|
|
1093
|
+
if (!SIMPLIFY_READING_LEVELS.includes(options.reading_level)) {
|
|
1094
|
+
throw new TypeError('simplify: reading_level must be one of '
|
|
1095
|
+
+ `${SIMPLIFY_READING_LEVELS.join(', ')}.`);
|
|
1096
|
+
}
|
|
1097
|
+
data.reading_level = options.reading_level;
|
|
1098
|
+
}
|
|
1099
|
+
if (options?.preserve_terms !== undefined) {
|
|
1100
|
+
const terms = options.preserve_terms;
|
|
1101
|
+
// An empty list is a valid no-op (callers may build it dynamically); it
|
|
1102
|
+
// is simply omitted from the payload below rather than rejected.
|
|
1103
|
+
// for...of (not terms.every) so holes in a sparse array are visited as
|
|
1104
|
+
// undefined and rejected, rather than skipped.
|
|
1105
|
+
let ok = Array.isArray(terms) && terms.length <= 20;
|
|
1106
|
+
if (ok) {
|
|
1107
|
+
for (const term of terms) {
|
|
1108
|
+
if (typeof term !== 'string' || term.trim().length === 0) {
|
|
1109
|
+
ok = false;
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (!ok) {
|
|
1115
|
+
throw new TypeError('simplify: preserve_terms must be an array of up to 20 '
|
|
1116
|
+
+ 'non-empty strings.');
|
|
1117
|
+
}
|
|
1118
|
+
if (terms.length > 0) {
|
|
1119
|
+
data.preserve_terms = terms;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (options?.lang !== undefined) {
|
|
1123
|
+
if (typeof options.lang !== 'string' || options.lang.trim().length === 0) {
|
|
1124
|
+
throw new TypeError('simplify: lang must be a non-empty string.');
|
|
1125
|
+
}
|
|
1126
|
+
data.lang = options.lang;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return this.axiosPromise(url, 'post', data);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
pii(text, options = {}) {
|
|
1133
|
+
const url = `${this.urlEndpoint}pii`;
|
|
1134
|
+
|
|
1135
|
+
// Shape/type checks only; the set of valid PII types is server configuration
|
|
1136
|
+
// and enforced there with a clear 400 (see seo()).
|
|
1137
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
1138
|
+
throw new TypeError('pii: text must be a non-empty string.');
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
let data = {
|
|
1142
|
+
text,
|
|
1143
|
+
key: this.apiKey,
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
1147
|
+
// type rather than silently treated as "omitted", matching classify()/seo().
|
|
1148
|
+
if (options?.types !== undefined) {
|
|
1149
|
+
if (!Array.isArray(options.types)
|
|
1150
|
+
|| !Array.from(options.types).every((type) => typeof type === 'string' && type.trim().length > 0)) {
|
|
1151
|
+
throw new TypeError('pii: types must be an array of non-empty strings.');
|
|
1152
|
+
}
|
|
1153
|
+
data.types = options.types;
|
|
1154
|
+
}
|
|
1155
|
+
if (options?.redact !== undefined) {
|
|
1156
|
+
if (typeof options.redact !== 'boolean') {
|
|
1157
|
+
throw new TypeError('pii: redact must be a boolean.');
|
|
1158
|
+
}
|
|
1159
|
+
data.redact = options.redact;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return this.axiosPromise(url, 'post', data);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Named-entity recognition: people, organizations, locations, dates, times,
|
|
1167
|
+
* money amounts, percentages, quantities, products, and events, each with
|
|
1168
|
+
* character offsets into the exact text submitted (nothing is stripped or
|
|
1169
|
+
* normalized, so spans apply directly to your copy of the document).
|
|
1170
|
+
* Complements pii(): that finds checksum-validated identifiers, this finds
|
|
1171
|
+
* names and real-world references.
|
|
1172
|
+
*
|
|
1173
|
+
* @param {string|string[]} text - The text to analyze, or an array of 1–10
|
|
1174
|
+
* texts for the batch form (sent as the API's `texts` parameter; combined
|
|
1175
|
+
* length up to 10,000 characters, same types applied to every item). A
|
|
1176
|
+
* batch response is `{results: [...]}` with one single-response-shaped
|
|
1177
|
+
* entry per input, in order, each item's spans indexing that item's own
|
|
1178
|
+
* text.
|
|
1179
|
+
* @param {Object} [options]
|
|
1180
|
+
* @param {string[]} [options.types] - Entity types to recognize (subset of
|
|
1181
|
+
* the API's supported types). Default: all of them.
|
|
1182
|
+
*/
|
|
1183
|
+
ner(text, options = {}) {
|
|
1184
|
+
const url = `${this.urlEndpoint}ner`;
|
|
1185
|
+
|
|
1186
|
+
// Shape/type checks only; the set of valid entity types is server
|
|
1187
|
+
// configuration and enforced there with a clear 400 (see pii()).
|
|
1188
|
+
// An array of texts is the batch form (SAP-395 series, sent as `texts`,
|
|
1189
|
+
// one result per item); the 1–10 item bound is structural like classify's.
|
|
1190
|
+
const isBatch = Array.isArray(text);
|
|
1191
|
+
if (isBatch) {
|
|
1192
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
1193
|
+
// undefined and rejected, rather than skipped.
|
|
1194
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
1195
|
+
if (ok) {
|
|
1196
|
+
for (const item of text) {
|
|
1197
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
1198
|
+
ok = false;
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (!ok) {
|
|
1204
|
+
throw new TypeError('ner: texts must be an array of 1 to 10 non-empty strings.');
|
|
1205
|
+
}
|
|
1206
|
+
} else if (typeof text !== 'string' || text.trim().length === 0) {
|
|
1207
|
+
throw new TypeError('ner: text must be a non-empty string.');
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
let data = {
|
|
1211
|
+
...(isBatch ? { texts: text } : { text }),
|
|
1212
|
+
key: this.apiKey,
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
1216
|
+
// type rather than silently treated as "omitted", matching pii().
|
|
1217
|
+
// Reject an empty list too: the MCP schema requires at least one type
|
|
1218
|
+
// (min(1)), so an empty array is typed-valid input that would otherwise
|
|
1219
|
+
// become an avoidable API error.
|
|
1220
|
+
if (options?.types !== undefined) {
|
|
1221
|
+
if (!Array.isArray(options.types) || options.types.length === 0
|
|
1222
|
+
|| !Array.from(options.types).every((type) => typeof type === 'string' && type.trim().length > 0)) {
|
|
1223
|
+
throw new TypeError('ner: types must be a non-empty array of non-empty strings.');
|
|
1224
|
+
}
|
|
1225
|
+
data.types = options.types;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
return this.axiosPromise(url, 'post', data);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Check whether an account-wide block (expired key/subscription,
|
|
1233
|
+
* depleted credits, billing limit, trial daily allowance, monthly quota)
|
|
1234
|
+
* stops this API key from making billable calls, and if so why and for
|
|
1235
|
+
* how long. Works while the key is blocked, which is when it is most
|
|
1236
|
+
* useful: call it after an unexpected 402 or monthly-quota 429 instead of
|
|
1237
|
+
* parsing the error text. It does not reflect a single endpoint's
|
|
1238
|
+
* short-term rate limit (each endpoint has its own bucket), so a
|
|
1239
|
+
* "Capacity used." 429 elsewhere can coexist with api_access: true — use
|
|
1240
|
+
* that 429's retry_after / X-RateLimit-* headers instead. Unbilled, but each call draws a fixed 2,000
|
|
1241
|
+
* characters from its own rate-limit bucket, so don't poll it per request.
|
|
1242
|
+
* Requires a per-account API key; the public demo key and MCP OAuth
|
|
1243
|
+
* access tokens get a 403.
|
|
1244
|
+
*
|
|
1245
|
+
* Always returns `api_access`, `blocked_reason` (null, or one of
|
|
1246
|
+
* key_expired, subscription_expired, credits_depleted,
|
|
1247
|
+
* monthly_billing_limit, trial_daily_limit, monthly_quota_exhausted),
|
|
1248
|
+
* `message` and `usage` ({monthly_quota_chars}); `retry_after` (seconds)
|
|
1249
|
+
* only when blocked. Keys that are not team-scoped also get
|
|
1250
|
+
* `usage.month_start`/`usage.chars_this_month`, `subscription`, `credits`
|
|
1251
|
+
* and, for free-trial accounts, `trial`.
|
|
1252
|
+
*
|
|
1253
|
+
* @returns {Promise<AxiosResponse>}
|
|
1254
|
+
*/
|
|
1255
|
+
accountStatus() {
|
|
1256
|
+
const url = `${this.urlEndpoint}account/status`;
|
|
1257
|
+
// Not axiosPromise: this is a GET and the key rides the query string,
|
|
1258
|
+
// like styleguideRulesets().
|
|
1259
|
+
return axios({
|
|
1260
|
+
url,
|
|
1261
|
+
method: 'get',
|
|
1262
|
+
params: { key: this.apiKey },
|
|
1263
|
+
timeout: this.timeout,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
211
1266
|
}
|