Skip to content

Commit

Permalink
[Security Assistant] Knowledge base switch to use semantic_text (el…
Browse files Browse the repository at this point in the history
…astic#197007)

## Summary

- `text_expansion` is deprecated, use `semantic_text` instead
- fix KB index entry form field options 
- explicitly create inference endpoint on KB setup if
`assistantKnowledgeBaseByDefault` is true
- when upgrade from v1 update KB ingest pipeline and remove unnecessary
processor, but keep the pipeline for the backward compatibility
- switch to use `doc` update for KB entries due to the limitations od
`semantic_text`
https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html#update-script
- split loading Security labs content into smaller chunks

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Pedro Jaramillo <[email protected]>
  • Loading branch information
3 people authored Oct 30, 2024
1 parent a0a4b42 commit a8c54f2
Show file tree
Hide file tree
Showing 33 changed files with 485 additions and 195 deletions.
2 changes: 2 additions & 0 deletions packages/kbn-data-stream-adapter/src/field_maps/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export type FieldMap<T extends string = string> = Record<
scaling_factor?: number;
dynamic?: boolean | 'strict';
properties?: Record<string, { type: string }>;
inference_id?: string;
copy_to?: string;
}
>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,5 @@ export const ReadKnowledgeBaseResponse = z.object({
is_setup_in_progress: z.boolean().optional(),
pipeline_exists: z.boolean().optional(),
security_labs_exists: z.boolean().optional(),
user_data_exists: z.boolean().optional(),
});
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ paths:
type: boolean
security_labs_exists:
type: boolean
user_data_exists:
type: boolean
400:
description: Generic Error
content:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface UseKnowledgeBaseEntriesParams {
signal?: AbortSignal | undefined;
toasts?: IToasts;
enabled?: boolean; // For disabling if FF is off
isRefetching?: boolean; // For enabling polling
}

const defaultQuery: FindKnowledgeBaseEntriesRequestQuery = {
Expand Down Expand Up @@ -56,6 +57,7 @@ export const useKnowledgeBaseEntries = ({
signal,
toasts,
enabled = false,
isRefetching = false,
}: UseKnowledgeBaseEntriesParams) =>
useQuery(
KNOWLEDGE_BASE_ENTRY_QUERY_KEY,
Expand All @@ -73,6 +75,7 @@ export const useKnowledgeBaseEntries = ({
enabled,
keepPreviousData: true,
initialData: { page: 1, perPage: 100, total: 0, data: [] },
refetchInterval: isRefetching ? 30000 : false,
onError: (error: IHttpFetchError<ResponseErrorBody>) => {
if (error.name !== 'AbortError') {
toasts?.addError(error, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const statusResponse = {
elser_exists: true,
index_exists: true,
pipeline_exists: true,
security_labs_exists: true,
};

const http = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const useKnowledgeBaseStatus = ({
{
retry: false,
keepPreviousData: true,
// Polling interval for Knowledge Base setup in progress
refetchInterval: (data) => (data?.is_setup_in_progress ? 30000 : false),
// Deprecated, hoist to `queryCache` w/in `QueryClient. See: https://stackoverflow.com/a/76961109
onError: (error: IHttpFetchError<ResponseErrorBody>) => {
if (error.name !== 'AbortError') {
Expand Down Expand Up @@ -86,12 +88,12 @@ export const useInvalidateKnowledgeBaseStatus = () => {
*
* @param kbStatus ReadKnowledgeBaseResponse
*/
export const isKnowledgeBaseSetup = (kbStatus: ReadKnowledgeBaseResponse | undefined): boolean => {
return (
(kbStatus?.elser_exists &&
kbStatus?.security_labs_exists &&
kbStatus?.index_exists &&
kbStatus?.pipeline_exists) ??
false
);
};
export const isKnowledgeBaseSetup = (kbStatus: ReadKnowledgeBaseResponse | undefined): boolean =>
(kbStatus?.elser_exists &&
kbStatus?.index_exists &&
kbStatus?.pipeline_exists &&
// Allows to use UI while importing Security Labs docs
(kbStatus?.security_labs_exists ||
kbStatus?.is_setup_in_progress ||
kbStatus?.user_data_exists)) ??
false;
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ export const KnowledgeBaseSettings: React.FC<Props> = React.memo(
const isSecurityLabsEnabled = kbStatus?.security_labs_exists ?? false;
const isKnowledgeBaseSetup =
(isElserEnabled &&
isSecurityLabsEnabled &&
kbStatus?.index_exists &&
kbStatus?.pipeline_exists) ??
kbStatus?.pipeline_exists &&
(isSecurityLabsEnabled || kbStatus?.user_data_exists)) ??
false;
const isSetupInProgress = kbStatus?.is_setup_in_progress ?? false;
const isSetupAvailable = kbStatus?.is_setup_available ?? false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
http,
toasts,
enabled: enableKnowledgeBaseByDefault,
isRefetching: kbStatus?.is_setup_in_progress,
});

// Flyout Save/Cancel Actions
Expand Down Expand Up @@ -190,13 +191,15 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
indices.push(entry.index);
}
});
return dataViews.getExistingIndices(indices);

return indices.length ? dataViews.getExistingIndices(indices) : Promise.resolve([]);
}, [entries.data]);

const { getColumns } = useKnowledgeBaseTable();
const columns = useMemo(
() =>
getColumns({
isKbSetupInProgress: kbStatus?.is_setup_in_progress ?? false,
existingIndices,
isDeleteEnabled: (entry: KnowledgeBaseEntryResponse) => {
return (
Expand All @@ -219,7 +222,14 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
openFlyout();
},
}),
[entries.data, existingIndices, getColumns, hasManageGlobalKnowledgeBase, openFlyout]
[
entries.data,
existingIndices,
getColumns,
hasManageGlobalKnowledgeBase,
kbStatus?.is_setup_in_progress,
openFlyout,
]
);

// Refresh button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(
dataViews.getFieldsForWildcard({
pattern: entry?.index ?? '',
}),
[]
[entry?.index]
);

const fieldOptions = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,10 @@ export const MISSING_INDEX_TOOLTIP_CONTENT = i18n.translate(
'The index assigned to this knowledge base entry is unavailable. Check the permissions on the configured index, or that the index has not been deleted. You can update the index to be used for this knowledge entry, or delete the entry entirely.',
}
);

export const SECURITY_LABS_NOT_FULLY_LOADED = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.securityLabsNotFullyLoadedTooltipContent',
{
defaultMessage: 'Security Labs content is not fully loaded. Click to reload.',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
EuiBasicTableColumn,
EuiIcon,
EuiText,
EuiLoadingSpinner,
EuiToolTip,
} from '@elastic/eui';
import { css } from '@emotion/react';
Expand All @@ -29,11 +30,16 @@ import * as i18n from './translations';
import { BadgesColumn } from '../../assistant/common/components/assistant_settings_management/badges';
import { useInlineActions } from '../../assistant/common/components/assistant_settings_management/inline_actions';
import { isSystemEntry } from './helpers';
import { SetupKnowledgeBaseButton } from '../setup_knowledge_base_button';

const AuthorColumn = ({ entry }: { entry: KnowledgeBaseEntryResponse }) => {
const { userProfileService } = useAssistantContext();

const userProfile = useAsync(async () => {
if (isSystemEntry(entry) || entry.createdBy === 'unknown') {
return;
}

const profile = await userProfileService?.bulkGet<{ avatar: UserProfileAvatarData }>({
uids: new Set([entry.createdBy]),
dataPath: 'avatar',
Expand All @@ -45,7 +51,7 @@ const AuthorColumn = ({ entry }: { entry: KnowledgeBaseEntryResponse }) => {
() => userProfile?.value?.username ?? 'Unknown',
[userProfile?.value?.username]
);
const userAvatar = userProfile.value?.avatar;
const userAvatar = userProfile?.value?.avatar;
const badgeItem = isSystemEntry(entry) ? 'Elastic' : userName;
const userImage = isSystemEntry(entry) ? (
<EuiIcon
Expand Down Expand Up @@ -142,12 +148,14 @@ export const useKnowledgeBaseTable = () => {
isEditEnabled,
onDeleteActionClicked,
onEditActionClicked,
isKbSetupInProgress,
}: {
existingIndices?: string[];
isDeleteEnabled: (entry: KnowledgeBaseEntryResponse) => boolean;
isEditEnabled: (entry: KnowledgeBaseEntryResponse) => boolean;
onDeleteActionClicked: (entry: KnowledgeBaseEntryResponse) => void;
onEditActionClicked: (entry: KnowledgeBaseEntryResponse) => void;
isKbSetupInProgress: boolean;
}): Array<EuiBasicTableColumn<KnowledgeBaseEntryResponse>> => {
return [
{
Expand Down Expand Up @@ -180,11 +188,27 @@ export const useKnowledgeBaseTable = () => {
{
name: i18n.COLUMN_ENTRIES,
render: (entry: KnowledgeBaseEntryResponse) => {
return isSystemEntry(entry)
? entry.text
: entry.type === DocumentEntryType.value
? '1'
: '-';
return isSystemEntry(entry) ? (
<>
{`${entry.text}`}
{isKbSetupInProgress ? (
<EuiLoadingSpinner
size="m"
css={css`
margin-left: 8px;
`}
/>
) : (
<EuiToolTip content={i18n.SECURITY_LABS_NOT_FULLY_LOADED}>
<SetupKnowledgeBaseButton display="refresh" />
</EuiToolTip>
)}
</>
) : entry.type === DocumentEntryType.value ? (
'1'
) : (
'-'
);
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
*/

import React, { useCallback } from 'react';
import { EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
import { EuiButton, EuiButtonIcon, EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { css } from '@emotion/react';
import { useAssistantContext } from '../..';
import { useSetupKnowledgeBase } from '../assistant/api/knowledge_base/use_setup_knowledge_base';
import { useKnowledgeBaseStatus } from '../assistant/api/knowledge_base/use_knowledge_base_status';

interface Props {
display?: 'mini';
display?: 'mini' | 'refresh';
}

/**
Expand Down Expand Up @@ -48,6 +49,23 @@ export const SetupKnowledgeBaseButton: React.FC<Props> = React.memo(({ display }
})
: undefined;

if (display === 'refresh') {
return (
<EuiButtonIcon
color="primary"
data-test-subj="setup-knowledge-base-button"
disabled={!kbStatus?.is_setup_available}
isLoading={isSetupInProgress}
iconType="refresh"
onClick={onInstallKnowledgeBase}
size="xs"
css={css`
margin-left: 8px;
`}
/>
);
}

return (
<EuiToolTip position={'bottom'} content={toolTipContent}>
{display === 'mini' ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@ export const mSearchQueryBody: MsearchQueryBody = {
],
must: [
{
text_expansion: {
'vector.tokens': {
model_id: '.elser_model_2',
model_text:
'Generate an ESQL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called "follow_up" that contains a value of "true", otherwise, it should contain "false". The user names should also be enriched with their respective group names.',
},
semantic: {
field: 'semantic_text',
query:
'Generate an ESQL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called "follow_up" that contains a value of "true", otherwise, it should contain "false". The user names should also be enriched with their respective group names.',
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,10 @@ export const mockVectorSearchQuery: QueryDslQueryContainer = {
],
must: [
{
text_expansion: {
'vector.tokens': {
model_id: '.elser_model_2',
model_text:
'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called "follow_up" that contains a value of "true", otherwise, it should contain "false". The user names should also be enriched with their respective group names.',
},
semantic: {
field: 'semantic_text',
query:
'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called "follow_up" that contains a value of "true", otherwise, it should contain "false". The user names should also be enriched with their respective group names.',
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ export const getUpdateScript = ({
isPatch?: boolean;
}) => {
return {
source: `
script: {
source: `
if (params.assignEmpty == true || params.containsKey('allowed')) {
ctx._source.allowed = params.allowed;
}
Expand All @@ -108,11 +109,12 @@ export const getUpdateScript = ({
}
ctx._source.updated_at = params.updated_at;
`,
lang: 'painless',
params: {
...anonymizationField, // 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),
lang: 'painless',
params: {
...anonymizationField, // 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),
},
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export const getUpdateScript = ({
isPatch?: boolean;
}) => {
return {
source: `
script: {
source: `
if (params.assignEmpty == true || params.containsKey('api_config')) {
if (ctx._source.api_config != null) {
if (params.assignEmpty == true || params.api_config.containsKey('connector_id')) {
Expand Down Expand Up @@ -70,11 +71,12 @@ export const getUpdateScript = ({
}
ctx._source.updated_at = params.updated_at;
`,
lang: 'painless',
params: {
...conversation, // 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),
lang: 'painless',
params: {
...conversation, // 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),
},
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const updateConversation = async ({
},
},
refresh: true,
script: getUpdateScript({ conversation: params, isPatch }),
script: getUpdateScript({ conversation: params, isPatch }).script,
});

if (response.failures && response.failures.length > 0) {
Expand Down
Loading

0 comments on commit a8c54f2

Please sign in to comment.