@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/index.ts +11 -4
- package/lib/index.d.ts +11 -4
- package/lib/index.js +12 -5
- package/lib/src/allowlist.js +6 -6
- package/lib/src/config.js +19 -19
- package/lib/src/plugin.d.ts +1 -1
- package/lib/src/plugin.js +5 -5
- package/lib/src/runtime.js +5 -5
- package/lib/src/schema.js +6 -6
- package/lib/src/search.d.ts +1 -1
- package/lib/src/search.js +24 -24
- package/package.json +8 -112
- package/src/allowlist.ts +5 -5
- package/src/config.ts +22 -33
- package/src/plugin.ts +5 -5
- package/src/runtime.ts +4 -4
- package/src/schema.ts +5 -5
- package/src/search.ts +28 -25
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 {
|
|
50
|
-
import {
|
|
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 =
|
|
300
|
+
const raw = text("WEB_SEARCH_TOOLS", ENV_ONLY);
|
|
301
301
|
if (raw === undefined) return {};
|
|
302
|
-
const parsed =
|
|
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 =
|
|
314
|
+
const fromConfig = trimToNull(config.model);
|
|
315
315
|
if (fromConfig !== null) return { model: fromConfig, source: "config" };
|
|
316
|
-
const fromModelEnv =
|
|
316
|
+
const fromModelEnv = text(MODEL_ENV, ENV_ONLY);
|
|
317
317
|
if (fromModelEnv !== undefined) return { model: fromModelEnv, source: MODEL_ENV };
|
|
318
|
-
const fromResourceEnv =
|
|
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 ??
|
|
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 (
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
410
|
+
DEFAULT_FUZZY_THRESHOLD,
|
|
411
|
+
ENV_ONLY,
|
|
413
412
|
),
|
|
414
|
-
maxCitations:
|
|
413
|
+
maxCitations: positiveInt(
|
|
415
414
|
config.maxCitations,
|
|
416
415
|
"WEB_SEARCH_MAX_CITATIONS",
|
|
417
416
|
DEFAULT_MAX_CITATIONS,
|
|
418
|
-
|
|
417
|
+
ENV_ONLY,
|
|
419
418
|
),
|
|
420
|
-
fetchMaxLength:
|
|
419
|
+
fetchMaxLength: positiveInt(
|
|
421
420
|
config.fetchMaxLength,
|
|
422
421
|
"WEB_SEARCH_FETCH_MAX_LENGTH",
|
|
423
422
|
DEFAULT_FETCH_MAX_LENGTH,
|
|
424
|
-
|
|
423
|
+
ENV_ONLY,
|
|
425
424
|
),
|
|
426
|
-
timeoutMs:
|
|
427
|
-
|
|
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
|
|
27
|
-
import { log,
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 {
|
|
12
|
+
import { runPluginExecution } from "@dbx-tools/appkit";
|
|
13
13
|
import { policy } from "@dbx-tools/model";
|
|
14
|
-
import {
|
|
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 =
|
|
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
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
31
|
-
import { invoke, policy,
|
|
32
|
-
import { log,
|
|
33
|
-
import {
|
|
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 } =
|
|
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
|
|
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 (!
|
|
293
|
+
if (!isRecord(event) || typeof event.type !== "string") return;
|
|
291
294
|
await emitWebSearchProgress(event, writeProgress);
|
|
292
|
-
if (event.type === "response.completed" &&
|
|
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" || !
|
|
325
|
-
const action =
|
|
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 (!
|
|
368
|
-
const parts =
|
|
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 (!
|
|
372
|
-
const 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 (!
|
|
377
|
-
const 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 =
|
|
383
|
+
const title = trimToEmpty(annotation.title);
|
|
381
384
|
citations.push({ url, ...(title ? { title } : {}) });
|
|
382
385
|
}
|
|
383
386
|
}
|
|
384
387
|
}
|
|
385
|
-
const answer =
|
|
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 =
|
|
402
|
-
const answer =
|
|
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 || !
|
|
408
|
-
const url =
|
|
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 =
|
|
414
|
+
const title = trimToEmpty(v.title);
|
|
412
415
|
citations.push({ url, ...(title ? { title } : {}) });
|
|
413
416
|
}
|
|
414
417
|
for (const val of Object.values(v)) {
|