feat(v5): Add follow up prompt suggestions (#2902)
* feat(v5): Add follow up prompt suggestions * fix: bump css bundle size limit
This commit is contained in:
parent
77e7470da4
commit
2b97bab785
12 changed files with 374 additions and 23 deletions
|
|
@ -2,7 +2,7 @@
|
|||
"files": [
|
||||
{
|
||||
"path": "packages/docsearch-css/dist/style.css",
|
||||
"maxSize": "7.80 kB"
|
||||
"maxSize": "8 kB"
|
||||
},
|
||||
{
|
||||
"path": "packages/docsearch-react/dist/umd/index.js",
|
||||
|
|
|
|||
|
|
@ -1234,7 +1234,7 @@ assistive tech users */
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 0.75em;
|
||||
gap: 0.8125rem;
|
||||
font-size: 0.8em;
|
||||
background: var(--docsearch-hit-background);
|
||||
padding: 16px;
|
||||
|
|
@ -1465,6 +1465,68 @@ assistive tech users */
|
|||
background: var(--docsearch-subtle-color);
|
||||
}
|
||||
|
||||
/* Prompt Suggestions */
|
||||
|
||||
.DocSearch-PromptSuggestions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.DocSearch-PromptSuggestions-Title {
|
||||
color: var(--docsearch-text-color);
|
||||
font-size: 0.875em;
|
||||
line-height: 1.25em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.DocSearch-PromptSuggestions-Content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
justify-content: start;
|
||||
gap: 0.625rem;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
align-items: unset;
|
||||
justify-content: unset;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-PromptSuggestions-Action {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--docsearch-subtle-color);
|
||||
padding-inline: 0.625rem;
|
||||
padding-block: 0.4375rem;
|
||||
border-radius: 100vw;
|
||||
background-color: var(--docsearch-modal-background);
|
||||
font-size: 0.875em;
|
||||
color: var(--docsearch-text-color);
|
||||
font-weight: 400;
|
||||
min-height: 1.875rem;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, color, border-color;
|
||||
transition-duration: 0.2s;
|
||||
transition-timing-function: ease;
|
||||
|
||||
@media screen and (prefers-reduced-motion: reduce) {
|
||||
transition-property: none;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--docsearch-primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--docsearch-soft-primary-color);
|
||||
color: var(--docsearch-highlight-color);
|
||||
border-color: var(--docsearch-soft-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-AskAiScreen-ActionButton {
|
||||
display: flex;
|
||||
|
|
@ -2081,10 +2143,21 @@ html[data-theme='dark'] .DocSearch-AskAiScreen-MessageContent-Thinking {
|
|||
padding-block: 0.375em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--docsearch-text-color);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--docsearch-soft-primary-color);
|
||||
color: var(--docsearch-highlight-color);
|
||||
border-color: var(--docsearch-soft-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-AskAiScreen-Sources-Action-icon {
|
||||
color: var(--docsearch-secondary-text-color);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -2105,7 +2178,6 @@ html[data-theme='dark'] .DocSearch-AskAiScreen-MessageContent-Thinking {
|
|||
font-size: 1em;
|
||||
line-height: 1.15em;
|
||||
white-space: nowrap;
|
||||
color: var(--docsearch-secondary-text-color);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -645,6 +645,10 @@ html[data-theme="dark"] .DocSearch-Sidepanel-Prompt--stop:hover {
|
|||
}
|
||||
}
|
||||
|
||||
.DocSearch-Sidepanel .DocSearch-AskAiScreen-Response-Container {
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
.DocSearch-Sidepanel .DocSearch-AskAiScreen-Response {
|
||||
padding: 0;
|
||||
font-size: 0.95em;
|
||||
|
|
@ -690,6 +694,17 @@ html[data-theme="dark"] .DocSearch-Sidepanel-Prompt--stop:hover {
|
|||
right: -4rem;
|
||||
}
|
||||
|
||||
.DocSearch-Sidepanel {
|
||||
@media screen and (min-width: 768px) {
|
||||
.DocSearch-PromptSuggestions-Content {
|
||||
align-items: start;
|
||||
justify-content: start;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React, { type JSX, useMemo } from 'react';
|
|||
|
||||
import { AggregatedSearchBlock } from './AggregatedSearchBlock';
|
||||
import type { AskAiScreenStateProps } from './AskAiScreenState';
|
||||
import { ConversationPromptSuggestions } from './components/ConversationPromptSuggestions';
|
||||
import { FeedbackActions } from './components/FeedbackActions';
|
||||
import { SourcesPanel } from './components/SourcesPanel';
|
||||
import { ToolCall, type ToolCallTranslations } from './components/ToolCall';
|
||||
|
|
@ -11,7 +12,13 @@ import { MemoizedMarkdown } from './MemoizedMarkdown';
|
|||
import type { StoredSearchPlugin } from './stored-searches';
|
||||
import type { InternalDocSearchHit, OnAskAiFeedback, StoredAskAiState } from './types';
|
||||
import { type AIMessage, type ToolCalls } from './types/AskiAi';
|
||||
import { extractLinksFromMessage, getMessageContent, isThreadDepthError, isAIToolPart } from './utils/ai';
|
||||
import {
|
||||
extractLinksFromMessage,
|
||||
getMessageContent,
|
||||
isThreadDepthError,
|
||||
isAIToolPart,
|
||||
getAgentPromptSuggestions,
|
||||
} from './utils/ai';
|
||||
import { groupConsecutiveToolResults } from './utils/groupConsecutiveToolResults';
|
||||
|
||||
export type AskAiScreenTranslations = Partial<
|
||||
|
|
@ -84,6 +91,7 @@ export type AskAiScreenTranslations = Partial<
|
|||
* Button text for starting a new conversation after thread depth error.
|
||||
*/
|
||||
startNewConversationButtonText: string;
|
||||
suggestedPromptsTitleText: string;
|
||||
}
|
||||
>;
|
||||
|
||||
|
|
@ -116,6 +124,7 @@ type AskAiScreenProps = Omit<AskAiScreenStateProps<InternalDocSearchHit>, 'trans
|
|||
askAiError?: Error;
|
||||
translations?: AskAiScreenTranslations;
|
||||
onNewConversation: () => void;
|
||||
onSelectPromptSuggestion: (prompt: string) => void;
|
||||
memoryEnabled?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -148,6 +157,7 @@ interface AskAiExchangeCardProps {
|
|||
conversations: StoredSearchPlugin<StoredAskAiState>;
|
||||
onFeedback?: OnAskAiFeedback;
|
||||
memoryEnabled?: boolean;
|
||||
onSelectPromptSuggestion: (prompt: string) => void;
|
||||
}
|
||||
|
||||
function AskAiExchangeCard({
|
||||
|
|
@ -161,6 +171,7 @@ function AskAiExchangeCard({
|
|||
conversations,
|
||||
onFeedback,
|
||||
memoryEnabled,
|
||||
onSelectPromptSuggestion,
|
||||
}: AskAiExchangeCardProps): JSX.Element {
|
||||
const { userMessage, assistantMessage } = exchange;
|
||||
|
||||
|
|
@ -168,6 +179,7 @@ function AskAiExchangeCard({
|
|||
stoppedStreamingText = 'You stopped this response',
|
||||
errorTitleText = 'Chat error',
|
||||
relatedSourcesText,
|
||||
suggestedPromptsTitleText = 'Suggested prompts',
|
||||
} = translations;
|
||||
|
||||
const toolCallTranslations = useMemo(() => toToolCallTranslations(translations), [translations]);
|
||||
|
|
@ -183,6 +195,11 @@ function AskAiExchangeCard({
|
|||
return groupConsecutiveToolResults(assistantMessage?.parts || []);
|
||||
}, [assistantMessage]);
|
||||
|
||||
const promptSuggestions = useMemo(() => {
|
||||
if (!isLastExchange) return [];
|
||||
return getAgentPromptSuggestions(assistantMessage?.parts || []);
|
||||
}, [assistantMessage, isLastExchange]);
|
||||
|
||||
const wasStopped = userMessage.metadata?.stopped || assistantMessage?.metadata?.stopped;
|
||||
|
||||
const showActions =
|
||||
|
|
@ -302,6 +319,14 @@ function AskAiExchangeCard({
|
|||
onFeedback={onFeedback}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{promptSuggestions.length > 0 && (
|
||||
<ConversationPromptSuggestions
|
||||
title={suggestedPromptsTitleText}
|
||||
suggestions={promptSuggestions}
|
||||
onSelectPromptSuggestion={onSelectPromptSuggestion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -393,6 +418,7 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
|
|||
memoryEnabled={memoryEnabled}
|
||||
onSearchQueryClick={handleSearchQueryClick}
|
||||
onFeedback={props.onFeedback}
|
||||
onSelectPromptSuggestion={props.onSelectPromptSuggestion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { AutocompleteApi, AutocompleteState, BaseItem } from '@algolia/autocomplete-core';
|
||||
import type { JSX } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AskAiScreenTranslations } from './AskAiScreen';
|
||||
|
|
@ -68,9 +69,16 @@ export interface AskAiScreenStateProps<TItem extends BaseItem>
|
|||
}
|
||||
|
||||
export const AskAiScreenState = React.memo(
|
||||
({ translations = {}, ...props }: AskAiScreenStateProps<InternalDocSearchHit>) => {
|
||||
({ translations = {}, selectAskAiQuestion, ...props }: AskAiScreenStateProps<InternalDocSearchHit>): JSX.Element => {
|
||||
const handleSelectPromptSuggestion = React.useCallback(
|
||||
(prompt: string) => {
|
||||
selectAskAiQuestion(true, prompt);
|
||||
},
|
||||
[selectAskAiQuestion],
|
||||
);
|
||||
|
||||
if (props.canHandleAskAi && props.isAskAiActive && props.askAiState === 'conversation-history') {
|
||||
return <ConversationHistoryScreen {...props} />;
|
||||
return <ConversationHistoryScreen selectAskAiQuestion={selectAskAiQuestion} {...props} />;
|
||||
}
|
||||
|
||||
if (props.canHandleAskAi && props.isAskAiActive && props.askAiState === 'new-conversation') {
|
||||
|
|
@ -93,6 +101,8 @@ export const AskAiScreenState = React.memo(
|
|||
askAiError={props.askAiError}
|
||||
translations={translations?.askAiScreen}
|
||||
memoryEnabled={props.memoryEnabled}
|
||||
selectAskAiQuestion={selectAskAiQuestion}
|
||||
onSelectPromptSuggestion={handleSelectPromptSuggestion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -103,7 +113,12 @@ export const AskAiScreenState = React.memo(
|
|||
|
||||
if (!props.state.query) {
|
||||
return (
|
||||
<AskAiStartScreen {...props} hasCollections={props.hasCollections} translations={translations?.startScreen} />
|
||||
<AskAiStartScreen
|
||||
{...props}
|
||||
hasCollections={props.hasCollections}
|
||||
translations={translations?.startScreen}
|
||||
selectAskAiQuestion={selectAskAiQuestion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { JSX } from 'react';
|
|||
import React, { memo, useMemo } from 'react';
|
||||
|
||||
import { type Exchange } from '../AskAiScreen';
|
||||
import { ConversationPromptSuggestions } from '../components/ConversationPromptSuggestions';
|
||||
import { FeedbackActions } from '../components/FeedbackActions';
|
||||
import { SourcesPanel } from '../components/SourcesPanel';
|
||||
import { ToolCall, type ToolCallTranslations } from '../components/ToolCall';
|
||||
|
|
@ -11,7 +12,13 @@ import { MemoizedMarkdown } from '../MemoizedMarkdown';
|
|||
import type { StoredSearchPlugin } from '../stored-searches';
|
||||
import type { OnAskAiFeedback, StoredAskAiState } from '../types';
|
||||
import { type AIMessage, type ToolCalls } from '../types/AskiAi';
|
||||
import { extractLinksFromMessage, getMessageContent, EMPTY_TOOLS, isAIToolPart } from '../utils/ai';
|
||||
import {
|
||||
extractLinksFromMessage,
|
||||
getMessageContent,
|
||||
EMPTY_TOOLS,
|
||||
isAIToolPart,
|
||||
getAgentPromptSuggestions,
|
||||
} from '../utils/ai';
|
||||
import { groupConsecutiveToolResults } from '../utils/groupConsecutiveToolResults';
|
||||
|
||||
import { AggregatedSearchBlock } from './AggregatedSearchBlock';
|
||||
|
|
@ -91,6 +98,7 @@ export type ConversationScreenTranslations = Partial<
|
|||
* Error title shown if there is an error while chatting.
|
||||
*/
|
||||
errorTitleText;
|
||||
suggestedPromptsTitleText: string;
|
||||
}
|
||||
>;
|
||||
|
||||
|
|
@ -103,6 +111,7 @@ export type ConversationScreenProps = {
|
|||
streamError?: Error;
|
||||
memoryEnabled?: boolean;
|
||||
tools?: ToolCalls;
|
||||
onSelectPromptSuggestion: (prompt: string) => void;
|
||||
};
|
||||
|
||||
type ConversationnExchangeProps = {
|
||||
|
|
@ -115,6 +124,7 @@ type ConversationnExchangeProps = {
|
|||
streamError?: ConversationScreenProps['streamError'];
|
||||
memoryEnabled?: boolean;
|
||||
tools: ToolCalls;
|
||||
onSelectPromptSuggestion: (prompt: string) => void;
|
||||
};
|
||||
|
||||
const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExchangeProps>(
|
||||
|
|
@ -129,6 +139,7 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
|
|||
streamError,
|
||||
memoryEnabled,
|
||||
tools,
|
||||
onSelectPromptSuggestion,
|
||||
},
|
||||
conversationRef,
|
||||
): JSX.Element => {
|
||||
|
|
@ -147,6 +158,7 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
|
|||
errorTitleText = 'Chat error',
|
||||
savedMemoryToolResultText = 'Saved to memory',
|
||||
memoryToolResultText = 'Used memory to enhance results',
|
||||
suggestedPromptsTitleText = 'Suggested prompts',
|
||||
} = translations;
|
||||
|
||||
const assistantContent = useMemo(() => getMessageContent(assistantMessage), [assistantMessage]);
|
||||
|
|
@ -157,6 +169,10 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
|
|||
[assistantMessage],
|
||||
);
|
||||
const urlsToDisplay = React.useMemo(() => extractLinksFromMessage(assistantMessage), [assistantMessage]);
|
||||
const suggestedPrompts = React.useMemo(() => {
|
||||
if (!isLastExchange) return [];
|
||||
return getAgentPromptSuggestions(assistantMessage?.parts || []);
|
||||
}, [assistantMessage, isLastExchange]);
|
||||
|
||||
const wasStopped = userMessage.metadata?.stopped || assistantMessage?.metadata?.stopped;
|
||||
const isThinking =
|
||||
|
|
@ -274,6 +290,14 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
|
|||
onFeedback={onFeedback}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{suggestedPrompts.length > 0 && (
|
||||
<ConversationPromptSuggestions
|
||||
title={suggestedPromptsTitleText}
|
||||
suggestions={suggestedPrompts}
|
||||
onSelectPromptSuggestion={onSelectPromptSuggestion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -286,7 +310,11 @@ export const ConversationScreen = memo(
|
|||
translations = {},
|
||||
handleFeedback,
|
||||
tools = EMPTY_TOOLS,
|
||||
...props
|
||||
status,
|
||||
conversations,
|
||||
streamError,
|
||||
memoryEnabled,
|
||||
onSelectPromptSuggestion,
|
||||
}: ConversationScreenProps): JSX.Element => {
|
||||
const { conversationDisclaimer = 'Answers are generated with AI which can make mistakes. Verify responses.' } =
|
||||
translations;
|
||||
|
|
@ -318,8 +346,12 @@ export const ConversationScreen = memo(
|
|||
isLastExchange={isLastExchange}
|
||||
ref={isLastExchange ? mostRecentExchangeRef : null}
|
||||
tools={tools}
|
||||
status={status}
|
||||
conversations={conversations}
|
||||
streamError={streamError}
|
||||
memoryEnabled={memoryEnabled}
|
||||
onFeedback={handleFeedback}
|
||||
{...props}
|
||||
onSelectPromptSuggestion={onSelectPromptSuggestion}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -207,12 +207,15 @@ function SidepanelInner(
|
|||
|
||||
const prevStatus = React.useRef(status);
|
||||
|
||||
const handleSend = (prompt: string): void => {
|
||||
setStoppedStreaming(false);
|
||||
const handleSend = React.useCallback(
|
||||
(prompt: string): void => {
|
||||
setStoppedStreaming(false);
|
||||
|
||||
sendMessage({ text: prompt });
|
||||
setSidepanelState('conversation');
|
||||
};
|
||||
sendMessage({ text: prompt });
|
||||
setSidepanelState('conversation');
|
||||
},
|
||||
[sendMessage],
|
||||
);
|
||||
|
||||
const handleStartNewConversation = (): void => {
|
||||
startNewConversation();
|
||||
|
|
@ -401,6 +404,7 @@ function SidepanelInner(
|
|||
streamError={askAiError}
|
||||
memoryEnabled={memory?.enabled ?? false}
|
||||
tools={tools}
|
||||
onSelectPromptSuggestion={handleSend}
|
||||
/>
|
||||
)}
|
||||
{sidepanelState === 'conversation-history' && (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import type { JSX } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
interface ConversationPromptSuggestionsProps {
|
||||
title: string;
|
||||
onSelectPromptSuggestion: (prompt: string) => void;
|
||||
suggestions: string[];
|
||||
}
|
||||
export const ConversationPromptSuggestions = React.memo(function ConversationPromptSuggestions({
|
||||
title,
|
||||
suggestions,
|
||||
onSelectPromptSuggestion,
|
||||
}: ConversationPromptSuggestionsProps): JSX.Element {
|
||||
const titleId = React.useId();
|
||||
|
||||
const handleSelectPromptSuggestion = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onSelectPromptSuggestion(e.currentTarget.value);
|
||||
},
|
||||
[onSelectPromptSuggestion],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="DocSearch-PromptSuggestions" aria-labelledby={titleId}>
|
||||
<h3 className="DocSearch-PromptSuggestions-Title" id={titleId}>
|
||||
{title}
|
||||
</h3>
|
||||
<div className="DocSearch-PromptSuggestions-Content">
|
||||
{suggestions.map((suggestion) => (
|
||||
<button
|
||||
key={suggestion}
|
||||
type="button"
|
||||
className="DocSearch-PromptSuggestions-Action"
|
||||
value={suggestion}
|
||||
onClick={handleSelectPromptSuggestion}
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { UIMessage } from '@ai-sdk/react';
|
||||
import { type ToolUIPart, type UIDataTypes, type UIMessagePart } from 'ai';
|
||||
import { type ToolUIPart, type UIMessagePart } from 'ai';
|
||||
|
||||
export type AskAiState = 'conversation-history' | 'conversation' | 'initial' | 'new-conversation';
|
||||
|
||||
|
|
@ -79,9 +79,15 @@ type CustomTools = {
|
|||
};
|
||||
type Tools = AgentStudioMemoryTools & CustomTools & SearchTools;
|
||||
|
||||
export type AIMessage = UIMessage<{ stopped?: boolean }, UIDataTypes, Tools>;
|
||||
type DataParts = {
|
||||
suggestions: {
|
||||
suggestions: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type AIMessagePart = UIMessagePart<UIDataTypes, Tools>;
|
||||
export type AIMessage = UIMessage<{ stopped?: boolean }, DataParts, Tools>;
|
||||
|
||||
export type AIMessagePart = UIMessagePart<DataParts, Tools>;
|
||||
|
||||
export type SearchToolPart = ToolUIPart<SearchTools>;
|
||||
export type MemoryToolPart = ToolUIPart<AgentStudioMemoryTools>;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type { StoredSearchPlugin } from './stored-searches';
|
|||
import { createStoredConversations } from './stored-searches';
|
||||
import { type AIMessage, type ToolCalls } from './types/AskiAi';
|
||||
import type { OnAskAiFeedback } from './types/Feedback';
|
||||
import { EMPTY_TOOLS } from './utils/ai';
|
||||
import { EMPTY_TOOLS, sanitizeMessagesForRequest } from './utils/ai';
|
||||
|
||||
import type { AgentStudioIndices, AgentStudioSearchParameters, Memory, StoredAskAiState } from '.';
|
||||
|
||||
|
|
@ -85,7 +85,22 @@ const getAgentStudioTransport = ({
|
|||
'x-algolia-api-key': apiKey,
|
||||
...(userToken ? { 'x-algolia-secure-user-token': userToken } : {}),
|
||||
},
|
||||
body: { algolia: algoliaParams },
|
||||
body: {
|
||||
algolia: algoliaParams,
|
||||
},
|
||||
prepareSendMessagesRequest({ id, messages, body, ...rest }) {
|
||||
// Filter out `data-*` part types since Agent Studio does not currently support them on the request
|
||||
const sanitizedMessages = sanitizeMessagesForRequest(messages);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
body: {
|
||||
id,
|
||||
messages: sanitizedMessages,
|
||||
...body,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import type { AIMessagePart } from '../../types/AskiAi';
|
||||
import { isAIToolPart, isAlgoliaMCPSearchOutputPart } from '../ai';
|
||||
import type { AIMessage, AIMessagePart } from '../../types/AskiAi';
|
||||
import {
|
||||
getAgentPromptSuggestions,
|
||||
isAIToolPart,
|
||||
isAlgoliaMCPSearchOutputPart,
|
||||
sanitizeMessagesForRequest,
|
||||
} from '../ai';
|
||||
|
||||
function message(id: string, parts: AIMessagePart[]): AIMessage {
|
||||
return {
|
||||
id,
|
||||
role: 'assistant',
|
||||
parts,
|
||||
};
|
||||
}
|
||||
|
||||
describe('isAIToolPart', () => {
|
||||
it.each([
|
||||
|
|
@ -79,3 +92,80 @@ describe('isAlgoliaMCPSearchOutputPart', () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('sanitizeMessagesForRequest', () => {
|
||||
it('returns the original messages array when there are no data parts', () => {
|
||||
const messages = [message('message-1', [{ type: 'text', text: 'Hello' }])];
|
||||
|
||||
expect(sanitizeMessagesForRequest(messages)).toBe(messages);
|
||||
});
|
||||
|
||||
it('removes data parts from messages', () => {
|
||||
const textPart: AIMessagePart = { type: 'text', text: 'Hello' };
|
||||
const reasoningPart: AIMessagePart = { type: 'reasoning', state: 'done', text: 'Thinking...' };
|
||||
const messages = [
|
||||
message('message-1', [
|
||||
textPart,
|
||||
{
|
||||
type: 'data-suggestions',
|
||||
data: { suggestions: ['How do I configure DocSearch?'] },
|
||||
},
|
||||
reasoningPart,
|
||||
]),
|
||||
];
|
||||
|
||||
expect(sanitizeMessagesForRequest(messages)).toEqual([message('message-1', [textPart, reasoningPart])]);
|
||||
});
|
||||
|
||||
it('keeps unchanged messages by reference when a later message is sanitized', () => {
|
||||
const unchangedMessage = message('message-1', [{ type: 'text', text: 'Hello' }]);
|
||||
const sanitizedMessage = message('message-2', [
|
||||
{ type: 'text', text: 'Hi' },
|
||||
{
|
||||
type: 'data-suggestions',
|
||||
data: { suggestions: ['What is DocSearch?'] },
|
||||
},
|
||||
]);
|
||||
|
||||
const result = sanitizeMessagesForRequest([unchangedMessage, sanitizedMessage]);
|
||||
|
||||
expect(result[0]).toBe(unchangedMessage);
|
||||
expect(result[1]).not.toBe(sanitizedMessage);
|
||||
expect(result[1].parts).toEqual([{ type: 'text', text: 'Hi' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentPromptSuggestions', () => {
|
||||
it('returns an empty array when there is no suggestions part', () => {
|
||||
expect(getAgentPromptSuggestions([{ type: 'text', text: 'Hello' }])).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns suggestions from the data suggestions part', () => {
|
||||
expect(
|
||||
getAgentPromptSuggestions([
|
||||
{ type: 'text', text: 'Hello' },
|
||||
{
|
||||
type: 'data-suggestions',
|
||||
data: {
|
||||
suggestions: ['How do I install DocSearch?', 'How do I configure facets?'],
|
||||
},
|
||||
},
|
||||
]),
|
||||
).toEqual(['How do I install DocSearch?', 'How do I configure facets?']);
|
||||
});
|
||||
|
||||
it('returns suggestions from the first suggestions part', () => {
|
||||
expect(
|
||||
getAgentPromptSuggestions([
|
||||
{
|
||||
type: 'data-suggestions',
|
||||
data: { suggestions: ['First suggestion'] },
|
||||
},
|
||||
{
|
||||
type: 'data-suggestions',
|
||||
data: { suggestions: ['Second suggestion'] },
|
||||
},
|
||||
]),
|
||||
).toEqual(['First suggestion']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -148,3 +148,36 @@ export function isAlgoliaMCPSearchOutputPart(part: AIMessagePart): part is Algol
|
|||
export function isSearchOutputPart(part: AIMessagePart): part is SearchOutputPart {
|
||||
return isAIToolPart(part) && isSearchToolPart(part) && part.state === 'output-available';
|
||||
}
|
||||
|
||||
export function sanitizeMessagesForRequest(messages: AIMessage[]): AIMessage[] {
|
||||
let sanitizedMessages: AIMessage[] | undefined;
|
||||
|
||||
messages.forEach((message, index) => {
|
||||
// Filter out `data-*` part types since Agent Studio does not currently support them on the request
|
||||
const parts = message.parts.filter((part) => !part.type.startsWith('data-'));
|
||||
|
||||
if (parts.length === message.parts.length) {
|
||||
sanitizedMessages?.push(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sanitizedMessages) {
|
||||
sanitizedMessages = messages.slice(0, index);
|
||||
}
|
||||
|
||||
sanitizedMessages.push({
|
||||
...message,
|
||||
parts,
|
||||
});
|
||||
});
|
||||
|
||||
return sanitizedMessages ?? messages;
|
||||
}
|
||||
|
||||
export function getAgentPromptSuggestions(parts: AIMessagePart[]): string[] {
|
||||
const suggestionsPart = parts.find((part) => part.type === 'data-suggestions');
|
||||
|
||||
if (!suggestionsPart) return [];
|
||||
|
||||
return suggestionsPart.data.suggestions;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue