Skip to content

Commit 0288bb4

Browse files
[Playground] Propagate Error message into FE (#182201)
## Summary - Fix error not being propagated into FE - Added tests ## UI ### Rate limit error message: ![Screenshot 2024-04-30 at 1 05 40 PM](https://github.com/elastic/kibana/assets/150824886/2734d27b-ef2b-469b-9344-a7c62cd502bc) ### BAD LLM ![Screenshot 2024-04-30 at 1 05 47 PM](https://github.com/elastic/kibana/assets/150824886/fb3025e5-5f5e-49e6-8277-7dba52cc76cd) ### Invalid API key ![Screenshot 2024-04-30 at 1 06 26 PM](https://github.com/elastic/kibana/assets/150824886/67c1bdf6-6785-48ed-870a-ae24e8ac7e9f) ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent f3d18fa commit 0288bb4

4 files changed

Lines changed: 184 additions & 4 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import {
9+
IRouter,
10+
KibanaRequest,
11+
RequestHandlerContext,
12+
RouteValidatorConfig,
13+
} from '@kbn/core/server';
14+
import { httpServiceMock, httpServerMock } from '@kbn/core/server/mocks';
15+
16+
/**
17+
* Test helper that mocks Kibana's router and DRYs out various helper (callRoute, schema validation)
18+
*/
19+
20+
type MethodType = 'get' | 'post' | 'put' | 'patch' | 'delete';
21+
type PayloadType = 'params' | 'query' | 'body';
22+
23+
interface IMockRouter {
24+
method: MethodType;
25+
path: string;
26+
context?: jest.Mocked<RequestHandlerContext>;
27+
}
28+
interface IMockRouterRequest {
29+
body?: object;
30+
query?: object;
31+
params?: object;
32+
}
33+
type MockRouterRequest = KibanaRequest | IMockRouterRequest;
34+
35+
export class MockRouter {
36+
public router!: jest.Mocked<IRouter>;
37+
public method: MethodType;
38+
public path: string;
39+
public context: jest.Mocked<RequestHandlerContext>;
40+
public payload?: PayloadType;
41+
public response = httpServerMock.createResponseFactory();
42+
43+
constructor({ method, path, context = {} as jest.Mocked<RequestHandlerContext> }: IMockRouter) {
44+
this.createRouter();
45+
this.method = method;
46+
this.path = path;
47+
this.context = context;
48+
}
49+
50+
public createRouter = () => {
51+
this.router = httpServiceMock.createRouter();
52+
};
53+
54+
public callRoute = async (request: MockRouterRequest) => {
55+
const route = this.findRouteRegistration();
56+
const [, handler] = route;
57+
await handler(this.context, httpServerMock.createKibanaRequest(request as any), this.response);
58+
};
59+
60+
/**
61+
* Schema validation helpers
62+
*/
63+
64+
public validateRoute = (request: MockRouterRequest) => {
65+
const route = this.findRouteRegistration();
66+
const [config] = route;
67+
const validate = config.validate as RouteValidatorConfig<{}, {}, {}>;
68+
const payloads = Object.keys(request) as PayloadType[];
69+
70+
payloads.forEach((payload: PayloadType) => {
71+
const payloadValidation = validate[payload] as { validate(request: KibanaRequest): void };
72+
const payloadRequest = request[payload] as KibanaRequest;
73+
74+
payloadValidation.validate(payloadRequest);
75+
});
76+
};
77+
78+
public shouldValidate = (request: MockRouterRequest) => {
79+
expect(() => this.validateRoute(request)).not.toThrow();
80+
};
81+
82+
public shouldThrow = (request: MockRouterRequest) => {
83+
expect(() => this.validateRoute(request)).toThrow();
84+
};
85+
86+
private findRouteRegistration = () => {
87+
const routerCalls = this.router[this.method].mock.calls as any[];
88+
if (!routerCalls.length) throw new Error('No routes registered.');
89+
90+
const route = routerCalls.find(([router]: any) => router.path === this.path);
91+
if (!route) throw new Error('No matching registered routes found - check method/path keys');
92+
93+
return route;
94+
};
95+
}

x-pack/plugins/search_playground/server/routes.test.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,19 @@
55
* 2.0.
66
*/
77

8-
import { createRetriever } from './routes';
8+
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
9+
import { RequestHandlerContext } from '@kbn/core/server';
10+
import { coreMock } from '@kbn/core/server/mocks';
11+
import { MockRouter } from '../__mocks__/router.mock';
12+
import { ConversationalChain } from './lib/conversational_chain';
13+
import { getChatParams } from './lib/get_chat_params';
14+
import { createRetriever, defineRoutes } from './routes';
15+
16+
jest.mock('./lib/get_chat_params', () => ({
17+
getChatParams: jest.fn(),
18+
}));
19+
20+
jest.mock('./lib/conversational_chain');
921

1022
describe('createRetriever', () => {
1123
test('works when the question has quotes', () => {
@@ -18,3 +30,75 @@ describe('createRetriever', () => {
1830
expect(result).toEqual({ query: { match: { text: 'How can I "do something" with quotes?' } } });
1931
});
2032
});
33+
34+
describe('Search Playground routes', () => {
35+
let mockRouter: MockRouter;
36+
const mockClient = {
37+
asCurrentUser: {},
38+
};
39+
40+
const mockCore = {
41+
elasticsearch: { client: mockClient },
42+
};
43+
const mockLogger = loggingSystemMock.createLogger().get();
44+
45+
describe('POST - Chat Messages', () => {
46+
const mockData = {
47+
connector_id: 'open-ai',
48+
indices: 'my-index',
49+
prompt: 'You are an assistant',
50+
citations: true,
51+
elasticsearch_query: {},
52+
summarization_model: 'GPT-4',
53+
doc_size: 3,
54+
source_fields: '{}',
55+
};
56+
57+
const mockRequestBody = {
58+
data: mockData,
59+
};
60+
61+
beforeEach(() => {
62+
jest.clearAllMocks();
63+
64+
const coreStart = coreMock.createStart();
65+
66+
const context = {
67+
core: Promise.resolve(mockCore),
68+
} as unknown as jest.Mocked<RequestHandlerContext>;
69+
70+
mockRouter = new MockRouter({
71+
context,
72+
method: 'post',
73+
path: '/internal/search_playground/chat',
74+
});
75+
76+
defineRoutes({
77+
logger: mockLogger,
78+
router: mockRouter.router,
79+
getStartServices: jest.fn().mockResolvedValue([coreStart, {}, {}]),
80+
});
81+
});
82+
83+
it('responds with error message if stream throws an error', async () => {
84+
(getChatParams as jest.Mock).mockResolvedValue({ model: 'open-ai' });
85+
(ConversationalChain as jest.Mock).mockImplementation(() => {
86+
return {
87+
stream: jest
88+
.fn()
89+
.mockRejectedValue(new Error('Unexpected API error - Some Open AI error message')),
90+
};
91+
});
92+
93+
await mockRouter.callRoute({
94+
body: mockRequestBody,
95+
});
96+
97+
expect(mockRouter.response.badRequest).toHaveBeenCalledWith({
98+
body: {
99+
message: 'Unexpected API error - Some Open AI error message',
100+
},
101+
});
102+
});
103+
});
104+
});

x-pack/plugins/search_playground/server/routes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ export function defineRoutes({
131131
} catch (e) {
132132
logger.error('Failed to create the chat stream', e);
133133

134-
if (typeof e === 'string') {
134+
if (typeof e === 'object') {
135135
return response.badRequest({
136136
body: {
137-
message: e,
137+
message: e.message,
138138
},
139139
});
140140
}

x-pack/plugins/search_playground/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"@kbn/elastic-assistant-common",
3636
"@kbn/logging",
3737
"@kbn/react-kibana-context-render",
38-
"@kbn/doc-links"
38+
"@kbn/doc-links",
39+
"@kbn/core-logging-server-mocks"
3940
],
4041
"exclude": [
4142
"target/**/*",

0 commit comments

Comments
 (0)