@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 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, resolveToken } from "@diffbot/typescript";
24
+ import { DiffbotClient, extract, resolveTokenFromEnv } from "@diffbot/typescript";
23
25
 
24
- const db = new DiffbotClient({ token: resolveToken() });
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 `resolveToken(token)`
32
- 2. `DIFFBOT_API_TOKEN` environment variable
33
- 3. `DIFFBOT_API_TOKEN=...` line in `~/.diffbot/credentials`
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()` | `dqlRefreshOntology()` |
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
- resolveToken: () => resolveToken,
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.1.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
- var import_node_fs = require("fs");
218
- var import_node_os = require("os");
219
- var import_node_path = require("path");
220
- var authConfig = {
221
- credentialsPath: (0, import_node_path.join)((0, import_node_os.homedir)(), ".diffbot", "credentials")
222
- };
223
- function readCredentialsFile(path = authConfig.credentialsPath) {
224
- if (!(0, import_node_fs.existsSync)(path)) {
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 resolveToken(token) {
238
+ function resolveTokenFromEnv(token, env) {
237
239
  if (token?.trim()) {
238
240
  return token.trim();
239
241
  }
240
- const envToken = process.env[TOKEN_ENV_VAR]?.trim();
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: "diffbot-small-xl", messages, stream: true };
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(KG_DQL_ENDPOINT, { params });
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 dqlRefreshOntology(client, dest) {
629
- const response = await client.http.get(KG_ONTOLOGY_ENDPOINT);
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
- await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(dest), { recursive: true });
633
- await (0, import_promises2.writeFile)(dest, body);
703
+ return body;
634
704
  }
635
705
  async function dqlFetchOntology(client) {
636
- const response = await client.http.get(KG_ONTOLOGY_ENDPOINT);
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