From 5e7a5fffff62b3b6a908dfa49b7487919ab0de88 Mon Sep 17 00:00:00 2001 From: Viduni Wickramarachchi Date: Tue, 25 Feb 2025 17:03:09 -0500 Subject: [PATCH] [Obs AI Assistant] Conversation sharing route and other related permission handling (#206590) --- .../kbn-ai-assistant/src/chat/chat_banner.tsx | 57 +++++++ .../kbn-ai-assistant/src/chat/chat_body.tsx | 152 +++++++++++++----- .../src/chat/chat_context_menu.tsx | 110 +++++++------ .../kbn-ai-assistant/src/chat/chat_header.tsx | 19 ++- .../src/chat/chat_sharing_menu.tsx | 44 +++-- .../src/conversation/conversation_view.tsx | 1 - .../src/hooks/use_conversation.ts | 76 ++++++++- .../common/index.ts | 2 +- .../common/types.ts | 5 + .../public/index.ts | 5 +- .../server/routes/conversations/route.ts | 36 ++++- .../server/service/client/index.ts | 37 +++++ 12 files changed, 434 insertions(+), 110 deletions(-) create mode 100644 x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_banner.tsx diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_banner.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_banner.tsx new file mode 100644 index 0000000000000..680b0f46c68b1 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_banner.tsx @@ -0,0 +1,57 @@ +/* + * 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 React, { ReactNode } from 'react'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/css'; +import { EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, useEuiTheme } from '@elastic/eui'; + +export function ChatBanner({ + title, + description, + button = null, +}: { + title: string; + description: string; + button?: ReactNode; +}) { + const { euiTheme } = useEuiTheme(); + + return ( + + + + + + + +

+ {i18n.translate('xpack.aiAssistant.shareBanner.title', { + defaultMessage: title, + })} +

+

+ {i18n.translate('xpack.aiAssistant.shareBanner.description', { + defaultMessage: description, + })} +

+ {button} +
+
+
+
+ ); +} diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx index 555f6b8b15def..4e01f5edd382e 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx @@ -6,6 +6,7 @@ */ import { + EuiButton, EuiCallOut, euiCanAnimate, EuiFlexGroup, @@ -19,7 +20,11 @@ import { } from '@elastic/eui'; import { css, keyframes } from '@emotion/css'; import { i18n } from '@kbn/i18n'; -import type { Conversation, Message } from '@kbn/observability-ai-assistant-plugin/common'; +import type { + Conversation, + ConversationAccess, + Message, +} from '@kbn/observability-ai-assistant-plugin/common'; import { ChatActionClickType, ChatState, @@ -47,6 +52,7 @@ import { WelcomeMessage } from './welcome_message'; import { useLicense } from '../hooks/use_license'; import { PromptEditor } from '../prompt_editor/prompt_editor'; import { useKibana } from '../hooks/use_kibana'; +import { ChatBanner } from './chat_banner'; const fullHeightClassName = css` height: 100%; @@ -71,6 +77,7 @@ const incorrectLicenseContainer = (euiTheme: UseEuiTheme['euiTheme']) => css` const chatBodyContainerClassNameWithError = css` align-self: center; + margin: 12px; `; const promptEditorContainerClassName = css` @@ -153,7 +160,19 @@ export function ChatBody({ false ); - const { conversation, messages, next, state, stop, saveTitle } = useConversation({ + const { + conversation, + conversationId, + messages, + next, + state, + stop, + saveTitle, + isConversationOwnedByCurrentUser, + user: conversationUser, + updateConversationAccess, + } = useConversation({ + currentUser, initialConversationId, initialMessages, initialTitle, @@ -323,6 +342,64 @@ export function ChatBody({ } }; + const handleConversationAccessUpdate = async (access: ConversationAccess) => { + await updateConversationAccess(access); + conversation.refresh(); + refreshConversations(); + }; + + let sharedBanner: React.ReactNode = null; + let showPromptEditor: boolean = false; + + if (!conversation.value?.public) { + // Private conversation: Show only the prompt editor + showPromptEditor = true; + } else if (!!conversationUser) { + if (isConversationOwnedByCurrentUser) { + // Public, conversation has a user, and current user is the owner: + // Show both prompt editor and banner + showPromptEditor = true; + sharedBanner = ( + + ); + } else { + // Public, conversation has a user, but current user is not the owner + // Don't show prompt editor, only show the banner with Duplicate button + sharedBanner = ( + {}} iconType="copy" size="s"> + {i18n.translate('xpack.aiAssistant.duplicateButton', { + defaultMessage: 'Duplicate', + })} + + } + /> + ); + } + } else { + // Public, but conversation doesn't have a user (for backwards compatibility with old conversations generated by the rule connector): + // Don't show prompt editor, only show the banner with Duplicate button + sharedBanner = ( + {}} iconType="copy" size="s"> + {i18n.translate('xpack.aiAssistant.duplicateButton', { + defaultMessage: 'Duplicate', + })} + + } + /> + ); + } + let footer: React.ReactNode; if (!hasCorrectLicense && !initialConversationId) { footer = ( @@ -415,35 +492,40 @@ export function ChatBody({ ) : null} - - - - - + <> + {conversationId ? sharedBanner : null} + {showPromptEditor ? ( + + + + + + ) : null} + ); } @@ -506,11 +588,7 @@ export function ChatBody({ diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_context_menu.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_context_menu.tsx index 7731239826e5b..f538480acc25e 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_context_menu.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_context_menu.tsx @@ -22,11 +22,13 @@ import { useConfirmModal } from '../hooks'; export function ChatContextMenu({ disabled = false, + isConversationOwnedByCurrentUser, onCopyToClipboardClick, onCopyUrlClick, onDeleteClick, }: { disabled?: boolean; + isConversationOwnedByCurrentUser: boolean; onCopyToClipboardClick: () => void; onCopyUrlClick: () => void; onDeleteClick: () => void; @@ -46,6 +48,61 @@ export function ChatContextMenu({ }), }); + const menuItems = [ + { + onCopyToClipboardClick(); + setIsPopoverOpen(false); + }} + > + {i18n.translate('xpack.aiAssistant.chatHeader.contextMenu.copyToClipboard', { + defaultMessage: 'Copy to clipboard', + })} + , + { + onCopyUrlClick(); + setIsPopoverOpen(false); + }} + > + {i18n.translate('xpack.aiAssistant.chatHeader.contextMenu.copyUrl', { + defaultMessage: 'Copy URL', + })} + , + ]; + + if (isConversationOwnedByCurrentUser) { + menuItems.push(); + menuItems.push( + } + onClick={() => { + confirmDeleteCallback().then((confirmed) => { + if (!confirmed) { + return; + } + onDeleteClick(); + }); + + setIsPopoverOpen(false); + }} + > + {i18n.translate('xpack.aiAssistant.conversationList.deleteConversationIconLabel', { + defaultMessage: 'Delete', + })} + + ); + } + return ( <> - { - onCopyToClipboardClick(); - setIsPopoverOpen(false); - }} - > - {i18n.translate('xpack.aiAssistant.chatHeader.contextMenu.copyToClipboard', { - defaultMessage: 'Copy to clipboard', - })} - , - { - onCopyUrlClick(); - setIsPopoverOpen(false); - }} - > - {i18n.translate('xpack.aiAssistant.chatHeader.contextMenu.copyUrl', { - defaultMessage: 'Copy URL', - })} - , - , - } - onClick={() => { - confirmDeleteCallback().then((confirmed) => { - if (!confirmed) { - return; - } - onDeleteClick(); - }); - - setIsPopoverOpen(false); - }} - > - {i18n.translate('xpack.aiAssistant.conversationList.deleteConversationIconLabel', { - defaultMessage: 'Delete', - })} - , - ]} - /> + {confirmDeleteElement} diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx index dd422f7f11acd..a518346806936 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx @@ -21,7 +21,7 @@ import { import { i18n } from '@kbn/i18n'; import { css } from '@emotion/css'; import { AssistantIcon } from '@kbn/ai-assistant-icon'; -import { Conversation } from '@kbn/observability-ai-assistant-plugin/common'; +import { Conversation, ConversationAccess } from '@kbn/observability-ai-assistant-plugin/common'; import { ChatActionsMenu } from './chat_actions_menu'; import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; import { FlyoutPositionMode } from './chat_flyout'; @@ -63,6 +63,8 @@ export function ChatHeader({ setIsUpdatingConversationList, refreshConversations, updateDisplayedConversation, + handleConversationAccessUpdate, + isConversationOwnedByCurrentUser, }: { connectors: UseGenAIConnectorsResult; conversationId?: string; @@ -77,6 +79,8 @@ export function ChatHeader({ setIsUpdatingConversationList: (isUpdating: boolean) => void; refreshConversations: () => void; updateDisplayedConversation: (id?: string) => void; + handleConversationAccessUpdate: (access: ConversationAccess) => Promise; + isConversationOwnedByCurrentUser: boolean; }) { const theme = useEuiTheme(); const breakpoint = useCurrentEuiBreakpoint(); @@ -165,9 +169,15 @@ export function ChatHeader({ {conversationId && conversation ? ( <> - - - + {isConversationOwnedByCurrentUser ? ( + + + + ) : null} { deleteConversation(conversationId).then(() => updateDisplayedConversation()); }} + isConversationOwnedByCurrentUser={isConversationOwnedByCurrentUser} /> diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_sharing_menu.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_sharing_menu.tsx index 1efc4763ee92c..be7e69698069c 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_sharing_menu.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_sharing_menu.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import React, { useState, useCallback } from 'react'; import { EuiPopover, @@ -17,6 +18,7 @@ import { useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { ConversationAccess } from '@kbn/observability-ai-assistant-plugin/public'; interface OptionData { description?: string; @@ -30,24 +32,34 @@ const sharedLabel = i18n.translate('xpack.aiAssistant.chatHeader.shareOptions.sh defaultMessage: 'Shared', }); -export function ChatSharingMenu() { +export function ChatSharingMenu({ + conversationId, + isPublic, + onChangeConversationAccess, +}: { + conversationId: string; + isPublic: boolean; + onChangeConversationAccess: (access: ConversationAccess) => Promise; +}) { const { euiTheme } = useEuiTheme(); - const [selectedValue, setSelectedValue] = useState('private'); + const [selectedValue, setSelectedValue] = useState( + isPublic ? ConversationAccess.SHARED : ConversationAccess.PRIVATE + ); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const options: Array> = [ { - key: 'private', + key: ConversationAccess.PRIVATE, label: privateLabel, - checked: !selectedValue || selectedValue === 'private' ? 'on' : undefined, + checked: !selectedValue || selectedValue === ConversationAccess.PRIVATE ? 'on' : undefined, description: i18n.translate('xpack.aiAssistant.chatHeader.shareOptions.privateDescription', { defaultMessage: 'This conversation is only visible to you.', }), }, { - key: 'shared', + key: ConversationAccess.SHARED, label: sharedLabel, - checked: selectedValue === 'shared' ? 'on' : undefined, + checked: selectedValue === ConversationAccess.SHARED ? 'on' : undefined, description: i18n.translate('xpack.aiAssistant.chatHeader.shareOptions.sharedDescription', { defaultMessage: 'Team members can view this conversation.', }), @@ -74,12 +86,17 @@ export function ChatSharingMenu() { setIsPopoverOpen(!isPopoverOpen)} - onClickAriaLabel="Toggle sharing options" + onClickAriaLabel={i18n.translate( + 'xpack.aiAssistant.chatHeader.shareOptions.toggleOptionsBadge', + { + defaultMessage: 'Toggle sharing options', + } + )} > - {selectedValue === 'shared' ? sharedLabel : privateLabel} + {selectedValue === ConversationAccess.SHARED ? sharedLabel : privateLabel} } @@ -89,13 +106,18 @@ export function ChatSharingMenu() { anchorPosition="downCenter" > { const selectedOption = newOptions.find((option) => option.checked === 'on'); - if (selectedOption) setSelectedValue(selectedOption.key as string); + if (selectedOption) { + setSelectedValue(selectedOption.key as ConversationAccess); + onChangeConversationAccess(selectedOption.key as ConversationAccess); + } setIsPopoverOpen(false); }} listProps={{ diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/conversation/conversation_view.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/conversation/conversation_view.tsx index fdfa310da13ea..e3220a611809b 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/conversation/conversation_view.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/conversation/conversation_view.tsx @@ -152,7 +152,6 @@ export const ConversationView: React.FC = ({ refreshConversations={refreshConversations} updateDisplayedConversation={updateDisplayedConversation} /> - {!chatService.value ? ( diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_conversation.ts b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_conversation.ts index d65fa19991334..77ea18cb6aa07 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_conversation.ts +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_conversation.ts @@ -15,6 +15,8 @@ import type { import type { ObservabilityAIAssistantChatService } from '@kbn/observability-ai-assistant-plugin/public'; import type { AbortableAsyncState } from '@kbn/observability-ai-assistant-plugin/public'; import type { UseChatResult } from '@kbn/observability-ai-assistant-plugin/public'; +import { AuthenticatedUser } from '@kbn/security-plugin-types-common'; +import { ConversationAccess } from '@kbn/observability-ai-assistant-plugin/public'; import { EMPTY_CONVERSATION_TITLE } from '../i18n'; import { useAIAssistantAppService } from './use_ai_assistant_app_service'; import { useKibana } from './use_kibana'; @@ -38,6 +40,7 @@ function createNewConversation({ } export interface UseConversationProps { + currentUser?: Pick; initialConversationId?: string; initialMessages?: Message[]; initialTitle?: string; @@ -48,12 +51,17 @@ export interface UseConversationProps { export type UseConversationResult = { conversation: AbortableAsyncState; + conversationId?: string; + user?: Pick; + isConversationOwnedByCurrentUser: boolean; saveTitle: (newTitle: string) => void; + updateConversationAccess: (access: ConversationAccess) => Promise; } & Omit; const DEFAULT_INITIAL_MESSAGES: Message[] = []; export function useConversation({ + currentUser, initialConversationId: initialConversationIdFromProps, initialMessages: initialMessagesFromProps = DEFAULT_INITIAL_MESSAGES, initialTitle: initialTitleFromProps, @@ -75,6 +83,8 @@ export function useConversation({ const initialMessages = useOnce(initialMessagesFromProps); const initialTitle = useOnce(initialTitleFromProps); + const [displayedConversationId, setDisplayedConversationId] = useState(initialConversationId); + if (initialMessages.length && initialConversationId) { throw new Error('Cannot set initialMessages if initialConversationId is set'); } @@ -110,6 +120,45 @@ export function useConversation({ }); }; + const updateConversationAccess = async (access: ConversationAccess) => { + if (!displayedConversationId || !conversation.value) { + throw new Error('Cannot share the conversation if conversation is not stored'); + } + + try { + const sharedConversation = await service.callApi( + `PUT /internal/observability_ai_assistant/conversation/{conversationId}/access`, + { + signal: null, + params: { + path: { + conversationId: displayedConversationId, + }, + body: { + access, + }, + }, + } + ); + + notifications!.toasts.addSuccess({ + title: i18n.translate('xpack.aiAssistant.updateConversationAccessSuccessToast', { + defaultMessage: 'Conversation access successfully updated to {access}', + values: { access }, + }), + }); + + return sharedConversation; + } catch (err) { + notifications!.toasts.addError(err, { + title: i18n.translate('xpack.aiAssistant.updateConversationAccessErrorToast', { + defaultMessage: 'Could not share conversation', + }), + }); + throw err; + } + }; + const { next, messages, setMessages, state, stop } = useChat({ initialMessages, initialConversationId, @@ -125,8 +174,6 @@ export function useConversation({ scopes, }); - const [displayedConversationId, setDisplayedConversationId] = useState(initialConversationId); - const conversation: AbortableAsyncState = useAbortableAsync( ({ signal }) => { @@ -161,8 +208,32 @@ export function useConversation({ } ); + const isConversationOwnedByUser = (conversationUser: Conversation['user']): boolean => { + if (!initialConversationId) return true; + + if (!conversationUser || !currentUser) return false; + + return conversationUser.id && currentUser.profile_uid + ? conversationUser.id === currentUser.profile_uid + : conversationUser.name === currentUser.username; + }; + return { conversation, + conversationId: + conversation.value?.conversation && 'id' in conversation.value.conversation + ? conversation.value.conversation.id + : undefined, + isConversationOwnedByCurrentUser: isConversationOwnedByUser( + conversation.value && 'user' in conversation.value ? conversation.value.user : undefined + ), + user: + initialConversationId && conversation.value?.conversation && 'user' in conversation.value + ? { + profile_uid: conversation.value.user?.id, + username: conversation.value.user?.name || '', + } + : undefined, state, next, stop, @@ -182,5 +253,6 @@ export function useConversation({ onConversationUpdate?.(nextConversation); }); }, + updateConversationAccess, }; } diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/index.ts index b5487a773e373..54ad61b818c91 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/common/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/common/index.ts @@ -6,7 +6,7 @@ */ export type { Message, Conversation, KnowledgeBaseEntry, ConversationCreateRequest } from './types'; -export { KnowledgeBaseEntryRole, MessageRole } from './types'; +export { KnowledgeBaseEntryRole, MessageRole, ConversationAccess } from './types'; export type { FunctionDefinition, CompatibleJSONSchema } from './functions/types'; export { FunctionVisibility } from './functions/function_visibility'; export { diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/common/types.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/common/types.ts index 2c004a2360b06..b7254d040858b 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/common/types.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/common/types.ts @@ -153,3 +153,8 @@ export interface ObservabilityAIAssistantScreenContext { actions?: Array>; starterPrompts?: StarterPrompt[]; } + +export enum ConversationAccess { + SHARED = 'shared', + PRIVATE = 'private', +} diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts index 156932c7e75a2..c99a4ce206659 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts @@ -66,7 +66,10 @@ export { KnowledgeBaseEntryRole, concatenateChatCompletionChunks, StreamingChatResponseEventType, + ConversationAccess, + KnowledgeBaseType, } from '../common'; + export type { CompatibleJSONSchema, Conversation, @@ -77,8 +80,6 @@ export type { ShortIdTable, } from '../common'; -export { KnowledgeBaseType } from '../common'; - export type { TelemetryEventTypeWithPayload } from './analytics'; export { ObservabilityAIAssistantTelemetryEventType } from './analytics/telemetry_event_type'; diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/conversations/route.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/conversations/route.ts index cd2bb5b6331cd..4998ff5291850 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/conversations/route.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/routes/conversations/route.ts @@ -6,7 +6,7 @@ */ import { notImplemented } from '@hapi/boom'; import * as t from 'io-ts'; -import { Conversation, MessageRole } from '../../../common/types'; +import { Conversation, ConversationAccess, MessageRole } from '../../../common/types'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; import { conversationCreateRt, conversationUpdateRt } from '../runtime_types'; @@ -191,6 +191,39 @@ const deleteConversationRoute = createObservabilityAIAssistantServerRoute({ }, }); +const updateConversationAccessRoute = createObservabilityAIAssistantServerRoute({ + endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}/access', + params: t.type({ + path: t.type({ + conversationId: t.string, + }), + body: t.type({ + access: t.string, + }), + }), + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, + }, + handler: async (resources): Promise => { + const { service, request, params } = resources; + + const client = await service.getClient({ request }); + + if (!client) { + throw notImplemented(); + } + + const conversation = await client.updateAccess({ + conversationId: params.path.conversationId, + access: params.body.access as ConversationAccess, + }); + + return Promise.resolve(conversation); + }, +}); + export const conversationRoutes = { ...getConversationRoute, ...findConversationsRoute, @@ -198,4 +231,5 @@ export const conversationRoutes = { ...updateConversationRoute, ...updateConversationTitle, ...deleteConversationRoute, + ...updateConversationAccessRoute, }; diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts index 2e2dddfbb08fe..59475d028d1cd 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts @@ -54,6 +54,7 @@ import { type Message, KnowledgeBaseType, KnowledgeBaseEntryRole, + ConversationAccess, } from '../../../common/types'; import { CONTEXT_FUNCTION_NAME } from '../../functions/context'; import type { ChatFunctionClient } from '../chat_function_client'; @@ -604,6 +605,42 @@ export class ObservabilityAIAssistantClient { return createdConversation; }; + updateAccess = async ({ + conversationId, + access, + }: { + conversationId: string; + access: ConversationAccess; + }) => { + const document = await this.getConversationWithMetaFields(conversationId); + if (!document) { + throw notFound(); + } + + const conversation = await this.get(conversationId); + if (!conversation) { + throw notFound(); + } + + const isPublic = access === ConversationAccess.SHARED; + + const updatedConversation: Conversation = merge({}, conversation, { + public: isPublic, + conversation: { + last_updated: new Date().toISOString(), + }, + }); + + await this.dependencies.esClient.asInternalUser.update({ + id: document._id!, + index: document._index, + doc: updatedConversation, + refresh: true, + }); + + return updatedConversation; + }; + recall = async ({ queries, categories,