From 6c33a6707ba5537a094d68329a29704963a6fbb6 Mon Sep 17 00:00:00 2001 From: Shuchang Zheng Date: Mon, 25 Nov 2024 12:44:54 -0800 Subject: [PATCH] Add extraction node (#1259) --- .../src/components/icons/ExtractIcon.tsx | 40 +++ .../routes/workflows/editor/FlowRenderer.tsx | 126 ++------ .../nodes/ExtractionNode/ExtractionNode.tsx | 268 ++++++++++++++++++ .../editor/nodes/ExtractionNode/types.ts | 31 ++ .../routes/workflows/editor/nodes/index.ts | 6 +- .../panels/WorkflowNodeLibraryPanel.tsx | 7 + .../workflows/editor/workflowEditorUtils.ts | 176 +++++++++++- .../routes/workflows/types/workflowTypes.ts | 38 ++- .../workflows/types/workflowYamlTypes.ts | 18 +- 9 files changed, 587 insertions(+), 123 deletions(-) create mode 100644 skyvern-frontend/src/components/icons/ExtractIcon.tsx create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/types.ts diff --git a/skyvern-frontend/src/components/icons/ExtractIcon.tsx b/skyvern-frontend/src/components/icons/ExtractIcon.tsx new file mode 100644 index 00000000..34c67989 --- /dev/null +++ b/skyvern-frontend/src/components/icons/ExtractIcon.tsx @@ -0,0 +1,40 @@ +type Props = { + className?: string; +}; + +function ExtractIcon({ className }: Props) { + return ( + + + + + + ); +} + +export { ExtractIcon }; diff --git a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx index c20b5db5..fdd62df0 100644 --- a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx @@ -1,5 +1,21 @@ +import { getClient } from "@/api/AxiosClient"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { toast } from "@/components/ui/use-toast"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab"; import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext"; +import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore"; import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore"; +import { ReloadIcon } from "@radix-ui/react-icons"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Background, BackgroundVariant, @@ -12,8 +28,11 @@ import { useNodesState, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; +import { AxiosError } from "axios"; import { nanoid } from "nanoid"; import { useEffect, useState } from "react"; +import { useBlocker, useParams } from "react-router-dom"; +import { stringify as convertToYAML } from "yaml"; import { AWSSecretParameter, WorkflowApiResponse, @@ -31,12 +50,7 @@ import { import { WorkflowHeader } from "./WorkflowHeader"; import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext"; import { edgeTypes } from "./edges"; -import { - AppNode, - isWorkflowBlockNode, - nodeTypes, - WorkflowBlockNode, -} from "./nodes"; +import { AppNode, nodeTypes, WorkflowBlockNode } from "./nodes"; import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel"; import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel"; import "./reactFlowOverrideStyles.css"; @@ -48,33 +62,11 @@ import { getAdditionalParametersForEmailBlock, getOutputParameterKey, getWorkflowBlocks, + getWorkflowErrors, layout, nodeAdderNode, startNode, } from "./workflowEditorUtils"; -import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore"; -import { useBlocker, useParams } from "react-router-dom"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { getClient } from "@/api/AxiosClient"; -import { useCredentialGetter } from "@/hooks/useCredentialGetter"; -import { stringify as convertToYAML } from "yaml"; -import { toast } from "@/components/ui/use-toast"; -import { AxiosError } from "axios"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { ReloadIcon } from "@radix-ui/react-icons"; -import { isLoopNode, LoopNode } from "./nodes/LoopNode/types"; -import { isTaskNode } from "./nodes/TaskNode/types"; -import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab"; -import { isValidationNode } from "./nodes/ValidationNode/types"; -import { isActionNode } from "./nodes/ActionNode/types"; function convertToParametersYAML( parameters: ParametersState, @@ -456,80 +448,6 @@ function FlowRenderer({ doLayout(newNodesWithUpdatedParameters, newEdges); } - function getWorkflowErrors(): Array { - const errors: Array = []; - - const workflowBlockNodes = nodes.filter(isWorkflowBlockNode); - if ( - workflowBlockNodes.length > 0 && - workflowBlockNodes[0]!.type === "validation" - ) { - const label = workflowBlockNodes[0]!.data.label; - errors.push( - `${label}: Validation block can't be the first block in a workflow.`, - ); - } - - const actionNodes = nodes.filter(isActionNode); - actionNodes.forEach((node) => { - if (node.data.navigationGoal.length === 0) { - errors.push(`${node.data.label}: Action Instruction is required.`); - } - try { - JSON.parse(node.data.errorCodeMapping); - } catch { - errors.push(`${node.data.label}: Error messages is not valid JSON.`); - } - }); - - // check loop node parameters - const loopNodes: Array = nodes.filter(isLoopNode); - const emptyLoopNodes = loopNodes.filter( - (node: LoopNode) => node.data.loopValue === "", - ); - if (emptyLoopNodes.length > 0) { - emptyLoopNodes.forEach((node) => { - errors.push( - `${node.data.label}: Loop value parameter must be selected.`, - ); - }); - } - - // check task node json fields - const taskNodes = nodes.filter(isTaskNode); - taskNodes.forEach((node) => { - try { - JSON.parse(node.data.dataSchema); - } catch { - errors.push(`${node.data.label}: Data schema is not valid JSON.`); - } - try { - JSON.parse(node.data.errorCodeMapping); - } catch { - errors.push(`${node.data.label}: Error messages is not valid JSON.`); - } - }); - - const validationNodes = nodes.filter(isValidationNode); - validationNodes.forEach((node) => { - try { - JSON.parse(node.data.errorCodeMapping); - } catch { - errors.push(`${node.data.label}: Error messages is not valid JSON`); - } - if ( - node.data.completeCriterion.length === 0 && - node.data.terminateCriterion.length === 0 - ) { - errors.push( - `${node.data.label}: At least one of completion or termination criteria must be provided`, - ); - } - }); - - return errors; - } - return ( <> { - const errors = getWorkflowErrors(); + const errors = getWorkflowErrors(nodes); if (errors.length > 0) { toast({ title: "Can not save workflow because of errors:", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx new file mode 100644 index 00000000..c7168bfc --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx @@ -0,0 +1,268 @@ +import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; +import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; +import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; +import { useState } from "react"; +import { EditableNodeTitle } from "../components/EditableNodeTitle"; +import { NodeActionMenu } from "../NodeActionMenu"; +import { HelpTooltip } from "@/components/HelpTooltip"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { dataSchemaExampleValue } from "../types"; +import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; +import { Switch } from "@/components/ui/switch"; +import type { ExtractionNode } from "./types"; +import { + commonFieldPlaceholders, + commonHelpTooltipContent, +} from "../../constants"; +import { ExtractIcon } from "@/components/icons/ExtractIcon"; + +const dataExtractionGoalTooltip = + "Tell Skyvern what data you would like to scrape."; +const dataSchemaTooltip = "Specify a format for extracted data in JSON."; +const dataExtractionGoalPlaceholder = "What data do you need to extract?"; + +function ExtractionNode({ id, data }: NodeProps) { + const { updateNodeData } = useReactFlow(); + const { editable } = data; + const [label, setLabel] = useNodeLabelChangeHandler({ + id, + initialValue: data.label, + }); + const [inputs, setInputs] = useState({ + url: data.url, + dataExtractionGoal: data.dataExtractionGoal, + dataSchema: data.dataSchema, + maxRetries: data.maxRetries, + maxStepsOverride: data.maxStepsOverride, + continueOnFailure: data.continueOnFailure, + cacheActions: data.cacheActions, + }); + const deleteNodeCallback = useDeleteNodeCallback(); + + function handleChange(key: string, value: unknown) { + if (!editable) { + return; + } + setInputs({ ...inputs, [key]: value }); + updateNodeData(id, { [key]: value }); + } + + return ( +
+ + +
+
+
+
+ +
+
+ + Extraction Block +
+
+ { + deleteNodeCallback(id); + }} + /> +
+
+
+ + +
+ { + if (!editable) { + return; + } + handleChange("dataExtractionGoal", event.target.value); + }} + value={inputs.dataExtractionGoal} + placeholder={dataExtractionGoalPlaceholder} + className="nopan text-xs" + /> +
+
+
+
+ + +
+ { + if (!editable) { + return; + } + handleChange( + "dataSchema", + checked + ? JSON.stringify(dataSchemaExampleValue, null, 2) + : "null", + ); + }} + /> +
+ {inputs.dataSchema !== "null" && ( +
+ { + if (!editable) { + return; + } + handleChange("dataSchema", value); + }} + className="nowheel nopan" + fontSize={8} + /> +
+ )} +
+ + + + + Advanced Settings + + +
+
+
+ + +
+ { + if (!editable) { + return; + } + const value = + event.target.value === "" + ? null + : Number(event.target.value); + handleChange("maxRetries", value); + }} + /> +
+
+
+ + +
+ { + if (!editable) { + return; + } + const value = + event.target.value === "" + ? null + : Number(event.target.value); + handleChange("maxStepsOverride", value); + }} + /> +
+ +
+
+ + +
+
+ { + if (!editable) { + return; + } + handleChange("continueOnFailure", checked); + }} + /> +
+
+
+
+ + +
+
+ { + if (!editable) { + return; + } + handleChange("cacheActions", checked); + }} + /> +
+
+
+
+
+
+
+
+ ); +} + +export { ExtractionNode }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/types.ts new file mode 100644 index 00000000..bb724dc1 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/types.ts @@ -0,0 +1,31 @@ +import type { Node } from "@xyflow/react"; +import { NodeBaseData } from "../types"; + +export type ExtractionNodeData = NodeBaseData & { + url: string; + dataExtractionGoal: string; + dataSchema: string; + maxRetries: number | null; + maxStepsOverride: number | null; + parameterKeys: Array; + cacheActions: boolean; +}; + +export type ExtractionNode = Node; + +export const extractionNodeDefaultData: ExtractionNodeData = { + label: "", + url: "", + dataExtractionGoal: "", + dataSchema: "null", + maxRetries: null, + maxStepsOverride: null, + editable: true, + parameterKeys: [], + continueOnFailure: false, + cacheActions: false, +} as const; + +export function isExtractionNode(node: Node): node is ExtractionNode { + return node.type === "extraction"; +} diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts index 7b152509..c2b4e784 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts @@ -25,6 +25,8 @@ import type { ActionNode } from "./ActionNode/types"; import { ActionNode as ActionNodeComponent } from "./ActionNode/ActionNode"; import { NavigationNode } from "./NavigationNode/types"; import { NavigationNode as NavigationNodeComponent } from "./NavigationNode/NavigationNode"; +import { ExtractionNode } from "./ExtractionNode/types"; +import { ExtractionNode as ExtractionNodeComponent } from "./ExtractionNode/ExtractionNode"; export type UtilityNode = StartNode | NodeAdderNode; @@ -39,7 +41,8 @@ export type WorkflowBlockNode = | DownloadNode | ValidationNode | ActionNode - | NavigationNode; + | NavigationNode + | ExtractionNode; export function isUtilityNode(node: AppNode): node is UtilityNode { return node.type === "nodeAdder" || node.type === "start"; @@ -65,4 +68,5 @@ export const nodeTypes = { validation: memo(ValidationNodeComponent), action: memo(ActionNodeComponent), navigation: memo(NavigationNodeComponent), + extraction: memo(ExtractionNodeComponent), } as const; diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx index 5268f8be..4189897e 100644 --- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx @@ -15,6 +15,7 @@ import { AddNodeProps } from "../FlowRenderer"; import { ClickIcon } from "@/components/icons/ClickIcon"; import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area"; import { RobotIcon } from "@/components/icons/RobotIcon"; +import { ExtractIcon } from "@/components/icons/ExtractIcon"; const nodeLibraryItems: Array<{ nodeType: NonNullable; @@ -90,6 +91,12 @@ const nodeLibraryItems: Array<{ title: "Navigation Block", description: "Navigate on the page", }, + { + nodeType: "extraction", + icon: , + title: "Extraction Block", + description: "Extract data from the page", + }, ]; type Props = { diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts index 2a584983..f2f48259 100644 --- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts @@ -25,6 +25,7 @@ import { ValidationBlockYAML, NavigationBlockYAML, WorkflowCreateYAMLRequest, + ExtractionBlockYAML, } from "../types/workflowYamlTypes"; import { EMAIL_BLOCK_SENDER, @@ -43,17 +44,31 @@ import { AppNode, isWorkflowBlockNode, WorkflowBlockNode } from "./nodes"; import { codeBlockNodeDefaultData } from "./nodes/CodeBlockNode/types"; import { downloadNodeDefaultData } from "./nodes/DownloadNode/types"; import { fileParserNodeDefaultData } from "./nodes/FileParserNode/types"; -import { LoopNode, loopNodeDefaultData } from "./nodes/LoopNode/types"; +import { + isLoopNode, + LoopNode, + loopNodeDefaultData, +} from "./nodes/LoopNode/types"; import { NodeAdderNode } from "./nodes/NodeAdderNode/types"; import { sendEmailNodeDefaultData } from "./nodes/SendEmailNode/types"; import { StartNode } from "./nodes/StartNode/types"; -import { taskNodeDefaultData } from "./nodes/TaskNode/types"; +import { isTaskNode, taskNodeDefaultData } from "./nodes/TaskNode/types"; import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types"; import { NodeBaseData } from "./nodes/types"; import { uploadNodeDefaultData } from "./nodes/UploadNode/types"; -import { validationNodeDefaultData } from "./nodes/ValidationNode/types"; -import { actionNodeDefaultData } from "./nodes/ActionNode/types"; -import { navigationNodeDefaultData } from "./nodes/NavigationNode/types"; +import { + isValidationNode, + validationNodeDefaultData, +} from "./nodes/ValidationNode/types"; +import { actionNodeDefaultData, isActionNode } from "./nodes/ActionNode/types"; +import { + isNavigationNode, + navigationNodeDefaultData, +} from "./nodes/NavigationNode/types"; +import { + extractionNodeDefaultData, + isExtractionNode, +} from "./nodes/ExtractionNode/types"; export const NEW_NODE_LABEL_PREFIX = "block_"; @@ -220,6 +235,23 @@ function convertToNode( }, }; } + case "extraction": { + return { + ...identifiers, + ...common, + type: "extraction", + data: { + ...commonData, + url: block.url ?? "", + dataExtractionGoal: block.data_extraction_goal ?? "", + dataSchema: JSON.stringify(block.data_schema, null, 2), + parameterKeys: block.parameters.map((p) => p.key), + maxRetries: block.max_retries ?? null, + maxStepsOverride: block.max_steps_per_run ?? null, + cacheActions: block.cache_actions, + }, + }; + } case "code": { return { ...identifiers, @@ -543,6 +575,17 @@ function createNode( }, }; } + case "extraction": { + return { + ...identifiers, + ...common, + type: "extraction", + data: { + ...extractionNodeDefaultData, + label, + }, + }; + } case "loop": { return { ...identifiers, @@ -679,6 +722,7 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML { return { ...base, block_type: "action", + title: node.data.label, navigation_goal: node.data.navigationGoal, error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record< string, @@ -700,6 +744,7 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML { return { ...base, block_type: "navigation", + title: node.data.label, navigation_goal: node.data.navigationGoal, error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record< string, @@ -718,6 +763,22 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML { cache_actions: node.data.cacheActions, }; } + case "extraction": { + return { + ...base, + block_type: "extraction", + url: node.data.url, + title: node.data.label, + data_extraction_goal: node.data.dataExtractionGoal, + data_schema: JSONParseSafe(node.data.dataSchema), + ...(node.data.maxRetries !== null && { + max_retries: node.data.maxRetries, + }), + max_steps_per_run: node.data.maxStepsOverride, + parameter_keys: node.data.parameterKeys, + cache_actions: node.data.cacheActions, + }; + } case "sendEmail": { return { ...base, @@ -1174,6 +1235,7 @@ function convertBlocksToBlockYAML( const blockYaml: TaskBlockYAML = { ...base, block_type: "task", + title: block.title, url: block.url, navigation_goal: block.navigation_goal, data_extraction_goal: block.data_extraction_goal, @@ -1206,6 +1268,7 @@ function convertBlocksToBlockYAML( ...base, block_type: "action", url: block.url, + title: block.title, navigation_goal: block.navigation_goal, error_code_mapping: block.error_code_mapping, max_retries: block.max_retries, @@ -1223,6 +1286,7 @@ function convertBlocksToBlockYAML( ...base, block_type: "navigation", url: block.url, + title: block.title, navigation_goal: block.navigation_goal, error_code_mapping: block.error_code_mapping, max_retries: block.max_retries, @@ -1236,6 +1300,21 @@ function convertBlocksToBlockYAML( }; return blockYaml; } + case "extraction": { + const blockYaml: ExtractionBlockYAML = { + ...base, + block_type: "extraction", + url: block.url, + title: block.title, + data_extraction_goal: block.data_extraction_goal, + data_schema: block.data_schema, + max_retries: block.max_retries, + max_steps_per_run: block.max_steps_per_run, + parameter_keys: block.parameters.map((p) => p.key), + cache_actions: block.cache_actions, + }; + return blockYaml; + } case "for_loop": { const blockYaml: ForLoopBlockYAML = { ...base, @@ -1327,6 +1406,92 @@ function convert(workflow: WorkflowApiResponse): WorkflowCreateYAMLRequest { }; } +function getWorkflowErrors(nodes: Array): Array { + const errors: Array = []; + + const workflowBlockNodes = nodes.filter(isWorkflowBlockNode); + if ( + workflowBlockNodes.length > 0 && + workflowBlockNodes[0]!.type === "validation" + ) { + const label = workflowBlockNodes[0]!.data.label; + errors.push( + `${label}: Validation block can't be the first block in a workflow.`, + ); + } + + const actionNodes = nodes.filter(isActionNode); + actionNodes.forEach((node) => { + if (node.data.navigationGoal.length === 0) { + errors.push(`${node.data.label}: Action Instruction is required.`); + } + try { + JSON.parse(node.data.errorCodeMapping); + } catch { + errors.push(`${node.data.label}: Error messages is not valid JSON.`); + } + }); + + // check loop node parameters + const loopNodes: Array = nodes.filter(isLoopNode); + const emptyLoopNodes = loopNodes.filter( + (node: LoopNode) => node.data.loopValue === "", + ); + if (emptyLoopNodes.length > 0) { + emptyLoopNodes.forEach((node) => { + errors.push(`${node.data.label}: Loop value parameter must be selected.`); + }); + } + + // check task node json fields + const taskNodes = nodes.filter(isTaskNode); + taskNodes.forEach((node) => { + try { + JSON.parse(node.data.dataSchema); + } catch { + errors.push(`${node.data.label}: Data schema is not valid JSON.`); + } + try { + JSON.parse(node.data.errorCodeMapping); + } catch { + errors.push(`${node.data.label}: Error messages is not valid JSON.`); + } + }); + + const validationNodes = nodes.filter(isValidationNode); + validationNodes.forEach((node) => { + try { + JSON.parse(node.data.errorCodeMapping); + } catch { + errors.push(`${node.data.label}: Error messages is not valid JSON`); + } + if ( + node.data.completeCriterion.length === 0 && + node.data.terminateCriterion.length === 0 + ) { + errors.push( + `${node.data.label}: At least one of completion or termination criteria must be provided`, + ); + } + }); + + const navigationNodes = nodes.filter(isNavigationNode); + navigationNodes.forEach((node) => { + if (node.data.navigationGoal.length === 0) { + errors.push(`${node.data.label}: Navigation goal is required.`); + } + }); + + const extractionNodes = nodes.filter(isExtractionNode); + extractionNodes.forEach((node) => { + if (node.data.dataExtractionGoal.length === 0) { + errors.push(`${node.data.label}: Data extraction goal is required.`); + } + }); + + return errors; +} + export { convert, convertEchoParameters, @@ -1344,6 +1509,7 @@ export { getUpdatedNodesAfterLabelUpdateForParameterKeys, getUpdatedParametersAfterLabelUpdateForSourceParameterKey, getWorkflowBlocks, + getWorkflowErrors, isOutputParameterKey, layout, }; diff --git a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts index 83b323ce..b7dd18bf 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts @@ -101,6 +101,20 @@ export type Parameter = | BitwardenSensitiveInformationParameter | AWSSecretParameter; +export type WorkflowBlock = + | TaskBlock + | ForLoopBlock + | TextPromptBlock + | CodeBlock + | UploadToS3Block + | DownloadToS3Block + | SendEmailBlock + | FileURLParserBlock + | ValidationBlock + | ActionBlock + | NavigationBlock + | ExtractionBlock; + export const WorkflowBlockType = { Task: "task", ForLoop: "for_loop", @@ -113,6 +127,7 @@ export const WorkflowBlockType = { Validation: "validation", Action: "action", Navigation: "navigation", + Extraction: "extraction", } as const; export type WorkflowBlockType = @@ -232,18 +247,17 @@ export type NavigationBlock = WorkflowBlockBase & { cache_actions: boolean; }; -export type WorkflowBlock = - | TaskBlock - | ForLoopBlock - | TextPromptBlock - | CodeBlock - | UploadToS3Block - | DownloadToS3Block - | SendEmailBlock - | FileURLParserBlock - | ValidationBlock - | ActionBlock - | NavigationBlock; +export type ExtractionBlock = WorkflowBlockBase & { + block_type: "extraction"; + data_extraction_goal: string | null; + url: string | null; + title: string; + data_schema: Record | null; + max_retries?: number; + max_steps_per_run?: number | null; + parameters: Array; + cache_actions: boolean; +}; export type WorkflowDefinition = { parameters: Array; diff --git a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts index 5ac3de8f..81c32a06 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts @@ -78,6 +78,7 @@ const BlockTypes = { VALIDATION: "validation", ACTION: "action", NAVIGATION: "navigation", + EXTRACTION: "extraction", } as const; export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes]; @@ -93,7 +94,8 @@ export type BlockYAML = | ForLoopBlockYAML | ValidationBlockYAML | ActionBlockYAML - | NavigationBlockYAML; + | NavigationBlockYAML + | ExtractionBlockYAML; export type BlockYAMLBase = { block_type: BlockType; @@ -130,6 +132,7 @@ export type ValidationBlockYAML = BlockYAMLBase & { export type ActionBlockYAML = BlockYAMLBase & { block_type: "action"; url: string | null; + title?: string; navigation_goal: string | null; error_code_mapping: Record | null; max_retries?: number; @@ -144,6 +147,7 @@ export type ActionBlockYAML = BlockYAMLBase & { export type NavigationBlockYAML = BlockYAMLBase & { block_type: "navigation"; url: string | null; + title?: string; navigation_goal: string | null; error_code_mapping: Record | null; max_retries?: number; @@ -156,6 +160,18 @@ export type NavigationBlockYAML = BlockYAMLBase & { cache_actions: boolean; }; +export type ExtractionBlockYAML = BlockYAMLBase & { + block_type: "extraction"; + url: string | null; + title?: string; + data_extraction_goal: string | null; + data_schema: Record | null; + max_retries?: number; + max_steps_per_run?: number | null; + parameter_keys?: Array | null; + cache_actions: boolean; +}; + export type CodeBlockYAML = BlockYAMLBase & { block_type: "code"; code: string;