add timezone support (#1389)

This commit is contained in:
LawyZheng
2024-12-16 11:22:51 +08:00
committed by GitHub
parent bc57dc105b
commit 9b1aeff79e
13 changed files with 86 additions and 28 deletions

View File

@@ -1310,6 +1310,7 @@ class ForgeAgent:
if not template: if not template:
raise UnsupportedTaskType(task_type=task_type) raise UnsupportedTaskType(task_type=task_type)
context = skyvern_context.ensure_context()
return prompt_engine.load_prompt( return prompt_engine.load_prompt(
template=template, template=template,
navigation_goal=navigation_goal, navigation_goal=navigation_goal,
@@ -1320,7 +1321,7 @@ class ForgeAgent:
data_extraction_goal=task.data_extraction_goal, data_extraction_goal=task.data_extraction_goal,
action_history=actions_and_results_str, action_history=actions_and_results_str,
error_code_mapping_str=(json.dumps(task.error_code_mapping) if task.error_code_mapping else None), error_code_mapping_str=(json.dumps(task.error_code_mapping) if task.error_code_mapping else None),
utc_datetime=datetime.utcnow().strftime("%Y-%m-%d %H:%M"), local_datetime=datetime.now(context.tz_info).isoformat(),
verification_code_check=verification_code_check, verification_code_check=verification_code_check,
complete_criterion=task.complete_criterion, complete_criterion=task.complete_criterion,
terminate_criterion=task.terminate_criterion, terminate_criterion=task.terminate_criterion,

View File

@@ -51,7 +51,7 @@ Select History:
{{ select_history }} {{ select_history }}
``` ```
{% endif %} {% endif %}
Current datetime in UTC, YYYY-MM-DD HH:MM format: Current datetime, ISO format:
``` ```
{{ utc_datetime }} {{ local_datetime }}
``` ```

View File

@@ -49,7 +49,7 @@ User details:
{{ navigation_payload_str }} {{ navigation_payload_str }}
``` ```
Current datetime in UTC, YYYY-MM-DD HH:MM format: Current datetime, ISO format:
``` ```
{{ utc_datetime }} {{ local_datetime }}
``` ```

View File

@@ -73,7 +73,7 @@ User details:
Action results from previous steps: (note: even if the action history suggests goal is achieved, check the screenshot and the DOM elements to make sure the goal is achieved) Action results from previous steps: (note: even if the action history suggests goal is achieved, check the screenshot and the DOM elements to make sure the goal is achieved)
{{ action_history }} {{ action_history }}
{% endif %} {% endif %}
Current datetime in UTC, YYYY-MM-DD HH:MM format: Current datetime, ISO format:
``` ```
{{ utc_datetime }} {{ local_datetime }}
``` ```

View File

@@ -26,7 +26,7 @@ Text extracted from the webpage: {{ extracted_text }}
User Navigation Payload: {{ navigation_payload }} User Navigation Payload: {{ navigation_payload }}
Current datetime in UTC, YYYY-MM-DD HH:MM format: Current datetime, ISO format:
``` ```
{{ utc_datetime }} {{ local_datetime }}
``` ```

View File

@@ -35,7 +35,7 @@ User details:
{{ navigation_payload_str }} {{ navigation_payload_str }}
``` ```
Current datetime in UTC, YYYY-MM-DD HH:MM format: Current datetime, ISO format:
``` ```
{{ utc_datetime }} {{ local_datetime }}
``` ```

View File

@@ -37,7 +37,7 @@ User details:
{{ navigation_payload_str }} {{ navigation_payload_str }}
``` ```
Current datetime in UTC, YYYY-MM-DD HH:MM format: Current datetime, ISO format:
``` ```
{{ utc_datetime }} {{ local_datetime }}
``` ```

View File

@@ -39,7 +39,7 @@ User details:
{{ navigation_payload_str }} {{ navigation_payload_str }}
``` ```
Current datetime in UTC, YYYY-MM-DD HH:MM format: Current datetime, ISO format:
``` ```
{{ utc_datetime }} {{ local_datetime }}
``` ```

View File

@@ -35,7 +35,7 @@ User details:
{{ navigation_payload_str }} {{ navigation_payload_str }}
``` ```
Current datetime in UTC, YYYY-MM-DD HH:MM format: Current datetime, ISO format:
``` ```
{{ utc_datetime }} {{ local_datetime }}
``` ```

View File

@@ -1,5 +1,6 @@
from contextvars import ContextVar from contextvars import ContextVar
from dataclasses import dataclass, field from dataclasses import dataclass, field
from zoneinfo import ZoneInfo
@dataclass @dataclass
@@ -10,6 +11,7 @@ class SkyvernContext:
workflow_id: str | None = None workflow_id: str | None = None
workflow_run_id: str | None = None workflow_run_id: str | None = None
max_steps_override: int | None = None max_steps_override: int | None = None
tz_info: ZoneInfo | None = None
totp_codes: dict[str, str | None] = field(default_factory=dict) totp_codes: dict[str, str | None] = field(default_factory=dict)
def __repr__(self) -> str: def __repr__(self) -> str:

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from enum import StrEnum from enum import StrEnum
from typing import Any from typing import Any
from zoneinfo import ZoneInfo
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
@@ -26,6 +27,46 @@ class ProxyLocation(StrEnum):
NONE = "NONE" NONE = "NONE"
def get_tzinfo_from_proxy(proxy_location: ProxyLocation) -> ZoneInfo | None:
if proxy_location == ProxyLocation.NONE:
return None
if proxy_location == ProxyLocation.US_CA:
return ZoneInfo("America/Los_Angeles")
if proxy_location == ProxyLocation.US_NY:
return ZoneInfo("America/New_York")
if proxy_location == ProxyLocation.US_TX:
return ZoneInfo("America/Chicago")
if proxy_location == ProxyLocation.US_FL:
return ZoneInfo("America/New_York")
if proxy_location == ProxyLocation.US_WA:
return ZoneInfo("America/New_York")
if proxy_location == ProxyLocation.RESIDENTIAL:
return ZoneInfo("America/New_York")
if proxy_location == ProxyLocation.RESIDENTIAL_ES:
return ZoneInfo("Europe/Madrid")
if proxy_location == ProxyLocation.RESIDENTIAL_IE:
return ZoneInfo("Europe/Dublin")
if proxy_location == ProxyLocation.RESIDENTIAL_GB:
return ZoneInfo("Europe/London")
if proxy_location == ProxyLocation.RESIDENTIAL_IN:
return ZoneInfo("Asia/Kolkata")
if proxy_location == ProxyLocation.RESIDENTIAL_JP:
return ZoneInfo("Asia/Kolkata")
return None
class TaskBase(BaseModel): class TaskBase(BaseModel):
title: str | None = Field( title: str | None = Field(
default=None, default=None,

View File

@@ -52,6 +52,7 @@ from skyvern.forge.prompts import prompt_engine
from skyvern.forge.sdk.api.files import download_file, get_download_dir, list_files_in_directory from skyvern.forge.sdk.api.files import download_file, get_download_dir, list_files_in_directory
from skyvern.forge.sdk.core.aiohttp_helper import aiohttp_post from skyvern.forge.sdk.core.aiohttp_helper import aiohttp_post
from skyvern.forge.sdk.core.security import generate_skyvern_signature from skyvern.forge.sdk.core.security import generate_skyvern_signature
from skyvern.forge.sdk.core.skyvern_context import ensure_context
from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
from skyvern.forge.sdk.models import Step from skyvern.forge.sdk.models import Step
from skyvern.forge.sdk.schemas.tasks import Task from skyvern.forge.sdk.schemas.tasks import Task
@@ -1877,6 +1878,7 @@ async def select_from_dropdown(
html = incremental_scraped.build_html_tree(element_tree=trimmed_element_tree) html = incremental_scraped.build_html_tree(element_tree=trimmed_element_tree)
skyvern_context = ensure_context()
prompt = prompt_engine.load_prompt( prompt = prompt_engine.load_prompt(
"custom-select", "custom-select",
field_information=context.field, field_information=context.field,
@@ -1886,7 +1888,7 @@ async def select_from_dropdown(
navigation_payload_str=json.dumps(task.navigation_payload), navigation_payload_str=json.dumps(task.navigation_payload),
elements=html, elements=html,
select_history=json.dumps(build_sequential_select_history(select_history)) if select_history else "", select_history=json.dumps(build_sequential_select_history(select_history)) if select_history else "",
utc_datetime=datetime.utcnow().strftime("%Y-%m-%d %H:%M"), local_datetime=datetime.now(skyvern_context.tz_info).isoformat(),
) )
LOG.info( LOG.info(
@@ -2449,6 +2451,8 @@ async def extract_information_for_navigation_goal(
element_tree_in_prompt: str = scraped_page.build_element_tree(element_tree_format) element_tree_in_prompt: str = scraped_page.build_element_tree(element_tree_format)
scraped_page_refreshed = await scraped_page.refresh() scraped_page_refreshed = await scraped_page.refresh()
context = ensure_context()
extract_information_prompt = prompt_engine.load_prompt( extract_information_prompt = prompt_engine.load_prompt(
prompt_template, prompt_template,
navigation_goal=task.navigation_goal, navigation_goal=task.navigation_goal,
@@ -2459,7 +2463,7 @@ async def extract_information_for_navigation_goal(
current_url=scraped_page_refreshed.url, current_url=scraped_page_refreshed.url,
extracted_text=scraped_page_refreshed.extracted_text, extracted_text=scraped_page_refreshed.extracted_text,
error_code_mapping_str=(json.dumps(task.error_code_mapping) if task.error_code_mapping else None), error_code_mapping_str=(json.dumps(task.error_code_mapping) if task.error_code_mapping else None),
utc_datetime=datetime.utcnow().strftime("%Y-%m-%d %H:%M"), local_datetime=datetime.now(context.tz_info).isoformat(),
) )
json_response = await app.LLM_API_HANDLER( json_response = await app.LLM_API_HANDLER(

View File

@@ -25,7 +25,7 @@ from skyvern.exceptions import (
) )
from skyvern.forge.sdk.api.files import get_download_dir, make_temp_directory from skyvern.forge.sdk.api.files import get_download_dir, make_temp_directory
from skyvern.forge.sdk.core.skyvern_context import current, ensure_context from skyvern.forge.sdk.core.skyvern_context import current, ensure_context
from skyvern.forge.sdk.schemas.tasks import ProxyLocation from skyvern.forge.sdk.schemas.tasks import ProxyLocation, get_tzinfo_from_proxy
from skyvern.webeye.utils.page import SkyvernFrame from skyvern.webeye.utils.page import SkyvernFrame
LOG = structlog.get_logger() LOG = structlog.get_logger()
@@ -128,7 +128,7 @@ def initialize_download_dir() -> str:
class BrowserContextCreator(Protocol): class BrowserContextCreator(Protocol):
def __call__( def __call__(
self, playwright: Playwright, **kwargs: dict[str, Any] self, playwright: Playwright, proxy_location: ProxyLocation | None = None, **kwargs: dict[str, Any]
) -> Awaitable[tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]]: ... ) -> Awaitable[tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]]: ...
@@ -162,14 +162,13 @@ class BrowserContextFactory:
f.write(preference_file_content) f.write(preference_file_content)
@staticmethod @staticmethod
def build_browser_args() -> dict[str, Any]: def build_browser_args(proxy_location: ProxyLocation | None = None) -> dict[str, Any]:
video_dir = f"{settings.VIDEO_PATH}/{datetime.utcnow().strftime('%Y-%m-%d')}" video_dir = f"{settings.VIDEO_PATH}/{datetime.utcnow().strftime('%Y-%m-%d')}"
har_dir = ( har_dir = (
f"{settings.HAR_PATH}/{datetime.utcnow().strftime('%Y-%m-%d')}/{BrowserContextFactory.get_subdir()}.har" f"{settings.HAR_PATH}/{datetime.utcnow().strftime('%Y-%m-%d')}/{BrowserContextFactory.get_subdir()}.har"
) )
return { args = {
"locale": settings.BROWSER_LOCALE, "locale": settings.BROWSER_LOCALE,
"timezone_id": settings.BROWSER_TIMEZONE,
"color_scheme": "no-preference", "color_scheme": "no-preference",
"args": [ "args": [
"--disable-blink-features=AutomationControlled", "--disable-blink-features=AutomationControlled",
@@ -188,6 +187,11 @@ class BrowserContextFactory:
}, },
} }
if proxy_location:
if tz_info := get_tzinfo_from_proxy(proxy_location=proxy_location):
args["timezone_id"] = tz_info.key
return args
@staticmethod @staticmethod
def build_browser_artifacts( def build_browser_artifacts(
video_artifacts: list[VideoArtifact] | None = None, video_artifacts: list[VideoArtifact] | None = None,
@@ -221,6 +225,12 @@ class BrowserContextFactory:
browser_context, browser_artifacts, cleanup_func = await creator(playwright, **kwargs) browser_context, browser_artifacts, cleanup_func = await creator(playwright, **kwargs)
set_browser_console_log(browser_context=browser_context, browser_artifacts=browser_artifacts) set_browser_console_log(browser_context=browser_context, browser_artifacts=browser_artifacts)
set_download_file_listener(browser_context=browser_context, **kwargs) set_download_file_listener(browser_context=browser_context, **kwargs)
proxy_location: ProxyLocation | None = kwargs.get("proxy_location")
if proxy_location is not None:
context = ensure_context()
context.tz_info = get_tzinfo_from_proxy(proxy_location)
return browser_context, browser_artifacts, cleanup_func return browser_context, browser_artifacts, cleanup_func
except Exception as e: except Exception as e:
if browser_context is not None: if browser_context is not None:
@@ -279,7 +289,7 @@ class BrowserArtifacts(BaseModel):
async def _create_headless_chromium( async def _create_headless_chromium(
playwright: Playwright, **kwargs: dict playwright: Playwright, proxy_location: ProxyLocation | None = None, **kwargs: dict
) -> tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]: ) -> tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]:
user_data_dir = make_temp_directory(prefix="skyvern_browser_") user_data_dir = make_temp_directory(prefix="skyvern_browser_")
download_dir = initialize_download_dir() download_dir = initialize_download_dir()
@@ -287,7 +297,7 @@ async def _create_headless_chromium(
user_data_dir=user_data_dir, user_data_dir=user_data_dir,
download_dir=download_dir, download_dir=download_dir,
) )
browser_args = BrowserContextFactory.build_browser_args() browser_args = BrowserContextFactory.build_browser_args(proxy_location=proxy_location)
browser_args.update( browser_args.update(
{ {
"user_data_dir": user_data_dir, "user_data_dir": user_data_dir,
@@ -301,7 +311,7 @@ async def _create_headless_chromium(
async def _create_headful_chromium( async def _create_headful_chromium(
playwright: Playwright, **kwargs: dict playwright: Playwright, proxy_location: ProxyLocation | None = None, **kwargs: dict
) -> tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]: ) -> tuple[BrowserContext, BrowserArtifacts, BrowserCleanupFunc]:
user_data_dir = make_temp_directory(prefix="skyvern_browser_") user_data_dir = make_temp_directory(prefix="skyvern_browser_")
download_dir = initialize_download_dir() download_dir = initialize_download_dir()
@@ -309,7 +319,7 @@ async def _create_headful_chromium(
user_data_dir=user_data_dir, user_data_dir=user_data_dir,
download_dir=download_dir, download_dir=download_dir,
) )
browser_args = BrowserContextFactory.build_browser_args() browser_args = BrowserContextFactory.build_browser_args(proxy_location=proxy_location)
browser_args.update( browser_args.update(
{ {
"user_data_dir": user_data_dir, "user_data_dir": user_data_dir,