AI Assistant Management Plugin + Knowledge Base Management (#171933)

## Summary

This PR adds a bunch of plugins to help manage AI Assistant Management
settings.

It offers a 'selection' plugin inside Stack Management where a user can
select which AI Assistant she wants to manage.
The Security team can hook into this one, so settings for both AI
Assistants can be accessed from inside one place inside Stack
Management.

This PR also adds the plugin to manage settings for the AI Assistant for
Observability, including Knowledge Base management. This plugin is
available both in Stack Management (stateful) and Project Settings
(serverless).

## What it looks like

51392ec5-05c9-4947-9bf2-810d8d0b7525


## Detailed
1. **Adds a Stack Management plugin**
(`/src/plugins/ai_assistant_management/selection`). Its primary function
is to render a selection screen to help users navigate to the settings
plugin for the AI Assistant for a specific solution. This plugin is
displayed in Stack Management, which is only available in stateful
versions of Kibana.

2. **Adds a AI Assistant for Observability Settings plugin**
(`/src/plugins/ai_assistant_management/observability`). This plugin
allows management of specific Observability AI Assistant settings. It is
available in stateful versions of Kibana (via the aforementioned Stack
Management plugin) or in serverless versions via Project Management.

3. **Knowledge Base management for Observability AI Assistant**: The AI
Assistant for Observability Settings plugin has a Knowledge Base tab,
which allows users to add / read / update / delete and bulk import
entries into the Knowledge Base of the Observability AI Assistant.

4. **Moving of KB endpoints in Observability AI Assistant plugin**: KB
endpoints and functions were located in the same folder. As this PR adds
new endpoints for the KB for CRUD operations, it also moves the existing
ones from the function folder into a dedicated one so there's a clearer
distinction between kb and functions.

5. **Adding of GenAI Connector inside Chat Flyout**: If the user has
admin rights, it is possible to set up a GenAI connector from within the
Observability AI Assistant Chat Flyout. This provides a faster and more
seamless onboarding experience. If the user does not, she will be
redirected to the Settings page.

## Bug fixes
* Fixes chat item styling issues (padding, background color).

## How to test
* Check if the Stack Management plugin works on stateful
* Check if the AI Assistant Settings plugin works on stateful +
serverless
* Check if CRUD operations on KB work
* Check if searching on KB entries work
* Check if its possible to navigate to KB tab directly
(`app/management/kibana/aiAssistantManagementObservability?tab=knowledge_base`)


## Todo
- [x] Add sorting to getEntries
- [x] Add params for tab routing
- [x] Add unit tests
- [ ] Add API tests
- [ ] Add fallback for already indexed entries when searching

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Coen Warmer 2023-12-05 23:07:52 +01:00 committed by GitHub
parent 45592cd777
commit 7d990cf749
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
117 changed files with 3962 additions and 691 deletions

View file

@ -921,6 +921,7 @@ module.exports = {
'x-pack/plugins/profiling/**/*.tsx',
'x-pack/plugins/synthetics/**/*.tsx',
'x-pack/plugins/ux/**/*.tsx',
'src/plugins/ai_assistant_management/**/*.tsx',
],
rules: {
'@kbn/telemetry/event_generating_elements_should_be_instrumented': 'error',
@ -938,6 +939,7 @@ module.exports = {
'x-pack/plugins/profiling/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
'x-pack/plugins/synthetics/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
'x-pack/plugins/ux/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
'src/plugins/ai_assistant_management/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
],
rules: {
'@kbn/i18n/strings_should_be_translated_with_i18n': 'warn',

2
.github/CODEOWNERS vendored
View file

@ -11,6 +11,8 @@ x-pack/plugins/actions @elastic/response-ops
x-pack/test/alerting_api_integration/common/plugins/actions_simulators @elastic/response-ops
packages/kbn-actions-types @elastic/response-ops
src/plugins/advanced_settings @elastic/appex-sharedux @elastic/platform-deployment-management
src/plugins/ai_assistant_management/observability @elastic/obs-knowledge-team
src/plugins/ai_assistant_management/selection @elastic/obs-knowledge-team
x-pack/packages/ml/aiops_components @elastic/ml-ui
x-pack/plugins/aiops @elastic/ml-ui
x-pack/packages/ml/aiops_utils @elastic/ml-ui

View file

@ -1,6 +1,8 @@
{
"paths": {
"advancedSettings": "src/plugins/advanced_settings",
"aiAssistantManagementSelection": "src/plugins/ai_assistant_management/selection",
"aiAssistantManagementObservability": "src/plugins/ai_assistant_management/observability",
"alerts": "packages/kbn-alerts/src",
"alertsUIShared": "packages/kbn-alerts-ui-shared/src",
"alertingTypes": "packages/kbn-alerting-types",

View file

@ -28,6 +28,14 @@ allowing users to configure their advanced settings, also known
as uiSettings within the code.
|{kib-repo}blob/{branch}/src/plugins/ai_assistant_management/observability/README.md[aiAssistantManagementObservability]
|The aiAssistantManagementObservability plugin manages the Ai Assistant for Observability management section.
|{kib-repo}blob/{branch}/src/plugins/ai_assistant_management/selection/README.md[aiAssistantManagementSelection]
|The aiAssistantManagementSelection plugin manages the Ai Assistant management section.
|{kib-repo}blob/{branch}/src/plugins/bfetch/README.md[bfetch]
|bfetch allows to batch HTTP requests and streams responses back.

View file

@ -135,6 +135,8 @@
"@kbn/actions-simulators-plugin": "link:x-pack/test/alerting_api_integration/common/plugins/actions_simulators",
"@kbn/actions-types": "link:packages/kbn-actions-types",
"@kbn/advanced-settings-plugin": "link:src/plugins/advanced_settings",
"@kbn/ai-assistant-management-observability-plugin": "link:src/plugins/ai_assistant_management/observability",
"@kbn/ai-assistant-management-plugin": "link:src/plugins/ai_assistant_management/selection",
"@kbn/aiops-components": "link:x-pack/packages/ml/aiops_components",
"@kbn/aiops-plugin": "link:x-pack/plugins/aiops",
"@kbn/aiops-utils": "link:x-pack/packages/ml/aiops_utils",

View file

@ -27,6 +27,8 @@ export type IntegrationsDeepLinkId = IntegrationsAppId | FleetAppId | OsQueryApp
// Management
export type ManagementAppId = typeof MANAGEMENT_APP_ID;
export type ManagementId =
| 'aiAssistantManagementSelection'
| 'aiAssistantManagementObservability'
| 'api_keys'
| 'cases'
| 'cross_cluster_replication'

View file

@ -119,6 +119,9 @@ export const defaultNavigation: ManagementNodeDefinition = {
{
link: 'management:dataViews',
},
{
link: 'management:aiAssistantManagementSelection',
},
{
// Saved objects
link: 'management:objects',

View file

@ -1,6 +1,8 @@
pageLoadAssetSize:
actions: 20000
advancedSettings: 27596
aiAssistantManagementObservability: 19279
aiAssistantManagementSelection: 19146
aiops: 10000
alerting: 106936
apm: 64385

View file

@ -29,6 +29,8 @@ const allNavLinks: AppDeepLinkId[] = [
'fleet',
'integrations',
'management',
'management:aiAssistantManagementSelection',
'management:aiAssistantManagementObservability',
'management:api_keys',
'management:cases',
'management:cross_cluster_replication',

View file

@ -0,0 +1,3 @@
# `aiAssistantManagementObservability` plugin
The `aiAssistantManagementObservability` plugin manages the `Ai Assistant for Observability` management section.

View file

@ -0,0 +1,18 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/src/plugins/ai_assistant_management/observability'],
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/src/plugins/ai_assistant_management',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/src/plugins/ai_assistant_management/observability/{common,public,server}/**/*.{ts,tsx}',
],
};

View file

@ -0,0 +1,13 @@
{
"type": "plugin",
"id": "@kbn/ai-assistant-management-observability-plugin",
"owner": "@elastic/obs-knowledge-team",
"plugin": {
"id": "aiAssistantManagementObservability",
"server": false,
"browser": true,
"requiredPlugins": ["management"],
"optionalPlugins": ["actions", "home", "observabilityAIAssistant", "serverless"],
"requiredBundles": ["kibanaReact"]
}
}

View file

@ -0,0 +1,79 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { I18nProvider } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { CoreSetup } from '@kbn/core/public';
import { wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
import { StartDependencies, AiAssistantManagementObservabilityPluginStart } from './plugin';
import { aIAssistantManagementObservabilityRouter } from './routes/config';
import { RedirectToHomeIfUnauthorized } from './routes/components/redirect_to_home_if_unauthorized';
import { AppContextProvider } from './context/app_context';
interface MountParams {
core: CoreSetup<StartDependencies, AiAssistantManagementObservabilityPluginStart>;
mountParams: ManagementAppMountParams;
}
export const mountManagementSection = async ({ core, mountParams }: MountParams) => {
const [coreStart, startDeps] = await core.getStartServices();
if (!startDeps.observabilityAIAssistant) return () => {};
const { element, history, setBreadcrumbs } = mountParams;
const { theme$ } = core.theme;
coreStart.chrome.docTitle.change(
i18n.translate('aiAssistantManagementObservability.app.titleBar', {
defaultMessage: 'AI Assistant for Observability Settings',
})
);
const queryClient = new QueryClient();
ReactDOM.render(
wrapWithTheme(
<RedirectToHomeIfUnauthorized coreStart={coreStart}>
<I18nProvider>
<AppContextProvider
value={{
application: coreStart.application,
http: coreStart.http,
notifications: coreStart.notifications,
observabilityAIAssistant: startDeps.observabilityAIAssistant,
uiSettings: coreStart.uiSettings,
serverless: startDeps.serverless,
setBreadcrumbs,
}}
>
<QueryClientProvider client={queryClient}>
<RouterProvider
history={history}
router={aIAssistantManagementObservabilityRouter as any}
>
<RouteRenderer />
</RouterProvider>
</QueryClientProvider>
</AppContextProvider>
</I18nProvider>
</RedirectToHomeIfUnauthorized>,
theme$
),
element
);
return () => {
coreStart.chrome.docTitle.reset();
ReactDOM.unmountComponentAtNode(element);
};
};

View file

@ -0,0 +1,14 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const REACT_QUERY_KEYS = {
GET_GENAI_CONNECTORS: 'get_genai_connectors',
GET_KB_ENTRIES: 'get_kb_entries',
CREATE_KB_ENTRIES: 'create_kb_entry',
IMPORT_KB_ENTRIES: 'import_kb_entry',
};

View file

@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { createContext } from 'react';
import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser';
import type { CoreStart, HttpSetup } from '@kbn/core/public';
import type { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public';
import type { StartDependencies } from '../plugin';
export interface ContextValue extends StartDependencies {
application: CoreStart['application'];
http: HttpSetup;
notifications: CoreStart['notifications'];
observabilityAIAssistant: ObservabilityAIAssistantPluginStart;
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
uiSettings: CoreStart['uiSettings'];
}
export const AppContext = createContext<ContextValue>(null as any);
export const AppContextProvider = ({
children,
value,
}: {
value: ContextValue;
children: React.ReactNode;
}) => {
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

View file

@ -0,0 +1,30 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common/types';
export interface KnowledgeBaseEntryCategory {
'@timestamp': string;
categoryName: string;
entries: KnowledgeBaseEntry[];
}
export function categorizeEntries({ entries }: { entries: KnowledgeBaseEntry[] }) {
return entries.reduce((acc, entry) => {
const categoryName = entry.labels.category ?? entry.id;
const index = acc.findIndex((item) => item.categoryName === categoryName);
if (index > -1) {
acc[index].entries.push(entry);
return acc;
} else {
return acc.concat({ categoryName, entries: [entry], '@timestamp': entry['@timestamp'] });
}
}, [] as Array<{ categoryName: string; entries: KnowledgeBaseEntry[]; '@timestamp': string }>);
}

View file

@ -0,0 +1,89 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { createMemoryHistory } from 'history';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render as testLibRender } from '@testing-library/react';
import { coreMock } from '@kbn/core/public/mocks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import translations from '@kbn/translations-plugin/translations/ja-JP.json';
import { mockObservabilityAIAssistantService } from '@kbn/observability-ai-assistant-plugin/public';
import { RouterProvider } from '@kbn/typed-react-router-config';
import { AppContextProvider } from '../context/app_context';
import { RedirectToHomeIfUnauthorized } from '../routes/components/redirect_to_home_if_unauthorized';
import { aIAssistantManagementObservabilityRouter } from '../routes/config';
export const coreStart = coreMock.createStart();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
logger: {
// eslint-disable-next-line no-console
log: console.log,
// eslint-disable-next-line no-console
warn: console.warn,
error: () => {},
},
});
export const render = (component: React.ReactNode, params?: { show: boolean }) => {
const history = createMemoryHistory();
return testLibRender(
// @ts-ignore
<IntlProvider locale="en-US" messages={translations.messages}>
<RedirectToHomeIfUnauthorized
coreStart={{
application: {
...coreStart.application,
capabilities: {
// @ts-ignore
management: { show: true },
observabilityAIAssistant: {
show: params?.show ?? true,
},
},
},
}}
>
<AppContextProvider
value={{
http: coreStart.http,
application: coreStart.application,
notifications: coreStart.notifications,
observabilityAIAssistant: {
service: mockObservabilityAIAssistantService,
useGenAIConnectors: () => ({
loading: false,
selectConnector: () => {},
reloadConnectors: () => {},
}),
},
uiSettings: coreStart.uiSettings,
setBreadcrumbs: () => {},
}}
>
<QueryClientProvider client={queryClient}>
<RouterProvider
history={history}
router={aIAssistantManagementObservabilityRouter as any}
>
{component}
</RouterProvider>
</QueryClientProvider>
</AppContextProvider>
</RedirectToHomeIfUnauthorized>
</IntlProvider>
);
};

View file

@ -0,0 +1,18 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { useContext } from 'react';
import { AppContext, ContextValue } from '../context/app_context';
export const useAppContext = () => {
const ctx = useContext<ContextValue>(AppContext);
if (!ctx) {
throw new Error('"useAppContext" can only be called inside of AppContext.Provider!');
}
return ctx;
};

View file

@ -0,0 +1,85 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import type { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common/types';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useAppContext } from './use_app_context';
import { REACT_QUERY_KEYS } from '../constants';
type ServerError = IHttpFetchError<ResponseErrorBody>;
export function useCreateKnowledgeBaseEntry() {
const {
notifications: { toasts },
observabilityAIAssistant,
} = useAppContext();
const queryClient = useQueryClient();
const observabilityAIAssistantApi = observabilityAIAssistant.service.callApi;
return useMutation<
void,
ServerError,
{
entry: Omit<
KnowledgeBaseEntry,
'@timestamp' | 'confidence' | 'is_correction' | 'public' | 'labels' | 'role'
>;
}
>(
[REACT_QUERY_KEYS.CREATE_KB_ENTRIES],
({ entry }) => {
if (!observabilityAIAssistantApi) {
return Promise.reject('Error with observabilityAIAssistantApi: API not found.');
}
return observabilityAIAssistantApi?.(
'POST /internal/observability_ai_assistant/kb/entries/save',
{
signal: null,
params: {
body: {
...entry,
role: 'user_entry',
},
},
}
);
},
{
onSuccess: (_data, { entry }) => {
toasts.addSuccess(
i18n.translate(
'aiAssistantManagementObservability.kb.addManualEntry.successNotification',
{
defaultMessage: 'Successfully created {name}',
values: { name: entry.id },
}
)
);
queryClient.invalidateQueries({
queryKey: [REACT_QUERY_KEYS.GET_KB_ENTRIES],
refetchType: 'all',
});
},
onError: (error, { entry }) => {
toasts.addError(new Error(error.body?.message ?? error.message), {
title: i18n.translate(
'aiAssistantManagementObservability.kb.addManualEntry.errorNotification',
{
defaultMessage: 'Something went wrong while creating {name}',
values: { name: entry.id },
}
),
});
},
}
);
}

View file

@ -0,0 +1,74 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useAppContext } from './use_app_context';
import { REACT_QUERY_KEYS } from '../constants';
type ServerError = IHttpFetchError<ResponseErrorBody>;
export function useDeleteKnowledgeBaseEntry() {
const {
observabilityAIAssistant,
notifications: { toasts },
} = useAppContext();
const queryClient = useQueryClient();
const observabilityAIAssistantApi = observabilityAIAssistant.service.callApi;
return useMutation<unknown, ServerError, { id: string }>(
[REACT_QUERY_KEYS.CREATE_KB_ENTRIES],
({ id: entryId }) => {
if (!observabilityAIAssistantApi) {
return Promise.reject('Error with observabilityAIAssistantApi: API not found.');
}
return observabilityAIAssistantApi?.(
'DELETE /internal/observability_ai_assistant/kb/entries/{entryId}',
{
signal: null,
params: {
path: {
entryId,
},
},
}
);
},
{
onSuccess: (_data, { id }) => {
toasts.addSuccess(
i18n.translate(
'aiAssistantManagementObservability.kb.deleteManualEntry.successNotification',
{
defaultMessage: 'Successfully deleted {id}',
values: { id },
}
)
);
queryClient.invalidateQueries({
queryKey: [REACT_QUERY_KEYS.GET_KB_ENTRIES],
refetchType: 'all',
});
},
onError: (error, { id }) => {
toasts.addError(new Error(error.body?.message ?? error.message), {
title: i18n.translate(
'aiAssistantManagementObservability.kb.deleteManualEntry.errorNotification',
{
defaultMessage: 'Something went wrong while deleting {name}',
values: { name: id },
}
),
});
},
}
);
}

View file

@ -0,0 +1,56 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { useQuery } from '@tanstack/react-query';
import { REACT_QUERY_KEYS } from '../constants';
import { useAppContext } from './use_app_context';
export function useGetKnowledgeBaseEntries({
query,
sortBy,
sortDirection,
}: {
query: string;
sortBy: string;
sortDirection: 'asc' | 'desc';
}) {
const { observabilityAIAssistant } = useAppContext();
const observabilityAIAssistantApi = observabilityAIAssistant.service.callApi;
const { isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery({
queryKey: [REACT_QUERY_KEYS.GET_KB_ENTRIES, query, sortBy, sortDirection],
queryFn: async ({ signal }) => {
if (!observabilityAIAssistantApi || !signal) {
return Promise.reject('Error with observabilityAIAssistantApi: API not found.');
}
return observabilityAIAssistantApi(`GET /internal/observability_ai_assistant/kb/entries`, {
signal,
params: {
query: {
query,
sortBy,
sortDirection,
},
},
});
},
keepPreviousData: true,
refetchOnWindowFocus: false,
});
return {
entries: data?.entries,
refetch,
isLoading,
isRefetching,
isSuccess,
isError,
};
}

View file

@ -0,0 +1,85 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import type { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common/types';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useAppContext } from './use_app_context';
import { REACT_QUERY_KEYS } from '../constants';
type ServerError = IHttpFetchError<ResponseErrorBody>;
export function useImportKnowledgeBaseEntries() {
const {
observabilityAIAssistant,
notifications: { toasts },
} = useAppContext();
const queryClient = useQueryClient();
const observabilityAIAssistantApi = observabilityAIAssistant.service.callApi;
return useMutation<
void,
ServerError,
{
entries: Array<
Omit<
KnowledgeBaseEntry,
'@timestamp' | 'confidence' | 'is_correction' | 'public' | 'labels'
>
>;
}
>(
[REACT_QUERY_KEYS.IMPORT_KB_ENTRIES],
({ entries }) => {
if (!observabilityAIAssistantApi) {
return Promise.reject('Error with observabilityAIAssistantApi: API not found.');
}
return observabilityAIAssistantApi?.(
'POST /internal/observability_ai_assistant/kb/entries/import',
{
signal: null,
params: {
body: {
entries,
},
},
}
);
},
{
onSuccess: (_data, { entries }) => {
toasts.addSuccess(
i18n.translate(
'aiAssistantManagementObservability.kb.importEntries.successNotification',
{
defaultMessage: 'Successfully imported {number} items',
values: { number: entries.length },
}
)
);
queryClient.invalidateQueries({
queryKey: [REACT_QUERY_KEYS.GET_KB_ENTRIES],
refetchType: 'all',
});
},
onError: (error) => {
toasts.addError(new Error(error.body?.message ?? error.message), {
title: i18n.translate(
'aiAssistantManagementObservability.kb.importEntries.errorNotification',
{
defaultMessage: 'Something went wrong while importing items',
}
),
});
},
}
);
}

View file

@ -0,0 +1,16 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { type PathsOf, type TypeOf, useParams } from '@kbn/typed-react-router-config';
import type { AIAssistantManagementObservabilityRoutes } from '../routes/config';
export function useObservabilityAIAssistantManagementRouterParams<
TPath extends PathsOf<AIAssistantManagementObservabilityRoutes>
>(path: TPath): TypeOf<AIAssistantManagementObservabilityRoutes, TPath> {
return useParams(path)! as TypeOf<AIAssistantManagementObservabilityRoutes, TPath>;
}

View file

@ -0,0 +1,60 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PathsOf, TypeAsArgs, TypeOf } from '@kbn/typed-react-router-config';
import { useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import { useAppContext } from './use_app_context';
import {
AIAssistantManagementObservabilityRouter,
AIAssistantManagementObservabilityRoutes,
} from '../routes/config';
import { aIAssistantManagementObservabilityRouter } from '../routes/config';
interface StatefulObservabilityAIAssistantRouter extends AIAssistantManagementObservabilityRouter {
push<T extends PathsOf<AIAssistantManagementObservabilityRoutes>>(
path: T,
...params: TypeAsArgs<TypeOf<AIAssistantManagementObservabilityRoutes, T>>
): void;
replace<T extends PathsOf<AIAssistantManagementObservabilityRoutes>>(
path: T,
...params: TypeAsArgs<TypeOf<AIAssistantManagementObservabilityRoutes, T>>
): void;
}
export function useObservabilityAIAssistantManagementRouter(): StatefulObservabilityAIAssistantRouter {
const history = useHistory();
const { http } = useAppContext();
const link = (...args: any[]) => {
// @ts-ignore
return aIAssistantManagementObservabilityRouter.link(...args);
};
return useMemo<StatefulObservabilityAIAssistantRouter>(
() => ({
...aIAssistantManagementObservabilityRouter,
push: (...args) => {
const next = link(...args);
history.push(next);
},
replace: (path, ...args) => {
const next = link(path, ...args);
history.replace(next);
},
link: (path, ...args) => {
return http.basePath.prepend(
'/app/management/aiAssistantManagementObservability' + link(path, ...args)
);
},
}),
[http, history]
);
}

View file

@ -0,0 +1,18 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { AiAssistantManagementObservabilityPlugin as AiAssistantManagementObservabilityPlugin } from './plugin';
export type {
AiAssistantManagementObservabilityPluginSetup,
AiAssistantManagementObservabilityPluginStart,
} from './plugin';
export function plugin() {
return new AiAssistantManagementObservabilityPlugin();
}

View file

@ -0,0 +1,90 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { CoreSetup, Plugin } from '@kbn/core/public';
import { ManagementSetup } from '@kbn/management-plugin/public';
import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { ServerlessPluginStart } from '@kbn/serverless/public';
import type {
ObservabilityAIAssistantPluginSetup,
ObservabilityAIAssistantPluginStart,
} from '@kbn/observability-ai-assistant-plugin/public';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AiAssistantManagementObservabilityPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AiAssistantManagementObservabilityPluginStart {}
export interface SetupDependencies {
management: ManagementSetup;
home?: HomePublicPluginSetup;
observabilityAIAssistant?: ObservabilityAIAssistantPluginSetup;
}
export interface StartDependencies {
observabilityAIAssistant?: ObservabilityAIAssistantPluginStart;
serverless?: ServerlessPluginStart;
}
export class AiAssistantManagementObservabilityPlugin
implements
Plugin<
AiAssistantManagementObservabilityPluginSetup,
AiAssistantManagementObservabilityPluginStart,
SetupDependencies,
StartDependencies
>
{
public setup(
core: CoreSetup<StartDependencies, AiAssistantManagementObservabilityPluginStart>,
{ home, management, observabilityAIAssistant }: SetupDependencies
): AiAssistantManagementObservabilityPluginSetup {
const title = i18n.translate('aiAssistantManagementObservability.app.title', {
defaultMessage: 'AI Assistant for Observability',
});
if (home) {
home.featureCatalogue.register({
id: 'ai_assistant_observability',
title,
description: i18n.translate('aiAssistantManagementObservability.app.description', {
defaultMessage: 'Manage your AI Assistant for Observability.',
}),
icon: 'sparkles',
path: '/app/management/kibana/ai-assistant/observability',
showOnHomePage: false,
category: 'admin',
});
}
if (observabilityAIAssistant) {
management.sections.section.kibana.registerApp({
id: 'aiAssistantManagementObservability',
title,
hideFromSidebar: true,
order: 1,
mount: async (mountParams) => {
const { mountManagementSection } = await import('./app');
return mountManagementSection({
core,
mountParams,
});
},
});
}
return {};
}
public start() {
return {};
}
}

View file

@ -0,0 +1,194 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButton,
EuiButtonEmpty,
EuiCode,
EuiCodeBlock,
EuiFilePicker,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiHorizontalRule,
EuiIcon,
EuiSpacer,
EuiText,
EuiTitle,
useGeneratedHtmlId,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common/types';
import { useImportKnowledgeBaseEntries } from '../../hooks/use_import_knowledge_base_entries';
import { useAppContext } from '../../hooks/use_app_context';
export function KnowledgeBaseBulkImportFlyout({ onClose }: { onClose: () => void }) {
const {
notifications: { toasts },
} = useAppContext();
const { mutateAsync, isLoading } = useImportKnowledgeBaseEntries();
const filePickerId = useGeneratedHtmlId({ prefix: 'filePicker' });
const [files, setFiles] = useState<File[]>([]);
const onChange = (file: FileList | null) => {
setFiles(file && file.length > 0 ? Array.from(file) : []);
};
const handleSubmitNewEntryClick = async () => {
let entries: Array<Omit<KnowledgeBaseEntry, '@timestamp'>> = [];
const text = await files[0].text();
const elements = text.split('\n').filter(Boolean);
try {
entries = elements.map((el) => JSON.parse(el)) as Array<
Omit<KnowledgeBaseEntry, '@timestamp'>
>;
} catch (_) {
toasts.addError(
new Error(
i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseBulkImportFlyout.errorParsingEntries.description',
{
defaultMessage: 'Error parsing JSON entries',
}
)
),
{
title: i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseBulkImportFlyout.errorParsingEntries.title',
{
defaultMessage: 'Something went wrong',
}
),
}
);
}
mutateAsync({ entries }).then(onClose);
};
return (
<EuiFlyout onClose={onClose} data-test-subj="knowledgeBaseBulkImportFlyout">
<EuiFlyoutHeader hasBorder>
<EuiTitle>
<h2>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseBulkImportFlyout.h2.bulkImportLabel',
{ defaultMessage: 'Import files' }
)}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="addDataApp" size="xl" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseBulkImportFlyout.addFilesToEnrichTitleLabel',
{ defaultMessage: 'Add files to enrich your Knowledge base' }
)}
</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiText size="s">
<FormattedMessage
id="aiAssistantManagementObservability.knowledgeBaseBulkImportFlyout.uploadAJSONFileTextLabel"
defaultMessage="Upload a newline delimited JSON ({ext}) file containing a list of entries to add to your Knowledge base."
values={{
ext: <EuiCode language="html">.ndjson</EuiCode>,
}}
/>
</EuiText>
<EuiSpacer size="m" />
<EuiText size="s">
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseBulkImportFlyout.theObjectsShouldBeTextLabel',
{ defaultMessage: 'The objects should be of the following format:' }
)}
</EuiText>
<EuiSpacer size="m" />
<EuiCodeBlock isCopyable paddingSize="s">
{`{
"id": "a_unique_human_readable_id",
"text": "Contents of item",
}
`}
</EuiCodeBlock>
<EuiHorizontalRule />
<EuiFilePicker
aria-label={i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseBulkImportFlyout.euiFilePicker.uploadJSONLabel',
{ defaultMessage: 'Upload JSON' }
)}
display="large"
fullWidth
id={filePickerId}
initialPromptText={i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseBulkImportFlyout.euiFilePicker.selectOrDragAndLabel',
{ defaultMessage: 'Select or drag and drop a .ndjson file' }
)}
onChange={onChange}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="knowledgeBaseBulkImportFlyoutCancelButton"
disabled={isLoading}
onClick={onClose}
>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseBulkImportFlyout.cancelButtonEmptyLabel',
{ defaultMessage: 'Cancel' }
)}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="knowledgeBaseBulkImportFlyoutSaveButton"
fill
isLoading={isLoading}
onClick={handleSubmitNewEntryClick}
>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseBulkImportFlyout.saveButtonLabel',
{ defaultMessage: 'Save' }
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
}

View file

@ -0,0 +1,125 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiBadge,
EuiBasicTable,
EuiBasicTableColumn,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { capitalize } from 'lodash';
import type { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common/types';
import moment from 'moment';
import { useDeleteKnowledgeBaseEntry } from '../../hooks/use_delete_knowledge_base_entry';
import { KnowledgeBaseEntryCategory } from '../../helpers/categorize_entries';
import { useAppContext } from '../../hooks/use_app_context';
const CATEGORY_MAP = {
lens: {
description: (
<>
<EuiText size="m">
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseCategoryFlyout.categoryMap.lensCategoryDescriptionLabel',
{
defaultMessage:
'Lens is a Kibana feature which allows the Assistant to visualize data in response to user queries. These Knowledge base items are loaded into the Knowledge base by default.',
}
)}
</EuiText>
</>
),
},
};
export function KnowledgeBaseCategoryFlyout({
category,
onClose,
}: {
category: KnowledgeBaseEntryCategory;
onClose: () => void;
}) {
const { uiSettings } = useAppContext();
const dateFormat = uiSettings.get('dateFormat');
const { mutate: deleteEntry } = useDeleteKnowledgeBaseEntry();
const columns: Array<EuiBasicTableColumn<KnowledgeBaseEntry>> = [
{
field: '@timestamp',
name: i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseCategoryFlyout.actions.dateCreated',
{
defaultMessage: 'Date created',
}
),
sortable: true,
render: (timestamp: KnowledgeBaseEntry['@timestamp']) => (
<EuiBadge color="hollow">{moment(timestamp).format(dateFormat)}</EuiBadge>
),
},
{
field: 'id',
name: i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseCategoryFlyout.actions.name',
{
defaultMessage: 'Name',
}
),
sortable: true,
width: '340px',
},
{
name: 'Actions',
actions: [
{
name: i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseCategoryFlyout.actions.delete',
{
defaultMessage: 'Delete',
}
),
description: i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseCategoryFlyout.actions.deleteDescription',
{ defaultMessage: 'Delete this entry' }
),
type: 'icon',
icon: 'trash',
onClick: ({ id }) => {
deleteEntry({ id });
},
},
],
},
];
const hasDescription =
CATEGORY_MAP[category.categoryName as unknown as keyof typeof CATEGORY_MAP]?.description;
return (
<EuiFlyout onClose={onClose} data-test-subj="knowledgeBaseCategoryFlyout">
<EuiFlyoutHeader hasBorder>
<EuiTitle>
<h2>{capitalize(category.categoryName)}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{hasDescription ? (
hasDescription
) : (
<EuiBasicTable<KnowledgeBaseEntry> columns={columns} items={category.entries ?? []} />
)}
</EuiFlyoutBody>
</EuiFlyout>
);
}

View file

@ -0,0 +1,189 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButton,
EuiButtonEmpty,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiFormRow,
EuiMarkdownEditor,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import moment from 'moment';
import type { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common/types';
import { useCreateKnowledgeBaseEntry } from '../../hooks/use_create_knowledge_base_entry';
import { useDeleteKnowledgeBaseEntry } from '../../hooks/use_delete_knowledge_base_entry';
import { useAppContext } from '../../hooks/use_app_context';
export function KnowledgeBaseEditManualEntryFlyout({
entry,
onClose,
}: {
entry?: KnowledgeBaseEntry;
onClose: () => void;
}) {
const { uiSettings } = useAppContext();
const dateFormat = uiSettings.get('dateFormat');
const { mutateAsync: createEntry, isLoading } = useCreateKnowledgeBaseEntry();
const { mutateAsync: deleteEntry, isLoading: isDeleting } = useDeleteKnowledgeBaseEntry();
const [newEntryId, setNewEntryId] = useState(entry?.id ?? '');
const [newEntryText, setNewEntryText] = useState(entry?.text ?? '');
const handleSubmitNewEntryClick = async () => {
createEntry({
entry: {
id: newEntryId,
doc_id: newEntryId,
text: newEntryText,
},
}).then(onClose);
};
const handleDelete = async () => {
await deleteEntry({ id: entry!.id });
onClose();
};
return (
<EuiFlyout onClose={onClose}>
<EuiFlyoutHeader hasBorder data-test-subj="knowledgeBaseManualEntryFlyout">
<EuiTitle>
<h2>
{!entry
? i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseNewEntryFlyout.h2.newEntryLabel',
{
defaultMessage: 'New entry',
}
)
: i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseNewEntryFlyout.h2.editEntryLabel',
{
defaultMessage: 'Edit {id}',
values: { id: entry.id },
}
)}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{!entry ? (
<EuiFormRow
fullWidth
label={i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseEditManualEntryFlyout.euiFormRow.idLabel',
{ defaultMessage: 'Name' }
)}
>
<EuiFieldText
data-test-subj="knowledgeBaseEditManualEntryFlyoutFieldText"
fullWidth
value={newEntryId}
onChange={(e) => setNewEntryId(e.target.value)}
/>
</EuiFormRow>
) : (
<EuiFlexGroup>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseEditManualEntryFlyout.createdOnTextLabel',
{ defaultMessage: 'Created on' }
)}
</EuiText>
<EuiText size="s">{moment(entry['@timestamp']).format(dateFormat)}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="knowledgeBaseEditManualEntryFlyoutDeleteEntryButton"
color="danger"
iconType="trash"
isLoading={isDeleting}
onClick={handleDelete}
>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseEditManualEntryFlyout.deleteEntryButtonLabel',
{ defaultMessage: 'Delete entry' }
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
label={i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseEditManualEntryFlyout.euiFormRow.contentsLabel',
{ defaultMessage: 'Contents' }
)}
>
<EuiMarkdownEditor
aria-label={i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseNewManualEntryFlyout.euiMarkdownEditor.observabilityAiAssistantKnowledgeBaseViewMarkdownEditorLabel',
{ defaultMessage: 'observabilityAiAssistantKnowledgeBaseViewMarkdownEditor' }
)}
height={300}
initialViewMode="editing"
readOnly={false}
placeholder={i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseEditManualEntryFlyout.euiMarkdownEditor.enterContentsLabel',
{ defaultMessage: 'Enter contents' }
)}
value={newEntryText}
onChange={(text) => setNewEntryText(text)}
/>
</EuiFormRow>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="knowledgeBaseEditManualEntryFlyoutCancelButton"
disabled={isLoading}
onClick={onClose}
>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseNewManualEntryFlyout.cancelButtonEmptyLabel',
{ defaultMessage: 'Cancel' }
)}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="knowledgeBaseEditManualEntryFlyoutSaveButton"
fill
isLoading={isLoading}
onClick={handleSubmitNewEntryClick}
>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseNewManualEntryFlyout.saveButtonLabel',
{ defaultMessage: 'Save' }
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
}

View file

@ -0,0 +1,180 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { fireEvent } from '@testing-library/react';
import { render } from '../../helpers/test_helper';
import { useCreateKnowledgeBaseEntry } from '../../hooks/use_create_knowledge_base_entry';
import { useDeleteKnowledgeBaseEntry } from '../../hooks/use_delete_knowledge_base_entry';
import { useGetKnowledgeBaseEntries } from '../../hooks/use_get_knowledge_base_entries';
import { useImportKnowledgeBaseEntries } from '../../hooks/use_import_knowledge_base_entries';
import { KnowledgeBaseTab } from './knowledge_base_tab';
jest.mock('../../hooks/use_get_knowledge_base_entries');
jest.mock('../../hooks/use_create_knowledge_base_entry');
jest.mock('../../hooks/use_import_knowledge_base_entries');
jest.mock('../../hooks/use_delete_knowledge_base_entry');
const useGetKnowledgeBaseEntriesMock = useGetKnowledgeBaseEntries as jest.Mock;
const useCreateKnowledgeBaseEntryMock = useCreateKnowledgeBaseEntry as jest.Mock;
const useImportKnowledgeBaseEntriesMock = useImportKnowledgeBaseEntries as jest.Mock;
const useDeleteKnowledgeBaseEntryMock = useDeleteKnowledgeBaseEntry as jest.Mock;
const createMock = jest.fn(() => Promise.resolve());
const importMock = jest.fn(() => Promise.resolve());
const deleteMock = jest.fn(() => Promise.resolve());
describe('KnowledgeBaseTab', () => {
beforeEach(() => {
useGetKnowledgeBaseEntriesMock.mockReturnValue({
loading: false,
entries: [],
});
useDeleteKnowledgeBaseEntryMock.mockReturnValue({
mutateAsync: deleteMock,
isLoading: false,
});
});
it('should render a table', () => {
const { getByTestId } = render(<KnowledgeBaseTab />);
expect(getByTestId('knowledgeBaseTable')).toBeInTheDocument();
});
describe('when creating a new item', () => {
beforeEach(() => {
useCreateKnowledgeBaseEntryMock.mockReturnValue({
mutateAsync: createMock,
isLoading: false,
});
});
it('should render a manual import flyout', () => {
const { getByTestId } = render(<KnowledgeBaseTab />);
fireEvent.click(getByTestId('knowledgeBaseNewEntryButton'));
fireEvent.click(getByTestId('knowledgeBaseSingleEntryContextMenuItem'));
expect(getByTestId('knowledgeBaseManualEntryFlyout')).toBeInTheDocument();
});
it('should allow creating of an item', () => {
const { getByTestId } = render(<KnowledgeBaseTab />);
fireEvent.click(getByTestId('knowledgeBaseNewEntryButton'));
fireEvent.click(getByTestId('knowledgeBaseSingleEntryContextMenuItem'));
fireEvent.click(getByTestId('knowledgeBaseEditManualEntryFlyoutFieldText'));
fireEvent.change(getByTestId('knowledgeBaseEditManualEntryFlyoutFieldText'), {
target: { value: 'foo' },
});
getByTestId('knowledgeBaseEditManualEntryFlyoutSaveButton').click();
expect(createMock).toHaveBeenCalledWith({ entry: { id: 'foo', doc_id: 'foo', text: '' } });
});
});
describe('when importing a file', () => {
beforeEach(() => {
useImportKnowledgeBaseEntriesMock.mockReturnValue({
mutateAsync: importMock,
isLoading: false,
});
});
it('should render an import flyout', () => {
const { getByTestId } = render(<KnowledgeBaseTab />);
fireEvent.click(getByTestId('knowledgeBaseNewEntryButton'));
fireEvent.click(getByTestId('knowledgeBaseBulkImportContextMenuItem'));
expect(getByTestId('knowledgeBaseBulkImportFlyout')).toBeInTheDocument();
});
});
describe('when there are entries', () => {
beforeEach(() => {
useGetKnowledgeBaseEntriesMock.mockReturnValue({
loading: false,
entries: [
{
id: 'test',
doc_id: 'test',
text: 'test',
'@timestamp': 1638340456,
labels: {},
role: 'user_entry',
},
{
id: 'test2',
doc_id: 'test2',
text: 'test',
'@timestamp': 1638340456,
labels: {
category: 'lens',
},
role: 'elastic',
},
{
id: 'test3',
doc_id: 'test3',
text: 'test',
'@timestamp': 1638340456,
labels: {
category: 'lens',
},
role: 'elastic',
},
],
});
useImportKnowledgeBaseEntriesMock.mockReturnValue({
mutateAsync: importMock,
isLoading: false,
});
useDeleteKnowledgeBaseEntryMock.mockReturnValue({
mutateAsync: deleteMock,
});
});
describe('when selecting an item', () => {
it('should render an edit flyout when clicking on an entry', () => {
const { getByTestId } = render(<KnowledgeBaseTab />);
fireEvent.click(getByTestId('knowledgeBaseTable').querySelectorAll('tbody tr')[0]);
expect(getByTestId('knowledgeBaseManualEntryFlyout')).toBeInTheDocument();
});
it('should be able to delete an item', () => {
const { getByTestId } = render(<KnowledgeBaseTab />);
fireEvent.click(getByTestId('knowledgeBaseTable').querySelectorAll('tbody tr')[0]);
fireEvent.click(getByTestId('knowledgeBaseEditManualEntryFlyoutDeleteEntryButton'));
expect(deleteMock).toHaveBeenCalledWith({ id: 'test' });
});
it('should render a category flyout when clicking on a categorized item', () => {
const { getByTestId } = render(<KnowledgeBaseTab />);
fireEvent.click(getByTestId('knowledgeBaseTable').querySelectorAll('tbody tr')[1]);
expect(getByTestId('knowledgeBaseCategoryFlyout')).toBeInTheDocument();
});
});
});
});

View file

@ -0,0 +1,343 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
Criteria,
EuiBadge,
EuiBasicTable,
EuiBasicTableColumn,
EuiButton,
EuiButtonIcon,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPopover,
EuiScreenReaderOnly,
} from '@elastic/eui';
import moment from 'moment';
import type { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common/types';
import { useAppContext } from '../../hooks/use_app_context';
import { useGetKnowledgeBaseEntries } from '../../hooks/use_get_knowledge_base_entries';
import { categorizeEntries, KnowledgeBaseEntryCategory } from '../../helpers/categorize_entries';
import { KnowledgeBaseEditManualEntryFlyout } from './knowledge_base_edit_manual_entry_flyout';
import { KnowledgeBaseCategoryFlyout } from './knowledge_base_category_flyout';
import { KnowledgeBaseBulkImportFlyout } from './knowledge_base_bulk_import_flyout';
export function KnowledgeBaseTab() {
const { uiSettings } = useAppContext();
const dateFormat = uiSettings.get('dateFormat');
const columns: Array<EuiBasicTableColumn<KnowledgeBaseEntryCategory>> = [
{
align: 'right',
width: '40px',
isExpander: true,
name: (
<EuiScreenReaderOnly>
<span>
{i18n.translate('aiAssistantManagementObservability.span.expandRowLabel', {
defaultMessage: 'Expand row',
})}
</span>
</EuiScreenReaderOnly>
),
render: (category: KnowledgeBaseEntryCategory) => {
return (
<EuiButtonIcon
data-test-subj="pluginsColumnsButton"
onClick={() => setSelectedCategory(category)}
aria-label={
category.categoryName === selectedCategory?.categoryName ? 'Collapse' : 'Expand'
}
iconType={
category.categoryName === selectedCategory?.categoryName ? 'minimize' : 'expand'
}
/>
);
},
},
{
field: '',
name: '',
render: (category: KnowledgeBaseEntryCategory) => {
if (category.entries.length === 1 && category.entries[0].role === 'user_entry') {
return <EuiIcon type="documentation" color="primary" />;
}
if (
category.entries.length === 1 &&
category.entries[0].role === 'assistant_summarization'
) {
return <EuiIcon type="sparkles" color="primary" />;
}
return <EuiIcon type="logoElastic" />;
},
width: '40px',
},
{
field: 'categoryName',
name: i18n.translate('aiAssistantManagementObservability.kbTab.columns.name', {
defaultMessage: 'Name',
}),
sortable: true,
},
{
name: i18n.translate('aiAssistantManagementObservability.kbTab.columns.numberOfEntries', {
defaultMessage: 'Number of entries',
}),
width: '140px',
render: (category: KnowledgeBaseEntryCategory) => {
if (category.entries.length > 1 && category.entries[0].role === 'elastic') {
return <EuiBadge>{category.entries.length}</EuiBadge>;
}
return null;
},
},
{
field: '@timestamp',
name: i18n.translate('aiAssistantManagementObservability.kbTab.columns.dateCreated', {
defaultMessage: 'Date created',
}),
width: '140px',
sortable: true,
render: (timestamp: KnowledgeBaseEntry['@timestamp']) => (
<EuiBadge color="hollow">{moment(timestamp).format(dateFormat)}</EuiBadge>
),
},
{
name: i18n.translate('aiAssistantManagementObservability.kbTab.columns.type', {
defaultMessage: 'Type',
}),
width: '140px',
render: (category: KnowledgeBaseEntryCategory) => {
if (category.entries.length === 1 && category.entries[0].role === 'user_entry') {
return (
<EuiBadge color="hollow">
{i18n.translate('aiAssistantManagementObservability.kbTab.columns.manualBadgeLabel', {
defaultMessage: 'Manual',
})}
</EuiBadge>
);
}
if (
category.entries.length === 1 &&
category.entries[0].role === 'assistant_summarization'
) {
return (
<EuiBadge color="hollow">
{i18n.translate(
'aiAssistantManagementObservability.kbTab.columns.assistantSummarization',
{
defaultMessage: 'Assistant',
}
)}
</EuiBadge>
);
}
return (
<EuiBadge>
{i18n.translate('aiAssistantManagementObservability.columns.systemBadgeLabel', {
defaultMessage: 'System',
})}
</EuiBadge>
);
},
},
];
const [selectedCategory, setSelectedCategory] = useState<
KnowledgeBaseEntryCategory | undefined
>();
const [flyoutOpenType, setFlyoutOpenType] = useState<
'singleEntry' | 'bulkImport' | 'category' | undefined
>();
const [newEntryPopoverOpen, setNewEntryPopoverOpen] = useState(false);
const [query, setQuery] = useState('');
const [sortBy, setSortBy] = useState<'doc_id' | '@timestamp'>('doc_id');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const {
entries = [],
isLoading,
refetch,
} = useGetKnowledgeBaseEntries({ query, sortBy, sortDirection });
const categories = categorizeEntries({ entries });
const handleChangeSort = ({
sort,
}: Criteria<KnowledgeBaseEntryCategory & KnowledgeBaseEntry>) => {
if (sort) {
const { field, direction } = sort;
if (field === '@timestamp') {
setSortBy(field);
}
if (field === 'categoryName') {
setSortBy('doc_id');
}
setSortDirection(direction);
}
};
const handleClickNewEntry = () => {
setNewEntryPopoverOpen(true);
};
const handleChangeQuery = (e: React.ChangeEvent<HTMLInputElement> | undefined) => {
setQuery(e?.currentTarget.value || '');
};
return (
<>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow>
<EuiFieldSearch
data-test-subj="knowledgeBaseTabFieldSearch"
fullWidth
placeholder={i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseTab.euiFieldSearch.searchThisLabel',
{ defaultMessage: 'Search for an entry' }
)}
value={query}
onChange={handleChangeQuery}
isClearable
aria-label={i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseTab.euiFieldSearch.searchEntriesLabel',
{ defaultMessage: 'Search entries' }
)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="knowledgeBaseTabReloadButton"
color="success"
iconType="refresh"
onClick={() => refetch()}
>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseTab.reloadButtonLabel',
{ defaultMessage: 'Reload' }
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
isOpen={newEntryPopoverOpen}
closePopover={() => setNewEntryPopoverOpen(false)}
button={
<EuiButton
fill
data-test-subj="knowledgeBaseNewEntryButton"
iconSide="right"
iconType="arrowDown"
onClick={handleClickNewEntry}
>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseTab.newEntryButtonLabel',
{
defaultMessage: 'New entry',
}
)}
</EuiButton>
}
>
<EuiContextMenuPanel
size="s"
items={[
<EuiContextMenuItem
key="singleEntry"
icon="document"
data-test-subj="knowledgeBaseSingleEntryContextMenuItem"
onClick={() => {
setNewEntryPopoverOpen(false);
setFlyoutOpenType('singleEntry');
}}
size="s"
>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseTab.singleEntryContextMenuItemLabel',
{ defaultMessage: 'Single entry' }
)}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="bulkImport"
icon="documents"
data-test-subj="knowledgeBaseBulkImportContextMenuItem"
onClick={() => {
setNewEntryPopoverOpen(false);
setFlyoutOpenType('bulkImport');
}}
>
{i18n.translate(
'aiAssistantManagementObservability.knowledgeBaseTab.bulkImportContextMenuItemLabel',
{ defaultMessage: 'Bulk import' }
)}
</EuiContextMenuItem>,
]}
/>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBasicTable<KnowledgeBaseEntryCategory>
data-test-subj="knowledgeBaseTable"
columns={columns}
items={categories}
loading={isLoading}
sorting={{
sort: {
field: sortBy === 'doc_id' ? 'categoryName' : sortBy,
direction: sortDirection,
},
}}
rowProps={(row) => ({
onClick: () => setSelectedCategory(row),
})}
onChange={handleChangeSort}
/>
</EuiFlexItem>
</EuiFlexGroup>
{flyoutOpenType === 'singleEntry' ? (
<KnowledgeBaseEditManualEntryFlyout onClose={() => setFlyoutOpenType(undefined)} />
) : null}
{flyoutOpenType === 'bulkImport' ? (
<KnowledgeBaseBulkImportFlyout onClose={() => setFlyoutOpenType(undefined)} />
) : null}
{selectedCategory ? (
selectedCategory.entries.length === 1 &&
(selectedCategory.entries[0].role === 'user_entry' ||
selectedCategory.entries[0].role === 'assistant_summarization') ? (
<KnowledgeBaseEditManualEntryFlyout
entry={selectedCategory.entries[0]}
onClose={() => setSelectedCategory(undefined)}
/>
) : (
<KnowledgeBaseCategoryFlyout
category={selectedCategory}
onClose={() => setSelectedCategory(undefined)}
/>
)
) : null}
</>
);
}

View file

@ -0,0 +1,31 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { ReactNode } from 'react';
import type { CoreStart } from '@kbn/core/public';
export function RedirectToHomeIfUnauthorized({
coreStart,
children,
}: {
coreStart: CoreStart;
children: ReactNode;
}) {
const {
application: { capabilities, navigateToApp },
} = coreStart;
const allowed =
(capabilities?.management && capabilities?.observabilityAIAssistant?.show) ?? false;
if (!allowed) {
navigateToApp('home');
return null;
}
return <>{children}</>;
}

View file

@ -0,0 +1,58 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { useAppContext } from '../../hooks/use_app_context';
import { coreStart, render } from '../../helpers/test_helper';
import { SettingsPage } from './settings_page';
jest.mock('../../hooks/use_app_context');
const useAppContextMock = useAppContext as jest.Mock;
const setBreadcrumbs = jest.fn();
const navigateToApp = jest.fn();
describe('Settings Page', () => {
beforeEach(() => {
useAppContextMock.mockReturnValue({
observabilityAIAssistant: {
useGenAIConnectors: () => ({ connectors: [] }),
},
setBreadcrumbs,
application: { navigateToApp },
});
});
it('should navigate to home when not authorized', () => {
render(<SettingsPage />, { show: false });
expect(coreStart.application.navigateToApp).toBeCalledWith('home');
});
it('should render settings and knowledge base tabs', () => {
const { getByTestId } = render(<SettingsPage />);
expect(getByTestId('settingsPageTab-settings')).toBeInTheDocument();
expect(getByTestId('settingsPageTab-knowledge_base')).toBeInTheDocument();
});
it('should set breadcrumbs', () => {
render(<SettingsPage />);
expect(setBreadcrumbs).toHaveBeenCalledWith([
{
text: 'AI Assistants',
onClick: expect.any(Function),
},
{
text: 'Observability',
},
]);
});
});

View file

@ -0,0 +1,123 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui';
import { useAppContext } from '../../hooks/use_app_context';
import { SettingsTab } from './settings_tab';
import { KnowledgeBaseTab } from './knowledge_base_tab';
import { useObservabilityAIAssistantManagementRouterParams } from '../../hooks/use_observability_management_params';
import { useObservabilityAIAssistantManagementRouter } from '../../hooks/use_observability_management_router';
import type { TabsRt } from '../config';
export function SettingsPage() {
const {
application: { navigateToApp },
serverless,
setBreadcrumbs,
} = useAppContext();
const router = useObservabilityAIAssistantManagementRouter();
const {
query: { tab },
} = useObservabilityAIAssistantManagementRouterParams('/');
useEffect(() => {
if (serverless) {
serverless.setBreadcrumbs([
{
text: i18n.translate(
'aiAssistantManagementObservability.breadcrumb.serverless.observability',
{
defaultMessage: 'AI Assistant for Observability Settings',
}
),
},
]);
} else {
setBreadcrumbs([
{
text: i18n.translate('aiAssistantManagementObservability.breadcrumb.index', {
defaultMessage: 'AI Assistants',
}),
onClick: (e) => {
e.preventDefault();
navigateToApp('management', { path: '/kibana/aiAssistantManagementSelection' });
},
},
{
text: i18n.translate('aiAssistantManagementObservability.breadcrumb.observability', {
defaultMessage: 'Observability',
}),
},
]);
}
}, [navigateToApp, serverless, setBreadcrumbs]);
const tabs: Array<{ id: TabsRt; name: string; content: JSX.Element }> = [
{
id: 'settings',
name: i18n.translate('aiAssistantManagementObservability.settingsPage.settingsLabel', {
defaultMessage: 'Settings',
}),
content: <SettingsTab />,
},
{
id: 'knowledge_base',
name: i18n.translate('aiAssistantManagementObservability.settingsPage.knowledgeBaseLabel', {
defaultMessage: 'Knowledge base',
}),
content: <KnowledgeBaseTab />,
},
];
const [selectedTabId, setSelectedTabId] = useState<TabsRt>(
tab ? tabs.find((t) => t.id === tab)?.id : tabs[0].id
);
const selectedTabContent = tabs.find((obj) => obj.id === selectedTabId)?.content;
const onSelectedTabChanged = (id: TabsRt) => {
setSelectedTabId(id);
router.push('/', { path: '/', query: { tab: id } });
};
return (
<>
<EuiTitle size="l">
<h2>
{i18n.translate('aiAssistantManagementObservability.settingsPage.h2.settingsLabel', {
defaultMessage: 'Settings',
})}
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiTabs data-test-subj="settingsPageTabs">
{tabs.map((t, index) => (
<EuiTab
key={index}
data-test-subj={`settingsPageTab-${t.id}`}
onClick={() => onSelectedTabChanged(t.id)}
isSelected={t.id === selectedTabId}
>
{t.name}
</EuiTab>
))}
</EuiTabs>
<EuiSpacer size="l" />
{selectedTabContent}
<EuiSpacer size="l" />
</>
);
}

View file

@ -0,0 +1,66 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { fireEvent } from '@testing-library/react';
import { render } from '../../helpers/test_helper';
import { useAppContext } from '../../hooks/use_app_context';
import { SettingsTab } from './settings_tab';
jest.mock('../../hooks/use_app_context');
const useAppContextMock = useAppContext as jest.Mock;
const navigateToAppMock = jest.fn(() => Promise.resolve());
const selectConnectorMock = jest.fn();
describe('SettingsTab', () => {
beforeEach(() => {
useAppContextMock.mockReturnValue({
application: { navigateToApp: navigateToAppMock },
observabilityAIAssistant: {
useGenAIConnectors: () => ({
connectors: [
{ name: 'openAi', id: 'openAi' },
{ name: 'azureOpenAi', id: 'azureOpenAi' },
{ name: 'bedrock', id: 'bedrock' },
],
selectConnector: selectConnectorMock,
}),
},
});
});
it('should offer a way to configure Observability AI Assistant visibility in apps', () => {
const { getByTestId } = render(<SettingsTab />);
fireEvent.click(getByTestId('settingsTabGoToSpacesButton'));
expect(navigateToAppMock).toBeCalledWith('management', { path: '/kibana/spaces' });
});
it('should offer a way to configure Gen AI connectors', () => {
const { getByTestId } = render(<SettingsTab />);
fireEvent.click(getByTestId('settingsTabGoToConnectorsButton'));
expect(navigateToAppMock).toBeCalledWith('management', {
path: '/insightsAndAlerting/triggersActionsConnectors/connectors',
});
});
it('should allow selection of a configured Observability AI Assistant connector', () => {
const { getByTestId } = render(<SettingsTab />);
fireEvent.change(getByTestId('settingsTabGenAIConnectorSelect'), {
target: { value: 'bedrock' },
});
expect(selectConnectorMock).toBeCalledWith('bedrock');
});
});

View file

@ -0,0 +1,187 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import {
EuiButton,
EuiDescribedFormGroup,
EuiForm,
EuiFormRow,
EuiPanel,
EuiSelect,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useAppContext } from '../../hooks/use_app_context';
export const SELECTED_CONNECTOR_LOCAL_STORAGE_KEY =
'xpack.observabilityAiAssistant.lastUsedConnector';
export function SettingsTab() {
const {
application: { navigateToApp },
observabilityAIAssistant,
} = useAppContext();
const {
connectors = [],
selectedConnector,
selectConnector,
} = observabilityAIAssistant.useGenAIConnectors();
const selectorOptions = connectors.map((connector) => ({
text: connector.name,
value: connector.id,
}));
const handleNavigateToConnectors = () => {
navigateToApp('management', {
path: '/insightsAndAlerting/triggersActionsConnectors/connectors',
});
};
const handleNavigateToSpacesConfiguration = () => {
navigateToApp('management', {
path: '/kibana/spaces',
});
};
return (
<>
<EuiPanel hasBorder grow={false}>
<EuiForm component="form">
<EuiDescribedFormGroup
fullWidth
title={
<h3>
{i18n.translate(
'aiAssistantManagementObservability.settingsPage.showAIAssistantButtonLabel',
{
defaultMessage:
'Show AI Assistant button and Contextual Insights in Observability apps',
}
)}
</h3>
}
description={
<p>
{i18n.translate(
'aiAssistantManagementObservability.settingsPage.showAIAssistantDescriptionLabel',
{
defaultMessage:
'Toggle the AI Assistant button and Contextual Insights on or off in Observability apps by checking or unchecking the AI Assistant feature in Spaces > <your space> > Features.',
}
)}
</p>
}
>
<EuiFormRow fullWidth>
<div css={{ textAlign: 'right' }}>
<EuiButton
data-test-subj="settingsTabGoToSpacesButton"
onClick={handleNavigateToSpacesConfiguration}
>
{i18n.translate(
'aiAssistantManagementObservability.settingsPage.goToFeatureControlsButtonLabel',
{ defaultMessage: 'Go to Spaces' }
)}
</EuiButton>
</div>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiForm>
</EuiPanel>
<EuiSpacer size="l" />
<EuiPanel hasBorder grow={false}>
<EuiForm component="form">
<EuiDescribedFormGroup
fullWidth
title={
<h3>
{i18n.translate(
'aiAssistantManagementObservability.settingsPage.connectorSettingsLabel',
{
defaultMessage: 'Connector settings',
}
)}
</h3>
}
description={i18n.translate(
'aiAssistantManagementObservability.settingsPage.euiDescribedFormGroup.inOrderToUseLabel',
{
defaultMessage:
'In order to use the Observability AI Assistant you must set up a Generative AI connector.',
}
)}
>
<EuiFormRow fullWidth>
<div css={{ textAlign: 'right' }}>
<EuiButton
data-test-subj="settingsTabGoToConnectorsButton"
onClick={handleNavigateToConnectors}
>
{i18n.translate(
'aiAssistantManagementObservability.settingsPage.goToConnectorsButtonLabel',
{
defaultMessage: 'Manage connectors',
}
)}
</EuiButton>
</div>
</EuiFormRow>
</EuiDescribedFormGroup>
<EuiDescribedFormGroup
fullWidth
title={
<h3>
{i18n.translate(
'aiAssistantManagementObservability.settingsPage.h4.selectDefaultConnectorLabel',
{ defaultMessage: 'Default connector' }
)}
</h3>
}
description={i18n.translate(
'aiAssistantManagementObservability.settingsPage.connectYourElasticAITextLabel',
{
defaultMessage:
'Select the Generative AI connector you want to use as the default for the Observability AI Assistant.',
}
)}
>
<EuiFormRow
fullWidth
label={i18n.translate(
'aiAssistantManagementObservability.settingsPage.selectConnectorLabel',
{
defaultMessage: 'Select connector',
}
)}
>
<EuiSelect
data-test-subj="settingsTabGenAIConnectorSelect"
id="generativeAIProvider"
options={selectorOptions}
value={selectedConnector}
onChange={(e) => {
selectConnector(e.target.value);
}}
aria-label={i18n.translate(
'aiAssistantManagementObservability.settingsPage.euiSelect.generativeAIProviderLabel',
{ defaultMessage: 'Generative AI provider' }
)}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiForm>
</EuiPanel>
</>
);
}

View file

@ -0,0 +1,36 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import * as t from 'io-ts';
import { createRouter } from '@kbn/typed-react-router-config';
import { SettingsPage } from './components/settings_page';
const Tabs = t.union([t.literal('settings'), t.literal('knowledge_base'), t.undefined]);
export type TabsRt = t.TypeOf<typeof Tabs>;
const aIAssistantManagementObservabilityRoutes = {
'/': {
element: <SettingsPage />,
params: t.type({
query: t.partial({
tab: Tabs,
}),
}),
},
};
export type AIAssistantManagementObservabilityRoutes =
typeof aIAssistantManagementObservabilityRoutes;
export const aIAssistantManagementObservabilityRouter = createRouter(
aIAssistantManagementObservabilityRoutes
);
export type AIAssistantManagementObservabilityRouter =
typeof aIAssistantManagementObservabilityRouter;

View file

@ -0,0 +1,21 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types"
},
"include": ["public/**/*"],
"kbn_references": [
"@kbn/core",
"@kbn/home-plugin",
"@kbn/kibana-react-plugin",
"@kbn/management-plugin",
"@kbn/i18n",
"@kbn/i18n-react",
"@kbn/typed-react-router-config",
"@kbn/core-chrome-browser",
"@kbn/observability-ai-assistant-plugin",
"@kbn/serverless",
"@kbn/translations-plugin"
],
"exclude": ["target/**/*"]
}

View file

@ -0,0 +1,3 @@
# `aiAssistantManagementSelection` plugin
The `aiAssistantManagementSelection` plugin manages the `Ai Assistant` management section.

View file

@ -0,0 +1,19 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/src/plugins/ai_assistant_management/selection'],
coverageDirectory:
'<rootDir>/target/kibana-coverage/jest/src/plugins/ai_assistant_management/selection',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/src/plugins/ai_assistant_management/selection/{common,public,server}/**/*.{ts,tsx}',
],
};

View file

@ -0,0 +1,13 @@
{
"type": "plugin",
"id": "@kbn/ai-assistant-management-plugin",
"owner": "@elastic/obs-knowledge-team",
"plugin": {
"id": "aiAssistantManagementSelection",
"server": false,
"browser": true,
"requiredPlugins": ["management"],
"optionalPlugins": ["home", "serverless"],
"requiredBundles": ["kibanaReact"]
}
}

View file

@ -0,0 +1,39 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { createContext, useContext } from 'react';
import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser';
import type { CoreStart } from '@kbn/core/public';
import type { StartDependencies } from './plugin';
interface ContextValue extends StartDependencies {
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
capabilities: CoreStart['application']['capabilities'];
navigateToApp: CoreStart['application']['navigateToApp'];
}
const AppContext = createContext<ContextValue>(null as any);
export const AppContextProvider = ({
children,
value,
}: {
value: ContextValue;
children: React.ReactNode;
}) => {
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
export const useAppContext = () => {
const ctx = useContext(AppContext);
if (!ctx) {
throw new Error('"useAppContext" can only be called inside of AppContext.Provider!');
}
return ctx;
};

View file

@ -0,0 +1,18 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { AiAssistantManagementPlugin } from './plugin';
export type {
AiAssistantManagementSelectionPluginSetup,
AiAssistantManagementSelectionPluginStart,
} from './plugin';
export function plugin() {
return new AiAssistantManagementPlugin();
}

View file

@ -0,0 +1,65 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import { I18nProvider } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { CoreSetup } from '@kbn/core/public';
import { wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
import { StartDependencies, AiAssistantManagementSelectionPluginStart } from '../plugin';
import { aIAssistantManagementSelectionRouter } from '../routes/config';
import { RedirectToHomeIfUnauthorized } from '../routes/components/redirect_to_home_if_unauthorized';
import { AppContextProvider } from '../app_context';
interface MountParams {
core: CoreSetup<StartDependencies, AiAssistantManagementSelectionPluginStart>;
mountParams: ManagementAppMountParams;
}
export const mountManagementSection = async ({ core, mountParams }: MountParams) => {
const [coreStart, startDeps] = await core.getStartServices();
const { element, history, setBreadcrumbs } = mountParams;
const { theme$ } = core.theme;
coreStart.chrome.docTitle.change(
i18n.translate('aiAssistantManagementSelection.app.titleBar', {
defaultMessage: 'AI Assistants',
})
);
ReactDOM.render(
wrapWithTheme(
<RedirectToHomeIfUnauthorized coreStart={coreStart}>
<I18nProvider>
<AppContextProvider
value={{
...startDeps,
capabilities: coreStart.application.capabilities,
navigateToApp: coreStart.application.navigateToApp,
setBreadcrumbs,
}}
>
<RouterProvider history={history} router={aIAssistantManagementSelectionRouter as any}>
<RouteRenderer />
</RouterProvider>
</AppContextProvider>
</I18nProvider>
</RedirectToHomeIfUnauthorized>,
theme$
),
element
);
return () => {
coreStart.chrome.docTitle.reset();
ReactDOM.unmountComponentAtNode(element);
};
};

View file

@ -0,0 +1,83 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { CoreSetup, Plugin } from '@kbn/core/public';
import { ManagementSetup } from '@kbn/management-plugin/public';
import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { ServerlessPluginSetup } from '@kbn/serverless/public';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AiAssistantManagementSelectionPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AiAssistantManagementSelectionPluginStart {}
export interface SetupDependencies {
management: ManagementSetup;
home?: HomePublicPluginSetup;
serverless?: ServerlessPluginSetup;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface StartDependencies {}
export class AiAssistantManagementPlugin
implements
Plugin<
AiAssistantManagementSelectionPluginSetup,
AiAssistantManagementSelectionPluginStart,
SetupDependencies,
StartDependencies
>
{
public setup(
core: CoreSetup<StartDependencies, AiAssistantManagementSelectionPluginStart>,
{ home, management, serverless }: SetupDependencies
): AiAssistantManagementSelectionPluginSetup {
if (serverless) return {};
if (home) {
home.featureCatalogue.register({
id: 'ai_assistant',
title: i18n.translate('aiAssistantManagementSelection.app.title', {
defaultMessage: 'AI Assistants',
}),
description: i18n.translate('aiAssistantManagementSelection.app.description', {
defaultMessage: 'Manage your AI Assistants.',
}),
icon: 'sparkles',
path: '/app/management/kibana/ai-assistant',
showOnHomePage: false,
category: 'admin',
});
}
management.sections.section.kibana.registerApp({
id: 'aiAssistantManagementSelection',
title: i18n.translate('aiAssistantManagementSelection.managementSectionLabel', {
defaultMessage: 'AI Assistants',
}),
order: 1,
mount: async (mountParams) => {
const { mountManagementSection } = await import('./management_section/mount_section');
return mountManagementSection({
core,
mountParams,
});
},
});
return {};
}
public start() {
return {};
}
}

View file

@ -0,0 +1,119 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiCallOut,
EuiCard,
EuiFlexGrid,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { useAppContext } from '../../app_context';
export function AiAssistantSelectionPage() {
const { capabilities, setBreadcrumbs, navigateToApp } = useAppContext();
const observabilityAIAssistantEnabled = capabilities.observabilityAIAssistant.show;
useEffect(() => {
setBreadcrumbs([
{
text: i18n.translate('aiAssistantManagementSelection.breadcrumb.index', {
defaultMessage: 'AI Assistant',
}),
},
]);
}, [setBreadcrumbs]);
return (
<>
<EuiTitle size="l">
<h2>
{i18n.translate(
'aiAssistantManagementSelection.aiAssistantSettingsPage.h2.aIAssistantLabel',
{
defaultMessage: 'AI Assistant',
}
)}
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiText>
{i18n.translate(
'aiAssistantManagementSelection.aiAssistantSettingsPage.descriptionTextLabel',
{
defaultMessage:
'AI Assistants use generative AI to help your team by explaining errors, suggesting remediation, and helping you request, analyze, and visualize your data.',
}
)}
</EuiText>
<EuiSpacer size="l" />
<EuiFlexGrid columns={2}>
<EuiFlexItem grow>
<EuiCard
description={
<div>
{!observabilityAIAssistantEnabled ? (
<>
<EuiSpacer size="s" />
<EuiCallOut
iconType="warning"
title={i18n.translate(
'aiAssistantManagementSelection.aiAssistantSelectionPage.thisFeatureIsDisabledCallOutLabel',
{
defaultMessage:
'This feature is disabled. It can be enabled from Spaces > Features.',
}
)}
size="s"
/>
<EuiSpacer size="s" />
</>
) : null}
<EuiLink
data-test-subj="pluginsAiAssistantSelectionPageDocumentationLink"
external
target="_blank"
href="https://www.elastic.co/guide/en/observability/current/obs-ai-assistant.html"
>
{i18n.translate(
'aiAssistantManagementSelection.aiAssistantSettingsPage.obsAssistant.documentationLinkLabel',
{ defaultMessage: 'Documentation' }
)}
</EuiLink>
</div>
}
display="plain"
hasBorder
icon={<EuiIcon size="l" type="logoObservability" />}
isDisabled={!observabilityAIAssistantEnabled}
layout="horizontal"
title={i18n.translate(
'aiAssistantManagementSelection.aiAssistantSelectionPage.observabilityLabel',
{ defaultMessage: 'Elastic AI Assistant for Observability' }
)}
titleSize="xs"
onClick={() =>
navigateToApp('management', { path: 'kibana/aiAssistantManagementObservability' })
}
/>
</EuiFlexItem>
</EuiFlexGrid>
</>
);
}

View file

@ -0,0 +1,30 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { ReactNode } from 'react';
import type { CoreStart } from '@kbn/core/public';
export function RedirectToHomeIfUnauthorized({
coreStart,
children,
}: {
coreStart: CoreStart;
children: ReactNode;
}) {
const {
application: { capabilities, navigateToApp },
} = coreStart;
const allowed = capabilities?.management ?? false;
if (!allowed) {
navigateToApp('home');
return null;
}
return <>{children}</>;
}

View file

@ -0,0 +1,29 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { createRouter } from '@kbn/typed-react-router-config';
import { AiAssistantSelectionPage } from './components/ai_assistant_selection_page';
/**
* The array of route definitions to be used when the application
* creates the routes.
*/
const aIAssistantManagementSelectionRoutes = {
'/': {
element: <AiAssistantSelectionPage />,
},
};
export type AIAssistantManagementSelectionRoutes = typeof aIAssistantManagementSelectionRoutes;
export const aIAssistantManagementSelectionRouter = createRouter(
aIAssistantManagementSelectionRoutes
);
export type AIAssistantManagementSelectionRouter = typeof aIAssistantManagementSelectionRouter;

View file

@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types"
},
"include": ["common/**/*", "public/**/*", "server/**/*"],
"kbn_references": [
"@kbn/core",
"@kbn/home-plugin",
"@kbn/kibana-react-plugin",
"@kbn/management-plugin",
"@kbn/i18n",
"@kbn/i18n-react",
"@kbn/core-chrome-browser",
"@kbn/typed-react-router-config",
"@kbn/serverless"
],
"exclude": ["target/**/*"]
}

View file

@ -35,12 +35,14 @@ export const managementSidebarNav = ({
const apps = sortBy(section.getAppsEnabled(), 'order');
if (apps.length) {
if (!section.hideFromSidebar) {
acc.push({
...createNavItem(section, {
items: appsToNavItems(apps),
items: appsToNavItems(apps.filter((app) => !app.hideFromSidebar)),
}),
});
}
}
return acc;
}, []);

View file

@ -82,6 +82,7 @@ export interface CreateManagementItemArgs {
order?: number;
euiIconType?: string; // takes precedence over `icon` property.
icon?: string; // URL to image file; fallback if no `euiIconType`
hideFromSidebar?: boolean;
capabilitiesId?: string; // overrides app id
redirectFrom?: string; // redirects from an old app id to the current app id
}

View file

@ -13,6 +13,7 @@ export class ManagementItem {
public readonly title: string;
public readonly tip?: string;
public readonly order: number;
public readonly hideFromSidebar?: boolean;
public readonly euiIconType?: string;
public readonly icon?: string;
public readonly capabilitiesId?: string;
@ -25,6 +26,7 @@ export class ManagementItem {
title,
tip,
order = 100,
hideFromSidebar = false,
euiIconType,
icon,
capabilitiesId,
@ -34,6 +36,7 @@ export class ManagementItem {
this.title = title;
this.tip = tip;
this.order = order;
this.hideFromSidebar = hideFromSidebar;
this.euiIconType = euiIconType;
this.icon = icon;
this.capabilitiesId = capabilitiesId;

View file

@ -16,6 +16,8 @@ export const capabilitiesProvider = () => ({
settings: true,
indexPatterns: true,
objects: true,
aiAssistantManagementSelection: true,
aiAssistantManagementObservability: true,
},
},
});

View file

@ -16,6 +16,10 @@
"@kbn/actions-types/*": ["packages/kbn-actions-types/*"],
"@kbn/advanced-settings-plugin": ["src/plugins/advanced_settings"],
"@kbn/advanced-settings-plugin/*": ["src/plugins/advanced_settings/*"],
"@kbn/ai-assistant-management-observability-plugin": ["src/plugins/ai_assistant_management/observability"],
"@kbn/ai-assistant-management-observability-plugin/*": ["src/plugins/ai_assistant_management/observability/*"],
"@kbn/ai-assistant-management-plugin": ["src/plugins/ai_assistant_management/selection"],
"@kbn/ai-assistant-management-plugin/*": ["src/plugins/ai_assistant_management/selection/*"],
"@kbn/aiops-components": ["x-pack/packages/ml/aiops_components"],
"@kbn/aiops-components/*": ["x-pack/packages/ml/aiops_components/*"],
"@kbn/aiops-plugin": ["x-pack/plugins/aiops"],

View file

@ -76,7 +76,7 @@ export function ApmAppRoot({
>
<i18nCore.Context>
<ObservabilityAIAssistantProvider
value={apmPluginContextValue.observabilityAIAssistant}
value={apmPluginContextValue.observabilityAIAssistant.service}
>
<TimeRangeIdContextProvider>
<RouterProvider history={history} router={apmRouter as any}>

View file

@ -427,7 +427,7 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
public start(core: CoreStart, plugins: ApmPluginStartDeps) {
const { fleet } = plugins;
plugins.observabilityAIAssistant.register(
plugins.observabilityAIAssistant.service.register(
async ({ signal, registerContext, registerFunction }) => {
const mod = await import('./assistant_functions');

View file

@ -13,6 +13,7 @@ import { AppMountParameters, CoreStart } from '@kbn/core/public';
import { themeServiceMock } from '@kbn/core/public/mocks';
import { ExploratoryViewPublicPluginsStart } from '../plugin';
import { renderApp } from '.';
import { mockObservabilityAIAssistantService } from '@kbn/observability-ai-assistant-plugin/public';
describe('renderApp', () => {
const originalConsole = global.console;
@ -28,7 +29,6 @@ describe('renderApp', () => {
it('renders', async () => {
const plugins = {
usageCollection: { reportUiCounter: noop },
data: {
query: {
timefilter: {
@ -42,6 +42,8 @@ describe('renderApp', () => {
},
},
},
usageCollection: { reportUiCounter: noop },
observabilityAIAssistant: { service: mockObservabilityAIAssistantService },
} as unknown as ExploratoryViewPublicPluginsStart;
const core = {

View file

@ -68,7 +68,7 @@ export const renderApp = ({
const ApplicationUsageTrackingProvider =
usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment;
const aiAssistantService = plugins.observabilityAIAssistant;
const aiAssistantService = plugins.observabilityAIAssistant.service;
ReactDOM.render(
<EuiErrorBoundary>

View file

@ -44,7 +44,10 @@ const AlertDetailsAppSection = ({
alert,
setAlertSummaryFields,
}: AlertDetailsAppSectionProps) => {
const { logsShared, observabilityAIAssistant } = useKibanaContextForPlugin().services;
const {
logsShared,
observabilityAIAssistant: { service: observabilityAIAssistantService },
} = useKibanaContextForPlugin().services;
const theme = useTheme();
const timeRange = getPaddedAlertTimeRange(alert.fields[ALERT_START]!, alert.fields[ALERT_END]);
const alertEnd = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]).valueOf() : undefined;
@ -242,7 +245,7 @@ const AlertDetailsAppSection = ({
};
return (
<ObservabilityAIAssistantProvider value={observabilityAIAssistant}>
<ObservabilityAIAssistantProvider value={observabilityAIAssistantService}>
<EuiFlexGroup direction="column" data-test-subj="logsThresholdAlertDetailsPage">
{getLogRatioChart()}
{getLogCountChart()}

View file

@ -32,7 +32,7 @@ export const CommonInfraProviders: React.FC<{
}> = ({
children,
triggersActionsUI,
observabilityAIAssistant,
observabilityAIAssistant: { service: observabilityAIAssistantService },
setHeaderActionMenu,
appName,
storage,
@ -44,7 +44,7 @@ export const CommonInfraProviders: React.FC<{
<TriggersActionsProvider triggersActionsUI={triggersActionsUI}>
<EuiThemeProvider darkMode={darkMode}>
<DataUIProviders appName={appName} storage={storage}>
<ObservabilityAIAssistantProvider value={observabilityAIAssistant}>
<ObservabilityAIAssistantProvider value={observabilityAIAssistantService}>
<HeaderActionMenuProvider setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
<NavigationWarningPromptProvider>{children}</NavigationWarningPromptProvider>
</HeaderActionMenuProvider>

View file

@ -20,9 +20,9 @@ export type LogAIAssistantComponent = ComponentType<
>;
export function createLogAIAssistant({
observabilityAIAssistant: aiAssistant,
observabilityAIAssistant: aiAssistantService,
}: LogAIAssistantFactoryDeps): LogAIAssistantComponent {
return ({ observabilityAIAssistant = aiAssistant, ...props }) => (
return ({ observabilityAIAssistant = aiAssistantService, ...props }) => (
<LogAIAssistant observabilityAIAssistant={observabilityAIAssistant} {...props} />
);
}

View file

@ -27,7 +27,7 @@ export interface LogAIAssistantProps {
}
export interface LogAIAssistantDeps extends LogAIAssistantProps {
observabilityAIAssistant: ObservabilityAIAssistantPluginStart;
observabilityAIAssistant: ObservabilityAIAssistantPluginStart['service'];
}
export const LogAIAssistant = withProviders(({ doc }: LogAIAssistantProps) => {
@ -102,11 +102,11 @@ export default LogAIAssistant;
function withProviders(Component: React.FunctionComponent<LogAIAssistantProps>) {
return function ComponentWithProviders({
observabilityAIAssistant,
observabilityAIAssistant: observabilityAIAssistantService,
...props
}: LogAIAssistantDeps) {
return (
<ObservabilityAIAssistantProvider value={observabilityAIAssistant}>
<ObservabilityAIAssistantProvider value={observabilityAIAssistantService}>
<Component {...props} />
</ObservabilityAIAssistantProvider>
);

View file

@ -101,7 +101,9 @@ export const LogEntryFlyout = ({
logViewReference,
}: LogEntryFlyoutProps) => {
const {
services: { observabilityAIAssistant },
services: {
observabilityAIAssistant: { service: observabilityAIAssistantService },
},
} = useKibanaContextForPlugin();
const {
@ -184,7 +186,10 @@ export const LogEntryFlyout = ({
>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<LogAIAssistant observabilityAIAssistant={observabilityAIAssistant} doc={logEntry} />
<LogAIAssistant
observabilityAIAssistant={observabilityAIAssistantService}
doc={logEntry}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogEntryFieldsTable logEntry={logEntry} onSetFieldFilter={onSetFieldFilter} />

View file

@ -33,7 +33,9 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
search: data.search,
});
const LogAIAssistant = createLogAIAssistant({ observabilityAIAssistant });
const LogAIAssistant = createLogAIAssistant({
observabilityAIAssistant: observabilityAIAssistant.service,
});
return {
logViews,

View file

@ -15,6 +15,7 @@ import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { ConfigSchema, ObservabilityPublicPluginsStart } from '../plugin';
import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock';
import { renderApp } from '.';
import { mockObservabilityAIAssistantService } from '@kbn/observability-ai-assistant-plugin/public';
describe('renderApp', () => {
const originalConsole = global.console;
@ -31,7 +32,6 @@ describe('renderApp', () => {
const mockSearchSessionClear = jest.fn();
const plugins = {
usageCollection: { reportUiCounter: noop },
data: {
query: {
timefilter: {
@ -50,6 +50,8 @@ describe('renderApp', () => {
},
},
},
usageCollection: { reportUiCounter: noop },
observabilityAIAssistant: { service: mockObservabilityAIAssistantService },
} as unknown as ObservabilityPublicPluginsStart;
const core = {

View file

@ -101,7 +101,7 @@ export const renderApp = ({
isServerless,
}}
>
<ObservabilityAIAssistantProvider value={plugins.observabilityAIAssistant}>
<ObservabilityAIAssistantProvider value={plugins.observabilityAIAssistant.service}>
<PluginContext.Provider
value={{
config,

View file

@ -5,5 +5,6 @@
* 2.0.
*/
export type { Message, Conversation } from './types';
export type { Message, Conversation, KnowledgeBaseEntry } from './types';
export { KnowledgeBaseEntryRole } from './types';
export { MessageRole } from './types';

View file

@ -18,6 +18,12 @@ export enum MessageRole {
Elastic = 'elastic',
}
export enum KnowledgeBaseEntryRole {
AssistantSummarization = 'assistant_summarization',
UserEntry = 'user_entry',
Elastic = 'elastic',
}
export interface PendingMessage {
message: Message['message'];
aborted?: boolean;
@ -68,10 +74,12 @@ export interface KnowledgeBaseEntry {
'@timestamp': string;
id: string;
text: string;
doc_id: string;
confidence: 'low' | 'medium' | 'high';
is_correction: boolean;
public: boolean;
labels: Record<string, string>;
role: KnowledgeBaseEntryRole;
}
export type CompatibleJSONSchema = Exclude<JSONSchema, boolean>;

View file

@ -7,51 +7,42 @@
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButton,
EuiButtonIcon,
EuiContextMenu,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiLoadingSpinner,
EuiPanel,
EuiPopover,
EuiSpacer,
EuiSwitch,
EuiText,
} from '@elastic/eui';
import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base';
import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover } from '@elastic/eui';
import { useKibana } from '../../hooks/use_kibana';
import { getSettingsHref } from '../../utils/get_settings_href';
import { getSettingsKnowledgeBaseHref } from '../../utils/get_settings_kb_href';
import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors';
import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base';
import type { StartedFrom } from '../../utils/get_timeline_items_from_conversation';
import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base';
export function ChatActionsMenu({
connectors,
connectorsManagementHref,
conversationId,
disabled,
knowledgeBase,
modelsManagementHref,
startedFrom,
onCopyConversationClick,
}: {
connectors: UseGenAIConnectorsResult;
connectorsManagementHref: string;
conversationId?: string;
disabled: boolean;
knowledgeBase: UseKnowledgeBaseResult;
modelsManagementHref: string;
startedFrom?: StartedFrom;
onCopyConversationClick: () => void;
}) {
const {
application: { navigateToUrl },
http,
} = useKibana().services;
const [isOpen, setIsOpen] = useState(false);
const toggleActionsMenu = () => {
setIsOpen(!isOpen);
};
const handleNavigateToSettings = () => {
navigateToUrl(getSettingsHref(http));
};
const handleNavigateToSettingsKnowledgeBase = () => {
navigateToUrl(getSettingsKnowledgeBaseHref(http));
};
return (
<EuiPopover
isOpen={isOpen}
@ -61,7 +52,10 @@ export function ChatActionsMenu({
disabled={disabled}
iconType="boxesVertical"
onClick={toggleActionsMenu}
aria-label="Menu"
aria-label={i18n.translate(
'xpack.observabilityAiAssistant.chatActionsMenu.euiButtonIcon.menuLabel',
{ defaultMessage: 'Menu' }
)}
/>
}
panelPaddingSize="none"
@ -93,28 +87,25 @@ export function ChatActionsMenu({
panel: 1,
},
{
name: (
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow>
{i18n.translate(
name: i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.settings', {
defaultMessage: 'AI Assistant Settings',
}),
onClick: () => {
toggleActionsMenu();
handleNavigateToSettings();
},
},
{
name: i18n.translate(
'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase',
{
defaultMessage: 'Knowledge base',
defaultMessage: 'Manage knowledge base',
}
)}
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ paddingRight: 4 }}>
{knowledgeBase.status.loading || knowledgeBase.isInstalling ? (
<EuiLoadingSpinner size="s" />
) : knowledgeBase.status.value?.ready ? (
<EuiIcon type="checkInCircleFilled" />
) : (
<EuiIcon type="dotInCircle" />
)}
</EuiFlexItem>
</EuiFlexGroup>
),
panel: 2,
onClick: () => {
toggleActionsMenu();
handleNavigateToSettingsKnowledgeBase();
},
},
{
name: i18n.translate(
@ -140,107 +131,6 @@ export function ChatActionsMenu({
content: (
<EuiPanel>
<ConnectorSelectorBase {...connectors} />
<EuiSpacer size="m" />
<EuiButton
data-test-subj="observabilityAiAssistantChatActionsMenuManageConnectorsButton"
href={connectorsManagementHref}
iconSide="right"
iconType="arrowRight"
size="s"
>
{i18n.translate(
'xpack.observabilityAiAssistant.chatHeader.actions.connectorManagement.button',
{
defaultMessage: 'Manage connectors',
}
)}
</EuiButton>
</EuiPanel>
),
},
{
id: 2,
width: 256,
title: i18n.translate(
'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.title',
{
defaultMessage: 'Knowledge base',
}
),
content: (
<EuiPanel>
<EuiText size="s">
{i18n.translate(
'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.description.paragraph',
{
defaultMessage:
'Using a knowledge base is optional but improves the experience of using the Assistant significantly.',
}
)}{' '}
<EuiLink
data-test-subj="observabilityAiAssistantChatActionsMenuLearnMoreLink"
external
target="_blank"
href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html"
>
{i18n.translate(
'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.elser.learnMore',
{
defaultMessage: 'Learn more',
}
)}
</EuiLink>
</EuiText>
<EuiSpacer size="l" />
{knowledgeBase.isInstalling || knowledgeBase.status.loading ? (
<EuiLoadingSpinner size="m" />
) : (
<>
<EuiSwitch
label={
knowledgeBase.isInstalling
? i18n.translate(
'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.switchLabel.installing',
{
defaultMessage: 'Setting up knowledge base',
}
)
: i18n.translate(
'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase.switchLabel.enable',
{
defaultMessage: 'Knowledge base installed',
}
)
}
checked={Boolean(knowledgeBase.status.value?.ready)}
disabled={
Boolean(knowledgeBase.status.value?.ready) || knowledgeBase.isInstalling
}
onChange={(e) => {
if (e.target.checked) {
knowledgeBase.install();
}
}}
/>
<EuiSpacer size="m" />
<EuiButton
data-test-subj="observabilityAiAssistantChatActionsMenuGoToMachineLearningButton"
fullWidth
href={modelsManagementHref}
size="s"
>
{i18n.translate(
'xpack.observabilityAiAssistant.chatHeader.actions.connectorManagement',
{
defaultMessage: 'Go to Machine Learning',
}
)}
</EuiButton>
</>
)}
</EuiPanel>
),
},

View file

@ -59,6 +59,7 @@ const defaultProps: ComponentStoryObj<typeof Component> = {
error: undefined,
selectedConnector: 'foo',
selectConnector: () => {},
reloadConnectors: () => {},
},
connectorsManagementHref: '',
currentUser: {

View file

@ -62,7 +62,6 @@ export function ChatBody({
connectors,
knowledgeBase,
connectorsManagementHref,
modelsManagementHref,
currentUser,
startedFrom,
onConversationUpdate,
@ -73,7 +72,6 @@ export function ChatBody({
connectors: UseGenAIConnectorsResult;
knowledgeBase: UseKnowledgeBaseResult;
connectorsManagementHref: string;
modelsManagementHref: string;
currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username'>;
startedFrom?: StartedFrom;
onConversationUpdate: (conversation: Conversation) => void;
@ -333,7 +331,6 @@ export function ChatBody({
: undefined
}
connectorsManagementHref={connectorsManagementHref}
modelsManagementHref={modelsManagementHref}
knowledgeBase={knowledgeBase}
licenseInvalid={!hasCorrectLicense && !initialConversationId}
loading={isLoading}

View file

@ -27,7 +27,7 @@ const noPanelStyle = css`
}
.euiLink {
padding: 0 8px;
padding: 0;
}
.euiLink:focus {
@ -37,10 +37,14 @@ const noPanelStyle = css`
.euiLink:hover {
text-decoration: underline;
}
`;
const avatarStyle = css`
cursor: 'pointer';
.euiAvatar {
cursor: pointer;
:hover {
border: solid 2px #d3dae6;
}
}
`;
export function ChatConsolidatedItems({
@ -71,7 +75,6 @@ export function ChatConsolidatedItems({
timelineAvatar={
<EuiAvatar
color="subdued"
css={avatarStyle}
name="inspect"
iconType="layers"
onClick={handleToggleExpand}

View file

@ -15,7 +15,6 @@ import { useKibana } from '../../hooks/use_kibana';
import { useKnowledgeBase } from '../../hooks/use_knowledge_base';
import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router';
import { getConnectorsManagementHref } from '../../utils/get_connectors_management_href';
import { getModelsManagementHref } from '../../utils/get_models_management_href';
import { StartedFrom } from '../../utils/get_timeline_items_from_conversation';
import { ChatBody } from './chat_body';
@ -100,7 +99,6 @@ export function ChatFlyout({
initialMessages={initialMessages}
currentUser={currentUser}
connectorsManagementHref={getConnectorsManagementHref(http)}
modelsManagementHref={getModelsManagementHref(http)}
knowledgeBase={knowledgeBase}
startedFrom={startedFrom}
onConversationUpdate={(conversation) => {

View file

@ -28,6 +28,7 @@ export const ChatHeaderLoaded: ComponentStoryObj<typeof Component> = {
{ id: 'gpt-3.5-turbo', name: 'OpenAI GPT-3.5 Turbo' },
] as FindActionResult[],
selectConnector: () => {},
reloadConnectors: () => {},
},
knowledgeBase: {
status: {

View file

@ -34,7 +34,6 @@ export function ChatHeader({
licenseInvalid,
connectors,
connectorsManagementHref,
modelsManagementHref,
conversationId,
knowledgeBase,
startedFrom,
@ -46,7 +45,6 @@ export function ChatHeader({
licenseInvalid: boolean;
connectors: UseGenAIConnectorsResult;
connectorsManagementHref: string;
modelsManagementHref: string;
conversationId?: string;
knowledgeBase: UseKnowledgeBaseResult;
startedFrom?: StartedFrom;
@ -103,12 +101,8 @@ export function ChatHeader({
<EuiFlexItem grow={false}>
<ChatActionsMenu
connectors={connectors}
connectorsManagementHref={connectorsManagementHref}
disabled={licenseInvalid}
modelsManagementHref={modelsManagementHref}
conversationId={conversationId}
knowledgeBase={knowledgeBase}
startedFrom={startedFrom}
onCopyConversationClick={onCopyConversation}
/>
</EuiFlexItem>

View file

@ -35,13 +35,10 @@ export interface ChatItemProps extends ChatTimelineItem {
}
const normalMessageClassName = css`
.euiCommentEvent__header {
padding: 4px 8px;
}
.euiCommentEvent__body {
padding: 0;
}
/* targets .*euiTimelineItemEvent-top, makes sure text properly wraps and doesn't overflow */
> :last-child {
overflow-x: hidden;
@ -56,6 +53,10 @@ const noPanelMessageClassName = css`
.euiCommentEvent__header {
background: transparent;
border-block-end: none;
> .euiPanel {
background: none;
}
}
.euiCommentEvent__body {
@ -89,10 +90,6 @@ export function ChatItem({
const actions = [canCopy, collapsed, canCopy].filter(Boolean);
const noBodyMessageClassName = css`
.euiCommentEvent__header {
padding: 4px 8px;
}
.euiCommentEvent__body {
padding: 0;
height: ${expanded ? 'fit-content' : '0px'};

View file

@ -31,7 +31,16 @@ export function ExperimentalFeatureBanner() {
<FormattedMessage
id="xpack.observabilityAiAssistant.experimentalFunctionBanner.title"
defaultMessage="This feature is currently in {techPreview} and may contain issues."
values={{ techPreview: <strong>Technical preview</strong> }}
values={{
techPreview: (
<strong>
{i18n.translate(
'xpack.observabilityAiAssistant.experimentalFeatureBanner.strong.technicalPreviewLabel',
{ defaultMessage: 'Technical preview' }
)}
</strong>
),
}}
/>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -5,11 +5,10 @@
* 2.0.
*/
import React from 'react';
import React, { useState } from 'react';
import {
EuiBetaBadge,
EuiButton,
EuiCallOut,
EuiCard,
EuiFlexGroup,
EuiFlexItem,
@ -19,16 +18,16 @@ import {
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base';
import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors';
import { ExperimentalFeatureBanner } from './experimental_feature_banner';
import { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base';
import { StartedFrom } from '../../utils/get_timeline_items_from_conversation';
import { useKibana } from '../../hooks/use_kibana';
export function InitialSetupPanel({
connectors,
connectorsManagementHref,
knowledgeBase,
startedFrom,
}: {
connectors: UseGenAIConnectorsResult;
@ -36,6 +35,31 @@ export function InitialSetupPanel({
knowledgeBase: UseKnowledgeBaseResult;
startedFrom?: StartedFrom;
}) {
const [connectorFlyoutOpen, setConnectorFlyoutOpen] = useState(false);
const {
application: { navigateToApp, capabilities },
triggersActionsUi: { getAddConnectorFlyout: ConnectorFlyout },
} = useKibana().services;
const handleConnectorClick = () => {
if (capabilities.management?.insightsAndAlerting?.triggersActions) {
setConnectorFlyoutOpen(true);
} else {
navigateToApp('management', {
path: '/insightsAndAlerting/triggersActionsConnectors/connectors',
});
}
};
const onConnectorCreated = (createdConnector: ActionConnector) => {
setConnectorFlyoutOpen(false);
if (createdConnector.actionTypeId === '.gen-ai') {
connectors.reloadConnectors();
}
};
return (
<>
<ExperimentalFeatureBanner />
@ -52,78 +76,6 @@ export function InitialSetupPanel({
<EuiSpacer size="l" />
<EuiFlexGroup direction={startedFrom === 'conversationView' ? 'row' : 'column'}>
<EuiFlexItem>
<EuiCard
icon={<EuiIcon type="machineLearningApp" size="xl" />}
title={i18n.translate(
'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.title',
{
defaultMessage: 'Knowledge Base',
}
)}
description={
<>
<EuiText size="s">
{i18n.translate(
'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.description.paragraph1',
{
defaultMessage:
'We recommend you enable the knowledge base for a better experience. It will provide the assistant with the ability to learn from your interaction with it.',
}
)}
</EuiText>
<EuiText size="s">
{i18n.translate(
'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.description.paragraph2',
{
defaultMessage: 'This step is optional, you can always do it later.',
}
)}
</EuiText>
</>
}
footer={
knowledgeBase.status.value?.ready ? (
<EuiCallOut
color="success"
iconType="checkInCircleFilled"
size="s"
style={{ padding: '10px 14px', display: 'inline-flex', borderRadius: '6px' }}
title={i18n.translate(
'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.buttonLabel.alreadyInstalled',
{
defaultMessage: 'Knowledge base installed',
}
)}
/>
) : (
<EuiButton
data-test-subj="observabilityAiAssistantInitialSetupPanelButton"
color={knowledgeBase.status.value?.ready ? 'success' : 'primary'}
fill
isLoading={knowledgeBase.isInstalling || knowledgeBase.status.loading}
onClick={knowledgeBase.install}
iconType="dotInCircle"
>
{knowledgeBase.isInstalling || knowledgeBase.status.loading
? i18n.translate(
'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.buttonLabel.installingKb',
{
defaultMessage: 'Installing knowledge base',
}
)
: i18n.translate(
'xpack.observabilityAiAssistant.initialSetupPanel.knowledgeBase.buttonLabel.kbNotInstalledYet',
{
defaultMessage: 'Set up knowledge base',
}
)}
</EuiButton>
)
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
icon={<EuiIcon type="devToolsApp" size="xl" />}
@ -177,9 +129,7 @@ export function InitialSetupPanel({
}
)}
</EuiText>
) : (
''
)
) : undefined
}
footer={
!connectors.connectors?.length ? (
@ -187,7 +137,7 @@ export function InitialSetupPanel({
data-test-subj="observabilityAiAssistantInitialSetupPanelSetUpGenerativeAiConnectorButton"
fill
color="primary"
href={connectorsManagementHref}
onClick={handleConnectorClick}
>
{i18n.translate(
'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel',
@ -213,6 +163,13 @@ export function InitialSetupPanel({
})}
</EuiText>
</EuiPanel>
{connectorFlyoutOpen ? (
<ConnectorFlyout
onClose={() => setConnectorFlyoutOpen(false)}
onConnectorCreated={onConnectorCreated}
/>
) : null}
</>
);
}

View file

@ -47,6 +47,7 @@ const defaultProps: InsightBaseProps = {
selectedConnector="gpt-4"
loading={false}
selectConnector={() => {}}
reloadConnectors={() => {}}
/>
),
onToggle: () => {},

View file

@ -35,7 +35,7 @@ export async function registerFunctions({
signal: AbortSignal;
}) {
return service
.callApi('GET /internal/observability_ai_assistant/functions/kb_status', {
.callApi('GET /internal/observability_ai_assistant/kb/status', {
signal,
})
.then((response) => {

View file

@ -14,5 +14,6 @@ export function useGenAIConnectors(): UseGenAIConnectorsResult {
error: undefined,
selectedConnector: 'foo',
selectConnector: (id: string) => {},
reloadConnectors: () => {},
};
}

View file

@ -38,6 +38,7 @@ const mockService: MockedService = {
getLicenseManagementLocator: jest.fn(),
isEnabled: jest.fn(),
start: jest.fn(),
register: jest.fn(),
};
const mockChatService = createMockChatService();

View file

@ -5,9 +5,10 @@
* 2.0.
*/
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { FindActionResult } from '@kbn/actions-plugin/server';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import type { ObservabilityAIAssistantService } from '../types';
import { useObservabilityAIAssistant } from './use_observability_ai_assistant';
export interface UseGenAIConnectorsResult {
@ -16,11 +17,18 @@ export interface UseGenAIConnectorsResult {
loading: boolean;
error?: Error;
selectConnector: (id: string) => void;
reloadConnectors: () => void;
}
export function useGenAIConnectors(): UseGenAIConnectorsResult {
const assistant = useObservabilityAIAssistant();
return useGenAIConnectorsWithoutContext(assistant);
}
export function useGenAIConnectorsWithoutContext(
assistant: ObservabilityAIAssistantService
): UseGenAIConnectorsResult {
const [connectors, setConnectors] = useState<FindActionResult[] | undefined>(undefined);
const [selectedConnector, setSelectedConnector] = useLocalStorage(
@ -32,11 +40,10 @@ export function useGenAIConnectors(): UseGenAIConnectorsResult {
const [error, setError] = useState<Error | undefined>(undefined);
useEffect(() => {
const controller = useMemo(() => new AbortController(), []);
const fetchConnectors = useCallback(async () => {
setLoading(true);
const controller = new AbortController();
assistant
.callApi('GET /internal/observability_ai_assistant/connectors', {
signal: controller.signal,
@ -59,11 +66,15 @@ export function useGenAIConnectors(): UseGenAIConnectorsResult {
.finally(() => {
setLoading(false);
});
}, [assistant, controller.signal, setSelectedConnector]);
useEffect(() => {
fetchConnectors();
return () => {
controller.abort();
};
}, [assistant, setSelectedConnector]);
}, [assistant, controller, fetchConnectors, setSelectedConnector]);
return {
connectors,
@ -73,5 +84,8 @@ export function useGenAIConnectors(): UseGenAIConnectorsResult {
selectConnector: (id: string) => {
setSelectedConnector(id);
},
reloadConnectors: () => {
fetchConnectors();
},
};
}

View file

@ -5,13 +5,15 @@
* 2.0.
*/
import { CoreStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ObservabilityAIAssistantPluginStartDependencies } from '../types';
import type { CoreStart } from '@kbn/core/public';
import type { ObservabilityAIAssistantPluginStartDependencies } from '../types';
export type StartServices<TAdditionalServices> = CoreStart & {
export type StartServices<TAdditionalServices> = CoreStart &
ObservabilityAIAssistantPluginStartDependencies & {
plugins: { start: ObservabilityAIAssistantPluginStartDependencies };
} & TAdditionalServices & {};
const useTypedKibana = <AdditionalServices extends object = {}>() =>
useKibana<StartServices<AdditionalServices>>();

View file

@ -29,7 +29,7 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult {
const service = useObservabilityAIAssistant();
const status = useAbortableAsync(({ signal }) => {
return service.callApi('GET /internal/observability_ai_assistant/functions/kb_status', {
return service.callApi('GET /internal/observability_ai_assistant/kb/status', {
signal,
});
}, []);
@ -45,7 +45,7 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult {
const install = (): Promise<void> => {
setIsInstalling(true);
return service
.callApi('POST /internal/observability_ai_assistant/functions/setup_kb', {
.callApi('POST /internal/observability_ai_assistant/kb/setup', {
signal: null,
})
.then(() => {

View file

@ -14,7 +14,9 @@ import type {
ObservabilityAIAssistantPluginSetupDependencies,
ObservabilityAIAssistantPluginStartDependencies,
ConfigSchema,
ObservabilityAIAssistantService,
} from './types';
export { mockService as mockObservabilityAIAssistantService } from './utils/storybook_decorator';
export const ContextualInsight = withSuspense(
lazy(() => import('./components/insight/insight').then((m) => ({ default: m.Insight })))
@ -30,15 +32,19 @@ export const ObservabilityAIAssistantActionMenuItem = withSuspense(
export { ObservabilityAIAssistantProvider } from './context/observability_ai_assistant_provider';
export type { ObservabilityAIAssistantPluginSetup, ObservabilityAIAssistantPluginStart };
export type {
ObservabilityAIAssistantPluginSetup,
ObservabilityAIAssistantPluginStart,
ObservabilityAIAssistantService,
};
export {
useObservabilityAIAssistant,
useObservabilityAIAssistantOptional,
} from './hooks/use_observability_ai_assistant';
export type { Conversation, Message } from '../common';
export { MessageRole } from '../common';
export type { Conversation, Message, KnowledgeBaseEntry } from '../common';
export { MessageRole, KnowledgeBaseEntryRole } from '../common';
export type {
ObservabilityAIAssistantAPIClientRequestParamsOf,

View file

@ -18,6 +18,7 @@ import {
import { i18n } from '@kbn/i18n';
import type { Logger } from '@kbn/logging';
import { createService } from './service/create_service';
import { useGenAIConnectorsWithoutContext } from './hooks/use_genai_connectors';
import type {
ConfigSchema,
ObservabilityAIAssistantPluginSetup,
@ -119,6 +120,6 @@ export class ObservabilityAIAssistantPlugin
});
});
return service;
return { service, useGenAIConnectors: () => useGenAIConnectorsWithoutContext(service) };
}
}

View file

@ -26,7 +26,6 @@ import { useObservabilityAIAssistantParams } from '../../hooks/use_observability
import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router';
import { EMPTY_CONVERSATION_TITLE } from '../../i18n';
import { getConnectorsManagementHref } from '../../utils/get_connectors_management_href';
import { getModelsManagementHref } from '../../utils/get_models_management_href';
const containerClassName = css`
max-width: 100%;
@ -229,7 +228,6 @@ export function ConversationView() {
currentUser={currentUser}
connectors={connectors}
connectorsManagementHref={getConnectorsManagementHref(http)}
modelsManagementHref={getModelsManagementHref(http)}
initialConversationId={conversationId}
knowledgeBase={knowledgeBase}
startedFrom="conversationView"

View file

@ -36,7 +36,7 @@ import {
} from '../../common/types';
import { ObservabilityAIAssistantAPIClient } from '../api';
import type {
ChatRegistrationFunction,
AssistantRegistrationFunction,
CreateChatCompletionResponseChunk,
ObservabilityAIAssistantChatService,
PendingMessage,
@ -65,7 +65,7 @@ export async function createChatService({
}: {
analytics: AnalyticsServiceStart;
signal: AbortSignal;
registrations: ChatRegistrationFunction[];
registrations: AssistantRegistrationFunction[];
client: ObservabilityAIAssistantAPIClient;
}): Promise<ObservabilityAIAssistantChatService> {
const contextRegistry: ContextRegistry = new Map();

View file

@ -10,7 +10,7 @@ import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import type { SecurityPluginStart } from '@kbn/security-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import { createCallObservabilityAIAssistantAPI } from '../api';
import type { ChatRegistrationFunction, ObservabilityAIAssistantService } from '../types';
import type { AssistantRegistrationFunction, ObservabilityAIAssistantService } from '../types';
export function createService({
analytics,
@ -26,10 +26,10 @@ export function createService({
licenseStart: LicensingPluginStart;
securityStart: SecurityPluginStart;
shareStart: SharePluginStart;
}): ObservabilityAIAssistantService & { register: (fn: ChatRegistrationFunction) => void } {
}): ObservabilityAIAssistantService {
const client = createCallObservabilityAIAssistantAPI(coreStart);
const registrations: ChatRegistrationFunction[] = [];
const registrations: AssistantRegistrationFunction[] = [];
return {
isEnabled: () => {

View file

@ -42,6 +42,7 @@ import type {
} from '../common/types';
import type { ObservabilityAIAssistantAPIClient } from './api';
import type { PendingMessage } from '../common/types';
import { UseGenAIConnectorsResult } from './hooks/use_genai_connectors';
/* eslint-disable @typescript-eslint/no-empty-interface*/
@ -78,7 +79,7 @@ export interface ObservabilityAIAssistantChatService {
) => React.ReactNode;
}
export type ChatRegistrationFunction = ({}: {
export type AssistantRegistrationFunction = ({}: {
signal: AbortSignal;
registerFunction: RegisterFunctionDefinition;
registerContext: RegisterContextDefinition;
@ -91,10 +92,12 @@ export interface ObservabilityAIAssistantService {
getLicense: () => Observable<ILicense>;
getLicenseManagementLocator: () => SharePluginStart;
start: ({}: { signal: AbortSignal }) => Promise<ObservabilityAIAssistantChatService>;
register: (fn: AssistantRegistrationFunction) => void;
}
export interface ObservabilityAIAssistantPluginStart extends ObservabilityAIAssistantService {
register: (fn: ChatRegistrationFunction) => void;
export interface ObservabilityAIAssistantPluginStart {
service: ObservabilityAIAssistantService;
useGenAIConnectors: () => UseGenAIConnectorsResult;
}
export interface ObservabilityAIAssistantPluginSetup {}

View file

@ -0,0 +1,12 @@
/*
* 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 { HttpStart } from '@kbn/core/public';
export function getSettingsHref(http: HttpStart) {
return http!.basePath.prepend(`/app/management/kibana/aiAssistantManagementObservability`);
}

View file

@ -0,0 +1,14 @@
/*
* 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 { HttpStart } from '@kbn/core/public';
export function getSettingsKnowledgeBaseHref(http: HttpStart) {
return http!.basePath.prepend(
`/app/management/kibana/aiAssistantManagementObservability?tab=knowledge_base`
);
}

View file

@ -44,7 +44,7 @@ const chatService: ObservabilityAIAssistantChatService = {
hasRenderFunction: () => true,
};
const service: ObservabilityAIAssistantService = {
export const mockService: ObservabilityAIAssistantService = {
isEnabled: () => true,
start: async () => {
return chatService;
@ -66,6 +66,7 @@ const service: ObservabilityAIAssistantService = {
url: {},
navigate: () => {},
} as unknown as SharePluginStart),
register: () => {},
};
export function KibanaReactStorybookDecorator(Story: ComponentType) {
@ -82,7 +83,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) {
},
}}
>
<ObservabilityAIAssistantProvider value={service}>
<ObservabilityAIAssistantProvider value={mockService}>
<ObservabilityAIAssistantChatServiceProvider value={chatService}>
<Story />
</ObservabilityAIAssistantChatServiceProvider>

View file

@ -11,6 +11,7 @@ import type { ObservabilityAIAssistantConfig } from './config';
export type { ObservabilityAIAssistantServerRouteRepository } from './routes/get_global_observability_ai_assistant_route_repository';
import { config as configSchema } from './config';
import { ObservabilityAIAssistantService } from './service';
export const config: PluginConfigDescriptor<ObservabilityAIAssistantConfig> = {
deprecations: ({ unusedFromRoot }) => [
@ -37,6 +38,20 @@ export const config: PluginConfigDescriptor<ObservabilityAIAssistantConfig> = {
schema: configSchema,
};
export interface ObservabilityAIAssistantPluginSetup {
/**
* Returns a Observability AI Assistant service instance
*/
service: ObservabilityAIAssistantService;
}
export interface ObservabilityAIAssistantPluginStart {
/**
* Returns a Observability AI Assistant service instance
*/
service: ObservabilityAIAssistantService;
}
export const plugin = async (ctx: PluginInitializerContext<ObservabilityAIAssistantConfig>) => {
const { ObservabilityAIAssistantPlugin } = await import('./plugin');
return new ObservabilityAIAssistantPlugin(ctx);

View file

@ -7,7 +7,6 @@
import {
CoreSetup,
CoreStart,
DEFAULT_APP_CATEGORIES,
Logger,
Plugin,
@ -31,7 +30,7 @@ import {
ObservabilityAIAssistantPluginSetupDependencies,
ObservabilityAIAssistantPluginStartDependencies,
} from './types';
import { addLensDocsToKb } from './service/kb_service/kb_docs/lens';
import { addLensDocsToKb } from './service/knowledge_base_service/kb_docs/lens';
export class ObservabilityAIAssistantPlugin
implements
@ -43,6 +42,8 @@ export class ObservabilityAIAssistantPlugin
>
{
logger: Logger;
service: ObservabilityAIAssistantService | undefined;
constructor(context: PluginInitializerContext<ObservabilityAIAssistantConfig>) {
this.logger = context.logger.get();
}
@ -103,30 +104,29 @@ export class ObservabilityAIAssistantPlugin
};
}) as ObservabilityAIAssistantRouteHandlerResources['plugins'];
const service = new ObservabilityAIAssistantService({
this.service = new ObservabilityAIAssistantService({
logger: this.logger.get('service'),
core,
taskManager: plugins.taskManager,
});
addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') });
addLensDocsToKb({ service: this.service, logger: this.logger.get('kb').get('lens') });
registerServerRoutes({
core,
logger: this.logger,
dependencies: {
plugins: routeHandlerPlugins,
service,
service: this.service,
},
});
return {};
return {
service: this.service,
};
}
public start(
core: CoreStart,
plugins: ObservabilityAIAssistantPluginStartDependencies
): ObservabilityAIAssistantPluginStart {
public start(): ObservabilityAIAssistantPluginStart {
return {};
}
}

View file

@ -15,8 +15,9 @@ import {
ALERT_STATUS,
ALERT_STATUS_ACTIVE,
} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names';
import { KnowledgeBaseEntryRole } from '../../../common/types';
import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route';
import type { RecalledEntry } from '../../service/kb_service';
import type { RecalledEntry } from '../../service/knowledge_base_service';
const functionElasticsearchRoute = createObservabilityAIAssistantServerRoute({
endpoint: 'POST /internal/observability_ai_assistant/functions/elasticsearch',
@ -219,63 +220,21 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({
labels,
} = resources.params.body;
return client.summarize({
return client.createKnowledgeBaseEntry({
entry: {
confidence,
id,
doc_id: id,
is_correction: isCorrection,
text,
public: isPublic,
labels,
role: KnowledgeBaseEntryRole.AssistantSummarization,
},
});
},
});
const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({
endpoint: 'GET /internal/observability_ai_assistant/functions/kb_status',
options: {
tags: ['access:ai_assistant'],
},
handler: async (
resources
): Promise<{
ready: boolean;
error?: any;
deployment_state?: string;
allocation_state?: string;
}> => {
const client = await resources.service.getClient({ request: resources.request });
if (!client) {
throw notImplemented();
}
return await client.getKnowledgeBaseStatus();
},
});
const setupKnowledgeBaseRoute = createObservabilityAIAssistantServerRoute({
endpoint: 'POST /internal/observability_ai_assistant/functions/setup_kb',
options: {
tags: ['access:ai_assistant'],
timeout: {
idleSocket: 20 * 60 * 1000, // 20 minutes
},
},
handler: async (resources): Promise<{}> => {
const client = await resources.service.getClient({ request: resources.request });
if (!client) {
throw notImplemented();
}
await client.setupKnowledgeBase();
return {};
},
});
const functionGetDatasetInfoRoute = createObservabilityAIAssistantServerRoute({
endpoint: 'POST /internal/observability_ai_assistant/functions/get_dataset_info',
params: t.type({
@ -352,8 +311,6 @@ export const functionRoutes = {
...functionElasticsearchRoute,
...functionRecallRoute,
...functionSummariseRoute,
...setupKnowledgeBaseRoute,
...getKnowledgeBaseStatus,
...functionAlertsRoute,
...functionGetDatasetInfoRoute,
};

View file

@ -9,6 +9,7 @@ import { chatRoutes } from './chat/route';
import { connectorRoutes } from './connectors/route';
import { conversationRoutes } from './conversations/route';
import { functionRoutes } from './functions/route';
import { knowledgeBaseRoutes } from './knowledge_base/route';
export function getGlobalObservabilityAIAssistantServerRouteRepository() {
return {
@ -16,6 +17,7 @@ export function getGlobalObservabilityAIAssistantServerRouteRepository() {
...conversationRoutes,
...connectorRoutes,
...functionRoutes,
...knowledgeBaseRoutes,
};
}

View file

@ -0,0 +1,200 @@
/*
* 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 { notImplemented } from '@hapi/boom';
import { nonEmptyStringRt, toBooleanRt } from '@kbn/io-ts-utils';
import * as t from 'io-ts';
import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route';
import { KnowledgeBaseEntry, KnowledgeBaseEntryRole } from '../../../common/types';
const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({
endpoint: 'GET /internal/observability_ai_assistant/kb/status',
options: {
tags: ['access:ai_assistant'],
},
handler: async (
resources
): Promise<{
ready: boolean;
error?: any;
deployment_state?: string;
allocation_state?: string;
}> => {
const client = await resources.service.getClient({ request: resources.request });
if (!client) {
throw notImplemented();
}
return await client.getKnowledgeBaseStatus();
},
});
const setupKnowledgeBase = createObservabilityAIAssistantServerRoute({
endpoint: 'POST /internal/observability_ai_assistant/kb/setup',
options: {
tags: ['access:ai_assistant'],
timeout: {
idleSocket: 20 * 60 * 1000, // 20 minutes
},
},
handler: async (resources): Promise<{}> => {
const client = await resources.service.getClient({ request: resources.request });
if (!client) {
throw notImplemented();
}
await client.setupKnowledgeBase();
return {};
},
});
const getKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
options: {
tags: ['access:ai_assistant'],
},
params: t.type({
query: t.type({
query: t.string,
sortBy: t.string,
sortDirection: t.union([t.literal('asc'), t.literal('desc')]),
}),
}),
handler: async (
resources
): Promise<{
entries: KnowledgeBaseEntry[];
}> => {
const client = await resources.service.getClient({ request: resources.request });
if (!client) {
throw notImplemented();
}
const { query, sortBy, sortDirection } = resources.params.query;
return await client.getKnowledgeBaseEntries({ query, sortBy, sortDirection });
},
});
const saveKnowledgeBaseEntry = createObservabilityAIAssistantServerRoute({
endpoint: 'POST /internal/observability_ai_assistant/kb/entries/save',
params: t.type({
body: t.intersection([
t.type({
id: t.string,
text: nonEmptyStringRt,
}),
t.partial({
confidence: t.union([t.literal('low'), t.literal('medium'), t.literal('high')]),
is_correction: toBooleanRt,
public: toBooleanRt,
labels: t.record(t.string, t.string),
role: t.union([
t.literal('assistant_summarization'),
t.literal('user_entry'),
t.literal('elastic'),
]),
}),
]),
}),
options: {
tags: ['access:ai_assistant'],
},
handler: async (resources): Promise<void> => {
const client = await resources.service.getClient({ request: resources.request });
if (!client) {
throw notImplemented();
}
const { id, text } = resources.params.body;
return client.createKnowledgeBaseEntry({
entry: {
id,
text,
doc_id: id,
confidence: resources.params.body.confidence ?? 'high',
is_correction: resources.params.body.is_correction ?? false,
public: resources.params.body.public ?? true,
labels: resources.params.body.labels ?? {},
role:
(resources.params.body.role as KnowledgeBaseEntryRole) ??
KnowledgeBaseEntryRole.UserEntry,
},
});
},
});
const deleteKnowledgeBaseEntry = createObservabilityAIAssistantServerRoute({
endpoint: 'DELETE /internal/observability_ai_assistant/kb/entries/{entryId}',
params: t.type({
path: t.type({
entryId: t.string,
}),
}),
options: {
tags: ['access:ai_assistant'],
},
handler: async (resources): Promise<void> => {
const client = await resources.service.getClient({ request: resources.request });
if (!client) {
throw notImplemented();
}
return client.deleteKnowledgeBaseEntry(resources.params.path.entryId);
},
});
const importKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({
endpoint: 'POST /internal/observability_ai_assistant/kb/entries/import',
params: t.type({
body: t.type({
entries: t.array(
t.type({
id: t.string,
text: nonEmptyStringRt,
})
),
}),
}),
options: {
tags: ['access:ai_assistant'],
},
handler: async (resources): Promise<void> => {
const client = await resources.service.getClient({ request: resources.request });
if (!client) {
throw notImplemented();
}
const entries = resources.params.body.entries.map((entry) => ({
doc_id: entry.id,
confidence: 'high' as KnowledgeBaseEntry['confidence'],
is_correction: false,
public: true,
labels: {},
role: KnowledgeBaseEntryRole.UserEntry,
...entry,
}));
return await client.importKnowledgeBaseEntries({ entries });
},
});
export const knowledgeBaseRoutes = {
...setupKnowledgeBase,
...getKnowledgeBaseStatus,
...getKnowledgeBaseEntries,
...importKnowledgeBaseEntries,
...saveKnowledgeBaseEntry,
...deleteKnowledgeBaseEntry,
};

View file

@ -28,7 +28,11 @@ import {
type KnowledgeBaseEntry,
type Message,
} from '../../../common/types';
import type { KnowledgeBaseService, RecalledEntry } from '../kb_service';
import {
KnowledgeBaseEntryOperationType,
KnowledgeBaseService,
RecalledEntry,
} from '../knowledge_base_service';
import type { ObservabilityAIAssistantResourceNames } from '../types';
import { getAccessQuery } from '../util/get_access_query';
@ -377,18 +381,6 @@ export class ObservabilityAIAssistantClient {
});
};
summarize = async ({
entry,
}: {
entry: Omit<KnowledgeBaseEntry, '@timestamp'>;
}): Promise<void> => {
return this.dependencies.knowledgeBaseService.summarize({
namespace: this.dependencies.namespace,
user: this.dependencies.user,
entry,
});
};
getKnowledgeBaseStatus = () => {
return this.dependencies.knowledgeBaseService.status();
};
@ -396,4 +388,45 @@ export class ObservabilityAIAssistantClient {
setupKnowledgeBase = () => {
return this.dependencies.knowledgeBaseService.setup();
};
createKnowledgeBaseEntry = async ({
entry,
}: {
entry: Omit<KnowledgeBaseEntry, '@timestamp'>;
}): Promise<void> => {
return this.dependencies.knowledgeBaseService.addEntry({
namespace: this.dependencies.namespace,
user: this.dependencies.user,
entry,
});
};
importKnowledgeBaseEntries = async ({
entries,
}: {
entries: Array<Omit<KnowledgeBaseEntry, '@timestamp'>>;
}): Promise<void> => {
const operations = entries.map((entry) => ({
type: KnowledgeBaseEntryOperationType.Index,
document: { ...entry, '@timestamp': new Date().toISOString() },
}));
await this.dependencies.knowledgeBaseService.addEntries({ operations });
};
getKnowledgeBaseEntries = async ({
query,
sortBy,
sortDirection,
}: {
query: string;
sortBy: string;
sortDirection: 'asc' | 'desc';
}) => {
return this.dependencies.knowledgeBaseService.getEntries({ query, sortBy, sortDirection });
};
deleteKnowledgeBaseEntry = async (id: string) => {
return this.dependencies.knowledgeBaseService.deleteEntry({ id });
};
}

Some files were not shown because too many files have changed in this diff Show more