@dbx-tools/appkit-web-search 0.9.154 → 0.9.158

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/src/config.ts CHANGED
@@ -45,9 +45,9 @@
45
45
  */
46
46
 
47
47
  import { ConfigurationError, type BasePluginConfig } from "@databricks/appkit";
48
- import { configUtils } from "@dbx-tools/core";
49
- import { modelCatalog } from "@dbx-tools/model";
50
- import { json, object, type OneOrMany, stringUtils } from "@dbx-tools/shared-core";
48
+ import { configUtils, text, ENV_ONLY, boolean, positiveNumber, positiveInt } from "@dbx-tools/core";
49
+ import { DEFAULT_FUZZY_THRESHOLD } from "@dbx-tools/model";
50
+ import { type OneOrMany, parseRecord, isRecord, trimToNull } from "@dbx-tools/shared-core";
51
51
  import type { JSONSchema7 } from "json-schema";
52
52
  import { parseAllowedUrls, toUrlAllowList, type UrlAllowList } from "./allowlist.ts";
53
53
 
@@ -297,9 +297,9 @@ export const WEB_SEARCH_CONFIG_SCHEMA: JSONSchema7 = {
297
297
  * silently leave the built-in tool specs in place, so it throws.
298
298
  */
299
299
  function parseToolsEnv(): Record<string, unknown> {
300
- const raw = configUtils.text("WEB_SEARCH_TOOLS", configUtils.ENV_ONLY);
300
+ const raw = text("WEB_SEARCH_TOOLS", ENV_ONLY);
301
301
  if (raw === undefined) return {};
302
- const parsed = json.parseRecord(raw);
302
+ const parsed = parseRecord(raw);
303
303
  if (!parsed) {
304
304
  throw new ConfigurationError(
305
305
  "WEB_SEARCH_TOOLS must be a JSON object mapping a provider family to its request fragment",
@@ -311,11 +311,11 @@ function parseToolsEnv(): Record<string, unknown> {
311
311
 
312
312
  /** Read the pinned endpoint id and record which source supplied it. */
313
313
  function resolveModelPin(config: WebSearchPluginConfig): { model?: string; source: ModelSource } {
314
- const fromConfig = stringUtils.trimToNull(config.model);
314
+ const fromConfig = trimToNull(config.model);
315
315
  if (fromConfig !== null) return { model: fromConfig, source: "config" };
316
- const fromModelEnv = configUtils.text(MODEL_ENV, configUtils.ENV_ONLY);
316
+ const fromModelEnv = text(MODEL_ENV, ENV_ONLY);
317
317
  if (fromModelEnv !== undefined) return { model: fromModelEnv, source: MODEL_ENV };
318
- const fromResourceEnv = configUtils.text(SERVING_ENDPOINT_ENV, configUtils.ENV_ONLY);
318
+ const fromResourceEnv = text(SERVING_ENDPOINT_ENV, ENV_ONLY);
319
319
  if (fromResourceEnv !== undefined) {
320
320
  return { model: fromResourceEnv, source: SERVING_ENDPOINT_ENV };
321
321
  }
@@ -330,7 +330,7 @@ function resolveUrlPolicy(
330
330
  configured: UrlPolicyMode | undefined,
331
331
  patterns: readonly string[],
332
332
  ): UrlPolicyMode {
333
- const raw = configured ?? configUtils.text("WEB_SEARCH_URL_POLICY", configUtils.ENV_ONLY);
333
+ const raw = configured ?? text("WEB_SEARCH_URL_POLICY", ENV_ONLY);
334
334
  if (raw === undefined) return patterns.length > 0 ? "allowlist" : "unrestricted";
335
335
  if (raw !== "allowlist" && raw !== "unrestricted") {
336
336
  throw new ConfigurationError(
@@ -362,7 +362,7 @@ function resolveUrlPolicy(
362
362
  export function toApprovalPolicy(gate: ApprovalGate | ApprovalPolicy | undefined): ApprovalPolicy {
363
363
  if (gate === undefined || gate === false) return { mode: "none" };
364
364
  if (gate === true) return { mode: "always" };
365
- if (object.isRecord(gate) && "mode" in gate) {
365
+ if (isRecord(gate) && "mode" in gate) {
366
366
  if (gate.mode === "urls") {
367
367
  const patterns = parseAllowedUrls([...gate.patterns]);
368
368
  return patterns.length > 0 ? { mode: "urls", patterns } : { mode: "none" };
@@ -388,7 +388,7 @@ export function resolveWebSearchConfig(
388
388
  config: WebSearchPluginConfig = {},
389
389
  ): ResolvedWebSearchConfig {
390
390
  const patterns = parseAllowedUrls(
391
- config.allowedUrls ?? configUtils.text("WEB_SEARCH_ALLOWED_URLS", configUtils.ENV_ONLY),
391
+ config.allowedUrls ?? text("WEB_SEARCH_ALLOWED_URLS", ENV_ONLY),
392
392
  );
393
393
  const urlPolicy = resolveUrlPolicy(config.urlPolicy, patterns);
394
394
  const { model, source } = resolveModelPin(config);
@@ -396,45 +396,34 @@ export function resolveWebSearchConfig(
396
396
  config.modelFallbacks,
397
397
  "WEB_SEARCH_MODEL_FALLBACKS",
398
398
  undefined,
399
- configUtils.ENV_ONLY,
399
+ ENV_ONLY,
400
400
  );
401
401
  return {
402
402
  ...(model ? { model } : {}),
403
403
  modelSource: source,
404
404
  modelFallbacks: fallbacks.length > 0 ? fallbacks : DEFAULT_MODEL_FALLBACKS,
405
405
  webSearchTools: { ...parseToolsEnv(), ...(config.webSearchTools ?? {}) },
406
- fuzzy:
407
- configUtils.boolean(config.modelFuzzyMatch, "WEB_SEARCH_FUZZY", configUtils.ENV_ONLY) ?? true,
408
- fuzzyThreshold: configUtils.positiveNumber(
406
+ fuzzy: boolean(config.modelFuzzyMatch, "WEB_SEARCH_FUZZY", ENV_ONLY) ?? true,
407
+ fuzzyThreshold: positiveNumber(
409
408
  config.modelFuzzyThreshold,
410
409
  "WEB_SEARCH_FUZZY_THRESHOLD",
411
- modelCatalog.DEFAULT_FUZZY_THRESHOLD,
412
- configUtils.ENV_ONLY,
410
+ DEFAULT_FUZZY_THRESHOLD,
411
+ ENV_ONLY,
413
412
  ),
414
- maxCitations: configUtils.positiveInt(
413
+ maxCitations: positiveInt(
415
414
  config.maxCitations,
416
415
  "WEB_SEARCH_MAX_CITATIONS",
417
416
  DEFAULT_MAX_CITATIONS,
418
- configUtils.ENV_ONLY,
417
+ ENV_ONLY,
419
418
  ),
420
- fetchMaxLength: configUtils.positiveInt(
419
+ fetchMaxLength: positiveInt(
421
420
  config.fetchMaxLength,
422
421
  "WEB_SEARCH_FETCH_MAX_LENGTH",
423
422
  DEFAULT_FETCH_MAX_LENGTH,
424
- configUtils.ENV_ONLY,
423
+ ENV_ONLY,
425
424
  ),
426
- timeoutMs: configUtils.positiveInt(
427
- config.timeoutMs,
428
- "WEB_SEARCH_TIMEOUT_MS",
429
- DEFAULT_TIMEOUT_MS,
430
- configUtils.ENV_ONLY,
431
- ),
432
- scrapeFallback:
433
- configUtils.boolean(
434
- config.scrapeFallback,
435
- "WEB_SEARCH_SCRAPE_FALLBACK",
436
- configUtils.ENV_ONLY,
437
- ) ?? true,
425
+ timeoutMs: positiveInt(config.timeoutMs, "WEB_SEARCH_TIMEOUT_MS", DEFAULT_TIMEOUT_MS, ENV_ONLY),
426
+ scrapeFallback: boolean(config.scrapeFallback, "WEB_SEARCH_SCRAPE_FALLBACK", ENV_ONLY) ?? true,
438
427
  urlPolicy,
439
428
  allowList: toUrlAllowList(urlPolicy === "allowlist" ? patterns : []),
440
429
  approval: toApprovalPolicy(config.approval),
package/src/plugin.ts CHANGED
@@ -23,8 +23,8 @@ import {
23
23
  } from "@databricks/appkit";
24
24
  import { defineTool, type ToolRegistry } from "@databricks/appkit/beta";
25
25
  import { ToolRegistryPlugin } from "@dbx-tools/appkit";
26
- import type { AgentToolExecutionContext } from "@dbx-tools/appkit/tool-provider";
27
- import { log, stringUtils } from "@dbx-tools/shared-core";
26
+ import type { AgentToolExecutionContext } from "@dbx-tools/appkit";
27
+ import { log, trimToNull } from "@dbx-tools/shared-core";
28
28
  import {
29
29
  MODEL_ENV,
30
30
  SERVING_ENDPOINT_ENV,
@@ -134,9 +134,9 @@ export class WebSearchPlugin extends ToolRegistryPlugin<WebSearchPluginConfig> {
134
134
  */
135
135
  static getResourceRequirements(config: WebSearchPluginConfig): ResourceRequirement[] {
136
136
  const pinned =
137
- stringUtils.trimToNull(config.model) ??
138
- stringUtils.trimToNull(process.env[MODEL_ENV]) ??
139
- stringUtils.trimToNull(process.env[SERVING_ENDPOINT_ENV]);
137
+ trimToNull(config.model) ??
138
+ trimToNull(process.env[MODEL_ENV]) ??
139
+ trimToNull(process.env[SERVING_ENDPOINT_ENV]);
140
140
  return pinned === null ? [] : [{ ...SERVING_ENDPOINT_RESOURCE, required: true }];
141
141
  }
142
142
 
package/src/runtime.ts CHANGED
@@ -9,9 +9,9 @@
9
9
  */
10
10
 
11
11
  import type { ExecutionResult } from "@databricks/appkit";
12
- import { pluginExecution } from "@dbx-tools/appkit";
12
+ import { runPluginExecution } from "@dbx-tools/appkit";
13
13
  import { policy } from "@dbx-tools/model";
14
- import { execution, log } from "@dbx-tools/shared-core";
14
+ import { log, directExecutor } from "@dbx-tools/shared-core";
15
15
  import {
16
16
  resolveWebSearchConfig,
17
17
  type ResolvedWebSearchConfig,
@@ -51,7 +51,7 @@ export type WebSearchRuntimeInput = WebSearchRuntime | ResolvedWebSearchConfig;
51
51
  * onto the same {@link ExecutionResult} shape so call sites branch on `ok`
52
52
  * either way.
53
53
  */
54
- const directExecute = execution.directExecutor<WebSearchExecutionSettings>();
54
+ const directExecute = directExecutor<WebSearchExecutionSettings>();
55
55
 
56
56
  /** Build an isolated runtime for one plugin instance or standalone tool set. */
57
57
  export function createWebSearchRuntime(
@@ -109,7 +109,7 @@ export async function executeRead<T>(
109
109
  signal?: AbortSignal,
110
110
  logFailure = true,
111
111
  ): Promise<T> {
112
- return pluginExecution.runPluginExecution({
112
+ return runPluginExecution({
113
113
  plugin: "web-search",
114
114
  logger: logFailure ? logger : { warn() {} },
115
115
  operation,
package/src/schema.ts CHANGED
@@ -17,14 +17,14 @@
17
17
  * @module
18
18
  */
19
19
 
20
- import { stringUtils } from "@dbx-tools/shared-core";
20
+ import { toDescription } from "@dbx-tools/shared-core";
21
21
  import { z } from "zod";
22
22
 
23
23
  /**
24
24
  * Description the model reads for `web_search`. Shared by the Mastra tool and
25
25
  * the AppKit tool provider so both hosts describe the tool identically.
26
26
  */
27
- export const WEB_SEARCH_TOOL_DESCRIPTION = stringUtils.toDescription(`
27
+ export const WEB_SEARCH_TOOL_DESCRIPTION = toDescription(`
28
28
  Search the web for current information and get an answer synthesized from
29
29
  live results, with the sources it used. Pass a natural-language query;
30
30
  the search runs inside a web-search-capable model (chosen independently
@@ -34,7 +34,7 @@ export const WEB_SEARCH_TOOL_DESCRIPTION = stringUtils.toDescription(`
34
34
  `);
35
35
 
36
36
  /** Description the model reads for `web_fetch`. Shared like {@link WEB_SEARCH_TOOL_DESCRIPTION}. */
37
- export const WEB_FETCH_TOOL_DESCRIPTION = stringUtils.toDescription(`
37
+ export const WEB_FETCH_TOOL_DESCRIPTION = toDescription(`
38
38
  Fetch a single web page and return its readable contents. Pass an
39
39
  absolute URL (including https://); set format to "html" for raw markup
40
40
  instead of extracted text. Use it to read a page returned by web_search
@@ -53,7 +53,7 @@ export const webSearchRequestSchema = z.object({
53
53
  .string()
54
54
  .optional()
55
55
  .describe(
56
- stringUtils.toDescription(`
56
+ toDescription(`
57
57
  Optional calling-model preference. The tool always tries the highest
58
58
  web-search-capable Gemini first. When Gemini is unavailable, a matching
59
59
  GPT endpoint wins before the highest-ranked GPT fallback.
@@ -108,7 +108,7 @@ export const webFetchRequestSchema = z.object({
108
108
  .enum(["text", "html"])
109
109
  .optional()
110
110
  .describe(
111
- stringUtils.toDescription(`
111
+ toDescription(`
112
112
  Return format: "text" (default) strips the page to readable plain
113
113
  text; "html" returns the raw response body. Prefer "text" unless you
114
114
  need the markup.
package/src/search.ts CHANGED
@@ -27,10 +27,14 @@ import {
27
27
  ExecutionError,
28
28
  getExecutionContext,
29
29
  } from "@databricks/appkit";
30
- import type { AgentToolExecutionContext } from "@dbx-tools/appkit/tool-provider";
31
- import { invoke, policy, resolve, modelCatalog } from "@dbx-tools/model";
32
- import { log, object, stringUtils } from "@dbx-tools/shared-core";
33
- import { openaiChat, type ServingEndpointSummary } from "@dbx-tools/shared-model";
30
+ import type { AgentToolExecutionContext } from "@dbx-tools/appkit";
31
+ import { invoke, policy, selection, modelCatalog } from "@dbx-tools/model";
32
+ import { log, isRecord, trimToEmpty } from "@dbx-tools/shared-core";
33
+ import {
34
+ type ServingEndpointSummary,
35
+ chatContentParts,
36
+ chatContentToText,
37
+ } from "@dbx-tools/shared-model";
34
38
  import { MODEL_ENV, type ResolvedWebSearchConfig } from "./config.ts";
35
39
  import { toCallSettings, webSearchExecuteDefaults } from "./defaults.ts";
36
40
  import { supportsWebSearch, webSearchProviderForFamily, webSearchToolSpec } from "./provider.ts";
@@ -46,7 +50,7 @@ import { runScrapeSearch } from "./scrape.ts";
46
50
  type WorkspaceClientLike = modelCatalog.WorkspaceClientLike & invoke.AuthenticatingClientLike;
47
51
  type ProgressWriter = NonNullable<AgentToolExecutionContext["writeProgress"]>;
48
52
  const logger = log.logger("web-search/search");
49
- const { lookupModels } = resolve;
53
+ const { lookupModels } = selection;
50
54
  const { listServingEndpoints } = modelCatalog;
51
55
 
52
56
  /**
@@ -229,8 +233,7 @@ async function postServing(
229
233
  });
230
234
  if (!response.ok) {
231
235
  const responseBody = await response.text();
232
- const model =
233
- object.isRecord(body) && typeof body.model === "string" ? body.model : undefined;
236
+ const model = isRecord(body) && typeof body.model === "string" ? body.model : undefined;
234
237
  const crossRegionDisabled =
235
238
  response.status === 400 &&
236
239
  model &&
@@ -267,7 +270,7 @@ async function postServing(
267
270
  signal,
268
271
  false,
269
272
  );
270
- return object.isRecord(payload) ? payload : {};
273
+ return isRecord(payload) ? payload : {};
271
274
  }
272
275
 
273
276
  /** Consume an OpenAI Responses SSE stream and retain its completed response. */
@@ -287,9 +290,9 @@ async function readStreamingResponse(
287
290
  .join("\n");
288
291
  if (!data || data === "[DONE]") return;
289
292
  const event = JSON.parse(data) as unknown;
290
- if (!object.isRecord(event) || typeof event.type !== "string") return;
293
+ if (!isRecord(event) || typeof event.type !== "string") return;
291
294
  await emitWebSearchProgress(event, writeProgress);
292
- if (event.type === "response.completed" && object.isRecord(event.response)) {
295
+ if (event.type === "response.completed" && isRecord(event.response)) {
293
296
  completed = event.response;
294
297
  }
295
298
  if (event.type === "response.failed") {
@@ -321,8 +324,8 @@ async function emitWebSearchProgress(
321
324
  event: Record<string, unknown>,
322
325
  writeProgress: ProgressWriter,
323
326
  ): Promise<void> {
324
- if (event.type !== "response.output_item.done" || !object.isRecord(event.item)) return;
325
- const action = object.isRecord(event.item.action) ? event.item.action : undefined;
327
+ if (event.type !== "response.output_item.done" || !isRecord(event.item)) return;
328
+ const action = isRecord(event.item.action) ? event.item.action : undefined;
326
329
  if (!action || action.type !== "search") return;
327
330
  const query = typeof action.query === "string" ? action.query : undefined;
328
331
  const queries = Array.isArray(action.queries)
@@ -364,25 +367,25 @@ function fromResponsesPayload(payload: Record<string, unknown>): {
364
367
  const citations: WebSearchCitation[] = [];
365
368
  const seen = new Set<string>();
366
369
  for (const item of output) {
367
- if (!object.isRecord(item)) continue;
368
- const parts = openaiChat.chatContentParts(item.content);
370
+ if (!isRecord(item)) continue;
371
+ const parts = chatContentParts(item.content);
369
372
  if (!parts) continue;
370
373
  for (const part of parts) {
371
- if (!object.isRecord(part)) continue;
372
- const text = stringUtils.trimToEmpty(part.text);
374
+ if (!isRecord(part)) continue;
375
+ const text = trimToEmpty(part.text);
373
376
  if (text) texts.push(text);
374
377
  const annotations = Array.isArray(part.annotations) ? part.annotations : [];
375
378
  for (const annotation of annotations) {
376
- if (!object.isRecord(annotation)) continue;
377
- const url = stringUtils.trimToEmpty(annotation.url);
379
+ if (!isRecord(annotation)) continue;
380
+ const url = trimToEmpty(annotation.url);
378
381
  if (!url || seen.has(url)) continue;
379
382
  seen.add(url);
380
- const title = stringUtils.trimToEmpty(annotation.title);
383
+ const title = trimToEmpty(annotation.title);
381
384
  citations.push({ url, ...(title ? { title } : {}) });
382
385
  }
383
386
  }
384
387
  }
385
- const answer = stringUtils.trimToEmpty(payload.output_text) || texts.join("\n").trim();
388
+ const answer = trimToEmpty(payload.output_text) || texts.join("\n").trim();
386
389
  return { answer, citations };
387
390
  }
388
391
 
@@ -398,17 +401,17 @@ function fromChatPayload(payload: Record<string, unknown>): {
398
401
  } {
399
402
  const choices = Array.isArray(payload.choices) ? payload.choices : [];
400
403
  const first = choices[0];
401
- const message = object.isRecord(first) && object.isRecord(first.message) ? first.message : {};
402
- const answer = openaiChat.chatContentToText(message.content);
404
+ const message = isRecord(first) && isRecord(first.message) ? first.message : {};
405
+ const answer = chatContentToText(message.content);
403
406
  const citations: WebSearchCitation[] = [];
404
407
  // Best-effort grounding extraction: walk any nested object for {uri|url,title}.
405
408
  const seen = new Set<string>();
406
409
  const visit = (v: unknown, depth: number): void => {
407
- if (depth > MAX_GROUNDING_WALK_DEPTH || !object.isRecord(v)) return;
408
- const url = stringUtils.trimToEmpty(v.url) || stringUtils.trimToEmpty(v.uri);
410
+ if (depth > MAX_GROUNDING_WALK_DEPTH || !isRecord(v)) return;
411
+ const url = trimToEmpty(v.url) || trimToEmpty(v.uri);
409
412
  if (url && !seen.has(url)) {
410
413
  seen.add(url);
411
- const title = stringUtils.trimToEmpty(v.title);
414
+ const title = trimToEmpty(v.title);
412
415
  citations.push({ url, ...(title ? { title } : {}) });
413
416
  }
414
417
  for (const val of Object.values(v)) {