android-midscene-automation 0.2.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +63 -1
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +10 -8
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +13 -1
  507. package/src/App.vue +72 -23
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +369 -186
  525. package/src/types.ts +1 -1
@@ -0,0 +1,1245 @@
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
+ from collections.abc import Iterator
16
+ from contextlib import contextmanager
17
+ import difflib
18
+ import json
19
+ from pathlib import Path
20
+ import shutil
21
+ import sqlite3
22
+ import threading
23
+ from typing import Any
24
+ import uuid
25
+ from uuid import UUID
26
+
27
+ from artemis.data_engine.models import (
28
+ BackgroundTaskRecord,
29
+ FailedOutputRecord,
30
+ HistoryChunkRecord,
31
+ ImageRecord,
32
+ SessionMetadata,
33
+ StepRecord,
34
+ TraceRecord,
35
+ VideoRecordingRecord,
36
+ )
37
+ from third_party.mobile_use.utils.logger import get_logger
38
+
39
+
40
+ def _safe_uuid(val: Any) -> UUID | str:
41
+ if val is None:
42
+ return ""
43
+ if isinstance(val, UUID):
44
+ return val
45
+ try:
46
+ return UUID(str(val))
47
+ except (ValueError, TypeError):
48
+ return str(val)
49
+
50
+
51
+ class SafeJSONEncoder(json.JSONEncoder):
52
+ def default(self, obj):
53
+ if isinstance(obj, bytes):
54
+ return f"<binary data: {len(obj)} bytes>"
55
+ try:
56
+ return super().default(obj)
57
+ except TypeError:
58
+ return str(obj)
59
+
60
+
61
+ logger = get_logger(__name__)
62
+
63
+
64
+ class StorageManager:
65
+ """Manages persistence for Data Engine using SQLite and File System."""
66
+
67
+ def __init__(self, db_path: str | Path, base_trace_dir: str | Path, *, read_only: bool = False):
68
+ self.db_path = Path(db_path)
69
+ self.base_trace_dir = Path(base_trace_dir)
70
+ self._lock = threading.RLock()
71
+ self.read_only = read_only
72
+
73
+ if read_only:
74
+ # Offline readers (MCP trace inspection) never create directories,
75
+ # tables or WAL files: they open the existing database read-only.
76
+ if not self.db_path.exists():
77
+ raise FileNotFoundError(f"Data engine database not found: {self.db_path}")
78
+ return
79
+
80
+ # Ensure directories exist
81
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
82
+ self.base_trace_dir.mkdir(parents=True, exist_ok=True)
83
+
84
+ self._init_db()
85
+
86
+ @contextmanager
87
+ def _get_connection(self) -> Iterator[sqlite3.Connection]:
88
+ with self._lock:
89
+ if self.read_only:
90
+ conn = sqlite3.connect(
91
+ f"{self.db_path.resolve().as_uri()}?mode=ro", uri=True, timeout=30.0
92
+ )
93
+ else:
94
+ conn = sqlite3.connect(self.db_path, timeout=30.0)
95
+ conn.row_factory = sqlite3.Row
96
+ try:
97
+ yield conn
98
+ finally:
99
+ conn.close()
100
+
101
+ def _init_db(self):
102
+ """Initialize database tables."""
103
+ with self._get_connection() as conn:
104
+ conn.execute("PRAGMA journal_mode=WAL")
105
+ conn.execute("""
106
+ CREATE TABLE IF NOT EXISTS sessions (
107
+ session_id TEXT PRIMARY KEY,
108
+ initial_goal TEXT,
109
+ start_time REAL,
110
+ end_time REAL,
111
+ status TEXT,
112
+ device_info TEXT,
113
+ video_filepath TEXT
114
+ )
115
+ """)
116
+
117
+ try:
118
+ conn.execute("ALTER TABLE sessions ADD COLUMN video_filepath TEXT")
119
+ except sqlite3.OperationalError:
120
+ pass
121
+
122
+ try:
123
+ conn.execute("ALTER TABLE sessions ADD COLUMN pid INTEGER")
124
+ except sqlite3.OperationalError:
125
+ pass
126
+
127
+ conn.execute("""
128
+ CREATE TABLE IF NOT EXISTS images (
129
+ image_name TEXT PRIMARY KEY,
130
+ timestamp REAL,
131
+ ocr_result TEXT,
132
+ ui_tree TEXT,
133
+ extra_metadata TEXT
134
+ )
135
+ """)
136
+
137
+ conn.execute("""
138
+ CREATE TABLE IF NOT EXISTS steps (
139
+ step_id TEXT PRIMARY KEY,
140
+ session_id TEXT,
141
+ step_number INTEGER,
142
+ timestamp REAL,
143
+ pre_image_name TEXT,
144
+ post_image_name TEXT,
145
+ summary TEXT,
146
+ action_taken TEXT,
147
+ operator_raw_thinking TEXT,
148
+ operator_native_thinking TEXT,
149
+ last_execution_result TEXT,
150
+ extra_metadata TEXT,
151
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id),
152
+ FOREIGN KEY(pre_image_name) REFERENCES images(image_name),
153
+ FOREIGN KEY(post_image_name) REFERENCES images(image_name)
154
+ )
155
+ """)
156
+ try:
157
+ conn.execute("ALTER TABLE steps ADD COLUMN action_taken TEXT")
158
+ except sqlite3.OperationalError:
159
+ pass
160
+
161
+ try:
162
+ conn.execute("ALTER TABLE steps ADD COLUMN operator_raw_thinking TEXT")
163
+ except sqlite3.OperationalError:
164
+ pass
165
+
166
+ try:
167
+ conn.execute("ALTER TABLE steps ADD COLUMN operator_native_thinking TEXT")
168
+ except sqlite3.OperationalError:
169
+ pass
170
+
171
+ try:
172
+ conn.execute("ALTER TABLE steps ADD COLUMN last_execution_result TEXT")
173
+ except sqlite3.OperationalError:
174
+ pass
175
+
176
+ conn.execute("""
177
+ CREATE TABLE IF NOT EXISTS traces (
178
+ trace_id TEXT PRIMARY KEY,
179
+ session_id TEXT,
180
+ step_id TEXT,
181
+ parent_trace_id TEXT,
182
+ type TEXT,
183
+ name TEXT,
184
+ timestamp REAL,
185
+ duration REAL,
186
+ status TEXT,
187
+ payload TEXT,
188
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id),
189
+ FOREIGN KEY(step_id) REFERENCES steps(step_id),
190
+ FOREIGN KEY(parent_trace_id) REFERENCES traces(trace_id)
191
+ )
192
+ """)
193
+
194
+ conn.execute("""
195
+ CREATE TABLE IF NOT EXISTS failed_outputs (
196
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
197
+ session_id TEXT,
198
+ trace_id TEXT,
199
+ model_name TEXT,
200
+ prompt TEXT,
201
+ raw_output TEXT,
202
+ error_message TEXT,
203
+ timestamp REAL,
204
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id),
205
+ FOREIGN KEY(trace_id) REFERENCES traces(trace_id)
206
+ )
207
+ """)
208
+
209
+ conn.execute("""
210
+ CREATE TABLE IF NOT EXISTS background_tasks (
211
+ task_id TEXT PRIMARY KEY,
212
+ session_id TEXT,
213
+ summary TEXT,
214
+ status TEXT,
215
+ start_time REAL,
216
+ end_time REAL,
217
+ trace_id TEXT,
218
+ logs TEXT,
219
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id)
220
+ )
221
+ """)
222
+ conn.execute("""
223
+ CREATE TABLE IF NOT EXISTS history_chunks (
224
+ chunk_id TEXT PRIMARY KEY,
225
+ session_id TEXT,
226
+ start_step_id TEXT,
227
+ end_step_id TEXT,
228
+ start_step_number INTEGER,
229
+ end_step_number INTEGER,
230
+ source_step_ids TEXT,
231
+ subgoal_hash TEXT,
232
+ version INTEGER,
233
+ status TEXT,
234
+ band1 TEXT,
235
+ band2 TEXT,
236
+ band3 TEXT,
237
+ rendered_text TEXT,
238
+ created_at REAL,
239
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id)
240
+ )
241
+ """)
242
+ conn.execute("""
243
+ CREATE TABLE IF NOT EXISTS video_recordings (
244
+ video_id TEXT PRIMARY KEY,
245
+ session_id TEXT,
246
+ device_id TEXT,
247
+ start_time REAL,
248
+ end_time REAL,
249
+ local_video_path TEXT,
250
+ status TEXT NOT NULL DEFAULT 'recording',
251
+ error TEXT,
252
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id)
253
+ )
254
+ """)
255
+ video_recording_columns = {
256
+ row[1] for row in conn.execute("PRAGMA table_info(video_recordings)").fetchall()
257
+ }
258
+ if "status" not in video_recording_columns:
259
+ conn.execute(
260
+ "ALTER TABLE video_recordings ADD COLUMN status TEXT "
261
+ "NOT NULL DEFAULT 'recording'"
262
+ )
263
+ conn.execute(
264
+ "UPDATE video_recordings SET status = 'ready' WHERE end_time IS NOT NULL"
265
+ )
266
+ if "error" not in video_recording_columns:
267
+ conn.execute("ALTER TABLE video_recordings ADD COLUMN error TEXT")
268
+ try:
269
+ conn.execute("ALTER TABLE background_tasks ADD COLUMN logs TEXT")
270
+ except sqlite3.OperationalError:
271
+ pass
272
+ conn.commit()
273
+ logger.info(f"Database initialized at {self.db_path}")
274
+
275
+ def create_session(self, session: SessionMetadata):
276
+ """Insert a new session record."""
277
+ with self._get_connection() as conn:
278
+ conn.execute(
279
+ """
280
+ INSERT OR REPLACE INTO sessions (session_id, initial_goal, start_time, end_time, status, device_info, pid, video_filepath)
281
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
282
+ """,
283
+ (
284
+ str(session.session_id),
285
+ session.initial_goal,
286
+ session.start_time,
287
+ session.end_time,
288
+ session.status,
289
+ json.dumps(session.device_info),
290
+ session.pid,
291
+ session.video_filepath,
292
+ ),
293
+ )
294
+ conn.commit()
295
+
296
+ # Create session directory
297
+ session_dir = self.base_trace_dir / str(session.session_id)
298
+ session_dir.mkdir(parents=True, exist_ok=True)
299
+ logger.info(f"Session directory created at {session_dir}")
300
+
301
+ def update_session(self, session: SessionMetadata):
302
+ """Update an existing session record."""
303
+ with self._get_connection() as conn:
304
+ conn.execute(
305
+ """
306
+ UPDATE sessions
307
+ SET end_time = ?, status = ?, device_info = ?, video_filepath = ?
308
+ WHERE session_id = ?
309
+ """,
310
+ (
311
+ session.end_time,
312
+ session.status,
313
+ json.dumps(session.device_info),
314
+ session.video_filepath,
315
+ str(session.session_id),
316
+ ),
317
+ )
318
+ conn.commit()
319
+
320
+ def create_video_recording(self, record: VideoRecordingRecord):
321
+ """Insert a new video recording record."""
322
+ with self._get_connection() as conn:
323
+ conn.execute(
324
+ """
325
+ INSERT INTO video_recordings (
326
+ video_id, session_id, device_id, start_time, end_time,
327
+ local_video_path, status, error
328
+ )
329
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
330
+ """,
331
+ (
332
+ str(record.video_id),
333
+ str(record.session_id) if record.session_id else None,
334
+ record.device_id,
335
+ record.start_time,
336
+ record.end_time,
337
+ record.local_video_path,
338
+ record.status,
339
+ record.error,
340
+ ),
341
+ )
342
+ conn.commit()
343
+
344
+ def update_video_recording(self, record: VideoRecordingRecord):
345
+ """Update an existing video recording record and session video path."""
346
+ with self._get_connection() as conn:
347
+ conn.execute(
348
+ """
349
+ UPDATE video_recordings
350
+ SET end_time = ?, local_video_path = ?, status = ?, error = ?
351
+ WHERE video_id = ?
352
+ """,
353
+ (
354
+ record.end_time,
355
+ record.local_video_path,
356
+ record.status,
357
+ record.error,
358
+ str(record.video_id),
359
+ ),
360
+ )
361
+ if record.session_id and record.local_video_path:
362
+ conn.execute(
363
+ """
364
+ UPDATE sessions
365
+ SET video_filepath = ?
366
+ WHERE session_id = ?
367
+ """,
368
+ (
369
+ record.local_video_path,
370
+ str(record.session_id),
371
+ ),
372
+ )
373
+ conn.commit()
374
+
375
+ def update_session_video_path(self, session_id: UUID, video_path: str):
376
+ """Update video filepath across session and video_recordings tables."""
377
+ with self._get_connection() as conn:
378
+ conn.execute(
379
+ "UPDATE sessions SET video_filepath = ? WHERE session_id = ?",
380
+ (str(video_path), str(session_id)),
381
+ )
382
+ conn.execute(
383
+ "UPDATE video_recordings SET local_video_path = ? WHERE session_id = ?",
384
+ (str(video_path), str(session_id)),
385
+ )
386
+ conn.commit()
387
+
388
+ def get_video_recording(self, video_id: UUID) -> VideoRecordingRecord | None:
389
+ """Retrieve a video recording record by ID."""
390
+ with self._get_connection() as conn:
391
+ cursor = conn.execute(
392
+ "SELECT * FROM video_recordings WHERE video_id = ?",
393
+ (str(video_id),),
394
+ )
395
+ row = cursor.fetchone()
396
+ if row:
397
+ return VideoRecordingRecord(
398
+ video_id=_safe_uuid(row["video_id"]),
399
+ session_id=_safe_uuid(row["session_id"]) if row["session_id"] else None,
400
+ device_id=row["device_id"],
401
+ start_time=row["start_time"],
402
+ end_time=row["end_time"],
403
+ local_video_path=row["local_video_path"],
404
+ status=row["status"]
405
+ if "status" in row.keys()
406
+ else ("ready" if row["end_time"] is not None else "recording"),
407
+ error=row["error"] if "error" in row.keys() else None,
408
+ )
409
+ return None
410
+
411
+ def create_image(self, image: ImageRecord):
412
+ """Insert a new image record."""
413
+ with self._get_connection() as conn:
414
+ conn.execute(
415
+ """
416
+ INSERT OR IGNORE INTO images (image_name, timestamp, ocr_result, ui_tree, extra_metadata)
417
+ VALUES (?, ?, ?, ?, ?)
418
+ """,
419
+ (
420
+ image.image_name,
421
+ image.timestamp,
422
+ json.dumps(image.ocr_result) if image.ocr_result else None,
423
+ json.dumps(image.ui_tree) if image.ui_tree else None,
424
+ json.dumps(image.extra_metadata),
425
+ ),
426
+ )
427
+ conn.commit()
428
+
429
+ def get_image(self, image_name: str) -> ImageRecord | None:
430
+ """Retrieve an image by its name."""
431
+ with self._get_connection() as conn:
432
+ cursor = conn.execute("SELECT * FROM images WHERE image_name = ?", (image_name,))
433
+ row = cursor.fetchone()
434
+ if row:
435
+ return ImageRecord(
436
+ image_name=row["image_name"],
437
+ timestamp=row["timestamp"],
438
+ ocr_result=json.loads(row["ocr_result"]) if row["ocr_result"] else None,
439
+ ui_tree=json.loads(row["ui_tree"]) if row["ui_tree"] else None,
440
+ extra_metadata=json.loads(row["extra_metadata"]),
441
+ )
442
+ return None
443
+
444
+ def update_image_data(self, image_name: str, ocr_result: Any | None, ui_tree: Any | None):
445
+ """Update image record with OCR and UI tree if they are not already set."""
446
+ with self._get_connection() as conn:
447
+ ocr_json = json.dumps(ocr_result) if ocr_result is not None else None
448
+ ui_tree_json = json.dumps(ui_tree) if ui_tree is not None else None
449
+
450
+ conn.execute(
451
+ """
452
+ UPDATE images
453
+ SET ocr_result = COALESCE(ocr_result, ?),
454
+ ui_tree = COALESCE(ui_tree, ?)
455
+ WHERE image_name = ?
456
+ """,
457
+ (ocr_json, ui_tree_json, image_name),
458
+ )
459
+ conn.commit()
460
+
461
+ def search_ui_by_hash(self, image_name: str, query: str, threshold: float = 0.6) -> list[dict]:
462
+ """Search for text in UI data (XML and OCR) retrieved by image hash."""
463
+ record = self.get_image(image_name)
464
+ if not record:
465
+ return []
466
+
467
+ matches = []
468
+
469
+ def check_match(text, target_query):
470
+ if not text:
471
+ return 0.0
472
+ return difflib.SequenceMatcher(None, target_query.lower(), text.lower()).ratio()
473
+
474
+ # Search in ui_tree (raw XML elements)
475
+ if record.ui_tree:
476
+ for node in record.ui_tree:
477
+ best_ratio = 0.0
478
+ best_target = ""
479
+
480
+ # Check text
481
+ text = node.get("text")
482
+ if text:
483
+ ratio = check_match(text, query)
484
+ if ratio > best_ratio:
485
+ best_ratio = ratio
486
+ best_target = text
487
+
488
+ # Check content-desc
489
+ desc = node.get("content-desc")
490
+ if desc:
491
+ ratio = check_match(desc, query)
492
+ if ratio > best_ratio:
493
+ best_ratio = ratio
494
+ best_target = desc
495
+
496
+ if best_ratio > threshold:
497
+ matches.append(
498
+ {
499
+ "score": best_ratio,
500
+ "type": "xml",
501
+ "node": node,
502
+ "matched_text": best_target,
503
+ }
504
+ )
505
+
506
+ # Search in ocr_result (raw OCR results)
507
+ if record.ocr_result:
508
+ for ocr in record.ocr_result:
509
+ text = ocr.get("text")
510
+ if text:
511
+ ratio = check_match(text, query)
512
+ if ratio > threshold:
513
+ matches.append(
514
+ {
515
+ "score": ratio,
516
+ "type": "ocr",
517
+ "node": ocr,
518
+ "matched_text": text,
519
+ }
520
+ )
521
+
522
+ # Sort by score descending
523
+ matches.sort(key=lambda x: x["score"], reverse=True)
524
+ return matches
525
+
526
+ def create_step(self, step: StepRecord):
527
+ """Insert a new step record."""
528
+ with self._get_connection() as conn:
529
+ conn.execute(
530
+ """
531
+ INSERT INTO steps (
532
+ step_id, session_id, step_number, timestamp,
533
+ pre_image_name, post_image_name,
534
+ summary, action_taken, operator_raw_thinking, operator_native_thinking, last_execution_result, extra_metadata
535
+ )
536
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
537
+ """,
538
+ (
539
+ str(step.step_id),
540
+ str(step.session_id),
541
+ step.step_number,
542
+ step.timestamp,
543
+ step.pre_image_name,
544
+ step.post_image_name,
545
+ step.summary,
546
+ json.dumps(step.action_taken) if step.action_taken else None,
547
+ step.operator_raw_thinking,
548
+ step.operator_native_thinking,
549
+ json.dumps(step.last_execution_result) if step.last_execution_result else None,
550
+ json.dumps(step.extra_metadata),
551
+ ),
552
+ )
553
+ conn.commit()
554
+
555
+ def update_step_action(self, step_id: UUID, action_taken: dict):
556
+ """Update the action taken for a step."""
557
+ with self._get_connection() as conn:
558
+ conn.execute(
559
+ """
560
+ UPDATE steps
561
+ SET action_taken = ?
562
+ WHERE step_id = ?
563
+ """,
564
+ (
565
+ json.dumps(action_taken),
566
+ str(step_id),
567
+ ),
568
+ )
569
+ conn.commit()
570
+
571
+ def update_step_summary(
572
+ self,
573
+ step_id: UUID,
574
+ summary: str | None,
575
+ *,
576
+ source: str | None = None,
577
+ version: int | None = None,
578
+ model: str | None = None,
579
+ status: str = "ready",
580
+ ) -> bool:
581
+ """Versioned, status-carrying summary write (replaces the blind overwrite).
582
+
583
+ Writes ``summary_status`` / ``summary_source`` / ``summary_version`` /
584
+ ``summary_model`` into the step's ``extra_metadata``. Concurrent writes
585
+ to the same step are ordered by version: an explicit ``version`` lower
586
+ than the stored one is dropped; ``version=None`` auto-increments the
587
+ stored version. Status downgrades are refused without a newer explicit
588
+ version: ``pending`` never overwrites ``ready``/``failed``, and
589
+ ``failed`` never overwrites ``ready``. The ``summary`` column itself is
590
+ only touched by a ``ready`` write with a non-None summary.
591
+
592
+ Returns True when the write was applied, False when it was dropped as
593
+ stale (or the step does not exist).
594
+ """
595
+ with self._get_connection() as conn:
596
+ cursor = conn.execute(
597
+ "SELECT extra_metadata FROM steps WHERE step_id = ?",
598
+ (str(step_id),),
599
+ )
600
+ row = cursor.fetchone()
601
+ if row is None:
602
+ return False
603
+
604
+ try:
605
+ meta = json.loads(row["extra_metadata"]) if row["extra_metadata"] else {}
606
+ if not isinstance(meta, dict):
607
+ meta = {}
608
+ except Exception:
609
+ meta = {}
610
+
611
+ current_version = meta.get("summary_version")
612
+ current_version = current_version if isinstance(current_version, int) else 0
613
+ current_status = meta.get("summary_status")
614
+
615
+ if version is not None:
616
+ if version < current_version:
617
+ return False
618
+ new_version = version
619
+ is_newer = version > current_version
620
+ else:
621
+ new_version = current_version + 1
622
+ is_newer = False
623
+
624
+ # Status downgrades require an explicitly newer version.
625
+ if not is_newer:
626
+ if status == "pending" and current_status in ("ready", "failed"):
627
+ return False
628
+ if status == "failed" and current_status == "ready":
629
+ return False
630
+
631
+ meta["summary_status"] = status
632
+ meta["summary_version"] = new_version
633
+ if source is not None:
634
+ meta["summary_source"] = source
635
+ if model is not None:
636
+ meta["summary_model"] = model
637
+
638
+ if status == "ready" and summary is not None:
639
+ conn.execute(
640
+ "UPDATE steps SET summary = ?, extra_metadata = ? WHERE step_id = ?",
641
+ (summary, json.dumps(meta), str(step_id)),
642
+ )
643
+ else:
644
+ conn.execute(
645
+ "UPDATE steps SET extra_metadata = ? WHERE step_id = ?",
646
+ (json.dumps(meta), str(step_id)),
647
+ )
648
+ conn.commit()
649
+ return True
650
+
651
+ def create_history_chunk(self, chunk: HistoryChunkRecord):
652
+ """Insert one history-chunk version row (append-only; never updates)."""
653
+ with self._get_connection() as conn:
654
+ conn.execute(
655
+ """
656
+ INSERT INTO history_chunks (
657
+ chunk_id, session_id, start_step_id, end_step_id,
658
+ start_step_number, end_step_number, source_step_ids,
659
+ subgoal_hash, version, status, band1, band2, band3,
660
+ rendered_text, created_at
661
+ )
662
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
663
+ """,
664
+ (
665
+ str(chunk.chunk_id),
666
+ str(chunk.session_id),
667
+ chunk.start_step_id,
668
+ chunk.end_step_id,
669
+ chunk.start_step_number,
670
+ chunk.end_step_number,
671
+ json.dumps(chunk.source_step_ids),
672
+ chunk.subgoal_hash,
673
+ chunk.version,
674
+ chunk.status,
675
+ json.dumps(chunk.band1, ensure_ascii=False, cls=SafeJSONEncoder),
676
+ chunk.band2,
677
+ chunk.band3,
678
+ chunk.rendered_text,
679
+ chunk.created_at,
680
+ ),
681
+ )
682
+ conn.commit()
683
+
684
+ def get_history_chunks(
685
+ self, session_id: UUID | str, *, all_versions: bool = False
686
+ ) -> list[HistoryChunkRecord]:
687
+ """History chunks of a session ordered by step range.
688
+
689
+ By default only the newest version per step range is returned; pass
690
+ ``all_versions=True`` for the full append-only trail (audit/tests).
691
+ """
692
+ with self._get_connection() as conn:
693
+ cursor = conn.execute(
694
+ """
695
+ SELECT * FROM history_chunks WHERE session_id = ?
696
+ ORDER BY start_step_number ASC, version ASC, created_at ASC
697
+ """,
698
+ (str(session_id),),
699
+ )
700
+ rows = cursor.fetchall()
701
+
702
+ records: list[HistoryChunkRecord] = []
703
+ for row in rows:
704
+ try:
705
+ source_ids = json.loads(row["source_step_ids"]) if row["source_step_ids"] else []
706
+ except Exception:
707
+ source_ids = []
708
+ try:
709
+ band1 = json.loads(row["band1"]) if row["band1"] else {}
710
+ if not isinstance(band1, dict):
711
+ band1 = {}
712
+ except Exception:
713
+ band1 = {}
714
+ records.append(
715
+ HistoryChunkRecord(
716
+ chunk_id=row["chunk_id"],
717
+ session_id=row["session_id"],
718
+ start_step_id=row["start_step_id"],
719
+ end_step_id=row["end_step_id"],
720
+ start_step_number=row["start_step_number"],
721
+ end_step_number=row["end_step_number"],
722
+ source_step_ids=source_ids,
723
+ subgoal_hash=row["subgoal_hash"],
724
+ version=row["version"],
725
+ status=row["status"],
726
+ band1=band1,
727
+ band2=row["band2"],
728
+ band3=row["band3"],
729
+ rendered_text=row["rendered_text"],
730
+ created_at=row["created_at"],
731
+ )
732
+ )
733
+ if all_versions:
734
+ return records
735
+
736
+ latest: dict[tuple[int, int], HistoryChunkRecord] = {}
737
+ for rec in records:
738
+ key = (rec.start_step_number, rec.end_step_number)
739
+ current = latest.get(key)
740
+ if current is None or rec.version >= current.version:
741
+ latest[key] = rec
742
+ return sorted(latest.values(), key=lambda r: r.start_step_number)
743
+
744
+ def update_step_thinking(self, step_id: UUID, operator_raw_thinking: str):
745
+ """Update the raw thinking process for a step."""
746
+ with self._get_connection() as conn:
747
+ conn.execute(
748
+ """
749
+ UPDATE steps
750
+ SET operator_raw_thinking = ?
751
+ WHERE step_id = ?
752
+ """,
753
+ (
754
+ operator_raw_thinking,
755
+ str(step_id),
756
+ ),
757
+ )
758
+ conn.commit()
759
+
760
+ def update_step_native_thinking(self, step_id: UUID, operator_native_thinking: str):
761
+ """Update the native thinking process for a step."""
762
+ with self._get_connection() as conn:
763
+ conn.execute(
764
+ """
765
+ UPDATE steps
766
+ SET operator_native_thinking = ?
767
+ WHERE step_id = ?
768
+ """,
769
+ (
770
+ operator_native_thinking,
771
+ str(step_id),
772
+ ),
773
+ )
774
+ conn.commit()
775
+
776
+ def update_step_execution_result(
777
+ self,
778
+ step_id: UUID,
779
+ last_execution_result: dict,
780
+ post_image_name: str | None = None,
781
+ ):
782
+ """Update the validator execution result and post_image_name for a step."""
783
+ with self._get_connection() as conn:
784
+ if post_image_name:
785
+ cursor = conn.execute(
786
+ "SELECT pre_image_name FROM steps WHERE step_id = ?",
787
+ (str(step_id),),
788
+ )
789
+ row = cursor.fetchone()
790
+ if row and row["pre_image_name"] == post_image_name:
791
+ post_image_name = None
792
+
793
+ if post_image_name:
794
+ conn.execute(
795
+ """
796
+ UPDATE steps
797
+ SET last_execution_result = ?, post_image_name = ?
798
+ WHERE step_id = ?
799
+ """,
800
+ (
801
+ json.dumps(last_execution_result),
802
+ post_image_name,
803
+ str(step_id),
804
+ ),
805
+ )
806
+ else:
807
+ conn.execute(
808
+ """
809
+ UPDATE steps
810
+ SET last_execution_result = ?
811
+ WHERE step_id = ?
812
+ """,
813
+ (
814
+ json.dumps(last_execution_result),
815
+ str(step_id),
816
+ ),
817
+ )
818
+ conn.commit()
819
+
820
+ def get_session(self, session_id: UUID | str) -> SessionMetadata | None:
821
+ """Retrieve a session by ID."""
822
+ with self._get_connection() as conn:
823
+ cursor = conn.execute(
824
+ "SELECT * FROM sessions WHERE session_id = ?",
825
+ (str(session_id),),
826
+ )
827
+ row = cursor.fetchone()
828
+ if row:
829
+ row_dict = dict(row)
830
+ return SessionMetadata(
831
+ session_id=_safe_uuid(row_dict["session_id"]),
832
+ initial_goal=row_dict["initial_goal"],
833
+ start_time=row_dict["start_time"],
834
+ end_time=row_dict["end_time"],
835
+ status=row_dict["status"],
836
+ device_info=json.loads(row_dict["device_info"]),
837
+ pid=row_dict.get("pid"),
838
+ video_filepath=row_dict.get("video_filepath"),
839
+ )
840
+ return None
841
+
842
+ def get_steps(self, session_id: UUID | str) -> list[StepRecord]:
843
+ """Retrieve all steps for a session."""
844
+ steps = []
845
+ with self._get_connection() as conn:
846
+ cursor = conn.execute(
847
+ "SELECT * FROM steps WHERE session_id = ? ORDER BY step_number ASC",
848
+ (str(session_id),),
849
+ )
850
+ for row in cursor.fetchall():
851
+ action_taken = None
852
+ if "action_taken" in row.keys() and row["action_taken"]:
853
+ try:
854
+ action_taken = json.loads(row["action_taken"])
855
+ except (ValueError, TypeError):
856
+ pass
857
+
858
+ operator_raw_thinking = None
859
+ if "operator_raw_thinking" in row.keys() and row["operator_raw_thinking"]:
860
+ operator_raw_thinking = row["operator_raw_thinking"]
861
+
862
+ operator_native_thinking = None
863
+ if "operator_native_thinking" in row.keys() and row["operator_native_thinking"]:
864
+ operator_native_thinking = row["operator_native_thinking"]
865
+
866
+ last_execution_result = None
867
+ if "last_execution_result" in row.keys() and row["last_execution_result"]:
868
+ try:
869
+ last_execution_result = json.loads(row["last_execution_result"])
870
+ except (ValueError, TypeError):
871
+ pass
872
+
873
+ steps.append(
874
+ StepRecord(
875
+ step_id=_safe_uuid(row["step_id"]),
876
+ session_id=_safe_uuid(row["session_id"]),
877
+ step_number=row["step_number"],
878
+ timestamp=row["timestamp"],
879
+ pre_image_name=row["pre_image_name"],
880
+ post_image_name=row["post_image_name"],
881
+ summary=row["summary"],
882
+ action_taken=action_taken,
883
+ operator_raw_thinking=operator_raw_thinking,
884
+ operator_native_thinking=operator_native_thinking,
885
+ last_execution_result=last_execution_result,
886
+ extra_metadata=json.loads(row["extra_metadata"]),
887
+ )
888
+ )
889
+ return steps
890
+
891
+ get_session_steps = get_steps
892
+
893
+ def get_traces_for_step(self, step_id: UUID | str) -> list[TraceRecord]:
894
+ """Retrieve all traces for a step."""
895
+ traces = []
896
+ with self._get_connection() as conn:
897
+ cursor = conn.execute(
898
+ "SELECT * FROM traces WHERE step_id = ? ORDER BY timestamp ASC",
899
+ (str(step_id),),
900
+ )
901
+ for row in cursor.fetchall():
902
+ traces.append(
903
+ TraceRecord(
904
+ trace_id=_safe_uuid(row["trace_id"]),
905
+ session_id=_safe_uuid(row["session_id"]),
906
+ step_id=_safe_uuid(row["step_id"]) if row["step_id"] else None,
907
+ parent_trace_id=_safe_uuid(row["parent_trace_id"])
908
+ if row["parent_trace_id"]
909
+ else None,
910
+ type=row["type"],
911
+ name=row["name"],
912
+ timestamp=row["timestamp"],
913
+ duration=row["duration"],
914
+ status=row["status"],
915
+ payload=json.loads(row["payload"]) if row["payload"] else {},
916
+ )
917
+ )
918
+ return traces
919
+
920
+ def get_steps_with_traces(self, session_id: UUID) -> list[tuple[StepRecord, list[TraceRecord]]]:
921
+ """Retrieve all steps and their associated traces for a session."""
922
+ steps = self.get_steps(session_id)
923
+ result = []
924
+ for step in steps:
925
+ traces = self.get_traces_for_step(step.step_id)
926
+ result.append((step, traces))
927
+ return result
928
+
929
+ def create_trace(self, trace: TraceRecord):
930
+ """Insert or update a trace record (handles out-of-order writes and preserves start timestamps)."""
931
+ with self._get_connection() as conn:
932
+ conn.execute(
933
+ """
934
+ INSERT INTO traces (
935
+ trace_id, session_id, step_id, parent_trace_id, type, name,
936
+ timestamp, duration, status, payload
937
+ )
938
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
939
+ ON CONFLICT(trace_id) DO UPDATE SET
940
+ status = CASE
941
+ WHEN excluded.status = 'running' AND traces.status IN ('success', 'failed') THEN traces.status
942
+ ELSE excluded.status
943
+ END,
944
+ duration = COALESCE(excluded.duration, traces.duration),
945
+ timestamp = MIN(traces.timestamp, excluded.timestamp),
946
+ payload = CASE
947
+ WHEN excluded.status = 'running' AND traces.status IN ('success', 'failed') THEN traces.payload
948
+ ELSE excluded.payload
949
+ END
950
+ """,
951
+ (
952
+ str(trace.trace_id),
953
+ str(trace.session_id),
954
+ str(trace.step_id) if trace.step_id else None,
955
+ str(trace.parent_trace_id) if trace.parent_trace_id else None,
956
+ trace.type,
957
+ trace.name,
958
+ trace.timestamp,
959
+ trace.duration,
960
+ trace.status,
961
+ json.dumps(trace.payload, cls=SafeJSONEncoder),
962
+ ),
963
+ )
964
+ conn.commit()
965
+
966
+ def create_failed_output(self, record: FailedOutputRecord):
967
+ """Insert a new failed output record (crime scene)."""
968
+ with self._get_connection() as conn:
969
+ conn.execute(
970
+ """
971
+ INSERT INTO failed_outputs (
972
+ session_id, trace_id, model_name, prompt, raw_output, error_message, timestamp
973
+ )
974
+ VALUES (?, ?, ?, ?, ?, ?, ?)
975
+ """,
976
+ (
977
+ str(record.session_id),
978
+ str(record.trace_id),
979
+ record.model_name,
980
+ record.prompt,
981
+ record.raw_output,
982
+ record.error_message,
983
+ record.timestamp,
984
+ ),
985
+ )
986
+ conn.commit()
987
+
988
+ def create_background_task(self, record: BackgroundTaskRecord):
989
+ with self._get_connection() as conn:
990
+ conn.execute(
991
+ """
992
+ INSERT OR REPLACE INTO background_tasks (
993
+ task_id, session_id, summary, status, start_time, end_time, trace_id, logs
994
+ )
995
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
996
+ """,
997
+ (
998
+ record.task_id,
999
+ str(record.session_id),
1000
+ record.summary,
1001
+ record.status,
1002
+ record.start_time,
1003
+ record.end_time,
1004
+ record.trace_id,
1005
+ record.logs,
1006
+ ),
1007
+ )
1008
+ conn.commit()
1009
+
1010
+ def update_background_task_status(self, task_id: str, status: str, end_time: float):
1011
+ with self._get_connection() as conn:
1012
+ conn.execute(
1013
+ """
1014
+ UPDATE background_tasks
1015
+ SET status = ?, end_time = ?
1016
+ WHERE task_id = ?
1017
+ """,
1018
+ (status, end_time, task_id),
1019
+ )
1020
+ conn.commit()
1021
+
1022
+ def update_background_task_status_and_logs(
1023
+ self, task_id: str, status: str, end_time: float, logs: str
1024
+ ):
1025
+ with self._get_connection() as conn:
1026
+ conn.execute(
1027
+ """
1028
+ UPDATE background_tasks
1029
+ SET status = ?, end_time = ?, logs = ?
1030
+ WHERE task_id = ?
1031
+ """,
1032
+ (status, end_time, logs, task_id),
1033
+ )
1034
+ conn.commit()
1035
+
1036
+ def get_background_tasks(self, session_id: UUID) -> list[BackgroundTaskRecord]:
1037
+ tasks = []
1038
+ with self._get_connection() as conn:
1039
+ cursor = conn.execute(
1040
+ "SELECT * FROM background_tasks WHERE session_id = ? ORDER BY start_time ASC",
1041
+ (str(session_id),),
1042
+ )
1043
+ for row in cursor.fetchall():
1044
+ logs = row["logs"] if "logs" in row.keys() else None
1045
+ tasks.append(
1046
+ BackgroundTaskRecord(
1047
+ task_id=row["task_id"],
1048
+ session_id=_safe_uuid(row["session_id"]),
1049
+ summary=row["summary"],
1050
+ status=row["status"],
1051
+ start_time=row["start_time"],
1052
+ end_time=row["end_time"],
1053
+ trace_id=row["trace_id"],
1054
+ logs=logs,
1055
+ )
1056
+ )
1057
+ return tasks
1058
+
1059
+ def get_step_traces(self, step_id: UUID | str) -> list[TraceRecord]:
1060
+ """Retrieve all traces for a specific step."""
1061
+ traces = []
1062
+ with self._get_connection() as conn:
1063
+ cursor = conn.execute(
1064
+ "SELECT * FROM traces WHERE step_id = ? ORDER BY timestamp ASC",
1065
+ (str(step_id),),
1066
+ )
1067
+ for row in cursor.fetchall():
1068
+ traces.append(
1069
+ TraceRecord(
1070
+ trace_id=_safe_uuid(row["trace_id"]),
1071
+ session_id=_safe_uuid(row["session_id"]),
1072
+ step_id=_safe_uuid(row["step_id"]) if row["step_id"] else None,
1073
+ parent_trace_id=_safe_uuid(row["parent_trace_id"])
1074
+ if row["parent_trace_id"]
1075
+ else None,
1076
+ type=row["type"],
1077
+ name=row["name"],
1078
+ timestamp=row["timestamp"],
1079
+ duration=row["duration"],
1080
+ status=row["status"],
1081
+ payload=json.loads(row["payload"]) if row["payload"] else {},
1082
+ )
1083
+ )
1084
+ return traces
1085
+
1086
+ def clear_all_data(self):
1087
+ """Clear all data from SQLite tables and delete session directories."""
1088
+ with self._get_connection() as conn:
1089
+ conn.execute("PRAGMA foreign_keys = OFF")
1090
+ tables = [
1091
+ "video_analysis_observations",
1092
+ "video_analysis_segments",
1093
+ "failed_outputs",
1094
+ "traces",
1095
+ "background_tasks",
1096
+ "steps",
1097
+ "images",
1098
+ "sessions",
1099
+ ]
1100
+ for table in tables:
1101
+ try:
1102
+ conn.execute(f"DELETE FROM {table}")
1103
+ except sqlite3.OperationalError as e:
1104
+ logger.warning(f"Failed to clear table {table}: {e}")
1105
+ conn.commit()
1106
+ conn.execute("VACUUM")
1107
+ conn.commit()
1108
+ logger.info("Database tables cleared and vacuumed.")
1109
+
1110
+ # Delete session directories
1111
+ if self.base_trace_dir.exists():
1112
+ for path in self.base_trace_dir.iterdir():
1113
+ if path.is_dir():
1114
+ if path.name == "images":
1115
+ # Clear images
1116
+ for img_path in path.iterdir():
1117
+ if img_path.is_file():
1118
+ try:
1119
+ img_path.unlink()
1120
+ except Exception as e:
1121
+ logger.error(f"Failed to delete image {img_path}: {e}")
1122
+ elif path.name.startswith("web_") or self._is_uuid(path.name):
1123
+ try:
1124
+ shutil.rmtree(path)
1125
+ except Exception as e:
1126
+ logger.error(f"Failed to delete directory {path}: {e}")
1127
+ elif path.is_file() and path.name == "mcp_server.log":
1128
+ try:
1129
+ path.unlink()
1130
+ except Exception as e:
1131
+ logger.error(f"Failed to delete log file {path}: {e}")
1132
+
1133
+ def _is_uuid(self, val: str) -> bool:
1134
+ # Support both pure UUID and UUID with suffix (e.g., UUID_PASS_...)
1135
+ parts = val.split("_")
1136
+ target = parts[0] if parts else val
1137
+ try:
1138
+ uuid.UUID(target)
1139
+ return True
1140
+ except ValueError:
1141
+ return False
1142
+
1143
+ def delete_session(self, session_id: UUID):
1144
+ """Delete all data associated with a session, including files on disk."""
1145
+ session_id_str = str(session_id)
1146
+
1147
+ # 1. Get video paths before deleting from DB
1148
+ video_paths = []
1149
+ video_ids: list[str] = []
1150
+ try:
1151
+ with self._get_connection() as conn:
1152
+ cursor = conn.execute(
1153
+ "SELECT video_id, local_video_path FROM video_recordings WHERE session_id = ?",
1154
+ (session_id_str,),
1155
+ )
1156
+ for row in cursor.fetchall():
1157
+ if row["video_id"]:
1158
+ video_ids.append(str(row["video_id"]))
1159
+ if row["local_video_path"]:
1160
+ video_paths.append(Path(row["local_video_path"]))
1161
+ except sqlite3.OperationalError as e:
1162
+ logger.warning(f"Could not query video_recordings: {e}")
1163
+
1164
+ # 2. Delete from DB tables
1165
+ with self._get_connection() as conn:
1166
+ conn.execute("PRAGMA foreign_keys = OFF")
1167
+ board_keys = [f"session:{session_id_str}"] + [
1168
+ f"video:{video_id}" for video_id in video_ids
1169
+ ]
1170
+ for table in (
1171
+ "video_analysis_observations",
1172
+ "video_analysis_segments",
1173
+ ):
1174
+ try:
1175
+ conn.executemany(
1176
+ f"DELETE FROM {table} WHERE board_key = ?",
1177
+ [(key,) for key in board_keys],
1178
+ )
1179
+ except sqlite3.OperationalError as e:
1180
+ logger.warning(f"Failed to delete video memory from {table}: {e}")
1181
+ tables = [
1182
+ "failed_outputs",
1183
+ "traces",
1184
+ "background_tasks",
1185
+ "video_recordings",
1186
+ "steps",
1187
+ "sessions",
1188
+ ]
1189
+ for table in tables:
1190
+ try:
1191
+ conn.execute(
1192
+ f"DELETE FROM {table} WHERE session_id = ?",
1193
+ (session_id_str,),
1194
+ )
1195
+ except sqlite3.OperationalError as e:
1196
+ logger.warning(f"Failed to delete from table {table}: {e}")
1197
+ conn.commit()
1198
+ conn.execute("VACUUM")
1199
+ conn.commit()
1200
+
1201
+ logger.info(f"Database records for session {session_id} cleared.")
1202
+
1203
+ # 3. Delete session directory (notes, etc.)
1204
+ session_dir = self.base_trace_dir / session_id_str
1205
+ if session_dir.exists() and session_dir.is_dir():
1206
+ try:
1207
+ shutil.rmtree(session_dir)
1208
+ logger.info(f"Deleted session directory: {session_dir}")
1209
+ except Exception as e:
1210
+ logger.error(f"Failed to delete session directory {session_dir}: {e}")
1211
+
1212
+ # 4. Delete video files and their containing folders (which might have been renamed)
1213
+ for video_path in video_paths:
1214
+ # video_path is likely: /path/to/traces/task_name/recording.mp4
1215
+ # If it was renamed, it might be: /path/to/traces/task_name_PASS_timestamp/recording.mp4
1216
+
1217
+ task_name = video_path.parent.name
1218
+
1219
+ if task_name and task_name != "traces" and task_name != "..":
1220
+ # Search for directories starting with task_name in base_trace_dir
1221
+ try:
1222
+ for path in self.base_trace_dir.iterdir():
1223
+ if path.is_dir() and path.name.startswith(task_name):
1224
+ try:
1225
+ shutil.rmtree(path)
1226
+ logger.info(f"Deleted trace/video directory: {path}")
1227
+ except Exception as e:
1228
+ logger.error(f"Failed to delete directory {path}: {e}")
1229
+ elif path.is_file() and path.name.startswith(task_name):
1230
+ try:
1231
+ path.unlink()
1232
+ logger.info(f"Deleted trace/video file: {path}")
1233
+ except Exception as e:
1234
+ logger.error(f"Failed to delete file {path}: {e}")
1235
+ except Exception as e:
1236
+ logger.error(f"Error iterating base_trace_dir for cleanup: {e}")
1237
+
1238
+ if video_path.exists():
1239
+ try:
1240
+ video_path.unlink()
1241
+ logger.info(f"Deleted video file directly: {video_path}")
1242
+ if video_path.parent.exists() and not any(video_path.parent.iterdir()):
1243
+ video_path.parent.rmdir()
1244
+ except Exception as e:
1245
+ logger.error(f"Failed to delete video file directly {video_path}: {e}")