mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security AI Assistant] Fixed bug with converting message timestamp (#179007)
Current PR resolving the bug with AI conversation messages timestamp conversion from the locale date string format to ISO date. Due to that issue creating conversation from the Kibana UI, thrown an error in some timezones:  After changes we use date local format only for rendering in UI: <img width="186" alt="Screenshot 2024-03-19 at 1 39 43 PM" src="9db16761
-72de-44f6-a2a0-064fc577051a">
This commit is contained in:
parent
43dcf4d395
commit
de0c289d5a
12 changed files with 339 additions and 63 deletions
|
@ -65,11 +65,11 @@ describe('useConnectorSetup', () => {
|
|||
).toEqual([
|
||||
{
|
||||
username: 'You',
|
||||
timestamp: 'at: 7/17/2023, 1:00:36 PM',
|
||||
timestamp: `at: ${new Date('2024-03-18T18:59:18.174Z').toLocaleString()}`,
|
||||
},
|
||||
{
|
||||
username: 'Assistant',
|
||||
timestamp: 'at: 7/17/2023, 1:00:40 PM',
|
||||
timestamp: `at: ${new Date('2024-03-19T18:59:18.174Z').toLocaleString()}`,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@ export const useConnectorSetup = ({
|
|||
conversation.messages[index].timestamp == null ||
|
||||
conversation.messages[index].timestamp.length === 0
|
||||
) {
|
||||
conversation.messages[index].timestamp = new Date().toLocaleString();
|
||||
conversation.messages[index].timestamp = new Date().toISOString();
|
||||
}
|
||||
const isLastMessage = index === length - 1;
|
||||
const enableStreaming =
|
||||
|
@ -151,7 +151,9 @@ export const useConnectorSetup = ({
|
|||
() =>
|
||||
conversation.messages.slice(0, currentMessageIndex + 1).map((message, index) => {
|
||||
const isUser = message.role === 'user';
|
||||
|
||||
const timestamp = `${i18n.CONNECTOR_SETUP_TIMESTAMP_AT}: ${new Date(
|
||||
message.timestamp
|
||||
).toLocaleString()}`;
|
||||
const commentProps: EuiCommentProps = {
|
||||
username: isUser ? i18n.CONNECTOR_SETUP_USER_YOU : i18n.CONNECTOR_SETUP_USER_ASSISTANT,
|
||||
children: commentBody(message, index, conversation.messages.length),
|
||||
|
@ -163,7 +165,7 @@ export const useConnectorSetup = ({
|
|||
iconType={AssistantAvatar}
|
||||
/>
|
||||
),
|
||||
timestamp: `${i18n.CONNECTOR_SETUP_TIMESTAMP_AT}: ${message.timestamp}`,
|
||||
timestamp,
|
||||
};
|
||||
return commentProps;
|
||||
}),
|
||||
|
|
|
@ -18,7 +18,7 @@ export const alertConvo: Conversation = {
|
|||
content:
|
||||
'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:\n\nCONTEXT:\n"""\ndestination.ip,67bf8338-261a-4de6-b43e-d30b59e884a7\nhost.name,0b2e352b-35fc-47bd-a8d4-43019ed38a25\nkibana.alert.rule.name,critical hosts\nsource.ip,94277492-11f8-493b-9c52-c1c9ecd330d2\n"""\n\nEvaluate the event from the context above and format your output neatly in markdown syntax for my Elastic Security case.\nAdd your description, recommended actions and bulleted triage steps. Use the MITRE ATT&CK data provided to add more context and recommendations from MITRE, and hyperlink to the relevant pages on MITRE\'s website. Be sure to include the user and host risk score data from the context. Your response should include steps that point to Elastic Security specific features, including endpoint response actions, the Elastic Agent OSQuery manager integration (with example osquery queries), timelines and entity analytics and link to all the relevant Elastic Security documentation.',
|
||||
role: 'user',
|
||||
timestamp: '7/18/2023, 10:39:11 AM',
|
||||
timestamp: '2023-03-19T18:59:18.174Z',
|
||||
},
|
||||
],
|
||||
apiConfig: {
|
||||
|
@ -54,13 +54,13 @@ export const welcomeConvo: Conversation = {
|
|||
content:
|
||||
'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:\n\n\n\nhow do i write host.name: * in EQL?',
|
||||
role: 'user',
|
||||
timestamp: '7/17/2023, 1:00:36 PM',
|
||||
timestamp: '2024-03-18T18:59:18.174Z',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content:
|
||||
"In EQL (Event Query Language), you can write the equivalent of `host.name: *` using the `exists` operator. Here's how you can write it:\n\n```\nexists(host.name)\n```\n\nThis query will match all events where the `host.name` field exists, effectively giving you the same result as `host.name: *`.",
|
||||
timestamp: '7/17/2023, 1:00:40 PM',
|
||||
timestamp: '2024-03-19T18:59:18.174Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -95,7 +95,7 @@ export const transformToUpdateScheme = (updatedAt: string, messages: Message[])
|
|||
return {
|
||||
updated_at: updatedAt,
|
||||
messages: messages?.map((message) => ({
|
||||
'@timestamp': new Date(message.timestamp).toISOString(),
|
||||
'@timestamp': message.timestamp,
|
||||
content: message.content,
|
||||
is_error: message.isError,
|
||||
reader: message.reader,
|
||||
|
|
|
@ -95,7 +95,7 @@ export const transformToCreateScheme = (
|
|||
exclude_from_last_conversation_storage: excludeFromLastConversationStorage,
|
||||
is_default: isDefault,
|
||||
messages: messages?.map((message) => ({
|
||||
'@timestamp': new Date(message.timestamp).toISOString(),
|
||||
'@timestamp': message.timestamp,
|
||||
content: message.content,
|
||||
is_error: message.isError,
|
||||
reader: message.reader,
|
||||
|
|
|
@ -49,7 +49,7 @@ export const transformESToConversations = (
|
|||
messages:
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
conversationSchema.messages?.map((message: Record<string, any>) => ({
|
||||
timestamp: new Date(message['@timestamp']).toLocaleString(),
|
||||
timestamp: message['@timestamp'],
|
||||
// always return anonymized data from the client
|
||||
content: replaceOriginalValuesWithUuidValues({
|
||||
messageContent: message.content,
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { updateConversation } from './update_conversation';
|
||||
import {
|
||||
UpdateConversationSchema,
|
||||
transformToUpdateScheme,
|
||||
updateConversation,
|
||||
} from './update_conversation';
|
||||
import { getConversation } from './get_conversation';
|
||||
import { ConversationResponse, ConversationUpdateProps } from '@kbn/elastic-assistant-common';
|
||||
import { AuthenticatedUser } from '@kbn/security-plugin-types-common';
|
||||
|
@ -47,7 +51,18 @@ export const getConversationResponseMock = (): ConversationResponse => ({
|
|||
},
|
||||
category: 'assistant',
|
||||
excludeFromLastConversationStorage: false,
|
||||
messages: [],
|
||||
messages: [
|
||||
{
|
||||
content: 'Message 3',
|
||||
role: 'user',
|
||||
timestamp: '2024-02-14T22:29:43.862Z',
|
||||
},
|
||||
{
|
||||
content: 'Message 4',
|
||||
role: 'user',
|
||||
timestamp: '2024-02-14T22:29:43.862Z',
|
||||
},
|
||||
],
|
||||
replacements: [],
|
||||
createdAt: '2020-04-20T15:25:31.830Z',
|
||||
namespace: 'default',
|
||||
|
@ -113,3 +128,75 @@ describe('updateConversation', () => {
|
|||
expect(updatedList).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformToUpdateScheme', () => {
|
||||
beforeEach(() => {
|
||||
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();
|
||||
(getConversation as unknown as jest.Mock).mockResolvedValueOnce(existingConversation);
|
||||
|
||||
const updateAt = new Date().toISOString();
|
||||
const transformed = transformToUpdateScheme(updateAt, {
|
||||
...conversation,
|
||||
messages: [
|
||||
{
|
||||
content: 'Message 3',
|
||||
role: 'user',
|
||||
timestamp: '2011-10-05T14:48:00.000Z',
|
||||
},
|
||||
{
|
||||
content: 'Message 4',
|
||||
role: 'user',
|
||||
timestamp: '2011-10-06T14:48:00.000Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
const expected: UpdateConversationSchema = {
|
||||
id: conversation.id,
|
||||
title: 'test',
|
||||
api_config: {
|
||||
connector_id: '1',
|
||||
connector_type_title: 'test-connector',
|
||||
default_system_prompt_id: 'default-system-prompt',
|
||||
model: 'test-model',
|
||||
provider: 'OpenAI',
|
||||
},
|
||||
exclude_from_last_conversation_storage: false,
|
||||
replacements: [],
|
||||
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: undefined,
|
||||
transaction_id: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
'@timestamp': '2011-10-06T14:48:00.000Z',
|
||||
content: 'Message 4',
|
||||
is_error: undefined,
|
||||
reader: undefined,
|
||||
role: 'user',
|
||||
trace_data: {
|
||||
trace_id: undefined,
|
||||
transaction_id: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -126,7 +126,7 @@ export const transformToUpdateScheme = (
|
|||
exclude_from_last_conversation_storage: excludeFromLastConversationStorage,
|
||||
replacements,
|
||||
messages: messages?.map((message) => ({
|
||||
'@timestamp': new Date(message.timestamp).toISOString(),
|
||||
'@timestamp': message.timestamp,
|
||||
content: message.content,
|
||||
is_error: message.isError,
|
||||
reader: message.reader,
|
||||
|
|
|
@ -24,7 +24,7 @@ const currentConversation = {
|
|||
{
|
||||
role: user,
|
||||
content: 'Hello {name}',
|
||||
timestamp: '2022-01-01',
|
||||
timestamp: '2024-03-19T18:59:18.174Z',
|
||||
isError: false,
|
||||
},
|
||||
],
|
||||
|
@ -67,4 +67,11 @@ describe('getComments', () => {
|
|||
});
|
||||
expect(result[0].eventColor).toEqual('danger');
|
||||
});
|
||||
|
||||
it('It transforms message timestamp from server side ISO format to local date string', () => {
|
||||
const result = getComments(testProps);
|
||||
expect(result[0].timestamp).toEqual(
|
||||
`at: ${new Date('2024-03-19T18:59:18.174Z').toLocaleString()}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -96,7 +96,9 @@ export const getComments = ({
|
|||
<EuiAvatar name="machine" size="l" color="subdued" iconType={AssistantAvatar} />
|
||||
),
|
||||
timestamp: i18n.AT(
|
||||
message.timestamp.length === 0 ? new Date().toLocaleString() : message.timestamp
|
||||
message.timestamp.length === 0
|
||||
? new Date().toLocaleString()
|
||||
: new Date(message.timestamp).toLocaleString()
|
||||
),
|
||||
username: isUser ? i18n.YOU : i18n.ASSISTANT,
|
||||
eventColor: message.isError ? 'danger' : undefined,
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { httpServiceMock, type HttpSetupMock } from '@kbn/core-http-browser-mocks';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { createConversations } from './provider';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
||||
let http: HttpSetupMock = coreMock.createSetup().http;
|
||||
const conversations = {
|
||||
'Alert summary': {
|
||||
id: 'Alert summary',
|
||||
isDefault: true,
|
||||
apiConfig: {
|
||||
connectorId: 'my-bedrock',
|
||||
connectorTypeTitle: 'Amazon Bedrock',
|
||||
defaultSystemPromptId: 'default-system-prompt',
|
||||
},
|
||||
replacements: {
|
||||
'2a39da36-f5f4-4265-90ff-a0b2df2eb932': '192.168.0.4',
|
||||
'c960d0e7-96b4-4b7a-b287-65f33fc7a812': '142.250.72.78',
|
||||
'76d4a1d1-dbc3-4427-927e-b31b36856dc2': 'Test-MacBook-Pro.local',
|
||||
'7bb8a91c-fcbb-4430-9742-cfaad2917d37':
|
||||
'9d628163346d084eb8b3926cbf10cdee034b48e0cf83f0edf6921bc0dc83f0dd',
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
content:
|
||||
'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL, 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.\nUse the following context to answer questions:\n\nCONTEXT:\n"""\n@timestamp,2024-01-23T19:15:59.194Z\n_id,7bb8a91c-fcbb-4430-9742-cfaad2917d37\ndestination.ip,c960d0e7-96b4-4b7a-b287-65f33fc7a812\nevent.action,network_flow\nevent.category,network\nevent.dataset,flow\nevent.type,connection\nhost.name,76d4a1d1-dbc3-4427-927e-b31b36856dc2\nkibana.alert.last_detected,2024-01-23T19:15:59.230Z\nkibana.alert.risk_score,21\nkibana.alert.rule.description,a\nkibana.alert.rule.name,a\nkibana.alert.severity,low\nkibana.alert.workflow_status,open\nsource.ip,2a39da36-f5f4-4265-90ff-a0b2df2eb932\n"""\n\nEvaluate the event from the context above and format your output neatly in markdown syntax for my Elastic Security case.\nAdd your description, recommended actions and bulleted triage steps. Use the MITRE ATT&CK data provided to add more context and recommendations from MITRE, and hyperlink to the relevant pages on MITRE\'s website. Be sure to include the user and host risk score data from the context. Your response should include steps that point to Elastic Security specific features, including endpoint response actions, the Elastic Agent OSQuery manager integration (with example osquery queries), timelines and entity analytics and link to all the relevant Elastic Security documentation.',
|
||||
role: 'user',
|
||||
timestamp: '1/23/2024, 12:23:44 PM',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
reader: {},
|
||||
timestamp: '1/23/2024, 3:29:46 PM',
|
||||
isError: false,
|
||||
content: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
'Data Quality dashboard': {
|
||||
id: 'Data Quality dashboard',
|
||||
isDefault: true,
|
||||
apiConfig: {
|
||||
connectorId: 'my-gen-ai',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
defaultSystemPromptId: 'default-system-prompt',
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
content:
|
||||
'You are a helpful, expert assistant who answers questions about Elastic Security. ',
|
||||
role: 'user',
|
||||
timestamp: '18/03/2024, 12:05:03',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
reader: {},
|
||||
timestamp: '19/03/2024, 12:05:03',
|
||||
isError: false,
|
||||
content:
|
||||
'Sure, here is an example of a KQL (Kibana Query Language) query that finds records where the `event.action` field contains the word "failure":\n\n```js\nevent.action: "failure"\n```\n\nIn Kibana, there are a variety of operators and techniques you can use to further enhance your search. For instance, you can combine multiple search terms or use wildcards.\n\nHere is an advanced example showing a combined search:\n\n```js\n(event.action: "failure") AND (user.name: "testuser")\n```\nThis will return only the events where `event.action` is "failure" and `user.name` is "testuser".\n\nFor more detailed information, you can check the official [Elasticsearch KQL documentation](https://www.elastic.co/guide/en/kibana/current/kuery-query.html)',
|
||||
},
|
||||
],
|
||||
},
|
||||
'Detection Rules': {
|
||||
id: 'Detection Rules',
|
||||
isDefault: true,
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
},
|
||||
'Event summary': {
|
||||
id: 'Event summary',
|
||||
isDefault: true,
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
},
|
||||
Timeline: {
|
||||
excludeFromLastConversationStorage: true,
|
||||
id: 'Timeline',
|
||||
isDefault: true,
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
},
|
||||
Welcome: {
|
||||
id: 'Welcome',
|
||||
isDefault: true,
|
||||
theme: {
|
||||
title: 'Elastic AI Assistant',
|
||||
titleIcon: 'logoSecurity',
|
||||
assistant: {
|
||||
name: 'Elastic AI Assistant',
|
||||
icon: 'logoSecurity',
|
||||
},
|
||||
system: {
|
||||
icon: 'logoElastic',
|
||||
},
|
||||
user: {},
|
||||
},
|
||||
apiConfig: {
|
||||
connectorId: 'my-gen-ai',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
defaultSystemPromptId: 'default-system-prompt',
|
||||
},
|
||||
messages: [],
|
||||
},
|
||||
};
|
||||
const getItemStorageMock = jest.fn().mockReturnValue(conversations);
|
||||
const mockStorage = {
|
||||
store: jest.fn(),
|
||||
set: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
get: getItemStorageMock,
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
};
|
||||
|
||||
describe('createConversations', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
http = httpServiceMock.createStartContract();
|
||||
});
|
||||
|
||||
it('should call bulk conversations with the transformed conversations from the local storage', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() =>
|
||||
createConversations(
|
||||
[],
|
||||
coreMock.createStart().notifications,
|
||||
http,
|
||||
mockStorage as unknown as Storage
|
||||
)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(http.fetch.mock.calls[0][0]).toBe(
|
||||
'/api/elastic_assistant/current_user/conversations/_bulk_action'
|
||||
);
|
||||
expect(
|
||||
http.fetch.mock.calls[0].length > 1
|
||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
JSON.parse((http.fetch.mock.calls[0] as any[])[1]?.body).create.length
|
||||
: 0
|
||||
).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,8 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
import { parse } from '@kbn/datemath';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { IToasts } from '@kbn/core-notifications-browser';
|
||||
import type { IToasts, NotificationsStart } from '@kbn/core-notifications-browser';
|
||||
import type { Conversation } from '@kbn/elastic-assistant';
|
||||
import {
|
||||
AssistantProvider as ElasticAssistantProvider,
|
||||
|
@ -17,6 +19,8 @@ import {
|
|||
|
||||
import type { FetchConversationsResponse } from '@kbn/elastic-assistant/impl/assistant/api';
|
||||
import { once } from 'lodash/fp';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
import type { Message } from '@kbn/elastic-assistant-common';
|
||||
import { useBasePath, useKibana } from '../common/lib/kibana';
|
||||
import { useAssistantTelemetry } from './use_assistant_telemetry';
|
||||
import { getComments } from './get_comments';
|
||||
|
@ -42,6 +46,70 @@ const LOCAL_CONVERSATIONS_MIGRATION_STATUS_TOAST_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const createConversations = async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
conversationsData: Record<string, any>,
|
||||
notifications: NotificationsStart,
|
||||
http: HttpSetup,
|
||||
storage: Storage
|
||||
) => {
|
||||
// migrate conversations with messages from the local storage
|
||||
// won't happen next time
|
||||
const conversations = storage.get(`securitySolution.${LOCAL_STORAGE_KEY}`);
|
||||
|
||||
if (
|
||||
conversationsData &&
|
||||
Object.keys(conversationsData).length === 0 &&
|
||||
conversations &&
|
||||
Object.keys(conversations).length > 0
|
||||
) {
|
||||
const conversationsToCreate = Object.values(conversations).filter(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(c: any) => c.messages && c.messages.length > 0
|
||||
);
|
||||
|
||||
const transformMessage = (m: Message) => {
|
||||
const timestamp = parse(m.timestamp ?? '')?.toISOString();
|
||||
return {
|
||||
...m,
|
||||
timestamp: timestamp == null ? new Date().toISOString() : timestamp,
|
||||
};
|
||||
};
|
||||
|
||||
// post bulk create
|
||||
const bulkResult = await bulkChangeConversations(
|
||||
http,
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
create: conversationsToCreate.reduce((res: Record<string, Conversation>, c: any) => {
|
||||
res[c.id] = {
|
||||
...c,
|
||||
messages: (c.messages ?? []).map(transformMessage),
|
||||
title: c.id,
|
||||
replacements: c.replacements
|
||||
? Object.keys(c.replacements).map((uuid) => ({
|
||||
uuid,
|
||||
value: c.replacements[uuid],
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
return res;
|
||||
}, {}),
|
||||
},
|
||||
notifications.toasts
|
||||
);
|
||||
if (bulkResult && bulkResult.success) {
|
||||
storage.remove(`securitySolution.${LOCAL_STORAGE_KEY}`);
|
||||
notifications.toasts?.addSuccess({
|
||||
iconType: 'check',
|
||||
title: LOCAL_CONVERSATIONS_MIGRATION_STATUS_TOAST_TITLE,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This component configures the Elastic AI Assistant context provider for the Security Solution app.
|
||||
*/
|
||||
|
@ -60,52 +128,8 @@ export const AssistantProvider: React.FC = ({ children }) => {
|
|||
const assistantTelemetry = useAssistantTelemetry();
|
||||
|
||||
const migrateConversationsFromLocalStorage = once(
|
||||
async (conversationsData: Record<string, Conversation>) => {
|
||||
// migrate conversations with messages from the local storage
|
||||
// won't happen next time
|
||||
const conversations = storage.get(`securitySolution.${LOCAL_STORAGE_KEY}`);
|
||||
if (
|
||||
conversationsData &&
|
||||
Object.keys(conversationsData).length === 0 &&
|
||||
conversations &&
|
||||
Object.keys(conversations).length > 0
|
||||
) {
|
||||
const conversationsToCreate = Object.values(conversations).filter(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(c: any) => c.messages && c.messages.length > 0
|
||||
);
|
||||
// post bulk create
|
||||
const bulkResult = await bulkChangeConversations(
|
||||
http,
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
create: conversationsToCreate.reduce((res: Record<string, Conversation>, c: any) => {
|
||||
res[c.id] = {
|
||||
...c,
|
||||
title: c.id,
|
||||
replacements: c.replacements
|
||||
? Object.keys(c.replacements).map((uuid) => ({
|
||||
uuid,
|
||||
value: c.replacements[uuid],
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
return res;
|
||||
}, {}),
|
||||
},
|
||||
notifications.toasts
|
||||
);
|
||||
if (bulkResult && bulkResult.success) {
|
||||
storage.remove(`securitySolution.${LOCAL_STORAGE_KEY}`);
|
||||
notifications.toasts?.addSuccess({
|
||||
iconType: 'check',
|
||||
title: LOCAL_CONVERSATIONS_MIGRATION_STATUS_TOAST_TITLE,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
(conversationsData: Record<string, Conversation>) =>
|
||||
createConversations(conversationsData, notifications, http, storage)
|
||||
);
|
||||
const onFetchedConversations = useCallback(
|
||||
(conversationsData: FetchConversationsResponse): Record<string, Conversation> => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue