android-midscene-automation 0.2.4 → 1.1.1
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 +10 -0
- package/README.md +159 -29
- 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 +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- 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 +340 -121
- package/src/types.ts +1 -1
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
# Copyright 2025-2026 Minitap, Inc.
|
|
2
|
+
# Modifications Copyright 2026 Google LLC
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
# Originally from mobile-use (https://github.com/minitap-ai/mobile-use).
|
|
17
|
+
# See third_party/mobile_use/METADATA for the upstream source and local modifications.
|
|
18
|
+
|
|
19
|
+
"""Base SDK agent: device/client setup, task creation and the task run loop.
|
|
20
|
+
|
|
21
|
+
Artemis extends this class in ``artemis.sdk.agent.Agent`` with tracing setup, device
|
|
22
|
+
environment preparation, cancellation watching and UI hierarchy backend reporting.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import os
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from datetime import UTC, datetime
|
|
30
|
+
except ImportError:
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
|
|
33
|
+
UTC = UTC
|
|
34
|
+
import shutil
|
|
35
|
+
import threading
|
|
36
|
+
import uuid
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from platform import system
|
|
39
|
+
from shutil import which
|
|
40
|
+
from types import NoneType
|
|
41
|
+
from typing import Any, TypeVar, overload
|
|
42
|
+
|
|
43
|
+
from adbutils import AdbClient
|
|
44
|
+
from PIL import Image
|
|
45
|
+
from pydantic import BaseModel
|
|
46
|
+
|
|
47
|
+
from artemis.agents.flash.runner import FlashRunner
|
|
48
|
+
from artemis.agents.outputter.outputter import outputter
|
|
49
|
+
from artemis.clients.screen_client_factory import (
|
|
50
|
+
ScreenClient,
|
|
51
|
+
create_screen_client,
|
|
52
|
+
)
|
|
53
|
+
from artemis.config import (
|
|
54
|
+
OutputConfig,
|
|
55
|
+
record_events,
|
|
56
|
+
)
|
|
57
|
+
from artemis.constants import RECURSION_LIMIT
|
|
58
|
+
from artemis.context import (
|
|
59
|
+
ArtemisContext,
|
|
60
|
+
DeviceContext,
|
|
61
|
+
DevicePlatform,
|
|
62
|
+
ExecutionSetup,
|
|
63
|
+
)
|
|
64
|
+
from artemis.controllers.controller_factory import get_controller
|
|
65
|
+
from artemis.data_engine.trace import DataEngineCallbackHandler
|
|
66
|
+
from artemis.graph.graph import get_graph
|
|
67
|
+
from artemis.graph.state import State
|
|
68
|
+
from artemis.runtime import DeviceExecutionLock
|
|
69
|
+
from artemis.sdk.run_outcome import attach_test_summary, resolve_trace_suffix
|
|
70
|
+
from artemis.sdk.types.agent import AgentConfig
|
|
71
|
+
from artemis.utils.startup_progress import publish_startup_progress
|
|
72
|
+
from third_party.mobile_use.controllers.platform_specific_commands_controller import (
|
|
73
|
+
get_first_device,
|
|
74
|
+
)
|
|
75
|
+
from third_party.mobile_use.sdk.builders.task_request_builder import TaskRequestBuilder
|
|
76
|
+
from third_party.mobile_use.sdk.types.exceptions import (
|
|
77
|
+
AgentError,
|
|
78
|
+
AgentNotInitializedError,
|
|
79
|
+
AgentProfileNotFoundError,
|
|
80
|
+
AgentTaskRequestError,
|
|
81
|
+
DeviceNotFoundError,
|
|
82
|
+
ExecutableNotFoundError,
|
|
83
|
+
)
|
|
84
|
+
from third_party.mobile_use.sdk.types.task import (
|
|
85
|
+
AgentProfile,
|
|
86
|
+
Task,
|
|
87
|
+
TaskRequest,
|
|
88
|
+
)
|
|
89
|
+
from third_party.mobile_use.utils.app_launch_utils import _handle_initial_app_launch
|
|
90
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
91
|
+
from third_party.mobile_use.utils.media import (
|
|
92
|
+
create_gif_from_trace_folder,
|
|
93
|
+
create_steps_json_from_trace_folder,
|
|
94
|
+
remove_images_from_trace_folder,
|
|
95
|
+
remove_steps_json_from_trace_folder,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
logger = get_logger(__name__)
|
|
99
|
+
|
|
100
|
+
TOutput = TypeVar("TOutput", bound=BaseModel | None)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AgentBase:
|
|
104
|
+
_config: AgentConfig
|
|
105
|
+
_tasks: list[Task] = []
|
|
106
|
+
_tmp_traces_dir: Path
|
|
107
|
+
_initialized: bool = False
|
|
108
|
+
_device_context: DeviceContext
|
|
109
|
+
_adb_client: AdbClient | None
|
|
110
|
+
_ui_adb_client: ScreenClient | Any | None
|
|
111
|
+
_current_task: asyncio.Task | None = None
|
|
112
|
+
_task_lock: asyncio.Lock
|
|
113
|
+
_cloud_mobile_id: str | None = None
|
|
114
|
+
|
|
115
|
+
async def init(
|
|
116
|
+
self,
|
|
117
|
+
api_key: str | None = None,
|
|
118
|
+
device_id: str | None = None,
|
|
119
|
+
device_serial: str | None = None,
|
|
120
|
+
retry_count: int = 5,
|
|
121
|
+
retry_wait_seconds: int = 5,
|
|
122
|
+
):
|
|
123
|
+
target_dev = device_serial or device_id
|
|
124
|
+
if target_dev:
|
|
125
|
+
self._config = self._config.model_copy(
|
|
126
|
+
update={"device_id": target_dev, "device_platform": DevicePlatform.ANDROID}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return await self._init_internal(
|
|
130
|
+
api_key=api_key,
|
|
131
|
+
retry_count=retry_count,
|
|
132
|
+
retry_wait_seconds=retry_wait_seconds,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def _init_internal(
|
|
136
|
+
self,
|
|
137
|
+
api_key: str | None = None,
|
|
138
|
+
retry_count: int = 5,
|
|
139
|
+
retry_wait_seconds: int = 5,
|
|
140
|
+
):
|
|
141
|
+
|
|
142
|
+
if os.environ.get("ARTEMIS_CLOUD_MODE") != "1" and not which("adb"):
|
|
143
|
+
raise ExecutableNotFoundError("adb")
|
|
144
|
+
|
|
145
|
+
if self._initialized:
|
|
146
|
+
logger.warning("Agent is already initialized. Skipping...")
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
# Get first available device ID
|
|
150
|
+
if os.environ.get("ARTEMIS_CLOUD_MODE") == "1":
|
|
151
|
+
device_id = os.environ.get("ADB_DEVICE_SERIAL", "cloud_device")
|
|
152
|
+
platform = DevicePlatform.ANDROID
|
|
153
|
+
elif not self._config.device_id or not self._config.device_platform:
|
|
154
|
+
device_id, platform, _ = get_first_device(logger=logger)
|
|
155
|
+
else:
|
|
156
|
+
device_id, platform = (
|
|
157
|
+
self._config.device_id,
|
|
158
|
+
self._config.device_platform,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if not device_id or not platform:
|
|
162
|
+
error_msg = "No device found. Exiting."
|
|
163
|
+
logger.error(error_msg)
|
|
164
|
+
raise DeviceNotFoundError(error_msg)
|
|
165
|
+
|
|
166
|
+
# Initialize clients
|
|
167
|
+
publish_startup_progress(
|
|
168
|
+
"device_check", "Checking the Android device", session_id=self._session_id
|
|
169
|
+
)
|
|
170
|
+
if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
|
|
171
|
+
self._init_clients(
|
|
172
|
+
device_id=device_id,
|
|
173
|
+
platform=platform,
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
# Cloud mode is pinned to UIAutomator2 through the gateway; the
|
|
177
|
+
# hierarchy backend switch only applies to locally attached devices.
|
|
178
|
+
from cloud_service.virtualization import RemoteAdbClient, RemoteUIAutomatorClient
|
|
179
|
+
|
|
180
|
+
self._adb_client = RemoteAdbClient()
|
|
181
|
+
self._ui_adb_client = RemoteUIAutomatorClient(
|
|
182
|
+
device_id=device_id,
|
|
183
|
+
adb_client=self._adb_client,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
self._device_context = await self._get_device_context(
|
|
187
|
+
device_id=device_id, platform=platform
|
|
188
|
+
)
|
|
189
|
+
logger.info(self._device_context.to_str())
|
|
190
|
+
publish_startup_progress(
|
|
191
|
+
"device_ready", "Android device connected", session_id=self._session_id
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Asynchronously pre-warm LLM connection pools in the background
|
|
195
|
+
asyncio.create_task(self._prewarm_llm_connections(api_key))
|
|
196
|
+
|
|
197
|
+
logger.info("✅ Artemis agent initialized.")
|
|
198
|
+
self._initialized = True
|
|
199
|
+
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
async def install_apk(self, apk_path: str | Path) -> None:
|
|
203
|
+
"""Install an APK on the connected device.
|
|
204
|
+
|
|
205
|
+
For cloud mobiles, the APK must be x86_64 compatible.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
apk_path: Path to the local APK file to install
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
AgentNotInitializedError: If the agent is not initialized
|
|
212
|
+
AgentError: If attempting to install on non-Android device or ADB
|
|
213
|
+
operations fail
|
|
214
|
+
FileNotFoundError: If the APK file doesn't exist
|
|
215
|
+
CloudMobileServiceUninitializedError: If cloud service is
|
|
216
|
+
unavailable
|
|
217
|
+
"""
|
|
218
|
+
await self._install_apk_internal(apk_path)
|
|
219
|
+
|
|
220
|
+
async def _install_apk_internal(self, apk_path: str | Path) -> None:
|
|
221
|
+
if isinstance(apk_path, str):
|
|
222
|
+
apk_path = Path(apk_path)
|
|
223
|
+
|
|
224
|
+
if not apk_path.exists():
|
|
225
|
+
raise FileNotFoundError(f"APK file not found: {apk_path}")
|
|
226
|
+
|
|
227
|
+
if not self._initialized:
|
|
228
|
+
raise AgentNotInitializedError()
|
|
229
|
+
|
|
230
|
+
device_id = self._device_context.device_id
|
|
231
|
+
logger.info(f"Installing APK on Android device '{device_id}'")
|
|
232
|
+
if not self._adb_client:
|
|
233
|
+
raise AgentError("ADB client not initialized")
|
|
234
|
+
|
|
235
|
+
device = self._adb_client.device(serial=device_id)
|
|
236
|
+
await asyncio.to_thread(device.install, apk_path)
|
|
237
|
+
logger.info(f"APK installed successfully on Android device '{device_id}'")
|
|
238
|
+
|
|
239
|
+
async def install_app(self, app_path: str | Path) -> str | None:
|
|
240
|
+
"""Install an app on the connected device.
|
|
241
|
+
|
|
242
|
+
For Android: Installs an APK file using ADB.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
app_path: Path to the app to install (Android APK file).
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
None.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
AgentNotInitializedError: If the agent is not initialized
|
|
252
|
+
AgentError: If installation fails or platform is unsupported
|
|
253
|
+
FileNotFoundError: If the app file/folder doesn't exist
|
|
254
|
+
"""
|
|
255
|
+
return await self._install_app_internal(app_path)
|
|
256
|
+
|
|
257
|
+
async def _install_app_internal(self, app_path: str | Path) -> str | None:
|
|
258
|
+
if isinstance(app_path, str):
|
|
259
|
+
app_path = Path(app_path)
|
|
260
|
+
|
|
261
|
+
if not app_path.exists():
|
|
262
|
+
raise FileNotFoundError(f"App not found: {app_path}")
|
|
263
|
+
|
|
264
|
+
if not self._initialized:
|
|
265
|
+
raise AgentNotInitializedError()
|
|
266
|
+
|
|
267
|
+
await self._install_apk_internal(app_path)
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def new_task(self, goal: str):
|
|
271
|
+
"""Create a new task request builder.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
goal: Natural language description of what to accomplish
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
TaskRequestBuilder that can be configured with:
|
|
278
|
+
- .with_output_format() for structured output
|
|
279
|
+
- .with_output_description() for output description
|
|
280
|
+
- .with_locked_app_package() to restrict execution to a specific app
|
|
281
|
+
- .using_profile() to specify an LLM profile
|
|
282
|
+
- .with_max_steps() to set maximum execution steps
|
|
283
|
+
- .with_trace_recording() to enable trace recording
|
|
284
|
+
- .with_name() to set a custom task name
|
|
285
|
+
"""
|
|
286
|
+
return TaskRequestBuilder[None].from_common(
|
|
287
|
+
goal=goal,
|
|
288
|
+
common=self._config.task_request_defaults,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
@overload
|
|
292
|
+
async def run_task(
|
|
293
|
+
self,
|
|
294
|
+
*,
|
|
295
|
+
goal: str,
|
|
296
|
+
output: type[TOutput],
|
|
297
|
+
profile: str | AgentProfile | None = None,
|
|
298
|
+
name: str | None = None,
|
|
299
|
+
locked_app_package: str | None = None,
|
|
300
|
+
app_path: str | Path | None = None,
|
|
301
|
+
) -> TOutput | None: ...
|
|
302
|
+
|
|
303
|
+
@overload
|
|
304
|
+
async def run_task(
|
|
305
|
+
self,
|
|
306
|
+
*,
|
|
307
|
+
goal: str,
|
|
308
|
+
output: str,
|
|
309
|
+
profile: str | AgentProfile | None = None,
|
|
310
|
+
name: str | None = None,
|
|
311
|
+
locked_app_package: str | None = None,
|
|
312
|
+
app_path: str | Path | None = None,
|
|
313
|
+
) -> str | dict | None: ...
|
|
314
|
+
|
|
315
|
+
@overload
|
|
316
|
+
async def run_task(
|
|
317
|
+
self,
|
|
318
|
+
*,
|
|
319
|
+
goal: str,
|
|
320
|
+
output=None,
|
|
321
|
+
profile: str | AgentProfile | None = None,
|
|
322
|
+
name: str | None = None,
|
|
323
|
+
locked_app_package: str | None = None,
|
|
324
|
+
app_path: str | Path | None = None,
|
|
325
|
+
) -> str | None: ...
|
|
326
|
+
|
|
327
|
+
@overload
|
|
328
|
+
async def run_task(
|
|
329
|
+
self,
|
|
330
|
+
*,
|
|
331
|
+
request: TaskRequest[None],
|
|
332
|
+
locked_app_package: str | None = None,
|
|
333
|
+
app_path: str | Path | None = None,
|
|
334
|
+
) -> str | dict | None: ...
|
|
335
|
+
|
|
336
|
+
@overload
|
|
337
|
+
async def run_task(
|
|
338
|
+
self,
|
|
339
|
+
*,
|
|
340
|
+
request: TaskRequest[TOutput],
|
|
341
|
+
locked_app_package: str | None = None,
|
|
342
|
+
app_path: str | Path | None = None,
|
|
343
|
+
) -> TOutput | None: ...
|
|
344
|
+
|
|
345
|
+
async def run_task(
|
|
346
|
+
self,
|
|
347
|
+
*,
|
|
348
|
+
goal: str | None = None,
|
|
349
|
+
output: type[TOutput] | str | None = None,
|
|
350
|
+
profile: str | AgentProfile | None = None,
|
|
351
|
+
locked_app_package: str | None = None,
|
|
352
|
+
name: str | None = None,
|
|
353
|
+
app_path: str | Path | None = None,
|
|
354
|
+
request: TaskRequest[TOutput] | None = None,
|
|
355
|
+
) -> str | dict | TOutput | None:
|
|
356
|
+
|
|
357
|
+
# Normal local execution path
|
|
358
|
+
if request is not None:
|
|
359
|
+
if locked_app_package is not None:
|
|
360
|
+
if request.locked_app_package:
|
|
361
|
+
logger.warning(
|
|
362
|
+
"Locked app package specified both in the request and"
|
|
363
|
+
" as a parameter. Using the parameter value."
|
|
364
|
+
)
|
|
365
|
+
request.locked_app_package = locked_app_package
|
|
366
|
+
# Handle app_path parameter override
|
|
367
|
+
if app_path is not None:
|
|
368
|
+
if request.app_path:
|
|
369
|
+
logger.warning(
|
|
370
|
+
"App path specified both in the request and as a"
|
|
371
|
+
" parameter. Using the parameter value."
|
|
372
|
+
)
|
|
373
|
+
request.app_path = Path(app_path) if isinstance(app_path, str) else app_path
|
|
374
|
+
return await self._run_task(request=request)
|
|
375
|
+
if goal is None:
|
|
376
|
+
raise AgentTaskRequestError("Goal is required")
|
|
377
|
+
task_request = self.new_task(goal=goal)
|
|
378
|
+
if output is not None:
|
|
379
|
+
if isinstance(output, str):
|
|
380
|
+
task_request.with_output_description(description=output)
|
|
381
|
+
elif output is not NoneType:
|
|
382
|
+
task_request.with_output_format(output_format=output)
|
|
383
|
+
if profile is not None:
|
|
384
|
+
task_request.using_profile(profile=profile)
|
|
385
|
+
if name is not None:
|
|
386
|
+
task_request.with_name(name=name)
|
|
387
|
+
if locked_app_package is not None:
|
|
388
|
+
task_request.with_locked_app_package(package_name=locked_app_package)
|
|
389
|
+
if app_path is not None:
|
|
390
|
+
task_request.with_app_path(app_path=app_path)
|
|
391
|
+
return await self._run_task(task_request.build())
|
|
392
|
+
|
|
393
|
+
async def _run_task(
|
|
394
|
+
self,
|
|
395
|
+
request: TaskRequest[TOutput],
|
|
396
|
+
) -> str | dict | TOutput | None:
|
|
397
|
+
if not self._initialized:
|
|
398
|
+
raise AgentNotInitializedError()
|
|
399
|
+
|
|
400
|
+
if request.profile:
|
|
401
|
+
agent_profile = self._config.agent_profiles.get(request.profile)
|
|
402
|
+
if agent_profile is None:
|
|
403
|
+
if request.profile.lower() in (
|
|
404
|
+
"flash",
|
|
405
|
+
"pro",
|
|
406
|
+
"ultra",
|
|
407
|
+
"default",
|
|
408
|
+
):
|
|
409
|
+
agent_profile = self._config.default_profile
|
|
410
|
+
else:
|
|
411
|
+
raise AgentProfileNotFoundError(request.profile)
|
|
412
|
+
else:
|
|
413
|
+
agent_profile = self._config.default_profile
|
|
414
|
+
|
|
415
|
+
logger.info(str(agent_profile))
|
|
416
|
+
|
|
417
|
+
on_status_changed = None
|
|
418
|
+
task_id = str(
|
|
419
|
+
self._session_id
|
|
420
|
+
or os.getenv("ARTEMIS_SESSION_ID")
|
|
421
|
+
or os.getenv("ARTEMIS_CLOUD_SESSION_ID")
|
|
422
|
+
or uuid.uuid4()
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
task = Task(
|
|
426
|
+
id=task_id,
|
|
427
|
+
device=self._device_context,
|
|
428
|
+
status="pending",
|
|
429
|
+
request=request,
|
|
430
|
+
created_at=datetime.now(),
|
|
431
|
+
on_status_changed=on_status_changed,
|
|
432
|
+
)
|
|
433
|
+
self._tasks.append(task)
|
|
434
|
+
task_name = task.get_name()
|
|
435
|
+
|
|
436
|
+
context = ArtemisContext(
|
|
437
|
+
device=self._device_context,
|
|
438
|
+
adb_client=self._adb_client,
|
|
439
|
+
ui_adb_client=self._ui_adb_client,
|
|
440
|
+
llm_config=agent_profile.llm_config,
|
|
441
|
+
agent_config=self._config,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
output_config = None
|
|
445
|
+
if request.output_description or request.output_format:
|
|
446
|
+
output_config = OutputConfig(
|
|
447
|
+
output_description=request.output_description,
|
|
448
|
+
structured_output=request.output_format, # type: ignore
|
|
449
|
+
)
|
|
450
|
+
logger.info(str(output_config))
|
|
451
|
+
|
|
452
|
+
logger.info(f"[{task_name}] Starting graph with goal: `{request.goal}`")
|
|
453
|
+
state = self._get_graph_state(task=task)
|
|
454
|
+
graph_input = state.model_dump()
|
|
455
|
+
datetime.now(UTC)
|
|
456
|
+
|
|
457
|
+
async def _execute_task_logic():
|
|
458
|
+
last_state: State | None = None
|
|
459
|
+
last_state_snapshot: dict | None = None
|
|
460
|
+
output = None
|
|
461
|
+
effective_mode = getattr(self._config, "concurrency_mode", "per_device")
|
|
462
|
+
effective_max = getattr(self._config, "max_concurrency", None)
|
|
463
|
+
sess_id = (
|
|
464
|
+
self._session_id
|
|
465
|
+
or os.getenv("ARTEMIS_SESSION_ID")
|
|
466
|
+
or os.getenv("ARTEMIS_CLOUD_SESSION_ID")
|
|
467
|
+
or getattr(task, "id", None)
|
|
468
|
+
or getattr(getattr(task, "request", None), "task_name", None)
|
|
469
|
+
)
|
|
470
|
+
active_owner = DeviceExecutionLock.get_active_owner(self._device_context.device_id)
|
|
471
|
+
already_held = (
|
|
472
|
+
active_owner is not None
|
|
473
|
+
and active_owner.pid == os.getpid()
|
|
474
|
+
and (
|
|
475
|
+
(sess_id and str(active_owner.session_id) == str(sess_id))
|
|
476
|
+
or active_owner.token == self._device_context.device_id
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
device_lock = (
|
|
480
|
+
None
|
|
481
|
+
if already_held
|
|
482
|
+
else DeviceExecutionLock(
|
|
483
|
+
self._device_context.device_id,
|
|
484
|
+
description=f"{request.goal[:120]}",
|
|
485
|
+
concurrency_mode=effective_mode,
|
|
486
|
+
max_concurrency=effective_max,
|
|
487
|
+
session_id=str(sess_id) if sess_id else None,
|
|
488
|
+
ingress=os.getenv("ARTEMIS_TASK_INGRESS") or "agent",
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
try:
|
|
492
|
+
if device_lock is not None and os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
|
|
493
|
+
queue_cancel_event = threading.Event()
|
|
494
|
+
acquire_task = asyncio.create_task(
|
|
495
|
+
asyncio.to_thread(
|
|
496
|
+
device_lock.acquire,
|
|
497
|
+
cancel_event=queue_cancel_event,
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
try:
|
|
501
|
+
await asyncio.shield(acquire_task)
|
|
502
|
+
except asyncio.CancelledError:
|
|
503
|
+
queue_cancel_event.set()
|
|
504
|
+
try:
|
|
505
|
+
await asyncio.shield(acquire_task)
|
|
506
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
507
|
+
# Draining the lock acquisition after cancellation is best effort.
|
|
508
|
+
logger.debug(
|
|
509
|
+
f"Device lock acquisition drain after cancel failed: {exc}",
|
|
510
|
+
exc_info=True,
|
|
511
|
+
)
|
|
512
|
+
raise
|
|
513
|
+
# All device mutation and UI initialization happens only after
|
|
514
|
+
# this task reaches the head of the one global FIFO queue.
|
|
515
|
+
# Session creation must be inside the same boundary as well:
|
|
516
|
+
# DataEngine uses a shared database, so a queued task must not
|
|
517
|
+
# publish a new active session while the current task is still
|
|
518
|
+
# finishing.
|
|
519
|
+
self._prepare_tracing(task=task, context=context)
|
|
520
|
+
self._prepare_output_files(task=task)
|
|
521
|
+
if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
|
|
522
|
+
if self._ui_adb_client is not None:
|
|
523
|
+
await self._connect_screen_client(context, str(sess_id))
|
|
524
|
+
await self._ensure_device_unlocked()
|
|
525
|
+
publish_startup_progress(
|
|
526
|
+
"environment", "Preparing the device environment", session_id=str(sess_id)
|
|
527
|
+
)
|
|
528
|
+
await self._prepare_app_installation(task=task)
|
|
529
|
+
await self._prepare_device_environment(context=context)
|
|
530
|
+
await self._prepare_app_lock(task=task, context=context)
|
|
531
|
+
async with context:
|
|
532
|
+
recording_started = False
|
|
533
|
+
if self._config.video_recording_tools_enabled:
|
|
534
|
+
try:
|
|
535
|
+
controller = get_controller(context)
|
|
536
|
+
|
|
537
|
+
output_dir = None
|
|
538
|
+
if context.execution_setup and context.execution_setup.traces_path:
|
|
539
|
+
output_dir = (
|
|
540
|
+
self._tmp_traces_dir / context.execution_setup.trace_name
|
|
541
|
+
).resolve()
|
|
542
|
+
|
|
543
|
+
logger.info(f"[{task_name}] Starting automated screen recording...")
|
|
544
|
+
start_res = await controller.start_video_recording(
|
|
545
|
+
output_dir=output_dir
|
|
546
|
+
)
|
|
547
|
+
if start_res and start_res.success:
|
|
548
|
+
recording_started = True
|
|
549
|
+
else:
|
|
550
|
+
logger.warning(
|
|
551
|
+
f"[{task_name}] Failed to start screen"
|
|
552
|
+
f" recording: {start_res.message if start_res else 'unknown'}"
|
|
553
|
+
)
|
|
554
|
+
except Exception as e:
|
|
555
|
+
logger.error(f"[{task_name}] Failed to start screen recording: {e}")
|
|
556
|
+
|
|
557
|
+
publish_startup_progress(
|
|
558
|
+
"environment_ready", "Device environment is ready", session_id=str(sess_id)
|
|
559
|
+
)
|
|
560
|
+
try:
|
|
561
|
+
if request.profile and request.profile.lower() == "flash":
|
|
562
|
+
logger.info(f"[{task_name}] Invoking FlashRunner reactive loop...")
|
|
563
|
+
await task.set_status(
|
|
564
|
+
status="running",
|
|
565
|
+
message="Invoking FlashRunner...",
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# An explicit max_steps caps the reactive loop; the
|
|
569
|
+
# default leaves the cap to agent.flash.max_turns
|
|
570
|
+
# (0 = unlimited).
|
|
571
|
+
max_turns = (
|
|
572
|
+
request.max_steps if request.max_steps != RECURSION_LIMIT else None
|
|
573
|
+
)
|
|
574
|
+
runner = FlashRunner(context, goal=request.goal, max_turns=max_turns)
|
|
575
|
+
flash_result = await runner.run(state)
|
|
576
|
+
|
|
577
|
+
output = flash_result
|
|
578
|
+
last_state_snapshot = state.model_dump()
|
|
579
|
+
|
|
580
|
+
status = flash_result.get("status")
|
|
581
|
+
if status == "completed":
|
|
582
|
+
logger.info(f"✅ Automation '{task_name}' is success ✅")
|
|
583
|
+
await task.finalize(content=output, state=last_state_snapshot)
|
|
584
|
+
if context.data_engine:
|
|
585
|
+
context.data_engine.end_session("completed")
|
|
586
|
+
else:
|
|
587
|
+
err = (
|
|
588
|
+
f"[{task_name}] FlashRunner failed:"
|
|
589
|
+
f" {flash_result.get('explanation')}"
|
|
590
|
+
)
|
|
591
|
+
logger.warning(err)
|
|
592
|
+
await task.finalize(
|
|
593
|
+
content=output,
|
|
594
|
+
state=last_state_snapshot,
|
|
595
|
+
error=err,
|
|
596
|
+
)
|
|
597
|
+
if context.data_engine:
|
|
598
|
+
context.data_engine.end_session("failed")
|
|
599
|
+
return output
|
|
600
|
+
else:
|
|
601
|
+
logger.info(f"[{task_name}] Invoking graph with input: {graph_input}")
|
|
602
|
+
await task.set_status(status="running", message="Invoking graph...")
|
|
603
|
+
async for chunk in (await get_graph(context)).astream(
|
|
604
|
+
input=graph_input,
|
|
605
|
+
config={
|
|
606
|
+
"recursion_limit": task.request.max_steps,
|
|
607
|
+
"callbacks": (
|
|
608
|
+
(
|
|
609
|
+
list(self._config.graph_config_callbacks)
|
|
610
|
+
if self._config.graph_config_callbacks is not None
|
|
611
|
+
else []
|
|
612
|
+
)
|
|
613
|
+
+ [DataEngineCallbackHandler(context)]
|
|
614
|
+
if context.data_engine
|
|
615
|
+
else self._config.graph_config_callbacks
|
|
616
|
+
),
|
|
617
|
+
},
|
|
618
|
+
stream_mode=[
|
|
619
|
+
"messages",
|
|
620
|
+
"custom",
|
|
621
|
+
"updates",
|
|
622
|
+
"values",
|
|
623
|
+
],
|
|
624
|
+
):
|
|
625
|
+
stream_mode, payload = chunk
|
|
626
|
+
if stream_mode == "values":
|
|
627
|
+
last_state_snapshot = payload # type: ignore
|
|
628
|
+
last_state = State(**last_state_snapshot) # type: ignore
|
|
629
|
+
|
|
630
|
+
if not last_state:
|
|
631
|
+
err = f"[{task_name}] No result received from graph"
|
|
632
|
+
logger.warning(err)
|
|
633
|
+
await task.finalize(
|
|
634
|
+
content=output,
|
|
635
|
+
state=last_state_snapshot,
|
|
636
|
+
error=err,
|
|
637
|
+
)
|
|
638
|
+
if context.data_engine:
|
|
639
|
+
context.data_engine.end_session("failed")
|
|
640
|
+
return None
|
|
641
|
+
|
|
642
|
+
output = await self._extract_output(
|
|
643
|
+
task_name=task_name,
|
|
644
|
+
ctx=context,
|
|
645
|
+
request=request,
|
|
646
|
+
output_config=output_config,
|
|
647
|
+
state=last_state,
|
|
648
|
+
)
|
|
649
|
+
# Run outcome (goal axis x test axis) drives the
|
|
650
|
+
# wrap-up instead of an unconditional success path.
|
|
651
|
+
run_outcome = getattr(last_state, "run_outcome", None)
|
|
652
|
+
context.run_outcome = run_outcome
|
|
653
|
+
output = attach_test_summary(output, run_outcome)
|
|
654
|
+
task_status_axis = (
|
|
655
|
+
run_outcome.get("task_status") if run_outcome else None
|
|
656
|
+
)
|
|
657
|
+
tests_failed = (
|
|
658
|
+
int((run_outcome.get("tests") or {}).get("failed", 0))
|
|
659
|
+
if run_outcome
|
|
660
|
+
else 0
|
|
661
|
+
)
|
|
662
|
+
if task_status_axis == "blocked":
|
|
663
|
+
err = (
|
|
664
|
+
f"[{task_name}] Task ended blocked: verify"
|
|
665
|
+
" criteria unmet after exhausting the final"
|
|
666
|
+
" check budget."
|
|
667
|
+
)
|
|
668
|
+
logger.warning(err)
|
|
669
|
+
await task.finalize(
|
|
670
|
+
content=output,
|
|
671
|
+
state=last_state_snapshot,
|
|
672
|
+
error=err,
|
|
673
|
+
)
|
|
674
|
+
if context.data_engine:
|
|
675
|
+
context.data_engine.end_session("failed")
|
|
676
|
+
return output
|
|
677
|
+
|
|
678
|
+
if tests_failed > 0:
|
|
679
|
+
logger.warning(
|
|
680
|
+
f"[{task_name}] Task completed, but"
|
|
681
|
+
f" {tests_failed} assertion(s) failed —"
|
|
682
|
+
" see the test summary."
|
|
683
|
+
)
|
|
684
|
+
else:
|
|
685
|
+
logger.info(f"✅ Automation '{task_name}' is success ✅")
|
|
686
|
+
await task.finalize(content=output, state=last_state_snapshot)
|
|
687
|
+
if context.data_engine:
|
|
688
|
+
context.data_engine.end_session("completed")
|
|
689
|
+
|
|
690
|
+
return output
|
|
691
|
+
finally:
|
|
692
|
+
if recording_started:
|
|
693
|
+
|
|
694
|
+
async def _safe_stop_recording():
|
|
695
|
+
try:
|
|
696
|
+
logger.info(
|
|
697
|
+
f"[{task_name}] Stopping automated screen recording..."
|
|
698
|
+
)
|
|
699
|
+
controller = get_controller(context)
|
|
700
|
+
return await controller.stop_video_recording()
|
|
701
|
+
except Exception as e:
|
|
702
|
+
logger.error(
|
|
703
|
+
f"[{task_name}] Error stopping screen recording: {e}"
|
|
704
|
+
)
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
stop_task = asyncio.create_task(_safe_stop_recording())
|
|
708
|
+
try:
|
|
709
|
+
stop_result = await asyncio.shield(stop_task)
|
|
710
|
+
except (asyncio.CancelledError, BaseException):
|
|
711
|
+
try:
|
|
712
|
+
stop_result = await asyncio.wait_for(stop_task, timeout=15.0)
|
|
713
|
+
except Exception:
|
|
714
|
+
stop_result = None
|
|
715
|
+
|
|
716
|
+
if stop_result and stop_result.success and stop_result.video_path:
|
|
717
|
+
logger.info(
|
|
718
|
+
f"[{task_name}] Screen recording saved"
|
|
719
|
+
f" to: {stop_result.video_path}"
|
|
720
|
+
)
|
|
721
|
+
elif stop_result:
|
|
722
|
+
logger.warning(
|
|
723
|
+
f"[{task_name}] Failed to save screen"
|
|
724
|
+
f" recording: {stop_result.message}"
|
|
725
|
+
)
|
|
726
|
+
except asyncio.CancelledError:
|
|
727
|
+
err = f"[{task_name}] Task cancelled"
|
|
728
|
+
logger.warning(err)
|
|
729
|
+
await task.finalize(
|
|
730
|
+
content=output,
|
|
731
|
+
state=last_state_snapshot,
|
|
732
|
+
error=err,
|
|
733
|
+
cancelled=True,
|
|
734
|
+
)
|
|
735
|
+
if context.data_engine:
|
|
736
|
+
context.data_engine.end_session("cancelled")
|
|
737
|
+
|
|
738
|
+
raise
|
|
739
|
+
except Exception as e:
|
|
740
|
+
err = f"[{task_name}] Error running automation: {e}"
|
|
741
|
+
logger.error(err)
|
|
742
|
+
await task.finalize(
|
|
743
|
+
content=output,
|
|
744
|
+
state=last_state_snapshot,
|
|
745
|
+
error=err,
|
|
746
|
+
)
|
|
747
|
+
if context.data_engine:
|
|
748
|
+
context.data_engine.end_session("failed")
|
|
749
|
+
|
|
750
|
+
raise
|
|
751
|
+
finally:
|
|
752
|
+
try:
|
|
753
|
+
# Background ADB processes (logcat, screenrecord, ...) started
|
|
754
|
+
# by the Operator must not outlive the automation task.
|
|
755
|
+
from artemis.tools.command_tool import shutdown_adb_background_tasks
|
|
756
|
+
|
|
757
|
+
await asyncio.wait_for(shutdown_adb_background_tasks(context), timeout=15.0)
|
|
758
|
+
except Exception as e:
|
|
759
|
+
logger.warning(f"[{task_name}] Failed to stop background ADB tasks: {e}")
|
|
760
|
+
try:
|
|
761
|
+
await self._finalize_tracing_safely(task=task, context=context)
|
|
762
|
+
finally:
|
|
763
|
+
if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
|
|
764
|
+
try:
|
|
765
|
+
if self._ui_adb_client is not None:
|
|
766
|
+
await asyncio.to_thread(self._ui_adb_client.disconnect)
|
|
767
|
+
finally:
|
|
768
|
+
if device_lock is not None:
|
|
769
|
+
await asyncio.to_thread(device_lock.release)
|
|
770
|
+
|
|
771
|
+
async with self._task_lock:
|
|
772
|
+
if self._current_task and not self._current_task.done():
|
|
773
|
+
logger.warning(
|
|
774
|
+
"Another automation task is already running. "
|
|
775
|
+
"Stopping it before starting the new one."
|
|
776
|
+
)
|
|
777
|
+
self.stop_current_task()
|
|
778
|
+
try:
|
|
779
|
+
await self._current_task
|
|
780
|
+
except asyncio.CancelledError:
|
|
781
|
+
pass
|
|
782
|
+
|
|
783
|
+
cancel_watcher: asyncio.Task | None = None
|
|
784
|
+
try:
|
|
785
|
+
self._current_task = asyncio.create_task(_execute_task_logic())
|
|
786
|
+
cancel_watcher = asyncio.create_task(self._watch_external_cancel(task_name))
|
|
787
|
+
return await self._current_task
|
|
788
|
+
finally:
|
|
789
|
+
self._current_task = None
|
|
790
|
+
if cancel_watcher is not None and not cancel_watcher.done():
|
|
791
|
+
cancel_watcher.cancel()
|
|
792
|
+
try:
|
|
793
|
+
await cancel_watcher
|
|
794
|
+
except asyncio.CancelledError:
|
|
795
|
+
pass
|
|
796
|
+
|
|
797
|
+
def stop_current_task(self):
|
|
798
|
+
"""Requests cancellation of the currently running automation task."""
|
|
799
|
+
if self._current_task and not self._current_task.done():
|
|
800
|
+
logger.info("Requesting to stop the current automation task...")
|
|
801
|
+
was_cancelled = self._current_task.cancel()
|
|
802
|
+
if was_cancelled:
|
|
803
|
+
logger.success("Cancellation request for the current task was sent.")
|
|
804
|
+
else:
|
|
805
|
+
logger.warning(
|
|
806
|
+
"Could not send cancellation request for the current task "
|
|
807
|
+
"(it may already be completing)."
|
|
808
|
+
)
|
|
809
|
+
else:
|
|
810
|
+
logger.info("No active automation task to stop.")
|
|
811
|
+
|
|
812
|
+
async def get_screenshot(self) -> Image.Image:
|
|
813
|
+
"""Capture a screenshot from the mobile device.
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
Screenshot as PIL Image
|
|
817
|
+
|
|
818
|
+
Raises:
|
|
819
|
+
AgentNotInitializedError: If the agent is not initialized
|
|
820
|
+
Exception: If screenshot capture fails
|
|
821
|
+
"""
|
|
822
|
+
if not self._initialized:
|
|
823
|
+
raise AgentNotInitializedError()
|
|
824
|
+
|
|
825
|
+
# Use ADB to capture screenshot
|
|
826
|
+
logger.info("Capturing screenshot from local Android device")
|
|
827
|
+
if not self._adb_client:
|
|
828
|
+
raise Exception("ADB client not initialized")
|
|
829
|
+
|
|
830
|
+
device = self._adb_client.device(serial=self._device_context.device_id)
|
|
831
|
+
screenshot = await asyncio.to_thread(device.screenshot)
|
|
832
|
+
logger.info("Screenshot captured from local Android device")
|
|
833
|
+
return screenshot
|
|
834
|
+
|
|
835
|
+
async def _prepare_app_installation(self, task: Task) -> str | None:
|
|
836
|
+
"""Install app if app_path is specified in the task request.
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
None.
|
|
840
|
+
"""
|
|
841
|
+
if not task.request.app_path:
|
|
842
|
+
return None
|
|
843
|
+
|
|
844
|
+
task_name = task.get_name()
|
|
845
|
+
logger.info(f"[{task_name}] Installing app from: {task.request.app_path}")
|
|
846
|
+
|
|
847
|
+
await self.install_app(task.request.app_path)
|
|
848
|
+
|
|
849
|
+
return None
|
|
850
|
+
|
|
851
|
+
async def _prepare_app_lock(self, task: Task, context: ArtemisContext):
|
|
852
|
+
"""Prepare app lock by launching the locked app if specified."""
|
|
853
|
+
if not task.request.locked_app_package:
|
|
854
|
+
return
|
|
855
|
+
|
|
856
|
+
task_name = task.get_name()
|
|
857
|
+
logger.info(f"[{task_name}] Preparing app lock for: {task.request.locked_app_package}")
|
|
858
|
+
|
|
859
|
+
app_lock_status = await _handle_initial_app_launch(
|
|
860
|
+
ctx=context, locked_app_package=task.request.locked_app_package
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
if context.execution_setup is None:
|
|
864
|
+
context.execution_setup = ExecutionSetup(app_lock_status=app_lock_status)
|
|
865
|
+
else:
|
|
866
|
+
context.execution_setup.app_lock_status = app_lock_status
|
|
867
|
+
|
|
868
|
+
if app_lock_status.locked_app_initial_launch_success is False:
|
|
869
|
+
error = app_lock_status.locked_app_initial_launch_error
|
|
870
|
+
logger.warning(f"[{task_name}] Failed to launch locked app: {error}")
|
|
871
|
+
|
|
872
|
+
def _prepare_trace_paths(self, task: Task) -> str:
|
|
873
|
+
"""Create the temp (and, when recording, output) trace directories; return the task name."""
|
|
874
|
+
task_name = task.get_name()
|
|
875
|
+
temp_trace_path = Path(self._tmp_traces_dir / task_name).resolve()
|
|
876
|
+
temp_trace_path.mkdir(parents=True, exist_ok=True)
|
|
877
|
+
|
|
878
|
+
if task.request.record_trace:
|
|
879
|
+
traces_output_path = Path(task.request.trace_path).resolve()
|
|
880
|
+
logger.info(f"[{task_name}] 📂 Traces output path: {traces_output_path}")
|
|
881
|
+
logger.info(f"[{task_name}] 📄📂 Traces temp path: {temp_trace_path}")
|
|
882
|
+
traces_output_path.mkdir(parents=True, exist_ok=True)
|
|
883
|
+
return task_name
|
|
884
|
+
|
|
885
|
+
async def _finalize_tracing(self, task: Task, context: ArtemisContext):
|
|
886
|
+
if context.data_engine:
|
|
887
|
+
session_status = "completed"
|
|
888
|
+
if task.status == "failed":
|
|
889
|
+
session_status = "failed"
|
|
890
|
+
elif task.status == "cancelled":
|
|
891
|
+
session_status = "cancelled"
|
|
892
|
+
context.data_engine.end_session(status=session_status)
|
|
893
|
+
|
|
894
|
+
exec_setup_ctx = context.execution_setup
|
|
895
|
+
if not exec_setup_ctx:
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
if exec_setup_ctx.traces_path is None or exec_setup_ctx.trace_name is None:
|
|
899
|
+
return
|
|
900
|
+
|
|
901
|
+
task_name = task.get_name()
|
|
902
|
+
temp_trace_path = (self._tmp_traces_dir / exec_setup_ctx.trace_name).resolve()
|
|
903
|
+
|
|
904
|
+
if not task.request.record_trace:
|
|
905
|
+
try:
|
|
906
|
+
if temp_trace_path.exists():
|
|
907
|
+
shutil.rmtree(temp_trace_path)
|
|
908
|
+
logger.info(f"[{task_name}] Cleaned up temporary trace folder.")
|
|
909
|
+
except Exception as e:
|
|
910
|
+
logger.error(f"[{task_name}] Failed to clean up temp trace folder: {e}")
|
|
911
|
+
return
|
|
912
|
+
|
|
913
|
+
status = resolve_trace_suffix(task.status, getattr(context, "run_outcome", None))
|
|
914
|
+
ts = task.created_at.strftime("%Y-%m-%dT%H-%M-%S")
|
|
915
|
+
new_name = f"{exec_setup_ctx.trace_name}{status}_{ts}"
|
|
916
|
+
|
|
917
|
+
traces_output_path = Path(task.request.trace_path).resolve()
|
|
918
|
+
|
|
919
|
+
logger.info(f"[{task_name}] Compiling trace FROM FOLDER: " + str(temp_trace_path))
|
|
920
|
+
create_gif_from_trace_folder(temp_trace_path)
|
|
921
|
+
create_steps_json_from_trace_folder(temp_trace_path)
|
|
922
|
+
|
|
923
|
+
logger.info(f"[{task_name}] Video created, removing dust...")
|
|
924
|
+
remove_images_from_trace_folder(temp_trace_path)
|
|
925
|
+
remove_steps_json_from_trace_folder(temp_trace_path)
|
|
926
|
+
logger.info(f"[{task_name}] 📽️ Trace compiled, moving to output path 📽️")
|
|
927
|
+
|
|
928
|
+
output_folder_path = temp_trace_path.rename(traces_output_path / new_name).resolve()
|
|
929
|
+
logger.info(f"[{task_name}] 📂✅ Traces located in: {output_folder_path}")
|
|
930
|
+
|
|
931
|
+
new_video_path = output_folder_path / "recording.mp4"
|
|
932
|
+
if new_video_path.exists() and context.data_engine:
|
|
933
|
+
context.data_engine.update_video_path(new_video_path)
|
|
934
|
+
|
|
935
|
+
def _prepare_output_files(self, task: Task):
|
|
936
|
+
if task.request.llm_output_path:
|
|
937
|
+
_validate_and_prepare_file(file_path=task.request.llm_output_path)
|
|
938
|
+
|
|
939
|
+
async def _extract_output(
|
|
940
|
+
self,
|
|
941
|
+
task_name: str,
|
|
942
|
+
ctx: ArtemisContext,
|
|
943
|
+
request: TaskRequest[TOutput],
|
|
944
|
+
output_config: OutputConfig | None,
|
|
945
|
+
state: State,
|
|
946
|
+
) -> str | dict | TOutput | None:
|
|
947
|
+
exec_setup = getattr(ctx, "execution_setup", None)
|
|
948
|
+
outputter_cfg = getattr(exec_setup, "outputter", None) or (
|
|
949
|
+
getattr(ctx.agent_config, "outputter", None) if ctx and ctx.agent_config else None
|
|
950
|
+
)
|
|
951
|
+
outputter_enabled = getattr(outputter_cfg, "enabled", True) if outputter_cfg else True
|
|
952
|
+
force_synthesis = (
|
|
953
|
+
getattr(outputter_cfg, "force_synthesis", False) if outputter_cfg else False
|
|
954
|
+
)
|
|
955
|
+
if exec_setup and getattr(exec_setup, "disable_outputter", False):
|
|
956
|
+
outputter_enabled = False
|
|
957
|
+
|
|
958
|
+
should_run = outputter_enabled and (
|
|
959
|
+
(
|
|
960
|
+
output_config
|
|
961
|
+
and output_config.needs_structured_format(default_enabled=outputter_enabled)
|
|
962
|
+
)
|
|
963
|
+
or force_synthesis
|
|
964
|
+
or (output_config and output_config.enable_outputter is True)
|
|
965
|
+
)
|
|
966
|
+
if output_config and output_config.enable_outputter is False:
|
|
967
|
+
should_run = False
|
|
968
|
+
|
|
969
|
+
if should_run:
|
|
970
|
+
logger.info(f"[{task_name}] Generating structured output via Outputter...")
|
|
971
|
+
try:
|
|
972
|
+
structured_output = await outputter(
|
|
973
|
+
ctx=ctx,
|
|
974
|
+
output_config=output_config or OutputConfig(),
|
|
975
|
+
graph_output=state,
|
|
976
|
+
)
|
|
977
|
+
logger.info(f"[{task_name}] Structured output: {structured_output}")
|
|
978
|
+
record_events(
|
|
979
|
+
output_path=request.llm_output_path,
|
|
980
|
+
events=structured_output,
|
|
981
|
+
)
|
|
982
|
+
if request.output_format is not None and request.output_format is not NoneType:
|
|
983
|
+
return request.output_format.model_validate(structured_output)
|
|
984
|
+
return structured_output
|
|
985
|
+
except Exception as e:
|
|
986
|
+
logger.error(f"[{task_name}] Failed to generate structured output: {e}")
|
|
987
|
+
return None
|
|
988
|
+
return None
|
|
989
|
+
|
|
990
|
+
def _init_clients(
|
|
991
|
+
self,
|
|
992
|
+
device_id: str,
|
|
993
|
+
platform: DevicePlatform,
|
|
994
|
+
):
|
|
995
|
+
self._adb_client = AdbClient(
|
|
996
|
+
host=self._config.servers.adb_host,
|
|
997
|
+
port=self._config.servers.adb_port,
|
|
998
|
+
)
|
|
999
|
+
self._ui_adb_client = create_screen_client(device_id)
|
|
1000
|
+
|
|
1001
|
+
async def _get_device_context(
|
|
1002
|
+
self,
|
|
1003
|
+
device_id: str,
|
|
1004
|
+
platform: DevicePlatform,
|
|
1005
|
+
) -> DeviceContext:
|
|
1006
|
+
|
|
1007
|
+
host_platform = system()
|
|
1008
|
+
if os.environ.get("ARTEMIS_CLOUD_MODE") == "1":
|
|
1009
|
+
width, height = (1080, 2400)
|
|
1010
|
+
if self._adb_client:
|
|
1011
|
+
try:
|
|
1012
|
+
width, height = self._adb_client.device(device_id).window_size()
|
|
1013
|
+
logger.info(
|
|
1014
|
+
f"Retrieved remote device screen dimensions dynamically: {width}x{height}"
|
|
1015
|
+
)
|
|
1016
|
+
except Exception as e:
|
|
1017
|
+
logger.debug(
|
|
1018
|
+
f"Failed to query remote window size: {e}. Defaulting to 1080x2400."
|
|
1019
|
+
)
|
|
1020
|
+
return DeviceContext(
|
|
1021
|
+
host_platform="LINUX",
|
|
1022
|
+
mobile_platform=platform,
|
|
1023
|
+
device_id=device_id,
|
|
1024
|
+
device_width=width,
|
|
1025
|
+
device_height=height,
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
# Query dimensions without starting UIAutomator or acquiring an awake
|
|
1029
|
+
# strategy. Those belong inside the global execution queue lease.
|
|
1030
|
+
if self._adb_client:
|
|
1031
|
+
try:
|
|
1032
|
+
device_width, device_height = self._adb_client.device(device_id).window_size()
|
|
1033
|
+
logger.info(f"Retrieved Android screen dimensions: {device_width}x{device_height}")
|
|
1034
|
+
except Exception as e:
|
|
1035
|
+
logger.warning(f"Failed to get Android window size: {e}, using defaults")
|
|
1036
|
+
device_width, device_height = 1080, 2340
|
|
1037
|
+
else:
|
|
1038
|
+
logger.warning("ADB client not available, using default dimensions")
|
|
1039
|
+
device_width, device_height = 1080, 2340
|
|
1040
|
+
|
|
1041
|
+
from artemis.platform import platform as pal_platform
|
|
1042
|
+
|
|
1043
|
+
return DeviceContext(
|
|
1044
|
+
host_platform=pal_platform.os_type.name,
|
|
1045
|
+
mobile_platform=platform,
|
|
1046
|
+
device_id=device_id,
|
|
1047
|
+
device_width=device_width,
|
|
1048
|
+
device_height=device_height,
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _validate_and_prepare_file(file_path: Path):
|
|
1053
|
+
path_obj = Path(file_path)
|
|
1054
|
+
if path_obj.exists() and path_obj.is_dir():
|
|
1055
|
+
raise AgentTaskRequestError(f"Error: Path '{file_path}' is a directory, not a file.")
|
|
1056
|
+
try:
|
|
1057
|
+
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
1058
|
+
path_obj.touch(exist_ok=True)
|
|
1059
|
+
except OSError as e:
|
|
1060
|
+
raise AgentTaskRequestError(f"Error creating file '{file_path}': {e}")
|