mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2025-09-01 18:20:06 +00:00
shu/file upload block ui (#2003)
This commit is contained in:
parent
6d8a49d5b5
commit
933ca0ab70
11 changed files with 360 additions and 19 deletions
|
@ -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. (let’s 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 we’re 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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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: (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue