@dbx-tools/appkit-web-search 0.9.4 → 0.9.6

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/search.ts CHANGED
@@ -28,9 +28,9 @@ import {
28
28
  getExecutionContext,
29
29
  ValidationError,
30
30
  } from "@databricks/appkit";
31
- import { invoke, resolve, serving } from "@dbx-tools/model";
32
- import { log, object, string } from "@dbx-tools/shared-core";
33
- import { openaiChat, openaiResponses } from "@dbx-tools/shared-model";
31
+ import { invoke, resolve, modelCatalog } from "@dbx-tools/model";
32
+ import { log, object, stringUtils } from "@dbx-tools/shared-core";
33
+ import { openaiChat } from "@dbx-tools/shared-model";
34
34
  import { MODEL_ENV, SERVING_ENDPOINT_ENV, type ResolvedWebSearchConfig } from "./config.ts";
35
35
  import { toCallSettings, webSearchExecuteDefaults } from "./defaults.ts";
36
36
  import { detectWebSearchProvider, supportsWebSearch, webSearchToolSpec } from "./provider.ts";
@@ -43,10 +43,10 @@ import {
43
43
  import type { WebSearchCitation, WebSearchRequest, WebSearchResult } from "./schema.ts";
44
44
  import { runScrapeSearch } from "./scrape.ts";
45
45
 
46
- type WorkspaceClientLike = serving.WorkspaceClientLike & invoke.AuthenticatingClientLike;
46
+ type WorkspaceClientLike = modelCatalog.WorkspaceClientLike & invoke.AuthenticatingClientLike;
47
47
  const logger = log.logger("web-search/search");
48
48
  const { resolveModel } = resolve;
49
- const { listServingEndpoints } = serving;
49
+ const { listServingEndpoints } = modelCatalog;
50
50
 
51
51
  /**
52
52
  * How deep the grounding-metadata walk descends. Gemini nests its sources a
@@ -66,6 +66,8 @@ const RATE_LIMITED_STATUS = 429;
66
66
  export interface WebSearchContext {
67
67
  client: WorkspaceClientLike;
68
68
  host: string;
69
+ /** Trusted identity of the credential carried by `client`. */
70
+ cacheIdentity?: string;
69
71
  }
70
72
 
71
73
  /**
@@ -77,7 +79,8 @@ export interface WebSearchContext {
77
79
  export async function resolveWebSearchContext(): Promise<WebSearchContext> {
78
80
  const ctx = getExecutionContext();
79
81
  const host = (await ctx.client.config.getHost()).toString();
80
- return { client: ctx.client, host };
82
+ const cacheIdentity = "userId" in ctx ? ctx.userId : ctx.serviceUserId;
83
+ return { client: ctx.client, host, cacheIdentity };
81
84
  }
82
85
 
83
86
  /**
@@ -104,7 +107,9 @@ async function resolveWebSearchModel(
104
107
  config: ResolvedWebSearchConfig,
105
108
  requested: string | undefined,
106
109
  ): Promise<string | null> {
107
- const endpoints = await listServingEndpoints(ctx.client, ctx.host);
110
+ const endpoints = await listServingEndpoints(ctx.client, ctx.host, {
111
+ ...(ctx.cacheIdentity ? { cacheIdentity: ctx.cacheIdentity } : {}),
112
+ });
108
113
  // Only deployed, web-search-capable endpoints are candidates.
109
114
  const capable = endpoints.filter((e) => supportsWebSearch(e.name));
110
115
  const pinned = requested ?? config.model;
@@ -213,15 +218,37 @@ async function postServing(
213
218
  /* --------------------------- response extraction --------------------------- */
214
219
 
215
220
  /**
216
- * Extract answer text and citations from an OpenAI Responses API payload through
217
- * the shared reader in `@dbx-tools/shared-model`.
221
+ * Extract answer text and citations from an OpenAI Responses API payload.
218
222
  */
219
223
  function fromResponsesPayload(payload: Record<string, unknown>): {
220
224
  answer: string;
221
225
  citations: WebSearchCitation[];
222
226
  } {
223
- const { text, citations } = openaiResponses.readResponsesOutput(payload);
224
- return { answer: text, citations };
227
+ const output = Array.isArray(payload.output) ? payload.output : [];
228
+ const texts: string[] = [];
229
+ const citations: WebSearchCitation[] = [];
230
+ const seen = new Set<string>();
231
+ for (const item of output) {
232
+ if (!object.isRecord(item)) continue;
233
+ const parts = openaiChat.chatContentParts(item.content);
234
+ if (!parts) continue;
235
+ for (const part of parts) {
236
+ if (!object.isRecord(part)) continue;
237
+ const text = stringUtils.trimToEmpty(part.text);
238
+ if (text) texts.push(text);
239
+ const annotations = Array.isArray(part.annotations) ? part.annotations : [];
240
+ for (const annotation of annotations) {
241
+ if (!object.isRecord(annotation)) continue;
242
+ const url = stringUtils.trimToEmpty(annotation.url);
243
+ if (!url || seen.has(url)) continue;
244
+ seen.add(url);
245
+ const title = stringUtils.trimToEmpty(annotation.title);
246
+ citations.push({ url, ...(title ? { title } : {}) });
247
+ }
248
+ }
249
+ }
250
+ const answer = stringUtils.trimToEmpty(payload.output_text) || texts.join("\n").trim();
251
+ return { answer, citations };
225
252
  }
226
253
 
227
254
  /**
@@ -243,10 +270,10 @@ function fromChatPayload(payload: Record<string, unknown>): {
243
270
  const seen = new Set<string>();
244
271
  const visit = (v: unknown, depth: number): void => {
245
272
  if (depth > MAX_GROUNDING_WALK_DEPTH || !object.isRecord(v)) return;
246
- const url = string.trimToEmpty(v.url) || string.trimToEmpty(v.uri);
273
+ const url = stringUtils.trimToEmpty(v.url) || stringUtils.trimToEmpty(v.uri);
247
274
  if (url && !seen.has(url)) {
248
275
  seen.add(url);
249
- const title = string.trimToEmpty(v.title);
276
+ const title = stringUtils.trimToEmpty(v.title);
250
277
  citations.push({ url, ...(title ? { title } : {}) });
251
278
  }
252
279
  for (const val of Object.values(v)) {