mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2025-09-17 02:39:40 +00:00
Update docs plus init (#2073)
Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com>
This commit is contained in:
parent
816d0e34d1
commit
ff57f9977c
11 changed files with 804 additions and 750 deletions
129
README.md
129
README.md
|
@ -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 |
|
||||||
|
|
53
integrations/mcp/README.md
Normal file
53
integrations/mcp/README.md
Normal 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
572
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
except Exception as e:
|
os.makedirs(os.path.dirname(path_windsurf_config), exist_ok=True)
|
||||||
print(f"Error setting up Cursor: {e}")
|
if not os.path.exists(path_windsurf_config):
|
||||||
return
|
with open(path_windsurf_config, "w") as f:
|
||||||
|
|
||||||
# Get command configuration
|
|
||||||
try:
|
|
||||||
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:
|
|
||||||
"""Set up Claude Desktop configuration for Skyvern MCP."""
|
|
||||||
if not is_claude_desktop_installed(host_system):
|
|
||||||
print("Claude Desktop is not installed. Skipping MCP setup.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get config file path
|
|
||||||
try:
|
|
||||||
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)
|
json.dump({"mcpServers": {}}, f, indent=2)
|
||||||
|
|
||||||
with open(path_claude_config, "r") as f:
|
windsurf_config: dict = {"mcpServers": {}}
|
||||||
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:
|
if os.path.exists(path_windsurf_config):
|
||||||
json.dump(claude_config, f, indent=2)
|
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
|
||||||
|
|
||||||
print("Claude Desktop configuration updated successfully.")
|
with open(path_windsurf_config, "w") as f:
|
||||||
|
json.dump(windsurf_config, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error configuring Windsurf: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Windsurf MCP configuration updated successfully at {path_windsurf_config}.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_mcp_server_url(deployment_type: str, host: str = "") -> str:
|
def setup_mcp_config() -> str:
|
||||||
"""Get the MCP server URL based on deployment type."""
|
"""
|
||||||
if deployment_type in ["local", "cloud"]:
|
return the path to the python environment
|
||||||
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")
|
python_path = shutil.which("python")
|
||||||
if python_path:
|
if python_path:
|
||||||
|
use_default = typer.prompt(f"Found Python at {python_path}. Use this path? (y/n)").lower() == "y"
|
||||||
|
if use_default:
|
||||||
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.")
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue