Skip to content

Commit dc1c477

Browse files
authored
chore(compass-assistant): add explain plan entry point COMPASS-9606 (#7208)
1 parent bfd0ab4 commit dc1c477

File tree

11 files changed

+269
-46
lines changed

11 files changed

+269
-46
lines changed

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-assistant/src/assistant-chat.spec.tsx

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import React from 'react';
22
import { render, screen, userEvent } from '@mongodb-js/testing-library-compass';
33
import { AssistantChat } from './assistant-chat';
44
import { expect } from 'chai';
5-
import type { UIMessage } from './@ai-sdk/react/use-chat';
65
import { createMockChat } from '../test/utils';
6+
import type { AssistantMessage } from './compass-assistant-provider';
77

88
describe('AssistantChat', function () {
9-
const mockMessages: UIMessage[] = [
9+
const mockMessages: AssistantMessage[] = [
1010
{
1111
id: 'user',
1212
role: 'user',
@@ -24,7 +24,7 @@ describe('AssistantChat', function () {
2424
},
2525
];
2626

27-
function renderWithChat(messages: UIMessage[]) {
27+
function renderWithChat(messages: AssistantMessage[]) {
2828
const chat = createMockChat({ messages });
2929
return {
3030
result: render(<AssistantChat chat={chat} />),
@@ -181,7 +181,7 @@ describe('AssistantChat', function () {
181181
});
182182

183183
it('handles messages with multiple text parts', function () {
184-
const messagesWithMultipleParts: UIMessage[] = [
184+
const messagesWithMultipleParts: AssistantMessage[] = [
185185
{
186186
id: '1',
187187
role: 'assistant',
@@ -198,7 +198,7 @@ describe('AssistantChat', function () {
198198
});
199199

200200
it('handles messages with mixed part types (filters to text only)', function () {
201-
const messagesWithMixedParts: UIMessage[] = [
201+
const messagesWithMixedParts: AssistantMessage[] = [
202202
{
203203
id: '1',
204204
role: 'assistant',
@@ -217,4 +217,33 @@ describe('AssistantChat', function () {
217217
.exist;
218218
expect(screen.queryByText('This should be filtered out.')).to.not.exist;
219219
});
220+
221+
it('displays displayText instead of message parts when displayText is set', function () {
222+
const messagesWithDisplayText: AssistantMessage[] = [
223+
{
224+
id: '1',
225+
role: 'assistant',
226+
parts: [
227+
{ type: 'text', text: 'This message part should be ignored.' },
228+
{ type: 'text', text: 'Another part that should not display.' },
229+
],
230+
metadata: {
231+
displayText: 'This is the custom display text that should show.',
232+
},
233+
},
234+
];
235+
236+
renderWithChat(messagesWithDisplayText);
237+
238+
// Should display the displayText
239+
expect(
240+
screen.getByText('This is the custom display text that should show.')
241+
).to.exist;
242+
243+
// Should NOT display the message parts
244+
expect(screen.queryByText('This message part should be ignored.')).to.not
245+
.exist;
246+
expect(screen.queryByText('Another part that should not display.')).to.not
247+
.exist;
248+
});
220249
});

packages/compass-assistant/src/assistant-chat.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React, { useCallback, useState } from 'react';
2-
import type { UIMessage } from './@ai-sdk/react/use-chat';
2+
import type { AssistantMessage } from './compass-assistant-provider';
33
import type { Chat } from './@ai-sdk/react/chat-react';
44
import { useChat } from './@ai-sdk/react/use-chat';
55

66
interface AssistantChatProps {
7-
chat: Chat<UIMessage>;
7+
chat: Chat<AssistantMessage>;
88
}
99

1010
/**
@@ -69,10 +69,12 @@ export const AssistantChat: React.FunctionComponent<AssistantChatProps> = ({
6969
whiteSpace: 'pre-wrap',
7070
}}
7171
>
72-
{message.parts
73-
?.filter((part) => part.type === 'text')
74-
.map((part) => part.text)
75-
.join('') || ''}
72+
{message.metadata?.displayText ||
73+
message.parts
74+
?.filter((part) => part.type === 'text')
75+
.map((part) => part.text)
76+
.join('') ||
77+
''}
7678
</div>
7779
))}
7880
</div>

packages/compass-assistant/src/compass-assistant-provider.spec.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import {
88
import {
99
AssistantProvider,
1010
CompassAssistantProvider,
11+
type AssistantMessage,
1112
} from './compass-assistant-provider';
1213
import { expect } from 'chai';
1314
import sinon from 'sinon';
14-
import type { UIMessage } from './@ai-sdk/react/use-chat';
1515
import { Chat } from './@ai-sdk/react/chat-react';
1616

1717
import {
@@ -24,7 +24,7 @@ import { createMockChat } from '../test/utils';
2424

2525
// Test component that renders AssistantProvider with children
2626
const TestComponent: React.FunctionComponent<{
27-
chat: Chat<UIMessage>;
27+
chat: Chat<AssistantMessage>;
2828
autoOpen?: boolean;
2929
}> = ({ chat, autoOpen }) => {
3030
return (
@@ -78,7 +78,7 @@ describe('AssistantProvider', function () {
7878
});
7979

8080
async function renderOpenAssistantDrawer(
81-
mockChat: Chat<UIMessage>
81+
mockChat: Chat<AssistantMessage>
8282
): Promise<ReturnType<typeof render>> {
8383
const result = render(<TestComponent chat={mockChat} autoOpen={true} />, {
8484
preferences: { enableAIAssistant: true },
@@ -92,7 +92,7 @@ describe('AssistantProvider', function () {
9292
}
9393

9494
it('displays messages in the chat feed', async function () {
95-
const mockMessages: UIMessage[] = [
95+
const mockMessages: AssistantMessage[] = [
9696
{
9797
id: '1',
9898
role: 'user',
@@ -120,7 +120,7 @@ describe('AssistantProvider', function () {
120120
});
121121

122122
it('handles message sending with custom chat when drawer is open', async function () {
123-
const mockChat = new Chat<UIMessage>({
123+
const mockChat = new Chat<AssistantMessage>({
124124
messages: [
125125
{
126126
id: 'assistant',
@@ -149,7 +149,7 @@ describe('AssistantProvider', function () {
149149
});
150150

151151
it('new messages are added to the chat feed when the send button is clicked', async function () {
152-
const mockChat = new Chat<UIMessage>({
152+
const mockChat = new Chat<AssistantMessage>({
153153
messages: [
154154
{
155155
id: 'assistant',

packages/compass-assistant/src/compass-assistant-provider.tsx

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,73 @@ import { createContext, useContext } from 'react';
55
import { registerCompassPlugin } from '@mongodb-js/compass-app-registry';
66
import { atlasServiceLocator } from '@mongodb-js/atlas-service/provider';
77
import { DocsProviderTransport } from './docs-provider-transport';
8+
import { useDrawerActions } from '@mongodb-js/compass-components';
9+
import { buildExplainPlanPrompt } from './prompts';
10+
import { usePreference } from 'compass-preferences-model/provider';
811

912
export const ASSISTANT_DRAWER_ID = 'compass-assistant-drawer';
1013

11-
type AssistantContextType = Chat<UIMessage>;
14+
export type AssistantMessage = UIMessage & {
15+
metadata?: {
16+
/** The text to display instead of the message text. */
17+
displayText?: string;
18+
};
19+
};
20+
21+
type AssistantContextType = Chat<AssistantMessage>;
1222

1323
export const AssistantContext = createContext<AssistantContextType | null>(
1424
null
1525
);
1626

17-
type AssistantActionsContextType = unknown;
27+
type AssistantActionsContextType = {
28+
interpretExplainPlan: ({
29+
namespace,
30+
explainPlan,
31+
}: {
32+
namespace: string;
33+
explainPlan: string;
34+
}) => void;
35+
};
1836
export const AssistantActionsContext =
19-
createContext<AssistantActionsContextType>({});
37+
createContext<AssistantActionsContextType>({
38+
interpretExplainPlan: () => {},
39+
});
2040

21-
export function useAssistantActions(): AssistantActionsContextType {
22-
return useContext(AssistantActionsContext);
41+
export function useAssistantActions(): AssistantActionsContextType & {
42+
isAssistantEnabled: boolean;
43+
} {
44+
const isAssistantEnabled = usePreference('enableAIAssistant');
45+
46+
return {
47+
...useContext(AssistantActionsContext),
48+
isAssistantEnabled,
49+
};
2350
}
2451

2552
export const AssistantProvider: React.FunctionComponent<
2653
PropsWithChildren<{
27-
chat: Chat<UIMessage>;
54+
chat: Chat<AssistantMessage>;
2855
}>
2956
> = ({ chat, children }) => {
30-
const assistantActionsContext = useRef<AssistantActionsContextType>();
57+
const assistantActionsContext = useRef<AssistantActionsContextType>({
58+
interpretExplainPlan: ({ explainPlan }) => {
59+
openDrawer(ASSISTANT_DRAWER_ID);
60+
const { prompt, displayText } = buildExplainPlanPrompt({
61+
explainPlan,
62+
});
63+
void chat.sendMessage(
64+
{
65+
text: prompt,
66+
metadata: {
67+
displayText,
68+
},
69+
},
70+
{}
71+
);
72+
},
73+
});
74+
const { openDrawer } = useDrawerActions();
3175

3276
return (
3377
<AssistantContext.Provider value={chat}>
@@ -45,7 +89,7 @@ export const CompassAssistantProvider = registerCompassPlugin(
4589
chat,
4690
children,
4791
}: PropsWithChildren<{
48-
chat?: Chat<UIMessage>;
92+
chat?: Chat<AssistantMessage>;
4993
}>) => {
5094
if (!chat) {
5195
throw new Error('Chat was not provided by the state');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { CompassAssistantProvider } from './compass-assistant-provider';
22
export { CompassAssistantDrawer } from './compass-assistant-drawer';
3+
export { useAssistantActions } from './compass-assistant-provider';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const buildExplainPlanPrompt = ({
2+
explainPlan,
3+
}: {
4+
explainPlan: string;
5+
}) => {
6+
return {
7+
prompt: `Given the MongoDB explain plan output below, provide a concise human readable explanation that explains the query execution plan and highlights aspects of the plan that might impact query performance. Respond with as much concision and clarity as possible.
8+
If a clear optimization should be made, please suggest the optimization and describe how it can be accomplished in MongoDB Compass. Do not advise users to create indexes without weighing the pros and cons.
9+
Explain output:
10+
${explainPlan}`,
11+
displayText: 'Provide an explanation of this explain plan.',
12+
};
13+
};
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import type { UIMessage } from 'ai';
21
import { Chat } from '../src/@ai-sdk/react/chat-react';
32
import sinon from 'sinon';
3+
import type { AssistantMessage } from '../src/compass-assistant-provider';
44

5-
export const createMockChat = ({ messages }: { messages: UIMessage[] }) => {
6-
const newChat = new Chat<UIMessage>({
5+
export const createMockChat = ({
6+
messages,
7+
}: {
8+
messages: AssistantMessage[];
9+
}) => {
10+
const newChat = new Chat<AssistantMessage>({
711
messages,
812
});
913
sinon.replace(newChat, 'sendMessage', sinon.stub());
10-
return newChat as unknown as Chat<UIMessage> & {
14+
return newChat as unknown as Chat<AssistantMessage> & {
1115
sendMessage: sinon.SinonStub;
1216
};
1317
};

packages/compass-explain-plan/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"xvfb-maybe": "^0.2.1"
6969
},
7070
"dependencies": {
71+
"@mongodb-js/compass-assistant": "^1.0.0",
7172
"@mongodb-js/compass-app-registry": "^9.4.19",
7273
"@mongodb-js/compass-collection": "^4.69.0",
7374
"@mongodb-js/compass-components": "^1.48.0",

packages/compass-explain-plan/src/components/explain-plan-modal.spec.tsx

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ import { ExplainPlanModal } from './explain-plan-modal';
1010
import { Provider } from 'react-redux';
1111
import { activatePlugin } from '../stores';
1212

13-
function render(props: Partial<ExplainPlanModalProps>) {
13+
function render(
14+
props: Partial<ExplainPlanModalProps>,
15+
{ preferences }: { preferences: { enableAIAssistant: boolean } } = {
16+
preferences: { enableAIAssistant: false },
17+
}
18+
) {
1419
const { store } = activatePlugin(
1520
{ namespace: 'test.test', isDataLake: false },
16-
{ dataService: {}, localAppRegistry: {}, preferences: {} } as any,
21+
{
22+
dataService: {},
23+
localAppRegistry: {},
24+
} as any,
1725
{ on() {}, cleanup() {} } as any
1826
);
1927

@@ -26,7 +34,8 @@ function render(props: Partial<ExplainPlanModalProps>) {
2634
onModalClose={() => {}}
2735
{...props}
2836
></ExplainPlanModal>
29-
</Provider>
37+
</Provider>,
38+
{ preferences: { enableAIAssistant: preferences?.enableAIAssistant } }
3039
);
3140
}
3241

@@ -51,4 +60,52 @@ describe('ExplainPlanModal', function () {
5160
render({ status: 'ready' });
5261
expect(screen.getByText('Query Performance Summary')).to.exist;
5362
});
63+
64+
it('should show "Interpret for me" button when AI assistant is enabled', function () {
65+
render(
66+
{
67+
status: 'ready',
68+
explainPlan: {
69+
namespace: 'test',
70+
usedIndexes: [],
71+
} as any,
72+
},
73+
{ preferences: { enableAIAssistant: true } }
74+
);
75+
expect(screen.getByTestId('interpret-for-me-button')).to.exist;
76+
expect(screen.getByTestId('interpret-for-me-button')).to.have.attr(
77+
'aria-disabled',
78+
'false'
79+
);
80+
});
81+
82+
it('should not show "Interpret for me" button when AI assistant is disabled', function () {
83+
render(
84+
{
85+
status: 'ready',
86+
explainPlan: {
87+
namespace: 'test',
88+
usedIndexes: [],
89+
} as any,
90+
},
91+
{ preferences: { enableAIAssistant: false } }
92+
);
93+
expect(screen.queryByTestId('interpret-for-me-button')).to.not.exist;
94+
});
95+
96+
it('should disable the "Interpret for me" button when the status is not ready', function () {
97+
render(
98+
{
99+
status: 'loading',
100+
explainPlan: {
101+
usedIndexes: [],
102+
} as any,
103+
},
104+
{ preferences: { enableAIAssistant: true } }
105+
);
106+
expect(screen.getByTestId('interpret-for-me-button')).to.have.attr(
107+
'aria-disabled',
108+
'true'
109+
);
110+
});
54111
});

0 commit comments

Comments
 (0)