android-midscene-automation 0.2.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +35 -0
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,162 @@
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
+ """Gemini File API helpers: upload/poll lifecycle and abandoned-file cleanup."""
16
+
17
+ import asyncio
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+ import time
21
+
22
+ from third_party.mobile_use.utils.logger import get_logger
23
+
24
+ try:
25
+ from datetime import UTC
26
+ except ImportError:
27
+ UTC = UTC
28
+
29
+ logger = get_logger(__name__)
30
+
31
+ _LAST_CLEANUP_TIME = 0.0
32
+ _CLEANUP_LOCK = asyncio.Lock()
33
+
34
+
35
+ async def cleanup_abandoned_gemini_files(client) -> None:
36
+ """Scan and delete remaining cloud video files whose creation time has elapsed standard TTL."""
37
+ global _LAST_CLEANUP_TIME
38
+ if _CLEANUP_LOCK.locked():
39
+ return
40
+
41
+ async with _CLEANUP_LOCK:
42
+ now_ts = time.time()
43
+ if now_ts - _LAST_CLEANUP_TIME < 3600:
44
+ return
45
+ _LAST_CLEANUP_TIME = now_ts
46
+
47
+ try:
48
+ pager = await asyncio.wait_for(client.aio.files.list(), timeout=30)
49
+ files = []
50
+ async for f in pager:
51
+ files.append(f)
52
+ now = datetime.now(UTC)
53
+ files_to_delete = []
54
+
55
+ for f in files:
56
+ display_name = getattr(f, "display_name", "") or ""
57
+
58
+ if (
59
+ display_name.startswith("compressed_")
60
+ or display_name.startswith("audio_")
61
+ or "artemis" in display_name
62
+ ):
63
+ create_time = getattr(f, "created_at", None) or getattr(f, "create_time", None)
64
+ if not create_time:
65
+ continue
66
+
67
+ if isinstance(create_time, str):
68
+ try:
69
+ parsed_time = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
70
+ except Exception:
71
+ continue
72
+ elif isinstance(create_time, datetime):
73
+ parsed_time = create_time
74
+ else:
75
+ continue
76
+
77
+ if parsed_time.tzinfo is None:
78
+ parsed_time = parsed_time.replace(tzinfo=UTC)
79
+
80
+ age_seconds = (now - parsed_time).total_seconds()
81
+
82
+ if age_seconds > 7200:
83
+ files_to_delete.append(f)
84
+ logger.info(
85
+ f"Marked expired cloud asset for deletion: {f.name}"
86
+ f" ({display_name}), Age: {age_seconds / 3600:.1f}h"
87
+ )
88
+
89
+ if files_to_delete:
90
+ logger.info(f"Purging {len(files_to_delete)} expired cloud assets in parallel...")
91
+ tasks = [
92
+ asyncio.wait_for(client.aio.files.delete(name=f.name), timeout=30)
93
+ for f in files_to_delete
94
+ ]
95
+ await asyncio.gather(*tasks, return_exceptions=True)
96
+ except Exception as e:
97
+ logger.warning(f"Routine cloud maintenance skipped: {e}")
98
+
99
+
100
+ async def delete_cloud_file(
101
+ client, file_name: str, cloud_files_to_cleanup: set, *, timeout: float = 30.0
102
+ ) -> None:
103
+ """Best-effort deletion of an uploaded file; failures are logged, never raised."""
104
+ logger.info(f"Cleaning up cloud file {file_name}...")
105
+ try:
106
+ await asyncio.wait_for(client.aio.files.delete(name=file_name), timeout=timeout)
107
+ cloud_files_to_cleanup.discard(file_name)
108
+ except Exception as ce:
109
+ logger.error(f"Failed to delete cloud file {file_name}: {ce}")
110
+
111
+
112
+ async def upload_and_poll_file(client, compressed_path: Path, cloud_files_to_cleanup: set) -> any:
113
+ """Upload a media file to the Gemini File API and poll until it is ACTIVE."""
114
+ logger.info(f"Uploading {compressed_path} to Gemini File API...")
115
+ file_size_mb = compressed_path.stat().st_size / (1024 * 1024)
116
+ upload_timeout = max(30.0, min(120.0, file_size_mb * 2.0))
117
+
118
+ file = await asyncio.wait_for(
119
+ client.aio.files.upload(file=compressed_path),
120
+ timeout=upload_timeout,
121
+ )
122
+ cloud_files_to_cleanup.add(file.name)
123
+
124
+ max_wait = max(60, min(180, 60 + int(file_size_mb * 2)))
125
+
126
+ wait_interval = 0.5
127
+ start_wait = time.time()
128
+ retry_count = 0
129
+ max_retries = 3
130
+
131
+ while True:
132
+ try:
133
+ f_state = await asyncio.wait_for(client.aio.files.get(name=file.name), timeout=20)
134
+ retry_count = 0
135
+ except Exception as poll_error:
136
+ retry_count += 1
137
+ logger.warning(
138
+ f"Temporary issue polling file {file.name} (attempt"
139
+ f" {retry_count}/{max_retries}): {poll_error}"
140
+ )
141
+ if retry_count > max_retries:
142
+ raise RuntimeError(
143
+ f"Failed to poll Gemini File API after {max_retries} attempts: {poll_error}"
144
+ )
145
+ await asyncio.sleep(2.0)
146
+ continue
147
+
148
+ if f_state.state.name == "ACTIVE":
149
+ break
150
+ elif f_state.state.name == "FAILED":
151
+ raise RuntimeError(f"Gemini File API processing failed for {file.name}")
152
+
153
+ if time.time() - start_wait > max_wait:
154
+ raise TimeoutError(
155
+ f"Gemini File API processing timeout for {file.name}"
156
+ f" (Waited {max_wait}s for size {file_size_mb:.1f}MB)"
157
+ )
158
+
159
+ logger.info(f"File {file.name} is {f_state.state.name}, waiting {wait_interval}s...")
160
+ await asyncio.sleep(wait_interval)
161
+ wait_interval = min(3.0, wait_interval * 1.5)
162
+ return file
@@ -0,0 +1,541 @@
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
+ """Native Gemini coordinator loop for the video analyzer main agent.
16
+
17
+ Extracted from ``video_analyzer.py`` as a pure structural split. Patchable
18
+ collaborators are looked up late through the facade module so
19
+ ``mock.patch("artemis.agents.video_analyzer.video_analyzer.<name>")`` targets
20
+ keep working.
21
+ """
22
+
23
+ import asyncio
24
+ import os
25
+ from pathlib import Path
26
+ import shutil
27
+ import tempfile
28
+
29
+ from google.genai import types
30
+
31
+ from artemis.agents.video_analyzer import video_analyzer as _va
32
+ from artemis.agents.video_analyzer.reliability import classify_video_failure
33
+ from artemis.constants import SAFETY_SETTINGS_BLOCK_NONE
34
+ from artemis.data_engine.trace import CURRENT_TRACE_ID, TraceSpan
35
+ from artemis.llm.google import normalize_usage
36
+ from third_party.mobile_use.utils.logger import get_logger
37
+
38
+ logger = get_logger(__name__)
39
+
40
+
41
+ class _DummyResponse:
42
+ """Aggregated view over a consumed stream, shaped like a full response."""
43
+
44
+ def __init__(self, text, function_calls, parts):
45
+ self.text = text
46
+ self.function_calls = function_calls
47
+ self.candidates = [
48
+ types.Candidate(
49
+ content=types.Content(
50
+ role="model",
51
+ parts=parts if parts else ([types.Part.from_text(text=text)] if text else []),
52
+ )
53
+ )
54
+ ]
55
+
56
+
57
+ async def run_native(
58
+ analyzer,
59
+ ctx,
60
+ time_description: str,
61
+ purpose: str,
62
+ system_prompt: str,
63
+ temperature: float,
64
+ thinking_level,
65
+ ) -> tuple[str, str]:
66
+ """Drives the native Gemini coordinator loop to a final answer."""
67
+ tools_declaration = analyzer.tools_declaration
68
+
69
+ contents = [
70
+ types.Content(
71
+ role="user",
72
+ parts=[
73
+ types.Part.from_text(
74
+ text=(f"Time description: {time_description}\nPurpose: {purpose}")
75
+ )
76
+ ],
77
+ )
78
+ ]
79
+
80
+ agent_outcome = ""
81
+ status = "success"
82
+ max_iterations = 10
83
+ iterations = 0
84
+ last_ledger_index = 0
85
+
86
+ try:
87
+ while iterations < max_iterations:
88
+ iterations += 1
89
+ logger.info(f"Iteration {iterations}: Invoking Native Gemini SDK...")
90
+
91
+ analyzer._clean_blackboard()
92
+
93
+ last_ledger_index = _append_ledger_diff(analyzer, contents, last_ledger_index)
94
+
95
+ is_final_turn = iterations == max_iterations
96
+ _append_turn_guidance(contents, iterations, max_iterations, is_final_turn)
97
+
98
+ response = await _stream_main_turn(
99
+ analyzer,
100
+ ctx,
101
+ contents,
102
+ system_prompt,
103
+ temperature,
104
+ thinking_level,
105
+ tools_declaration,
106
+ is_final_turn,
107
+ )
108
+
109
+ function_calls = response.function_calls
110
+ if not function_calls:
111
+ agent_outcome = response.text
112
+ break
113
+
114
+ if response.candidates and response.candidates[0].content:
115
+ contents.append(response.candidates[0].content)
116
+ else:
117
+ contents.append(
118
+ types.Content(
119
+ role="model",
120
+ parts=[types.Part(function_call=fc) for fc in function_calls],
121
+ )
122
+ )
123
+
124
+ tool_response_parts = await _execute_native_tool_calls(analyzer, function_calls)
125
+
126
+ if tool_response_parts:
127
+ contents.append(types.Content(role="user", parts=tool_response_parts))
128
+
129
+ if iterations >= max_iterations and not agent_outcome:
130
+ agent_outcome = (
131
+ "Error: Video analyzer agent reached maximum iterations"
132
+ " without settling on a final answer."
133
+ )
134
+ status = "error"
135
+
136
+ except Exception as e:
137
+ logger.error(f"Video analyzer failed: {e}")
138
+ failure = classify_video_failure(e)
139
+ if failure.should_fallback:
140
+ logger.warning(
141
+ f"Native coordinator failed with {failure.category.value}; "
142
+ "continuing through universal fallback"
143
+ )
144
+ agent_outcome, status = await analyzer._run_universal(
145
+ time_description,
146
+ purpose,
147
+ system_prompt,
148
+ force_fallback=True,
149
+ )
150
+ else:
151
+ agent_outcome = f"Video analysis failed: {e}"
152
+ status = "error"
153
+ finally:
154
+ analyzer._record_reliability_metrics()
155
+ await _cleanup_native_run(analyzer)
156
+
157
+ return agent_outcome, status
158
+
159
+
160
+ def _append_ledger_diff(analyzer, contents: list, last_ledger_index: int) -> int:
161
+ """Appends new blackboard entries (and proofs) since the last coordinator turn."""
162
+ new_entries = analyzer.blackboard_entries[last_ledger_index:]
163
+ if analyzer.enable_ledger and new_entries:
164
+ lines = []
165
+ image_parts = []
166
+ for e in new_entries:
167
+ line = f"[{e['start']}s - {e['end']}s] SUMMARY: {e['summary']}"
168
+ screenshot_file = e.get("screenshot") or e.get("screenshot_path")
169
+ if screenshot_file and Path(screenshot_file).exists():
170
+ line += f" PROOF: {Path(screenshot_file).name}"
171
+ image_parts.append(
172
+ types.Part.from_text(text=f"PROOF: {Path(screenshot_file).name}")
173
+ )
174
+ image_parts.append(
175
+ types.Part.from_bytes(
176
+ data=Path(screenshot_file).read_bytes(),
177
+ mime_type="image/jpeg",
178
+ )
179
+ )
180
+ lines.append(line)
181
+ diff_text = "NEW BLACKBOARD ENTRIES ADDED: " + " | ".join(lines)
182
+ contents.append(
183
+ types.Content(
184
+ role="user",
185
+ parts=[types.Part.from_text(text=diff_text)],
186
+ )
187
+ )
188
+ if image_parts:
189
+ contents.append(types.Content(role="user", parts=image_parts))
190
+ last_ledger_index = len(analyzer.blackboard_entries)
191
+ return last_ledger_index
192
+
193
+
194
+ def _append_turn_guidance(
195
+ contents: list, iterations: int, max_iterations: int, is_final_turn: bool
196
+ ) -> None:
197
+ """Appends per-turn steering messages (final-turn notice or progress nudge)."""
198
+ if is_final_turn:
199
+ contents.append(
200
+ types.Content(
201
+ role="user",
202
+ parts=[
203
+ types.Part.from_text(
204
+ text=(
205
+ "This is your final iteration; all"
206
+ " tools are stripped, and you must"
207
+ " provide your final answer directly."
208
+ )
209
+ )
210
+ ],
211
+ )
212
+ )
213
+ else:
214
+ if iterations > 1:
215
+ contents.append(
216
+ types.Content(
217
+ role="user",
218
+ parts=[
219
+ types.Part.from_text(
220
+ text=(
221
+ "You have not completed the video"
222
+ " analysis yet (iteration"
223
+ f" {iterations} of"
224
+ f" {max_iterations})."
225
+ )
226
+ )
227
+ ],
228
+ )
229
+ )
230
+
231
+
232
+ def _record_main_llm_trace(
233
+ ctx, trace_id, contents: list, system_prompt: str, tools_declaration, is_final_turn: bool
234
+ ) -> None:
235
+ """Records the sanitized coordinator LLM call into the trace store."""
236
+ serialized_contents = []
237
+ for c in contents:
238
+ if hasattr(c, "model_dump"):
239
+ dumped = c.model_dump()
240
+ if isinstance(dumped, dict) and "parts" in dumped and dumped["parts"]:
241
+ for p in dumped["parts"]:
242
+ if (
243
+ isinstance(p, dict)
244
+ and "inline_data" in p
245
+ and p["inline_data"]
246
+ and "data" in p["inline_data"]
247
+ ):
248
+ p["inline_data"]["data"] = "<image_bytes_sanitized>"
249
+ serialized_contents.append(dumped)
250
+ else:
251
+ serialized_contents.append(str(c))
252
+
253
+ ctx.data_engine.record_trace(
254
+ type="llm_call",
255
+ name="video_analyzer_main",
256
+ payload={
257
+ "contents": serialized_contents,
258
+ "system_instruction": system_prompt,
259
+ "tools": (
260
+ [t.name for t in tools_declaration]
261
+ if (tools_declaration and not is_final_turn)
262
+ else []
263
+ ),
264
+ },
265
+ parent_trace_id=trace_id,
266
+ )
267
+
268
+
269
+ async def _stream_main_turn(
270
+ analyzer,
271
+ ctx,
272
+ contents: list,
273
+ system_prompt: str,
274
+ temperature: float,
275
+ thinking_level,
276
+ tools_declaration,
277
+ is_final_turn: bool,
278
+ ):
279
+ """Runs one traced, retry-wrapped streaming turn of the main coordinator."""
280
+ with TraceSpan(name="gemini_main_agent_call") as span:
281
+ trace_id = CURRENT_TRACE_ID.get()
282
+
283
+ if ctx.data_engine and trace_id:
284
+ _record_main_llm_trace(
285
+ ctx, trace_id, contents, system_prompt, tools_declaration, is_final_turn
286
+ )
287
+
288
+ async def run_stream():
289
+ return await _consume_main_stream(
290
+ analyzer,
291
+ ctx,
292
+ span,
293
+ trace_id,
294
+ contents,
295
+ system_prompt,
296
+ temperature,
297
+ thinking_level,
298
+ tools_declaration,
299
+ is_final_turn,
300
+ )
301
+
302
+ response = await _va._invoke_with_retry(
303
+ lambda: asyncio.wait_for(run_stream(), timeout=300),
304
+ "Native Gemini video coordinator",
305
+ )
306
+ span.result = (
307
+ f"Function calls: {len(response.function_calls)}"
308
+ if response.function_calls
309
+ else "Final answer"
310
+ )
311
+ return response
312
+
313
+
314
+ async def _consume_main_stream(
315
+ analyzer,
316
+ ctx,
317
+ span,
318
+ trace_id,
319
+ contents: list,
320
+ system_prompt: str,
321
+ temperature: float,
322
+ thinking_level,
323
+ tools_declaration,
324
+ is_final_turn: bool,
325
+ ) -> _DummyResponse:
326
+ """Starts the coordinator stream and aggregates it into a response object."""
327
+ stream = await asyncio.wait_for(
328
+ analyzer.client.aio.models.generate_content_stream(
329
+ model=analyzer.model_name,
330
+ contents=contents,
331
+ config=types.GenerateContentConfig(
332
+ system_instruction=system_prompt,
333
+ temperature=temperature,
334
+ tools=[]
335
+ if is_final_turn
336
+ else [types.Tool(function_declarations=tools_declaration)],
337
+ safety_settings=SAFETY_SETTINGS_BLOCK_NONE,
338
+ **(
339
+ {"thinking_config": types.ThinkingConfig(thinking_level=thinking_level)}
340
+ if thinking_level
341
+ else {}
342
+ ),
343
+ ),
344
+ ),
345
+ timeout=60,
346
+ )
347
+
348
+ text = ""
349
+ accumulated_parts = []
350
+ function_calls = []
351
+
352
+ async for chunk in stream:
353
+ usage = normalize_usage(getattr(chunk, "usage_metadata", None))
354
+ if usage:
355
+ span.payload["usage_metadata"] = usage
356
+
357
+ chunk_text = chunk.text or ""
358
+ text += chunk_text
359
+ if ctx.data_engine and trace_id and chunk_text:
360
+ ctx.data_engine.stream_output(trace_id, chunk_text, is_thinking=False)
361
+
362
+ if chunk.function_calls:
363
+ function_calls.extend(chunk.function_calls)
364
+
365
+ candidates = getattr(chunk, "candidates", None)
366
+ if candidates and isinstance(candidates, list) and len(candidates) > 0:
367
+ content = getattr(candidates[0], "content", None)
368
+ parts = getattr(content, "parts", None) if content else None
369
+ if parts:
370
+ for part in parts:
371
+ accumulated_parts.append(part)
372
+ if getattr(part, "thought", False) and part.text:
373
+ if ctx.data_engine and trace_id:
374
+ ctx.data_engine.stream_output(
375
+ trace_id,
376
+ part.text,
377
+ is_thinking=True,
378
+ )
379
+ else:
380
+ if chunk.function_calls:
381
+ for fc in chunk.function_calls:
382
+ accumulated_parts.append(types.Part(function_call=fc))
383
+
384
+ return _DummyResponse(text, function_calls, accumulated_parts)
385
+
386
+
387
+ async def _execute_native_tool_calls(analyzer, function_calls: list) -> list:
388
+ """Executes coordinator tool calls (parallel where safe) and collects responses."""
389
+ sub_agent_calls = [
390
+ fc
391
+ for fc in function_calls
392
+ if (fc.name.split(":")[-1] if ":" in fc.name else fc.name) == "spawn_sub_agent"
393
+ ]
394
+ audio_only_calls = [
395
+ fc
396
+ for fc in function_calls
397
+ if (fc.name.split(":")[-1] if ":" in fc.name else fc.name) == "analyze_audio_only"
398
+ ]
399
+ other_calls = [
400
+ fc
401
+ for fc in function_calls
402
+ if (fc.name.split(":")[-1] if ":" in fc.name else fc.name)
403
+ not in ["spawn_sub_agent", "analyze_audio_only"]
404
+ ]
405
+
406
+ tool_response_parts = []
407
+
408
+ if sub_agent_calls:
409
+ await _run_spawn_calls(analyzer, sub_agent_calls, tool_response_parts)
410
+
411
+ if audio_only_calls:
412
+ await _run_audio_calls(analyzer, audio_only_calls, tool_response_parts)
413
+
414
+ await _run_other_calls(analyzer, other_calls, tool_response_parts)
415
+
416
+ return tool_response_parts
417
+
418
+
419
+ async def _run_spawn_calls(analyzer, sub_agent_calls: list, tool_response_parts: list) -> None:
420
+ """Runs spawn_sub_agent calls in parallel and appends their responses."""
421
+ logger.info(f"Executing {len(sub_agent_calls)} sub-agent calls in parallel...")
422
+
423
+ async def bounded_spawn(fc):
424
+ # exec_spawn_sub_agent limits each actual model leaf.
425
+ # Holding the same semaphore around the coordinator
426
+ # would deadlock when all slots wait for child slots.
427
+ return await analyzer.exec_spawn_sub_agent(
428
+ fc.args.get("start_time"),
429
+ fc.args.get("end_time"),
430
+ fc.args.get("specific_query"),
431
+ )
432
+
433
+ tasks = [bounded_spawn(fc) for fc in sub_agent_calls]
434
+ results = await asyncio.gather(*tasks, return_exceptions=True)
435
+
436
+ for fc, result in zip(sub_agent_calls, results):
437
+ if isinstance(result, Exception):
438
+ error_msg = f"Sub-agent failed. Error: {result}"
439
+ logger.error(error_msg)
440
+ tool_response_parts.append(
441
+ types.Part.from_function_response(name=fc.name, response={"error": error_msg})
442
+ )
443
+ else:
444
+ tool_response_parts.append(
445
+ types.Part.from_function_response(name=fc.name, response={"result": result})
446
+ )
447
+
448
+
449
+ async def _run_audio_calls(analyzer, audio_only_calls: list, tool_response_parts: list) -> None:
450
+ """Runs analyze_audio_only calls in parallel and appends their responses."""
451
+ logger.info(f"Executing {len(audio_only_calls)} analyze_audio_only calls in parallel...")
452
+
453
+ async def bounded_audio_spawn(fc):
454
+ async with _va.API_SEMAPHORE:
455
+ return await analyzer.exec_analyze_audio_only(
456
+ fc.args.get("start_time"),
457
+ fc.args.get("end_time"),
458
+ fc.args.get("specific_query"),
459
+ )
460
+
461
+ tasks = [bounded_audio_spawn(fc) for fc in audio_only_calls]
462
+ results = await asyncio.gather(*tasks, return_exceptions=True)
463
+
464
+ for fc, result in zip(audio_only_calls, results):
465
+ if isinstance(result, Exception):
466
+ error_msg = f"Audio analysis failed. Error: {result}"
467
+ logger.error(error_msg)
468
+ tool_response_parts.append(
469
+ types.Part.from_function_response(name=fc.name, response={"error": error_msg})
470
+ )
471
+ else:
472
+ tool_response_parts.append(
473
+ types.Part.from_function_response(name=fc.name, response={"result": result})
474
+ )
475
+
476
+
477
+ async def _run_other_calls(analyzer, other_calls: list, tool_response_parts: list) -> None:
478
+ """Runs remaining coordinator tool calls sequentially."""
479
+ for fc in other_calls:
480
+ name = fc.name.split(":")[-1] if ":" in fc.name else fc.name
481
+ args = fc.args
482
+ logger.info(f"Executing tool '{name}' sequentially...")
483
+
484
+ if name == "extract_segment_metadata":
485
+ try:
486
+ res = await analyzer.exec_extract_segment_metadata(
487
+ args.get("start_time"), args.get("end_time")
488
+ )
489
+ tool_response_parts.append(
490
+ types.Part.from_function_response(name=name, response={"result": res})
491
+ )
492
+ except Exception as e:
493
+ tool_response_parts.append(
494
+ types.Part.from_function_response(name=name, response={"error": str(e)})
495
+ )
496
+
497
+
498
+ async def _cleanup_native_run(analyzer) -> None:
499
+ """Releases remaining cloud files and temporary local media after a run."""
500
+ if analyzer.cloud_files_to_cleanup:
501
+ logger.info(
502
+ f"Cleaning up {len(analyzer.cloud_files_to_cleanup)} remaining"
503
+ " cloud files in parallel..."
504
+ )
505
+ tasks = [
506
+ asyncio.wait_for(analyzer.client.aio.files.delete(name=name), timeout=30)
507
+ for name in list(analyzer.cloud_files_to_cleanup)
508
+ ]
509
+ await asyncio.gather(*tasks, return_exceptions=True)
510
+ analyzer.cloud_files_to_cleanup.clear()
511
+
512
+ if os.environ.get("KEEP_VIDEOS") or os.environ.get("ARTEMIS_DEBUG"):
513
+ logger.info("Skipping cleanup of local video files and directories due to debug mode.")
514
+ else:
515
+ for path in list(analyzer.local_files_to_cleanup):
516
+ try:
517
+ if path.exists():
518
+ logger.info(f"Cleaning up local file {path}...")
519
+ path.unlink()
520
+ except Exception as e:
521
+ logger.error(f"Failed to delete local file {path}: {e}")
522
+
523
+ for dir_path in list(analyzer.local_dirs_to_cleanup):
524
+ try:
525
+ resolved = dir_path.resolve()
526
+ system_dirs = {
527
+ Path("/").resolve(),
528
+ Path("/tmp").resolve(),
529
+ Path("/var/tmp").resolve(),
530
+ Path.home().resolve(),
531
+ Path(tempfile.gettempdir()).resolve(),
532
+ }
533
+ if resolved in system_dirs:
534
+ logger.debug(f"Skipping deletion of root/system directory: {dir_path}")
535
+ continue
536
+
537
+ if dir_path.exists():
538
+ logger.info(f"Cleaning up local directory {dir_path}...")
539
+ shutil.rmtree(dir_path, ignore_errors=True)
540
+ except Exception as e:
541
+ logger.error(f"Failed to delete local directory {dir_path}: {e}")