[Security AI Assistant] Persist prompts (#187040)

Moving prompts persistence layer from the local storage to the server
side data stream `.kibana-elastic-ai-assistant-prompts`

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yuliia Naumenko 2024-07-03 10:28:15 -07:00 committed by GitHub
parent aad2239c32
commit 0a0bb1498e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 1629 additions and 623 deletions

View file

@ -52,3 +52,6 @@ export * from './knowledge_base/bulk_crud_knowledge_base_route.gen';
export * from './knowledge_base/common_attributes.gen';
export * from './knowledge_base/crud_knowledge_base_route.gen';
export * from './knowledge_base/find_knowledge_base_entries_route.gen';
export * from './prompts/find_prompts_route.gen';
export { PromptResponse, PromptTypeEnum } from './prompts/bulk_crud_prompts_route.gen';

View file

@ -34,6 +34,14 @@ export const PromptDetailsInError = z.object({
name: z.string().optional(),
});
/**
* Prompt type
*/
export type PromptType = z.infer<typeof PromptType>;
export const PromptType = z.enum(['system', 'quick']);
export type PromptTypeEnum = typeof PromptType.enum;
export const PromptTypeEnum = PromptType.enum;
export type NormalizedPromptError = z.infer<typeof NormalizedPromptError>;
export const NormalizedPromptError = z.object({
message: z.string(),
@ -47,11 +55,13 @@ export const PromptResponse = z.object({
id: NonEmptyString,
timestamp: NonEmptyString.optional(),
name: z.string(),
promptType: z.string(),
promptType: PromptType,
content: z.string(),
categories: z.array(z.string()).optional(),
color: z.string().optional(),
isNewConversationDefault: z.boolean().optional(),
isDefault: z.boolean().optional(),
isShared: z.boolean().optional(),
consumer: z.string().optional(),
updatedAt: z.string().optional(),
updatedBy: z.string().optional(),
createdAt: z.string().optional(),
@ -107,20 +117,24 @@ export const BulkActionBase = z.object({
export type PromptCreateProps = z.infer<typeof PromptCreateProps>;
export const PromptCreateProps = z.object({
name: z.string(),
promptType: z.string(),
promptType: PromptType,
content: z.string(),
color: z.string().optional(),
categories: z.array(z.string()).optional(),
isNewConversationDefault: z.boolean().optional(),
isDefault: z.boolean().optional(),
isShared: z.boolean().optional(),
consumer: z.string().optional(),
});
export type PromptUpdateProps = z.infer<typeof PromptUpdateProps>;
export const PromptUpdateProps = z.object({
id: z.string(),
content: z.string().optional(),
color: z.string().optional(),
categories: z.array(z.string()).optional(),
isNewConversationDefault: z.boolean().optional(),
isDefault: z.boolean().optional(),
isShared: z.boolean().optional(),
consumer: z.string().optional(),
});
export type PerformBulkActionRequestBody = z.infer<typeof PerformBulkActionRequestBody>;

View file

@ -78,6 +78,13 @@ components:
required:
- id
PromptType:
type: string
description: Prompt type
enum:
- system
- quick
NormalizedPromptError:
type: object
properties:
@ -111,15 +118,21 @@ components:
name:
type: string
promptType:
type: string
$ref: '#/components/schemas/PromptType'
content:
type: string
categories:
type: array
items:
type: string
color:
type: string
isNewConversationDefault:
type: boolean
isDefault:
type: boolean
isShared:
type: boolean
consumer:
type: string
updatedAt:
type: string
updatedBy:
@ -231,15 +244,21 @@ components:
name:
type: string
promptType:
type: string
$ref: '#/components/schemas/PromptType'
content:
type: string
color:
type: string
categories:
type: array
items:
type: string
isNewConversationDefault:
type: boolean
isDefault:
type: boolean
isShared:
type: boolean
consumer:
type: string
PromptUpdateProps:
type: object
@ -250,9 +269,15 @@ components:
type: string
content:
type: string
color:
type: string
categories:
type: array
items:
type: string
isNewConversationDefault:
type: boolean
isDefault:
type: boolean
isShared:
type: boolean
consumer:
type: string

View file

@ -11,6 +11,7 @@ import { API_ERROR } from '../translations';
import { getOptionalRequestParams } from '../helpers';
import { TraceOptions } from '../types';
export * from './conversations';
export * from './prompts';
export interface FetchConnectorExecuteAction {
conversationId: string;

View file

@ -0,0 +1,137 @@
/*
* 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 {
API_VERSIONS,
ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION,
} from '@kbn/elastic-assistant-common';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { IToasts } from '@kbn/core-notifications-browser';
import { bulkUpdatePrompts } from './bulk_update_prompts';
import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
const prompt1 = {
id: 'field1',
content: 'Prompt 1',
name: 'test',
promptType: PromptTypeEnum.system,
};
const prompt2 = {
...prompt1,
id: 'field2',
content: 'Prompt 2',
name: 'test2',
promptType: PromptTypeEnum.system,
};
const toasts = {
addError: jest.fn(),
};
describe('bulkUpdatePrompts', () => {
let httpMock: ReturnType<typeof httpServiceMock.createSetupContract>;
beforeEach(() => {
httpMock = httpServiceMock.createSetupContract();
jest.clearAllMocks();
});
it('should send a POST request with the correct parameters and receive a successful response', async () => {
const promptsActions = {
create: [],
update: [],
delete: { ids: [] },
};
await bulkUpdatePrompts(httpMock, promptsActions);
expect(httpMock.fetch).toHaveBeenCalledWith(ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, {
method: 'POST',
version: API_VERSIONS.internal.v1,
body: JSON.stringify({
create: [],
update: [],
delete: { ids: [] },
}),
});
});
it('should transform the prompts dictionary to an array of fields to create', async () => {
const promptsActions = {
create: [prompt1, prompt2],
update: [],
delete: { ids: [] },
};
await bulkUpdatePrompts(httpMock, promptsActions);
expect(httpMock.fetch).toHaveBeenCalledWith(ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, {
method: 'POST',
version: API_VERSIONS.internal.v1,
body: JSON.stringify({
create: [prompt1, prompt2],
update: [],
delete: { ids: [] },
}),
});
});
it('should transform the prompts dictionary to an array of fields to update', async () => {
const promptsActions = {
update: [prompt1, prompt2],
delete: { ids: [] },
};
await bulkUpdatePrompts(httpMock, promptsActions);
expect(httpMock.fetch).toHaveBeenCalledWith(ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, {
method: 'POST',
version: API_VERSIONS.internal.v1,
body: JSON.stringify({
update: [prompt1, prompt2],
delete: { ids: [] },
}),
});
});
it('should throw an error with the correct message when receiving an unsuccessful response', async () => {
httpMock.fetch.mockResolvedValue({
success: false,
attributes: {
errors: [
{
statusCode: 400,
message: 'Error updating prompt',
prompts: [{ id: prompt1.id, name: prompt1.content }],
},
],
},
});
const promptsActions = {
create: [],
update: [prompt1],
delete: { ids: [] },
};
await bulkUpdatePrompts(httpMock, promptsActions, toasts as unknown as IToasts);
expect(toasts.addError.mock.calls[0][0]).toEqual(
new Error('Error message: Error updating prompt for prompt Prompt 1')
);
});
it('should handle cases where result.attributes.errors is undefined', async () => {
httpMock.fetch.mockResolvedValue({
success: false,
attributes: {},
});
const promptsActions = {
create: [],
update: [],
delete: { ids: [] },
};
await bulkUpdatePrompts(httpMock, promptsActions, toasts as unknown as IToasts);
expect(toasts.addError.mock.calls[0][0]).toEqual(new Error(''));
});
});

View file

@ -0,0 +1,54 @@
/*
* 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';
import { HttpSetup, IToasts } from '@kbn/core/public';
import {
ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION,
API_VERSIONS,
} from '@kbn/elastic-assistant-common';
import {
PerformBulkActionRequestBody,
PerformBulkActionResponse,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
export const bulkUpdatePrompts = async (
http: HttpSetup,
prompts: PerformBulkActionRequestBody,
toasts?: IToasts
) => {
try {
const result = await http.fetch<PerformBulkActionResponse>(
ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION,
{
method: 'POST',
version: API_VERSIONS.internal.v1,
body: JSON.stringify(prompts),
}
);
if (!result.success) {
const serverError = result.attributes.errors
?.map(
(e) =>
`${e.status_code ? `Error code: ${e.status_code}. ` : ''}Error message: ${
e.message
} for prompt ${e.prompts.map((c) => c.name).join(',')}`
)
.join(',\n');
throw new Error(serverError);
}
return result;
} catch (error) {
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
title: i18n.translate('xpack.elasticAssistant.prompts.bulkActionspromptsError', {
defaultMessage: 'Error updating prompts {error}',
values: { error },
}),
});
}
};

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './bulk_update_prompts';
export * from './use_fetch_prompts';

View file

@ -0,0 +1,61 @@
/*
* 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 { act, renderHook } from '@testing-library/react-hooks';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';
import React from 'react';
import { useFetchPrompts } from './use_fetch_prompts';
import { HttpSetup } from '@kbn/core-http-browser';
import { useAssistantContext } from '../../../assistant_context';
import { API_VERSIONS, defaultAssistantFeatures } from '@kbn/elastic-assistant-common';
const http = {
fetch: jest.fn().mockResolvedValue(defaultAssistantFeatures),
} as unknown as HttpSetup;
jest.mock('../../../assistant_context');
const createWrapper = () => {
const queryClient = new QueryClient();
// eslint-disable-next-line react/display-name
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe('useFetchPrompts', () => {
(useAssistantContext as jest.Mock).mockReturnValue({
http,
assistantAvailability: {
isAssistantEnabled: true,
},
});
it(`should make http request to fetch prompts`, async () => {
renderHook(() => useFetchPrompts(), {
wrapper: createWrapper(),
});
await act(async () => {
const { waitForNextUpdate } = renderHook(() => useFetchPrompts());
await waitForNextUpdate();
expect(http.fetch).toHaveBeenCalledWith('/internal/elastic_assistant/prompts/_find', {
method: 'GET',
query: {
page: 1,
per_page: 1000,
filter: 'consumer:*',
},
version: API_VERSIONS.internal.v1,
signal: undefined,
});
expect(http.fetch).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,101 @@
/*
* 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 { FindPromptsResponse } from '@kbn/elastic-assistant-common/impl/schemas/prompts/find_prompts_route.gen';
import { useQuery } from '@tanstack/react-query';
import { API_VERSIONS, ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND } from '@kbn/elastic-assistant-common';
import { HttpSetup, IToasts } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { useAssistantContext } from '../../../assistant_context';
export interface UseFetchPromptsParams {
signal?: AbortSignal | undefined;
consumer?: string;
}
/**
* API call for fetching prompts for current spaceId
*
* @param {Object} options - The options object.
* @param {string} options.consumer - prompt consumer
* @param {AbortSignal} [options.signal] - AbortSignal
*
* @returns {useQuery} hook for getting the status of the prompts
*/
export const useFetchPrompts = (payload?: UseFetchPromptsParams) => {
const {
assistantAvailability: { isAssistantEnabled },
http,
} = useAssistantContext();
const QUERY = {
page: 1,
per_page: 1000, // Continue use in-memory paging till the new design will be ready
filter: `consumer:${payload?.consumer ?? '*'}`,
};
const CACHING_KEYS = [
ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND,
QUERY.page,
QUERY.per_page,
QUERY.filter,
API_VERSIONS.internal.v1,
];
return useQuery<FindPromptsResponse, unknown, FindPromptsResponse>(
CACHING_KEYS,
async () =>
http.fetch(ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, {
method: 'GET',
version: API_VERSIONS.internal.v1,
query: QUERY,
signal: payload?.signal,
}),
{
initialData: {
data: [],
page: 1,
perPage: 5,
total: 0,
},
placeholderData: {
data: [],
page: 1,
perPage: 5,
total: 0,
},
keepPreviousData: true,
enabled: isAssistantEnabled,
}
);
};
export const getPrompts = async ({
http,
signal,
toasts,
}: {
http: HttpSetup;
toasts: IToasts;
signal?: AbortSignal | undefined;
}) => {
try {
return await http.fetch<FindPromptsResponse>(ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, {
method: 'GET',
version: API_VERSIONS.internal.v1,
signal,
});
} catch (error) {
toasts.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
title: i18n.translate('xpack.elasticAssistant.prompts.getPromptsError', {
defaultMessage: 'Error fetching prompts',
}),
});
throw error;
}
};

View file

@ -6,6 +6,7 @@
*/
import React, { useState, useMemo, useCallback } from 'react';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import {
EuiFlexGroup,
EuiFlexItem,
@ -47,6 +48,9 @@ interface OwnProps {
refetchConversationsState: () => Promise<void>;
onConversationCreate: () => Promise<void>;
isAssistantEnabled: boolean;
refetchPrompts?: (
options?: RefetchOptions & RefetchQueryFilters<unknown>
) => Promise<QueryObserverResult<unknown, unknown>>;
}
type Props = OwnProps;
@ -74,6 +78,7 @@ export const AssistantHeaderFlyout: React.FC<Props> = ({
refetchConversationsState,
onConversationCreate,
isAssistantEnabled,
refetchPrompts,
}) => {
const showAnonymizedValuesChecked = useMemo(
() =>
@ -164,6 +169,7 @@ export const AssistantHeaderFlyout: React.FC<Props> = ({
conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
isFlyoutMode={true}
refetchPrompts={refetchPrompts}
/>
</EuiFlexItem>

View file

@ -38,6 +38,7 @@ const testProps = {
refetchConversationsState: jest.fn(),
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
refetchAnonymizationFieldsResults: jest.fn(),
allPrompts: [],
};
jest.mock('../../connectorland/use_load_connectors', () => ({

View file

@ -14,9 +14,11 @@ import {
EuiSwitch,
EuiToolTip,
} from '@elastic/eui';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import { css } from '@emotion/react';
import { DocLinksStart } from '@kbn/core-doc-links-browser';
import { isEmpty } from 'lodash';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { AIConnector } from '../../connectorland/connector_selector';
import { Conversation } from '../../..';
import { AssistantTitle } from '../assistant_title';
@ -40,6 +42,10 @@ interface OwnProps {
conversations: Record<string, Conversation>;
conversationsLoaded: boolean;
refetchConversationsState: () => Promise<void>;
allPrompts: PromptResponse[];
refetchPrompts?: (
options?: RefetchOptions & RefetchQueryFilters<unknown>
) => Promise<QueryObserverResult<unknown, unknown>>;
}
type Props = OwnProps;
@ -64,6 +70,8 @@ export const AssistantHeader: React.FC<Props> = ({
conversations,
conversationsLoaded,
refetchConversationsState,
allPrompts,
refetchPrompts,
}) => {
const showAnonymizedValuesChecked = useMemo(
() =>
@ -122,6 +130,7 @@ export const AssistantHeader: React.FC<Props> = ({
isDisabled={isDisabled}
conversations={conversations}
onConversationDeleted={onConversationDeleted}
allPrompts={allPrompts}
/>
<>
@ -156,6 +165,7 @@ export const AssistantHeader: React.FC<Props> = ({
conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
isFlyoutMode={false}
refetchPrompts={refetchPrompts}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -8,18 +8,18 @@
import React, { useCallback } from 'react';
import { HttpSetup } from '@kbn/core-http-browser';
import { i18n } from '@kbn/i18n';
import { Replacements } from '@kbn/elastic-assistant-common';
import { PromptResponse, Replacements } from '@kbn/elastic-assistant-common';
import type { ClientMessage } from '../../assistant_context/types';
import { SelectedPromptContext } from '../prompt_context/types';
import { useSendMessage } from '../use_send_message';
import { useConversation } from '../use_conversation';
import { getCombinedMessage } from '../prompt/helpers';
import { Conversation, Prompt, useAssistantContext } from '../../..';
import { Conversation, useAssistantContext } from '../../..';
import { getMessageFromRawResponse } from '../helpers';
import { getDefaultSystemPrompt } from '../use_conversation/helpers';
export interface UseChatSendProps {
allSystemPrompts: Prompt[];
allSystemPrompts: PromptResponse[];
currentConversation?: Conversation;
editingSystemPromptId: string | undefined;
http: HttpSetup;

View file

@ -50,6 +50,7 @@ const defaultProps = {
defaultProvider: OpenAiProviderType.OpenAi,
conversations: mockConversations,
onConversationDeleted,
allPrompts: [],
};
describe('Conversation selector', () => {
beforeAll(() => {

View file

@ -18,10 +18,13 @@ import {
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { css } from '@emotion/react';
import {
PromptResponse,
PromptTypeEnum,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { getGenAiConfig } from '../../../connectorland/helpers';
import { AIConnector } from '../../../connectorland/connector_selector';
import { Conversation } from '../../../..';
import { useAssistantContext } from '../../../assistant_context';
import * as i18n from './translations';
import { DEFAULT_CONVERSATION_TITLE } from '../../use_conversation/translations';
import { useConversation } from '../../use_conversation';
@ -35,6 +38,7 @@ interface Props {
shouldDisableKeyboardShortcut?: () => boolean;
isDisabled?: boolean;
conversations: Record<string, Conversation>;
allPrompts: PromptResponse[];
}
const getPreviousConversationId = (conversationIds: string[], selectedConversationId: string) => {
@ -64,10 +68,13 @@ export const ConversationSelector: React.FC<Props> = React.memo(
shouldDisableKeyboardShortcut = () => false,
isDisabled = false,
conversations,
allPrompts,
}) => {
const { allSystemPrompts } = useAssistantContext();
const { createConversation } = useConversation();
const allSystemPrompts = useMemo(
() => allPrompts.filter((p) => p.promptType === PromptTypeEnum.system),
[allPrompts]
);
const conversationIds = useMemo(() => Object.keys(conversations), [conversations]);
const conversationOptions = useMemo<ConversationSelectorOption[]>(() => {
return Object.values(conversations).map((conversation) => ({

View file

@ -18,7 +18,8 @@ import React, { useMemo } from 'react';
import { HttpSetup } from '@kbn/core-http-browser';
import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import { Conversation, Prompt } from '../../../..';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../..';
import * as i18n from './translations';
import { AIConnector } from '../../../connectorland/connector_selector';
@ -33,7 +34,7 @@ import { getConversationApiConfig } from '../../use_conversation/helpers';
export interface ConversationSettingsProps {
actionTypeRegistry: ActionTypeRegistryContract;
allSystemPrompts: Prompt[];
allSystemPrompts: PromptResponse[];
connectors?: AIConnector[];
conversationSettings: Record<string, Conversation>;
conversationsSettingsBulkActions: ConversationsBulkActions;

View file

@ -12,7 +12,8 @@ import { HttpSetup } from '@kbn/core-http-browser';
import { FormattedMessage } from '@kbn/i18n-react';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
import { noop } from 'lodash/fp';
import { Conversation, Prompt } from '../../../..';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../..';
import * as i18n from './translations';
import * as i18nModel from '../../../connectorland/models/model_selector/translations';
@ -25,7 +26,7 @@ import { ConversationsBulkActions } from '../../api';
import { getDefaultSystemPrompt } from '../../use_conversation/helpers';
export interface ConversationSettingsEditorProps {
allSystemPrompts: Prompt[];
allSystemPrompts: PromptResponse[];
conversationSettings: Record<string, Conversation>;
conversationsSettingsBulkActions: ConversationsBulkActions;
http: HttpSetup;
@ -268,7 +269,7 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
helpText={i18n.SETTINGS_PROMPT_HELP_TEXT_TITLE}
>
<SelectSystemPrompt
allSystemPrompts={allSystemPrompts}
allPrompts={allSystemPrompts}
compressed
conversation={selectedConversation}
isEditing={true}

View file

@ -6,13 +6,14 @@
*/
import { useCallback, useMemo } from 'react';
import { Conversation, Prompt } from '../../../..';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../..';
import { getDefaultSystemPrompt } from '../../use_conversation/helpers';
import { ConversationsBulkActions } from '../../api';
import { AIConnector } from '../../../connectorland/connector_selector';
interface Props {
allSystemPrompts: Prompt[];
allSystemPrompts: PromptResponse[];
conversationSettings: Record<string, Conversation>;
conversationsSettingsBulkActions: ConversationsBulkActions;
defaultConnector?: AIConnector;

View file

@ -8,13 +8,13 @@
import { EuiPanel, EuiSpacer, EuiConfirmModal, EuiInMemoryTable } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../assistant_context/types';
import { ConversationTableItem, useConversationsTable } from './use_conversations_table';
import { ConversationStreamingSwitch } from '../conversation_settings/conversation_streaming_switch';
import { AIConnector } from '../../../connectorland/connector_selector';
import * as i18n from './translations';
import { Prompt } from '../../types';
import { ConversationsBulkActions } from '../../api';
import { useAssistantContext } from '../../../assistant_context';
import { useConversationDeleted } from '../conversation_settings/use_conversation_deleted';
@ -27,7 +27,7 @@ import { CONVERSATION_TABLE_SESSION_STORAGE_KEY } from '../../../assistant_conte
import { useSessionPagination } from '../../common/components/assistant_settings_management/pagination/use_session_pagination';
import { DEFAULT_PAGE_SIZE } from '../../settings/const';
interface Props {
allSystemPrompts: Prompt[];
allSystemPrompts: PromptResponse[];
assistantStreamingEnabled: boolean;
connectors: AIConnector[] | undefined;
conversationSettings: Record<string, Conversation>;

View file

@ -11,10 +11,10 @@ import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/publ
import { EuiBadge, EuiBasicTableColumn, EuiLink } from '@elastic/eui';
import { FormattedDate } from '@kbn/i18n-react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../assistant_context/types';
import { AIConnector } from '../../../connectorland/connector_selector';
import { getConnectorTypeTitle } from '../../../connectorland/helpers';
import { Prompt } from '../../../..';
import {
getConversationApiConfig,
getInitialDefaultSystemPrompt,
@ -25,7 +25,7 @@ import { RowActions } from '../../common/components/assistant_settings_managemen
const emptyConversations = {};
export interface GetConversationsListParams {
allSystemPrompts: Prompt[];
allSystemPrompts: PromptResponse[];
actionTypeRegistry: ActionTypeRegistryContract;
connectors: AIConnector[] | undefined;
conversations: Record<string, Conversation>;
@ -126,7 +126,7 @@ export const useConversationsTable = () => {
);
const connectorTypeTitle = getConnectorTypeTitle(connector, actionTypeRegistry);
const systemPrompt: Prompt | undefined = allSystemPrompts.find(
const systemPrompt: PromptResponse | undefined = allSystemPrompts.find(
({ id }) => id === conversation.apiConfig?.defaultSystemPromptId
);
const defaultSystemPrompt = getInitialDefaultSystemPrompt({
@ -135,10 +135,10 @@ export const useConversationsTable = () => {
});
const systemPromptTitle =
systemPrompt?.label ||
systemPrompt?.name ||
defaultSystemPrompt?.label ||
defaultSystemPrompt?.name;
systemPrompt?.id ||
defaultSystemPrompt?.name ||
defaultSystemPrompt?.id;
return {
...conversation,

View file

@ -39,6 +39,7 @@ import deepEqual from 'fast-deep-equal';
import { find, isEmpty, uniqBy } from 'lodash';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { useChatSend } from './chat_send/use_chat_send';
import { ChatSend } from './chat_send';
import { BlockBotCallToAction } from './block_bot/cta';
@ -91,6 +92,7 @@ import { getGenAiConfig } from '../connectorland/helpers';
import { AssistantAnimatedIcon } from './assistant_animated_icon';
import { useFetchAnonymizationFields } from './api/anonymization_fields/use_fetch_anonymization_fields';
import { InstallKnowledgeBaseButton } from '../knowledge_base/install_knowledge_base_button';
import { useFetchPrompts } from './api/prompts/use_fetch_prompts';
export interface Props {
conversationTitle?: string;
@ -135,7 +137,6 @@ const AssistantComponent: React.FC<Props> = ({
setLastConversationId,
getLastConversationId,
title,
allSystemPrompts,
baseConversations,
} = useAssistantContext();
@ -182,6 +183,19 @@ const AssistantComponent: React.FC<Props> = ({
isFetched: isFetchedAnonymizationFields,
} = useFetchAnonymizationFields();
const {
data: { data: allPrompts },
refetch: refetchPrompts,
isLoading: isLoadingPrompts,
} = useFetchPrompts();
const allSystemPrompts = useMemo(() => {
if (!isLoadingPrompts) {
return allPrompts.filter((p) => p.promptType === PromptTypeEnum.system);
}
return [];
}, [allPrompts, isLoadingPrompts]);
// Connector details
const { data: connectors, isFetchedAfterMount: areConnectorsFetched } = useLoadConnectors({
http,
@ -397,7 +411,11 @@ const AssistantComponent: React.FC<Props> = ({
// End Scrolling
const selectedSystemPrompt = useMemo(
() => getDefaultSystemPrompt({ allSystemPrompts, conversation: currentConversation }),
() =>
getDefaultSystemPrompt({
allSystemPrompts,
conversation: currentConversation,
}),
[allSystemPrompts, currentConversation]
);
@ -409,20 +427,21 @@ const AssistantComponent: React.FC<Props> = ({
async ({ cId, cTitle }: { cId: string; cTitle: string }) => {
const updatedConv = await refetchResults();
let selectedConversation;
if (cId === '') {
setCurrentConversationId(cTitle);
setEditingSystemPromptId(
getDefaultSystemPrompt({ allSystemPrompts, conversation: updatedConv?.data?.[cTitle] })
?.id
);
selectedConversation = updatedConv?.data?.[cTitle];
setCurrentConversationId(cTitle);
} else {
const refetchedConversation = await refetchCurrentConversation({ cId });
setEditingSystemPromptId(
getDefaultSystemPrompt({ allSystemPrompts, conversation: refetchedConversation })?.id
);
selectedConversation = await refetchCurrentConversation({ cId });
setCurrentConversationId(cId);
}
setEditingSystemPromptId(
getDefaultSystemPrompt({
allSystemPrompts,
conversation: selectedConversation,
})?.id
);
},
[allSystemPrompts, refetchCurrentConversation, refetchResults]
);
@ -639,18 +658,18 @@ const AssistantComponent: React.FC<Props> = ({
setIsSettingsModalVisible={setIsSettingsModalVisible}
setSelectedPromptContexts={setSelectedPromptContexts}
isFlyoutMode={isFlyoutMode}
allSystemPrompts={allSystemPrompts}
/>
</ModalPromptEditorWrapper>
)}
</>
),
[
abortStream,
refetchCurrentConversation,
currentConversation,
editingSystemPromptId,
getComments,
abortStream,
currentConversation,
showAnonymizedValues,
refetchCurrentConversation,
handleRegenerateResponse,
isEnabledKnowledgeBase,
isEnabledRAGAlerts,
@ -658,12 +677,14 @@ const AssistantComponent: React.FC<Props> = ({
currentUserAvatar,
isFlyoutMode,
selectedPromptContextsCount,
editingSystemPromptId,
isNewConversation,
isSettingsModalVisible,
promptContexts,
promptTextPreview,
handleOnSystemPromptSelectionChange,
selectedPromptContexts,
allSystemPrompts,
]
);
@ -859,6 +880,7 @@ const AssistantComponent: React.FC<Props> = ({
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode
allSystemPrompts={allSystemPrompts}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
@ -882,6 +904,7 @@ const AssistantComponent: React.FC<Props> = ({
</EuiPanel>
);
}, [
allSystemPrompts,
comments,
connectorPrompt,
currentConversation,
@ -948,6 +971,7 @@ const AssistantComponent: React.FC<Props> = ({
refetchConversationsState={refetchConversationsState}
onConversationCreate={handleCreateConversation}
isAssistantEnabled={isAssistantEnabled}
refetchPrompts={refetchPrompts}
/>
{/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */}
@ -1080,6 +1104,7 @@ const AssistantComponent: React.FC<Props> = ({
setIsSettingsModalVisible={setIsSettingsModalVisible}
trackPrompt={trackPrompt}
isFlyoutMode={isFlyoutMode}
allPrompts={allPrompts}
/>
</EuiPanel>
)}
@ -1116,6 +1141,8 @@ const AssistantComponent: React.FC<Props> = ({
conversationsLoaded={conversationsLoaded}
onConversationDeleted={handleOnConversationDeleted}
refetchConversationsState={refetchConversationsState}
allPrompts={allPrompts}
refetchPrompts={refetchPrompts}
/>
)}
@ -1194,6 +1221,7 @@ const AssistantComponent: React.FC<Props> = ({
setIsSettingsModalVisible={setIsSettingsModalVisible}
trackPrompt={trackPrompt}
isFlyoutMode={isFlyoutMode}
allPrompts={allPrompts}
/>
)}
</EuiModalFooter>

View file

@ -5,11 +5,10 @@
* 2.0.
*/
import { Replacements, transformRawData } from '@kbn/elastic-assistant-common';
import { Replacements, transformRawData, PromptResponse } from '@kbn/elastic-assistant-common';
import type { ClientMessage } from '../../assistant_context/types';
import { getAnonymizedValue as defaultGetAnonymizedValue } from '../get_anonymized_value';
import type { SelectedPromptContext } from '../prompt_context/types';
import type { Prompt } from '../types';
import { SYSTEM_PROMPT_CONTEXT_NON_I18N } from '../../content/prompts/system/translations';
export const getSystemMessages = ({
@ -17,7 +16,7 @@ export const getSystemMessages = ({
selectedSystemPrompt,
}: {
isNewChat: boolean;
selectedSystemPrompt: Prompt | undefined;
selectedSystemPrompt: PromptResponse | undefined;
}): ClientMessage[] => {
if (!isNewChat || selectedSystemPrompt == null) {
return [];
@ -53,7 +52,7 @@ export function getCombinedMessage({
isNewChat: boolean;
promptText: string;
selectedPromptContexts: Record<string, SelectedPromptContext>;
selectedSystemPrompt: Prompt | undefined;
selectedSystemPrompt: PromptResponse | undefined;
}): ClientMessageWithReplacements {
let replacements: Replacements = currentReplacements ?? {};
const onNewReplacements = (newReplacements: Replacements) => {

View file

@ -7,11 +7,11 @@
import { getPromptById } from './helpers';
import { mockSystemPrompt, mockSuperheroSystemPrompt } from '../../mock/system_prompt';
import type { Prompt } from '../types';
import { PromptResponse } from '@kbn/elastic-assistant-common';
describe('helpers', () => {
describe('getPromptById', () => {
const prompts: Prompt[] = [mockSystemPrompt, mockSuperheroSystemPrompt];
const prompts: PromptResponse[] = [mockSystemPrompt, mockSuperheroSystemPrompt];
it('returns the correct prompt by id', () => {
const result = getPromptById({ prompts, id: mockSuperheroSystemPrompt.id });

View file

@ -5,12 +5,12 @@
* 2.0.
*/
import type { Prompt } from '../types';
import { PromptResponse } from '@kbn/elastic-assistant-common';
export const getPromptById = ({
prompts,
id,
}: {
prompts: Prompt[];
prompts: PromptResponse[];
id: string;
}): Prompt | undefined => prompts.find((p) => p.id === id);
}): PromptResponse | undefined => prompts.find((p) => p.id === id);

View file

@ -40,6 +40,7 @@ const defaultProps: Props = {
setIsSettingsModalVisible: jest.fn(),
setSelectedPromptContexts: jest.fn(),
isFlyoutMode: false,
allSystemPrompts: [],
};
describe('PromptEditorComponent', () => {

View file

@ -10,6 +10,7 @@ import React, { useMemo } from 'react';
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../..';
import type { PromptContext, SelectedPromptContext } from '../prompt_context/types';
import { SystemPrompt } from './system_prompt';
@ -31,6 +32,7 @@ export interface Props {
React.SetStateAction<Record<string, SelectedPromptContext>>
>;
isFlyoutMode: boolean;
allSystemPrompts: PromptResponse[];
}
const PreviewText = styled(EuiText)`
@ -49,12 +51,14 @@ const PromptEditorComponent: React.FC<Props> = ({
setIsSettingsModalVisible,
setSelectedPromptContexts,
isFlyoutMode,
allSystemPrompts,
}) => {
const commentBody = useMemo(
() => (
<>
{isNewConversation && (
<SystemPrompt
allSystemPrompts={allSystemPrompts}
conversation={conversation}
editingSystemPromptId={editingSystemPromptId}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
@ -79,17 +83,18 @@ const PromptEditorComponent: React.FC<Props> = ({
</>
),
[
isNewConversation,
allSystemPrompts,
conversation,
editingSystemPromptId,
isNewConversation,
isSettingsModalVisible,
onSystemPromptSelectionChange,
promptContexts,
promptTextPreview,
selectedPromptContexts,
isSettingsModalVisible,
setIsSettingsModalVisible,
setSelectedPromptContexts,
isFlyoutMode,
promptContexts,
selectedPromptContexts,
setSelectedPromptContexts,
promptTextPreview,
]
);

View file

@ -16,13 +16,13 @@ import { getOptions, getOptionFromPrompt } from './helpers';
describe('helpers', () => {
describe('getOptionFromPrompt', () => {
it('returns an EuiSuperSelectOption with the correct value', () => {
const option = getOptionFromPrompt(mockSystemPrompt);
const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true });
expect(option.value).toBe(mockSystemPrompt.id);
});
it('returns an EuiSuperSelectOption with the correct inputDisplay', () => {
const option = getOptionFromPrompt(mockSystemPrompt);
const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: false });
render(<>{option.inputDisplay}</>);
@ -30,7 +30,7 @@ describe('helpers', () => {
});
it('shows the expected name in the dropdownDisplay', () => {
const option = getOptionFromPrompt(mockSystemPrompt);
const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true });
render(<TestProviders>{option.dropdownDisplay}</TestProviders>);
@ -38,7 +38,7 @@ describe('helpers', () => {
});
it('shows the expected prompt content in the dropdownDisplay', () => {
const option = getOptionFromPrompt(mockSystemPrompt);
const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true });
render(<TestProviders>{option.dropdownDisplay}</TestProviders>);

View file

@ -13,7 +13,7 @@ import styled from 'styled-components';
import { css } from '@emotion/react';
import { isEmpty } from 'lodash/fp';
import type { Prompt } from '../../types';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { EMPTY_PROMPT } from './translations';
const Strong = styled.strong`
@ -26,7 +26,10 @@ export const getOptionFromPrompt = ({
name,
showTitles = false,
isFlyoutMode,
}: Prompt & { showTitles?: boolean }): EuiSuperSelectOption<string> => ({
}: PromptResponse & {
showTitles?: boolean;
isFlyoutMode: boolean;
}): EuiSuperSelectOption<string> => ({
value: id,
inputDisplay: isFlyoutMode ? (
name
@ -60,13 +63,13 @@ export const getOptionFromPrompt = ({
});
interface GetOptionsProps {
prompts: Prompt[] | undefined;
prompts: PromptResponse[] | undefined;
showTitles?: boolean;
isFlyoutMode: boolean;
}
export const getOptions = ({
prompts,
showTitles = false,
isFlyoutMode = false,
isFlyoutMode,
}: GetOptionsProps): Array<EuiSuperSelectOption<string>> =>
prompts?.map((p) => getOptionFromPrompt({ ...p, showTitles, isFlyoutMode })) ?? [];

View file

@ -13,11 +13,11 @@ import { mockSystemPrompt } from '../../../mock/system_prompt';
import { SystemPrompt } from '.';
import { Conversation } from '../../../..';
import { DEFAULT_CONVERSATION_TITLE } from '../../use_conversation/translations';
import { Prompt } from '../../types';
import { TestProviders } from '../../../mock/test_providers/test_providers';
import { TEST_IDS } from '../../constants';
import { useAssistantContext } from '../../../assistant_context';
import { WELCOME_CONVERSATION } from '../../use_conversation/sample_conversations';
import { PromptResponse } from '@kbn/elastic-assistant-common';
const BASE_CONVERSATION: Conversation = {
...WELCOME_CONVERSATION,
@ -32,7 +32,7 @@ const mockConversations = {
[DEFAULT_CONVERSATION_TITLE]: BASE_CONVERSATION,
};
const mockSystemPrompts: Prompt[] = [mockSystemPrompt];
const mockSystemPrompts: PromptResponse[] = [mockSystemPrompt];
const mockUseAssistantContext = {
conversations: mockConversations,
@ -91,6 +91,7 @@ describe('SystemPrompt', () => {
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
);
});
@ -117,11 +118,12 @@ describe('SystemPrompt', () => {
render(
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={BASE_CONVERSATION.id}
editingSystemPromptId={mockSystemPrompt.id}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
);
});
@ -157,6 +159,7 @@ describe('SystemPrompt', () => {
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
);
@ -204,6 +207,7 @@ describe('SystemPrompt', () => {
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
);
@ -265,6 +269,7 @@ describe('SystemPrompt', () => {
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
);
@ -333,6 +338,7 @@ describe('SystemPrompt', () => {
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
);
@ -416,6 +422,7 @@ describe('SystemPrompt', () => {
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
);
@ -481,11 +488,12 @@ describe('SystemPrompt', () => {
<TestProviders>
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={BASE_CONVERSATION.id}
editingSystemPromptId={mockSystemPrompt.id}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
);
@ -500,11 +508,12 @@ describe('SystemPrompt', () => {
<TestProviders>
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={BASE_CONVERSATION.id}
editingSystemPromptId={mockSystemPrompt.id}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
);

View file

@ -10,7 +10,7 @@ import React, { useCallback, useMemo } from 'react';
import { css } from '@emotion/react';
import { isEmpty } from 'lodash/fp';
import { useAssistantContext } from '../../../assistant_context';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../..';
import * as i18n from './translations';
import { SelectSystemPrompt } from './select_system_prompt';
@ -22,6 +22,7 @@ interface Props {
onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
isFlyoutMode: boolean;
allSystemPrompts: PromptResponse[];
}
const SystemPromptComponent: React.FC<Props> = ({
@ -31,17 +32,13 @@ const SystemPromptComponent: React.FC<Props> = ({
onSystemPromptSelectionChange,
setIsSettingsModalVisible,
isFlyoutMode,
allSystemPrompts,
}) => {
const { allSystemPrompts } = useAssistantContext();
const selectedPrompt = useMemo(() => {
if (editingSystemPromptId !== undefined) {
return (
allSystemPrompts?.find((p) => p.id === editingSystemPromptId) ??
allSystemPrompts?.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId)
);
return allSystemPrompts.find((p) => p.id === editingSystemPromptId);
} else {
return undefined;
return allSystemPrompts.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId);
}
}, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, editingSystemPromptId]);
@ -58,7 +55,7 @@ const SystemPromptComponent: React.FC<Props> = ({
if (isFlyoutMode) {
return (
<SelectSystemPrompt
allSystemPrompts={allSystemPrompts}
allPrompts={allSystemPrompts}
clearSelectedSystemPrompt={handleClearSystemPrompt}
conversation={conversation}
data-test-subj="systemPrompt"
@ -78,7 +75,7 @@ const SystemPromptComponent: React.FC<Props> = ({
<div>
{selectedPrompt == null || isEditing ? (
<SelectSystemPrompt
allSystemPrompts={allSystemPrompts}
allPrompts={allSystemPrompts}
clearSelectedSystemPrompt={handleClearSystemPrompt}
conversation={conversation}
data-test-subj="systemPrompt"

View file

@ -11,9 +11,32 @@ import userEvent from '@testing-library/user-event';
import { Props, SelectSystemPrompt } from '.';
import { TEST_IDS } from '../../../constants';
import { defaultAssistantFeatures } from '@kbn/elastic-assistant-common';
import { HttpSetup } from '@kbn/core/public';
import { useFetchPrompts } from '../../../api';
import { mockSystemPrompts } from '../../../../mock/system_prompt';
import { DefinedUseQueryResult } from '@tanstack/react-query';
jest.mock('../../../api/prompts/use_fetch_prompts');
const http = {
fetch: jest.fn().mockResolvedValue(defaultAssistantFeatures),
} as unknown as HttpSetup;
jest.mocked(useFetchPrompts).mockReturnValue({
data: { page: 1, perPage: 1000, data: mockSystemPrompts, total: 10 },
isLoading: false,
refetch: jest.fn().mockResolvedValue({
isLoading: false,
data: {
...mockSystemPrompts,
},
}),
isFetched: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as unknown as DefinedUseQueryResult<any, unknown>);
const props: Props = {
allSystemPrompts: [
allPrompts: [
{
id: 'default-system-prompt',
content: 'default',
@ -31,6 +54,8 @@ const props: Props = {
};
const mockUseAssistantContext = {
http,
assistantAvailability: { isAssistantEnabled: true },
allSystemPrompts: [
{
id: 'default-system-prompt',

View file

@ -18,10 +18,13 @@ import {
import React, { useCallback, useMemo, useState } from 'react';
import { euiThemeVars } from '@kbn/ui-theme';
import {
PromptResponse,
PromptTypeEnum,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { Conversation } from '../../../../..';
import { getOptions } from '../helpers';
import * as i18n from '../translations';
import type { Prompt } from '../../../types';
import { useAssistantContext } from '../../../../assistant_context';
import { useConversation } from '../../../use_conversation';
import { TEST_IDS } from '../../../constants';
@ -29,10 +32,10 @@ import { PROMPT_CONTEXT_SELECTOR_PREFIX } from '../../../quick_prompts/prompt_co
import { SYSTEM_PROMPTS_TAB } from '../../../settings/const';
export interface Props {
allSystemPrompts: Prompt[];
allPrompts: PromptResponse[];
compressed?: boolean;
conversation?: Conversation;
selectedPrompt: Prompt | undefined;
selectedPrompt: PromptResponse | undefined;
clearSelectedSystemPrompt?: () => void;
isClearable?: boolean;
isEditing?: boolean;
@ -49,7 +52,7 @@ export interface Props {
const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT';
const SelectSystemPromptComponent: React.FC<Props> = ({
allSystemPrompts,
allPrompts,
compressed = false,
conversation,
selectedPrompt,
@ -68,21 +71,24 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
const { setSelectedSettingsTab } = useAssistantContext();
const { setApiConfig } = useConversation();
const [isOpenLocal, setIsOpenLocal] = useState<boolean>(isOpen);
const [valueOfSelected, setValueOfSelected] = useState<string | undefined>(
selectedPrompt?.id ?? allSystemPrompts?.[0]?.id
const allSystemPrompts = useMemo(
() => allPrompts.filter((p) => p.promptType === PromptTypeEnum.system),
[allPrompts]
);
const [isOpenLocal, setIsOpenLocal] = useState<boolean>(isOpen);
const handleOnBlur = useCallback(() => setIsOpenLocal(false), []);
const valueOfSelected = useMemo(() => selectedPrompt?.id, [selectedPrompt?.id]);
// Write the selected system prompt to the conversation config
const setSelectedSystemPrompt = useCallback(
(prompt: Prompt | undefined) => {
(promptId?: string) => {
if (conversation && conversation.apiConfig) {
setApiConfig({
conversation,
apiConfig: {
...conversation.apiConfig,
defaultSystemPromptId: prompt?.id,
defaultSystemPromptId: promptId,
},
});
}
@ -126,14 +132,11 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
// Note: if callback is provided, this component does not persist. Extract to separate component
if (onSystemPromptSelectionChange != null) {
onSystemPromptSelectionChange(selectedSystemPromptId);
} else {
setSelectedSystemPrompt(allSystemPrompts.find((sp) => sp.id === selectedSystemPromptId));
}
setValueOfSelected(selectedSystemPromptId);
setSelectedSystemPrompt(selectedSystemPromptId);
setIsEditing?.(false);
},
[
allSystemPrompts,
onSystemPromptSelectionChange,
setIsEditing,
setIsSettingsModalVisible,
@ -146,7 +149,6 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
setSelectedSystemPrompt(undefined);
setIsEditing?.(false);
clearSelectedSystemPrompt?.();
setValueOfSelected(undefined);
}, [clearSelectedSystemPrompt, setIsEditing, setSelectedSystemPrompt]);
const onShowSelectSystemPrompt = useCallback(() => {

View file

@ -18,9 +18,13 @@ import {
import { keyBy } from 'lodash/fp';
import { css } from '@emotion/react';
import {
PromptResponse,
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { ApiConfig } from '@kbn/elastic-assistant-common';
import { AIConnector } from '../../../../connectorland/connector_selector';
import { Conversation, Prompt } from '../../../../..';
import { Conversation } from '../../../../..';
import * as i18n from './translations';
import { ConversationMultiSelector } from './conversation_multi_selector/conversation_multi_selector';
import { SystemPromptSelector } from './system_prompt_selector/system_prompt_selector';
@ -34,16 +38,18 @@ interface Props {
connectors: AIConnector[] | undefined;
conversationSettings: Record<string, Conversation>;
conversationsSettingsBulkActions: ConversationsBulkActions;
onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
selectedSystemPrompt: Prompt | undefined;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
onSelectedSystemPromptChange: (systemPrompt?: PromptResponse) => void;
selectedSystemPrompt: PromptResponse | undefined;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
systemPromptSettings: Prompt[];
systemPromptSettings: PromptResponse[];
setConversationsSettingsBulkActions: React.Dispatch<
React.SetStateAction<ConversationsBulkActions>
>;
defaultConnector?: AIConnector;
resetSettings?: () => void;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
}
/**
@ -61,6 +67,8 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
setConversationsSettingsBulkActions,
defaultConnector,
resetSettings,
promptsBulkActions,
setPromptsBulkActions,
}) => {
// Prompt
const promptContent = useMemo(
@ -72,11 +80,11 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
const handlePromptContentChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (selectedSystemPrompt != null) {
setUpdatedSystemPromptSettings((prev): Prompt[] => {
setUpdatedSystemPromptSettings((prev): PromptResponse[] => {
const alreadyExists = prev.some((sp) => sp.id === selectedSystemPrompt.id);
if (alreadyExists) {
return prev.map((sp): Prompt => {
return prev.map((sp): PromptResponse => {
if (sp.id === selectedSystemPrompt.id) {
return {
...sp,
@ -89,9 +97,44 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
return prev;
});
const existingPrompt = systemPromptSettings.find((sp) => sp.id === selectedSystemPrompt.id);
if (existingPrompt) {
setPromptsBulkActions({
...promptsBulkActions,
...(selectedSystemPrompt.name !== selectedSystemPrompt.id
? {
update: [
...(promptsBulkActions.update ?? []).filter(
(p) => p.id !== selectedSystemPrompt.id
),
{
...selectedSystemPrompt,
content: e.target.value,
},
],
}
: {
create: [
...(promptsBulkActions.create ?? []).filter(
(p) => p.name !== selectedSystemPrompt.name
),
{
...selectedSystemPrompt,
content: e.target.value,
},
],
}),
});
}
}
},
[selectedSystemPrompt, setUpdatedSystemPromptSettings]
[
promptsBulkActions,
selectedSystemPrompt,
setPromptsBulkActions,
setUpdatedSystemPromptSettings,
systemPromptSettings,
]
);
const conversationsWithApiConfig = Object.entries(conversationSettings).reduce<
@ -258,14 +301,47 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
};
});
});
setPromptsBulkActions({
...promptsBulkActions,
...(selectedSystemPrompt.name !== selectedSystemPrompt.id
? {
update: [
...(promptsBulkActions.update ?? []).filter(
(p) => p.id !== selectedSystemPrompt.id
),
{
...selectedSystemPrompt,
isNewConversationDefault: isChecked,
},
],
}
: {
create: [
...(promptsBulkActions.create ?? []).filter(
(p) => p.name !== selectedSystemPrompt.name
),
{
...selectedSystemPrompt,
isNewConversationDefault: isChecked,
},
],
}),
});
}
},
[selectedSystemPrompt, setUpdatedSystemPromptSettings]
[
promptsBulkActions,
selectedSystemPrompt,
setPromptsBulkActions,
setUpdatedSystemPromptSettings,
]
);
const { onSystemPromptSelectionChange, onSystemPromptDeleted } = useSystemPromptEditor({
setUpdatedSystemPromptSettings,
onSelectedSystemPromptChange,
promptsBulkActions,
setPromptsBulkActions,
});
return (

View file

@ -35,7 +35,7 @@ describe('SystemPromptSelector', () => {
fireEvent.click(getByTestId('comboBoxToggleListButton'));
// there is only one delete system prompt because there is only one custom option
fireEvent.click(getAllByTestId('delete-prompt')[1]);
expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[1].name);
expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[1].id);
expect(onSystemPromptSelectionChange).not.toHaveBeenCalled();
});
it('Deletes a system prompt that is selected', () => {
@ -43,7 +43,7 @@ describe('SystemPromptSelector', () => {
fireEvent.click(getByTestId('comboBoxToggleListButton'));
// there is only one delete system prompt because there is only one custom option
fireEvent.click(getAllByTestId('delete-prompt')[0]);
expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[0].name);
expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[0].id);
expect(onSystemPromptSelectionChange).toHaveBeenCalledWith(undefined);
});
it('Selects existing system prompt from the search input', () => {

View file

@ -18,8 +18,8 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { TEST_IDS } from '../../../../constants';
import { Prompt } from '../../../../../..';
import * as i18n from './translations';
import { SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION } from '../translations';
@ -28,10 +28,10 @@ export const SYSTEM_PROMPT_SELECTOR_CLASSNAME = 'systemPromptSelector';
interface Props {
autoFocus?: boolean;
onSystemPromptDeleted: (systemPromptTitle: string) => void;
onSystemPromptSelectionChange: (systemPrompt?: Prompt | string) => void;
onSystemPromptSelectionChange: (systemPrompt?: PromptResponse | string) => void;
systemPrompts: PromptResponse[];
selectedSystemPrompt?: PromptResponse;
resetSettings?: () => void;
selectedSystemPrompt?: Prompt;
systemPrompts: Prompt[];
}
export type SystemPromptSelectorOption = EuiComboBoxOptionOption<{
@ -59,6 +59,7 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
isNewConversationDefault: sp.isNewConversationDefault ?? false,
},
label: sp.name,
id: sp.id,
'data-test-subj': `${TEST_IDS.SYSTEM_PROMPT_SELECTOR}-${sp.id}`,
}))
);
@ -70,6 +71,7 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
isDefault: selectedSystemPrompt.isDefault ?? false,
isNewConversationDefault: selectedSystemPrompt.isNewConversationDefault ?? false,
},
id: selectedSystemPrompt.id,
label: selectedSystemPrompt.name,
},
]
@ -106,6 +108,7 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
const newOption = {
value: searchValue,
id: searchValue,
label: searchValue,
};
@ -132,11 +135,12 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
// Callback for when user deletes a quick prompt
const onDelete = useCallback(
(label: string) => {
const deleteId = options.find((o) => o.label === label)?.id;
setOptions(options.filter((o) => o.label !== label));
if (selectedOptions?.[0]?.label === label) {
handleSelectionChange([]);
}
onSystemPromptDeleted(label);
onSystemPromptDeleted(deleteId ?? label);
},
[handleSelectionChange, onSystemPromptDeleted, options, selectedOptions]
);

View file

@ -36,6 +36,8 @@ const testProps = {
systemPromptSettings: mockSystemPrompts,
conversationsSettingsBulkActions: {},
setConversationsSettingsBulkActions: jest.fn(),
promptsBulkActions: {},
setPromptsBulkActions: jest.fn(),
};
jest.mock('./system_prompt_selector/system_prompt_selector', () => ({
@ -96,6 +98,7 @@ describe('SystemPromptSettings', () => {
);
fireEvent.click(getByTestId('change-sp-custom'));
const customOption = {
consumer: 'test',
content: '',
id: 'sooper custom prompt',
name: 'sooper custom prompt',

View file

@ -26,7 +26,9 @@ export const SystemPromptSettings: React.FC<SystemPromptSettingsProps> = React.m
systemPromptSettings,
conversationsSettingsBulkActions,
setConversationsSettingsBulkActions,
promptsBulkActions,
defaultConnector,
setPromptsBulkActions,
}) => {
return (
<>
@ -48,6 +50,8 @@ export const SystemPromptSettings: React.FC<SystemPromptSettingsProps> = React.m
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
defaultConnector={defaultConnector}
setPromptsBulkActions={setPromptsBulkActions}
promptsBulkActions={promptsBulkActions}
/>
</>
);

View file

@ -4,21 +4,27 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
PromptResponse,
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { AIConnector } from '../../../../connectorland/connector_selector';
import { Conversation, Prompt } from '../../../../..';
import { Conversation } from '../../../../..';
import { ConversationsBulkActions } from '../../../api';
export interface SystemPromptSettingsProps {
connectors: AIConnector[] | undefined;
conversationSettings: Record<string, Conversation>;
conversationsSettingsBulkActions: ConversationsBulkActions;
onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
selectedSystemPrompt: Prompt | undefined;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
onSelectedSystemPromptChange: (systemPrompt?: PromptResponse) => void;
selectedSystemPrompt: PromptResponse | undefined;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
systemPromptSettings: Prompt[];
systemPromptSettings: PromptResponse[];
setConversationsSettingsBulkActions: React.Dispatch<
React.SetStateAction<ConversationsBulkActions>
>;
defaultConnector?: AIConnector;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
}

View file

@ -6,21 +6,27 @@
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useSystemPromptEditor } from './use_system_prompt_editor';
import { Prompt } from '../../../types';
import {
mockSystemPrompt,
mockSuperheroSystemPrompt,
mockSystemPrompts,
} from '../../../../mock/system_prompt';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { useAssistantContext } from '../../../../assistant_context';
jest.mock('../../../../assistant_context');
// Mock functions for the tests
const mockOnSelectedSystemPromptChange = jest.fn();
const mockSetUpdatedSystemPromptSettings = jest.fn();
const mockSetPromptsBulkActions = jest.fn();
const mockPreviousSystemPrompts = [...mockSystemPrompts];
describe('useSystemPromptEditor', () => {
beforeEach(() => {
jest.clearAllMocks();
(useAssistantContext as jest.Mock).mockReturnValue({
currentAppId: 'securitySolutionUI',
});
});
test('should delete a system prompt by id', () => {
@ -28,6 +34,8 @@ describe('useSystemPromptEditor', () => {
useSystemPromptEditor({
onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange,
setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings,
setPromptsBulkActions: mockSetPromptsBulkActions,
promptsBulkActions: {},
})
);
@ -41,11 +49,13 @@ describe('useSystemPromptEditor', () => {
});
test('should handle selection of an existing system prompt', () => {
const existingPrompt: Prompt = mockSystemPrompt;
const existingPrompt: PromptResponse = mockSystemPrompt;
const { result } = renderHook(() =>
useSystemPromptEditor({
onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange,
setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings,
setPromptsBulkActions: mockSetPromptsBulkActions,
promptsBulkActions: {},
})
);
@ -65,6 +75,8 @@ describe('useSystemPromptEditor', () => {
useSystemPromptEditor({
onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange,
setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings,
setPromptsBulkActions: mockSetPromptsBulkActions,
promptsBulkActions: {},
})
);
@ -72,11 +84,12 @@ describe('useSystemPromptEditor', () => {
result.current.onSystemPromptSelectionChange(newPromptId);
});
const newPrompt: Prompt = {
const newPrompt: PromptResponse = {
id: newPromptId,
content: '',
name: newPromptId,
promptType: 'system',
consumer: 'securitySolutionUI',
};
expect(mockOnSelectedSystemPromptChange).toHaveBeenCalledWith(newPrompt);
@ -90,10 +103,12 @@ describe('useSystemPromptEditor', () => {
useSystemPromptEditor({
onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange,
setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings,
setPromptsBulkActions: mockSetPromptsBulkActions,
promptsBulkActions: {},
})
);
const expectedPrompt: Prompt = mockSuperheroSystemPrompt;
const expectedPrompt: PromptResponse = mockSuperheroSystemPrompt;
act(() => {
result.current.onSystemPromptSelectionChange(expectedPrompt);

View file

@ -5,28 +5,38 @@
* 2.0.
*/
import {
PromptResponse,
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { useCallback } from 'react';
import { Prompt } from '../../../types';
import { useAssistantContext } from '../../../../..';
interface Props {
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
onSelectedSystemPromptChange: (systemPrompt?: PromptResponse) => void;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
}
export const useSystemPromptEditor = ({
setUpdatedSystemPromptSettings,
onSelectedSystemPromptChange,
promptsBulkActions,
setPromptsBulkActions,
}: Props) => {
const { currentAppId } = useAssistantContext();
// When top level system prompt selection changes
const onSystemPromptSelectionChange = useCallback(
(systemPrompt?: Prompt | string) => {
(systemPrompt?: PromptResponse | string) => {
const isNew = typeof systemPrompt === 'string';
const newSelectedSystemPrompt: Prompt | undefined = isNew
const newSelectedSystemPrompt: PromptResponse | undefined = isNew
? {
id: systemPrompt ?? '',
content: '',
name: systemPrompt ?? '',
promptType: 'system',
consumer: currentAppId,
}
: systemPrompt;
@ -40,18 +50,42 @@ export const useSystemPromptEditor = ({
return prev;
});
if (isNew) {
setPromptsBulkActions({
...promptsBulkActions,
create: [
...(promptsBulkActions.create ?? []),
{
...newSelectedSystemPrompt,
},
],
});
}
}
onSelectedSystemPromptChange(newSelectedSystemPrompt);
},
[onSelectedSystemPromptChange, setUpdatedSystemPromptSettings]
[
currentAppId,
onSelectedSystemPromptChange,
promptsBulkActions,
setPromptsBulkActions,
setUpdatedSystemPromptSettings,
]
);
const onSystemPromptDeleted = useCallback(
(id: string) => {
setUpdatedSystemPromptSettings((prev) => prev.filter((sp) => sp.id !== id));
setPromptsBulkActions({
...promptsBulkActions,
delete: {
ids: [...(promptsBulkActions.delete?.ids ?? []), id],
},
});
},
[setUpdatedSystemPromptSettings]
[promptsBulkActions, setPromptsBulkActions, setUpdatedSystemPromptSettings]
);
return { onSystemPromptSelectionChange, onSystemPromptDeleted };

View file

@ -16,6 +16,10 @@ import {
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import {
PromptResponse,
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { Conversation, ConversationsBulkActions, useAssistantContext } from '../../../../..';
import { SYSTEM_PROMPT_TABLE_SESSION_STORAGE_KEY } from '../../../../assistant_context/constants';
import { AIConnector } from '../../../../connectorland/connector_selector';
@ -26,7 +30,6 @@ import {
useSessionPagination,
} from '../../../common/components/assistant_settings_management/pagination/use_session_pagination';
import { CANCEL, DELETE } from '../../../settings/translations';
import { Prompt } from '../../../types';
import { SystemPromptEditor } from '../system_prompt_modal/system_prompt_editor';
import { SETTINGS_TITLE } from '../system_prompt_modal/translations';
import { useSystemPromptEditor } from '../system_prompt_modal/use_system_prompt_editor';
@ -37,11 +40,11 @@ interface Props {
connectors: AIConnector[] | undefined;
conversationSettings: Record<string, Conversation>;
conversationsSettingsBulkActions: ConversationsBulkActions;
onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
selectedSystemPrompt: Prompt | undefined;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
onSelectedSystemPromptChange: (systemPrompt?: PromptResponse) => void;
selectedSystemPrompt: PromptResponse | undefined;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
systemPromptSettings: Prompt[];
systemPromptSettings: PromptResponse[];
setConversationsSettingsBulkActions: React.Dispatch<
React.SetStateAction<ConversationsBulkActions>
>;
@ -49,6 +52,8 @@ interface Props {
handleSave: (shouldRefetchConversation?: boolean) => void;
onCancelClick: () => void;
resetSettings: () => void;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
}
const SystemPromptSettingsManagementComponent = ({
@ -65,6 +70,8 @@ const SystemPromptSettingsManagementComponent = ({
handleSave,
onCancelClick,
resetSettings,
promptsBulkActions,
setPromptsBulkActions,
}: Props) => {
const { nameSpace } = useAssistantContext();
const { isFlyoutOpen: editFlyoutVisible, openFlyout, closeFlyout } = useFlyoutModalVisibility();
@ -73,7 +80,7 @@ const SystemPromptSettingsManagementComponent = ({
openFlyout: openConfirmModal,
closeFlyout: closeConfirmModal,
} = useFlyoutModalVisibility();
const [deletedPrompt, setDeletedPrompt] = useState<Prompt | null>();
const [deletedPrompt, setDeletedPrompt] = useState<PromptResponse | null>();
const onCreate = useCallback(() => {
onSelectedSystemPromptChange({
@ -88,10 +95,12 @@ const SystemPromptSettingsManagementComponent = ({
const { onSystemPromptSelectionChange, onSystemPromptDeleted } = useSystemPromptEditor({
setUpdatedSystemPromptSettings,
onSelectedSystemPromptChange,
promptsBulkActions,
setPromptsBulkActions,
});
const onEditActionClicked = useCallback(
(prompt: Prompt) => {
(prompt: PromptResponse) => {
onSystemPromptSelectionChange(prompt);
openFlyout();
},
@ -99,7 +108,7 @@ const SystemPromptSettingsManagementComponent = ({
);
const onDeleteActionClicked = useCallback(
(prompt: Prompt) => {
(prompt: PromptResponse) => {
setDeletedPrompt(prompt);
onSystemPromptDeleted(prompt.id);
openConfirmModal();
@ -200,6 +209,8 @@ const SystemPromptSettingsManagementComponent = ({
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
defaultConnector={defaultConnector}
resetSettings={resetSettings}
promptsBulkActions={promptsBulkActions}
setPromptsBulkActions={setPromptsBulkActions}
/>
</Flyout>
{deleteConfirmModalVisibility && deletedPrompt?.name && (

View file

@ -7,26 +7,25 @@
import { renderHook } from '@testing-library/react-hooks';
import { useSystemPromptTable } from './use_system_prompt_table';
import { Prompt } from '../../../types';
import { Conversation } from '../../../../assistant_context/types';
import { AIConnector } from '../../../../connectorland/connector_selector';
import { customConvo, welcomeConvo } from '../../../../mock/conversation';
import { mockConnectors } from '../../../../mock/connectors';
import { ApiConfig } from '@kbn/elastic-assistant-common';
import { ApiConfig, PromptResponse } from '@kbn/elastic-assistant-common';
// Mock data for tests
const mockSystemPrompts: Prompt[] = [
const mockSystemPrompts: PromptResponse[] = [
{
id: 'prompt-1',
content: 'Prompt 1',
name: 'Prompt 1',
promptType: 'user',
promptType: 'quick',
},
{
id: 'prompt-2',
content: 'Prompt 2',
name: 'Prompt 2',
promptType: 'user',
promptType: 'quick',
isNewConversationDefault: true,
},
];

View file

@ -6,11 +6,11 @@
*/
import { EuiBasicTableColumn, EuiIcon, EuiLink } from '@elastic/eui';
import React, { useCallback } from 'react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../../assistant_context/types';
import { AIConnector } from '../../../../connectorland/connector_selector';
import { BadgesColumn } from '../../../common/components/assistant_settings_management/badges';
import { RowActions } from '../../../common/components/assistant_settings_management/row_actions';
import { Prompt } from '../../../types';
import {
getConversationApiConfig,
getInitialDefaultSystemPrompt,
@ -21,10 +21,10 @@ import { getSelectedConversations } from './utils';
type ConversationsWithSystemPrompt = Record<
string,
Conversation & { systemPrompt: Prompt | undefined }
Conversation & { systemPrompt: PromptResponse | undefined }
>;
type SystemPromptTableItem = Prompt & { defaultConversations: string[] };
type SystemPromptTableItem = PromptResponse & { defaultConversations: string[] };
export const useSystemPromptTable = () => {
const getColumns = useCallback(
@ -97,7 +97,7 @@ export const useSystemPromptTable = () => {
connectors: AIConnector[] | undefined;
conversationSettings: Record<string, Conversation>;
defaultConnector: AIConnector | undefined;
systemPromptSettings: Prompt[];
systemPromptSettings: PromptResponse[];
}): SystemPromptTableItem[] => {
const conversationsWithApiConfig = Object.entries(
conversationSettings

View file

@ -6,8 +6,8 @@
*/
import { ProviderEnum } from '@kbn/elastic-assistant-common';
import { mockSystemPrompts } from '../../../../mock/system_prompt';
import { PromptType } from '../../../types';
import { getSelectedConversations } from './utils';
import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
describe('getSelectedConversations', () => {
const allSystemPrompts = [...mockSystemPrompts];
const conversationSettings = {
@ -39,7 +39,7 @@ describe('getSelectedConversations', () => {
content:
'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nProvide the most detailed and relevant answer possible, as if you were relaying this information back to a cyber security expert.\nIf you answer a question related to KQL, EQL, or ES|QL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query. xxx',
name: 'Enhanced system prompt',
promptType: 'system' as PromptType,
promptType: PromptTypeEnum.system,
isDefault: true,
isNewConversationDefault: true,
},

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../../assistant_context/types';
import { Prompt } from '../../../types';
export const getSelectedConversations = (
allSystemPrompts: Prompt[],
allSystemPrompts: PromptResponse[],
conversationSettings: Record<string, Conversation>,
systemPromptId: string
) => {

View file

@ -25,9 +25,9 @@ describe('QuickPromptSelector', () => {
});
it('Selects an existing quick prompt', () => {
const { getByTestId } = render(<QuickPromptSelector {...testProps} />);
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(MOCK_QUICK_PROMPTS[0].title);
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(MOCK_QUICK_PROMPTS[0].name);
fireEvent.click(getByTestId('comboBoxToggleListButton'));
fireEvent.click(getByTestId(MOCK_QUICK_PROMPTS[1].title));
fireEvent.click(getByTestId(MOCK_QUICK_PROMPTS[1].name));
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(MOCK_QUICK_PROMPTS[1]);
});
it('Only custom option can be deleted', () => {
@ -49,8 +49,10 @@ describe('QuickPromptSelector', () => {
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith({
categories: [],
color: '#D36086',
prompt: 'quickly prompt please',
title: 'A_CUSTOM_OPTION',
content: 'quickly prompt please',
id: 'A_CUSTOM_OPTION',
name: 'A_CUSTOM_OPTION',
promptType: 'quick',
});
});
it('Reset settings every time before selecting an system prompt from the input if resetSettings is provided', () => {
@ -60,7 +62,7 @@ describe('QuickPromptSelector', () => {
);
// changing the selection
fireEvent.change(getByTestId('comboBoxSearchInput'), {
target: { value: MOCK_QUICK_PROMPTS[1].title },
target: { value: MOCK_QUICK_PROMPTS[1].name },
});
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
key: 'Enter',

View file

@ -18,16 +18,16 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import * as i18n from './translations';
import { QuickPrompt } from '../types';
interface Props {
isDisabled?: boolean;
onQuickPromptDeleted: (quickPromptTitle: string) => void;
onQuickPromptSelectionChange: (quickPrompt?: QuickPrompt | string) => void;
quickPrompts: QuickPrompt[];
onQuickPromptSelectionChange: (quickPrompt?: PromptResponse | string) => void;
quickPrompts: PromptResponse[];
selectedQuickPrompt?: PromptResponse;
resetSettings?: () => void;
selectedQuickPrompt?: QuickPrompt;
}
export type QuickPromptSelectorOption = EuiComboBoxOptionOption<{ isDefault: boolean }>;
@ -50,8 +50,9 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
value: {
isDefault: qp.isDefault ?? false,
},
label: qp.title,
'data-test-subj': qp.title,
label: qp.name,
'data-test-subj': qp.name,
id: qp.id,
color: qp.color,
}))
);
@ -62,7 +63,8 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
value: {
isDefault: true,
},
label: selectedQuickPrompt.title,
label: selectedQuickPrompt.name,
id: selectedQuickPrompt.id,
color: selectedQuickPrompt.color,
},
]
@ -76,7 +78,7 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
const newQuickPrompt =
quickPromptSelectorOption.length === 0
? undefined
: quickPrompts.find((qp) => qp.title === quickPromptSelectorOption[0]?.label) ??
: quickPrompts.find((qp) => qp.name === quickPromptSelectorOption[0]?.label) ??
quickPromptSelectorOption[0]?.label;
onQuickPromptSelectionChange(newQuickPrompt);
},
@ -100,6 +102,7 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
const newOption = {
value: searchValue,
label: searchValue,
id: searchValue,
};
if (!optionExists) {
@ -125,11 +128,12 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
// Callback for when user deletes a quick prompt
const onDelete = useCallback(
(label: string) => {
const deleteId = options.find((o) => o.label === label)?.id;
setOptions(options.filter((o) => o.label !== label));
if (selectedOptions?.[0]?.label === label) {
handleSelectionChange([]);
}
onQuickPromptDeleted(label);
onQuickPromptDeleted(deleteId ?? label);
},
[handleSelectionChange, onQuickPromptDeleted, options, selectedOptions]
);

View file

@ -10,9 +10,12 @@ import { EuiFormRow, EuiColorPicker, EuiTextArea } from '@elastic/eui';
import { EuiSetColorMethod } from '@elastic/eui/src/services/color_picker/color_picker';
import { css } from '@emotion/react';
import {
PromptResponse,
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { PromptContextTemplate } from '../../../..';
import * as i18n from './translations';
import { QuickPrompt } from '../types';
import { QuickPromptSelector } from '../quick_prompt_selector/quick_prompt_selector';
import { PromptContextSelector } from '../prompt_context_selector/prompt_context_selector';
import { useAssistantContext } from '../../../assistant_context';
@ -21,11 +24,13 @@ import { useQuickPromptEditor } from './use_quick_prompt_editor';
const DEFAULT_COLOR = '#D36086';
interface Props {
onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void;
quickPromptSettings: QuickPrompt[];
onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void;
quickPromptSettings: PromptResponse[];
resetSettings?: () => void;
selectedQuickPrompt: QuickPrompt | undefined;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<QuickPrompt[]>>;
selectedQuickPrompt: PromptResponse | undefined;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
}
const QuickPromptSettingsEditorComponent = ({
@ -34,28 +39,30 @@ const QuickPromptSettingsEditorComponent = ({
resetSettings,
selectedQuickPrompt,
setUpdatedQuickPromptSettings,
promptsBulkActions,
setPromptsBulkActions,
}: Props) => {
const { basePromptContexts } = useAssistantContext();
// Prompt
const prompt = useMemo(
const promptContent = useMemo(
// Fixing Cursor Jump in text area
() => quickPromptSettings.find((p) => p.title === selectedQuickPrompt?.title)?.prompt ?? '',
[selectedQuickPrompt?.title, quickPromptSettings]
() => quickPromptSettings.find((p) => p.id === selectedQuickPrompt?.id)?.content ?? '',
[selectedQuickPrompt?.id, quickPromptSettings]
);
const handlePromptChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (selectedQuickPrompt != null) {
setUpdatedQuickPromptSettings((prev) => {
const alreadyExists = prev.some((qp) => qp.title === selectedQuickPrompt.title);
setUpdatedQuickPromptSettings((prev): PromptResponse[] => {
const alreadyExists = prev.some((qp) => qp.id === selectedQuickPrompt.id);
if (alreadyExists) {
return prev.map((qp) => {
if (qp.title === selectedQuickPrompt.title) {
if (qp.id === selectedQuickPrompt.id) {
return {
...qp,
prompt: e.target.value,
content: e.target.value,
};
}
return qp;
@ -64,9 +71,45 @@ const QuickPromptSettingsEditorComponent = ({
return prev;
});
const existingPrompt = quickPromptSettings.find((sp) => sp.id === selectedQuickPrompt.id);
if (existingPrompt) {
setPromptsBulkActions({
...promptsBulkActions,
...(selectedQuickPrompt.name !== selectedQuickPrompt.id
? {
update: [
...(promptsBulkActions.update ?? []).filter(
(p) => p.id !== selectedQuickPrompt.id
),
{
...selectedQuickPrompt,
content: e.target.value,
},
],
}
: {
create: [
...(promptsBulkActions.create ?? []).filter(
(p) => p.name !== selectedQuickPrompt.name
),
{
...selectedQuickPrompt,
content: e.target.value,
},
],
}),
});
}
}
},
[selectedQuickPrompt, setUpdatedQuickPromptSettings]
[
promptsBulkActions,
quickPromptSettings,
selectedQuickPrompt,
setPromptsBulkActions,
setUpdatedQuickPromptSettings,
]
);
// Color
@ -79,11 +122,11 @@ const QuickPromptSettingsEditorComponent = ({
(color, { hex, isValid }) => {
if (selectedQuickPrompt != null) {
setUpdatedQuickPromptSettings((prev) => {
const alreadyExists = prev.some((qp) => qp.title === selectedQuickPrompt.title);
const alreadyExists = prev.some((qp) => qp.name === selectedQuickPrompt.name);
if (alreadyExists) {
return prev.map((qp) => {
if (qp.title === selectedQuickPrompt.title) {
if (qp.name === selectedQuickPrompt.name) {
return {
...qp,
color,
@ -94,9 +137,44 @@ const QuickPromptSettingsEditorComponent = ({
}
return prev;
});
const existingPrompt = quickPromptSettings.find((sp) => sp.id === selectedQuickPrompt.id);
if (existingPrompt) {
setPromptsBulkActions({
...promptsBulkActions,
...(selectedQuickPrompt.name !== selectedQuickPrompt.id
? {
update: [
...(promptsBulkActions.update ?? []).filter(
(p) => p.id !== selectedQuickPrompt.id
),
{
...selectedQuickPrompt,
color,
},
],
}
: {
create: [
...(promptsBulkActions.create ?? []).filter(
(p) => p.name !== selectedQuickPrompt.name
),
{
...selectedQuickPrompt,
color,
},
],
}),
});
}
}
},
[selectedQuickPrompt, setUpdatedQuickPromptSettings]
[
promptsBulkActions,
quickPromptSettings,
selectedQuickPrompt,
setPromptsBulkActions,
setUpdatedQuickPromptSettings,
]
);
// Prompt Contexts
@ -112,11 +190,11 @@ const QuickPromptSettingsEditorComponent = ({
(pc: PromptContextTemplate[]) => {
if (selectedQuickPrompt != null) {
setUpdatedQuickPromptSettings((prev) => {
const alreadyExists = prev.some((qp) => qp.title === selectedQuickPrompt.title);
const alreadyExists = prev.some((qp) => qp.name === selectedQuickPrompt.name);
if (alreadyExists) {
return prev.map((qp) => {
if (qp.title === selectedQuickPrompt.title) {
if (qp.name === selectedQuickPrompt.name) {
return {
...qp,
categories: pc.map((p) => p.category),
@ -127,15 +205,53 @@ const QuickPromptSettingsEditorComponent = ({
}
return prev;
});
const existingPrompt = quickPromptSettings.find((sp) => sp.id === selectedQuickPrompt.id);
if (existingPrompt) {
setPromptsBulkActions({
...promptsBulkActions,
...(selectedQuickPrompt.name !== selectedQuickPrompt.id
? {
update: [
...(promptsBulkActions.update ?? []).filter(
(p) => p.id !== selectedQuickPrompt.id
),
{
...selectedQuickPrompt,
categories: pc.map((p) => p.category),
},
],
}
: {
create: [
...(promptsBulkActions.create ?? []).filter(
(p) => p.name !== selectedQuickPrompt.name
),
{
...selectedQuickPrompt,
categories: pc.map((p) => p.category),
},
],
}),
});
}
}
},
[selectedQuickPrompt, setUpdatedQuickPromptSettings]
[
promptsBulkActions,
quickPromptSettings,
selectedQuickPrompt,
setPromptsBulkActions,
setUpdatedQuickPromptSettings,
]
);
// When top level quick prompt selection changes
const { onQuickPromptDeleted, onQuickPromptSelectionChange } = useQuickPromptEditor({
onSelectedQuickPromptChange,
setUpdatedQuickPromptSettings,
promptsBulkActions,
setPromptsBulkActions,
});
return (
@ -158,7 +274,7 @@ const QuickPromptSettingsEditorComponent = ({
data-test-subj="quick-prompt-prompt"
onChange={handlePromptChange}
placeholder={i18n.QUICK_PROMPT_PROMPT_PLACEHOLDER}
value={prompt}
value={promptContent}
css={css`
min-height: 150px;
`}

View file

@ -13,6 +13,7 @@ import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
import { mockPromptContexts } from '../../../mock/prompt_context';
const onSelectedQuickPromptChange = jest.fn();
const setPromptsBulkActions = jest.fn();
const setUpdatedQuickPromptSettings = jest.fn().mockImplementation((fn) => {
return fn(MOCK_QUICK_PROMPTS);
});
@ -22,6 +23,8 @@ const testProps = {
quickPromptSettings: MOCK_QUICK_PROMPTS,
selectedQuickPrompt: MOCK_QUICK_PROMPTS[0],
setUpdatedQuickPromptSettings,
promptsBulkActions: {},
setPromptsBulkActions,
};
const mockContext = {
basePromptContexts: MOCK_QUICK_PROMPTS,
@ -91,8 +94,11 @@ describe('QuickPromptSettings', () => {
const customOption = {
categories: [],
color: '#D36086',
prompt: '',
title: 'sooper custom prompt',
consumer: undefined,
content: '',
id: 'sooper custom prompt',
name: 'sooper custom prompt',
promptType: 'quick',
};
expect(setUpdatedQuickPromptSettings).toHaveReturnedWith([...MOCK_QUICK_PROMPTS, customOption]);
expect(onSelectedQuickPromptChange).toHaveBeenCalledWith(customOption);
@ -130,7 +136,7 @@ describe('QuickPromptSettings', () => {
const previousFirstElementOfTheArray = mutatableQuickPrompts.shift();
expect(setUpdatedQuickPromptSettings).toHaveReturnedWith([
{ ...previousFirstElementOfTheArray, prompt: 'what does this do' },
{ ...previousFirstElementOfTheArray, content: 'what does this do' },
...mutatableQuickPrompts,
]);
});

View file

@ -8,15 +8,20 @@
import React from 'react';
import { EuiTitle, EuiText, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import {
PromptResponse,
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import * as i18n from './translations';
import { QuickPrompt } from '../types';
import { QuickPromptSettingsEditor } from './quick_prompt_editor';
interface Props {
onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void;
quickPromptSettings: QuickPrompt[];
selectedQuickPrompt: QuickPrompt | undefined;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<QuickPrompt[]>>;
onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void;
quickPromptSettings: PromptResponse[];
selectedQuickPrompt: PromptResponse | undefined;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
}
/**
@ -28,6 +33,8 @@ export const QuickPromptSettings: React.FC<Props> = React.memo<Props>(
quickPromptSettings,
selectedQuickPrompt,
setUpdatedQuickPromptSettings,
promptsBulkActions,
setPromptsBulkActions,
}) => {
return (
<>
@ -43,6 +50,8 @@ export const QuickPromptSettings: React.FC<Props> = React.memo<Props>(
quickPromptSettings={quickPromptSettings}
selectedQuickPrompt={selectedQuickPrompt}
setUpdatedQuickPromptSettings={setUpdatedQuickPromptSettings}
promptsBulkActions={promptsBulkActions}
setPromptsBulkActions={setPromptsBulkActions}
/>
</>
);

View file

@ -7,18 +7,24 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useQuickPromptEditor, DEFAULT_COLOR } from './use_quick_prompt_editor';
import { QuickPrompt } from '../types';
import { mockAlertPromptContext } from '../../../mock/prompt_context';
import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { useAssistantContext } from '../../../assistant_context';
jest.mock('../../../assistant_context');
// Mock functions for the tests
const mockOnSelectedQuickPromptChange = jest.fn();
const mockSetUpdatedQuickPromptSettings = jest.fn();
const mockPreviousQuickPrompts = [...MOCK_QUICK_PROMPTS];
const setPromptsBulkActions = jest.fn();
describe('useQuickPromptEditor', () => {
beforeEach(() => {
jest.clearAllMocks();
(useAssistantContext as jest.Mock).mockReturnValue({
currentAppId: 'securitySolutionUI',
});
});
test('should delete a quick prompt by title', () => {
@ -26,6 +32,8 @@ describe('useQuickPromptEditor', () => {
useQuickPromptEditor({
onSelectedQuickPromptChange: mockOnSelectedQuickPromptChange,
setUpdatedQuickPromptSettings: mockSetUpdatedQuickPromptSettings,
setPromptsBulkActions,
promptsBulkActions: {},
})
);
@ -34,7 +42,7 @@ describe('useQuickPromptEditor', () => {
});
expect(mockSetUpdatedQuickPromptSettings.mock.calls[0][0]?.(mockPreviousQuickPrompts)).toEqual(
MOCK_QUICK_PROMPTS.filter((qp) => qp.title !== 'ALERT_SUMMARIZATION_TITLE')
MOCK_QUICK_PROMPTS.filter((qp) => qp.name !== 'ALERT_SUMMARIZATION_TITLE')
);
});
@ -44,6 +52,8 @@ describe('useQuickPromptEditor', () => {
useQuickPromptEditor({
onSelectedQuickPromptChange: mockOnSelectedQuickPromptChange,
setUpdatedQuickPromptSettings: mockSetUpdatedQuickPromptSettings,
setPromptsBulkActions,
promptsBulkActions: {},
})
);
@ -51,11 +61,14 @@ describe('useQuickPromptEditor', () => {
result.current.onQuickPromptSelectionChange(newPromptTitle);
});
const newPrompt: QuickPrompt = {
title: newPromptTitle,
prompt: '',
const newPrompt: PromptResponse = {
name: newPromptTitle,
content: '',
color: DEFAULT_COLOR,
categories: [],
id: newPromptTitle,
promptType: 'quick',
consumer: 'securitySolutionUI',
};
expect(mockOnSelectedQuickPromptChange).toHaveBeenCalledWith(newPrompt);
@ -70,17 +83,21 @@ describe('useQuickPromptEditor', () => {
useQuickPromptEditor({
onSelectedQuickPromptChange: mockOnSelectedQuickPromptChange,
setUpdatedQuickPromptSettings: mockSetUpdatedQuickPromptSettings,
setPromptsBulkActions,
promptsBulkActions: {},
})
);
const alertData = await mockAlertPromptContext.getPromptContext();
const expectedPrompt: QuickPrompt = {
title: mockAlertPromptContext.description,
prompt: alertData,
const expectedPrompt: PromptResponse = {
name: mockAlertPromptContext.description,
content: JSON.stringify(alertData ?? {}),
color: DEFAULT_COLOR,
categories: [mockAlertPromptContext.category],
} as QuickPrompt;
id: '',
promptType: 'quick',
};
act(() => {
result.current.onQuickPromptSelectionChange(expectedPrompt);

View file

@ -5,41 +5,60 @@
* 2.0.
*/
import {
PromptResponse,
PromptTypeEnum,
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { useCallback } from 'react';
import { QuickPrompt } from '../types';
import { useAssistantContext } from '../../../..';
export const DEFAULT_COLOR = '#D36086';
export const useQuickPromptEditor = ({
onSelectedQuickPromptChange,
setUpdatedQuickPromptSettings,
promptsBulkActions,
setPromptsBulkActions,
}: {
onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<QuickPrompt[]>>;
onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
}) => {
const { currentAppId } = useAssistantContext();
const onQuickPromptDeleted = useCallback(
(title: string) => {
setUpdatedQuickPromptSettings((prev) => prev.filter((qp) => qp.title !== title));
(id: string) => {
setUpdatedQuickPromptSettings((prev) => prev.filter((qp) => qp.id !== id));
setPromptsBulkActions({
...promptsBulkActions,
delete: {
ids: [...(promptsBulkActions.delete?.ids ?? []), id],
},
});
},
[setUpdatedQuickPromptSettings]
[promptsBulkActions, setPromptsBulkActions, setUpdatedQuickPromptSettings]
);
// When top level quick prompt selection changes
const onQuickPromptSelectionChange = useCallback(
(quickPrompt?: QuickPrompt | string) => {
(quickPrompt?: PromptResponse | string) => {
const isNew = typeof quickPrompt === 'string';
const newSelectedQuickPrompt: QuickPrompt | undefined = isNew
const newSelectedQuickPrompt: PromptResponse | undefined = isNew
? {
title: quickPrompt ?? '',
prompt: '',
name: quickPrompt,
id: quickPrompt,
content: '',
color: DEFAULT_COLOR,
categories: [],
promptType: PromptTypeEnum.quick,
consumer: currentAppId,
}
: quickPrompt;
if (newSelectedQuickPrompt != null) {
setUpdatedQuickPromptSettings((prev) => {
const alreadyExists = prev.some((qp) => qp.title === newSelectedQuickPrompt.title);
const alreadyExists = prev.some((qp) => qp.name === newSelectedQuickPrompt.name);
if (!alreadyExists) {
return [...prev, newSelectedQuickPrompt];
@ -47,11 +66,29 @@ export const useQuickPromptEditor = ({
return prev;
});
if (isNew) {
setPromptsBulkActions({
...promptsBulkActions,
create: [
...(promptsBulkActions.create ?? []),
{
...newSelectedQuickPrompt,
},
],
});
}
}
onSelectedQuickPromptChange(newSelectedQuickPrompt);
},
[onSelectedQuickPromptChange, setUpdatedQuickPromptSettings]
[
currentAppId,
onSelectedQuickPromptChange,
promptsBulkActions,
setPromptsBulkActions,
setUpdatedQuickPromptSettings,
]
);
return { onQuickPromptDeleted, onQuickPromptSelectionChange };

View file

@ -14,7 +14,10 @@ import {
EuiPanel,
EuiSpacer,
} from '@elastic/eui';
import { QuickPrompt } from '../types';
import {
PromptResponse,
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { QuickPromptSettingsEditor } from '../quick_prompt_settings/quick_prompt_editor';
import * as i18n from './translations';
import { useFlyoutModalVisibility } from '../../common/components/assistant_settings_management/flyout/use_flyout_modal_visibility';
@ -32,11 +35,13 @@ import { useAssistantContext } from '../../../assistant_context';
interface Props {
handleSave: (shouldRefetchConversation?: boolean) => void;
onCancelClick: () => void;
onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void;
quickPromptSettings: QuickPrompt[];
onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void;
quickPromptSettings: PromptResponse[];
resetSettings?: () => void;
selectedQuickPrompt: QuickPrompt | undefined;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<QuickPrompt[]>>;
selectedQuickPrompt: PromptResponse | undefined;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
}
const QuickPromptSettingsManagementComponent = ({
@ -47,11 +52,13 @@ const QuickPromptSettingsManagementComponent = ({
resetSettings,
selectedQuickPrompt,
setUpdatedQuickPromptSettings,
promptsBulkActions,
setPromptsBulkActions,
}: Props) => {
const { nameSpace, basePromptContexts } = useAssistantContext();
const { isFlyoutOpen: editFlyoutVisible, openFlyout, closeFlyout } = useFlyoutModalVisibility();
const [deletedQuickPrompt, setDeletedQuickPrompt] = useState<QuickPrompt | null>();
const [deletedQuickPrompt, setDeletedQuickPrompt] = useState<PromptResponse | null>();
const {
isFlyoutOpen: deleteConfirmModalVisibility,
openFlyout: openConfirmModal,
@ -61,10 +68,12 @@ const QuickPromptSettingsManagementComponent = ({
const { onQuickPromptDeleted, onQuickPromptSelectionChange } = useQuickPromptEditor({
onSelectedQuickPromptChange,
setUpdatedQuickPromptSettings,
promptsBulkActions,
setPromptsBulkActions,
});
const onEditActionClicked = useCallback(
(prompt: QuickPrompt) => {
(prompt: PromptResponse) => {
onQuickPromptSelectionChange(prompt);
openFlyout();
},
@ -72,9 +81,9 @@ const QuickPromptSettingsManagementComponent = ({
);
const onDeleteActionClicked = useCallback(
(prompt: QuickPrompt) => {
(prompt: PromptResponse) => {
setDeletedQuickPrompt(prompt);
onQuickPromptDeleted(prompt.title);
onQuickPromptDeleted(prompt.id);
openConfirmModal();
},
[onQuickPromptDeleted, openConfirmModal]
@ -123,10 +132,10 @@ const QuickPromptSettingsManagementComponent = ({
const confirmationTitle = useMemo(
() =>
deletedQuickPrompt?.title
? i18n.DELETE_QUICK_PROMPT_MODAL_TITLE(deletedQuickPrompt.title)
deletedQuickPrompt?.name
? i18n.DELETE_QUICK_PROMPT_MODAL_TITLE(deletedQuickPrompt.name)
: i18n.DELETE_QUICK_PROMPT_MODAL_DEFAULT_TITLE,
[deletedQuickPrompt?.title]
[deletedQuickPrompt?.name]
);
return (
@ -161,6 +170,8 @@ const QuickPromptSettingsManagementComponent = ({
resetSettings={resetSettings}
selectedQuickPrompt={selectedQuickPrompt}
setUpdatedQuickPromptSettings={setUpdatedQuickPromptSettings}
promptsBulkActions={promptsBulkActions}
setPromptsBulkActions={setPromptsBulkActions}
/>
</Flyout>
{deleteConfirmModalVisibility && deletedQuickPrompt && (

View file

@ -8,9 +8,9 @@
import { renderHook } from '@testing-library/react-hooks';
import { useQuickPromptTable } from './use_quick_prompt_table';
import { EuiTableComputedColumnType } from '@elastic/eui';
import { QuickPrompt } from '../types';
import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
import { mockPromptContexts } from '../../../mock/prompt_context';
import { PromptResponse } from '@kbn/elastic-assistant-common';
const mockOnEditActionClicked = jest.fn();
const mockOnDeleteActionClicked = jest.fn();
@ -43,7 +43,7 @@ describe('useQuickPromptTable', () => {
});
const mockQuickPrompt = { ...MOCK_QUICK_PROMPTS[0], categories: ['alert'] };
const mockBadgesColumn = (columns[1] as EuiTableComputedColumnType<QuickPrompt>).render(
const mockBadgesColumn = (columns[1] as EuiTableComputedColumnType<PromptResponse>).render(
mockQuickPrompt
);
const selectedPromptContexts = mockPromptContexts
@ -51,7 +51,7 @@ describe('useQuickPromptTable', () => {
.map((bpc) => bpc.description);
expect(mockBadgesColumn).toHaveProperty('props', {
items: selectedPromptContexts,
prefix: MOCK_QUICK_PROMPTS[0].title,
prefix: MOCK_QUICK_PROMPTS[0].name,
});
});
@ -62,7 +62,7 @@ describe('useQuickPromptTable', () => {
onDeleteActionClicked: mockOnDeleteActionClicked,
});
const mockRowActions = (columns[2] as EuiTableComputedColumnType<QuickPrompt>).render(
const mockRowActions = (columns[2] as EuiTableComputedColumnType<PromptResponse>).render(
MOCK_QUICK_PROMPTS[0]
);
@ -83,7 +83,7 @@ describe('useQuickPromptTable', () => {
const nonDefaultPrompt = MOCK_QUICK_PROMPTS.find((qp) => !qp.isDefault);
if (nonDefaultPrompt) {
const mockRowActions = (columns[2] as EuiTableComputedColumnType<QuickPrompt>).render(
const mockRowActions = (columns[2] as EuiTableComputedColumnType<PromptResponse>).render(
nonDefaultPrompt
);
expect(mockRowActions).toHaveProperty('props', {

View file

@ -7,10 +7,10 @@
import { EuiBasicTableColumn, EuiLink } from '@elastic/eui';
import React, { useCallback } from 'react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { BadgesColumn } from '../../common/components/assistant_settings_management/badges';
import { RowActions } from '../../common/components/assistant_settings_management/row_actions';
import { PromptContextTemplate } from '../../prompt_context/types';
import { QuickPrompt } from '../types';
import * as i18n from './translations';
export const useQuickPromptTable = () => {
@ -21,29 +21,29 @@ export const useQuickPromptTable = () => {
onDeleteActionClicked,
}: {
basePromptContexts: PromptContextTemplate[];
onEditActionClicked: (prompt: QuickPrompt) => void;
onDeleteActionClicked: (prompt: QuickPrompt) => void;
}): Array<EuiBasicTableColumn<QuickPrompt>> => [
onEditActionClicked: (prompt: PromptResponse) => void;
onDeleteActionClicked: (prompt: PromptResponse) => void;
}): Array<EuiBasicTableColumn<PromptResponse>> => [
{
align: 'left',
name: i18n.QUICK_PROMPTS_TABLE_COLUMN_NAME,
render: (prompt: QuickPrompt) =>
prompt?.title ? (
<EuiLink onClick={() => onEditActionClicked(prompt)}>{prompt?.title}</EuiLink>
render: (prompt: PromptResponse) =>
prompt?.name ? (
<EuiLink onClick={() => onEditActionClicked(prompt)}>{prompt?.name}</EuiLink>
) : null,
sortable: ({ title }: QuickPrompt) => title,
sortable: ({ name }: PromptResponse) => name,
},
{
align: 'left',
name: i18n.QUICK_PROMPTS_TABLE_COLUMN_CONTEXTS,
render: (prompt: QuickPrompt) => {
render: (prompt: PromptResponse) => {
const selectedPromptContexts = (
basePromptContexts.filter((bpc) =>
prompt?.categories?.some((cat) => bpc?.category === cat)
) ?? []
).map((bpc) => bpc?.description);
return selectedPromptContexts ? (
<BadgesColumn items={selectedPromptContexts} prefix={prompt.title} />
<BadgesColumn items={selectedPromptContexts} prefix={prompt.name} />
) : null;
},
},
@ -58,13 +58,13 @@ export const useQuickPromptTable = () => {
align: 'center',
name: i18n.QUICK_PROMPTS_TABLE_COLUMN_ACTIONS,
width: '120px',
render: (prompt: QuickPrompt) => {
render: (prompt: PromptResponse) => {
if (!prompt) {
return null;
}
const isDeletable = !prompt.isDefault;
return (
<RowActions<QuickPrompt>
<RowActions<PromptResponse>
rowItem={prompt}
onDelete={isDeletable ? onDeleteActionClicked : undefined}
onEdit={onEditActionClicked}

View file

@ -7,10 +7,10 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { QuickPrompts } from './quick_prompts';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt';
import { QUICK_PROMPTS_TAB } from '../settings/const';
import { QuickPrompts } from './quick_prompts';
const setInput = jest.fn();
const setIsSettingsModalVisible = jest.fn();
@ -20,6 +20,7 @@ const testProps = {
setIsSettingsModalVisible,
trackPrompt,
isFlyoutMode: false,
allPrompts: MOCK_QUICK_PROMPTS,
};
const setSelectedSettingsTab = jest.fn();
const mockUseAssistantContext = {

View file

@ -17,7 +17,10 @@ import {
import { useMeasure } from 'react-use';
import { css } from '@emotion/react';
import { QuickPrompt } from '../../..';
import {
PromptResponse,
PromptTypeEnum,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
import { QUICK_PROMPTS_TAB } from '../settings/const';
@ -30,6 +33,7 @@ interface QuickPromptsProps {
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
trackPrompt: (prompt: string) => void;
isFlyoutMode: boolean;
allPrompts: PromptResponse[];
}
/**
@ -38,11 +42,10 @@ interface QuickPromptsProps {
* and localstorage for storing new and edited prompts.
*/
export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(
({ setInput, setIsSettingsModalVisible, trackPrompt, isFlyoutMode }) => {
({ setInput, setIsSettingsModalVisible, trackPrompt, isFlyoutMode, allPrompts }) => {
const [quickPromptsContainerRef, { width }] = useMeasure();
const { allQuickPrompts, knowledgeBase, promptContexts, setSelectedSettingsTab } =
useAssistantContext();
const { knowledgeBase, promptContexts, setSelectedSettingsTab } = useAssistantContext();
const contextFilteredQuickPrompts = useMemo(() => {
const registeredPromptContextTitles = Object.values(promptContexts).map((pc) => pc.category);
@ -50,17 +53,21 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(
if (knowledgeBase.isEnabledKnowledgeBase) {
registeredPromptContextTitles.push(KNOWLEDGE_BASE_CATEGORY);
}
return allQuickPrompts.filter((quickPrompt) => {
return allPrompts.filter((prompt) => {
// only quick prompts
if (prompt.promptType !== PromptTypeEnum.quick) {
return false;
}
// Return quick prompt as match if it has no categories, otherwise ensure category exists in registered prompt contexts
if (quickPrompt.categories == null || quickPrompt.categories.length === 0) {
if (!prompt.categories || prompt.categories.length === 0) {
return true;
} else {
return quickPrompt.categories.some((category) => {
return prompt.categories?.some((category) => {
return registeredPromptContextTitles.includes(category);
});
}
});
}, [allQuickPrompts, knowledgeBase.isEnabledKnowledgeBase, promptContexts]);
}, [allPrompts, knowledgeBase.isEnabledKnowledgeBase, promptContexts]);
// Overflow state
const [isOverflowPopoverOpen, setIsOverflowPopoverOpen] = useState(false);
@ -71,10 +78,10 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(
const closeOverflowPopover = useCallback(() => setIsOverflowPopoverOpen(false), []);
const onClickAddQuickPrompt = useCallback(
(badge: QuickPrompt) => {
setInput(badge.prompt);
(badge: PromptResponse) => {
setInput(badge.content);
if (badge.isDefault) {
trackPrompt(badge.title);
trackPrompt(badge.name);
} else {
trackPrompt('Custom');
}
@ -83,7 +90,7 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(
);
const onClickOverflowQuickPrompt = useCallback(
(badge: QuickPrompt) => {
(badge: PromptResponse) => {
onClickAddQuickPrompt(badge);
closeOverflowPopover();
},
@ -137,9 +144,9 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(
<EuiBadge
color={badge.color}
onClick={() => onClickAddQuickPrompt(badge)}
onClickAriaLabel={badge.title}
onClickAriaLabel={badge.name}
>
{badge.title}
{badge.name}
</EuiBadge>
</EuiFlexItem>
))}
@ -172,9 +179,9 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(
<EuiBadge
color={badge.color}
onClick={() => onClickOverflowQuickPrompt(badge)}
onClickAriaLabel={badge.title}
onClickAriaLabel={badge.name}
>
{badge.title}
{badge.name}
</EuiBadge>
</EuiFlexItem>
))}

View file

@ -1,25 +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 { PromptContext } from '../../..';
/**
* A QuickPrompt is a badge that is displayed below the Assistant's input field. They provide
* a quick way for users to insert prompts as templates into the Assistant's input field. If no
* categories are provided they will always display with the assistant, however categories can be
* supplied to only display the QuickPrompt when the Assistant is registered with corresponding
* PromptContext's containing the same category.
*
* isDefault: If true, this QuickPrompt cannot be deleted by the user
*/
export interface QuickPrompt {
title: string;
prompt: string;
color: string;
categories?: Array<PromptContext['category']>;
isDefault?: boolean;
}

View file

@ -23,8 +23,9 @@ import {
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import { css } from '@emotion/react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { AIConnector } from '../../connectorland/connector_selector';
import { Conversation, Prompt, QuickPrompt, useLoadConnectors } from '../../..';
import { Conversation, useLoadConnectors } from '../../..';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
import { TEST_IDS } from '../constants';
@ -46,6 +47,7 @@ import {
QUICK_PROMPTS_TAB,
SYSTEM_PROMPTS_TAB,
} from './const';
import { useFetchPrompts } from '../api/prompts/use_fetch_prompts';
const StyledEuiModal = styled(EuiModal)`
width: 800px;
@ -97,6 +99,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
const { data: anonymizationFields, refetch: refetchAnonymizationFieldsResults } =
useFetchAnonymizationFields();
const { data: allPrompts } = useFetchPrompts();
const { data: connectors } = useLoadConnectors({
http,
@ -112,7 +115,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
setUpdatedAssistantStreamingEnabled,
setUpdatedKnowledgeBaseSettings,
setUpdatedQuickPromptSettings,
setUpdatedSystemPromptSettings,
promptsBulkActions,
saveSettings,
conversationsSettingsBulkActions,
updatedAnonymizationData,
@ -120,7 +123,9 @@ export const AssistantSettings: React.FC<Props> = React.memo(
anonymizationFieldsBulkActions,
setAnonymizationFieldsBulkActions,
setUpdatedAnonymizationData,
} = useSettingsUpdater(conversations, conversationsLoaded, anonymizationFields);
setPromptsBulkActions,
setUpdatedSystemPromptSettings,
} = useSettingsUpdater(conversations, allPrompts, conversationsLoaded, anonymizationFields);
// Local state for saving previously selected items so tab switching is friendlier
// Conversation Selection State
@ -137,21 +142,21 @@ export const AssistantSettings: React.FC<Props> = React.memo(
);
// Quick Prompt Selection State
const [selectedQuickPrompt, setSelectedQuickPrompt] = useState<QuickPrompt | undefined>();
const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: QuickPrompt) => {
const [selectedQuickPrompt, setSelectedQuickPrompt] = useState<PromptResponse | undefined>();
const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: PromptResponse) => {
setSelectedQuickPrompt(quickPrompt);
}, []);
useEffect(() => {
if (selectedQuickPrompt != null) {
setSelectedQuickPrompt(
quickPromptSettings.find((q) => q.title === selectedQuickPrompt.title)
quickPromptSettings.find((q) => q.name === selectedQuickPrompt.name)
);
}
}, [quickPromptSettings, selectedQuickPrompt]);
// System Prompt Selection State
const [selectedSystemPrompt, setSelectedSystemPrompt] = useState<Prompt | undefined>();
const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: Prompt) => {
const [selectedSystemPrompt, setSelectedSystemPrompt] = useState<PromptResponse | undefined>();
const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: PromptResponse) => {
setSelectedSystemPrompt(systemPrompt);
}, []);
useEffect(() => {
@ -342,6 +347,8 @@ export const AssistantSettings: React.FC<Props> = React.memo(
onSelectedQuickPromptChange={onHandleSelectedQuickPromptChange}
selectedQuickPrompt={selectedQuickPrompt}
setUpdatedQuickPromptSettings={setUpdatedQuickPromptSettings}
setPromptsBulkActions={setPromptsBulkActions}
promptsBulkActions={promptsBulkActions}
/>
)}
{selectedSettingsTab === SYSTEM_PROMPTS_TAB && (
@ -356,6 +363,8 @@ export const AssistantSettings: React.FC<Props> = React.memo(
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
setUpdatedSystemPromptSettings={setUpdatedSystemPromptSettings}
setPromptsBulkActions={setPromptsBulkActions}
promptsBulkActions={promptsBulkActions}
/>
)}
{selectedSettingsTab === ANONYMIZATION_TAB && (

View file

@ -8,6 +8,7 @@
import React, { useCallback } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import { AIConnector } from '../../connectorland/connector_selector';
import { Conversation } from '../../..';
import { AssistantSettings } from './assistant_settings';
@ -26,6 +27,9 @@ interface Props {
conversations: Record<string, Conversation>;
conversationsLoaded: boolean;
refetchConversationsState: () => Promise<void>;
refetchPrompts?: (
options?: RefetchOptions & RefetchQueryFilters<unknown>
) => Promise<QueryObserverResult<unknown, unknown>>;
}
/**
@ -43,6 +47,7 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
conversations,
conversationsLoaded,
refetchConversationsState,
refetchPrompts,
}) => {
const { toasts, setSelectedSettingsTab } = useAssistantContext();
@ -59,6 +64,9 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
async (success: boolean) => {
cleanupAndCloseModal();
await refetchConversationsState();
if (refetchPrompts) {
await refetchPrompts();
}
if (success) {
toasts?.addSuccess({
iconType: 'check',
@ -66,7 +74,7 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
});
}
},
[cleanupAndCloseModal, refetchConversationsState, toasts]
[cleanupAndCloseModal, refetchConversationsState, refetchPrompts, toasts]
);
const handleShowConversationSettings = useCallback(() => {

View file

@ -23,6 +23,7 @@ import {
QUICK_PROMPTS_TAB,
SYSTEM_PROMPTS_TAB,
} from './const';
import { mockSystemPrompts } from '../../mock/system_prompt';
const mockConversations = {
[alertConvo.title]: alertConvo,
@ -33,6 +34,8 @@ const saveSettings = jest.fn();
const mockValues = {
conversationSettings: mockConversations,
saveSettings,
systemPromptSettings: mockSystemPrompts,
quickPromptSettings: [],
};
const setSelectedSettingsTab = jest.fn();

View file

@ -19,7 +19,8 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/react';
import { Conversation, Prompt, QuickPrompt } from '../../..';
import { PromptResponse, PromptTypeEnum } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../..';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
import { useSettingsUpdater } from './use_settings_updater/use_settings_updater';
@ -42,6 +43,7 @@ import {
QUICK_PROMPTS_TAB,
SYSTEM_PROMPTS_TAB,
} from './const';
import { useFetchPrompts } from '../api/prompts/use_fetch_prompts';
interface Props {
conversations: Record<string, Conversation>;
@ -73,6 +75,9 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
const { data: anonymizationFields } = useFetchAnonymizationFields();
const { data: allPrompts } = useFetchPrompts();
// Connector details
const { data: connectors } = useLoadConnectors({
http,
});
@ -92,7 +97,7 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
setUpdatedAssistantStreamingEnabled,
setUpdatedKnowledgeBaseSettings,
setUpdatedQuickPromptSettings,
setUpdatedSystemPromptSettings,
setPromptsBulkActions,
saveSettings,
conversationsSettingsBulkActions,
updatedAnonymizationData,
@ -100,13 +105,32 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
anonymizationFieldsBulkActions,
setAnonymizationFieldsBulkActions,
setUpdatedAnonymizationData,
setUpdatedSystemPromptSettings,
promptsBulkActions,
resetSettings,
} = useSettingsUpdater(
conversations,
allPrompts,
conversationsLoaded,
anonymizationFields ?? { page: 0, perPage: 0, total: 0, data: [] }
);
const quickPrompts = useMemo(
() =>
quickPromptSettings.length === 0
? allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.quick)
: quickPromptSettings,
[allPrompts.data, quickPromptSettings]
);
const systemPrompts = useMemo(
() =>
systemPromptSettings.length === 0
? allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.system)
: systemPromptSettings,
[allPrompts.data, systemPromptSettings]
);
// Local state for saving previously selected items so tab switching is friendlier
// Conversation Selection State
const [selectedConversation, setSelectedConversation] = useState<Conversation | undefined>(
@ -136,21 +160,21 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
}, [selectedSettingsTab, setSelectedSettingsTab]);
// Quick Prompt Selection State
const [selectedQuickPrompt, setSelectedQuickPrompt] = useState<QuickPrompt | undefined>();
const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: QuickPrompt) => {
const [selectedQuickPrompt, setSelectedQuickPrompt] = useState<PromptResponse | undefined>();
const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: PromptResponse) => {
setSelectedQuickPrompt(quickPrompt);
}, []);
useEffect(() => {
if (selectedQuickPrompt != null) {
setSelectedQuickPrompt(
quickPromptSettings.find((q) => q.title === selectedQuickPrompt.title)
quickPromptSettings.find((q) => q.name === selectedQuickPrompt.name)
);
}
}, [quickPromptSettings, selectedQuickPrompt]);
// System Prompt Selection State
const [selectedSystemPrompt, setSelectedSystemPrompt] = useState<Prompt | undefined>();
const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: Prompt) => {
const [selectedSystemPrompt, setSelectedSystemPrompt] = useState<PromptResponse | undefined>();
const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: PromptResponse) => {
setSelectedSystemPrompt(systemPrompt);
}, []);
useEffect(() => {
@ -303,7 +327,9 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
setConversationSettings={setConversationSettings}
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
setUpdatedSystemPromptSettings={setUpdatedSystemPromptSettings}
systemPromptSettings={systemPromptSettings}
systemPromptSettings={systemPrompts}
promptsBulkActions={promptsBulkActions}
setPromptsBulkActions={setPromptsBulkActions}
/>
)}
{selectedSettingsTab === QUICK_PROMPTS_TAB && (
@ -311,10 +337,12 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
handleSave={handleSave}
onCancelClick={onCancelClick}
onSelectedQuickPromptChange={onHandleSelectedQuickPromptChange}
quickPromptSettings={quickPromptSettings}
quickPromptSettings={quickPrompts}
resetSettings={resetSettings}
selectedQuickPrompt={selectedQuickPrompt}
setUpdatedQuickPromptSettings={setUpdatedQuickPromptSettings}
promptsBulkActions={promptsBulkActions}
setPromptsBulkActions={setPromptsBulkActions}
/>
)}
{selectedSettingsTab === ANONYMIZATION_TAB && (

View file

@ -9,13 +9,9 @@ import { act, renderHook } from '@testing-library/react-hooks';
import { DEFAULT_LATEST_ALERTS } from '../../../assistant_context/constants';
import { alertConvo, welcomeConvo } from '../../../mock/conversation';
import { useSettingsUpdater } from './use_settings_updater';
import { Prompt } from '../../../..';
import {
defaultSystemPrompt,
mockSuperheroSystemPrompt,
mockSystemPrompt,
} from '../../../mock/system_prompt';
import { defaultQuickPrompt, mockSystemPrompt } from '../../../mock/system_prompt';
import { HttpSetup } from '@kbn/core/public';
import { PromptResponse } from '@kbn/elastic-assistant-common';
const mockConversations = {
[alertConvo.title]: alertConvo,
@ -27,8 +23,8 @@ const mockHttp = {
fetch: jest.fn(),
} as unknown as HttpSetup;
const mockSystemPrompts: Prompt[] = [mockSystemPrompt];
const mockQuickPrompts: Prompt[] = [defaultSystemPrompt];
const mockSystemPrompts: PromptResponse[] = [mockSystemPrompt];
const mockQuickPrompts: PromptResponse[] = [defaultQuickPrompt];
const anonymizationFields = {
total: 2,
@ -40,8 +36,6 @@ const anonymizationFields = {
],
};
const setAllQuickPromptsMock = jest.fn();
const setAllSystemPromptsMock = jest.fn();
const setAssistantStreamingEnabled = jest.fn();
const setKnowledgeBaseMock = jest.fn();
const reportAssistantSettingToggled = jest.fn();
@ -58,8 +52,6 @@ const mockValues = {
latestAlerts: DEFAULT_LATEST_ALERTS,
},
baseConversations: {},
setAllQuickPrompts: setAllQuickPromptsMock,
setAllSystemPrompts: setAllSystemPromptsMock,
setKnowledgeBase: setKnowledgeBaseMock,
http: mockHttp,
anonymizationFieldsBulkActions: {},
@ -67,8 +59,18 @@ const mockValues = {
const updatedValues = {
conversations: { ...mockConversations },
allSystemPrompts: [mockSuperheroSystemPrompt],
allQuickPrompts: [{ title: 'Prompt 2', prompt: 'Prompt 2', color: 'red' }],
allSystemPrompts: [mockSystemPrompt],
allQuickPrompts: [
{
consumer: 'securitySolutionUI',
content:
'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:',
id: 'default-system-prompt',
name: 'Default system prompt',
promptType: 'quick',
color: 'red',
},
],
updatedAnonymizationData: {
total: 2,
page: 1,
@ -101,23 +103,31 @@ describe('useSettingsUpdater', () => {
it('should set all state variables to their initial values when resetSettings is called', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields)
useSettingsUpdater(
mockConversations,
{
data: [...mockSystemPrompts, ...mockQuickPrompts],
page: 1,
perPage: 100,
total: 10,
},
conversationsLoaded,
anonymizationFields
)
);
await waitForNextUpdate();
const {
setConversationSettings,
setConversationsSettingsBulkActions,
setUpdatedQuickPromptSettings,
setUpdatedSystemPromptSettings,
setUpdatedKnowledgeBaseSettings,
setUpdatedAssistantStreamingEnabled,
resetSettings,
setPromptsBulkActions,
} = result.current;
setConversationSettings(updatedValues.conversations);
setConversationsSettingsBulkActions({});
setUpdatedQuickPromptSettings(updatedValues.allQuickPrompts);
setUpdatedSystemPromptSettings(updatedValues.allSystemPrompts);
setPromptsBulkActions({});
setUpdatedAnonymizationData(updatedValues.updatedAnonymizationData);
setUpdatedKnowledgeBaseSettings(updatedValues.knowledgeBase);
setUpdatedAssistantStreamingEnabled(updatedValues.assistantStreamingEnabled);
@ -149,23 +159,31 @@ describe('useSettingsUpdater', () => {
it('should update all state variables to their updated values when saveSettings is called', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields)
useSettingsUpdater(
mockConversations,
{
data: mockSystemPrompts,
page: 1,
perPage: 100,
total: 10,
},
conversationsLoaded,
anonymizationFields
)
);
await waitForNextUpdate();
const {
setConversationSettings,
setConversationsSettingsBulkActions,
setUpdatedQuickPromptSettings,
setUpdatedSystemPromptSettings,
setAnonymizationFieldsBulkActions,
setUpdatedKnowledgeBaseSettings,
setPromptsBulkActions,
} = result.current;
setConversationSettings(updatedValues.conversations);
setConversationsSettingsBulkActions({ delete: { ids: ['1'] } });
setAnonymizationFieldsBulkActions({ delete: { ids: ['1'] } });
setUpdatedQuickPromptSettings(updatedValues.allQuickPrompts);
setUpdatedSystemPromptSettings(updatedValues.allSystemPrompts);
setPromptsBulkActions({});
setUpdatedAnonymizationData(updatedValues.updatedAnonymizationData);
setUpdatedKnowledgeBaseSettings(updatedValues.knowledgeBase);
@ -179,8 +197,6 @@ describe('useSettingsUpdater', () => {
body: '{"delete":{"ids":["1"]}}',
}
);
expect(setAllQuickPromptsMock).toHaveBeenCalledWith(updatedValues.allQuickPrompts);
expect(setAllSystemPromptsMock).toHaveBeenCalledWith(updatedValues.allSystemPrompts);
expect(setUpdatedAnonymizationData).toHaveBeenCalledWith(
updatedValues.updatedAnonymizationData
);
@ -190,7 +206,17 @@ describe('useSettingsUpdater', () => {
it('should track which toggles have been updated when saveSettings is called', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields)
useSettingsUpdater(
mockConversations,
{
data: mockSystemPrompts,
page: 1,
perPage: 100,
total: 10,
},
conversationsLoaded,
anonymizationFields
)
);
await waitForNextUpdate();
const { setUpdatedKnowledgeBaseSettings } = result.current;
@ -207,7 +233,17 @@ describe('useSettingsUpdater', () => {
it('should track only toggles that updated', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields)
useSettingsUpdater(
mockConversations,
{
data: mockSystemPrompts,
page: 1,
perPage: 100,
total: 10,
},
conversationsLoaded,
anonymizationFields
)
);
await waitForNextUpdate();
const { setUpdatedKnowledgeBaseSettings } = result.current;
@ -225,7 +261,17 @@ describe('useSettingsUpdater', () => {
it('if no toggles update, do not track anything', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields)
useSettingsUpdater(
mockConversations,
{
data: mockSystemPrompts,
page: 1,
perPage: 100,
total: 10,
},
conversationsLoaded,
anonymizationFields
)
);
await waitForNextUpdate();
const { setUpdatedKnowledgeBaseSettings } = result.current;

View file

@ -8,7 +8,13 @@
import React, { useCallback, useEffect, useState } from 'react';
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
import { PerformBulkActionRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
import { Conversation, Prompt, QuickPrompt } from '../../../..';
import {
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
PromptResponse,
PromptTypeEnum,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { FindPromptsResponse } from '@kbn/elastic-assistant-common/impl/schemas/prompts/find_prompts_route.gen';
import { Conversation } from '../../../..';
import { useAssistantContext } from '../../../assistant_context';
import type { KnowledgeBaseConfig } from '../../types';
import {
@ -16,6 +22,7 @@ import {
bulkUpdateConversations,
} from '../../api/conversations/bulk_update_actions_conversations';
import { bulkUpdateAnonymizationFields } from '../../api/anonymization_fields/bulk_update_anonymization_fields';
import { bulkUpdatePrompts } from '../../api/prompts/bulk_update_prompts';
interface UseSettingsUpdater {
assistantStreamingEnabled: boolean;
@ -23,9 +30,9 @@ interface UseSettingsUpdater {
conversationsSettingsBulkActions: ConversationsBulkActions;
updatedAnonymizationData: FindAnonymizationFieldsResponse;
knowledgeBase: KnowledgeBaseConfig;
quickPromptSettings: QuickPrompt[];
quickPromptSettings: PromptResponse[];
resetSettings: () => void;
systemPromptSettings: Prompt[];
systemPromptSettings: PromptResponse[];
setUpdatedAnonymizationData: React.Dispatch<
React.SetStateAction<FindAnonymizationFieldsResponse>
>;
@ -37,26 +44,25 @@ interface UseSettingsUpdater {
setAnonymizationFieldsBulkActions: React.Dispatch<
React.SetStateAction<PerformBulkActionRequestBody>
>;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
setUpdatedKnowledgeBaseSettings: React.Dispatch<React.SetStateAction<KnowledgeBaseConfig>>;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<QuickPrompt[]>>;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
setUpdatedAssistantStreamingEnabled: React.Dispatch<React.SetStateAction<boolean>>;
saveSettings: () => Promise<boolean>;
}
export const useSettingsUpdater = (
conversations: Record<string, Conversation>,
allPrompts: FindPromptsResponse,
conversationsLoaded: boolean,
anonymizationFields: FindAnonymizationFieldsResponse
): UseSettingsUpdater => {
// Initial state from assistant context
const {
allQuickPrompts,
allSystemPrompts,
assistantTelemetry,
knowledgeBase,
setAllQuickPrompts,
setAllSystemPrompts,
assistantStreamingEnabled,
setAssistantStreamingEnabled,
setKnowledgeBase,
@ -73,14 +79,20 @@ export const useSettingsUpdater = (
const [conversationsSettingsBulkActions, setConversationsSettingsBulkActions] =
useState<ConversationsBulkActions>({});
// Quick Prompts
const [updatedQuickPromptSettings, setUpdatedQuickPromptSettings] =
useState<QuickPrompt[]>(allQuickPrompts);
const [quickPromptSettings, setUpdatedQuickPromptSettings] = useState<PromptResponse[]>(
allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.quick)
);
// System Prompts
const [updatedSystemPromptSettings, setUpdatedSystemPromptSettings] =
useState<Prompt[]>(allSystemPrompts);
const [systemPromptSettings, setUpdatedSystemPromptSettings] = useState<PromptResponse[]>(
allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.system)
);
// Anonymization
const [anonymizationFieldsBulkActions, setAnonymizationFieldsBulkActions] =
useState<PerformBulkActionRequestBody>({});
// Prompts
const [promptsBulkActions, setPromptsBulkActions] = useState<PromptsPerformBulkActionRequestBody>(
{}
);
const [updatedAnonymizationData, setUpdatedAnonymizationData] =
useState<FindAnonymizationFieldsResponse>(anonymizationFields);
const [updatedAssistantStreamingEnabled, setUpdatedAssistantStreamingEnabled] =
@ -95,31 +107,57 @@ export const useSettingsUpdater = (
const resetSettings = useCallback((): void => {
setConversationSettings(conversations);
setConversationsSettingsBulkActions({});
setUpdatedQuickPromptSettings(allQuickPrompts);
setUpdatedQuickPromptSettings(
allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.quick)
);
setUpdatedKnowledgeBaseSettings(knowledgeBase);
setUpdatedAssistantStreamingEnabled(assistantStreamingEnabled);
setUpdatedSystemPromptSettings(allSystemPrompts);
setUpdatedSystemPromptSettings(
allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.system)
);
setUpdatedAnonymizationData(anonymizationFields);
}, [
allQuickPrompts,
allSystemPrompts,
anonymizationFields,
assistantStreamingEnabled,
conversations,
knowledgeBase,
]);
}, [allPrompts, anonymizationFields, assistantStreamingEnabled, conversations, knowledgeBase]);
const hasBulkConversations =
conversationsSettingsBulkActions.create ||
conversationsSettingsBulkActions.update ||
conversationsSettingsBulkActions.delete;
const hasBulkAnonymizationFields =
anonymizationFieldsBulkActions.create ||
anonymizationFieldsBulkActions.update ||
anonymizationFieldsBulkActions.delete;
const hasBulkPrompts =
promptsBulkActions.create || promptsBulkActions.update || promptsBulkActions.delete;
/**
* Save all pending settings
*/
const saveSettings = useCallback(async (): Promise<boolean> => {
setAllQuickPrompts(updatedQuickPromptSettings);
setAllSystemPrompts(updatedSystemPromptSettings);
const bulkPromptsResult = hasBulkPrompts
? await bulkUpdatePrompts(http, promptsBulkActions, toasts)
: undefined;
// replace conversation references for created
if (bulkPromptsResult) {
bulkPromptsResult.attributes.results.created.forEach((p) => {
if (conversationsSettingsBulkActions.create) {
Object.values(conversationsSettingsBulkActions.create).forEach((c) => {
if (c.apiConfig?.defaultSystemPromptId === p.name) {
c.apiConfig.defaultSystemPromptId = p.id;
}
});
}
if (conversationsSettingsBulkActions.update) {
Object.values(conversationsSettingsBulkActions.update).forEach((c) => {
if (c.apiConfig?.defaultSystemPromptId === p.name) {
c.apiConfig.defaultSystemPromptId = p.id;
}
});
}
});
}
const hasBulkConversations =
conversationsSettingsBulkActions.create ||
conversationsSettingsBulkActions.update ||
conversationsSettingsBulkActions.delete;
const bulkResult = hasBulkConversations
? await bulkUpdateConversations(http, conversationsSettingsBulkActions, toasts)
: undefined;
@ -145,21 +183,20 @@ export const useSettingsUpdater = (
}
setAssistantStreamingEnabled(updatedAssistantStreamingEnabled);
setKnowledgeBase(updatedKnowledgeBaseSettings);
const hasBulkAnonymizationFields =
anonymizationFieldsBulkActions.create ||
anonymizationFieldsBulkActions.update ||
anonymizationFieldsBulkActions.delete;
const bulkAnonymizationFieldsResult = hasBulkAnonymizationFields
? await bulkUpdateAnonymizationFields(http, anonymizationFieldsBulkActions, toasts)
: undefined;
return (bulkResult?.success ?? true) && (bulkAnonymizationFieldsResult?.success ?? true);
return (
(bulkResult?.success ?? true) &&
(bulkAnonymizationFieldsResult?.success ?? true) &&
(bulkPromptsResult?.success ?? true)
);
}, [
setAllQuickPrompts,
updatedQuickPromptSettings,
setAllSystemPrompts,
updatedSystemPromptSettings,
conversationsSettingsBulkActions,
hasBulkConversations,
http,
conversationsSettingsBulkActions,
toasts,
knowledgeBase.isEnabledKnowledgeBase,
knowledgeBase.isEnabledRAGAlerts,
@ -168,7 +205,10 @@ export const useSettingsUpdater = (
updatedAssistantStreamingEnabled,
setAssistantStreamingEnabled,
setKnowledgeBase,
hasBulkAnonymizationFields,
anonymizationFieldsBulkActions,
hasBulkPrompts,
promptsBulkActions,
assistantTelemetry,
]);
@ -200,9 +240,9 @@ export const useSettingsUpdater = (
conversationsSettingsBulkActions,
knowledgeBase: updatedKnowledgeBaseSettings,
assistantStreamingEnabled: updatedAssistantStreamingEnabled,
quickPromptSettings: updatedQuickPromptSettings,
quickPromptSettings,
resetSettings,
systemPromptSettings: updatedSystemPromptSettings,
systemPromptSettings,
saveSettings,
updatedAnonymizationData,
setUpdatedAnonymizationData,
@ -214,5 +254,7 @@ export const useSettingsUpdater = (
setUpdatedSystemPromptSettings,
setConversationSettings,
setConversationsSettingsBulkActions,
promptsBulkActions,
setPromptsBulkActions,
};
};

View file

@ -5,19 +5,6 @@
* 2.0.
*/
export type PromptType = 'system' | 'user';
export interface Prompt {
id: string;
content: string;
name: string;
promptType: PromptType;
isDefault?: boolean; // TODO: Should be renamed to isImmutable as this flag is used to prevent users from deleting prompts
isNewConversationDefault?: boolean;
isFlyoutMode?: boolean;
label?: string;
}
export interface KnowledgeBaseConfig {
isEnabledRAGAlerts: boolean;
isEnabledKnowledgeBase: boolean;

View file

@ -13,7 +13,8 @@ import {
getDefaultSystemPrompt,
} from './helpers';
import { AIConnector } from '../../connectorland/connector_selector';
import { Conversation, Prompt } from '../../..';
import { Conversation } from '../../..';
import { PromptResponse } from '@kbn/elastic-assistant-common';
const tilde = '`';
const codeDelimiter = '```';
@ -61,28 +62,28 @@ ${codeDelimiter}
This query will filter the events based on the condition that the ${tilde}user.name${tilde} field should exactly match the value \"9dcc9960-78cf-4ef6-9a2e-dbd5816daa60\".`;
describe('useConversation helpers', () => {
const allSystemPrompts: Prompt[] = [
const allSystemPrompts: PromptResponse[] = [
{
id: '1',
content: 'Prompt 1',
name: 'Prompt 1',
promptType: 'user',
promptType: 'quick',
},
{
id: '2',
content: 'Prompt 2',
name: 'Prompt 2',
promptType: 'user',
promptType: 'quick',
isNewConversationDefault: true,
},
{
id: '3',
content: 'Prompt 3',
name: 'Prompt 3',
promptType: 'user',
promptType: 'quick',
},
];
const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter(
const allSystemPromptsNoDefault: PromptResponse[] = allSystemPrompts.filter(
({ isNewConversationDefault }) => isNewConversationDefault !== true
);
@ -237,25 +238,25 @@ describe('useConversation helpers', () => {
});
describe('getConversationApiConfig', () => {
const allSystemPrompts: Prompt[] = [
const allSystemPrompts: PromptResponse[] = [
{
id: '1',
content: 'Prompt 1',
name: 'Prompt 1',
promptType: 'user',
promptType: 'quick',
},
{
id: '2',
content: 'Prompt 2',
name: 'Prompt 2',
promptType: 'user',
promptType: 'quick',
isNewConversationDefault: true,
},
{
id: '3',
content: 'Prompt 3',
name: 'Prompt 3',
promptType: 'user',
promptType: 'quick',
},
];
@ -390,7 +391,7 @@ describe('getConversationApiConfig', () => {
});
test('should return the first system prompt if both conversation system prompt and default new system prompt do not exist', () => {
const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter(
const allSystemPromptsNoDefault: PromptResponse[] = allSystemPrompts.filter(
({ isNewConversationDefault }) => isNewConversationDefault !== true
);
@ -418,7 +419,7 @@ describe('getConversationApiConfig', () => {
});
test('should return the first system prompt if conversation system prompt does not exist within all system prompts', () => {
const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter(
const allSystemPromptsNoDefault: PromptResponse[] = allSystemPrompts.filter(
({ isNewConversationDefault }) => isNewConversationDefault !== true
);

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { Prompt } from '../types';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../assistant_context/types';
import { AIConnector } from '../../connectorland/connector_selector';
import { getGenAiConfig } from '../../connectorland/helpers';
@ -75,7 +75,7 @@ export const analyzeMarkdown = (markdown: string): CodeBlockDetails[] => {
*
* @param allSystemPrompts All available System Prompts
*/
export const getDefaultNewSystemPrompt = (allSystemPrompts: Prompt[]) =>
export const getDefaultNewSystemPrompt = (allSystemPrompts: PromptResponse[]) =>
allSystemPrompts.find((prompt) => prompt.isNewConversationDefault) ?? allSystemPrompts?.[0];
/**
@ -88,15 +88,15 @@ export const getDefaultSystemPrompt = ({
allSystemPrompts,
conversation,
}: {
allSystemPrompts: Prompt[];
allSystemPrompts: PromptResponse[];
conversation: Conversation | undefined;
}): Prompt | undefined => {
}): PromptResponse | undefined => {
const conversationSystemPrompt = allSystemPrompts.find(
(prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId
);
const defaultNewSystemPrompt = getDefaultNewSystemPrompt(allSystemPrompts);
return conversationSystemPrompt ?? defaultNewSystemPrompt;
return conversationSystemPrompt?.id ? conversationSystemPrompt : defaultNewSystemPrompt;
};
/**
@ -109,9 +109,9 @@ export const getInitialDefaultSystemPrompt = ({
allSystemPrompts,
conversation,
}: {
allSystemPrompts: Prompt[];
allSystemPrompts: PromptResponse[];
conversation: Conversation | undefined;
}): Prompt | undefined => {
}): PromptResponse | undefined => {
const conversationSystemPrompt = allSystemPrompts.find(
(prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId
);
@ -133,7 +133,7 @@ export const getConversationApiConfig = ({
connectors,
defaultConnector,
}: {
allSystemPrompts: Prompt[];
allSystemPrompts: PromptResponse[];
conversation: Conversation;
connectors?: AIConnector[];
defaultConnector?: AIConnector;

View file

@ -18,6 +18,7 @@ import {
updateConversation,
} from '../api/conversations';
import { WELCOME_CONVERSATION } from './sample_conversations';
import { useFetchPrompts } from '../api/prompts/use_fetch_prompts';
export const DEFAULT_CONVERSATION_STATE: Conversation = {
id: '',
@ -63,7 +64,10 @@ interface UseConversation {
}
export const useConversation = (): UseConversation => {
const { allSystemPrompts, http, toasts } = useAssistantContext();
const { http, toasts } = useAssistantContext();
const {
data: { data: allPrompts },
} = useFetchPrompts();
const getConversation = useCallback(
async (conversationId: string, silent?: boolean) => {
@ -101,7 +105,7 @@ export const useConversation = (): UseConversation => {
async (conversation: Conversation) => {
if (conversation.apiConfig) {
const defaultSystemPromptId = getDefaultSystemPrompt({
allSystemPrompts,
allSystemPrompts: allPrompts,
conversation,
})?.id;
@ -115,7 +119,7 @@ export const useConversation = (): UseConversation => {
});
}
},
[allSystemPrompts, http, toasts]
[allPrompts, http, toasts]
);
/**

View file

@ -25,17 +25,13 @@ import type { Conversation } from './types';
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 { KnowledgeBaseConfig, Prompt, TraceOptions } from '../assistant/types';
import { BASE_SYSTEM_PROMPTS } from '../content/prompts/system';
import { KnowledgeBaseConfig, TraceOptions } from '../assistant/types';
import {
DEFAULT_ASSISTANT_NAMESPACE,
DEFAULT_KNOWLEDGE_BASE_SETTINGS,
KNOWLEDGE_BASE_LOCAL_STORAGE_KEY,
LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY,
QUICK_PROMPT_LOCAL_STORAGE_KEY,
STREAMING_LOCAL_STORAGE_KEY,
SYSTEM_PROMPT_LOCAL_STORAGE_KEY,
TRACE_OPTIONS_SESSION_STORAGE_KEY,
} from './constants';
import { AssistantAvailability, AssistantTelemetry } from './types';
@ -65,8 +61,6 @@ export interface AssistantProviderProps {
) => CodeBlockDetails[][];
basePath: string;
basePromptContexts?: PromptContextTemplate[];
baseQuickPrompts?: QuickPrompt[];
baseSystemPrompts?: Prompt[];
docLinks: Omit<DocLinksStart, 'links'>;
children: React.ReactNode;
getComments: (commentArgs: {
@ -87,6 +81,7 @@ export interface AssistantProviderProps {
navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise<void>;
title?: string;
toasts?: IToasts;
currentAppId: string;
}
export interface UserAvatar {
@ -106,13 +101,8 @@ export interface UseAssistantContext {
currentConversation: Conversation,
showAnonymizedValues: boolean
) => CodeBlockDetails[][];
allQuickPrompts: QuickPrompt[];
allSystemPrompts: Prompt[];
docLinks: Omit<DocLinksStart, 'links'>;
basePath: string;
basePromptContexts: PromptContextTemplate[];
baseQuickPrompts: QuickPrompt[];
baseSystemPrompts: Prompt[];
baseConversations: Record<string, Conversation>;
getComments: (commentArgs: {
abortStream: () => void;
@ -134,8 +124,6 @@ export interface UseAssistantContext {
nameSpace: string;
registerPromptContext: RegisterPromptContext;
selectedSettingsTab: SettingsTabs | null;
setAllQuickPrompts: React.Dispatch<React.SetStateAction<QuickPrompt[] | undefined>>;
setAllSystemPrompts: React.Dispatch<React.SetStateAction<Prompt[] | undefined>>;
setAssistantStreamingEnabled: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setKnowledgeBase: React.Dispatch<React.SetStateAction<KnowledgeBaseConfig | undefined>>;
setLastConversationId: React.Dispatch<React.SetStateAction<string | undefined>>;
@ -150,7 +138,9 @@ export interface UseAssistantContext {
title: string;
toasts: IToasts | undefined;
traceOptions: TraceOptions;
basePromptContexts: PromptContextTemplate[];
unRegisterPromptContext: UnRegisterPromptContext;
currentAppId: string;
}
const AssistantContext = React.createContext<UseAssistantContext | undefined>(undefined);
@ -164,8 +154,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
docLinks,
basePath,
basePromptContexts = [],
baseQuickPrompts = [],
baseSystemPrompts = BASE_SYSTEM_PROMPTS,
children,
getComments,
http,
@ -174,6 +162,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
nameSpace = DEFAULT_ASSISTANT_NAMESPACE,
title = DEFAULT_ASSISTANT_TITLE,
toasts,
currentAppId,
}) => {
/**
* Session storage for traceOptions, including APM URL and LangSmith Project/API Key
@ -189,22 +178,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
defaultTraceOptions
);
/**
* Local storage for all quick prompts, prefixed by assistant nameSpace
*/
const [localStorageQuickPrompts, setLocalStorageQuickPrompts] = useLocalStorage(
`${nameSpace}.${QUICK_PROMPT_LOCAL_STORAGE_KEY}`,
baseQuickPrompts
);
/**
* Local storage for all system prompts, prefixed by assistant nameSpace
*/
const [localStorageSystemPrompts, setLocalStorageSystemPrompts] = useLocalStorage(
`${nameSpace}.${SYSTEM_PROMPT_LOCAL_STORAGE_KEY}`,
baseSystemPrompts
);
const [localStorageLastConversationId, setLocalStorageLastConversationId] =
useLocalStorage<string>(`${nameSpace}.${LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY}`);
@ -290,12 +263,8 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
assistantFeatures: assistantFeatures ?? defaultAssistantFeatures,
assistantTelemetry,
augmentMessageCodeBlocks,
allQuickPrompts: localStorageQuickPrompts ?? [],
allSystemPrompts: localStorageSystemPrompts ?? [],
basePath,
basePromptContexts,
baseQuickPrompts,
baseSystemPrompts,
docLinks,
getComments,
http,
@ -308,8 +277,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
// can be undefined from localStorage, if not defined, default to true
assistantStreamingEnabled: localStorageStreaming ?? true,
setAssistantStreamingEnabled: setLocalStorageStreaming,
setAllQuickPrompts: setLocalStorageQuickPrompts,
setAllSystemPrompts: setLocalStorageSystemPrompts,
setKnowledgeBase: setLocalStorageKnowledgeBase,
setSelectedSettingsTab,
setShowAssistantOverlay,
@ -322,6 +289,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
getLastConversationId,
setLastConversationId: setLocalStorageLastConversationId,
baseConversations,
currentAppId,
}),
[
actionTypeRegistry,
@ -330,12 +298,8 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
assistantFeatures,
assistantTelemetry,
augmentMessageCodeBlocks,
localStorageQuickPrompts,
localStorageSystemPrompts,
basePath,
basePromptContexts,
baseQuickPrompts,
baseSystemPrompts,
docLinks,
getComments,
http,
@ -347,8 +311,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
selectedSettingsTab,
localStorageStreaming,
setLocalStorageStreaming,
setLocalStorageQuickPrompts,
setLocalStorageSystemPrompts,
setLocalStorageKnowledgeBase,
setSessionStorageTraceOptions,
showAssistantOverlay,
@ -359,6 +321,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
getLastConversationId,
setLocalStorageLastConversationId,
baseConversations,
currentAppId,
]
);

View file

@ -1,36 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Prompt } from '../../../..';
import {
DEFAULT_SYSTEM_PROMPT_LABEL,
DEFAULT_SYSTEM_PROMPT_NAME,
DEFAULT_SYSTEM_PROMPT_NON_I18N,
SUPERHERO_SYSTEM_PROMPT_LABEL,
SUPERHERO_SYSTEM_PROMPT_NAME,
SUPERHERO_SYSTEM_PROMPT_NON_I18N,
} from './translations';
/**
* Base System Prompts for Elastic AI Assistant (if not overridden on initialization).
*/
export const BASE_SYSTEM_PROMPTS: Prompt[] = [
{
id: 'default-system-prompt',
content: DEFAULT_SYSTEM_PROMPT_NON_I18N,
name: DEFAULT_SYSTEM_PROMPT_NAME,
promptType: 'system',
label: DEFAULT_SYSTEM_PROMPT_LABEL,
},
{
id: 'CB9FA555-B59F-4F71-AFF9-8A891AC5BC28',
content: SUPERHERO_SYSTEM_PROMPT_NON_I18N,
name: SUPERHERO_SYSTEM_PROMPT_NAME,
promptType: 'system',
label: SUPERHERO_SYSTEM_PROMPT_LABEL,
},
];

View file

@ -1,26 +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 THEN_SUMMARIZE_SUGGESTED_KQL_AND_EQL_QUERIES = i18n.translate(
'xpack.elasticAssistant.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries',
{
defaultMessage:
'Evaluate the event from the context above and format your output neatly in markdown syntax for my Elastic Security case.',
}
);
export const FINALLY_SUGGEST_INVESTIGATION_GUIDE_AND_FORMAT_AS_MARKDOWN = i18n.translate(
'xpack.elasticAssistant.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown',
{
defaultMessage: `Add your description, recommended actions and bulleted triage steps. Use the MITRE ATT&CK data provided to add more context and recommendations from MITRE, and hyperlink to the relevant pages on MITRE\'s website. Be sure to include the user and host risk score data from the context. Your response should include steps that point to Elastic Security specific features, including endpoint response actions, the Elastic Agent OSQuery manager integration (with example osquery queries), timelines and entity analytics and link to all the relevant Elastic Security documentation.`,
}
);
export const EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N = `${THEN_SUMMARIZE_SUGGESTED_KQL_AND_EQL_QUERIES}
${FINALLY_SUGGEST_INVESTIGATION_GUIDE_AND_FORMAT_AS_MARKDOWN}`;

View file

@ -5,52 +5,69 @@
* 2.0.
*/
import { QuickPrompt } from '../..';
import {
PromptResponse,
PromptTypeEnum,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
export const MOCK_QUICK_PROMPTS: QuickPrompt[] = [
export const MOCK_QUICK_PROMPTS: PromptResponse[] = [
{
title: 'ALERT_SUMMARIZATION_TITLE',
prompt: 'ALERT_SUMMARIZATION_PROMPT',
name: 'ALERT_SUMMARIZATION_TITLE',
content: 'ALERT_SUMMARIZATION_PROMPT',
color: '#F68FBE',
categories: ['PROMPT_CONTEXT_ALERT_CATEGORY'],
isDefault: true,
id: 'ALERT_SUMMARIZATION_TITLE',
promptType: PromptTypeEnum.quick,
},
{
title: 'RULE_CREATION_TITLE',
prompt: 'RULE_CREATION_PROMPT',
name: 'RULE_CREATION_TITLE',
content: 'RULE_CREATION_PROMPT',
categories: ['PROMPT_CONTEXT_DETECTION_RULES_CATEGORY'],
color: '#7DDED8',
isDefault: true,
id: 'RULE_CREATION_TITLE',
promptType: PromptTypeEnum.quick,
},
{
title: 'WORKFLOW_ANALYSIS_TITLE',
prompt: 'WORKFLOW_ANALYSIS_PROMPT',
name: 'WORKFLOW_ANALYSIS_TITLE',
content: 'WORKFLOW_ANALYSIS_PROMPT',
color: '#36A2EF',
isDefault: true,
id: 'WORKFLOW_ANALYSIS_TITLE',
promptType: PromptTypeEnum.quick,
},
{
title: 'THREAT_INVESTIGATION_GUIDES_TITLE',
prompt: 'THREAT_INVESTIGATION_GUIDES_PROMPT',
name: 'THREAT_INVESTIGATION_GUIDES_TITLE',
content: 'THREAT_INVESTIGATION_GUIDES_PROMPT',
categories: ['PROMPT_CONTEXT_EVENT_CATEGORY'],
color: '#F3D371',
isDefault: true,
id: 'THREAT_INVESTIGATION_GUIDES_TITLE',
promptType: PromptTypeEnum.quick,
},
{
title: 'SPL_QUERY_CONVERSION_TITLE',
prompt: 'SPL_QUERY_CONVERSION_PROMPT',
name: 'SPL_QUERY_CONVERSION_TITLE',
content: 'SPL_QUERY_CONVERSION_PROMPT',
color: '#BADA55',
isDefault: true,
id: 'SPL_QUERY_CONVERSION_TITLE',
promptType: PromptTypeEnum.quick,
},
{
title: 'AUTOMATION_TITLE',
prompt: 'AUTOMATION_PROMPT',
name: 'AUTOMATION_TITLE',
content: 'AUTOMATION_PROMPT',
color: '#FFA500',
isDefault: true,
id: 'AUTOMATION_TITLE',
promptType: PromptTypeEnum.quick,
},
{
title: 'A_CUSTOM_OPTION',
prompt: 'quickly prompt please',
name: 'A_CUSTOM_OPTION',
content: 'quickly prompt please',
color: '#D36086',
categories: [],
id: 'A_CUSTOM_OPTION',
promptType: PromptTypeEnum.quick,
},
];

View file

@ -5,35 +5,47 @@
* 2.0.
*/
import { Prompt } from '../../assistant/types';
import { PromptResponse } from '@kbn/elastic-assistant-common';
export const mockSystemPrompt: Prompt = {
export const mockSystemPrompt: PromptResponse = {
id: 'mock-system-prompt-1',
content: 'You are a helpful, expert assistant who answers questions about Elastic Security.',
name: 'Mock system prompt',
consumer: 'securitySolutionUI',
promptType: 'system',
isFlyoutMode: false,
};
export const mockSuperheroSystemPrompt: Prompt = {
export const mockSuperheroSystemPrompt: PromptResponse = {
id: 'mock-superhero-system-prompt-1',
content: `You are a helpful, expert assistant who answers questions about Elastic Security.
You have the personality of a mutant superhero who says "bub" a lot.`,
name: 'Mock superhero system prompt',
consumer: 'securitySolutionUI',
promptType: 'system',
};
export const defaultSystemPrompt: Prompt = {
export const defaultSystemPrompt: PromptResponse = {
id: 'default-system-prompt',
content:
'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:',
name: 'Default system prompt',
promptType: 'system',
consumer: 'securitySolutionUI',
isDefault: true,
isNewConversationDefault: true,
};
export const mockSystemPrompts: Prompt[] = [
export const defaultQuickPrompt: PromptResponse = {
id: 'default-system-prompt',
content:
'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:',
name: 'Default system prompt',
promptType: 'quick',
consumer: 'securitySolutionUI',
color: 'red',
};
export const mockSystemPrompts: PromptResponse[] = [
mockSystemPrompt,
mockSuperheroSystemPrompt,
defaultSystemPrompt,

View file

@ -81,6 +81,7 @@ export const TestProvidersComponent: React.FC<Props> = ({
baseConversations={{}}
navigateToApp={mockNavigateToApp}
{...providerContext}
currentAppId={'test'}
>
{children}
</AssistantProvider>

View file

@ -5,12 +5,12 @@
* 2.0.
*/
import { Prompt } from '../../assistant/types';
import { PromptResponse } from '@kbn/elastic-assistant-common';
export const mockUserPrompt: Prompt = {
export const mockUserPrompt: PromptResponse = {
id: 'mock-user-prompt-1',
content: `Explain the meaning from the context above, then summarize a list of suggested Elasticsearch KQL and EQL queries.
Finally, suggest an investigation guide, and format it as markdown.`,
name: 'Mock user prompt',
promptType: 'user',
promptType: 'quick',
};

View file

@ -90,12 +90,6 @@ export {
WELCOME_CONVERSATION_TITLE,
} from './impl/assistant/use_conversation/translations';
/** i18n translations of system prompts */
export * as SYSTEM_PROMPTS from './impl/content/prompts/system/translations';
/** i18n translations of user prompts */
export * as USER_PROMPTS from './impl/content/prompts/user/translations';
export type {
/** for rendering results in a code block */
CodeBlockDetails,
@ -114,9 +108,6 @@ export type {
ClientMessage,
} from './impl/assistant_context/types';
/** Interface for defining system/user prompts */
export type { Prompt } from './impl/assistant/types';
/**
* This interface is used to pass context to the assistant,
* for the purpose of building prompts. Examples of context include:
@ -139,12 +130,6 @@ export type { PromptContext } from './impl/assistant/prompt_context/types';
*/
export type { PromptContextTemplate } from './impl/assistant/prompt_context/types';
/**
* This interface is used to pass a default or base set of Quick Prompts to the Elastic Assistant that
* can be displayed when corresponding PromptContext's are registered.
*/
export type { QuickPrompt } from './impl/assistant/quick_prompts/types';
export { useFetchCurrentUserConversations } from './impl/assistant/api/conversations/use_fetch_current_user_conversations';
export * from './impl/assistant/api/conversations/bulk_update_actions_conversations';
export { getConversationById } from './impl/assistant/api/conversations/conversations';
@ -152,4 +137,4 @@ export { getConversationById } from './impl/assistant/api/conversations/conversa
export { mergeBaseWithPersistedConversations } from './impl/assistant/helpers';
export { UpgradeButtons } from './impl/upgrade/upgrade_buttons';
export { getUserConversations } from './impl/assistant/api';
export { getUserConversations, getPrompts, bulkUpdatePrompts } from './impl/assistant/api';

View file

@ -73,6 +73,7 @@ export const TestProvidersComponent: React.FC<Props> = ({ children, isILMAvailab
http={mockHttp}
baseConversations={{}}
navigateToApp={mockNavigateToApp}
currentAppId={'securitySolutionUI'}
>
<DataQualityProvider
httpFetch={http.fetch}

View file

@ -42,8 +42,10 @@ export const getPromptsSearchEsMock = () => {
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
content: 'test content',
name: 'test',
prompt_type: 'quickPrompt',
is_shared: false,
prompt_type: 'quick',
consumer: 'securitySolutionUI',
categories: [],
color: 'red',
created_by: 'elastic',
users: [
{
@ -62,15 +64,19 @@ export const getCreatePromptSchemaMock = (): PromptCreateProps => ({
name: 'test',
content: 'test content',
isNewConversationDefault: false,
isShared: true,
consumer: 'securitySolutionUI',
categories: [],
color: 'red',
isDefault: false,
promptType: 'quickPrompt',
promptType: 'quick',
});
export const getUpdatePromptSchemaMock = (promptId = 'prompt-1'): PromptUpdateProps => ({
content: 'test content',
isNewConversationDefault: false,
isShared: true,
consumer: 'securitySolutionUI',
categories: [],
color: 'red',
isDefault: false,
id: promptId,
});
@ -79,7 +85,7 @@ export const getPromptMock = (params: PromptCreateProps | PromptUpdateProps): Pr
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
content: 'test content',
name: 'test',
promptType: 'quickPrompt',
promptType: 'quick',
isDefault: false,
...params,
createdAt: '2019-12-13T16:40:33.400Z',
@ -97,19 +103,23 @@ export const getQueryPromptParams = (isUpdate?: boolean): PromptCreateProps | Pr
? {
content: 'test 2',
name: 'test',
promptType: 'quickPrompt',
promptType: 'quick',
isDefault: false,
isNewConversationDefault: true,
isShared: true,
consumer: 'securitySolutionUI',
categories: [],
color: 'red',
id: '1',
}
: {
content: 'test 2',
name: 'test',
promptType: 'quickPrompt',
promptType: 'quick',
isDefault: false,
isNewConversationDefault: true,
isShared: true,
consumer: 'securitySolutionUI',
categories: [],
color: 'red',
};
};

View file

@ -23,11 +23,21 @@ export const assistantPromptsFieldMap: FieldMap = {
array: false,
required: false,
},
is_shared: {
type: 'boolean',
consumer: {
type: 'text',
array: false,
required: false,
},
color: {
type: 'keyword',
array: false,
required: false,
},
categories: {
type: 'keyword',
array: true,
required: false,
},
is_new_conversation_default: {
type: 'boolean',
array: false,

View file

@ -9,6 +9,7 @@ import { estypes } from '@elastic/elasticsearch';
import {
PromptCreateProps,
PromptResponse,
PromptType,
PromptUpdateProps,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { AuthenticatedUser } from '@kbn/core-security-common';
@ -31,8 +32,10 @@ export const transformESToPrompts = (response: EsPromptsSchema[]): PromptRespons
namespace: promptSchema.namespace,
id: promptSchema.id,
name: promptSchema.name,
promptType: promptSchema.prompt_type,
isShared: promptSchema.is_shared,
promptType: promptSchema.prompt_type as unknown as PromptType,
color: promptSchema.color,
categories: promptSchema.categories,
consumer: promptSchema.consumer,
createdBy: promptSchema.created_by,
updatedBy: promptSchema.updated_by,
};
@ -65,8 +68,10 @@ export const transformESSearchToPrompts = (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: hit._id!,
name: promptSchema.name,
promptType: promptSchema.prompt_type,
isShared: promptSchema.is_shared,
promptType: promptSchema.prompt_type as unknown as PromptType,
color: promptSchema.color,
categories: promptSchema.categories,
consumer: promptSchema.consumer,
createdBy: promptSchema.created_by,
updatedBy: promptSchema.updated_by,
};
@ -78,14 +83,15 @@ export const transformESSearchToPrompts = (
export const transformToUpdateScheme = (
user: AuthenticatedUser,
updatedAt: string,
{ content, isNewConversationDefault, isShared, id }: PromptUpdateProps
{ content, isNewConversationDefault, categories, color, id }: PromptUpdateProps
): UpdatePromptSchema => {
return {
id,
updated_at: updatedAt,
content: content ?? '',
is_new_conversation_default: isNewConversationDefault,
is_shared: isShared,
categories,
color,
users: [
{
id: user.profile_uid,
@ -98,13 +104,25 @@ export const transformToUpdateScheme = (
export const transformToCreateScheme = (
user: AuthenticatedUser,
updatedAt: string,
{ content, isDefault, isNewConversationDefault, isShared, name, promptType }: PromptCreateProps
{
content,
isDefault,
isNewConversationDefault,
categories,
color,
consumer,
name,
promptType,
}: PromptCreateProps
): CreatePromptSchema => {
return {
'@timestamp': updatedAt,
updated_at: updatedAt,
content: content ?? '',
is_new_conversation_default: isNewConversationDefault,
is_shared: isShared,
color,
consumer,
categories,
name,
is_default: isDefault,
prompt_type: promptType,
@ -132,8 +150,11 @@ export const getUpdateScript = ({
if (params.assignEmpty == true || params.containsKey('is_new_conversation_default')) {
ctx._source.is_new_conversation_default = params.is_new_conversation_default;
}
if (params.assignEmpty == true || params.containsKey('is_shared')) {
ctx._source.is_shared = params.is_shared;
if (params.assignEmpty == true || params.containsKey('color')) {
ctx._source.color = params.color;
}
if (params.assignEmpty == true || params.containsKey('categories')) {
ctx._source.categories = params.categories;
}
ctx._source.updated_at = params.updated_at;
`,

View file

@ -12,7 +12,9 @@ export interface EsPromptsSchema {
created_by: string;
content: string;
is_default?: boolean;
is_shared?: boolean;
consumer?: string;
color?: string;
categories?: string[];
is_new_conversation_default?: boolean;
name: string;
prompt_type: string;
@ -28,7 +30,8 @@ export interface EsPromptsSchema {
export interface UpdatePromptSchema {
id: string;
'@timestamp'?: string;
is_shared?: boolean;
color?: string;
categories?: string[];
is_new_conversation_default?: boolean;
content?: string;
updated_at?: string;
@ -42,7 +45,9 @@ export interface UpdatePromptSchema {
export interface CreatePromptSchema {
'@timestamp'?: string;
is_shared?: boolean;
consumer?: string;
color?: string;
categories?: string[];
is_new_conversation_default?: boolean;
is_default?: boolean;
name: string;

View file

@ -59,7 +59,7 @@ export const findPromptsRoute = (router: ElasticAssistantPluginRouter, logger: L
page: query.page,
sortField: query.sort_field,
sortOrder: query.sort_order,
filter: query.filter,
filter: query.filter ? decodeURIComponent(query.filter) : undefined,
fields: query.fields,
});

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import type { Prompt } from '@kbn/elastic-assistant';
import {
PromptTypeEnum,
type PromptResponse,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { APP_UI_ID } from '../../../../../common';
import {
DEFAULT_SYSTEM_PROMPT_NAME,
DEFAULT_SYSTEM_PROMPT_NON_I18N,
@ -16,20 +20,22 @@ import {
/**
* Base System Prompts for Security Solution.
*/
export const BASE_SECURITY_SYSTEM_PROMPTS: Prompt[] = [
export const BASE_SECURITY_SYSTEM_PROMPTS: PromptResponse[] = [
{
id: 'default-system-prompt',
content: DEFAULT_SYSTEM_PROMPT_NON_I18N,
name: DEFAULT_SYSTEM_PROMPT_NAME,
promptType: 'system',
promptType: PromptTypeEnum.system,
isDefault: true,
isNewConversationDefault: true,
consumer: APP_UI_ID,
},
{
id: 'CB9FA555-B59F-4F71-AFF9-8A891AC5BC28',
content: SUPERHERO_SYSTEM_PROMPT_NON_I18N,
name: SUPERHERO_SYSTEM_PROMPT_NAME,
promptType: 'system',
promptType: PromptTypeEnum.system,
consumer: APP_UI_ID,
isDefault: true,
},
];

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import type { QuickPrompt } from '@kbn/elastic-assistant';
import {
PromptTypeEnum,
type PromptResponse,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { APP_UI_ID } from '../../../../common';
import * as i18n from './translations';
import {
KNOWLEDGE_BASE_CATEGORY,
@ -19,51 +23,72 @@ import {
* Useful if wanting to see all available QuickPrompts in one place, or if needing
* to reference when constructing a new chat window to include a QuickPrompt.
*/
export const BASE_SECURITY_QUICK_PROMPTS: QuickPrompt[] = [
export const BASE_SECURITY_QUICK_PROMPTS: PromptResponse[] = [
{
title: i18n.ALERT_SUMMARIZATION_TITLE,
prompt: i18n.ALERT_SUMMARIZATION_PROMPT,
name: i18n.ALERT_SUMMARIZATION_TITLE,
content: i18n.ALERT_SUMMARIZATION_PROMPT,
color: '#F68FBE',
categories: [PROMPT_CONTEXT_ALERT_CATEGORY],
isDefault: true,
id: i18n.ALERT_SUMMARIZATION_TITLE,
promptType: PromptTypeEnum.quick,
consumer: APP_UI_ID,
},
{
title: i18n.ESQL_QUERY_GENERATION_TITLE,
prompt: i18n.ESQL_QUERY_GENERATION_PROMPT,
name: i18n.ESQL_QUERY_GENERATION_TITLE,
content: i18n.ESQL_QUERY_GENERATION_PROMPT,
color: '#9170B8',
categories: [KNOWLEDGE_BASE_CATEGORY],
isDefault: true,
id: i18n.ESQL_QUERY_GENERATION_TITLE,
promptType: PromptTypeEnum.quick,
consumer: APP_UI_ID,
},
{
title: i18n.RULE_CREATION_TITLE,
prompt: i18n.RULE_CREATION_PROMPT,
name: i18n.RULE_CREATION_TITLE,
content: i18n.RULE_CREATION_PROMPT,
categories: [PROMPT_CONTEXT_DETECTION_RULES_CATEGORY],
color: '#7DDED8',
isDefault: true,
id: i18n.RULE_CREATION_TITLE,
promptType: PromptTypeEnum.quick,
consumer: APP_UI_ID,
},
{
title: i18n.WORKFLOW_ANALYSIS_TITLE,
prompt: i18n.WORKFLOW_ANALYSIS_PROMPT,
name: i18n.WORKFLOW_ANALYSIS_TITLE,
content: i18n.WORKFLOW_ANALYSIS_PROMPT,
color: '#36A2EF',
isDefault: true,
id: i18n.WORKFLOW_ANALYSIS_TITLE,
promptType: PromptTypeEnum.quick,
consumer: APP_UI_ID,
},
{
title: i18n.THREAT_INVESTIGATION_GUIDES_TITLE,
prompt: i18n.THREAT_INVESTIGATION_GUIDES_PROMPT,
name: i18n.THREAT_INVESTIGATION_GUIDES_TITLE,
content: i18n.THREAT_INVESTIGATION_GUIDES_PROMPT,
categories: [PROMPT_CONTEXT_EVENT_CATEGORY],
color: '#F3D371',
isDefault: true,
id: i18n.THREAT_INVESTIGATION_GUIDES_TITLE,
promptType: PromptTypeEnum.quick,
consumer: APP_UI_ID,
},
{
title: i18n.SPL_QUERY_CONVERSION_TITLE,
prompt: i18n.SPL_QUERY_CONVERSION_PROMPT,
name: i18n.SPL_QUERY_CONVERSION_TITLE,
content: i18n.SPL_QUERY_CONVERSION_PROMPT,
color: '#BADA55',
isDefault: true,
id: i18n.SPL_QUERY_CONVERSION_TITLE,
promptType: PromptTypeEnum.quick,
consumer: APP_UI_ID,
},
{
title: i18n.AUTOMATION_TITLE,
prompt: i18n.AUTOMATION_PROMPT,
name: i18n.AUTOMATION_TITLE,
content: i18n.AUTOMATION_PROMPT,
color: '#FFA500',
isDefault: true,
id: i18n.AUTOMATION_TITLE,
promptType: PromptTypeEnum.quick,
consumer: APP_UI_ID,
},
];

View file

@ -15,24 +15,28 @@ import {
AssistantProvider as ElasticAssistantProvider,
bulkUpdateConversations,
getUserConversations,
getPrompts,
bulkUpdatePrompts,
} from '@kbn/elastic-assistant';
import { once } from 'lodash/fp';
import type { HttpSetup } from '@kbn/core-http-browser';
import type { Message } from '@kbn/elastic-assistant-common';
import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
import { useObservable } from 'react-use';
import { APP_ID } from '../../common';
import { useBasePath, useKibana } from '../common/lib/kibana';
import { useAssistantTelemetry } from './use_assistant_telemetry';
import { getComments } from './get_comments';
import { LOCAL_STORAGE_KEY, augmentMessageCodeBlocks } from './helpers';
import { useBaseConversations } from './use_conversation_store';
import { PROMPT_CONTEXTS } from './content/prompt_contexts';
import { BASE_SECURITY_QUICK_PROMPTS } from './content/quick_prompts';
import { BASE_SECURITY_SYSTEM_PROMPTS } from './content/prompts/system';
import { useBaseConversations } from './use_conversation_store';
import { PROMPT_CONTEXTS } from './content/prompt_contexts';
import { useAssistantAvailability } from './use_assistant_availability';
import { useAppToasts } from '../common/hooks/use_app_toasts';
import { useSignalIndex } from '../detections/containers/detection_engine/alerts/use_signal_index';
import { licenseService } from '../common/hooks/use_license';
const ASSISTANT_TITLE = i18n.translate('xpack.securitySolution.assistant.title', {
defaultMessage: 'Elastic AI Assistant',
@ -112,12 +116,28 @@ export const createConversations = async (
}
};
export const createBasePrompts = async (notifications: NotificationsStart, http: HttpSetup) => {
const promptsToCreate = [...BASE_SECURITY_QUICK_PROMPTS, ...BASE_SECURITY_SYSTEM_PROMPTS];
// post bulk create
const bulkResult = await bulkUpdatePrompts(
http,
{
create: promptsToCreate,
},
notifications.toasts
);
if (bulkResult && bulkResult.success) {
return true;
}
};
/**
* This component configures the Elastic AI Assistant context provider for the Security Solution app.
*/
export const AssistantProvider: FC<PropsWithChildren<unknown>> = ({ children }) => {
const {
application: { navigateToApp },
application: { navigateToApp, currentAppId$ },
http,
notifications,
storage,
@ -129,29 +149,59 @@ export const AssistantProvider: FC<PropsWithChildren<unknown>> = ({ children })
const baseConversations = useBaseConversations();
const assistantAvailability = useAssistantAvailability();
const assistantTelemetry = useAssistantTelemetry();
const currentAppId = useObservable(currentAppId$, '');
const hasEnterpriseLicence = licenseService.isEnterprise();
useEffect(() => {
const migrateConversationsFromLocalStorage = once(async () => {
const res = await getUserConversations({
http,
});
if (
hasEnterpriseLicence &&
assistantAvailability.isAssistantEnabled &&
assistantAvailability.hasAssistantPrivilege &&
res.total === 0
assistantAvailability.hasAssistantPrivilege
) {
await createConversations(notifications, http, storage);
const res = await getUserConversations({
http,
});
if (res.total === 0) {
await createConversations(notifications, http, storage);
}
}
});
migrateConversationsFromLocalStorage();
}, [
assistantAvailability.hasAssistantPrivilege,
assistantAvailability.isAssistantEnabled,
hasEnterpriseLicence,
http,
notifications,
storage,
]);
useEffect(() => {
const createSecurityPrompts = once(async () => {
if (
hasEnterpriseLicence &&
assistantAvailability.isAssistantEnabled &&
assistantAvailability.hasAssistantPrivilege
) {
const res = await getPrompts({
http,
toasts: notifications.toasts,
});
if (res.total === 0) {
await createBasePrompts(notifications, http);
}
}
});
createSecurityPrompts();
}, [
assistantAvailability.hasAssistantPrivilege,
assistantAvailability.isAssistantEnabled,
hasEnterpriseLicence,
http,
notifications,
]);
const { signalIndexName } = useSignalIndex();
const alertsIndexPattern = signalIndexName ?? undefined;
const toasts = useAppToasts() as unknown as IToasts; // useAppToasts is the current, non-deprecated method of getting the toasts service in the Security Solution, but it doesn't return the IToasts interface (defined by core)
@ -166,14 +216,13 @@ export const AssistantProvider: FC<PropsWithChildren<unknown>> = ({ children })
docLinks={{ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }}
basePath={basePath}
basePromptContexts={Object.values(PROMPT_CONTEXTS)}
baseQuickPrompts={BASE_SECURITY_QUICK_PROMPTS} // to server and plugin start
baseSystemPrompts={BASE_SECURITY_SYSTEM_PROMPTS} // to server and plugin start
baseConversations={baseConversations}
getComments={getComments}
http={http}
navigateToApp={navigateToApp}
title={ASSISTANT_TITLE}
toasts={toasts}
currentAppId={currentAppId ?? 'securitySolutionUI'}
>
{children}
</ElasticAssistantProvider>

View file

@ -50,6 +50,7 @@ export const MockAssistantProviderComponent: React.FC<Props> = ({
http={mockHttp}
navigateToApp={mockNavigateToApp}
baseConversations={BASE_SECURITY_CONVERSATIONS}
currentAppId={'test'}
>
{children}
</AssistantProvider>

View file

@ -64,6 +64,7 @@ const ContextWrapper: FC<PropsWithChildren<unknown>> = ({ children }) => (
http={mockHttp}
navigateToApp={mockNavigationToApp}
baseConversations={BASE_SECURITY_CONVERSATIONS}
currentAppId={'security'}
>
{children}
</AssistantProvider>

View file

@ -13341,8 +13341,6 @@
"xpack.elasticAssistant.assistant.content.prompts.system.superheroPersonality": "Donnez la réponse la plus pertinente et détaillée possible, comme si vous deviez communiquer ces informations à un expert en cybersécurité.",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptName": "Invite système améliorée",
"xpack.elasticAssistant.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "Vous êtes un assistant expert et serviable qui répond à des questions au sujet dElastic Security.",
"xpack.elasticAssistant.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "Ajoutez votre description, les actions que vous recommandez ainsi que les étapes de triage à puces. Utilisez les données \"MITRE ATT&CK\" fournies pour ajouter du contexte et des recommandations de MITRE ainsi que des liens hypertexte vers les pages pertinentes sur le site web de MITRE. Assurez-vous dinclure les scores de risque de lutilisateur et de lhôte du contexte. Votre réponse doit inclure des étapes qui pointent vers les fonctionnalités spécifiques dElastic Security, y compris les actions de réponse du terminal, lintégration OSQuery Manager dElastic Agent (avec des exemples de requêtes OSQuery), des analyses de timeline et dentités, ainsi quun lien pour toute la documentation Elastic Security pertinente.",
"xpack.elasticAssistant.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "Évaluer lévénement depuis le contexte ci-dessus et formater soigneusement la sortie en syntaxe Markdown pour mon cas Elastic Security.",
"xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "Connecteur",
"xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "Contexte fournit dans le cadre de chaque conversation",
"xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "Invite système",

View file

@ -13320,8 +13320,6 @@
"xpack.elasticAssistant.assistant.content.prompts.system.superheroPersonality": "サイバーセキュリティの専門家に情報を伝えるつもりで、できるだけ詳細で関連性のある回答を入力してください。",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptName": "拡張システムプロンプト",
"xpack.elasticAssistant.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "あなたはElasticセキュリティに関する質問に答える、親切で専門的なアシスタントです。",
"xpack.elasticAssistant.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "説明、推奨されるアクション、箇条書きのトリアージステップを追加します。提供された MITRE ATT&CKデータを使用して、MITREからのコンテキストや推奨事項を追加し、MITREのWebサイトの関連ページにハイパーリンクを貼ります。コンテキストのユーザーとホストのリスクスコアデータを必ず含めてください。回答には、エンドポイント対応アクション、ElasticエージェントOSQueryマネージャー統合osqueryクエリの例を付けて、タイムライン、エンティティ分析など、Elasticセキュリティ固有の機能を指す手順を含め、関連するElasticセキュリティのドキュメントすべてにリンクしてください。",
"xpack.elasticAssistant.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "上記のコンテキストからイベントを評価し、Elasticセキュリティのケース用に、出力をマークダウン構文で正しく書式設定してください。",
"xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "コネクター",
"xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "すべての会話の一部として提供されたコンテキスト",
"xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "システムプロンプト",

View file

@ -13346,8 +13346,6 @@
"xpack.elasticAssistant.assistant.content.prompts.system.superheroPersonality": "提供可能的最详细、最相关的答案,就好像您正将此信息转发给网络安全专家一样。",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptName": "已增强系统提示",
"xpack.elasticAssistant.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "您是一位可帮助回答 Elastic Security 相关问题的专家助手。",
"xpack.elasticAssistant.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "添加描述、建议操作和带项目符号的分类步骤。使用提供的 MITRE ATT&CK 数据以从 MITRE 添加更多上下文和建议,以及指向 MITRE 网站上的相关页面的超链接。确保包括上下文中的用户和主机风险分数数据。您的响应应包含指向 Elastic Security 特定功能的步骤包括终端响应操作、Elastic 代理 OSQuery 管理器集成(带示例 osquery 查询)、时间线和实体分析,以及所有相关 Elastic Security 文档的链接。",
"xpack.elasticAssistant.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "评估来自上述上下文的事件,并以用于我的 Elastic Security 案例的 Markdown 语法对您的输出进行全面格式化。",
"xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "连接器",
"xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "已作为每个对话的一部分提供上下文",
"xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "系统提示",