Skip to content

Commit 88645f7

Browse files
authored
chore(query-bar): open query options when applied from ai, better error reporting (#4626)
1 parent 97c4c3d commit 88645f7

File tree

8 files changed

+198
-39
lines changed

8 files changed

+198
-39
lines changed

packages/compass-query-bar/src/modules/ai-query-request.spec.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ describe('#runFetchAIQuery', function () {
136136
...mockUserPrompt,
137137
sampleDocuments: [
138138
{
139-
test: '4'.repeat(6000),
139+
test: '4'.repeat(60000),
140140
},
141141
],
142142
signal: new AbortController().signal,
@@ -161,7 +161,7 @@ describe('#runFetchAIQuery', function () {
161161
a: ['3'],
162162
},
163163
{
164-
a: ['4'.repeat(5000)],
164+
a: ['4'.repeat(50000)],
165165
},
166166
],
167167
signal: new AbortController().signal,
@@ -190,15 +190,17 @@ describe('#runFetchAIQuery', function () {
190190
});
191191
});
192192

193-
describe('when the endpoint is set and the server errors', function () {
193+
describe('when the server errors', function () {
194194
let stopServer: () => Promise<void>;
195195

196196
beforeEach(async function () {
197197
// Start a mock server to pass an ai response.
198198
// Set the server endpoint in the env.
199199
const { endpoint, stop } = await startMockAIServer({
200-
response: {},
201-
sendError: true,
200+
response: {
201+
status: 500,
202+
body: 'error',
203+
},
202204
});
203205

204206
stopServer = stop;
@@ -225,4 +227,44 @@ describe('#runFetchAIQuery', function () {
225227
);
226228
});
227229
});
230+
231+
describe('when the server errors with an AIError', function () {
232+
let stopServer: () => Promise<void>;
233+
234+
beforeEach(async function () {
235+
// Start a mock server to pass an ai response.
236+
// Set the server endpoint in the env.
237+
const { endpoint, stop } = await startMockAIServer({
238+
response: {
239+
status: 500,
240+
body: {
241+
name: 'AIError',
242+
errorMessage: 'tortillas',
243+
codeName: 'ExampleCode',
244+
},
245+
},
246+
});
247+
248+
stopServer = stop;
249+
process.env.DEV_AI_QUERY_ENDPOINT = endpoint;
250+
process.env.DEV_AI_USERNAME = TEST_AUTH_USERNAME;
251+
process.env.DEV_AI_PASSWORD = TEST_AUTH_PASSWORD;
252+
});
253+
254+
afterEach(async function () {
255+
await stopServer();
256+
delete process.env.DEV_AI_QUERY_ENDPOINT;
257+
delete process.env.DEV_AI_USERNAME;
258+
delete process.env.DEV_AI_PASSWORD;
259+
});
260+
261+
it('throws the error', async function () {
262+
const promise = runFetchAIQuery({
263+
...mockUserPrompt,
264+
signal: new AbortController().signal,
265+
});
266+
267+
await expect(promise).to.be.rejectedWith('Error: ExampleCode: tortillas');
268+
});
269+
});
228270
});

packages/compass-query-bar/src/modules/ai-query-request.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type { AbortSignal as NodeFetchAbortSignal } from 'node-fetch/externals';
55
import type { SimplifiedSchema } from 'mongodb-schema';
66
import type { Document } from 'mongodb';
77

8+
const serverErrorMessageName = 'AIError';
9+
810
function getAIQueryEndpoint(): string {
911
if (!process.env.DEV_AI_QUERY_ENDPOINT) {
1012
throw new Error(
@@ -81,7 +83,20 @@ export async function runFetchAIQuery({
8183
});
8284

8385
if (!res.ok) {
84-
throw new Error(`Error: ${res.status} ${res.statusText}`);
86+
// We try to parse the response to see if the server returned any
87+
// information we can show a user.
88+
let serverErrorMessage = `${res.status} ${res.statusText}`;
89+
try {
90+
const messageJSON = await res.json();
91+
if (messageJSON.name === serverErrorMessageName) {
92+
serverErrorMessage = `${messageJSON.codeName as string}: ${
93+
messageJSON.errorMessage as string
94+
}`;
95+
}
96+
} catch (err) {
97+
// no-op, use the default status and statusText in the message.
98+
}
99+
throw new Error(`Error: ${serverErrorMessage}`);
85100
}
86101

87102
const jsonResponse = await res.json();

packages/compass-query-bar/src/stores/ai-query-reducer.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,10 @@ describe('aiQueryReducer', function () {
169169

170170
beforeEach(async function () {
171171
const { endpoint, stop } = await startMockAIServer({
172-
response: {},
173-
sendError: true,
172+
response: {
173+
status: 500,
174+
body: 'test',
175+
},
174176
});
175177

176178
stopServer = stop;

packages/compass-query-bar/src/stores/ai-query-reducer.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import preferences from 'compass-preferences-model';
77
import type { QueryBarThunkAction } from './query-bar-store';
88
import { isAction } from '../utils';
99
import { runFetchAIQuery } from '../modules/ai-query-request';
10+
import { mapQueryToFormFields } from '../utils/query';
11+
import type { QueryFormFields } from '../constants/query-properties';
12+
import { DEFAULT_FIELD_VALUES } from '../constants/query-bar-store';
1013

1114
const { log, mongoLogId } = createLoggerAndTelemetry('AI-QUERY-UI');
1215

@@ -79,7 +82,7 @@ type AIQueryFailedAction = {
7982

8083
export type AIQuerySucceededAction = {
8184
type: AIQueryActionTypes.AIQuerySucceeded;
82-
query: unknown;
85+
fields: QueryFormFields;
8386
};
8487

8588
function logFailed(errorMessage: string) {
@@ -166,15 +169,19 @@ export const runAIQuery = (
166169
return;
167170
}
168171

169-
let query;
172+
let fields;
170173
try {
171174
if (!jsonResponse?.content?.query) {
172175
throw new Error(
173176
'No query returned. Please try again with a different prompt.'
174177
);
175178
}
176179

177-
query = jsonResponse?.content?.query;
180+
const query = jsonResponse?.content?.query;
181+
fields = mapQueryToFormFields({
182+
...DEFAULT_FIELD_VALUES,
183+
...(query ?? {}),
184+
});
178185
} catch (err: any) {
179186
logFailed(err?.message);
180187
dispatch({
@@ -184,9 +191,8 @@ export const runAIQuery = (
184191
return;
185192
}
186193

187-
// Error if the response is empty. TODO: We'll want to also parse if no
188-
// applicable query fields are detected.
189-
if (!query || Object.keys(query).length === 0) {
194+
// Error when the response is empty or there is nothing to map.
195+
if (!fields || Object.keys(fields).length === 0) {
190196
const msg =
191197
'No query was returned from the ai. Consider re-wording your prompt.';
192198
logFailed(msg);
@@ -202,13 +208,15 @@ export const runAIQuery = (
202208
'AIQuery',
203209
'AI query request succeeded',
204210
{
205-
query,
211+
query: {
212+
...fields,
213+
},
206214
}
207215
);
208216

209217
dispatch({
210218
type: AIQueryActionTypes.AIQuerySucceeded,
211-
query,
219+
fields,
212220
});
213221
};
214222
};

packages/compass-query-bar/src/stores/query-bar-reducer.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
isQueryFieldsValid,
2424
validateField,
2525
isEqualDefaultQuery,
26+
doesQueryHaveExtraOptionsSet,
2627
} from '../utils/query';
2728
import type { ChangeFilterEvent } from '../modules/change-filter';
2829
import { changeFilter } from '../modules/change-filter';
@@ -473,14 +474,7 @@ export const queryBarReducer: Reducer<QueryBarState> = (
473474
}
474475

475476
if (
476-
isAction<ApplyFromHistoryAction>(
477-
action,
478-
QueryBarActions.ApplyFromHistory
479-
) ||
480-
isAction<AIQuerySucceededAction>(
481-
action,
482-
AIQueryActionTypes.AIQuerySucceeded
483-
)
477+
isAction<ApplyFromHistoryAction>(action, QueryBarActions.ApplyFromHistory)
484478
) {
485479
return {
486480
...state,
@@ -491,6 +485,19 @@ export const queryBarReducer: Reducer<QueryBarState> = (
491485
};
492486
}
493487

488+
if (
489+
isAction<AIQuerySucceededAction>(
490+
action,
491+
AIQueryActionTypes.AIQuerySucceeded
492+
)
493+
) {
494+
return {
495+
...state,
496+
expanded: state.expanded || doesQueryHaveExtraOptionsSet(action.fields),
497+
fields: action.fields,
498+
};
499+
}
500+
494501
if (
495502
isAction<RecentQueriesFetchedAction>(
496503
action,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { expect } from 'chai';
2+
3+
import { doesQueryHaveExtraOptionsSet, mapQueryToFormFields } from './query';
4+
import { DEFAULT_FIELD_VALUES } from '../constants/query-bar-store';
5+
6+
describe('#doesQueryHaveExtraOptionsSet', function () {
7+
it('returns true when there is a non filter option, false otherwise', function () {
8+
const defaultFields = mapQueryToFormFields(DEFAULT_FIELD_VALUES);
9+
10+
expect(
11+
doesQueryHaveExtraOptionsSet({
12+
...defaultFields,
13+
})
14+
).to.be.false;
15+
expect(
16+
doesQueryHaveExtraOptionsSet({
17+
...defaultFields,
18+
filter: {
19+
valid: true,
20+
string: '{test: 2}',
21+
value: {
22+
test: 2,
23+
},
24+
},
25+
})
26+
).to.be.false;
27+
expect(
28+
doesQueryHaveExtraOptionsSet({
29+
...defaultFields,
30+
sort: {
31+
valid: true,
32+
string: '[["a", -1]]',
33+
value: [['a', -1]],
34+
},
35+
})
36+
).to.be.true;
37+
expect(
38+
doesQueryHaveExtraOptionsSet({
39+
...defaultFields,
40+
skip: {
41+
valid: true,
42+
string: '25',
43+
value: 25,
44+
},
45+
})
46+
).to.be.true;
47+
expect(
48+
doesQueryHaveExtraOptionsSet({
49+
...defaultFields,
50+
project: {
51+
valid: true,
52+
string: '{test: 1}',
53+
value: {
54+
test: 1,
55+
},
56+
},
57+
})
58+
).to.be.true;
59+
});
60+
});

packages/compass-query-bar/src/utils/query.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,34 @@ export function mapFormFieldsToQuery(fields: QueryFormFields): BaseQuery {
3030
};
3131
}
3232

33+
// Returns true if any fields that aren't filter have non-default values.
34+
export function doesQueryHaveExtraOptionsSet(fields?: QueryFormFields) {
35+
if (!fields) {
36+
return false;
37+
}
38+
39+
for (const property of QUERY_PROPERTIES) {
40+
if (property === 'filter') {
41+
continue;
42+
}
43+
44+
if (
45+
!isEqual(fields[property].value, DEFAULT_QUERY_VALUES[property]) &&
46+
!isEqual(fields[property].value, DEFAULT_FIELD_VALUES[property])
47+
) {
48+
return true;
49+
}
50+
}
51+
return false;
52+
}
53+
3354
/**
3455
* Map query document to the query fields state only preserving valid values
3556
*/
36-
export function mapQueryToFormFields(query?: BaseQuery, onlyValid = true) {
57+
export function mapQueryToFormFields(
58+
query?: BaseQuery,
59+
onlyValid = true
60+
): QueryFormFields {
3761
return Object.fromEntries(
3862
Object.entries(query ?? {})
3963
.map(([key, _value]) => {

packages/compass-query-bar/test/create-mock-ai-endpoint.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,20 @@ function checkReqAuth(req: http.IncomingMessage) {
2020
export async function startMockAIServer(
2121
{
2222
response,
23-
sendError,
2423
}: {
25-
response: any;
26-
sendError?: boolean;
24+
response: {
25+
status: number;
26+
body: any;
27+
};
2728
} = {
2829
response: {
29-
content: {
30-
query: {
31-
find: {
32-
test: 'pineapple',
30+
status: 200,
31+
body: {
32+
content: {
33+
query: {
34+
find: {
35+
test: 'pineapple',
36+
},
3337
},
3438
},
3539
},
@@ -71,14 +75,11 @@ export async function startMockAIServer(
7175
content: jsonObject,
7276
});
7377

74-
if (sendError) {
75-
res.writeHead(500);
76-
res.end('Error occurred.');
77-
return;
78-
}
79-
8078
res.setHeader('Content-Type', 'application/json');
81-
return res.end(JSON.stringify(response));
79+
if (response.status !== 200) {
80+
res.writeHead(response.status);
81+
}
82+
return res.end(JSON.stringify(response.body));
8283
});
8384
})
8485
.listen(0);

0 commit comments

Comments
 (0)