crw-sdk 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cjs/client.js +42 -6
- package/dist/esm/client.d.ts +1 -1
- package/dist/esm/client.js +42 -6
- package/dist/esm/types.d.ts +49 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ CRW_LOCAL=1 node app.js
|
|
|
58
58
|
| `changeTrackingDiff(cur, prev?)` | Diff vs a prior snapshot | HTTP |
|
|
59
59
|
| `close()` | Shut down the local subprocess | — |
|
|
60
60
|
|
|
61
|
-
¹ Local search needs a
|
|
61
|
+
¹ Local search needs a search backend configured on the engine.
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
64
|
// Structured extraction (async job → per-URL results array):
|
package/dist/cjs/client.js
CHANGED
|
@@ -95,7 +95,7 @@ class CrwClient {
|
|
|
95
95
|
return result.links ?? [];
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
* Works in both modes; local mode needs a
|
|
98
|
+
* Works in both modes; local mode needs a search backend configured on the engine.
|
|
99
99
|
*/
|
|
100
100
|
async search(query, opts = {}) {
|
|
101
101
|
const { limit = 5, lang, tbs, sources, categories, scrapeOptions, ...rest } = opts;
|
|
@@ -111,8 +111,36 @@ class CrwClient {
|
|
|
111
111
|
if (scrapeOptions)
|
|
112
112
|
args.scrapeOptions = scrapeOptions;
|
|
113
113
|
Object.assign(args, rest);
|
|
114
|
-
if (this.apiUrl)
|
|
115
|
-
|
|
114
|
+
if (this.apiUrl) {
|
|
115
|
+
// `data` IS the results, in whatever shape the caller asked for: a flat
|
|
116
|
+
// array, a `{ web, news }` object when `sources` is set, or the engine's
|
|
117
|
+
// `{ results, answer, … }` when you point the client at a self-hosted
|
|
118
|
+
// server. Return it untouched — reshaping it here silently emptied both the
|
|
119
|
+
// grouped and the self-hosted cases.
|
|
120
|
+
//
|
|
121
|
+
// `answer` / `citations` / `llmUsage` ride BESIDE `data` on the managed API,
|
|
122
|
+
// and the old `data` unwrap dropped them: you could pay for an AI answer and
|
|
123
|
+
// have no way to read it. Attach them non-enumerably, so `results.answer`
|
|
124
|
+
// works while `for…of`, spread and JSON.stringify keep seeing exactly the
|
|
125
|
+
// array they saw before.
|
|
126
|
+
const raw = await this.httpRequest("POST", "/v1/search", args, {
|
|
127
|
+
raw: true,
|
|
128
|
+
});
|
|
129
|
+
const data = (raw.data ?? []);
|
|
130
|
+
if (data !== null && typeof data === "object") {
|
|
131
|
+
for (const key of ["answer", "citations", "llmUsage"]) {
|
|
132
|
+
if (raw[key] !== undefined) {
|
|
133
|
+
Object.defineProperty(data, key, {
|
|
134
|
+
value: raw[key],
|
|
135
|
+
enumerable: false,
|
|
136
|
+
configurable: true,
|
|
137
|
+
writable: true,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
116
144
|
return this.localTransport().toolCall("crw_search", args);
|
|
117
145
|
}
|
|
118
146
|
/**
|
|
@@ -178,12 +206,14 @@ class CrwClient {
|
|
|
178
206
|
async extract(opts) {
|
|
179
207
|
if (!this.apiUrl)
|
|
180
208
|
throw new errors_js_1.CrwError(httpOnlyHint("extract", "LLM extract job endpoint"));
|
|
181
|
-
const { urls, prompt, schema, llmApiKey, llmProvider, llmModel, pollInterval = 2, timeout = 120 } = opts;
|
|
209
|
+
const { urls, prompt, schema, basis, llmApiKey, llmProvider, llmModel, pollInterval = 2, timeout = 120 } = opts;
|
|
182
210
|
const body = { urls: [...urls] };
|
|
183
211
|
if (prompt !== undefined)
|
|
184
212
|
body.prompt = prompt;
|
|
185
213
|
if (schema !== undefined)
|
|
186
214
|
body.schema = schema;
|
|
215
|
+
if (basis !== undefined)
|
|
216
|
+
body.basis = basis;
|
|
187
217
|
if (llmApiKey !== undefined)
|
|
188
218
|
body.llmApiKey = llmApiKey;
|
|
189
219
|
if (llmProvider !== undefined)
|
|
@@ -192,8 +222,14 @@ class CrwClient {
|
|
|
192
222
|
body.llmModel = llmModel;
|
|
193
223
|
const start = await this.httpRequest("POST", "/v1/extract", body, { raw: true });
|
|
194
224
|
const jobId = start.id;
|
|
195
|
-
|
|
196
|
-
|
|
225
|
+
// The managed API answers synchronously: the extraction is already finished and
|
|
226
|
+
// the payload is in this first response, with no job to poll. Only the async
|
|
227
|
+
// path hands back an `id`. Demanding one made every managed extract() throw.
|
|
228
|
+
if (!jobId) {
|
|
229
|
+
if (Array.isArray(start.results))
|
|
230
|
+
return start.results;
|
|
231
|
+
throw new errors_js_1.CrwError(`extract returned neither a job id nor results: ${JSON.stringify(start)}`);
|
|
232
|
+
}
|
|
197
233
|
const deadline = Date.now() + timeout * 1000;
|
|
198
234
|
for (;;) {
|
|
199
235
|
if (Date.now() > deadline)
|
package/dist/esm/client.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare class CrwClient {
|
|
|
17
17
|
crawl(url: string, opts?: CrawlOptions): Promise<CrawlResult>;
|
|
18
18
|
map(url: string, opts?: MapOptions): Promise<string[]>;
|
|
19
19
|
/**
|
|
20
|
-
* Works in both modes; local mode needs a
|
|
20
|
+
* Works in both modes; local mode needs a search backend configured on the engine.
|
|
21
21
|
*/
|
|
22
22
|
search(query: string, opts?: SearchOptions): Promise<SearchResult>;
|
|
23
23
|
/**
|
package/dist/esm/client.js
CHANGED
|
@@ -92,7 +92,7 @@ export class CrwClient {
|
|
|
92
92
|
return result.links ?? [];
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
* Works in both modes; local mode needs a
|
|
95
|
+
* Works in both modes; local mode needs a search backend configured on the engine.
|
|
96
96
|
*/
|
|
97
97
|
async search(query, opts = {}) {
|
|
98
98
|
const { limit = 5, lang, tbs, sources, categories, scrapeOptions, ...rest } = opts;
|
|
@@ -108,8 +108,36 @@ export class CrwClient {
|
|
|
108
108
|
if (scrapeOptions)
|
|
109
109
|
args.scrapeOptions = scrapeOptions;
|
|
110
110
|
Object.assign(args, rest);
|
|
111
|
-
if (this.apiUrl)
|
|
112
|
-
|
|
111
|
+
if (this.apiUrl) {
|
|
112
|
+
// `data` IS the results, in whatever shape the caller asked for: a flat
|
|
113
|
+
// array, a `{ web, news }` object when `sources` is set, or the engine's
|
|
114
|
+
// `{ results, answer, … }` when you point the client at a self-hosted
|
|
115
|
+
// server. Return it untouched — reshaping it here silently emptied both the
|
|
116
|
+
// grouped and the self-hosted cases.
|
|
117
|
+
//
|
|
118
|
+
// `answer` / `citations` / `llmUsage` ride BESIDE `data` on the managed API,
|
|
119
|
+
// and the old `data` unwrap dropped them: you could pay for an AI answer and
|
|
120
|
+
// have no way to read it. Attach them non-enumerably, so `results.answer`
|
|
121
|
+
// works while `for…of`, spread and JSON.stringify keep seeing exactly the
|
|
122
|
+
// array they saw before.
|
|
123
|
+
const raw = await this.httpRequest("POST", "/v1/search", args, {
|
|
124
|
+
raw: true,
|
|
125
|
+
});
|
|
126
|
+
const data = (raw.data ?? []);
|
|
127
|
+
if (data !== null && typeof data === "object") {
|
|
128
|
+
for (const key of ["answer", "citations", "llmUsage"]) {
|
|
129
|
+
if (raw[key] !== undefined) {
|
|
130
|
+
Object.defineProperty(data, key, {
|
|
131
|
+
value: raw[key],
|
|
132
|
+
enumerable: false,
|
|
133
|
+
configurable: true,
|
|
134
|
+
writable: true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return data;
|
|
140
|
+
}
|
|
113
141
|
return this.localTransport().toolCall("crw_search", args);
|
|
114
142
|
}
|
|
115
143
|
/**
|
|
@@ -175,12 +203,14 @@ export class CrwClient {
|
|
|
175
203
|
async extract(opts) {
|
|
176
204
|
if (!this.apiUrl)
|
|
177
205
|
throw new CrwError(httpOnlyHint("extract", "LLM extract job endpoint"));
|
|
178
|
-
const { urls, prompt, schema, llmApiKey, llmProvider, llmModel, pollInterval = 2, timeout = 120 } = opts;
|
|
206
|
+
const { urls, prompt, schema, basis, llmApiKey, llmProvider, llmModel, pollInterval = 2, timeout = 120 } = opts;
|
|
179
207
|
const body = { urls: [...urls] };
|
|
180
208
|
if (prompt !== undefined)
|
|
181
209
|
body.prompt = prompt;
|
|
182
210
|
if (schema !== undefined)
|
|
183
211
|
body.schema = schema;
|
|
212
|
+
if (basis !== undefined)
|
|
213
|
+
body.basis = basis;
|
|
184
214
|
if (llmApiKey !== undefined)
|
|
185
215
|
body.llmApiKey = llmApiKey;
|
|
186
216
|
if (llmProvider !== undefined)
|
|
@@ -189,8 +219,14 @@ export class CrwClient {
|
|
|
189
219
|
body.llmModel = llmModel;
|
|
190
220
|
const start = await this.httpRequest("POST", "/v1/extract", body, { raw: true });
|
|
191
221
|
const jobId = start.id;
|
|
192
|
-
|
|
193
|
-
|
|
222
|
+
// The managed API answers synchronously: the extraction is already finished and
|
|
223
|
+
// the payload is in this first response, with no job to poll. Only the async
|
|
224
|
+
// path hands back an `id`. Demanding one made every managed extract() throw.
|
|
225
|
+
if (!jobId) {
|
|
226
|
+
if (Array.isArray(start.results))
|
|
227
|
+
return start.results;
|
|
228
|
+
throw new CrwError(`extract returned neither a job id nor results: ${JSON.stringify(start)}`);
|
|
229
|
+
}
|
|
194
230
|
const deadline = Date.now() + timeout * 1000;
|
|
195
231
|
for (;;) {
|
|
196
232
|
if (Date.now() > deadline)
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -25,9 +25,53 @@ export interface ScrapeOptions {
|
|
|
25
25
|
waitFor?: number;
|
|
26
26
|
/** JSON Schema for structured LLM extraction (auto-adds the `json` format). */
|
|
27
27
|
jsonSchema?: Json;
|
|
28
|
+
/**
|
|
29
|
+
* Return per-field evidence alongside `json`. Every top-level scalar property
|
|
30
|
+
* of `jsonSchema` comes back as a {@link Basis}. Requires `jsonSchema`.
|
|
31
|
+
*/
|
|
32
|
+
basis?: boolean;
|
|
28
33
|
/** Any other engine scrape option, passed through verbatim. */
|
|
29
34
|
[key: string]: unknown;
|
|
30
35
|
}
|
|
36
|
+
/** How well an extracted field is attributed to its source. */
|
|
37
|
+
export type FieldStatus = "supported" | "unverified" | "unsupported" | "notFound";
|
|
38
|
+
/** The citation backing an extracted value. Every field is server-produced. */
|
|
39
|
+
export interface EvidenceCitation {
|
|
40
|
+
/** The document the engine fetched. Never a model-supplied url. */
|
|
41
|
+
url: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Verbatim span from the canonical source text. Absent when the span could
|
|
45
|
+
* not be established (`status: "unverified"`).
|
|
46
|
+
*/
|
|
47
|
+
excerpt?: string;
|
|
48
|
+
/** `sha256:<hex>` of the exact text sent to the model. */
|
|
49
|
+
sourceHash: string;
|
|
50
|
+
/** Which text `sourceHash` covers. `"llmInput"` for extraction basis. */
|
|
51
|
+
sourceTextKind: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Per-field extraction evidence. Emitted only for top-level scalar schema
|
|
55
|
+
* properties, and only when the request set `basis: true`.
|
|
56
|
+
*
|
|
57
|
+
* `status` is honest: a field whose attribution could not be verified is marked
|
|
58
|
+
* `unverified`/`unsupported` rather than given a fabricated citation.
|
|
59
|
+
* `citations` is empty exactly when `status` is `unsupported` or `notFound`;
|
|
60
|
+
* `value` is `null` exactly when `status` is `notFound`.
|
|
61
|
+
*/
|
|
62
|
+
export interface Basis {
|
|
63
|
+
basisVersion: number;
|
|
64
|
+
field: string;
|
|
65
|
+
value: unknown | null;
|
|
66
|
+
status: FieldStatus;
|
|
67
|
+
confidence?: "low" | "medium" | "high";
|
|
68
|
+
citations: EvidenceCitation[];
|
|
69
|
+
}
|
|
70
|
+
/** A coded reason a field's attribution was downgraded. Never upstream text. */
|
|
71
|
+
export interface BasisWarning {
|
|
72
|
+
field: string;
|
|
73
|
+
code: string;
|
|
74
|
+
}
|
|
31
75
|
export interface CrawlOptions {
|
|
32
76
|
maxDepth?: number;
|
|
33
77
|
maxPages?: number;
|
|
@@ -60,6 +104,11 @@ export interface ExtractOptions {
|
|
|
60
104
|
urls: string[];
|
|
61
105
|
prompt?: string;
|
|
62
106
|
schema?: Json;
|
|
107
|
+
/**
|
|
108
|
+
* Return per-field evidence: each result carries a `basis` array, one
|
|
109
|
+
* {@link Basis} per top-level scalar schema property. Requires `schema`.
|
|
110
|
+
*/
|
|
111
|
+
basis?: boolean;
|
|
63
112
|
/** BYOK: use your own LLM key/provider/model instead of the server's. */
|
|
64
113
|
llmApiKey?: string;
|
|
65
114
|
llmProvider?: string;
|
package/package.json
CHANGED