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,267 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Failure classification, retry policies, and circuit breaking for LLM calls.
16
+
17
+ This is the single source of truth for how ARTEMIS reacts to a failed model
18
+ call. Every provider/transport exception is converted into a ``Failure``
19
+ carrying explicit recovery decisions, and every retry loop in the codebase is
20
+ expected to consult ``retry_policy_for`` instead of inventing its own rules.
21
+
22
+ The taxonomy was promoted from ``artemis.agents.video_analyzer.reliability``
23
+ (which now extends it with media-specific categories) so that the same
24
+ classification and circuit-breaker quality applies to every agent call, not
25
+ just the video pipeline.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from dataclasses import dataclass
31
+ from enum import StrEnum
32
+ import random
33
+ import threading
34
+ import time
35
+ from typing import Any
36
+
37
+
38
+ class FailureCategory(StrEnum):
39
+ RATE_LIMIT = "rate_limit"
40
+ PROVIDER_UNAVAILABLE = "provider_unavailable"
41
+ TIMEOUT = "timeout"
42
+ CONNECTION = "connection"
43
+ AUTHENTICATION = "authentication"
44
+ BAD_REQUEST = "bad_request"
45
+ CANCELLED = "cancelled"
46
+ UNKNOWN = "unknown"
47
+
48
+
49
+ #: Categories that count toward circuit-breaker state: transient provider-side
50
+ #: trouble where hammering the endpoint makes things worse.
51
+ TRANSIENT_CATEGORIES: frozenset[str] = frozenset(
52
+ {
53
+ FailureCategory.RATE_LIMIT.value,
54
+ FailureCategory.PROVIDER_UNAVAILABLE.value,
55
+ FailureCategory.TIMEOUT.value,
56
+ FailureCategory.CONNECTION.value,
57
+ }
58
+ )
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class Failure:
63
+ category: FailureCategory
64
+ retryable: bool
65
+ should_fallback: bool
66
+
67
+
68
+ def extract_status_code(error: BaseException) -> int | None:
69
+ """Best-effort extraction of an HTTP-ish status code from an exception."""
70
+ code = getattr(error, "code", None) or getattr(error, "status_code", None)
71
+ try:
72
+ return int(code) if code is not None else None
73
+ except (TypeError, ValueError):
74
+ return None
75
+
76
+
77
+ _RATE_LIMIT_MARKERS = ("rate limit", "quota", "resource_exhausted")
78
+ _UNAVAILABLE_MARKERS = ("unavailable", "overloaded", "high demand", "service unavailable")
79
+ _AUTH_MARKERS = ("unauthorized", "forbidden", "api key", "credential")
80
+ # "deadline exceeded"/"deadline_exceeded" cover google-genai SDK timeouts that
81
+ # surface without an HTTP status code attached.
82
+ _TIMEOUT_MARKERS = ("timed out", "timeout", "deadline exceeded", "deadline_exceeded")
83
+
84
+
85
+ def classify_failure(error: BaseException) -> Failure:
86
+ """Convert a provider/transport exception into explicit recovery decisions."""
87
+ if isinstance(error, KeyboardInterrupt) or type(error).__name__ == "CancelledError":
88
+ return Failure(FailureCategory.CANCELLED, False, False)
89
+
90
+ code = extract_status_code(error)
91
+ message = str(error).lower()
92
+
93
+ if code == 429 or any(marker in message for marker in _RATE_LIMIT_MARKERS):
94
+ return Failure(FailureCategory.RATE_LIMIT, True, True)
95
+ if code in {500, 502, 503, 504} or any(marker in message for marker in _UNAVAILABLE_MARKERS):
96
+ return Failure(FailureCategory.PROVIDER_UNAVAILABLE, True, True)
97
+ if isinstance(error, TimeoutError) or any(marker in message for marker in _TIMEOUT_MARKERS):
98
+ return Failure(FailureCategory.TIMEOUT, True, True)
99
+ if isinstance(error, (ConnectionError, OSError)):
100
+ return Failure(FailureCategory.CONNECTION, True, True)
101
+ if code in {401, 403} or any(marker in message for marker in _AUTH_MARKERS):
102
+ # Not retryable against the same endpoint, but a fallback endpoint may
103
+ # use a different (working) credential.
104
+ return Failure(FailureCategory.AUTHENTICATION, False, True)
105
+ if code in {400, 404, 413, 415, 422}:
106
+ # Programmer/request errors: retrying is useless and falling back only
107
+ # hides the bug inside a different model's output.
108
+ return Failure(FailureCategory.BAD_REQUEST, False, False)
109
+ return Failure(FailureCategory.UNKNOWN, True, True)
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Typed terminal errors
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ class LLMCallError(Exception):
118
+ """Base class for terminal LLM call failures raised by the gateway."""
119
+
120
+ def __init__(self, message: str, *, failure: Failure, cause: BaseException | None = None):
121
+ super().__init__(message)
122
+ self.failure = failure
123
+ if cause is not None:
124
+ self.__cause__ = cause
125
+
126
+
127
+ class LLMPermanentError(LLMCallError):
128
+ """A non-retryable failure (auth, bad request). Retrying cannot help."""
129
+
130
+
131
+ class LLMExhaustedError(LLMCallError):
132
+ """Retryable failures exhausted every attempt and the pause deadline."""
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Retry policies
137
+ # ---------------------------------------------------------------------------
138
+
139
+
140
+ @dataclass(frozen=True)
141
+ class RetryPolicy:
142
+ """How many attempts a failure category deserves and how to space them."""
143
+
144
+ max_attempts: int
145
+ base_delay: float
146
+ max_delay: float
147
+
148
+ def delay_for(self, attempt: int) -> float:
149
+ """Backoff delay before retry number ``attempt`` (1-based)."""
150
+ delay = min(self.max_delay, self.base_delay * (2 ** max(0, attempt - 1)))
151
+ return delay + random.uniform(0.1, 0.5)
152
+
153
+
154
+ _DEFAULT_RETRY_POLICIES: dict[FailureCategory, RetryPolicy] = {
155
+ FailureCategory.RATE_LIMIT: RetryPolicy(max_attempts=4, base_delay=10.0, max_delay=60.0),
156
+ FailureCategory.PROVIDER_UNAVAILABLE: RetryPolicy(
157
+ max_attempts=4, base_delay=5.0, max_delay=30.0
158
+ ),
159
+ FailureCategory.TIMEOUT: RetryPolicy(max_attempts=3, base_delay=2.0, max_delay=15.0),
160
+ FailureCategory.CONNECTION: RetryPolicy(max_attempts=3, base_delay=2.0, max_delay=15.0),
161
+ FailureCategory.UNKNOWN: RetryPolicy(max_attempts=2, base_delay=1.0, max_delay=5.0),
162
+ # Non-retryable categories get a single attempt by definition.
163
+ FailureCategory.AUTHENTICATION: RetryPolicy(max_attempts=1, base_delay=0.0, max_delay=0.0),
164
+ FailureCategory.BAD_REQUEST: RetryPolicy(max_attempts=1, base_delay=0.0, max_delay=0.0),
165
+ FailureCategory.CANCELLED: RetryPolicy(max_attempts=1, base_delay=0.0, max_delay=0.0),
166
+ }
167
+
168
+
169
+ def retry_policy_for(category: FailureCategory | str) -> RetryPolicy:
170
+ """Look up the retry policy for a failure category.
171
+
172
+ Accepts the generic ``FailureCategory`` or any string-valued category from
173
+ a domain extension (e.g. the video pipeline's ``VideoFailureCategory``).
174
+ Extension-only categories fall back to the UNKNOWN policy, so callers must
175
+ gate on ``failure.retryable`` before consulting attempt counts.
176
+ """
177
+ if not isinstance(category, FailureCategory):
178
+ try:
179
+ category = FailureCategory(str(category))
180
+ except ValueError:
181
+ category = FailureCategory.UNKNOWN
182
+ return _DEFAULT_RETRY_POLICIES.get(category, _DEFAULT_RETRY_POLICIES[FailureCategory.UNKNOWN])
183
+
184
+
185
+ # ---------------------------------------------------------------------------
186
+ # Circuit breaker
187
+ # ---------------------------------------------------------------------------
188
+
189
+
190
+ @dataclass
191
+ class _CircuitState:
192
+ consecutive_failures: int = 0
193
+ open_until: float = 0.0
194
+ half_open_in_flight: bool = False
195
+
196
+
197
+ class CircuitBreaker:
198
+ """Small circuit breaker keyed by provider/model identity.
199
+
200
+ Only transient categories (see ``TRANSIENT_CATEGORIES``) trip the breaker;
201
+ programmer errors and auth failures never open it. ``failure`` may be any
202
+ object with a ``category`` attribute whose value matches this module's
203
+ category strings, so domain extensions (e.g. the video pipeline's failure
204
+ type) plug in without conversion.
205
+ """
206
+
207
+ def __init__(self, threshold: int = 3, cooldown_seconds: float = 60.0) -> None:
208
+ self.threshold = max(1, int(threshold))
209
+ self.cooldown_seconds = max(0.1, float(cooldown_seconds))
210
+ self._states: dict[str, _CircuitState] = {}
211
+ self._lock = threading.RLock()
212
+
213
+ def allow(self, key: str, *, now: float | None = None) -> bool:
214
+ current = time.time() if now is None else now
215
+ with self._lock:
216
+ state = self._states.get(key)
217
+ if state is None:
218
+ return True
219
+ if state.open_until:
220
+ if current < state.open_until or state.half_open_in_flight:
221
+ return False
222
+ # Half-open: permit exactly one trial. Other concurrent calls
223
+ # continue to bypass the primary until it succeeds or fails.
224
+ state.half_open_in_flight = True
225
+ state.consecutive_failures = max(0, self.threshold - 1)
226
+ return True
227
+ return not state.half_open_in_flight
228
+
229
+ def open_remaining(self, key: str, *, now: float | None = None) -> float:
230
+ """Seconds until the circuit for ``key`` half-opens (0 if not open)."""
231
+ current = time.time() if now is None else now
232
+ with self._lock:
233
+ state = self._states.get(key)
234
+ if state is None or not state.open_until:
235
+ return 0.0
236
+ return max(0.0, state.open_until - current)
237
+
238
+ def record_success(self, key: str) -> None:
239
+ with self._lock:
240
+ self._states.pop(key, None)
241
+
242
+ def record_failure(self, key: str, failure: Any, *, now: float | None = None) -> None:
243
+ category = str(getattr(failure, "category", ""))
244
+ if category not in TRANSIENT_CATEGORIES:
245
+ with self._lock:
246
+ state = self._states.get(key)
247
+ if state is not None and state.half_open_in_flight:
248
+ self._states.pop(key, None)
249
+ return
250
+ current = time.time() if now is None else now
251
+ with self._lock:
252
+ state = self._states.setdefault(key, _CircuitState())
253
+ state.half_open_in_flight = False
254
+ state.consecutive_failures += 1
255
+ if state.consecutive_failures >= self.threshold:
256
+ state.open_until = current + self.cooldown_seconds
257
+
258
+ def snapshot(self) -> dict[str, dict[str, Any]]:
259
+ with self._lock:
260
+ return {
261
+ key: {
262
+ "consecutive_failures": state.consecutive_failures,
263
+ "open_until": state.open_until,
264
+ "half_open_in_flight": state.half_open_in_flight,
265
+ }
266
+ for key, state in self._states.items()
267
+ }
@@ -0,0 +1,389 @@
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
+ """Dynamic Multi-Provider Model Router for ARTEMIS.
16
+
17
+ Provides role-based LLM/VLM dispatching, dynamic fallback chains,
18
+ and unified provider configuration across Gemini, Vertex AI, OpenAI,
19
+ Anthropic, OpenRouter, and local Ollama/vLLM endpoints.
20
+ """
21
+
22
+ from enum import StrEnum
23
+ import hashlib
24
+ import os
25
+ from typing import Any
26
+
27
+ from langchain_core.language_models.chat_models import BaseChatModel
28
+ from pydantic import BaseModel, Field
29
+
30
+ from artemis.config.settings import settings
31
+ from artemis.llm.google.provider import supports_thinking_level
32
+ from third_party.mobile_use.utils.logger import get_logger
33
+
34
+ logger = get_logger(__name__)
35
+
36
+
37
+ class ModelProvider(StrEnum):
38
+ GOOGLE = "google"
39
+ GEMINI = "google"
40
+ VERTEX_AI = "vertexai"
41
+ OPENAI = "openai"
42
+ ANTHROPIC = "anthropic"
43
+ OPENROUTER = "openrouter"
44
+ XAI = "xai"
45
+ OLLAMA = "ollama"
46
+ VLLM = "vllm"
47
+ CUSTOM = "custom"
48
+
49
+ @classmethod
50
+ def from_string(cls, val: Any) -> "ModelProvider":
51
+ """Converts a string or enum into a canonical ModelProvider.
52
+
53
+ ``None``/empty means "use the default" (Google); an unrecognized
54
+ non-empty value raises instead of silently routing to Gemini with
55
+ the wrong credentials.
56
+ """
57
+ if isinstance(val, cls):
58
+ return val
59
+ if val is None:
60
+ return cls.GOOGLE
61
+ s = str(val).lower().replace("_", "").replace("-", "").strip()
62
+ if not s:
63
+ return cls.GOOGLE
64
+ mapping = {
65
+ "google": cls.GOOGLE,
66
+ "gemini": cls.GOOGLE,
67
+ "vertexai": cls.VERTEX_AI,
68
+ "vertex": cls.VERTEX_AI,
69
+ "openai": cls.OPENAI,
70
+ "anthropic": cls.ANTHROPIC,
71
+ "claude": cls.ANTHROPIC,
72
+ "openrouter": cls.OPENROUTER,
73
+ "xai": cls.XAI,
74
+ "grok": cls.XAI,
75
+ "ollama": cls.OLLAMA,
76
+ "vllm": cls.VLLM,
77
+ "custom": cls.CUSTOM,
78
+ }
79
+ provider = mapping.get(s)
80
+ if provider is None:
81
+ raise ValueError(
82
+ f"Unknown LLM provider {val!r}. Valid providers: {sorted(set(mapping))}"
83
+ )
84
+ return provider
85
+
86
+
87
+ class ModelEndpoint(BaseModel):
88
+ """Configuration definition for an LLM/VLM model endpoint."""
89
+
90
+ provider: ModelProvider = Field(default=ModelProvider.GOOGLE, description="Model provider")
91
+ model_name: str = Field(default="gemini-2.5-flash", description="Model name identifier")
92
+ api_key: str | None = Field(default=None, description="API Key or secret")
93
+ api_base: str | None = Field(default=None, description="Custom API endpoint base URL")
94
+ temperature: float = Field(default=0.0, description="Sampling temperature")
95
+ max_tokens: int | None = Field(default=None, description="Maximum completion tokens")
96
+ timeout_seconds: float = Field(default=60.0, description="Request timeout in seconds")
97
+ is_multimodal: bool = Field(
98
+ default=True, description="Whether endpoint accepts image/video inputs"
99
+ )
100
+ reasoning_effort: str | None = Field(
101
+ default=None, description="Reasoning effort: 'low', 'medium', 'high'"
102
+ )
103
+ thinking_budget: int | None = Field(default=None, description="Thinking budget token count")
104
+ thinking_level: str | None = Field(
105
+ default=None, description="Thinking level: 'minimal', 'low', 'medium', 'high'"
106
+ )
107
+ include_thoughts: bool | None = Field(
108
+ default=None, description="Whether to include thought traces in response"
109
+ )
110
+ enable_grounding: bool = Field(
111
+ default=False,
112
+ description="Whether to enable Google Search grounding for Gemini endpoints",
113
+ )
114
+
115
+ def cache_key(self) -> tuple:
116
+ """Key the client cache by endpoint settings, hashing the API key."""
117
+ api_key_digest = (
118
+ hashlib.sha256(self.api_key.encode("utf-8")).hexdigest()[:16] if self.api_key else None
119
+ )
120
+ return (
121
+ self.provider.value,
122
+ self.model_name,
123
+ self.temperature,
124
+ self.max_tokens,
125
+ self.timeout_seconds,
126
+ self.thinking_budget,
127
+ self.thinking_level,
128
+ self.include_thoughts,
129
+ self.reasoning_effort,
130
+ self.enable_grounding,
131
+ self.api_base,
132
+ api_key_digest,
133
+ )
134
+
135
+
136
+ def _patch_langchain_google_genai():
137
+ """Patches ChatGoogleGenerativeAI._process_tool_config to preserve and ensure include_server_side_tool_invocations."""
138
+ try:
139
+ from langchain_google_genai.chat_models import ChatGoogleGenerativeAI
140
+ from google.genai.types import ToolConfig
141
+
142
+ if getattr(ChatGoogleGenerativeAI, "_is_artemis_patched", False):
143
+ return
144
+
145
+ original_process = ChatGoogleGenerativeAI._process_tool_config
146
+
147
+ def patched_process_tool_config(self, tool_choice, tool_config, formatted_tools):
148
+ config = original_process(self, tool_choice, tool_config, formatted_tools)
149
+
150
+ has_builtin = False
151
+ has_functions = False
152
+ if formatted_tools:
153
+ for tool in formatted_tools:
154
+ tool_dict = tool if isinstance(tool, dict) else getattr(tool, "__dict__", {})
155
+ if any(
156
+ tool_dict.get(k) is not None
157
+ for k in (
158
+ "google_search",
159
+ "code_execution",
160
+ "google_maps",
161
+ "google_search_retrieval",
162
+ )
163
+ ):
164
+ has_builtin = True
165
+ if tool_dict.get("function_declarations"):
166
+ has_functions = True
167
+
168
+ if tool_config:
169
+ normalized = (
170
+ ToolConfig.model_validate(tool_config)
171
+ if isinstance(tool_config, dict)
172
+ else tool_config
173
+ )
174
+ if getattr(normalized, "include_server_side_tool_invocations", None) is not None:
175
+ if config is None:
176
+ config = ToolConfig(
177
+ include_server_side_tool_invocations=normalized.include_server_side_tool_invocations
178
+ )
179
+ else:
180
+ config.include_server_side_tool_invocations = (
181
+ normalized.include_server_side_tool_invocations
182
+ )
183
+
184
+ if has_builtin and has_functions:
185
+ if config is None:
186
+ config = ToolConfig(include_server_side_tool_invocations=True)
187
+ else:
188
+ config.include_server_side_tool_invocations = True
189
+
190
+ return config
191
+
192
+ ChatGoogleGenerativeAI._process_tool_config = patched_process_tool_config
193
+ ChatGoogleGenerativeAI._is_artemis_patched = True
194
+ except Exception as e:
195
+ logger.warning(f"Could not patch ChatGoogleGenerativeAI._process_tool_config: {e}")
196
+
197
+
198
+ class ModelFactory:
199
+ """Unified factory for instantiating and caching LangChain chat models across providers."""
200
+
201
+ _cache: dict[tuple, BaseChatModel] = {}
202
+
203
+ @classmethod
204
+ def get_model(cls, endpoint: ModelEndpoint) -> BaseChatModel:
205
+ key = endpoint.cache_key()
206
+ if key not in cls._cache:
207
+ cls._cache[key] = cls.create_model(endpoint)
208
+ return cls._cache[key]
209
+
210
+ @classmethod
211
+ def create_model(cls, endpoint: ModelEndpoint) -> BaseChatModel:
212
+ if os.environ.get("ARTEMIS_FAKE_LLM") == "1":
213
+ from artemis.llm.fake_model import FakeChatModel
214
+
215
+ delay = float(os.environ.get("ARTEMIS_FAKE_LLM_DELAY_S", "0") or 0)
216
+ logger.warning(
217
+ f"ARTEMIS_FAKE_LLM=1 — returning FakeChatModel (delay={delay}s) "
218
+ f"instead of {endpoint.provider}/{endpoint.model_name}"
219
+ )
220
+ return FakeChatModel(delay_s=delay)
221
+
222
+ provider = ModelProvider.from_string(endpoint.provider)
223
+
224
+ if provider == ModelProvider.GOOGLE:
225
+ _patch_langchain_google_genai()
226
+ from langchain_google_genai import (
227
+ ChatGoogleGenerativeAI,
228
+ HarmBlockThreshold,
229
+ HarmCategory,
230
+ )
231
+
232
+ api_key = (
233
+ endpoint.api_key
234
+ or (settings.GOOGLE_API_KEY.get_secret_value() if settings.GOOGLE_API_KEY else None)
235
+ or os.environ.get("GOOGLE_API_KEY")
236
+ or os.environ.get("GEMINI_API_KEY")
237
+ )
238
+ # Gemini 1.x/2.x only understand thinking_budget.
239
+ thinking_level = (
240
+ endpoint.thinking_level if supports_thinking_level(endpoint.model_name) else None
241
+ )
242
+
243
+ kwargs: dict[str, Any] = {
244
+ "model": endpoint.model_name,
245
+ "temperature": endpoint.temperature,
246
+ "max_output_tokens": endpoint.max_tokens,
247
+ "api_key": api_key,
248
+ "timeout": endpoint.timeout_seconds,
249
+ "thinking_budget": endpoint.thinking_budget,
250
+ "thinking_level": thinking_level,
251
+ "include_thoughts": endpoint.include_thoughts,
252
+ "safety_settings": {
253
+ HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
254
+ HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
255
+ HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
256
+ HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
257
+ },
258
+ }
259
+ return ChatGoogleGenerativeAI(**{k: v for k, v in kwargs.items() if v is not None})
260
+
261
+ elif provider == ModelProvider.VERTEX_AI:
262
+ from langchain_google_vertexai import (
263
+ ChatVertexAI,
264
+ HarmBlockThreshold,
265
+ HarmCategory,
266
+ )
267
+
268
+ kwargs = {
269
+ "model_name": endpoint.model_name,
270
+ "temperature": endpoint.temperature,
271
+ "max_output_tokens": endpoint.max_tokens,
272
+ "timeout": endpoint.timeout_seconds,
273
+ "thinking_budget": endpoint.thinking_budget,
274
+ "safety_settings": {
275
+ HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
276
+ HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
277
+ HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
278
+ HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
279
+ },
280
+ }
281
+ return ChatVertexAI(**{k: v for k, v in kwargs.items() if v is not None})
282
+
283
+ elif provider == ModelProvider.OPENAI:
284
+ from langchain_openai import ChatOpenAI
285
+
286
+ api_key = (
287
+ endpoint.api_key
288
+ or (settings.OPENAI_API_KEY.get_secret_value() if settings.OPENAI_API_KEY else None)
289
+ or os.environ.get("OPENAI_API_KEY", "EMPTY")
290
+ )
291
+ base_url = endpoint.api_base or (
292
+ str(settings.OPENAI_BASE_URL) if settings.OPENAI_BASE_URL else None
293
+ )
294
+ kwargs = {
295
+ "model": endpoint.model_name,
296
+ "temperature": endpoint.temperature,
297
+ "max_tokens": endpoint.max_tokens,
298
+ "api_key": api_key,
299
+ "base_url": base_url,
300
+ "timeout": endpoint.timeout_seconds,
301
+ }
302
+ if endpoint.reasoning_effort:
303
+ kwargs["reasoning_effort"] = endpoint.reasoning_effort
304
+ return ChatOpenAI(**{k: v for k, v in kwargs.items() if v is not None})
305
+
306
+ elif provider == ModelProvider.ANTHROPIC:
307
+ from langchain_anthropic import ChatAnthropic
308
+
309
+ api_key = (
310
+ endpoint.api_key
311
+ or (
312
+ settings.ANTHROPIC_API_KEY.get_secret_value()
313
+ if settings.ANTHROPIC_API_KEY
314
+ else None
315
+ )
316
+ or os.environ.get("ANTHROPIC_API_KEY")
317
+ )
318
+ kwargs = {
319
+ "model": endpoint.model_name,
320
+ "temperature": endpoint.temperature,
321
+ "api_key": api_key,
322
+ "timeout": endpoint.timeout_seconds,
323
+ }
324
+ budget = endpoint.thinking_budget
325
+ if not budget and endpoint.reasoning_effort:
326
+ effort_map = {"low": 2048, "medium": 8192, "high": 32768}
327
+ budget = effort_map.get(endpoint.reasoning_effort.lower())
328
+ if budget:
329
+ kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget}
330
+ kwargs["temperature"] = 1.0
331
+ return ChatAnthropic(**{k: v for k, v in kwargs.items() if v is not None})
332
+
333
+ elif provider == ModelProvider.OPENROUTER:
334
+ from langchain_openai import ChatOpenAI
335
+
336
+ api_key = (
337
+ endpoint.api_key
338
+ or (
339
+ settings.OPEN_ROUTER_API_KEY.get_secret_value()
340
+ if settings.OPEN_ROUTER_API_KEY
341
+ else None
342
+ )
343
+ or os.environ.get("OPEN_ROUTER_API_KEY")
344
+ )
345
+ return ChatOpenAI(
346
+ model=endpoint.model_name,
347
+ temperature=endpoint.temperature,
348
+ api_key=api_key,
349
+ base_url=endpoint.api_base or "https://openrouter.ai/api/v1",
350
+ timeout=endpoint.timeout_seconds,
351
+ )
352
+
353
+ elif provider == ModelProvider.XAI:
354
+ from langchain_openai import ChatOpenAI
355
+
356
+ api_key = (
357
+ endpoint.api_key
358
+ or (settings.XAI_API_KEY.get_secret_value() if settings.XAI_API_KEY else None)
359
+ or os.environ.get("XAI_API_KEY")
360
+ )
361
+ return ChatOpenAI(
362
+ model=endpoint.model_name,
363
+ temperature=endpoint.temperature,
364
+ api_key=api_key,
365
+ base_url=endpoint.api_base or "https://api.x.ai/v1",
366
+ timeout=endpoint.timeout_seconds,
367
+ )
368
+
369
+ elif provider in (ModelProvider.OLLAMA, ModelProvider.VLLM, ModelProvider.CUSTOM):
370
+ from langchain_openai import ChatOpenAI
371
+
372
+ api_key = endpoint.api_key or os.environ.get("OPENAI_API_KEY", "EMPTY")
373
+ base_url = endpoint.api_base or os.environ.get(
374
+ "OPENAI_BASE_URL", "http://localhost:8000/v1"
375
+ )
376
+ kwargs = {
377
+ "model": endpoint.model_name,
378
+ "temperature": endpoint.temperature,
379
+ "max_tokens": endpoint.max_tokens,
380
+ "api_key": api_key,
381
+ "base_url": base_url,
382
+ "timeout": endpoint.timeout_seconds,
383
+ }
384
+ if endpoint.reasoning_effort:
385
+ kwargs["reasoning_effort"] = endpoint.reasoning_effort
386
+ return ChatOpenAI(**{k: v for k, v in kwargs.items() if v is not None})
387
+
388
+ else:
389
+ raise ValueError(f"Unsupported model provider: {provider}")
@@ -0,0 +1,36 @@
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
+ """Dynamic LLM Schema definitions."""
16
+
17
+ from pydantic import BaseModel, Field
18
+
19
+
20
+ class ModelParameters(BaseModel):
21
+ """Fine-grained generation hyperparameters."""
22
+
23
+ temperature: float = Field(default=0.0, ge=0.0, le=2.0)
24
+ top_p: float | None = Field(default=None, ge=0.0, le=1.0)
25
+ top_k: int | None = Field(default=None, ge=1)
26
+ max_output_tokens: int = Field(default=4096, ge=1)
27
+ seed: int | None = Field(default=None)
28
+
29
+
30
+ class ThinkingBlock(BaseModel):
31
+ """Extracted internal chain-of-thought from thinking models."""
32
+
33
+ content: str = Field(..., description="Raw thought reasoning string")
34
+ is_native: bool = Field(
35
+ default=False, description="Whether thought was returned via native API field"
36
+ )