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,760 @@
|
|
|
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
|
+
"""MCP-backed action executor for the FlashRunner.
|
|
16
|
+
|
|
17
|
+
Device actions use the in-process MCP session. Argument normalization,
|
|
18
|
+
element-index resolution, coordinate descriptions, smart swipes, post-action
|
|
19
|
+
observations, state updates, tracing, and helper tools are handled here on the
|
|
20
|
+
agent side.
|
|
21
|
+
|
|
22
|
+
Flash binds the same agent dialect as the Pro Operator: a ``click``,
|
|
23
|
+
``long_press`` or ``input_text`` target is either an element index into the
|
|
24
|
+
indexed ``--- Visible UI Elements ---`` list (resolved here to the element's
|
|
25
|
+
center, its observed text/bounds/id recorded) or a normalized ``[x, y]`` pair
|
|
26
|
+
carrying the model's own ``target_description``. The wire only ever sees
|
|
27
|
+
coordinates.
|
|
28
|
+
|
|
29
|
+
Device action status comes from ``ActionResult.ok``; helper tools report failures
|
|
30
|
+
through ``ToolFailure`` or an explicit status.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import ast
|
|
34
|
+
import hashlib
|
|
35
|
+
import json
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from artemis.core.tool_failure import ToolFailure, is_tool_failure
|
|
40
|
+
from artemis.context import ArtemisContext
|
|
41
|
+
from artemis.controllers.unified_controller import UnifiedMobileController
|
|
42
|
+
from artemis.data_engine.trace import TraceSpan
|
|
43
|
+
from artemis.mcp.action_manifest import OPTIONAL_ACTIONS, REQUIRED_ACTIONS
|
|
44
|
+
from artemis.mcp.action_specs import exception_prefix
|
|
45
|
+
from artemis.mcp.action_session import ActionSession, get_action_session
|
|
46
|
+
from artemis.mcp.action_types import ActionCode, ActionResult
|
|
47
|
+
from artemis.mcp.actuators.adb import AdbActuator
|
|
48
|
+
from artemis.agents.validator.tool_declarations import (
|
|
49
|
+
ToolExecutionResult,
|
|
50
|
+
normalize_coordinate_target,
|
|
51
|
+
)
|
|
52
|
+
from artemis.tools.history import HISTORY_TOOL_NAMES, history_tool_by_name
|
|
53
|
+
from artemis.tools.tool_wrapper import split_multimodal_result
|
|
54
|
+
from artemis.utils.coordinates import (
|
|
55
|
+
compute_smart_swipe_coordinates,
|
|
56
|
+
parse_swipe_parameters,
|
|
57
|
+
)
|
|
58
|
+
from artemis.utils.notes import (
|
|
59
|
+
format_list_notes_failure,
|
|
60
|
+
format_list_notes_success,
|
|
61
|
+
format_read_note_failure,
|
|
62
|
+
list_notes_info,
|
|
63
|
+
read_note_content,
|
|
64
|
+
)
|
|
65
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
66
|
+
|
|
67
|
+
logger = get_logger(__name__)
|
|
68
|
+
|
|
69
|
+
__all__ = ["McpActionExecutor"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
#: Non-device tools executed agent-side (never behind the action server).
|
|
73
|
+
AGENT_TOOL_NAMES: frozenset[str] = (
|
|
74
|
+
frozenset({"read_note", "list_notes", "ask_explorer", "video_analyzer"}) | HISTORY_TOOL_NAMES
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
#: Actions whose ``target`` is a single point: an element index or an [x, y] pair.
|
|
78
|
+
_POINT_TARGET_ACTIONS: frozenset[str] = frozenset({"click", "long_press", "input_text"})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _ArgError(ValueError):
|
|
82
|
+
"""Argument-translation failure whose message is already fully formatted."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class McpActionExecutor:
|
|
86
|
+
"""Routes agent tool calls through the unified action MCP session."""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
ctx: ArtemisContext,
|
|
91
|
+
controller: UnifiedMobileController | None = None,
|
|
92
|
+
actuator: AdbActuator | None = None,
|
|
93
|
+
agent_name: str = "validator",
|
|
94
|
+
):
|
|
95
|
+
self.ctx = ctx
|
|
96
|
+
self.actuator = actuator or getattr(ctx, "actuator", None) or AdbActuator(ctx, controller)
|
|
97
|
+
self.controller = self.actuator.controller
|
|
98
|
+
# Identifies the calling agent / profile to the Explorer tier resolver
|
|
99
|
+
# (``explorer.flash_mode`` vs ``explorer.pro_mode``); the agent itself
|
|
100
|
+
# never sees or chooses the tier.
|
|
101
|
+
self.agent_name = agent_name
|
|
102
|
+
self._session: ActionSession | None = None
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def action_tool_names(self) -> frozenset[str]:
|
|
106
|
+
"""Device actions plus backend extension names -- the dynamic dispatch set."""
|
|
107
|
+
return (REQUIRED_ACTIONS | OPTIONAL_ACTIONS) | {e.name for e in self.actuator.extensions()}
|
|
108
|
+
|
|
109
|
+
async def _session_or_start(self) -> ActionSession:
|
|
110
|
+
if self._session is None or not self._session.started:
|
|
111
|
+
self._session = await get_action_session(self.ctx, actuator=self.actuator)
|
|
112
|
+
return self._session
|
|
113
|
+
|
|
114
|
+
# --- Public entry ----------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
async def execute(
|
|
117
|
+
self,
|
|
118
|
+
name: str,
|
|
119
|
+
args: dict[str, Any],
|
|
120
|
+
tool_call_id: str,
|
|
121
|
+
state: Any,
|
|
122
|
+
*,
|
|
123
|
+
index_elements: list[dict[str, Any]] | None = None,
|
|
124
|
+
) -> ToolExecutionResult:
|
|
125
|
+
"""Executes the named tool and wraps the outcome for the calling agent.
|
|
126
|
+
|
|
127
|
+
``index_elements`` is the element list an index target (``click(3)``)
|
|
128
|
+
resolves against; ``None`` means ``state.indexed_elements`` at call
|
|
129
|
+
time. A multi-action turn passes the list from the observation the
|
|
130
|
+
model decided on, so its later indices are not resolved against the
|
|
131
|
+
list refreshed by its earlier actions (``state.indexed_elements`` is
|
|
132
|
+
still refreshed after every action: it is what the next observation
|
|
133
|
+
shows).
|
|
134
|
+
"""
|
|
135
|
+
raw_name = name.split(":")[-1] if ":" in name else name
|
|
136
|
+
|
|
137
|
+
if raw_name in AGENT_TOOL_NAMES:
|
|
138
|
+
return await self._execute_agent_tool(raw_name, name, args, tool_call_id, state)
|
|
139
|
+
|
|
140
|
+
if raw_name in self.action_tool_names:
|
|
141
|
+
return await self._execute_device_action(
|
|
142
|
+
raw_name, name, args, tool_call_id, state, index_elements=index_elements
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return ToolExecutionResult(
|
|
146
|
+
tool_call_id=tool_call_id,
|
|
147
|
+
tool_name=name,
|
|
148
|
+
status="error",
|
|
149
|
+
text_summary=f"Error: Tool '{name}' not supported.",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# --- Device actions --------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
async def _execute_device_action(
|
|
155
|
+
self,
|
|
156
|
+
raw_name: str,
|
|
157
|
+
name: str,
|
|
158
|
+
args: dict[str, Any],
|
|
159
|
+
tool_call_id: str,
|
|
160
|
+
state: Any,
|
|
161
|
+
*,
|
|
162
|
+
index_elements: list[dict[str, Any]] | None = None,
|
|
163
|
+
) -> ToolExecutionResult:
|
|
164
|
+
span = TraceSpan(name=raw_name, trace_type="action", ctx=self.ctx)
|
|
165
|
+
span.payload = {"args": args}
|
|
166
|
+
|
|
167
|
+
message = ""
|
|
168
|
+
res: ActionResult | None = None
|
|
169
|
+
img_bytes: bytes | None = None
|
|
170
|
+
shot_path: str | None = None
|
|
171
|
+
xml_list: str | None = None
|
|
172
|
+
target_semantics: dict[str, Any] = {}
|
|
173
|
+
target_coordinates: list[int] | None = None
|
|
174
|
+
with span:
|
|
175
|
+
try:
|
|
176
|
+
session = await self._session_or_start()
|
|
177
|
+
wire_name, wire_args, finalize, target_semantics = self._translate(
|
|
178
|
+
raw_name, args, state, index_elements=index_elements
|
|
179
|
+
)
|
|
180
|
+
if raw_name in _POINT_TARGET_ACTIONS:
|
|
181
|
+
# The normalized point the action was sent to: the recorded
|
|
182
|
+
# coordinates of an index target (the model named an index,
|
|
183
|
+
# not a point).
|
|
184
|
+
target_coordinates = wire_args.get("target")
|
|
185
|
+
extension = raw_name not in (REQUIRED_ACTIONS | OPTIONAL_ACTIONS)
|
|
186
|
+
|
|
187
|
+
if extension:
|
|
188
|
+
raw_result = await session.call_raw(wire_name, wire_args)
|
|
189
|
+
message = next(
|
|
190
|
+
(b.text for b in raw_result.content if getattr(b, "type", "") == "text"),
|
|
191
|
+
f"Extension tool '{raw_name}' completed.",
|
|
192
|
+
)
|
|
193
|
+
res = ActionResult.success(raw_name, message)
|
|
194
|
+
else:
|
|
195
|
+
res = await session.call(wire_name, wire_args)
|
|
196
|
+
message = finalize(res) if finalize else res.message
|
|
197
|
+
|
|
198
|
+
if res.ok or self._observe_despite_failure(raw_name, res):
|
|
199
|
+
obs = await session.observe(settle_ms=400)
|
|
200
|
+
if obs.ok:
|
|
201
|
+
shot_path = obs.screenshot_path
|
|
202
|
+
xml_list = obs.elements_text
|
|
203
|
+
if obs.hierarchy_ok:
|
|
204
|
+
state.indexed_points = [el["center"] for el in obs.elements]
|
|
205
|
+
state.indexed_elements = obs.elements
|
|
206
|
+
if shot_path:
|
|
207
|
+
try:
|
|
208
|
+
img_bytes = Path(shot_path).read_bytes()
|
|
209
|
+
except Exception as read_err:
|
|
210
|
+
logger.warning(f"Failed to read observed screenshot: {read_err}")
|
|
211
|
+
except _ArgError as e:
|
|
212
|
+
res = ActionResult.failure(raw_name, str(e), code=ActionCode.INVALID_ARGS)
|
|
213
|
+
message = str(e)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
message = f"{exception_prefix(raw_name)}: {e}"
|
|
216
|
+
res = ActionResult.failure(raw_name, message, detail=repr(e))
|
|
217
|
+
|
|
218
|
+
if shot_path:
|
|
219
|
+
state.latest_screenshot = shot_path
|
|
220
|
+
|
|
221
|
+
status = "success" if res.ok else "error"
|
|
222
|
+
post_image_name = None
|
|
223
|
+
if shot_path:
|
|
224
|
+
post_image_name = Path(shot_path).stem
|
|
225
|
+
elif img_bytes:
|
|
226
|
+
post_image_name = hashlib.sha256(img_bytes).hexdigest()
|
|
227
|
+
span.result = {
|
|
228
|
+
"outcome": message,
|
|
229
|
+
"post_image_name": post_image_name,
|
|
230
|
+
"has_xml": bool(xml_list),
|
|
231
|
+
"status": status,
|
|
232
|
+
}
|
|
233
|
+
if status == "error":
|
|
234
|
+
span.status = "failed"
|
|
235
|
+
span.error = message
|
|
236
|
+
|
|
237
|
+
return ToolExecutionResult(
|
|
238
|
+
tool_call_id=tool_call_id,
|
|
239
|
+
tool_name=name,
|
|
240
|
+
status=status,
|
|
241
|
+
text_summary=message,
|
|
242
|
+
screenshot_bytes=img_bytes,
|
|
243
|
+
screenshot_path=shot_path,
|
|
244
|
+
ui_elements_text=xml_list,
|
|
245
|
+
metadata={
|
|
246
|
+
"code": res.code.value,
|
|
247
|
+
"normalized_coordinates": res.normalized_coordinates,
|
|
248
|
+
"target_semantics": target_semantics,
|
|
249
|
+
"target_coordinates": target_coordinates,
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def _observe_despite_failure(raw_name: str, res: ActionResult) -> bool:
|
|
255
|
+
"""Capture the screen after launch failures and wait_for_text timeouts.
|
|
256
|
+
|
|
257
|
+
Argument and package lookup errors do not require an observation.
|
|
258
|
+
"""
|
|
259
|
+
if raw_name == "manage_app":
|
|
260
|
+
return res.code not in (ActionCode.PACKAGE_NOT_FOUND, ActionCode.INVALID_ARGS)
|
|
261
|
+
if raw_name == "wait_for_text":
|
|
262
|
+
return res.code is ActionCode.TIMEOUT
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
# --- Argument translation --------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
def _translate(
|
|
268
|
+
self,
|
|
269
|
+
raw_name: str,
|
|
270
|
+
args: dict[str, Any],
|
|
271
|
+
state: Any,
|
|
272
|
+
*,
|
|
273
|
+
index_elements: list[dict[str, Any]] | None = None,
|
|
274
|
+
) -> tuple[str, dict[str, Any], Any, dict[str, Any]]:
|
|
275
|
+
"""Maps agent-facing args to wire args; returns (name, args, finalize, recorded).
|
|
276
|
+
|
|
277
|
+
``finalize(res)`` adds client-side context to swipe result messages.
|
|
278
|
+
``recorded`` holds the target semantics in the Pro Operator's record
|
|
279
|
+
format: the observed element fields of an index target, or the validated
|
|
280
|
+
``target_description`` of a coordinate target. Focused input and
|
|
281
|
+
directional swipes have no target, so they record nothing.
|
|
282
|
+
``index_elements`` overrides the list index targets resolve against
|
|
283
|
+
(see :meth:`execute`).
|
|
284
|
+
"""
|
|
285
|
+
if raw_name == "click":
|
|
286
|
+
target, recorded = self._resolve_target(
|
|
287
|
+
args.get("target") or args.get("coordinates"),
|
|
288
|
+
"click",
|
|
289
|
+
args,
|
|
290
|
+
state,
|
|
291
|
+
index_elements=index_elements,
|
|
292
|
+
)
|
|
293
|
+
return (
|
|
294
|
+
"click",
|
|
295
|
+
{
|
|
296
|
+
"target": target,
|
|
297
|
+
"times": args.get("times", 1),
|
|
298
|
+
"delay_ms": args.get("delay_ms", 100),
|
|
299
|
+
},
|
|
300
|
+
None,
|
|
301
|
+
recorded,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if raw_name == "long_press":
|
|
305
|
+
target, recorded = self._resolve_target(
|
|
306
|
+
args.get("target") or args.get("coordinates"),
|
|
307
|
+
"long press",
|
|
308
|
+
args,
|
|
309
|
+
state,
|
|
310
|
+
index_elements=index_elements,
|
|
311
|
+
)
|
|
312
|
+
return (
|
|
313
|
+
"long_press",
|
|
314
|
+
{
|
|
315
|
+
"target": target,
|
|
316
|
+
"duration_ms": args.get("duration_ms", args.get("duration", 1000)),
|
|
317
|
+
},
|
|
318
|
+
None,
|
|
319
|
+
recorded,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if raw_name == "input_text":
|
|
323
|
+
raw_target = args.get("target") or args.get("coordinates")
|
|
324
|
+
target = None
|
|
325
|
+
recorded = {}
|
|
326
|
+
if raw_target:
|
|
327
|
+
target, recorded = self._resolve_target(
|
|
328
|
+
raw_target, "input text", args, state, index_elements=index_elements
|
|
329
|
+
)
|
|
330
|
+
return (
|
|
331
|
+
"input_text",
|
|
332
|
+
{
|
|
333
|
+
"text": args.get("text", ""),
|
|
334
|
+
"target": target,
|
|
335
|
+
"clear_exist": args.get("clear_exist", True),
|
|
336
|
+
},
|
|
337
|
+
None,
|
|
338
|
+
recorded,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if raw_name == "click_sequence":
|
|
342
|
+
sequence = self._resolve_sequence(args.get("sequence") or [])
|
|
343
|
+
recorded = self._require_descriptions(args, len(sequence))
|
|
344
|
+
return (
|
|
345
|
+
"click_sequence",
|
|
346
|
+
{
|
|
347
|
+
"sequence": sequence,
|
|
348
|
+
"delay_ms": args.get("delay_ms", 50),
|
|
349
|
+
},
|
|
350
|
+
None,
|
|
351
|
+
recorded,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if raw_name == "swipe":
|
|
355
|
+
return self._translate_swipe(args, state, index_elements=index_elements)
|
|
356
|
+
|
|
357
|
+
if raw_name == "press_key":
|
|
358
|
+
return "press_key", {"key": args.get("key", "BACK")}, None, {}
|
|
359
|
+
|
|
360
|
+
if raw_name == "manage_app":
|
|
361
|
+
return (
|
|
362
|
+
"manage_app",
|
|
363
|
+
{
|
|
364
|
+
"action": args.get("action", "launch"),
|
|
365
|
+
"app_name": args.get("app_name", ""),
|
|
366
|
+
},
|
|
367
|
+
None,
|
|
368
|
+
{},
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if raw_name == "wait_for_delay":
|
|
372
|
+
return "wait_for_delay", {"time_in_ms": args.get("time_in_ms", 1000)}, None, {}
|
|
373
|
+
|
|
374
|
+
if raw_name == "wait_for_text":
|
|
375
|
+
return (
|
|
376
|
+
"wait_for_text",
|
|
377
|
+
{
|
|
378
|
+
"text": args.get("text", ""),
|
|
379
|
+
"wait_state": args.get("wait_state") or "appear",
|
|
380
|
+
"timeout_ms": args.get("timeout_ms", 5000),
|
|
381
|
+
},
|
|
382
|
+
None,
|
|
383
|
+
{},
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Backend extension: pass the arguments straight through.
|
|
387
|
+
return raw_name, dict(args), None, {}
|
|
388
|
+
|
|
389
|
+
def _screen_size(self) -> tuple[int, int]:
|
|
390
|
+
"""The device resolution the indexed element list was built against."""
|
|
391
|
+
device = getattr(self.ctx, "device", None)
|
|
392
|
+
width = getattr(device, "device_width", None) if device else None
|
|
393
|
+
height = getattr(device, "device_height", None) if device else None
|
|
394
|
+
return (
|
|
395
|
+
width if isinstance(width, int) and width > 0 else 1080,
|
|
396
|
+
height if isinstance(height, int) and height > 0 else 2400,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
def _resolve_target(
|
|
400
|
+
self,
|
|
401
|
+
raw: Any,
|
|
402
|
+
label: str,
|
|
403
|
+
args: dict[str, Any],
|
|
404
|
+
state: Any,
|
|
405
|
+
*,
|
|
406
|
+
index_elements: list[dict[str, Any]] | None = None,
|
|
407
|
+
) -> tuple[list[int], dict[str, Any]]:
|
|
408
|
+
"""Resolves a point target to ``([x, y] normalized, recorded semantics)``.
|
|
409
|
+
|
|
410
|
+
An element index carries observed semantics (text, bounds, id, class
|
|
411
|
+
straight from the indexed list); a coordinate pair carries only the
|
|
412
|
+
model's own ``target_description``. The two never mix (Pro parity).
|
|
413
|
+
"""
|
|
414
|
+
target = normalize_coordinate_target(raw)
|
|
415
|
+
if isinstance(target, bool):
|
|
416
|
+
target = None
|
|
417
|
+
if isinstance(target, (int, float)):
|
|
418
|
+
return self._resolve_index(int(target), label, state, index_elements=index_elements)
|
|
419
|
+
if isinstance(target, (list, tuple)) and len(target) == 2:
|
|
420
|
+
recorded = self._require_description(args, label)
|
|
421
|
+
return [int(target[0]), int(target[1])], recorded
|
|
422
|
+
raise _ArgError(
|
|
423
|
+
f"Error during {label}: Invalid target format: {raw!r}. Use an element"
|
|
424
|
+
" index from the Visible UI Elements list (e.g. 3) or normalized [x, y]"
|
|
425
|
+
" coordinates with a target_description."
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def _resolve_index(
|
|
429
|
+
self,
|
|
430
|
+
index: int,
|
|
431
|
+
label: str,
|
|
432
|
+
state: Any,
|
|
433
|
+
*,
|
|
434
|
+
index_elements: list[dict[str, Any]] | None = None,
|
|
435
|
+
) -> tuple[list[int], dict[str, Any]]:
|
|
436
|
+
"""Maps an element index onto the element's center (normalized 0-1000).
|
|
437
|
+
|
|
438
|
+
The recorded fields are what the Operator records for an index target,
|
|
439
|
+
with bounds normalized like every other Flash coordinate. The index is
|
|
440
|
+
resolved against ``index_elements`` when given, else against the
|
|
441
|
+
state's current list.
|
|
442
|
+
"""
|
|
443
|
+
elements = self._index_elements(state, index_elements)
|
|
444
|
+
if not 1 <= index <= len(elements):
|
|
445
|
+
hint = (
|
|
446
|
+
" The list is empty on this screen."
|
|
447
|
+
if not elements
|
|
448
|
+
else f" Active index range is 1 to {len(elements)}."
|
|
449
|
+
)
|
|
450
|
+
raise _ArgError(
|
|
451
|
+
f"Error during {label}: Invalid target index {index}.{hint} Use an index"
|
|
452
|
+
" shown in the Visible UI Elements list, or normalized [x, y]"
|
|
453
|
+
" coordinates with a target_description (ask_explorer can locate an"
|
|
454
|
+
" element that is visible but not listed)."
|
|
455
|
+
)
|
|
456
|
+
element = elements[index - 1]
|
|
457
|
+
width, height = self._screen_size()
|
|
458
|
+
|
|
459
|
+
def _norm_x(v: Any) -> int:
|
|
460
|
+
return int(max(0, min(1000, round(float(v) * 1000 / max(1, width)))))
|
|
461
|
+
|
|
462
|
+
def _norm_y(v: Any) -> int:
|
|
463
|
+
return int(max(0, min(1000, round(float(v) * 1000 / max(1, height)))))
|
|
464
|
+
|
|
465
|
+
center = element.get("center")
|
|
466
|
+
if not (isinstance(center, (list, tuple)) and len(center) == 2):
|
|
467
|
+
raise _ArgError(f"Error during {label}: element [{index}] has no usable center.")
|
|
468
|
+
bounds = element.get("bounds")
|
|
469
|
+
norm_bounds = None
|
|
470
|
+
if isinstance(bounds, (list, tuple)) and len(bounds) == 4:
|
|
471
|
+
norm_bounds = [
|
|
472
|
+
_norm_x(bounds[0]),
|
|
473
|
+
_norm_y(bounds[1]),
|
|
474
|
+
_norm_x(bounds[2]),
|
|
475
|
+
_norm_y(bounds[3]),
|
|
476
|
+
]
|
|
477
|
+
recorded = {
|
|
478
|
+
"target_text": element.get("text"),
|
|
479
|
+
"target_bounds": norm_bounds,
|
|
480
|
+
"target_resource_id": element.get("resource_id"),
|
|
481
|
+
"target_class": element.get("class"),
|
|
482
|
+
}
|
|
483
|
+
return [_norm_x(center[0]), _norm_y(center[1])], recorded
|
|
484
|
+
|
|
485
|
+
@staticmethod
|
|
486
|
+
def _require_description(args: dict[str, Any], label: str) -> dict[str, Any]:
|
|
487
|
+
"""Validate and trim the coordinate target description for the action record."""
|
|
488
|
+
description = args.get("target_description")
|
|
489
|
+
if not (isinstance(description, str) and description.strip()):
|
|
490
|
+
raise _ArgError(
|
|
491
|
+
f"Error during {label}: 'target_description' is required (what the"
|
|
492
|
+
" target is, in a few words)."
|
|
493
|
+
)
|
|
494
|
+
return {"target_description": description.strip()}
|
|
495
|
+
|
|
496
|
+
@staticmethod
|
|
497
|
+
def _require_descriptions(args: dict[str, Any], count: int) -> dict[str, Any]:
|
|
498
|
+
"""``click_sequence``: one description per sequence entry, in order.
|
|
499
|
+
|
|
500
|
+
Returns the cleaned statements as the recorded ``target_descriptions`` field.
|
|
501
|
+
"""
|
|
502
|
+
descriptions = args.get("target_descriptions")
|
|
503
|
+
if isinstance(descriptions, str):
|
|
504
|
+
descriptions = [descriptions]
|
|
505
|
+
if (
|
|
506
|
+
not isinstance(descriptions, (list, tuple))
|
|
507
|
+
or len(descriptions) != count
|
|
508
|
+
or not all(isinstance(d, str) and d.strip() for d in descriptions)
|
|
509
|
+
):
|
|
510
|
+
raise _ArgError(
|
|
511
|
+
"Error during click sequence: 'target_descriptions' is required, one"
|
|
512
|
+
f" non-empty entry per sequence entry ({count} expected)."
|
|
513
|
+
)
|
|
514
|
+
return {"target_descriptions": [d.strip() for d in descriptions]}
|
|
515
|
+
|
|
516
|
+
@staticmethod
|
|
517
|
+
def _resolve_sequence(sequence: Any) -> list[list[int]]:
|
|
518
|
+
"""Normalize click_sequence entries to coordinate pairs; reject element indices."""
|
|
519
|
+
if isinstance(sequence, str):
|
|
520
|
+
sequence_str = sequence.strip()
|
|
521
|
+
try:
|
|
522
|
+
sequence = json.loads(sequence_str)
|
|
523
|
+
except ValueError:
|
|
524
|
+
try:
|
|
525
|
+
sequence = ast.literal_eval(sequence_str)
|
|
526
|
+
except (ValueError, SyntaxError, TypeError, RecursionError):
|
|
527
|
+
pass
|
|
528
|
+
|
|
529
|
+
if not isinstance(sequence, (list, tuple)):
|
|
530
|
+
raise _ArgError(f"Error during click sequence: Invalid sequence format: {sequence}")
|
|
531
|
+
|
|
532
|
+
resolved: list[list[int]] = []
|
|
533
|
+
for position, raw_target in enumerate(sequence, start=1):
|
|
534
|
+
target = normalize_coordinate_target(raw_target)
|
|
535
|
+
if isinstance(target, (int, float)) and not isinstance(target, bool):
|
|
536
|
+
raise _ArgError(
|
|
537
|
+
f"Error during click sequence: entry {position} ({raw_target!r}) is an"
|
|
538
|
+
" element index; click_sequence takes normalized [x, y] coordinate"
|
|
539
|
+
" pairs only. Use the element's coordinates from the element list,"
|
|
540
|
+
" or ask_explorer to locate it."
|
|
541
|
+
)
|
|
542
|
+
if isinstance(target, (list, tuple)) and len(target) == 2:
|
|
543
|
+
nx, ny = int(target[0]), int(target[1])
|
|
544
|
+
else:
|
|
545
|
+
raise _ArgError(f"Error during click sequence: Invalid target format: {raw_target}")
|
|
546
|
+
resolved.append([nx, ny])
|
|
547
|
+
return resolved
|
|
548
|
+
|
|
549
|
+
@staticmethod
|
|
550
|
+
def _index_elements(
|
|
551
|
+
state: Any, index_elements: list[dict[str, Any]] | None
|
|
552
|
+
) -> list[dict[str, Any]]:
|
|
553
|
+
"""The element list index targets resolve against (``None`` -> state)."""
|
|
554
|
+
if index_elements is not None:
|
|
555
|
+
return index_elements
|
|
556
|
+
return getattr(state, "indexed_elements", None) or []
|
|
557
|
+
|
|
558
|
+
def _translate_swipe(
|
|
559
|
+
self,
|
|
560
|
+
args: dict[str, Any],
|
|
561
|
+
state: Any,
|
|
562
|
+
*,
|
|
563
|
+
index_elements: list[dict[str, Any]] | None = None,
|
|
564
|
+
) -> tuple[str, dict[str, Any], Any, dict[str, Any]]:
|
|
565
|
+
width, height = self._screen_size()
|
|
566
|
+
default_duration = args.get("duration", 400)
|
|
567
|
+
kind, target, final_duration = parse_swipe_parameters(
|
|
568
|
+
dict(args), default_duration=default_duration
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
if kind == "direction":
|
|
572
|
+
dir_name = target
|
|
573
|
+
x1, y1, x2, y2, smart_dur = compute_smart_swipe_coordinates(
|
|
574
|
+
direction=target,
|
|
575
|
+
target=args.get("target"),
|
|
576
|
+
indexed_elements=self._index_elements(state, index_elements) if state else None,
|
|
577
|
+
ui_hierarchy=getattr(state, "latest_ui_hierarchy", None) if state else None,
|
|
578
|
+
width=width,
|
|
579
|
+
height=height,
|
|
580
|
+
duration=final_duration,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
def _norm(v: float, size: int) -> int:
|
|
584
|
+
return int(max(0, min(1000, round(v * 1000 / max(1, size)))))
|
|
585
|
+
|
|
586
|
+
def finalize(res: ActionResult) -> str:
|
|
587
|
+
if not res.ok:
|
|
588
|
+
return f"Error swiping {dir_name}: {res.detail}"
|
|
589
|
+
return f"Swiped {dir_name}."
|
|
590
|
+
|
|
591
|
+
return (
|
|
592
|
+
"swipe",
|
|
593
|
+
{
|
|
594
|
+
"start": [_norm(x1, width), _norm(y1, height)],
|
|
595
|
+
"end": [_norm(x2, width), _norm(y2, height)],
|
|
596
|
+
"duration_ms": smart_dur,
|
|
597
|
+
},
|
|
598
|
+
finalize,
|
|
599
|
+
{},
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
if kind == "coords":
|
|
603
|
+
x1, y1, x2, y2 = target
|
|
604
|
+
recorded = self._require_description(args, "swipe")
|
|
605
|
+
|
|
606
|
+
def finalize(res: ActionResult) -> str:
|
|
607
|
+
if not res.ok:
|
|
608
|
+
return f"Error dragging: {res.detail}"
|
|
609
|
+
return f"Swiped from [{x1}, {y1}] to [{x2}, {y2}] (normalized)."
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
"swipe",
|
|
613
|
+
{
|
|
614
|
+
"start": [int(x1), int(y1)],
|
|
615
|
+
"end": [int(x2), int(y2)],
|
|
616
|
+
"duration_ms": final_duration,
|
|
617
|
+
},
|
|
618
|
+
finalize,
|
|
619
|
+
recorded,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
raise _ArgError(f"Error during swipe: Invalid direction: {args}")
|
|
623
|
+
|
|
624
|
+
# --- Non-device tools (stay agent-side) ------------------------------------------
|
|
625
|
+
|
|
626
|
+
async def _execute_agent_tool(
|
|
627
|
+
self,
|
|
628
|
+
raw_name: str,
|
|
629
|
+
name: str,
|
|
630
|
+
args: dict[str, Any],
|
|
631
|
+
tool_call_id: str,
|
|
632
|
+
state: Any,
|
|
633
|
+
) -> ToolExecutionResult:
|
|
634
|
+
span = TraceSpan(name=raw_name, trace_type="tool", ctx=self.ctx)
|
|
635
|
+
span.payload = {"args": args}
|
|
636
|
+
ok: bool | None = None
|
|
637
|
+
blocks: list[dict[str, Any]] | None = None
|
|
638
|
+
with span:
|
|
639
|
+
if raw_name == "read_note":
|
|
640
|
+
text = await self._read_note(
|
|
641
|
+
args.get("key", ""), args.get("start_line"), args.get("end_line")
|
|
642
|
+
)
|
|
643
|
+
elif raw_name == "list_notes":
|
|
644
|
+
text = await self._list_notes()
|
|
645
|
+
elif raw_name == "video_analyzer":
|
|
646
|
+
text, ok = await self._video_analyzer(
|
|
647
|
+
args.get("time_description") or args.get("TimeDescription") or "",
|
|
648
|
+
args.get("purpose") or args.get("Purpose") or "",
|
|
649
|
+
)
|
|
650
|
+
elif raw_name in HISTORY_TOOL_NAMES:
|
|
651
|
+
text, blocks = await self._history_tool(raw_name, args)
|
|
652
|
+
else:
|
|
653
|
+
text, ok = await self._ask_explorer(
|
|
654
|
+
# ``task_description`` is the pre-contract argument name
|
|
655
|
+
# kept for old MCP clients.
|
|
656
|
+
args.get("query") or args.get("task_description") or "",
|
|
657
|
+
args.get("context_feedback"),
|
|
658
|
+
state,
|
|
659
|
+
)
|
|
660
|
+
span.result = {"outcome": text}
|
|
661
|
+
if ok is False:
|
|
662
|
+
span.status = "failed"
|
|
663
|
+
span.error = text
|
|
664
|
+
|
|
665
|
+
# The Explorer and the video analyzer report their status explicitly;
|
|
666
|
+
# note and history texts carry it structurally (a ``ToolFailure``), so a
|
|
667
|
+
# history lookup's "no match" answer is an answer, not an error.
|
|
668
|
+
if ok is None:
|
|
669
|
+
ok = not is_tool_failure(text)
|
|
670
|
+
return ToolExecutionResult(
|
|
671
|
+
tool_call_id=tool_call_id,
|
|
672
|
+
tool_name=name,
|
|
673
|
+
status="success" if ok else "error",
|
|
674
|
+
text_summary=text,
|
|
675
|
+
# Content blocks (a step screenshot) ride along for callers that
|
|
676
|
+
# forward multimodal tool results; the text summary stays complete.
|
|
677
|
+
raw_result=blocks,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
async def _read_note(self, key: str, start_line: int | None, end_line: int | None) -> str:
|
|
681
|
+
try:
|
|
682
|
+
base_dir = self.ctx.data_engine.base_dir if self.ctx.data_engine else None
|
|
683
|
+
if not base_dir:
|
|
684
|
+
return format_read_note_failure(key, "DataEngine not initialized.")
|
|
685
|
+
return read_note_content(base_dir, key, start_line, end_line)
|
|
686
|
+
except Exception as e:
|
|
687
|
+
return format_read_note_failure(key, str(e))
|
|
688
|
+
|
|
689
|
+
async def _list_notes(self) -> str:
|
|
690
|
+
try:
|
|
691
|
+
base_dir = self.ctx.data_engine.base_dir if self.ctx.data_engine else None
|
|
692
|
+
if not base_dir:
|
|
693
|
+
return format_list_notes_failure("DataEngine not initialized.")
|
|
694
|
+
return format_list_notes_success(list_notes_info(base_dir))
|
|
695
|
+
except Exception as e:
|
|
696
|
+
return format_list_notes_failure(str(e))
|
|
697
|
+
|
|
698
|
+
async def _history_tool(
|
|
699
|
+
self, raw_name: str, args: dict[str, Any]
|
|
700
|
+
) -> tuple[str, list[dict] | None]:
|
|
701
|
+
"""Runs one of the shared history tools (the same instances the Pro
|
|
702
|
+
agents bind), dispatched generically by name.
|
|
703
|
+
|
|
704
|
+
Returns the text answer and, for a screenshot, the multimodal content
|
|
705
|
+
blocks the tool produced.
|
|
706
|
+
"""
|
|
707
|
+
tool = history_tool_by_name(raw_name)
|
|
708
|
+
if tool is None:
|
|
709
|
+
return ToolFailure(f"Error: Tool '{raw_name}' not supported."), None
|
|
710
|
+
accepted = {k: v for k, v in args.items() if k in tool.args_schema.model_fields}
|
|
711
|
+
try:
|
|
712
|
+
result = await tool.execute(ctx=self.ctx, **accepted)
|
|
713
|
+
except Exception as e:
|
|
714
|
+
logger.error(f"Error running {raw_name}: {e}")
|
|
715
|
+
return ToolFailure(f"{raw_name} failed: {e}"), None
|
|
716
|
+
text, images = split_multimodal_result(result)
|
|
717
|
+
if images:
|
|
718
|
+
return text or "Screenshot attached.", result
|
|
719
|
+
return text, None
|
|
720
|
+
|
|
721
|
+
async def _video_analyzer(self, time_description: str, purpose: str) -> tuple[str, bool]:
|
|
722
|
+
"""Runs the video-analyzing subagent over the session recording.
|
|
723
|
+
|
|
724
|
+
Same subagent the Pro operator's ``video_analyzer`` LangChain tool
|
|
725
|
+
delegates to; imported lazily because the video stack is heavy.
|
|
726
|
+
"""
|
|
727
|
+
from artemis.agents.video_analyzer.video_analyzer import VideoAnalyzer
|
|
728
|
+
|
|
729
|
+
try:
|
|
730
|
+
outcome, status = await VideoAnalyzer(self.ctx).run(time_description, purpose)
|
|
731
|
+
except Exception as e:
|
|
732
|
+
logger.error(f"Error running video analyzer: {e}")
|
|
733
|
+
return f"Error running video analyzer: {e}", False
|
|
734
|
+
if status in ("failed", "error"):
|
|
735
|
+
return f"Video analysis failed: {outcome}", False
|
|
736
|
+
return outcome, True
|
|
737
|
+
|
|
738
|
+
async def _ask_explorer(
|
|
739
|
+
self, query: str, context_feedback: str | None, state: Any
|
|
740
|
+
) -> tuple[str, bool]:
|
|
741
|
+
"""Runs the Explorer pipeline for this executor's agent; returns ``(text, ok)``.
|
|
742
|
+
|
|
743
|
+
``ok`` is False only when the Explorer run itself failed: a clean
|
|
744
|
+
"not found" is a successful answer the agent has to reason about.
|
|
745
|
+
"""
|
|
746
|
+
# Imported lazily: the explorer stack is heavy and recursive (it spawns an LLM
|
|
747
|
+
# sub-agent), which is also why it must never live behind the action server.
|
|
748
|
+
from artemis.tools.explorer_tool import locate, register_candidates, render_text
|
|
749
|
+
|
|
750
|
+
try:
|
|
751
|
+
outcome = await locate(
|
|
752
|
+
self.ctx, state, query, context_feedback or "", agent_name=self.agent_name
|
|
753
|
+
)
|
|
754
|
+
registered = register_candidates(self.ctx, state, outcome)
|
|
755
|
+
# Candidates join ``state.indexed_elements``, and this executor's
|
|
756
|
+
# ``click`` resolves an index against that list (Pro parity), so the
|
|
757
|
+
# answer teaches the index syntax next to the coordinates.
|
|
758
|
+
return render_text(query, outcome, registered, index_targets=True), not outcome.error
|
|
759
|
+
except Exception as e:
|
|
760
|
+
return f"Error executing ask_explorer: {e}", False
|