android-midscene-automation 0.2.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +159 -29
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +340 -121
- package/src/types.ts +1 -1
|
@@ -0,0 +1,403 @@
|
|
|
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
|
+
"""Setup phases of ``Explorer.run``.
|
|
16
|
+
|
|
17
|
+
Split out of ``artemis.agents.explorer.explorer``: the flash-tier one-shot
|
|
18
|
+
detection path, the per-run screen loading shared by every tier (image record
|
|
19
|
+
resolution, screen dimensions, fused hierarchy, in-memory screen index) and
|
|
20
|
+
the named preparation phases executed before a reasoning loop (image pool,
|
|
21
|
+
model parameters, tier-aware prompt construction, initial screenshot
|
|
22
|
+
annotation), packaged as a mixin consumed by ``Explorer``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import base64
|
|
26
|
+
import glob
|
|
27
|
+
import hashlib
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
import re
|
|
32
|
+
from typing import TYPE_CHECKING, Any
|
|
33
|
+
|
|
34
|
+
import httpx
|
|
35
|
+
|
|
36
|
+
from artemis.agents.explorer.constants import (
|
|
37
|
+
DEFAULT_EXPLORER_MODEL,
|
|
38
|
+
EXECUTION_CONSTRAINT_TEMPLATE,
|
|
39
|
+
)
|
|
40
|
+
from artemis.agents.explorer.geometry import resolve_screen_size
|
|
41
|
+
from artemis.agents.explorer.perception_tools import load_detector_templates
|
|
42
|
+
from artemis.agents.explorer.screen_index import ScreenElement, ScreenIndex
|
|
43
|
+
from artemis.agents.explorer.tiers import SUBMIT_TOOL, ExplorerTier
|
|
44
|
+
from artemis.agents.object_detector.object_detector import _run_object_detection
|
|
45
|
+
from artemis.config import settings
|
|
46
|
+
from artemis.data_engine.storage import StorageManager
|
|
47
|
+
from artemis.graph.state import State
|
|
48
|
+
from artemis.utils.ocr_api import is_ocr_configured, perform_ocr
|
|
49
|
+
from artemis.utils.ocr_xml_fusion import fuse_ocr_with_xml
|
|
50
|
+
from artemis.utils.visualization import draw_dots, format_minimal_list_with_elements
|
|
51
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
52
|
+
|
|
53
|
+
logger = get_logger(__name__)
|
|
54
|
+
|
|
55
|
+
#: Prompt bullets may be plain strings or ``{"text": ..., "requires": [...]}``
|
|
56
|
+
#: objects; the latter are emitted only when every listed tool is exposed.
|
|
57
|
+
PromptBullet = str | dict[str, Any]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _bullet_text(bullet: PromptBullet, exposed_tools: set[str]) -> str | None:
|
|
61
|
+
"""Returns the bullet text, or None when its tool requirements are unmet."""
|
|
62
|
+
if isinstance(bullet, dict):
|
|
63
|
+
required = bullet.get("requires") or []
|
|
64
|
+
if not all(tool in exposed_tools for tool in required):
|
|
65
|
+
return None
|
|
66
|
+
return str(bullet.get("text", ""))
|
|
67
|
+
return str(bullet)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_prompt_sections(prompt_path: Path) -> dict[str, Any]:
|
|
71
|
+
"""Loads ``explorer.json`` tolerating ``//`` and ``/* */`` comments and trailing commas."""
|
|
72
|
+
content = prompt_path.read_text(encoding="utf-8")
|
|
73
|
+
content = re.sub(r"(?<!:)\/\/.*", "", content)
|
|
74
|
+
content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
|
|
75
|
+
content = re.sub(r",\s*([\]}])", r"\1", content)
|
|
76
|
+
return json.loads(content)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class RunSetupMixin:
|
|
80
|
+
"""Flash mode and pre-loop setup phases of :class:`Explorer`."""
|
|
81
|
+
|
|
82
|
+
if TYPE_CHECKING:
|
|
83
|
+
from artemis.context import ArtemisContext
|
|
84
|
+
|
|
85
|
+
DENYLIST_TEMPLATE: str
|
|
86
|
+
ctx: ArtemisContext
|
|
87
|
+
width: int
|
|
88
|
+
height: int
|
|
89
|
+
image_name: str | None
|
|
90
|
+
screenshot_path: str | None
|
|
91
|
+
image_pool: dict[str, dict[str, Any]]
|
|
92
|
+
next_img_id: int
|
|
93
|
+
global_label_idx: int
|
|
94
|
+
http_client: httpx.AsyncClient | None
|
|
95
|
+
denylisted_tools: set[str]
|
|
96
|
+
_user_denylist: frozenset[str]
|
|
97
|
+
screen_index: ScreenIndex
|
|
98
|
+
label_registry: dict[str, ScreenElement]
|
|
99
|
+
|
|
100
|
+
def _hidden_tool_names(self) -> set[str]: ...
|
|
101
|
+
|
|
102
|
+
def _register_label(self, label: str, element: ScreenElement) -> None: ...
|
|
103
|
+
|
|
104
|
+
def _registry_element(self, entry: dict[str, Any]) -> ScreenElement: ...
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _failure_outcome(message: str) -> str:
|
|
108
|
+
"""The contract-shaped answer for a run that produced no candidates."""
|
|
109
|
+
return json.dumps({"candidates": [], "fallback_message": message}, ensure_ascii=False)
|
|
110
|
+
|
|
111
|
+
async def _run_flash(self, query: str, screenshot_path: str) -> str:
|
|
112
|
+
"""Flash tier: one-shot object detection without a reasoning loop.
|
|
113
|
+
|
|
114
|
+
Several targets may be listed in ``query`` separated by ``|``; each
|
|
115
|
+
detection becomes a ``D<n>`` candidate described by its detected label.
|
|
116
|
+
"""
|
|
117
|
+
templates, global_timeout = load_detector_templates()
|
|
118
|
+
|
|
119
|
+
queries = [q.strip() for q in query.split("|") if q.strip()]
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
result = await _run_object_detection(
|
|
123
|
+
self.ctx,
|
|
124
|
+
screenshot_path,
|
|
125
|
+
queries,
|
|
126
|
+
templates,
|
|
127
|
+
global_timeout=global_timeout,
|
|
128
|
+
)
|
|
129
|
+
detected_items = result.get("detected", [])
|
|
130
|
+
candidates = []
|
|
131
|
+
for idx, item in enumerate(detected_items):
|
|
132
|
+
pos = item.get("point")
|
|
133
|
+
if pos and isinstance(pos, list) and len(pos) == 2:
|
|
134
|
+
candidates.append(
|
|
135
|
+
{
|
|
136
|
+
"label": f"D{idx + 1}",
|
|
137
|
+
"coords": pos,
|
|
138
|
+
"description": item.get("label", query),
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
fallback_message = "" if candidates else f"Failed to detect: {query}"
|
|
142
|
+
return json.dumps(
|
|
143
|
+
{"candidates": candidates, "fallback_message": fallback_message},
|
|
144
|
+
ensure_ascii=False,
|
|
145
|
+
)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Flash mode object detection failed: {e}")
|
|
148
|
+
return self._failure_outcome(f"Flash mode detection error: {e}")
|
|
149
|
+
|
|
150
|
+
def _resolve_image_record(self, screenshot_path: str) -> tuple:
|
|
151
|
+
"""Computes the screenshot hash and looks it up in the Data Engine DB."""
|
|
152
|
+
image_name = None
|
|
153
|
+
record = None
|
|
154
|
+
try:
|
|
155
|
+
sha256_hash = hashlib.sha256()
|
|
156
|
+
with open(screenshot_path, "rb") as f:
|
|
157
|
+
for byte_block in iter(lambda: f.read(4096), b""):
|
|
158
|
+
sha256_hash.update(byte_block)
|
|
159
|
+
computed_hash = sha256_hash.hexdigest()
|
|
160
|
+
logger.info(f"Computed screenshot hash: {computed_hash}")
|
|
161
|
+
|
|
162
|
+
# Check if it exists in DB
|
|
163
|
+
db_path = settings.DATA_ENGINE_DB_PATH
|
|
164
|
+
base_dir = settings.TRACES_PATH
|
|
165
|
+
storage = StorageManager(db_path, base_dir)
|
|
166
|
+
record = storage.get_image(computed_hash)
|
|
167
|
+
|
|
168
|
+
if record:
|
|
169
|
+
image_name = computed_hash
|
|
170
|
+
else:
|
|
171
|
+
logger.warning(
|
|
172
|
+
f"Image hash {computed_hash} not found in Data Engine DB."
|
|
173
|
+
" Data Engine might not be synced yet."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.warning(f"Failed to compute hash or check DB: {e}")
|
|
178
|
+
|
|
179
|
+
return image_name, record
|
|
180
|
+
|
|
181
|
+
def _resolve_screen_dimensions(self, state: State) -> None:
|
|
182
|
+
"""Adopts the screenshot's pixel size (operator observation, device, fallback)."""
|
|
183
|
+
self.width, self.height = resolve_screen_size(self.ctx, state)
|
|
184
|
+
|
|
185
|
+
async def _load_screen(
|
|
186
|
+
self, screenshot_path: str, state: State, tier: ExplorerTier
|
|
187
|
+
) -> list[dict[str, Any]]:
|
|
188
|
+
"""Loads the screen once per run: record, screen size, fused hierarchy, index.
|
|
189
|
+
|
|
190
|
+
Every tier goes through here so the structural pre-pass and the
|
|
191
|
+
perception tools share one in-memory snapshot instead of asking the
|
|
192
|
+
Data Engine per call. On-the-fly OCR is reserved for loop tiers:
|
|
193
|
+
flash never exposes text tools and must stay a single round trip.
|
|
194
|
+
"""
|
|
195
|
+
image_name, record = self._resolve_image_record(screenshot_path)
|
|
196
|
+
self.image_name = image_name
|
|
197
|
+
self._resolve_screen_dimensions(state)
|
|
198
|
+
fused_xml = await self._build_fused_hierarchy(
|
|
199
|
+
record, state, screenshot_path, allow_ocr=not tier.is_oneshot
|
|
200
|
+
)
|
|
201
|
+
self.screen_index = ScreenIndex.from_hierarchy(fused_xml, self.width, self.height)
|
|
202
|
+
return fused_xml
|
|
203
|
+
|
|
204
|
+
def _init_image_pool(self, screenshot_path: str) -> None:
|
|
205
|
+
"""Seeds the Image Pool with the original screenshot as img_0."""
|
|
206
|
+
self.image_pool = {
|
|
207
|
+
"img_0": {
|
|
208
|
+
"path": screenshot_path,
|
|
209
|
+
"transform": {
|
|
210
|
+
"offset_x": 0.0,
|
|
211
|
+
"offset_y": 0.0,
|
|
212
|
+
"scale_x": 1.0,
|
|
213
|
+
"scale_y": 1.0,
|
|
214
|
+
},
|
|
215
|
+
"description": "Original complete screenshot",
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
self.next_img_id = 1
|
|
219
|
+
|
|
220
|
+
def _resolve_model_params(self) -> tuple:
|
|
221
|
+
"""Resolves model name, temperature, thinking level, and fallback model."""
|
|
222
|
+
llm_config = getattr(self.ctx, "llm_config", None)
|
|
223
|
+
llm_cfg = getattr(llm_config, "explorer", None) if llm_config else None
|
|
224
|
+
temperature = 0.1
|
|
225
|
+
fallback_model = None
|
|
226
|
+
thinking_level = None
|
|
227
|
+
if llm_cfg:
|
|
228
|
+
model_name = llm_cfg.model
|
|
229
|
+
if "/" in model_name:
|
|
230
|
+
model_name = model_name.split("/")[-1]
|
|
231
|
+
if getattr(llm_cfg, "temperature", None) is not None:
|
|
232
|
+
temperature = llm_cfg.temperature
|
|
233
|
+
if getattr(llm_cfg, "thinking_level", None) is not None:
|
|
234
|
+
thinking_level = llm_cfg.thinking_level
|
|
235
|
+
if getattr(llm_cfg, "fallback", None):
|
|
236
|
+
fallback_model = llm_cfg.fallback.model
|
|
237
|
+
if "/" in fallback_model:
|
|
238
|
+
fallback_model = fallback_model.split("/")[-1]
|
|
239
|
+
else:
|
|
240
|
+
model_name = DEFAULT_EXPLORER_MODEL
|
|
241
|
+
|
|
242
|
+
return model_name, temperature, thinking_level, fallback_model
|
|
243
|
+
|
|
244
|
+
def _exposed_tool_names(self, tier: ExplorerTier) -> set[str]:
|
|
245
|
+
"""Tools the model can actually call in this run (tier minus hidden)."""
|
|
246
|
+
return (set(tier.tools) | {SUBMIT_TOOL}) - self._hidden_tool_names()
|
|
247
|
+
|
|
248
|
+
def _build_prompt_template(self, tier: ExplorerTier) -> tuple[str | None, str | None]:
|
|
249
|
+
"""Builds the tier-aware system prompt; returns ``(prompt, error_message)``.
|
|
250
|
+
|
|
251
|
+
Bullets whose ``requires`` list names a tool the model cannot call in
|
|
252
|
+
this tier are dropped, so the prompt never describes capabilities
|
|
253
|
+
that are absent from the tool declarations. Tier-hidden tools are
|
|
254
|
+
simply never mentioned; only the user's own denylist (restricted to
|
|
255
|
+
tools the tier would otherwise expose) is spelled out.
|
|
256
|
+
"""
|
|
257
|
+
prompt_path = Path(__file__).parent / "explorer.json"
|
|
258
|
+
if not prompt_path.exists():
|
|
259
|
+
return None, "Error: Explorer prompt template not found."
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
data = load_prompt_sections(prompt_path)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
return None, f"Error loading or parsing explorer.json: {e}"
|
|
265
|
+
|
|
266
|
+
exposed = self._exposed_tool_names(tier)
|
|
267
|
+
prompt_parts: list[str] = []
|
|
268
|
+
for section, content_val in data.items():
|
|
269
|
+
if isinstance(content_val, list):
|
|
270
|
+
bullets = [
|
|
271
|
+
text
|
|
272
|
+
for text in (_bullet_text(b, exposed) for b in content_val)
|
|
273
|
+
if text is not None
|
|
274
|
+
]
|
|
275
|
+
if not bullets:
|
|
276
|
+
continue
|
|
277
|
+
prompt_parts.append(f"# {section}")
|
|
278
|
+
prompt_parts.extend(f"- {bullet}" for bullet in bullets)
|
|
279
|
+
else:
|
|
280
|
+
text = _bullet_text(content_val, exposed)
|
|
281
|
+
if text is None:
|
|
282
|
+
continue
|
|
283
|
+
prompt_parts.append(f"# {section}")
|
|
284
|
+
prompt_parts.append(text)
|
|
285
|
+
prompt_parts.append("")
|
|
286
|
+
|
|
287
|
+
prompt_template = "\n".join(prompt_parts)
|
|
288
|
+
|
|
289
|
+
user_denied = sorted(set(self._user_denylist) & set(tier.tools))
|
|
290
|
+
if user_denied:
|
|
291
|
+
prompt_template += self.DENYLIST_TEMPLATE.format(tools=", ".join(user_denied))
|
|
292
|
+
|
|
293
|
+
constraint = EXECUTION_CONSTRAINT_TEMPLATE.format(max_turns=tier.max_turns)
|
|
294
|
+
prompt_template += "\n# EXECUTION CONSTRAINT\n- " + constraint + "\n"
|
|
295
|
+
|
|
296
|
+
return prompt_template, None
|
|
297
|
+
|
|
298
|
+
async def _build_fused_hierarchy(
|
|
299
|
+
self, record, state: State, screenshot_path: str, allow_ocr: bool = True
|
|
300
|
+
) -> list[dict[str, Any]]:
|
|
301
|
+
"""Loads (and if needed OCR-fuses) the UI hierarchy for the screenshot.
|
|
302
|
+
|
|
303
|
+
Precedence: the Data Engine record for the screenshot hash, else the
|
|
304
|
+
state's latest hierarchy when the screenshot is the latest one. The
|
|
305
|
+
HTTP client backing on-the-fly OCR is created here on first need and
|
|
306
|
+
released by ``Explorer.run`` regardless of the engine used.
|
|
307
|
+
"""
|
|
308
|
+
fused_xml: Any = []
|
|
309
|
+
if record and getattr(record, "ui_tree", None):
|
|
310
|
+
ui_tree = record.ui_tree
|
|
311
|
+
ocr_results = getattr(record, "ocr_result", None)
|
|
312
|
+
if ocr_results is None:
|
|
313
|
+
if allow_ocr and is_ocr_configured():
|
|
314
|
+
try:
|
|
315
|
+
logger.info("Previous screenshot OCR is missing. Running OCR on-the-fly...")
|
|
316
|
+
with open(screenshot_path, "rb") as img_file:
|
|
317
|
+
img_b64 = base64.b64encode(img_file.read()).decode("utf-8")
|
|
318
|
+
if self.http_client is None:
|
|
319
|
+
self.http_client = httpx.AsyncClient()
|
|
320
|
+
ocr_results = await perform_ocr(img_b64, client=self.http_client)
|
|
321
|
+
except Exception as ocr_err:
|
|
322
|
+
logger.error(f"On-the-fly OCR failed for previous screenshot: {ocr_err}")
|
|
323
|
+
ocr_results = []
|
|
324
|
+
else:
|
|
325
|
+
ocr_results = []
|
|
326
|
+
|
|
327
|
+
fused_xml = fuse_ocr_with_xml(ui_tree, ocr_results or [])
|
|
328
|
+
logger.info("Successfully loaded and fused UI hierarchy for previous screenshot.")
|
|
329
|
+
elif screenshot_path == getattr(state, "latest_screenshot", None):
|
|
330
|
+
fused_xml = getattr(state, "latest_ui_hierarchy", None)
|
|
331
|
+
return list(fused_xml) if isinstance(fused_xml, (list, tuple)) else []
|
|
332
|
+
|
|
333
|
+
def _annotate_initial_screenshot(
|
|
334
|
+
self, fused_xml, screenshot_path: str, image_name, minimal_list: str
|
|
335
|
+
) -> tuple[str, object]:
|
|
336
|
+
"""Draws labeled dots for the fused hierarchy; returns (minimal_list, marked_path).
|
|
337
|
+
|
|
338
|
+
Every numbered label is registered against its element so a
|
|
339
|
+
candidate the model later submits by number inherits real bounds.
|
|
340
|
+
"""
|
|
341
|
+
ctx = self.ctx
|
|
342
|
+
marked_path = None
|
|
343
|
+
try:
|
|
344
|
+
logger.info("Explorer self-annotating initial screenshot using latest_ui_hierarchy...")
|
|
345
|
+
formatted_list, elements, labels = format_minimal_list_with_elements(
|
|
346
|
+
fused_xml, self.width, self.height
|
|
347
|
+
)
|
|
348
|
+
minimal_list = formatted_list
|
|
349
|
+
points = [list(entry["center"]) for entry in elements]
|
|
350
|
+
for label, entry in zip(labels, elements, strict=True):
|
|
351
|
+
self._register_label(label, self._registry_element(entry))
|
|
352
|
+
self.global_label_idx = len(points) + 1
|
|
353
|
+
|
|
354
|
+
base_dir = (
|
|
355
|
+
Path(ctx.data_engine.base_dir)
|
|
356
|
+
if ctx.data_engine and getattr(ctx.data_engine, "base_dir", None)
|
|
357
|
+
else None
|
|
358
|
+
)
|
|
359
|
+
if not base_dir:
|
|
360
|
+
base_dir = settings.TRACES_PATH
|
|
361
|
+
images_dir = base_dir / "images"
|
|
362
|
+
initial_marked_dir = images_dir / "initial_marked"
|
|
363
|
+
initial_marked_dir.mkdir(parents=True, exist_ok=True)
|
|
364
|
+
|
|
365
|
+
existing_files = glob.glob(
|
|
366
|
+
str(initial_marked_dir / f"{image_name or 'temp_image'}_*.jpg")
|
|
367
|
+
)
|
|
368
|
+
max_seq = 0
|
|
369
|
+
for f in existing_files:
|
|
370
|
+
match = re.search(r"_(\d+)\.jpg$", f)
|
|
371
|
+
if match:
|
|
372
|
+
max_seq = max(max_seq, int(match.group(1)))
|
|
373
|
+
seq = max_seq + 1
|
|
374
|
+
marked_path = initial_marked_dir / f"{image_name or 'temp_image'}_{seq}.jpg"
|
|
375
|
+
|
|
376
|
+
# Draw dots on the raw screenshot
|
|
377
|
+
draw_dots(screenshot_path, points, labels, str(marked_path))
|
|
378
|
+
logger.info(
|
|
379
|
+
f"Successfully drew {len(points)} dots and saved marked image to {marked_path}"
|
|
380
|
+
)
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.error(f"Failed to self-annotate initial screenshot: {e}")
|
|
383
|
+
return minimal_list, marked_path
|
|
384
|
+
|
|
385
|
+
def _prepare_initial_annotation(
|
|
386
|
+
self, fused_xml, screenshot_path: str, minimal_list: str, image_name
|
|
387
|
+
) -> tuple[str, str]:
|
|
388
|
+
"""Generates initial visual annotations when no minimal list is provided.
|
|
389
|
+
|
|
390
|
+
Returns the (possibly updated) minimal list and the image path the
|
|
391
|
+
model should read (marked screenshot when available, raw otherwise).
|
|
392
|
+
"""
|
|
393
|
+
marked_path = None
|
|
394
|
+
if not minimal_list and fused_xml:
|
|
395
|
+
minimal_list, marked_path = self._annotate_initial_screenshot(
|
|
396
|
+
fused_xml, screenshot_path, image_name, minimal_list
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if marked_path and os.path.exists(str(marked_path)):
|
|
400
|
+
img_to_read = str(marked_path)
|
|
401
|
+
else:
|
|
402
|
+
img_to_read = screenshot_path
|
|
403
|
+
return minimal_list, img_to_read
|
|
@@ -0,0 +1,266 @@
|
|
|
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
|
+
"""In-memory index of the current screen's UI elements for the Explorer.
|
|
16
|
+
|
|
17
|
+
Built from the fused XML/OCR hierarchy so searches do not depend on the Data
|
|
18
|
+
Engine having persisted the frame. Supports fuzzy text search, exact matches
|
|
19
|
+
and coordinate hit testing.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
import difflib
|
|
24
|
+
import re
|
|
25
|
+
from typing import Any, Literal
|
|
26
|
+
|
|
27
|
+
from artemis.utils.visualization import parse_bounds
|
|
28
|
+
|
|
29
|
+
Bounds = tuple[int, int, int, int]
|
|
30
|
+
ElementSource = Literal["xml", "ocr"]
|
|
31
|
+
|
|
32
|
+
#: Attributes that mark an XML node as something the user can interact with.
|
|
33
|
+
INTERACTION_KEYS = (
|
|
34
|
+
"clickable",
|
|
35
|
+
"scrollable",
|
|
36
|
+
"long-clickable",
|
|
37
|
+
"checkable",
|
|
38
|
+
"focusable",
|
|
39
|
+
"editable",
|
|
40
|
+
"selected",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
#: Minimum query length for substring matches to count as strong evidence;
|
|
44
|
+
#: shorter fragments ("ok", "a") match far too much.
|
|
45
|
+
_SUBSTRING_MIN_CHARS = 3
|
|
46
|
+
_SUBSTRING_SCORE = 0.85
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def normalize_text(value: Any) -> str:
|
|
50
|
+
"""Case-folds and collapses whitespace so visually equal labels compare equal."""
|
|
51
|
+
return re.sub(r"\s+", " ", str(value or "")).strip().casefold()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def text_similarity(query: str, text: str) -> float:
|
|
55
|
+
"""Similarity in ``[0, 1]`` between a query and an element label.
|
|
56
|
+
|
|
57
|
+
Uses the Data Engine's ``SequenceMatcher`` ratio, with a minimum score
|
|
58
|
+
for substring matches when the query is at least three characters long.
|
|
59
|
+
"""
|
|
60
|
+
q, t = normalize_text(query), normalize_text(text)
|
|
61
|
+
if not q or not t:
|
|
62
|
+
return 0.0
|
|
63
|
+
ratio = difflib.SequenceMatcher(None, q, t).ratio()
|
|
64
|
+
if len(q) >= _SUBSTRING_MIN_CHARS and (q in t or t in q):
|
|
65
|
+
ratio = max(ratio, _SUBSTRING_SCORE)
|
|
66
|
+
return ratio
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_truthy_attr(value: Any) -> bool:
|
|
70
|
+
return value is True or str(value).strip().lower() == "true"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class ScreenElement:
|
|
75
|
+
"""One text-bearing element of the screen with pixel bounds."""
|
|
76
|
+
|
|
77
|
+
text: str
|
|
78
|
+
bounds: Bounds
|
|
79
|
+
source: ElementSource
|
|
80
|
+
class_name: str | None = None
|
|
81
|
+
resource_id: str | None = None
|
|
82
|
+
interactive: bool = False
|
|
83
|
+
node: dict[str, Any] = field(default_factory=dict, compare=False, hash=False, repr=False)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def center(self) -> tuple[int, int]:
|
|
87
|
+
left, top, right, bottom = self.bounds
|
|
88
|
+
return (left + right) // 2, (top + bottom) // 2
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def area(self) -> int:
|
|
92
|
+
left, top, right, bottom = self.bounds
|
|
93
|
+
return max(1, right - left) * max(1, bottom - top)
|
|
94
|
+
|
|
95
|
+
def contains(self, x: int, y: int) -> bool:
|
|
96
|
+
left, top, right, bottom = self.bounds
|
|
97
|
+
return left <= x <= right and top <= y <= bottom
|
|
98
|
+
|
|
99
|
+
def overlap_ratio(self, other: "ScreenElement") -> float:
|
|
100
|
+
"""Intersection over the smaller area; 1.0 when one box covers the other."""
|
|
101
|
+
l1, t1, r1, b1 = self.bounds
|
|
102
|
+
l2, t2, r2, b2 = other.bounds
|
|
103
|
+
inter_w = min(r1, r2) - max(l1, l2)
|
|
104
|
+
inter_h = min(b1, b2) - max(t1, t2)
|
|
105
|
+
if inter_w <= 0 or inter_h <= 0:
|
|
106
|
+
return 0.0
|
|
107
|
+
return (inter_w * inter_h) / min(self.area, other.area)
|
|
108
|
+
|
|
109
|
+
def describe(self) -> str:
|
|
110
|
+
"""Compact one-line description for tool text output."""
|
|
111
|
+
parts = [f"'{self.text}'"] if self.text else []
|
|
112
|
+
if self.class_name:
|
|
113
|
+
parts.append(f"class={self.class_name}")
|
|
114
|
+
if self.resource_id:
|
|
115
|
+
parts.append(f"id={self.resource_id}")
|
|
116
|
+
parts.append("source=ocr" if self.source == "ocr" else "source=ui-tree")
|
|
117
|
+
if self.interactive:
|
|
118
|
+
parts.append("interactive")
|
|
119
|
+
return " ".join(parts)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class TextMatch:
|
|
124
|
+
element: ScreenElement
|
|
125
|
+
score: float
|
|
126
|
+
matched_text: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ScreenIndex:
|
|
130
|
+
"""Queryable snapshot of the visible, text-bearing elements of one screen."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, elements: list[ScreenElement], width: int, height: int):
|
|
133
|
+
self.elements = elements
|
|
134
|
+
self.width = width
|
|
135
|
+
self.height = height
|
|
136
|
+
|
|
137
|
+
def __len__(self) -> int:
|
|
138
|
+
return len(self.elements)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def ocr_elements(self) -> list[ScreenElement]:
|
|
142
|
+
return [e for e in self.elements if e.source == "ocr"]
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------------ #
|
|
145
|
+
# Construction
|
|
146
|
+
# ------------------------------------------------------------------ #
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def empty(cls, width: int, height: int) -> "ScreenIndex":
|
|
150
|
+
return cls([], width, height)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def from_hierarchy(
|
|
154
|
+
cls, fused_xml: list[dict[str, Any]] | None, width: int, height: int
|
|
155
|
+
) -> "ScreenIndex":
|
|
156
|
+
"""Builds the index from a fused UI hierarchy (XML nodes plus OCR text).
|
|
157
|
+
|
|
158
|
+
Off-screen and zero-area boxes are dropped, and the same label at the
|
|
159
|
+
same place is kept once. Nodes without any label are skipped: the
|
|
160
|
+
index exists to answer text and hit-test queries, and an unlabeled
|
|
161
|
+
wrapper answers neither usefully.
|
|
162
|
+
"""
|
|
163
|
+
elements: list[ScreenElement] = []
|
|
164
|
+
seen: set[tuple[str, Bounds]] = set()
|
|
165
|
+
|
|
166
|
+
def add(element: ScreenElement) -> None:
|
|
167
|
+
key = (normalize_text(element.text), element.bounds)
|
|
168
|
+
if key in seen or not cls._is_visible(element.bounds, width, height):
|
|
169
|
+
return
|
|
170
|
+
seen.add(key)
|
|
171
|
+
elements.append(element)
|
|
172
|
+
|
|
173
|
+
for node in fused_xml or []:
|
|
174
|
+
if not isinstance(node, dict):
|
|
175
|
+
continue
|
|
176
|
+
class_name = node.get("class") or node.get("className")
|
|
177
|
+
resource_id = node.get("resource-id") or node.get("resourceId")
|
|
178
|
+
interactive = any(_is_truthy_attr(node.get(key)) for key in INTERACTION_KEYS)
|
|
179
|
+
|
|
180
|
+
for ocr in node.get("ocr_elements") or []:
|
|
181
|
+
ocr_bounds = parse_bounds(ocr.get("bounds")) if isinstance(ocr, dict) else None
|
|
182
|
+
ocr_text = str(ocr.get("text") or "").strip() if isinstance(ocr, dict) else ""
|
|
183
|
+
if ocr_bounds and ocr_text:
|
|
184
|
+
add(
|
|
185
|
+
ScreenElement(
|
|
186
|
+
text=ocr_text,
|
|
187
|
+
bounds=ocr_bounds,
|
|
188
|
+
source="ocr",
|
|
189
|
+
class_name=str(class_name) if class_name else None,
|
|
190
|
+
resource_id=str(resource_id) if resource_id else None,
|
|
191
|
+
interactive=interactive,
|
|
192
|
+
node=node,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
label = str(node.get("text") or node.get("content-desc") or "").strip()
|
|
197
|
+
bounds = parse_bounds(node.get("bounds"))
|
|
198
|
+
if label and bounds:
|
|
199
|
+
add(
|
|
200
|
+
ScreenElement(
|
|
201
|
+
text=label,
|
|
202
|
+
bounds=bounds,
|
|
203
|
+
source="xml",
|
|
204
|
+
class_name=str(class_name) if class_name else None,
|
|
205
|
+
resource_id=str(resource_id) if resource_id else None,
|
|
206
|
+
interactive=interactive,
|
|
207
|
+
node=node,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
return cls(elements, width, height)
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _is_visible(bounds: Bounds, width: int, height: int) -> bool:
|
|
214
|
+
left, top, right, bottom = bounds
|
|
215
|
+
if right <= left or bottom <= top:
|
|
216
|
+
return False
|
|
217
|
+
return right > 0 and bottom > 0 and left < width and top < height
|
|
218
|
+
|
|
219
|
+
# ------------------------------------------------------------------ #
|
|
220
|
+
# Queries
|
|
221
|
+
# ------------------------------------------------------------------ #
|
|
222
|
+
|
|
223
|
+
def search_text(self, query: str, threshold: float = 0.6, limit: int = 8) -> list[TextMatch]:
|
|
224
|
+
"""Fuzzy label search, best matches first."""
|
|
225
|
+
matches: list[TextMatch] = []
|
|
226
|
+
for element in self.elements:
|
|
227
|
+
score = text_similarity(query, element.text)
|
|
228
|
+
if score > threshold:
|
|
229
|
+
matches.append(TextMatch(element=element, score=score, matched_text=element.text))
|
|
230
|
+
matches.sort(key=lambda m: (-m.score, m.element.area))
|
|
231
|
+
return matches[:limit]
|
|
232
|
+
|
|
233
|
+
def exact_matches(self, query: str) -> list[ScreenElement]:
|
|
234
|
+
"""Elements whose label equals ``query`` after normalization.
|
|
235
|
+
|
|
236
|
+
Overlapping duplicates (the same label from the UI tree and from OCR
|
|
237
|
+
at the same place) collapse to one element, preferring the UI-tree
|
|
238
|
+
node because it carries structure the OCR box lacks.
|
|
239
|
+
"""
|
|
240
|
+
wanted = normalize_text(query)
|
|
241
|
+
if not wanted:
|
|
242
|
+
return []
|
|
243
|
+
hits = [e for e in self.elements if normalize_text(e.text) == wanted]
|
|
244
|
+
hits.sort(key=lambda e: (e.source != "xml", e.area))
|
|
245
|
+
unique: list[ScreenElement] = []
|
|
246
|
+
for element in hits:
|
|
247
|
+
if all(element.overlap_ratio(kept) < 0.5 for kept in unique):
|
|
248
|
+
unique.append(element)
|
|
249
|
+
return unique
|
|
250
|
+
|
|
251
|
+
def elements_at(self, x: int, y: int) -> list[ScreenElement]:
|
|
252
|
+
"""Elements covering pixel ``(x, y)``: UI-tree nodes innermost first, then OCR.
|
|
253
|
+
|
|
254
|
+
Mirrors the historical coordinate audit: UI-tree candidates stop at the
|
|
255
|
+
first interactive node (at most two are reported) so the answer names
|
|
256
|
+
what a tap would actually hit rather than every enclosing wrapper.
|
|
257
|
+
"""
|
|
258
|
+
covering = [e for e in self.elements if e.contains(x, y)]
|
|
259
|
+
xml = sorted((e for e in covering if e.source == "xml"), key=lambda e: e.area)
|
|
260
|
+
picked: list[ScreenElement] = []
|
|
261
|
+
for element in xml:
|
|
262
|
+
picked.append(element)
|
|
263
|
+
if element.interactive or len(picked) >= 2:
|
|
264
|
+
break
|
|
265
|
+
ocr = sorted((e for e in covering if e.source == "ocr"), key=lambda e: e.area)
|
|
266
|
+
return picked + ocr
|