feat(askai): add compatibility with algolia mcp search tool [DASH-2294] (#2862)
## Summary
Fixes DASH-2294
Add compatibility with the Algolia MCP search tool (`algolia_search_index_${string}`) in AskAI.
## Changes
- Add `AlgoliaMCPSearchTool` type to handle the Algolia MCP server search tool
- Refactor how number of hits are retrieved in `ToolCall` into a `getNumberOfHits` helper
## Test plan
- Added unit tests for modified code ✅
This commit is contained in:
parent
f86785218c
commit
ad7e8a7f93
5 changed files with 132 additions and 4 deletions
|
|
@ -8,7 +8,7 @@ import { MemoizedMarkdown } from '../MemoizedMarkdown';
|
|||
import type { StoredSearchPlugin } from '../stored-searches';
|
||||
import { ToolCall, type ToolCallTranslations } from '../ToolCall';
|
||||
import type { StoredAskAiState } from '../types';
|
||||
import type { AIMessage } from '../types/AskiAi';
|
||||
import { isAIToolPart, type AIMessage } from '../types/AskiAi';
|
||||
import { extractLinksFromMessage, getMessageContent } from '../utils/ai';
|
||||
import { groupConsecutiveToolResults } from '../utils/groupConsecutiveToolResults';
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ const ConversationExchange = React.forwardRef<HTMLDivElement, ConversationnExcha
|
|||
return <AggregatedSearchBlock key={index} queries={part.queries} />;
|
||||
}
|
||||
|
||||
if (part.type === 'tool-searchIndex' || part.type === 'tool-algolia_search_index') {
|
||||
if (isAIToolPart(part)) {
|
||||
return (
|
||||
<ToolCall
|
||||
key={index}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function ToolCall({ part, translations, onSearchQueryClick }: ToolCallPro
|
|||
);
|
||||
case 'output-available': {
|
||||
const query = part.type === 'tool-searchIndex' ? part.output.query : part.input.query;
|
||||
const numberOfHits = part.type === 'tool-searchIndex' ? part.output.hits?.length : part.output.nbHits;
|
||||
const numberOfHits = part.output.hits?.length ?? 0;
|
||||
|
||||
return (
|
||||
<div className="DocSearch-AskAiScreen-MessageContent-Tool Tool--Result">
|
||||
|
|
@ -73,7 +73,7 @@ export function ToolCall({ part, translations, onSearchQueryClick }: ToolCallPro
|
|||
) : (
|
||||
<span className="DocSearch-AskAiScreen-MessageContent-Tool-Query"> "{query || ''}"</span>
|
||||
)}{' '}
|
||||
found {numberOfHits || 0} results
|
||||
found {numberOfHits} results
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
71
packages/docsearch-react/src/__tests__/ToolCall.test.tsx
Normal file
71
packages/docsearch-react/src/__tests__/ToolCall.test.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import React from 'react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { ToolCall, type ToolCallTranslations } from '../ToolCall';
|
||||
import type { AIToolPart } from '../types/AskiAi';
|
||||
|
||||
const TRANSLATIONS: ToolCallTranslations = {
|
||||
preToolCallText: 'Searching for',
|
||||
searchingText: 'Searching...',
|
||||
toolCallResultText: 'Searched',
|
||||
};
|
||||
|
||||
describe('ToolCall', () => {
|
||||
describe('number of hits rendering', () => {
|
||||
it.each([
|
||||
{
|
||||
description: 'tool-searchIndex with hits array',
|
||||
part: {
|
||||
type: 'tool-searchIndex',
|
||||
toolCallId: 'id-1',
|
||||
state: 'output-available',
|
||||
input: { query: 'test' },
|
||||
output: { query: 'test', hits: [{}, {}, {}] },
|
||||
},
|
||||
expectedHits: 3,
|
||||
},
|
||||
{
|
||||
description: 'tool-searchIndex with empty hits',
|
||||
part: {
|
||||
type: 'tool-searchIndex',
|
||||
toolCallId: 'id-2',
|
||||
state: 'output-available',
|
||||
input: { query: 'test' },
|
||||
output: { query: 'test', hits: [] },
|
||||
},
|
||||
expectedHits: 0,
|
||||
},
|
||||
{
|
||||
description: 'tool-algolia_search_index with nbHits',
|
||||
part: {
|
||||
type: 'tool-algolia_search_index',
|
||||
toolCallId: 'id-3',
|
||||
state: 'output-available',
|
||||
input: { index: 'docs', query: 'test', number_of_results: 10, facet_filters: null },
|
||||
output: { hits: [{}, {}] },
|
||||
},
|
||||
expectedHits: 2,
|
||||
},
|
||||
{
|
||||
description: 'tool-algolia_search_index_custom with results array',
|
||||
part: {
|
||||
type: 'tool-algolia_search_index_custom',
|
||||
toolCallId: 'id-4',
|
||||
state: 'output-available',
|
||||
input: { query: 'test' },
|
||||
output: { hits: [{}] },
|
||||
},
|
||||
expectedHits: 1,
|
||||
},
|
||||
] satisfies Array<{ description: string; part: AIToolPart; expectedHits: number }>)(
|
||||
'displays $expectedHits results for $description',
|
||||
({ part, expectedHits }) => {
|
||||
render(<ToolCall part={part} translations={TRANSLATIONS} />);
|
||||
|
||||
expect(screen.getByText(`found ${expectedHits} results`, { exact: false })).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -27,7 +27,19 @@ export interface AgentStudioSearchTool {
|
|||
};
|
||||
}
|
||||
|
||||
export interface AlgoliaMCPSearchTool {
|
||||
input: {
|
||||
query: string;
|
||||
};
|
||||
output: {
|
||||
hits?: any[];
|
||||
nbHits?: number;
|
||||
};
|
||||
}
|
||||
|
||||
type Tools = {
|
||||
[K in `algolia_search_index_${string}`]: AlgoliaMCPSearchTool;
|
||||
} & {
|
||||
searchIndex: SearchIndexTool;
|
||||
algolia_search_index: AgentStudioSearchTool;
|
||||
};
|
||||
|
|
@ -37,3 +49,7 @@ export type AIMessage = UIMessage<{ stopped?: boolean }, UIDataTypes, Tools>;
|
|||
export type AIMessagePart = UIMessagePart<UIDataTypes, Tools>;
|
||||
|
||||
export type AIToolPart = ToolUIPart<Tools>;
|
||||
|
||||
export function isAIToolPart(part: AIMessagePart): part is AIToolPart {
|
||||
return part.type.startsWith('tool-');
|
||||
}
|
||||
|
|
|
|||
41
packages/docsearch-react/src/types/__tests__/AskiAi.test.ts
Normal file
41
packages/docsearch-react/src/types/__tests__/AskiAi.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import type { AIMessagePart } from '../AskiAi';
|
||||
import { isAIToolPart } from '../AskiAi';
|
||||
|
||||
describe('isAIToolPart', () => {
|
||||
it.each([
|
||||
{
|
||||
part: {
|
||||
type: 'tool-searchIndex',
|
||||
toolCallId: 'id-1',
|
||||
state: 'output-available',
|
||||
input: { query: 'test' },
|
||||
output: { hits: [] },
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
part: {
|
||||
type: 'tool-algolia_search_index',
|
||||
toolCallId: 'id-2',
|
||||
state: 'input-streaming',
|
||||
input: {},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
part: { type: 'text', text: 'Hello' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
part: { type: 'reasoning', text: 'Thinking...' },
|
||||
expected: false,
|
||||
},
|
||||
] satisfies Array<{ part: AIMessagePart; expected: boolean }>)(
|
||||
'returns $expected for $part.type',
|
||||
({ part, expected }) => {
|
||||
expect(isAIToolPart(part)).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
Loading…
Reference in a new issue