From 0fceaac8c8430d8787b4970fdd1dbfe0599b40bf Mon Sep 17 00:00:00 2001 From: Shuchang Zheng Date: Thu, 4 Sep 2025 21:24:51 -0700 Subject: [PATCH] run script with browser session (#3368) --- .../core/script_generations/run_initializer.py | 7 +++++-- .../core/script_generations/skyvern_page.py | 14 +++++++++----- skyvern/forge/sdk/workflow/service.py | 9 ++++++++- skyvern/services/script_service.py | 18 +++++++++++++++--- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/skyvern/core/script_generations/run_initializer.py b/skyvern/core/script_generations/run_initializer.py index e8d2fb18..3976b85c 100644 --- a/skyvern/core/script_generations/run_initializer.py +++ b/skyvern/core/script_generations/run_initializer.py @@ -9,13 +9,16 @@ from skyvern.forge.sdk.workflow.models.parameter import WorkflowParameterType async def setup( - parameters: dict[str, Any], generated_parameter_cls: type[BaseModel] | None = None + parameters: dict[str, Any], + generated_parameter_cls: type[BaseModel] | None = None, + browser_session_id: str | None = None, ) -> tuple[SkyvernPage, RunContext]: # transform any secrets/credential parameters. For example, if there's only one credential in the parameters: {"cred_12345": "cred_12345"}, # it should be transformed to {"cred_12345": {"username": "secret_5fBoa_username", "password": "secret_5fBoa_password"}} # context comes from app.WORKFLOW_CONTEXT_MANAGER.get_workflow_run_context(workflow_run_id) context = skyvern_context.current() if context and context.organization_id and context.workflow_run_id: + browser_session_id = browser_session_id or context.browser_session_id workflow_run_context = app.WORKFLOW_CONTEXT_MANAGER.get_workflow_run_context(context.workflow_run_id) parameters_in_workflow_context = workflow_run_context.parameters for key in parameters: @@ -23,7 +26,7 @@ async def setup( parameter = parameters_in_workflow_context[key] if parameter.workflow_parameter_type == WorkflowParameterType.CREDENTIAL_ID: parameters[key] = workflow_run_context.values[key] - skyvern_page = await SkyvernPage.create() + skyvern_page = await SkyvernPage.create(browser_session_id=browser_session_id) run_context = RunContext( parameters=parameters, page=skyvern_page, diff --git a/skyvern/core/script_generations/skyvern_page.py b/skyvern/core/script_generations/skyvern_page.py index 2f9fc4ed..5b3d018c 100644 --- a/skyvern/core/script_generations/skyvern_page.py +++ b/skyvern/core/script_generations/skyvern_page.py @@ -71,7 +71,7 @@ class SkyvernPage: self._record = recorder or (lambda ac: None) @classmethod - async def _get_or_create_browser_state(cls) -> BrowserState: + async def _get_or_create_browser_state(cls, browser_session_id: str | None = None) -> BrowserState: context = skyvern_context.current() if context and context.workflow_run_id and context.organization_id: workflow_run = await app.DATABASE.get_workflow_run( @@ -79,12 +79,13 @@ class SkyvernPage: ) if workflow_run: browser_state = await app.BROWSER_MANAGER.get_or_create_for_workflow_run( - workflow_run=workflow_run, browser_session_id=None + workflow_run=workflow_run, + browser_session_id=browser_session_id, ) else: raise WorkflowRunNotFound(workflow_run_id=context.workflow_run_id) else: - browser_state = await app.BROWSER_MANAGER.get_or_create_for_script() + browser_state = await app.BROWSER_MANAGER.get_or_create_for_script(browser_session_id=browser_session_id) return browser_state @classmethod @@ -103,10 +104,13 @@ class SkyvernPage: return browser_state @classmethod - async def create(cls) -> SkyvernPage: + async def create( + cls, + browser_session_id: str | None = None, + ) -> SkyvernPage: # initialize browser state # TODO: add workflow_run_id or eventually script_id/script_run_id - browser_state = await cls._get_or_create_browser_state() + browser_state = await cls._get_or_create_browser_state(browser_session_id=browser_session_id) scraped_page = await scrape_website( browser_state=browser_state, url="", diff --git a/skyvern/forge/sdk/workflow/service.py b/skyvern/forge/sdk/workflow/service.py index b685ce21..bfad43ef 100644 --- a/skyvern/forge/sdk/workflow/service.py +++ b/skyvern/forge/sdk/workflow/service.py @@ -2438,7 +2438,13 @@ class WorkflowService: return None, rendered_cache_key_value async def _execute_workflow_script( - self, script_id: str, workflow: Workflow, workflow_run: WorkflowRun, api_key: str, organization: Organization + self, + script_id: str, + workflow: Workflow, + workflow_run: WorkflowRun, + api_key: str, + organization: Organization, + browser_session_id: str | None = None, ) -> WorkflowRun: """ Execute the related workflow script instead of running the workflow blocks. @@ -2458,6 +2464,7 @@ class WorkflowService: organization_id=organization.organization_id, parameters=parameters, workflow_run_id=workflow_run.workflow_run_id, + browser_session_id=browser_session_id, background_tasks=None, # Execute synchronously ) diff --git a/skyvern/services/script_service.py b/skyvern/services/script_service.py index ed963360..c39cce1f 100644 --- a/skyvern/services/script_service.py +++ b/skyvern/services/script_service.py @@ -167,6 +167,7 @@ async def execute_script( organization_id: str, parameters: dict[str, Any] | None = None, workflow_run_id: str | None = None, + browser_session_id: str | None = None, background_tasks: BackgroundTasks | None = None, ) -> None: # TODO: assume the script only has one ScriptFile called main.py @@ -229,17 +230,26 @@ async def execute_script( parameters = {wf_param.key: run_param.value for wf_param, run_param in parameter_tuples} LOG.info("Script run Parameters is using workflow run parameters", parameters=parameters) + script_path = os.path.join(script.script_id, "main.py") if background_tasks: # Execute asynchronously in background background_tasks.add_task( - run_script, parameters=parameters, organization_id=organization_id, workflow_run_id=workflow_run_id + run_script, + script_path, + parameters=parameters, + organization_id=organization_id, + workflow_run_id=workflow_run_id, + browser_session_id=browser_session_id, ) else: # Execute synchronously - script_path = os.path.join(script.script_id, "main.py") if os.path.exists(script_path): await run_script( - script_path, parameters=parameters, organization_id=organization_id, workflow_run_id=workflow_run_id + script_path, + parameters=parameters, + organization_id=organization_id, + workflow_run_id=workflow_run_id, + browser_session_id=browser_session_id, ) else: LOG.error("Script main.py not found", script_path=script_path, script_id=script_id) @@ -1252,12 +1262,14 @@ async def run_script( parameters: dict[str, Any] | None = None, organization_id: str | None = None, workflow_run_id: str | None = None, + browser_session_id: str | None = None, ) -> None: # register the script run context = skyvern_context.current() if not context: context = skyvern_context.ensure_context() skyvern_context.set(skyvern_context.SkyvernContext()) + context.browser_session_id = browser_session_id if workflow_run_id and organization_id: workflow_run = await app.DATABASE.get_workflow_run( workflow_run_id=workflow_run_id, organization_id=organization_id