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,672 @@
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
+ """XML-hierarchy Safety Net: pre-execution validation of the action target.
16
+
17
+ Checks that the element the Operator decided to interact with is still present
18
+ on the live screen (via the UI hierarchy) and substantially consistent, with
19
+ coordinate self-healing for small drifts and a structured failure taxonomy
20
+ (shifted / occupied / disappeared) surfaced to the Operator as an execution incident.
21
+
22
+ Extracted from ``validator.py``; the public entry points remain the
23
+ ``ValidatorNode`` methods, which delegate here.
24
+ """
25
+
26
+ import asyncio
27
+ import difflib
28
+ import math
29
+ import re
30
+
31
+ from artemis.agents.validator.categories import ValidationErrorCategory
32
+ from artemis.constants import VALIDATOR_UI_HIERARCHY_TIMEOUT
33
+ from artemis.context import ArtemisContext
34
+ from artemis.graph.state import State
35
+ from artemis.utils import visualization
36
+ from third_party.mobile_use.utils.logger import get_logger
37
+
38
+ logger = get_logger(__name__)
39
+
40
+ # Only interactive, element-focused actions are subject to XML validation.
41
+ XML_VALIDATED_ACTIONS = (
42
+ "tap",
43
+ "long_press_on",
44
+ "focus_and_input_text",
45
+ "focus_and_clear_text",
46
+ )
47
+
48
+ # Signal weights for candidate scoring.
49
+ W_ID = 0.5
50
+ W_TEXT = 0.4
51
+ W_BOUNDS = 0.3
52
+ W_COORD = 0.3
53
+
54
+
55
+ async def validate_action_precondition(
56
+ node, session, action_item: dict, state: State | None = None
57
+ ) -> tuple[bool, ValidationErrorCategory, str]:
58
+ """Retry wrapper around the single-shot XML precondition validation.
59
+
60
+ A short retry loop accommodates screen transitions and loading delays.
61
+ ``node`` is the ValidatorNode; the single-shot check is dispatched through
62
+ it so tests can patch the method.
63
+ """
64
+ max_pre_attempts = 3
65
+ pre_retry_delay = 0.4
66
+
67
+ passed = False
68
+ category = ValidationErrorCategory.NONE
69
+ reason = ""
70
+
71
+ for pre_attempt in range(1, max_pre_attempts + 1):
72
+ passed, category, reason = await node._validate_action_precondition_single(
73
+ session, action_item, state
74
+ )
75
+ if passed:
76
+ return True, category, reason
77
+
78
+ if category == ValidationErrorCategory.XML_BYPASSED:
79
+ # Do not retry on XML bypass/timeout; return immediately
80
+ # so it falls back to Pixel-based VLM validation
81
+ return False, category, reason
82
+
83
+ if pre_attempt < max_pre_attempts:
84
+ logger.info(
85
+ "Pre-execution validation failed on attempt"
86
+ f" {pre_attempt}/{max_pre_attempts}: {reason}. Retrying in"
87
+ f" {pre_retry_delay}s..."
88
+ )
89
+ await asyncio.sleep(pre_retry_delay)
90
+ else:
91
+ break
92
+
93
+ return passed, category, reason
94
+
95
+
96
+ def _resolve_screen_dims(ctx: ArtemisContext, state: State | None) -> tuple[int, int]:
97
+ """Resolves the screen dimensions the Operator's coordinates refer to."""
98
+ width = 1080
99
+ height = 2400
100
+ operator_raw_data = getattr(state, "operator_raw_data", {}) or {}
101
+ w_raw = operator_raw_data.get("width")
102
+ h_raw = operator_raw_data.get("height")
103
+ if isinstance(w_raw, int) and isinstance(h_raw, int):
104
+ width = w_raw
105
+ height = h_raw
106
+ else:
107
+ device = getattr(ctx, "device", None)
108
+ if device:
109
+ width = getattr(device, "device_width", 1080)
110
+ height = getattr(device, "device_height", 2400)
111
+ return width, height
112
+
113
+
114
+ async def _fetch_live_elements(session):
115
+ """Pulls the live XML tree with a strict timeout.
116
+
117
+ Returns ``(elements, None)`` on success, or ``(None, (False, category,
118
+ reason))`` when the check should be bypassed to Pixel-based validation.
119
+ """
120
+ try:
121
+ try:
122
+ # The timeout must ride inside the session (MCP read timeout), never an
123
+ # outer asyncio.wait_for: cancelling call_tool mid-flight corrupts the
124
+ # in-memory transport and bricks the session for all later callers.
125
+ elements = await session.ui_hierarchy(timeout=VALIDATOR_UI_HIERARCHY_TIMEOUT)
126
+ except Exception as e:
127
+ logger.warning(
128
+ f"Failed to get live XML via MCP: {e!r}. Falling back to Pixel-based validation."
129
+ )
130
+ return None, (
131
+ False,
132
+ ValidationErrorCategory.XML_BYPASSED,
133
+ f"XML hierarchy fetch timed out or errored: {e!r}",
134
+ )
135
+
136
+ if not elements or not isinstance(elements, list):
137
+ logger.warning(
138
+ "Live hierarchy empty or invalid. Falling back to Pixel-based validation."
139
+ )
140
+ return None, (
141
+ False,
142
+ ValidationErrorCategory.XML_BYPASSED,
143
+ "Live hierarchy empty or invalid.",
144
+ )
145
+
146
+ except Exception as e:
147
+ logger.error(
148
+ f"Unexpected error during live XML fetch: {e}. Falling back to Pixel-based validation."
149
+ )
150
+ return None, (
151
+ False,
152
+ ValidationErrorCategory.XML_BYPASSED,
153
+ f"Unexpected error during live XML fetch: {e}",
154
+ )
155
+
156
+ return elements, None
157
+
158
+
159
+ def _normalize_text(s: str | None) -> str:
160
+ if not s:
161
+ return ""
162
+ s = s.lower()
163
+ s = re.sub(r"\(\d+\+?\)", "", s)
164
+ s = re.sub(r"\[\d+\+?\]", "", s)
165
+ s = re.sub(r"[^\w\s\d]", "", s)
166
+ return s.strip()
167
+
168
+
169
+ def _aggregate_text(elem: dict) -> str:
170
+ """Recursively aggregates text from a candidate container node."""
171
+ text = elem.get("text") or elem.get("content-desc") or elem.get("accessibilityText") or ""
172
+ if not isinstance(text, str):
173
+ text = str(text)
174
+ children = elem.get("children") or []
175
+ if children:
176
+ for child in children:
177
+ if isinstance(child, dict):
178
+ text += " " + _aggregate_text(child)
179
+ return text
180
+
181
+
182
+ def _text_similarity(norm_target_text: str, elem_norm_text: str) -> float:
183
+ """Scores text similarity between normalized target and element text."""
184
+ if not (norm_target_text and elem_norm_text):
185
+ return 0.0
186
+ if norm_target_text == elem_norm_text:
187
+ return 1.0
188
+ if norm_target_text in elem_norm_text or elem_norm_text in norm_target_text:
189
+ return 0.8
190
+ return difflib.SequenceMatcher(None, norm_target_text, elem_norm_text).ratio()
191
+
192
+
193
+ def _bounds_match(target_bounds, elem_bounds) -> tuple[float, bool]:
194
+ """Returns (IoU bounds score, size-mismatch flag) for the bounds pair."""
195
+ bounds_score = 0.0
196
+ size_mismatch = False
197
+ if target_bounds and elem_bounds and len(target_bounds) == 4 and len(elem_bounds) == 4:
198
+ l1, t1, r1, b1 = target_bounds
199
+ l2, t2, r2, b2 = elem_bounds
200
+
201
+ # Check size ratio mismatch to prevent matching small widget to giant container
202
+ w1, h1 = r1 - l1, b1 - t1
203
+ w2, h2 = r2 - l2, b2 - t2
204
+ if w1 > 0 and h1 > 0 and w2 > 0 and h2 > 0:
205
+ size_ratio_w = max(w1, w2) / min(w1, w2)
206
+ size_ratio_h = max(h1, h2) / min(h1, h2)
207
+ if size_ratio_w > 2.5 or size_ratio_h > 2.5:
208
+ size_mismatch = True
209
+
210
+ li = max(l1, l2)
211
+ ti = max(t1, t2)
212
+ ri = min(r1, r2)
213
+ bi = min(b1, b2)
214
+ if ri > li and bi > ti:
215
+ inter_area = (ri - li) * (bi - ti)
216
+ union_area = (r1 - l1) * (b1 - t1) + (r2 - l2) * (b2 - t2) - inter_area
217
+ if union_area > 0:
218
+ bounds_score = inter_area / union_area
219
+
220
+ return bounds_score, size_mismatch
221
+
222
+
223
+ def _combine_signals(
224
+ *,
225
+ target_resource_id,
226
+ target_text,
227
+ target_bounds,
228
+ original_coords,
229
+ id_match: bool,
230
+ elem_res_id: str,
231
+ norm_target_text: str,
232
+ elem_norm_text: str,
233
+ text_score: float,
234
+ bounds_score: float,
235
+ contains_coord: bool,
236
+ ) -> tuple[float, float]:
237
+ """Combines the weighted match signals into (score, identity_score)."""
238
+ signals = []
239
+ weights = []
240
+ identity_signals = []
241
+ identity_weights = []
242
+
243
+ # 1. Resource ID Signal
244
+ if target_resource_id:
245
+ weights.append(W_ID)
246
+ identity_weights.append(W_ID)
247
+ if id_match:
248
+ signals.append(W_ID * 1.0)
249
+ identity_signals.append(W_ID * 1.0)
250
+ elif not elem_res_id:
251
+ signals.append(W_ID * 0.3) # Soft penalty for missing ID
252
+ identity_signals.append(W_ID * 0.3)
253
+ else:
254
+ signals.append(W_ID * -0.5) # Hard penalty for mismatched ID
255
+ identity_signals.append(W_ID * -0.5)
256
+
257
+ # 2. Text Signal
258
+ if target_text:
259
+ weights.append(W_TEXT)
260
+ identity_weights.append(W_TEXT)
261
+ if norm_target_text and elem_norm_text:
262
+ signals.append(W_TEXT * text_score)
263
+ identity_signals.append(W_TEXT * text_score)
264
+ elif not elem_norm_text:
265
+ signals.append(W_TEXT * 0.0) # Penalty for missing expected text
266
+ identity_signals.append(W_TEXT * 0.0)
267
+
268
+ # 3. Bounds Signal
269
+ if target_bounds:
270
+ weights.append(W_BOUNDS)
271
+ signals.append(W_BOUNDS * bounds_score)
272
+
273
+ # 4. Coordinate Containment Signal
274
+ if original_coords:
275
+ weights.append(W_COORD)
276
+ signals.append(W_COORD * (1.0 if contains_coord else 0.0))
277
+
278
+ score = sum(signals) / sum(weights) if weights else 1.0
279
+ identity_score = sum(identity_signals) / sum(identity_weights) if identity_weights else 1.0
280
+ return score, identity_score
281
+
282
+
283
+ def _score_element(
284
+ elem: dict,
285
+ action_item: dict,
286
+ *,
287
+ target_text,
288
+ target_bounds,
289
+ target_resource_id,
290
+ norm_target_text: str,
291
+ ) -> dict | None:
292
+ """Scores one hierarchy element against the action target metadata.
293
+
294
+ Returns a candidate dict, or None if the element has no usable bounds.
295
+ """
296
+ elem_bounds_str = elem.get("bounds")
297
+ elem_bounds = visualization.parse_bounds(elem_bounds_str)
298
+ if not elem_bounds:
299
+ return None
300
+
301
+ elem_raw_text = _aggregate_text(elem)
302
+ elem_norm_text = _normalize_text(elem_raw_text)
303
+ elem_res_id = elem.get("resource-id") or elem.get("resourceId") or ""
304
+
305
+ elem_cx, elem_cy = visualization.get_center_coordinates(*elem_bounds)
306
+
307
+ original_coords = action_item.get("coordinates") or [0, 0]
308
+ dist = math.sqrt((elem_cx - original_coords[0]) ** 2 + (elem_cy - original_coords[1]) ** 2)
309
+
310
+ id_match = target_resource_id and elem_res_id and target_resource_id == elem_res_id
311
+
312
+ text_score = _text_similarity(norm_target_text, elem_norm_text)
313
+ bounds_score, size_mismatch = _bounds_match(target_bounds, elem_bounds)
314
+
315
+ # Calculate Coordinate Containment
316
+ contains_coord = False
317
+ if original_coords and len(original_coords) == 2 and elem_bounds and len(elem_bounds) == 4:
318
+ cx, cy = original_coords
319
+ l2, t2, r2, b2 = elem_bounds
320
+ contains_coord = l2 <= cx <= r2 and t2 <= cy <= b2
321
+
322
+ # Apply size mismatch penalty
323
+ if size_mismatch and not id_match:
324
+ text_score = 0.0
325
+ bounds_score = 0.0
326
+ contains_coord = False
327
+
328
+ score, identity_score = _combine_signals(
329
+ target_resource_id=target_resource_id,
330
+ target_text=target_text,
331
+ target_bounds=target_bounds,
332
+ original_coords=original_coords,
333
+ id_match=id_match,
334
+ elem_res_id=elem_res_id,
335
+ norm_target_text=norm_target_text,
336
+ elem_norm_text=elem_norm_text,
337
+ text_score=text_score,
338
+ bounds_score=bounds_score,
339
+ contains_coord=contains_coord,
340
+ )
341
+
342
+ decay = max(0.5, 1.0 - (dist / 800.0))
343
+ score *= decay
344
+
345
+ return {
346
+ "element": elem,
347
+ "center": [elem_cx, elem_cy],
348
+ "bounds": elem_bounds,
349
+ "text": elem_raw_text,
350
+ "resource_id": elem_res_id,
351
+ "score": score,
352
+ "identity_score": identity_score,
353
+ "distance": dist,
354
+ "text_score": text_score,
355
+ "id_match": id_match,
356
+ }
357
+
358
+
359
+ def _handle_match_success(
360
+ action_item: dict, best: dict, target_text, scale_factor: float
361
+ ) -> tuple[bool, ValidationErrorCategory, str]:
362
+ """Handles a passing match: self-heals coordinates for moderate drifts."""
363
+ if best["distance"] <= 200 * scale_factor:
364
+ new_coords = best["center"]
365
+ old_coords = action_item.get("coordinates")
366
+ if old_coords != new_coords:
367
+ logger.success(
368
+ "Pre-execution validation SUCCESS"
369
+ f" (score={best['score']:.2f}). Target element"
370
+ f" '{target_text}' shifted by {best['distance']:.1f}px."
371
+ f" Self-healing: correcting coordinates {old_coords} ->"
372
+ f" {new_coords}."
373
+ )
374
+ action_item["coordinates"] = new_coords
375
+ # Drop the now-stale normalized coordinates so the canonical
376
+ # translation re-derives them from the healed pixel center.
377
+ action_item.pop("normalized_coordinates", None)
378
+ else:
379
+ logger.info(
380
+ "Pre-execution validation SUCCESS"
381
+ f" (score={best['score']:.2f}). Target element matched but"
382
+ f" shifted by {best['distance']:.1f}px"
383
+ f" (>{200 * scale_factor:.1f}px). Bypassing coordinate"
384
+ " self-healing to remain conservative."
385
+ )
386
+ return True, ValidationErrorCategory.NONE, ""
387
+
388
+
389
+ def _is_valid_shift(best: dict, scale_factor: float) -> bool:
390
+ """Applies the strict shift criteria (preferring disappeared over shifted).
391
+
392
+ We must be conservative to avoid confusing disappearance as shift. Shift
393
+ misclassification as disappeared is acceptable, but disappeared as shift
394
+ causes errors.
395
+ """
396
+ max_allowed_distance = 100.0 * scale_factor
397
+ if best.get("id_match", False):
398
+ # Allow larger distance threshold only for strong resource ID matches
399
+ max_allowed_distance = 300.0 * scale_factor
400
+
401
+ if best["distance"] <= max_allowed_distance:
402
+ if best["identity_score"] >= 0.85:
403
+ # High confidence identity match (e.g. exact text or exact ID)
404
+ return True
405
+ if best["identity_score"] >= 0.5 and best.get("id_match", False):
406
+ # Moderate confidence with exact resource ID match
407
+ return True
408
+ return False
409
+
410
+
411
+ def _element_is_interactive(e: dict) -> bool:
412
+ clickable = e.get("clickable")
413
+ long_clickable = e.get("long-clickable") or e.get("longClickable")
414
+ focusable = e.get("focusable")
415
+ enabled = e.get("enabled")
416
+
417
+ def to_bool(v) -> bool:
418
+ if v is None:
419
+ return False
420
+ if isinstance(v, bool):
421
+ return v
422
+ return str(v).lower() == "true"
423
+
424
+ return to_bool(clickable) or to_bool(long_clickable) or to_bool(focusable) or to_bool(enabled)
425
+
426
+
427
+ def _find_occupant(elements: list, original_coords) -> tuple | None:
428
+ """Finds the most specific element containing the expected coordinates."""
429
+ occupant = None
430
+ if original_coords and len(original_coords) == 2:
431
+ orig_cx, orig_cy = original_coords
432
+ for elem in elements:
433
+ if not isinstance(elem, dict):
434
+ continue
435
+ elem_bounds_str = elem.get("bounds")
436
+ elem_bounds = visualization.parse_bounds(elem_bounds_str)
437
+ if elem_bounds:
438
+ left, top, right, bottom = elem_bounds
439
+ if left <= orig_cx <= right and top <= orig_cy <= bottom:
440
+ elem_area = (right - left) * (bottom - top)
441
+ if not occupant or elem_area < occupant[2]:
442
+ occupant = (elem, elem_bounds, elem_area)
443
+ return occupant
444
+
445
+
446
+ def _coords_hit_interactive_element(
447
+ elements: list, original_coords, width: int, height: int
448
+ ) -> bool:
449
+ """True if any non-full-screen interactive element covers the coordinates."""
450
+ if not (original_coords and len(original_coords) == 2):
451
+ return False
452
+ orig_cx, orig_cy = original_coords
453
+ for e in elements:
454
+ if not isinstance(e, dict):
455
+ continue
456
+ eb_str = e.get("bounds")
457
+ eb = visualization.parse_bounds(eb_str)
458
+ if eb:
459
+ e_left, e_top, e_right, e_bottom = eb
460
+ if e_left <= orig_cx <= e_right and e_top <= orig_cy <= e_bottom:
461
+ # Exclude full-screen root background elements
462
+ e_width = e_right - e_left
463
+ e_height = e_bottom - e_top
464
+ is_full_screen = e_width >= width - 10 and e_height >= height - 10
465
+
466
+ if _element_is_interactive(e) and not is_full_screen:
467
+ return True
468
+ return False
469
+
470
+
471
+ def _describe_occupation(
472
+ elements: list,
473
+ occupant: tuple | None,
474
+ original_coords,
475
+ target_bounds,
476
+ width: int,
477
+ height: int,
478
+ ) -> tuple[bool, str, list | None]:
479
+ """Decides whether the expected coordinates are occupied by another element.
480
+
481
+ Returns (is_occupied, occupant_desc, occupant_bounds).
482
+ """
483
+ is_occupied = False
484
+ occupant_desc = "anonymous element"
485
+ occupant_bounds = None
486
+
487
+ if occupant:
488
+ elem, elem_bounds, _ = occupant
489
+ occupant_text = (
490
+ elem.get("text") or elem.get("content-desc") or elem.get("accessibilityText") or ""
491
+ )
492
+ occupant_res_id = elem.get("resource-id") or elem.get("resourceId") or ""
493
+ occupant_bounds = elem_bounds
494
+
495
+ # Check if the occupant has content
496
+ has_content = bool(occupant_text or occupant_res_id)
497
+
498
+ # Check if this occupant or any container covering this point is interactive
499
+ coords_interactive = _coords_hit_interactive_element(
500
+ elements, original_coords, width, height
501
+ )
502
+
503
+ # Exclude full-screen or giant parent containers from being considered blockers
504
+ is_occupant_full_screen = False
505
+ if occupant_bounds:
506
+ o_w = occupant_bounds[2] - occupant_bounds[0]
507
+ o_h = occupant_bounds[3] - occupant_bounds[1]
508
+ is_occupant_full_screen = o_w >= width - 10 and o_h >= height - 10
509
+
510
+ # If target bounds are known, check if occupant is a giant container
511
+ if target_bounds:
512
+ t_w = target_bounds[2] - target_bounds[0]
513
+ t_h = target_bounds[3] - target_bounds[1]
514
+ t_area = t_w * t_h
515
+ o_area = o_w * o_h
516
+ if t_area > 0 and o_area > 3.0 * t_area:
517
+ is_occupant_full_screen = True
518
+
519
+ if (has_content or coords_interactive) and not is_occupant_full_screen:
520
+ is_occupied = True
521
+ occupant_desc = (
522
+ f"'{occupant_text}' ({occupant_res_id})"
523
+ if has_content
524
+ else "interactive anonymous element"
525
+ )
526
+
527
+ return is_occupied, occupant_desc, occupant_bounds
528
+
529
+
530
+ def _classify_failure(
531
+ elements: list,
532
+ action_item: dict,
533
+ best: dict,
534
+ *,
535
+ target_text,
536
+ target_bounds,
537
+ target_resource_id,
538
+ width: int,
539
+ height: int,
540
+ scale_factor: float,
541
+ ) -> tuple[bool, ValidationErrorCategory, str]:
542
+ """Classifies a below-threshold match as shifted / occupied / disappeared."""
543
+ original_coords = action_item.get("coordinates")
544
+
545
+ # 1. Shifted Case
546
+ if _is_valid_shift(best, scale_factor):
547
+ reason = (
548
+ f"Target element '{target_text}' ({target_resource_id})"
549
+ f" expected at {original_coords} (bounds: {target_bounds})"
550
+ f" has shifted.\n- New location: {best['center']} (bounds:"
551
+ f" {best['bounds']})"
552
+ )
553
+ # Structured facts for the execution incident handed to the Operator.
554
+ action_item["safety_net_evidence"] = {
555
+ "new_center": list(best["center"]),
556
+ "new_bounds": best.get("bounds"),
557
+ }
558
+ return False, ValidationErrorCategory.TARGET_SHIFTED, reason
559
+
560
+ # 2. Occupied Case
561
+ occupant = _find_occupant(elements, original_coords)
562
+ is_occupied, occupant_desc, occupant_bounds = _describe_occupation(
563
+ elements, occupant, original_coords, target_bounds, width, height
564
+ )
565
+ if is_occupied:
566
+ reason = (
567
+ f"The expected position {original_coords} for"
568
+ f" '{target_text}' ({target_resource_id}) is"
569
+ " occupied/intercepted by a different element:"
570
+ f" {occupant_desc} at bounds {occupant_bounds}."
571
+ )
572
+ action_item["safety_net_evidence"] = {
573
+ "occupant": occupant_desc,
574
+ "occupant_bounds": occupant_bounds,
575
+ }
576
+ return False, ValidationErrorCategory.TARGET_OCCUPIED, reason
577
+
578
+ # 3. Completely Missing Case
579
+ reason = (
580
+ f"Target element '{target_text}' ({target_resource_id})"
581
+ f" expected at {original_coords} (bounds:"
582
+ f" {target_bounds}) was not found on the screen."
583
+ )
584
+ return False, ValidationErrorCategory.TARGET_DISAPPEARED, reason
585
+
586
+
587
+ async def validate_action_precondition_single(
588
+ ctx: ArtemisContext, session, action_item: dict, state: State | None = None
589
+ ) -> tuple[bool, ValidationErrorCategory, str]:
590
+ """Single-shot Safety Net check against the live XML hierarchy.
591
+
592
+ Returns (True, ValidationErrorCategory.NONE, "") if validation passes or
593
+ is skipped. Returns (False, category, error_msg) if validation fails.
594
+ """
595
+ action_name = action_item.get("action")
596
+
597
+ # 1. White-list filtering: Only validate interactive, element-focused actions
598
+ if action_name not in XML_VALIDATED_ACTIONS:
599
+ return True, ValidationErrorCategory.NONE, ""
600
+
601
+ target_text = action_item.get("target_text")
602
+ target_bounds = action_item.get("target_bounds") # [l, t, r, b]
603
+ target_resource_id = action_item.get("target_resource_id")
604
+
605
+ width, height = _resolve_screen_dims(ctx, state)
606
+
607
+ # Calculate diagonal-based scale factor for resolution-independent thresholds
608
+ ref_diagonal = math.sqrt(1080**2 + 2400**2) # ~2631.8
609
+ current_diagonal = math.sqrt(width**2 + height**2)
610
+ scale_factor = current_diagonal / ref_diagonal
611
+
612
+ # 2. Skip validation if there's no target metadata to match against
613
+ if not target_text and not target_bounds and not target_resource_id:
614
+ return True, ValidationErrorCategory.NONE, ""
615
+
616
+ logger.info(
617
+ f"Pre-execution validation: checking target '{target_text}'"
618
+ f" ({target_resource_id}) at {target_bounds}..."
619
+ )
620
+
621
+ # 3. Pull live XML tree with strict timeout to avoid hanging the execution flow
622
+ elements, bypass = await _fetch_live_elements(session)
623
+ if bypass is not None:
624
+ return bypass
625
+
626
+ # 4. Matching Heuristics & Scoring
627
+ norm_target_text = _normalize_text(target_text)
628
+
629
+ candidates = []
630
+ for elem in elements:
631
+ if not isinstance(elem, dict):
632
+ continue
633
+ candidate = _score_element(
634
+ elem,
635
+ action_item,
636
+ target_text=target_text,
637
+ target_bounds=target_bounds,
638
+ target_resource_id=target_resource_id,
639
+ norm_target_text=norm_target_text,
640
+ )
641
+ if candidate is not None:
642
+ candidates.append(candidate)
643
+
644
+ if not candidates:
645
+ logger.warning(
646
+ "No candidates with valid bounds found. Falling back to Pixel-based validation."
647
+ )
648
+ return (
649
+ False,
650
+ ValidationErrorCategory.XML_BYPASSED,
651
+ "No candidates with valid bounds found.",
652
+ )
653
+
654
+ candidates.sort(key=lambda x: x["score"], reverse=True)
655
+ best = candidates[0]
656
+
657
+ threshold = 0.55 if best["distance"] <= 150 * scale_factor else 0.75
658
+
659
+ if best["score"] >= threshold:
660
+ return _handle_match_success(action_item, best, target_text, scale_factor)
661
+
662
+ return _classify_failure(
663
+ elements,
664
+ action_item,
665
+ best,
666
+ target_text=target_text,
667
+ target_bounds=target_bounds,
668
+ target_resource_id=target_resource_id,
669
+ width=width,
670
+ height=height,
671
+ scale_factor=scale_factor,
672
+ )