diff --git a/alembic/versions/2025_07_28_1411-044b4a3c3dbc_add_debug_sessions_table.py b/alembic/versions/2025_07_28_1411-044b4a3c3dbc_add_debug_sessions_table.py new file mode 100644 index 00000000..c59d235b --- /dev/null +++ b/alembic/versions/2025_07_28_1411-044b4a3c3dbc_add_debug_sessions_table.py @@ -0,0 +1,41 @@ +"""Add debug_sessions table + +Revision ID: 044b4a3c3dbc +Revises: 2e997076a3aa +Create Date: 2025-07-28 14:11:17.158184+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "044b4a3c3dbc" +down_revision: Union[str, None] = "2e997076a3aa" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "debug_sessions", + sa.Column("debug_session_id", sa.String(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("browser_session_id", sa.String(), nullable=False), + sa.Column("workflow_permanent_id", sa.String(), nullable=True), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("modified_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("debug_session_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("debug_sessions") + # ### end Alembic commands ### diff --git a/skyvern/forge/sdk/db/client.py b/skyvern/forge/sdk/db/client.py index 50203542..924bf47f 100644 --- a/skyvern/forge/sdk/db/client.py +++ b/skyvern/forge/sdk/db/client.py @@ -22,6 +22,7 @@ from skyvern.forge.sdk.db.models import ( BitwardenSensitiveInformationParameterModel, CredentialModel, CredentialParameterModel, + DebugSessionModel, OnePasswordCredentialParameterModel, OrganizationAuthTokenModel, OrganizationBitwardenCollectionModel, @@ -65,6 +66,7 @@ from skyvern.forge.sdk.log_artifacts import save_workflow_run_logs from skyvern.forge.sdk.models import Step, StepStatus from skyvern.forge.sdk.schemas.ai_suggestions import AISuggestion from skyvern.forge.sdk.schemas.credentials import Credential, CredentialType +from skyvern.forge.sdk.schemas.debug_sessions import DebugSession from skyvern.forge.sdk.schemas.organization_bitwarden_collections import OrganizationBitwardenCollection from skyvern.forge.sdk.schemas.organizations import Organization, OrganizationAuthToken from skyvern.forge.sdk.schemas.persistent_browser_sessions import PersistentBrowserSession @@ -3353,3 +3355,73 @@ class AgentDB: query = query.filter_by(organization_id=organization_id) task_run = (await session.scalars(query)).first() return Run.model_validate(task_run) if task_run else None + + async def get_debug_session( + self, + organization_id: str, + workflow_permanent_id: str, + user_id: str, + timeout_minutes: int = 10, + ) -> DebugSession | None: + async with self.Session() as session: + debug_session = ( + await session.scalars( + select(DebugSessionModel) + .filter_by(organization_id=organization_id) + .filter_by(workflow_permanent_id=workflow_permanent_id) + .filter_by(user_id=user_id) + ) + ).first() + + if not debug_session: + return None + + browser_session = await self.get_persistent_browser_session( + debug_session.browser_session_id, organization_id + ) + + if browser_session and browser_session.completed_at is None: + # TODO: should we check for expiry here - within some threshold? + return DebugSession.model_validate(debug_session) + + browser_session = await self.create_persistent_browser_session( + organization_id=organization_id, + runnable_type="workflow", + runnable_id=workflow_permanent_id, + timeout_minutes=timeout_minutes, + ) + + debug_session.browser_session_id = browser_session.persistent_browser_session_id + + await session.commit() + await session.refresh(debug_session) + + return DebugSession.model_validate(debug_session) + + async def create_debug_session( + self, + organization_id: str, + workflow_permanent_id: str, + user_id: str, + timeout_minutes: int = 10, + ) -> DebugSession: + async with self.Session() as session: + browser_session = await self.create_persistent_browser_session( + organization_id=organization_id, + runnable_type="workflow", + runnable_id=workflow_permanent_id, + timeout_minutes=timeout_minutes, + ) + + debug_session = DebugSessionModel( + organization_id=organization_id, + workflow_permanent_id=workflow_permanent_id, + user_id=user_id, + browser_session_id=browser_session.persistent_browser_session_id, + ) + + session.add(debug_session) + await session.commit() + await session.refresh(debug_session) + + return DebugSession.model_validate(debug_session) diff --git a/skyvern/forge/sdk/db/id.py b/skyvern/forge/sdk/db/id.py index 70ded104..e30b8d87 100644 --- a/skyvern/forge/sdk/db/id.py +++ b/skyvern/forge/sdk/db/id.py @@ -37,6 +37,7 @@ BITWARDEN_SENSITIVE_INFORMATION_PARAMETER_PREFIX = "bsi" CREDENTIAL_ONEPASSWORD_PARAMETER_PREFIX = "opp" CREDENTIAL_PARAMETER_PREFIX = "cp" CREDENTIAL_PREFIX = "cred" +DEBUG_SESSION_PREFIX = "ds" ORGANIZATION_BITWARDEN_COLLECTION_PREFIX = "obc" TASK_V2_ID = "tsk_v2" THOUGHT_ID = "ot" @@ -192,6 +193,11 @@ def generate_credential_id() -> str: return f"{CREDENTIAL_PREFIX}_{int_id}" +def generate_debug_session_id() -> str: + int_id = generate_id() + return f"{DEBUG_SESSION_PREFIX}_{int_id}" + + def generate_organization_bitwarden_collection_id() -> str: int_id = generate_id() return f"{ORGANIZATION_BITWARDEN_COLLECTION_PREFIX}_{int_id}" diff --git a/skyvern/forge/sdk/db/models.py b/skyvern/forge/sdk/db/models.py index fa51ba45..46f75451 100644 --- a/skyvern/forge/sdk/db/models.py +++ b/skyvern/forge/sdk/db/models.py @@ -29,6 +29,7 @@ from skyvern.forge.sdk.db.id import ( generate_bitwarden_sensitive_information_parameter_id, generate_credential_id, generate_credential_parameter_id, + generate_debug_session_id, generate_onepassword_credential_parameter_id, generate_org_id, generate_organization_auth_token_id, @@ -751,3 +752,15 @@ class CredentialModel(Base): created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False) deleted_at = Column(DateTime, nullable=True) + + +class DebugSessionModel(Base): + __tablename__ = "debug_sessions" + + debug_session_id = Column(String, primary_key=True, default=generate_debug_session_id) + organization_id = Column(String, nullable=False) + browser_session_id = Column(String, nullable=False) + workflow_permanent_id = Column(String, nullable=True) + user_id = Column(String, nullable=True) # comes from identity vendor (Clerk at time of writing) + created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) + modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False) diff --git a/skyvern/forge/sdk/schemas/debug_sessions.py b/skyvern/forge/sdk/schemas/debug_sessions.py new file mode 100644 index 00000000..02de4be2 --- /dev/null +++ b/skyvern/forge/sdk/schemas/debug_sessions.py @@ -0,0 +1,13 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class DebugSession(BaseModel): + debug_session_id: str + organization_id: str + browser_session_id: str + workflow_permanent_id: str + user_id: str + created_at: datetime + modified_at: datetime