diff --git a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx index 993aa9b2..34a11a22 100644 --- a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx +++ b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx @@ -1,3 +1,11 @@ +import { AxiosError } from "axios"; +import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { useNavigate, useParams } from "react-router-dom"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { z } from "zod"; + import { getClient } from "@/api/AxiosClient"; import { ProxyLocation } from "@/api/types"; import { ProxySelector } from "@/components/ProxySelector"; @@ -16,6 +24,15 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown"; import { Input } from "@/components/ui/input"; import { KeyValueInput } from "@/components/KeyValueInput"; import { toast } from "@/components/ui/use-toast"; @@ -23,20 +40,27 @@ import { useApiCredential } from "@/hooks/useApiCredential"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useSyncFormFieldToStorage } from "@/hooks/useSyncFormFieldToStorage"; import { useLocalStorageFormDefault } from "@/hooks/useLocalStorageFormDefault"; -import { apiBaseUrl } from "@/util/env"; +import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery"; +import { constructCacheKeyValueFromParameters } from "@/routes/workflows/editor/utils"; +import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery"; import { type ApiCommandOptions } from "@/util/apiCommands"; -import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown"; -import { useForm } from "react-hook-form"; -import { useNavigate, useParams } from "react-router-dom"; -import { z } from "zod"; +import { apiBaseUrl, lsKeys } from "@/util/env"; +import { cn } from "@/util/utils"; + +import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "./editor/nodes/Taskv2Node/types"; +import { getLabelForWorkflowParameterType } from "./editor/workflowEditorUtils"; import { WorkflowParameter } from "./types/workflowTypes"; import { WorkflowParameterInput } from "./WorkflowParameterInput"; -import { AxiosError } from "axios"; -import { getLabelForWorkflowParameterType } from "./editor/workflowEditorUtils"; -import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "./editor/nodes/Taskv2Node/types"; -import { lsKeys } from "@/util/env"; + +// Utility function to omit specified keys from an object +function omit, K extends keyof T>( + obj: T, + keys: K[], +): Omit { + const result = { ...obj }; + keys.forEach((key) => delete result[key]); + return result; +} type Props = { workflowParameters: Array; @@ -89,6 +113,8 @@ type RunWorkflowRequestBody = { max_screenshot_scrolls?: number | null; extra_http_headers?: Record | null; browser_address?: string | null; + run_with?: "agent" | "code"; + ai_fallback?: boolean; }; function getRunWorkflowRequestBody( @@ -102,6 +128,8 @@ function getRunWorkflowRequestBody( cdpAddress, maxScreenshotScrolls, extraHttpHeaders, + runWithCode, + aiFallback, ...parameters } = values; @@ -117,6 +145,8 @@ function getRunWorkflowRequestBody( proxy_location: proxyLocation, browser_session_id: bsi, browser_address: cdpAddress, + run_with: runWithCode === true ? "code" : "agent", + ai_fallback: aiFallback ?? true, }; if (maxScreenshotScrolls) { @@ -146,6 +176,8 @@ type RunWorkflowFormType = Record & { cdpAddress: string | null; maxScreenshotScrolls: number | null; extraHttpHeaders: string | null; + runWithCode: boolean | null; + aiFallback: boolean | null; }; function RunWorkflowForm({ @@ -172,9 +204,12 @@ function RunWorkflowForm({ extraHttpHeaders: initialSettings.extraHttpHeaders ? JSON.stringify(initialSettings.extraHttpHeaders) : null, + runWithCode: false, + aiFallback: true, }, }); const apiCredential = useApiCredential(); + const { data: workflow } = useWorkflowQuery({ workflowPermanentId }); useSyncFormFieldToStorage(form, "browserSessionId", lsKeys.browserSessionId); @@ -213,6 +248,53 @@ function RunWorkflowForm({ }, }); + const [runParameters, setRunParameters] = useState | null>(null); + const [cacheKeyValue, setCacheKeyValue] = useState(""); + const cacheKey = workflow?.cache_key ?? "default"; + + useEffect(() => { + if (!runParameters) { + setCacheKeyValue(""); + return; + } + + const ckv = constructCacheKeyValueFromParameters({ + codeKey: cacheKey, + parameters: runParameters, + }); + + setCacheKeyValue(ckv); + }, [cacheKey, runParameters]); + + const { data: blockScripts } = useBlockScriptsQuery({ + cacheKey, + cacheKeyValue, + workflowPermanentId, + }); + + const [runWithCodeIsEnabled, setRunWithCodeIsEnabled] = useState(false); + + useEffect(() => { + setRunWithCodeIsEnabled(Object.keys(blockScripts ?? {}).length > 0); + }, [blockScripts]); + + useEffect(() => { + onChange(form.getValues()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [form]); + + // if we're coming from debugger, block scripts may already be cached; let's ensure we bust it + // on mount + useEffect(() => { + queryClient.invalidateQueries({ + queryKey: ["block-scripts"], + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + function onSubmit(values: RunWorkflowFormType) { const { webhookCallbackUrl, @@ -221,9 +303,17 @@ function RunWorkflowForm({ maxScreenshotScrolls, extraHttpHeaders, cdpAddress, + runWithCode, + aiFallback, ...parameters } = values; + const actuallyRunWithCode = !runWithCodeIsEnabled ? false : runWithCode; + const actuallyFallbackToAi = + !form.getValues().runWithCode || !runWithCodeIsEnabled + ? false + : aiFallback; + const parsedParameters = parseValuesForWorkflowRun( parameters, workflowParameters, @@ -236,12 +326,41 @@ function RunWorkflowForm({ maxScreenshotScrolls, extraHttpHeaders, cdpAddress, + runWithCode: actuallyRunWithCode, + aiFallback: actuallyFallbackToAi, }); } + function onChange(values: RunWorkflowFormType) { + const parameters = omit(values, [ + "webhookCallbackUrl", + "proxyLocation", + "browserSessionId", + "maxScreenshotScrolls", + "extraHttpHeaders", + "cdpAddress", + "runWithCode", + ]); + + const parsedParameters = parseValuesForWorkflowRun( + parameters, + workflowParameters, + ); + + setRunParameters(parsedParameters); + } + + if (!workflowPermanentId || !workflow) { + return
Invalid workflow
; + } + return (
- +

Input Parameters

@@ -404,32 +523,97 @@ function RunWorkflowForm({ }} /> { return ( -
+
- Browser Session ID + Run With

- Use a persistent browser session to maintain state and - enable browser interaction. + In a past run, code was generated with the input + parameters you've specified above. Choose to run this + workflow with that generated code, or with the Skyvern + Agent.

- + field.onChange(v === "code" ? true : false) + } + > + + + + + Skyvern Agent + Code + + + + +
+
+ + ); + }} + /> + + { + return ( + +
+ +
+
+ AI Fallback (self-healing) +
+

+ If the run fails when using code, turn this on to have + AI attempt to fix the issue and regenerate the code. +

+
+
+
+ + @@ -451,6 +635,44 @@ function RunWorkflowForm({
+ { + return ( + +
+ +
+
+ Browser Session ID +
+

+ Use a persistent browser session to maintain + state and enable browser interaction. +

+
+
+
+ + + + +
+
+
+ ); + }} + />
+
{ diff --git a/skyvern-frontend/src/routes/workflows/editor/utils.ts b/skyvern-frontend/src/routes/workflows/editor/utils.ts index c9b0b55d..4eac8d6d 100644 --- a/skyvern-frontend/src/routes/workflows/editor/utils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/utils.ts @@ -120,7 +120,7 @@ const constructCacheKeyValue = (opts: { workflowRun?: WorkflowRunStatusApiResponse; }) => { const { workflow, workflowRun } = opts; - let codeKey = opts.codeKey; + const codeKey = opts.codeKey; if (!workflow) { return ""; @@ -138,7 +138,20 @@ const constructCacheKeyValue = (opts: { {} as Record, ); - for (const [name, value] of Object.entries(workflowParameters)) { + return constructCacheKeyValueFromParameters({ + codeKey, + parameters: workflowParameters, + }); +}; + +const constructCacheKeyValueFromParameters = (opts: { + codeKey: string; + parameters: Record; +}) => { + const parameters = opts.parameters; + let codeKey = opts.codeKey; + + for (const [name, value] of Object.entries(parameters)) { if (value === null || value === undefined || value === "") { continue; } @@ -153,4 +166,8 @@ const constructCacheKeyValue = (opts: { return codeKey; }; -export { constructCacheKeyValue, getInitialParameters }; +export { + constructCacheKeyValue, + constructCacheKeyValueFromParameters, + getInitialParameters, +};