diff --git a/poetry.lock b/poetry.lock index 1f6ddd88..74bcd839 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "about-time" @@ -3697,6 +3697,81 @@ files = [ {file = "legacy_cgi-2.6.3.tar.gz", hash = "sha256:4c119d6cb8e9d8b6ad7cc0ddad880552c62df4029622835d06dfd18f438a8154"}, ] +[[package]] +name = "libcst" +version = "1.8.2" +description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "libcst-1.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:67d9720d91f507c87b3e5f070627ad640a00bc6cfdf5635f8c6ee9f2964cf71c"}, + {file = "libcst-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:94b7c032b72566077614a02baab1929739fd0af0cc1d46deaba4408b870faef2"}, + {file = "libcst-1.8.2-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:11ea148902e3e1688afa392087c728ac3a843e54a87d334d1464d2097d3debb7"}, + {file = "libcst-1.8.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:22c9473a2cc53faabcc95a0ac6ca4e52d127017bf34ba9bc0f8e472e44f7b38e"}, + {file = "libcst-1.8.2-cp310-cp310-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b5269b96367e65793a7714608f6d906418eb056d59eaac9bba980486aabddbed"}, + {file = "libcst-1.8.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d20e932ddd9a389da57b060c26e84a24118c96ff6fc5dcc7b784da24e823b694"}, + {file = "libcst-1.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a553d452004e44b841788f6faa7231a02157527ddecc89dbbe5b689b74822226"}, + {file = "libcst-1.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fe762c4c390039b79b818cbc725d8663586b25351dc18a2704b0e357d69b924"}, + {file = "libcst-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:5c513e64eff0f7bf2a908e2d987a98653eb33e1062ce2afd3a84af58159a24f9"}, + {file = "libcst-1.8.2-cp310-cp310-win_arm64.whl", hash = "sha256:41613fe08e647213546c7c59a5a1fc5484666e7d4cab6e80260c612acbb20e8c"}, + {file = "libcst-1.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:688a03bac4dfb9afc5078ec01d53c21556381282bdf1a804dd0dbafb5056de2a"}, + {file = "libcst-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c34060ff2991707c710250463ae9f415ebb21653f2f5b013c61c9c376ff9b715"}, + {file = "libcst-1.8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f54f5c4176d60e7cd6b0880e18fb3fa8501ae046069151721cab457c7c538a3d"}, + {file = "libcst-1.8.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d11992561de0ad29ec2800230fbdcbef9efaa02805d5c633a73ab3cf2ba51bf1"}, + {file = "libcst-1.8.2-cp311-cp311-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fa3b807c2d2b34397c135d19ad6abb20c47a2ddb7bf65d90455f2040f7797e1e"}, + {file = "libcst-1.8.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b0110140738be1287e3724080a101e7cec6ae708008b7650c9d8a1c1788ec03a"}, + {file = "libcst-1.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a50618f4819a97ef897e055ac7aaf1cad5df84c206f33be35b0759d671574197"}, + {file = "libcst-1.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e9bb599c175dc34a4511f0e26d5b5374fbcc91ea338871701a519e95d52f3c28"}, + {file = "libcst-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:96e2363e1f6e44bd7256bbbf3a53140743f821b5133046e6185491e0d9183447"}, + {file = "libcst-1.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:f5391d71bd7e9e6c73dcb3ee8d8c63b09efc14ce6e4dad31568d4838afc9aae0"}, + {file = "libcst-1.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2e8c1dfa854e700fcf6cd79b2796aa37d55697a74646daf5ea47c7c764bac31c"}, + {file = "libcst-1.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b5c57a3c1976c365678eb0730bcb140d40510990cb77df9a91bb5c41d587ba6"}, + {file = "libcst-1.8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:0f23409add2aaebbb6d8e881babab43c2d979f051b8bd8aed5fe779ea180a4e8"}, + {file = "libcst-1.8.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b88e9104c456590ad0ef0e82851d4fc03e9aa9d621fa8fdd4cd0907152a825ae"}, + {file = "libcst-1.8.2-cp312-cp312-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5ba3ea570c8fb6fc44f71aa329edc7c668e2909311913123d0d7ab8c65fc357"}, + {file = "libcst-1.8.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:460fcf3562f078781e1504983cb11909eb27a1d46eaa99e65c4b0fafdc298298"}, + {file = "libcst-1.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1381ddbd1066d543e05d580c15beacf671e1469a0b2adb6dba58fec311f4eed"}, + {file = "libcst-1.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a70e40ce7600e1b32e293bb9157e9de3b69170e2318ccb219102f1abb826c94a"}, + {file = "libcst-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:3ece08ba778b6eeea74d9c705e9af2d1b4e915e9bc6de67ad173b962e575fcc0"}, + {file = "libcst-1.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:5efd1bf6ee5840d1b0b82ec8e0b9c64f182fa5a7c8aad680fbd918c4fa3826e0"}, + {file = "libcst-1.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08e9dca4ab6f8551794ce7ec146f86def6a82da41750cbed2c07551345fa10d3"}, + {file = "libcst-1.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8310521f2ccb79b5c4345750d475b88afa37bad930ab5554735f85ad5e3add30"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:da2d8b008aff72acd5a4a588491abdda1b446f17508e700f26df9be80d8442ae"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:be821d874ce8b26cbadd7277fa251a9b37f6d2326f8b5682b6fc8966b50a3a59"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f74b0bc7378ad5afcf25ac9d0367b4dbba50f6f6468faa41f5dfddcf8bf9c0f8"}, + {file = "libcst-1.8.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b68ea4a6018abfea1f68d50f74de7d399172684c264eb09809023e2c8696fc23"}, + {file = "libcst-1.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e264307ec49b2c72480422abafe80457f90b4e6e693b7ddf8a23d24b5c24001"}, + {file = "libcst-1.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5d5519962ce7c72d81888fb0c09e58e308ba4c376e76bcd853b48151063d6a8"}, + {file = "libcst-1.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:b62aa11d6b74ed5545e58ac613d3f63095e5fd0254b3e0d1168fda991b9a6b41"}, + {file = "libcst-1.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9c2bd4ac288a9cdb7ffc3229a9ce8027a66a3fd3f2ab9e13da60f5fbfe91f3b2"}, + {file = "libcst-1.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:08a8c7d9922ca6eed24e2c13a3c552b3c186af8fc78e5d4820b58487d780ec19"}, + {file = "libcst-1.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bba7c2b5063e8ada5a5477f9fa0c01710645426b5a8628ec50d558542a0a292e"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d97c9fe13aacfbefded6861f5200dcb8e837da7391a9bdeb44ccb133705990af"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d2194ae959630aae4176a4b75bd320b3274c20bef2a5ca6b8d6fc96d3c608edf"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0be639f5b2e1999a4b4a82a0f4633969f97336f052d0c131627983589af52f56"}, + {file = "libcst-1.8.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6753e50904e05c27915933da41518ecd7a8ca4dd3602112ba44920c6e353a455"}, + {file = "libcst-1.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:706d07106af91c343150be86caeae1ea3851b74aa0730fcbbf8cd089e817f818"}, + {file = "libcst-1.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd4310ea8ddc49cc8872e083737cf806299b17f93159a1f354d59aa08993e876"}, + {file = "libcst-1.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:51bbafdd847529e8a16d1965814ed17831af61452ee31943c414cb23451de926"}, + {file = "libcst-1.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:4f14f5045766646ed9e8826b959c6d07194788babed1e0ba08c94ea4f39517e3"}, + {file = "libcst-1.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f69582e24667715e3860d80d663f1caeb2398110077e23cc0a1e0066a851f5ab"}, + {file = "libcst-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ba85f9e6a7f37ef998168aa3fd28d263d7f83016bd306a4508a2394e5e793b4"}, + {file = "libcst-1.8.2-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:43ccaa6c54daa1749cec53710c70d47150965574d4c6d4c4f2e3f87b9bf9f591"}, + {file = "libcst-1.8.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8a81d816c2088d2055112af5ecd82fdfbe8ff277600e94255e2639b07de10234"}, + {file = "libcst-1.8.2-cp39-cp39-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:449f9ff8a5025dcd5c8d4ad28f6c291de5de89e4c044b0bda96b45bef8999b75"}, + {file = "libcst-1.8.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:36d5ab95f39f855521585b0e819dc2d4d1b2a4080bad04c2f3de1e387a5d2233"}, + {file = "libcst-1.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:207575dec2dae722acf6ab39b4b361151c65f8f895fd37edf9d384f5541562e1"}, + {file = "libcst-1.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52a1067cf31d9e9e4be514b253bea6276f1531dd7de6ab0917df8ce5b468a820"}, + {file = "libcst-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:59e8f611c977206eba294c296c2d29a1c1b1b88206cb97cd0d4847c1a3d923e7"}, + {file = "libcst-1.8.2-cp39-cp39-win_arm64.whl", hash = "sha256:ae22376633cfa3db21c4eed2870d1c36b5419289975a41a45f34a085b2d9e6ea"}, + {file = "libcst-1.8.2.tar.gz", hash = "sha256:66e82cedba95a6176194a817be4232c720312f8be6d2c8f3847f3317d95a0c7f"}, +] + +[package.dependencies] +pyyaml = {version = ">=5.2", markers = "python_version < \"3.13\""} +pyyaml-ft = {version = ">=8.0.0", markers = "python_version >= \"3.13\""} + [[package]] name = "litellm" version = "1.71.1" @@ -6249,6 +6324,34 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +description = "YAML parser and emitter for Python with support for free-threading" +optional = false +python-versions = ">=3.13" +groups = ["main"] +markers = "python_version == \"3.13\"" +files = [ + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"}, + {file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"}, +] + [[package]] name = "pyzmq" version = "26.4.0" @@ -8752,4 +8855,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.14" -content-hash = "f4b8e2deae971c4cc7812d81bd636f3869c1ff2317e3092220af66bd1430f94f" +content-hash = "959335d9e839697bd22e8cd53101394ac178749cc9da18d0adfd0b5e809cfe32" diff --git a/pyproject.toml b/pyproject.toml index 80343ccf..4d8479af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,7 @@ colorama = "^0.4.6" onepassword-sdk = "0.3.0" types-boto3 = {extras = ["full"], version = "^1.38.31"} lark = "^1.2.2" +libcst = "^1.8.2" [tool.poetry.group.dev.dependencies] isort = "^5.13.2" diff --git a/skyvern/core/code_generations/generate_code.py b/skyvern/core/code_generations/generate_code.py new file mode 100644 index 00000000..af2f4dc7 --- /dev/null +++ b/skyvern/core/code_generations/generate_code.py @@ -0,0 +1,315 @@ +# skyvern_codegen_cst.py +""" +Generate a runnable Skyvern workflow script **with LibCST**. + +Example +------- +from skyvern_codegen_cst import generate_workflow_script + +src = generate_workflow_script( + workflow=workflow_dict, + tasks=[task1, task2, ...], + actions_by_task={ + task1["task_id"]: task1_actions, + task2["task_id"]: task2_actions, + }, +) +Path("workflow.py").write_text(src) +""" + +from __future__ import annotations + +import keyword +from typing import Any, Iterable, Mapping + +import libcst as cst +from libcst import Attribute, Call, Dict, DictElement, FunctionDef, Name, Param + +# --------------------------------------------------------------------- # +# 1. helpers # +# --------------------------------------------------------------------- # + +ACTION_MAP = { + "click": "click", + "input_text": "input_text", + "upload_file": "upload_file", + "select_option": "select_option", + "goto": "goto", + "scroll": "scroll", + "keypress": "keypress", + "type": "type", + "move": "move", + "drag": "drag", + "solve_captcha": "solve_captcha", + "verification_code": "verification_code", +} + +INDENT = " " * 4 + + +def _safe_name(label: str) -> str: + s = "".join(c if c.isalnum() else "_" for c in label).lower() + if not s or s[0].isdigit() or keyword.iskeyword(s): + s = f"_{s}" + while "__" in s: + s = s.replace("__", "_") + return s + + +def _value(value: Any) -> cst.BaseExpression: + """Convert simple Python objects to CST expressions.""" + if isinstance(value, str): + if "\n" in value: + return cst.SimpleString('"""' + value.replace('"""', '\\"\\"\\"') + '"""') + return cst.SimpleString(repr(value)) + if isinstance(value, (int, float, bool)) or value is None: + return cst.parse_expression(repr(value)) + if isinstance(value, dict): + return Dict( + [ + DictElement( + key=_value(k), + value=_value(v), + ) + for k, v in value.items() + ] + ) + if isinstance(value, (list, tuple)): + elts = [cst.Element(_value(v)) for v in value] + return cst.List(elts) if isinstance(value, list) else cst.Tuple(elts) + # fallback + return cst.SimpleString(repr(str(value))) + + +# --------------------------------------------------------------------- # +# 2. builders # +# --------------------------------------------------------------------- # + + +def _make_decorator(block: Mapping[str, Any]) -> cst.Decorator: + bt = block["block_type"] + deco_name = { + "task": "task_block", + "file_download": "file_download_block", + "send_email": "email_block", + }[bt] + + kwargs = [] + field_map = { + "title": "title", + "navigation_goal": "prompt", + "url": "url", + "engine": "engine", + "model": "model", + "totp_identifier": "totp_identifier", + "webhook_callback_url": "webhook_callback_url", + "max_steps_per_run": "max_steps", + } + + for src_key, kw in field_map.items(): + v = block.get(src_key) + if v not in (None, "", [], {}): + kwargs.append(cst.Arg(value=_value(v), keyword=Name(kw))) + + # booleans + if block.get("complete_on_download"): + kwargs.append(cst.Arg(value=Name("True"), keyword=Name("complete_on_download"))) + if block.get("download_suffix"): + kwargs.append(cst.Arg(value=_value(block["download_suffix"]), keyword=Name("download_suffix"))) + + return cst.Decorator( + decorator=Call( + func=Attribute(value=Name("skyvern"), attr=Name(deco_name)), + args=kwargs, + ) + ) + + +def _action_to_stmt(act: Mapping[str, Any]) -> cst.BaseStatement: + """ + Turn one Action dict into: + + await page.(xpath=..., intention=..., data=context.parameters) + """ + method = ACTION_MAP[act["action_type"]] + + args = [ + cst.Arg(keyword=cst.Name("xpath"), value=_value(act["xpath"])), + cst.Arg( + keyword=cst.Name("intention"), + value=_value(act.get("intention") or act.get("reasoning") or ""), + ), + cst.Arg( + keyword=cst.Name("data"), + value=cst.Attribute(value=cst.Name("context"), attr=cst.Name("parameters")), + ), + ] + + call = cst.Call( + func=cst.Attribute(value=cst.Name("page"), attr=cst.Name(method)), + args=args, + ) + + # await page.method(...) + await_expr = cst.Await(call) + + # Wrap in a statement line: await ... + return cst.SimpleStatementLine([cst.Expr(await_expr)]) + + +def _build_block_fn(block: Mapping[str, Any], actions: Iterable[Mapping[str, Any]]) -> FunctionDef: + name = _safe_name(block["title"]) + body_stmts: list[cst.BaseStatement] = [] + + if block.get("url"): + body_stmts.append(cst.parse_statement(f"await page.goto({repr(block['url'])})")) + + for act in actions: + body_stmts.append(_action_to_stmt(act)) + + if not body_stmts: + body_stmts.append(cst.parse_statement("return None")) + + return FunctionDef( + name=Name(name), + params=cst.Parameters( + params=[ + Param(name=Name("page")), + Param(name=Name("context")), + ] + ), + decorators=[_make_decorator(block)], + body=cst.IndentedBlock(body_stmts), + returns=None, + asynchronous=cst.Asynchronous(), + ) + + +def _build_model(workflow: Mapping[str, Any]) -> cst.ClassDef: + """ + class WorkflowParameters(BaseModel): + ein_info: str + company_name: str + ... + """ + ann_lines: list[cst.BaseStatement] = [] + + for p in workflow["workflow_definition"]["parameters"]: + if p["parameter_type"] != "workflow": + continue + + # ein_info: str + ann = cst.AnnAssign( + target=cst.Name(p["key"]), + annotation=cst.Annotation(cst.Name("str")), + value=None, + ) + ann_lines.append(cst.SimpleStatementLine([ann])) + + if not ann_lines: # no parameters + ann_lines.append(cst.SimpleStatementLine([cst.Pass()])) + + return cst.ClassDef( + name=cst.Name("WorkflowParameters"), + bases=[cst.Arg(cst.Name("BaseModel"))], + body=cst.IndentedBlock(ann_lines), # ← wrap in block + ) + + +def _build_cached_params() -> cst.SimpleStatementLine: + src = "cached_parameters = WorkflowParameters(**{k: f'<{k}>' for k in WorkflowParameters.model_fields})" + return cst.parse_statement(src) + + +def _build_run_fn(task_fns: list[str]) -> FunctionDef: + body = [cst.parse_statement("page, context = await skyvern.setup(parameters.model_dump())")] + [ + cst.parse_statement(f"await {_safe_name(t)}(page, context)") for t in task_fns + ] + + return FunctionDef( + name=Name("run_workflow"), + decorators=[cst.Decorator(Attribute(value=Name("skyvern"), attr=Name("workflow")))], + params=cst.Parameters( + params=[ + Param( + name=Name("parameters"), + default=Name("cached_parameters"), + annotation=cst.Annotation(Name("WorkflowParameters")), + ) + ] + ), + body=cst.IndentedBlock(body), + returns=None, + asynchronous=cst.Asynchronous(), + ) + + +# --------------------------------------------------------------------- # +# 3. entrypoint # +# --------------------------------------------------------------------- # + + +def generate_workflow_script( + *, + workflow: Mapping[str, Any], + tasks: Iterable[Mapping[str, Any]], + actions_by_task: Mapping[str, Iterable[Mapping[str, Any]]], +) -> str: + """ + Build a LibCST Module and emit .code (PEP-8-formatted source). + """ + # --- imports -------------------------------------------------------- + imports: list[cst.BaseStatement] = [ + cst.SimpleStatementLine([cst.Import(names=[cst.ImportAlias(cst.Name("pydantic"))])]), + cst.SimpleStatementLine( + [ + cst.ImportFrom( + module=cst.Name("pydantic"), + names=[cst.ImportAlias(cst.Name("BaseModel"))], + ) + ] + ), + cst.SimpleStatementLine([cst.Import(names=[cst.ImportAlias(cst.Name("skyvern"))])]), + cst.SimpleStatementLine( + [ + cst.ImportFrom( + module=cst.Name("skyvern"), + names=[ + cst.ImportAlias(cst.Name("RunContext")), + cst.ImportAlias(cst.Name("SkyvernPage")), + ], + ) + ] + ), + ] + + # --- class + cached params ----------------------------------------- + model_cls = _build_model(workflow) + cached_params_stmt = _build_cached_params() + + # --- blocks --------------------------------------------------------- + block_fns: list[FunctionDef] = [] + task_titles = [] + for t in tasks: + fn = _build_block_fn(t, actions_by_task.get(t["task_id"], [])) + block_fns.append(fn) + task_titles.append(t["title"]) + + # --- runner --------------------------------------------------------- + run_fn = _build_run_fn(task_titles) + + module = cst.Module( + body=[ + *imports, + cst.EmptyLine(), + model_cls, + cst.EmptyLine(), + cached_params_stmt, + cst.EmptyLine(), + *block_fns, + cst.EmptyLine(), + run_fn, + cst.EmptyLine(), + ] + ) + return module.code