android-midscene-automation 0.2.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +159 -29
  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 +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  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 +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,802 @@
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
+ import io
16
+ import os
17
+ import re
18
+ from typing import Any
19
+
20
+ from artemis.utils.coordinates import parse_swipe_parameters
21
+ from PIL import Image, ImageDraw, ImageFont
22
+
23
+
24
+ def draw_bounding_boxes(
25
+ image_path: str, boxes: list[list[int]], labels: list[str], output_path: str
26
+ ):
27
+ """Draw bounding boxes and labels on an image.
28
+
29
+ Args:
30
+ image_path: Path to the original image.
31
+ boxes: List of boxes, each box is [left, top, right, bottom].
32
+ labels: List of labels corresponding to each box.
33
+ output_path: Path to save the annotated image.
34
+ """
35
+ if not os.path.exists(image_path):
36
+ raise FileNotFoundError(f"Image not found at {image_path}")
37
+
38
+ img = Image.open(image_path)
39
+ if img.mode in ("RGBA", "P"):
40
+ img = img.convert("RGB")
41
+ draw = ImageDraw.Draw(img)
42
+
43
+ # Use a default font or try to load a system font
44
+ try:
45
+ font = ImageFont.load_default(size=45)
46
+ except Exception:
47
+ font = None
48
+
49
+ for box, label in zip(boxes, labels):
50
+ left, top, right, bottom = box
51
+
52
+ # Draw rectangle
53
+ draw.rectangle([left, top, right, bottom], outline="red", width=3)
54
+
55
+ # Draw label with outline stroke for high contrast
56
+ if font:
57
+ draw.text(
58
+ (left, top),
59
+ label,
60
+ fill="red",
61
+ font=font,
62
+ stroke_width=5,
63
+ stroke_fill="white",
64
+ )
65
+ else:
66
+ draw.text(
67
+ (left, top),
68
+ label,
69
+ fill="red",
70
+ stroke_width=5,
71
+ stroke_fill="white",
72
+ )
73
+ img.save(output_path)
74
+
75
+
76
+ def draw_dots(
77
+ image_path: str,
78
+ points: list[list[int]],
79
+ labels: list[str],
80
+ output_path: str,
81
+ radius: int = 10,
82
+ color: str = "red",
83
+ ):
84
+ """Draw dots and labels on an image.
85
+
86
+ Args:
87
+ image_path: Path to the original image.
88
+ points: List of points, each point is [x, y].
89
+ labels: List of labels corresponding to each point.
90
+ output_path: Path to save the annotated image.
91
+ radius: Radius of the dot.
92
+ color: Color of the indicator dot and text label.
93
+ """
94
+ if not os.path.exists(image_path):
95
+ raise FileNotFoundError(f"Image not found at {image_path}")
96
+
97
+ img = Image.open(image_path)
98
+ if img.mode in ("RGBA", "P"):
99
+ img = img.convert("RGB")
100
+ draw = ImageDraw.Draw(img)
101
+
102
+ try:
103
+ font = ImageFont.load_default(size=45)
104
+ except Exception:
105
+ font = None
106
+
107
+ # 1. Draw all background and foreground dots first
108
+ for point in points:
109
+ x, y = point
110
+
111
+ # Draw larger white background dot (2x radius)
112
+ white_radius = 2 * radius
113
+ draw.ellipse(
114
+ [
115
+ x - white_radius,
116
+ y - white_radius,
117
+ x + white_radius,
118
+ y + white_radius,
119
+ ],
120
+ fill="white",
121
+ outline="white",
122
+ )
123
+
124
+ # Draw colored indicator dot on top
125
+ draw.ellipse(
126
+ [x - radius, y - radius, x + radius, y + radius],
127
+ fill=color,
128
+ outline=color,
129
+ )
130
+
131
+ # 2. Draw all text labels on top
132
+ for point, label in zip(points, labels):
133
+ x, y = point
134
+ white_radius = 2 * radius
135
+
136
+ # Draw label shifted to prevent overlap with white dot and outline with a white stroke
137
+ if font:
138
+ draw.text(
139
+ (x + white_radius + 4, y - 22),
140
+ label,
141
+ fill=color,
142
+ font=font,
143
+ stroke_width=5,
144
+ stroke_fill="white",
145
+ )
146
+ else:
147
+ draw.text(
148
+ (x + white_radius + 4, y - 22),
149
+ label,
150
+ fill=color,
151
+ stroke_width=5,
152
+ stroke_fill="white",
153
+ )
154
+
155
+ img.save(output_path)
156
+
157
+
158
+ def parse_bounds(bounds_str: str | None) -> tuple[int, int, int, int] | None:
159
+ """Parses standard Android '[left,top][right,bottom]' bounds string."""
160
+ if not bounds_str:
161
+ return None
162
+ match = re.match(r"\[(\-?\d+),(\-?\d+)\]\[(\-?\d+),(\-?\d+)\]", bounds_str)
163
+ if match:
164
+ return tuple(map(int, match.groups()))
165
+ return None
166
+
167
+
168
+ def get_center_coordinates(left: int, top: int, right: int, bottom: int) -> tuple[int, int]:
169
+ """Returns pixel center coordinates of a bounding box."""
170
+ return (left + right) // 2, (top + bottom) // 2
171
+
172
+
173
+ def _inject_mutual_occlusion_warnings(
174
+ lines: list[str], items_bounds: list[tuple[int, tuple[int, int, int, int]]]
175
+ ) -> list[str]:
176
+ """Detect pairs of elements with >= 50% overlap and append a soft, restrained warning
177
+
178
+ to the generated element description lines.
179
+ Excludes pure parent-child enclosing bounds to prevent noise on standard
180
+ nested containers.
181
+ """
182
+ if len(items_bounds) < 2:
183
+ return lines
184
+
185
+ warnings_map: dict[int, list[int]] = {}
186
+ n = len(items_bounds)
187
+
188
+ for i in range(n):
189
+ idx_i, b_i = items_bounds[i]
190
+ l_i, t_i, r_i, bot_i = b_i
191
+ area_i = max(1, (r_i - l_i) * (bot_i - t_i))
192
+
193
+ for j in range(i + 1, n):
194
+ idx_j, b_j = items_bounds[j]
195
+ l_j, t_j, r_j, bot_j = b_j
196
+ area_j = max(1, (r_j - l_j) * (bot_j - t_j))
197
+
198
+ # Calculate intersection
199
+ inter_l = max(l_i, l_j)
200
+ inter_t = max(t_i, t_j)
201
+ inter_r = min(r_i, r_j)
202
+ inter_bot = min(bot_i, bot_j)
203
+
204
+ if inter_l >= inter_r or inter_t >= inter_bot:
205
+ continue
206
+
207
+ inter_area = (inter_r - inter_l) * (inter_bot - inter_t)
208
+ ratio_i = inter_area / area_i
209
+ ratio_j = inter_area / area_j
210
+
211
+ # To be considered typical parent-child nesting (and NOT occlusion), one node must be inside another,
212
+ # AND their centers must be closely aligned (i.e. concentric/enclosing layout).
213
+ # If one node overlaps/sits inside another but their centers are significantly offset (e.g. a right-aligned Floating Action Button covering the right end of a left-aligned list item), it is mutual occlusion!
214
+ center_i_x = (l_i + r_i) / 2.0
215
+ center_i_y = (t_i + bot_i) / 2.0
216
+ center_j_x = (l_j + r_j) / 2.0
217
+ center_j_y = (t_j + bot_j) / 2.0
218
+ center_dist = ((center_i_x - center_j_x) ** 2 + (center_i_y - center_j_y) ** 2) ** 0.5
219
+
220
+ max_dim = max(r_i - l_i, r_j - l_j, bot_i - t_i, bot_j - t_j)
221
+ is_concentric = center_dist < (max_dim * 0.20)
222
+
223
+ is_i_inside_j = (
224
+ (l_i >= l_j and t_i >= t_j and r_i <= r_j and bot_i <= bot_j)
225
+ and (area_j > area_i * 2.0)
226
+ and is_concentric
227
+ )
228
+ is_j_inside_i = (
229
+ (l_j >= l_i and t_j >= t_i and r_j <= r_i and bot_j <= bot_i)
230
+ and (area_i > area_j * 2.0)
231
+ and is_concentric
232
+ )
233
+ if is_i_inside_j or is_j_inside_i:
234
+ continue
235
+
236
+ if ratio_i >= 0.50 or ratio_j >= 0.50:
237
+ warnings_map.setdefault(idx_i, []).append(idx_j)
238
+ warnings_map.setdefault(idx_j, []).append(idx_i)
239
+
240
+ if not warnings_map:
241
+ return lines
242
+
243
+ new_lines = []
244
+ for line in lines:
245
+ match = re.match(r"^\[(\d+)\]", line)
246
+ if match:
247
+ el_idx = int(match.group(1))
248
+ if el_idx in warnings_map:
249
+ overlaps = sorted(list(set(warnings_map[el_idx])))
250
+ overlaps_str = " and ".join(f"[{o}]" for o in overlaps[:2])
251
+ if len(overlaps) > 2:
252
+ overlaps_str += f" (+{len(overlaps) - 2} more)"
253
+ line = f"{line} (WARNING: may overlap with {overlaps_str}, possible occlusion)"
254
+ new_lines.append(line)
255
+ return new_lines
256
+
257
+
258
+ def format_minimal_list_with_points(
259
+ fused_xml: list[dict[str, Any]], width: int = 1080, height: int = 2400
260
+ ) -> tuple[str, list[list[int]], list[str]]:
261
+ """Formats fused XML hierarchy into a minimal list with sequence numbers,
262
+
263
+ and returns physical coordinate points & labels for drawing.
264
+ """
265
+ lines = []
266
+ points = []
267
+ labels = []
268
+ idx = 1
269
+ added_items = [] # list of tuples: (text, cx, cy)
270
+ items_bounds = [] # list of tuples: (idx, (left, top, right, bottom))
271
+
272
+ def normalize_bounds(left: int, top: int, right: int, bottom: int) -> str:
273
+ n_left = int(max(0, min(1000, left * 1000 / width)))
274
+ n_top = int(max(0, min(1000, top * 1000 / height)))
275
+ n_right = int(max(0, min(1000, right * 1000 / width)))
276
+ n_bottom = int(max(0, min(1000, bottom * 1000 / height)))
277
+ return f"[{n_left},{n_top}][{n_right},{n_bottom}]"
278
+
279
+ def is_duplicate(text_val: str, cx: int, cy: int) -> bool:
280
+ text_clean = text_val.strip()
281
+ for ex_text, ex_cx, ex_cy in added_items:
282
+ if ex_text == text_clean:
283
+ dist = ((cx - ex_cx) ** 2 + (cy - ex_cy) ** 2) ** 0.5
284
+ if dist < 8.0:
285
+ return True
286
+ return False
287
+
288
+ for node in fused_xml:
289
+ text = node.get("text") or node.get("content-desc") or ""
290
+ bounds_str = node.get("bounds")
291
+ bounds = parse_bounds(bounds_str)
292
+
293
+ ocr_elements = node.get("ocr_elements")
294
+ registered = False
295
+
296
+ if ocr_elements:
297
+ for ocr in ocr_elements:
298
+ ocr_bounds = parse_bounds(ocr.get("bounds"))
299
+ if ocr_bounds:
300
+ left, top, right, bottom = ocr_bounds
301
+ cx, cy = get_center_coordinates(left, top, right, bottom)
302
+ if is_duplicate(ocr["text"], cx, cy):
303
+ continue
304
+
305
+ norm_bounds = normalize_bounds(left, top, right, bottom)
306
+ lines.append(f"[{idx}] OCR Text: '{ocr['text']}' | Bounds: {norm_bounds}")
307
+ items_bounds.append((idx, (left, top, right, bottom)))
308
+
309
+ points.append([cx, cy])
310
+ labels.append(str(idx))
311
+ added_items.append((ocr["text"].strip(), cx, cy))
312
+ idx += 1
313
+ registered = True
314
+
315
+ if not registered and text.strip() and bounds:
316
+ left, top, right, bottom = bounds
317
+ cx, cy = get_center_coordinates(left, top, right, bottom)
318
+ if is_duplicate(text, cx, cy):
319
+ continue
320
+
321
+ norm_bounds = normalize_bounds(left, top, right, bottom)
322
+ lines.append(f"[{idx}] Text: '{text.strip()}' | Bounds: {norm_bounds}")
323
+ items_bounds.append((idx, (left, top, right, bottom)))
324
+
325
+ points.append([cx, cy])
326
+ labels.append(str(idx))
327
+ added_items.append((text.strip(), cx, cy))
328
+ idx += 1
329
+
330
+ lines = _inject_mutual_occlusion_warnings(lines, items_bounds)
331
+ return "\n".join(lines), points, labels
332
+
333
+
334
+ def format_minimal_list_with_elements(
335
+ fused_xml: list[dict[str, Any]], width: int = 1080, height: int = 2400
336
+ ) -> tuple[str, list[dict[str, Any]], list[str]]:
337
+ """Formats fused XML hierarchy into a minimal list with sequence numbers,
338
+
339
+ and returns structured element metadata & labels for drawing/validation.
340
+ """
341
+ lines = []
342
+ elements = []
343
+ labels = []
344
+ idx = 1
345
+ added_items = [] # list of tuples: (text, cx, cy)
346
+ items_bounds = [] # list of tuples: (idx, (left, top, right, bottom))
347
+
348
+ def normalize_bounds(left: int, top: int, right: int, bottom: int) -> str:
349
+ n_left = int(max(0, min(1000, left * 1000 / width)))
350
+ n_top = int(max(0, min(1000, top * 1000 / height)))
351
+ n_right = int(max(0, min(1000, right * 1000 / width)))
352
+ n_bottom = int(max(0, min(1000, bottom * 1000 / height)))
353
+ return f"[{n_left},{n_top}][{n_right},{n_bottom}]"
354
+
355
+ def is_duplicate(text_val: str, cx: int, cy: int) -> bool:
356
+ text_clean = text_val.strip()
357
+ for ex_text, ex_cx, ex_cy in added_items:
358
+ if ex_text == text_clean:
359
+ dist = ((cx - ex_cx) ** 2 + (cy - ex_cy) ** 2) ** 0.5
360
+ if dist < 8.0:
361
+ return True
362
+ return False
363
+
364
+ for node in fused_xml:
365
+ text = node.get("text") or node.get("content-desc") or ""
366
+ bounds_str = node.get("bounds")
367
+ bounds = parse_bounds(bounds_str)
368
+
369
+ ocr_elements = node.get("ocr_elements")
370
+ registered = False
371
+
372
+ if ocr_elements:
373
+ for ocr in ocr_elements:
374
+ ocr_bounds = parse_bounds(ocr.get("bounds"))
375
+ if ocr_bounds:
376
+ left, top, right, bottom = ocr_bounds
377
+ cx, cy = get_center_coordinates(left, top, right, bottom)
378
+ if is_duplicate(ocr["text"], cx, cy):
379
+ continue
380
+
381
+ norm_bounds = normalize_bounds(left, top, right, bottom)
382
+ lines.append(f"[{idx}] OCR Text: '{ocr['text']}' | Bounds: {norm_bounds}")
383
+ items_bounds.append((idx, (left, top, right, bottom)))
384
+
385
+ elements.append(
386
+ {
387
+ "index": idx,
388
+ "center": [cx, cy],
389
+ "text": ocr["text"].strip(),
390
+ "bounds": [left, top, right, bottom],
391
+ "class": node.get("class"),
392
+ "resource_id": node.get("resource-id"),
393
+ "is_ocr": True,
394
+ }
395
+ )
396
+ labels.append(str(idx))
397
+ added_items.append((ocr["text"].strip(), cx, cy))
398
+ idx += 1
399
+ registered = True
400
+
401
+ # An empty input field carries no text but usually a hint ("Search settings");
402
+ # a rejected one carries an error ("Password too short"). Both come from the
403
+ # accessibility helper's dump and are exactly what the agent needs to see.
404
+ hint = str(node.get("hint") or "").strip()
405
+ error = str(node.get("error") or "").strip()
406
+ shown = text.strip() or hint
407
+ if not registered and shown and bounds:
408
+ left, top, right, bottom = bounds
409
+ cx, cy = get_center_coordinates(left, top, right, bottom)
410
+ if is_duplicate(shown, cx, cy):
411
+ continue
412
+
413
+ norm_bounds = normalize_bounds(left, top, right, bottom)
414
+ kind = "Text" if text.strip() else "Hint"
415
+ line = f"[{idx}] {kind}: '{shown}' | Bounds: {norm_bounds}"
416
+ if error:
417
+ line += f" | Error: '{error}'"
418
+ lines.append(line)
419
+ items_bounds.append((idx, (left, top, right, bottom)))
420
+
421
+ element = {
422
+ "index": idx,
423
+ "center": [cx, cy],
424
+ "text": shown,
425
+ "bounds": [left, top, right, bottom],
426
+ "class": node.get("class"),
427
+ "resource_id": node.get("resource-id"),
428
+ "is_ocr": False,
429
+ }
430
+ if not text.strip():
431
+ element["is_hint"] = True
432
+ if error:
433
+ element["error"] = error
434
+ elements.append(element)
435
+ labels.append(str(idx))
436
+ added_items.append((shown, cx, cy))
437
+ idx += 1
438
+
439
+ lines = _inject_mutual_occlusion_warnings(lines, items_bounds)
440
+ return "\n".join(lines), elements, labels
441
+
442
+
443
+ def crop_and_annotate_target(
444
+ image_bytes: bytes,
445
+ coords: list[int],
446
+ crop_size: int | None = 256,
447
+ dot_radius: int = 8,
448
+ ) -> bytes:
449
+ """Crops a square region centered around coords, and draws a red dot with black outline at coords.
450
+
451
+ If crop_size is None, returns the full annotated image.
452
+ """
453
+ img = Image.open(io.BytesIO(image_bytes))
454
+ width, height = img.size
455
+ x, y = coords
456
+
457
+ # Ensure coordinates are within image boundaries
458
+ x = max(0, min(x, width - 1))
459
+ y = max(0, min(y, height - 1))
460
+
461
+ if crop_size is not None:
462
+ # Calculate crop boundaries (centered around x, y; clamped to image boundaries)
463
+ w_crop = min(crop_size, width)
464
+ h_crop = min(crop_size, height)
465
+
466
+ x_min = max(0, min(x - w_crop // 2, width - w_crop))
467
+ y_min = max(0, min(y - h_crop // 2, height - h_crop))
468
+ x_max = x_min + w_crop
469
+ y_max = y_min + h_crop
470
+
471
+ # Crop
472
+ cropped_img = img.crop((x_min, y_min, x_max, y_max))
473
+
474
+ # Local coordinates within the cropped frame
475
+ local_x = x - x_min
476
+ local_y = y - y_min
477
+ else:
478
+ cropped_img = img
479
+ local_x = x
480
+ local_y = y
481
+
482
+ # Draw solid red circle with a black outline
483
+ draw = ImageDraw.Draw(cropped_img)
484
+ draw.ellipse(
485
+ [
486
+ local_x - dot_radius,
487
+ local_y - dot_radius,
488
+ local_x + dot_radius,
489
+ local_y + dot_radius,
490
+ ],
491
+ fill="red",
492
+ outline="black",
493
+ width=2 if crop_size is None else 1,
494
+ )
495
+
496
+ out_buf = io.BytesIO()
497
+ if crop_size is None:
498
+ if cropped_img.mode != "RGB":
499
+ cropped_img = cropped_img.convert("RGB")
500
+ cropped_img.save(out_buf, format="JPEG", quality=85)
501
+ else:
502
+ cropped_img.save(out_buf, format="PNG")
503
+ return out_buf.getvalue()
504
+
505
+
506
+ def _resolve_coordinates(coord: Any, width: int, height: int) -> tuple[int, int] | None:
507
+ """Helper to safely parse coordinates into pixel space."""
508
+ if not isinstance(coord, (list, tuple)) or len(coord) < 2:
509
+ return None
510
+ try:
511
+ x, y = float(coord[0]), float(coord[1])
512
+ # If coordinates are normalized (0.0 <= x <= 1.0)
513
+ if 0.0 <= x <= 1.0 and 0.0 <= y <= 1.0:
514
+ px = int(round(x * width))
515
+ py = int(round(y * height))
516
+ elif 0 <= x <= 1000 and 0 <= y <= 1000 and (width > 1000 or height > 1000):
517
+ # Android normalized 1000 coordinate space
518
+ px = int(round(x * width / 1000.0))
519
+ py = int(round(y * height / 1000.0))
520
+ else:
521
+ px = int(round(x))
522
+ py = int(round(y))
523
+ px = max(0, min(width - 1, px))
524
+ py = max(0, min(height - 1, py))
525
+ return px, py
526
+ except Exception:
527
+ return None
528
+
529
+
530
+ def draw_action_overlay_on_image(
531
+ image_bytes: bytes,
532
+ action_name: str,
533
+ action_args: dict[str, Any],
534
+ ) -> bytes:
535
+ """Draws visual action indicators (tap ripples, sequence numbers, swipe arrows, focus boxes)
536
+
537
+ on top of the before-action screenshot.
538
+
539
+ Args:
540
+ image_bytes: Raw JPEG/PNG image bytes of the screenshot.
541
+ action_name: Action name (e.g. 'click', 'click_sequence', 'swipe', 'input_text').
542
+ action_args: Action parameters dictionary.
543
+
544
+ Returns:
545
+ Annotated JPEG image bytes, or original image_bytes on error.
546
+ """
547
+ import math
548
+
549
+ if not image_bytes:
550
+ return image_bytes
551
+
552
+ try:
553
+ img = Image.open(io.BytesIO(image_bytes))
554
+ if img.mode in ("RGBA", "P"):
555
+ img = img.convert("RGB")
556
+ width, height = img.size
557
+ draw = ImageDraw.Draw(img)
558
+
559
+ # Scale indicator sizes dynamically based on screen resolution
560
+ base_dim = min(width, height)
561
+ dot_radius = max(18, int(base_dim * 0.025))
562
+ line_width = max(4, int(base_dim * 0.006))
563
+
564
+ action = (action_name or "").lower()
565
+
566
+ # 1. Single Click / Tap
567
+ if action in ("click", "tap", "click_coordinate", "long_click", "press"):
568
+ coord = (
569
+ action_args.get("target")
570
+ or action_args.get("coordinates")
571
+ or action_args.get("point")
572
+ )
573
+ pt = _resolve_coordinates(coord, width, height)
574
+ if pt:
575
+ x, y = pt
576
+ # Outer white aura
577
+ draw.ellipse(
578
+ [
579
+ x - dot_radius * 1.5,
580
+ y - dot_radius * 1.5,
581
+ x + dot_radius * 1.5,
582
+ y + dot_radius * 1.5,
583
+ ],
584
+ outline="white",
585
+ width=line_width + 2,
586
+ )
587
+ # Red ripple circle
588
+ draw.ellipse(
589
+ [x - dot_radius, y - dot_radius, x + dot_radius, y + dot_radius],
590
+ fill="red",
591
+ outline="white",
592
+ width=line_width,
593
+ )
594
+ # Crosshair
595
+ cross_len = int(dot_radius * 1.8)
596
+ draw.line([(x - cross_len, y), (x + cross_len, y)], fill="white", width=line_width)
597
+ draw.line([(x, y - cross_len), (x, y + cross_len)], fill="white", width=line_width)
598
+
599
+ # 2. Click Sequence
600
+ elif action in ("click_sequence", "tap_sequence"):
601
+ sequence = (
602
+ action_args.get("sequence")
603
+ or action_args.get("targets")
604
+ or action_args.get("coordinates")
605
+ or []
606
+ )
607
+ if isinstance(sequence, str):
608
+ sequence_str = sequence.strip()
609
+ try:
610
+ import json
611
+
612
+ sequence = json.loads(sequence_str)
613
+ except Exception:
614
+ try:
615
+ import ast
616
+
617
+ sequence = ast.literal_eval(sequence_str)
618
+ except (ValueError, TypeError, SyntaxError):
619
+ # Unparseable sequence string: leave it as-is; the
620
+ # isinstance check below skips non-list values.
621
+ pass
622
+
623
+ if isinstance(sequence, list):
624
+ pts = []
625
+ for coord in sequence:
626
+ pt = _resolve_coordinates(coord, width, height)
627
+ if pt:
628
+ pts.append(pt)
629
+
630
+ # Draw connecting lines/arrows between consecutive sequence points
631
+ for i in range(len(pts) - 1):
632
+ p1, p2 = pts[i], pts[i + 1]
633
+ draw.line([p1, p2], fill="white", width=line_width + 3)
634
+ draw.line([p1, p2], fill="red", width=line_width)
635
+
636
+ # Arrow head
637
+ dx = p2[0] - p1[0]
638
+ dy = p2[1] - p1[1]
639
+ angle = math.atan2(dy, dx)
640
+ arrow_len = max(16, int(base_dim * 0.025))
641
+ arrow_angle = math.pi / 6
642
+ x1 = p2[0] - arrow_len * math.cos(angle - arrow_angle)
643
+ y1 = p2[1] - arrow_len * math.sin(angle - arrow_angle)
644
+ x2 = p2[0] - arrow_len * math.cos(angle + arrow_angle)
645
+ y2 = p2[1] - arrow_len * math.sin(angle + arrow_angle)
646
+ draw.polygon([p2, (x1, y1), (x2, y2)], fill="red", outline="white")
647
+
648
+ # Draw numbered circular touch points
649
+ for idx, (x, y) in enumerate(pts, 1):
650
+ # Outer aura
651
+ draw.ellipse(
652
+ [
653
+ x - dot_radius * 1.3,
654
+ y - dot_radius * 1.3,
655
+ x + dot_radius * 1.3,
656
+ y + dot_radius * 1.3,
657
+ ],
658
+ outline="white",
659
+ width=line_width + 1,
660
+ )
661
+ # Colored circle
662
+ draw.ellipse(
663
+ [x - dot_radius, y - dot_radius, x + dot_radius, y + dot_radius],
664
+ fill="red",
665
+ outline="white",
666
+ width=line_width,
667
+ )
668
+ label = str(idx)
669
+ try:
670
+ font = ImageFont.load_default(size=int(dot_radius * 1.2))
671
+ draw.text(
672
+ (x - dot_radius // 3, y - dot_radius // 2),
673
+ label,
674
+ fill="white",
675
+ font=font,
676
+ stroke_width=2,
677
+ stroke_fill="black",
678
+ )
679
+ except Exception:
680
+ draw.text(
681
+ (x - 4, y - 6),
682
+ label,
683
+ fill="white",
684
+ )
685
+
686
+ # 3. Swipe / Drag / Scroll
687
+ elif action in ("swipe", "drag", "scroll"):
688
+ kind, target, _ = parse_swipe_parameters(action_args)
689
+ start_pt, end_pt = None, None
690
+
691
+ if kind == "coords" and isinstance(target, list) and len(target) == 4:
692
+ start_pt = _resolve_coordinates(target[:2], width, height)
693
+ end_pt = _resolve_coordinates(target[2:], width, height)
694
+ elif kind == "direction" and isinstance(target, str):
695
+ direction = target.lower()
696
+ cx, cy = width // 2, height // 2
697
+ dist_y = int(height * 0.35)
698
+ dist_x = int(width * 0.35)
699
+ if "up" in direction:
700
+ start_pt = (cx, cy + dist_y // 2)
701
+ end_pt = (cx, cy - dist_y // 2)
702
+ elif "down" in direction:
703
+ start_pt = (cx, cy - dist_y // 2)
704
+ end_pt = (cx, cy + dist_y // 2)
705
+ elif "left" in direction:
706
+ start_pt = (cx + dist_x // 2, cy)
707
+ end_pt = (cx - dist_x // 2, cy)
708
+ elif "right" in direction:
709
+ start_pt = (cx - dist_x // 2, cy)
710
+ end_pt = (cx + dist_x // 2, cy)
711
+
712
+ if start_pt and end_pt:
713
+ sx, sy = start_pt
714
+ ex, ey = end_pt
715
+
716
+ draw.ellipse(
717
+ [
718
+ sx - dot_radius // 2,
719
+ sy - dot_radius // 2,
720
+ sx + dot_radius // 2,
721
+ sy + dot_radius // 2,
722
+ ],
723
+ fill="yellow",
724
+ outline="black",
725
+ width=2,
726
+ )
727
+ draw.line([(sx, sy), (ex, ey)], fill="white", width=line_width + 4)
728
+ draw.line([(sx, sy), (ex, ey)], fill="red", width=line_width)
729
+
730
+ dx = ex - sx
731
+ dy = ey - sy
732
+ angle = math.atan2(dy, dx)
733
+ arrow_len = max(24, int(base_dim * 0.04))
734
+ arrow_angle = math.pi / 6
735
+
736
+ x1 = ex - arrow_len * math.cos(angle - arrow_angle)
737
+ y1 = ey - arrow_len * math.sin(angle - arrow_angle)
738
+ x2 = ex - arrow_len * math.cos(angle + arrow_angle)
739
+ y2 = ey - arrow_len * math.sin(angle + arrow_angle)
740
+
741
+ draw.polygon([(ex, ey), (x1, y1), (x2, y2)], fill="red", outline="white")
742
+
743
+ # 4. Input Text
744
+ elif action in ("input_text", "type"):
745
+ coord = action_args.get("target") or action_args.get("coordinates")
746
+ if coord:
747
+ pt = _resolve_coordinates(coord, width, height)
748
+ if pt:
749
+ x, y = pt
750
+ box_w = max(60, int(base_dim * 0.15))
751
+ box_h = max(30, int(base_dim * 0.06))
752
+ draw.rectangle(
753
+ [x - box_w, y - box_h, x + box_w, y + box_h],
754
+ outline="cyan",
755
+ width=line_width,
756
+ )
757
+
758
+ out_buf = io.BytesIO()
759
+ img.save(out_buf, format="JPEG", quality=85)
760
+ return out_buf.getvalue()
761
+
762
+ except Exception:
763
+ return image_bytes
764
+
765
+
766
+ def overlay_action_on_screenshot(image_bytes: bytes, action_obj: Any) -> bytes | None:
767
+ """Draws a recorded step action (as stored in ``action_taken``) onto the
768
+ step's before-action screenshot.
769
+
770
+ Accepts the raw stored action (a dict, or a burst list whose first member
771
+ is used), maps the usual coordinate keys into the drawing arguments and
772
+ returns the annotated JPEG bytes. Returns ``None`` when there is nothing to
773
+ draw (no action, no coordinates, or the drawing left the image unchanged).
774
+ Shared by the Checker's ``get_step_screenshot(which="overlay")`` and the
775
+ MCP trace inspector so both produce the same picture.
776
+ """
777
+ if not image_bytes:
778
+ return None
779
+ action = action_obj[0] if isinstance(action_obj, list) and action_obj else action_obj
780
+ if not isinstance(action, dict):
781
+ return None
782
+ act_name = action.get("action") or action.get("name") or ""
783
+ if not act_name:
784
+ return None
785
+ action_args = dict(action.get("args") or {})
786
+ for key in (
787
+ "target",
788
+ "coordinates",
789
+ "point",
790
+ "direction",
791
+ "sequence",
792
+ "targets",
793
+ "normalized_coordinates",
794
+ ):
795
+ if key in action and key not in action_args:
796
+ action_args[key] = action[key]
797
+ annotated = draw_action_overlay_on_image(
798
+ image_bytes=image_bytes, action_name=act_name, action_args=action_args
799
+ )
800
+ if not annotated or annotated == image_bytes:
801
+ return None
802
+ return annotated