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,128 @@
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
+ """Failure classification and circuit breaking for video model calls.
16
+
17
+ Thin media-aware extension of :mod:`artemis.llm.reliability`, which owns the
18
+ generic taxonomy and circuit breaker. This module adds the media-processing
19
+ category and the ``should_split`` recovery decision (bisecting long clips).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass
25
+ from enum import StrEnum
26
+
27
+ from artemis.llm.reliability import (
28
+ CircuitBreaker,
29
+ FailureCategory,
30
+ classify_failure,
31
+ extract_status_code,
32
+ )
33
+
34
+
35
+ class VideoFailureCategory(StrEnum):
36
+ RATE_LIMIT = "rate_limit"
37
+ PROVIDER_UNAVAILABLE = "provider_unavailable"
38
+ TIMEOUT = "timeout"
39
+ CONNECTION = "connection"
40
+ AUTHENTICATION = "authentication"
41
+ BAD_REQUEST = "bad_request"
42
+ MEDIA_PROCESSING = "media_processing"
43
+ CANCELLED = "cancelled"
44
+ UNKNOWN = "unknown"
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class VideoFailure:
49
+ category: VideoFailureCategory
50
+ retryable: bool
51
+ should_split: bool
52
+ should_fallback: bool
53
+
54
+
55
+ _MEDIA_MARKERS = ("ffmpeg", "codec", "corrupt", "invalid video", "invalid audio", "media")
56
+ _AGENTIC_REJECTION_MARKERS = ("processing", "agentic")
57
+
58
+
59
+ class SubAgentAnswerExhausted(RuntimeError):
60
+ """The sub-agent exhausted its turns without a valid submit_answer.
61
+
62
+ The caller records a failed segment so a later claim can retry it. The shared
63
+ classifier permits retries, splitting, and fallback for this failure.
64
+ """
65
+
66
+ def __init__(self, turns: int, reason: str):
67
+ self.turns = turns
68
+ self.reason = reason
69
+ super().__init__(
70
+ f"submit_answer was not accepted within {turns} turn(s); last rejection: {reason}"
71
+ )
72
+
73
+
74
+ class AgenticVideoDegraded(RuntimeError):
75
+ """Return an interval to the coordinator after switching to static mode.
76
+
77
+ The coordinator splits oversized clips and retries smaller clips as-is.
78
+ """
79
+
80
+
81
+ def is_agentic_rejection(failure: VideoFailure, error: BaseException) -> bool:
82
+ """True when a bad request is the API refusing agentic video processing.
83
+
84
+ Only an HTTP 400 whose message mentions ``processing`` / ``agentic``
85
+ qualifies; other bad requests (413 payload too large, 404, ...) keep the
86
+ shared handling.
87
+ """
88
+ if failure.category is not VideoFailureCategory.BAD_REQUEST:
89
+ return False
90
+ if extract_status_code(error) != 400:
91
+ return False
92
+ message = str(error).lower()
93
+ return any(marker in message for marker in _AGENTIC_REJECTION_MARKERS)
94
+
95
+
96
+ def classify_video_failure(error: BaseException) -> VideoFailure:
97
+ """Convert provider/transport/media exceptions into recovery decisions."""
98
+ if isinstance(error, AgenticVideoDegraded):
99
+ return VideoFailure(VideoFailureCategory.BAD_REQUEST, False, True, False)
100
+ generic = classify_failure(error)
101
+ message = str(error).lower()
102
+ has_media_marker = any(marker in message for marker in _MEDIA_MARKERS)
103
+
104
+ # Media markers take precedence over generic connection / bad-request /
105
+ # unknown classification (rate-limit, unavailable, timeout, and auth
106
+ # signals still win, matching the original ordering of checks).
107
+ if has_media_marker and generic.category in {
108
+ FailureCategory.CONNECTION,
109
+ FailureCategory.BAD_REQUEST,
110
+ FailureCategory.UNKNOWN,
111
+ }:
112
+ return VideoFailure(VideoFailureCategory.MEDIA_PROCESSING, False, True, True)
113
+
114
+ category = VideoFailureCategory(generic.category.value)
115
+ should_split = category in {VideoFailureCategory.TIMEOUT, VideoFailureCategory.UNKNOWN} or (
116
+ category is VideoFailureCategory.BAD_REQUEST and extract_status_code(error) == 413
117
+ )
118
+ # The video pipeline keeps its historical behavior of falling back to the
119
+ # universal analyzer even for bad requests (a different provider may accept
120
+ # the same media payload).
121
+ should_fallback = generic.should_fallback or category is VideoFailureCategory.BAD_REQUEST
122
+ if category is VideoFailureCategory.CANCELLED:
123
+ should_fallback = False
124
+ return VideoFailure(category, generic.retryable, should_split, should_fallback)
125
+
126
+
127
+ class VideoCircuitBreaker(CircuitBreaker):
128
+ """Context-scoped circuit breaker keyed by provider/model identity."""
@@ -0,0 +1,487 @@
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
+ """Universal (OpenAI-compatible / LangChain) execution paths for the video analyzer.
16
+
17
+ Extracted from ``video_analyzer.py`` as a pure structural split. Patchable
18
+ collaborators (``extract_keyframes_from_video``, ``extract_audio_from_video``,
19
+ semaphores, retry helper, ...) are looked up late through the facade module so
20
+ ``mock.patch("artemis.agents.video_analyzer.video_analyzer.<name>")`` targets
21
+ keep working.
22
+ """
23
+
24
+ import asyncio
25
+ import base64
26
+ import glob
27
+ from pathlib import Path
28
+ import re
29
+ from typing import Any
30
+
31
+ from langchain_core.messages import (
32
+ BaseMessage,
33
+ HumanMessage,
34
+ SystemMessage,
35
+ ToolMessage,
36
+ )
37
+
38
+ from artemis.agents.video_analyzer import video_analyzer as _va
39
+ from artemis.agents.video_analyzer.reliability import (
40
+ SubAgentAnswerExhausted,
41
+ classify_video_failure,
42
+ )
43
+ from artemis.data_engine.trace import CURRENT_TRACE_ID, TraceSpan
44
+ from third_party.mobile_use.utils.logger import get_logger
45
+
46
+ logger = get_logger(__name__)
47
+
48
+
49
+ async def exec_single_chunk_universal(
50
+ analyzer,
51
+ compressed_path: Path,
52
+ raw_path: Path,
53
+ start_time: float,
54
+ end_time: float | None,
55
+ actual_start: float,
56
+ prompt_with_context: str,
57
+ specific_query: str,
58
+ force_fallback: bool = False,
59
+ ) -> str:
60
+ """Executes sub-agent chunk analysis via keyframe extraction and LangChain ChatModel."""
61
+ keyframes = _gather_universal_frames(analyzer, raw_path, start_time, end_time, actual_start)
62
+
63
+ user_blocks, audio_block = await _build_universal_user_blocks(
64
+ analyzer, keyframes, actual_start, prompt_with_context, raw_path
65
+ )
66
+
67
+ messages = [
68
+ SystemMessage(content=analyzer.sub_system_prompt),
69
+ HumanMessage(content=user_blocks),
70
+ ]
71
+
72
+ trace_id = CURRENT_TRACE_ID.get()
73
+ if analyzer.ctx.data_engine and trace_id:
74
+ analyzer.ctx.data_engine.record_trace(
75
+ type="llm_call",
76
+ name="video_sub_agent_universal",
77
+ payload={
78
+ "num_keyframes": len(keyframes),
79
+ "prompt": prompt_with_context,
80
+ "system_instruction": analyzer.sub_system_prompt,
81
+ },
82
+ parent_trace_id=trace_id,
83
+ )
84
+
85
+ response = await _invoke_universal_with_visual_retry(
86
+ analyzer, messages, user_blocks, audio_block, force_fallback
87
+ )
88
+
89
+ final_summary, final_analysis, timeline_events = _extract_submit_payload(response)
90
+
91
+ await _persist_universal_events(
92
+ analyzer,
93
+ timeline_events,
94
+ start_time,
95
+ end_time,
96
+ actual_start,
97
+ raw_path,
98
+ specific_query,
99
+ final_summary,
100
+ )
101
+
102
+ end_str = f"{end_time:.1f}s" if end_time is not None else "unknown"
103
+ return (
104
+ f"[from {start_time:.1f}s to {end_str}] Summary: {final_summary} Analysis: {final_analysis}"
105
+ )
106
+
107
+
108
+ def _gather_universal_frames(
109
+ analyzer,
110
+ raw_path: Path,
111
+ start_time: float,
112
+ end_time: float | None,
113
+ actual_start: float,
114
+ ) -> list:
115
+ """Extracts uniform keyframes plus dense action-window frames, merged by timestamp."""
116
+ # Universal providers consume stills rather than the uploaded clip. Use
117
+ # the raw extracted segment so slowdown/transcode PTS never skew labels.
118
+ keyframes = _va.extract_keyframes_from_video(
119
+ raw_path,
120
+ fps=1.0,
121
+ max_frames=30,
122
+ max_dimension=1080,
123
+ )
124
+ dense_offsets = analyzer._dense_action_offsets(
125
+ start_time,
126
+ end_time if end_time is not None else start_time,
127
+ actual_start,
128
+ )
129
+ targeted_frames = _va.extract_frames_at_timestamps(
130
+ raw_path,
131
+ dense_offsets,
132
+ max_frames=analyzer.max_dense_action_frames,
133
+ max_dimension=1080,
134
+ )
135
+ merged_frames = {round(timestamp, 3): data for timestamp, data in keyframes}
136
+ merged_frames.update({round(timestamp, 3): data for timestamp, data in targeted_frames})
137
+ return sorted(merged_frames.items())
138
+
139
+
140
+ async def _build_universal_user_blocks(
141
+ analyzer,
142
+ keyframes: list,
143
+ actual_start: float,
144
+ prompt_with_context: str,
145
+ raw_path: Path,
146
+ ) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
147
+ """Builds the multimodal user content blocks (text, frames, optional audio)."""
148
+ user_blocks: list[dict[str, Any]] = [{"type": "text", "text": prompt_with_context}]
149
+
150
+ for ts_sec, frame_bytes in keyframes:
151
+ b64_str = base64.b64encode(frame_bytes).decode("utf-8")
152
+ abs_time = actual_start + ts_sec
153
+ user_blocks.append(
154
+ {
155
+ "type": "text",
156
+ "text": f"--- Video Keyframe at {abs_time:.1f}s ---",
157
+ }
158
+ )
159
+ user_blocks.append(
160
+ {
161
+ "type": "image_url",
162
+ "image_url": {"url": f"data:image/jpeg;base64,{b64_str}"},
163
+ }
164
+ )
165
+
166
+ audio_block = None
167
+ try:
168
+ async with _va.TRANSCODE_SEMAPHORE:
169
+ audio_path = await _va.extract_audio_from_video(raw_path)
170
+ analyzer.local_files_to_cleanup.add(audio_path)
171
+ analyzer.local_dirs_to_cleanup.add(audio_path.parent)
172
+ audio_block = analyzer._audio_content_block(audio_path)
173
+ user_blocks.append(audio_block)
174
+ except Exception as audio_error:
175
+ logger.info(f"No usable audio track for universal video chunk: {audio_error}")
176
+
177
+ return user_blocks, audio_block
178
+
179
+
180
+ async def _invoke_universal_with_visual_retry(
181
+ analyzer,
182
+ messages: list[BaseMessage],
183
+ user_blocks: list[dict[str, Any]],
184
+ audio_block: dict[str, Any] | None,
185
+ force_fallback: bool,
186
+ ):
187
+ """Invokes the universal model, retrying visually when audio input is rejected."""
188
+ with TraceSpan(name="universal_sub_agent_call") as span:
189
+ try:
190
+ response = await analyzer._invoke_universal_model(
191
+ messages,
192
+ [_va.UNIVERSAL_SUBMIT_ANSWER_TOOL],
193
+ timeout=analyzer.model_call_timeout_seconds,
194
+ label="Universal video sub-agent",
195
+ force_fallback=force_fallback,
196
+ )
197
+ except Exception as multimodal_error:
198
+ failure = classify_video_failure(multimodal_error)
199
+ if audio_block is None or failure.category.value not in {
200
+ "bad_request",
201
+ "media_processing",
202
+ }:
203
+ raise
204
+ logger.warning(
205
+ "Configured video model rejected audio input; retrying this chunk visually"
206
+ )
207
+ visual_blocks = [block for block in user_blocks if block is not audio_block]
208
+ messages = [
209
+ SystemMessage(content=analyzer.sub_system_prompt),
210
+ HumanMessage(content=visual_blocks),
211
+ ]
212
+ response = await analyzer._invoke_universal_model(
213
+ messages,
214
+ [_va.UNIVERSAL_SUBMIT_ANSWER_TOOL],
215
+ timeout=analyzer.model_call_timeout_seconds,
216
+ label="Universal visual-only sub-agent",
217
+ force_fallback=force_fallback,
218
+ )
219
+ span.result = f"Received response (tool_calls={len(getattr(response, 'tool_calls', []))})"
220
+ return response
221
+
222
+
223
+ def _extract_submit_payload(response) -> tuple[str, str, list]:
224
+ """Extract a summary, analysis, and events from the model response.
225
+
226
+ Use the first submit_answer call if its summary is non-empty. Otherwise,
227
+ fall back to response text, using its first 200 characters as the summary
228
+ and dropping any events from the rejected call. Raise SubAgentAnswerExhausted
229
+ if neither source provides an answer.
230
+ """
231
+ for tc in getattr(response, "tool_calls", None) or []:
232
+ if tc.get("name") != "submit_answer":
233
+ continue
234
+ args = tc.get("args") or {}
235
+ final_summary = str(args.get("summary", "")).strip().replace("\n", " ")
236
+ if not final_summary:
237
+ break
238
+ final_analysis = (
239
+ str(args.get("analysis", "")).strip().replace("\n", " ") or "No analysis provided."
240
+ )
241
+ return final_summary, final_analysis, list(args.get("timeline_events") or [])
242
+
243
+ raw_content = getattr(response, "content", None)
244
+ text_content = str(raw_content).strip() if raw_content else ""
245
+ if text_content:
246
+ return text_content[:200].replace("\n", " "), text_content, []
247
+
248
+ raise SubAgentAnswerExhausted(
249
+ 1, "the model neither called submit_answer with a summary nor replied with text"
250
+ )
251
+
252
+
253
+ async def _persist_universal_events(
254
+ analyzer,
255
+ timeline_events: list,
256
+ start_time: float,
257
+ end_time: float | None,
258
+ actual_start: float,
259
+ raw_path: Path,
260
+ specific_query: str,
261
+ final_summary: str,
262
+ ) -> None:
263
+ """Processes timeline events & saves screenshots for the blackboard ledger."""
264
+ for event in timeline_events:
265
+ if not isinstance(event, dict):
266
+ continue
267
+ try:
268
+ v_ts = float(event.get("verification_timestamp_secs", start_time))
269
+ conf = float(event.get("confidence_score", 0.8))
270
+ event_start = float(event.get("start_time", start_time))
271
+ event_end = float(
272
+ event.get(
273
+ "end_time",
274
+ end_time if end_time is not None else event_start,
275
+ )
276
+ )
277
+ except (TypeError, ValueError):
278
+ logger.warning(f"Ignoring malformed universal video event: {event}")
279
+ continue
280
+ interval_end = end_time if end_time is not None else max(event_end, v_ts)
281
+ if (
282
+ not 0.0 <= conf <= 1.0
283
+ or event_end < event_start
284
+ or event_start < start_time
285
+ or event_end > interval_end
286
+ or v_ts < start_time
287
+ or v_ts > interval_end
288
+ ):
289
+ logger.warning(f"Ignoring out-of-range universal video event: {event}")
290
+ continue
291
+
292
+ screenshot_path_str = None
293
+ try:
294
+ rel_offset = max(0.0, v_ts - actual_start)
295
+ out_dir = _va.get_temp_dir("video_analyzer")
296
+ out_dir.mkdir(parents=True, exist_ok=True)
297
+
298
+ existing_imgs = glob.glob(f"{out_dir}/img_*.jpg")
299
+ max_idx = -1
300
+ for img in existing_imgs:
301
+ m = re.search(r"img_(\d+)\.jpg$", img)
302
+ if m:
303
+ max_idx = max(max_idx, int(m.group(1)))
304
+ next_idx = max_idx + 1
305
+ shot_path = out_dir / f"img_{next_idx}.jpg"
306
+
307
+ ffmpeg_cmd = [
308
+ "ffmpeg",
309
+ "-ss",
310
+ str(rel_offset),
311
+ "-i",
312
+ str(raw_path),
313
+ "-vframes",
314
+ "1",
315
+ "-q:v",
316
+ "2",
317
+ "-y",
318
+ str(shot_path),
319
+ ]
320
+ proc = await asyncio.create_subprocess_exec(
321
+ *ffmpeg_cmd,
322
+ stdout=asyncio.subprocess.PIPE,
323
+ stderr=asyncio.subprocess.PIPE,
324
+ )
325
+ await proc.communicate()
326
+ if shot_path.exists():
327
+ analyzer.local_files_to_cleanup.add(shot_path)
328
+ screenshot_path_str = str(shot_path)
329
+ except Exception as fe:
330
+ logger.warning(f"Failed extracting keyframe for event {v_ts}s: {fe}")
331
+
332
+ entry = {
333
+ "start": event_start,
334
+ "end": event_end,
335
+ "target": specific_query,
336
+ "summary": event.get("transcription", final_summary),
337
+ "confidence_score": conf,
338
+ }
339
+ if screenshot_path_str:
340
+ entry["screenshot"] = screenshot_path_str
341
+ analyzer._record_blackboard_entry(entry)
342
+
343
+
344
+ async def exec_analyze_audio_universal(
345
+ analyzer,
346
+ audio_path: Path,
347
+ start_time: float,
348
+ end_time: float | None,
349
+ actual_start: float,
350
+ prompt_with_context: str,
351
+ specific_query: str,
352
+ force_fallback: bool = False,
353
+ ) -> str:
354
+ """Executes audio analysis via Universal LangChain ChatModel."""
355
+ messages = [
356
+ SystemMessage(content=analyzer.audio_system_prompt),
357
+ HumanMessage(
358
+ content=[
359
+ {"type": "text", "text": prompt_with_context},
360
+ analyzer._audio_content_block(audio_path),
361
+ ]
362
+ ),
363
+ ]
364
+ response = await analyzer._invoke_universal_model(
365
+ messages,
366
+ [_va.UNIVERSAL_SUBMIT_ANSWER_TOOL],
367
+ timeout=analyzer.model_call_timeout_seconds,
368
+ label="Universal audio sub-agent",
369
+ force_fallback=force_fallback,
370
+ )
371
+ # Same resolution order as the video chunk (raises SubAgentAnswerExhausted
372
+ # when the model produced nothing usable); audio has no timeline events.
373
+ final_summary, final_analysis, _ = _extract_submit_payload(response)
374
+
375
+ end_val = end_time if end_time is not None else "unknown"
376
+ analyzer._record_blackboard_entry(
377
+ {
378
+ "start": start_time,
379
+ "end": end_val,
380
+ "target": specific_query,
381
+ "summary": final_summary,
382
+ "confidence_score": 0.8,
383
+ },
384
+ modality="audio",
385
+ )
386
+
387
+ end_str = f"{end_time:.1f}s" if end_time is not None else "unknown"
388
+ return (
389
+ f"[from {start_time:.1f}s to {end_str}] Audio Summary: {final_summary}"
390
+ f" Analysis: {final_analysis}"
391
+ )
392
+
393
+
394
+ async def run_universal(
395
+ analyzer,
396
+ time_description: str,
397
+ purpose: str,
398
+ system_prompt: str,
399
+ *,
400
+ force_fallback: bool = False,
401
+ ) -> tuple[str, str]:
402
+ """Runs VideoAnalyzer main reasoning loop using Universal LangChain BaseChatModel."""
403
+ messages: list[BaseMessage] = [
404
+ SystemMessage(content=system_prompt),
405
+ HumanMessage(
406
+ content=(
407
+ f"Time description: {time_description}\n"
408
+ f"Purpose: {purpose}\n"
409
+ f"Persistent video blackboard: {analyzer.blackboard_ledger}\n"
410
+ "Reuse sufficient prior evidence and do not request an identical "
411
+ "interval/query analysis again.\n"
412
+ "Please coordinate video metadata extraction and sub-agent analysis."
413
+ )
414
+ ),
415
+ ]
416
+
417
+ max_iterations = 6
418
+ iterations = 0
419
+ agent_outcome = ""
420
+ status = "success"
421
+
422
+ try:
423
+ while iterations < max_iterations:
424
+ iterations += 1
425
+ response = await _va._invoke_with_retry(
426
+ lambda: analyzer._invoke_universal_model(
427
+ messages,
428
+ _va.UNIVERSAL_MAIN_TOOLS,
429
+ timeout=300,
430
+ label="Universal video coordinator",
431
+ force_fallback=force_fallback,
432
+ ),
433
+ "Universal video coordinator",
434
+ )
435
+ messages.append(response)
436
+
437
+ if not getattr(response, "tool_calls", None):
438
+ agent_outcome = str(response.content)
439
+ break
440
+
441
+ for tc in response.tool_calls:
442
+ t_name = tc["name"]
443
+ t_args = tc.get("args", {})
444
+ t_id = tc.get("id", f"call_{iterations}")
445
+ logger.info(f"Executing universal tool '{t_name}' with args {t_args}")
446
+
447
+ res = await _execute_universal_tool(analyzer, t_name, t_args)
448
+
449
+ messages.append(ToolMessage(tool_call_id=t_id, name=t_name, content=str(res)))
450
+
451
+ if not agent_outcome:
452
+ agent_outcome = "Video analyzer completed all turns without error."
453
+
454
+ except Exception as e:
455
+ logger.error(f"Universal video analyzer failed: {e}")
456
+ agent_outcome = f"Video analysis failed: {e}"
457
+ status = "error"
458
+
459
+ analyzer._record_reliability_metrics()
460
+ return agent_outcome, status
461
+
462
+
463
+ async def _execute_universal_tool(analyzer, t_name: str, t_args: dict) -> str:
464
+ """Dispatches one universal coordinator tool call, converting errors to text."""
465
+ try:
466
+ if t_name == "extract_segment_metadata":
467
+ res = await analyzer.exec_extract_segment_metadata(
468
+ t_args.get("start_time"), t_args.get("end_time")
469
+ )
470
+ elif t_name == "spawn_sub_agent":
471
+ res = await analyzer.exec_spawn_sub_agent(
472
+ t_args.get("start_time"),
473
+ t_args.get("end_time"),
474
+ t_args.get("specific_query"),
475
+ )
476
+ elif t_name == "analyze_audio_only":
477
+ res = await analyzer.exec_analyze_audio_only(
478
+ t_args.get("start_time"),
479
+ t_args.get("end_time"),
480
+ t_args.get("specific_query"),
481
+ )
482
+ else:
483
+ res = f"Error: Tool '{t_name}' is not recognized."
484
+ except Exception as tool_err:
485
+ logger.error(f"Error executing tool '{t_name}': {tool_err}")
486
+ res = f"Error executing tool '{t_name}': {tool_err}"
487
+ return res