Skip to content

Commit

Permalink
update conversation duplicate action in app to allow using same assis…
Browse files Browse the repository at this point in the history
…tants or copies of assistants (#275)
  • Loading branch information
bkrabach authored Dec 1, 2024
1 parent d9b5716 commit ff55fcf
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 25 deletions.
19 changes: 18 additions & 1 deletion workbench-app/src/components/App/DialogControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,20 @@ import {
DialogSurface,
DialogTitle,
DialogTrigger,
makeStyles,
mergeClasses,
tokens,
} from '@fluentui/react-components';
import React from 'react';

const useClasses = makeStyles({
dialogContent: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalM,
},
});

export interface DialogControlContent {
open?: boolean;
defaultOpen?: boolean;
Expand Down Expand Up @@ -42,13 +53,19 @@ export const DialogControl: React.FC<DialogControlContent> = (props) => {
onOpenChange,
} = props;

const classes = useClasses();

return (
<Dialog open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
<DialogTrigger disableButtonEnhancement>{trigger}</DialogTrigger>
<DialogSurface className={classNames?.dialogSurface}>
<DialogBody>
{title && <DialogTitle>{title}</DialogTitle>}
{content && <DialogContent className={classNames?.dialogContent}>{content}</DialogContent>}
{content && (
<DialogContent className={mergeClasses(classes.dialogContent, classNames?.dialogContent)}>
{content}
</DialogContent>
)}
<DialogActions fluid>
{!hideDismissButton && (
<DialogTrigger disableButtonEnhancement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const AssistantDuplicate: React.FC<AssistantDuplicateProps> = (props) =>
setSubmitted(true);

try {
const newAssistantId = await workbenchService.duplicateAssistantAsync(assistant.id);
const newAssistantId = await workbenchService.exportThenImportAssistantAsync(assistant.id);
onDuplicate?.(newAssistantId);
} catch (error) {
onDuplicateError?.(error as Error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,107 @@
// Copyright (c) Microsoft. All rights reserved.

import { Button, DialogOpenChangeData, DialogOpenChangeEvent, DialogTrigger } from '@fluentui/react-components';
import {
Button,
DialogOpenChangeData,
DialogOpenChangeEvent,
DialogTrigger,
Field,
Input,
Radio,
RadioGroup,
} from '@fluentui/react-components';
import { SaveCopy24Regular } from '@fluentui/react-icons';
import React from 'react';
import { useNotify } from '../../libs/useNotify';
import { useWorkbenchService } from '../../libs/useWorkbenchService';
import { Utility } from '../../libs/Utility';
import { useDuplicateConversationMutation } from '../../services/workbench';
import { CommandButton } from '../App/CommandButton';
import { DialogControl } from '../App/DialogControl';

const enum AssistantParticipantOption {
SameAssistants = 'Include the same assistants in the new conversation.',
CloneAssistants = 'Create copies of the assistants in the new conversation.',
}

const useConversationDuplicateControls = (id: string) => {
const workbenchService = useWorkbenchService();
const [assistantParticipantOption, setAssistantParticipantOption] = React.useState<AssistantParticipantOption>(
AssistantParticipantOption.SameAssistants,
);
const [duplicateConversation] = useDuplicateConversationMutation();
const [submitted, setSubmitted] = React.useState(false);
const [title, setTitle] = React.useState('');

const duplicateConversation = React.useCallback(
const handleDuplicateConversation = React.useCallback(
async (onDuplicate?: (conversationId: string) => Promise<void>, onError?: (error: Error) => void) => {
try {
await Utility.withStatus(setSubmitted, async () => {
const duplicates = await workbenchService.duplicateConversationsAsync([id]);
await onDuplicate?.(duplicates[0]);
switch (assistantParticipantOption) {
case AssistantParticipantOption.SameAssistants:
const results = await duplicateConversation({ id, title }).unwrap();
if (results.conversationIds.length === 0) {
throw new Error('No conversation ID returned');
}
await onDuplicate?.(results.conversationIds[0]);
break;
case AssistantParticipantOption.CloneAssistants:
const duplicateIds = await workbenchService.exportThenImportConversationAsync([id]);
await onDuplicate?.(duplicateIds[0]);
break;
}
});
} catch (error) {
onError?.(error as Error);
}
},
[id, workbenchService],
[assistantParticipantOption, duplicateConversation, id, title, workbenchService],
);

const duplicateConversationForm = React.useCallback(
() => <p>Are you sure you want to duplicate this conversation?</p>,
[],
() => (
<>
<Field label="Title" required={true}>
<Input
value={title}
onChange={(_, data) => setTitle(data.value)}
required={true}
placeholder="Enter a title for the duplicated conversation"
/>
</Field>
<Field label="Assistant Duplication Options" required={true}>
<RadioGroup
defaultValue={assistantParticipantOption}
onChange={(_, data) => setAssistantParticipantOption(data.value as AssistantParticipantOption)}
required={true}
>
<Radio
value={AssistantParticipantOption.SameAssistants}
label={AssistantParticipantOption.SameAssistants}
/>
<Radio
value={AssistantParticipantOption.CloneAssistants}
label={AssistantParticipantOption.CloneAssistants}
/>
</RadioGroup>
</Field>
</>
),
[assistantParticipantOption, title],
);

const duplicateConversationButton = React.useCallback(
(onDuplicate?: (conversationId: string) => Promise<void>, onError?: (error: Error) => void) => (
<Button
key="duplicate"
appearance="primary"
onClick={() => duplicateConversation(onDuplicate, onError)}
onClick={() => handleDuplicateConversation(onDuplicate, onError)}
disabled={submitted}
>
{submitted ? 'Duplicating...' : 'Duplicate'}
</Button>
),
[duplicateConversation, submitted],
[handleDuplicateConversation, submitted],
);

return {
Expand Down
8 changes: 4 additions & 4 deletions workbench-app/src/libs/useWorkbenchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export const useWorkbenchService = () => {
[dispatch, environment.url, tryFetchAsync],
);

const duplicateConversationsAsync = React.useCallback(
const exportThenImportConversationAsync = React.useCallback(
async (conversationIds: string[]) => {
const { blob, filename } = await exportConversationsAsync(conversationIds);
const result = await importConversationsAsync(new File([blob], filename));
Expand All @@ -289,7 +289,7 @@ export const useWorkbenchService = () => {
[tryFetchFileAsync],
);

const duplicateAssistantAsync = React.useCallback(
const exportThenImportAssistantAsync = React.useCallback(
async (assistantId: string) => {
const { blob, filename } = await exportAssistantAsync(assistantId);
const result = await importConversationsAsync(new File([blob], filename));
Expand Down Expand Up @@ -351,9 +351,9 @@ export const useWorkbenchService = () => {
exportTranscriptAsync,
exportConversationsAsync,
importConversationsAsync,
duplicateConversationsAsync,
exportThenImportConversationAsync,
exportAssistantAsync,
duplicateAssistantAsync,
exportThenImportAssistantAsync,
getAssistantServiceInfoAsync,
getAssistantServiceInfosAsync,
};
Expand Down
2 changes: 1 addition & 1 deletion workbench-app/src/routes/ShareRedeem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const ShareRedeem: React.FC = () => {
const redemption = await redeemShare(conversationShare.id).unwrap();

// duplicate it
const duplicatedConversationIds = await workbenchService.duplicateConversationsAsync([
const duplicatedConversationIds = await workbenchService.exportThenImportConversationAsync([
redemption.conversationId,
]);

Expand Down
24 changes: 24 additions & 0 deletions workbench-app/src/services/workbench/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export const conversationApi = workbenchApi.injectEndpoints({
invalidatesTags: ['Conversation'],
transformResponse: (response: any) => transformResponseToConversation(response),
}),
duplicateConversation: builder.mutation<
{ conversationIds: string[]; assistantIds: string[] },
Pick<Conversation, 'id' | 'title' | 'metadata'>
>({
query: (body) => ({
url: `/conversations/${body.id}`,
method: 'POST',
body: transformConversationForRequest(body),
}),
invalidatesTags: ['Conversation'],
transformResponse: (response: any) => transformResponseToImportResult(response),
}),
updateConversation: builder.mutation<Conversation, Pick<Conversation, 'id' | 'title' | 'metadata'>>({
query: (body) => ({
url: `/conversations/${body.id}`,
Expand Down Expand Up @@ -163,6 +175,7 @@ export const updateGetConversationMessagesQueryData = (conversationId: string, d

export const {
useCreateConversationMutation,
useDuplicateConversationMutation,
useUpdateConversationMutation,
useGetConversationsQuery,
useGetAssistantConversationsQuery,
Expand Down Expand Up @@ -203,6 +216,17 @@ const transformResponseToConversation = (response: any): Conversation => {
}
};

const transformResponseToImportResult = (response: any): { conversationIds: string[]; assistantIds: string[] } => {
try {
return {
conversationIds: response.conversation_ids,
assistantIds: response.assistant_ids,
};
} catch (error) {
throw new Error(`Failed to transform import result response: ${error}`);
}
};

const transformResponseToConversationMessages = (response: any): ConversationMessage[] => {
try {
return response.messages.map(transformResponseToMessage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -873,22 +873,62 @@ async def duplicate_conversation(
.where(db.ConversationMessage.conversation_id == conversation_id)
.order_by(col(db.ConversationMessage.sequence))
)
message_id_old_to_new = {}
for message in messages:
new_message_id = uuid.uuid4()
message_id_old_to_new[message.message_id] = new_message_id
new_message = db.ConversationMessage(
# Do not set 'sequence'; let the database assign it
message_id=uuid.uuid4(), # Generate a new unique message ID
**message.model_dump(exclude={"message_id", "conversation_id", "sequence"}),
message_id=new_message_id,
conversation_id=conversation.conversation_id,
created_datetime=message.created_datetime,
sender_participant_id=message.sender_participant_id,
sender_participant_role=message.sender_participant_role,
message_type=message.message_type,
content=message.content,
content_type=message.content_type,
meta_data=message.meta_data.copy(),
filenames=message.filenames.copy(),
)
session.add(new_message)

# Copy message debug data from the original conversation
for old_message_id, new_message_id in message_id_old_to_new.items():
message_debugs = await session.exec(
select(db.ConversationMessageDebug).where(db.ConversationMessageDebug.message_id == old_message_id)
)
for debug in message_debugs:
new_debug = db.ConversationMessageDebug(
**debug.model_dump(exclude={"message_id"}),
message_id=new_message_id,
)
session.add(new_debug)

# Copy File entries associated with the conversation
files = await session.exec(
select(db.File)
.where(db.File.conversation_id == original_conversation.conversation_id)
.order_by(col(db.File.created_datetime).asc())
)

file_id_old_to_new = {}
for file in files:
new_file_id = uuid.uuid4()
file_id_old_to_new[file.file_id] = new_file_id
new_file = db.File(
**file.model_dump(exclude={"file_id", "conversation_id"}),
file_id=new_file_id,
conversation_id=conversation.conversation_id,
)
session.add(new_file)

# Copy FileVersion entries associated with the files
for old_file_id, new_file_id in file_id_old_to_new.items():
file_versions = await session.exec(
select(db.FileVersion)
.where(db.FileVersion.file_id == old_file_id)
.order_by(col(db.FileVersion.version).asc())
)
for version in file_versions:
new_version = db.FileVersion(
**version.model_dump(exclude={"file_id"}),
file_id=new_file_id,
)
session.add(new_version)

# Copy files associated with the conversation
original_files_path = self._file_storage.path_for(
namespace=str(original_conversation.conversation_id), filename=""
Expand Down

0 comments on commit ff55fcf

Please sign in to comment.