[Observability AI Assistant]: Move to new plugin, update design and use connectors (#162243)

This PR makes the following changes:

- Update look & feel of contextual insights (previously called prompts)
according to the new design that is being developed. Some things might
still change, but hopefully not too much.
- Move all the Observability AI Assistant (previously called CoPilot)
code into a separate plugin for better isolation, more specific code
ownership and to solve some circular dependency issues
- Use connectors instead of a kibana.yml setting

Note: for OpenAI, the model is currently hardcoded to `gpt-4` until
https://github.com/elastic/kibana/issues/162204 has been addressed.


557676b6-065a-4b6f-86b2-1f0c2fd5e07e

---------

Co-authored-by: Coen Warmer <coen.warmer@gmail.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dario Gieselaar 2023-07-28 17:52:00 +02:00 committed by GitHub
parent ecb7f3eaf7
commit f0ebb7097d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
133 changed files with 4090 additions and 1636 deletions

View file

@ -340,6 +340,7 @@ enabled:
- x-pack/test/observability_functional/with_rac_write.config.ts
- x-pack/test/observability_onboarding_api_integration/basic/config.ts
- x-pack/test/observability_onboarding_api_integration/cloud/config.ts
- x-pack/test/observability_ai_assistant_api_integration/enterprise/config.ts
- x-pack/test/plugin_api_integration/config.ts
- x-pack/test/plugin_functional/config.ts
- x-pack/test/reporting_api_integration/reporting_and_security.config.ts

View file

@ -43,6 +43,7 @@ const STORYBOOKS = [
'kibana_react',
'lists',
'observability',
'observability_ai_assistant',
'presentation',
// 'security_solution', => This build is error out and failing CI. SEE: https://github.com/elastic/kibana/issues/162290
'security_solution_packages',

1
.github/CODEOWNERS vendored
View file

@ -515,6 +515,7 @@ src/plugins/newsfeed @elastic/kibana-core
test/common/plugins/newsfeed @elastic/kibana-core
x-pack/plugins/notifications @elastic/appex-sharedux
packages/kbn-object-versioning @elastic/appex-sharedux
x-pack/plugins/observability_ai_assistant @elastic/apm-ui
x-pack/packages/observability/alert_details @elastic/actionable-observability
x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops
x-pack/plugins/observability_onboarding @elastic/apm-ui

View file

@ -653,6 +653,10 @@ Elastic.
|This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI.
|{kib-repo}blob/{branch}/x-pack/plugins/observability_ai_assistant/README.md[observabilityAIAssistant]
|This plugin provides the Observability AI Assistant service and UI components.
|{kib-repo}blob/{branch}/x-pack/plugins/observability_onboarding/README.md[observabilityOnboarding]
|This plugin provides an onboarding framework for observability solutions: Logs and APM.

View file

@ -531,6 +531,7 @@
"@kbn/newsfeed-test-plugin": "link:test/common/plugins/newsfeed",
"@kbn/notifications-plugin": "link:x-pack/plugins/notifications",
"@kbn/object-versioning": "link:packages/kbn-object-versioning",
"@kbn/observability-ai-assistant-plugin": "link:x-pack/plugins/observability_ai_assistant",
"@kbn/observability-alert-details": "link:x-pack/packages/observability/alert_details",
"@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability",
"@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_onboarding",
@ -839,6 +840,7 @@
"fast-deep-equal": "^3.1.1",
"fflate": "^0.6.9",
"file-saver": "^1.3.8",
"fnv-plus": "^1.3.1",
"font-awesome": "4.7.0",
"formik": "^2.2.9",
"fp-ts": "^2.3.1",
@ -905,7 +907,7 @@
"normalize-path": "^3.0.0",
"object-hash": "^1.3.1",
"object-path-immutable": "^3.1.1",
"openai": "^3.2.1",
"openai": "^3.3.0",
"openpgp": "5.3.0",
"opn": "^5.5.0",
"ora": "^4.0.4",
@ -1436,7 +1438,6 @@
"faker": "^5.1.0",
"fetch-mock": "^7.3.9",
"file-loader": "^4.2.0",
"fnv-plus": "^1.3.1",
"form-data": "^4.0.0",
"geckodriver": "^4.0.0",
"gulp-brotli": "^3.0.0",

View file

@ -97,6 +97,7 @@ pageLoadAssetSize:
navigation: 37269
newsfeed: 42228
observability: 115443
observabilityAIAssistant: 16759
observabilityOnboarding: 19573
observabilityShared: 52256
osquery: 107090

View file

@ -11,6 +11,8 @@ export { formatRequest } from './src/format_request';
export { parseEndpoint } from './src/parse_endpoint';
export { decodeRequestParams } from './src/decode_request_params';
export { routeValidationObject } from './src/route_validation_object';
export { registerRoutes } from './src/register_routes';
export type {
RouteRepositoryClient,
ReturnOf,

View file

@ -0,0 +1,141 @@
/*
* 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 { errors } from '@elastic/elasticsearch';
import { isBoom } from '@hapi/boom';
import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import type { KibanaRequest, KibanaResponseFactory } from '@kbn/core-http-server';
import type { CoreSetup } from '@kbn/core-lifecycle-server';
import type { Logger } from '@kbn/logging';
import * as t from 'io-ts';
import { merge, pick } from 'lodash';
import { decodeRequestParams } from './decode_request_params';
import { parseEndpoint } from './parse_endpoint';
import { routeValidationObject } from './route_validation_object';
import type { ServerRoute, ServerRouteCreateOptions } from './typings';
const CLIENT_CLOSED_REQUEST = {
statusCode: 499,
body: {
message: 'Client closed request',
},
};
export function registerRoutes({
core,
repository,
logger,
dependencies,
}: {
core: CoreSetup;
repository: Record<string, ServerRoute<string, any, any, any, ServerRouteCreateOptions>>;
logger: Logger;
dependencies: Record<string, any>;
}) {
const routes = Object.values(repository);
const router = core.http.createRouter();
routes.forEach((route) => {
const { params, endpoint, options, handler } = route;
const { method, pathname, version } = parseEndpoint(endpoint);
const wrappedHandler = async (
context: RequestHandlerContext,
request: KibanaRequest,
response: KibanaResponseFactory
) => {
try {
const runtimeType = params || t.strict({});
const validatedParams = decodeRequestParams(
pick(request, 'params', 'body', 'query'),
runtimeType
);
const { aborted, data } = await Promise.race([
handler({
request,
context,
params: validatedParams,
...dependencies,
}).then((value) => {
return {
aborted: false,
data: value,
};
}),
request.events.aborted$.toPromise().then(() => {
return {
aborted: true,
data: undefined,
};
}),
]);
if (aborted) {
return response.custom(CLIENT_CLOSED_REQUEST);
}
const body = data || {};
return response.ok({ body });
} catch (error) {
logger.error(error);
const opts = {
statusCode: 500,
body: {
message: error.message,
attributes: {
data: {},
},
},
};
if (error instanceof errors.RequestAbortedError) {
return response.custom(merge(opts, CLIENT_CLOSED_REQUEST));
}
if (isBoom(error)) {
opts.statusCode = error.output.statusCode;
opts.body.attributes.data = error?.data;
}
return response.custom(opts);
}
};
logger.debug(`Registering endpoint ${endpoint}`);
if (!version) {
router[method](
{
path: pathname,
options,
validate: routeValidationObject,
},
wrappedHandler
);
} else {
router.versioned[method]({
path: pathname,
access: pathname.startsWith('/internal/') ? 'internal' : 'public',
options,
}).addVersion(
{
version,
validate: {
request: routeValidationObject,
},
},
wrappedHandler
);
}
});
}

View file

@ -13,7 +13,11 @@
],
"kbn_references": [
"@kbn/config-schema",
"@kbn/io-ts-utils"
"@kbn/io-ts-utils",
"@kbn/core-http-request-handler-context-server",
"@kbn/core-http-server",
"@kbn/core-lifecycle-server",
"@kbn/logging"
],
"exclude": [
"target/**/*",

View file

@ -48,6 +48,7 @@ export const storybookAliases = {
lists: 'x-pack/plugins/lists/.storybook',
management: 'packages/kbn-management/storybook/config',
observability: 'x-pack/plugins/observability/.storybook',
observability_ai_assistant: 'x-pack/plugins/observability_ai_assistant/.storybook',
presentation: 'src/plugins/presentation_util/storybook',
random_sampling: 'x-pack/packages/kbn-random-sampling/.storybook',
text_based_editor: 'packages/kbn-text-based-editor/.storybook',

View file

@ -279,8 +279,6 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.upgrade_assistant.featureSet.mlSnapshots (boolean)',
'xpack.upgrade_assistant.featureSet.reindexCorrectiveActions (boolean)',
'xpack.upgrade_assistant.ui.enabled (boolean)',
'xpack.observability.aiAssistant.enabled (boolean)',
'xpack.observability.aiAssistant.feedback.enabled (boolean)',
'xpack.observability.unsafe.alertDetails.metrics.enabled (boolean)',
'xpack.observability.unsafe.alertDetails.logs.enabled (boolean)',
'xpack.observability.unsafe.alertDetails.uptime.enabled (boolean)',

View file

@ -1024,6 +1024,8 @@
"@kbn/notifications-plugin/*": ["x-pack/plugins/notifications/*"],
"@kbn/object-versioning": ["packages/kbn-object-versioning"],
"@kbn/object-versioning/*": ["packages/kbn-object-versioning/*"],
"@kbn/observability-ai-assistant-plugin": ["x-pack/plugins/observability_ai_assistant"],
"@kbn/observability-ai-assistant-plugin/*": ["x-pack/plugins/observability_ai_assistant/*"],
"@kbn/observability-alert-details": ["x-pack/packages/observability/alert_details"],
"@kbn/observability-alert-details/*": ["x-pack/packages/observability/alert_details/*"],
"@kbn/observability-fixtures-plugin": ["x-pack/test/cases_api_integration/common/plugins/observability"],

View file

@ -61,6 +61,7 @@
"xpack.observability": "plugins/observability",
"xpack.observabilityShared": "plugins/observability_shared",
"xpack.observability_onboarding": "plugins/observability_onboarding",
"xpack.observabilityAiAssistant": "plugins/observability_ai_assistant",
"xpack.osquery": ["plugins/osquery"],
"xpack.painlessLab": "plugins/painless_lab",
"xpack.profiling": ["plugins/profiling"],

View file

@ -28,7 +28,8 @@
"dataViews",
"lens",
"maps",
"uiActions"
"uiActions",
"observabilityAIAssistant"
],
"optionalPlugins": [
"actions",
@ -47,7 +48,7 @@
"usageCollection",
"customIntegrations", // Move this to requiredPlugins after completely migrating from the Tutorials Home App
"licenseManagement",
"profiling"
"profiling",
],
"requiredBundles": [
"advancedSettings",
@ -57,7 +58,8 @@
"ml",
"observability",
"esUiShared",
"maps"
"maps",
"observabilityAIAssistant"
]
}
}

View file

@ -56,6 +56,7 @@ export const renderApp = ({
unifiedSearch: pluginsStart.unifiedSearch,
lens: pluginsStart.lens,
uiActions: pluginsStart.uiActions,
observabilityAIAssistant: pluginsStart.observabilityAIAssistant,
};
// render APM feedback link in global help menu

View file

@ -1,86 +0,0 @@
/*
* 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 React, { useMemo, useState } from 'react';
import { useCoPilot, CoPilotPrompt } from '@kbn/observability-plugin/public';
import { EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { CoPilotPromptId } from '@kbn/observability-plugin/common';
import { APMError } from '../../../../../typings/es_schemas/ui/apm_error';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
import { exceptionStacktraceTab, logStacktraceTab } from './error_tabs';
import { ErrorSampleDetailTabContent } from './error_sample_detail';
export function ErrorSampleCoPilotPrompt({
error,
transaction,
}: {
error: APMError;
transaction?: Transaction;
}) {
const coPilot = useCoPilot();
const [logStacktrace, setLogStacktrace] = useState('');
const [exceptionStacktrace, setExceptionStacktrace] = useState('');
const promptParams = useMemo(() => {
return {
serviceName: error.service.name,
languageName: error.service.language?.name ?? '',
runtimeName: error.service.runtime?.name ?? '',
runtimeVersion: error.service.runtime?.version ?? '',
transactionName: transaction?.transaction.name ?? '',
logStacktrace,
exceptionStacktrace,
};
}, [error, transaction, logStacktrace, exceptionStacktrace]);
return coPilot?.isEnabled() && promptParams ? (
<>
<EuiFlexItem>
<CoPilotPrompt
coPilot={coPilot}
title={i18n.translate(
'xpack.apm.errorGroupCoPilotPrompt.explainErrorTitle',
{ defaultMessage: "What's this error?" }
)}
promptId={CoPilotPromptId.ApmExplainError}
params={promptParams}
feedbackEnabled={false}
/>
</EuiFlexItem>
<EuiSpacer size="s" />
<div
ref={(next) => {
setLogStacktrace(next?.innerText ?? '');
}}
style={{ display: 'none' }}
>
{error.error.log?.message && (
<ErrorSampleDetailTabContent
error={error}
currentTab={logStacktraceTab}
/>
)}
</div>
<div
ref={(next) => {
setExceptionStacktrace(next?.innerText ?? '');
}}
style={{ display: 'none' }}
>
{error.error.exception?.length && (
<ErrorSampleDetailTabContent
error={error}
currentTab={exceptionStacktraceTab}
/>
)}
</div>
</>
) : (
<></>
);
}

View file

@ -0,0 +1,125 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
useObservabilityAIAssistant,
ContextualInsight,
type Message,
MessageRole,
} from '@kbn/observability-ai-assistant-plugin/public';
import React, { useMemo, useState } from 'react';
import { APMError } from '../../../../../typings/es_schemas/ui/apm_error';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
import { ErrorSampleDetailTabContent } from './error_sample_detail';
import { exceptionStacktraceTab, logStacktraceTab } from './error_tabs';
export function ErrorSampleContextualInsight({
error,
transaction,
}: {
error: APMError;
transaction?: Transaction;
}) {
const aiAssistant = useObservabilityAIAssistant();
const [logStacktrace, setLogStacktrace] = useState('');
const [exceptionStacktrace, setExceptionStacktrace] = useState('');
const messages = useMemo<Message[]>(() => {
const now = new Date().toISOString();
const serviceName = error.service.name;
const languageName = error.service.language?.name ?? '';
const runtimeName = error.service.runtime?.name ?? '';
const runtimeVersion = error.service.runtime?.version ?? '';
const transactionName = transaction?.transaction.name ?? '';
return [
{
'@timestamp': now,
message: {
role: MessageRole.System,
content: `You are apm-gpt, a helpful assistant for performance analysis, optimisation and
root cause analysis of software. Answer as concisely as possible.`,
},
},
{
'@timestamp': now,
message: {
role: MessageRole.User,
content: `I'm an SRE. I am looking at an exception and trying to understand what it means.
Your task is to describe what the error means and what it could be caused by.
The error occurred on a service called ${serviceName}, which is a ${runtimeName} service written in ${languageName}. The
runtime version is ${runtimeVersion}.
The request it occurred for is called ${transactionName}.
${
logStacktrace
? `The log stacktrace:
${logStacktrace}`
: ''
}
${
exceptionStacktrace
? `The exception stacktrace:
${exceptionStacktrace}`
: ''
}
`,
},
},
];
}, [error, transaction, logStacktrace, exceptionStacktrace]);
return aiAssistant.isEnabled() && messages ? (
<>
<EuiFlexItem>
<ContextualInsight
messages={messages}
title={i18n.translate(
'xpack.apm.errorGroupContextualInsight.explainErrorTitle',
{ defaultMessage: "What's this error?" }
)}
/>
</EuiFlexItem>
<EuiSpacer size="s" />
<div
ref={(next) => {
setLogStacktrace(next?.innerText ?? '');
}}
style={{ display: 'none' }}
>
{error.error.log?.message && (
<ErrorSampleDetailTabContent
error={error}
currentTab={logStacktraceTab}
/>
)}
</div>
<div
ref={(next) => {
setExceptionStacktrace(next?.innerText ?? '');
}}
style={{ display: 'none' }}
>
{error.error.exception?.length && (
<ErrorSampleDetailTabContent
error={error}
currentTab={exceptionStacktraceTab}
/>
)}
</div>
</>
) : (
<></>
);
}

View file

@ -50,11 +50,11 @@ import { UserAgentSummaryItem } from '../../../shared/summary/user_agent_summary
import { TimestampTooltip } from '../../../shared/timestamp_tooltip';
import { PlaintextStacktrace } from './plaintext_stacktrace';
import { TransactionTab } from '../../transaction_details/waterfall_with_summary/transaction_tabs';
import { ErrorSampleCoPilotPrompt } from './error_sample_co_pilot_prompt';
import { ErrorTab, ErrorTabKey, getTabs } from './error_tabs';
import { ErrorUiActionsContextMenu } from './error_ui_actions_context_menu';
import { ExceptionStacktrace } from './exception_stacktrace';
import { SampleSummary } from './sample_summary';
import { ErrorSampleContextualInsight } from './error_sample_contextual_insight';
const TransactionLinkName = euiStyled.div`
margin-left: ${({ theme }) => theme.eui.euiSizeS};
@ -337,7 +337,7 @@ export function ErrorSampleDetails({
<SampleSummary error={error} />
)}
<ErrorSampleCoPilotPrompt error={error} transaction={transaction} />
<ErrorSampleContextualInsight error={error} transaction={transaction} />
<EuiTabs>
{tabs.map(({ key, label }) => {

View file

@ -12,14 +12,16 @@ import {
useUiSetting$,
} from '@kbn/kibana-react-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { InspectorContextProvider } from '@kbn/observability-shared-plugin/public';
import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public';
import { ObservabilityAIAssistantProvider } from '@kbn/observability-ai-assistant-plugin/public';
import {
HeaderMenuPortal,
InspectorContextProvider,
} from '@kbn/observability-shared-plugin/public';
import { Route } from '@kbn/shared-ux-router';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import { euiDarkVars, euiLightVars } from '@kbn/ui-theme';
import React from 'react';
import { Route } from '@kbn/shared-ux-router';
import { DefaultTheme, ThemeProvider } from 'styled-components';
import { CoPilotContextProvider } from '@kbn/observability-plugin/public';
import { AnomalyDetectionJobsContextProvider } from '../../../context/anomaly_detection_jobs/anomaly_detection_jobs_context';
import {
ApmPluginContext,
@ -31,15 +33,15 @@ import { LicenseProvider } from '../../../context/license/license_context';
import { TimeRangeIdContextProvider } from '../../../context/time_range_id/time_range_id_context';
import { UrlParamsProvider } from '../../../context/url_params_context/url_params_context';
import { ApmPluginStartDeps } from '../../../plugin';
import { ScrollToTopOnPathChange } from './scroll_to_top_on_path_change';
import { ApmErrorBoundary } from '../apm_error_boundary';
import { apmRouter } from '../apm_route_config';
import { TrackPageview } from '../track_pageview';
import { ApmHeaderActionMenu } from './apm_header_action_menu';
import { RedirectDependenciesToDependenciesInventory } from './redirect_dependencies_to_dependencies_inventory';
import { RedirectWithDefaultDateRange } from './redirect_with_default_date_range';
import { RedirectWithDefaultEnvironment } from './redirect_with_default_environment';
import { RedirectWithOffset } from './redirect_with_offset';
import { ApmErrorBoundary } from '../apm_error_boundary';
import { apmRouter } from '../apm_route_config';
import { RedirectDependenciesToDependenciesInventory } from './redirect_dependencies_to_dependencies_inventory';
import { TrackPageview } from '../track_pageview';
import { ScrollToTopOnPathChange } from './scroll_to_top_on_path_change';
import { UpdateExecutionContextOnRouteChange } from './update_execution_context_on_route_change';
const storage = new Storage(localStorage);
@ -55,9 +57,6 @@ export function ApmAppRoot({
const { history } = appMountParameters;
const i18nCore = core.i18n;
const coPilotService =
apmPluginContextValue.plugins.observability.getCoPilotService();
return (
<RedirectAppLinks
application={core.application}
@ -68,9 +67,11 @@ export function ApmAppRoot({
<ApmPluginContext.Provider value={apmPluginContextValue}>
<KibanaContextProvider services={{ ...core, ...pluginsStart, storage }}>
<i18nCore.Context>
<TimeRangeIdContextProvider>
<RouterProvider history={history} router={apmRouter as any}>
<CoPilotContextProvider value={coPilotService}>
<ObservabilityAIAssistantProvider
value={apmPluginContextValue.observabilityAIAssistant}
>
<TimeRangeIdContextProvider>
<RouterProvider history={history} router={apmRouter as any}>
<ApmErrorBoundary>
<RedirectDependenciesToDependenciesInventory>
<RedirectWithDefaultEnvironment>
@ -105,9 +106,9 @@ export function ApmAppRoot({
</RedirectWithDefaultEnvironment>
</RedirectDependenciesToDependenciesInventory>
</ApmErrorBoundary>
</CoPilotContextProvider>
</RouterProvider>
</TimeRangeIdContextProvider>
</RouterProvider>
</TimeRangeIdContextProvider>
</ObservabilityAIAssistantProvider>
</i18nCore.Context>
</KibanaContextProvider>
</ApmPluginContext.Provider>

View file

@ -5,19 +5,20 @@
* 2.0.
*/
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
import { createContext } from 'react';
import type { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public';
import { MapsStartApi } from '@kbn/maps-plugin/public';
import { ObservabilityPublicStart } from '@kbn/observability-plugin/public';
import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { MapsStartApi } from '@kbn/maps-plugin/public';
import type { ObservabilityPublicStart } from '@kbn/observability-plugin/public';
import type { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { InfraClientStartExports } from '@kbn/infra-plugin/public';
import { ApmPluginSetupDeps } from '../../plugin';
import { ConfigSchema } from '../..';
import type { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public';
import type { ApmPluginSetupDeps } from '../../plugin';
import type { ConfigSchema } from '../..';
export interface ApmPluginContextValue {
appMountParameters: AppMountParameters;
@ -32,6 +33,7 @@ export interface ApmPluginContextValue {
data: DataPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
uiActions: UiActionsStart;
observabilityAIAssistant: ObservabilityAIAssistantPluginStart;
}
export const ApmPluginContext = createContext({} as ApmPluginContextValue);

View file

@ -70,6 +70,7 @@ import {
DiscoverStart,
DiscoverSetup,
} from '@kbn/discover-plugin/public/plugin';
import type { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public';
import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types';
import {
getApmEnrollmentFlyoutData,
@ -130,6 +131,7 @@ export interface ApmPluginStartDeps {
lens: LensPublicStart;
uiActions: UiActionsStart;
profiling?: ProfilingPluginStart;
observabilityAIAssistant: ObservabilityAIAssistantPluginStart;
}
const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', {

View file

@ -95,7 +95,8 @@
"@kbn/profiling-plugin",
"@kbn/logs-shared-plugin",
"@kbn/unified-field-list",
"@kbn/discover-plugin"
"@kbn/discover-plugin",
"@kbn/observability-ai-assistant-plugin"
],
"exclude": ["target/**/*"]
}

View file

@ -22,6 +22,7 @@
"lens",
"logsShared",
"observability",
"observabilityAIAssistant",
"observabilityShared",
"ruleRegistry",
"security",
@ -36,6 +37,7 @@
"requiredBundles": [
"unifiedSearch",
"observability",
"observabilityAIAssistant",
"licenseManagement",
"kibanaUtils",
"kibanaReact",

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { FC, useEffect, useState } from 'react';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { pick, orderBy } from 'lodash';
import moment from 'moment';
@ -15,10 +15,15 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { DataView } from '@kbn/data-views-plugin/common';
import { LogRateAnalysisContent, type LogRateAnalysisResultsData } from '@kbn/aiops-plugin/public';
import { Rule } from '@kbn/alerting-plugin/common';
import { CoPilotPrompt, TopAlert, useCoPilot } from '@kbn/observability-plugin/public';
import { TopAlert } from '@kbn/observability-plugin/public';
import {
ContextualInsight,
useObservabilityAIAssistant,
type Message,
MessageRole,
} from '@kbn/observability-ai-assistant-plugin/public';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import { CoPilotPromptId } from '@kbn/observability-plugin/common';
import { ALERT_END } from '@kbn/rule-data-utils';
import { Color, colorTransformer } from '../../../../../../common/color_palette';
import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana';
@ -169,8 +174,52 @@ export const LogRateAnalysis: FC<AlertDetailsLogRateAnalysisSectionProps> = ({ r
setLogSpikeParams(significantFieldValues ? { significantFieldValues } : undefined);
};
const coPilotService = useCoPilot();
const hasLogSpikeParams = logSpikeParams && logSpikeParams.significantFieldValues?.length > 0;
const aiAssistant = useObservabilityAIAssistant();
const messages = useMemo<Message[] | undefined>(() => {
const hasLogSpikeParams = logSpikeParams && logSpikeParams.significantFieldValues?.length > 0;
if (!hasLogSpikeParams) {
return undefined;
}
const header = 'Field name,Field value,Doc count,p-value';
const rows = logSpikeParams.significantFieldValues
.map((item) => Object.values(item).join(','))
.join('\n');
const content = `You are an observability expert using Elastic Observability Suite on call being consulted about a log threshold alert that got triggered by a spike of log messages. Your job is to take immediate action and proceed with both urgency and precision.
"Log Rate Analysis" is an AIOps feature that uses advanced statistical methods to identify reasons for increases in log rates. It makes it easy to find and investigate causes of unusual spikes by using the analysis workflow view.
You are using "Log Rate Analysis" and ran the statistical analysis on the log messages which occured during the alert.
You received the following analysis results from "Log Rate Analysis" which list statistically significant co-occuring field/value combinations sorted from most significant (lower p-values) to least significant (higher p-values) that contribute to the log messages spike:
${header}
${rows}
Based on the above analysis results and your observability expert knowledge, output the following:
Analyse the type of these logs and explain their usual purpose (1 paragraph).
Based on the type of these logs do a root cause analysis on why the field and value combinations from the anlaysis results are causing this spike in logs (2 parapraphs).
Recommend concrete remediations to resolve the root cause (3 bullet points).
Do not repeat the given instructions in your output.`;
const now = new Date().toString();
return [
{
'@timestamp': now,
message: {
role: MessageRole.System,
content: `You are logs-gpt, a helpful assistant for logs-based observability. Answer as
concisely as possible.`,
},
},
{
'@timestamp': now,
message: {
content,
role: MessageRole.User,
},
},
];
}, [logSpikeParams]);
if (!dataView || !esSearchQuery) return null;
@ -215,15 +264,9 @@ export const LogRateAnalysis: FC<AlertDetailsLogRateAnalysisSectionProps> = ({ r
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="m">
{coPilotService?.isEnabled() && hasLogSpikeParams ? (
{aiAssistant.isEnabled() && messages ? (
<EuiFlexItem grow={false}>
<CoPilotPrompt
coPilot={coPilotService}
title={explainLogSpikeTitle}
params={logSpikeParams}
promptId={CoPilotPromptId.ExplainLogSpike}
feedbackEnabled={false}
/>
<ContextualInsight title={explainLogSpikeTitle} messages={messages} />
</EuiFlexItem>
) : null}
</EuiFlexGroup>

View file

@ -20,7 +20,7 @@ import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getPaddedAlertTimeRange } from '@kbn/observability-alert-details';
import { get, identity } from 'lodash';
import { CoPilotContextProvider } from '@kbn/observability-plugin/public';
import { ObservabilityAIAssistantProvider } from '@kbn/observability-ai-assistant-plugin/public';
import { useLogView } from '@kbn/logs-shared-plugin/public';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import {
@ -44,7 +44,7 @@ const AlertDetailsAppSection = ({
alert,
setAlertSummaryFields,
}: AlertDetailsAppSectionProps) => {
const { observability, logsShared } = useKibanaContextForPlugin().services;
const { logsShared, observabilityAIAssistant } = 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,14 +242,14 @@ const AlertDetailsAppSection = ({
};
return (
<CoPilotContextProvider value={observability.getCoPilotService()}>
<ObservabilityAIAssistantProvider value={observabilityAIAssistant}>
<EuiFlexGroup direction="column" data-test-subj="logsThresholdAlertDetailsPage">
{getLogRatioChart()}
{getLogCountChart()}
{getLogRateAnalysisSection()}
{getLogsHistoryChart()}
</EuiFlexGroup>
</CoPilotContextProvider>
</ObservabilityAIAssistantProvider>
);
};

View file

@ -9,8 +9,10 @@ import { AppMountParameters, CoreStart } from '@kbn/core/public';
import React from 'react';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { CoPilotContextProvider } from '@kbn/observability-plugin/public';
import { CoPilotService } from '@kbn/observability-plugin/public/typings/co_pilot';
import {
ObservabilityAIAssistantProvider,
ObservabilityAIAssistantPluginStart,
} from '@kbn/observability-ai-assistant-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { NavigationWarningPromptProvider } from '@kbn/observability-shared-plugin/public';
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
@ -24,13 +26,13 @@ export const CommonInfraProviders: React.FC<{
appName: string;
storage: Storage;
triggersActionsUI: TriggersAndActionsUIPublicPluginStart;
observabilityCopilot: CoPilotService;
observabilityAIAssistant: ObservabilityAIAssistantPluginStart;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
theme$: AppMountParameters['theme$'];
}> = ({
children,
triggersActionsUI,
observabilityCopilot,
observabilityAIAssistant,
setHeaderActionMenu,
appName,
storage,
@ -42,11 +44,11 @@ export const CommonInfraProviders: React.FC<{
<TriggersActionsProvider triggersActionsUI={triggersActionsUI}>
<EuiThemeProvider darkMode={darkMode}>
<DataUIProviders appName={appName} storage={storage}>
<CoPilotContextProvider value={observabilityCopilot}>
<ObservabilityAIAssistantProvider value={observabilityAIAssistant}>
<HeaderActionMenuProvider setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
<NavigationWarningPromptProvider>{children}</NavigationWarningPromptProvider>
</HeaderActionMenuProvider>
</CoPilotContextProvider>
</ObservabilityAIAssistantProvider>
</DataUIProviders>
</EuiThemeProvider>
</TriggersActionsProvider>

View file

@ -67,7 +67,7 @@ const LogsApp: React.FC<{
storage={storage}
theme$={theme$}
triggersActionsUI={plugins.triggersActionsUi}
observabilityCopilot={plugins.observability.getCoPilotService()}
observabilityAIAssistant={plugins.observabilityAIAssistant}
>
<Router history={history}>
<KbnUrlStateStorageFromRouterProvider

View file

@ -71,7 +71,7 @@ const MetricsApp: React.FC<{
storage={storage}
theme$={theme$}
triggersActionsUI={plugins.triggersActionsUi}
observabilityCopilot={plugins.observability.getCoPilotService()}
observabilityAIAssistant={plugins.observabilityAIAssistant}
>
<SourceProvider sourceId="default">
<Router history={history}>

View file

@ -213,7 +213,7 @@ const ProcessesTableBody = ({ items, currentTime }: TableBodyProps) => (
{column.render ? column.render(item[column.field], currentTime) : item[column.field]}
</EuiTableRowCell>
));
return <ProcessRow cells={cells} item={item} key={`row-${i}`} supportCopilot={true} />;
return <ProcessRow cells={cells} item={item} key={`row-${i}`} supportAIAssistant={true} />;
})}
</>
);

View file

@ -22,34 +22,97 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { useCoPilot, CoPilotPrompt } from '@kbn/observability-plugin/public';
import { CoPilotPromptId } from '@kbn/observability-plugin/common';
import useToggle from 'react-use/lib/useToggle';
import {
useObservabilityAIAssistant,
type Message,
MessageRole,
ContextualInsight,
} from '@kbn/observability-ai-assistant-plugin/public';
import { Process } from './types';
import { ProcessRowCharts } from './process_row_charts';
interface Props {
cells: React.ReactNode[];
item: Process;
supportCopilot?: boolean;
supportAIAssistant?: boolean;
}
export const CopilotProcessRow = ({ command }: { command: string }) => {
const coPilotService = useCoPilot();
const explainProcessParams = useMemo(() => {
return command ? { command } : undefined;
export const ContextualInsightProcessRow = ({ command }: { command: string }) => {
const aiAssistant = useObservabilityAIAssistant();
const explainProcessMessages = useMemo<Message[] | undefined>(() => {
if (!command) {
return undefined;
}
const now = new Date().toISOString();
return [
{
'@timestamp': now,
message: {
role: MessageRole.System,
content: `You are infra-gpt, a helpful assistant for metrics-based infrastructure observability. Answer as
concisely as possible.`,
},
},
{
'@timestamp': now,
message: {
role: MessageRole.User,
content: `I am a software engineer. I am trying to understand what a process running on my
machine does.
Your task is to first describe what the process is and what its general use cases are. If I also provide you
with the arguments to the process you should then explain its arguments and how they influence the behaviour
of the process. If I do not provide any arguments then explain the behaviour of the process when no arguments are
provided.
If you do not recognise the process say "No information available for this process". If I provide an argument
to the process that you do not recognise then say "No information available for this argument" when explaining
that argument.
Here is an example with arguments.
Process: metricbeat -c /etc/metricbeat.yml -d autodiscover,kafka -e -system.hostfs=/hostfs
Explanation: Metricbeat is part of the Elastic Stack. It is a lightweight shipper that you can install on your
servers to periodically collect metrics from the operating system and from services running on the server.
Use cases for Metricbeat generally revolve around infrastructure monitoring. You would typically install
Metricbeat on your servers to collect metrics from your systems and services. These metrics are then
used for performance monitoring, anomaly detection, system status checks, etc.
Here is a breakdown of the arguments used:
* -c /etc/metricbeat.yml: The -c option is used to specify the configuration file for Metricbeat. In
this case, /etc/metricbeat.yml is the configuration file. This file contains configurations for what
metrics to collect and where to send them (e.g., to Elasticsearch or Logstash).
* -d autodiscover,kafka: The -d option is used to enable debug output for selected components. In
this case, debug output is enabled for autodiscover and kafka components. The autodiscover feature
allows Metricbeat to automatically discover services as they get started and stopped in your environment,
and kafka is presumably a monitored service from which Metricbeat collects metrics.
* -e: The -e option is used to log to stderr and disable syslog/file output. This is useful for debugging.
* -system.hostfs=/hostfs: The -system.hostfs option is used to set the mount point of the hosts
filesystem for use in monitoring a host from within a container. In this case, /hostfs is the mount
point. When running Metricbeat inside a container, filesystem metrics would be for the container by
default, but with this option, Metricbeat can get metrics for the host system.
Here is an example without arguments.
Process: metricbeat
Explanation: Metricbeat is part of the Elastic Stack. It is a lightweight shipper that you can install on your
servers to periodically collect metrics from the operating system and from services running on the server.
Use cases for Metricbeat generally revolve around infrastructure monitoring. You would typically install
Metricbeat on your servers to collect metrics from your systems and services. These metrics are then
used for performance monitoring, anomaly detection, system status checks, etc.
Running it without any arguments will start the process with the default configuration file, typically
located at /etc/metricbeat/metricbeat.yml. This file specifies the metrics to be collected and where
to ship them to.
Now explain this process to me.
Process: ${command}
Explanation:
`,
},
},
];
}, [command]);
return (
<>
{coPilotService?.isEnabled() && explainProcessParams ? (
{aiAssistant.isEnabled() && explainProcessMessages ? (
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexItem grow={false}>
<CoPilotPrompt
coPilot={coPilotService}
<ContextualInsight
title={explainProcessMessageTitle}
params={explainProcessParams}
promptId={CoPilotPromptId.InfraExplainProcess}
feedbackEnabled={true}
messages={explainProcessMessages}
/>
</EuiFlexItem>
</EuiFlexItem>
@ -59,7 +122,7 @@ export const CopilotProcessRow = ({ command }: { command: string }) => {
);
};
export const ProcessRow = ({ cells, item, supportCopilot = false }: Props) => {
export const ProcessRow = ({ cells, item, supportAIAssistant = false }: Props) => {
const [isExpanded, toggle] = useToggle(false);
return (
@ -135,7 +198,7 @@ export const ProcessRow = ({ cells, item, supportCopilot = false }: Props) => {
</EuiFlexItem>
<ProcessRowCharts command={item.command} />
</EuiFlexGrid>
{supportCopilot && <CopilotProcessRow command={item.command} />}
{supportAIAssistant && <ContextualInsightProcessRow command={item.command} />}
</ExpandedRowDescriptionList>
</ExpandedRowCell>
)}

View file

@ -44,6 +44,7 @@ import {
} from '@kbn/logs-shared-plugin/public';
import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public';
import type { UnwrapPromise } from '../common/utility_types';
import type {
SourceProviderProps,
@ -104,6 +105,7 @@ export interface InfraClientStartDeps {
ml: MlPluginStart;
observability: ObservabilityPublicStart;
observabilityShared: ObservabilitySharedPluginStart;
observabilityAIAssistant: ObservabilityAIAssistantPluginStart;
osquery?: unknown; // OsqueryPluginStart - can't be imported due to cyclic dependency;
share: SharePluginStart;
spaces: SpacesPluginStart;

View file

@ -61,6 +61,7 @@
"@kbn/discover-plugin",
"@kbn/observability-alert-details",
"@kbn/observability-shared-plugin",
"@kbn/observability-ai-assistant-plugin",
"@kbn/ui-theme",
"@kbn/ml-anomaly-utils",
"@kbn/aiops-plugin",

View file

@ -8,10 +8,9 @@
"server": true,
"browser": true,
"configPath": ["xpack", "logs_shared"],
"requiredPlugins": ["data", "dataViews", "usageCollection", "observabilityShared"],
"requiredPlugins": ["data", "dataViews", "usageCollection", "observabilityShared", "observabilityAIAssistant"],
"optionalPlugins": ["observability"],
"requiredBundles": [
"observability",
"kibanaUtils",
"kibanaReact",
],

View file

@ -15,21 +15,21 @@ import {
EuiTextColor,
EuiTitle,
} from '@elastic/eui';
import { OverlayRef } from '@kbn/core/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Query } from '@kbn/es-query';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { OverlayRef } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { createKibanaReactContext, useKibana } from '@kbn/kibana-react-plugin/public';
import {
useCoPilot,
CoPilotPrompt,
ObservabilityPublicStart,
CoPilotContextProvider,
} from '@kbn/observability-plugin/public';
import { CoPilotPromptId } from '@kbn/observability-plugin/common';
ContextualInsight,
MessageRole,
ObservabilityAIAssistantPluginStart,
ObservabilityAIAssistantProvider,
useObservabilityAIAssistant,
type Message,
} from '@kbn/observability-ai-assistant-plugin/public';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { LogViewReference } from '../../../../common/log_views';
import { TimeKey } from '../../../../common/time';
import { useLogEntry } from '../../../containers/logs/log_entry';
@ -39,6 +39,12 @@ import { DataSearchProgress } from '../../data_search_progress';
import { LogEntryActionsMenu } from './log_entry_actions_menu';
import { LogEntryFieldsTable } from './log_entry_fields_table';
const LOGS_SYSTEM_MESSAGE = {
content: `You are logs-gpt, a helpful assistant for logs-based observability. Answer as
concisely as possible.`,
role: MessageRole.System,
};
export interface LogEntryFlyoutProps {
logEntryId: string | null | undefined;
onCloseFlyout: () => void;
@ -49,9 +55,12 @@ export interface LogEntryFlyoutProps {
export const useLogEntryFlyout = (logViewReference: LogViewReference) => {
const flyoutRef = useRef<OverlayRef>();
const {
services: { http, data, uiSettings, application, observability },
services: { http, data, uiSettings, application, observabilityAIAssistant },
overlays: { openFlyout },
} = useKibana<{ data: DataPublicPluginStart; observability?: ObservabilityPublicStart }>();
} = useKibana<{
data: DataPublicPluginStart;
observabilityAIAssistant?: ObservabilityAIAssistantPluginStart;
}>();
const closeLogEntryFlyout = useCallback(() => {
flyoutRef.current?.close();
@ -68,13 +77,13 @@ export const useLogEntryFlyout = (logViewReference: LogViewReference) => {
flyoutRef.current = openFlyout(
<KibanaReactContextProvider>
<CoPilotContextProvider value={observability?.getCoPilotService()}>
<ObservabilityAIAssistantProvider value={observabilityAIAssistant}>
<LogEntryFlyout
logEntryId={logEntryId}
onCloseFlyout={closeLogEntryFlyout}
logViewReference={logViewReference}
/>
</CoPilotContextProvider>
</ObservabilityAIAssistantProvider>
</KibanaReactContextProvider>
);
},
@ -86,7 +95,7 @@ export const useLogEntryFlyout = (logViewReference: LogViewReference) => {
openFlyout,
logViewReference,
closeLogEntryFlyout,
observability,
observabilityAIAssistant,
]
);
@ -127,15 +136,55 @@ export const LogEntryFlyout = ({
}
}, [fetchLogEntry, logViewReference, logEntryId]);
const explainLogMessageParams = useMemo(() => {
return logEntry ? { logEntry: { fields: logEntry.fields } } : undefined;
const explainLogMessageMessages = useMemo<Message[] | undefined>(() => {
if (!logEntry) {
return undefined;
}
const now = new Date().toISOString();
return [
{
'@timestamp': now,
message: LOGS_SYSTEM_MESSAGE,
},
{
'@timestamp': now,
message: {
role: MessageRole.User,
content: `I'm looking at a log entry. Can you explain me what the log message means? Where it could be coming from, whether it is expected and whether it is an issue. Here's the context, serialized: ${JSON.stringify(
{ logEntry: { fields: logEntry.fields } }
)} `,
},
},
];
}, [logEntry]);
const similarLogMessageParams = useMemo(() => {
return logEntry ? { logEntry: { fields: logEntry.fields } } : undefined;
const similarLogMessageMessages = useMemo<Message[] | undefined>(() => {
if (!logEntry) {
return undefined;
}
const now = new Date().toISOString();
const message = logEntry.fields.find((field) => field.field === 'message')?.value[0];
return [
{
'@timestamp': now,
message: LOGS_SYSTEM_MESSAGE,
},
{
'@timestamp': now,
message: {
role: MessageRole.User,
content: `I'm looking at a log entry. Can you construct a Kibana KQL query that I can enter in the search bar that gives me similar log entries, based on the \`message\` field: ${message}`,
},
},
];
}, [logEntry]);
const coPilotService = useCoPilot();
const aiAssistant = useObservabilityAIAssistant();
return (
<EuiFlyout onClose={onCloseFlyout} size="m">
@ -197,25 +246,19 @@ export const LogEntryFlyout = ({
}
>
<EuiFlexGroup direction="column" gutterSize="m">
{coPilotService?.isEnabled() && explainLogMessageParams ? (
{aiAssistant.isEnabled() && explainLogMessageMessages ? (
<EuiFlexItem grow={false}>
<CoPilotPrompt
coPilot={coPilotService}
<ContextualInsight
title={explainLogMessageTitle}
params={explainLogMessageParams}
promptId={CoPilotPromptId.LogsExplainMessage}
feedbackEnabled={false}
messages={explainLogMessageMessages}
/>
</EuiFlexItem>
) : null}
{coPilotService?.isEnabled() && similarLogMessageParams ? (
{aiAssistant.isEnabled() && similarLogMessageMessages ? (
<EuiFlexItem grow={false}>
<CoPilotPrompt
coPilot={coPilotService}
<ContextualInsight
title={similarLogMessagesTitle}
params={similarLogMessageParams}
promptId={CoPilotPromptId.LogsFindSimilar}
feedbackEnabled={false}
messages={similarLogMessageMessages}
/>
</EuiFlexItem>
) : null}

View file

@ -23,9 +23,9 @@
"@kbn/kibana-react-plugin",
"@kbn/test-subj-selector",
"@kbn/observability-shared-plugin",
"@kbn/observability-plugin",
"@kbn/datemath",
"@kbn/core-http-browser",
"@kbn/ui-actions-plugin",
"@kbn/observability-ai-assistant-plugin",
]
}

View file

@ -1,28 +0,0 @@
/*
* 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.
*/
export enum OpenAIProvider {
OpenAI = 'openAI',
AzureOpenAI = 'azureOpenAI',
}
export enum CoPilotPromptId {
ProfilingOptimizeFunction = 'profilingOptimizeFunction',
ApmExplainError = 'apmExplainError',
LogsExplainMessage = 'logsExplainMessage',
LogsFindSimilar = 'logsFindSimilar',
InfraExplainProcess = 'infraExplainProcess',
ExplainLogSpike = 'explainLogSpike',
}
export type {
CoPilotPromptMap,
CreateChatCompletionResponseChunk,
PromptParamsOf,
} from './prompts';
export const loadCoPilotPrompts = () => import('./prompts').then((m) => m.coPilotPrompts);

View file

@ -1,332 +0,0 @@
/*
* 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 * as t from 'io-ts';
import type {
ChatCompletionRequestMessage,
CreateChatCompletionResponse,
CreateChatCompletionResponseChoicesInner,
} from 'openai';
import { CoPilotPromptId } from '.';
const PERF_GPT_SYSTEM_MESSAGE = {
content: `You are perf-gpt, a helpful assistant for performance analysis and optimisation
of software. Answer as concisely as possible.`,
role: 'system' as const,
};
const APM_GPT_SYSTEM_MESSAGE = {
content: `You are apm-gpt, a helpful assistant for performance analysis, optimisation and
root cause analysis of software. Answer as concisely as possible.`,
role: 'system' as const,
};
const LOGS_SYSTEM_MESSAGE = {
content: `You are logs-gpt, a helpful assistant for logs-based observability. Answer as
concisely as possible.`,
role: 'system' as const,
};
const INFRA_SYSTEM_MESSAGE = {
content: `You are infra-gpt, a helpful assistant for metrics-based infrastructure observability. Answer as
concisely as possible.`,
role: 'system' as const,
};
function prompt<TParams extends t.Type<any, any, any>>({
params,
messages,
}: {
params: TParams;
messages: (params: t.OutputOf<TParams>) => ChatCompletionRequestMessage[];
}) {
return {
params,
messages,
};
}
const logEntryRt = t.type({
fields: t.array(
t.type({
field: t.string,
value: t.array(t.any),
})
),
});
const significantFieldValuesRt = t.array(
t.type({
field: t.string,
value: t.union([t.string, t.number]),
docCount: t.number,
pValue: t.union([t.number, t.null]),
})
);
export const coPilotPrompts = {
[CoPilotPromptId.ProfilingOptimizeFunction]: prompt({
params: t.type({
library: t.string,
functionName: t.string,
}),
messages: ({ library, functionName }) => {
return [
PERF_GPT_SYSTEM_MESSAGE,
{
content: `I am a software engineer. I am trying to understand what a function in a particular
software library does.
The library is: ${library}
The function is: ${functionName}
Your have two tasks. Your first task is to desribe what the library is and what its use cases are, and to
describe what the function does. The output format should look as follows:
Library description: Provide a concise description of the library
Library use-cases: Provide a concise description of what the library is typically used for.
Function description: Provide a concise, technical, description of what the function does.
Assume the function ${functionName} from the library ${library} is consuming significant CPU resources.
Your second task is to suggest ways to optimize or improve the system that involve the ${functionName} function from the
${library} library. Types of improvements that would be useful to me are improvements that result in:
- Higher performance so that the system runs faster or uses less CPU
- Better memory efficient so that the system uses less RAM
- Better storage efficient so that the system stores less data on disk.
- Better network I/O efficiency so that less data is sent over the network
- Better disk I/O efficiency so that less data is read and written from disk
Make up to five suggestions. Your suggestions must meet all of the following criteria:
1. Your suggestions should detailed, technical and include concrete examples.
2. Your suggestions should be specific to improving performance of a system in which the ${functionName} function from
the ${library} library is consuming significant CPU.
3. If you suggest replacing the function or library with a more efficient replacement you must suggest at least
one concrete replacement.
If you know of fewer than five ways to improve the performance of a system in which the ${functionName} function from the
${library} library is consuming significant CPU, then provide fewer than five suggestions. If you do not know of any
way in which to improve the performance then say "I do not know how to improve the performance of systems where
this function is consuming a significant amount of CPU".
Do not suggest using a CPU profiler. I have already profiled my code. The profiler I used is Elastic Universal Profiler.
If there is specific information I should look for in the profiler output then tell me what information to look for
in the output of Elastic Universal Profiler.
You must not include URLs, web addresses or websites of any kind in your output.
If you have suggestions, the output format should look as follows:
Here are some suggestions as to how you might optimize your system if ${functionName} in ${library} is consuming
significant CPU resources:
1. Insert first suggestion
2. Insert second suggestion`,
role: 'user',
},
];
},
}),
[CoPilotPromptId.ApmExplainError]: prompt({
params: t.intersection([
t.type({
serviceName: t.string,
languageName: t.string,
runtimeName: t.string,
runtimeVersion: t.string,
transactionName: t.string,
logStacktrace: t.string,
exceptionStacktrace: t.string,
}),
t.partial({
spanName: t.string,
}),
]),
messages: ({
serviceName,
languageName,
runtimeName,
runtimeVersion,
transactionName,
logStacktrace,
exceptionStacktrace,
}) => {
return [
APM_GPT_SYSTEM_MESSAGE,
{
content: `I'm an SRE. I am looking at an exception and trying to understand what it means.
Your task is to describe what the error means and what it could be caused by.
The error occurred on a service called ${serviceName}, which is a ${runtimeName} service written in ${languageName}. The
runtime version is ${runtimeVersion}.
The request it occurred for is called ${transactionName}.
${
logStacktrace
? `The log stacktrace:
${logStacktrace}`
: ''
}
${
exceptionStacktrace
? `The exception stacktrace:
${exceptionStacktrace}`
: ''
}
`,
role: 'user',
},
];
},
}),
[CoPilotPromptId.LogsExplainMessage]: prompt({
params: t.type({
logEntry: logEntryRt,
}),
messages: ({ logEntry }) => {
return [
LOGS_SYSTEM_MESSAGE,
{
content: `I'm looking at a log entry. Can you explain me what the log message means? Where it could be coming from, whether it is expected and whether it is an issue. Here's the context, serialized: ${JSON.stringify(
logEntry
)} `,
role: 'user',
},
];
},
}),
[CoPilotPromptId.LogsFindSimilar]: prompt({
params: t.type({
logEntry: logEntryRt,
}),
messages: ({ logEntry }) => {
const message = logEntry.fields.find((field) => field.field === 'message')?.value[0];
return [
LOGS_SYSTEM_MESSAGE,
{
content: `I'm looking at a log entry. Can you construct a Kibana KQL query that I can enter in the search bar that gives me similar log entries, based on the \`message\` field: ${message}`,
role: 'user',
},
];
},
}),
[CoPilotPromptId.InfraExplainProcess]: prompt({
params: t.type({
command: t.string,
}),
messages: ({ command }) => {
return [
INFRA_SYSTEM_MESSAGE,
{
content: `I am a software engineer. I am trying to understand what a process running on my
machine does.
Your task is to first describe what the process is and what its general use cases are. If I also provide you
with the arguments to the process you should then explain its arguments and how they influence the behaviour
of the process. If I do not provide any arguments then explain the behaviour of the process when no arguments are
provided.
If you do not recognise the process say "No information available for this process". If I provide an argument
to the process that you do not recognise then say "No information available for this argument" when explaining
that argument.
Here is an example with arguments.
Process: metricbeat -c /etc/metricbeat.yml -d autodiscover,kafka -e -system.hostfs=/hostfs
Explanation: Metricbeat is part of the Elastic Stack. It is a lightweight shipper that you can install on your
servers to periodically collect metrics from the operating system and from services running on the server.
Use cases for Metricbeat generally revolve around infrastructure monitoring. You would typically install
Metricbeat on your servers to collect metrics from your systems and services. These metrics are then
used for performance monitoring, anomaly detection, system status checks, etc.
Here is a breakdown of the arguments used:
* -c /etc/metricbeat.yml: The -c option is used to specify the configuration file for Metricbeat. In
this case, /etc/metricbeat.yml is the configuration file. This file contains configurations for what
metrics to collect and where to send them (e.g., to Elasticsearch or Logstash).
* -d autodiscover,kafka: The -d option is used to enable debug output for selected components. In
this case, debug output is enabled for autodiscover and kafka components. The autodiscover feature
allows Metricbeat to automatically discover services as they get started and stopped in your environment,
and kafka is presumably a monitored service from which Metricbeat collects metrics.
* -e: The -e option is used to log to stderr and disable syslog/file output. This is useful for debugging.
* -system.hostfs=/hostfs: The -system.hostfs option is used to set the mount point of the hosts
filesystem for use in monitoring a host from within a container. In this case, /hostfs is the mount
point. When running Metricbeat inside a container, filesystem metrics would be for the container by
default, but with this option, Metricbeat can get metrics for the host system.
Here is an example without arguments.
Process: metricbeat
Explanation: Metricbeat is part of the Elastic Stack. It is a lightweight shipper that you can install on your
servers to periodically collect metrics from the operating system and from services running on the server.
Use cases for Metricbeat generally revolve around infrastructure monitoring. You would typically install
Metricbeat on your servers to collect metrics from your systems and services. These metrics are then
used for performance monitoring, anomaly detection, system status checks, etc.
Running it without any arguments will start the process with the default configuration file, typically
located at /etc/metricbeat/metricbeat.yml. This file specifies the metrics to be collected and where
to ship them to.
Now explain this process to me.
Process: ${command}
Explanation:
`,
role: 'user',
},
];
},
}),
[CoPilotPromptId.ExplainLogSpike]: prompt({
params: t.type({
significantFieldValues: significantFieldValuesRt,
}),
messages: ({ significantFieldValues }) => {
const header = 'Field name,Field value,Doc count,p-value';
const rows = significantFieldValues.map((item) => Object.values(item).join(',')).join('\n');
const content = `You are an observability expert using Elastic Observability Suite on call being consulted about a log threshold alert that got triggered by a spike of log messages. Your job is to take immediate action and proceed with both urgency and precision.
"Log Rate Analysis" is an AIOps feature that uses advanced statistical methods to identify reasons for increases in log rates. It makes it easy to find and investigate causes of unusual spikes by using the analysis workflow view.
You are using "Log Rate Analysis" and ran the statistical analysis on the log messages which occured during the alert.
You received the following analysis results from "Log Rate Analysis" which list statistically significant co-occuring field/value combinations sorted from most significant (lower p-values) to least significant (higher p-values) that contribute to the log messages spike:
${header}
${rows}
Based on the above analysis results and your observability expert knowledge, output the following:
Analyse the type of these logs and explain their usual purpose (1 paragraph).
Based on the type of these logs do a root cause analysis on why the field and value combinations from the anlaysis results are causing this spike in logs (2 parapraphs).
Recommend concrete remediations to resolve the root cause (3 bullet points).
Do not repeat the given instructions in your output.`;
return [
LOGS_SYSTEM_MESSAGE,
{
content,
role: 'user',
},
];
},
}),
};
export type CoPilotPromptMap = typeof coPilotPrompts;
export type PromptParamsOf<TPromptId extends CoPilotPromptId> = t.OutputOf<
{
[TKey in keyof CoPilotPromptMap]: CoPilotPromptMap[TKey];
}[TPromptId]['params']
>;
export type CreateChatCompletionResponseChunk = Omit<CreateChatCompletionResponse, 'choices'> & {
choices: Array<
Omit<CreateChatCompletionResponseChoicesInner, 'message'> & {
delta: { content?: string };
}
>;
};

View file

@ -68,5 +68,3 @@ export const sloDetailsLocatorID = 'SLO_DETAILS_LOCATOR';
export const sloEditLocatorID = 'SLO_EDIT_LOCATOR';
export type { AlertsLocatorParams } from './locators/alerts';
export { CoPilotPromptId, loadCoPilotPrompts } from './co_pilot';

View file

@ -1,104 +0,0 @@
# CoPilotPrompt
CoPilotPrompt is a React component that allows for interaction with OpenAI-compatible APIs. The component supports streaming of responses and basic error handling. As of now, it doesn't support chat or any kind of persistence. We will likely add a feedback button before the first release.
## Usage
### Step 1: Define a Prompt
Firstly, define a prompt in `x-pack/plugins/observability/common/co_pilot.ts`.
```typescript
[CoPilotPromptId.ProfilingExplainFunction]: prompt({
params: t.type({
library: t.string,
functionName: t.string,
}),
messages: ({ library, functionName }) => {
return [
PERF_GPT_SYSTEM_MESSAGE,
{
content: `I am a software engineer. I am trying to understand what a function in a particular
software library does.
The library is: ${library}
The function is: ${functionName}
Your task is to describe what the library is and what its use cases are, and to describe what the function
does. The output format should look as follows:
Library description: Provide a concise description of the library
Library use-cases: Provide a concise description of what the library is typically used for.
Function description: Provide a concise, technical, description of what the function does.
`,
role: 'user',
},
];
},
});
```
Here, the key is a prompt ID, `params` define the expected inputs, and `PERF_GPT_SYSTEM_MESSAGE` is used to instruct ChatGPT's role.
### Step 2: Wrap your app in CoPilotContextProvider
Next, we need to make the CoPilot service available through context, so we can use it in our components. Wrap your app in the CoPilotContextProvider, by calling `getCoPilotService()` from the Observability plugin setup contract:
```typescript
function renderMyApp(pluginsSetup) {
const coPilotService = pluginsSetup.observability.getCoPilotService();
return (
<CoPilotContextProvider value={coPilotService}>
<MyApp />
</CoPilotContextProvider>
);
}
```
### Step 2: Retrieve the CoPilot Service
You can use the `useCoPilot` hook from `@kbn/observability-plugin/public` to retrieve the co-pilot service.
```typescript
const coPilot = useCoPilot();
```
Note: `useCoPilot.isEnabled()` will return undefined if co-pilot has not been enabled. You can use this to render the `CoPilotPrompt` component conditionally.
### Step 3: Use the CoPilotPrompt Component
Finally, you can use the `CoPilotPrompt` component like so:
```jsx
{
coPilot.isEnabled() && (
<CoPilotPrompt
coPilot={coPilot}
promptId={CoPilotPromptId.ProfilingExplainFunction}
params={promptParams}
title={i18n.translate('xpack.profiling.frameInformationWindow.explainFunction', {
defaultMessage: 'Explain function',
})}
/>
);
}
```
## Properties
### coPilot
A `CoPilotService` instance. This is required for establishing connection with the OpenAI-compatible API.
### promptId
A unique identifier for the prompt. This should match one of the keys you defined in `x-pack/plugins/observability/common/co_pilot.ts`.
### params
Parameters for the prompt. These should align with the `params` in the prompt definition.
### title
The title that will be displayed on the component. It can be a simple string or a localized string via the `i18n` library.

View file

@ -1,220 +0,0 @@
/*
* 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 {
EuiAccordion,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiLoadingSpinner,
EuiPanel,
EuiSpacer,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/css';
import { i18n } from '@kbn/i18n';
import { TechnicalPreviewBadge } from '@kbn/observability-shared-plugin/public';
import type { ChatCompletionRequestMessage } from 'openai';
import React, { useMemo, useState } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { catchError, Observable, of } from 'rxjs';
import { CoPilotPromptId } from '../../../common';
import type { PromptParamsOf } from '../../../common/co_pilot';
import type { CoPilotService, PromptObservableState } from '../../typings/co_pilot';
import { CoPilotPromptFeedback } from './co_pilot_prompt_feedback';
const cursorCss = css`
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
animation: blink 1s infinite;
width: 10px;
height: 16px;
vertical-align: middle;
display: inline-block;
background: rgba(0, 0, 0, 0.25);
`;
export interface CoPilotPromptProps<TPromptId extends CoPilotPromptId> {
title: string;
promptId: TPromptId;
coPilot: CoPilotService;
params: PromptParamsOf<TPromptId>;
feedbackEnabled: boolean;
}
// eslint-disable-next-line import/no-default-export
export default function CoPilotPrompt<TPromptId extends CoPilotPromptId>({
title,
coPilot,
promptId,
params,
feedbackEnabled,
}: CoPilotPromptProps<TPromptId>) {
const [hasOpened, setHasOpened] = useState(false);
const theme = useEuiTheme();
const [responseTime, setResponseTime] = useState<number | undefined>(undefined);
const conversation$ = useMemo(() => {
if (hasOpened) {
setResponseTime(undefined);
const now = Date.now();
const observable = coPilot.prompt(promptId, params).pipe(
catchError((err) =>
of({
messages: [] as ChatCompletionRequestMessage[],
loading: false,
error: err,
message: String(err.message),
})
)
);
observable.subscribe({
complete: () => {
setResponseTime(Date.now() - now);
},
});
return observable;
}
return new Observable<PromptObservableState & { error?: any }>(() => {});
}, [params, promptId, coPilot, hasOpened, setResponseTime]);
const conversation = useObservable(conversation$);
const content = conversation?.message ?? '';
const messages = conversation?.messages;
let state: 'init' | 'loading' | 'streaming' | 'error' | 'complete' = 'init';
if (conversation?.loading) {
state = content ? 'streaming' : 'loading';
} else if (conversation && 'error' in conversation && conversation.error) {
state = 'error';
} else if (content) {
state = 'complete';
}
let inner: React.ReactElement;
if (state === 'complete' || state === 'streaming') {
inner = (
<>
<p style={{ whiteSpace: 'pre-wrap', lineHeight: 1.5 }}>
{content}
{state === 'streaming' ? <span className={cursorCss} /> : undefined}
</p>
{state === 'complete' ? (
<>
<EuiSpacer size="m" />
{coPilot.isTrackingEnabled() && feedbackEnabled ? (
<CoPilotPromptFeedback
messages={messages}
response={content}
responseTime={responseTime!}
promptId={promptId}
coPilot={coPilot}
/>
) : undefined}
</>
) : undefined}
</>
);
} else if (state === 'init' || state === 'loading') {
inner = (
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
{i18n.translate('xpack.observability.coPilotPrompt.chatLoading', {
defaultMessage: 'Waiting for a response...',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
} else {
/* if (state === 'error') {*/
inner = (
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon color="danger" type="warning" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{content}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<EuiPanel color="primary">
<EuiAccordion
id={title}
css={css`
.euiButtonIcon {
color: ${theme.euiTheme.colors.primaryText};
}
`}
buttonClassName={css`
display: block;
width: 100%;
`}
buttonContent={
<EuiFlexGroup direction="row" alignItems="center">
<EuiFlexItem grow>
<EuiFlexGroup direction="column" gutterSize="none" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiText size="m" color={theme.euiTheme.colors.primaryText}>
<strong>{title}</strong>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color={theme.euiTheme.colors.primaryText}>
{i18n.translate('xpack.observability.coPilotChatPrompt.subtitle', {
defaultMessage: 'Get helpful insights from our Elastic AI Assistant',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TechnicalPreviewBadge />
</EuiFlexItem>
</EuiFlexGroup>
}
initialIsOpen={false}
onToggle={() => {
setHasOpened(true);
}}
>
<EuiSpacer size="m" />
<EuiHorizontalRule margin="none" />
<EuiSpacer size="m" />
{inner}
</EuiAccordion>
</EuiPanel>
);
}

View file

@ -1,131 +0,0 @@
/*
* 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 {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { ChatCompletionRequestMessage } from 'openai';
import React, { useCallback, useEffect, useState } from 'react';
import { CoPilotPromptId } from '../../../common';
import type { CoPilotService } from '../../typings/co_pilot';
interface Props {
coPilot: CoPilotService;
promptId: CoPilotPromptId;
messages?: ChatCompletionRequestMessage[];
response: string;
responseTime: number;
}
export function CoPilotPromptFeedback({
coPilot,
promptId,
messages,
response,
responseTime,
}: Props) {
const theme = useEuiTheme();
const [hasSubmittedFeedback, setHasSubmittedFeedback] = useState(false);
const submitFeedback = useCallback(
(positive: boolean) => {
setHasSubmittedFeedback(true);
if (messages) {
coPilot
.track({
messages,
response,
responseTime,
promptId,
feedbackAction: positive ? 'thumbsup' : 'thumbsdown',
})
.catch((err) => {});
}
},
[coPilot, promptId, messages, response, responseTime]
);
const [hasSubmittedTelemetry, setHasSubmittedTelemetry] = useState(false);
useEffect(() => {
if (!hasSubmittedTelemetry && messages) {
setHasSubmittedTelemetry(true);
coPilot
.track({
messages,
response,
responseTime,
promptId,
})
.catch((err) => {});
}
}, [coPilot, promptId, messages, response, responseTime, hasSubmittedTelemetry]);
if (hasSubmittedFeedback) {
return (
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="check" color={theme.euiTheme.colors.primaryText} />
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" color={theme.euiTheme.colors.primaryText}>
{i18n.translate('xpack.observability.coPilotPrompt.feedbackSubmittedText', {
defaultMessage:
"Thank you for submitting your feedback! We'll use this to improve responses.",
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiText size="s" color={theme.euiTheme.colors.primaryText}>
{i18n.translate('xpack.observability.coPilotPrompt.feedbackActionTitle', {
defaultMessage: 'Did you find this response helpful?',
})}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
data-test-subj="CoPilotPromptButtonHappy"
iconType="faceHappy"
onClick={() => {
submitFeedback(true);
}}
>
{i18n.translate('xpack.observability.coPilotPrompt.likedFeedbackButtonTitle', {
defaultMessage: 'Yes',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
data-test-subj="CoPilotPromptButtonSad"
iconType="faceSad"
onClick={() => {
submitFeedback(false);
}}
>
{i18n.translate('xpack.observability.coPilotPrompt.dislikedFeedbackButtonTitle', {
defaultMessage: 'No',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -1,18 +0,0 @@
/*
* 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 React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
const LazyCoPilotPrompt = lazy(() => import('./co_pilot_prompt'));
export function CoPilotPrompt(props: React.ComponentProps<typeof LazyCoPilotPrompt>) {
return (
<Suspense fallback={<EuiLoadingSpinner size="s" />}>
<LazyCoPilotPrompt {...props} />
</Suspense>
);
}

View file

@ -1,159 +0,0 @@
/*
* 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 { type HttpSetup } from '@kbn/core/public';
import { ChatCompletionRequestMessage } from 'openai';
import { BehaviorSubject, concatMap, delay, of } from 'rxjs';
import {
type CreateChatCompletionResponseChunk,
loadCoPilotPrompts,
} from '../../../common/co_pilot';
import type { CoPilotService } from '../../typings/co_pilot';
function getMessageFromChunks(chunks: CreateChatCompletionResponseChunk[]) {
let message = '';
chunks.forEach((chunk) => {
message += chunk.choices[0]?.delta.content ?? '';
});
return message;
}
export function createCoPilotService({
enabled,
trackingEnabled,
http,
}: {
enabled: boolean;
trackingEnabled: boolean;
http: HttpSetup;
}) {
const service: CoPilotService = {
isEnabled: () => enabled,
isTrackingEnabled: () => trackingEnabled,
prompt: (promptId, params) => {
const subject = new BehaviorSubject({
messages: [] as ChatCompletionRequestMessage[],
loading: true,
message: '',
});
loadCoPilotPrompts()
.then((coPilotPrompts) => {
const messages = coPilotPrompts[promptId].messages(params as any);
subject.next({
messages,
loading: true,
message: '',
});
http
.post(`/internal/observability/copilot/prompts/${promptId}`, {
body: JSON.stringify(params),
asResponse: true,
rawResponse: true,
})
.then((response) => {
const status = response.response?.status;
if (!status || status >= 400) {
throw new Error(response.response?.statusText || 'Unexpected error');
}
const reader = response.response.body?.getReader();
if (!reader) {
throw new Error('Could not get reader from response');
}
const decoder = new TextDecoder();
const chunks: CreateChatCompletionResponseChunk[] = [];
let prev: string = '';
function read() {
reader!.read().then(({ done, value }) => {
try {
if (done) {
subject.next({
messages,
message: getMessageFromChunks(chunks),
loading: false,
});
subject.complete();
return;
}
let lines = (prev + decoder.decode(value)).split('\n');
const lastLine = lines[lines.length - 1];
const isPartialChunk = !!lastLine && lastLine !== 'data: [DONE]';
if (isPartialChunk) {
prev = lastLine;
lines.pop();
} else {
prev = '';
}
lines = lines
.map((str) => str.substr(6))
.filter((str) => !!str && str !== '[DONE]');
const nextChunks: CreateChatCompletionResponseChunk[] = lines.map((line) =>
JSON.parse(line)
);
nextChunks.forEach((chunk) => {
chunks.push(chunk);
subject.next({
messages,
message: getMessageFromChunks(chunks),
loading: true,
});
});
} catch (err) {
subject.error(err);
return;
}
read();
});
}
read();
})
.catch(async (err) => {
if ('response' in err) {
try {
const responseBody = await err.response.json();
err.message = responseBody.message;
} catch {
// leave message as-is
}
}
subject.error(err);
});
})
.catch((err) => {});
return subject.pipe(concatMap((value) => of(value).pipe(delay(25))));
},
track: async ({ messages, response, responseTime, feedbackAction, promptId }) => {
await http.post(`/internal/observability/copilot/prompts/${promptId}/track`, {
body: JSON.stringify({
response,
feedbackAction,
messages,
responseTime,
}),
});
},
};
return service;
}

View file

@ -1,18 +0,0 @@
/*
* 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 { useContext } from 'react';
import { CoPilotContext } from '../context/co_pilot_context';
export function useCoPilot() {
const coPilotService = useContext(CoPilotContext);
// Ideally we throw, but we can't guarantee coPilotService being available
// in some embedded contexts
return coPilotService;
}

View file

@ -89,7 +89,3 @@ export { calculateTimeRangeBucketSize } from './pages/overview/helpers/calculate
export { convertTo } from '../common/utils/formatters/duration';
export { formatAlertEvaluationValue } from './utils/format_alert_evaluation_value';
export { CoPilotPrompt } from './components/co_pilot_prompt';
export { useCoPilot } from './hooks/use_co_pilot';
export { CoPilotContextProvider } from './context/co_pilot_context';

View file

@ -89,12 +89,6 @@ const withCore = makeDecorator({
thresholdRule: { enabled: false },
},
compositeSlo: { enabled: false },
aiAssistant: {
enabled: false,
feedback: {
enabled: false,
},
},
};
return (

View file

@ -71,8 +71,6 @@ import {
RULES_PATH,
SLOS_PATH,
} from './routes/paths';
import { createCoPilotService } from './context/co_pilot_context/create_co_pilot_service';
import { type CoPilotService } from './typings/co_pilot';
export interface ConfigSchema {
unsafe: {
@ -95,13 +93,8 @@ export interface ConfigSchema {
};
};
compositeSlo: { enabled: boolean };
aiAssistant?: {
enabled: boolean;
feedback: {
enabled: boolean;
};
};
}
export type ObservabilityPublicSetup = ReturnType<Plugin['setup']>;
export interface ObservabilityPublicPluginsSetup {
@ -153,8 +146,6 @@ export class Plugin
private observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry =
{} as ObservabilityRuleTypeRegistry;
private coPilotService: CoPilotService | undefined;
// Define deep links as constant and hidden. Whether they are shown or hidden
// in the global navigation will happen in `updateGlobalNavigation`.
private readonly deepLinks: AppDeepLink[] = [
@ -346,12 +337,6 @@ export class Plugin
)
);
this.coPilotService = createCoPilotService({
enabled: !!config.aiAssistant?.enabled,
http: coreSetup.http,
trackingEnabled: !!config.aiAssistant?.feedback.enabled,
});
return {
dashboard: { register: registerDataHandler },
observabilityRuleTypeRegistry: this.observabilityRuleTypeRegistry,
@ -360,7 +345,6 @@ export class Plugin
ruleDetailsLocator,
sloDetailsLocator,
sloEditLocator,
getCoPilotService: () => this.coPilotService!,
};
}
@ -390,7 +374,6 @@ export class Plugin
return {
observabilityRuleTypeRegistry: this.observabilityRuleTypeRegistry,
useRulesLink: createUseRulesLink(),
getCoPilotService: () => this.coPilotService!,
};
}
}

View file

@ -1,32 +0,0 @@
/*
* 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 type { ChatCompletionRequestMessage } from 'openai';
import type { Observable } from 'rxjs';
import type { CoPilotPromptId, PromptParamsOf } from '../../common/co_pilot';
export interface PromptObservableState {
message?: string;
messages: ChatCompletionRequestMessage[];
loading: boolean;
}
export interface CoPilotService {
isEnabled: () => boolean;
isTrackingEnabled: () => boolean;
prompt<TPromptId extends CoPilotPromptId>(
promptId: TPromptId,
params: PromptParamsOf<TPromptId>
): Observable<PromptObservableState>;
track: (options: {
messages: ChatCompletionRequestMessage[];
response: string;
promptId: CoPilotPromptId;
feedbackAction?: 'thumbsup' | 'thumbsdown';
responseTime: number;
}) => Promise<void>;
}

View file

@ -36,13 +36,8 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) {
thresholdRule: { enabled: false },
},
compositeSlo: { enabled: false },
aiAssistant: {
enabled: false,
feedback: {
enabled: false,
},
},
};
const mockTheme: CoreTheme = {
darkMode: false,
};

View file

@ -40,12 +40,6 @@ const defaultConfig: ConfigSchema = {
thresholdRule: { enabled: false },
},
compositeSlo: { enabled: false },
aiAssistant: {
enabled: false,
feedback: {
enabled: false,
},
},
};
const queryClient = new QueryClient({

View file

@ -18,7 +18,6 @@ import {
unwrapEsResponse,
WrappedElasticsearchClientError,
} from '../common/utils/unwrap_es_response';
import { observabilityCoPilotConfig } from './services/openai/config';
export { rangeQuery, kqlQuery, termQuery, termsQuery } from './utils/queries';
export { getInspectResponse } from '../common/utils/get_inspect_response';
@ -54,7 +53,6 @@ const configSchema = schema.object({
groupByPageSize: schema.number({ defaultValue: 10_000 }),
}),
enabled: schema.boolean({ defaultValue: true }),
aiAssistant: schema.maybe(observabilityCoPilotConfig),
compositeSlo: schema.object({
enabled: schema.boolean({ defaultValue: false }),
}),

View file

@ -45,7 +45,6 @@ import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common'
import { registerRuleTypes } from './lib/rules/register_rule_types';
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../common/constants';
import { registerSloUsageCollector } from './lib/collectors/register';
import { OpenAIService } from './services/openai';
import { threshold } from './saved_objects/threshold';
export type ObservabilityPluginSetup = ReturnType<ObservabilityPlugin['setup']>;
@ -244,10 +243,6 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
);
registerSloUsageCollector(plugins.usageCollection);
const openAIService = config.aiAssistant?.enabled
? new OpenAIService(config.aiAssistant)
: undefined;
core.getStartServices().then(([coreStart, pluginStart]) => {
registerRoutes({
core,
@ -259,7 +254,6 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
},
ruleDataService,
getRulesClientWithRequest: pluginStart.alerting.getRulesClientWithRequest,
getOpenAIClient: () => openAIService?.client,
},
logger: this.logger,
repository: getObservabilityServerRouteRepository(config),
@ -279,9 +273,6 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
const api = await annotationsApiPromise;
return api?.getScopedAnnotationsClient(...args);
},
getOpenAIClient() {
return openAIService?.client;
},
alertsLocator,
};
}

View file

@ -1,124 +0,0 @@
/*
* 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 Boom from '@hapi/boom';
import { ServerRoute } from '@kbn/server-route-repository';
import axios from 'axios';
import * as t from 'io-ts';
import { map } from 'lodash';
import { ChatCompletionRequestMessageRoleEnum, CreateChatCompletionResponse } from 'openai';
import { Readable } from 'stream';
import { CoPilotPromptMap } from '../../../common/co_pilot';
import { coPilotPrompts } from '../../../common/co_pilot/prompts';
import { createObservabilityServerRoute } from '../create_observability_server_route';
import { ObservabilityRouteCreateOptions, ObservabilityRouteHandlerResources } from '../types';
const promptRoutes: {
[TPromptId in keyof CoPilotPromptMap as `POST /internal/observability/copilot/prompts/${TPromptId}`]: ServerRoute<
`POST /internal/observability/copilot/prompts/${TPromptId}`,
t.TypeC<{ body: CoPilotPromptMap[TPromptId]['params'] }>,
ObservabilityRouteHandlerResources,
unknown,
ObservabilityRouteCreateOptions
>;
} = Object.assign(
{},
...map(coPilotPrompts, (prompt, promptId) => {
return createObservabilityServerRoute({
endpoint: `POST /internal/observability/copilot/prompts/${promptId}`,
params: t.type({
body: prompt.params,
}),
options: {
tags: ['ai_assistant'],
},
handler: async (resources): Promise<CreateChatCompletionResponse | Readable> => {
const client = resources.dependencies.getOpenAIClient();
if (!client) {
throw Boom.notImplemented();
}
try {
return await client.chatCompletion.create(prompt.messages(resources.params.body as any));
} catch (error: any) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
throw Boom.forbidden(error.response?.statusText);
}
throw error;
}
},
});
})
);
const trackRoute = createObservabilityServerRoute({
endpoint: 'POST /internal/observability/copilot/prompts/{promptId}/track',
params: t.type({
path: t.type({
promptId: t.string,
}),
body: t.intersection([
t.type({
responseTime: t.number,
messages: t.array(
t.intersection([
t.type({
role: t.union([
t.literal(ChatCompletionRequestMessageRoleEnum.System),
t.literal(ChatCompletionRequestMessageRoleEnum.User),
t.literal(ChatCompletionRequestMessageRoleEnum.Assistant),
]),
content: t.string,
}),
t.partial({
name: t.string,
}),
])
),
response: t.string,
}),
t.partial({
feedbackAction: t.union([t.literal('thumbsup'), t.literal('thumbsdown')]),
}),
]),
}),
options: {
tags: ['ai_assistant'],
},
handler: async (resources): Promise<void> => {
const { params, config } = resources;
if (
!config.aiAssistant?.enabled ||
!config.aiAssistant.feedback.enabled ||
!config.aiAssistant.feedback.url
) {
throw Boom.notImplemented();
}
const feedbackBody = {
prompt_name: params.path.promptId,
feedback_action: params.body.feedbackAction,
model:
'openAI' in config.aiAssistant.provider
? config.aiAssistant.provider.openAI.model
: config.aiAssistant.provider.azureOpenAI.resourceName,
response_time: params.body.responseTime,
conversation: [
...params.body.messages.map(({ role, content }) => ({ role, content })),
{ role: 'system', content: params.body.response },
],
};
await axios.post(config.aiAssistant.feedback.url, feedbackBody);
},
});
export const observabilityCoPilotRouteRepository = {
...promptRoutes,
...trackRoute,
};

View file

@ -7,7 +7,6 @@
import { ObservabilityConfig } from '..';
import { compositeSloRouteRepository } from './composite_slo/route';
import { observabilityCoPilotRouteRepository } from './copilot/route';
import { rulesRouteRepository } from './rules/route';
import { sloRouteRepository } from './slo/route';
@ -18,7 +17,6 @@ export function getObservabilityServerRouteRepository(config: ObservabilityConfi
...rulesRouteRepository,
...sloRouteRepository,
...(isCompositeSloFeatureEnabled ? compositeSloRouteRepository : {}),
...observabilityCoPilotRouteRepository,
};
return repository;
}

View file

@ -18,7 +18,6 @@ import axios from 'axios';
import * as t from 'io-ts';
import { ObservabilityConfig } from '..';
import { getHTTPResponseCode, ObservabilityError } from '../errors';
import { IOpenAIClient } from '../services/openai/types';
import { ObservabilityRequestHandlerContext } from '../types';
import { AbstractObservabilityServerRouteRepository } from './types';
@ -36,7 +35,6 @@ export interface RegisterRoutesDependencies {
};
ruleDataService: RuleDataPluginService;
getRulesClientWithRequest: (request: KibanaRequest) => RulesClientApi;
getOpenAIClient: () => IOpenAIClient | undefined;
}
export function registerRoutes({ config, repository, core, logger, dependencies }: RegisterRoutes) {

View file

@ -1,49 +0,0 @@
/*
* 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 axios from 'axios';
import { ChatCompletionRequestMessage, CreateChatCompletionResponse } from 'openai';
import { Readable } from 'stream';
import { format } from 'url';
import { AzureOpenAIConfig } from './config';
import { pipeStreamingResponse } from './pipe_streaming_response';
import { IOpenAIClient } from './types';
export class AzureOpenAIClient implements IOpenAIClient {
constructor(private readonly config: AzureOpenAIConfig) {}
chatCompletion: {
create: (
messages: ChatCompletionRequestMessage[]
) => Promise<CreateChatCompletionResponse | Readable>;
} = {
create: async (messages) => {
const response = await axios.post(
format({
host: `${this.config.resourceName}.openai.azure.com`,
pathname: `/openai/deployments/${this.config.deploymentId}/chat/completions`,
protocol: 'https',
query: {
'api-version': '2023-05-15',
},
}),
{
messages,
stream: true,
},
{
headers: {
'api-key': this.config.apiKey,
},
responseType: 'stream',
}
);
return pipeStreamingResponse(response);
},
};
}

View file

@ -1,36 +0,0 @@
/*
* 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 { schema, type TypeOf } from '@kbn/config-schema';
export const openAIConfig = schema.object({
openAI: schema.object({
model: schema.string(),
apiKey: schema.string(),
}),
});
export const azureOpenAIConfig = schema.object({
azureOpenAI: schema.object({
resourceName: schema.string(),
deploymentId: schema.string(),
apiKey: schema.string(),
}),
});
export const observabilityCoPilotConfig = schema.object({
enabled: schema.boolean({ defaultValue: false }),
feedback: schema.object({
enabled: schema.boolean({ defaultValue: false }),
url: schema.maybe(schema.string()),
}),
provider: schema.oneOf([openAIConfig, azureOpenAIConfig]),
});
export type OpenAIConfig = TypeOf<typeof openAIConfig>['openAI'];
export type AzureOpenAIConfig = TypeOf<typeof azureOpenAIConfig>['azureOpenAI'];
export type ObservabilityCoPilotConfig = TypeOf<typeof observabilityCoPilotConfig>;

View file

@ -1,23 +0,0 @@
/*
* 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 { AzureOpenAIClient } from './azure_openai_client';
import { ObservabilityCoPilotConfig } from './config';
import { OpenAIClient } from './openai_client';
import { IOpenAIClient } from './types';
export class OpenAIService {
public readonly client: IOpenAIClient;
constructor(config: ObservabilityCoPilotConfig) {
if ('openAI' in config.provider) {
this.client = new OpenAIClient(config.provider.openAI);
} else {
this.client = new AzureOpenAIClient(config.provider.azureOpenAI);
}
}
}

View file

@ -1,45 +0,0 @@
/*
* 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 {
ChatCompletionRequestMessage,
Configuration,
CreateChatCompletionResponse,
OpenAIApi,
} from 'openai';
import type { OpenAIConfig } from './config';
import type { IOpenAIClient } from './types';
import { pipeStreamingResponse } from './pipe_streaming_response';
export class OpenAIClient implements IOpenAIClient {
private readonly client: OpenAIApi;
constructor(private readonly config: OpenAIConfig) {
const clientConfig = new Configuration({
apiKey: config.apiKey,
});
this.client = new OpenAIApi(clientConfig);
}
chatCompletion: {
create: (messages: ChatCompletionRequestMessage[]) => Promise<CreateChatCompletionResponse>;
} = {
create: async (messages) => {
const response = await this.client.createChatCompletion(
{
messages,
model: this.config.model,
stream: true,
},
{ responseType: 'stream' }
);
return pipeStreamingResponse(response);
},
};
}

View file

@ -1,17 +0,0 @@
/*
* 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 { ChatCompletionRequestMessage, CreateChatCompletionResponse } from 'openai';
import { Readable } from 'stream';
export interface IOpenAIClient {
chatCompletion: {
create: (
messages: ChatCompletionRequestMessage[]
) => Promise<CreateChatCompletionResponse | Readable>;
};
}

View file

@ -5,8 +5,7 @@
* 2.0.
*/
export function pipeStreamingResponse(response: { data: any; headers: Record<string, string> }) {
response.headers['Content-Type'] = 'dont-compress-this';
import { setGlobalConfig } from '@storybook/testing-react';
import * as globalStorybookConfig from './preview';
return response.data;
}
setGlobalConfig(globalStorybookConfig);

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
module.exports = require('@kbn/storybook').defaultConfig;

View file

@ -0,0 +1,10 @@
/*
* 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 { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common';
export const decorators = [EuiThemeProviderDecorator];

View file

@ -0,0 +1,3 @@
# Observability AI Assistant plugin
This plugin provides the Observability AI Assistant service and UI components.

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export type { Message, Conversation } from './types';
export { MessageRole } from './types';

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Serializable } from '@kbn/utility-types';
export enum MessageRole {
System = 'system',
Assistant = 'assistant',
User = 'user',
Function = 'function',
Event = 'event',
Elastic = 'elastic',
}
export interface Message {
'@timestamp': string;
message: {
content?: string;
name?: string;
role: MessageRole;
function_call?: {
name: string;
args?: Serializable;
trigger: MessageRole.Assistant | MessageRole.User | MessageRole.Elastic;
};
data?: Serializable;
};
}
export interface Conversation {
'@timestamp': string;
user: {
id?: string;
name: string;
};
conversation: {
id: string;
title: string;
last_updated: string;
};
messages: Message[];
labels: Record<string, string>;
numeric_labels: Record<string, number>;
namespace: string;
}
export type ConversationRequestBase = Omit<Conversation, 'user' | 'conversation' | 'namespace'> & {
conversation: { title: string };
};
export type ConversationCreateRequest = ConversationRequestBase;
export type ConversationUpdateRequest = ConversationRequestBase & { conversation: { id: string } };

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/x-pack/plugins/observability_ai_assistant'],
setupFiles: ['<rootDir>/x-pack/plugins/observability_ai_assistant/.storybook/jest_setup.js'],
};

View file

@ -0,0 +1,24 @@
{
"type": "plugin",
"id": "@kbn/observability-ai-assistant-plugin",
"owner": "@elastic/apm-ui",
"plugin": {
"id": "observabilityAIAssistant",
"server": true,
"browser": true,
"configPath": [
"xpack",
"observabilityAIAssistant"
],
"requiredPlugins": [
"triggersActionsUi",
"actions",
"security"
],
"requiredBundles": [
"kibanaReact"
],
"optionalPlugins": [],
"extraPublicDirs": []
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { CoreSetup, CoreStart, HttpFetchOptions } from '@kbn/core/public';
import type {
ClientRequestParamsOf,
ReturnOf,
RouteRepositoryClient,
} from '@kbn/server-route-repository';
import { formatRequest } from '@kbn/server-route-repository';
import type { ObservabilityAIAssistantServerRouteRepository } from '../../server';
type FetchOptions = Omit<HttpFetchOptions, 'body'> & {
body?: any;
};
export type ObservabilityAIAssistantAPIClientOptions = Omit<
FetchOptions,
'query' | 'body' | 'pathname' | 'signal'
> & {
signal: AbortSignal | null;
};
export type ObservabilityAIAssistantAPIClient = RouteRepositoryClient<
ObservabilityAIAssistantServerRouteRepository,
ObservabilityAIAssistantAPIClientOptions
>;
export type AutoAbortedObservabilityAIAssistantAPIClient = RouteRepositoryClient<
ObservabilityAIAssistantServerRouteRepository,
Omit<ObservabilityAIAssistantAPIClientOptions, 'signal'>
>;
export type ObservabilityAIAssistantAPIEndpoint =
keyof ObservabilityAIAssistantServerRouteRepository;
export type APIReturnType<TEndpoint extends ObservabilityAIAssistantAPIEndpoint> = ReturnOf<
ObservabilityAIAssistantServerRouteRepository,
TEndpoint
>;
export type ObservabilityAIAssistantAPIClientRequestParamsOf<
TEndpoint extends ObservabilityAIAssistantAPIEndpoint
> = ClientRequestParamsOf<ObservabilityAIAssistantServerRouteRepository, TEndpoint>;
export function createCallObservabilityAIAssistantAPI(core: CoreStart | CoreSetup) {
return ((endpoint, options) => {
const { params } = options as unknown as {
params?: Partial<Record<string, any>>;
};
const { method, pathname, version } = formatRequest(endpoint, params?.path);
return core.http[method](pathname, {
...options,
body: params && params.body ? JSON.stringify(params.body) : undefined,
query: params?.query,
version,
});
}) as ObservabilityAIAssistantAPIClient;
}

View file

@ -0,0 +1,51 @@
/*
* 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 React from 'react';
import { ComponentStory } from '@storybook/react';
import { EuiButtonSize } from '@elastic/eui';
import { AskAssistantButton as Component, AskAssistantButtonProps } from './ask_assistant_button';
export default {
component: Component,
title: 'app/Atoms/AskAiAssistantButton',
argTypes: {
size: {
options: ['xs', 's', 'm'] as EuiButtonSize[],
control: { type: 'radio' },
},
fill: {
control: {
type: 'boolean',
},
},
flush: {
control: {
type: 'boolean',
if: { arg: 'variant', eq: 'empty' },
},
},
variant: {
options: ['basic', 'empty', 'iconOnly'],
control: { type: 'radio' },
},
},
};
const Template: ComponentStory<typeof Component> = (props: AskAssistantButtonProps) => (
<Component {...props} />
);
const defaultProps = {
fill: true,
size: 'm' as EuiButtonSize,
variant: 'basic' as const,
};
export const AskAiAssistantButton = Template.bind({});
AskAiAssistantButton.args = defaultProps;

View file

@ -0,0 +1,95 @@
/*
* 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 React from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiButtonSize,
EuiButtonEmptySizes,
EuiToolTip,
EuiButtonIcon,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export type AskAssistantButtonProps = (
| {
variant: 'basic';
size: EuiButtonSize;
fill?: boolean;
flush?: false;
}
| {
variant: 'empty';
size: EuiButtonEmptySizes;
fill?: false;
flush?: 'both';
}
| {
variant: 'iconOnly';
size: EuiButtonSize;
fill?: boolean;
flush?: false;
}
) & {
onClick: () => void;
};
export function AskAssistantButton({
fill,
flush,
size,
variant,
onClick,
}: AskAssistantButtonProps) {
const buttonLabel = i18n.translate(
'xpack.observabilityAiAssistant.askAssistantButton.buttonLabel',
{
defaultMessage: 'Ask Assistant',
}
);
switch (variant) {
case 'basic':
return (
<EuiButton fill={fill} size={size} iconType="sparkles" onClick={onClick}>
{buttonLabel}
</EuiButton>
);
case 'empty':
return (
<EuiButtonEmpty size={size} flush={flush} iconType="sparkles" onClick={onClick}>
{buttonLabel}
</EuiButtonEmpty>
);
case 'iconOnly':
return (
<EuiToolTip
position="top"
title={i18n.translate('xpack.observabilityAiAssistant.askAssistantButton.popoverTitle', {
defaultMessage: 'Elastic Assistant',
})}
content={i18n.translate(
'xpack.observabilityAiAssistant.askAssistantButton.popoverContent',
{
defaultMessage: 'Get insights into your data with the Elastic Assistant',
}
)}
>
<EuiButtonIcon
iconType="sparkles"
display={fill ? 'fill' : 'base'}
size={size}
style={{ minWidth: 'auto' }}
onClick={onClick}
/>
</EuiToolTip>
);
}
}

View file

@ -0,0 +1,33 @@
/*
* 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 React from 'react';
import { ComponentStory } from '@storybook/react';
import { AssistantAvatar as Component, AssistantAvatarProps } from './assistant_avatar';
export default {
component: Component,
title: 'app/Atoms/AssistantAvatar',
argTypes: {
size: {
options: ['xs', 's', 'm', 'l', 'xl'],
control: { type: 'radio' },
},
},
};
const Template: ComponentStory<typeof Component> = (props: AssistantAvatarProps) => (
<Component {...props} />
);
const defaultProps = {
size: 'm' as const,
};
export const AssistantAvatar = Template.bind({});
AssistantAvatar.args = defaultProps;

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
export interface AssistantAvatarProps {
size: keyof typeof sizeMap;
}
export const sizeMap = {
xl: 64,
l: 48,
m: 32,
s: 24,
xs: 16,
};
export function AssistantAvatar({ size }: AssistantAvatarProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={sizeMap[size]}
height={sizeMap[size]}
viewBox="0 0 64 64"
fill="none"
>
<path fill="#F04E98" d="M36 28h24v36H36V28Z" />
<path fill="#00BFB3" d="M4 46c0-9.941 8.059-18 18-18h6v36h-6c-9.941 0-18-8.059-18-18Z" />
<path
fill="#343741"
d="M60 12c0 6.627-5.373 12-12 12s-12-5.373-12-12S41.373 0 48 0s12 5.373 12 12Z"
/>
<path fill="#FA744E" d="M6 23C6 10.85 15.85 1 28 1v22H6Z" />
</svg>
);
}

View file

@ -0,0 +1,48 @@
/*
* 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 { ComponentMeta, ComponentStoryObj } from '@storybook/react';
import { FindActionResult } from '@kbn/actions-plugin/server';
import { ConnectorSelectorBase as Component } from './connector_selector_base';
const meta: ComponentMeta<typeof Component> = {
component: Component,
title: 'app/Molecules/ConnectorSelectorBase',
};
export default meta;
export const Loaded: ComponentStoryObj<typeof Component> = {
args: {
loading: false,
selectedConnector: 'gpt-4',
connectors: [
{ id: 'gpt-4', name: 'OpenAI GPT-4' },
{ id: 'gpt-3.5-turbo', name: 'OpenAI GPT-3.5 Turbo' },
] as FindActionResult[],
},
};
export const Loading: ComponentStoryObj<typeof Component> = {
args: {
loading: true,
},
};
export const Empty: ComponentStoryObj<typeof Component> = {
args: {
loading: false,
connectors: [],
},
};
export const FailedToLoad: ComponentStoryObj<typeof Component> = {
args: {
loading: false,
error: new Error('Failed to load connectors'),
},
};

View file

@ -0,0 +1,124 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLoadingSpinner,
EuiSuperSelect,
EuiText,
} from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/css';
import { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors';
type ConnectorSelectorBaseProps = UseGenAIConnectorsResult;
const wrapperClassName = css`
height: 32px;
.euiSuperSelectControl {
border: none;
box-shadow: none;
background: none;
}
`;
const noWrapClassName = css`
white-space: nowrap;
`;
export function ConnectorSelectorBase(props: ConnectorSelectorBaseProps) {
if (props.loading) {
return (
<EuiFlexGroup alignItems="center" className={wrapperClassName}>
<EuiFlexItem>
<EuiLoadingSpinner size="s" />
</EuiFlexItem>
</EuiFlexGroup>
);
}
if (props.error) {
return (
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s" className={wrapperClassName}>
<EuiFlexItem grow={false}>
<EuiIcon type="warning" color="danger" size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="danger">
{i18n.translate('xpack.observabilityAiAssistant.connectorSelector.error', {
defaultMessage: 'Failed to load connectors',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
if (!props.connectors?.length) {
return (
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s" className={wrapperClassName}>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="warning">
{i18n.translate('xpack.observabilityAiAssistant.connectorSelector.empty', {
defaultMessage: 'No connectors',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<EuiFlexGroup
className={wrapperClassName}
direction="row"
alignItems="center"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem>
<EuiSuperSelect
compressed
valueOfSelected={props.selectedConnector}
options={props.connectors.map((connector) => ({
value: connector.id,
inputDisplay: (
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
{i18n.translate(
'xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel',
{
defaultMessage: 'Connector:',
}
)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" className={noWrapClassName}>
{connector.name}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
),
dropdownDisplay: (
<EuiText size="xs" className={noWrapClassName}>
{connector.name}
</EuiText>
),
}))}
onChange={(id) => {
props.selectConnector(id);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,62 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
export type Feedback = 'positive' | 'negative';
interface FeedbackButtonsProps {
onClickFeedback: (feedback: Feedback) => void;
}
export function FeedbackButtons({ onClickFeedback }: FeedbackButtonsProps) {
return (
<EuiFlexGroup responsive={false} direction="row" alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
<em>
{i18n.translate('xpack.observabilityAiAssistant.insight.feedbackButtons.title', {
defaultMessage: 'Was this helpful?',
})}
</em>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup responsive={false} direction="row" alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
color="success"
iconType="faceHappy"
size="s"
onClick={() => onClickFeedback('positive')}
>
{i18n.translate('xpack.observabilityAiAssistant.insight.feedbackButtons.positive', {
defaultMessage: 'Yes',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
color="danger"
iconType="faceSad"
size="s"
onClick={() => onClickFeedback('negative')}
>
{i18n.translate('xpack.observabilityAiAssistant.insight.feedbackButtons.negative', {
defaultMessage: 'No',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { Message } from '../../../common/types';
import { useChat } from '../../hooks/use_chat';
import { useGenAIConnectors } from '../../hooks/use_genai_connectors';
import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base';
import { MessagePanel } from '../message_panel/message_panel';
import { MessageText } from '../message_panel/message_text';
import { InsightBase } from './insight_base';
import { InsightMissingCredentials } from './insight_missing_credentials';
function ChatContent({ messages, connectorId }: { messages: Message[]; connectorId: string }) {
const chat = useChat({ messages, connectorId });
return (
<MessagePanel
body={<MessageText content={chat.content ?? ''} loading={chat.loading} />}
error={chat.error}
controls={null}
/>
);
}
export function Insight({ messages, title }: { messages: Message[]; title: string }) {
const [hasOpened, setHasOpened] = useState(false);
const connectors = useGenAIConnectors();
const {
services: { http },
} = useKibana();
let children: React.ReactNode = null;
if (hasOpened && connectors.selectedConnector) {
children = <ChatContent messages={messages} connectorId={connectors.selectedConnector} />;
} else if (!connectors.loading && !connectors.connectors?.length) {
children = (
<InsightMissingCredentials
connectorsManagementHref={http!.basePath.prepend(
`/app/management/insightsAndAlerting/triggersActionsConnectors/connectors`
)}
/>
);
}
return (
<InsightBase
title={title}
onToggle={(isOpen) => {
setHasOpened((prevHasOpened) => prevHasOpened || isOpen);
}}
controls={<ConnectorSelectorBase {...connectors} />}
loading={connectors.loading}
>
{children}
</InsightBase>
);
}

View file

@ -0,0 +1,91 @@
/*
* 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 React from 'react';
import { ComponentStory } from '@storybook/react';
import { FindActionResult } from '@kbn/actions-plugin/server';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InsightBase as Component, InsightBaseProps } from './insight_base';
import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator';
import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base';
import { MessagePanel } from '../message_panel/message_panel';
import { MessageText } from '../message_panel/message_text';
import { FeedbackButtons } from '../feedback_buttons';
import { RegenerateResponseButton } from '../regenerate_response_button';
import { StartChatButton } from '../start_chat_button';
export default {
component: Component,
title: 'app/Molecules/Insight',
decorators: [KibanaReactStorybookDecorator],
};
const Template: ComponentStory<typeof Component> = (props: InsightBaseProps) => (
<Component {...props} />
);
const defaultProps: InsightBaseProps = {
title: 'What is the root cause of performance degradation in my service?',
actions: [
{ id: 'foo', label: 'Put hands in pockets', handler: () => {} },
{ id: 'bar', label: 'Drop kick', handler: () => {} },
],
loading: false,
controls: (
<ConnectorSelectorBase
connectors={
[
{ id: 'gpt-4', name: 'GPT-4' },
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo' },
] as FindActionResult[]
}
selectedConnector="gpt-4"
loading={false}
selectConnector={() => {}}
/>
),
onToggle: () => {},
children: (
<MessagePanel
body={
<MessageText
content={`Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam commodo sollicitudin erat in ultrices. Vestibulum euismod ex ac lectus semper hendrerit.
Morbi mattis odio justo, in ullamcorper metus aliquet eu. Praesent risus velit, rutrum ac magna non, vehicula vestibulum sapien. Quisque pulvinar eros eu finibus iaculis.
Morbi dapibus sapien lacus, vitae suscipit ex egestas pharetra. In velit eros, fermentum sit amet augue ut, aliquam sodales nulla. Nunc mattis lobortis eros sit amet dapibus.
Morbi non faucibus massa. Aliquam sed augue in eros ornare luctus sit amet cursus dolor. Pellentesque pellentesque lorem eu odio auctor convallis. Sed sodales felis at velit tempus tincidunt. Nulla sed ante cursus nibh mollis blandit. In mattis imperdiet tellus. Vestibulum nisl turpis, efficitur quis sollicitudin id, mollis in arcu. Vestibulum pulvinar tincidunt magna, vitae facilisis massa congue quis. Cras commodo efficitur tellus, et commodo risus rutrum at.`}
loading={false}
/>
}
controls={
<EuiFlexGroup>
<EuiFlexItem grow>
<FeedbackButtons onClickFeedback={() => {}} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem grow={false}>
<RegenerateResponseButton />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<StartChatButton />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
),
};
export const Insight = Template.bind({});
Insight.args = defaultProps;

View file

@ -0,0 +1,129 @@
/*
* 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 {
EuiAccordion,
EuiButtonIcon,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiPopover,
EuiSpacer,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { AssistantAvatar } from '../assistant_avatar';
export interface InsightBaseProps {
title: string;
description?: string;
controls?: React.ReactNode;
debug?: boolean;
actions?: Array<{ id: string; label: string; icon?: string; handler: () => void }>;
onToggle: (isOpen: boolean) => void;
children: React.ReactNode;
loading?: boolean;
}
export function InsightBase({
title,
description = i18n.translate('xpack.observabilityAiAssistant.insight.defaultDescription', {
defaultMessage: 'Get helpful insights from our Elastic AI Assistant.',
}),
controls,
children,
actions,
onToggle,
loading,
}: InsightBaseProps) {
const { euiTheme } = useEuiTheme();
const [isActionsPopoverOpen, setIsActionsPopover] = useState(false);
const handleClickActions = () => {
setIsActionsPopover(!isActionsPopoverOpen);
};
return (
<EuiPanel hasBorder hasShadow={false}>
<EuiAccordion
id="obsAiAssistantInsight"
arrowProps={{ css: { alignSelf: 'flex-start' } }}
buttonContent={
<EuiFlexGroup wrap responsive={false} gutterSize="m">
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<AssistantAvatar size="xs" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText css={{ marginTop: 2, marginBottom: 1 }}>
<h5>{title}</h5>
</EuiText>
<EuiText size="s" css={{ color: euiTheme.colors.subduedText }}>
<span>{description}</span>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
}
isLoading={loading}
isDisabled={loading}
extraAction={
actions?.length || controls ? (
<EuiFlexGroup direction="row" gutterSize="s" responsive={false}>
{controls && <EuiFlexItem grow={false}>{controls}</EuiFlexItem>}
{actions?.length ? (
<EuiFlexItem>
<EuiPopover
anchorPosition="downLeft"
button={
<EuiButtonIcon
aria-label={i18n.translate(
'xpack.observabilityAiAssistant.insight.actions',
{
defaultMessage: 'Actions',
}
)}
color="text"
css={{ alignSelf: 'flex-start' }}
disabled={actions?.length === 0}
display="empty"
iconType="boxesHorizontal"
size="s"
onClick={handleClickActions}
/>
}
panelPaddingSize="s"
closePopover={handleClickActions}
isOpen={isActionsPopoverOpen}
>
<EuiContextMenuPanel
size="s"
items={actions?.map(({ id, icon, label, handler }) => (
<EuiContextMenuItem key={id} icon={icon} onClick={handler}>
{label}
</EuiContextMenuItem>
))}
/>
</EuiPopover>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
) : null
}
onToggle={onToggle}
>
<EuiSpacer size="m" />
{children}
</EuiAccordion>
</EuiPanel>
);
}

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export function InsightError() {
return (
<EuiCallOut
title={i18n.translate('xpack.observabilityAiAssistant.insight.error.title', {
defaultMessage: 'Error',
})}
color="danger"
iconType="error"
>
{i18n.translate('xpack.observabilityAiAssistant.insight.error.description', {
defaultMessage: 'An error occured.',
})}
<EuiSpacer size="m" />
<EuiButton fill color="danger">
{i18n.translate('xpack.observabilityAiAssistant.insight.error.buttonLabel', {
defaultMessage: 'Regenerate',
})}
</EuiButton>
</EuiCallOut>
);
}

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
interface Props {
connectorsManagementHref: string;
}
export function InsightMissingCredentials(props: Props) {
return (
<EuiCallOut
title={i18n.translate('xpack.observabilityAiAssistant.insight.missing.title', {
defaultMessage: 'Missing credentials',
})}
color="primary"
iconType="iInCircle"
>
{i18n.translate('xpack.observabilityAiAssistant.insight.missing.description', {
defaultMessage:
'You havent authorised OpenAI in order to generate responses from the Elastic Assistant. Authorise the model in order to proceed.',
})}
<EuiSpacer size="m" />
<EuiButton fill color="primary" href={props.connectorsManagementHref}>
{i18n.translate('xpack.observabilityAiAssistant.insight.missing.buttonLabel', {
defaultMessage: 'Connect Assistant',
})}
</EuiButton>
</EuiCallOut>
);
}

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
import React from 'react';
import { FeedbackButtons } from '../feedback_buttons';
import { MessagePanel as Component } from './message_panel';
import { MessageText } from './message_text';
const meta: ComponentMeta<typeof Component> = {
component: Component,
title: 'app/Molecules/MessagePanel',
};
export default meta;
export const ContentLoading: ComponentStoryObj<typeof Component> = {
args: {
body: (
<MessageText
content={`# This is a piece of text.
And an extra _paragraph_.
#### With a title
This text is loa`}
loading
/>
),
},
};
export const ContentLoaded: ComponentStoryObj<typeof Component> = {
args: {
body: <MessageText content={`This response has fully loaded.`} loading={false} />,
},
};
export const ContentFailed: ComponentStoryObj<typeof Component> = {
args: {
body: <MessageText content={`This is a partial re`} loading={false} />,
error: new Error(),
},
};
export const Controls: ComponentStoryObj<typeof Component> = {
args: {
body: <MessageText content={`This is a partial re`} loading={false} />,
error: new Error(),
controls: <FeedbackButtons onClickFeedback={() => {}} />,
},
};

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiPanel,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
interface Props {
error?: Error;
body?: React.ReactNode;
controls?: React.ReactNode;
}
export function MessagePanel(props: Props) {
return (
<EuiPanel color="subdued" hasShadow={false}>
{props.body}
{props.error ? (
<>
{props.body ? <EuiSpacer size="xs" /> : null}
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="alert" color="danger" size="s" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" color="danger">
{i18n.translate('xpack.observabilityAiAssistant.messagePanel.failedLoadingText', {
defaultMessage: 'Failed to load response',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</>
) : null}
{props.controls ? (
<>
<EuiSpacer size="m" />
<EuiHorizontalRule margin="none" />
<EuiSpacer size="s" />
{props.controls}
</>
) : null}
</EuiPanel>
);
}

View file

@ -0,0 +1,97 @@
/*
* 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 { v4 } from 'uuid';
import React from 'react';
import type { Node } from 'unist';
import { css } from '@emotion/css';
import type { Parent, Text } from 'mdast';
import ReactMarkdown from 'react-markdown';
import { EuiText } from '@elastic/eui';
interface Props {
content: string;
loading: boolean;
}
const cursorCss = css`
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
animation: blink 1s infinite;
width: 10px;
height: 16px;
vertical-align: middle;
display: inline-block;
background: rgba(0, 0, 0, 0.25);
`;
const cursor = <span className={cursorCss} />;
const CURSOR = `{{${v4()}}`;
const loadingCursorPlugin = () => {
const visitor = (node: Node, parent?: Parent) => {
if ('children' in node) {
const nodeAsParent = node as Parent;
nodeAsParent.children.forEach((child) => {
visitor(child, nodeAsParent);
});
}
if (node.type !== 'text') {
return;
}
const textNode = node as Text;
const indexOfCursor = textNode.value.indexOf(CURSOR);
if (indexOfCursor === -1) {
return;
}
textNode.value = textNode.value.replace(CURSOR, '');
const indexOfNode = parent!.children.indexOf(textNode);
parent!.children.splice(indexOfNode + 1, 0, {
type: 'cursor' as Text['type'],
value: CURSOR,
data: {
hName: 'cursor',
},
});
};
return (tree: Node) => {
visitor(tree);
};
};
export function MessageText(props: Props) {
return (
<EuiText size="s">
<ReactMarkdown
plugins={[loadingCursorPlugin]}
components={
{
cursor: () => cursor,
} as Record<string, any>
}
>
{`${props.content}${props.loading ? CURSOR : ''}`}
</ReactMarkdown>
</EuiText>
);
}

View file

@ -0,0 +1,20 @@
/*
* 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 { EuiButtonEmpty, EuiButtonEmptyProps } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
export function RegenerateResponseButton(props: Partial<EuiButtonEmptyProps>) {
return (
<EuiButtonEmpty {...props} iconType="sparkles" size="s">
{i18n.translate('xpack.observabilityAiAssistant.regenerateResponseButtonLabel', {
defaultMessage: 'Regenerate',
})}
</EuiButtonEmpty>
);
}

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiButton, EuiButtonProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export function StartChatButton(props: Partial<EuiButtonProps>) {
return (
<EuiButton {...props} fill iconType="discuss" size="s">
{i18n.translate('xpack.observabilityAiAssistant.insight.response.startChat', {
defaultMessage: 'Start chat',
})}
</EuiButton>
);
}

View file

@ -6,8 +6,10 @@
*/
import { createContext } from 'react';
import { type CoPilotService } from '../../typings/co_pilot';
import type { ObservabilityAIAssistantService } from '../types';
export const CoPilotContext = createContext<CoPilotService | undefined>(undefined);
export const ObservabilityAIAssistantContext = createContext<
ObservabilityAIAssistantService | undefined
>(undefined);
export const CoPilotContextProvider = CoPilotContext.Provider;
export const ObservabilityAIAssistantProvider = ObservabilityAIAssistantContext.Provider;

View file

@ -0,0 +1,111 @@
/*
* 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 { clone } from 'lodash';
import { useEffect, useState } from 'react';
import { concatMap, delay, of } from 'rxjs';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { i18n } from '@kbn/i18n';
import type { Message } from '../../common/types';
import { useObservabilityAIAssistant } from './use_observability_ai_assistant';
interface MessageResponse {
content?: string;
function_call?: {
name?: string;
args?: string;
};
}
export function useChat({ messages, connectorId }: { messages: Message[]; connectorId: string }): {
content?: string;
function_call?: {
name?: string;
args?: string;
};
loading: boolean;
error?: Error;
} {
const assistant = useObservabilityAIAssistant();
const {
services: { notifications },
} = useKibana();
const [response, setResponse] = useState<MessageResponse | undefined>(undefined);
const [error, setError] = useState<Error | undefined>(undefined);
const [loading, setLoading] = useState(false);
useEffect(() => {
const controller = new AbortController();
setResponse(undefined);
setError(undefined);
setLoading(true);
const partialResponse = {
content: '',
function_call: {
name: '',
args: '',
},
};
assistant
.chat({ messages, connectorId, signal: controller.signal })
.then((response$) => {
return new Promise<void>((resolve, reject) => {
const subscription = response$
.pipe(concatMap((value) => of(value).pipe(delay(50))))
.subscribe({
next: (chunk) => {
partialResponse.content += chunk.choices[0].delta.content ?? '';
partialResponse.function_call.name +=
chunk.choices[0].delta.function_call?.name ?? '';
partialResponse.function_call.args +=
chunk.choices[0].delta.function_call?.args ?? '';
setResponse(clone(partialResponse));
},
error: (err) => {
reject(err);
},
complete: () => {
resolve();
},
});
controller.signal.addEventListener('abort', () => {
subscription.unsubscribe();
});
});
})
.catch((err) => {
notifications?.showErrorDialog({
title: i18n.translate('xpack.observabilityAiAssistant.failedToLoadChatTitle', {
defaultMessage: 'Failed to load chat',
}),
error: err,
});
setError(err);
})
.finally(() => {
setLoading(false);
});
return () => {
controller.abort();
};
}, [messages, connectorId, assistant, notifications]);
return {
...response,
error,
loading,
};
}

View file

@ -0,0 +1,70 @@
/*
* 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 { useEffect, useState } from 'react';
import type { FindActionResult } from '@kbn/actions-plugin/server';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { useObservabilityAIAssistant } from './use_observability_ai_assistant';
export interface UseGenAIConnectorsResult {
connectors?: FindActionResult[];
selectedConnector?: string;
loading: boolean;
error?: Error;
selectConnector: (id: string) => void;
}
export function useGenAIConnectors(): UseGenAIConnectorsResult {
const assistant = useObservabilityAIAssistant();
const [connectors, setConnectors] = useState<FindActionResult[] | undefined>(undefined);
const [selectedConnector, setSelectedConnector] = useLocalStorage(
`xpack.observabilityAiAssistant.lastUsedConnector`,
''
);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | undefined>(undefined);
useEffect(() => {
setLoading(true);
const controller = new AbortController();
assistant
.callApi('GET /internal/observability_ai_assistant/connectors', {
signal: controller.signal,
})
.then((results) => {
setConnectors(results);
setError(undefined);
})
.catch((err) => {
setError(err);
setConnectors(undefined);
})
.finally(() => {
setLoading(false);
});
return () => {
controller.abort();
};
}, [assistant]);
return {
connectors,
loading,
error,
selectedConnector: selectedConnector || connectors?.[0]?.id,
selectConnector: (id: string) => {
setSelectedConnector(id);
},
};
}

View file

@ -0,0 +1,20 @@
/*
* 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 { useContext } from 'react';
import { ObservabilityAIAssistantContext } from '../context/observability_ai_assistant_provider';
export function useObservabilityAIAssistant() {
const services = useContext(ObservabilityAIAssistantContext);
if (!services) {
throw new Error(
'ObservabilityAIAssistantContext not set. Did you wrap your component in `<ObservabilityAIAssistantProvider/>`?'
);
}
return services;
}

View file

@ -0,0 +1,44 @@
/*
* 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 type { PluginInitializer, PluginInitializerContext } from '@kbn/core/public';
import { lazy } from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
import { ObservabilityAIAssistantPlugin } from './plugin';
import type {
ObservabilityAIAssistantPluginSetup,
ObservabilityAIAssistantPluginStart,
ObservabilityAIAssistantPluginSetupDependencies,
ObservabilityAIAssistantPluginStartDependencies,
ConfigSchema,
} from './types';
export const ContextualInsight = withSuspense(
lazy(() => import('./components/insight/insight').then((m) => ({ default: m.Insight })))
);
export { ObservabilityAIAssistantProvider } from './context/observability_ai_assistant_provider';
export type { ObservabilityAIAssistantPluginSetup, ObservabilityAIAssistantPluginStart };
export { useObservabilityAIAssistant } from './hooks/use_observability_ai_assistant';
export type { Conversation, Message } from '../common';
export { MessageRole } from '../common';
export type {
ObservabilityAIAssistantAPIClientRequestParamsOf,
ObservabilityAIAssistantAPIEndpoint,
APIReturnType,
} from './api';
export const plugin: PluginInitializer<
ObservabilityAIAssistantPluginSetup,
ObservabilityAIAssistantPluginStart,
ObservabilityAIAssistantPluginSetupDependencies,
ObservabilityAIAssistantPluginStartDependencies
> = (pluginInitializerContext: PluginInitializerContext<ConfigSchema>) =>
new ObservabilityAIAssistantPlugin(pluginInitializerContext);

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
import type { Logger } from '@kbn/logging';
import { createService } from './service/create_service';
import type {
ConfigSchema,
ObservabilityAIAssistantPluginSetup,
ObservabilityAIAssistantPluginSetupDependencies,
ObservabilityAIAssistantPluginStart,
ObservabilityAIAssistantPluginStartDependencies,
} from './types';
export class ObservabilityAIAssistantPlugin
implements
Plugin<
ObservabilityAIAssistantPluginSetup,
ObservabilityAIAssistantPluginStart,
ObservabilityAIAssistantPluginSetupDependencies,
ObservabilityAIAssistantPluginStartDependencies
>
{
logger: Logger;
constructor(context: PluginInitializerContext<ConfigSchema>) {
this.logger = context.logger.get();
}
setup(): ObservabilityAIAssistantPluginSetup {
return {};
}
start(coreStart: CoreStart): ObservabilityAIAssistantPluginStart {
return createService(coreStart);
}
}

View file

@ -0,0 +1,108 @@
/*
* 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 { CoreStart } from '@kbn/core/public';
import { ReadableStream } from 'stream/web';
import { ObservabilityAIAssistantService } from '../types';
import { createService } from './create_service';
describe('createService', () => {
describe('chat', () => {
let service: ObservabilityAIAssistantService;
const httpPostSpy = jest.fn();
function respondWithChunks({ chunks, status = 200 }: { status?: number; chunks: string[][] }) {
const response = {
response: {
status,
body: new ReadableStream({
start(controller) {
chunks.forEach((chunk) => {
controller.enqueue(new TextEncoder().encode(chunk.join('\n')));
});
controller.close();
},
}),
},
};
httpPostSpy.mockResolvedValueOnce(response);
}
async function chat(signal: AbortSignal = new AbortController().signal) {
const response = await service.chat({ messages: [], connectorId: '', signal });
return response;
}
beforeEach(() => {
service = createService({
http: {
post: httpPostSpy,
},
} as unknown as CoreStart);
});
afterEach(() => {
httpPostSpy.mockReset();
});
it('correctly parses a stream of JSON lines', async () => {
const chunk1 = ['data: {}', 'data: {}'];
const chunk2 = ['data: {}', 'data: [DONE]'];
respondWithChunks({ chunks: [chunk1, chunk2] });
const response$ = await chat();
const results: any = [];
response$.subscribe({
next: (data) => results.push(data),
complete: () => {
expect(results).toHaveLength(3);
},
});
});
it('correctly buffers partial lines', async () => {
const chunk1 = ['data: {}', 'data: {'];
const chunk2 = ['}', 'data: [DONE]'];
respondWithChunks({ chunks: [chunk1, chunk2] });
const response$ = await chat();
const results: any = [];
response$.subscribe({
next: (data) => results.push(data),
complete: () => {
expect(results).toHaveLength(2);
},
});
});
it('propagates invalid requests as an error', () => {
respondWithChunks({ status: 400, chunks: [] });
expect(() => chat()).rejects.toThrowErrorMatchingInlineSnapshot(`"Unexpected error"`);
});
it('propagates JSON parsing errors', async () => {
const chunk1 = ['data: {}', 'data: invalid json'];
respondWithChunks({ chunks: [chunk1] });
const response$ = await chat();
response$.subscribe({
error: (err) => {
expect(err).toBeInstanceOf(SyntaxError);
},
});
});
});
});

View file

@ -0,0 +1,64 @@
/*
* 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 type { CoreStart, HttpResponse } from '@kbn/core/public';
import { filter, map } from 'rxjs';
import type { Message } from '../../common';
import { createCallObservabilityAIAssistantAPI } from '../api';
import { CreateChatCompletionResponseChunk, ObservabilityAIAssistantService } from '../types';
import { readableStreamReaderIntoObservable } from '../utils/readable_stream_reader_into_observable';
export function createService(coreStart: CoreStart): ObservabilityAIAssistantService {
const client = createCallObservabilityAIAssistantAPI(coreStart);
return {
isEnabled: () => {
return true;
},
async chat({
connectorId,
messages,
signal,
}: {
connectorId: string;
messages: Message[];
signal: AbortSignal;
}) {
const response = (await client('POST /internal/observability_ai_assistant/chat', {
params: {
body: {
messages,
connectorId,
},
},
signal,
asResponse: true,
rawResponse: true,
})) as unknown as HttpResponse;
const status = response.response?.status;
if (!status || status >= 400) {
throw new Error(response.response?.statusText || 'Unexpected error');
}
const reader = response.response.body?.getReader();
if (!reader) {
throw new Error('Could not get reader from response');
}
return readableStreamReaderIntoObservable(reader).pipe(
map((line) => line.substring(6)),
filter((line) => !!line && line !== '[DONE]'),
map((line) => JSON.parse(line) as CreateChatCompletionResponseChunk),
filter((line) => line.object === 'chat.completion.chunk')
);
},
callApi: client,
};
}

View file

@ -0,0 +1,49 @@
/*
* 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 type {
TriggersAndActionsUIPublicPluginSetup,
TriggersAndActionsUIPublicPluginStart,
} from '@kbn/triggers-actions-ui-plugin/public';
import type {
CreateChatCompletionResponse,
CreateChatCompletionResponseChoicesInner,
} from 'openai';
import type { Observable } from 'rxjs';
import type { Message } from '../common/types';
import type { ObservabilityAIAssistantAPIClient } from './api';
/* eslint-disable @typescript-eslint/no-empty-interface*/
export type CreateChatCompletionResponseChunk = Omit<CreateChatCompletionResponse, 'choices'> & {
choices: Array<
Omit<CreateChatCompletionResponseChoicesInner, 'message'> & {
delta: { content?: string; function_call?: { name?: string; args?: string } };
}
>;
};
export interface ObservabilityAIAssistantService {
isEnabled: () => boolean;
chat: (options: {
messages: Message[];
connectorId: string;
signal: AbortSignal;
}) => Promise<Observable<CreateChatCompletionResponseChunk>>;
callApi: ObservabilityAIAssistantAPIClient;
}
export interface ObservabilityAIAssistantPluginStart extends ObservabilityAIAssistantService {}
export interface ObservabilityAIAssistantPluginSetup {}
export interface ObservabilityAIAssistantPluginSetupDependencies {
triggersActions: TriggersAndActionsUIPublicPluginSetup;
}
export interface ObservabilityAIAssistantPluginStartDependencies {
triggersActions: TriggersAndActionsUIPublicPluginStart;
}
export interface ConfigSchema {}

View file

@ -0,0 +1,47 @@
/*
* 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 { Observable, share } from 'rxjs';
export function readableStreamReaderIntoObservable(
readableStreamReader: ReadableStreamDefaultReader<Uint8Array>
): Observable<string> {
return new Observable<string>((subscriber) => {
let lineBuffer: string = '';
async function read() {
const { done, value } = await readableStreamReader.read();
if (done) {
if (lineBuffer) {
subscriber.next(lineBuffer);
}
subscriber.complete();
return;
}
const textChunk = new TextDecoder().decode(value);
const lines = textChunk.split('\n');
lines[0] = lineBuffer + lines[0];
lineBuffer = lines.pop() || '';
for (const line of lines) {
subscriber.next(line);
}
read();
}
read().catch((err) => subscriber.error(err));
return () => {
readableStreamReader.cancel();
};
}).pipe(share());
}

View file

@ -0,0 +1,27 @@
/*
* 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 React, { ComponentType } from 'react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
export function KibanaReactStorybookDecorator(Story: ComponentType) {
return (
<KibanaContextProvider
services={{
triggersActionsUi: { getAddRuleFlyout: {} },
uiSettings: {
get: (setting: string) => {
if (setting === 'dateFormat') {
return 'MMM D, YYYY HH:mm';
}
},
},
}}
>
<Story />
</KibanaContextProvider>
);
}

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { schema, type TypeOf } from '@kbn/config-schema';
export const config = schema.object({
enabled: schema.boolean({ defaultValue: true }),
});
export type ObservabilityAIAssistantConfig = TypeOf<typeof config>;

View file

@ -0,0 +1,15 @@
/*
* 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 type { PluginInitializerContext } from '@kbn/core/server';
import type { ObservabilityAIAssistantConfig } from './config';
import { ObservabilityAIAssistantPlugin } from './plugin';
export type { ObservabilityAIAssistantServerRouteRepository } from './routes/get_global_observability_ai_assistant_route_repository';
export const plugin = (ctx: PluginInitializerContext<ObservabilityAIAssistantConfig>) =>
new ObservabilityAIAssistantPlugin(ctx);

View file

@ -0,0 +1,82 @@
/*
* 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 type {
CoreSetup,
CoreStart,
Logger,
Plugin,
PluginInitializerContext,
} from '@kbn/core/server';
import { mapValues } from 'lodash';
import type { ObservabilityAIAssistantConfig } from './config';
import { registerServerRoutes } from './routes/register_routes';
import { ObservabilityAIAssistantRouteHandlerResources } from './routes/types';
import { ObservabilityAIAssistantService } from './service';
import {
ObservabilityAIAssistantPluginSetup,
ObservabilityAIAssistantPluginStart,
ObservabilityAIAssistantPluginSetupDependencies,
ObservabilityAIAssistantPluginStartDependencies,
} from './types';
export class ObservabilityAIAssistantPlugin
implements
Plugin<
ObservabilityAIAssistantPluginSetup,
ObservabilityAIAssistantPluginStart,
ObservabilityAIAssistantPluginSetupDependencies,
ObservabilityAIAssistantPluginStartDependencies
>
{
logger: Logger;
constructor(context: PluginInitializerContext<ObservabilityAIAssistantConfig>) {
this.logger = context.logger.get();
}
public start(
core: CoreStart,
plugins: ObservabilityAIAssistantPluginStartDependencies
): ObservabilityAIAssistantPluginStart {
return {};
}
public setup(
core: CoreSetup<
ObservabilityAIAssistantPluginStartDependencies,
ObservabilityAIAssistantPluginStart
>,
plugins: ObservabilityAIAssistantPluginSetupDependencies
): ObservabilityAIAssistantPluginSetup {
const routeHandlerPlugins = mapValues(plugins, (value, key) => {
return {
setup: value,
start: () =>
core.getStartServices().then((services) => {
const [, pluginsStartContracts] = services;
return pluginsStartContracts[
key as keyof ObservabilityAIAssistantPluginStartDependencies
];
}),
};
}) as ObservabilityAIAssistantRouteHandlerResources['plugins'];
const service = new ObservabilityAIAssistantService({
logger: this.logger.get('service'),
core,
});
registerServerRoutes({
core,
logger: this.logger,
dependencies: {
plugins: routeHandlerPlugins,
service,
},
});
return {};
}
}

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