Skip to content

Commit 0101c92

Browse files
authored
feat(askai): add Agent Studio memory support (#2888)
1 parent 3da3079 commit 0101c92

15 files changed

Lines changed: 476 additions & 99 deletions

File tree

packages/docsearch-react/src/AskAiScreen.tsx

Lines changed: 92 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, { type JSX, useMemo, useState, useEffect } from 'react';
33

44
import { AggregatedSearchBlock } from './AggregatedSearchBlock';
55
import type { AskAiScreenStateProps } from './AskAiScreenState';
6-
import { ToolCall } from './components/ui/ToolCall';
6+
import { ToolCall, type ToolCallTranslations } from './components/ToolCall';
77
import { AlertIcon, LoadingIcon } from './icons';
88
import { MemoizedMarkdown } from './MemoizedMarkdown';
99
import type { StoredSearchPlugin } from './stored-searches';
@@ -12,56 +12,88 @@ import { type AIMessage, type ToolCalls } from './types/AskiAi';
1212
import { extractLinksFromMessage, getMessageContent, isThreadDepthError, isAIToolPart } from './utils/ai';
1313
import { groupConsecutiveToolResults } from './utils/groupConsecutiveToolResults';
1414

15-
export type AskAiScreenTranslations = Partial<{
16-
// Misc texts
17-
disclaimerText: string;
18-
relatedSourcesText: string;
19-
thinkingText: string;
20-
copyButtonText: string;
21-
copyButtonCopiedText: string;
22-
// Feedback buttons
23-
copyButtonTitle: string;
24-
likeButtonTitle: string;
25-
dislikeButtonTitle: string;
26-
thanksForFeedbackText: string;
27-
// Tool call texts
28-
preToolCallText: string;
29-
duringToolCallText: string;
30-
afterToolCallText: string;
31-
/**
32-
* Build the full jsx element for the aggregated search block.
33-
* If provided, completely overrides the default english renderer.
34-
*/
35-
aggregatedToolCallNode?: (queries: string[], onSearchQueryClick: (query: string) => void) => React.ReactNode;
36-
37-
/**
38-
* Generate the list connective parts only (backwards compatibility).
39-
* Receives full list of queries and should return translation parts for before/after/separators.
40-
* Example: (qs) => ({ before: 'searched for ', separator: ', ', lastSeparator: ' and ', after: '' }).
41-
*/
42-
aggregatedToolCallText?: (queries: string[]) => {
43-
before?: string;
44-
separator?: string;
45-
lastSeparator?: string;
46-
after?: string;
15+
export type AskAiScreenTranslations = Partial<
16+
// Inherit the shared tool-call translations, but expose the search-related
17+
// keys under AskAiScreen's own public names (see mapping below).
18+
Omit<ToolCallTranslations, 'searchingText' | 'toolCallResultText'> & {
19+
// Misc texts
20+
disclaimerText: string;
21+
relatedSourcesText: string;
22+
thinkingText: string;
23+
copyButtonText: string;
24+
copyButtonCopiedText: string;
25+
// Feedback buttons
26+
copyButtonTitle: string;
27+
likeButtonTitle: string;
28+
dislikeButtonTitle: string;
29+
thanksForFeedbackText: string;
30+
// Tool call texts
31+
/**
32+
* Text shown while assistant is performing search tool call.
33+
* Maps to `ToolCallTranslations.searchingText`.
34+
*/
35+
duringToolCallText: string;
36+
/**
37+
* Text shown while assistant is finished performing tool call.
38+
* Maps to `ToolCallTranslations.toolCallResultText`.
39+
*/
40+
afterToolCallText: string;
41+
/**
42+
* Build the full jsx element for the aggregated search block.
43+
* If provided, completely overrides the default english renderer.
44+
*/
45+
aggregatedToolCallNode?: (queries: string[], onSearchQueryClick: (query: string) => void) => React.ReactNode;
46+
/**
47+
* Generate the list connective parts only (backwards compatibility).
48+
* Receives full list of queries and should return translation parts for before/after/separators.
49+
* Example: (qs) => ({ before: 'searched for ', separator: ', ', lastSeparator: ' and ', after: '' }).
50+
*/
51+
aggregatedToolCallText?: (queries: string[]) => {
52+
before?: string;
53+
separator?: string;
54+
lastSeparator?: string;
55+
after?: string;
56+
};
57+
/**
58+
* Message that's shown when user has stopped the streaming of a message.
59+
*/
60+
stoppedStreamingText: string;
61+
/**
62+
* Error title shown if there is an error while chatting.
63+
*/
64+
errorTitleText: string;
65+
/**
66+
* Message shown when thread depth limit is exceeded (AI-217 error).
67+
*/
68+
threadDepthExceededMessage: string;
69+
/**
70+
* Button text for starting a new conversation after thread depth error.
71+
*/
72+
startNewConversationButtonText: string;
73+
}
74+
>;
75+
76+
/**
77+
* Maps AskAiScreen's public translation keys to the shared `ToolCallTranslations`
78+
* shape consumed by the `ToolCall` component, applying default English values.
79+
*/
80+
function toToolCallTranslations(translations: AskAiScreenTranslations): ToolCallTranslations {
81+
const {
82+
preToolCallText = 'Searching...',
83+
duringToolCallText = 'Searching...',
84+
afterToolCallText = 'Searched for',
85+
savedMemoryToolResultText = 'Saved to memory',
86+
memoryToolResultText = 'Used memory to enhance results',
87+
} = translations;
88+
89+
return {
90+
preToolCallText,
91+
searchingText: duringToolCallText,
92+
toolCallResultText: afterToolCallText,
93+
savedMemoryToolResultText,
94+
memoryToolResultText,
4795
};
48-
/**
49-
* Message that's shown when user has stopped the streaming of a message.
50-
*/
51-
stoppedStreamingText: string;
52-
/**
53-
* Error title shown if there is an error while chatting.
54-
*/
55-
errorTitleText: string;
56-
/**
57-
* Message shown when thread depth limit is exceeded (AI-217 error).
58-
*/
59-
threadDepthExceededMessage: string;
60-
/**
61-
* Button text for starting a new conversation after thread depth error.
62-
*/
63-
startNewConversationButtonText: string;
64-
}>;
96+
}
6597

6698
type AskAiScreenProps = Omit<AskAiScreenStateProps<InternalDocSearchHit>, 'translations'> & {
6799
messages: AIMessage[];
@@ -71,6 +103,7 @@ type AskAiScreenProps = Omit<AskAiScreenStateProps<InternalDocSearchHit>, 'trans
71103
translations?: AskAiScreenTranslations;
72104
onNewConversation: () => void;
73105
agentStudio?: boolean;
106+
memoryEnabled?: boolean;
74107
};
75108

76109
interface AskAiScreenHeaderProps {
@@ -98,6 +131,7 @@ interface AskAiExchangeCardProps {
98131
conversations: StoredSearchPlugin<StoredAskAiState>;
99132
onFeedback?: (messageId: string, thumbs: 0 | 1) => Promise<void>;
100133
agentStudio?: boolean;
134+
memoryEnabled?: boolean;
101135
}
102136

103137
function AskAiExchangeCard({
@@ -111,16 +145,13 @@ function AskAiExchangeCard({
111145
conversations,
112146
onFeedback,
113147
agentStudio,
148+
memoryEnabled,
114149
}: AskAiExchangeCardProps): JSX.Element {
115150
const { userMessage, assistantMessage } = exchange;
116151

117-
const {
118-
stoppedStreamingText = 'You stopped this response',
119-
errorTitleText = 'Chat error',
120-
preToolCallText = 'Searching...',
121-
afterToolCallText = 'Searched for',
122-
duringToolCallText = 'Searching...',
123-
} = translations;
152+
const { stoppedStreamingText = 'You stopped this response', errorTitleText = 'Chat error' } = translations;
153+
154+
const toolCallTranslations = useMemo(() => toToolCallTranslations(translations), [translations]);
124155

125156
const isThreadDepth = isThreadDepthError(askAiError);
126157

@@ -191,13 +222,10 @@ function AskAiExchangeCard({
191222
return (
192223
<ToolCall
193224
key={index}
194-
translations={{
195-
preToolCallText,
196-
searchingText: duringToolCallText,
197-
toolCallResultText: afterToolCallText,
198-
}}
225+
translations={toolCallTranslations}
199226
part={part}
200227
tools={tools}
228+
memoryEnabled={memoryEnabled}
201229
onSearchQueryClick={onSearchQueryClick}
202230
/>
203231
);
@@ -377,7 +405,7 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
377405
startNewConversationButtonText = 'Start a new conversation',
378406
} = translations;
379407

380-
const { messages, tools, askAiError, status, agentStudio } = props;
408+
const { messages, tools, askAiError, status, agentStudio, memoryEnabled } = props;
381409

382410
// Check if there's a thread depth error
383411
const hasThreadDepthError = useMemo(() => {
@@ -454,6 +482,7 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
454482
tools={tools}
455483
conversations={props.conversations}
456484
agentStudio={agentStudio}
485+
memoryEnabled={memoryEnabled}
457486
onSearchQueryClick={handleSearchQueryClick}
458487
onFeedback={props.onFeedback}
459488
/>

packages/docsearch-react/src/AskAiScreenState.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface AskAiScreenStateProps<TItem extends BaseItem>
5858
selectSuggestedQuestion: (question: SuggestedQuestionHit) => void;
5959
onNewConversation: () => void;
6060
agentStudio?: boolean;
61+
memoryEnabled?: boolean;
6162
}
6263

6364
export const AskAiScreenState = React.memo(
@@ -86,6 +87,7 @@ export const AskAiScreenState = React.memo(
8687
askAiError={props.askAiError}
8788
translations={translations?.askAiScreen}
8889
agentStudio={props.agentStudio}
90+
memoryEnabled={props.memoryEnabled}
8991
/>
9092
);
9193
}

packages/docsearch-react/src/DocSearch.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,21 @@ export interface DocSearchProps {
234234
keyboardShortcuts?: DocSearchModalShortcuts;
235235
}
236236

237+
export interface Memory {
238+
/**
239+
* Determines whether or not to display the memory based tool calls.
240+
*
241+
* @default false
242+
*/
243+
enabled?: boolean;
244+
/**
245+
* The JWT used by the agent to know which user's memory to read.
246+
*
247+
* @see https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/user-authentication
248+
*/
249+
userToken?: string;
250+
}
251+
237252
export interface DocSearchAIProps extends DocSearchProps {
238253
/**
239254
* Configuration or assistant id to enable ask ai mode. Pass a string assistant id or a full config object.
@@ -254,6 +269,10 @@ export interface DocSearchAIProps extends DocSearchProps {
254269
* render but will not affect correctness.
255270
**/
256271
tools?: ToolCalls;
272+
/**
273+
* Configuration for the Agent Studio memory feature.
274+
*/
275+
memory?: Memory;
257276
}
258277

259278
function DocSearchComponent(props: DocSearchProps, ref: React.ForwardedRef<DocSearchRef>): JSX.Element {

packages/docsearch-react/src/DocSearchAskAiModal.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export function DocSearchAskAiModal({
117117
suggestedQuestionsEnabled: askAiConfig?.suggestedQuestions,
118118
});
119119
const agentStudio = askAiConfig?.agentStudio ?? false;
120+
const memoryEnabled = props.memory?.enabled ?? false;
120121

121122
const indexes = normalizeDocSearchIndexes({
122123
indexName,
@@ -143,6 +144,7 @@ export function DocSearchAskAiModal({
143144
useStagingEnv: askAiUseStagingEnv,
144145
agentStudio,
145146
tools,
147+
memory: props.memory,
146148
});
147149

148150
const prevStatus = React.useRef(status);
@@ -464,6 +466,7 @@ export function DocSearchAskAiModal({
464466
suggestedQuestions={suggestedQuestions}
465467
selectSuggestedQuestion={selectSuggestedQuestion}
466468
agentStudio={agentStudio}
469+
memoryEnabled={memoryEnabled}
467470
onAskAiToggle={onAskAiToggle}
468471
onNewConversation={handleNewConversation}
469472
onItemClick={(item, event) => {

packages/docsearch-react/src/Sidepanel.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { JSX } from 'react';
44
import React from 'react';
55
import { createPortal } from 'react-dom';
66

7-
import type { AgentStudioSearchParameters, AskAiSearchParameters } from './DocSearch';
7+
import type { AgentStudioSearchParameters, AskAiSearchParameters, Memory } from './DocSearch';
88
import type { SidepanelButtonProps, SidepanelProps as SidepanelPanelProps } from './Sidepanel/index';
99
import { SidepanelButton, Sidepanel } from './Sidepanel/index';
1010
import type { ToolCalls } from './types/AskiAi';
@@ -93,6 +93,10 @@ export type DocSearchSidepanelProps = DocSearchCallbacks & {
9393
* render but will not affect correctness.
9494
**/
9595
tools?: ToolCalls;
96+
/**
97+
* Configuration for the Agent Studio memory feature.
98+
*/
99+
memory?: Memory;
96100
};
97101

98102
type SidepanelProps = DocSearchSidepanelProps & SidepanelSearchParameters;

packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { JSX } from 'react';
33
import React, { memo, useMemo } from 'react';
44

55
import { AskAiSourcesPanel, type Exchange } from '../AskAiScreen';
6-
import { ToolCall, type ToolCallTranslations } from '../components/ui/ToolCall';
6+
import { ToolCall, type ToolCallTranslations } from '../components/ToolCall';
77
import { AlertIcon, LoadingIcon } from '../icons';
88
import { MemoizedMarkdown } from '../MemoizedMarkdown';
99
import type { StoredSearchPlugin } from '../stored-searches';
@@ -72,6 +72,7 @@ export type ConversationScreenProps = {
7272
handleFeedback?: (messageId: string, thumbs: 0 | 1) => Promise<void>;
7373
streamError?: Error;
7474
agentStudio?: boolean;
75+
memoryEnabled?: boolean;
7576
tools?: ToolCalls;
7677
};
7778

@@ -84,12 +85,24 @@ type ConversationnExchangeProps = {
8485
onFeedback?: ConversationScreenProps['handleFeedback'];
8586
streamError?: ConversationScreenProps['streamError'];
8687
agentStudio?: boolean;
88+
memoryEnabled?: boolean;
8789
tools: ToolCalls;
8890
};
8991

9092
const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExchangeProps>(
9193
(
92-
{ exchange, translations = {}, isLastExchange, conversations, onFeedback, status, streamError, agentStudio, tools },
94+
{
95+
exchange,
96+
translations = {},
97+
isLastExchange,
98+
conversations,
99+
onFeedback,
100+
status,
101+
streamError,
102+
agentStudio,
103+
memoryEnabled,
104+
tools,
105+
},
93106
conversationRef,
94107
): JSX.Element => {
95108
const { userMessage, assistantMessage } = exchange;
@@ -105,6 +118,8 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
105118
copyButtonText = 'Copy',
106119
copyButtonCopiedText = 'Copied!',
107120
errorTitleText = 'Chat error',
121+
savedMemoryToolResultText = 'Saved to memory',
122+
memoryToolResultText = 'Used memory to enhance results',
108123
} = translations;
109124

110125
const assistantContent = useMemo(() => getMessageContent(assistantMessage), [assistantMessage]);
@@ -172,8 +187,11 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
172187
preToolCallText,
173188
searchingText,
174189
toolCallResultText,
190+
savedMemoryToolResultText,
191+
memoryToolResultText,
175192
}}
176193
tools={tools}
194+
memoryEnabled={memoryEnabled}
177195
/>
178196
);
179197
}

packages/docsearch-react/src/Sidepanel/Sidepanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ function SidepanelInner(
153153
useStagingEnv = false,
154154
agentStudio = false,
155155
tools = EMPTY_TOOLS,
156+
memory,
156157
}: Props,
157158
ref: React.ForwardedRef<SidepanelRef>,
158159
): JSX.Element {
@@ -197,6 +198,7 @@ function SidepanelInner(
197198
useStagingEnv,
198199
agentStudio,
199200
tools,
201+
memory,
200202
});
201203

202204
const suggestedQuestions = useSuggestedQuestions({
@@ -400,6 +402,7 @@ function SidepanelInner(
400402
translations={translations.conversationScreen}
401403
streamError={askAiError}
402404
agentStudio={agentStudio}
405+
memoryEnabled={memory?.enabled ?? false}
403406
tools={tools}
404407
/>
405408
)}

0 commit comments

Comments
 (0)