Skip to content

Commit

Permalink
[Security Solution] Security Assistant: Prompt Editor / Sends System …
Browse files Browse the repository at this point in the history
…Prompts / Sends Prompt Context elastic#7

- adds the query editor
- passes context and system prompt to the api
  • Loading branch information
andrew-goldstein authored May 12, 2023
1 parent a3ddec0 commit 521f26a
Show file tree
Hide file tree
Showing 20 changed files with 848 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
import { getSummaryRows } from './get_alert_summary_rows';
import { EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE } from '../../../security_assistant/content/prompts/user/translations';
import { useSecurityAssistantContext } from '../../../security_assistant/security_assistant_context';
import { SummaryView } from './summary_view';
import * as i18n from './translations';
import { getAutoRunPromptFromEventDetailsItem } from '../../../security_assistant/prompt/helpers';
import { getPromptContextFromEventDetailsItem } from '../../../security_assistant/prompt_context/helpers';
import { getUniquePromptContextId } from '../../../security_assistant/security_assistant_context/helpers';

Expand All @@ -39,29 +39,19 @@ const AlertSummaryViewComponent: React.FC<{
async () => getPromptContextFromEventDetailsItem(data),
[data]
);
const getAutoRunPrompt = useCallback(
async () => getAutoRunPromptFromEventDetailsItem(data),
[data]
);

useEffect(() => {
registerPromptContext({
category: 'alert',
description: i18n.ALERT_SUMMARY_VIEW_CONTEXT_DESCRIPTION,
id: promptContextId,
getPromptContext,
getAutoRunPrompt,
suggestedUserPrompt: EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE,
tooltip: i18n.ALERT_SUMMARY_VIEW_CONTEXT_TOOLTIP,
});

return () => unRegisterPromptContext(promptContextId);
}, [
getAutoRunPrompt,
getPromptContext,
promptContextId,
registerPromptContext,
unRegisterPromptContext,
]);
}, [getPromptContext, promptContextId, registerPromptContext, unRegisterPromptContext]);

return (
<SummaryView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@ const StyledEuiModal = styled(EuiModal)`
height: 100%;
`;

interface AssistantOverlayProps {}

/**
* Modal container for Security Assistant conversations, receiving the page contents as context, plus whatever
* component currently has focus and any specific context it may provide through the SAssInterface.
*/
export const AssistantOverlay: React.FC<AssistantOverlayProps> = React.memo(({}) => {
export const AssistantOverlay: React.FC = React.memo(() => {
const [isModalVisible, setIsModalVisible] = useState(false);
const [input, setInput] = useState<string | undefined>();
const [conversationId, setConversationId] = useState<string | undefined>('default');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.securityAssistant.content.prompts.system.youAreAHelpfulExpertAssistant',
{
defaultMessage:
'You are a helpful, expert assistant who answers questions about Elastic Security.',
}
);

export const USE_THE_FOLLOWING_CONTEXT_TO_ANSWER = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.system.useTheFollowingContextToAnswer',
{
defaultMessage: 'Use the following context to answer questions:',
}
);

export const IF_YOU_DONT_KNOW_THE_ANSWER = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.system.ifYouDontKnowTheAnswer',
{
defaultMessage: "If you don't know the answer, don't try to make one up.",
}
);

export const SUPERHERO_PERSONALITY = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.system.superheroPersonality',
{
defaultMessage: 'You have the personality of a mutant superhero who says "bub" a lot.',
}
);

export const DEFAULT_SYSTEM_PROMPT = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.system.defaultSystemPrompt',
{
defaultMessage: `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}
${USE_THE_FOLLOWING_CONTEXT_TO_ANSWER}`,
}
);

export const DEFAULT_SYSTEM_PROMPT_NAME = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.system.defaultSystemPromptName',
{
defaultMessage: 'default system prompt',
}
);

export const SUPERHERO_SYSTEM_PROMPT = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.system.superheroSystemPrompt',
{
defaultMessage: `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}
${SUPERHERO_PERSONALITY}
${USE_THE_FOLLOWING_CONTEXT_TO_ANSWER}`,
}
);

export const SUPERHERO_SYSTEM_PROMPT_NAME = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.system.superheroSystemPromptName',
{
defaultMessage: 'superhero system prompt',
}
);

export const SYSTEM_PROMPT_CONTEXT = (context: string) =>
i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.system.systemPromptContext',
{
values: { context },
defaultMessage: `CONTEXT:\n"""\n${context}\n"""`,
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 EXPLAIN_THE_MEANING_FROM_CONTEXT_ABOVE = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.user.explainTheMeaningFromContextAbove',
{
defaultMessage: 'Explain the meaning from the context above',
}
);

export const THEN_SUMMARIZE_SUGGESTED_KQL_AND_EQL_QUERIES = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries',
{
defaultMessage: 'then summarize a list of suggested Elasticsearch KQL and EQL queries',
}
);

export const FINALLY_SUGGEST_INVESTIGATION_GUIDE_AND_FORMAT_AS_MARKDOWN = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown',
{
defaultMessage: 'Finally, suggest an investigation guide, and format it as markdown',
}
);

export const EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE = i18n.translate(
'xpack.securitySolution.securityAssistant.content.prompts.user.defaultAlertOrEventUserPrompt',
{
defaultMessage: `${EXPLAIN_THE_MEANING_FROM_CONTEXT_ABOVE}, ${THEN_SUMMARIZE_SUGGESTED_KQL_AND_EQL_QUERIES}.
${FINALLY_SUGGEST_INVESTIGATION_GUIDE_AND_FORMAT_AS_MARKDOWN}.`,
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,67 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';

import { sortBy } from 'lodash/fp';
import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';

import type { PromptContext } from '../prompt_context/types';

interface Props {
isNewChat: boolean;
onSendContextMessage: (content: string) => void;
promptContexts: Record<string, PromptContext>;
selectedPromptContextIds: string[];
setSelectedPromptContextIds: React.Dispatch<React.SetStateAction<string[]>>;
}

const ContextPillsComponent: React.FC<Props> = ({ promptContexts }) => {
const ContextPillsComponent: React.FC<Props> = ({
isNewChat,
onSendContextMessage,
promptContexts,
selectedPromptContextIds,
setSelectedPromptContextIds,
}) => {
const sortedPromptContexts = useMemo(
() => sortBy('description', Object.values(promptContexts)),
[promptContexts]
);

const selectPromptContext = useCallback(
(id: string) => {
if (!selectedPromptContextIds.includes(id)) {
setSelectedPromptContextIds((prev) => [...prev, id]);
}
},
[selectedPromptContextIds, setSelectedPromptContextIds]
);

const selectAndSendContextMessage = useCallback(
async (id: string) => {
const promptContext: PromptContext | undefined = promptContexts[id];
if (promptContext != null) {
if (!selectedPromptContextIds.includes(id)) {
setSelectedPromptContextIds((prev) => [...prev, id]);
}

const content = await promptContext.getPromptContext();
onSendContextMessage(content);
}
},
[onSendContextMessage, promptContexts, selectedPromptContextIds, setSelectedPromptContextIds]
);

return (
<EuiFlexGroup gutterSize="none">
<EuiFlexGroup gutterSize="none" wrap>
{sortedPromptContexts.map(({ description, id, getPromptContext, tooltip }) => (
<EuiFlexItem grow={false} key={id}>
<EuiToolTip content={tooltip}>
<EuiButton onClick={async () => alert(await getPromptContext())}>
<EuiButton
disabled={selectedPromptContextIds.includes(id)}
iconSide="left"
iconType="plus"
onClick={() =>
isNewChat ? selectPromptContext(id) : selectAndSendContextMessage(id)
}
>
{description}
</EuiButton>
</EuiToolTip>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 type { Prompt } from '../types';

export const getPromptById = ({
prompts,
id,
}: {
prompts: Prompt[];
id: string;
}): Prompt | undefined => prompts.find((p) => p.id === id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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 { EuiAvatar, EuiCommentList, EuiText } from '@elastic/eui';
import React, { useMemo } from 'react';
import styled from 'styled-components';

import type { PromptContext } from '../prompt_context/types';
import { SystemPrompt } from './system_prompt';
import type { Prompt } from '../types';
import { getPromptById } from './helpers';

import * as i18n from './translations';
import { SelectedPromptContexts } from './selected_prompt_contexts';

interface Props {
promptContexts: Record<string, PromptContext>;
promptTextPreview: string;
selectedPromptContextIds: string[];
selectedSystemPromptId: string | null;
setSelectedPromptContextIds: React.Dispatch<React.SetStateAction<string[]>>;
setSelectedSystemPromptId: React.Dispatch<React.SetStateAction<string | null>>;
systemPrompts: Prompt[];
}

const PreviewText = styled(EuiText)`
white-space: pre-line;
`;

const FirstPromptEditorComponent: React.FC<Props> = ({
promptContexts,
promptTextPreview,
selectedPromptContextIds,
selectedSystemPromptId,
setSelectedPromptContextIds,
setSelectedSystemPromptId,
systemPrompts,
}) => {
const selectedSystemPrompt = useMemo(
() =>
getPromptById({
id: selectedSystemPromptId ?? '',
prompts: systemPrompts,
}),
[selectedSystemPromptId, systemPrompts]
);

const commentBody = useMemo(
() => (
<>
<SystemPrompt
selectedSystemPromptId={selectedSystemPromptId}
setSelectedSystemPromptId={setSelectedSystemPromptId}
systemPrompts={systemPrompts}
/>

<SelectedPromptContexts
promptContexts={promptContexts}
selectedPromptContextIds={selectedPromptContextIds}
setSelectedPromptContextIds={setSelectedPromptContextIds}
/>

<PreviewText color="subdued">
{selectedSystemPrompt != null && <>{'\n'}</>}
{promptTextPreview}
</PreviewText>
</>
),
[
promptContexts,
promptTextPreview,
selectedPromptContextIds,
selectedSystemPrompt,
selectedSystemPromptId,
setSelectedPromptContextIds,
setSelectedSystemPromptId,
systemPrompts,
]
);

const comments = useMemo(
() => [
{
children: commentBody,
event: (
<EuiText size="xs">
<i>{i18n.EDITING_PROMPT}</i>
</EuiText>
),
timelineAvatar: <EuiAvatar name="user" size="l" color="subdued" iconType="logoSecurity" />,
timelineAvatarAriaLabel: i18n.YOU,
username: i18n.YOU,
},
],
[commentBody]
);

return <EuiCommentList comments={comments} aria-label={i18n.COMMENTS_LIST_ARIA_LABEL} />;
};

export const FirstPromptEditor = React.memo(FirstPromptEditorComponent);
Loading

0 comments on commit 521f26a

Please sign in to comment.