android-midscene-automation 0.2.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -1
- package/README.md +63 -1
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +10 -8
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +13 -1
- package/src/App.vue +72 -23
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +369 -186
- package/src/types.ts +1 -1
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""System Readiness & Diagnostics Router for Artemis Admin Console."""
|
|
16
|
+
|
|
17
|
+
import ipaddress
|
|
18
|
+
import os
|
|
19
|
+
import secrets
|
|
20
|
+
from urllib.parse import urlsplit
|
|
21
|
+
|
|
22
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
23
|
+
from pydantic import BaseModel, Field
|
|
24
|
+
|
|
25
|
+
from artemis.core.diagnostics import readiness_engine
|
|
26
|
+
from artemis.core.diagnostics.adb_server_connection import (
|
|
27
|
+
InvalidAdbServerEndpoint,
|
|
28
|
+
adb_server_connection,
|
|
29
|
+
)
|
|
30
|
+
from artemis.core.diagnostics.schema import SystemReadinessReport
|
|
31
|
+
|
|
32
|
+
router = APIRouter(prefix="/api/system", tags=["system"])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _require_local_admin_request(request: Request) -> None:
|
|
36
|
+
"""Keep endpoint probing and mutation on the local administration boundary."""
|
|
37
|
+
client_host = request.client.host if request.client else None
|
|
38
|
+
allow_remote = os.getenv("ARTEMIS_ALLOW_REMOTE_ADB_CONFIGURATION", "").lower() in {
|
|
39
|
+
"1",
|
|
40
|
+
"true",
|
|
41
|
+
"yes",
|
|
42
|
+
}
|
|
43
|
+
if client_host and not allow_remote:
|
|
44
|
+
try:
|
|
45
|
+
is_loopback = ipaddress.ip_address(client_host).is_loopback
|
|
46
|
+
except ValueError:
|
|
47
|
+
is_loopback = client_host.lower() == "localhost"
|
|
48
|
+
if not is_loopback:
|
|
49
|
+
raise HTTPException(
|
|
50
|
+
status_code=403,
|
|
51
|
+
detail=(
|
|
52
|
+
"ADB server settings are local-only. Set "
|
|
53
|
+
"ARTEMIS_ALLOW_REMOTE_ADB_CONFIGURATION=true to manage them from another "
|
|
54
|
+
"computer."
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
origin = request.headers.get("origin")
|
|
59
|
+
host = request.headers.get("host")
|
|
60
|
+
if not origin or not host:
|
|
61
|
+
return
|
|
62
|
+
origin_host = urlsplit(origin).netloc.lower()
|
|
63
|
+
if origin_host != host.lower():
|
|
64
|
+
raise HTTPException(
|
|
65
|
+
status_code=403,
|
|
66
|
+
detail="ADB server settings can only be changed from the Artemis console.",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _require_loopback_request(request: Request, detail: str) -> None:
|
|
71
|
+
"""Reject requests whose TCP peer is not the local machine."""
|
|
72
|
+
client_host = request.client.host if request.client else None
|
|
73
|
+
try:
|
|
74
|
+
is_loopback = bool(client_host and ipaddress.ip_address(client_host).is_loopback)
|
|
75
|
+
except ValueError:
|
|
76
|
+
is_loopback = bool(client_host and client_host.lower() == "localhost")
|
|
77
|
+
if not is_loopback:
|
|
78
|
+
raise HTTPException(status_code=403, detail=detail)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _require_local_lifecycle_request(request: Request) -> None:
|
|
82
|
+
"""Authorize a process-lifecycle request from the local CLI only."""
|
|
83
|
+
_require_loopback_request(request, "Server lifecycle controls are local-only.")
|
|
84
|
+
|
|
85
|
+
expected = getattr(request.app.state, "lifecycle_token", None)
|
|
86
|
+
supplied = request.headers.get("x-artemis-lifecycle-token")
|
|
87
|
+
if not (
|
|
88
|
+
isinstance(expected, str)
|
|
89
|
+
and isinstance(supplied, str)
|
|
90
|
+
and secrets.compare_digest(expected, supplied)
|
|
91
|
+
):
|
|
92
|
+
raise HTTPException(status_code=403, detail="Invalid server lifecycle token.")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SelectDeviceRequest(BaseModel):
|
|
96
|
+
"""Payload to select an active target Android device."""
|
|
97
|
+
|
|
98
|
+
serial: str = Field(description="Serial number or identifier of the Android device to select")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.get("/readiness", response_model=SystemReadinessReport)
|
|
102
|
+
async def get_system_readiness(force: bool = False) -> SystemReadinessReport:
|
|
103
|
+
"""Execute all diagnostic probes and return a comprehensive system readiness report."""
|
|
104
|
+
return await readiness_engine.run_all(force_refresh=force)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.post("/devices/select")
|
|
108
|
+
async def select_active_device(request: SelectDeviceRequest):
|
|
109
|
+
"""Select the active Android device or emulator for subsequent automated tasks."""
|
|
110
|
+
serial = request.serial.strip()
|
|
111
|
+
if not serial:
|
|
112
|
+
raise HTTPException(status_code=400, detail="Device serial cannot be empty.")
|
|
113
|
+
|
|
114
|
+
readiness_engine.set_probe_target_serial(serial)
|
|
115
|
+
# Return updated readiness
|
|
116
|
+
report = await readiness_engine.run_all(force_refresh=True)
|
|
117
|
+
return {
|
|
118
|
+
"status": "success",
|
|
119
|
+
"selected_serial": serial,
|
|
120
|
+
"report": report,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@router.post("/adb/restart")
|
|
125
|
+
async def restart_adb_server():
|
|
126
|
+
"""Restart local ADB server and return an updated readiness check."""
|
|
127
|
+
restart_result = await readiness_engine.restart_adb_server()
|
|
128
|
+
readiness_engine.invalidate_cache()
|
|
129
|
+
updated_report = await readiness_engine.run_all(force_refresh=True)
|
|
130
|
+
return {
|
|
131
|
+
"restart_result": restart_result,
|
|
132
|
+
"report": updated_report,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@router.post("/adb/heal-keys")
|
|
137
|
+
async def heal_adb_keys():
|
|
138
|
+
"""Auto-heal corrupted ADB authentication RSA keys and return updated readiness."""
|
|
139
|
+
heal_result = await readiness_engine.heal_adb_keys()
|
|
140
|
+
updated_report = await readiness_engine.run_all()
|
|
141
|
+
return {
|
|
142
|
+
"heal_result": heal_result,
|
|
143
|
+
"report": updated_report,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ConnectAdbRequest(BaseModel):
|
|
148
|
+
"""Payload to connect to an Android device over Wi-Fi."""
|
|
149
|
+
|
|
150
|
+
host: str = Field(description="IP address of Android device")
|
|
151
|
+
port: int = Field(default=5555, description="Port number")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@router.post("/adb/connect")
|
|
155
|
+
async def connect_wireless_adb(request: ConnectAdbRequest):
|
|
156
|
+
"""Connect to a device over Wi-Fi and return updated readiness."""
|
|
157
|
+
connect_result = await readiness_engine.connect_wireless_adb(request.host, request.port)
|
|
158
|
+
readiness_engine.invalidate_cache()
|
|
159
|
+
updated_report = await readiness_engine.run_all(force_refresh=True)
|
|
160
|
+
return {
|
|
161
|
+
"connect_result": connect_result,
|
|
162
|
+
"report": updated_report,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ConnectAdbServerRequest(BaseModel):
|
|
167
|
+
"""Payload to select an ADB server endpoint accessible from this computer."""
|
|
168
|
+
|
|
169
|
+
host: str = Field(description="Host name or IP address of the ADB server")
|
|
170
|
+
port: int = Field(default=5037, ge=1, le=65535, description="ADB server port")
|
|
171
|
+
persist: bool = Field(default=True, description="Persist the endpoint for future launches")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@router.get("/adb/server")
|
|
175
|
+
async def get_adb_server_status():
|
|
176
|
+
"""Return the process-wide ADB server endpoint currently used by Artemis."""
|
|
177
|
+
return adb_server_connection.status()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@router.post("/adb/server/connect")
|
|
181
|
+
async def connect_adb_server(payload: ConnectAdbServerRequest, request: Request):
|
|
182
|
+
"""Validate and activate an ADB server endpoint."""
|
|
183
|
+
_require_local_admin_request(request)
|
|
184
|
+
try:
|
|
185
|
+
connection_result = await adb_server_connection.connect(
|
|
186
|
+
payload.host,
|
|
187
|
+
payload.port,
|
|
188
|
+
persist=payload.persist,
|
|
189
|
+
)
|
|
190
|
+
except InvalidAdbServerEndpoint as exc:
|
|
191
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
192
|
+
|
|
193
|
+
response: dict[str, object] = {"connection_result": connection_result}
|
|
194
|
+
if connection_result["success"]:
|
|
195
|
+
readiness_engine.set_probe_target_serial(None)
|
|
196
|
+
readiness_engine.invalidate_cache()
|
|
197
|
+
response["report"] = await readiness_engine.run_all(force_refresh=True)
|
|
198
|
+
return response
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@router.post("/adb/server/probe")
|
|
202
|
+
async def probe_adb_server(payload: ConnectAdbServerRequest, request: Request):
|
|
203
|
+
"""Test an ADB server endpoint without changing the active endpoint."""
|
|
204
|
+
_require_local_admin_request(request)
|
|
205
|
+
try:
|
|
206
|
+
connection_result = await adb_server_connection.probe(payload.host, payload.port)
|
|
207
|
+
except InvalidAdbServerEndpoint as exc:
|
|
208
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
209
|
+
return {"connection_result": connection_result}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@router.post("/adb/server/local")
|
|
213
|
+
async def use_local_adb_server(request: Request, persist: bool = True):
|
|
214
|
+
"""Restore the standard local ADB server without touching a remote daemon."""
|
|
215
|
+
_require_local_admin_request(request)
|
|
216
|
+
connection_result = await adb_server_connection.use_local_server(persist=persist)
|
|
217
|
+
readiness_engine.set_probe_target_serial(None)
|
|
218
|
+
readiness_engine.invalidate_cache()
|
|
219
|
+
updated_report = await readiness_engine.run_all(force_refresh=True)
|
|
220
|
+
return {
|
|
221
|
+
"connection_result": connection_result,
|
|
222
|
+
"report": updated_report,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class LaunchEmulatorRequest(BaseModel):
|
|
227
|
+
"""Payload to launch a local Android Virtual Device (AVD)."""
|
|
228
|
+
|
|
229
|
+
avd_name: str = Field(
|
|
230
|
+
description="Name of the installed AVD emulator to launch (e.g. Android_2)"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@router.post("/emulator/launch")
|
|
235
|
+
async def launch_emulator(request: LaunchEmulatorRequest):
|
|
236
|
+
"""Launch an Android emulator in the background and return initiation status."""
|
|
237
|
+
avd_name = request.avd_name.strip()
|
|
238
|
+
if not avd_name:
|
|
239
|
+
raise HTTPException(status_code=400, detail="AVD name cannot be empty.")
|
|
240
|
+
|
|
241
|
+
launch_res = await readiness_engine.launch_emulator(avd_name)
|
|
242
|
+
return launch_res
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@router.get("/emulator/status")
|
|
246
|
+
async def get_emulator_status():
|
|
247
|
+
"""Query real-time progress and logs of background emulator launch."""
|
|
248
|
+
return readiness_engine.get_emulator_status()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@router.post("/emulator/stop")
|
|
252
|
+
async def stop_emulator():
|
|
253
|
+
"""Stop active emulator process."""
|
|
254
|
+
return await readiness_engine.stop_emulator()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@router.post("/emulator/dismiss")
|
|
258
|
+
async def dismiss_emulator():
|
|
259
|
+
"""Dismiss emulator launch tracking state."""
|
|
260
|
+
return readiness_engine.dismiss_emulator()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class UpdateCredentialsRequest(BaseModel):
|
|
264
|
+
"""Payload to update and configure LLM or Vision OCR API credentials."""
|
|
265
|
+
|
|
266
|
+
provider: str = Field(
|
|
267
|
+
default="google",
|
|
268
|
+
description="Provider identifier (e.g. google, gemini, openai, anthropic, openrouter, ocr)",
|
|
269
|
+
)
|
|
270
|
+
api_key: str = Field(description="The secret API key string to configure")
|
|
271
|
+
persist_to_env: bool = Field(
|
|
272
|
+
default=True, description="Whether to persist the key to .env file"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class ValidateCredentialsRequest(BaseModel):
|
|
277
|
+
"""Payload to test and verify LLM or Vision OCR API credentials without saving."""
|
|
278
|
+
|
|
279
|
+
provider: str = Field(
|
|
280
|
+
default="google",
|
|
281
|
+
description="Provider identifier (e.g. google, gemini, openai, anthropic, openrouter, ocr)",
|
|
282
|
+
)
|
|
283
|
+
api_key: str = Field(description="The secret API key string to test")
|
|
284
|
+
base_url: str | None = Field(default=None, description="Optional custom base URL")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@router.get("/credentials")
|
|
288
|
+
async def get_credentials():
|
|
289
|
+
"""Report which providers have an API key configured.
|
|
290
|
+
|
|
291
|
+
Secret values never leave the process: this endpoint intentionally returns
|
|
292
|
+
presence booleans only. Keys are written via POST /credentials and used
|
|
293
|
+
server-side.
|
|
294
|
+
"""
|
|
295
|
+
from artemis.config import settings
|
|
296
|
+
|
|
297
|
+
providers = ("google", "openai", "anthropic", "openrouter", "ocr")
|
|
298
|
+
status = {name: bool(settings.get_api_key(name)) for name in providers}
|
|
299
|
+
status["gemini"] = status["google"]
|
|
300
|
+
return {
|
|
301
|
+
"providers": [
|
|
302
|
+
{"name": name, "configured": configured} for name, configured in status.items()
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@router.post("/credentials/test")
|
|
308
|
+
async def test_credentials(request: ValidateCredentialsRequest):
|
|
309
|
+
"""Test and verify whether an API key is valid and usable with the corresponding provider endpoint."""
|
|
310
|
+
from artemis.utils.credentials_validator import validate_api_key
|
|
311
|
+
|
|
312
|
+
provider = request.provider.strip().lower()
|
|
313
|
+
key = request.api_key.strip()
|
|
314
|
+
|
|
315
|
+
if not key:
|
|
316
|
+
raise HTTPException(status_code=400, detail="API key cannot be empty.")
|
|
317
|
+
|
|
318
|
+
is_valid, message = await validate_api_key(
|
|
319
|
+
provider=provider,
|
|
320
|
+
api_key=key,
|
|
321
|
+
base_url=request.base_url,
|
|
322
|
+
)
|
|
323
|
+
if not is_valid:
|
|
324
|
+
raise HTTPException(status_code=400, detail=message)
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
"valid": True,
|
|
328
|
+
"provider": provider,
|
|
329
|
+
"message": message,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@router.post("/credentials")
|
|
334
|
+
async def update_credentials(request: UpdateCredentialsRequest):
|
|
335
|
+
"""Dynamically configure and persist LLM or Vision API key, returning updated readiness report."""
|
|
336
|
+
from artemis.utils.credentials_validator import validate_api_key
|
|
337
|
+
|
|
338
|
+
provider = request.provider.strip().lower()
|
|
339
|
+
key = request.api_key.strip()
|
|
340
|
+
|
|
341
|
+
# If a non-empty key is provided, verify it before saving
|
|
342
|
+
if key:
|
|
343
|
+
is_valid, validation_msg = await validate_api_key(provider=provider, api_key=key)
|
|
344
|
+
if not is_valid:
|
|
345
|
+
raise HTTPException(
|
|
346
|
+
status_code=400,
|
|
347
|
+
detail=f"API key verification failed: {validation_msg}",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
from artemis.config import settings
|
|
352
|
+
|
|
353
|
+
settings.set_api_key(provider, key, persist_to_env=request.persist_to_env)
|
|
354
|
+
|
|
355
|
+
# Re-run all diagnostic probes to build updated report
|
|
356
|
+
readiness_engine.invalidate_cache()
|
|
357
|
+
updated_report = await readiness_engine.run_all(force_refresh=True)
|
|
358
|
+
action_desc = (
|
|
359
|
+
"successfully verified, updated, and applied" if key else "successfully cleared"
|
|
360
|
+
)
|
|
361
|
+
return {
|
|
362
|
+
"status": "success",
|
|
363
|
+
"message": f"API key for {provider} {action_desc}.",
|
|
364
|
+
"provider": provider,
|
|
365
|
+
"report": updated_report,
|
|
366
|
+
}
|
|
367
|
+
except HTTPException:
|
|
368
|
+
raise
|
|
369
|
+
except Exception as exc:
|
|
370
|
+
raise HTTPException(status_code=500, detail=f"Failed to update credentials: {exc}")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class SaveModelConfigRequest(BaseModel):
|
|
374
|
+
content: str = Field(min_length=1, max_length=2_000_000)
|
|
375
|
+
original_content: str = Field(max_length=2_000_000)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@router.put("/model-config")
|
|
379
|
+
async def save_model_config(payload: SaveModelConfigRequest, request: Request):
|
|
380
|
+
"""Validate and atomically save the active JSONC file without changing credentials."""
|
|
381
|
+
import io
|
|
382
|
+
import json
|
|
383
|
+
from pathlib import Path
|
|
384
|
+
import tempfile
|
|
385
|
+
|
|
386
|
+
from pydantic import ValidationError
|
|
387
|
+
from artemis.config.agent import AgentGlobalConfig
|
|
388
|
+
from artemis.config.llm import LLMConfig, _expand_default_into_nodes
|
|
389
|
+
from artemis.config.paths import get_config_path
|
|
390
|
+
from apps.admin_console.services.model_service import ModelService
|
|
391
|
+
from third_party.mobile_use.utils.file import load_jsonc
|
|
392
|
+
|
|
393
|
+
_require_loopback_request(request, "Model configuration can only be edited locally.")
|
|
394
|
+
_require_local_admin_request(request)
|
|
395
|
+
try:
|
|
396
|
+
data = load_jsonc(io.StringIO(payload.content))
|
|
397
|
+
except (json.JSONDecodeError, ValueError):
|
|
398
|
+
raise HTTPException(status_code=400, detail="Invalid JSONC. Please check the syntax.")
|
|
399
|
+
if not isinstance(data, dict) or not (
|
|
400
|
+
isinstance(data.get("default"), dict)
|
|
401
|
+
or (isinstance(data.get("planner"), dict) and isinstance(data.get("utils"), dict))
|
|
402
|
+
):
|
|
403
|
+
raise HTTPException(status_code=400, detail="Configuration must contain a default model.")
|
|
404
|
+
try:
|
|
405
|
+
LLMConfig.model_validate(_expand_default_into_nodes(data))
|
|
406
|
+
AgentGlobalConfig.model_validate(data.get("agent", data))
|
|
407
|
+
except (ValidationError, TypeError, AttributeError):
|
|
408
|
+
raise HTTPException(
|
|
409
|
+
status_code=400,
|
|
410
|
+
detail="Invalid model or agent configuration. Please check provider, model and fallback.",
|
|
411
|
+
)
|
|
412
|
+
temporary = None
|
|
413
|
+
try:
|
|
414
|
+
path = get_config_path("artemis.jsonc").resolve()
|
|
415
|
+
if path.read_text(encoding="utf-8") != payload.original_content:
|
|
416
|
+
raise HTTPException(
|
|
417
|
+
status_code=409,
|
|
418
|
+
detail="Configuration changed on disk. Cancel editing and reload before saving.",
|
|
419
|
+
)
|
|
420
|
+
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, delete=False) as file:
|
|
421
|
+
temporary = Path(file.name)
|
|
422
|
+
os.chmod(temporary, path.stat().st_mode & 0o777)
|
|
423
|
+
file.write(payload.content)
|
|
424
|
+
file.flush()
|
|
425
|
+
os.fsync(file.fileno())
|
|
426
|
+
os.replace(temporary, path)
|
|
427
|
+
except OSError:
|
|
428
|
+
raise HTTPException(status_code=500, detail="Unable to save configuration. Check file permissions.")
|
|
429
|
+
finally:
|
|
430
|
+
if temporary is not None:
|
|
431
|
+
temporary.unlink(missing_ok=True)
|
|
432
|
+
ModelService._llm_info_cache = None
|
|
433
|
+
return await get_model_config_and_env()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@router.post("/model-config/refresh")
|
|
437
|
+
async def refresh_model_configuration(request: Request):
|
|
438
|
+
"""Validate disk configuration, then reload only model-related environment settings."""
|
|
439
|
+
import json
|
|
440
|
+
from dotenv import dotenv_values
|
|
441
|
+
from pydantic import ValidationError
|
|
442
|
+
from artemis.config import settings
|
|
443
|
+
from artemis.config.settings import Settings
|
|
444
|
+
from artemis.config.agent import AgentGlobalConfig
|
|
445
|
+
from artemis.config.llm import LLMConfig, _expand_default_into_nodes
|
|
446
|
+
from artemis.config.paths import get_config_path, get_env_file
|
|
447
|
+
from artemis.llm.router import ModelFactory
|
|
448
|
+
from apps.admin_console.services.model_service import ModelService
|
|
449
|
+
from third_party.mobile_use.utils.file import load_jsonc
|
|
450
|
+
|
|
451
|
+
_require_loopback_request(request, "Model configuration can only be refreshed locally.")
|
|
452
|
+
_require_local_admin_request(request)
|
|
453
|
+
try:
|
|
454
|
+
with get_config_path("artemis.jsonc").open(encoding="utf-8") as file:
|
|
455
|
+
data = load_jsonc(file)
|
|
456
|
+
if not isinstance(data, dict) or not (
|
|
457
|
+
isinstance(data.get("default"), dict)
|
|
458
|
+
or (isinstance(data.get("planner"), dict) and isinstance(data.get("utils"), dict))
|
|
459
|
+
):
|
|
460
|
+
raise ValueError("Missing model configuration")
|
|
461
|
+
LLMConfig.model_validate(_expand_default_into_nodes(data))
|
|
462
|
+
AgentGlobalConfig.model_validate(data.get("agent", data))
|
|
463
|
+
except json.JSONDecodeError:
|
|
464
|
+
raise HTTPException(status_code=400, detail="Invalid JSONC. Please check the syntax.")
|
|
465
|
+
except (ValidationError, TypeError, ValueError):
|
|
466
|
+
raise HTTPException(status_code=400, detail="Invalid model or agent configuration.")
|
|
467
|
+
except OSError:
|
|
468
|
+
raise HTTPException(status_code=400, detail="Failed to read configuration files.")
|
|
469
|
+
|
|
470
|
+
env_path = get_env_file()
|
|
471
|
+
try:
|
|
472
|
+
with env_path.open(encoding="utf-8") as file:
|
|
473
|
+
env_values = dotenv_values(stream=file)
|
|
474
|
+
except OSError:
|
|
475
|
+
raise HTTPException(status_code=400, detail="Failed to read configuration files.")
|
|
476
|
+
names = (
|
|
477
|
+
"OPENAI_API_KEY", "OPENAI_BASE_URL", "GOOGLE_API_KEY", "GEMINI_API_KEY",
|
|
478
|
+
"GCP_API_KEY", "ANTHROPIC_API_KEY", "OPEN_ROUTER_API_KEY", "XAI_API_KEY",
|
|
479
|
+
"OCR_API_KEY", "VISION_API_KEY", "API_KEY",
|
|
480
|
+
)
|
|
481
|
+
values = {name: (env_values.get(name) or "").strip() or None for name in names}
|
|
482
|
+
base_url = values["OPENAI_BASE_URL"]
|
|
483
|
+
if base_url:
|
|
484
|
+
try:
|
|
485
|
+
parsed = urlsplit(base_url)
|
|
486
|
+
valid_url = parsed.scheme in {"http", "https"} and bool(parsed.hostname)
|
|
487
|
+
except ValueError:
|
|
488
|
+
valid_url = False
|
|
489
|
+
if not valid_url:
|
|
490
|
+
raise HTTPException(status_code=400, detail="Invalid OPENAI_BASE_URL. Use an HTTP or HTTPS URL.")
|
|
491
|
+
# Validate before mutation, and keep the singleton object used by existing imports.
|
|
492
|
+
try:
|
|
493
|
+
updated = Settings(_env_file=None, **{**settings.model_dump(), **values})
|
|
494
|
+
except ValidationError:
|
|
495
|
+
raise HTTPException(status_code=400, detail="Invalid model environment configuration.")
|
|
496
|
+
for name in names:
|
|
497
|
+
setattr(settings, name, getattr(updated, name))
|
|
498
|
+
if values[name] is None:
|
|
499
|
+
os.environ.pop(name, None)
|
|
500
|
+
else:
|
|
501
|
+
os.environ[name] = values[name]
|
|
502
|
+
ModelFactory._cache.clear()
|
|
503
|
+
ModelService._llm_info_cache = None
|
|
504
|
+
readiness_engine.invalidate_cache()
|
|
505
|
+
return await get_model_config_and_env()
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
@router.get("/model-config-env")
|
|
509
|
+
async def get_model_config_and_env():
|
|
510
|
+
"""Retrieve the current active artemis.jsonc configuration and .env status for custom setup."""
|
|
511
|
+
import os
|
|
512
|
+
from artemis.config.paths import get_config_path, get_env_file
|
|
513
|
+
from artemis.config import settings
|
|
514
|
+
from third_party.mobile_use.utils.file import load_jsonc
|
|
515
|
+
|
|
516
|
+
from artemis.config.settings import is_placeholder_key
|
|
517
|
+
|
|
518
|
+
# 1. Config file resolution
|
|
519
|
+
config_path = None
|
|
520
|
+
config_content = ""
|
|
521
|
+
parsed_config = {}
|
|
522
|
+
try:
|
|
523
|
+
config_path_obj = get_config_path("artemis.jsonc")
|
|
524
|
+
config_path = str(config_path_obj)
|
|
525
|
+
config_content = config_path_obj.read_text(encoding="utf-8")
|
|
526
|
+
with open(config_path_obj, encoding="utf-8") as f:
|
|
527
|
+
parsed_config = load_jsonc(f)
|
|
528
|
+
except Exception as e:
|
|
529
|
+
config_content = f"// Error reading config: {e}"
|
|
530
|
+
|
|
531
|
+
# 2. Env file resolution
|
|
532
|
+
env_path = get_env_file()
|
|
533
|
+
|
|
534
|
+
# 3. Relevant Env Variables status
|
|
535
|
+
def get_real_env_value(key_name: str, provider: str) -> str | None:
|
|
536
|
+
k = settings.get_api_key(provider)
|
|
537
|
+
if k and not is_placeholder_key(k):
|
|
538
|
+
return k.get_secret_value()
|
|
539
|
+
val = os.environ.get(key_name)
|
|
540
|
+
if val and val.strip() and not is_placeholder_key(val):
|
|
541
|
+
return val.strip()
|
|
542
|
+
return None
|
|
543
|
+
|
|
544
|
+
def mask_key(k: str | None) -> str | None:
|
|
545
|
+
# Expose only enough to recognize which key is active, never a usable
|
|
546
|
+
# fragment of the secret itself.
|
|
547
|
+
if not k:
|
|
548
|
+
return None
|
|
549
|
+
if len(k) <= 8:
|
|
550
|
+
return "****"
|
|
551
|
+
return f"****{k[-4:]}"
|
|
552
|
+
|
|
553
|
+
gemini_real = get_real_env_value("GEMINI_API_KEY", "google") or get_real_env_value(
|
|
554
|
+
"GOOGLE_API_KEY", "google"
|
|
555
|
+
)
|
|
556
|
+
openai_real = get_real_env_value("OPENAI_API_KEY", "openai")
|
|
557
|
+
anthropic_real = get_real_env_value("ANTHROPIC_API_KEY", "anthropic")
|
|
558
|
+
openrouter_real = get_real_env_value("OPEN_ROUTER_API_KEY", "openrouter")
|
|
559
|
+
xai_real = get_real_env_value("XAI_API_KEY", "xai")
|
|
560
|
+
base_url_val = settings.OPENAI_BASE_URL or os.environ.get("OPENAI_BASE_URL")
|
|
561
|
+
if base_url_val and is_placeholder_key(base_url_val):
|
|
562
|
+
base_url_val = None
|
|
563
|
+
ocr_real = get_real_env_value("OCR_API_KEY", "ocr") or get_real_env_value(
|
|
564
|
+
"VISION_API_KEY", "ocr"
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
env_vars = [
|
|
568
|
+
{
|
|
569
|
+
"name": "GEMINI_API_KEY",
|
|
570
|
+
"provider": "google",
|
|
571
|
+
"is_set": bool(gemini_real),
|
|
572
|
+
"preview": mask_key(gemini_real),
|
|
573
|
+
"description": "Google Gemini multimodal vision API key (free tier available)",
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"name": "OPENAI_API_KEY",
|
|
577
|
+
"provider": "openai",
|
|
578
|
+
"is_set": bool(openai_real),
|
|
579
|
+
"preview": mask_key(openai_real),
|
|
580
|
+
"description": "OpenAI API key (GPT-4o, GPT-4o-mini)",
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
"name": "ANTHROPIC_API_KEY",
|
|
584
|
+
"provider": "anthropic",
|
|
585
|
+
"is_set": bool(anthropic_real),
|
|
586
|
+
"preview": mask_key(anthropic_real),
|
|
587
|
+
"description": "Anthropic Claude API key (Claude 3.5 Sonnet, Claude 3.7)",
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
"name": "OPEN_ROUTER_API_KEY",
|
|
591
|
+
"provider": "openrouter",
|
|
592
|
+
"is_set": bool(openrouter_real),
|
|
593
|
+
"preview": mask_key(openrouter_real),
|
|
594
|
+
"description": "OpenRouter unified API gateway key",
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
"name": "XAI_API_KEY",
|
|
598
|
+
"provider": "xai",
|
|
599
|
+
"is_set": bool(xai_real),
|
|
600
|
+
"preview": mask_key(xai_real),
|
|
601
|
+
"description": "xAI Grok vision API key",
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
"name": "OPENAI_BASE_URL",
|
|
605
|
+
"provider": "custom",
|
|
606
|
+
"is_set": bool(base_url_val),
|
|
607
|
+
"preview": base_url_val,
|
|
608
|
+
"description": "Custom API endpoint (for local Ollama, vLLM, DeepSeek, or proxies)",
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
"name": "VISION_API_KEY",
|
|
612
|
+
"provider": "ocr",
|
|
613
|
+
"is_set": bool(ocr_real),
|
|
614
|
+
"preview": mask_key(ocr_real),
|
|
615
|
+
"description": "Google Cloud Vision OCR key for screen text detection (optional)",
|
|
616
|
+
},
|
|
617
|
+
]
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
"config_path": config_path or "config/artemis.jsonc",
|
|
621
|
+
"config_filename": "artemis.jsonc",
|
|
622
|
+
"config_content": config_content,
|
|
623
|
+
"default_model": parsed_config.get("default", {}),
|
|
624
|
+
"presets": parsed_config.get("presets", {}),
|
|
625
|
+
"env_path": str(env_path),
|
|
626
|
+
"env_filename": ".env",
|
|
627
|
+
"env_vars": env_vars,
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
@router.get("/server-status")
|
|
632
|
+
async def get_server_runtime_status():
|
|
633
|
+
"""Retrieve runtime status, PID, port, and uptime of the Artemis server.
|
|
634
|
+
|
|
635
|
+
Answers from in-process state. ``server_lifecycle.get_server_status`` is
|
|
636
|
+
not used here: it runs ``lsof``/``fuser``, which can take over a second and
|
|
637
|
+
is too slow for the ``is_artemis_daemon`` probe.
|
|
638
|
+
"""
|
|
639
|
+
import os
|
|
640
|
+
import time
|
|
641
|
+
|
|
642
|
+
from artemis.runtime.process_probe import pid_is_alive
|
|
643
|
+
from artemis.runtime.server_lifecycle import read_server_info
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
from apps.admin_console.core.state import state
|
|
647
|
+
except ImportError:
|
|
648
|
+
from admin_console.core.state import state
|
|
649
|
+
|
|
650
|
+
port = getattr(state, "port", 8000)
|
|
651
|
+
current_pid = os.getpid()
|
|
652
|
+
pids = {current_pid}
|
|
653
|
+
started_at = None
|
|
654
|
+
|
|
655
|
+
info = read_server_info()
|
|
656
|
+
if info and info.get("port") == port:
|
|
657
|
+
saved_pid = info.get("pid")
|
|
658
|
+
if isinstance(saved_pid, int) and saved_pid != current_pid and pid_is_alive(saved_pid):
|
|
659
|
+
pids.add(saved_pid)
|
|
660
|
+
if isinstance(info.get("started_at"), (int, float)):
|
|
661
|
+
started_at = float(info["started_at"])
|
|
662
|
+
if started_at is None:
|
|
663
|
+
try:
|
|
664
|
+
import psutil
|
|
665
|
+
|
|
666
|
+
started_at = psutil.Process(current_pid).create_time()
|
|
667
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
668
|
+
# psutil is optional; uptime is best-effort.
|
|
669
|
+
started_at = None
|
|
670
|
+
|
|
671
|
+
uptime_seconds = max(0.0, time.time() - started_at) if started_at is not None else None
|
|
672
|
+
# Explicit DTO: the raw metadata file additionally holds the lifecycle
|
|
673
|
+
# token, cmdline, and filesystem paths, none of which belong on the wire.
|
|
674
|
+
return {
|
|
675
|
+
"running": True,
|
|
676
|
+
"port": port,
|
|
677
|
+
"pids": sorted(pids),
|
|
678
|
+
"active_pid": current_pid,
|
|
679
|
+
"uptime_seconds": uptime_seconds,
|
|
680
|
+
"url": f"http://localhost:{port}",
|
|
681
|
+
"admin_url": f"http://localhost:{port}/admin",
|
|
682
|
+
"current_pid": current_pid,
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@router.post("/restart")
|
|
687
|
+
async def restart_server_endpoint(request: Request):
|
|
688
|
+
"""Request a graceful restart of the Artemis server from thin clients/UI."""
|
|
689
|
+
import asyncio
|
|
690
|
+
import os
|
|
691
|
+
import sys
|
|
692
|
+
import threading
|
|
693
|
+
|
|
694
|
+
_require_loopback_request(request, "Server lifecycle controls are local-only.")
|
|
695
|
+
|
|
696
|
+
try:
|
|
697
|
+
from apps.admin_console.core.state import state
|
|
698
|
+
except ImportError:
|
|
699
|
+
from admin_console.core.state import state
|
|
700
|
+
|
|
701
|
+
port = getattr(state, "port", 8000)
|
|
702
|
+
current_pid = os.getpid()
|
|
703
|
+
|
|
704
|
+
def _restart_worker():
|
|
705
|
+
import time
|
|
706
|
+
|
|
707
|
+
time.sleep(0.6)
|
|
708
|
+
if sys.platform != "win32":
|
|
709
|
+
try:
|
|
710
|
+
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
711
|
+
except Exception:
|
|
712
|
+
import subprocess
|
|
713
|
+
|
|
714
|
+
subprocess.Popen([sys.executable] + sys.argv)
|
|
715
|
+
os._exit(0)
|
|
716
|
+
else:
|
|
717
|
+
import subprocess
|
|
718
|
+
|
|
719
|
+
subprocess.Popen([sys.executable] + sys.argv)
|
|
720
|
+
os._exit(0)
|
|
721
|
+
|
|
722
|
+
threading.Thread(target=_restart_worker, daemon=True).start()
|
|
723
|
+
|
|
724
|
+
return {
|
|
725
|
+
"status": "restarting",
|
|
726
|
+
"message": "Artemis server is restarting. Client reconnection should occur in 2-3 seconds.",
|
|
727
|
+
"previous_pid": current_pid,
|
|
728
|
+
"port": port,
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
@router.post("/shutdown", status_code=202)
|
|
733
|
+
async def shutdown_server_endpoint(request: Request):
|
|
734
|
+
"""Request a graceful shutdown of the Artemis server."""
|
|
735
|
+
import asyncio
|
|
736
|
+
|
|
737
|
+
try:
|
|
738
|
+
from apps.admin_console.core.state import state
|
|
739
|
+
except ImportError:
|
|
740
|
+
from admin_console.core.state import state
|
|
741
|
+
|
|
742
|
+
_require_local_lifecycle_request(request)
|
|
743
|
+
server = getattr(request.app.state, "uvicorn_server", None)
|
|
744
|
+
if server is None:
|
|
745
|
+
raise HTTPException(status_code=503, detail="Server lifecycle controller is unavailable.")
|
|
746
|
+
|
|
747
|
+
async def _shutdown_after_response() -> None:
|
|
748
|
+
# Let Starlette flush the accepted response before Uvicorn leaves its
|
|
749
|
+
# main loop and invokes the FastAPI shutdown lifecycle.
|
|
750
|
+
await asyncio.sleep(0.05)
|
|
751
|
+
state.is_shutting_down = True
|
|
752
|
+
state.shutdown_event.set()
|
|
753
|
+
server.should_exit = True
|
|
754
|
+
|
|
755
|
+
asyncio.create_task(_shutdown_after_response())
|
|
756
|
+
|
|
757
|
+
return {
|
|
758
|
+
"status": "shutting_down",
|
|
759
|
+
"message": "Artemis server is shutting down.",
|
|
760
|
+
"pid": os.getpid(),
|
|
761
|
+
}
|