@diffbot/typescript 0.1.0 → 0.3.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 +196 -9
- package/dist/index.cjs +242 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -9
- package/dist/index.d.ts +163 -9
- package/dist/index.js +242 -47
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +93 -0
- package/dist/node/index.cjs.map +1 -0
- package/dist/node/index.d.cts +27 -0
- package/dist/node/index.d.ts +27 -0
- package/dist/node/index.js +63 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +18 -12
package/README.md
CHANGED
|
@@ -8,7 +8,9 @@ TypeScript client library for [Diffbot](https://www.diffbot.com) APIs. This is a
|
|
|
8
8
|
pnpm add @diffbot/typescript
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Requires Node.js 18+ (native `fetch`).
|
|
11
|
+
Requires Node.js 18+ (native `fetch`) to build and install. The main entry point itself
|
|
12
|
+
runs anywhere `fetch` exists, including Cloudflare Workers with no compatibility flags —
|
|
13
|
+
see [Cloudflare Workers](#cloudflare-workers) below.
|
|
12
14
|
|
|
13
15
|
## Usage
|
|
14
16
|
|
|
@@ -19,18 +21,66 @@ Create a client once, then pass it to the API functions you need. Import only th
|
|
|
19
21
|
### Authentication
|
|
20
22
|
|
|
21
23
|
```typescript
|
|
22
|
-
import { DiffbotClient, extract,
|
|
24
|
+
import { DiffbotClient, extract, resolveTokenFromEnv } from "@diffbot/typescript";
|
|
23
25
|
|
|
24
|
-
const db = new DiffbotClient({ token:
|
|
26
|
+
const db = new DiffbotClient({ token: resolveTokenFromEnv() });
|
|
25
27
|
const data = await extract(db, "https://www.example.com");
|
|
26
28
|
await db.close();
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
Token resolution order:
|
|
30
32
|
|
|
31
|
-
1. Explicit argument to `
|
|
32
|
-
2. `
|
|
33
|
-
3. `DIFFBOT_API_TOKEN
|
|
33
|
+
1. Explicit argument to `resolveTokenFromEnv(token)`
|
|
34
|
+
2. An `env`-style object passed as the second argument — the Workers convention: `resolveTokenFromEnv(undefined, env)`
|
|
35
|
+
3. `DIFFBOT_API_TOKEN` environment variable (`process.env`)
|
|
36
|
+
|
|
37
|
+
Node users who want a `~/.diffbot/credentials` file as a final fallback should import
|
|
38
|
+
`resolveToken` from `@diffbot/typescript/node` instead — same first three steps, plus the
|
|
39
|
+
file. See [Runtime support](#runtime-support-node-vs-everywhere-else).
|
|
40
|
+
|
|
41
|
+
### Client configuration
|
|
42
|
+
|
|
43
|
+
`DiffbotClient` is the single place to configure the SDK — every API function takes its
|
|
44
|
+
settings from the client you hand it.
|
|
45
|
+
|
|
46
|
+
| Option | Default | Used by |
|
|
47
|
+
|--------|---------|---------|
|
|
48
|
+
| `token` | — (required) | all |
|
|
49
|
+
| `timeout` | `30000` (ms) | all |
|
|
50
|
+
| `fetch` | global `fetch` | all |
|
|
51
|
+
| `analyzeUrl` | `https://api.diffbot.com/v3` | `extract` |
|
|
52
|
+
| `crawlerUrl` | `https://api.diffbot.com/v3/crawl` | `crawl`, `crawlListJobs`, `crawlGetJob`, `crawlDeleteJob` |
|
|
53
|
+
| `llmUrl` | `https://llm.diffbot.com/rag/v1/chat/completions` | `ask`, `askJson` |
|
|
54
|
+
| `webSearchUrl` | `https://llm.diffbot.com/api/v1/web_search` | `webSearch` |
|
|
55
|
+
| `nlpUrl` | `https://nl.diffbot.com/v1/` | `entities` |
|
|
56
|
+
| `dqlUrl` | `https://kg.diffbot.com/kg/v3/dql` | `dql`, `dqlParallel` |
|
|
57
|
+
| `ontologyUrl` | `https://kg.diffbot.com/kg/ontology` | `dqlFetchOntology`, `dqlFetchOntologyText`, `OntologyStore` |
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { DiffbotClient } from "@diffbot/typescript";
|
|
61
|
+
|
|
62
|
+
const db = new DiffbotClient({
|
|
63
|
+
token: "YOUR_TOKEN",
|
|
64
|
+
timeout: 60_000,
|
|
65
|
+
dqlUrl: "http://localhost:8080/kg/v3/dql",
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`analyzeUrl` and `crawlerUrl` are bases that the SDK appends to (`/${api}` and `/data`
|
|
70
|
+
respectively); the rest are complete endpoints, used as given.
|
|
71
|
+
|
|
72
|
+
Passing `fetch` replaces the transport, which is the hook for retries, logging, extra
|
|
73
|
+
headers, or mocking in tests:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const db = new DiffbotClient({
|
|
77
|
+
token: "YOUR_TOKEN",
|
|
78
|
+
fetch: async (input, init) => {
|
|
79
|
+
console.log("→", input); // the SDK always calls fetch with a URL string
|
|
80
|
+
return fetch(input, init);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
34
84
|
|
|
35
85
|
### Extract structured content
|
|
36
86
|
|
|
@@ -51,6 +101,49 @@ for await (const chunk of ask(db, [{ role: "user", content: "What's the capital
|
|
|
51
101
|
}
|
|
52
102
|
```
|
|
53
103
|
|
|
104
|
+
### Structured output
|
|
105
|
+
|
|
106
|
+
Pass a JSON Schema to constrain the answer. The model is held to the schema by a grammar
|
|
107
|
+
during decoding, so the result parses reliably even though the answer is retrieved live
|
|
108
|
+
from the web.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { askJson } from "@diffbot/typescript";
|
|
112
|
+
|
|
113
|
+
const schema = {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: { country: { type: "string" }, capital: { type: "string" } },
|
|
116
|
+
required: ["country", "capital"],
|
|
117
|
+
};
|
|
118
|
+
const answer = await askJson<{ country: string; capital: string }>(
|
|
119
|
+
db,
|
|
120
|
+
[{ role: "user", content: "What's the capital of France?" }],
|
|
121
|
+
schema,
|
|
122
|
+
);
|
|
123
|
+
console.log(answer.capital);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Omit the schema to let the model choose the shape, or use `ask` with `responseFormat` to
|
|
127
|
+
stream a constrained answer:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { ask, askJson, jsonSchemaFormat } from "@diffbot/typescript";
|
|
131
|
+
|
|
132
|
+
const answer = await askJson(db, [{ role: "user", content: "What's the capital of France?" }]);
|
|
133
|
+
|
|
134
|
+
for await (const chunk of ask(db, [{ role: "user", content: "What's the capital of France?" }], {
|
|
135
|
+
responseFormat: jsonSchemaFormat(schema),
|
|
136
|
+
})) {
|
|
137
|
+
process.stdout.write(chunk);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
> **Avoid `responseFormat: { type: "json_object" }`.** The endpoint accepts it, but applies
|
|
142
|
+
> no grammar to it: the RAG loop's internal tool call is itself a JSON object, so it can be
|
|
143
|
+
> returned as the final answer. This is reproducible whenever the request includes a system
|
|
144
|
+
> message. `askJson` therefore defaults to a permissive JSON Schema instead, and throws
|
|
145
|
+
> `ValidationError` if it ever sees a tool call come back.
|
|
146
|
+
|
|
54
147
|
### Crawl a site
|
|
55
148
|
|
|
56
149
|
```typescript
|
|
@@ -78,8 +171,39 @@ const results = await webSearch(db, "diffbot knowledge graph");
|
|
|
78
171
|
for (const r of results.search_results as Array<Record<string, unknown>>) {
|
|
79
172
|
console.log(r.score, r.title, r.pageUrl);
|
|
80
173
|
}
|
|
174
|
+
|
|
175
|
+
// snippetLength controls how much of each page comes back in "content":
|
|
176
|
+
// "short" (~1-1.5 KB per result), "medium", "long" (default), or "full"
|
|
177
|
+
const brief = await webSearch(db, "diffbot knowledge graph", { numResults: 5, snippetLength: "short" });
|
|
178
|
+
|
|
179
|
+
// "full" with a url: query returns the whole stored page
|
|
180
|
+
const page = await webSearch(db, "url:https://en.wikipedia.org/wiki/Knowledge_graph", { snippetLength: "full" });
|
|
81
181
|
```
|
|
82
182
|
|
|
183
|
+
### Caching the Knowledge Graph ontology
|
|
184
|
+
|
|
185
|
+
`dqlFetchOntology` fetches on every call. For anything that calls it more than once,
|
|
186
|
+
`OntologyStore` caches the parsed `Ontology`, dedupes concurrent fetches into one request,
|
|
187
|
+
and clears its memo on a rejection so a failed fetch doesn't get stuck:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { DiffbotClient, OntologyStore } from "@diffbot/typescript";
|
|
191
|
+
|
|
192
|
+
const db = new DiffbotClient({ token: "YOUR_TOKEN" });
|
|
193
|
+
const store = new OntologyStore(db);
|
|
194
|
+
|
|
195
|
+
const ontology = await store.load();
|
|
196
|
+
console.log(ontology.types());
|
|
197
|
+
|
|
198
|
+
// Force a refetch, e.g. after `dqlRefreshOntology`'s old role:
|
|
199
|
+
await store.load({ refresh: true });
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The base `OntologyStore` caches only in memory, for the life of the instance.
|
|
203
|
+
`KVOntologyStore` (below) persists across instances via Cloudflare KV; Node users
|
|
204
|
+
wanting a filesystem-backed cache should use `FileOntologyStore` from
|
|
205
|
+
`@diffbot/typescript/node` — see [Runtime support](#runtime-support-node-vs-everywhere-else).
|
|
206
|
+
|
|
83
207
|
### Entities (NLP)
|
|
84
208
|
|
|
85
209
|
```typescript
|
|
@@ -99,18 +223,70 @@ await using db = new DiffbotClient({ token: "YOUR_TOKEN" });
|
|
|
99
223
|
const data = await extract(db, "https://www.example.com");
|
|
100
224
|
```
|
|
101
225
|
|
|
226
|
+
## Runtime support: Node vs. everywhere else
|
|
227
|
+
|
|
228
|
+
The main entry point (`@diffbot/typescript`) imports no Node builtins and has no
|
|
229
|
+
import-time side effects — it runs in Node, browsers, and edge runtimes including
|
|
230
|
+
Cloudflare Workers, with **no compatibility flags required**. It is enforced, not just
|
|
231
|
+
tested: the build target is `platform: "neutral"`, so an accidental `node:fs` import
|
|
232
|
+
anywhere in the main entry fails the build, and a post-build script scans the emitted
|
|
233
|
+
bundle for node builtins as a second check.
|
|
234
|
+
|
|
235
|
+
Two things genuinely need a filesystem and live in a separate `@diffbot/typescript/node`
|
|
236
|
+
entry point instead:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { resolveToken, FileOntologyStore } from "@diffbot/typescript/node";
|
|
240
|
+
|
|
241
|
+
const token = resolveToken(); // env, then ~/.diffbot/credentials
|
|
242
|
+
const store = new FileOntologyStore(db, "/path/to/ontology.json");
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
| | Main entry (`@diffbot/typescript`) | Node entry (`@diffbot/typescript/node`) |
|
|
246
|
+
|---|---|---|
|
|
247
|
+
| Token resolution | `resolveTokenFromEnv(token?, env?)` — arg, `env`, `process.env` | `resolveToken(token?, env?)` — same, plus `~/.diffbot/credentials` |
|
|
248
|
+
| Ontology cache | `OntologyStore` (memory), `KVOntologyStore` (Cloudflare KV) | `FileOntologyStore` (filesystem) |
|
|
249
|
+
|
|
250
|
+
## Cloudflare Workers
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { DiffbotClient, dql, resolveTokenFromEnv, KVOntologyStore } from "@diffbot/typescript";
|
|
254
|
+
|
|
255
|
+
export default {
|
|
256
|
+
async fetch(request: Request, env: { DIFFBOT_API_TOKEN: string; ONTOLOGY: KVNamespace }) {
|
|
257
|
+
const db = new DiffbotClient({ token: resolveTokenFromEnv(undefined, env) });
|
|
258
|
+
const ontology = await new KVOntologyStore(db, env.ONTOLOGY).load();
|
|
259
|
+
const results = await dql(db, 'type:Organization name:"Diffbot"');
|
|
260
|
+
return Response.json({ results, types: ontology.types() });
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
No `compatibility_flags` entry is needed for this package. Tokens come from `env`, not a
|
|
266
|
+
credentials file — there is no filesystem in a Worker, so `resolveToken` (the Node-only,
|
|
267
|
+
file-backed version) is not importable here; use `resolveTokenFromEnv`. For the ontology
|
|
268
|
+
cache, pass a `KVNamespace` binding straight to `KVOntologyStore` — it satisfies the
|
|
269
|
+
store's `KVLike` interface directly, with a lock against concurrent refresh stampedes and
|
|
270
|
+
optional stale-while-revalidate via `staleAfterSeconds` + `waitUntil`.
|
|
271
|
+
|
|
272
|
+
`fixtures/worker/` in this repo is a working Worker exercising exactly this path, verified
|
|
273
|
+
in CI against workerd with `compatibility_flags: []`.
|
|
274
|
+
|
|
102
275
|
## Python ↔ TypeScript API mapping
|
|
103
276
|
|
|
104
277
|
| Python | TypeScript |
|
|
105
278
|
|--------|------------|
|
|
106
|
-
| `resolve_token()` | `resolveToken()` |
|
|
279
|
+
| `resolve_token()` | `resolveTokenFromEnv()` (or `resolveToken()` from `@diffbot/typescript/node`) |
|
|
107
280
|
| `crawl_list_jobs()` | `crawlListJobs()` |
|
|
108
281
|
| `crawl_get_job()` | `crawlGetJob()` |
|
|
109
282
|
| `crawl_delete_job()` | `crawlDeleteJob()` |
|
|
110
283
|
| `dql_parallel()` | `dqlParallel()` |
|
|
111
284
|
| `dql_fetch_ontology()` | `dqlFetchOntology()` |
|
|
112
|
-
| `dql_refresh_ontology()` | `
|
|
113
|
-
| `web_search()` | `webSearch()` |
|
|
285
|
+
| `dql_refresh_ontology()` | `new OntologyStore(db).load({ refresh: true })` (or `FileOntologyStore` from `@diffbot/typescript/node`) |
|
|
286
|
+
| `web_search(snippet_length=...)` | `webSearch(db, text, { snippetLength })` |
|
|
287
|
+
| `ask(response_format=...)` | `ask(db, messages, { responseFormat })` |
|
|
288
|
+
| `ask_json()` | `askJson()` |
|
|
289
|
+
| `json_schema_format()` | `jsonSchemaFormat()` |
|
|
114
290
|
|
|
115
291
|
## Development
|
|
116
292
|
|
|
@@ -126,6 +302,17 @@ Live integration tests (requires `DIFFBOT_API_TOKEN`):
|
|
|
126
302
|
pnpm test:live
|
|
127
303
|
```
|
|
128
304
|
|
|
305
|
+
Cloudflare Worker fixture (proves the main entry needs no compat flags — see
|
|
306
|
+
`fixtures/worker/`):
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
pnpm build # fixture resolves the package through dist/
|
|
310
|
+
cd fixtures/worker
|
|
311
|
+
pnpm install
|
|
312
|
+
pnpm exec wrangler deploy --dry-run # bundle check, no Cloudflare credentials needed
|
|
313
|
+
pnpm exec vitest run # runs in workerd via @cloudflare/vitest-pool-workers
|
|
314
|
+
```
|
|
315
|
+
|
|
129
316
|
## License
|
|
130
317
|
|
|
131
318
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -20,34 +20,42 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ANY_OBJECT_SCHEMA: () => ANY_OBJECT_SCHEMA,
|
|
23
24
|
APIError: () => APIError,
|
|
24
25
|
AuthError: () => AuthError,
|
|
25
26
|
CrawlEventType: () => CrawlEventType,
|
|
26
27
|
DiffbotClient: () => DiffbotClient,
|
|
27
28
|
DiffbotError: () => DiffbotError,
|
|
28
29
|
ExtractionError: () => ExtractionError,
|
|
30
|
+
KVOntologyStore: () => KVOntologyStore,
|
|
29
31
|
Ontology: () => Ontology,
|
|
32
|
+
OntologyStore: () => OntologyStore,
|
|
33
|
+
RESPONSE_FORMAT_TYPES: () => RESPONSE_FORMAT_TYPES,
|
|
30
34
|
RateLimitError: () => RateLimitError,
|
|
35
|
+
SNIPPET_LENGTHS: () => SNIPPET_LENGTHS,
|
|
31
36
|
VERSION: () => VERSION,
|
|
32
37
|
ValidationError: () => ValidationError,
|
|
33
38
|
ask: () => ask,
|
|
39
|
+
askJson: () => askJson,
|
|
34
40
|
crawl: () => crawl,
|
|
35
41
|
crawlDeleteJob: () => crawlDeleteJob,
|
|
36
42
|
crawlGetJob: () => crawlGetJob,
|
|
37
43
|
crawlListJobs: () => crawlListJobs,
|
|
38
44
|
dql: () => dql,
|
|
39
45
|
dqlFetchOntology: () => dqlFetchOntology,
|
|
46
|
+
dqlFetchOntologyText: () => dqlFetchOntologyText,
|
|
40
47
|
dqlParallel: () => dqlParallel,
|
|
41
|
-
dqlRefreshOntology: () => dqlRefreshOntology,
|
|
42
48
|
entities: () => entities,
|
|
43
49
|
extract: () => extract,
|
|
44
|
-
|
|
50
|
+
extractJson: () => extractJson,
|
|
51
|
+
jsonSchemaFormat: () => jsonSchemaFormat,
|
|
52
|
+
resolveTokenFromEnv: () => resolveTokenFromEnv,
|
|
45
53
|
webSearch: () => webSearch
|
|
46
54
|
});
|
|
47
55
|
module.exports = __toCommonJS(index_exports);
|
|
48
56
|
|
|
49
57
|
// src/constants.ts
|
|
50
|
-
var VERSION = "0.
|
|
58
|
+
var VERSION = "0.3.0";
|
|
51
59
|
var DEFAULT_TIMEOUT = 3e4;
|
|
52
60
|
var EXTRACT_BASE = "https://api.diffbot.com/v3";
|
|
53
61
|
var CRAWL_BASE = "https://api.diffbot.com/v3/crawl";
|
|
@@ -188,6 +196,8 @@ var DiffbotClient = class {
|
|
|
188
196
|
crawlerUrl;
|
|
189
197
|
webSearchUrl;
|
|
190
198
|
nlpUrl;
|
|
199
|
+
dqlUrl;
|
|
200
|
+
ontologyUrl;
|
|
191
201
|
http;
|
|
192
202
|
constructor(options) {
|
|
193
203
|
if (!options.token) {
|
|
@@ -199,6 +209,8 @@ var DiffbotClient = class {
|
|
|
199
209
|
this.crawlerUrl = options.crawlerUrl ?? CRAWL_BASE;
|
|
200
210
|
this.webSearchUrl = options.webSearchUrl ?? WEB_SEARCH_BASE;
|
|
201
211
|
this.nlpUrl = options.nlpUrl ?? NLP_BASE;
|
|
212
|
+
this.dqlUrl = options.dqlUrl ?? KG_DQL_ENDPOINT;
|
|
213
|
+
this.ontologyUrl = options.ontologyUrl ?? KG_ONTOLOGY_ENDPOINT;
|
|
202
214
|
this.http = new HttpClient(
|
|
203
215
|
options.fetch ?? fetch,
|
|
204
216
|
options.timeout ?? DEFAULT_TIMEOUT,
|
|
@@ -214,34 +226,20 @@ var DiffbotClient = class {
|
|
|
214
226
|
};
|
|
215
227
|
|
|
216
228
|
// src/auth.ts
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return "";
|
|
226
|
-
}
|
|
227
|
-
const text = (0, import_node_fs.readFileSync)(path, "utf8");
|
|
228
|
-
for (const line of text.split("\n")) {
|
|
229
|
-
const trimmed = line.trim();
|
|
230
|
-
if (trimmed.startsWith(`${TOKEN_ENV_VAR}=`)) {
|
|
231
|
-
return trimmed.slice(TOKEN_ENV_VAR.length + 1).trim();
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return "";
|
|
229
|
+
function envToken(env) {
|
|
230
|
+
const fromEnv = env?.[TOKEN_ENV_VAR];
|
|
231
|
+
if (typeof fromEnv === "string" && fromEnv.trim()) {
|
|
232
|
+
return fromEnv.trim();
|
|
233
|
+
}
|
|
234
|
+
const proc = globalThis.process;
|
|
235
|
+
const fromProcess = proc?.env?.[TOKEN_ENV_VAR];
|
|
236
|
+
return fromProcess?.trim() ?? "";
|
|
235
237
|
}
|
|
236
|
-
function
|
|
238
|
+
function resolveTokenFromEnv(token, env) {
|
|
237
239
|
if (token?.trim()) {
|
|
238
240
|
return token.trim();
|
|
239
241
|
}
|
|
240
|
-
|
|
241
|
-
if (envToken) {
|
|
242
|
-
return envToken;
|
|
243
|
-
}
|
|
244
|
-
return readCredentialsFile();
|
|
242
|
+
return envToken(env);
|
|
245
243
|
}
|
|
246
244
|
|
|
247
245
|
// src/extract.ts
|
|
@@ -279,6 +277,80 @@ async function extract(client, url, api = "analyze", fmt = "markdown") {
|
|
|
279
277
|
}
|
|
280
278
|
|
|
281
279
|
// src/ask.ts
|
|
280
|
+
var MODEL = "diffbot-small-xl";
|
|
281
|
+
var RESPONSE_FORMAT_TYPES = ["text", "json_object", "json_schema"];
|
|
282
|
+
function jsonSchemaFormat(schema, name = "response") {
|
|
283
|
+
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
|
284
|
+
throw new ValidationError("schema must be a JSON Schema object");
|
|
285
|
+
}
|
|
286
|
+
return { type: "json_schema", json_schema: { name, schema } };
|
|
287
|
+
}
|
|
288
|
+
function validateResponseFormat(responseFormat) {
|
|
289
|
+
if (responseFormat === void 0) return;
|
|
290
|
+
if (responseFormat === null || typeof responseFormat !== "object" || Array.isArray(responseFormat)) {
|
|
291
|
+
throw new ValidationError("responseFormat must be an object");
|
|
292
|
+
}
|
|
293
|
+
const fmtType = responseFormat.type ?? "text";
|
|
294
|
+
if (!RESPONSE_FORMAT_TYPES.includes(fmtType)) {
|
|
295
|
+
throw new ValidationError(
|
|
296
|
+
`responseFormat type must be one of ${RESPONSE_FORMAT_TYPES.join(", ")}; got ${JSON.stringify(fmtType)}`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
if (fmtType === "json_schema") {
|
|
300
|
+
const jsonSchema = responseFormat.json_schema;
|
|
301
|
+
if (jsonSchema === null || typeof jsonSchema !== "object" || !("schema" in jsonSchema)) {
|
|
302
|
+
throw new ValidationError(
|
|
303
|
+
'responseFormat { type: "json_schema" } requires the schema nested as { json_schema: { schema: {...} } }. Without it the server returns 200 and ignores the constraint. Use jsonSchemaFormat(schema) to build it.'
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
var THINK_BLOCK = /<think>[\s\S]*?<\/think>/g;
|
|
309
|
+
var JSON_FENCE = /^```(?:json)?\s*|\s*```$/gm;
|
|
310
|
+
function extractJson(text) {
|
|
311
|
+
let cleaned = text.replace(THINK_BLOCK, "").trim();
|
|
312
|
+
cleaned = cleaned.replace(JSON_FENCE, "").trim();
|
|
313
|
+
try {
|
|
314
|
+
return JSON.parse(cleaned);
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
const spans = [];
|
|
318
|
+
for (const [opener, closer] of [
|
|
319
|
+
["{", "}"],
|
|
320
|
+
["[", "]"]
|
|
321
|
+
]) {
|
|
322
|
+
const start = cleaned.indexOf(opener);
|
|
323
|
+
const end = cleaned.lastIndexOf(closer);
|
|
324
|
+
if (start !== -1 && end > start) spans.push([start, cleaned.slice(start, end + 1)]);
|
|
325
|
+
}
|
|
326
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
327
|
+
for (const [, span] of spans) {
|
|
328
|
+
try {
|
|
329
|
+
return JSON.parse(span);
|
|
330
|
+
} catch {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
throw new ValidationError(
|
|
335
|
+
`could not parse JSON from the model response: ${JSON.stringify(text.slice(0, 200))}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
var ANY_OBJECT_SCHEMA = { type: "object" };
|
|
339
|
+
function resolveFormat(schema, responseFormat) {
|
|
340
|
+
if (schema !== void 0 && responseFormat !== void 0) {
|
|
341
|
+
throw new ValidationError("pass either schema or responseFormat, not both");
|
|
342
|
+
}
|
|
343
|
+
if (responseFormat !== void 0) return responseFormat;
|
|
344
|
+
return jsonSchemaFormat(schema ?? ANY_OBJECT_SCHEMA);
|
|
345
|
+
}
|
|
346
|
+
function checkToolCallLeak(parsed) {
|
|
347
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.name === "functioncall" && "arguments" in parsed) {
|
|
348
|
+
throw new ValidationError(
|
|
349
|
+
'the model returned its internal tool call instead of an answer; this happens with responseFormat { type: "json_object" } because the server applies no grammar to it. Pass a schema instead.'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return parsed;
|
|
353
|
+
}
|
|
282
354
|
function parseChunk(line) {
|
|
283
355
|
try {
|
|
284
356
|
const chunk = JSON.parse(line.replace(/^data: /, ""));
|
|
@@ -309,9 +381,11 @@ async function* linesFromStream(body) {
|
|
|
309
381
|
}
|
|
310
382
|
if (buffer) yield buffer;
|
|
311
383
|
}
|
|
312
|
-
async function* ask(client, messages) {
|
|
384
|
+
async function* ask(client, messages, options = {}) {
|
|
385
|
+
validateResponseFormat(options.responseFormat);
|
|
313
386
|
const headers = { Authorization: `Bearer ${client.token}` };
|
|
314
|
-
const payload = { model:
|
|
387
|
+
const payload = { model: MODEL, messages, stream: true };
|
|
388
|
+
if (options.responseFormat !== void 0) payload.response_format = options.responseFormat;
|
|
315
389
|
const response = await client.http.post(client.llmUrl, { headers, json: payload });
|
|
316
390
|
if (!response.ok) {
|
|
317
391
|
const body = await readBodyText(response);
|
|
@@ -331,6 +405,12 @@ async function* ask(client, messages) {
|
|
|
331
405
|
if (content) yield content;
|
|
332
406
|
}
|
|
333
407
|
}
|
|
408
|
+
async function askJson(client, messages, schema, options = {}) {
|
|
409
|
+
const responseFormat = resolveFormat(schema, options.responseFormat);
|
|
410
|
+
let text = "";
|
|
411
|
+
for await (const chunk of ask(client, messages, { responseFormat })) text += chunk;
|
|
412
|
+
return checkToolCallLeak(extractJson(text));
|
|
413
|
+
}
|
|
334
414
|
|
|
335
415
|
// src/crawl.ts
|
|
336
416
|
var CrawlEventType = /* @__PURE__ */ ((CrawlEventType2) => {
|
|
@@ -470,12 +550,7 @@ async function crawlDeleteJob(client, jobName) {
|
|
|
470
550
|
raiseForStatus(response, body);
|
|
471
551
|
}
|
|
472
552
|
|
|
473
|
-
// src/kg.ts
|
|
474
|
-
var import_promises2 = require("fs/promises");
|
|
475
|
-
var import_node_path2 = require("path");
|
|
476
|
-
|
|
477
553
|
// src/ontology.ts
|
|
478
|
-
var import_promises = require("fs/promises");
|
|
479
554
|
var Ontology = class _Ontology {
|
|
480
555
|
data;
|
|
481
556
|
constructor(data) {
|
|
@@ -485,10 +560,6 @@ var Ontology = class _Ontology {
|
|
|
485
560
|
const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
486
561
|
return new _Ontology(JSON.parse(text));
|
|
487
562
|
}
|
|
488
|
-
static async fromPath(path) {
|
|
489
|
-
const text = await (0, import_promises.readFile)(path, "utf8");
|
|
490
|
-
return _Ontology.fromJson(text);
|
|
491
|
-
}
|
|
492
563
|
types() {
|
|
493
564
|
const types = this.data.types;
|
|
494
565
|
return Object.keys(types ?? {}).sort();
|
|
@@ -601,7 +672,7 @@ function buildDqlParams(client, query, options) {
|
|
|
601
672
|
}
|
|
602
673
|
async function dql(client, query, options = {}) {
|
|
603
674
|
const params = buildDqlParams(client, query, options);
|
|
604
|
-
const response = await client.http.get(
|
|
675
|
+
const response = await client.http.get(client.dqlUrl, { params });
|
|
605
676
|
const body = await readBodyText(response);
|
|
606
677
|
raiseForStatus(response, body);
|
|
607
678
|
if (options.raw) {
|
|
@@ -625,18 +696,14 @@ async function dqlParallel(client, queries, workers = 8) {
|
|
|
625
696
|
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
626
697
|
return results;
|
|
627
698
|
}
|
|
628
|
-
async function
|
|
629
|
-
const response = await client.http.get(
|
|
699
|
+
async function dqlFetchOntologyText(client) {
|
|
700
|
+
const response = await client.http.get(client.ontologyUrl);
|
|
630
701
|
const body = await readBodyText(response);
|
|
631
702
|
raiseForStatus(response, body);
|
|
632
|
-
|
|
633
|
-
await (0, import_promises2.writeFile)(dest, body);
|
|
703
|
+
return body;
|
|
634
704
|
}
|
|
635
705
|
async function dqlFetchOntology(client) {
|
|
636
|
-
|
|
637
|
-
const body = await readBodyText(response);
|
|
638
|
-
raiseForStatus(response, body);
|
|
639
|
-
return Ontology.fromJson(body);
|
|
706
|
+
return Ontology.fromJson(await dqlFetchOntologyText(client));
|
|
640
707
|
}
|
|
641
708
|
|
|
642
709
|
// src/nlp.ts
|
|
@@ -651,41 +718,143 @@ async function entities(client, text, lang = "auto") {
|
|
|
651
718
|
return Array.isArray(data) ? data[0] ?? {} : data;
|
|
652
719
|
}
|
|
653
720
|
|
|
721
|
+
// src/ontology-store.ts
|
|
722
|
+
var OntologyStore = class {
|
|
723
|
+
client;
|
|
724
|
+
ttlSeconds;
|
|
725
|
+
inFlight = null;
|
|
726
|
+
constructor(client, options = {}) {
|
|
727
|
+
this.client = client;
|
|
728
|
+
this.ttlSeconds = options.ttlSeconds ?? 86400;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Resolve the ontology, from cache when possible. Concurrent calls share
|
|
732
|
+
* one in-flight fetch rather than issuing duplicate requests; a rejected
|
|
733
|
+
* fetch clears the in-flight memo so the next call retries instead of
|
|
734
|
+
* replaying the same failure forever.
|
|
735
|
+
*/
|
|
736
|
+
load(options = {}) {
|
|
737
|
+
if (options.refresh) {
|
|
738
|
+
this.inFlight = null;
|
|
739
|
+
}
|
|
740
|
+
if (!this.inFlight) {
|
|
741
|
+
this.inFlight = this.resolve(options.refresh ?? false).catch((error) => {
|
|
742
|
+
this.inFlight = null;
|
|
743
|
+
throw error;
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
return this.inFlight;
|
|
747
|
+
}
|
|
748
|
+
async resolve(refresh) {
|
|
749
|
+
if (!refresh) {
|
|
750
|
+
const cached = await this.read();
|
|
751
|
+
if (cached !== void 0) {
|
|
752
|
+
try {
|
|
753
|
+
return Ontology.fromJson(cached);
|
|
754
|
+
} catch {
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const text = await dqlFetchOntologyText(this.client);
|
|
759
|
+
await this.write(text);
|
|
760
|
+
return Ontology.fromJson(text);
|
|
761
|
+
}
|
|
762
|
+
/** Read the cached ontology text, or `undefined` on a miss. Memory-only by default. */
|
|
763
|
+
read() {
|
|
764
|
+
return Promise.resolve(void 0);
|
|
765
|
+
}
|
|
766
|
+
/** Persist ontology text after a fetch. No-op by default. */
|
|
767
|
+
write(_text) {
|
|
768
|
+
return Promise.resolve();
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
var KVOntologyStore = class extends OntologyStore {
|
|
772
|
+
kv;
|
|
773
|
+
key;
|
|
774
|
+
refreshedAtKey;
|
|
775
|
+
lockKey;
|
|
776
|
+
staleAfterSeconds;
|
|
777
|
+
lockTtlSeconds;
|
|
778
|
+
waitUntil;
|
|
779
|
+
constructor(client, kv, options = {}) {
|
|
780
|
+
super(client, options);
|
|
781
|
+
this.kv = kv;
|
|
782
|
+
this.key = options.key ?? "diffbot:ontology:v1";
|
|
783
|
+
this.refreshedAtKey = `${this.key}:refreshed_at`;
|
|
784
|
+
this.lockKey = `${this.key}:lock`;
|
|
785
|
+
this.staleAfterSeconds = options.staleAfterSeconds ?? 0;
|
|
786
|
+
this.lockTtlSeconds = options.lockTtlSeconds ?? 60;
|
|
787
|
+
this.waitUntil = options.waitUntil;
|
|
788
|
+
}
|
|
789
|
+
load(options = {}) {
|
|
790
|
+
if (!options.refresh && this.staleAfterSeconds > 0) {
|
|
791
|
+
return this.loadWithStaleWhileRevalidate();
|
|
792
|
+
}
|
|
793
|
+
return super.load(options);
|
|
794
|
+
}
|
|
795
|
+
async loadWithStaleWhileRevalidate() {
|
|
796
|
+
const [cached, refreshedAt] = await Promise.all([
|
|
797
|
+
this.kv.get(this.key),
|
|
798
|
+
this.kv.get(this.refreshedAtKey)
|
|
799
|
+
]);
|
|
800
|
+
if (cached === null) {
|
|
801
|
+
return super.load();
|
|
802
|
+
}
|
|
803
|
+
const age = refreshedAt ? (Date.now() - Number(refreshedAt)) / 1e3 : Infinity;
|
|
804
|
+
const isStale = age > this.staleAfterSeconds;
|
|
805
|
+
const ontology = Ontology.fromJson(cached);
|
|
806
|
+
if (isStale) {
|
|
807
|
+
const refresh = super.load({ refresh: true }).catch(() => {
|
|
808
|
+
});
|
|
809
|
+
if (this.waitUntil) {
|
|
810
|
+
this.waitUntil(refresh);
|
|
811
|
+
} else {
|
|
812
|
+
await refresh;
|
|
813
|
+
return super.load();
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return ontology;
|
|
817
|
+
}
|
|
818
|
+
async read() {
|
|
819
|
+
const cached = await this.kv.get(this.key);
|
|
820
|
+
return cached ?? void 0;
|
|
821
|
+
}
|
|
822
|
+
async write(text) {
|
|
823
|
+
if (await this.kv.get(this.lockKey)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
await this.kv.put(this.lockKey, "1", { expirationTtl: this.lockTtlSeconds });
|
|
827
|
+
try {
|
|
828
|
+
await this.kv.put(this.key, text, { expirationTtl: this.ttlSeconds });
|
|
829
|
+
await this.kv.put(this.refreshedAtKey, String(Date.now()), {
|
|
830
|
+
expirationTtl: this.ttlSeconds
|
|
831
|
+
});
|
|
832
|
+
} finally {
|
|
833
|
+
await this.kv.delete(this.lockKey);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
|
|
654
838
|
// src/web-search.ts
|
|
839
|
+
var SNIPPET_LENGTHS = ["short", "medium", "long", "full"];
|
|
655
840
|
async function webSearch(client, text, options = {}) {
|
|
656
841
|
const headers = { Authorization: `Bearer ${client.token}` };
|
|
657
842
|
const params = { text };
|
|
658
843
|
if (options.numResults !== void 0) params.size = options.numResults;
|
|
659
844
|
if (options.maxTokens !== void 0) params.maxTokens = options.maxTokens;
|
|
845
|
+
if (options.snippetLength !== void 0) {
|
|
846
|
+
const length = options.snippetLength.toLowerCase();
|
|
847
|
+
if (!SNIPPET_LENGTHS.includes(length)) {
|
|
848
|
+
throw new ValidationError(
|
|
849
|
+
`snippetLength must be one of ${SNIPPET_LENGTHS.join(", ")}; got ${JSON.stringify(options.snippetLength)}`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
params.snippetLength = length.toUpperCase();
|
|
853
|
+
if (length === "full") params.rerank = "none";
|
|
854
|
+
}
|
|
660
855
|
const response = await client.http.get(client.webSearchUrl, { headers, params });
|
|
661
856
|
const body = await readBodyText(response);
|
|
662
857
|
raiseForStatus(response, body);
|
|
663
858
|
return JSON.parse(body);
|
|
664
859
|
}
|
|
665
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
666
|
-
0 && (module.exports = {
|
|
667
|
-
APIError,
|
|
668
|
-
AuthError,
|
|
669
|
-
CrawlEventType,
|
|
670
|
-
DiffbotClient,
|
|
671
|
-
DiffbotError,
|
|
672
|
-
ExtractionError,
|
|
673
|
-
Ontology,
|
|
674
|
-
RateLimitError,
|
|
675
|
-
VERSION,
|
|
676
|
-
ValidationError,
|
|
677
|
-
ask,
|
|
678
|
-
crawl,
|
|
679
|
-
crawlDeleteJob,
|
|
680
|
-
crawlGetJob,
|
|
681
|
-
crawlListJobs,
|
|
682
|
-
dql,
|
|
683
|
-
dqlFetchOntology,
|
|
684
|
-
dqlParallel,
|
|
685
|
-
dqlRefreshOntology,
|
|
686
|
-
entities,
|
|
687
|
-
extract,
|
|
688
|
-
resolveToken,
|
|
689
|
-
webSearch
|
|
690
|
-
});
|
|
691
860
|
//# sourceMappingURL=index.cjs.map
|