@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/dist/index.d.ts CHANGED
@@ -28,6 +28,8 @@ interface DiffbotClientOptions {
28
28
  crawlerUrl?: string;
29
29
  webSearchUrl?: string;
30
30
  nlpUrl?: string;
31
+ dqlUrl?: string;
32
+ ontologyUrl?: string;
31
33
  fetch?: FetchFn;
32
34
  }
33
35
  declare class DiffbotClient {
@@ -37,14 +39,21 @@ declare class DiffbotClient {
37
39
  readonly crawlerUrl: string;
38
40
  readonly webSearchUrl: string;
39
41
  readonly nlpUrl: string;
42
+ readonly dqlUrl: string;
43
+ readonly ontologyUrl: string;
40
44
  readonly http: HttpClient;
41
45
  constructor(options: DiffbotClientOptions);
42
46
  close(): Promise<void>;
43
47
  [Symbol.asyncDispose](): Promise<void>;
44
48
  }
45
49
 
46
- /** Resolve a Diffbot API token from explicit arg, env var, or credentials file. */
47
- declare function resolveToken(token?: string | null): string;
50
+ type TokenSource = object;
51
+ /**
52
+ * Resolve a Diffbot API token from an explicit arg, then a Workers-style
53
+ * `env` object, then `process.env`. Does not touch the filesystem — for a
54
+ * credentials-file fallback, use `resolveToken` from `@diffbot/typescript/node`.
55
+ */
56
+ declare function resolveTokenFromEnv(token?: string | null, env?: TokenSource): string;
48
57
 
49
58
  type JsonObject = Record<string, unknown>;
50
59
 
@@ -54,7 +63,46 @@ interface ChatMessage {
54
63
  role: string;
55
64
  content: string;
56
65
  }
57
- declare function ask(client: DiffbotClient, messages: ChatMessage[]): AsyncGenerator<string>;
66
+ /** `response_format` types accepted by the Diffbot LLM endpoint. */
67
+ declare const RESPONSE_FORMAT_TYPES: readonly ["text", "json_object", "json_schema"];
68
+ type ResponseFormatType = (typeof RESPONSE_FORMAT_TYPES)[number];
69
+ interface ResponseFormat {
70
+ type: ResponseFormatType;
71
+ json_schema?: {
72
+ name?: string;
73
+ schema: JsonObject;
74
+ };
75
+ }
76
+ interface AskOptions {
77
+ /**
78
+ * Constrain the output, e.g. `{ type: "json_object" }` or `jsonSchemaFormat(schema)`.
79
+ * Prefer a schema: `json_object` applies no server-side grammar.
80
+ */
81
+ responseFormat?: ResponseFormat;
82
+ }
83
+ /**
84
+ * Build a `response_format` that constrains output to `schema`. The endpoint requires
85
+ * the schema nested under `json_schema.schema`; anywhere else is ignored server-side
86
+ * without an error, so prefer this helper over hand-building the object.
87
+ */
88
+ declare function jsonSchemaFormat(schema: JsonObject, name?: string): ResponseFormat;
89
+ /** Parse the model's final answer as JSON, tolerating think blocks and fences. */
90
+ declare function extractJson(text: string): unknown;
91
+ /**
92
+ * Schema used when the caller wants JSON but has no shape in mind. This goes through
93
+ * the json_schema path rather than `{ type: "json_object" }` on purpose: json_object
94
+ * applies no server-side grammar, so the RAG loop's internal <functioncall> JSON
95
+ * satisfies it and gets returned as the final answer. That is reproducible whenever
96
+ * the request carries a system message.
97
+ */
98
+ declare const ANY_OBJECT_SCHEMA: JsonObject;
99
+ declare function ask(client: DiffbotClient, messages: ChatMessage[], options?: AskOptions): AsyncGenerator<string>;
100
+ /**
101
+ * Ask the Diffbot LLM and return the answer parsed as JSON. With `schema` (a JSON Schema
102
+ * object) the model is constrained to it by grammar; without one it falls back to
103
+ * unconstrained JSON output. Pass `responseFormat` instead of `schema` for full control.
104
+ */
105
+ declare function askJson<T = unknown>(client: DiffbotClient, messages: ChatMessage[], schema?: JsonObject, options?: AskOptions): Promise<T>;
58
106
 
59
107
  declare enum CrawlEventType {
60
108
  JobCreated = "job_created",
@@ -92,7 +140,6 @@ declare class Ontology {
92
140
  readonly data: JsonObject;
93
141
  constructor(data: JsonObject);
94
142
  static fromJson(raw: string | Uint8Array): Ontology;
95
- static fromPath(path: string): Promise<Ontology>;
96
143
  types(): string[];
97
144
  composites(): string[];
98
145
  enums(): string[];
@@ -119,15 +166,122 @@ declare function dql(client: DiffbotClient, query: string, options?: DqlOptions)
119
166
  declare function dqlParallel(client: DiffbotClient, queries: Array<DqlOptions & {
120
167
  query: string;
121
168
  }>, workers?: number): Promise<Array<JsonObject | Uint8Array>>;
122
- declare function dqlRefreshOntology(client: DiffbotClient, dest: string): Promise<void>;
169
+ /** Fetch the raw ontology JSON text. For a parsed, cached `Ontology`, see `OntologyStore`. */
170
+ declare function dqlFetchOntologyText(client: DiffbotClient): Promise<string>;
123
171
  declare function dqlFetchOntology(client: DiffbotClient): Promise<Ontology>;
124
172
 
125
173
  declare function entities(client: DiffbotClient, text: string, lang?: string): Promise<JsonObject>;
126
174
 
127
- declare function webSearch(client: DiffbotClient, text: string, options?: {
175
+ interface OntologyStoreOptions {
176
+ /**
177
+ * How long a cached entry should be kept, in seconds. Default 86_400 (24h).
178
+ * The base `OntologyStore` doesn't expire its in-memory cache on its own —
179
+ * an explicit `refresh: true` is the only way to bust it — so this only
180
+ * takes effect in subclasses backed by a store with real expiration, like
181
+ * `KVOntologyStore` (where it becomes the KV entry's `expirationTtl`).
182
+ */
183
+ ttlSeconds?: number;
184
+ }
185
+ /**
186
+ * Fetches and caches the Diffbot KG ontology for a client. The base class
187
+ * caches only in memory, for the lifetime of the instance; subclasses
188
+ * override `read`/`write` to back the cache with a durable store (a
189
+ * filesystem, a KV namespace, ...).
190
+ */
191
+ declare class OntologyStore {
192
+ readonly client: DiffbotClient;
193
+ protected readonly ttlSeconds: number;
194
+ private inFlight;
195
+ constructor(client: DiffbotClient, options?: OntologyStoreOptions);
196
+ /**
197
+ * Resolve the ontology, from cache when possible. Concurrent calls share
198
+ * one in-flight fetch rather than issuing duplicate requests; a rejected
199
+ * fetch clears the in-flight memo so the next call retries instead of
200
+ * replaying the same failure forever.
201
+ */
202
+ load(options?: {
203
+ refresh?: boolean;
204
+ }): Promise<Ontology>;
205
+ private resolve;
206
+ /** Read the cached ontology text, or `undefined` on a miss. Memory-only by default. */
207
+ protected read(): Promise<string | undefined>;
208
+ /** Persist ontology text after a fetch. No-op by default. */
209
+ protected write(_text: string): Promise<void>;
210
+ }
211
+ /**
212
+ * Structurally typed rather than importing `@cloudflare/workers-types`, so
213
+ * the SDK takes no Cloudflare dependency — a `KVNamespace` binding satisfies
214
+ * this directly.
215
+ */
216
+ interface KVLike {
217
+ get(key: string): Promise<string | null>;
218
+ put(key: string, value: string, options?: {
219
+ expirationTtl?: number;
220
+ }): Promise<void>;
221
+ delete(key: string): Promise<void>;
222
+ }
223
+ interface KVOntologyStoreOptions extends OntologyStoreOptions {
224
+ /** Default `"diffbot:ontology:v1"`. Bump the suffix to force cache invalidation on a schema change. */
225
+ key?: string;
226
+ /**
227
+ * After this many seconds, a cached entry is served immediately while a
228
+ * refresh runs behind it (stale-while-revalidate) instead of being trusted
229
+ * outright. 0 (default) disables SWR — a cached entry is trusted for its
230
+ * full `ttlSeconds`. Should be less than `ttlSeconds`, or the KV entry
231
+ * expires before it is ever considered stale.
232
+ */
233
+ staleAfterSeconds?: number;
234
+ /**
235
+ * TTL on the refresh lock that prevents concurrent refetches (cron, SWR,
236
+ * manual). Default 60s — Cloudflare KV rejects `expirationTtl` below 60.
237
+ */
238
+ lockTtlSeconds?: number;
239
+ /**
240
+ * `ExecutionContext.waitUntil`. Without it, a background stale-while-revalidate
241
+ * refresh is killed as soon as the response is returned, so SWR degrades to a
242
+ * blocking refresh-on-expiry.
243
+ */
244
+ waitUntil?: (promise: Promise<unknown>) => void;
245
+ }
246
+ /**
247
+ * KV-backed ontology cache for Cloudflare Workers. Ports the stampede
248
+ * protection and stale-while-revalidate behavior of a production Worker's
249
+ * hand-rolled ontology cache: a short-lived lock key prevents concurrent
250
+ * refreshes from cron, SWR, and a manual refresh all refetching at once, and
251
+ * a `refreshed_at` companion key lets a stale entry be served immediately
252
+ * while a refresh runs in the background.
253
+ */
254
+ declare class KVOntologyStore extends OntologyStore {
255
+ private readonly kv;
256
+ private readonly key;
257
+ private readonly refreshedAtKey;
258
+ private readonly lockKey;
259
+ private readonly staleAfterSeconds;
260
+ private readonly lockTtlSeconds;
261
+ private readonly waitUntil;
262
+ constructor(client: DiffbotClient, kv: KVLike, options?: KVOntologyStoreOptions);
263
+ load(options?: {
264
+ refresh?: boolean;
265
+ }): Promise<Ontology>;
266
+ private loadWithStaleWhileRevalidate;
267
+ protected read(): Promise<string | undefined>;
268
+ protected write(text: string): Promise<void>;
269
+ }
270
+
271
+ /** How much of each page's matching content the web search returns in `content`. */
272
+ type SnippetLength = "short" | "medium" | "long" | "full";
273
+ declare const SNIPPET_LENGTHS: readonly SnippetLength[];
274
+ interface WebSearchOptions {
128
275
  numResults?: number;
129
276
  maxTokens?: number;
130
- }): Promise<JsonObject>;
277
+ /**
278
+ * `short` keeps a result to ~1-1.5 KB; `long` is the API default; `full` returns the
279
+ * whole stored page and is meant for `url:<URL>` queries. The reranker always chunks,
280
+ * so `full` also sends `rerank=none` and query terms after the URL are ignored.
281
+ */
282
+ snippetLength?: SnippetLength;
283
+ }
284
+ declare function webSearch(client: DiffbotClient, text: string, options?: WebSearchOptions): Promise<JsonObject>;
131
285
 
132
286
  /** Exception hierarchy for the Diffbot SDK. */
133
287
  declare class DiffbotError extends Error {
@@ -158,6 +312,6 @@ declare class ExtractionError extends DiffbotError {
158
312
  constructor(errorCode: number, error: string);
159
313
  }
160
314
 
161
- declare const VERSION = "0.1.0";
315
+ declare const VERSION = "0.3.0";
162
316
 
163
- export { APIError, AuthError, type ChatMessage, type CrawlEvent, CrawlEventType, type CrawlOptions, DiffbotClient, type DiffbotClientOptions, DiffbotError, type DqlOptions, ExtractionError, type FetchFn, type JsonObject, Ontology, RateLimitError, VERSION, ValidationError, ask, crawl, crawlDeleteJob, crawlGetJob, crawlListJobs, dql, dqlFetchOntology, dqlParallel, dqlRefreshOntology, entities, extract, resolveToken, webSearch };
317
+ export { ANY_OBJECT_SCHEMA, APIError, type AskOptions, AuthError, type ChatMessage, type CrawlEvent, CrawlEventType, type CrawlOptions, DiffbotClient, type DiffbotClientOptions, DiffbotError, type DqlOptions, ExtractionError, type FetchFn, type JsonObject, type KVLike, KVOntologyStore, type KVOntologyStoreOptions, Ontology, OntologyStore, type OntologyStoreOptions, RESPONSE_FORMAT_TYPES, RateLimitError, type ResponseFormat, type ResponseFormatType, SNIPPET_LENGTHS, type SnippetLength, type TokenSource, VERSION, ValidationError, type WebSearchOptions, ask, askJson, crawl, crawlDeleteJob, crawlGetJob, crawlListJobs, dql, dqlFetchOntology, dqlFetchOntologyText, dqlParallel, entities, extract, extractJson, jsonSchemaFormat, resolveTokenFromEnv, webSearch };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/constants.ts
2
- var VERSION = "0.1.0";
2
+ var VERSION = "0.3.0";
3
3
  var DEFAULT_TIMEOUT = 3e4;
4
4
  var EXTRACT_BASE = "https://api.diffbot.com/v3";
5
5
  var CRAWL_BASE = "https://api.diffbot.com/v3/crawl";
@@ -140,6 +140,8 @@ var DiffbotClient = class {
140
140
  crawlerUrl;
141
141
  webSearchUrl;
142
142
  nlpUrl;
143
+ dqlUrl;
144
+ ontologyUrl;
143
145
  http;
144
146
  constructor(options) {
145
147
  if (!options.token) {
@@ -151,6 +153,8 @@ var DiffbotClient = class {
151
153
  this.crawlerUrl = options.crawlerUrl ?? CRAWL_BASE;
152
154
  this.webSearchUrl = options.webSearchUrl ?? WEB_SEARCH_BASE;
153
155
  this.nlpUrl = options.nlpUrl ?? NLP_BASE;
156
+ this.dqlUrl = options.dqlUrl ?? KG_DQL_ENDPOINT;
157
+ this.ontologyUrl = options.ontologyUrl ?? KG_ONTOLOGY_ENDPOINT;
154
158
  this.http = new HttpClient(
155
159
  options.fetch ?? fetch,
156
160
  options.timeout ?? DEFAULT_TIMEOUT,
@@ -166,34 +170,20 @@ var DiffbotClient = class {
166
170
  };
167
171
 
168
172
  // src/auth.ts
169
- import { existsSync, readFileSync } from "fs";
170
- import { homedir } from "os";
171
- import { join } from "path";
172
- var authConfig = {
173
- credentialsPath: join(homedir(), ".diffbot", "credentials")
174
- };
175
- function readCredentialsFile(path = authConfig.credentialsPath) {
176
- if (!existsSync(path)) {
177
- return "";
178
- }
179
- const text = readFileSync(path, "utf8");
180
- for (const line of text.split("\n")) {
181
- const trimmed = line.trim();
182
- if (trimmed.startsWith(`${TOKEN_ENV_VAR}=`)) {
183
- return trimmed.slice(TOKEN_ENV_VAR.length + 1).trim();
184
- }
185
- }
186
- return "";
173
+ function envToken(env) {
174
+ const fromEnv = env?.[TOKEN_ENV_VAR];
175
+ if (typeof fromEnv === "string" && fromEnv.trim()) {
176
+ return fromEnv.trim();
177
+ }
178
+ const proc = globalThis.process;
179
+ const fromProcess = proc?.env?.[TOKEN_ENV_VAR];
180
+ return fromProcess?.trim() ?? "";
187
181
  }
188
- function resolveToken(token) {
182
+ function resolveTokenFromEnv(token, env) {
189
183
  if (token?.trim()) {
190
184
  return token.trim();
191
185
  }
192
- const envToken = process.env[TOKEN_ENV_VAR]?.trim();
193
- if (envToken) {
194
- return envToken;
195
- }
196
- return readCredentialsFile();
186
+ return envToken(env);
197
187
  }
198
188
 
199
189
  // src/extract.ts
@@ -231,6 +221,80 @@ async function extract(client, url, api = "analyze", fmt = "markdown") {
231
221
  }
232
222
 
233
223
  // src/ask.ts
224
+ var MODEL = "diffbot-small-xl";
225
+ var RESPONSE_FORMAT_TYPES = ["text", "json_object", "json_schema"];
226
+ function jsonSchemaFormat(schema, name = "response") {
227
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
228
+ throw new ValidationError("schema must be a JSON Schema object");
229
+ }
230
+ return { type: "json_schema", json_schema: { name, schema } };
231
+ }
232
+ function validateResponseFormat(responseFormat) {
233
+ if (responseFormat === void 0) return;
234
+ if (responseFormat === null || typeof responseFormat !== "object" || Array.isArray(responseFormat)) {
235
+ throw new ValidationError("responseFormat must be an object");
236
+ }
237
+ const fmtType = responseFormat.type ?? "text";
238
+ if (!RESPONSE_FORMAT_TYPES.includes(fmtType)) {
239
+ throw new ValidationError(
240
+ `responseFormat type must be one of ${RESPONSE_FORMAT_TYPES.join(", ")}; got ${JSON.stringify(fmtType)}`
241
+ );
242
+ }
243
+ if (fmtType === "json_schema") {
244
+ const jsonSchema = responseFormat.json_schema;
245
+ if (jsonSchema === null || typeof jsonSchema !== "object" || !("schema" in jsonSchema)) {
246
+ throw new ValidationError(
247
+ '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.'
248
+ );
249
+ }
250
+ }
251
+ }
252
+ var THINK_BLOCK = /<think>[\s\S]*?<\/think>/g;
253
+ var JSON_FENCE = /^```(?:json)?\s*|\s*```$/gm;
254
+ function extractJson(text) {
255
+ let cleaned = text.replace(THINK_BLOCK, "").trim();
256
+ cleaned = cleaned.replace(JSON_FENCE, "").trim();
257
+ try {
258
+ return JSON.parse(cleaned);
259
+ } catch {
260
+ }
261
+ const spans = [];
262
+ for (const [opener, closer] of [
263
+ ["{", "}"],
264
+ ["[", "]"]
265
+ ]) {
266
+ const start = cleaned.indexOf(opener);
267
+ const end = cleaned.lastIndexOf(closer);
268
+ if (start !== -1 && end > start) spans.push([start, cleaned.slice(start, end + 1)]);
269
+ }
270
+ spans.sort((a, b) => a[0] - b[0]);
271
+ for (const [, span] of spans) {
272
+ try {
273
+ return JSON.parse(span);
274
+ } catch {
275
+ continue;
276
+ }
277
+ }
278
+ throw new ValidationError(
279
+ `could not parse JSON from the model response: ${JSON.stringify(text.slice(0, 200))}`
280
+ );
281
+ }
282
+ var ANY_OBJECT_SCHEMA = { type: "object" };
283
+ function resolveFormat(schema, responseFormat) {
284
+ if (schema !== void 0 && responseFormat !== void 0) {
285
+ throw new ValidationError("pass either schema or responseFormat, not both");
286
+ }
287
+ if (responseFormat !== void 0) return responseFormat;
288
+ return jsonSchemaFormat(schema ?? ANY_OBJECT_SCHEMA);
289
+ }
290
+ function checkToolCallLeak(parsed) {
291
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.name === "functioncall" && "arguments" in parsed) {
292
+ throw new ValidationError(
293
+ '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.'
294
+ );
295
+ }
296
+ return parsed;
297
+ }
234
298
  function parseChunk(line) {
235
299
  try {
236
300
  const chunk = JSON.parse(line.replace(/^data: /, ""));
@@ -261,9 +325,11 @@ async function* linesFromStream(body) {
261
325
  }
262
326
  if (buffer) yield buffer;
263
327
  }
264
- async function* ask(client, messages) {
328
+ async function* ask(client, messages, options = {}) {
329
+ validateResponseFormat(options.responseFormat);
265
330
  const headers = { Authorization: `Bearer ${client.token}` };
266
- const payload = { model: "diffbot-small-xl", messages, stream: true };
331
+ const payload = { model: MODEL, messages, stream: true };
332
+ if (options.responseFormat !== void 0) payload.response_format = options.responseFormat;
267
333
  const response = await client.http.post(client.llmUrl, { headers, json: payload });
268
334
  if (!response.ok) {
269
335
  const body = await readBodyText(response);
@@ -283,6 +349,12 @@ async function* ask(client, messages) {
283
349
  if (content) yield content;
284
350
  }
285
351
  }
352
+ async function askJson(client, messages, schema, options = {}) {
353
+ const responseFormat = resolveFormat(schema, options.responseFormat);
354
+ let text = "";
355
+ for await (const chunk of ask(client, messages, { responseFormat })) text += chunk;
356
+ return checkToolCallLeak(extractJson(text));
357
+ }
286
358
 
287
359
  // src/crawl.ts
288
360
  var CrawlEventType = /* @__PURE__ */ ((CrawlEventType2) => {
@@ -422,12 +494,7 @@ async function crawlDeleteJob(client, jobName) {
422
494
  raiseForStatus(response, body);
423
495
  }
424
496
 
425
- // src/kg.ts
426
- import { mkdir, writeFile } from "fs/promises";
427
- import { dirname } from "path";
428
-
429
497
  // src/ontology.ts
430
- import { readFile } from "fs/promises";
431
498
  var Ontology = class _Ontology {
432
499
  data;
433
500
  constructor(data) {
@@ -437,10 +504,6 @@ var Ontology = class _Ontology {
437
504
  const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
438
505
  return new _Ontology(JSON.parse(text));
439
506
  }
440
- static async fromPath(path) {
441
- const text = await readFile(path, "utf8");
442
- return _Ontology.fromJson(text);
443
- }
444
507
  types() {
445
508
  const types = this.data.types;
446
509
  return Object.keys(types ?? {}).sort();
@@ -553,7 +616,7 @@ function buildDqlParams(client, query, options) {
553
616
  }
554
617
  async function dql(client, query, options = {}) {
555
618
  const params = buildDqlParams(client, query, options);
556
- const response = await client.http.get(KG_DQL_ENDPOINT, { params });
619
+ const response = await client.http.get(client.dqlUrl, { params });
557
620
  const body = await readBodyText(response);
558
621
  raiseForStatus(response, body);
559
622
  if (options.raw) {
@@ -577,18 +640,14 @@ async function dqlParallel(client, queries, workers = 8) {
577
640
  await Promise.all(Array.from({ length: limit }, () => worker()));
578
641
  return results;
579
642
  }
580
- async function dqlRefreshOntology(client, dest) {
581
- const response = await client.http.get(KG_ONTOLOGY_ENDPOINT);
643
+ async function dqlFetchOntologyText(client) {
644
+ const response = await client.http.get(client.ontologyUrl);
582
645
  const body = await readBodyText(response);
583
646
  raiseForStatus(response, body);
584
- await mkdir(dirname(dest), { recursive: true });
585
- await writeFile(dest, body);
647
+ return body;
586
648
  }
587
649
  async function dqlFetchOntology(client) {
588
- const response = await client.http.get(KG_ONTOLOGY_ENDPOINT);
589
- const body = await readBodyText(response);
590
- raiseForStatus(response, body);
591
- return Ontology.fromJson(body);
650
+ return Ontology.fromJson(await dqlFetchOntologyText(client));
592
651
  }
593
652
 
594
653
  // src/nlp.ts
@@ -603,40 +662,176 @@ async function entities(client, text, lang = "auto") {
603
662
  return Array.isArray(data) ? data[0] ?? {} : data;
604
663
  }
605
664
 
665
+ // src/ontology-store.ts
666
+ var OntologyStore = class {
667
+ client;
668
+ ttlSeconds;
669
+ inFlight = null;
670
+ constructor(client, options = {}) {
671
+ this.client = client;
672
+ this.ttlSeconds = options.ttlSeconds ?? 86400;
673
+ }
674
+ /**
675
+ * Resolve the ontology, from cache when possible. Concurrent calls share
676
+ * one in-flight fetch rather than issuing duplicate requests; a rejected
677
+ * fetch clears the in-flight memo so the next call retries instead of
678
+ * replaying the same failure forever.
679
+ */
680
+ load(options = {}) {
681
+ if (options.refresh) {
682
+ this.inFlight = null;
683
+ }
684
+ if (!this.inFlight) {
685
+ this.inFlight = this.resolve(options.refresh ?? false).catch((error) => {
686
+ this.inFlight = null;
687
+ throw error;
688
+ });
689
+ }
690
+ return this.inFlight;
691
+ }
692
+ async resolve(refresh) {
693
+ if (!refresh) {
694
+ const cached = await this.read();
695
+ if (cached !== void 0) {
696
+ try {
697
+ return Ontology.fromJson(cached);
698
+ } catch {
699
+ }
700
+ }
701
+ }
702
+ const text = await dqlFetchOntologyText(this.client);
703
+ await this.write(text);
704
+ return Ontology.fromJson(text);
705
+ }
706
+ /** Read the cached ontology text, or `undefined` on a miss. Memory-only by default. */
707
+ read() {
708
+ return Promise.resolve(void 0);
709
+ }
710
+ /** Persist ontology text after a fetch. No-op by default. */
711
+ write(_text) {
712
+ return Promise.resolve();
713
+ }
714
+ };
715
+ var KVOntologyStore = class extends OntologyStore {
716
+ kv;
717
+ key;
718
+ refreshedAtKey;
719
+ lockKey;
720
+ staleAfterSeconds;
721
+ lockTtlSeconds;
722
+ waitUntil;
723
+ constructor(client, kv, options = {}) {
724
+ super(client, options);
725
+ this.kv = kv;
726
+ this.key = options.key ?? "diffbot:ontology:v1";
727
+ this.refreshedAtKey = `${this.key}:refreshed_at`;
728
+ this.lockKey = `${this.key}:lock`;
729
+ this.staleAfterSeconds = options.staleAfterSeconds ?? 0;
730
+ this.lockTtlSeconds = options.lockTtlSeconds ?? 60;
731
+ this.waitUntil = options.waitUntil;
732
+ }
733
+ load(options = {}) {
734
+ if (!options.refresh && this.staleAfterSeconds > 0) {
735
+ return this.loadWithStaleWhileRevalidate();
736
+ }
737
+ return super.load(options);
738
+ }
739
+ async loadWithStaleWhileRevalidate() {
740
+ const [cached, refreshedAt] = await Promise.all([
741
+ this.kv.get(this.key),
742
+ this.kv.get(this.refreshedAtKey)
743
+ ]);
744
+ if (cached === null) {
745
+ return super.load();
746
+ }
747
+ const age = refreshedAt ? (Date.now() - Number(refreshedAt)) / 1e3 : Infinity;
748
+ const isStale = age > this.staleAfterSeconds;
749
+ const ontology = Ontology.fromJson(cached);
750
+ if (isStale) {
751
+ const refresh = super.load({ refresh: true }).catch(() => {
752
+ });
753
+ if (this.waitUntil) {
754
+ this.waitUntil(refresh);
755
+ } else {
756
+ await refresh;
757
+ return super.load();
758
+ }
759
+ }
760
+ return ontology;
761
+ }
762
+ async read() {
763
+ const cached = await this.kv.get(this.key);
764
+ return cached ?? void 0;
765
+ }
766
+ async write(text) {
767
+ if (await this.kv.get(this.lockKey)) {
768
+ return;
769
+ }
770
+ await this.kv.put(this.lockKey, "1", { expirationTtl: this.lockTtlSeconds });
771
+ try {
772
+ await this.kv.put(this.key, text, { expirationTtl: this.ttlSeconds });
773
+ await this.kv.put(this.refreshedAtKey, String(Date.now()), {
774
+ expirationTtl: this.ttlSeconds
775
+ });
776
+ } finally {
777
+ await this.kv.delete(this.lockKey);
778
+ }
779
+ }
780
+ };
781
+
606
782
  // src/web-search.ts
783
+ var SNIPPET_LENGTHS = ["short", "medium", "long", "full"];
607
784
  async function webSearch(client, text, options = {}) {
608
785
  const headers = { Authorization: `Bearer ${client.token}` };
609
786
  const params = { text };
610
787
  if (options.numResults !== void 0) params.size = options.numResults;
611
788
  if (options.maxTokens !== void 0) params.maxTokens = options.maxTokens;
789
+ if (options.snippetLength !== void 0) {
790
+ const length = options.snippetLength.toLowerCase();
791
+ if (!SNIPPET_LENGTHS.includes(length)) {
792
+ throw new ValidationError(
793
+ `snippetLength must be one of ${SNIPPET_LENGTHS.join(", ")}; got ${JSON.stringify(options.snippetLength)}`
794
+ );
795
+ }
796
+ params.snippetLength = length.toUpperCase();
797
+ if (length === "full") params.rerank = "none";
798
+ }
612
799
  const response = await client.http.get(client.webSearchUrl, { headers, params });
613
800
  const body = await readBodyText(response);
614
801
  raiseForStatus(response, body);
615
802
  return JSON.parse(body);
616
803
  }
617
804
  export {
805
+ ANY_OBJECT_SCHEMA,
618
806
  APIError,
619
807
  AuthError,
620
808
  CrawlEventType,
621
809
  DiffbotClient,
622
810
  DiffbotError,
623
811
  ExtractionError,
812
+ KVOntologyStore,
624
813
  Ontology,
814
+ OntologyStore,
815
+ RESPONSE_FORMAT_TYPES,
625
816
  RateLimitError,
817
+ SNIPPET_LENGTHS,
626
818
  VERSION,
627
819
  ValidationError,
628
820
  ask,
821
+ askJson,
629
822
  crawl,
630
823
  crawlDeleteJob,
631
824
  crawlGetJob,
632
825
  crawlListJobs,
633
826
  dql,
634
827
  dqlFetchOntology,
828
+ dqlFetchOntologyText,
635
829
  dqlParallel,
636
- dqlRefreshOntology,
637
830
  entities,
638
831
  extract,
639
- resolveToken,
832
+ extractJson,
833
+ jsonSchemaFormat,
834
+ resolveTokenFromEnv,
640
835
  webSearch
641
836
  };
642
837
  //# sourceMappingURL=index.js.map