inspect-ai 0.3.74__py3-none-any.whl → 0.3.76__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/__init__.py +3 -2
- inspect_ai/_cli/cache.py +1 -1
- inspect_ai/_cli/common.py +15 -0
- inspect_ai/_cli/eval.py +4 -5
- inspect_ai/_cli/log.py +1 -1
- inspect_ai/_cli/sandbox.py +1 -1
- inspect_ai/_cli/trace.py +1 -1
- inspect_ai/_cli/view.py +1 -1
- inspect_ai/_display/core/config.py +3 -1
- inspect_ai/_eval/eval.py +55 -61
- inspect_ai/_eval/evalset.py +64 -154
- inspect_ai/_eval/loader.py +27 -54
- inspect_ai/_eval/registry.py +4 -15
- inspect_ai/_eval/run.py +7 -4
- inspect_ai/_eval/task/__init__.py +8 -2
- inspect_ai/_eval/task/log.py +9 -1
- inspect_ai/_eval/task/resolved.py +35 -0
- inspect_ai/_eval/task/run.py +4 -0
- inspect_ai/_eval/task/task.py +50 -69
- inspect_ai/_eval/task/tasks.py +30 -0
- inspect_ai/_util/constants.py +3 -0
- inspect_ai/_util/dotenv.py +17 -0
- inspect_ai/_util/logger.py +3 -0
- inspect_ai/_util/registry.py +43 -2
- inspect_ai/_view/server.py +28 -10
- inspect_ai/_view/www/dist/assets/index.css +32 -19
- inspect_ai/_view/www/dist/assets/index.js +17682 -29989
- inspect_ai/_view/www/log-schema.json +79 -9
- inspect_ai/_view/www/package.json +2 -2
- inspect_ai/_view/www/src/appearance/styles.ts +6 -5
- inspect_ai/_view/www/src/components/AnsiDisplay.tsx +2 -2
- inspect_ai/_view/www/src/constants.ts +3 -0
- inspect_ai/_view/www/src/logfile/remoteZipFile.ts +141 -20
- inspect_ai/_view/www/src/plan/PlanDetailView.tsx +2 -1
- inspect_ai/_view/www/src/samples/SampleSummaryView.tsx +1 -1
- inspect_ai/_view/www/src/samples/chat/tools/tool.ts +7 -5
- inspect_ai/_view/www/src/samples/descriptor/score/CategoricalScoreDescriptor.tsx +1 -1
- inspect_ai/_view/www/src/samples/descriptor/score/NumericScoreDescriptor.tsx +2 -2
- inspect_ai/_view/www/src/samples/error/FlatSampleErrorView.module.css +1 -0
- inspect_ai/_view/www/src/samples/error/FlatSampleErrorView.tsx +3 -1
- inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +1 -1
- inspect_ai/_view/www/src/samples/sample-tools/sample-filter/SampleFilter.tsx +5 -2
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +2 -2
- inspect_ai/_view/www/src/samples/transcript/state/StateEventView.tsx +5 -1
- inspect_ai/_view/www/src/types/log.d.ts +11 -5
- inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.tsx +17 -12
- inspect_ai/_view/www/src/workspace/sidebar/SidebarLogEntry.tsx +2 -1
- inspect_ai/_view/www/yarn.lock +12 -5
- inspect_ai/log/_log.py +10 -1
- inspect_ai/log/_recorders/eval.py +27 -8
- inspect_ai/log/_recorders/json.py +10 -2
- inspect_ai/log/_transcript.py +13 -4
- inspect_ai/model/_call_tools.py +13 -4
- inspect_ai/model/_chat_message.py +15 -1
- inspect_ai/model/_model.py +30 -12
- inspect_ai/model/_model_output.py +6 -1
- inspect_ai/model/_openai.py +11 -6
- inspect_ai/model/_providers/anthropic.py +167 -77
- inspect_ai/model/_providers/google.py +6 -2
- inspect_ai/model/_providers/none.py +31 -0
- inspect_ai/model/_providers/openai.py +11 -8
- inspect_ai/model/_providers/providers.py +7 -0
- inspect_ai/model/_providers/vertex.py +5 -2
- inspect_ai/solver/_bridge/bridge.py +1 -1
- inspect_ai/solver/_chain.py +7 -6
- inspect_ai/tool/__init__.py +4 -0
- inspect_ai/tool/_tool_call.py +5 -2
- inspect_ai/tool/_tool_support_helpers.py +200 -0
- inspect_ai/tool/_tools/_bash_session.py +119 -0
- inspect_ai/tool/_tools/_computer/_computer.py +1 -1
- inspect_ai/tool/_tools/_text_editor.py +121 -0
- inspect_ai/tool/_tools/_web_browser/_back_compat.py +150 -0
- inspect_ai/tool/_tools/_web_browser/_web_browser.py +75 -130
- inspect_ai/tool/_tools/_web_search.py +2 -2
- inspect_ai/util/_json.py +28 -0
- inspect_ai/util/_sandbox/context.py +18 -8
- inspect_ai/util/_sandbox/docker/config.py +1 -1
- inspect_ai/util/_sandbox/docker/internal.py +3 -3
- inspect_ai/util/_sandbox/environment.py +17 -2
- {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info}/METADATA +8 -5
- {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info}/RECORD +85 -108
- {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info}/WHEEL +1 -1
- inspect_ai/tool/_tools/_web_browser/_resources/.pylintrc +0 -8
- inspect_ai/tool/_tools/_web_browser/_resources/.vscode/launch.json +0 -24
- inspect_ai/tool/_tools/_web_browser/_resources/.vscode/settings.json +0 -25
- inspect_ai/tool/_tools/_web_browser/_resources/Dockerfile +0 -22
- inspect_ai/tool/_tools/_web_browser/_resources/README.md +0 -63
- inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree.py +0 -71
- inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree_node.py +0 -323
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/__init__.py +0 -5
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/a11y.py +0 -279
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom.py +0 -9
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom_snapshot.py +0 -293
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/page.py +0 -94
- inspect_ai/tool/_tools/_web_browser/_resources/constants.py +0 -2
- inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.svg +0 -2
- inspect_ai/tool/_tools/_web_browser/_resources/mock_environment.py +0 -45
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_browser.py +0 -50
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_crawler.py +0 -48
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_page_crawler.py +0 -280
- inspect_ai/tool/_tools/_web_browser/_resources/pyproject.toml +0 -65
- inspect_ai/tool/_tools/_web_browser/_resources/rectangle.py +0 -64
- inspect_ai/tool/_tools/_web_browser/_resources/rpc_client_helpers.py +0 -146
- inspect_ai/tool/_tools/_web_browser/_resources/scale_factor.py +0 -64
- inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_tree_node.py +0 -180
- inspect_ai/tool/_tools/_web_browser/_resources/test_playwright_crawler.py +0 -99
- inspect_ai/tool/_tools/_web_browser/_resources/test_rectangle.py +0 -15
- inspect_ai/tool/_tools/_web_browser/_resources/test_web_client.py +0 -44
- inspect_ai/tool/_tools/_web_browser/_resources/web_browser_rpc_types.py +0 -39
- inspect_ai/tool/_tools/_web_browser/_resources/web_client.py +0 -214
- inspect_ai/tool/_tools/_web_browser/_resources/web_client_new_session.py +0 -35
- inspect_ai/tool/_tools/_web_browser/_resources/web_server.py +0 -192
- {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info/licenses}/LICENSE +0 -0
- {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info}/top_level.txt +0 -0
@@ -1,2 +0,0 @@
|
|
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>
|
@@ -1,45 +0,0 @@
|
|
1
|
-
"""A mock dm_env for unit testing."""
|
2
|
-
|
3
|
-
from typing import Any
|
4
|
-
|
5
|
-
import dm_env
|
6
|
-
from dm_env import specs
|
7
|
-
|
8
|
-
|
9
|
-
class MockEnvironment(dm_env.Environment):
|
10
|
-
"""A Mock DM environment."""
|
11
|
-
|
12
|
-
def __init__(self, context):
|
13
|
-
"""Initializes the environment."""
|
14
|
-
super().__init__()
|
15
|
-
self._last_command = ""
|
16
|
-
|
17
|
-
def reset(self) -> dm_env.TimeStep:
|
18
|
-
"""Starts a new sequence and returns the first `TimeStep` of this sequence."""
|
19
|
-
self._last_command = ""
|
20
|
-
return dm_env.restart(observation=self.get_observations())
|
21
|
-
|
22
|
-
def step(self, action: list[str]) -> dm_env.TimeStep:
|
23
|
-
"""Updates the environment according to the action and returns a `TimeStep`."""
|
24
|
-
self._last_command = " ".join(action)
|
25
|
-
return dm_env.transition(
|
26
|
-
reward=0.0,
|
27
|
-
observation=self.get_observations(),
|
28
|
-
)
|
29
|
-
|
30
|
-
def observation_spec(self) -> dict[str, specs.Array]:
|
31
|
-
"""Defines the observations provided by the environment."""
|
32
|
-
obs_shapes = {
|
33
|
-
"last_command": specs.Array(shape=(), dtype=str, name="last_command"),
|
34
|
-
}
|
35
|
-
return obs_shapes
|
36
|
-
|
37
|
-
def action_spec(self) -> specs.Array:
|
38
|
-
"""Defines the actions that should be provided to `step`."""
|
39
|
-
return specs.Array(shape=(), dtype=str, name="command")
|
40
|
-
|
41
|
-
def get_observations(self) -> dict[str, Any]:
|
42
|
-
"""Returns dictionary containing observations."""
|
43
|
-
return {
|
44
|
-
"last_command": self._last_command,
|
45
|
-
}
|
@@ -1,50 +0,0 @@
|
|
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
|
@@ -1,48 +0,0 @@
|
|
1
|
-
"""A crawler implementation using Playwright.
|
2
|
-
|
3
|
-
Largely based on https://github.com/web-arena-x/webarena
|
4
|
-
"""
|
5
|
-
|
6
|
-
from __future__ import annotations
|
7
|
-
|
8
|
-
from asyncio.futures import Future
|
9
|
-
|
10
|
-
from playwright.async_api import BrowserContext, Page
|
11
|
-
|
12
|
-
from playwright_page_crawler import PageCrawler
|
13
|
-
|
14
|
-
|
15
|
-
class PlaywrightCrawler:
|
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
|
22
|
-
)
|
23
|
-
|
24
|
-
return PlaywrightCrawler(browser_context, page_crawler, device_scale_factor)
|
25
|
-
|
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)
|
36
|
-
|
37
|
-
@property
|
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
|
-
)
|
@@ -1,280 +0,0 @@
|
|
1
|
-
"""A crawler implementation using Playwright.
|
2
|
-
|
3
|
-
Portions based on https://github.com/web-arena-x/webarena
|
4
|
-
"""
|
5
|
-
|
6
|
-
from __future__ import annotations
|
7
|
-
|
8
|
-
import asyncio
|
9
|
-
import re
|
10
|
-
from typing import Literal
|
11
|
-
|
12
|
-
from playwright.async_api import CDPSession, Page
|
13
|
-
|
14
|
-
from accessibility_tree import AccessibilityTree, create_accessibility_tree
|
15
|
-
from accessibility_tree_node import AccessibilityTreeNode
|
16
|
-
from cdp.a11y import AXNodeId, AXTree
|
17
|
-
from cdp.dom_snapshot import DOMSnapshot
|
18
|
-
from rectangle import Rectangle
|
19
|
-
|
20
|
-
# Number of seconds to wait for possible click induced navigation before proceeding
|
21
|
-
_WAIT_FOR_NAVIGATION_TIME = 2.0
|
22
|
-
|
23
|
-
# The waiting strategy to use between browser commands.
|
24
|
-
# see https://playwright.dev/docs/api/class-page.
|
25
|
-
_WAIT_STRATEGY: Literal["domcontentloaded"] = "domcontentloaded"
|
26
|
-
|
27
|
-
|
28
|
-
class PageCrawler:
|
29
|
-
@classmethod
|
30
|
-
async def create(
|
31
|
-
cls, page: Page, device_scale_factor: float | None = None
|
32
|
-
) -> PageCrawler:
|
33
|
-
# Enable chrome development tools, and accessibility tree output.
|
34
|
-
cdp_session = await page.context.new_cdp_session(page)
|
35
|
-
await cdp_session.send("Accessibility.enable")
|
36
|
-
return PageCrawler(
|
37
|
-
page,
|
38
|
-
cdp_session,
|
39
|
-
device_scale_factor or await page.evaluate("window.devicePixelRatio"),
|
40
|
-
)
|
41
|
-
|
42
|
-
def __init__(
|
43
|
-
self, page: Page, cdp_session: CDPSession, device_scale_factor: float
|
44
|
-
) -> None:
|
45
|
-
self._page = page
|
46
|
-
self._cdp_session = cdp_session
|
47
|
-
|
48
|
-
# Start with an empty accessibility tree
|
49
|
-
self._rendered_main_content: str | None = None
|
50
|
-
self._rendered_accessibility_tree: str = ""
|
51
|
-
self._accessibility_tree: AccessibilityTree | None = None
|
52
|
-
self._device_scale_factor = device_scale_factor
|
53
|
-
|
54
|
-
@property
|
55
|
-
def page(self) -> Page:
|
56
|
-
return self._page
|
57
|
-
|
58
|
-
@property
|
59
|
-
def url(self) -> str:
|
60
|
-
return self._page.url
|
61
|
-
|
62
|
-
def lookup_node(self, node_id_or_tag: int | str) -> AccessibilityTreeNode:
|
63
|
-
"""Looks up the node by id or tag.
|
64
|
-
|
65
|
-
Args:
|
66
|
-
node_id_or_tag: Either the id number (as int or str), or <tag_name>
|
67
|
-
|
68
|
-
Returns:
|
69
|
-
AccessibilityNode.
|
70
|
-
|
71
|
-
Raise:
|
72
|
-
LookupError if node is not matched.
|
73
|
-
"""
|
74
|
-
node: AccessibilityTreeNode | None = None
|
75
|
-
node_id_or_tag = str(node_id_or_tag)
|
76
|
-
nodes = self._accessibility_tree["nodes"] if self._accessibility_tree else {}
|
77
|
-
if re.match("^<.*>", node_id_or_tag):
|
78
|
-
tag = node_id_or_tag[1:-1].lower()
|
79
|
-
# This is a smart tag, try to resolve it.
|
80
|
-
if node := next(
|
81
|
-
# We match on anything that starts with the code, this is potentially
|
82
|
-
# a little brittle, can be replaced with an RE if there are issues.
|
83
|
-
(
|
84
|
-
n
|
85
|
-
for n in nodes.values()
|
86
|
-
if n.name.lower().startswith(tag) and not n.is_ignored
|
87
|
-
),
|
88
|
-
None,
|
89
|
-
):
|
90
|
-
return node
|
91
|
-
else:
|
92
|
-
raise LookupError(
|
93
|
-
f"Could not find tag {node_id_or_tag} from {[node.name for node in nodes.values() if node.name]}"
|
94
|
-
)
|
95
|
-
else:
|
96
|
-
if (
|
97
|
-
node := nodes.get(AXNodeId(node_id_or_tag), None)
|
98
|
-
) and not node.is_ignored:
|
99
|
-
return node
|
100
|
-
else:
|
101
|
-
raise LookupError(f"Could not find element with id {node_id_or_tag}")
|
102
|
-
|
103
|
-
async def update(self) -> None:
|
104
|
-
"""Updates the accessibility tree and DOM from current page."""
|
105
|
-
await self._page.wait_for_load_state(_WAIT_STRATEGY)
|
106
|
-
|
107
|
-
available_retries = 2
|
108
|
-
retry_delay = 0.25
|
109
|
-
while available_retries:
|
110
|
-
self._accessibility_tree = create_accessibility_tree(
|
111
|
-
ax_nodes=AXTree(
|
112
|
-
**await self._cdp_session.send("Accessibility.getFullAXTree", {})
|
113
|
-
).nodes,
|
114
|
-
dom_snapshot=DOMSnapshot(
|
115
|
-
**await self._cdp_session.send(
|
116
|
-
"DOMSnapshot.captureSnapshot",
|
117
|
-
{
|
118
|
-
"computedStyles": [],
|
119
|
-
"includeDOMRects": True,
|
120
|
-
},
|
121
|
-
)
|
122
|
-
),
|
123
|
-
device_scale_factor=self._device_scale_factor,
|
124
|
-
window_bounds=Rectangle(
|
125
|
-
await self._page.evaluate("window.pageXOffset"),
|
126
|
-
await self._page.evaluate("window.pageYOffset"),
|
127
|
-
await self._page.evaluate("window.screen.width"),
|
128
|
-
await self._page.evaluate("window.screen.height"),
|
129
|
-
),
|
130
|
-
)
|
131
|
-
|
132
|
-
self._rendered_main_content, self._rendered_accessibility_tree = (
|
133
|
-
(
|
134
|
-
self._accessibility_tree["root"].render_main_content(),
|
135
|
-
self._accessibility_tree["root"].render_accessibility_tree(),
|
136
|
-
)
|
137
|
-
if self._accessibility_tree
|
138
|
-
else (None, "")
|
139
|
-
)
|
140
|
-
|
141
|
-
if self._rendered_accessibility_tree:
|
142
|
-
return
|
143
|
-
# sometimes, the entire tree is initially ignored. in such cases, it's typically
|
144
|
-
# because we're sampling too soon. Waiting a small amount of time and trying again
|
145
|
-
# resolves the issue.
|
146
|
-
available_retries = available_retries - 1
|
147
|
-
await asyncio.sleep(retry_delay)
|
148
|
-
|
149
|
-
def render_at(self) -> str:
|
150
|
-
"""Returns the current webpage accessibility tree.
|
151
|
-
|
152
|
-
Only elements visible on the screen will be rendered.
|
153
|
-
"""
|
154
|
-
return self._rendered_accessibility_tree
|
155
|
-
|
156
|
-
def render_main_content(self) -> str | None:
|
157
|
-
return self._rendered_main_content
|
158
|
-
|
159
|
-
async def go_to_url(self, url: str) -> None:
|
160
|
-
"""Goes to the given url.
|
161
|
-
|
162
|
-
Args:
|
163
|
-
url: The url to redirect crawler to.
|
164
|
-
"""
|
165
|
-
if "://" not in url:
|
166
|
-
url = f"https://{url}"
|
167
|
-
try:
|
168
|
-
await self._page.goto(url, wait_until=_WAIT_STRATEGY)
|
169
|
-
except Exception as e:
|
170
|
-
print(f"caught {e}")
|
171
|
-
raise
|
172
|
-
|
173
|
-
async def click(self, element_id: int | str) -> None:
|
174
|
-
"""Clicks the element with the given id.
|
175
|
-
|
176
|
-
Args:
|
177
|
-
element_id: The id for the element we want to click on.
|
178
|
-
"""
|
179
|
-
element = self.lookup_node(element_id)
|
180
|
-
if element.bounds is None:
|
181
|
-
raise LookupError(f"Element with id {element_id} has no layout info.")
|
182
|
-
|
183
|
-
# Mouse.click() requires coordinates relative to the viewport:
|
184
|
-
# https://playwright.dev/python/docs/api/class-mouse#mouse-click,
|
185
|
-
# thus adjusting the Y coordinate since we only scroll up/down.
|
186
|
-
scroll_y = await self._page.evaluate("window.scrollY")
|
187
|
-
await self._click_and_await_navigation(
|
188
|
-
element.bounds.center_x, element.bounds.center_y - scroll_y
|
189
|
-
)
|
190
|
-
|
191
|
-
async def clear(self, element_id: int | str) -> None:
|
192
|
-
"""Clears text within a field."""
|
193
|
-
await self.click(element_id)
|
194
|
-
await self._page.keyboard.press("Control+A")
|
195
|
-
await self._page.keyboard.press("Backspace")
|
196
|
-
|
197
|
-
async def type(self, element_id: int | str, text: str) -> None:
|
198
|
-
"""Types into the element with the given id."""
|
199
|
-
await self.click(element_id)
|
200
|
-
await self._page.keyboard.type(text)
|
201
|
-
|
202
|
-
async def scroll(self, direction: Literal["up", "down"]) -> None:
|
203
|
-
"""Scrolls the page to the given direction.
|
204
|
-
|
205
|
-
Args:
|
206
|
-
direction: The direction to scroll in ('up' or 'down')
|
207
|
-
"""
|
208
|
-
match direction.lower():
|
209
|
-
case "up":
|
210
|
-
await self._page.evaluate(
|
211
|
-
"(document.scrollingElement || document.body).scrollTop ="
|
212
|
-
" (document.scrollingElement || document.body).scrollTop -"
|
213
|
-
" window.innerHeight;"
|
214
|
-
)
|
215
|
-
case "down":
|
216
|
-
await self._page.evaluate(
|
217
|
-
"(document.scrollingElement || document.body).scrollTop ="
|
218
|
-
" (document.scrollingElement || document.body).scrollTop +"
|
219
|
-
" window.innerHeight;"
|
220
|
-
)
|
221
|
-
|
222
|
-
case _:
|
223
|
-
raise ValueError(f"Invalid scroll direction {direction}")
|
224
|
-
|
225
|
-
async def forward(self) -> None:
|
226
|
-
"""Move browser forward one history step."""
|
227
|
-
await self._page.go_forward(wait_until=_WAIT_STRATEGY)
|
228
|
-
|
229
|
-
async def back(self) -> None:
|
230
|
-
"""Move browser backward one history step."""
|
231
|
-
await self._page.go_back(wait_until=_WAIT_STRATEGY)
|
232
|
-
|
233
|
-
async def refresh(self) -> None:
|
234
|
-
"""Refresh (reload) the page."""
|
235
|
-
await self._page.reload(wait_until=_WAIT_STRATEGY)
|
236
|
-
|
237
|
-
async def _click_and_await_navigation(self, x: float, y: float) -> None:
|
238
|
-
"""
|
239
|
-
Clicks on the specified coordinates and waits for navigation (if any) to occur.
|
240
|
-
|
241
|
-
This function sets up event listeners to detect in-page navigation or new page
|
242
|
-
navigation, performs a mouse click at the given coordinates, and waits for the
|
243
|
-
navigation to complete within the specified timeout period.
|
244
|
-
|
245
|
-
The point of this is to allow enough time to switch our page in the event of a new
|
246
|
-
page being opened. The problem is that it takes some amount of time, and the challenge
|
247
|
-
is determining how long to wait.
|
248
|
-
|
249
|
-
A naïve approach would simply sleep for some amount of time. However, this time may
|
250
|
-
not be long enough AND it would delay the common case by that delay waiting for a new
|
251
|
-
page navigation that never comes.
|
252
|
-
|
253
|
-
This approach accomplishes waiting the minimal amount of time in the common cases of
|
254
|
-
a click inducing an in page or new page navigation. The downside is that clicks that
|
255
|
-
do not induce navigation are delayed by the timeout. Since navigating clicks are much
|
256
|
-
more common, this is a reasonable approach.
|
257
|
-
"""
|
258
|
-
future = asyncio.Future[None]()
|
259
|
-
|
260
|
-
async def on_in_page_navigation(_frame):
|
261
|
-
if not future.done():
|
262
|
-
await self._page.wait_for_load_state(_WAIT_STRATEGY)
|
263
|
-
future.set_result()
|
264
|
-
|
265
|
-
async def on_new_page(new_page):
|
266
|
-
if not future.done():
|
267
|
-
await new_page.wait_for_load_state(_WAIT_STRATEGY)
|
268
|
-
future.set_result(None)
|
269
|
-
|
270
|
-
self._page.once("framenavigated", on_in_page_navigation)
|
271
|
-
self._page.context.once("page", on_new_page)
|
272
|
-
|
273
|
-
await self._page.mouse.click(x, y)
|
274
|
-
|
275
|
-
try:
|
276
|
-
await asyncio.wait_for(future, timeout=_WAIT_FOR_NAVIGATION_TIME)
|
277
|
-
# a navigation of some sort has occurred and gotten to domcontentloaded
|
278
|
-
except (asyncio.TimeoutError, TimeoutError):
|
279
|
-
# No navigation occurred within the timeout period
|
280
|
-
pass
|
@@ -1,65 +0,0 @@
|
|
1
|
-
[build-system]
|
2
|
-
requires = ["setuptools>=64", "setuptools_scm[toml]>=8"]
|
3
|
-
build-backend = "setuptools.build_meta"
|
4
|
-
|
5
|
-
[tool.setuptools_scm]
|
6
|
-
|
7
|
-
[tool.setuptools.packages.find]
|
8
|
-
where = ["."]
|
9
|
-
include = ["inspect_ai*"]
|
10
|
-
|
11
|
-
[tool.ruff]
|
12
|
-
src = ["."]
|
13
|
-
|
14
|
-
[tool.ruff.lint]
|
15
|
-
select = [
|
16
|
-
"E", # pycodestyle errors
|
17
|
-
"W", # pycodestyle warnings
|
18
|
-
"F", # flake8
|
19
|
-
"D", # pydocstyle
|
20
|
-
"I", # isort
|
21
|
-
"SIM101", # duplicate isinstance
|
22
|
-
"UP038", # non-pep604-isinstance
|
23
|
-
# "RET", # flake8-return
|
24
|
-
# "RUF", # ruff rules
|
25
|
-
]
|
26
|
-
ignore = ["E203", "E501", "D10", "D212", "D415"]
|
27
|
-
|
28
|
-
[tool.ruff.lint.pydocstyle]
|
29
|
-
convention = "google"
|
30
|
-
|
31
|
-
[tool.pytest.ini_options]
|
32
|
-
minversion = "7.0"
|
33
|
-
addopts = "-rA --doctest-modules --color=yes"
|
34
|
-
doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL"]
|
35
|
-
asyncio_mode = "auto"
|
36
|
-
asyncio_default_fixture_loop_scope = "function"
|
37
|
-
log_level = "warning"
|
38
|
-
|
39
|
-
[tool.mypy]
|
40
|
-
warn_unused_ignores = true
|
41
|
-
no_implicit_reexport = true
|
42
|
-
strict_equality = true
|
43
|
-
warn_redundant_casts = true
|
44
|
-
warn_unused_configs = true
|
45
|
-
disallow_any_explicit = true
|
46
|
-
disallow_any_generics = true
|
47
|
-
disallow_subclassing_any = true
|
48
|
-
plugins=["pydantic.mypy"]
|
49
|
-
|
50
|
-
|
51
|
-
[tool.pydantic-mypy]
|
52
|
-
init_forbid_extra = true
|
53
|
-
init_typed = true
|
54
|
-
|
55
|
-
[tool.check-wheel-contents]
|
56
|
-
ignore = ["W002", "W009"]
|
57
|
-
|
58
|
-
[project]
|
59
|
-
name = "web_browser_tool_container"
|
60
|
-
requires-python = ">=3.10"
|
61
|
-
dynamic = ["version", "dependencies"]
|
62
|
-
|
63
|
-
|
64
|
-
[project.optional-dependencies]
|
65
|
-
dev = ["pytest"]
|
@@ -1,64 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
|
4
|
-
class Rectangle:
|
5
|
-
def __init__(self, left: float, top: float, width: float, height: float):
|
6
|
-
self._left = int(left)
|
7
|
-
self._width = int(width)
|
8
|
-
self._top = int(top)
|
9
|
-
self._height = int(height)
|
10
|
-
|
11
|
-
@classmethod
|
12
|
-
def _from_left_right_top_bottom(
|
13
|
-
cls, left: float, right: float, top: float, bottom: float
|
14
|
-
) -> Rectangle:
|
15
|
-
return cls(left, top, right - left, bottom - top)
|
16
|
-
|
17
|
-
@property
|
18
|
-
def _right(self) -> int:
|
19
|
-
return self._left + self._width
|
20
|
-
|
21
|
-
@property
|
22
|
-
def _bottom(self) -> int:
|
23
|
-
return self._top + self._height
|
24
|
-
|
25
|
-
@property
|
26
|
-
def center_x(self) -> int:
|
27
|
-
return self._left + self._width // 2
|
28
|
-
|
29
|
-
@property
|
30
|
-
def center_y(self) -> int:
|
31
|
-
return self._top + self._height // 2
|
32
|
-
|
33
|
-
@property
|
34
|
-
def has_area(self) -> bool:
|
35
|
-
return self._width > 0 and self._height > 0
|
36
|
-
|
37
|
-
def __str__(self) -> str:
|
38
|
-
return f"({self._left}, {self._top}, {self._width}, {self._height})"
|
39
|
-
|
40
|
-
def scale(self, scale: float) -> Rectangle:
|
41
|
-
return self._from_left_right_top_bottom(
|
42
|
-
self._left * scale,
|
43
|
-
self._right * scale,
|
44
|
-
self._top * scale,
|
45
|
-
self._bottom * scale,
|
46
|
-
)
|
47
|
-
|
48
|
-
def overlaps(self, other: Rectangle) -> bool:
|
49
|
-
"""Returns if the two rectangles intersect."""
|
50
|
-
return (
|
51
|
-
other._left < self._right # pylint: disable=protected-access
|
52
|
-
and other._right > self._left # pylint: disable=protected-access
|
53
|
-
and other._top < self._bottom # pylint: disable=protected-access
|
54
|
-
and other._bottom > self._top # pylint: disable=protected-access
|
55
|
-
)
|
56
|
-
|
57
|
-
def within(self, other: Rectangle) -> bool:
|
58
|
-
"""Returns if this rectangle is within the other rectangle."""
|
59
|
-
return (
|
60
|
-
other._left <= self._left # pylint: disable=protected-access
|
61
|
-
and other._right >= self._left # pylint: disable=protected-access
|
62
|
-
and other._top <= self._bottom # pylint: disable=protected-access
|
63
|
-
and other._bottom >= self._top # pylint: disable=protected-access
|
64
|
-
)
|