diff --git a/integrations/langchain/skyvern_langchain/agent.py b/integrations/langchain/skyvern_langchain/agent.py index bea0364b..0f9f2c3d 100644 --- a/integrations/langchain/skyvern_langchain/agent.py +++ b/integrations/langchain/skyvern_langchain/agent.py @@ -46,7 +46,7 @@ class RunTask(SkyvernTaskBaseTool): if url is not None: task_request.url = url - return await self.agent.run_task_v1(task_request=task_request, timeout_seconds=self.run_task_timeout_seconds) + return await self.agent.run_task(task_request=task_request, timeout_seconds=self.run_task_timeout_seconds) async def _arun_task_v2(self, user_prompt: str, url: str | None = None) -> TaskV2: task_request = TaskV2Request(user_prompt=user_prompt, url=url) @@ -72,7 +72,7 @@ class DispatchTask(SkyvernTaskBaseTool): if url is not None: task_request.url = url - return await self.agent.create_task_v1(task_request=task_request) + return await self.agent.create_task(task_request=task_request) async def _arun_task_v2(self, user_prompt: str, url: str | None = None) -> TaskV2: task_request = TaskV2Request(user_prompt=user_prompt, url=url) diff --git a/integrations/llama_index/skyvern_llamaindex/agent.py b/integrations/llama_index/skyvern_llamaindex/agent.py index 1c6ecffd..c79ec3f4 100644 --- a/integrations/llama_index/skyvern_llamaindex/agent.py +++ b/integrations/llama_index/skyvern_llamaindex/agent.py @@ -104,7 +104,7 @@ class SkyvernTaskToolSpec(BaseToolSpec): if url is not None: task_request.url = url - return await self.agent.run_task_v1(task_request=task_request, timeout_seconds=self.run_task_timeout_seconds) + return await self.agent.run_task(task_request=task_request, timeout_seconds=self.run_task_timeout_seconds) async def dispatch_task_v1(self, user_prompt: str, url: Optional[str] = None) -> CreateTaskResponse: task_generation = await self._generate_v1_task_request(user_prompt=user_prompt) @@ -112,7 +112,7 @@ class SkyvernTaskToolSpec(BaseToolSpec): if url is not None: task_request.url = url - return await self.agent.create_task_v1(task_request=task_request) + return await self.agent.create_task(task_request=task_request) async def get_task_v1(self, task_id: str) -> TaskResponse | None: return await self.agent.get_task(task_id=task_id) diff --git a/skyvern/agent/agent.py b/skyvern/agent/agent.py index 6862be14..144b97fa 100644 --- a/skyvern/agent/agent.py +++ b/skyvern/agent/agent.py @@ -1,16 +1,9 @@ import asyncio -import os -import subprocess -from typing import Any, cast from dotenv import load_dotenv -from skyvern.agent.client import SkyvernClient -from skyvern.agent.constants import DEFAULT_AGENT_HEARTBEAT_INTERVAL, DEFAULT_AGENT_TIMEOUT -from skyvern.config import settings from skyvern.forge import app from skyvern.forge.sdk.core import security, skyvern_context -from skyvern.forge.sdk.core.hashing import generate_url_hash from skyvern.forge.sdk.core.skyvern_context import SkyvernContext from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType from skyvern.forge.sdk.schemas.organizations import Organization @@ -18,58 +11,14 @@ from skyvern.forge.sdk.schemas.task_v2 import TaskV2, TaskV2Request, TaskV2Statu from skyvern.forge.sdk.schemas.tasks import CreateTaskResponse, Task, TaskRequest, TaskResponse, TaskStatus from skyvern.forge.sdk.services.org_auth_token_service import API_KEY_LIFETIME from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus -from skyvern.schemas.runs import ProxyLocation, RunEngine, RunResponse, RunType, TaskRunResponse -from skyvern.services import run_service, task_v1_service, task_v2_service +from skyvern.services import task_v2_service from skyvern.utils import migrate_db class SkyvernAgent: - def __init__( - self, - base_url: str | None = None, - api_key: str | None = None, - cdp_url: str | None = None, - browser_path: str | None = None, - browser_type: str | None = None, - ) -> None: - self.skyvern_client: SkyvernClient | None = None - if base_url is None and api_key is None: - # TODO: run at the root wherever the code is initiated - load_dotenv(".env") - migrate_db() - # TODO: will this change the already imported settings? - # TODO: maybe refresh the settings - - self.cdp_url = cdp_url - if browser_path: - # TODO validate browser_path - # Supported Browsers: Google Chrome, Brave Browser, Microsoft Edge, Firefox - if "Chrome" in browser_path or "Brave" in browser_path or "Edge" in browser_path: - result = subprocess.Popen( - ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "--remote-debugging-port=9222"] - ) - if result.returncode != 0: - raise Exception(f"Failed to open browser. browser_path: {browser_path}") - - self.cdp_url = "http://127.0.0.1:9222" - settings.BROWSER_TYPE = "cdp-connect" - settings.BROWSER_REMOTE_DEBUGGING_URL = self.cdp_url - else: - raise ValueError( - f"Unsupported browser or invalid path: {browser_path}. " - "Here's a list of supported browsers Skyvern can connect to: Google Chrome, Brave Browser, Microsoft Edge, Firefox." - ) - elif base_url is None and api_key is None: - if not browser_type: - if "BROWSER_TYPE" not in os.environ: - raise Exception("browser type is missing") - browser_type = os.environ["BROWSER_TYPE"] - - settings.BROWSER_TYPE = browser_type - elif base_url and api_key: - self.client = SkyvernClient(base_url=base_url, api_key=api_key) - else: - raise ValueError("base_url and api_key must be both provided") + def __init__(self) -> None: + load_dotenv(".env") + migrate_db() async def _get_organization(self) -> Organization: organization = await app.DATABASE.get_organization_by_domain("skyvern.local") @@ -92,7 +41,7 @@ class SkyvernAgent: ) return organization - async def _run_task(self, organization: Organization, task: Task, max_steps: int | None = None) -> None: + async def _run_task(self, organization: Organization, task: Task) -> None: org_auth_token = await app.DATABASE.get_valid_org_auth_token( organization_id=organization.organization_id, token_type=OrganizationAuthTokenType.api, @@ -109,23 +58,13 @@ class SkyvernAgent: status=TaskStatus.running, organization_id=organization.organization_id, ) - try: - skyvern_context.set( - SkyvernContext( - organization_id=organization.organization_id, - task_id=task.task_id, - max_steps_override=max_steps, - ) - ) - step, _, _ = await app.agent.execute_step( - organization=organization, - task=updated_task, - step=step, - api_key=org_auth_token.token if org_auth_token else None, - ) - finally: - skyvern_context.reset() + step, _, _ = await app.agent.execute_step( + organization=organization, + task=updated_task, + step=step, + api_key=org_auth_token.token if org_auth_token else None, + ) async def _run_task_v2(self, organization: Organization, task_v2: TaskV2) -> None: # mark task v2 as queued @@ -146,15 +85,22 @@ class SkyvernAgent: task_v2_id=task_v2.observer_cruise_id, ) - async def create_task_v1( + async def create_task( self, task_request: TaskRequest, ) -> CreateTaskResponse: organization = await self._get_organization() created_task = await app.agent.create_task(task_request, organization.organization_id) + skyvern_context.set( + SkyvernContext( + organization_id=organization.organization_id, + task_id=created_task.task_id, + max_steps_override=created_task.max_steps_per_run, + ) + ) - asyncio.create_task(self._run_task(organization, created_task, max_steps=task_request.max_steps_per_run)) + asyncio.create_task(self._run_task(organization, created_task)) return CreateTaskResponse(task_id=created_task.task_id) async def get_task( @@ -192,12 +138,12 @@ class SkyvernAgent: task=task, last_step=latest_step, failure_reason=failure_reason, need_browser_log=True ) - async def run_task_v1( + async def run_task( self, task_request: TaskRequest, timeout_seconds: int = 600, ) -> TaskResponse: - created_task = await self.create_task_v1(task_request) + created_task = await self.create_task(task_request) async with asyncio.timeout(timeout_seconds): while True: @@ -241,148 +187,3 @@ class SkyvernAgent: if refreshed_task_v2.status.is_final(): return refreshed_task_v2 await asyncio.sleep(1) - - ############### officially supported interfaces ############### - async def get_run(self, run_id: str) -> RunResponse | None: - if not self.client: - organization = await self._get_organization() - return await run_service.get_run_response(run_id, organization_id=organization.organization_id) - - return await self.client.get_run(run_id) - - async def run_task( - self, - prompt: str, - engine: RunEngine = RunEngine.skyvern_v1, - url: str | None = None, - webhook_url: str | None = None, - totp_identifier: str | None = None, - totp_url: str | None = None, - title: str | None = None, - error_code_mapping: dict[str, str] | None = None, - data_extraction_schema: dict[str, Any] | None = None, - proxy_location: ProxyLocation | None = None, - max_steps: int | None = None, - wait_for_completion: bool = True, - timeout: float = DEFAULT_AGENT_TIMEOUT, - browser_session_id: str | None = None, - ) -> TaskRunResponse: - if not self.client: - if engine == RunEngine.skyvern_v1: - data_extraction_goal = None - data_extraction_schema = data_extraction_schema - navigation_goal = prompt - navigation_payload = None - organization = await self._get_organization() - if not url: - task_generation = await task_v1_service.generate_task( - user_prompt=prompt, - organization=organization, - ) - url = task_generation.url - navigation_goal = task_generation.navigation_goal or prompt - navigation_payload = task_generation.navigation_payload - data_extraction_goal = task_generation.data_extraction_goal - data_extraction_schema = data_extraction_schema or task_generation.extracted_information_schema - - task_request = TaskRequest( - title=title, - url=url, - navigation_goal=navigation_goal, - navigation_payload=navigation_payload, - data_extraction_goal=data_extraction_goal, - extracted_information_schema=data_extraction_schema, - error_code_mapping=error_code_mapping, - proxy_location=proxy_location, - ) - - if wait_for_completion: - created_task = await app.agent.create_task(task_request, organization.organization_id) - url_hash = generate_url_hash(task_request.url) - await app.DATABASE.create_task_run( - task_run_type=RunType.task_v1, - organization_id=organization.organization_id, - run_id=created_task.task_id, - title=task_request.title, - url=task_request.url, - url_hash=url_hash, - ) - try: - await self._run_task(organization, created_task) - run_obj = await self.get_run(run_id=created_task.task_id) - return cast(TaskRunResponse, run_obj) - except Exception: - # TODO: better error handling and logging - run_obj = await self.get_run(run_id=created_task.task_id) - return cast(TaskRunResponse, run_obj) - else: - create_task_resp = await self.create_task_v1(task_request) - run_obj = await self.get_run(run_id=create_task_resp.task_id) - return cast(TaskRunResponse, run_obj) - elif engine == RunEngine.skyvern_v2: - # initialize task v2 - organization = await self._get_organization() - - task_v2 = await task_v2_service.initialize_task_v2( - organization=organization, - user_prompt=prompt, - user_url=url, - totp_identifier=totp_identifier, - totp_verification_url=totp_url, - webhook_callback_url=webhook_url, - proxy_location=proxy_location, - publish_workflow=False, - extracted_information_schema=data_extraction_schema, - error_code_mapping=error_code_mapping, - create_task_run=True, - ) - - if wait_for_completion: - await self._run_task_v2(organization, task_v2) - run_obj = await self.get_run(run_id=task_v2.observer_cruise_id) - return cast(TaskRunResponse, run_obj) - else: - asyncio.create_task(self._run_task_v2(organization, task_v2)) - run_obj = await self.get_run(run_id=task_v2.observer_cruise_id) - return cast(TaskRunResponse, run_obj) - else: - raise ValueError("Local mode is not supported for this method") - - task_run = await self.client.run_task( - prompt=prompt, - engine=engine, - url=url, - webhook_url=webhook_url, - totp_identifier=totp_identifier, - totp_url=totp_url, - title=title, - error_code_mapping=error_code_mapping, - proxy_location=proxy_location, - max_steps=max_steps, - ) - - if wait_for_completion: - async with asyncio.timeout(timeout): - while True: - task_run = await self.client.get_run(task_run.run_id) - if task_run.status.is_final(): - return task_run - await asyncio.sleep(DEFAULT_AGENT_HEARTBEAT_INTERVAL) - return task_run - - async def run_workflow( - self, - workflow_id: str, - parameters: dict[str, Any], - webhook_url: str | None = None, - totp_identifier: str | None = None, - totp_url: str | None = None, - title: str | None = None, - error_code_mapping: dict[str, str] | None = None, - proxy_location: ProxyLocation | None = None, - max_steps: int | None = None, - wait_for_completion: bool = True, - timeout: float = DEFAULT_AGENT_TIMEOUT, - browser_session_id: str | None = None, - ) -> None: - raise NotImplementedError("Running workflows is currently not supported with skyvern SDK.") diff --git a/skyvern/agent/constants.py b/skyvern/agent/constants.py deleted file mode 100644 index 7a0e6ab3..00000000 --- a/skyvern/agent/constants.py +++ /dev/null @@ -1,2 +0,0 @@ -DEFAULT_AGENT_TIMEOUT = 1800 # 30 minutes -DEFAULT_AGENT_HEARTBEAT_INTERVAL = 10 # 10 seconds diff --git a/skyvern/forge/api_app.py b/skyvern/forge/api_app.py index 77a66d05..c07a7a9d 100644 --- a/skyvern/forge/api_app.py +++ b/skyvern/forge/api_app.py @@ -18,9 +18,7 @@ from skyvern.forge import app as forge_app from skyvern.forge.sdk.core import skyvern_context from skyvern.forge.sdk.core.skyvern_context import SkyvernContext from skyvern.forge.sdk.db.exceptions import NotFoundError -from skyvern.forge.sdk.routes.agent_protocol import base_router, official_api_router, v2_router -from skyvern.forge.sdk.routes.streaming import websocket_router -from skyvern.forge.sdk.routes.totp import totp_router +from skyvern.forge.sdk.routes.routers import base_router, legacy_base_router, legacy_v2_router LOG = structlog.get_logger() @@ -66,11 +64,9 @@ def get_agent_app() -> FastAPI: allow_headers=["*"], ) - app.include_router(official_api_router, prefix="/v1") - app.include_router(base_router, prefix="/api/v1") - app.include_router(v2_router, prefix="/api/v2") - app.include_router(websocket_router, prefix="/api/v1/stream") - app.include_router(totp_router, prefix="/api/v1/totp") + app.include_router(base_router, prefix="/v1") + app.include_router(legacy_base_router, prefix="/api/v1") + app.include_router(legacy_v2_router, prefix="/api/v2") app.openapi = custom_openapi app.add_middleware( diff --git a/skyvern/forge/sdk/routes/__init__.py b/skyvern/forge/sdk/routes/__init__.py index e69de29b..d80f63ed 100644 --- a/skyvern/forge/sdk/routes/__init__.py +++ b/skyvern/forge/sdk/routes/__init__.py @@ -0,0 +1,4 @@ +from skyvern.forge.sdk.routes import agent_protocol # noqa: F401 +from skyvern.forge.sdk.routes import browser_sessions # noqa: F401 +from skyvern.forge.sdk.routes import streaming # noqa: F401 +from skyvern.forge.sdk.routes import totp # noqa: F401 diff --git a/skyvern/forge/sdk/routes/agent_protocol.py b/skyvern/forge/sdk/routes/agent_protocol.py index 34a06d4e..4bff9e21 100644 --- a/skyvern/forge/sdk/routes/agent_protocol.py +++ b/skyvern/forge/sdk/routes/agent_protocol.py @@ -6,18 +6,7 @@ from typing import Annotated, Any import structlog import yaml -from fastapi import ( - APIRouter, - BackgroundTasks, - Depends, - Header, - HTTPException, - Query, - Request, - Response, - UploadFile, - status, -) +from fastapi import BackgroundTasks, Depends, Header, HTTPException, Query, Request, Response, UploadFile, status from fastapi.responses import ORJSONResponse from skyvern import analytics @@ -33,6 +22,7 @@ from skyvern.forge.sdk.core.security import generate_skyvern_signature from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType from skyvern.forge.sdk.executor.factory import AsyncExecutorFactory from skyvern.forge.sdk.models import Step +from skyvern.forge.sdk.routes.routers import base_router, legacy_base_router, legacy_v2_router from skyvern.forge.sdk.schemas.ai_suggestions import AISuggestionBase, AISuggestionRequest from skyvern.forge.sdk.schemas.organizations import ( GetOrganizationAPIKeysResponse, @@ -73,11 +63,6 @@ from skyvern.forge.sdk.workflow.models.yaml import WorkflowCreateYAMLRequest from skyvern.schemas.runs import RunEngine, RunResponse, RunType, TaskRunRequest, TaskRunResponse from skyvern.services import run_service, task_v1_service, task_v2_service from skyvern.webeye.actions.actions import Action -from skyvern.webeye.schemas import BrowserSessionResponse - -official_api_router = APIRouter() -base_router = APIRouter() -v2_router = APIRouter() LOG = structlog.get_logger() @@ -103,15 +88,16 @@ class AISuggestionType(str, Enum): DATA_SCHEMA = "data_schema" -@base_router.post( +@legacy_base_router.post( "/webhook", tags=["server"], openapi_extra={ "x-fern-sdk-group-name": "server", "x-fern-sdk-method-name": "webhook", }, + include_in_schema=False, ) -@base_router.post("/webhook/", include_in_schema=False) +@legacy_base_router.post("/webhook/", include_in_schema=False) async def webhook( request: Request, x_skyvern_signature: Annotated[str | None, Header()] = None, @@ -148,7 +134,7 @@ async def webhook( return Response(content="webhook validation", status_code=200) -@base_router.get( +@legacy_base_router.get( "/heartbeat", tags=["server"], openapi_extra={ @@ -156,7 +142,7 @@ async def webhook( "x-fern-sdk-method-name": "heartbeat", }, ) -@base_router.get("/heartbeat/", include_in_schema=False) +@legacy_base_router.get("/heartbeat/", include_in_schema=False) async def heartbeat() -> Response: """ Check if the server is running. @@ -164,7 +150,7 @@ async def heartbeat() -> Response: return Response(content="Server is running.", status_code=200) -@base_router.post( +@legacy_base_router.post( "/tasks", tags=["agent"], response_model=CreateTaskResponse, @@ -173,7 +159,7 @@ async def heartbeat() -> Response: "x-fern-sdk-method-name": "run_task_v1", }, ) -@base_router.post( +@legacy_base_router.post( "/tasks/", response_model=CreateTaskResponse, include_in_schema=False, @@ -200,7 +186,7 @@ async def run_task_v1( return CreateTaskResponse(task_id=created_task.task_id) -@base_router.get( +@legacy_base_router.get( "/tasks/{task_id}", tags=["agent"], response_model=TaskResponse, @@ -209,7 +195,7 @@ async def run_task_v1( "x-fern-sdk-method-name": "get_task_v1", }, ) -@base_router.get("/tasks/{task_id}/", response_model=TaskResponse, include_in_schema=False) +@legacy_base_router.get("/tasks/{task_id}/", response_model=TaskResponse, include_in_schema=False) async def get_task_v1( task_id: str, current_org: Organization = Depends(org_auth_service.get_current_org), @@ -248,7 +234,7 @@ async def get_task_v1( ) -@base_router.post( +@legacy_base_router.post( "/tasks/{task_id}/cancel", tags=["agent"], openapi_extra={ @@ -256,7 +242,7 @@ async def get_task_v1( "x-fern-sdk-method-name": "cancel_task", }, ) -@base_router.post("/tasks/{task_id}/cancel/", include_in_schema=False) +@legacy_base_router.post("/tasks/{task_id}/cancel/", include_in_schema=False) async def cancel_task( task_id: str, current_org: Organization = Depends(org_auth_service.get_current_org), @@ -276,7 +262,7 @@ async def cancel_task( await app.agent.execute_task_webhook(task=task, last_step=latest_step, api_key=x_api_key) -@base_router.post( +@legacy_base_router.post( "/workflows/runs/{workflow_run_id}/cancel", tags=["agent"], openapi_extra={ @@ -284,7 +270,7 @@ async def cancel_task( "x-fern-sdk-method-name": "cancel_workflow_run", }, ) -@base_router.post("/workflows/runs/{workflow_run_id}/cancel/", include_in_schema=False) +@legacy_base_router.post("/workflows/runs/{workflow_run_id}/cancel/", include_in_schema=False) async def cancel_workflow_run( workflow_run_id: str, current_org: Organization = Depends(org_auth_service.get_current_org), @@ -316,7 +302,7 @@ async def cancel_workflow_run( await app.WORKFLOW_SERVICE.execute_workflow_webhook(workflow_run, api_key=x_api_key) -@base_router.post( +@legacy_base_router.post( "/tasks/{task_id}/retry_webhook", tags=["agent"], response_model=TaskResponse, @@ -325,7 +311,7 @@ async def cancel_workflow_run( "x-fern-sdk-method-name": "retry_webhook", }, ) -@base_router.post( +@legacy_base_router.post( "/tasks/{task_id}/retry_webhook/", response_model=TaskResponse, include_in_schema=False, @@ -354,7 +340,7 @@ async def retry_webhook( return await app.agent.build_task_response(task=task_obj, last_step=latest_step) -@base_router.get( +@legacy_base_router.get( "/tasks", tags=["agent"], response_model=list[Task], @@ -363,7 +349,7 @@ async def retry_webhook( "x-fern-sdk-method-name": "get_tasks", }, ) -@base_router.get( +@legacy_base_router.get( "/tasks/", response_model=list[Task], include_in_schema=False, @@ -411,7 +397,7 @@ async def get_tasks( return ORJSONResponse([(await app.agent.build_task_response(task=task)).model_dump() for task in tasks]) -@base_router.get( +@legacy_base_router.get( "/runs", tags=["agent"], response_model=list[WorkflowRun | Task], @@ -420,7 +406,7 @@ async def get_tasks( "x-fern-sdk-method-name": "get_runs", }, ) -@base_router.get( +@legacy_base_router.get( "/runs/", response_model=list[WorkflowRun | Task], include_in_schema=False, @@ -441,7 +427,7 @@ async def get_runs( return ORJSONResponse([run.model_dump() for run in runs]) -@official_api_router.get( +@base_router.get( "/runs/{run_id}", tags=["agent"], response_model=RunResponse, @@ -450,7 +436,7 @@ async def get_runs( "x-fern-sdk-method-name": "get_run", }, ) -@official_api_router.get( +@base_router.get( "/runs/{run_id}/", response_model=RunResponse, include_in_schema=False, @@ -468,7 +454,7 @@ async def get_run( return run_response -@base_router.get( +@legacy_base_router.get( "/tasks/{task_id}/steps", tags=["agent"], response_model=list[Step], @@ -477,7 +463,7 @@ async def get_run( "x-fern-sdk-method-name": "get_steps", }, ) -@base_router.get( +@legacy_base_router.get( "/tasks/{task_id}/steps/", response_model=list[Step], include_in_schema=False, @@ -496,7 +482,7 @@ async def get_steps( return ORJSONResponse([step.model_dump(exclude_none=True) for step in steps]) -@base_router.get( +@legacy_base_router.get( "/{entity_type}/{entity_id}/artifacts", tags=["agent"], response_model=list[Artifact], @@ -505,7 +491,7 @@ async def get_steps( "x-fern-sdk-method-name": "get_artifacts", }, ) -@base_router.get( +@legacy_base_router.get( "/{entity_type}/{entity_id}/artifacts/", response_model=list[Artifact], include_in_schema=False, @@ -560,7 +546,7 @@ async def get_artifacts( return ORJSONResponse([artifact.model_dump() for artifact in artifacts]) -@base_router.get( +@legacy_base_router.get( "/tasks/{task_id}/steps/{step_id}/artifacts", tags=["agent"], response_model=list[Artifact], @@ -569,7 +555,7 @@ async def get_artifacts( "x-fern-sdk-method-name": "get_step_artifacts", }, ) -@base_router.get( +@legacy_base_router.get( "/tasks/{task_id}/steps/{step_id}/artifacts/", response_model=list[Artifact], include_in_schema=False, @@ -605,7 +591,7 @@ async def get_step_artifacts( return ORJSONResponse([artifact.model_dump() for artifact in artifacts]) -@base_router.get( +@legacy_base_router.get( "/tasks/{task_id}/actions", response_model=list[Action], tags=["agent"], @@ -614,7 +600,7 @@ async def get_step_artifacts( "x-fern-sdk-method-name": "get_actions", }, ) -@base_router.get( +@legacy_base_router.get( "/tasks/{task_id}/actions/", response_model=list[Action], include_in_schema=False, @@ -628,7 +614,7 @@ async def get_actions( return actions -@base_router.post( +@legacy_base_router.post( "/workflows/{workflow_id}/run", response_model=RunWorkflowResponse, tags=["agent"], @@ -637,7 +623,7 @@ async def get_actions( "x-fern-sdk-method-name": "run_workflow", }, ) -@base_router.post( +@legacy_base_router.post( "/workflows/{workflow_id}/run/", response_model=RunWorkflowResponse, include_in_schema=False, @@ -703,7 +689,7 @@ async def run_workflow( ) -@base_router.get( +@legacy_base_router.get( "/workflows/runs", response_model=list[WorkflowRun], tags=["agent"], @@ -712,7 +698,7 @@ async def run_workflow( "x-fern-sdk-method-name": "get_workflow_runs", }, ) -@base_router.get( +@legacy_base_router.get( "/workflows/runs/", response_model=list[WorkflowRun], include_in_schema=False, @@ -732,7 +718,7 @@ async def get_workflow_runs( ) -@base_router.get( +@legacy_base_router.get( "/workflows/{workflow_id}/runs", response_model=list[WorkflowRun], tags=["agent"], @@ -741,7 +727,7 @@ async def get_workflow_runs( "x-fern-sdk-method-name": "get_workflow_runs_by_id", }, ) -@base_router.get( +@legacy_base_router.get( "/workflows/{workflow_id}/runs/", response_model=list[WorkflowRun], include_in_schema=False, @@ -763,7 +749,7 @@ async def get_workflow_runs_by_id( ) -@base_router.get( +@legacy_base_router.get( "/workflows/{workflow_id}/runs/{workflow_run_id}", tags=["agent"], openapi_extra={ @@ -771,7 +757,7 @@ async def get_workflow_runs_by_id( "x-fern-sdk-method-name": "get_workflow_run_with_workflow_id", }, ) -@base_router.get( +@legacy_base_router.get( "/workflows/{workflow_id}/runs/{workflow_run_id}/", include_in_schema=False, ) @@ -797,7 +783,7 @@ async def get_workflow_run_with_workflow_id( return return_dict -@base_router.get( +@legacy_base_router.get( "/workflows/{workflow_id}/runs/{workflow_run_id}/timeline", tags=["agent"], openapi_extra={ @@ -805,7 +791,7 @@ async def get_workflow_run_with_workflow_id( "x-fern-sdk-method-name": "get_workflow_run_timeline", }, ) -@base_router.get( +@legacy_base_router.get( "/workflows/{workflow_id}/runs/{workflow_run_id}/timeline/", include_in_schema=False, ) @@ -818,7 +804,7 @@ async def get_workflow_run_timeline( return await _flatten_workflow_run_timeline(current_org.organization_id, workflow_run_id) -@base_router.get( +@legacy_base_router.get( "/workflows/runs/{workflow_run_id}", response_model=WorkflowRunResponse, tags=["agent"], @@ -827,7 +813,7 @@ async def get_workflow_run_timeline( "x-fern-sdk-method-name": "get_workflow_run", }, ) -@base_router.get( +@legacy_base_router.get( "/workflows/runs/{workflow_run_id}/", response_model=WorkflowRunResponse, include_in_schema=False, @@ -843,7 +829,7 @@ async def get_workflow_run( ) -@base_router.post( +@legacy_base_router.post( "/workflows", openapi_extra={ "requestBody": { @@ -856,7 +842,7 @@ async def get_workflow_run( response_model=Workflow, tags=["agent"], ) -@base_router.post( +@legacy_base_router.post( "/workflows/", openapi_extra={ "requestBody": { @@ -890,7 +876,7 @@ async def create_workflow( raise FailedToCreateWorkflow(str(e)) -@base_router.put( +@legacy_base_router.put( "/workflows/{workflow_permanent_id}", openapi_extra={ "requestBody": { @@ -903,7 +889,7 @@ async def create_workflow( response_model=Workflow, tags=["agent"], ) -@base_router.put( +@legacy_base_router.put( "/workflows/{workflow_permanent_id}/", openapi_extra={ "requestBody": { @@ -945,7 +931,7 @@ async def update_workflow( raise FailedToUpdateWorkflow(workflow_permanent_id, f"<{type(e).__name__}: {str(e)}>") -@base_router.delete( +@legacy_base_router.delete( "/workflows/{workflow_permanent_id}", tags=["agent"], openapi_extra={ @@ -953,7 +939,7 @@ async def update_workflow( "x-fern-sdk-method-name": "delete_workflow", }, ) -@base_router.delete("/workflows/{workflow_permanent_id}/", include_in_schema=False) +@legacy_base_router.delete("/workflows/{workflow_permanent_id}/", include_in_schema=False) async def delete_workflow( workflow_permanent_id: str, current_org: Organization = Depends(org_auth_service.get_current_org), @@ -962,7 +948,7 @@ async def delete_workflow( await app.WORKFLOW_SERVICE.delete_workflow_by_permanent_id(workflow_permanent_id, current_org.organization_id) -@base_router.get( +@legacy_base_router.get( "/workflows", response_model=list[Workflow], tags=["agent"], @@ -971,7 +957,7 @@ async def delete_workflow( "x-fern-sdk-method-name": "get_workflows", }, ) -@base_router.get( +@legacy_base_router.get( "/workflows/", response_model=list[Workflow], include_in_schema=False, @@ -1020,7 +1006,7 @@ async def get_workflows( ) -@base_router.get( +@legacy_base_router.get( "/workflows/templates", response_model=list[Workflow], tags=["agent"], @@ -1029,7 +1015,7 @@ async def get_workflows( "x-fern-sdk-method-name": "get_workflow_templates", }, ) -@base_router.get( +@legacy_base_router.get( "/workflows/templates/", response_model=list[Workflow], include_in_schema=False, @@ -1048,7 +1034,7 @@ async def get_workflow_templates() -> list[Workflow]: return workflows -@base_router.get( +@legacy_base_router.get( "/workflows/{workflow_permanent_id}", response_model=Workflow, tags=["agent"], @@ -1057,7 +1043,7 @@ async def get_workflow_templates() -> list[Workflow]: "x-fern-sdk-method-name": "get_workflow", }, ) -@base_router.get("/workflows/{workflow_permanent_id}/", response_model=Workflow, include_in_schema=False) +@legacy_base_router.get("/workflows/{workflow_permanent_id}/", response_model=Workflow, include_in_schema=False) async def get_workflow( workflow_permanent_id: str, version: int | None = None, @@ -1076,7 +1062,7 @@ async def get_workflow( ) -@base_router.post( +@legacy_base_router.post( "/suggest/{ai_suggestion_type}", include_in_schema=False, tags=["agent"], @@ -1085,7 +1071,7 @@ async def get_workflow( "x-fern-sdk-method-name": "suggest", }, ) -@base_router.post("/suggest/{ai_suggestion_type}/", include_in_schema=False) +@legacy_base_router.post("/suggest/{ai_suggestion_type}/", include_in_schema=False) async def suggest( ai_suggestion_type: AISuggestionType, data: AISuggestionRequest, @@ -1114,7 +1100,7 @@ async def suggest( raise HTTPException(status_code=400, detail="Failed to suggest data schema. Please try again later.") -@base_router.post( +@legacy_base_router.post( "/generate/task", tags=["agent"], openapi_extra={ @@ -1122,7 +1108,7 @@ async def suggest( "x-fern-sdk-method-name": "generate_task", }, ) -@base_router.post("/generate/task/", include_in_schema=False) +@legacy_base_router.post("/generate/task/", include_in_schema=False) async def generate_task( data: GenerateTaskRequest, current_org: Organization = Depends(org_auth_service.get_current_org), @@ -1134,7 +1120,7 @@ async def generate_task( ) -@base_router.put( +@legacy_base_router.put( "/organizations", tags=["server"], openapi_extra={ @@ -1142,7 +1128,7 @@ async def generate_task( "x-fern-sdk-method-name": "update_organization", }, ) -@base_router.put( +@legacy_base_router.put( "/organizations", include_in_schema=False, ) @@ -1156,7 +1142,7 @@ async def update_organization( ) -@base_router.get( +@legacy_base_router.get( "/organizations", tags=["server"], openapi_extra={ @@ -1164,7 +1150,7 @@ async def update_organization( "x-fern-sdk-method-name": "get_organizations", }, ) -@base_router.get( +@legacy_base_router.get( "/organizations/", include_in_schema=False, ) @@ -1174,7 +1160,7 @@ async def get_organizations( return GetOrganizationsResponse(organizations=[current_org]) -@base_router.get( +@legacy_base_router.get( "/organizations/{organization_id}/apikeys/", tags=["server"], openapi_extra={ @@ -1182,7 +1168,7 @@ async def get_organizations( "x-fern-sdk-method-name": "get_api_keys", }, ) -@base_router.get( +@legacy_base_router.get( "/organizations/{organization_id}/apikeys", include_in_schema=False, ) @@ -1215,7 +1201,7 @@ async def _validate_file_size(file: UploadFile) -> UploadFile: return file -@base_router.post( +@legacy_base_router.post( "/upload_file", tags=["server"], openapi_extra={ @@ -1223,7 +1209,7 @@ async def _validate_file_size(file: UploadFile) -> UploadFile: "x-fern-sdk-method-name": "upload_file", }, ) -@base_router.post( +@legacy_base_router.post( "/upload_file/", include_in_schema=False, ) @@ -1268,7 +1254,7 @@ async def upload_file( ) -@v2_router.post( +@legacy_v2_router.post( "/tasks", tags=["agent"], openapi_extra={ @@ -1276,7 +1262,7 @@ async def upload_file( "x-fern-sdk-method-name": "run_task_v2", }, ) -@v2_router.post( +@legacy_v2_router.post( "/tasks/", include_in_schema=False, ) @@ -1327,7 +1313,7 @@ async def run_task_v2( return task_v2.model_dump(by_alias=True) -@v2_router.get( +@legacy_v2_router.get( "/tasks/{task_id}", tags=["agent"], openapi_extra={ @@ -1335,7 +1321,7 @@ async def run_task_v2( "x-fern-sdk-method-name": "get_task_v2", }, ) -@v2_router.get( +@legacy_v2_router.get( "/tasks/{task_id}/", include_in_schema=False, ) @@ -1349,102 +1335,6 @@ async def get_task_v2( return task_v2.model_dump(by_alias=True) -@base_router.get( - "/browser_sessions/{browser_session_id}", - response_model=BrowserSessionResponse, - tags=["session"], - openapi_extra={ - "x-fern-sdk-group-name": "session", - "x-fern-sdk-method-name": "get_browser_session", - }, -) -@base_router.get( - "/browser_sessions/{browser_session_id}/", - response_model=BrowserSessionResponse, - include_in_schema=False, -) -async def get_browser_session( - browser_session_id: str, - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> BrowserSessionResponse: - analytics.capture("skyvern-oss-agent-workflow-run-get") - browser_session = await app.PERSISTENT_SESSIONS_MANAGER.get_session( - browser_session_id, - current_org.organization_id, - ) - if not browser_session: - raise HTTPException(status_code=404, detail=f"Browser session {browser_session_id} not found") - return BrowserSessionResponse.from_browser_session(browser_session) - - -@base_router.get( - "/browser_sessions", - response_model=list[BrowserSessionResponse], - tags=["session"], - openapi_extra={ - "x-fern-sdk-group-name": "session", - "x-fern-sdk-method-name": "get_browser_sessions", - }, -) -@base_router.get( - "/browser_sessions/", - response_model=list[BrowserSessionResponse], - include_in_schema=False, -) -async def get_browser_sessions( - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> list[BrowserSessionResponse]: - """Get all active browser sessions for the organization""" - analytics.capture("skyvern-oss-agent-browser-sessions-get") - browser_sessions = await app.PERSISTENT_SESSIONS_MANAGER.get_active_sessions(current_org.organization_id) - return [BrowserSessionResponse.from_browser_session(browser_session) for browser_session in browser_sessions] - - -@base_router.post( - "/browser_sessions", - response_model=BrowserSessionResponse, - tags=["session"], - openapi_extra={ - "x-fern-sdk-group-name": "session", - "x-fern-sdk-method-name": "create_browser_session", - }, -) -@base_router.post( - "/browser_sessions/", - response_model=BrowserSessionResponse, - include_in_schema=False, -) -async def create_browser_session( - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> BrowserSessionResponse: - browser_session = await app.PERSISTENT_SESSIONS_MANAGER.create_session(current_org.organization_id) - return BrowserSessionResponse.from_browser_session(browser_session) - - -@base_router.post( - "/browser_sessions/{session_id}/close", - tags=["session"], - openapi_extra={ - "x-fern-sdk-group-name": "session", - "x-fern-sdk-method-name": "close_browser_session", - }, -) -@base_router.post( - "/browser_sessions/{session_id}/close/", - include_in_schema=False, -) -async def close_browser_session( - session_id: str, - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> ORJSONResponse: - await app.PERSISTENT_SESSIONS_MANAGER.close_session(current_org.organization_id, session_id) - return ORJSONResponse( - content={"message": "Browser session closed"}, - status_code=200, - media_type="application/json", - ) - - async def _flatten_workflow_run_timeline(organization_id: str, workflow_run_id: str) -> list[WorkflowRunTimeline]: """ Get the timeline workflow runs including the nested workflow runs in a flattened list @@ -1494,15 +1384,21 @@ async def _flatten_workflow_run_timeline(organization_id: str, workflow_run_id: return final_workflow_run_block_timeline -@official_api_router.post( +@base_router.post( "/tasks", - tags=["agent"], + tags=["Agent"], openapi_extra={ "x-fern-sdk-group-name": "agent", "x-fern-sdk-method-name": "run_task", }, + description="Run a task", + summary="Run a task", + responses={ + 200: {"description": "Successfully run task"}, + 400: {"description": "Invalid agent engine"}, + }, ) -@official_api_router.post("/tasks/", include_in_schema=False) +@base_router.post("/tasks/", include_in_schema=False) async def run_task( request: Request, background_tasks: BackgroundTasks, diff --git a/skyvern/forge/sdk/routes/browser_sessions.py b/skyvern/forge/sdk/routes/browser_sessions.py new file mode 100644 index 00000000..1d48f60a --- /dev/null +++ b/skyvern/forge/sdk/routes/browser_sessions.py @@ -0,0 +1,165 @@ +from fastapi import Depends, HTTPException +from fastapi.responses import ORJSONResponse + +from skyvern import analytics +from skyvern.forge import app +from skyvern.forge.sdk.routes.routers import base_router, legacy_base_router +from skyvern.forge.sdk.schemas.organizations import Organization +from skyvern.forge.sdk.services import org_auth_service +from skyvern.webeye.schemas import BrowserSessionResponse + + +@base_router.get( + "/browser_sessions/{browser_session_id}", + response_model=BrowserSessionResponse, + tags=["Browser Sessions"], + openapi_extra={ + "x-fern-sdk-group-name": "browser_session", + "x-fern-sdk-method-name": "get_browser_session", + }, + description="Get details about a specific browser session by ID", + summary="Get browser session details", + responses={ + 200: {"description": "Successfully retrieved browser session details"}, + 404: {"description": "Browser session not found"}, + 401: {"description": "Unauthorized - Invalid or missing authentication"}, + }, +) +@legacy_base_router.get( + "/browser_sessions/{browser_session_id}", + response_model=BrowserSessionResponse, + tags=["session"], + openapi_extra={ + "x-fern-sdk-group-name": "session", + "x-fern-sdk-method-name": "get_browser_session", + }, +) +@legacy_base_router.get( + "/browser_sessions/{browser_session_id}/", + response_model=BrowserSessionResponse, + include_in_schema=False, +) +async def get_browser_session( + browser_session_id: str, + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> BrowserSessionResponse: + analytics.capture("skyvern-oss-agent-browser-session-get") + browser_session = await app.PERSISTENT_SESSIONS_MANAGER.get_session( + browser_session_id, + current_org.organization_id, + ) + if not browser_session: + raise HTTPException(status_code=404, detail=f"Browser session {browser_session_id} not found") + return BrowserSessionResponse.from_browser_session(browser_session) + + +@base_router.get( + "/browser_sessions", + response_model=list[BrowserSessionResponse], + tags=["Browser Sessions"], + openapi_extra={ + "x-fern-sdk-group-name": "browser_session", + "x-fern-sdk-method-name": "get_browser_sessions", + }, + description="Get all active browser sessions for the organization", + summary="Get all active browser sessions", + responses={ + 200: {"description": "Successfully retrieved all active browser sessions"}, + 401: {"description": "Unauthorized - Invalid or missing authentication"}, + }, +) +@legacy_base_router.get( + "/browser_sessions", + response_model=list[BrowserSessionResponse], + tags=["session"], + openapi_extra={ + "x-fern-sdk-group-name": "browser_sessions", + "x-fern-sdk-method-name": "get_browser_sessions", + }, +) +@legacy_base_router.get( + "/browser_sessions/", + response_model=list[BrowserSessionResponse], + include_in_schema=False, +) +async def get_browser_sessions( + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> list[BrowserSessionResponse]: + """Get all active browser sessions for the organization""" + analytics.capture("skyvern-oss-agent-browser-sessions-get") + browser_sessions = await app.PERSISTENT_SESSIONS_MANAGER.get_active_sessions(current_org.organization_id) + return [BrowserSessionResponse.from_browser_session(browser_session) for browser_session in browser_sessions] + + +@base_router.post( + "/browser_sessions", + response_model=BrowserSessionResponse, + tags=["Browser Sessions"], + openapi_extra={ + "x-fern-sdk-group-name": "browser_session", + "x-fern-sdk-method-name": "create_browser_session", + }, + description="Create a new browser session", + summary="Create a new browser session", + responses={ + 200: {"description": "Successfully created browser session"}, + 401: {"description": "Unauthorized - Invalid or missing authentication"}, + }, +) +@legacy_base_router.post( + "/browser_sessions", + response_model=BrowserSessionResponse, + tags=["Browser Sessions"], + openapi_extra={ + "x-fern-sdk-group-name": "session", + "x-fern-sdk-method-name": "create_browser_session", + }, +) +@legacy_base_router.post( + "/browser_sessions/", + response_model=BrowserSessionResponse, + include_in_schema=False, +) +async def create_browser_session( + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> BrowserSessionResponse: + browser_session = await app.PERSISTENT_SESSIONS_MANAGER.create_session(current_org.organization_id) + return BrowserSessionResponse.from_browser_session(browser_session) + + +@base_router.post( + "/browser_sessions/{browser_session_id}/close", + tags=["Browser Sessions"], + openapi_extra={ + "x-fern-sdk-group-name": "browser_session", + "x-fern-sdk-method-name": "close_browser_session", + }, + description="Close a browser session", + summary="Close a browser session", + responses={ + 200: {"description": "Successfully closed browser session"}, + 401: {"description": "Unauthorized - Invalid or missing authentication"}, + }, +) +@legacy_base_router.post( + "/browser_sessions/{browser_session_id}/close", + tags=["Browser Sessions"], + openapi_extra={ + "x-fern-sdk-group-name": "browser_session", + "x-fern-sdk-method-name": "close_browser_session", + }, +) +@legacy_base_router.post( + "/browser_sessions/{browser_session_id}/close/", + include_in_schema=False, +) +async def close_browser_session( + browser_session_id: str, + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> ORJSONResponse: + await app.PERSISTENT_SESSIONS_MANAGER.close_session(current_org.organization_id, browser_session_id) + return ORJSONResponse( + content={"message": "Browser session closed"}, + status_code=200, + media_type="application/json", + ) diff --git a/skyvern/forge/sdk/routes/routers.py b/skyvern/forge/sdk/routes/routers.py new file mode 100644 index 00000000..7c42b712 --- /dev/null +++ b/skyvern/forge/sdk/routes/routers.py @@ -0,0 +1,5 @@ +from fastapi import APIRouter + +base_router = APIRouter() +legacy_base_router = APIRouter(include_in_schema=False) +legacy_v2_router = APIRouter(include_in_schema=False) diff --git a/skyvern/forge/sdk/routes/streaming.py b/skyvern/forge/sdk/routes/streaming.py index 746e815a..302ff5c3 100644 --- a/skyvern/forge/sdk/routes/streaming.py +++ b/skyvern/forge/sdk/routes/streaming.py @@ -3,21 +3,21 @@ import base64 from datetime import datetime import structlog -from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from fastapi import WebSocket, WebSocketDisconnect from pydantic import ValidationError from websockets.exceptions import ConnectionClosedOK from skyvern.forge import app +from skyvern.forge.sdk.routes.routers import legacy_base_router from skyvern.forge.sdk.schemas.tasks import TaskStatus from skyvern.forge.sdk.services.org_auth_service import get_current_org from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus LOG = structlog.get_logger() -websocket_router = APIRouter() STREAMING_TIMEOUT = 300 -@websocket_router.websocket("/tasks/{task_id}") +@legacy_base_router.websocket("/stream/tasks/{task_id}") async def task_stream( websocket: WebSocket, task_id: str, @@ -119,7 +119,7 @@ async def task_stream( return -@websocket_router.websocket("/workflow_runs/{workflow_run_id}") +@legacy_base_router.websocket("/stream/workflow_runs/{workflow_run_id}") async def workflow_run_streaming( websocket: WebSocket, workflow_run_id: str, diff --git a/skyvern/forge/sdk/routes/totp.py b/skyvern/forge/sdk/routes/totp.py index 70af41f9..58d722ec 100644 --- a/skyvern/forge/sdk/routes/totp.py +++ b/skyvern/forge/sdk/routes/totp.py @@ -1,25 +1,25 @@ import structlog -from fastapi import APIRouter, Depends, HTTPException +from fastapi import Depends, HTTPException from skyvern.forge import app from skyvern.forge.prompts import prompt_engine +from skyvern.forge.sdk.routes.routers import legacy_base_router from skyvern.forge.sdk.schemas.organizations import Organization from skyvern.forge.sdk.schemas.totp_codes import TOTPCode, TOTPCodeCreate from skyvern.forge.sdk.services import org_auth_service LOG = structlog.get_logger() -totp_router = APIRouter() -@totp_router.post( - "", +@legacy_base_router.post( + "/totp", tags=["agent"], openapi_extra={ "x-fern-sdk-group-name": "agent", "x-fern-sdk-method-name": "send_totp_code", }, ) -@totp_router.post("/", include_in_schema=False) +@legacy_base_router.post("/totp/", include_in_schema=False) async def send_totp_code( data: TOTPCodeCreate, curr_org: Organization = Depends(org_auth_service.get_current_org) ) -> TOTPCode: diff --git a/skyvern/forge/sdk/workflow/models/workflow.py b/skyvern/forge/sdk/workflow/models/workflow.py index 4c3a4ee3..97e2cfa4 100644 --- a/skyvern/forge/sdk/workflow/models/workflow.py +++ b/skyvern/forge/sdk/workflow/models/workflow.py @@ -3,6 +3,7 @@ from enum import StrEnum from typing import Any, List from pydantic import BaseModel, field_validator +from typing_extensions import deprecated from skyvern.forge.sdk.schemas.files import FileInfo from skyvern.forge.sdk.schemas.task_v2 import TaskV2 @@ -13,6 +14,7 @@ from skyvern.schemas.runs import ProxyLocation from skyvern.utils.url_validators import validate_url +@deprecated("Use WorkflowRunRequest instead") class WorkflowRequestBody(BaseModel): data: dict[str, Any] | None = None proxy_location: ProxyLocation | None = None @@ -29,6 +31,7 @@ class WorkflowRequestBody(BaseModel): return validate_url(url) +@deprecated("Use WorkflowRunResponse instead") class RunWorkflowResponse(BaseModel): workflow_id: str workflow_run_id: str diff --git a/skyvern/schemas/runs.py b/skyvern/schemas/runs.py index 1817fa3e..86ede80d 100644 --- a/skyvern/schemas/runs.py +++ b/skyvern/schemas/runs.py @@ -9,12 +9,12 @@ from skyvern.utils.url_validators import validate_url class ProxyLocation(StrEnum): + RESIDENTIAL = "RESIDENTIAL" US_CA = "US-CA" US_NY = "US-NY" US_TX = "US-TX" US_FL = "US-FL" US_WA = "US-WA" - RESIDENTIAL = "RESIDENTIAL" RESIDENTIAL_ES = "RESIDENTIAL_ES" RESIDENTIAL_IE = "RESIDENTIAL_IE" RESIDENTIAL_GB = "RESIDENTIAL_GB" @@ -113,23 +113,56 @@ class RunStatus(StrEnum): class TaskRunRequest(BaseModel): - prompt: str - url: str | None = None - title: str | None = None - engine: RunEngine = RunEngine.skyvern_v2 - proxy_location: ProxyLocation | None = None - data_extraction_schema: dict | list | str | None = None - error_code_mapping: dict[str, str] | None = None - max_steps: int | None = None - webhook_url: str | None = None - totp_identifier: str | None = None - totp_url: str | None = None - browser_session_id: str | None = None - publish_workflow: bool = False + prompt: str = Field(description="The goal or task description for Skyvern to accomplish") + url: str | None = Field( + default=None, + description="The starting URL for the task. If not provided, Skyvern will attempt to determine an appropriate URL", + ) + title: str | None = Field(default=None, description="Optional title for the task") + engine: RunEngine = Field( + default=RunEngine.skyvern_v2, description="The Skyvern engine version to use for this task" + ) + proxy_location: ProxyLocation = Field( + default=ProxyLocation.RESIDENTIAL, description="Geographic Proxy location to route the browser traffic through" + ) + data_extraction_schema: dict | list | str | None = Field( + default=None, description="Schema defining what data should be extracted from the webpage" + ) + error_code_mapping: dict[str, str] | None = Field( + default=None, description="Custom mapping of error codes to error messages if Skyvern encounters an error" + ) + max_steps: int | None = Field( + default=None, description="Maximum number of steps the task can take before timing out" + ) + webhook_url: str | None = Field( + default=None, description="URL to send task status updates to after a run is finished" + ) + totp_identifier: str | None = Field( + default=None, + description="Identifier for TOTP (Time-based One-Time Password) authentication if codes are being pushed to Skyvern", + ) + totp_url: str | None = Field( + default=None, + description="URL for TOTP authentication setup if Skyvern should be polling endpoint for 2FA codes", + ) + browser_session_id: str | None = Field( + default=None, + description="ID of an existing browser session to reuse, having it continue from the current screen state", + ) + publish_workflow: bool = Field(default=False, description="Whether to publish this task as a reusable workflow. ") @field_validator("url", "webhook_url", "totp_url") @classmethod def validate_urls(cls, url: str | None) -> str | None: + """ + Validates that URLs provided to Skyvern are properly formatted. + + Args: + url: The URL for Skyvern to validate + + Returns: + The validated URL or None if no URL was provided + """ if url is None: return None @@ -137,13 +170,27 @@ class TaskRunRequest(BaseModel): class WorkflowRunRequest(BaseModel): - title: str | None = None - parameters: dict[str, Any] | None = None - proxy_location: ProxyLocation | None = None - webhook_url: str | None = None - totp_url: str | None = None - totp_identifier: str | None = None - browser_session_id: str | None = None + workflow_id: str = Field(description="ID of the workflow to run") + title: str | None = Field(default=None, description="Optional title for this workflow run") + parameters: dict[str, Any] = Field(default={}, description="Parameters to pass to the workflow") + proxy_location: ProxyLocation = Field( + default=ProxyLocation.RESIDENTIAL, description="Location of proxy to use for this workflow run" + ) + webhook_url: str | None = Field( + default=None, description="URL to send workflow status updates to after a run is finished" + ) + totp_url: str | None = Field( + default=None, + description="URL for TOTP authentication setup if Skyvern should be polling endpoint for 2FA codes", + ) + totp_identifier: str | None = Field( + default=None, + description="Identifier for TOTP (Time-based One-Time Password) authentication if codes are being pushed to Skyvern", + ) + browser_session_id: str | None = Field( + default=None, + description="ID of an existing browser session to reuse, having it continue from the current screen state", + ) @field_validator("webhook_url", "totp_url") @classmethod @@ -154,22 +201,30 @@ class WorkflowRunRequest(BaseModel): class BaseRunResponse(BaseModel): - run_id: str - status: RunStatus - output: dict | list | str | None = None - failure_reason: str | None = None - created_at: datetime - modified_at: datetime + run_id: str = Field(description="Unique identifier for this run") + status: RunStatus = Field(description="Current status of the run") + output: dict | list | str | None = Field( + default=None, description="Output data from the run, if any. Format depends on the schema in the input" + ) + failure_reason: str | None = Field(default=None, description="Reason for failure if the run failed") + created_at: datetime = Field(description="Timestamp when this run was created") + modified_at: datetime = Field(description="Timestamp when this run was last modified") class TaskRunResponse(BaseRunResponse): - run_type: Literal[RunType.task_v1, RunType.task_v2] - run_request: TaskRunRequest | None = None + run_type: Literal[RunType.task_v1, RunType.task_v2] = Field( + description="Type of task run - either task_v1 or task_v2" + ) + run_request: TaskRunRequest | None = Field( + default=None, description="The original request parameters used to start this task run" + ) class WorkflowRunResponse(BaseRunResponse): - run_type: Literal[RunType.workflow_run] - run_request: WorkflowRunRequest | None = None + run_type: Literal[RunType.workflow_run] = Field(description="Type of run - always workflow_run for workflow runs") + run_request: WorkflowRunRequest | None = Field( + default=None, description="The original request parameters used to start this workflow run" + ) RunResponse = Annotated[Union[TaskRunResponse, WorkflowRunResponse], Field(discriminator="run_type")] diff --git a/skyvern/webeye/persistent_sessions_manager.py b/skyvern/webeye/persistent_sessions_manager.py index 2ffab70c..da20f0fb 100644 --- a/skyvern/webeye/persistent_sessions_manager.py +++ b/skyvern/webeye/persistent_sessions_manager.py @@ -101,16 +101,16 @@ class PersistentSessionsManager: await self.database.mark_persistent_browser_session_deleted(session_id, organization_id) self._browser_sessions.pop(session_id, None) - async def close_session(self, organization_id: str, session_id: str) -> None: + async def close_session(self, organization_id: str, browser_session_id: str) -> None: """Close a specific browser session.""" - browser_session = self._browser_sessions.get(session_id) + browser_session = self._browser_sessions.get(browser_session_id) if browser_session: LOG.info( "Closing browser session", organization_id=organization_id, - session_id=session_id, + session_id=browser_session_id, ) - self._browser_sessions.pop(session_id, None) + self._browser_sessions.pop(browser_session_id, None) try: await browser_session.browser_state.close() @@ -118,23 +118,23 @@ class PersistentSessionsManager: LOG.info( "Browser context already closed", organization_id=organization_id, - session_id=session_id, + session_id=browser_session_id, ) except Exception: LOG.warning( "Error while closing browser session", organization_id=organization_id, - session_id=session_id, + session_id=browser_session_id, exc_info=True, ) else: LOG.info( "Browser session not found in memory, marking as deleted in database", organization_id=organization_id, - session_id=session_id, + session_id=browser_session_id, ) - await self.database.mark_persistent_browser_session_deleted(session_id, organization_id) + await self.database.mark_persistent_browser_session_deleted(browser_session_id, organization_id) async def close_all_sessions(self, organization_id: str) -> None: """Close all browser sessions for an organization.""" diff --git a/skyvern/webeye/schemas.py b/skyvern/webeye/schemas.py index 57d0a7bf..35910d49 100644 --- a/skyvern/webeye/schemas.py +++ b/skyvern/webeye/schemas.py @@ -2,24 +2,37 @@ from __future__ import annotations from datetime import datetime -from pydantic import BaseModel +from pydantic import BaseModel, Field from skyvern.forge.sdk.schemas.persistent_browser_sessions import PersistentBrowserSession class BrowserSessionResponse(BaseModel): - session_id: str - organization_id: str - runnable_type: str | None = None - runnable_id: str | None = None - created_at: datetime - modified_at: datetime - deleted_at: datetime | None = None + """Response model for browser session information.""" + + browser_session_id: str = Field(description="Unique identifier for the browser session") + organization_id: str = Field(description="ID of the organization that owns this session") + runnable_type: str | None = Field( + None, description="Type of runnable associated with this session (workflow, task etc)" + ) + runnable_id: str | None = Field(None, description="ID of the associated runnable") + created_at: datetime = Field(description="Timestamp when the session was created") + 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 def from_browser_session(cls, browser_session: PersistentBrowserSession) -> BrowserSessionResponse: + """ + Creates a BrowserSessionResponse from a PersistentBrowserSession object. + + Args: + browser_session: The persistent browser session to convert + + Returns: + BrowserSessionResponse: The converted response object + """ return cls( - session_id=browser_session.persistent_browser_session_id, + browser_session_id=browser_session.persistent_browser_session_id, organization_id=browser_session.organization_id, runnable_type=browser_session.runnable_type, runnable_id=browser_session.runnable_id,