From f1aaf911d70a27935d628898bc7c12e2a85c4c2d Mon Sep 17 00:00:00 2001 From: Paul Jankowski <8bittitan@gmail.com> Date: Fri, 16 Jan 2026 10:06:04 -0500 Subject: [PATCH] feat(askai): Allow Agent Studio specific search params (#2842) * feat(askai): Allow Agent Studio specific search params * Simplify DocSearchProps types definitions * Revert "Simplify DocSearchProps types definitions" This reverts commit b53bc976884b666e65a22226f538ccc208e12f1a. * fix: types --- .../demo-react/src/examples/agent-studio.tsx | 3 +- packages/docsearch-react/src/DocSearch.tsx | 55 +++++++++++---- .../docsearch-react/src/DocSearchModal.tsx | 2 +- packages/docsearch-react/src/Sidepanel.tsx | 69 ++++++++++++------- .../src/Sidepanel/ConversationScreen.tsx | 23 +++++-- .../src/Sidepanel/Sidepanel.tsx | 5 +- packages/docsearch-react/src/askai.ts | 32 +++++++++ packages/docsearch-react/src/useAskAi.ts | 54 ++++++++++----- .../docsearch-sidepanel/src/Sidepanel.tsx | 9 ++- 9 files changed, 178 insertions(+), 74 deletions(-) diff --git a/examples/demo-react/src/examples/agent-studio.tsx b/examples/demo-react/src/examples/agent-studio.tsx index c3a133e6..6916e836 100644 --- a/examples/demo-react/src/examples/agent-studio.tsx +++ b/examples/demo-react/src/examples/agent-studio.tsx @@ -21,9 +21,8 @@ export function AgentStudioExample(): JSX.Element { apiKey="a00716d83c64f6c61905c078b7d5ab66" askAi={{ assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef', - indexName: 'docsearch-markdown', + agentStudio: true, }} - agentStudio={true} /> diff --git a/packages/docsearch-react/src/DocSearch.tsx b/packages/docsearch-react/src/DocSearch.tsx index 9cc122ef..9c8b3fb7 100644 --- a/packages/docsearch-react/src/DocSearch.tsx +++ b/packages/docsearch-react/src/DocSearch.tsx @@ -31,9 +31,11 @@ export type AskAiSearchParameters = { filters?: string; attributesToRetrieve?: string[]; restrictSearchableAttributes?: string[]; - distinct?: boolean; + distinct?: boolean | number | string; }; +export type AgentStudioSearchParameters = Record>; + export type DocSearchAskAi = { /** * The index name to use for the ask AI feature. Your assistant will search this index for relevant documents. @@ -54,10 +56,6 @@ export type DocSearchAskAi = { * The assistant ID to use for the ask AI feature. */ assistantId: string; - /** - * The search parameters to use for the ask AI feature. - */ - searchParameters?: AskAiSearchParameters; /** * Enables displaying suggested questions on Ask AI's new conversation screen. * @@ -66,7 +64,43 @@ export type DocSearchAskAi = { suggestedQuestions?: boolean; // HACK: This is a hack for testing staging, remove before releasing useStagingEnv?: boolean; -}; +} & ( + | { + /** + * **Experimental:** Whether to use Agent Studio as the chat backend. + * + * This is an experimental feature and its API may change without notice in future releases. + * Use with caution in production environments. + * + * @default false + */ + agentStudio?: never; + /** + * The search parameters to use for the ask AI feature. + * + * **NOTE**: If using `agentStudio = true`, the `searchParameters` object is + * keyed by the index name. + */ + searchParameters?: AskAiSearchParameters; + } + | { + agentStudio: false; + searchParameters?: AskAiSearchParameters; + } + | { + agentStudio: true; + /** + * The search parameters to use for the ask AI feature. + * Keyed by the index name. + * + * @example + * { + * "INDEX_NAME": { distinct: false } + * } + */ + searchParameters?: AgentStudioSearchParameters; + } +); export interface DocSearchIndex { name: string; @@ -105,15 +139,6 @@ export interface DocSearchProps { * Useful to route Ask AI into a different UI (e.g. `@docsearch/sidepanel-js`) without flicker. */ interceptAskAiEvent?: (initialMessage: InitialAskAiMessage) => boolean | void; - /** - * **Experimental:** Whether to use Agent Studio as the chat backend. - * - * This is an experimental feature and its API may change without notice in future releases. - * Use with caution in production environments. - * - * @default false - */ - agentStudio?: boolean; /** * Theme overrides applied to the modal and related components. */ diff --git a/packages/docsearch-react/src/DocSearchModal.tsx b/packages/docsearch-react/src/DocSearchModal.tsx index c59494c8..9fe5022d 100644 --- a/packages/docsearch-react/src/DocSearchModal.tsx +++ b/packages/docsearch-react/src/DocSearchModal.tsx @@ -310,7 +310,6 @@ export function DocSearchModal({ indexName, searchParameters, isHybridModeSupported = false, - agentStudio = false, ...props }: DocSearchModalProps): JSX.Element { const { footer: footerTranslations, searchBox: searchBoxTranslations, ...screenStateTranslations } = translations; @@ -360,6 +359,7 @@ export function DocSearchModal({ searchClient, suggestedQuestionsEnabled: askAiConfig?.suggestedQuestions, }); + const agentStudio = askAiConfig?.agentStudio ?? false; // Format the `indexes` to be used until `indexName` and `searchParameters` props are fully removed. const indexes: DocSearchIndex[] = []; diff --git a/packages/docsearch-react/src/Sidepanel.tsx b/packages/docsearch-react/src/Sidepanel.tsx index b094304d..67e9650d 100644 --- a/packages/docsearch-react/src/Sidepanel.tsx +++ b/packages/docsearch-react/src/Sidepanel.tsx @@ -4,12 +4,49 @@ import type { JSX } from 'react'; import React from 'react'; import { createPortal } from 'react-dom'; -import type { AskAiSearchParameters } from './DocSearch'; -import type { SidepanelButtonProps, SidepanelProps } from './Sidepanel/index'; +import type { AgentStudioSearchParameters, AskAiSearchParameters } from './DocSearch'; +import type { SidepanelButtonProps, SidepanelProps as SidepanelPanelProps } from './Sidepanel/index'; import { SidepanelButton, Sidepanel } from './Sidepanel/index'; export type { DocSearchRef, DocSearchCallbacks } from '@docsearch/core'; +export type SidepanelSearchParameters = + | { + /** + * **Experimental:** Whether to use Agent Studio as the chat backend. + * + * This is an experimental feature and its API may change without notice in future releases. + * Use with caution in production environments. + * + * @default false + */ + agentStudio?: never; + /** + * The search parameters to use for the ask AI feature. + * + * **NOTE**: If using `agentStudio = true`, the `searchParameters` object is + * keyed by the index name. + */ + searchParameters?: AskAiSearchParameters; + } + | { + agentStudio: false; + searchParameters?: AskAiSearchParameters; + } + | { + agentStudio: true; + /** + * The search parameters to use for the ask AI feature. + * Keyed by the index name. + * + * @example + * { + * "INDEX_NAME": { distinct: false } + * } + */ + searchParameters?: AgentStudioSearchParameters; + }; + export type DocSearchSidepanelProps = DocSearchCallbacks & { /** * The assistant ID to use for the ask AI feature. @@ -27,10 +64,6 @@ export type DocSearchSidepanelProps = DocSearchCallbacks & { * The index name to use for the ask AI feature. Your assistant will search this index for relevant documents. */ indexName: string; - /** - * The search parameters to use for the ask AI feature. - */ - searchParameters?: AskAiSearchParameters; /** * Configuration for keyboard shortcuts. Allows enabling/disabling specific shortcuts. * @@ -50,29 +83,13 @@ export type DocSearchSidepanelProps = DocSearchCallbacks & { /** * Props specific to the Sidepanel panel. */ - panel?: Omit; - /** - * **Experimental:** Whether to use Agent Studio as the chat backend. - * - * This is an experimental feature and its API may change without notice in future releases. - * Use with caution in production environments. - * - * @default false - */ - agentStudio?: boolean; + panel?: Omit; }; +type SidepanelProps = DocSearchSidepanelProps & SidepanelSearchParameters; + function DocSearchSidepanelComponent( - { - keyboardShortcuts, - theme, - onReady, - onOpen, - onClose, - onSidepanelOpen, - onSidepanelClose, - ...props - }: DocSearchSidepanelProps, + { keyboardShortcuts, theme, onReady, onOpen, onClose, onSidepanelOpen, onSidepanelClose, ...props }: SidepanelProps, ref: React.ForwardedRef, ): JSX.Element { return ( diff --git a/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx b/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx index 056d7057..4dc7db40 100644 --- a/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx +++ b/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx @@ -57,6 +57,10 @@ export type ConversationScreenTranslations = Partial< * Message displayed after feedback action. **/ thanksForFeedbackText: string; + /** + * Error title shown if there is an error while chatting. + */ + errorTitleText; } >; @@ -98,6 +102,7 @@ const ConversationExchange = React.forwardRef getMessageContent(assistantMessage), [assistantMessage]); @@ -110,7 +115,8 @@ const ConversationExchange = React.forwardRef extractLinksFromMessage(assistantMessage), [assistantMessage]); const wasStopped = userMessage.metadata?.stopped || assistantMessage?.metadata?.stopped; - const isThinking = !assistantParts.some((part) => part.type !== 'step-start'); + const isThinking = + ['submitted', 'streaming'].includes(status) && !assistantParts.some((part) => part.type !== 'step-start'); const showActions = !wasStopped && (!isLastExchange || (isLastExchange && status === 'ready' && Boolean(assistantMessage))); @@ -125,12 +131,15 @@ const ConversationExchange = React.forwardRef - +
+

{errorTitleText}

+ +
)} diff --git a/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx b/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx index 4d66d2de..6d6df3bd 100644 --- a/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx +++ b/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx @@ -3,7 +3,7 @@ import React, { useCallback } from 'react'; import type { JSX } from 'react'; import { AlgoliaLogo, type AlgoliaLogoTranslations } from '../AlgoliaLogo'; -import type { DocSearchSidepanelProps } from '../Sidepanel'; +import type { DocSearchSidepanelProps, SidepanelSearchParameters } from '../Sidepanel'; import type { StoredAskAiState, SuggestedQuestionHit } from '../types'; import { useAskAi } from '../useAskAi'; import { useSearchClient } from '../useSearchClient'; @@ -122,7 +122,8 @@ export type SidepanelProps = { }; type Props = Omit & - SidepanelProps & { + SidepanelProps & + SidepanelSearchParameters & { isOpen?: boolean; onOpen: () => void; onClose: () => void; diff --git a/packages/docsearch-react/src/askai.ts b/packages/docsearch-react/src/askai.ts index 19c8e990..809e24a5 100644 --- a/packages/docsearch-react/src/askai.ts +++ b/packages/docsearch-react/src/askai.ts @@ -102,3 +102,35 @@ export const postFeedback = async ({ headers, }); }; + +interface AgentStudioValidationError extends Error { + name: 'ValidationError'; + detail?: Array<{ type: string; loc: string[]; msg: string }>; +} + +// Parse Agent Studio errors as they are returned as JSON rather than Markdown/text +export const getAgentStudioErrorMessage = (error: Error): Error => { + let errorMessage = error.message; + + try { + const parsedError = JSON.parse(error.message) as Error; + + // Check for known errors that we know how to parse + if (parsedError.name === 'ValidationError') { + const validationError = parsedError as AgentStudioValidationError; + + if (validationError.detail && validationError.detail.length > 0) { + const { msg, loc } = validationError.detail[0]; + const field = loc.at(-1); + + errorMessage = `${msg}: ${field}`; + } + } else { + errorMessage = parsedError.message; + } + } catch { + // We don't care about this catch, we default to the error.message above + } + + return new Error(errorMessage); +}; diff --git a/packages/docsearch-react/src/useAskAi.ts b/packages/docsearch-react/src/useAskAi.ts index cf0cc2bb..a30b6d49 100644 --- a/packages/docsearch-react/src/useAskAi.ts +++ b/packages/docsearch-react/src/useAskAi.ts @@ -3,14 +3,14 @@ import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai'; import { useCallback, useMemo, useRef } from 'react'; -import { getValidToken, postFeedback } from './askai'; +import { getAgentStudioErrorMessage, getValidToken, postFeedback } from './askai'; import type { Exchange } from './AskAiScreen'; import { ASK_AI_API_URL, BETA_ASK_AI_API_URL } from './constants'; import type { StoredSearchPlugin } from './stored-searches'; import { createStoredConversations } from './stored-searches'; import type { AIMessage } from './types/AskiAi'; -import type { AskAiSearchParameters, StoredAskAiState } from '.'; +import type { AgentStudioSearchParameters, AskAiSearchParameters, StoredAskAiState } from '.'; type UseChat = UseChatHelpers; @@ -19,10 +19,19 @@ type UseAskAiParams = { apiKey: string; appId: string; indexName: string; - searchParameters?: AskAiSearchParameters; useStagingEnv?: boolean; - agentStudio?: boolean; -}; + searchParameters?: AskAiSearchParameters; + agentStudio: boolean; +} & ( + | { + agentStudio: false; + searchParameters?: AskAiSearchParameters; + } + | { + agentStudio: true; + searchParameters?: AgentStudioSearchParameters; + } +); type UseAskAiReturn = { messages: AIMessage[]; @@ -39,17 +48,23 @@ type UseAskAiReturn = { type UseAskAi = (params: UseAskAiParams) => UseAskAiReturn; +type AgentStudioTransportParams = Pick & { + searchParameters?: AgentStudioSearchParameters; +}; + const getAgentStudioTransport = ({ appId, apiKey, assistantId, -}: Pick): DefaultChatTransport => { + searchParameters, +}: AgentStudioTransportParams): DefaultChatTransport => { return new DefaultChatTransport({ api: `https://${appId}.algolia.net/agent-studio/1/agents/${assistantId}/completions?stream=true&compatibilityMode=ai-sdk-5`, headers: { 'x-algolia-application-id': appId, 'x-algolia-api-key': apiKey, }, + body: searchParameters ? { algolia: { searchParameters } } : {}, }); }; @@ -90,35 +105,28 @@ const getAskAiTransport = ({ }); }; -export const useAskAi: UseAskAi = ({ - assistantId, - apiKey, - appId, - indexName, - searchParameters, - useStagingEnv = false, - agentStudio = false, -}) => { +export const useAskAi: UseAskAi = ({ assistantId, apiKey, appId, indexName, useStagingEnv = false, ...params }) => { const abortControllerRef = useRef(new AbortController()); const askAiTransport = useMemo( () => - agentStudio + params.agentStudio ? getAgentStudioTransport({ apiKey, appId, assistantId: assistantId ?? '', + searchParameters: params.searchParameters, }) : getAskAiTransport({ assistantId: assistantId ?? '', apiKey, appId, indexName, - searchParameters, + searchParameters: params.searchParameters, abortController: abortControllerRef.current, useStagingEnv, }), - [apiKey, appId, assistantId, indexName, searchParameters, agentStudio, useStagingEnv], + [apiKey, appId, assistantId, indexName, useStagingEnv, params], ); const { messages, sendMessage, status, setMessages, error, stop } = useChat({ @@ -176,12 +184,20 @@ export const useAskAi: UseAskAi = ({ const isStreaming = status === 'streaming' || status === 'submitted'; + const askAiError = useMemo((): Error | undefined => { + if (!error) return undefined; + + if (!params.agentStudio) return error; + + return getAgentStudioErrorMessage(error); + }, [error, params.agentStudio]); + return { messages, sendMessage, status, setMessages, - askAiError: error, + askAiError, stopAskAiStreaming: onStopStreaming, isStreaming, exchanges, diff --git a/packages/docsearch-sidepanel/src/Sidepanel.tsx b/packages/docsearch-sidepanel/src/Sidepanel.tsx index d3d445e6..027cc171 100644 --- a/packages/docsearch-sidepanel/src/Sidepanel.tsx +++ b/packages/docsearch-sidepanel/src/Sidepanel.tsx @@ -1,11 +1,16 @@ import { useDocSearch } from '@docsearch/core'; -import { Sidepanel as SidepanelComp, type DocSearchSidepanelProps } from '@docsearch/react/sidepanel'; +import { + Sidepanel as SidepanelComp, + type DocSearchSidepanelProps, + type SidepanelSearchParameters, +} from '@docsearch/react/sidepanel'; import React from 'react'; import type { JSX } from 'react'; import { createPortal } from 'react-dom'; export type SidepanelProps = DocSearchSidepanelProps['panel'] & - Omit; + Omit & + SidepanelSearchParameters; export function Sidepanel({ portalContainer, ...props }: SidepanelProps): JSX.Element { const { docsearchState, setDocsearchState, keyboardShortcuts, registerView, initialAskAiMessage } = useDocSearch();