[Security AI Assistant] Conversation migration bugs (#181512)

Fixed duplication conversations and actionTypeId for removed connectors:
<img width="2030" alt="Screenshot 2024-04-23 at 8 17 16 AM"
src="e85a894a-c9ae-4691-95ba-24f37931f47e">

<img width="2253" alt="Screenshot 2024-04-23 at 8 03 44 AM"
src="c8040605-b994-424e-bf52-652f30057d31">
This commit is contained in:
Yuliia Naumenko 2024-04-24 17:34:05 -07:00 committed by GitHub
parent 7895df6ee2
commit f0e4a97219
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 74 additions and 42 deletions

View file

@ -12,8 +12,10 @@ import {
ApiConfig,
Replacements,
API_VERSIONS,
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
} from '@kbn/elastic-assistant-common';
import { Conversation, ClientMessage } from '../../../assistant_context/types';
import { FetchConversationsResponse } from './use_fetch_current_user_conversations';
export interface GetConversationByIdParams {
http: HttpSetup;
@ -58,6 +60,44 @@ export const getConversationById = async ({
}
};
/**
* API call for getting all user conversations.
*
* @param {Object} options - The options object.
* @param {HttpSetup} options.http - HttpSetup
* @param {IToasts} [options.toasts] - IToasts
* @param {AbortSignal} [options.signal] - AbortSignal
*
* @returns {Promise<FetchConversationsResponse>}
*/
export const getUserConversations = async ({
http,
signal,
toasts,
}: {
http: HttpSetup;
toasts?: IToasts;
signal?: AbortSignal | undefined;
}) => {
try {
return await http.fetch<FetchConversationsResponse>(
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
{
method: 'GET',
version: API_VERSIONS.public.v1,
signal,
}
);
} catch (error) {
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
title: i18n.translate('xpack.elasticAssistant.conversations.getUserConversationsError', {
defaultMessage: 'Error fetching conversations',
}),
});
throw error;
}
};
export interface PostConversationParams {
http: HttpSetup;
conversation: Partial<Conversation>;

View file

@ -159,3 +159,4 @@ export { getConversationById } from './impl/assistant/api/conversations/conversa
export { mergeBaseWithPersistedConversations } from './impl/assistant/helpers';
export { UpgradeButtons } from './impl/upgrade/upgrade_buttons';
export { getUserConversations } from './impl/assistant/api';

View file

@ -158,7 +158,6 @@ describe('createConversations', () => {
await act(async () => {
const { waitForNextUpdate } = renderHook(() =>
createConversations(
[],
coreMock.createStart().notifications,
http,
mockStorage as unknown as Storage
@ -181,7 +180,6 @@ describe('createConversations', () => {
await act(async () => {
const { waitForNextUpdate } = renderHook(() =>
createConversations(
[],
coreMock.createStart().notifications,
http,
mockStorage as unknown as Storage

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback } 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';
@ -13,15 +13,14 @@ import type { Conversation } from '@kbn/elastic-assistant';
import {
AssistantProvider as ElasticAssistantProvider,
bulkUpdateConversations,
mergeBaseWithPersistedConversations,
useFetchCurrentUserConversations,
getUserConversations,
} from '@kbn/elastic-assistant';
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 { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
import { APP_ID } from '../../common';
import { useBasePath, useKibana } from '../common/lib/kibana';
import { useAssistantTelemetry } from './use_assistant_telemetry';
import { getComments } from './get_comments';
@ -46,22 +45,15 @@ 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}`);
const conversations = storage.get(`${APP_ID}.${LOCAL_STORAGE_KEY}`);
if (
conversationsData &&
Object.keys(conversationsData).length === 0 &&
conversations &&
Object.keys(conversations).length > 0
) {
if (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
@ -87,10 +79,14 @@ export const createConversations = async (
const selectedConnector = (connectors ?? []).find(
(connector) => connector.id === c.apiConfig.connectorId
);
c.apiConfig = {
...c.apiConfig,
actionTypeId: selectedConnector?.actionTypeId,
};
if (selectedConnector) {
c.apiConfig = {
...c.apiConfig,
actionTypeId: selectedConnector.actionTypeId,
};
} else {
c.apiConfig = undefined;
}
}
res[c.id] = {
...c,
@ -104,7 +100,7 @@ export const createConversations = async (
notifications.toasts
);
if (bulkResult && bulkResult.success) {
storage.remove(`securitySolution.${LOCAL_STORAGE_KEY}`);
storage.remove(`${APP_ID}.${LOCAL_STORAGE_KEY}`);
notifications.toasts?.addSuccess({
iconType: 'check',
title: LOCAL_CONVERSATIONS_MIGRATION_STATUS_TOAST_TITLE,
@ -132,30 +128,27 @@ export const AssistantProvider: React.FC = ({ children }) => {
const assistantAvailability = useAssistantAvailability();
const assistantTelemetry = useAssistantTelemetry();
const migrateConversationsFromLocalStorage = once(
(conversationsData: Record<string, Conversation>) =>
createConversations(conversationsData, notifications, http, storage)
);
const onFetchedConversations = useCallback(
(conversationsData: FetchConversationsResponse): Record<string, Conversation> => {
const mergedData = mergeBaseWithPersistedConversations({}, conversationsData);
if (assistantAvailability.isAssistantEnabled && assistantAvailability.hasAssistantPrivilege) {
migrateConversationsFromLocalStorage(mergedData);
useEffect(() => {
const migrateConversationsFromLocalStorage = once(async () => {
const res = await getUserConversations({
http,
});
if (
assistantAvailability.isAssistantEnabled &&
assistantAvailability.hasAssistantPrivilege &&
res.total === 0
) {
await createConversations(notifications, http, storage);
}
return mergedData;
},
[
assistantAvailability.hasAssistantPrivilege,
assistantAvailability.isAssistantEnabled,
migrateConversationsFromLocalStorage,
]
);
useFetchCurrentUserConversations({
});
migrateConversationsFromLocalStorage();
}, [
assistantAvailability.hasAssistantPrivilege,
assistantAvailability.isAssistantEnabled,
http,
onFetch: onFetchedConversations,
isAssistantEnabled:
assistantAvailability.isAssistantEnabled && assistantAvailability.hasAssistantPrivilege,
});
notifications,
storage,
]);
const { signalIndexName } = useSignalIndex();
const alertsIndexPattern = signalIndexName ?? undefined;