[8.x] [Security Assistant] V2 Knowledge Base Settings feedback and fixes (#194354) (#195644)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Assistant] V2 Knowledge Base Settings feedback and fixes
(#194354)](https://github.com/elastic/kibana/pull/194354)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Garrett
Spong","email":"spong@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-09T16:17:47Z","message":"[Security
Assistant] V2 Knowledge Base Settings feedback and fixes (#194354)\n\n##
Summary\r\n\r\nThis PR is a follow up to #192665 and addresses a bunch
of feedback and\r\nfixes including:\r\n\r\n- [X] Adds support for
updating/editing entries\r\n- [X] Fixes initial loading experience of
the KB Settings Setup/Table\r\n- [X] Fixes two bugs where
`semantic_text` and `text` must be declared\r\nfor `IndexEntries` to
work\r\n- [X] Add new Settings Context Menu items for KB and Alerts\r\n
- [X] Add support for `required` entries in initial prompt\r\n* See
[this\r\ntrace](https://smith.langchain.com/public/84a17a31-8ce8-4bd9-911e-38a854484dd8/r)\r\nfor
included knowledge. Note that the KnowledgeBaseRetrievalTool was
not\r\nselected.\r\n* Note: All prompts were updated to include the
`{knowledge_history}`\r\nplaceholder, and _not behind the feature flag_,
as this will just be the\r\nempty case until the feature flag is
enabled.\r\n\r\nTODO (in this or follow-up PR):\r\n - [ ] Add
suggestions to `index` and `fields` inputs\r\n - [ ] Adds URL
deeplinking to securityAssistantManagement\r\n- [ ] Fix bug where
updating entry does not re-create embeddings
(see\r\n[comment](https://github.com/elastic/kibana/pull/194354#discussion_r1786475496))\r\n
- [ ] Fix loading indicators when adding/editing entries\r\n - [ ] API
integration tests for update API (@e40pud)\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [X] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n* Docs being
tracked in\r\nhttps://github.com/elastic/security-docs/issues/5337 for
when feature\r\nflag is enabled\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Patryk Kopycinski
<contact@patrykkopycinski.com>","sha":"7df36721923159f45bc4fdbd26f76b20ad84249a","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Feature:Security
Assistant","Team:Security Generative
AI","v8.16.0","backport:version"],"title":"[Security Assistant] V2
Knowledge Base Settings feedback and
fixes","number":194354,"url":"https://github.com/elastic/kibana/pull/194354","mergeCommit":{"message":"[Security
Assistant] V2 Knowledge Base Settings feedback and fixes (#194354)\n\n##
Summary\r\n\r\nThis PR is a follow up to #192665 and addresses a bunch
of feedback and\r\nfixes including:\r\n\r\n- [X] Adds support for
updating/editing entries\r\n- [X] Fixes initial loading experience of
the KB Settings Setup/Table\r\n- [X] Fixes two bugs where
`semantic_text` and `text` must be declared\r\nfor `IndexEntries` to
work\r\n- [X] Add new Settings Context Menu items for KB and Alerts\r\n
- [X] Add support for `required` entries in initial prompt\r\n* See
[this\r\ntrace](https://smith.langchain.com/public/84a17a31-8ce8-4bd9-911e-38a854484dd8/r)\r\nfor
included knowledge. Note that the KnowledgeBaseRetrievalTool was
not\r\nselected.\r\n* Note: All prompts were updated to include the
`{knowledge_history}`\r\nplaceholder, and _not behind the feature flag_,
as this will just be the\r\nempty case until the feature flag is
enabled.\r\n\r\nTODO (in this or follow-up PR):\r\n - [ ] Add
suggestions to `index` and `fields` inputs\r\n - [ ] Adds URL
deeplinking to securityAssistantManagement\r\n- [ ] Fix bug where
updating entry does not re-create embeddings
(see\r\n[comment](https://github.com/elastic/kibana/pull/194354#discussion_r1786475496))\r\n
- [ ] Fix loading indicators when adding/editing entries\r\n - [ ] API
integration tests for update API (@e40pud)\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [X] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n* Docs being
tracked in\r\nhttps://github.com/elastic/security-docs/issues/5337 for
when feature\r\nflag is enabled\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Patryk Kopycinski
<contact@patrykkopycinski.com>","sha":"7df36721923159f45bc4fdbd26f76b20ad84249a"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194354","number":194354,"mergeCommit":{"message":"[Security
Assistant] V2 Knowledge Base Settings feedback and fixes (#194354)\n\n##
Summary\r\n\r\nThis PR is a follow up to #192665 and addresses a bunch
of feedback and\r\nfixes including:\r\n\r\n- [X] Adds support for
updating/editing entries\r\n- [X] Fixes initial loading experience of
the KB Settings Setup/Table\r\n- [X] Fixes two bugs where
`semantic_text` and `text` must be declared\r\nfor `IndexEntries` to
work\r\n- [X] Add new Settings Context Menu items for KB and Alerts\r\n
- [X] Add support for `required` entries in initial prompt\r\n* See
[this\r\ntrace](https://smith.langchain.com/public/84a17a31-8ce8-4bd9-911e-38a854484dd8/r)\r\nfor
included knowledge. Note that the KnowledgeBaseRetrievalTool was
not\r\nselected.\r\n* Note: All prompts were updated to include the
`{knowledge_history}`\r\nplaceholder, and _not behind the feature flag_,
as this will just be the\r\nempty case until the feature flag is
enabled.\r\n\r\nTODO (in this or follow-up PR):\r\n - [ ] Add
suggestions to `index` and `fields` inputs\r\n - [ ] Adds URL
deeplinking to securityAssistantManagement\r\n- [ ] Fix bug where
updating entry does not re-create embeddings
(see\r\n[comment](https://github.com/elastic/kibana/pull/194354#discussion_r1786475496))\r\n
- [ ] Fix loading indicators when adding/editing entries\r\n - [ ] API
integration tests for update API (@e40pud)\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [X] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n* Docs being
tracked in\r\nhttps://github.com/elastic/security-docs/issues/5337 for
when feature\r\nflag is enabled\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Patryk Kopycinski
<contact@patrykkopycinski.com>","sha":"7df36721923159f45bc4fdbd26f76b20ad84249a"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-10-10 04:59:46 +11:00 committed by GitHub
parent 1da5439d8d
commit e8992e3749
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 686 additions and 143 deletions

View file

@ -106,7 +106,11 @@ export type BaseCreateProps = z.infer<typeof BaseCreateProps>;
export const BaseCreateProps = BaseRequiredFields.merge(BaseDefaultableFields);
export type BaseUpdateProps = z.infer<typeof BaseUpdateProps>;
export const BaseUpdateProps = BaseCreateProps.partial();
export const BaseUpdateProps = BaseCreateProps.partial().merge(
z.object({
id: NonEmptyString,
})
);
export type BaseResponseProps = z.infer<typeof BaseResponseProps>;
export const BaseResponseProps = BaseRequiredFields.merge(BaseDefaultableFields.required());

View file

@ -112,6 +112,12 @@ components:
allOf:
- $ref: "#/components/schemas/BaseCreateProps"
x-modify: partial
- type: object
properties:
id:
$ref: "../../common_attributes.schema.yaml#/components/schemas/NonEmptyString"
required:
- id
BaseResponseProps:
x-inline: true

View file

@ -5,16 +5,13 @@
* 2.0.
*/
import React, { useState, useMemo, useCallback } from 'react';
import React, { useMemo, useCallback } from 'react';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
EuiContextMenu,
EuiButtonIcon,
EuiPanel,
EuiConfirmModal,
EuiToolTip,
EuiSkeletonTitle,
} from '@elastic/eui';
@ -29,6 +26,7 @@ import { FlyoutNavigation } from '../assistant_overlay/flyout_navigation';
import { AssistantSettingsButton } from '../settings/assistant_settings_button';
import * as i18n from './translations';
import { AIConnector } from '../../connectorland/connector_selector';
import { SettingsContextMenu } from '../settings/settings_context_menu/settings_context_menu';
interface OwnProps {
selectedConversation: Conversation | undefined;
@ -94,21 +92,6 @@ export const AssistantHeader: React.FC<Props> = ({
[selectedConversation?.apiConfig?.connectorId]
);
const [isPopoverOpen, setPopover] = useState(false);
const onButtonClick = useCallback(() => {
setPopover(!isPopoverOpen);
}, [isPopoverOpen]);
const closePopover = useCallback(() => {
setPopover(false);
}, []);
const [isResetConversationModalVisible, setIsResetConversationModalVisible] = useState(false);
const closeDestroyModal = useCallback(() => setIsResetConversationModalVisible(false), []);
const showDestroyModal = useCallback(() => setIsResetConversationModalVisible(true), []);
const onConversationChange = useCallback(
(updatedConversation: Conversation) => {
onConversationSelected({
@ -119,32 +102,6 @@ export const AssistantHeader: React.FC<Props> = ({
[onConversationSelected]
);
const panels = useMemo(
() => [
{
id: 0,
items: [
{
name: i18n.RESET_CONVERSATION,
css: css`
color: ${euiThemeVars.euiColorDanger};
`,
onClick: showDestroyModal,
icon: 'refresh',
'data-test-subj': 'clear-chat',
},
],
},
],
[showDestroyModal]
);
const handleReset = useCallback(() => {
onChatCleared();
closeDestroyModal();
closePopover();
}, [onChatCleared, closeDestroyModal, closePopover]);
return (
<>
<FlyoutNavigation
@ -246,42 +203,12 @@ export const AssistantHeader: React.FC<Props> = ({
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem>
<EuiPopover
button={
<EuiButtonIcon
aria-label="test"
isDisabled={isDisabled}
iconType="boxesVertical"
onClick={onButtonClick}
data-test-subj="chat-context-menu"
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
<SettingsContextMenu isDisabled={isDisabled} onChatCleared={onChatCleared} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
{isResetConversationModalVisible && (
<EuiConfirmModal
title={i18n.RESET_CONVERSATION}
onCancel={closeDestroyModal}
onConfirm={handleReset}
cancelButtonText={i18n.CANCEL_BUTTON_TEXT}
confirmButtonText={i18n.RESET_BUTTON_TEXT}
buttonColor="danger"
defaultFocusedButton="confirm"
data-test-subj="reset-conversation-modal"
>
<p>{i18n.CLEAR_CHAT_CONFIRMATION}</p>
</EuiConfirmModal>
)}
</>
);
};

View file

@ -7,6 +7,34 @@
import { i18n } from '@kbn/i18n';
export const AI_ASSISTANT_SETTINGS = i18n.translate(
'xpack.elasticAssistant.assistant.settings.aiAssistantSettings',
{
defaultMessage: 'AI Assistant settings',
}
);
export const ANONYMIZATION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.anonymization',
{
defaultMessage: 'Anonymization',
}
);
export const KNOWLEDGE_BASE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBase',
{
defaultMessage: 'Knowledge Base',
}
);
export const ALERTS_TO_ANALYZE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.alertsToAnalyze',
{
defaultMessage: 'Alerts to analyze',
}
);
export const RESET_CONVERSATION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.resetConversation',
{

View file

@ -9,8 +9,8 @@ import { render, screen, fireEvent } from '@testing-library/react';
import React from 'react';
import { AlertsSettings } from './alerts_settings';
import { KnowledgeBaseConfig } from '../../assistant/types';
import { DEFAULT_LATEST_ALERTS } from '../../assistant_context/constants';
import { KnowledgeBaseConfig } from '../../types';
import { DEFAULT_LATEST_ALERTS } from '../../../assistant_context/constants';
describe('AlertsSettings', () => {
beforeEach(() => {

View file

@ -9,9 +9,9 @@ import { EuiFlexGroup, EuiFormRow, EuiFlexItem, EuiSpacer, EuiText } from '@elas
import { css } from '@emotion/react';
import React from 'react';
import { KnowledgeBaseConfig } from '../../assistant/types';
import { AlertsRange } from '../../knowledge_base/alerts_range';
import * as i18n from '../../knowledge_base/translations';
import { KnowledgeBaseConfig } from '../../types';
import { AlertsRange } from '../../../knowledge_base/alerts_range';
import * as i18n from '../../../knowledge_base/translations';
export const MIN_LATEST_ALERTS = 10;
export const MAX_LATEST_ALERTS = 100;

View file

@ -7,19 +7,24 @@
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import React from 'react';
import { KnowledgeBaseConfig } from '../../assistant/types';
import { AlertsRange } from '../../knowledge_base/alerts_range';
import * as i18n from '../../knowledge_base/translations';
import { KnowledgeBaseConfig } from '../../types';
import { AlertsRange } from '../../../knowledge_base/alerts_range';
import * as i18n from '../../../knowledge_base/translations';
interface Props {
knowledgeBase: KnowledgeBaseConfig;
setUpdatedKnowledgeBaseSettings: React.Dispatch<React.SetStateAction<KnowledgeBaseConfig>>;
hasBorder?: boolean;
}
/**
* Replaces the AlertsSettings component used in the existing settings modal. Once the modal is
* fully removed we can delete that component in favor of this one.
*/
export const AlertsSettingsManagement: React.FC<Props> = React.memo(
({ knowledgeBase, setUpdatedKnowledgeBaseSettings }) => {
({ knowledgeBase, setUpdatedKnowledgeBaseSettings, hasBorder = true }) => {
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="l" title={i18n.ALERTS_LABEL}>
<EuiPanel hasShadow={false} hasBorder={hasBorder} paddingSize="l" title={i18n.ALERTS_LABEL}>
<EuiTitle size="m">
<h3>{i18n.ALERTS_LABEL}</h3>
</EuiTitle>

View file

@ -25,6 +25,7 @@ import {
SYSTEM_PROMPTS_TAB,
} from './const';
import { mockSystemPrompts } from '../../mock/system_prompt';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
const mockConversations = {
[alertConvo.title]: alertConvo,
@ -53,8 +54,13 @@ const mockContext = {
},
};
const mockDataViews = {
getIndices: jest.fn(),
} as unknown as DataViewsContract;
const testProps = {
selectedConversation: welcomeConvo,
dataViews: mockDataViews,
};
jest.mock('../../assistant_context');

View file

@ -9,6 +9,7 @@ import React, { useEffect, useMemo } from 'react';
import { EuiAvatar, EuiPageTemplate, EuiTitle, useEuiShadow, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
import { Conversation } from '../../..';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
@ -33,6 +34,7 @@ import { KnowledgeBaseSettingsManagement } from '../../knowledge_base/knowledge_
import { EvaluationSettings } from '.';
interface Props {
dataViews: DataViewsContract;
selectedConversation: Conversation;
}
@ -41,7 +43,7 @@ interface Props {
* anonymization, knowledge base, and evaluation via the `isModelEvaluationEnabled` feature flag.
*/
export const AssistantSettingsManagement: React.FC<Props> = React.memo(
({ selectedConversation: defaultSelectedConversation }) => {
({ dataViews, selectedConversation: defaultSelectedConversation }) => {
const {
assistantFeatures: { assistantModelEvaluation: modelEvaluatorEnabled },
http,
@ -158,7 +160,9 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
)}
{selectedSettingsTab === QUICK_PROMPTS_TAB && <QuickPromptSettingsManagement />}
{selectedSettingsTab === ANONYMIZATION_TAB && <AnonymizationSettingsManagement />}
{selectedSettingsTab === KNOWLEDGE_BASE_TAB && <KnowledgeBaseSettingsManagement />}
{selectedSettingsTab === KNOWLEDGE_BASE_TAB && (
<KnowledgeBaseSettingsManagement dataViews={dataViews} />
)}
{selectedSettingsTab === EVALUATION_TAB && <EvaluationSettings />}
</EuiPageTemplate.Section>
</>

View file

@ -0,0 +1,186 @@
/*
* 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, { ReactElement, useCallback, useMemo, useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiConfirmModal,
EuiNotificationBadge,
EuiPopover,
EuiButtonIcon,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { useAssistantContext } from '../../../..';
import * as i18n from '../../assistant_header/translations';
interface Params {
isDisabled?: boolean;
onChatCleared?: () => void;
}
export const SettingsContextMenu: React.FC<Params> = React.memo(
({ isDisabled = false, onChatCleared }: Params) => {
const {
navigateToApp,
knowledgeBase,
assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
} = useAssistantContext();
const [isPopoverOpen, setPopover] = useState(false);
const [isResetConversationModalVisible, setIsResetConversationModalVisible] = useState(false);
const closeDestroyModal = useCallback(() => setIsResetConversationModalVisible(false), []);
const onButtonClick = useCallback(() => {
setPopover(!isPopoverOpen);
}, [isPopoverOpen]);
const closePopover = useCallback(() => {
setPopover(false);
}, []);
const showDestroyModal = useCallback(() => {
closePopover?.();
setIsResetConversationModalVisible(true);
}, [closePopover]);
const handleNavigateToSettings = useCallback(
() =>
navigateToApp('management', {
path: 'kibana/securityAiAssistantManagement',
}),
[navigateToApp]
);
const handleNavigateToKnowledgeBase = useCallback(
() =>
navigateToApp('management', {
path: 'kibana/securityAiAssistantManagement',
}),
[navigateToApp]
);
// We are migrating away from the settings modal in favor of the new Stack Management UI
// Currently behind `assistantKnowledgeBaseByDefault` FF
const newItems: ReactElement[] = useMemo(
() => [
<EuiContextMenuItem
aria-label={'ai-assistant-settings'}
onClick={handleNavigateToSettings}
icon={'gear'}
data-test-subj={'ai-assistant-settings'}
>
{i18n.AI_ASSISTANT_SETTINGS}
</EuiContextMenuItem>,
<EuiContextMenuItem
aria-label={'anonymization'}
onClick={handleNavigateToSettings}
icon={'eye'}
data-test-subj={'anonymization'}
>
{i18n.ANONYMIZATION}
</EuiContextMenuItem>,
<EuiContextMenuItem
aria-label={'knowledge-base'}
onClick={handleNavigateToKnowledgeBase}
icon={'documents'}
data-test-subj={'knowledge-base'}
>
{i18n.KNOWLEDGE_BASE}
</EuiContextMenuItem>,
<EuiContextMenuItem
aria-label={'alerts-to-analyze'}
onClick={handleNavigateToSettings}
icon={'magnifyWithExclamation'}
data-test-subj={'alerts-to-analyze'}
>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>{i18n.ALERTS_TO_ANALYZE}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiNotificationBadge color="subdued">
{knowledgeBase.latestAlerts}
</EuiNotificationBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiContextMenuItem>,
],
[handleNavigateToKnowledgeBase, handleNavigateToSettings, knowledgeBase]
);
const items = useMemo(
() => [
...(enableKnowledgeBaseByDefault ? newItems : []),
<EuiContextMenuItem
aria-label={'clear-chat'}
onClick={showDestroyModal}
icon={'refresh'}
data-test-subj={'clear-chat'}
css={css`
color: ${euiThemeVars.euiColorDanger};
`}
>
{i18n.RESET_CONVERSATION}
</EuiContextMenuItem>,
],
[enableKnowledgeBaseByDefault, newItems, showDestroyModal]
);
const handleReset = useCallback(() => {
onChatCleared?.();
closeDestroyModal();
closePopover?.();
}, [onChatCleared, closeDestroyModal, closePopover]);
return (
<>
<EuiPopover
button={
<EuiButtonIcon
aria-label="test"
isDisabled={isDisabled}
iconType="boxesVertical"
onClick={onButtonClick}
data-test-subj="chat-context-menu"
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="leftUp"
>
<EuiContextMenuPanel
items={items}
css={css`
width: 250px;
`}
/>
</EuiPopover>
{isResetConversationModalVisible && (
<EuiConfirmModal
title={i18n.RESET_CONVERSATION}
onCancel={closeDestroyModal}
onConfirm={handleReset}
cancelButtonText={i18n.CANCEL_BUTTON_TEXT}
confirmButtonText={i18n.RESET_BUTTON_TEXT}
buttonColor="danger"
defaultFocusedButton="confirm"
data-test-subj="reset-conversation-modal"
>
<p>{i18n.CLEAR_CHAT_CONFIRMATION}</p>
</EuiConfirmModal>
)}
</>
);
}
);
SettingsContextMenu.displayName = 'SettingsContextMenu';

View file

@ -12,7 +12,7 @@ import {
MAX_LATEST_ALERTS,
MIN_LATEST_ALERTS,
TICK_INTERVAL,
} from '../alerts/settings/alerts_settings';
} from '../assistant/settings/alerts_settings/alerts_settings';
import { KnowledgeBaseConfig } from '../assistant/types';
import { ALERTS_RANGE } from './translations';

View file

@ -23,7 +23,7 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { AlertsSettings } from '../alerts/settings/alerts_settings';
import { AlertsSettings } from '../assistant/settings/alerts_settings/alerts_settings';
import { useAssistantContext } from '../assistant_context';
import type { KnowledgeBaseConfig } from '../assistant/types';
import * as i18n from './translations';

View file

@ -127,7 +127,6 @@ export const DocumentEntryEditor: React.FC<Props> = React.memo(({ entry, setEntr
id="requiredKnowledge"
onChange={onRequiredKnowledgeChanged}
checked={entry?.required ?? false}
disabled={true}
/>
</EuiFormRow>
</EuiForm>

View file

@ -6,8 +6,12 @@
*/
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiLink,
EuiLoadingSpinner,
EuiPanel,
EuiSearchBarProps,
EuiSpacer,
@ -23,7 +27,9 @@ import {
KnowledgeBaseEntryCreateProps,
KnowledgeBaseEntryResponse,
} from '@kbn/elastic-assistant-common';
import { AlertsSettingsManagement } from '../../alerts/settings/alerts_settings_management';
import { css } from '@emotion/react';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
import { AlertsSettingsManagement } from '../../assistant/settings/alerts_settings/alerts_settings_management';
import { useKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_knowledge_base_entries';
import { useAssistantContext } from '../../assistant_context';
import { useKnowledgeBaseTable } from './use_knowledge_base_table';
@ -40,7 +46,7 @@ import { useFlyoutModalVisibility } from '../../assistant/common/components/assi
import { IndexEntryEditor } from './index_entry_editor';
import { DocumentEntryEditor } from './document_entry_editor';
import { KnowledgeBaseSettings } from '../knowledge_base_settings';
import { SetupKnowledgeBaseButton } from '../setup_knowledge_base_button';
import { ESQL_RESOURCE, SetupKnowledgeBaseButton } from '../setup_knowledge_base_button';
import { useDeleteKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_delete_knowledge_base_entries';
import {
isSystemEntry,
@ -51,14 +57,24 @@ import { useCreateKnowledgeBaseEntry } from '../../assistant/api/knowledge_base/
import { useUpdateKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_update_knowledge_base_entries';
import { SETTINGS_UPDATED_TOAST_TITLE } from '../../assistant/settings/translations';
import { KnowledgeBaseConfig } from '../../assistant/types';
import {
isKnowledgeBaseSetup,
useKnowledgeBaseStatus,
} from '../../assistant/api/knowledge_base/use_knowledge_base_status';
export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
interface Params {
dataViews: DataViewsContract;
}
export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ dataViews }) => {
const {
assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
http,
toasts,
} = useAssistantContext();
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const { data: kbStatus, isFetched } = useKnowledgeBaseStatus({ http, resource: ESQL_RESOURCE });
const isKbSetup = isKnowledgeBaseSetup(kbStatus);
// Only needed for legacy settings management
const { knowledgeBase, setUpdatedKnowledgeBaseSettings, resetSettings, saveSettings } =
@ -123,12 +139,12 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
// Flyout Save/Cancel Actions
const onSaveConfirmed = useCallback(() => {
if (isKnowledgeBaseEntryCreateProps(selectedEntry)) {
createEntry(selectedEntry);
closeFlyout();
} else if (isKnowledgeBaseEntryResponse(selectedEntry)) {
if (isKnowledgeBaseEntryResponse(selectedEntry)) {
updateEntries([selectedEntry]);
closeFlyout();
} else if (isKnowledgeBaseEntryCreateProps(selectedEntry)) {
createEntry(selectedEntry);
closeFlyout();
}
}, [closeFlyout, selectedEntry, createEntry, updateEntries]);
@ -137,7 +153,11 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
closeFlyout();
}, [closeFlyout]);
const { data: entries } = useKnowledgeBaseEntries({
const {
data: entries,
isFetching: isFetchingEntries,
refetch: refetchEntries,
} = useKnowledgeBaseEntries({
http,
toasts,
enabled: enableKnowledgeBaseByDefault,
@ -169,6 +189,9 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
[deleteEntry, entries.data, getColumns, openFlyout]
);
// Refresh button
const handleRefreshTable = useCallback(() => refetchEntries(), [refetchEntries]);
const onDocumentClicked = useCallback(() => {
setSelectedEntry({ type: DocumentEntryType.value, kbResource: 'user', source: 'user' });
openFlyout();
@ -182,7 +205,30 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
const search: EuiSearchBarProps = useMemo(
() => ({
toolsRight: (
<AddEntryButton onDocumentClicked={onDocumentClicked} onIndexClicked={onIndexClicked} />
<EuiFlexGroup
gutterSize={'m'}
css={css`
margin-left: -5px;
`}
>
<EuiFlexItem>
<EuiButton
color={'text'}
isDisabled={isFetchingEntries}
onClick={handleRefreshTable}
iconType={'refresh'}
isLoading={isFetchingEntries}
>
<FormattedMessage
id="xpack.elasticAssistant.assistant.settings.knowledgeBasedSettingManagements.refreshButton"
defaultMessage="Refresh"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<AddEntryButton onDocumentClicked={onDocumentClicked} onIndexClicked={onIndexClicked} />
</EuiFlexItem>
</EuiFlexGroup>
),
box: {
incremental: true,
@ -190,7 +236,7 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
},
filters: [],
}),
[onDocumentClicked, onIndexClicked]
[isFetchingEntries, handleRefreshTable, onDocumentClicked, onIndexClicked]
);
const flyoutTitle = useMemo(() => {
@ -247,15 +293,40 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
),
}}
/>
<SetupKnowledgeBaseButton display={'mini'} />
</EuiText>
<EuiSpacer size="l" />
<EuiInMemoryTable
columns={columns}
items={entries.data ?? []}
search={search}
sorting={sorting}
/>
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
{!isFetched ? (
<EuiLoadingSpinner size="l" />
) : isKbSetup ? (
<EuiInMemoryTable
columns={columns}
items={entries.data ?? []}
search={search}
sorting={sorting}
/>
) : (
<>
<EuiSpacer size="l" />
<EuiText size={'m'}>
<FormattedMessage
id="xpack.elasticAssistant.assistant.settings.knowledgeBasedSettingManagements.knowledgeBaseSetupDescription"
defaultMessage="Setup to get started with the Knowledge Base."
/>
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<SetupKnowledgeBaseButton />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiSpacer size="m" />
<AlertsSettingsManagement
@ -286,6 +357,7 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
) : (
<IndexEntryEditor
entry={selectedEntry as IndexEntry}
dataViews={dataViews}
setEntry={
setSelectedEntry as React.Dispatch<React.SetStateAction<Partial<IndexEntry>>>
}

View file

@ -17,14 +17,16 @@ import {
} from '@elastic/eui';
import React, { useCallback } from 'react';
import { IndexEntry } from '@kbn/elastic-assistant-common';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
import * as i18n from './translations';
interface Props {
dataViews: DataViewsContract;
entry?: IndexEntry;
setEntry: React.Dispatch<React.SetStateAction<Partial<IndexEntry>>>;
}
export const IndexEntryEditor: React.FC<Props> = React.memo(({ entry, setEntry }) => {
export const IndexEntryEditor: React.FC<Props> = React.memo(({ dataViews, entry, setEntry }) => {
// Name
const setName = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) =>
@ -74,9 +76,17 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(({ entry, setEntry }
entry?.users?.length === 0 ? sharingOptions[1].value : sharingOptions[0].value;
// Index
// TODO: For index field autocomplete
// const indexOptions = useMemo(() => {
// const indices = await dataViews.getIndices({
// pattern: e[0]?.value ?? '',
// isRollupIndex: () => false,
// });
// }, [dataViews]);
const setIndex = useCallback(
(e: Array<EuiComboBoxOptionOption<string>>) =>
setEntry((prevEntry) => ({ ...prevEntry, index: e[0].value })),
async (e: Array<EuiComboBoxOptionOption<string>>) => {
setEntry((prevEntry) => ({ ...prevEntry, index: e[0]?.value }));
},
[setEntry]
);
@ -162,30 +172,51 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(({ entry, setEntry }
<EuiFormRow label={i18n.ENTRY_FIELD_INPUT_LABEL} fullWidth>
<EuiFieldText
name="field"
placeholder={i18n.ENTRY_INPUT_PLACEHOLDER}
placeholder={i18n.ENTRY_FIELD_PLACEHOLDER}
fullWidth
value={entry?.field}
onChange={setField}
/>
</EuiFormRow>
<EuiFormRow label={i18n.ENTRY_DESCRIPTION_INPUT_LABEL} fullWidth>
<EuiFormRow
label={i18n.ENTRY_DESCRIPTION_INPUT_LABEL}
helpText={i18n.ENTRY_DESCRIPTION_HELP_LABEL}
fullWidth
>
<EuiFieldText
name="description"
placeholder={i18n.ENTRY_INPUT_PLACEHOLDER}
fullWidth
value={entry?.description}
onChange={setDescription}
/>
</EuiFormRow>
<EuiFormRow label={i18n.ENTRY_QUERY_DESCRIPTION_INPUT_LABEL} fullWidth>
<EuiFormRow
label={i18n.ENTRY_QUERY_DESCRIPTION_INPUT_LABEL}
helpText={i18n.ENTRY_QUERY_DESCRIPTION_HELP_LABEL}
fullWidth
>
<EuiFieldText
name="description"
placeholder={i18n.ENTRY_INPUT_PLACEHOLDER}
fullWidth
value={entry?.queryDescription}
onChange={setQueryDescription}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.ENTRY_OUTPUT_FIELDS_INPUT_LABEL}
helpText={i18n.ENTRY_OUTPUT_FIELDS_HELP_LABEL}
fullWidth
>
<EuiComboBox
aria-label={i18n.ENTRY_OUTPUT_FIELDS_INPUT_LABEL}
isClearable={true}
singleSelection={{ asPlainText: true }}
onCreateOption={onCreateOption}
fullWidth
selectedOptions={[]}
onChange={setIndex}
/>
</EuiFormRow>
</EuiForm>
);
});

View file

@ -251,14 +251,44 @@ export const ENTRY_FIELD_INPUT_LABEL = i18n.translate(
export const ENTRY_DESCRIPTION_INPUT_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryDescriptionInputLabel',
{
defaultMessage: 'Description',
defaultMessage: 'Data Description',
}
);
export const ENTRY_DESCRIPTION_HELP_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryDescriptionHelpLabel',
{
defaultMessage:
'A description of the type of data in this index and/or when the assistant should look for data here.',
}
);
export const ENTRY_QUERY_DESCRIPTION_INPUT_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryQueryDescriptionInputLabel',
{
defaultMessage: 'Query Description',
defaultMessage: 'Query Instruction',
}
);
export const ENTRY_QUERY_DESCRIPTION_HELP_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryQueryDescriptionHelpLabel',
{
defaultMessage: 'Any instructions for extracting the search query from the user request.',
}
);
export const ENTRY_OUTPUT_FIELDS_INPUT_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryOutputFieldsInputLabel',
{
defaultMessage: 'Output Fields',
}
);
export const ENTRY_OUTPUT_FIELDS_HELP_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryOutputFieldsHelpLabel',
{
defaultMessage:
'What fields should be sent to the LLM. Leave empty to send the entire document.',
}
);
@ -269,6 +299,13 @@ export const ENTRY_INPUT_PLACEHOLDER = i18n.translate(
}
);
export const ENTRY_FIELD_PLACEHOLDER = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryFieldPlaceholder',
{
defaultMessage: 'semantic_text',
}
);
export const KNOWLEDGE_BASE_DOCUMENTATION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.knowledgeBaseDocumentation',
{

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiAvatar, EuiBadge, EuiBasicTableColumn, EuiIcon, EuiLink, EuiText } from '@elastic/eui';
import { EuiAvatar, EuiBadge, EuiBasicTableColumn, EuiIcon, EuiText } from '@elastic/eui';
import { css } from '@emotion/react';
import React, { useCallback } from 'react';
import { FormattedDate } from '@kbn/i18n-react';
@ -32,7 +32,7 @@ export const useKnowledgeBaseTable = () => {
if (['esql', 'security_labs'].includes(entry.kbResource)) {
return 'logoElastic';
}
return 'visText';
return 'document';
} else if (entry.type === IndexEntryType.value) {
return 'index';
}
@ -61,9 +61,7 @@ export const useKnowledgeBaseTable = () => {
},
{
name: i18n.COLUMN_NAME,
render: (entry: KnowledgeBaseEntryResponse) => (
<EuiLink onClick={() => onEntryNameClicked(entry)}>{entry.name}</EuiLink>
),
render: ({ name }: KnowledgeBaseEntryResponse) => name,
sortable: ({ name }: KnowledgeBaseEntryResponse) => name,
width: '30%',
},

View file

@ -30,5 +30,6 @@
"@kbn/core-doc-links-browser",
"@kbn/core",
"@kbn/zod",
"@kbn/data-views-plugin",
]
}

View file

@ -12,10 +12,11 @@ import {
DocumentEntryCreateFields,
KnowledgeBaseEntryCreateProps,
KnowledgeBaseEntryResponse,
KnowledgeBaseEntryUpdateProps,
Metadata,
} from '@kbn/elastic-assistant-common';
import { getKnowledgeBaseEntry } from './get_knowledge_base_entry';
import { CreateKnowledgeBaseEntrySchema } from './types';
import { CreateKnowledgeBaseEntrySchema, UpdateKnowledgeBaseEntrySchema } from './types';
export interface CreateKnowledgeBaseEntryParams {
esClient: ElasticsearchClient;
@ -77,6 +78,111 @@ export const createKnowledgeBaseEntry = async ({
}
};
interface TransformToUpdateSchemaProps {
user: AuthenticatedUser;
updatedAt: string;
entry: KnowledgeBaseEntryUpdateProps;
global?: boolean;
}
export const transformToUpdateSchema = ({
user,
updatedAt,
entry,
global = false,
}: TransformToUpdateSchemaProps): UpdateKnowledgeBaseEntrySchema => {
const base = {
id: entry.id,
updated_at: updatedAt,
updated_by: user.profile_uid ?? 'unknown',
name: entry.name,
type: entry.type,
users: global
? []
: [
{
id: user.profile_uid,
name: user.username,
},
],
};
if (entry.type === 'index') {
const { inputSchema, outputFields, queryDescription, ...restEntry } = entry;
return {
...base,
...restEntry,
query_description: queryDescription,
input_schema:
entry.inputSchema?.map((schema) => ({
field_name: schema.fieldName,
field_type: schema.fieldType,
description: schema.description,
})) ?? undefined,
output_fields: outputFields ?? undefined,
};
}
return {
...base,
kb_resource: entry.kbResource,
required: entry.required ?? false,
source: entry.source,
text: entry.text,
vector: undefined,
};
};
export const getUpdateScript = ({
entry,
isPatch,
}: {
entry: UpdateKnowledgeBaseEntrySchema;
isPatch?: boolean;
}) => {
return {
source: `
if (params.assignEmpty == true || params.containsKey('name')) {
ctx._source.name = params.name;
}
if (params.assignEmpty == true || params.containsKey('type')) {
ctx._source.type = params.type;
}
if (params.assignEmpty == true || params.containsKey('users')) {
ctx._source.users = params.users;
}
if (params.assignEmpty == true || params.containsKey('query_description')) {
ctx._source.query_description = params.query_description;
}
if (params.assignEmpty == true || params.containsKey('input_schema')) {
ctx._source.input_schema = params.input_schema;
}
if (params.assignEmpty == true || params.containsKey('output_fields')) {
ctx._source.output_fields = params.output_fields;
}
if (params.assignEmpty == true || params.containsKey('kb_resource')) {
ctx._source.kb_resource = params.kb_resource;
}
if (params.assignEmpty == true || params.containsKey('required')) {
ctx._source.required = params.required;
}
if (params.assignEmpty == true || params.containsKey('source')) {
ctx._source.source = params.source;
}
if (params.assignEmpty == true || params.containsKey('text')) {
ctx._source.text = params.text;
}
ctx._source.updated_at = params.updated_at;
ctx._source.updated_by = params.updated_by;
`,
lang: 'painless',
params: {
...entry, // when assigning undefined in painless, it will remove property and wil set it to null
// for patch we don't want to remove unspecified value in payload
assignEmpty: !(isPatch ?? true),
},
};
};
interface TransformToCreateSchemaProps {
createdAt: string;
spaceId: string;

View file

@ -6,6 +6,7 @@
*/
import { z } from '@kbn/zod';
import { get } from 'lodash';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { errors } from '@elastic/elasticsearch';
import { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types';
@ -189,7 +190,7 @@ export const getStructuredToolForIndexEntry = ({
standard: {
query: {
nested: {
path: 'semantic_text.inference.chunks',
path: `${indexEntry.field}.inference.chunks`,
query: {
sparse_vector: {
inference_id: elserId,
@ -220,7 +221,7 @@ export const getStructuredToolForIndexEntry = ({
}, {});
}
return {
text: (hit._source as { text: string }).text,
text: get(hit._source, `${indexEntry.field}.inference.chunks[0].text`),
};
});

View file

@ -15,6 +15,7 @@ import { Document } from 'langchain/document';
import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import {
DocumentEntryType,
DocumentEntry,
IndexEntry,
KnowledgeBaseEntryCreateProps,
KnowledgeBaseEntryResponse,
@ -444,7 +445,9 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
);
this.options.logger.debug(
() =>
`getKnowledgeBaseDocuments() - Similarity Search Results:\n ${JSON.stringify(results)}`
`getKnowledgeBaseDocuments() - Similarity Search returned [${JSON.stringify(
results.length
)}] results`
);
return results;
@ -454,6 +457,47 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
}
};
/**
* Returns all global and current user's private `required` document entries.
*/
public getRequiredKnowledgeBaseDocumentEntries = async (): Promise<DocumentEntry[]> => {
const user = this.options.currentUser;
if (user == null) {
throw new Error(
'Authenticated user not found! Ensure kbDataClient was initialized from a request.'
);
}
try {
const userFilter = getKBUserFilter(user);
const results = await this.findDocuments<EsIndexEntry>({
// Note: This is a magic number to set some upward bound as to not blow the context with too
// many historical KB entries. Ideally we'd query for all and token trim.
perPage: 100,
page: 1,
sortField: 'created_at',
sortOrder: 'asc',
filter: `${userFilter} AND type:document AND kb_resource:user AND required:true`,
});
this.options.logger.debug(
`kbDataClient.getRequiredKnowledgeBaseDocumentEntries() - results:\n${JSON.stringify(
results
)}`
);
if (results) {
return transformESSearchToKnowledgeBaseEntry(results.data) as DocumentEntry[];
}
} catch (e) {
this.options.logger.error(
`kbDataClient.getRequiredKnowledgeBaseDocumentEntries() - Failed to fetch DocumentEntries`
);
return [];
}
return [];
};
/**
* Creates a new Knowledge Base Entry.
*
@ -492,7 +536,10 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
};
/**
* Returns AssistantTools for any 'relevant' KB IndexEntries that exist in the knowledge base
* Returns AssistantTools for any 'relevant' KB IndexEntries that exist in the knowledge base.
*
* Note: Accepts esClient so retrieval can be scoped to the current user as esClient on kbDataClient
* is scoped to system user.
*/
public getAssistantTools = async ({
assistantToolParams,
@ -520,7 +567,7 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
page: 1,
sortField: 'created_at',
sortOrder: 'asc',
filter: `${userFilter}${` AND type:index`}`, // TODO: Support global tools (no user filter), and filter by space as well
filter: `${userFilter} AND type:index`,
});
this.options.logger.debug(
`kbDataClient.getAssistantTools() - results:\n${JSON.stringify(results, null, 2)}`

View file

@ -82,6 +82,39 @@ export interface LegacyEsKnowledgeBaseEntrySchema {
model_id: string;
};
}
export interface UpdateKnowledgeBaseEntrySchema {
id: string;
created_at?: string;
created_by?: string;
updated_at?: string;
updated_by?: string;
users?: Array<{
id?: string;
name?: string;
}>;
name?: string;
type?: string;
// Document Entry Fields
kb_resource?: string;
required?: boolean;
source?: string;
text?: string;
vector?: {
tokens: Record<string, number>;
model_id: string;
};
// Index Entry Fields
index?: string;
field?: string;
description?: string;
query_description?: string;
input_schema?: Array<{
field_name: string;
field_type: string;
description: string;
}>;
output_fields?: string[];
}
export interface CreateKnowledgeBaseEntrySchema {
'@timestamp'?: string;

View file

@ -84,6 +84,7 @@ export class AIAssistantService {
private isKBSetupInProgress: boolean = false;
// Temporary 'feature flag' to determine if we should initialize the new kb mappings, toggled when accessing kbDataClient
private v2KnowledgeBaseEnabled: boolean = false;
private hasInitializedV2KnowledgeBase: boolean = false;
constructor(private readonly options: AIAssistantServiceOpts) {
this.initialized = false;
@ -363,8 +364,13 @@ export class AIAssistantService {
// If either v2 KB or a modelIdOverride is provided, we need to reinitialize all persistence resources to make sure
// they're using the correct model/mappings. Technically all existing KB data is stale since it was created
// with a different model/mappings, but modelIdOverride is only intended for testing purposes at this time
if (opts.v2KnowledgeBaseEnabled || opts.modelIdOverride != null) {
// Added hasInitializedV2KnowledgeBase to prevent the console noise from re-init on each KB request
if (
!this.hasInitializedV2KnowledgeBase &&
(opts.v2KnowledgeBaseEnabled || opts.modelIdOverride != null)
) {
await this.initializeResources();
this.hasInitializedV2KnowledgeBase = true;
}
const res = await this.checkResourcesInstallation(opts);

View file

@ -141,7 +141,7 @@ export const getDefaultAssistantGraph = ({
})
)
.addNode(NodeType.AGENT, (state: AgentState) =>
runAgent({ ...nodeParams, state, agentRunnable })
runAgent({ ...nodeParams, state, agentRunnable, kbDataClient: dataClients?.kbDataClient })
)
.addNode(NodeType.TOOLS, (state: AgentState) => executeTools({ ...nodeParams, state, tools }))
.addNode(NodeType.RESPOND, (state: AgentState) =>

View file

@ -10,15 +10,20 @@ import { AgentRunnableSequence } from 'langchain/dist/agents/agent';
import { formatLatestUserMessage } from '../prompts';
import { AgentState, NodeParamsBase } from '../types';
import { NodeType } from '../constants';
import { AIAssistantKnowledgeBaseDataClient } from '../../../../../ai_assistant_data_clients/knowledge_base';
export interface RunAgentParams extends NodeParamsBase {
state: AgentState;
config?: RunnableConfig;
agentRunnable: AgentRunnableSequence;
kbDataClient?: AIAssistantKnowledgeBaseDataClient;
}
export const AGENT_NODE_TAG = 'agent_run';
const KNOWLEDGE_HISTORY_PREFIX = 'Knowledge History:';
const NO_KNOWLEDGE_HISTORY = '[No existing knowledge history]';
/**
* Node to run the agent
*
@ -26,18 +31,27 @@ export const AGENT_NODE_TAG = 'agent_run';
* @param state - The current state of the graph
* @param config - Any configuration that may've been supplied
* @param agentRunnable - The agent to run
* @param kbDataClient - Data client for accessing the Knowledge Base on behalf of the current user
*/
export async function runAgent({
logger,
state,
agentRunnable,
config,
kbDataClient,
}: RunAgentParams): Promise<Partial<AgentState>> {
logger.debug(() => `${NodeType.AGENT}: Node state:\n${JSON.stringify(state, null, 2)}`);
const knowledgeHistory = await kbDataClient?.getRequiredKnowledgeBaseDocumentEntries();
const agentOutcome = await agentRunnable.withConfig({ tags: [AGENT_NODE_TAG] }).invoke(
{
...state,
knowledge_history: `${KNOWLEDGE_HISTORY_PREFIX}\n${
knowledgeHistory?.length
? JSON.stringify(knowledgeHistory.map((e) => e.text))
: NO_KNOWLEDGE_HISTORY
}`,
// prepend any user prompt (gemini)
input: formatLatestUserMessage(state.input, state.llmType),
chat_history: state.messages, // TODO: Message de-dupe with ...state spread

View file

@ -8,8 +8,10 @@
const YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT =
'You are a security analyst and expert in resolving security incidents. Your role is to assist by answering questions about Elastic Security.';
const IF_YOU_DONT_KNOW_THE_ANSWER = 'Do not answer questions unrelated to Elastic Security.';
export const KNOWLEDGE_HISTORY =
'If available, use the Knowledge History provided to try and answer the question. If not provided, you can try and query for additional knowledge via the KnowledgeBaseRetrievalTool.';
export const DEFAULT_SYSTEM_PROMPT = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}`;
export const DEFAULT_SYSTEM_PROMPT = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER} ${KNOWLEDGE_HISTORY}`;
// system prompt from @afirstenberg
const BASE_GEMINI_PROMPT =
'You are an assistant that is an expert at using tools and Elastic Security, doing your best to use these tools to answer questions or follow instructions. It is very important to use tools to answer the question or follow the instructions rather than coming up with your own answer. Tool calls are good. Sometimes you may need to make several tool calls to accomplish the task or get an answer to the question that was asked. Use as many tool calls as necessary.';
@ -19,7 +21,7 @@ export const GEMINI_SYSTEM_PROMPT = `${BASE_GEMINI_PROMPT} ${KB_CATCH}`;
export const BEDROCK_SYSTEM_PROMPT = `Use tools as often as possible, as they have access to the latest data and syntax. Always return value from ESQLKnowledgeBaseTool as is. Never return <thinking> tags in the response, but make sure to include <result> tags content in the response. Do not reflect on the quality of the returned search results in your response.`;
export const GEMINI_USER_PROMPT = `Now, always using the tools at your disposal, step by step, come up with a response to this request:\n\n`;
export const STRUCTURED_SYSTEM_PROMPT = `Respond to the human as helpfully and accurately as possible. You have access to the following tools:
export const STRUCTURED_SYSTEM_PROMPT = `Respond to the human as helpfully and accurately as possible. ${KNOWLEDGE_HISTORY} You have access to the following tools:
{tools}

View file

@ -17,6 +17,7 @@ import {
export const formatPrompt = (prompt: string, additionalPrompt?: string) =>
ChatPromptTemplate.fromMessages([
['system', additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt],
['placeholder', '{knowledge_history}'],
['placeholder', '{chat_history}'],
['human', '{input}'],
['placeholder', '{agent_scratchpad}'],
@ -39,6 +40,7 @@ export const geminiToolCallingAgentPrompt = formatPrompt(systemPrompts.gemini);
export const formatPromptStructured = (prompt: string, additionalPrompt?: string) =>
ChatPromptTemplate.fromMessages([
['system', additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt],
['placeholder', '{knowledge_history}'],
['placeholder', '{chat_history}'],
[
'human',

View file

@ -22,11 +22,18 @@ import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/
import { performChecks } from '../../helpers';
import { KNOWLEDGE_BASE_ENTRIES_TABLE_MAX_PAGE_SIZE } from '../../../../common/constants';
import { EsKnowledgeBaseEntrySchema } from '../../../ai_assistant_data_clients/knowledge_base/types';
import {
EsKnowledgeBaseEntrySchema,
UpdateKnowledgeBaseEntrySchema,
} from '../../../ai_assistant_data_clients/knowledge_base/types';
import { ElasticAssistantPluginRouter } from '../../../types';
import { buildResponse } from '../../utils';
import { transformESSearchToKnowledgeBaseEntry } from '../../../ai_assistant_data_clients/knowledge_base/transforms';
import { transformToCreateSchema } from '../../../ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry';
import {
getUpdateScript,
transformToCreateSchema,
transformToUpdateSchema,
} from '../../../ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry';
export interface BulkOperationError {
message: string;
@ -210,7 +217,17 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug
})
),
documentsToDelete: body.delete?.ids,
documentsToUpdate: [], // TODO: Support bulk update
documentsToUpdate: body.update?.map((entry) =>
// TODO: KB-RBAC check, required when users != null as entry will either be created globally if empty
transformToUpdateSchema({
user: authenticatedUser,
updatedAt: changedAt,
entry,
global: entry.users != null && entry.users.length === 0,
})
),
getUpdateScript: (entry: UpdateKnowledgeBaseEntrySchema) =>
getUpdateScript({ entry, isPatch: true }),
authenticatedUser,
});
const created =

View file

@ -66,7 +66,7 @@ export const createKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
logger.debug(() => `Creating KB Entry:\n${JSON.stringify(request.body)}`);
const createResponse = await kbDataClient?.createKnowledgeBaseEntry({
knowledgeBaseEntry: request.body,
// TODO: KB-RBAC check, required when users != null as entry will either be created globally if empty, or for specific users (only admin API feature)
// TODO: KB-RBAC check, required when users != null as entry will either be created globally if empty
global: request.body.users != null && request.body.users.length === 0,
});

View file

@ -74,7 +74,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout
});
const currentUser = ctx.elasticAssistant.getCurrentUser();
const userFilter = getKBUserFilter(currentUser);
const systemFilter = ` AND kb_resource:"user"`;
const systemFilter = ` AND (kb_resource:"user" OR type:"index")`;
const additionalFilter = query.filter ? ` AND ${query.filter}` : '';
const result = await kbDataClient?.findDocuments<EsKnowledgeBaseEntrySchema>({
@ -166,7 +166,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout
body: {
perPage: result.perPage,
page: result.page,
total: result.total,
total: result.total + systemEntries.length,
data: [...transformESSearchToKnowledgeBaseEntry(result.data), ...systemEntries],
},
});

View file

@ -77,6 +77,11 @@ describe('ManagementSettings', () => {
securitySolutionAssistant: { 'ai-assistant': false },
},
},
data: {
dataViews: {
getIndices: jest.fn(),
},
},
security: {
userProfiles: {
getCurrent: jest.fn().mockResolvedValue({ data: { color: 'blue', initials: 'P' } }),

View file

@ -37,6 +37,7 @@ export const ManagementSettings = React.memo(() => {
securitySolutionAssistant: { 'ai-assistant': securityAIAssistantEnabled },
},
},
data: { dataViews },
security,
} = useKibana().services;
@ -46,8 +47,8 @@ export const ManagementSettings = React.memo(() => {
security?.userProfiles.getCurrent<{ avatar: UserAvatar }>({
dataPath: 'avatar',
}),
select: (data) => {
return data.data.avatar;
select: (d) => {
return d.data.avatar;
},
keepPreviousData: true,
refetchOnWindowFocus: false,
@ -79,7 +80,12 @@ export const ManagementSettings = React.memo(() => {
}
if (conversations) {
return <AssistantSettingsManagement selectedConversation={currentConversation} />;
return (
<AssistantSettingsManagement
selectedConversation={currentConversation}
dataViews={dataViews}
/>
);
}
return <></>;