from typing import Literal, Optional from pydantic import BaseModel, Field, model_validator class PixelTarget(BaseModel): mode: Literal["pixel"] x: int y: int dx: int = 0 dy: int = 0 class GridTarget(BaseModel): mode: Literal["grid"] region_x: int region_y: int region_width: int = Field(gt=0) region_height: int = Field(gt=0) rows: int = Field(gt=0) cols: int = Field(gt=0) row: int = Field(ge=0) col: int = Field(ge=0) dx: float = 0.0 dy: float = 0.0 @model_validator(mode="after") def _validate_indices(self): if self.row >= self.rows or self.col >= self.cols: raise ValueError("row/col must be inside rows/cols") if not -1.0 <= self.dx <= 1.0: raise ValueError("dx must be in [-1, 1]") if not -1.0 <= self.dy <= 1.0: raise ValueError("dy must be in [-1, 1]") return self Target = PixelTarget | GridTarget class ActionRequest(BaseModel): action: Literal[ "move", "click", "right_click", "double_click", "middle_click", "scroll", "type", "hotkey", ] target: Optional[Target] = None duration_ms: int = Field(default=0, ge=0, le=20000) button: Literal["left", "right", "middle"] = "left" clicks: int = Field(default=1, ge=1, le=10) scroll_amount: int = 0 text: str = "" keys: list[str] = Field(default_factory=list) interval_ms: int = Field(default=20, ge=0, le=5000) dry_run: bool = False class ExecRequest(BaseModel): command: str = Field(min_length=1, max_length=10000) shell: Literal["powershell", "bash", "cmd"] | None = None timeout_s: int | None = Field(default=None, ge=1, le=600) cwd: str | None = None dry_run: bool = False class WindowQuery(BaseModel): title_contains: str | None = Field(default=None, max_length=512) title_regex: str | None = Field(default=None, max_length=512) process_name: str | None = Field(default=None, max_length=260) hwnd: int | None = Field(default=None, ge=1) visible_only: bool = True class WindowActionRequest(WindowQuery): action: Literal["focus", "restore", "minimize", "maximize", "close"] timeout_ms: int = Field(default=3000, ge=0, le=60000) class LaunchRequest(BaseModel): executable: str = Field(min_length=1, max_length=2048) args: list[str] = Field(default_factory=list, max_length=100) cwd: str | None = None wait_for_window: bool = False match: WindowQuery | None = None timeout_ms: int = Field(default=5000, ge=0, le=120000) dry_run: bool = False class SeeRequest(BaseModel): screen: int = 0 region_x: int | None = Field(default=None, ge=0) region_y: int | None = Field(default=None, ge=0) region_width: int | None = Field(default=None, gt=0) region_height: int | None = Field(default=None, gt=0) with_grid: bool = True grid_rows: int = Field(default=12, ge=1, le=300) grid_cols: int = Field(default=12, ge=1, le=300) include_labels: bool = True image_format: Literal["png", "jpeg"] = "png" jpeg_quality: int = Field(default=85, ge=1, le=100) class SeeZoomRequest(BaseModel): screen: int = 0 center_x: int = Field(ge=0) center_y: int = Field(ge=0) width: int = Field(default=500, ge=10) height: int = Field(default=350, ge=10) with_grid: bool = True grid_rows: int = Field(default=20, ge=1, le=300) grid_cols: int = Field(default=20, ge=1, le=300) include_labels: bool = True image_format: Literal["png", "jpeg"] = "png" jpeg_quality: int = Field(default=90, ge=1, le=100) class InteractRequest(BaseModel): screen: int = 0 action: ActionRequest