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,672 @@
|
|
|
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
|
+
"""XML-hierarchy Safety Net: pre-execution validation of the action target.
|
|
16
|
+
|
|
17
|
+
Checks that the element the Operator decided to interact with is still present
|
|
18
|
+
on the live screen (via the UI hierarchy) and substantially consistent, with
|
|
19
|
+
coordinate self-healing for small drifts and a structured failure taxonomy
|
|
20
|
+
(shifted / occupied / disappeared) surfaced to the Operator as an execution incident.
|
|
21
|
+
|
|
22
|
+
Extracted from ``validator.py``; the public entry points remain the
|
|
23
|
+
``ValidatorNode`` methods, which delegate here.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import difflib
|
|
28
|
+
import math
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
from artemis.agents.validator.categories import ValidationErrorCategory
|
|
32
|
+
from artemis.constants import VALIDATOR_UI_HIERARCHY_TIMEOUT
|
|
33
|
+
from artemis.context import ArtemisContext
|
|
34
|
+
from artemis.graph.state import State
|
|
35
|
+
from artemis.utils import visualization
|
|
36
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
37
|
+
|
|
38
|
+
logger = get_logger(__name__)
|
|
39
|
+
|
|
40
|
+
# Only interactive, element-focused actions are subject to XML validation.
|
|
41
|
+
XML_VALIDATED_ACTIONS = (
|
|
42
|
+
"tap",
|
|
43
|
+
"long_press_on",
|
|
44
|
+
"focus_and_input_text",
|
|
45
|
+
"focus_and_clear_text",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Signal weights for candidate scoring.
|
|
49
|
+
W_ID = 0.5
|
|
50
|
+
W_TEXT = 0.4
|
|
51
|
+
W_BOUNDS = 0.3
|
|
52
|
+
W_COORD = 0.3
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def validate_action_precondition(
|
|
56
|
+
node, session, action_item: dict, state: State | None = None
|
|
57
|
+
) -> tuple[bool, ValidationErrorCategory, str]:
|
|
58
|
+
"""Retry wrapper around the single-shot XML precondition validation.
|
|
59
|
+
|
|
60
|
+
A short retry loop accommodates screen transitions and loading delays.
|
|
61
|
+
``node`` is the ValidatorNode; the single-shot check is dispatched through
|
|
62
|
+
it so tests can patch the method.
|
|
63
|
+
"""
|
|
64
|
+
max_pre_attempts = 3
|
|
65
|
+
pre_retry_delay = 0.4
|
|
66
|
+
|
|
67
|
+
passed = False
|
|
68
|
+
category = ValidationErrorCategory.NONE
|
|
69
|
+
reason = ""
|
|
70
|
+
|
|
71
|
+
for pre_attempt in range(1, max_pre_attempts + 1):
|
|
72
|
+
passed, category, reason = await node._validate_action_precondition_single(
|
|
73
|
+
session, action_item, state
|
|
74
|
+
)
|
|
75
|
+
if passed:
|
|
76
|
+
return True, category, reason
|
|
77
|
+
|
|
78
|
+
if category == ValidationErrorCategory.XML_BYPASSED:
|
|
79
|
+
# Do not retry on XML bypass/timeout; return immediately
|
|
80
|
+
# so it falls back to Pixel-based VLM validation
|
|
81
|
+
return False, category, reason
|
|
82
|
+
|
|
83
|
+
if pre_attempt < max_pre_attempts:
|
|
84
|
+
logger.info(
|
|
85
|
+
"Pre-execution validation failed on attempt"
|
|
86
|
+
f" {pre_attempt}/{max_pre_attempts}: {reason}. Retrying in"
|
|
87
|
+
f" {pre_retry_delay}s..."
|
|
88
|
+
)
|
|
89
|
+
await asyncio.sleep(pre_retry_delay)
|
|
90
|
+
else:
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
return passed, category, reason
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _resolve_screen_dims(ctx: ArtemisContext, state: State | None) -> tuple[int, int]:
|
|
97
|
+
"""Resolves the screen dimensions the Operator's coordinates refer to."""
|
|
98
|
+
width = 1080
|
|
99
|
+
height = 2400
|
|
100
|
+
operator_raw_data = getattr(state, "operator_raw_data", {}) or {}
|
|
101
|
+
w_raw = operator_raw_data.get("width")
|
|
102
|
+
h_raw = operator_raw_data.get("height")
|
|
103
|
+
if isinstance(w_raw, int) and isinstance(h_raw, int):
|
|
104
|
+
width = w_raw
|
|
105
|
+
height = h_raw
|
|
106
|
+
else:
|
|
107
|
+
device = getattr(ctx, "device", None)
|
|
108
|
+
if device:
|
|
109
|
+
width = getattr(device, "device_width", 1080)
|
|
110
|
+
height = getattr(device, "device_height", 2400)
|
|
111
|
+
return width, height
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def _fetch_live_elements(session):
|
|
115
|
+
"""Pulls the live XML tree with a strict timeout.
|
|
116
|
+
|
|
117
|
+
Returns ``(elements, None)`` on success, or ``(None, (False, category,
|
|
118
|
+
reason))`` when the check should be bypassed to Pixel-based validation.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
try:
|
|
122
|
+
# The timeout must ride inside the session (MCP read timeout), never an
|
|
123
|
+
# outer asyncio.wait_for: cancelling call_tool mid-flight corrupts the
|
|
124
|
+
# in-memory transport and bricks the session for all later callers.
|
|
125
|
+
elements = await session.ui_hierarchy(timeout=VALIDATOR_UI_HIERARCHY_TIMEOUT)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.warning(
|
|
128
|
+
f"Failed to get live XML via MCP: {e!r}. Falling back to Pixel-based validation."
|
|
129
|
+
)
|
|
130
|
+
return None, (
|
|
131
|
+
False,
|
|
132
|
+
ValidationErrorCategory.XML_BYPASSED,
|
|
133
|
+
f"XML hierarchy fetch timed out or errored: {e!r}",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if not elements or not isinstance(elements, list):
|
|
137
|
+
logger.warning(
|
|
138
|
+
"Live hierarchy empty or invalid. Falling back to Pixel-based validation."
|
|
139
|
+
)
|
|
140
|
+
return None, (
|
|
141
|
+
False,
|
|
142
|
+
ValidationErrorCategory.XML_BYPASSED,
|
|
143
|
+
"Live hierarchy empty or invalid.",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(
|
|
148
|
+
f"Unexpected error during live XML fetch: {e}. Falling back to Pixel-based validation."
|
|
149
|
+
)
|
|
150
|
+
return None, (
|
|
151
|
+
False,
|
|
152
|
+
ValidationErrorCategory.XML_BYPASSED,
|
|
153
|
+
f"Unexpected error during live XML fetch: {e}",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return elements, None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _normalize_text(s: str | None) -> str:
|
|
160
|
+
if not s:
|
|
161
|
+
return ""
|
|
162
|
+
s = s.lower()
|
|
163
|
+
s = re.sub(r"\(\d+\+?\)", "", s)
|
|
164
|
+
s = re.sub(r"\[\d+\+?\]", "", s)
|
|
165
|
+
s = re.sub(r"[^\w\s\d]", "", s)
|
|
166
|
+
return s.strip()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _aggregate_text(elem: dict) -> str:
|
|
170
|
+
"""Recursively aggregates text from a candidate container node."""
|
|
171
|
+
text = elem.get("text") or elem.get("content-desc") or elem.get("accessibilityText") or ""
|
|
172
|
+
if not isinstance(text, str):
|
|
173
|
+
text = str(text)
|
|
174
|
+
children = elem.get("children") or []
|
|
175
|
+
if children:
|
|
176
|
+
for child in children:
|
|
177
|
+
if isinstance(child, dict):
|
|
178
|
+
text += " " + _aggregate_text(child)
|
|
179
|
+
return text
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _text_similarity(norm_target_text: str, elem_norm_text: str) -> float:
|
|
183
|
+
"""Scores text similarity between normalized target and element text."""
|
|
184
|
+
if not (norm_target_text and elem_norm_text):
|
|
185
|
+
return 0.0
|
|
186
|
+
if norm_target_text == elem_norm_text:
|
|
187
|
+
return 1.0
|
|
188
|
+
if norm_target_text in elem_norm_text or elem_norm_text in norm_target_text:
|
|
189
|
+
return 0.8
|
|
190
|
+
return difflib.SequenceMatcher(None, norm_target_text, elem_norm_text).ratio()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _bounds_match(target_bounds, elem_bounds) -> tuple[float, bool]:
|
|
194
|
+
"""Returns (IoU bounds score, size-mismatch flag) for the bounds pair."""
|
|
195
|
+
bounds_score = 0.0
|
|
196
|
+
size_mismatch = False
|
|
197
|
+
if target_bounds and elem_bounds and len(target_bounds) == 4 and len(elem_bounds) == 4:
|
|
198
|
+
l1, t1, r1, b1 = target_bounds
|
|
199
|
+
l2, t2, r2, b2 = elem_bounds
|
|
200
|
+
|
|
201
|
+
# Check size ratio mismatch to prevent matching small widget to giant container
|
|
202
|
+
w1, h1 = r1 - l1, b1 - t1
|
|
203
|
+
w2, h2 = r2 - l2, b2 - t2
|
|
204
|
+
if w1 > 0 and h1 > 0 and w2 > 0 and h2 > 0:
|
|
205
|
+
size_ratio_w = max(w1, w2) / min(w1, w2)
|
|
206
|
+
size_ratio_h = max(h1, h2) / min(h1, h2)
|
|
207
|
+
if size_ratio_w > 2.5 or size_ratio_h > 2.5:
|
|
208
|
+
size_mismatch = True
|
|
209
|
+
|
|
210
|
+
li = max(l1, l2)
|
|
211
|
+
ti = max(t1, t2)
|
|
212
|
+
ri = min(r1, r2)
|
|
213
|
+
bi = min(b1, b2)
|
|
214
|
+
if ri > li and bi > ti:
|
|
215
|
+
inter_area = (ri - li) * (bi - ti)
|
|
216
|
+
union_area = (r1 - l1) * (b1 - t1) + (r2 - l2) * (b2 - t2) - inter_area
|
|
217
|
+
if union_area > 0:
|
|
218
|
+
bounds_score = inter_area / union_area
|
|
219
|
+
|
|
220
|
+
return bounds_score, size_mismatch
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _combine_signals(
|
|
224
|
+
*,
|
|
225
|
+
target_resource_id,
|
|
226
|
+
target_text,
|
|
227
|
+
target_bounds,
|
|
228
|
+
original_coords,
|
|
229
|
+
id_match: bool,
|
|
230
|
+
elem_res_id: str,
|
|
231
|
+
norm_target_text: str,
|
|
232
|
+
elem_norm_text: str,
|
|
233
|
+
text_score: float,
|
|
234
|
+
bounds_score: float,
|
|
235
|
+
contains_coord: bool,
|
|
236
|
+
) -> tuple[float, float]:
|
|
237
|
+
"""Combines the weighted match signals into (score, identity_score)."""
|
|
238
|
+
signals = []
|
|
239
|
+
weights = []
|
|
240
|
+
identity_signals = []
|
|
241
|
+
identity_weights = []
|
|
242
|
+
|
|
243
|
+
# 1. Resource ID Signal
|
|
244
|
+
if target_resource_id:
|
|
245
|
+
weights.append(W_ID)
|
|
246
|
+
identity_weights.append(W_ID)
|
|
247
|
+
if id_match:
|
|
248
|
+
signals.append(W_ID * 1.0)
|
|
249
|
+
identity_signals.append(W_ID * 1.0)
|
|
250
|
+
elif not elem_res_id:
|
|
251
|
+
signals.append(W_ID * 0.3) # Soft penalty for missing ID
|
|
252
|
+
identity_signals.append(W_ID * 0.3)
|
|
253
|
+
else:
|
|
254
|
+
signals.append(W_ID * -0.5) # Hard penalty for mismatched ID
|
|
255
|
+
identity_signals.append(W_ID * -0.5)
|
|
256
|
+
|
|
257
|
+
# 2. Text Signal
|
|
258
|
+
if target_text:
|
|
259
|
+
weights.append(W_TEXT)
|
|
260
|
+
identity_weights.append(W_TEXT)
|
|
261
|
+
if norm_target_text and elem_norm_text:
|
|
262
|
+
signals.append(W_TEXT * text_score)
|
|
263
|
+
identity_signals.append(W_TEXT * text_score)
|
|
264
|
+
elif not elem_norm_text:
|
|
265
|
+
signals.append(W_TEXT * 0.0) # Penalty for missing expected text
|
|
266
|
+
identity_signals.append(W_TEXT * 0.0)
|
|
267
|
+
|
|
268
|
+
# 3. Bounds Signal
|
|
269
|
+
if target_bounds:
|
|
270
|
+
weights.append(W_BOUNDS)
|
|
271
|
+
signals.append(W_BOUNDS * bounds_score)
|
|
272
|
+
|
|
273
|
+
# 4. Coordinate Containment Signal
|
|
274
|
+
if original_coords:
|
|
275
|
+
weights.append(W_COORD)
|
|
276
|
+
signals.append(W_COORD * (1.0 if contains_coord else 0.0))
|
|
277
|
+
|
|
278
|
+
score = sum(signals) / sum(weights) if weights else 1.0
|
|
279
|
+
identity_score = sum(identity_signals) / sum(identity_weights) if identity_weights else 1.0
|
|
280
|
+
return score, identity_score
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _score_element(
|
|
284
|
+
elem: dict,
|
|
285
|
+
action_item: dict,
|
|
286
|
+
*,
|
|
287
|
+
target_text,
|
|
288
|
+
target_bounds,
|
|
289
|
+
target_resource_id,
|
|
290
|
+
norm_target_text: str,
|
|
291
|
+
) -> dict | None:
|
|
292
|
+
"""Scores one hierarchy element against the action target metadata.
|
|
293
|
+
|
|
294
|
+
Returns a candidate dict, or None if the element has no usable bounds.
|
|
295
|
+
"""
|
|
296
|
+
elem_bounds_str = elem.get("bounds")
|
|
297
|
+
elem_bounds = visualization.parse_bounds(elem_bounds_str)
|
|
298
|
+
if not elem_bounds:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
elem_raw_text = _aggregate_text(elem)
|
|
302
|
+
elem_norm_text = _normalize_text(elem_raw_text)
|
|
303
|
+
elem_res_id = elem.get("resource-id") or elem.get("resourceId") or ""
|
|
304
|
+
|
|
305
|
+
elem_cx, elem_cy = visualization.get_center_coordinates(*elem_bounds)
|
|
306
|
+
|
|
307
|
+
original_coords = action_item.get("coordinates") or [0, 0]
|
|
308
|
+
dist = math.sqrt((elem_cx - original_coords[0]) ** 2 + (elem_cy - original_coords[1]) ** 2)
|
|
309
|
+
|
|
310
|
+
id_match = target_resource_id and elem_res_id and target_resource_id == elem_res_id
|
|
311
|
+
|
|
312
|
+
text_score = _text_similarity(norm_target_text, elem_norm_text)
|
|
313
|
+
bounds_score, size_mismatch = _bounds_match(target_bounds, elem_bounds)
|
|
314
|
+
|
|
315
|
+
# Calculate Coordinate Containment
|
|
316
|
+
contains_coord = False
|
|
317
|
+
if original_coords and len(original_coords) == 2 and elem_bounds and len(elem_bounds) == 4:
|
|
318
|
+
cx, cy = original_coords
|
|
319
|
+
l2, t2, r2, b2 = elem_bounds
|
|
320
|
+
contains_coord = l2 <= cx <= r2 and t2 <= cy <= b2
|
|
321
|
+
|
|
322
|
+
# Apply size mismatch penalty
|
|
323
|
+
if size_mismatch and not id_match:
|
|
324
|
+
text_score = 0.0
|
|
325
|
+
bounds_score = 0.0
|
|
326
|
+
contains_coord = False
|
|
327
|
+
|
|
328
|
+
score, identity_score = _combine_signals(
|
|
329
|
+
target_resource_id=target_resource_id,
|
|
330
|
+
target_text=target_text,
|
|
331
|
+
target_bounds=target_bounds,
|
|
332
|
+
original_coords=original_coords,
|
|
333
|
+
id_match=id_match,
|
|
334
|
+
elem_res_id=elem_res_id,
|
|
335
|
+
norm_target_text=norm_target_text,
|
|
336
|
+
elem_norm_text=elem_norm_text,
|
|
337
|
+
text_score=text_score,
|
|
338
|
+
bounds_score=bounds_score,
|
|
339
|
+
contains_coord=contains_coord,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
decay = max(0.5, 1.0 - (dist / 800.0))
|
|
343
|
+
score *= decay
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
"element": elem,
|
|
347
|
+
"center": [elem_cx, elem_cy],
|
|
348
|
+
"bounds": elem_bounds,
|
|
349
|
+
"text": elem_raw_text,
|
|
350
|
+
"resource_id": elem_res_id,
|
|
351
|
+
"score": score,
|
|
352
|
+
"identity_score": identity_score,
|
|
353
|
+
"distance": dist,
|
|
354
|
+
"text_score": text_score,
|
|
355
|
+
"id_match": id_match,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _handle_match_success(
|
|
360
|
+
action_item: dict, best: dict, target_text, scale_factor: float
|
|
361
|
+
) -> tuple[bool, ValidationErrorCategory, str]:
|
|
362
|
+
"""Handles a passing match: self-heals coordinates for moderate drifts."""
|
|
363
|
+
if best["distance"] <= 200 * scale_factor:
|
|
364
|
+
new_coords = best["center"]
|
|
365
|
+
old_coords = action_item.get("coordinates")
|
|
366
|
+
if old_coords != new_coords:
|
|
367
|
+
logger.success(
|
|
368
|
+
"Pre-execution validation SUCCESS"
|
|
369
|
+
f" (score={best['score']:.2f}). Target element"
|
|
370
|
+
f" '{target_text}' shifted by {best['distance']:.1f}px."
|
|
371
|
+
f" Self-healing: correcting coordinates {old_coords} ->"
|
|
372
|
+
f" {new_coords}."
|
|
373
|
+
)
|
|
374
|
+
action_item["coordinates"] = new_coords
|
|
375
|
+
# Drop the now-stale normalized coordinates so the canonical
|
|
376
|
+
# translation re-derives them from the healed pixel center.
|
|
377
|
+
action_item.pop("normalized_coordinates", None)
|
|
378
|
+
else:
|
|
379
|
+
logger.info(
|
|
380
|
+
"Pre-execution validation SUCCESS"
|
|
381
|
+
f" (score={best['score']:.2f}). Target element matched but"
|
|
382
|
+
f" shifted by {best['distance']:.1f}px"
|
|
383
|
+
f" (>{200 * scale_factor:.1f}px). Bypassing coordinate"
|
|
384
|
+
" self-healing to remain conservative."
|
|
385
|
+
)
|
|
386
|
+
return True, ValidationErrorCategory.NONE, ""
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _is_valid_shift(best: dict, scale_factor: float) -> bool:
|
|
390
|
+
"""Applies the strict shift criteria (preferring disappeared over shifted).
|
|
391
|
+
|
|
392
|
+
We must be conservative to avoid confusing disappearance as shift. Shift
|
|
393
|
+
misclassification as disappeared is acceptable, but disappeared as shift
|
|
394
|
+
causes errors.
|
|
395
|
+
"""
|
|
396
|
+
max_allowed_distance = 100.0 * scale_factor
|
|
397
|
+
if best.get("id_match", False):
|
|
398
|
+
# Allow larger distance threshold only for strong resource ID matches
|
|
399
|
+
max_allowed_distance = 300.0 * scale_factor
|
|
400
|
+
|
|
401
|
+
if best["distance"] <= max_allowed_distance:
|
|
402
|
+
if best["identity_score"] >= 0.85:
|
|
403
|
+
# High confidence identity match (e.g. exact text or exact ID)
|
|
404
|
+
return True
|
|
405
|
+
if best["identity_score"] >= 0.5 and best.get("id_match", False):
|
|
406
|
+
# Moderate confidence with exact resource ID match
|
|
407
|
+
return True
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _element_is_interactive(e: dict) -> bool:
|
|
412
|
+
clickable = e.get("clickable")
|
|
413
|
+
long_clickable = e.get("long-clickable") or e.get("longClickable")
|
|
414
|
+
focusable = e.get("focusable")
|
|
415
|
+
enabled = e.get("enabled")
|
|
416
|
+
|
|
417
|
+
def to_bool(v) -> bool:
|
|
418
|
+
if v is None:
|
|
419
|
+
return False
|
|
420
|
+
if isinstance(v, bool):
|
|
421
|
+
return v
|
|
422
|
+
return str(v).lower() == "true"
|
|
423
|
+
|
|
424
|
+
return to_bool(clickable) or to_bool(long_clickable) or to_bool(focusable) or to_bool(enabled)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _find_occupant(elements: list, original_coords) -> tuple | None:
|
|
428
|
+
"""Finds the most specific element containing the expected coordinates."""
|
|
429
|
+
occupant = None
|
|
430
|
+
if original_coords and len(original_coords) == 2:
|
|
431
|
+
orig_cx, orig_cy = original_coords
|
|
432
|
+
for elem in elements:
|
|
433
|
+
if not isinstance(elem, dict):
|
|
434
|
+
continue
|
|
435
|
+
elem_bounds_str = elem.get("bounds")
|
|
436
|
+
elem_bounds = visualization.parse_bounds(elem_bounds_str)
|
|
437
|
+
if elem_bounds:
|
|
438
|
+
left, top, right, bottom = elem_bounds
|
|
439
|
+
if left <= orig_cx <= right and top <= orig_cy <= bottom:
|
|
440
|
+
elem_area = (right - left) * (bottom - top)
|
|
441
|
+
if not occupant or elem_area < occupant[2]:
|
|
442
|
+
occupant = (elem, elem_bounds, elem_area)
|
|
443
|
+
return occupant
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _coords_hit_interactive_element(
|
|
447
|
+
elements: list, original_coords, width: int, height: int
|
|
448
|
+
) -> bool:
|
|
449
|
+
"""True if any non-full-screen interactive element covers the coordinates."""
|
|
450
|
+
if not (original_coords and len(original_coords) == 2):
|
|
451
|
+
return False
|
|
452
|
+
orig_cx, orig_cy = original_coords
|
|
453
|
+
for e in elements:
|
|
454
|
+
if not isinstance(e, dict):
|
|
455
|
+
continue
|
|
456
|
+
eb_str = e.get("bounds")
|
|
457
|
+
eb = visualization.parse_bounds(eb_str)
|
|
458
|
+
if eb:
|
|
459
|
+
e_left, e_top, e_right, e_bottom = eb
|
|
460
|
+
if e_left <= orig_cx <= e_right and e_top <= orig_cy <= e_bottom:
|
|
461
|
+
# Exclude full-screen root background elements
|
|
462
|
+
e_width = e_right - e_left
|
|
463
|
+
e_height = e_bottom - e_top
|
|
464
|
+
is_full_screen = e_width >= width - 10 and e_height >= height - 10
|
|
465
|
+
|
|
466
|
+
if _element_is_interactive(e) and not is_full_screen:
|
|
467
|
+
return True
|
|
468
|
+
return False
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _describe_occupation(
|
|
472
|
+
elements: list,
|
|
473
|
+
occupant: tuple | None,
|
|
474
|
+
original_coords,
|
|
475
|
+
target_bounds,
|
|
476
|
+
width: int,
|
|
477
|
+
height: int,
|
|
478
|
+
) -> tuple[bool, str, list | None]:
|
|
479
|
+
"""Decides whether the expected coordinates are occupied by another element.
|
|
480
|
+
|
|
481
|
+
Returns (is_occupied, occupant_desc, occupant_bounds).
|
|
482
|
+
"""
|
|
483
|
+
is_occupied = False
|
|
484
|
+
occupant_desc = "anonymous element"
|
|
485
|
+
occupant_bounds = None
|
|
486
|
+
|
|
487
|
+
if occupant:
|
|
488
|
+
elem, elem_bounds, _ = occupant
|
|
489
|
+
occupant_text = (
|
|
490
|
+
elem.get("text") or elem.get("content-desc") or elem.get("accessibilityText") or ""
|
|
491
|
+
)
|
|
492
|
+
occupant_res_id = elem.get("resource-id") or elem.get("resourceId") or ""
|
|
493
|
+
occupant_bounds = elem_bounds
|
|
494
|
+
|
|
495
|
+
# Check if the occupant has content
|
|
496
|
+
has_content = bool(occupant_text or occupant_res_id)
|
|
497
|
+
|
|
498
|
+
# Check if this occupant or any container covering this point is interactive
|
|
499
|
+
coords_interactive = _coords_hit_interactive_element(
|
|
500
|
+
elements, original_coords, width, height
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Exclude full-screen or giant parent containers from being considered blockers
|
|
504
|
+
is_occupant_full_screen = False
|
|
505
|
+
if occupant_bounds:
|
|
506
|
+
o_w = occupant_bounds[2] - occupant_bounds[0]
|
|
507
|
+
o_h = occupant_bounds[3] - occupant_bounds[1]
|
|
508
|
+
is_occupant_full_screen = o_w >= width - 10 and o_h >= height - 10
|
|
509
|
+
|
|
510
|
+
# If target bounds are known, check if occupant is a giant container
|
|
511
|
+
if target_bounds:
|
|
512
|
+
t_w = target_bounds[2] - target_bounds[0]
|
|
513
|
+
t_h = target_bounds[3] - target_bounds[1]
|
|
514
|
+
t_area = t_w * t_h
|
|
515
|
+
o_area = o_w * o_h
|
|
516
|
+
if t_area > 0 and o_area > 3.0 * t_area:
|
|
517
|
+
is_occupant_full_screen = True
|
|
518
|
+
|
|
519
|
+
if (has_content or coords_interactive) and not is_occupant_full_screen:
|
|
520
|
+
is_occupied = True
|
|
521
|
+
occupant_desc = (
|
|
522
|
+
f"'{occupant_text}' ({occupant_res_id})"
|
|
523
|
+
if has_content
|
|
524
|
+
else "interactive anonymous element"
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
return is_occupied, occupant_desc, occupant_bounds
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _classify_failure(
|
|
531
|
+
elements: list,
|
|
532
|
+
action_item: dict,
|
|
533
|
+
best: dict,
|
|
534
|
+
*,
|
|
535
|
+
target_text,
|
|
536
|
+
target_bounds,
|
|
537
|
+
target_resource_id,
|
|
538
|
+
width: int,
|
|
539
|
+
height: int,
|
|
540
|
+
scale_factor: float,
|
|
541
|
+
) -> tuple[bool, ValidationErrorCategory, str]:
|
|
542
|
+
"""Classifies a below-threshold match as shifted / occupied / disappeared."""
|
|
543
|
+
original_coords = action_item.get("coordinates")
|
|
544
|
+
|
|
545
|
+
# 1. Shifted Case
|
|
546
|
+
if _is_valid_shift(best, scale_factor):
|
|
547
|
+
reason = (
|
|
548
|
+
f"Target element '{target_text}' ({target_resource_id})"
|
|
549
|
+
f" expected at {original_coords} (bounds: {target_bounds})"
|
|
550
|
+
f" has shifted.\n- New location: {best['center']} (bounds:"
|
|
551
|
+
f" {best['bounds']})"
|
|
552
|
+
)
|
|
553
|
+
# Structured facts for the execution incident handed to the Operator.
|
|
554
|
+
action_item["safety_net_evidence"] = {
|
|
555
|
+
"new_center": list(best["center"]),
|
|
556
|
+
"new_bounds": best.get("bounds"),
|
|
557
|
+
}
|
|
558
|
+
return False, ValidationErrorCategory.TARGET_SHIFTED, reason
|
|
559
|
+
|
|
560
|
+
# 2. Occupied Case
|
|
561
|
+
occupant = _find_occupant(elements, original_coords)
|
|
562
|
+
is_occupied, occupant_desc, occupant_bounds = _describe_occupation(
|
|
563
|
+
elements, occupant, original_coords, target_bounds, width, height
|
|
564
|
+
)
|
|
565
|
+
if is_occupied:
|
|
566
|
+
reason = (
|
|
567
|
+
f"The expected position {original_coords} for"
|
|
568
|
+
f" '{target_text}' ({target_resource_id}) is"
|
|
569
|
+
" occupied/intercepted by a different element:"
|
|
570
|
+
f" {occupant_desc} at bounds {occupant_bounds}."
|
|
571
|
+
)
|
|
572
|
+
action_item["safety_net_evidence"] = {
|
|
573
|
+
"occupant": occupant_desc,
|
|
574
|
+
"occupant_bounds": occupant_bounds,
|
|
575
|
+
}
|
|
576
|
+
return False, ValidationErrorCategory.TARGET_OCCUPIED, reason
|
|
577
|
+
|
|
578
|
+
# 3. Completely Missing Case
|
|
579
|
+
reason = (
|
|
580
|
+
f"Target element '{target_text}' ({target_resource_id})"
|
|
581
|
+
f" expected at {original_coords} (bounds:"
|
|
582
|
+
f" {target_bounds}) was not found on the screen."
|
|
583
|
+
)
|
|
584
|
+
return False, ValidationErrorCategory.TARGET_DISAPPEARED, reason
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
async def validate_action_precondition_single(
|
|
588
|
+
ctx: ArtemisContext, session, action_item: dict, state: State | None = None
|
|
589
|
+
) -> tuple[bool, ValidationErrorCategory, str]:
|
|
590
|
+
"""Single-shot Safety Net check against the live XML hierarchy.
|
|
591
|
+
|
|
592
|
+
Returns (True, ValidationErrorCategory.NONE, "") if validation passes or
|
|
593
|
+
is skipped. Returns (False, category, error_msg) if validation fails.
|
|
594
|
+
"""
|
|
595
|
+
action_name = action_item.get("action")
|
|
596
|
+
|
|
597
|
+
# 1. White-list filtering: Only validate interactive, element-focused actions
|
|
598
|
+
if action_name not in XML_VALIDATED_ACTIONS:
|
|
599
|
+
return True, ValidationErrorCategory.NONE, ""
|
|
600
|
+
|
|
601
|
+
target_text = action_item.get("target_text")
|
|
602
|
+
target_bounds = action_item.get("target_bounds") # [l, t, r, b]
|
|
603
|
+
target_resource_id = action_item.get("target_resource_id")
|
|
604
|
+
|
|
605
|
+
width, height = _resolve_screen_dims(ctx, state)
|
|
606
|
+
|
|
607
|
+
# Calculate diagonal-based scale factor for resolution-independent thresholds
|
|
608
|
+
ref_diagonal = math.sqrt(1080**2 + 2400**2) # ~2631.8
|
|
609
|
+
current_diagonal = math.sqrt(width**2 + height**2)
|
|
610
|
+
scale_factor = current_diagonal / ref_diagonal
|
|
611
|
+
|
|
612
|
+
# 2. Skip validation if there's no target metadata to match against
|
|
613
|
+
if not target_text and not target_bounds and not target_resource_id:
|
|
614
|
+
return True, ValidationErrorCategory.NONE, ""
|
|
615
|
+
|
|
616
|
+
logger.info(
|
|
617
|
+
f"Pre-execution validation: checking target '{target_text}'"
|
|
618
|
+
f" ({target_resource_id}) at {target_bounds}..."
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# 3. Pull live XML tree with strict timeout to avoid hanging the execution flow
|
|
622
|
+
elements, bypass = await _fetch_live_elements(session)
|
|
623
|
+
if bypass is not None:
|
|
624
|
+
return bypass
|
|
625
|
+
|
|
626
|
+
# 4. Matching Heuristics & Scoring
|
|
627
|
+
norm_target_text = _normalize_text(target_text)
|
|
628
|
+
|
|
629
|
+
candidates = []
|
|
630
|
+
for elem in elements:
|
|
631
|
+
if not isinstance(elem, dict):
|
|
632
|
+
continue
|
|
633
|
+
candidate = _score_element(
|
|
634
|
+
elem,
|
|
635
|
+
action_item,
|
|
636
|
+
target_text=target_text,
|
|
637
|
+
target_bounds=target_bounds,
|
|
638
|
+
target_resource_id=target_resource_id,
|
|
639
|
+
norm_target_text=norm_target_text,
|
|
640
|
+
)
|
|
641
|
+
if candidate is not None:
|
|
642
|
+
candidates.append(candidate)
|
|
643
|
+
|
|
644
|
+
if not candidates:
|
|
645
|
+
logger.warning(
|
|
646
|
+
"No candidates with valid bounds found. Falling back to Pixel-based validation."
|
|
647
|
+
)
|
|
648
|
+
return (
|
|
649
|
+
False,
|
|
650
|
+
ValidationErrorCategory.XML_BYPASSED,
|
|
651
|
+
"No candidates with valid bounds found.",
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
candidates.sort(key=lambda x: x["score"], reverse=True)
|
|
655
|
+
best = candidates[0]
|
|
656
|
+
|
|
657
|
+
threshold = 0.55 if best["distance"] <= 150 * scale_factor else 0.75
|
|
658
|
+
|
|
659
|
+
if best["score"] >= threshold:
|
|
660
|
+
return _handle_match_success(action_item, best, target_text, scale_factor)
|
|
661
|
+
|
|
662
|
+
return _classify_failure(
|
|
663
|
+
elements,
|
|
664
|
+
action_item,
|
|
665
|
+
best,
|
|
666
|
+
target_text=target_text,
|
|
667
|
+
target_bounds=target_bounds,
|
|
668
|
+
target_resource_id=target_resource_id,
|
|
669
|
+
width=width,
|
|
670
|
+
height=height,
|
|
671
|
+
scale_factor=scale_factor,
|
|
672
|
+
)
|