diff --git a/examples/demo-react/src/examples/hybrid.tsx b/examples/demo-react/src/examples/hybrid.tsx
index 745a0014..979aa1d4 100644
--- a/examples/demo-react/src/examples/hybrid.tsx
+++ b/examples/demo-react/src/examples/hybrid.tsx
@@ -10,20 +10,19 @@ export default function BasicHybrid(): JSX.Element {
);
diff --git a/packages/docsearch-react/src/DocSearch.tsx b/packages/docsearch-react/src/DocSearch.tsx
index 55773215..6d39f30b 100644
--- a/packages/docsearch-react/src/DocSearch.tsx
+++ b/packages/docsearch-react/src/DocSearch.tsx
@@ -1,6 +1,6 @@
import type { AutocompleteOptions, AutocompleteState } from '@algolia/autocomplete-core';
import { DocSearch as DocSearchProvider, useDocSearch } from '@docsearch/core';
-import type { DocSearchModalShortcuts, DocSearchRef, InitialAskAiMessage } from '@docsearch/core';
+import type { DocSearchModalShortcuts, DocSearchRef } from '@docsearch/core';
import type { LiteClient, SearchParamsObject } from 'algoliasearch/lite';
import React, { type JSX } from 'react';
import { createPortal } from 'react-dom';
@@ -10,7 +10,6 @@ import type { ButtonTranslations } from './DocSearchButton';
import { DocSearchModal } from './DocSearchModal';
import type { ModalTranslations } from './DocSearchModal';
import type { DocSearchHit, DocSearchTheme, InternalDocSearchHit, StoredDocSearchHit } from './types';
-import type { ToolCalls } from './types/AskiAi';
export type { DocSearchRef } from '@docsearch/core';
@@ -26,55 +25,6 @@ export type DocSearchTransformClient = {
transporter: Pick;
};
-// Define the specific search parameters allowed for Ask AI
-export type AskAiSearchParameters = {
- facetFilters?: string[];
- filters?: string;
- attributesToRetrieve?: string[];
- restrictSearchableAttributes?: string[];
- 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.
- * If not provided, the index name will be used.
- */
- indexName?: string;
- /**
- * The API key to use for the ask AI feature. Your assistant will use this API key to search the index.
- * If not provided, the API key will be used.
- */
- apiKey?: string;
- /**
- * The app ID to use for the ask AI feature. Your assistant will use this app ID to search the index.
- * If not provided, the app ID will be used.
- */
- appId?: string;
- /**
- * The assistant ID to use for the ask AI feature.
- */
- assistantId: string;
- /**
- * Enables displaying suggested questions on Ask AI's new conversation screen.
- *
- * @default false
- */
- suggestedQuestions?: boolean;
- /**
- * 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;
searchParameters?: SearchParamsObject;
@@ -206,47 +156,6 @@ export interface DocSearchProps {
keyboardShortcuts?: DocSearchModalShortcuts;
}
-export interface Memory {
- /**
- * Determines whether or not to display the memory based tool calls.
- *
- * @default false
- */
- enabled?: boolean;
- /**
- * The JWT used by the agent to know which user's memory to read.
- *
- * @see https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/user-authentication
- */
- userToken?: string;
-}
-
-export interface DocSearchAIProps extends DocSearchProps {
- /**
- * Configuration or assistant id to enable ask ai mode. Pass a string assistant id or a full config object.
- */
- askAi: DocSearchAskAi | string;
- /**
- * Intercept Ask AI requests (e.g. Submitting a prompt or selecting a suggested question).
- *
- * Return `true` to prevent the default modal Ask AI flow (no toggle, no sendMessage).
- * Useful to route Ask AI into a different UI (e.g. `@docsearch/sidepanel-js`) without flicker.
- */
- interceptAskAiEvent?: (initialMessage: InitialAskAiMessage) => boolean | void;
- /**
- * Use custom tools driven by Agent Studio.
- *
- * For best performance, memoize this object with `useMemo` or define it
- * outside the component. Inline object literals will be recreated every
- * render but will not affect correctness.
- **/
- tools?: ToolCalls;
- /**
- * Configuration for the Agent Studio memory feature.
- */
- memory?: Memory;
-}
-
function DocSearchComponent(props: DocSearchProps, ref: React.ForwardedRef): JSX.Element {
return (
diff --git a/packages/docsearch-react/src/DocSearchAI.tsx b/packages/docsearch-react/src/DocSearchAI.tsx
index 25e4b3b1..a6d3d002 100644
--- a/packages/docsearch-react/src/DocSearchAI.tsx
+++ b/packages/docsearch-react/src/DocSearchAI.tsx
@@ -1,11 +1,214 @@
import { DocSearch as DocSearchProvider, useDocSearch } from '@docsearch/core';
-import type { DocSearchRef } from '@docsearch/core';
+import type { DocSearchRef, InitialAskAiMessage } from '@docsearch/core';
+import type { SearchParamsObject } from 'algoliasearch/lite';
import React, { type JSX } from 'react';
import { createPortal } from 'react-dom';
-import type { DocSearchAIProps } from './DocSearch';
+import type { DocSearchProps } from './DocSearch';
import { DocSearchAskAiModal } from './DocSearchAskAiModal';
import { DocSearchButton } from './DocSearchButton';
+import type { ToolCalls } from './types/AskiAi';
+
+export interface AskAiSearchParameters {
+ facetFilters?: string[];
+ filters?: string;
+ attributesToRetrieve?: string[];
+ restrictSearchableAttributes?: string[];
+ distinct?: boolean | number | string;
+}
+
+export type AgentStudioSearchParameters = Record>;
+
+interface IndexTextParam {
+ exposed: boolean;
+ default?: string;
+}
+
+interface NumberConstraint {
+ min?: number;
+ max?: number;
+}
+
+interface IndexNumberParam {
+ exposed: boolean;
+ default?: number;
+ constraint?: NumberConstraint;
+}
+
+interface StringArrayConstraints {
+ values?: string[];
+}
+
+interface IndexStringArrayParam {
+ exposed: boolean;
+ default?: string[];
+ constraint?: StringArrayConstraints;
+ merge?: boolean;
+}
+
+interface IndexFacetParam {
+ exposed: false;
+ default?: string[];
+}
+
+export interface AgentStudioSearchControls {
+ /**
+ * Augmented query for the MCP search tool to use.
+ *
+ * @default undefined
+ */
+ query?: IndexTextParam;
+ /**
+ * Number of hits for the MCP to return per page.
+ *
+ * @default { exposed: false, default: 7 }
+ */
+ hits_per_page?: IndexNumberParam;
+ /**
+ * The page number the MCP should pull results from.
+ *
+ * @default { exposed: false, default: 0 }
+ */
+ page?: IndexNumberParam;
+ /**
+ * List of attributes that the MCP can retrieve from the index.
+ *
+ * @default { exposed: false, default: ['*'] }
+ */
+ attributesToRetrieve?: IndexStringArrayParam;
+ /**
+ * List of fields that the MCP will return to the Agent.
+ *
+ * @default { exposed: false, default: ["hits", "nbHits", "page", "nbPages", "hitsPerPage", "facets"] }
+ */
+ responseFields?: IndexStringArrayParam;
+ /**
+ * Defined facets the MCP will use when querying the index.
+ *
+ * @default undefined
+ */
+ facets?: IndexFacetParam;
+ /**
+ * Any other custom properties the MCP should send when querying the index.
+ *
+ * @default undefined
+ */
+ custom?: Record;
+}
+
+export interface AgentStudioIndices {
+ /**
+ * The name of the index used by the search tool.
+ */
+ index: string;
+ /**
+ * A brief description for the search tool.
+ */
+ description: string;
+ /**
+ * A description used to steer the agent on how/when to use the search tool.
+ *
+ * @default ""
+ */
+ enhancedDescription?: string;
+ /**
+ * Default search parameters for the internal (non-MCP) search tool path.
+ *
+ * @default undefined
+ */
+ searchParameters?: SearchParamsObject;
+ /**
+ * Structured search parameters for the MCP-based search tool path.
+ *
+ * Each parameter controls whether it is exposed to the LLM and it's default value.
+ *
+ * @default undefined
+ */
+ searchControls?: AgentStudioSearchControls;
+}
+
+export interface DocSearchAskAi {
+ /**
+ * The index name to use for the Ask AI feature. Your assistant will search for relevant documents.
+ * If not provided, the root index name will be used.
+ */
+ indexName?: string;
+ /**
+ * The API key to use for the ask AI feature. Your assistant will use this API key to search the index.
+ * If not provided, the API key will be used.
+ */
+ apiKey?: string;
+ /**
+ * The app ID to use for the ask AI feature. Your assistant will use this app ID to search the index.
+ * If not provided, the app ID will be used.
+ */
+ appId?: string;
+ /**
+ * The assistant ID to use for the ask AI feature.
+ */
+ assistantId: string;
+ /**
+ * Enables displaying suggested questions on Ask AI's new conversation screen.
+ *
+ * @default false
+ */
+ suggestedQuestions?: boolean;
+ /**
+ * The search parameters to use for the ask AI feature.
+ * Keyed by the index name.
+ *
+ * @example
+ * {
+ * "INDEX_NAME": { distinct: false }
+ * }
+ */
+ searchParameters?: AgentStudioSearchParameters;
+ /**
+ * List of dynamic indices for the Agent Studio search tool to use.
+ */
+ indices?: AgentStudioIndices[];
+}
+
+export interface Memory {
+ /**
+ * Determines whether or not to display the memory based tool calls.
+ *
+ * @default false
+ */
+ enabled?: boolean;
+ /**
+ * The JWT used by the agent to know which user's memory to read.
+ *
+ * @see https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/user-authentication
+ */
+ userToken?: string;
+}
+
+export interface DocSearchAIProps extends DocSearchProps {
+ /**
+ * Configuration or assistant id to enable ask ai mode. Pass a string assistant id or a full config object.
+ */
+ askAi: DocSearchAskAi | string;
+ /**
+ * Intercept Ask AI requests (e.g. Submitting a prompt or selecting a suggested question).
+ *
+ * Return `true` to prevent the default modal Ask AI flow (no toggle, no sendMessage).
+ * Useful to route Ask AI into a different UI (e.g. `@docsearch/sidepanel-js`) without flicker.
+ */
+ interceptAskAiEvent?: (initialMessage: InitialAskAiMessage) => boolean | void;
+ /**
+ * Use custom tools driven by Agent Studio.
+ *
+ * For best performance, memoize this object with `useMemo` or define it
+ * outside the component. Inline object literals will be recreated every
+ * render but will not affect correctness.
+ **/
+ tools?: ToolCalls;
+ /**
+ * Configuration for the Agent Studio memory feature.
+ */
+ memory?: Memory;
+}
function DocSearchAIComponent(props: DocSearchAIProps, ref: React.ForwardedRef): JSX.Element {
return (
diff --git a/packages/docsearch-react/src/DocSearchAskAiModal.tsx b/packages/docsearch-react/src/DocSearchAskAiModal.tsx
index e90f9fa1..8c8a3e8e 100644
--- a/packages/docsearch-react/src/DocSearchAskAiModal.tsx
+++ b/packages/docsearch-react/src/DocSearchAskAiModal.tsx
@@ -8,7 +8,7 @@ import { AskAiScreenState } from './AskAiScreenState';
import type { AskAiSearchBoxTranslations } from './components/AskAiSearchBox';
import { AskAiSearchBox } from './components/AskAiSearchBox';
import { ModalShell } from './components/ui/ModalShell';
-import type { DocSearchAIProps } from './DocSearch';
+import type { DocSearchAIProps } from './DocSearchAI';
import type { FooterTranslations } from './Footer';
import { Footer } from './Footer';
import { Hit } from './Hit';
@@ -113,7 +113,7 @@ export function DocSearchAskAiModal({
const searchClient = useSearchClient(appId, apiKey, transformSearchClient);
const askAiConfig = typeof askAi === 'object' ? askAi : null;
- const askAiConfigurationId = typeof askAi === 'string' ? askAi : askAiConfig?.assistantId || null;
+ const askAiConfigurationId = askAiConfig ? askAiConfig.assistantId : (askAi as string);
const askAiSearchParameters = askAiConfig?.searchParameters;
const [askAiState, setAskAiState] = React.useState('initial');
const suggestedQuestions = useSuggestedQuestions({
@@ -147,6 +147,7 @@ export function DocSearchAskAiModal({
searchParameters: askAiSearchParameters,
tools,
memory: props.memory,
+ indices: askAiConfig?.indices,
});
const prevStatus = React.useRef(status);
diff --git a/packages/docsearch-react/src/Sidepanel.tsx b/packages/docsearch-react/src/Sidepanel.tsx
index a09d8db4..7ce3c406 100644
--- a/packages/docsearch-react/src/Sidepanel.tsx
+++ b/packages/docsearch-react/src/Sidepanel.tsx
@@ -4,7 +4,7 @@ import type { JSX } from 'react';
import React from 'react';
import { createPortal } from 'react-dom';
-import type { AgentStudioSearchParameters, Memory } from './DocSearch';
+import type { AgentStudioIndices, AgentStudioSearchParameters, Memory } from './DocSearchAI';
import type { SidepanelButtonProps, SidepanelProps as SidepanelPanelProps } from './Sidepanel/index';
import { SidepanelButton, Sidepanel } from './Sidepanel/index';
import type { ToolCalls } from './types/AskiAi';
@@ -73,6 +73,10 @@ export type DocSearchSidepanelProps = DocSearchCallbacks & {
* Configuration for the Agent Studio memory feature.
*/
memory?: Memory;
+ /**
+ * List of dynamic indices for the Agent Studio search tool to use.
+ */
+ indices?: AgentStudioIndices[];
};
type SidepanelProps = DocSearchSidepanelProps & SidepanelSearchParameters;
diff --git a/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx b/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx
index 26ecd339..eddbd086 100644
--- a/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx
+++ b/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx
@@ -150,6 +150,7 @@ function SidepanelInner(
initialMessage,
tools = EMPTY_TOOLS,
memory,
+ indices,
}: Props,
ref: React.ForwardedRef,
): JSX.Element {
@@ -193,6 +194,7 @@ function SidepanelInner(
searchParameters,
tools,
memory,
+ indices,
});
const suggestedQuestions = useSuggestedQuestions({
diff --git a/packages/docsearch-react/src/__tests__/api.test.tsx b/packages/docsearch-react/src/__tests__/api.test.tsx
index 0ac72edf..534ca094 100644
--- a/packages/docsearch-react/src/__tests__/api.test.tsx
+++ b/packages/docsearch-react/src/__tests__/api.test.tsx
@@ -5,8 +5,9 @@ import { describe, it, expect, afterEach } from 'vitest';
import '@testing-library/jest-dom/vitest';
import { DocSearch as DocSearchComponent } from '../DocSearch';
-import type { DocSearchAIProps, DocSearchProps } from '../DocSearch';
+import type { DocSearchProps } from '../DocSearch';
import { DocSearchAI as DocSearchAIComponent } from '../DocSearchAI';
+import type { DocSearchAIProps } from '../DocSearchAI';
function DocSearch(props: Partial): JSX.Element {
return ;
diff --git a/packages/docsearch-react/src/__tests__/useAskAi.test.tsx b/packages/docsearch-react/src/__tests__/useAskAi.test.tsx
index 9f99a8a7..64ebe196 100644
--- a/packages/docsearch-react/src/__tests__/useAskAi.test.tsx
+++ b/packages/docsearch-react/src/__tests__/useAskAi.test.tsx
@@ -11,7 +11,12 @@ type ToolCall = {
type ChatOptions = {
onToolCall: (params: { toolCall: ToolCall }) => unknown;
- transport: { options: { headers?: Record } };
+ transport: {
+ options: {
+ headers?: Record;
+ body?: Record;
+ };
+ };
};
type CustomOnToolCallParams = ToolCall & {
@@ -57,6 +62,14 @@ describe('useAskAi', () => {
return chatOptions.transport.options.headers ?? {};
}
+ function getTransportBody(): Record {
+ if (!chatOptions) {
+ throw new Error('useChat was not initialized');
+ }
+
+ return chatOptions.transport.options.body ?? {};
+ }
+
beforeEach(() => {
vi.clearAllMocks();
@@ -187,4 +200,106 @@ describe('useAskAi', () => {
expect(getTransportHeaders()).not.toHaveProperty('x-algolia-secure-user-token');
});
+
+ it('sends an empty transport body when no search parameters or indices are provided', () => {
+ renderHook(() =>
+ useAskAi({
+ apiKey: 'api-key',
+ appId: 'app-id',
+ assistantId: 'assistant-id',
+ indexName: 'index-name',
+ tools: {},
+ }),
+ );
+
+ expect(getTransportBody()).toEqual({ algolia: {} });
+ });
+
+ it('includes searchParameters under the algolia body when provided', () => {
+ const searchParameters = {
+ 'index-name': { distinct: false },
+ };
+
+ renderHook(() =>
+ useAskAi({
+ apiKey: 'api-key',
+ appId: 'app-id',
+ assistantId: 'assistant-id',
+ indexName: 'index-name',
+ tools: {},
+ searchParameters,
+ }),
+ );
+
+ expect(getTransportBody()).toEqual({
+ algolia: { searchParameters },
+ });
+ });
+
+ it('includes indices under the algolia body when provided', () => {
+ const indices = [
+ {
+ index: 'docsearch-markdown',
+ description: 'Use this to gather specific results.',
+ },
+ ];
+
+ renderHook(() =>
+ useAskAi({
+ apiKey: 'api-key',
+ appId: 'app-id',
+ assistantId: 'assistant-id',
+ indexName: 'index-name',
+ tools: {},
+ indices,
+ }),
+ );
+
+ expect(getTransportBody()).toEqual({
+ algolia: { indices },
+ });
+ });
+
+ it('includes both searchParameters and indices under the algolia body when both are provided', () => {
+ const searchParameters = {
+ 'index-name': { distinct: false },
+ };
+ const indices = [
+ {
+ index: 'docsearch-markdown',
+ description: 'Use this to gather specific results.',
+ },
+ ];
+
+ renderHook(() =>
+ useAskAi({
+ apiKey: 'api-key',
+ appId: 'app-id',
+ assistantId: 'assistant-id',
+ indexName: 'index-name',
+ tools: {},
+ searchParameters,
+ indices,
+ }),
+ );
+
+ expect(getTransportBody()).toEqual({
+ algolia: { searchParameters, indices },
+ });
+ });
+
+ it('omits indices from the body when an empty indices array is provided', () => {
+ renderHook(() =>
+ useAskAi({
+ apiKey: 'api-key',
+ appId: 'app-id',
+ assistantId: 'assistant-id',
+ indexName: 'index-name',
+ tools: {},
+ indices: [],
+ }),
+ );
+
+ expect(getTransportBody()).toEqual({ algolia: {} });
+ });
});
diff --git a/packages/docsearch-react/src/useAskAi.ts b/packages/docsearch-react/src/useAskAi.ts
index 40ba7f81..0bbb37f1 100644
--- a/packages/docsearch-react/src/useAskAi.ts
+++ b/packages/docsearch-react/src/useAskAi.ts
@@ -12,18 +12,19 @@ import { type AIMessage, type ToolCalls } from './types/AskiAi';
import type { OnAskAiFeedback } from './types/Feedback';
import { EMPTY_TOOLS } from './utils/ai';
-import type { AgentStudioSearchParameters, Memory, StoredAskAiState } from '.';
+import type { AgentStudioIndices, AgentStudioSearchParameters, Memory, StoredAskAiState } from '.';
type UseChat = UseChatHelpers;
type UseAskAiParams = {
- assistantId?: string | null;
+ assistantId: string;
apiKey: string;
appId: string;
indexName: string;
searchParameters?: AgentStudioSearchParameters;
tools: ToolCalls;
memory?: Memory;
+ indices?: AgentStudioIndices[];
};
type UseAskAiReturn = {
@@ -44,6 +45,7 @@ type UseAskAi = (params: UseAskAiParams) => UseAskAiReturn;
type AgentStudioTransportParams = Pick & {
searchParameters?: AgentStudioSearchParameters;
userToken?: string;
+ indices?: AgentStudioIndices[];
};
const getAgentStudioTransport = ({
@@ -52,7 +54,21 @@ const getAgentStudioTransport = ({
assistantId,
searchParameters,
userToken,
+ indices,
}: AgentStudioTransportParams): DefaultChatTransport => {
+ const algoliaParams: {
+ searchParameters?: AgentStudioSearchParameters;
+ indices?: AgentStudioIndices[];
+ } = {};
+
+ if (searchParameters) {
+ algoliaParams.searchParameters = searchParameters;
+ }
+
+ if (indices && indices.length > 0) {
+ algoliaParams.indices = indices;
+ }
+
return new DefaultChatTransport({
api: `${agentStudioBaseUrl(appId)}/agents/${assistantId}/completions?stream=true&compatibilityMode=ai-sdk-5`,
headers: {
@@ -60,7 +76,7 @@ const getAgentStudioTransport = ({
'x-algolia-api-key': apiKey,
...(userToken ? { 'x-algolia-secure-user-token': userToken } : {}),
},
- body: searchParameters ? { algolia: { searchParameters } } : {},
+ body: { algolia: algoliaParams },
});
};
@@ -72,6 +88,7 @@ export const useAskAi: UseAskAi = ({
tools = EMPTY_TOOLS,
searchParameters,
memory,
+ indices,
}) => {
const abortControllerRef = useRef(new AbortController());
@@ -80,11 +97,12 @@ export const useAskAi: UseAskAi = ({
getAgentStudioTransport({
apiKey,
appId,
- assistantId: assistantId ?? '',
+ assistantId,
searchParameters,
userToken: memory?.userToken,
+ indices,
}),
- [apiKey, appId, assistantId, searchParameters, memory?.userToken],
+ [apiKey, appId, assistantId, searchParameters, memory?.userToken, indices],
);
// Sync ref during render so the stable `handleToolCall` (registered once