Files
Artemis-Eapil/artemis/utils/ui_hierarchy.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

167 lines
5.1 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.
from artemis.utils.logger import get_logger
from pydantic import BaseModel, Field
logger = get_logger(__name__)
def __find_element_by_resource_id_in_rich_hierarchy(
hierarchy: list[dict], resource_id: str
) -> dict | None:
"""Retrieves all the sibling elements for a given resource ID from a nested dictionary.
Args:
hierarchy (dict): The nested dictionary representing the UI hierarchy.
resource_id (str): The resource-id to find.
Returns:
list: A list of the sibling elements, or None if the resource_id is not
found.
"""
if not hierarchy:
return None
for child in hierarchy:
if child.get("attributes", {}).get("resource-id") == resource_id:
return child.get("attributes", {})
for child in hierarchy:
result = __find_element_by_resource_id_in_rich_hierarchy(
child.get("children", []), resource_id
)
if result is not None:
return result
return None
def text_input_is_empty(text: str | None, hint_text: str | None) -> bool:
return not text or text == hint_text
def find_element_by_resource_id(
ui_hierarchy: list[dict],
resource_id: str,
index: int | None = None,
is_rich_hierarchy: bool = False,
) -> dict | None:
"""Find a UI element by its resource-id in the UI hierarchy.
Args:
ui_hierarchy: List of UI element dictionaries
resource_id: The resource-id to search for (e.g.,
"com.google.android.settings.intelligence:id/open_search_view_edit_text")
Returns:
The complete UI element dictionary if found, None otherwise
"""
if is_rich_hierarchy:
return __find_element_by_resource_id_in_rich_hierarchy(ui_hierarchy, resource_id)
target_idx = index or 0
def search_recursive(elements: list[dict]) -> dict | None:
nonlocal target_idx
for element in elements:
if isinstance(element, dict):
if element.get("resourceId") == resource_id:
if target_idx == 0:
return element
target_idx -= 1
continue
children = element.get("children", [])
if children:
result = search_recursive(children)
if result:
return result
return None
return search_recursive(ui_hierarchy)
def is_element_focused(element: dict) -> bool:
return element.get("focused", None) == "true"
def get_element_text(element: dict, hint_text: bool = False) -> str | None:
if hint_text:
return element.get("hintText", None)
return element.get("text", None)
class _CyFunctionDetectorMeta(type):
def __instancecheck__(self, instance):
name = type(instance).__name__
return (
name
in (
"cyfunction",
"cython_function_or_method",
"builtin_function_or_method",
)
or "cyfunction" in name.lower()
)
class CyFunctionDetector(metaclass=_CyFunctionDetectorMeta):
pass
class Point(BaseModel):
model_config = {"ignored_types": (CyFunctionDetector,)}
x: int
y: int
class ElementBounds(BaseModel):
model_config = {"ignored_types": (CyFunctionDetector,)}
x: int = Field(description="The x coordinate of the top-left corner of the element.")
y: int = Field(description="The y coordinate of the top-left corner of the element.")
width: int = Field(description="The width of the element.")
height: int = Field(description="The height of the element.")
def get_center(self) -> Point:
return Point(x=self.x + self.width // 2, y=self.y + self.height // 2)
def get_relative_point(self, x_percent: float, y_percent: float) -> Point:
"""Returns the coordinates of the point at x_percent of the width and y_percent
of the height of the element.
Ex if x_percent = 0.95 and y_percent = 0.95,
the point is at the bottom right of the element:
<------>
| |
| x|
<------>
"""
return Point(
x=int(self.x + self.width * x_percent),
y=int(self.y + self.height * y_percent),
)
def get_bounds_for_element(element: dict) -> ElementBounds | None:
bounds = element.get("bounds", None)
if bounds:
try:
return ElementBounds(**bounds)
except Exception as e:
logger.error(f"Failed to validate bounds: {e}")
return None
return None