247 lines
11 KiB
Python
247 lines
11 KiB
Python
from typing import Annotated
|
|
|
|
import structlog
|
|
from fastapi import BackgroundTasks, Depends, Header, HTTPException, Request
|
|
|
|
from skyvern.config import settings
|
|
from skyvern.exceptions import MissingBrowserAddressError
|
|
from skyvern.forge import app
|
|
from skyvern.forge.sdk.core import skyvern_context
|
|
from skyvern.forge.sdk.routes.code_samples import (
|
|
LOGIN_CODE_SAMPLE_BITWARDEN_PYTHON,
|
|
LOGIN_CODE_SAMPLE_BITWARDEN_TS,
|
|
LOGIN_CODE_SAMPLE_ONEPASSWORD_PYTHON,
|
|
LOGIN_CODE_SAMPLE_ONEPASSWORD_TS,
|
|
LOGIN_CODE_SAMPLE_SKYVERN_PYTHON,
|
|
LOGIN_CODE_SAMPLE_SKYVERN_TS,
|
|
)
|
|
from skyvern.forge.sdk.routes.routers import base_router
|
|
from skyvern.forge.sdk.schemas.organizations import Organization
|
|
from skyvern.forge.sdk.services import org_auth_service
|
|
from skyvern.forge.sdk.workflow.models.parameter import WorkflowParameterType
|
|
from skyvern.forge.sdk.workflow.models.workflow import WorkflowRequestBody
|
|
from skyvern.schemas.run_blocks import CredentialType, LoginRequest
|
|
from skyvern.schemas.runs import ProxyLocation, RunType, WorkflowRunRequest, WorkflowRunResponse
|
|
from skyvern.schemas.workflows import (
|
|
AzureVaultCredentialParameterYAML,
|
|
BitwardenLoginCredentialParameterYAML,
|
|
LoginBlockYAML,
|
|
OnePasswordCredentialParameterYAML,
|
|
WorkflowCreateYAMLRequest,
|
|
WorkflowDefinitionYAML,
|
|
WorkflowParameterYAML,
|
|
WorkflowStatus,
|
|
)
|
|
from skyvern.services import workflow_service
|
|
from skyvern.utils.url_validators import prepend_scheme_and_validate_url
|
|
|
|
LOG = structlog.get_logger()
|
|
DEFAULT_LOGIN_PROMPT = """If you're not on the login page, navigate to login page and login using the credentials given.
|
|
First, take actions on promotional popups or cookie prompts that could prevent taking other action on the web page.
|
|
If a 2-factor step appears, enter the authentication code.
|
|
If you fail to login to find the login page or can't login after several trials, terminate.
|
|
If login is completed, you're successful."""
|
|
|
|
|
|
@base_router.post(
|
|
"/run/tasks/login",
|
|
tags=["Agent"],
|
|
response_model=WorkflowRunResponse,
|
|
openapi_extra={
|
|
"x-fern-sdk-method-name": "login",
|
|
"x-fern-examples": [
|
|
{
|
|
"code-samples": [
|
|
{"sdk": "python", "code": LOGIN_CODE_SAMPLE_SKYVERN_PYTHON},
|
|
{"sdk": "python", "code": LOGIN_CODE_SAMPLE_BITWARDEN_PYTHON},
|
|
{"sdk": "python", "code": LOGIN_CODE_SAMPLE_ONEPASSWORD_PYTHON},
|
|
{"sdk": "typescript", "code": LOGIN_CODE_SAMPLE_SKYVERN_TS},
|
|
{"sdk": "typescript", "code": LOGIN_CODE_SAMPLE_BITWARDEN_TS},
|
|
{"sdk": "typescript", "code": LOGIN_CODE_SAMPLE_ONEPASSWORD_TS},
|
|
]
|
|
}
|
|
],
|
|
},
|
|
description="Log in to a website using either credential stored in Skyvern, Bitwarden, 1Password, or Azure Vault",
|
|
summary="Login Task",
|
|
)
|
|
async def login(
|
|
request: Request,
|
|
background_tasks: BackgroundTasks,
|
|
login_request: LoginRequest,
|
|
organization: Organization = Depends(org_auth_service.get_current_org),
|
|
x_api_key: Annotated[str | None, Header()] = None,
|
|
) -> WorkflowRunResponse:
|
|
try:
|
|
url = prepend_scheme_and_validate_url(login_request.url) if login_request.url else None
|
|
totp_verification_url = (
|
|
prepend_scheme_and_validate_url(login_request.totp_url) if login_request.totp_url else None
|
|
)
|
|
webhook_url = prepend_scheme_and_validate_url(login_request.webhook_url) if login_request.webhook_url else None
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
|
# 1. create empty workflow with a credential parameter
|
|
new_workflow = await app.WORKFLOW_SERVICE.create_empty_workflow(
|
|
organization,
|
|
"Login",
|
|
proxy_location=login_request.proxy_location,
|
|
max_screenshot_scrolling_times=login_request.max_screenshot_scrolling_times,
|
|
extra_http_headers=login_request.extra_http_headers,
|
|
status=WorkflowStatus.auto_generated,
|
|
)
|
|
# 2. add a login block to the workflow
|
|
label = "login"
|
|
yaml_parameters = []
|
|
parameter_key = "credential"
|
|
if login_request.credential_type == CredentialType.skyvern:
|
|
if not login_request.credential_id:
|
|
raise HTTPException(status_code=400, detail="credential_id is required to login with Skyvern credential")
|
|
credential = await app.DATABASE.get_credential(login_request.credential_id, organization.organization_id)
|
|
if not credential:
|
|
raise HTTPException(status_code=404, detail=f"Credential {login_request.credential_id} not found")
|
|
|
|
yaml_parameters = [
|
|
WorkflowParameterYAML(
|
|
key=parameter_key,
|
|
workflow_parameter_type=WorkflowParameterType.CREDENTIAL_ID,
|
|
description="The ID of the credential to use for login",
|
|
default_value=login_request.credential_id,
|
|
)
|
|
]
|
|
elif login_request.credential_type == CredentialType.bitwarden:
|
|
yaml_parameters = [
|
|
BitwardenLoginCredentialParameterYAML(
|
|
key=parameter_key,
|
|
collection_id=login_request.bitwarden_collection_id,
|
|
item_id=login_request.bitwarden_item_id,
|
|
url=login_request.url,
|
|
description="The ID of the bitwarden collection to use for login",
|
|
bitwarden_client_id_aws_secret_key="SKYVERN_BITWARDEN_CLIENT_ID",
|
|
bitwarden_client_secret_aws_secret_key="SKYVERN_BITWARDEN_CLIENT_SECRET",
|
|
bitwarden_master_password_aws_secret_key="SKYVERN_BITWARDEN_MASTER_PASSWORD",
|
|
)
|
|
]
|
|
elif login_request.credential_type == CredentialType.onepassword:
|
|
if not login_request.onepassword_vault_id:
|
|
raise HTTPException(
|
|
status_code=400, detail="onepassword_vault_id is required to login with 1Password credential"
|
|
)
|
|
if not login_request.onepassword_item_id:
|
|
raise HTTPException(
|
|
status_code=400, detail="onepassword_item_id is required to login with 1Password credential"
|
|
)
|
|
yaml_parameters = [
|
|
OnePasswordCredentialParameterYAML(
|
|
key=parameter_key,
|
|
vault_id=login_request.onepassword_vault_id,
|
|
item_id=login_request.onepassword_item_id,
|
|
)
|
|
]
|
|
elif login_request.credential_type == CredentialType.azure_vault:
|
|
if not login_request.azure_vault_name:
|
|
raise HTTPException(
|
|
status_code=400, detail="azure_vault_name is required to login with Azure Vault credential"
|
|
)
|
|
if not login_request.azure_vault_username_key:
|
|
raise HTTPException(
|
|
status_code=400, detail="azure_vault_username_key is required to login with Azure Vault credential"
|
|
)
|
|
if not login_request.azure_vault_password_key:
|
|
raise HTTPException(
|
|
status_code=400, detail="azure_vault_password_key is required to login with Azure Vault credential"
|
|
)
|
|
yaml_parameters = [
|
|
AzureVaultCredentialParameterYAML(
|
|
key=parameter_key,
|
|
vault_name=login_request.azure_vault_name,
|
|
username_key=login_request.azure_vault_username_key,
|
|
password_key=login_request.azure_vault_password_key,
|
|
totp_secret_key=login_request.azure_vault_totp_secret_key,
|
|
)
|
|
]
|
|
|
|
login_block_yaml = LoginBlockYAML(
|
|
label=label,
|
|
title=label,
|
|
url=url,
|
|
navigation_goal=login_request.prompt or DEFAULT_LOGIN_PROMPT,
|
|
max_steps_per_run=10,
|
|
parameter_keys=[parameter_key],
|
|
totp_verification_url=totp_verification_url,
|
|
totp_identifier=login_request.totp_identifier,
|
|
)
|
|
yaml_blocks = [login_block_yaml]
|
|
workflow_definition_yaml = WorkflowDefinitionYAML(
|
|
parameters=yaml_parameters,
|
|
blocks=yaml_blocks,
|
|
)
|
|
workflow_create_request = WorkflowCreateYAMLRequest(
|
|
title=new_workflow.title,
|
|
description=new_workflow.description,
|
|
proxy_location=login_request.proxy_location or ProxyLocation.RESIDENTIAL,
|
|
workflow_definition=workflow_definition_yaml,
|
|
status=new_workflow.status,
|
|
max_screenshot_scrolls=login_request.max_screenshot_scrolling_times,
|
|
)
|
|
workflow = await app.WORKFLOW_SERVICE.create_workflow_from_request(
|
|
organization=organization,
|
|
request=workflow_create_request,
|
|
workflow_permanent_id=new_workflow.workflow_permanent_id,
|
|
)
|
|
LOG.info("Workflow created", workflow_id=workflow.workflow_id)
|
|
|
|
# 3. create and run workflow with the credential
|
|
workflow_id = new_workflow.workflow_permanent_id
|
|
context = skyvern_context.ensure_context()
|
|
request_id = context.request_id
|
|
legacy_workflow_request = WorkflowRequestBody(
|
|
proxy_location=login_request.proxy_location,
|
|
webhook_callback_url=webhook_url,
|
|
totp_identifier=login_request.totp_identifier,
|
|
totp_verification_url=totp_verification_url,
|
|
browser_session_id=login_request.browser_session_id,
|
|
browser_profile_id=login_request.browser_profile_id,
|
|
browser_address=login_request.browser_address,
|
|
max_screenshot_scrolls=login_request.max_screenshot_scrolling_times,
|
|
extra_http_headers=login_request.extra_http_headers,
|
|
)
|
|
|
|
try:
|
|
workflow_run = await workflow_service.run_workflow(
|
|
workflow_id=workflow_id,
|
|
organization=organization,
|
|
workflow_request=legacy_workflow_request,
|
|
template=False,
|
|
version=None,
|
|
api_key=x_api_key or None,
|
|
request_id=request_id,
|
|
request=request,
|
|
background_tasks=background_tasks,
|
|
)
|
|
except MissingBrowserAddressError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
|
return WorkflowRunResponse(
|
|
run_id=workflow_run.workflow_run_id,
|
|
run_type=RunType.workflow_run,
|
|
status=str(workflow_run.status),
|
|
failure_reason=workflow_run.failure_reason,
|
|
created_at=workflow_run.created_at,
|
|
modified_at=workflow_run.modified_at,
|
|
run_request=WorkflowRunRequest(
|
|
workflow_id=new_workflow.workflow_id,
|
|
title=new_workflow.title,
|
|
proxy_location=login_request.proxy_location,
|
|
webhook_url=webhook_url,
|
|
totp_url=totp_verification_url,
|
|
totp_identifier=login_request.totp_identifier,
|
|
browser_session_id=login_request.browser_session_id,
|
|
browser_profile_id=login_request.browser_profile_id,
|
|
max_screenshot_scrolls=login_request.max_screenshot_scrolling_times,
|
|
),
|
|
app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{workflow_run.workflow_run_id}",
|
|
browser_session_id=login_request.browser_session_id,
|
|
browser_profile_id=login_request.browser_profile_id,
|
|
)
|