Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security assistant] Conversation pagination patch #196782

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions oas_docs/output/kibana.serverless.staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40636,6 +40636,7 @@ components:
id:
$ref: '#/components/schemas/Security_AI_Assistant_API_NonEmptyString'
isDefault:
default: false
description: Is default conversation.
type: boolean
messages:
Expand Down
1 change: 1 addition & 0 deletions oas_docs/output/kibana.serverless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40636,6 +40636,7 @@ components:
id:
$ref: '#/components/schemas/Security_AI_Assistant_API_NonEmptyString'
isDefault:
default: false
description: Is default conversation.
type: boolean
messages:
Expand Down
1 change: 1 addition & 0 deletions oas_docs/output/kibana.staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49215,6 +49215,7 @@ components:
id:
$ref: '#/components/schemas/Security_AI_Assistant_API_NonEmptyString'
isDefault:
default: false
description: Is default conversation.
type: boolean
messages:
Expand Down
1 change: 1 addition & 0 deletions oas_docs/output/kibana.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49215,6 +49215,7 @@ components:
id:
$ref: '#/components/schemas/Security_AI_Assistant_API_NonEmptyString'
isDefault:
default: false
description: Is default conversation.
type: boolean
messages:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,7 @@ components:
id:
$ref: '#/components/schemas/NonEmptyString'
isDefault:
default: false
description: Is default conversation.
type: boolean
messages:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,7 @@ components:
id:
$ref: '#/components/schemas/NonEmptyString'
isDefault:
default: false
description: Is default conversation.
type: boolean
messages:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export const ConversationResponse = z.object({
/**
* Is default conversation.
*/
isDefault: z.boolean().optional(),
isDefault: z.boolean().optional().default(false),
/**
* excludeFromLastConversationStorage.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ components:
isDefault:
description: Is default conversation.
type: boolean
default: false
excludeFromLastConversationStorage:
description: excludeFromLastConversationStorage.
type: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,33 +61,41 @@ export const getConversationById = async ({
};

/**
* API call for getting all user conversations.
* API call for checking if user has any 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>}
* @returns {Promise<boolean>}
*/
export const getUserConversations = async ({
export const getUserConversationsExist = async ({
http,
signal,
toasts,
}: {
http: HttpSetup;
toasts?: IToasts;
signal?: AbortSignal | undefined;
}) => {
}): Promise<boolean> => {
try {
return await http.fetch<FetchConversationsResponse>(
const { total } = await http.fetch<FetchConversationsResponse>(
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
{
method: 'GET',
version: API_VERSIONS.public.v1,
signal,
query: {
// TODO introduce a new API endpoint for count
// in meantime, fetch only the title field to reduce payload
per_page: 1,
fields: ['title'],
},
}
);

return total > 0;
} catch (error) {
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
title: i18n.translate('xpack.elasticAssistant.conversations.getUserConversationsError', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,28 @@ import { defaultAssistantFeatures } from '@kbn/elastic-assistant-common';
const http = {
fetch: jest.fn().mockResolvedValue(defaultAssistantFeatures),
};
const onFetch = jest.fn();
const mockData = {
welcome_id: {
id: 'welcome_id',
title: 'Welcome',
category: 'assistant',
messages: [],
apiConfig: { actionTypeId: '.gen-ai', connectorId: '123' },
replacements: {},
},
electric_sheep_id: {
id: 'electric_sheep_id',
category: 'assistant',
title: 'electric sheep',
messages: [],
apiConfig: { actionTypeId: '.gen-ai', connectorId: '123' },
replacements: {},
},
};

const defaultProps = {
http,
onFetch,
baseConversations: {},
isAssistantEnabled: true,
} as unknown as UseFetchCurrentUserConversationsParams;

Expand All @@ -36,14 +53,20 @@ const createWrapper = () => {
};

describe('useFetchCurrentUserConversations', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it(`should make http request to fetch conversations`, async () => {
renderHook(() => useFetchCurrentUserConversations(defaultProps), {
wrapper: createWrapper(),
});

await act(async () => {
const { waitForNextUpdate } = renderHook(() =>
useFetchCurrentUserConversations(defaultProps)
const { result, waitForNextUpdate } = renderHook(
() => useFetchCurrentUserConversations(defaultProps),
{
wrapper: createWrapper(),
}
);
await waitForNextUpdate();
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
Expand All @@ -52,14 +75,47 @@ describe('useFetchCurrentUserConversations', () => {
method: 'GET',
query: {
page: 1,
perPage: 100,
per_page: 5000,
fields: ['title', 'is_default', 'updated_at', 'api_config'],
sort_field: 'is_default',
sort_order: 'desc',
},
version: '2023-10-31',
signal: undefined,
}
);
expect(result.current.data).toEqual({});
});
});
it(`Combines baseConversations with result`, async () => {
renderHook(() => useFetchCurrentUserConversations(defaultProps), {
wrapper: createWrapper(),
});

expect(onFetch).toHaveBeenCalled();
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
() => useFetchCurrentUserConversations({ ...defaultProps, baseConversations: mockData }),
{
wrapper: createWrapper(),
}
);
await waitForNextUpdate();
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
'/api/security_ai_assistant/current_user/conversations/_find',
{
method: 'GET',
query: {
page: 1,
per_page: 5000,
fields: ['title', 'is_default', 'updated_at', 'api_config'],
sort_field: 'is_default',
sort_order: 'desc',
},
version: '2023-10-31',
signal: undefined,
}
);
expect(result.current.data).toEqual(mockData);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,24 @@ import {
API_VERSIONS,
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
} from '@kbn/elastic-assistant-common';
import { mergeBaseWithPersistedConversations } from '../../helpers';
import { Conversation } from '../../../assistant_context/types';

export interface FetchConversationsResponse {
page: number;
perPage: number;
per_page: number;
total: number;
data: Conversation[];
}

export interface UseFetchCurrentUserConversationsParams {
http: HttpSetup;
onFetch: (result: FetchConversationsResponse) => Record<string, Conversation>;
baseConversations?: Record<string, Conversation>;
signal?: AbortSignal | undefined;
refetchOnWindowFocus?: boolean;
isAssistantEnabled: boolean;
fields?: string[];
filter?: string;
}

/**
Expand All @@ -40,36 +43,45 @@ export interface UseFetchCurrentUserConversationsParams {
*/
const query = {
page: 1,
perPage: 100,
// TODO optimize with pagination
// https://github.com/elastic/kibana/issues/192714
per_page: 5000,
// ensure default conversations are fetched first to avoid recreating them
sort_field: 'is_default',
sort_order: 'desc',
};

export const CONVERSATIONS_QUERY_KEYS = [
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
query.page,
query.perPage,
API_VERSIONS.public.v1,
];
export const CONVERSATIONS_QUERY_KEYS = [ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND];

export const useFetchCurrentUserConversations = ({
http,
onFetch,
baseConversations = {},
signal,
refetchOnWindowFocus = true,
isAssistantEnabled,
// defaults to only return these fields to keep conversations object small
// will fill in mock data for the other fields ie: `messages: []`
// when you need the full conversation object, call the getConversation api
fields = ['title', 'is_default', 'updated_at', 'api_config'],
filter,
}: UseFetchCurrentUserConversationsParams) =>
useQuery(
CONVERSATIONS_QUERY_KEYS,
fields && fields.length ? [...CONVERSATIONS_QUERY_KEYS, fields] : CONVERSATIONS_QUERY_KEYS,
async () =>
http.fetch<FetchConversationsResponse>(ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, {
method: 'GET',
version: API_VERSIONS.public.v1,
query,
query: {
...query,
...(fields && fields.length ? { fields } : {}),
...(filter && filter.length ? { filter } : {}),
},
signal,
}),
{
select: (data) => onFetch(data),
select: (data) => mergeBaseWithPersistedConversations(baseConversations, data),
keepPreviousData: true,
initialData: { page: 1, perPage: 100, total: 0, data: [] },
initialData: { ...query, total: 0, data: [] },
refetchOnWindowFocus,
enabled: isAssistantEnabled,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,15 @@ import {
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';

import { useConversation } from '../../use_conversation';
import { WELCOME_CONVERSATION_TITLE } from '../../../..';
import { Conversation } from '../../../assistant_context/types';
import { ConversationTableItem, useConversationsTable } from './use_conversations_table';
import { ConversationStreamingSwitch } from '../conversation_settings/conversation_streaming_switch';
import { AIConnector } from '../../../connectorland/connector_selector';
import * as i18n from './translations';

import {
FetchConversationsResponse,
useFetchCurrentUserConversations,
useFetchPrompts,
} from '../../api';
import { useFetchCurrentUserConversations, useFetchPrompts } from '../../api';
import { useAssistantContext } from '../../../assistant_context';
import { useConversationDeleted } from '../conversation_settings/use_conversation_deleted';
import { useFlyoutModalVisibility } from '../../common/components/assistant_settings_management/flyout/use_flyout_modal_visibility';
Expand All @@ -37,12 +35,10 @@ import { CONVERSATION_TABLE_SESSION_STORAGE_KEY } from '../../../assistant_conte
import { useSessionPagination } from '../../common/components/assistant_settings_management/pagination/use_session_pagination';
import { DEFAULT_PAGE_SIZE } from '../../settings/const';
import { useSettingsUpdater } from '../../settings/use_settings_updater/use_settings_updater';
import { mergeBaseWithPersistedConversations } from '../../helpers';
import { AssistantSettingsBottomBar } from '../../settings/assistant_settings_bottom_bar';
interface Props {
connectors: AIConnector[] | undefined;
defaultConnector?: AIConnector;
defaultSelectedConversation: Conversation;
isDisabled?: boolean;
}

Expand All @@ -51,10 +47,10 @@ export const DEFAULT_TABLE_OPTIONS = {
sort: { field: 'createdAt', direction: 'desc' as const },
};

const defaultSelectedConversationId = WELCOME_CONVERSATION_TITLE;
const ConversationSettingsManagementComponent: React.FC<Props> = ({
connectors,
defaultConnector,
defaultSelectedConversation,
isDisabled,
}) => {
const {
Expand All @@ -66,12 +62,6 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
toasts,
} = useAssistantContext();

const onFetchedConversations = useCallback(
(conversationsData: FetchConversationsResponse): Record<string, Conversation> =>
mergeBaseWithPersistedConversations(baseConversations, conversationsData),
[baseConversations]
);

const { data: allPrompts, isFetched: promptsLoaded, refetch: refetchPrompts } = useFetchPrompts();

const {
Expand All @@ -80,10 +70,19 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
refetch: refetchConversations,
} = useFetchCurrentUserConversations({
http,
onFetch: onFetchedConversations,
baseConversations,
isAssistantEnabled,
});

const { getDefaultConversation } = useConversation();

const defaultSelectedConversation = useMemo(
() =>
conversations?.[defaultSelectedConversationId] ??
getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE }),
[conversations, getDefaultConversation]
);

const refetchAll = useCallback(() => {
refetchPrompts();
refetchConversations();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('helpers', () => {
};
const conversationsData = {
page: 1,
perPage: 10,
per_page: 10,
total: 2,
data: Object.values(baseConversations).map((c) => c),
};
Expand Down
Loading