diff --git a/skyvern/exceptions.py b/skyvern/exceptions.py index 48ade26a..40df493d 100644 --- a/skyvern/exceptions.py +++ b/skyvern/exceptions.py @@ -168,6 +168,17 @@ class WorkflowNotFound(SkyvernHTTPException): ) +class WorkflowNotFoundForWorkflowRun(SkyvernHTTPException): + def __init__( + self, + workflow_run_id: str | None = None, + ) -> None: + super().__init__( + f"Workflow not found for workflow run {workflow_run_id}", + status_code=status.HTTP_404_NOT_FOUND, + ) + + class WorkflowRunNotFound(SkyvernHTTPException): def __init__(self, workflow_run_id: str) -> None: super().__init__(f"WorkflowRun {workflow_run_id} not found", status_code=status.HTTP_404_NOT_FOUND) diff --git a/skyvern/forge/sdk/db/client.py b/skyvern/forge/sdk/db/client.py index d0584f09..515c2138 100644 --- a/skyvern/forge/sdk/db/client.py +++ b/skyvern/forge/sdk/db/client.py @@ -1494,6 +1494,34 @@ class AgentDB: LOG.error("SQLAlchemyError", exc_info=True) raise + async def get_workflow_for_workflow_run( + self, + workflow_run_id: str, + organization_id: str | None = None, + exclude_deleted: bool = True, + ) -> Workflow | None: + try: + get_workflow_query = select(WorkflowModel) + + if exclude_deleted: + get_workflow_query = get_workflow_query.filter(WorkflowModel.deleted_at.is_(None)) + if organization_id: + get_workflow_query = get_workflow_query.filter_by(organization_id=organization_id) + + get_workflow_query = get_workflow_query.join( + WorkflowRunModel, + WorkflowRunModel.workflow_id == WorkflowModel.workflow_id, + ) + + get_workflow_query = get_workflow_query.filter(WorkflowRunModel.workflow_run_id == workflow_run_id) + async with self.Session() as session: + if workflow := (await session.scalars(get_workflow_query)).first(): + return convert_to_workflow(workflow, self.debug_enabled) + return None + except SQLAlchemyError: + LOG.error("SQLAlchemyError", exc_info=True) + raise + async def get_workflow_versions_by_permanent_id( self, workflow_permanent_id: str, diff --git a/skyvern/forge/sdk/routes/agent_protocol.py b/skyvern/forge/sdk/routes/agent_protocol.py index 0b86abdf..cdbba6fc 100644 --- a/skyvern/forge/sdk/routes/agent_protocol.py +++ b/skyvern/forge/sdk/routes/agent_protocol.py @@ -81,6 +81,7 @@ from skyvern.forge.sdk.workflow.models.workflow import ( WorkflowRun, WorkflowRunResponseBase, WorkflowRunStatus, + WorkflowRunWithWorkflowResponse, ) from skyvern.schemas.artifacts import EntityType, entity_type_to_param from skyvern.schemas.runs import ( @@ -203,7 +204,7 @@ async def run_task( failure_reason=task_v1_response.failure_reason, created_at=task_v1_response.created_at, modified_at=task_v1_response.modified_at, - app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/tasks/{task_v1_response.task_id}", + app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{task_v1_response.task_id}", run_request=TaskRunRequest( engine=run_request.engine, prompt=task_v1_response.navigation_goal, @@ -266,7 +267,7 @@ async def run_task( failure_reason=None, created_at=task_v2.created_at, modified_at=task_v2.modified_at, - app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/{task_v2.workflow_permanent_id}/{task_v2.workflow_run_id}", + app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{task_v2.workflow_run_id}", run_request=TaskRunRequest( engine=RunEngine.skyvern_v2, prompt=task_v2.prompt, @@ -367,7 +368,7 @@ async def run_workflow( run_request=workflow_run_request, downloaded_files=None, recording_url=None, - app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/{workflow_run.workflow_permanent_id}/{workflow_run.workflow_run_id}", + app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{workflow_run.workflow_run_id}", run_with=workflow_run.run_with, ai_fallback=workflow_run.ai_fallback, ) @@ -1118,7 +1119,7 @@ async def run_block( run_request=block_run_request, downloaded_files=None, recording_url=None, - app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/{workflow_run.workflow_permanent_id}/{workflow_run.workflow_run_id}", + app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{workflow_run.workflow_run_id}", ) @@ -1808,6 +1809,42 @@ async def get_workflow_run_with_workflow_id( return return_dict +@base_router.get( + "/workflows/runs/{workflow_run_id}", + include_in_schema=False, +) +@base_router.get( + "/workflows/runs/{workflow_run_id}/", + include_in_schema=False, +) +async def get_workflow_and_run_from_workflow_run_id( + workflow_run_id: str, + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> WorkflowRunWithWorkflowResponse: + workflow = await app.WORKFLOW_SERVICE.get_workflow_by_workflow_run_id( + workflow_run_id=workflow_run_id, + organization_id=current_org.organization_id, + ) + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Workflow run not found {workflow_run_id}", + ) + + workflow_run_status_api_response = await get_workflow_run_with_workflow_id( + workflow_id=workflow.workflow_permanent_id, + workflow_run_id=workflow_run_id, + current_org=current_org, + ) + + workflow_run_status_api_response["workflow"] = workflow + + response = WorkflowRunWithWorkflowResponse.model_validate(workflow_run_status_api_response) + + return response + + @legacy_base_router.get( "/workflows/{workflow_id}/runs/{workflow_run_id}/timeline", tags=["agent"], diff --git a/skyvern/forge/sdk/routes/run_blocks.py b/skyvern/forge/sdk/routes/run_blocks.py index 917f260c..3299d8a5 100644 --- a/skyvern/forge/sdk/routes/run_blocks.py +++ b/skyvern/forge/sdk/routes/run_blocks.py @@ -238,6 +238,6 @@ async def login( browser_session_id=login_request.browser_session_id, max_screenshot_scrolls=login_request.max_screenshot_scrolling_times, ), - app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/{workflow_run.workflow_permanent_id}/{workflow_run.workflow_run_id}", + app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{workflow_run.workflow_run_id}", browser_session_id=login_request.browser_session_id, ) diff --git a/skyvern/forge/sdk/workflow/models/block.py b/skyvern/forge/sdk/workflow/models/block.py index 1cd99247..fadcb789 100644 --- a/skyvern/forge/sdk/workflow/models/block.py +++ b/skyvern/forge/sdk/workflow/models/block.py @@ -3187,8 +3187,7 @@ class HumanInteractionBlock(BaseTaskBlock): organization_id=organization_id, ) - workflow_permanent_id = workflow_run.workflow_permanent_id - app_url = f"{settings.SKYVERN_APP_URL}/workflows/{workflow_permanent_id}/{workflow_run_id}/overview" + app_url = f"{settings.SKYVERN_APP_URL}/runs/{workflow_run_id}/overview" body = f"{self.body}\n\nKindly visit {app_url}\n\n{self.instructions}\n\n" subject = f"{self.subject} - Workflow Run ID: {workflow_run_id}" diff --git a/skyvern/forge/sdk/workflow/models/workflow.py b/skyvern/forge/sdk/workflow/models/workflow.py index ddf72612..ed3d5b19 100644 --- a/skyvern/forge/sdk/workflow/models/workflow.py +++ b/skyvern/forge/sdk/workflow/models/workflow.py @@ -201,3 +201,7 @@ class WorkflowRunResponseBase(BaseModel): browser_address: str | None = None script_run: ScriptRunResponse | None = None errors: list[dict[str, Any]] | None = None + + +class WorkflowRunWithWorkflowResponse(WorkflowRunResponseBase): + workflow: Workflow diff --git a/skyvern/forge/sdk/workflow/service.py b/skyvern/forge/sdk/workflow/service.py index ebf38c3e..b5209f6d 100644 --- a/skyvern/forge/sdk/workflow/service.py +++ b/skyvern/forge/sdk/workflow/service.py @@ -26,6 +26,7 @@ from skyvern.exceptions import ( ScriptTerminationException, SkyvernException, WorkflowNotFound, + WorkflowNotFoundForWorkflowRun, WorkflowRunNotFound, ) from skyvern.forge import app @@ -1123,6 +1124,23 @@ class WorkflowService: ) return workflows + async def get_workflow_by_workflow_run_id( + self, + workflow_run_id: str, + organization_id: str | None = None, + exclude_deleted: bool = True, + ) -> Workflow: + workflow = await app.DATABASE.get_workflow_for_workflow_run( + workflow_run_id, + organization_id=organization_id, + exclude_deleted=exclude_deleted, + ) + + if not workflow: + raise WorkflowNotFoundForWorkflowRun(workflow_run_id=workflow_run_id) + + return workflow + async def get_block_outputs_for_debug_session( self, workflow_permanent_id: str, @@ -2049,10 +2067,8 @@ class WorkflowService: return # build new schema for backward compatible webhook payload - app_url = ( - f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/" - f"{workflow_run.workflow_permanent_id}/{workflow_run.workflow_run_id}" - ) + app_url = f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{workflow_run.workflow_run_id}" + workflow_run_response = WorkflowRunResponse( run_id=workflow_run.workflow_run_id, run_type=RunType.workflow_run, diff --git a/skyvern/services/task_v2_service.py b/skyvern/services/task_v2_service.py index 2a7084e9..edc7b0a1 100644 --- a/skyvern/services/task_v2_service.py +++ b/skyvern/services/task_v2_service.py @@ -1758,11 +1758,8 @@ async def build_task_v2_run_response(task_v2: TaskV2) -> TaskRunResponse: ) app_url = None - if task_v2.workflow_run_id and task_v2.workflow_permanent_id: - app_url = ( - f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/" - f"{task_v2.workflow_permanent_id}/{task_v2.workflow_run_id}" - ) + if task_v2.workflow_run_id: + app_url = f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{task_v2.workflow_run_id}" return TaskRunResponse( run_id=task_v2.observer_cruise_id, diff --git a/skyvern/services/webhook_service.py b/skyvern/services/webhook_service.py index 8854ca10..775f1c79 100644 --- a/skyvern/services/webhook_service.py +++ b/skyvern/services/webhook_service.py @@ -166,6 +166,8 @@ def build_sample_workflow_run_payload(run_id: str | None = None) -> dict: payload_dict = json.loads(workflow_base.model_dump_json()) + app_url = f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{workflow_run_id}" + workflow_run_response = WorkflowRunResponse( run_id=workflow_run_id, run_type=RunType.workflow_run, @@ -180,7 +182,7 @@ def build_sample_workflow_run_payload(run_id: str | None = None) -> dict: queued_at=payload_dict.get("queued_at"), started_at=payload_dict.get("started_at"), finished_at=payload_dict.get("finished_at"), - app_url=f"https://app.skyvern.com/workflows/{workflow_id}/{workflow_run_id}", + app_url=app_url, browser_session_id=payload_dict.get("browser_session_id"), max_screenshot_scrolls=payload_dict.get("max_screenshot_scrolls"), script_run=None, @@ -402,10 +404,7 @@ async def _build_workflow_payload( f"Run {workflow_run_id} has not reached a terminal state (status={status_response.status})." ) - app_url = ( - f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/" - f"{workflow_run.workflow_permanent_id}/{workflow_run.workflow_run_id}" - ) + app_url = f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{workflow_run.workflow_run_id}" run_response = WorkflowRunResponse( run_id=workflow_run.workflow_run_id, diff --git a/skyvern/services/workflow_service.py b/skyvern/services/workflow_service.py index 787ba931..9eab4ea6 100644 --- a/skyvern/services/workflow_service.py +++ b/skyvern/services/workflow_service.py @@ -114,7 +114,7 @@ async def get_workflow_run_response( workflow_run_id=workflow_run.workflow_run_id, organization_id=organization_id, ) - app_url = f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/{workflow_run.workflow_permanent_id}/{workflow_run.workflow_run_id}" + app_url = f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{workflow_run.workflow_run_id}" return WorkflowRunResponse( run_id=workflow_run_id, run_type=RunType.workflow_run,