shu/file upload block ui (#2003)

This commit is contained in:
Shuchang Zheng 2025-03-23 15:58:41 -07:00 committed by GitHub
parent 6d8a49d5b5
commit 933ca0ab70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 360 additions and 19 deletions

View file

@ -149,18 +149,53 @@ Inputs:
1. **Prompt *(required):*** Write a natural language prompt to be sent to the LLM to generate a text response
2. **JSON Schema *(optional):*** Craft a JSON input that structures the LLM output for use in another programming task
## DownloadToS3Block / UploadToS3Block
## FileUploadBlock
Persists files inside S3
Persists files inside custom destinations.
Supported integrations:
- AWS S3
```
- block_type: upload_to_s3
- block_type: file_upload
label: upload_downloaded_files_to_s3
path: SKYVERN_DOWNLOAD_DIRECTORY
storage_type: s3
aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID
aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
s3_bucket: YOUR_S3_BUCKET
region_name: us-east-1
```
### How to set up FileUploadBlock with AWS
- Step 1. Create your bucket. (lets say `YOUR-BUCKET-NAME` is the name)
- Step 2. Create a limited IAM policy that only has GetObject and PutObject permissions to this s3 bucket, named `skyvern-s3-access-policy` (any name works):
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SkyvernS3Access",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
}
]
}
```
- Step 3. Create an AWS IAM user for skyvern
- Go to https://us-east-1.console.aws.amazon.com/iam/home
- click "Create User" → do not check "Provide user access to the AWS Management Console" → input user name → click "Next"
- At the the "Set Permissions" page, click "Attach policies directly" → pick the `skyvern-s3-access-policy` created in the previous step → click "Next" → click "Create user"
- After the user is created, go to the new IAM user page → go to the "Security credentials" tab → click "create access key" → on the "Access key best practices & alternatives" page, click "Application running on an AWS compute service" → click "Next" → click "Create access key" → you will see the "Access Key" and the "Secret access key" at the end of user creation.
- Step 4. Create a FileUploadBlock
- Place this block after the file download has completed.
- Copy the "Access Key" to the "AWS Access Key ID" field of the File Upload Block and "Secret access key".
- Copy the "Access Key" to the "AWS Secret Access Key" field in the File Upload Block.
- Add your s3 bucket name.
- Add your AWS region.
* Since were in beta, this feature is unavailable right now, [contact us](https://meetings.hubspot.com/skyvern/demo?uuid=7c83865f-1a92-4c44-9e52-1ba0dbc04f7a) if you would like to use it.
## SendEmailBlock

View file

@ -81,6 +81,17 @@ export const helpTooltips = {
...baseHelpTooltipContent,
path: "Since we're in beta this section isn't fully customizable yet, contact us if you'd like to integrate it into your workflow.",
},
fileUpload: {
...baseHelpTooltipContent,
path: "The path of the folder to upload the files to.",
storage_type:
"The type of storage to upload the file to. Currently only S3 is supported. Please contact us if you'd like to integrate other storage types.",
s3_bucket: "The S3 bucket to upload the file to.",
aws_access_key_id: "The AWS access key ID to use to upload the file to S3.",
aws_secret_access_key:
"The AWS secret access key to use to upload the file to S3.",
region_name: "The AWS region",
},
download: {
...baseHelpTooltipContent,
url: "Since we're in beta this section isn't fully customizable yet, contact us if you'd like to integrate it into your workflow.",
@ -136,6 +147,7 @@ export const placeholders = {
loop: basePlaceholderContent,
sendEmail: basePlaceholderContent,
upload: basePlaceholderContent,
fileUpload: basePlaceholderContent,
download: basePlaceholderContent,
codeBlock: basePlaceholderContent,
fileUrl: basePlaceholderContent,

View file

@ -0,0 +1,183 @@
import { HelpTooltip } from "@/components/HelpTooltip";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
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 { helpTooltips } from "../../helpContent";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import { type FileUploadNode } from "./types";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { useState } from "react";
function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
const { updateNodeData } = useReactFlow();
const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useNodeLabelChangeHandler({
id,
initialValue: data.label,
});
const [inputs, setInputs] = useState({
storageType: data.storageType,
awsAccessKeyId: data.awsAccessKeyId,
awsSecretAccessKey: data.awsSecretAccessKey,
s3Bucket: data.s3Bucket,
regionName: data.regionName,
path: data.path,
});
function handleChange(key: string, value: unknown) {
if (!data.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.UploadToS3}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle
value={label}
editable={data.editable}
onChange={setLabel}
titleClassName="text-base"
inputClassName="text-base"
/>
<span className="text-xs text-slate-400">File Upload Block</span>
</div>
</div>
<NodeActionMenu
onDelete={() => {
deleteNodeCallback(id);
}}
/>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="text-sm text-slate-400">Storage Type</Label>
<HelpTooltip
content={helpTooltips["fileUpload"]["storage_type"]}
/>
</div>
<Input
value={data.storageType}
className="nopan text-xs"
disabled
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="text-sm text-slate-400">
AWS Access Key ID
</Label>
<HelpTooltip
content={helpTooltips["fileUpload"]["aws_access_key_id"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("awsAccessKeyId", value);
}}
value={inputs.awsAccessKeyId}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="text-sm text-slate-400">
AWS Secret Access Key
</Label>
<HelpTooltip
content={helpTooltips["fileUpload"]["aws_secret_access_key"]}
/>
</div>
<Input
type="password"
value={inputs.awsSecretAccessKey}
className="nopan text-xs"
onChange={(value) => {
handleChange("awsSecretAccessKey", value);
}}
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="text-sm text-slate-400">S3 Bucket</Label>
<HelpTooltip content={helpTooltips["fileUpload"]["s3_bucket"]} />
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("s3Bucket", value);
}}
value={inputs.s3Bucket}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="text-sm text-slate-400">Region Name</Label>
<HelpTooltip
content={helpTooltips["fileUpload"]["region_name"]}
/>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("regionName", value);
}}
value={inputs.regionName}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="text-sm text-slate-400">
(Optional) Folder Path
</Label>
<HelpTooltip content={helpTooltips["fileUpload"]["path"]} />
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
handleChange("path", value);
}}
value={inputs.path}
className="nopan text-xs"
/>
</div>
</div>
</div>
</div>
);
}
export { FileUploadNode };

View file

@ -0,0 +1,26 @@
import type { Node } from "@xyflow/react";
import { NodeBaseData } from "../types";
export type FileUploadNodeData = NodeBaseData & {
path: string;
editable: boolean;
storageType: string;
s3Bucket: string;
awsAccessKeyId: string;
awsSecretAccessKey: string;
regionName: string;
};
export type FileUploadNode = Node<FileUploadNodeData, "fileUpload">;
export const fileUploadNodeDefaultData: FileUploadNodeData = {
editable: true,
storageType: "s3",
label: "",
path: "",
s3Bucket: "",
awsAccessKeyId: "",
awsSecretAccessKey: "",
regionName: "",
continueOnFailure: false,
} as const;

View file

@ -64,6 +64,9 @@ function WorkflowBlockIcon({ workflowBlockType, className }: Props) {
case "upload_to_s3": {
return <UploadIcon className={className} />;
}
case "file_upload": {
return <UploadIcon className={className} />;
}
case "validation": {
return <CheckCircledIcon className={className} />;
}

View file

@ -13,6 +13,8 @@ import type { FileParserNode } from "./FileParserNode/types";
import { FileParserNode as FileParserNodeComponent } from "./FileParserNode/FileParserNode";
import type { UploadNode } from "./UploadNode/types";
import { UploadNode as UploadNodeComponent } from "./UploadNode/UploadNode";
import type { FileUploadNode } from "./FileUploadNode/types";
import { FileUploadNode as FileUploadNodeComponent } from "./FileUploadNode/FileUploadNode";
import type { DownloadNode } from "./DownloadNode/types";
import { DownloadNode as DownloadNodeComponent } from "./DownloadNode/DownloadNode";
import type { NodeAdderNode } from "./NodeAdderNode/types";
@ -50,6 +52,7 @@ export type WorkflowBlockNode =
| CodeBlockNode
| FileParserNode
| UploadNode
| FileUploadNode
| DownloadNode
| ValidationNode
| ActionNode
@ -80,6 +83,7 @@ export const nodeTypes = {
codeBlock: memo(CodeBlockNodeComponent),
fileParser: memo(FileParserNodeComponent),
upload: memo(UploadNodeComponent),
fileUpload: memo(FileUploadNodeComponent),
download: memo(DownloadNodeComponent),
nodeAdder: memo(NodeAdderNodeComponent),
start: memo(StartNodeComponent),

View file

@ -42,7 +42,8 @@ export const workflowBlockTitle: {
send_email: "Send Email",
task: "Task",
text_prompt: "Text Prompt",
upload_to_s3: "Upload",
upload_to_s3: "Upload To S3",
file_upload: "Upload Files",
validation: "Validation",
wait: "Wait",
pdf_parser: "PDF Parser",

View file

@ -45,7 +45,7 @@ const nodeLibraryItems: Array<{
/>
),
title: "Task v2 Block",
description: "Runs a Skyvern v2 Task",
description: "Runs a Skyvern 2.0 Task",
},
{
nodeType: "action",
@ -181,17 +181,17 @@ const nodeLibraryItems: Array<{
// title: "Download Block",
// description: "Downloads a file from S3",
// },
// {
// nodeType: "upload",
// icon: (
// <WorkflowBlockIcon
// workflowBlockType={WorkflowBlockTypes.UploadToS3}
// className="size-6"
// />
// ),
// title: "Upload Block",
// description: "Uploads a file to S3",
// },
{
nodeType: "fileUpload",
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.FileUpload}
className="size-6"
/>
),
title: "File Upload Block",
description: "Uploads downloaded files to where you want.",
},
{
nodeType: "fileDownload",
icon: (

View file

@ -35,6 +35,7 @@ import {
PDFParserBlockYAML,
Taskv2BlockYAML,
URLBlockYAML,
FileUploadBlockYAML,
} from "../types/workflowYamlTypes";
import {
EMAIL_BLOCK_SENDER,
@ -96,7 +97,7 @@ import {
} from "./nodes/PDFParserNode/types";
import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types";
import { urlNodeDefaultData } from "./nodes/URLNode/types";
import { fileUploadNodeDefaultData } from "./nodes/FileUploadNode/types";
export const NEW_NODE_LABEL_PREFIX = "block_";
function layoutUtil(
@ -483,6 +484,23 @@ function convertToNode(
};
}
case "file_upload": {
return {
...identifiers,
...common,
type: "fileUpload",
data: {
...commonData,
path: block.path,
storageType: block.storage_type,
s3Bucket: block.s3_bucket,
awsAccessKeyId: block.aws_access_key_id,
awsSecretAccessKey: block.aws_secret_access_key,
regionName: block.region_name,
},
};
}
case "goto_url": {
return {
...identifiers,
@ -902,6 +920,17 @@ function createNode(
},
};
}
case "fileUpload": {
return {
...identifiers,
...common,
type: "fileUpload",
data: {
...fileUploadNodeDefaultData,
label,
},
};
}
}
}
@ -1127,6 +1156,18 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
path: node.data.path,
};
}
case "fileUpload": {
return {
...base,
block_type: "file_upload",
path: node.data.path,
storage_type: node.data.storageType,
s3_bucket: node.data.s3Bucket,
aws_access_key_id: node.data.awsAccessKeyId,
aws_secret_access_key: node.data.awsSecretAccessKey,
region_name: node.data.regionName,
};
}
case "fileParser": {
return {
...base,
@ -1813,6 +1854,19 @@ function convertBlocksToBlockYAML(
};
return blockYaml;
}
case "file_upload": {
const blockYaml: FileUploadBlockYAML = {
...base,
block_type: "file_upload",
path: block.path,
storage_type: block.storage_type,
s3_bucket: block.s3_bucket,
aws_access_key_id: block.aws_access_key_id,
aws_secret_access_key: block.aws_secret_access_key,
region_name: block.region_name,
};
return blockYaml;
}
case "file_url_parser": {
const blockYaml: FileUrlParserBlockYAML = {
...base,

View file

@ -161,6 +161,7 @@ export type WorkflowBlock =
| TextPromptBlock
| CodeBlock
| UploadToS3Block
| FileUploadBlock
| DownloadToS3Block
| SendEmailBlock
| FileURLParserBlock
@ -182,6 +183,7 @@ export const WorkflowBlockTypes = {
TextPrompt: "text_prompt",
DownloadToS3: "download_to_s3",
UploadToS3: "upload_to_s3",
FileUpload: "file_upload",
SendEmail: "send_email",
FileURLParser: "file_url_parser",
Validation: "validation",
@ -292,6 +294,16 @@ export type UploadToS3Block = WorkflowBlockBase & {
path: string;
};
export type FileUploadBlock = WorkflowBlockBase & {
block_type: "file_upload";
path: string;
storage_type: string;
s3_bucket: string;
region_name: string;
aws_access_key_id: string;
aws_secret_access_key: string;
};
export type SendEmailBlock = WorkflowBlockBase & {
block_type: "send_email";
smtp_host?: AWSSecretParameter;

View file

@ -95,6 +95,7 @@ export type BlockYAML =
| TextPromptBlockYAML
| DownloadToS3BlockYAML
| UploadToS3BlockYAML
| FileUploadBlockYAML
| SendEmailBlockYAML
| FileUrlParserBlockYAML
| ForLoopBlockYAML
@ -257,6 +258,16 @@ export type UploadToS3BlockYAML = BlockYAMLBase & {
path?: string | null;
};
export type FileUploadBlockYAML = BlockYAMLBase & {
block_type: "file_upload";
path?: string | null;
storage_type: string;
s3_bucket: string;
region_name: string;
aws_access_key_id: string;
aws_secret_access_key: string;
};
export type SendEmailBlockYAML = BlockYAMLBase & {
block_type: "send_email";