diff --git a/skyvern/analytics.py b/skyvern/analytics.py index ae0a11d11..21d2bc0d8 100644 --- a/skyvern/analytics.py +++ b/skyvern/analytics.py @@ -6,7 +6,7 @@ import typer from posthog import Posthog -from skyvern.forge.sdk.settings_manager import SettingsManager +from skyvern.config import settings posthog = Posthog( "phc_bVT2ugnZhMHRWqMvSRHPdeTjaPxQqT3QSsI3r5FlQR5", @@ -31,7 +31,7 @@ def analytics_metadata() -> Dict[str, Any]: "machine": platform.machine(), "platform": platform.platform(), "python_version": platform.python_version(), - "environment": SettingsManager.get_settings().ENV, + "environment": settings.ENV, } @@ -40,10 +40,10 @@ def capture( data: dict[str, Any] | None = None, ) -> None: # If telemetry is disabled, don't send any data - if not SettingsManager.get_settings().SKYVERN_TELEMETRY: + if not settings.SKYVERN_TELEMETRY: return - distinct_id = SettingsManager.get_settings().ANALYTICS_ID + distinct_id = settings.ANALYTICS_ID payload: dict[str, Any] = data or {} try: diff --git a/skyvern/config.py b/skyvern/config.py index bce59736f..89e584bc9 100644 --- a/skyvern/config.py +++ b/skyvern/config.py @@ -103,6 +103,7 @@ class Settings(BaseSettings): ENABLE_AZURE: bool = False ENABLE_AZURE_GPT4O_MINI: bool = False ENABLE_BEDROCK: bool = False + ENABLE_GEMINI: bool = False # OPENAI OPENAI_API_KEY: str | None = None # ANTHROPIC @@ -119,6 +120,9 @@ class Settings(BaseSettings): AZURE_GPT4O_MINI_API_BASE: str | None = None AZURE_GPT4O_MINI_API_VERSION: str | None = None + # GEMINI + GEMINI_API_KEY: str | None = None + # TOTP Settings TOTP_LIFESPAN_MINUTES: int = 10 VERIFICATION_CODE_INITIAL_WAIT_TIME_SECS: int = 40 diff --git a/skyvern/forge/__main__.py b/skyvern/forge/__main__.py index b16f5f52b..9c04274c8 100644 --- a/skyvern/forge/__main__.py +++ b/skyvern/forge/__main__.py @@ -3,18 +3,18 @@ from dotenv import load_dotenv from skyvern import analytics -from skyvern.forge.sdk.settings_manager import SettingsManager +from skyvern.config import settings LOG = structlog.stdlib.get_logger() if __name__ == "__main__": analytics.capture("skyvern-oss-run-server") - port = SettingsManager.get_settings().PORT + port = settings.PORT LOG.info("Agent server starting.", host="0.0.0.0", port=port) load_dotenv() - reload = SettingsManager.get_settings().ENV == "local" + reload = settings.ENV == "local" uvicorn.run( "skyvern.forge.api_app:app", host="0.0.0.0", diff --git a/skyvern/forge/agent.py b/skyvern/forge/agent.py index 3a151986b..e1a282646 100644 --- a/skyvern/forge/agent.py +++ b/skyvern/forge/agent.py @@ -14,6 +14,7 @@ from playwright.async_api import Page from skyvern import analytics +from skyvern.config import settings from skyvern.constants import ( GET_DOWNLOADED_FILES_TIMEOUT, SAVE_DOWNLOADED_FILES_TIMEOUT, @@ -51,7 +52,6 @@ from skyvern.forge.sdk.db.enums import TaskType from skyvern.forge.sdk.models import Organization, Step, StepStatus from skyvern.forge.sdk.schemas.tasks import Task, TaskRequest, TaskStatus -from skyvern.forge.sdk.settings_manager import SettingsManager from skyvern.forge.sdk.workflow.context_manager import WorkflowRunContext from skyvern.forge.sdk.workflow.models.block import ActionBlock, BaseTaskBlock, ValidationBlock from skyvern.forge.sdk.workflow.models.workflow import Workflow, WorkflowRun, WorkflowRunStatus @@ -84,25 +84,25 @@ def __init__(self, action: Action) -> None: class ForgeAgent: def __init__(self) -> None: - if SettingsManager.get_settings().ADDITIONAL_MODULES: - for module in SettingsManager.get_settings().ADDITIONAL_MODULES: + if settings.ADDITIONAL_MODULES: + for module in settings.ADDITIONAL_MODULES: LOG.info("Loading additional module", module=module) __import__(module) LOG.info( "Additional modules loaded", - modules=SettingsManager.get_settings().ADDITIONAL_MODULES, + modules=settings.ADDITIONAL_MODULES, ) LOG.info( "Initializing ForgeAgent", - env=SettingsManager.get_settings().ENV, - execute_all_steps=SettingsManager.get_settings().EXECUTE_ALL_STEPS, - browser_type=SettingsManager.get_settings().BROWSER_TYPE, - max_scraping_retries=SettingsManager.get_settings().MAX_SCRAPING_RETRIES, - video_path=SettingsManager.get_settings().VIDEO_PATH, - browser_action_timeout_ms=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, - max_steps_per_run=SettingsManager.get_settings().MAX_STEPS_PER_RUN, - long_running_task_warning_ratio=SettingsManager.get_settings().LONG_RUNNING_TASK_WARNING_RATIO, - debug_mode=SettingsManager.get_settings().DEBUG_MODE, + 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() @@ -290,7 +290,7 @@ async def execute_step( override_max_steps_per_run or task.max_steps_per_run or organization.max_steps_per_run - or SettingsManager.get_settings().MAX_STEPS_PER_RUN + or settings.MAX_STEPS_PER_RUN ) if max_steps_per_run and task.max_steps_per_run != max_steps_per_run: await app.DATABASE.update_task( @@ -423,7 +423,7 @@ async def execute_step( close_browser_on_completion=close_browser_on_completion, task_block=task_block, ) - elif SettingsManager.get_settings().execute_all_steps() and next_step: + elif settings.execute_all_steps() and next_step: return await self.execute_step( organization, task, @@ -437,8 +437,8 @@ async def execute_step( "Step executed but continuous execution is disabled.", task_id=task.task_id, step_id=step.step_id, - is_cloud_env=SettingsManager.get_settings().is_cloud_environment(), - execute_all_steps=SettingsManager.get_settings().execute_all_steps(), + is_cloud_env=settings.is_cloud_environment(), + execute_all_steps=settings.execute_all_steps(), next_step_id=next_step.step_id if next_step else None, ) @@ -1342,7 +1342,7 @@ async def _get_action_results(self, task: Task) -> str: # Get action results from the last app.SETTINGS.PROMPT_ACTION_HISTORY_WINDOW steps steps = await app.DATABASE.get_task_steps(task_id=task.task_id, organization_id=task.organization_id) # the last step is always the newly created one and it should be excluded from the history window - window_steps = steps[-1 - SettingsManager.get_settings().PROMPT_ACTION_HISTORY_WINDOW : -1] + window_steps = steps[-1 - settings.PROMPT_ACTION_HISTORY_WINDOW : -1] actions_and_results: list[tuple[Action, list[ActionResult]]] = [] for window_step in window_steps: if window_step.output and window_step.output.actions_and_results: @@ -1576,7 +1576,7 @@ async def execute_task_webhook( task_id=task.task_id, organization_id=task.organization_id, artifact_types=[ArtifactType.SCREENSHOT_ACTION], - n=SettingsManager.get_settings().TASK_RESPONSE_ACTION_SCREENSHOT_COUNT, + n=settings.TASK_RESPONSE_ACTION_SCREENSHOT_COUNT, ) if latest_action_screenshot_artifacts: latest_action_screenshot_urls = await app.ARTIFACT_MANAGER.get_share_links( @@ -1790,7 +1790,7 @@ async def handle_failed_step(self, organization: Organization, task: Task, step: organization.max_retries_per_step # we need to check by None because 0 is a valid value for max_retries_per_step if organization.max_retries_per_step is not None - else SettingsManager.get_settings().MAX_RETRIES_PER_STEP + else settings.MAX_RETRIES_PER_STEP ) if step.retry_index >= max_retries_per_step: LOG.warning( @@ -1799,7 +1799,7 @@ async def handle_failed_step(self, organization: Organization, task: Task, step: step_id=step.step_id, step_order=step.order, step_retry=step.retry_index, - max_retries=SettingsManager.get_settings().MAX_RETRIES_PER_STEP, + max_retries=settings.MAX_RETRIES_PER_STEP, ) await self.update_task( task, @@ -1923,7 +1923,7 @@ async def handle_completed_step( override_max_steps_per_run or task.max_steps_per_run or organization.max_steps_per_run - or SettingsManager.get_settings().MAX_STEPS_PER_RUN + or settings.MAX_STEPS_PER_RUN ) # HACK: action block only have one step to execute without complete action, so we consider the task is completed as long as the step is completed @@ -1985,14 +1985,12 @@ async def handle_completed_step( organization_id=task.organization_id, ) - if step.order == int( - max_steps_per_run * SettingsManager.get_settings().LONG_RUNNING_TASK_WARNING_RATIO - 1 - ): + if step.order == int(max_steps_per_run * settings.LONG_RUNNING_TASK_WARNING_RATIO - 1): LOG.info( "Long running task warning", order=step.order, max_steps=max_steps_per_run, - warning_ratio=SettingsManager.get_settings().LONG_RUNNING_TASK_WARNING_RATIO, + warning_ratio=settings.LONG_RUNNING_TASK_WARNING_RATIO, ) return None, None, next_step diff --git a/skyvern/forge/api_app.py b/skyvern/forge/api_app.py index 4d68a555c..ca2ca93ce 100644 --- a/skyvern/forge/api_app.py +++ b/skyvern/forge/api_app.py @@ -11,6 +11,7 @@ from starlette_context.middleware import RawContextMiddleware from starlette_context.plugins.base import Plugin +from skyvern.config import settings from skyvern.exceptions import SkyvernHTTPException from skyvern.forge import app as forge_app from skyvern.forge.sdk.core import skyvern_context @@ -18,7 +19,6 @@ from skyvern.forge.sdk.db.exceptions import NotFoundError from skyvern.forge.sdk.routes.agent_protocol import base_router from skyvern.forge.sdk.routes.streaming import websocket_router -from skyvern.forge.sdk.settings_manager import SettingsManager LOG = structlog.get_logger() @@ -40,7 +40,7 @@ def get_agent_app() -> FastAPI: # Add CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=SettingsManager.get_settings().ALLOWED_ORIGINS, + allow_origins=settings.ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -90,13 +90,13 @@ async def request_middleware(request: Request, call_next: Callable[[Request], Aw finally: skyvern_context.reset() - if SettingsManager.get_settings().ADDITIONAL_MODULES: - for module in SettingsManager.get_settings().ADDITIONAL_MODULES: + if settings.ADDITIONAL_MODULES: + for module in settings.ADDITIONAL_MODULES: LOG.info("Loading additional module to set up api app", module=module) __import__(module) LOG.info( "Additional modules loaded to set up api app", - modules=SettingsManager.get_settings().ADDITIONAL_MODULES, + modules=settings.ADDITIONAL_MODULES, ) if forge_app.setup_api_app: diff --git a/skyvern/forge/app.py b/skyvern/forge/app.py index 630aa700d..3d97e6967 100644 --- a/skyvern/forge/app.py +++ b/skyvern/forge/app.py @@ -2,6 +2,7 @@ from fastapi import FastAPI +from skyvern.config import settings from skyvern.forge.agent import ForgeAgent from skyvern.forge.agent_functions import AgentFunction from skyvern.forge.sdk.api.llm.api_handler_factory import LLMAPIHandlerFactory @@ -12,25 +13,24 @@ from skyvern.forge.sdk.db.client import AgentDB from skyvern.forge.sdk.experimentation.providers import BaseExperimentationProvider, NoOpExperimentationProvider from skyvern.forge.sdk.models import Organization -from skyvern.forge.sdk.settings_manager import SettingsManager from skyvern.forge.sdk.workflow.context_manager import WorkflowContextManager from skyvern.forge.sdk.workflow.service import WorkflowService from skyvern.webeye.browser_manager import BrowserManager from skyvern.webeye.scraper.scraper import ScrapeExcludeFunc -SETTINGS_MANAGER = SettingsManager.get_settings() +SETTINGS_MANAGER = settings DATABASE = AgentDB( - SettingsManager.get_settings().DATABASE_STRING, - debug_enabled=SettingsManager.get_settings().DEBUG_MODE, + settings.DATABASE_STRING, + debug_enabled=settings.DEBUG_MODE, ) -if SettingsManager.get_settings().SKYVERN_STORAGE_TYPE == "s3": +if settings.SKYVERN_STORAGE_TYPE == "s3": StorageFactory.set_storage(S3Storage()) STORAGE = StorageFactory.get_storage() CACHE = CacheFactory.get_cache() ARTIFACT_MANAGER = ArtifactManager() BROWSER_MANAGER = BrowserManager() EXPERIMENTATION_PROVIDER: BaseExperimentationProvider = NoOpExperimentationProvider() -LLM_API_HANDLER = LLMAPIHandlerFactory.get_llm_api_handler(SettingsManager.get_settings().LLM_KEY) +LLM_API_HANDLER = LLMAPIHandlerFactory.get_llm_api_handler(settings.LLM_KEY) SECONDARY_LLM_API_HANDLER = LLMAPIHandlerFactory.get_llm_api_handler( SETTINGS_MANAGER.SECONDARY_LLM_KEY if SETTINGS_MANAGER.SECONDARY_LLM_KEY else SETTINGS_MANAGER.LLM_KEY ) diff --git a/skyvern/forge/sdk/api/aws.py b/skyvern/forge/sdk/api/aws.py index c1f2c99b0..4e1ff20cd 100644 --- a/skyvern/forge/sdk/api/aws.py +++ b/skyvern/forge/sdk/api/aws.py @@ -6,7 +6,7 @@ import structlog from aiobotocore.client import AioBaseClient -from skyvern.forge.sdk.settings_manager import SettingsManager +from skyvern.config import settings LOG = structlog.get_logger() @@ -22,7 +22,7 @@ async def wrapper(*args: list[Any], **kwargs: dict[str, Any]) -> Any: self = args[0] assert isinstance(self, AsyncAWSClient) session = aioboto3.Session() - async with session.client(client_type, region_name=SettingsManager.get_settings().AWS_REGION) as client: + async with session.client(client_type, region_name=settings.AWS_REGION) as client: return await f(*args, client=client, **kwargs) return wrapper @@ -95,7 +95,7 @@ async def create_presigned_urls(self, uris: list[str], client: AioBaseClient = N url = await client.generate_presigned_url( "get_object", Params={"Bucket": parsed_uri.bucket, "Key": parsed_uri.key}, - ExpiresIn=SettingsManager.get_settings().PRESIGNED_URL_EXPIRATION, + ExpiresIn=settings.PRESIGNED_URL_EXPIRATION, ) presigned_urls.append(url) diff --git a/skyvern/forge/sdk/api/files.py b/skyvern/forge/sdk/api/files.py index 925ab1581..2d80b7c92 100644 --- a/skyvern/forge/sdk/api/files.py +++ b/skyvern/forge/sdk/api/files.py @@ -12,10 +12,10 @@ import structlog from multidict import CIMultiDictProxy +from skyvern.config import settings from skyvern.constants import REPO_ROOT_DIR from skyvern.exceptions import DownloadFileMaxSizeExceeded from skyvern.forge.sdk.api.aws import AsyncAWSClient -from skyvern.forge.sdk.settings_manager import SettingsManager LOG = structlog.get_logger() @@ -169,7 +169,7 @@ def create_folder_if_not_exist(dir: str) -> None: def get_skyvern_temp_dir() -> str: - temp_dir = SettingsManager.get_settings().TEMP_PATH + temp_dir = settings.TEMP_PATH create_folder_if_not_exist(temp_dir) return temp_dir @@ -178,13 +178,13 @@ def make_temp_directory( suffix: str | None = None, prefix: str | None = None, ) -> str: - temp_dir = SettingsManager.get_settings().TEMP_PATH + temp_dir = settings.TEMP_PATH create_folder_if_not_exist(temp_dir) return tempfile.mkdtemp(suffix=suffix, prefix=prefix, dir=temp_dir) def create_named_temporary_file(delete: bool = True) -> tempfile._TemporaryFileWrapper: - temp_dir = SettingsManager.get_settings().TEMP_PATH + temp_dir = settings.TEMP_PATH create_folder_if_not_exist(temp_dir) return tempfile.NamedTemporaryFile(dir=temp_dir, delete=delete) diff --git a/skyvern/forge/sdk/api/llm/api_handler_factory.py b/skyvern/forge/sdk/api/llm/api_handler_factory.py index 5258ac21b..5f8a58baa 100644 --- a/skyvern/forge/sdk/api/llm/api_handler_factory.py +++ b/skyvern/forge/sdk/api/llm/api_handler_factory.py @@ -7,6 +7,7 @@ import litellm import structlog +from skyvern.config import settings from skyvern.forge import app from skyvern.forge.sdk.api.llm.config_registry import LLMConfigRegistry from skyvern.forge.sdk.api.llm.exceptions import ( @@ -19,7 +20,6 @@ from skyvern.forge.sdk.api.llm.utils import llm_messages_builder, parse_api_response from skyvern.forge.sdk.artifact.models import ArtifactType from skyvern.forge.sdk.models import Step -from skyvern.forge.sdk.settings_manager import SettingsManager LOG = structlog.get_logger() @@ -50,7 +50,7 @@ def get_llm_api_handler_with_router(llm_key: str) -> LLMAPIHandler: allowed_fails=llm_config.allowed_fails, allowed_fails_policy=llm_config.allowed_fails_policy, cooldown_time=llm_config.cooldown_time, - set_verbose=(False if SettingsManager.get_settings().is_cloud_environment() else llm_config.set_verbose), + set_verbose=(False if settings.is_cloud_environment() else llm_config.set_verbose), enable_pre_call_checks=True, ) main_model_group = llm_config.main_model_group @@ -213,7 +213,7 @@ async def llm_api_handler( response = await litellm.acompletion( model=llm_config.model_name, messages=messages, - timeout=SettingsManager.get_settings().LLM_CONFIG_TIMEOUT, + timeout=settings.LLM_CONFIG_TIMEOUT, **active_parameters, ) LOG.info("LLM API call successful", llm_key=llm_key, model=llm_config.model_name) @@ -263,7 +263,7 @@ async def llm_api_handler( def get_api_parameters(llm_config: LLMConfig | LLMRouterConfig) -> dict[str, Any]: return { "max_tokens": llm_config.max_output_tokens, - "temperature": SettingsManager.get_settings().LLM_CONFIG_TEMPERATURE, + "temperature": settings.LLM_CONFIG_TEMPERATURE, } @classmethod diff --git a/skyvern/forge/sdk/api/llm/config_registry.py b/skyvern/forge/sdk/api/llm/config_registry.py index 793596ed5..0a3c650eb 100644 --- a/skyvern/forge/sdk/api/llm/config_registry.py +++ b/skyvern/forge/sdk/api/llm/config_registry.py @@ -1,5 +1,6 @@ import structlog +from skyvern.config import settings from skyvern.forge.sdk.api.llm.exceptions import ( DuplicateLLMConfigError, InvalidLLMConfigError, @@ -7,7 +8,6 @@ NoProviderEnabledError, ) from skyvern.forge.sdk.api.llm.models import LiteLLMParams, LLMConfig, LLMRouterConfig -from skyvern.forge.sdk.settings_manager import SettingsManager LOG = structlog.get_logger() @@ -46,17 +46,17 @@ def get_config(cls, llm_key: str) -> LLMRouterConfig | LLMConfig: # if none of the LLM providers are enabled, raise an error if not any( [ - SettingsManager.get_settings().ENABLE_OPENAI, - SettingsManager.get_settings().ENABLE_ANTHROPIC, - SettingsManager.get_settings().ENABLE_AZURE, - SettingsManager.get_settings().ENABLE_AZURE_GPT4O_MINI, - SettingsManager.get_settings().ENABLE_BEDROCK, + settings.ENABLE_OPENAI, + settings.ENABLE_ANTHROPIC, + settings.ENABLE_AZURE, + settings.ENABLE_AZURE_GPT4O_MINI, + settings.ENABLE_BEDROCK, ] ): raise NoProviderEnabledError() -if SettingsManager.get_settings().ENABLE_OPENAI: +if settings.ENABLE_OPENAI: LLMConfigRegistry.register_config( "OPENAI_GPT4_TURBO", LLMConfig( @@ -103,7 +103,7 @@ def get_config(cls, llm_key: str) -> LLMRouterConfig | LLMConfig: ) -if SettingsManager.get_settings().ENABLE_ANTHROPIC: +if settings.ENABLE_ANTHROPIC: LLMConfigRegistry.register_config( "ANTHROPIC_CLAUDE3", LLMConfig( @@ -151,7 +151,7 @@ def get_config(cls, llm_key: str) -> LLMRouterConfig | LLMConfig: ), ) -if SettingsManager.get_settings().ENABLE_BEDROCK: +if settings.ENABLE_BEDROCK: # Supported through AWS IAM authentication LLMConfigRegistry.register_config( "BEDROCK_ANTHROPIC_CLAUDE3_OPUS", @@ -209,11 +209,11 @@ def get_config(cls, llm_key: str) -> LLMRouterConfig | LLMConfig: ) -if SettingsManager.get_settings().ENABLE_AZURE: +if settings.ENABLE_AZURE: LLMConfigRegistry.register_config( "AZURE_OPENAI", LLMConfig( - f"azure/{SettingsManager.get_settings().AZURE_DEPLOYMENT}", + f"azure/{settings.AZURE_DEPLOYMENT}", [ "AZURE_DEPLOYMENT", "AZURE_API_KEY", @@ -225,11 +225,11 @@ def get_config(cls, llm_key: str) -> LLMRouterConfig | LLMConfig: ), ) -if SettingsManager.get_settings().ENABLE_AZURE_GPT4O_MINI: +if settings.ENABLE_AZURE_GPT4O_MINI: LLMConfigRegistry.register_config( "AZURE_OPENAI_GPT4O_MINI", LLMConfig( - f"azure/{SettingsManager.get_settings().AZURE_GPT4O_MINI_DEPLOYMENT}", + f"azure/{settings.AZURE_GPT4O_MINI_DEPLOYMENT}", [ "AZURE_GPT4O_MINI_DEPLOYMENT", "AZURE_GPT4O_MINI_API_KEY", @@ -237,9 +237,9 @@ def get_config(cls, llm_key: str) -> LLMRouterConfig | LLMConfig: "AZURE_GPT4O_MINI_API_VERSION", ], litellm_params=LiteLLMParams( - api_base=SettingsManager.get_settings().AZURE_GPT4O_MINI_API_BASE, - api_key=SettingsManager.get_settings().AZURE_GPT4O_MINI_API_KEY, - api_version=SettingsManager.get_settings().AZURE_GPT4O_MINI_API_VERSION, + api_base=settings.AZURE_GPT4O_MINI_API_BASE, + api_key=settings.AZURE_GPT4O_MINI_API_KEY, + api_version=settings.AZURE_GPT4O_MINI_API_VERSION, model_info={"model_name": "azure/gpt-4o-mini"}, ), supports_vision=True, diff --git a/skyvern/forge/sdk/artifact/storage/local.py b/skyvern/forge/sdk/artifact/storage/local.py index e41144fcf..0fdcef69a 100644 --- a/skyvern/forge/sdk/artifact/storage/local.py +++ b/skyvern/forge/sdk/artifact/storage/local.py @@ -6,17 +6,17 @@ import structlog +from skyvern.config import settings from skyvern.forge.sdk.api.files import get_download_dir, get_skyvern_temp_dir from skyvern.forge.sdk.artifact.models import Artifact, ArtifactType from skyvern.forge.sdk.artifact.storage.base import FILE_EXTENTSION_MAP, BaseStorage from skyvern.forge.sdk.models import Step -from skyvern.forge.sdk.settings_manager import SettingsManager LOG = structlog.get_logger() class LocalStorage(BaseStorage): - def __init__(self, artifact_path: str = SettingsManager.get_settings().ARTIFACT_STORAGE_PATH) -> None: + def __init__(self, artifact_path: str = settings.ARTIFACT_STORAGE_PATH) -> None: self.artifact_path = artifact_path def build_uri(self, artifact_id: str, step: Step, artifact_type: ArtifactType) -> str: @@ -84,9 +84,7 @@ async def get_streaming_file(self, organization_id: str, file_name: str, use_def return None async def store_browser_session(self, organization_id: str, workflow_permanent_id: str, directory: str) -> None: - stored_folder_path = ( - Path(SettingsManager.get_settings().BROWSER_SESSION_BASE_PATH) / organization_id / workflow_permanent_id - ) + stored_folder_path = Path(settings.BROWSER_SESSION_BASE_PATH) / organization_id / workflow_permanent_id if directory == str(stored_folder_path): return self._create_directories_if_not_exists(stored_folder_path) @@ -108,9 +106,7 @@ async def store_browser_session(self, organization_id: str, workflow_permanent_i shutil.copy2(source_file_path, target_file_path) async def retrieve_browser_session(self, organization_id: str, workflow_permanent_id: str) -> str | None: - stored_folder_path = ( - Path(SettingsManager.get_settings().BROWSER_SESSION_BASE_PATH) / organization_id / workflow_permanent_id - ) + stored_folder_path = Path(settings.BROWSER_SESSION_BASE_PATH) / organization_id / workflow_permanent_id if not stored_folder_path.exists(): return None return str(stored_folder_path) diff --git a/skyvern/forge/sdk/core/security.py b/skyvern/forge/sdk/core/security.py index 353d2a7e1..d197cfcb2 100644 --- a/skyvern/forge/sdk/core/security.py +++ b/skyvern/forge/sdk/core/security.py @@ -5,7 +5,7 @@ from jose import jwt -from skyvern.forge.sdk.settings_manager import SettingsManager +from skyvern.config import settings def create_access_token( @@ -16,13 +16,13 @@ def create_access_token( expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta( - minutes=SettingsManager.get_settings().ACCESS_TOKEN_EXPIRE_MINUTES, + minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES, ) to_encode = {"exp": expire, "sub": str(subject)} encoded_jwt = jwt.encode( to_encode, - SettingsManager.get_settings().SECRET_KEY, - algorithm=SettingsManager.get_settings().SIGNATURE_ALGORITHM, + settings.SECRET_KEY, + algorithm=settings.SIGNATURE_ALGORITHM, ) return encoded_jwt diff --git a/skyvern/forge/sdk/forge_log.py b/skyvern/forge/sdk/forge_log.py index 912a5b3e7..612780900 100644 --- a/skyvern/forge/sdk/forge_log.py +++ b/skyvern/forge/sdk/forge_log.py @@ -3,8 +3,8 @@ import structlog from structlog.typing import EventDict +from skyvern.config import settings from skyvern.forge.sdk.core import skyvern_context -from skyvern.forge.sdk.settings_manager import SettingsManager LOGGING_LEVEL_MAP: dict[str, int] = { "DEBUG": logging.DEBUG, @@ -34,7 +34,7 @@ def add_kv_pairs_to_msg(logger: logging.Logger, method_name: str, event_dict: Ev event_dict["workflow_run_id"] = context.workflow_run_id # Add env to the log - event_dict["env"] = SettingsManager.get_settings().ENV + event_dict["env"] = settings.ENV if method_name not in ["info", "warning", "error", "critical", "exception"]: # Only modify the log for these log levels @@ -59,11 +59,7 @@ def setup_logger() -> None: Setup the logger with the specified format """ # logging.config.dictConfig(logging_config) - renderer = ( - structlog.processors.JSONRenderer() - if SettingsManager.get_settings().JSON_LOGGING - else structlog.dev.ConsoleRenderer() - ) + renderer = structlog.processors.JSONRenderer() if settings.JSON_LOGGING else structlog.dev.ConsoleRenderer() additional_processors = ( [ structlog.processors.EventRenamer("msg"), @@ -78,10 +74,10 @@ def setup_logger() -> None: } ), ] - if SettingsManager.get_settings().JSON_LOGGING + if settings.JSON_LOGGING else [] ) - LOG_LEVEL_VAL = LOGGING_LEVEL_MAP.get(SettingsManager.get_settings().LOG_LEVEL, logging.INFO) + LOG_LEVEL_VAL = LOGGING_LEVEL_MAP.get(settings.LOG_LEVEL, logging.INFO) structlog.configure( wrapper_class=structlog.make_filtering_bound_logger(LOG_LEVEL_VAL), diff --git a/skyvern/forge/sdk/routes/agent_protocol.py b/skyvern/forge/sdk/routes/agent_protocol.py index 4690a3de9..2f87ed889 100644 --- a/skyvern/forge/sdk/routes/agent_protocol.py +++ b/skyvern/forge/sdk/routes/agent_protocol.py @@ -22,6 +22,7 @@ from sqlalchemy.exc import OperationalError from skyvern import analytics +from skyvern.config import settings from skyvern.exceptions import StepNotFound from skyvern.forge import app from skyvern.forge.prompts import prompt_engine @@ -50,7 +51,6 @@ TaskStatus, ) from skyvern.forge.sdk.services import org_auth_service -from skyvern.forge.sdk.settings_manager import SettingsManager from skyvern.forge.sdk.workflow.exceptions import ( FailedToCreateWorkflow, FailedToUpdateWorkflow, @@ -95,7 +95,7 @@ async def webhook( generated_signature = generate_skyvern_signature( payload.decode("utf-8"), - SettingsManager.get_settings().SKYVERN_API_KEY, + settings.SKYVERN_API_KEY, ) LOG.info( @@ -291,7 +291,7 @@ async def get_task( task_id=task_obj.task_id, organization_id=task_obj.organization_id, artifact_types=[ArtifactType.SCREENSHOT_ACTION], - n=SettingsManager.get_settings().TASK_RESPONSE_ACTION_SCREENSHOT_COUNT, + n=settings.TASK_RESPONSE_ACTION_SCREENSHOT_COUNT, ) latest_action_screenshot_urls: list[str] | None = None if latest_action_screenshot_artifacts: @@ -532,7 +532,7 @@ async def get_agent_task_step_artifacts( step_id, organization_id=current_org.organization_id, ) - if SettingsManager.get_settings().ENV != "local" or SettingsManager.get_settings().GENERATE_PRESIGNED_URLS: + if settings.ENV != "local" or settings.GENERATE_PRESIGNED_URLS: signed_urls = await app.ARTIFACT_MANAGER.get_share_links(artifacts) if signed_urls: for i, artifact in enumerate(artifacts): @@ -863,7 +863,7 @@ async def generate_task( # check if there's a same user_prompt within the past x Hours # in the future, we can use vector db to fetch similar prompts existing_task_generation = await app.DATABASE.get_task_generation_by_prompt_hash( - user_prompt_hash=user_prompt_hash, query_window_hours=SettingsManager.get_settings().PROMPT_CACHE_WINDOW_HOURS + user_prompt_hash=user_prompt_hash, query_window_hours=settings.PROMPT_CACHE_WINDOW_HOURS ) if existing_task_generation: new_task_generation = await app.DATABASE.create_task_generation( @@ -898,7 +898,7 @@ async def generate_task( data_extraction_goal=parsed_task_generation_obj.data_extraction_goal, extracted_information_schema=parsed_task_generation_obj.extracted_information_schema, suggested_title=parsed_task_generation_obj.suggested_title, - llm=SettingsManager.get_settings().LLM_KEY, + llm=settings.LLM_KEY, llm_prompt=llm_prompt, llm_response=str(llm_response), ) diff --git a/skyvern/forge/sdk/services/org_auth_service.py b/skyvern/forge/sdk/services/org_auth_service.py index 8dd3ad213..1eda1a7ea 100644 --- a/skyvern/forge/sdk/services/org_auth_service.py +++ b/skyvern/forge/sdk/services/org_auth_service.py @@ -8,11 +8,11 @@ from jose.exceptions import JWTError from pydantic import ValidationError +from skyvern.config import settings from skyvern.forge import app from skyvern.forge.sdk.core import skyvern_context from skyvern.forge.sdk.db.client import AgentDB from skyvern.forge.sdk.models import Organization, OrganizationAuthTokenType, TokenPayload -from skyvern.forge.sdk.settings_manager import SettingsManager AUTHENTICATION_TTL = 60 * 60 # one hour CACHE_SIZE = 128 @@ -85,7 +85,7 @@ async def _get_current_org_cached(x_api_key: str, db: AgentDB) -> Organization: try: payload = jwt.decode( x_api_key, - SettingsManager.get_settings().SECRET_KEY, + settings.SECRET_KEY, algorithms=[ALGORITHM], ) api_key_data = TokenPayload(**payload) diff --git a/skyvern/forge/sdk/workflow/models/block.py b/skyvern/forge/sdk/workflow/models/block.py index c2b42e477..74d99e1f9 100644 --- a/skyvern/forge/sdk/workflow/models/block.py +++ b/skyvern/forge/sdk/workflow/models/block.py @@ -46,7 +46,6 @@ from skyvern.forge.sdk.api.llm.api_handler_factory import LLMAPIHandlerFactory from skyvern.forge.sdk.db.enums import TaskType from skyvern.forge.sdk.schemas.tasks import Task, TaskOutput, TaskStatus -from skyvern.forge.sdk.settings_manager import SettingsManager from skyvern.forge.sdk.workflow.context_manager import WorkflowRunContext from skyvern.forge.sdk.workflow.exceptions import ( InvalidEmailClientConfiguration, @@ -376,8 +375,8 @@ async def execute(self, workflow_run_id: str, **kwargs: dict) -> BlockResult: ) else: browser_state = app.BROWSER_MANAGER.get_for_workflow_run(workflow_run_id=workflow_run_id) - if browser_state is None: - raise MissingBrowserState(task_id=task.task_id, workflow_run_id=workflow_run_id) + if browser_state is None: + raise MissingBrowserState(task_id=task.task_id, workflow_run_id=workflow_run_id) except FailedToNavigateToUrl as e: # Make sure the task is marked as failed in the database before raising the exception await app.DATABASE.update_task( @@ -982,7 +981,7 @@ async def execute(self, workflow_run_id: str, **kwargs: dict) -> BlockResult: uri = None try: - uri = f"s3://{SettingsManager.get_settings().AWS_S3_BUCKET_UPLOADS}/{SettingsManager.get_settings().ENV}/{workflow_run_id}/{uuid.uuid4()}" + uri = f"s3://{settings.AWS_S3_BUCKET_UPLOADS}/{settings.ENV}/{workflow_run_id}/{uuid.uuid4()}" await self._upload_file_to_s3(uri, file_path) except Exception as e: LOG.error("DownloadToS3Block: Failed to upload file to S3", uri=uri, error=str(e)) @@ -1019,8 +1018,8 @@ def format_potential_template_parameters(self, workflow_run_context: WorkflowRun @staticmethod def _get_s3_uri(workflow_run_id: str, path: str) -> str: - s3_bucket = SettingsManager.get_settings().AWS_S3_BUCKET_UPLOADS - s3_key = f"{SettingsManager.get_settings().ENV}/{workflow_run_id}/{uuid.uuid4()}_{Path(path).name}" + s3_bucket = settings.AWS_S3_BUCKET_UPLOADS + s3_key = f"{settings.ENV}/{workflow_run_id}/{uuid.uuid4()}_{Path(path).name}" return f"s3://{s3_bucket}/{s3_key}" async def execute(self, workflow_run_id: str, **kwargs: dict) -> BlockResult: @@ -1037,7 +1036,7 @@ async def execute(self, workflow_run_id: str, **kwargs: dict) -> BlockResult: ) self.path = file_path_parameter_value # if the path is WORKFLOW_DOWNLOAD_DIRECTORY_PARAMETER_KEY, use the download directory for the workflow run - elif self.path == SettingsManager.get_settings().WORKFLOW_DOWNLOAD_DIRECTORY_PARAMETER_KEY: + elif self.path == settings.WORKFLOW_DOWNLOAD_DIRECTORY_PARAMETER_KEY: self.path = str(get_path_for_workflow_download_directory(workflow_run_id).absolute()) self.format_potential_template_parameters(workflow_run_context) @@ -1173,7 +1172,7 @@ def _get_file_paths(self, workflow_run_context: WorkflowRunContext, workflow_run else: path = file_path_parameter_value - if path == SettingsManager.get_settings().WORKFLOW_DOWNLOAD_DIRECTORY_PARAMETER_KEY: + if path == settings.WORKFLOW_DOWNLOAD_DIRECTORY_PARAMETER_KEY: # if the path is WORKFLOW_DOWNLOAD_DIRECTORY_PARAMETER_KEY, use download directory for the workflow run path = str(get_path_for_workflow_download_directory(workflow_run_id).absolute()) LOG.info( diff --git a/skyvern/forge/sdk/workflow/service.py b/skyvern/forge/sdk/workflow/service.py index 6a932da98..c407f19dd 100644 --- a/skyvern/forge/sdk/workflow/service.py +++ b/skyvern/forge/sdk/workflow/service.py @@ -7,6 +7,7 @@ import structlog from skyvern import analytics +from skyvern.config import settings from skyvern.constants import GET_DOWNLOADED_FILES_TIMEOUT, SAVE_DOWNLOADED_FILES_TIMEOUT from skyvern.exceptions import ( FailedToSendWebhook, @@ -23,7 +24,6 @@ from skyvern.forge.sdk.db.enums import TaskType from skyvern.forge.sdk.models import Organization, Step from skyvern.forge.sdk.schemas.tasks import ProxyLocation, Task -from skyvern.forge.sdk.settings_manager import SettingsManager from skyvern.forge.sdk.workflow.exceptions import ( ContextParameterSourceNotDefined, InvalidWaitBlockTime, @@ -1508,11 +1508,8 @@ async def block_yaml_to_block( ) elif block_yaml.block_type == BlockType.WAIT: - if ( - block_yaml.wait_sec <= 0 - or block_yaml.wait_sec > SettingsManager.get_settings().WORKFLOW_WAIT_BLOCK_MAX_SEC - ): - raise InvalidWaitBlockTime(SettingsManager.get_settings().WORKFLOW_WAIT_BLOCK_MAX_SEC) + if block_yaml.wait_sec <= 0 or block_yaml.wait_sec > settings.WORKFLOW_WAIT_BLOCK_MAX_SEC: + raise InvalidWaitBlockTime(settings.WORKFLOW_WAIT_BLOCK_MAX_SEC) return WaitBlock( label=block_yaml.label, diff --git a/skyvern/webeye/actions/handler.py b/skyvern/webeye/actions/handler.py index e2b2885ba..6f8f333c6 100644 --- a/skyvern/webeye/actions/handler.py +++ b/skyvern/webeye/actions/handler.py @@ -12,6 +12,7 @@ from playwright.async_api import FileChooser, Frame, Locator, Page, TimeoutError from pydantic import BaseModel +from skyvern.config import settings from skyvern.constants import REPO_ROOT_DIR, SKYVERN_ID_ATTR from skyvern.exceptions import ( EmptySelect, @@ -52,7 +53,6 @@ from skyvern.forge.sdk.models import Step from skyvern.forge.sdk.schemas.tasks import Task from skyvern.forge.sdk.services.bitwarden import BitwardenConstants -from skyvern.forge.sdk.settings_manager import SettingsManager from skyvern.webeye.actions import actions from skyvern.webeye.actions.actions import ( Action, @@ -371,7 +371,7 @@ async def handle_click_action( page, action, skyvern_element, - timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout=settings.BROWSER_ACTION_TIMEOUT_MS, ) if results and task.workflow_run_id and download_dir: @@ -400,8 +400,8 @@ async def handle_click_to_download_file_action( locator = skyvern_element.locator try: - await locator.click(timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) - await page.wait_for_load_state(timeout=SettingsManager.get_settings().BROWSER_LOADING_TIMEOUT_MS) + await locator.click(timeout=settings.BROWSER_ACTION_TIMEOUT_MS) + await page.wait_for_load_state(timeout=settings.BROWSER_LOADING_TIMEOUT_MS) except Exception as e: LOG.exception("ClickAction with download failed", action=action, exc_info=True) return [ActionFailure(e, download_triggered=False)] @@ -420,7 +420,7 @@ async def handle_input_text_action( skyvern_element = await dom.get_skyvern_element_by_id(action.element_id) skyvern_frame = await SkyvernFrame.create_instance(skyvern_element.get_frame()) incremental_scraped = IncrementalScrapePage(skyvern_frame=skyvern_frame) - timeout = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS + timeout = settings.BROWSER_ACTION_TIMEOUT_MS current_text = await get_input_value(skyvern_element.get_tag_name(), skyvern_element.get_locator()) if current_text == action.text: @@ -656,7 +656,7 @@ async def handle_upload_file_action( if file_path: await locator.set_input_files( file_path, - timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout=settings.BROWSER_ACTION_TIMEOUT_MS, ) # Sleep for 10 seconds after uploading a file to let the page process it @@ -675,7 +675,7 @@ async def handle_upload_file_action( page, action, skyvern_element, - timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout=settings.BROWSER_ACTION_TIMEOUT_MS, ) @@ -699,7 +699,7 @@ async def handle_download_file_action( locator = skyvern_element.locator await locator.click( - timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout=settings.BROWSER_ACTION_TIMEOUT_MS, modifiers=["Alt"], ) @@ -847,7 +847,7 @@ async def handle_select_option_action( action=action, ) - timeout = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS + timeout = settings.BROWSER_ACTION_TIMEOUT_MS skyvern_frame = await SkyvernFrame.create_instance(skyvern_element.get_frame()) incremental_scraped = IncrementalScrapePage(skyvern_frame=skyvern_frame) is_open = False @@ -935,7 +935,7 @@ async def handle_select_option_action( ) try: await incremental_scraped.start_listen_dom_increment() - timeout = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS + timeout = settings.BROWSER_ACTION_TIMEOUT_MS await skyvern_element.scroll_into_view() try: @@ -999,9 +999,9 @@ async def handle_checkbox_action( locator = skyvern_element.locator if action.is_checked: - await locator.check(timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) + await locator.check(timeout=settings.BROWSER_ACTION_TIMEOUT_MS) else: - await locator.uncheck(timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) + await locator.uncheck(timeout=settings.BROWSER_ACTION_TIMEOUT_MS) # TODO (suchintan): Why does checking the label work, but not the actual input element? return [ActionSuccess()] @@ -1136,7 +1136,7 @@ async def chain_click( page: Page, action: ClickAction | UploadFileAction, skyvern_element: SkyvernElement, - timeout: int = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout: int = settings.BROWSER_ACTION_TIMEOUT_MS, ) -> List[ActionResult]: # Add a defensive page handler here in case a click action opens a file chooser. # This automatically dismisses the dialog @@ -1374,9 +1374,7 @@ async def choose_auto_completion_dropdown( if cnt == 0: continue - element_handler = await locator.element_handle( - timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS - ) + element_handler = await locator.element_handle(timeout=settings.BROWSER_ACTION_TIMEOUT_MS) if not element_handler: continue @@ -1444,7 +1442,7 @@ async def choose_auto_completion_dropdown( if await locator.count() == 0: raise MissingElement(element_id=element_id) - await locator.click(timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) + await locator.click(timeout=settings.BROWSER_ACTION_TIMEOUT_MS) clear_input = False return result except Exception as e: @@ -1776,7 +1774,7 @@ async def select_from_dropdown( select_history = [] if select_history is None else select_history single_select_result = CustomSingleSelectResult(skyvern_frame=skyvern_frame) - timeout = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS + timeout = settings.BROWSER_ACTION_TIMEOUT_MS if dropdown_menu_element is None: dropdown_menu_element = await locate_dropdown_menu( @@ -1927,7 +1925,7 @@ async def select_from_dropdown_by_value( step: Step, dropdown_menu_element: SkyvernElement | None = None, ) -> ActionResult: - timeout = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS + timeout = settings.BROWSER_ACTION_TIMEOUT_MS await incremental_scraped.get_incremental_element_tree( clean_and_remove_element_tree_factory(task=task, step=step, check_exist_funcs=[dom.check_id_in_dom]), ) @@ -2061,9 +2059,7 @@ async def locate_dropdown_menu( # sometimes taking screenshot might scroll away, need to scroll back after the screenshot x, y = await skyvern_frame.get_scroll_x_y() - screenshot = await head_element.get_locator().screenshot( - timeout=SettingsManager.get_settings().BROWSER_SCREENSHOT_TIMEOUT_MS - ) + screenshot = await head_element.get_locator().screenshot(timeout=settings.BROWSER_SCREENSHOT_TIMEOUT_MS) await skyvern_frame.scroll_to_x_y(x, y) # TODO: better to send untrimmed HTML without skyvern attributes in the future @@ -2139,7 +2135,7 @@ async def scroll_down_to_load_all_options( step_id=step.step_id if step else "none", task_id=task.task_id if task else "none", ) - timeout = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS + timeout = settings.BROWSER_ACTION_TIMEOUT_MS dropdown_menu_element_handle = await scrollable_element.get_locator().element_handle(timeout=timeout) if dropdown_menu_element_handle is None: @@ -2153,12 +2149,10 @@ async def scroll_down_to_load_all_options( scroll_pace = 0 previous_num = await incremental_scraped.get_incremental_elements_num() - deadline = datetime.now(timezone.utc) + timedelta( - milliseconds=SettingsManager.get_settings().OPTION_LOADING_TIMEOUT_MS - ) + deadline = datetime.now(timezone.utc) + timedelta(milliseconds=settings.OPTION_LOADING_TIMEOUT_MS) while datetime.now(timezone.utc) < deadline: # make sure we can scroll to the bottom - scroll_interval = SettingsManager.get_settings().BROWSER_HEIGHT * 5 + scroll_interval = settings.BROWSER_HEIGHT * 5 if dropdown_menu_element_handle is None: LOG.info("element handle is None, using mouse to scroll down", element_id=scrollable_element.get_id()) await page.mouse.wheel(0, scroll_interval) @@ -2250,7 +2244,7 @@ async def normal_select( try: await locator.click( - timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout=settings.BROWSER_ACTION_TIMEOUT_MS, ) except Exception as e: LOG.info( @@ -2267,7 +2261,7 @@ async def normal_select( # click by value (if it matches) await locator.select_option( value=value, - timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout=settings.BROWSER_ACTION_TIMEOUT_MS, ) is_success = True action_result.append(ActionSuccess()) @@ -2293,7 +2287,7 @@ async def normal_select( # This means the supplied index was for the select element, not a reference to the css dict await locator.select_option( index=index, - timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout=settings.BROWSER_ACTION_TIMEOUT_MS, ) is_success = True action_result.append(ActionSuccess()) @@ -2308,7 +2302,7 @@ async def normal_select( try: await locator.click( - timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout=settings.BROWSER_ACTION_TIMEOUT_MS, ) except Exception as e: LOG.info( @@ -2464,7 +2458,7 @@ async def poll_verification_code( totp_verification_url: str | None = None, totp_identifier: str | None = None, ) -> str | None: - timeout = timedelta(minutes=SettingsManager.get_settings().VERIFICATION_CODE_POLLING_TIMEOUT_MINS) + timeout = timedelta(minutes=settings.VERIFICATION_CODE_POLLING_TIMEOUT_MINS) start_datetime = datetime.utcnow() timeout_datetime = start_datetime + timeout org_token = await app.DATABASE.get_valid_org_auth_token(organization_id, OrganizationAuthTokenType.api) @@ -2472,7 +2466,7 @@ async def poll_verification_code( LOG.error("Failed to get organization token when trying to get verification code") return None # wait for 40 seconds to let the verification code comes in before polling - await asyncio.sleep(SettingsManager.get_settings().VERIFICATION_CODE_INITIAL_WAIT_TIME_SECS) + await asyncio.sleep(settings.VERIFICATION_CODE_INITIAL_WAIT_TIME_SECS) while True: # check timeout if datetime.utcnow() > timeout_datetime: diff --git a/skyvern/webeye/actions/models.py b/skyvern/webeye/actions/models.py index 338305d2b..33a877e60 100644 --- a/skyvern/webeye/actions/models.py +++ b/skyvern/webeye/actions/models.py @@ -4,7 +4,7 @@ from pydantic import BaseModel -from skyvern.forge.sdk.settings_manager import SettingsManager +from skyvern.config import settings from skyvern.webeye.actions.actions import Action, DecisiveAction, UserDefinedError from skyvern.webeye.actions.responses import ActionResult from skyvern.webeye.scraper.scraper import ScrapedPage @@ -45,7 +45,7 @@ class Config: exclude = ["scraped_page", "extract_action_prompt"] def __repr__(self) -> str: - if SettingsManager.get_settings().DEBUG_MODE: + if settings.DEBUG_MODE: return f"DetailedAgentStepOutput({self.model_dump()})" else: return f"AgentStepOutput({self.to_agent_step_output().model_dump()})" diff --git a/skyvern/webeye/browser_factory.py b/skyvern/webeye/browser_factory.py index d7787d766..5957a2dab 100644 --- a/skyvern/webeye/browser_factory.py +++ b/skyvern/webeye/browser_factory.py @@ -26,7 +26,6 @@ from skyvern.forge.sdk.api.files import make_temp_directory from skyvern.forge.sdk.core.skyvern_context import current from skyvern.forge.sdk.schemas.tasks import ProxyLocation -from skyvern.forge.sdk.settings_manager import SettingsManager from skyvern.webeye.utils.page import SkyvernFrame LOG = structlog.get_logger() @@ -143,12 +142,14 @@ def get_subdir() -> str: @staticmethod def build_browser_args() -> dict[str, Any]: - video_dir = f"{SettingsManager.get_settings().VIDEO_PATH}/{datetime.utcnow().strftime('%Y-%m-%d')}" - har_dir = f"{SettingsManager.get_settings().HAR_PATH}/{datetime.utcnow().strftime('%Y-%m-%d')}/{BrowserContextFactory.get_subdir()}.har" + video_dir = f"{settings.VIDEO_PATH}/{datetime.utcnow().strftime('%Y-%m-%d')}" + har_dir = ( + f"{settings.HAR_PATH}/{datetime.utcnow().strftime('%Y-%m-%d')}/{BrowserContextFactory.get_subdir()}.har" + ) return { "user_data_dir": make_temp_directory(prefix="skyvern_browser_"), - "locale": SettingsManager.get_settings().BROWSER_LOCALE, - "timezone_id": SettingsManager.get_settings().BROWSER_TIMEZONE, + "locale": settings.BROWSER_LOCALE, + "timezone_id": settings.BROWSER_TIMEZONE, "color_scheme": "no-preference", "args": [ "--disable-blink-features=AutomationControlled", @@ -191,7 +192,7 @@ def register_type(cls, browser_type: str, creator: BrowserContextCreator) -> Non async def create_browser_context( cls, playwright: Playwright, **kwargs: Any ) -> tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]: - browser_type = SettingsManager.get_settings().BROWSER_TYPE + browser_type = settings.BROWSER_TYPE browser_context: BrowserContext | None = None try: creator = cls._creators.get(browser_type) diff --git a/skyvern/webeye/scraper/scraper.py b/skyvern/webeye/scraper/scraper.py index 57a17726b..975e5dfa8 100644 --- a/skyvern/webeye/scraper/scraper.py +++ b/skyvern/webeye/scraper/scraper.py @@ -9,10 +9,10 @@ from playwright.async_api import Frame, Locator, Page from pydantic import BaseModel, PrivateAttr +from skyvern.config import settings from skyvern.constants import BUILDING_ELEMENT_TREE_TIMEOUT_MS, SKYVERN_DIR, SKYVERN_ID_ATTR from skyvern.exceptions import FailedToTakeScreenshot, UnknownElementTreeFormat from skyvern.forge.sdk.api.crypto import calculate_sha256 -from skyvern.forge.sdk.settings_manager import SettingsManager from skyvern.webeye.browser_factory import BrowserState from skyvern.webeye.utils.page import SkyvernFrame @@ -301,10 +301,10 @@ async def scrape_website( ) except Exception as e: # NOTE: MAX_SCRAPING_RETRIES is set to 0 in both staging and production - if num_retry > SettingsManager.get_settings().MAX_SCRAPING_RETRIES: + if num_retry > settings.MAX_SCRAPING_RETRIES: LOG.error( "Scraping failed after max retries, aborting.", - max_retries=SettingsManager.get_settings().MAX_SCRAPING_RETRIES, + max_retries=settings.MAX_SCRAPING_RETRIES, url=url, exc_info=True, ) diff --git a/skyvern/webeye/utils/dom.py b/skyvern/webeye/utils/dom.py index b44b89d16..7eb06477b 100644 --- a/skyvern/webeye/utils/dom.py +++ b/skyvern/webeye/utils/dom.py @@ -9,6 +9,7 @@ import structlog from playwright.async_api import ElementHandle, Frame, FrameLocator, Locator, Page, TimeoutError +from skyvern.config import settings from skyvern.constants import SKYVERN_ID_ATTR from skyvern.exceptions import ( ElementIsNotLabel, @@ -21,7 +22,6 @@ NoneFrameError, SkyvernException, ) -from skyvern.forge.sdk.settings_manager import SettingsManager from skyvern.webeye.scraper.scraper import IncrementalScrapePage, ScrapedPage, json_to_html, trim_element from skyvern.webeye.utils.page import SkyvernFrame @@ -313,9 +313,7 @@ def get_frame(self) -> Page | Frame: def get_locator(self) -> Locator: return self.locator - async def get_element_handler( - self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS - ) -> ElementHandle: + async def get_element_handler(self, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS) -> ElementHandle: handler = await self.locator.element_handle(timeout=timeout) assert handler is not None return handler @@ -368,7 +366,7 @@ async def find_children_element_id_by_callback( return None async def find_label_for( - self, dom: DomUtil, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS + self, dom: DomUtil, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS ) -> SkyvernElement | None: if self.get_tag_name() != "label": return None @@ -388,9 +386,7 @@ async def find_label_for( return await dom.get_skyvern_element_by_id(unique_id) - async def find_bound_label_by_attr_id( - self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS - ) -> Locator | None: + async def find_bound_label_by_attr_id(self, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS) -> Locator | None: if self.get_tag_name() == "label": return None @@ -406,7 +402,7 @@ async def find_bound_label_by_attr_id( return None async def find_bound_label_by_direct_parent( - self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS + self, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS ) -> Locator | None: if self.get_tag_name() == "label": return None @@ -490,7 +486,7 @@ async def get_attr( self, attr_name: str, dynamic: bool = False, - timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, ) -> typing.Any: if not dynamic: attr = self.get_attributes().get(attr_name) @@ -499,12 +495,10 @@ async def get_attr( return await self.locator.get_attribute(attr_name, timeout=timeout) - async def focus(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None: + async def focus(self, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS) -> None: await self.get_locator().focus(timeout=timeout) - async def input_sequentially( - self, text: str, default_timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS - ) -> None: + async def input_sequentially(self, text: str, default_timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS) -> None: length = len(text) if length > TEXT_PRESS_MAX_LENGTH: # if the text is longer than TEXT_PRESS_MAX_LENGTH characters, we will locator.fill in initial texts until the last TEXT_PRESS_MAX_LENGTH characters @@ -514,22 +508,16 @@ async def input_sequentially( await self.press_fill(text, timeout=default_timeout) - async def press_key( - self, key: str, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS - ) -> None: + async def press_key(self, key: str, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS) -> None: await self.get_locator().press(key=key, timeout=timeout) - async def press_fill( - self, text: str, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS - ) -> None: + async def press_fill(self, text: str, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS) -> None: await self.get_locator().press_sequentially(text, delay=TEXT_INPUT_DELAY, timeout=timeout) - async def input_fill( - self, text: str, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS - ) -> None: + async def input_fill(self, text: str, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS) -> None: await self.get_locator().fill(text, timeout=timeout) - async def input_clear(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None: + async def input_clear(self, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS) -> None: await self.get_locator().clear(timeout=timeout) async def move_mouse_to_safe( @@ -537,7 +525,7 @@ async def move_mouse_to_safe( page: Page, task_id: str | None = None, step_id: str | None = None, - timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, ) -> tuple[float, float] | tuple[None, None]: element_id = self.get_id() try: @@ -561,7 +549,7 @@ async def move_mouse_to_safe( return None, None async def move_mouse_to( - self, page: Page, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS + self, page: Page, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS ) -> tuple[float, float]: bounding_box = await self.get_locator().bounding_box(timeout=timeout) if not bounding_box: @@ -580,9 +568,7 @@ async def click_in_javascript(self) -> None: skyvern_frame = await SkyvernFrame.create_instance(self.get_frame()) await skyvern_frame.click_element_in_javascript(await self.get_element_handler()) - async def coordinate_click( - self, page: Page, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS - ) -> None: + async def coordinate_click(self, page: Page, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS) -> None: click_x, click_y = await self.move_mouse_to(page=page, timeout=timeout) await page.mouse.click(click_x, click_y) @@ -591,7 +577,7 @@ async def blur(self) -> None: frame=self.get_frame(), expression="(element) => element.blur()", arg=await self.get_element_handler() ) - async def scroll_into_view(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None: + async def scroll_into_view(self, timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS) -> None: element_handler = await self.get_element_handler(timeout=timeout) try: await element_handler.scroll_into_view_if_needed(timeout=timeout) @@ -608,7 +594,7 @@ async def calculate_vertical_distance_to( self, target_locator: Locator, mode: typing.Literal["inner", "outer"], - timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, ) -> float: self_rect = await self.get_locator().bounding_box(timeout=timeout) if self_rect is None: diff --git a/skyvern/webeye/utils/page.py b/skyvern/webeye/utils/page.py index c5e6d883d..81ff124ba 100644 --- a/skyvern/webeye/utils/page.py +++ b/skyvern/webeye/utils/page.py @@ -8,9 +8,9 @@ from playwright._impl._errors import TimeoutError from playwright.async_api import ElementHandle, Frame, Page +from skyvern.config import settings from skyvern.constants import BUILDING_ELEMENT_TREE_TIMEOUT_MS, PAGE_CONTENT_TIMEOUT, SKYVERN_DIR from skyvern.exceptions import FailedToTakeScreenshot -from skyvern.forge.sdk.settings_manager import SettingsManager LOG = structlog.get_logger() @@ -37,7 +37,7 @@ async def evaluate( frame: Page | Frame, expression: str, arg: Any | None = None, - timeout_ms: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, + timeout_ms: float = settings.BROWSER_ACTION_TIMEOUT_MS, ) -> Any: try: async with asyncio.timeout(timeout_ms / 1000): @@ -51,12 +51,12 @@ async def take_screenshot( page: Page, full_page: bool = False, file_path: str | None = None, - timeout: float = SettingsManager.get_settings().BROWSER_LOADING_TIMEOUT_MS, + timeout: float = settings.BROWSER_LOADING_TIMEOUT_MS, ) -> bytes: if page.is_closed(): raise FailedToTakeScreenshot(error_message="Page is closed") try: - await page.wait_for_load_state(timeout=SettingsManager.get_settings().BROWSER_LOADING_TIMEOUT_MS) + await page.wait_for_load_state(timeout=settings.BROWSER_LOADING_TIMEOUT_MS) LOG.debug("Page is fully loaded, agent is about to take screenshots") start_time = time.time() screenshot: bytes = bytes() @@ -92,7 +92,7 @@ async def take_split_screenshots( page: Page, url: str, draw_boxes: bool = False, - max_number: int = SettingsManager.get_settings().MAX_NUM_SCREENSHOTS, + max_number: int = settings.MAX_NUM_SCREENSHOTS, ) -> List[bytes]: skyvern_page = await SkyvernFrame.create_instance(frame=page) assert isinstance(skyvern_page.frame, Page)