[AI Assistant] Add setting for preferred type (#179233)

Adds a setting in the `aiAssistantManagementSelection` plugin that
allows users to set the preferred Assistant type. The allowed values
are:

- `default`: the status quo, which is: show the Observability AI
Assistant in Observability apps, the Security AI Assistant in Security
solution apps, and none in the other apps.
- `never`: Never show any AI Assistant.
- `observability`: Show the Observability AI Assistant everywhere,
except for Security solution apps.
- `security`: ~Show the Security AI Assistant everywhere, except for
Observability apps.~

it is up to the solutions to respect the setting. See
[x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx](https://github.com/elastic/kibana/pull/179233/files#diff-753a9aae298da45c5bd96bc2cacaa437ac53e165069ad6323973dd3c2879d4ae)
for an example. It should also be set in the specific Serverless config:
[config/serverless.oblt.yml](https://github.com/elastic/kibana/pull/179233/files#diff-358680d2b72268283dca4f3efb976cd924b7b5bce822e07c8871d061b4bb339e).

Update: this is now labeled as an Observability-only setting.

![CleanShot 2024-04-05 at 09 28
32@2x](fa6edeea-eaca-4ec3-86ec-f00effa95f93)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dario Gieselaar 2024-04-08 20:26:23 +02:00 committed by GitHub
parent 826f7cb42b
commit 52c65b78fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 308 additions and 41 deletions

View file

@ -81,6 +81,9 @@ xpack.apm.featureFlags.migrationToFleetAvailable: false
xpack.apm.featureFlags.sourcemapApiAvailable: false
xpack.apm.featureFlags.storageExplorerAvailable: false
## Set the AI Assistant type
aiAssistantManagementSelection.preferredAIAssistantType: "observability"
# Specify in telemetry the project type
telemetry.labels.serverless: observability

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export enum AIAssistantType {
Observability = 'observability',
Default = 'default',
Never = 'never',
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY = 'aiAssistant:preferredAIAssistantType';

View file

@ -4,10 +4,13 @@
"owner": "@elastic/obs-knowledge-team",
"plugin": {
"id": "aiAssistantManagementSelection",
"server": false,
"server": true,
"browser": true,
"requiredPlugins": ["management"],
"optionalPlugins": ["home", "serverless"],
"requiredBundles": ["kibanaReact"]
}
"requiredBundles": ["kibanaReact"],
"configPath": [
"aiAssistantManagementSelection"
],
},
}

View file

@ -6,13 +6,22 @@
* Side Public License, v 1.
*/
import { AiAssistantManagementPlugin } from './plugin';
import type { PluginInitializer } from '@kbn/core/public';
import { AIAssistantManagementPlugin } from './plugin';
export type {
AiAssistantManagementSelectionPluginSetup,
AiAssistantManagementSelectionPluginStart,
import type {
AIAssistantManagementSelectionPluginPublicSetup,
AIAssistantManagementSelectionPluginPublicStart,
} from './plugin';
export function plugin() {
return new AiAssistantManagementPlugin();
}
export { AIAssistantType } from '../common/ai_assistant_type';
export type {
AIAssistantManagementSelectionPluginPublicSetup,
AIAssistantManagementSelectionPluginPublicStart,
};
export const plugin: PluginInitializer<
AIAssistantManagementSelectionPluginPublicSetup,
AIAssistantManagementSelectionPluginPublicStart
> = () => new AIAssistantManagementPlugin();

View file

@ -11,16 +11,16 @@ import ReactDOM from 'react-dom';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import { I18nProvider } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { CoreSetup } from '@kbn/core/public';
import type { CoreSetup } from '@kbn/core/public';
import { wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
import { StartDependencies, AiAssistantManagementSelectionPluginStart } from '../plugin';
import type { ManagementAppMountParams } from '@kbn/management-plugin/public';
import type { StartDependencies, AIAssistantManagementSelectionPluginPublicStart } from '../plugin';
import { aIAssistantManagementSelectionRouter } from '../routes/config';
import { RedirectToHomeIfUnauthorized } from '../routes/components/redirect_to_home_if_unauthorized';
import { AppContextProvider } from '../app_context';
interface MountParams {
core: CoreSetup<StartDependencies, AiAssistantManagementSelectionPluginStart>;
core: CoreSetup<StartDependencies, AIAssistantManagementSelectionPluginPublicStart>;
mountParams: ManagementAppMountParams;
}

View file

@ -7,16 +7,20 @@
*/
import { i18n } from '@kbn/i18n';
import { CoreSetup, Plugin } from '@kbn/core/public';
import { ManagementSetup } from '@kbn/management-plugin/public';
import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { ServerlessPluginSetup } from '@kbn/serverless/public';
import { type CoreSetup, Plugin, type CoreStart } from '@kbn/core/public';
import type { ManagementSetup } from '@kbn/management-plugin/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import type { ServerlessPluginSetup } from '@kbn/serverless/public';
import { BehaviorSubject, Observable } from 'rxjs';
import { AIAssistantType } from '../common/ai_assistant_type';
import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AiAssistantManagementSelectionPluginSetup {}
export interface AIAssistantManagementSelectionPluginPublicSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AiAssistantManagementSelectionPluginStart {}
export interface AIAssistantManagementSelectionPluginPublicStart {
aiAssistantType$: Observable<AIAssistantType>;
}
export interface SetupDependencies {
management: ManagementSetup;
@ -27,20 +31,24 @@ export interface SetupDependencies {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface StartDependencies {}
export class AiAssistantManagementPlugin
export class AIAssistantManagementPlugin
implements
Plugin<
AiAssistantManagementSelectionPluginSetup,
AiAssistantManagementSelectionPluginStart,
AIAssistantManagementSelectionPluginPublicSetup,
AIAssistantManagementSelectionPluginPublicStart,
SetupDependencies,
StartDependencies
>
{
constructor() {}
public setup(
core: CoreSetup<StartDependencies, AiAssistantManagementSelectionPluginStart>,
core: CoreSetup<StartDependencies, AIAssistantManagementSelectionPluginPublicStart>,
{ home, management, serverless }: SetupDependencies
): AiAssistantManagementSelectionPluginSetup {
if (serverless) return {};
): AIAssistantManagementSelectionPluginPublicSetup {
if (serverless) {
return {};
}
if (home) {
home.featureCatalogue.register({
@ -77,7 +85,15 @@ export class AiAssistantManagementPlugin
return {};
}
public start() {
return {};
public start(coreStart: CoreStart) {
const preferredAIAssistantType: AIAssistantType = coreStart.uiSettings.get(
PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY
);
const aiAssistantType$ = new BehaviorSubject(preferredAIAssistantType);
return {
aiAssistantType$: aiAssistantType$.asObservable(),
};
}
}

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { AIAssistantType } from '../common/ai_assistant_type';
export interface ConfigSchema {
preferredAIAssistantType: AIAssistantType;
}

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginConfigDescriptor } from '@kbn/core-plugins-server';
import { AIAssistantType } from '../common/ai_assistant_type';
const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
preferredAIAssistantType: schema.oneOf(
[
schema.literal(AIAssistantType.Default),
schema.literal(AIAssistantType.Never),
schema.literal(AIAssistantType.Observability),
],
{ defaultValue: AIAssistantType.Default }
),
});
export type AIAssistantManagementSelectionConfig = TypeOf<typeof configSchema>;
export const config: PluginConfigDescriptor<AIAssistantManagementSelectionConfig> = {
schema: configSchema,
exposeToBrowser: {
preferredAIAssistantType: true,
},
};

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginInitializerContext } from '@kbn/core/server';
export { config } from './config';
export const plugin = async (initContext: PluginInitializerContext) => {
const { AIAssistantManagementSelectionPlugin } = await import('./plugin');
return new AIAssistantManagementSelectionPlugin(initContext);
};

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import {
PluginInitializerContext,
CoreSetup,
CoreStart,
Plugin,
DEFAULT_APP_CATEGORIES,
} from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import type { AIAssistantManagementSelectionConfig } from './config';
import type {
AIAssistantManagementSelectionPluginServerSetup,
AIAssistantManagementSelectionPluginServerStart,
} from './types';
import { AIAssistantType } from '../common/ai_assistant_type';
import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys';
export class AIAssistantManagementSelectionPlugin
implements
Plugin<
AIAssistantManagementSelectionPluginServerSetup,
AIAssistantManagementSelectionPluginServerStart
>
{
private readonly config: AIAssistantManagementSelectionConfig;
constructor(initializerContext: PluginInitializerContext) {
this.config = initializerContext.config.get();
}
public setup(core: CoreSetup) {
core.uiSettings.register({
[PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: {
name: i18n.translate('aiAssistantManagementSelection.preferredAIAssistantTypeSettingName', {
defaultMessage: 'Observability AI Assistant scope',
}),
category: [DEFAULT_APP_CATEGORIES.observability.id],
value: this.config.preferredAIAssistantType,
description: i18n.translate(
'aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription',
{
defaultMessage:
'<em>[technical preview]</em> Whether to show the Observability AI Assistant menu item in Observability, everywhere, or nowhere.',
}
),
schema: schema.oneOf(
[
schema.literal(AIAssistantType.Default),
schema.literal(AIAssistantType.Observability),
schema.literal(AIAssistantType.Never),
],
{ defaultValue: this.config.preferredAIAssistantType }
),
options: [AIAssistantType.Default, AIAssistantType.Observability, AIAssistantType.Never],
type: 'select',
optionLabels: {
[AIAssistantType.Default]: i18n.translate(
'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault',
{ defaultMessage: 'Observability only (default)' }
),
[AIAssistantType.Observability]: i18n.translate(
'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueObservability',
{ defaultMessage: 'Everywhere' }
),
[AIAssistantType.Never]: i18n.translate(
'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever',
{ defaultMessage: 'Nowhere' }
),
},
requiresPageReload: true,
},
});
return {};
}
public start(core: CoreStart) {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AIAssistantManagementSelectionPluginServerDependenciesStart {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AIAssistantManagementSelectionPluginServerDependenciesSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AIAssistantManagementSelectionPluginServerStart {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AIAssistantManagementSelectionPluginServerSetup {}

View file

@ -13,7 +13,9 @@
"@kbn/i18n-react",
"@kbn/core-chrome-browser",
"@kbn/typed-react-router-config",
"@kbn/serverless"
"@kbn/serverless",
"@kbn/config-schema",
"@kbn/core-plugins-server"
],
"exclude": ["target/**/*"]
}

View file

@ -655,4 +655,8 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'aiAssistant:preferredAIAssistantType': {
type: 'keyword',
_meta: { description: 'Non-default value of setting.' },
},
};

View file

@ -172,4 +172,5 @@ export interface UsageStats {
'data_views:fields_excluded_data_tiers': string;
'observability:apmEnableTransactionProfiling': boolean;
'devTools:enablePersistentConsole': boolean;
'aiAssistant:preferredAIAssistantType': string;
}

View file

@ -10427,6 +10427,12 @@
"_meta": {
"description": "Non-default value of setting."
}
},
"aiAssistant:preferredAIAssistantType": {
"type": "keyword",
"_meta": {
"description": "Non-default value of setting."
}
}
}
},

View file

@ -340,6 +340,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.observability_onboarding.ui.enabled (boolean)',
'xpack.observabilityLogsExplorer.navigation.showAppLink (any)', // conditional, is actually a boolean
'share.new_version.enabled (boolean)',
'aiAssistantManagementSelection.preferredAIAssistantType (alternatives)',
/**
* Rule form V2 feature flags
*/

View file

@ -8,6 +8,7 @@
"browser": true,
"configPath": ["xpack", "observabilityAIAssistantApp"],
"requiredPlugins": [
"aiAssistantManagementSelection",
"observabilityAIAssistant",
"observabilityShared",
"actions",

View file

@ -6,7 +6,7 @@
*/
import React, { useEffect, useRef, useState } from 'react';
import { AssistantAvatar, useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public';
import { EuiButton } from '@elastic/eui';
import { EuiButton, EuiLoadingSpinner } from '@elastic/eui';
import { css } from '@emotion/react';
import { v4 } from 'uuid';
import useObservable from 'react-use/lib/useObservable';
@ -114,7 +114,7 @@ export function NavControl({}: {}) {
fullWidth={false}
minWidth={0}
>
<AssistantAvatar size="xs" />
{chatService.loading ? <EuiLoadingSpinner size="s" /> : <AssistantAvatar size="xs" />}
</EuiButton>
{chatService.value ? (
<ObservabilityAIAssistantChatServiceContext.Provider value={chatService.value}>

View file

@ -7,31 +7,54 @@
import { useEffect, useState } from 'react';
import { combineLatest } from 'rxjs';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import { DEFAULT_APP_CATEGORIES, type PublicAppInfo } from '@kbn/core/public';
import { AIAssistantType } from '@kbn/ai-assistant-management-plugin/public';
import { useKibana } from './use_kibana';
function getVisibility(
appId: string | undefined,
applications: ReadonlyMap<string, PublicAppInfo>,
preferredAssistantType: AIAssistantType
) {
if (preferredAssistantType === AIAssistantType.Never) {
return false;
}
const categoryId =
(appId && applications.get(appId)?.category?.id) || DEFAULT_APP_CATEGORIES.kibana.id;
if (preferredAssistantType === AIAssistantType.Observability) {
return categoryId !== DEFAULT_APP_CATEGORIES.security.id;
}
return categoryId === DEFAULT_APP_CATEGORIES.observability.id;
}
export function useIsNavControlVisible() {
const [isVisible, setIsVisible] = useState(false);
const {
services: {
application: { currentAppId$, applications$ },
plugins: {
start: { aiAssistantManagementSelection },
},
},
} = useKibana();
useEffect(() => {
const appSubscription = combineLatest([currentAppId$, applications$]).subscribe({
next: ([appId, applications]) => {
const isObservabilityApp =
appId &&
applications.get(appId)?.category?.id === DEFAULT_APP_CATEGORIES.observability.id;
setIsVisible(!!isObservabilityApp);
const appSubscription = combineLatest([
currentAppId$,
applications$,
aiAssistantManagementSelection.aiAssistantType$,
]).subscribe({
next: ([appId, applications, preferredAssistantType]) => {
setIsVisible(getVisibility(appId, applications, preferredAssistantType));
},
});
return appSubscription.unsubscribe;
}, [currentAppId$, applications$]);
}, [currentAppId$, applications$, aiAssistantManagementSelection.aiAssistantType$]);
return {
isVisible,

View file

@ -28,6 +28,10 @@ import type {
TriggersAndActionsUIPublicPluginStart,
} from '@kbn/triggers-actions-ui-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type {
AIAssistantManagementSelectionPluginPublicStart,
AIAssistantManagementSelectionPluginPublicSetup,
} from '@kbn/ai-assistant-management-plugin/public';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ObservabilityAIAssistantAppPublicStart {}
@ -46,6 +50,7 @@ export interface ObservabilityAIAssistantAppPluginStartDependencies {
ml: MlPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
data: DataPublicPluginStart;
aiAssistantManagementSelection: AIAssistantManagementSelectionPluginPublicStart;
}
export interface ObservabilityAIAssistantAppPluginSetupDependencies {
@ -59,4 +64,5 @@ export interface ObservabilityAIAssistantAppPluginSetupDependencies {
observabilityShared: ObservabilitySharedPluginSetup;
ml: MlPluginSetup;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
aiAssistantManagementSelection: AIAssistantManagementSelectionPluginPublicSetup;
}

View file

@ -50,6 +50,7 @@
"@kbn/shared-ux-link-redirect-app",
"@kbn/shared-ux-utility",
"@kbn/data-plugin",
"@kbn/ai-assistant-management-plugin",
"@kbn/deeplinks-observability"
],
"exclude": ["target/**/*"]