CLI - skyvern run code (#3738)
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Any, List
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
import typer
|
import typer
|
||||||
@@ -12,14 +14,16 @@ from mcp.server.fastmcp import FastMCP
|
|||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.prompt import Confirm
|
from rich.prompt import Confirm
|
||||||
|
|
||||||
|
from skyvern.cli.console import console
|
||||||
from skyvern.cli.utils import start_services
|
from skyvern.cli.utils import start_services
|
||||||
from skyvern.config import settings
|
from skyvern.config import settings
|
||||||
|
from skyvern.forge.sdk.core import skyvern_context
|
||||||
|
from skyvern.forge.sdk.forge_log import setup_logger
|
||||||
from skyvern.library.skyvern import Skyvern
|
from skyvern.library.skyvern import Skyvern
|
||||||
|
from skyvern.services.script_service import run_script
|
||||||
from skyvern.utils import detect_os
|
from skyvern.utils import detect_os
|
||||||
from skyvern.utils.env_paths import resolve_backend_env_path, resolve_frontend_env_path
|
from skyvern.utils.env_paths import resolve_backend_env_path, resolve_frontend_env_path
|
||||||
|
|
||||||
from .console import console
|
|
||||||
|
|
||||||
run_app = typer.Typer(help="Commands to run Skyvern services such as the API server or UI.")
|
run_app = typer.Typer(help="Commands to run Skyvern services such as the API server or UI.")
|
||||||
|
|
||||||
mcp = FastMCP("Skyvern")
|
mcp = FastMCP("Skyvern")
|
||||||
@@ -163,3 +167,135 @@ def run_mcp() -> None:
|
|||||||
# This breaks the MCP processing because it expects json output only
|
# This breaks the MCP processing because it expects json output only
|
||||||
# console.print(Panel("[bold green]Starting MCP Server...[/bold green]", border_style="green"))
|
# console.print(Panel("[bold green]Starting MCP Server...[/bold green]", border_style="green"))
|
||||||
mcp.run(transport="stdio")
|
mcp.run(transport="stdio")
|
||||||
|
|
||||||
|
|
||||||
|
@run_app.command(
|
||||||
|
name="code",
|
||||||
|
context_settings={"allow_interspersed_args": False},
|
||||||
|
)
|
||||||
|
def run_code(
|
||||||
|
script_path: str = typer.Argument(..., help="Path to the Python script to run"),
|
||||||
|
params: List[str] = typer.Option([], "-p", help="Parameters in format param=value (without leading dash)"),
|
||||||
|
params_json: str = typer.Option(None, "--params", help="JSON string of parameters"),
|
||||||
|
params_file: str = typer.Option(None, "--params-file", help="Path to JSON file with parameters"),
|
||||||
|
ai: Optional[str] = typer.Option(
|
||||||
|
"fallback", "--ai", help="AI mode to use for the script. Options: fallback, proactive or None"
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""Run a Python script with parameters.
|
||||||
|
|
||||||
|
Supports three ways to pass parameters (in order of priority):
|
||||||
|
|
||||||
|
1. JSON file (highest priority):
|
||||||
|
skyvern run code main.py --params-file params.json
|
||||||
|
|
||||||
|
2. JSON string:
|
||||||
|
skyvern run code main.py --params '{"param1": "val1", "param2": "val2"}'
|
||||||
|
|
||||||
|
3. Individual flags (lowest priority):
|
||||||
|
skyvern run code main.py -p param1=val1 -p param2=val2
|
||||||
|
|
||||||
|
Note: For backward compatibility, leading dashes in -p values are automatically stripped.
|
||||||
|
"""
|
||||||
|
# Disable LiteLLM loggers
|
||||||
|
os.environ["LITELLM_LOG"] = "CRITICAL"
|
||||||
|
import litellm # noqa: PLC0415
|
||||||
|
|
||||||
|
litellm.suppress_debug_info = True
|
||||||
|
litellm.set_verbose = False
|
||||||
|
|
||||||
|
logging.getLogger("LiteLLM").setLevel(logging.CRITICAL)
|
||||||
|
logging.getLogger("LiteLLM Router").setLevel(logging.CRITICAL)
|
||||||
|
logging.getLogger("LiteLLM Proxy").setLevel(logging.CRITICAL)
|
||||||
|
settings.LOG_LEVEL = "CRITICAL"
|
||||||
|
setup_logger()
|
||||||
|
|
||||||
|
# Validate script path
|
||||||
|
if not script_path:
|
||||||
|
console.print("[red]❌ Error: No script path provided[/red]")
|
||||||
|
console.print("[yellow]→ Action: Provide a path to your Python script[/yellow]")
|
||||||
|
console.print("[blue]Example: skyvern run code main.py -p param1=value1[/blue]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
if not os.path.exists(script_path):
|
||||||
|
console.print("[red]❌ Error: Cannot find script file[/red]")
|
||||||
|
console.print(f"[yellow]→ Looked for: {script_path}[/yellow]")
|
||||||
|
console.print("[yellow]→ Action: Check that the file exists and the path is correct[/yellow]")
|
||||||
|
# Show current directory to help user understand relative paths
|
||||||
|
console.print(f"[blue]Current directory: {os.getcwd()}[/blue]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
if not script_path.endswith(".py"):
|
||||||
|
console.print("[red]❌ Error: Invalid file type[/red]")
|
||||||
|
console.print(f"[yellow]→ Provided: {script_path}[/yellow]")
|
||||||
|
console.print("[yellow]→ Action: Please provide a Python script file ending with .py[/yellow]")
|
||||||
|
console.print("[blue]Example: skyvern run code my_script.py[/blue]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
parameters = {}
|
||||||
|
|
||||||
|
# Priority: params_file > params_json > individual -p flags
|
||||||
|
if params_file:
|
||||||
|
try:
|
||||||
|
with open(params_file) as f:
|
||||||
|
parameters = json.load(f)
|
||||||
|
console.print(f"[blue]✓ Loaded parameters from file: {params_file}[/blue]")
|
||||||
|
except FileNotFoundError:
|
||||||
|
console.print("[red]❌ Error: Cannot find parameters file[/red]")
|
||||||
|
console.print(f"[yellow]→ Looked for: {params_file}[/yellow]")
|
||||||
|
console.print("[yellow]→ Action: Check that the file exists and the path is correct[/yellow]")
|
||||||
|
console.print(f"[blue]Current directory: {os.getcwd()}[/blue]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
console.print("[red]❌ Error: Invalid JSON format in parameters file[/red]")
|
||||||
|
console.print(f"[yellow]→ File: {params_file}[/yellow]")
|
||||||
|
console.print(f"[yellow]→ Details: {e}[/yellow]")
|
||||||
|
console.print("[yellow]→ Action: Fix the JSON syntax in your parameters file[/yellow]")
|
||||||
|
console.print('[blue]Expected format: {{"param1": "value1", "param2": "value2"}}[/blue]')
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
elif params_json:
|
||||||
|
try:
|
||||||
|
parameters = json.loads(params_json)
|
||||||
|
console.print("[blue]✓ Loaded parameters from JSON string[/blue]")
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
console.print("[red]❌ Error: Invalid JSON format in --params string[/red]")
|
||||||
|
console.print(f"[yellow]→ Details: {e}[/yellow]")
|
||||||
|
console.print("[yellow]→ Action: Check your JSON syntax (quotes, brackets, commas)[/yellow]")
|
||||||
|
console.print('[blue]Example: --params \'{{"param1": "value1", "param2": "value2"}}\'[/blue]')
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
elif params:
|
||||||
|
for param in params:
|
||||||
|
# Remove leading dash if present (for backward compatibility)
|
||||||
|
if param.startswith("-"):
|
||||||
|
param = param[1:]
|
||||||
|
|
||||||
|
if "=" in param:
|
||||||
|
key, value = param.split("=", 1)
|
||||||
|
parameters[key] = value
|
||||||
|
else:
|
||||||
|
console.print("[yellow]⚠️ Warning: Skipping invalid parameter format[/yellow]")
|
||||||
|
console.print(f"[yellow]→ Invalid: {param}[/yellow]")
|
||||||
|
console.print("[yellow]→ Expected format: -p param=value[/yellow]")
|
||||||
|
console.print("[blue]Example: -p download_start_date=31/07/2025[/blue]")
|
||||||
|
console.print("[blue]✓ Loaded parameters from command-line flags[/blue]")
|
||||||
|
|
||||||
|
console.print(Panel(f"[bold green]Running script: {script_path}[/bold green]", border_style="green"))
|
||||||
|
if parameters:
|
||||||
|
console.print("[blue]📋 Parameters:[/blue]")
|
||||||
|
console.print(f"[blue]{json.dumps(parameters, indent=2)}[/blue]")
|
||||||
|
else:
|
||||||
|
console.print("[blue]ℹ️ Running script without parameters[/blue]")
|
||||||
|
console.print("[dim]Tip: Add parameters with -p, --params, or --params-file[/dim]")
|
||||||
|
|
||||||
|
# set up skyvern context
|
||||||
|
|
||||||
|
skyvern_context.set(skyvern_context.SkyvernContext(script_mode=True, ai_mode_override=ai))
|
||||||
|
try:
|
||||||
|
asyncio.run(run_script(path=script_path, parameters=parameters))
|
||||||
|
console.print("✅ [green]Script execution completed successfully![/green]")
|
||||||
|
except Exception as e:
|
||||||
|
console.print("[red]❌ Error: Script execution failed[/red]")
|
||||||
|
console.print(f"[yellow]→ Script: {script_path}[/yellow]")
|
||||||
|
console.print(f"[yellow]→ Details: {e}[/yellow]")
|
||||||
|
console.print("[yellow]→ Action: Check the error message above and fix any issues in your script[/yellow]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|||||||
@@ -233,6 +233,21 @@ class SkyvernPage:
|
|||||||
and screenshot artifacts after action execution.
|
and screenshot artifacts after action execution.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Emoji mapping for different action types
|
||||||
|
ACTION_EMOJIS = {
|
||||||
|
ActionType.CLICK: "👆",
|
||||||
|
ActionType.INPUT_TEXT: "⌨️",
|
||||||
|
ActionType.UPLOAD_FILE: "📤",
|
||||||
|
ActionType.DOWNLOAD_FILE: "📥",
|
||||||
|
ActionType.SELECT_OPTION: "🎯",
|
||||||
|
ActionType.WAIT: "⏳",
|
||||||
|
ActionType.SOLVE_CAPTCHA: "🔓",
|
||||||
|
ActionType.VERIFICATION_CODE: "🔐",
|
||||||
|
ActionType.SCROLL: "📜",
|
||||||
|
ActionType.COMPLETE: "✅",
|
||||||
|
ActionType.TERMINATE: "🛑",
|
||||||
|
}
|
||||||
|
|
||||||
def decorator(fn: Callable) -> Callable:
|
def decorator(fn: Callable) -> Callable:
|
||||||
async def wrapper(
|
async def wrapper(
|
||||||
skyvern_page: SkyvernPage,
|
skyvern_page: SkyvernPage,
|
||||||
@@ -246,6 +261,17 @@ class SkyvernPage:
|
|||||||
|
|
||||||
action_status = ActionStatus.completed
|
action_status = ActionStatus.completed
|
||||||
|
|
||||||
|
# Print action in script mode
|
||||||
|
context = skyvern_context.current()
|
||||||
|
if context and context.script_mode:
|
||||||
|
emoji = ACTION_EMOJIS.get(action, "🔧")
|
||||||
|
action_name = action.value if hasattr(action, "value") else str(action)
|
||||||
|
print(f"{emoji} {action_name.replace('_', ' ').title()}", end="")
|
||||||
|
if intention:
|
||||||
|
print(f": {intention}")
|
||||||
|
else:
|
||||||
|
print()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
call.result = await fn(
|
call.result = await fn(
|
||||||
skyvern_page, *args, intention=intention, data=data, **kwargs
|
skyvern_page, *args, intention=intention, data=data, **kwargs
|
||||||
@@ -253,12 +279,20 @@ class SkyvernPage:
|
|||||||
|
|
||||||
# Note: Action status would be updated to completed here if update method existed
|
# Note: Action status would be updated to completed here if update method existed
|
||||||
|
|
||||||
|
# Print success in script mode
|
||||||
|
if context and context.script_mode:
|
||||||
|
print(" ✓ Completed")
|
||||||
|
|
||||||
return call.result
|
return call.result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
call.error = e
|
call.error = e
|
||||||
action_status = ActionStatus.failed
|
action_status = ActionStatus.failed
|
||||||
# Note: Action status would be updated to failed here if update method existed
|
# Note: Action status would be updated to failed here if update method existed
|
||||||
|
|
||||||
|
# Print failure in script mode
|
||||||
|
if context and context.script_mode:
|
||||||
|
print(f" ✗ Failed: {str(e)}")
|
||||||
|
|
||||||
# LLM fallback hook could go here ...
|
# LLM fallback hook could go here ...
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
@@ -282,11 +316,20 @@ class SkyvernPage:
|
|||||||
|
|
||||||
async def goto(self, url: str, timeout: float = settings.BROWSER_LOADING_TIMEOUT_MS) -> None:
|
async def goto(self, url: str, timeout: float = settings.BROWSER_LOADING_TIMEOUT_MS) -> None:
|
||||||
url = render_template(url)
|
url = render_template(url)
|
||||||
|
|
||||||
|
# Print navigation in script mode
|
||||||
|
context = skyvern_context.current()
|
||||||
|
if context and context.script_mode:
|
||||||
|
print(f"🌐 Navigating to: {url}")
|
||||||
|
|
||||||
await self.page.goto(
|
await self.page.goto(
|
||||||
url,
|
url,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if context and context.script_mode:
|
||||||
|
print(" ✓ Page loaded")
|
||||||
|
|
||||||
async def _update_action_reasoning(
|
async def _update_action_reasoning(
|
||||||
self,
|
self,
|
||||||
action_id: str,
|
action_id: str,
|
||||||
@@ -548,6 +591,9 @@ class SkyvernPage:
|
|||||||
If the prompt generation or parsing fails for any reason we fall back to
|
If the prompt generation or parsing fails for any reason we fall back to
|
||||||
clicking the originally supplied ``selector``.
|
clicking the originally supplied ``selector``.
|
||||||
"""
|
"""
|
||||||
|
context = skyvern_context.current()
|
||||||
|
if context and context.ai_mode_override:
|
||||||
|
ai = context.ai_mode_override
|
||||||
if ai == "fallback":
|
if ai == "fallback":
|
||||||
# try to click the element with the original selector first
|
# try to click the element with the original selector first
|
||||||
error_to_raise = None
|
error_to_raise = None
|
||||||
@@ -739,12 +785,16 @@ class SkyvernPage:
|
|||||||
If the prompt generation or parsing fails for any reason we fall back to
|
If the prompt generation or parsing fails for any reason we fall back to
|
||||||
inputting the originally supplied ``text``.
|
inputting the originally supplied ``text``.
|
||||||
"""
|
"""
|
||||||
|
context = skyvern_context.current()
|
||||||
|
if context and context.ai_mode_override:
|
||||||
|
ai = context.ai_mode_override
|
||||||
# format the text with the actual value of the parameter if it's a secret when running a workflow
|
# format the text with the actual value of the parameter if it's a secret when running a workflow
|
||||||
if ai == "fallback":
|
if ai == "fallback":
|
||||||
error_to_raise = None
|
error_to_raise = None
|
||||||
try:
|
try:
|
||||||
locator = self.page.locator(selector)
|
locator = self.page.locator(selector)
|
||||||
await handler_utils.input_sequentially(locator, value, timeout=timeout)
|
await handler_utils.input_sequentially(locator, value, timeout=timeout)
|
||||||
|
return value
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_to_raise = e
|
error_to_raise = e
|
||||||
|
|
||||||
@@ -820,6 +870,9 @@ class SkyvernPage:
|
|||||||
data: str | dict[str, Any] | None = None,
|
data: str | dict[str, Any] | None = None,
|
||||||
timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS,
|
timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
context = skyvern_context.current()
|
||||||
|
if context and context.ai_mode_override:
|
||||||
|
ai = context.ai_mode_override
|
||||||
if ai == "fallback":
|
if ai == "fallback":
|
||||||
error_to_raise = None
|
error_to_raise = None
|
||||||
try:
|
try:
|
||||||
@@ -927,12 +980,16 @@ class SkyvernPage:
|
|||||||
data: str | dict[str, Any] | None = None,
|
data: str | dict[str, Any] | None = None,
|
||||||
timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS,
|
timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
context = skyvern_context.current()
|
||||||
|
if context and context.ai_mode_override:
|
||||||
|
ai = context.ai_mode_override
|
||||||
value = value or ""
|
value = value or ""
|
||||||
if ai == "fallback":
|
if ai == "fallback":
|
||||||
error_to_raise = None
|
error_to_raise = None
|
||||||
try:
|
try:
|
||||||
locator = self.page.locator(selector)
|
locator = self.page.locator(selector)
|
||||||
await locator.select_option(value, timeout=timeout)
|
await locator.select_option(value, timeout=timeout)
|
||||||
|
return value
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_to_raise = e
|
error_to_raise = e
|
||||||
if intention:
|
if intention:
|
||||||
@@ -1070,6 +1127,18 @@ class SkyvernPage:
|
|||||||
screenshots=scraped_page_refreshed.screenshots,
|
screenshots=scraped_page_refreshed.screenshots,
|
||||||
prompt_name="extract-information",
|
prompt_name="extract-information",
|
||||||
)
|
)
|
||||||
|
if context and context.script_mode:
|
||||||
|
print(f"\n✨ 📊 Extracted Information:\n{'-' * 50}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Pretty print JSON if result is a dict/list
|
||||||
|
if isinstance(result, (dict, list)):
|
||||||
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||||
|
else:
|
||||||
|
print(result)
|
||||||
|
except Exception:
|
||||||
|
print(result)
|
||||||
|
print(f"{'-' * 50}\n")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@action_wrap(ActionType.VERIFICATION_CODE)
|
@action_wrap(ActionType.VERIFICATION_CODE)
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ class SkyvernContext:
|
|||||||
loop_metadata: dict[str, Any] | None = None
|
loop_metadata: dict[str, Any] | None = None
|
||||||
loop_output_values: list[dict[str, Any]] | None = None
|
loop_output_values: list[dict[str, Any]] | None = None
|
||||||
script_run_parameters: dict[str, Any] = field(default_factory=dict)
|
script_run_parameters: dict[str, Any] = field(default_factory=dict)
|
||||||
|
script_mode: bool = False
|
||||||
|
ai_mode_override: str | None = None
|
||||||
"""
|
"""
|
||||||
Example output value:
|
Example output value:
|
||||||
{"loop_value": "str", "output_parameter": "the key of the parameter", "output_value": Any}
|
{"loop_value": "str", "output_parameter": "the key of the parameter", "output_value": Any}
|
||||||
|
|||||||
@@ -1954,20 +1954,24 @@ async def goto(
|
|||||||
label: str | None = None,
|
label: str | None = None,
|
||||||
parameters: list[str] | None = None,
|
parameters: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
block_validation_output = await _validate_and_get_output_parameter(label, parameters)
|
try:
|
||||||
url = _render_template_with_label(url, label)
|
block_validation_output = await _validate_and_get_output_parameter(label, parameters)
|
||||||
goto_url_block = UrlBlock(
|
url = _render_template_with_label(url, label)
|
||||||
url=url,
|
goto_url_block = UrlBlock(
|
||||||
label=block_validation_output.label,
|
url=url,
|
||||||
output_parameter=block_validation_output.output_parameter,
|
label=block_validation_output.label,
|
||||||
parameters=block_validation_output.input_parameters,
|
output_parameter=block_validation_output.output_parameter,
|
||||||
)
|
parameters=block_validation_output.input_parameters,
|
||||||
await goto_url_block.execute_safe(
|
)
|
||||||
workflow_run_id=block_validation_output.workflow_run_id,
|
await goto_url_block.execute_safe(
|
||||||
parent_workflow_run_block_id=block_validation_output.context.parent_workflow_run_block_id,
|
workflow_run_id=block_validation_output.workflow_run_id,
|
||||||
organization_id=block_validation_output.organization_id,
|
parent_workflow_run_block_id=block_validation_output.context.parent_workflow_run_block_id,
|
||||||
browser_session_id=block_validation_output.browser_session_id,
|
organization_id=block_validation_output.organization_id,
|
||||||
)
|
browser_session_id=block_validation_output.browser_session_id,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
run_context = script_run_context_manager.ensure_run_context()
|
||||||
|
await run_context.page.goto(url)
|
||||||
|
|
||||||
|
|
||||||
async def prompt(
|
async def prompt(
|
||||||
|
|||||||
Reference in New Issue
Block a user