mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-26 10:41:14 +00:00
169 lines
6.6 KiB
Python
169 lines
6.6 KiB
Python
import time
|
|
from collections import defaultdict
|
|
|
|
import structlog
|
|
from playwright.async_api import Locator, Page
|
|
|
|
from skyvern.forge.sdk.event.base import CursorEventStrategy, InputEventStrategy, ScrollEventStrategy
|
|
from skyvern.forge.sdk.event.default import DefaultCursorStrategy, DefaultInputStrategy, DefaultScrollStrategy
|
|
|
|
LOG = structlog.get_logger(__name__)
|
|
|
|
_default_cursor = DefaultCursorStrategy()
|
|
_default_input = DefaultInputStrategy()
|
|
_default_scroll = DefaultScrollStrategy()
|
|
|
|
|
|
class _EventMetrics:
|
|
"""Accumulates per-event-type timing within a step."""
|
|
|
|
def __init__(self) -> None:
|
|
self._counts: dict[str, int] = defaultdict(int)
|
|
self._durations: dict[str, float] = defaultdict(float)
|
|
|
|
def record(self, event_type: str, duration: float) -> None:
|
|
self._counts[event_type] += 1
|
|
self._durations[event_type] += duration
|
|
|
|
def flush(self, **log_kwargs: object) -> None:
|
|
"""Log per-event-type summary and reset. No-op if nothing was recorded."""
|
|
if not self._counts:
|
|
return
|
|
|
|
total_duration = sum(self._durations.values())
|
|
total_count = sum(self._counts.values())
|
|
per_event = {
|
|
event_type: {"count": self._counts[event_type], "duration_seconds": round(self._durations[event_type], 4)}
|
|
for event_type in sorted(self._counts)
|
|
}
|
|
LOG.info(
|
|
"Event strategy duration metrics",
|
|
total_count=total_count,
|
|
total_duration_seconds=round(total_duration, 4),
|
|
per_event=per_event,
|
|
**log_kwargs,
|
|
)
|
|
self._counts.clear()
|
|
self._durations.clear()
|
|
|
|
|
|
class EventStrategyFactory:
|
|
__cursor: CursorEventStrategy | None = None
|
|
__input: InputEventStrategy | None = None
|
|
__scroll: ScrollEventStrategy | None = None
|
|
__metrics: _EventMetrics = _EventMetrics()
|
|
|
|
# -- setters ----------------------------------------------------------------
|
|
|
|
@staticmethod
|
|
def set_cursor_strategy(strategy: CursorEventStrategy) -> None:
|
|
EventStrategyFactory.__cursor = strategy
|
|
|
|
@staticmethod
|
|
def set_input_strategy(strategy: InputEventStrategy) -> None:
|
|
EventStrategyFactory.__input = strategy
|
|
|
|
@staticmethod
|
|
def set_scroll_strategy(strategy: ScrollEventStrategy) -> None:
|
|
EventStrategyFactory.__scroll = strategy
|
|
|
|
@staticmethod
|
|
def reset() -> None:
|
|
"""Clear all custom strategies, reverting to defaults."""
|
|
EventStrategyFactory.__cursor = None
|
|
EventStrategyFactory.__input = None
|
|
EventStrategyFactory.__scroll = None
|
|
EventStrategyFactory.__metrics = _EventMetrics()
|
|
|
|
# -- getters (always return a non-None strategy) ----------------------------
|
|
|
|
@staticmethod
|
|
def get_cursor_strategy() -> CursorEventStrategy:
|
|
return EventStrategyFactory.__cursor or _default_cursor
|
|
|
|
@staticmethod
|
|
def get_input_strategy() -> InputEventStrategy:
|
|
return EventStrategyFactory.__input or _default_input
|
|
|
|
@staticmethod
|
|
def get_scroll_strategy() -> ScrollEventStrategy:
|
|
return EventStrategyFactory.__scroll or _default_scroll
|
|
|
|
# -- metrics ----------------------------------------------------------------
|
|
|
|
@staticmethod
|
|
def flush_metrics(**log_kwargs: object) -> None:
|
|
"""Log and reset accumulated event strategy metrics."""
|
|
EventStrategyFactory.__metrics.flush(**log_kwargs)
|
|
|
|
# -- cursor convenience methods ---------------------------------------------
|
|
|
|
@staticmethod
|
|
async def move_cursor(page: Page, x: float, y: float) -> None:
|
|
"""Move cursor using the active strategy."""
|
|
start = time.perf_counter()
|
|
try:
|
|
await EventStrategyFactory.get_cursor_strategy().move_to(page, x, y)
|
|
finally:
|
|
EventStrategyFactory.__metrics.record("move_cursor", time.perf_counter() - start)
|
|
|
|
@staticmethod
|
|
async def move_to_element(page: Page, locator: Locator) -> None:
|
|
"""Move cursor to element. Failures are logged and swallowed."""
|
|
start = time.perf_counter()
|
|
try:
|
|
await EventStrategyFactory.get_cursor_strategy().move_to_element(page, locator)
|
|
except Exception:
|
|
LOG.debug("Cursor move_to_element failed, proceeding with action", exc_info=True)
|
|
finally:
|
|
EventStrategyFactory.__metrics.record("move_to_element", time.perf_counter() - start)
|
|
|
|
@staticmethod
|
|
def sync_cursor_position(page: Page, x: float, y: float) -> None:
|
|
"""Update cursor position without generating movement."""
|
|
EventStrategyFactory.get_cursor_strategy().sync_position(page, x, y)
|
|
|
|
# -- input convenience methods ----------------------------------------------
|
|
|
|
@staticmethod
|
|
async def type_text(page: Page, locator: Locator | None, text: str) -> None:
|
|
"""Type text using the active input strategy."""
|
|
start = time.perf_counter()
|
|
try:
|
|
await EventStrategyFactory.get_input_strategy().type_text(page, locator, text)
|
|
finally:
|
|
EventStrategyFactory.__metrics.record("type_text", time.perf_counter() - start)
|
|
|
|
@staticmethod
|
|
async def clear_field(page: Page, locator: Locator, char_count: int) -> None:
|
|
"""Clear field using the active input strategy."""
|
|
start = time.perf_counter()
|
|
try:
|
|
await EventStrategyFactory.get_input_strategy().clear_field(page, locator, char_count)
|
|
finally:
|
|
EventStrategyFactory.__metrics.record("clear_field", time.perf_counter() - start)
|
|
|
|
# -- scroll convenience methods ---------------------------------------------
|
|
|
|
@staticmethod
|
|
async def scroll_by(page: Page, scroll_x: float, scroll_y: float) -> None:
|
|
"""Scroll using the active strategy for vertical-only, raw wheel for horizontal."""
|
|
start = time.perf_counter()
|
|
try:
|
|
if scroll_x == 0:
|
|
await EventStrategyFactory.get_scroll_strategy().scroll_by(page, scroll_y)
|
|
else:
|
|
await page.mouse.wheel(scroll_x, scroll_y)
|
|
finally:
|
|
EventStrategyFactory.__metrics.record("scroll_by", time.perf_counter() - start)
|
|
|
|
@staticmethod
|
|
async def scroll_to_element(page: Page, locator: Locator) -> None:
|
|
"""Scroll to element using the active strategy. Failures are logged and swallowed."""
|
|
start = time.perf_counter()
|
|
try:
|
|
await EventStrategyFactory.get_scroll_strategy().scroll_to_element(page, locator)
|
|
except Exception:
|
|
LOG.debug("scroll_to_element failed, proceeding with action", exc_info=True)
|
|
finally:
|
|
EventStrategyFactory.__metrics.record("scroll_to_element", time.perf_counter() - start)
|