android-midscene-automation 0.2.4 → 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 +7 -0
- package/README.md +35 -0
- 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,57 @@
|
|
|
1
|
+
// Explorer system prompt. Each section renders as "# SECTION" followed by
|
|
2
|
+
// "- bullet" lines. A bullet is either a plain string or an object
|
|
3
|
+
// {"text": "...", "requires": ["tool_a", ...]} that is emitted only when
|
|
4
|
+
// every listed tool is exposed for the active tier (see tiers.py).
|
|
5
|
+
// Tools by tier: pro = ask_perception_tool; ultra = ask_perception_tool,
|
|
6
|
+
// detect_objects, get_ocr_list, inspect_region, ask_image_processor.
|
|
7
|
+
// submit_answer is always available.
|
|
8
|
+
{
|
|
9
|
+
"IDENTITY": "You are the agentic UI Explorer: given a phone screenshot and a query, you locate the matching UI element(s) and return their normalized [nx, ny] coordinates on a 0-1000 scale.",
|
|
10
|
+
"OPERATING PRINCIPLES": [
|
|
11
|
+
"**Confidence-Driven Cost-Benefit Strategy**: Maximize localization accuracy while minimizing turns and tool calls. Every turn costs latency; stop exploring as soon as further calls would yield negligible accuracy gains.",
|
|
12
|
+
"**Direct Submit (High Confidence)**: If the initial annotated screenshot and the marked UI element list already make the target coordinates obvious, call `submit_answer` immediately in the first turn.",
|
|
13
|
+
{
|
|
14
|
+
"text": "**Parallel Context Gathering (Low Confidence / Ambiguity)**: If you are not confident, call `ask_perception_tool` once in the first turn with `search_query` (the visible text or name of the target), `nx`/`ny` (the most plausible spot for the target, to audit which element actually sits there) and `detect_queries` (short visual descriptions of the target). It runs the UI-tree text search, the coordinate audit and visual object detection concurrently and returns all three results with annotated screenshots. Reconcile the results, then call `submit_answer`.",
|
|
15
|
+
"requires": ["ask_perception_tool"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"text": "**Targeted Verification**: If micro-details or exact coordinates are still ambiguous after context gathering, zoom in on the promising candidate areas with `inspect_region` (several regions may be inspected in parallel within one turn). Each zoom-in turn costs one of your turns, so plan verification steps accordingly.",
|
|
19
|
+
"requires": ["inspect_region"]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"text": "**Focused Detection**: Use `detect_objects` on its own when you only need visual detection (for example after a transformation, on a derived image ID), instead of re-running the full `ask_perception_tool` bundle.",
|
|
23
|
+
"requires": ["detect_objects"]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"text": "**Text Inventory**: Use `get_ocr_list` when the target is text that the UI tree does not expose (canvas-rendered or image text); it lists every OCR-detected text with coordinates.",
|
|
27
|
+
"requires": ["get_ocr_list"]
|
|
28
|
+
},
|
|
29
|
+
"**Finalization**: Submit coordinates using `submit_answer`. In your final turn all other tools are stripped away, leaving only `submit_answer`, so never postpone the submission past the point where you are reasonably confident.",
|
|
30
|
+
"**Tool Classes**: `Perception` tools extract and locate elements, coordinates and text on the screen; the `Finalization` tool (`submit_answer`) submits your ranked candidates and terminates the search.",
|
|
31
|
+
{
|
|
32
|
+
"text": "**Transformation Tool**: `ask_image_processor` performs pixel-level image editing (cropping, upscaling, masking, contrast changes) and registers the resulting images in the Image Pool. It is slow and expensive; use it only when perception on the original screenshot is insufficient, and call it sequentially because later perception steps depend on its output.",
|
|
33
|
+
"requires": ["ask_image_processor"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"text": "**Parallel vs Sequential Invocations**: Perception tools may be called in parallel within a single turn to expedite the search; transformation tools must be called sequentially.",
|
|
37
|
+
"requires": ["inspect_region", "ask_image_processor"]
|
|
38
|
+
},
|
|
39
|
+
"**Multimodal Perception**: Tool results come as text lists plus annotated screenshots with labeled dots. Use both the visual evidence and the structured text to decide.",
|
|
40
|
+
{
|
|
41
|
+
"text": "**Image Pool Tracking**: Images are managed in an Image Pool starting with `img_0` (the original screenshot). Transformations register new IDs (`img_1`, `img_2`, ...) that can be passed as `target_image_id` to `detect_objects`. Coordinates returned for derived images are mapped back to the original screen automatically.",
|
|
42
|
+
"requires": ["ask_image_processor", "detect_objects"]
|
|
43
|
+
},
|
|
44
|
+
"**Label Prefixes**: Plain numbers label the elements of the initial marked UI list. Labels starting with `X` are UI-tree matches from the text search (green dots) and the coordinate audit (blue dot). Labels starting with `O` are OCR text matches (green dots). Labels starting with `D` are visually detected objects (red dots). Always reference these exact labels when reasoning (e.g. \"I choose X2\").",
|
|
45
|
+
"**Label Inheritance**: A candidate you submit under a plain-number, `X` or `O` label inherits that element's exact bounds, which lets the caller verify the tap target. When a detection point (`D`) and such a label describe the same element, submit the `X`/`O`/number label rather than the raw detection point.",
|
|
46
|
+
"**Locating Components**: Match each label in a tool's text response with its dot of the corresponding color on the returned annotated screenshot to confirm where the element really is before trusting its coordinates.",
|
|
47
|
+
"**Contextual Awareness**: Always check the context feedback first to learn from previous mistakes (for example candidates that were already rejected)."
|
|
48
|
+
],
|
|
49
|
+
"BEHAVIORAL GUIDELINES & SOFT CONSTRAINTS": [
|
|
50
|
+
"**Avoid Hallucination**: Never guess coordinates. If the target cannot be located after using the available perception options, submit an empty `candidates` list and document your observations in `fallback_message`.",
|
|
51
|
+
"**Ambiguity Resolution**: If several elements match the query (e.g. three \"Add\" buttons), return a ranked list of all viable candidates (up to 10) via `submit_answer`, ordered by confidence, with brief descriptions that distinguish them.",
|
|
52
|
+
"**Semantic & Icon Synonym Fallback**: Literal matches are ideal, but semantic equivalents and synonyms are acceptable. If text is requested but only a corresponding icon exists, map the synonym and locate the icon (e.g. \"Search\" to a magnifying glass, \"Add\" to a \"+\" symbol)."
|
|
53
|
+
],
|
|
54
|
+
"STRICT CONSTRAINTS": [
|
|
55
|
+
"**Validation Standards**: Every submitted `coords` value must be `[nx, ny]` with both integers in the inclusive `[0-1000]` range. If a submission fails validation you receive the errors as a tool response so you can correct and re-submit."
|
|
56
|
+
]
|
|
57
|
+
}
|
|
@@ -0,0 +1,473 @@
|
|
|
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
|
+
"""Explorer agent entry point.
|
|
16
|
+
|
|
17
|
+
The :class:`Explorer` class combines the following modules:
|
|
18
|
+
|
|
19
|
+
- ``tiers``: engine, tools and budgets for flash / pro / ultra.
|
|
20
|
+
- ``tool_declarations``: static tool schemas (universal + native).
|
|
21
|
+
- ``perception_tools``: the ``exec_*`` perception/vision tool method group.
|
|
22
|
+
- ``universal_runner``: the LangChain-based ``_run_universal`` loop.
|
|
23
|
+
- ``run_setup``: flash mode and the pre-loop setup phases of ``run``.
|
|
24
|
+
- ``native_runner``: the native Gemini reasoning loop of ``run``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
from typing import Any, Literal
|
|
30
|
+
|
|
31
|
+
from google import genai
|
|
32
|
+
from google.genai import types
|
|
33
|
+
|
|
34
|
+
from artemis.agents.explorer.constants import DEFAULT_EXPLORER_MODEL
|
|
35
|
+
from artemis.agents.explorer.geometry import (
|
|
36
|
+
FALLBACK_SCREEN_SIZE,
|
|
37
|
+
is_valid_norm_point,
|
|
38
|
+
norm_to_pixel,
|
|
39
|
+
pixel_to_norm,
|
|
40
|
+
)
|
|
41
|
+
from artemis.agents.explorer.native_runner import NativeRunnerMixin
|
|
42
|
+
from artemis.agents.explorer.perception_tools import PerceptionToolsMixin
|
|
43
|
+
from artemis.agents.explorer.run_setup import RunSetupMixin
|
|
44
|
+
from artemis.agents.explorer.screen_index import normalize_text, ScreenElement, ScreenIndex
|
|
45
|
+
from artemis.agents.explorer.tiers import ExplorerTier, get_tier
|
|
46
|
+
from artemis.agents.explorer.tool_declarations import NATIVE_EXPLORER_TOOL_DECLARATIONS
|
|
47
|
+
from artemis.agents.explorer.universal_runner import UniversalRunnerMixin
|
|
48
|
+
from artemis.config import settings
|
|
49
|
+
from artemis.context import ArtemisContext
|
|
50
|
+
from artemis.data_engine.trace import trace
|
|
51
|
+
from artemis.graph.state import State
|
|
52
|
+
from artemis.llm.google import is_gemini_model, strip_provider_prefix
|
|
53
|
+
from artemis.utils.ocr_api import is_ocr_configured
|
|
54
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
55
|
+
|
|
56
|
+
logger = get_logger(__name__)
|
|
57
|
+
|
|
58
|
+
__all__ = ["Explorer"]
|
|
59
|
+
|
|
60
|
+
#: Environment override for Gemini explicit context caching ("true" / "false").
|
|
61
|
+
CACHING_ENV_VAR = "ARTEMIS_EXPLORER_CACHING"
|
|
62
|
+
|
|
63
|
+
#: How far outside its labeled element (as a fraction of the screen, per side)
|
|
64
|
+
#: a candidate's point may fall and still inherit that element's bounds.
|
|
65
|
+
LABEL_TOLERANCE_RATIO = 0.02
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Explorer(PerceptionToolsMixin, UniversalRunnerMixin, RunSetupMixin, NativeRunnerMixin):
|
|
69
|
+
"""Locates UI elements on a screenshot for the ``ask_explorer`` tool.
|
|
70
|
+
|
|
71
|
+
The tier (flash / pro / ultra) is applied per ``run`` call from
|
|
72
|
+
:data:`artemis.agents.explorer.tiers.EXPLORER_TIERS`; it decides the
|
|
73
|
+
engine, the turn budget, the exposed perception tools and the caching
|
|
74
|
+
default. Construction is side-effect free: no network client is created
|
|
75
|
+
until the native Gemini branch actually needs one.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
DENYLIST_TEMPLATE = (
|
|
79
|
+
"\n# TOOL DENYLIST\n- The following tools are denylisted and cannot be used: {tools}\n"
|
|
80
|
+
)
|
|
81
|
+
TOOLS = NATIVE_EXPLORER_TOOL_DECLARATIONS
|
|
82
|
+
|
|
83
|
+
def __init__(self, ctx: ArtemisContext):
|
|
84
|
+
self.ctx = ctx
|
|
85
|
+
self.global_label_idx = 1
|
|
86
|
+
self.width, self.height = FALLBACK_SCREEN_SIZE
|
|
87
|
+
self.image_name: str | None = None
|
|
88
|
+
self.screenshot_path: str | None = None
|
|
89
|
+
self.image_pool: dict[str, dict[str, Any]] = {}
|
|
90
|
+
self.next_img_id = 1
|
|
91
|
+
self.http_client = None
|
|
92
|
+
self.client = None
|
|
93
|
+
self.turn_latencies: list[float] = []
|
|
94
|
+
self.turn_cached_tokens: list[int] = []
|
|
95
|
+
self.trace_history: list[dict[str, Any]] = []
|
|
96
|
+
|
|
97
|
+
# Rebuilt per ``run``: the queryable snapshot of the screen, and the
|
|
98
|
+
# element behind every label the model has been shown so far.
|
|
99
|
+
self.screen_index: ScreenIndex = ScreenIndex.empty(self.width, self.height)
|
|
100
|
+
self.label_registry: dict[str, ScreenElement] = {}
|
|
101
|
+
|
|
102
|
+
# No tier is applied until ``run``; until then only the user's own
|
|
103
|
+
# denylist (plus unconfigured OCR) restricts the tool set.
|
|
104
|
+
self.tier: ExplorerTier | None = None
|
|
105
|
+
self.max_turns = 0
|
|
106
|
+
self._user_denylist = self._load_user_denylist()
|
|
107
|
+
self.denylisted_tools = self._compute_denylist(None)
|
|
108
|
+
|
|
109
|
+
self.model_name = self._resolve_model_name()
|
|
110
|
+
self.use_native_gemini = self._detect_native_engine()
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------ #
|
|
113
|
+
# Engine selection (no side effects)
|
|
114
|
+
# ------------------------------------------------------------------ #
|
|
115
|
+
|
|
116
|
+
def _resolve_model_name(self) -> str:
|
|
117
|
+
"""Returns the configured Explorer model name without a provider prefix."""
|
|
118
|
+
llm_config = getattr(self.ctx, "llm_config", None)
|
|
119
|
+
llm_cfg = getattr(llm_config, "explorer", None) if llm_config else None
|
|
120
|
+
model = getattr(llm_cfg, "model", None) if llm_cfg else None
|
|
121
|
+
model_name = model if isinstance(model, str) and model else DEFAULT_EXPLORER_MODEL
|
|
122
|
+
return strip_provider_prefix(model_name)
|
|
123
|
+
|
|
124
|
+
def _detect_native_engine(self) -> bool:
|
|
125
|
+
"""True when the native google-genai SDK should drive the reasoning loop.
|
|
126
|
+
|
|
127
|
+
A Gemini model alone is not enough: without a Google API key (or a
|
|
128
|
+
client already shared on the context) the SDK cannot be constructed,
|
|
129
|
+
so such configurations run through the universal LangChain engine.
|
|
130
|
+
"""
|
|
131
|
+
if not is_gemini_model(self.model_name):
|
|
132
|
+
return False
|
|
133
|
+
if getattr(self.ctx, "_genai_client", None) is not None:
|
|
134
|
+
return True
|
|
135
|
+
api_key = getattr(settings, "GOOGLE_API_KEY", None)
|
|
136
|
+
return bool(api_key and api_key.get_secret_value())
|
|
137
|
+
|
|
138
|
+
def _ensure_genai_client(self):
|
|
139
|
+
"""Returns the context-level GenAI client, creating it on first use.
|
|
140
|
+
|
|
141
|
+
Only the native branch calls this, so universal configurations never
|
|
142
|
+
touch ``genai`` (and never fail on a missing Google key). The client
|
|
143
|
+
is stored on the context for connection pooling across agents.
|
|
144
|
+
"""
|
|
145
|
+
client = getattr(self.ctx, "_genai_client", None)
|
|
146
|
+
if client is None:
|
|
147
|
+
logger.info("Initializing GenAI client on context for connection pooling (Explorer)")
|
|
148
|
+
api_key = getattr(settings, "GOOGLE_API_KEY", None)
|
|
149
|
+
client = genai.Client(api_key=api_key.get_secret_value() if api_key else None)
|
|
150
|
+
self.ctx._genai_client = client
|
|
151
|
+
self.client = client
|
|
152
|
+
return client
|
|
153
|
+
|
|
154
|
+
# ------------------------------------------------------------------ #
|
|
155
|
+
# Tier application
|
|
156
|
+
# ------------------------------------------------------------------ #
|
|
157
|
+
|
|
158
|
+
def _load_user_denylist(self) -> frozenset[str]:
|
|
159
|
+
"""Reads the user-configured Explorer denylist from the agent config."""
|
|
160
|
+
try:
|
|
161
|
+
agent_cfg = getattr(self.ctx, "agent_config", None)
|
|
162
|
+
denylists = getattr(agent_cfg, "denylisted_tools", None) if agent_cfg else None
|
|
163
|
+
configured = denylists.get("explorer", []) if isinstance(denylists, dict) else []
|
|
164
|
+
return frozenset(str(name) for name in configured)
|
|
165
|
+
except (TypeError, AttributeError):
|
|
166
|
+
return frozenset()
|
|
167
|
+
|
|
168
|
+
def _compute_denylist(self, tier: ExplorerTier | None) -> set[str]:
|
|
169
|
+
"""Tools the model must not see: tier-hidden, user-denylisted, unconfigured OCR."""
|
|
170
|
+
denylist = set(self._user_denylist)
|
|
171
|
+
if tier is not None:
|
|
172
|
+
denylist |= tier.hidden_tools
|
|
173
|
+
if not is_ocr_configured():
|
|
174
|
+
denylist.add("get_ocr_list")
|
|
175
|
+
return denylist
|
|
176
|
+
|
|
177
|
+
def _apply_tier(self, tier: ExplorerTier) -> None:
|
|
178
|
+
"""Adopts the tier's turn budget and tool exposure for this run."""
|
|
179
|
+
self.tier = tier
|
|
180
|
+
self.max_turns = tier.max_turns
|
|
181
|
+
self.denylisted_tools = self._compute_denylist(tier)
|
|
182
|
+
|
|
183
|
+
def _resolve_caching(self, tier: ExplorerTier, enable_caching: bool | None) -> bool:
|
|
184
|
+
"""Resolves Gemini explicit context caching for this run.
|
|
185
|
+
|
|
186
|
+
Precedence: explicit argument, ``ARTEMIS_EXPLORER_CACHING`` env
|
|
187
|
+
override, the agent configuration, the execution setup, the global
|
|
188
|
+
settings, then the tier default. Configuration values are only
|
|
189
|
+
trusted when they are real booleans so a mocked or malformed config
|
|
190
|
+
(``None``, sentinel objects) falls through instead of forcing a value.
|
|
191
|
+
"""
|
|
192
|
+
if isinstance(enable_caching, bool):
|
|
193
|
+
return enable_caching
|
|
194
|
+
env_value = os.getenv(CACHING_ENV_VAR, "").strip().lower()
|
|
195
|
+
if env_value in ("true", "false"):
|
|
196
|
+
return env_value == "true"
|
|
197
|
+
for source in (
|
|
198
|
+
getattr(self.ctx, "agent_config", None),
|
|
199
|
+
getattr(self.ctx, "execution_setup", None),
|
|
200
|
+
):
|
|
201
|
+
value = getattr(getattr(source, "explorer", None), "caching", None)
|
|
202
|
+
if isinstance(value, bool):
|
|
203
|
+
return value
|
|
204
|
+
value = getattr(settings, "EXPLORER_CACHING", None)
|
|
205
|
+
if isinstance(value, bool):
|
|
206
|
+
return value
|
|
207
|
+
return tier.caching
|
|
208
|
+
|
|
209
|
+
def _hidden_tool_names(self) -> set[str]:
|
|
210
|
+
"""Denylisted tools plus OCR when it is unconfigured, evaluated at call time.
|
|
211
|
+
|
|
212
|
+
OCR availability is re-checked here (not only in ``_apply_tier``) so
|
|
213
|
+
callers that reset ``denylisted_tools`` still never see ``get_ocr_list``
|
|
214
|
+
without a configured OCR backend.
|
|
215
|
+
"""
|
|
216
|
+
hidden = set(self.denylisted_tools)
|
|
217
|
+
if not is_ocr_configured():
|
|
218
|
+
hidden.add("get_ocr_list")
|
|
219
|
+
return hidden
|
|
220
|
+
|
|
221
|
+
def get_exposed_tools(self, only_submit: bool = False) -> list[types.FunctionDeclaration]:
|
|
222
|
+
"""Native tool declarations the model may see for the current tier."""
|
|
223
|
+
hidden = self._hidden_tool_names()
|
|
224
|
+
tools = [tool for tool in self.TOOLS if tool.name not in hidden]
|
|
225
|
+
if only_submit:
|
|
226
|
+
tools = [tool for tool in tools if tool.name == "submit_answer"]
|
|
227
|
+
return tools
|
|
228
|
+
|
|
229
|
+
def _prune_historical_images(self, contents, keep_last=1):
|
|
230
|
+
image_parts = []
|
|
231
|
+
for content in contents:
|
|
232
|
+
for part in content.parts:
|
|
233
|
+
if getattr(part, "inline_data", None) or getattr(part, "file_data", None):
|
|
234
|
+
image_parts.append(part)
|
|
235
|
+
if len(image_parts) <= keep_last:
|
|
236
|
+
return
|
|
237
|
+
to_keep_ids = {id(p) for p in image_parts[-keep_last:]}
|
|
238
|
+
for content in contents:
|
|
239
|
+
content.parts = [
|
|
240
|
+
types.Part.from_text(
|
|
241
|
+
text=("[Image pruned to maintain visual focus on latest state]")
|
|
242
|
+
)
|
|
243
|
+
if (getattr(p, "inline_data", None) or getattr(p, "file_data", None))
|
|
244
|
+
and id(p) not in to_keep_ids
|
|
245
|
+
else p
|
|
246
|
+
for p in content.parts
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
# ------------------------------------------------------------------ #
|
|
250
|
+
# Label bounds and the structural pre-pass
|
|
251
|
+
# ------------------------------------------------------------------ #
|
|
252
|
+
|
|
253
|
+
def _norm_bounds(self, element: ScreenElement) -> list[int] | None:
|
|
254
|
+
"""The element's bounds on the 0-1000 grid, or None when they collapse.
|
|
255
|
+
|
|
256
|
+
A box that rounds to zero width or height is useless to the caller's
|
|
257
|
+
hit test, so it is withheld rather than shipped as a degenerate rect.
|
|
258
|
+
"""
|
|
259
|
+
left, top, right, bottom = element.bounds
|
|
260
|
+
n_left, n_top = pixel_to_norm(left, top, self.width, self.height)
|
|
261
|
+
n_right, n_bottom = pixel_to_norm(right, bottom, self.width, self.height)
|
|
262
|
+
if n_right <= n_left or n_bottom <= n_top:
|
|
263
|
+
return None
|
|
264
|
+
return [n_left, n_top, n_right, n_bottom]
|
|
265
|
+
|
|
266
|
+
def _enrich_candidates(self, candidates: list[Any]) -> list[Any]:
|
|
267
|
+
"""Attaches the registered element's bounds to candidates that point at it.
|
|
268
|
+
|
|
269
|
+
The label alone is not trusted: models occasionally reuse a label for
|
|
270
|
+
a different spot, so the coordinates must also fall inside the
|
|
271
|
+
element (padded by :data:`LABEL_TOLERANCE_RATIO` of the screen to
|
|
272
|
+
absorb rounding). Unknown labels, detection-only labels and points
|
|
273
|
+
outside the element pass through untouched.
|
|
274
|
+
"""
|
|
275
|
+
enriched: list[Any] = []
|
|
276
|
+
for cand in candidates:
|
|
277
|
+
element = (
|
|
278
|
+
self.label_registry.get(str(cand.get("label"))) if isinstance(cand, dict) else None
|
|
279
|
+
)
|
|
280
|
+
coords = cand.get("coords") if element is not None else None
|
|
281
|
+
if element is None or not is_valid_norm_point(coords):
|
|
282
|
+
enriched.append(cand)
|
|
283
|
+
continue
|
|
284
|
+
px, py = norm_to_pixel(int(coords[0]), int(coords[1]), self.width, self.height)
|
|
285
|
+
left, top, right, bottom = element.bounds
|
|
286
|
+
pad_x = self.width * LABEL_TOLERANCE_RATIO
|
|
287
|
+
pad_y = self.height * LABEL_TOLERANCE_RATIO
|
|
288
|
+
inside = left - pad_x <= px <= right + pad_x and top - pad_y <= py <= bottom + pad_y
|
|
289
|
+
norm_bounds = self._norm_bounds(element) if inside else None
|
|
290
|
+
if norm_bounds is None:
|
|
291
|
+
enriched.append(cand)
|
|
292
|
+
continue
|
|
293
|
+
enriched.append({**cand, "bounds": norm_bounds, "source": element.source})
|
|
294
|
+
return enriched
|
|
295
|
+
|
|
296
|
+
def _structural_prepass(self, query: str) -> tuple[list[dict[str, Any]], list[str]]:
|
|
297
|
+
"""Resolves the ``|``-separated parts of ``query`` that exactly name one element.
|
|
298
|
+
|
|
299
|
+
Returns ``(candidates, unresolved_parts)``. Only a *unique* exact
|
|
300
|
+
label match is taken: several matches mean the caller's wording does
|
|
301
|
+
not disambiguate, which is exactly what the model is for.
|
|
302
|
+
"""
|
|
303
|
+
parts = [part.strip() for part in query.split("|") if part.strip()]
|
|
304
|
+
candidates: list[dict[str, Any]] = []
|
|
305
|
+
unresolved: list[str] = []
|
|
306
|
+
for part in parts:
|
|
307
|
+
hits = self.screen_index.exact_matches(part)
|
|
308
|
+
if len(hits) != 1:
|
|
309
|
+
unresolved.append(part)
|
|
310
|
+
continue
|
|
311
|
+
element = hits[0]
|
|
312
|
+
label = f"T{len(candidates) + 1}"
|
|
313
|
+
self._register_label(label, element)
|
|
314
|
+
cx, cy = element.center
|
|
315
|
+
candidate: dict[str, Any] = {
|
|
316
|
+
"label": label,
|
|
317
|
+
"coords": list(pixel_to_norm(cx, cy, self.width, self.height)),
|
|
318
|
+
}
|
|
319
|
+
norm_bounds = self._norm_bounds(element)
|
|
320
|
+
if norm_bounds is not None:
|
|
321
|
+
candidate["bounds"] = norm_bounds
|
|
322
|
+
candidate["source"] = element.source
|
|
323
|
+
candidate["description"] = (
|
|
324
|
+
f"{element.text} (exact text match)"
|
|
325
|
+
if normalize_text(part) == normalize_text(element.text)
|
|
326
|
+
else f"{part} (exact text match: '{element.text}')"
|
|
327
|
+
)
|
|
328
|
+
candidates.append(candidate)
|
|
329
|
+
return candidates, unresolved
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def _merge_prepass(prepass: list[dict[str, Any]], raw: str) -> str:
|
|
333
|
+
"""Prepends pre-pass candidates to the engine's outcome JSON.
|
|
334
|
+
|
|
335
|
+
An engine answer that is not outcome-shaped (a bare error string)
|
|
336
|
+
becomes the ``fallback_message`` so the structural hits survive it.
|
|
337
|
+
"""
|
|
338
|
+
if not prepass:
|
|
339
|
+
return raw
|
|
340
|
+
try:
|
|
341
|
+
data = json.loads(raw)
|
|
342
|
+
except (TypeError, ValueError):
|
|
343
|
+
data = None
|
|
344
|
+
if not isinstance(data, dict):
|
|
345
|
+
return json.dumps(
|
|
346
|
+
{"candidates": prepass, "fallback_message": str(raw)}, ensure_ascii=False
|
|
347
|
+
)
|
|
348
|
+
engine_candidates = data.get("candidates") or []
|
|
349
|
+
return json.dumps(
|
|
350
|
+
{
|
|
351
|
+
"candidates": [*prepass, *engine_candidates],
|
|
352
|
+
"fallback_message": str(data.get("fallback_message") or ""),
|
|
353
|
+
},
|
|
354
|
+
ensure_ascii=False,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# ------------------------------------------------------------------ #
|
|
358
|
+
# Entry point
|
|
359
|
+
# ------------------------------------------------------------------ #
|
|
360
|
+
|
|
361
|
+
@trace(type="agent", name="explorer")
|
|
362
|
+
async def run(
|
|
363
|
+
self,
|
|
364
|
+
query: str,
|
|
365
|
+
context_feedback: str,
|
|
366
|
+
screenshot_path: str,
|
|
367
|
+
state: State,
|
|
368
|
+
minimal_list: str = "",
|
|
369
|
+
enable_caching: bool | None = None,
|
|
370
|
+
version: Literal["flash", "pro", "ultra"] = "pro",
|
|
371
|
+
) -> str:
|
|
372
|
+
"""Locates ``query`` on ``screenshot_path`` under the ``version`` tier.
|
|
373
|
+
|
|
374
|
+
Every tier first indexes the screen in memory and tries the
|
|
375
|
+
structural pre-pass; only the parts of the query it cannot settle
|
|
376
|
+
reach the tier's engine. Returns the JSON string
|
|
377
|
+
``{"candidates": [...], "fallback_message": "..."}`` consumed by
|
|
378
|
+
``artemis.tools.explorer_tool``.
|
|
379
|
+
"""
|
|
380
|
+
tier = get_tier(version)
|
|
381
|
+
self._apply_tier(tier)
|
|
382
|
+
self.screenshot_path = screenshot_path
|
|
383
|
+
self.global_label_idx = 1
|
|
384
|
+
self.label_registry = {}
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
fused_xml = await self._load_screen(screenshot_path, state, tier)
|
|
388
|
+
prepass, unresolved = self._structural_prepass(query)
|
|
389
|
+
if prepass and not unresolved:
|
|
390
|
+
logger.info(
|
|
391
|
+
f"Explorer pre-pass answered {query!r} from the UI tree without a model call."
|
|
392
|
+
)
|
|
393
|
+
return json.dumps(
|
|
394
|
+
{"candidates": prepass, "fallback_message": ""}, ensure_ascii=False
|
|
395
|
+
)
|
|
396
|
+
engine_query = " | ".join(unresolved) if prepass else query
|
|
397
|
+
|
|
398
|
+
if tier.is_oneshot:
|
|
399
|
+
raw = await self._run_flash(engine_query, screenshot_path)
|
|
400
|
+
else:
|
|
401
|
+
raw = await self._run_loop(
|
|
402
|
+
tier,
|
|
403
|
+
engine_query,
|
|
404
|
+
context_feedback,
|
|
405
|
+
screenshot_path,
|
|
406
|
+
state,
|
|
407
|
+
minimal_list,
|
|
408
|
+
enable_caching,
|
|
409
|
+
fused_xml,
|
|
410
|
+
)
|
|
411
|
+
return self._merge_prepass(prepass, raw)
|
|
412
|
+
finally:
|
|
413
|
+
await self._close_http_client()
|
|
414
|
+
|
|
415
|
+
async def _run_loop(
|
|
416
|
+
self,
|
|
417
|
+
tier: ExplorerTier,
|
|
418
|
+
query: str,
|
|
419
|
+
context_feedback: str,
|
|
420
|
+
screenshot_path: str,
|
|
421
|
+
state: State,
|
|
422
|
+
minimal_list: str,
|
|
423
|
+
enable_caching: bool | None,
|
|
424
|
+
fused_xml: list[dict[str, Any]],
|
|
425
|
+
) -> str:
|
|
426
|
+
"""Shared setup for the loop engines, then the engine-specific loop."""
|
|
427
|
+
self._init_image_pool(screenshot_path)
|
|
428
|
+
|
|
429
|
+
prompt_template, prompt_error = self._build_prompt_template(tier)
|
|
430
|
+
if prompt_error is not None:
|
|
431
|
+
return self._failure_outcome(prompt_error)
|
|
432
|
+
|
|
433
|
+
minimal_list, img_to_read = self._prepare_initial_annotation(
|
|
434
|
+
fused_xml, screenshot_path, minimal_list, self.image_name
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if not self.use_native_gemini:
|
|
438
|
+
return await self._run_universal(
|
|
439
|
+
query=query,
|
|
440
|
+
context_feedback=context_feedback,
|
|
441
|
+
screenshot_path=img_to_read,
|
|
442
|
+
state=state,
|
|
443
|
+
minimal_list=minimal_list,
|
|
444
|
+
prompt_template=prompt_template,
|
|
445
|
+
max_turns=self.max_turns,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
client = self._ensure_genai_client()
|
|
449
|
+
model_name, temperature, thinking_level, _fallback_model = self._resolve_model_params()
|
|
450
|
+
return await self._run_native(
|
|
451
|
+
client=client,
|
|
452
|
+
query=query,
|
|
453
|
+
context_feedback=context_feedback,
|
|
454
|
+
minimal_list=minimal_list,
|
|
455
|
+
img_to_read=img_to_read,
|
|
456
|
+
enable_caching=self._resolve_caching(tier, enable_caching),
|
|
457
|
+
prompt_template=prompt_template,
|
|
458
|
+
model_name=model_name,
|
|
459
|
+
temperature=temperature,
|
|
460
|
+
thinking_level=thinking_level,
|
|
461
|
+
max_turns=self.max_turns,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
async def _close_http_client(self) -> None:
|
|
465
|
+
"""Releases the on-demand OCR HTTP client; both engines share this path."""
|
|
466
|
+
client, self.http_client = self.http_client, None
|
|
467
|
+
if client is None:
|
|
468
|
+
return
|
|
469
|
+
try:
|
|
470
|
+
await client.aclose()
|
|
471
|
+
logger.info("Closed Explorer HTTP client.")
|
|
472
|
+
except Exception as close_err:
|
|
473
|
+
logger.warning(f"Failed to close Explorer HTTP client: {close_err}")
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
"""Screen geometry shared by the Explorer agent and the ``ask_explorer`` tool.
|
|
16
|
+
|
|
17
|
+
Explorer candidates use normalized ``[x, y]`` coordinates on a 0-1000 scale;
|
|
18
|
+
device actions use pixels.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
#: Fallback screen size when neither the operator observation nor the device
|
|
24
|
+
#: context reports one (a common phone portrait resolution).
|
|
25
|
+
FALLBACK_SCREEN_SIZE: tuple[int, int] = (1080, 2400)
|
|
26
|
+
|
|
27
|
+
NORMALIZED_MAX = 1000
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve_screen_size(ctx: Any, state: Any) -> tuple[int, int]:
|
|
31
|
+
"""Returns ``(width, height)`` in pixels for the current screenshot.
|
|
32
|
+
|
|
33
|
+
Precedence: the operator's latest raw observation (which measured the
|
|
34
|
+
screenshot it annotated), then the device context, then the fallback.
|
|
35
|
+
"""
|
|
36
|
+
raw = getattr(state, "operator_raw_data", None) or {}
|
|
37
|
+
width = raw.get("width") if isinstance(raw, dict) else None
|
|
38
|
+
height = raw.get("height") if isinstance(raw, dict) else None
|
|
39
|
+
if isinstance(width, int) and isinstance(height, int) and width > 0 and height > 0:
|
|
40
|
+
return width, height
|
|
41
|
+
|
|
42
|
+
device = getattr(ctx, "device", None)
|
|
43
|
+
dev_w = getattr(device, "device_width", None) if device else None
|
|
44
|
+
dev_h = getattr(device, "device_height", None) if device else None
|
|
45
|
+
fb_w, fb_h = FALLBACK_SCREEN_SIZE
|
|
46
|
+
width = dev_w if isinstance(dev_w, int) and dev_w > 0 else fb_w
|
|
47
|
+
height = dev_h if isinstance(dev_h, int) and dev_h > 0 else fb_h
|
|
48
|
+
return width, height
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def norm_to_pixel(nx: float, ny: float, width: int, height: int) -> tuple[int, int]:
|
|
52
|
+
"""Maps normalized 0-1000 coordinates to clamped pixel coordinates."""
|
|
53
|
+
px = int(max(0, min(width, nx * width / NORMALIZED_MAX)))
|
|
54
|
+
py = int(max(0, min(height, ny * height / NORMALIZED_MAX)))
|
|
55
|
+
return px, py
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def pixel_to_norm(px: float, py: float, width: int, height: int) -> tuple[int, int]:
|
|
59
|
+
"""Maps pixel coordinates to clamped normalized 0-1000 coordinates."""
|
|
60
|
+
nx = int(max(0, min(NORMALIZED_MAX, px * NORMALIZED_MAX / width)))
|
|
61
|
+
ny = int(max(0, min(NORMALIZED_MAX, py * NORMALIZED_MAX / height)))
|
|
62
|
+
return nx, ny
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_valid_norm_point(coords: Any) -> bool:
|
|
66
|
+
"""True when ``coords`` is a 2-item ``[nx, ny]`` inside the 0-1000 range."""
|
|
67
|
+
if not isinstance(coords, (list, tuple)) or len(coords) != 2:
|
|
68
|
+
return False
|
|
69
|
+
try:
|
|
70
|
+
nx, ny = int(coords[0]), int(coords[1])
|
|
71
|
+
except (TypeError, ValueError):
|
|
72
|
+
return False
|
|
73
|
+
return 0 <= nx <= NORMALIZED_MAX and 0 <= ny <= NORMALIZED_MAX
|