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,403 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Setup phases of ``Explorer.run``.
16
+
17
+ Split out of ``artemis.agents.explorer.explorer``: the flash-tier one-shot
18
+ detection path, the per-run screen loading shared by every tier (image record
19
+ resolution, screen dimensions, fused hierarchy, in-memory screen index) and
20
+ the named preparation phases executed before a reasoning loop (image pool,
21
+ model parameters, tier-aware prompt construction, initial screenshot
22
+ annotation), packaged as a mixin consumed by ``Explorer``.
23
+ """
24
+
25
+ import base64
26
+ import glob
27
+ import hashlib
28
+ import json
29
+ import os
30
+ from pathlib import Path
31
+ import re
32
+ from typing import TYPE_CHECKING, Any
33
+
34
+ import httpx
35
+
36
+ from artemis.agents.explorer.constants import (
37
+ DEFAULT_EXPLORER_MODEL,
38
+ EXECUTION_CONSTRAINT_TEMPLATE,
39
+ )
40
+ from artemis.agents.explorer.geometry import resolve_screen_size
41
+ from artemis.agents.explorer.perception_tools import load_detector_templates
42
+ from artemis.agents.explorer.screen_index import ScreenElement, ScreenIndex
43
+ from artemis.agents.explorer.tiers import SUBMIT_TOOL, ExplorerTier
44
+ from artemis.agents.object_detector.object_detector import _run_object_detection
45
+ from artemis.config import settings
46
+ from artemis.data_engine.storage import StorageManager
47
+ from artemis.graph.state import State
48
+ from artemis.utils.ocr_api import is_ocr_configured, perform_ocr
49
+ from artemis.utils.ocr_xml_fusion import fuse_ocr_with_xml
50
+ from artemis.utils.visualization import draw_dots, format_minimal_list_with_elements
51
+ from third_party.mobile_use.utils.logger import get_logger
52
+
53
+ logger = get_logger(__name__)
54
+
55
+ #: Prompt bullets may be plain strings or ``{"text": ..., "requires": [...]}``
56
+ #: objects; the latter are emitted only when every listed tool is exposed.
57
+ PromptBullet = str | dict[str, Any]
58
+
59
+
60
+ def _bullet_text(bullet: PromptBullet, exposed_tools: set[str]) -> str | None:
61
+ """Returns the bullet text, or None when its tool requirements are unmet."""
62
+ if isinstance(bullet, dict):
63
+ required = bullet.get("requires") or []
64
+ if not all(tool in exposed_tools for tool in required):
65
+ return None
66
+ return str(bullet.get("text", ""))
67
+ return str(bullet)
68
+
69
+
70
+ def load_prompt_sections(prompt_path: Path) -> dict[str, Any]:
71
+ """Loads ``explorer.json`` tolerating ``//`` and ``/* */`` comments and trailing commas."""
72
+ content = prompt_path.read_text(encoding="utf-8")
73
+ content = re.sub(r"(?<!:)\/\/.*", "", content)
74
+ content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
75
+ content = re.sub(r",\s*([\]}])", r"\1", content)
76
+ return json.loads(content)
77
+
78
+
79
+ class RunSetupMixin:
80
+ """Flash mode and pre-loop setup phases of :class:`Explorer`."""
81
+
82
+ if TYPE_CHECKING:
83
+ from artemis.context import ArtemisContext
84
+
85
+ DENYLIST_TEMPLATE: str
86
+ ctx: ArtemisContext
87
+ width: int
88
+ height: int
89
+ image_name: str | None
90
+ screenshot_path: str | None
91
+ image_pool: dict[str, dict[str, Any]]
92
+ next_img_id: int
93
+ global_label_idx: int
94
+ http_client: httpx.AsyncClient | None
95
+ denylisted_tools: set[str]
96
+ _user_denylist: frozenset[str]
97
+ screen_index: ScreenIndex
98
+ label_registry: dict[str, ScreenElement]
99
+
100
+ def _hidden_tool_names(self) -> set[str]: ...
101
+
102
+ def _register_label(self, label: str, element: ScreenElement) -> None: ...
103
+
104
+ def _registry_element(self, entry: dict[str, Any]) -> ScreenElement: ...
105
+
106
+ @staticmethod
107
+ def _failure_outcome(message: str) -> str:
108
+ """The contract-shaped answer for a run that produced no candidates."""
109
+ return json.dumps({"candidates": [], "fallback_message": message}, ensure_ascii=False)
110
+
111
+ async def _run_flash(self, query: str, screenshot_path: str) -> str:
112
+ """Flash tier: one-shot object detection without a reasoning loop.
113
+
114
+ Several targets may be listed in ``query`` separated by ``|``; each
115
+ detection becomes a ``D<n>`` candidate described by its detected label.
116
+ """
117
+ templates, global_timeout = load_detector_templates()
118
+
119
+ queries = [q.strip() for q in query.split("|") if q.strip()]
120
+
121
+ try:
122
+ result = await _run_object_detection(
123
+ self.ctx,
124
+ screenshot_path,
125
+ queries,
126
+ templates,
127
+ global_timeout=global_timeout,
128
+ )
129
+ detected_items = result.get("detected", [])
130
+ candidates = []
131
+ for idx, item in enumerate(detected_items):
132
+ pos = item.get("point")
133
+ if pos and isinstance(pos, list) and len(pos) == 2:
134
+ candidates.append(
135
+ {
136
+ "label": f"D{idx + 1}",
137
+ "coords": pos,
138
+ "description": item.get("label", query),
139
+ }
140
+ )
141
+ fallback_message = "" if candidates else f"Failed to detect: {query}"
142
+ return json.dumps(
143
+ {"candidates": candidates, "fallback_message": fallback_message},
144
+ ensure_ascii=False,
145
+ )
146
+ except Exception as e:
147
+ logger.error(f"Flash mode object detection failed: {e}")
148
+ return self._failure_outcome(f"Flash mode detection error: {e}")
149
+
150
+ def _resolve_image_record(self, screenshot_path: str) -> tuple:
151
+ """Computes the screenshot hash and looks it up in the Data Engine DB."""
152
+ image_name = None
153
+ record = None
154
+ try:
155
+ sha256_hash = hashlib.sha256()
156
+ with open(screenshot_path, "rb") as f:
157
+ for byte_block in iter(lambda: f.read(4096), b""):
158
+ sha256_hash.update(byte_block)
159
+ computed_hash = sha256_hash.hexdigest()
160
+ logger.info(f"Computed screenshot hash: {computed_hash}")
161
+
162
+ # Check if it exists in DB
163
+ db_path = settings.DATA_ENGINE_DB_PATH
164
+ base_dir = settings.TRACES_PATH
165
+ storage = StorageManager(db_path, base_dir)
166
+ record = storage.get_image(computed_hash)
167
+
168
+ if record:
169
+ image_name = computed_hash
170
+ else:
171
+ logger.warning(
172
+ f"Image hash {computed_hash} not found in Data Engine DB."
173
+ " Data Engine might not be synced yet."
174
+ )
175
+
176
+ except Exception as e:
177
+ logger.warning(f"Failed to compute hash or check DB: {e}")
178
+
179
+ return image_name, record
180
+
181
+ def _resolve_screen_dimensions(self, state: State) -> None:
182
+ """Adopts the screenshot's pixel size (operator observation, device, fallback)."""
183
+ self.width, self.height = resolve_screen_size(self.ctx, state)
184
+
185
+ async def _load_screen(
186
+ self, screenshot_path: str, state: State, tier: ExplorerTier
187
+ ) -> list[dict[str, Any]]:
188
+ """Loads the screen once per run: record, screen size, fused hierarchy, index.
189
+
190
+ Every tier goes through here so the structural pre-pass and the
191
+ perception tools share one in-memory snapshot instead of asking the
192
+ Data Engine per call. On-the-fly OCR is reserved for loop tiers:
193
+ flash never exposes text tools and must stay a single round trip.
194
+ """
195
+ image_name, record = self._resolve_image_record(screenshot_path)
196
+ self.image_name = image_name
197
+ self._resolve_screen_dimensions(state)
198
+ fused_xml = await self._build_fused_hierarchy(
199
+ record, state, screenshot_path, allow_ocr=not tier.is_oneshot
200
+ )
201
+ self.screen_index = ScreenIndex.from_hierarchy(fused_xml, self.width, self.height)
202
+ return fused_xml
203
+
204
+ def _init_image_pool(self, screenshot_path: str) -> None:
205
+ """Seeds the Image Pool with the original screenshot as img_0."""
206
+ self.image_pool = {
207
+ "img_0": {
208
+ "path": screenshot_path,
209
+ "transform": {
210
+ "offset_x": 0.0,
211
+ "offset_y": 0.0,
212
+ "scale_x": 1.0,
213
+ "scale_y": 1.0,
214
+ },
215
+ "description": "Original complete screenshot",
216
+ }
217
+ }
218
+ self.next_img_id = 1
219
+
220
+ def _resolve_model_params(self) -> tuple:
221
+ """Resolves model name, temperature, thinking level, and fallback model."""
222
+ llm_config = getattr(self.ctx, "llm_config", None)
223
+ llm_cfg = getattr(llm_config, "explorer", None) if llm_config else None
224
+ temperature = 0.1
225
+ fallback_model = None
226
+ thinking_level = None
227
+ if llm_cfg:
228
+ model_name = llm_cfg.model
229
+ if "/" in model_name:
230
+ model_name = model_name.split("/")[-1]
231
+ if getattr(llm_cfg, "temperature", None) is not None:
232
+ temperature = llm_cfg.temperature
233
+ if getattr(llm_cfg, "thinking_level", None) is not None:
234
+ thinking_level = llm_cfg.thinking_level
235
+ if getattr(llm_cfg, "fallback", None):
236
+ fallback_model = llm_cfg.fallback.model
237
+ if "/" in fallback_model:
238
+ fallback_model = fallback_model.split("/")[-1]
239
+ else:
240
+ model_name = DEFAULT_EXPLORER_MODEL
241
+
242
+ return model_name, temperature, thinking_level, fallback_model
243
+
244
+ def _exposed_tool_names(self, tier: ExplorerTier) -> set[str]:
245
+ """Tools the model can actually call in this run (tier minus hidden)."""
246
+ return (set(tier.tools) | {SUBMIT_TOOL}) - self._hidden_tool_names()
247
+
248
+ def _build_prompt_template(self, tier: ExplorerTier) -> tuple[str | None, str | None]:
249
+ """Builds the tier-aware system prompt; returns ``(prompt, error_message)``.
250
+
251
+ Bullets whose ``requires`` list names a tool the model cannot call in
252
+ this tier are dropped, so the prompt never describes capabilities
253
+ that are absent from the tool declarations. Tier-hidden tools are
254
+ simply never mentioned; only the user's own denylist (restricted to
255
+ tools the tier would otherwise expose) is spelled out.
256
+ """
257
+ prompt_path = Path(__file__).parent / "explorer.json"
258
+ if not prompt_path.exists():
259
+ return None, "Error: Explorer prompt template not found."
260
+
261
+ try:
262
+ data = load_prompt_sections(prompt_path)
263
+ except Exception as e:
264
+ return None, f"Error loading or parsing explorer.json: {e}"
265
+
266
+ exposed = self._exposed_tool_names(tier)
267
+ prompt_parts: list[str] = []
268
+ for section, content_val in data.items():
269
+ if isinstance(content_val, list):
270
+ bullets = [
271
+ text
272
+ for text in (_bullet_text(b, exposed) for b in content_val)
273
+ if text is not None
274
+ ]
275
+ if not bullets:
276
+ continue
277
+ prompt_parts.append(f"# {section}")
278
+ prompt_parts.extend(f"- {bullet}" for bullet in bullets)
279
+ else:
280
+ text = _bullet_text(content_val, exposed)
281
+ if text is None:
282
+ continue
283
+ prompt_parts.append(f"# {section}")
284
+ prompt_parts.append(text)
285
+ prompt_parts.append("")
286
+
287
+ prompt_template = "\n".join(prompt_parts)
288
+
289
+ user_denied = sorted(set(self._user_denylist) & set(tier.tools))
290
+ if user_denied:
291
+ prompt_template += self.DENYLIST_TEMPLATE.format(tools=", ".join(user_denied))
292
+
293
+ constraint = EXECUTION_CONSTRAINT_TEMPLATE.format(max_turns=tier.max_turns)
294
+ prompt_template += "\n# EXECUTION CONSTRAINT\n- " + constraint + "\n"
295
+
296
+ return prompt_template, None
297
+
298
+ async def _build_fused_hierarchy(
299
+ self, record, state: State, screenshot_path: str, allow_ocr: bool = True
300
+ ) -> list[dict[str, Any]]:
301
+ """Loads (and if needed OCR-fuses) the UI hierarchy for the screenshot.
302
+
303
+ Precedence: the Data Engine record for the screenshot hash, else the
304
+ state's latest hierarchy when the screenshot is the latest one. The
305
+ HTTP client backing on-the-fly OCR is created here on first need and
306
+ released by ``Explorer.run`` regardless of the engine used.
307
+ """
308
+ fused_xml: Any = []
309
+ if record and getattr(record, "ui_tree", None):
310
+ ui_tree = record.ui_tree
311
+ ocr_results = getattr(record, "ocr_result", None)
312
+ if ocr_results is None:
313
+ if allow_ocr and is_ocr_configured():
314
+ try:
315
+ logger.info("Previous screenshot OCR is missing. Running OCR on-the-fly...")
316
+ with open(screenshot_path, "rb") as img_file:
317
+ img_b64 = base64.b64encode(img_file.read()).decode("utf-8")
318
+ if self.http_client is None:
319
+ self.http_client = httpx.AsyncClient()
320
+ ocr_results = await perform_ocr(img_b64, client=self.http_client)
321
+ except Exception as ocr_err:
322
+ logger.error(f"On-the-fly OCR failed for previous screenshot: {ocr_err}")
323
+ ocr_results = []
324
+ else:
325
+ ocr_results = []
326
+
327
+ fused_xml = fuse_ocr_with_xml(ui_tree, ocr_results or [])
328
+ logger.info("Successfully loaded and fused UI hierarchy for previous screenshot.")
329
+ elif screenshot_path == getattr(state, "latest_screenshot", None):
330
+ fused_xml = getattr(state, "latest_ui_hierarchy", None)
331
+ return list(fused_xml) if isinstance(fused_xml, (list, tuple)) else []
332
+
333
+ def _annotate_initial_screenshot(
334
+ self, fused_xml, screenshot_path: str, image_name, minimal_list: str
335
+ ) -> tuple[str, object]:
336
+ """Draws labeled dots for the fused hierarchy; returns (minimal_list, marked_path).
337
+
338
+ Every numbered label is registered against its element so a
339
+ candidate the model later submits by number inherits real bounds.
340
+ """
341
+ ctx = self.ctx
342
+ marked_path = None
343
+ try:
344
+ logger.info("Explorer self-annotating initial screenshot using latest_ui_hierarchy...")
345
+ formatted_list, elements, labels = format_minimal_list_with_elements(
346
+ fused_xml, self.width, self.height
347
+ )
348
+ minimal_list = formatted_list
349
+ points = [list(entry["center"]) for entry in elements]
350
+ for label, entry in zip(labels, elements, strict=True):
351
+ self._register_label(label, self._registry_element(entry))
352
+ self.global_label_idx = len(points) + 1
353
+
354
+ base_dir = (
355
+ Path(ctx.data_engine.base_dir)
356
+ if ctx.data_engine and getattr(ctx.data_engine, "base_dir", None)
357
+ else None
358
+ )
359
+ if not base_dir:
360
+ base_dir = settings.TRACES_PATH
361
+ images_dir = base_dir / "images"
362
+ initial_marked_dir = images_dir / "initial_marked"
363
+ initial_marked_dir.mkdir(parents=True, exist_ok=True)
364
+
365
+ existing_files = glob.glob(
366
+ str(initial_marked_dir / f"{image_name or 'temp_image'}_*.jpg")
367
+ )
368
+ max_seq = 0
369
+ for f in existing_files:
370
+ match = re.search(r"_(\d+)\.jpg$", f)
371
+ if match:
372
+ max_seq = max(max_seq, int(match.group(1)))
373
+ seq = max_seq + 1
374
+ marked_path = initial_marked_dir / f"{image_name or 'temp_image'}_{seq}.jpg"
375
+
376
+ # Draw dots on the raw screenshot
377
+ draw_dots(screenshot_path, points, labels, str(marked_path))
378
+ logger.info(
379
+ f"Successfully drew {len(points)} dots and saved marked image to {marked_path}"
380
+ )
381
+ except Exception as e:
382
+ logger.error(f"Failed to self-annotate initial screenshot: {e}")
383
+ return minimal_list, marked_path
384
+
385
+ def _prepare_initial_annotation(
386
+ self, fused_xml, screenshot_path: str, minimal_list: str, image_name
387
+ ) -> tuple[str, str]:
388
+ """Generates initial visual annotations when no minimal list is provided.
389
+
390
+ Returns the (possibly updated) minimal list and the image path the
391
+ model should read (marked screenshot when available, raw otherwise).
392
+ """
393
+ marked_path = None
394
+ if not minimal_list and fused_xml:
395
+ minimal_list, marked_path = self._annotate_initial_screenshot(
396
+ fused_xml, screenshot_path, image_name, minimal_list
397
+ )
398
+
399
+ if marked_path and os.path.exists(str(marked_path)):
400
+ img_to_read = str(marked_path)
401
+ else:
402
+ img_to_read = screenshot_path
403
+ return minimal_list, img_to_read
@@ -0,0 +1,266 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """In-memory index of the current screen's UI elements for the Explorer.
16
+
17
+ Built from the fused XML/OCR hierarchy so searches do not depend on the Data
18
+ Engine having persisted the frame. Supports fuzzy text search, exact matches
19
+ and coordinate hit testing.
20
+ """
21
+
22
+ from dataclasses import dataclass, field
23
+ import difflib
24
+ import re
25
+ from typing import Any, Literal
26
+
27
+ from artemis.utils.visualization import parse_bounds
28
+
29
+ Bounds = tuple[int, int, int, int]
30
+ ElementSource = Literal["xml", "ocr"]
31
+
32
+ #: Attributes that mark an XML node as something the user can interact with.
33
+ INTERACTION_KEYS = (
34
+ "clickable",
35
+ "scrollable",
36
+ "long-clickable",
37
+ "checkable",
38
+ "focusable",
39
+ "editable",
40
+ "selected",
41
+ )
42
+
43
+ #: Minimum query length for substring matches to count as strong evidence;
44
+ #: shorter fragments ("ok", "a") match far too much.
45
+ _SUBSTRING_MIN_CHARS = 3
46
+ _SUBSTRING_SCORE = 0.85
47
+
48
+
49
+ def normalize_text(value: Any) -> str:
50
+ """Case-folds and collapses whitespace so visually equal labels compare equal."""
51
+ return re.sub(r"\s+", " ", str(value or "")).strip().casefold()
52
+
53
+
54
+ def text_similarity(query: str, text: str) -> float:
55
+ """Similarity in ``[0, 1]`` between a query and an element label.
56
+
57
+ Uses the Data Engine's ``SequenceMatcher`` ratio, with a minimum score
58
+ for substring matches when the query is at least three characters long.
59
+ """
60
+ q, t = normalize_text(query), normalize_text(text)
61
+ if not q or not t:
62
+ return 0.0
63
+ ratio = difflib.SequenceMatcher(None, q, t).ratio()
64
+ if len(q) >= _SUBSTRING_MIN_CHARS and (q in t or t in q):
65
+ ratio = max(ratio, _SUBSTRING_SCORE)
66
+ return ratio
67
+
68
+
69
+ def _is_truthy_attr(value: Any) -> bool:
70
+ return value is True or str(value).strip().lower() == "true"
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class ScreenElement:
75
+ """One text-bearing element of the screen with pixel bounds."""
76
+
77
+ text: str
78
+ bounds: Bounds
79
+ source: ElementSource
80
+ class_name: str | None = None
81
+ resource_id: str | None = None
82
+ interactive: bool = False
83
+ node: dict[str, Any] = field(default_factory=dict, compare=False, hash=False, repr=False)
84
+
85
+ @property
86
+ def center(self) -> tuple[int, int]:
87
+ left, top, right, bottom = self.bounds
88
+ return (left + right) // 2, (top + bottom) // 2
89
+
90
+ @property
91
+ def area(self) -> int:
92
+ left, top, right, bottom = self.bounds
93
+ return max(1, right - left) * max(1, bottom - top)
94
+
95
+ def contains(self, x: int, y: int) -> bool:
96
+ left, top, right, bottom = self.bounds
97
+ return left <= x <= right and top <= y <= bottom
98
+
99
+ def overlap_ratio(self, other: "ScreenElement") -> float:
100
+ """Intersection over the smaller area; 1.0 when one box covers the other."""
101
+ l1, t1, r1, b1 = self.bounds
102
+ l2, t2, r2, b2 = other.bounds
103
+ inter_w = min(r1, r2) - max(l1, l2)
104
+ inter_h = min(b1, b2) - max(t1, t2)
105
+ if inter_w <= 0 or inter_h <= 0:
106
+ return 0.0
107
+ return (inter_w * inter_h) / min(self.area, other.area)
108
+
109
+ def describe(self) -> str:
110
+ """Compact one-line description for tool text output."""
111
+ parts = [f"'{self.text}'"] if self.text else []
112
+ if self.class_name:
113
+ parts.append(f"class={self.class_name}")
114
+ if self.resource_id:
115
+ parts.append(f"id={self.resource_id}")
116
+ parts.append("source=ocr" if self.source == "ocr" else "source=ui-tree")
117
+ if self.interactive:
118
+ parts.append("interactive")
119
+ return " ".join(parts)
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class TextMatch:
124
+ element: ScreenElement
125
+ score: float
126
+ matched_text: str
127
+
128
+
129
+ class ScreenIndex:
130
+ """Queryable snapshot of the visible, text-bearing elements of one screen."""
131
+
132
+ def __init__(self, elements: list[ScreenElement], width: int, height: int):
133
+ self.elements = elements
134
+ self.width = width
135
+ self.height = height
136
+
137
+ def __len__(self) -> int:
138
+ return len(self.elements)
139
+
140
+ @property
141
+ def ocr_elements(self) -> list[ScreenElement]:
142
+ return [e for e in self.elements if e.source == "ocr"]
143
+
144
+ # ------------------------------------------------------------------ #
145
+ # Construction
146
+ # ------------------------------------------------------------------ #
147
+
148
+ @classmethod
149
+ def empty(cls, width: int, height: int) -> "ScreenIndex":
150
+ return cls([], width, height)
151
+
152
+ @classmethod
153
+ def from_hierarchy(
154
+ cls, fused_xml: list[dict[str, Any]] | None, width: int, height: int
155
+ ) -> "ScreenIndex":
156
+ """Builds the index from a fused UI hierarchy (XML nodes plus OCR text).
157
+
158
+ Off-screen and zero-area boxes are dropped, and the same label at the
159
+ same place is kept once. Nodes without any label are skipped: the
160
+ index exists to answer text and hit-test queries, and an unlabeled
161
+ wrapper answers neither usefully.
162
+ """
163
+ elements: list[ScreenElement] = []
164
+ seen: set[tuple[str, Bounds]] = set()
165
+
166
+ def add(element: ScreenElement) -> None:
167
+ key = (normalize_text(element.text), element.bounds)
168
+ if key in seen or not cls._is_visible(element.bounds, width, height):
169
+ return
170
+ seen.add(key)
171
+ elements.append(element)
172
+
173
+ for node in fused_xml or []:
174
+ if not isinstance(node, dict):
175
+ continue
176
+ class_name = node.get("class") or node.get("className")
177
+ resource_id = node.get("resource-id") or node.get("resourceId")
178
+ interactive = any(_is_truthy_attr(node.get(key)) for key in INTERACTION_KEYS)
179
+
180
+ for ocr in node.get("ocr_elements") or []:
181
+ ocr_bounds = parse_bounds(ocr.get("bounds")) if isinstance(ocr, dict) else None
182
+ ocr_text = str(ocr.get("text") or "").strip() if isinstance(ocr, dict) else ""
183
+ if ocr_bounds and ocr_text:
184
+ add(
185
+ ScreenElement(
186
+ text=ocr_text,
187
+ bounds=ocr_bounds,
188
+ source="ocr",
189
+ class_name=str(class_name) if class_name else None,
190
+ resource_id=str(resource_id) if resource_id else None,
191
+ interactive=interactive,
192
+ node=node,
193
+ )
194
+ )
195
+
196
+ label = str(node.get("text") or node.get("content-desc") or "").strip()
197
+ bounds = parse_bounds(node.get("bounds"))
198
+ if label and bounds:
199
+ add(
200
+ ScreenElement(
201
+ text=label,
202
+ bounds=bounds,
203
+ source="xml",
204
+ class_name=str(class_name) if class_name else None,
205
+ resource_id=str(resource_id) if resource_id else None,
206
+ interactive=interactive,
207
+ node=node,
208
+ )
209
+ )
210
+ return cls(elements, width, height)
211
+
212
+ @staticmethod
213
+ def _is_visible(bounds: Bounds, width: int, height: int) -> bool:
214
+ left, top, right, bottom = bounds
215
+ if right <= left or bottom <= top:
216
+ return False
217
+ return right > 0 and bottom > 0 and left < width and top < height
218
+
219
+ # ------------------------------------------------------------------ #
220
+ # Queries
221
+ # ------------------------------------------------------------------ #
222
+
223
+ def search_text(self, query: str, threshold: float = 0.6, limit: int = 8) -> list[TextMatch]:
224
+ """Fuzzy label search, best matches first."""
225
+ matches: list[TextMatch] = []
226
+ for element in self.elements:
227
+ score = text_similarity(query, element.text)
228
+ if score > threshold:
229
+ matches.append(TextMatch(element=element, score=score, matched_text=element.text))
230
+ matches.sort(key=lambda m: (-m.score, m.element.area))
231
+ return matches[:limit]
232
+
233
+ def exact_matches(self, query: str) -> list[ScreenElement]:
234
+ """Elements whose label equals ``query`` after normalization.
235
+
236
+ Overlapping duplicates (the same label from the UI tree and from OCR
237
+ at the same place) collapse to one element, preferring the UI-tree
238
+ node because it carries structure the OCR box lacks.
239
+ """
240
+ wanted = normalize_text(query)
241
+ if not wanted:
242
+ return []
243
+ hits = [e for e in self.elements if normalize_text(e.text) == wanted]
244
+ hits.sort(key=lambda e: (e.source != "xml", e.area))
245
+ unique: list[ScreenElement] = []
246
+ for element in hits:
247
+ if all(element.overlap_ratio(kept) < 0.5 for kept in unique):
248
+ unique.append(element)
249
+ return unique
250
+
251
+ def elements_at(self, x: int, y: int) -> list[ScreenElement]:
252
+ """Elements covering pixel ``(x, y)``: UI-tree nodes innermost first, then OCR.
253
+
254
+ Mirrors the historical coordinate audit: UI-tree candidates stop at the
255
+ first interactive node (at most two are reported) so the answer names
256
+ what a tap would actually hit rather than every enclosing wrapper.
257
+ """
258
+ covering = [e for e in self.elements if e.contains(x, y)]
259
+ xml = sorted((e for e in covering if e.source == "xml"), key=lambda e: e.area)
260
+ picked: list[ScreenElement] = []
261
+ for element in xml:
262
+ picked.append(element)
263
+ if element.interactive or len(picked) >= 2:
264
+ break
265
+ ocr = sorted((e for e in covering if e.source == "ocr"), key=lambda e: e.area)
266
+ return picked + ocr