diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx index 5876269d8e03a..6df0a63dd8c07 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx @@ -12,7 +12,6 @@ import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; import { GenerativeAIForObservabilityConnectorFeatureId } from '@kbn/actions-plugin/common'; import { isSupportedConnectorType } from '@kbn/inference-common'; import { AssistantBeacon } from '@kbn/ai-assistant-icon'; -import { KnowledgeBaseState } from '@kbn/observability-ai-assistant-plugin/public'; import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; import { Disclaimer } from './disclaimer'; @@ -61,13 +60,6 @@ export function WelcomeMessage({ if (isSupportedConnectorType(createdConnector.actionTypeId)) { connectors.reloadConnectors(); } - - if ( - !knowledgeBase.status.value || - knowledgeBase.status.value?.kbState === KnowledgeBaseState.NOT_INSTALLED - ) { - knowledgeBase.install(); - } }; const ConnectorFlyout = useMemo( diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message_knowledge_base.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message_knowledge_base.tsx index 75631ec635d08..6bc2b12d769e0 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message_knowledge_base.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message_knowledge_base.tsx @@ -23,6 +23,8 @@ import usePrevious from 'react-use/lib/usePrevious'; import { WelcomeMessageKnowledgeBaseSetupErrorPanel } from './welcome_message_knowledge_base_setup_error_panel'; import { UseKnowledgeBaseResult } from '../hooks'; +const inferenceId = '.elser-2-elasticsearch'; // TODO: remove hardcoded inferenceId + const SettingUpKnowledgeBase = () => ( <> @@ -56,7 +58,7 @@ const InspectKnowledgeBasePopover = ({ const handleInstall = async () => { setIsPopoverOpen(false); - await knowledgeBase.install(); + await knowledgeBase.install(inferenceId); }; return knowledgeBase.status.value?.modelStats ? ( @@ -101,7 +103,7 @@ export function WelcomeMessageKnowledgeBase({ }, [knowledgeBase.isInstalling, prevIsInstalling]); const install = async () => { - await knowledgeBase.install(); + await knowledgeBase.install(inferenceId); }; if (knowledgeBase.isInstalling) return ; diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_knowledge_base.test.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_knowledge_base.test.tsx index cacb80c484209..5650232c4e380 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_knowledge_base.test.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_knowledge_base.test.tsx @@ -73,7 +73,7 @@ describe('useKnowledgeBase', () => { // Trigger setup act(() => { - result.current.install(); + result.current.install('.elser-2-elasticsearch'); }); // Verify that the install was called @@ -81,6 +81,11 @@ describe('useKnowledgeBase', () => { expect(mockCallApi).toHaveBeenCalledWith( 'POST /internal/observability_ai_assistant/kb/setup', { + params: { + query: { + inference_id: '.elser-2-elasticsearch', + }, + }, signal: null, } ); diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx index 40aede3d9e04c..baaadadeec0e2 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx @@ -20,7 +20,7 @@ export interface UseKnowledgeBaseResult { status: AbortableAsyncState>; isInstalling: boolean; isPolling: boolean; - install: () => Promise; + install: (inferenceId: string) => Promise; } export function useKnowledgeBase(): UseKnowledgeBaseResult { @@ -44,36 +44,44 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult { } }, [isInstalling, statusRequest]); - const install = useCallback(async () => { - setIsInstalling(true); - try { - // Retry the setup with a maximum of 5 attempts - await pRetry( - async () => { - await service.callApi('POST /internal/observability_ai_assistant/kb/setup', { - signal: null, - }); - }, - { - retries: 5, + const install = useCallback( + async (inferenceId: string) => { + setIsInstalling(true); + try { + // Retry the setup with a maximum of 5 attempts + await pRetry( + async () => { + await service.callApi('POST /internal/observability_ai_assistant/kb/setup', { + params: { + query: { + inference_id: inferenceId, + }, + }, + signal: null, + }); + }, + { + retries: 5, + } + ); + if (ml.mlApi?.savedObjects.syncSavedObjects) { + await ml.mlApi.savedObjects.syncSavedObjects(); } - ); - if (ml.mlApi?.savedObjects.syncSavedObjects) { - await ml.mlApi.savedObjects.syncSavedObjects(); - } - // Refresh status after installation - statusRequest.refresh(); - } catch (error) { - notifications!.toasts.addError(error, { - title: i18n.translate('xpack.aiAssistant.errorSettingUpInferenceEndpoint', { - defaultMessage: 'Could not create inference endpoint', - }), - }); - } - }, [ml, service, notifications, statusRequest]); + // Refresh status after installation + statusRequest.refresh(); + } catch (error) { + notifications!.toasts.addError(error, { + title: i18n.translate('xpack.aiAssistant.errorSettingUpKnowledgeBase', { + defaultMessage: 'Could not setup knowledge base', + }), + }); + } + }, + [ml, service, notifications, statusRequest] + ); - // poll the status if isPolling (inference endpoint is created but deployment is not ready) + // poll the status if isPolling useEffect(() => { if (!isPolling) { return; diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/select_knowledge_base_model.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/select_knowledge_base_model.tsx new file mode 100644 index 0000000000000..6bc5a65a20727 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/select_knowledge_base_model.tsx @@ -0,0 +1,50 @@ +/* + * 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, useMemo } from 'react'; +import { EuiSelect } from '@elastic/eui'; + +export function SelectKnowledgeBaseModel({ + onSelectInferenceId, + inferenceId, +}: { + onSelectInferenceId: (inferenceId: string) => void; + inferenceId: string; +}) { + const inferenceEndpoints: Array<{ id: string; label: string }> = useMemo( + () => [ + { + label: 'ELSER v2 (English-only)', + id: '.elser-2-elasticsearch', + }, + { + label: 'E5 Small (Multilingual)', + id: '.multilingual-e5-small-elasticsearch', + }, + ], + [] + ); + + useEffect(() => { + if (!inferenceId) { + onSelectInferenceId(inferenceEndpoints[0].id); + } + }, [inferenceId, inferenceEndpoints, onSelectInferenceId]); + + return ( + ({ value: id, text: label }))} + onChange={(event) => { + const inferenceEndpoint = inferenceEndpoints.find(({ id }) => id === event.target.value)!; + onSelectInferenceId(inferenceEndpoint.id); + }} + value={inferenceId} + /> + ); +} diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 4881c517286fd..2ea3a3fa95d28 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -10175,7 +10175,6 @@ "xpack.aiAssistant.couldNotFindConversationTitle": "Conversation introuvable", "xpack.aiAssistant.disclaimer.disclaimerLabel": "Ce chat est alimenté par une intégration avec votre fournisseur LLM. Il arrive que les grands modèles de langage (LLM) présentent comme correctes des informations incorrectes. Elastic prend en charge la configuration ainsi que la connexion au fournisseur LLM et à votre base de connaissances, mais n'est pas responsable des réponses fournies par le LLM.", "xpack.aiAssistant.emptyConversationTitle": "Nouvelle conversation", - "xpack.aiAssistant.errorSettingUpInferenceEndpoint": "Impossible de créer le point de terminaison d'inférence", "xpack.aiAssistant.errorUpdatingConversation": "Impossible de mettre à jour la conversation", "xpack.aiAssistant.executedFunctionFailureEvent": "impossible d'exécuter la fonction {functionName}", "xpack.aiAssistant.flyout.confirmDeleteButtonText": "Supprimer la conversation", @@ -18632,6 +18631,9 @@ "xpack.fleet.agentLogs.logLevelSelectText": "Niveau du log", "xpack.fleet.agentLogs.oldAgentWarningTitle": "La vue Logs requiert Elastic Agent 7.11 ou une version ultérieure. Pour mettre à niveau un agent, accédez au menu Actions ou {downloadLink} une version plus récente.", "xpack.fleet.agentLogs.openInDiscoverUiLinkText": "Ouvrir dans Discover", + "xpack.fleet.agentLogs.resetLogLevel.errorTitleText": "Erreur lors de la réinitialisation du niveau de logging de l'agent", + "xpack.fleet.agentLogs.resetLogLevel.successText": "Réinitialiser le niveau de logging de l'agent pour la politique", + "xpack.fleet.agentLogs.resetLogLevelLabelText": "Réinitialiser pour rétablir la politique", "xpack.fleet.agentLogs.searchPlaceholderText": "Rechercher dans les logs…", "xpack.fleet.agentLogs.selectLogLevel.errorTitleText": "Erreur lors de la mise à jour du niveau de logging de l'agent", "xpack.fleet.agentLogs.selectLogLevel.successText": "Modification du niveau de logging de l'agent en \"{logLevel}\"", @@ -27807,6 +27809,7 @@ "xpack.ml.dataVisualizer.pageHeader": "Data Visualizer (Visualiseur de données)", "xpack.ml.datavisualizer.selector.dataVisualizerDescription": "L'outil de Machine Learning Data Visualizer (Visualiseur de données) vous aide à comprendre vos données en analysant les indicateurs et les champs dans un fichier log ou un index Elasticsearch existant.", "xpack.ml.datavisualizer.selector.dataVisualizerTitle": "Data Visualizer (Visualiseur de données)", + "xpack.ml.datavisualizer.selector.esqlTechnicalPreviewBadge.titleMsg": "Le visualiseur de données ES|QL est en version préliminaire technique.", "xpack.ml.datavisualizer.selector.importDataTitle": "Visualiser les données à partir d'un fichier", "xpack.ml.datavisualizer.selector.selectDataViewButtonLabel": "Sélectionner la vue de données", "xpack.ml.datavisualizer.selector.selectDataViewTitle": "Visualiser les données à partir d'une vue de données", @@ -27853,6 +27856,7 @@ "xpack.ml.deepLink.overview": "Aperçu", "xpack.ml.deepLink.resultExplorer": "Explorateur de résultats", "xpack.ml.deepLink.singleMetricViewer": "Visionneuse d’indicateur unique", + "xpack.ml.deepLink.suppliedConfigurations": "Configurations fournies", "xpack.ml.deleteSpaceAwareItemCheckModal.buttonTextCanDelete.job": "Continuer pour supprimer {length, plural, one {# tâche} other {# tâches}}", "xpack.ml.deleteSpaceAwareItemCheckModal.buttonTextCanDelete.model": "Continuer pour supprimer {length, plural, one {# modèle} other {# modèles}}", "xpack.ml.deleteSpaceAwareItemCheckModal.buttonTextCanUnTagConfirm": "Retirer de l'espace en cours", @@ -32675,6 +32679,20 @@ "xpack.observabilityShared.bottomBarActions.unsavedChanges": "{unsavedChangesCount, plural, =0{0 modification non enregistrée} one {1 modification non enregistrée} other {# modifications non enregistrées}}", "xpack.observabilityShared.breadcrumbs.observabilityLinkText": "Observabilité", "xpack.observabilityShared.common.constants.grouping": "Observabilité", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemDescription": "Guides détaillés des fonctionnalités d'Elastic", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemLabel": "Parcourir la documentation", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemLinkARIALabel": "En savoir plus sur toutes les fonctionnalités d'Elastic", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemLinkLabel": "En savoir plus", + "xpack.observabilityShared.experimentalOnboardingFlow.demoEnvironmentFlexItemDescription": "Explorer notre environnement de démonstration en direct", + "xpack.observabilityShared.experimentalOnboardingFlow.demoEnvironmentFlexItemLabel": "Environnement de démonstration", + "xpack.observabilityShared.experimentalOnboardingFlow.demoEnvironmentFlexItemLinkLabel": "Explorer la démonstration", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemDescription": "Échanger à propos d'Elastic", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemLabel": "Explorer le forum", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemLinkARIALabel": "Ouvrir le forum de discussion sur Elastic", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemLinkLabel": "Forum de discussion", + "xpack.observabilityShared.experimentalOnboardingFlow.supportHubFlexItemDescription": "Obtenez de l'aide dans l’ouverture d’un cas", + "xpack.observabilityShared.experimentalOnboardingFlow.supportHubFlexItemLabel": "Hub de support technique", + "xpack.observabilityShared.experimentalOnboardingFlow.supportHubFlexItemLinkLabel": "Ouvrir le Hub de support technique", "xpack.observabilityShared.featureFeedbackButton.tellUsWhatYouThinkLink": "Dites-nous ce que vous pensez !", "xpack.observabilityShared.fieldValueSelection.apply": "Appliquer", "xpack.observabilityShared.fieldValueSelection.apply.label": "Appliquer les filtres sélectionnés pour {label}", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index b5738f3afe1ce..cde893f3c11d4 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -10166,7 +10166,6 @@ "xpack.aiAssistant.couldNotFindConversationTitle": "会話が見つかりません", "xpack.aiAssistant.disclaimer.disclaimerLabel": "この会話は、LLMプロバイダーとの統合によって提供されています。LLMは、正しくない情報を正しい情報であるかのように表示する場合があることが知られています。Elasticは、構成やLLMプロバイダーへの接続、お客様のナレッジベースへの接続はサポートしますが、LLMの応答については責任を負いません。", "xpack.aiAssistant.emptyConversationTitle": "新しい会話", - "xpack.aiAssistant.errorSettingUpInferenceEndpoint": "推論エンドポイントを作成できませんでした", "xpack.aiAssistant.errorUpdatingConversation": "会話を更新できませんでした", "xpack.aiAssistant.executedFunctionFailureEvent": "関数{functionName}の実行に失敗しました", "xpack.aiAssistant.flyout.confirmDeleteButtonText": "会話を削除", @@ -18608,6 +18607,9 @@ "xpack.fleet.agentLogs.logLevelSelectText": "ログレベル", "xpack.fleet.agentLogs.oldAgentWarningTitle": "ログの表示には、Elastic Agent 7.11以降が必要です。エージェントをアップグレードするには、[アクション]メニューに移動するか、新しいバージョンを{downloadLink}。", "xpack.fleet.agentLogs.openInDiscoverUiLinkText": "Discoverで開く", + "xpack.fleet.agentLogs.resetLogLevel.errorTitleText": "エージェントログレベルのリセットエラー", + "xpack.fleet.agentLogs.resetLogLevel.successText": "エージェントログレベルをポリシーにリセット", + "xpack.fleet.agentLogs.resetLogLevelLabelText": "ポリシーにリセット", "xpack.fleet.agentLogs.searchPlaceholderText": "ログを検索…", "xpack.fleet.agentLogs.selectLogLevel.errorTitleText": "エージェントログレベルの更新エラー", "xpack.fleet.agentLogs.selectLogLevel.successText": "エージェントログレベルを''{logLevel}''に変更しました", @@ -27831,6 +27833,7 @@ "xpack.ml.deepLink.overview": "概要", "xpack.ml.deepLink.resultExplorer": "結果エクスプローラー", "xpack.ml.deepLink.singleMetricViewer": "シングルメトリックビューアー", + "xpack.ml.deepLink.suppliedConfigurations": "提供された構成", "xpack.ml.deleteSpaceAwareItemCheckModal.buttonTextCanDelete.job": "続行して、{length, plural, other {# 個のジョブ}}を削除します", "xpack.ml.deleteSpaceAwareItemCheckModal.buttonTextCanDelete.model": "続行して、{length, plural, other {# 個のモデル}}を削除します", "xpack.ml.deleteSpaceAwareItemCheckModal.buttonTextCanUnTagConfirm": "現在のスペースから削除", @@ -32655,6 +32658,20 @@ "xpack.observabilityShared.bottomBarActions.unsavedChanges": "{unsavedChangesCount, plural, other {# 未保存変更}}", "xpack.observabilityShared.breadcrumbs.observabilityLinkText": "Observability", "xpack.observabilityShared.common.constants.grouping": "Observability", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemDescription": "すべてのElastic機能に関する詳細なガイド", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemLabel": "ドキュメントを参照", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemLinkARIALabel": "すべてのElastic機能の詳細", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemLinkLabel": "詳細", + "xpack.observabilityShared.experimentalOnboardingFlow.demoEnvironmentFlexItemDescription": "Elasticのライブデモを見る", + "xpack.observabilityShared.experimentalOnboardingFlow.demoEnvironmentFlexItemLabel": "デモ環境", + "xpack.observabilityShared.experimentalOnboardingFlow.demoEnvironmentFlexItemLinkLabel": "デモの探索", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemDescription": "Elasticに関する意見を交換", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemLabel": "フォーラムを探索", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemLinkARIALabel": "Elasticディスカッションフォーラムを開く", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemLinkLabel": "ディスカッションフォーラム", + "xpack.observabilityShared.experimentalOnboardingFlow.supportHubFlexItemDescription": "ケースを作成してヘルプを依頼", + "xpack.observabilityShared.experimentalOnboardingFlow.supportHubFlexItemLabel": "サポートハブ", + "xpack.observabilityShared.experimentalOnboardingFlow.supportHubFlexItemLinkLabel": "サポートハブを開く", "xpack.observabilityShared.featureFeedbackButton.tellUsWhatYouThinkLink": "ご意見をお聞かせください。", "xpack.observabilityShared.fieldValueSelection.apply": "適用", "xpack.observabilityShared.fieldValueSelection.apply.label": "{label}に選択したフィルターを適用", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 9d3b7e27ad16d..e3e52eb2e7caf 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -10182,7 +10182,6 @@ "xpack.aiAssistant.couldNotFindConversationTitle": "未找到对话", "xpack.aiAssistant.disclaimer.disclaimerLabel": "通过集成 LLM 提供商来支持此对话。众所周知,LLM 有时会提供错误信息,好像它是正确的。Elastic 支持配置并连接到 LLM 提供商和知识库,但不对 LLM 响应负责。", "xpack.aiAssistant.emptyConversationTitle": "新对话", - "xpack.aiAssistant.errorSettingUpInferenceEndpoint": "无法创建推理终端", "xpack.aiAssistant.errorUpdatingConversation": "无法更新对话", "xpack.aiAssistant.executedFunctionFailureEvent": "无法执行函数 {functionName}", "xpack.aiAssistant.flyout.confirmDeleteButtonText": "删除对话", @@ -10196,7 +10195,7 @@ "xpack.aiAssistant.incorrectLicense.title": "升级您的许可证", "xpack.aiAssistant.initialSetupPanel.setupConnector.buttonLabel": "设置 GenAI 连接器", "xpack.aiAssistant.initialSetupPanel.setupConnector.description2": "通过为您的 AI 提供商设置连接器,开始使用 Elastic AI 助手。此模型需要支持函数调用。使用 OpenAI 或 Azure 时,建议使用 GPT4。", - "xpack.aiAssistant.newChatButton": "新聊天", + "xpack.aiAssistant.newChatButton": "新对话", "xpack.aiAssistant.prompt.placeholder": "向助手发送消息", "xpack.aiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "选择选项", "xpack.aiAssistant.settingsPage.goToConnectorsButtonLabel": "管理连接器", @@ -18649,6 +18648,9 @@ "xpack.fleet.agentLogs.logLevelSelectText": "日志级别", "xpack.fleet.agentLogs.oldAgentWarningTitle": "“日志”视图需要 Elastic Agent 7.11 或更高版本。要升级代理,请前往“操作”菜单或{downloadLink}更新的版本。", "xpack.fleet.agentLogs.openInDiscoverUiLinkText": "在 Discover 中打开", + "xpack.fleet.agentLogs.resetLogLevel.errorTitleText": "重置代理日志记录级别时出错", + "xpack.fleet.agentLogs.resetLogLevel.successText": "将代理日志记录级别重置为策略", + "xpack.fleet.agentLogs.resetLogLevelLabelText": "重置为策略", "xpack.fleet.agentLogs.searchPlaceholderText": "搜索日志……", "xpack.fleet.agentLogs.selectLogLevel.errorTitleText": "更新代理日志记录级别时出错", "xpack.fleet.agentLogs.selectLogLevel.successText": "已将代理日志记录级别更改为“{logLevel}”", @@ -27881,6 +27883,7 @@ "xpack.ml.deepLink.overview": "概览", "xpack.ml.deepLink.resultExplorer": "结果浏览器", "xpack.ml.deepLink.singleMetricViewer": "Single Metric Viewer", + "xpack.ml.deepLink.suppliedConfigurations": "提供的配置", "xpack.ml.deleteSpaceAwareItemCheckModal.buttonTextCanDelete.job": "继续删除 {length, plural, other {# 个作业}}", "xpack.ml.deleteSpaceAwareItemCheckModal.buttonTextCanDelete.model": "继续删除 {length, plural, other {# 个模型}}", "xpack.ml.deleteSpaceAwareItemCheckModal.buttonTextCanUnTagConfirm": "从当前工作区中移除", @@ -32711,6 +32714,20 @@ "xpack.observabilityShared.bottomBarActions.unsavedChanges": "{unsavedChangesCount, plural, =0{0 个未保存更改} one {1 个未保存更改} other {# 个未保存更改}}", "xpack.observabilityShared.breadcrumbs.observabilityLinkText": "Observability", "xpack.observabilityShared.common.constants.grouping": "Observability", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemDescription": "有关所有 Elastic 功能的深入指南", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemLabel": "浏览文档", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemLinkARIALabel": "详细了解所有 Elastic 功能", + "xpack.observabilityShared.experimentalOnboardingFlow.browseDocumentationFlexItemLinkLabel": "了解详情", + "xpack.observabilityShared.experimentalOnboardingFlow.demoEnvironmentFlexItemDescription": "浏览我们的实时演示环境", + "xpack.observabilityShared.experimentalOnboardingFlow.demoEnvironmentFlexItemLabel": "演示环境", + "xpack.observabilityShared.experimentalOnboardingFlow.demoEnvironmentFlexItemLinkLabel": "浏览演示", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemDescription": "交流有关 Elastic 的看法", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemLabel": "浏览论坛", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemLinkARIALabel": "打开 Elastic 讨论论坛", + "xpack.observabilityShared.experimentalOnboardingFlow.exploreForumFlexItemLinkLabel": "讨论论坛", + "xpack.observabilityShared.experimentalOnboardingFlow.supportHubFlexItemDescription": "通过创建案例获取帮助", + "xpack.observabilityShared.experimentalOnboardingFlow.supportHubFlexItemLabel": "支持中心", + "xpack.observabilityShared.experimentalOnboardingFlow.supportHubFlexItemLinkLabel": "打开支持中心", "xpack.observabilityShared.featureFeedbackButton.tellUsWhatYouThinkLink": "告诉我们您的看法!", "xpack.observabilityShared.fieldValueSelection.apply": "应用", "xpack.observabilityShared.fieldValueSelection.apply.label": "为 {label} 应用选定筛选", diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/plugin.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/plugin.ts index 56c9c76bff051..5d5e4fc8706e4 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/plugin.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/plugin.ts @@ -31,7 +31,7 @@ import { registerFunctions } from './functions'; import { recallRankingEvent } from './analytics/recall_ranking'; import { initLangtrace } from './service/client/instrumentation/init_langtrace'; import { aiAssistantCapabilities } from '../common/capabilities'; -import { populateMissingSemanticTextFieldMigration } from './service/startup_migrations/populate_missing_semantic_text_field_migration'; +import { runStartupMigrations } from './service/startup_migrations/run_startup_migrations'; import { updateExistingIndexAssets } from './service/startup_migrations/create_or_update_index_assets'; export class ObservabilityAIAssistantPlugin @@ -132,7 +132,7 @@ export class ObservabilityAIAssistantPlugin // Update existing index assets (mappings, templates, etc). This will not create assets if they do not exist. updateExistingIndexAssets({ logger: this.logger, core }) .then(() => - populateMissingSemanticTextFieldMigration({ + runStartupMigrations({ core, logger: this.logger, config: this.config, diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/knowledge_base/route.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/knowledge_base/route.ts index fefb176255579..b1559b4842ed0 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/knowledge_base/route.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/knowledge_base/route.ts @@ -13,7 +13,6 @@ import { InferenceInferenceEndpointInfo, MlTrainedModelStats, } from '@elastic/elasticsearch/lib/api/types'; -import moment from 'moment'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; import { Instruction, @@ -51,56 +50,36 @@ const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({ const setupKnowledgeBase = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', - params: t.partial({ - query: t.partial({ - model_id: t.string, + params: t.type({ + query: t.type({ + inference_id: t.string, }), }), - options: { - timeout: { - idleSocket: moment.duration(20, 'minutes').asMilliseconds(), - }, - }, - security: { - authz: { - requiredPrivileges: ['ai_assistant'], - }, - }, - handler: async (resources): Promise => { - const client = await resources.service.getClient({ request: resources.request }); - - if (!client) { - throw notImplemented(); - } - - const { model_id: modelId } = resources.params?.query ?? {}; - - return await client.setupKnowledgeBase(modelId); - }, -}); - -const resetKnowledgeBase = createObservabilityAIAssistantServerRoute({ - endpoint: 'POST /internal/observability_ai_assistant/kb/reset', security: { authz: { requiredPrivileges: ['ai_assistant'], }, }, - handler: async (resources): Promise<{ result: string }> => { + handler: async ( + resources + ): Promise<{ + reindex: boolean; + currentInferenceId: string | undefined; + nextInferenceId: string; + }> => { const client = await resources.service.getClient({ request: resources.request }); - - if (!client) { - throw notImplemented(); - } - - await client.resetKnowledgeBase(); - - return { result: 'success' }; + const { inference_id: inferenceId } = resources.params.query; + return client.setupKnowledgeBase(inferenceId); }, }); const reIndexKnowledgeBase = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/kb/reindex', + params: t.type({ + query: t.type({ + inference_id: t.string, + }), + }), security: { authz: { requiredPrivileges: ['ai_assistant'], @@ -108,14 +87,14 @@ const reIndexKnowledgeBase = createObservabilityAIAssistantServerRoute({ }, handler: async (resources): Promise<{ result: boolean }> => { const client = await resources.service.getClient({ request: resources.request }); - const result = await client.reIndexKnowledgeBaseWithLock(); + const { inference_id: inferenceId } = resources.params.query; + const result = await client.reIndexKnowledgeBaseWithLock(inferenceId); return { result }; }, }); -const semanticTextMigrationKnowledgeBase = createObservabilityAIAssistantServerRoute({ - endpoint: - 'POST /internal/observability_ai_assistant/kb/migrations/populate_missing_semantic_text_field', +const startupMigrationsKnowledgeBase = createObservabilityAIAssistantServerRoute({ + endpoint: 'POST /internal/observability_ai_assistant/kb/migrations/startup', security: { authz: { requiredPrivileges: ['ai_assistant'], @@ -128,7 +107,7 @@ const semanticTextMigrationKnowledgeBase = createObservabilityAIAssistantServerR throw notImplemented(); } - return client.reIndexKnowledgeBaseAndPopulateSemanticTextField(); + return client.runStartupMigrations(); }, }); @@ -336,9 +315,9 @@ const importKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({ export const knowledgeBaseRoutes = { ...reIndexKnowledgeBase, - ...semanticTextMigrationKnowledgeBase, + ...startupMigrationsKnowledgeBase, ...setupKnowledgeBase, - ...resetKnowledgeBase, + ...reIndexKnowledgeBase, ...getKnowledgeBaseStatus, ...getKnowledgeBaseEntries, ...saveKnowledgeBaseUserInstruction, diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/top_level/route.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/top_level/route.ts index b56b2e1f07bd2..9aa5b4492082d 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/top_level/route.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/top_level/route.ts @@ -5,20 +5,29 @@ * 2.0. */ +import * as t from 'io-ts'; import { createOrUpdateIndexAssets } from '../../service/startup_migrations/create_or_update_index_assets'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; const createOrUpdateIndexAssetsRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/index_assets', + params: t.type({ + query: t.type({ + inference_id: t.string, + }), + }), security: { authz: { requiredPrivileges: ['ai_assistant'], }, }, handler: async (resources): Promise => { + const { inference_id: inferenceId } = resources.params.query; + return createOrUpdateIndexAssets({ logger: resources.logger, core: resources.plugins.core.setup, + inferenceId, }); }, }); diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts index 5790849f3a1cb..a68062c3c2bcb 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts @@ -67,12 +67,13 @@ import { continueConversation } from './operators/continue_conversation'; import { convertInferenceEventsToStreamingEvents } from './operators/convert_inference_events_to_streaming_events'; import { extractMessages } from './operators/extract_messages'; import { getGeneratedTitle } from './operators/get_generated_title'; -import { populateMissingSemanticTextFieldMigration } from '../startup_migrations/populate_missing_semantic_text_field_migration'; +import { runStartupMigrations } from '../startup_migrations/run_startup_migrations'; import { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; import { ObservabilityAIAssistantConfig } from '../../config'; -import { getElserModelId } from '../knowledge_base_service/get_elser_model_id'; import { apmInstrumentation } from './operators/apm_instrumentation'; +import { getInferenceIdFromWriteIndex, waitForKbModel, warmupModel } from '../inference_endpoint'; import { reIndexKnowledgeBaseWithLock } from '../knowledge_base_service/reindex_knowledge_base'; +import { populateMissingSemanticTextFieldWithLock } from '../startup_migrations/populate_missing_semantic_text_fields'; const MAX_FUNCTION_CALLS = 8; @@ -103,7 +104,7 @@ export class ObservabilityAIAssistantClient { conversationId: string ): Promise | undefined> => { const response = await this.dependencies.esClient.asInternalUser.search({ - index: resourceNames.aliases.conversations, + index: resourceNames.writeIndexAlias.conversations, query: { bool: { filter: [ @@ -529,7 +530,7 @@ export class ObservabilityAIAssistantClient { find = async (options?: { query?: string }): Promise => { const response = await this.dependencies.esClient.asInternalUser.search({ - index: resourceNames.aliases.conversations, + index: resourceNames.writeIndexAlias.conversations, allow_no_indices: true, query: { bool: { @@ -594,7 +595,7 @@ export class ObservabilityAIAssistantClient { ); await this.dependencies.esClient.asInternalUser.index({ - index: resourceNames.aliases.conversations, + index: resourceNames.writeIndexAlias.conversations, document: createdConversation, refresh: true, }); @@ -663,47 +664,68 @@ export class ObservabilityAIAssistantClient { }; getKnowledgeBaseStatus = () => { - return this.dependencies.knowledgeBaseService.getStatus(); + return this.dependencies.knowledgeBaseService.getModelStatus(); }; - setupKnowledgeBase = async (modelId: string | undefined) => { - const { esClient, core, logger, knowledgeBaseService } = this.dependencies; + setupKnowledgeBase = async ( + inferenceId: string + ): Promise<{ + reindex: boolean; + currentInferenceId: string | undefined; + nextInferenceId: string; + }> => { + const { esClient, core, logger } = this.dependencies; - if (!modelId) { - modelId = await getElserModelId({ core, logger }); - } - - // setup the knowledge base - const res = await knowledgeBaseService.setup(esClient, modelId); + logger.debug(`Setting up knowledge base with inference_id: ${inferenceId}`); - populateMissingSemanticTextFieldMigration({ - core, - logger, - config: this.dependencies.config, - }).catch((e) => { - this.dependencies.logger.error( - `Failed to populate missing semantic text fields: ${e.message}` + const currentInferenceId = await getInferenceIdFromWriteIndex(esClient).catch(() => { + logger.debug( + `Current KB write index does not have an inference_id. This is to be expected for indices created before 8.16` ); + return undefined; }); - return res; - }; + if (currentInferenceId === inferenceId) { + logger.debug('Inference ID is unchanged. No need to re-index knowledge base.'); + warmupModel({ esClient, logger, inferenceId }).catch(() => {}); + return { reindex: false, currentInferenceId, nextInferenceId: inferenceId }; + } + + waitForKbModel({ esClient, logger, config: this.dependencies.config, inferenceId }) + .then(async () => { + logger.info( + `Inference ID has changed from "${currentInferenceId}" to "${inferenceId}". Re-indexing knowledge base.` + ); + + await reIndexKnowledgeBaseWithLock({ core, logger, esClient, inferenceId }); + await populateMissingSemanticTextFieldWithLock({ + core, + logger, + config: this.dependencies.config, + esClient: this.dependencies.esClient, + }); + }) + .catch((e) => { + logger.error( + `Failed to setup knowledge base with inference_id: ${inferenceId}. Error: ${e.message}` + ); + logger.debug(e); + }); - resetKnowledgeBase = () => { - const { esClient } = this.dependencies; - return this.dependencies.knowledgeBaseService.reset(esClient); + return { reindex: true, currentInferenceId, nextInferenceId: inferenceId }; }; - reIndexKnowledgeBaseWithLock = () => { + reIndexKnowledgeBaseWithLock = (inferenceId: string) => { return reIndexKnowledgeBaseWithLock({ core: this.dependencies.core, esClient: this.dependencies.esClient, logger: this.dependencies.logger, + inferenceId, }); }; - reIndexKnowledgeBaseAndPopulateSemanticTextField = () => { - return populateMissingSemanticTextFieldMigration({ + runStartupMigrations = () => { + return runStartupMigrations({ core: this.dependencies.core, logger: this.dependencies.logger, config: this.dependencies.config, diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/distributed_lock_manager/lock_manager_client.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/distributed_lock_manager/lock_manager_client.ts index 4d19684ee2f7c..b36830e224168 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/distributed_lock_manager/lock_manager_client.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/distributed_lock_manager/lock_manager_client.ts @@ -274,9 +274,7 @@ export async function withLock( // extend the ttl periodically const extendInterval = Math.floor(ttl / 4); - logger.debug( - `Lock "${lockId}" acquired. Extending TTL every ${prettyMilliseconds(extendInterval)}` - ); + logger.debug(`Extending TTL for lock "${lockId}" every ${prettyMilliseconds(extendInterval)}`); let extendTTlPromise = Promise.resolve(true); const intervalId = setInterval(() => { diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/distributed_lock_manager/lock_manager_service.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/distributed_lock_manager/lock_manager_service.ts index da9cdfef5a811..066d1a65b1efc 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/distributed_lock_manager/lock_manager_service.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/distributed_lock_manager/lock_manager_service.ts @@ -33,7 +33,7 @@ export class LockManagerService { ) { const [coreStart] = await this.coreSetup.getStartServices(); const esClient = coreStart.elasticsearch.client.asInternalUser; - const logger = this.logger.get('LockManager'); + const logger = this.logger.get('lock-manager'); return withLock({ esClient, logger, lockId, metadata }, callback); } diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index.ts index 62bf0ffb6c4e2..be1a7b9698c44 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/index.ts @@ -28,7 +28,7 @@ export const resourceNames = { conversations: getResourceName('component-template-conversations'), kb: getResourceName('component-template-kb'), }, - aliases: { + writeIndexAlias: { conversations: getResourceName('conversations'), kb: getResourceName('kb'), }, @@ -40,7 +40,7 @@ export const resourceNames = { conversations: getResourceName('index-template-conversations'), kb: getResourceName('index-template-kb'), }, - concreteIndexName: { + concreteWriteIndexName: { conversations: getResourceName('conversations-000001'), kb: getResourceName('kb-000001'), }, diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/inference_endpoint.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/inference_endpoint.ts index 946fbd2c8afe8..c6e7af847fde1 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/inference_endpoint.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/inference_endpoint.ts @@ -8,86 +8,31 @@ import { errors } from '@elastic/elasticsearch'; import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { Logger } from '@kbn/logging'; -import moment from 'moment'; -import pRetry from 'p-retry'; import { InferenceInferenceEndpointInfo, + MappingSemanticTextProperty, MlGetTrainedModelsStatsResponse, MlTrainedModelStats, } from '@elastic/elasticsearch/lib/api/types'; +import pRetry from 'p-retry'; import { KnowledgeBaseState } from '../../common'; import { ObservabilityAIAssistantConfig } from '../config'; - -export const AI_ASSISTANT_KB_INFERENCE_ID = 'obs_ai_assistant_kb_inference'; - -export async function createInferenceEndpoint({ - esClient, - logger, - modelId, -}: { - esClient: { - asCurrentUser: ElasticsearchClient; - }; - logger: Logger; - modelId: string; -}) { - try { - logger.debug(`Creating inference endpoint "${AI_ASSISTANT_KB_INFERENCE_ID}"`); - - return await esClient.asCurrentUser.inference.put( - { - inference_id: AI_ASSISTANT_KB_INFERENCE_ID, - task_type: 'sparse_embedding', - inference_config: { - service: 'elasticsearch', - service_settings: { - model_id: modelId, - adaptive_allocations: { enabled: true, min_number_of_allocations: 1 }, - num_threads: 1, - }, - task_settings: {}, - }, - }, - { - requestTimeout: moment.duration(2, 'minutes').asMilliseconds(), - } - ); - } catch (e) { - logger.error( - `Failed to create inference endpoint "${AI_ASSISTANT_KB_INFERENCE_ID}": ${e.message}` - ); - throw e; - } -} - -export async function deleteInferenceEndpoint({ - esClient, -}: { - esClient: { - asCurrentUser: ElasticsearchClient; - }; -}) { - const response = await esClient.asCurrentUser.inference.delete({ - inference_id: AI_ASSISTANT_KB_INFERENCE_ID, - force: true, - }); - - return response; -} +import { resourceNames } from '.'; export async function getInferenceEndpoint({ esClient, + inferenceId, }: { esClient: { asInternalUser: ElasticsearchClient }; + inferenceId: string; }) { const response = await esClient.asInternalUser.inference.get({ - inference_id: AI_ASSISTANT_KB_INFERENCE_ID, + inference_id: inferenceId, }); if (response.endpoints.length === 0) { throw new Error('Inference endpoint not found'); } - return response.endpoints[0]; } @@ -99,14 +44,36 @@ export function isInferenceEndpointMissingOrUnavailable(error: Error) { ); } +export async function getInferenceIdFromWriteIndex(esClient: { + asInternalUser: ElasticsearchClient; +}): Promise { + const response = await esClient.asInternalUser.indices.getFieldMapping({ + index: resourceNames.writeIndexAlias.kb, + fields: 'semantic_text', + }); + + const [indexName, indexMappings] = Object.entries(response)[0]; + const inferenceId = ( + indexMappings.mappings.semantic_text?.mapping.semantic_text as MappingSemanticTextProperty + )?.inference_id; + + if (!inferenceId) { + throw new Error(`inference_id not found in field mappings for index ${indexName}`); + } + + return inferenceId as string; +} + export async function getKbModelStatus({ esClient, logger, config, + inferenceId, }: { esClient: { asInternalUser: ElasticsearchClient }; logger: Logger; config: ObservabilityAIAssistantConfig; + inferenceId?: string; }): Promise<{ enabled: boolean; endpoint?: InferenceInferenceEndpointInfo; @@ -116,28 +83,49 @@ export async function getKbModelStatus({ }> { const enabled = config.enableKnowledgeBase; + if (!inferenceId) { + try { + inferenceId = await getInferenceIdFromWriteIndex(esClient); + logger.debug(`Using existing inference id "${inferenceId}" from write index`); + } catch (error) { + logger.debug(`Inference id not found: ${error.message}`); + return { + enabled, + errorMessage: error.message, + kbState: KnowledgeBaseState.NOT_INSTALLED, + }; + } + } + let endpoint: InferenceInferenceEndpointInfo; try { - endpoint = await getInferenceEndpoint({ esClient }); + endpoint = await getInferenceEndpoint({ esClient, inferenceId }); + logger.debug( + `Inference endpoint "${inferenceId}" found with model id "${endpoint?.service_settings?.model_id}"` + ); } catch (error) { if (!isInferenceEndpointMissingOrUnavailable(error)) { throw error; } + logger.debug(`Inference endpoint "${inferenceId}" not found or unavailable: ${error.message}`); return { enabled, errorMessage: error.message, kbState: KnowledgeBaseState.NOT_INSTALLED }; } + const modelId = endpoint?.service_settings?.model_id; let trainedModelStatsResponse: MlGetTrainedModelsStatsResponse; try { trainedModelStatsResponse = await esClient.asInternalUser.ml.getTrainedModelsStats({ - model_id: endpoint.service_settings?.model_id, + model_id: modelId, }); } catch (error) { - logger.error(`Failed to get model stats: ${error.message}`); - return { enabled, errorMessage: error.message, kbState: KnowledgeBaseState.ERROR }; + logger.error( + `Failed to get model stats for model "${modelId}" and inference id ${inferenceId}: ${error.message}` + ); + return { enabled, errorMessage: error.message, kbState: KnowledgeBaseState.NOT_INSTALLED }; } const modelStats = trainedModelStatsResponse.trained_model_stats.find( - (stats) => stats.deployment_stats?.deployment_id === AI_ASSISTANT_KB_INFERENCE_ID + (stats) => stats.deployment_stats?.deployment_id === inferenceId ); let kbState: KnowledgeBaseState; @@ -173,14 +161,20 @@ export async function waitForKbModel({ esClient, logger, config, + inferenceId, }: { esClient: { asInternalUser: ElasticsearchClient }; logger: Logger; config: ObservabilityAIAssistantConfig; + inferenceId: string; }) { + // Run a dummy inference to trigger the model to deploy + // This is a workaround for the fact that the model may not be deployed yet + await warmupModel({ esClient, logger, inferenceId }).catch(() => {}); + return pRetry( async () => { - const { kbState } = await getKbModelStatus({ esClient, logger, config }); + const { kbState } = await getKbModelStatus({ esClient, logger, config, inferenceId }); if (kbState !== KnowledgeBaseState.READY) { logger.debug('Knowledge base model is not yet ready. Retrying...'); @@ -190,3 +184,25 @@ export async function waitForKbModel({ { retries: 30, factor: 2, maxTimeout: 30_000 } ); } + +export async function warmupModel({ + esClient, + logger, + inferenceId, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + logger: Logger; + inferenceId: string; +}) { + logger.debug(`Running inference to trigger model deployment for "${inferenceId}"`); + await pRetry( + () => + esClient.asInternalUser.inference.inference({ + inference_id: inferenceId, + input: 'hello world', + }), + { retries: 10 } + ).catch((error) => { + logger.error(`Unable to run inference on endpoint "${inferenceId}": ${error.message}`); + }); +} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/kb_component_template.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/kb_component_template.ts deleted file mode 100644 index 9307aa8443497..0000000000000 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/kb_component_template.ts +++ /dev/null @@ -1,78 +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 { ClusterComponentTemplate } from '@elastic/elasticsearch/lib/api/types'; -import { AI_ASSISTANT_KB_INFERENCE_ID } from './inference_endpoint'; - -const keyword = { - type: 'keyword' as const, - ignore_above: 1024, -}; - -const text = { - type: 'text' as const, -}; - -const date = { - type: 'date' as const, -}; - -const dynamic = { - type: 'object' as const, - dynamic: true, -}; - -export const kbComponentTemplate: ClusterComponentTemplate['component_template']['template'] = { - mappings: { - dynamic: false, - properties: { - '@timestamp': date, - id: keyword, - doc_id: { type: 'text', fielddata: true }, // deprecated but kept for backwards compatibility - title: { - type: 'text', - fields: { - keyword: { - type: 'keyword', - ignore_above: 256, - }, - }, - }, - user: { - properties: { - id: keyword, - name: keyword, - }, - }, - type: keyword, - labels: dynamic, - conversation: { - properties: { - id: keyword, - title: text, - last_updated: date, - }, - }, - namespace: keyword, - text, - semantic_text: { - type: 'semantic_text', - inference_id: AI_ASSISTANT_KB_INFERENCE_ID, - }, - 'ml.tokens': { - type: 'rank_features', - }, - confidence: keyword, - is_correction: { - type: 'boolean', - }, - public: { - type: 'boolean', - }, - }, - }, -}; diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/index.ts index 9c5e16a673e16..1f41f6c7f905f 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/index.ts @@ -21,20 +21,17 @@ import { getAccessQuery, getUserAccessFilters } from '../util/get_access_query'; import { getCategoryQuery } from '../util/get_category_query'; import { getSpaceQuery } from '../util/get_space_query'; import { - createInferenceEndpoint, - deleteInferenceEndpoint, + getInferenceIdFromWriteIndex, getKbModelStatus, isInferenceEndpointMissingOrUnavailable, } from '../inference_endpoint'; import { recallFromSearchConnectors } from './recall_from_search_connectors'; import { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; import { ObservabilityAIAssistantConfig } from '../../config'; -import { - isKnowledgeBaseIndexWriteBlocked, - isSemanticTextUnsupportedError, - reIndexKnowledgeBaseWithLock, -} from './reindex_knowledge_base'; +import { reIndexKnowledgeBaseWithLock } from './reindex_knowledge_base'; import { LockAcquisitionError } from '../distributed_lock_manager/lock_manager_client'; +import { isSemanticTextUnsupportedError } from '../startup_migrations/run_startup_migrations'; +import { isKnowledgeBaseIndexWriteBlocked } from './index_write_block_utils'; interface Dependencies { core: CoreSetup; @@ -54,35 +51,13 @@ export interface RecalledEntry { labels?: Record; } -function throwKnowledgeBaseNotReady(body: any) { - throw serverUnavailable(`Knowledge base is not ready yet`, body); +function throwKnowledgeBaseNotReady(error: Error) { + throw serverUnavailable(`Knowledge base is not ready yet: ${error.message}`); } export class KnowledgeBaseService { constructor(private readonly dependencies: Dependencies) {} - async setup( - esClient: { - asCurrentUser: ElasticsearchClient; - asInternalUser: ElasticsearchClient; - }, - modelId: string - ) { - await deleteInferenceEndpoint({ esClient }).catch((e) => {}); // ensure existing inference endpoint is deleted - return createInferenceEndpoint({ esClient, logger: this.dependencies.logger, modelId }); - } - - async reset(esClient: { asCurrentUser: ElasticsearchClient }) { - try { - await deleteInferenceEndpoint({ esClient }); - } catch (error) { - if (isInferenceEndpointMissingOrUnavailable(error)) { - return; - } - throw error; - } - } - private async recallFromKnowledgeBase({ queries, categories, @@ -97,7 +72,7 @@ export class KnowledgeBaseService { const response = await this.dependencies.esClient.asInternalUser.search< Pick & { doc_id?: string } >({ - index: [resourceNames.aliases.kb], + index: [resourceNames.writeIndexAlias.kb], query: { bool: { should: queries.map(({ text, boost = 1 }) => ({ @@ -168,7 +143,7 @@ export class KnowledgeBaseService { namespace, }).catch((error) => { if (isInferenceEndpointMissingOrUnavailable(error)) { - throwKnowledgeBaseNotReady(error.body); + throwKnowledgeBaseNotReady(error); } throw error; }), @@ -229,7 +204,7 @@ export class KnowledgeBaseService { } try { const response = await this.dependencies.esClient.asInternalUser.search({ - index: resourceNames.aliases.kb, + index: resourceNames.writeIndexAlias.kb, query: { bool: { filter: [ @@ -277,7 +252,7 @@ export class KnowledgeBaseService { const response = await this.dependencies.esClient.asInternalUser.search< KnowledgeBaseEntry & { doc_id?: string } >({ - index: resourceNames.aliases.kb, + index: resourceNames.writeIndexAlias.kb, query: { bool: { filter: [ @@ -332,12 +307,28 @@ export class KnowledgeBaseService { }; } catch (error) { if (isInferenceEndpointMissingOrUnavailable(error)) { - throwKnowledgeBaseNotReady(error.body); + throwKnowledgeBaseNotReady(error); } throw error; } }; + hasEntries = async () => { + const response = await this.dependencies.esClient.asInternalUser.search({ + index: resourceNames.writeIndexAlias.kb, + size: 0, + track_total_hits: 1, + terminate_after: 1, + }); + + const hitCount = + typeof response.hits.total === 'number' + ? response.hits.total + : response.hits.total?.value ?? 0; + + return hitCount > 0; + }; + getPersonalUserInstructionId = async ({ isPublic, user, @@ -351,7 +342,7 @@ export class KnowledgeBaseService { return null; } const res = await this.dependencies.esClient.asInternalUser.search({ - index: resourceNames.aliases.kb, + index: resourceNames.writeIndexAlias.kb, query: { bool: { filter: [ @@ -399,7 +390,7 @@ export class KnowledgeBaseService { const response = await this.dependencies.esClient.asInternalUser.search({ size: 1, - index: resourceNames.aliases.kb, + index: resourceNames.writeIndexAlias.kb, query, _source: false, }); @@ -424,7 +415,7 @@ export class KnowledgeBaseService { await this.dependencies.esClient.asInternalUser.index< Omit & { namespace: string } >({ - index: resourceNames.aliases.kb, + index: resourceNames.writeIndexAlias.kb, id, document: { '@timestamp': new Date().toISOString(), @@ -440,14 +431,17 @@ export class KnowledgeBaseService { } catch (error) { this.dependencies.logger.error(`Failed to add entry to knowledge base ${error}`); if (isInferenceEndpointMissingOrUnavailable(error)) { - throwKnowledgeBaseNotReady(error.body); + throwKnowledgeBaseNotReady(error); } if (isSemanticTextUnsupportedError(error)) { + const inferenceId = await getInferenceIdFromWriteIndex(this.dependencies.esClient); + reIndexKnowledgeBaseWithLock({ core: this.dependencies.core, logger: this.dependencies.logger, esClient: this.dependencies.esClient, + inferenceId, }).catch((e) => { if (error instanceof LockAcquisitionError) { this.dependencies.logger.debug(`Re-indexing operation is already in progress`); @@ -457,7 +451,7 @@ export class KnowledgeBaseService { }); throw serverUnavailable( - `The index "${resourceNames.aliases.kb}" does not support semantic text and must be reindexed. This re-index operation has been scheduled and will be started automatically. Please try again later.` + `The index "${resourceNames.writeIndexAlias.kb}" does not support semantic text and must be reindexed. This re-index operation has been scheduled and will be started automatically. Please try again later.` ); } @@ -474,7 +468,7 @@ export class KnowledgeBaseService { deleteEntry = async ({ id }: { id: string }): Promise => { try { await this.dependencies.esClient.asInternalUser.delete({ - index: resourceNames.aliases.kb, + index: resourceNames.writeIndexAlias.kb, id, refresh: 'wait_for', }); @@ -482,25 +476,17 @@ export class KnowledgeBaseService { return Promise.resolve(); } catch (error) { if (isInferenceEndpointMissingOrUnavailable(error)) { - throwKnowledgeBaseNotReady(error.body); + throwKnowledgeBaseNotReady(error); } throw error; } }; - getStatus = async () => { - const { enabled, errorMessage, endpoint, modelStats, kbState } = await getKbModelStatus({ + getModelStatus = async () => { + return getKbModelStatus({ esClient: this.dependencies.esClient, logger: this.dependencies.logger, config: this.dependencies.config, }); - - return { - enabled, - errorMessage, - endpoint, - modelStats, - kbState, - }; }; } diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/index_write_block_utils.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/index_write_block_utils.ts new file mode 100644 index 0000000000000..053240191e34d --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/index_write_block_utils.ts @@ -0,0 +1,77 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; +import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import pRetry from 'p-retry'; +import { resourceNames } from '..'; + +export async function addIndexWriteBlock({ + esClient, + index, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + index: string; +}) { + await esClient.asInternalUser.indices.addBlock({ index, block: 'write' }); +} + +export function removeIndexWriteBlock({ + esClient, + index, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + index: string; +}) { + return esClient.asInternalUser.indices.putSettings({ + index, + body: { 'index.blocks.write': false }, + }); +} + +export async function hasIndexWriteBlock({ + esClient, + index, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + index: string; +}) { + const response = await esClient.asInternalUser.indices.getSettings({ index }); + const writeBlockSetting = Object.values(response)[0]?.settings?.index?.blocks?.write; + return writeBlockSetting === 'true' || writeBlockSetting === true; +} + +export async function waitForWriteBlockToBeRemoved({ + esClient, + logger, + index, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + logger: Logger; + index: string; +}) { + return pRetry( + async () => { + const isBlocked = await hasIndexWriteBlock({ esClient, index }); + if (isBlocked) { + logger.debug(`Waiting for the write block to be removed from "${index}"...`); + throw new Error( + 'Waiting for the re-index operation to complete and the write block to be removed...' + ); + } + }, + { forever: true, maxTimeout: 10000 } + ); +} + +export function isKnowledgeBaseIndexWriteBlocked(error: any) { + return ( + error instanceof errors.ResponseError && + error.message.includes(`cluster_block_exception`) && + error.message.includes(resourceNames.writeIndexAlias.kb) + ); +} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/reindex_knowledge_base.test.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/reindex_knowledge_base.test.ts new file mode 100644 index 0000000000000..3c2c630dbace6 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/reindex_knowledge_base.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { getNextWriteIndexName } from './reindex_knowledge_base'; + +describe('getNextWriteIndexName', () => { + it('should return the next write index name', async () => { + expect(getNextWriteIndexName('.kibana-observability-ai-assistant-kb-000008')).toBe( + '.kibana-observability-ai-assistant-kb-000009' + ); + }); + + it('should return empty when input is empty', async () => { + expect(getNextWriteIndexName(undefined)).toBe(undefined); + }); + + it('should return empty when the sequence number is missing', async () => { + expect(getNextWriteIndexName('.kibana-observability-ai-assistant-kb')).toBe(undefined); + }); + + it('should return empty when the sequence number is not a number', async () => { + expect(getNextWriteIndexName('.kibana-observability-ai-assistant-kb-foobar')).toBe(undefined); + }); +}); diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/reindex_knowledge_base.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/reindex_knowledge_base.ts index 873c47da58da8..860b583e3a38b 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/reindex_knowledge_base.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/reindex_knowledge_base.ts @@ -5,106 +5,271 @@ * 2.0. */ -import { errors as EsErrors } from '@elastic/elasticsearch'; import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { Logger } from '@kbn/logging'; +import { last } from 'lodash'; +import pRetry from 'p-retry'; import { CoreSetup } from '@kbn/core/server'; +import { errors } from '@elastic/elasticsearch'; import { resourceNames } from '..'; -import { createKbConcreteIndex } from '../startup_migrations/create_or_update_index_assets'; import { LockManagerService } from '../distributed_lock_manager/lock_manager_service'; import { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; +import { + addIndexWriteBlock, + hasIndexWriteBlock, + removeIndexWriteBlock, +} from './index_write_block_utils'; export const KB_REINDEXING_LOCK_ID = 'observability_ai_assistant:kb_reindexing'; export async function reIndexKnowledgeBaseWithLock({ core, logger, esClient, + inferenceId, }: { core: CoreSetup; logger: Logger; esClient: { asInternalUser: ElasticsearchClient; }; + inferenceId: string; }): Promise { const lmService = new LockManagerService(core, logger); return lmService.withLock(KB_REINDEXING_LOCK_ID, () => - reIndexKnowledgeBase({ logger, esClient }) + reIndexKnowledgeBaseWithWriteIndexBlock({ + logger: logger.get('kb-reindex'), + esClient, + inferenceId, + }) ); } -async function reIndexKnowledgeBase({ +async function reIndexKnowledgeBaseWithWriteIndexBlock({ logger, esClient, + inferenceId, }: { logger: Logger; - esClient: { - asInternalUser: ElasticsearchClient; - }; + esClient: { asInternalUser: ElasticsearchClient }; + inferenceId: string; }): Promise { - logger.debug('Initiating knowledge base re-indexing...'); + logger.debug('Initializing re-indexing of knowledge base...'); + if (await hasIndexWriteBlock({ esClient, index: resourceNames.writeIndexAlias.kb })) { + throw new Error( + `Write block is already set on the knowledge base index: ${resourceNames.writeIndexAlias.kb}` + ); + } try { - const originalIndex = resourceNames.concreteIndexName.kb; - const tempIndex = `${resourceNames.aliases.kb}-000002`; - - // Create temporary index - logger.debug(`Creating temporary index "${tempIndex}"...`); - await esClient.asInternalUser.indices.delete({ index: tempIndex }, { ignore: [404] }); - await esClient.asInternalUser.indices.create({ index: tempIndex }); - - // Perform reindex to temporary index - logger.debug(`Re-indexing knowledge base to temporary index "${tempIndex}"...`); - await esClient.asInternalUser.reindex({ - source: { index: originalIndex }, - dest: { index: tempIndex }, - refresh: true, - wait_for_completion: true, - }); + await addIndexWriteBlock({ esClient, index: resourceNames.writeIndexAlias.kb }); + await reIndexKnowledgeBase({ logger, esClient, inferenceId }); + logger.info('Re-indexing knowledge base completed successfully.'); + } catch (error) { + logger.error(`Re-indexing knowledge base failed: ${error.message}`); + throw error; + } finally { + await removeIndexWriteBlock({ esClient, index: resourceNames.writeIndexAlias.kb }); + } - // Delete and re-create original index - logger.debug(`Deleting original index "${originalIndex}" and re-creating it...`); - await esClient.asInternalUser.indices.delete({ index: originalIndex }); - await createKbConcreteIndex({ logger, esClient }); - - // Perform reindex back to original index - logger.debug(`Re-indexing knowledge base back to original index "${originalIndex}"...`); - await esClient.asInternalUser.reindex({ - source: { index: tempIndex }, - dest: { index: originalIndex }, - refresh: true, - wait_for_completion: true, - }); + return true; +} + +async function reIndexKnowledgeBase({ + logger, + esClient, + inferenceId, +}: { + logger: Logger; + esClient: { asInternalUser: ElasticsearchClient }; + inferenceId: string; +}): Promise { + const activeReindexingTask = await getActiveReindexingTaskId(esClient); + if (activeReindexingTask) { + throw new Error( + `Re-indexing task "${activeReindexingTask}" is already in progress for the knowledge base index: ${resourceNames.writeIndexAlias.kb}` + ); + } + + const { currentWriteIndexName, nextWriteIndexName } = await getCurrentAndNextWriteIndexNames({ + esClient, + logger, + }); + + await createTargetIndex({ esClient, logger, inferenceId, indexName: nextWriteIndexName }); + + logger.info( + `Re-indexing knowledge base from "${currentWriteIndexName}" to index "${nextWriteIndexName}"...` + ); + + const reindexResponse = await esClient.asInternalUser.reindex({ + source: { index: currentWriteIndexName }, + dest: { index: nextWriteIndexName }, + refresh: true, + wait_for_completion: false, + }); + + const taskId = reindexResponse.task?.toString(); + if (taskId) { + await waitForReIndexTaskToComplete({ esClient, taskId, logger }); + } else { + throw new Error(`ID for re-indexing task was not found`); + } - // Delete temporary index - logger.debug(`Deleting temporary index "${tempIndex}"...`); - await esClient.asInternalUser.indices.delete({ index: tempIndex }); + // Delete original index + logger.debug(`Deleting write index "${currentWriteIndexName}"`); + await esClient.asInternalUser.indices.delete({ index: currentWriteIndexName }); + + // Point write index alias to the new index + await updateWriteIndexAlias({ esClient, logger, index: nextWriteIndexName }); +} + +export async function updateWriteIndexAlias({ + esClient, + logger, + index, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + logger: Logger; + index: string; +}) { + logger.debug(`Updating write index alias to "${index}"`); + await esClient.asInternalUser.indices.updateAliases({ + actions: [ + { + add: { + index, + alias: resourceNames.writeIndexAlias.kb, + is_write_index: true, + }, + }, + ], + }); +} - logger.info('Re-indexing knowledge base completed successfully'); - return true; +export async function createTargetIndex({ + esClient, + logger, + inferenceId, + indexName, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + logger: Logger; + inferenceId: string; + indexName: string; +}) { + logger.debug(`Creating new write index "${indexName}"`); + try { + await esClient.asInternalUser.indices.create({ + index: indexName, + mappings: { + properties: { + semantic_text: { + type: 'semantic_text', + inference_id: inferenceId, + }, + }, + }, + }); } catch (error) { - logger.error(`Failed to re-index knowledge base: ${error.message}`); - throw new Error(`Failed to re-index knowledge base: ${error.message}`); + if ( + error instanceof errors.ResponseError && + error?.body?.error?.type === 'resource_already_exists_exception' + ) { + throw new Error( + `Write index "${indexName}" already exists. Please delete it before re-indexing.` + ); + } + throw error; } } -export function isKnowledgeBaseIndexWriteBlocked(error: any) { - return ( - error instanceof EsErrors.ResponseError && - error.message.includes( - `cluster_block_exception: index [${resourceNames.concreteIndexName.kb}] blocked` - ) +async function getCurrentWriteIndexName(esClient: { asInternalUser: ElasticsearchClient }) { + const response = await esClient.asInternalUser.indices.getAlias( + { name: resourceNames.writeIndexAlias.kb }, + { ignore: [404] } ); + + const currentWriteIndexName = Object.entries(response).find( + ([, aliasInfo]) => aliasInfo.aliases[resourceNames.writeIndexAlias.kb]?.is_write_index + )?.[0]; + + return currentWriteIndexName; +} + +export function getNextWriteIndexName(currentWriteIndexName: string | undefined) { + if (!currentWriteIndexName) { + return; + } + + const latestIndexNumber = last(currentWriteIndexName.split('-')); + if (!latestIndexNumber) { + return; + } + + // sequence number must be a six digit zero padded number like 000008 or 002201 + const isSequenceNumberValid = /^\d{6}$/.test(latestIndexNumber); + if (!isSequenceNumberValid) { + return; + } + + const nextIndexSequenceNumber = (parseInt(latestIndexNumber, 10) + 1).toString().padStart(6, '0'); + return `${resourceNames.writeIndexAlias.kb}-${nextIndexSequenceNumber}`; } -export function isSemanticTextUnsupportedError(error: Error) { - const semanticTextUnsupportedError = - 'The [sparse_vector] field type is not supported on indices created on versions 8.0 to 8.10'; +async function getCurrentAndNextWriteIndexNames({ + esClient, + logger, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + logger: Logger; +}) { + const currentWriteIndexName = await getCurrentWriteIndexName(esClient); + const nextWriteIndexName = getNextWriteIndexName(currentWriteIndexName); + if (!currentWriteIndexName || !nextWriteIndexName) { + throw new Error( + `"${currentWriteIndexName}" is not a valid write index name. Skipping re-indexing of knowledge base.` + ); + } + + return { currentWriteIndexName, nextWriteIndexName }; +} + +export async function getActiveReindexingTaskId(esClient: { asInternalUser: ElasticsearchClient }) { + const response = await esClient.asInternalUser.tasks.list({ + detailed: true, + actions: ['indices:data/write/reindex'], + }); - const isSemanticTextUnspported = - error instanceof EsErrors.ResponseError && - (error.message.includes(semanticTextUnsupportedError) || - // @ts-expect-error - error.meta?.body?.error?.caused_by?.reason.includes(semanticTextUnsupportedError)); + for (const node of Object.values(response.nodes ?? {})) { + for (const [taskId, task] of Object.entries(node.tasks)) { + if (task.description?.includes(resourceNames.writeIndexAlias.kb)) { + return taskId; + } + } + } +} + +async function waitForReIndexTaskToComplete({ + esClient, + taskId, + logger, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + taskId: string; + logger: Logger; +}): Promise { + return pRetry( + async () => { + const taskResponse = await esClient.asInternalUser.tasks.get({ + task_id: taskId, + wait_for_completion: false, + }); - return isSemanticTextUnspported; + if (!taskResponse.completed) { + logger.debug(`Waiting for re-indexing task "${taskId}" to complete...`); + throw new Error(`Waiting for re-indexing task "${taskId}" to complete...`); + } + }, + { forever: true, maxTimeout: 10000 } + ); } diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/create_or_update_index_assets.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/create_or_update_index_assets.ts index 3c4a0b43c8006..ebe67e39d6124 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/create_or_update_index_assets.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/create_or_update_index_assets.ts @@ -6,28 +6,30 @@ */ import { createConcreteWriteIndex, getDataStreamAdapter } from '@kbn/alerting-plugin/server'; -import type { CoreSetup, ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { CoreSetup, Logger } from '@kbn/core/server'; import type { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; import { conversationComponentTemplate } from '../conversation_component_template'; -import { kbComponentTemplate } from '../kb_component_template'; +import { DEFAULT_INFERENCE_ENDPOINT, getComponentTemplate } from './kb_component_template'; import { resourceNames } from '..'; export async function updateExistingIndexAssets({ logger, core, + inferenceId, }: { logger: Logger; core: CoreSetup; + inferenceId?: string; }) { const [coreStart] = await core.getStartServices(); const { asInternalUser } = coreStart.elasticsearch.client; const hasKbIndex = await asInternalUser.indices.exists({ - index: resourceNames.aliases.kb, + index: resourceNames.writeIndexAlias.kb, }); const hasConversationIndex = await asInternalUser.indices.exists({ - index: resourceNames.aliases.conversations, + index: resourceNames.writeIndexAlias.conversations, }); if (!hasKbIndex && !hasConversationIndex) { @@ -35,15 +37,17 @@ export async function updateExistingIndexAssets({ return; } - await createOrUpdateIndexAssets({ logger, core }); + await createOrUpdateIndexAssets({ logger, core, inferenceId }); } export async function createOrUpdateIndexAssets({ logger, core, + inferenceId = DEFAULT_INFERENCE_ENDPOINT, // TODO: use `.elser-v2-elastic` for serverless on EIS }: { logger: Logger; core: CoreSetup; + inferenceId?: string; }) { try { logger.debug('Setting up index assets'); @@ -73,7 +77,7 @@ export async function createOrUpdateIndexAssets({ }); // Conversations: write index - const conversationAliasName = resourceNames.aliases.conversations; + const conversationAliasName = resourceNames.writeIndexAlias.conversations; await createConcreteWriteIndex({ esClient: asInternalUser, logger, @@ -82,7 +86,7 @@ export async function createOrUpdateIndexAssets({ alias: conversationAliasName, pattern: `${conversationAliasName}*`, basePattern: `${conversationAliasName}*`, - name: resourceNames.concreteIndexName.conversations, + name: resourceNames.concreteWriteIndexName.conversations, template: resourceNames.indexTemplate.conversations, }, dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts: false }), @@ -92,7 +96,7 @@ export async function createOrUpdateIndexAssets({ await asInternalUser.cluster.putComponentTemplate({ create: false, name: resourceNames.componentTemplate.kb, - template: kbComponentTemplate, + template: getComponentTemplate(inferenceId), }); // Knowledge base: index template @@ -111,7 +115,20 @@ export async function createOrUpdateIndexAssets({ }); // Knowledge base: write index - await createKbConcreteIndex({ logger, esClient: coreStart.elasticsearch.client }); + const kbAliasName = resourceNames.writeIndexAlias.kb; + await createConcreteWriteIndex({ + esClient: asInternalUser, + logger, + totalFieldsLimit: 10000, + indexPatterns: { + alias: kbAliasName, + pattern: `${kbAliasName}*`, + basePattern: `${kbAliasName}*`, + name: resourceNames.concreteWriteIndexName.kb, + template: resourceNames.indexTemplate.kb, + }, + dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts: false }), + }); logger.info('Successfully set up index assets'); } catch (error) { @@ -119,28 +136,3 @@ export async function createOrUpdateIndexAssets({ logger.debug(error); } } - -export async function createKbConcreteIndex({ - logger, - esClient, -}: { - logger: Logger; - esClient: { - asInternalUser: ElasticsearchClient; - }; -}) { - const kbAliasName = resourceNames.aliases.kb; - return createConcreteWriteIndex({ - esClient: esClient.asInternalUser, - logger, - totalFieldsLimit: 10000, - indexPatterns: { - alias: kbAliasName, - pattern: `${kbAliasName}*`, - basePattern: `${kbAliasName}*`, - name: resourceNames.concreteIndexName.kb, - template: resourceNames.indexTemplate.kb, - }, - dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts: false }), - }); -} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/kb_component_template.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/kb_component_template.ts new file mode 100644 index 0000000000000..c12b90bd932e6 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/kb_component_template.ts @@ -0,0 +1,83 @@ +/* + * 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 { ClusterComponentTemplate } from '@elastic/elasticsearch/lib/api/types'; + +export const DEFAULT_INFERENCE_ENDPOINT = '.elser-2-elasticsearch'; // TODO: use `.elser-v2-elastic` for serverless on EIS + +const keyword = { + type: 'keyword' as const, + ignore_above: 1024, +}; + +const text = { + type: 'text' as const, +}; + +const date = { + type: 'date' as const, +}; + +const dynamic = { + type: 'object' as const, + dynamic: true, +}; + +export function getComponentTemplate(inferenceId: string) { + const kbComponentTemplate: ClusterComponentTemplate['component_template']['template'] = { + mappings: { + dynamic: false, + properties: { + '@timestamp': date, + id: keyword, + doc_id: { type: 'text', fielddata: true }, // deprecated but kept for backwards compatibility + title: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + user: { + properties: { + id: keyword, + name: keyword, + }, + }, + type: keyword, + labels: dynamic, + conversation: { + properties: { + id: keyword, + title: text, + last_updated: date, + }, + }, + namespace: keyword, + text, + semantic_text: { + type: 'semantic_text', + inference_id: inferenceId, + }, + 'ml.tokens': { + type: 'rank_features', + }, + confidence: keyword, + is_correction: { + type: 'boolean', + }, + public: { + type: 'boolean', + }, + }, + }, + }; + + return kbComponentTemplate; +} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/populate_missing_semantic_text_fields.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/populate_missing_semantic_text_fields.ts new file mode 100644 index 0000000000000..5ead5a735bc67 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/populate_missing_semantic_text_fields.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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import pLimit from 'p-limit'; +import type { CoreSetup, Logger } from '@kbn/core/server'; +import { uniq } from 'lodash'; +import { KnowledgeBaseEntry } from '../../../common'; +import { resourceNames } from '..'; +import { getInferenceIdFromWriteIndex, waitForKbModel } from '../inference_endpoint'; +import { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; +import { ObservabilityAIAssistantConfig } from '../../config'; +import { LockManagerService } from '../distributed_lock_manager/lock_manager_service'; +import { sleep } from '../util/sleep'; + +const POPULATE_MISSING_SEMANTIC_TEXT_FIELDS_LOCK_ID = 'populate_missing_semantic_text_fields'; +export async function populateMissingSemanticTextFieldWithLock({ + core, + logger, + config, + esClient, +}: { + core: CoreSetup; + logger: Logger; + config: ObservabilityAIAssistantConfig; + esClient: { asInternalUser: ElasticsearchClient }; +}) { + const lmService = new LockManagerService(core, logger); + await lmService.withLock(POPULATE_MISSING_SEMANTIC_TEXT_FIELDS_LOCK_ID, async () => + populateMissingSemanticTextFieldRecursively({ esClient, logger, config }) + ); +} + +// Ensures that every doc has populated the `semantic_text` field. +// It retrieves entries without the field, updates them in batches, and continues until no entries remain. +async function populateMissingSemanticTextFieldRecursively({ + esClient, + logger, + config, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + logger: Logger; + config: ObservabilityAIAssistantConfig; +}) { + logger.debug( + 'Checking for remaining entries without semantic_text field that need to be migrated' + ); + + const response = await esClient.asInternalUser.search({ + size: 100, + track_total_hits: true, + index: [resourceNames.writeIndexAlias.kb], + query: { + bool: { + must_not: { + exists: { + field: 'semantic_text', + }, + }, + }, + }, + _source: { + excludes: ['ml.tokens'], + }, + }); + + if (response.hits.hits.length === 0) { + logger.debug('No remaining entries to migrate'); + return; + } + + const inferenceId = await getInferenceIdFromWriteIndex(esClient); + await waitForKbModel({ esClient, logger, config, inferenceId }); + + const indicesWithOutdatedEntries = uniq(response.hits.hits.map((hit) => hit._index)); + logger.debug( + `Found ${response.hits.hits.length} entries without semantic_text field in "${indicesWithOutdatedEntries}". Updating now...` + ); + + // Limit the number of concurrent requests to avoid overloading the cluster + const limiter = pLimit(20); + const promises = response.hits.hits.map((hit) => { + return limiter(() => { + if (!hit._source || !hit._id) { + return; + } + + return esClient.asInternalUser.update({ + refresh: 'wait_for', + index: resourceNames.writeIndexAlias.kb, + id: hit._id, + doc: { + ...hit._source, + semantic_text: hit._source.text ?? 'No text', + }, + }); + }); + }); + + await Promise.all(promises); + logger.debug(`Updated ${promises.length} entries`); + + await sleep(100); + await populateMissingSemanticTextFieldRecursively({ esClient, logger, config }); +} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/populate_missing_semantic_text_field_migration.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/run_startup_migrations.ts similarity index 59% rename from x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/populate_missing_semantic_text_field_migration.ts rename to x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/run_startup_migrations.ts index 7ab8d1aaf3c85..267c03a2d3bb3 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/populate_missing_semantic_text_field_migration.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/startup_migrations/run_startup_migrations.ts @@ -6,25 +6,24 @@ */ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import pLimit from 'p-limit'; import type { CoreSetup, Logger } from '@kbn/core/server'; -import { uniq } from 'lodash'; import pRetry from 'p-retry'; -import { KnowledgeBaseEntry } from '../../../common'; +import { errors } from '@elastic/elasticsearch'; import { resourceNames } from '..'; -import { waitForKbModel } from '../inference_endpoint'; +import { getInferenceIdFromWriteIndex } from '../inference_endpoint'; import { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; import { ObservabilityAIAssistantConfig } from '../../config'; import { reIndexKnowledgeBaseWithLock } from '../knowledge_base_service/reindex_knowledge_base'; import { LockManagerService } from '../distributed_lock_manager/lock_manager_service'; import { LockAcquisitionError } from '../distributed_lock_manager/lock_manager_client'; +import { populateMissingSemanticTextFieldWithLock } from './populate_missing_semantic_text_fields'; const PLUGIN_STARTUP_LOCK_ID = 'observability_ai_assistant:startup_migrations'; // This function populates the `semantic_text` field for knowledge base entries during the plugin's startup process. // It ensures all missing fields are updated in batches and uses a distributed lock to prevent conflicts in distributed environments. // If the knowledge base index does not support the `semantic_text` field, it is re-indexed. -export async function populateMissingSemanticTextFieldMigration({ +export async function runStartupMigrations({ core, logger, config, @@ -40,7 +39,7 @@ export async function populateMissingSemanticTextFieldMigration({ await lmService .withLock(PLUGIN_STARTUP_LOCK_ID, async () => { const hasKbIndex = await esClient.asInternalUser.indices.exists({ - index: resourceNames.aliases.kb, + index: resourceNames.writeIndexAlias.kb, }); if (!hasKbIndex) { @@ -54,11 +53,12 @@ export async function populateMissingSemanticTextFieldMigration({ }); if (!isKbSemanticTextCompatible) { - await reIndexKnowledgeBaseWithLock({ core, logger, esClient }); + const inferenceId = await getInferenceIdFromWriteIndex(esClient); + await reIndexKnowledgeBaseWithLock({ core, logger, esClient, inferenceId }); } await pRetry( - async () => populateMissingSemanticTextFieldRecursively({ esClient, logger, config }), + async () => populateMissingSemanticTextFieldWithLock({ core, logger, config, esClient }), { retries: 5, minTimeout: 10_000 } ); }) @@ -69,82 +69,6 @@ export async function populateMissingSemanticTextFieldMigration({ }); } -// Ensures that every doc has populated the `semantic_text` field. -// It retrieves entries without the field, updates them in batches, and continues until no entries remain. -async function populateMissingSemanticTextFieldRecursively({ - esClient, - logger, - config, -}: { - esClient: { asInternalUser: ElasticsearchClient }; - logger: Logger; - config: ObservabilityAIAssistantConfig; -}) { - logger.debug( - 'Checking for remaining entries without semantic_text field that need to be migrated' - ); - - const response = await esClient.asInternalUser.search({ - size: 100, - track_total_hits: true, - index: [resourceNames.aliases.kb], - query: { - bool: { - must_not: { - exists: { - field: 'semantic_text', - }, - }, - }, - }, - _source: { - excludes: ['ml.tokens'], - }, - }); - - if (response.hits.hits.length === 0) { - logger.debug('No remaining entries to migrate'); - return; - } - - await waitForKbModel({ esClient, logger, config }); - - const indicesWithOutdatedEntries = uniq(response.hits.hits.map((hit) => hit._index)); - logger.debug( - `Found ${response.hits.hits.length} entries without semantic_text field in "${indicesWithOutdatedEntries}". Updating now...` - ); - - // Limit the number of concurrent requests to avoid overloading the cluster - const limiter = pLimit(20); - const promises = response.hits.hits.map((hit) => { - return limiter(() => { - if (!hit._source || !hit._id) { - return; - } - - return esClient.asInternalUser.update({ - refresh: 'wait_for', - index: resourceNames.aliases.kb, - id: hit._id, - doc: { - ...hit._source, - semantic_text: hit._source.text ?? 'No text', - }, - }); - }); - }); - - await Promise.all(promises); - logger.debug(`Updated ${promises.length} entries`); - - await sleep(100); - await populateMissingSemanticTextFieldRecursively({ esClient, logger, config }); -} - -async function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - // Checks if the knowledge base index supports `semantic_text` // If the index was created before version 8.11, it requires re-indexing to support the `semantic_text` field. async function isKnowledgeBaseSemanticTextCompatible({ @@ -155,7 +79,7 @@ async function isKnowledgeBaseSemanticTextCompatible({ esClient: { asInternalUser: ElasticsearchClient }; }): Promise { const indexSettingsResponse = await esClient.asInternalUser.indices.getSettings({ - index: resourceNames.aliases.kb, + index: resourceNames.writeIndexAlias.kb, }); const results = Object.entries(indexSettingsResponse); @@ -182,3 +106,16 @@ async function isKnowledgeBaseSemanticTextCompatible({ return false; } + +export function isSemanticTextUnsupportedError(error: Error) { + const semanticTextUnsupportedError = + 'The [sparse_vector] field type is not supported on indices created on versions 8.0 to 8.10'; + + const isSemanticTextUnspported = + error instanceof errors.ResponseError && + (error.message.includes(semanticTextUnsupportedError) || + // @ts-expect-error + error.meta?.body?.error?.caused_by?.reason.includes(semanticTextUnsupportedError)); + + return isSemanticTextUnspported; +} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/sleep.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/sleep.ts new file mode 100644 index 0000000000000..358a3a6fcd020 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/util/sleep.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export async function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/context.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/context.spec.ts index fac60295be749..59caf09c31136 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/context.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/context.spec.ts @@ -25,8 +25,8 @@ import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_pr import { addSampleDocsToInternalKb, clearKnowledgeBase, - deleteKnowledgeBaseModel, - setupKnowledgeBase, + deleteTinyElserModelAndInferenceEndpoint, + deployTinyElserAndSetupKb, } from '../../utils/knowledge_base'; import { chatComplete } from '../../utils/conversation'; @@ -84,7 +84,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon connectorId = await observabilityAIAssistantAPIClient.createProxyActionConnector({ port: llmProxy.getPort(), }); - await setupKnowledgeBase(getService); + await deployTinyElserAndSetupKb(getService); await addSampleDocsToInternalKb(getService, sampleDocsForInternalKb); ({ getDocuments } = llmProxy.interceptScoreToolChoice(log)); @@ -107,7 +107,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon actionId: connectorId, }); - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); await clearKnowledgeBase(es); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/recall.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/recall.spec.ts index c3f4e4607ca71..d084d45ccd604 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/recall.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/recall.spec.ts @@ -10,10 +10,10 @@ import { first, uniq } from 'lodash'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { clearKnowledgeBase, - deleteKnowledgeBaseModel, + deleteTinyElserModelAndInferenceEndpoint, addSampleDocsToInternalKb, addSampleDocsToCustomIndex, - setupKnowledgeBase, + deployTinyElserAndSetupKb, } from '../../utils/knowledge_base'; import { animalSampleDocs, technicalSampleDocs } from '../../utils/sample_docs'; @@ -25,13 +25,13 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('recall', function () { before(async () => { - await setupKnowledgeBase(getService); + await deployTinyElserAndSetupKb(getService); await addSampleDocsToInternalKb(getService, technicalSampleDocs); await addSampleDocsToCustomIndex(getService, animalSampleDocs, customSearchConnectorIndex); }); after(async () => { - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); await clearKnowledgeBase(es); // clear custom index await es.indices.delete({ index: customSearchConnectorIndex }, { ignore: [404] }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/summarize.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/summarize.spec.ts index d074832e26c4c..6b322e4b874b3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/summarize.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/complete/functions/summarize.spec.ts @@ -15,8 +15,8 @@ import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_pr import { invokeChatCompleteWithFunctionRequest } from '../../utils/conversation'; import { clearKnowledgeBase, - deleteKnowledgeBaseModel, - setupKnowledgeBase, + deleteTinyElserModelAndInferenceEndpoint, + deployTinyElserAndSetupKb, } from '../../utils/knowledge_base'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { @@ -31,9 +31,9 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let connectorId: string; before(async () => { - await setupKnowledgeBase(getService); - + await deployTinyElserAndSetupKb(getService); proxy = await createLlmProxy(log); + connectorId = await observabilityAIAssistantAPIClient.createProxyActionConnector({ port: proxy.getPort(), }); @@ -61,12 +61,12 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }); after(async () => { - proxy.close(); + proxy?.close(); await observabilityAIAssistantAPIClient.deleteActionConnector({ actionId: connectorId, }); - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); await clearKnowledgeBase(es); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/distributed_lock_manager/distributed_lock_manager.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/distributed_lock_manager/distributed_lock_manager.spec.ts index dbc5fb9e656da..34b4330092529 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/distributed_lock_manager/distributed_lock_manager.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/distributed_lock_manager/distributed_lock_manager.spec.ts @@ -23,7 +23,7 @@ import { times } from 'lodash'; import { ToolingLog } from '@kbn/tooling-log'; import pRetry from 'p-retry'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -import { getLoggerMock } from '../utils/logger'; +import { getLoggerMock } from '../utils/kibana_mocks'; import { dateAsTimestamp, durationAsMs, sleep } from '../utils/time'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index.ts index d7f318d9244b7..0ba27d012c04a 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index.ts @@ -11,36 +11,44 @@ export default function aiAssistantApiIntegrationTests({ loadTestFile, }: DeploymentAgnosticFtrProviderContext) { describe('observability AI Assistant', function () { - loadTestFile(require.resolve('./conversations/conversations.spec.ts')); - loadTestFile(require.resolve('./connectors/connectors.spec.ts')); - loadTestFile(require.resolve('./chat/chat.spec.ts')); - loadTestFile(require.resolve('./complete/complete.spec.ts')); + // Functions loadTestFile(require.resolve('./complete/functions/alerts.spec.ts')); + loadTestFile(require.resolve('./complete/functions/context.spec.ts')); + loadTestFile(require.resolve('./complete/functions/elasticsearch.spec.ts')); + loadTestFile(require.resolve('./complete/functions/execute_query.spec.ts')); loadTestFile(require.resolve('./complete/functions/get_alerts_dataset_info.spec.ts')); loadTestFile(require.resolve('./complete/functions/get_dataset_info.spec.ts')); - loadTestFile(require.resolve('./complete/functions/execute_query.spec.ts')); - loadTestFile(require.resolve('./complete/functions/elasticsearch.spec.ts')); + loadTestFile(require.resolve('./complete/functions/recall.spec.ts')); loadTestFile(require.resolve('./complete/functions/retrieve_elastic_doc.spec.ts')); loadTestFile(require.resolve('./complete/functions/summarize.spec.ts')); - loadTestFile(require.resolve('./complete/functions/recall.spec.ts')); - loadTestFile(require.resolve('./complete/functions/context.spec.ts')); loadTestFile(require.resolve('./complete/functions/title_conversation.spec.ts')); - loadTestFile(require.resolve('./public_complete/public_complete.spec.ts')); - loadTestFile(require.resolve('./knowledge_base/knowledge_base_setup.spec.ts')); + + // knowledge base loadTestFile( require.resolve( './knowledge_base/knowledge_base_reindex_and_populate_missing_semantic_text_fields.spec.ts' ) ); + loadTestFile(require.resolve('./knowledge_base/knowledge_base_reindex_concurrency.spec.ts')); loadTestFile( require.resolve( './knowledge_base/knowledge_base_reindex_to_fix_sparse_vector_support.spec.ts' ) ); - loadTestFile(require.resolve('./knowledge_base/knowledge_base_reindex_concurrency.spec.ts')); + loadTestFile(require.resolve('./knowledge_base/knowledge_base_setup.spec.ts')); loadTestFile(require.resolve('./knowledge_base/knowledge_base_status.spec.ts')); - loadTestFile(require.resolve('./knowledge_base/knowledge_base.spec.ts')); loadTestFile(require.resolve('./knowledge_base/knowledge_base_user_instructions.spec.ts')); + loadTestFile(require.resolve('./knowledge_base/knowledge_base.spec.ts')); + + // Misc. + loadTestFile(require.resolve('./chat/chat.spec.ts')); + loadTestFile(require.resolve('./complete/complete.spec.ts')); + loadTestFile(require.resolve('./index_assets/index_assets.spec.ts')); + loadTestFile(require.resolve('./connectors/connectors.spec.ts')); + loadTestFile(require.resolve('./conversations/conversations.spec.ts')); + + // public endpoints + loadTestFile(require.resolve('./public_complete/public_complete.spec.ts')); loadTestFile(require.resolve('./distributed_lock_manager/distributed_lock_manager.spec.ts')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index_assets/index_assets.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index_assets/index_assets.spec.ts index b70ee2aad033f..1f3c6a3b0d955 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index_assets/index_assets.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index_assets/index_assets.spec.ts @@ -33,7 +33,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }); } - for (const writeIndexName of Object.values(resourceNames.concreteIndexName)) { + for (const writeIndexName of Object.values(resourceNames.concreteWriteIndexName)) { it(`should create write index: "${writeIndexName}"`, async () => { const exists = await es.indices.exists({ index: writeIndexName }); expect(exists).to.be(true); @@ -54,7 +54,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon expect(indices).to.have.length(2); expect(indices.map(({ index }) => index).sort()).to.eql( - Object.values(resourceNames.concreteIndexName).sort() + Object.values(resourceNames.concreteWriteIndexName).sort() ); }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base.spec.ts index 689e66fe984ea..22bf9a88a99e2 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base.spec.ts @@ -10,8 +10,8 @@ import { type KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/ import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { clearKnowledgeBase, - deleteKnowledgeBaseModel, - setupKnowledgeBase, + deleteTinyElserModelAndInferenceEndpoint, + deployTinyElserAndSetupKb, } from '../utils/knowledge_base'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { @@ -45,11 +45,11 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('Knowledge base', function () { before(async () => { - await setupKnowledgeBase(getService); + await deployTinyElserAndSetupKb(getService); }); after(async () => { - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); await clearKnowledgeBase(es); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_and_populate_missing_semantic_text_fields.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_and_populate_missing_semantic_text_fields.spec.ts index d280a00a11bf5..52b1bd8788c15 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_and_populate_missing_semantic_text_fields.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_and_populate_missing_semantic_text_fields.spec.ts @@ -7,14 +7,13 @@ import { orderBy } from 'lodash'; import expect from '@kbn/expect'; -import { AI_ASSISTANT_KB_INFERENCE_ID } from '@kbn/observability-ai-assistant-plugin/server/service/inference_endpoint'; import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { - deleteKnowledgeBaseModel, - clearKnowledgeBase, - setupKnowledgeBase, + deleteTinyElserModelAndInferenceEndpoint, + deployTinyElserAndSetupKb, + TINY_ELSER_INFERENCE_ID, } from '../utils/knowledge_base'; import { restoreIndexAssets } from '../utils/index_assets'; @@ -67,15 +66,14 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon this.tags(['skipServerless']); before(async () => { - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); await restoreIndexAssets(observabilityAIAssistantAPIClient, es); - await clearKnowledgeBase(es); await esArchiver.load(archive); - await setupKnowledgeBase(getService); + await deployTinyElserAndSetupKb(getService); }); after(async () => { - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); await restoreIndexAssets(observabilityAIAssistantAPIClient, es); }); @@ -90,8 +88,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('after migrating', () => { before(async () => { const { status } = await observabilityAIAssistantAPIClient.editor({ - endpoint: - 'POST /internal/observability_ai_assistant/kb/migrations/populate_missing_semantic_text_field', + endpoint: 'POST /internal/observability_ai_assistant/kb/migrations/startup', }); expect(status).to.be(200); }); @@ -116,12 +113,12 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon ).to.eql([ { text: 'To infinity and beyond!', - inferenceId: AI_ASSISTANT_KB_INFERENCE_ID, + inferenceId: TINY_ELSER_INFERENCE_ID, chunkCount: 1, }, { text: "The user's favourite color is blue.", - inferenceId: AI_ASSISTANT_KB_INFERENCE_ID, + inferenceId: TINY_ELSER_INFERENCE_ID, chunkCount: 1, }, ]); @@ -129,13 +126,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }); it('returns entries correctly via API', async () => { - const { status } = await observabilityAIAssistantAPIClient.editor({ - endpoint: - 'POST /internal/observability_ai_assistant/kb/migrations/populate_missing_semantic_text_field', - }); - - expect(status).to.be(200); - const res = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/entries', params: { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_concurrency.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_concurrency.spec.ts index 9051b00261d28..fc1833d02dabd 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_concurrency.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_concurrency.spec.ts @@ -7,12 +7,15 @@ import expect from '@kbn/expect'; import { times } from 'lodash'; +import { resourceNames } from '@kbn/observability-ai-assistant-plugin/server/service'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { - deleteKnowledgeBaseModel, - setupKnowledgeBase, + deleteTinyElserModelAndInferenceEndpoint, + deployTinyElserAndSetupKb, deleteKbIndices, addSampleDocsToInternalKb, + getConcreteWriteIndexFromAlias, + TINY_ELSER_INFERENCE_ID, } from '../utils/knowledge_base'; import { createOrUpdateIndexAssets } from '../utils/index_assets'; import { animalSampleDocs } from '../utils/sample_docs'; @@ -29,13 +32,13 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon before(async () => { await deleteKbIndices(es); await createOrUpdateIndexAssets(observabilityAIAssistantAPIClient); - await setupKnowledgeBase(getService); + await deployTinyElserAndSetupKb(getService); }); after(async () => { await deleteKbIndices(es); await createOrUpdateIndexAssets(observabilityAIAssistantAPIClient); - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); }); describe('when running multiple re-index operations in parallel', () => { @@ -59,12 +62,12 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon expect(successResults).to.have.length(1); }); - it('should fail every request but 1', async () => { + it('should fail all requests but 1', async () => { const failures = results.filter((result) => result.status !== 200); expect(failures).to.have.length(19); }); - it('throw a LockAcquisitionException for the failing requests', async () => { + it('should throw a LockAcquisitionException for the failing requests', async () => { const failures = results.filter((result) => result.status === 500); const errorMessages = failures.every( (result) => @@ -75,23 +78,29 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }); }); - describe('when running multiple re-index operations in sequence', () => { + const iterations = 5; + describe(`when running ${iterations} re-index operations in sequence`, () => { let results: Array<{ status: number; result: boolean; errorMessage: string | undefined }>; + let initialIndexSequenceNumber: number; before(async () => { + const writeIndex = await getConcreteWriteIndexFromAlias(es); + // get sequence number from write index + initialIndexSequenceNumber = parseInt(writeIndex!.slice(-6), 10); + results = []; - for (const _ of times(20)) { + for (const _ of times(iterations)) { results.push(await reIndexKnowledgeBase()); } }); - it('makes 20 requests', async () => { - expect(results).to.have.length(20); + it(`makes ${iterations} requests`, async () => { + expect(results).to.have.length(iterations); }); it('every re-index operation succeeds', async () => { const successResults = results.filter((result) => result.status === 200); - expect(successResults).to.have.length(20); + expect(successResults).to.have.length(iterations); expect(successResults.every((r) => r.result === true)).to.be(true); }); @@ -99,12 +108,25 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon const failures = results.filter((result) => result.status !== 200); expect(failures).to.have.length(0); }); + + it('should increment the write index sequence number', async () => { + const writeIndex = await getConcreteWriteIndexFromAlias(es); + const sequenceNumber = (iterations + initialIndexSequenceNumber) + .toString() + .padStart(6, '0'); // e.g. 000021 + expect(writeIndex).to.be(`${resourceNames.writeIndexAlias.kb}-${sequenceNumber}`); + }); }); }); async function reIndexKnowledgeBase() { const res = await observabilityAIAssistantAPIClient.editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/reindex', + params: { + query: { + inference_id: TINY_ELSER_INFERENCE_ID, + }, + }, }); return { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_to_fix_sparse_vector_support.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_to_fix_sparse_vector_support.spec.ts index 0ede33cc3097e..de8261366434b 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_to_fix_sparse_vector_support.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_reindex_to_fix_sparse_vector_support.spec.ts @@ -9,20 +9,28 @@ import expect from '@kbn/expect'; import { resourceNames } from '@kbn/observability-ai-assistant-plugin/server/service'; import AdmZip from 'adm-zip'; import path from 'path'; +import { KnowledgeBaseState } from '@kbn/observability-ai-assistant-plugin/common'; import { AI_ASSISTANT_SNAPSHOT_REPO_PATH } from '../../../../default_configs/stateful.config.base'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { - deleteKbIndices, - deleteKnowledgeBaseModel, - setupKnowledgeBase, + TINY_ELSER_INFERENCE_ID, + TINY_ELSER_MODEL_ID, + createTinyElserInferenceEndpoint, + deleteTinyElserModelAndInferenceEndpoint, + importTinyElserModel, } from '../utils/knowledge_base'; -import { createOrUpdateIndexAssets, restoreIndexAssets } from '../utils/index_assets'; +import { + createOrUpdateIndexAssets, + deleteIndexAssets, + restoreIndexAssets, +} from '../utils/index_assets'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantApi'); const es = getService('es'); const retry = getService('retry'); const log = getService('log'); + const ml = getService('ml'); describe('when the knowledge base index was created before 8.11', function () { // Intentionally skipped in all serverless environnments (local and MKI) @@ -30,23 +38,91 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon this.tags(['skipServerless']); before(async () => { - await unZipKbSnapshot(); - await setupKnowledgeBase(getService); - }); - - beforeEach(async () => { - await restoreKbSnapshot(); - await createOrUpdateIndexAssets(observabilityAIAssistantAPIClient); + const zipFilePath = `${AI_ASSISTANT_SNAPSHOT_REPO_PATH}.zip`; + log.debug(`Unzipping ${zipFilePath} to ${AI_ASSISTANT_SNAPSHOT_REPO_PATH}`); + new AdmZip(zipFilePath).extractAllTo(path.dirname(AI_ASSISTANT_SNAPSHOT_REPO_PATH), true); + + // in a real environment we will use the ELSER inference endpoint (`.elser-2-elasticsearch`) which is pre-installed + // the model is also preloaded (but not deployed) + await importTinyElserModel(ml); + await createTinyElserInferenceEndpoint(es, log, TINY_ELSER_INFERENCE_ID); }); after(async () => { await restoreIndexAssets(observabilityAIAssistantAPIClient, es); - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); }); - it('has an index created version earlier than 8.11', async () => { - await retry.try(async () => { - expect(await getKbIndexCreatedVersion()).to.be.lessThan(8110000); + describe('before running migrations', () => { + before(async () => { + await deleteIndexAssets(es); + await restoreKbSnapshot(); + await createOrUpdateIndexAssets(observabilityAIAssistantAPIClient); + }); + + it('has an index created version earlier than 8.11', async () => { + await retry.try(async () => { + expect(await getKbIndexCreatedVersion()).to.be.lessThan(8110000); + }); + }); + + it('cannot add new entries to KB until reindex has completed', async () => { + const res1 = await createKnowledgeBaseEntry(); + + expect(res1.status).to.be(503); + expect((res1.body as unknown as Error).message).to.eql( + 'The index ".kibana-observability-ai-assistant-kb" does not support semantic text and must be reindexed. This re-index operation has been scheduled and will be started automatically. Please try again later.' + ); + + // wait for reindex to have updated the index + await retry.try(async () => { + expect(await getKbIndexCreatedVersion()).to.be.greaterThan(8180000); + }); + + const res2 = await createKnowledgeBaseEntry(); + expect(res2.status).to.be(200); + }); + }); + + describe('after running migrations', () => { + beforeEach(async () => { + await deleteIndexAssets(es); + await restoreKbSnapshot(); + await createOrUpdateIndexAssets(observabilityAIAssistantAPIClient); + await runStartupMigrations(); + }); + + it('has an index created version later than 8.18', async () => { + await retry.try(async () => { + const indexCreatedVersion = await getKbIndexCreatedVersion(); + expect(indexCreatedVersion).to.be.greaterThan(8180000); + }); + }); + + it('can add new entries', async () => { + const { status } = await createKnowledgeBaseEntry(); + expect(status).to.be(200); + }); + + it('has default ELSER inference endpoint', async () => { + await retry.try(async () => { + const { body } = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'GET /internal/observability_ai_assistant/kb/status', + }); + + expect(body.endpoint?.inference_id).to.eql(TINY_ELSER_INFERENCE_ID); + expect(body.endpoint?.service_settings.model_id).to.eql(TINY_ELSER_MODEL_ID); + }); + }); + + it('have a deployed model', async () => { + await retry.try(async () => { + const { body } = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'GET /internal/observability_ai_assistant/kb/status', + }); + + expect(body.kbState === KnowledgeBaseState.READY).to.be(true); + }); }); }); @@ -62,49 +138,22 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon params: { body: knowledgeBaseEntry }, }); } - - it('cannot add new entries to KB', async () => { - const { status, body } = await createKnowledgeBaseEntry(); - - // @ts-expect-error - expect(body.message).to.eql( - 'The index ".kibana-observability-ai-assistant-kb" does not support semantic text and must be reindexed. This re-index operation has been scheduled and will be started automatically. Please try again later.' - ); - - expect(status).to.be(503); - }); - - it('can add new entries after re-indexing', async () => { - await reIndexKnowledgeBase(); - - await retry.try(async () => { - const { status } = await createKnowledgeBaseEntry(); - expect(status).to.be(200); - }); - }); }); async function getKbIndexCreatedVersion() { const indexSettings = await es.indices.getSettings({ - index: resourceNames.concreteIndexName.kb, + index: resourceNames.writeIndexAlias.kb, }); const { settings } = Object.values(indexSettings)[0]; return parseInt(settings?.index?.version?.created ?? '', 10); } - async function unZipKbSnapshot() { - const zipFilePath = `${AI_ASSISTANT_SNAPSHOT_REPO_PATH}.zip`; - log.debug(`Unzipping ${zipFilePath} to ${AI_ASSISTANT_SNAPSHOT_REPO_PATH}`); - new AdmZip(zipFilePath).extractAllTo(path.dirname(AI_ASSISTANT_SNAPSHOT_REPO_PATH), true); - } - async function restoreKbSnapshot() { - await deleteKbIndices(es); - log.debug( - `Restoring snapshot of ${resourceNames.concreteIndexName.kb} from ${AI_ASSISTANT_SNAPSHOT_REPO_PATH}` + `Restoring snapshot of "${resourceNames.concreteWriteIndexName.kb}" from ${AI_ASSISTANT_SNAPSHOT_REPO_PATH}` ); + const snapshotRepoName = 'snapshot-repo-8-10'; const snapshotName = 'my_snapshot'; await es.snapshot.createRepository({ @@ -119,15 +168,15 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon repository: snapshotRepoName, snapshot: snapshotName, wait_for_completion: true, - indices: resourceNames.concreteIndexName.kb, + indices: resourceNames.concreteWriteIndexName.kb, }); await es.snapshot.deleteRepository({ name: snapshotRepoName }); } - async function reIndexKnowledgeBase() { + async function runStartupMigrations() { const { status } = await observabilityAIAssistantAPIClient.editor({ - endpoint: 'POST /internal/observability_ai_assistant/kb/reindex', + endpoint: 'POST /internal/observability_ai_assistant/kb/migrations/startup', }); expect(status).to.be(200); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_setup.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_setup.spec.ts index 27284746bec29..3b6718ca5a88d 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_setup.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_setup.spec.ts @@ -6,58 +6,129 @@ */ import expect from '@kbn/expect'; +import { resourceNames } from '@kbn/observability-ai-assistant-plugin/server/service'; +import { getInferenceIdFromWriteIndex } from '@kbn/observability-ai-assistant-plugin/server/service/inference_endpoint'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -import { TINY_ELSER, deleteKnowledgeBaseModel, setupKnowledgeBase } from '../utils/knowledge_base'; +import { + TINY_ELSER_INFERENCE_ID, + deleteTinyElserModelAndInferenceEndpoint, + getConcreteWriteIndexFromAlias, + deployTinyElserAndSetupKb, + createTinyElserInferenceEndpoint, + waitForKnowledgeBaseReady, + deleteInferenceEndpoint, +} from '../utils/knowledge_base'; import { restoreIndexAssets } from '../utils/index_assets'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const es = getService('es'); + const retry = getService('retry'); + const log = getService('log'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantApi'); describe('/internal/observability_ai_assistant/kb/setup', function () { before(async () => { - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); await restoreIndexAssets(observabilityAIAssistantAPIClient, es); }); afterEach(async () => { - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); await restoreIndexAssets(observabilityAIAssistantAPIClient, es); }); - it('returns model info when successful', async () => { - const res = await setupKnowledgeBase(getService); + it('returns 200 when model is deployed', async () => { + const { status } = await deployTinyElserAndSetupKb(getService); + expect(status).to.be(200); + }); + + it('returns 200 if model is not deployed', async () => { + const { status } = await setupKbAsAdmin(TINY_ELSER_INFERENCE_ID); + expect(status).to.be(200); + }); - expect(res.body.service_settings.model_id).to.be('pt_tiny_elser'); - expect(res.body.inference_id).to.be('obs_ai_assistant_kb_inference'); + it('has "pt_tiny_elser_inference_id" as initial inference id', async () => { + const inferenceId = await getInferenceIdFromWriteIndex({ asInternalUser: es }); + expect(inferenceId).to.be(TINY_ELSER_INFERENCE_ID); }); - it('returns error message if model is not deployed', async () => { - const res = await setupKnowledgeBase(getService, { deployModel: false }); + describe('re-indexing', () => { + describe('running setup for a different inference endpoint', () => { + const CUSTOM_TINY_ELSER_INFERENCE_ID = 'custom_tiny_elser_inference_id'; + let body: Awaited>['body']; + + before(async () => { + // setup KB initially + await deployTinyElserAndSetupKb(getService); + + // setup KB with custom inference endpoint + await createTinyElserInferenceEndpoint(es, log, CUSTOM_TINY_ELSER_INFERENCE_ID); + const res = await setupKbAsAdmin(CUSTOM_TINY_ELSER_INFERENCE_ID); + body = res.body; + + await waitForKnowledgeBaseReady({ observabilityAIAssistantAPIClient, log, retry }); + }); + + after(async () => { + await deleteInferenceEndpoint({ es, log, inferenceId: CUSTOM_TINY_ELSER_INFERENCE_ID }); + }); + + it('should re-index the KB', async () => { + expect(body.reindex).to.be(true); + expect(body.currentInferenceId).to.be(TINY_ELSER_INFERENCE_ID); + expect(body.nextInferenceId).to.be(CUSTOM_TINY_ELSER_INFERENCE_ID); + await expectWriteIndexName(`${resourceNames.writeIndexAlias.kb}-000002`); + }); + }); - expect(res.status).to.be(500); + describe('running setup for the same inference id', () => { + let body: Awaited>['body']; - // @ts-expect-error - expect(res.body.message).to.include.string( - 'No known trained model with model_id [pt_tiny_elser]' - ); + before(async () => { + await deployTinyElserAndSetupKb(getService); + const res = await setupKbAsAdmin(TINY_ELSER_INFERENCE_ID); + body = res.body; + }); - // @ts-expect-error - expect(res.body.statusCode).to.be(500); + it('does not re-index', async () => { + expect(body.reindex).to.be(false); + expect(body.currentInferenceId).to.be(TINY_ELSER_INFERENCE_ID); + expect(body.nextInferenceId).to.be(TINY_ELSER_INFERENCE_ID); + await expectWriteIndexName(`${resourceNames.writeIndexAlias.kb}-000001`); + }); + }); }); describe('security roles and access privileges', () => { it('should deny access for users without the ai_assistant privilege', async () => { - const { status } = await observabilityAIAssistantAPIClient.viewer({ - endpoint: 'POST /internal/observability_ai_assistant/kb/setup', - params: { - query: { - model_id: TINY_ELSER.id, - }, - }, - }); + const { status } = await setupKbAsViewer(TINY_ELSER_INFERENCE_ID); expect(status).to.be(403); }); }); }); + + async function expectWriteIndexName(expectedName: string) { + await retry.try(async () => { + const writeIndex = await getConcreteWriteIndexFromAlias(es); + expect(writeIndex).to.be(expectedName); + }); + } + + function setupKbAsAdmin(inferenceId: string) { + return observabilityAIAssistantAPIClient.admin({ + endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { inference_id: inferenceId }, + }, + }); + } + + function setupKbAsViewer(inferenceId: string) { + return observabilityAIAssistantAPIClient.viewer({ + endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { inference_id: inferenceId }, + }, + }); + } } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_status.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_status.spec.ts index 3b2932e8ca8fe..16d1d9eb2a189 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_status.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_status.spec.ts @@ -9,23 +9,26 @@ import expect from '@kbn/expect'; import { KnowledgeBaseState } from '@kbn/observability-ai-assistant-plugin/common'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { - deleteKnowledgeBaseModel, - TINY_ELSER, + deleteTinyElserModelAndInferenceEndpoint, deleteInferenceEndpoint, - setupKnowledgeBase, + deployTinyElserAndSetupKb, + TINY_ELSER_MODEL_ID, + TINY_ELSER_INFERENCE_ID, + deleteTinyElserModel, } from '../utils/knowledge_base'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const es = getService('es'); + const log = getService('log'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantApi'); describe('/internal/observability_ai_assistant/kb/status', function () { beforeEach(async () => { - await setupKnowledgeBase(getService); + await deployTinyElserAndSetupKb(getService); }); afterEach(async () => { - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); }); it('returns correct status after knowledge base is setup', async () => { @@ -37,11 +40,11 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon expect(res.body.kbState).to.be(KnowledgeBaseState.READY); expect(res.body.enabled).to.be(true); - expect(res.body.endpoint?.service_settings?.model_id).to.eql(TINY_ELSER.id); + expect(res.body.endpoint?.service_settings?.model_id).to.eql(TINY_ELSER_MODEL_ID); }); it('returns correct status after model is deleted', async () => { - await deleteKnowledgeBaseModel(getService, { shouldDeleteInferenceEndpoint: false }); + await deleteTinyElserModel(getService); const res = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/status', @@ -57,7 +60,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }); it('returns correct status after inference endpoint is deleted', async () => { - await deleteInferenceEndpoint({ es }); + await deleteInferenceEndpoint({ es, log, inferenceId: TINY_ELSER_INFERENCE_ID }); const res = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/status', @@ -68,7 +71,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon expect(res.body.kbState).to.not.be(KnowledgeBaseState.READY); expect(res.body.enabled).to.be(true); expect(res.body.errorMessage).to.include.string( - 'Inference endpoint not found [obs_ai_assistant_kb_inference]' + 'Inference endpoint not found [pt_tiny_elser_inference_id]' ); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_user_instructions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_user_instructions.spec.ts index a88373ebcd42a..ef8bff73abeb7 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_user_instructions.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base_user_instructions.spec.ts @@ -14,8 +14,8 @@ import pRetry from 'p-retry'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { clearKnowledgeBase, - deleteKnowledgeBaseModel, - setupKnowledgeBase, + deleteTinyElserModelAndInferenceEndpoint, + deployTinyElserAndSetupKb, } from '../utils/knowledge_base'; import { LlmProxy, @@ -33,11 +33,11 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('Knowledge base user instructions', function () { before(async () => { - await setupKnowledgeBase(getService); + await deployTinyElserAndSetupKb(getService); }); after(async () => { - await deleteKnowledgeBaseModel(getService); + await deleteTinyElserModelAndInferenceEndpoint(getService); await clearKnowledgeBase(es); await clearConversations(es); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/index_assets.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/index_assets.ts index 5facf5a62f325..b322d10d3917d 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/index_assets.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/index_assets.ts @@ -9,29 +9,29 @@ import expect from '@kbn/expect'; import { Client } from '@elastic/elasticsearch'; import { resourceNames } from '@kbn/observability-ai-assistant-plugin/server/service'; import type { ObservabilityAIAssistantApiClient } from '../../../../services/observability_ai_assistant_api'; +import { TINY_ELSER_INFERENCE_ID } from './knowledge_base'; export async function createOrUpdateIndexAssets( observabilityAIAssistantAPIClient: ObservabilityAIAssistantApiClient ) { const { status } = await observabilityAIAssistantAPIClient.editor({ endpoint: 'POST /internal/observability_ai_assistant/index_assets', + params: { + query: { + inference_id: TINY_ELSER_INFERENCE_ID, + }, + }, }); expect(status).to.be(200); } -async function deleteWriteIndices(es: Client) { +export async function deleteIndexAssets(es: Client) { + // delete write indices const response = await es.indices.get({ index: Object.values(resourceNames.indexPatterns) }); const indicesToDelete = Object.keys(response); if (indicesToDelete.length > 0) { await es.indices.delete({ index: indicesToDelete, ignore_unavailable: true }); } -} - -export async function restoreIndexAssets( - observabilityAIAssistantAPIClient: ObservabilityAIAssistantApiClient, - es: Client -) { - await deleteWriteIndices(es); // delete index templates await es.indices.deleteIndexTemplate( @@ -44,7 +44,12 @@ export async function restoreIndexAssets( { name: Object.values(resourceNames.componentTemplate) }, { ignore: [404] } ); +} - // create index assets from scratch +export async function restoreIndexAssets( + observabilityAIAssistantAPIClient: ObservabilityAIAssistantApiClient, + es: Client +) { + await deleteIndexAssets(es); await createOrUpdateIndexAssets(observabilityAIAssistantAPIClient); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/logger.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/kibana_mocks.ts similarity index 51% rename from x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/logger.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/kibana_mocks.ts index 7248d279bd492..0968b5429fed6 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/logger.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/kibana_mocks.ts @@ -5,7 +5,11 @@ * 2.0. */ +import { Client } from '@elastic/elasticsearch'; +import { CoreSetup } from '@kbn/core/server'; import { Logger } from '@kbn/logging'; +import { ObservabilityAIAssistantConfig } from '@kbn/observability-ai-assistant-plugin/server/config'; +import { ObservabilityAIAssistantPluginStartDependencies } from '@kbn/observability-ai-assistant-plugin/server/types'; import { ToolingLog } from '@kbn/tooling-log'; export function getLoggerMock(toolingLog: ToolingLog) { @@ -16,5 +20,19 @@ export function getLoggerMock(toolingLog: ToolingLog) { warn: (...args: any[]) => toolingLog.warning(...args), fatal: (...args: any[]) => toolingLog.warning(...args), trace: (...args: any[]) => toolingLog.debug(...args), + get: () => getLoggerMock(toolingLog), } as unknown as Logger; } + +export function getCoreMock(es: Client) { + return { + getStartServices: async () => [{ elasticsearch: { client: { asInternalUser: es } } }], + } as unknown as CoreSetup; +} + +export function getConfigMock(config: Partial) { + return { + enableKnowledgeBase: true, + ...config, + } as ObservabilityAIAssistantConfig; +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/knowledge_base.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/knowledge_base.ts index 37ed6179d9d6e..ce4e01d18a372 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/knowledge_base.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/knowledge_base.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import { Client } from '@elastic/elasticsearch'; -import { AI_ASSISTANT_KB_INFERENCE_ID } from '@kbn/observability-ai-assistant-plugin/server/service/inference_endpoint'; import { ToolingLog } from '@kbn/tooling-log'; import { RetryService } from '@kbn/ftr-common-functional-services'; import { @@ -15,62 +14,76 @@ import { KnowledgeBaseState, } from '@kbn/observability-ai-assistant-plugin/common/types'; import { resourceNames } from '@kbn/observability-ai-assistant-plugin/server/service'; +import { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types'; import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import type { ObservabilityAIAssistantApiClient } from '../../../../services/observability_ai_assistant_api'; import { MachineLearningProvider } from '../../../../../services/ml'; import { SUPPORTED_TRAINED_MODELS } from '../../../../../../functional/services/ml/api'; import { setAdvancedSettings } from './advanced_settings'; -export const TINY_ELSER = { - ...SUPPORTED_TRAINED_MODELS.TINY_ELSER, - id: SUPPORTED_TRAINED_MODELS.TINY_ELSER.name, -}; +export const LEGACY_INFERENCE_ID = 'obs_ai_assistant_kb_inference'; +export const TINY_ELSER_MODEL_ID = SUPPORTED_TRAINED_MODELS.TINY_ELSER.name; +export const TINY_ELSER_INFERENCE_ID = 'pt_tiny_elser_inference_id'; export async function importTinyElserModel(ml: ReturnType) { const config = { - ...ml.api.getTrainedModelConfig(TINY_ELSER.name), + ...ml.api.getTrainedModelConfig(TINY_ELSER_MODEL_ID), input: { field_names: ['text_field'], }, }; // necessary for MKI, check indices before importing model. compatible with stateful await ml.api.assureMlStatsIndexExists(); - await ml.api.importTrainedModel(TINY_ELSER.name, TINY_ELSER.id, config); + await ml.api.importTrainedModel(TINY_ELSER_MODEL_ID, TINY_ELSER_MODEL_ID, config); } -export async function setupKnowledgeBase( - getService: DeploymentAgnosticFtrProviderContext['getService'], - { - deployModel: deployModel = true, - }: { - deployModel?: boolean; - } = {} +export function createTinyElserInferenceEndpoint(es: Client, log: ToolingLog, inferenceId: string) { + return createInferenceEndpoint({ + es, + log, + modelId: TINY_ELSER_MODEL_ID, + inferenceId, + taskType: 'sparse_embedding', + }); +} + +export async function deployTinyElserAndSetupKb( + getService: DeploymentAgnosticFtrProviderContext['getService'] ) { const log = getService('log'); const ml = getService('ml'); const retry = getService('retry'); + const es = getService('es'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantApi'); - if (deployModel) { - await importTinyElserModel(ml); - } + await importTinyElserModel(ml); + await createTinyElserInferenceEndpoint(es, log, TINY_ELSER_INFERENCE_ID).catch(() => {}); const { status, body } = await observabilityAIAssistantAPIClient.admin({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', params: { - query: { - model_id: TINY_ELSER.id, - }, + query: { inference_id: TINY_ELSER_INFERENCE_ID }, }, }); - if (deployModel) { - await waitForKnowledgeBaseReady({ observabilityAIAssistantAPIClient, log, retry }); - } + await waitForKnowledgeBaseReady({ observabilityAIAssistantAPIClient, log, retry }); return { status, body }; } +export async function reIndexKnowledgeBase( + observabilityAIAssistantAPIClient: ObservabilityAIAssistantApiClient +) { + return observabilityAIAssistantAPIClient.admin({ + endpoint: 'POST /internal/observability_ai_assistant/kb/reindex', + params: { + query: { + inference_id: TINY_ELSER_INFERENCE_ID, + }, + }, + }); +} + export async function waitForKnowledgeBaseReady({ observabilityAIAssistantAPIClient, log, @@ -87,39 +100,40 @@ export async function waitForKnowledgeBaseReady({ }); expect(res.status).to.be(200); expect(res.body.kbState).to.be(KnowledgeBaseState.READY); + log.debug(`Knowledge base is in ready state.`); }); } -export async function deleteKnowledgeBaseModel( - getService: DeploymentAgnosticFtrProviderContext['getService'], - { - shouldDeleteInferenceEndpoint = true, - }: { - shouldDeleteInferenceEndpoint?: boolean; - } = {} +export async function deleteTinyElserModel( + getService: DeploymentAgnosticFtrProviderContext['getService'] ) { const log = getService('log'); const ml = getService('ml'); - const es = getService('es'); try { - await ml.api.stopTrainedModelDeploymentES(TINY_ELSER.id, true); - await ml.api.deleteTrainedModelES(TINY_ELSER.id); + await ml.api.stopTrainedModelDeploymentES(TINY_ELSER_MODEL_ID, true); + await ml.api.deleteTrainedModelES(TINY_ELSER_MODEL_ID); await ml.testResources.cleanMLSavedObjects(); - - if (shouldDeleteInferenceEndpoint) { - await deleteInferenceEndpoint({ es }); - } + log.info(`Knowledge base model deleted.`); } catch (e) { if (e.message.includes('resource_not_found_exception')) { log.debug(`Knowledge base model was already deleted.`); - return; + } else { + log.error(`Could not delete knowledge base model: ${e}`); } - - log.error(`Could not delete knowledge base model: ${e}`); } } +export async function deleteTinyElserModelAndInferenceEndpoint( + getService: DeploymentAgnosticFtrProviderContext['getService'] +) { + const log = getService('log'); + const es = getService('es'); + + await deleteTinyElserModel(getService); + await deleteInferenceEndpoint({ es, log, inferenceId: TINY_ELSER_INFERENCE_ID }); +} + export async function clearKnowledgeBase(es: Client) { return es.deleteByQuery({ index: resourceNames.indexPatterns.kb, @@ -139,12 +153,59 @@ export async function getAllKbEntries(es: Client) { export async function deleteInferenceEndpoint({ es, - name = AI_ASSISTANT_KB_INFERENCE_ID, + log, + inferenceId, +}: { + es: Client; + log: ToolingLog; + inferenceId: string; +}) { + try { + await es.inference.delete({ inference_id: inferenceId, force: true }); + log.info(`Inference endpoint "${inferenceId}" deleted.`); + } catch (e) { + if (e.message.includes('resource_not_found_exception')) { + log.debug(`Inference endpoint "${inferenceId}" was already deleted.`); + } else { + log.error(`Could not delete inference endpoint "${inferenceId}": ${e}`); + } + } +} + +export async function createInferenceEndpoint({ + es, + log, + modelId, + inferenceId, + taskType, }: { es: Client; - name?: string; + log: ToolingLog; + modelId: string; + inferenceId: string; + taskType?: InferenceTaskType; }) { - return es.inference.delete({ inference_id: name, force: true }); + try { + const res = await es.inference.put({ + inference_id: inferenceId, + task_type: taskType, + inference_config: { + service: 'elasticsearch', + service_settings: { + model_id: modelId, + adaptive_allocations: { enabled: true, min_number_of_allocations: 1 }, + num_threads: 1, + }, + task_settings: {}, + }, + }); + + log.info(`Inference endpoint ${inferenceId} created.`); + return res; + } catch (e) { + log.error(`Error creating inference endpoint "${inferenceId}": ${e}`); + throw e; + } } export async function addSampleDocsToInternalKb( @@ -179,7 +240,7 @@ export async function addSampleDocsToCustomIndex( mappings: { properties: { title: { type: 'text' }, - text: { type: 'semantic_text', inference_id: AI_ASSISTANT_KB_INFERENCE_ID }, + text: { type: 'semantic_text', inference_id: TINY_ELSER_INFERENCE_ID }, }, }, }); @@ -222,9 +283,9 @@ export async function deleteKbIndices(es: Client) { } export async function getConcreteWriteIndexFromAlias(es: Client) { - const response = await es.indices.getAlias({ index: resourceNames.aliases.kb }); + const response = await es.indices.getAlias({ index: resourceNames.writeIndexAlias.kb }); return Object.entries(response).find( - ([index, aliasInfo]) => aliasInfo.aliases[resourceNames.aliases.kb]?.is_write_index + ([index, aliasInfo]) => aliasInfo.aliases[resourceNames.writeIndexAlias.kb]?.is_write_index )?.[0]; } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/tasks.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/tasks.ts new file mode 100644 index 0000000000000..17dfd6dc37457 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/tasks.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Client } from '@elastic/elasticsearch'; +import { getActiveReindexingTaskId } from '@kbn/observability-ai-assistant-plugin/server/service/knowledge_base_service/reindex_knowledge_base'; +import pRetry from 'p-retry'; + +export async function waitForIndexTaskToComplete(es: Client) { + await pRetry( + async () => { + const taskId = await getActiveReindexingTaskId({ asInternalUser: es }); + if (!taskId) { + throw new Error('Waiting for reindexing task to start'); + } + }, + { retries: 50, factor: 1, minTimeout: 500 } + ); + + await pRetry( + async () => { + const taskId = await getActiveReindexingTaskId({ asInternalUser: es }); + if (taskId) { + throw new Error('Waiting for reindexing task to complete'); + } + }, + { retries: 10, factor: 1, minTimeout: 500 } + ); +} diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts deleted file mode 100644 index 2d7acb7fd485e..0000000000000 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts +++ /dev/null @@ -1,55 +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 { Client } from '@elastic/elasticsearch'; -import { AI_ASSISTANT_KB_INFERENCE_ID } from '@kbn/observability-ai-assistant-plugin/server/service/inference_endpoint'; -import { MachineLearningProvider } from '../../../api_integration/services/ml'; -import { SUPPORTED_TRAINED_MODELS } from '../../../functional/services/ml/api'; - -export const TINY_ELSER = { - ...SUPPORTED_TRAINED_MODELS.TINY_ELSER, - id: SUPPORTED_TRAINED_MODELS.TINY_ELSER.name, -}; - -export async function importTinyElserModel(ml: ReturnType) { - const config = { - ...ml.api.getTrainedModelConfig(TINY_ELSER.name), - input: { - field_names: ['text_field'], - }, - }; - // necessary for MKI, check indices before importing model. compatible with stateful - await ml.api.assureMlStatsIndexExists(); - await ml.api.importTrainedModel(TINY_ELSER.name, TINY_ELSER.id, config); -} - -export async function deleteKnowledgeBaseModel(ml: ReturnType) { - await ml.api.stopTrainedModelDeploymentES(TINY_ELSER.id, true); - await ml.api.deleteTrainedModelES(TINY_ELSER.id); - await ml.testResources.cleanMLSavedObjects(); -} - -export async function clearKnowledgeBase(es: Client) { - const KB_INDEX = '.kibana-observability-ai-assistant-kb-*'; - - return es.deleteByQuery({ - index: KB_INDEX, - conflicts: 'proceed', - query: { match_all: {} }, - refresh: true, - }); -} - -export async function deleteInferenceEndpoint({ - es, - name = AI_ASSISTANT_KB_INFERENCE_ID, -}: { - es: Client; - name?: string; -}) { - return es.inference.delete({ inference_id: name, force: true }); -} diff --git a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts index 5ca02f8607335..55d09c183f22c 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts @@ -16,7 +16,6 @@ import { createLlmProxy, LlmProxy, } from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; -import { interceptRequest } from '../../common/intercept_request'; import { FtrProviderContext } from '../../ftr_provider_context'; import { editor } from '../../../observability_ai_assistant_api_integration/common/users/users'; @@ -33,13 +32,8 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte const retry = getService('retry'); const log = getService('log'); const telemetry = getService('kibana_ebt_ui'); - - const driver = getService('__webdriver__'); - const toasts = getService('toasts'); - const { header } = getPageObjects(['header', 'security']); - const flyoutService = getService('flyout'); async function login(username: string, password: string | undefined) { @@ -166,18 +160,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte ); await testSubjects.setValue(ui.pages.createConnectorFlyout.apiKeyInput, 'myApiKey'); - // intercept the request to set up the knowledge base, - // so we don't have to wait until it's fully downloaded - await interceptRequest( - driver.driver, - '*kb\\/setup*', - (responseFactory) => { - return responseFactory.fail(); - }, - async () => { - await testSubjects.clickWhenNotDisabled(ui.pages.createConnectorFlyout.saveButton); - } - ); + await testSubjects.clickWhenNotDisabled(ui.pages.createConnectorFlyout.saveButton); await retry.waitFor('Connector created toast', async () => { const count = await toasts.getCount(); diff --git a/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts index 87ada1e65a754..61e478be86904 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts @@ -8,12 +8,10 @@ import expect from '@kbn/expect'; import { subj as testSubjSelector } from '@kbn/test-subj-selector'; import { - TINY_ELSER, clearKnowledgeBase, - importTinyElserModel, - deleteInferenceEndpoint, - deleteKnowledgeBaseModel, -} from '../../../observability_ai_assistant_api_integration/tests/knowledge_base/helpers'; + deleteTinyElserModelAndInferenceEndpoint, + deployTinyElserAndSetupKb, +} from '../../../api_integration/deployment_agnostic/apis/observability/ai_assistant/utils/knowledge_base'; import { ObservabilityAIAssistantApiClient } from '../../../observability_ai_assistant_api_integration/common/observability_ai_assistant_api_client'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -22,7 +20,6 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte const ui = getService('observabilityAIAssistantUI'); const testSubjects = getService('testSubjects'); const log = getService('log'); - const ml = getService('ml'); const es = getService('es'); const { common } = getPageObjects(['common']); @@ -51,32 +48,13 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte describe('Knowledge management tab', () => { before(async () => { await clearKnowledgeBase(es); - - // create a knowledge base model - await importTinyElserModel(ml); - - await Promise.all([ - // setup the knowledge base - observabilityAIAssistantAPIClient - .admin({ - endpoint: 'POST /internal/observability_ai_assistant/kb/setup', - params: { - query: { - model_id: TINY_ELSER.id, - }, - }, - }) - .expect(200), - - // login as editor - ui.auth.login('editor'), - ]); + await deployTinyElserAndSetupKb(getService); + await ui.auth.login('editor'); }); after(async () => { await Promise.all([ - deleteKnowledgeBaseModel(ml), - deleteInferenceEndpoint({ es }), + deleteTinyElserModelAndInferenceEndpoint(getService), clearKnowledgeBase(es), ui.auth.logout(), ]);