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

workflow-lite, duplicate conversation, conversation list updates #265

Merged
merged 4 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@
from semantic_workbench_assistant.config import UISchema


class UserMessage(BaseModel):
class Config:
json_schema_extra = {
"required": ["status_label", "message"],
}

status_label: Annotated[
str,
Field(
description="The status label to be displayed when the message is sent to the assistant.",
),
] = ""

message: Annotated[
str,
Field(
description="The message to be sent to the assistant.",
),
UISchema(widget="textarea"),
] = ""


class UserProxyWorkflowDefinition(BaseModel):
class Config:
json_schema_extra = {
Expand Down Expand Up @@ -37,11 +59,10 @@ class Config:
UISchema(widget="textarea"),
] = ""
user_messages: Annotated[
list[str],
list[UserMessage],
Field(
description="A list of user messages that will be sequentially sent to the assistant during the workflow.",
),
UISchema(schema={"items": {"widget": "textarea"}}),
] = []


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ConversationMessage,
MessageSender,
MessageType,
NewConversation,
NewConversationMessage,
UpdateParticipant,
)
Expand Down Expand Up @@ -101,7 +102,7 @@ async def run(
)

# duplicate the current conversation and get the context
workflow_context = await self.duplicate_conversation(context)
workflow_context = await self.duplicate_conversation(context, workflow_definition)

# set the current workflow id
workflow_state = WorkflowState(
Expand Down Expand Up @@ -156,21 +157,41 @@ async def _listen_for_events(
continue
await self._on_assistant_message(context, workflow_state, message)

async def duplicate_conversation(self, context: ConversationContext) -> ConversationContext:
async def duplicate_conversation(
self, context: ConversationContext, workflow_definition: UserProxyWorkflowDefinition
) -> ConversationContext:
"""
Duplicate the current conversation
"""

title = f"Workflow: {workflow_definition.name} [{context.title}]"

# duplicate the current conversation
response = await context._workbench_client.duplicate_conversation()
response = await context._workbench_client.duplicate_conversation(
new_conversation=NewConversation(
title=title,
metadata={"parent_conversation_id": context.id},
)
)

conversation_id = response.conversation_ids[0]

# create a new conversation context
workflow_context = ConversationContext(
id=str(response.conversation_ids[0]),
title="Workflow",
id=str(conversation_id),
title=title,
assistant=context.assistant,
)

# send link to chat for the new conversation
await context.send_messages(
NewConversationMessage(
content=f"New conversation: {title}",
message_type=MessageType.command_response,
metadata={"attribution": "workflows:user_proxy", "href": f"/{conversation_id}"},
)
)

# return the new conversation context
return workflow_context

Expand All @@ -187,7 +208,7 @@ async def _start_step(self, context: ConversationContext, workflow_state: Workfl
await workflow_state.context.send_messages(
NewConversationMessage(
sender=workflow_state.send_as,
content=user_message,
content=user_message.message,
message_type=MessageType.chat,
metadata={"attribution": "user"},
)
Expand All @@ -199,7 +220,7 @@ async def _start_step(self, context: ConversationContext, workflow_state: Workfl
# )
await context.update_participant_me(
UpdateParticipant(
status=f"Workflow {workflow_state.definition.name}: Step {workflow_state.current_step}, awaiting assistant response..."
status=f"Workflow {workflow_state.definition.name} [Step {workflow_state.current_step} - {user_message.status_label}]: awaiting assistant response..."
)
)

Expand Down Expand Up @@ -258,7 +279,7 @@ async def _send_final_response(
NewConversationMessage(
content=assistant_response.content,
message_type=MessageType.chat,
metadata={"attribution": "system"},
metadata={"attribution": "workflows:user_proxy"},
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,14 @@ async def delete_conversation(self) -> None:
return
http_response.raise_for_status()

async def duplicate_conversation(self) -> workbench_model.ConversationImportResult:
async def duplicate_conversation(
self, new_conversation: workbench_model.NewConversation
) -> workbench_model.ConversationImportResult:
async with self._client as client:
http_response = await client.post(f"/conversations/duplicate?id={self._conversation_id}")
http_response = await client.post(
f"/conversations/{self._conversation_id}",
json=new_conversation.model_dump(exclude_defaults=True, exclude_unset=True, mode="json"),
)
http_response.raise_for_status()
return workbench_model.ConversationImportResult.model_validate(http_response.json())

Expand Down
2 changes: 2 additions & 0 deletions workbench-app/docs/MESSAGE_METADATA.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The app has built-in support for a few metadata child properties, which can be u

- `attribution`: A string that will be displayed after the sender of the message. The intent is to allow the sender to indicate the source of the message, possibly coming from an internal part of its system.

- `href`: If provided, the app will display the message as a hyperlink. The value of this property will be used as the URL of the hyperlink and use the React Router navigation system to navigate to the URL when the user clicks on the message. Will be ignored for messages of type `chat`.

- `debug`: A dictionary that can contain additional information that can be used for debugging purposes. If included, it will cause the app to display a button that will allow the user to see the contents of the dictionary in a popup for further inspection.

- `footer_items`: A list of strings that will be displayed in the footer of the message. The intent is to allow the sender to include additional information that is not part of the message body, but is still relevant to the message.
Expand Down
61 changes: 38 additions & 23 deletions workbench-app/src/components/Conversations/ConversationRemove.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,25 @@ const useConversationRemoveControls = () => {
const [submitted, setSubmitted] = React.useState(false);

const handleRemove = React.useCallback(
async (conversationId: string, participantId: string, onRemove?: () => void) => {
async (conversations: Conversation[], participantId: string, onRemove?: () => void) => {
if (submitted) {
return;
}
setSubmitted(true);

try {
if (activeConversationId === conversationId) {
// Clear the active conversation if it is the one being removed
dispatch(setActiveConversationId(undefined));
}
for (const conversation of conversations) {
const conversationId = conversation.id;
if (activeConversationId === conversationId) {
// Clear the active conversation if it is the one being removed
dispatch(setActiveConversationId(undefined));
}

await removeConversationParticipant({
conversationId,
participantId,
});
await removeConversationParticipant({
conversationId,
participantId,
});
}
onRemove?.();
} finally {
setSubmitted(false);
Expand All @@ -42,16 +45,21 @@ const useConversationRemoveControls = () => {
);

const removeConversationForm = React.useCallback(
() => <p>Are you sure you want to remove this conversation from your list?</p>,
(hasMultipleConversations: boolean) =>
hasMultipleConversations ? (
<p>Are you sure you want to remove these conversations from your list ?</p>
) : (
<p>Are you sure you want to remove this conversation from your list ?</p>
),
[],
);

const removeConversationButton = React.useCallback(
(conversationId: string, participantId: string, onRemove?: () => void) => (
(conversations: Conversation[], participantId: string, onRemove?: () => void) => (
<DialogTrigger disableButtonEnhancement>
<Button
appearance="primary"
onClick={() => handleRemove(conversationId, participantId, onRemove)}
onClick={() => handleRemove(conversations, participantId, onRemove)}
disabled={submitted}
>
{submitted ? 'Removing...' : 'Remove'}
Expand All @@ -68,51 +76,58 @@ const useConversationRemoveControls = () => {
};

interface ConversationRemoveDialogProps {
conversationId: string;
conversations: Conversation | Conversation[];
participantId: string;
onRemove: () => void;
onCancel: () => void;
}

export const ConversationRemoveDialog: React.FC<ConversationRemoveDialogProps> = (props) => {
const { conversationId, participantId, onRemove, onCancel } = props;
const { conversations, participantId, onRemove, onCancel } = props;
const { removeConversationForm, removeConversationButton } = useConversationRemoveControls();

const hasMultipleConversations = Array.isArray(conversations);
const conversationsToRemove = hasMultipleConversations ? conversations : [conversations];

return (
<DialogControl
open={true}
onOpenChange={onCancel}
title="Remove Conversation"
content={removeConversationForm()}
additionalActions={[removeConversationButton(conversationId, participantId, onRemove)]}
title={hasMultipleConversations ? 'Remove Conversations' : 'Remove Conversation'}
content={removeConversationForm(hasMultipleConversations)}
additionalActions={[removeConversationButton(conversationsToRemove, participantId, onRemove)]}
/>
);
};

interface ConversationRemoveProps {
conversation: Conversation;
conversations: Conversation | Conversation[];
participantId: string;
onRemove?: () => void;
iconOnly?: boolean;
asToolbarButton?: boolean;
}

export const ConversationRemove: React.FC<ConversationRemoveProps> = (props) => {
const { conversation, onRemove, iconOnly, asToolbarButton, participantId } = props;
const { conversations, onRemove, iconOnly, asToolbarButton, participantId } = props;
const { removeConversationForm, removeConversationButton } = useConversationRemoveControls();

const hasMultipleConversations = Array.isArray(conversations);
const conversationsToRemove = hasMultipleConversations ? conversations : [conversations];
const description = hasMultipleConversations ? 'Remove Conversations' : 'Remove Conversation';

return (
<CommandButton
description="Remove Conversation"
description={description}
icon={<PlugDisconnected24Regular />}
iconOnly={iconOnly}
asToolbarButton={asToolbarButton}
label="Remove"
dialogContent={{
title: 'Remove Conversation',
content: removeConversationForm(),
title: description,
content: removeConversationForm(hasMultipleConversations),
closeLabel: 'Cancel',
additionalActions: [removeConversationButton(conversation.id, participantId, onRemove)],
additionalActions: [removeConversationButton(conversationsToRemove, participantId, onRemove)],
}}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
TextBulletListSquareSparkleRegular,
} from '@fluentui/react-icons';
import React from 'react';
import { Link } from 'react-router-dom';
import { useConversationUtility } from '../../libs/useConversationUtility';
import { useParticipantUtility } from '../../libs/useParticipantUtility';
import { Utility } from '../../libs/Utility';
Expand Down Expand Up @@ -261,6 +262,7 @@ export const InteractMessage: React.FC<InteractMessageProps> = (props) => {
);

const getRenderedMessage = React.useCallback(() => {
let allowLink = true;
let renderedContent: JSX.Element;
if (message.messageType === 'notice') {
renderedContent = (
Expand Down Expand Up @@ -299,11 +301,17 @@ export const InteractMessage: React.FC<InteractMessageProps> = (props) => {
</div>
);
} else if (isUser) {
allowLink = false;
renderedContent = <UserMessage>{content}</UserMessage>;
} else {
allowLink = false;
renderedContent = <CopilotMessage>{content}</CopilotMessage>;
}

if (message.metadata?.href && allowLink) {
renderedContent = <Link to={message.metadata?.href}>{renderedContent}</Link>;
}

const attachmentList =
message.filenames && message.filenames.length > 0 ? (
<AttachmentList className={classes.attachments}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const MyConversations: React.FC<MyConversationsProps> = (props) => {
<ConversationDuplicate conversationId={conversation.id} iconOnly />
<ConversationShare conversation={conversation} iconOnly />
<ConversationRemove
conversation={conversation}
conversations={conversation}
participantId={participantId}
iconOnly
/>
Expand Down
Loading