@databricks/appkit 0.56.0 → 0.58.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/appkit/package.js +1 -1
- package/dist/cli/commands/doctor/bundle.js +128 -0
- package/dist/cli/commands/doctor/bundle.js.map +1 -0
- package/dist/cli/commands/doctor/checks-existence.js +247 -0
- package/dist/cli/commands/doctor/checks-existence.js.map +1 -0
- package/dist/cli/commands/doctor/checks-wiring.js +35 -0
- package/dist/cli/commands/doctor/checks-wiring.js.map +1 -0
- package/dist/cli/commands/doctor/checks.js +206 -0
- package/dist/cli/commands/doctor/checks.js.map +1 -0
- package/dist/cli/commands/doctor/databricks-client.js +80 -0
- package/dist/cli/commands/doctor/databricks-client.js.map +1 -0
- package/dist/cli/commands/doctor/index.js +40 -0
- package/dist/cli/commands/doctor/index.js.map +1 -0
- package/dist/cli/commands/doctor/report.js +177 -0
- package/dist/cli/commands/doctor/report.js.map +1 -0
- package/dist/cli/commands/doctor/resolve-targets.js +86 -0
- package/dist/cli/commands/doctor/resolve-targets.js.map +1 -0
- package/dist/cli/commands/doctor/run.js +153 -0
- package/dist/cli/commands/doctor/run.js.map +1 -0
- package/dist/cli/commands/doctor/types.js +27 -0
- package/dist/cli/commands/doctor/types.js.map +1 -0
- package/dist/cli/commands/doctor/utils.js +34 -0
- package/dist/cli/commands/doctor/utils.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/lakebase/index.js +2 -3
- package/dist/connectors/lakebase/index.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts +21 -0
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js +74 -13
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/ai-search/defaults.js +4 -1
- package/dist/plugins/ai-search/defaults.js.map +1 -1
- package/docs/plugins/ai-search.md +10 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-search.js","names":["manifest"],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"sourcesContent":["import type express from \"express\";\nimport type { IAppRouter, PluginExecutionSettings } from \"shared\";\nimport { AiSearchConnector } from \"../../connectors/ai-search/client\";\nimport type {\n VsQueryParams,\n VsRawResponse,\n} from \"../../connectors/ai-search/types\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { formatWarningBanner } from \"../../utils/banner\";\nimport { aiSearchDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport type {\n IAiSearchConfig,\n IndexConfig,\n IndexSummary,\n SearchQueryType,\n SearchRequest,\n SearchResponse,\n SearchResult,\n} from \"./types\";\n\nconst logger = createLogger(\"ai-search\");\n\nconst querySettings: PluginExecutionSettings = {\n default: aiSearchDefaults,\n};\n\nexport class AiSearchPlugin extends Plugin<IAiSearchConfig> {\n static manifest = manifest as PluginManifest<\"aiSearch\">;\n\n protected static description =\n \"Query Databricks Vector Search indexes with hybrid search, reranking, and pagination\";\n protected declare config: IAiSearchConfig;\n\n private connector: AiSearchConnector;\n\n constructor(config: IAiSearchConfig) {\n super(config);\n this.config = {\n ...config,\n indexes: config.indexes ?? this._defaultIndexes(),\n };\n this.connector = new AiSearchConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n /**\n * Seeds a `default` index from `DATABRICKS_VS_INDEX_NAME` when no `indexes`\n * are configured, so `aiSearch()` works with just the env var.\n */\n private _defaultIndexes(): Record<string, IndexConfig> {\n const indexName = process.env.DATABRICKS_VS_INDEX_NAME;\n return indexName ? { default: { indexName } } : {};\n }\n\n async setup(): Promise<void> {\n // pagination needs an endpointName the framework's resource validation\n // can't see, so check it here.\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.pagination && !idx.endpointName) {\n throw new Error(\n `Index \"${alias}\" has pagination enabled but is missing \"endpointName\"`,\n );\n }\n }\n\n // Dev fills in missing `columns` from the source table; prod can't query\n // without them (VS requires `columns`), so fail fast at boot.\n if (process.env.NODE_ENV === \"development\") {\n await this._autoDiscoverColumns();\n } else {\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (!idx.columns || idx.columns.length === 0) {\n throw new Error(\n `Index \"${alias}\" has no columns configured. Vector Search queries require \"columns\"; set them explicitly (auto-discovered only in development).`,\n );\n }\n }\n }\n }\n\n /**\n * For each configured index missing `columns`, fill them from its Delta-Sync\n * source table (all source columns minus embedding vectors). Best-effort:\n * failures are logged and skipped, never thrown. A partial `columns_to_sync`\n * isn't honored, so the discovered list is a starting point to trim.\n */\n private async _autoDiscoverColumns(): Promise<void> {\n const discovered: Record<string, string[]> = {};\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.columns && idx.columns.length > 0) continue;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) continue;\n try {\n const client = getWorkspaceClient();\n const info = await this.connector.getIndex(client, indexName);\n const sourceTable = info.delta_sync_index_spec?.source_table;\n if (!sourceTable) continue;\n const excluded = new Set(\n (info.delta_sync_index_spec?.embedding_vector_columns ?? []).map(\n (c) => c.name,\n ),\n );\n const columns = (\n await this.connector.getSourceColumns(client, sourceTable)\n ).filter((c) => !excluded.has(c));\n if (columns.length > 0) {\n idx.columns = columns;\n discovered[alias] = columns;\n }\n } catch (error) {\n logger.warn(\n 'Could not auto-discover columns for index \"%s\": %s',\n alias,\n error instanceof Error ? error.message : String(error),\n );\n }\n }\n if (Object.keys(discovered).length > 0) {\n logger.warn(\"\\n%s\", this._formatColumnDiscoveryBanner(discovered));\n }\n }\n\n private _formatColumnDiscoveryBanner(\n discovered: Record<string, string[]>,\n ): string {\n const lines = [\n \"AI SEARCH: columns auto-discovered (dev mode — would fail in production)\",\n \"\",\n ];\n for (const [alias, columns] of Object.entries(discovered)) {\n lines.push(` ${alias}: ${columns.join(\", \")}`);\n }\n lines.push(\"\");\n lines.push(\n \"Set `columns` explicitly in the plugin config before deploying.\",\n );\n\n return formatWarningBanner(lines);\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"query\",\n method: \"post\",\n path: \"/:alias/query\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n const body: SearchRequest = req.body;\n if (!body.queryText && !body.queryVector) {\n res.status(400).json({\n error: \"queryText or queryVector is required\",\n plugin: this.name,\n });\n return;\n }\n\n // Drop client-supplied `columns` so an HTTP caller can't widen the\n // projection past what the app configured. (query() callers are\n // trusted and keep the override.)\n const { columns: _clientColumns, ...safeBody } = body;\n const plugin =\n indexConfig.auth === \"on-behalf-of-user\" ? this.asUser(req) : this;\n const queryType =\n safeBody.queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n try {\n // Prepare inside execute so a self-managed embeddingFn runs in the\n // same OBO context as the query, not as the service principal.\n const result = await plugin.execute(async (signal) => {\n const prepared = await this._prepareQuery(safeBody, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n }, querySettings);\n\n this._sendResult(res, result, queryType);\n } catch (error) {\n this._handleError(res, error, \"Query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"queryNextPage\",\n method: \"post\",\n path: \"/:alias/next-page\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n if (!indexConfig.pagination) {\n res.status(400).json({\n error: `Pagination is not enabled for index \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return;\n }\n\n if (!indexConfig.endpointName) {\n res.status(400).json({\n error: `Index \"${req.params.alias}\" is missing endpointName required for pagination`,\n plugin: this.name,\n });\n return;\n }\n\n const { pageToken, queryType } = req.body;\n if (!pageToken) {\n res.status(400).json({\n error: \"pageToken is required\",\n plugin: this.name,\n });\n return;\n }\n // Echo the original query's queryType so paged responses stay\n // consistent with page 1; fall back to the index default.\n const pageQueryType = queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n try {\n const plugin =\n indexConfig.auth === \"on-behalf-of-user\" ? this.asUser(req) : this;\n\n const result = await plugin.execute(\n async (signal) =>\n this.connector.queryNextPage(\n getWorkspaceClient(),\n {\n indexName: indexConfig.indexName,\n endpointName: indexConfig.endpointName as string,\n pageToken,\n },\n signal,\n ),\n querySettings,\n );\n\n this._sendResult(res, result, pageQueryType);\n } catch (error) {\n this._handleError(res, error, \"Next-page query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"getConfig\",\n method: \"get\",\n path: \"/:alias/config\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n res.json({\n alias: req.params.alias,\n columns: indexConfig.columns,\n queryType: indexConfig.queryType ?? \"hybrid\",\n numResults: indexConfig.numResults ?? 20,\n reranker: !!indexConfig.reranker,\n pagination: !!indexConfig.pagination,\n });\n },\n });\n }\n\n /**\n * Index aliases + non-sensitive query metadata, serialized to the client so\n * the UI can discover available indexes instead of hardcoding an alias.\n */\n clientConfig(): { indexes: IndexSummary[] } {\n const indexes = Object.entries(this.config.indexes ?? {}).map(\n ([alias, idx]) => ({\n alias,\n queryType: idx.queryType ?? \"hybrid\",\n pagination: !!idx.pagination,\n }),\n );\n return { indexes };\n }\n\n /**\n * Programmatic query API — available as `appkit.aiSearch.query()`.\n * When called through `asUser(req)`, executes with the user's credentials.\n *\n * @remarks `T` types each result's `data` but is an unchecked cast — the row\n * shape isn't validated at runtime.\n */\n async query<T extends Record<string, unknown> = Record<string, unknown>>(\n alias: string,\n request: SearchRequest,\n ): Promise<SearchResponse<T>> {\n const indexConfig = this._resolveIndex(alias);\n if (!indexConfig) {\n throw new Error(`No index configured with alias \"${alias}\"`);\n }\n\n const prepared = await this._prepareQuery(request, indexConfig);\n\n const result = await this.execute(\n async (signal) =>\n this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n ),\n querySettings,\n );\n\n if (!result.ok) {\n throw new Error(\n `Vector search query failed for index \"${alias}\": ${result.message}`,\n );\n }\n\n return this._parseResponse(result.data, prepared.queryType);\n }\n\n async shutdown(): Promise<void> {\n // No streams or persistent connections to clean up\n }\n\n exports() {\n return {\n query: this.query.bind(this),\n };\n }\n\n private _resolveIndex(\n alias: string,\n ): (IndexConfig & { indexName: string }) | undefined {\n const idx = this.config.indexes?.[alias];\n if (!idx) return undefined;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) return undefined;\n return { ...idx, indexName };\n }\n\n /** Resolve an index by route alias, or send a 404 and return null. */\n private _resolveOr404(\n req: express.Request,\n res: express.Response,\n ): (IndexConfig & { indexName: string }) | null {\n const indexConfig = this._resolveIndex(req.params.alias);\n if (!indexConfig) {\n res.status(404).json({\n error: `No index configured with alias \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return null;\n }\n return indexConfig;\n }\n\n /** Send an execution result as JSON, or its error status/message. */\n private _sendResult(\n res: express.Response,\n result: Awaited<ReturnType<typeof this.execute<VsRawResponse>>>,\n queryType: SearchQueryType,\n ): void {\n if (!result.ok) {\n res\n .status(result.status)\n .json({ error: result.message, plugin: this.name });\n return;\n }\n res.json(this._parseResponse(result.data, queryType));\n }\n\n private async _prepareQuery(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Promise<Omit<VsQueryParams, \"indexName\">> {\n const queryType = request.queryType ?? indexConfig.queryType ?? \"hybrid\";\n let queryText = request.queryText;\n let queryVector = request.queryVector;\n\n // full_text uses no vector; hybrid keeps the text for its keyword half.\n if (\n indexConfig.embeddingFn &&\n queryText &&\n !queryVector &&\n queryType !== \"full_text\"\n ) {\n try {\n queryVector = await indexConfig.embeddingFn(queryText);\n if (queryType === \"ann\") queryText = undefined;\n } catch (error) {\n throw new Error(\n `Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n const columns = request.columns ?? indexConfig.columns ?? [];\n return {\n queryText,\n queryVector,\n queryType,\n columns,\n numResults: request.numResults ?? indexConfig.numResults ?? 20,\n filters: request.filters,\n reranker: this._resolveReranker(request.reranker, indexConfig, columns),\n };\n }\n\n private _resolveReranker(\n requestReranker: boolean | undefined,\n indexConfig: IndexConfig,\n columns: string[],\n ): { columnsToRerank: string[] } | undefined {\n const shouldRerank = requestReranker ?? indexConfig.reranker;\n if (!shouldRerank) return undefined;\n\n if (typeof indexConfig.reranker === \"object\") {\n return indexConfig.reranker;\n }\n // Auto-derive from returnable columns (excluding the id). With no columns\n // resolved there's nothing to rerank on, so skip it.\n const columnsToRerank = columns.filter((c) => c !== \"id\");\n return columnsToRerank.length > 0 ? { columnsToRerank } : undefined;\n }\n\n private _parseResponse<\n T extends Record<string, unknown> = Record<string, unknown>,\n >(raw: VsRawResponse, queryType: SearchQueryType): SearchResponse<T> {\n const columnNames = raw.manifest.columns.map((c) => c.name);\n const scoreIndex = columnNames.indexOf(\"score\");\n\n const results: SearchResult<T>[] = raw.result.data_array.map((row) => {\n const data: Record<string, unknown> = {};\n for (let i = 0; i < columnNames.length; i++) {\n if (i !== scoreIndex) data[columnNames[i]] = row[i];\n }\n return {\n score: scoreIndex >= 0 ? (row[scoreIndex] as number) : 0,\n data: data as T,\n };\n });\n\n return {\n results,\n totalCount: raw.result.row_count,\n queryTimeMs:\n raw.debug_info?.response_time ?? raw.debug_info?.latency_ms ?? 0,\n queryType,\n nextPageToken: raw.next_page_token ?? null,\n };\n }\n\n private _handleError(\n res: express.Response,\n error: unknown,\n fallbackMessage: string,\n ): void {\n logger.error(\"%s: %O\", fallbackMessage, error);\n // Match Plugin.execute(): the raw message is only exposed outside production.\n const isDev = process.env.NODE_ENV !== \"production\";\n const message =\n isDev && error instanceof Error ? error.message : fallbackMessage;\n res.status(500).json({ error: message, plugin: this.name });\n }\n}\n\nexport const aiSearch = toPlugin(AiSearchPlugin);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ai-search.js","names":["manifest"],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type express from \"express\";\nimport type { CacheConfig, IAppRouter, PluginExecutionSettings } from \"shared\";\nimport { AiSearchConnector } from \"../../connectors/ai-search/client\";\nimport type {\n VsQueryParams,\n VsRawResponse,\n} from \"../../connectors/ai-search/types\";\nimport { getCurrentUserId, getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { formatWarningBanner } from \"../../utils/banner\";\nimport { aiSearchDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport type {\n IAiSearchConfig,\n IndexConfig,\n IndexSummary,\n SearchQueryType,\n SearchRequest,\n SearchResponse,\n SearchResult,\n} from \"./types\";\n\nconst logger = createLogger(\"ai-search\");\n\nconst querySettings: PluginExecutionSettings = {\n default: aiSearchDefaults,\n};\n\nexport class AiSearchPlugin extends Plugin<IAiSearchConfig> {\n static manifest = manifest as PluginManifest<\"aiSearch\">;\n\n protected static description =\n \"Query Databricks Vector Search indexes with hybrid search, reranking, and pagination\";\n protected declare config: IAiSearchConfig;\n\n private connector: AiSearchConnector;\n\n constructor(config: IAiSearchConfig) {\n super(config);\n this.config = {\n ...config,\n indexes: config.indexes ?? this._defaultIndexes(),\n };\n this.connector = new AiSearchConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n /**\n * Seeds a `default` index from `DATABRICKS_VS_INDEX_NAME` when no `indexes`\n * are configured, so `aiSearch()` works with just the env var.\n */\n private _defaultIndexes(): Record<string, IndexConfig> {\n const indexName = process.env.DATABRICKS_VS_INDEX_NAME;\n return indexName ? { default: { indexName } } : {};\n }\n\n async setup(): Promise<void> {\n // pagination needs an endpointName the framework's resource validation\n // can't see, so check it here.\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.pagination && !idx.endpointName) {\n throw new Error(\n `Index \"${alias}\" has pagination enabled but is missing \"endpointName\"`,\n );\n }\n }\n\n // Dev fills in missing `columns` from the source table; prod can't query\n // without them (VS requires `columns`), so fail fast at boot.\n if (process.env.NODE_ENV === \"development\") {\n await this._autoDiscoverColumns();\n } else {\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (!idx.columns || idx.columns.length === 0) {\n throw new Error(\n `Index \"${alias}\" has no columns configured. Vector Search queries require \"columns\"; set them explicitly (auto-discovered only in development).`,\n );\n }\n }\n }\n }\n\n /**\n * For each configured index missing `columns`, fill them from its Delta-Sync\n * source table (all source columns minus embedding vectors). Best-effort:\n * failures are logged and skipped, never thrown. A partial `columns_to_sync`\n * isn't honored, so the discovered list is a starting point to trim.\n */\n private async _autoDiscoverColumns(): Promise<void> {\n const discovered: Record<string, string[]> = {};\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.columns && idx.columns.length > 0) continue;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) continue;\n try {\n const client = getWorkspaceClient();\n const info = await this.connector.getIndex(client, indexName);\n const sourceTable = info.delta_sync_index_spec?.source_table;\n if (!sourceTable) continue;\n const excluded = new Set(\n (info.delta_sync_index_spec?.embedding_vector_columns ?? []).map(\n (c) => c.name,\n ),\n );\n const columns = (\n await this.connector.getSourceColumns(client, sourceTable)\n ).filter((c) => !excluded.has(c));\n if (columns.length > 0) {\n idx.columns = columns;\n discovered[alias] = columns;\n }\n } catch (error) {\n logger.warn(\n 'Could not auto-discover columns for index \"%s\": %s',\n alias,\n error instanceof Error ? error.message : String(error),\n );\n }\n }\n if (Object.keys(discovered).length > 0) {\n logger.warn(\"\\n%s\", this._formatColumnDiscoveryBanner(discovered));\n }\n }\n\n private _formatColumnDiscoveryBanner(\n discovered: Record<string, string[]>,\n ): string {\n const lines = [\n \"AI SEARCH: columns auto-discovered (dev mode — would fail in production)\",\n \"\",\n ];\n for (const [alias, columns] of Object.entries(discovered)) {\n lines.push(` ${alias}: ${columns.join(\", \")}`);\n }\n lines.push(\"\");\n lines.push(\n \"Set `columns` explicitly in the plugin config before deploying.\",\n );\n\n return formatWarningBanner(lines);\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"query\",\n method: \"post\",\n path: \"/:alias/query\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n const body: SearchRequest = req.body;\n if (!body.queryText && !body.queryVector) {\n res.status(400).json({\n error: \"queryText or queryVector is required\",\n plugin: this.name,\n });\n return;\n }\n\n // Drop client-supplied `columns` so an HTTP caller can't widen the\n // projection past what the app configured. (query() callers are\n // trusted and keep the override.)\n const { columns: _clientColumns, ...safeBody } = body;\n const isAsUser = indexConfig.auth === \"on-behalf-of-user\";\n const plugin = isAsUser ? this.asUser(req) : this;\n const queryType =\n safeBody.queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n // Key per user for OBO so results never leak across users; SP shares \"global\".\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n try {\n // Prepare inside execute so a self-managed embeddingFn runs in the\n // same OBO context as the query, not as the service principal.\n const result = await plugin.execute(\n async (signal) => {\n const prepared = await this._prepareQuery(safeBody, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n },\n this._executeSettings(safeBody, indexConfig, executorKey),\n );\n\n this._sendResult(res, result, queryType);\n } catch (error) {\n this._handleError(res, error, \"Query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"queryNextPage\",\n method: \"post\",\n path: \"/:alias/next-page\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n if (!indexConfig.pagination) {\n res.status(400).json({\n error: `Pagination is not enabled for index \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return;\n }\n\n if (!indexConfig.endpointName) {\n res.status(400).json({\n error: `Index \"${req.params.alias}\" is missing endpointName required for pagination`,\n plugin: this.name,\n });\n return;\n }\n\n const { pageToken, queryType } = req.body;\n if (!pageToken) {\n res.status(400).json({\n error: \"pageToken is required\",\n plugin: this.name,\n });\n return;\n }\n // Echo the original query's queryType so paged responses stay\n // consistent with page 1; fall back to the index default.\n const pageQueryType = queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n try {\n const plugin =\n indexConfig.auth === \"on-behalf-of-user\" ? this.asUser(req) : this;\n\n // Uncached: a page token is a single-use cursor. `querySettings` has\n // no `cacheKey`, so caching stays off.\n const result = await plugin.execute(\n async (signal) =>\n this.connector.queryNextPage(\n getWorkspaceClient(),\n {\n indexName: indexConfig.indexName,\n endpointName: indexConfig.endpointName as string,\n pageToken,\n },\n signal,\n ),\n querySettings,\n );\n\n this._sendResult(res, result, pageQueryType);\n } catch (error) {\n this._handleError(res, error, \"Next-page query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"getConfig\",\n method: \"get\",\n path: \"/:alias/config\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n res.json({\n alias: req.params.alias,\n columns: indexConfig.columns,\n queryType: indexConfig.queryType ?? \"hybrid\",\n numResults: indexConfig.numResults ?? 20,\n reranker: !!indexConfig.reranker,\n pagination: !!indexConfig.pagination,\n });\n },\n });\n }\n\n /**\n * Index aliases + non-sensitive query metadata, serialized to the client so\n * the UI can discover available indexes instead of hardcoding an alias.\n */\n clientConfig(): { indexes: IndexSummary[] } {\n const indexes = Object.entries(this.config.indexes ?? {}).map(\n ([alias, idx]) => ({\n alias,\n queryType: idx.queryType ?? \"hybrid\",\n pagination: !!idx.pagination,\n }),\n );\n return { indexes };\n }\n\n /**\n * Programmatic query API — available as `appkit.aiSearch.query()`.\n * When called through `asUser(req)`, executes with the user's credentials.\n *\n * @remarks `T` types each result's `data` but is an unchecked cast — the row\n * shape isn't validated at runtime.\n */\n async query<T extends Record<string, unknown> = Record<string, unknown>>(\n alias: string,\n request: SearchRequest,\n ): Promise<SearchResponse<T>> {\n const indexConfig = this._resolveIndex(alias);\n if (!indexConfig) {\n throw new Error(`No index configured with alias \"${alias}\"`);\n }\n\n // Resolve queryType for the response here; _prepareQuery runs inside\n // execute so a cache hit skips the embedding and the VS call.\n const { queryType } = this._resolveQueryParams(request, indexConfig);\n\n // getCurrentUserId() is the user's id under asUser(), the service id\n // otherwise — keying per caller like the route's executorKey.\n const result = await this.execute(\n async (signal) => {\n const prepared = await this._prepareQuery(request, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n },\n this._executeSettings(request, indexConfig, getCurrentUserId()),\n );\n\n if (!result.ok) {\n throw new Error(\n `Vector search query failed for index \"${alias}\": ${result.message}`,\n );\n }\n\n return this._parseResponse(result.data, queryType);\n }\n\n async shutdown(): Promise<void> {\n // No streams or persistent connections to clean up\n }\n\n exports() {\n return {\n query: this.query.bind(this),\n };\n }\n\n private _resolveIndex(\n alias: string,\n ): (IndexConfig & { indexName: string }) | undefined {\n const idx = this.config.indexes?.[alias];\n if (!idx) return undefined;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) return undefined;\n return { ...idx, indexName };\n }\n\n /** Resolve an index by route alias, or send a 404 and return null. */\n private _resolveOr404(\n req: express.Request,\n res: express.Response,\n ): (IndexConfig & { indexName: string }) | null {\n const indexConfig = this._resolveIndex(req.params.alias);\n if (!indexConfig) {\n res.status(404).json({\n error: `No index configured with alias \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return null;\n }\n return indexConfig;\n }\n\n /** Send an execution result as JSON, or its error status/message. */\n private _sendResult(\n res: express.Response,\n result: Awaited<ReturnType<typeof this.execute<VsRawResponse>>>,\n queryType: SearchQueryType,\n ): void {\n if (!result.ok) {\n res\n .status(result.status)\n .json({ error: result.message, plugin: this.name });\n return;\n }\n res.json(this._parseResponse(result.data, queryType));\n }\n\n /**\n * Resolve request-vs-index defaults for the result-determining fields.\n * Shared by `_prepareQuery` (the payload) and `_cacheKeyFor` (the key) so\n * the two can't drift.\n */\n private _resolveQueryParams(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Pick<VsQueryParams, \"queryType\" | \"columns\" | \"numResults\" | \"reranker\"> {\n const queryType = request.queryType ?? indexConfig.queryType ?? \"hybrid\";\n const columns = request.columns ?? indexConfig.columns ?? [];\n return {\n queryType,\n columns,\n numResults: request.numResults ?? indexConfig.numResults ?? 20,\n reranker: this._resolveReranker(request.reranker, indexConfig, columns),\n };\n }\n\n private async _prepareQuery(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Promise<Omit<VsQueryParams, \"indexName\">> {\n const { queryType, columns, numResults, reranker } =\n this._resolveQueryParams(request, indexConfig);\n let queryText = request.queryText;\n let queryVector = request.queryVector;\n\n // full_text uses no vector; hybrid keeps the text for its keyword half.\n if (\n indexConfig.embeddingFn &&\n queryText &&\n !queryVector &&\n queryType !== \"full_text\"\n ) {\n try {\n queryVector = await indexConfig.embeddingFn(queryText);\n if (queryType === \"ann\") queryText = undefined;\n } catch (error) {\n throw new Error(\n `Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return {\n queryText,\n queryVector,\n queryType,\n columns,\n numResults,\n filters: request.filters,\n reranker,\n };\n }\n\n /**\n * Cache key for a query: every input that changes the VS result, resolved\n * via `_resolveQueryParams` so the key matches the payload (post-allowlist\n * `columns`, not the raw request). `queryVector` is hashed (vectors are\n * large); the key uses `queryText`, not the derived embedding, since the\n * embedding is a function of `queryText` and hasn't run at key-build time.\n * `columns`/`filters` are order-normalized so equivalent requests share an\n * entry.\n */\n private _cacheKeyFor(\n request: SearchRequest,\n indexConfig: IndexConfig & { indexName: string },\n executorKey: string,\n ): CacheConfig[\"cacheKey\"] {\n const { queryType, columns, numResults, reranker } =\n this._resolveQueryParams(request, indexConfig);\n return [\n \"ai-search:query\",\n indexConfig.indexName,\n request.queryText ?? \"\",\n request.queryVector ? this._hashVector(request.queryVector) : \"\",\n queryType,\n numResults,\n // columns is a projection; order doesn't affect results, so sort a copy.\n JSON.stringify([...columns].sort()),\n this._stableStringify(request.filters ?? null),\n String(!!reranker),\n executorKey,\n ];\n }\n\n private _hashVector(vector: number[]): string {\n return createHash(\"sha256\").update(JSON.stringify(vector)).digest(\"hex\");\n }\n\n /** Execute settings with the per-call cache key folded in. */\n private _executeSettings(\n request: SearchRequest,\n indexConfig: IndexConfig & { indexName: string },\n executorKey: string,\n ): PluginExecutionSettings {\n return {\n default: {\n ...aiSearchDefaults,\n cache: {\n ...aiSearchDefaults.cache,\n cacheKey: this._cacheKeyFor(request, indexConfig, executorKey),\n },\n },\n };\n }\n\n /** `JSON.stringify` with object keys sorted recursively; array order kept. */\n private _stableStringify(value: unknown): string {\n return JSON.stringify(value, (_key, val) =>\n val && typeof val === \"object\" && !Array.isArray(val)\n ? Object.fromEntries(\n Object.entries(val).sort(([a], [b]) => a.localeCompare(b)),\n )\n : val,\n );\n }\n\n private _resolveReranker(\n requestReranker: boolean | undefined,\n indexConfig: IndexConfig,\n columns: string[],\n ): { columnsToRerank: string[] } | undefined {\n const shouldRerank = requestReranker ?? indexConfig.reranker;\n if (!shouldRerank) return undefined;\n\n if (typeof indexConfig.reranker === \"object\") {\n return indexConfig.reranker;\n }\n // Auto-derive from returnable columns (excluding the id). With no columns\n // resolved there's nothing to rerank on, so skip it.\n const columnsToRerank = columns.filter((c) => c !== \"id\");\n return columnsToRerank.length > 0 ? { columnsToRerank } : undefined;\n }\n\n private _parseResponse<\n T extends Record<string, unknown> = Record<string, unknown>,\n >(raw: VsRawResponse, queryType: SearchQueryType): SearchResponse<T> {\n const columnNames = raw.manifest.columns.map((c) => c.name);\n const scoreIndex = columnNames.indexOf(\"score\");\n\n const results: SearchResult<T>[] = raw.result.data_array.map((row) => {\n const data: Record<string, unknown> = {};\n for (let i = 0; i < columnNames.length; i++) {\n if (i !== scoreIndex) data[columnNames[i]] = row[i];\n }\n return {\n score: scoreIndex >= 0 ? (row[scoreIndex] as number) : 0,\n data: data as T,\n };\n });\n\n return {\n results,\n totalCount: raw.result.row_count,\n queryTimeMs:\n raw.debug_info?.response_time ?? raw.debug_info?.latency_ms ?? 0,\n queryType,\n nextPageToken: raw.next_page_token ?? null,\n };\n }\n\n private _handleError(\n res: express.Response,\n error: unknown,\n fallbackMessage: string,\n ): void {\n logger.error(\"%s: %O\", fallbackMessage, error);\n // Match Plugin.execute(): the raw message is only exposed outside production.\n const isDev = process.env.NODE_ENV !== \"production\";\n const message =\n isDev && error instanceof Error ? error.message : fallbackMessage;\n res.status(500).json({ error: message, plugin: this.name });\n }\n}\n\nexport const aiSearch = toPlugin(AiSearchPlugin);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.js","names":[],"sources":["../../../src/plugins/ai-search/defaults.ts"],"sourcesContent":["import type { PluginExecuteConfig } from \"shared\";\n\nexport const aiSearchDefaults: PluginExecuteConfig = {\n cache: { enabled:
|
|
1
|
+
{"version":3,"file":"defaults.js","names":[],"sources":["../../../src/plugins/ai-search/defaults.ts"],"sourcesContent":["import type { PluginExecuteConfig } from \"shared\";\n\nexport const aiSearchDefaults: PluginExecuteConfig = {\n // Short TTL: VS results shift as the index resyncs, so cache only long\n // enough to absorb bursts without serving stale results.\n cache: { enabled: true, ttl: 60 },\n retry: { enabled: true, initialDelay: 1000, attempts: 3 },\n timeout: 30_000,\n};\n"],"mappings":";AAEA,MAAa,mBAAwC;CAGnD,OAAO;EAAE,SAAS;EAAM,KAAK;EAAI;CACjC,OAAO;EAAE,SAAS;EAAM,cAAc;EAAM,UAAU;EAAG;CACzD,SAAS;CACV"}
|
|
@@ -264,6 +264,16 @@ console.log(result.results);
|
|
|
264
264
|
|
|
265
265
|
Pass optional overrides as a second argument to `query` to adjust `numResults` or other per-call settings.
|
|
266
266
|
|
|
267
|
+
## Caching[](#caching "Direct link to Caching")
|
|
268
|
+
|
|
269
|
+
Query results are cached with a short TTL (60s) so repeated identical queries — including a component that re-renders or mounts twice — reuse a single Vector Search call instead of hitting the index each time. The next-page route is not cached: a page token is a single-use cursor and already identifies the exact page.
|
|
270
|
+
|
|
271
|
+
The cache key covers everything that changes results: the resolved index, `queryText`, `queryVector` (hashed), `queryType`, `numResults`, the resolved `columns`, `filters`, and whether reranking is on. Two queries that differ in any of these are cached separately.
|
|
272
|
+
|
|
273
|
+
### Per-user isolation[](#per-user-isolation "Direct link to Per-user isolation")
|
|
274
|
+
|
|
275
|
+
For `auth: "on-behalf-of-user"` indexes the caller's identity is part of the cache key, so one user never sees another user's cached results — and an on-behalf-of-user query never reads a service-principal-populated entry. Service-principal indexes share a single cache entry across callers.
|
|
276
|
+
|
|
267
277
|
## React hook[](#react-hook "Direct link to React hook")
|
|
268
278
|
|
|
269
279
|
`useAiSearchQuery` reads the configured indexes from the plugin's client config and posts to the right `/:alias/query` route, so the UI never hardcodes an alias. With one index configured it needs no arguments; pass `{ alias }` to target a specific one.
|