mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
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:
parent
45592cd777
commit
7d990cf749
117 changed files with 3962 additions and 691 deletions
|
@ -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
2
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -119,6 +119,9 @@ export const defaultNavigation: ManagementNodeDefinition = {
|
|||
{
|
||||
link: 'management:dataViews',
|
||||
},
|
||||
{
|
||||
link: 'management:aiAssistantManagementSelection',
|
||||
},
|
||||
{
|
||||
// Saved objects
|
||||
link: 'management:objects',
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
pageLoadAssetSize:
|
||||
actions: 20000
|
||||
advancedSettings: 27596
|
||||
aiAssistantManagementObservability: 19279
|
||||
aiAssistantManagementSelection: 19146
|
||||
aiops: 10000
|
||||
alerting: 106936
|
||||
apm: 64385
|
||||
|
|
|
@ -29,6 +29,8 @@ const allNavLinks: AppDeepLinkId[] = [
|
|||
'fleet',
|
||||
'integrations',
|
||||
'management',
|
||||
'management:aiAssistantManagementSelection',
|
||||
'management:aiAssistantManagementObservability',
|
||||
'management:api_keys',
|
||||
'management:cases',
|
||||
'management:cross_cluster_replication',
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# `aiAssistantManagementObservability` plugin
|
||||
|
||||
The `aiAssistantManagementObservability` plugin manages the `Ai Assistant for Observability` management section.
|
|
@ -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}',
|
||||
],
|
||||
};
|
|
@ -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"]
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -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',
|
||||
};
|
|
@ -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>;
|
||||
};
|
|
@ -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 }>);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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 },
|
||||
}
|
||||
),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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 },
|
||||
}
|
||||
),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
}
|
||||
),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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]
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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 {};
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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}</>;
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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/**/*"]
|
||||
}
|
3
src/plugins/ai_assistant_management/selection/README.md
Normal file
3
src/plugins/ai_assistant_management/selection/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# `aiAssistantManagementSelection` plugin
|
||||
|
||||
The `aiAssistantManagementSelection` plugin manages the `Ai Assistant` management section.
|
19
src/plugins/ai_assistant_management/selection/jest.config.js
Normal file
19
src/plugins/ai_assistant_management/selection/jest.config.js
Normal 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}',
|
||||
],
|
||||
};
|
13
src/plugins/ai_assistant_management/selection/kibana.jsonc
Normal file
13
src/plugins/ai_assistant_management/selection/kibana.jsonc
Normal 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"]
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -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 {};
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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}</>;
|
||||
}
|
|
@ -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;
|
19
src/plugins/ai_assistant_management/selection/tsconfig.json
Normal file
19
src/plugins/ai_assistant_management/selection/tsconfig.json
Normal 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/**/*"]
|
||||
}
|
|
@ -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;
|
||||
}, []);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -16,6 +16,8 @@ export const capabilitiesProvider = () => ({
|
|||
settings: true,
|
||||
indexPatterns: true,
|
||||
objects: true,
|
||||
aiAssistantManagementSelection: true,
|
||||
aiAssistantManagementObservability: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -33,7 +33,9 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
|
|||
search: data.search,
|
||||
});
|
||||
|
||||
const LogAIAssistant = createLogAIAssistant({ observabilityAIAssistant });
|
||||
const LogAIAssistant = createLogAIAssistant({
|
||||
observabilityAIAssistant: observabilityAIAssistant.service,
|
||||
});
|
||||
|
||||
return {
|
||||
logViews,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -101,7 +101,7 @@ export const renderApp = ({
|
|||
isServerless,
|
||||
}}
|
||||
>
|
||||
<ObservabilityAIAssistantProvider value={plugins.observabilityAIAssistant}>
|
||||
<ObservabilityAIAssistantProvider value={plugins.observabilityAIAssistant.service}>
|
||||
<PluginContext.Provider
|
||||
value={{
|
||||
config,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -59,6 +59,7 @@ const defaultProps: ComponentStoryObj<typeof Component> = {
|
|||
error: undefined,
|
||||
selectedConnector: 'foo',
|
||||
selectConnector: () => {},
|
||||
reloadConnectors: () => {},
|
||||
},
|
||||
connectorsManagementHref: '',
|
||||
currentUser: {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ const defaultProps: InsightBaseProps = {
|
|||
selectedConnector="gpt-4"
|
||||
loading={false}
|
||||
selectConnector={() => {}}
|
||||
reloadConnectors={() => {}}
|
||||
/>
|
||||
),
|
||||
onToggle: () => {},
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -14,5 +14,6 @@ export function useGenAIConnectors(): UseGenAIConnectorsResult {
|
|||
error: undefined,
|
||||
selectedConnector: 'foo',
|
||||
selectConnector: (id: string) => {},
|
||||
reloadConnectors: () => {},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ const mockService: MockedService = {
|
|||
getLicenseManagementLocator: jest.fn(),
|
||||
isEnabled: jest.fn(),
|
||||
start: jest.fn(),
|
||||
register: jest.fn(),
|
||||
};
|
||||
|
||||
const mockChatService = createMockChatService();
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>>();
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: () => {
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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`);
|
||||
}
|
|
@ -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`
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue