Workflow Copilot: Work on Workflow instead of WorkflowDefinition level (#4523)

This commit is contained in:
Stanislav Novosad
2026-01-22 15:05:04 -07:00
committed by GitHub
parent 8be0669b04
commit a52a174e28
5 changed files with 88 additions and 38 deletions

View File

@@ -7,7 +7,7 @@ import { ReloadIcon, Cross2Icon } from "@radix-ui/react-icons";
import { stringify as convertToYAML } from "yaml"; import { stringify as convertToYAML } from "yaml";
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore"; import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
import { WorkflowCreateYAMLRequest } from "@/routes/workflows/types/workflowYamlTypes"; 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 { toast } from "@/components/ui/use-toast";
import { getSseClient } from "@/api/sse"; import { getSseClient } from "@/api/sse";
import { import {
@@ -67,7 +67,7 @@ const MessageItem = memo(({ message }: { message: ChatMessage }) => {
}); });
interface WorkflowCopilotChatProps { interface WorkflowCopilotChatProps {
onWorkflowUpdate?: (workflow: WorkflowDefinition) => void; onWorkflowUpdate?: (workflow: WorkflowApiResponse) => void;
isOpen?: boolean; isOpen?: boolean;
onClose?: () => void; onClose?: () => void;
onMessageCountChange?: (count: number) => void; onMessageCountChange?: (count: number) => void;
@@ -425,7 +425,7 @@ export function WorkflowCopilotChat({
if (response.updated_workflow && onWorkflowUpdate) { if (response.updated_workflow && onWorkflowUpdate) {
try { try {
onWorkflowUpdate(response.updated_workflow as WorkflowDefinition); onWorkflowUpdate(response.updated_workflow);
} catch (updateError) { } catch (updateError) {
console.error("Failed to update workflow:", updateError); console.error("Failed to update workflow:", updateError);
toast({ toast({

View File

@@ -1,3 +1,5 @@
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
export type WorkflowCopilotChatSender = "user" | "ai"; export type WorkflowCopilotChatSender = "user" | "ai";
export interface WorkflowCopilotChat { export interface WorkflowCopilotChat {
@@ -53,7 +55,7 @@ export interface WorkflowCopilotStreamResponseUpdate {
type: "response"; type: "response";
workflow_copilot_chat_id: string; workflow_copilot_chat_id: string;
message: string; message: string;
updated_workflow?: Record<string, unknown> | null; updated_workflow?: WorkflowApiResponse | null;
response_time: string; response_time: string;
} }

View File

@@ -94,7 +94,7 @@ import {
import { WorkflowHeader } from "./WorkflowHeader"; import { WorkflowHeader } from "./WorkflowHeader";
import { WorkflowHistoryPanel } from "./panels/WorkflowHistoryPanel"; import { WorkflowHistoryPanel } from "./panels/WorkflowHistoryPanel";
import { WorkflowVersion } from "../hooks/useWorkflowVersionsQuery"; import { WorkflowVersion } from "../hooks/useWorkflowVersionsQuery";
import { WorkflowApiResponse, WorkflowSettings } from "../types/workflowTypes"; import { WorkflowSettings } from "../types/workflowTypes";
import { ProxyLocation } from "@/api/types"; import { ProxyLocation } from "@/api/types";
import { import {
nodeAdderNode, nodeAdderNode,
@@ -1698,36 +1698,36 @@ function Workspace({
buttonRef={copilotButtonRef} buttonRef={copilotButtonRef}
onWorkflowUpdate={(workflowData) => { onWorkflowUpdate={(workflowData) => {
try { try {
const saveData = workflowChangesStore.getSaveData?.();
const settings: WorkflowSettings = { const settings: WorkflowSettings = {
proxyLocation: proxyLocation:
saveData?.settings.proxyLocation ?? ProxyLocation.Residential, workflowData.proxy_location ?? ProxyLocation.Residential,
webhookCallbackUrl: saveData?.settings.webhookCallbackUrl || "", webhookCallbackUrl: workflowData.webhook_callback_url || "",
persistBrowserSession: persistBrowserSession:
saveData?.settings.persistBrowserSession ?? false, workflowData.persist_browser_session ?? false,
model: saveData?.settings.model ?? null, model: workflowData.model ?? null,
maxScreenshotScrolls: maxScreenshotScrolls: workflowData.max_screenshot_scrolls || 3,
saveData?.settings.maxScreenshotScrolls || 3, extraHttpHeaders: workflowData.extra_http_headers
extraHttpHeaders: saveData?.settings.extraHttpHeaders ?? null, ? JSON.stringify(workflowData.extra_http_headers)
runWith: saveData?.settings.runWith ?? null, : null,
scriptCacheKey: saveData?.settings.scriptCacheKey ?? null, runWith: workflowData.run_with ?? null,
aiFallback: saveData?.settings.aiFallback ?? true, scriptCacheKey: workflowData.cache_key ?? null,
runSequentially: saveData?.settings.runSequentially ?? false, aiFallback: workflowData.ai_fallback ?? true,
sequentialKey: saveData?.settings.sequentialKey ?? null, runSequentially: workflowData.run_sequentially ?? false,
finallyBlockLabel: workflowData?.finally_block_label ?? null, 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); setNodes(elements.nodes);
setEdges(elements.edges); setEdges(elements.edges);
const initialParameters = getInitialParameters({ const initialParameters = getInitialParameters(workflowData);
workflow_definition: {
parameters: workflowData.parameters,
},
} as WorkflowApiResponse);
useWorkflowParametersStore useWorkflowParametersStore
.getState() .getState()
.setParameters(initialParameters); .setParameters(initialParameters);

View File

@@ -46,6 +46,7 @@ const workflowParameterTypeOptions = [
{ label: "integer", value: WorkflowParameterValueType.Integer }, { label: "integer", value: WorkflowParameterValueType.Integer },
{ label: "boolean", value: WorkflowParameterValueType.Boolean }, { label: "boolean", value: WorkflowParameterValueType.Boolean },
{ label: "file", value: WorkflowParameterValueType.FileURL }, { label: "file", value: WorkflowParameterValueType.FileURL },
{ label: "credential", value: WorkflowParameterValueType.CredentialId },
{ label: "JSON", value: WorkflowParameterValueType.JSON }, { label: "JSON", value: WorkflowParameterValueType.JSON },
]; ];

View File

@@ -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.services import org_auth_service
from skyvern.forge.sdk.workflow.exceptions import BaseWorkflowHTTPException from skyvern.forge.sdk.workflow.exceptions import BaseWorkflowHTTPException
from skyvern.forge.sdk.workflow.models.parameter import ParameterType 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.forge.sdk.workflow.workflow_definition_converter import convert_workflow_definition
from skyvern.schemas.workflows import ( from skyvern.schemas.workflows import (
LoginBlockYAML, LoginBlockYAML,
@@ -124,7 +124,7 @@ async def copilot_call_llm(
chat_history: list[WorkflowCopilotChatHistoryMessage], chat_history: list[WorkflowCopilotChatHistoryMessage],
global_llm_context: str | None, global_llm_context: str | None,
debug_run_info_text: str, 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) chat_history_text = _format_chat_history(chat_history)
workflow_knowledge_base = WORKFLOW_KNOWLEDGE_BASE_PATH.read_text(encoding="utf-8") workflow_knowledge_base = WORKFLOW_KNOWLEDGE_BASE_PATH.read_text(encoding="utf-8")
@@ -142,15 +142,19 @@ async def copilot_call_llm(
LOG.info( LOG.info(
"Calling LLM", "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_len=len(chat_request.message),
user_message=chat_request.message,
workflow_yaml_len=len(chat_request.workflow_yaml or ""), 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_len=len(chat_history_text),
chat_history=chat_history_text,
global_llm_context_len=len(global_llm_context or ""), 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), 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_len=len(llm_prompt),
llm_prompt=llm_prompt,
) )
llm_api_handler = ( llm_api_handler = (
await get_llm_handler_for_prompt_type("workflow-copilot", chat_request.workflow_permanent_id, organization_id) 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( LOG.info(
"LLM response", "LLM response",
workflow_permanent_id=chat_request.workflow_permanent_id,
workflow_id=chat_request.workflow_id,
duration_seconds=time.monotonic() - llm_start_time, duration_seconds=time.monotonic() - llm_start_time,
user_message_len=len(chat_request.message), user_message_len=len(chat_request.message),
workflow_yaml_len=len(chat_request.workflow_yaml or ""), workflow_yaml_len=len(chat_request.workflow_yaml or ""),
@@ -185,6 +191,8 @@ async def copilot_call_llm(
user_response = str(user_response_value) user_response = str(user_response_value)
LOG.info( LOG.info(
"LLM response received", "LLM response received",
workflow_permanent_id=chat_request.workflow_permanent_id,
workflow_id=chat_request.workflow_id,
organization_id=organization_id, organization_id=organization_id,
action_type=action_type, action_type=action_type,
) )
@@ -194,9 +202,14 @@ async def copilot_call_llm(
global_llm_context = str(global_llm_context) global_llm_context = str(global_llm_context)
if action_type == "REPLACE_WORKFLOW": if action_type == "REPLACE_WORKFLOW":
workflow_yaml = action_data.get("workflow_yaml", "") llm_workflow_yaml = action_data.get("workflow_yaml", "")
try: 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: except (yaml.YAMLError, ValidationError, BaseWorkflowHTTPException) as e:
await stream.send( await stream.send(
WorkflowCopilotProcessingUpdate( WorkflowCopilotProcessingUpdate(
@@ -209,13 +222,18 @@ async def copilot_call_llm(
llm_api_handler=llm_api_handler, llm_api_handler=llm_api_handler,
organization_id=organization_id, organization_id=organization_id,
user_response=user_response, user_response=user_response,
workflow_yaml=workflow_yaml, workflow_yaml=llm_workflow_yaml,
chat_history=chat_history, chat_history=chat_history,
global_llm_context=global_llm_context, global_llm_context=global_llm_context,
debug_run_info_text=debug_run_info_text, debug_run_info_text=debug_run_info_text,
error=e, 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 return user_response, updated_workflow, global_llm_context
elif action_type == "REPLY": elif action_type == "REPLY":
@@ -280,7 +298,12 @@ async def _auto_correct_workflow_yaml(
return action_data.get("workflow_yaml", 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) parsed_yaml = yaml.safe_load(workflow_yaml)
# Fixing trivial common LLM mistakes # 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 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_definition_yaml=workflow_yaml_request.workflow_definition,
workflow_id=workflow_id, 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) @base_router.post("/workflow/copilot/chat-post", include_in_schema=False)