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
This commit is contained in:
somew1nd committed 2026-08-13 11:46:47 -07:00
commit 206a9ce921
229 files changed
+49653

No files matched your search

+212
View File
@@ -0,0 +1,212 @@
# 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 enum import Enum
import logging
import os
from pathlib import Path
import queue
import sys
import threading
from colorama import Fore, Style, init
from artemis.data_engine.context_vars import CURRENT_TRACE_ID
init(autoreset=True)
class LogLevel(Enum):
DEBUG = ("DEBUG", Fore.MAGENTA, "🔍")
INFO = ("INFO", Fore.WHITE, "ℹ")
SUCCESS = ("SUCCESS", Fore.GREEN, "✓")
WARNING = ("WARNING", Fore.YELLOW, "⚠")
ERROR = ("ERROR", Fore.RED, "❌")
CRITICAL = ("CRITICAL", Fore.RED + Style.BRIGHT, "💥")
class DataEngineHandler(logging.Handler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._log_queue = queue.Queue(maxsize=5000)
self._worker_thread = threading.Thread(target=self._drain_queue, daemon=True)
self._worker_thread.start()
def _drain_queue(self):
while True:
item = self._log_queue.get()
if item is None:
break
engine_mod = sys.modules.get("artemis.data_engine.engine")
if engine_mod and getattr(engine_mod, "_CURRENT_DATA_ENGINE", None):
_CURRENT_DATA_ENGINE = engine_mod._CURRENT_DATA_ENGINE
if _CURRENT_DATA_ENGINE and _CURRENT_DATA_ENGINE.current_session_id:
record_name, message, level_name, formatted, parent_id = item
_CURRENT_DATA_ENGINE.record_trace(
type="log",
name=record_name,
payload={
"message": message,
"level": level_name,
"formatted": formatted,
},
status="success",
parent_trace_id=parent_id,
)
self._log_queue.task_done()
def emit(self, record):
engine_mod = sys.modules.get("artemis.data_engine.engine")
if not engine_mod or not getattr(engine_mod, "_CURRENT_DATA_ENGINE", None):
return
_CURRENT_DATA_ENGINE = engine_mod._CURRENT_DATA_ENGINE
if _CURRENT_DATA_ENGINE and _CURRENT_DATA_ENGINE.current_session_id:
log_entry = self.format(record)
log_level = getattr(record, "log_level", None)
if log_level and hasattr(log_level, "value"):
level_name, _color, _symbol = log_level.value
else:
level_name = record.levelname
parent_id = CURRENT_TRACE_ID.get()
try:
self._log_queue.put_nowait(
(
record.name,
record.getMessage(),
level_name,
log_entry,
parent_id,
)
)
except queue.Full as e:
raise RuntimeError(
"DataEngine log queue is full - cannot accept more log"
" entries without dropping records"
) from e
class ArtemisLogger:
def __init__(
self,
name: str,
console_level: str = "INFO",
):
"""Initialize the ARTEMIS logger.
Args:
name: Logger name (usually __name__)
console_level: Minimum level for console output
"""
self.name = name
self.logger = logging.getLogger(name)
self.logger.setLevel(logging.DEBUG)
self.logger.handlers.clear()
self.logger.propagate = False
self._setup_console_handler(console_level)
# Add DataEngineHandler
data_engine_handler = DataEngineHandler()
data_engine_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(fmt="%(asctime)s | %(name)s | %(levelname)s | %(message)s")
data_engine_handler.setFormatter(formatter)
self.logger.addHandler(data_engine_handler)
if os.environ.get("ARTEMIS_MCP_SERVER") == "true":
traces_dir = os.environ.get("ARTEMIS_TRACES_PATH", "traces")
log_path = Path(traces_dir) / "mcp_server.log"
log_path.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
def _setup_console_handler(self, level: str):
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(getattr(logging, level.upper()))
console_formatter = ColoredFormatter()
console_handler.setFormatter(console_formatter)
self.logger.addHandler(console_handler)
def debug(self, message: str, **kwargs):
self.logger.debug(message, extra={"log_level": LogLevel.DEBUG}, **kwargs)
def info(self, message: str, **kwargs):
self.logger.info(message, extra={"log_level": LogLevel.INFO}, **kwargs)
def success(self, message: str, **kwargs):
self.logger.info(message, extra={"log_level": LogLevel.SUCCESS}, **kwargs)
def warning(self, message: str, **kwargs):
self.logger.warning(message, extra={"log_level": LogLevel.WARNING}, **kwargs)
def error(self, message: str, **kwargs):
self.logger.error(message, extra={"log_level": LogLevel.ERROR}, **kwargs)
def critical(self, message: str, **kwargs):
self.logger.critical(message, extra={"log_level": LogLevel.CRITICAL}, **kwargs)
def header(self, message: str, **_kwargs):
separator = "=" * 60
colored_separator = f"{Fore.CYAN}{separator}{Style.RESET_ALL}"
colored_message = f"{Fore.CYAN}{message}{Style.RESET_ALL}"
print(colored_separator)
print(colored_message)
print(colored_separator)
self.logger.info(f"\n{separator}\n{message}\n{separator}")
class ColoredFormatter(logging.Formatter):
def format(self, record):
log_level = getattr(record, "log_level", LogLevel.INFO)
_level_name, color, symbol = log_level.value
colored_message = f"{color}{symbol} {record.getMessage()}{Style.RESET_ALL}"
return colored_message
_loggers = {}
def get_logger(
name: str,
console_level: str = "INFO",
) -> ArtemisLogger:
"""Get or create a logger instance.
Args:
name: Logger name (usually __name__)
log_file: Path to log file (defaults to logs/{name}.log)
console_level: Minimum level for console output
file_level: Minimum level for file output
enable_file_logging: Whether to enable file logging
Returns:
ArtemisLogger instance
"""
if name not in _loggers:
_loggers[name] = ArtemisLogger(
name=name,
console_level=console_level,
)
return _loggers[name]