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.
- inspect_ai/_cli/eval.py +14 -8
- inspect_ai/_display/core/display.py +2 -0
- inspect_ai/_display/core/footer.py +13 -3
- inspect_ai/_display/plain/display.py +6 -2
- inspect_ai/_display/rich/display.py +19 -6
- inspect_ai/_display/textual/app.py +6 -1
- inspect_ai/_display/textual/display.py +4 -0
- inspect_ai/_display/textual/widgets/transcript.py +10 -6
- inspect_ai/_eval/task/run.py +5 -8
- inspect_ai/_util/content.py +20 -1
- inspect_ai/_util/transcript.py +10 -4
- inspect_ai/_util/working.py +4 -0
- inspect_ai/_view/www/App.css +6 -0
- inspect_ai/_view/www/dist/assets/index.css +115 -87
- inspect_ai/_view/www/dist/assets/index.js +5324 -2276
- inspect_ai/_view/www/eslint.config.mjs +24 -1
- inspect_ai/_view/www/log-schema.json +283 -20
- inspect_ai/_view/www/package.json +8 -3
- inspect_ai/_view/www/src/App.tsx +2 -2
- inspect_ai/_view/www/src/components/AnsiDisplay.tsx +4 -3
- inspect_ai/_view/www/src/components/Card.tsx +9 -8
- inspect_ai/_view/www/src/components/DownloadButton.tsx +2 -1
- inspect_ai/_view/www/src/components/EmptyPanel.tsx +2 -2
- inspect_ai/_view/www/src/components/ErrorPanel.tsx +4 -3
- inspect_ai/_view/www/src/components/ExpandablePanel.tsx +13 -5
- inspect_ai/_view/www/src/components/FindBand.tsx +3 -3
- inspect_ai/_view/www/src/components/HumanBaselineView.tsx +3 -3
- inspect_ai/_view/www/src/components/LabeledValue.tsx +5 -4
- inspect_ai/_view/www/src/components/LargeModal.tsx +18 -13
- inspect_ai/_view/www/src/components/{LightboxCarousel.css → LightboxCarousel.module.css} +22 -18
- inspect_ai/_view/www/src/components/LightboxCarousel.tsx +36 -27
- inspect_ai/_view/www/src/components/MessageBand.tsx +2 -1
- inspect_ai/_view/www/src/components/NavPills.tsx +9 -8
- inspect_ai/_view/www/src/components/ProgressBar.tsx +2 -1
- inspect_ai/_view/www/src/components/TabSet.tsx +21 -15
- inspect_ai/_view/www/src/index.tsx +2 -2
- inspect_ai/_view/www/src/metadata/MetaDataGrid.tsx +11 -9
- inspect_ai/_view/www/src/metadata/MetaDataView.tsx +3 -2
- inspect_ai/_view/www/src/metadata/MetadataGrid.module.css +1 -0
- inspect_ai/_view/www/src/metadata/RenderedContent.tsx +16 -0
- inspect_ai/_view/www/src/plan/DatasetDetailView.tsx +3 -2
- inspect_ai/_view/www/src/plan/DetailStep.tsx +2 -1
- inspect_ai/_view/www/src/plan/PlanCard.tsx +2 -5
- inspect_ai/_view/www/src/plan/PlanDetailView.tsx +6 -9
- inspect_ai/_view/www/src/plan/ScorerDetailView.tsx +2 -1
- inspect_ai/_view/www/src/plan/SolverDetailView.tsx +3 -3
- inspect_ai/_view/www/src/samples/InlineSampleDisplay.tsx +2 -2
- inspect_ai/_view/www/src/samples/SampleDialog.tsx +3 -3
- inspect_ai/_view/www/src/samples/SampleDisplay.tsx +2 -2
- inspect_ai/_view/www/src/samples/SampleSummaryView.tsx +2 -2
- inspect_ai/_view/www/src/samples/SamplesTools.tsx +2 -1
- inspect_ai/_view/www/src/samples/chat/ChatMessage.tsx +3 -19
- inspect_ai/_view/www/src/samples/chat/ChatMessageRenderer.tsx +2 -1
- inspect_ai/_view/www/src/samples/chat/ChatMessageRow.tsx +2 -1
- inspect_ai/_view/www/src/samples/chat/ChatView.tsx +2 -1
- inspect_ai/_view/www/src/samples/chat/ChatViewVirtualList.tsx +22 -7
- inspect_ai/_view/www/src/samples/chat/MessageContent.tsx +35 -6
- inspect_ai/_view/www/src/samples/chat/MessageContents.tsx +2 -2
- inspect_ai/_view/www/src/samples/chat/messages.ts +15 -2
- inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.tsx +13 -4
- inspect_ai/_view/www/src/samples/chat/tools/ToolInput.module.css +2 -2
- inspect_ai/_view/www/src/samples/chat/tools/ToolInput.tsx +18 -19
- inspect_ai/_view/www/src/samples/chat/tools/ToolOutput.module.css +1 -1
- inspect_ai/_view/www/src/samples/chat/tools/ToolOutput.tsx +4 -3
- inspect_ai/_view/www/src/samples/chat/tools/ToolTitle.tsx +2 -2
- inspect_ai/_view/www/src/samples/error/FlatSampleErrorView.tsx +2 -3
- inspect_ai/_view/www/src/samples/error/SampleErrorView.tsx +3 -2
- inspect_ai/_view/www/src/samples/list/SampleFooter.tsx +2 -1
- inspect_ai/_view/www/src/samples/list/SampleHeader.tsx +2 -1
- inspect_ai/_view/www/src/samples/list/SampleList.tsx +57 -45
- inspect_ai/_view/www/src/samples/list/SampleRow.tsx +2 -1
- inspect_ai/_view/www/src/samples/list/SampleSeparator.tsx +2 -1
- inspect_ai/_view/www/src/samples/sample-tools/EpochFilter.tsx +2 -2
- inspect_ai/_view/www/src/samples/sample-tools/SelectScorer.tsx +4 -3
- inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +2 -5
- inspect_ai/_view/www/src/samples/sample-tools/sample-filter/SampleFilter.tsx +2 -2
- inspect_ai/_view/www/src/samples/scores/SampleScoreView.tsx +2 -1
- inspect_ai/_view/www/src/samples/scores/SampleScores.tsx +2 -2
- inspect_ai/_view/www/src/samples/transcript/ApprovalEventView.tsx +2 -1
- inspect_ai/_view/www/src/samples/transcript/ErrorEventView.tsx +2 -1
- inspect_ai/_view/www/src/samples/transcript/InfoEventView.tsx +2 -1
- inspect_ai/_view/www/src/samples/transcript/InputEventView.tsx +2 -1
- inspect_ai/_view/www/src/samples/transcript/LoggerEventView.module.css +4 -0
- inspect_ai/_view/www/src/samples/transcript/LoggerEventView.tsx +12 -2
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +1 -1
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.tsx +25 -28
- inspect_ai/_view/www/src/samples/transcript/SampleInitEventView.tsx +2 -1
- inspect_ai/_view/www/src/samples/transcript/SampleLimitEventView.tsx +5 -4
- inspect_ai/_view/www/src/samples/transcript/SampleTranscript.tsx +2 -2
- inspect_ai/_view/www/src/samples/transcript/SandboxEventView.tsx +8 -7
- inspect_ai/_view/www/src/samples/transcript/ScoreEventView.tsx +2 -2
- inspect_ai/_view/www/src/samples/transcript/StepEventView.tsx +3 -3
- inspect_ai/_view/www/src/samples/transcript/SubtaskEventView.tsx +18 -14
- inspect_ai/_view/www/src/samples/transcript/ToolEventView.tsx +5 -5
- inspect_ai/_view/www/src/samples/transcript/TranscriptView.tsx +34 -15
- inspect_ai/_view/www/src/samples/transcript/event/EventNav.tsx +2 -1
- inspect_ai/_view/www/src/samples/transcript/event/EventNavs.tsx +2 -1
- inspect_ai/_view/www/src/samples/transcript/event/EventRow.tsx +3 -2
- inspect_ai/_view/www/src/samples/transcript/event/EventSection.tsx +2 -2
- inspect_ai/_view/www/src/samples/transcript/event/EventTimingPanel.module.css +28 -0
- inspect_ai/_view/www/src/samples/transcript/event/EventTimingPanel.tsx +115 -0
- inspect_ai/_view/www/src/samples/transcript/event/utils.ts +29 -0
- inspect_ai/_view/www/src/samples/transcript/state/StateDiffView.tsx +2 -1
- inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.tsx +3 -3
- inspect_ai/_view/www/src/samples/transcript/state/StateEventView.tsx +11 -8
- inspect_ai/_view/www/src/types/log.d.ts +129 -34
- inspect_ai/_view/www/src/usage/ModelTokenTable.tsx +6 -10
- inspect_ai/_view/www/src/usage/ModelUsagePanel.module.css +4 -0
- inspect_ai/_view/www/src/usage/ModelUsagePanel.tsx +32 -9
- inspect_ai/_view/www/src/usage/TokenTable.tsx +4 -6
- inspect_ai/_view/www/src/usage/UsageCard.tsx +2 -1
- inspect_ai/_view/www/src/utils/format.ts +1 -1
- inspect_ai/_view/www/src/utils/json.ts +24 -0
- inspect_ai/_view/www/src/workspace/WorkSpace.tsx +6 -5
- inspect_ai/_view/www/src/workspace/WorkSpaceView.tsx +9 -2
- inspect_ai/_view/www/src/workspace/error/TaskErrorPanel.tsx +2 -1
- inspect_ai/_view/www/src/workspace/navbar/Navbar.tsx +2 -1
- inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.tsx +3 -3
- inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.tsx +4 -3
- inspect_ai/_view/www/src/workspace/navbar/SecondaryBar.tsx +5 -4
- inspect_ai/_view/www/src/workspace/navbar/StatusPanel.tsx +5 -8
- inspect_ai/_view/www/src/workspace/sidebar/EvalStatus.tsx +5 -4
- inspect_ai/_view/www/src/workspace/sidebar/LogDirectoryTitleView.tsx +2 -1
- inspect_ai/_view/www/src/workspace/sidebar/Sidebar.tsx +2 -1
- inspect_ai/_view/www/src/workspace/sidebar/SidebarLogEntry.tsx +2 -2
- inspect_ai/_view/www/src/workspace/sidebar/SidebarScoreView.tsx +2 -1
- inspect_ai/_view/www/src/workspace/sidebar/SidebarScoresView.tsx +2 -2
- inspect_ai/_view/www/src/workspace/tabs/InfoTab.tsx +2 -2
- inspect_ai/_view/www/src/workspace/tabs/JsonTab.tsx +2 -5
- inspect_ai/_view/www/src/workspace/tabs/SamplesTab.tsx +12 -11
- inspect_ai/_view/www/yarn.lock +241 -5
- inspect_ai/log/_condense.py +3 -0
- inspect_ai/log/_recorders/eval.py +6 -1
- inspect_ai/log/_transcript.py +58 -1
- inspect_ai/model/__init__.py +2 -0
- inspect_ai/model/_call_tools.py +7 -0
- inspect_ai/model/_chat_message.py +22 -7
- inspect_ai/model/_conversation.py +10 -8
- inspect_ai/model/_generate_config.py +25 -4
- inspect_ai/model/_model.py +133 -57
- inspect_ai/model/_model_output.py +3 -0
- inspect_ai/model/_openai.py +106 -40
- inspect_ai/model/_providers/anthropic.py +281 -153
- inspect_ai/model/_providers/google.py +27 -8
- inspect_ai/model/_providers/groq.py +9 -4
- inspect_ai/model/_providers/openai.py +57 -4
- inspect_ai/model/_providers/openai_o1.py +10 -0
- inspect_ai/model/_providers/providers.py +1 -1
- inspect_ai/model/_reasoning.py +15 -2
- inspect_ai/scorer/_model.py +23 -19
- inspect_ai/solver/_human_agent/agent.py +14 -10
- inspect_ai/solver/_human_agent/commands/__init__.py +7 -3
- inspect_ai/solver/_human_agent/commands/submit.py +76 -30
- inspect_ai/tool/__init__.py +2 -0
- inspect_ai/tool/_tool.py +3 -1
- inspect_ai/tool/_tools/_computer/_common.py +117 -58
- inspect_ai/tool/_tools/_computer/_computer.py +80 -57
- inspect_ai/tool/_tools/_computer/_resources/image_home_dir/.config/Code/User/settings.json +7 -1
- inspect_ai/tool/_tools/_computer/_resources/image_home_dir/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml +91 -0
- inspect_ai/tool/_tools/_computer/_resources/tool/.pylintrc +8 -0
- inspect_ai/tool/_tools/_computer/_resources/tool/.vscode/settings.json +12 -0
- inspect_ai/tool/_tools/_computer/_resources/tool/_args.py +78 -0
- inspect_ai/tool/_tools/_computer/_resources/tool/_constants.py +20 -0
- inspect_ai/tool/_tools/_computer/_resources/tool/_run.py +1 -1
- inspect_ai/tool/_tools/_computer/_resources/tool/_x11_client.py +175 -113
- inspect_ai/tool/_tools/_computer/_resources/tool/computer_tool.py +76 -20
- inspect_ai/tool/_tools/_computer/_resources/tool/pyproject.toml +65 -0
- inspect_ai/tool/_tools/_computer/test_args.py +151 -0
- inspect_ai/tool/_tools/_web_browser/_resources/.pylintrc +8 -0
- inspect_ai/tool/_tools/_web_browser/_resources/.vscode/launch.json +24 -0
- inspect_ai/tool/_tools/_web_browser/_resources/.vscode/settings.json +25 -0
- inspect_ai/tool/_tools/_web_browser/_resources/Dockerfile +5 -6
- inspect_ai/tool/_tools/_web_browser/_resources/README.md +10 -11
- inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree.py +71 -0
- inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree_node.py +323 -0
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/__init__.py +5 -0
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/a11y.py +279 -0
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom.py +9 -0
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom_snapshot.py +293 -0
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/page.py +94 -0
- inspect_ai/tool/_tools/_web_browser/_resources/constants.py +2 -0
- inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.svg +2 -0
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_browser.py +50 -0
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_crawler.py +31 -359
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_page_crawler.py +280 -0
- inspect_ai/tool/_tools/_web_browser/_resources/pyproject.toml +65 -0
- inspect_ai/tool/_tools/_web_browser/_resources/rectangle.py +64 -0
- inspect_ai/tool/_tools/_web_browser/_resources/rpc_client_helpers.py +146 -0
- inspect_ai/tool/_tools/_web_browser/_resources/scale_factor.py +64 -0
- inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_tree_node.py +180 -0
- inspect_ai/tool/_tools/_web_browser/_resources/test_playwright_crawler.py +15 -9
- inspect_ai/tool/_tools/_web_browser/_resources/test_rectangle.py +15 -0
- inspect_ai/tool/_tools/_web_browser/_resources/test_web_client.py +44 -0
- inspect_ai/tool/_tools/_web_browser/_resources/web_browser_rpc_types.py +39 -0
- inspect_ai/tool/_tools/_web_browser/_resources/web_client.py +198 -48
- inspect_ai/tool/_tools/_web_browser/_resources/web_client_new_session.py +26 -25
- inspect_ai/tool/_tools/_web_browser/_resources/web_server.py +178 -39
- inspect_ai/tool/_tools/_web_browser/_web_browser.py +38 -19
- inspect_ai/util/__init__.py +2 -1
- inspect_ai/util/_display.py +12 -0
- inspect_ai/util/_sandbox/events.py +55 -21
- inspect_ai/util/_sandbox/self_check.py +131 -43
- inspect_ai/util/_subtask.py +11 -0
- {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/METADATA +1 -1
- {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/RECORD +209 -186
- {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/WHEEL +1 -1
- inspect_ai/_view/www/src/components/VirtualList.module.css +0 -19
- inspect_ai/_view/www/src/components/VirtualList.tsx +0 -292
- inspect_ai/tool/_tools/_computer/_computer_split.py +0 -198
- inspect_ai/tool/_tools/_web_browser/_resources/accessibility_node.py +0 -312
- inspect_ai/tool/_tools/_web_browser/_resources/dm_env_servicer.py +0 -275
- inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.png +0 -0
- inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_node.py +0 -176
- inspect_ai/tool/_tools/_web_browser/_resources/test_dm_env_servicer.py +0 -135
- inspect_ai/tool/_tools/_web_browser/_resources/test_web_environment.py +0 -71
- inspect_ai/tool/_tools/_web_browser/_resources/web_environment.py +0 -184
- {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/LICENSE +0 -0
- {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
16
|
-
from playwright.sync_api import sync_playwright
|
10
|
+
from playwright.async_api import BrowserContext, Page
|
17
11
|
|
18
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
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
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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
|
376
|
-
return self.
|
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
|
+
)
|