android-midscene-automation 0.2.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +63 -1
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +10 -8
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +13 -1
  507. package/src/App.vue +72 -23
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +369 -186
  525. package/src/types.ts +1 -1
@@ -0,0 +1,632 @@
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
+ """Perception and vision tool implementations for the Explorer agent.
16
+
17
+ Split out of ``artemis.agents.explorer.explorer``: the ``exec_*`` tool method
18
+ group plus its search helpers, packaged as a mixin consumed by ``Explorer``.
19
+ Text search, the coordinate audit and the OCR inventory all answer from the
20
+ run's in-memory :class:`~artemis.agents.explorer.screen_index.ScreenIndex`;
21
+ no Data Engine round trip is involved, so an unsynced frame never degrades
22
+ to "no UI data".
23
+ """
24
+
25
+ import asyncio
26
+ import glob
27
+ import json
28
+ from pathlib import Path
29
+ import re
30
+ from typing import TYPE_CHECKING, Any
31
+
32
+ import cv2
33
+
34
+ from artemis.agents.explorer.geometry import norm_to_pixel, pixel_to_norm
35
+ from artemis.agents.explorer.screen_index import ScreenElement, ScreenIndex, normalize_text
36
+ from artemis.agents.image_processor.image_processor import ImageProcessor
37
+ from artemis.agents.object_detector.object_detector import _run_object_detection
38
+ from artemis.config import settings
39
+ from artemis.data_engine.trace import trace
40
+ from artemis.utils.visualization import draw_dots
41
+ from third_party.mobile_use.utils.logger import get_logger
42
+
43
+ logger = get_logger(__name__)
44
+
45
+ #: Text-search answer when the screen could not be indexed at all.
46
+ NO_UI_TREE_MESSAGE = "No UI-tree data is available for this screen; rely on visual detection."
47
+
48
+
49
+ def load_detector_templates() -> tuple[list[str], float]:
50
+ """Load object-detector prompt templates and the optional global timeout.
51
+
52
+ Shared by ``exec_detect_objects`` and the flash-mode ``run`` path; the two
53
+ call sites historically carried an identical inline copy of this block.
54
+ """
55
+ detector_prompt_path = Path(__file__).parent.parent / "object_detector" / "object_detector.json"
56
+ global_timeout = 30.0
57
+ try:
58
+ with open(detector_prompt_path, encoding="utf-8") as f:
59
+ detector_config = json.load(f)
60
+ detector_templates = detector_config.get("templates", [])
61
+ detector_instructions = detector_config.get("instructions", "")
62
+ raw_timeout = detector_config.get("global_timeout")
63
+ if isinstance(raw_timeout, (int, float)) and raw_timeout > 0:
64
+ global_timeout = float(raw_timeout)
65
+ except Exception as e:
66
+ logger.warning(f"Failed to load detector prompt config: {e}")
67
+ detector_templates = ["Point to the following objects in the provided image: {labels_str}."]
68
+ detector_instructions = ""
69
+
70
+ templates = [f"{t}\n\n{detector_instructions}" for t in detector_templates]
71
+ return templates, global_timeout
72
+
73
+
74
+ class PerceptionToolsMixin:
75
+ """Perception/vision tool method group of :class:`Explorer`."""
76
+
77
+ # Runtime initialization lives in ``Explorer``/``RunSetupMixin``. Keep a
78
+ # precise declaration here so this mixin's host requirements are explicit.
79
+ if TYPE_CHECKING:
80
+ from artemis.context import ArtemisContext
81
+
82
+ ctx: ArtemisContext
83
+ global_label_idx: int
84
+ width: int
85
+ height: int
86
+ image_name: str | None
87
+ screenshot_path: str | None
88
+ image_pool: dict[str, dict[str, Any]]
89
+ next_img_id: int
90
+ screen_index: ScreenIndex
91
+ label_registry: dict[str, ScreenElement]
92
+
93
+ # ------------------------------------------------------------------ #
94
+ # Label registry and annotation helpers
95
+ # ------------------------------------------------------------------ #
96
+
97
+ def _register_label(self, label: str, element: ScreenElement) -> None:
98
+ """Remembers which element a label shown to the model stands for.
99
+
100
+ Only labels backed by a real UI-tree or OCR element are registered;
101
+ detection points (``D``) carry no bounds and are deliberately absent.
102
+ """
103
+ self.label_registry[label] = element
104
+
105
+ def _registry_element(self, entry: dict[str, Any]) -> ScreenElement:
106
+ """Maps a numbered-list entry back to its indexed element.
107
+
108
+ The index is preferred over rebuilding the element from the entry
109
+ because it carries the interaction flags the entry dropped.
110
+ """
111
+ raw_bounds = entry["bounds"]
112
+ bounds = (int(raw_bounds[0]), int(raw_bounds[1]), int(raw_bounds[2]), int(raw_bounds[3]))
113
+ text = str(entry.get("text") or "")
114
+ source = "ocr" if entry.get("is_ocr") else "xml"
115
+ for element in self.screen_index.elements:
116
+ if (
117
+ element.bounds == bounds
118
+ and element.source == source
119
+ and normalize_text(element.text) == normalize_text(text)
120
+ ):
121
+ return element
122
+ return ScreenElement(
123
+ text=text,
124
+ bounds=bounds,
125
+ source=source,
126
+ class_name=str(entry["class"]) if entry.get("class") else None,
127
+ resource_id=str(entry["resource_id"]) if entry.get("resource_id") else None,
128
+ )
129
+
130
+ def _next_output_path(self, subdir: str) -> Path:
131
+ """Next free ``images/<subdir>/<image>_<n>.jpg`` under the traces directory."""
132
+ out_dir = Path(settings.TRACES_PATH) / "images" / subdir
133
+ out_dir.mkdir(parents=True, exist_ok=True)
134
+ stem = self.image_name or "temp_image"
135
+ max_seq = 0
136
+ for existing in glob.glob(str(out_dir / f"{stem}_*.jpg")):
137
+ match = re.search(r"_(\d+)\.jpg$", existing)
138
+ if match:
139
+ max_seq = max(max_seq, int(match.group(1)))
140
+ return out_dir / f"{stem}_{max_seq + 1}.jpg"
141
+
142
+ def _draw_labeled_dots(
143
+ self, subdir: str, points: list[list[int]], labels: list[str], color: str
144
+ ) -> str | None:
145
+ """Draws ``labels`` at ``points`` on the screenshot; returns the annotated path.
146
+
147
+ Returns None when the screenshot is missing or drawing fails: the
148
+ textual result is still delivered, so one bad annotation never sinks
149
+ the tool call.
150
+ """
151
+ if not self.screenshot_path:
152
+ return None
153
+ try:
154
+ output_path = self._next_output_path(subdir)
155
+ draw_dots(self.screenshot_path, points, labels, str(output_path), color=color)
156
+ return str(output_path)
157
+ except Exception as e:
158
+ logger.warning(f"Failed to draw {subdir} dots: {e}")
159
+ return None
160
+
161
+ # ------------------------------------------------------------------ #
162
+ # Screen-index searches
163
+ # ------------------------------------------------------------------ #
164
+
165
+ async def _search_ui_helper(self, query: str, prefix: str = "S", color: str = "red") -> dict:
166
+ """Fuzzy text search over the screen index, annotated with labeled dots.
167
+
168
+ A strict pass (0.7) keeps the answer short when the label is
169
+ unambiguous; when it yields fewer than three hits a lenient pass
170
+ (0.4) is merged in so near-misses (abbreviations, truncated labels)
171
+ still surface for the model to judge.
172
+ """
173
+ if not len(self.screen_index):
174
+ return {"text": NO_UI_TREE_MESSAGE, "image_path": None}
175
+
176
+ matches = list(self.screen_index.search_text(query, 0.7))
177
+ if len(matches) < 3:
178
+ seen = {(m.matched_text, m.element.bounds) for m in matches}
179
+ for m in self.screen_index.search_text(query, 0.4):
180
+ key = (m.matched_text, m.element.bounds)
181
+ if key not in seen:
182
+ matches.append(m)
183
+ seen.add(key)
184
+
185
+ if not matches:
186
+ return {"text": "No matches found.", "image_path": None}
187
+
188
+ points: list[list[int]] = []
189
+ labels: list[str] = []
190
+ text_output: list[str] = []
191
+ for m in matches:
192
+ element = m.element
193
+ m_prefix = "O" if element.source == "ocr" else prefix
194
+ label_id = f"{m_prefix}{self.global_label_idx}"
195
+ self.global_label_idx += 1
196
+ self._register_label(label_id, element)
197
+ cx, cy = element.center
198
+ points.append([cx, cy])
199
+ labels.append(label_id)
200
+ nx, ny = pixel_to_norm(cx, cy, self.width, self.height)
201
+ text_output.append(f"[{label_id}] '{m.matched_text}' at [{nx},{ny}]")
202
+
203
+ image_path = self._draw_labeled_dots("search_ui", points, labels, color)
204
+ return {"text": " | ".join(text_output), "image_path": image_path}
205
+
206
+ async def _search_by_coords_helper(
207
+ self, nx: int, ny: int, prefix: str = "X", color: str = "blue"
208
+ ) -> dict:
209
+ """Coordinate audit: names what actually sits under a normalized point.
210
+
211
+ UI-tree hits come innermost first (see ``ScreenIndex.elements_at``),
212
+ so the label is bound to the node a tap would reach. The blue dot is
213
+ drawn even without a hit so the model can see where it probed.
214
+ """
215
+ x, y = norm_to_pixel(nx, ny, self.width, self.height)
216
+ found = self.screen_index.elements_at(x, y)
217
+
218
+ label_id = f"{prefix}{self.global_label_idx}"
219
+ self.global_label_idx += 1
220
+ image_path = self._draw_labeled_dots("coords_audit", [[x, y]], [label_id], color)
221
+
222
+ if not found:
223
+ return {"text": f"No elements found at [{nx},{ny}].", "image_path": image_path}
224
+
225
+ self._register_label(label_id, found[0])
226
+ described = " | ".join(element.describe() for element in found)
227
+ return {
228
+ "text": f"Matched element at [{nx},{ny}]: [{label_id}] | {described}",
229
+ "image_path": image_path,
230
+ }
231
+
232
+ @trace(type="tool", name="detect_objects")
233
+ async def exec_detect_objects(
234
+ self,
235
+ queries: list[str],
236
+ target_image_id: str = "img_0",
237
+ prefix: str = "D",
238
+ color: str = "red",
239
+ ) -> dict:
240
+ try:
241
+ if not self.screenshot_path:
242
+ return {
243
+ "text": "Error: Original screenshot is not available.",
244
+ "image_path": None,
245
+ }
246
+ target_info = self.image_pool.get(target_image_id)
247
+ if not target_info:
248
+ return {
249
+ "text": (f"Error: {target_image_id} not found in Image Pool."),
250
+ "image_path": None,
251
+ }
252
+
253
+ target_path = target_info["path"]
254
+ transform = target_info["transform"]
255
+
256
+ # Load object detector templates
257
+ templates, global_timeout = load_detector_templates()
258
+
259
+ result = await _run_object_detection(
260
+ self.ctx,
261
+ target_path,
262
+ queries,
263
+ templates,
264
+ global_timeout=global_timeout,
265
+ )
266
+ try:
267
+ detected_items = result.get("detected", [])
268
+ failed_queries = result.get("failed", [])
269
+
270
+ if not detected_items:
271
+ text_output = ["No objects detected."]
272
+ if failed_queries:
273
+ text_output.append(
274
+ "\n[NOTE: Failed to detect the following queries:"
275
+ f" {', '.join(failed_queries)}]"
276
+ )
277
+ return {
278
+ "text": "".join(text_output),
279
+ "image_path": str(target_path),
280
+ }
281
+
282
+ base_dir = settings.TRACES_PATH
283
+ images_dir = base_dir / "images"
284
+ object_detect_dir = images_dir / "object_detect"
285
+ object_detect_dir.mkdir(parents=True, exist_ok=True)
286
+
287
+ image_name_safe = self.image_name or "temp_image"
288
+ existing_files = glob.glob(str(object_detect_dir / f"{image_name_safe}_*.jpg"))
289
+ max_seq = 0
290
+ for f in existing_files:
291
+ match = re.search(r"_(\d+)\.jpg$", f)
292
+ if match:
293
+ max_seq = max(max_seq, int(match.group(1)))
294
+ seq = max_seq + 1
295
+ output_path = object_detect_dir / f"{image_name_safe}_{seq}.jpg"
296
+
297
+ points = []
298
+ d_labels = []
299
+ text_output = []
300
+
301
+ if target_image_id != "img_0":
302
+ text_output.append(
303
+ "[NOTE: Coordinates are automatically mapped back to"
304
+ f" the original {self.width}x{self.height} screen space from"
305
+ f" {target_image_id}]"
306
+ )
307
+
308
+ for item in detected_items:
309
+ label_id = f"{prefix}{self.global_label_idx}"
310
+ self.global_label_idx += 1
311
+ pos = item.get("point")
312
+ if pos and isinstance(pos, list) and len(pos) == 2:
313
+ x_norm, y_norm = pos
314
+
315
+ t_img = cv2.imread(target_path)
316
+ if t_img is None:
317
+ raise ValueError(f"Failed to read target image at {target_path}")
318
+ t_h, t_w = t_img.shape[:2]
319
+
320
+ # Pixels in target image
321
+ x_target_pixel = (x_norm / 1000) * t_w
322
+ y_target_pixel = (y_norm / 1000) * t_h
323
+
324
+ # Pixels in original image
325
+ x_orig_pixel = (x_target_pixel / transform["scale_x"]) + transform[
326
+ "offset_x"
327
+ ]
328
+ y_orig_pixel = (y_target_pixel / transform["scale_y"]) + transform[
329
+ "offset_y"
330
+ ]
331
+
332
+ points.append([int(x_orig_pixel), int(y_orig_pixel)])
333
+ d_labels.append(label_id)
334
+
335
+ # Original image norm
336
+ x_orig_norm = int(max(0, min(1000, x_orig_pixel * 1000 / self.width)))
337
+ y_orig_norm = int(max(0, min(1000, y_orig_pixel * 1000 / self.height)))
338
+
339
+ text_output.append(
340
+ f'[{label_id}] "{item.get("label")}" at [{x_orig_norm},{y_orig_norm}]'
341
+ )
342
+
343
+ draw_dots(
344
+ self.screenshot_path,
345
+ points,
346
+ d_labels,
347
+ str(output_path),
348
+ color=color,
349
+ )
350
+
351
+ if failed_queries:
352
+ text_output.append(
353
+ " | [NOTE: Failed to detect the following queries:"
354
+ f" {', '.join(failed_queries)}]"
355
+ )
356
+
357
+ return {
358
+ "text": " | ".join(text_output),
359
+ "image_path": str(output_path),
360
+ }
361
+
362
+ except Exception as e:
363
+ logger.warning(f"Failed to process detection output for annotation: {e}")
364
+ return {"text": str(result), "image_path": None}
365
+ except Exception as e:
366
+ return {
367
+ "text": f"Error: Object detection failed: {e}",
368
+ "image_path": None,
369
+ }
370
+
371
+ @trace(type="tool", name="ask_perception_tool")
372
+ async def exec_ask_perception_tool(
373
+ self, search_query: str, nx: int, ny: int, detect_queries: list[str]
374
+ ) -> dict:
375
+ if nx is None or ny is None or not (0 <= nx <= 1000) or not (0 <= ny <= 1000):
376
+ coords_invalid = True
377
+ else:
378
+ coords_invalid = False
379
+
380
+ # Run tasks concurrently in parallel!
381
+ tasks = [
382
+ self._search_ui_helper(search_query, prefix="X", color="green"),
383
+ self._search_by_coords_helper(nx, ny, prefix="X", color="blue")
384
+ if not coords_invalid
385
+ else asyncio.sleep(
386
+ 0,
387
+ result={
388
+ "text": "Error: Invalid coordinates format",
389
+ "image_path": None,
390
+ },
391
+ ),
392
+ self.exec_detect_objects(
393
+ detect_queries, target_image_id="img_0", prefix="D", color="red"
394
+ ),
395
+ ]
396
+
397
+ results = await asyncio.gather(*tasks)
398
+
399
+ # Consolidate text results
400
+ text_parts = []
401
+
402
+ xml_text = results[0].get("text", "No matches found.")
403
+ if xml_text and xml_text != "No matches found.":
404
+ text_parts.append(f"Text Search Results are: {xml_text}")
405
+
406
+ coord_text = results[1].get("text", "No elements found.")
407
+ if coord_text and not coord_text.startswith("No elements found"):
408
+ text_parts.append(f"Coordinate Search Results are: {coord_text}")
409
+
410
+ obj_text = results[2].get("text", "No objects detected.")
411
+ if obj_text:
412
+ if obj_text.startswith("No objects detected."):
413
+ note_part = obj_text.replace("No objects detected.", "").strip()
414
+ if note_part:
415
+ text_parts.append(f"Object Detection Results are: {note_part}")
416
+ else:
417
+ text_parts.append(f"Object Detection Results are: {obj_text}")
418
+
419
+ if not text_parts:
420
+ unified_text = "No matches or objects found across any perception method."
421
+ else:
422
+ unified_text = ". ".join(text_parts) + "."
423
+
424
+ # Collect image paths
425
+ image_paths = []
426
+ for r in results:
427
+ if isinstance(r, dict) and r.get("image_path"):
428
+ image_paths.append(r["image_path"])
429
+
430
+ return {
431
+ "text": unified_text,
432
+ "image_paths": image_paths,
433
+ }
434
+
435
+ @trace(type="tool", name="ask_image_processor")
436
+ async def exec_ask_image_processor(
437
+ self, instruction: str, target_image_id: str = "img_0"
438
+ ) -> dict:
439
+ coder = ImageProcessor(self.ctx)
440
+ target_info = self.image_pool.get(target_image_id)
441
+ if not target_info:
442
+ return {
443
+ "text": f"Error: {target_image_id} not found in Image Pool.",
444
+ "image_path": None,
445
+ }
446
+
447
+ target_path = target_info["path"]
448
+ result = await coder.run(instruction, target_path)
449
+
450
+ if not isinstance(result, dict) or "error" in result:
451
+ err_msg = result.get("error") if isinstance(result, dict) else "Unknown error"
452
+ return {
453
+ "text": f"Image Processor failed: {err_msg}",
454
+ "image_paths": [],
455
+ }
456
+
457
+ outputs = result.get("outputs", [])
458
+ summary = result.get("summary", "")
459
+
460
+ parent_transform = target_info["transform"]
461
+ registered_ids = []
462
+ annotations_sections = []
463
+ image_paths = []
464
+
465
+ for entry in outputs:
466
+ new_id = f"img_{self.next_img_id}"
467
+ self.next_img_id += 1
468
+
469
+ new_path = entry["path"]
470
+ image_paths.append(new_path)
471
+ transform = entry["transform"]
472
+
473
+ final_scale_x = parent_transform["scale_x"] * transform["scale_x"]
474
+ final_scale_y = parent_transform["scale_y"] * transform["scale_y"]
475
+ final_offset_x = (
476
+ transform["offset_x"] / parent_transform["scale_x"]
477
+ ) + parent_transform["offset_x"]
478
+ final_offset_y = (
479
+ transform["offset_y"] / parent_transform["scale_y"]
480
+ ) + parent_transform["offset_y"]
481
+
482
+ self.image_pool[new_id] = {
483
+ "path": new_path,
484
+ "transform": {
485
+ "offset_x": final_offset_x,
486
+ "offset_y": final_offset_y,
487
+ "scale_x": final_scale_x,
488
+ "scale_y": final_scale_y,
489
+ },
490
+ "description": (f"Generated by coder from {target_image_id}: {summary}"),
491
+ }
492
+ registered_ids.append(new_id)
493
+
494
+ # Parse and translate annotations
495
+ annotations = entry.get("annotations", {})
496
+ if annotations:
497
+ lines = [f"{new_id}:"]
498
+ for label, coord in sorted(annotations.items(), key=lambda item: item[0]):
499
+ px, py = coord
500
+ sx = int(round((px / final_scale_x) + final_offset_x))
501
+ sy = int(round((py / final_scale_y) + final_offset_y))
502
+ lines.append(f" - [{label}]: [{sx}, {sy}]")
503
+ annotations_sections.append("\n".join(lines))
504
+
505
+ if registered_ids:
506
+ ids_str = ", ".join(registered_ids)
507
+ text = (
508
+ "Image Processor completed successfully. New image(s) saved as"
509
+ f" {ids_str} in Image Pool. Summary: {summary}"
510
+ )
511
+ if annotations_sections:
512
+ text += "\n\nAnnotations (wrt original screenshot):\n" + "\n".join(
513
+ annotations_sections
514
+ )
515
+ return {
516
+ "text": text,
517
+ "image_paths": image_paths,
518
+ }
519
+ return {
520
+ "text": "ImageProcessor error: no output images were produced.",
521
+ "image_paths": [],
522
+ }
523
+
524
+ @trace(type="tool", name="get_ocr_list")
525
+ async def exec_get_ocr_list(self, prefix: str = "O", color: str = "green") -> dict:
526
+ """Lists every OCR-detected text on the screen with a labeled dot."""
527
+ try:
528
+ ocr_elements = self.screen_index.ocr_elements
529
+ if not ocr_elements:
530
+ return {
531
+ "text": "No text elements detected on the screen.",
532
+ "image_path": None,
533
+ }
534
+
535
+ points: list[list[int]] = []
536
+ labels: list[str] = []
537
+ text_output: list[str] = []
538
+ for element in ocr_elements:
539
+ cx, cy = element.center
540
+ if not (0 <= cx <= self.width and 0 <= cy <= self.height):
541
+ continue
542
+ label_id = f"{prefix}{self.global_label_idx}"
543
+ self.global_label_idx += 1
544
+ self._register_label(label_id, element)
545
+ points.append([cx, cy])
546
+ labels.append(label_id)
547
+ nx, ny = pixel_to_norm(cx, cy, self.width, self.height)
548
+ text_output.append(f"[{label_id}] '{element.text}' coords: [{nx},{ny}]")
549
+
550
+ if not points:
551
+ return {"text": "No valid OCR results found.", "image_path": None}
552
+
553
+ image_path = self._draw_labeled_dots("ocr", points, labels, color)
554
+ return {"text": "\n".join(text_output), "image_path": image_path}
555
+
556
+ except Exception as e:
557
+ return {
558
+ "text": f"Error retrieving or processing OCR results: {e}",
559
+ "image_path": None,
560
+ }
561
+
562
+ @trace(type="tool", name="inspect_region")
563
+ async def exec_inspect_region(
564
+ self,
565
+ x_min: int,
566
+ y_min: int,
567
+ x_max: int,
568
+ y_max: int,
569
+ zoom_factor: float = 2.0,
570
+ ) -> dict:
571
+ try:
572
+ if not self.screenshot_path:
573
+ return {
574
+ "text": "Error: Original screenshot is not available.",
575
+ "image_path": None,
576
+ }
577
+ px_start = int(max(0, min(self.width, x_min * self.width / 1000.0)))
578
+ py_start = int(max(0, min(self.height, y_min * self.height / 1000.0)))
579
+ px_end = int(max(0, min(self.width, x_max * self.width / 1000.0)))
580
+ py_end = int(max(0, min(self.height, y_max * self.height / 1000.0)))
581
+
582
+ if px_end <= px_start or py_end <= py_start:
583
+ return {
584
+ "text": (
585
+ f"Error: Invalid crop coordinates. x_min={x_min},"
586
+ f" x_max={x_max}, y_min={y_min}, y_max={y_max}."
587
+ " Bounding box dimensions must be strictly positive."
588
+ ),
589
+ "image_path": None,
590
+ }
591
+
592
+ img = cv2.imread(self.screenshot_path)
593
+ if img is None:
594
+ return {
595
+ "text": (
596
+ f"Error: Failed to read original screenshot at {self.screenshot_path}"
597
+ ),
598
+ "image_path": None,
599
+ }
600
+
601
+ cropped = img[py_start:py_end, px_start:px_end]
602
+
603
+ if zoom_factor is None:
604
+ zoom_factor = 2.0
605
+ zoom_factor = max(1.0, min(4.0, float(zoom_factor)))
606
+
607
+ c_h, c_w = cropped.shape[:2]
608
+ if c_h > 0 and c_w > 0:
609
+ new_w = int(round(c_w * zoom_factor))
610
+ new_h = int(round(c_h * zoom_factor))
611
+ if new_w > 0 and new_h > 0:
612
+ cropped = cv2.resize(cropped, (new_w, new_h))
613
+
614
+ base_dir = settings.TRACES_PATH
615
+ images_dir = base_dir / "images"
616
+ inspect_region_dir = images_dir / "inspect_region"
617
+ inspect_region_dir.mkdir(parents=True, exist_ok=True)
618
+
619
+ image_name_safe = self.image_name or "temp_image"
620
+ output_path = inspect_region_dir / f"{image_name_safe}_{self.global_label_idx}.jpg"
621
+ cv2.imwrite(str(output_path), cropped)
622
+
623
+ return {
624
+ "text": (f"Inspected region coordinates: [{x_min}, {y_min}] to [{x_max}, {y_max}]"),
625
+ "image_path": str(output_path),
626
+ }
627
+
628
+ except Exception as e:
629
+ return {
630
+ "text": f"Error: Region inspection failed: {e}",
631
+ "image_path": None,
632
+ }