diff --git a/docs/workflows/workflow-blocks.mdx b/docs/workflows/workflow-blocks.mdx index 9f8744c9..24aa2f15 100644 --- a/docs/workflows/workflow-blocks.mdx +++ b/docs/workflows/workflow-blocks.mdx @@ -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 diff --git a/skyvern-frontend/src/routes/workflows/editor/helpContent.ts b/skyvern-frontend/src/routes/workflows/editor/helpContent.ts index 71ba098c..2417d263 100644 --- a/skyvern-frontend/src/routes/workflows/editor/helpContent.ts +++ b/skyvern-frontend/src/routes/workflows/editor/helpContent.ts @@ -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, diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/FileUploadNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/FileUploadNode.tsx new file mode 100644 index 00000000..682e407c --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/FileUploadNode.tsx @@ -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) { + 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 ( +
+ + +
+
+
+
+ +
+
+ + File Upload Block +
+
+ { + deleteNodeCallback(id); + }} + /> +
+
+
+
+ + +
+ +
+
+
+ + +
+ { + handleChange("awsAccessKeyId", value); + }} + value={inputs.awsAccessKeyId} + className="nopan text-xs" + /> +
+
+
+ + +
+ { + handleChange("awsSecretAccessKey", value); + }} + /> +
+
+
+ + +
+ { + handleChange("s3Bucket", value); + }} + value={inputs.s3Bucket} + className="nopan text-xs" + /> +
+
+
+ + +
+ { + handleChange("regionName", value); + }} + value={inputs.regionName} + className="nopan text-xs" + /> +
+
+
+ + +
+ { + handleChange("path", value); + }} + value={inputs.path} + className="nopan text-xs" + /> +
+
+
+
+ ); +} + +export { FileUploadNode }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/types.ts new file mode 100644 index 00000000..12e39b1d --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/types.ts @@ -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; + +export const fileUploadNodeDefaultData: FileUploadNodeData = { + editable: true, + storageType: "s3", + label: "", + path: "", + s3Bucket: "", + awsAccessKeyId: "", + awsSecretAccessKey: "", + regionName: "", + continueOnFailure: false, +} as const; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx index 8e3883de..2e21af49 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx @@ -64,6 +64,9 @@ function WorkflowBlockIcon({ workflowBlockType, className }: Props) { case "upload_to_s3": { return ; } + case "file_upload": { + return ; + } case "validation": { return ; } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts index 664dbef7..96ca6353 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts @@ -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), diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts index c6f1e784..e6ff06b8 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts @@ -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", diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx index d0dbc637..5336d258 100644 --- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx @@ -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: ( - // - // ), - // title: "Upload Block", - // description: "Uploads a file to S3", - // }, + { + nodeType: "fileUpload", + icon: ( + + ), + title: "File Upload Block", + description: "Uploads downloaded files to where you want.", + }, { nodeType: "fileDownload", icon: ( diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts index bf561fe4..d8b43a81 100644 --- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts @@ -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, diff --git a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts index 8785481d..35dbc719 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts @@ -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; diff --git a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts index f6b4b297..e309e076 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts @@ -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";