- Add NOTICE file and README license references for mobile-use and finalrun-agent - Add derivative attribution headers to 17 files derived from mobile-use - Rename hopper agent to entity_extractor across prompts, tools, and configs - Remove obsolete adb_tunnel, ApiBaseUrl, and legacy CLI utility scripts - Replace legacy examples with native Artemis diagnostic and workflow examples - Update unit and tool tests to validate entity_extractor and SDK builders
174 lines
6.3 KiB
Python
174 lines
6.3 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
# Portions of this file are derived from mobile-use (https://github.com/minitap-ai/mobile-use)
|
|
# Copyright 2025-2026 Minitap, Inc. Licensed under the Apache License 2.0.
|
|
|
|
"""Abstract device controller contract and telemetry interfaces for ARTEMIS.
|
|
|
|
Defines the hardware interaction lifecycle including touch gestures, text entry,
|
|
process lifecycle operations, display capture, and multi-segment recording.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from artemis.controllers.types import Bounds, CoordinatesSelectorRequest, TapOutput
|
|
from artemis.utils.video import VideoRecordingResult
|
|
|
|
|
|
class ScreenDataResponse(BaseModel):
|
|
"""Aggregate snapshot containing live display frames and parsed node hierarchies."""
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
base64: str = Field(default="", description="Base64 encoded JPEG/PNG frame capture.")
|
|
elements: list[dict[str, Any]] = Field(default_factory=list, description="Parsed hierarchy node elements.")
|
|
width: int = Field(default=1080, description="Active viewport width in physical pixels.")
|
|
height: int = Field(default=2400, description="Active viewport height in physical pixels.")
|
|
platform: str = Field(default="android", description="Target platform identifier.")
|
|
|
|
|
|
class MobileDeviceController(ABC):
|
|
"""Abstract driver interface establishing device actuation and telemetry operations."""
|
|
|
|
@abstractmethod
|
|
async def tap(
|
|
self,
|
|
coords: CoordinatesSelectorRequest,
|
|
long_press: bool = False,
|
|
long_press_duration: int = 1000,
|
|
times: int = 1,
|
|
delay_ms: int = 100,
|
|
) -> TapOutput:
|
|
"""Dispatch single, repeated, or long-press tap events to the target coordinates."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def swipe(
|
|
self,
|
|
start: CoordinatesSelectorRequest,
|
|
end: CoordinatesSelectorRequest,
|
|
duration: int = 400,
|
|
) -> str | None:
|
|
"""Execute directional drag or fling between two coordinate positions."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def screenshot(self) -> str:
|
|
"""Capture current display frame as an encoded image payload."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def input_text(self, text: str) -> bool:
|
|
"""Stream textual characters into the currently active input field."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def launch_app(self, package_name: str) -> bool:
|
|
"""Bring target Android application package into foreground."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def terminate_app(self, package_name: str | None) -> bool:
|
|
"""Force-stop application process matching the target package."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def open_url(self, url: str) -> bool:
|
|
"""Dispatch system VIEW intent for the specified URI."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def press_back(self) -> bool:
|
|
"""Trigger Android BACK system navigation key event."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def press_home(self) -> bool:
|
|
"""Trigger Android HOME system navigation key event."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def press_enter(self) -> bool:
|
|
"""Trigger ENTER / ACTION_DONE key event on virtual keyboard."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def press_key(self, keycode: str) -> bool:
|
|
"""Dispatch raw Android keycode event (e.g., 'KEYCODE_TAB')."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def get_ui_hierarchy(self) -> list[dict[str, Any]]:
|
|
"""Retrieve flattened or nested accessibility tree node descriptors."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def find_element(
|
|
self,
|
|
ui_hierarchy: list[dict[str, Any]],
|
|
resource_id: str | None = None,
|
|
text: str | None = None,
|
|
index: int = 0,
|
|
) -> tuple[dict[str, Any] | None, Bounds | None, str | None]:
|
|
"""Locate target element within parsed hierarchy by ID or label matching."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def cleanup(self) -> None:
|
|
"""Tear down companion sessions, tunnels, and streaming handles."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def erase_text(self, nb_chars: int | None = None) -> bool:
|
|
"""Dispatch backspace events to clear trailing characters in focused field."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def get_screen_data(self) -> ScreenDataResponse:
|
|
"""Capture unified screen frame, layout tree, and viewport dimensions."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def get_compressed_b64_screenshot(self, image_base64: str, quality: int = 50) -> str:
|
|
"""Produce compressed image payload for bandwidth-constrained turns."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def start_video_recording(
|
|
self,
|
|
max_duration_seconds: int = 900,
|
|
) -> VideoRecordingResult:
|
|
"""Initialize session screen capture recording."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def stop_video_recording(self) -> VideoRecordingResult:
|
|
"""Terminate active screen capture and finalize video artifact."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def extract_segment_metadata(
|
|
self,
|
|
start_relative_time: float,
|
|
end_relative_time: float | None = None,
|
|
) -> VideoRecordingResult:
|
|
"""Slice video segment for the requested relative execution epoch."""
|
|
raise NotImplementedError
|