Update docs plus init (#2073)

Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com>
This commit is contained in:
Suchintan 2025-04-03 00:46:57 -04:00 committed by GitHub
parent 816d0e34d1
commit ff57f9977c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 804 additions and 750 deletions

129
README.md
View file

@ -69,23 +69,37 @@ https://github.com/user-attachments/assets/5cab4668-e8e2-4982-8551-aab05ff73a7f
# Skyvern Cloud # Skyvern Cloud
We offer a managed cloud version of Skyvern that allows you to run Skyvern without having to manage the infrastructure. It allows you to run multiple Skyvern instances in parallel to automate your workflows at scale. In addition, Skyvern cloud comes bundled with anti-bot detection mechanisms, proxy network, and CAPTCHA solving to allow you to complete more complicated workflows. We offer a managed cloud version of Skyvern that allows you to run Skyvern without having to manage the infrastructure. It allows you to run multiple Skyvern instances in parallel and comes bundled with anti-bot detection mechanisms, proxy network, and CAPTCHA solvers.
If you'd like to try it out, If you'd like to try it out,
1. Navigate to [app.skyvern.com](https://app.skyvern.com) 1. Navigate to [app.skyvern.com](https://app.skyvern.com)
1. Create an account & Get $5 of credits on us 1. Create an account & Get $5 of credits on us
1. Kick off your first task and see Skyvern in action! 1. Kick off your first task and see Skyvern in action!
Here are some tips that may help you on your adventure:
1. Skyvern is really good at carrying out a single goal. If you give it too many instructions to do, it has a high likelihood of getting confused along the way.
2. Being really explicit about goals is very important. For example, if you're generating an insurance quote, let it know very clearly how it can identify it has accomplished its goals. Use words like "COMPLETE" or "TERMINATE" to indicate success and failure modes, respectively.
3. Workflows can be used if you'd like to do more advanced things such as chaining multiple instructions together, or securely logging in. If you need any help with this, please feel free to book some time with us! We're always happy to help
# Quickstart # Quickstart
This quickstart guide will walk you through getting Skyvern up and running on your local machine. This quickstart guide will walk you through getting Skyvern up and running on your local machine.
## Docker Compose setup (Recommended) ## Local
> ⚠️ **REQUIREMENT**: This project requires Python 3.11 ⚠️
1. **Install Skyvern**
```bash
pip install skyvern
```
2. **Configure Skyvern** Run the setup wizard which will guide you through the configuration process, including Skyvern [MCP](https://github.com/Skyvern-AI/skyvern/blob/main/integrations/mcp/README.md) integration. This will generate a `.env` as the configuration settings file.
```bash
skyvern init
```
3. **Launch the Skyvern Server**
```bash
skyvern run server
```
## Docker Compose setup
1. Make sure you have [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running on your machine 1. Make sure you have [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running on your machine
1. Make sure you don't have postgres running locally (Run `docker ps` to check) 1. Make sure you don't have postgres running locally (Run `docker ps` to check)
@ -97,66 +111,29 @@ This quickstart guide will walk you through getting Skyvern up and running on yo
``` ```
3. Navigate to `http://localhost:8080` in your browser to start using the UI 3. Navigate to `http://localhost:8080` in your browser to start using the UI
## Model Context Protocol (MCP)
See the MCP documentation [here](https://github.com/Skyvern-AI/skyvern/blob/main/integrations/mcp/README.md)
## Full Setup (Contributors) - Prerequisites ## Prompting Tips
### :warning: :warning: MAKE SURE YOU ARE USING PYTHON 3.11 :warning: :warning: Here are some tips that may help you on your adventure:
:warning: :warning: Only well-tested on MacOS :warning: :warning: 1. Skyvern is really good at carrying out a single goal. If you give it too many instructions to do, it has a high likelihood of hallucinating along the way.
2. Being really explicit about goals is very important. For example, if you're generating an insurance quote, let it know very clearly how it can identify it has accomplished its goals. Use words like "COMPLETE" or "TERMINATE" to indicate success and failure modes, respectively.
3. Workflows can be used if you'd like to do more advanced things such as chaining multiple instructions together, or securely logging in. If you need any help with this, please feel free to book some time with us! We're always happy to help
Before you begin, make sure you have the following installed:
- [Brew (if you're on a Mac)](https://brew.sh/)
- [Poetry](https://python-poetry.org/docs/#installation)
- `brew install poetry`
- [node](https://nodejs.org/en/download/)
- [Docker](https://docs.docker.com/engine/install/)
Note: Our setup script does these two for you, but they are here for reference.
- [Python 3.11](https://www.python.org/downloads/)
- `poetry env use 3.11`
- [PostgreSQL 14](https://www.postgresql.org/download/) (if you're on a Mac, setup script will install it for you if you have homebrew installed)
- `brew install postgresql`
## Setup (Contributors)
1. Clone the repository and navigate to the root directory
1. Open Docker Desktop (Works for Windows, macOS, and Linux) or run Docker Daemon
1. Run the setup script to install the necessary dependencies and setup your environment
```bash
./setup.sh
```
1. Start the server
```bash
./run_skyvern.sh
```
1. You can start sending requests to the server, but we built a simple UI to help you get started. To start the UI, run the following command:
```bash
./run_ui.sh
```
1. Navigate to `http://localhost:8080` in your browser to start using the UI
## Additional Setup for Contributors
If you're looking to contribute to Skyvern, you'll need to install the pre-commit hooks to ensure code quality and consistency. You can do this by running the following command:
```bash
pre-commit install
```
# Supported Functionality # Supported Functionality
## Skyvern 2.0 ## Skyvern 2.0
Skyvern 2.0 is a major overhaul of Skyvern that includes a multi-agent architecture with a planner + validator agent, allowing Skyvern to complete more complex tasks with a zero-shot prompt. Skyvern 2.0 is a major overhaul of Skyvern that includes a multi-agent architecture with a planner + validator agent, allowing Skyvern to complete more complex tasks with a zero-shot prompt.
<p align="center">
<img src="docs/images/skyvern_2_0_screenshot.png"/>
</p>
## Skyvern Tasks ## Skyvern Tasks
Tasks are the fundamental building block inside Skyvern. Each task is a single request to Skyvern, instructing it to navigate through a website and accomplish a specific goal. Tasks are the fundamental building block inside Skyvern. Each task is a single request to Skyvern, instructing it to navigate through a website and accomplish a specific goal.
Tasks require you to specify a `url`, `navigation_goal`, and optionally `data_extraction_goal` if you'd like to extract data from the website, and a `navigation_payload` if you'd like to provide additional context to help Skyvern fill information or answer questions presented by a website. Tasks require you to specify a `url`, `prompt`, and can optionally include a `data schema` (if you want the output to conform to a specific schema) and `error codes` (if you want Skyvern to stop running in specific situations).
<p align="center"> <p align="center">
<img src="docs/images/task_creation_form_example.png"/> <img src="docs/images/skyvern_2_0_screenshot.png"/>
</p> </p>
@ -268,9 +245,53 @@ We love to see how Skyvern is being used in the wild. Here are some examples of
<img src="docs/images/geico_shu_recording_cropped.gif"/> <img src="docs/images/geico_shu_recording_cropped.gif"/>
</p> </p>
# Contributor Setup
### Prerequisites
> :warning: :warning: MAKE SURE YOU ARE USING PYTHON 3.11 :warning: :warning:
:warning: :warning: Only well-tested on MacOS :warning: :warning:
Before you begin, make sure you have the following installed:
- [Brew (if you're on a Mac)](https://brew.sh/)
- [Poetry](https://python-poetry.org/docs/#installation)
- `brew install poetry`
- [node](https://nodejs.org/en/download/)
- [Docker](https://docs.docker.com/engine/install/)
Note: Our setup script does these two for you, but they are here for reference.
- [Python 3.11](https://www.python.org/downloads/)
- `poetry env use 3.11`
- [PostgreSQL 14](https://www.postgresql.org/download/) (if you're on a Mac, setup script will install it for you if you have homebrew installed)
- `brew install postgresql`
## Setup (Contributors)
1. Clone the repository and navigate to the root directory
1. Open Docker Desktop (Works for Windows, macOS, and Linux) or run Docker Daemon
1. Run the setup script to install the necessary dependencies and setup your environment
```bash
skyvern/scripts/setup.sh
```
1. Start the server
```bash
./run_skyvern.sh
```
1. You can start sending requests to the server, but we built a simple UI to help you get started. To start the UI, run the following command:
```bash
./run_ui.sh
```
1. Navigate to `http://localhost:8080` in your browser to start using the UI
## Additional Setup for Contributors
If you're looking to contribute to Skyvern, you'll need to install the pre-commit hooks to ensure code quality and consistency. You can do this by running the following command:
```bash
pre-commit install
```
# Documentation # Documentation
More extensive documentation can be found on our [documentation website](https://docs.skyvern.com). Please let us know if something is unclear or missing by opening an issue or reaching out to us [via email](mailto:founders@skyvern.com) or [discord](https://discord.gg/fG2XXEuQX3). More extensive documentation can be found on our [docs page](https://docs.skyvern.com). Please let us know if something is unclear or missing by opening an issue or reaching out to us [via email](mailto:founders@skyvern.com) or [discord](https://discord.gg/fG2XXEuQX3).
# Supported LLMs # Supported LLMs
| Provider | Supported Models | | Provider | Supported Models |

View file

@ -0,0 +1,53 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Model Context Protocol (MCP)](#model-context-protocol-mcp)
- [Integration Options](#integration-options)
- [Supported Applications](#supported-applications)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Model Context Protocol (MCP)
Skyvern provides an MCP server implementation that seamlessly integrates with applications so your application gets access to the browser, fetching any live information from the browser and take actions through Skyvern's browser agent.
## Integration Options
You can connect your MCP-enabled applications to Skyvern in two ways:
1. **Local Skyvern Server**
- Configure your applications to connect to skyvern server running on the localhost
- To run Skyvern server locally: `skyvern run server`
2. **Skyvern Cloud**
- Configure your applications to connect to Skyvern Cloud
- Create an account at [cloud.skyvern.com](https://cloud.skyvern.com)
- Get the API key from the settings page which will be used for setup
Follow the [installation instructions](#local) to set up.
## Supported Applications
- Cursor
- Windsurf
- Claude Desktop
`skyvern init` helps you set up the MCP config files for these supported applications automatically - no need to copy-paste the config files. In case you want to set up Skyvern for any other MCP-enabled application, here's the config:
```
{
"mcpServers": {
"Skyvern": {
"env": {
"SKYVERN_BASE_URL": "https://api.skyvern.com", # "http://localhost:8000" if running locally
"SKYVERN_API_KEY": "YOUR_SKYVERN_API_KEY" # find the local SKYVERN_API_KEY in the .env file after running `skyvern init`
},
"command": "PATH_TO_PYTHON",
"args": [
"-m",
"skyvern",
"run",
"mcp"
]
}
}
}
```

572
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -53,6 +53,7 @@ asyncpg = "^0.30.0"
json-repair = "^0.34.0" json-repair = "^0.34.0"
pypdf = "^5.1.0" pypdf = "^5.1.0"
fastmcp = "^0.4.1" fastmcp = "^0.4.1"
psutil = ">=7.0.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
isort = "^5.13.2" isort = "^5.13.2"
@ -133,7 +134,7 @@ skip = ["webeye/actions/__init__.py", "forge/sdk/__init__.py"]
plugins = "sqlalchemy.ext.mypy.plugin" plugins = "sqlalchemy.ext.mypy.plugin"
[tool.poetry.scripts] [tool.poetry.scripts]
skyvern = "skyvern.cli.commands:app" skyvern = "skyvern.cli.commands:cli_app"
[tool.codeflash] [tool.codeflash]
# All paths are relative to this pyproject.toml's directory. # All paths are relative to this pyproject.toml's directory.

View file

@ -1,4 +1,4 @@
from skyvern.cli.commands import app from skyvern.cli.commands import cli_app
if __name__ == "__main__": if __name__ == "__main__":
app() cli_app() # type: ignore

View file

@ -1,4 +1,5 @@
import asyncio import asyncio
import os
import subprocess import subprocess
from typing import Any, cast from typing import Any, cast
@ -35,21 +36,21 @@ class SkyvernAgent:
self.extra_headers = extra_headers self.extra_headers = extra_headers
self.client: SkyvernClient | None = None self.client: SkyvernClient | None = None
if base_url is None and api_key is None: if base_url is None and api_key is None:
# TODO: run at the root wherever the code is initiated if not os.path.exists(".env"):
raise Exception("No .env file found. Please run 'skyvern init' first to set up your environment.")
load_dotenv(".env") load_dotenv(".env")
migrate_db() migrate_db()
# TODO: will this change the already imported settings?
# TODO: maybe refresh the settings
self.cdp_url = cdp_url self.cdp_url = cdp_url
if browser_path: if browser_path:
# TODO validate browser_path # TODO validate browser_path
# Supported Browsers: Google Chrome, Brave Browser, Microsoft Edge, Firefox # Supported Browsers: Google Chrome, Brave Browser, Microsoft Edge, Firefox
if "Chrome" in browser_path or "Brave" in browser_path or "Edge" in browser_path: if "Chrome" in browser_path or "Brave" in browser_path or "Edge" in browser_path:
self.browser_process = subprocess.Popen( browser_process = subprocess.Popen(
[browser_path, "--remote-debugging-port=9222"], stdout=subprocess.PIPE, stderr=subprocess.PIPE [browser_path, "--remote-debugging-port=9222"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
) )
if self.browser_process.poll() is not None: if browser_process.poll() is not None:
raise Exception(f"Failed to open browser. browser_path: {browser_path}") raise Exception(f"Failed to open browser. browser_path: {browser_path}")
self.cdp_url = "http://127.0.0.1:9222" self.cdp_url = "http://127.0.0.1:9222"
@ -76,7 +77,7 @@ class SkyvernAgent:
else: else:
raise ValueError("base_url and api_key must be both provided") 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")
if not organization: if not organization:
organization = await app.DATABASE.create_organization( organization = await app.DATABASE.create_organization(
@ -154,7 +155,7 @@ class SkyvernAgent:
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)
@ -165,7 +166,7 @@ class SkyvernAgent:
self, self,
task_id: str, task_id: str,
) -> TaskResponse | None: ) -> TaskResponse | None:
organization = await self._get_organization() organization = await self.get_organization()
task = await app.DATABASE.get_task(task_id, organization.organization_id) task = await app.DATABASE.get_task(task_id, organization.organization_id)
if task is None: if task is None:
@ -212,7 +213,7 @@ class SkyvernAgent:
await asyncio.sleep(1) await asyncio.sleep(1)
async def observer_task_v_2(self, task_request: TaskV2Request) -> TaskV2: async def observer_task_v_2(self, task_request: TaskV2Request) -> TaskV2:
organization = await self._get_organization() organization = await self.get_organization()
task_v2 = await task_v2_service.initialize_task_v2( task_v2 = await task_v2_service.initialize_task_v2(
organization=organization, organization=organization,
@ -232,7 +233,7 @@ class SkyvernAgent:
return task_v2 return task_v2
async def get_observer_task_v_2(self, task_id: str) -> TaskV2 | None: async def get_observer_task_v_2(self, task_id: str) -> TaskV2 | None:
organization = await self._get_organization() organization = await self.get_organization()
return await app.DATABASE.get_task_v2(task_id, organization.organization_id) return await app.DATABASE.get_task_v2(task_id, organization.organization_id)
async def run_observer_task_v_2(self, task_request: TaskV2Request, timeout_seconds: int = 600) -> TaskV2: async def run_observer_task_v_2(self, task_request: TaskV2Request, timeout_seconds: int = 600) -> TaskV2:
@ -249,7 +250,7 @@ class SkyvernAgent:
############### officially supported interfaces ############### ############### officially supported interfaces ###############
async def get_run(self, run_id: str) -> RunResponse | None: async def get_run(self, run_id: str) -> RunResponse | None:
if not self.client: if not self.client:
organization = await self._get_organization() organization = await self.get_organization()
return await run_service.get_run_response(run_id, organization_id=organization.organization_id) return await run_service.get_run_response(run_id, organization_id=organization.organization_id)
return await self.client.get_run(run_id) return await self.client.get_run(run_id)
@ -276,7 +277,7 @@ class SkyvernAgent:
data_extraction_goal = None data_extraction_goal = None
navigation_goal = prompt navigation_goal = prompt
navigation_payload = None navigation_payload = None
organization = await self._get_organization() organization = await self.get_organization()
task_generation = await task_v1_service.generate_task( task_generation = await task_v1_service.generate_task(
user_prompt=prompt, user_prompt=prompt,
organization=organization, organization=organization,
@ -318,7 +319,7 @@ class SkyvernAgent:
return cast(TaskRunResponse, run_obj) return cast(TaskRunResponse, run_obj)
elif engine == RunEngine.skyvern_v2: elif engine == RunEngine.skyvern_v2:
# initialize task v2 # initialize task v2
organization = await self._get_organization() organization = await self.get_organization()
task_v2 = await task_v2_service.initialize_task_v2( task_v2 = await task_v2_service.initialize_task_v2(
organization=organization, organization=organization,

View file

@ -4,25 +4,27 @@ import os
import shutil import shutil
import subprocess import subprocess
import time import time
import uuid
from pathlib import Path
from typing import Optional from typing import Optional
import typer import typer
import uvicorn import uvicorn
from click import Choice from dotenv import load_dotenv, set_key
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from skyvern.agent import SkyvernAgent from skyvern.agent import SkyvernAgent
from skyvern.config import settings from skyvern.config import settings
from skyvern.schemas.runs import RunEngine from skyvern.forge import app
from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
from skyvern.utils import detect_os, get_windows_appdata_roaming, migrate_db from skyvern.utils import detect_os, get_windows_appdata_roaming, migrate_db
load_dotenv()
cli_app = typer.Typer()
run_app = typer.Typer()
cli_app.add_typer(run_app, name="run")
mcp = FastMCP("Skyvern") mcp = FastMCP("Skyvern")
skyvern_agent = SkyvernAgent(
base_url=settings.SKYVERN_BASE_URL,
api_key=settings.SKYVERN_API_KEY,
extra_headers={"X-User-Agent": "skyvern-mcp"},
)
@mcp.tool() @mcp.tool()
@ -33,17 +35,15 @@ async def skyvern_run_task(prompt: str, url: str) -> str:
prompt: brief description of what the user wants to accomplish prompt: brief description of what the user wants to accomplish
url: the target website for the user goal url: the target website for the user goal
""" """
res = await skyvern_agent.run_task(prompt=prompt, url=url, engine=RunEngine.skyvern_v1) skyvern_agent = SkyvernAgent(
base_url=settings.SKYVERN_BASE_URL,
api_key=settings.SKYVERN_API_KEY,
extra_headers={"X-User-Agent": "skyvern-mcp"},
)
res = await skyvern_agent.run_task(prompt=prompt, url=url)
return res.model_dump()["output"] return res.model_dump()["output"]
load_dotenv()
app = typer.Typer()
run_app = typer.Typer()
app.add_typer(run_app, name="run")
def command_exists(command: str) -> bool: def command_exists(command: str) -> bool:
return shutil.which(command) is not None return shutil.which(command) is not None
@ -141,33 +141,260 @@ def setup_postgresql() -> None:
print("Database and user created successfully.") print("Database and user created successfully.")
def update_or_add_env_var(key: str, value: str) -> None:
"""Update or add environment variable in .env file."""
env_path = Path(".env")
if not env_path.exists():
env_path.touch()
# Write default environment variables using dotenv
defaults = {
"ENV": "local",
"ENABLE_OPENAI": "false",
"OPENAI_API_KEY": "",
"ENABLE_ANTHROPIC": "false",
"ANTHROPIC_API_KEY": "",
"ENABLE_AZURE": "false",
"AZURE_DEPLOYMENT": "",
"AZURE_API_KEY": "",
"AZURE_API_BASE": "",
"AZURE_API_VERSION": "",
"ENABLE_AZURE_GPT4O_MINI": "false",
"AZURE_GPT4O_MINI_DEPLOYMENT": "",
"AZURE_GPT4O_MINI_API_KEY": "",
"AZURE_GPT4O_MINI_API_BASE": "",
"AZURE_GPT4O_MINI_API_VERSION": "",
"ENABLE_GEMINI": "false",
"GEMINI_API_KEY": "",
"ENABLE_NOVITA": "false",
"NOVITA_API_KEY": "",
"LLM_KEY": "",
"SECONDARY_LLM_KEY": "",
"BROWSER_TYPE": "chromium-headful",
"MAX_SCRAPING_RETRIES": "0",
"VIDEO_PATH": "./videos",
"BROWSER_ACTION_TIMEOUT_MS": "5000",
"MAX_STEPS_PER_RUN": "50",
"LOG_LEVEL": "INFO",
"DATABASE_STRING": "postgresql+psycopg://skyvern@localhost/skyvern",
"PORT": "8000",
"ANALYTICS_ID": "anonymous",
"ENABLE_LOG_ARTIFACTS": "false",
}
for k, v in defaults.items():
set_key(env_path, k, v)
load_dotenv(env_path)
set_key(env_path, key, value)
def setup_llm_providers() -> None:
"""Configure Large Language Model (LLM) Providers."""
print("Configuring Large Language Model (LLM) Providers...")
print("Note: All information provided here will be stored only on your local machine.")
model_options = []
# OpenAI Configuration
print("To enable OpenAI, you must have an OpenAI API key.")
enable_openai = input("Do you want to enable OpenAI (y/n)? ").lower() == "y"
if enable_openai:
openai_api_key = input("Enter your OpenAI API key: ")
if not openai_api_key:
print("Error: OpenAI API key is required.")
print("OpenAI will not be enabled.")
else:
update_or_add_env_var("OPENAI_API_KEY", openai_api_key)
update_or_add_env_var("ENABLE_OPENAI", "true")
model_options.extend(["OPENAI_GPT4O"])
else:
update_or_add_env_var("ENABLE_OPENAI", "false")
# Anthropic Configuration
print("To enable Anthropic, you must have an Anthropic API key.")
enable_anthropic = input("Do you want to enable Anthropic (y/n)? ").lower() == "y"
if enable_anthropic:
anthropic_api_key = input("Enter your Anthropic API key: ")
if not anthropic_api_key:
print("Error: Anthropic API key is required.")
print("Anthropic will not be enabled.")
else:
update_or_add_env_var("ANTHROPIC_API_KEY", anthropic_api_key)
update_or_add_env_var("ENABLE_ANTHROPIC", "true")
model_options.extend(
[
"ANTHROPIC_CLAUDE3_OPUS",
"ANTHROPIC_CLAUDE3_HAIKU",
"ANTHROPIC_CLAUDE3.5_SONNET",
"ANTHROPIC_CLAUDE3.7_SONNET",
]
)
else:
update_or_add_env_var("ENABLE_ANTHROPIC", "false")
# Azure Configuration
print("To enable Azure, you must have an Azure deployment name, API key, base URL, and API version.")
enable_azure = input("Do you want to enable Azure (y/n)? ").lower() == "y"
if enable_azure:
azure_deployment = input("Enter your Azure deployment name: ")
azure_api_key = input("Enter your Azure API key: ")
azure_api_base = input("Enter your Azure API base URL: ")
azure_api_version = input("Enter your Azure API version: ")
if not all([azure_deployment, azure_api_key, azure_api_base, azure_api_version]):
print("Error: All Azure fields must be populated.")
print("Azure will not be enabled.")
else:
update_or_add_env_var("AZURE_DEPLOYMENT", azure_deployment)
update_or_add_env_var("AZURE_API_KEY", azure_api_key)
update_or_add_env_var("AZURE_API_BASE", azure_api_base)
update_or_add_env_var("AZURE_API_VERSION", azure_api_version)
update_or_add_env_var("ENABLE_AZURE", "true")
model_options.append("AZURE_OPENAI_GPT4O")
else:
update_or_add_env_var("ENABLE_AZURE", "false")
# Gemini Configuration
print("To enable Gemini, you must have an Gemini API key.")
enable_gemini = input("Do you want to enable Gemini (y/n)? ").lower() == "y"
if enable_gemini:
gemini_api_key = input("Enter your Gemini API key: ")
if not gemini_api_key:
print("Error: Gemini API key is required.")
print("Gemini will not be enabled.")
else:
update_or_add_env_var("GEMINI_API_KEY", gemini_api_key)
update_or_add_env_var("ENABLE_GEMINI", "true")
model_options.extend(["GEMINI_FLASH_2_0", "GEMINI_FLASH_2_0_LITE", "GEMINI_PRO"])
else:
update_or_add_env_var("ENABLE_GEMINI", "false")
# Novita AI Configuration
print("To enable Novita AI, you must have an Novita AI API key.")
enable_novita = input("Do you want to enable Novita AI (y/n)? ").lower() == "y"
if enable_novita:
novita_api_key = input("Enter your Novita AI API key: ")
if not novita_api_key:
print("Error: Novita AI API key is required.")
print("Novita AI will not be enabled.")
else:
update_or_add_env_var("NOVITA_API_KEY", novita_api_key)
update_or_add_env_var("ENABLE_NOVITA", "true")
model_options.extend(
[
"NOVITA_DEEPSEEK_R1",
"NOVITA_DEEPSEEK_V3",
"NOVITA_LLAMA_3_3_70B",
"NOVITA_LLAMA_3_2_1B",
"NOVITA_LLAMA_3_2_3B",
"NOVITA_LLAMA_3_2_11B_VISION",
"NOVITA_LLAMA_3_1_8B",
"NOVITA_LLAMA_3_1_70B",
"NOVITA_LLAMA_3_1_405B",
"NOVITA_LLAMA_3_8B",
"NOVITA_LLAMA_3_70B",
]
)
else:
update_or_add_env_var("ENABLE_NOVITA", "false")
# Model Selection
if not model_options:
print(
"No LLM providers enabled. You won't be able to run Skyvern unless you enable at least one provider. You can re-run this script to enable providers or manually update the .env file."
)
else:
print("Available LLM models based on your selections:")
for i, model in enumerate(model_options, 1):
print(f"{i}. {model}")
while True:
try:
model_choice = int(input(f"Choose a model by number (e.g., 1 for {model_options[0]}): "))
if 1 <= model_choice <= len(model_options):
break
print(f"Please enter a number between 1 and {len(model_options)}")
except ValueError:
print("Please enter a valid number")
chosen_model = model_options[model_choice - 1]
print(f"Chosen LLM Model: {chosen_model}")
update_or_add_env_var("LLM_KEY", chosen_model)
print("LLM provider configurations updated in .env.")
def get_default_chrome_location(host_system: str) -> str:
"""Get the default Chrome/Chromium location based on OS."""
if host_system == "darwin":
return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
elif host_system == "linux":
# Common Linux locations
chrome_paths = ["/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser"]
for path in chrome_paths:
if os.path.exists(path):
return path
return "/usr/bin/google-chrome" # default if not found
elif host_system == "wsl":
return "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
else:
return "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
def setup_browser_config() -> tuple[str, Optional[str], Optional[str]]:
"""Configure browser settings for Skyvern."""
print("\nConfiguring web browser for scraping...")
browser_types = ["chromium-headless", "chromium-headful", "cdp-connect"]
for i, browser_type in enumerate(browser_types, 1):
print(f"{i}. {browser_type}")
if browser_type == "chromium-headless":
print(" - Runs Chrome in headless mode (no visible window)")
elif browser_type == "chromium-headful":
print(" - Runs Chrome with visible window")
elif browser_type == "cdp-connect":
print(" - Connects to an existing Chrome instance")
print(" - Requires Chrome to be running with remote debugging enabled")
while True:
try:
choice = int(input("\nChoose browser type (1-3): "))
if 1 <= choice <= len(browser_types):
selected_browser = browser_types[choice - 1]
break
print(f"Please enter a number between 1 and {len(browser_types)}")
except ValueError:
print("Please enter a valid number")
browser_location = None
remote_debugging_url = None
if selected_browser == "cdp-connect":
host_system = detect_os()
default_location = get_default_chrome_location(host_system)
print(f"\nDefault Chrome location for your system: {default_location}")
browser_location = input("Enter Chrome executable location (press Enter to use default): ").strip()
if not browser_location:
browser_location = default_location
if not os.path.exists(browser_location):
print(f"Warning: Chrome not found at {browser_location}. Please verify the location is correct.")
print("\nTo use CDP connection, Chrome must be running with remote debugging enabled.")
print("Example: chrome --remote-debugging-port=9222")
print("Default debugging URL: http://localhost:9222")
remote_debugging_url = input("Enter remote debugging URL (press Enter for default): ").strip()
if not remote_debugging_url:
remote_debugging_url = "http://localhost:9222"
return selected_browser, browser_location, remote_debugging_url
async def _setup_local_organization() -> str: async def _setup_local_organization() -> str:
""" """
Returns the API key for the local organization generated Returns the API key for the local organization generated
""" """
from skyvern.forge import app skyvern_agent = SkyvernAgent()
from skyvern.forge.sdk.core import security organization = await skyvern_agent.get_organization()
from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
from skyvern.forge.sdk.services.org_auth_token_service import API_KEY_LIFETIME
organization = await app.DATABASE.get_organization_by_domain("skyvern.local")
if not organization:
organization = await app.DATABASE.create_organization(
organization_name="Skyvern-local",
domain="skyvern.local",
max_steps_per_run=10,
max_retries_per_step=3,
)
api_key = security.create_access_token(
organization.organization_id,
expires_delta=API_KEY_LIFETIME,
)
# generate OrganizationAutoToken
await app.DATABASE.create_org_auth_token(
organization_id=organization.organization_id,
token=api_key,
token_type=OrganizationAuthTokenType.api,
)
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,
@ -175,25 +402,7 @@ async def _setup_local_organization() -> str:
return org_auth_token.token if org_auth_token else "" return org_auth_token.token if org_auth_token else ""
@app.command(name="init") @cli_app.command(name="migrate")
def init(
openai_api_key: str = typer.Option(..., help="The OpenAI API key"),
log_level: str = typer.Option("INFO", help="The log level"),
) -> None:
setup_postgresql()
api_key = asyncio.run(_setup_local_organization())
# Generate .env file
with open(".env", "w") as env_file:
env_file.write("LITELLM_LOG=ERROR\n")
env_file.write("ENABLE_OPENAI=true\n")
env_file.write(f"OPENAI_API_KEY={openai_api_key}\n")
env_file.write(f"LOG_LEVEL={log_level}\n")
env_file.write("ARTIFACT_STORAGE_PATH=./artifacts\n")
env_file.write(f"SKYVERN_API_KEY={api_key}\n")
print(".env file created with the parameters provided.")
@app.command(name="migrate")
def migrate() -> None: def migrate() -> None:
migrate_db() migrate_db()
@ -272,176 +481,91 @@ def is_cursor_installed(host_system: str) -> bool:
return False return False
def setup_cursor_mcp(host_system: str, path_to_env: str, path_to_server: str, env_vars: str) -> None: def is_windsurf_installed(host_system: str) -> bool:
"""Set up Cursor MCP configuration.""" """Check if Windsurf is installed by looking for its config directory."""
if not is_cursor_installed(host_system): try:
print("Cursor is not installed. Skipping Cursor MCP setup.") config_dir = os.path.expanduser("~/.codeium/windsurf")
return return os.path.exists(config_dir)
except Exception:
return False
def get_windsurf_config_path(host_system: str) -> str:
"""Get the Windsurf config file path for the current OS."""
return os.path.expanduser("~/.codeium/windsurf/mcp_config.json")
def setup_windsurf_config(host_system: str, path_to_env: str) -> bool:
"""Set up Windsurf configuration for Skyvern MCP."""
if not is_windsurf_installed(host_system):
return False
load_dotenv(".env")
skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "")
skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "")
if not skyvern_base_url or not skyvern_api_key:
print(
"Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in .env file to set up Windsurf MCP. Please open {path_windsurf_config} and set the these variables manually."
)
try: try:
path_cursor_config = get_cursor_config_path(host_system) path_windsurf_config = get_windsurf_config_path(host_system)
os.makedirs(os.path.dirname(path_windsurf_config), exist_ok=True)
if not os.path.exists(path_windsurf_config):
with open(path_windsurf_config, "w") as f:
json.dump({"mcpServers": {}}, f, indent=2)
windsurf_config: dict = {"mcpServers": {}}
if os.path.exists(path_windsurf_config):
try:
with open(path_windsurf_config, "r") as f:
windsurf_config = json.load(f)
windsurf_config["mcpServers"].pop("Skyvern", None)
windsurf_config["mcpServers"]["Skyvern"] = {
"env": {
"SKYVERN_BASE_URL": skyvern_base_url,
"SKYVERN_API_KEY": skyvern_api_key,
},
"command": path_to_env,
"args": ["-m", "skyvern", "run", "mcp"],
}
except json.JSONDecodeError:
print(
f"JSONDecodeError when reading Error configuring Windsurf. Please open {path_windsurf_config} and fix the json config first."
)
return False
with open(path_windsurf_config, "w") as f:
json.dump(windsurf_config, f, indent=2)
except Exception as e: except Exception as e:
print(f"Error setting up Cursor: {e}") print(f"Error configuring Windsurf: {e}")
return return False
# Get command configuration print(f"Windsurf MCP configuration updated successfully at {path_windsurf_config}.")
try: return True
command, args = get_claude_command_config(host_system, path_to_env, path_to_server, env_vars)
except Exception as e:
print(f"Error configuring Cursor command: {e}")
return
# Create or update Cursor config file
os.makedirs(os.path.dirname(path_cursor_config), exist_ok=True)
config = {"Skyvern": {"command": command, "args": args}}
if os.path.exists(path_cursor_config):
try:
with open(path_cursor_config, "r") as f:
existing_config = json.load(f)
existing_config.update(config)
config = existing_config
except json.JSONDecodeError:
pass # Use default config if file is corrupted
with open(path_cursor_config, "w") as f:
json.dump(config, f, indent=2)
print("Cursor MCP configuration updated successfully.")
def setup_claude_desktop(host_system: str, path_to_env: str, path_to_server: str) -> None: def setup_mcp_config() -> str:
"""Set up Claude Desktop configuration for Skyvern MCP.""" """
if not is_claude_desktop_installed(host_system): return the path to the python environment
print("Claude Desktop is not installed. Skipping MCP setup.") """
return python_path = shutil.which("python")
if python_path:
# Get config file path use_default = typer.prompt(f"Found Python at {python_path}. Use this path? (y/n)").lower() == "y"
try: if use_default:
path_claude_config = get_claude_config_path(host_system)
except Exception as e:
print(f"Error setting up Claude Desktop: {e}")
return
# Setup environment variables
env_vars = ""
for key in ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]:
value = os.getenv(key)
if value is None:
value = typer.prompt(f"Enter your {key}")
env_vars += f"{key}={value} "
# Get command configuration
try:
claude_command, claude_args = get_claude_command_config(host_system, path_to_env, path_to_server, env_vars)
except Exception as e:
print(f"Error configuring Claude Desktop command: {e}")
return
# Create or update Claude config file
os.makedirs(os.path.dirname(path_claude_config), exist_ok=True)
if not os.path.exists(path_claude_config):
with open(path_claude_config, "w") as f:
json.dump({"mcpServers": {}}, f, indent=2)
with open(path_claude_config, "r") as f:
claude_config = json.load(f)
claude_config["mcpServers"].pop("Skyvern", None)
claude_config["mcpServers"]["Skyvern"] = {"command": claude_command, "args": claude_args}
with open(path_claude_config, "w") as f:
json.dump(claude_config, f, indent=2)
print("Claude Desktop configuration updated successfully.")
def get_mcp_server_url(deployment_type: str, host: str = "") -> str:
"""Get the MCP server URL based on deployment type."""
if deployment_type in ["local", "cloud"]:
return os.path.join(os.path.abspath("./skyvern/mcp"), "server.py")
else:
raise ValueError(f"Invalid deployment type: {deployment_type}")
def setup_mcp_config(host_system: str, deployment_type: str, host: str = "") -> tuple[str, str]:
"""Set up MCP configuration based on deployment type."""
if deployment_type in ["local", "cloud"]:
# For local deployment, we need Python environment
python_path = shutil.which("python")
if python_path:
path_to_env = python_path path_to_env = python_path
else: else:
path_to_env = typer.prompt("Enter the full path to your configured python environment") path_to_env = typer.prompt("Enter the full path to your configured python environment")
return path_to_env, get_mcp_server_url(deployment_type) return path_to_env
else:
raise NotImplementedError()
def get_command_config(host_system: str, command: str, target: str, env_vars: str) -> tuple[str, list]: def setup_claude_desktop_config(host_system: str, path_to_env: str) -> bool:
"""Get the command and arguments for MCP configuration."""
base_env_vars = f"{env_vars} ENABLE_OPENAI=true LOG_LEVEL=CRITICAL"
artifacts_path = os.path.join(os.path.abspath("./"), "artifacts")
if host_system == "wsl":
env_vars = f"{base_env_vars} ARTIFACT_STORAGE_PATH={artifacts_path} BROWSER_TYPE=chromium-headless"
return "wsl.exe", ["bash", "-c", f"{env_vars} {command} {target}"]
if host_system in ["linux", "darwin"]:
env_vars = f"{base_env_vars} ARTIFACT_STORAGE_PATH={artifacts_path}"
if target.startswith("http"):
return command, ["-X", "POST", target]
return command, [target]
raise Exception(f"Unsupported host system: {host_system}")
@run_app.command(name="setupmcp")
def setup_mcp() -> None:
"""Configure MCP for different Skyvern deployments."""
host_system = detect_os()
# Prompt for deployment type
deployment_types = ["local", "cloud"]
deployment_type = typer.prompt("Select Skyvern deployment type", type=Choice(deployment_types), default="local")
try:
command, target = setup_mcp_config(host_system, deployment_type)
except Exception as e:
print(f"Error setting up MCP configuration: {e}")
return
# Cloud deployment variables
env_vars = ""
if deployment_type == "cloud":
for key in ["SKYVERN_MCP_CLOUD_URL", "SKYVERN_MCP_API_KEY"]:
value = os.getenv(key)
if value is None:
value = typer.prompt(f"Enter your {key}")
env_vars += f"{key}={value} "
# Setup environment variables
for key in ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]:
value = os.getenv(key)
if value is None:
value = typer.prompt(f"Enter your {key}")
env_vars += f"{key}={value} "
# Configure both Claude Desktop and Cursor
success = False
success |= setup_claude_desktop_config(host_system, command, target, env_vars)
success |= setup_cursor_config(host_system, command, target, env_vars)
if not success:
print("Neither Claude Desktop nor Cursor is installed. Please install at least one of them.")
def setup_claude_desktop_config(host_system: str, command: str, target: str, env_vars: str) -> bool:
"""Set up Claude Desktop configuration with given command and args.""" """Set up Claude Desktop configuration with given command and args."""
if not is_claude_desktop_installed(host_system): if not is_claude_desktop_installed(host_system):
return False return False
try: try:
claude_command, claude_args = get_command_config(host_system, command, target, env_vars)
path_claude_config = get_claude_config_path(host_system) path_claude_config = get_claude_config_path(host_system)
os.makedirs(os.path.dirname(path_claude_config), exist_ok=True) os.makedirs(os.path.dirname(path_claude_config), exist_ok=True)
@ -449,15 +573,30 @@ def setup_claude_desktop_config(host_system: str, command: str, target: str, env
with open(path_claude_config, "w") as f: with open(path_claude_config, "w") as f:
json.dump({"mcpServers": {}}, f, indent=2) json.dump({"mcpServers": {}}, f, indent=2)
# Read environment variables from .env file
load_dotenv(".env")
skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "")
skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "")
if not skyvern_base_url or not skyvern_api_key:
print("Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in .env file")
with open(path_claude_config, "r") as f: with open(path_claude_config, "r") as f:
claude_config = json.load(f) claude_config = json.load(f)
claude_config["mcpServers"].pop("Skyvern", None) claude_config["mcpServers"].pop("Skyvern", None)
claude_config["mcpServers"]["Skyvern"] = {"command": claude_command, "args": claude_args} claude_config["mcpServers"]["Skyvern"] = {
"env": {
"SKYVERN_BASE_URL": skyvern_base_url,
"SKYVERN_API_KEY": skyvern_api_key,
},
"command": path_to_env,
"args": ["-m", "skyvern", "run", "mcp"],
}
with open(path_claude_config, "w") as f: with open(path_claude_config, "w") as f:
json.dump(claude_config, f, indent=2) json.dump(claude_config, f, indent=2)
print("Claude Desktop configuration updated successfully.") print(f"Claude Desktop MCP configuration updated successfully at {path_claude_config}.")
return True return True
except Exception as e: except Exception as e:
@ -465,31 +604,53 @@ def setup_claude_desktop_config(host_system: str, command: str, target: str, env
return False return False
def setup_cursor_config(host_system: str, command: str, target: str, env_vars: str) -> bool: def setup_cursor_config(host_system: str, path_to_env: str) -> bool:
"""Set up Cursor configuration with given command and args.""" """Set up Cursor configuration with given command and args."""
if not is_cursor_installed(host_system): if not is_cursor_installed(host_system):
return False return False
try: try:
cursor_command, cursor_args = get_command_config(host_system, command, target, env_vars)
path_cursor_config = get_cursor_config_path(host_system) path_cursor_config = get_cursor_config_path(host_system)
os.makedirs(os.path.dirname(path_cursor_config), exist_ok=True) os.makedirs(os.path.dirname(path_cursor_config), exist_ok=True)
config = {"Skyvern": {"command": cursor_command, "args": cursor_args}} if not os.path.exists(path_cursor_config):
with open(path_cursor_config, "w") as f:
json.dump({"mcpServers": {}}, f, indent=2)
load_dotenv(".env")
skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "")
skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "")
if not skyvern_base_url or not skyvern_api_key:
print(
f"Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in .env file to set up Cursor MCP. Please open {path_cursor_config} and set the these variables manually."
)
cursor_config: dict = {"mcpServers": {}}
if os.path.exists(path_cursor_config): if os.path.exists(path_cursor_config):
try: try:
with open(path_cursor_config, "r") as f: with open(path_cursor_config, "r") as f:
existing_config = json.load(f) cursor_config = json.load(f)
existing_config.update(config) cursor_config["mcpServers"].pop("Skyvern", None)
config = existing_config cursor_config["mcpServers"]["Skyvern"] = {
"env": {
"SKYVERN_BASE_URL": skyvern_base_url,
"SKYVERN_API_KEY": skyvern_api_key,
},
"command": path_to_env,
"args": ["-m", "skyvern", "run", "mcp"],
}
except json.JSONDecodeError: except json.JSONDecodeError:
pass print(
f"JSONDecodeError when reading Error configuring Cursor. Please open {path_cursor_config} and fix the json config first."
)
return False
with open(path_cursor_config, "w") as f: with open(path_cursor_config, "w") as f:
json.dump(config, f, indent=2) json.dump(cursor_config, f, indent=2)
print(f"Cursor configuration updated successfully at {path_cursor_config}") print(f"Cursor MCP configuration updated successfully at {path_cursor_config}")
return True return True
except Exception as e: except Exception as e:
@ -497,9 +658,24 @@ def setup_cursor_config(host_system: str, command: str, target: str, env_vars: s
return False return False
def setup_mcp() -> None:
"""Configure MCP for different Skyvern deployments."""
host_system = detect_os()
path_to_env = setup_mcp_config()
# Configure both Claude Desktop and Cursor
setup_claude_desktop_config(host_system, path_to_env)
setup_cursor_config(host_system, path_to_env)
setup_windsurf_config(host_system, path_to_env)
@run_app.command(name="server") @run_app.command(name="server")
def run_server() -> None: def run_server() -> None:
port = int(os.environ.get("PORT", 8000)) load_dotenv()
from skyvern.config import settings
port = settings.PORT
uvicorn.run( uvicorn.run(
"skyvern.forge.api_app:app", "skyvern.forge.api_app:app",
host="0.0.0.0", host="0.0.0.0",
@ -512,3 +688,66 @@ def run_server() -> None:
def run_mcp() -> None: def run_mcp() -> None:
"""Run the MCP server.""" """Run the MCP server."""
mcp.run(transport="stdio") mcp.run(transport="stdio")
@cli_app.command(name="init")
def init() -> None:
run_local_str = (
input("Would you like to run Skyvern locally or in the cloud? (local/cloud) [cloud]: ").strip().lower()
)
run_local = run_local_str == "local" if run_local_str else False
if run_local:
setup_postgresql()
api_key = asyncio.run(_setup_local_organization())
if os.path.exists(".env"):
print(".env file already exists, skipping initialization.")
redo_llm_setup = input("Do you want to go through LLM provider setup again (y/n)? ")
if redo_llm_setup.lower() != "y":
return
print("Initializing .env file...")
setup_llm_providers()
# Configure browser settings
browser_type, browser_location, remote_debugging_url = setup_browser_config()
update_or_add_env_var("BROWSER_TYPE", browser_type)
if browser_location:
update_or_add_env_var("CHROME_EXECUTABLE_PATH", browser_location)
if remote_debugging_url:
update_or_add_env_var("BROWSER_REMOTE_DEBUGGING_URL", remote_debugging_url)
print("Defaulting Skyvern Base URL to: http://localhost:8000")
update_or_add_env_var("SKYVERN_BASE_URL", "http://localhost:8000")
else:
base_url = input("Enter Skyvern base URL (press Enter for https://api.skyvern.com): ").strip()
if not base_url:
base_url = "https://api.skyvern.com"
print("To get your API key:")
print("1. Create an account at https://app.skyvern.com")
print("2. Go to Settings")
print("3. Copy your API key")
api_key = input("Enter your Skyvern API key: ").strip()
if not api_key:
print("API key is required")
api_key = input("Enter your Skyvern API key: ").strip()
update_or_add_env_var("SKYVERN_BASE_URL", base_url)
# Ask for email or generate UUID
analytics_id = input("Please enter your email for analytics (press enter to skip): ")
if not analytics_id:
analytics_id = str(uuid.uuid4())
update_or_add_env_var("ANALYTICS_ID", analytics_id)
update_or_add_env_var("SKYVERN_API_KEY", api_key)
print(".env file has been initialized.")
# Ask if user wants to configure MCP server
configure_mcp = input("\nWould you like to configure the MCP server (y/n)? ").lower() == "y"
if configure_mcp:
setup_mcp()
print("\nMCP server configuration completed.")

View file

@ -11,6 +11,7 @@ class Settings(BaseSettings):
BROWSER_TYPE: str = "chromium-headful" BROWSER_TYPE: str = "chromium-headful"
BROWSER_REMOTE_DEBUGGING_URL: str = "http://127.0.0.1:9222" BROWSER_REMOTE_DEBUGGING_URL: str = "http://127.0.0.1:9222"
CHROME_EXECUTABLE_PATH: str | None = None
MAX_SCRAPING_RETRIES: int = 0 MAX_SCRAPING_RETRIES: int = 0
VIDEO_PATH: str | None = "./video" VIDEO_PATH: str | None = "./video"
HAR_PATH: str | None = "./har" HAR_PATH: str | None = "./har"

View file

@ -109,18 +109,6 @@ class ForgeAgent:
"Additional modules loaded", "Additional modules loaded",
modules=settings.ADDITIONAL_MODULES, modules=settings.ADDITIONAL_MODULES,
) )
LOG.info(
"Initializing ForgeAgent",
env=settings.ENV,
execute_all_steps=settings.EXECUTE_ALL_STEPS,
browser_type=settings.BROWSER_TYPE,
max_scraping_retries=settings.MAX_SCRAPING_RETRIES,
video_path=settings.VIDEO_PATH,
browser_action_timeout_ms=settings.BROWSER_ACTION_TIMEOUT_MS,
max_steps_per_run=settings.MAX_STEPS_PER_RUN,
long_running_task_warning_ratio=settings.LONG_RUNNING_TASK_WARNING_RATIO,
debug_mode=settings.DEBUG_MODE,
)
self.async_operation_pool = AsyncOperationPool() self.async_operation_pool = AsyncOperationPool()
async def create_task_and_step_from_block( async def create_task_and_step_from_block(

View file

@ -42,24 +42,6 @@ class LLMConfigRegistry:
return cls._configs[llm_key] return cls._configs[llm_key]
# if none of the LLM providers are enabled, raise an error
if not any(
[
settings.ENABLE_OPENAI,
settings.ENABLE_ANTHROPIC,
settings.ENABLE_AZURE,
settings.ENABLE_AZURE_GPT4O_MINI,
settings.ENABLE_BEDROCK,
settings.ENABLE_GEMINI,
settings.ENABLE_NOVITA,
]
):
LOG.warning(
"To run skyvern locally, you need to enable at least one LLM provider. Run setup.sh and follow through the LLM provider setup, or "
"update the .env file (check out .env.example to see the required environment variables)."
)
if settings.ENABLE_OPENAI: if settings.ENABLE_OPENAI:
LLMConfigRegistry.register_config( LLMConfigRegistry.register_config(
"OPENAI_GPT4_TURBO", "OPENAI_GPT4_TURBO",

View file

@ -2,6 +2,8 @@ from __future__ import annotations
import asyncio import asyncio
import os import os
import socket
import subprocess
import time import time
import uuid import uuid
from datetime import datetime from datetime import datetime
@ -9,6 +11,7 @@ from pathlib import Path
from typing import Any, Awaitable, Callable, Protocol from typing import Any, Awaitable, Callable, Protocol
import aiofiles import aiofiles
import psutil
import structlog import structlog
from playwright.async_api import BrowserContext, ConsoleMessage, Download, Page, Playwright from playwright.async_api import BrowserContext, ConsoleMessage, Download, Page, Playwright
from pydantic import BaseModel, PrivateAttr from pydantic import BaseModel, PrivateAttr
@ -303,6 +306,31 @@ def _get_cdp_port(kwargs: dict) -> int | None:
return None return None
def _is_port_in_use(port: int) -> bool:
"""Check if a port is already in use."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("localhost", port))
return False
except socket.error:
return True
def _is_chrome_running() -> bool:
"""Check if Chrome is already running."""
chrome_process_names = ["chrome", "google-chrome", "google chrome"]
for proc in psutil.process_iter(["name"]):
try:
proc_name = proc.info["name"].lower()
if proc_name == "chrome_crashpad_handler":
continue
if any(chrome_name in proc_name for chrome_name in chrome_process_names):
return True
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
return False
async def _create_headless_chromium( async def _create_headless_chromium(
playwright: Playwright, proxy_location: ProxyLocation | None = None, **kwargs: dict playwright: Playwright, proxy_location: ProxyLocation | None = None, **kwargs: dict
) -> tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]: ) -> tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]:
@ -352,6 +380,28 @@ async def _create_headful_chromium(
async def _create_cdp_connection_browser( async def _create_cdp_connection_browser(
playwright: Playwright, proxy_location: ProxyLocation | None = None, **kwargs: dict playwright: Playwright, proxy_location: ProxyLocation | None = None, **kwargs: dict
) -> tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]: ) -> tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]:
browser_type = settings.BROWSER_TYPE
browser_path = settings.CHROME_EXECUTABLE_PATH
if browser_type == "cdp-connect" and browser_path:
# First check if Chrome is already running
if _is_chrome_running():
raise Exception(
"Chrome is already running. Please close all Chrome instances before starting with remote debugging."
)
# Then check if the debugging port is already in use
if _is_port_in_use(9222):
raise Exception("Port 9222 is already in use. Another process may be using this port.")
browser_process = subprocess.Popen(
[browser_path, "--remote-debugging-port=9222"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
# Add small delay to allow browser to start
time.sleep(1)
if browser_process.poll() is not None:
raise Exception(f"Failed to open browser. browser_path: {browser_path}")
browser_args = BrowserContextFactory.build_browser_args() browser_args = BrowserContextFactory.build_browser_args()
browser_artifacts = BrowserContextFactory.build_browser_artifacts( browser_artifacts = BrowserContextFactory.build_browser_artifacts(