mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
ecb7f3eaf7
commit
f0ebb7097d
133 changed files with 4090 additions and 1636 deletions
|
@ -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
|
||||
|
|
|
@ -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
1
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -97,6 +97,7 @@ pageLoadAssetSize:
|
|||
navigation: 37269
|
||||
newsfeed: 42228
|
||||
observability: 115443
|
||||
observabilityAIAssistant: 16759
|
||||
observabilityOnboarding: 19573
|
||||
observabilityShared: 52256
|
||||
osquery: 107090
|
||||
|
|
|
@ -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,
|
||||
|
|
141
packages/kbn-server-route-repository/src/register_routes.ts
Normal file
141
packages/kbn-server-route-repository/src/register_routes.ts
Normal 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
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)',
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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/**/*"]
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"lens",
|
||||
"logsShared",
|
||||
"observability",
|
||||
"observabilityAIAssistant",
|
||||
"observabilityShared",
|
||||
"ruleRegistry",
|
||||
"security",
|
||||
|
@ -36,6 +37,7 @@
|
|||
"requiredBundles": [
|
||||
"unifiedSearch",
|
||||
"observability",
|
||||
"observabilityAIAssistant",
|
||||
"licenseManagement",
|
||||
"kibanaUtils",
|
||||
"kibanaReact",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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 host’s
|
||||
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>
|
||||
)}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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 host’s
|
||||
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 };
|
||||
}
|
||||
>;
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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.
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -89,12 +89,6 @@ const withCore = makeDecorator({
|
|||
thresholdRule: { enabled: false },
|
||||
},
|
||||
compositeSlo: { enabled: false },
|
||||
aiAssistant: {
|
||||
enabled: false,
|
||||
feedback: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -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!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -40,12 +40,6 @@ const defaultConfig: ConfigSchema = {
|
|||
thresholdRule: { enabled: false },
|
||||
},
|
||||
compositeSlo: { enabled: false },
|
||||
aiAssistant: {
|
||||
enabled: false,
|
||||
feedback: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
|
|
|
@ -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 }),
|
||||
}),
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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>;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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>;
|
||||
};
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
|
@ -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];
|
3
x-pack/plugins/observability_ai_assistant/README.md
Normal file
3
x-pack/plugins/observability_ai_assistant/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Observability AI Assistant plugin
|
||||
|
||||
This plugin provides the Observability AI Assistant service and UI components.
|
|
@ -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';
|
56
x-pack/plugins/observability_ai_assistant/common/types.ts
Normal file
56
x-pack/plugins/observability_ai_assistant/common/types.ts
Normal 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 } };
|
13
x-pack/plugins/observability_ai_assistant/jest.config.js
Normal file
13
x-pack/plugins/observability_ai_assistant/jest.config.js
Normal 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'],
|
||||
};
|
24
x-pack/plugins/observability_ai_assistant/kibana.jsonc
Normal file
24
x-pack/plugins/observability_ai_assistant/kibana.jsonc
Normal 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": []
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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'),
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 haven’t 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>
|
||||
);
|
||||
}
|
|
@ -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={() => {}} />,
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
44
x-pack/plugins/observability_ai_assistant/public/index.ts
Normal file
44
x-pack/plugins/observability_ai_assistant/public/index.ts
Normal 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);
|
39
x-pack/plugins/observability_ai_assistant/public/plugin.ts
Normal file
39
x-pack/plugins/observability_ai_assistant/public/plugin.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
49
x-pack/plugins/observability_ai_assistant/public/types.ts
Normal file
49
x-pack/plugins/observability_ai_assistant/public/types.ts
Normal 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 {}
|
|
@ -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());
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
14
x-pack/plugins/observability_ai_assistant/server/config.ts
Normal file
14
x-pack/plugins/observability_ai_assistant/server/config.ts
Normal 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>;
|
15
x-pack/plugins/observability_ai_assistant/server/index.ts
Normal file
15
x-pack/plugins/observability_ai_assistant/server/index.ts
Normal 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);
|
82
x-pack/plugins/observability_ai_assistant/server/plugin.ts
Normal file
82
x-pack/plugins/observability_ai_assistant/server/plugin.ts
Normal 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
Loading…
Add table
Add a link
Reference in a new issue