Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security solution] Implement dashboard to track Gen AI Token Usage #159075

Merged
merged 79 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
1a99899
wip
stephmilovic May 25, 2023
aa9dfb7
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine May 25, 2023
06843a0
wip user name
stephmilovic May 25, 2023
a0625be
Merge branch 'gen_ai_tokens' of github.com:stephmilovic/kibana into g…
stephmilovic May 30, 2023
8161281
merge main
stephmilovic May 30, 2023
c99edf9
add user name to event log on gen ai events
stephmilovic May 30, 2023
388e8fa
add tests
stephmilovic May 30, 2023
fde9446
add comment
stephmilovic May 30, 2023
6f898c1
Merge branch 'main' into gen_ai_tokens
stephmilovic May 30, 2023
43181f1
rm silly line
stephmilovic May 30, 2023
c3a323b
remove imports from stack-connectors
stephmilovic May 30, 2023
c18afc0
Update action_executor.ts
stephmilovic May 31, 2023
31d69cc
update user
stephmilovic May 31, 2023
ce09752
add user to every event log event
stephmilovic May 31, 2023
45a32da
Merge branch 'main' into gen_ai_tokens
kibanamachine May 31, 2023
293c846
Merge remote-tracking branch 'upstream/main' into gen_ai_tokens
stephmilovic Jun 1, 2023
38ff3c3
fix bad name
stephmilovic Jun 1, 2023
57a0e07
ys
stephmilovic Jun 1, 2023
e2c5818
Changes for Patrick
stephmilovic Jun 5, 2023
332e404
Merge remote-tracking branch 'upstream/main' into gen_ai_tokens
stephmilovic Jun 5, 2023
797b147
Merge branch 'gen_ai_tokens' into token_lens
stephmilovic Jun 5, 2023
3da6f77
wip still
stephmilovic Jun 5, 2023
98bbc5a
wip still
stephmilovic Jun 5, 2023
2872be8
one more
stephmilovic Jun 5, 2023
1926ef3
Merge branch 'gen_ai_tokens' into token_lens
stephmilovic Jun 5, 2023
b38ef81
add usage dashboard to gen ai connector
stephmilovic Jun 5, 2023
2c87991
Merge branch 'main' into token_lens
stephmilovic Jun 5, 2023
2067313
rm console logs
stephmilovic Jun 5, 2023
8cf01ad
rm more logs
stephmilovic Jun 5, 2023
1f6a939
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 5, 2023
c0e276c
update translation
stephmilovic Jun 5, 2023
0d123e5
by title
stephmilovic Jun 7, 2023
024e3d2
?
stephmilovic Jun 7, 2023
bf86ce8
Merge branch 'token_lens' of github.com:stephmilovic/kibana into toke…
stephmilovic Jun 7, 2023
cfad2b1
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 7, 2023
c5cb490
fix type
stephmilovic Jun 9, 2023
885ebf7
fix broken tests
stephmilovic Jun 9, 2023
98f07fc
Merge branch 'token_lens' of github.com:stephmilovic/kibana into toke…
stephmilovic Jun 9, 2023
f3ec82a
Merge branch 'main' into token_lens
stephmilovic Jun 9, 2023
3fe2cc8
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 9, 2023
7ccef4b
fix type
stephmilovic Jun 9, 2023
cfd339b
Merge branch 'token_lens' of github.com:stephmilovic/kibana into toke…
stephmilovic Jun 9, 2023
4d5740a
WIP big refactor
stephmilovic Jun 12, 2023
a193923
fix
stephmilovic Jun 12, 2023
85b4ca3
much better now
stephmilovic Jun 12, 2023
55ac05f
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 12, 2023
f88f804
rm console logs
stephmilovic Jun 12, 2023
01e4edf
Merge branch 'main' into token_lens
stephmilovic Jun 12, 2023
ac58449
fixes
stephmilovic Jun 12, 2023
5da2400
Merge branch 'token_lens' of github.com:stephmilovic/kibana into toke…
stephmilovic Jun 12, 2023
fe65eee
fix import
stephmilovic Jun 12, 2023
ccb187b
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 12, 2023
b5974cd
better naming
stephmilovic Jun 12, 2023
12fa944
Merge branch 'token_lens' of github.com:stephmilovic/kibana into toke…
stephmilovic Jun 12, 2023
6253899
more fix
stephmilovic Jun 12, 2023
7c35011
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 12, 2023
7964825
add more tests
stephmilovic Jun 13, 2023
eb58c26
complete client side testing
stephmilovic Jun 13, 2023
1ca9b91
Merge branch 'token_lens' of github.com:stephmilovic/kibana into toke…
stephmilovic Jun 13, 2023
3d74443
rm silly
stephmilovic Jun 13, 2023
1499627
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 13, 2023
85e686a
more tests
stephmilovic Jun 13, 2023
f241d89
Merge branch 'token_lens' of github.com:stephmilovic/kibana into toke…
stephmilovic Jun 13, 2023
4fc2103
api tests
stephmilovic Jun 13, 2023
3d31027
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 13, 2023
ccb4f5d
add doc markdown panel to dashboard
stephmilovic Jun 13, 2023
51bdfd1
Merge branch 'token_lens' of github.com:stephmilovic/kibana into toke…
stephmilovic Jun 13, 2023
7f4e2e2
better div height
stephmilovic Jun 13, 2023
e2f4028
test and log changes!
stephmilovic Jun 14, 2023
b66d0a8
add comment to other code that threw me
stephmilovic Jun 14, 2023
41b139b
add retry for tests
stephmilovic Jun 14, 2023
8f8f920
fix tests
stephmilovic Jun 14, 2023
40ebeec
rm logs
stephmilovic Jun 14, 2023
f5d160d
Merge branch 'main' into token_lens
stephmilovic Jun 14, 2023
76ea01d
Merge branch 'main' into token_lens
kibanamachine Jun 14, 2023
cf961aa
test fixed
stephmilovic Jun 14, 2023
fcf019a
Merge branch 'token_lens' of github.com:stephmilovic/kibana into toke…
stephmilovic Jun 14, 2023
1101190
i hope this is it!
stephmilovic Jun 15, 2023
2478a53
fix and test
stephmilovic Jun 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { isPlainObject, isEmpty } from 'lodash';
import { Type } from '@kbn/config-schema';
import { Logger } from '@kbn/logging';
import axios, { AxiosInstance, AxiosResponse, AxiosError, AxiosRequestHeaders } from 'axios';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { assertURL } from './helpers/validators';
import { ActionsConfigurationUtilities } from '../actions_config';
import { SubAction, SubActionRequestParams } from './types';
Expand All @@ -28,6 +30,8 @@ export abstract class SubActionConnector<Config, Secrets> {
private subActions: Map<string, SubAction> = new Map();
private configurationUtilities: ActionsConfigurationUtilities;
protected logger: Logger;
protected esClient: ElasticsearchClient;
protected savedObjectsClient: SavedObjectsClientContract;
protected connector: ServiceParams<Config, Secrets>['connector'];
protected config: Config;
protected secrets: Secrets;
Expand All @@ -37,6 +41,8 @@ export abstract class SubActionConnector<Config, Secrets> {
this.logger = params.logger;
this.config = params.config;
this.secrets = params.secrets;
this.savedObjectsClient = params.services.savedObjectsClient;
this.esClient = params.services.scopedClusterClient;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add these services to the sub action connector to give to GenAI to use to check index permissions (esClient) create dashboard (savedObjectsClient)

this.configurationUtilities = params.configurationUtilities;
this.axiosInstance = axios.create();
}
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/actions/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"@kbn/core-http-server-mocks",
"@kbn/tinymath",
"@kbn/core-saved-objects-utils-server",
"@kbn/core-saved-objects-api-server",
"@kbn/core-elasticsearch-server",
],
"exclude": [
"target/**/*",
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/stack_connectors/common/gen_ai/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const GEN_AI_TITLE = i18n.translate(
export const GEN_AI_CONNECTOR_ID = '.gen-ai';
export enum SUB_ACTION {
RUN = 'run',
DASHBOARD = 'getDashboard',
TEST = 'test',
}
export enum OpenAiProviderType {
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/stack_connectors/common/gen_ai/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,12 @@ export const GenAiRunActionResponseSchema = schema.object(
},
{ unknowns: 'ignore' }
);

// Run action schema
export const GenAiDashboardActionParamsSchema = schema.object({
dashboardId: schema.string(),
});

export const GenAiDashboardActionResponseSchema = schema.object({
available: schema.boolean(),
});
4 changes: 4 additions & 0 deletions x-pack/plugins/stack_connectors/common/gen_ai/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ import {
GenAiSecretsSchema,
GenAiRunActionParamsSchema,
GenAiRunActionResponseSchema,
GenAiDashboardActionParamsSchema,
GenAiDashboardActionResponseSchema,
} from './schema';

export type GenAiConfig = TypeOf<typeof GenAiConfigSchema>;
export type GenAiSecrets = TypeOf<typeof GenAiSecretsSchema>;
export type GenAiRunActionParams = TypeOf<typeof GenAiRunActionParamsSchema>;
export type GenAiRunActionResponse = TypeOf<typeof GenAiRunActionResponseSchema>;
export type GenAiDashboardActionParams = TypeOf<typeof GenAiDashboardActionParamsSchema>;
export type GenAiDashboardActionResponse = TypeOf<typeof GenAiDashboardActionResponseSchema>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { getDashboard } from './api';
import { SUB_ACTION } from '../../../common/gen_ai/constants';
const response = {
available: true,
};

describe('Gen AI Dashboard API', () => {
const http = httpServiceMock.createStartContract();

beforeEach(() => jest.resetAllMocks());
describe('getDashboard', () => {
test('should call get dashboard API', async () => {
const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(response);
const res = await getDashboard({
http,
signal: abortCtrl.signal,
connectorId: 'te/st',
dashboardId: 'cool-dashboard',
});

expect(res).toEqual(response);
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: `{"params":{"subAction":"${SUB_ACTION.DASHBOARD}","subActionParams":{"dashboardId":"cool-dashboard"}}}`,
signal: abortCtrl.signal,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { HttpSetup } from '@kbn/core-http-browser';
import { ActionTypeExecutorResult, BASE_ACTION_API_PATH } from '@kbn/actions-plugin/common';
import { SUB_ACTION } from '../../../common/gen_ai/constants';
import { ConnectorExecutorResult, rewriteResponseToCamelCase } from '../lib/rewrite_response_body';

export async function getDashboard({
http,
signal,
dashboardId,
connectorId,
}: {
http: HttpSetup;
signal: AbortSignal;
connectorId: string;
dashboardId: string;
}): Promise<ActionTypeExecutorResult<{ available: boolean }>> {
const res = await http.post<ConnectorExecutorResult<{ available: boolean }>>(
`${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
{
body: JSON.stringify({
params: { subAction: SUB_ACTION.DASHBOARD, subActionParams: { dashboardId } },
}),
signal,
}
);
return rewriteResponseToCamelCase(res);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,54 @@
import React from 'react';
import GenerativeAiConnectorFields from './connector';
import { ConnectorFormTestProvider } from '../lib/test_utils';
import { act, render, waitFor } from '@testing-library/react';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { OpenAiProviderType } from '../../../common/gen_ai/constants';
import { useKibana } from '@kbn/triggers-actions-ui-plugin/public';
import { useGetDashboard } from './use_get_dashboard';

jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
jest.mock('./use_get_dashboard');

const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
const mockDashboard = useGetDashboard as jest.Mock;
const openAiConnector = {
actionTypeId: '.gen-ai',
name: 'genAi',
id: '123',
config: {
apiUrl: 'https://openaiurl.com',
apiProvider: OpenAiProviderType.OpenAi,
},
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
isDeprecated: false,
};
const azureConnector = {
...openAiConnector,
config: {
apiUrl: 'https://azureaiurl.com',
apiProvider: OpenAiProviderType.AzureAi,
},
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
};

const navigateToUrl = jest.fn();

describe('GenerativeAiConnectorFields renders', () => {
beforeEach(() => {
jest.clearAllMocks();
useKibanaMock().services.application.navigateToUrl = navigateToUrl;
mockDashboard.mockImplementation(({ connectorId }) => ({
dashboardUrl: `https://dashboardurl.com/${connectorId}`,
}));
});
test('open ai connector fields are rendered', async () => {
const actionConnector = {
actionTypeId: '.gen-ai',
name: 'genAi',
config: {
apiUrl: 'https://openaiurl.com',
apiProvider: OpenAiProviderType.OpenAi,
},
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
isDeprecated: false,
};

const { getAllByTestId } = render(
<ConnectorFormTestProvider connector={actionConnector}>
<ConnectorFormTestProvider connector={openAiConnector}>
<GenerativeAiConnectorFields
readOnly={false}
isEdit={false}
Expand All @@ -37,71 +64,99 @@ describe('GenerativeAiConnectorFields renders', () => {
</ConnectorFormTestProvider>
);
expect(getAllByTestId('config.apiUrl-input')[0]).toBeInTheDocument();
expect(getAllByTestId('config.apiUrl-input')[0]).toHaveValue(actionConnector.config.apiUrl);
expect(getAllByTestId('config.apiUrl-input')[0]).toHaveValue(openAiConnector.config.apiUrl);
expect(getAllByTestId('config.apiProvider-select')[0]).toBeInTheDocument();
expect(getAllByTestId('config.apiProvider-select')[0]).toHaveValue(
actionConnector.config.apiProvider
openAiConnector.config.apiProvider
);
expect(getAllByTestId('open-ai-api-doc')[0]).toBeInTheDocument();
expect(getAllByTestId('open-ai-api-keys-doc')[0]).toBeInTheDocument();
});

test('azure ai connector fields are rendered', async () => {
const actionConnector = {
actionTypeId: '.gen-ai',
name: 'genAi',
config: {
apiUrl: 'https://azureaiurl.com',
apiProvider: OpenAiProviderType.AzureAi,
},
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
isDeprecated: false,
};

const { getAllByTestId } = render(
<ConnectorFormTestProvider connector={actionConnector}>
<ConnectorFormTestProvider connector={azureConnector}>
<GenerativeAiConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);

expect(getAllByTestId('config.apiUrl-input')[0]).toBeInTheDocument();
expect(getAllByTestId('config.apiUrl-input')[0]).toHaveValue(actionConnector.config.apiUrl);
expect(getAllByTestId('config.apiUrl-input')[0]).toHaveValue(azureConnector.config.apiUrl);
expect(getAllByTestId('config.apiProvider-select')[0]).toBeInTheDocument();
expect(getAllByTestId('config.apiProvider-select')[0]).toHaveValue(
actionConnector.config.apiProvider
azureConnector.config.apiProvider
);
expect(getAllByTestId('azure-ai-api-doc')[0]).toBeInTheDocument();
expect(getAllByTestId('azure-ai-api-keys-doc')[0]).toBeInTheDocument();
});

describe('Dashboard link', () => {
it('Does not render if isEdit is false and dashboardUrl is defined', async () => {
const { queryByTestId } = render(
<ConnectorFormTestProvider connector={openAiConnector}>
<GenerativeAiConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
expect(queryByTestId('link-gen-ai-token-dashboard')).not.toBeInTheDocument();
});
it('Does not render if isEdit is true and dashboardUrl is null', async () => {
mockDashboard.mockImplementation((id: string) => ({
dashboardUrl: null,
}));
const { queryByTestId } = render(
<ConnectorFormTestProvider connector={openAiConnector}>
<GenerativeAiConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
expect(queryByTestId('link-gen-ai-token-dashboard')).not.toBeInTheDocument();
});
it('Renders if isEdit is true and dashboardUrl is defined', async () => {
const { getByTestId } = render(
<ConnectorFormTestProvider connector={openAiConnector}>
<GenerativeAiConnectorFields
readOnly={false}
isEdit={true}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
expect(getByTestId('link-gen-ai-token-dashboard')).toBeInTheDocument();
});
it('On click triggers redirect with correct saved object id', async () => {
const { getByTestId } = render(
<ConnectorFormTestProvider connector={openAiConnector}>
<GenerativeAiConnectorFields
readOnly={false}
isEdit={true}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
fireEvent.click(getByTestId('link-gen-ai-token-dashboard'));
expect(navigateToUrl).toHaveBeenCalledWith(`https://dashboardurl.com/123`);
});
});
describe('Validation', () => {
const onSubmit = jest.fn();
const actionConnector = {
actionTypeId: '.gen-ai',
name: 'genAi',
config: {
apiUrl: 'https://openaiurl.com',
apiProvider: OpenAiProviderType.OpenAi,
},
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
isDeprecated: false,
};

beforeEach(() => {
jest.clearAllMocks();
});

it('connector validation succeeds when connector config is valid', async () => {
const { getByTestId } = render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<ConnectorFormTestProvider connector={openAiConnector} onSubmit={onSubmit}>
<GenerativeAiConnectorFields
readOnly={false}
isEdit={false}
Expand All @@ -119,16 +174,16 @@ describe('GenerativeAiConnectorFields renders', () => {
});

expect(onSubmit).toBeCalledWith({
data: actionConnector,
data: openAiConnector,
isValid: true,
});
});

it('validates correctly if the apiUrl is empty', async () => {
const connector = {
...actionConnector,
...openAiConnector,
config: {
...actionConnector.config,
...openAiConnector.config,
apiUrl: '',
},
};
Expand Down Expand Up @@ -159,9 +214,9 @@ describe('GenerativeAiConnectorFields renders', () => {
];
it.each(tests)('validates correctly %p', async (field, value) => {
const connector = {
...actionConnector,
...openAiConnector,
config: {
...actionConnector.config,
...openAiConnector.config,
headers: [],
},
};
Expand Down
Loading