from __future__ import annotations import asyncio from datetime import datetime import structlog from pydantic import BaseModel, Field from skyvern.config import settings from skyvern.constants import GET_DOWNLOADED_FILES_TIMEOUT from skyvern.forge.sdk.artifact.storage.base import BaseStorage from skyvern.forge.sdk.schemas.files import FileInfo from skyvern.forge.sdk.schemas.persistent_browser_sessions import ( Extensions, PersistentBrowserSession, PersistentBrowserType, ) LOG = structlog.get_logger() class BrowserSessionResponse(BaseModel): """Response model for browser session information.""" browser_session_id: str = Field( description="Unique identifier for the browser session. browser_session_id starts with `pbs_`.", examples=["pbs_123456"], ) organization_id: str = Field(description="ID of the organization that owns this session") status: str | None = Field( None, description="Current status of the browser session", examples=["created", "running", "completed", "failed", "timeout"], ) runnable_type: str | None = Field( None, description="Type of the current runnable associated with this session (workflow, task etc)", examples=["task", "workflow_run"], ) runnable_id: str | None = Field( None, description="ID of the current runnable", examples=["tsk_123456", "wr_123456"] ) timeout: int | None = Field( None, description="Timeout in minutes for the session. Timeout is applied after the session is started. Defaults to 60 minutes.", examples=[60, 120], ) browser_address: str | None = Field( None, description="Url for connecting to the browser", examples=["http://localhost:9222", "https://3.12.10.11/browser/123456"], ) app_url: str | None = Field( None, description="Url for the browser session page", examples=["https://app.skyvern.com/browser-session/pbs_123456"], ) extensions: list[Extensions] | None = Field( None, description="A list of extensions installed in the browser session.", ) browser_type: PersistentBrowserType | None = Field( default=None, description="The type of browser used for the session.", ) vnc_streaming_supported: bool = Field(False, description="Whether the browser session supports VNC streaming") download_path: str | None = Field(None, description="The path where the browser session downloads files") downloaded_files: list[FileInfo] | None = Field( None, description="The list of files downloaded by the browser session" ) recordings: list[FileInfo] | None = Field(None, description="The list of video recordings from the browser session") started_at: datetime | None = Field(None, description="Timestamp when the session was started") completed_at: datetime | None = Field(None, description="Timestamp when the session was completed") created_at: datetime = Field( description="Timestamp when the session was created (the timestamp for the initial request)" ) modified_at: datetime = Field(description="Timestamp when the session was last modified") deleted_at: datetime | None = Field(None, description="Timestamp when the session was deleted, if applicable") @classmethod async def from_browser_session( cls, browser_session: PersistentBrowserSession, storage: BaseStorage | None = None ) -> BrowserSessionResponse: """ Creates a BrowserSessionResponse from a PersistentBrowserSession object. Args: browser_session: The persistent browser session to convert Returns: BrowserSessionResponse: The converted response object """ app_url = ( f"{settings.SKYVERN_APP_URL.rstrip('/')}/browser-session/{browser_session.persistent_browser_session_id}" ) download_path = ( f"/app/downloads/{browser_session.organization_id}/{browser_session.persistent_browser_session_id}" ) downloaded_files: list[FileInfo] = [] recordings: list[FileInfo] = [] if storage: try: async with asyncio.timeout(GET_DOWNLOADED_FILES_TIMEOUT): downloaded_files = await storage.get_shared_downloaded_files_in_browser_session( organization_id=browser_session.organization_id, browser_session_id=browser_session.persistent_browser_session_id, ) except asyncio.TimeoutError: LOG.warning( "Timeout getting downloaded files", browser_session_id=browser_session.persistent_browser_session_id ) try: async with asyncio.timeout(GET_DOWNLOADED_FILES_TIMEOUT): recordings = await storage.get_shared_recordings_in_browser_session( organization_id=browser_session.organization_id, browser_session_id=browser_session.persistent_browser_session_id, ) except asyncio.TimeoutError: LOG.warning( "Timeout getting recordings", browser_session_id=browser_session.persistent_browser_session_id ) # Sort downloaded files by modified_at in descending order (newest first) # Treat None as "oldest". downloaded_files.sort(key=lambda f: (f.modified_at is not None, f.modified_at), reverse=True) # Sort recordings by modified_at in descending order (newest first) # Treat None as "oldest". recordings.sort(key=lambda f: (f.modified_at is not None, f.modified_at), reverse=True) return cls( browser_session_id=browser_session.persistent_browser_session_id, organization_id=browser_session.organization_id, status=browser_session.status, runnable_type=browser_session.runnable_type, runnable_id=browser_session.runnable_id, timeout=browser_session.timeout_minutes, browser_address=browser_session.browser_address, vnc_streaming_supported=True if browser_session.ip_address else False, app_url=app_url, started_at=browser_session.started_at, completed_at=browser_session.completed_at, created_at=browser_session.created_at, modified_at=browser_session.modified_at, deleted_at=browser_session.deleted_at, download_path=download_path, downloaded_files=downloaded_files, recordings=recordings, extensions=browser_session.extensions, browser_type=browser_session.browser_type, )