From 59c28f9a02497d24c17c9490689f36a56b53b736 Mon Sep 17 00:00:00 2001 From: Kerem Yilmaz Date: Mon, 30 Sep 2024 09:08:27 -0700 Subject: [PATCH] Add error code mapping to task (#890) --- skyvern-frontend/src/api/types.ts | 33 ++--- skyvern-frontend/src/router.tsx | 31 ++--- .../routes/tasks/create/CreateNewTaskForm.tsx | 123 ++++++++--------- .../tasks/create/CreateNewTaskFormPage.tsx | 7 +- .../tasks/create/CreateNewTaskFromPrompt.tsx | 44 ------- .../src/routes/tasks/create/SavedTaskForm.tsx | 124 +++++++++--------- .../routes/tasks/create/retry/RetryTask.tsx | 3 + .../src/routes/tasks/create/taskFormTypes.ts | 81 ++++++++++++ .../src/routes/tasks/data/sampleTaskData.ts | 38 +++++- 9 files changed, 270 insertions(+), 214 deletions(-) delete mode 100644 skyvern-frontend/src/routes/tasks/create/CreateNewTaskFromPrompt.tsx create mode 100644 skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index 3fcd53b0..eaa1496a 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -67,28 +67,31 @@ export type StepApiResponse = { }; export type TaskApiResponse = { - request: { - title: string | null; - url: string; - webhook_callback_url: string; - navigation_goal: string | null; - data_extraction_goal: string | null; - navigation_payload: string | object; // stringified JSON - error_code_mapping: null; - proxy_location: string; - extracted_information_schema: string | object; - totp_verification_url: string | null; - totp_identifier: string | null; - }; + request: CreateTaskRequest; task_id: string; status: Status; created_at: string; // ISO 8601 modified_at: string; // ISO 8601 - extracted_information: unknown; + extracted_information: Record | string | null; screenshot_url: string | null; recording_url: string | null; failure_reason: string | null; - errors: unknown[]; + errors: Array>; + max_steps_per_run: number | null; +}; + +export type CreateTaskRequest = { + title: string | null; + url: string; + webhook_callback_url: string | null; + navigation_goal: string | null; + data_extraction_goal: string | null; + navigation_payload: Record | string | null; + extracted_information_schema: Record | string | null; + error_code_mapping: Record | null; + proxy_location: string | null; + totp_verification_url: string | null; + totp_identifier: string | null; }; export type User = { diff --git a/skyvern-frontend/src/router.tsx b/skyvern-frontend/src/router.tsx index d72b2329..7e03ce3b 100644 --- a/skyvern-frontend/src/router.tsx +++ b/skyvern-frontend/src/router.tsx @@ -1,24 +1,23 @@ import { Navigate, Outlet, createBrowserRouter } from "react-router-dom"; import { RootLayout } from "./routes/root/RootLayout"; -import { TasksPageLayout } from "./routes/tasks/TasksPageLayout"; -import { TaskTemplates } from "./routes/tasks/create/TaskTemplates"; -import { TaskList } from "./routes/tasks/list/TaskList"; import { Settings } from "./routes/settings/Settings"; import { SettingsPageLayout } from "./routes/settings/SettingsPageLayout"; -import { TaskDetails } from "./routes/tasks/detail/TaskDetails"; -import { CreateNewTaskLayout } from "./routes/tasks/create/CreateNewTaskLayout"; +import { TasksPageLayout } from "./routes/tasks/TasksPageLayout"; import { CreateNewTaskFormPage } from "./routes/tasks/create/CreateNewTaskFormPage"; -import { TaskActions } from "./routes/tasks/detail/TaskActions"; -import { TaskRecording } from "./routes/tasks/detail/TaskRecording"; -import { TaskParameters } from "./routes/tasks/detail/TaskParameters"; -import { StepArtifactsLayout } from "./routes/tasks/detail/StepArtifactsLayout"; -import { CreateNewTaskFromPrompt } from "./routes/tasks/create/CreateNewTaskFromPrompt"; -import { WorkflowsPageLayout } from "./routes/workflows/WorkflowsPageLayout"; -import { Workflows } from "./routes/workflows/Workflows"; -import { WorkflowPage } from "./routes/workflows/WorkflowPage"; -import { WorkflowRunParameters } from "./routes/workflows/WorkflowRunParameters"; +import { CreateNewTaskLayout } from "./routes/tasks/create/CreateNewTaskLayout"; +import { TaskTemplates } from "./routes/tasks/create/TaskTemplates"; import { RetryTask } from "./routes/tasks/create/retry/RetryTask"; +import { StepArtifactsLayout } from "./routes/tasks/detail/StepArtifactsLayout"; +import { TaskActions } from "./routes/tasks/detail/TaskActions"; +import { TaskDetails } from "./routes/tasks/detail/TaskDetails"; +import { TaskParameters } from "./routes/tasks/detail/TaskParameters"; +import { TaskRecording } from "./routes/tasks/detail/TaskRecording"; +import { TaskList } from "./routes/tasks/list/TaskList"; +import { WorkflowPage } from "./routes/workflows/WorkflowPage"; import { WorkflowRun } from "./routes/workflows/WorkflowRun"; +import { WorkflowRunParameters } from "./routes/workflows/WorkflowRunParameters"; +import { Workflows } from "./routes/workflows/Workflows"; +import { WorkflowsPageLayout } from "./routes/workflows/WorkflowsPageLayout"; import { WorkflowEditor } from "./routes/workflows/editor/WorkflowEditor"; const router = createBrowserRouter([ @@ -74,10 +73,6 @@ const router = createBrowserRouter([ index: true, element: , }, - { - path: "sk-prompt", - element: , - }, { path: ":template", element: , diff --git a/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx b/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx index 3a031a04..1e7a3c2b 100644 --- a/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx +++ b/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx @@ -1,4 +1,5 @@ import { getClient } from "@/api/AxiosClient"; +import { CreateTaskRequest } from "@/api/types"; import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { Button } from "@/components/ui/button"; import { @@ -10,6 +11,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; import { useToast } from "@/components/ui/use-toast"; import { useApiCredential } from "@/hooks/useApiCredential"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; @@ -25,69 +27,24 @@ import fetchToCurl from "fetch-to-curl"; import { useState } from "react"; import { useForm, useFormState } from "react-hook-form"; import { Link } from "react-router-dom"; -import { z } from "zod"; import { MAX_STEPS_DEFAULT } from "../constants"; import { TaskFormSection } from "./TaskFormSection"; - -const createNewTaskFormSchema = z - .object({ - url: z.string().url({ - message: "Invalid URL", - }), - webhookCallbackUrl: z.string().or(z.null()).optional(), - navigationGoal: z.string().or(z.null()).optional(), - dataExtractionGoal: z.string().or(z.null()).optional(), - navigationPayload: z.string().or(z.null()).optional(), - extractedInformationSchema: z.string().or(z.null()).optional(), - maxStepsOverride: z.number().optional(), - totpVerificationUrl: z.string().or(z.null()).optional(), - totpIdentifier: z.string().or(z.null()).optional(), - }) - .superRefine( - ( - { navigationGoal, dataExtractionGoal, extractedInformationSchema }, - ctx, - ) => { - if (!navigationGoal && !dataExtractionGoal) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "At least one of navigation goal or data extraction goal must be provided", - path: ["navigationGoal"], - }); - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "At least one of navigation goal or data extraction goal must be provided", - path: ["dataExtractionGoal"], - }); - return z.NEVER; - } - if (extractedInformationSchema) { - try { - JSON.parse(extractedInformationSchema); - } catch (e) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Invalid JSON", - path: ["extractedInformationSchema"], - }); - } - } - }, - ); - -export type CreateNewTaskFormValues = z.infer; +import { + createNewTaskFormSchema, + CreateNewTaskFormValues, +} from "./taskFormTypes"; type Props = { initialValues: CreateNewTaskFormValues; }; -function transform(value: unknown) { +function transform(value: T): T | null { return value === "" ? null : value; } -function createTaskRequestObject(formValues: CreateNewTaskFormValues) { +function createTaskRequestObject( + formValues: CreateNewTaskFormValues, +): CreateTaskRequest { let extractedInformationSchema = null; if (formValues.extractedInformationSchema) { try { @@ -98,8 +55,17 @@ function createTaskRequestObject(formValues: CreateNewTaskFormValues) { extractedInformationSchema = formValues.extractedInformationSchema; } } + let errorCodeMapping = null; + if (formValues.errorCodeMapping) { + try { + errorCodeMapping = JSON.parse(formValues.errorCodeMapping); + } catch (e) { + errorCodeMapping = formValues.errorCodeMapping; + } + } return { + title: null, url: formValues.url, webhook_callback_url: transform(formValues.webhookCallbackUrl), navigation_goal: transform(formValues.navigationGoal), @@ -109,6 +75,7 @@ function createTaskRequestObject(formValues: CreateNewTaskFormValues) { extracted_information_schema: extractedInformationSchema, totp_verification_url: transform(formValues.totpVerificationUrl), totp_identifier: transform(formValues.totpIdentifier), + error_code_mapping: errorCodeMapping, }; } @@ -334,12 +301,7 @@ function CreateNewTaskForm({ initialValues }: Props) { language="json" fontSize={12} minHeight="96px" - value={ - field.value === null || - typeof field.value === "undefined" - ? "" - : field.value - } + value={field.value === null ? "" : field.value} /> @@ -362,7 +324,8 @@ function CreateNewTaskForm({ initialValues }: Props) { hasError={ typeof errors.navigationPayload !== "undefined" || typeof errors.maxStepsOverride !== "undefined" || - typeof errors.webhookCallbackUrl !== "undefined" + typeof errors.webhookCallbackUrl !== "undefined" || + typeof errors.errorCodeMapping !== "undefined" } > {section === "advanced" && ( @@ -389,12 +352,7 @@ function CreateNewTaskForm({ initialValues }: Props) { language="json" fontSize={12} minHeight="96px" - value={ - field.value === null || - typeof field.value === "undefined" - ? "" - : field.value - } + value={field.value === null ? "" : field.value} /> @@ -465,6 +423,39 @@ function CreateNewTaskForm({ initialValues }: Props) { )} /> + + ( + +
+ +
+

Error Messages

+

+ Specify any error outputs you would like to be + notified about +

+
+
+
+ + + + +
+
+
+ )} + /> + ); diff --git a/skyvern-frontend/src/routes/tasks/create/CreateNewTaskFromPrompt.tsx b/skyvern-frontend/src/routes/tasks/create/CreateNewTaskFromPrompt.tsx deleted file mode 100644 index 5c02bef5..00000000 --- a/skyvern-frontend/src/routes/tasks/create/CreateNewTaskFromPrompt.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useLocation } from "react-router-dom"; -import { CreateNewTaskForm } from "./CreateNewTaskForm"; -import { MagicWandIcon } from "@radix-ui/react-icons"; - -function CreateNewTaskFromPrompt() { - const location = useLocation(); - - const state = location.state.data; - return ( -
-
-
- -

Create New Task

-
-

- Prompt: {state.user_prompt} -

-

- Below are the parameters we generated automatically. You can go ahead - and create the task if everything looks correct. -

-
- -
- ); -} - -export { CreateNewTaskFromPrompt }; diff --git a/skyvern-frontend/src/routes/tasks/create/SavedTaskForm.tsx b/skyvern-frontend/src/routes/tasks/create/SavedTaskForm.tsx index 9174c46e..2c19e0bd 100644 --- a/skyvern-frontend/src/routes/tasks/create/SavedTaskForm.tsx +++ b/skyvern-frontend/src/routes/tasks/create/SavedTaskForm.tsx @@ -28,62 +28,9 @@ import { useState } from "react"; import { useForm, useFormState } from "react-hook-form"; import { Link, useParams } from "react-router-dom"; import { stringify as convertToYAML } from "yaml"; -import { z } from "zod"; import { MAX_STEPS_DEFAULT } from "../constants"; import { TaskFormSection } from "./TaskFormSection"; - -const savedTaskFormSchema = z - .object({ - title: z.string().min(1, "Title is required"), - description: z.string(), - url: z.string().url({ - message: "Invalid URL", - }), - proxyLocation: z.string().or(z.null()).optional(), - webhookCallbackUrl: z.string().or(z.null()).optional(), - navigationGoal: z.string().or(z.null()).optional(), - dataExtractionGoal: z.string().or(z.null()).optional(), - navigationPayload: z.string().or(z.null()).optional(), - extractedInformationSchema: z.string().or(z.null()).optional(), - maxSteps: z.number().optional(), - totpVerificationUrl: z.string().or(z.null()).optional(), - totpIdentifier: z.string().or(z.null()).optional(), - }) - .superRefine( - ( - { navigationGoal, dataExtractionGoal, extractedInformationSchema }, - ctx, - ) => { - if (!navigationGoal && !dataExtractionGoal) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "At least one of navigation goal or data extraction goal must be provided", - path: ["navigationGoal"], - }); - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "At least one of navigation goal or data extraction goal must be provided", - path: ["dataExtractionGoal"], - }); - return z.NEVER; - } - if (extractedInformationSchema) { - try { - JSON.parse(extractedInformationSchema); - } catch (e) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Invalid JSON", - path: ["extractedInformationSchema"], - }); - } - } - }, - ); - -export type SavedTaskFormValues = z.infer; +import { savedTaskFormSchema, SavedTaskFormValues } from "./taskFormTypes"; type Props = { initialValues: SavedTaskFormValues; @@ -105,17 +52,26 @@ function createTaskRequestObject(formValues: SavedTaskFormValues) { } } + let errorCodeMapping = null; + if (formValues.errorCodeMapping) { + try { + errorCodeMapping = JSON.parse(formValues.errorCodeMapping); + } catch (e) { + errorCodeMapping = formValues.errorCodeMapping; + } + } + return { url: formValues.url, webhook_callback_url: transform(formValues.webhookCallbackUrl), navigation_goal: transform(formValues.navigationGoal), data_extraction_goal: transform(formValues.dataExtractionGoal), proxy_location: transform(formValues.proxyLocation), - error_code_mapping: null, navigation_payload: transform(formValues.navigationPayload), extracted_information_schema: extractedInformationSchema, totp_verification_url: transform(formValues.totpVerificationUrl), totp_identifier: transform(formValues.totpIdentifier), + error_code_mapping: errorCodeMapping, }; } @@ -131,6 +87,15 @@ function createTaskTemplateRequestObject(values: SavedTaskFormValues) { } } + let errorCodeMapping = null; + if (values.errorCodeMapping) { + try { + errorCodeMapping = JSON.parse(values.errorCodeMapping); + } catch (e) { + errorCodeMapping = values.errorCodeMapping; + } + } + return { title: values.title, description: values.description, @@ -154,9 +119,10 @@ function createTaskTemplateRequestObject(values: SavedTaskFormValues) { navigation_goal: values.navigationGoal, data_extraction_goal: values.dataExtractionGoal, data_schema: extractedInformationSchema, - max_steps_per_run: values.maxSteps, + max_steps_per_run: values.maxStepsOverride, totp_verification_url: values.totpVerificationUrl, totp_identifier: values.totpIdentifier, + error_code_mapping: errorCodeMapping, }, ], }, @@ -178,7 +144,7 @@ function SavedTaskForm({ initialValues }: Props) { defaultValues: initialValues, values: { ...initialValues, - maxSteps: initialValues.maxSteps ?? MAX_STEPS_DEFAULT, + maxStepsOverride: initialValues.maxStepsOverride ?? MAX_STEPS_DEFAULT, }, }); @@ -199,7 +165,7 @@ function SavedTaskForm({ initialValues }: Props) { .then(() => { const taskRequest = createTaskRequestObject(formValues); const includeOverrideHeader = - formValues.maxSteps !== MAX_STEPS_DEFAULT; + formValues.maxStepsOverride !== MAX_STEPS_DEFAULT; return client.post< ReturnType, { data: { task_id: string } } @@ -207,7 +173,7 @@ function SavedTaskForm({ initialValues }: Props) { ...(includeOverrideHeader && { headers: { "x-max-steps-override": - formValues.maxSteps ?? MAX_STEPS_DEFAULT, + formValues.maxStepsOverride ?? MAX_STEPS_DEFAULT, }, }), }); @@ -523,8 +489,9 @@ function SavedTaskForm({ initialValues }: Props) { onClick={() => setSection("advanced")} hasError={ typeof errors.navigationPayload !== "undefined" || - typeof errors.maxSteps !== "undefined" || - typeof errors.webhookCallbackUrl !== "undefined" + typeof errors.maxStepsOverride !== "undefined" || + typeof errors.webhookCallbackUrl !== "undefined" || + typeof errors.errorCodeMapping !== "undefined" } > {section === "advanced" && ( @@ -567,7 +534,7 @@ function SavedTaskForm({ initialValues }: Props) { /> (
@@ -627,6 +594,39 @@ function SavedTaskForm({ initialValues }: Props) { )} /> + + ( + +
+ +
+

Error Messages

+

+ Specify any error outputs you would like to be + notified about +

+
+
+
+ + + + +
+
+
+ )} + /> + ); diff --git a/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts b/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts new file mode 100644 index 00000000..d0805460 --- /dev/null +++ b/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts @@ -0,0 +1,81 @@ +import { z } from "zod"; + +const createNewTaskFormSchemaBase = z.object({ + url: z.string().url({ + message: "Invalid URL", + }), + webhookCallbackUrl: z.string().or(z.null()), + navigationGoal: z.string().or(z.null()), + dataExtractionGoal: z.string().or(z.null()), + navigationPayload: z.string().or(z.null()), + extractedInformationSchema: z.string().or(z.null()), + maxStepsOverride: z.number().optional(), + totpVerificationUrl: z.string().or(z.null()), + totpIdentifier: z.string().or(z.null()), + errorCodeMapping: z.string().or(z.null()), +}); + +const savedTaskFormSchemaBase = createNewTaskFormSchemaBase.extend({ + title: z.string().min(1, "Title is required"), + description: z.string(), + proxyLocation: z.string().or(z.null()), +}); + +function refineTaskFormValues( + values: CreateNewTaskFormValues | SavedTaskFormValues, + ctx: z.RefinementCtx, +) { + const { + navigationGoal, + dataExtractionGoal, + extractedInformationSchema, + errorCodeMapping, + } = values; + if (!navigationGoal && !dataExtractionGoal) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "At least one of navigation goal or data extraction goal must be provided", + path: ["navigationGoal"], + }); + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "At least one of navigation goal or data extraction goal must be provided", + path: ["dataExtractionGoal"], + }); + } + if (extractedInformationSchema) { + try { + JSON.parse(extractedInformationSchema); + } catch (e) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid JSON", + path: ["extractedInformationSchema"], + }); + } + } + if (errorCodeMapping) { + try { + JSON.parse(errorCodeMapping); + } catch (e) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid JSON", + path: ["errorCodeMapping"], + }); + } + } +} + +export const createNewTaskFormSchema = + createNewTaskFormSchemaBase.superRefine(refineTaskFormValues); + +export const savedTaskFormSchema = + savedTaskFormSchemaBase.superRefine(refineTaskFormValues); + +export type CreateNewTaskFormValues = z.infer< + typeof createNewTaskFormSchemaBase +>; +export type SavedTaskFormValues = z.infer; diff --git a/skyvern-frontend/src/routes/tasks/data/sampleTaskData.ts b/skyvern-frontend/src/routes/tasks/data/sampleTaskData.ts index d274696a..058ee0d2 100644 --- a/skyvern-frontend/src/routes/tasks/data/sampleTaskData.ts +++ b/skyvern-frontend/src/routes/tasks/data/sampleTaskData.ts @@ -1,11 +1,16 @@ +import { CreateNewTaskFormValues } from "../create/taskFormTypes"; import { SampleCase } from "../types"; export const blank = { url: "https://www.example.com", - navigationGoal: null, - dataExtractionGoal: null, + navigationGoal: "", + dataExtractionGoal: "", navigationPayload: null, extractedInformationSchema: null, + webhookCallbackUrl: null, + totpIdentifier: null, + totpVerificationUrl: null, + errorCodeMapping: null, }; export const bci_seguros = { @@ -30,6 +35,10 @@ export const bci_seguros = { "km approx a recorrer": "28,000", }, extractedInformationSchema: null, + webhookCallbackUrl: null, + totpIdentifier: null, + totpVerificationUrl: null, + errorCodeMapping: null, }; export const california_edd = { @@ -47,6 +56,10 @@ export const california_edd = { phone_number: "412-444-1234", }, extractedInformationSchema: null, + webhookCallbackUrl: null, + totpIdentifier: null, + totpVerificationUrl: null, + errorCodeMapping: null, }; export const finditparts = { @@ -59,6 +72,10 @@ export const finditparts = { product_id: "W01-377-8537", }, extractedInformationSchema: null, + webhookCallbackUrl: null, + totpIdentifier: null, + totpVerificationUrl: null, + errorCodeMapping: null, }; export const job_application = { @@ -73,6 +90,10 @@ export const job_application = { "https://writing.colostate.edu/guides/documents/resume/functionalSample.pdf", cover_letter: "Generate a compelling cover letter for me", }, + webhookCallbackUrl: null, + totpIdentifier: null, + totpVerificationUrl: null, + errorCodeMapping: null, }; export const geico = { @@ -263,6 +284,10 @@ export const geico = { }, type: "object", }, + webhookCallbackUrl: null, + totpIdentifier: null, + totpVerificationUrl: null, + errorCodeMapping: null, }; export function getSample(sample: SampleCase) { @@ -329,15 +354,14 @@ function generatePhoneNumber() { } function transformKV([key, value]: [string, unknown]) { - if (value === null) { - return [key, ""]; - } - if (typeof value === "object") { + if (value !== null && typeof value === "object") { return [key, JSON.stringify(value, null, 2)]; } return [key, value]; } -export function getSampleForInitialFormValues(sample: SampleCase) { +export function getSampleForInitialFormValues( + sample: SampleCase, +): CreateNewTaskFormValues { return Object.fromEntries(Object.entries(getSample(sample)).map(transformKV)); }