mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Security Assistant] Fixes relationship between system prompts & conversations (#161039)
## Summary This PR handles bugs - elastic/security-team#6977 - https://github.com/elastic/security-team/issues/6978 - elastic/security-team#6979. Currently, below operations between System Prompts and Conversarions do not work. 1. When a prompt is set as default for all conversation, it should be automatically selected for any new conversation user creates. 2. When a new prompt is creates and set as default for all conversation, it should be automatically selected for any new conversation user creates. 3. When a prompt is edited such that, it is default for only certain conversation, it should be automatically selected for that conversation. 4. When a prompt is edited such that conversations are removed to have that default prompt, it should be automatically removed from conversation default system prompt list. In addition to above scenarios, this PR also handles one more bug. Consider below interface of Conversation which has a property `apiConfig.defaultSystemPrompt` is of type Prompt. It has been changed from `defaultSystemPrompt?: Prompt` to `defaultSystemPrompt?: string` where it will store `promptId` instead of complete prompt. The current model was posing a problem where, if a prompt was updated, all its copies in `Conversation` were needed to be updated leading to inconsistencies. This is now resolved. ```typescript export interface Conversation { apiConfig: { connectorId?: string; defaultSystemPrompt?: Prompt; provider?: OpenAiProviderType; }; id: string; messages: Message[]; replacements?: Record<string, string>; theme?: ConversationTheme; isDefault?: boolean; } ```
This commit is contained in:
parent
f82588ba5e
commit
75bd6dd854
13 changed files with 500 additions and 58 deletions
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const TEST_IDS = {
|
||||
SYSTEM_PROMPT_SELECTOR: 'systemPromptSelector',
|
||||
CONVERSATIONS_MULTISELECTOR: 'conversationMultiSelector',
|
||||
ADD_SYSTEM_PROMPT: 'addSystemPrompt',
|
||||
PROMPT_SUPERSELECT: 'promptSuperSelect',
|
||||
CONVERSATIONS_MULTISELECTOR_OPTION: (id: string) => `conversationMultiSelectorOption-${id}`,
|
||||
SYSTEM_PROMPT_MODAL: {
|
||||
ID: 'systemPromptModal',
|
||||
PROMPT_TEXT: 'systemPromptModalPromptText',
|
||||
TOGGLE_ALL_DEFAULT_CONVERSATIONS: 'systemPromptModalToggleDefaultConversations',
|
||||
SAVE: 'systemPromptModalSave',
|
||||
CANCEL: 'systemPromptModalCancel',
|
||||
},
|
||||
};
|
|
@ -105,7 +105,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
apiConfig: {
|
||||
connectorId: defaultConnectorId,
|
||||
provider: defaultProvider,
|
||||
defaultSystemPrompt,
|
||||
defaultSystemPromptId: defaultSystemPrompt?.id,
|
||||
},
|
||||
};
|
||||
setConversation({ conversation: newConversation });
|
||||
|
|
|
@ -29,10 +29,11 @@ export interface ConversationSettingsPopoverProps {
|
|||
conversation: Conversation;
|
||||
http: HttpSetup;
|
||||
isDisabled?: boolean;
|
||||
allSystemPrompts: Prompt[];
|
||||
}
|
||||
|
||||
export const ConversationSettingsPopover: React.FC<ConversationSettingsPopoverProps> = React.memo(
|
||||
({ actionTypeRegistry, conversation, http, isDisabled = false }) => {
|
||||
({ actionTypeRegistry, conversation, http, isDisabled = false, allSystemPrompts }) => {
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
// So we can hide the settings popover when the connector modal is displayed
|
||||
const popoverPanelRef = useRef<HTMLElement | null>(null);
|
||||
|
@ -41,10 +42,13 @@ export const ConversationSettingsPopover: React.FC<ConversationSettingsPopoverPr
|
|||
return conversation.apiConfig?.provider;
|
||||
}, [conversation.apiConfig]);
|
||||
|
||||
const selectedPrompt: Prompt | undefined = useMemo(
|
||||
() => conversation?.apiConfig.defaultSystemPrompt,
|
||||
[conversation]
|
||||
);
|
||||
const selectedPrompt: Prompt | undefined = useMemo(() => {
|
||||
const convoDefaultSystemPromptId = conversation?.apiConfig.defaultSystemPromptId;
|
||||
if (convoDefaultSystemPromptId && allSystemPrompts) {
|
||||
return allSystemPrompts.find((prompt) => prompt.id === convoDefaultSystemPromptId);
|
||||
}
|
||||
return allSystemPrompts.find((prompt) => prompt.isNewConversationDefault);
|
||||
}, [conversation, allSystemPrompts]);
|
||||
|
||||
const closeSettingsHandler = useCallback(() => {
|
||||
setIsSettingsOpen(false);
|
||||
|
|
|
@ -79,6 +79,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
http,
|
||||
promptContexts,
|
||||
title,
|
||||
allSystemPrompts,
|
||||
} = useAssistantContext();
|
||||
const [selectedPromptContexts, setSelectedPromptContexts] = useState<
|
||||
Record<string, SelectedPromptContext>
|
||||
|
@ -170,6 +171,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
|
||||
// For auto-focusing prompt within timeline
|
||||
const promptTextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldRefocusPrompt && promptTextAreaRef.current) {
|
||||
promptTextAreaRef?.current.focus();
|
||||
|
@ -187,6 +189,15 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
}, 0);
|
||||
}, [currentConversation.messages.length, selectedPromptContextsCount]);
|
||||
////
|
||||
//
|
||||
|
||||
const selectedSystemPrompt = useMemo(() => {
|
||||
if (currentConversation.apiConfig.defaultSystemPromptId) {
|
||||
return allSystemPrompts.find(
|
||||
(prompt) => prompt.id === currentConversation.apiConfig.defaultSystemPromptId
|
||||
);
|
||||
}
|
||||
}, [allSystemPrompts, currentConversation.apiConfig.defaultSystemPromptId]);
|
||||
|
||||
// Handles sending latest user prompt to API
|
||||
const handleSendMessage = useCallback(
|
||||
|
@ -203,7 +214,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
onNewReplacements,
|
||||
promptText,
|
||||
selectedPromptContexts,
|
||||
selectedSystemPrompt: currentConversation.apiConfig.defaultSystemPrompt,
|
||||
selectedSystemPrompt,
|
||||
});
|
||||
|
||||
const updatedMessages = appendMessage({
|
||||
|
@ -224,6 +235,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
appendMessage({ conversationId: selectedConversationId, message: responseMessage });
|
||||
},
|
||||
[
|
||||
selectedSystemPrompt,
|
||||
appendMessage,
|
||||
appendReplacements,
|
||||
currentConversation.apiConfig,
|
||||
|
@ -561,6 +573,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
conversation={currentConversation}
|
||||
isDisabled={isWelcomeSetup}
|
||||
http={http}
|
||||
allSystemPrompts={allSystemPrompts}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -6,23 +6,43 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { mockSystemPrompt } from '../../../mock/system_prompt';
|
||||
import { SystemPrompt } from '.';
|
||||
import { BASE_CONVERSATIONS, Conversation } from '../../../..';
|
||||
import { DEFAULT_CONVERSATION_TITLE } from '../../use_conversation/translations';
|
||||
import { Prompt } from '../../types';
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { TEST_IDS } from '../../constants';
|
||||
import { useAssistantContext } from '../../../assistant_context';
|
||||
|
||||
const BASE_CONVERSATION: Conversation = {
|
||||
...BASE_CONVERSATIONS[DEFAULT_CONVERSATION_TITLE],
|
||||
apiConfig: {
|
||||
defaultSystemPromptId: mockSystemPrompt.id,
|
||||
},
|
||||
};
|
||||
|
||||
const mockConversations = {
|
||||
[DEFAULT_CONVERSATION_TITLE]: BASE_CONVERSATION,
|
||||
};
|
||||
|
||||
const mockSystemPrompts: Prompt[] = [mockSystemPrompt];
|
||||
|
||||
const mockUseAssistantContext = {
|
||||
conversations: mockConversations,
|
||||
setConversations: jest.fn(),
|
||||
setAllSystemPrompts: jest.fn(),
|
||||
allSystemPrompts: mockSystemPrompts,
|
||||
};
|
||||
|
||||
jest.mock('../../../assistant_context', () => {
|
||||
const original = jest.requireActual('../../../assistant_context');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useAssistantContext: () => mockUseAssistantContext,
|
||||
useAssistantContext: jest.fn().mockImplementation(() => mockUseAssistantContext),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -38,15 +58,18 @@ jest.mock('../../use_conversation', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const BASE_CONVERSATION: Conversation = {
|
||||
...BASE_CONVERSATIONS[DEFAULT_CONVERSATION_TITLE],
|
||||
apiConfig: {
|
||||
defaultSystemPrompt: mockSystemPrompt,
|
||||
},
|
||||
};
|
||||
|
||||
describe('SystemPrompt', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
jest.mock('../../../assistant_context', () => {
|
||||
const original = jest.requireActual('../../../assistant_context');
|
||||
return {
|
||||
...original,
|
||||
useAssistantContext: jest.fn().mockImplementation(() => mockUseAssistantContext),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe('when conversation is undefined', () => {
|
||||
const conversation = undefined;
|
||||
|
@ -94,8 +117,303 @@ describe('SystemPrompt', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('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} />
|
||||
</TestProviders>
|
||||
);
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
userEvent.click(screen.getByTestId(TEST_IDS.ADD_SYSTEM_PROMPT));
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeVisible();
|
||||
|
||||
userEvent.type(
|
||||
within(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_SELECTOR)).getByTestId('comboBoxInput'),
|
||||
`${customPromptName}[Enter]`
|
||||
);
|
||||
|
||||
userEvent.type(
|
||||
screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT),
|
||||
customPromptText
|
||||
);
|
||||
|
||||
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} />
|
||||
</TestProviders>
|
||||
);
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
userEvent.click(screen.getByTestId(TEST_IDS.ADD_SYSTEM_PROMPT));
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeVisible();
|
||||
|
||||
userEvent.type(
|
||||
within(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_SELECTOR)).getByTestId('comboBoxInput'),
|
||||
`${customPromptName}[Enter]`
|
||||
);
|
||||
|
||||
userEvent.type(
|
||||
screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT),
|
||||
customPromptText
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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} />
|
||||
</TestProviders>
|
||||
);
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
userEvent.click(screen.getByTestId(TEST_IDS.ADD_SYSTEM_PROMPT));
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeVisible();
|
||||
|
||||
userEvent.type(
|
||||
within(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_SELECTOR)).getByTestId('comboBoxInput'),
|
||||
`${customPromptName}[Enter]`
|
||||
);
|
||||
|
||||
userEvent.type(
|
||||
screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT),
|
||||
customPromptText
|
||||
);
|
||||
|
||||
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
|
||||
userEvent.click(
|
||||
screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR_OPTION(DEFAULT_CONVERSATION_TITLE))
|
||||
);
|
||||
|
||||
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} />
|
||||
</TestProviders>
|
||||
);
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
userEvent.click(screen.getByTestId(TEST_IDS.ADD_SYSTEM_PROMPT));
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeVisible();
|
||||
|
||||
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();
|
||||
|
||||
userEvent.click(
|
||||
within(screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR)).getByTestId(
|
||||
'comboBoxClearButton'
|
||||
)
|
||||
);
|
||||
|
||||
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',
|
||||
apiConfig: {
|
||||
defaultSystemPromptId: undefined,
|
||||
},
|
||||
messages: [],
|
||||
};
|
||||
const localMockConversations: Record<string, Conversation> = {
|
||||
[DEFAULT_CONVERSATION_TITLE]: BASE_CONVERSATION,
|
||||
[secondMockConversation.id]: 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} />
|
||||
</TestProviders>
|
||||
);
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
userEvent.click(screen.getByTestId(TEST_IDS.ADD_SYSTEM_PROMPT));
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.ID)).toBeVisible();
|
||||
|
||||
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
|
||||
userEvent.click(
|
||||
within(screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR)).getByTestId(
|
||||
'comboBoxClearButton'
|
||||
)
|
||||
);
|
||||
|
||||
// add `second` conversation
|
||||
userEvent.type(
|
||||
within(screen.getByTestId(TEST_IDS.CONVERSATIONS_MULTISELECTOR)).getByTestId(
|
||||
'comboBoxInput'
|
||||
),
|
||||
'second[Enter]'
|
||||
);
|
||||
|
||||
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.id]: {
|
||||
...secondMockConversation,
|
||||
apiConfig: {
|
||||
defaultSystemPromptId: mockSystemPrompt.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the system prompt select when the edit button is clicked', () => {
|
||||
render(<SystemPrompt conversation={BASE_CONVERSATION} />);
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt conversation={BASE_CONVERSATION} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
|
||||
|
@ -103,7 +421,10 @@ describe('SystemPrompt', () => {
|
|||
});
|
||||
|
||||
it('clears the selected system prompt when the clear button is clicked', () => {
|
||||
const apiConfig = { apiConfig: { defaultSystemPrompt: undefined }, conversationId: 'Default' };
|
||||
const apiConfig = {
|
||||
apiConfig: { defaultSystemPromptId: undefined },
|
||||
conversationId: 'Default',
|
||||
};
|
||||
render(<SystemPrompt conversation={BASE_CONVERSATION} />);
|
||||
|
||||
userEvent.click(screen.getByTestId('clear'));
|
||||
|
@ -112,7 +433,11 @@ describe('SystemPrompt', () => {
|
|||
});
|
||||
|
||||
it('shows the system prompt select when system prompt text is clicked', () => {
|
||||
render(<SystemPrompt conversation={BASE_CONVERSATION} />);
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt conversation={BASE_CONVERSATION} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('systemPromptText'));
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { useAssistantContext } from '../../../assistant_context';
|
||||
import { Conversation } from '../../../..';
|
||||
import * as i18n from './translations';
|
||||
import type { Prompt } from '../../types';
|
||||
import { SelectSystemPrompt } from './select_system_prompt';
|
||||
import { useConversation } from '../../use_conversation';
|
||||
|
||||
|
@ -20,12 +20,14 @@ interface Props {
|
|||
}
|
||||
|
||||
const SystemPromptComponent: React.FC<Props> = ({ conversation }) => {
|
||||
const { allSystemPrompts } = useAssistantContext();
|
||||
const { setApiConfig } = useConversation();
|
||||
|
||||
const selectedPrompt: Prompt | undefined = useMemo(
|
||||
() => conversation?.apiConfig.defaultSystemPrompt,
|
||||
[conversation]
|
||||
const selectedPrompt = useMemo(
|
||||
() => allSystemPrompts?.find((p) => p.id === conversation?.apiConfig.defaultSystemPromptId),
|
||||
[allSystemPrompts, conversation]
|
||||
);
|
||||
|
||||
const [isEditing, setIsEditing] = React.useState<boolean>(false);
|
||||
|
||||
const handleClearSystemPrompt = useCallback(() => {
|
||||
|
@ -34,7 +36,7 @@ const SystemPromptComponent: React.FC<Props> = ({ conversation }) => {
|
|||
conversationId: conversation.id,
|
||||
apiConfig: {
|
||||
...conversation.apiConfig,
|
||||
defaultSystemPrompt: undefined,
|
||||
defaultSystemPromptId: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { render } from '@testing-library/react';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { Props, SelectSystemPrompt } from '.';
|
||||
import { TEST_IDS } from '../../../constants';
|
||||
|
||||
const props: Props = {
|
||||
conversation: undefined,
|
||||
|
@ -51,13 +52,13 @@ describe('SelectSystemPrompt', () => {
|
|||
it('renders the prompt super select when isEditing is true', () => {
|
||||
const { getByTestId } = render(<SelectSystemPrompt {...props} isEditing={true} />);
|
||||
|
||||
expect(getByTestId('promptSuperSelect')).toBeInTheDocument();
|
||||
expect(getByTestId(TEST_IDS.PROMPT_SUPERSELECT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render the prompt super select when isEditing is false', () => {
|
||||
const { queryByTestId } = render(<SelectSystemPrompt {...props} isEditing={false} />);
|
||||
|
||||
expect(queryByTestId('promptSuperSelect')).not.toBeInTheDocument();
|
||||
expect(queryByTestId(TEST_IDS.PROMPT_SUPERSELECT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render the clear system prompt button when isEditing is true', () => {
|
||||
|
|
|
@ -25,6 +25,7 @@ import type { Prompt } from '../../../types';
|
|||
import { useAssistantContext } from '../../../../assistant_context';
|
||||
import { useConversation } from '../../../use_conversation';
|
||||
import { SystemPromptModal } from '../system_prompt_modal/system_prompt_modal';
|
||||
import { TEST_IDS } from '../../../constants';
|
||||
|
||||
export interface Props {
|
||||
conversation: Conversation | undefined;
|
||||
|
@ -53,7 +54,9 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
setIsEditing,
|
||||
showTitles = false,
|
||||
}) => {
|
||||
const { allSystemPrompts, setAllSystemPrompts } = useAssistantContext();
|
||||
const { allSystemPrompts, setAllSystemPrompts, conversations, setConversations } =
|
||||
useAssistantContext();
|
||||
|
||||
const { setApiConfig } = useConversation();
|
||||
|
||||
const [isOpenLocal, setIsOpenLocal] = useState<boolean>(isOpen);
|
||||
|
@ -67,7 +70,7 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
conversationId: conversation.id,
|
||||
apiConfig: {
|
||||
...conversation.apiConfig,
|
||||
defaultSystemPrompt: prompt,
|
||||
defaultSystemPromptId: prompt?.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -84,7 +87,7 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
dropdownDisplay: (
|
||||
<EuiFlexGroup gutterSize="none" key={ADD_NEW_SYSTEM_PROMPT}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiButtonEmpty iconType="plus" size="xs">
|
||||
<EuiButtonEmpty iconType="plus" size="xs" data-test-subj="addSystemPrompt">
|
||||
{i18n.ADD_NEW_SYSTEM_PROMPT}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
@ -99,12 +102,25 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
|
||||
// Callback for modal onSave, saves to local storage on change
|
||||
const onSystemPromptsChange = useCallback(
|
||||
(newSystemPrompts: Prompt[]) => {
|
||||
(newSystemPrompts: Prompt[], updatedConversations?: Conversation[]) => {
|
||||
setAllSystemPrompts(newSystemPrompts);
|
||||
setIsSystemPromptModalVisible(false);
|
||||
onSystemPromptModalVisibilityChange?.(false);
|
||||
|
||||
if (updatedConversations && updatedConversations.length > 0) {
|
||||
const updatedConversationObject = updatedConversations?.reduce<
|
||||
Record<string, Conversation>
|
||||
>((updatedObj, currentConv) => {
|
||||
updatedObj[currentConv.id] = currentConv;
|
||||
return updatedObj;
|
||||
}, {});
|
||||
setConversations({
|
||||
...conversations,
|
||||
...updatedConversationObject,
|
||||
});
|
||||
}
|
||||
},
|
||||
[onSystemPromptModalVisibilityChange, setAllSystemPrompts]
|
||||
[onSystemPromptModalVisibilityChange, setAllSystemPrompts, conversations, setConversations]
|
||||
);
|
||||
|
||||
// SuperSelect State/Actions
|
||||
|
@ -112,6 +128,7 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
() => getOptions({ prompts: allSystemPrompts, showTitles }),
|
||||
[allSystemPrompts, showTitles]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(selectedSystemPromptId) => {
|
||||
if (selectedSystemPromptId === ADD_NEW_SYSTEM_PROMPT) {
|
||||
|
@ -153,7 +170,7 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
// Limits popover z-index to prevent it from getting too high and covering tooltips.
|
||||
// If the z-index is not defined, when a popover is opened, it sets the target z-index + 2000
|
||||
popoverProps={{ zIndex: euiThemeVars.euiZLevel8 }}
|
||||
data-test-subj="promptSuperSelect"
|
||||
data-test-subj={TEST_IDS.PROMPT_SUPERSELECT}
|
||||
fullWidth={fullWidth}
|
||||
hasDividers
|
||||
itemLayoutAlign="top"
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { TEST_IDS } from '../../../../constants';
|
||||
import { Conversation } from '../../../../../..';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface Props {
|
||||
onConversationSelectionChange: (conversations: Conversation[]) => void;
|
||||
onConversationSelectionChange: (currentPromptConversations: Conversation[]) => void;
|
||||
conversations: Conversation[];
|
||||
selectedConversations?: Conversation[];
|
||||
}
|
||||
|
@ -27,6 +28,7 @@ export const ConversationMultiSelector: React.FC<Props> = React.memo(
|
|||
() =>
|
||||
conversations.map((conversation) => ({
|
||||
label: conversation.id,
|
||||
'data-test-subj': TEST_IDS.CONVERSATIONS_MULTISELECTOR_OPTION(conversation.id),
|
||||
})),
|
||||
[conversations]
|
||||
);
|
||||
|
@ -62,6 +64,7 @@ export const ConversationMultiSelector: React.FC<Props> = React.memo(
|
|||
|
||||
return (
|
||||
<EuiComboBox
|
||||
data-test-subj={TEST_IDS.CONVERSATIONS_MULTISELECTOR}
|
||||
aria-label={i18n.SYSTEM_PROMPT_DEFAULT_CONVERSATIONS}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
SYSTEM_PROMPT_SELECTOR_CLASSNAME,
|
||||
SystemPromptSelector,
|
||||
} from './system_prompt_selector/system_prompt_selector';
|
||||
import { TEST_IDS } from '../../../constants';
|
||||
|
||||
const StyledEuiModal = styled(EuiModal)`
|
||||
min-width: 400px;
|
||||
|
@ -45,7 +46,7 @@ interface Props {
|
|||
onClose: (
|
||||
event?: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLButtonElement>
|
||||
) => void;
|
||||
onSystemPromptsChange: (systemPrompts: Prompt[]) => void;
|
||||
onSystemPromptsChange: (systemPrompts: Prompt[], newConversation?: Conversation[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,23 +67,58 @@ export const SystemPromptModal: React.FC<Props> = React.memo(
|
|||
}, []);
|
||||
// Conversations this system prompt should be a default for
|
||||
const [selectedConversations, setSelectedConversations] = useState<Conversation[]>([]);
|
||||
const onConversationSelectionChange = useCallback((newConversations: Conversation[]) => {
|
||||
setSelectedConversations(newConversations);
|
||||
}, []);
|
||||
|
||||
const onConversationSelectionChange = useCallback(
|
||||
(currentPromptConversations: Conversation[]) => {
|
||||
setSelectedConversations(currentPromptConversations);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/*
|
||||
* updatedConversationWithPrompts calculates the present of prompt for
|
||||
* each conversation. Based on the values of selected conversation, it goes
|
||||
* through each conversation adds/removed the selected prompt on each conversation.
|
||||
*
|
||||
* */
|
||||
const getUpdatedConversationWithPrompts = useCallback(() => {
|
||||
const currentPromptConversationIds = selectedConversations.map((convo) => convo.id);
|
||||
|
||||
const allConversations = Object.values(conversations).map((convo) => ({
|
||||
...convo,
|
||||
apiConfig: {
|
||||
...convo.apiConfig,
|
||||
defaultSystemPromptId: currentPromptConversationIds.includes(convo.id)
|
||||
? selectedSystemPrompt?.id
|
||||
: convo.apiConfig.defaultSystemPromptId === selectedSystemPrompt?.id
|
||||
? // remove the the default System Prompt if it is assigned to a conversation
|
||||
// but that conversation is not in the currentPromptConversationList
|
||||
// This means conversation was removed in the current transaction
|
||||
undefined
|
||||
: // leave it as it is .. if that conversation was neither added nor removed.
|
||||
convo.apiConfig.defaultSystemPromptId,
|
||||
},
|
||||
}));
|
||||
|
||||
return allConversations;
|
||||
}, [selectedSystemPrompt, conversations, selectedConversations]);
|
||||
// Whether this system prompt should be the default for new conversations
|
||||
const [isNewConversationDefault, setIsNewConversationDefault] = useState(false);
|
||||
const handleNewConversationDefaultChange = useCallback(
|
||||
(e) => {
|
||||
setIsNewConversationDefault(e.target.checked);
|
||||
const isChecked = e.target.checked;
|
||||
setIsNewConversationDefault(isChecked);
|
||||
if (selectedSystemPrompt != null) {
|
||||
setUpdatedSystemPrompts((prev) => {
|
||||
return prev.map((pp) => ({
|
||||
...pp,
|
||||
isNewConversationDefault: selectedSystemPrompt.id === pp.id && e.target.checked,
|
||||
}));
|
||||
return prev.map((pp) => {
|
||||
return {
|
||||
...pp,
|
||||
isNewConversationDefault: selectedSystemPrompt.id === pp.id && isChecked,
|
||||
};
|
||||
});
|
||||
});
|
||||
setSelectedSystemPrompt((prev) =>
|
||||
prev != null ? { ...prev, isNewConversationDefault: e.target.checked } : prev
|
||||
prev != null ? { ...prev, isNewConversationDefault: isChecked } : prev
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -106,13 +142,13 @@ export const SystemPromptModal: React.FC<Props> = React.memo(
|
|||
setPrompt(newPrompt?.content ?? '');
|
||||
setIsNewConversationDefault(newPrompt?.isNewConversationDefault ?? false);
|
||||
// Find all conversations that have this system prompt as a default
|
||||
setSelectedConversations(
|
||||
const currenlySelectedConversations =
|
||||
newPrompt != null
|
||||
? Object.values(conversations).filter(
|
||||
(conversation) => conversation?.apiConfig.defaultSystemPrompt?.id === newPrompt?.id
|
||||
(conversation) => conversation?.apiConfig.defaultSystemPromptId === newPrompt?.id
|
||||
)
|
||||
: []
|
||||
);
|
||||
: [];
|
||||
setSelectedConversations(currenlySelectedConversations);
|
||||
},
|
||||
[conversations]
|
||||
);
|
||||
|
@ -122,8 +158,9 @@ export const SystemPromptModal: React.FC<Props> = React.memo(
|
|||
}, []);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSystemPromptsChange(updatedSystemPrompts);
|
||||
}, [onSystemPromptsChange, updatedSystemPrompts]);
|
||||
const updatedConversations = getUpdatedConversationWithPrompts();
|
||||
onSystemPromptsChange(updatedSystemPrompts, updatedConversations);
|
||||
}, [onSystemPromptsChange, updatedSystemPrompts, getUpdatedConversationWithPrompts]);
|
||||
|
||||
// useEffects
|
||||
// Update system prompts on any field change since editing is in place
|
||||
|
@ -157,7 +194,11 @@ export const SystemPromptModal: React.FC<Props> = React.memo(
|
|||
}, [prompt, selectedSystemPrompt]);
|
||||
|
||||
return (
|
||||
<StyledEuiModal onClose={onClose} initialFocus={`.${SYSTEM_PROMPT_SELECTOR_CLASSNAME}`}>
|
||||
<StyledEuiModal
|
||||
onClose={onClose}
|
||||
initialFocus={`.${SYSTEM_PROMPT_SELECTOR_CLASSNAME}`}
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.ID}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.ADD_SYSTEM_PROMPT_MODAL_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
@ -173,7 +214,11 @@ export const SystemPromptModal: React.FC<Props> = React.memo(
|
|||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow label={i18n.SYSTEM_PROMPT_PROMPT}>
|
||||
<EuiTextArea onChange={handlePromptTextChange} value={prompt} />
|
||||
<EuiTextArea
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT}
|
||||
onChange={handlePromptTextChange}
|
||||
value={prompt}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
|
@ -189,6 +234,7 @@ export const SystemPromptModal: React.FC<Props> = React.memo(
|
|||
<EuiFormRow>
|
||||
<EuiCheckbox
|
||||
id={'defaultNewConversation'}
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.TOGGLE_ALL_DEFAULT_CONVERSATIONS}
|
||||
label={
|
||||
<EuiFlexGroup alignItems="center" gutterSize={'xs'}>
|
||||
<EuiFlexItem>{i18n.SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION}</EuiFlexItem>
|
||||
|
@ -205,9 +251,16 @@ export const SystemPromptModal: React.FC<Props> = React.memo(
|
|||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onClose}>{i18n.CANCEL}</EuiButtonEmpty>
|
||||
<EuiButtonEmpty onClick={onClose} data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.CANCEL}>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton type="submit" onClick={handleSave} fill>
|
||||
<EuiButton
|
||||
type="submit"
|
||||
onClick={handleSave}
|
||||
fill
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.SAVE}
|
||||
>
|
||||
{i18n.SAVE}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { TEST_IDS } from '../../../../constants';
|
||||
import { Prompt } from '../../../../../..';
|
||||
import * as i18n from './translations';
|
||||
import { SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION } from '../translations';
|
||||
|
@ -54,6 +55,7 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
|
|||
isNewConversationDefault: sp.isNewConversationDefault ?? false,
|
||||
},
|
||||
label: sp.name,
|
||||
'data-test-subj': `${TEST_IDS.SYSTEM_PROMPT_SELECTOR}-${sp.id}`,
|
||||
}))
|
||||
);
|
||||
const selectedOptions = useMemo<SystemPromptSelectorOption[]>(() => {
|
||||
|
@ -146,6 +148,7 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
|
|||
component={'span'}
|
||||
gutterSize={'none'}
|
||||
justifyContent="spaceBetween"
|
||||
data-test-subj="systemPromptOptionSelector"
|
||||
>
|
||||
<EuiFlexItem grow={1} component={'span'}>
|
||||
<EuiFlexGroup alignItems="center" component={'span'} gutterSize={'s'}>
|
||||
|
@ -201,6 +204,7 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
|
|||
return (
|
||||
<EuiComboBox
|
||||
className={SYSTEM_PROMPT_SELECTOR_CLASSNAME}
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_SELECTOR}
|
||||
aria-label={i18n.SYSTEM_PROMPT_SELECTOR}
|
||||
placeholder={i18n.SYSTEM_PROMPT_SELECTOR}
|
||||
customOptionText={`${i18n.CUSTOM_OPTION_TEXT} {searchValue}`}
|
||||
|
|
|
@ -47,7 +47,7 @@ export const SETTINGS_CONNECTOR_TITLE = i18n.translate(
|
|||
export const SETTINGS_PROMPT_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.promptTitle',
|
||||
{
|
||||
defaultMessage: 'System Prompt',
|
||||
defaultMessage: 'System prompt',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
|
||||
import { Prompt } from '../assistant/types';
|
||||
|
||||
export type ConversationRole = 'system' | 'user' | 'assistant';
|
||||
|
||||
|
@ -46,7 +45,7 @@ export interface ConversationTheme {
|
|||
export interface Conversation {
|
||||
apiConfig: {
|
||||
connectorId?: string;
|
||||
defaultSystemPrompt?: Prompt;
|
||||
defaultSystemPromptId?: string;
|
||||
provider?: OpenAiProviderType;
|
||||
};
|
||||
id: string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue