[Security Solution][Elastic AI Assistant] Refactors Knowledge Base feature flag to UI feature toggle (#167935)

## Summary

This PR refactors the `assistantLangChain` code feature flag introduced
in https://github.com/elastic/kibana/pull/164908, to be a UI feature
toggle that users can enable/disable via the `Knowledge Base` assistant
advanced settings.


Left image shows the feature disabled, and the right image shows the
feature partly enabled. If ELSER is configured, the UI will attempt to
install all resources automatically for a one-click UX, however if ELSER
is not configured, or there are failures, the user can manually enable
the Knowledge Base or ES|QL base documentation:

<p align="center">
<img width="400"
src="be85522e-b2f5-4a39-9f0e-d359424caf37"
/> <img width="400"
src="d901c4f8-2184-4fb7-8c59-f2ff877118b9"
/>
</p> 




Also, since this code feature flag was shared with the model evaluator
experimental feature, a `modelEvaluatorEnabled` flag has been plumbed to
fully decouple the two settings. Now _only the_ model evaluator is
enabled when setting security Solution Advanced setting:

```
xpack.securitySolution.enableExperimental: ['assistantModelEvaluation']
```

and the previous `assistantLangChain` code feature flag is now enabled
by simply toggling on the Knowledge Base in the settings shown above.

> [!NOTE]
> Even if ELSER isn't configured, and the knowledge base/docs aren't
setup, if the Knowledge Base is enabled, the LangChain code path will
still be enabled as intended, but we can change this behavior if testing
shows this is not ideal.


### Checklist

Delete any items that are not applicable to this PR.

- [X] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)added
to match the most common scenarios
- [X] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
This commit is contained in:
Garrett Spong 2023-10-03 17:53:46 -06:00 committed by GitHub
parent ce5ae7d562
commit e74e5e2bfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 487 additions and 309 deletions

View file

@ -1,193 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useMemo, useState } from 'react';
import {
EuiFormRow,
EuiTitle,
EuiText,
EuiTextColor,
EuiHorizontalRule,
EuiLoadingSpinner,
EuiSpacer,
EuiSwitch,
EuiToolTip,
EuiSwitchEvent,
EuiLink,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import * as i18n from './translations';
import { useKnowledgeBaseStatus } from '../../../knowledge_base/use_knowledge_base_status/use_knowledge_base_status';
import { useAssistantContext } from '../../../assistant_context';
import { useSetupKnowledgeBase } from '../../../knowledge_base/use_setup_knowledge_base/use_setup_knowledge_base';
import { useDeleteKnowledgeBase } from '../../../knowledge_base/use_delete_knowledge_base/use_delete_knowledge_base';
const ESQL_RESOURCE = 'esql';
interface Props {
onAdvancedSettingsChange?: () => void;
}
/**
* Advanced Settings -- enable and disable LangChain integration, Knowledge Base, and ESQL KB Documents
*/
export const AdvancedSettings: React.FC<Props> = React.memo(({ onAdvancedSettingsChange }) => {
const { http, assistantLangChain } = useAssistantContext();
const {
data: kbStatus,
isLoading,
isFetching,
} = useKnowledgeBaseStatus({ http, resource: ESQL_RESOURCE });
const { mutate: setupKB, isLoading: isSettingUpKB } = useSetupKnowledgeBase({ http });
const { mutate: deleteKB, isLoading: isDeletingUpKB } = useDeleteKnowledgeBase({ http });
const [isLangChainEnabled, setIsLangChainEnabled] = useState(assistantLangChain);
const isKnowledgeBaseEnabled =
(kbStatus?.index_exists && kbStatus?.pipeline_exists && kbStatus?.elser_exists) ?? false;
const isESQLEnabled = kbStatus?.esql_exists ?? false;
const isLoadingKb = isLoading || isFetching || isSettingUpKB || isDeletingUpKB;
const isKnowledgeBaseAvailable = isLangChainEnabled && kbStatus?.elser_exists;
const isESQLAvailable = isLangChainEnabled && isKnowledgeBaseAvailable && isKnowledgeBaseEnabled;
const onEnableKnowledgeBaseChange = useCallback(
(event: EuiSwitchEvent) => {
if (event.target.checked) {
setupKB();
} else {
deleteKB();
}
},
[deleteKB, setupKB]
);
const onEnableESQLChange = useCallback(
(event: EuiSwitchEvent) => {
if (event.target.checked) {
setupKB(ESQL_RESOURCE);
} else {
deleteKB(ESQL_RESOURCE);
}
},
[deleteKB, setupKB]
);
const langchainSwitch = useMemo(() => {
return (
<EuiSwitch
checked={isLangChainEnabled}
compressed
disabled={true} // Advanced settings only shown if assistantLangChain=true, remove when storing to localstorage as ui feature toggle
label={i18n.LANNGCHAIN_LABEL}
onChange={() => setIsLangChainEnabled(!isLangChainEnabled)}
showLabel={false}
/>
);
}, [isLangChainEnabled]);
const knowledgeBaseSwitch = useMemo(() => {
return isLoadingKb ? (
<EuiLoadingSpinner size="s" />
) : (
<EuiToolTip
position={'right'}
content={!isKnowledgeBaseAvailable ? i18n.KNOWLEDGE_BASE_LABEL_TOOLTIP : undefined}
>
<EuiSwitch
showLabel={false}
label={i18n.KNOWLEDGE_BASE_LABEL}
checked={isKnowledgeBaseEnabled}
disabled={!isKnowledgeBaseAvailable}
onChange={onEnableKnowledgeBaseChange}
compressed
/>
</EuiToolTip>
);
}, [isLoadingKb, isKnowledgeBaseAvailable, isKnowledgeBaseEnabled, onEnableKnowledgeBaseChange]);
const esqlSwitch = useMemo(() => {
return isLoadingKb ? (
<EuiLoadingSpinner size="s" />
) : (
<EuiToolTip
position={'right'}
content={!isESQLAvailable ? i18n.ESQL_LABEL_TOOLTIP : undefined}
>
<EuiSwitch
showLabel={false}
label={i18n.ESQL_LABEL}
checked={isESQLEnabled}
disabled={!isESQLAvailable}
onChange={onEnableESQLChange}
compressed
/>
</EuiToolTip>
);
}, [isLoadingKb, isESQLAvailable, isESQLEnabled, onEnableESQLChange]);
return (
<>
<EuiTitle size={'s'}>
<h2>{i18n.SETTINGS_TITLE}</h2>
</EuiTitle>
<EuiSpacer size="xs" />
<EuiText size={'s'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
<EuiHorizontalRule margin={'s'} />
<EuiFormRow display="columnCompressedSwitch" label={i18n.LANNGCHAIN_LABEL}>
{langchainSwitch}
</EuiFormRow>
<EuiSpacer size="xs" />
<EuiTextColor color={'subdued'}>{i18n.LANNGCHAIN_DESCRIPTION}</EuiTextColor>
<EuiSpacer size="m" />
<EuiFormRow
display="columnCompressedSwitch"
label={i18n.KNOWLEDGE_BASE_LABEL}
isDisabled={!isKnowledgeBaseAvailable}
>
{knowledgeBaseSwitch}
</EuiFormRow>
<EuiSpacer size="xs" />
<EuiTextColor color={'subdued'}>
<FormattedMessage
defaultMessage="Initializes a local knowledge base for saving and retrieving relevant context for your conversations. Note: ELSER must be configured and started. {seeDocs}"
id="xpack.elasticAssistant.assistant.settings.advancedSettings.knowledgeBaseDescription"
values={{
seeDocs: (
<EuiLink
external
href={
'https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html#download-deploy-elser'
}
target="_blank"
>
{i18n.KNOWLEDGE_BASE_DESCRIPTION_ELSER_LEARN_MORE}
</EuiLink>
),
}}
/>
</EuiTextColor>
<EuiSpacer size="m" />
<EuiFormRow
isDisabled={!isESQLAvailable}
display="columnCompressedSwitch"
label={i18n.ESQL_LABEL}
>
{esqlSwitch}
</EuiFormRow>
<EuiSpacer size="xs" />
<EuiTextColor color={'subdued'}>{i18n.ESQL_DESCRIPTION}</EuiTextColor>
<EuiSpacer size="m" />
</>
);
});
AdvancedSettings.displayName = 'AdvancedSettings';

View file

@ -1,79 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const SETTINGS_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.advancedSettings.settingsTitle',
{
defaultMessage: 'Advanced Settings',
}
);
export const SETTINGS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.advancedSettings.settingsDescription',
{
defaultMessage: 'Additional knobs and dials for the Elastic AI Assistant.',
}
);
export const LANNGCHAIN_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.advancedSettings.langChainLabel',
{
defaultMessage: 'Experimental LangChain Integration',
}
);
export const LANNGCHAIN_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.advancedSettings.langChainDescription',
{
defaultMessage:
'Enables advanced features and workflows like the Knowledge Base, Functions, Memories, and advanced agent and chain configurations. ',
}
);
export const KNOWLEDGE_BASE_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.advancedSettings.knowledgeBaseLabel',
{
defaultMessage: 'Knowledge Base',
}
);
export const KNOWLEDGE_BASE_LABEL_TOOLTIP = i18n.translate(
'xpack.elasticAssistant.assistant.settings.advancedSettings.knowledgeBaseLabelTooltip',
{
defaultMessage: 'Requires ELSER to be configured and started.',
}
);
export const KNOWLEDGE_BASE_DESCRIPTION_ELSER_LEARN_MORE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.advancedSettings.knowledgeBaseElserLearnMoreDescription',
{
defaultMessage: 'Learn more.',
}
);
export const ESQL_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.advancedSettings.esqlLabel',
{
defaultMessage: 'ES|QL Knowledge Base Documents',
}
);
export const ESQL_LABEL_TOOLTIP = i18n.translate(
'xpack.elasticAssistant.assistant.settings.advancedSettings.esqlTooltip',
{
defaultMessage: 'Requires `Knowledge Base` to be enabled.',
}
);
export const ESQL_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.advancedSettings.esqlDescription',
{
defaultMessage:
'Loads ES|QL documentation and language files into the Knowledge Base for use in generating ES|QL queries.',
}
);

View file

@ -30,7 +30,7 @@ import { useAssistantContext } from '../../assistant_context';
import { AnonymizationSettings } from '../../data_anonymization/settings/anonymization_settings';
import { QuickPromptSettings } from '../quick_prompts/quick_prompt_settings/quick_prompt_settings';
import { SystemPromptSettings } from '../prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings';
import { AdvancedSettings } from './advanced_settings/advanced_settings';
import { KnowledgeBaseSettings } from '../../knowledge_base/knowledge_base_settings/knowledge_base_settings';
import { ConversationSettings } from '../conversations/conversation_settings/conversation_settings';
import { TEST_IDS } from '../constants';
import { useSettingsUpdater } from './use_settings_updater/use_settings_updater';
@ -45,7 +45,7 @@ export const CONVERSATIONS_TAB = 'CONVERSATION_TAB' as const;
export const QUICK_PROMPTS_TAB = 'QUICK_PROMPTS_TAB' as const;
export const SYSTEM_PROMPTS_TAB = 'SYSTEM_PROMPTS_TAB' as const;
export const ANONYMIZATION_TAB = 'ANONYMIZATION_TAB' as const;
export const ADVANCED_TAB = 'ADVANCED_TAB' as const;
export const KNOWLEDGE_BASE_TAB = 'KNOWLEDGE_BASE_TAB' as const;
export const EVALUATION_TAB = 'EVALUATION_TAB' as const;
export type SettingsTabs =
@ -53,7 +53,7 @@ export type SettingsTabs =
| typeof QUICK_PROMPTS_TAB
| typeof SYSTEM_PROMPTS_TAB
| typeof ANONYMIZATION_TAB
| typeof ADVANCED_TAB
| typeof KNOWLEDGE_BASE_TAB
| typeof EVALUATION_TAB;
interface Props {
defaultConnectorId?: string;
@ -68,7 +68,7 @@ interface Props {
/**
* Modal for overall Assistant Settings, including conversation settings, quick prompts, system prompts,
* anonymization, functions (coming soon!), and advanced settings.
* anonymization, knowledge base, and evaluation via the `isModelEvaluationEnabled` feature flag.
*/
export const AssistantSettings: React.FC<Props> = React.memo(
({
@ -79,17 +79,19 @@ export const AssistantSettings: React.FC<Props> = React.memo(
selectedConversation: defaultSelectedConversation,
setSelectedConversationId,
}) => {
const { assistantLangChain, http, selectedSettingsTab, setSelectedSettingsTab } =
const { modelEvaluatorEnabled, http, selectedSettingsTab, setSelectedSettingsTab } =
useAssistantContext();
const {
conversationSettings,
defaultAllow,
defaultAllowReplacement,
knowledgeBase,
quickPromptSettings,
systemPromptSettings,
setUpdatedConversationSettings,
setUpdatedDefaultAllow,
setUpdatedDefaultAllowReplacement,
setUpdatedKnowledgeBaseSettings,
setUpdatedQuickPromptSettings,
setUpdatedSystemPromptSettings,
saveSettings,
@ -236,17 +238,15 @@ export const AssistantSettings: React.FC<Props> = React.memo(
>
<EuiIcon type="eyeClosed" size="l" />
</EuiKeyPadMenuItem>
{assistantLangChain && (
<EuiKeyPadMenuItem
id={ADVANCED_TAB}
label={i18n.ADVANCED_MENU_ITEM}
isSelected={selectedSettingsTab === ADVANCED_TAB}
onClick={() => setSelectedSettingsTab(ADVANCED_TAB)}
>
<EuiIcon type="advancedSettingsApp" size="l" />
</EuiKeyPadMenuItem>
)}
{assistantLangChain && (
<EuiKeyPadMenuItem
id={KNOWLEDGE_BASE_TAB}
label={i18n.KNOWLEDGE_BASE_MENU_ITEM}
isSelected={selectedSettingsTab === KNOWLEDGE_BASE_TAB}
onClick={() => setSelectedSettingsTab(KNOWLEDGE_BASE_TAB)}
>
<EuiIcon type="notebookApp" size="l" />
</EuiKeyPadMenuItem>
{modelEvaluatorEnabled && (
<EuiKeyPadMenuItem
id={EVALUATION_TAB}
label={i18n.EVALUATION_MENU_ITEM}
@ -307,7 +307,12 @@ export const AssistantSettings: React.FC<Props> = React.memo(
setUpdatedDefaultAllowReplacement={setUpdatedDefaultAllowReplacement}
/>
)}
{selectedSettingsTab === ADVANCED_TAB && <AdvancedSettings />}
{selectedSettingsTab === KNOWLEDGE_BASE_TAB && (
<KnowledgeBaseSettings
knowledgeBase={knowledgeBase}
setUpdatedKnowledgeBaseSettings={setUpdatedKnowledgeBaseSettings}
/>
)}
{selectedSettingsTab === EVALUATION_TAB && <EvaluationSettings />}
</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner

View file

@ -49,10 +49,10 @@ export const ANONYMIZATION_MENU_ITEM = i18n.translate(
}
);
export const ADVANCED_MENU_ITEM = i18n.translate(
'xpack.elasticAssistant.assistant.settings.settingsAdvancedMenuItemTitle',
export const KNOWLEDGE_BASE_MENU_ITEM = i18n.translate(
'xpack.elasticAssistant.assistant.settings.settingsKnowledgeBaseMenuItemTitle',
{
defaultMessage: 'Advanced',
defaultMessage: 'Knowledge Base',
}
);

View file

@ -8,11 +8,13 @@
import React, { useCallback, useState } from 'react';
import { Prompt, QuickPrompt } from '../../../..';
import { UseAssistantContext, useAssistantContext } from '../../../assistant_context';
import type { KnowledgeBaseConfig } from '../../types';
interface UseSettingsUpdater {
conversationSettings: UseAssistantContext['conversations'];
defaultAllow: string[];
defaultAllowReplacement: string[];
knowledgeBase: KnowledgeBaseConfig;
quickPromptSettings: QuickPrompt[];
resetSettings: () => void;
systemPromptSettings: Prompt[];
@ -21,6 +23,7 @@ interface UseSettingsUpdater {
setUpdatedConversationSettings: React.Dispatch<
React.SetStateAction<UseAssistantContext['conversations']>
>;
setUpdatedKnowledgeBaseSettings: React.Dispatch<React.SetStateAction<KnowledgeBaseConfig>>;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<QuickPrompt[]>>;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
saveSettings: () => void;
@ -34,11 +37,13 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
conversations,
defaultAllow,
defaultAllowReplacement,
knowledgeBase,
setAllQuickPrompts,
setAllSystemPrompts,
setConversations,
setDefaultAllow,
setDefaultAllowReplacement,
setKnowledgeBase,
} = useAssistantContext();
/**
@ -57,6 +62,9 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
const [updatedDefaultAllow, setUpdatedDefaultAllow] = useState<string[]>(defaultAllow);
const [updatedDefaultAllowReplacement, setUpdatedDefaultAllowReplacement] =
useState<string[]>(defaultAllowReplacement);
// Knowledge Base
const [updatedKnowledgeBaseSettings, setUpdatedKnowledgeBaseSettings] =
useState<KnowledgeBaseConfig>(knowledgeBase);
/**
* Reset all pending settings
@ -64,10 +72,18 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
const resetSettings = useCallback((): void => {
setUpdatedConversationSettings(conversations);
setUpdatedQuickPromptSettings(allQuickPrompts);
setUpdatedKnowledgeBaseSettings(knowledgeBase);
setUpdatedSystemPromptSettings(allSystemPrompts);
setUpdatedDefaultAllow(defaultAllow);
setUpdatedDefaultAllowReplacement(defaultAllowReplacement);
}, [allQuickPrompts, allSystemPrompts, conversations, defaultAllow, defaultAllowReplacement]);
}, [
allQuickPrompts,
allSystemPrompts,
conversations,
defaultAllow,
defaultAllowReplacement,
knowledgeBase,
]);
/**
* Save all pending settings
@ -76,6 +92,7 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
setAllQuickPrompts(updatedQuickPromptSettings);
setAllSystemPrompts(updatedSystemPromptSettings);
setConversations(updatedConversationSettings);
setKnowledgeBase(updatedKnowledgeBaseSettings);
setDefaultAllow(updatedDefaultAllow);
setDefaultAllowReplacement(updatedDefaultAllowReplacement);
}, [
@ -84,9 +101,11 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
setConversations,
setDefaultAllow,
setDefaultAllowReplacement,
setKnowledgeBase,
updatedConversationSettings,
updatedDefaultAllow,
updatedDefaultAllowReplacement,
updatedKnowledgeBaseSettings,
updatedQuickPromptSettings,
updatedSystemPromptSettings,
]);
@ -95,6 +114,7 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
conversationSettings: updatedConversationSettings,
defaultAllow: updatedDefaultAllow,
defaultAllowReplacement: updatedDefaultAllowReplacement,
knowledgeBase: updatedKnowledgeBaseSettings,
quickPromptSettings: updatedQuickPromptSettings,
resetSettings,
systemPromptSettings: updatedSystemPromptSettings,
@ -102,6 +122,7 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
setUpdatedDefaultAllow,
setUpdatedDefaultAllowReplacement,
setUpdatedConversationSettings,
setUpdatedKnowledgeBaseSettings,
setUpdatedQuickPromptSettings,
setUpdatedSystemPromptSettings,
};

View file

@ -15,3 +15,7 @@ export interface Prompt {
isDefault?: boolean; // TODO: Should be renamed to isImmutable as this flag is used to prevent users from deleting prompts
isNewConversationDefault?: boolean;
}
export interface KnowledgeBaseConfig {
assistantLangChain: boolean;
}

View file

@ -29,7 +29,7 @@ interface UseSendMessages {
}
export const useSendMessages = (): UseSendMessages => {
const { assistantLangChain } = useAssistantContext();
const { knowledgeBase } = useAssistantContext();
const [isLoading, setIsLoading] = useState(false);
const sendMessages = useCallback(
@ -37,7 +37,7 @@ export const useSendMessages = (): UseSendMessages => {
setIsLoading(true);
try {
return await fetchConnectorExecuteAction({
assistantLangChain,
assistantLangChain: knowledgeBase.assistantLangChain,
http,
messages,
apiConfig,
@ -46,7 +46,7 @@ export const useSendMessages = (): UseSendMessages => {
setIsLoading(false);
}
},
[assistantLangChain]
[knowledgeBase.assistantLangChain]
);
return { isLoading, sendMessages };

View file

@ -5,7 +5,14 @@
* 2.0.
*/
import { KnowledgeBaseConfig } from '../assistant/types';
export const DEFAULT_ASSISTANT_NAMESPACE = 'elasticAssistantDefault';
export const QUICK_PROMPT_LOCAL_STORAGE_KEY = 'quickPrompts';
export const SYSTEM_PROMPT_LOCAL_STORAGE_KEY = 'systemPrompts';
export const LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY = 'lastConversationId';
export const KNOWLEDGE_BASE_LOCAL_STORAGE_KEY = 'knowledgeBase';
export const DEFAULT_KNOWLEDGE_BASE_SETTINGS: KnowledgeBaseConfig = {
assistantLangChain: false,
};

View file

@ -28,7 +28,6 @@ const ContextWrapper: React.FC = ({ children }) => (
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
assistantLangChain={false}
augmentMessageCodeBlocks={jest.fn()}
baseAllow={[]}
baseAllowReplacement={[]}

View file

@ -24,10 +24,12 @@ import { DEFAULT_ASSISTANT_TITLE } from '../assistant/translations';
import { CodeBlockDetails } from '../assistant/use_conversation/helpers';
import { PromptContextTemplate } from '../assistant/prompt_context/types';
import { QuickPrompt } from '../assistant/quick_prompts/types';
import { Prompt } from '../assistant/types';
import type { KnowledgeBaseConfig, Prompt } from '../assistant/types';
import { BASE_SYSTEM_PROMPTS } from '../content/prompts/system';
import {
DEFAULT_ASSISTANT_NAMESPACE,
DEFAULT_KNOWLEDGE_BASE_SETTINGS,
KNOWLEDGE_BASE_LOCAL_STORAGE_KEY,
LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY,
QUICK_PROMPT_LOCAL_STORAGE_KEY,
SYSTEM_PROMPT_LOCAL_STORAGE_KEY,
@ -49,7 +51,6 @@ type ShowAssistantOverlay = ({
export interface AssistantProviderProps {
actionTypeRegistry: ActionTypeRegistryContract;
assistantAvailability: AssistantAvailability;
assistantLangChain: boolean;
assistantTelemetry?: AssistantTelemetry;
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
baseAllow: string[];
@ -73,6 +74,7 @@ export interface AssistantProviderProps {
}) => EuiCommentProps[];
http: HttpSetup;
getInitialConversations: () => Record<string, Conversation>;
modelEvaluatorEnabled?: boolean;
nameSpace?: string;
setConversations: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
setDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
@ -87,7 +89,6 @@ export interface UseAssistantContext {
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
allQuickPrompts: QuickPrompt[];
allSystemPrompts: Prompt[];
assistantLangChain: boolean;
baseAllow: string[];
baseAllowReplacement: string[];
docLinks: Omit<DocLinksStart, 'links'>;
@ -110,8 +111,10 @@ export interface UseAssistantContext {
showAnonymizedValues: boolean;
}) => EuiCommentProps[];
http: HttpSetup;
knowledgeBase: KnowledgeBaseConfig;
localStorageLastConversationId: string | undefined;
promptContexts: Record<string, PromptContext>;
modelEvaluatorEnabled: boolean;
nameSpace: string;
registerPromptContext: RegisterPromptContext;
selectedSettingsTab: SettingsTabs;
@ -120,6 +123,7 @@ export interface UseAssistantContext {
setConversations: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
setDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
setDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
setKnowledgeBase: React.Dispatch<React.SetStateAction<KnowledgeBaseConfig | undefined>>;
setLastConversationId: React.Dispatch<React.SetStateAction<string | undefined>>;
setSelectedSettingsTab: React.Dispatch<React.SetStateAction<SettingsTabs>>;
setShowAssistantOverlay: (showAssistantOverlay: ShowAssistantOverlay) => void;
@ -133,7 +137,6 @@ const AssistantContext = React.createContext<UseAssistantContext | undefined>(un
export const AssistantProvider: React.FC<AssistantProviderProps> = ({
actionTypeRegistry,
assistantAvailability,
assistantLangChain,
assistantTelemetry,
augmentMessageCodeBlocks,
baseAllow,
@ -149,6 +152,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
getComments,
http,
getInitialConversations,
modelEvaluatorEnabled = false,
nameSpace = DEFAULT_ASSISTANT_NAMESPACE,
setConversations,
setDefaultAllow,
@ -174,6 +178,14 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
const [localStorageLastConversationId, setLocalStorageLastConversationId] =
useLocalStorage<string>(`${nameSpace}.${LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY}`);
/**
* Local storage for knowledge base configuration, prefixed by assistant nameSpace
*/
const [localStorageKnowledgeBase, setLocalStorageKnowledgeBase] = useLocalStorage(
`${nameSpace}.${KNOWLEDGE_BASE_LOCAL_STORAGE_KEY}`,
DEFAULT_KNOWLEDGE_BASE_SETTINGS
);
/**
* Prompt contexts are used to provide components a way to register and make their data available to the assistant.
*/
@ -254,7 +266,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
() => ({
actionTypeRegistry,
assistantAvailability,
assistantLangChain,
assistantTelemetry,
augmentMessageCodeBlocks,
allQuickPrompts: localStorageQuickPrompts ?? [],
@ -272,6 +283,8 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
docLinks,
getComments,
http,
knowledgeBase: localStorageKnowledgeBase ?? DEFAULT_KNOWLEDGE_BASE_SETTINGS,
modelEvaluatorEnabled,
promptContexts,
nameSpace,
registerPromptContext,
@ -281,6 +294,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
setConversations: onConversationsUpdated,
setDefaultAllow,
setDefaultAllowReplacement,
setKnowledgeBase: setLocalStorageKnowledgeBase,
setSelectedSettingsTab,
setShowAssistantOverlay,
showAssistantOverlay,
@ -292,7 +306,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
[
actionTypeRegistry,
assistantAvailability,
assistantLangChain,
assistantTelemetry,
augmentMessageCodeBlocks,
baseAllow,
@ -308,9 +321,11 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
docLinks,
getComments,
http,
localStorageKnowledgeBase,
localStorageLastConversationId,
localStorageQuickPrompts,
localStorageSystemPrompts,
modelEvaluatorEnabled,
nameSpace,
onConversationsUpdated,
promptContexts,
@ -318,6 +333,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
selectedSettingsTab,
setDefaultAllow,
setDefaultAllowReplacement,
setLocalStorageKnowledgeBase,
setLocalStorageLastConversationId,
setLocalStorageQuickPrompts,
setLocalStorageSystemPrompts,

View file

@ -0,0 +1,294 @@
/*
* 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, { useCallback, useMemo } from 'react';
import {
EuiFormRow,
EuiTitle,
EuiText,
EuiHorizontalRule,
EuiLoadingSpinner,
EuiSpacer,
EuiSwitchEvent,
EuiLink,
EuiBetaBadge,
EuiFlexGroup,
EuiFlexItem,
EuiHealth,
EuiButtonEmpty,
EuiSwitch,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
import { useDeleteKnowledgeBase } from '../use_delete_knowledge_base/use_delete_knowledge_base';
import { useKnowledgeBaseStatus } from '../use_knowledge_base_status/use_knowledge_base_status';
import { useSetupKnowledgeBase } from '../use_setup_knowledge_base/use_setup_knowledge_base';
import type { KnowledgeBaseConfig } from '../../assistant/types';
const ESQL_RESOURCE = 'esql';
const KNOWLEDGE_BASE_INDEX_PATTERN = '.kibana-elastic-ai-assistant-kb';
interface Props {
knowledgeBase: KnowledgeBaseConfig;
setUpdatedKnowledgeBaseSettings: React.Dispatch<React.SetStateAction<KnowledgeBaseConfig>>;
}
/**
* Knowledge Base Settings -- enable and disable LangChain integration, Knowledge Base, and ESQL KB Documents
*/
export const KnowledgeBaseSettings: React.FC<Props> = React.memo(
({ knowledgeBase, setUpdatedKnowledgeBaseSettings }) => {
const { http } = useAssistantContext();
const {
data: kbStatus,
isLoading,
isFetching,
} = useKnowledgeBaseStatus({ http, resource: ESQL_RESOURCE });
const { mutate: setupKB, isLoading: isSettingUpKB } = useSetupKnowledgeBase({ http });
const { mutate: deleteKB, isLoading: isDeletingUpKB } = useDeleteKnowledgeBase({ http });
// Resource enabled state
const isKnowledgeBaseEnabled =
(kbStatus?.index_exists && kbStatus?.pipeline_exists && kbStatus?.elser_exists) ?? false;
const isESQLEnabled = kbStatus?.esql_exists ?? false;
// Resource availability state
const isLoadingKb = isLoading || isFetching || isSettingUpKB || isDeletingUpKB;
const isKnowledgeBaseAvailable = knowledgeBase.assistantLangChain && kbStatus?.elser_exists;
const isESQLAvailable =
knowledgeBase.assistantLangChain && isKnowledgeBaseAvailable && isKnowledgeBaseEnabled;
// Calculated health state for EuiHealth component
const elserHealth = kbStatus?.elser_exists ? 'success' : 'subdued';
const knowledgeBaseHealth = isKnowledgeBaseEnabled ? 'success' : 'subdued';
const esqlHealth = isESQLEnabled ? 'success' : 'subdued';
//////////////////////////////////////////////////////////////////////////////////////////
// Main `Knowledge Base` switch, which toggles the `assistantLangChain` UI feature toggle
// setting that is saved to localstorage
const onEnableAssistantLangChainChange = useCallback(
(event: EuiSwitchEvent) => {
setUpdatedKnowledgeBaseSettings({
...knowledgeBase,
assistantLangChain: event.target.checked,
});
// If enabling and ELSER exists, try to set up automatically
if (event.target.checked && kbStatus?.elser_exists) {
setupKB(ESQL_RESOURCE);
}
},
[kbStatus?.elser_exists, knowledgeBase, setUpdatedKnowledgeBaseSettings, setupKB]
);
const assistantLangChainSwitch = useMemo(() => {
return isLoadingKb ? (
<EuiLoadingSpinner size="s" />
) : (
<EuiSwitch
showLabel={false}
checked={knowledgeBase.assistantLangChain}
onChange={onEnableAssistantLangChainChange}
label={i18n.KNOWLEDGE_BASE_LABEL}
compressed
/>
);
}, [isLoadingKb, knowledgeBase.assistantLangChain, onEnableAssistantLangChainChange]);
//////////////////////////////////////////////////////////////////////////////////////////
// Knowledge Base Resource
const onEnableKB = useCallback(
(enabled: boolean) => {
if (enabled) {
setupKB();
} else {
deleteKB();
}
},
[deleteKB, setupKB]
);
const knowledgeBaseActionButton = useMemo(() => {
return isLoadingKb || !isKnowledgeBaseAvailable ? (
<></>
) : (
<EuiButtonEmpty
color={isKnowledgeBaseEnabled ? 'danger' : 'primary'}
flush="left"
onClick={() => onEnableKB(!isKnowledgeBaseEnabled)}
size="xs"
>
{isKnowledgeBaseEnabled
? i18n.KNOWLEDGE_BASE_DELETE_BUTTON
: i18n.KNOWLEDGE_BASE_INIT_BUTTON}
</EuiButtonEmpty>
);
}, [isKnowledgeBaseAvailable, isKnowledgeBaseEnabled, isLoadingKb, onEnableKB]);
const knowledgeBaseDescription = useMemo(() => {
return isKnowledgeBaseEnabled ? (
<>
{i18n.KNOWLEDGE_BASE_DESCRIPTION_INSTALLED(KNOWLEDGE_BASE_INDEX_PATTERN)}{' '}
{knowledgeBaseActionButton}
</>
) : (
<>
{i18n.KNOWLEDGE_BASE_DESCRIPTION} {knowledgeBaseActionButton}
</>
);
}, [isKnowledgeBaseEnabled, knowledgeBaseActionButton]);
//////////////////////////////////////////////////////////////////////////////////////////
// ESQL Resource
const onEnableESQL = useCallback(
(enabled: boolean) => {
if (enabled) {
setupKB(ESQL_RESOURCE);
} else {
deleteKB(ESQL_RESOURCE);
}
},
[deleteKB, setupKB]
);
const esqlActionButton = useMemo(() => {
return isLoadingKb || !isESQLAvailable ? (
<></>
) : (
<EuiButtonEmpty
color={isESQLEnabled ? 'danger' : 'primary'}
flush="left"
onClick={() => onEnableESQL(!isESQLEnabled)}
size="xs"
>
{isESQLEnabled ? i18n.KNOWLEDGE_BASE_DELETE_BUTTON : i18n.KNOWLEDGE_BASE_INIT_BUTTON}
</EuiButtonEmpty>
);
}, [isLoadingKb, isESQLAvailable, isESQLEnabled, onEnableESQL]);
const esqlDescription = useMemo(() => {
return isESQLEnabled ? (
<>
{i18n.ESQL_DESCRIPTION_INSTALLED} {esqlActionButton}
</>
) : (
<>
{i18n.ESQL_DESCRIPTION} {esqlActionButton}
</>
);
}, [esqlActionButton, isESQLEnabled]);
return (
<>
<EuiTitle size={'s'}>
<h2>
{i18n.SETTINGS_TITLE}{' '}
<EuiBetaBadge iconType={'beaker'} label={i18n.SETTINGS_BADGE} size="s" color="hollow" />
</h2>
</EuiTitle>
<EuiSpacer size="xs" />
<EuiText size={'s'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
<EuiHorizontalRule margin={'s'} />
<EuiFormRow
display="columnCompressedSwitch"
label={i18n.KNOWLEDGE_BASE_LABEL}
css={css`
div {
min-width: 95px !important;
}
`}
>
{assistantLangChainSwitch}
</EuiFormRow>
<EuiSpacer size="s" />
<EuiFlexGroup
direction={'column'}
gutterSize={'s'}
css={css`
padding-left: 5px;
`}
>
<EuiFlexItem>
<div>
<EuiHealth color={elserHealth}>{i18n.KNOWLEDGE_BASE_ELSER_LABEL}</EuiHealth>
<EuiText
size={'xs'}
color={'subdued'}
css={css`
padding-left: 20px;
`}
>
<FormattedMessage
defaultMessage="Configure ELSER within {machineLearning} to get started. {seeDocs}"
id="xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription"
values={{
machineLearning: (
<EuiLink
external
href={http.basePath.prepend('/app/ml/trained_models')}
target="_blank"
>
{i18n.KNOWLEDGE_BASE_ELSER_MACHINE_LEARNING}
</EuiLink>
),
seeDocs: (
<EuiLink
external
href={
'https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html#download-deploy-elser'
}
target="_blank"
>
{i18n.KNOWLEDGE_BASE_ELSER_SEE_DOCS}
</EuiLink>
),
}}
/>
</EuiText>
</div>
</EuiFlexItem>
<EuiFlexItem>
<div>
<EuiHealth color={knowledgeBaseHealth}>{i18n.KNOWLEDGE_BASE_LABEL}</EuiHealth>
<EuiText
size={'xs'}
color={'subdued'}
css={css`
padding-left: 20px;
`}
>
{knowledgeBaseDescription}
</EuiText>
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<span>
<EuiHealth color={esqlHealth}>{i18n.ESQL_LABEL}</EuiHealth>
<EuiText
size={'xs'}
color={'subdued'}
css={css`
padding-left: 20px;
`}
>
{esqlDescription}
</EuiText>
</span>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}
);
KnowledgeBaseSettings.displayName = 'KnowledgeBaseSettings';

View file

@ -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 { i18n } from '@kbn/i18n';
export const SETTINGS_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsTitle',
{
defaultMessage: 'Knowledge Base',
}
);
export const SETTINGS_BADGE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsBadgeTitle',
{
defaultMessage: 'Experimental',
}
);
export const SETTINGS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsDescription',
{
defaultMessage:
'Powered by ELSER, the Knowledge Base enables the ability to recall documents and other relevant context within your conversation.',
}
);
export const KNOWLEDGE_BASE_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseLabel',
{
defaultMessage: 'Knowledge Base',
}
);
export const KNOWLEDGE_BASE_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseDescription',
{
defaultMessage: 'Index where Knowledge Base docs are stored',
}
);
export const KNOWLEDGE_BASE_DESCRIPTION_INSTALLED = (kbIndexPattern: string) =>
i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseInstalledDescription',
{
defaultMessage: 'Initialized to `{kbIndexPattern}`',
values: { kbIndexPattern },
}
);
export const KNOWLEDGE_BASE_INIT_BUTTON = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.initializeKnowledgeBaseButton',
{
defaultMessage: 'Initialize',
}
);
export const KNOWLEDGE_BASE_DELETE_BUTTON = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.deleteKnowledgeBaseButton',
{
defaultMessage: 'Delete',
}
);
export const KNOWLEDGE_BASE_ELSER_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserLabel',
{
defaultMessage: 'ELSER Configured',
}
);
export const KNOWLEDGE_BASE_ELSER_MACHINE_LEARNING = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserMachineLearningDescription',
{
defaultMessage: 'Machine Learning',
}
);
export const KNOWLEDGE_BASE_ELSER_SEE_DOCS = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserSeeDocsDescription',
{
defaultMessage: 'See docs',
}
);
export const ESQL_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlLabel',
{
defaultMessage: 'ES|QL Knowledge Base Documents',
}
);
export const ESQL_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlDescription',
{
defaultMessage: 'Knowledge Base docs for generating ES|QL queries',
}
);
export const ESQL_DESCRIPTION_INSTALLED = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlInstalledDescription',
{
defaultMessage: 'ES|QL Knowledge Base docs loaded',
}
);

View file

@ -72,7 +72,6 @@ export const TestProvidersComponent: React.FC<Props> = ({
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={assistantAvailability}
assistantLangChain={false}
augmentMessageCodeBlocks={jest.fn().mockReturnValue([])}
baseAllow={[]}
baseAllowReplacement={[]}

View file

@ -46,7 +46,6 @@ export const TestProvidersComponent: React.FC<Props> = ({ children, isILMAvailab
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
assistantLangChain={false}
augmentMessageCodeBlocks={jest.fn()}
baseAllow={[]}
baseAllowReplacement={[]}

View file

@ -55,9 +55,6 @@ export const AssistantProvider: React.FC = ({ children }) => {
actionTypeRegistry={actionTypeRegistry}
augmentMessageCodeBlocks={augmentMessageCodeBlocks}
assistantAvailability={assistantAvailability}
// NOTE: `assistantLangChain` and `assistantModelEvaluation` experimental feature will be coupled until upcoming
// Knowledge Base UI updates, which will remove the `assistantLangChain` feature flag in favor of a UI feature toggle
assistantLangChain={isModelEvaluationEnabled}
assistantTelemetry={assistantTelemetry}
defaultAllow={defaultAllow}
defaultAllowReplacement={defaultAllowReplacement}
@ -71,6 +68,7 @@ export const AssistantProvider: React.FC = ({ children }) => {
getInitialConversations={getInitialConversation}
getComments={getComments}
http={http}
modelEvaluatorEnabled={isModelEvaluationEnabled}
nameSpace={nameSpace}
setConversations={setConversations}
setDefaultAllow={setDefaultAllow}

View file

@ -34,7 +34,6 @@ export const MockAssistantProviderComponent: React.FC<Props> = ({ children }) =>
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
assistantLangChain={false}
augmentMessageCodeBlocks={jest.fn(() => [])}
baseAllow={[]}
baseAllowReplacement={[]}