diff --git a/skyvern/forge/sdk/db/client.py b/skyvern/forge/sdk/db/client.py index e08aaab5..c7bc98d7 100644 --- a/skyvern/forge/sdk/db/client.py +++ b/skyvern/forge/sdk/db/client.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta, timezone from typing import Any, List, Literal, Sequence, overload import structlog -from sqlalchemy import and_, asc, delete, distinct, func, or_, pool, select, tuple_, update +from sqlalchemy import and_, asc, case, delete, distinct, func, or_, pool, select, tuple_, update from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine @@ -878,7 +878,7 @@ class AgentDB: ) -> OrganizationAuthToken | None: ... @overload - async def get_valid_org_auth_token( + async def get_valid_org_auth_token( # type: ignore self, organization_id: str, token_type: Literal["azure_client_secret_credential"], @@ -3210,6 +3210,50 @@ class AgentDB: LOG.error("UnexpectedError", exc_info=True) raise + async def get_persistent_browser_sessions_history( + self, + organization_id: str, + page: int = 1, + page_size: int = 10, + lookback_hours: int = 24 * 7, + ) -> list[PersistentBrowserSession]: + """Get persistent browser sessions history for an organization.""" + try: + async with self.Session() as session: + open_first = case( + ( + and_( + PersistentBrowserSessionModel.started_at.is_not(None), + PersistentBrowserSessionModel.completed_at.is_(None), + ), + 0, # open + ), + else_=1, # not open + ) + + result = await session.execute( + select(PersistentBrowserSessionModel) + .filter_by(organization_id=organization_id) + .filter_by(deleted_at=None) + .filter( + PersistentBrowserSessionModel.created_at > datetime.utcnow() - timedelta(hours=lookback_hours) + ) + .order_by( + open_first.asc(), # open sessions first + PersistentBrowserSessionModel.created_at.desc(), # then newest within each group + ) + .offset((page - 1) * page_size) + .limit(page_size) + ) + sessions = result.scalars().all() + return [PersistentBrowserSession.model_validate(session) for session in sessions] + except SQLAlchemyError: + LOG.error("SQLAlchemyError", exc_info=True) + raise + except Exception: + LOG.error("UnexpectedError", exc_info=True) + raise + async def get_persistent_browser_session_by_runnable_id( self, runnable_id: str, organization_id: str | None = None ) -> PersistentBrowserSession | None: diff --git a/skyvern/forge/sdk/routes/browser_sessions.py b/skyvern/forge/sdk/routes/browser_sessions.py index 862f4109..78c00cfa 100644 --- a/skyvern/forge/sdk/routes/browser_sessions.py +++ b/skyvern/forge/sdk/routes/browser_sessions.py @@ -1,6 +1,6 @@ import asyncio -from fastapi import Depends, HTTPException, Path +from fastapi import Depends, HTTPException, Path, Query from fastapi.responses import ORJSONResponse from skyvern import analytics @@ -18,6 +18,38 @@ from skyvern.schemas.browser_sessions import CreateBrowserSessionRequest from skyvern.webeye.schemas import BrowserSessionResponse +@base_router.get( + "/browser_sessions/history", + include_in_schema=False, +) +@base_router.get( + "/browser_sessions/history/", + include_in_schema=False, +) +async def get_browser_sessions_all( + current_org: Organization = Depends(org_auth_service.get_current_org), + page: int = Query(1, ge=1, description="Page number for pagination"), + page_size: int = Query(10, ge=1, le=100, description="Number of items per page"), +) -> list[BrowserSessionResponse]: + """Get all browser sessions for the organization""" + analytics.capture("skyvern-oss-agent-browser-sessions-get-all") + + browser_sessions = await app.DATABASE.get_persistent_browser_sessions_history( + current_org.organization_id, + page=page, + page_size=page_size, + ) + + responses = await asyncio.gather( + *[ + BrowserSessionResponse.from_browser_session(browser_session, app.STORAGE) + for browser_session in browser_sessions + ] + ) + + return responses + + @base_router.post( "/browser_sessions", response_model=BrowserSessionResponse,