kibana/x-pack/plugins/security_solution/public/assistant/helpers.tsx
Garrett Spong 83a31bfcc0
[Security Solution] [Elastic AI Assistant] Fixes System Prompt not sending as part of the conversation (#161920)
## Summary

Resolves System Prompt not sending issues:
https://github.com/elastic/kibana/issues/161809

Also resolves:
- [X] Not being able to delete really long Conversation, System Prompt,
and Quick Prompt names
- [X] Fix user/all System Prompts being overridden on refresh
- [X] Conversation without default System Prompt not healed if it is
initial conversation when Assistant opens (Timeline)
- [X] New conversation created from Conversations Settings not getting a
connector by default
- [X] Current conversation not selected by default when settings gear is
clicked (and other assistant instances exist)
- [X] Sent to Timeline action sends anonymized values instead of actual
plaintext
- [X] Clicking Submit does not clear the text area
- [X] Remove System Prompt Tooltip
- [X] Fixes confusion when System or Quick Prompt is empty by adding a
placeholder value
- [X] Shows (empty prompt) in System Prompt selector when the Prompt
content is empty
- [X] Fixes connector error callout flashing on initial load
- [X] Shows `(empty prompt)` text within Prompt Editor when prompt
content is empty to prevent confusion

### Checklist

Delete any items that are not applicable to this PR.

- [X] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
2023-07-14 17:42:57 -06:00

132 lines
4.3 KiB
TypeScript

/*
* 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 { EuiIcon, EuiToolTip } from '@elastic/eui';
import { analyzeMarkdown } from '@kbn/elastic-assistant';
import type { Conversation, CodeBlockDetails } from '@kbn/elastic-assistant';
import React from 'react';
import type { TimelineEventsDetailsItem } from '../../common/search_strategy';
import type { Rule } from '../detection_engine/rule_management/logic';
import { SendToTimelineButton } from './send_to_timeline';
import { INVESTIGATE_IN_TIMELINE } from '../actions/add_to_timeline/constants';
export const LOCAL_STORAGE_KEY = `securityAssistant`;
export interface QueryField {
field: string;
values: string;
}
export const SECURITY_ASSISTANT_UI_SETTING_KEY = 'securityAssistant';
export const getPromptContextFromDetectionRules = (rules: Rule[]): string => {
const data = rules.map((rule) => `Rule Name:${rule.name}\nRule Description:${rule.description}`);
return data.join('\n\n');
};
export const getAllFields = (data: TimelineEventsDetailsItem[]): QueryField[] =>
data
.filter(({ field }) => !field.startsWith('signal.'))
.map(({ field, values }) => ({ field, values: values?.join(',') ?? '' }));
export const getRawData = (data: TimelineEventsDetailsItem[]): Record<string, string[]> =>
data
.filter(({ field }) => !field.startsWith('signal.'))
.reduce((acc, { field, values }) => ({ ...acc, [field]: values ?? [] }), {});
export const getFieldsAsCsv = (queryFields: QueryField[]): string =>
queryFields.map(({ field, values }) => `${field},${values}`).join('\n');
export const getPromptContextFromEventDetailsItem = (data: TimelineEventsDetailsItem[]): string => {
const allFields = getAllFields(data);
return getFieldsAsCsv(allFields);
};
const sendToTimelineEligibleQueryTypes: Array<CodeBlockDetails['type']> = ['kql', 'dsl', 'eql'];
/**
* Returns message contents with replacements applied.
*
* @param message
* @param replacements
*/
export const getMessageContentWithReplacements = ({
messageContent,
replacements,
}: {
messageContent: string;
replacements: Record<string, string> | undefined;
}): string =>
replacements != null
? Object.keys(replacements).reduce(
(acc, replacement) => acc.replaceAll(replacement, replacements[replacement]),
messageContent
)
: messageContent;
/**
* Augments the messages in a conversation with code block details, including
* the start and end indices of the code block in the message, the type of the
* code block, and the button to add the code block to the timeline.
*
* @param currentConversation
*/
export const augmentMessageCodeBlocks = (
currentConversation: Conversation
): CodeBlockDetails[][] => {
const cbd = currentConversation.messages.map(({ content }) =>
analyzeMarkdown(
getMessageContentWithReplacements({
messageContent: content,
replacements: currentConversation.replacements,
})
)
);
const output = cbd.map((codeBlocks, messageIndex) =>
codeBlocks.map((codeBlock, codeBlockIndex) => {
return {
...codeBlock,
getControlContainer: () =>
document.querySelectorAll(`.message-${messageIndex} .euiCodeBlock__controls`)[
codeBlockIndex
],
button: sendToTimelineEligibleQueryTypes.includes(codeBlock.type) ? (
<SendToTimelineButton
asEmptyButton={true}
dataProviders={[
{
id: 'assistant-data-provider',
name: `Assistant Query from conversation ${currentConversation.id}`,
enabled: true,
excluded: false,
queryType: codeBlock.type,
kqlQuery: codeBlock.content ?? '',
queryMatch: {
field: 'host.name',
operator: ':',
value: 'test',
},
and: [],
},
]}
keepDataView={true}
>
<EuiToolTip position="right" content={INVESTIGATE_IN_TIMELINE}>
<EuiIcon type="timeline" />
</EuiToolTip>
</SendToTimelineButton>
) : null,
};
})
);
return output;
};