android-midscene-automation 0.2.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +63 -1
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +10 -8
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +13 -1
  507. package/src/App.vue +72 -23
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +369 -186
  525. 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