mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Observability] Co-pilot (tech preview) (#158678)
This commit is contained in:
parent
58c4e73c39
commit
4a5dcbdea8
40 changed files with 1366 additions and 102 deletions
|
@ -875,6 +875,7 @@
|
|||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^1.3.1",
|
||||
"object-path-immutable": "^3.1.1",
|
||||
"openai": "^3.2.1",
|
||||
"openpgp": "5.3.0",
|
||||
"opn": "^5.5.0",
|
||||
"ora": "^4.0.4",
|
||||
|
|
|
@ -851,4 +851,53 @@ describe('Fetch', () => {
|
|||
expect(usedSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rawResponse', () => {
|
||||
it("throws if rawResponse is set to true but asResponse isn't", async () => {
|
||||
fetchMock.get('*', { foo: 'bar' });
|
||||
|
||||
await expect(async () =>
|
||||
fetchInstance.fetch('/my/path', {
|
||||
rawResponse: true,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
'Invalid fetch arguments, rawResponse = true is only supported when asResponse = true'
|
||||
);
|
||||
});
|
||||
|
||||
it('immediately returns an unawaited Response object if rawResponse = true', async () => {
|
||||
fetchMock.get('*', { foo: 'bar' });
|
||||
|
||||
const response = await fetchInstance.fetch('/my/path', {
|
||||
rawResponse: true,
|
||||
asResponse: true,
|
||||
});
|
||||
|
||||
expect(response.response).toBeInstanceOf(Response);
|
||||
expect(response.body).toEqual(null);
|
||||
|
||||
const body = await response.response?.json();
|
||||
|
||||
expect(body).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('calls the request/response interceptors if rawResponse = true', async () => {
|
||||
fetchMock.get('*', { foo: 'bar' });
|
||||
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
const responseSpy = jest.fn();
|
||||
|
||||
fetchInstance.intercept({ request: requestSpy, response: responseSpy });
|
||||
|
||||
await fetchInstance.fetch('/my/path', {
|
||||
rawResponse: true,
|
||||
asResponse: true,
|
||||
});
|
||||
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
|
||||
expect(responseSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -90,6 +90,7 @@ export class Fetch {
|
|||
controller
|
||||
);
|
||||
const initialResponse = this.fetchResponse(interceptedOptions);
|
||||
|
||||
const interceptedResponse = await interceptResponse(
|
||||
interceptedOptions,
|
||||
initialResponse,
|
||||
|
@ -115,6 +116,7 @@ export class Fetch {
|
|||
private createRequest(options: HttpFetchOptionsWithPath): Request {
|
||||
const context = this.params.executionContext.withGlobalContext(options.context);
|
||||
const { version } = options;
|
||||
|
||||
// Merge and destructure options out that are not applicable to the Fetch API.
|
||||
const {
|
||||
query,
|
||||
|
@ -168,7 +170,9 @@ export class Fetch {
|
|||
const contentType = response.headers.get('Content-Type') || '';
|
||||
|
||||
try {
|
||||
if (NDJSON_CONTENT.test(contentType) || ZIP_CONTENT.test(contentType)) {
|
||||
if (fetchOptions.rawResponse) {
|
||||
body = null;
|
||||
} else if (NDJSON_CONTENT.test(contentType) || ZIP_CONTENT.test(contentType)) {
|
||||
body = await response.blob();
|
||||
} else if (JSON_CONTENT.test(contentType)) {
|
||||
body = await response.json();
|
||||
|
@ -227,6 +231,12 @@ const validateFetchArguments = (
|
|||
);
|
||||
}
|
||||
|
||||
if (fullOptions.rawResponse && !fullOptions.asResponse) {
|
||||
throw new Error(
|
||||
'Invalid fetch arguments, rawResponse = true is only supported when asResponse = true'
|
||||
);
|
||||
}
|
||||
|
||||
const invalidKbnHeaders = Object.keys(fullOptions.headers ?? {}).filter((headerName) =>
|
||||
headerName.startsWith('kbn-')
|
||||
);
|
||||
|
|
|
@ -281,6 +281,12 @@ export interface HttpFetchOptions extends HttpRequestInit {
|
|||
*/
|
||||
asResponse?: boolean;
|
||||
|
||||
/**
|
||||
* When true, the response from the `fetch` call will be returned as is, without being awaited or processed.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
rawResponse?: boolean;
|
||||
|
||||
context?: KibanaExecutionContext;
|
||||
|
||||
/** @experimental */
|
||||
|
|
|
@ -252,6 +252,7 @@ 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.coPilot.enabled (boolean)',
|
||||
'xpack.observability.unsafe.alertDetails.metrics.enabled (boolean)',
|
||||
'xpack.observability.unsafe.alertDetails.logs.enabled (boolean)',
|
||||
'xpack.observability.unsafe.alertDetails.uptime.enabled (boolean)',
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; 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}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
|
@ -49,12 +49,8 @@ import { HttpInfoSummaryItem } from '../../../shared/summary/http_info_summary_i
|
|||
import { UserAgentSummaryItem } from '../../../shared/summary/user_agent_summary_item';
|
||||
import { TimestampTooltip } from '../../../shared/timestamp_tooltip';
|
||||
import { TransactionTab } from '../../transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
import {
|
||||
ErrorTab,
|
||||
exceptionStacktraceTab,
|
||||
getTabs,
|
||||
logStacktraceTab,
|
||||
} from './error_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';
|
||||
|
@ -340,6 +336,8 @@ export function ErrorSampleDetails({
|
|||
<SampleSummary error={error} />
|
||||
)}
|
||||
|
||||
<ErrorSampleCoPilotPrompt error={error} transaction={transaction} />
|
||||
|
||||
<EuiTabs>
|
||||
{tabs.map(({ key, label }) => {
|
||||
return (
|
||||
|
@ -365,13 +363,13 @@ export function ErrorSampleDetails({
|
|||
{isLoading || !error ? (
|
||||
<EuiSkeletonText lines={3} data-test-sub="loading-content" />
|
||||
) : (
|
||||
<TabContent error={error} currentTab={currentTab} />
|
||||
<ErrorSampleDetailTabContent error={error} currentTab={currentTab} />
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function TabContent({
|
||||
export function ErrorSampleDetailTabContent({
|
||||
error,
|
||||
currentTab,
|
||||
}: {
|
||||
|
@ -383,11 +381,11 @@ function TabContent({
|
|||
const logStackframes = error?.error.log?.stacktrace;
|
||||
|
||||
switch (currentTab.key) {
|
||||
case logStacktraceTab.key:
|
||||
case ErrorTabKey.LogStackTrace:
|
||||
return (
|
||||
<Stacktrace stackframes={logStackframes} codeLanguage={codeLanguage} />
|
||||
);
|
||||
case exceptionStacktraceTab.key:
|
||||
case ErrorTabKey.ExceptionStacktrace:
|
||||
return (
|
||||
<ExceptionStacktrace
|
||||
codeLanguage={codeLanguage}
|
||||
|
|
|
@ -9,27 +9,33 @@ import { i18n } from '@kbn/i18n';
|
|||
import { isEmpty } from 'lodash';
|
||||
import { APMError } from '../../../../../typings/es_schemas/ui/apm_error';
|
||||
|
||||
export enum ErrorTabKey {
|
||||
LogStackTrace = 'log_stacktrace',
|
||||
ExceptionStacktrace = 'exception_stacktrace',
|
||||
Metadata = 'metadata',
|
||||
}
|
||||
|
||||
export interface ErrorTab {
|
||||
key: 'log_stacktrace' | 'exception_stacktrace' | 'metadata' | 'summary';
|
||||
key: ErrorTabKey;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const logStacktraceTab: ErrorTab = {
|
||||
key: 'log_stacktrace',
|
||||
key: ErrorTabKey.LogStackTrace,
|
||||
label: i18n.translate('xpack.apm.errorGroup.tabs.logStacktraceLabel', {
|
||||
defaultMessage: 'Log stack trace',
|
||||
}),
|
||||
};
|
||||
|
||||
export const exceptionStacktraceTab: ErrorTab = {
|
||||
key: 'exception_stacktrace',
|
||||
key: ErrorTabKey.ExceptionStacktrace,
|
||||
label: i18n.translate('xpack.apm.errorGroup.tabs.exceptionStacktraceLabel', {
|
||||
defaultMessage: 'Exception stack trace',
|
||||
}),
|
||||
};
|
||||
|
||||
export const metadataTab: ErrorTab = {
|
||||
key: 'metadata',
|
||||
key: ErrorTabKey.Metadata,
|
||||
label: i18n.translate('xpack.apm.errorGroup.tabs.metadataLabel', {
|
||||
defaultMessage: 'Metadata',
|
||||
}),
|
||||
|
|
|
@ -19,6 +19,7 @@ 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,
|
||||
|
@ -54,6 +55,9 @@ export function ApmAppRoot({
|
|||
const { history } = appMountParameters;
|
||||
const i18nCore = core.i18n;
|
||||
|
||||
const coPilotService =
|
||||
apmPluginContextValue.plugins.observability.getCoPilotService();
|
||||
|
||||
return (
|
||||
<RedirectAppLinks
|
||||
application={core.application}
|
||||
|
@ -66,38 +70,42 @@ export function ApmAppRoot({
|
|||
<i18nCore.Context>
|
||||
<TimeRangeIdContextProvider>
|
||||
<RouterProvider history={history} router={apmRouter as any}>
|
||||
<ApmErrorBoundary>
|
||||
<RedirectDependenciesToDependenciesInventory>
|
||||
<RedirectWithDefaultEnvironment>
|
||||
<RedirectWithDefaultDateRange>
|
||||
<RedirectWithOffset>
|
||||
<TrackPageview>
|
||||
<UpdateExecutionContextOnRouteChange>
|
||||
<BreadcrumbsContextProvider>
|
||||
<UrlParamsProvider>
|
||||
<LicenseProvider>
|
||||
<AnomalyDetectionJobsContextProvider>
|
||||
<InspectorContextProvider>
|
||||
<ApmThemeProvider>
|
||||
<MountApmHeaderActionMenu />
|
||||
<CoPilotContextProvider value={coPilotService}>
|
||||
<ApmErrorBoundary>
|
||||
<RedirectDependenciesToDependenciesInventory>
|
||||
<RedirectWithDefaultEnvironment>
|
||||
<RedirectWithDefaultDateRange>
|
||||
<RedirectWithOffset>
|
||||
<TrackPageview>
|
||||
<UpdateExecutionContextOnRouteChange>
|
||||
<BreadcrumbsContextProvider>
|
||||
<UrlParamsProvider>
|
||||
<LicenseProvider>
|
||||
<AnomalyDetectionJobsContextProvider>
|
||||
<InspectorContextProvider>
|
||||
<ApmThemeProvider>
|
||||
<MountApmHeaderActionMenu />
|
||||
|
||||
<Route
|
||||
component={ScrollToTopOnPathChange}
|
||||
/>
|
||||
<RouteRenderer />
|
||||
</ApmThemeProvider>
|
||||
</InspectorContextProvider>
|
||||
</AnomalyDetectionJobsContextProvider>
|
||||
</LicenseProvider>
|
||||
</UrlParamsProvider>
|
||||
</BreadcrumbsContextProvider>
|
||||
</UpdateExecutionContextOnRouteChange>
|
||||
</TrackPageview>
|
||||
</RedirectWithOffset>
|
||||
</RedirectWithDefaultDateRange>
|
||||
</RedirectWithDefaultEnvironment>
|
||||
</RedirectDependenciesToDependenciesInventory>
|
||||
</ApmErrorBoundary>
|
||||
<Route
|
||||
component={
|
||||
ScrollToTopOnPathChange
|
||||
}
|
||||
/>
|
||||
<RouteRenderer />
|
||||
</ApmThemeProvider>
|
||||
</InspectorContextProvider>
|
||||
</AnomalyDetectionJobsContextProvider>
|
||||
</LicenseProvider>
|
||||
</UrlParamsProvider>
|
||||
</BreadcrumbsContextProvider>
|
||||
</UpdateExecutionContextOnRouteChange>
|
||||
</TrackPageview>
|
||||
</RedirectWithOffset>
|
||||
</RedirectWithDefaultDateRange>
|
||||
</RedirectWithDefaultEnvironment>
|
||||
</RedirectDependenciesToDependenciesInventory>
|
||||
</ApmErrorBoundary>
|
||||
</CoPilotContextProvider>
|
||||
</RouterProvider>
|
||||
</TimeRangeIdContextProvider>
|
||||
</i18nCore.Context>
|
||||
|
|
|
@ -14,6 +14,7 @@ import { Route } from '@kbn/shared-ux-router';
|
|||
import { AppMountParameters } from '@kbn/core/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import '../index.scss';
|
||||
import { CoPilotContextProvider } from '@kbn/observability-plugin/public';
|
||||
import { LinkToLogsPage } from '../pages/link_to/link_to_logs';
|
||||
import { LogsPage } from '../pages/logs';
|
||||
import { InfraClientStartDeps, InfraClientStartExports } from '../types';
|
||||
|
@ -69,17 +70,19 @@ const LogsApp: React.FC<{
|
|||
theme$={theme$}
|
||||
triggersActionsUI={plugins.triggersActionsUi}
|
||||
>
|
||||
<Router history={history}>
|
||||
<KbnUrlStateStorageFromRouterProvider
|
||||
history={history}
|
||||
toastsService={core.notifications.toasts}
|
||||
>
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
{uiCapabilities?.logs?.show && <Route path="/" component={LogsPage} />}
|
||||
</Switch>
|
||||
</KbnUrlStateStorageFromRouterProvider>
|
||||
</Router>
|
||||
<CoPilotContextProvider value={plugins.observability.getCoPilotService()}>
|
||||
<Router history={history}>
|
||||
<KbnUrlStateStorageFromRouterProvider
|
||||
history={history}
|
||||
toastsService={core.notifications.toasts}
|
||||
>
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
{uiCapabilities?.logs?.show && <Route path="/" component={LogsPage} />}
|
||||
</Switch>
|
||||
</KbnUrlStateStorageFromRouterProvider>
|
||||
</Router>
|
||||
</CoPilotContextProvider>
|
||||
</CommonInfraProviders>
|
||||
</CoreProviders>
|
||||
);
|
||||
|
|
|
@ -18,11 +18,18 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
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 {
|
||||
useCoPilot,
|
||||
CoPilotPrompt,
|
||||
ObservabilityPublicStart,
|
||||
CoPilotContextProvider,
|
||||
} from '@kbn/observability-plugin/public';
|
||||
import { CoPilotPromptId } from '@kbn/observability-plugin/common';
|
||||
import { LogViewReference } from '../../../../common/log_views';
|
||||
import { TimeKey } from '../../../../common/time';
|
||||
import { useLogEntry } from '../../../containers/logs/log_entry';
|
||||
|
@ -42,9 +49,9 @@ export interface LogEntryFlyoutProps {
|
|||
export const useLogEntryFlyout = (logViewReference: LogViewReference) => {
|
||||
const flyoutRef = useRef<OverlayRef>();
|
||||
const {
|
||||
services: { http, data, uiSettings, application },
|
||||
services: { http, data, uiSettings, application, observability },
|
||||
overlays: { openFlyout },
|
||||
} = useKibana<{ data: DataPublicPluginStart }>();
|
||||
} = useKibana<{ data: DataPublicPluginStart; observability?: ObservabilityPublicStart }>();
|
||||
|
||||
const closeLogEntryFlyout = useCallback(() => {
|
||||
flyoutRef.current?.close();
|
||||
|
@ -61,15 +68,26 @@ export const useLogEntryFlyout = (logViewReference: LogViewReference) => {
|
|||
|
||||
flyoutRef.current = openFlyout(
|
||||
<KibanaReactContextProvider>
|
||||
<LogEntryFlyout
|
||||
logEntryId={logEntryId}
|
||||
onCloseFlyout={closeLogEntryFlyout}
|
||||
logViewReference={logViewReference}
|
||||
/>
|
||||
<CoPilotContextProvider value={observability?.getCoPilotService()}>
|
||||
<LogEntryFlyout
|
||||
logEntryId={logEntryId}
|
||||
onCloseFlyout={closeLogEntryFlyout}
|
||||
logViewReference={logViewReference}
|
||||
/>
|
||||
</CoPilotContextProvider>
|
||||
</KibanaReactContextProvider>
|
||||
);
|
||||
},
|
||||
[http, data, uiSettings, application, openFlyout, logViewReference, closeLogEntryFlyout]
|
||||
[
|
||||
http,
|
||||
data,
|
||||
uiSettings,
|
||||
application,
|
||||
openFlyout,
|
||||
logViewReference,
|
||||
closeLogEntryFlyout,
|
||||
observability,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -109,6 +127,16 @@ export const LogEntryFlyout = ({
|
|||
}
|
||||
}, [fetchLogEntry, logViewReference, logEntryId]);
|
||||
|
||||
const explainLogMessageParams = useMemo(() => {
|
||||
return logEntry ? { logEntry: { fields: logEntry.fields } } : undefined;
|
||||
}, [logEntry]);
|
||||
|
||||
const similarLogMessageParams = useMemo(() => {
|
||||
return logEntry ? { logEntry: { fields: logEntry.fields } } : undefined;
|
||||
}, [logEntry]);
|
||||
|
||||
const coPilotService = useCoPilot();
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onCloseFlyout} size="m">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
|
@ -168,7 +196,31 @@ export const LogEntryFlyout = ({
|
|||
) : undefined
|
||||
}
|
||||
>
|
||||
<LogEntryFieldsTable logEntry={logEntry} onSetFieldFilter={onSetFieldFilter} />
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
{coPilotService?.isEnabled() && explainLogMessageParams ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<CoPilotPrompt
|
||||
coPilot={coPilotService}
|
||||
title={explainLogMessageTitle}
|
||||
params={explainLogMessageParams}
|
||||
promptId={CoPilotPromptId.LogsExplainMessage}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{coPilotService?.isEnabled() && similarLogMessageParams ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<CoPilotPrompt
|
||||
coPilot={coPilotService}
|
||||
title={similarLogMessagesTitle}
|
||||
params={similarLogMessageParams}
|
||||
promptId={CoPilotPromptId.LogsFindSimilar}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<LogEntryFieldsTable logEntry={logEntry} onSetFieldFilter={onSetFieldFilter} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
) : (
|
||||
<CenteredEuiFlyoutBody>
|
||||
|
@ -185,6 +237,14 @@ export const LogEntryFlyout = ({
|
|||
);
|
||||
};
|
||||
|
||||
const explainLogMessageTitle = i18n.translate('xpack.infra.logFlyout.explainLogMessageTitle', {
|
||||
defaultMessage: "What's this message?",
|
||||
});
|
||||
|
||||
const similarLogMessagesTitle = i18n.translate('xpack.infra.logFlyout.similarLogMessagesTitle', {
|
||||
defaultMessage: 'How do I find similar log messages?',
|
||||
});
|
||||
|
||||
const loadingProgressMessage = i18n.translate('xpack.infra.logFlyout.loadingMessage', {
|
||||
defaultMessage: 'Searching log entry in shards',
|
||||
});
|
||||
|
|
240
x-pack/plugins/observability/common/co_pilot.ts
Normal file
240
x-pack/plugins/observability/common/co_pilot.ts
Normal file
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* 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,
|
||||
type CreateChatCompletionResponse,
|
||||
type CreateChatCompletionResponseChoicesInner,
|
||||
} from 'openai';
|
||||
|
||||
export enum OpenAIProvider {
|
||||
OpenAI = 'openAI',
|
||||
AzureOpenAI = 'azureOpenAI',
|
||||
}
|
||||
|
||||
export enum CoPilotPromptId {
|
||||
ProfilingExplainFunction = 'profilingExplainFunction',
|
||||
ProfilingOptimizeFunction = 'profilingOptimizeFunction',
|
||||
ApmExplainError = 'apmExplainError',
|
||||
LogsExplainMessage = 'logsExplainMessage',
|
||||
LogsFindSimilar = 'logsFindSimilar',
|
||||
}
|
||||
|
||||
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 logsapm-gpt, a helpful assistant for logs-based 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),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export const coPilotPrompts = {
|
||||
[CoPilotPromptId.ProfilingOptimizeFunction]: prompt({
|
||||
params: t.type({
|
||||
library: t.string,
|
||||
functionName: t.string,
|
||||
}),
|
||||
messages: ({ library, functionName }) => {
|
||||
return [
|
||||
PERF_GPT_SYSTEM_MESSAGE,
|
||||
{
|
||||
content: `Assuming the function ${functionName} from the library ${library} is consuming significant CPU resources.
|
||||
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.
|
||||
2. 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".
|
||||
|
||||
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
|
||||
etc.`,
|
||||
role: 'user',
|
||||
},
|
||||
];
|
||||
},
|
||||
}),
|
||||
[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 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.
|
||||
`,
|
||||
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',
|
||||
},
|
||||
];
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
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 };
|
||||
}
|
||||
>;
|
||||
};
|
|
@ -84,3 +84,5 @@ export {
|
|||
SYNTHETICS_TOTAL_TIMINGS,
|
||||
SYNTHETICS_WAIT_TIMINGS,
|
||||
} from './field_names/synthetics';
|
||||
|
||||
export { CoPilotPromptId, coPilotPrompts } from './co_pilot';
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
# 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.
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiIcon,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, 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';
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function CoPilotPrompt<TPromptId extends CoPilotPromptId>({
|
||||
title,
|
||||
coPilot,
|
||||
promptId,
|
||||
params,
|
||||
}: CoPilotPromptProps<TPromptId>) {
|
||||
const [hasOpened, setHasOpened] = useState(false);
|
||||
|
||||
const theme = useEuiTheme();
|
||||
|
||||
const conversation$ = useMemo(() => {
|
||||
return hasOpened
|
||||
? coPilot
|
||||
.prompt(promptId, params)
|
||||
.pipe(
|
||||
catchError((err) => of({ loading: false, error: err, message: String(err.message) }))
|
||||
)
|
||||
: new Observable<PromptObservableState>(() => {});
|
||||
}, [params, promptId, coPilot, hasOpened]);
|
||||
|
||||
const conversation = useObservable(conversation$);
|
||||
|
||||
useEffect(() => {}, [conversation$]);
|
||||
|
||||
const content = conversation?.message ?? '';
|
||||
|
||||
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} /> : <></>}
|
||||
</p>
|
||||
);
|
||||
} 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>
|
||||
);
|
||||
}
|
||||
|
||||
const tooltipContent = i18n.translate('xpack.observability.coPilotPrompt.askCoPilot', {
|
||||
defaultMessage: 'Ask Observability Co-Pilot for assistence',
|
||||
});
|
||||
|
||||
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>
|
||||
<EuiText size="m" color={theme.euiTheme.colors.primaryText}>
|
||||
<strong>{title}</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={tooltipContent}>
|
||||
<EuiIcon color={theme.euiTheme.colors.primaryText} type="questionInCircle" />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
initialIsOpen={false}
|
||||
onToggle={() => {
|
||||
setHasOpened(true);
|
||||
}}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
{inner}
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; 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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { concatMap, delay, Observable, of } from 'rxjs';
|
||||
import { type CreateChatCompletionResponseChunk } from '../../../common/co_pilot';
|
||||
import { type CoPilotService, type PromptObservableState } 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, http }: { enabled: boolean; http: HttpSetup }) {
|
||||
const service: CoPilotService = {
|
||||
isEnabled: () => enabled,
|
||||
prompt: (promptId, params) => {
|
||||
return new Observable<PromptObservableState>((observer) => {
|
||||
observer.next({ chunks: [], loading: true });
|
||||
|
||||
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[] = [];
|
||||
|
||||
function read() {
|
||||
reader!.read().then(({ done, value }) => {
|
||||
try {
|
||||
if (done) {
|
||||
observer.next({
|
||||
chunks,
|
||||
message: getMessageFromChunks(chunks),
|
||||
loading: false,
|
||||
});
|
||||
observer.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = decoder
|
||||
.decode(value)
|
||||
.trim()
|
||||
.split('\n')
|
||||
.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);
|
||||
observer.next({ chunks, message: getMessageFromChunks(chunks), loading: true });
|
||||
});
|
||||
} catch (err) {
|
||||
observer.error(err);
|
||||
return;
|
||||
}
|
||||
read();
|
||||
});
|
||||
}
|
||||
|
||||
read();
|
||||
|
||||
return () => {
|
||||
reader.cancel();
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
observer.error(err);
|
||||
});
|
||||
}).pipe(concatMap((value) => of(value).pipe(delay(50))));
|
||||
},
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import { createContext } from 'react';
|
||||
import { type CoPilotService } from '../../typings/co_pilot';
|
||||
|
||||
export const CoPilotContext = createContext<CoPilotService | undefined>(undefined);
|
||||
|
||||
export const CoPilotContextProvider = CoPilotContext.Provider;
|
18
x-pack/plugins/observability/public/hooks/use_co_pilot.ts
Normal file
18
x-pack/plugins/observability/public/hooks/use_co_pilot.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; 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;
|
||||
}
|
|
@ -84,3 +84,7 @@ 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';
|
||||
|
|
|
@ -86,6 +86,9 @@ const withCore = makeDecorator({
|
|||
uptime: { enabled: false },
|
||||
},
|
||||
},
|
||||
coPilot: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -42,6 +42,9 @@ jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
|
|||
uptime: { enabled: false },
|
||||
},
|
||||
},
|
||||
coPilot: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
|
||||
ObservabilityPageTemplate: KibanaPageTemplate,
|
||||
|
|
|
@ -60,6 +60,8 @@ import {
|
|||
} from './rules/create_observability_rule_type_registry';
|
||||
import { createUseRulesLink } from './hooks/create_use_rules_link';
|
||||
import { registerObservabilityRuleTypes } from './rules/register_observability_rule_types';
|
||||
import { createCoPilotService } from './context/co_pilot_context/create_co_pilot_service';
|
||||
import { type CoPilotService } from './typings/co_pilot';
|
||||
|
||||
export interface ConfigSchema {
|
||||
unsafe: {
|
||||
|
@ -75,6 +77,9 @@ export interface ConfigSchema {
|
|||
};
|
||||
};
|
||||
};
|
||||
coPilot?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
}
|
||||
export type ObservabilityPublicSetup = ReturnType<Plugin['setup']>;
|
||||
|
||||
|
@ -125,6 +130,8 @@ 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[] = [
|
||||
|
@ -314,6 +321,11 @@ export class Plugin
|
|||
)
|
||||
);
|
||||
|
||||
this.coPilotService = createCoPilotService({
|
||||
enabled: !!config.coPilot?.enabled,
|
||||
http: coreSetup.http,
|
||||
});
|
||||
|
||||
return {
|
||||
dashboard: { register: registerDataHandler },
|
||||
observabilityRuleTypeRegistry: this.observabilityRuleTypeRegistry,
|
||||
|
@ -321,6 +333,7 @@ export class Plugin
|
|||
rulesLocator,
|
||||
ruleDetailsLocator,
|
||||
sloDetailsLocator,
|
||||
getCoPilotService: () => this.coPilotService!,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -350,6 +363,7 @@ export class Plugin
|
|||
return {
|
||||
observabilityRuleTypeRegistry: this.observabilityRuleTypeRegistry,
|
||||
useRulesLink: createUseRulesLink(),
|
||||
getCoPilotService: () => this.coPilotService!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
27
x-pack/plugins/observability/public/typings/co_pilot.ts
Normal file
27
x-pack/plugins/observability/public/typings/co_pilot.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Observable } from 'rxjs';
|
||||
import {
|
||||
type CoPilotPromptId,
|
||||
type PromptParamsOf,
|
||||
type CreateChatCompletionResponseChunk,
|
||||
} from '../../common/co_pilot';
|
||||
|
||||
export interface PromptObservableState {
|
||||
chunks: CreateChatCompletionResponseChunk[];
|
||||
message?: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export interface CoPilotService {
|
||||
isEnabled: () => boolean;
|
||||
prompt<TPromptId extends CoPilotPromptId>(
|
||||
promptId: TPromptId,
|
||||
params: PromptParamsOf<TPromptId>
|
||||
): Observable<PromptObservableState>;
|
||||
}
|
|
@ -33,6 +33,9 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) {
|
|||
uptime: { enabled: false },
|
||||
},
|
||||
},
|
||||
coPilot: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
const mockTheme: CoreTheme = {
|
||||
darkMode: false,
|
||||
|
|
|
@ -37,6 +37,9 @@ const defaultConfig: ConfigSchema = {
|
|||
uptime: { enabled: false },
|
||||
},
|
||||
},
|
||||
coPilot: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
|
|
|
@ -18,6 +18,7 @@ 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';
|
||||
|
||||
|
@ -42,11 +43,15 @@ const configSchema = schema.object({
|
|||
}),
|
||||
}),
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
coPilot: schema.maybe(observabilityCoPilotConfig),
|
||||
});
|
||||
|
||||
export const config: PluginConfigDescriptor = {
|
||||
exposeToBrowser: {
|
||||
unsafe: true,
|
||||
coPilot: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
schema: configSchema,
|
||||
};
|
||||
|
|
|
@ -49,6 +49,7 @@ import { registerRuleTypes } from './lib/rules/register_rule_types';
|
|||
import { SLO_BURN_RATE_RULE_ID } from '../common/constants';
|
||||
import { registerSloUsageCollector } from './lib/collectors/register';
|
||||
import { sloRuleFieldMap } from './lib/rules/slo_burn_rate/field_map';
|
||||
import { OpenAIService } from './services/openai';
|
||||
|
||||
export type ObservabilityPluginSetup = ReturnType<ObservabilityPlugin['setup']>;
|
||||
|
||||
|
@ -253,12 +254,15 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
);
|
||||
registerSloUsageCollector(plugins.usageCollection);
|
||||
|
||||
const openAIService = config.coPilot?.enabled ? new OpenAIService(config.coPilot) : undefined;
|
||||
|
||||
core.getStartServices().then(([coreStart, pluginStart]) => {
|
||||
registerRoutes({
|
||||
core,
|
||||
dependencies: {
|
||||
ruleDataService,
|
||||
getRulesClientWithRequest: pluginStart.alerting.getRulesClientWithRequest,
|
||||
getOpenAIClient: () => openAIService?.client,
|
||||
},
|
||||
logger: this.logger,
|
||||
repository: getObservabilityServerRouteRepository(),
|
||||
|
@ -278,6 +282,9 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
const api = await annotationsApiPromise;
|
||||
return api?.getScopedAnnotationsClient(...args);
|
||||
},
|
||||
getOpenAIClient() {
|
||||
return openAIService?.client;
|
||||
},
|
||||
alertsLocator,
|
||||
};
|
||||
}
|
||||
|
|
51
x-pack/plugins/observability/server/routes/copilot/route.ts
Normal file
51
x-pack/plugins/observability/server/routes/copilot/route.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import Boom from '@hapi/boom';
|
||||
import { ServerRoute } from '@kbn/server-route-repository';
|
||||
import * as t from 'io-ts';
|
||||
import { map } from 'lodash';
|
||||
import { CreateChatCompletionResponse } from 'openai';
|
||||
import { Readable } from 'stream';
|
||||
import { CoPilotPromptMap, coPilotPrompts } from '../../../common/co_pilot';
|
||||
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: [],
|
||||
},
|
||||
handler: async (resources): Promise<CreateChatCompletionResponse | Readable> => {
|
||||
const client = resources.dependencies.getOpenAIClient();
|
||||
|
||||
if (!client) {
|
||||
throw Boom.notImplemented();
|
||||
}
|
||||
|
||||
return client.chatCompletion.create(prompt.messages(resources.params.body as any));
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
export const observabilityCoPilotRouteRepository = {
|
||||
...promptRoutes,
|
||||
};
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { compositeSloRouteRepository } from './composite_slo/route';
|
||||
import { observabilityCoPilotRouteRepository } from './copilot/route';
|
||||
import { rulesRouteRepository } from './rules/route';
|
||||
import { sloRouteRepository } from './slo/route';
|
||||
|
||||
|
@ -14,6 +15,7 @@ export function getObservabilityServerRouteRepository() {
|
|||
...rulesRouteRepository,
|
||||
...sloRouteRepository,
|
||||
...compositeSloRouteRepository,
|
||||
...observabilityCoPilotRouteRepository,
|
||||
};
|
||||
return repository;
|
||||
}
|
||||
|
|
|
@ -4,21 +4,22 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import Boom from '@hapi/boom';
|
||||
import { RulesClientApi } from '@kbn/alerting-plugin/server/types';
|
||||
import { CoreSetup, KibanaRequest, Logger, RouteRegistrar } from '@kbn/core/server';
|
||||
import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server';
|
||||
import {
|
||||
decodeRequestParams,
|
||||
parseEndpoint,
|
||||
routeValidationObject,
|
||||
} from '@kbn/server-route-repository';
|
||||
import { CoreSetup, KibanaRequest, Logger, RouteRegistrar } from '@kbn/core/server';
|
||||
import Boom from '@hapi/boom';
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server';
|
||||
import { RulesClientApi } from '@kbn/alerting-plugin/server/types';
|
||||
|
||||
import axios from 'axios';
|
||||
import * as t from 'io-ts';
|
||||
import { getHTTPResponseCode, ObservabilityError } from '../errors';
|
||||
import { IOpenAIClient } from '../services/openai/types';
|
||||
import { ObservabilityRequestHandlerContext } from '../types';
|
||||
import { AbstractObservabilityServerRouteRepository } from './types';
|
||||
import { getHTTPResponseCode, ObservabilityError } from '../errors';
|
||||
|
||||
interface RegisterRoutes {
|
||||
core: CoreSetup;
|
||||
|
@ -30,6 +31,7 @@ interface RegisterRoutes {
|
|||
export interface RegisterRoutesDependencies {
|
||||
ruleDataService: RuleDataPluginService;
|
||||
getRulesClientWithRequest: (request: KibanaRequest) => RulesClientApi;
|
||||
getOpenAIClient: () => IOpenAIClient | undefined;
|
||||
}
|
||||
|
||||
export function registerRoutes({ repository, core, logger, dependencies }: RegisterRoutes) {
|
||||
|
@ -82,6 +84,16 @@ export function registerRoutes({ repository, core, logger, dependencies }: Regis
|
|||
});
|
||||
}
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(error);
|
||||
return response.customError({
|
||||
statusCode: error.response?.status || 500,
|
||||
body: {
|
||||
message: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (Boom.isBoom(error)) {
|
||||
logger.error(error.output.payload.message);
|
||||
return response.customError({
|
||||
|
|
|
@ -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 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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 }),
|
||||
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>;
|
23
x-pack/plugins/observability/server/services/openai/index.ts
Normal file
23
x-pack/plugins/observability/server/services/openai/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export function pipeStreamingResponse(response: { data: any; headers: Record<string, string> }) {
|
||||
response.headers['Content-Type'] = 'dont-compress-this';
|
||||
|
||||
return response.data;
|
||||
}
|
17
x-pack/plugins/observability/server/services/openai/types.ts
Normal file
17
x-pack/plugins/observability/server/services/openai/types.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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>;
|
||||
};
|
||||
}
|
|
@ -13,6 +13,7 @@ import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
|
|||
import React, { useMemo } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public';
|
||||
import { CoPilotContextProvider } from '@kbn/observability-plugin/public';
|
||||
import { CheckSetup } from './components/check_setup';
|
||||
import { ProfilingDependenciesContextProvider } from './components/contexts/profiling_dependencies/profiling_dependencies_context';
|
||||
import { RouteBreadcrumbsContextProvider } from './components/contexts/route_breadcrumbs_context';
|
||||
|
@ -78,35 +79,39 @@ function App({
|
|||
};
|
||||
}, [coreStart, coreSetup, pluginsStart, pluginsSetup, profilingFetchServices]);
|
||||
|
||||
const coPilotService = pluginsSetup.observability.getCoPilotService();
|
||||
|
||||
return (
|
||||
<KibanaThemeProvider theme$={theme$}>
|
||||
<KibanaContextProvider services={{ ...coreStart, ...pluginsStart, storage }}>
|
||||
<i18nCore.Context>
|
||||
<RedirectAppLinks coreStart={coreStart} currentAppId="profiling">
|
||||
<RouterProvider router={profilingRouter as any} history={history}>
|
||||
<RouterErrorBoundary>
|
||||
<TimeRangeContextProvider>
|
||||
<ProfilingDependenciesContextProvider value={profilingDependencies}>
|
||||
<LicenseProvider>
|
||||
<>
|
||||
<CheckSetup>
|
||||
<RedirectWithDefaultDateRange>
|
||||
<RouteBreadcrumbsContextProvider>
|
||||
<RouteRenderer />
|
||||
</RouteBreadcrumbsContextProvider>
|
||||
</RedirectWithDefaultDateRange>
|
||||
</CheckSetup>
|
||||
<MountProfilingActionMenu
|
||||
setHeaderActionMenu={setHeaderActionMenu}
|
||||
theme$={theme$}
|
||||
/>
|
||||
</>
|
||||
</LicenseProvider>
|
||||
</ProfilingDependenciesContextProvider>
|
||||
</TimeRangeContextProvider>
|
||||
</RouterErrorBoundary>
|
||||
</RouterProvider>
|
||||
</RedirectAppLinks>
|
||||
<CoPilotContextProvider value={coPilotService}>
|
||||
<RedirectAppLinks coreStart={coreStart} currentAppId="profiling">
|
||||
<RouterProvider router={profilingRouter as any} history={history}>
|
||||
<RouterErrorBoundary>
|
||||
<TimeRangeContextProvider>
|
||||
<ProfilingDependenciesContextProvider value={profilingDependencies}>
|
||||
<LicenseProvider>
|
||||
<>
|
||||
<CheckSetup>
|
||||
<RedirectWithDefaultDateRange>
|
||||
<RouteBreadcrumbsContextProvider>
|
||||
<RouteRenderer />
|
||||
</RouteBreadcrumbsContextProvider>
|
||||
</RedirectWithDefaultDateRange>
|
||||
</CheckSetup>
|
||||
<MountProfilingActionMenu
|
||||
setHeaderActionMenu={setHeaderActionMenu}
|
||||
theme$={theme$}
|
||||
/>
|
||||
</>
|
||||
</LicenseProvider>
|
||||
</ProfilingDependenciesContextProvider>
|
||||
</TimeRangeContextProvider>
|
||||
</RouterErrorBoundary>
|
||||
</RouterProvider>
|
||||
</RedirectAppLinks>
|
||||
</CoPilotContextProvider>
|
||||
</i18nCore.Context>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useCoPilot, CoPilotPrompt } from '@kbn/observability-plugin/public';
|
||||
import React, { useMemo } from 'react';
|
||||
import { CoPilotPromptId } from '@kbn/observability-plugin/common';
|
||||
import { FrameSymbolStatus, getFrameSymbolStatus } from '../../../common/profiling';
|
||||
import { FrameInformationPanel } from './frame_information_panel';
|
||||
import { getImpactRows } from './get_impact_rows';
|
||||
|
@ -31,6 +33,17 @@ export interface Props {
|
|||
}
|
||||
|
||||
export function FrameInformationWindow({ frame, totalSamples, totalSeconds }: Props) {
|
||||
const coPilotService = useCoPilot();
|
||||
|
||||
const promptParams = useMemo(() => {
|
||||
return frame?.functionName && frame?.exeFileName
|
||||
? {
|
||||
functionName: frame?.functionName,
|
||||
library: frame?.exeFileName,
|
||||
}
|
||||
: undefined;
|
||||
}, [frame?.functionName, frame?.exeFileName]);
|
||||
|
||||
if (!frame) {
|
||||
return (
|
||||
<FrameInformationPanel>
|
||||
|
@ -84,6 +97,30 @@ export function FrameInformationWindow({ frame, totalSamples, totalSeconds }: Pr
|
|||
<EuiFlexItem>
|
||||
<KeyValueList rows={informationRows} />
|
||||
</EuiFlexItem>
|
||||
{coPilotService?.isEnabled() && promptParams ? (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<CoPilotPrompt
|
||||
coPilot={coPilotService}
|
||||
promptId={CoPilotPromptId.ProfilingExplainFunction}
|
||||
params={promptParams}
|
||||
title={i18n.translate('xpack.profiling.frameInformationWindow.explainFunction', {
|
||||
defaultMessage: 'Explain function',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CoPilotPrompt
|
||||
coPilot={coPilotService}
|
||||
promptId={CoPilotPromptId.ProfilingOptimizeFunction}
|
||||
params={promptParams}
|
||||
title={i18n.translate('xpack.profiling.frameInformationWindow.optimizeFunction', {
|
||||
defaultMessage: 'Optimize function',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
) : undefined}
|
||||
{symbolStatus !== FrameSymbolStatus.SYMBOLIZED && (
|
||||
<EuiFlexItem>
|
||||
<MissingSymbolsCallout frameType={frame.frameType} />
|
||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -10990,6 +10990,13 @@ axios@^0.21.1:
|
|||
dependencies:
|
||||
follow-redirects "^1.14.0"
|
||||
|
||||
axios@^0.26.0:
|
||||
version "0.26.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
|
||||
integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.8"
|
||||
|
||||
axios@^0.27.2:
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
|
||||
|
@ -16294,7 +16301,7 @@ folktale@2.3.2:
|
|||
resolved "https://registry.yarnpkg.com/folktale/-/folktale-2.3.2.tgz#38231b039e5ef36989920cbf805bf6b227bf4fd4"
|
||||
integrity sha512-+8GbtQBwEqutP0v3uajDDoN64K2ehmHd0cjlghhxh0WpcfPzAIjPA03e1VvHlxL02FVGR0A6lwXsNQKn3H1RNQ==
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0:
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.8, follow-redirects@^1.14.9, follow-redirects@^1.15.0:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
||||
|
@ -22526,6 +22533,14 @@ open@^8.0.9, open@^8.4.0:
|
|||
is-docker "^2.1.1"
|
||||
is-wsl "^2.2.0"
|
||||
|
||||
openai@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/openai/-/openai-3.2.1.tgz#1fa35bdf979cbde8453b43f2dd3a7d401ee40866"
|
||||
integrity sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==
|
||||
dependencies:
|
||||
axios "^0.26.0"
|
||||
form-data "^4.0.0"
|
||||
|
||||
openapi-types@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-10.0.0.tgz#0debbf663b2feed0322030b5b7c9080804076934"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue