Skip to content

Commit 6cf0d4f

Browse files
authored
Workflow lite (#256)
Early merge of workflow-lite experiment. This helped expose an issue with SSE event listeners, so merging this to go fix that separately. Will further develop this experimental feature after the event fix and early usage.
1 parent bd4d47c commit 6cf0d4f

20 files changed

Lines changed: 1093 additions & 222 deletions

File tree

assistants/explorer-assistant/assistant/chat.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from assistant_extensions.artifacts import ArtifactsExtension
1616
from assistant_extensions.artifacts._model import ArtifactsConfigModel
1717
from assistant_extensions.attachments import AttachmentsExtension
18+
from assistant_extensions.workflows import WorkflowsConfigModel, WorkflowsExtension
1819
from content_safety.evaluators import CombinedContentSafetyEvaluator
1920
from openai.types.chat import (
2021
ChatCompletion,
@@ -79,12 +80,17 @@ async def content_evaluator_factory(context: ConversationContext) -> ContentSafe
7980
)
8081

8182

82-
async def artifact_config_provider(context: AssistantContext) -> ArtifactsConfigModel:
83+
async def artifacts_config_provider(context: AssistantContext) -> ArtifactsConfigModel:
8384
return (await assistant_config.get(context)).extensions_config.artifacts
8485

8586

86-
artifacts_extension = ArtifactsExtension(assistant, artifact_config_provider)
87+
async def workflows_config_provider(context: AssistantContext) -> WorkflowsConfigModel:
88+
return (await assistant_config.get(context)).extensions_config.workflows
89+
90+
91+
artifacts_extension = ArtifactsExtension(assistant, artifacts_config_provider)
8792
attachments_extension = AttachmentsExtension(assistant)
93+
workflows_extension = WorkflowsExtension(assistant, "content_safety", workflows_config_provider)
8894

8995
#
9096
# create the FastAPI app instance
@@ -134,7 +140,7 @@ async def on_message_created(
134140
metadata: dict[str, Any] = {"debug": {"content_safety": event.data.get(content_safety.metadata_key, {})}}
135141

136142
# Prospector assistant response
137-
await respond_to_conversation(context, config, message, metadata)
143+
await respond_to_conversation(context, config, metadata)
138144

139145

140146
@assistant.events.conversation.on_created
@@ -172,7 +178,6 @@ async def on_conversation_created(context: ConversationContext) -> None:
172178
async def respond_to_conversation(
173179
context: ConversationContext,
174180
config: AssistantConfigModel,
175-
message: ConversationMessage,
176181
metadata: dict[str, Any] = {},
177182
) -> None:
178183
"""

assistants/explorer-assistant/assistant/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import openai_client
44
from assistant_extensions.artifacts import ArtifactsConfigModel
55
from assistant_extensions.attachments import AttachmentsConfigModel
6+
from assistant_extensions.workflows import WorkflowsConfigModel
67
from content_safety.evaluators import CombinedContentSafetyEvaluatorConfig
78
from pydantic import BaseModel, ConfigDict, Field
89
from semantic_workbench_assistant.config import UISchema
@@ -25,6 +26,14 @@
2526

2627

2728
class ExtensionsConfigModel(BaseModel):
29+
workflows: Annotated[
30+
WorkflowsConfigModel,
31+
Field(
32+
title="Workflows Extension Configuration",
33+
description="Configuration for the workflows extension.",
34+
),
35+
] = WorkflowsConfigModel()
36+
2837
attachments: Annotated[
2938
AttachmentsConfigModel,
3039
Field(

assistants/explorer-assistant/uv.lock

Lines changed: 115 additions & 100 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assistants/prospector-assistant/assistant/chat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ async def send_error_message_on_exception(context: ConversationContext):
213213
# endregion
214214

215215
#
216-
# region Form fill extension helpers
216+
# region Form Fill Extension Helpers
217217
#
218218

219219

@@ -262,7 +262,7 @@ async def get(filename: str) -> str:
262262

263263

264264
#
265-
# region document agent extension helpers
265+
# region Document Extension Helpers
266266
#
267267

268268

assistants/prospector-assistant/uv.lock

Lines changed: 174 additions & 98 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libraries/python/assistant-extensions/.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"**/__pycache__": true
3838
},
3939
"cSpell.words": [
40+
"deepmerge",
41+
"DMAIC",
4042
"endregion",
4143
"Excalidraw",
4244
"openai",

libraries/python/assistant-extensions/assistant_extensions/attachments/_attachments.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ async def _get_attachment_for_file(
330330
content = ""
331331
error = ""
332332
# process the file to create an attachment
333-
async with context.set_status(f"updating attachment {file.filename} ..."):
333+
async with context.set_status(f"updating attachment {file.filename}..."):
334334
try:
335335
# read the content of the file
336336
file_bytes = await _read_conversation_file(context, file)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from ._model import WorkflowsConfigModel
2+
from ._workflows import WorkflowsExtension, WorkflowsProcessingErrorHandler
3+
4+
__all__ = ["WorkflowsExtension", "WorkflowsConfigModel", "WorkflowsProcessingErrorHandler"]
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from typing import Annotated, Literal, Union
2+
3+
from pydantic import BaseModel, Field
4+
from semantic_workbench_assistant.config import UISchema
5+
6+
7+
class UserProxyWorkflowDefinition(BaseModel):
8+
class Config:
9+
json_schema_extra = {
10+
"required": ["command", "name", "description", "user_messages"],
11+
}
12+
13+
workflow_type: Annotated[
14+
Literal["user_proxy"],
15+
Field(
16+
description="The type of workflow.",
17+
),
18+
UISchema(widget="hidden"),
19+
] = "user_proxy"
20+
command: Annotated[
21+
str,
22+
Field(
23+
description="The command that will trigger the workflow. The command should be unique and not conflict with other commands and should only include alphanumeric characters and underscores.",
24+
),
25+
] = ""
26+
name: Annotated[
27+
str,
28+
Field(
29+
description="The name of the workflow, to be displayed in the help, logs, and status messages.",
30+
),
31+
] = ""
32+
description: Annotated[
33+
str,
34+
Field(
35+
description="A description of the workflow that will be displayed in the help.",
36+
),
37+
UISchema(widget="textarea"),
38+
] = ""
39+
user_messages: Annotated[
40+
list[str],
41+
Field(
42+
description="A list of user messages that will be sequentially sent to the assistant during the workflow.",
43+
),
44+
UISchema(schema={"items": {"widget": "textarea"}}),
45+
] = []
46+
47+
48+
WorkflowDefinition = Union[UserProxyWorkflowDefinition]
49+
50+
51+
class WorkflowsConfigModel(BaseModel):
52+
enabled: Annotated[
53+
bool,
54+
Field(
55+
description="Enable the workflows feature.",
56+
),
57+
] = False
58+
59+
workflow_definitions: Annotated[
60+
list[WorkflowDefinition],
61+
Field(
62+
description="A list of workflow definitions.",
63+
),
64+
] = []
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import asyncio
2+
import logging
3+
from typing import Any, Awaitable, Callable
4+
5+
import deepmerge
6+
from semantic_workbench_api_model.workbench_model import (
7+
ConversationEvent,
8+
ConversationMessage,
9+
MessageSender,
10+
MessageType,
11+
NewConversationMessage,
12+
)
13+
from semantic_workbench_assistant.assistant_app import AssistantAppProtocol, AssistantContext, ConversationContext
14+
15+
from assistant_extensions.workflows.runners._user_proxy import UserProxyRunner
16+
17+
from ._model import WorkflowsConfigModel
18+
19+
logger = logging.getLogger(__name__)
20+
21+
WorkflowsProcessingErrorHandler = Callable[[ConversationContext, str, Exception], Awaitable]
22+
23+
24+
trigger_command = "workflow"
25+
26+
27+
async def log_and_send_message_on_error(context: ConversationContext, filename: str, e: Exception) -> None:
28+
"""
29+
Default error handler for attachment processing, which logs the exception and sends
30+
a message to the conversation.
31+
"""
32+
33+
logger.exception("exception occurred processing attachment", exc_info=e)
34+
await context.send_messages(
35+
NewConversationMessage(
36+
content=f"There was an error processing the attachment ({filename}): {e}",
37+
message_type=MessageType.notice,
38+
metadata={"attribution": "workflows"},
39+
)
40+
)
41+
42+
43+
class WorkflowsExtension:
44+
def __init__(
45+
self,
46+
assistant: AssistantAppProtocol,
47+
content_safety_metadata_key: str,
48+
config_provider: Callable[[AssistantContext], Awaitable[WorkflowsConfigModel]],
49+
error_handler: WorkflowsProcessingErrorHandler = log_and_send_message_on_error,
50+
) -> None:
51+
"""
52+
WorkflowsExtension enables the assistant to execute pre-configured workflows. Current workflows act
53+
as an auto-proxy for a series of user messages. Future workflows may include more complex interactions.
54+
"""
55+
56+
self._error_handler = error_handler
57+
self._user_proxy_runner = UserProxyRunner(config_provider, error_handler)
58+
59+
@assistant.events.conversation.message.command.on_created
60+
async def on_command_message_created(
61+
context: ConversationContext, event: ConversationEvent, message: ConversationMessage
62+
) -> None:
63+
config = await config_provider(context.assistant)
64+
metadata: dict[str, Any] = {"debug": {"content_safety": event.data.get(content_safety_metadata_key, {})}}
65+
66+
if not config.enabled or message.command_name != f"/{trigger_command}":
67+
return
68+
69+
if len(message.command_args) > 0:
70+
await self.on_command(config, context, message, metadata)
71+
else:
72+
await self.on_help(config, context, metadata)
73+
74+
async def on_help(
75+
self,
76+
config: WorkflowsConfigModel,
77+
context: ConversationContext,
78+
metadata: dict[str, Any] = {},
79+
) -> None:
80+
# Iterate over the workflow definitions and create a list of commands in markdown format
81+
content = "Available workflows:\n"
82+
for workflow in config.workflow_definitions:
83+
content += f"- `{workflow.command}`: {workflow.description}\n"
84+
85+
# send the message
86+
await context.send_messages(
87+
NewConversationMessage(
88+
content=content,
89+
message_type=MessageType.command_response,
90+
metadata=deepmerge.always_merger.merge(
91+
metadata,
92+
{"attribution": "workflows"},
93+
),
94+
)
95+
)
96+
97+
async def on_command(
98+
self,
99+
config: WorkflowsConfigModel,
100+
context: ConversationContext,
101+
message: ConversationMessage,
102+
metadata: dict[str, Any] = {},
103+
) -> None:
104+
# find the workflow definition
105+
workflow_command = message.command_args.split(" ")[0]
106+
workflow_definition = None
107+
for workflow in config.workflow_definitions:
108+
if workflow.command == workflow_command:
109+
workflow_definition = workflow
110+
break
111+
112+
if workflow_definition is None:
113+
await self.on_help(config, context, metadata)
114+
return
115+
116+
# run the workflow in the background
117+
asyncio.create_task(self.run_workflow(context, workflow_definition, message.sender, metadata))
118+
119+
async def run_workflow(
120+
self,
121+
context: ConversationContext,
122+
workflow_definition: Any,
123+
send_as: MessageSender,
124+
metadata: dict[str, Any] = {},
125+
) -> None:
126+
try:
127+
await self._user_proxy_runner.run(context, workflow_definition, send_as, metadata)
128+
except Exception as e:
129+
await self._error_handler(context, workflow_definition.command, e)

0 commit comments

Comments
 (0)