1
0
Fork 0

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:
Vincent Lemeunier 2026-02-19 09:42:56 +01:00 committed by GitHub
parent f86785218c
commit ad7e8a7f93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 132 additions and 4 deletions

View file

@ -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}

View file

@ -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"> &quot;{query || ''}&quot;</span>
)}{' '}
found {numberOfHits || 0} results
found {numberOfHits} results
</span>
</div>
);

View 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();
},
);
});
});

View file

@ -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-');
}

View 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);
},
);
});