Skip to content

Commit

Permalink
Add Task v2 Block (#1683)
Browse files Browse the repository at this point in the history
Co-authored-by: Muhammed Salih Altun <[email protected]>
  • Loading branch information
wintonzheng and msalihaltun authored Jan 30, 2025
1 parent 35f9b70 commit d218391
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 4 deletions.
9 changes: 9 additions & 0 deletions skyvern-frontend/src/routes/workflows/editor/helpContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export const basePlaceholderContent = {

export const helpTooltips = {
task: baseHelpTooltipContent,
taskv2: {
...baseHelpTooltipContent,
maxIterations:
"The maximum number of iterations this task will take to achieve its goal.",
},
navigation: baseHelpTooltipContent,
extraction: {
...baseHelpTooltipContent,
Expand Down Expand Up @@ -100,6 +105,10 @@ export const helpTooltips = {

export const placeholders = {
task: basePlaceholderContent,
taskv2: {
...basePlaceholderContent,
prompt: "Tell Skyvern what to do",
},
navigation: {
...basePlaceholderContent,
navigationGoal:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { HelpTooltip } from "@/components/HelpTooltip";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Input } from "@/components/ui/input";
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 { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import { useState } from "react";
import { helpTooltips, placeholders } from "../../helpContent";
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
import { NodeActionMenu } from "../NodeActionMenu";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { MAX_ITERATIONS_DEFAULT, type Taskv2Node } from "./types";

function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
const { updateNodeData } = useReactFlow();
const { editable } = data;
const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useNodeLabelChangeHandler({
id,
initialValue: data.label,
});

const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });

const [inputs, setInputs] = useState({
prompt: data.prompt,
url: data.url,
totpVerificationUrl: data.totpVerificationUrl,
totpIdentifier: data.totpIdentifier,
maxIterations: data.maxIterations,
});

function handleChange(key: string, value: unknown) {
if (!editable) {
return;
}
setInputs({ ...inputs, [key]: value });
updateNodeData(id, { [key]: value });
}

return (
<div>
<Handle
type="source"
position={Position.Bottom}
id="a"
className="opacity-0"
/>
<Handle
type="target"
position={Position.Top}
id="b"
className="opacity-0"
/>
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
<div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Taskv2}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle
value={label}
editable={editable}
onChange={setLabel}
titleClassName="text-base"
inputClassName="text-base"
/>
<span className="text-xs text-slate-400">Task v2 Block</span>
</div>
</div>
<NodeActionMenu
onDelete={() => {
deleteNodeCallback(id);
}}
/>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between">
<Label className="text-xs text-slate-300">Prompt</Label>
{isFirstWorkflowBlock ? (
<div className="flex justify-end text-xs text-slate-400">
Tip: Use the {"+"} button to add parameters!
</div>
) : null}
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("prompt", value);
}}
value={inputs.prompt}
placeholder={placeholders[type]["prompt"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<Label className="text-xs text-slate-300">URL</Label>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("url", value);
}}
value={inputs.url}
placeholder={placeholders[type]["url"]}
className="nopan text-xs"
/>
</div>
</div>
<Separator />
<Accordion type="single" collapsible>
<AccordionItem value="advanced" className="border-b-0">
<AccordionTrigger className="py-0">
Advanced Settings
</AccordionTrigger>
<AccordionContent className="pl-6 pr-1 pt-4">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Max Iterations
</Label>
<HelpTooltip
content={helpTooltips[type]["maxIterations"]}
/>
</div>
<Input
type="number"
placeholder="10"
className="nopan text-xs"
value={data.maxIterations ?? MAX_ITERATIONS_DEFAULT}
onChange={(event) => {
handleChange("maxIterations", Number(event.target.value));
}}
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Identifier
</Label>
<HelpTooltip
content={helpTooltips[type]["totpIdentifier"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("totpIdentifier", value);
}}
value={inputs.totpIdentifier ?? ""}
placeholder={placeholders["navigation"]["totpIdentifier"]}
className="nopan text-xs"
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
);
}

export { Taskv2Node };
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Node } from "@xyflow/react";
import { NodeBaseData } from "../types";

export const MAX_ITERATIONS_DEFAULT = 10;

export type Taskv2NodeData = NodeBaseData & {
prompt: string;
url: string;
totpVerificationUrl: string | null;
totpIdentifier: string | null;
maxIterations: number | null;
};

export type Taskv2Node = Node<Taskv2NodeData, "taskv2">;

export const taskv2NodeDefaultData: Taskv2NodeData = {
label: "",
continueOnFailure: false,
editable: true,
prompt: "",
url: "",
totpIdentifier: null,
totpVerificationUrl: null,
maxIterations: 10,
};

export function isTaskV2Node(node: Node): node is Taskv2Node {
return node.type === "taskv2";
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ function WorkflowBlockIcon({ workflowBlockType, className }: Props) {
case "send_email": {
return <EnvelopeClosedIcon className={className} />;
}
case "task": {
case "task":
case "task_v2": {
return <ListBulletIcon className={className} />;
}
case "text_prompt": {
Expand Down
6 changes: 5 additions & 1 deletion skyvern-frontend/src/routes/workflows/editor/nodes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import { FileDownloadNode } from "./FileDownloadNode/types";
import { FileDownloadNode as FileDownloadNodeComponent } from "./FileDownloadNode/FileDownloadNode";
import { PDFParserNode } from "./PDFParserNode/types";
import { PDFParserNode as PDFParserNodeComponent } from "./PDFParserNode/PDFParserNode";
import { Taskv2Node } from "./Taskv2Node/types";
import { Taskv2Node as Taskv2NodeComponent } from "./Taskv2Node/Taskv2Node";

export type UtilityNode = StartNode | NodeAdderNode;

Expand All @@ -54,7 +56,8 @@ export type WorkflowBlockNode =
| LoginNode
| WaitNode
| FileDownloadNode
| PDFParserNode;
| PDFParserNode
| Taskv2Node;

export function isUtilityNode(node: AppNode): node is UtilityNode {
return node.type === "nodeAdder" || node.type === "start";
Expand Down Expand Up @@ -85,4 +88,5 @@ export const nodeTypes = {
wait: memo(WaitNodeComponent),
fileDownload: memo(FileDownloadNodeComponent),
pdfParser: memo(PDFParserNodeComponent),
taskv2: memo(Taskv2NodeComponent),
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ export const workflowBlockTitle: {
validation: "Validation",
wait: "Wait",
pdf_parser: "PDF Parser",
task_v2: "Task v2",
};
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ const nodeLibraryItems: Array<{
title: "Task Block",
description: "Takes actions or extracts information",
},
{
nodeType: "taskv2",
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Taskv2}
className="size-6"
/>
),
title: "Task v2 Block",
description: "Runs a Skyvern v2 Task",
},
{
nodeType: "textPrompt",
icon: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
WaitBlockYAML,
FileDownloadBlockYAML,
PDFParserBlockYAML,
Taskv2BlockYAML,
} from "../types/workflowYamlTypes";
import {
EMAIL_BLOCK_SENDER,
Expand Down Expand Up @@ -86,6 +87,7 @@ import { isWaitNode, waitNodeDefaultData } from "./nodes/WaitNode/types";
import { fileDownloadNodeDefaultData } from "./nodes/FileDownloadNode/types";
import { ProxyLocation } from "@/api/types";
import { pdfParserNodeDefaultData } from "./nodes/PDFParserNode/types";
import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types";

export const NEW_NODE_LABEL_PREFIX = "block_";

Expand Down Expand Up @@ -199,6 +201,21 @@ function convertToNode(
},
};
}
case "task_v2": {
return {
...identifiers,
...common,
type: "taskv2",
data: {
...commonData,
prompt: block.prompt,
url: block.url ?? "",
maxIterations: block.max_iterations,
totpIdentifier: block.totp_identifier,
totpVerificationUrl: block.totp_verification_url,
},
};
}
case "validation": {
return {
...identifiers,
Expand Down Expand Up @@ -650,6 +667,17 @@ function createNode(
},
};
}
case "taskv2": {
return {
...identifiers,
...common,
type: "taskv2",
data: {
...taskv2NodeDefaultData,
label,
},
};
}
case "validation": {
return {
...identifiers,
Expand Down Expand Up @@ -859,6 +887,17 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
cache_actions: node.data.cacheActions,
};
}
case "taskv2": {
return {
...base,
block_type: "task_v2",
prompt: node.data.prompt,
max_iterations: node.data.maxIterations,
totp_identifier: node.data.totpIdentifier,
totp_verification_url: node.data.totpVerificationUrl,
url: node.data.url,
};
}
case "validation": {
return {
...base,
Expand Down Expand Up @@ -1513,6 +1552,18 @@ function convertBlocksToBlockYAML(
};
return blockYaml;
}
case "task_v2": {
const blockYaml: Taskv2BlockYAML = {
...base,
block_type: "task_v2",
prompt: block.prompt,
url: block.url,
max_iterations: block.max_iterations,
totp_identifier: block.totp_identifier,
totp_verification_url: block.totp_verification_url,
};
return blockYaml;
}
case "validation": {
const blockYaml: ValidationBlockYAML = {
...base,
Expand Down
Loading

0 comments on commit d218391

Please sign in to comment.