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,761 @@
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
+ """System Readiness & Diagnostics Router for Artemis Admin Console."""
16
+
17
+ import ipaddress
18
+ import os
19
+ import secrets
20
+ from urllib.parse import urlsplit
21
+
22
+ from fastapi import APIRouter, HTTPException, Request
23
+ from pydantic import BaseModel, Field
24
+
25
+ from artemis.core.diagnostics import readiness_engine
26
+ from artemis.core.diagnostics.adb_server_connection import (
27
+ InvalidAdbServerEndpoint,
28
+ adb_server_connection,
29
+ )
30
+ from artemis.core.diagnostics.schema import SystemReadinessReport
31
+
32
+ router = APIRouter(prefix="/api/system", tags=["system"])
33
+
34
+
35
+ def _require_local_admin_request(request: Request) -> None:
36
+ """Keep endpoint probing and mutation on the local administration boundary."""
37
+ client_host = request.client.host if request.client else None
38
+ allow_remote = os.getenv("ARTEMIS_ALLOW_REMOTE_ADB_CONFIGURATION", "").lower() in {
39
+ "1",
40
+ "true",
41
+ "yes",
42
+ }
43
+ if client_host and not allow_remote:
44
+ try:
45
+ is_loopback = ipaddress.ip_address(client_host).is_loopback
46
+ except ValueError:
47
+ is_loopback = client_host.lower() == "localhost"
48
+ if not is_loopback:
49
+ raise HTTPException(
50
+ status_code=403,
51
+ detail=(
52
+ "ADB server settings are local-only. Set "
53
+ "ARTEMIS_ALLOW_REMOTE_ADB_CONFIGURATION=true to manage them from another "
54
+ "computer."
55
+ ),
56
+ )
57
+
58
+ origin = request.headers.get("origin")
59
+ host = request.headers.get("host")
60
+ if not origin or not host:
61
+ return
62
+ origin_host = urlsplit(origin).netloc.lower()
63
+ if origin_host != host.lower():
64
+ raise HTTPException(
65
+ status_code=403,
66
+ detail="ADB server settings can only be changed from the Artemis console.",
67
+ )
68
+
69
+
70
+ def _require_loopback_request(request: Request, detail: str) -> None:
71
+ """Reject requests whose TCP peer is not the local machine."""
72
+ client_host = request.client.host if request.client else None
73
+ try:
74
+ is_loopback = bool(client_host and ipaddress.ip_address(client_host).is_loopback)
75
+ except ValueError:
76
+ is_loopback = bool(client_host and client_host.lower() == "localhost")
77
+ if not is_loopback:
78
+ raise HTTPException(status_code=403, detail=detail)
79
+
80
+
81
+ def _require_local_lifecycle_request(request: Request) -> None:
82
+ """Authorize a process-lifecycle request from the local CLI only."""
83
+ _require_loopback_request(request, "Server lifecycle controls are local-only.")
84
+
85
+ expected = getattr(request.app.state, "lifecycle_token", None)
86
+ supplied = request.headers.get("x-artemis-lifecycle-token")
87
+ if not (
88
+ isinstance(expected, str)
89
+ and isinstance(supplied, str)
90
+ and secrets.compare_digest(expected, supplied)
91
+ ):
92
+ raise HTTPException(status_code=403, detail="Invalid server lifecycle token.")
93
+
94
+
95
+ class SelectDeviceRequest(BaseModel):
96
+ """Payload to select an active target Android device."""
97
+
98
+ serial: str = Field(description="Serial number or identifier of the Android device to select")
99
+
100
+
101
+ @router.get("/readiness", response_model=SystemReadinessReport)
102
+ async def get_system_readiness(force: bool = False) -> SystemReadinessReport:
103
+ """Execute all diagnostic probes and return a comprehensive system readiness report."""
104
+ return await readiness_engine.run_all(force_refresh=force)
105
+
106
+
107
+ @router.post("/devices/select")
108
+ async def select_active_device(request: SelectDeviceRequest):
109
+ """Select the active Android device or emulator for subsequent automated tasks."""
110
+ serial = request.serial.strip()
111
+ if not serial:
112
+ raise HTTPException(status_code=400, detail="Device serial cannot be empty.")
113
+
114
+ readiness_engine.set_probe_target_serial(serial)
115
+ # Return updated readiness
116
+ report = await readiness_engine.run_all(force_refresh=True)
117
+ return {
118
+ "status": "success",
119
+ "selected_serial": serial,
120
+ "report": report,
121
+ }
122
+
123
+
124
+ @router.post("/adb/restart")
125
+ async def restart_adb_server():
126
+ """Restart local ADB server and return an updated readiness check."""
127
+ restart_result = await readiness_engine.restart_adb_server()
128
+ readiness_engine.invalidate_cache()
129
+ updated_report = await readiness_engine.run_all(force_refresh=True)
130
+ return {
131
+ "restart_result": restart_result,
132
+ "report": updated_report,
133
+ }
134
+
135
+
136
+ @router.post("/adb/heal-keys")
137
+ async def heal_adb_keys():
138
+ """Auto-heal corrupted ADB authentication RSA keys and return updated readiness."""
139
+ heal_result = await readiness_engine.heal_adb_keys()
140
+ updated_report = await readiness_engine.run_all()
141
+ return {
142
+ "heal_result": heal_result,
143
+ "report": updated_report,
144
+ }
145
+
146
+
147
+ class ConnectAdbRequest(BaseModel):
148
+ """Payload to connect to an Android device over Wi-Fi."""
149
+
150
+ host: str = Field(description="IP address of Android device")
151
+ port: int = Field(default=5555, description="Port number")
152
+
153
+
154
+ @router.post("/adb/connect")
155
+ async def connect_wireless_adb(request: ConnectAdbRequest):
156
+ """Connect to a device over Wi-Fi and return updated readiness."""
157
+ connect_result = await readiness_engine.connect_wireless_adb(request.host, request.port)
158
+ readiness_engine.invalidate_cache()
159
+ updated_report = await readiness_engine.run_all(force_refresh=True)
160
+ return {
161
+ "connect_result": connect_result,
162
+ "report": updated_report,
163
+ }
164
+
165
+
166
+ class ConnectAdbServerRequest(BaseModel):
167
+ """Payload to select an ADB server endpoint accessible from this computer."""
168
+
169
+ host: str = Field(description="Host name or IP address of the ADB server")
170
+ port: int = Field(default=5037, ge=1, le=65535, description="ADB server port")
171
+ persist: bool = Field(default=True, description="Persist the endpoint for future launches")
172
+
173
+
174
+ @router.get("/adb/server")
175
+ async def get_adb_server_status():
176
+ """Return the process-wide ADB server endpoint currently used by Artemis."""
177
+ return adb_server_connection.status()
178
+
179
+
180
+ @router.post("/adb/server/connect")
181
+ async def connect_adb_server(payload: ConnectAdbServerRequest, request: Request):
182
+ """Validate and activate an ADB server endpoint."""
183
+ _require_local_admin_request(request)
184
+ try:
185
+ connection_result = await adb_server_connection.connect(
186
+ payload.host,
187
+ payload.port,
188
+ persist=payload.persist,
189
+ )
190
+ except InvalidAdbServerEndpoint as exc:
191
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
192
+
193
+ response: dict[str, object] = {"connection_result": connection_result}
194
+ if connection_result["success"]:
195
+ readiness_engine.set_probe_target_serial(None)
196
+ readiness_engine.invalidate_cache()
197
+ response["report"] = await readiness_engine.run_all(force_refresh=True)
198
+ return response
199
+
200
+
201
+ @router.post("/adb/server/probe")
202
+ async def probe_adb_server(payload: ConnectAdbServerRequest, request: Request):
203
+ """Test an ADB server endpoint without changing the active endpoint."""
204
+ _require_local_admin_request(request)
205
+ try:
206
+ connection_result = await adb_server_connection.probe(payload.host, payload.port)
207
+ except InvalidAdbServerEndpoint as exc:
208
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
209
+ return {"connection_result": connection_result}
210
+
211
+
212
+ @router.post("/adb/server/local")
213
+ async def use_local_adb_server(request: Request, persist: bool = True):
214
+ """Restore the standard local ADB server without touching a remote daemon."""
215
+ _require_local_admin_request(request)
216
+ connection_result = await adb_server_connection.use_local_server(persist=persist)
217
+ readiness_engine.set_probe_target_serial(None)
218
+ readiness_engine.invalidate_cache()
219
+ updated_report = await readiness_engine.run_all(force_refresh=True)
220
+ return {
221
+ "connection_result": connection_result,
222
+ "report": updated_report,
223
+ }
224
+
225
+
226
+ class LaunchEmulatorRequest(BaseModel):
227
+ """Payload to launch a local Android Virtual Device (AVD)."""
228
+
229
+ avd_name: str = Field(
230
+ description="Name of the installed AVD emulator to launch (e.g. Android_2)"
231
+ )
232
+
233
+
234
+ @router.post("/emulator/launch")
235
+ async def launch_emulator(request: LaunchEmulatorRequest):
236
+ """Launch an Android emulator in the background and return initiation status."""
237
+ avd_name = request.avd_name.strip()
238
+ if not avd_name:
239
+ raise HTTPException(status_code=400, detail="AVD name cannot be empty.")
240
+
241
+ launch_res = await readiness_engine.launch_emulator(avd_name)
242
+ return launch_res
243
+
244
+
245
+ @router.get("/emulator/status")
246
+ async def get_emulator_status():
247
+ """Query real-time progress and logs of background emulator launch."""
248
+ return readiness_engine.get_emulator_status()
249
+
250
+
251
+ @router.post("/emulator/stop")
252
+ async def stop_emulator():
253
+ """Stop active emulator process."""
254
+ return await readiness_engine.stop_emulator()
255
+
256
+
257
+ @router.post("/emulator/dismiss")
258
+ async def dismiss_emulator():
259
+ """Dismiss emulator launch tracking state."""
260
+ return readiness_engine.dismiss_emulator()
261
+
262
+
263
+ class UpdateCredentialsRequest(BaseModel):
264
+ """Payload to update and configure LLM or Vision OCR API credentials."""
265
+
266
+ provider: str = Field(
267
+ default="google",
268
+ description="Provider identifier (e.g. google, gemini, openai, anthropic, openrouter, ocr)",
269
+ )
270
+ api_key: str = Field(description="The secret API key string to configure")
271
+ persist_to_env: bool = Field(
272
+ default=True, description="Whether to persist the key to .env file"
273
+ )
274
+
275
+
276
+ class ValidateCredentialsRequest(BaseModel):
277
+ """Payload to test and verify LLM or Vision OCR API credentials without saving."""
278
+
279
+ provider: str = Field(
280
+ default="google",
281
+ description="Provider identifier (e.g. google, gemini, openai, anthropic, openrouter, ocr)",
282
+ )
283
+ api_key: str = Field(description="The secret API key string to test")
284
+ base_url: str | None = Field(default=None, description="Optional custom base URL")
285
+
286
+
287
+ @router.get("/credentials")
288
+ async def get_credentials():
289
+ """Report which providers have an API key configured.
290
+
291
+ Secret values never leave the process: this endpoint intentionally returns
292
+ presence booleans only. Keys are written via POST /credentials and used
293
+ server-side.
294
+ """
295
+ from artemis.config import settings
296
+
297
+ providers = ("google", "openai", "anthropic", "openrouter", "ocr")
298
+ status = {name: bool(settings.get_api_key(name)) for name in providers}
299
+ status["gemini"] = status["google"]
300
+ return {
301
+ "providers": [
302
+ {"name": name, "configured": configured} for name, configured in status.items()
303
+ ]
304
+ }
305
+
306
+
307
+ @router.post("/credentials/test")
308
+ async def test_credentials(request: ValidateCredentialsRequest):
309
+ """Test and verify whether an API key is valid and usable with the corresponding provider endpoint."""
310
+ from artemis.utils.credentials_validator import validate_api_key
311
+
312
+ provider = request.provider.strip().lower()
313
+ key = request.api_key.strip()
314
+
315
+ if not key:
316
+ raise HTTPException(status_code=400, detail="API key cannot be empty.")
317
+
318
+ is_valid, message = await validate_api_key(
319
+ provider=provider,
320
+ api_key=key,
321
+ base_url=request.base_url,
322
+ )
323
+ if not is_valid:
324
+ raise HTTPException(status_code=400, detail=message)
325
+
326
+ return {
327
+ "valid": True,
328
+ "provider": provider,
329
+ "message": message,
330
+ }
331
+
332
+
333
+ @router.post("/credentials")
334
+ async def update_credentials(request: UpdateCredentialsRequest):
335
+ """Dynamically configure and persist LLM or Vision API key, returning updated readiness report."""
336
+ from artemis.utils.credentials_validator import validate_api_key
337
+
338
+ provider = request.provider.strip().lower()
339
+ key = request.api_key.strip()
340
+
341
+ # If a non-empty key is provided, verify it before saving
342
+ if key:
343
+ is_valid, validation_msg = await validate_api_key(provider=provider, api_key=key)
344
+ if not is_valid:
345
+ raise HTTPException(
346
+ status_code=400,
347
+ detail=f"API key verification failed: {validation_msg}",
348
+ )
349
+
350
+ try:
351
+ from artemis.config import settings
352
+
353
+ settings.set_api_key(provider, key, persist_to_env=request.persist_to_env)
354
+
355
+ # Re-run all diagnostic probes to build updated report
356
+ readiness_engine.invalidate_cache()
357
+ updated_report = await readiness_engine.run_all(force_refresh=True)
358
+ action_desc = (
359
+ "successfully verified, updated, and applied" if key else "successfully cleared"
360
+ )
361
+ return {
362
+ "status": "success",
363
+ "message": f"API key for {provider} {action_desc}.",
364
+ "provider": provider,
365
+ "report": updated_report,
366
+ }
367
+ except HTTPException:
368
+ raise
369
+ except Exception as exc:
370
+ raise HTTPException(status_code=500, detail=f"Failed to update credentials: {exc}")
371
+
372
+
373
+ class SaveModelConfigRequest(BaseModel):
374
+ content: str = Field(min_length=1, max_length=2_000_000)
375
+ original_content: str = Field(max_length=2_000_000)
376
+
377
+
378
+ @router.put("/model-config")
379
+ async def save_model_config(payload: SaveModelConfigRequest, request: Request):
380
+ """Validate and atomically save the active JSONC file without changing credentials."""
381
+ import io
382
+ import json
383
+ from pathlib import Path
384
+ import tempfile
385
+
386
+ from pydantic import ValidationError
387
+ from artemis.config.agent import AgentGlobalConfig
388
+ from artemis.config.llm import LLMConfig, _expand_default_into_nodes
389
+ from artemis.config.paths import get_config_path
390
+ from apps.admin_console.services.model_service import ModelService
391
+ from third_party.mobile_use.utils.file import load_jsonc
392
+
393
+ _require_loopback_request(request, "Model configuration can only be edited locally.")
394
+ _require_local_admin_request(request)
395
+ try:
396
+ data = load_jsonc(io.StringIO(payload.content))
397
+ except (json.JSONDecodeError, ValueError):
398
+ raise HTTPException(status_code=400, detail="Invalid JSONC. Please check the syntax.")
399
+ if not isinstance(data, dict) or not (
400
+ isinstance(data.get("default"), dict)
401
+ or (isinstance(data.get("planner"), dict) and isinstance(data.get("utils"), dict))
402
+ ):
403
+ raise HTTPException(status_code=400, detail="Configuration must contain a default model.")
404
+ try:
405
+ LLMConfig.model_validate(_expand_default_into_nodes(data))
406
+ AgentGlobalConfig.model_validate(data.get("agent", data))
407
+ except (ValidationError, TypeError, AttributeError):
408
+ raise HTTPException(
409
+ status_code=400,
410
+ detail="Invalid model or agent configuration. Please check provider, model and fallback.",
411
+ )
412
+ temporary = None
413
+ try:
414
+ path = get_config_path("artemis.jsonc").resolve()
415
+ if path.read_text(encoding="utf-8") != payload.original_content:
416
+ raise HTTPException(
417
+ status_code=409,
418
+ detail="Configuration changed on disk. Cancel editing and reload before saving.",
419
+ )
420
+ with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, delete=False) as file:
421
+ temporary = Path(file.name)
422
+ os.chmod(temporary, path.stat().st_mode & 0o777)
423
+ file.write(payload.content)
424
+ file.flush()
425
+ os.fsync(file.fileno())
426
+ os.replace(temporary, path)
427
+ except OSError:
428
+ raise HTTPException(status_code=500, detail="Unable to save configuration. Check file permissions.")
429
+ finally:
430
+ if temporary is not None:
431
+ temporary.unlink(missing_ok=True)
432
+ ModelService._llm_info_cache = None
433
+ return await get_model_config_and_env()
434
+
435
+
436
+ @router.post("/model-config/refresh")
437
+ async def refresh_model_configuration(request: Request):
438
+ """Validate disk configuration, then reload only model-related environment settings."""
439
+ import json
440
+ from dotenv import dotenv_values
441
+ from pydantic import ValidationError
442
+ from artemis.config import settings
443
+ from artemis.config.settings import Settings
444
+ from artemis.config.agent import AgentGlobalConfig
445
+ from artemis.config.llm import LLMConfig, _expand_default_into_nodes
446
+ from artemis.config.paths import get_config_path, get_env_file
447
+ from artemis.llm.router import ModelFactory
448
+ from apps.admin_console.services.model_service import ModelService
449
+ from third_party.mobile_use.utils.file import load_jsonc
450
+
451
+ _require_loopback_request(request, "Model configuration can only be refreshed locally.")
452
+ _require_local_admin_request(request)
453
+ try:
454
+ with get_config_path("artemis.jsonc").open(encoding="utf-8") as file:
455
+ data = load_jsonc(file)
456
+ if not isinstance(data, dict) or not (
457
+ isinstance(data.get("default"), dict)
458
+ or (isinstance(data.get("planner"), dict) and isinstance(data.get("utils"), dict))
459
+ ):
460
+ raise ValueError("Missing model configuration")
461
+ LLMConfig.model_validate(_expand_default_into_nodes(data))
462
+ AgentGlobalConfig.model_validate(data.get("agent", data))
463
+ except json.JSONDecodeError:
464
+ raise HTTPException(status_code=400, detail="Invalid JSONC. Please check the syntax.")
465
+ except (ValidationError, TypeError, ValueError):
466
+ raise HTTPException(status_code=400, detail="Invalid model or agent configuration.")
467
+ except OSError:
468
+ raise HTTPException(status_code=400, detail="Failed to read configuration files.")
469
+
470
+ env_path = get_env_file()
471
+ try:
472
+ with env_path.open(encoding="utf-8") as file:
473
+ env_values = dotenv_values(stream=file)
474
+ except OSError:
475
+ raise HTTPException(status_code=400, detail="Failed to read configuration files.")
476
+ names = (
477
+ "OPENAI_API_KEY", "OPENAI_BASE_URL", "GOOGLE_API_KEY", "GEMINI_API_KEY",
478
+ "GCP_API_KEY", "ANTHROPIC_API_KEY", "OPEN_ROUTER_API_KEY", "XAI_API_KEY",
479
+ "OCR_API_KEY", "VISION_API_KEY", "API_KEY",
480
+ )
481
+ values = {name: (env_values.get(name) or "").strip() or None for name in names}
482
+ base_url = values["OPENAI_BASE_URL"]
483
+ if base_url:
484
+ try:
485
+ parsed = urlsplit(base_url)
486
+ valid_url = parsed.scheme in {"http", "https"} and bool(parsed.hostname)
487
+ except ValueError:
488
+ valid_url = False
489
+ if not valid_url:
490
+ raise HTTPException(status_code=400, detail="Invalid OPENAI_BASE_URL. Use an HTTP or HTTPS URL.")
491
+ # Validate before mutation, and keep the singleton object used by existing imports.
492
+ try:
493
+ updated = Settings(_env_file=None, **{**settings.model_dump(), **values})
494
+ except ValidationError:
495
+ raise HTTPException(status_code=400, detail="Invalid model environment configuration.")
496
+ for name in names:
497
+ setattr(settings, name, getattr(updated, name))
498
+ if values[name] is None:
499
+ os.environ.pop(name, None)
500
+ else:
501
+ os.environ[name] = values[name]
502
+ ModelFactory._cache.clear()
503
+ ModelService._llm_info_cache = None
504
+ readiness_engine.invalidate_cache()
505
+ return await get_model_config_and_env()
506
+
507
+
508
+ @router.get("/model-config-env")
509
+ async def get_model_config_and_env():
510
+ """Retrieve the current active artemis.jsonc configuration and .env status for custom setup."""
511
+ import os
512
+ from artemis.config.paths import get_config_path, get_env_file
513
+ from artemis.config import settings
514
+ from third_party.mobile_use.utils.file import load_jsonc
515
+
516
+ from artemis.config.settings import is_placeholder_key
517
+
518
+ # 1. Config file resolution
519
+ config_path = None
520
+ config_content = ""
521
+ parsed_config = {}
522
+ try:
523
+ config_path_obj = get_config_path("artemis.jsonc")
524
+ config_path = str(config_path_obj)
525
+ config_content = config_path_obj.read_text(encoding="utf-8")
526
+ with open(config_path_obj, encoding="utf-8") as f:
527
+ parsed_config = load_jsonc(f)
528
+ except Exception as e:
529
+ config_content = f"// Error reading config: {e}"
530
+
531
+ # 2. Env file resolution
532
+ env_path = get_env_file()
533
+
534
+ # 3. Relevant Env Variables status
535
+ def get_real_env_value(key_name: str, provider: str) -> str | None:
536
+ k = settings.get_api_key(provider)
537
+ if k and not is_placeholder_key(k):
538
+ return k.get_secret_value()
539
+ val = os.environ.get(key_name)
540
+ if val and val.strip() and not is_placeholder_key(val):
541
+ return val.strip()
542
+ return None
543
+
544
+ def mask_key(k: str | None) -> str | None:
545
+ # Expose only enough to recognize which key is active, never a usable
546
+ # fragment of the secret itself.
547
+ if not k:
548
+ return None
549
+ if len(k) <= 8:
550
+ return "****"
551
+ return f"****{k[-4:]}"
552
+
553
+ gemini_real = get_real_env_value("GEMINI_API_KEY", "google") or get_real_env_value(
554
+ "GOOGLE_API_KEY", "google"
555
+ )
556
+ openai_real = get_real_env_value("OPENAI_API_KEY", "openai")
557
+ anthropic_real = get_real_env_value("ANTHROPIC_API_KEY", "anthropic")
558
+ openrouter_real = get_real_env_value("OPEN_ROUTER_API_KEY", "openrouter")
559
+ xai_real = get_real_env_value("XAI_API_KEY", "xai")
560
+ base_url_val = settings.OPENAI_BASE_URL or os.environ.get("OPENAI_BASE_URL")
561
+ if base_url_val and is_placeholder_key(base_url_val):
562
+ base_url_val = None
563
+ ocr_real = get_real_env_value("OCR_API_KEY", "ocr") or get_real_env_value(
564
+ "VISION_API_KEY", "ocr"
565
+ )
566
+
567
+ env_vars = [
568
+ {
569
+ "name": "GEMINI_API_KEY",
570
+ "provider": "google",
571
+ "is_set": bool(gemini_real),
572
+ "preview": mask_key(gemini_real),
573
+ "description": "Google Gemini multimodal vision API key (free tier available)",
574
+ },
575
+ {
576
+ "name": "OPENAI_API_KEY",
577
+ "provider": "openai",
578
+ "is_set": bool(openai_real),
579
+ "preview": mask_key(openai_real),
580
+ "description": "OpenAI API key (GPT-4o, GPT-4o-mini)",
581
+ },
582
+ {
583
+ "name": "ANTHROPIC_API_KEY",
584
+ "provider": "anthropic",
585
+ "is_set": bool(anthropic_real),
586
+ "preview": mask_key(anthropic_real),
587
+ "description": "Anthropic Claude API key (Claude 3.5 Sonnet, Claude 3.7)",
588
+ },
589
+ {
590
+ "name": "OPEN_ROUTER_API_KEY",
591
+ "provider": "openrouter",
592
+ "is_set": bool(openrouter_real),
593
+ "preview": mask_key(openrouter_real),
594
+ "description": "OpenRouter unified API gateway key",
595
+ },
596
+ {
597
+ "name": "XAI_API_KEY",
598
+ "provider": "xai",
599
+ "is_set": bool(xai_real),
600
+ "preview": mask_key(xai_real),
601
+ "description": "xAI Grok vision API key",
602
+ },
603
+ {
604
+ "name": "OPENAI_BASE_URL",
605
+ "provider": "custom",
606
+ "is_set": bool(base_url_val),
607
+ "preview": base_url_val,
608
+ "description": "Custom API endpoint (for local Ollama, vLLM, DeepSeek, or proxies)",
609
+ },
610
+ {
611
+ "name": "VISION_API_KEY",
612
+ "provider": "ocr",
613
+ "is_set": bool(ocr_real),
614
+ "preview": mask_key(ocr_real),
615
+ "description": "Google Cloud Vision OCR key for screen text detection (optional)",
616
+ },
617
+ ]
618
+
619
+ return {
620
+ "config_path": config_path or "config/artemis.jsonc",
621
+ "config_filename": "artemis.jsonc",
622
+ "config_content": config_content,
623
+ "default_model": parsed_config.get("default", {}),
624
+ "presets": parsed_config.get("presets", {}),
625
+ "env_path": str(env_path),
626
+ "env_filename": ".env",
627
+ "env_vars": env_vars,
628
+ }
629
+
630
+
631
+ @router.get("/server-status")
632
+ async def get_server_runtime_status():
633
+ """Retrieve runtime status, PID, port, and uptime of the Artemis server.
634
+
635
+ Answers from in-process state. ``server_lifecycle.get_server_status`` is
636
+ not used here: it runs ``lsof``/``fuser``, which can take over a second and
637
+ is too slow for the ``is_artemis_daemon`` probe.
638
+ """
639
+ import os
640
+ import time
641
+
642
+ from artemis.runtime.process_probe import pid_is_alive
643
+ from artemis.runtime.server_lifecycle import read_server_info
644
+
645
+ try:
646
+ from apps.admin_console.core.state import state
647
+ except ImportError:
648
+ from admin_console.core.state import state
649
+
650
+ port = getattr(state, "port", 8000)
651
+ current_pid = os.getpid()
652
+ pids = {current_pid}
653
+ started_at = None
654
+
655
+ info = read_server_info()
656
+ if info and info.get("port") == port:
657
+ saved_pid = info.get("pid")
658
+ if isinstance(saved_pid, int) and saved_pid != current_pid and pid_is_alive(saved_pid):
659
+ pids.add(saved_pid)
660
+ if isinstance(info.get("started_at"), (int, float)):
661
+ started_at = float(info["started_at"])
662
+ if started_at is None:
663
+ try:
664
+ import psutil
665
+
666
+ started_at = psutil.Process(current_pid).create_time()
667
+ except Exception: # pylint: disable=broad-exception-caught
668
+ # psutil is optional; uptime is best-effort.
669
+ started_at = None
670
+
671
+ uptime_seconds = max(0.0, time.time() - started_at) if started_at is not None else None
672
+ # Explicit DTO: the raw metadata file additionally holds the lifecycle
673
+ # token, cmdline, and filesystem paths, none of which belong on the wire.
674
+ return {
675
+ "running": True,
676
+ "port": port,
677
+ "pids": sorted(pids),
678
+ "active_pid": current_pid,
679
+ "uptime_seconds": uptime_seconds,
680
+ "url": f"http://localhost:{port}",
681
+ "admin_url": f"http://localhost:{port}/admin",
682
+ "current_pid": current_pid,
683
+ }
684
+
685
+
686
+ @router.post("/restart")
687
+ async def restart_server_endpoint(request: Request):
688
+ """Request a graceful restart of the Artemis server from thin clients/UI."""
689
+ import asyncio
690
+ import os
691
+ import sys
692
+ import threading
693
+
694
+ _require_loopback_request(request, "Server lifecycle controls are local-only.")
695
+
696
+ try:
697
+ from apps.admin_console.core.state import state
698
+ except ImportError:
699
+ from admin_console.core.state import state
700
+
701
+ port = getattr(state, "port", 8000)
702
+ current_pid = os.getpid()
703
+
704
+ def _restart_worker():
705
+ import time
706
+
707
+ time.sleep(0.6)
708
+ if sys.platform != "win32":
709
+ try:
710
+ os.execv(sys.executable, [sys.executable] + sys.argv)
711
+ except Exception:
712
+ import subprocess
713
+
714
+ subprocess.Popen([sys.executable] + sys.argv)
715
+ os._exit(0)
716
+ else:
717
+ import subprocess
718
+
719
+ subprocess.Popen([sys.executable] + sys.argv)
720
+ os._exit(0)
721
+
722
+ threading.Thread(target=_restart_worker, daemon=True).start()
723
+
724
+ return {
725
+ "status": "restarting",
726
+ "message": "Artemis server is restarting. Client reconnection should occur in 2-3 seconds.",
727
+ "previous_pid": current_pid,
728
+ "port": port,
729
+ }
730
+
731
+
732
+ @router.post("/shutdown", status_code=202)
733
+ async def shutdown_server_endpoint(request: Request):
734
+ """Request a graceful shutdown of the Artemis server."""
735
+ import asyncio
736
+
737
+ try:
738
+ from apps.admin_console.core.state import state
739
+ except ImportError:
740
+ from admin_console.core.state import state
741
+
742
+ _require_local_lifecycle_request(request)
743
+ server = getattr(request.app.state, "uvicorn_server", None)
744
+ if server is None:
745
+ raise HTTPException(status_code=503, detail="Server lifecycle controller is unavailable.")
746
+
747
+ async def _shutdown_after_response() -> None:
748
+ # Let Starlette flush the accepted response before Uvicorn leaves its
749
+ # main loop and invokes the FastAPI shutdown lifecycle.
750
+ await asyncio.sleep(0.05)
751
+ state.is_shutting_down = True
752
+ state.shutdown_event.set()
753
+ server.should_exit = True
754
+
755
+ asyncio.create_task(_shutdown_after_response())
756
+
757
+ return {
758
+ "status": "shutting_down",
759
+ "message": "Artemis server is shutting down.",
760
+ "pid": os.getpid(),
761
+ }