@saplingai/sapling-js 1.0.38 → 1.0.45
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 +67 -1
- package/client/index.d.ts +1 -0
- package/package.json +37 -17
- package/sapling/client.d.ts +192 -12
- package/sapling/client.js +395 -8
- package/npmtest/package.json +0 -11
package/README.md
CHANGED
|
@@ -76,6 +76,24 @@ You should see something like the following output:
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
### Text classification
|
|
80
|
+
|
|
81
|
+
Zero-shot classification with your own labels (plain names or `{ name, description }` objects, 2–20):
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
client
|
|
85
|
+
.classify(
|
|
86
|
+
'I was charged twice and the app crashes on launch.',
|
|
87
|
+
['billing', { name: 'technical issue', description: 'Bugs, crashes, errors.' }, 'other'],
|
|
88
|
+
{ multiLabel: true, threshold: 0.5, context: 'Support tickets for a SaaS billing product' },
|
|
89
|
+
)
|
|
90
|
+
.then(function (response) {
|
|
91
|
+
console.log(response.data);
|
|
92
|
+
// { label: 'billing', labels: ['billing', 'technical issue'],
|
|
93
|
+
// scores: [{ label: 'billing', score: 0.92 }, ...], rationale: '...', multi_label: true }
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
79
97
|
## Frontend Quickstart
|
|
80
98
|
|
|
81
99
|
The SDK provides a way to automatically apply edit suggestions to any HTML contenteditable and textarea elements.
|
|
@@ -108,8 +126,56 @@ function App() {
|
|
|
108
126
|
}
|
|
109
127
|
```
|
|
110
128
|
|
|
129
|
+
## Structured Data Extraction
|
|
130
|
+
|
|
131
|
+
Pull caller-named fields out of unstructured text — invoices, emails, resumes, support tickets.
|
|
132
|
+
Pass 1–20 fields (plain names, or `{ name, type, description, required }` objects):
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
client
|
|
136
|
+
.extract(
|
|
137
|
+
'Invoice INV-1042 for Acme Corp, total $1,299.00, due March 5, 2026.',
|
|
138
|
+
[
|
|
139
|
+
'invoice_number',
|
|
140
|
+
{ name: 'total', type: 'number', description: 'Amount due', required: true },
|
|
141
|
+
{ name: 'due_date', type: 'date' },
|
|
142
|
+
'purchase_order',
|
|
143
|
+
],
|
|
144
|
+
{ context: 'A vendor invoice' },
|
|
145
|
+
)
|
|
146
|
+
.then(function (response) {
|
|
147
|
+
console.log(response.data);
|
|
148
|
+
// { data: { invoice_number: 'INV-1042', total: 1299.0, due_date: '2026-03-05', purchase_order: null },
|
|
149
|
+
// fields: [{ name: 'invoice_number', type: 'string', value: 'INV-1042',
|
|
150
|
+
// evidence: 'Invoice INV-1042', found: true }, ...],
|
|
151
|
+
// missing: ['purchase_order'] }
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`data` always contains every field you asked for, `null` where the text didn't state it, and each
|
|
156
|
+
entry in `fields` carries the verbatim `evidence` span its value came from. Values are coerced to
|
|
157
|
+
the declared `type` (`'string'` (default), `'number'`, `'integer'`, `'boolean'`, `'date'` returned
|
|
158
|
+
as `YYYY-MM-DD`, or `'list'`), and every value must be grounded in a quote from the text — this is
|
|
159
|
+
extraction, not inference, so a value that isn't stated is reported in `missing` rather than
|
|
160
|
+
guessed.
|
|
161
|
+
|
|
111
162
|
## Further Documentation
|
|
112
163
|
|
|
113
164
|
- Full Sapling HTTP API and SDK [documentation](https://sapling.ai/docs).
|
|
114
165
|
- Create a key by following [these instructions](https://sapling.ai/docs/api/api-access).
|
|
115
|
-
- JavaScript, HTML, and Python [starter code](https://github.com/saplingai/sapling-samples).
|
|
166
|
+
- JavaScript, HTML, and Python [starter code](https://github.com/saplingai/sapling-samples).
|
|
167
|
+
|
|
168
|
+
## Error handling
|
|
169
|
+
|
|
170
|
+
Every client method returns the axios promise, so API errors (a `400` for
|
|
171
|
+
over-limit input, `401` for a bad key, `429` for rate limits, network errors and
|
|
172
|
+
timeouts) **reject** with the [axios error](https://axios-http.com/docs/handling_errors);
|
|
173
|
+
inspect `error.response.status` / `error.response.data`:
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
try {
|
|
177
|
+
const response = await client.edits(text);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(error.response?.status, error.response?.data ?? error.message);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../sapling/client';
|
package/package.json
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saplingai/sapling-js",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.45",
|
|
4
4
|
"description": "JavaScript library for using the Sapling.ai API.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "./sapling/client.js",
|
|
7
|
+
"types": "./sapling/client.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./sapling/client.d.ts",
|
|
11
|
+
"default": "./sapling/client.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./sapling/client.d.ts",
|
|
15
|
+
"default": "./sapling/client.js"
|
|
16
|
+
},
|
|
17
|
+
"./observer": {
|
|
18
|
+
"types": "./sapling/observer.d.ts",
|
|
19
|
+
"default": "./sapling/observer.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"sapling",
|
|
24
|
+
"client",
|
|
25
|
+
"observer",
|
|
26
|
+
"index.ts",
|
|
27
|
+
"index.d.ts",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node --test test/client.test.js test/errors.test.js test/pack.test.js",
|
|
33
|
+
"prepublishOnly": "npm test"
|
|
34
|
+
},
|
|
6
35
|
"dependencies": {
|
|
7
|
-
"axios": "^1.
|
|
36
|
+
"axios": "^1.19.0",
|
|
8
37
|
"uuid": "^8.3.2"
|
|
9
38
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
12
42
|
"author": "Sapling Intelligence",
|
|
13
43
|
"email": "info@sapling.ai",
|
|
14
44
|
"url": "https://sapling.ai",
|
|
15
45
|
"homepage": "https://sapling.ai/docs/",
|
|
16
46
|
"repository": {
|
|
17
47
|
"type": "git",
|
|
18
|
-
"url": "https://github.com/saplingai/sapling-
|
|
48
|
+
"url": "git+https://github.com/saplingai/sapling-js.git"
|
|
19
49
|
},
|
|
20
50
|
"bugs": {
|
|
21
|
-
"url": "https://github.com/saplingai/sapling-
|
|
51
|
+
"url": "https://github.com/saplingai/sapling-js/issues",
|
|
22
52
|
"email": "support@sapling.ai"
|
|
23
53
|
},
|
|
24
54
|
"license": "Apache-2.0",
|
|
@@ -27,15 +57,5 @@
|
|
|
27
57
|
"type": "Apache-2.0",
|
|
28
58
|
"url": "http://www.apache.org/licenses/LICENSE-2.0"
|
|
29
59
|
}
|
|
30
|
-
]
|
|
31
|
-
"exports": {
|
|
32
|
-
"./client": {
|
|
33
|
-
"types": "./sapling/client.d.ts",
|
|
34
|
-
"default": "./sapling/client.js"
|
|
35
|
-
},
|
|
36
|
-
"./observer": {
|
|
37
|
-
"types": "./sapling/observer.d.ts",
|
|
38
|
-
"default": "./sapling/observer.js"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
60
|
+
]
|
|
41
61
|
}
|
package/sapling/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AxiosResponse } from "axios";
|
|
2
2
|
|
|
3
|
-
type AdvancedEditOptions = {
|
|
3
|
+
export type AdvancedEditOptions = {
|
|
4
4
|
advanced_edits?: boolean,
|
|
5
5
|
adverbs?: boolean,
|
|
6
6
|
simplifications?: boolean,
|
|
@@ -20,13 +20,13 @@ type AdvancedEditOptions = {
|
|
|
20
20
|
violence?: boolean,
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
type ConstructorOptions = {
|
|
23
|
+
export type ConstructorOptions = {
|
|
24
24
|
timeout?: number,
|
|
25
25
|
hostname?: string,
|
|
26
26
|
pathname?: string,
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
type EditOptions = {
|
|
29
|
+
export type EditOptions = {
|
|
30
30
|
variety?: string,
|
|
31
31
|
autoApply?: boolean,
|
|
32
32
|
lang?: string,
|
|
@@ -34,11 +34,150 @@ type EditOptions = {
|
|
|
34
34
|
advancedEdits?: AdvancedEditOptions,
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
type AIDetectOptions = {
|
|
37
|
+
export type AIDetectOptions = {
|
|
38
38
|
sent_scores?: boolean,
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
type
|
|
41
|
+
export type LangdetectOptions = {
|
|
42
|
+
top_k?: number,
|
|
43
|
+
segments?: boolean,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ClassifyLabel = string | { name: string; description?: string };
|
|
47
|
+
|
|
48
|
+
export type ClassifyOptions = {
|
|
49
|
+
multiLabel?: boolean,
|
|
50
|
+
threshold?: number,
|
|
51
|
+
context?: string,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ClassifyResponse = {
|
|
55
|
+
label: string;
|
|
56
|
+
labels: string[];
|
|
57
|
+
scores: { label: string; score: number }[];
|
|
58
|
+
rationale: string;
|
|
59
|
+
multi_label: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type SeoOptions = {
|
|
63
|
+
keywords?: string[],
|
|
64
|
+
suggestions?: boolean,
|
|
65
|
+
lang?: string,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type SeoStats = {
|
|
69
|
+
chars: number;
|
|
70
|
+
words: number;
|
|
71
|
+
sentences: number;
|
|
72
|
+
paragraphs: number;
|
|
73
|
+
reading_time_min: number;
|
|
74
|
+
flesch_reading_ease: number | null;
|
|
75
|
+
flesch_kincaid_grade: number | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type SeoKeyword = {
|
|
79
|
+
keyword: string;
|
|
80
|
+
count: number;
|
|
81
|
+
density: number;
|
|
82
|
+
in_first_100_words: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type SeoTopTerm = {
|
|
86
|
+
term: string;
|
|
87
|
+
count: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type SeoSuggestions = {
|
|
91
|
+
titles: string[];
|
|
92
|
+
meta_descriptions: string[];
|
|
93
|
+
slug: string | null;
|
|
94
|
+
keywords: string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type SeoResponse = {
|
|
98
|
+
stats: SeoStats;
|
|
99
|
+
keywords: SeoKeyword[];
|
|
100
|
+
top_terms: SeoTopTerm[];
|
|
101
|
+
suggestions?: SeoSuggestions;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type QualityOptions = {
|
|
105
|
+
sentence_scores?: boolean,
|
|
106
|
+
rubric?: boolean,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type QualitySentenceScore = {
|
|
110
|
+
start: number;
|
|
111
|
+
end: number;
|
|
112
|
+
text: string;
|
|
113
|
+
score: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type QualityRubricIssue = {
|
|
117
|
+
quote: string;
|
|
118
|
+
dimension: string;
|
|
119
|
+
note: string;
|
|
120
|
+
suggestion: string;
|
|
121
|
+
start: number;
|
|
122
|
+
end: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type QualityRubric = {
|
|
126
|
+
overall: number;
|
|
127
|
+
dimensions: {
|
|
128
|
+
clarity: number;
|
|
129
|
+
coherence: number;
|
|
130
|
+
correctness: number;
|
|
131
|
+
concision: number;
|
|
132
|
+
};
|
|
133
|
+
summary: string;
|
|
134
|
+
issues: QualityRubricIssue[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type QualityResponse = {
|
|
138
|
+
score: number;
|
|
139
|
+
sentences?: QualitySentenceScore[];
|
|
140
|
+
rubric?: QualityRubric;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type PIIType = 'email' | 'phone' | 'ssn' | 'credit_card' | 'ip_address' | 'iban' | 'us_bank_routing';
|
|
144
|
+
|
|
145
|
+
export type PIIOptions = {
|
|
146
|
+
types?: PIIType[],
|
|
147
|
+
redact?: boolean,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type PIIEntity = {
|
|
151
|
+
type: PIIType;
|
|
152
|
+
text: string;
|
|
153
|
+
start: number;
|
|
154
|
+
end: number;
|
|
155
|
+
replacement: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type PIIResponse = {
|
|
159
|
+
entities: PIIEntity[];
|
|
160
|
+
flagged: boolean;
|
|
161
|
+
types: PIIType[];
|
|
162
|
+
redacted_text?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type SafetyResponse = {
|
|
166
|
+
toxicity: number;
|
|
167
|
+
profanity: number;
|
|
168
|
+
harassment: number;
|
|
169
|
+
hate_speech: number;
|
|
170
|
+
self_harm: number;
|
|
171
|
+
sexual: number;
|
|
172
|
+
violence: number;
|
|
173
|
+
flagged: string[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type SafetyOptions = {
|
|
177
|
+
threshold?: number,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type SpellcheckOptions = {
|
|
42
181
|
minLength?: number,
|
|
43
182
|
multipleEdits?: boolean,
|
|
44
183
|
lang?: string,
|
|
@@ -47,18 +186,59 @@ type SpellcheckOptions = {
|
|
|
47
186
|
userData?: any,
|
|
48
187
|
}
|
|
49
188
|
|
|
50
|
-
|
|
189
|
+
export type ExtractFieldType = 'string' | 'number' | 'integer' | 'boolean' | 'date' | 'list';
|
|
190
|
+
|
|
191
|
+
export type ExtractField = string | {
|
|
192
|
+
name: string,
|
|
193
|
+
type?: ExtractFieldType,
|
|
194
|
+
description?: string,
|
|
195
|
+
required?: boolean,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type ExtractOptions = {
|
|
199
|
+
context?: string,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type ExtractedValue = string | number | boolean | string[] | null;
|
|
203
|
+
|
|
204
|
+
export type ExtractedField = {
|
|
205
|
+
name: string,
|
|
206
|
+
type: ExtractFieldType,
|
|
207
|
+
value: ExtractedValue,
|
|
208
|
+
evidence: string,
|
|
209
|
+
found: boolean,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export type ExtractResponse = {
|
|
213
|
+
data: Record<string, ExtractedValue>,
|
|
214
|
+
fields: ExtractedField[],
|
|
215
|
+
missing: string[],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type UserDetails = {
|
|
219
|
+
userId?: string,
|
|
220
|
+
anonUser?: boolean,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export declare class Client {
|
|
51
224
|
constructor(apiKey: string, options?: ConstructorOptions);
|
|
52
225
|
|
|
53
|
-
edits(text: string, sessionId
|
|
54
|
-
acceptEdit(editUUID: string, sessionId
|
|
55
|
-
rejectEdit(editUUID: string, sessionId
|
|
226
|
+
edits(text: string, sessionId?: string, options?: EditOptions, userDetails?: UserDetails): Promise<AxiosResponse>;
|
|
227
|
+
acceptEdit(editUUID: string, sessionId?: string, userId?: string): Promise<AxiosResponse>;
|
|
228
|
+
rejectEdit(editUUID: string, sessionId?: string, userId?: string): Promise<AxiosResponse>;
|
|
56
229
|
|
|
57
|
-
spellcheck(text: string, sessionId
|
|
230
|
+
spellcheck(text: string, sessionId?: string, options?: SpellcheckOptions, userDetails?: UserDetails): Promise<AxiosResponse>;
|
|
58
231
|
|
|
59
|
-
complete(query: string, sessionId
|
|
60
|
-
acceptComplete(completeUUID: string, query: string, completion: string, sessionId
|
|
232
|
+
complete(query: string, sessionId?: string): Promise<AxiosResponse>;
|
|
233
|
+
acceptComplete(completeUUID: string, query: string, completion: string, sessionId?: string): Promise<AxiosResponse>;
|
|
61
234
|
tone(text: string): Promise<AxiosResponse>;
|
|
235
|
+
extract(text: string, fields: ExtractField[], options?: ExtractOptions): Promise<AxiosResponse<ExtractResponse>>;
|
|
62
236
|
|
|
63
237
|
aidetect(text: string, options?: AIDetectOptions): Promise<AxiosResponse>;
|
|
238
|
+
langdetect(text: string, options?: LangdetectOptions): Promise<AxiosResponse>;
|
|
239
|
+
quality(text: string, options?: QualityOptions): Promise<AxiosResponse<QualityResponse>>;
|
|
240
|
+
pii(text: string, options?: PIIOptions): Promise<AxiosResponse<PIIResponse>>;
|
|
241
|
+
safety(text: string, options?: SafetyOptions): Promise<AxiosResponse<SafetyResponse>>;
|
|
242
|
+
classify(text: string, labels: ClassifyLabel[], options?: ClassifyOptions): Promise<AxiosResponse<ClassifyResponse>>;
|
|
243
|
+
seo(text: string, options?: SeoOptions): Promise<AxiosResponse<SeoResponse>>;
|
|
64
244
|
}
|
package/sapling/client.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
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
|
+
|
|
4
7
|
export class Client {
|
|
5
8
|
constructor(apiKey, options = {}) {
|
|
6
9
|
this.apiKey = apiKey;
|
|
@@ -13,7 +16,7 @@ export class Client {
|
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
axiosPromise(url, method, data) {
|
|
16
|
-
return new Promise((resolve) => {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
17
20
|
axios({
|
|
18
21
|
url,
|
|
19
22
|
method,
|
|
@@ -22,7 +25,8 @@ export class Client {
|
|
|
22
25
|
})
|
|
23
26
|
.then(function (response) {
|
|
24
27
|
resolve(response);
|
|
25
|
-
})
|
|
28
|
+
})
|
|
29
|
+
.catch(reject);
|
|
26
30
|
});
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -106,27 +110,27 @@ export class Client {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
if (options.minLength != null) {
|
|
109
|
-
data.min_length = minLength;
|
|
113
|
+
data.min_length = options.minLength;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
if (options.multipleEdits != null) {
|
|
113
|
-
data.multiple_edits = multipleEdits;
|
|
117
|
+
data.multiple_edits = options.multipleEdits;
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
if (options.lang != null) {
|
|
117
|
-
data.lang = lang;
|
|
121
|
+
data.lang = options.lang;
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
if (options.autoApply != null) {
|
|
121
|
-
data.auto_apply = autoApply;
|
|
125
|
+
data.auto_apply = options.autoApply;
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
if (options.variety != null) {
|
|
125
|
-
data.variety = variety;
|
|
129
|
+
data.variety = options.variety;
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
if (options.userData != null) {
|
|
129
|
-
data.user_data = userData;
|
|
133
|
+
data.user_data = options.userData;
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
if (userDetails) {
|
|
@@ -176,6 +180,356 @@ export class Client {
|
|
|
176
180
|
return this.axiosPromise(url, 'post', data);
|
|
177
181
|
}
|
|
178
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Structured data extraction: pull caller-named fields out of unstructured text.
|
|
185
|
+
*
|
|
186
|
+
* `classify()` answers "what kind of text is this"; `extract()` answers "what's in it".
|
|
187
|
+
* Pass 1–20 fields — plain names, or `{name, type, description, required}` objects to
|
|
188
|
+
* declare the value type and sharpen ambiguous fields. Returns `data` (a
|
|
189
|
+
* `{field name: value}` map with every requested field present and `null` where the text
|
|
190
|
+
* didn't state it), `fields` (one entry per requested field, in request order, each with
|
|
191
|
+
* the verbatim `evidence` span the value came from) and `missing` (the names the text
|
|
192
|
+
* didn't yield).
|
|
193
|
+
*
|
|
194
|
+
* Values are coerced to the declared type server-side and must be grounded in a quote
|
|
195
|
+
* from the text, so this is extraction, not inference: a value that isn't stated is
|
|
196
|
+
* reported missing rather than guessed. Finding nothing is a normal 200.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} text - The document to extract from, plain text or HTML (the API caps
|
|
199
|
+
* it at 10,000 characters).
|
|
200
|
+
* @param {Array<string|{name: string, type?: string, description?: string, required?: boolean}>} fields -
|
|
201
|
+
* The fields to pull out (1–20): field names, or objects. `type` is one of 'string'
|
|
202
|
+
* (default), 'number', 'integer', 'boolean', 'date' (returned as YYYY-MM-DD) or 'list'.
|
|
203
|
+
* Names must be unique case-insensitively; the API caps names at 50 characters and
|
|
204
|
+
* descriptions at 200.
|
|
205
|
+
* @param {Object} [options]
|
|
206
|
+
* @param {string} [options.context] - Optional guidance (the API caps it at 500
|
|
207
|
+
* characters): what the document is and how to read ambiguous fields, e.g.
|
|
208
|
+
* 'A vendor invoice'.
|
|
209
|
+
* @returns {Promise<AxiosResponse>}
|
|
210
|
+
*/
|
|
211
|
+
extract(text, fields, options = {}) {
|
|
212
|
+
const url = `${this.urlEndpoint}extract`;
|
|
213
|
+
|
|
214
|
+
// Shape/type checks only. Size limits (text length, field name/description/
|
|
215
|
+
// context length) are server configuration and enforced there with a clear
|
|
216
|
+
// 400 — mirroring them here would drift the day they change (and JS
|
|
217
|
+
// `length` counts UTF-16 units, not the characters the API counts).
|
|
218
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
219
|
+
throw new TypeError('extract: text must be a non-empty string.');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const isValidField = (field) => {
|
|
223
|
+
if (typeof field === 'string') {
|
|
224
|
+
return field.trim().length > 0;
|
|
225
|
+
}
|
|
226
|
+
if (field == null || typeof field !== 'object' || Array.isArray(field)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (typeof field.name !== 'string' || field.name.trim().length === 0) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (field.type !== undefined && !EXTRACT_FIELD_TYPES.includes(field.type)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
if (field.description !== undefined && typeof field.description !== 'string') {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
if (field.required !== undefined && typeof field.required !== 'boolean') {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
// Reject unknown keys so a typo like `{ name, typ: 'number' }` fails here
|
|
242
|
+
// rather than being forwarded to the API as a malformed field (the MCP
|
|
243
|
+
// schema is `.strict()` for the same reason).
|
|
244
|
+
const allowed = new Set(['name', 'type', 'description', 'required']);
|
|
245
|
+
if (Object.keys(field).some((key) => !allowed.has(key))) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (
|
|
252
|
+
!Array.isArray(fields)
|
|
253
|
+
|| fields.length < 1
|
|
254
|
+
|| fields.length > 20
|
|
255
|
+
// Array.from() materializes holes in sparse arrays as `undefined` (which
|
|
256
|
+
// isValidField rejects); a bare `.every()` would skip them and let a hole
|
|
257
|
+
// through to be serialized as `null`.
|
|
258
|
+
|| !Array.from(fields).every(isValidField)
|
|
259
|
+
) {
|
|
260
|
+
throw new TypeError('extract: fields must be an array of 1 to 20 non-empty strings or '
|
|
261
|
+
+ '{name, type?, description?, required?} objects (string name, type one of '
|
|
262
|
+
+ `${EXTRACT_FIELD_TYPES.join(', ')}, optional string description, optional boolean required).`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const names = fields.map((field) => (typeof field === 'string' ? field : field.name).toLowerCase());
|
|
266
|
+
if (names.length !== new Set(names).size) {
|
|
267
|
+
throw new TypeError('extract: field names must be unique (case-insensitive).');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let data = {
|
|
271
|
+
text,
|
|
272
|
+
fields,
|
|
273
|
+
key: this.apiKey,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
277
|
+
// type rather than silently treated as "omitted", matching how a null
|
|
278
|
+
// `description` is rejected above.
|
|
279
|
+
if (options?.context !== undefined) {
|
|
280
|
+
if (typeof options.context !== 'string') {
|
|
281
|
+
throw new TypeError('extract: context must be a string.');
|
|
282
|
+
}
|
|
283
|
+
data.context = options.context;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return this.axiosPromise(url, 'post', data);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
langdetect(text, options = {}) {
|
|
290
|
+
const url = `${this.urlEndpoint}langdetect`;
|
|
291
|
+
|
|
292
|
+
let data = {
|
|
293
|
+
text,
|
|
294
|
+
key: this.apiKey,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (options?.top_k != null) {
|
|
298
|
+
data.top_k = options.top_k;
|
|
299
|
+
}
|
|
300
|
+
if (options?.segments != null) {
|
|
301
|
+
data.segments = options.segments;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return this.axiosPromise(url, 'post', data);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Zero-shot text classification: assign one (or several) of your own labels to a text.
|
|
309
|
+
*
|
|
310
|
+
* No training data needed — pass 2–20 candidate labels (plain names, or
|
|
311
|
+
* `{name, description}` objects; a short description per label sharpens borderline
|
|
312
|
+
* decisions). Returns `label` (best match, always one of your labels), `labels`
|
|
313
|
+
* (single-label: `[label]`; multi-label: every label with `score >= threshold`, possibly
|
|
314
|
+
* empty), `scores` (one `{label, score}` per input label, descending), a one-sentence
|
|
315
|
+
* `rationale` and the echoed `multi_label` mode.
|
|
316
|
+
*
|
|
317
|
+
* @param {string} text - Text to classify, plain text or HTML (the API caps it at 10,000 chars).
|
|
318
|
+
* @param {Array<string|{name: string, description?: string}>} labels - Candidate labels
|
|
319
|
+
* (2–20). Names unique case-insensitively; the API caps names at 50 and descriptions at 200 chars.
|
|
320
|
+
* @param {Object} [options]
|
|
321
|
+
* @param {boolean} [options.multiLabel] - true: zero, one or several labels may apply
|
|
322
|
+
* (independent 0–1 scores). Defaults to false: exactly one label (scores sum to ~1).
|
|
323
|
+
* @param {number} [options.threshold] - Multi-label cut-off on the per-label score (0–1,
|
|
324
|
+
* default 0.5). Ignored in single-label mode.
|
|
325
|
+
* @param {string} [options.context] - Optional guidance (max 500 chars): what the texts
|
|
326
|
+
* are and how to decide borderline cases, e.g. 'Support tickets for a SaaS billing product'.
|
|
327
|
+
* @returns {Promise<AxiosResponse>}
|
|
328
|
+
*/
|
|
329
|
+
classify(text, labels, options = {}) {
|
|
330
|
+
const url = `${this.urlEndpoint}classify`;
|
|
331
|
+
|
|
332
|
+
// Shape/type checks only. Size limits (text length, label/description/
|
|
333
|
+
// context length) are server configuration and enforced there with a
|
|
334
|
+
// clear 400 — mirroring them here would drift the day they change (and
|
|
335
|
+
// JS `length` counts UTF-16 units, not the characters the API counts).
|
|
336
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
337
|
+
throw new TypeError('classify: text must be a non-empty string.');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const isValidLabel = (label) => {
|
|
341
|
+
if (typeof label === 'string') {
|
|
342
|
+
return label.trim().length > 0;
|
|
343
|
+
}
|
|
344
|
+
if (label == null || typeof label !== 'object' || Array.isArray(label)) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
if (typeof label.name !== 'string' || label.name.trim().length === 0) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
if (label.description !== undefined && typeof label.description !== 'string') {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
// Reject unknown keys so a typo like `{ name, descrption: '...' }` fails
|
|
354
|
+
// here rather than being forwarded to the API as a malformed label (the
|
|
355
|
+
// MCP object schema is `.strict()` and `extract()` does the same).
|
|
356
|
+
const allowed = new Set(['name', 'description']);
|
|
357
|
+
if (Object.keys(label).some((key) => !allowed.has(key))) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
return true;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
!Array.isArray(labels)
|
|
365
|
+
|| labels.length < 2
|
|
366
|
+
|| labels.length > 20
|
|
367
|
+
// Array.from() materializes holes in sparse arrays as `undefined` (which
|
|
368
|
+
// isValidLabel rejects); a bare `.every()` would skip them and let a hole
|
|
369
|
+
// through to be serialized as `null`.
|
|
370
|
+
|| !Array.from(labels).every(isValidLabel)
|
|
371
|
+
) {
|
|
372
|
+
throw new TypeError('classify: labels must be an array of 2 to 20 non-empty strings or '
|
|
373
|
+
+ '{name, description?} objects (string name, optional string description).');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const names = labels.map((label) => (typeof label === 'string' ? label : label.name).toLowerCase());
|
|
377
|
+
if (names.length !== new Set(names).size) {
|
|
378
|
+
throw new TypeError('classify: label names must be unique (case-insensitive).');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let data = {
|
|
382
|
+
text,
|
|
383
|
+
labels,
|
|
384
|
+
key: this.apiKey,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
388
|
+
// type rather than silently treated as "omitted", matching how a null
|
|
389
|
+
// `description` is rejected above.
|
|
390
|
+
if (options?.multiLabel !== undefined) {
|
|
391
|
+
if (typeof options.multiLabel !== 'boolean') {
|
|
392
|
+
throw new TypeError('classify: multiLabel must be a boolean.');
|
|
393
|
+
}
|
|
394
|
+
data.multi_label = options.multiLabel;
|
|
395
|
+
}
|
|
396
|
+
if (options?.threshold !== undefined) {
|
|
397
|
+
if (typeof options.threshold !== 'number' || !Number.isFinite(options.threshold)
|
|
398
|
+
|| options.threshold < 0 || options.threshold > 1) {
|
|
399
|
+
throw new TypeError('classify: threshold must be a finite number between 0 and 1.');
|
|
400
|
+
}
|
|
401
|
+
data.threshold = options.threshold;
|
|
402
|
+
}
|
|
403
|
+
if (options?.context !== undefined) {
|
|
404
|
+
if (typeof options.context !== 'string') {
|
|
405
|
+
throw new TypeError('classify: context must be a string.');
|
|
406
|
+
}
|
|
407
|
+
data.context = options.context;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return this.axiosPromise(url, 'post', data);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* SEO analysis and metadata generation for a page or article body (plain text or HTML).
|
|
415
|
+
*
|
|
416
|
+
* Always returns deterministic `stats` (word/sentence/paragraph counts, reading time,
|
|
417
|
+
* Flesch readability), `keywords` (count/density/in_first_100_words for each target
|
|
418
|
+
* keyword) and `top_terms` (most frequent unigrams/bigrams). By default also returns
|
|
419
|
+
* LLM-generated `suggestions` (titles, meta_descriptions, slug — null when no ASCII slug
|
|
420
|
+
* can be derived — and keywords) — pass
|
|
421
|
+
* `suggestions: false` for the free stats-only mode.
|
|
422
|
+
*
|
|
423
|
+
* @param {string} text - Page/article body, plain text or HTML (max 20,000 chars).
|
|
424
|
+
* @param {Object} [options]
|
|
425
|
+
* @param {string[]} [options.keywords] - Target keyword phrases, most important first
|
|
426
|
+
* (max 10, each 1–100 chars). Measured in the text and steered into the suggestions.
|
|
427
|
+
* @param {boolean} [options.suggestions] - Generate LLM suggestions. Defaults to true;
|
|
428
|
+
* false returns stats/keywords/top_terms only (unbilled).
|
|
429
|
+
* @param {string} [options.lang] - ISO 639-1 language code of the text (region subtag
|
|
430
|
+
* allowed, e.g. 'pt-BR'; or 'auto'). Defaults to 'en'. Only selects the readability
|
|
431
|
+
* formulas: supported for en/de/es/fr/it/nl/pl/ru, otherwise flesch_reading_ease /
|
|
432
|
+
* flesch_kincaid_grade are null.
|
|
433
|
+
* @returns {Promise<AxiosResponse>}
|
|
434
|
+
*/
|
|
435
|
+
seo(text, options = {}) {
|
|
436
|
+
const url = `${this.urlEndpoint}seo`;
|
|
437
|
+
|
|
438
|
+
// Shape/type checks only. Size limits (text length, keyword count/length)
|
|
439
|
+
// are server configuration and enforced there with a clear 400 — mirroring
|
|
440
|
+
// them here would drift the day they change (and JS `length` counts UTF-16
|
|
441
|
+
// units, not the characters the API counts).
|
|
442
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
443
|
+
throw new TypeError('seo: text must be a non-empty string.');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let data = {
|
|
447
|
+
text,
|
|
448
|
+
key: this.apiKey,
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
452
|
+
// type rather than silently treated as "omitted", matching classify().
|
|
453
|
+
if (options?.keywords !== undefined) {
|
|
454
|
+
if (!Array.isArray(options.keywords)
|
|
455
|
+
|| !Array.from(options.keywords).every((k) => typeof k === 'string' && k.trim().length > 0)) {
|
|
456
|
+
throw new TypeError('seo: keywords must be an array of non-empty strings.');
|
|
457
|
+
}
|
|
458
|
+
data.keywords = options.keywords;
|
|
459
|
+
}
|
|
460
|
+
if (options?.suggestions !== undefined) {
|
|
461
|
+
if (typeof options.suggestions !== 'boolean') {
|
|
462
|
+
throw new TypeError('seo: suggestions must be a boolean.');
|
|
463
|
+
}
|
|
464
|
+
data.suggestions = options.suggestions;
|
|
465
|
+
}
|
|
466
|
+
if (options?.lang !== undefined) {
|
|
467
|
+
if (typeof options.lang !== 'string') {
|
|
468
|
+
throw new TypeError('seo: lang must be a string.');
|
|
469
|
+
}
|
|
470
|
+
data.lang = options.lang;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return this.axiosPromise(url, 'post', data);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
quality(text, options = {}) {
|
|
477
|
+
const url = `${this.urlEndpoint}quality`;
|
|
478
|
+
|
|
479
|
+
// Shape/type checks only; size limits are server configuration (see seo()).
|
|
480
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
481
|
+
throw new TypeError('quality: text must be a non-empty string.');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
let data = {
|
|
485
|
+
text,
|
|
486
|
+
key: this.apiKey,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
490
|
+
// type rather than silently treated as "omitted", matching classify()/seo().
|
|
491
|
+
if (options?.sentence_scores !== undefined) {
|
|
492
|
+
if (typeof options.sentence_scores !== 'boolean') {
|
|
493
|
+
throw new TypeError('quality: sentence_scores must be a boolean.');
|
|
494
|
+
}
|
|
495
|
+
data.sentence_scores = options.sentence_scores;
|
|
496
|
+
}
|
|
497
|
+
if (options?.rubric !== undefined) {
|
|
498
|
+
if (typeof options.rubric !== 'boolean') {
|
|
499
|
+
throw new TypeError('quality: rubric must be a boolean.');
|
|
500
|
+
}
|
|
501
|
+
data.rubric = options.rubric;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return this.axiosPromise(url, 'post', data);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
safety(text, options = {}) {
|
|
508
|
+
const url = `${this.urlEndpoint}safety`;
|
|
509
|
+
|
|
510
|
+
// Shape/type checks only; size limits are server configuration (see seo()).
|
|
511
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
512
|
+
throw new TypeError('safety: text must be a non-empty string.');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let data = {
|
|
516
|
+
text,
|
|
517
|
+
key: this.apiKey,
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
521
|
+
// type rather than silently treated as "omitted", matching classify().
|
|
522
|
+
if (options?.threshold !== undefined) {
|
|
523
|
+
if (typeof options.threshold !== 'number' || !Number.isFinite(options.threshold)
|
|
524
|
+
|| options.threshold < 0 || options.threshold > 1) {
|
|
525
|
+
throw new TypeError('safety: threshold must be a finite number between 0 and 1.');
|
|
526
|
+
}
|
|
527
|
+
data.threshold = options.threshold;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return this.axiosPromise(url, 'post', data);
|
|
531
|
+
}
|
|
532
|
+
|
|
179
533
|
aidetect(text, options = {}) {
|
|
180
534
|
const url = `${this.urlEndpoint}aidetect`;
|
|
181
535
|
|
|
@@ -190,4 +544,37 @@ export class Client {
|
|
|
190
544
|
|
|
191
545
|
return this.axiosPromise(url, 'post', data);
|
|
192
546
|
}
|
|
547
|
+
|
|
548
|
+
pii(text, options = {}) {
|
|
549
|
+
const url = `${this.urlEndpoint}pii`;
|
|
550
|
+
|
|
551
|
+
// Shape/type checks only; the set of valid PII types is server configuration
|
|
552
|
+
// and enforced there with a clear 400 (see seo()).
|
|
553
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
554
|
+
throw new TypeError('pii: text must be a non-empty string.');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
let data = {
|
|
558
|
+
text,
|
|
559
|
+
key: this.apiKey,
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
563
|
+
// type rather than silently treated as "omitted", matching classify()/seo().
|
|
564
|
+
if (options?.types !== undefined) {
|
|
565
|
+
if (!Array.isArray(options.types)
|
|
566
|
+
|| !Array.from(options.types).every((type) => typeof type === 'string' && type.trim().length > 0)) {
|
|
567
|
+
throw new TypeError('pii: types must be an array of non-empty strings.');
|
|
568
|
+
}
|
|
569
|
+
data.types = options.types;
|
|
570
|
+
}
|
|
571
|
+
if (options?.redact !== undefined) {
|
|
572
|
+
if (typeof options.redact !== 'boolean') {
|
|
573
|
+
throw new TypeError('pii: redact must be a boolean.');
|
|
574
|
+
}
|
|
575
|
+
data.redact = options.redact;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return this.axiosPromise(url, 'post', data);
|
|
579
|
+
}
|
|
193
580
|
}
|
package/npmtest/package.json
DELETED