Rename old router to legacy_base_router (#2048)

Co-authored-by: Suchintan Singh <suchintansingh@gmail.com>
This commit is contained in:
Shuchang Zheng
2025-03-30 23:57:54 -07:00
committed by GitHub
parent 0107054a75
commit 83ad2adabd
15 changed files with 412 additions and 476 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.")

View File

@@ -1,2 +0,0 @@
DEFAULT_AGENT_TIMEOUT = 1800 # 30 minutes
DEFAULT_AGENT_HEARTBEAT_INTERVAL = 10 # 10 seconds

View File

@@ -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(

View File

@@ -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

View File

@@ -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,

View File

@@ -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",
)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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:

View File

@@ -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

View File

@@ -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")]

View File

@@ -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."""

View File

@@ -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,