diff --git a/package.json b/package.json index 4ad7bef830d37..c8fa01e81009e 100644 --- a/package.json +++ b/package.json @@ -772,7 +772,6 @@ "@opentelemetry/sdk-metrics-base": "^0.31.0", "@opentelemetry/semantic-conventions": "^1.4.0", "@reduxjs/toolkit": "1.7.2", - "@sindresorhus/fnv1a": "^3.0.0", "@slack/webhook": "^5.0.4", "@tanstack/react-query": "^4.29.12", "@tanstack/react-query-devtools": "^4.29.12", @@ -840,6 +839,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", @@ -1437,7 +1437,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", diff --git a/x-pack/plugins/apm/kibana.jsonc b/x-pack/plugins/apm/kibana.jsonc index 6acd21a9ad213..7c6b3266940a4 100644 --- a/x-pack/plugins/apm/kibana.jsonc +++ b/x-pack/plugins/apm/kibana.jsonc @@ -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" ] } } diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index da2fe62b47266..6f92ded082a01 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -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 diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_co_pilot_prompt.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_co_pilot_prompt.tsx deleted file mode 100644 index dd78c7f090e98..0000000000000 --- a/x-pack/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_co_pilot_prompt.tsx +++ /dev/null @@ -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 ? ( - <> - - - - -
{ - setLogStacktrace(next?.innerText ?? ''); - }} - style={{ display: 'none' }} - > - {error.error.log?.message && ( - - )} -
-
{ - setExceptionStacktrace(next?.innerText ?? ''); - }} - style={{ display: 'none' }} - > - {error.error.exception?.length && ( - - )} -
- - ) : ( - <> - ); -} diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_contextual_insight.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_contextual_insight.tsx new file mode 100644 index 0000000000000..5137713457d5c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_contextual_insight.tsx @@ -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(() => { + 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 ? ( + <> + + + + +
{ + setLogStacktrace(next?.innerText ?? ''); + }} + style={{ display: 'none' }} + > + {error.error.log?.message && ( + + )} +
+
{ + setExceptionStacktrace(next?.innerText ?? ''); + }} + style={{ display: 'none' }} + > + {error.error.exception?.length && ( + + )} +
+ + ) : ( + <> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx index 8d20aba18e292..574e9db2f507b 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx @@ -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({ )} - + {tabs.map(({ key, label }) => { diff --git a/x-pack/plugins/apm/public/components/routing/app_root/index.tsx b/x-pack/plugins/apm/public/components/routing/app_root/index.tsx index 990d3dc238a35..a9a0331d1c8c7 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root/index.tsx @@ -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 ( - - - + + + @@ -105,9 +106,9 @@ export function ApmAppRoot({ - - - + + + diff --git a/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx index 32084c66b289b..6bc5fd0ca2eb0 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx @@ -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); diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 253e83c234dcc..357ab12668d25 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -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', { diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index 77edc4a4943a4..a81771fbd1f6b 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -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/**/*"] } diff --git a/x-pack/plugins/infra/kibana.jsonc b/x-pack/plugins/infra/kibana.jsonc index 973d979ed90aa..ee8e8baa83337 100644 --- a/x-pack/plugins/infra/kibana.jsonc +++ b/x-pack/plugins/infra/kibana.jsonc @@ -22,6 +22,7 @@ "lens", "logsShared", "observability", + "observabilityAIAssistant", "observabilityShared", "ruleRegistry", "security", @@ -36,6 +37,7 @@ "requiredBundles": [ "unifiedSearch", "observability", + "observabilityAIAssistant", "licenseManagement", "kibanaUtils", "kibanaReact", diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx index fda5c9934b904..e6e649f964747 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/log_rate_analysis.tsx @@ -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,9 +174,54 @@ export const LogRateAnalysis: FC = ({ r setLogSpikeParams(significantFieldValues ? { significantFieldValues } : undefined); }; - const coPilotService = useCoPilot(); + const aiAssistant = useObservabilityAIAssistant(); + const hasLogSpikeParams = logSpikeParams && logSpikeParams.significantFieldValues?.length > 0; + const messages = useMemo(() => { + if (!logSpikeParams) { + 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; return ( @@ -215,15 +265,9 @@ export const LogRateAnalysis: FC = ({ r - {coPilotService?.isEnabled() && hasLogSpikeParams ? ( + {aiAssistant.isEnabled() && hasLogSpikeParams ? ( - + ) : null} diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx index 00041cdffec02..bf77c7f75e1c5 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx @@ -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,8 @@ const AlertDetailsAppSection = ({ alert, setAlertSummaryFields, }: AlertDetailsAppSectionProps) => { - const { observability, logsShared } = useKibanaContextForPlugin().services; + const { observability, 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 +243,14 @@ const AlertDetailsAppSection = ({ }; return ( - + {getLogRatioChart()} {getLogCountChart()} {getLogRateAnalysisSection()} {getLogsHistoryChart()} - + ); }; diff --git a/x-pack/plugins/infra/public/apps/common_providers.tsx b/x-pack/plugins/infra/public/apps/common_providers.tsx index 901e8c8b47edf..b0eccab420fd6 100644 --- a/x-pack/plugins/infra/public/apps/common_providers.tsx +++ b/x-pack/plugins/infra/public/apps/common_providers.tsx @@ -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<{ - + {children} - + diff --git a/x-pack/plugins/infra/public/apps/logs_app.tsx b/x-pack/plugins/infra/public/apps/logs_app.tsx index 3d6346818873a..f75f6fa29efd7 100644 --- a/x-pack/plugins/infra/public/apps/logs_app.tsx +++ b/x-pack/plugins/infra/public/apps/logs_app.tsx @@ -67,7 +67,7 @@ const LogsApp: React.FC<{ storage={storage} theme$={theme$} triggersActionsUI={plugins.triggersActionsUi} - observabilityCopilot={plugins.observability.getCoPilotService()} + observabilityAIAssistant={plugins.observabilityAIAssistant} > diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx index c3e1cbd307398..061acb4c803dd 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx @@ -213,7 +213,7 @@ const ProcessesTableBody = ({ items, currentTime }: TableBodyProps) => ( {column.render ? column.render(item[column.field], currentTime) : item[column.field]} )); - return ; + return ; })} ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx index c1365364c9ae1..0a748eee09835 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx @@ -22,34 +22,54 @@ 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(() => { + if (!command) { + return undefined; + } + const now = new Date().toISOString(); + return [ + { + '@timestamp': now, + message: { + role: MessageRole.System, + content: '', + }, + }, + { + '@timestamp': now, + message: { + role: MessageRole.User, + content: '', + }, + }, + ]; }, [command]); return ( <> - {coPilotService?.isEnabled() && explainProcessParams ? ( + {aiAssistant.isEnabled() && explainProcessMessages ? ( - @@ -59,7 +79,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 +155,7 @@ export const ProcessRow = ({ cells, item, supportCopilot = false }: Props) => { - {supportCopilot && } + {supportAIAssistant && } )} diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 488d92573e746..c663f57592f52 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -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; diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index 3f1ece70501de..d1f52ab041c1b 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -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", diff --git a/x-pack/plugins/logs_shared/kibana.jsonc b/x-pack/plugins/logs_shared/kibana.jsonc index fea2ccc878485..e14b6ad8dc837 100644 --- a/x-pack/plugins/logs_shared/kibana.jsonc +++ b/x-pack/plugins/logs_shared/kibana.jsonc @@ -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", ], diff --git a/x-pack/plugins/logs_shared/public/components/logging/log_entry_flyout/log_entry_flyout.tsx b/x-pack/plugins/logs_shared/public/components/logging/log_entry_flyout/log_entry_flyout.tsx index 70185794f380f..5d0c6123d4dca 100644 --- a/x-pack/plugins/logs_shared/public/components/logging/log_entry_flyout/log_entry_flyout.tsx +++ b/x-pack/plugins/logs_shared/public/components/logging/log_entry_flyout/log_entry_flyout.tsx @@ -15,21 +15,21 @@ import { EuiTextColor, EuiTitle, } from '@elastic/eui'; -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 type { Query } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +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(); 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( - + - + ); }, @@ -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(() => { + 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(() => { + 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 ( @@ -197,25 +246,19 @@ export const LogEntryFlyout = ({ } > - {coPilotService?.isEnabled() && explainLogMessageParams ? ( + {aiAssistant.isEnabled() && explainLogMessageMessages ? ( - ) : null} - {coPilotService?.isEnabled() && similarLogMessageParams ? ( + {aiAssistant.isEnabled() && similarLogMessageMessages ? ( - ) : null} diff --git a/x-pack/plugins/observability/common/co_pilot/index.ts b/x-pack/plugins/observability/common/co_pilot/index.ts deleted file mode 100644 index 0d1765402a53e..0000000000000 --- a/x-pack/plugins/observability/common/co_pilot/index.ts +++ /dev/null @@ -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); diff --git a/x-pack/plugins/observability/common/co_pilot/prompts.ts b/x-pack/plugins/observability/common/co_pilot/prompts.ts deleted file mode 100644 index a1a8264c43882..0000000000000 --- a/x-pack/plugins/observability/common/co_pilot/prompts.ts +++ /dev/null @@ -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>({ - params, - messages, -}: { - params: TParams; - messages: (params: t.OutputOf) => 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 = t.OutputOf< - { - [TKey in keyof CoPilotPromptMap]: CoPilotPromptMap[TKey]; - }[TPromptId]['params'] ->; - -export type CreateChatCompletionResponseChunk = Omit & { - choices: Array< - Omit & { - delta: { content?: string }; - } - >; -}; diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index d71b292803996..b0c87966ef89b 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -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'; diff --git a/x-pack/plugins/observability/public/components/co_pilot_prompt/README.md b/x-pack/plugins/observability/public/components/co_pilot_prompt/README.md deleted file mode 100644 index e1d608d329b60..0000000000000 --- a/x-pack/plugins/observability/public/components/co_pilot_prompt/README.md +++ /dev/null @@ -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 ( - - - - ); -} -``` - -### 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() && ( - - ); -} -``` - -## 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. diff --git a/x-pack/plugins/observability/public/components/co_pilot_prompt/co_pilot_prompt.tsx b/x-pack/plugins/observability/public/components/co_pilot_prompt/co_pilot_prompt.tsx deleted file mode 100644 index c1241ca62d93b..0000000000000 --- a/x-pack/plugins/observability/public/components/co_pilot_prompt/co_pilot_prompt.tsx +++ /dev/null @@ -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 { - title: string; - promptId: TPromptId; - coPilot: CoPilotService; - params: PromptParamsOf; - feedbackEnabled: boolean; -} - -// eslint-disable-next-line import/no-default-export -export default function CoPilotPrompt({ - title, - coPilot, - promptId, - params, - feedbackEnabled, -}: CoPilotPromptProps) { - const [hasOpened, setHasOpened] = useState(false); - - const theme = useEuiTheme(); - - const [responseTime, setResponseTime] = useState(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(() => {}); - }, [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 = ( - <> -

- {content} - {state === 'streaming' ? : undefined} -

- {state === 'complete' ? ( - <> - - {coPilot.isTrackingEnabled() && feedbackEnabled ? ( - - ) : undefined} - - ) : undefined} - - ); - } else if (state === 'init' || state === 'loading') { - inner = ( - - - - - - - {i18n.translate('xpack.observability.coPilotPrompt.chatLoading', { - defaultMessage: 'Waiting for a response...', - })} - - - - ); - } else { - /* if (state === 'error') {*/ - inner = ( - - - - - - {content} - - - ); - } - - return ( - - - - - - - {title} - - - - - {i18n.translate('xpack.observability.coPilotChatPrompt.subtitle', { - defaultMessage: 'Get helpful insights from our Elastic AI Assistant', - })} - - - - - - - -
- } - initialIsOpen={false} - onToggle={() => { - setHasOpened(true); - }} - > - - - - {inner} - - - ); -} diff --git a/x-pack/plugins/observability/public/components/co_pilot_prompt/co_pilot_prompt_feedback.tsx b/x-pack/plugins/observability/public/components/co_pilot_prompt/co_pilot_prompt_feedback.tsx deleted file mode 100644 index f51d8061ce491..0000000000000 --- a/x-pack/plugins/observability/public/components/co_pilot_prompt/co_pilot_prompt_feedback.tsx +++ /dev/null @@ -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 ( - - - - - - - {i18n.translate('xpack.observability.coPilotPrompt.feedbackSubmittedText', { - defaultMessage: - "Thank you for submitting your feedback! We'll use this to improve responses.", - })} - - - - ); - } - - return ( - - - - {i18n.translate('xpack.observability.coPilotPrompt.feedbackActionTitle', { - defaultMessage: 'Did you find this response helpful?', - })} - - - - { - submitFeedback(true); - }} - > - {i18n.translate('xpack.observability.coPilotPrompt.likedFeedbackButtonTitle', { - defaultMessage: 'Yes', - })} - - - - { - submitFeedback(false); - }} - > - {i18n.translate('xpack.observability.coPilotPrompt.dislikedFeedbackButtonTitle', { - defaultMessage: 'No', - })} - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/co_pilot_prompt/index.tsx b/x-pack/plugins/observability/public/components/co_pilot_prompt/index.tsx deleted file mode 100644 index eced0eecb5f48..0000000000000 --- a/x-pack/plugins/observability/public/components/co_pilot_prompt/index.tsx +++ /dev/null @@ -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) { - return ( - }> - - - ); -} diff --git a/x-pack/plugins/observability/public/context/co_pilot_context/create_co_pilot_service.ts b/x-pack/plugins/observability/public/context/co_pilot_context/create_co_pilot_service.ts deleted file mode 100644 index 9f2bdddfe412e..0000000000000 --- a/x-pack/plugins/observability/public/context/co_pilot_context/create_co_pilot_service.ts +++ /dev/null @@ -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; -} diff --git a/x-pack/plugins/observability/public/hooks/use_co_pilot.ts b/x-pack/plugins/observability/public/hooks/use_co_pilot.ts deleted file mode 100644 index f104057426e84..0000000000000 --- a/x-pack/plugins/observability/public/hooks/use_co_pilot.ts +++ /dev/null @@ -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; -} diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index fabc5914087ff..6c0bc5fa4d8cb 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -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'; diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 287248538b633..9092a5e5fb885 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -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: { @@ -92,13 +90,8 @@ export interface ConfigSchema { }; }; compositeSlo: { enabled: boolean }; - aiAssistant?: { - enabled: boolean; - feedback: { - enabled: boolean; - }; - }; } + export type ObservabilityPublicSetup = ReturnType; export interface ObservabilityPublicPluginsSetup { @@ -150,8 +143,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[] = [ @@ -343,12 +334,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, @@ -357,7 +342,6 @@ export class Plugin ruleDetailsLocator, sloDetailsLocator, sloEditLocator, - getCoPilotService: () => this.coPilotService!, }; } @@ -387,7 +371,6 @@ export class Plugin return { observabilityRuleTypeRegistry: this.observabilityRuleTypeRegistry, useRulesLink: createUseRulesLink(), - getCoPilotService: () => this.coPilotService!, }; } } diff --git a/x-pack/plugins/observability/public/typings/co_pilot.ts b/x-pack/plugins/observability/public/typings/co_pilot.ts deleted file mode 100644 index 3dac1895a8ed3..0000000000000 --- a/x-pack/plugins/observability/public/typings/co_pilot.ts +++ /dev/null @@ -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( - promptId: TPromptId, - params: PromptParamsOf - ): Observable; - track: (options: { - messages: ChatCompletionRequestMessage[]; - response: string; - promptId: CoPilotPromptId; - feedbackAction?: 'thumbsup' | 'thumbsdown'; - responseTime: number; - }) => Promise; -} diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index 1f06f63c5de00..f80378a57d719 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -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'; @@ -51,7 +50,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 }), }), diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index ccd437865f47d..02cc2f33cc9f5 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -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; @@ -244,10 +243,6 @@ export class ObservabilityPlugin implements Plugin { ); 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 { }, ruleDataService, getRulesClientWithRequest: pluginStart.alerting.getRulesClientWithRequest, - getOpenAIClient: () => openAIService?.client, }, logger: this.logger, repository: getObservabilityServerRouteRepository(config), @@ -279,9 +273,6 @@ export class ObservabilityPlugin implements Plugin { const api = await annotationsApiPromise; return api?.getScopedAnnotationsClient(...args); }, - getOpenAIClient() { - return openAIService?.client; - }, alertsLocator, }; } diff --git a/x-pack/plugins/observability/server/routes/copilot/route.ts b/x-pack/plugins/observability/server/routes/copilot/route.ts deleted file mode 100644 index d3e6c0213162c..0000000000000 --- a/x-pack/plugins/observability/server/routes/copilot/route.ts +++ /dev/null @@ -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 => { - 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 => { - 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, -}; diff --git a/x-pack/plugins/observability/server/routes/get_global_observability_server_route_repository.ts b/x-pack/plugins/observability/server/routes/get_global_observability_server_route_repository.ts index 6bbd3eccb7b59..53f9ffcb750db 100644 --- a/x-pack/plugins/observability/server/routes/get_global_observability_server_route_repository.ts +++ b/x-pack/plugins/observability/server/routes/get_global_observability_server_route_repository.ts @@ -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; } diff --git a/x-pack/plugins/observability/server/routes/register_routes.ts b/x-pack/plugins/observability/server/routes/register_routes.ts index 0d982c6bf48ca..7726e54793d32 100644 --- a/x-pack/plugins/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability/server/routes/register_routes.ts @@ -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) { diff --git a/x-pack/plugins/observability/server/services/openai/azure_openai_client.ts b/x-pack/plugins/observability/server/services/openai/azure_openai_client.ts deleted file mode 100644 index 34e15f5297403..0000000000000 --- a/x-pack/plugins/observability/server/services/openai/azure_openai_client.ts +++ /dev/null @@ -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; - } = { - 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); - }, - }; -} diff --git a/x-pack/plugins/observability/server/services/openai/config.ts b/x-pack/plugins/observability/server/services/openai/config.ts deleted file mode 100644 index a54f61cb9f46f..0000000000000 --- a/x-pack/plugins/observability/server/services/openai/config.ts +++ /dev/null @@ -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['openAI']; -export type AzureOpenAIConfig = TypeOf['azureOpenAI']; -export type ObservabilityCoPilotConfig = TypeOf; diff --git a/x-pack/plugins/observability/server/services/openai/index.ts b/x-pack/plugins/observability/server/services/openai/index.ts deleted file mode 100644 index e70d683652c63..0000000000000 --- a/x-pack/plugins/observability/server/services/openai/index.ts +++ /dev/null @@ -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); - } - } -} diff --git a/x-pack/plugins/observability/server/services/openai/openai_client.ts b/x-pack/plugins/observability/server/services/openai/openai_client.ts deleted file mode 100644 index 4dbaa886041b3..0000000000000 --- a/x-pack/plugins/observability/server/services/openai/openai_client.ts +++ /dev/null @@ -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; - } = { - create: async (messages) => { - const response = await this.client.createChatCompletion( - { - messages, - model: this.config.model, - stream: true, - }, - { responseType: 'stream' } - ); - - return pipeStreamingResponse(response); - }, - }; -} diff --git a/x-pack/plugins/observability/server/services/openai/types.ts b/x-pack/plugins/observability/server/services/openai/types.ts deleted file mode 100644 index 504819eea4678..0000000000000 --- a/x-pack/plugins/observability/server/services/openai/types.ts +++ /dev/null @@ -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; - }; -} diff --git a/x-pack/plugins/observability/server/services/openai/pipe_streaming_response.ts b/x-pack/plugins/observability_ai_assistant/common/index.ts similarity index 57% rename from x-pack/plugins/observability/server/services/openai/pipe_streaming_response.ts rename to x-pack/plugins/observability_ai_assistant/common/index.ts index 737fa486f21f1..92cd91871da69 100644 --- a/x-pack/plugins/observability/server/services/openai/pipe_streaming_response.ts +++ b/x-pack/plugins/observability_ai_assistant/common/index.ts @@ -5,8 +5,5 @@ * 2.0. */ -export function pipeStreamingResponse(response: { data: any; headers: Record }) { - response.headers['Content-Type'] = 'dont-compress-this'; - - return response.data; -} +export type { Message, Conversation } from './types'; +export { MessageRole } from './types'; diff --git a/x-pack/plugins/observability_ai_assistant/jest.config.js b/x-pack/plugins/observability_ai_assistant/jest.config.js index e4a140341d07f..5eaabe2dcf492 100644 --- a/x-pack/plugins/observability_ai_assistant/jest.config.js +++ b/x-pack/plugins/observability_ai_assistant/jest.config.js @@ -5,10 +5,9 @@ * 2.0. */ -const path = require('path'); - module.exports = { preset: '@kbn/test', - rootDir: path.resolve(__dirname, '../../..'), + rootDir: '../../..', roots: ['/x-pack/plugins/observability_ai_assistant'], + setupFiles: ['/x-pack/plugins/observability_ai_assistant/.storybook/jest_setup.js'], }; diff --git a/x-pack/plugins/observability_ai_assistant/kibana.jsonc b/x-pack/plugins/observability_ai_assistant/kibana.jsonc index c3a1dcd41a270..d25b17df8c90b 100644 --- a/x-pack/plugins/observability_ai_assistant/kibana.jsonc +++ b/x-pack/plugins/observability_ai_assistant/kibana.jsonc @@ -12,7 +12,11 @@ ], "requiredPlugins": [ "triggersActionsUi", - "actions" + "actions", + "security" + ], + "requiredBundles": [ + "kibanaReact" ], "optionalPlugins": [], "extraPublicDirs": [] diff --git a/x-pack/plugins/observability_ai_assistant/public/api/index.ts b/x-pack/plugins/observability_ai_assistant/public/api/index.ts new file mode 100644 index 0000000000000..ad80d738be335 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/api/index.ts @@ -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 & { + 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 +>; + +export type ObservabilityAIAssistantAPIEndpoint = + keyof ObservabilityAIAssistantServerRouteRepository; + +export type APIReturnType = ReturnOf< + ObservabilityAIAssistantServerRouteRepository, + TEndpoint +>; + +export type ObservabilityAIAssistantAPIClientRequestParamsOf< + TEndpoint extends ObservabilityAIAssistantAPIEndpoint +> = ClientRequestParamsOf; + +export function createCallObservabilityAIAssistantAPI(core: CoreStart | CoreSetup) { + return ((endpoint, options) => { + const { params } = options as unknown as { + params?: Partial>; + }; + + 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; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/connector_selector/connector_selector_base.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/connector_selector/connector_selector_base.stories.tsx new file mode 100644 index 0000000000000..904c2ed8a4fcd --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/connector_selector/connector_selector_base.stories.tsx @@ -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 = { + component: Component, + title: 'app/Molecules/ConnectorSelectorBase', +}; + +export default meta; + +export const Loaded: ComponentStoryObj = { + 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 = { + args: { + loading: true, + }, +}; + +export const Empty: ComponentStoryObj = { + args: { + loading: false, + connectors: [], + }, +}; + +export const FailedToLoad: ComponentStoryObj = { + args: { + loading: false, + error: new Error('Failed to load connectors'), + }, +}; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/connector_selector/connector_selector_base.tsx b/x-pack/plugins/observability_ai_assistant/public/components/connector_selector/connector_selector_base.tsx new file mode 100644 index 0000000000000..0667cb66c8e3c --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/connector_selector/connector_selector_base.tsx @@ -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 ( + + + + + + ); + } + + if (props.error) { + return ( + + + + + + + {i18n.translate('xpack.observabilityAiAssistant.connectorSelector.error', { + defaultMessage: 'Failed to load connectors', + })} + + + + ); + } + + if (!props.connectors?.length) { + return ( + + + + {i18n.translate('xpack.observabilityAiAssistant.connectorSelector.empty', { + defaultMessage: 'No connectors', + })} + + + + ); + } + + return ( + + + ({ + value: connector.id, + inputDisplay: ( + + + + {i18n.translate( + 'xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel', + { + defaultMessage: 'Connector:', + } + )} + + + + + {connector.name} + + + + ), + dropdownDisplay: ( + + {connector.name} + + ), + }))} + onChange={(id) => { + props.selectConnector(id); + }} + /> + + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx deleted file mode 100644 index 8ca01d2f2588c..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx +++ /dev/null @@ -1,42 +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 from 'react'; -import { ComponentStory } from '@storybook/react'; - -import { Insight as Component, InsightProps } from './insight'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator'; - -export default { - component: Component, - title: 'app/Molecules/Insight', - argTypes: { - debug: { - control: { - type: 'boolean', - }, - }, - }, - decorators: [KibanaReactStorybookDecorator], -}; - -const Template: ComponentStory = (props: InsightProps) => ( - -); - -const defaultProps = { - title: 'Elastic Assistant', - actions: [ - { id: 'foo', label: 'Put hands in pockets', handler: () => {} }, - { id: 'bar', label: 'Drop kick', handler: () => {} }, - ], - description: 'What is the root cause of performance degradation in my service?', - debug: true, -}; - -export const Insight = Template.bind({}); -Insight.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx index 56c0126907f68..f474c20fd20c4 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx @@ -4,168 +4,62 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiAccordion, - EuiButtonIcon, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFilterButton, - EuiFilterGroup, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiPopover, - EuiSpacer, - EuiText, - useEuiTheme, -} from '@elastic/eui'; -import moment from 'moment'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { AssistantAvatar } from '../assistant_avatar'; +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'; -import { InsightError } from './insight_error'; -import { InsightGeneratedResponse } from './insight_generated_response'; -import { Feedback } from '../feedback_buttons'; - -export interface InsightProps { - title: string; - description?: string; - date?: Date; - debug?: boolean; - actions: Array<{ id: string; label: string; icon?: string; handler: () => void }>; -} - -export function Insight({ - title, - description, - date = new Date(), - debug, - actions = [], -}: InsightProps) { - const { euiTheme } = useEuiTheme(); - const { uiSettings } = useKibana().services; - - const dateFormat = uiSettings?.get('dateFormat'); - - const [isActionsPopoverOpen, setIsActionsPopover] = useState(false); - - const [state, setState] = useState<'missing' | 'error' | 'insightGenerated'>('insightGenerated'); - const handleClickActions = () => { - setIsActionsPopover(!isActionsPopoverOpen); - }; - - const handleFeedback = (feedback: Feedback) => {}; - - const handleRegenerate = () => {}; - - const handleStartChat = () => {}; +function ChatContent({ messages, connectorId }: { messages: Message[]; connectorId: string }) { + const chat = useChat({ messages, connectorId }); return ( - - - - - - - - -
{title}
-
- - - {description} - + } + error={chat.error} + controls={null} + /> + ); +} - +export function Insight({ messages, title }: { messages: Message[]; title: string }) { + const [hasOpened, setHasOpened] = useState(false); - - - {i18n.translate('xpack.observabilityAiAssistant.insight.generatedAt', { - defaultMessage: 'Generated at', - })}{' '} - {moment(date).format(dateFormat)} - - -
-
- } - extraAction={ - - } - panelPaddingSize="s" - closePopover={handleClickActions} - isOpen={isActionsPopoverOpen} - > - ( - - {label} - - ))} - /> - - } - > - + const connectors = useGenAIConnectors(); - {/* Debug controls. */} - {debug ? ( - - setState('insightGenerated')} - > - Normal - - setState('missing')} - > - Missing credentials - - setState('error')}> - Error - - - ) : null} + const { + services: { http }, + } = useKibana(); - {state === 'insightGenerated' ? ( - - ) : null} + let children: React.ReactNode = null; - {state === 'error' ? : null} + if (hasOpened && connectors.selectedConnector) { + children = ; + } else if (!connectors.loading && !connectors.connectors?.length) { + children = ( + + ); + } - {state === 'missing' ? : null} - - + return ( + { + setHasOpened((prevHasOpened) => prevHasOpened || isOpen); + }} + controls={} + loading={connectors.loading} + > + {children} + ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_base.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_base.stories.tsx new file mode 100644 index 0000000000000..45b184832e619 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_base.stories.tsx @@ -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 = (props: InsightBaseProps) => ( + +); + +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: ( + {}} + /> + ), + onToggle: () => {}, + children: ( + + } + controls={ + + + {}} /> + + + + + + + + + + + + + } + /> + ), +}; + +export const Insight = Template.bind({}); +Insight.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_base.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_base.tsx new file mode 100644 index 0000000000000..47f0abb3e1ec1 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_base.tsx @@ -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 ( + + + + + + + + +
{title}
+
+ + + {description} + +
+ + } + isLoading={loading} + isDisabled={loading} + extraAction={ + actions?.length || controls ? ( + + {controls && {controls}} + {actions?.length ? ( + + + } + panelPaddingSize="s" + closePopover={handleClickActions} + isOpen={isActionsPopoverOpen} + > + ( + + {label} + + ))} + /> + + + ) : null} + + ) : null + } + onToggle={onToggle} + > + + {children} +
+
+ ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx deleted file mode 100644 index b396899849240..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx +++ /dev/null @@ -1,79 +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 from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiPanel, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Feedback, FeedbackButtons } from '../feedback_buttons'; -import { useStreamingText } from './use_streaming_words'; -interface InsightGeneratedResponseProps { - answer: string; - onClickFeedback: (feedback: Feedback) => void; - onClickRegenerate: () => void; - onClickStartChat: () => void; -} - -export function InsightGeneratedResponse({ - onClickFeedback, - onClickRegenerate, - onClickStartChat, -}: InsightGeneratedResponseProps) { - const answer = useStreamingText({ - message: `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.`, - }); - return ( - - -

{answer}

-
- - - - - - - - - - - - - {i18n.translate('xpack.observabilityAiAssistant.insight.response.regenerate', { - defaultMessage: 'Regenerate', - })} - - - - - - {i18n.translate('xpack.observabilityAiAssistant.insight.response.startChat', { - defaultMessage: 'Start chat', - })} - - - - - -
- ); -} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx index e074abe7830c5..aab2dd87a3d08 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx @@ -9,7 +9,11 @@ import React from 'react'; import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -export function InsightMissingCredentials() { +interface Props { + connectorsManagementHref: string; +} + +export function InsightMissingCredentials(props: Props) { return ( - + {i18n.translate('xpack.observabilityAiAssistant.insight.missing.buttonLabel', { defaultMessage: 'Connect Assistant', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts b/x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts deleted file mode 100644 index 4bd4cedd1bff0..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts +++ /dev/null @@ -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. - */ - -import { useEffect, useState } from 'react'; - -interface UseStreamingTextProps { - message: string; -} - -export function useStreamingText({ message }: UseStreamingTextProps) { - const [chatMessages, setChatMessages] = useState(''); - - useEffect(() => { - const words = message.split(' '); - - for (let i = 0; i < words.length; i++) { - setTimeout(() => { - setChatMessages((prevState) => `${prevState} ${words[i]}`); - }, i * 50); // Adjust typing speed here (milliseconds per word) - } - }, [message]); - - return chatMessages; -} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.stories.tsx new file mode 100644 index 0000000000000..d58907b5766e5 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.stories.tsx @@ -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 = { + component: Component, + title: 'app/Molecules/MessagePanel', +}; + +export default meta; + +export const ContentLoading: ComponentStoryObj = { + args: { + body: ( + + ), + }, +}; + +export const ContentLoaded: ComponentStoryObj = { + args: { + body: , + }, +}; + +export const ContentFailed: ComponentStoryObj = { + args: { + body: , + error: new Error(), + }, +}; + +export const Controls: ComponentStoryObj = { + args: { + body: , + error: new Error(), + controls: {}} />, + }, +}; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.tsx b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.tsx new file mode 100644 index 0000000000000..be39fa01f11df --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.tsx @@ -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 ( + + {props.body} + {props.error ? ( + <> + {props.body ? : null} + + + + + + + {i18n.translate('xpack.observabilityAiAssistant.messagePanel.failedLoadingText', { + defaultMessage: 'Failed to load response', + })} + + + + + ) : null} + {props.controls ? ( + <> + + + + {props.controls} + + ) : null} + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx new file mode 100644 index 0000000000000..897488d73f507 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx @@ -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 = ; + +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 ( + + cursor, + } as Record + } + > + {`${props.content}${props.loading ? CURSOR : ''}`} + + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/regenerate_response_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/regenerate_response_button.tsx new file mode 100644 index 0000000000000..922f3c34a302b --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/regenerate_response_button.tsx @@ -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) { + return ( + + {i18n.translate('xpack.observabilityAiAssistant.regenerateResponseButtonLabel', { + defaultMessage: 'Regenerate', + })} + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/start_chat_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/start_chat_button.tsx new file mode 100644 index 0000000000000..b40586c5cfe35 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/start_chat_button.tsx @@ -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) { + return ( + + {i18n.translate('xpack.observabilityAiAssistant.insight.response.startChat', { + defaultMessage: 'Start chat', + })} + + ); +} diff --git a/x-pack/plugins/observability/public/context/co_pilot_context/index.tsx b/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_provider.tsx similarity index 51% rename from x-pack/plugins/observability/public/context/co_pilot_context/index.tsx rename to x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_provider.tsx index 0f6accee310ae..1c7927db4896c 100644 --- a/x-pack/plugins/observability/public/context/co_pilot_context/index.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_provider.tsx @@ -6,8 +6,10 @@ */ import { createContext } from 'react'; -import { type CoPilotService } from '../../typings/co_pilot'; +import type { ObservabilityAIAssistantService } from '../types'; -export const CoPilotContext = createContext(undefined); +export const ObservabilityAIAssistantContext = createContext< + ObservabilityAIAssistantService | undefined +>(undefined); -export const CoPilotContextProvider = CoPilotContext.Provider; +export const ObservabilityAIAssistantProvider = ObservabilityAIAssistantContext.Provider; diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts new file mode 100644 index 0000000000000..36a9d7482ffe7 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts @@ -0,0 +1,109 @@ +/* + * 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 { delay } 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(undefined); + + const [error, setError] = useState(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((resolve, reject) => { + const subscription = response$.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, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_genai_connectors.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_genai_connectors.ts new file mode 100644 index 0000000000000..e8373e0124909 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_genai_connectors.ts @@ -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(undefined); + + const [selectedConnector, setSelectedConnector] = useLocalStorage( + `xpack.observabilityAiAssistant.lastUsedConnector`, + '' + ); + + const [loading, setLoading] = useState(false); + + const [error, setError] = useState(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); + }, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant.ts new file mode 100644 index 0000000000000..8d8938fc49521 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant.ts @@ -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 ``?' + ); + } + + return services; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/index.ts b/x-pack/plugins/observability_ai_assistant/public/index.ts index 94587cdbb7ddd..e22b7415a43ac 100644 --- a/x-pack/plugins/observability_ai_assistant/public/index.ts +++ b/x-pack/plugins/observability_ai_assistant/public/index.ts @@ -5,6 +5,8 @@ * 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, @@ -14,6 +16,19 @@ import type { 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 const plugin: PluginInitializer< ObservabilityAIAssistantPluginSetup, ObservabilityAIAssistantPluginStart, diff --git a/x-pack/plugins/observability_ai_assistant/public/plugin.ts b/x-pack/plugins/observability_ai_assistant/public/plugin.ts index 6cafbfc55555b..607f54a3c6da7 100644 --- a/x-pack/plugins/observability_ai_assistant/public/plugin.ts +++ b/x-pack/plugins/observability_ai_assistant/public/plugin.ts @@ -5,8 +5,9 @@ * 2.0. */ -import type { Plugin, PluginInitializerContext } from '@kbn/core/public'; +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, @@ -28,11 +29,11 @@ export class ObservabilityAIAssistantPlugin constructor(context: PluginInitializerContext) { this.logger = context.logger.get(); } - setup() { + setup(): ObservabilityAIAssistantPluginSetup { return {}; } - start() { - return {}; + start(coreStart: CoreStart): ObservabilityAIAssistantPluginStart { + return createService(coreStart); } } diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.test.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_service.test.ts new file mode 100644 index 0000000000000..010503e7abf4f --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_service.test.ts @@ -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); + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts new file mode 100644 index 0000000000000..a27d390706c11 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts @@ -0,0 +1,63 @@ +/* + * 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) + ); + }, + callApi: client, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 68f18af530f8f..fc553cb201010 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -8,10 +8,36 @@ 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 interface ObservabilityAIAssistantPluginStart {} +export type CreateChatCompletionResponseChunk = Omit & { + choices: Array< + Omit & { + delta: { content?: string; function_call?: { name?: string; args?: string } }; + } + >; +}; + +export interface ObservabilityAIAssistantService { + isEnabled: () => boolean; + chat: (options: { + messages: Message[]; + connectorId: string; + signal: AbortSignal; + }) => Promise>; + callApi: ObservabilityAIAssistantAPIClient; +} + +export interface ObservabilityAIAssistantPluginStart extends ObservabilityAIAssistantService {} + export interface ObservabilityAIAssistantPluginSetup {} export interface ObservabilityAIAssistantPluginSetupDependencies { triggersActions: TriggersAndActionsUIPublicPluginSetup; diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/readable_stream_reader_into_observable.ts b/x-pack/plugins/observability_ai_assistant/public/utils/readable_stream_reader_into_observable.ts new file mode 100644 index 0000000000000..f65e0fbd7ee7f --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/utils/readable_stream_reader_into_observable.ts @@ -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 +): Observable { + return new Observable((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()); +} diff --git a/x-pack/plugins/observability_ai_assistant/server/index.ts b/x-pack/plugins/observability_ai_assistant/server/index.ts index b44fd9edace38..f471407678309 100644 --- a/x-pack/plugins/observability_ai_assistant/server/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/index.ts @@ -9,5 +9,7 @@ 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) => new ObservabilityAIAssistantPlugin(ctx); diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts index fc806cc332ebf..5e0744a7f7238 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import type { IncomingMessage } from 'http'; import { notImplemented } from '@hapi/boom'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; -import { conversationRt } from '../runtime_types'; +import { messageRt } from '../runtime_types'; const chatRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/chat', @@ -17,7 +17,7 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ }, params: t.type({ body: t.type({ - conversation: conversationRt, + messages: t.array(messageRt), connectorId: t.string, }), }), @@ -31,7 +31,7 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ } return client.chat({ - messages: params.body.conversation.messages, + messages: params.body.messages, connectorId: params.body.connectorId, }); }, diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/connectors/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/connectors/route.ts new file mode 100644 index 0000000000000..894896fec6b3c --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/routes/connectors/route.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FindActionResult } from '@kbn/actions-plugin/server'; +import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; + +const listConnectorsRoute = createObservabilityAIAssistantServerRoute({ + endpoint: 'GET /internal/observability_ai_assistant/connectors', + options: { + tags: ['access:ai_assistant'], + }, + handler: async (resources): Promise => { + const { request, plugins } = resources; + + const actionsClient = await ( + await plugins.actions.start() + ).getActionsClientWithRequest(request); + + const connectors = await actionsClient.getAll(); + + return connectors.filter((connector) => connector.actionTypeId === '.gen-ai'); + }, +}); + +export const connectorRoutes = { + ...listConnectorsRoute, +}; diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/get_global_observability_ai_assistant_route_repository.ts b/x-pack/plugins/observability_ai_assistant/server/routes/get_global_observability_ai_assistant_route_repository.ts index 5fa24a13555ca..fcca8df0f03e7 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/get_global_observability_ai_assistant_route_repository.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/get_global_observability_ai_assistant_route_repository.ts @@ -6,12 +6,14 @@ */ import { chatRoutes } from './chat/route'; +import { connectorRoutes } from './connectors/route'; import { conversationRoutes } from './conversations/route'; export function getGlobalObservabilityAIAssistantServerRouteRepository() { return { ...chatRoutes, ...conversationRoutes, + ...connectorRoutes, }; } diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts index 7dacfc704d108..914f24fd75432 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts @@ -13,7 +13,7 @@ import type { Logger } from '@kbn/logging'; import type { ActionsClient } from '@kbn/actions-plugin/server/actions_client'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { internal, notFound } from '@hapi/boom'; -import { compact, merge, omit } from 'lodash'; +import { compact, isEmpty, merge, omit } from 'lodash'; import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { type Conversation, @@ -125,7 +125,9 @@ export class ObservabilityAIAssistantClient implements IObservabilityAIAssistant return { role, content: message.message.content, - function_call: omit(message.message.function_call, 'trigger'), + function_call: isEmpty(message.message.function_call?.name) + ? undefined + : omit(message.message.function_call, 'trigger'), name: message.message.name, }; }) @@ -141,7 +143,10 @@ export class ObservabilityAIAssistantClient implements IObservabilityAIAssistant actionId: connectorId, params: { subAction: 'stream', - subActionParams: JSON.stringify(request), + subActionParams: { + body: JSON.stringify(request), + stream: true, + }, }, }); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/index.ts index eeac8253a0626..709a81627e480 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/index.ts @@ -12,6 +12,7 @@ import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import * as Boom from '@hapi/boom'; import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server/plugin'; import { getSpaceIdFromPath } from '@kbn/spaces-plugin/common'; +import fnv from 'fnv-plus'; import type { ObservabilityAIAssistantResourceNames } from './types'; import { conversationComponentTemplate } from './conversation_component_template'; import type { IObservabilityAIAssistantClient, IObservabilityAIAssistantService } from './types'; @@ -56,9 +57,7 @@ export class ObservabilityAIAssistantService implements IObservabilityAIAssistan const esClient = coreStart.elasticsearch.client.asInternalUser; - const fnv1a = await import('@sindresorhus/fnv1a'); - - const versionHash = fnv1a.default(stringify(conversationComponentTemplate), { size: 64 }); + const versionHash = fnv.fast1a64(stringify(conversationComponentTemplate)); await esClient.cluster.putComponentTemplate({ create: false, @@ -149,8 +148,9 @@ export class ObservabilityAIAssistantService implements IObservabilityAIAssistan }); } } catch (error) { - this.logger.error(`Failed to initialize CoPilotService: ${error.message}`); + this.logger.error(`Failed to initialize service: ${error.message}`); this.logger.debug(error); + throw error; } }); @@ -159,14 +159,14 @@ export class ObservabilityAIAssistantService implements IObservabilityAIAssistan }: { request: KibanaRequest; }): Promise { - const [_, [coreStart, { security, actions }]] = await Promise.all([ + const [_, [coreStart, plugins]] = await Promise.all([ this.init(), this.core.getStartServices() as Promise< [CoreStart, { security: SecurityPluginStart; actions: ActionsPluginStart }, unknown] >, ]); - const user = security.authc.getCurrentUser(request); + const user = plugins.security.authc.getCurrentUser(request); if (!user) { throw Boom.forbidden(`User not found for current request`); @@ -177,7 +177,7 @@ export class ObservabilityAIAssistantService implements IObservabilityAIAssistan const { spaceId } = getSpaceIdFromPath(basePath, coreStart.http.basePath.serverBasePath); return new ObservabilityAIAssistantClient({ - actionsClient: await actions.getActionsClientWithRequest(request), + actionsClient: await plugins.actions.getActionsClientWithRequest(request), namespace: spaceId, esClient: coreStart.elasticsearch.client.asInternalUser, resources: this.resourceNames, diff --git a/x-pack/plugins/observability_ai_assistant/server/types.ts b/x-pack/plugins/observability_ai_assistant/server/types.ts index c1753b1f9497f..faa5c346541c0 100644 --- a/x-pack/plugins/observability_ai_assistant/server/types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/types.ts @@ -5,14 +5,17 @@ * 2.0. */ -import { PluginSetupContract, PluginStartContract } from '@kbn/actions-plugin/server'; +import type { PluginSetupContract, PluginStartContract } from '@kbn/actions-plugin/server'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; /* eslint-disable @typescript-eslint/no-empty-interface*/ export interface ObservabilityAIAssistantPluginStart {} export interface ObservabilityAIAssistantPluginSetup {} export interface ObservabilityAIAssistantPluginSetupDependencies { actions: PluginSetupContract; + security: SecurityPluginSetup; } export interface ObservabilityAIAssistantPluginStartDependencies { actions: PluginStartContract; + security: SecurityPluginStart; } diff --git a/x-pack/plugins/profiling/kibana.jsonc b/x-pack/plugins/profiling/kibana.jsonc index 4d81adbea7b4c..2985456d4ec8b 100644 --- a/x-pack/plugins/profiling/kibana.jsonc +++ b/x-pack/plugins/profiling/kibana.jsonc @@ -21,12 +21,14 @@ "licensing", "observability", "observabilityShared", + "observabilityAIAssistant", "unifiedSearch", "share" ], "requiredBundles": [ "kibanaReact", "kibanaUtils", + "observabilityAIAssistant", ] } } diff --git a/x-pack/plugins/profiling/public/app.tsx b/x-pack/plugins/profiling/public/app.tsx index ad1023aeb70db..01bd240dd4028 100644 --- a/x-pack/plugins/profiling/public/app.tsx +++ b/x-pack/plugins/profiling/public/app.tsx @@ -13,7 +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 { ObservabilityAIAssistantProvider } from '@kbn/observability-ai-assistant-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'; @@ -79,13 +79,11 @@ function App({ }; }, [coreStart, coreSetup, pluginsStart, pluginsSetup, profilingFetchServices]); - const coPilotService = pluginsSetup.observability.getCoPilotService(); - return ( - + @@ -111,7 +109,7 @@ function App({ - + diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx index bd059f74cbfea..0d7e35a93d8c0 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx +++ b/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx @@ -6,9 +6,13 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useCoPilot, CoPilotPrompt } from '@kbn/observability-plugin/public'; +import { + ContextualInsight, + Message, + MessageRole, + useObservabilityAIAssistant, +} from '@kbn/observability-ai-assistant-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'; @@ -34,15 +38,81 @@ export interface Props { } export function FrameInformationWindow({ frame, totalSamples, totalSeconds, samplingRate }: Props) { - const coPilotService = useCoPilot(); + const aiAssistant = useObservabilityAIAssistant(); - const promptParams = useMemo(() => { - return frame?.functionName && frame?.exeFileName - ? { - functionName: frame?.functionName, - library: frame?.exeFileName, - } - : undefined; + const promptMessages = useMemo(() => { + if (frame?.functionName && frame.exeFileName) { + const functionName = frame.functionName; + const library = frame.exeFileName; + + const now = new Date().toISOString(); + + return [ + { + '@timestamp': now, + message: { + role: MessageRole.System, + content: `You are perf-gpt, a helpful assistant for performance analysis and optimisation + of software. Answer as concisely as possible.`, + }, + }, + { + '@timestamp': now, + message: { + role: MessageRole.User, + 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`, + }, + }, + ]; + } + + return undefined; }, [frame?.functionName, frame?.exeFileName]); if (!frame) { @@ -103,17 +173,14 @@ export function FrameInformationWindow({ frame, totalSamples, totalSeconds, samp - {coPilotService?.isEnabled() && promptParams ? ( + {aiAssistant.isEnabled() && promptMessages ? ( <> - diff --git a/x-pack/plugins/profiling/public/types.ts b/x-pack/plugins/profiling/public/types.ts index 1d39c5d2088bb..1d914c2d31a72 100644 --- a/x-pack/plugins/profiling/public/types.ts +++ b/x-pack/plugins/profiling/public/types.ts @@ -20,10 +20,15 @@ import { import { ChartsPluginSetup, ChartsPluginStart } from '@kbn/charts-plugin/public'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; +import { + ObservabilityAIAssistantPluginSetup, + ObservabilityAIAssistantPluginStart, +} from '@kbn/observability-ai-assistant-plugin/public'; export interface ProfilingPluginPublicSetupDeps { observability: ObservabilityPublicSetup; observabilityShared: ObservabilitySharedPluginSetup; + observabilityAIAssistant: ObservabilityAIAssistantPluginSetup; dataViews: DataViewsPublicPluginSetup; data: DataPublicPluginSetup; charts: ChartsPluginSetup; @@ -34,6 +39,7 @@ export interface ProfilingPluginPublicSetupDeps { export interface ProfilingPluginPublicStartDeps { observability: ObservabilityPublicStart; observabilityShared: ObservabilitySharedPluginStart; + observabilityAIAssistant: ObservabilityAIAssistantPluginStart; dataViews: DataViewsPublicPluginStart; data: DataPublicPluginStart; charts: ChartsPluginStart; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/gen_ai/gen_ai.ts b/x-pack/plugins/stack_connectors/server/connector_types/gen_ai/gen_ai.ts index b4bec8d5022c2..29214d18709bd 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/gen_ai/gen_ai.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/gen_ai/gen_ai.ts @@ -75,14 +75,16 @@ export class GenAiConnector extends SubActionConnector): string { if (!error.response?.status) { return 'Unknown API Error'; } if (error.response.status === 401) { return 'Unauthorized API Error'; } - return `API Error: ${error.response?.status} - ${error.response?.statusText}`; + return `API Error: ${error.response?.status} - ${error.response?.statusText}${ + error.response?.data?.error?.message ? ` - ${error.response.data.error?.message}` : '' + }`; } public async runApi({ body }: GenAiRunActionParams): Promise { diff --git a/yarn.lock b/yarn.lock index 0b25249c8f4cd..5625c7ad381af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6847,11 +6847,6 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== -"@sindresorhus/fnv1a@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/fnv1a/-/fnv1a-3.0.0.tgz#e8ce2e7c7738ec8c354867d38e3bfcde622b87ca" - integrity sha512-M6pmbdZqAryzjZ4ELAzrdCMoMZk5lH/fshKrapfSeXdf2W+GDqZvPmfXaNTZp43//FVbSwkTPwpEMnehSyskkQ== - "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"