Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Task v2 Block #1683

Merged
merged 3 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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