377 lines
13 KiB
Python
377 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from enum import StrEnum
|
|
from typing import Annotated, Any, Literal, Union
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
from skyvern.forge.sdk.schemas.files import FileInfo
|
|
from skyvern.schemas.docs.doc_examples import (
|
|
BROWSER_SESSION_ID_EXAMPLES,
|
|
ERROR_CODE_MAPPING_EXAMPLES,
|
|
TASK_PROMPT_EXAMPLES,
|
|
TASK_URL_EXAMPLES,
|
|
TOTP_IDENTIFIER_EXAMPLES,
|
|
TOTP_URL_EXAMPLES,
|
|
)
|
|
from skyvern.schemas.docs.doc_strings import (
|
|
BROWSER_SESSION_ID_DOC_STRING,
|
|
DATA_EXTRACTION_SCHEMA_DOC_STRING,
|
|
ERROR_CODE_MAPPING_DOC_STRING,
|
|
MAX_STEPS_DOC_STRING,
|
|
PROXY_LOCATION_DOC_STRING,
|
|
TASK_ENGINE_DOC_STRING,
|
|
TASK_PROMPT_DOC_STRING,
|
|
TASK_URL_DOC_STRING,
|
|
TOTP_IDENTIFIER_DOC_STRING,
|
|
TOTP_URL_DOC_STRING,
|
|
WEBHOOK_URL_DOC_STRING,
|
|
)
|
|
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_ES = "RESIDENTIAL_ES"
|
|
RESIDENTIAL_IE = "RESIDENTIAL_IE"
|
|
RESIDENTIAL_GB = "RESIDENTIAL_GB"
|
|
RESIDENTIAL_IN = "RESIDENTIAL_IN"
|
|
RESIDENTIAL_JP = "RESIDENTIAL_JP"
|
|
RESIDENTIAL_FR = "RESIDENTIAL_FR"
|
|
RESIDENTIAL_DE = "RESIDENTIAL_DE"
|
|
RESIDENTIAL_NZ = "RESIDENTIAL_NZ"
|
|
RESIDENTIAL_ZA = "RESIDENTIAL_ZA"
|
|
RESIDENTIAL_AR = "RESIDENTIAL_AR"
|
|
RESIDENTIAL_ISP = "RESIDENTIAL_ISP"
|
|
NONE = "NONE"
|
|
|
|
@staticmethod
|
|
def get_zone(proxy_location: ProxyLocation) -> str:
|
|
zone_mapping = {
|
|
ProxyLocation.US_CA: "california",
|
|
ProxyLocation.US_NY: "newyork",
|
|
ProxyLocation.US_TX: "texas",
|
|
ProxyLocation.US_FL: "florida",
|
|
ProxyLocation.US_WA: "washington",
|
|
ProxyLocation.RESIDENTIAL: "residential_long-country-us",
|
|
}
|
|
if proxy_location in zone_mapping:
|
|
return zone_mapping[proxy_location]
|
|
raise ValueError(f"No zone mapping for proxy location: {proxy_location}")
|
|
|
|
@classmethod
|
|
def residential_country_locations(cls) -> set[ProxyLocation]:
|
|
return {
|
|
cls.RESIDENTIAL,
|
|
cls.RESIDENTIAL_ES,
|
|
cls.RESIDENTIAL_IE,
|
|
cls.RESIDENTIAL_GB,
|
|
cls.RESIDENTIAL_IN,
|
|
cls.RESIDENTIAL_JP,
|
|
cls.RESIDENTIAL_FR,
|
|
cls.RESIDENTIAL_DE,
|
|
cls.RESIDENTIAL_NZ,
|
|
cls.RESIDENTIAL_ZA,
|
|
cls.RESIDENTIAL_AR,
|
|
}
|
|
|
|
@staticmethod
|
|
def get_proxy_count(proxy_location: ProxyLocation) -> int:
|
|
counts = {
|
|
ProxyLocation.RESIDENTIAL: 10000,
|
|
ProxyLocation.RESIDENTIAL_ES: 2000,
|
|
ProxyLocation.RESIDENTIAL_IE: 2000,
|
|
ProxyLocation.RESIDENTIAL_GB: 2000,
|
|
ProxyLocation.RESIDENTIAL_IN: 2000,
|
|
ProxyLocation.RESIDENTIAL_JP: 2000,
|
|
ProxyLocation.RESIDENTIAL_FR: 2000,
|
|
ProxyLocation.RESIDENTIAL_DE: 2000,
|
|
ProxyLocation.RESIDENTIAL_NZ: 2000,
|
|
ProxyLocation.RESIDENTIAL_ZA: 2000,
|
|
ProxyLocation.RESIDENTIAL_AR: 2000,
|
|
}
|
|
return counts.get(proxy_location, 10000)
|
|
|
|
@staticmethod
|
|
def get_country_code(proxy_location: ProxyLocation) -> str:
|
|
mapping = {
|
|
ProxyLocation.RESIDENTIAL: "US",
|
|
ProxyLocation.RESIDENTIAL_ES: "ES",
|
|
ProxyLocation.RESIDENTIAL_IE: "IE",
|
|
ProxyLocation.RESIDENTIAL_GB: "GB",
|
|
ProxyLocation.RESIDENTIAL_IN: "IN",
|
|
ProxyLocation.RESIDENTIAL_JP: "JP",
|
|
ProxyLocation.RESIDENTIAL_FR: "FR",
|
|
ProxyLocation.RESIDENTIAL_DE: "DE",
|
|
ProxyLocation.RESIDENTIAL_NZ: "NZ",
|
|
ProxyLocation.RESIDENTIAL_ZA: "ZA",
|
|
ProxyLocation.RESIDENTIAL_AR: "AR",
|
|
}
|
|
return mapping.get(proxy_location, "US")
|
|
|
|
|
|
def get_tzinfo_from_proxy(proxy_location: ProxyLocation) -> ZoneInfo | None:
|
|
if proxy_location == ProxyLocation.NONE:
|
|
return None
|
|
|
|
if proxy_location == ProxyLocation.US_CA:
|
|
return ZoneInfo("America/Los_Angeles")
|
|
|
|
if proxy_location == ProxyLocation.US_NY:
|
|
return ZoneInfo("America/New_York")
|
|
|
|
if proxy_location == ProxyLocation.US_TX:
|
|
return ZoneInfo("America/Chicago")
|
|
|
|
if proxy_location == ProxyLocation.US_FL:
|
|
return ZoneInfo("America/New_York")
|
|
|
|
if proxy_location == ProxyLocation.US_WA:
|
|
return ZoneInfo("America/New_York")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL:
|
|
return ZoneInfo("America/New_York")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_ES:
|
|
return ZoneInfo("Europe/Madrid")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_IE:
|
|
return ZoneInfo("Europe/Dublin")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_GB:
|
|
return ZoneInfo("Europe/London")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_IN:
|
|
return ZoneInfo("Asia/Kolkata")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_JP:
|
|
return ZoneInfo("Asia/Tokyo")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_FR:
|
|
return ZoneInfo("Europe/Paris")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_DE:
|
|
return ZoneInfo("Europe/Berlin")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_NZ:
|
|
return ZoneInfo("Pacific/Auckland")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_ZA:
|
|
return ZoneInfo("Africa/Johannesburg")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_AR:
|
|
return ZoneInfo("America/Argentina/Buenos_Aires")
|
|
|
|
if proxy_location == ProxyLocation.RESIDENTIAL_ISP:
|
|
return ZoneInfo("America/New_York")
|
|
|
|
return None
|
|
|
|
|
|
class RunType(StrEnum):
|
|
task_v1 = "task_v1"
|
|
task_v2 = "task_v2"
|
|
workflow_run = "workflow_run"
|
|
openai_cua = "openai_cua"
|
|
anthropic_cua = "anthropic_cua"
|
|
|
|
|
|
class RunEngine(StrEnum):
|
|
skyvern_v1 = "skyvern-1.0"
|
|
skyvern_v2 = "skyvern-2.0"
|
|
openai_cua = "openai-cua"
|
|
anthropic_cua = "anthropic-cua"
|
|
|
|
|
|
CUA_ENGINES = [RunEngine.openai_cua, RunEngine.anthropic_cua]
|
|
CUA_RUN_TYPES = [RunType.openai_cua, RunType.anthropic_cua]
|
|
|
|
|
|
class RunStatus(StrEnum):
|
|
created = "created"
|
|
queued = "queued"
|
|
running = "running"
|
|
timed_out = "timed_out"
|
|
failed = "failed"
|
|
terminated = "terminated"
|
|
completed = "completed"
|
|
canceled = "canceled"
|
|
|
|
def is_final(self) -> bool:
|
|
return self in [self.failed, self.terminated, self.canceled, self.timed_out, self.completed]
|
|
|
|
|
|
class TaskRunRequest(BaseModel):
|
|
prompt: str = Field(
|
|
description=TASK_PROMPT_DOC_STRING,
|
|
examples=TASK_PROMPT_EXAMPLES,
|
|
)
|
|
url: str | None = Field(
|
|
default=None,
|
|
description=TASK_URL_DOC_STRING,
|
|
examples=TASK_URL_EXAMPLES,
|
|
)
|
|
engine: RunEngine = Field(
|
|
default=RunEngine.skyvern_v2,
|
|
description=TASK_ENGINE_DOC_STRING,
|
|
)
|
|
title: str | None = Field(
|
|
default=None, description="The title for the task", examples=["The title of my first skyvern task"]
|
|
)
|
|
proxy_location: ProxyLocation | None = Field(
|
|
default=ProxyLocation.RESIDENTIAL,
|
|
description=PROXY_LOCATION_DOC_STRING,
|
|
)
|
|
data_extraction_schema: dict | list | str | None = Field(
|
|
default=None,
|
|
description=DATA_EXTRACTION_SCHEMA_DOC_STRING,
|
|
)
|
|
error_code_mapping: dict[str, str] | None = Field(
|
|
default=None,
|
|
description=ERROR_CODE_MAPPING_DOC_STRING,
|
|
examples=ERROR_CODE_MAPPING_EXAMPLES,
|
|
)
|
|
max_steps: int | None = Field(
|
|
default=None,
|
|
description=MAX_STEPS_DOC_STRING,
|
|
examples=[10, 25],
|
|
)
|
|
webhook_url: str | None = Field(
|
|
default=None,
|
|
description=WEBHOOK_URL_DOC_STRING,
|
|
examples=["https://my-site.com/webhook"],
|
|
)
|
|
totp_identifier: str | None = Field(
|
|
default=None,
|
|
description=TOTP_IDENTIFIER_DOC_STRING,
|
|
examples=TOTP_IDENTIFIER_EXAMPLES,
|
|
)
|
|
totp_url: str | None = Field(
|
|
default=None,
|
|
description=TOTP_URL_DOC_STRING,
|
|
examples=TOTP_URL_EXAMPLES,
|
|
)
|
|
browser_session_id: str | None = Field(
|
|
default=None,
|
|
description=BROWSER_SESSION_ID_DOC_STRING,
|
|
examples=BROWSER_SESSION_ID_EXAMPLES,
|
|
)
|
|
publish_workflow: bool = Field(
|
|
default=False,
|
|
description="Whether to publish this task as a reusable workflow. Only available for skyvern-2.0.",
|
|
)
|
|
include_action_history_in_verification: bool | None = Field(
|
|
default=False, description="Whether to include action history when verifying that the task is complete"
|
|
)
|
|
|
|
@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
|
|
|
|
return validate_url(url)
|
|
|
|
|
|
class WorkflowRunRequest(BaseModel):
|
|
workflow_id: str = Field(
|
|
description="ID of the workflow to run. Workflow ID starts with `wpid_`.", examples=["wpid_123"]
|
|
)
|
|
parameters: dict[str, Any] = Field(default={}, description="Parameters to pass to the workflow")
|
|
title: str | None = Field(default=None, description="The title for this workflow run")
|
|
proxy_location: ProxyLocation | None = Field(
|
|
default=ProxyLocation.RESIDENTIAL,
|
|
description=PROXY_LOCATION_DOC_STRING,
|
|
)
|
|
webhook_url: str | None = Field(
|
|
default=None,
|
|
description="URL to send workflow status updates to after a run is finished. Refer to https://docs.skyvern.com/running-tasks/webhooks-faq for webhook questions.",
|
|
)
|
|
totp_url: str | None = Field(
|
|
default=None,
|
|
description=TOTP_URL_DOC_STRING,
|
|
examples=TOTP_URL_EXAMPLES,
|
|
)
|
|
totp_identifier: str | None = Field(
|
|
default=None,
|
|
description=TOTP_IDENTIFIER_DOC_STRING,
|
|
examples=TOTP_IDENTIFIER_EXAMPLES,
|
|
)
|
|
browser_session_id: str | None = Field(
|
|
default=None,
|
|
description="ID of a Skyvern browser session to reuse, having it continue from the current screen state",
|
|
)
|
|
|
|
@field_validator("webhook_url", "totp_url")
|
|
@classmethod
|
|
def validate_urls(cls, url: str | None) -> str | None:
|
|
if url is None:
|
|
return None
|
|
return validate_url(url)
|
|
|
|
|
|
class BaseRunResponse(BaseModel):
|
|
run_id: str = Field(
|
|
description="Unique identifier for this run. Run ID starts with `tsk_` for task runs and `wr_` for workflow runs.",
|
|
examples=["tsk_123", "tsk_v2_123", "wr_123"],
|
|
)
|
|
status: RunStatus = Field(
|
|
description="Current status of the run",
|
|
examples=["created", "queued", "running", "timed_out", "failed", "terminated", "completed", "canceled"],
|
|
)
|
|
output: dict | list | str | None = Field(
|
|
default=None,
|
|
description="Output data from the run, if any. Format/schema depends on the data extracted by the run.",
|
|
)
|
|
downloaded_files: list[FileInfo] | None = Field(default=None, description="List of files downloaded during the run")
|
|
recording_url: str | None = Field(default=None, description="URL to the recording of the run")
|
|
screenshot_urls: list[str] | None = Field(
|
|
default=None,
|
|
description="List of last n screenshot URLs in reverse chronological order - the first one the list is the latest screenshot.",
|
|
)
|
|
failure_reason: str | None = Field(default=None, description="Reason for failure if the run failed or terminated")
|
|
created_at: datetime = Field(description="Timestamp when this run was created", examples=["2025-01-01T00:00:00Z"])
|
|
modified_at: datetime = Field(
|
|
description="Timestamp when this run was last modified", examples=["2025-01-01T00:05:00Z"]
|
|
)
|
|
app_url: str | None = Field(
|
|
default=None,
|
|
description="URL to the application UI where the run can be viewed",
|
|
examples=["https://app.skyvern.com/tasks/tsk_123", "https://app.skyvern.com/workflows/wpid_123/wr_123"],
|
|
)
|
|
|
|
|
|
class TaskRunResponse(BaseRunResponse):
|
|
run_type: Literal[RunType.task_v1, RunType.task_v2, RunType.openai_cua, RunType.anthropic_cua] = Field(
|
|
description="Types of a task run - task_v1, task_v2, openai_cua, anthropic_cua"
|
|
)
|
|
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] = 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")]
|