feat: introduce MCP server, cross-platform runtime, and Showcase UI reorganization
- Showcase UI Reorganization: * Move web UI dashboard from root directory into apps/showcase_ui/ for cleaner project structure * Add 'artemis ui' CLI command (artemis/interfaces/cli/commands/ui.py) and launcher scripts for the interactive showcase console - Standalone MCP Server & Notifiers (mcp_server/): * Implement dedicated MCP server package with mobile automation tools (device_state, inspect_trace, task_manager, task_runner) * Add multi-channel notifiers (AgentAPI, Desktop notifications, Webhooks, File output, and Composite routing) * Introduce background task runner and trace store for autonomous mobile execution monitoring - Cross-Platform Runtime & Toolchain Support: * Add artemis/platform/ with OS-specific implementations for Linux, macOS (Darwin), and Windows * Add artemis/runtime/supervisor.py for resilient process and agent runtime supervision * Add artemis/toolchain/ with dependency resolution and SDK descriptors * Add cross-platform launcher scripts (start.sh, start.bat) and installer scripts (scripts/install_deps.*) - Core Enhancements & Diagnostics: * Enhance coordinate normalization, OCR API fallback, and perception pipeline * Add credential validation (credentials_validator.py) and system diagnostics * Update bilingual documentation (README.md, README_CN.md) and architecture diagrams - Test Suite Expansion: * Add integration tests for cloud gateway, remote ADB, and Artemis login * Add unit tests covering core diagnostics, platform detection, toolchain, credentials validator, optional OCR, and MCP tools
This commit is contained in:
1 parent
78e478f502
commit
9a5aaf99b7
249 files changed
+37226
-9339
No files matched your search
@@ -1,157 +0,0 @@
|
||||
# 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.
|
||||
|
||||
"""Benchmark and evaluation command (artemis bench)."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Annotated
|
||||
|
||||
from artemis.config import initialize_llm_config
|
||||
from artemis.sdk import Agent
|
||||
from artemis.sdk.builders import Builders
|
||||
from artemis.sdk.types.task import AgentProfile
|
||||
from artemis.utils.logger import get_logger
|
||||
import typer
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def check_and_start_emulator(adb_path: str = "adb") -> bool:
|
||||
"""Checks if Android emulator is running; optionally starts it."""
|
||||
try:
|
||||
res = subprocess.run([adb_path, "devices"], capture_output=True, text=True, timeout=5)
|
||||
for line in res.stdout.splitlines():
|
||||
if "emulator-" in line and "device" in line:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
async def run_benchmark(
|
||||
task_name: str | None = None,
|
||||
profile_name: str = "pro",
|
||||
console_port: int = 5554,
|
||||
adb_path: str = "adb",
|
||||
) -> None:
|
||||
"""Runs AndroidWorld benchmark task evaluations."""
|
||||
# Add android_world-main if present
|
||||
workspace_root = Path(__file__).resolve().parent.parent.parent.parent.parent
|
||||
aw_path = workspace_root / "android_world-main"
|
||||
if aw_path.exists() and str(aw_path) not in sys.path:
|
||||
sys.path.insert(0, str(aw_path))
|
||||
|
||||
try:
|
||||
from android_world import registry
|
||||
from android_world.env import env_launcher
|
||||
except ImportError:
|
||||
logger.error("AndroidWorld benchmark suite is not installed or available in path.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
task_registry = registry.TaskRegistry()
|
||||
aw_registry = task_registry.get_registry(task_registry.ANDROID_WORLD_FAMILY)
|
||||
|
||||
if task_name and task_name not in aw_registry:
|
||||
logger.error(f"Task '{task_name}' not found in AndroidWorld registry.")
|
||||
available = sorted(list(aw_registry.keys()))
|
||||
logger.info(f"Available tasks ({len(available)}): {', '.join(available[:10])}...")
|
||||
raise typer.Exit(1)
|
||||
|
||||
logger.info(f"Initializing AndroidWorld environment on console port {console_port}...")
|
||||
env = env_launcher.load_and_setup_env(
|
||||
console_port=console_port,
|
||||
emulator_setup=False,
|
||||
adb_path=adb_path,
|
||||
)
|
||||
|
||||
llm_config = initialize_llm_config()
|
||||
profile = AgentProfile(name="default", llm_config=llm_config)
|
||||
config = Builders.AgentConfig.with_default_profile(profile).build()
|
||||
|
||||
agent = Agent(config=config)
|
||||
await agent.init()
|
||||
|
||||
tasks_to_run = [task_name] if task_name else list(aw_registry.keys())
|
||||
|
||||
try:
|
||||
for t_name in tasks_to_run:
|
||||
task_type = aw_registry[t_name]
|
||||
params = task_type.generate_random_params()
|
||||
task_instance = task_type(params)
|
||||
|
||||
logger.info(f"\n🎯 [Benchmark] Starting: {t_name} | Goal: {task_instance.goal}")
|
||||
task_instance.initialize_task(env)
|
||||
|
||||
result = await agent.run_task(
|
||||
goal=task_instance.goal,
|
||||
name=f"bench_{t_name}",
|
||||
profile=profile_name,
|
||||
)
|
||||
success = task_instance.is_successful(env)
|
||||
logger.info(f"🏆 Task '{t_name}' Result: {result} | Benchmark Verified: {success}")
|
||||
finally:
|
||||
await agent.clean()
|
||||
env.close()
|
||||
|
||||
|
||||
def bench_command(
|
||||
task: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--task",
|
||||
"-t",
|
||||
help="Specific AndroidWorld task name to evaluate (e.g. Clock_1, Calculator_1).",
|
||||
),
|
||||
] = None,
|
||||
profile: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--profile",
|
||||
"-p",
|
||||
help="Agent profile to evaluate ('flash' or 'pro').",
|
||||
),
|
||||
] = "pro",
|
||||
adb_path: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--adb-path",
|
||||
help="Path to ADB executable.",
|
||||
),
|
||||
] = "adb",
|
||||
console_port: Annotated[
|
||||
int,
|
||||
typer.Option(
|
||||
"--port",
|
||||
help="Emulator console port.",
|
||||
),
|
||||
] = 5554,
|
||||
) -> None:
|
||||
"""Run AndroidWorld benchmark task evaluations against the agent."""
|
||||
if not check_and_start_emulator(adb_path=adb_path):
|
||||
typer.secho(
|
||||
f"Warning: No active emulator detected on port {console_port}. Please start an emulator.",
|
||||
fg=typer.colors.YELLOW,
|
||||
)
|
||||
|
||||
asyncio.run(
|
||||
run_benchmark(
|
||||
task_name=task,
|
||||
profile_name=profile,
|
||||
console_port=console_port,
|
||||
adb_path=adb_path,
|
||||
)
|
||||
)
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
"""System, environment, and device diagnostics (artemis doctor)."""
|
||||
|
||||
import shutil
|
||||
import platform as sys_platform
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
@@ -26,16 +26,21 @@ from artemis.config import (
|
||||
parse_llm_config,
|
||||
settings,
|
||||
)
|
||||
from artemis.platform import OSType, platform
|
||||
from artemis.toolchain import toolchain
|
||||
from artemis.utils.ocr_api import is_ocr_configured
|
||||
|
||||
|
||||
def doctor_command() -> None:
|
||||
"""Run diagnostics to inspect system dependencies, device connectivity, and configuration."""
|
||||
console = Console()
|
||||
console.print()
|
||||
os_name = sys_platform.system()
|
||||
arch_name = sys_platform.machine()
|
||||
console.print(
|
||||
Panel(
|
||||
"[bold cyan]☕ Artemis System & Environment Doctor[/bold cyan]\n"
|
||||
"[dim]Checking prerequisites for autonomous mobile testing...[/dim]",
|
||||
f"[bold cyan]☕ Artemis System & Environment Doctor[/bold cyan]\n"
|
||||
f"[dim]Platform: {os_name} ({arch_name}) | PAL: {platform.os_type.value}[/dim]",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
@@ -53,7 +58,7 @@ def doctor_command() -> None:
|
||||
table.add_row(
|
||||
"Python Runtime",
|
||||
"[bold green]✔ OK[/bold green]",
|
||||
f"Python {py_ver} (64-bit)",
|
||||
f"Python {py_ver} ({arch_name}, 64-bit)",
|
||||
)
|
||||
else:
|
||||
all_passed = False
|
||||
@@ -64,7 +69,7 @@ def doctor_command() -> None:
|
||||
)
|
||||
|
||||
# 2. ADB Installation & Server
|
||||
adb_path = shutil.which("adb")
|
||||
adb_path = toolchain.resolve("adb")
|
||||
if adb_path:
|
||||
try:
|
||||
res = subprocess.run(
|
||||
@@ -72,6 +77,8 @@ def doctor_command() -> None:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
adb_info = res.stdout.splitlines()[0] if res.stdout else "Installed"
|
||||
table.add_row(
|
||||
@@ -81,10 +88,17 @@ def doctor_command() -> None:
|
||||
table.add_row("Android ADB", "[bold green]✔ OK[/bold green]", f"{adb_path}")
|
||||
else:
|
||||
all_passed = False
|
||||
hint = (
|
||||
"Run: winget install Google.PlatformTools"
|
||||
if platform.os_type == OSType.WINDOWS
|
||||
else "Run: brew install android-platform-tools"
|
||||
if platform.os_type == OSType.MACOS
|
||||
else "Run: sudo apt-get install -y adb"
|
||||
)
|
||||
table.add_row(
|
||||
"Android ADB",
|
||||
"[bold red]✖ Missing[/bold red]",
|
||||
"adb not found in PATH. Install Android Platform-Tools.",
|
||||
f"adb not found in PATH or Android SDK. {hint}",
|
||||
)
|
||||
|
||||
# 3. Connected Android Devices
|
||||
@@ -96,6 +110,8 @@ def doctor_command() -> None:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
lines = [line_str.strip() for line_str in res.stdout.splitlines() if line_str.strip()]
|
||||
for line in lines[1:]: # skip 'List of devices attached'
|
||||
@@ -123,7 +139,7 @@ def doctor_command() -> None:
|
||||
table.add_row(
|
||||
"Android Devices",
|
||||
"[bold yellow]⚠ Unauthorized[/bold yellow]",
|
||||
"Device detected but unauthorized. Check phone screen for USB Debugging permission prompt.",
|
||||
"Device detected but unauthorized. Check phone screen for USB Debugging prompt.",
|
||||
)
|
||||
else:
|
||||
table.add_row(
|
||||
@@ -160,11 +176,11 @@ def doctor_command() -> None:
|
||||
table.add_row(
|
||||
"LLM Providers",
|
||||
"[bold red]✖ Missing[/bold red]",
|
||||
"No LLM key found in .env. Run [bold cyan]artemis init[/bold cyan] to configure in 10 seconds.",
|
||||
"No LLM key found in .env. Run [bold cyan]artemis init[/bold cyan] to configure.",
|
||||
)
|
||||
|
||||
# 5. Vision OCR Key Check
|
||||
if ocr_key:
|
||||
# 5. Vision OCR Key Check (Optional)
|
||||
if is_ocr_configured() and ocr_key:
|
||||
val = ocr_key.get_secret_value()
|
||||
masked = f"{val[:6]}...{val[-4:]}" if len(val) > 10 else "***"
|
||||
table.add_row(
|
||||
@@ -175,8 +191,8 @@ def doctor_command() -> None:
|
||||
else:
|
||||
table.add_row(
|
||||
"Vision OCR",
|
||||
"[bold yellow]⚠ Missing[/bold yellow]",
|
||||
"OCR_API_KEY / API_KEY not set. Perception fast path will use fallback.",
|
||||
"[bold cyan]○ Optional[/bold cyan]",
|
||||
"OCR_API_KEY is not set. Perception operates in pure XML layout mode.",
|
||||
)
|
||||
|
||||
# 6. Configuration File Health
|
||||
@@ -197,33 +213,45 @@ def doctor_command() -> None:
|
||||
)
|
||||
|
||||
# 7. Video Recording Tools (FFmpeg & scrcpy)
|
||||
ffmpeg_path = shutil.which("ffmpeg")
|
||||
scrcpy_path = shutil.which("scrcpy")
|
||||
ffmpeg_path = toolchain.resolve("ffmpeg")
|
||||
scrcpy_path = toolchain.resolve("scrcpy")
|
||||
|
||||
if ffmpeg_path:
|
||||
table.add_row(
|
||||
"FFmpeg Tools",
|
||||
"[bold green]✔ Installed[/bold green]",
|
||||
f"{ffmpeg_path} (Video encoding/trimming ready)",
|
||||
"FFmpeg Tools", "[bold green]✔ Installed[/bold green]", f"{ffmpeg_path} (Video ready)"
|
||||
)
|
||||
else:
|
||||
hint = (
|
||||
"winget install Gyan.FFmpeg"
|
||||
if platform.os_type == OSType.WINDOWS
|
||||
else "brew install ffmpeg"
|
||||
if platform.os_type == OSType.MACOS
|
||||
else "sudo apt-get install -y ffmpeg"
|
||||
)
|
||||
table.add_row(
|
||||
"FFmpeg Tools",
|
||||
"[dim]⚪ Optional[/dim]",
|
||||
"Not found. Install ffmpeg for video compression and audio analysis.",
|
||||
f"Not found. ({hint})",
|
||||
)
|
||||
|
||||
if scrcpy_path:
|
||||
table.add_row(
|
||||
"Screen Recorder",
|
||||
"[bold green]✔ Installed[/bold green]",
|
||||
f"{scrcpy_path} (Live screen recording ready)",
|
||||
f"{scrcpy_path} (Live stream ready)",
|
||||
)
|
||||
else:
|
||||
hint = (
|
||||
"winget install Genymobile.scrcpy"
|
||||
if platform.os_type == OSType.WINDOWS
|
||||
else "brew install scrcpy"
|
||||
if platform.os_type == OSType.MACOS
|
||||
else "sudo apt-get install -y scrcpy"
|
||||
)
|
||||
table.add_row(
|
||||
"Screen Recorder",
|
||||
"[dim]⚪ Optional[/dim]",
|
||||
"scrcpy not found. Install scrcpy for --with-video-recording-tools.",
|
||||
f"scrcpy not found. ({hint})",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
@@ -14,13 +14,40 @@
|
||||
|
||||
"""Model Context Protocol (MCP) server command (artemis mcp)."""
|
||||
|
||||
import json
|
||||
from typing import Annotated
|
||||
|
||||
from mcp_server.base import mcp as agent_mcp
|
||||
from mcp_server.utils import env_utils
|
||||
from artemis.mcp.adb_server import mcp as adb_mcp
|
||||
from artemis.utils.logger import get_logger
|
||||
from rich.console import Console
|
||||
from rich.syntax import Syntax
|
||||
import typer
|
||||
|
||||
logger = get_logger(__name__)
|
||||
console = Console()
|
||||
|
||||
|
||||
def _get_config_snippet(client: str, python_exe: str, project_root: str) -> dict:
|
||||
"""Generates MCP configuration dictionary for the specified client."""
|
||||
config_body = {
|
||||
"command": python_exe,
|
||||
"args": ["-m", "mcp_server"],
|
||||
"cwd": project_root,
|
||||
"env": {
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
},
|
||||
}
|
||||
|
||||
if client == "cursor":
|
||||
return {"mcpServers": {"artemis": config_body}}
|
||||
elif client in ("claude", "claude_code", "claude_desktop"):
|
||||
return {"mcpServers": {"artemis": config_body}}
|
||||
elif client == "openclaw":
|
||||
return {"plugins": {"artemis_mcp": {"enabled": True, "type": "mcp", "server": config_body}}}
|
||||
else: # generic
|
||||
return {"mcpServers": {"artemis": config_body}}
|
||||
|
||||
|
||||
def mcp_command(
|
||||
@@ -29,14 +56,89 @@ def mcp_command(
|
||||
typer.Option(
|
||||
"--type",
|
||||
"-t",
|
||||
help="Type of MCP server to start ('adb' or 'xml').",
|
||||
help="Type of MCP server to start: 'agent' (default, universal IDE mobile agent), 'adb' (raw adb), 'xml' (xml fuzzy search).",
|
||||
),
|
||||
] = "adb",
|
||||
] = "agent",
|
||||
transport: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--transport",
|
||||
help="MCP transport protocol ('stdio' or 'sse').",
|
||||
),
|
||||
] = "stdio",
|
||||
host: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--host",
|
||||
help="Host address when running with SSE transport.",
|
||||
),
|
||||
] = "127.0.0.1",
|
||||
port: Annotated[
|
||||
int,
|
||||
typer.Option(
|
||||
"--port",
|
||||
"-p",
|
||||
help="Port number when running with SSE transport.",
|
||||
),
|
||||
] = 8001,
|
||||
generate_config: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--generate-config",
|
||||
"-g",
|
||||
help="Output ready-to-use MCP configuration JSON for 'cursor', 'claude', 'openclaw', or 'all'.",
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Launch a ARTEMIS Model Context Protocol (MCP) server."""
|
||||
if server_type.lower() == "adb":
|
||||
logger.info("Starting Artemis ADB MCP Server over stdio...")
|
||||
adb_mcp.run(transport="stdio")
|
||||
"""Launch or configure the ARTEMIS Model Context Protocol (MCP) server."""
|
||||
project_root = env_utils.get_project_root()
|
||||
python_exe = env_utils.resolve_python_executable(project_root)
|
||||
|
||||
if generate_config:
|
||||
client = generate_config.lower()
|
||||
if client == "all":
|
||||
all_configs = {
|
||||
"cursor (.cursor/mcp.json)": _get_config_snippet(
|
||||
"cursor", python_exe, project_root
|
||||
),
|
||||
"claude (claude_desktop_config.json)": _get_config_snippet(
|
||||
"claude", python_exe, project_root
|
||||
),
|
||||
"openclaw (openclaw.json)": _get_config_snippet(
|
||||
"openclaw", python_exe, project_root
|
||||
),
|
||||
}
|
||||
json_str = json.dumps(all_configs, indent=2)
|
||||
else:
|
||||
snippet = _get_config_snippet(client, python_exe, project_root)
|
||||
json_str = json.dumps(snippet, indent=2)
|
||||
|
||||
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False)
|
||||
console.print(f"[bold cyan]MCP Configuration for {client.upper()}:[/bold cyan]")
|
||||
console.print(syntax)
|
||||
raise typer.Exit(0)
|
||||
|
||||
st = server_type.lower()
|
||||
if st in ("agent", "mobile", "artemis", "default"):
|
||||
logger.info(f"Starting Artemis Mobile Agent MCP Server over {transport}...")
|
||||
if transport.lower() == "sse":
|
||||
agent_mcp.run(transport="sse", host=host, port=port)
|
||||
else:
|
||||
agent_mcp.run(transport="stdio")
|
||||
elif st == "adb":
|
||||
logger.info(f"Starting Artemis ADB MCP Server over {transport}...")
|
||||
if transport.lower() == "sse":
|
||||
adb_mcp.run(transport="sse", host=host, port=port)
|
||||
else:
|
||||
adb_mcp.run(transport="stdio")
|
||||
elif st == "xml":
|
||||
from artemis.mcp.xml_search_server import mcp as xml_mcp
|
||||
|
||||
logger.info(f"Starting Artemis XML Fuzzy Search MCP Server over {transport}...")
|
||||
if transport.lower() == "sse":
|
||||
xml_mcp.run(transport="sse", host=host, port=port)
|
||||
else:
|
||||
xml_mcp.run(transport="stdio")
|
||||
else:
|
||||
logger.error(f"Unsupported MCP server type: {server_type}")
|
||||
logger.error(f"Unsupported MCP server type: {server_type}. Use 'agent', 'adb', or 'xml'.")
|
||||
raise typer.Exit(1)
|
||||
@@ -15,6 +15,7 @@
|
||||
"""Server and cloud proxy commands (artemis server)."""
|
||||
|
||||
import http.server
|
||||
import os
|
||||
import socketserver
|
||||
import subprocess
|
||||
import time
|
||||
@@ -50,7 +51,7 @@ def get_identity_token() -> str:
|
||||
|
||||
|
||||
class CloudRunProxyHandler(http.server.BaseHTTPRequestHandler):
|
||||
target_url: str = "https://artemis-cloud-brain-971929466682.us-central1.run.app"
|
||||
target_url: str = os.getenv("ARTEMIS_CLOUD_URL", "http://127.0.0.1:8000")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
logger.info(f"[{self.log_date_time_string()}] {format % args}")
|
||||
@@ -81,7 +82,9 @@ class CloudRunProxyHandler(http.server.BaseHTTPRequestHandler):
|
||||
token = get_identity_token()
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
headers["X-Artemis-Token"] = "alice-token"
|
||||
tenant_token = os.getenv("ARTEMIS_TENANT_TOKEN")
|
||||
if tenant_token:
|
||||
headers["X-Artemis-Token"] = tenant_token
|
||||
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(content_length) if content_length > 0 else None
|
||||
@@ -119,8 +122,10 @@ 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",
|
||||
typer.Option(
|
||||
"--target", "-t", help="Target Artemis Cloud URL.", envvar="ARTEMIS_CLOUD_URL"
|
||||
),
|
||||
] = "http://127.0.0.1:8000",
|
||||
) -> None:
|
||||
"""Start the local authenticated proxy for ARTEMIS Cloud Web Dashboard."""
|
||||
CloudRunProxyHandler.target_url = target
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# 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.
|
||||
|
||||
"""UI Launch and Web Console CLI Command (artemis ui)."""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import Annotated
|
||||
import urllib.request
|
||||
import webbrowser
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
import typer
|
||||
|
||||
|
||||
def _poll_and_open_browser(url: str, stop_event: threading.Event, timeout: float = 15.0) -> None:
|
||||
"""Poll the UI server until it responds, then launch the user's default browser."""
|
||||
start = time.time()
|
||||
while not stop_event.is_set() and (time.time() - start < timeout):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Artemis-HealthCheck/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=1.5) as resp:
|
||||
if resp.status == 200:
|
||||
time.sleep(0.3)
|
||||
webbrowser.open(url)
|
||||
return
|
||||
except Exception:
|
||||
time.sleep(0.4)
|
||||
|
||||
|
||||
def ui_command(
|
||||
host: Annotated[
|
||||
str,
|
||||
typer.Option("--host", "-h", help="Bind host address for the UI server."),
|
||||
] = "0.0.0.0",
|
||||
port: Annotated[
|
||||
int,
|
||||
typer.Option("--port", "-p", help="Port to run the unified UI server on."),
|
||||
] = 8000,
|
||||
open_browser: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--open/--no-open",
|
||||
help="Automatically open the Showcase UI in default web browser.",
|
||||
),
|
||||
] = True,
|
||||
reload: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--reload/--no-reload",
|
||||
help="Enable uvicorn live auto-reload (for development).",
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Launch the unified Artemis Showcase UI & Admin Console in one click."""
|
||||
console = Console()
|
||||
local_url = f"http://localhost:{port}"
|
||||
|
||||
console.print()
|
||||
msg = (
|
||||
f"[bold cyan]✨ Artemis Autonomous Mobile Agent UI[/bold cyan]\n\n"
|
||||
f"📱 [bold green]Showcase UI & Onboarding:[/bold green] [cyan]{local_url}[/cyan]\n"
|
||||
f"🛠️ [bold magenta]Admin Debug Console:[/bold magenta] [cyan]{local_url}/admin[/cyan]\n"
|
||||
f"📖 [dim]API Documentation:[/dim] [dim]{local_url}/docs[/dim]\n\n"
|
||||
f"[dim]Press Ctrl+C to stop the server.[/dim]"
|
||||
)
|
||||
console.print(Panel(msg, title="🚀 Web Server Active", border_style="cyan", expand=False))
|
||||
# Ensure .env exists in workspace root
|
||||
from artemis.config.paths import ROOT_DIR
|
||||
|
||||
env_file = ROOT_DIR / ".env"
|
||||
env_example = ROOT_DIR / ".env.example"
|
||||
if not env_file.exists():
|
||||
if env_example.exists():
|
||||
try:
|
||||
env_file.write_text(env_example.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
env_file.touch()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
stop_event = threading.Event()
|
||||
if open_browser:
|
||||
t = threading.Thread(
|
||||
target=_poll_and_open_browser,
|
||||
args=(local_url, stop_event),
|
||||
daemon=True,
|
||||
)
|
||||
t.start()
|
||||
|
||||
try:
|
||||
import uvicorn
|
||||
from apps.admin_console.server import app
|
||||
|
||||
uvicorn.run(app, host=host, port=port, reload=reload)
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[yellow]🛑 UI Server stopped.[/yellow]")
|
||||
finally:
|
||||
stop_event.set()
|
||||
@@ -18,13 +18,13 @@ import importlib.metadata
|
||||
from typing import Annotated
|
||||
|
||||
from artemis.interfaces.cli.commands.batch import batch_command
|
||||
from artemis.interfaces.cli.commands.bench import bench_command
|
||||
from artemis.interfaces.cli.commands.doctor import doctor_command
|
||||
from artemis.interfaces.cli.commands.init import init_command
|
||||
from artemis.interfaces.cli.commands.mcp import mcp_command
|
||||
from artemis.interfaces.cli.commands.run import run_command
|
||||
from artemis.interfaces.cli.commands.server import server_app
|
||||
from artemis.interfaces.cli.commands.trace import trace_app
|
||||
from artemis.interfaces.cli.commands.ui import ui_command
|
||||
from artemis.utils.logger import get_logger
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
@@ -41,6 +41,9 @@ app = typer.Typer(
|
||||
)
|
||||
|
||||
# Register subcommands
|
||||
app.command(name="ui", help="Launch the unified Showcase UI & Admin Console in your browser.")(
|
||||
ui_command
|
||||
)
|
||||
app.command(name="run", help="Execute an autonomous task on a mobile device.")(run_command)
|
||||
app.command(name="init", help="Interactive quickstart wizard to configure API keys & device.")(
|
||||
init_command
|
||||
@@ -49,7 +52,6 @@ app.command(name="doctor", help="Check system prerequisites, device status, and
|
||||
doctor_command
|
||||
)
|
||||
app.command(name="batch", help="Execute a batch sequence of automation tasks.")(batch_command)
|
||||
app.command(name="bench", help="Run AndroidWorld benchmark task evaluations.")(bench_command)
|
||||
app.command(name="mcp", help="Start the Artemis Model Context Protocol (MCP) server.")(mcp_command)
|
||||
app.add_typer(server_app, name="server", help="Cloud Run proxy and web dashboard server.")
|
||||
app.add_typer(trace_app, name="trace", help="Inspect and query execution traces.")
|
||||
|
||||
Reference in new issue
Block a user