From 4c2c7df42cd0532ec4acf53f0b8faeb26268f5e0 Mon Sep 17 00:00:00 2001 From: Marc Kelechava Date: Wed, 14 Jan 2026 15:46:49 -0800 Subject: [PATCH] Add "Print PDF" Block (#4452) --- skyvern-frontend/src/api/types.ts | 1 + .../nodes/PrintPageNode/PrintPageNode.tsx | 162 +++++++++++++++ .../editor/nodes/PrintPageNode/types.ts | 30 +++ .../editor/nodes/WorkflowBlockIcon.tsx | 3 + .../routes/workflows/editor/nodes/index.ts | 6 +- .../routes/workflows/editor/nodes/types.ts | 1 + .../panels/WorkflowNodeLibraryPanel.tsx | 11 + .../workflows/editor/workflowEditorUtils.ts | 51 +++++ .../routes/workflows/types/workflowTypes.ts | 13 +- .../workflows/types/workflowYamlTypes.ts | 12 +- skyvern/forge/sdk/artifact/manager.py | 36 +++- skyvern/forge/sdk/artifact/models.py | 3 + skyvern/forge/sdk/artifact/storage/base.py | 1 + skyvern/forge/sdk/workflow/models/block.py | 188 +++++++++++++++++- .../workflow/workflow_definition_converter.py | 10 + skyvern/schemas/workflows.py | 18 ++ 16 files changed, 539 insertions(+), 7 deletions(-) create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/PrintPageNode/PrintPageNode.tsx create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/PrintPageNode/types.ts diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index adf561a7..df9a3357 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -14,6 +14,7 @@ export const ArtifactType = { HTMLScrape: "html_scrape", SkyvernLog: "skyvern_log", SkyvernLogRaw: "skyvern_log_raw", + PDF: "pdf", } as const; export type ArtifactType = (typeof ArtifactType)[keyof typeof ArtifactType]; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/PrintPageNode/PrintPageNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/PrintPageNode/PrintPageNode.tsx new file mode 100644 index 00000000..a9a7a17c --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/PrintPageNode/PrintPageNode.tsx @@ -0,0 +1,162 @@ +import { HelpTooltip } from "@/components/HelpTooltip"; +import { Label } from "@/components/ui/label"; +import { Handle, NodeProps, Position } from "@xyflow/react"; +import type { PrintPageNode } from "./types"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; +import { statusIsRunningOrQueued } from "@/routes/tasks/types"; +import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; +import { useUpdate } from "@/routes/workflows/editor/useUpdate"; + +function PrintPageNode({ id, data }: NodeProps) { + const { editable, label } = data; + const { blockLabel: urlBlockLabel } = useParams(); + const { data: workflowRun } = useWorkflowRunQuery(); + const workflowRunIsRunningOrQueued = + workflowRun && statusIsRunningOrQueued(workflowRun); + const thisBlockIsTargetted = + urlBlockLabel !== undefined && urlBlockLabel === label; + const thisBlockIsPlaying = + workflowRunIsRunningOrQueued && thisBlockIsTargetted; + + const update = useUpdate({ id, editable }); + + return ( +
+ + +
+ +
+
+ + +
+
+
+ + +
+ + update({ printBackground: checked }) + } + disabled={!editable} + /> +
+
+
+ + +
+ + update({ includeTimestamp: checked }) + } + disabled={!editable} + /> +
+
+ + + + + Advanced Settings + + +
+
+ + update({ customFilename: e.target.value })} + placeholder="my_report" + disabled={!editable} + className="nopan text-xs" + /> +
+
+ + + update({ landscape: checked }) + } + disabled={!editable} + /> +
+
+
+
+
+
+
+ ); +} + +export { PrintPageNode }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/PrintPageNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/PrintPageNode/types.ts new file mode 100644 index 00000000..97bb5295 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/PrintPageNode/types.ts @@ -0,0 +1,30 @@ +import type { Node } from "@xyflow/react"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; +import { NodeBaseData } from "../types"; + +export type PrintPageNodeData = NodeBaseData & { + includeTimestamp: boolean; + customFilename: string; + format: string; + landscape: boolean; + printBackground: boolean; +}; + +export type PrintPageNode = Node; + +export const printPageNodeDefaultData: PrintPageNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("print_page"), + label: "", + continueOnFailure: false, + editable: true, + model: null, + includeTimestamp: true, + customFilename: "", + format: "A4", + landscape: false, + printBackground: true, +}; + +export function isPrintPageNode(node: Node): node is PrintPageNode { + return node.type === "printPage"; +} diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx index f7001d37..124d4b5a 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx @@ -91,6 +91,9 @@ function WorkflowBlockIcon({ workflowBlockType, className }: Props) { case "http_request": { return ; } + case "print_page": { + return ; + } } } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts index 8b8afca0..a7d6b575 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts @@ -47,6 +47,8 @@ import { HttpRequestNode } from "./HttpRequestNode/types"; import { HttpRequestNode as HttpRequestNodeComponent } from "./HttpRequestNode/HttpRequestNode"; import { HumanInteractionNode } from "./HumanInteractionNode/types"; import { HumanInteractionNode as HumanInteractionNodeComponent } from "./HumanInteractionNode/HumanInteractionNode"; +import { PrintPageNode } from "./PrintPageNode/types"; +import { PrintPageNode as PrintPageNodeComponent } from "./PrintPageNode/PrintPageNode"; export type UtilityNode = StartNode | NodeAdderNode; @@ -72,7 +74,8 @@ export type WorkflowBlockNode = | PDFParserNode | Taskv2Node | URLNode - | HttpRequestNode; + | HttpRequestNode + | PrintPageNode; export function isUtilityNode(node: AppNode): node is UtilityNode { return node.type === "nodeAdder" || node.type === "start"; @@ -109,4 +112,5 @@ export const nodeTypes = { taskv2: memo(Taskv2NodeComponent), url: memo(URLNodeComponent), http_request: memo(HttpRequestNodeComponent), + printPage: memo(PrintPageNodeComponent), } as const; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts index cc70db4c..3eb9ba11 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts @@ -66,4 +66,5 @@ export const workflowBlockTitle: { task_v2: "Browser Task v2", goto_url: "Go to URL", http_request: "HTTP Request", + print_page: "Print Page", }; diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx index fd6bbc43..99f6cc72 100644 --- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx @@ -266,6 +266,17 @@ const nodeLibraryItems: Array<{ title: "HTTP Request Block", description: "Make HTTP API calls", }, + { + nodeType: "printPage", + icon: ( + + ), + title: "Print Page Block", + description: "Print current page to PDF", + }, ]; type Props = { diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts index 306fbc04..02ec34c5 100644 --- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts @@ -46,6 +46,7 @@ import { URLBlockYAML, FileUploadBlockYAML, HttpRequestBlockYAML, + PrintPageBlockYAML, } from "../types/workflowYamlTypes"; import { EMAIL_BLOCK_SENDER, @@ -122,6 +123,7 @@ import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types"; import { urlNodeDefaultData } from "./nodes/URLNode/types"; import { fileUploadNodeDefaultData } from "./nodes/FileUploadNode/types"; import { httpRequestNodeDefaultData } from "./nodes/HttpRequestNode/types"; +import { printPageNodeDefaultData } from "./nodes/PrintPageNode/types"; export const NEW_NODE_LABEL_PREFIX = "block_"; @@ -839,6 +841,21 @@ function convertToNode( }, }; } + case "print_page": { + return { + ...identifiers, + ...common, + type: "printPage", + data: { + ...commonData, + includeTimestamp: block.include_timestamp ?? false, + customFilename: block.custom_filename ?? "", + format: block.format ?? "A4", + landscape: block.landscape ?? false, + printBackground: block.print_background ?? true, + }, + }; + } } } @@ -1877,6 +1894,17 @@ function createNode( }, }; } + case "printPage": { + return { + ...identifiers, + ...common, + type: "printPage", + data: { + ...printPageNodeDefaultData, + label, + }, + }; + } case "conditional": { const branches = createDefaultBranchConditions(); return { @@ -2332,6 +2360,17 @@ function getWorkflowBlock( save_response_as_file: node.data.saveResponseAsFile, }; } + case "printPage": { + return { + ...base, + block_type: "print_page", + include_timestamp: node.data.includeTimestamp, + custom_filename: node.data.customFilename || null, + format: node.data.format, + landscape: node.data.landscape, + print_background: node.data.printBackground, + }; + } case "conditional": { return serializeConditionalBlock(node as ConditionalNode, nodes, edges); } @@ -3338,6 +3377,18 @@ function convertBlocksToBlockYAML( }; return blockYaml; } + case "print_page": { + const blockYaml: PrintPageBlockYAML = { + ...base, + block_type: "print_page", + include_timestamp: block.include_timestamp, + custom_filename: block.custom_filename, + format: block.format, + landscape: block.landscape, + print_background: block.print_background, + }; + return blockYaml; + } } }); } diff --git a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts index 24b6faa5..bffdd878 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts @@ -211,7 +211,8 @@ export type WorkflowBlock = | PDFParserBlock | Taskv2Block | URLBlock - | HttpRequestBlock; + | HttpRequestBlock + | PrintPageBlock; export const WorkflowBlockTypes = { Task: "task", @@ -236,6 +237,7 @@ export const WorkflowBlockTypes = { Taskv2: "task_v2", URL: "goto_url", HttpRequest: "http_request", + PrintPage: "print_page", } as const; // all of them @@ -554,6 +556,15 @@ export type HttpRequestBlock = WorkflowBlockBase & { save_response_as_file: boolean; }; +export type PrintPageBlock = WorkflowBlockBase & { + block_type: "print_page"; + include_timestamp: boolean; + custom_filename: string | null; + format: string; + landscape: boolean; + print_background: boolean; +}; + export type WorkflowDefinition = { version?: number | null; parameters: Array; diff --git a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts index 7b680c81..3cbd3750 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts @@ -141,7 +141,8 @@ export type BlockYAML = | PDFParserBlockYAML | Taskv2BlockYAML | URLBlockYAML - | HttpRequestBlockYAML; + | HttpRequestBlockYAML + | PrintPageBlockYAML; export type BlockYAMLBase = { block_type: WorkflowBlockType; @@ -404,3 +405,12 @@ export type HttpRequestBlockYAML = BlockYAMLBase & { download_filename?: string | null; save_response_as_file?: boolean; }; + +export type PrintPageBlockYAML = BlockYAMLBase & { + block_type: "print_page"; + include_timestamp: boolean; + custom_filename: string | null; + format: string; + landscape: boolean; + print_background: boolean; +}; diff --git a/skyvern/forge/sdk/artifact/manager.py b/skyvern/forge/sdk/artifact/manager.py index f0db0ffb..4c945f0d 100644 --- a/skyvern/forge/sdk/artifact/manager.py +++ b/skyvern/forge/sdk/artifact/manager.py @@ -279,13 +279,13 @@ class ArtifactManager: path=path, ) - async def create_workflow_run_block_artifact( + async def _create_workflow_run_block_artifact_internal( self, workflow_run_block: WorkflowRunBlock, artifact_type: ArtifactType, data: bytes | None = None, path: str | None = None, - ) -> str: + ) -> tuple[str, str]: artifact_id = generate_artifact_id() uri = app.STORAGE.build_workflow_run_block_uri( organization_id=workflow_run_block.organization_id, @@ -293,7 +293,7 @@ class ArtifactManager: workflow_run_block=workflow_run_block, artifact_type=artifact_type, ) - return await self._create_artifact( + await self._create_artifact( aio_task_primary_key=workflow_run_block.workflow_run_block_id, artifact_id=artifact_id, artifact_type=artifact_type, @@ -304,6 +304,36 @@ class ArtifactManager: data=data, path=path, ) + return artifact_id, uri + + async def create_workflow_run_block_artifact( + self, + workflow_run_block: WorkflowRunBlock, + artifact_type: ArtifactType, + data: bytes | None = None, + path: str | None = None, + ) -> str: + artifact_id, _ = await self._create_workflow_run_block_artifact_internal( + workflow_run_block=workflow_run_block, + artifact_type=artifact_type, + data=data, + path=path, + ) + return artifact_id + + async def create_workflow_run_block_artifact_with_uri( + self, + workflow_run_block: WorkflowRunBlock, + artifact_type: ArtifactType, + data: bytes | None = None, + path: str | None = None, + ) -> tuple[str, str]: + return await self._create_workflow_run_block_artifact_internal( + workflow_run_block=workflow_run_block, + artifact_type=artifact_type, + data=data, + path=path, + ) async def create_workflow_run_block_artifacts( self, diff --git a/skyvern/forge/sdk/artifact/models.py b/skyvern/forge/sdk/artifact/models.py index 6780e01c..adeb1e78 100644 --- a/skyvern/forge/sdk/artifact/models.py +++ b/skyvern/forge/sdk/artifact/models.py @@ -52,6 +52,9 @@ class ArtifactType(StrEnum): # Script files SCRIPT_FILE = "script_file" + # PDF files + PDF = "pdf" + class Artifact(BaseModel): created_at: datetime = Field( diff --git a/skyvern/forge/sdk/artifact/storage/base.py b/skyvern/forge/sdk/artifact/storage/base.py index cf899320..e4d6f2ec 100644 --- a/skyvern/forge/sdk/artifact/storage/base.py +++ b/skyvern/forge/sdk/artifact/storage/base.py @@ -34,6 +34,7 @@ FILE_EXTENTSION_MAP: dict[ArtifactType, str] = { ArtifactType.HASHED_HREF_MAP: "json", # DEPRECATED: we're using CSS selector map now ArtifactType.VISIBLE_ELEMENTS_ID_XPATH_MAP: "json", + ArtifactType.PDF: "pdf", } diff --git a/skyvern/forge/sdk/workflow/models/block.py b/skyvern/forge/sdk/workflow/models/block.py index bf0050f4..4954c3e7 100644 --- a/skyvern/forge/sdk/workflow/models/block.py +++ b/skyvern/forge/sdk/workflow/models/block.py @@ -11,13 +11,14 @@ import smtplib import textwrap import uuid from collections import defaultdict, deque -from datetime import datetime +from datetime import datetime, timezone from email.message import EmailMessage from pathlib import Path from types import SimpleNamespace from typing import Annotated, Any, Awaitable, Callable, ClassVar, Literal, Union, cast from urllib.parse import quote, urlparse +import aiofiles import aiohttp import filetype import pandas as pd @@ -66,6 +67,7 @@ from skyvern.forge.sdk.artifact.models import ArtifactType from skyvern.forge.sdk.core import skyvern_context from skyvern.forge.sdk.core.aiohttp_helper import aiohttp_request from skyvern.forge.sdk.db.enums import TaskType +from skyvern.forge.sdk.db.exceptions import NotFoundError from skyvern.forge.sdk.experimentation.llm_prompt_config import get_llm_handler_for_prompt_type from skyvern.forge.sdk.schemas.files import FileInfo from skyvern.forge.sdk.schemas.task_v2 import TaskV2Status @@ -4474,6 +4476,189 @@ class HttpRequestBlock(Block): ) +class PrintPageBlock(Block): + block_type: Literal[BlockType.PRINT_PAGE] = BlockType.PRINT_PAGE # type: ignore + + include_timestamp: bool = True + custom_filename: str | None = None + format: str = "A4" + landscape: bool = False + print_background: bool = True + + VALID_FORMATS: ClassVar[set[str]] = {"A4", "Letter", "Legal", "Tabloid"} + + def get_all_parameters(self, workflow_run_id: str) -> list[PARAMETER_TYPE]: + return [] + + @staticmethod + def _sanitize_filename(filename: str) -> str: + sanitized = re.sub(r'[<>:"/\\|?*]', "_", filename) + sanitized = sanitized.strip(". ") + return sanitized[:200] if sanitized else "document" + + def _build_pdf_options(self) -> dict[str, Any]: + pdf_format = self.format if self.format in self.VALID_FORMATS else "A4" + pdf_options: dict[str, Any] = { + "format": pdf_format, + "landscape": self.landscape, + "print_background": self.print_background, + } + + if self.include_timestamp: + pdf_options["display_header_footer"] = True + pdf_options["header_template"] = ( + '
' + '
' + ) + pdf_options["footer_template"] = ( + '
' + '/
' + ) + pdf_options["margin"] = {"top": "40px", "bottom": "40px"} + + return pdf_options + + async def _upload_pdf_artifact( + self, + *, + pdf_bytes: bytes, + workflow_run_id: str, + workflow_run_block_id: str, + workflow_run_context: WorkflowRunContext, + organization_id: str | None, + ) -> str | None: + artifact_org_id = organization_id or workflow_run_context.organization_id + if not artifact_org_id: + LOG.warning( + "PrintPageBlock: Missing organization_id, skipping artifact upload", + workflow_run_id=workflow_run_id, + workflow_run_block_id=workflow_run_block_id, + ) + return None + + try: + workflow_run_block = await app.DATABASE.get_workflow_run_block( + workflow_run_block_id, + organization_id=artifact_org_id, + ) + except NotFoundError: + LOG.warning( + "PrintPageBlock: Workflow run block not found, skipping artifact upload", + workflow_run_id=workflow_run_id, + workflow_run_block_id=workflow_run_block_id, + organization_id=artifact_org_id, + ) + return None + + _, artifact_uri = await app.ARTIFACT_MANAGER.create_workflow_run_block_artifact_with_uri( + workflow_run_block=workflow_run_block, + artifact_type=ArtifactType.PDF, + data=pdf_bytes, + ) + try: + await app.ARTIFACT_MANAGER.wait_for_upload_aiotasks([workflow_run_block.workflow_run_block_id]) + except Exception: + LOG.warning( + "PrintPageBlock: Failed to upload PDF artifact", + workflow_run_id=workflow_run_id, + workflow_run_block_id=workflow_run_block.workflow_run_block_id, + exc_info=True, + ) + return None + + return artifact_uri + + async def execute( + self, + workflow_run_id: str, + workflow_run_block_id: str, + organization_id: str | None = None, + browser_session_id: str | None = None, + **kwargs: dict, + ) -> BlockResult: + workflow_run_context = self.get_workflow_run_context(workflow_run_id) + browser_state = app.BROWSER_MANAGER.get_for_workflow_run(workflow_run_id) + + if not browser_state: + return await self.build_block_result( + success=False, + failure_reason="No browser state available", + status=BlockStatus.failed, + workflow_run_block_id=workflow_run_block_id, + organization_id=organization_id, + ) + + page = await browser_state.get_working_page() + if not page: + return await self.build_block_result( + success=False, + failure_reason="No page available", + status=BlockStatus.failed, + workflow_run_block_id=workflow_run_block_id, + organization_id=organization_id, + ) + + pdf_options = self._build_pdf_options() + + try: + pdf_bytes = await page.pdf(**pdf_options) + except Exception as e: + error_msg = str(e) + if "pdf" in error_msg.lower() and ("not supported" in error_msg.lower() or "chromium" in error_msg.lower()): + error_msg = "PDF generation requires Chromium browser. Current browser does not support page.pdf()." + LOG.warning("PrintPageBlock: Failed to generate PDF", error=error_msg, workflow_run_id=workflow_run_id) + return await self.build_block_result( + success=False, + failure_reason=f"Failed to generate PDF: {error_msg}", + status=BlockStatus.failed, + workflow_run_block_id=workflow_run_block_id, + organization_id=organization_id, + ) + + timestamp_str = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") + if self.custom_filename: + filename = self.format_block_parameter_template_from_workflow_run_context( + self.custom_filename, workflow_run_context + ) + filename = self._sanitize_filename(filename) + if not filename.endswith(".pdf"): + filename += ".pdf" + else: + filename = f"page_{timestamp_str}.pdf" + + # Save PDF to download directory so it appears in runs UI + download_dir = get_download_dir(workflow_run_id) + file_path = os.path.join(download_dir, filename) + async with aiofiles.open(file_path, "wb") as f: + await f.write(pdf_bytes) + + # Upload to artifact storage for downstream block access (e.g., File Extraction Block) + artifact_uri = await self._upload_pdf_artifact( + pdf_bytes=pdf_bytes, + workflow_run_id=workflow_run_id, + workflow_run_block_id=workflow_run_block_id, + workflow_run_context=workflow_run_context, + organization_id=organization_id, + ) + + output = { + "filename": filename, + "file_path": file_path, + "size_bytes": len(pdf_bytes), + "artifact_uri": artifact_uri, + } + await self.record_output_parameter_value(workflow_run_context, workflow_run_id, output) + + return await self.build_block_result( + success=True, + failure_reason=None, + output_parameter_value=output, + status=BlockStatus.completed, + workflow_run_block_id=workflow_run_block_id, + organization_id=organization_id, + ) + + class BranchEvaluationContext: """Collection of runtime data that BranchCriteria evaluators can consume.""" @@ -5246,6 +5431,7 @@ BlockSubclasses = Union[ TaskV2Block, FileUploadBlock, HttpRequestBlock, + PrintPageBlock, ] BlockTypeVar = Annotated[BlockSubclasses, Field(discriminator="block_type")] diff --git a/skyvern/forge/sdk/workflow/workflow_definition_converter.py b/skyvern/forge/sdk/workflow/workflow_definition_converter.py index e6e2346f..27b634ee 100644 --- a/skyvern/forge/sdk/workflow/workflow_definition_converter.py +++ b/skyvern/forge/sdk/workflow/workflow_definition_converter.py @@ -43,6 +43,7 @@ from skyvern.forge.sdk.workflow.models.block import ( LoginBlock, NavigationBlock, PDFParserBlock, + PrintPageBlock, PromptBranchCriteria, SendEmailBlock, TaskBlock, @@ -729,6 +730,15 @@ def block_yaml_to_block( url=block_yaml.url, complete_verification=False, ) + elif block_yaml.block_type == BlockType.PRINT_PAGE: + return PrintPageBlock( + **base_kwargs, + include_timestamp=block_yaml.include_timestamp, + custom_filename=block_yaml.custom_filename, + format=block_yaml.format, + landscape=block_yaml.landscape, + print_background=block_yaml.print_background, + ) raise ValueError(f"Invalid block type {block_yaml.block_type}") diff --git a/skyvern/schemas/workflows.py b/skyvern/schemas/workflows.py index 503bf0dd..43c08df8 100644 --- a/skyvern/schemas/workflows.py +++ b/skyvern/schemas/workflows.py @@ -41,6 +41,7 @@ class BlockType(StrEnum): PDF_PARSER = "pdf_parser" HTTP_REQUEST = "http_request" HUMAN_INTERACTION = "human_interaction" + PRINT_PAGE = "print_page" class BlockStatus(StrEnum): @@ -68,6 +69,13 @@ class FileType(StrEnum): PDF = "pdf" +class PDFFormat(StrEnum): + A4 = "A4" + LETTER = "Letter" + LEGAL = "Legal" + TABLOID = "Tabloid" + + class FileStorageType(StrEnum): S3 = "s3" AZURE = "azure" @@ -546,6 +554,15 @@ class HttpRequestBlockYAML(BlockYAML): parameter_keys: list[str] | None = None +class PrintPageBlockYAML(BlockYAML): + block_type: Literal[BlockType.PRINT_PAGE] = BlockType.PRINT_PAGE # type: ignore + include_timestamp: bool = True + custom_filename: str | None = None + format: PDFFormat = PDFFormat.A4 + landscape: bool = False + print_background: bool = True + + PARAMETER_YAML_SUBCLASSES = ( AWSSecretParameterYAML | BitwardenLoginCredentialParameterYAML @@ -583,6 +600,7 @@ BLOCK_YAML_SUBCLASSES = ( | TaskV2BlockYAML | HttpRequestBlockYAML | ConditionalBlockYAML + | PrintPageBlockYAML ) BLOCK_YAML_TYPES = Annotated[BLOCK_YAML_SUBCLASSES, Field(discriminator="block_type")]