From a52a174e2868458865460d4c68ac33316ee43430 Mon Sep 17 00:00:00 2001 From: Stanislav Novosad Date: Thu, 22 Jan 2026 15:05:04 -0700 Subject: [PATCH] Workflow Copilot: Work on Workflow instead of WorkflowDefinition level (#4523) --- .../workflows/copilot/WorkflowCopilotChat.tsx | 6 +- .../workflows/copilot/workflowCopilotTypes.ts | 4 +- .../src/routes/workflows/editor/Workspace.tsx | 44 ++++++------ .../panels/WorkflowParameterEditPanel.tsx | 1 + skyvern/forge/sdk/routes/workflow_copilot.py | 71 +++++++++++++++---- 5 files changed, 88 insertions(+), 38 deletions(-) diff --git a/skyvern-frontend/src/routes/workflows/copilot/WorkflowCopilotChat.tsx b/skyvern-frontend/src/routes/workflows/copilot/WorkflowCopilotChat.tsx index 984ff3b0..82f57494 100644 --- a/skyvern-frontend/src/routes/workflows/copilot/WorkflowCopilotChat.tsx +++ b/skyvern-frontend/src/routes/workflows/copilot/WorkflowCopilotChat.tsx @@ -7,7 +7,7 @@ import { ReloadIcon, Cross2Icon } from "@radix-ui/react-icons"; import { stringify as convertToYAML } from "yaml"; import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore"; import { WorkflowCreateYAMLRequest } from "@/routes/workflows/types/workflowYamlTypes"; -import { WorkflowDefinition } from "@/routes/workflows/types/workflowTypes"; +import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes"; import { toast } from "@/components/ui/use-toast"; import { getSseClient } from "@/api/sse"; import { @@ -67,7 +67,7 @@ const MessageItem = memo(({ message }: { message: ChatMessage }) => { }); interface WorkflowCopilotChatProps { - onWorkflowUpdate?: (workflow: WorkflowDefinition) => void; + onWorkflowUpdate?: (workflow: WorkflowApiResponse) => void; isOpen?: boolean; onClose?: () => void; onMessageCountChange?: (count: number) => void; @@ -425,7 +425,7 @@ export function WorkflowCopilotChat({ if (response.updated_workflow && onWorkflowUpdate) { try { - onWorkflowUpdate(response.updated_workflow as WorkflowDefinition); + onWorkflowUpdate(response.updated_workflow); } catch (updateError) { console.error("Failed to update workflow:", updateError); toast({ diff --git a/skyvern-frontend/src/routes/workflows/copilot/workflowCopilotTypes.ts b/skyvern-frontend/src/routes/workflows/copilot/workflowCopilotTypes.ts index 41e5f6ba..cce6ab72 100644 --- a/skyvern-frontend/src/routes/workflows/copilot/workflowCopilotTypes.ts +++ b/skyvern-frontend/src/routes/workflows/copilot/workflowCopilotTypes.ts @@ -1,3 +1,5 @@ +import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes"; + export type WorkflowCopilotChatSender = "user" | "ai"; export interface WorkflowCopilotChat { @@ -53,7 +55,7 @@ export interface WorkflowCopilotStreamResponseUpdate { type: "response"; workflow_copilot_chat_id: string; message: string; - updated_workflow?: Record | null; + updated_workflow?: WorkflowApiResponse | null; response_time: string; } diff --git a/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx b/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx index 13cbc4d3..480dcc56 100644 --- a/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx @@ -94,7 +94,7 @@ import { import { WorkflowHeader } from "./WorkflowHeader"; import { WorkflowHistoryPanel } from "./panels/WorkflowHistoryPanel"; import { WorkflowVersion } from "../hooks/useWorkflowVersionsQuery"; -import { WorkflowApiResponse, WorkflowSettings } from "../types/workflowTypes"; +import { WorkflowSettings } from "../types/workflowTypes"; import { ProxyLocation } from "@/api/types"; import { nodeAdderNode, @@ -1698,36 +1698,36 @@ function Workspace({ buttonRef={copilotButtonRef} onWorkflowUpdate={(workflowData) => { try { - const saveData = workflowChangesStore.getSaveData?.(); - const settings: WorkflowSettings = { proxyLocation: - saveData?.settings.proxyLocation ?? ProxyLocation.Residential, - webhookCallbackUrl: saveData?.settings.webhookCallbackUrl || "", + workflowData.proxy_location ?? ProxyLocation.Residential, + webhookCallbackUrl: workflowData.webhook_callback_url || "", persistBrowserSession: - saveData?.settings.persistBrowserSession ?? false, - model: saveData?.settings.model ?? null, - maxScreenshotScrolls: - saveData?.settings.maxScreenshotScrolls || 3, - extraHttpHeaders: saveData?.settings.extraHttpHeaders ?? null, - runWith: saveData?.settings.runWith ?? null, - scriptCacheKey: saveData?.settings.scriptCacheKey ?? null, - aiFallback: saveData?.settings.aiFallback ?? true, - runSequentially: saveData?.settings.runSequentially ?? false, - sequentialKey: saveData?.settings.sequentialKey ?? null, - finallyBlockLabel: workflowData?.finally_block_label ?? null, + workflowData.persist_browser_session ?? false, + model: workflowData.model ?? null, + maxScreenshotScrolls: workflowData.max_screenshot_scrolls || 3, + extraHttpHeaders: workflowData.extra_http_headers + ? JSON.stringify(workflowData.extra_http_headers) + : null, + runWith: workflowData.run_with ?? null, + scriptCacheKey: workflowData.cache_key ?? null, + aiFallback: workflowData.ai_fallback ?? true, + runSequentially: workflowData.run_sequentially ?? false, + sequentialKey: workflowData.sequential_key ?? null, + finallyBlockLabel: + workflowData.workflow_definition?.finally_block_label ?? null, }; - const elements = getElements(workflowData.blocks, settings, true); + const elements = getElements( + workflowData.workflow_definition.blocks, + settings, + true, + ); setNodes(elements.nodes); setEdges(elements.edges); - const initialParameters = getInitialParameters({ - workflow_definition: { - parameters: workflowData.parameters, - }, - } as WorkflowApiResponse); + const initialParameters = getInitialParameters(workflowData); useWorkflowParametersStore .getState() .setParameters(initialParameters); diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParameterEditPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParameterEditPanel.tsx index d7ede3fc..64834fac 100644 --- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParameterEditPanel.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParameterEditPanel.tsx @@ -46,6 +46,7 @@ const workflowParameterTypeOptions = [ { label: "integer", value: WorkflowParameterValueType.Integer }, { label: "boolean", value: WorkflowParameterValueType.Boolean }, { label: "file", value: WorkflowParameterValueType.FileURL }, + { label: "credential", value: WorkflowParameterValueType.CredentialId }, { label: "JSON", value: WorkflowParameterValueType.JSON }, ]; diff --git a/skyvern/forge/sdk/routes/workflow_copilot.py b/skyvern/forge/sdk/routes/workflow_copilot.py index 69a4978b..d96a759e 100644 --- a/skyvern/forge/sdk/routes/workflow_copilot.py +++ b/skyvern/forge/sdk/routes/workflow_copilot.py @@ -34,7 +34,7 @@ from skyvern.forge.sdk.schemas.workflow_copilot import ( from skyvern.forge.sdk.services import org_auth_service from skyvern.forge.sdk.workflow.exceptions import BaseWorkflowHTTPException from skyvern.forge.sdk.workflow.models.parameter import ParameterType -from skyvern.forge.sdk.workflow.models.workflow import WorkflowDefinition +from skyvern.forge.sdk.workflow.models.workflow import Workflow from skyvern.forge.sdk.workflow.workflow_definition_converter import convert_workflow_definition from skyvern.schemas.workflows import ( LoginBlockYAML, @@ -124,7 +124,7 @@ async def copilot_call_llm( chat_history: list[WorkflowCopilotChatHistoryMessage], global_llm_context: str | None, debug_run_info_text: str, -) -> tuple[str, WorkflowDefinition | None, str | None]: +) -> tuple[str, Workflow | None, str | None]: chat_history_text = _format_chat_history(chat_history) workflow_knowledge_base = WORKFLOW_KNOWLEDGE_BASE_PATH.read_text(encoding="utf-8") @@ -142,15 +142,19 @@ async def copilot_call_llm( LOG.info( "Calling LLM", - user_message=chat_request.message, + workflow_permanent_id=chat_request.workflow_permanent_id, + workflow_id=chat_request.workflow_id, user_message_len=len(chat_request.message), + user_message=chat_request.message, workflow_yaml_len=len(chat_request.workflow_yaml or ""), + workflow_yaml=chat_request.workflow_yaml or "", chat_history_len=len(chat_history_text), + chat_history=chat_history_text, global_llm_context_len=len(global_llm_context or ""), - debug_run_info_len=len(debug_run_info_text), + global_llm_context=global_llm_context or "", workflow_knowledge_base_len=len(workflow_knowledge_base), + debug_run_info_len=len(debug_run_info_text), llm_prompt_len=len(llm_prompt), - llm_prompt=llm_prompt, ) llm_api_handler = ( await get_llm_handler_for_prompt_type("workflow-copilot", chat_request.workflow_permanent_id, organization_id) @@ -164,6 +168,8 @@ async def copilot_call_llm( ) LOG.info( "LLM response", + workflow_permanent_id=chat_request.workflow_permanent_id, + workflow_id=chat_request.workflow_id, duration_seconds=time.monotonic() - llm_start_time, user_message_len=len(chat_request.message), workflow_yaml_len=len(chat_request.workflow_yaml or ""), @@ -185,6 +191,8 @@ async def copilot_call_llm( user_response = str(user_response_value) LOG.info( "LLM response received", + workflow_permanent_id=chat_request.workflow_permanent_id, + workflow_id=chat_request.workflow_id, organization_id=organization_id, action_type=action_type, ) @@ -194,9 +202,14 @@ async def copilot_call_llm( global_llm_context = str(global_llm_context) if action_type == "REPLACE_WORKFLOW": - workflow_yaml = action_data.get("workflow_yaml", "") + llm_workflow_yaml = action_data.get("workflow_yaml", "") try: - updated_workflow = await _process_workflow_yaml(chat_request.workflow_id, workflow_yaml) + updated_workflow = await _process_workflow_yaml( + workflow_id=chat_request.workflow_id, + workflow_permanent_id=chat_request.workflow_permanent_id, + organization_id=organization_id, + workflow_yaml=llm_workflow_yaml, + ) except (yaml.YAMLError, ValidationError, BaseWorkflowHTTPException) as e: await stream.send( WorkflowCopilotProcessingUpdate( @@ -209,13 +222,18 @@ async def copilot_call_llm( llm_api_handler=llm_api_handler, organization_id=organization_id, user_response=user_response, - workflow_yaml=workflow_yaml, + workflow_yaml=llm_workflow_yaml, chat_history=chat_history, global_llm_context=global_llm_context, debug_run_info_text=debug_run_info_text, error=e, ) - updated_workflow = await _process_workflow_yaml(chat_request.workflow_id, corrected_workflow_yaml) + updated_workflow = await _process_workflow_yaml( + workflow_id=chat_request.workflow_id, + workflow_permanent_id=chat_request.workflow_permanent_id, + organization_id=organization_id, + workflow_yaml=corrected_workflow_yaml, + ) return user_response, updated_workflow, global_llm_context elif action_type == "REPLY": @@ -280,7 +298,12 @@ async def _auto_correct_workflow_yaml( return action_data.get("workflow_yaml", workflow_yaml) -async def _process_workflow_yaml(workflow_id: str, workflow_yaml: str) -> WorkflowDefinition: +async def _process_workflow_yaml( + workflow_id: str, + workflow_permanent_id: str, + organization_id: str, + workflow_yaml: str, +) -> Workflow: parsed_yaml = yaml.safe_load(workflow_yaml) # Fixing trivial common LLM mistakes @@ -301,11 +324,35 @@ async def _process_workflow_yaml(workflow_id: str, workflow_yaml: str) -> Workfl p for p in workflow_yaml_request.workflow_definition.parameters if p.parameter_type != ParameterType.OUTPUT ] - updated_workflow = convert_workflow_definition( + updated_workflow_definition = convert_workflow_definition( workflow_definition_yaml=workflow_yaml_request.workflow_definition, workflow_id=workflow_id, ) - return updated_workflow + + now = datetime.now(timezone.utc) + return Workflow( + workflow_id=workflow_id, + organization_id=organization_id, + title=workflow_yaml_request.title or "", + workflow_permanent_id=workflow_permanent_id, + version=1, + is_saved_task=workflow_yaml_request.is_saved_task, + description=workflow_yaml_request.description, + workflow_definition=updated_workflow_definition, + proxy_location=workflow_yaml_request.proxy_location, + webhook_callback_url=workflow_yaml_request.webhook_callback_url, + persist_browser_session=workflow_yaml_request.persist_browser_session or False, + model=workflow_yaml_request.model, + max_screenshot_scrolls=workflow_yaml_request.max_screenshot_scrolls, + extra_http_headers=workflow_yaml_request.extra_http_headers, + run_with=workflow_yaml_request.run_with, + ai_fallback=workflow_yaml_request.ai_fallback, + cache_key=workflow_yaml_request.cache_key, + run_sequentially=workflow_yaml_request.run_sequentially, + sequential_key=workflow_yaml_request.sequential_key, + created_at=now, + modified_at=now, + ) @base_router.post("/workflow/copilot/chat-post", include_in_schema=False)