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,512 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import re
17
+ from typing import Any
18
+
19
+ try:
20
+ from admin_console.database.connection import db_session
21
+ except ImportError:
22
+ from apps.admin_console.database.connection import db_session
23
+
24
+
25
+ class StepRepository:
26
+ """Repository handling querying and formatting of steps and associated traces."""
27
+
28
+ def __init__(self, db_path=None):
29
+ self.db_path = db_path
30
+
31
+ def get_step_session_id(self, step_id: str) -> str | None:
32
+ with db_session(self.db_path) as conn:
33
+ cursor = conn.cursor()
34
+ cursor.execute("SELECT session_id FROM steps WHERE step_id = ?", (step_id,))
35
+ row = cursor.fetchone()
36
+ return row["session_id"] if row else None
37
+
38
+ @staticmethod
39
+ def _clean_value(val: Any) -> Any:
40
+ """Recursively cleans values to remove non-serializable and internal
41
+ Python runtime representations.
42
+ """
43
+ if val is None:
44
+ return None
45
+ if isinstance(val, (bool, int, float)):
46
+ return val
47
+ if isinstance(val, str):
48
+ if (
49
+ "object at 0x" in val
50
+ or val.startswith("<artemis.")
51
+ or val.startswith("<controller")
52
+ ):
53
+ return None
54
+ return val
55
+ if isinstance(val, dict):
56
+ cleaned = {}
57
+ for k, v in val.items():
58
+ cv = StepRepository._clean_value(v)
59
+ if cv is not None:
60
+ cleaned[k] = cv
61
+ return cleaned
62
+ if isinstance(val, (list, tuple)):
63
+ cleaned_list = []
64
+ for item in val:
65
+ cv = StepRepository._clean_value(item)
66
+ if cv is not None:
67
+ cleaned_list.append(cv)
68
+ return cleaned_list
69
+ val_str = str(val)
70
+ if "object at 0x" in val_str or val_str.startswith("<"):
71
+ return None
72
+ return val_str
73
+
74
+ @staticmethod
75
+ def _extract_clean_text_and_images(content: Any) -> tuple[str, int]:
76
+ """Extracts readable text and count of image blocks from LLM content."""
77
+ if not content:
78
+ return "", 0
79
+ if isinstance(content, str):
80
+ if content.startswith("data:image/") or "base64," in content[:50]:
81
+ return "", 1
82
+ return content, 0
83
+ if isinstance(content, dict):
84
+ if content.get("type") == "image_url" or "image_url" in content:
85
+ return "", 1
86
+ if content.get("type") == "text":
87
+ return str(content.get("text", "")), 0
88
+ t = content.get("text") or content.get("thought") or ""
89
+ return str(t), 0
90
+ if isinstance(content, list):
91
+ total_text = ""
92
+ num_images = 0
93
+ for item in content:
94
+ txt, imgs = StepRepository._extract_clean_text_and_images(item)
95
+ total_text += txt
96
+ num_images += imgs
97
+ return total_text, num_images
98
+ return str(content), 0
99
+
100
+ @staticmethod
101
+ def _extract_token_usage_from_trace(payload_raw: Any) -> tuple[int, int, int]:
102
+ """Extract prompt, completion, and total tokens from an LLM trace payload with multimodal awareness."""
103
+ if not payload_raw:
104
+ return 0, 0, 0
105
+ payload_obj = payload_raw
106
+ if isinstance(payload_raw, str):
107
+ try:
108
+ payload_obj = json.loads(payload_raw)
109
+ except (ValueError, TypeError):
110
+ # Non-JSON payload: the isinstance(dict) guard below bails out.
111
+ pass
112
+ if not isinstance(payload_obj, dict):
113
+ return 0, 0, 0
114
+
115
+ u = payload_obj.get("token_usage") or payload_obj.get("usage_metadata")
116
+ if isinstance(u, dict):
117
+ pr = u.get("prompt_tokens") or u.get("prompt_token_count") or u.get("input_tokens") or 0
118
+ co = (
119
+ u.get("completion_tokens")
120
+ or u.get("candidates_token_count")
121
+ or u.get("output_tokens")
122
+ or 0
123
+ )
124
+ to = u.get("total_tokens") or u.get("total_token_count") or (pr + co)
125
+ if to > 0:
126
+ return int(pr), int(co), int(to)
127
+
128
+ # Fallback for historical traces: estimate tokens from text and images
129
+ msgs = payload_obj.get("messages", [])
130
+ resp = payload_obj.get("response", [])
131
+
132
+ prompt_text = ""
133
+ prompt_images = 0
134
+ if isinstance(msgs, list):
135
+ for m in msgs:
136
+ if isinstance(m, dict):
137
+ t, imgs = StepRepository._extract_clean_text_and_images(m.get("content"))
138
+ prompt_text += t
139
+ prompt_images += imgs
140
+ else:
141
+ t, imgs = StepRepository._extract_clean_text_and_images(m)
142
+ prompt_text += t
143
+ prompt_images += imgs
144
+ else:
145
+ t, imgs = StepRepository._extract_clean_text_and_images(msgs)
146
+ prompt_text += t
147
+ prompt_images += imgs
148
+
149
+ resp_text = ""
150
+ resp_images = 0
151
+ if isinstance(resp, list):
152
+ for r in resp:
153
+ if isinstance(r, dict):
154
+ t, imgs = StepRepository._extract_clean_text_and_images(r.get("content"))
155
+ resp_text += t
156
+ resp_images += imgs
157
+ else:
158
+ t, imgs = StepRepository._extract_clean_text_and_images(r)
159
+ resp_text += t
160
+ resp_images += imgs
161
+
162
+ # Standard Gemini multimodal token pricing: 258 tokens per image
163
+ pr = (len(prompt_text) // 4) + (prompt_images * 258)
164
+ co = (len(resp_text) // 4) + (resp_images * 258)
165
+ return int(pr), int(co), int(pr + co)
166
+
167
+ def _clean_tool_payload(
168
+ self,
169
+ payload_raw: Any,
170
+ trace_type: str | None = None,
171
+ trace_name: str | None = None,
172
+ ) -> Any:
173
+ if not payload_raw:
174
+ return None
175
+ payload_obj = payload_raw
176
+ if isinstance(payload_raw, str):
177
+ try:
178
+ payload_obj = json.loads(payload_raw)
179
+ except (ValueError, TypeError):
180
+ # Non-JSON payload: the isinstance(dict) guard below bails out.
181
+ pass
182
+
183
+ if not isinstance(payload_obj, dict):
184
+ return None
185
+
186
+ if trace_type == "llm_call":
187
+ allowed_keys = (
188
+ "error",
189
+ "delay",
190
+ "attempt",
191
+ "max_retries",
192
+ "provider",
193
+ "source",
194
+ "recoverable",
195
+ "pause",
196
+ "request_id",
197
+ "scheduled_at",
198
+ "waited_seconds",
199
+ "retries",
200
+ )
201
+ cleaned = {
202
+ key: self._clean_value(payload_obj.get(key))
203
+ for key in allowed_keys
204
+ if key in payload_obj
205
+ }
206
+ return cleaned or None
207
+
208
+ args = payload_obj.get("args", payload_obj)
209
+ cleaned_args = (
210
+ self._clean_value(args) if isinstance(args, dict) else self._clean_value(args)
211
+ )
212
+ result_payload: dict[str, Any] = {"args": cleaned_args}
213
+
214
+ # Extract structured action execution results. Video analysis keeps its
215
+ # result because the task stream needs to distinguish cached, partial,
216
+ # recovering, waiting, completed, and terminal outcomes. Other tools
217
+ # retain the historical compact payload to avoid sending large blobs.
218
+ res = payload_obj.get("result")
219
+ is_video_analysis = str(trace_name or "").lower() in {
220
+ "video_analysis",
221
+ "video_analyzer",
222
+ "video_analyzer_pure",
223
+ "spawn_sub_agent",
224
+ "analyze_audio_only",
225
+ }
226
+ if isinstance(res, dict):
227
+ if res.get("post_image_name"):
228
+ result_payload["post_image_name"] = res["post_image_name"]
229
+ if res.get("pre_image_name"):
230
+ result_payload["pre_image_name"] = res["pre_image_name"]
231
+ if res.get("outcome"):
232
+ result_payload["outcome"] = res["outcome"]
233
+ if is_video_analysis:
234
+ result_payload["result"] = self._clean_value(res)
235
+ elif res:
236
+ res_str = str(res)
237
+ if is_video_analysis:
238
+ result_payload["result"] = res_str
239
+ match = re.search(r"([a-f0-9]{64})", res_str)
240
+ if match:
241
+ result_payload["post_image_name"] = match.group(1)
242
+
243
+ return result_payload
244
+
245
+ @staticmethod
246
+ def _extract_legacy_pause_error(payload_raw: Any) -> str | None:
247
+ """Extract an LLM error from the warning trace used by older runners."""
248
+ payload_obj = payload_raw
249
+ if isinstance(payload_raw, str):
250
+ try:
251
+ payload_obj = json.loads(payload_raw)
252
+ except Exception:
253
+ return None
254
+ if not isinstance(payload_obj, dict):
255
+ return None
256
+
257
+ message = payload_obj.get("message")
258
+ if not isinstance(message, str) or "LLM Error:" not in message:
259
+ return None
260
+ pause_marker = ". Pausing execution"
261
+ if pause_marker not in message:
262
+ return None
263
+
264
+ error = message.split("LLM Error:", 1)[1].split(pause_marker, 1)[0].strip()
265
+ return error or None
266
+
267
+ def _normalize_display_trace(self, trace_dict: dict[str, Any]) -> dict[str, Any]:
268
+ """Normalize traces consumed by the task stream's tool/error cards."""
269
+ if trace_dict.get("type") == "log":
270
+ error = self._extract_legacy_pause_error(trace_dict.get("payload"))
271
+ if error:
272
+ trace_dict.update(
273
+ {
274
+ "type": "llm_call",
275
+ "name": "llm_pause",
276
+ "status": "failed",
277
+ "payload": {"error": error, "pause": True},
278
+ }
279
+ )
280
+ return trace_dict
281
+
282
+ trace_dict["payload"] = self._clean_tool_payload(
283
+ trace_dict.get("payload"), trace_dict.get("type"), trace_dict.get("name")
284
+ )
285
+ return trace_dict
286
+
287
+ def get_session_steps(self, session_id: str, client: str | None = None) -> list[dict[str, Any]]:
288
+ with db_session(self.db_path) as conn:
289
+ cursor = conn.cursor()
290
+ cursor.execute(
291
+ "SELECT * FROM steps WHERE session_id = ? ORDER BY step_number ASC",
292
+ (session_id,),
293
+ )
294
+ rows = cursor.fetchall()
295
+
296
+ steps = []
297
+ for r in rows:
298
+ step_dict = dict(r)
299
+
300
+ # Strip raw binary bytes for API efficiency (clients fetch images by url/hash)
301
+ step_dict["pre_screenshot_bytes"] = None
302
+ step_dict["post_screenshot_bytes"] = None
303
+
304
+ # Parse JSON fields safely
305
+ for json_col in [
306
+ "action_taken",
307
+ "last_execution_result",
308
+ "extra_metadata",
309
+ ]:
310
+ if step_dict.get(json_col):
311
+ try:
312
+ step_dict[json_col] = json.loads(step_dict[json_col])
313
+ except (ValueError, TypeError):
314
+ # Non-JSON column value: keep the raw string.
315
+ pass
316
+
317
+ # If post_image_name is identical to pre_image_name, normalize to None
318
+ pre_img = step_dict.get("pre_image_name")
319
+ if pre_img and step_dict.get("post_image_name") == pre_img:
320
+ step_dict["post_image_name"] = None
321
+
322
+ # Fetch action and generic tool traces for this step
323
+ step_id = step_dict.get("step_id")
324
+ if step_id:
325
+ cursor.execute(
326
+ "SELECT t1.trace_id, t1.parent_trace_id, t1.step_id,"
327
+ " t1.type, t1.name, t1.timestamp, t1.duration, t1.status,"
328
+ " t1.payload, t2.name as agent_name FROM traces t1 LEFT"
329
+ " JOIN traces t2 ON t1.parent_trace_id = t2.trace_id WHERE"
330
+ " t1.step_id = ? AND (t1.type = 'tool' OR t1.type = 'action' OR (t1.type ="
331
+ " 'llm_call' AND t1.status IN ('failed', 'retrying')) OR (t1.type = 'log' AND"
332
+ " t1.payload LIKE '%LLM Error:%Pausing execution%' AND NOT EXISTS"
333
+ " (SELECT 1 FROM traces t3 WHERE t3.session_id = t1.session_id AND"
334
+ " t3.type = 'llm_call' AND t3.status = 'failed' AND"
335
+ " ABS(t3.timestamp - t1.timestamp) < 2))) ORDER BY"
336
+ " t1.timestamp ASC",
337
+ (step_id,),
338
+ )
339
+ trace_rows = cursor.fetchall()
340
+ generic_tools = []
341
+ for tr in trace_rows:
342
+ trace_dict = self._normalize_display_trace(dict(tr))
343
+ generic_tools.append(trace_dict)
344
+ step_dict["generic_tools"] = generic_tools
345
+
346
+ # Compute token usage for this step from all LLM call traces
347
+ cursor.execute(
348
+ "SELECT payload FROM traces WHERE step_id = ? AND type = 'llm_call'",
349
+ (step_id,),
350
+ )
351
+ llm_rows = cursor.fetchall()
352
+ step_p, step_c, step_t = 0, 0, 0
353
+ for lr in llm_rows:
354
+ p, c, t = self._extract_token_usage_from_trace(lr["payload"])
355
+ step_p += p
356
+ step_c += c
357
+ step_t += t
358
+
359
+ # If step_t is 0, also check extra_metadata for token_usage
360
+ if step_t == 0 and step_dict.get("extra_metadata"):
361
+ meta = (
362
+ step_dict["extra_metadata"]
363
+ if isinstance(step_dict["extra_metadata"], dict)
364
+ else {}
365
+ )
366
+ u = meta.get("token_usage")
367
+ if isinstance(u, dict):
368
+ step_p = int(u.get("prompt_tokens") or u.get("input_tokens") or 0)
369
+ step_c = int(u.get("completion_tokens") or u.get("output_tokens") or 0)
370
+ step_t = int(u.get("total_tokens") or (step_p + step_c))
371
+
372
+ step_dict["token_usage"] = {
373
+ "prompt_tokens": step_p,
374
+ "completion_tokens": step_c,
375
+ "total_tokens": step_t,
376
+ }
377
+ step_dict["total_tokens"] = step_t
378
+ else:
379
+ step_dict["generic_tools"] = []
380
+ step_dict["token_usage"] = {
381
+ "prompt_tokens": 0,
382
+ "completion_tokens": 0,
383
+ "total_tokens": 0,
384
+ }
385
+ step_dict["total_tokens"] = 0
386
+
387
+ try:
388
+ from artemis.utils.coordinates import normalize_step_actions
389
+
390
+ step_dict = normalize_step_actions(step_dict)
391
+ except ImportError:
392
+ pass
393
+
394
+ steps.append(step_dict)
395
+
396
+ # Fetch step-less tool traces (e.g. planner tools or turns without recorded step)
397
+ cursor.execute(
398
+ "SELECT t1.trace_id, t1.parent_trace_id, t1.step_id, t1.type,"
399
+ " t1.name, t1.timestamp, t1.duration, t1.status, t1.payload,"
400
+ " t2.name as agent_name FROM traces t1 LEFT JOIN traces t2 ON"
401
+ " t1.parent_trace_id = t2.trace_id WHERE t1.session_id = ? AND"
402
+ " (t1.step_id IS NULL OR t1.step_id = '' OR t1.step_id NOT IN (SELECT step_id FROM steps WHERE session_id = ?)) AND (t1.type = 'tool' OR"
403
+ " (t1.type = 'llm_call' AND t1.status IN ('failed', 'retrying')) OR (t1.type = 'log'"
404
+ " AND t1.payload LIKE '%LLM Error:%Pausing execution%' AND NOT EXISTS"
405
+ " (SELECT 1 FROM traces t3 WHERE t3.session_id = t1.session_id AND"
406
+ " t3.type = 'llm_call' AND t3.status = 'failed' AND"
407
+ " ABS(t3.timestamp - t1.timestamp) < 2))) ORDER BY"
408
+ " t1.timestamp ASC",
409
+ (session_id, session_id),
410
+ )
411
+ stepless_rows = cursor.fetchall()
412
+ if stepless_rows:
413
+ unassigned_tools = []
414
+ sorted_steps = sorted(steps, key=lambda s: s.get("timestamp") or 0)
415
+
416
+ for tr in stepless_rows:
417
+ trace_dict = self._normalize_display_trace(dict(tr))
418
+
419
+ tr_time = trace_dict.get("timestamp") or 0
420
+ target_step = None
421
+ for s in sorted_steps:
422
+ s_time = s.get("timestamp") or 0
423
+ if s_time <= tr_time:
424
+ target_step = s
425
+ else:
426
+ break
427
+
428
+ if target_step:
429
+ if "generic_tools" not in target_step or not target_step["generic_tools"]:
430
+ target_step["generic_tools"] = []
431
+ target_step["generic_tools"].append(trace_dict)
432
+ target_step["generic_tools"].sort(key=lambda t: t.get("timestamp") or 0)
433
+ else:
434
+ unassigned_tools.append(trace_dict)
435
+
436
+ if unassigned_tools:
437
+ unassigned_tools.sort(key=lambda t: t.get("timestamp") or 0)
438
+ first_timestamp = unassigned_tools[0].get("timestamp") or 0
439
+
440
+ # Calculate token usage ONLY for pre-planning LLM calls (before first step)
441
+ first_recorded_step_time = (
442
+ sorted_steps[0].get("timestamp") if sorted_steps else float("inf")
443
+ )
444
+ cursor.execute(
445
+ "SELECT payload FROM traces WHERE session_id = ? AND (step_id IS NULL OR step_id = '') AND timestamp < ? AND type = 'llm_call'",
446
+ (session_id, first_recorded_step_time),
447
+ )
448
+ plan_llm_rows = cursor.fetchall()
449
+ plan_p, plan_c, plan_t = 0, 0, 0
450
+ for lr in plan_llm_rows:
451
+ p, c, t = self._extract_token_usage_from_trace(lr["payload"])
452
+ plan_p += p
453
+ plan_c += c
454
+ plan_t += t
455
+
456
+ # Retrieve planner thinking / thoughts
457
+ cursor.execute(
458
+ "SELECT type, payload FROM traces WHERE session_id = ? AND (step_id IS NULL OR step_id = '') AND timestamp < ? AND type IN ('raw_thinking', 'thinking') ORDER BY timestamp ASC",
459
+ (session_id, first_recorded_step_time),
460
+ )
461
+ think_rows = cursor.fetchall()
462
+ p_raw_th = []
463
+ p_nat_th = []
464
+ for thr in think_rows:
465
+ if thr["payload"]:
466
+ try:
467
+ th_obj = (
468
+ json.loads(thr["payload"])
469
+ if isinstance(thr["payload"], str)
470
+ else thr["payload"]
471
+ )
472
+ th_text = th_obj.get("thought") or th_obj.get("text") or ""
473
+ if thr["type"] == "raw_thinking" and th_text:
474
+ p_raw_th.append(str(th_text).strip())
475
+ elif thr["type"] == "thinking" and th_text:
476
+ p_nat_th.append(str(th_text).strip())
477
+ except (ValueError, TypeError, AttributeError):
478
+ # Unparseable or non-dict thinking payload: skip it.
479
+ pass
480
+
481
+ virtual_step = {
482
+ "step_id": "pre-planning",
483
+ "step_type": "planning",
484
+ "session_id": session_id,
485
+ "step_number": 0,
486
+ "timestamp": first_timestamp,
487
+ "operator_native_thinking": "\n\n".join(p_nat_th) if p_nat_th else "",
488
+ "operator_raw_thinking": "\n\n".join(p_raw_th) if p_raw_th else "",
489
+ "action_taken": None,
490
+ "last_execution_result": None,
491
+ "extra_metadata": None,
492
+ "generic_tools": unassigned_tools,
493
+ "token_usage": {
494
+ "prompt_tokens": plan_p,
495
+ "completion_tokens": plan_c,
496
+ "total_tokens": plan_t,
497
+ },
498
+ "total_tokens": plan_t,
499
+ }
500
+ steps.append(virtual_step)
501
+
502
+ steps.sort(
503
+ key=lambda x: (
504
+ x.get("timestamp") or 0,
505
+ x.get("step_number") or 0,
506
+ )
507
+ )
508
+
509
+ return steps
510
+
511
+
512
+ step_repo = StepRepository()
@@ -0,0 +1,133 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ from typing import Any
17
+
18
+ try:
19
+ from admin_console.database.connection import db_session
20
+ except ImportError:
21
+ from apps.admin_console.database.connection import db_session
22
+
23
+
24
+ class TraceRepository:
25
+ """Repository handling querying traces and constructing hierarchical trace trees."""
26
+
27
+ def __init__(self, db_path=None):
28
+ self.db_path = db_path
29
+
30
+ def get_trace_by_id(self, trace_id: str, db_path=None) -> dict[str, Any] | None:
31
+ with db_session(db_path or self.db_path) as conn:
32
+ cursor = conn.cursor()
33
+ cursor.execute("SELECT * FROM traces WHERE trace_id = ?", (trace_id,))
34
+ row = cursor.fetchone()
35
+ return dict(row) if row else None
36
+
37
+ def get_trace_tree(self, session_id: str) -> list[dict[str, Any]]:
38
+ with db_session(self.db_path) as conn:
39
+ cursor = conn.cursor()
40
+ cursor.execute(
41
+ "SELECT trace_id, parent_trace_id, type, name, status, timestamp,"
42
+ " duration, CASE WHEN type IN ('raw_thinking', 'thinking') THEN"
43
+ " payload ELSE NULL END AS payload FROM traces WHERE session_id = ?"
44
+ " ORDER BY timestamp ASC",
45
+ (session_id,),
46
+ )
47
+ rows = cursor.fetchall()
48
+
49
+ nodes = {}
50
+ for r in rows:
51
+ row_dict = dict(r)
52
+ if row_dict.get("payload"):
53
+ try:
54
+ row_dict["payload"] = json.loads(row_dict["payload"])
55
+ except (ValueError, TypeError):
56
+ # Non-JSON payload: keep the raw string for display.
57
+ pass
58
+ nodes[row_dict["trace_id"]] = {**row_dict, "children": []}
59
+
60
+ root_nodes = []
61
+ for node_id, node in nodes.items():
62
+ parent_id = node["parent_trace_id"]
63
+ if parent_id and parent_id in nodes:
64
+ nodes[parent_id]["children"].append(node)
65
+ else:
66
+ root_nodes.append(node)
67
+
68
+ def sanitize_tree(nodes_list):
69
+ sanitized = []
70
+ for node in nodes_list:
71
+ node["children"] = sanitize_tree(node["children"])
72
+ if node["name"] == "node":
73
+ sanitized.extend(node["children"])
74
+ else:
75
+ sanitized.append(node)
76
+ return sanitized
77
+
78
+ return sanitize_tree(root_nodes)
79
+
80
+ def get_step_traces_tree(self, session_id: str, step_id: str) -> list[dict[str, Any]]:
81
+ with db_session(self.db_path) as conn:
82
+ cursor = conn.cursor()
83
+ cursor.execute(
84
+ "SELECT trace_id, parent_trace_id, step_id, type, name, status,"
85
+ " timestamp, duration, CASE WHEN type IN ('raw_thinking',"
86
+ " 'thinking') THEN payload ELSE NULL END AS payload FROM traces"
87
+ " WHERE session_id = ? ORDER BY timestamp ASC",
88
+ (session_id,),
89
+ )
90
+ rows = cursor.fetchall()
91
+
92
+ nodes = {}
93
+ for r in rows:
94
+ row_dict = dict(r)
95
+ if row_dict.get("payload"):
96
+ try:
97
+ row_dict["payload"] = json.loads(row_dict["payload"])
98
+ except (ValueError, TypeError):
99
+ # Non-JSON payload: keep the raw string for display.
100
+ pass
101
+ nodes[row_dict["trace_id"]] = {**row_dict, "children": []}
102
+
103
+ step_roots = []
104
+ for n_id, node in nodes.items():
105
+ if node.get("step_id") == step_id:
106
+ step_roots.append(node)
107
+
108
+ for n_id, node in nodes.items():
109
+ parent_id = node["parent_trace_id"]
110
+ if parent_id and parent_id in nodes:
111
+ nodes[parent_id]["children"].append(node)
112
+
113
+ def sanitize_tree(nodes_list, visited=None):
114
+ if visited is None:
115
+ visited = set()
116
+ sanitized = []
117
+ for node in nodes_list:
118
+ node_id = node.get("trace_id")
119
+ if node_id and node_id in visited:
120
+ continue
121
+ if node_id:
122
+ visited.add(node_id)
123
+ node["children"] = sanitize_tree(node["children"], visited.copy())
124
+ if node["name"] == "node":
125
+ sanitized.extend(node["children"])
126
+ else:
127
+ sanitized.append(node)
128
+ return sanitized
129
+
130
+ return sanitize_tree(step_roots)
131
+
132
+
133
+ trace_repo = TraceRepository()