scripts gen all block support (#3407)

This commit is contained in:
Shuchang Zheng
2025-09-10 17:22:00 -07:00
committed by GitHub
parent 4a787de5ef
commit f582f84c95
4 changed files with 448 additions and 32 deletions

View File

@@ -33,12 +33,17 @@ from skyvern.services.script_service import ( # noqa: E402
action, # noqa: E402 action, # noqa: E402
download, # noqa: E402 download, # noqa: E402
extract, # noqa: E402 extract, # noqa: E402
http_request, # noqa: E402
generate_text, # noqa: E402 generate_text, # noqa: E402
goto, # noqa: E402
login, # noqa: E402 login, # noqa: E402
parse_file, # noqa: E402
prompt, # noqa: E402
render_template, # noqa: E402 render_template, # noqa: E402
run_code, # noqa: E402 run_code, # noqa: E402
run_script, # noqa: E402 run_script, # noqa: E402
run_task, # noqa: E402 run_task, # noqa: E402
send_email, # noqa: E402
wait, # noqa: E402 wait, # noqa: E402
) # noqa: E402 ) # noqa: E402
@@ -51,12 +56,17 @@ __all__ = [
"cached", "cached",
"download", "download",
"extract", "extract",
"http_request",
"generate_text", "generate_text",
"goto",
"login", "login",
"parse_file",
"prompt",
"render_template", "render_template",
"run_code", "run_code",
"run_script", "run_script",
"run_task", "run_task",
"send_email",
"setup", "setup",
"wait", "wait",
"workflow", "workflow",

View File

@@ -726,6 +726,14 @@ def _build_send_email_statement(block: dict[str, Any]) -> cst.SimpleStatementLin
last_line=cst.SimpleWhitespace(INDENT), last_line=cst.SimpleWhitespace(INDENT),
), ),
), ),
cst.Arg(
keyword=cst.Name("recipients"),
value=_value(block.get("recipients", [])),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
),
cst.Arg( cst.Arg(
keyword=cst.Name("subject"), keyword=cst.Name("subject"),
value=_value(block.get("subject", "")), value=_value(block.get("subject", "")),
@@ -742,17 +750,18 @@ def _build_send_email_statement(block: dict[str, Any]) -> cst.SimpleStatementLin
last_line=cst.SimpleWhitespace(INDENT), last_line=cst.SimpleWhitespace(INDENT),
), ),
), ),
# TODO: support file attachments?
# cst.Arg(
# keyword=cst.Name("file_attachments"),
# value=_value(block.get("file_attachments", [])),
# whitespace_after_arg=cst.ParenthesizedWhitespace(
# indent=True,
# last_line=cst.SimpleWhitespace(INDENT),
# ),
# ),
cst.Arg( cst.Arg(
keyword=cst.Name("recipients"), keyword=cst.Name("label"),
value=_value(block.get("recipients", [])), value=_value(block.get("label", "")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
),
cst.Arg(
keyword=cst.Name("attach_downloaded_files"),
value=_value(block.get("attach_downloaded_files", False)),
whitespace_after_arg=cst.ParenthesizedWhitespace( whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True, indent=True,
), ),
@@ -861,6 +870,14 @@ def _build_goto_statement(block: dict[str, Any]) -> cst.SimpleStatementLine:
cst.Arg( cst.Arg(
keyword=cst.Name("url"), keyword=cst.Name("url"),
value=_value(block.get("url", "")), value=_value(block.get("url", "")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
),
cst.Arg(
keyword=cst.Name("label"),
value=_value(block.get("label") or block.get("title") or f"block_{block.get('workflow_run_block_id')}"),
whitespace_after_arg=cst.ParenthesizedWhitespace( whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True, indent=True,
), ),
@@ -937,6 +954,7 @@ def _build_file_upload_statement(block: dict[str, Any]) -> cst.SimpleStatementLi
value=_value(block.get("parameters", None)), value=_value(block.get("parameters", None)),
whitespace_after_arg=cst.ParenthesizedWhitespace( whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True, indent=True,
last_line=cst.SimpleWhitespace(INDENT),
), ),
), ),
cst.Arg( cst.Arg(
@@ -944,6 +962,7 @@ def _build_file_upload_statement(block: dict[str, Any]) -> cst.SimpleStatementLi
value=_value(block.get("storage_type", FileStorageType.S3)), value=_value(block.get("storage_type", FileStorageType.S3)),
whitespace_after_arg=cst.ParenthesizedWhitespace( whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True, indent=True,
last_line=cst.SimpleWhitespace(INDENT),
), ),
), ),
] ]
@@ -964,9 +983,12 @@ def _build_file_upload_statement(block: dict[str, Any]) -> cst.SimpleStatementLi
value=_value(block.get(key, "")), value=_value(block.get(key, "")),
whitespace_after_arg=cst.ParenthesizedWhitespace( whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True, indent=True,
last_line=cst.SimpleWhitespace(INDENT),
), ),
) )
) )
_mark_last_arg_as_comma(args)
call = cst.Call( call = cst.Call(
func=cst.Attribute(value=cst.Name("skyvern"), attr=cst.Name("upload_file")), func=cst.Attribute(value=cst.Name("skyvern"), attr=cst.Name("upload_file")),
args=args, args=args,
@@ -979,6 +1001,238 @@ def _build_file_upload_statement(block: dict[str, Any]) -> cst.SimpleStatementLi
return cst.SimpleStatementLine([cst.Expr(cst.Await(call))]) return cst.SimpleStatementLine([cst.Expr(cst.Await(call))])
def _build_file_url_parser_statement(block: dict[str, Any]) -> cst.SimpleStatementLine:
"""Build a skyvern.parse_file statement."""
args = [
cst.Arg(
keyword=cst.Name("file_url"),
value=_value(block.get("file_url", "")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
),
cst.Arg(
keyword=cst.Name("file_type"),
value=_value(str(block.get("file_type"))),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
),
]
# Add optional parameters if they exist
if block.get("json_schema") is not None:
args.append(
cst.Arg(
keyword=cst.Name("schema"),
value=_value(block.get("json_schema")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
)
if block.get("label") is not None:
args.append(
cst.Arg(
keyword=cst.Name("label"),
value=_value(block.get("label")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
)
_mark_last_arg_as_comma(args)
call = cst.Call(
func=cst.Attribute(value=cst.Name("skyvern"), attr=cst.Name("parse_file")),
args=args,
whitespace_before_args=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
return cst.SimpleStatementLine([cst.Expr(cst.Await(call))])
def _build_http_request_statement(block: dict[str, Any]) -> cst.SimpleStatementLine:
"""Build a skyvern.http_request statement."""
args = [
cst.Arg(
keyword=cst.Name("method"),
value=_value(block.get("method", "GET")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
),
cst.Arg(
keyword=cst.Name("url"),
value=_value(block.get("url", "")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
),
]
# Add optional parameters if they exist
if block.get("headers") is not None:
args.append(
cst.Arg(
keyword=cst.Name("headers"),
value=_value(block.get("headers")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
)
if block.get("body") is not None:
args.append(
cst.Arg(
keyword=cst.Name("body"),
value=_value(block.get("body")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
)
if block.get("timeout") is not None:
args.append(
cst.Arg(
keyword=cst.Name("timeout"),
value=_value(block.get("timeout")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
)
if block.get("follow_redirects") is not None:
args.append(
cst.Arg(
keyword=cst.Name("follow_redirects"),
value=_value(block.get("follow_redirects")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
)
if block.get("label") is not None:
args.append(
cst.Arg(
keyword=cst.Name("label"),
value=_value(block.get("label")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
)
_mark_last_arg_as_comma(args)
call = cst.Call(
func=cst.Attribute(value=cst.Name("skyvern"), attr=cst.Name("http_request")),
args=args,
whitespace_before_args=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
return cst.SimpleStatementLine([cst.Expr(cst.Await(call))])
def _build_prompt_statement(block: dict[str, Any]) -> cst.SimpleStatementLine:
"""Build a skyvern.prompt statement."""
args = [
cst.Arg(
keyword=cst.Name("prompt"),
value=_render_value(block.get("prompt", "")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
),
]
# Add optional parameters if they exist
if block.get("json_schema") is not None:
args.append(
cst.Arg(
keyword=cst.Name("schema"),
value=_value(block.get("json_schema")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
)
if block.get("label") is not None:
args.append(
cst.Arg(
keyword=cst.Name("label"),
value=_value(block.get("label")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
)
if block.get("parameters") is not None:
args.append(
cst.Arg(
keyword=cst.Name("parameters"),
value=_value(block.get("parameters")),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
),
)
)
call = cst.Call(
func=cst.Attribute(value=cst.Name("skyvern"), attr=cst.Name("prompt")),
args=args,
whitespace_before_args=cst.ParenthesizedWhitespace(
indent=True,
last_line=cst.SimpleWhitespace(INDENT),
),
)
return cst.SimpleStatementLine([cst.Expr(cst.Await(call))])
def _mark_last_arg_as_comma(args: list[cst.Arg]) -> None:
if not args:
return
last_arg = args.pop()
new_arg = cst.Arg(
keyword=last_arg.keyword,
value=last_arg.value,
comma=cst.Comma(),
whitespace_after_arg=cst.ParenthesizedWhitespace(
indent=True,
),
)
args.append(new_arg)
def __build_base_task_statement(block_title: str, block: dict[str, Any]) -> list[cst.Arg]: def __build_base_task_statement(block_title: str, block: dict[str, Any]) -> list[cst.Arg]:
args = [ args = [
cst.Arg( cst.Arg(
@@ -1081,7 +1335,7 @@ def _build_run_fn(blocks: list[dict[str, Any]], wf_req: dict[str, Any]) -> Funct
elif block_type == "send_email": elif block_type == "send_email":
stmt = _build_send_email_statement(block) stmt = _build_send_email_statement(block)
elif block_type == "text_prompt": elif block_type == "text_prompt":
stmt = _build_validate_statement(block) stmt = _build_prompt_statement(block)
elif block_type == "wait": elif block_type == "wait":
stmt = _build_wait_statement(block) stmt = _build_wait_statement(block)
elif block_type == "for_loop": elif block_type == "for_loop":
@@ -1092,6 +1346,10 @@ def _build_run_fn(blocks: list[dict[str, Any]], wf_req: dict[str, Any]) -> Funct
stmt = _build_code_statement(block) stmt = _build_code_statement(block)
elif block_type == "file_upload": elif block_type == "file_upload":
stmt = _build_file_upload_statement(block) stmt = _build_file_upload_statement(block)
elif block_type == "file_url_parser":
stmt = _build_file_url_parser_statement(block)
elif block_type == "http_request":
stmt = _build_http_request_statement(block)
else: else:
# Default case for unknown block types # Default case for unknown block types
stmt = cst.SimpleStatementLine([cst.Expr(cst.SimpleString(f"# Unknown block type: {block_type}"))]) stmt = cst.SimpleStatementLine([cst.Expr(cst.SimpleString(f"# Unknown block type: {block_type}"))])
@@ -1099,26 +1357,26 @@ def _build_run_fn(blocks: list[dict[str, Any]], wf_req: dict[str, Any]) -> Funct
body.append(stmt) body.append(stmt)
# Add a final validation step if not already present # Add a final validation step if not already present
has_validation = any(block.get("block_type") == "text_prompt" for block in blocks) # has_validation = any(block.get("block_type") == "text_prompt" for block in blocks)
has_task_blocks = any(block.get("block_type") in SCRIPT_TASK_BLOCKS for block in blocks) # has_task_blocks = any(block.get("block_type") in SCRIPT_TASK_BLOCKS for block in blocks)
if not has_validation and not has_task_blocks: # if not has_validation and not has_task_blocks:
# Build the final validation statement using LibCST components # # Build the final validation statement using LibCST components
args = [ # args = [
cst.Arg( # cst.Arg(
keyword=cst.Name("prompt"), # keyword=cst.Name("prompt"),
value=cst.SimpleString( # value=cst.SimpleString(
'"Your goal is to validate that the workflow completed successfully. COMPLETE if successful, TERMINATE if there are issues."' # '"Your goal is to validate that the workflow completed successfully. COMPLETE if successful, TERMINATE if there are issues."'
), # ),
), # ),
] # ]
call = cst.Call( # call = cst.Call(
func=cst.Attribute(value=cst.Name("skyvern"), attr=cst.Name("validate")), # func=cst.Attribute(value=cst.Name("skyvern"), attr=cst.Name("validate")),
args=args, # args=args,
) # )
validation_stmt = cst.SimpleStatementLine([cst.Expr(cst.Await(call))]) # validation_stmt = cst.SimpleStatementLine([cst.Expr(cst.Await(call))])
body.append(validation_stmt) # body.append(validation_stmt)
params = cst.Parameters( params = cst.Parameters(
params=[ params=[

View File

@@ -90,6 +90,12 @@ class Workflow(BaseModel):
return block.output_parameter return block.output_parameter
return None return None
def get_parameter(self, key: str) -> PARAMETER_TYPE | None:
for parameter in self.workflow_definition.parameters:
if parameter.key == key:
return parameter
return None
class WorkflowRunStatus(StrEnum): class WorkflowRunStatus(StrEnum):
created = "created" created = "created"

View File

@@ -27,12 +27,21 @@ from skyvern.forge.sdk.core import skyvern_context
from skyvern.forge.sdk.models import Step, StepStatus from skyvern.forge.sdk.models import Step, StepStatus
from skyvern.forge.sdk.schemas.files import FileInfo from skyvern.forge.sdk.schemas.files import FileInfo
from skyvern.forge.sdk.schemas.tasks import Task, TaskOutput, TaskStatus from skyvern.forge.sdk.schemas.tasks import Task, TaskOutput, TaskStatus
from skyvern.forge.sdk.workflow.models.block import CodeBlock, FileUploadBlock, TaskBlock from skyvern.forge.sdk.workflow.models.block import (
CodeBlock,
FileParserBlock,
FileUploadBlock,
HttpRequestBlock,
SendEmailBlock,
TaskBlock,
TextPromptBlock,
UrlBlock,
)
from skyvern.forge.sdk.workflow.models.parameter import PARAMETER_TYPE, OutputParameter from skyvern.forge.sdk.workflow.models.parameter import PARAMETER_TYPE, OutputParameter
from skyvern.forge.sdk.workflow.models.workflow import Workflow from skyvern.forge.sdk.workflow.models.workflow import Workflow
from skyvern.schemas.runs import RunEngine from skyvern.schemas.runs import RunEngine
from skyvern.schemas.scripts import CreateScriptResponse, FileEncoding, FileNode, ScriptFileCreate from skyvern.schemas.scripts import CreateScriptResponse, FileEncoding, FileNode, ScriptFileCreate
from skyvern.schemas.workflows import BlockStatus, BlockType, FileStorageType from skyvern.schemas.workflows import BlockStatus, BlockType, FileStorageType, FileType
LOG = structlog.get_logger(__name__) LOG = structlog.get_logger(__name__)
jinja_sandbox_env = SandboxedEnvironment() jinja_sandbox_env = SandboxedEnvironment()
@@ -1323,7 +1332,7 @@ async def run_script(
if parameters: if parameters:
await user_script.run_workflow(parameters=parameters) await user_script.run_workflow(parameters=parameters)
else: else:
await user_script.run_workflow() await user_script.run_workflow(parameters={})
else: else:
raise Exception(f"No 'run_workflow' function found in {path}") raise Exception(f"No 'run_workflow' function found in {path}")
@@ -1382,6 +1391,7 @@ def render_template(template: str, data: dict[str, Any] | None = None) -> str:
class BlockValidationOutput: class BlockValidationOutput:
label: str label: str
output_parameter: OutputParameter output_parameter: OutputParameter
workflow: Workflow
workflow_id: str workflow_id: str
workflow_run_id: str workflow_run_id: str
organization_id: str organization_id: str
@@ -1410,6 +1420,7 @@ async def _validate_and_get_output_parameter(label: str | None = None) -> BlockV
return BlockValidationOutput( return BlockValidationOutput(
label=label, label=label,
output_parameter=output_parameter, output_parameter=output_parameter,
workflow=workflow,
workflow_id=workflow_id, workflow_id=workflow_id,
workflow_run_id=workflow_run_id, workflow_run_id=workflow_run_id,
organization_id=organization_id, organization_id=organization_id,
@@ -1470,3 +1481,134 @@ async def upload_file(
organization_id=block_validation_output.organization_id, organization_id=block_validation_output.organization_id,
browser_session_id=block_validation_output.browser_session_id, browser_session_id=block_validation_output.browser_session_id,
) )
async def send_email(
sender: str,
recipients: list[str],
subject: str,
body: str,
file_attachments: list[str] = [],
label: str | None = None,
parameters: list[PARAMETER_TYPE] | None = None,
) -> None:
block_validation_output = await _validate_and_get_output_parameter(label)
workflow = block_validation_output.workflow
smtp_host_parameter = workflow.get_parameter("smtp_host")
smtp_port_parameter = workflow.get_parameter("smtp_port")
smtp_username_parameter = workflow.get_parameter("smtp_username")
smtp_password_parameter = workflow.get_parameter("smtp_password")
if not smtp_host_parameter or not smtp_port_parameter or not smtp_username_parameter or not smtp_password_parameter:
raise Exception("SMTP host, port, username, and password parameters are required")
send_email_block = SendEmailBlock(
smtp_host=smtp_host_parameter,
smtp_port=smtp_port_parameter,
smtp_username=smtp_username_parameter,
smtp_password=smtp_password_parameter,
sender=sender,
recipients=recipients,
subject=subject,
body=body,
file_attachments=file_attachments,
label=block_validation_output.label,
output_parameter=block_validation_output.output_parameter,
parameters=parameters or [],
)
await send_email_block.execute_safe(
workflow_run_id=block_validation_output.workflow_run_id,
organization_id=block_validation_output.organization_id,
browser_session_id=block_validation_output.browser_session_id,
)
async def parse_file(
file_url: str,
file_type: FileType,
schema: dict[str, Any] | None = None,
label: str | None = None,
parameters: list[PARAMETER_TYPE] | None = None,
) -> None:
block_validation_output = await _validate_and_get_output_parameter(label)
file_parser_block = FileParserBlock(
file_url=file_url,
file_type=file_type,
json_schema=schema,
label=block_validation_output.label,
output_parameter=block_validation_output.output_parameter,
parameters=parameters or [],
)
await file_parser_block.execute_safe(
workflow_run_id=block_validation_output.workflow_run_id,
organization_id=block_validation_output.organization_id,
browser_session_id=block_validation_output.browser_session_id,
)
async def http_request(
method: str,
url: str,
headers: dict[str, str] | None = None,
body: dict[str, Any] | None = None,
timeout: int = 30,
follow_redirects: bool = True,
label: str | None = None,
parameters: list[PARAMETER_TYPE] | None = None,
) -> None:
block_validation_output = await _validate_and_get_output_parameter(label)
http_request_block = HttpRequestBlock(
method=method,
url=url,
headers=headers,
body=body,
timeout=timeout,
follow_redirects=follow_redirects,
label=block_validation_output.label,
output_parameter=block_validation_output.output_parameter,
parameters=parameters or [],
)
await http_request_block.execute_safe(
workflow_run_id=block_validation_output.workflow_run_id,
organization_id=block_validation_output.organization_id,
browser_session_id=block_validation_output.browser_session_id,
)
async def goto(
url: str,
label: str | None = None,
parameters: list[PARAMETER_TYPE] | None = None,
) -> None:
block_validation_output = await _validate_and_get_output_parameter(label)
goto_url_block = UrlBlock(
url=url,
label=block_validation_output.label,
output_parameter=block_validation_output.output_parameter,
parameters=parameters or [],
)
await goto_url_block.execute_safe(
workflow_run_id=block_validation_output.workflow_run_id,
organization_id=block_validation_output.organization_id,
browser_session_id=block_validation_output.browser_session_id,
)
async def prompt(
prompt: str,
schema: dict[str, Any] | None = None,
label: str | None = None,
parameters: list[PARAMETER_TYPE] | None = None,
) -> dict[str, Any] | list | str | None:
block_validation_output = await _validate_and_get_output_parameter(label)
prompt_block = TextPromptBlock(
prompt=prompt,
json_schema=schema,
label=block_validation_output.label,
output_parameter=block_validation_output.output_parameter,
parameters=parameters or [],
)
result = await prompt_block.execute_safe(
workflow_run_id=block_validation_output.workflow_run_id,
organization_id=block_validation_output.organization_id,
browser_session_id=block_validation_output.browser_session_id,
)
return result.output_parameter_value