From 327d648bf38a32a65831acca4805d5856381debd Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:03:04 +0200 Subject: [PATCH] [AI Assistant] Add assistant to Serverless Search (#196832) ## Summary This adds the AI assistant to Serverless Elasticsearch. It also disables the knowledge base, and disables a few config values we don't want users to be able to set in that context. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elena Shostak <165678770+elena-shostak@users.noreply.github.com> (cherry picked from commit 3bc5e2db73799dc9c7831b6f9da4a52063cf112f) # Conflicts: # config/serverless.es.yml --- config/serverless.es.yml | 14 +- config/serverless.oblt.yml | 1 + .../test_suites/core_plugins/rendering.ts | 3 + .../src/chat/chat_actions_menu.tsx | 24 +-- .../src/chat/chat_body.stories.tsx | 1 + .../kbn-ai-assistant/src/chat/chat_body.tsx | 2 +- .../kbn-ai-assistant/src/chat/chat_flyout.tsx | 14 +- .../kbn-ai-assistant/src/chat/chat_header.tsx | 49 +++--- .../src/chat/chat_timeline.stories.tsx | 1 + .../chat/knowledge_base_callout.stories.tsx | 12 +- .../src/chat/welcome_message.tsx | 5 +- .../src/conversation/conversation_view.tsx | 8 +- .../__storybook_mocks__/use_knowledge_base.ts | 1 + .../src/hooks/use_knowledge_base.tsx | 1 + .../server/config.ts | 1 + .../server/plugin.ts | 5 +- .../server/routes/knowledge_base/route.ts | 1 + .../server/service/client/index.ts | 18 ++- .../server/service/index.ts | 144 +++++++++-------- .../service/knowledge_base_service/index.ts | 36 ++++- .../public/components/nav_control/index.tsx | 2 +- .../kibana.jsonc | 23 ++- .../public/app.tsx | 11 +- .../public/context/app_context.tsx | 2 + .../public/helpers/test_helper.tsx | 5 + .../public/index.ts | 21 ++- .../public/plugin.ts | 15 +- .../routes/components/settings_page.test.tsx | 23 ++- .../routes/components/settings_page.tsx | 3 + .../settings_tab/settings_tab.test.tsx | 4 + .../components/settings_tab/settings_tab.tsx | 80 ++++----- .../components/settings_tab/ui_settings.tsx | 29 ++-- .../server/config.ts | 22 +++ .../server/index.ts | 2 + .../tsconfig.json | 13 +- x-pack/plugins/search_assistant/kibana.jsonc | 1 - .../search_assistant/public/application.tsx | 34 ---- .../public/components/app.tsx | 15 -- .../public/components/nav_control/index.tsx | 152 ++++++++++++++++++ .../nav_control/lazy_nav_control.tsx | 26 +++ .../conversation_view_with_props.tsx | 36 ----- .../public/components/routes/router.tsx | 32 ---- .../public/{plugin.ts => plugin.tsx} | 65 ++++---- .../plugins/search_assistant/public/types.ts | 8 + x-pack/plugins/search_assistant/tsconfig.json | 13 +- .../search_inference_endpoints/kibana.jsonc | 2 +- .../knowledge_base_status.spec.ts | 1 + .../knowledge_base_status.spec.ts | 1 + .../page_objects/svl_search_homepage.ts | 3 + .../test_suites/search/search_homepage.ts | 4 + 50 files changed, 639 insertions(+), 350 deletions(-) create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_management/server/config.ts delete mode 100644 x-pack/plugins/search_assistant/public/application.tsx delete mode 100644 x-pack/plugins/search_assistant/public/components/app.tsx create mode 100644 x-pack/plugins/search_assistant/public/components/nav_control/index.tsx create mode 100644 x-pack/plugins/search_assistant/public/components/nav_control/lazy_nav_control.tsx delete mode 100644 x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx delete mode 100644 x-pack/plugins/search_assistant/public/components/routes/router.tsx rename x-pack/plugins/search_assistant/public/{plugin.ts => plugin.tsx} (51%) diff --git a/config/serverless.es.yml b/config/serverless.es.yml index 326b5f2d403bd..481b46155dc64 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -10,7 +10,6 @@ xpack.observability.enabled: false xpack.securitySolution.enabled: false xpack.serverless.observability.enabled: false enterpriseSearch.enabled: false -xpack.fleet.enabled: false xpack.observabilityAIAssistant.enabled: false xpack.osquery.enabled: false @@ -88,4 +87,15 @@ xpack.searchInferenceEndpoints.ui.enabled: false xpack.search.notebooks.catalog.url: https://elastic-enterprise-search.s3.us-east-2.amazonaws.com/serverless/catalog.json # Semantic text UI -xpack.index_management.dev.enableSemanticText: false +xpack.index_management.dev.enableSemanticText: true + +# AI Assistant config +xpack.observabilityAIAssistant.enabled: true +xpack.searchAssistant.enabled: true +xpack.searchAssistant.ui.enabled: true +xpack.observabilityAIAssistant.scope: "search" +xpack.observabilityAIAssistant.enableKnowledgeBase: false +aiAssistantManagementSelection.preferredAIAssistantType: "observability" +xpack.observabilityAiAssistantManagement.logSourcesEnabled: false +xpack.observabilityAiAssistantManagement.spacesEnabled: false +xpack.observabilityAiAssistantManagement.visibilityEnabled: false diff --git a/config/serverless.oblt.yml b/config/serverless.oblt.yml index 1146a9280ac4e..91f1227ce0d9f 100644 --- a/config/serverless.oblt.yml +++ b/config/serverless.oblt.yml @@ -183,6 +183,7 @@ xpack.apm.featureFlags.storageExplorerAvailable: false ## Set the AI Assistant type aiAssistantManagementSelection.preferredAIAssistantType: "observability" +xpack.observabilityAIAssistant.scope: "observability" # Specify in telemetry the project type telemetry.labels.serverless: observability diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 6a863a78cff15..83ef8629a6efc 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -362,6 +362,9 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.observability_onboarding.ui.enabled (boolean?)', 'xpack.observabilityLogsExplorer.navigation.showAppLink (boolean?|never)', 'xpack.observabilityAIAssistant.scope (observability?|search?)', + 'xpack.observabilityAiAssistantManagement.logSourcesEnabled (boolean?)', + 'xpack.observabilityAiAssistantManagement.spacesEnabled (boolean?)', + 'xpack.observabilityAiAssistantManagement.visibilityEnabled (boolean?)', 'share.new_version.enabled (boolean?)', 'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)', /** diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx index ac25fe6c3703a..4a19272e8938b 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx @@ -18,6 +18,7 @@ import { import { ConnectorSelectorBase } from '@kbn/observability-ai-assistant-plugin/public'; import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; import { useKibana } from '../hooks/use_kibana'; +import { useKnowledgeBase } from '../hooks'; export function ChatActionsMenu({ connectors, @@ -31,6 +32,7 @@ export function ChatActionsMenu({ onCopyConversationClick: () => void; }) { const { application, http } = useKibana().services; + const knowledgeBase = useKnowledgeBase(); const [isOpen, setIsOpen] = useState(false); const handleNavigateToConnectors = () => { @@ -91,15 +93,19 @@ export function ChatActionsMenu({ defaultMessage: 'Actions', }), items: [ - { - name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', { - defaultMessage: 'Manage knowledge base', - }), - onClick: () => { - toggleActionsMenu(); - handleNavigateToSettingsKnowledgeBase(); - }, - }, + ...(knowledgeBase?.status.value?.enabled + ? [ + { + name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', { + defaultMessage: 'Manage knowledge base', + }), + onClick: () => { + toggleActionsMenu(); + handleNavigateToSettingsKnowledgeBase(); + }, + }, + ] + : []), { name: i18n.translate('xpack.aiAssistant.chatHeader.actions.settings', { defaultMessage: 'AI Assistant Settings', diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx index 182cb046cba70..3809e97f059b6 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx @@ -37,6 +37,7 @@ const defaultProps: ComponentStoryObj = { loading: false, value: { ready: true, + enabled: true, }, refresh: () => {}, }, diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx index 5b80a34e0bf7b..12cb747d148c4 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx @@ -123,7 +123,7 @@ export function ChatBody({ showLinkToConversationsApp: boolean; onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void; onToggleFlyoutPositionMode?: (flyoutPositionMode: FlyoutPositionMode) => void; - navigateToConversation: (conversationId?: string) => void; + navigateToConversation?: (conversationId?: string) => void; }) { const license = useLicense(); const hasCorrectLicense = license?.hasAtLeast('enterprise'); diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx index 8d636374ac768..1343f5ed9a4bb 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx @@ -53,7 +53,7 @@ export function ChatFlyout({ initialFlyoutPositionMode?: FlyoutPositionMode; isOpen: boolean; onClose: () => void; - navigateToConversation(conversationId?: string): void; + navigateToConversation?: (conversationId?: string) => void; }) { const { euiTheme } = useEuiTheme(); const breakpoint = useCurrentEuiBreakpoint(); @@ -272,10 +272,14 @@ export function ChatFlyout({ conversationList.conversations.refresh(); }} onToggleFlyoutPositionMode={handleToggleFlyoutPositionMode} - navigateToConversation={(newConversationId?: string) => { - if (onClose) onClose(); - navigateToConversation(newConversationId); - }} + navigateToConversation={ + navigateToConversation + ? (newConversationId?: string) => { + if (onClose) onClose(); + navigateToConversation(newConversationId); + } + : undefined + } /> diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx index c9f0588a1c90f..5110eec04c6e6 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx @@ -60,7 +60,7 @@ export function ChatHeader({ onCopyConversation: () => void; onSaveTitle: (title: string) => void; onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void; - navigateToConversation: (nextConversationId?: string) => void; + navigateToConversation?: (nextConversationId?: string) => void; }) { const theme = useEuiTheme(); const breakpoint = useCurrentEuiBreakpoint(); @@ -164,31 +164,32 @@ export function ChatHeader({ } /> - - - - + navigateToConversation(conversationId)} - /> - - } - /> - + display="block" + > + navigateToConversation(conversationId)} + /> + + } + /> + + ) : null} ) : null} diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx index 0afb0c7e79fc0..7c04c3ad0bae7 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx @@ -58,6 +58,7 @@ const defaultProps: ComponentProps = { loading: false, value: { ready: true, + enabled: true, }, refresh: () => {}, }, diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx index e87aa161d80c3..84c730129348e 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx @@ -24,6 +24,7 @@ const defaultProps: ComponentStoryObj = { loading: false, value: { ready: false, + enabled: true, }, refresh: () => {}, }, @@ -43,12 +44,15 @@ export const Loading: ComponentStoryObj = merge({}, defaultPro }); export const NotInstalled: ComponentStoryObj = merge({}, defaultProps, { - args: { knowledgeBase: { status: { loading: false, value: { ready: false } } } }, + args: { knowledgeBase: { status: { loading: false, value: { ready: false, enabled: true } } } }, }); export const Installing: ComponentStoryObj = merge({}, defaultProps, { args: { - knowledgeBase: { status: { loading: false, value: { ready: false } }, isInstalling: true }, + knowledgeBase: { + status: { loading: false, value: { ready: false, enabled: true } }, + isInstalling: true, + }, }, }); @@ -57,7 +61,7 @@ export const InstallError: ComponentStoryObj = merge({}, defau knowledgeBase: { status: { loading: false, - value: { ready: false }, + value: { ready: false, enabled: true }, }, isInstalling: false, installError: new Error(), @@ -66,5 +70,5 @@ export const InstallError: ComponentStoryObj = merge({}, defau }); export const Installed: ComponentStoryObj = merge({}, defaultProps, { - args: { knowledgeBase: { status: { loading: false, value: { ready: true } } } }, + args: { knowledgeBase: { status: { loading: false, value: { ready: true, enabled: true } } } }, }); diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx index a449235ba44e6..2ce11d16905af 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx @@ -85,8 +85,9 @@ export function WelcomeMessage({ connectors={connectors} onSetupConnectorClick={handleConnectorClick} /> - - + {knowledgeBase.status.value?.enabled ? ( + + ) : null} diff --git a/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx b/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx index fe71a9585dd1e..fb74ff7647a21 100644 --- a/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx @@ -25,7 +25,7 @@ const SECOND_SLOT_CONTAINER_WIDTH = 400; interface ConversationViewProps { conversationId?: string; - navigateToConversation: (nextConversationId?: string) => void; + navigateToConversation?: (nextConversationId?: string) => void; getConversationHref?: (conversationId: string) => string; newConversationHref?: string; scopes?: AssistantScope[]; @@ -81,7 +81,9 @@ export const ConversationView: React.FC = ({ const handleConversationUpdate = (conversation: { conversation: { id: string } }) => { if (!conversationId) { updateConversationIdInPlace(conversation.conversation.id); - navigateToConversation(conversation.conversation.id); + if (navigateToConversation) { + navigateToConversation(conversation.conversation.id); + } } handleRefreshConversations(); }; @@ -143,7 +145,7 @@ export const ConversationView: React.FC = ({ isLoading={conversationList.isLoading} onConversationDeleteClick={(deletedConversationId) => { conversationList.deleteConversation(deletedConversationId).then(() => { - if (deletedConversationId === conversationId) { + if (deletedConversationId === conversationId && navigateToConversation) { navigateToConversation(undefined); } }); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts index bcb1725d35109..8859cc716cc52 100644 --- a/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts @@ -17,6 +17,7 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult { error: undefined, value: { ready: true, + enabled: true, }, }, }; diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx b/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx index 0b949fcdbff0e..72d4fa0acf737 100644 --- a/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx @@ -20,6 +20,7 @@ import { useAIAssistantAppService } from './use_ai_assistant_app_service'; export interface UseKnowledgeBaseResult { status: AbortableAsyncState<{ ready: boolean; + enabled: boolean; error?: any; deployment_state?: MlDeploymentState; allocation_state?: MlDeploymentAllocationState; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts index dc9a780c82a1f..4d0b9fef3f2f4 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts @@ -11,6 +11,7 @@ export const config = schema.object({ enabled: schema.boolean({ defaultValue: true }), modelId: schema.maybe(schema.string()), scope: schema.maybe(schema.oneOf([schema.literal('observability'), schema.literal('search')])), + enableKnowledgeBase: schema.boolean({ defaultValue: true }), }); export type ObservabilityAIAssistantConfig = TypeOf; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts index 50687920478af..81f4e24d4d21f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts @@ -159,11 +159,14 @@ export class ObservabilityAIAssistantPlugin core, taskManager: plugins.taskManager, getModelId, + enableKnowledgeBase: this.config.enableKnowledgeBase, })); service.register(registerFunctions); - addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') }); + if (this.config.enableKnowledgeBase) { + addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') }); + } registerServerRoutes({ core, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts index 6bb024b913cde..1eb1650545781 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts @@ -28,6 +28,7 @@ const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({ handler: async ( resources ): Promise<{ + enabled: boolean; ready: boolean; error?: any; deployment_state?: MlDeploymentState; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts index 19a3dd827107b..a050edc8008fb 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts @@ -707,14 +707,16 @@ export class ObservabilityAIAssistantClient { queries: Array<{ text: string; boost?: number }>; categories?: string[]; }): Promise<{ entries: RecalledEntry[] }> => { - return this.dependencies.knowledgeBaseService.recall({ - namespace: this.dependencies.namespace, - user: this.dependencies.user, - queries, - categories, - esClient: this.dependencies.esClient, - uiSettingsClient: this.dependencies.uiSettingsClient, - }); + return ( + this.dependencies.knowledgeBaseService?.recall({ + namespace: this.dependencies.namespace, + user: this.dependencies.user, + queries, + categories, + esClient: this.dependencies.esClient, + uiSettingsClient: this.dependencies.uiSettingsClient, + }) || { entries: [] } + ); }; getKnowledgeBaseStatus = () => { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts index 63e2ee240927c..d1aba4f232b0d 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts @@ -70,6 +70,7 @@ export class ObservabilityAIAssistantService { private readonly logger: Logger; private readonly getModelId: () => Promise; private kbService?: KnowledgeBaseService; + private enableKnowledgeBase: boolean; private readonly registrations: RegistrationCallback[] = []; @@ -78,36 +79,40 @@ export class ObservabilityAIAssistantService { core, taskManager, getModelId, + enableKnowledgeBase, }: { logger: Logger; core: CoreSetup; taskManager: TaskManagerSetupContract; getModelId: () => Promise; + enableKnowledgeBase: boolean; }) { this.core = core; this.logger = logger; this.getModelId = getModelId; + this.enableKnowledgeBase = enableKnowledgeBase; this.allowInit(); - - taskManager.registerTaskDefinitions({ - [INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: { - title: 'Index queued KB articles', - description: - 'Indexes previously registered entries into the knowledge base when it is ready', - timeout: '30m', - maxAttempts: 2, - createTaskRunner: (context) => { - return { - run: async () => { - if (this.kbService) { - await this.kbService.processQueue(); - } - }, - }; + if (enableKnowledgeBase) { + taskManager.registerTaskDefinitions({ + [INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: { + title: 'Index queued KB articles', + description: + 'Indexes previously registered entries into the knowledge base when it is ready', + timeout: '30m', + maxAttempts: 2, + createTaskRunner: (context) => { + return { + run: async () => { + if (this.kbService) { + await this.kbService.processQueue(); + } + }, + }; + }, }, - }, - }); + }); + } } getKnowledgeBaseStatus() { @@ -237,6 +242,7 @@ export class ObservabilityAIAssistantService { esClient, taskManagerStart: pluginsStart.taskManager, getModelId: this.getModelId, + enabled: this.enableKnowledgeBase, }); this.logger.info('Successfully set up index assets'); @@ -331,58 +337,62 @@ export class ObservabilityAIAssistantService { } addToKnowledgeBaseQueue(entries: KnowledgeBaseEntryRequest[]): void { - this.init() - .then(() => { - this.kbService!.queue( - entries.flatMap((entry) => { - const entryWithSystemProperties = { - ...entry, - '@timestamp': new Date().toISOString(), - doc_id: entry.id, - public: true, - confidence: 'high' as const, - type: 'contextual' as const, - is_correction: false, - labels: { - ...entry.labels, - }, - role: KnowledgeBaseEntryRole.Elastic, - }; - - const operations = - 'texts' in entryWithSystemProperties - ? splitKbText(entryWithSystemProperties) - : [ - { - type: KnowledgeBaseEntryOperationType.Index, - document: entryWithSystemProperties, - }, - ]; - - return operations; - }) - ); - }) - .catch((error) => { - this.logger.error( - `Could not index ${entries.length} entries because of an initialisation error` - ); - this.logger.error(error); - }); + if (this.enableKnowledgeBase) { + this.init() + .then(() => { + this.kbService!.queue( + entries.flatMap((entry) => { + const entryWithSystemProperties = { + ...entry, + '@timestamp': new Date().toISOString(), + doc_id: entry.id, + public: true, + confidence: 'high' as const, + type: 'contextual' as const, + is_correction: false, + labels: { + ...entry.labels, + }, + role: KnowledgeBaseEntryRole.Elastic, + }; + + const operations = + 'texts' in entryWithSystemProperties + ? splitKbText(entryWithSystemProperties) + : [ + { + type: KnowledgeBaseEntryOperationType.Index, + document: entryWithSystemProperties, + }, + ]; + + return operations; + }) + ); + }) + .catch((error) => { + this.logger.error( + `Could not index ${entries.length} entries because of an initialisation error` + ); + this.logger.error(error); + }); + } } addCategoryToKnowledgeBase(categoryId: string, entries: KnowledgeBaseEntryRequest[]) { - this.addToKnowledgeBaseQueue( - entries.map((entry) => { - return { - ...entry, - labels: { - ...entry.labels, - category: categoryId, - }, - }; - }) - ); + if (this.enableKnowledgeBase) { + this.addToKnowledgeBaseQueue( + entries.map((entry) => { + return { + ...entry, + labels: { + ...entry.labels, + category: categoryId, + }, + }; + }) + ); + } } register(cb: RegistrationCallback) { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts index ee977b30f5cc7..7306a0df7c572 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts @@ -34,6 +34,7 @@ interface Dependencies { logger: Logger; taskManagerStart: TaskManagerStartContract; getModelId: () => Promise; + enabled: boolean; } export interface RecalledEntry { @@ -92,6 +93,9 @@ export class KnowledgeBaseService { } setup = async () => { + if (!this.dependencies.enabled) { + return; + } const elserModelId = await this.dependencies.getModelId(); const retryOptions = { factor: 1, minTimeout: 10000, retries: 12 }; @@ -113,9 +117,9 @@ export class KnowledgeBaseService { } catch (error) { if (isModelMissingOrUnavailableError(error)) { return false; - } else { - throw error; } + + throw error; } }; @@ -202,6 +206,9 @@ export class KnowledgeBaseService { }; private ensureTaskScheduled() { + if (!this.dependencies.enabled) { + return; + } this.dependencies.taskManagerStart .ensureScheduled({ taskType: INDEX_QUEUED_DOCUMENTS_TASK_TYPE, @@ -251,7 +258,7 @@ export class KnowledgeBaseService { } async processQueue() { - if (!this._queue.length) { + if (!this._queue.length || !this.dependencies.enabled) { return; } @@ -305,6 +312,9 @@ export class KnowledgeBaseService { } status = async () => { + if (!this.dependencies.enabled) { + return { ready: false, enabled: false }; + } const elserModelId = await this.dependencies.getModelId(); try { @@ -320,11 +330,13 @@ export class KnowledgeBaseService { deployment_state: deploymentState, allocation_state: allocationState, model_name: elserModelId, + enabled: true, }; } catch (error) { return { error: error instanceof errors.ResponseError ? error.body.error : String(error), ready: false, + enabled: true, model_name: elserModelId, }; } @@ -402,6 +414,9 @@ export class KnowledgeBaseService { }): Promise<{ entries: RecalledEntry[]; }> => { + if (!this.dependencies.enabled) { + return { entries: [] }; + } this.dependencies.logger.debug( () => `Recalling entries from KB for queries: "${JSON.stringify(queries)}"` ); @@ -474,6 +489,9 @@ export class KnowledgeBaseService { namespace: string, user?: { name: string } ): Promise> => { + if (!this.dependencies.enabled) { + return []; + } try { const response = await this.dependencies.esClient.asInternalUser.search({ index: resourceNames.aliases.kb, @@ -514,6 +532,9 @@ export class KnowledgeBaseService { sortBy?: string; sortDirection?: 'asc' | 'desc'; }): Promise<{ entries: KnowledgeBaseEntry[] }> => { + if (!this.dependencies.enabled) { + return { entries: [] }; + } try { const response = await this.dependencies.esClient.asInternalUser.search({ index: resourceNames.aliases.kb, @@ -578,6 +599,9 @@ export class KnowledgeBaseService { user?: { name: string; id?: string }; namespace?: string; }) => { + if (!this.dependencies.enabled) { + return null; + } const res = await this.dependencies.esClient.asInternalUser.search< Pick >({ @@ -607,6 +631,9 @@ export class KnowledgeBaseService { user?: { name: string; id?: string }; namespace?: string; }): Promise => { + if (!this.dependencies.enabled) { + return; + } // for now we want to limit the number of user instructions to 1 per user if (document.type === KnowledgeBaseType.UserInstruction) { const existingId = await this.getExistingUserInstructionId({ @@ -647,6 +674,9 @@ export class KnowledgeBaseService { }: { operations: KnowledgeBaseEntryOperation[]; }): Promise => { + if (!this.dependencies.enabled) { + return; + } this.dependencies.logger.info(`Starting import of ${operations.length} entries`); const limiter = pLimit(5); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx index 01202b385917a..883317c02274f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx @@ -164,7 +164,7 @@ export function NavControl() { onClose={() => { setIsOpen(false); }} - navigateToConversation={(conversationId: string) => { + navigateToConversation={(conversationId?: string) => { application.navigateToUrl( http.basePath.prepend( `/app/observabilityAIAssistant/conversations/${conversationId || ''}` diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc b/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc index ddf00c84c0ac3..f42dc2d2074d8 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc @@ -6,9 +6,24 @@ "id": "observabilityAiAssistantManagement", "server": true, "browser": true, - "configPath": ["xpack", "observabilityAiAssistantManagement"], - "requiredPlugins": ["management", "observabilityAIAssistant", "observabilityShared"], - "optionalPlugins": ["actions", "home", "serverless", "enterpriseSearch"], - "requiredBundles": ["kibanaReact", "logsDataAccess"] + "configPath": [ + "xpack", + "observabilityAiAssistantManagement" + ], + "requiredPlugins": [ + "actions", + "management", + "observabilityAIAssistant", + "observabilityShared" + ], + "optionalPlugins": [ + "home", + "serverless", + "enterpriseSearch" + ], + "requiredBundles": [ + "kibanaReact", + "logsDataAccess", + ] } } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/app.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/app.tsx index 4522e00fb37d2..9ab40cc467853 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/app.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/app.tsx @@ -15,7 +15,11 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup } from '@kbn/core/public'; import { wrapWithTheme } from '@kbn/kibana-react-plugin/public'; import { ManagementAppMountParams } from '@kbn/management-plugin/public'; -import { StartDependencies, AiAssistantManagementObservabilityPluginStart } from './plugin'; +import { + StartDependencies, + AiAssistantManagementObservabilityPluginStart, + ConfigSchema, +} from './plugin'; import { aIAssistantManagementObservabilityRouter } from './routes/config'; import { RedirectToHomeIfUnauthorized } from './routes/components/redirect_to_home_if_unauthorized'; import { AppContextProvider } from './context/app_context'; @@ -23,9 +27,10 @@ import { AppContextProvider } from './context/app_context'; interface MountParams { core: CoreSetup; mountParams: ManagementAppMountParams; + config: ConfigSchema; } -export const mountManagementSection = async ({ core, mountParams }: MountParams) => { +export const mountManagementSection = async ({ core, mountParams, config }: MountParams) => { const [coreStart, startDeps] = await core.getStartServices(); if (!startDeps.observabilityAIAssistant) return () => {}; @@ -46,7 +51,7 @@ export const mountManagementSection = async ({ core, mountParams }: MountParams) - + void; + config: ConfigSchema; } export const AppContext = createContext(null as any); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/test_helper.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/test_helper.tsx index d3941b3cd50d8..a12fb8e12d90f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/test_helper.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/test_helper.tsx @@ -70,6 +70,11 @@ export const render = ( const appContextValue = mocks?.appContextValue ?? { setBreadcrumbs: () => {}, + config: { + logSourcesEnabled: true, + spacesEnabled: true, + visibilityEnabled: true, + }, }; return testLibRender( diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/index.ts index 56d5051243f69..f61a188f37c62 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/index.ts @@ -5,13 +5,26 @@ * 2.0. */ -import { AiAssistantManagementObservabilityPlugin as AiAssistantManagementObservabilityPlugin } from './plugin'; +import { PluginInitializer, PluginInitializerContext } from '@kbn/core-plugins-browser'; +import { + AiAssistantManagementObservabilityPlugin, + AiAssistantManagementObservabilityPluginSetup, + AiAssistantManagementObservabilityPluginStart, + ConfigSchema, + SetupDependencies, + StartDependencies, +} from './plugin'; export type { AiAssistantManagementObservabilityPluginSetup, AiAssistantManagementObservabilityPluginStart, } from './plugin'; -export function plugin() { - return new AiAssistantManagementObservabilityPlugin(); -} +export const plugin: PluginInitializer< + AiAssistantManagementObservabilityPluginSetup, + AiAssistantManagementObservabilityPluginStart, + SetupDependencies, + StartDependencies +> = (pluginInitializerContext: PluginInitializerContext) => { + return new AiAssistantManagementObservabilityPlugin(pluginInitializerContext); +}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts index e2e69ef5600cf..88d007045052e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, Plugin } from '@kbn/core/public'; +import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { ManagementSetup } from '@kbn/management-plugin/public'; import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { ServerlessPluginStart } from '@kbn/serverless/public'; @@ -35,6 +35,12 @@ export interface StartDependencies { enterpriseSearch?: EnterpriseSearchPublicStart; } +export interface ConfigSchema { + logSourcesEnabled: boolean; + spacesEnabled: boolean; + visibilityEnabled: boolean; +} + export class AiAssistantManagementObservabilityPlugin implements Plugin< @@ -44,6 +50,12 @@ export class AiAssistantManagementObservabilityPlugin StartDependencies > { + private readonly config: ConfigSchema; + + constructor(context: PluginInitializerContext) { + this.config = context.config.get(); + } + public setup( core: CoreSetup, { home, management, observabilityAIAssistant }: SetupDependencies @@ -78,6 +90,7 @@ export class AiAssistantManagementObservabilityPlugin return mountManagementSection({ core, mountParams, + config: this.config, }); }, }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.test.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.test.tsx index 9a543be1938ea..c4051b9665b57 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.test.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.test.tsx @@ -8,8 +8,24 @@ import React from 'react'; import { coreStartMock, render } from '../../helpers/test_helper'; import { SettingsPage } from './settings_page'; +import { useKnowledgeBase } from '@kbn/ai-assistant'; + +jest.mock('@kbn/ai-assistant'); + +const useKnowledgeBaseMock = useKnowledgeBase as jest.Mock; describe('Settings Page', () => { + const appContextValue = { + config: { spacesEnabled: true, visibilityEnabled: true, logSourcesEnabled: true }, + setBreadcrumbs: () => {}, + }; + useKnowledgeBaseMock.mockReturnValue({ + status: { + value: { + enabled: true, + }, + }, + }); it('should navigate to home when not authorized', () => { render(, { coreStart: { @@ -21,13 +37,16 @@ describe('Settings Page', () => { }, }, }, + appContextValue, }); expect(coreStartMock.application.navigateToApp).toBeCalledWith('home'); }); it('should render settings and knowledge base tabs', () => { - const { getByTestId } = render(); + const { getByTestId } = render(, { + appContextValue, + }); expect(getByTestId('settingsPageTab-settings')).toBeInTheDocument(); expect(getByTestId('settingsPageTab-knowledge_base')).toBeInTheDocument(); @@ -36,7 +55,7 @@ describe('Settings Page', () => { it('should set breadcrumbs', () => { const setBreadcrumbs = jest.fn(); render(, { - appContextValue: { setBreadcrumbs }, + appContextValue: { ...appContextValue, setBreadcrumbs }, }); expect(setBreadcrumbs).toHaveBeenCalledWith([ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx index 075aaeb0aeb75..57a167b1080fa 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx @@ -8,6 +8,7 @@ import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; +import { useKnowledgeBase } from '@kbn/ai-assistant'; import { useAppContext } from '../../hooks/use_app_context'; import { SettingsTab } from './settings_tab/settings_tab'; import { KnowledgeBaseTab } from './knowledge_base_tab'; @@ -28,6 +29,7 @@ export function SettingsPage() { } = useKibana(); const router = useObservabilityAIAssistantManagementRouter(); + const knowledgeBase = useKnowledgeBase(); const { query: { tab }, @@ -85,6 +87,7 @@ export function SettingsPage() { } ), content: , + disabled: !knowledgeBase.status.value?.enabled, }, { id: 'search_connector', diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx index 2bed5aed37160..02ee9ba06b1ee 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx @@ -9,10 +9,14 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import { render } from '../../../helpers/test_helper'; import { SettingsTab } from './settings_tab'; +import { useAppContext } from '../../../hooks/use_app_context'; jest.mock('../../../hooks/use_app_context'); +const useAppContextMock = useAppContext as jest.Mock; + describe('SettingsTab', () => { + useAppContextMock.mockReturnValue({ config: { spacesEnabled: true, visibilityEnabled: true } }); it('should offer a way to configure Observability AI Assistant visibility in apps', () => { const navigateToAppMock = jest.fn(() => Promise.resolve()); const { getByTestId } = render(, { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx index 71b758f27f580..831ba9ff58054 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiButton, EuiDescribedFormGroup, EuiFormRow, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useAppContext } from '../../../hooks/use_app_context'; import { useKibana } from '../../../hooks/use_kibana'; import { UISettings } from './ui_settings'; @@ -15,6 +16,7 @@ export function SettingsTab() { const { application: { navigateToApp }, } = useKibana().services; + const { config } = useAppContext(); const handleNavigateToConnectors = () => { navigateToApp('management', { @@ -30,44 +32,46 @@ export function SettingsTab() { return ( - - {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantButtonLabel', - { - defaultMessage: - 'Show AI Assistant button and Contextual Insights in Observability apps', - } - )} - - } - description={ -

- {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantDescriptionLabel', - { - defaultMessage: - 'Toggle the AI Assistant button and Contextual Insights on or off in Observability apps by checking or unchecking the AI Assistant feature in Spaces > > Features.', - ignoreTag: true, - } - )} -

- } - > - - - {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel', - { defaultMessage: 'Go to Spaces' } - )} - - -
+ {config.spacesEnabled && ( + + {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantButtonLabel', + { + defaultMessage: + 'Show AI Assistant button and Contextual Insights in Observability apps', + } + )} + + } + description={ +

+ {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantDescriptionLabel', + { + defaultMessage: + 'Toggle the AI Assistant button and Contextual Insights on or off in Observability apps by checking or unchecking the AI Assistant feature in Spaces > > Features.', + ignoreTag: true, + } + )} +

+ } + > + + + {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel', + { defaultMessage: 'Go to Spaces' } + )} + + +
+ )} ); })} - - + {config.logSourcesEnabled && ( + + )} {!isEmpty(unsavedChanges) && ( ; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { logSourcesEnabled: true, spacesEnabled: true, visibilityEnabled: true }, +}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/server/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/server/index.ts index 55332dbba35c5..1592b6f4cd72e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/server/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/server/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +export { config } from './config'; + export const plugin = async () => { const { AiAssistantManagementPlugin } = await import('./plugin'); return new AiAssistantManagementPlugin(); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json index 12148ec014725..99bce73e1722f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json @@ -3,7 +3,11 @@ "compilerOptions": { "outDir": "target/types" }, - "include": ["common/**/*", "public/**/*", "server/**/*"], + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], "kbn_references": [ "@kbn/core", "@kbn/home-plugin", @@ -22,6 +26,11 @@ "@kbn/config-schema", "@kbn/core-ui-settings-common", "@kbn/logs-data-access-plugin", + "@kbn/core-plugins-browser", + "@kbn/ai-assistant", + "@kbn/core-plugins-server" ], - "exclude": ["target/**/*"] + "exclude": [ + "target/**/*" + ] } diff --git a/x-pack/plugins/search_assistant/kibana.jsonc b/x-pack/plugins/search_assistant/kibana.jsonc index 53af40cee6cc6..0f94105943037 100644 --- a/x-pack/plugins/search_assistant/kibana.jsonc +++ b/x-pack/plugins/search_assistant/kibana.jsonc @@ -15,7 +15,6 @@ "actions", "licensing", "observabilityAIAssistant", - "observabilityAIAssistantApp", "triggersActionsUi", "share" ], diff --git a/x-pack/plugins/search_assistant/public/application.tsx b/x-pack/plugins/search_assistant/public/application.tsx deleted file mode 100644 index 1bbf7063ec373..0000000000000 --- a/x-pack/plugins/search_assistant/public/application.tsx +++ /dev/null @@ -1,34 +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 ReactDOM from 'react-dom'; -import type { AppMountParameters, CoreStart } from '@kbn/core/public'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { I18nProvider } from '@kbn/i18n-react'; -import type { SearchAssistantPluginStartDependencies } from './types'; -import { SearchAssistantRouter } from './components/routes/router'; - -export const renderApp = ( - core: CoreStart, - services: SearchAssistantPluginStartDependencies, - appMountParameters: AppMountParameters -) => { - ReactDOM.render( - - - - - - - , - appMountParameters.element - ); - - return () => ReactDOM.unmountComponentAtNode(appMountParameters.element); -}; diff --git a/x-pack/plugins/search_assistant/public/components/app.tsx b/x-pack/plugins/search_assistant/public/components/app.tsx deleted file mode 100644 index 7d9497c0e1457..0000000000000 --- a/x-pack/plugins/search_assistant/public/components/app.tsx +++ /dev/null @@ -1,15 +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 { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; -export const App: React.FC = () => { - return ( - -
- - ); -}; diff --git a/x-pack/plugins/search_assistant/public/components/nav_control/index.tsx b/x-pack/plugins/search_assistant/public/components/nav_control/index.tsx new file mode 100644 index 0000000000000..a341fdbe81412 --- /dev/null +++ b/x-pack/plugins/search_assistant/public/components/nav_control/index.tsx @@ -0,0 +1,152 @@ +/* + * 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, { useEffect, useRef, useState } from 'react'; +import { AssistantAvatar, useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; +import { EuiButton, EuiLoadingSpinner, EuiToolTip, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { v4 } from 'uuid'; +import useObservable from 'react-use/lib/useObservable'; +import { i18n } from '@kbn/i18n'; +import { useAIAssistantAppService, ChatFlyout } from '@kbn/ai-assistant'; +import { useKibana } from '@kbn/ai-assistant/src/hooks/use_kibana'; +import { AIAssistantPluginStartDependencies } from '@kbn/ai-assistant/src/types'; +import { EuiErrorBoundary } from '@elastic/eui'; +import type { CoreStart } from '@kbn/core/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; +import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; + +interface NavControlWithProviderDeps { + coreStart: CoreStart; + pluginsStart: AIAssistantPluginStartDependencies; +} + +export const NavControlWithProvider = ({ coreStart, pluginsStart }: NavControlWithProviderDeps) => { + return ( + + + + + + + + + + + + ); +}; + +export function NavControl() { + const service = useAIAssistantAppService(); + + const { + services: { notifications, observabilityAIAssistant }, + } = useKibana(); + + const [hasBeenOpened, setHasBeenOpened] = useState(false); + + const chatService = useAbortableAsync( + ({ signal }) => { + return hasBeenOpened + ? service.start({ signal }).catch((error) => { + notifications?.toasts.addError(error, { + title: i18n.translate('xpack.searchAssistant.navControl.initFailureErrorTitle', { + defaultMessage: 'Failed to initialize AI Assistant', + }), + }); + + setHasBeenOpened(false); + setIsOpen(false); + + throw error; + }) + : undefined; + }, + [service, hasBeenOpened, notifications?.toasts] + ); + + const [isOpen, setIsOpen] = useState(false); + + const keyRef = useRef(v4()); + + useEffect(() => { + const conversationSubscription = service.conversations.predefinedConversation$.subscribe(() => { + keyRef.current = v4(); + setHasBeenOpened(true); + setIsOpen(true); + }); + + return () => { + conversationSubscription.unsubscribe(); + }; + }, [service.conversations.predefinedConversation$]); + + const { messages, title } = useObservable(service.conversations.predefinedConversation$) ?? { + messages: [], + title: undefined, + }; + + const theme = useEuiTheme().euiTheme; + + const buttonCss = css` + padding: 0px 8px; + + svg path { + fill: ${theme.colors.darkestShade}; + } + `; + + return ( + <> + + { + service.conversations.openNewConversation({ + messages: [], + }); + }} + color="primary" + size="s" + fullWidth={false} + minWidth={0} + > + {chatService.loading ? : } + + + {chatService.value && + Boolean(observabilityAIAssistant?.ObservabilityAIAssistantChatServiceContext) ? ( + + { + setIsOpen(false); + }} + /> + + ) : undefined} + + ); +} + +const buttonLabel = i18n.translate( + 'xpack.searchAssistant.navControl.openTheAIAssistantPopoverLabel', + { defaultMessage: 'Open the AI Assistant' } +); diff --git a/x-pack/plugins/search_assistant/public/components/nav_control/lazy_nav_control.tsx b/x-pack/plugins/search_assistant/public/components/nav_control/lazy_nav_control.tsx new file mode 100644 index 0000000000000..d37eea2fae9f4 --- /dev/null +++ b/x-pack/plugins/search_assistant/public/components/nav_control/lazy_nav_control.tsx @@ -0,0 +1,26 @@ +/* + * 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 { dynamic } from '@kbn/shared-ux-utility'; +import React from 'react'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { AIAssistantAppService } from '@kbn/ai-assistant'; +import { AIAssistantPluginStartDependencies } from '@kbn/ai-assistant/src/types'; + +const LazyNavControlWithProvider = dynamic(() => + import('.').then((m) => ({ default: m.NavControlWithProvider })) +); + +interface NavControlInitiatorProps { + appService: AIAssistantAppService; + coreStart: CoreStart; + pluginsStart: AIAssistantPluginStartDependencies; +} + +export const NavControlInitiator = ({ coreStart, pluginsStart }: NavControlInitiatorProps) => { + return ; +}; diff --git a/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx b/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx deleted file mode 100644 index 28ed6d00863f3..0000000000000 --- a/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx +++ /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 React from 'react'; -import { ConversationView } from '@kbn/ai-assistant'; -import { useParams } from 'react-router-dom'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; - -export function ConversationViewWithProps() { - const { conversationId } = useParams<{ conversationId?: string }>(); - const { - services: { application, http }, - } = useKibana(); - function navigateToConversation(nextConversationId?: string) { - application?.navigateToUrl( - http?.basePath.prepend(`/app/searchAssistant/conversations/${nextConversationId || ''}`) || '' - ); - } - return ( - - http?.basePath.prepend(`/app/searchAssistant/conversations/${id || ''}`) || '' - } - scopes={['search']} - /> - ); -} diff --git a/x-pack/plugins/search_assistant/public/components/routes/router.tsx b/x-pack/plugins/search_assistant/public/components/routes/router.tsx deleted file mode 100644 index 154bc2ab46a3e..0000000000000 --- a/x-pack/plugins/search_assistant/public/components/routes/router.tsx +++ /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 React from 'react'; -import { History } from 'history'; -import { Route, Router, Routes } from '@kbn/shared-ux-router'; -import { Redirect } from 'react-router-dom'; -import { SearchAIAssistantPageTemplate } from '../page_template'; -import { ConversationViewWithProps } from './conversations/conversation_view_with_props'; - -export const SearchAssistantRouter: React.FC<{ history: History }> = ({ history }) => { - return ( - - - - - - - - - - - - - - - ); -}; diff --git a/x-pack/plugins/search_assistant/public/plugin.ts b/x-pack/plugins/search_assistant/public/plugin.tsx similarity index 51% rename from x-pack/plugins/search_assistant/public/plugin.ts rename to x-pack/plugins/search_assistant/public/plugin.tsx index 1c09502c154ad..15c1443045cdc 100644 --- a/x-pack/plugins/search_assistant/public/plugin.ts +++ b/x-pack/plugins/search_assistant/public/plugin.tsx @@ -5,20 +5,16 @@ * 2.0. */ -import { - DEFAULT_APP_CATEGORIES, - type CoreSetup, - type Plugin, - CoreStart, - AppMountParameters, - PluginInitializerContext, -} from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; +import { type CoreSetup, type Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import { createAppService } from '@kbn/ai-assistant'; +import ReactDOM from 'react-dom'; +import React from 'react'; import type { SearchAssistantPluginSetup, SearchAssistantPluginStart, SearchAssistantPluginStartDependencies, } from './types'; +import { NavControlInitiator } from './components/nav_control/lazy_nav_control'; export interface PublicConfigType { ui: { @@ -44,36 +40,43 @@ export class SearchAssistantPlugin public setup( core: CoreSetup ): SearchAssistantPluginSetup { + return {}; + } + + public start( + coreStart: CoreStart, + pluginsStart: SearchAssistantPluginStartDependencies + ): SearchAssistantPluginStart { if (!this.config.ui.enabled) { return {}; } + const appService = createAppService({ + pluginsStart, + }); + const isEnabled = appService.isEnabled(); - core.application.register({ - id: 'searchAssistant', - title: i18n.translate('xpack.searchAssistant.appTitle', { - defaultMessage: 'Search Assistant', - }), - euiIconType: 'logoEnterpriseSearch', - appRoute: '/app/searchAssistant', - category: DEFAULT_APP_CATEGORIES.search, - visibleIn: [], - deepLinks: [], - mount: async (appMountParameters: AppMountParameters) => { - // Load application bundle and Get start services - const [{ renderApp }, [coreStart, pluginsStart]] = await Promise.all([ - import('./application'), - core.getStartServices() as Promise< - [CoreStart, SearchAssistantPluginStartDependencies, unknown] - >, - ]); + if (!isEnabled) { + return {}; + } + + coreStart.chrome.navControls.registerRight({ + mount: (element) => { + ReactDOM.render( + , + element, + () => {} + ); - return renderApp(coreStart, pluginsStart, appMountParameters); + return () => {}; }, + // right before the user profile + order: 1001, }); - return {}; - } - public start(): SearchAssistantPluginStart { return {}; } diff --git a/x-pack/plugins/search_assistant/public/types.ts b/x-pack/plugins/search_assistant/public/types.ts index b1a5d6164b1f1..5b70941d2bf0c 100644 --- a/x-pack/plugins/search_assistant/public/types.ts +++ b/x-pack/plugins/search_assistant/public/types.ts @@ -7,6 +7,10 @@ import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; import { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; +import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import { MlPluginStart } from '@kbn/ml-plugin/public'; +import { SharePluginStart } from '@kbn/share-plugin/public'; +import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SearchAssistantPluginSetup {} @@ -15,6 +19,10 @@ export interface SearchAssistantPluginSetup {} export interface SearchAssistantPluginStart {} export interface SearchAssistantPluginStartDependencies { + licensing: LicensingPluginStart; + ml: MlPluginStart; observabilityAIAssistant: ObservabilityAIAssistantPublicStart; + share: SharePluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; usageCollection?: UsageCollectionStart; } diff --git a/x-pack/plugins/search_assistant/tsconfig.json b/x-pack/plugins/search_assistant/tsconfig.json index b95020aca1dfc..30002038bbc2d 100644 --- a/x-pack/plugins/search_assistant/tsconfig.json +++ b/x-pack/plugins/search_assistant/tsconfig.json @@ -13,17 +13,22 @@ ], "kbn_references": [ "@kbn/core", - "@kbn/react-kibana-context-render", "@kbn/kibana-react-plugin", - "@kbn/i18n-react", "@kbn/shared-ux-page-kibana-template", "@kbn/usage-collection-plugin", "@kbn/observability-ai-assistant-plugin", "@kbn/config-schema", "@kbn/ai-assistant", "@kbn/i18n", - "@kbn/shared-ux-router", - "@kbn/serverless" + "@kbn/serverless", + "@kbn/react-kibana-context-theme", + "@kbn/shared-ux-link-redirect-app", + "@kbn/shared-ux-utility", + "@kbn/core-lifecycle-browser", + "@kbn/licensing-plugin", + "@kbn/ml-plugin", + "@kbn/share-plugin", + "@kbn/triggers-actions-ui-plugin" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/search_inference_endpoints/kibana.jsonc b/x-pack/plugins/search_inference_endpoints/kibana.jsonc index e7ba67795f7bf..ce49397901748 100644 --- a/x-pack/plugins/search_inference_endpoints/kibana.jsonc +++ b/x-pack/plugins/search_inference_endpoints/kibana.jsonc @@ -13,12 +13,12 @@ "requiredPlugins": [ "actions", "features", + "ml", "share", ], "optionalPlugins": [ "cloud", "console", - "ml" ], "requiredBundles": [ "kibanaReact" diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts index 62d2ab7cae785..4e9778630a535 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts @@ -49,6 +49,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(res.body).to.eql({ ready: false, model_name: TINY_ELSER.id, + enabled: true, }); }); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts index 60e02152fd4ac..60e7c743bbbbb 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts @@ -69,6 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(res.body).to.eql({ ready: false, model_name: TINY_ELSER.id, + enabled: true, }); }); }); diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts b/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts index 44bdd417330a3..0da7003e7b8dd 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts @@ -64,5 +64,8 @@ export function SvlSearchHomePageProvider({ getService }: FtrProviderContext) { keyName ); }, + async expectAIAssistantToExist() { + await testSubjects.existOrFail('AiAssistantAppNavControlButton'); + }, }; } diff --git a/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts b/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts index 036751ef970da..195790e1b0faf 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts @@ -84,5 +84,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.svlSearchHomePage.createApiKeyInFlyout('ftr-test-key'); await pageObjects.svlSearchHomePage.closeConnectionDetailsFlyout(); }); + + it('shows the AI assistant', async () => { + await pageObjects.svlSearchHomePage.expectAIAssistantToExist(); + }); }); }