Files
Artemis-Eapil/artemis/tools/actions/device_actions.py
T
somew1nd 206a9ce921 feat: initial release of ARTEMIS Android Agent & Automation Platform
ARTEMIS is an autonomous multimodal Android agent and mobile UI automation
platform engineered for physical devices and emulators. It combines state-of-the-art
multimodal Vision-Language Models (VLMs) with hierarchical closed-loop planning,
resilient self-healing, and deep Android subsystem integration.

Key Features & Architectural Highlights:
- Dual-Engine Orchestration:
  * Pro Engine: Multi-agent closed-loop (Planner -> Operator -> Validator/Failure Analyzer -> Summarizer) powered by LangGraph.
  * Flash Engine: Ultra-fast reactive loop for deterministic, low-latency UI execution (<35 steps).
- Multimodal Perception & Grounding:
  * Hybrid visual screenshot grounding, real-time OCR extraction, and XML accessibility hierarchy parsing.
  * Dynamic-First, Coordinate-Fallback locating strategy for maximum resilience against UI drift.
- Autonomous Self-Healing & Diagnostics:
  * Failure analyzer and safety net for popup dismissal, crash recovery, and auto-retry.
  * Diagnostic sub-agents for log analysis, video recording inspection, and object detection.
- Unified CLI & Python SDK:
  * Rich CLI with commands: init, doctor, run, trace, batch, bench, server, and mcp.
  * Fluent Python SDK with Pydantic structured output models and package locking.
- Multi-Provider LLM Architecture:
  * Support for Google Gemini, OpenAI, Anthropic Claude, Vertex AI, OpenRouter, xAI Grok, and local endpoints (Ollama/vLLM).
  * Centralized configuration in config/artemis.jsonc.
- Native MCP & Observability:
  * Model Context Protocol (MCP) server endpoints for AI IDEs.
  * Full trace recording with before/after screenshots, action overlays, and execution replays.

Change-Id: I430b93ee2b76b54cc5a3c6eeccd15d2cf29830a1
2026-08-13 11:46:47 -07:00

238 lines
8.2 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.
"""Universal device action tools implemented on top of BaseDeviceDriver."""
import asyncio
from typing import Any, Literal
from artemis.drivers.base import BaseDeviceDriver
from artemis.tools.base import artemis_tool
from artemis.utils.coordinates import parse_swipe_parameters
from artemis.utils.logger import get_logger
from pydantic import BaseModel, Field
logger = get_logger(__name__)
# Schemas
class ClickArgs(BaseModel):
target: list[int] = Field(
..., description="Click target normalized coordinates [x, y] in 0-1000 scale."
)
class LongPressArgs(BaseModel):
target: list[int] = Field(
..., description="Long press target normalized coordinates [x, y] in 0-1000 scale."
)
duration: int = Field(1000, description="Long press duration in milliseconds (default 1000).")
class InputTextArgs(BaseModel):
text: str = Field(..., description="The text content to input.")
target: list[int] = Field(
..., description="Input target field normalized coordinates [x, y] in 0-1000 scale."
)
clear_exist: bool = Field(
True, description="Whether to clear existing text in the input box before typing."
)
class SwipeArgs(BaseModel):
direction: Literal["up", "down", "left", "right"] | None = Field(
None,
description=(
"Smart directional swipe ('up', 'down', 'left', 'right') for general page"
" scrolling/browsing. Automatically applies safe vectors and a ~40% visual"
" overlap anchor: 'up' (drags bottom-to-top, scrolling down to reveal content below),"
" 'down' (drags top-to-bottom, scrolling up to reveal content above),"
" 'left' (drags right-to-left, scrolling right),"
" 'right' (drags left-to-right, scrolling left)."
),
)
start: list[int] | None = Field(
None,
description=(
"Start normalized coordinates [start_x, start_y] in 0-1000 scale for precise,"
" local interactions (e.g. adjusting sliders, SeekBars, fine range selection, or drag-and-drop)."
),
)
end: list[int] | None = Field(
None,
description=(
"End normalized coordinates [end_x, end_y] in 0-1000 scale for precise,"
" local interactions (e.g. adjusting sliders, SeekBars, fine range selection, or drag-and-drop)."
),
)
action: Literal["up", "down", "left", "right"] | list[int] | None = Field(
None,
description=(
"Backward-compatible swipe gesture: smart direction string ('up', 'down', 'left', 'right')"
" OR precise custom coordinates [start_x, start_y, end_x, end_y] in 0-1000 scale."
),
)
duration: int = Field(
400,
description=(
"Swipe/drag duration in milliseconds (default 400). For drag-and-drop, "
"list reordering, or sliding/adjusting sliders (e.g. volume, brightness, SeekBars), "
"set duration >= 1000 (e.g. 1500)."
),
)
class PressKeyArgs(BaseModel):
key: str = Field(
...,
description="Key name to press (e.g. 'home', 'back', 'enter', 'delete', 'power').",
)
class WaitForDelayArgs(BaseModel):
seconds: float = Field(
..., description="Duration in seconds to wait for animations, pages, or state updates."
)
# Universal Tool Definitions
@artemis_tool(
name="click",
description="[ACTION] Clicks on the specified screen coordinate [x, y] in normalized 0-1000 scale.",
args_schema=ClickArgs,
category="action",
)
async def click(target: list[int], driver: BaseDeviceDriver) -> str:
width, height = driver.screen_size
abs_x = int(target[0] * width / 1000.0)
abs_y = int(target[1] * height / 1000.0)
success = await driver.tap(abs_x, abs_y)
return (
f"Click executed at normalized {target} (pixels: {abs_x}, {abs_y})"
if success
else f"Click failed at {target}"
)
@artemis_tool(
name="long_press",
description="[ACTION] Long presses on the specified screen coordinate [x, y] in normalized 0-1000 scale.",
args_schema=LongPressArgs,
category="action",
)
async def long_press(
target: list[int], duration: int = 1000, driver: BaseDeviceDriver = None
) -> str:
width, height = driver.screen_size
abs_x = int(target[0] * width / 1000.0)
abs_y = int(target[1] * height / 1000.0)
success = await driver.long_press(abs_x, abs_y, duration_ms=duration)
return (
f"Long press executed at normalized {target} for {duration}ms"
if success
else f"Long press failed at {target}"
)
@artemis_tool(
name="input_text",
description="[ACTION] Clicks an input field target [x, y] and types text into it.",
args_schema=InputTextArgs,
category="action",
)
async def input_text(
text: str, target: list[int], clear_exist: bool = True, driver: BaseDeviceDriver = None
) -> str:
# First tap to focus
width, height = driver.screen_size
abs_x = int(target[0] * width / 1000.0)
abs_y = int(target[1] * height / 1000.0)
await driver.tap(abs_x, abs_y)
await asyncio.sleep(0.2)
# Then input text
success = await driver.input_text(text, clear_existing=clear_exist)
return (
f"Input text '{text}' into target {target}"
if success
else f"Failed to input text into {target}"
)
@artemis_tool(
name="swipe",
description=(
"[ACTION] Performs smart directional swipe for browsing, or precise coordinate drag/slider gestures."
),
args_schema=SwipeArgs,
category="action",
)
async def swipe(
action: Any = None,
direction: str | None = None,
start: list[int] | None = None,
end: list[int] | None = None,
duration: int = 400,
driver: BaseDeviceDriver = None,
**kwargs: Any,
) -> str:
params = dict(kwargs)
if action is not None:
params["action"] = action
if direction is not None:
params["direction"] = direction
if start is not None:
params["start"] = start
if end is not None:
params["end"] = end
params["duration"] = duration
kind, target, dur = parse_swipe_parameters(params, default_duration=duration)
if kind == "direction" and isinstance(target, str):
success = await driver.swipe_direction(target, duration_ms=dur)
return f"Swiped {target}" if success else f"Swipe {target} failed"
elif kind == "coords" and isinstance(target, list) and len(target) == 4:
width, height = driver.screen_size
start_x = int(target[0] * width / 1000.0)
start_y = int(target[1] * height / 1000.0)
end_x = int(target[2] * width / 1000.0)
end_y = int(target[3] * height / 1000.0)
success = await driver.swipe(start_x, start_y, end_x, end_y, duration_ms=dur)
return (
f"Swiped from {target[:2]} to {target[2:]}" if success else "Swipe coordinates failed"
)
return f"Invalid swipe action parameter: {action or direction or (start, end)}"
@artemis_tool(
name="press_key",
description="[ACTION] Presses a hardware or system key on the device ('home', 'back', 'enter', 'delete', 'power').",
args_schema=PressKeyArgs,
category="action",
)
async def press_key(key: str, driver: BaseDeviceDriver) -> str:
success = await driver.press_key(key)
return f"Key '{key}' pressed" if success else f"Failed to press key '{key}'"
@artemis_tool(
name="wait_for_delay",
description="[ACTION] Pauses execution for a specified number of seconds to allow UI loading or transition.",
args_schema=WaitForDelayArgs,
category="action",
)
async def wait_for_delay(seconds: float, driver: BaseDeviceDriver = None) -> str:
await asyncio.sleep(seconds)
return f"Waited for {seconds} seconds."