android-midscene-automation 0.2.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +35 -0
  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,37 @@
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
+ """Text extraction and manipulation utilities."""
16
+
17
+ from typing import Any
18
+
19
+
20
+ def safe_extract_text(content: Any) -> str:
21
+ """Safely extracts a flat string from potentially complex or nested LLM response content.
22
+
23
+ Guarantees returning a string object to prevent AttributeError when calling
24
+ string methods.
25
+ """
26
+ if isinstance(content, str):
27
+ return content
28
+ elif isinstance(content, list):
29
+ return "".join(safe_extract_text(item) for item in content)
30
+ elif isinstance(content, dict):
31
+ if "text" in content:
32
+ return safe_extract_text(content["text"])
33
+ return str(content)
34
+ elif content is None:
35
+ return ""
36
+ else:
37
+ return str(content)
@@ -0,0 +1,651 @@
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 re
16
+ from typing import Any
17
+
18
+ from third_party.mobile_use.utils.logger import get_logger
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ def _parse_bounds(bounds: Any) -> dict[str, int] | None:
24
+ """Normalize bounds from different formats to a standard dictionary.
25
+
26
+ Supports: 1. String format: "[x1,y1][x2,y2]" 2. Dictionary format: {"x":
27
+ int, "y": int, "width": int, "height": int} 3. Dictionary format: {"left":
28
+ int, "top": int, "right": int, "bottom": int} 4. Cached format:
29
+ "parsed_bounds" inside element dict
30
+ """
31
+ if not bounds:
32
+ return None
33
+
34
+ if isinstance(bounds, dict):
35
+ if "parsed_bounds" in bounds and isinstance(bounds["parsed_bounds"], dict):
36
+ return bounds["parsed_bounds"]
37
+ if "left" in bounds and "top" in bounds:
38
+ return {
39
+ "left": int(bounds.get("left", 0)),
40
+ "top": int(bounds.get("top", 0)),
41
+ "right": int(bounds.get("right", 0)),
42
+ "bottom": int(bounds.get("bottom", 0)),
43
+ }
44
+ if "x" in bounds and "y" in bounds:
45
+ x = int(bounds.get("x", 0))
46
+ y = int(bounds.get("y", 0))
47
+ width = int(bounds.get("width", 0))
48
+ height = int(bounds.get("height", 0))
49
+ return {
50
+ "left": x,
51
+ "top": y,
52
+ "right": x + width,
53
+ "bottom": y + height,
54
+ }
55
+ if "bounds" in bounds and isinstance(bounds["bounds"], str):
56
+ res = _parse_bounds(bounds["bounds"])
57
+ if res:
58
+ bounds["parsed_bounds"] = res
59
+ return res
60
+
61
+ if isinstance(bounds, str):
62
+ match = re.match(r"\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]", bounds)
63
+ if match:
64
+ x1, y1, x2, y2 = map(int, match.groups())
65
+ return {"left": x1, "top": y1, "right": x2, "bottom": y2}
66
+ return None
67
+ return None
68
+
69
+
70
+ def _intersects_screen(bounds: dict[str, int], screen_width: int, screen_height: int) -> bool:
71
+ """Check if element intersects screen bounds."""
72
+ left = bounds.get("left", 0)
73
+ top = bounds.get("top", 0)
74
+ right = bounds.get("right", 0)
75
+ bottom = bounds.get("bottom", 0)
76
+ return not (right <= 0 or bottom <= 0 or left >= screen_width or top >= screen_height)
77
+
78
+
79
+ def _meets_min_size(bounds: dict[str, int], min_size: int) -> bool:
80
+ """Check if element meets minimum size requirements."""
81
+ width = bounds.get("right", 0) - bounds.get("left", 0)
82
+ height = bounds.get("bottom", 0) - bounds.get("top", 0)
83
+ return width > min_size and height > min_size
84
+
85
+
86
+ def _clip_bounds(
87
+ node: dict[str, Any],
88
+ bounds_data: dict[str, int],
89
+ ancestor_bounds: dict[str, int],
90
+ ) -> dict[str, int]:
91
+ """Clips bounds to ancestor bounds and updates the node inplace."""
92
+ left = max(bounds_data["left"], ancestor_bounds.get("left", 0))
93
+ top = max(bounds_data["top"], ancestor_bounds.get("top", 0))
94
+ right = min(bounds_data["right"], ancestor_bounds.get("right", 0))
95
+ bottom = min(bounds_data["bottom"], ancestor_bounds.get("bottom", 0))
96
+
97
+ is_fully_clipped = False
98
+ if left >= right or top >= bottom:
99
+ is_fully_clipped = True
100
+ # Clamp to a point on the edge of ancestor bounds
101
+ left = right = max(
102
+ ancestor_bounds.get("left", 0),
103
+ min(bounds_data["left"], ancestor_bounds.get("right", 0)),
104
+ )
105
+ top = bottom = max(
106
+ ancestor_bounds.get("top", 0),
107
+ min(bounds_data["top"], ancestor_bounds.get("bottom", 0)),
108
+ )
109
+
110
+ new_bounds = {"left": left, "top": top, "right": right, "bottom": bottom}
111
+
112
+ if is_fully_clipped:
113
+ node["is_clipped"] = True
114
+
115
+ # The cached parse is what every later consumer (and _parse_bounds itself)
116
+ # reads first; leaving it stale would hand out the unclipped rectangle.
117
+ if isinstance(node.get("parsed_bounds"), dict):
118
+ node["parsed_bounds"] = new_bounds.copy()
119
+
120
+ # Re-serialize bounds into the node matching original format
121
+ orig = node.get("bounds")
122
+ if isinstance(orig, str):
123
+ node["bounds"] = f"[{left},{top}][{right},{bottom}]"
124
+ elif isinstance(orig, dict):
125
+ if "left" in orig and "top" in orig:
126
+ node["bounds"] = new_bounds.copy()
127
+ elif "x" in orig and "y" in orig:
128
+ node["bounds"] = {
129
+ "x": left,
130
+ "y": top,
131
+ "width": right - left,
132
+ "height": bottom - top,
133
+ }
134
+ return new_bounds
135
+
136
+
137
+ def _is_semantic_empty(node: dict[str, Any]) -> bool:
138
+ """Check if a node has no semantic value or interactive capability."""
139
+ # Check text
140
+ text = node.get("text")
141
+ if text and str(text).strip():
142
+ return False
143
+
144
+ # Check content description
145
+ content_desc = (
146
+ node.get("content-desc")
147
+ or node.get("contentDescription")
148
+ or node.get("content-description")
149
+ )
150
+ if content_desc and str(content_desc).strip():
151
+ return False
152
+
153
+ # Check clickable
154
+ clickable = node.get("clickable")
155
+ if clickable is True or str(clickable).lower() == "true":
156
+ return False
157
+
158
+ # Check focusable
159
+ focusable = node.get("focusable")
160
+ if focusable is True or str(focusable).lower() == "true":
161
+ return False
162
+
163
+ # Check if it's a valid image container
164
+ class_name = str(node.get("className") or node.get("class") or "").lower()
165
+ if "image" in class_name or "icon" in class_name or "photo" in class_name:
166
+ return False
167
+
168
+ return True
169
+
170
+
171
+ def _merge_complementary_children(children: list[dict], parent_node: dict) -> list[dict]:
172
+ """Strategy 2: Merge complementary siblings under a clickable parent."""
173
+ # Rule 1: Parent must be clickable or focusable
174
+ clickable = parent_node.get("clickable")
175
+ focusable = parent_node.get("focusable")
176
+ if not (
177
+ clickable is True
178
+ or str(clickable).lower() == "true"
179
+ or focusable is True
180
+ or str(focusable).lower() == "true"
181
+ ):
182
+ return children
183
+
184
+ # Classify children
185
+ textual_children = []
186
+ decoration_children = []
187
+
188
+ for child in children:
189
+ # Rule 3: No child is a form control
190
+ class_name = str(child.get("className") or child.get("class") or "").lower()
191
+ if any(kw in class_name for kw in ["checkbox", "switch", "radio", "edittext"]):
192
+ return children
193
+
194
+ text = child.get("text")
195
+ content_desc = (
196
+ child.get("content-desc")
197
+ or child.get("contentDescription")
198
+ or child.get("content-description")
199
+ )
200
+
201
+ if (text and str(text).strip()) or (content_desc and str(content_desc).strip()):
202
+ textual_children.append(child)
203
+ else:
204
+ # Check if it's a decoration (not clickable, not focusable)
205
+ c_clickable = child.get("clickable")
206
+ c_focusable = child.get("focusable")
207
+ if not (
208
+ c_clickable is True
209
+ or str(c_clickable).lower() == "true"
210
+ or c_focusable is True
211
+ or str(c_focusable).lower() == "true"
212
+ ):
213
+ decoration_children.append(child)
214
+ else:
215
+ # It's an interactive element without text, don't merge as decoration
216
+ return children
217
+
218
+ # Rule 2: Exactly ONE textual child and at least one decoration child
219
+ if (
220
+ len(textual_children) == 1
221
+ and len(decoration_children) >= 1
222
+ and len(children) == len(textual_children) + len(decoration_children)
223
+ ):
224
+ # Perform merge
225
+ txt_child = textual_children[0]
226
+
227
+ # Compute Union Bounds
228
+ all_bounds = []
229
+ for c in children:
230
+ b = _parse_bounds(c.get("bounds"))
231
+ if b:
232
+ all_bounds.append(b)
233
+
234
+ if not all_bounds:
235
+ return children
236
+
237
+ left = min(b["left"] for b in all_bounds)
238
+ top = min(b["top"] for b in all_bounds)
239
+ right = max(b["right"] for b in all_bounds)
240
+ bottom = max(b["bottom"] for b in all_bounds)
241
+
242
+ merged_node = txt_child.copy()
243
+
244
+ # Update bounds matching original format if possible, or just dict
245
+ orig = txt_child.get("bounds")
246
+ if isinstance(orig, str):
247
+ merged_node["bounds"] = f"[{left},{top}][{right},{bottom}]"
248
+ else:
249
+ merged_node["bounds"] = {
250
+ "left": left,
251
+ "top": top,
252
+ "right": right,
253
+ "bottom": bottom,
254
+ }
255
+
256
+ # Concatenate text and content-desc
257
+ desc_parts = []
258
+ for c in children:
259
+ desc = (
260
+ c.get("content-desc") or c.get("contentDescription") or c.get("content-description")
261
+ )
262
+ if desc and str(desc).strip():
263
+ desc_parts.append(f"[{str(desc).strip()}]")
264
+ elif "image" in str(c.get("className") or c.get("class") or "").lower():
265
+ # User tip: use [Icon] for images without desc
266
+ desc_parts.append("[Icon]")
267
+
268
+ text_parts = []
269
+ for c in children:
270
+ txt = c.get("text")
271
+ if txt and str(txt).strip():
272
+ text_parts.append(str(txt).strip())
273
+
274
+ if desc_parts:
275
+ merged_node["content-desc"] = " ".join(desc_parts)
276
+ if text_parts:
277
+ merged_node["text"] = " ".join(text_parts)
278
+
279
+ return [merged_node]
280
+
281
+ return children
282
+
283
+
284
+ def _prune_parent_child_redundancy(
285
+ parent: dict[str, Any], children: list[dict[str, Any]]
286
+ ) -> list[dict[str, Any]]:
287
+ """Strategy 3: Remove redundant children that have the same text as the parent."""
288
+ parent_text = str(parent.get("text") or "").strip()
289
+ parent_desc = str(
290
+ parent.get("content-desc")
291
+ or parent.get("contentDescription")
292
+ or parent.get("content-description")
293
+ or ""
294
+ ).strip()
295
+
296
+ if not parent_text and not parent_desc:
297
+ return children
298
+
299
+ new_children = []
300
+ for child in children:
301
+ child_text = str(child.get("text") or "").strip()
302
+ child_desc = str(
303
+ child.get("content-desc")
304
+ or child.get("contentDescription")
305
+ or child.get("content-description")
306
+ or ""
307
+ ).strip()
308
+
309
+ text_matches = False
310
+ if parent_text and child_text and parent_text == child_text:
311
+ text_matches = True
312
+ elif parent_desc and child_text and parent_desc == child_text:
313
+ text_matches = True
314
+ elif parent_text and child_desc and parent_text == child_desc:
315
+ text_matches = True
316
+ elif parent_desc and child_desc and parent_desc == child_desc:
317
+ text_matches = True
318
+
319
+ if text_matches:
320
+ class_name = str(child.get("className") or child.get("class") or "").lower()
321
+ is_form_control = any(
322
+ kw in class_name for kw in ["checkbox", "switch", "radio", "edittext"]
323
+ )
324
+ has_children = bool(child.get("children"))
325
+
326
+ clickable = child.get("clickable")
327
+ focusable = child.get("focusable")
328
+ is_interactive = (
329
+ clickable is True
330
+ or str(clickable).lower() == "true"
331
+ or focusable is True
332
+ or str(focusable).lower() == "true"
333
+ )
334
+
335
+ if is_form_control or has_children or is_interactive:
336
+ # Keep but erase text
337
+ merged_node = child.copy()
338
+ if "text" in merged_node:
339
+ merged_node["text"] = ""
340
+ if "content-desc" in merged_node:
341
+ merged_node["content-desc"] = ""
342
+ if "contentDescription" in merged_node:
343
+ merged_node["contentDescription"] = ""
344
+ if "content-description" in merged_node:
345
+ merged_node["content-description"] = ""
346
+ new_children.append(merged_node)
347
+ else:
348
+ # Remove child
349
+ continue
350
+ else:
351
+ new_children.append(child)
352
+
353
+ return new_children
354
+
355
+
356
+ def _filter_and_prune(
357
+ node: dict[str, Any],
358
+ screen_width: int,
359
+ screen_height: int,
360
+ min_element_size: int = 5,
361
+ use_dynamic_min_size: bool = True,
362
+ clip_bounds: bool = True,
363
+ semantic_pruning: str = "hoist",
364
+ ancestor_bounds: dict[str, int] | None = None,
365
+ ) -> list[dict[str, Any]]:
366
+ """Recursively filters, clips bounds, and applies semantic pruning strategies."""
367
+ if ancestor_bounds is None:
368
+ ancestor_bounds = {
369
+ "left": 0,
370
+ "top": 0,
371
+ "right": screen_width,
372
+ "bottom": screen_height,
373
+ }
374
+
375
+ bounds_data = _parse_bounds(node.get("bounds"))
376
+
377
+ if bounds_data:
378
+ # We no longer check _intersects_screen to return [] immediately,
379
+ # to ensure zero info loss. Out-of-bounds elements will be clipped to a point.
380
+
381
+ if clip_bounds:
382
+ bounds_data = _clip_bounds(node, bounds_data, ancestor_bounds)
383
+
384
+ # Dynamic min size logic: use relative percentage (0.5% of short side) if enabled
385
+ actual_min_size = min_element_size
386
+ if use_dynamic_min_size:
387
+ short_side = min(screen_width, screen_height)
388
+ dynamic_min_size = int(short_side * 0.005)
389
+ actual_min_size = max(min_element_size, dynamic_min_size)
390
+
391
+ # If it's fully clipped, we skip min size check to preserve it
392
+ if not node.get("is_clipped", False) and not _meets_min_size(bounds_data, actual_min_size):
393
+ return []
394
+
395
+ # Recursively process children
396
+ filtered_node = node.copy()
397
+ filtered_children = []
398
+ if "children" in node:
399
+ for child in node.get("children", []):
400
+ if isinstance(child, dict):
401
+ filtered_children.extend(
402
+ _filter_and_prune(
403
+ child,
404
+ screen_width,
405
+ screen_height,
406
+ min_element_size,
407
+ use_dynamic_min_size,
408
+ clip_bounds,
409
+ semantic_pruning,
410
+ ancestor_bounds=bounds_data if bounds_data else ancestor_bounds,
411
+ )
412
+ )
413
+
414
+ # Apply Strategy 3: Parent-Child Redundancy Removal
415
+ if filtered_children:
416
+ filtered_children = _prune_parent_child_redundancy(filtered_node, filtered_children)
417
+
418
+ # Apply Strategy 2: Sibling Merging
419
+ if filtered_children:
420
+ filtered_children = _merge_complementary_children(filtered_children, filtered_node)
421
+
422
+ filtered_node["children"] = filtered_children
423
+
424
+ # Apply semantic pruning strategies
425
+ if _is_semantic_empty(filtered_node):
426
+ if semantic_pruning == "safe":
427
+ # Only eliminate if it's a leaf node
428
+ if not filtered_node.get("children"):
429
+ return []
430
+ elif semantic_pruning == "hoist":
431
+ # Skip this node and hoist its valid children up
432
+ return filtered_node.get("children", [])
433
+
434
+ return [filtered_node]
435
+
436
+
437
+ def _detect_fixed_system_bars(
438
+ ui_hierarchy: list[dict[str, Any]], screen_width: int, screen_height: int
439
+ ) -> list[dict[str, int]]:
440
+ """Detect fixed, non-scrollable system or navigation bars anchored to the extreme top/bottom edges.
441
+
442
+ Enforces strict triple guardrails to guarantee near-zero false positives:
443
+
444
+ 1. Edge & Width Guarantee: Must anchor to top (top==0) or bottom
445
+ (bottom==screen_height) and span >= 95% of screen width.
446
+ 2. Non-Scrollable Root Branch: The candidate node and all its
447
+ descendants/ancestors must NOT be scrollable.
448
+ 3. Identity & Height Bounds: High-confidence system package/IDs OR strict
449
+ navigation bar height bounds.
450
+ """
451
+ fixed_bars = []
452
+
453
+ def check_scrollable(node: dict[str, Any]) -> bool:
454
+ if node.get("scrollable") is True or str(node.get("scrollable", "")).lower() == "true":
455
+ return True
456
+ for child in node.get("children", []):
457
+ if isinstance(child, dict) and check_scrollable(child):
458
+ return True
459
+ return False
460
+
461
+ def scan_for_bars(node: dict[str, Any], has_scrollable_ancestor: bool):
462
+ is_curr_scrollable = has_scrollable_ancestor or (
463
+ node.get("scrollable") is True or str(node.get("scrollable", "")).lower() == "true"
464
+ )
465
+
466
+ bounds = _parse_bounds(node.get("bounds"))
467
+ if bounds and not is_curr_scrollable and not check_scrollable(node):
468
+ width = bounds["right"] - bounds["left"]
469
+ height = bounds["bottom"] - bounds["top"]
470
+ package_name = str(node.get("package") or node.get("packageName") or "").lower()
471
+ res_id = str(node.get("resource-id") or node.get("resourceId") or "").lower()
472
+ class_name = str(node.get("className") or node.get("class") or "").lower()
473
+
474
+ is_full_width = width >= int(screen_width * 0.95)
475
+ is_high_conf_id = (
476
+ any(
477
+ kw in res_id or kw in class_name
478
+ for kw in [
479
+ "bottom_navigation",
480
+ "navigation_bar",
481
+ "bottom_bar",
482
+ "tab_layout",
483
+ "action_bar",
484
+ ]
485
+ )
486
+ or package_name == "com.android.systemui"
487
+ )
488
+
489
+ # Check bottom fixed bar (e.g., bottom navigation bar / system navigation bar)
490
+ if bounds["bottom"] >= screen_height and is_full_width and height > 0:
491
+ if is_high_conf_id or (25 <= height <= int(screen_height * 0.15)):
492
+ fixed_bars.append(bounds.copy())
493
+ return # Don't recurse into children of confirmed fixed bars
494
+
495
+ # Check top fixed bar (e.g., status bar / top action bar)
496
+ if bounds["top"] <= 0 and is_full_width and height > 0:
497
+ if package_name == "com.android.systemui" or (
498
+ 15 <= height <= int(screen_height * 0.12)
499
+ ):
500
+ fixed_bars.append(bounds.copy())
501
+ return
502
+
503
+ for child in node.get("children", []):
504
+ if isinstance(child, dict):
505
+ scan_for_bars(child, is_curr_scrollable)
506
+
507
+ for root_node in ui_hierarchy:
508
+ if isinstance(root_node, dict):
509
+ scan_for_bars(root_node, False)
510
+
511
+ return fixed_bars
512
+
513
+
514
+ def _clamp_by_fixed_bars(
515
+ node: dict[str, Any], fixed_bars: list[dict[str, int]], min_size: int
516
+ ) -> bool:
517
+ """Non-destructively clamp the vertical bounds of a node if it overlaps with confirmed fixed system bars.
518
+
519
+ Returns False if the remaining visible height is smaller than min_size
520
+ (silent discard of thin slivers).
521
+ """
522
+ bounds = _parse_bounds(node.get("bounds"))
523
+ if not bounds or not fixed_bars:
524
+ return True
525
+
526
+ left, top, right, bottom = (
527
+ bounds["left"],
528
+ bounds["top"],
529
+ bounds["right"],
530
+ bounds["bottom"],
531
+ )
532
+
533
+ for bar in fixed_bars:
534
+ # Check horizontal overlap
535
+ if right <= bar["left"] or left >= bar["right"]:
536
+ continue
537
+
538
+ # If it's the fixed bar itself or fully inside the bar, keep it if it belongs to the bar
539
+ if (
540
+ left == bar["left"]
541
+ and top == bar["top"]
542
+ and right == bar["right"]
543
+ and bottom == bar["bottom"]
544
+ ):
545
+ return True
546
+
547
+ # Clamp against a bottom fixed bar
548
+ if bar["bottom"] >= bottom and top < bar["top"] < bottom:
549
+ bottom = min(bottom, bar["top"])
550
+
551
+ # Clamp against a top fixed bar
552
+ if bar["top"] <= top and top < bar["bottom"] < bottom:
553
+ top = max(top, bar["bottom"])
554
+
555
+ # If remaining height is too small, silently discard this occluded sliver
556
+ if bottom - top <= max(min_size, 20):
557
+ return False
558
+
559
+ new_bounds = {"left": left, "top": top, "right": right, "bottom": bottom}
560
+ orig = node.get("bounds")
561
+ if isinstance(orig, str):
562
+ node["bounds"] = f"[{left},{top}][{right},{bottom}]"
563
+ elif isinstance(orig, dict):
564
+ if "left" in orig and "top" in orig:
565
+ node["bounds"] = new_bounds.copy()
566
+ elif "x" in orig and "y" in orig:
567
+ node["bounds"] = {
568
+ "x": left,
569
+ "y": top,
570
+ "width": right - left,
571
+ "height": bottom - top,
572
+ }
573
+ return True
574
+
575
+
576
+ def filter_node(
577
+ node: dict[str, Any],
578
+ screen_width: int,
579
+ screen_height: int,
580
+ min_element_size: int = 5,
581
+ use_dynamic_min_size: bool = True,
582
+ clip_bounds: bool = True,
583
+ semantic_pruning: str = "hoist",
584
+ ) -> dict[str, Any] | None:
585
+ """Recursively filter a UI node and its children based on screen bounds, size, and semantics.
586
+
587
+ Returns None if the node does not meet the criteria.
588
+ """
589
+ res_list = _filter_and_prune(
590
+ node,
591
+ screen_width,
592
+ screen_height,
593
+ min_element_size,
594
+ use_dynamic_min_size,
595
+ clip_bounds,
596
+ semantic_pruning,
597
+ )
598
+ if not res_list:
599
+ return None
600
+
601
+ if len(res_list) == 1:
602
+ return res_list[0]
603
+
604
+ # If the root node itself was hoisted resulting in multiple children, wrap or attach
605
+ new_node = node.copy()
606
+ new_node["children"] = res_list
607
+ return new_node
608
+
609
+
610
+ def filter_ui_hierarchy(
611
+ ui_hierarchy: list[dict[str, Any]],
612
+ screen_width: int = 1080,
613
+ screen_height: int = 2400,
614
+ min_element_size: int = 5,
615
+ use_dynamic_min_size: bool = True,
616
+ clip_bounds: bool = True,
617
+ semantic_pruning: str = "hoist",
618
+ ) -> list[dict[str, Any]]:
619
+ """Pure function to filter an entire list of UI elements (either flat or nested).
620
+
621
+ Removes elements outside screen bounds, smaller than minimum size, or
622
+ semantically empty. Applies non-destructive safe-bounds clamping against
623
+ confirmed fixed system/navigation bars.
624
+ """
625
+ # 1. Detect high-confidence fixed system bars before flattening
626
+ fixed_bars = _detect_fixed_system_bars(ui_hierarchy, screen_width, screen_height)
627
+
628
+ filtered_elements = []
629
+ for element in ui_hierarchy:
630
+ if isinstance(element, dict):
631
+ filtered_elements.extend(
632
+ _filter_and_prune(
633
+ element,
634
+ screen_width=screen_width,
635
+ screen_height=screen_height,
636
+ min_element_size=min_element_size,
637
+ use_dynamic_min_size=use_dynamic_min_size,
638
+ clip_bounds=clip_bounds,
639
+ semantic_pruning=semantic_pruning,
640
+ )
641
+ )
642
+
643
+ # 2. Apply safe-bounds clamping against fixed system bars on final candidate elements
644
+ if fixed_bars:
645
+ clamped_elements = []
646
+ for el in filtered_elements:
647
+ if _clamp_by_fixed_bars(el, fixed_bars, min_element_size):
648
+ clamped_elements.append(el)
649
+ return clamped_elements
650
+
651
+ return filtered_elements