android-midscene-automation 0.2.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +159 -29
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,581 @@
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
+ """``ask_explorer``: locate UI elements through the Explorer sub-agent.
16
+
17
+ The module is layered so that every caller (Pro Operator via LangChain, the
18
+ Validator / Flash runner tool executor, the MCP action executor, replay) shares
19
+ one pipeline and differs only in presentation:
20
+
21
+ 1. :func:`locate` runs the user-configured Explorer tier and parses its answer
22
+ into an :class:`ExplorerOutcome`.
23
+ 2. :func:`register_candidates` appends the located elements to the calling
24
+ agent's indexed element list so they can be acted on by index.
25
+ 3. :func:`render_text` / :func:`render_operator_blocks` present the result.
26
+
27
+ The Explorer tier (flash / pro / ultra) is a user setting resolved from
28
+ configuration; it is never part of the tool contract shown to agents.
29
+ """
30
+
31
+ import base64
32
+ from dataclasses import dataclass, field
33
+ import glob
34
+ import json
35
+ from pathlib import Path
36
+ import re
37
+ from typing import Any
38
+
39
+ from langchain_core.tools import BaseTool
40
+ from pydantic import BaseModel, Field
41
+
42
+ from artemis.agents.explorer.constants import (
43
+ ASK_EXPLORER_CONTEXT_FEEDBACK_DESCRIPTION,
44
+ ASK_EXPLORER_DESCRIPTION,
45
+ ASK_EXPLORER_QUERY_DESCRIPTION,
46
+ ASK_EXPLORER_TOOL_NAME,
47
+ )
48
+ from artemis.agents.explorer.explorer import Explorer
49
+ from artemis.agents.explorer.geometry import (
50
+ is_valid_norm_point,
51
+ norm_to_pixel,
52
+ resolve_screen_size,
53
+ )
54
+ from artemis.agents.explorer.tiers import get_tier
55
+ from artemis.config import ExplorerVersion, resolve_explorer_version, settings
56
+ from artemis.context import ArtemisContext
57
+ from artemis.drivers.base import BaseDeviceDriver
58
+ from artemis.graph.state import State
59
+ from artemis.tools.base import ArtemisTool, ToolCategory
60
+ from artemis.tools.tool_wrapper import ToolWrapper
61
+ from artemis.utils.cython_compat import CyFunctionDetector
62
+ from artemis.utils.element_hit_test import find_element_at_point
63
+ from artemis.utils.visualization import draw_dots
64
+ from third_party.mobile_use.utils.logger import get_logger
65
+
66
+ logger = get_logger(__name__)
67
+
68
+ __all__ = [
69
+ "AskExplorerArgs",
70
+ "AskExplorerTool",
71
+ "ExplorerCandidate",
72
+ "ExplorerOutcome",
73
+ "RegisteredCandidate",
74
+ "ask_explorer",
75
+ "ask_explorer_text",
76
+ "ask_explorer_wrapper",
77
+ "get_ask_explorer_tool",
78
+ "locate",
79
+ "register_candidates",
80
+ "render_operator_blocks",
81
+ "render_text",
82
+ "resolve_explorer_version",
83
+ ]
84
+
85
+ #: Color of the dots drawn for newly located candidates; distinct from the
86
+ #: operator's own element numbering so the new indices stand out.
87
+ CANDIDATE_DOT_COLOR = "magenta"
88
+
89
+
90
+ # --------------------------------------------------------------------------- #
91
+ # Tool contract (tier-agnostic)
92
+ # --------------------------------------------------------------------------- #
93
+
94
+
95
+ class AskExplorerArgs(BaseModel):
96
+ """Arguments of ``ask_explorer`` as seen by every calling agent."""
97
+
98
+ model_config = {"ignored_types": (CyFunctionDetector,)}
99
+ query: str = Field(..., description=ASK_EXPLORER_QUERY_DESCRIPTION)
100
+ context_feedback: str = Field("", description=ASK_EXPLORER_CONTEXT_FEEDBACK_DESCRIPTION)
101
+
102
+
103
+ # --------------------------------------------------------------------------- #
104
+ # Result model
105
+ # --------------------------------------------------------------------------- #
106
+
107
+
108
+ NormBounds = tuple[int, int, int, int]
109
+
110
+
111
+ def _parse_norm_bounds(value: Any) -> NormBounds | None:
112
+ """Accepts ``[l, t, r, b]`` in the 0-1000 scale with a positive area."""
113
+ if not isinstance(value, (list, tuple)) or len(value) != 4:
114
+ return None
115
+ try:
116
+ left, top, right, bottom = (int(v) for v in value)
117
+ except (TypeError, ValueError):
118
+ return None
119
+ if not all(0 <= v <= 1000 for v in (left, top, right, bottom)):
120
+ return None
121
+ if right <= left or bottom <= top:
122
+ return None
123
+ return left, top, right, bottom
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class ExplorerCandidate:
128
+ """One element the Explorer located, in normalized 0-1000 coordinates.
129
+
130
+ ``bounds`` is present when the Explorer could tie the candidate to a
131
+ UI-tree or OCR element; it lets the pre-execution safety net verify the
132
+ target like any other indexed element.
133
+ """
134
+
135
+ label: str
136
+ coords: tuple[int, int]
137
+ description: str
138
+ bounds: NormBounds | None = None
139
+ #: "xml", "ocr", or "" when the candidate is a bare visual detection.
140
+ source: str = ""
141
+
142
+
143
+ @dataclass
144
+ class ExplorerOutcome:
145
+ """Parsed answer of one Explorer run."""
146
+
147
+ candidates: list[ExplorerCandidate] = field(default_factory=list)
148
+ #: Fallback explanation or extra observations from the Explorer.
149
+ message: str = ""
150
+ #: True when the run itself failed (as opposed to a clean "not found").
151
+ error: bool = False
152
+ #: Raw Explorer answer, kept for traces and replay.
153
+ raw: str = ""
154
+
155
+ @property
156
+ def found(self) -> bool:
157
+ return bool(self.candidates)
158
+
159
+ @classmethod
160
+ def failure(cls, message: str, raw: str = "") -> "ExplorerOutcome":
161
+ return cls(message=message, error=True, raw=raw)
162
+
163
+ @classmethod
164
+ def from_raw(cls, raw: Any, default_label: str = "") -> "ExplorerOutcome":
165
+ """Parses the Explorer's JSON answer, dropping malformed candidates."""
166
+ text = raw if isinstance(raw, str) else json.dumps(raw, ensure_ascii=False)
167
+ try:
168
+ data = json.loads(text)
169
+ except (TypeError, ValueError):
170
+ return cls.failure(f"Explorer returned an unreadable answer: {text[:300]}", raw=text)
171
+ if not isinstance(data, dict):
172
+ return cls.failure(f"Explorer returned an unexpected answer: {text[:300]}", raw=text)
173
+
174
+ candidates: list[ExplorerCandidate] = []
175
+ for item in data.get("candidates") or []:
176
+ if not isinstance(item, dict):
177
+ continue
178
+ coords = item.get("coords")
179
+ if not is_valid_norm_point(coords):
180
+ logger.warning(f"Dropping Explorer candidate with invalid coords: {item}")
181
+ continue
182
+ label = str(item.get("label") or default_label or f"C{len(candidates) + 1}")
183
+ description = str(item.get("description") or label)
184
+ candidates.append(
185
+ ExplorerCandidate(
186
+ label=label,
187
+ coords=(int(coords[0]), int(coords[1])),
188
+ description=description,
189
+ bounds=_parse_norm_bounds(item.get("bounds")),
190
+ source=str(item.get("source") or ""),
191
+ )
192
+ )
193
+ return cls(
194
+ candidates=candidates,
195
+ message=str(data.get("fallback_message") or ""),
196
+ raw=text,
197
+ )
198
+
199
+
200
+ @dataclass(frozen=True)
201
+ class RegisteredCandidate:
202
+ """A candidate after registration in the agent's indexed element list."""
203
+
204
+ index: int
205
+ pixel: tuple[int, int]
206
+ coords: tuple[int, int]
207
+ description: str
208
+ #: Pixel bounds when known (from the Explorer or from the reused element).
209
+ bounds: tuple[int, int, int, int] | None = None
210
+ #: True when the candidate mapped onto an element that was already indexed.
211
+ reused: bool = False
212
+ #: The text the agent's own indexed list shows for the reused element, so
213
+ #: the answer can name it the way the agent already knows it.
214
+ existing_text: str = ""
215
+
216
+
217
+ #: A bare-point candidate this close to an existing indexed center (in
218
+ #: pixels) is the same element; scaled with the screen but never tiny.
219
+ DEDUP_RADIUS_RATIO = 0.02
220
+ DEDUP_RADIUS_MIN_PX = 20
221
+
222
+
223
+ def _find_existing_element(
224
+ indexed_elements: list[dict[str, Any]], pixel: tuple[int, int], width: int
225
+ ) -> dict[str, Any] | None:
226
+ """Returns the already-indexed element the pixel point belongs to, if any.
227
+
228
+ An element with bounds must contain the point; an element known only by
229
+ its center (earlier Explorer candidates) must lie within the dedup radius.
230
+ """
231
+ x, y = pixel
232
+ hit, _source = find_element_at_point(indexed_elements, x, y)
233
+ if hit is not None:
234
+ return hit
235
+ radius = max(DEDUP_RADIUS_MIN_PX, int(round(width * DEDUP_RADIUS_RATIO)))
236
+ best: tuple[float, dict[str, Any]] | None = None
237
+ for element in indexed_elements:
238
+ if not isinstance(element, dict) or element.get("bounds"):
239
+ continue
240
+ center = element.get("center")
241
+ if not (isinstance(center, (list, tuple)) and len(center) == 2):
242
+ continue
243
+ try:
244
+ distance = ((int(center[0]) - x) ** 2 + (int(center[1]) - y) ** 2) ** 0.5
245
+ except (TypeError, ValueError):
246
+ continue
247
+ if distance <= radius and (best is None or distance < best[0]):
248
+ best = (distance, element)
249
+ return best[1] if best else None
250
+
251
+
252
+ def _element_pixel_bounds(element: dict[str, Any]) -> tuple[int, int, int, int] | None:
253
+ bounds = element.get("bounds")
254
+ if isinstance(bounds, (list, tuple)) and len(bounds) == 4:
255
+ try:
256
+ return (int(bounds[0]), int(bounds[1]), int(bounds[2]), int(bounds[3]))
257
+ except (TypeError, ValueError):
258
+ return None
259
+ return None
260
+
261
+
262
+ # --------------------------------------------------------------------------- #
263
+ # Pipeline
264
+ # --------------------------------------------------------------------------- #
265
+
266
+
267
+ async def locate(
268
+ ctx: ArtemisContext,
269
+ state: State,
270
+ query: str,
271
+ context_feedback: str = "",
272
+ *,
273
+ agent_name: str = "operator",
274
+ version: ExplorerVersion | None = None,
275
+ ) -> ExplorerOutcome:
276
+ """Runs the configured Explorer tier for ``query`` on the latest screenshot.
277
+
278
+ ``version`` is a programmatic pin (tests, replay); calling agents never
279
+ provide it -- the tier comes from the user's configuration.
280
+ """
281
+ screenshot_path = getattr(state, "latest_screenshot", None)
282
+ if not screenshot_path:
283
+ return ExplorerOutcome.failure("No screenshot is available for the current screen yet.")
284
+
285
+ tier = get_tier(
286
+ resolve_explorer_version(ctx, explicit_version=version, agent_or_profile_name=agent_name)
287
+ )
288
+ logger.info(f"ask_explorer [{agent_name}] tier={tier.name} query={query!r}")
289
+
290
+ try:
291
+ raw = await Explorer(ctx).run(
292
+ query,
293
+ context_feedback,
294
+ screenshot_path,
295
+ state,
296
+ version=tier.name,
297
+ )
298
+ except Exception as e: # pylint: disable=broad-exception-caught
299
+ logger.error(f"Explorer run failed [{tier.name}]: {e}")
300
+ return ExplorerOutcome.failure(f"Explorer failed: {e}")
301
+
302
+ return ExplorerOutcome.from_raw(raw, default_label=query)
303
+
304
+
305
+ def register_candidates(
306
+ ctx: ArtemisContext, state: State, outcome: ExplorerOutcome
307
+ ) -> list[RegisteredCandidate]:
308
+ """Appends the outcome's candidates to ``state``'s indexed element list."""
309
+ if not outcome.found:
310
+ return []
311
+
312
+ indexed_points = getattr(state, "indexed_points", None)
313
+ if indexed_points is None:
314
+ indexed_points = []
315
+ state.indexed_points = indexed_points
316
+ indexed_elements = getattr(state, "indexed_elements", None)
317
+ if indexed_elements is None:
318
+ indexed_elements = []
319
+ state.indexed_elements = indexed_elements
320
+
321
+ width, height = resolve_screen_size(ctx, state)
322
+ registered: list[RegisteredCandidate] = []
323
+ for cand in outcome.candidates:
324
+ pixel = norm_to_pixel(cand.coords[0], cand.coords[1], width, height)
325
+
326
+ existing = _find_existing_element(indexed_elements, pixel, width)
327
+ if existing is not None:
328
+ index = int(existing.get("index") or (indexed_elements.index(existing) + 1))
329
+ registered.append(
330
+ RegisteredCandidate(
331
+ index=index,
332
+ pixel=pixel,
333
+ coords=cand.coords,
334
+ description=cand.description,
335
+ bounds=_element_pixel_bounds(existing),
336
+ reused=True,
337
+ existing_text=str(existing.get("text") or ""),
338
+ )
339
+ )
340
+ continue
341
+
342
+ pixel_bounds = None
343
+ if cand.bounds is not None:
344
+ left, top = norm_to_pixel(cand.bounds[0], cand.bounds[1], width, height)
345
+ right, bottom = norm_to_pixel(cand.bounds[2], cand.bounds[3], width, height)
346
+ pixel_bounds = (left, top, right, bottom)
347
+
348
+ indexed_points.append([pixel[0], pixel[1]])
349
+ index = len(indexed_points)
350
+ indexed_elements.append(
351
+ {
352
+ "index": index,
353
+ "center": [pixel[0], pixel[1]],
354
+ "text": cand.description,
355
+ "bounds": list(pixel_bounds) if pixel_bounds else None,
356
+ "class": "ExplorerCandidate",
357
+ "resource_id": None,
358
+ "is_ocr": cand.source == "ocr",
359
+ }
360
+ )
361
+ registered.append(
362
+ RegisteredCandidate(
363
+ index=index,
364
+ pixel=pixel,
365
+ coords=cand.coords,
366
+ description=cand.description,
367
+ bounds=pixel_bounds,
368
+ )
369
+ )
370
+ return registered
371
+
372
+
373
+ # --------------------------------------------------------------------------- #
374
+ # Presentation
375
+ # --------------------------------------------------------------------------- #
376
+
377
+
378
+ def render_text(
379
+ query: str,
380
+ outcome: ExplorerOutcome,
381
+ registered: list[RegisteredCandidate],
382
+ *,
383
+ index_targets: bool = False,
384
+ ) -> str:
385
+ """Plain-text answer for the calling agent.
386
+
387
+ ``index_targets`` selects the targeting syntax the caller's ``click``
388
+ accepts: the Pro Operator and the Flash / MCP action executor take a bare
389
+ integer index next to coordinates; the Validator's text entry takes
390
+ coordinates only.
391
+ """
392
+ if registered:
393
+ lines = [
394
+ f"Explorer located {len(registered)} candidate(s) for '{query}' in the"
395
+ " indexed element list:"
396
+ ]
397
+ for c in registered:
398
+ line = f"- [{c.index}] '{c.description}' at normalized [{c.coords[0]}, {c.coords[1]}]"
399
+ if c.reused:
400
+ line += f" (matches your indexed element [{c.index}] '{c.existing_text}')"
401
+ lines.append(line)
402
+ if index_targets:
403
+ lines.append(
404
+ "Act on a candidate by its index (target=3, a bare integer) or by its"
405
+ " normalized coordinate. Candidates are ranked by confidence."
406
+ )
407
+ else:
408
+ lines.append(
409
+ "Act on a candidate by its normalized coordinate (target=[x, y]) with a"
410
+ " target_description. Candidates are ranked by confidence."
411
+ )
412
+ if outcome.message:
413
+ lines.append(f"Explorer notes: {outcome.message}")
414
+ return "\n".join(lines)
415
+
416
+ if outcome.error:
417
+ return f"Explorer could not run for '{query}'. {outcome.message}"
418
+ detail = outcome.message or "It gave no further detail."
419
+ return (
420
+ f"Explorer could not locate '{query}'. {detail}\n"
421
+ "Consider describing the target differently (visible text, shape, color,"
422
+ " position) or checking whether it is actually visible on this screen."
423
+ )
424
+
425
+
426
+ def render_annotated_image(
427
+ ctx: ArtemisContext, screenshot_path: str, registered: list[RegisteredCandidate]
428
+ ) -> str | None:
429
+ """Draws the newly registered candidates on the screenshot; returns the path."""
430
+ if not registered:
431
+ return None
432
+ base_dir = (
433
+ Path(ctx.data_engine.base_dir)
434
+ if getattr(ctx, "data_engine", None) and getattr(ctx.data_engine, "base_dir", None)
435
+ else Path(settings.TRACES_PATH)
436
+ )
437
+ explorer_dir = base_dir / "images" / "explorer_tool"
438
+ explorer_dir.mkdir(parents=True, exist_ok=True)
439
+
440
+ max_seq = 0
441
+ for existing in glob.glob(str(explorer_dir / "explorer_output_*.jpg")):
442
+ match = re.search(r"_(\d+)\.jpg$", existing)
443
+ if match:
444
+ max_seq = max(max_seq, int(match.group(1)))
445
+ output_path = explorer_dir / f"explorer_output_{max_seq + 1}.jpg"
446
+
447
+ try:
448
+ draw_dots(
449
+ screenshot_path,
450
+ [list(c.pixel) for c in registered],
451
+ [str(c.index) for c in registered],
452
+ str(output_path),
453
+ color=CANDIDATE_DOT_COLOR,
454
+ )
455
+ except Exception as e: # pylint: disable=broad-exception-caught
456
+ logger.error(f"Failed to annotate explorer candidates: {e}")
457
+ return None
458
+ return str(output_path)
459
+
460
+
461
+ def render_operator_blocks(
462
+ ctx: ArtemisContext,
463
+ state: State,
464
+ query: str,
465
+ outcome: ExplorerOutcome,
466
+ registered: list[RegisteredCandidate],
467
+ ) -> str | list[dict[str, Any]]:
468
+ """Multimodal answer for the Operator: text plus the annotated screenshot."""
469
+ text = render_text(query, outcome, registered, index_targets=True)
470
+ screenshot_path = getattr(state, "latest_screenshot", None)
471
+ image_path = (
472
+ render_annotated_image(ctx, screenshot_path, registered) if screenshot_path else None
473
+ )
474
+ if not image_path:
475
+ return text
476
+ try:
477
+ img_b64 = base64.b64encode(Path(image_path).read_bytes()).decode("utf-8")
478
+ except OSError as e:
479
+ logger.error(f"Failed to read annotated explorer image: {e}")
480
+ return text
481
+ return [
482
+ {"type": "text", "text": text},
483
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}},
484
+ ]
485
+
486
+
487
+ # --------------------------------------------------------------------------- #
488
+ # Entry points
489
+ # --------------------------------------------------------------------------- #
490
+
491
+
492
+ async def ask_explorer_text(
493
+ ctx: ArtemisContext,
494
+ state: State,
495
+ query: str,
496
+ context_feedback: str = "",
497
+ *,
498
+ agent_name: str = "validator",
499
+ ) -> str:
500
+ """Text-only, coordinate-only entry used by the Validator (Flash and the MCP
501
+ executor reach the explorer through ``McpActionExecutor`` with index targets)."""
502
+ outcome = await locate(ctx, state, query, context_feedback, agent_name=agent_name)
503
+ registered = register_candidates(ctx, state, outcome)
504
+ return render_text(query, outcome, registered, index_targets=False)
505
+
506
+
507
+ class AskExplorerTool(ArtemisTool):
508
+ """``ask_explorer`` as an :class:`ArtemisTool` (LangChain / GenAI / MCP export)."""
509
+
510
+ def __init__(
511
+ self,
512
+ version: ExplorerVersion | None = None,
513
+ agent_name: str = "operator",
514
+ description: str | None = None,
515
+ category: ToolCategory = "explorer",
516
+ ):
517
+ self.version = version
518
+ self.agent_name = agent_name
519
+ super().__init__(
520
+ name=ASK_EXPLORER_TOOL_NAME,
521
+ description=description or ASK_EXPLORER_DESCRIPTION,
522
+ args_schema=AskExplorerArgs,
523
+ category=category,
524
+ )
525
+
526
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
527
+ async def execute(
528
+ self,
529
+ driver: BaseDeviceDriver | None = None, # pylint: disable=unused-argument
530
+ ctx: ArtemisContext | None = None,
531
+ query: str = "",
532
+ context_feedback: str = "",
533
+ tool_call_id: str | None = None, # pylint: disable=unused-argument
534
+ state: State | None = None,
535
+ version: ExplorerVersion | None = None,
536
+ **kwargs: Any,
537
+ ) -> Any:
538
+ q = query or kwargs.get("Query") or ""
539
+ cf = context_feedback or kwargs.get("ContextFeedback") or ""
540
+ outcome = await locate(
541
+ ctx,
542
+ state,
543
+ q,
544
+ cf,
545
+ agent_name=self.agent_name,
546
+ version=version or self.version,
547
+ )
548
+ registered = register_candidates(ctx, state, outcome)
549
+ return render_operator_blocks(ctx, state, q, outcome, registered)
550
+
551
+
552
+ ask_explorer = AskExplorerTool()
553
+
554
+
555
+ def get_ask_explorer_tool(
556
+ ctx: ArtemisContext,
557
+ version: ExplorerVersion | None = None,
558
+ agent_name: str = "operator",
559
+ ) -> BaseTool:
560
+ """Exports ``ask_explorer`` as a LangChain tool bound to ``ctx``."""
561
+ return AskExplorerTool(version=version, agent_name=agent_name).to_langchain_tool(ctx)
562
+
563
+
564
+ ask_explorer_wrapper = ToolWrapper(
565
+ tool_fn_getter=get_ask_explorer_tool,
566
+ on_success_fn=lambda output: output,
567
+ on_failure_fn=lambda error: f"Explorer failed: {error}",
568
+ )
569
+
570
+
571
+ async def _run_explorer_logic(
572
+ ctx: ArtemisContext,
573
+ state: State,
574
+ query: str,
575
+ context_feedback: str = "",
576
+ version: ExplorerVersion | None = None,
577
+ ) -> Any:
578
+ """Replay-compatible entry: full pipeline with the Operator presentation."""
579
+ outcome = await locate(ctx, state, query, context_feedback, version=version)
580
+ registered = register_candidates(ctx, state, outcome)
581
+ return render_operator_blocks(ctx, state, query, outcome, registered)