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: if url is not None:
task_request.url = url 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: async def _arun_task_v2(self, user_prompt: str, url: str | None = None) -> TaskV2:
task_request = TaskV2Request(user_prompt=user_prompt, url=url) task_request = TaskV2Request(user_prompt=user_prompt, url=url)
@@ -72,7 +72,7 @@ class DispatchTask(SkyvernTaskBaseTool):
if url is not None: if url is not None:
task_request.url = url 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: async def _arun_task_v2(self, user_prompt: str, url: str | None = None) -> TaskV2:
task_request = TaskV2Request(user_prompt=user_prompt, url=url) task_request = TaskV2Request(user_prompt=user_prompt, url=url)

View File

@@ -104,7 +104,7 @@ class SkyvernTaskToolSpec(BaseToolSpec):
if url is not None: if url is not None:
task_request.url = url 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: 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) task_generation = await self._generate_v1_task_request(user_prompt=user_prompt)
@@ -112,7 +112,7 @@ class SkyvernTaskToolSpec(BaseToolSpec):
if url is not None: if url is not None:
task_request.url = url 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: async def get_task_v1(self, task_id: str) -> TaskResponse | None:
return await self.agent.get_task(task_id=task_id) return await self.agent.get_task(task_id=task_id)

View File

@@ -1,16 +1,9 @@
import asyncio import asyncio
import os
import subprocess
from typing import Any, cast
from dotenv import load_dotenv 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 import app
from skyvern.forge.sdk.core import security, skyvern_context 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.core.skyvern_context import SkyvernContext
from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
from skyvern.forge.sdk.schemas.organizations import Organization 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.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.services.org_auth_token_service import API_KEY_LIFETIME
from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus
from skyvern.schemas.runs import ProxyLocation, RunEngine, RunResponse, RunType, TaskRunResponse from skyvern.services import task_v2_service
from skyvern.services import run_service, task_v1_service, task_v2_service
from skyvern.utils import migrate_db from skyvern.utils import migrate_db
class SkyvernAgent: class SkyvernAgent:
def __init__( def __init__(self) -> None:
self, load_dotenv(".env")
base_url: str | None = None, migrate_db()
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")
async def _get_organization(self) -> Organization: async def _get_organization(self) -> Organization:
organization = await app.DATABASE.get_organization_by_domain("skyvern.local") organization = await app.DATABASE.get_organization_by_domain("skyvern.local")
@@ -92,7 +41,7 @@ class SkyvernAgent:
) )
return organization 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( org_auth_token = await app.DATABASE.get_valid_org_auth_token(
organization_id=organization.organization_id, organization_id=organization.organization_id,
token_type=OrganizationAuthTokenType.api, token_type=OrganizationAuthTokenType.api,
@@ -109,23 +58,13 @@ class SkyvernAgent:
status=TaskStatus.running, status=TaskStatus.running,
organization_id=organization.organization_id, 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( step, _, _ = await app.agent.execute_step(
organization=organization, organization=organization,
task=updated_task, task=updated_task,
step=step, step=step,
api_key=org_auth_token.token if org_auth_token else None, api_key=org_auth_token.token if org_auth_token else None,
) )
finally:
skyvern_context.reset()
async def _run_task_v2(self, organization: Organization, task_v2: TaskV2) -> None: async def _run_task_v2(self, organization: Organization, task_v2: TaskV2) -> None:
# mark task v2 as queued # mark task v2 as queued
@@ -146,15 +85,22 @@ class SkyvernAgent:
task_v2_id=task_v2.observer_cruise_id, task_v2_id=task_v2.observer_cruise_id,
) )
async def create_task_v1( async def create_task(
self, self,
task_request: TaskRequest, task_request: TaskRequest,
) -> CreateTaskResponse: ) -> CreateTaskResponse:
organization = await self._get_organization() organization = await self._get_organization()
created_task = await app.agent.create_task(task_request, organization.organization_id) 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) return CreateTaskResponse(task_id=created_task.task_id)
async def get_task( async def get_task(
@@ -192,12 +138,12 @@ class SkyvernAgent:
task=task, last_step=latest_step, failure_reason=failure_reason, need_browser_log=True task=task, last_step=latest_step, failure_reason=failure_reason, need_browser_log=True
) )
async def run_task_v1( async def run_task(
self, self,
task_request: TaskRequest, task_request: TaskRequest,
timeout_seconds: int = 600, timeout_seconds: int = 600,
) -> TaskResponse: ) -> TaskResponse:
created_task = await self.create_task_v1(task_request) created_task = await self.create_task(task_request)
async with asyncio.timeout(timeout_seconds): async with asyncio.timeout(timeout_seconds):
while True: while True:
@@ -241,148 +187,3 @@ class SkyvernAgent:
if refreshed_task_v2.status.is_final(): if refreshed_task_v2.status.is_final():
return refreshed_task_v2 return refreshed_task_v2
await asyncio.sleep(1) 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 import skyvern_context
from skyvern.forge.sdk.core.skyvern_context import SkyvernContext from skyvern.forge.sdk.core.skyvern_context import SkyvernContext
from skyvern.forge.sdk.db.exceptions import NotFoundError 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.routers import base_router, legacy_base_router, legacy_v2_router
from skyvern.forge.sdk.routes.streaming import websocket_router
from skyvern.forge.sdk.routes.totp import totp_router
LOG = structlog.get_logger() LOG = structlog.get_logger()
@@ -66,11 +64,9 @@ def get_agent_app() -> FastAPI:
allow_headers=["*"], allow_headers=["*"],
) )
app.include_router(official_api_router, prefix="/v1") app.include_router(base_router, prefix="/v1")
app.include_router(base_router, prefix="/api/v1") app.include_router(legacy_base_router, prefix="/api/v1")
app.include_router(v2_router, prefix="/api/v2") app.include_router(legacy_v2_router, prefix="/api/v2")
app.include_router(websocket_router, prefix="/api/v1/stream")
app.include_router(totp_router, prefix="/api/v1/totp")
app.openapi = custom_openapi app.openapi = custom_openapi
app.add_middleware( 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 structlog
import yaml import yaml
from fastapi import ( from fastapi import BackgroundTasks, Depends, Header, HTTPException, Query, Request, Response, UploadFile, status
APIRouter,
BackgroundTasks,
Depends,
Header,
HTTPException,
Query,
Request,
Response,
UploadFile,
status,
)
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from skyvern import analytics 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.db.enums import OrganizationAuthTokenType
from skyvern.forge.sdk.executor.factory import AsyncExecutorFactory from skyvern.forge.sdk.executor.factory import AsyncExecutorFactory
from skyvern.forge.sdk.models import Step 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.ai_suggestions import AISuggestionBase, AISuggestionRequest
from skyvern.forge.sdk.schemas.organizations import ( from skyvern.forge.sdk.schemas.organizations import (
GetOrganizationAPIKeysResponse, 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.schemas.runs import RunEngine, RunResponse, RunType, TaskRunRequest, TaskRunResponse
from skyvern.services import run_service, task_v1_service, task_v2_service from skyvern.services import run_service, task_v1_service, task_v2_service
from skyvern.webeye.actions.actions import Action 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() LOG = structlog.get_logger()
@@ -103,15 +88,16 @@ class AISuggestionType(str, Enum):
DATA_SCHEMA = "data_schema" DATA_SCHEMA = "data_schema"
@base_router.post( @legacy_base_router.post(
"/webhook", "/webhook",
tags=["server"], tags=["server"],
openapi_extra={ openapi_extra={
"x-fern-sdk-group-name": "server", "x-fern-sdk-group-name": "server",
"x-fern-sdk-method-name": "webhook", "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( async def webhook(
request: Request, request: Request,
x_skyvern_signature: Annotated[str | None, Header()] = None, x_skyvern_signature: Annotated[str | None, Header()] = None,
@@ -148,7 +134,7 @@ async def webhook(
return Response(content="webhook validation", status_code=200) return Response(content="webhook validation", status_code=200)
@base_router.get( @legacy_base_router.get(
"/heartbeat", "/heartbeat",
tags=["server"], tags=["server"],
openapi_extra={ openapi_extra={
@@ -156,7 +142,7 @@ async def webhook(
"x-fern-sdk-method-name": "heartbeat", "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: async def heartbeat() -> Response:
""" """
Check if the server is running. Check if the server is running.
@@ -164,7 +150,7 @@ async def heartbeat() -> Response:
return Response(content="Server is running.", status_code=200) return Response(content="Server is running.", status_code=200)
@base_router.post( @legacy_base_router.post(
"/tasks", "/tasks",
tags=["agent"], tags=["agent"],
response_model=CreateTaskResponse, response_model=CreateTaskResponse,
@@ -173,7 +159,7 @@ async def heartbeat() -> Response:
"x-fern-sdk-method-name": "run_task_v1", "x-fern-sdk-method-name": "run_task_v1",
}, },
) )
@base_router.post( @legacy_base_router.post(
"/tasks/", "/tasks/",
response_model=CreateTaskResponse, response_model=CreateTaskResponse,
include_in_schema=False, include_in_schema=False,
@@ -200,7 +186,7 @@ async def run_task_v1(
return CreateTaskResponse(task_id=created_task.task_id) return CreateTaskResponse(task_id=created_task.task_id)
@base_router.get( @legacy_base_router.get(
"/tasks/{task_id}", "/tasks/{task_id}",
tags=["agent"], tags=["agent"],
response_model=TaskResponse, response_model=TaskResponse,
@@ -209,7 +195,7 @@ async def run_task_v1(
"x-fern-sdk-method-name": "get_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( async def get_task_v1(
task_id: str, task_id: str,
current_org: Organization = Depends(org_auth_service.get_current_org), 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", "/tasks/{task_id}/cancel",
tags=["agent"], tags=["agent"],
openapi_extra={ openapi_extra={
@@ -256,7 +242,7 @@ async def get_task_v1(
"x-fern-sdk-method-name": "cancel_task", "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( async def cancel_task(
task_id: str, task_id: str,
current_org: Organization = Depends(org_auth_service.get_current_org), 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) 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", "/workflows/runs/{workflow_run_id}/cancel",
tags=["agent"], tags=["agent"],
openapi_extra={ openapi_extra={
@@ -284,7 +270,7 @@ async def cancel_task(
"x-fern-sdk-method-name": "cancel_workflow_run", "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( async def cancel_workflow_run(
workflow_run_id: str, workflow_run_id: str,
current_org: Organization = Depends(org_auth_service.get_current_org), 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) 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", "/tasks/{task_id}/retry_webhook",
tags=["agent"], tags=["agent"],
response_model=TaskResponse, response_model=TaskResponse,
@@ -325,7 +311,7 @@ async def cancel_workflow_run(
"x-fern-sdk-method-name": "retry_webhook", "x-fern-sdk-method-name": "retry_webhook",
}, },
) )
@base_router.post( @legacy_base_router.post(
"/tasks/{task_id}/retry_webhook/", "/tasks/{task_id}/retry_webhook/",
response_model=TaskResponse, response_model=TaskResponse,
include_in_schema=False, 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) return await app.agent.build_task_response(task=task_obj, last_step=latest_step)
@base_router.get( @legacy_base_router.get(
"/tasks", "/tasks",
tags=["agent"], tags=["agent"],
response_model=list[Task], response_model=list[Task],
@@ -363,7 +349,7 @@ async def retry_webhook(
"x-fern-sdk-method-name": "get_tasks", "x-fern-sdk-method-name": "get_tasks",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/tasks/", "/tasks/",
response_model=list[Task], response_model=list[Task],
include_in_schema=False, 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]) return ORJSONResponse([(await app.agent.build_task_response(task=task)).model_dump() for task in tasks])
@base_router.get( @legacy_base_router.get(
"/runs", "/runs",
tags=["agent"], tags=["agent"],
response_model=list[WorkflowRun | Task], response_model=list[WorkflowRun | Task],
@@ -420,7 +406,7 @@ async def get_tasks(
"x-fern-sdk-method-name": "get_runs", "x-fern-sdk-method-name": "get_runs",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/runs/", "/runs/",
response_model=list[WorkflowRun | Task], response_model=list[WorkflowRun | Task],
include_in_schema=False, include_in_schema=False,
@@ -441,7 +427,7 @@ async def get_runs(
return ORJSONResponse([run.model_dump() for run in runs]) return ORJSONResponse([run.model_dump() for run in runs])
@official_api_router.get( @base_router.get(
"/runs/{run_id}", "/runs/{run_id}",
tags=["agent"], tags=["agent"],
response_model=RunResponse, response_model=RunResponse,
@@ -450,7 +436,7 @@ async def get_runs(
"x-fern-sdk-method-name": "get_run", "x-fern-sdk-method-name": "get_run",
}, },
) )
@official_api_router.get( @base_router.get(
"/runs/{run_id}/", "/runs/{run_id}/",
response_model=RunResponse, response_model=RunResponse,
include_in_schema=False, include_in_schema=False,
@@ -468,7 +454,7 @@ async def get_run(
return run_response return run_response
@base_router.get( @legacy_base_router.get(
"/tasks/{task_id}/steps", "/tasks/{task_id}/steps",
tags=["agent"], tags=["agent"],
response_model=list[Step], response_model=list[Step],
@@ -477,7 +463,7 @@ async def get_run(
"x-fern-sdk-method-name": "get_steps", "x-fern-sdk-method-name": "get_steps",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/tasks/{task_id}/steps/", "/tasks/{task_id}/steps/",
response_model=list[Step], response_model=list[Step],
include_in_schema=False, include_in_schema=False,
@@ -496,7 +482,7 @@ async def get_steps(
return ORJSONResponse([step.model_dump(exclude_none=True) for step in 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", "/{entity_type}/{entity_id}/artifacts",
tags=["agent"], tags=["agent"],
response_model=list[Artifact], response_model=list[Artifact],
@@ -505,7 +491,7 @@ async def get_steps(
"x-fern-sdk-method-name": "get_artifacts", "x-fern-sdk-method-name": "get_artifacts",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/{entity_type}/{entity_id}/artifacts/", "/{entity_type}/{entity_id}/artifacts/",
response_model=list[Artifact], response_model=list[Artifact],
include_in_schema=False, include_in_schema=False,
@@ -560,7 +546,7 @@ async def get_artifacts(
return ORJSONResponse([artifact.model_dump() for artifact in artifacts]) return ORJSONResponse([artifact.model_dump() for artifact in artifacts])
@base_router.get( @legacy_base_router.get(
"/tasks/{task_id}/steps/{step_id}/artifacts", "/tasks/{task_id}/steps/{step_id}/artifacts",
tags=["agent"], tags=["agent"],
response_model=list[Artifact], response_model=list[Artifact],
@@ -569,7 +555,7 @@ async def get_artifacts(
"x-fern-sdk-method-name": "get_step_artifacts", "x-fern-sdk-method-name": "get_step_artifacts",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/tasks/{task_id}/steps/{step_id}/artifacts/", "/tasks/{task_id}/steps/{step_id}/artifacts/",
response_model=list[Artifact], response_model=list[Artifact],
include_in_schema=False, include_in_schema=False,
@@ -605,7 +591,7 @@ async def get_step_artifacts(
return ORJSONResponse([artifact.model_dump() for artifact in artifacts]) return ORJSONResponse([artifact.model_dump() for artifact in artifacts])
@base_router.get( @legacy_base_router.get(
"/tasks/{task_id}/actions", "/tasks/{task_id}/actions",
response_model=list[Action], response_model=list[Action],
tags=["agent"], tags=["agent"],
@@ -614,7 +600,7 @@ async def get_step_artifacts(
"x-fern-sdk-method-name": "get_actions", "x-fern-sdk-method-name": "get_actions",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/tasks/{task_id}/actions/", "/tasks/{task_id}/actions/",
response_model=list[Action], response_model=list[Action],
include_in_schema=False, include_in_schema=False,
@@ -628,7 +614,7 @@ async def get_actions(
return actions return actions
@base_router.post( @legacy_base_router.post(
"/workflows/{workflow_id}/run", "/workflows/{workflow_id}/run",
response_model=RunWorkflowResponse, response_model=RunWorkflowResponse,
tags=["agent"], tags=["agent"],
@@ -637,7 +623,7 @@ async def get_actions(
"x-fern-sdk-method-name": "run_workflow", "x-fern-sdk-method-name": "run_workflow",
}, },
) )
@base_router.post( @legacy_base_router.post(
"/workflows/{workflow_id}/run/", "/workflows/{workflow_id}/run/",
response_model=RunWorkflowResponse, response_model=RunWorkflowResponse,
include_in_schema=False, include_in_schema=False,
@@ -703,7 +689,7 @@ async def run_workflow(
) )
@base_router.get( @legacy_base_router.get(
"/workflows/runs", "/workflows/runs",
response_model=list[WorkflowRun], response_model=list[WorkflowRun],
tags=["agent"], tags=["agent"],
@@ -712,7 +698,7 @@ async def run_workflow(
"x-fern-sdk-method-name": "get_workflow_runs", "x-fern-sdk-method-name": "get_workflow_runs",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/workflows/runs/", "/workflows/runs/",
response_model=list[WorkflowRun], response_model=list[WorkflowRun],
include_in_schema=False, include_in_schema=False,
@@ -732,7 +718,7 @@ async def get_workflow_runs(
) )
@base_router.get( @legacy_base_router.get(
"/workflows/{workflow_id}/runs", "/workflows/{workflow_id}/runs",
response_model=list[WorkflowRun], response_model=list[WorkflowRun],
tags=["agent"], tags=["agent"],
@@ -741,7 +727,7 @@ async def get_workflow_runs(
"x-fern-sdk-method-name": "get_workflow_runs_by_id", "x-fern-sdk-method-name": "get_workflow_runs_by_id",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/workflows/{workflow_id}/runs/", "/workflows/{workflow_id}/runs/",
response_model=list[WorkflowRun], response_model=list[WorkflowRun],
include_in_schema=False, 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}", "/workflows/{workflow_id}/runs/{workflow_run_id}",
tags=["agent"], tags=["agent"],
openapi_extra={ openapi_extra={
@@ -771,7 +757,7 @@ async def get_workflow_runs_by_id(
"x-fern-sdk-method-name": "get_workflow_run_with_workflow_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}/", "/workflows/{workflow_id}/runs/{workflow_run_id}/",
include_in_schema=False, include_in_schema=False,
) )
@@ -797,7 +783,7 @@ async def get_workflow_run_with_workflow_id(
return return_dict return return_dict
@base_router.get( @legacy_base_router.get(
"/workflows/{workflow_id}/runs/{workflow_run_id}/timeline", "/workflows/{workflow_id}/runs/{workflow_run_id}/timeline",
tags=["agent"], tags=["agent"],
openapi_extra={ openapi_extra={
@@ -805,7 +791,7 @@ async def get_workflow_run_with_workflow_id(
"x-fern-sdk-method-name": "get_workflow_run_timeline", "x-fern-sdk-method-name": "get_workflow_run_timeline",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/workflows/{workflow_id}/runs/{workflow_run_id}/timeline/", "/workflows/{workflow_id}/runs/{workflow_run_id}/timeline/",
include_in_schema=False, 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) 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}", "/workflows/runs/{workflow_run_id}",
response_model=WorkflowRunResponse, response_model=WorkflowRunResponse,
tags=["agent"], tags=["agent"],
@@ -827,7 +813,7 @@ async def get_workflow_run_timeline(
"x-fern-sdk-method-name": "get_workflow_run", "x-fern-sdk-method-name": "get_workflow_run",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/workflows/runs/{workflow_run_id}/", "/workflows/runs/{workflow_run_id}/",
response_model=WorkflowRunResponse, response_model=WorkflowRunResponse,
include_in_schema=False, include_in_schema=False,
@@ -843,7 +829,7 @@ async def get_workflow_run(
) )
@base_router.post( @legacy_base_router.post(
"/workflows", "/workflows",
openapi_extra={ openapi_extra={
"requestBody": { "requestBody": {
@@ -856,7 +842,7 @@ async def get_workflow_run(
response_model=Workflow, response_model=Workflow,
tags=["agent"], tags=["agent"],
) )
@base_router.post( @legacy_base_router.post(
"/workflows/", "/workflows/",
openapi_extra={ openapi_extra={
"requestBody": { "requestBody": {
@@ -890,7 +876,7 @@ async def create_workflow(
raise FailedToCreateWorkflow(str(e)) raise FailedToCreateWorkflow(str(e))
@base_router.put( @legacy_base_router.put(
"/workflows/{workflow_permanent_id}", "/workflows/{workflow_permanent_id}",
openapi_extra={ openapi_extra={
"requestBody": { "requestBody": {
@@ -903,7 +889,7 @@ async def create_workflow(
response_model=Workflow, response_model=Workflow,
tags=["agent"], tags=["agent"],
) )
@base_router.put( @legacy_base_router.put(
"/workflows/{workflow_permanent_id}/", "/workflows/{workflow_permanent_id}/",
openapi_extra={ openapi_extra={
"requestBody": { "requestBody": {
@@ -945,7 +931,7 @@ async def update_workflow(
raise FailedToUpdateWorkflow(workflow_permanent_id, f"<{type(e).__name__}: {str(e)}>") raise FailedToUpdateWorkflow(workflow_permanent_id, f"<{type(e).__name__}: {str(e)}>")
@base_router.delete( @legacy_base_router.delete(
"/workflows/{workflow_permanent_id}", "/workflows/{workflow_permanent_id}",
tags=["agent"], tags=["agent"],
openapi_extra={ openapi_extra={
@@ -953,7 +939,7 @@ async def update_workflow(
"x-fern-sdk-method-name": "delete_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( async def delete_workflow(
workflow_permanent_id: str, workflow_permanent_id: str,
current_org: Organization = Depends(org_auth_service.get_current_org), 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) await app.WORKFLOW_SERVICE.delete_workflow_by_permanent_id(workflow_permanent_id, current_org.organization_id)
@base_router.get( @legacy_base_router.get(
"/workflows", "/workflows",
response_model=list[Workflow], response_model=list[Workflow],
tags=["agent"], tags=["agent"],
@@ -971,7 +957,7 @@ async def delete_workflow(
"x-fern-sdk-method-name": "get_workflows", "x-fern-sdk-method-name": "get_workflows",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/workflows/", "/workflows/",
response_model=list[Workflow], response_model=list[Workflow],
include_in_schema=False, include_in_schema=False,
@@ -1020,7 +1006,7 @@ async def get_workflows(
) )
@base_router.get( @legacy_base_router.get(
"/workflows/templates", "/workflows/templates",
response_model=list[Workflow], response_model=list[Workflow],
tags=["agent"], tags=["agent"],
@@ -1029,7 +1015,7 @@ async def get_workflows(
"x-fern-sdk-method-name": "get_workflow_templates", "x-fern-sdk-method-name": "get_workflow_templates",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/workflows/templates/", "/workflows/templates/",
response_model=list[Workflow], response_model=list[Workflow],
include_in_schema=False, include_in_schema=False,
@@ -1048,7 +1034,7 @@ async def get_workflow_templates() -> list[Workflow]:
return workflows return workflows
@base_router.get( @legacy_base_router.get(
"/workflows/{workflow_permanent_id}", "/workflows/{workflow_permanent_id}",
response_model=Workflow, response_model=Workflow,
tags=["agent"], tags=["agent"],
@@ -1057,7 +1043,7 @@ async def get_workflow_templates() -> list[Workflow]:
"x-fern-sdk-method-name": "get_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( async def get_workflow(
workflow_permanent_id: str, workflow_permanent_id: str,
version: int | None = None, version: int | None = None,
@@ -1076,7 +1062,7 @@ async def get_workflow(
) )
@base_router.post( @legacy_base_router.post(
"/suggest/{ai_suggestion_type}", "/suggest/{ai_suggestion_type}",
include_in_schema=False, include_in_schema=False,
tags=["agent"], tags=["agent"],
@@ -1085,7 +1071,7 @@ async def get_workflow(
"x-fern-sdk-method-name": "suggest", "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( async def suggest(
ai_suggestion_type: AISuggestionType, ai_suggestion_type: AISuggestionType,
data: AISuggestionRequest, data: AISuggestionRequest,
@@ -1114,7 +1100,7 @@ async def suggest(
raise HTTPException(status_code=400, detail="Failed to suggest data schema. Please try again later.") raise HTTPException(status_code=400, detail="Failed to suggest data schema. Please try again later.")
@base_router.post( @legacy_base_router.post(
"/generate/task", "/generate/task",
tags=["agent"], tags=["agent"],
openapi_extra={ openapi_extra={
@@ -1122,7 +1108,7 @@ async def suggest(
"x-fern-sdk-method-name": "generate_task", "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( async def generate_task(
data: GenerateTaskRequest, data: GenerateTaskRequest,
current_org: Organization = Depends(org_auth_service.get_current_org), 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", "/organizations",
tags=["server"], tags=["server"],
openapi_extra={ openapi_extra={
@@ -1142,7 +1128,7 @@ async def generate_task(
"x-fern-sdk-method-name": "update_organization", "x-fern-sdk-method-name": "update_organization",
}, },
) )
@base_router.put( @legacy_base_router.put(
"/organizations", "/organizations",
include_in_schema=False, include_in_schema=False,
) )
@@ -1156,7 +1142,7 @@ async def update_organization(
) )
@base_router.get( @legacy_base_router.get(
"/organizations", "/organizations",
tags=["server"], tags=["server"],
openapi_extra={ openapi_extra={
@@ -1164,7 +1150,7 @@ async def update_organization(
"x-fern-sdk-method-name": "get_organizations", "x-fern-sdk-method-name": "get_organizations",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/organizations/", "/organizations/",
include_in_schema=False, include_in_schema=False,
) )
@@ -1174,7 +1160,7 @@ async def get_organizations(
return GetOrganizationsResponse(organizations=[current_org]) return GetOrganizationsResponse(organizations=[current_org])
@base_router.get( @legacy_base_router.get(
"/organizations/{organization_id}/apikeys/", "/organizations/{organization_id}/apikeys/",
tags=["server"], tags=["server"],
openapi_extra={ openapi_extra={
@@ -1182,7 +1168,7 @@ async def get_organizations(
"x-fern-sdk-method-name": "get_api_keys", "x-fern-sdk-method-name": "get_api_keys",
}, },
) )
@base_router.get( @legacy_base_router.get(
"/organizations/{organization_id}/apikeys", "/organizations/{organization_id}/apikeys",
include_in_schema=False, include_in_schema=False,
) )
@@ -1215,7 +1201,7 @@ async def _validate_file_size(file: UploadFile) -> UploadFile:
return file return file
@base_router.post( @legacy_base_router.post(
"/upload_file", "/upload_file",
tags=["server"], tags=["server"],
openapi_extra={ openapi_extra={
@@ -1223,7 +1209,7 @@ async def _validate_file_size(file: UploadFile) -> UploadFile:
"x-fern-sdk-method-name": "upload_file", "x-fern-sdk-method-name": "upload_file",
}, },
) )
@base_router.post( @legacy_base_router.post(
"/upload_file/", "/upload_file/",
include_in_schema=False, include_in_schema=False,
) )
@@ -1268,7 +1254,7 @@ async def upload_file(
) )
@v2_router.post( @legacy_v2_router.post(
"/tasks", "/tasks",
tags=["agent"], tags=["agent"],
openapi_extra={ openapi_extra={
@@ -1276,7 +1262,7 @@ async def upload_file(
"x-fern-sdk-method-name": "run_task_v2", "x-fern-sdk-method-name": "run_task_v2",
}, },
) )
@v2_router.post( @legacy_v2_router.post(
"/tasks/", "/tasks/",
include_in_schema=False, include_in_schema=False,
) )
@@ -1327,7 +1313,7 @@ async def run_task_v2(
return task_v2.model_dump(by_alias=True) return task_v2.model_dump(by_alias=True)
@v2_router.get( @legacy_v2_router.get(
"/tasks/{task_id}", "/tasks/{task_id}",
tags=["agent"], tags=["agent"],
openapi_extra={ openapi_extra={
@@ -1335,7 +1321,7 @@ async def run_task_v2(
"x-fern-sdk-method-name": "get_task_v2", "x-fern-sdk-method-name": "get_task_v2",
}, },
) )
@v2_router.get( @legacy_v2_router.get(
"/tasks/{task_id}/", "/tasks/{task_id}/",
include_in_schema=False, include_in_schema=False,
) )
@@ -1349,102 +1335,6 @@ async def get_task_v2(
return task_v2.model_dump(by_alias=True) 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]: 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 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 return final_workflow_run_block_timeline
@official_api_router.post( @base_router.post(
"/tasks", "/tasks",
tags=["agent"], tags=["Agent"],
openapi_extra={ openapi_extra={
"x-fern-sdk-group-name": "agent", "x-fern-sdk-group-name": "agent",
"x-fern-sdk-method-name": "run_task", "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( async def run_task(
request: Request, request: Request,
background_tasks: BackgroundTasks, 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 from datetime import datetime
import structlog import structlog
from fastapi import APIRouter, WebSocket, WebSocketDisconnect from fastapi import WebSocket, WebSocketDisconnect
from pydantic import ValidationError from pydantic import ValidationError
from websockets.exceptions import ConnectionClosedOK from websockets.exceptions import ConnectionClosedOK
from skyvern.forge import app 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.schemas.tasks import TaskStatus
from skyvern.forge.sdk.services.org_auth_service import get_current_org from skyvern.forge.sdk.services.org_auth_service import get_current_org
from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus
LOG = structlog.get_logger() LOG = structlog.get_logger()
websocket_router = APIRouter()
STREAMING_TIMEOUT = 300 STREAMING_TIMEOUT = 300
@websocket_router.websocket("/tasks/{task_id}") @legacy_base_router.websocket("/stream/tasks/{task_id}")
async def task_stream( async def task_stream(
websocket: WebSocket, websocket: WebSocket,
task_id: str, task_id: str,
@@ -119,7 +119,7 @@ async def task_stream(
return return
@websocket_router.websocket("/workflow_runs/{workflow_run_id}") @legacy_base_router.websocket("/stream/workflow_runs/{workflow_run_id}")
async def workflow_run_streaming( async def workflow_run_streaming(
websocket: WebSocket, websocket: WebSocket,
workflow_run_id: str, workflow_run_id: str,

View File

@@ -1,25 +1,25 @@
import structlog import structlog
from fastapi import APIRouter, Depends, HTTPException from fastapi import Depends, HTTPException
from skyvern.forge import app from skyvern.forge import app
from skyvern.forge.prompts import prompt_engine 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.organizations import Organization
from skyvern.forge.sdk.schemas.totp_codes import TOTPCode, TOTPCodeCreate from skyvern.forge.sdk.schemas.totp_codes import TOTPCode, TOTPCodeCreate
from skyvern.forge.sdk.services import org_auth_service from skyvern.forge.sdk.services import org_auth_service
LOG = structlog.get_logger() LOG = structlog.get_logger()
totp_router = APIRouter()
@totp_router.post( @legacy_base_router.post(
"", "/totp",
tags=["agent"], tags=["agent"],
openapi_extra={ openapi_extra={
"x-fern-sdk-group-name": "agent", "x-fern-sdk-group-name": "agent",
"x-fern-sdk-method-name": "send_totp_code", "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( async def send_totp_code(
data: TOTPCodeCreate, curr_org: Organization = Depends(org_auth_service.get_current_org) data: TOTPCodeCreate, curr_org: Organization = Depends(org_auth_service.get_current_org)
) -> TOTPCode: ) -> TOTPCode:

View File

@@ -3,6 +3,7 @@ from enum import StrEnum
from typing import Any, List from typing import Any, List
from pydantic import BaseModel, field_validator from pydantic import BaseModel, field_validator
from typing_extensions import deprecated
from skyvern.forge.sdk.schemas.files import FileInfo from skyvern.forge.sdk.schemas.files import FileInfo
from skyvern.forge.sdk.schemas.task_v2 import TaskV2 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 from skyvern.utils.url_validators import validate_url
@deprecated("Use WorkflowRunRequest instead")
class WorkflowRequestBody(BaseModel): class WorkflowRequestBody(BaseModel):
data: dict[str, Any] | None = None data: dict[str, Any] | None = None
proxy_location: ProxyLocation | None = None proxy_location: ProxyLocation | None = None
@@ -29,6 +31,7 @@ class WorkflowRequestBody(BaseModel):
return validate_url(url) return validate_url(url)
@deprecated("Use WorkflowRunResponse instead")
class RunWorkflowResponse(BaseModel): class RunWorkflowResponse(BaseModel):
workflow_id: str workflow_id: str
workflow_run_id: str workflow_run_id: str

View File

@@ -9,12 +9,12 @@ from skyvern.utils.url_validators import validate_url
class ProxyLocation(StrEnum): class ProxyLocation(StrEnum):
RESIDENTIAL = "RESIDENTIAL"
US_CA = "US-CA" US_CA = "US-CA"
US_NY = "US-NY" US_NY = "US-NY"
US_TX = "US-TX" US_TX = "US-TX"
US_FL = "US-FL" US_FL = "US-FL"
US_WA = "US-WA" US_WA = "US-WA"
RESIDENTIAL = "RESIDENTIAL"
RESIDENTIAL_ES = "RESIDENTIAL_ES" RESIDENTIAL_ES = "RESIDENTIAL_ES"
RESIDENTIAL_IE = "RESIDENTIAL_IE" RESIDENTIAL_IE = "RESIDENTIAL_IE"
RESIDENTIAL_GB = "RESIDENTIAL_GB" RESIDENTIAL_GB = "RESIDENTIAL_GB"
@@ -113,23 +113,56 @@ class RunStatus(StrEnum):
class TaskRunRequest(BaseModel): class TaskRunRequest(BaseModel):
prompt: str prompt: str = Field(description="The goal or task description for Skyvern to accomplish")
url: str | None = None url: str | None = Field(
title: str | None = None default=None,
engine: RunEngine = RunEngine.skyvern_v2 description="The starting URL for the task. If not provided, Skyvern will attempt to determine an appropriate URL",
proxy_location: ProxyLocation | None = None )
data_extraction_schema: dict | list | str | None = None title: str | None = Field(default=None, description="Optional title for the task")
error_code_mapping: dict[str, str] | None = None engine: RunEngine = Field(
max_steps: int | None = None default=RunEngine.skyvern_v2, description="The Skyvern engine version to use for this task"
webhook_url: str | None = None )
totp_identifier: str | None = None proxy_location: ProxyLocation = Field(
totp_url: str | None = None default=ProxyLocation.RESIDENTIAL, description="Geographic Proxy location to route the browser traffic through"
browser_session_id: str | None = None )
publish_workflow: bool = False 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") @field_validator("url", "webhook_url", "totp_url")
@classmethod @classmethod
def validate_urls(cls, url: str | None) -> str | None: 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: if url is None:
return None return None
@@ -137,13 +170,27 @@ class TaskRunRequest(BaseModel):
class WorkflowRunRequest(BaseModel): class WorkflowRunRequest(BaseModel):
title: str | None = None workflow_id: str = Field(description="ID of the workflow to run")
parameters: dict[str, Any] | None = None title: str | None = Field(default=None, description="Optional title for this workflow run")
proxy_location: ProxyLocation | None = None parameters: dict[str, Any] = Field(default={}, description="Parameters to pass to the workflow")
webhook_url: str | None = None proxy_location: ProxyLocation = Field(
totp_url: str | None = None default=ProxyLocation.RESIDENTIAL, description="Location of proxy to use for this workflow run"
totp_identifier: str | None = None )
browser_session_id: str | None = None 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") @field_validator("webhook_url", "totp_url")
@classmethod @classmethod
@@ -154,22 +201,30 @@ class WorkflowRunRequest(BaseModel):
class BaseRunResponse(BaseModel): class BaseRunResponse(BaseModel):
run_id: str run_id: str = Field(description="Unique identifier for this run")
status: RunStatus status: RunStatus = Field(description="Current status of the run")
output: dict | list | str | None = None output: dict | list | str | None = Field(
failure_reason: str | None = None default=None, description="Output data from the run, if any. Format depends on the schema in the input"
created_at: datetime )
modified_at: datetime 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): class TaskRunResponse(BaseRunResponse):
run_type: Literal[RunType.task_v1, RunType.task_v2] run_type: Literal[RunType.task_v1, RunType.task_v2] = Field(
run_request: TaskRunRequest | None = None 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): class WorkflowRunResponse(BaseRunResponse):
run_type: Literal[RunType.workflow_run] run_type: Literal[RunType.workflow_run] = Field(description="Type of run - always workflow_run for workflow runs")
run_request: WorkflowRunRequest | None = None 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")] 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) await self.database.mark_persistent_browser_session_deleted(session_id, organization_id)
self._browser_sessions.pop(session_id, None) 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.""" """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: if browser_session:
LOG.info( LOG.info(
"Closing browser session", "Closing browser session",
organization_id=organization_id, 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: try:
await browser_session.browser_state.close() await browser_session.browser_state.close()
@@ -118,23 +118,23 @@ class PersistentSessionsManager:
LOG.info( LOG.info(
"Browser context already closed", "Browser context already closed",
organization_id=organization_id, organization_id=organization_id,
session_id=session_id, session_id=browser_session_id,
) )
except Exception: except Exception:
LOG.warning( LOG.warning(
"Error while closing browser session", "Error while closing browser session",
organization_id=organization_id, organization_id=organization_id,
session_id=session_id, session_id=browser_session_id,
exc_info=True, exc_info=True,
) )
else: else:
LOG.info( LOG.info(
"Browser session not found in memory, marking as deleted in database", "Browser session not found in memory, marking as deleted in database",
organization_id=organization_id, 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: async def close_all_sessions(self, organization_id: str) -> None:
"""Close all browser sessions for an organization.""" """Close all browser sessions for an organization."""

View File

@@ -2,24 +2,37 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel, Field
from skyvern.forge.sdk.schemas.persistent_browser_sessions import PersistentBrowserSession from skyvern.forge.sdk.schemas.persistent_browser_sessions import PersistentBrowserSession
class BrowserSessionResponse(BaseModel): class BrowserSessionResponse(BaseModel):
session_id: str """Response model for browser session information."""
organization_id: str
runnable_type: str | None = None browser_session_id: str = Field(description="Unique identifier for the browser session")
runnable_id: str | None = None organization_id: str = Field(description="ID of the organization that owns this session")
created_at: datetime runnable_type: str | None = Field(
modified_at: datetime None, description="Type of runnable associated with this session (workflow, task etc)"
deleted_at: datetime | None = None )
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 @classmethod
def from_browser_session(cls, browser_session: PersistentBrowserSession) -> BrowserSessionResponse: 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( 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, organization_id=browser_session.organization_id,
runnable_type=browser_session.runnable_type, runnable_type=browser_session.runnable_type,
runnable_id=browser_session.runnable_id, runnable_id=browser_session.runnable_id,