Files
Artemis-Eapil/artemis/interfaces/cli/commands/server.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

138 lines
5.0 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.
"""Server and cloud proxy commands (artemis server)."""
import http.server
import socketserver
import subprocess
import time
from typing import Annotated
import urllib.error
import urllib.request
from artemis.utils.logger import get_logger
import typer
logger = get_logger(__name__)
server_app = typer.Typer(help="Cloud proxy and server management commands.")
cached_token = ""
token_expire_time = 0.0
def get_identity_token() -> str:
"""Fetch Google Cloud IAM identity token for authenticated requests."""
global cached_token, token_expire_time
now = time.time()
if not cached_token or now >= token_expire_time:
try:
res = subprocess.check_output(
["gcloud", "auth", "print-identity-token"], text=True
).strip()
cached_token = res
token_expire_time = now + 1800 # cache 30 mins
except Exception as e:
logger.warning(f"Failed to get GCP identity token: {e}")
return ""
return cached_token
class CloudRunProxyHandler(http.server.BaseHTTPRequestHandler):
target_url: str = "https://artemis-cloud-brain-971929466682.us-central1.run.app"
def log_message(self, format, *args):
logger.info(f"[{self.log_date_time_string()}] {format % args}")
def do_GET(self):
self.proxy_request("GET")
def do_POST(self):
self.proxy_request("POST")
def do_DELETE(self):
self.proxy_request("DELETE")
def do_OPTIONS(self):
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "*")
self.end_headers()
def proxy_request(self, method: str):
url = f"{self.target_url}{self.path}"
headers = {}
for k, v in self.headers.items():
if k.lower() not in ("host", "authorization", "content-length"):
headers[k] = v
token = get_identity_token()
if token:
headers["Authorization"] = f"Bearer {token}"
headers["X-Artemis-Token"] = "alice-token"
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length) if content_length > 0 else None
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
self.send_response(resp.status)
for k, v in resp.getheaders():
if k.lower() not in ("transfer-encoding", "content-encoding", "content-length"):
self.send_header(k, v)
data = resp.read()
self.send_header("Content-Length", str(len(data)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(data)
except urllib.error.HTTPError as e:
self.send_response(e.code)
for k, v in e.headers.items():
if k.lower() not in ("transfer-encoding", "content-encoding", "content-length"):
self.send_header(k, v)
data = e.read()
self.send_header("Content-Length", str(len(data)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(data)
except Exception as e:
self.send_response(500)
self.end_headers()
self.wfile.write(f"Proxy Error: {e}".encode())
@server_app.command("web")
def web_proxy_command(
port: Annotated[int, typer.Option("--port", "-p", help="Local port to bind.")] = 8080,
target: Annotated[
str,
typer.Option("--target", "-t", help="Target Cloud Run URL."),
] = "https://artemis-cloud-brain-971929466682.us-central1.run.app",
) -> None:
"""Start the local authenticated proxy for ARTEMIS Cloud Web Dashboard."""
CloudRunProxyHandler.target_url = target
typer.secho(
f"πŸš€ Starting Artemis Cloud Web Proxy at http://localhost:{port}", fg=typer.colors.GREEN
)
typer.secho(f"πŸ”— Forwarding authenticated requests to: {target}", fg=typer.colors.CYAN)
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("", port), CloudRunProxyHandler) as httpd:
try:
httpd.serve_forever()
except KeyboardInterrupt:
typer.secho("\nπŸ›‘ Stopping server.", fg=typer.colors.YELLOW)