diff --git a/libraries/python/assistant-extensions/assistant_extensions/workflows/_model.py b/libraries/python/assistant-extensions/assistant_extensions/workflows/_model.py
index a5c33a9a..b8aa4d85 100644
--- a/libraries/python/assistant-extensions/assistant_extensions/workflows/_model.py
+++ b/libraries/python/assistant-extensions/assistant_extensions/workflows/_model.py
@@ -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 = {
@@ -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"}}),
] = []
diff --git a/libraries/python/assistant-extensions/assistant_extensions/workflows/runners/_user_proxy.py b/libraries/python/assistant-extensions/assistant_extensions/workflows/runners/_user_proxy.py
index fc92d29c..e7503644 100644
--- a/libraries/python/assistant-extensions/assistant_extensions/workflows/runners/_user_proxy.py
+++ b/libraries/python/assistant-extensions/assistant_extensions/workflows/runners/_user_proxy.py
@@ -8,6 +8,7 @@
ConversationMessage,
MessageSender,
MessageType,
+ NewConversation,
NewConversationMessage,
UpdateParticipant,
)
@@ -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(
@@ -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
@@ -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"},
)
@@ -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..."
)
)
@@ -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"},
)
)
diff --git a/libraries/python/semantic-workbench-api-model/semantic_workbench_api_model/workbench_service_client.py b/libraries/python/semantic-workbench-api-model/semantic_workbench_api_model/workbench_service_client.py
index 3a8ac757..9a571e63 100644
--- a/libraries/python/semantic-workbench-api-model/semantic_workbench_api_model/workbench_service_client.py
+++ b/libraries/python/semantic-workbench-api-model/semantic_workbench_api_model/workbench_service_client.py
@@ -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())
diff --git a/workbench-app/docs/MESSAGE_METADATA.md b/workbench-app/docs/MESSAGE_METADATA.md
index b615a1de..c17b93fa 100644
--- a/workbench-app/docs/MESSAGE_METADATA.md
+++ b/workbench-app/docs/MESSAGE_METADATA.md
@@ -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.
diff --git a/workbench-app/src/components/Conversations/ConversationRemove.tsx b/workbench-app/src/components/Conversations/ConversationRemove.tsx
index 8b081e68..d3c49aac 100644
--- a/workbench-app/src/components/Conversations/ConversationRemove.tsx
+++ b/workbench-app/src/components/Conversations/ConversationRemove.tsx
@@ -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);
@@ -42,16 +45,21 @@ const useConversationRemoveControls = () => {
);
const removeConversationForm = React.useCallback(
- () =>
Are you sure you want to remove this conversation from your list?