Add Task v2 Block (#1683)
Co-authored-by: Muhammed Salih Altun <muhammedsalihaltun@gmail.com>
This commit is contained in:
@@ -39,6 +39,11 @@ export const basePlaceholderContent = {
|
|||||||
|
|
||||||
export const helpTooltips = {
|
export const helpTooltips = {
|
||||||
task: baseHelpTooltipContent,
|
task: baseHelpTooltipContent,
|
||||||
|
taskv2: {
|
||||||
|
...baseHelpTooltipContent,
|
||||||
|
maxIterations:
|
||||||
|
"The maximum number of iterations this task will take to achieve its goal.",
|
||||||
|
},
|
||||||
navigation: baseHelpTooltipContent,
|
navigation: baseHelpTooltipContent,
|
||||||
extraction: {
|
extraction: {
|
||||||
...baseHelpTooltipContent,
|
...baseHelpTooltipContent,
|
||||||
@@ -100,6 +105,10 @@ export const helpTooltips = {
|
|||||||
|
|
||||||
export const placeholders = {
|
export const placeholders = {
|
||||||
task: basePlaceholderContent,
|
task: basePlaceholderContent,
|
||||||
|
taskv2: {
|
||||||
|
...basePlaceholderContent,
|
||||||
|
prompt: "Tell Skyvern what to do",
|
||||||
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
...basePlaceholderContent,
|
...basePlaceholderContent,
|
||||||
navigationGoal:
|
navigationGoal:
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -53,7 +53,8 @@ function WorkflowBlockIcon({ workflowBlockType, className }: Props) {
|
|||||||
case "send_email": {
|
case "send_email": {
|
||||||
return <EnvelopeClosedIcon className={className} />;
|
return <EnvelopeClosedIcon className={className} />;
|
||||||
}
|
}
|
||||||
case "task": {
|
case "task":
|
||||||
|
case "task_v2": {
|
||||||
return <ListBulletIcon className={className} />;
|
return <ListBulletIcon className={className} />;
|
||||||
}
|
}
|
||||||
case "text_prompt": {
|
case "text_prompt": {
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import { FileDownloadNode } from "./FileDownloadNode/types";
|
|||||||
import { FileDownloadNode as FileDownloadNodeComponent } from "./FileDownloadNode/FileDownloadNode";
|
import { FileDownloadNode as FileDownloadNodeComponent } from "./FileDownloadNode/FileDownloadNode";
|
||||||
import { PDFParserNode } from "./PDFParserNode/types";
|
import { PDFParserNode } from "./PDFParserNode/types";
|
||||||
import { PDFParserNode as PDFParserNodeComponent } from "./PDFParserNode/PDFParserNode";
|
import { PDFParserNode as PDFParserNodeComponent } from "./PDFParserNode/PDFParserNode";
|
||||||
|
import { Taskv2Node } from "./Taskv2Node/types";
|
||||||
|
import { Taskv2Node as Taskv2NodeComponent } from "./Taskv2Node/Taskv2Node";
|
||||||
|
|
||||||
export type UtilityNode = StartNode | NodeAdderNode;
|
export type UtilityNode = StartNode | NodeAdderNode;
|
||||||
|
|
||||||
@@ -54,7 +56,8 @@ export type WorkflowBlockNode =
|
|||||||
| LoginNode
|
| LoginNode
|
||||||
| WaitNode
|
| WaitNode
|
||||||
| FileDownloadNode
|
| FileDownloadNode
|
||||||
| PDFParserNode;
|
| PDFParserNode
|
||||||
|
| Taskv2Node;
|
||||||
|
|
||||||
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
||||||
return node.type === "nodeAdder" || node.type === "start";
|
return node.type === "nodeAdder" || node.type === "start";
|
||||||
@@ -85,4 +88,5 @@ export const nodeTypes = {
|
|||||||
wait: memo(WaitNodeComponent),
|
wait: memo(WaitNodeComponent),
|
||||||
fileDownload: memo(FileDownloadNodeComponent),
|
fileDownload: memo(FileDownloadNodeComponent),
|
||||||
pdfParser: memo(PDFParserNodeComponent),
|
pdfParser: memo(PDFParserNodeComponent),
|
||||||
|
taskv2: memo(Taskv2NodeComponent),
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -46,4 +46,5 @@ export const workflowBlockTitle: {
|
|||||||
validation: "Validation",
|
validation: "Validation",
|
||||||
wait: "Wait",
|
wait: "Wait",
|
||||||
pdf_parser: "PDF Parser",
|
pdf_parser: "PDF Parser",
|
||||||
|
task_v2: "Task v2",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,6 +67,17 @@ const nodeLibraryItems: Array<{
|
|||||||
title: "Task Block",
|
title: "Task Block",
|
||||||
description: "Takes actions or extracts information",
|
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",
|
nodeType: "textPrompt",
|
||||||
icon: (
|
icon: (
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
WaitBlockYAML,
|
WaitBlockYAML,
|
||||||
FileDownloadBlockYAML,
|
FileDownloadBlockYAML,
|
||||||
PDFParserBlockYAML,
|
PDFParserBlockYAML,
|
||||||
|
Taskv2BlockYAML,
|
||||||
} from "../types/workflowYamlTypes";
|
} from "../types/workflowYamlTypes";
|
||||||
import {
|
import {
|
||||||
EMAIL_BLOCK_SENDER,
|
EMAIL_BLOCK_SENDER,
|
||||||
@@ -86,6 +87,7 @@ import { isWaitNode, waitNodeDefaultData } from "./nodes/WaitNode/types";
|
|||||||
import { fileDownloadNodeDefaultData } from "./nodes/FileDownloadNode/types";
|
import { fileDownloadNodeDefaultData } from "./nodes/FileDownloadNode/types";
|
||||||
import { ProxyLocation } from "@/api/types";
|
import { ProxyLocation } from "@/api/types";
|
||||||
import { pdfParserNodeDefaultData } from "./nodes/PDFParserNode/types";
|
import { pdfParserNodeDefaultData } from "./nodes/PDFParserNode/types";
|
||||||
|
import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types";
|
||||||
|
|
||||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||||
|
|
||||||
@@ -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": {
|
case "validation": {
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
@@ -650,6 +667,17 @@ function createNode(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "taskv2": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "taskv2",
|
||||||
|
data: {
|
||||||
|
...taskv2NodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
case "validation": {
|
case "validation": {
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
@@ -859,6 +887,17 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
|||||||
cache_actions: node.data.cacheActions,
|
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": {
|
case "validation": {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -1513,6 +1552,18 @@ function convertBlocksToBlockYAML(
|
|||||||
};
|
};
|
||||||
return blockYaml;
|
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": {
|
case "validation": {
|
||||||
const blockYaml: ValidationBlockYAML = {
|
const blockYaml: ValidationBlockYAML = {
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
@@ -155,7 +155,8 @@ export type WorkflowBlock =
|
|||||||
| LoginBlock
|
| LoginBlock
|
||||||
| WaitBlock
|
| WaitBlock
|
||||||
| FileDownloadBlock
|
| FileDownloadBlock
|
||||||
| PDFParserBlock;
|
| PDFParserBlock
|
||||||
|
| Taskv2Block;
|
||||||
|
|
||||||
export const WorkflowBlockTypes = {
|
export const WorkflowBlockTypes = {
|
||||||
Task: "task",
|
Task: "task",
|
||||||
@@ -174,6 +175,7 @@ export const WorkflowBlockTypes = {
|
|||||||
Wait: "wait",
|
Wait: "wait",
|
||||||
FileDownload: "file_download",
|
FileDownload: "file_download",
|
||||||
PDFParser: "pdf_parser",
|
PDFParser: "pdf_parser",
|
||||||
|
Taskv2: "task_v2",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function isTaskVariantBlock(item: {
|
export function isTaskVariantBlock(item: {
|
||||||
@@ -231,6 +233,15 @@ export type TaskBlock = WorkflowBlockBase & {
|
|||||||
cache_actions: boolean;
|
cache_actions: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Taskv2Block = WorkflowBlockBase & {
|
||||||
|
block_type: "task_v2";
|
||||||
|
prompt: string;
|
||||||
|
url: string | null;
|
||||||
|
totp_verification_url: string | null;
|
||||||
|
totp_identifier: string | null;
|
||||||
|
max_iterations: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ForLoopBlock = WorkflowBlockBase & {
|
export type ForLoopBlock = WorkflowBlockBase & {
|
||||||
block_type: "for_loop";
|
block_type: "for_loop";
|
||||||
loop_over: WorkflowParameter;
|
loop_over: WorkflowParameter;
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ export type BlockYAML =
|
|||||||
| LoginBlockYAML
|
| LoginBlockYAML
|
||||||
| WaitBlockYAML
|
| WaitBlockYAML
|
||||||
| FileDownloadBlockYAML
|
| FileDownloadBlockYAML
|
||||||
| PDFParserBlockYAML;
|
| PDFParserBlockYAML
|
||||||
|
| Taskv2BlockYAML;
|
||||||
|
|
||||||
export type BlockYAMLBase = {
|
export type BlockYAMLBase = {
|
||||||
block_type: WorkflowBlockType;
|
block_type: WorkflowBlockType;
|
||||||
@@ -126,6 +127,15 @@ export type TaskBlockYAML = BlockYAMLBase & {
|
|||||||
terminate_criterion: string | null;
|
terminate_criterion: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Taskv2BlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "task_v2";
|
||||||
|
url: string | null;
|
||||||
|
prompt: string;
|
||||||
|
totp_verification_url: string | null;
|
||||||
|
totp_identifier: string | null;
|
||||||
|
max_iterations: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ValidationBlockYAML = BlockYAMLBase & {
|
export type ValidationBlockYAML = BlockYAMLBase & {
|
||||||
block_type: "validation";
|
block_type: "validation";
|
||||||
complete_criterion: string | null;
|
complete_criterion: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user