mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] AI Assistant - System prompt move (#191847)
This commit is contained in:
parent
cb1286c736
commit
2dc1aca175
57 changed files with 690 additions and 1199 deletions
|
@ -9,31 +9,23 @@ import React, { Dispatch, SetStateAction } from 'react';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { PromptResponse } from '@kbn/elastic-assistant-common';
|
||||
import { QueryObserverResult } from '@tanstack/react-query';
|
||||
import { Conversation } from '../../..';
|
||||
import { AssistantAnimatedIcon } from '../assistant_animated_icon';
|
||||
import { SystemPrompt } from '../prompt_editor/system_prompt';
|
||||
import { SetupKnowledgeBaseButton } from '../../knowledge_base/setup_knowledge_base_button';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface Props {
|
||||
currentConversation: Conversation | undefined;
|
||||
currentSystemPromptId: string | undefined;
|
||||
isSettingsModalVisible: boolean;
|
||||
refetchCurrentUserConversations: () => Promise<
|
||||
QueryObserverResult<Record<string, Conversation>, unknown>
|
||||
>;
|
||||
setIsSettingsModalVisible: Dispatch<SetStateAction<boolean>>;
|
||||
setCurrentSystemPromptId: Dispatch<SetStateAction<string | undefined>>;
|
||||
setCurrentSystemPromptId: (promptId: string | undefined) => void;
|
||||
allSystemPrompts: PromptResponse[];
|
||||
}
|
||||
|
||||
export const EmptyConvo: React.FC<Props> = ({
|
||||
allSystemPrompts,
|
||||
currentConversation,
|
||||
currentSystemPromptId,
|
||||
isSettingsModalVisible,
|
||||
refetchCurrentUserConversations,
|
||||
setCurrentSystemPromptId,
|
||||
setIsSettingsModalVisible,
|
||||
}) => {
|
||||
|
@ -59,13 +51,11 @@ export const EmptyConvo: React.FC<Props> = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SystemPrompt
|
||||
conversation={currentConversation}
|
||||
currentSystemPromptId={currentSystemPromptId}
|
||||
onSystemPromptSelectionChange={setCurrentSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
allSystemPrompts={allSystemPrompts}
|
||||
refetchConversations={refetchCurrentUserConversations}
|
||||
currentSystemPromptId={currentSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={setCurrentSystemPromptId}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -18,7 +18,6 @@ import { HttpSetup } from '@kbn/core-http-browser';
|
|||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { css } from '@emotion/react';
|
||||
import { PromptResponse } from '@kbn/elastic-assistant-common';
|
||||
import { QueryObserverResult } from '@tanstack/react-query';
|
||||
import { AssistantAnimatedIcon } from '../assistant_animated_icon';
|
||||
import { EmptyConvo } from './empty_convo';
|
||||
import { WelcomeSetup } from './welcome_setup';
|
||||
|
@ -35,11 +34,8 @@ interface Props {
|
|||
isSettingsModalVisible: boolean;
|
||||
isWelcomeSetup: boolean;
|
||||
isLoading: boolean;
|
||||
refetchCurrentUserConversations: () => Promise<
|
||||
QueryObserverResult<Record<string, Conversation>, unknown>
|
||||
>;
|
||||
http: HttpSetup;
|
||||
setCurrentSystemPromptId: Dispatch<SetStateAction<string | undefined>>;
|
||||
setCurrentSystemPromptId: (promptId: string | undefined) => void;
|
||||
setIsSettingsModalVisible: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
|
@ -55,17 +51,16 @@ export const AssistantBody: FunctionComponent<Props> = ({
|
|||
isLoading,
|
||||
isSettingsModalVisible,
|
||||
isWelcomeSetup,
|
||||
refetchCurrentUserConversations,
|
||||
setIsSettingsModalVisible,
|
||||
}) => {
|
||||
const isNewConversation = useMemo(
|
||||
const isEmptyConversation = useMemo(
|
||||
() => currentConversation?.messages.length === 0,
|
||||
[currentConversation?.messages.length]
|
||||
);
|
||||
|
||||
const disclaimer = useMemo(
|
||||
() =>
|
||||
isNewConversation && (
|
||||
isEmptyConversation && (
|
||||
<EuiText
|
||||
data-test-subj="assistant-disclaimer"
|
||||
textAlign="center"
|
||||
|
@ -78,7 +73,7 @@ export const AssistantBody: FunctionComponent<Props> = ({
|
|||
{i18n.DISCLAIMER}
|
||||
</EuiText>
|
||||
),
|
||||
[isNewConversation]
|
||||
[isEmptyConversation]
|
||||
);
|
||||
|
||||
// Start Scrolling
|
||||
|
@ -113,13 +108,11 @@ export const AssistantBody: FunctionComponent<Props> = ({
|
|||
currentConversation={currentConversation}
|
||||
handleOnConversationSelected={handleOnConversationSelected}
|
||||
/>
|
||||
) : currentConversation?.messages.length === 0 ? (
|
||||
) : isEmptyConversation ? (
|
||||
<EmptyConvo
|
||||
allSystemPrompts={allSystemPrompts}
|
||||
currentConversation={currentConversation}
|
||||
currentSystemPromptId={currentSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
refetchCurrentUserConversations={refetchCurrentUserConversations}
|
||||
setCurrentSystemPromptId={setCurrentSystemPromptId}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
|
|
|
@ -9,7 +9,6 @@ import { HttpSetup } from '@kbn/core-http-browser';
|
|||
import { useSendMessage } from '../use_send_message';
|
||||
import { useConversation } from '../use_conversation';
|
||||
import { emptyWelcomeConvo, welcomeConvo } from '../../mock/conversation';
|
||||
import { defaultSystemPrompt, mockSystemPrompt } from '../../mock/system_prompt';
|
||||
import { useChatSend, UseChatSendProps } from './use_chat_send';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
@ -28,7 +27,6 @@ const setCurrentConversation = jest.fn();
|
|||
|
||||
export const testProps: UseChatSendProps = {
|
||||
selectedPromptContexts: {},
|
||||
allSystemPrompts: [defaultSystemPrompt, mockSystemPrompt],
|
||||
currentConversation: { ...emptyWelcomeConvo, id: 'an-id' },
|
||||
http: {
|
||||
basePath: {
|
||||
|
@ -38,7 +36,6 @@ export const testProps: UseChatSendProps = {
|
|||
anonymousPaths: {},
|
||||
externalUrl: {},
|
||||
} as unknown as HttpSetup,
|
||||
currentSystemPromptId: defaultSystemPrompt.id,
|
||||
setSelectedPromptContexts,
|
||||
setCurrentConversation,
|
||||
refetchCurrentUserConversations: jest.fn(),
|
||||
|
@ -78,21 +75,7 @@ describe('use chat send', () => {
|
|||
expect(setCurrentConversation).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('handleChatSend sends message with context prompt when a valid prompt text is provided', async () => {
|
||||
const promptText = 'prompt text';
|
||||
const { result } = renderHook(() => useChatSend(testProps), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
result.current.handleChatSend(promptText);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendMessage).toHaveBeenCalled();
|
||||
const appendMessageSend = sendMessage.mock.calls[0][0].message;
|
||||
expect(appendMessageSend).toEqual(
|
||||
`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:\n\n${promptText}`
|
||||
);
|
||||
});
|
||||
});
|
||||
it('handleChatSend sends message with only provided prompt text and context already exists in convo history', async () => {
|
||||
const promptText = 'prompt text';
|
||||
const { result } = renderHook(
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PromptResponse, Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { DataStreamApis } from '../use_data_stream_apis';
|
||||
import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations';
|
||||
import type { ClientMessage } from '../../assistant_context/types';
|
||||
|
@ -20,9 +20,7 @@ import { Conversation, useAssistantContext } from '../../..';
|
|||
import { getMessageFromRawResponse } from '../helpers';
|
||||
|
||||
export interface UseChatSendProps {
|
||||
allSystemPrompts: PromptResponse[];
|
||||
currentConversation?: Conversation;
|
||||
currentSystemPromptId: string | undefined;
|
||||
http: HttpSetup;
|
||||
refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations'];
|
||||
selectedPromptContexts: Record<string, SelectedPromptContext>;
|
||||
|
@ -46,9 +44,7 @@ export interface UseChatSend {
|
|||
* Handles sending user messages to the API and updating the conversation state.
|
||||
*/
|
||||
export const useChatSend = ({
|
||||
allSystemPrompts,
|
||||
currentConversation,
|
||||
currentSystemPromptId,
|
||||
http,
|
||||
refetchCurrentUserConversations,
|
||||
selectedPromptContexts,
|
||||
|
@ -75,14 +71,11 @@ export const useChatSend = ({
|
|||
);
|
||||
return;
|
||||
}
|
||||
const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === currentSystemPromptId);
|
||||
|
||||
const userMessage = getCombinedMessage({
|
||||
isNewChat: currentConversation.messages.length === 0,
|
||||
currentReplacements: currentConversation.replacements,
|
||||
promptText,
|
||||
selectedPromptContexts,
|
||||
selectedSystemPrompt: systemPrompt,
|
||||
});
|
||||
|
||||
const baseReplacements: Replacements =
|
||||
|
@ -141,10 +134,8 @@ export const useChatSend = ({
|
|||
});
|
||||
},
|
||||
[
|
||||
allSystemPrompts,
|
||||
assistantTelemetry,
|
||||
currentConversation,
|
||||
currentSystemPromptId,
|
||||
http,
|
||||
selectedPromptContexts,
|
||||
sendMessage,
|
||||
|
|
|
@ -126,7 +126,6 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
selectedConversation={selectedConversationWithApiConfig}
|
||||
setConversationSettings={setConversationSettings}
|
||||
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
|
||||
onSelectedConversationChange={onSelectedConversationChange}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
|
|
@ -13,7 +13,6 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { PromptResponse } from '@kbn/elastic-assistant-common';
|
||||
import { QueryObserverResult } from '@tanstack/react-query';
|
||||
import { Conversation } from '../../../..';
|
||||
import * as i18n from './translations';
|
||||
import * as i18nModel from '../../../connectorland/models/model_selector/translations';
|
||||
|
@ -37,8 +36,6 @@ export interface ConversationSettingsEditorProps {
|
|||
setConversationsSettingsBulkActions: React.Dispatch<
|
||||
React.SetStateAction<ConversationsBulkActions>
|
||||
>;
|
||||
onSelectedConversationChange: (conversation?: Conversation) => void;
|
||||
refetchConversations?: () => Promise<QueryObserverResult<Record<string, Conversation>, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,15 +44,13 @@ export interface ConversationSettingsEditorProps {
|
|||
export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProps> = React.memo(
|
||||
({
|
||||
allSystemPrompts,
|
||||
selectedConversation,
|
||||
conversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
http,
|
||||
isDisabled = false,
|
||||
selectedConversation,
|
||||
setConversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
setConversationsSettingsBulkActions,
|
||||
onSelectedConversationChange,
|
||||
refetchConversations,
|
||||
}) => {
|
||||
const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({
|
||||
http,
|
||||
|
@ -276,16 +271,11 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
|
|||
<SelectSystemPrompt
|
||||
allPrompts={allSystemPrompts}
|
||||
compressed
|
||||
conversation={selectedConversation}
|
||||
isDisabled={isDisabled}
|
||||
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
|
||||
refetchConversations={refetchConversations}
|
||||
selectedPrompt={selectedSystemPrompt}
|
||||
isSettingsModalVisible={true}
|
||||
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
|
||||
selectedPrompt={selectedSystemPrompt}
|
||||
setIsSettingsModalVisible={noop} // noop, already in settings
|
||||
onSelectedConversationChange={onSelectedConversationChange}
|
||||
setConversationSettings={setConversationSettings}
|
||||
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
|
|
|
@ -321,11 +321,9 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
|
|||
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
|
||||
http={http}
|
||||
isDisabled={isDisabled}
|
||||
refetchConversations={refetchConversations}
|
||||
selectedConversation={selectedConversation}
|
||||
setConversationSettings={setConversationSettings}
|
||||
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
|
||||
onSelectedConversationChange={onSelectedConversationChange}
|
||||
/>
|
||||
</Flyout>
|
||||
)}
|
||||
|
|
|
@ -125,7 +125,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
|
||||
const {
|
||||
currentConversation,
|
||||
currentSystemPromptId,
|
||||
currentSystemPrompt,
|
||||
handleCreateConversation,
|
||||
handleOnConversationDeleted,
|
||||
handleOnConversationSelected,
|
||||
|
@ -272,16 +272,14 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
|
||||
const {
|
||||
abortStream,
|
||||
handleOnChatCleared: onChatCleared,
|
||||
handleOnChatCleared,
|
||||
handleChatSend,
|
||||
handleRegenerateResponse,
|
||||
isLoading: isLoadingChatSend,
|
||||
setUserPrompt,
|
||||
userPrompt,
|
||||
} = useChatSend({
|
||||
allSystemPrompts,
|
||||
currentConversation,
|
||||
currentSystemPromptId,
|
||||
http,
|
||||
refetchCurrentUserConversations,
|
||||
selectedPromptContexts,
|
||||
|
@ -289,18 +287,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
setCurrentConversation,
|
||||
});
|
||||
|
||||
const handleOnChatCleared = useCallback(() => {
|
||||
onChatCleared();
|
||||
if (!currentSystemPromptId) {
|
||||
setCurrentSystemPromptId(currentConversation?.apiConfig?.defaultSystemPromptId);
|
||||
}
|
||||
}, [
|
||||
currentConversation?.apiConfig?.defaultSystemPromptId,
|
||||
currentSystemPromptId,
|
||||
onChatCleared,
|
||||
setCurrentSystemPromptId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// Adding `conversationTitle !== selectedConversationTitle` to prevent auto-run still executing after changing selected conversation
|
||||
if (currentConversation?.messages.length || conversationTitle !== currentConversation?.title) {
|
||||
|
@ -389,6 +375,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
isFetchingResponse: isLoadingChatSend,
|
||||
setIsStreaming,
|
||||
currentUserAvatar,
|
||||
systemPromptContent: currentSystemPrompt?.content,
|
||||
})}
|
||||
// Avoid comments going off the flyout
|
||||
css={css`
|
||||
|
@ -415,6 +402,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
isLoadingChatSend,
|
||||
setIsStreaming,
|
||||
currentUserAvatar,
|
||||
currentSystemPrompt?.content,
|
||||
selectedPromptContextsCount,
|
||||
]
|
||||
);
|
||||
|
@ -530,14 +518,13 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
allSystemPrompts={allSystemPrompts}
|
||||
comments={comments}
|
||||
currentConversation={currentConversation}
|
||||
currentSystemPromptId={currentSystemPromptId}
|
||||
currentSystemPromptId={currentSystemPrompt?.id}
|
||||
handleOnConversationSelected={handleOnConversationSelected}
|
||||
http={http}
|
||||
isAssistantEnabled={isAssistantEnabled}
|
||||
isLoading={isInitialLoad}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
isWelcomeSetup={isWelcomeSetup}
|
||||
refetchCurrentUserConversations={refetchCurrentUserConversations}
|
||||
setCurrentSystemPromptId={setCurrentSystemPromptId}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
*/
|
||||
|
||||
import type { ClientMessage } from '../../assistant_context/types';
|
||||
import { getCombinedMessage, getSystemMessages } from './helpers';
|
||||
import { getCombinedMessage } from './helpers';
|
||||
import { mockGetAnonymizedValue } from '../../mock/get_anonymized_value';
|
||||
import { mockSystemPrompt } from '../../mock/system_prompt';
|
||||
import { mockAlertPromptContext } from '../../mock/prompt_context';
|
||||
import type { SelectedPromptContext } from '../prompt_context/types';
|
||||
|
||||
|
@ -21,77 +20,14 @@ const mockSelectedAlertPromptContext: SelectedPromptContext = {
|
|||
describe('helpers', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('getSystemMessages', () => {
|
||||
it('should return an empty array if isNewChat is false', () => {
|
||||
const result = getSystemMessages({
|
||||
isNewChat: false,
|
||||
selectedSystemPrompt: mockSystemPrompt,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if selectedSystemPrompt is undefined', () => {
|
||||
const result = getSystemMessages({ isNewChat: true, selectedSystemPrompt: undefined });
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
describe('when isNewChat is true and selectedSystemPrompt is defined', () => {
|
||||
let result: ClientMessage[];
|
||||
|
||||
beforeEach(() => {
|
||||
result = getSystemMessages({ isNewChat: true, selectedSystemPrompt: mockSystemPrompt });
|
||||
});
|
||||
|
||||
it('should return a message with the content of the selectedSystemPrompt', () => {
|
||||
expect(result[0].content).toBe(mockSystemPrompt.content);
|
||||
});
|
||||
|
||||
it('should return a message with the role "system"', () => {
|
||||
expect(result[0].role).toBe('system');
|
||||
});
|
||||
|
||||
it('should return a message with a valid timestamp', () => {
|
||||
const timestamp = new Date(result[0].timestamp);
|
||||
|
||||
expect(timestamp instanceof Date && !isNaN(timestamp.valueOf())).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCombinedMessage', () => {
|
||||
it('returns correct content for a new chat with a system prompt', async () => {
|
||||
it('returns correct content for a chat', async () => {
|
||||
const message: ClientMessage = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
isNewChat: true,
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
[mockSelectedAlertPromptContext.promptContextId]: mockSelectedAlertPromptContext,
|
||||
},
|
||||
selectedSystemPrompt: mockSystemPrompt,
|
||||
});
|
||||
|
||||
expect(message.content)
|
||||
.toEqual(`You are a helpful, expert assistant who answers questions about Elastic Security.
|
||||
|
||||
CONTEXT:
|
||||
"""
|
||||
alert data
|
||||
"""
|
||||
|
||||
User prompt text`);
|
||||
});
|
||||
|
||||
it('returns correct content for a new chat WITHOUT a system prompt', async () => {
|
||||
const message: ClientMessage = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
isNewChat: true,
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
[mockSelectedAlertPromptContext.promptContextId]: mockSelectedAlertPromptContext,
|
||||
},
|
||||
selectedSystemPrompt: undefined, // <-- no system prompt
|
||||
});
|
||||
|
||||
expect(message.content).toEqual(`CONTEXT:
|
||||
|
@ -105,12 +41,10 @@ User prompt text`);
|
|||
it('returns the correct content for an existing chat', async () => {
|
||||
const message: ClientMessage = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
isNewChat: false,
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
[mockSelectedAlertPromptContext.promptContextId]: mockSelectedAlertPromptContext,
|
||||
},
|
||||
selectedSystemPrompt: mockSystemPrompt,
|
||||
});
|
||||
|
||||
expect(message.content).toEqual(`CONTEXT:
|
||||
|
@ -124,12 +58,10 @@ User prompt text`);
|
|||
it('returns the expected role', async () => {
|
||||
const message: ClientMessage = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
isNewChat: true,
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
[mockSelectedAlertPromptContext.promptContextId]: mockSelectedAlertPromptContext,
|
||||
},
|
||||
selectedSystemPrompt: mockSystemPrompt,
|
||||
});
|
||||
|
||||
expect(message.role).toBe('user');
|
||||
|
@ -138,32 +70,25 @@ User prompt text`);
|
|||
it('returns a valid timestamp', async () => {
|
||||
const message: ClientMessage = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
isNewChat: true,
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {},
|
||||
selectedSystemPrompt: mockSystemPrompt,
|
||||
});
|
||||
|
||||
expect(Date.parse(message.timestamp)).not.toBeNaN();
|
||||
});
|
||||
it('should return the correct combined message for a new chat without prompt context', () => {
|
||||
it('should return the correct combined message for a chat without prompt context', () => {
|
||||
const result = getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
isNewChat: true,
|
||||
promptText: 'User prompt text',
|
||||
selectedSystemPrompt: mockSystemPrompt,
|
||||
selectedPromptContexts: {},
|
||||
});
|
||||
|
||||
expect(result.content).toEqual(
|
||||
`You are a helpful, expert assistant who answers questions about Elastic Security.\n\nUser prompt text`
|
||||
);
|
||||
expect(result.content).toEqual(`User prompt text`);
|
||||
});
|
||||
|
||||
it('should return the correct combined message for a new chat without system context and multiple selectedPromptContext', () => {
|
||||
it('should return the correct combined message for a chat with multiple selectedPromptContext', () => {
|
||||
const result = getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
isNewChat: true,
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
context1: {
|
||||
|
@ -177,7 +102,6 @@ User prompt text`);
|
|||
replacements: {},
|
||||
},
|
||||
},
|
||||
selectedSystemPrompt: { ...mockSystemPrompt, content: '' },
|
||||
});
|
||||
|
||||
expect(result.content).toEqual(
|
||||
|
@ -188,10 +112,8 @@ User prompt text`);
|
|||
it('should remove extra spaces when there is no prompt content or system prompt', () => {
|
||||
const result = getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
isNewChat: true,
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {},
|
||||
selectedSystemPrompt: { ...mockSystemPrompt, content: '' },
|
||||
});
|
||||
|
||||
expect(result.content).toEqual(`User prompt text`);
|
||||
|
@ -229,13 +151,11 @@ User prompt text`);
|
|||
const message = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
isNewChat: true,
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
[mockPromptContextWithDataToAnonymize.promptContextId]:
|
||||
mockPromptContextWithDataToAnonymize,
|
||||
},
|
||||
selectedSystemPrompt: mockSystemPrompt,
|
||||
});
|
||||
|
||||
expect(message.replacements).toEqual({
|
||||
|
@ -247,15 +167,11 @@ User prompt text`);
|
|||
});
|
||||
|
||||
it('returns the expected content when `isNewChat` is false', async () => {
|
||||
const isNewChat = false; // <-- not a new chat
|
||||
|
||||
const message: ClientMessage = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
isNewChat,
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {},
|
||||
selectedSystemPrompt: mockSystemPrompt,
|
||||
});
|
||||
|
||||
expect(message.content).toEqual(`User prompt text`);
|
||||
|
|
|
@ -5,41 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Replacements, transformRawData, PromptResponse } from '@kbn/elastic-assistant-common';
|
||||
import { Replacements, transformRawData } 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 { SYSTEM_PROMPT_CONTEXT_NON_I18N } from '../../content/prompts/system/translations';
|
||||
import { SYSTEM_PROMPT_CONTEXT_NON_I18N } from './translations';
|
||||
|
||||
export const getSystemMessages = ({
|
||||
isNewChat,
|
||||
selectedSystemPrompt,
|
||||
}: {
|
||||
isNewChat: boolean;
|
||||
selectedSystemPrompt: PromptResponse | undefined;
|
||||
}): ClientMessage[] => {
|
||||
if (!isNewChat || selectedSystemPrompt == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const message: ClientMessage = {
|
||||
content: selectedSystemPrompt.content,
|
||||
role: 'system',
|
||||
timestamp: new Date().toLocaleString(),
|
||||
};
|
||||
|
||||
return [message];
|
||||
};
|
||||
interface ClientMessageWithReplacements extends ClientMessage {
|
||||
replacements: Replacements;
|
||||
}
|
||||
export function getCombinedMessage({
|
||||
currentReplacements,
|
||||
getAnonymizedValue = defaultGetAnonymizedValue,
|
||||
isNewChat,
|
||||
promptText,
|
||||
selectedPromptContexts,
|
||||
selectedSystemPrompt,
|
||||
}: {
|
||||
currentReplacements: Replacements | undefined;
|
||||
getAnonymizedValue?: ({
|
||||
|
@ -49,10 +28,8 @@ export function getCombinedMessage({
|
|||
currentReplacements: Replacements | undefined;
|
||||
rawValue: string;
|
||||
}) => string;
|
||||
isNewChat: boolean;
|
||||
promptText: string;
|
||||
selectedPromptContexts: Record<string, SelectedPromptContext>;
|
||||
selectedSystemPrompt: PromptResponse | undefined;
|
||||
}): ClientMessageWithReplacements {
|
||||
let replacements: Replacements = currentReplacements ?? {};
|
||||
const onNewReplacements = (newReplacements: Replacements) => {
|
||||
|
@ -74,10 +51,8 @@ export function getCombinedMessage({
|
|||
});
|
||||
|
||||
const content = `${
|
||||
isNewChat && selectedSystemPrompt && selectedSystemPrompt.content.length > 0
|
||||
? `${selectedSystemPrompt?.content ?? ''}\n\n`
|
||||
: ''
|
||||
}${promptContextsContent.length > 0 ? `${promptContextsContent}\n` : ''}${promptText}`;
|
||||
promptContextsContent.length > 0 ? `${promptContextsContent}\n` : ''
|
||||
}${promptText}`;
|
||||
|
||||
return {
|
||||
// trim ensures any extra \n and other whitespace is removed
|
||||
|
|
|
@ -5,15 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const DEFAULT_SYSTEM_PROMPT_NAME = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptName',
|
||||
{
|
||||
defaultMessage: 'Default system prompt',
|
||||
}
|
||||
);
|
||||
|
||||
export const SYSTEM_PROMPT_CONTEXT_NON_I18N = (context: string) => {
|
||||
return `CONTEXT:\n"""\n${context}\n"""`;
|
||||
};
|
|
@ -15,23 +15,18 @@ import { getOptions, getOptionFromPrompt } from './helpers';
|
|||
|
||||
describe('helpers', () => {
|
||||
describe('getOptionFromPrompt', () => {
|
||||
const option = getOptionFromPrompt(mockSystemPrompt);
|
||||
it('returns an EuiSuperSelectOption with the correct value', () => {
|
||||
const option = getOptionFromPrompt({ ...mockSystemPrompt, isCleared: false });
|
||||
|
||||
expect(option.value).toBe(mockSystemPrompt.id);
|
||||
});
|
||||
|
||||
it('returns an EuiSuperSelectOption with the correct inputDisplay', () => {
|
||||
const option = getOptionFromPrompt({ ...mockSystemPrompt, isCleared: false });
|
||||
|
||||
render(<>{option.inputDisplay}</>);
|
||||
|
||||
expect(screen.getByTestId('systemPromptText')).toHaveTextContent(mockSystemPrompt.name);
|
||||
});
|
||||
|
||||
it('shows the expected name in the dropdownDisplay', () => {
|
||||
const option = getOptionFromPrompt({ ...mockSystemPrompt, isCleared: false });
|
||||
|
||||
render(<TestProviders>{option.dropdownDisplay}</TestProviders>);
|
||||
|
||||
expect(screen.getByTestId(`systemPrompt-${mockSystemPrompt.name}`)).toHaveTextContent(
|
||||
|
@ -40,8 +35,6 @@ describe('helpers', () => {
|
|||
});
|
||||
|
||||
it('shows the expected prompt content in the dropdownDisplay', () => {
|
||||
const option = getOptionFromPrompt({ ...mockSystemPrompt, isCleared: false });
|
||||
|
||||
render(<TestProviders>{option.dropdownDisplay}</TestProviders>);
|
||||
|
||||
expect(screen.getByTestId('content')).toHaveTextContent(mockSystemPrompt.content);
|
||||
|
@ -53,7 +46,7 @@ describe('helpers', () => {
|
|||
const prompts = [mockSystemPrompt, mockSuperheroSystemPrompt];
|
||||
const promptIds = prompts.map(({ id }) => id);
|
||||
|
||||
const options = getOptions({ prompts, isCleared: false });
|
||||
const options = getOptions(prompts);
|
||||
const optionValues = options.map(({ value }) => value);
|
||||
|
||||
expect(optionValues).toEqual(promptIds);
|
||||
|
|
|
@ -23,13 +23,11 @@ interface GetOptionFromPromptProps extends PromptResponse {
|
|||
content: string;
|
||||
id: string;
|
||||
name: string;
|
||||
isCleared: boolean;
|
||||
}
|
||||
|
||||
export const getOptionFromPrompt = ({
|
||||
content,
|
||||
id,
|
||||
isCleared,
|
||||
name,
|
||||
}: GetOptionFromPromptProps): EuiSuperSelectOption<string> => ({
|
||||
value: id,
|
||||
|
@ -38,7 +36,7 @@ export const getOptionFromPrompt = ({
|
|||
data-test-subj="systemPromptText"
|
||||
// @ts-ignore
|
||||
css={css`
|
||||
color: ${isCleared ? euiThemeVars.euiColorLightShade : euiThemeVars.euiColorDarkestShade};
|
||||
color: ${euiThemeVars.euiColorDarkestShade};
|
||||
`}
|
||||
>
|
||||
{name}
|
||||
|
@ -58,12 +56,6 @@ export const getOptionFromPrompt = ({
|
|||
),
|
||||
});
|
||||
|
||||
interface GetOptionsProps {
|
||||
prompts: PromptResponse[] | undefined;
|
||||
isCleared: boolean;
|
||||
}
|
||||
export const getOptions = ({
|
||||
prompts,
|
||||
isCleared,
|
||||
}: GetOptionsProps): Array<EuiSuperSelectOption<string>> =>
|
||||
prompts?.map((p) => getOptionFromPrompt({ ...p, isCleared })) ?? [];
|
||||
export const getOptions = (
|
||||
prompts: PromptResponse[] | undefined
|
||||
): Array<EuiSuperSelectOption<string>> => prompts?.map((p) => getOptionFromPrompt(p)) ?? [];
|
||||
|
|
|
@ -6,16 +6,13 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { mockSystemPrompt } from '../../../mock/system_prompt';
|
||||
import { SystemPrompt } from '.';
|
||||
import { Conversation } from '../../../..';
|
||||
import { DEFAULT_CONVERSATION_TITLE } from '../../use_conversation/translations';
|
||||
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';
|
||||
|
||||
|
@ -62,7 +59,6 @@ jest.mock('../../use_conversation', () => {
|
|||
});
|
||||
|
||||
describe('SystemPrompt', () => {
|
||||
const currentSystemPromptId = undefined;
|
||||
const isSettingsModalVisible = false;
|
||||
const onSystemPromptSelectionChange = jest.fn();
|
||||
const setIsSettingsModalVisible = jest.fn();
|
||||
|
@ -79,14 +75,11 @@ describe('SystemPrompt', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when conversation is undefined', () => {
|
||||
const conversation = undefined;
|
||||
|
||||
describe('when currentSystemPromptId is undefined', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<SystemPrompt
|
||||
conversation={conversation}
|
||||
currentSystemPromptId={currentSystemPromptId}
|
||||
currentSystemPromptId={undefined}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
|
@ -104,15 +97,40 @@ describe('SystemPrompt', () => {
|
|||
});
|
||||
|
||||
it('does NOT render the clear button', () => {
|
||||
expect(screen.queryByTestId('clear')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('clearSystemPrompt')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when conversation is NOT null', () => {
|
||||
describe('when currentSystemPromptId does not exist', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<SystemPrompt
|
||||
currentSystemPromptId={'bad-id'}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
allSystemPrompts={mockSystemPrompts}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the system prompt select', () => {
|
||||
expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render the edit button', () => {
|
||||
expect(screen.queryByTestId('edit')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render the clear button', () => {
|
||||
expect(screen.queryByTestId('clearSystemPrompt')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when currentSystemPromptId exists', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
currentSystemPromptId={mockSystemPrompt.id}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
|
@ -134,345 +152,10 @@ describe('SystemPrompt', () => {
|
|||
expect(screen.getByTestId('clearSystemPrompt')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: To be implemented as part of the global settings tests instead of within the SystemPrompt component
|
||||
describe.skip('when a new prompt is saved', () => {
|
||||
it('should save new prompt correctly', async () => {
|
||||
const customPromptName = 'custom prompt';
|
||||
const customPromptText = 'custom prompt text';
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
currentSystemPromptId={currentSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
allSystemPrompts={mockSystemPrompts}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
await userEvent.click(screen.getByTestId('edit'));
|
||||
await userEvent.click(screen.getByTestId(TEST_IDS.ADD_SYSTEM_PROMPT));
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeVisible();
|
||||
|
||||
await userEvent.type(
|
||||
within(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_SELECTOR)).getByTestId('comboBoxInput'),
|
||||
`${customPromptName}[Enter]`
|
||||
);
|
||||
|
||||
await userEvent.type(
|
||||
screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT),
|
||||
customPromptText
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.SAVE));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUseAssistantContext.setAllSystemPrompts).toHaveBeenCalledTimes(1);
|
||||
expect(mockUseAssistantContext.setAllSystemPrompts).toHaveBeenNthCalledWith(1, [
|
||||
mockSystemPrompt,
|
||||
{
|
||||
id: customPromptName,
|
||||
content: customPromptText,
|
||||
name: customPromptName,
|
||||
promptType: 'system',
|
||||
},
|
||||
]);
|
||||
expect(screen.queryByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should save new prompt as a default prompt', async () => {
|
||||
const customPromptName = 'custom prompt';
|
||||
const customPromptText = 'custom prompt text';
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
currentSystemPromptId={currentSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
allSystemPrompts={mockSystemPrompts}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
await userEvent.click(screen.getByTestId('edit'));
|
||||
await userEvent.click(screen.getByTestId(TEST_IDS.ADD_SYSTEM_PROMPT));
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeVisible();
|
||||
|
||||
await userEvent.type(
|
||||
within(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_SELECTOR)).getByTestId('comboBoxInput'),
|
||||
`${customPromptName}[Enter]`
|
||||
);
|
||||
|
||||
await userEvent.type(
|
||||
screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT),
|
||||
customPromptText
|
||||
);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.TOGGLE_ALL_DEFAULT_CONVERSATIONS)
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.TOGGLE_ALL_DEFAULT_CONVERSATIONS)
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.SAVE));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUseAssistantContext.setAllSystemPrompts).toHaveBeenCalledTimes(1);
|
||||
expect(mockUseAssistantContext.setAllSystemPrompts).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
...mockSystemPrompt,
|
||||
isNewConversationDefault: false,
|
||||
},
|
||||
{
|
||||
id: customPromptName,
|
||||
content: customPromptText,
|
||||
name: customPromptName,
|
||||
promptType: 'system',
|
||||
isNewConversationDefault: true,
|
||||
},
|
||||
]);
|
||||
expect(screen.queryByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should save new prompt as a default prompt for selected conversations', async () => {
|
||||
const customPromptName = 'custom prompt';
|
||||
const customPromptText = 'custom prompt text';
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
currentSystemPromptId={currentSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
allSystemPrompts={mockSystemPrompts}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
await userEvent.click(screen.getByTestId('edit'));
|
||||
await userEvent.click(screen.getByTestId(TEST_IDS.ADD_SYSTEM_PROMPT));
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeVisible();
|
||||
|
||||
await userEvent.type(
|
||||
within(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_SELECTOR)).getByTestId('comboBoxInput'),
|
||||
`${customPromptName}[Enter]`
|
||||
);
|
||||
|
||||
await userEvent.type(
|
||||
screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT),
|
||||
customPromptText
|
||||
);
|
||||
|
||||
await userEvent.click(
|
||||
within(screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR)).getByTestId(
|
||||
'comboBoxInput'
|
||||
)
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId(
|
||||
TEST_IDS.CONVERSATIONS_MULTISELECTOR_OPTION(DEFAULT_CONVERSATION_TITLE)
|
||||
)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
// select Default Conversation
|
||||
await userEvent.click(
|
||||
screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR_OPTION(DEFAULT_CONVERSATION_TITLE))
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.SAVE));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(mockUseAssistantContext.setAllSystemPrompts).toHaveBeenCalledTimes(1);
|
||||
expect(mockUseAssistantContext.setConversations).toHaveBeenCalledTimes(1);
|
||||
expect(mockUseAssistantContext.setConversations).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
[DEFAULT_CONVERSATION_TITLE]: expect.objectContaining({
|
||||
id: DEFAULT_CONVERSATION_TITLE,
|
||||
apiConfig: expect.objectContaining({
|
||||
defaultSystemPromptId: customPromptName,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should save new prompt correctly when prompt is removed from selected conversation', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
currentSystemPromptId={currentSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
allSystemPrompts={mockSystemPrompts}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
await userEvent.click(screen.getByTestId('edit'));
|
||||
await userEvent.click(screen.getByTestId(TEST_IDS.ADD_SYSTEM_PROMPT));
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeVisible();
|
||||
|
||||
await userEvent.type(
|
||||
within(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_SELECTOR)).getByTestId('comboBoxInput'),
|
||||
`${mockSystemPrompt.name}[Enter]`
|
||||
);
|
||||
|
||||
expect(
|
||||
within(screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR)).getByText(
|
||||
DEFAULT_CONVERSATION_TITLE
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
await userEvent.click(
|
||||
within(screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR)).getByTestId(
|
||||
'comboBoxClearButton'
|
||||
)
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.SAVE));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeFalsy();
|
||||
});
|
||||
expect(mockUseAssistantContext.setAllSystemPrompts).toHaveBeenCalledTimes(1);
|
||||
expect(mockUseAssistantContext.setConversations).toHaveBeenCalledTimes(1);
|
||||
expect(mockUseAssistantContext.setConversations).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
[DEFAULT_CONVERSATION_TITLE]: expect.objectContaining({
|
||||
id: DEFAULT_CONVERSATION_TITLE,
|
||||
apiConfig: expect.objectContaining({
|
||||
defaultSystemPromptId: undefined,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should save new prompt correctly when prompt is removed from a conversation and linked to another conversation in a single transaction', async () => {
|
||||
const secondMockConversation: Conversation = {
|
||||
id: 'second',
|
||||
category: 'assistant',
|
||||
apiConfig: {
|
||||
actionTypeId: '.gen-ai',
|
||||
connectorId: '123',
|
||||
defaultSystemPromptId: undefined,
|
||||
},
|
||||
title: 'second',
|
||||
messages: [],
|
||||
replacements: {},
|
||||
};
|
||||
const localMockConversations: Record<string, Conversation> = {
|
||||
[DEFAULT_CONVERSATION_TITLE]: BASE_CONVERSATION,
|
||||
[secondMockConversation.title]: secondMockConversation,
|
||||
};
|
||||
|
||||
const localMockUseAssistantContext = {
|
||||
conversations: localMockConversations,
|
||||
setConversations: jest.fn(),
|
||||
setAllSystemPrompts: jest.fn(),
|
||||
allSystemPrompts: mockSystemPrompts,
|
||||
hero: 'abc',
|
||||
};
|
||||
|
||||
(useAssistantContext as jest.Mock).mockImplementation(() => ({
|
||||
...localMockUseAssistantContext,
|
||||
}));
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
currentSystemPromptId={currentSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
allSystemPrompts={mockSystemPrompts}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
await userEvent.click(screen.getByTestId('edit'));
|
||||
await userEvent.click(screen.getByTestId(TEST_IDS.ADD_SYSTEM_PROMPT));
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeVisible();
|
||||
|
||||
await userEvent.type(
|
||||
within(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_SELECTOR)).getByTestId('comboBoxInput'),
|
||||
`${mockSystemPrompt.name}[Enter]`
|
||||
);
|
||||
|
||||
expect(
|
||||
within(screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR)).getByText(
|
||||
DEFAULT_CONVERSATION_TITLE
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
// removed selected conversation
|
||||
await userEvent.click(
|
||||
within(screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR)).getByTestId(
|
||||
'comboBoxClearButton'
|
||||
)
|
||||
);
|
||||
|
||||
// add `second` conversation
|
||||
await userEvent.type(
|
||||
within(screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR)).getByTestId(
|
||||
'comboBoxInput'
|
||||
),
|
||||
'second[Enter]'
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.SAVE));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeFalsy();
|
||||
});
|
||||
|
||||
expect(localMockUseAssistantContext.setAllSystemPrompts).toHaveBeenCalledTimes(1);
|
||||
expect(localMockUseAssistantContext.setConversations).toHaveBeenCalledTimes(1);
|
||||
expect(localMockUseAssistantContext.setConversations).toHaveBeenNthCalledWith(1, {
|
||||
[DEFAULT_CONVERSATION_TITLE]: expect.objectContaining({
|
||||
id: DEFAULT_CONVERSATION_TITLE,
|
||||
apiConfig: expect.objectContaining({
|
||||
defaultSystemPromptId: undefined,
|
||||
}),
|
||||
}),
|
||||
[secondMockConversation.title]: {
|
||||
...secondMockConversation,
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
defaultSystemPromptId: mockSystemPrompt.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the system prompt select when system prompt text is clicked', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
currentSystemPromptId={mockSystemPrompt.id}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
|
|
|
@ -5,68 +5,45 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { PromptResponse } from '@kbn/elastic-assistant-common';
|
||||
import { QueryObserverResult } from '@tanstack/react-query';
|
||||
import { Conversation } from '../../../..';
|
||||
import { SelectSystemPrompt } from './select_system_prompt';
|
||||
|
||||
interface Props {
|
||||
conversation: Conversation | undefined;
|
||||
allSystemPrompts: PromptResponse[];
|
||||
currentSystemPromptId: string | undefined;
|
||||
isSettingsModalVisible: boolean;
|
||||
onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
allSystemPrompts: PromptResponse[];
|
||||
refetchConversations?: () => Promise<QueryObserverResult<Record<string, Conversation>, unknown>>;
|
||||
}
|
||||
|
||||
const SystemPromptComponent: React.FC<Props> = ({
|
||||
conversation,
|
||||
allSystemPrompts,
|
||||
currentSystemPromptId,
|
||||
isSettingsModalVisible,
|
||||
onSystemPromptSelectionChange,
|
||||
setIsSettingsModalVisible,
|
||||
allSystemPrompts,
|
||||
refetchConversations,
|
||||
}) => {
|
||||
const [isCleared, setIsCleared] = useState(false);
|
||||
const selectedPrompt = useMemo(() => {
|
||||
if (currentSystemPromptId !== undefined) {
|
||||
setIsCleared(false);
|
||||
return allSystemPrompts.find((p) => p.id === currentSystemPromptId);
|
||||
} else {
|
||||
return allSystemPrompts.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId);
|
||||
}
|
||||
}, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, currentSystemPromptId]);
|
||||
const selectedPrompt = useMemo(
|
||||
() =>
|
||||
currentSystemPromptId !== undefined
|
||||
? allSystemPrompts.find((p) => p.id === currentSystemPromptId)
|
||||
: undefined,
|
||||
[allSystemPrompts, currentSystemPromptId]
|
||||
);
|
||||
|
||||
const handleClearSystemPrompt = useCallback(() => {
|
||||
if (currentSystemPromptId === undefined) {
|
||||
setIsCleared(false);
|
||||
onSystemPromptSelectionChange(
|
||||
allSystemPrompts.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId)?.id
|
||||
);
|
||||
} else {
|
||||
setIsCleared(true);
|
||||
onSystemPromptSelectionChange(undefined);
|
||||
}
|
||||
}, [
|
||||
allSystemPrompts,
|
||||
conversation?.apiConfig?.defaultSystemPromptId,
|
||||
currentSystemPromptId,
|
||||
onSystemPromptSelectionChange,
|
||||
]);
|
||||
onSystemPromptSelectionChange(undefined);
|
||||
}, [onSystemPromptSelectionChange]);
|
||||
|
||||
return (
|
||||
<SelectSystemPrompt
|
||||
allPrompts={allSystemPrompts}
|
||||
clearSelectedSystemPrompt={handleClearSystemPrompt}
|
||||
conversation={conversation}
|
||||
data-test-subj="systemPrompt"
|
||||
isClearable={true}
|
||||
isCleared={isCleared}
|
||||
refetchConversations={refetchConversations}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
selectedPrompt={selectedPrompt}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
|
|
|
@ -46,9 +46,9 @@ const props: Props = {
|
|||
isNewConversationDefault: true,
|
||||
},
|
||||
],
|
||||
conversation: undefined,
|
||||
isSettingsModalVisible: false,
|
||||
isClearable: true,
|
||||
onSystemPromptSelectionChange: jest.fn(),
|
||||
selectedPrompt: { id: 'default-system-prompt', content: '', name: '', promptType: 'system' },
|
||||
setIsSettingsModalVisible: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -22,12 +22,9 @@ import {
|
|||
PromptResponse,
|
||||
PromptTypeEnum,
|
||||
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
|
||||
import { QueryObserverResult } from '@tanstack/react-query';
|
||||
import { Conversation } from '../../../../..';
|
||||
import { getOptions } from '../helpers';
|
||||
import * as i18n from '../translations';
|
||||
import { useAssistantContext } from '../../../../assistant_context';
|
||||
import { useConversation } from '../../../use_conversation';
|
||||
import { TEST_IDS } from '../../../constants';
|
||||
import { PROMPT_CONTEXT_SELECTOR_PREFIX } from '../../../quick_prompts/prompt_context_selector/translations';
|
||||
import { SYSTEM_PROMPTS_TAB } from '../../../settings/const';
|
||||
|
@ -35,20 +32,14 @@ import { SYSTEM_PROMPTS_TAB } from '../../../settings/const';
|
|||
export interface Props {
|
||||
allPrompts: PromptResponse[];
|
||||
compressed?: boolean;
|
||||
conversation?: Conversation;
|
||||
selectedPrompt: PromptResponse | undefined;
|
||||
clearSelectedSystemPrompt?: () => void;
|
||||
isClearable?: boolean;
|
||||
isCleared?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isOpen?: boolean;
|
||||
isSettingsModalVisible: boolean;
|
||||
selectedPrompt: PromptResponse | undefined;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onSystemPromptSelectionChange?: (promptId: string | undefined) => void;
|
||||
onSelectedConversationChange?: (result: Conversation) => void;
|
||||
setConversationSettings?: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
|
||||
setConversationsSettingsBulkActions?: React.Dispatch<Record<string, Conversation>>;
|
||||
refetchConversations?: () => Promise<QueryObserverResult<Record<string, Conversation>, unknown>>;
|
||||
onSystemPromptSelectionChange: (promptId: string | undefined) => void;
|
||||
}
|
||||
|
||||
const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT';
|
||||
|
@ -56,24 +47,16 @@ const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT';
|
|||
const SelectSystemPromptComponent: React.FC<Props> = ({
|
||||
allPrompts,
|
||||
compressed = false,
|
||||
conversation,
|
||||
selectedPrompt,
|
||||
clearSelectedSystemPrompt,
|
||||
isClearable = false,
|
||||
isCleared = false,
|
||||
isDisabled = false,
|
||||
isOpen = false,
|
||||
refetchConversations,
|
||||
isSettingsModalVisible,
|
||||
onSystemPromptSelectionChange,
|
||||
selectedPrompt,
|
||||
setIsSettingsModalVisible,
|
||||
onSelectedConversationChange,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
}) => {
|
||||
const { setSelectedSettingsTab } = useAssistantContext();
|
||||
const { setApiConfig } = useConversation();
|
||||
|
||||
const allSystemPrompts = useMemo(
|
||||
() => allPrompts.filter((p) => p.promptType === PromptTypeEnum.system),
|
||||
[allPrompts]
|
||||
|
@ -83,26 +66,8 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
const handleOnBlur = useCallback(() => setIsOpenLocal(false), []);
|
||||
const valueOfSelected = useMemo(() => selectedPrompt?.id, [selectedPrompt?.id]);
|
||||
|
||||
// Write the selected system prompt to the conversation config
|
||||
const setSelectedSystemPrompt = useCallback(
|
||||
async (promptId?: string) => {
|
||||
if (conversation && conversation.apiConfig) {
|
||||
const result = await setApiConfig({
|
||||
conversation,
|
||||
apiConfig: {
|
||||
...conversation.apiConfig,
|
||||
defaultSystemPromptId: promptId,
|
||||
},
|
||||
});
|
||||
await refetchConversations?.();
|
||||
return result;
|
||||
}
|
||||
},
|
||||
[conversation, refetchConversations, setApiConfig]
|
||||
);
|
||||
|
||||
const addNewSystemPrompt = useMemo(() => {
|
||||
return {
|
||||
const addNewSystemPrompt = useMemo(
|
||||
() => ({
|
||||
value: ADD_NEW_SYSTEM_PROMPT,
|
||||
inputDisplay: i18n.ADD_NEW_SYSTEM_PROMPT,
|
||||
dropdownDisplay: (
|
||||
|
@ -118,14 +83,12 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
};
|
||||
}, []);
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// SuperSelect State/Actions
|
||||
const options = useMemo(
|
||||
() => getOptions({ prompts: allSystemPrompts, isCleared }),
|
||||
[allSystemPrompts, isCleared]
|
||||
);
|
||||
const options = useMemo(() => getOptions(allSystemPrompts), [allSystemPrompts]);
|
||||
|
||||
const onChange = useCallback(
|
||||
async (selectedSystemPromptId: string) => {
|
||||
|
@ -134,38 +97,9 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
setSelectedSettingsTab(SYSTEM_PROMPTS_TAB);
|
||||
return;
|
||||
}
|
||||
// Note: if callback is provided, this component does not persist. Extract to separate component
|
||||
if (onSystemPromptSelectionChange != null) {
|
||||
onSystemPromptSelectionChange(selectedSystemPromptId);
|
||||
}
|
||||
const result = await setSelectedSystemPrompt(selectedSystemPromptId);
|
||||
if (result) {
|
||||
setConversationSettings?.((prev: Record<string, Conversation>) => {
|
||||
const newConversationsSettings = Object.entries(prev).reduce<
|
||||
Record<string, Conversation>
|
||||
>((acc, [key, convo]) => {
|
||||
if (result.title === convo.title) {
|
||||
acc[result.id] = result;
|
||||
} else {
|
||||
acc[key] = convo;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return newConversationsSettings;
|
||||
});
|
||||
onSelectedConversationChange?.(result);
|
||||
setConversationsSettingsBulkActions?.({});
|
||||
}
|
||||
onSystemPromptSelectionChange(selectedSystemPromptId);
|
||||
},
|
||||
[
|
||||
onSelectedConversationChange,
|
||||
onSystemPromptSelectionChange,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
setIsSettingsModalVisible,
|
||||
setSelectedSettingsTab,
|
||||
setSelectedSystemPrompt,
|
||||
]
|
||||
[onSystemPromptSelectionChange, setIsSettingsModalVisible, setSelectedSettingsTab]
|
||||
);
|
||||
|
||||
const clearSystemPrompt = useCallback(() => {
|
||||
|
@ -234,14 +168,10 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
inline-size: 16px;
|
||||
block-size: 16px;
|
||||
border-radius: 16px;
|
||||
background: ${isCleared
|
||||
? euiThemeVars.euiColorLightShade
|
||||
: euiThemeVars.euiColorMediumShade};
|
||||
background: ${euiThemeVars.euiColorMediumShade};
|
||||
|
||||
:hover:not(:disabled) {
|
||||
background: ${isCleared
|
||||
? euiThemeVars.euiColorLightShade
|
||||
: euiThemeVars.euiColorMediumShade};
|
||||
background: ${euiThemeVars.euiColorMediumShade};
|
||||
transform: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,10 +32,7 @@ import { TEST_IDS } from '../../../constants';
|
|||
import { ConversationsBulkActions } from '../../../api';
|
||||
import { getSelectedConversations } from '../system_prompt_settings_management/utils';
|
||||
import { useSystemPromptEditor } from './use_system_prompt_editor';
|
||||
import {
|
||||
getConversationApiConfig,
|
||||
getFallbackDefaultSystemPrompt,
|
||||
} from '../../../use_conversation/helpers';
|
||||
import { getConversationApiConfig } from '../../../use_conversation/helpers';
|
||||
|
||||
interface Props {
|
||||
connectors: AIConnector[] | undefined;
|
||||
|
@ -186,7 +183,7 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
|
|||
if (selectedSystemPrompt != null) {
|
||||
setConversationSettings((prev) =>
|
||||
keyBy(
|
||||
'title',
|
||||
'id',
|
||||
/*
|
||||
* updatedConversationWithPrompts calculates the present of prompt for
|
||||
* each conversation. Based on the values of selected conversation, it goes
|
||||
|
@ -228,9 +225,7 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
|
|||
conversation: convo,
|
||||
defaultConnector,
|
||||
}).apiConfig,
|
||||
defaultSystemPromptId:
|
||||
getDefaultSystemPromptId(convo) ??
|
||||
getFallbackDefaultSystemPrompt({ allSystemPrompts: systemPromptSettings })?.id,
|
||||
defaultSystemPromptId: getDefaultSystemPromptId(convo),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -133,14 +133,14 @@ describe('SystemPromptSettings', () => {
|
|||
fireEvent.click(getByTestId('change-multi'));
|
||||
|
||||
expect(setConversationSettings).toHaveReturnedWith({
|
||||
[welcomeConvo.title]: {
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
apiConfig: {
|
||||
...welcomeConvo.apiConfig,
|
||||
defaultSystemPromptId: 'mock-system-prompt-1',
|
||||
},
|
||||
},
|
||||
[alertConvo.title]: {
|
||||
[alertConvo.id]: {
|
||||
...alertConvo,
|
||||
apiConfig: {
|
||||
...alertConvo.apiConfig,
|
||||
|
|
|
@ -172,7 +172,9 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
// If the selected conversation is deleted, we need to select a new conversation to prevent a crash creating a conversation that already exists
|
||||
const isSelectedConversationDeleted =
|
||||
defaultSelectedConversationId &&
|
||||
conversationSettings[defaultSelectedConversationId] == null;
|
||||
// sometimes the key is a title, so do not rely on conversationSettings[defaultSelectedConversationId]
|
||||
!Object.values(conversationSettings).some(({ id }) => id === defaultSelectedConversationId);
|
||||
|
||||
const newSelectedConversation: Conversation | undefined =
|
||||
Object.values(conversationSettings)[0];
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { ENTERPRISE } from '../../content/prompts/welcome/translations';
|
||||
import { ENTERPRISE } from './translations';
|
||||
import { UpgradeButtons } from '../../upgrade/upgrade_buttons';
|
||||
|
||||
interface OwnProps {
|
||||
|
|
|
@ -120,10 +120,10 @@ describe('useConversation helpers', () => {
|
|||
expect(result).toEqual(systemPrompts[1]);
|
||||
});
|
||||
|
||||
test('should return the fallback prompt if default new system prompt do not exist', () => {
|
||||
test('should return undefined if default new system prompt do not exist', () => {
|
||||
const result = getDefaultNewSystemPrompt([systemPrompts[0]]);
|
||||
|
||||
expect(result).toEqual(systemPrompts[0]);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('should return undefined if default (starred) isNewConversationDefault system prompt does not exist and there are no system prompts', () => {
|
||||
|
|
|
@ -10,7 +10,6 @@ import { ApiConfig, PromptResponse } from '@kbn/elastic-assistant-common';
|
|||
import { Conversation } from '../../assistant_context/types';
|
||||
import { AIConnector } from '../../connectorland/connector_selector';
|
||||
import { getGenAiConfig } from '../../connectorland/helpers';
|
||||
import { DEFAULT_SYSTEM_PROMPT_NAME } from '../../content/prompts/system/translations';
|
||||
|
||||
export interface CodeBlockDetails {
|
||||
type: QueryType;
|
||||
|
@ -76,11 +75,10 @@ export const analyzeMarkdown = (markdown: string): CodeBlockDetails[] => {
|
|||
*
|
||||
* @param allSystemPrompts All available System Prompts
|
||||
*/
|
||||
export const getDefaultNewSystemPrompt = (allSystemPrompts: PromptResponse[]) => {
|
||||
const fallbackSystemPrompt = allSystemPrompts.find(
|
||||
(prompt) => prompt.name === DEFAULT_SYSTEM_PROMPT_NAME
|
||||
);
|
||||
return allSystemPrompts.find((prompt) => prompt.isNewConversationDefault) ?? fallbackSystemPrompt;
|
||||
export const getDefaultNewSystemPrompt = (
|
||||
allSystemPrompts: PromptResponse[]
|
||||
): PromptResponse | undefined => {
|
||||
return allSystemPrompts.find((prompt) => prompt.isNewConversationDefault);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -103,24 +101,6 @@ export const getDefaultSystemPrompt = ({
|
|||
return conversationSystemPrompt;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the default system prompt
|
||||
*
|
||||
* @param allSystemPrompts All available System Prompts
|
||||
* @param conversation Conversation to get the default system prompt for
|
||||
*/
|
||||
export const getFallbackDefaultSystemPrompt = ({
|
||||
allSystemPrompts,
|
||||
}: {
|
||||
allSystemPrompts: PromptResponse[];
|
||||
}): PromptResponse | undefined => {
|
||||
const fallbackSystemPrompt = allSystemPrompts.find(
|
||||
(prompt) => prompt.name === DEFAULT_SYSTEM_PROMPT_NAME
|
||||
);
|
||||
|
||||
return fallbackSystemPrompt;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the API config for a conversation
|
||||
*
|
||||
|
|
|
@ -72,17 +72,31 @@ describe('useCurrentConversation', () => {
|
|||
const { result } = setupHook();
|
||||
|
||||
expect(result.current.currentConversation).toBeUndefined();
|
||||
expect(result.current.currentSystemPromptId).toBeUndefined();
|
||||
expect(result.current.currentSystemPrompt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set the current system prompt ID when the prompt selection changes', () => {
|
||||
const { result } = setupHook();
|
||||
it('should set the current system prompt ID when the prompt selection changes', async () => {
|
||||
const conversationId = 'welcome_id';
|
||||
const conversation = mockData.welcome_id;
|
||||
mockUseConversation.getConversation.mockResolvedValue(conversation);
|
||||
|
||||
act(() => {
|
||||
result.current.setCurrentSystemPromptId('prompt-id');
|
||||
const { result } = setupHook({
|
||||
conversationId,
|
||||
conversations: { [conversationId]: conversation },
|
||||
});
|
||||
|
||||
expect(result.current.currentSystemPromptId).toBe('prompt-id');
|
||||
await act(async () => {
|
||||
await result.current.setCurrentSystemPromptId('prompt-id');
|
||||
});
|
||||
|
||||
expect(mockUseConversation.setApiConfig).toHaveBeenCalledWith({
|
||||
conversation,
|
||||
apiConfig: {
|
||||
...conversation.apiConfig,
|
||||
defaultSystemPromptId: 'prompt-id',
|
||||
},
|
||||
});
|
||||
expect(defaultProps.refetchCurrentUserConversations).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch and set the current conversation', async () => {
|
||||
|
@ -136,7 +150,7 @@ describe('useCurrentConversation', () => {
|
|||
});
|
||||
|
||||
expect(result.current.currentConversation).toEqual(conversation);
|
||||
expect(result.current.currentSystemPromptId).toBe('something-crazy');
|
||||
expect(result.current.currentSystemPrompt?.id).toBe('something-crazy');
|
||||
});
|
||||
|
||||
it('should non-existing handle conversation selection', async () => {
|
||||
|
@ -169,7 +183,7 @@ describe('useCurrentConversation', () => {
|
|||
});
|
||||
|
||||
expect(result.current.currentConversation).toEqual(mockData.welcome_id);
|
||||
expect(result.current.currentSystemPromptId).toBe('system-prompt-id');
|
||||
expect(result.current.currentSystemPrompt?.id).toBe('system-prompt-id');
|
||||
});
|
||||
|
||||
it('should create a new conversation', async () => {
|
||||
|
@ -210,6 +224,115 @@ describe('useCurrentConversation', () => {
|
|||
expect(mockUseConversation.createConversation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a new conversation using the connector portion of the apiConfig of the current conversation', async () => {
|
||||
const newConversation = {
|
||||
...mockData.welcome_id,
|
||||
id: 'new-id',
|
||||
title: 'NEW_CHAT',
|
||||
messages: [],
|
||||
} as Conversation;
|
||||
mockUseConversation.createConversation.mockResolvedValue(newConversation);
|
||||
|
||||
const { result } = setupHook({
|
||||
conversations: {
|
||||
'old-id': {
|
||||
...mockData.welcome_id,
|
||||
id: 'old-id',
|
||||
title: 'Old Chat',
|
||||
messages: [],
|
||||
} as Conversation,
|
||||
},
|
||||
conversationId: 'old-id',
|
||||
refetchCurrentUserConversations: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
'old-id': {
|
||||
...mockData.welcome_id,
|
||||
id: 'old-id',
|
||||
title: 'Old Chat',
|
||||
messages: [],
|
||||
} as Conversation,
|
||||
[newConversation.id]: newConversation,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleCreateConversation();
|
||||
});
|
||||
const { defaultSystemPromptId: _, ...everythingExceptSystemPromptId } =
|
||||
mockData.welcome_id.apiConfig;
|
||||
|
||||
expect(mockUseConversation.createConversation).toHaveBeenCalledWith({
|
||||
apiConfig: everythingExceptSystemPromptId,
|
||||
title: 'New chat',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new conversation with correct isNewConversationDefault: true system prompt', async () => {
|
||||
const newConversation = {
|
||||
...mockData.welcome_id,
|
||||
id: 'new-id',
|
||||
title: 'NEW_CHAT',
|
||||
messages: [],
|
||||
} as Conversation;
|
||||
mockUseConversation.createConversation.mockResolvedValue(newConversation);
|
||||
|
||||
const { result } = setupHook({
|
||||
conversations: {
|
||||
'old-id': {
|
||||
...mockData.welcome_id,
|
||||
id: 'old-id',
|
||||
title: 'Old Chat',
|
||||
messages: [],
|
||||
} as Conversation,
|
||||
},
|
||||
allSystemPrompts: [
|
||||
{
|
||||
timestamp: '2024-09-10T15:52:24.761Z',
|
||||
users: [
|
||||
{
|
||||
id: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0',
|
||||
name: 'elastic',
|
||||
},
|
||||
],
|
||||
content: 'Address the user as Mr Orange in each response',
|
||||
isNewConversationDefault: true,
|
||||
updatedAt: '2024-09-10T22:07:44.915Z',
|
||||
id: 'LBOi3JEBy3uD9EGi1d_G',
|
||||
name: 'Call me Orange',
|
||||
promptType: 'system',
|
||||
consumer: 'securitySolutionUI',
|
||||
},
|
||||
],
|
||||
conversationId: 'old-id',
|
||||
refetchCurrentUserConversations: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
'old-id': {
|
||||
...mockData.welcome_id,
|
||||
id: 'old-id',
|
||||
title: 'Old Chat',
|
||||
messages: [],
|
||||
} as Conversation,
|
||||
[newConversation.id]: newConversation,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleCreateConversation();
|
||||
});
|
||||
const { defaultSystemPromptId: _, ...everythingExceptSystemPromptId } =
|
||||
mockData.welcome_id.apiConfig;
|
||||
|
||||
expect(mockUseConversation.createConversation).toHaveBeenCalledWith({
|
||||
apiConfig: {
|
||||
...everythingExceptSystemPromptId,
|
||||
defaultSystemPromptId: 'LBOi3JEBy3uD9EGi1d_G',
|
||||
},
|
||||
title: 'New chat',
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a conversation', async () => {
|
||||
const conversationTitle = 'Test Conversation';
|
||||
const conversation = {
|
||||
|
|
|
@ -13,7 +13,7 @@ import deepEqual from 'fast-deep-equal';
|
|||
import { AIConnector } from '../../connectorland/connector_selector';
|
||||
import { getGenAiConfig } from '../../connectorland/helpers';
|
||||
import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations';
|
||||
import { getDefaultSystemPrompt } from '../use_conversation/helpers';
|
||||
import { getDefaultNewSystemPrompt, getDefaultSystemPrompt } from '../use_conversation/helpers';
|
||||
import { useConversation } from '../use_conversation';
|
||||
import { sleep } from '../helpers';
|
||||
import { Conversation, WELCOME_CONVERSATION_TITLE } from '../../..';
|
||||
|
@ -31,7 +31,7 @@ export interface Props {
|
|||
|
||||
interface UseCurrentConversation {
|
||||
currentConversation: Conversation | undefined;
|
||||
currentSystemPromptId: string | undefined;
|
||||
currentSystemPrompt: PromptResponse | undefined;
|
||||
handleCreateConversation: () => Promise<void>;
|
||||
handleOnConversationDeleted: (cTitle: string) => Promise<void>;
|
||||
handleOnConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise<void>;
|
||||
|
@ -41,7 +41,7 @@ interface UseCurrentConversation {
|
|||
isStreamRefetch?: boolean;
|
||||
}) => Promise<Conversation | undefined>;
|
||||
setCurrentConversation: Dispatch<SetStateAction<Conversation | undefined>>;
|
||||
setCurrentSystemPromptId: Dispatch<SetStateAction<string | undefined>>;
|
||||
setCurrentSystemPromptId: (promptId: string | undefined) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,12 +83,22 @@ export const useCurrentConversation = ({
|
|||
[allSystemPrompts, currentConversation]
|
||||
);
|
||||
|
||||
const [currentSystemPromptId, setCurrentSystemPromptId] = useState<string | undefined>(
|
||||
currentSystemPrompt?.id
|
||||
// Write the selected system prompt to the conversation config
|
||||
const setCurrentSystemPromptId = useCallback(
|
||||
async (promptId?: string) => {
|
||||
if (currentConversation && currentConversation.apiConfig) {
|
||||
await setApiConfig({
|
||||
conversation: currentConversation,
|
||||
apiConfig: {
|
||||
...currentConversation.apiConfig,
|
||||
defaultSystemPromptId: promptId,
|
||||
},
|
||||
});
|
||||
await refetchCurrentUserConversations();
|
||||
}
|
||||
},
|
||||
[currentConversation, refetchCurrentUserConversations, setApiConfig]
|
||||
);
|
||||
useEffect(() => {
|
||||
setCurrentSystemPromptId(currentSystemPrompt?.id);
|
||||
}, [currentSystemPrompt?.id]);
|
||||
|
||||
/**
|
||||
* END SYSTEM PROMPT
|
||||
|
@ -248,10 +258,20 @@ export const useCurrentConversation = ({
|
|||
});
|
||||
return;
|
||||
}
|
||||
const newSystemPrompt = getDefaultNewSystemPrompt(allSystemPrompts);
|
||||
|
||||
const newConversation = await createConversation({
|
||||
title: NEW_CHAT,
|
||||
apiConfig: currentConversation?.apiConfig,
|
||||
...(currentConversation?.apiConfig != null &&
|
||||
currentConversation?.apiConfig?.actionTypeId != null
|
||||
? {
|
||||
apiConfig: {
|
||||
connectorId: currentConversation.apiConfig.connectorId,
|
||||
actionTypeId: currentConversation.apiConfig.actionTypeId,
|
||||
...(newSystemPrompt?.id != null ? { defaultSystemPromptId: newSystemPrompt.id } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (newConversation) {
|
||||
|
@ -263,6 +283,7 @@ export const useCurrentConversation = ({
|
|||
await refetchCurrentUserConversations();
|
||||
}
|
||||
}, [
|
||||
allSystemPrompts,
|
||||
conversations,
|
||||
createConversation,
|
||||
currentConversation?.apiConfig,
|
||||
|
@ -272,7 +293,7 @@ export const useCurrentConversation = ({
|
|||
|
||||
return {
|
||||
currentConversation,
|
||||
currentSystemPromptId,
|
||||
currentSystemPrompt,
|
||||
handleCreateConversation,
|
||||
handleOnConversationDeleted,
|
||||
handleOnConversationSelected,
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiCommentProps } from '@elastic/eui';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { omit } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo, useState, useRef } from 'react';
|
||||
|
@ -21,7 +20,12 @@ import type {
|
|||
RegisterPromptContext,
|
||||
UnRegisterPromptContext,
|
||||
} from '../assistant/prompt_context/types';
|
||||
import type { Conversation } from './types';
|
||||
import {
|
||||
AssistantAvailability,
|
||||
AssistantTelemetry,
|
||||
Conversation,
|
||||
GetAssistantMessages,
|
||||
} from './types';
|
||||
import { DEFAULT_ASSISTANT_TITLE } from '../assistant/translations';
|
||||
import { CodeBlockDetails } from '../assistant/use_conversation/helpers';
|
||||
import { PromptContextTemplate } from '../assistant/prompt_context/types';
|
||||
|
@ -34,7 +38,6 @@ import {
|
|||
STREAMING_LOCAL_STORAGE_KEY,
|
||||
TRACE_OPTIONS_SESSION_STORAGE_KEY,
|
||||
} from './constants';
|
||||
import { AssistantAvailability, AssistantTelemetry } from './types';
|
||||
import { useCapabilities } from '../assistant/api/capabilities/use_capabilities';
|
||||
import { WELCOME_CONVERSATION_TITLE } from '../assistant/use_conversation/translations';
|
||||
import { SettingsTabs } from '../assistant/settings/types';
|
||||
|
@ -63,16 +66,7 @@ export interface AssistantProviderProps {
|
|||
basePromptContexts?: PromptContextTemplate[];
|
||||
docLinks: Omit<DocLinksStart, 'links'>;
|
||||
children: React.ReactNode;
|
||||
getComments: (commentArgs: {
|
||||
abortStream: () => void;
|
||||
currentConversation?: Conversation;
|
||||
isFetchingResponse: boolean;
|
||||
refetchCurrentConversation: ({ isStreamRefetch }: { isStreamRefetch?: boolean }) => void;
|
||||
regenerateMessage: (conversationId: string) => void;
|
||||
showAnonymizedValues: boolean;
|
||||
setIsStreaming: (isStreaming: boolean) => void;
|
||||
currentUserAvatar?: UserAvatar;
|
||||
}) => EuiCommentProps[];
|
||||
getComments: GetAssistantMessages;
|
||||
http: HttpSetup;
|
||||
baseConversations: Record<string, Conversation>;
|
||||
nameSpace?: string;
|
||||
|
@ -102,16 +96,7 @@ export interface UseAssistantContext {
|
|||
docLinks: Omit<DocLinksStart, 'links'>;
|
||||
basePath: string;
|
||||
baseConversations: Record<string, Conversation>;
|
||||
getComments: (commentArgs: {
|
||||
abortStream: () => void;
|
||||
currentConversation?: Conversation;
|
||||
isFetchingResponse: boolean;
|
||||
refetchCurrentConversation: ({ isStreamRefetch }: { isStreamRefetch?: boolean }) => void;
|
||||
regenerateMessage: () => void;
|
||||
showAnonymizedValues: boolean;
|
||||
currentUserAvatar?: UserAvatar;
|
||||
setIsStreaming: (isStreaming: boolean) => void;
|
||||
}) => EuiCommentProps[];
|
||||
getComments: GetAssistantMessages;
|
||||
http: HttpSetup;
|
||||
knowledgeBase: KnowledgeBaseConfig;
|
||||
getLastConversationId: (conversationTitle?: string) => string;
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import { ApiConfig, Message, Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { EuiCommentProps } from '@elastic/eui';
|
||||
import { UserAvatar } from '.';
|
||||
|
||||
export interface MessagePresentation {
|
||||
delay?: number;
|
||||
|
@ -67,3 +69,15 @@ export interface AssistantAvailability {
|
|||
// When true, user has `Edit` privilege for `AnonymizationFields`
|
||||
hasUpdateAIAssistantAnonymization: boolean;
|
||||
}
|
||||
|
||||
export type GetAssistantMessages = (commentArgs: {
|
||||
abortStream: () => void;
|
||||
currentConversation?: Conversation;
|
||||
isFetchingResponse: boolean;
|
||||
refetchCurrentConversation: ({ isStreamRefetch }: { isStreamRefetch?: boolean }) => void;
|
||||
regenerateMessage: (conversationId: string) => void;
|
||||
showAnonymizedValues: boolean;
|
||||
currentUserAvatar?: UserAvatar;
|
||||
setIsStreaming: (isStreaming: boolean) => void;
|
||||
systemPromptContent?: string;
|
||||
}) => EuiCommentProps[];
|
||||
|
|
|
@ -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 React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { SYSTEM_PROMPT_CONTEXT_NON_I18N } from '../../content/prompts/system/translations';
|
||||
import { ReadOnlyContextViewer, Props } from '.';
|
||||
|
||||
const defaultProps: Props = {
|
||||
rawData: 'this content is NOT anonymized',
|
||||
};
|
||||
|
||||
describe('ReadOnlyContextViewer', () => {
|
||||
it('renders the context with the correct formatting', () => {
|
||||
render(<ReadOnlyContextViewer {...defaultProps} />);
|
||||
|
||||
const contextBlock = screen.getByTestId('readOnlyContextViewer');
|
||||
|
||||
expect(contextBlock.textContent).toBe(SYSTEM_PROMPT_CONTEXT_NON_I18N(defaultProps.rawData));
|
||||
});
|
||||
});
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
import { EuiCodeBlock } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { SYSTEM_PROMPT_CONTEXT_NON_I18N } from '../../content/prompts/system/translations';
|
||||
import { SYSTEM_PROMPT_CONTEXT_NON_I18N } from '../../assistant/prompt/translations';
|
||||
|
||||
export interface Props {
|
||||
rawData: string;
|
||||
|
|
|
@ -106,6 +106,8 @@ export type {
|
|||
Conversation,
|
||||
/** Message interface on the client */
|
||||
ClientMessage,
|
||||
/** Function type to return messages UI */
|
||||
GetAssistantMessages,
|
||||
} from './impl/assistant_context/types';
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,6 +23,10 @@ export const getUpdateScript = ({
|
|||
ctx._source.api_config.remove('model');
|
||||
ctx._source.api_config.remove('provider');
|
||||
}
|
||||
// an update to apiConfig that does not contain defaultSystemPromptId should remove it
|
||||
if (params.assignEmpty == true || (params.containsKey('api_config') && !params.api_config.containsKey('default_system_prompt_id'))) {
|
||||
ctx._source.api_config.remove('default_system_prompt_id');
|
||||
}
|
||||
if (params.assignEmpty == true || params.api_config.containsKey('action_type_id')) {
|
||||
ctx._source.api_config.action_type_id = params.api_config.action_type_id;
|
||||
}
|
||||
|
|
|
@ -134,10 +134,6 @@ describe('transformToUpdateScheme', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('it returns a transformed conversation with converted string datetime to ISO from the client', async () => {
|
||||
const conversation: ConversationUpdateProps = getUpdateConversationOptionsMock();
|
||||
const existingConversation = getConversationResponseMock();
|
||||
|
@ -199,4 +195,43 @@ describe('transformToUpdateScheme', () => {
|
|||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
test('it does not pass api_config if apiConfig is not updated', async () => {
|
||||
const conversation: ConversationUpdateProps = getUpdateConversationOptionsMock();
|
||||
const existingConversation = getConversationResponseMock();
|
||||
(getConversation as unknown as jest.Mock).mockResolvedValueOnce(existingConversation);
|
||||
|
||||
const updateAt = new Date().toISOString();
|
||||
const transformed = transformToUpdateScheme(updateAt, {
|
||||
id: conversation.id,
|
||||
messages: [
|
||||
{
|
||||
content: 'Message 3',
|
||||
role: 'user',
|
||||
timestamp: '2011-10-05T14:48:00.000Z',
|
||||
traceData: {
|
||||
traceId: 'something',
|
||||
transactionId: 'something',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const expected: UpdateConversationSchema = {
|
||||
id: conversation.id,
|
||||
updated_at: updateAt,
|
||||
messages: [
|
||||
{
|
||||
'@timestamp': '2011-10-05T14:48:00.000Z',
|
||||
content: 'Message 3',
|
||||
is_error: undefined,
|
||||
reader: undefined,
|
||||
role: 'user',
|
||||
trace_data: {
|
||||
trace_id: 'something',
|
||||
transaction_id: 'something',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -115,13 +115,17 @@ export const transformToUpdateScheme = (
|
|||
id,
|
||||
updated_at: updatedAt,
|
||||
title,
|
||||
api_config: {
|
||||
action_type_id: apiConfig?.actionTypeId,
|
||||
connector_id: apiConfig?.connectorId,
|
||||
default_system_prompt_id: apiConfig?.defaultSystemPromptId,
|
||||
model: apiConfig?.model,
|
||||
provider: apiConfig?.provider,
|
||||
},
|
||||
...(apiConfig
|
||||
? {
|
||||
api_config: {
|
||||
action_type_id: apiConfig?.actionTypeId,
|
||||
connector_id: apiConfig?.connectorId,
|
||||
default_system_prompt_id: apiConfig?.defaultSystemPromptId,
|
||||
model: apiConfig?.model,
|
||||
provider: apiConfig?.provider,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
exclude_from_last_conversation_storage: excludeFromLastConversationStorage,
|
||||
replacements: replacements
|
||||
? Object.keys(replacements).map((key) => ({
|
||||
|
|
|
@ -54,6 +54,7 @@ export interface AgentExecutorParams<T extends boolean> {
|
|||
request: KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>;
|
||||
response?: KibanaResponseFactory;
|
||||
size?: number;
|
||||
systemPrompt?: string;
|
||||
traceOptions?: TraceOptions;
|
||||
responseLanguage?: string;
|
||||
}
|
||||
|
|
|
@ -18,12 +18,7 @@ import { getLlmClass } from '../../../../routes/utils';
|
|||
import { EsAnonymizationFieldsSchema } from '../../../../ai_assistant_data_clients/anonymization_fields/types';
|
||||
import { AssistantToolParams } from '../../../../types';
|
||||
import { AgentExecutor } from '../../executors/types';
|
||||
import {
|
||||
bedrockToolCallingAgentPrompt,
|
||||
geminiToolCallingAgentPrompt,
|
||||
openAIFunctionAgentPrompt,
|
||||
structuredChatAgentPrompt,
|
||||
} from './prompts';
|
||||
import { formatPrompt, formatPromptStructured, systemPrompts } from './prompts';
|
||||
import { GraphInputs } from './types';
|
||||
import { getDefaultAssistantGraph } from './graph';
|
||||
import { invokeGraph, streamGraph } from './helpers';
|
||||
|
@ -52,6 +47,7 @@ export const callAssistantGraph: AgentExecutor<true | false> = async ({
|
|||
replacements,
|
||||
request,
|
||||
size,
|
||||
systemPrompt,
|
||||
traceOptions,
|
||||
responseLanguage = 'English',
|
||||
}) => {
|
||||
|
@ -141,7 +137,7 @@ export const callAssistantGraph: AgentExecutor<true | false> = async ({
|
|||
? await createOpenAIFunctionsAgent({
|
||||
llm: createLlmInstance(),
|
||||
tools,
|
||||
prompt: openAIFunctionAgentPrompt,
|
||||
prompt: formatPrompt(systemPrompts.openai, systemPrompt),
|
||||
streamRunnable: isStream,
|
||||
})
|
||||
: llmType && ['bedrock', 'gemini'].includes(llmType) && bedrockChatEnabled
|
||||
|
@ -149,13 +145,15 @@ export const callAssistantGraph: AgentExecutor<true | false> = async ({
|
|||
llm: createLlmInstance(),
|
||||
tools,
|
||||
prompt:
|
||||
llmType === 'bedrock' ? bedrockToolCallingAgentPrompt : geminiToolCallingAgentPrompt,
|
||||
llmType === 'bedrock'
|
||||
? formatPrompt(systemPrompts.bedrock, systemPrompt)
|
||||
: formatPrompt(systemPrompts.gemini, systemPrompt),
|
||||
streamRunnable: isStream,
|
||||
})
|
||||
: await createStructuredChatAgent({
|
||||
llm: createLlmInstance(),
|
||||
tools,
|
||||
prompt: structuredChatAgentPrompt,
|
||||
prompt: formatPromptStructured(systemPrompts.structuredChat, systemPrompt),
|
||||
streamRunnable: isStream,
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
// TODO determine whether or not system prompts should be i18n'd
|
||||
const YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT =
|
||||
'You are a security analyst and expert in resolving security incidents. Your role is to assist by answering questions about Elastic Security.';
|
||||
const IF_YOU_DONT_KNOW_THE_ANSWER = 'Do not answer questions unrelated to Elastic Security.';
|
||||
|
||||
export const DEFAULT_SYSTEM_PROMPT = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}`;
|
||||
|
||||
export const GEMINI_SYSTEM_PROMPT =
|
||||
`ALWAYS use the provided tools, as they have access to the latest data and syntax.` +
|
||||
"The final response is the only output the user sees and should be a complete answer to the user's question. Do not leave out important tool output. The final response should never be empty. Don't forget to use tools.";
|
||||
export const BEDROCK_SYSTEM_PROMPT = `Use tools as often as possible, as they have access to the latest data and syntax. Always return value from ESQLKnowledgeBaseTool as is. Never return <thinking> tags in the response, but make sure to include <result> tags content in the response. Do not reflect on the quality of the returned search results in your response.`;
|
|
@ -6,69 +6,95 @@
|
|||
*/
|
||||
|
||||
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||
import {
|
||||
BEDROCK_SYSTEM_PROMPT,
|
||||
DEFAULT_SYSTEM_PROMPT,
|
||||
GEMINI_SYSTEM_PROMPT,
|
||||
} from './nodes/translations';
|
||||
|
||||
export const openAIFunctionAgentPrompt = ChatPromptTemplate.fromMessages([
|
||||
['system', 'You are a helpful assistant'],
|
||||
['placeholder', '{chat_history}'],
|
||||
['human', '{input}'],
|
||||
['placeholder', '{agent_scratchpad}'],
|
||||
]);
|
||||
export const formatPrompt = (prompt: string, additionalPrompt?: string) =>
|
||||
ChatPromptTemplate.fromMessages([
|
||||
['system', additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt],
|
||||
['placeholder', '{chat_history}'],
|
||||
['human', '{input}'],
|
||||
['placeholder', '{agent_scratchpad}'],
|
||||
]);
|
||||
|
||||
export const bedrockToolCallingAgentPrompt = ChatPromptTemplate.fromMessages([
|
||||
[
|
||||
'system',
|
||||
'You are a helpful assistant. ALWAYS use the provided tools. Use tools as often as possible, as they have access to the latest data and syntax. Always return value from ESQLKnowledgeBaseTool as is. Never return <thinking> tags in the response, but make sure to include <result> tags content in the response. Do not reflect on the quality of the returned search results in your response.',
|
||||
],
|
||||
['placeholder', '{chat_history}'],
|
||||
['human', '{input}'],
|
||||
['placeholder', '{agent_scratchpad}'],
|
||||
]);
|
||||
export const systemPrompts = {
|
||||
openai: DEFAULT_SYSTEM_PROMPT,
|
||||
bedrock: `${DEFAULT_SYSTEM_PROMPT} ${BEDROCK_SYSTEM_PROMPT}`,
|
||||
gemini: `${DEFAULT_SYSTEM_PROMPT} ${GEMINI_SYSTEM_PROMPT}`,
|
||||
structuredChat: `Respond to the human as helpfully and accurately as possible. You have access to the following tools:
|
||||
|
||||
export const geminiToolCallingAgentPrompt = ChatPromptTemplate.fromMessages([
|
||||
[
|
||||
'system',
|
||||
'You are a helpful assistant. ALWAYS use the provided tools. Use tools as often as possible, as they have access to the latest data and syntax.\n\n' +
|
||||
"The final response will be the only output the user sees and should be a complete answer to the user's question, as if you were responding to the user's initial question. The final response should never be empty.",
|
||||
],
|
||||
['placeholder', '{chat_history}'],
|
||||
['human', '{input}'],
|
||||
['placeholder', '{agent_scratchpad}'],
|
||||
]);
|
||||
{tools}
|
||||
|
||||
export const structuredChatAgentPrompt = ChatPromptTemplate.fromMessages([
|
||||
[
|
||||
'system',
|
||||
'Respond to the human as helpfully and accurately as possible. You have access to the following tools:\n\n' +
|
||||
'{tools}\n\n' +
|
||||
`The tool action_input should ALWAYS follow the tool JSON schema args.\n\n` +
|
||||
'Valid "action" values: "Final Answer" or {tool_names}\n\n' +
|
||||
'Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input strictly adhering to the tool JSON schema args).\n\n' +
|
||||
'Provide only ONE action per $JSON_BLOB, as shown:\n\n' +
|
||||
'```\n\n' +
|
||||
'{{\n\n' +
|
||||
' "action": $TOOL_NAME,\n\n' +
|
||||
' "action_input": $TOOL_INPUT\n\n' +
|
||||
'}}\n\n' +
|
||||
'```\n\n' +
|
||||
'Follow this format:\n\n' +
|
||||
'Question: input question to answer\n\n' +
|
||||
'Thought: consider previous and subsequent steps\n\n' +
|
||||
'Action:\n\n' +
|
||||
'```\n\n' +
|
||||
'$JSON_BLOB\n\n' +
|
||||
'```\n\n' +
|
||||
'Observation: action result\n\n' +
|
||||
'... (repeat Thought/Action/Observation N times)\n\n' +
|
||||
'Thought: I know what to respond\n\n' +
|
||||
'Action:\n\n' +
|
||||
'```\n\n' +
|
||||
'{{\n\n' +
|
||||
' "action": "Final Answer",\n\n' +
|
||||
// important, no new line here
|
||||
' "action_input": "Final response to human"' +
|
||||
'}}\n\n' +
|
||||
'Begin! Reminder to ALWAYS respond with a valid json blob of a single action with no additional output. When using tools, ALWAYS input the expected JSON schema args. Your answer will be parsed as JSON, so never use double quotes within the output and instead use backticks. Single quotes may be used, such as apostrophes. Response format is Action:```$JSON_BLOB```then Observation',
|
||||
],
|
||||
['placeholder', '{chat_history}'],
|
||||
['human', '{input}\n\n{agent_scratchpad}\n\n(reminder to respond in a JSON blob no matter what)'],
|
||||
]);
|
||||
The tool action_input should ALWAYS follow the tool JSON schema args.
|
||||
|
||||
Valid "action" values: "Final Answer" or {tool_names}
|
||||
|
||||
Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input strictly adhering to the tool JSON schema args).
|
||||
|
||||
Provide only ONE action per $JSON_BLOB, as shown:
|
||||
|
||||
\`\`\`
|
||||
|
||||
{{
|
||||
|
||||
"action": $TOOL_NAME,
|
||||
|
||||
"action_input": $TOOL_INPUT
|
||||
|
||||
}}
|
||||
|
||||
\`\`\`
|
||||
|
||||
Follow this format:
|
||||
|
||||
Question: input question to answer
|
||||
|
||||
Thought: consider previous and subsequent steps
|
||||
|
||||
Action:
|
||||
|
||||
\`\`\`
|
||||
|
||||
$JSON_BLOB
|
||||
|
||||
\`\`\`
|
||||
|
||||
Observation: action result
|
||||
|
||||
... (repeat Thought/Action/Observation N times)
|
||||
|
||||
Thought: I know what to respond
|
||||
|
||||
Action:
|
||||
|
||||
\`\`\`
|
||||
|
||||
{{
|
||||
|
||||
"action": "Final Answer",
|
||||
|
||||
"action_input": "Final response to human"}}
|
||||
|
||||
Begin! Reminder to ALWAYS respond with a valid json blob of a single action with no additional output. When using tools, ALWAYS input the expected JSON schema args. Your answer will be parsed as JSON, so never use double quotes within the output and instead use backticks. Single quotes may be used, such as apostrophes. Response format is Action:\`\`\`$JSON_BLOB\`\`\`then Observation`,
|
||||
};
|
||||
|
||||
export const openAIFunctionAgentPrompt = formatPrompt(systemPrompts.openai);
|
||||
|
||||
export const bedrockToolCallingAgentPrompt = formatPrompt(systemPrompts.bedrock);
|
||||
|
||||
export const geminiToolCallingAgentPrompt = formatPrompt(systemPrompts.gemini);
|
||||
|
||||
export const formatPromptStructured = (prompt: string, additionalPrompt?: string) =>
|
||||
ChatPromptTemplate.fromMessages([
|
||||
['system', additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt],
|
||||
['placeholder', '{chat_history}'],
|
||||
[
|
||||
'human',
|
||||
'{input}\n\n{agent_scratchpad}\n\n(reminder to respond in a JSON blob no matter what)',
|
||||
],
|
||||
]);
|
||||
|
||||
export const structuredChatAgentPrompt = formatPromptStructured(systemPrompts.structuredChat);
|
||||
|
|
|
@ -28,6 +28,9 @@ import { AwaitedProperties, PublicMethodsOf } from '@kbn/utility-types';
|
|||
import { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import { AssistantFeatureKey } from '@kbn/elastic-assistant-common/impl/capabilities';
|
||||
import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith';
|
||||
import { FindResponse } from '../ai_assistant_data_clients/find';
|
||||
import { EsPromptsSchema } from '../ai_assistant_data_clients/prompts/types';
|
||||
import { AIAssistantDataClient } from '../ai_assistant_data_clients';
|
||||
import { MINIMUM_AI_ASSISTANT_LICENSE } from '../../common/constants';
|
||||
import { ESQL_RESOURCE } from './knowledge_base/constants';
|
||||
import { buildResponse, getLlmType } from './utils';
|
||||
|
@ -214,6 +217,39 @@ export const appendMessageToConversation = async ({
|
|||
return updatedConversation;
|
||||
};
|
||||
|
||||
export interface GetSystemPromptFromUserConversationParams {
|
||||
conversationsDataClient: AIAssistantConversationsDataClient;
|
||||
conversationId: string;
|
||||
promptsDataClient: AIAssistantDataClient;
|
||||
}
|
||||
const extractPromptFromESResult = (result: FindResponse<EsPromptsSchema>): string | undefined => {
|
||||
if (result.total > 0 && result.data.hits.hits.length > 0) {
|
||||
return result.data.hits.hits[0]._source?.content;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getSystemPromptFromUserConversation = async ({
|
||||
conversationsDataClient,
|
||||
conversationId,
|
||||
promptsDataClient,
|
||||
}: GetSystemPromptFromUserConversationParams): Promise<string | undefined> => {
|
||||
const conversation = await conversationsDataClient.getConversation({ id: conversationId });
|
||||
if (!conversation) {
|
||||
return undefined;
|
||||
}
|
||||
const currentSystemPromptId = conversation.apiConfig?.defaultSystemPromptId;
|
||||
if (!currentSystemPromptId) {
|
||||
return undefined;
|
||||
}
|
||||
const result = await promptsDataClient.findDocuments<EsPromptsSchema>({
|
||||
perPage: 1,
|
||||
page: 1,
|
||||
filter: `_id: "${currentSystemPromptId}"`,
|
||||
});
|
||||
return extractPromptFromESResult(result);
|
||||
};
|
||||
|
||||
export interface AppendAssistantMessageToConversationParams {
|
||||
conversationsDataClient: AIAssistantConversationsDataClient;
|
||||
messageContent: string;
|
||||
|
@ -300,6 +336,7 @@ export interface LangChainExecuteParams {
|
|||
getElser: GetElser;
|
||||
response: KibanaResponseFactory;
|
||||
responseLanguage?: string;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
export const langChainExecute = async ({
|
||||
messages,
|
||||
|
@ -319,6 +356,7 @@ export const langChainExecute = async ({
|
|||
response,
|
||||
responseLanguage,
|
||||
isStream = true,
|
||||
systemPrompt,
|
||||
}: LangChainExecuteParams) => {
|
||||
// Fetch any tools registered by the request's originating plugin
|
||||
const pluginName = getPluginNameFromRequest({
|
||||
|
@ -389,6 +427,7 @@ export const langChainExecute = async ({
|
|||
replacements,
|
||||
responseLanguage,
|
||||
size: request.body.size,
|
||||
systemPrompt,
|
||||
traceOptions: {
|
||||
projectName: request.body.langSmithProject,
|
||||
tracers: getLangSmithTracer({
|
||||
|
|
|
@ -79,6 +79,9 @@ const mockContext = {
|
|||
appendConversationMessages:
|
||||
appendConversationMessages.mockResolvedValue(existingConversation),
|
||||
}),
|
||||
getAIAssistantPromptsDataClient: jest.fn().mockResolvedValue({
|
||||
findDocuments: jest.fn(),
|
||||
}),
|
||||
getAIAssistantAnonymizationFieldsDataClient: jest.fn().mockResolvedValue({
|
||||
findDocuments: jest.fn().mockResolvedValue(getFindAnonymizationFieldsResultWithSingleHit()),
|
||||
}),
|
||||
|
|
|
@ -21,7 +21,11 @@ import { INVOKE_ASSISTANT_ERROR_EVENT } from '../lib/telemetry/event_based_telem
|
|||
import { POST_ACTIONS_CONNECTOR_EXECUTE } from '../../common/constants';
|
||||
import { buildResponse } from '../lib/build_response';
|
||||
import { ElasticAssistantRequestHandlerContext, GetElser } from '../types';
|
||||
import { appendAssistantMessageToConversation, langChainExecute } from './helpers';
|
||||
import {
|
||||
appendAssistantMessageToConversation,
|
||||
getSystemPromptFromUserConversation,
|
||||
langChainExecute,
|
||||
} from './helpers';
|
||||
|
||||
export const postActionsConnectorExecuteRoute = (
|
||||
router: IRouter<ElasticAssistantRequestHandlerContext>,
|
||||
|
@ -89,6 +93,7 @@ export const postActionsConnectorExecuteRoute = (
|
|||
|
||||
const conversationsDataClient =
|
||||
await assistantContext.getAIAssistantConversationsDataClient();
|
||||
const promptsDataClient = await assistantContext.getAIAssistantPromptsDataClient();
|
||||
|
||||
onLlmResponse = async (
|
||||
content: string,
|
||||
|
@ -106,7 +111,14 @@ export const postActionsConnectorExecuteRoute = (
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
let systemPrompt;
|
||||
if (conversationsDataClient && promptsDataClient && conversationId) {
|
||||
systemPrompt = await getSystemPromptFromUserConversation({
|
||||
conversationsDataClient,
|
||||
conversationId,
|
||||
promptsDataClient,
|
||||
});
|
||||
}
|
||||
return await langChainExecute({
|
||||
abortSignal,
|
||||
isStream: request.body.subAction !== 'invokeAI',
|
||||
|
@ -124,6 +136,7 @@ export const postActionsConnectorExecuteRoute = (
|
|||
request,
|
||||
response,
|
||||
telemetry,
|
||||
systemPrompt,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
|
|
@ -59,7 +59,11 @@ export const findPromptsRoute = (router: ElasticAssistantPluginRouter, logger: L
|
|||
page: query.page,
|
||||
sortField: query.sort_field,
|
||||
sortOrder: query.sort_order,
|
||||
filter: query.filter ? decodeURIComponent(query.filter) : undefined,
|
||||
filter: query.filter
|
||||
? `${decodeURIComponent(
|
||||
query.filter
|
||||
)} and not (prompt_type: "system" and is_default: true)`
|
||||
: 'not (prompt_type: "system" and is_default: true)',
|
||||
fields: query.fields,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,41 +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 {
|
||||
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,
|
||||
SUPERHERO_SYSTEM_PROMPT_NAME,
|
||||
SUPERHERO_SYSTEM_PROMPT_NON_I18N,
|
||||
} from './translations';
|
||||
|
||||
/**
|
||||
* Base System Prompts for Security Solution.
|
||||
*/
|
||||
export const BASE_SECURITY_SYSTEM_PROMPTS: PromptResponse[] = [
|
||||
{
|
||||
id: 'default-system-prompt',
|
||||
content: DEFAULT_SYSTEM_PROMPT_NON_I18N,
|
||||
name: DEFAULT_SYSTEM_PROMPT_NAME,
|
||||
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: PromptTypeEnum.system,
|
||||
consumer: APP_UI_ID,
|
||||
isDefault: true,
|
||||
},
|
||||
];
|
|
@ -1,60 +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 YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT = i18n.translate(
|
||||
'xpack.securitySolution.assistant.content.prompts.system.youAreAHelpfulExpertAssistant',
|
||||
{
|
||||
defaultMessage:
|
||||
'You are a helpful, expert assistant who answers questions about Elastic Security.',
|
||||
}
|
||||
);
|
||||
|
||||
export const IF_YOU_DONT_KNOW_THE_ANSWER = i18n.translate(
|
||||
'xpack.securitySolution.assistant.content.prompts.system.ifYouDontKnowTheAnswer',
|
||||
{
|
||||
defaultMessage: 'Do not answer questions unrelated to Elastic Security.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SUPERHERO_PERSONALITY = i18n.translate(
|
||||
'xpack.securitySolution.assistant.content.prompts.system.superheroPersonality',
|
||||
{
|
||||
defaultMessage:
|
||||
'Provide the most detailed and relevant answer possible, as if you were relaying this information back to a cyber security expert.',
|
||||
}
|
||||
);
|
||||
|
||||
export const FORMAT_OUTPUT_CORRECTLY = i18n.translate(
|
||||
'xpack.securitySolution.assistant.content.prompts.system.outputFormatting',
|
||||
{
|
||||
defaultMessage:
|
||||
'If 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.',
|
||||
}
|
||||
);
|
||||
|
||||
export const DEFAULT_SYSTEM_PROMPT_NON_I18N = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}
|
||||
${FORMAT_OUTPUT_CORRECTLY}`;
|
||||
|
||||
export const DEFAULT_SYSTEM_PROMPT_NAME = i18n.translate(
|
||||
'xpack.securitySolution.assistant.content.prompts.system.defaultSystemPromptName',
|
||||
{
|
||||
defaultMessage: 'Default system prompt',
|
||||
}
|
||||
);
|
||||
|
||||
export const SUPERHERO_SYSTEM_PROMPT_NON_I18N = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}
|
||||
${SUPERHERO_PERSONALITY}
|
||||
${FORMAT_OUTPUT_CORRECTLY}`;
|
||||
|
||||
export const SUPERHERO_SYSTEM_PROMPT_NAME = i18n.translate(
|
||||
'xpack.securitySolution.assistant.content.prompts.system.superheroSystemPromptName',
|
||||
{
|
||||
defaultMessage: 'Enhanced system prompt',
|
||||
}
|
||||
);
|
|
@ -5,8 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiCommentProps } from '@elastic/eui';
|
||||
import type { Conversation, ClientMessage } from '@kbn/elastic-assistant';
|
||||
import type { ClientMessage, GetAssistantMessages } from '@kbn/elastic-assistant';
|
||||
import { EuiAvatar, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
|
@ -14,7 +13,7 @@ import { AssistantAvatar } from '@kbn/elastic-assistant';
|
|||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { replaceAnonymizedValuesWithOriginalValues } from '@kbn/elastic-assistant-common';
|
||||
import styled from '@emotion/styled';
|
||||
import type { UserAvatar } from '@kbn/elastic-assistant/impl/assistant_context';
|
||||
import type { EuiPanelProps } from '@elastic/eui/src/components/panel';
|
||||
import { StreamComment } from './stream';
|
||||
import { CommentActions } from '../comment_actions';
|
||||
import * as i18n from './translations';
|
||||
|
@ -52,7 +51,7 @@ const transformMessageWithReplacements = ({
|
|||
};
|
||||
};
|
||||
|
||||
export const getComments = ({
|
||||
export const getComments: GetAssistantMessages = ({
|
||||
abortStream,
|
||||
currentConversation,
|
||||
isFetchingResponse,
|
||||
|
@ -61,16 +60,8 @@ export const getComments = ({
|
|||
showAnonymizedValues,
|
||||
currentUserAvatar,
|
||||
setIsStreaming,
|
||||
}: {
|
||||
abortStream: () => void;
|
||||
currentConversation?: Conversation;
|
||||
isFetchingResponse: boolean;
|
||||
refetchCurrentConversation: ({ isStreamRefetch }: { isStreamRefetch?: boolean }) => void;
|
||||
regenerateMessage: (conversationId: string) => void;
|
||||
showAnonymizedValues: boolean;
|
||||
currentUserAvatar?: UserAvatar;
|
||||
setIsStreaming: (isStreaming: boolean) => void;
|
||||
}): EuiCommentProps[] => {
|
||||
systemPromptContent,
|
||||
}) => {
|
||||
if (!currentConversation) return [];
|
||||
|
||||
const regenerateMessageOfConversation = () => {
|
||||
|
@ -122,6 +113,32 @@ export const getComments = ({
|
|||
};
|
||||
|
||||
return [
|
||||
...(systemPromptContent && currentConversation.messages.length
|
||||
? [
|
||||
{
|
||||
username: i18n.SYSTEM,
|
||||
timelineAvatar: (
|
||||
<EuiAvatar name="machine" size="l" color="subdued" iconType={AssistantAvatar} />
|
||||
),
|
||||
timestamp:
|
||||
currentConversation.messages[0].timestamp.length === 0
|
||||
? new Date().toLocaleString()
|
||||
: new Date(currentConversation.messages[0].timestamp).toLocaleString(),
|
||||
children: (
|
||||
<StreamComment
|
||||
abortStream={abortStream}
|
||||
content={systemPromptContent}
|
||||
refetchCurrentConversation={refetchCurrentConversation}
|
||||
regenerateMessage={regenerateMessageOfConversation}
|
||||
setIsStreaming={setIsStreaming}
|
||||
transformMessage={() => ({ content: '' } as unknown as ContentMessage)}
|
||||
// we never need to append to a code block in the system comment, which is what this index is used for
|
||||
index={999}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...currentConversation.messages.map((message, index) => {
|
||||
const isLastComment = index === currentConversation.messages.length - 1;
|
||||
const isUser = message.role === 'user';
|
||||
|
@ -139,7 +156,7 @@ export const getComments = ({
|
|||
: new Date(message.timestamp).toLocaleString()
|
||||
),
|
||||
username: isUser ? i18n.YOU : i18n.ASSISTANT,
|
||||
eventColor: message.isError ? 'danger' : undefined,
|
||||
eventColor: message.isError ? ('danger' as EuiPanelProps['color']) : undefined,
|
||||
};
|
||||
|
||||
const isControlsEnabled = isLastComment && !isUser;
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SYSTEM = i18n.translate('xpack.securitySolution.assistant.getComments.system', {
|
||||
defaultMessage: 'System',
|
||||
});
|
||||
|
||||
export const ASSISTANT = i18n.translate('xpack.securitySolution.assistant.getComments.assistant', {
|
||||
defaultMessage: 'Assistant',
|
||||
});
|
||||
|
|
|
@ -5,28 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { httpServiceMock, type HttpSetupMock } from '@kbn/core-http-browser-mocks';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { AssistantProvider, createConversations } from './provider';
|
||||
import { createConversations } from './provider';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { useKibana as mockUseKibana } from '../common/lib/kibana/__mocks__';
|
||||
import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { TestProviders } from '../common/mock';
|
||||
import { useAssistantAvailability } from './use_assistant_availability';
|
||||
import {
|
||||
bulkUpdatePrompts,
|
||||
getPrompts,
|
||||
getUserConversations,
|
||||
} from '@kbn/elastic-assistant/impl/assistant/api';
|
||||
import { BASE_SECURITY_SYSTEM_PROMPTS } from './content/prompts/system';
|
||||
|
||||
const mockedUseKibana = mockUseKibana();
|
||||
jest.mock('./use_assistant_availability');
|
||||
jest.mock('../common/lib/kibana');
|
||||
|
||||
jest.mock('@kbn/elastic-assistant/impl/assistant/api');
|
||||
jest.mock('../common/hooks/use_license', () => ({
|
||||
|
@ -224,85 +210,3 @@ describe('createConversations', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe('AssistantProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
...mockedUseKibana,
|
||||
services: {
|
||||
...mockedUseKibana.services,
|
||||
},
|
||||
});
|
||||
jest.mocked(useAssistantAvailability).mockReturnValue({
|
||||
hasAssistantPrivilege: true,
|
||||
hasConnectorsAllPrivilege: true,
|
||||
hasConnectorsReadPrivilege: true,
|
||||
hasUpdateAIAssistantAnonymization: true,
|
||||
isAssistantEnabled: true,
|
||||
});
|
||||
|
||||
(getUserConversations as jest.Mock).mockResolvedValue({
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
total: 5,
|
||||
data: [],
|
||||
});
|
||||
(getPrompts as jest.Mock).mockResolvedValue({
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
total: 0,
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
it('should not render the assistant when no prompts have been returned', async () => {
|
||||
const { queryByTestId } = render(
|
||||
<AssistantProvider>
|
||||
<span data-test-subj="ourAssistant" />
|
||||
</AssistantProvider>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(queryByTestId('ourAssistant')).toBeNull();
|
||||
});
|
||||
it('should render the assistant when prompts are returned', async () => {
|
||||
(getPrompts as jest.Mock).mockResolvedValue({
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
total: 2,
|
||||
data: BASE_SECURITY_SYSTEM_PROMPTS,
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<AssistantProvider>
|
||||
<span data-test-subj="ourAssistant" />
|
||||
</AssistantProvider>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('ourAssistant')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
it('should render the assistant once prompts have been created', async () => {
|
||||
(bulkUpdatePrompts as jest.Mock).mockResolvedValue({
|
||||
success: true,
|
||||
attributes: {
|
||||
results: {
|
||||
created: BASE_SECURITY_SYSTEM_PROMPTS,
|
||||
},
|
||||
},
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<AssistantProvider>
|
||||
<span data-test-subj="ourAssistant" />
|
||||
</AssistantProvider>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('ourAssistant')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { parse } from '@kbn/datemath';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -30,7 +30,6 @@ import { useAssistantTelemetry } from './use_assistant_telemetry';
|
|||
import { getComments } from './get_comments';
|
||||
import { LOCAL_STORAGE_KEY, augmentMessageCodeBlocks } from './helpers';
|
||||
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';
|
||||
|
@ -117,7 +116,7 @@ export const createConversations = async (
|
|||
};
|
||||
|
||||
export const createBasePrompts = async (notifications: NotificationsStart, http: HttpSetup) => {
|
||||
const promptsToCreate = [...BASE_SECURITY_QUICK_PROMPTS, ...BASE_SECURITY_SYSTEM_PROMPTS];
|
||||
const promptsToCreate = [...BASE_SECURITY_QUICK_PROMPTS];
|
||||
|
||||
// post bulk create
|
||||
const bulkResult = await bulkUpdatePrompts(
|
||||
|
@ -176,8 +175,6 @@ export const AssistantProvider: FC<PropsWithChildren<unknown>> = ({ children })
|
|||
storage,
|
||||
]);
|
||||
|
||||
const [basePromptsLoaded, setBasePromptsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const createSecurityPrompts = once(async () => {
|
||||
if (
|
||||
|
@ -197,8 +194,6 @@ export const AssistantProvider: FC<PropsWithChildren<unknown>> = ({ children })
|
|||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
setBasePromptsLoaded(true);
|
||||
});
|
||||
createSecurityPrompts();
|
||||
}, [
|
||||
|
@ -212,9 +207,6 @@ export const AssistantProvider: FC<PropsWithChildren<unknown>> = ({ children })
|
|||
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)
|
||||
// Because our conversations need an assigned system prompt at create time,
|
||||
// we want to make sure the prompts are there before creating the first conversation
|
||||
// however if there is an error fetching the prompts, we don't want to block the app
|
||||
|
||||
return (
|
||||
<ElasticAssistantProvider
|
||||
|
@ -234,7 +226,7 @@ export const AssistantProvider: FC<PropsWithChildren<unknown>> = ({ children })
|
|||
toasts={toasts}
|
||||
currentAppId={currentAppId ?? 'securitySolutionUI'}
|
||||
>
|
||||
{basePromptsLoaded ? children : null}
|
||||
{children}
|
||||
</ElasticAssistantProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15026,7 +15026,6 @@
|
|||
"xpack.elasticAssistant.assistant.connectors.connectorSelector.newConnectorOptions": "Ajouter un nouveau connecteur...",
|
||||
"xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorPlaceholder": "Sélectionner un connecteur",
|
||||
"xpack.elasticAssistant.assistant.connectors.preconfiguredTitle": "Préconfiguré",
|
||||
"xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptName": "Invite de système par défaut",
|
||||
"xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "Connecteur",
|
||||
"xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "Contexte fourni dans le cadre de chaque conversation.",
|
||||
"xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "Invite système",
|
||||
|
@ -35521,12 +35520,6 @@
|
|||
"xpack.securitySolution.assistant.commentActions.viewAPMTraceLabel": "Voir la trace APM pour ce message",
|
||||
"xpack.securitySolution.assistant.content.promptContexts.indexTitle": "index",
|
||||
"xpack.securitySolution.assistant.content.promptContexts.viewTitle": "vue",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.defaultSystemPromptName": "Invite de système par défaut",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.ifYouDontKnowTheAnswer": "Ne répondez pas aux questions qui ne sont pas liées à Elastic Security.",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.outputFormatting": "Si vous répondez à une question liée à KQL, à EQL, ou à ES|QL, la réponse doit être immédiatement utilisable dans une chronologie d'Elastic Security ; veuillez toujours formater correctement la sortie avec des accents graves. Toute réponse à une requête DSL doit aussi être utilisable dans une chronologie de sécurité. Cela signifie que vous ne devez inclure que la portion \"filtre\" de la requête.",
|
||||
"xpack.securitySolution.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.securitySolution.assistant.content.prompts.system.superheroSystemPromptName": "Invite système améliorée",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "Vous êtes un assistant expert et serviable qui répond à des questions au sujet d’Elastic Security.",
|
||||
"xpack.securitySolution.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 d’inclure les scores de risque de l’utilisateur et de l’hôte du contexte. Votre réponse doit inclure des étapes qui pointent vers les fonctionnalités spécifiques d’Elastic Security, y compris les actions de réponse du terminal, l’intégration OSQuery Manager d’Elastic Agent (avec des exemples de requêtes OSQuery), des analyses de timeline et d’entités, ainsi qu’un lien pour toute la documentation Elastic Security pertinente.",
|
||||
"xpack.securitySolution.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.securitySolution.assistant.conversationMigrationStatus.title": "Les conversations de stockage local ont été persistées avec succès.",
|
||||
|
|
|
@ -15013,7 +15013,6 @@
|
|||
"xpack.elasticAssistant.assistant.connectors.connectorSelector.newConnectorOptions": "新しいコネクターを追加...",
|
||||
"xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorPlaceholder": "コネクターを選択",
|
||||
"xpack.elasticAssistant.assistant.connectors.preconfiguredTitle": "構成済み",
|
||||
"xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptName": "デフォルトシステムプロンプト",
|
||||
"xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "コネクター",
|
||||
"xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "すべての会話の一部として提供されたコンテキスト。",
|
||||
"xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "システムプロンプト",
|
||||
|
@ -35506,12 +35505,6 @@
|
|||
"xpack.securitySolution.assistant.commentActions.viewAPMTraceLabel": "このメッセージのAPMトレースを表示",
|
||||
"xpack.securitySolution.assistant.content.promptContexts.indexTitle": "インデックス",
|
||||
"xpack.securitySolution.assistant.content.promptContexts.viewTitle": "表示",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.defaultSystemPromptName": "デフォルトシステムプロンプト",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.ifYouDontKnowTheAnswer": "Elasticセキュリティに関連していない質問には回答しないでください。",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.outputFormatting": "KQL、EQL、ES|QLに関連する質問に回答した場合、Elastic Securityのタイムライン内ですぐに使用できるようにする必要があります。出力は常にバックティックで正しい形式にしてください。クエリDSLで提供されるすべての回答は、セキュリティタイムラインでも使用可能でなければなりません。つまり、クエリの\"フィルター\"部分のみを含める必要があります。",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.superheroPersonality": "サイバーセキュリティの専門家に情報を伝えるつもりで、できるだけ詳細で関連性のある回答を入力してください。",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.superheroSystemPromptName": "拡張システムプロンプト",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "あなたはElasticセキュリティに関する質問に答える、親切で専門的なアシスタントです。",
|
||||
"xpack.securitySolution.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "説明、推奨されるアクション、箇条書きのトリアージステップを追加します。提供された MITRE ATT&CKデータを使用して、MITREからのコンテキストや推奨事項を追加し、MITREのWebサイトの関連ページにハイパーリンクを貼ります。コンテキストのユーザーとホストのリスクスコアデータを必ず含めてください。回答には、エンドポイント対応アクション、ElasticエージェントOSQueryマネージャー統合(osqueryクエリーの例を付けて)、タイムライン、エンティティ分析など、Elasticセキュリティ固有の機能を指す手順を含め、関連するElasticセキュリティのドキュメントすべてにリンクしてください。",
|
||||
"xpack.securitySolution.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "上記のコンテキストからイベントを評価し、Elasticセキュリティのケース用に、出力をマークダウン構文で正しく書式設定してください。",
|
||||
"xpack.securitySolution.assistant.conversationMigrationStatus.title": "ローカルストレージ会話は正常に永続しました。",
|
||||
|
|
|
@ -15038,7 +15038,6 @@
|
|||
"xpack.elasticAssistant.assistant.connectors.connectorSelector.newConnectorOptions": "添加新连接器……",
|
||||
"xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorPlaceholder": "选择连接器",
|
||||
"xpack.elasticAssistant.assistant.connectors.preconfiguredTitle": "预配置",
|
||||
"xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptName": "默认系统提示",
|
||||
"xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "连接器",
|
||||
"xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "已作为每个对话的一部分提供上下文。",
|
||||
"xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "系统提示",
|
||||
|
@ -35547,12 +35546,6 @@
|
|||
"xpack.securitySolution.assistant.commentActions.viewAPMTraceLabel": "查看此消息的 APM 跟踪",
|
||||
"xpack.securitySolution.assistant.content.promptContexts.indexTitle": "索引",
|
||||
"xpack.securitySolution.assistant.content.promptContexts.viewTitle": "视图",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.defaultSystemPromptName": "默认系统提示",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.ifYouDontKnowTheAnswer": "不回答与 Elastic Security 无关的问题。",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.outputFormatting": "如果您回答与 KQL、EQL 或 ES|QL 相关的问题,它应在 Elastic Security 时间线中立即可用;请始终用反勾号对输出进行正确格式化。为查询 DSL 提供的任何答案也应在安全时间线中可用。这意味着您只应包括查询的“筛选”部分。",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.superheroPersonality": "提供可能的最详细、最相关的答案,就好像您正将此信息转发给网络安全专家一样。",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.superheroSystemPromptName": "已增强系统提示",
|
||||
"xpack.securitySolution.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "您是一位可帮助回答 Elastic Security 相关问题的专家助手。",
|
||||
"xpack.securitySolution.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "添加描述、建议操作和带项目符号的分类步骤。使用提供的 MITRE ATT&CK 数据以从 MITRE 添加更多上下文和建议,以及指向 MITRE 网站上的相关页面的超链接。确保包括上下文中的用户和主机风险分数数据。您的响应应包含指向 Elastic Security 特定功能的步骤,包括终端响应操作、Elastic 代理 OSQuery 管理器集成(带示例 osquery 查询)、时间线和实体分析,以及所有相关 Elastic Security 文档的链接。",
|
||||
"xpack.securitySolution.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "评估来自上述上下文的事件,并以用于我的 Elastic Security 案例的 Markdown 语法对您的输出进行全面格式化。",
|
||||
"xpack.securitySolution.assistant.conversationMigrationStatus.title": "已成功保持本地存储对话。",
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
selectRule,
|
||||
assertErrorToastShown,
|
||||
updateConversationTitle,
|
||||
assertSystemPrompt,
|
||||
} from '../../tasks/assistant';
|
||||
import { deleteConversations } from '../../tasks/api_calls/assistant';
|
||||
import {
|
||||
|
@ -71,7 +70,6 @@ describe('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () =>
|
|||
openAssistant();
|
||||
assertNewConversation(false, 'Welcome');
|
||||
assertConnectorSelected(azureConnectorAPIPayload.name);
|
||||
assertSystemPrompt('Default system prompt');
|
||||
cy.get(USER_PROMPT).should('not.have.text');
|
||||
});
|
||||
it('When invoked from rules page', () => {
|
||||
|
@ -81,7 +79,6 @@ describe('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () =>
|
|||
openAssistant('rule');
|
||||
assertNewConversation(false, 'Detection Rules');
|
||||
assertConnectorSelected(azureConnectorAPIPayload.name);
|
||||
assertSystemPrompt('Default system prompt');
|
||||
cy.get(USER_PROMPT).should('have.text', EXPLAIN_THEN_SUMMARIZE_RULE_DETAILS);
|
||||
cy.get(PROMPT_CONTEXT_BUTTON(0)).should('have.text', RULE_MANAGEMENT_CONTEXT_DESCRIPTION);
|
||||
});
|
||||
|
@ -94,7 +91,6 @@ describe('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () =>
|
|||
openAssistant('alert');
|
||||
assertNewConversation(false, 'Alert summary');
|
||||
assertConnectorSelected(azureConnectorAPIPayload.name);
|
||||
assertSystemPrompt('Default system prompt');
|
||||
cy.get(USER_PROMPT).should(
|
||||
'have.text',
|
||||
EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N
|
||||
|
@ -135,19 +131,19 @@ describe('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () =>
|
|||
assertNewConversation(false, 'Welcome');
|
||||
assertConnectorSelected(azureConnectorAPIPayload.name);
|
||||
typeAndSendMessage('hello');
|
||||
assertMessageSent('hello', true);
|
||||
assertMessageSent('hello');
|
||||
assertErrorResponse();
|
||||
selectConversation('Alert summary');
|
||||
selectConnector(bedrockConnectorAPIPayload.name);
|
||||
typeAndSendMessage('goodbye');
|
||||
assertMessageSent('goodbye', true);
|
||||
assertMessageSent('goodbye');
|
||||
assertErrorResponse();
|
||||
selectConversation('Welcome');
|
||||
assertConnectorSelected(azureConnectorAPIPayload.name);
|
||||
assertMessageSent('hello', true);
|
||||
assertMessageSent('hello');
|
||||
selectConversation('Alert summary');
|
||||
assertConnectorSelected(bedrockConnectorAPIPayload.name);
|
||||
assertMessageSent('goodbye', true);
|
||||
assertMessageSent('goodbye');
|
||||
});
|
||||
// This test is flakey due to the issue linked below and will be skipped until it is fixed
|
||||
it.skip('Only allows one conversation called "New chat" at a time', () => {
|
||||
|
@ -159,7 +155,7 @@ describe('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () =>
|
|||
typeAndSendMessage('hello');
|
||||
// TODO fix bug with new chat and error message
|
||||
// https://github.com/elastic/kibana/issues/191025
|
||||
// assertMessageSent('hello', true);
|
||||
// assertMessageSent('hello');
|
||||
assertErrorResponse();
|
||||
selectConversation('Welcome');
|
||||
createNewChat();
|
||||
|
|
|
@ -5,14 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SUPERHERO_SYSTEM_PROMPT_NON_I18N } from '@kbn/security-solution-plugin/public/assistant/content/prompts/system/translations';
|
||||
import { EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N } from '@kbn/security-solution-plugin/public/assistant/content/prompts/user/translations';
|
||||
import { PromptCreateProps } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
|
||||
import { QUICK_PROMPT_BADGE, USER_PROMPT } from '../../screens/ai_assistant';
|
||||
import { createRule } from '../../tasks/api_calls/rules';
|
||||
import {
|
||||
assertEmptySystemPrompt,
|
||||
assertErrorResponse,
|
||||
assertMessageSent,
|
||||
assertSystemPrompt,
|
||||
assertSystemPromptSelected,
|
||||
assertSystemPromptSent,
|
||||
clearSystemPrompt,
|
||||
createQuickPrompt,
|
||||
createSystemPrompt,
|
||||
|
@ -23,7 +25,11 @@ import {
|
|||
sendQuickPrompt,
|
||||
typeAndSendMessage,
|
||||
} from '../../tasks/assistant';
|
||||
import { deleteConversations, deletePrompts } from '../../tasks/api_calls/assistant';
|
||||
import {
|
||||
deleteConversations,
|
||||
deletePrompts,
|
||||
waitForCreatePrompts,
|
||||
} from '../../tasks/api_calls/assistant';
|
||||
import { createAzureConnector } from '../../tasks/api_calls/connectors';
|
||||
import { deleteConnectors } from '../../tasks/api_calls/common';
|
||||
import { login } from '../../tasks/login';
|
||||
|
@ -33,10 +39,23 @@ import { ALERTS_URL } from '../../urls/navigation';
|
|||
import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
|
||||
import { expandFirstAlert } from '../../tasks/alerts';
|
||||
|
||||
const promptType: PromptCreateProps['promptType'] = 'system';
|
||||
const testPrompt = {
|
||||
title: 'Cool prompt',
|
||||
prompt: 'This is a super cool prompt.',
|
||||
name: 'Cool prompt',
|
||||
content: 'This is a super cool prompt.',
|
||||
};
|
||||
|
||||
const customPrompt1 = {
|
||||
name: 'Custom system prompt',
|
||||
content: 'This is a custom system prompt.',
|
||||
promptType,
|
||||
};
|
||||
const customPrompt2 = {
|
||||
name: 'Enhanced system prompt',
|
||||
content: 'This is an enhanced system prompt.',
|
||||
promptType,
|
||||
};
|
||||
|
||||
describe('AI Assistant Prompts', { tags: ['@ess', '@serverless'] }, () => {
|
||||
beforeEach(() => {
|
||||
deleteConnectors();
|
||||
|
@ -47,88 +66,103 @@ describe('AI Assistant Prompts', { tags: ['@ess', '@serverless'] }, () => {
|
|||
});
|
||||
|
||||
describe('System Prompts', () => {
|
||||
it('Deselecting default system prompt prevents prompt from being sent. When conversation is then cleared, the prompt is reset.', () => {
|
||||
beforeEach(() => {
|
||||
waitForCreatePrompts([customPrompt2, customPrompt1]);
|
||||
});
|
||||
it('No prompt is selected by default, custom prompts can be selected and deselected', () => {
|
||||
visitGetStartedPage();
|
||||
openAssistant();
|
||||
assertEmptySystemPrompt();
|
||||
selectSystemPrompt(customPrompt2.name);
|
||||
selectSystemPrompt(customPrompt1.name);
|
||||
clearSystemPrompt();
|
||||
});
|
||||
it('Deselecting a system prompt prevents prompt from being sent. When conversation is then cleared, the prompt remains cleared.', () => {
|
||||
visitGetStartedPage();
|
||||
openAssistant();
|
||||
selectSystemPrompt(customPrompt2.name);
|
||||
clearSystemPrompt();
|
||||
typeAndSendMessage('hello');
|
||||
assertMessageSent('hello');
|
||||
// ensure response before clearing convo
|
||||
assertErrorResponse();
|
||||
resetConversation();
|
||||
assertEmptySystemPrompt();
|
||||
typeAndSendMessage('hello');
|
||||
assertMessageSent('hello', true);
|
||||
assertMessageSent('hello');
|
||||
});
|
||||
|
||||
it('Last selected system prompt persists in conversation', () => {
|
||||
visitGetStartedPage();
|
||||
openAssistant();
|
||||
selectSystemPrompt('Enhanced system prompt');
|
||||
selectSystemPrompt(customPrompt2.name);
|
||||
typeAndSendMessage('hello');
|
||||
assertMessageSent('hello', true, SUPERHERO_SYSTEM_PROMPT_NON_I18N);
|
||||
assertSystemPromptSent(customPrompt2.content);
|
||||
assertMessageSent('hello', true);
|
||||
resetConversation();
|
||||
assertSystemPrompt('Enhanced system prompt');
|
||||
selectConversation('Alert summary');
|
||||
assertSystemPrompt('Default system prompt');
|
||||
assertSystemPromptSelected(customPrompt2.name);
|
||||
selectConversation('Timeline');
|
||||
assertEmptySystemPrompt();
|
||||
selectConversation('Welcome');
|
||||
assertSystemPrompt('Enhanced system prompt');
|
||||
assertSystemPromptSelected(customPrompt2.name);
|
||||
});
|
||||
|
||||
it('Add prompt from system prompt selector without setting a default conversation', () => {
|
||||
visitGetStartedPage();
|
||||
openAssistant();
|
||||
createSystemPrompt(testPrompt.title, testPrompt.prompt);
|
||||
createSystemPrompt(testPrompt.name, testPrompt.content);
|
||||
// we did not set a default conversation, so the prompt should not be set
|
||||
assertSystemPrompt('Default system prompt');
|
||||
selectSystemPrompt(testPrompt.title);
|
||||
assertEmptySystemPrompt();
|
||||
selectSystemPrompt(testPrompt.name);
|
||||
typeAndSendMessage('hello');
|
||||
assertMessageSent('hello', true, testPrompt.prompt);
|
||||
assertSystemPromptSent(testPrompt.content);
|
||||
assertMessageSent('hello', true);
|
||||
});
|
||||
|
||||
it('Add prompt from system prompt selector and set multiple conversations (including current) as default conversation', () => {
|
||||
visitGetStartedPage();
|
||||
openAssistant();
|
||||
createSystemPrompt(testPrompt.title, testPrompt.prompt, [
|
||||
'Welcome',
|
||||
'Alert summary',
|
||||
'Data Quality Dashboard',
|
||||
]);
|
||||
assertSystemPrompt(testPrompt.title);
|
||||
createSystemPrompt(testPrompt.name, testPrompt.content, ['Welcome', 'Timeline']);
|
||||
assertSystemPromptSelected(testPrompt.name);
|
||||
typeAndSendMessage('hello');
|
||||
assertMessageSent('hello', true, testPrompt.prompt);
|
||||
|
||||
assertSystemPromptSent(testPrompt.content);
|
||||
assertMessageSent('hello', true);
|
||||
// ensure response before changing convo
|
||||
assertErrorResponse();
|
||||
selectConversation('Alert summary');
|
||||
assertSystemPrompt(testPrompt.title);
|
||||
selectConversation('Timeline');
|
||||
assertSystemPromptSelected(testPrompt.name);
|
||||
typeAndSendMessage('hello');
|
||||
assertMessageSent('hello', true, testPrompt.prompt);
|
||||
|
||||
assertSystemPromptSent(testPrompt.content);
|
||||
assertMessageSent('hello', true);
|
||||
});
|
||||
});
|
||||
describe('User Prompts', () => {
|
||||
describe('Quick Prompts', () => {
|
||||
it('Add a quick prompt and send it in the conversation', () => {
|
||||
visitGetStartedPage();
|
||||
openAssistant();
|
||||
createQuickPrompt(testPrompt.title, testPrompt.prompt);
|
||||
sendQuickPrompt(testPrompt.title);
|
||||
assertMessageSent(testPrompt.prompt, true);
|
||||
createQuickPrompt(testPrompt.name, testPrompt.content);
|
||||
sendQuickPrompt(testPrompt.name);
|
||||
assertMessageSent(testPrompt.content);
|
||||
});
|
||||
it('Add a quick prompt with context and it is only available in the selected context', () => {
|
||||
visitGetStartedPage();
|
||||
openAssistant();
|
||||
createQuickPrompt(testPrompt.title, testPrompt.prompt, ['Alert (from view)']);
|
||||
cy.get(QUICK_PROMPT_BADGE(testPrompt.title)).should('not.exist');
|
||||
createQuickPrompt(testPrompt.name, testPrompt.content, ['Alert (from view)']);
|
||||
cy.get(QUICK_PROMPT_BADGE(testPrompt.name)).should('not.exist');
|
||||
createRule(getNewRule());
|
||||
visit(ALERTS_URL);
|
||||
waitForAlertsToPopulate();
|
||||
expandFirstAlert();
|
||||
openAssistant('alert');
|
||||
cy.get(QUICK_PROMPT_BADGE(testPrompt.title)).should('be.visible');
|
||||
cy.get(QUICK_PROMPT_BADGE(testPrompt.name)).should('be.visible');
|
||||
cy.get(USER_PROMPT).should(
|
||||
'have.text',
|
||||
EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N
|
||||
);
|
||||
cy.get(QUICK_PROMPT_BADGE(testPrompt.title)).click();
|
||||
cy.get(USER_PROMPT).should('have.text', testPrompt.prompt);
|
||||
cy.get(QUICK_PROMPT_BADGE(testPrompt.name)).click();
|
||||
cy.get(USER_PROMPT).should('have.text', testPrompt.content);
|
||||
});
|
||||
// TODO delete quick prompt
|
||||
// I struggled to do this since the element is hidden with css and I cannot get it to show
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
ConversationResponse,
|
||||
Provider,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { PromptCreateProps } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
|
||||
|
||||
export const getMockConversation = (body?: Partial<ConversationCreateProps>) => ({
|
||||
title: 'Test Conversation',
|
||||
|
@ -38,3 +39,11 @@ export const getMockConversationResponse = (
|
|||
namespace: 'default',
|
||||
...getMockConversation(body),
|
||||
});
|
||||
|
||||
export const getMockCreatePrompt = (body?: Partial<PromptCreateProps>): PromptCreateProps => ({
|
||||
name: 'Mock Prompt Name',
|
||||
promptType: 'quick',
|
||||
content: 'Mock Prompt Content',
|
||||
consumer: 'securitySolutionUI',
|
||||
...body,
|
||||
});
|
||||
|
|
|
@ -43,7 +43,7 @@ export const QUICK_PROMPT_BODY_INPUT = '[data-test-subj="quick-prompt-prompt"]';
|
|||
export const SEND_TO_TIMELINE_BUTTON = '[data-test-subj="sendToTimelineEmptyButton"]';
|
||||
export const SHOW_ANONYMIZED_BUTTON = '[data-test-subj="showAnonymizedValues"]';
|
||||
export const SUBMIT_CHAT = '[data-test-subj="submit-chat"]';
|
||||
export const SYSTEM_PROMPT = '[data-test-subj="systemPromptText"]';
|
||||
export const SYSTEM_PROMPT = '[data-test-subj="promptSuperSelect"]';
|
||||
export const SYSTEM_PROMPT_BODY_INPUT = '[data-test-subj="systemPromptModalPromptText"]';
|
||||
export const SYSTEM_PROMPT_TITLE_INPUT =
|
||||
'[data-test-subj="systemPromptSelector"] [data-test-subj="comboBoxSearchInput"]';
|
||||
|
|
|
@ -6,8 +6,13 @@
|
|||
*/
|
||||
|
||||
import { ConversationCreateProps, ConversationResponse } from '@kbn/elastic-assistant-common';
|
||||
import {
|
||||
PerformPromptsBulkActionRequestBody,
|
||||
PerformPromptsBulkActionResponse,
|
||||
PromptCreateProps,
|
||||
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
|
||||
import { deleteAllDocuments } from './elasticsearch';
|
||||
import { getMockConversation } from '../../objects/assistant';
|
||||
import { getMockConversation, getMockCreatePrompt } from '../../objects/assistant';
|
||||
import { getSpaceUrl } from '../space';
|
||||
import { rootRequest, waitForRootRequest } from './common';
|
||||
|
||||
|
@ -36,3 +41,21 @@ export const deletePrompts = () => {
|
|||
cy.log('Delete all prompts');
|
||||
deleteAllDocuments(`.kibana-elastic-ai-assistant-prompts-*`);
|
||||
};
|
||||
|
||||
const bulkPrompts = (
|
||||
body: PerformPromptsBulkActionRequestBody
|
||||
): Cypress.Chainable<Cypress.Response<PerformPromptsBulkActionResponse>> =>
|
||||
cy.currentSpace().then((spaceId) =>
|
||||
rootRequest<PerformPromptsBulkActionResponse>({
|
||||
method: 'POST',
|
||||
url: spaceId
|
||||
? getSpaceUrl(spaceId, `api/security_ai_assistant/prompts/_bulk_action`)
|
||||
: `api/security_ai_assistant/prompts/_bulk_action`,
|
||||
body,
|
||||
})
|
||||
);
|
||||
export const waitForCreatePrompts = (prompts: Array<Partial<PromptCreateProps>>) => {
|
||||
return waitForRootRequest<PerformPromptsBulkActionResponse>(
|
||||
bulkPrompts({ create: prompts.map((prompt) => getMockCreatePrompt(prompt)) })
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DEFAULT_SYSTEM_PROMPT_NON_I18N } from '@kbn/security-solution-plugin/public/assistant/content/prompts/system/translations';
|
||||
import { TIMELINE_CHECKBOX } from '../screens/timelines';
|
||||
import { CLOSE_FLYOUT } from '../screens/alerts';
|
||||
import {
|
||||
|
@ -110,6 +109,7 @@ export const sendQueryToTimeline = () => {
|
|||
|
||||
export const clearSystemPrompt = () => {
|
||||
cy.get(CLEAR_SYSTEM_PROMPT).click();
|
||||
assertEmptySystemPrompt();
|
||||
};
|
||||
|
||||
export const sendQuickPrompt = (prompt: string) => {
|
||||
|
@ -120,7 +120,7 @@ export const sendQuickPrompt = (prompt: string) => {
|
|||
export const selectSystemPrompt = (systemPrompt: string) => {
|
||||
cy.get(SYSTEM_PROMPT).click();
|
||||
cy.get(SYSTEM_PROMPT_SELECT(systemPrompt)).click();
|
||||
assertSystemPrompt(systemPrompt);
|
||||
assertSystemPromptSelected(systemPrompt);
|
||||
};
|
||||
|
||||
export const createSystemPrompt = (
|
||||
|
@ -174,23 +174,30 @@ export const assertNewConversation = (isWelcome: boolean, title: string) => {
|
|||
cy.get(CONVERSATION_TITLE + ' h2').should('have.text', title);
|
||||
};
|
||||
|
||||
export const assertMessageSent = (message: string, hasDefaultPrompt = false, prompt?: string) => {
|
||||
cy.get(CONVERSATION_MESSAGE)
|
||||
.first()
|
||||
.should(
|
||||
'contain',
|
||||
hasDefaultPrompt ? `${prompt ?? DEFAULT_SYSTEM_PROMPT_NON_I18N}\n${message}` : message
|
||||
);
|
||||
export const assertSystemPromptSent = (message: string) => {
|
||||
cy.get(CONVERSATION_MESSAGE).eq(0).should('contain', message);
|
||||
};
|
||||
|
||||
export const assertMessageSent = (message: string, prompt: boolean = false) => {
|
||||
if (prompt) {
|
||||
return cy.get(CONVERSATION_MESSAGE).eq(1).should('contain', message);
|
||||
}
|
||||
cy.get(CONVERSATION_MESSAGE).eq(0).should('contain', message);
|
||||
};
|
||||
|
||||
export const assertErrorResponse = () => {
|
||||
cy.get(CONVERSATION_MESSAGE_ERROR).should('be.visible');
|
||||
};
|
||||
|
||||
export const assertSystemPrompt = (systemPrompt: string) => {
|
||||
export const assertSystemPromptSelected = (systemPrompt: string) => {
|
||||
cy.get(SYSTEM_PROMPT).should('have.text', systemPrompt);
|
||||
};
|
||||
|
||||
export const assertEmptySystemPrompt = () => {
|
||||
const EMPTY = 'Select a system prompt';
|
||||
cy.get(SYSTEM_PROMPT).should('have.text', EMPTY);
|
||||
};
|
||||
|
||||
export const assertConnectorSelected = (connectorName: string) => {
|
||||
cy.get(CONNECTOR_SELECTOR).should('have.text', connectorName);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue