From 4665f8907da7b11924aae039f05ae94d18155871 Mon Sep 17 00:00:00 2001 From: Stanislav Novosad Date: Thu, 4 Dec 2025 10:50:29 -0700 Subject: [PATCH] SDK: download files (#4196) --- skyvern/client/__init__.py | 27 +- skyvern/client/client.py | 254 ++++++++++++++- skyvern/client/raw_client.py | 292 +++++++++++++++++- skyvern/client/types/__init__.py | 26 +- skyvern/client/types/branch_condition.py | 4 +- .../client/types/branch_condition_criteria.py | 26 ++ skyvern/client/types/branch_condition_yaml.py | 23 ++ skyvern/client/types/branch_criteria_yaml.py | 21 ++ .../client/types/conditional_block_yaml.py | 32 ++ .../for_loop_block_yaml_loop_blocks_item.py | 20 ++ ...h_criteria.py => jinja_branch_criteria.py} | 5 +- .../workflow_definition_yaml_blocks_item.py | 20 ++ skyvern/forge/sdk/routes/code_samples.py | 18 ++ skyvern/forge/sdk/routes/run_blocks.py | 234 ++++++++++---- skyvern/library/skyvern_browser_page.py | 57 ++++ skyvern/schemas/run_blocks.py | 29 +- 16 files changed, 983 insertions(+), 105 deletions(-) create mode 100644 skyvern/client/types/branch_condition_criteria.py create mode 100644 skyvern/client/types/branch_condition_yaml.py create mode 100644 skyvern/client/types/branch_criteria_yaml.py create mode 100644 skyvern/client/types/conditional_block_yaml.py rename skyvern/client/types/{branch_criteria.py => jinja_branch_criteria.py} (79%) diff --git a/skyvern/client/__init__.py b/skyvern/client/__init__.py index 2ca8c695..bec5a8eb 100644 --- a/skyvern/client/__init__.py +++ b/skyvern/client/__init__.py @@ -42,7 +42,10 @@ if typing.TYPE_CHECKING: BitwardenSensitiveInformationParameterYaml, BlockType, BranchCondition, - BranchCriteria, + BranchConditionCriteria, + BranchConditionCriteria_Jinja2Template, + BranchConditionYaml, + BranchCriteriaYaml, BrowserProfile, BrowserSessionResponse, ClickAction, @@ -63,6 +66,7 @@ if typing.TYPE_CHECKING: CodeBlockParametersItem_Workflow, CodeBlockYaml, ConditionalBlock, + ConditionalBlockYaml, ContextParameter, ContextParameterSource, ContextParameterSource_AwsSecret, @@ -172,6 +176,7 @@ if typing.TYPE_CHECKING: ForLoopBlockYamlLoopBlocksItem, ForLoopBlockYamlLoopBlocksItem_Action, ForLoopBlockYamlLoopBlocksItem_Code, + ForLoopBlockYamlLoopBlocksItem_Conditional, ForLoopBlockYamlLoopBlocksItem_DownloadToS3, ForLoopBlockYamlLoopBlocksItem_Extraction, ForLoopBlockYamlLoopBlocksItem_FileDownload, @@ -232,6 +237,7 @@ if typing.TYPE_CHECKING: InputOrSelectContext, InputTextAction, InputTextActionData, + JinjaBranchCriteria, LocateElementAction, LoginBlock, LoginBlockDataSchema, @@ -431,6 +437,7 @@ if typing.TYPE_CHECKING: WorkflowDefinitionYamlBlocksItem, WorkflowDefinitionYamlBlocksItem_Action, WorkflowDefinitionYamlBlocksItem_Code, + WorkflowDefinitionYamlBlocksItem_Conditional, WorkflowDefinitionYamlBlocksItem_DownloadToS3, WorkflowDefinitionYamlBlocksItem_Extraction, WorkflowDefinitionYamlBlocksItem_FileDownload, @@ -523,7 +530,10 @@ _dynamic_imports: typing.Dict[str, str] = { "BitwardenSensitiveInformationParameterYaml": ".types", "BlockType": ".types", "BranchCondition": ".types", - "BranchCriteria": ".types", + "BranchConditionCriteria": ".types", + "BranchConditionCriteria_Jinja2Template": ".types", + "BranchConditionYaml": ".types", + "BranchCriteriaYaml": ".types", "BrowserProfile": ".types", "BrowserSessionResponse": ".types", "ClickAction": ".types", @@ -544,6 +554,7 @@ _dynamic_imports: typing.Dict[str, str] = { "CodeBlockParametersItem_Workflow": ".types", "CodeBlockYaml": ".types", "ConditionalBlock": ".types", + "ConditionalBlockYaml": ".types", "ConflictError": ".errors", "ContextParameter": ".types", "ContextParameterSource": ".types", @@ -654,6 +665,7 @@ _dynamic_imports: typing.Dict[str, str] = { "ForLoopBlockYamlLoopBlocksItem": ".types", "ForLoopBlockYamlLoopBlocksItem_Action": ".types", "ForLoopBlockYamlLoopBlocksItem_Code": ".types", + "ForLoopBlockYamlLoopBlocksItem_Conditional": ".types", "ForLoopBlockYamlLoopBlocksItem_DownloadToS3": ".types", "ForLoopBlockYamlLoopBlocksItem_Extraction": ".types", "ForLoopBlockYamlLoopBlocksItem_FileDownload": ".types", @@ -715,6 +727,7 @@ _dynamic_imports: typing.Dict[str, str] = { "InputOrSelectContext": ".types", "InputTextAction": ".types", "InputTextActionData": ".types", + "JinjaBranchCriteria": ".types", "LocateElementAction": ".types", "LoginBlock": ".types", "LoginBlockDataSchema": ".types", @@ -918,6 +931,7 @@ _dynamic_imports: typing.Dict[str, str] = { "WorkflowDefinitionYamlBlocksItem": ".types", "WorkflowDefinitionYamlBlocksItem_Action": ".types", "WorkflowDefinitionYamlBlocksItem_Code": ".types", + "WorkflowDefinitionYamlBlocksItem_Conditional": ".types", "WorkflowDefinitionYamlBlocksItem_DownloadToS3": ".types", "WorkflowDefinitionYamlBlocksItem_Extraction": ".types", "WorkflowDefinitionYamlBlocksItem_FileDownload": ".types", @@ -1031,7 +1045,10 @@ __all__ = [ "BitwardenSensitiveInformationParameterYaml", "BlockType", "BranchCondition", - "BranchCriteria", + "BranchConditionCriteria", + "BranchConditionCriteria_Jinja2Template", + "BranchConditionYaml", + "BranchCriteriaYaml", "BrowserProfile", "BrowserSessionResponse", "ClickAction", @@ -1052,6 +1069,7 @@ __all__ = [ "CodeBlockParametersItem_Workflow", "CodeBlockYaml", "ConditionalBlock", + "ConditionalBlockYaml", "ConflictError", "ContextParameter", "ContextParameterSource", @@ -1162,6 +1180,7 @@ __all__ = [ "ForLoopBlockYamlLoopBlocksItem", "ForLoopBlockYamlLoopBlocksItem_Action", "ForLoopBlockYamlLoopBlocksItem_Code", + "ForLoopBlockYamlLoopBlocksItem_Conditional", "ForLoopBlockYamlLoopBlocksItem_DownloadToS3", "ForLoopBlockYamlLoopBlocksItem_Extraction", "ForLoopBlockYamlLoopBlocksItem_FileDownload", @@ -1223,6 +1242,7 @@ __all__ = [ "InputOrSelectContext", "InputTextAction", "InputTextActionData", + "JinjaBranchCriteria", "LocateElementAction", "LoginBlock", "LoginBlockDataSchema", @@ -1426,6 +1446,7 @@ __all__ = [ "WorkflowDefinitionYamlBlocksItem", "WorkflowDefinitionYamlBlocksItem_Action", "WorkflowDefinitionYamlBlocksItem_Code", + "WorkflowDefinitionYamlBlocksItem_Conditional", "WorkflowDefinitionYamlBlocksItem_DownloadToS3", "WorkflowDefinitionYamlBlocksItem_Extraction", "WorkflowDefinitionYamlBlocksItem_FileDownload", diff --git a/skyvern/client/client.py b/skyvern/client/client.py index b1a6df48..803838c4 100644 --- a/skyvern/client/client.py +++ b/skyvern/client/client.py @@ -1391,7 +1391,6 @@ class Skyvern: *, credential_type: SkyvernSchemasRunBlocksCredentialType, url: typing.Optional[str] = OMIT, - prompt: typing.Optional[str] = OMIT, webhook_url: typing.Optional[str] = OMIT, proxy_location: typing.Optional[ProxyLocation] = OMIT, totp_identifier: typing.Optional[str] = OMIT, @@ -1401,6 +1400,7 @@ class Skyvern: browser_address: typing.Optional[str] = OMIT, extra_http_headers: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, max_screenshot_scrolling_times: typing.Optional[int] = OMIT, + prompt: typing.Optional[str] = OMIT, credential_id: typing.Optional[str] = OMIT, bitwarden_collection_id: typing.Optional[str] = OMIT, bitwarden_item_id: typing.Optional[str] = OMIT, @@ -1421,13 +1421,10 @@ class Skyvern: Where to get the credential from url : typing.Optional[str] - Website url - - prompt : typing.Optional[str] - Login instructions. Skyvern has default prompt/instruction for login if this field is not provided. + Website URL webhook_url : typing.Optional[str] - Webhook URL to send login status updates + Webhook URL to send status updates proxy_location : typing.Optional[ProxyLocation] Proxy location to use @@ -1453,6 +1450,9 @@ class Skyvern: max_screenshot_scrolling_times : typing.Optional[int] Maximum number of times to scroll for screenshots + prompt : typing.Optional[str] + Login instructions. Skyvern has default prompt/instruction for login if this field is not provided. + credential_id : typing.Optional[str] ID of the Skyvern credential to use for login. @@ -1502,7 +1502,6 @@ class Skyvern: _response = self._raw_client.login( credential_type=credential_type, url=url, - prompt=prompt, webhook_url=webhook_url, proxy_location=proxy_location, totp_identifier=totp_identifier, @@ -1512,6 +1511,7 @@ class Skyvern: browser_address=browser_address, extra_http_headers=extra_http_headers, max_screenshot_scrolling_times=max_screenshot_scrolling_times, + prompt=prompt, credential_id=credential_id, bitwarden_collection_id=bitwarden_collection_id, bitwarden_item_id=bitwarden_item_id, @@ -1525,6 +1525,115 @@ class Skyvern: ) return _response.data + def file_download( + self, + *, + navigation_goal: str, + url: typing.Optional[str] = OMIT, + webhook_url: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + totp_identifier: typing.Optional[str] = OMIT, + totp_url: typing.Optional[str] = OMIT, + browser_session_id: typing.Optional[str] = OMIT, + browser_profile_id: typing.Optional[str] = OMIT, + browser_address: typing.Optional[str] = OMIT, + extra_http_headers: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + max_screenshot_scrolling_times: typing.Optional[int] = OMIT, + download_suffix: typing.Optional[str] = OMIT, + download_timeout: typing.Optional[float] = OMIT, + max_steps_per_run: typing.Optional[int] = OMIT, + parameter_keys: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> WorkflowRunResponse: + """ + Download a file from a website by navigating and clicking download buttons + + Parameters + ---------- + navigation_goal : str + Instructions for navigating to and downloading the file + + url : typing.Optional[str] + Website URL + + webhook_url : typing.Optional[str] + Webhook URL to send status updates + + proxy_location : typing.Optional[ProxyLocation] + Proxy location to use + + totp_identifier : typing.Optional[str] + Identifier for TOTP (Time-based One-Time Password) if required + + totp_url : typing.Optional[str] + TOTP URL to fetch one-time passwords + + browser_session_id : typing.Optional[str] + ID of the browser session to use, which is prefixed by `pbs_` e.g. `pbs_123456` + + browser_profile_id : typing.Optional[str] + ID of a browser profile to reuse for this run + + browser_address : typing.Optional[str] + The CDP address for the task. + + extra_http_headers : typing.Optional[typing.Dict[str, typing.Optional[str]]] + Additional HTTP headers to include in requests + + max_screenshot_scrolling_times : typing.Optional[int] + Maximum number of times to scroll for screenshots + + download_suffix : typing.Optional[str] + Suffix or complete filename for the downloaded file + + download_timeout : typing.Optional[float] + Timeout in seconds for the download operation + + max_steps_per_run : typing.Optional[int] + Maximum number of steps to execute + + parameter_keys : typing.Optional[typing.Sequence[str]] + List of parameter keys to use in the workflow + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + WorkflowRunResponse + Successful Response + + Examples + -------- + from skyvern import Skyvern + + client = Skyvern( + api_key="YOUR_API_KEY", + ) + client.file_download( + navigation_goal="navigation_goal", + ) + """ + _response = self._raw_client.file_download( + navigation_goal=navigation_goal, + url=url, + webhook_url=webhook_url, + proxy_location=proxy_location, + totp_identifier=totp_identifier, + totp_url=totp_url, + browser_session_id=browser_session_id, + browser_profile_id=browser_profile_id, + browser_address=browser_address, + extra_http_headers=extra_http_headers, + max_screenshot_scrolling_times=max_screenshot_scrolling_times, + download_suffix=download_suffix, + download_timeout=download_timeout, + max_steps_per_run=max_steps_per_run, + parameter_keys=parameter_keys, + request_options=request_options, + ) + return _response.data + def get_scripts( self, *, @@ -3317,7 +3426,6 @@ class AsyncSkyvern: *, credential_type: SkyvernSchemasRunBlocksCredentialType, url: typing.Optional[str] = OMIT, - prompt: typing.Optional[str] = OMIT, webhook_url: typing.Optional[str] = OMIT, proxy_location: typing.Optional[ProxyLocation] = OMIT, totp_identifier: typing.Optional[str] = OMIT, @@ -3327,6 +3435,7 @@ class AsyncSkyvern: browser_address: typing.Optional[str] = OMIT, extra_http_headers: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, max_screenshot_scrolling_times: typing.Optional[int] = OMIT, + prompt: typing.Optional[str] = OMIT, credential_id: typing.Optional[str] = OMIT, bitwarden_collection_id: typing.Optional[str] = OMIT, bitwarden_item_id: typing.Optional[str] = OMIT, @@ -3347,13 +3456,10 @@ class AsyncSkyvern: Where to get the credential from url : typing.Optional[str] - Website url - - prompt : typing.Optional[str] - Login instructions. Skyvern has default prompt/instruction for login if this field is not provided. + Website URL webhook_url : typing.Optional[str] - Webhook URL to send login status updates + Webhook URL to send status updates proxy_location : typing.Optional[ProxyLocation] Proxy location to use @@ -3379,6 +3485,9 @@ class AsyncSkyvern: max_screenshot_scrolling_times : typing.Optional[int] Maximum number of times to scroll for screenshots + prompt : typing.Optional[str] + Login instructions. Skyvern has default prompt/instruction for login if this field is not provided. + credential_id : typing.Optional[str] ID of the Skyvern credential to use for login. @@ -3436,7 +3545,6 @@ class AsyncSkyvern: _response = await self._raw_client.login( credential_type=credential_type, url=url, - prompt=prompt, webhook_url=webhook_url, proxy_location=proxy_location, totp_identifier=totp_identifier, @@ -3446,6 +3554,7 @@ class AsyncSkyvern: browser_address=browser_address, extra_http_headers=extra_http_headers, max_screenshot_scrolling_times=max_screenshot_scrolling_times, + prompt=prompt, credential_id=credential_id, bitwarden_collection_id=bitwarden_collection_id, bitwarden_item_id=bitwarden_item_id, @@ -3459,6 +3568,123 @@ class AsyncSkyvern: ) return _response.data + async def file_download( + self, + *, + navigation_goal: str, + url: typing.Optional[str] = OMIT, + webhook_url: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + totp_identifier: typing.Optional[str] = OMIT, + totp_url: typing.Optional[str] = OMIT, + browser_session_id: typing.Optional[str] = OMIT, + browser_profile_id: typing.Optional[str] = OMIT, + browser_address: typing.Optional[str] = OMIT, + extra_http_headers: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + max_screenshot_scrolling_times: typing.Optional[int] = OMIT, + download_suffix: typing.Optional[str] = OMIT, + download_timeout: typing.Optional[float] = OMIT, + max_steps_per_run: typing.Optional[int] = OMIT, + parameter_keys: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> WorkflowRunResponse: + """ + Download a file from a website by navigating and clicking download buttons + + Parameters + ---------- + navigation_goal : str + Instructions for navigating to and downloading the file + + url : typing.Optional[str] + Website URL + + webhook_url : typing.Optional[str] + Webhook URL to send status updates + + proxy_location : typing.Optional[ProxyLocation] + Proxy location to use + + totp_identifier : typing.Optional[str] + Identifier for TOTP (Time-based One-Time Password) if required + + totp_url : typing.Optional[str] + TOTP URL to fetch one-time passwords + + browser_session_id : typing.Optional[str] + ID of the browser session to use, which is prefixed by `pbs_` e.g. `pbs_123456` + + browser_profile_id : typing.Optional[str] + ID of a browser profile to reuse for this run + + browser_address : typing.Optional[str] + The CDP address for the task. + + extra_http_headers : typing.Optional[typing.Dict[str, typing.Optional[str]]] + Additional HTTP headers to include in requests + + max_screenshot_scrolling_times : typing.Optional[int] + Maximum number of times to scroll for screenshots + + download_suffix : typing.Optional[str] + Suffix or complete filename for the downloaded file + + download_timeout : typing.Optional[float] + Timeout in seconds for the download operation + + max_steps_per_run : typing.Optional[int] + Maximum number of steps to execute + + parameter_keys : typing.Optional[typing.Sequence[str]] + List of parameter keys to use in the workflow + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + WorkflowRunResponse + Successful Response + + Examples + -------- + import asyncio + + from skyvern import AsyncSkyvern + + client = AsyncSkyvern( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.file_download( + navigation_goal="navigation_goal", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.file_download( + navigation_goal=navigation_goal, + url=url, + webhook_url=webhook_url, + proxy_location=proxy_location, + totp_identifier=totp_identifier, + totp_url=totp_url, + browser_session_id=browser_session_id, + browser_profile_id=browser_profile_id, + browser_address=browser_address, + extra_http_headers=extra_http_headers, + max_screenshot_scrolling_times=max_screenshot_scrolling_times, + download_suffix=download_suffix, + download_timeout=download_timeout, + max_steps_per_run=max_steps_per_run, + parameter_keys=parameter_keys, + request_options=request_options, + ) + return _response.data + async def get_scripts( self, *, diff --git a/skyvern/client/raw_client.py b/skyvern/client/raw_client.py index ddf11c77..59cc55aa 100644 --- a/skyvern/client/raw_client.py +++ b/skyvern/client/raw_client.py @@ -1951,7 +1951,6 @@ class RawSkyvern: *, credential_type: SkyvernSchemasRunBlocksCredentialType, url: typing.Optional[str] = OMIT, - prompt: typing.Optional[str] = OMIT, webhook_url: typing.Optional[str] = OMIT, proxy_location: typing.Optional[ProxyLocation] = OMIT, totp_identifier: typing.Optional[str] = OMIT, @@ -1961,6 +1960,7 @@ class RawSkyvern: browser_address: typing.Optional[str] = OMIT, extra_http_headers: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, max_screenshot_scrolling_times: typing.Optional[int] = OMIT, + prompt: typing.Optional[str] = OMIT, credential_id: typing.Optional[str] = OMIT, bitwarden_collection_id: typing.Optional[str] = OMIT, bitwarden_item_id: typing.Optional[str] = OMIT, @@ -1981,13 +1981,10 @@ class RawSkyvern: Where to get the credential from url : typing.Optional[str] - Website url - - prompt : typing.Optional[str] - Login instructions. Skyvern has default prompt/instruction for login if this field is not provided. + Website URL webhook_url : typing.Optional[str] - Webhook URL to send login status updates + Webhook URL to send status updates proxy_location : typing.Optional[ProxyLocation] Proxy location to use @@ -2013,6 +2010,9 @@ class RawSkyvern: max_screenshot_scrolling_times : typing.Optional[int] Maximum number of times to scroll for screenshots + prompt : typing.Optional[str] + Login instructions. Skyvern has default prompt/instruction for login if this field is not provided. + credential_id : typing.Optional[str] ID of the Skyvern credential to use for login. @@ -2052,9 +2052,7 @@ class RawSkyvern: "v1/run/tasks/login", method="POST", json={ - "credential_type": credential_type, "url": url, - "prompt": prompt, "webhook_url": webhook_url, "proxy_location": proxy_location, "totp_identifier": totp_identifier, @@ -2064,6 +2062,8 @@ class RawSkyvern: "browser_address": browser_address, "extra_http_headers": extra_http_headers, "max_screenshot_scrolling_times": max_screenshot_scrolling_times, + "credential_type": credential_type, + "prompt": prompt, "credential_id": credential_id, "bitwarden_collection_id": bitwarden_collection_id, "bitwarden_item_id": bitwarden_item_id, @@ -2106,6 +2106,136 @@ class RawSkyvern: raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + def file_download( + self, + *, + navigation_goal: str, + url: typing.Optional[str] = OMIT, + webhook_url: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + totp_identifier: typing.Optional[str] = OMIT, + totp_url: typing.Optional[str] = OMIT, + browser_session_id: typing.Optional[str] = OMIT, + browser_profile_id: typing.Optional[str] = OMIT, + browser_address: typing.Optional[str] = OMIT, + extra_http_headers: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + max_screenshot_scrolling_times: typing.Optional[int] = OMIT, + download_suffix: typing.Optional[str] = OMIT, + download_timeout: typing.Optional[float] = OMIT, + max_steps_per_run: typing.Optional[int] = OMIT, + parameter_keys: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[WorkflowRunResponse]: + """ + Download a file from a website by navigating and clicking download buttons + + Parameters + ---------- + navigation_goal : str + Instructions for navigating to and downloading the file + + url : typing.Optional[str] + Website URL + + webhook_url : typing.Optional[str] + Webhook URL to send status updates + + proxy_location : typing.Optional[ProxyLocation] + Proxy location to use + + totp_identifier : typing.Optional[str] + Identifier for TOTP (Time-based One-Time Password) if required + + totp_url : typing.Optional[str] + TOTP URL to fetch one-time passwords + + browser_session_id : typing.Optional[str] + ID of the browser session to use, which is prefixed by `pbs_` e.g. `pbs_123456` + + browser_profile_id : typing.Optional[str] + ID of a browser profile to reuse for this run + + browser_address : typing.Optional[str] + The CDP address for the task. + + extra_http_headers : typing.Optional[typing.Dict[str, typing.Optional[str]]] + Additional HTTP headers to include in requests + + max_screenshot_scrolling_times : typing.Optional[int] + Maximum number of times to scroll for screenshots + + download_suffix : typing.Optional[str] + Suffix or complete filename for the downloaded file + + download_timeout : typing.Optional[float] + Timeout in seconds for the download operation + + max_steps_per_run : typing.Optional[int] + Maximum number of steps to execute + + parameter_keys : typing.Optional[typing.Sequence[str]] + List of parameter keys to use in the workflow + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[WorkflowRunResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "v1/run/tasks/file_download", + method="POST", + json={ + "url": url, + "webhook_url": webhook_url, + "proxy_location": proxy_location, + "totp_identifier": totp_identifier, + "totp_url": totp_url, + "browser_session_id": browser_session_id, + "browser_profile_id": browser_profile_id, + "browser_address": browser_address, + "extra_http_headers": extra_http_headers, + "max_screenshot_scrolling_times": max_screenshot_scrolling_times, + "navigation_goal": navigation_goal, + "download_suffix": download_suffix, + "download_timeout": download_timeout, + "max_steps_per_run": max_steps_per_run, + "parameter_keys": parameter_keys, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + WorkflowRunResponse, + parse_obj_as( + type_=WorkflowRunResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + parse_obj_as( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + def get_scripts( self, *, @@ -4342,7 +4472,6 @@ class AsyncRawSkyvern: *, credential_type: SkyvernSchemasRunBlocksCredentialType, url: typing.Optional[str] = OMIT, - prompt: typing.Optional[str] = OMIT, webhook_url: typing.Optional[str] = OMIT, proxy_location: typing.Optional[ProxyLocation] = OMIT, totp_identifier: typing.Optional[str] = OMIT, @@ -4352,6 +4481,7 @@ class AsyncRawSkyvern: browser_address: typing.Optional[str] = OMIT, extra_http_headers: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, max_screenshot_scrolling_times: typing.Optional[int] = OMIT, + prompt: typing.Optional[str] = OMIT, credential_id: typing.Optional[str] = OMIT, bitwarden_collection_id: typing.Optional[str] = OMIT, bitwarden_item_id: typing.Optional[str] = OMIT, @@ -4372,13 +4502,10 @@ class AsyncRawSkyvern: Where to get the credential from url : typing.Optional[str] - Website url - - prompt : typing.Optional[str] - Login instructions. Skyvern has default prompt/instruction for login if this field is not provided. + Website URL webhook_url : typing.Optional[str] - Webhook URL to send login status updates + Webhook URL to send status updates proxy_location : typing.Optional[ProxyLocation] Proxy location to use @@ -4404,6 +4531,9 @@ class AsyncRawSkyvern: max_screenshot_scrolling_times : typing.Optional[int] Maximum number of times to scroll for screenshots + prompt : typing.Optional[str] + Login instructions. Skyvern has default prompt/instruction for login if this field is not provided. + credential_id : typing.Optional[str] ID of the Skyvern credential to use for login. @@ -4443,9 +4573,7 @@ class AsyncRawSkyvern: "v1/run/tasks/login", method="POST", json={ - "credential_type": credential_type, "url": url, - "prompt": prompt, "webhook_url": webhook_url, "proxy_location": proxy_location, "totp_identifier": totp_identifier, @@ -4455,6 +4583,8 @@ class AsyncRawSkyvern: "browser_address": browser_address, "extra_http_headers": extra_http_headers, "max_screenshot_scrolling_times": max_screenshot_scrolling_times, + "credential_type": credential_type, + "prompt": prompt, "credential_id": credential_id, "bitwarden_collection_id": bitwarden_collection_id, "bitwarden_item_id": bitwarden_item_id, @@ -4497,6 +4627,136 @@ class AsyncRawSkyvern: raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + async def file_download( + self, + *, + navigation_goal: str, + url: typing.Optional[str] = OMIT, + webhook_url: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + totp_identifier: typing.Optional[str] = OMIT, + totp_url: typing.Optional[str] = OMIT, + browser_session_id: typing.Optional[str] = OMIT, + browser_profile_id: typing.Optional[str] = OMIT, + browser_address: typing.Optional[str] = OMIT, + extra_http_headers: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + max_screenshot_scrolling_times: typing.Optional[int] = OMIT, + download_suffix: typing.Optional[str] = OMIT, + download_timeout: typing.Optional[float] = OMIT, + max_steps_per_run: typing.Optional[int] = OMIT, + parameter_keys: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[WorkflowRunResponse]: + """ + Download a file from a website by navigating and clicking download buttons + + Parameters + ---------- + navigation_goal : str + Instructions for navigating to and downloading the file + + url : typing.Optional[str] + Website URL + + webhook_url : typing.Optional[str] + Webhook URL to send status updates + + proxy_location : typing.Optional[ProxyLocation] + Proxy location to use + + totp_identifier : typing.Optional[str] + Identifier for TOTP (Time-based One-Time Password) if required + + totp_url : typing.Optional[str] + TOTP URL to fetch one-time passwords + + browser_session_id : typing.Optional[str] + ID of the browser session to use, which is prefixed by `pbs_` e.g. `pbs_123456` + + browser_profile_id : typing.Optional[str] + ID of a browser profile to reuse for this run + + browser_address : typing.Optional[str] + The CDP address for the task. + + extra_http_headers : typing.Optional[typing.Dict[str, typing.Optional[str]]] + Additional HTTP headers to include in requests + + max_screenshot_scrolling_times : typing.Optional[int] + Maximum number of times to scroll for screenshots + + download_suffix : typing.Optional[str] + Suffix or complete filename for the downloaded file + + download_timeout : typing.Optional[float] + Timeout in seconds for the download operation + + max_steps_per_run : typing.Optional[int] + Maximum number of steps to execute + + parameter_keys : typing.Optional[typing.Sequence[str]] + List of parameter keys to use in the workflow + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[WorkflowRunResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/run/tasks/file_download", + method="POST", + json={ + "url": url, + "webhook_url": webhook_url, + "proxy_location": proxy_location, + "totp_identifier": totp_identifier, + "totp_url": totp_url, + "browser_session_id": browser_session_id, + "browser_profile_id": browser_profile_id, + "browser_address": browser_address, + "extra_http_headers": extra_http_headers, + "max_screenshot_scrolling_times": max_screenshot_scrolling_times, + "navigation_goal": navigation_goal, + "download_suffix": download_suffix, + "download_timeout": download_timeout, + "max_steps_per_run": max_steps_per_run, + "parameter_keys": parameter_keys, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + WorkflowRunResponse, + parse_obj_as( + type_=WorkflowRunResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + parse_obj_as( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + async def get_scripts( self, *, diff --git a/skyvern/client/types/__init__.py b/skyvern/client/types/__init__.py index d82e0f68..7d92efbd 100644 --- a/skyvern/client/types/__init__.py +++ b/skyvern/client/types/__init__.py @@ -43,7 +43,9 @@ if typing.TYPE_CHECKING: from .bitwarden_sensitive_information_parameter_yaml import BitwardenSensitiveInformationParameterYaml from .block_type import BlockType from .branch_condition import BranchCondition - from .branch_criteria import BranchCriteria + from .branch_condition_criteria import BranchConditionCriteria, BranchConditionCriteria_Jinja2Template + from .branch_condition_yaml import BranchConditionYaml + from .branch_criteria_yaml import BranchCriteriaYaml from .browser_profile import BrowserProfile from .browser_session_response import BrowserSessionResponse from .click_action import ClickAction @@ -66,6 +68,7 @@ if typing.TYPE_CHECKING: ) from .code_block_yaml import CodeBlockYaml from .conditional_block import ConditionalBlock + from .conditional_block_yaml import ConditionalBlockYaml from .context_parameter import ContextParameter from .context_parameter_source import ( ContextParameterSource, @@ -186,6 +189,7 @@ if typing.TYPE_CHECKING: ForLoopBlockYamlLoopBlocksItem, ForLoopBlockYamlLoopBlocksItem_Action, ForLoopBlockYamlLoopBlocksItem_Code, + ForLoopBlockYamlLoopBlocksItem_Conditional, ForLoopBlockYamlLoopBlocksItem_DownloadToS3, ForLoopBlockYamlLoopBlocksItem_Extraction, ForLoopBlockYamlLoopBlocksItem_FileDownload, @@ -253,6 +257,7 @@ if typing.TYPE_CHECKING: from .input_or_select_context import InputOrSelectContext from .input_text_action import InputTextAction from .input_text_action_data import InputTextActionData + from .jinja_branch_criteria import JinjaBranchCriteria from .locate_element_action import LocateElementAction from .login_block import LoginBlock from .login_block_data_schema import LoginBlockDataSchema @@ -473,6 +478,7 @@ if typing.TYPE_CHECKING: WorkflowDefinitionYamlBlocksItem, WorkflowDefinitionYamlBlocksItem_Action, WorkflowDefinitionYamlBlocksItem_Code, + WorkflowDefinitionYamlBlocksItem_Conditional, WorkflowDefinitionYamlBlocksItem_DownloadToS3, WorkflowDefinitionYamlBlocksItem_Extraction, WorkflowDefinitionYamlBlocksItem_FileDownload, @@ -560,7 +566,10 @@ _dynamic_imports: typing.Dict[str, str] = { "BitwardenSensitiveInformationParameterYaml": ".bitwarden_sensitive_information_parameter_yaml", "BlockType": ".block_type", "BranchCondition": ".branch_condition", - "BranchCriteria": ".branch_criteria", + "BranchConditionCriteria": ".branch_condition_criteria", + "BranchConditionCriteria_Jinja2Template": ".branch_condition_criteria", + "BranchConditionYaml": ".branch_condition_yaml", + "BranchCriteriaYaml": ".branch_criteria_yaml", "BrowserProfile": ".browser_profile", "BrowserSessionResponse": ".browser_session_response", "ClickAction": ".click_action", @@ -581,6 +590,7 @@ _dynamic_imports: typing.Dict[str, str] = { "CodeBlockParametersItem_Workflow": ".code_block_parameters_item", "CodeBlockYaml": ".code_block_yaml", "ConditionalBlock": ".conditional_block", + "ConditionalBlockYaml": ".conditional_block_yaml", "ContextParameter": ".context_parameter", "ContextParameterSource": ".context_parameter_source", "ContextParameterSource_AwsSecret": ".context_parameter_source", @@ -690,6 +700,7 @@ _dynamic_imports: typing.Dict[str, str] = { "ForLoopBlockYamlLoopBlocksItem": ".for_loop_block_yaml_loop_blocks_item", "ForLoopBlockYamlLoopBlocksItem_Action": ".for_loop_block_yaml_loop_blocks_item", "ForLoopBlockYamlLoopBlocksItem_Code": ".for_loop_block_yaml_loop_blocks_item", + "ForLoopBlockYamlLoopBlocksItem_Conditional": ".for_loop_block_yaml_loop_blocks_item", "ForLoopBlockYamlLoopBlocksItem_DownloadToS3": ".for_loop_block_yaml_loop_blocks_item", "ForLoopBlockYamlLoopBlocksItem_Extraction": ".for_loop_block_yaml_loop_blocks_item", "ForLoopBlockYamlLoopBlocksItem_FileDownload": ".for_loop_block_yaml_loop_blocks_item", @@ -750,6 +761,7 @@ _dynamic_imports: typing.Dict[str, str] = { "InputOrSelectContext": ".input_or_select_context", "InputTextAction": ".input_text_action", "InputTextActionData": ".input_text_action_data", + "JinjaBranchCriteria": ".jinja_branch_criteria", "LocateElementAction": ".locate_element_action", "LoginBlock": ".login_block", "LoginBlockDataSchema": ".login_block_data_schema", @@ -949,6 +961,7 @@ _dynamic_imports: typing.Dict[str, str] = { "WorkflowDefinitionYamlBlocksItem": ".workflow_definition_yaml_blocks_item", "WorkflowDefinitionYamlBlocksItem_Action": ".workflow_definition_yaml_blocks_item", "WorkflowDefinitionYamlBlocksItem_Code": ".workflow_definition_yaml_blocks_item", + "WorkflowDefinitionYamlBlocksItem_Conditional": ".workflow_definition_yaml_blocks_item", "WorkflowDefinitionYamlBlocksItem_DownloadToS3": ".workflow_definition_yaml_blocks_item", "WorkflowDefinitionYamlBlocksItem_Extraction": ".workflow_definition_yaml_blocks_item", "WorkflowDefinitionYamlBlocksItem_FileDownload": ".workflow_definition_yaml_blocks_item", @@ -1057,7 +1070,10 @@ __all__ = [ "BitwardenSensitiveInformationParameterYaml", "BlockType", "BranchCondition", - "BranchCriteria", + "BranchConditionCriteria", + "BranchConditionCriteria_Jinja2Template", + "BranchConditionYaml", + "BranchCriteriaYaml", "BrowserProfile", "BrowserSessionResponse", "ClickAction", @@ -1078,6 +1094,7 @@ __all__ = [ "CodeBlockParametersItem_Workflow", "CodeBlockYaml", "ConditionalBlock", + "ConditionalBlockYaml", "ContextParameter", "ContextParameterSource", "ContextParameterSource_AwsSecret", @@ -1187,6 +1204,7 @@ __all__ = [ "ForLoopBlockYamlLoopBlocksItem", "ForLoopBlockYamlLoopBlocksItem_Action", "ForLoopBlockYamlLoopBlocksItem_Code", + "ForLoopBlockYamlLoopBlocksItem_Conditional", "ForLoopBlockYamlLoopBlocksItem_DownloadToS3", "ForLoopBlockYamlLoopBlocksItem_Extraction", "ForLoopBlockYamlLoopBlocksItem_FileDownload", @@ -1247,6 +1265,7 @@ __all__ = [ "InputOrSelectContext", "InputTextAction", "InputTextActionData", + "JinjaBranchCriteria", "LocateElementAction", "LoginBlock", "LoginBlockDataSchema", @@ -1446,6 +1465,7 @@ __all__ = [ "WorkflowDefinitionYamlBlocksItem", "WorkflowDefinitionYamlBlocksItem_Action", "WorkflowDefinitionYamlBlocksItem_Code", + "WorkflowDefinitionYamlBlocksItem_Conditional", "WorkflowDefinitionYamlBlocksItem_DownloadToS3", "WorkflowDefinitionYamlBlocksItem_Extraction", "WorkflowDefinitionYamlBlocksItem_FileDownload", diff --git a/skyvern/client/types/branch_condition.py b/skyvern/client/types/branch_condition.py index a40e98b9..bb3ac3ce 100644 --- a/skyvern/client/types/branch_condition.py +++ b/skyvern/client/types/branch_condition.py @@ -4,7 +4,7 @@ import typing import pydantic from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .branch_criteria import BranchCriteria +from .branch_condition_criteria import BranchConditionCriteria class BranchCondition(UniversalBaseModel): @@ -12,7 +12,7 @@ class BranchCondition(UniversalBaseModel): Represents a single conditional branch edge within a ConditionalBlock. """ - criteria: typing.Optional[BranchCriteria] = None + criteria: typing.Optional[BranchConditionCriteria] = None next_block_label: typing.Optional[str] = None description: typing.Optional[str] = None is_default: typing.Optional[bool] = None diff --git a/skyvern/client/types/branch_condition_criteria.py b/skyvern/client/types/branch_condition_criteria.py new file mode 100644 index 00000000..eca58d11 --- /dev/null +++ b/skyvern/client/types/branch_condition_criteria.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class BranchConditionCriteria_Jinja2Template(UniversalBaseModel): + criteria_type: typing.Literal["jinja2_template"] = "jinja2_template" + expression: str + description: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +BranchConditionCriteria = BranchConditionCriteria_Jinja2Template diff --git a/skyvern/client/types/branch_condition_yaml.py b/skyvern/client/types/branch_condition_yaml.py new file mode 100644 index 00000000..82e1db9a --- /dev/null +++ b/skyvern/client/types/branch_condition_yaml.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .branch_criteria_yaml import BranchCriteriaYaml + + +class BranchConditionYaml(UniversalBaseModel): + criteria: typing.Optional[BranchCriteriaYaml] = None + next_block_label: typing.Optional[str] = None + description: typing.Optional[str] = None + is_default: typing.Optional[bool] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/skyvern/client/types/branch_criteria_yaml.py b/skyvern/client/types/branch_criteria_yaml.py new file mode 100644 index 00000000..83ebade4 --- /dev/null +++ b/skyvern/client/types/branch_criteria_yaml.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class BranchCriteriaYaml(UniversalBaseModel): + criteria_type: typing.Optional[typing.Literal["jinja2_template"]] = None + expression: str + description: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/skyvern/client/types/conditional_block_yaml.py b/skyvern/client/types/conditional_block_yaml.py new file mode 100644 index 00000000..39723736 --- /dev/null +++ b/skyvern/client/types/conditional_block_yaml.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .branch_condition_yaml import BranchConditionYaml + + +class ConditionalBlockYaml(UniversalBaseModel): + label: str = pydantic.Field() + """ + Author-facing identifier; must be unique per workflow. + """ + + next_block_label: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional pointer to the label of the next block. When omitted, it will default to sequential order. See [[s-4bnl]]. + """ + + continue_on_failure: typing.Optional[bool] = None + model: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = None + branch_conditions: typing.Optional[typing.List[BranchConditionYaml]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/skyvern/client/types/for_loop_block_yaml_loop_blocks_item.py b/skyvern/client/types/for_loop_block_yaml_loop_blocks_item.py index 4ac61204..e3ff79bb 100644 --- a/skyvern/client/types/for_loop_block_yaml_loop_blocks_item.py +++ b/skyvern/client/types/for_loop_block_yaml_loop_blocks_item.py @@ -8,6 +8,7 @@ import pydantic import typing_extensions from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel, update_forward_refs from ..core.serialization import FieldMetadata +from .branch_condition_yaml import BranchConditionYaml from .extraction_block_yaml_data_schema import ExtractionBlockYamlDataSchema from .file_storage_type import FileStorageType from .file_type import FileType @@ -524,6 +525,24 @@ class ForLoopBlockYamlLoopBlocksItem_HttpRequest(UniversalBaseModel): extra = pydantic.Extra.allow +class ForLoopBlockYamlLoopBlocksItem_Conditional(UniversalBaseModel): + block_type: typing.Literal["conditional"] = "conditional" + label: str + next_block_label: typing.Optional[str] = None + continue_on_failure: typing.Optional[bool] = None + model: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = None + branch_conditions: typing.Optional[typing.List[BranchConditionYaml]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + ForLoopBlockYamlLoopBlocksItem = typing.Union[ ForLoopBlockYamlLoopBlocksItem_Task, ForLoopBlockYamlLoopBlocksItem_ForLoop, @@ -546,5 +565,6 @@ ForLoopBlockYamlLoopBlocksItem = typing.Union[ ForLoopBlockYamlLoopBlocksItem_PdfParser, ForLoopBlockYamlLoopBlocksItem_TaskV2, ForLoopBlockYamlLoopBlocksItem_HttpRequest, + ForLoopBlockYamlLoopBlocksItem_Conditional, ] update_forward_refs(ForLoopBlockYamlLoopBlocksItem_ForLoop) diff --git a/skyvern/client/types/branch_criteria.py b/skyvern/client/types/jinja_branch_criteria.py similarity index 79% rename from skyvern/client/types/branch_criteria.py rename to skyvern/client/types/jinja_branch_criteria.py index 89c5e59e..fc5ced78 100644 --- a/skyvern/client/types/branch_criteria.py +++ b/skyvern/client/types/jinja_branch_criteria.py @@ -6,12 +6,11 @@ import pydantic from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -class BranchCriteria(UniversalBaseModel): +class JinjaBranchCriteria(UniversalBaseModel): """ - Abstract interface describing how a branch condition should be evaluated. + Jinja2-templated branch criteria (only supported criteria type for now). """ - criteria_type: str expression: str description: typing.Optional[str] = None diff --git a/skyvern/client/types/workflow_definition_yaml_blocks_item.py b/skyvern/client/types/workflow_definition_yaml_blocks_item.py index 2b0b5b8b..0d803b08 100644 --- a/skyvern/client/types/workflow_definition_yaml_blocks_item.py +++ b/skyvern/client/types/workflow_definition_yaml_blocks_item.py @@ -8,6 +8,7 @@ import pydantic import typing_extensions from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel, update_forward_refs from ..core.serialization import FieldMetadata +from .branch_condition_yaml import BranchConditionYaml from .extraction_block_yaml_data_schema import ExtractionBlockYamlDataSchema from .file_storage_type import FileStorageType from .file_type import FileType @@ -63,6 +64,24 @@ class WorkflowDefinitionYamlBlocksItem_Code(UniversalBaseModel): extra = pydantic.Extra.allow +class WorkflowDefinitionYamlBlocksItem_Conditional(UniversalBaseModel): + block_type: typing.Literal["conditional"] = "conditional" + label: str + next_block_label: typing.Optional[str] = None + continue_on_failure: typing.Optional[bool] = None + model: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = None + branch_conditions: typing.Optional[typing.List[BranchConditionYaml]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + class WorkflowDefinitionYamlBlocksItem_DownloadToS3(UniversalBaseModel): block_type: typing.Literal["download_to_s3"] = "download_to_s3" label: str @@ -528,6 +547,7 @@ class WorkflowDefinitionYamlBlocksItem_Wait(UniversalBaseModel): WorkflowDefinitionYamlBlocksItem = typing.Union[ WorkflowDefinitionYamlBlocksItem_Action, WorkflowDefinitionYamlBlocksItem_Code, + WorkflowDefinitionYamlBlocksItem_Conditional, WorkflowDefinitionYamlBlocksItem_DownloadToS3, WorkflowDefinitionYamlBlocksItem_Extraction, WorkflowDefinitionYamlBlocksItem_FileDownload, diff --git a/skyvern/forge/sdk/routes/code_samples.py b/skyvern/forge/sdk/routes/code_samples.py index 6caed929..48216118 100644 --- a/skyvern/forge/sdk/routes/code_samples.py +++ b/skyvern/forge/sdk/routes/code_samples.py @@ -160,6 +160,24 @@ await skyvern.login({ onepassword_item_id: "1PASSWORD ITEM ID" }); """ +FILE_DOWNLOAD_CODE_SAMPLE_PYTHON = """from skyvern import Skyvern + +skyvern = Skyvern(api_key="YOUR_API_KEY") +await skyvern.file_download( + url="https://example.com/downloads", + navigation_goal="Navigate to the downloads page and click the 'Download PDF' button", + download_suffix="report.pdf" +) +""" +FILE_DOWNLOAD_CODE_SAMPLE_TS = """import { SkyvernClient } from "@skyvern/client"; + +const skyvern = new SkyvernClient({ apiKey: "YOUR_API_KEY" }); +await skyvern.fileDownload({ + url: "https://example.com/downloads", + navigation_goal: "Navigate to the downloads page and click the 'Download PDF' button", + download_suffix: "report.pdf" +}); +""" # Workflows CREATE_WORKFLOW_CODE_SAMPLE_CURL = """curl -X POST https://api.skyvern.com/v1/workflows \ diff --git a/skyvern/forge/sdk/routes/run_blocks.py b/skyvern/forge/sdk/routes/run_blocks.py index c2a94af8..035546ed 100644 --- a/skyvern/forge/sdk/routes/run_blocks.py +++ b/skyvern/forge/sdk/routes/run_blocks.py @@ -8,6 +8,8 @@ from skyvern.exceptions import MissingBrowserAddressError from skyvern.forge import app from skyvern.forge.sdk.core import skyvern_context from skyvern.forge.sdk.routes.code_samples import ( + FILE_DOWNLOAD_CODE_SAMPLE_PYTHON, + FILE_DOWNLOAD_CODE_SAMPLE_TS, LOGIN_CODE_SAMPLE_BITWARDEN_PYTHON, LOGIN_CODE_SAMPLE_BITWARDEN_TS, LOGIN_CODE_SAMPLE_ONEPASSWORD_PYTHON, @@ -19,12 +21,13 @@ from skyvern.forge.sdk.routes.routers import base_router from skyvern.forge.sdk.schemas.organizations import Organization from skyvern.forge.sdk.services import org_auth_service from skyvern.forge.sdk.workflow.models.parameter import WorkflowParameterType -from skyvern.forge.sdk.workflow.models.workflow import WorkflowRequestBody -from skyvern.schemas.run_blocks import CredentialType, LoginRequest +from skyvern.forge.sdk.workflow.models.workflow import Workflow, WorkflowRequestBody +from skyvern.schemas.run_blocks import BaseRunBlockRequest, CredentialType, FileDownloadRequest, LoginRequest from skyvern.schemas.runs import ProxyLocation, RunType, WorkflowRunRequest, WorkflowRunResponse from skyvern.schemas.workflows import ( AzureVaultCredentialParameterYAML, BitwardenLoginCredentialParameterYAML, + FileDownloadBlockYAML, LoginBlockYAML, OnePasswordCredentialParameterYAML, WorkflowCreateYAMLRequest, @@ -43,6 +46,80 @@ If you fail to login to find the login page or can't login after several trials, If login is completed, you're successful.""" +def _validate_url(url: str | None) -> str | None: + if not url: + return None + try: + return prepend_scheme_and_validate_url(url) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + +async def _run_workflow_and_build_response( + request: Request, + background_tasks: BackgroundTasks, + new_workflow: Workflow, + workflow_id: str, + organization: Organization, + run_block_request: BaseRunBlockRequest, + webhook_url: str | None, + totp_verification_url: str | None, + totp_identifier: str | None, + x_api_key: str | None, +) -> WorkflowRunResponse: + context = skyvern_context.ensure_context() + request_id = context.request_id + legacy_workflow_request = WorkflowRequestBody( + proxy_location=run_block_request.proxy_location, + webhook_callback_url=webhook_url, + totp_identifier=totp_identifier, + totp_verification_url=totp_verification_url, + browser_session_id=run_block_request.browser_session_id, + browser_profile_id=run_block_request.browser_profile_id, + browser_address=run_block_request.browser_address, + max_screenshot_scrolls=run_block_request.max_screenshot_scrolling_times, + extra_http_headers=run_block_request.extra_http_headers, + ) + + try: + workflow_run = await workflow_service.run_workflow( + workflow_id=workflow_id, + organization=organization, + workflow_request=legacy_workflow_request, + template=False, + version=None, + api_key=x_api_key or None, + request_id=request_id, + request=request, + background_tasks=background_tasks, + ) + except MissingBrowserAddressError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + return WorkflowRunResponse( + run_id=workflow_run.workflow_run_id, + run_type=RunType.workflow_run, + status=str(workflow_run.status), + failure_reason=workflow_run.failure_reason, + created_at=workflow_run.created_at, + modified_at=workflow_run.modified_at, + run_request=WorkflowRunRequest( + workflow_id=new_workflow.workflow_id, + title=new_workflow.title, + proxy_location=run_block_request.proxy_location, + webhook_url=webhook_url, + totp_url=totp_verification_url, + totp_identifier=totp_identifier, + browser_session_id=run_block_request.browser_session_id, + browser_profile_id=run_block_request.browser_profile_id, + max_screenshot_scrolls=run_block_request.max_screenshot_scrolling_times, + ), + app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{workflow_run.workflow_run_id}", + browser_session_id=run_block_request.browser_session_id, + browser_profile_id=run_block_request.browser_profile_id, + ) + + @base_router.post( "/run/tasks/login", tags=["Agent"], @@ -72,14 +149,9 @@ async def login( organization: Organization = Depends(org_auth_service.get_current_org), x_api_key: Annotated[str | None, Header()] = None, ) -> WorkflowRunResponse: - try: - url = prepend_scheme_and_validate_url(login_request.url) if login_request.url else None - totp_verification_url = ( - prepend_scheme_and_validate_url(login_request.totp_url) if login_request.totp_url else None - ) - webhook_url = prepend_scheme_and_validate_url(login_request.webhook_url) if login_request.webhook_url else None - except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) from e + url = _validate_url(login_request.url) + totp_verification_url = _validate_url(login_request.totp_url) + webhook_url = _validate_url(login_request.webhook_url) # 1. create empty workflow with a credential parameter new_workflow = await app.WORKFLOW_SERVICE.create_empty_workflow( @@ -196,54 +268,104 @@ async def login( # 3. create and run workflow with the credential workflow_id = new_workflow.workflow_permanent_id - context = skyvern_context.ensure_context() - request_id = context.request_id - legacy_workflow_request = WorkflowRequestBody( - proxy_location=login_request.proxy_location, - webhook_callback_url=webhook_url, - totp_identifier=resolved_totp_identifier, + return await _run_workflow_and_build_response( + request=request, + background_tasks=background_tasks, + new_workflow=new_workflow, + workflow_id=workflow_id, + organization=organization, + run_block_request=login_request, + webhook_url=webhook_url, totp_verification_url=totp_verification_url, - browser_session_id=login_request.browser_session_id, - browser_profile_id=login_request.browser_profile_id, - browser_address=login_request.browser_address, - max_screenshot_scrolls=login_request.max_screenshot_scrolling_times, - extra_http_headers=login_request.extra_http_headers, + totp_identifier=resolved_totp_identifier, + x_api_key=x_api_key, ) - try: - workflow_run = await workflow_service.run_workflow( - workflow_id=workflow_id, - organization=organization, - workflow_request=legacy_workflow_request, - template=False, - version=None, - api_key=x_api_key or None, - request_id=request_id, - request=request, - background_tasks=background_tasks, - ) - except MissingBrowserAddressError as e: - raise HTTPException(status_code=400, detail=str(e)) from e - return WorkflowRunResponse( - run_id=workflow_run.workflow_run_id, - run_type=RunType.workflow_run, - status=str(workflow_run.status), - failure_reason=workflow_run.failure_reason, - created_at=workflow_run.created_at, - modified_at=workflow_run.modified_at, - run_request=WorkflowRunRequest( - workflow_id=new_workflow.workflow_id, - title=new_workflow.title, - proxy_location=login_request.proxy_location, - webhook_url=webhook_url, - totp_url=totp_verification_url, - totp_identifier=resolved_totp_identifier, - browser_session_id=login_request.browser_session_id, - browser_profile_id=login_request.browser_profile_id, - max_screenshot_scrolls=login_request.max_screenshot_scrolling_times, - ), - app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{workflow_run.workflow_run_id}", - browser_session_id=login_request.browser_session_id, - browser_profile_id=login_request.browser_profile_id, +@base_router.post( + "/run/tasks/file_download", + tags=["Agent"], + response_model=WorkflowRunResponse, + openapi_extra={ + "x-fern-sdk-method-name": "file_download", + "x-fern-examples": [ + { + "code-samples": [ + {"sdk": "python", "code": FILE_DOWNLOAD_CODE_SAMPLE_PYTHON}, + {"sdk": "typescript", "code": FILE_DOWNLOAD_CODE_SAMPLE_TS}, + ] + } + ], + }, + description="Download a file from a website by navigating and clicking download buttons", + summary="File Download Task", +) +async def file_download( + request: Request, + background_tasks: BackgroundTasks, + file_download_request: FileDownloadRequest, + organization: Organization = Depends(org_auth_service.get_current_org), + x_api_key: Annotated[str | None, Header()] = None, +) -> WorkflowRunResponse: + url = _validate_url(file_download_request.url) + totp_verification_url = _validate_url(file_download_request.totp_url) + webhook_url = _validate_url(file_download_request.webhook_url) + + # 1. create empty workflow + new_workflow = await app.WORKFLOW_SERVICE.create_empty_workflow( + organization, + "File Download", + proxy_location=file_download_request.proxy_location, + max_screenshot_scrolling_times=file_download_request.max_screenshot_scrolling_times, + extra_http_headers=file_download_request.extra_http_headers, + status=WorkflowStatus.auto_generated, + ) + + # 2. add a file download block to the workflow + label = "file_download" + file_download_block_yaml = FileDownloadBlockYAML( + label=label, + title=label, + url=url, + navigation_goal=file_download_request.navigation_goal, + max_steps_per_run=file_download_request.max_steps_per_run or 10, + parameter_keys=file_download_request.parameter_keys or [], + totp_verification_url=totp_verification_url, + totp_identifier=file_download_request.totp_identifier, + download_suffix=file_download_request.download_suffix, + download_timeout=file_download_request.download_timeout, + ) + yaml_blocks = [file_download_block_yaml] + workflow_definition_yaml = WorkflowDefinitionYAML( + parameters=[], + blocks=yaml_blocks, + ) + workflow_create_request = WorkflowCreateYAMLRequest( + title=new_workflow.title, + description=new_workflow.description, + proxy_location=file_download_request.proxy_location or ProxyLocation.RESIDENTIAL, + workflow_definition=workflow_definition_yaml, + status=new_workflow.status, + max_screenshot_scrolls=file_download_request.max_screenshot_scrolling_times, + ) + workflow = await app.WORKFLOW_SERVICE.create_workflow_from_request( + organization=organization, + request=workflow_create_request, + workflow_permanent_id=new_workflow.workflow_permanent_id, + ) + LOG.info("Workflow created", workflow_id=workflow.workflow_id) + + # 3. create and run workflow + workflow_id = new_workflow.workflow_permanent_id + return await _run_workflow_and_build_response( + request=request, + background_tasks=background_tasks, + new_workflow=new_workflow, + workflow_id=workflow_id, + organization=organization, + run_block_request=file_download_request, + webhook_url=webhook_url, + totp_verification_url=totp_verification_url, + totp_identifier=file_download_request.totp_identifier, + x_api_key=x_api_key, ) diff --git a/skyvern/library/skyvern_browser_page.py b/skyvern/library/skyvern_browser_page.py index 27c50776..e9996fb5 100644 --- a/skyvern/library/skyvern_browser_page.py +++ b/skyvern/library/skyvern_browser_page.py @@ -157,6 +157,63 @@ class SkyvernPageRun: LOG.info("AI login workflow finished", run_id=workflow_run.run_id, status=workflow_run.status) return WorkflowRunResponse.model_validate(workflow_run.model_dump()) + async def file_download( + self, + navigation_goal: str, + *, + url: str | None = None, + download_suffix: str | None = None, + download_timeout: float | None = None, + max_steps_per_run: int | None = None, + parameter_keys: list[str] | None = None, + webhook_url: str | None = None, + totp_identifier: str | None = None, + totp_url: str | None = None, + extra_http_headers: dict[str, str] | None = None, + timeout: float = DEFAULT_AGENT_TIMEOUT, + ) -> WorkflowRunResponse: + """Run a file download task in the context of this page and wait for it to finish. + + Args: + navigation_goal: Instructions for navigating to and downloading the file. + url: URL to navigate to for file download. If not provided, uses the current page URL. + download_suffix: Suffix or complete filename for the downloaded file. + download_timeout: Timeout in seconds for the download operation. + max_steps_per_run: Maximum number of steps to execute. + parameter_keys: List of parameter keys to use in the workflow. + webhook_url: URL to receive webhook notifications about download progress. + totp_identifier: Identifier for TOTP authentication. + totp_url: URL to fetch TOTP codes from. + extra_http_headers: Additional HTTP headers to include in requests. + timeout: Maximum time in seconds to wait for download completion. + + Returns: + WorkflowRunResponse containing the file download workflow execution results. + """ + + LOG.info("Starting AI file download workflow", navigation_goal=navigation_goal) + + workflow_run = await self._browser.skyvern.file_download( + navigation_goal=navigation_goal, + url=url or self._get_page_url(), + download_suffix=download_suffix, + download_timeout=download_timeout, + max_steps_per_run=max_steps_per_run, + parameter_keys=parameter_keys, + webhook_url=webhook_url, + totp_identifier=totp_identifier, + totp_url=totp_url, + browser_session_id=self._browser.browser_session_id, + browser_address=self._browser.browser_address, + extra_http_headers=extra_http_headers, + request_options=RequestOptions(additional_headers={"X-User-Agent": "skyvern-sdk"}), + ) + LOG.info("AI file download workflow is running, this may take a while", run_id=workflow_run.run_id) + + workflow_run = await self._wait_for_run_completion(workflow_run.run_id, timeout) + LOG.info("AI file download workflow finished", run_id=workflow_run.run_id, status=workflow_run.status) + return WorkflowRunResponse.model_validate(workflow_run.model_dump()) + async def workflow( self, workflow_id: str, diff --git a/skyvern/schemas/run_blocks.py b/skyvern/schemas/run_blocks.py index 7a9904ff..066a650c 100644 --- a/skyvern/schemas/run_blocks.py +++ b/skyvern/schemas/run_blocks.py @@ -12,14 +12,11 @@ class CredentialType(StrEnum): azure_vault = "azure_vault" -class LoginRequest(BaseModel): - credential_type: CredentialType = Field(..., description="Where to get the credential from") - url: str | None = Field(default=None, description="Website url") - prompt: str | None = Field( - default=None, - description="Login instructions. Skyvern has default prompt/instruction for login if this field is not provided.", - ) - webhook_url: str | None = Field(default=None, description="Webhook URL to send login status updates") +class BaseRunBlockRequest(BaseModel): + """Base class for run block requests with common browser automation parameters""" + + url: str | None = Field(default=None, description="Website URL") + webhook_url: str | None = Field(default=None, description="Webhook URL to send status updates") proxy_location: ProxyLocation | None = Field(default=None, description="Proxy location to use") totp_identifier: str | None = Field( default=None, description="Identifier for TOTP (Time-based One-Time Password) if required" @@ -46,6 +43,14 @@ class LoginRequest(BaseModel): default=None, description="Maximum number of times to scroll for screenshots" ) + +class LoginRequest(BaseRunBlockRequest): + credential_type: CredentialType = Field(..., description="Where to get the credential from") + prompt: str | None = Field( + default=None, + description="Login instructions. Skyvern has default prompt/instruction for login if this field is not provided.", + ) + # Skyvern credential credential_id: str | None = Field( default=None, description="ID of the Skyvern credential to use for login.", examples=["cred_123"] @@ -67,3 +72,11 @@ class LoginRequest(BaseModel): azure_vault_username_key: str | None = Field(default=None, description="Azure Vault username key") azure_vault_password_key: str | None = Field(default=None, description="Azure Vault password key") azure_vault_totp_secret_key: str | None = Field(default=None, description="Azure Vault TOTP secret key") + + +class FileDownloadRequest(BaseRunBlockRequest): + navigation_goal: str = Field(..., description="Instructions for navigating to and downloading the file") + download_suffix: str | None = Field(default=None, description="Suffix or complete filename for the downloaded file") + download_timeout: float | None = Field(default=None, description="Timeout in seconds for the download operation") + max_steps_per_run: int | None = Field(default=None, description="Maximum number of steps to execute") + parameter_keys: list[str] | None = Field(default=None, description="List of parameter keys to use in the workflow")