diff --git a/skyvern-frontend/src/components/EngineSelector.tsx b/skyvern-frontend/src/components/EngineSelector.tsx index b0a8024a..32284e01 100644 --- a/skyvern-frontend/src/components/EngineSelector.tsx +++ b/skyvern-frontend/src/components/EngineSelector.tsx @@ -6,23 +6,100 @@ import { SelectTrigger, SelectValue, } from "./ui/select"; +import { cn } from "@/util/utils"; + +type EngineOption = { + value: RunEngine; + label: string; + badge?: string; + badgeVariant?: "default" | "success" | "warning"; +}; type Props = { value: RunEngine | null; onChange: (value: RunEngine) => void; className?: string; + availableEngines?: Array; }; -function RunEngineSelector({ value, onChange, className }: Props) { +const allEngineOptions: Array = [ + { + value: RunEngine.SkyvernV1, + label: "Skyvern 1.0", + badge: "Recommended", + badgeVariant: "success", + }, + { + value: RunEngine.SkyvernV2, + label: "Skyvern 2.0", + badge: "Multi-Goal", + badgeVariant: "warning", + }, + { + value: RunEngine.OpenaiCua, + label: "OpenAI CUA", + }, + { + value: RunEngine.AnthropicCua, + label: "Anthropic CUA", + }, +]; + +// Default engines for blocks that don't support V2 mode +const defaultEngines: Array = [ + RunEngine.SkyvernV1, + RunEngine.OpenaiCua, + RunEngine.AnthropicCua, +]; + +function BadgeLabel({ option }: { option: EngineOption }) { + return ( +
+ {option.label} + {option.badge && ( + + {option.badge} + + )} +
+ ); +} + +function RunEngineSelector({ + value, + onChange, + className, + availableEngines, +}: Props) { + const engines = availableEngines ?? defaultEngines; + const engineOptions = allEngineOptions.filter((opt) => + engines.includes(opt.value), + ); + + const selectedOption = engineOptions.find( + (opt) => opt.value === (value ?? RunEngine.SkyvernV1), + ); + return ( ); diff --git a/skyvern-frontend/src/routes/workflows/editor/helpContent.ts b/skyvern-frontend/src/routes/workflows/editor/helpContent.ts index d0bc4e69..0f176234 100644 --- a/skyvern-frontend/src/routes/workflows/editor/helpContent.ts +++ b/skyvern-frontend/src/routes/workflows/editor/helpContent.ts @@ -27,6 +27,8 @@ export const baseHelpTooltipContent = { "When inside a for loop, continue to the next iteration if this block fails.", includeActionHistoryInVerification: "Include the action history in the completion verification.", + engine: + "Skyvern 1.0: Fast, single-goal tasks. Skyvern 2.0: Complex, multi-goal tasks (slower).", } as const; export const basePlaceholderContent = { diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx index 98c37956..c746dba6 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx @@ -23,6 +23,7 @@ import { useState } from "react"; import { helpTooltips, placeholders } from "../../helpContent"; import { errorMappingExampleValue } from "../types"; import type { NavigationNode } from "./types"; +import { MAX_STEPS_DEFAULT } from "./types"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { AppNode } from ".."; import { @@ -35,9 +36,11 @@ import { ModelSelector } from "@/components/ModelSelector"; import { cn } from "@/util/utils"; import { useParams } from "react-router-dom"; import { NodeHeader } from "../components/NodeHeader"; +import { NodeTabs } from "../components/NodeTabs"; import { statusIsRunningOrQueued } from "@/routes/tasks/types"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { useUpdate } from "@/routes/workflows/editor/useUpdate"; +import { RunEngine } from "@/api/types"; import { DisableCache } from "../DisableCache"; import { BlockExecutionOptions } from "../components/BlockExecutionOptions"; @@ -64,10 +67,484 @@ function NavigationNode({ id, data, type }: NodeProps) { const update = useUpdate({ id, editable }); const isInsideForLoop = isNodeInsideForLoop(nodes, id); + // Determine if we're in V2 mode (Skyvern 2.0) + const isV2Mode = data.engine === RunEngine.SkyvernV2; + + const handleEngineChange = (value: RunEngine) => { + const updates: Partial = { engine: value }; + if (value === RunEngine.SkyvernV2) { + // Switching to V2 — clear V1-specific fields + updates.navigationGoal = ""; + updates.completeCriterion = ""; + updates.terminateCriterion = ""; + updates.errorCodeMapping = "null"; + updates.parameterKeys = []; + updates.maxRetries = null; + updates.maxStepsOverride = null; + updates.allowDownloads = false; + updates.downloadSuffix = null; + updates.includeActionHistoryInVerification = false; + } else if (data.engine === RunEngine.SkyvernV2) { + // Switching away from V2 — clear V2-specific fields + updates.prompt = ""; + updates.maxSteps = MAX_STEPS_DEFAULT; + } + update(updates); + }; + useEffect(() => { setFacing(data.showCode ? "back" : "front"); }, [data.showCode]); + // V2 Mode UI (simpler interface) + const renderV2Content = () => ( + <> +
+
+
+ + +
+ { + update({ url: value }); + }} + value={data.url} + placeholder={placeholders["taskv2"]["url"]} + className="nopan text-xs" + /> +
+
+
+ + {isFirstWorkflowBlock ? ( +
+ Tip: Use the {"+"} button to add parameters! +
+ ) : null} +
+ { + update({ prompt: value }); + }} + value={data.prompt} + placeholder={placeholders["taskv2"]["prompt"]} + className="nopan text-xs" + /> +
+
+
+ + +
+ +
+
+ + rerender.bump()} + > + + + Advanced Settings + + +
+ { + update({ model: value }); + }} + /> +
+
+ + +
+ { + update({ + maxSteps: Number(event.target.value), + }); + }} + /> +
+ + { + update({ disableCache }); + }} + /> + +
+
+ + +
+ { + update({ totpIdentifier: value }); + }} + value={data.totpIdentifier ?? ""} + placeholder={placeholders["navigation"]["totpIdentifier"]} + className="nopan text-xs" + /> +
+
+
+ + +
+ { + update({ totpVerificationUrl: value }); + }} + value={data.totpVerificationUrl ?? ""} + placeholder={placeholders["task"]["totpVerificationUrl"]} + className="nopan text-xs" + /> +
+
+
+
+
+ + ); + + // V1 Mode UI (full navigation interface) + const renderV1Content = () => ( + <> +
+
+
+
+ + +
+ {isFirstWorkflowBlock ? ( +
+ Tip: Use the {"+"} button to add parameters! +
+ ) : null} +
+ + { + update({ url: value }); + }} + value={data.url} + placeholder={placeholders["navigation"]["url"]} + className="nopan text-xs" + /> +
+
+
+ + +
+ { + update({ navigationGoal: value }); + }} + value={data.navigationGoal} + placeholder={placeholders["navigation"]["navigationGoal"]} + className="nopan text-xs" + /> +
+
+
+ Tip: Try to phrase your prompt as a goal with an explicit completion + criteria. While executing, Skyvern will take as many actions as + necessary to accomplish the goal. Use words like "Complete" or + "Terminate" to help Skyvern identify when it's finished or when it + should give up. +
+
+
+
+ + +
+ +
+
+ + rerender.bump()} + > + + + Advanced Settings + + +
+
+ { + update({ parameterKeys }); + }} + /> +
+
+ + { + update({ completeCriterion: value }); + }} + value={data.completeCriterion} + className="nopan text-xs" + /> +
+ + { + update({ model: value }); + }} + /> +
+
+ + +
+ { + const value = + event.target.value === "" + ? null + : Number(event.target.value); + update({ maxStepsOverride: value }); + }} + /> +
+
+
+
+ + +
+ { + update({ + errorCodeMapping: checked + ? JSON.stringify(errorMappingExampleValue, null, 2) + : "null", + }); + }} + /> +
+ {data.errorCodeMapping !== "null" && ( +
+ { + update({ errorCodeMapping: value }); + }} + className="nopan" + fontSize={8} + /> +
+ )} +
+ { + update({ continueOnFailure: checked }); + }} + onNextLoopOnFailureChange={(checked) => { + update({ nextLoopOnFailure: checked }); + }} + onIncludeActionHistoryInVerificationChange={(checked) => { + update({ + includeActionHistoryInVerification: checked, + }); + }} + /> + { + update({ disableCache }); + }} + /> + +
+
+ + +
+
+ { + update({ allowDownloads: checked }); + }} + /> +
+
+
+
+ + +
+ { + update({ downloadSuffix: value }); + }} + /> +
+ +
+
+ + +
+ { + update({ totpIdentifier: value }); + }} + value={data.totpIdentifier ?? ""} + placeholder={placeholders["navigation"]["totpIdentifier"]} + className="nopan text-xs" + /> +
+
+
+ + +
+ { + update({ totpVerificationUrl: value }); + }} + value={data.totpVerificationUrl ?? ""} + placeholder={placeholders["task"]["totpVerificationUrl"]} + className="nopan text-xs" + /> +
+
+
+
+
+ + ); + return (
@@ -101,317 +578,18 @@ function NavigationNode({ id, data, type }: NodeProps) { nodeId={id} totpIdentifier={data.totpIdentifier} totpUrl={data.totpVerificationUrl} - type={type} + type={isV2Mode ? "task_v2" : type} /> -
-
-
-
- - -
- {isFirstWorkflowBlock ? ( -
- Tip: Use the {"+"} button to add parameters! -
- ) : null} -
- - { - update({ url: value }); - }} - value={data.url} - placeholder={placeholders["navigation"]["url"]} - className="nopan text-xs" - /> -
-
-
- - -
- { - update({ navigationGoal: value }); - }} - value={data.navigationGoal} - placeholder={placeholders["navigation"]["navigationGoal"]} - className="nopan text-xs" - /> -
-
-
- Tip: Try to phrase your prompt as a goal with an explicit - completion criteria. While executing, Skyvern will take as many - actions as necessary to accomplish the goal. Use words like - "Complete" or "Terminate" to help Skyvern identify when it's - finished or when it should give up. -
-
-
- - rerender.bump()} - > - - - Advanced Settings - - -
-
- { - update({ parameterKeys }); - }} - /> -
-
- - { - update({ completeCriterion: value }); - }} - value={data.completeCriterion} - className="nopan text-xs" - /> -
- - { - update({ model: value }); - }} - /> -
-
- -
- { - update({ engine: value }); - }} - className="nopan w-52 text-xs" - /> -
-
-
- - -
- { - const value = - event.target.value === "" - ? null - : Number(event.target.value); - update({ maxStepsOverride: value }); - }} - /> -
-
-
-
- - -
- { - update({ - errorCodeMapping: checked - ? JSON.stringify( - errorMappingExampleValue, - null, - 2, - ) - : "null", - }); - }} - /> -
- {data.errorCodeMapping !== "null" && ( -
- { - update({ errorCodeMapping: value }); - }} - className="nopan" - fontSize={8} - /> -
- )} -
- { - update({ continueOnFailure: checked }); - }} - onNextLoopOnFailureChange={(checked) => { - update({ nextLoopOnFailure: checked }); - }} - onIncludeActionHistoryInVerificationChange={(checked) => { - update({ - includeActionHistoryInVerification: checked, - }); - }} - /> - { - update({ disableCache }); - }} - /> - -
-
- - -
-
- { - update({ allowDownloads: checked }); - }} - /> -
-
-
-
- - -
- { - update({ downloadSuffix: value }); - }} - /> -
- -
-
- - -
- { - update({ totpIdentifier: value }); - }} - value={data.totpIdentifier ?? ""} - placeholder={placeholders["navigation"]["totpIdentifier"]} - className="nopan text-xs" - /> -
-
-
- - -
- { - update({ totpVerificationUrl: value }); - }} - value={data.totpVerificationUrl ?? ""} - placeholder={placeholders["task"]["totpVerificationUrl"]} - className="nopan text-xs" - /> -
-
-
-
-
+ {isV2Mode ? renderV2Content() : renderV1Content()} +
- +
); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/types.ts index 643f86ed..9894be52 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/types.ts @@ -3,6 +3,8 @@ import { NodeBaseData } from "../types"; import { RunEngine } from "@/api/types"; import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; +export const MAX_STEPS_DEFAULT = 25; + export type NavigationNodeData = NodeBaseData & { url: string; navigationGoal: string; @@ -19,6 +21,9 @@ export type NavigationNodeData = NodeBaseData & { totpIdentifier: string | null; disableCache: boolean; includeActionHistoryInVerification: boolean; + // V2-specific fields (used when engine is SkyvernV2) + prompt: string; + maxSteps: number | null; }; export type NavigationNode = Node; @@ -44,6 +49,9 @@ export const navigationNodeDefaultData: NavigationNodeData = { continueOnFailure: false, disableCache: false, includeActionHistoryInVerification: false, + // V2-specific fields + prompt: "", + maxSteps: MAX_STEPS_DEFAULT, } as const; export function isNavigationNode(node: Node): node is NavigationNode { diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx index 6444baca..801c6538 100644 --- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowComparisonPanel.tsx @@ -38,7 +38,7 @@ type Props = { // Mapping from WorkflowBlock.block_type to ReactFlow node.type const BLOCK_TYPE_TO_NODE_TYPE: Record = { task: "task", - task_v2: "taskv2", + task_v2: "navigation", // task_v2 blocks are displayed as navigation nodes with V2 engine validation: "validation", action: "action", navigation: "navigation", diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx index 20e058a8..d31a7b39 100644 --- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx @@ -43,17 +43,6 @@ const nodeLibraryItems: Array<{ title: "Browser Task Block", description: "Take actions to achieve a task.", }, - { - nodeType: "taskv2", - icon: ( - - ), - title: "Browser Task v2 Block", - description: "Achieve complex tasks with deep thinking.", - }, { nodeType: "action", icon: ( diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts index 7c415ccb..388fb5b9 100644 --- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts @@ -106,6 +106,7 @@ import { actionNodeDefaultData, isActionNode } from "./nodes/ActionNode/types"; import { isNavigationNode, navigationNodeDefaultData, + MAX_STEPS_DEFAULT, } from "./nodes/NavigationNode/types"; import { extractionNodeDefaultData, @@ -119,7 +120,6 @@ import { isPdfParserNode, pdfParserNodeDefaultData, } from "./nodes/PDFParserNode/types"; -import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types"; import { urlNodeDefaultData } from "./nodes/URLNode/types"; import { fileUploadNodeDefaultData } from "./nodes/FileUploadNode/types"; import { @@ -527,19 +527,33 @@ function convertToNode( }; } case "task_v2": { + // Convert task_v2 blocks to navigation nodes with engine=SkyvernV2 return { ...identifiers, ...common, - type: "taskv2", + type: "navigation", data: { ...commonData, + // V2-specific fields prompt: block.prompt, url: block.url ?? "", - maxSteps: block.max_steps, + maxSteps: block.max_steps ?? MAX_STEPS_DEFAULT, disableCache: block.disable_cache ?? false, totpIdentifier: block.totp_identifier, totpVerificationUrl: block.totp_verification_url, - maxScreenshotScrolls: null, + // Set engine to SkyvernV2 to indicate V2 mode + engine: RunEngine.SkyvernV2, + // Default V1 fields (not used in V2 mode but needed for type compatibility) + navigationGoal: "", + errorCodeMapping: "null", + completeCriterion: "", + terminateCriterion: "", + maxRetries: null, + maxStepsOverride: null, + allowDownloads: false, + downloadSuffix: null, + parameterKeys: [], + includeActionHistoryInVerification: false, }, }; } @@ -602,6 +616,8 @@ function convertToNode( engine: block.engine ?? RunEngine.SkyvernV1, includeActionHistoryInVerification: block.include_action_history_in_verification ?? false, + prompt: "", + maxSteps: MAX_STEPS_DEFAULT, }, }; } @@ -1699,13 +1715,15 @@ function createNode( }; } case "taskv2": { + // Redirect taskv2 creation to navigation with SkyvernV2 engine return { ...identifiers, ...common, - type: "taskv2", + type: "navigation", data: { - ...taskv2NodeDefaultData, + ...navigationNodeDefaultData, label, + engine: RunEngine.SkyvernV2, }, }; } @@ -2164,6 +2182,20 @@ function getWorkflowBlock( }; } case "navigation": { + // If engine is SkyvernV2, convert to task_v2 block + if (node.data.engine === RunEngine.SkyvernV2) { + return { + ...base, + block_type: "task_v2", + prompt: node.data.prompt, + max_steps: node.data.maxSteps, + totp_identifier: node.data.totpIdentifier, + totp_verification_url: node.data.totpVerificationUrl, + url: node.data.url, + disable_cache: node.data.disableCache ?? false, + }; + } + // Otherwise, create a navigation block return { ...base, block_type: "navigation", @@ -3931,8 +3963,15 @@ function getWorkflowErrors(nodes: Array): Array { const navigationNodes = nodes.filter(isNavigationNode); navigationNodes.forEach((node) => { - if (node.data.navigationGoal.length === 0) { - errors.push(`${node.data.label}: Navigation goal is required.`); + // V2 mode uses prompt, V1 mode uses navigationGoal + if (node.data.engine === RunEngine.SkyvernV2) { + if (!node.data.prompt || node.data.prompt.length === 0) { + errors.push(`${node.data.label}: Prompt is required.`); + } + } else { + if (!node.data.navigationGoal || node.data.navigationGoal.length === 0) { + errors.push(`${node.data.label}: Prompt is required.`); + } } });