inspect-ai 0.3.70__py3-none-any.whl → 0.3.72__py3-none-any.whl

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 (219) hide show
  1. inspect_ai/_cli/eval.py +14 -8
  2. inspect_ai/_display/core/display.py +2 -0
  3. inspect_ai/_display/core/footer.py +13 -3
  4. inspect_ai/_display/plain/display.py +6 -2
  5. inspect_ai/_display/rich/display.py +19 -6
  6. inspect_ai/_display/textual/app.py +6 -1
  7. inspect_ai/_display/textual/display.py +4 -0
  8. inspect_ai/_display/textual/widgets/transcript.py +10 -6
  9. inspect_ai/_eval/task/run.py +5 -8
  10. inspect_ai/_util/content.py +20 -1
  11. inspect_ai/_util/transcript.py +10 -4
  12. inspect_ai/_util/working.py +4 -0
  13. inspect_ai/_view/www/App.css +6 -0
  14. inspect_ai/_view/www/dist/assets/index.css +115 -87
  15. inspect_ai/_view/www/dist/assets/index.js +5324 -2276
  16. inspect_ai/_view/www/eslint.config.mjs +24 -1
  17. inspect_ai/_view/www/log-schema.json +283 -20
  18. inspect_ai/_view/www/package.json +8 -3
  19. inspect_ai/_view/www/src/App.tsx +2 -2
  20. inspect_ai/_view/www/src/components/AnsiDisplay.tsx +4 -3
  21. inspect_ai/_view/www/src/components/Card.tsx +9 -8
  22. inspect_ai/_view/www/src/components/DownloadButton.tsx +2 -1
  23. inspect_ai/_view/www/src/components/EmptyPanel.tsx +2 -2
  24. inspect_ai/_view/www/src/components/ErrorPanel.tsx +4 -3
  25. inspect_ai/_view/www/src/components/ExpandablePanel.tsx +13 -5
  26. inspect_ai/_view/www/src/components/FindBand.tsx +3 -3
  27. inspect_ai/_view/www/src/components/HumanBaselineView.tsx +3 -3
  28. inspect_ai/_view/www/src/components/LabeledValue.tsx +5 -4
  29. inspect_ai/_view/www/src/components/LargeModal.tsx +18 -13
  30. inspect_ai/_view/www/src/components/{LightboxCarousel.css → LightboxCarousel.module.css} +22 -18
  31. inspect_ai/_view/www/src/components/LightboxCarousel.tsx +36 -27
  32. inspect_ai/_view/www/src/components/MessageBand.tsx +2 -1
  33. inspect_ai/_view/www/src/components/NavPills.tsx +9 -8
  34. inspect_ai/_view/www/src/components/ProgressBar.tsx +2 -1
  35. inspect_ai/_view/www/src/components/TabSet.tsx +21 -15
  36. inspect_ai/_view/www/src/index.tsx +2 -2
  37. inspect_ai/_view/www/src/metadata/MetaDataGrid.tsx +11 -9
  38. inspect_ai/_view/www/src/metadata/MetaDataView.tsx +3 -2
  39. inspect_ai/_view/www/src/metadata/MetadataGrid.module.css +1 -0
  40. inspect_ai/_view/www/src/metadata/RenderedContent.tsx +16 -0
  41. inspect_ai/_view/www/src/plan/DatasetDetailView.tsx +3 -2
  42. inspect_ai/_view/www/src/plan/DetailStep.tsx +2 -1
  43. inspect_ai/_view/www/src/plan/PlanCard.tsx +2 -5
  44. inspect_ai/_view/www/src/plan/PlanDetailView.tsx +6 -9
  45. inspect_ai/_view/www/src/plan/ScorerDetailView.tsx +2 -1
  46. inspect_ai/_view/www/src/plan/SolverDetailView.tsx +3 -3
  47. inspect_ai/_view/www/src/samples/InlineSampleDisplay.tsx +2 -2
  48. inspect_ai/_view/www/src/samples/SampleDialog.tsx +3 -3
  49. inspect_ai/_view/www/src/samples/SampleDisplay.tsx +2 -2
  50. inspect_ai/_view/www/src/samples/SampleSummaryView.tsx +2 -2
  51. inspect_ai/_view/www/src/samples/SamplesTools.tsx +2 -1
  52. inspect_ai/_view/www/src/samples/chat/ChatMessage.tsx +3 -19
  53. inspect_ai/_view/www/src/samples/chat/ChatMessageRenderer.tsx +2 -1
  54. inspect_ai/_view/www/src/samples/chat/ChatMessageRow.tsx +2 -1
  55. inspect_ai/_view/www/src/samples/chat/ChatView.tsx +2 -1
  56. inspect_ai/_view/www/src/samples/chat/ChatViewVirtualList.tsx +22 -7
  57. inspect_ai/_view/www/src/samples/chat/MessageContent.tsx +35 -6
  58. inspect_ai/_view/www/src/samples/chat/MessageContents.tsx +2 -2
  59. inspect_ai/_view/www/src/samples/chat/messages.ts +15 -2
  60. inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.tsx +13 -4
  61. inspect_ai/_view/www/src/samples/chat/tools/ToolInput.module.css +2 -2
  62. inspect_ai/_view/www/src/samples/chat/tools/ToolInput.tsx +18 -19
  63. inspect_ai/_view/www/src/samples/chat/tools/ToolOutput.module.css +1 -1
  64. inspect_ai/_view/www/src/samples/chat/tools/ToolOutput.tsx +4 -3
  65. inspect_ai/_view/www/src/samples/chat/tools/ToolTitle.tsx +2 -2
  66. inspect_ai/_view/www/src/samples/error/FlatSampleErrorView.tsx +2 -3
  67. inspect_ai/_view/www/src/samples/error/SampleErrorView.tsx +3 -2
  68. inspect_ai/_view/www/src/samples/list/SampleFooter.tsx +2 -1
  69. inspect_ai/_view/www/src/samples/list/SampleHeader.tsx +2 -1
  70. inspect_ai/_view/www/src/samples/list/SampleList.tsx +57 -45
  71. inspect_ai/_view/www/src/samples/list/SampleRow.tsx +2 -1
  72. inspect_ai/_view/www/src/samples/list/SampleSeparator.tsx +2 -1
  73. inspect_ai/_view/www/src/samples/sample-tools/EpochFilter.tsx +2 -2
  74. inspect_ai/_view/www/src/samples/sample-tools/SelectScorer.tsx +4 -3
  75. inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +2 -5
  76. inspect_ai/_view/www/src/samples/sample-tools/sample-filter/SampleFilter.tsx +2 -2
  77. inspect_ai/_view/www/src/samples/scores/SampleScoreView.tsx +2 -1
  78. inspect_ai/_view/www/src/samples/scores/SampleScores.tsx +2 -2
  79. inspect_ai/_view/www/src/samples/transcript/ApprovalEventView.tsx +2 -1
  80. inspect_ai/_view/www/src/samples/transcript/ErrorEventView.tsx +2 -1
  81. inspect_ai/_view/www/src/samples/transcript/InfoEventView.tsx +2 -1
  82. inspect_ai/_view/www/src/samples/transcript/InputEventView.tsx +2 -1
  83. inspect_ai/_view/www/src/samples/transcript/LoggerEventView.module.css +4 -0
  84. inspect_ai/_view/www/src/samples/transcript/LoggerEventView.tsx +12 -2
  85. inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +1 -1
  86. inspect_ai/_view/www/src/samples/transcript/ModelEventView.tsx +25 -28
  87. inspect_ai/_view/www/src/samples/transcript/SampleInitEventView.tsx +2 -1
  88. inspect_ai/_view/www/src/samples/transcript/SampleLimitEventView.tsx +5 -4
  89. inspect_ai/_view/www/src/samples/transcript/SampleTranscript.tsx +2 -2
  90. inspect_ai/_view/www/src/samples/transcript/SandboxEventView.tsx +8 -7
  91. inspect_ai/_view/www/src/samples/transcript/ScoreEventView.tsx +2 -2
  92. inspect_ai/_view/www/src/samples/transcript/StepEventView.tsx +3 -3
  93. inspect_ai/_view/www/src/samples/transcript/SubtaskEventView.tsx +18 -14
  94. inspect_ai/_view/www/src/samples/transcript/ToolEventView.tsx +5 -5
  95. inspect_ai/_view/www/src/samples/transcript/TranscriptView.tsx +34 -15
  96. inspect_ai/_view/www/src/samples/transcript/event/EventNav.tsx +2 -1
  97. inspect_ai/_view/www/src/samples/transcript/event/EventNavs.tsx +2 -1
  98. inspect_ai/_view/www/src/samples/transcript/event/EventRow.tsx +3 -2
  99. inspect_ai/_view/www/src/samples/transcript/event/EventSection.tsx +2 -2
  100. inspect_ai/_view/www/src/samples/transcript/event/EventTimingPanel.module.css +28 -0
  101. inspect_ai/_view/www/src/samples/transcript/event/EventTimingPanel.tsx +115 -0
  102. inspect_ai/_view/www/src/samples/transcript/event/utils.ts +29 -0
  103. inspect_ai/_view/www/src/samples/transcript/state/StateDiffView.tsx +2 -1
  104. inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.tsx +3 -3
  105. inspect_ai/_view/www/src/samples/transcript/state/StateEventView.tsx +11 -8
  106. inspect_ai/_view/www/src/types/log.d.ts +129 -34
  107. inspect_ai/_view/www/src/usage/ModelTokenTable.tsx +6 -10
  108. inspect_ai/_view/www/src/usage/ModelUsagePanel.module.css +4 -0
  109. inspect_ai/_view/www/src/usage/ModelUsagePanel.tsx +32 -9
  110. inspect_ai/_view/www/src/usage/TokenTable.tsx +4 -6
  111. inspect_ai/_view/www/src/usage/UsageCard.tsx +2 -1
  112. inspect_ai/_view/www/src/utils/format.ts +1 -1
  113. inspect_ai/_view/www/src/utils/json.ts +24 -0
  114. inspect_ai/_view/www/src/workspace/WorkSpace.tsx +6 -5
  115. inspect_ai/_view/www/src/workspace/WorkSpaceView.tsx +9 -2
  116. inspect_ai/_view/www/src/workspace/error/TaskErrorPanel.tsx +2 -1
  117. inspect_ai/_view/www/src/workspace/navbar/Navbar.tsx +2 -1
  118. inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.tsx +3 -3
  119. inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.tsx +4 -3
  120. inspect_ai/_view/www/src/workspace/navbar/SecondaryBar.tsx +5 -4
  121. inspect_ai/_view/www/src/workspace/navbar/StatusPanel.tsx +5 -8
  122. inspect_ai/_view/www/src/workspace/sidebar/EvalStatus.tsx +5 -4
  123. inspect_ai/_view/www/src/workspace/sidebar/LogDirectoryTitleView.tsx +2 -1
  124. inspect_ai/_view/www/src/workspace/sidebar/Sidebar.tsx +2 -1
  125. inspect_ai/_view/www/src/workspace/sidebar/SidebarLogEntry.tsx +2 -2
  126. inspect_ai/_view/www/src/workspace/sidebar/SidebarScoreView.tsx +2 -1
  127. inspect_ai/_view/www/src/workspace/sidebar/SidebarScoresView.tsx +2 -2
  128. inspect_ai/_view/www/src/workspace/tabs/InfoTab.tsx +2 -2
  129. inspect_ai/_view/www/src/workspace/tabs/JsonTab.tsx +2 -5
  130. inspect_ai/_view/www/src/workspace/tabs/SamplesTab.tsx +12 -11
  131. inspect_ai/_view/www/yarn.lock +241 -5
  132. inspect_ai/log/_condense.py +3 -0
  133. inspect_ai/log/_recorders/eval.py +6 -1
  134. inspect_ai/log/_transcript.py +58 -1
  135. inspect_ai/model/__init__.py +2 -0
  136. inspect_ai/model/_call_tools.py +7 -0
  137. inspect_ai/model/_chat_message.py +22 -7
  138. inspect_ai/model/_conversation.py +10 -8
  139. inspect_ai/model/_generate_config.py +25 -4
  140. inspect_ai/model/_model.py +133 -57
  141. inspect_ai/model/_model_output.py +3 -0
  142. inspect_ai/model/_openai.py +106 -40
  143. inspect_ai/model/_providers/anthropic.py +281 -153
  144. inspect_ai/model/_providers/google.py +27 -8
  145. inspect_ai/model/_providers/groq.py +9 -4
  146. inspect_ai/model/_providers/openai.py +57 -4
  147. inspect_ai/model/_providers/openai_o1.py +10 -0
  148. inspect_ai/model/_providers/providers.py +1 -1
  149. inspect_ai/model/_reasoning.py +15 -2
  150. inspect_ai/scorer/_model.py +23 -19
  151. inspect_ai/solver/_human_agent/agent.py +14 -10
  152. inspect_ai/solver/_human_agent/commands/__init__.py +7 -3
  153. inspect_ai/solver/_human_agent/commands/submit.py +76 -30
  154. inspect_ai/tool/__init__.py +2 -0
  155. inspect_ai/tool/_tool.py +3 -1
  156. inspect_ai/tool/_tools/_computer/_common.py +117 -58
  157. inspect_ai/tool/_tools/_computer/_computer.py +80 -57
  158. inspect_ai/tool/_tools/_computer/_resources/image_home_dir/.config/Code/User/settings.json +7 -1
  159. inspect_ai/tool/_tools/_computer/_resources/image_home_dir/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml +91 -0
  160. inspect_ai/tool/_tools/_computer/_resources/tool/.pylintrc +8 -0
  161. inspect_ai/tool/_tools/_computer/_resources/tool/.vscode/settings.json +12 -0
  162. inspect_ai/tool/_tools/_computer/_resources/tool/_args.py +78 -0
  163. inspect_ai/tool/_tools/_computer/_resources/tool/_constants.py +20 -0
  164. inspect_ai/tool/_tools/_computer/_resources/tool/_run.py +1 -1
  165. inspect_ai/tool/_tools/_computer/_resources/tool/_x11_client.py +175 -113
  166. inspect_ai/tool/_tools/_computer/_resources/tool/computer_tool.py +76 -20
  167. inspect_ai/tool/_tools/_computer/_resources/tool/pyproject.toml +65 -0
  168. inspect_ai/tool/_tools/_computer/test_args.py +151 -0
  169. inspect_ai/tool/_tools/_web_browser/_resources/.pylintrc +8 -0
  170. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/launch.json +24 -0
  171. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/settings.json +25 -0
  172. inspect_ai/tool/_tools/_web_browser/_resources/Dockerfile +5 -6
  173. inspect_ai/tool/_tools/_web_browser/_resources/README.md +10 -11
  174. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree.py +71 -0
  175. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree_node.py +323 -0
  176. inspect_ai/tool/_tools/_web_browser/_resources/cdp/__init__.py +5 -0
  177. inspect_ai/tool/_tools/_web_browser/_resources/cdp/a11y.py +279 -0
  178. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom.py +9 -0
  179. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom_snapshot.py +293 -0
  180. inspect_ai/tool/_tools/_web_browser/_resources/cdp/page.py +94 -0
  181. inspect_ai/tool/_tools/_web_browser/_resources/constants.py +2 -0
  182. inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.svg +2 -0
  183. inspect_ai/tool/_tools/_web_browser/_resources/playwright_browser.py +50 -0
  184. inspect_ai/tool/_tools/_web_browser/_resources/playwright_crawler.py +31 -359
  185. inspect_ai/tool/_tools/_web_browser/_resources/playwright_page_crawler.py +280 -0
  186. inspect_ai/tool/_tools/_web_browser/_resources/pyproject.toml +65 -0
  187. inspect_ai/tool/_tools/_web_browser/_resources/rectangle.py +64 -0
  188. inspect_ai/tool/_tools/_web_browser/_resources/rpc_client_helpers.py +146 -0
  189. inspect_ai/tool/_tools/_web_browser/_resources/scale_factor.py +64 -0
  190. inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_tree_node.py +180 -0
  191. inspect_ai/tool/_tools/_web_browser/_resources/test_playwright_crawler.py +15 -9
  192. inspect_ai/tool/_tools/_web_browser/_resources/test_rectangle.py +15 -0
  193. inspect_ai/tool/_tools/_web_browser/_resources/test_web_client.py +44 -0
  194. inspect_ai/tool/_tools/_web_browser/_resources/web_browser_rpc_types.py +39 -0
  195. inspect_ai/tool/_tools/_web_browser/_resources/web_client.py +198 -48
  196. inspect_ai/tool/_tools/_web_browser/_resources/web_client_new_session.py +26 -25
  197. inspect_ai/tool/_tools/_web_browser/_resources/web_server.py +178 -39
  198. inspect_ai/tool/_tools/_web_browser/_web_browser.py +38 -19
  199. inspect_ai/util/__init__.py +2 -1
  200. inspect_ai/util/_display.py +12 -0
  201. inspect_ai/util/_sandbox/events.py +55 -21
  202. inspect_ai/util/_sandbox/self_check.py +131 -43
  203. inspect_ai/util/_subtask.py +11 -0
  204. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/METADATA +1 -1
  205. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/RECORD +209 -186
  206. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/WHEEL +1 -1
  207. inspect_ai/_view/www/src/components/VirtualList.module.css +0 -19
  208. inspect_ai/_view/www/src/components/VirtualList.tsx +0 -292
  209. inspect_ai/tool/_tools/_computer/_computer_split.py +0 -198
  210. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_node.py +0 -312
  211. inspect_ai/tool/_tools/_web_browser/_resources/dm_env_servicer.py +0 -275
  212. inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.png +0 -0
  213. inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_node.py +0 -176
  214. inspect_ai/tool/_tools/_web_browser/_resources/test_dm_env_servicer.py +0 -135
  215. inspect_ai/tool/_tools/_web_browser/_resources/test_web_environment.py +0 -71
  216. inspect_ai/tool/_tools/_web_browser/_resources/web_environment.py +0 -184
  217. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/LICENSE +0 -0
  218. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/entry_points.txt +0 -0
  219. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2 @@
1
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1151.9489726043614 684.1200665344755" width="1151.9489726043614" height="684.1200665344755" class="excalidraw-svg"><!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1da1PbyFx1MDAxMv2eX+Fiv9xbXHUwMDE1tNPT89xPl+eFkFx1MDAxMFx1MDAxMshcdTAwMDLZbG3JtoxcdTAwMDXGNn7wyFb+++1cdTAwMTFgyZIsP1x1MDAxMIG9kGxlXHUwMDEzzUhqzXSfc3pe/P2mUllcdTAwMWHcdIOl3ypLwXXNb4X1nn+19NZdv1xmev2w06ZcIlx1MDAxZf2731x1MDAxOfZqUU2/2/3t11871X5YXHUwMDBm/bZ3Xr+9IWhcdTAwMDXnQXvQpyp/0L8rlb+jP6kkrLvbVqu7XHUwMDFierDVvj5a8Vt8s1x1MDAxZWxeXHUwMDFk8+jWqNK9XHUwMDFkvaA28NsnrSAuuqbry0JzXHUwMDBmXHUwMDE4/UYrjJSMj4pvXFwxR+VcdTAwMDFyXHUwMDA1QmpulEAzKr9cbuuDJtVcdTAwMDFA8Ohmq7liXHUwMDAyqeqoSjNcYk+aXHUwMDAzqiO09SRVofdcYmnBKjmqc2vUb1x1MDAxNTa60lx1MDAxZvQ6Z8Fap9XpOct/gcD9ju2u+rWzk15n2K6P6lxmen673/V71FBxvUbYau1cdTAwMGZuoqdTO1MvLKXecXj3XHQ8dX3SXfTSk2Y76PfH7O10/Vo4cM1cdTAwMDUsvuos7G7Xo377M7ap559cdTAwMDfbruPaw1ZrdDls14PryFx0+Njb2vW7t913etyjeHflR2x7XHUwMDEwuFx1MDAwNytqY7RcdTAwMThbXHUwMDEyu5xMdN/d1d1OO3I/tFppbbmKbeqvk+9ccqJnNvxWP4g7wFx1MDAxObaR9sukb4653iC4jnsl4bnr29ubXHUwMDE2Ts6WRkU/7v5cdTAwMTa317Bb929NXHUwMDAwLVx1MDAxOEfJQYLGUXkrbJ+lXHUwMDFis9WpncVWv0k0Uip0Rlx1MDAwNmSiZczkKFCQeVx1MDAxY9zr6e1cdTAwMDKYUSZcdTAwMWQoMDVQuPZcdTAwMTCspc9wgWIxXHUwMDFiJ/w1LibEXHUwMDA15sfFWO1RXHUwMDAwXGJcdTAwMTBK5fg/aj3J/0FcdTAwMTM+cU7/LVx1MDAxMlx1MDAwMGU6bOyMzlx06du/9INe5YNfa4btRG9cdTAwMTOjXHUwMDFjXHUwMDE0Vmh02oP98Lv7OM7Grm7652HL9YtcdTAwMWN710orPHGttFSjz1xuekvJplx1MDAxYYREY6NcboNONy6t0Vx1MDAxM316c297XHUwMDE2Pur0wpOw7beKbfeHg87noH9r/aA3XGaS7Vx1MDAxN2zdh1x1MDAwYnhcXFx1MDAxNsT3p+NLeX067OxcdTAwMGWPvrdbwfuj37Hbm4NcdTAwMWGZ9ZApxZQhxlx1MDAxMjzmtSjiXHUwMDAxmKc1l0xJTpDLIG7N+4jngnvaWK4lXG4tNf0vXHUwMDFi8iDAU1xcSis0MrohQZ+lQMAvvqybRuM5hn/MaV/58eD96coh9i5qO83TZvD1XWPlnlx1MDAxN+ZAXHTxYPZcdTAwMDRjOKJcdTAwMTImXHUwMDBmP1x1MDAwMFx1MDAxMSZcdTAwMDFcYtdMaKZcdTAwMTPd9+hcdTAwMDR62fjdXGbD041cdTAwMDSBvs1/0m3989PNzWFjb79cdTAwMDH7O/ZqQ1ZxXHUwMDA1zfiD71/p93qdq/mIWVxiQJ2M08WJefRhM1x1MDAxMPMycumR6rQopVaEtTpcdTAwMTWmTE5cdTAwMGJTLTxjrUXibaJwXHUwMDAzPFx1MDAxYqUvjJjLj0w5O39cdTAwMTNhMmY1ZsVqlG1AJjJjXHUwMDBlXHUwMDA3JSnJWCxcdTAwMDZcdTAwMGJ8m0RcdTAwMWRpvodw+Db1NtFMLn1nyp6IuafQZZq5M2aXQ9pcdTAwMWLb8OVD6+vpJlx1MDAxYoR4XHUwMDA14c7h9lxyzk7alMd4UnLLXHUwMDA0M8hI0sUtXHUwMDE1wYGUVMzQSqYp4+EsdpVcdTAwMTFpg4NcdTAwMDOUjJQ8XHRJncPZKu6Vcji6XHUwMDEx/XpcdTAwMTlIoGZHXHUwMDAyNMyCYZCXy4JkXHUwMDEzk1lQpKUkUlx1MDAxZv48Mj4+t43q3uHBrGT85WRQW/1+svZBrFx1MDAxZJzwU9g24ceV0shYXHUwMDExgEIy/lx1MDAxNifj0YfNRMaKeSSYmWVaMVx1MDAwM1x1MDAwNtPhx6aFXHUwMDFmUbRHZGzcYIRVRsts+MmSw+/FkbGeg4yZRVJVJsGpST3MJ+phIDlmSXTJ50fG/1x1MDAxOXQ6rW/tq6D6V5VcIosy0H/9O5eZo4qVXHT1noilp/BjmqVcdTAwMGI/oVx1MDAxY8YuTitSKDZcdTAwMGVcdTAwMTfcXG5PXHUwMDEyUysuUFx1MDAxM1x1MDAxZaS1O6CnLFx1MDAwM03ZN1x1MDAxN1xu40i7R4u44eOEWlx1MDAxMX9cdTAwMWKGwiq09NuUnFA/c7SYXHUwMDAzXHUwMDA2TD5cZmSzZX53JZstXHUwMDEzy3CpVD468ILhNlx1MDAwYlxcXHUwMDE5xn8mQ1x1MDAxZqi9le94czpcdTAwMTeTklxiRFxcXGJtup0wbe5cdTAwMWZcdKNY0kI2+vufb6fXXs66eHx/5pNafn+w1jk/XHUwMDBmXHUwMDA39GF7zqhcZtRcdTAwMGb83mCV/FwibJ+M9//d5NMs+UGEfbWh++Bl5jEpSFx1MDAwNlx1MDAxMFRcdTAwMWKFxjKbqHXid51LeZbCWkWj4ZyEepx0R9F0PW5qxv+Cdn26wZfVj6p9IJdPL9vQqF7uc32UXHUwMDE4ZExcdTAwMThM9lx1MDAxMpUx66hOKG2MXHUwMDExXCJjMcxlYNSmK1x1MDAwZfKagZ+JQTI/WZbGxqBV7VxczSTORj49kzjj4EmKPCFIXHUwMDFkU3IjU3BLLUBwK1x1MDAxNedIToXJycB7vKX0yYC12oJlhiuRSMZf6ljJXHUwMDFjgGtn113coSdcdTAwMDVcYuZcIutE1YWgOVx1MDAwM1x1MDAxMFx1MDAwYk3jXHUwMDE1waAgXHUwMDE0XHUwMDFjRfFcIqKrOVx1MDAxOHT7uSorVfKIuuo8rNeTI1x1MDAwNilpNUXIpKVVyu5yxNTq4LAx3Fxc391W5+y6sbq90v/cWZt5+INbj6FmXHUwMDFjyG+EtVwi9pNbPaUowIWO5Fx1MDAxNFquRXaSUlruXHUwMDExQCglXGYwQVx1MDAwMj5cdTAwMWLfqMFzXHUwMDEzXCLuNUpcdTAwMTn5svRVTFx1MDAwMuzT0fFlz3xcdTAwMWVemsN9/8rU2OHV1Vx1MDAwMtnYyqwybOKkXHUwMDA19Vx1MDAxMzFoMupjrFx1MDAxMIKnr96DhaC7qJvVT5zzb5/pXHUwMDBmq9vhylxcXHUwMDFhzI0liJJGM0ZcdTAwMDbMQJhcXIBH6kWA0Vx1MDAwMrTOzPk7XHUwMDE5NiWcQGlPoDWaSSstvs4slFx1MDAxZj6rs5OqVFpYkHnzXG5cXMhJYcJcdTAwMTFcdTAwMWPSlT6rMLdbZzh13Vx1MDAxNfcqa/c0lkuvkys91VxugWKKS9PsZPvLYdxO53N7a+P6uHbx6ejdUfM97lh1PDPjolx1MDAxNlx1MDAxZeXAmpFnMctlrJHuRjDAU9pcdTAwMWFkympSzDlDXHUwMDE4XHUwMDFjXFz6YYxiJL2NXHUwMDA0mbN+jnJlXHUwMDBmSNpcdTAwMTGEWMqaTTwnUc78Q5U3eLX6MvBi7eF0q6REUDxDrFRmcPL0pLRW0p2LLTFajG+H9f6nz5fBfGNcdTAwMWUyXHUwMDFhnSmHb0dcdTAwMDbMwLdcdTAwMDK4R3lcdTAwMGZYpIhRaFJrUYGpabFcdTAwMDQoKZYsJfAoQFDU5MxcdTAwMWW8XHUwMDEy7lx1MDAwM1x1MDAwM2h9dsLVXHUwMDFhjVwi/sybwNMsc3WUxXLr1klas0igXHUwMDE0OraRiSmnXHUwMDA1XHUwMDE411xypPeDXHUwMDFl2et1b3LpdkKNJ+LaKeSW5tpcdMaXQ7S1xtlaf+f31uF6/6vZOTSbLf86R4dPXCJaKzxrgUBcdTAwMDApt2Upnlx1MDAxNdJcdTAwMTOU9KJynaytjGVVrMStJ1x1MDAxNKfbuVwigJA8XHUwMDA3XHUwMDFhXuS8flmwsDE7LFx1MDAwMOVCwDilVDm4QFnVRClcdTAwMGUgJaksxUqk0FvnfH+8/mnj8HCYP1x1MDAwM1x1MDAxZoV/TKFvi560tdrfYltqcPTlUp33m9f90F/eyH/s/Fx1MDAxM/uS8lx1MDAxNDNcdTAwMGaCXHUwMDE1xGPmk4uomTOPW05WKLBMSjVcdTAwMWV+XFx7dkr4gfFcZlx0IK5cYvelTc78vzJzWSG4OVx1MDAwNzOTXHUwMDFmXHUwMDExXHUwMDE45i6TXHUwMDA3YScureGUpUgh9ELzdmU6doaa3XBsxZFXWMtfJp9f4emGm6fwYd5wc9b8cqj5xH9nVju1L19u9mpcdTAwMWNcdTAwMGZ2r1x1MDAwZj6vrJdDzVx1MDAxYTyrmKT0mHBcdTAwMDFVjmZ/JebHRYX/zoFcbkpoUt75XHUwMDBi7nT2cpzauuHnsV1nj57adjdcdTAwMGZrovlufVZevrlcdTAwMThe1C7+svi+1tzdOVvfap6Y/dJ4XHUwMDE5hUpcZrQ/iJdHXHUwMDFmNlx1MDAxMy9cdTAwMWK33JU4l+KLWTs+o2tcXGRcdTAwMTXGXHUwMDFl41x1MDAxZVwidbiyQkiGXHRB9srKZcXf1lx1MDAxY8KYc2uoP3J3b1x1MDAwMnVkQVx1MDAwMHKjlS57jHpuv87Q8l7Lv7nqRb6UR8p5xU9HyVN4ME3JecaXQ8jHXHUwMDFm19pcdTAwMWLrq2ts46/9LXZZXHUwMDA3ubVTnZmQlfFcdTAwMTi5XHUwMDEywVx1MDAwMicpx1hqXHUwMDFhWFwiaXmlXHJIRM21zFmEq5hcdTAwMDdotdvvaqTKXHUwMDEz61x1MDAxYT2pXHUwMDE1N25Dt7Ral71trc6qL4aht1x1MDAxZjwkTVx1MDAwNGA0XHUwMDEzLHduS1mbvnqPXHUwMDFiXHUwMDE2XHUwMDA1MqbL5O1bXHUwMDA3lpdX68OrxnY+wf6kfLrwuY+6XHUwMDAwXHUwMDFm0ZiyXHUwMDE24GeaskBcdTAwMGZcdTAwMTj0tNtDqq3bzS9Mas+qW1x1MDAxZsKMXHUwMDEyzDC03CidjXxcdTAwMTJcdTAwMTRukN1a0uPEQ/p1gVf58f5udkVgQUvUNneBrZJcdTAwMTNcdTAwMDU5N5ZcdTAwMTk0uuxcdTAwMTF0UJDEmFx1MDAwNUfQa62Q3lU0gp5T4+lUwVx1MDAxNDLOXHUwMDFiRM+xv1x1MDAxY2HQv/h0cvbfz9368c3FatO3vn2/488jXGasJGGghFx1MDAwMqmSh1x1MDAxZETNXHUwMDA3lEtQMoCSa6BMMG85y1RdXHUwMDAwTFDSYSxHelxmd6fClCxcZlx1MDAxYY26SS5d/39cdTAwMDaKnYdcdTAwMGJcdTAwMDPOjLCE5nmT1Sp7dbQ2XGZcclx1MDAwN5Os8OhcdP3uoCGHXHK4eVx1MDAxNlx0PUpGPlxcXHUwMDBlgY8+bFx1MDAwNlx1MDAwMo+2qytcdTAwMGXILONgXHUwMDE1XHUwMDFiX3OmXHUwMDE0SXeBXHUwMDE0gMwtyFVZ6W6UW+NNdTiSrFx1MDAxM8pmQ1S/8vfDwvL9XHUwMDFjXHUwMDE5vVKgKD/KUrXrTZwozFx0oyWpsLL521xy+eOD1pxtXHUwMDA1PnFlv/+tvdbsdc7D4fm39urt1rJcXDa/r1+5r17J1H46Zp/Cpmlmn+FbymH54pynaEtcdTAwMWQniVx1MDAwZlx1MDAwNFx1MDAwMFx1MDAxYY1AKZhIL6KRnpJcdTAwMDY1aJBcZlCZPJJcdTAwMTeeQZJcdTAwMDFcXFh30JXMS1x1MDAwMTzK+oVwi3VcdTAwMTRcYnd43CukPFxiUj7MyvST9+JJhVx1MDAwNlx1MDAwNMtcdTAwMGVcdTAwMTJGSFMwp25cdTAwMWOlWPtcdTAwMTNPrjn9Lj6yVT7fMnBiZCFcdTAwMTdcdTAwMWGLLHUr3uTwcL+ygVx1MDAxMT8v84mlbc0rzlAq41vzXHUwMDAw3EE4mmLfLbVcdTAwMTDxqvXKYlvdZtqLVzzZOW4heSS4QVxuyjUpiVx1MDAwNczuXHUwMDFlfJZ78UZOPYPM45p7yClkhVCaa5FcdTAwMWWnUZ5RjFIwy7S2UmdnbrTxnKyXwFx1MDAwNVxuxlx1MDAxMlv5XodpysLk3XlknmYgXHLknlk0eVx1MDAxYrR2p75cdTAwMDFbbFx1MDAwN86jQOW9zDvtd9qVXreWq+myhU8n4aZIpbSEy5pejmIrTkaLXHUwMDE0XHUwMDFiWlx1MDAxN8qSmFx1MDAwNCmaU6dcZrpcdFryXHUwMDEwxYQkLDRcdEeJ1Vx1MDAxYd3O0Fx1MDAxOKs1MCsxZ/nESz0vpSwg+PhgccZcdTAwMTknNFx1MDAxN9bkXHUwMDBlw+jJ4ky50yNcdTAwMDUsdk5CXHUwMDExQlx1MDAxMD6YhTb0liqmlie7b+r+x1x1MDAxM0/Fk74p8cSlZsxcdTAwMWHhXHUwMDAyXHUwMDEyXHUwMDEwOSbqPZp4Kk5T01x1MDAxNjJ0o7tWKyRYSVx1MDAxY0j5vNVT8XRcXOEpMiRcdTAwMTcpqbWUwrhBO5bcN3Cb86LnXCJcdTAwMGa5va2Rs1x1MDAxMIZcdTAwMGI37Vx1MDAwNYpcdTAwMDN3U3fxXGJJjKEk7ImoKYZRuIM3Slx1MDAxZdZ+5pg6XHUwMDA3WO49PJOl3IQxUsV5i1Mp9FxuMtnoMKHFRs1G9s2VyeL1hVx1MDAxY/avz+c8nk1bvtCZ0KWC7ySnj25Ou/vPQOLis61cdTAwMTI4x9y+bNJLym3MJo8wXHUwMDBmXHUwMDA1uZlgeJ4sm4RcdTAwMWI1oLFgXGZcdTAwMTPqn3KczMibZ0hhl1x1MDAwMaTnTvhcdTAwMTPE3FaMzVx1MDAwNt4msdLTjLtlKZJb7Vx1MDAwZW7Lild6RLTgVCsp3Fx1MDAxMuLX/XpzIO2nOfJTI8lcdTAwMTEpWnJ33Ew+SNNcdTAwMWTByKVZbGywaFx1MDAxZFx1MDAwMYUte9A6gvrttvDgOsjPUXPLny5NnVwib9Jpaq715WSqxadixT+4ptVcbrv9zPG6SnjUdU5JIeWlNn28rtsnpFx1MDAxOWrN3PAnSdFsyFx1MDAwYveDMizltFx1MDAwMpDQUeSEfEGdctZcdTAwMTBcdTAwMDSqUWvIf3j4XHUwMDFmPFxcaCHXilx1MDAwNHPuTnZcdTAwMGVcdTAwMTMnJ91UXHUwMDAzSJ48JP/RhdZF82Zv87z+4VlcdTAwMWN2L4zboD5cdTAwMDd8XHUwMDE1xOPow2bhXFyk+DKkfaJcdTAwMTNJKNfBXHUwMDE056KWXHUwMDFlXUSrXHUwMDE0XHUwMDE4zZMrxkcnPFx0lyxcdTAwMTEhg1x1MDAxMFx1MDAxMiFvu03ZXHUwMDA37D7jNTtzxNuX2elWKMI/wq/8XHUwMDEz7CefNMOsO4aQQdnT/u6HXHUwMDEyyVx1MDAwN037f+jUg9a39sredi7bRsWVsdKn49opXGaX5tpcdTAwMWPb52baN3dw4X6K2/6AXHUwMDFhf1x1MDAwNMTkbWE91Vx1MDAwZbfXXHUwMDA2QTdug+iSs2Sj7Vdb6Y5Yulxmg6vVyfvY3tyBi1x1MDAwYqwgXCKBXHUwMDFmb378XHUwMDBm7mQwXHUwMDE2In0=<!-- payload-end --></metadata><defs><style class="style-fonts">
2
+ @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,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); }</style></defs><rect x="0" y="0" width="1151.9489726043614" height="684.1200665344755" fill="#ffffff"></rect><g stroke-linecap="round" transform="translate(10 194.5711563885559) rotate(0 565.9744863021807 239.77445507295982)"><path d="M32 0 C440.26 0, 848.52 0, 1099.95 0 M32 0 C277.05 0, 522.1 0, 1099.95 0 M1099.95 0 C1121.28 0, 1131.95 10.67, 1131.95 32 M1099.95 0 C1121.28 0, 1131.95 10.67, 1131.95 32 M1131.95 32 C1131.95 150.62, 1131.95 269.25, 1131.95 447.55 M1131.95 32 C1131.95 185.13, 1131.95 338.25, 1131.95 447.55 M1131.95 447.55 C1131.95 468.88, 1121.28 479.55, 1099.95 479.55 M1131.95 447.55 C1131.95 468.88, 1121.28 479.55, 1099.95 479.55 M1099.95 479.55 C748.24 479.55, 396.52 479.55, 32 479.55 M1099.95 479.55 C834.42 479.55, 568.89 479.55, 32 479.55 M32 479.55 C10.67 479.55, 0 468.88, 0 447.55 M32 479.55 C10.67 479.55, 0 468.88, 0 447.55 M0 447.55 C0 312.26, 0 176.97, 0 32 M0 447.55 C0 300.27, 0 152.99, 0 32 M0 32 C0 10.67, 10.67 0, 32 0 M0 32 C0 10.67, 10.67 0, 32 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(512.3145351219611 199.5711563885559) rotate(0 63.65995118021965 12.5)"><text x="63.65995118021965" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">User Machine</text></g><g stroke-linecap="round" transform="translate(72.79440712890778 319.9312644412801) rotate(0 121.39463767378876 70.81279736512107)"><path d="M32 0 C83.4 0, 134.8 0, 210.79 0 C232.12 0, 242.79 10.67, 242.79 32 C242.79 54.49, 242.79 76.98, 242.79 109.63 C242.79 130.96, 232.12 141.63, 210.79 141.63 C156.76 141.63, 102.72 141.63, 32 141.63 C10.67 141.63, 0 130.96, 0 109.63 C0 91.26, 0 72.9, 0 32 C0 10.67, 10.67 0, 32 0" stroke="none" stroke-width="0" fill="#a5d8ff"></path><path d="M32 0 C69.15 0, 106.31 0, 210.79 0 M32 0 C69.22 0, 106.43 0, 210.79 0 M210.79 0 C232.12 0, 242.79 10.67, 242.79 32 M210.79 0 C232.12 0, 242.79 10.67, 242.79 32 M242.79 32 C242.79 59.47, 242.79 86.93, 242.79 109.63 M242.79 32 C242.79 57.81, 242.79 83.63, 242.79 109.63 M242.79 109.63 C242.79 130.96, 232.12 141.63, 210.79 141.63 M242.79 109.63 C242.79 130.96, 232.12 141.63, 210.79 141.63 M210.79 141.63 C150.82 141.63, 90.85 141.63, 32 141.63 M210.79 141.63 C162.02 141.63, 113.25 141.63, 32 141.63 M32 141.63 C10.67 141.63, 0 130.96, 0 109.63 M32 141.63 C10.67 141.63, 0 130.96, 0 109.63 M0 109.63 C0 79.86, 0 50.09, 0 32 M0 109.63 C0 90.44, 0 71.25, 0 32 M0 32 C0 10.67, 10.67 0, 32 0 M0 32 C0 10.67, 10.67 0, 32 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(156.7390783720325 324.9312644412801) rotate(0 37.44996643066406 12.5)"><text x="37.44996643066406" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Inspect</text></g><g stroke-linecap="round" transform="translate(84.54810986550194 375.15337588165823) rotate(0 107.44675228707365 30)"><path d="M0 0 L214.89 0 L214.89 60 L0 60" stroke="none" stroke-width="0" fill="#ffffff"></path><path d="M0 0 C53.75 0, 107.5 0, 214.89 0 M0 0 C68.76 0, 137.53 0, 214.89 0 M214.89 0 C214.89 15.05, 214.89 30.11, 214.89 60 M214.89 0 C214.89 14.27, 214.89 28.53, 214.89 60 M214.89 60 C165.27 60, 115.65 60, 0 60 M214.89 60 C159.4 60, 103.9 60, 0 60 M0 60 C0 44.54, 0 29.09, 0 0 M0 60 C0 44.31, 0 28.63, 0 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(121.49492318773184 380.15337588165823) rotate(0 70.49993896484375 25)"><text x="70.49993896484375" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">@tool</text><text x="70.49993896484375" y="42.62" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">web_browser()</text></g><g mask="url(#mask-mjFFufPSf1SK9wE5b3A38)" stroke-linecap="round"><g transform="translate(187.56015151078748 317.0135945389571) rotate(0 0 -82.44017481969692)"><path d="M0 0 C0 -27.48, 0 -137.4, 0 -164.88 M0 0 C0 -27.48, 0 -137.4, 0 -164.88" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(187.56015151078748 317.0135945389571) rotate(0 0 -82.44017481969692)"><path d="M8.55 -141.39 C5.51 -149.75, 2.46 -158.11, 0 -164.88 M8.55 -141.39 C5.63 -149.41, 2.71 -157.43, 0 -164.88" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(187.56015151078748 317.0135945389571) rotate(0 0 -82.44017481969692)"><path d="M-8.55 -141.39 C-5.51 -149.75, -2.46 -158.11, 0 -164.88 M-8.55 -141.39 C-5.63 -149.41, -2.71 -157.43, 0 -164.88" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask id="mask-mjFFufPSf1SK9wE5b3A38"><rect x="0" y="0" fill="#fff" width="287.5601515107875" height="581.8939441783509"></rect><rect x="159.65016191537427" y="222.07341971926016" fill="#000" width="55.819979190826416" height="25" opacity="1"></rect></mask><g transform="translate(159.65016191537427 222.07341971926016) rotate(0 27.909989595413208 12.5)"><text x="27.909989595413208" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">https</text></g><g stroke-linecap="round" transform="translate(511.1380355830496 261.0560067374928) rotate(0 296.0723324052453 185.5982010833427)"><path d="M32 0 C149.62 0, 267.24 0, 560.14 0 M32 0 C187.09 0, 342.18 0, 560.14 0 M560.14 0 C581.48 0, 592.14 10.67, 592.14 32 M560.14 0 C581.48 0, 592.14 10.67, 592.14 32 M592.14 32 C592.14 106.07, 592.14 180.14, 592.14 339.2 M592.14 32 C592.14 119.37, 592.14 206.75, 592.14 339.2 M592.14 339.2 C592.14 360.53, 581.48 371.2, 560.14 371.2 M592.14 339.2 C592.14 360.53, 581.48 371.2, 560.14 371.2 M560.14 371.2 C378.98 371.2, 197.81 371.2, 32 371.2 M560.14 371.2 C414.46 371.2, 268.78 371.2, 32 371.2 M32 371.2 C10.67 371.2, 0 360.53, 0 339.2 M32 371.2 C10.67 371.2, 0 360.53, 0 339.2 M0 339.2 C0 241.91, 0 144.63, 0 32 M0 339.2 C0 231.42, 0 123.64, 0 32 M0 32 C0 10.67, 10.67 0, 32 0 M0 32 C0 10.67, 10.67 0, 32 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(723.4904326903288 266.0560067374928) rotate(0 83.719935297966 12.5)"><text x="83.719935297966" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Docker Container</text></g><g stroke-linecap="round" transform="translate(856.813721439476 319.02394026433797) rotate(0 105.00943047842574 134.05862969901415)"><path d="M32 0 C86.15 0, 140.29 0, 178.02 0 C199.35 0, 210.02 10.67, 210.02 32 C210.02 106.31, 210.02 180.62, 210.02 236.12 C210.02 257.45, 199.35 268.12, 178.02 268.12 C148.78 268.12, 119.55 268.12, 32 268.12 C10.67 268.12, 0 257.45, 0 236.12 C0 180.91, 0 125.69, 0 32 C0 10.67, 10.67 0, 32 0" stroke="none" stroke-width="0" fill="#b2f2bb"></path><path d="M32 0 C66.75 0, 101.51 0, 178.02 0 M32 0 C80.59 0, 129.17 0, 178.02 0 M178.02 0 C199.35 0, 210.02 10.67, 210.02 32 M178.02 0 C199.35 0, 210.02 10.67, 210.02 32 M210.02 32 C210.02 81.92, 210.02 131.85, 210.02 236.12 M210.02 32 C210.02 96.51, 210.02 161.02, 210.02 236.12 M210.02 236.12 C210.02 257.45, 199.35 268.12, 178.02 268.12 M210.02 236.12 C210.02 257.45, 199.35 268.12, 178.02 268.12 M178.02 268.12 C148.2 268.12, 118.39 268.12, 32 268.12 M178.02 268.12 C133.28 268.12, 88.54 268.12, 32 268.12 M32 268.12 C10.67 268.12, 0 257.45, 0 236.12 M32 268.12 C10.67 268.12, 0 257.45, 0 236.12 M0 236.12 C0 182.7, 0 129.28, 0 32 M0 236.12 C0 157.95, 0 79.78, 0 32 M0 32 C0 10.67, 10.67 0, 32 0 M0 32 C0 10.67, 10.67 0, 32 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(894.3132077471885 324.02394026433797) rotate(0 67.50994417071345 12.5)"><text x="67.50994417071342" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">web_server.py</text></g><g stroke-linecap="round" transform="translate(877.0921202829592 385.28743460624077) rotate(0 84.73103163494261 30)"><path d="M0 0 L169.46 0 L169.46 60 L0 60" stroke="none" stroke-width="0" fill="#ffffff"></path><path d="M0 0 C53.78 0, 107.57 0, 169.46 0 M0 0 C52.33 0, 104.66 0, 169.46 0 M169.46 0 C169.46 18.81, 169.46 37.62, 169.46 60 M169.46 0 C169.46 16.96, 169.46 33.91, 169.46 60 M169.46 60 C117.09 60, 64.72 60, 0 60 M169.46 60 C103.78 60, 38.09 60, 0 60 M0 60 C0 37.64, 0 15.28, 0 0 M0 60 C0 40.17, 0 20.35, 0 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(902.3931885676058 402.78743460624077) rotate(0 59.42996335029602 12.5)"><text x="59.42996335029602" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">http service</text></g><g stroke-linecap="round" transform="translate(877.0921202829592 502.6643459409565) rotate(0 84.73103163494261 30)"><path d="M0 0 L169.46 0 L169.46 60 L0 60" stroke="none" stroke-width="0" fill="#ffffff"></path><path d="M0 0 C44.91 0, 89.83 0, 169.46 0 M0 0 C48.46 0, 96.93 0, 169.46 0 M169.46 0 C169.46 17.32, 169.46 34.64, 169.46 60 M169.46 0 C169.46 17.54, 169.46 35.09, 169.46 60 M169.46 60 C115.49 60, 61.52 60, 0 60 M169.46 60 C126.2 60, 82.93 60, 0 60 M0 60 C0 40.68, 0 21.36, 0 0 M0 60 C0 46.57, 0 33.14, 0 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(910.6531934456499 520.1643459409565) rotate(0 51.16995847225189 12.5)"><text x="51.16995847225189" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Playwright</text></g><g stroke-linecap="round" transform="translate(550.1132356735302 377.40698942769274) rotate(0 80.0698760592802 36.78814072979887)"><path d="M18.39 0 C52.31 0, 86.22 0, 141.75 0 C154.01 0, 160.14 6.13, 160.14 18.39 C160.14 31.12, 160.14 43.85, 160.14 55.18 C160.14 67.44, 154.01 73.58, 141.75 73.58 C105.91 73.58, 70.07 73.58, 18.39 73.58 C6.13 73.58, 0 67.44, 0 55.18 C0 42.96, 0 30.73, 0 18.39 C0 6.13, 6.13 0, 18.39 0" stroke="none" stroke-width="0" fill="#d0bfff"></path><path d="M18.39 0 C46.88 0, 75.37 0, 141.75 0 M18.39 0 C64.75 0, 111.1 0, 141.75 0 M141.75 0 C154.01 0, 160.14 6.13, 160.14 18.39 M141.75 0 C154.01 0, 160.14 6.13, 160.14 18.39 M160.14 18.39 C160.14 32.43, 160.14 46.47, 160.14 55.18 M160.14 18.39 C160.14 30.05, 160.14 41.71, 160.14 55.18 M160.14 55.18 C160.14 67.44, 154.01 73.58, 141.75 73.58 M160.14 55.18 C160.14 67.44, 154.01 73.58, 141.75 73.58 M141.75 73.58 C108.04 73.58, 74.33 73.58, 18.39 73.58 M141.75 73.58 C114.19 73.58, 86.62 73.58, 18.39 73.58 M18.39 73.58 C6.13 73.58, 0 67.44, 0 55.18 M18.39 73.58 C6.13 73.58, 0 67.44, 0 55.18 M0 55.18 C0 41.45, 0 27.72, 0 18.39 M0 55.18 C0 43.04, 0 30.89, 0 18.39 M0 18.39 C0 6.13, 6.13 0, 18.39 0 M0 18.39 C0 6.13, 6.13 0, 18.39 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(565.843161916345 401.6951301574916) rotate(0 64.33994981646538 12.5)"><text x="64.33994981646538" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">web_client.py</text></g><g stroke-linecap="round" transform="translate(551.0532404101184 482.26368448858506) rotate(0 80.0698760592802 52.23446167614486)"><path d="M26.12 0 C58.82 0, 91.53 0, 134.02 0 C151.43 0, 160.14 8.71, 160.14 26.12 C160.14 40.3, 160.14 54.49, 160.14 78.35 C160.14 95.76, 151.43 104.47, 134.02 104.47 C103.97 104.47, 73.91 104.47, 26.12 104.47 C8.71 104.47, 0 95.76, 0 78.35 C0 60.51, 0 42.68, 0 26.12 C0 8.71, 8.71 0, 26.12 0" stroke="none" stroke-width="0" fill="#ffd8a8"></path><path d="M26.12 0 C60.32 0, 94.52 0, 134.02 0 M26.12 0 C57.47 0, 88.83 0, 134.02 0 M134.02 0 C151.43 0, 160.14 8.71, 160.14 26.12 M134.02 0 C151.43 0, 160.14 8.71, 160.14 26.12 M160.14 26.12 C160.14 42.25, 160.14 58.38, 160.14 78.35 M160.14 26.12 C160.14 42.48, 160.14 58.85, 160.14 78.35 M160.14 78.35 C160.14 95.76, 151.43 104.47, 134.02 104.47 M160.14 78.35 C160.14 95.76, 151.43 104.47, 134.02 104.47 M134.02 104.47 C106.99 104.47, 79.96 104.47, 26.12 104.47 M134.02 104.47 C93.31 104.47, 52.6 104.47, 26.12 104.47 M26.12 104.47 C8.71 104.47, 0 95.76, 0 78.35 M26.12 104.47 C8.71 104.47, 0 95.76, 0 78.35 M0 78.35 C0 62.11, 0 45.88, 0 26.12 M0 78.35 C0 65.59, 0 52.83, 0 26.12 M0 26.12 C0 8.71, 8.71 0, 26.12 0 M0 26.12 C0 8.71, 8.71 0, 26.12 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(587.8631448507463 496.9981461647299) rotate(0 43.259971618652344 37.5)"><text x="43.259971618652344" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Headless</text><text x="43.259971618652344" y="42.62" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Chromium</text><text x="43.259971618652344" y="67.62" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Browser</text></g><g mask="url(#mask-HBsH0H6tXUv6mshxsia-E)" stroke-linecap="round"><g transform="translate(711.2529877920906 415.0453992464066) rotate(0 82.41956624543428 1.2977206098070697)"><path d="M0 0 C27.47 0.43, 137.37 2.16, 164.84 2.6 M0 0 C27.47 0.43, 137.37 2.16, 164.84 2.6" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(711.2529877920906 415.0453992464066) rotate(0 82.41956624543428 1.2977206098070697)"><path d="M141.22 10.78 C147.42 8.63, 153.62 6.48, 164.84 2.6 M141.22 10.78 C148.05 8.41, 154.88 6.04, 164.84 2.6" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(711.2529877920906 415.0453992464066) rotate(0 82.41956624543428 1.2977206098070697)"><path d="M141.48 -6.32 C147.62 -3.98, 153.75 -1.64, 164.84 2.6 M141.48 -6.32 C148.24 -3.74, 155 -1.16, 164.84 2.6" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask id="mask-HBsH0H6tXUv6mshxsia-E"><rect x="0" y="0" fill="#fff" width="976.0921202829592" height="517.6408404660208"></rect><rect x="754.4225784158245" y="403.84311985621366" fill="#000" width="78.49995124340057" height="25" opacity="1"></rect></mask><g transform="translate(754.4225784158245 403.84311985621366) rotate(0 39.24997562170029 12.5)"><text x="39.24997562170029" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">json rpc</text></g><g stroke-linecap="round"><g transform="translate(880.5968822997742 533.4353754623069) rotate(0 -84.20194488554768 0)"><path d="M0 0 C-28.07 0, -140.34 0, -168.4 0 M0 0 C-28.07 0, -140.34 0, -168.4 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(880.5968822997742 533.4353754623069) rotate(0 -84.20194488554768 0)"><path d="M-144.91 -8.55 C-154.19 -5.17, -163.47 -1.8, -168.4 0 M-144.91 -8.55 C-153.52 -5.42, -162.13 -2.28, -168.4 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(880.5968822997742 533.4353754623069) rotate(0 -84.20194488554768 0)"><path d="M-144.91 8.55 C-154.19 5.17, -163.47 1.8, -168.4 0 M-144.91 8.55 C-153.52 5.42, -162.13 2.28, -168.4 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g mask="url(#mask-UgtcBzgCM4CTg2j1I8iOA)" stroke-linecap="round"><g transform="translate(300.44161443964924 417.2055379619113) rotate(0 124.3358106169405 0.295333746717481)"><path d="M0 0 C41.45 0.1, 207.23 0.49, 248.67 0.59 M0 0 C41.45 0.1, 207.23 0.49, 248.67 0.59" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(300.44161443964924 417.2055379619113) rotate(0 124.3358106169405 0.295333746717481)"><path d="M225.16 9.09 C233.43 6.1, 241.7 3.11, 248.67 0.59 M225.16 9.09 C231.21 6.9, 237.25 4.72, 248.67 0.59" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(300.44161443964924 417.2055379619113) rotate(0 124.3358106169405 0.295333746717481)"><path d="M225.2 -8.02 C233.46 -4.99, 241.71 -1.96, 248.67 0.59 M225.2 -8.02 C231.24 -5.8, 237.27 -3.59, 248.67 0.59" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask id="mask-UgtcBzgCM4CTg2j1I8iOA"><rect x="0" y="0" fill="#fff" width="649.1132356735302" height="517.7962054553462"></rect><rect x="366.9174662290942" y="405.0008717086288" fill="#000" width="115.71991765499115" height="25" opacity="1"></rect></mask><g transform="translate(366.91746622909415 405.0008717086288) rotate(0 57.859958827495575 12.5)"><text x="57.859958827495575" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">docker exec</text></g><g stroke-linecap="round" transform="translate(117.92271401123094 10) rotate(0 70.56965820659423 70.56965820659423)"><path d="M141.14 70.57 C141.14 73.2, 140.99 75.85, 140.7 78.47 C140.4 81.09, 139.96 83.71, 139.37 86.27 C138.78 88.84, 138.05 91.39, 137.18 93.88 C136.31 96.36, 135.29 98.82, 134.15 101.19 C133.01 103.56, 131.72 105.88, 130.32 108.11 C128.92 110.35, 127.39 112.51, 125.74 114.57 C124.1 116.63, 122.33 118.61, 120.47 120.47 C118.61 122.33, 116.63 124.1, 114.57 125.74 C112.51 127.39, 110.35 128.92, 108.11 130.32 C105.88 131.72, 103.56 133.01, 101.19 134.15 C98.82 135.29, 96.36 136.31, 93.88 137.18 C91.39 138.05, 88.84 138.78, 86.27 139.37 C83.71 139.96, 81.09 140.4, 78.47 140.7 C75.85 140.99, 73.2 141.14, 70.57 141.14 C67.94 141.14, 65.29 140.99, 62.67 140.7 C60.05 140.4, 57.43 139.96, 54.87 139.37 C52.3 138.78, 49.75 138.05, 47.26 137.18 C44.78 136.31, 42.32 135.29, 39.95 134.15 C37.58 133.01, 35.25 131.72, 33.02 130.32 C30.79 128.92, 28.63 127.39, 26.57 125.74 C24.51 124.1, 22.53 122.33, 20.67 120.47 C18.81 118.61, 17.04 116.63, 15.4 114.57 C13.75 112.51, 12.22 110.35, 10.82 108.11 C9.42 105.88, 8.13 103.56, 6.99 101.19 C5.85 98.82, 4.83 96.36, 3.96 93.88 C3.09 91.39, 2.36 88.84, 1.77 86.27 C1.18 83.71, 0.74 81.09, 0.44 78.47 C0.15 75.85, 0 73.2, 0 70.57 C0 67.94, 0.15 65.29, 0.44 62.67 C0.74 60.05, 1.18 57.43, 1.77 54.87 C2.36 52.3, 3.09 49.75, 3.96 47.26 C4.83 44.78, 5.85 42.32, 6.99 39.95 C8.13 37.58, 9.42 35.25, 10.82 33.02 C12.22 30.79, 13.75 28.63, 15.4 26.57 C17.04 24.51, 18.81 22.53, 20.67 20.67 C22.53 18.81, 24.51 17.04, 26.57 15.4 C28.63 13.75, 30.79 12.22, 33.02 10.82 C35.25 9.42, 37.58 8.13, 39.95 6.99 C42.32 5.85, 44.78 4.83, 47.26 3.96 C49.75 3.09, 52.3 2.36, 54.87 1.77 C57.43 1.18, 60.05 0.74, 62.67 0.44 C65.29 0.15, 67.94 0, 70.57 0 C73.2 0, 75.85 0.15, 78.47 0.44 C81.09 0.74, 83.71 1.18, 86.27 1.77 C88.84 2.36, 91.39 3.09, 93.88 3.96 C96.36 4.83, 98.82 5.85, 101.19 6.99 C103.56 8.13, 105.88 9.42, 108.11 10.82 C110.35 12.22, 112.51 13.75, 114.57 15.4 C116.63 17.04, 118.61 18.81, 120.47 20.67 C122.33 22.53, 124.1 24.51, 125.74 26.57 C127.39 28.63, 128.92 30.79, 130.32 33.02 C131.72 35.25, 133.01 37.58, 134.15 39.95 C135.29 42.32, 136.31 44.78, 137.18 47.26 C138.05 49.75, 138.78 52.3, 139.37 54.87 C139.96 57.43, 140.4 60.05, 140.7 62.67 C140.99 65.29, 141.07 69.25, 141.14 70.57 C141.21 71.89, 141.21 69.25, 141.14 70.57" stroke="none" stroke-width="0" fill="#e6fcf5"></path><path d="M141.14 70.57 C141.14 73.2, 140.99 75.85, 140.7 78.47 C140.4 81.09, 139.96 83.71, 139.37 86.27 C138.78 88.84, 138.05 91.39, 137.18 93.88 C136.31 96.36, 135.29 98.82, 134.15 101.19 C133.01 103.56, 131.72 105.88, 130.32 108.11 C128.92 110.35, 127.39 112.51, 125.74 114.57 C124.1 116.63, 122.33 118.61, 120.47 120.47 C118.61 122.33, 116.63 124.1, 114.57 125.74 C112.51 127.39, 110.35 128.92, 108.11 130.32 C105.88 131.72, 103.56 133.01, 101.19 134.15 C98.82 135.29, 96.36 136.31, 93.88 137.18 C91.39 138.05, 88.84 138.78, 86.27 139.37 C83.71 139.96, 81.09 140.4, 78.47 140.7 C75.85 140.99, 73.2 141.14, 70.57 141.14 C67.94 141.14, 65.29 140.99, 62.67 140.7 C60.05 140.4, 57.43 139.96, 54.87 139.37 C52.3 138.78, 49.75 138.05, 47.26 137.18 C44.78 136.31, 42.32 135.29, 39.95 134.15 C37.58 133.01, 35.25 131.72, 33.02 130.32 C30.79 128.92, 28.63 127.39, 26.57 125.74 C24.51 124.1, 22.53 122.33, 20.67 120.47 C18.81 118.61, 17.04 116.63, 15.4 114.57 C13.75 112.51, 12.22 110.35, 10.82 108.11 C9.42 105.88, 8.13 103.56, 6.99 101.19 C5.85 98.82, 4.83 96.36, 3.96 93.88 C3.09 91.39, 2.36 88.84, 1.77 86.27 C1.18 83.71, 0.74 81.09, 0.44 78.47 C0.15 75.85, 0 73.2, 0 70.57 C0 67.94, 0.15 65.29, 0.44 62.67 C0.74 60.05, 1.18 57.43, 1.77 54.87 C2.36 52.3, 3.09 49.75, 3.96 47.26 C4.83 44.78, 5.85 42.32, 6.99 39.95 C8.13 37.58, 9.42 35.25, 10.82 33.02 C12.22 30.79, 13.75 28.63, 15.4 26.57 C17.04 24.51, 18.81 22.53, 20.67 20.67 C22.53 18.81, 24.51 17.04, 26.57 15.4 C28.63 13.75, 30.79 12.22, 33.02 10.82 C35.25 9.42, 37.58 8.13, 39.95 6.99 C42.32 5.85, 44.78 4.83, 47.26 3.96 C49.75 3.09, 52.3 2.36, 54.87 1.77 C57.43 1.18, 60.05 0.74, 62.67 0.44 C65.29 0.15, 67.94 0, 70.57 0 C73.2 0, 75.85 0.15, 78.47 0.44 C81.09 0.74, 83.71 1.18, 86.27 1.77 C88.84 2.36, 91.39 3.09, 93.88 3.96 C96.36 4.83, 98.82 5.85, 101.19 6.99 C103.56 8.13, 105.88 9.42, 108.11 10.82 C110.35 12.22, 112.51 13.75, 114.57 15.4 C116.63 17.04, 118.61 18.81, 120.47 20.67 C122.33 22.53, 124.1 24.51, 125.74 26.57 C127.39 28.63, 128.92 30.79, 130.32 33.02 C131.72 35.25, 133.01 37.58, 134.15 39.95 C135.29 42.32, 136.31 44.78, 137.18 47.26 C138.05 49.75, 138.78 52.3, 139.37 54.87 C139.96 57.43, 140.4 60.05, 140.7 62.67 C140.99 65.29, 141.07 69.25, 141.14 70.57 C141.21 71.89, 141.21 69.25, 141.14 70.57" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(161.26210178165985 55.66937434269454) rotate(0 27.329986572265625 25)"><text x="27.329986572265625" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Model</text><text x="27.329986572265625" y="42.62" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">API</text></g></svg>
@@ -0,0 +1,50 @@
1
+ import logging
2
+ from os import getenv
3
+
4
+ from playwright.async_api import Browser, BrowserContext, Playwright, async_playwright
5
+
6
+
7
+ class PlaywrightBrowser:
8
+ """Stores the browser and creates new contexts."""
9
+
10
+ WIDTH = 1280
11
+ HEIGHT = 1080
12
+ _playwright: Playwright | None = None
13
+
14
+ @classmethod
15
+ async def create(cls, headless: bool | None = None) -> "PlaywrightBrowser":
16
+ if PlaywrightBrowser._playwright is None:
17
+ PlaywrightBrowser._playwright = await async_playwright().start()
18
+
19
+ headless = True if headless is None else headless
20
+ logging.info(
21
+ "Starting chromium in %s mode.", "headless" if headless else "headful"
22
+ )
23
+
24
+ return PlaywrightBrowser(
25
+ await PlaywrightBrowser._playwright.chromium.launch(
26
+ headless=headless,
27
+ # Required for Gmail signup see
28
+ # https://stackoverflow.com/questions/65139098/how-to-login-to-google-account-with-playwright
29
+ args=["--disable-blink-features=AutomationControlled"],
30
+ )
31
+ )
32
+
33
+ def __init__(self, browser: Browser) -> None:
34
+ self._browser = browser
35
+
36
+ async def get_new_context(self) -> BrowserContext:
37
+ return await self._browser.new_context(
38
+ geolocation={"longitude": -0.12, "latitude": 51},
39
+ locale="en-GB",
40
+ permissions=["geolocation"],
41
+ timezone_id="Europe/London",
42
+ viewport={"width": self.WIDTH, "height": self.HEIGHT},
43
+ ignore_https_errors=getenv("IGNORE_HTTPS_ERRORS", "") != "",
44
+ )
45
+
46
+ async def close(self) -> None:
47
+ await self._browser.close()
48
+ if PlaywrightBrowser._playwright is not None:
49
+ await PlaywrightBrowser._playwright.stop()
50
+ PlaywrightBrowser._playwright = None
@@ -5,372 +5,44 @@ Largely based on https://github.com/web-arena-x/webarena
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- import enum
9
- import logging
10
- import re
11
- import time
12
- from os import getenv
13
- from typing import Any, Literal
8
+ from asyncio.futures import Future
14
9
 
15
- import accessibility_node as at
16
- from playwright.sync_api import sync_playwright
10
+ from playwright.async_api import BrowserContext, Page
17
11
 
18
- # Number of seconds to wait for page to load before processing it.
19
- WAIT_FOR_PAGE_TIME = 2.0
20
-
21
- # The waiting strategy to use between browser commands.
22
- # see https://playwright.dev/docs/api/class-page.
23
- WAIT_STRATEGY = "domcontentloaded"
24
-
25
-
26
- class CrawlerOutputFormat(enum.Enum):
27
- # Raw HTML.
28
- HTML = 0
29
- # Raw Document Object Model.
30
- DOM = 1
31
- # Accessibility tree.
32
- AT = 2
33
- # A pixel-based rending of the webpage.
34
- PIXELS = 3
35
-
36
-
37
- class PlaywrightBrowser:
38
- """Stores the browser and creates new contexts."""
39
-
40
- WIDTH = 1280
41
- HEIGHT = 1080
42
- _playwright_api = None
43
-
44
- def __init__(self):
45
- """Creates the browser."""
46
- if PlaywrightBrowser._playwright_api is None:
47
- PlaywrightBrowser._playwright_api = sync_playwright().start()
48
-
49
- logging.info("Starting chromium in headless mode.")
50
-
51
- self._browser = PlaywrightBrowser._playwright_api.chromium.launch(
52
- headless=True,
53
- # Required for Gmail signup see
54
- # https://stackoverflow.com/questions/65139098/how-to-login-to-google-account-with-playwright
55
- args=["--disable-blink-features=AutomationControlled"],
56
- )
57
-
58
- def get_new_context(self):
59
- return self._browser.new_context(
60
- geolocation={"longitude": -0.12, "latitude": 51},
61
- locale="en-GB",
62
- permissions=["geolocation"],
63
- timezone_id="Europe/London",
64
- viewport={"width": self.WIDTH, "height": self.HEIGHT},
65
- ignore_https_errors=getenv("IGNORE_HTTPS_ERRORS", "") != "",
66
- )
67
-
68
- def close(self):
69
- self._browser.close()
70
- if PlaywrightBrowser._playwright_api is not None:
71
- PlaywrightBrowser._playwright_api.stop()
72
- PlaywrightBrowser._playwright_api = None
12
+ from playwright_page_crawler import PageCrawler
73
13
 
74
14
 
75
15
  class PlaywrightCrawler:
76
- """Stores the accessibility tree."""
77
-
78
- def __init__(self, browser_context):
79
- """Initialize the craweler."""
80
- self._context = browser_context
81
-
82
- self._page = None
83
- self._client = None
84
- self._root = None
85
- self._nodes = {}
86
- self._dom_tree = None
87
-
88
- self._initialize_context()
89
-
90
- def _initialize_context(self):
91
- """Creates playwright page, and client."""
92
- if self._page:
93
- # Close the previous page if it was open.
94
- self._page.close()
95
-
96
- self._page = self._context.new_page()
97
-
98
- # Enable chrome development tools, and accessabiltiy tree output.
99
- self._client = self._page.context.new_cdp_session(self._page)
100
- self._client.send("Accessibility.enable")
101
-
102
- # Start with an empty accessibility tree and DOM.
103
- self._root = None
104
- self._nodes = {}
105
- self._dom_tree = None
106
-
107
- def lookup_node(
108
- self, node_id_or_tag: int | str, include_ignored: bool = False
109
- ) -> at.AccessibilityNode:
110
- """Looks up the node by id or tag.
111
-
112
- Args:
113
- node_id_or_tag: Either the id number (as int or str), or <tag_name>
114
- include_ignored: If true will also lookup ignored (hidden) node.
115
-
116
- Returns:
117
- Node.
118
-
119
- Raise:
120
- Value error if node is not matched.
121
- """
122
- if re.match("^<.*>", str(node_id_or_tag)):
123
- tag = node_id_or_tag[1:-1]
124
- # This is a smart tag, try to resolve it.
125
- for node in self._nodes.values():
126
- # We match on anything that starts with the code, this is potentially
127
- # a little brittle, can be replaced with an RE if there are issues.
128
- if node.name.lower().startswith(tag.lower()):
129
- if not node.is_ignored or include_ignored:
130
- return node
131
- else:
132
- raise ValueError(
133
- f"Could not find tag {node_id_or_tag} from"
134
- + f" {[node.name for node in self._nodes.values() if node.name]}"
135
- )
136
- else:
137
- node_id = str(node_id_or_tag)
138
- node = self._nodes.get(node_id, None)
139
- if node and (include_ignored or not node.is_ignored):
140
- return node
141
- else:
142
- raise ValueError(f"Could not find element with id {node_id}")
143
-
144
- def update(self):
145
- """Updates the accessibility tree and DOM from current page."""
146
- # Wait for page to load.
147
- self._page.wait_for_load_state(WAIT_STRATEGY)
148
- time.sleep(WAIT_FOR_PAGE_TIME)
149
-
150
- # Get the DOM
151
- self._dom_tree = self._client.send(
152
- "DOMSnapshot.captureSnapshot",
153
- {
154
- "computedStyles": [],
155
- "includeDOMRects": True,
156
- "includePaintOrder": True,
157
- },
158
- )
159
-
160
- at_nodes = self._client.send("Accessibility.getFullAXTree", {})["nodes"]
161
-
162
- document = self._dom_tree["documents"][0]
163
- nodes = document["nodes"]
164
- layouts = document["layout"]
165
- srcs = nodes["currentSourceURL"]
166
- backendnode_ids = nodes["backendNodeId"]
167
- strings = self._dom_tree["strings"]
168
-
169
- text_value = nodes["textValue"]
170
-
171
- # Check current screen bounds.
172
- win_upper_bound = self._page.evaluate("window.pageYOffset")
173
- win_left_bound = self._page.evaluate("window.pageXOffset")
174
- win_width = self._page.evaluate("window.screen.width")
175
- win_height = self._page.evaluate("window.screen.height")
176
- window_bounds = at.NodeBounds(
177
- win_left_bound, win_upper_bound, win_width, win_height
16
+ @classmethod
17
+ async def create(
18
+ cls, browser_context: BrowserContext, device_scale_factor: float | None = None
19
+ ) -> PlaywrightCrawler:
20
+ page_crawler = await PageCrawler.create(
21
+ await browser_context.new_page(), device_scale_factor
178
22
  )
179
23
 
180
- # We have ids for the DOM and for the AT, and need to map between them.
181
- dom_to_at = {}
182
-
183
- # Build the AT tree.
184
- self._nodes: dict[str, at.AccessibilityNode] = {}
185
- self._root = None
186
- for at_node in at_nodes:
187
- node = at.AccessibilityNode(at_node)
188
- self._nodes[node.node_id] = node
189
- if not node.is_ignored:
190
- self._root = self._root or node
191
- dom_to_at[node.dom_id] = node.node_id
192
- # Default node to invisible. For it to be made visible it must turn up in
193
- # the layout below.
194
- node["is_visible"] = False
195
- # Also keep track of any AT nodes that did not show up in the DOM tree.
196
- node["is_matched"] = False
24
+ return PlaywrightCrawler(browser_context, page_crawler, device_scale_factor)
197
25
 
198
- # Create a lookup so that the matching below is not O(n^2)
199
- backend_index_lookup = {idx: index for index, idx in enumerate(backendnode_ids)}
200
-
201
- layout_from_backend = {}
202
- for i, key in enumerate(layouts["nodeIndex"]):
203
- layout_from_backend[key] = {
204
- k: layouts[k][i]
205
- for k in layouts.keys()
206
- if len(layouts[k]) == len(layouts["nodeIndex"])
207
- }
208
-
209
- text_from_backend = {
210
- index: value
211
- for index, value in zip(text_value["index"], text_value["value"])
212
- }
213
-
214
- src_from_backend = {
215
- index: value for index, value in zip(srcs["index"], srcs["value"])
216
- }
217
-
218
- # Set element bounds and visibility.
219
- # To do this we first lookup the position of an AT node's dom_id in the
220
- # backendNodeIds. We then lookup this index in layout_node_index, which
221
- # gives us the "index of the index" which is what we need to find the bounds
222
- # of this element.
223
- for node in self._nodes.values():
224
- if node.dom_id not in backend_index_lookup:
225
- # Sometimes we can not match, but that's fine, just ignore.
226
- node["is_matched"] = False
227
- continue
228
-
229
- backend_index = backend_index_lookup[node.dom_id]
230
-
231
- if src := src_from_backend.get(backend_index, None):
232
- node["src"] = strings[src]
233
-
234
- layout = layout_from_backend.get(backend_index, None)
235
- if layout:
236
- node.bounds = at.NodeBounds(*layout["bounds"])
237
- used_bounds = (
238
- node.get_union_bounds() if at.USE_UNION_BOUNDS else node.bounds
239
- )
240
- has_bounds = used_bounds.area > 0
241
- on_screen = used_bounds.overlaps(window_bounds)
242
- node["is_visible"] = has_bounds and on_screen
243
- else:
244
- node["is_visible"] = False
245
-
246
- node["is_matched"] = True
247
-
248
- # For nodes that are editable also record their current input text.
249
- for node in filter(lambda x: x.is_editable, self._nodes.values()):
250
- # Sometimes a node will have it's input stored as 'value' othertimes we
251
- # need to go looking through the DOM tree to find its matching string.
252
-
253
- if node.value:
254
- node["input"] = node.value
255
- else:
256
- if node.dom_id not in backend_index_lookup:
257
- # Sometimes web_at nodes don't appear in the DOM for some reason.
258
- continue
259
- backend_index = backend_index_lookup[node.dom_id]
260
- text_index = text_from_backend.get(backend_index, -1)
261
- if text_index >= 0:
262
- node["input"] = strings[text_index]
263
-
264
- # Map AT children and parents.
265
- for node in self._nodes.values():
266
- node.link_children(self._nodes)
267
-
268
- # Make menuitem's visible
269
- for node in [node for node in self._nodes.values() if node.role == "menuitem"]:
270
- node["is_visible"] = (
271
- node.role == "menuitem"
272
- and node.parent is not None
273
- and node.parent.is_expanded
274
- )
275
-
276
- def render(self, output_format: CrawlerOutputFormat) -> Any:
277
- """Returns the current webpage in the desired format.
278
-
279
- Only elements visible on the screen will be rendered.
280
-
281
- Args:
282
- output_format: The rending format to output.
283
-
284
- Returns:
285
- the currently active webpage rendered using given format.
286
- """
287
- match output_format:
288
- case CrawlerOutputFormat.AT:
289
- return self._render_at()
290
- case _:
291
- # TODO: Implement DOM, HTML, PIXELS formats
292
- raise NotImplementedError(
293
- "Playwright crawler does not currently support"
294
- f" {output_format} output."
295
- )
296
-
297
- def _render_at(self) -> str:
298
- """Render the current page's accessibility tree to text."""
299
- if self._root is None:
300
- return "<empty>"
301
- return self._root.to_string()
302
-
303
- def go_to_page(self, url: str) -> None:
304
- """Goes to the given url.
305
-
306
- Args:
307
- url: The url to redirect crawler to.
308
- """
309
- if "://" not in url:
310
- url = f"https://{url}"
311
- self._page.goto(url, wait_until=WAIT_STRATEGY)
312
-
313
- def click(self, element_id: int | str) -> None:
314
- """Clicks the element with the given id.
315
-
316
- Args:
317
- element_id: The id for the element we want to click on.
318
- """
319
- element = self.lookup_node(element_id)
320
- # Mouse.click() requires coordinates relative to the viewport:
321
- # https://playwright.dev/python/docs/api/class-mouse#mouse-click,
322
- # thus adjusting the Y coordinate since we only scroll up/down.
323
- scroll_y = self._page.evaluate("window.scrollY")
324
- self._page.mouse.click(
325
- element.bounds.center_x, element.bounds.center_y - scroll_y
326
- )
327
-
328
- def clear(self, element_id: int) -> None:
329
- """Clears text within a field."""
330
- self.click(element_id)
331
- self._page.keyboard.press("Control+A")
332
- self._page.keyboard.press("Backspace")
333
-
334
- def type(self, element_id: int | str, text: str) -> None:
335
- """Types into the element with the given id."""
336
- self.click(element_id)
337
- self._page.keyboard.type(text)
338
-
339
- def scroll(self, direction: Literal["up", "down"]) -> None:
340
- """Scrolls the page to the given direction.
341
-
342
- Args:
343
- direction: The direction to scroll in ('up' or 'down')
344
- """
345
- match direction.lower():
346
- case "up":
347
- self._page.evaluate(
348
- "(document.scrollingElement || document.body).scrollTop ="
349
- " (document.scrollingElement || document.body).scrollTop -"
350
- " window.innerHeight;"
351
- )
352
- case "down":
353
- self._page.evaluate(
354
- "(document.scrollingElement || document.body).scrollTop ="
355
- " (document.scrollingElement || document.body).scrollTop +"
356
- " window.innerHeight;"
357
- )
358
-
359
- case _:
360
- raise ValueError(f"Invalid scroll direction {direction}")
361
-
362
- def forward(self) -> None:
363
- """Move browser forward one history step."""
364
- self._page.go_forward(wait_until=WAIT_STRATEGY)
365
-
366
- def back(self) -> None:
367
- """Move browser backward one history step."""
368
- self._page.go_back(wait_until=WAIT_STRATEGY)
369
-
370
- def refresh(self) -> None:
371
- """Refresh (reload) the page."""
372
- self._page.reload(wait_until=WAIT_STRATEGY)
26
+ def __init__(
27
+ self,
28
+ browser_context: BrowserContext,
29
+ page_crawler: PageCrawler,
30
+ device_scale_factor: float | None,
31
+ ):
32
+ self._device_scale_factor = device_scale_factor
33
+ self._page_future = Future[PageCrawler]()
34
+ self._page_future.set_result(page_crawler)
35
+ browser_context.on("page", self._on_page)
373
36
 
374
37
  @property
375
- def url(self) -> str:
376
- return self._page.url
38
+ async def current_page(self) -> PageCrawler:
39
+ return await self._page_future
40
+
41
+ async def _on_page(self, new_page: Page):
42
+ # we know we're switching pages, but it will take time to get the new page crawler, so
43
+ # reset the future to force new callers to wait.
44
+ # TODO: A race remains in the case that we get multiple on_pages before the first one sets the result
45
+ self._page_future = Future()
46
+ self._page_future.set_result(
47
+ await PageCrawler.create(new_page, self._device_scale_factor)
48
+ )