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.
Files changed (115) hide show
  1. inspect_ai/__init__.py +3 -2
  2. inspect_ai/_cli/cache.py +1 -1
  3. inspect_ai/_cli/common.py +15 -0
  4. inspect_ai/_cli/eval.py +4 -5
  5. inspect_ai/_cli/log.py +1 -1
  6. inspect_ai/_cli/sandbox.py +1 -1
  7. inspect_ai/_cli/trace.py +1 -1
  8. inspect_ai/_cli/view.py +1 -1
  9. inspect_ai/_display/core/config.py +3 -1
  10. inspect_ai/_eval/eval.py +55 -61
  11. inspect_ai/_eval/evalset.py +64 -154
  12. inspect_ai/_eval/loader.py +27 -54
  13. inspect_ai/_eval/registry.py +4 -15
  14. inspect_ai/_eval/run.py +7 -4
  15. inspect_ai/_eval/task/__init__.py +8 -2
  16. inspect_ai/_eval/task/log.py +9 -1
  17. inspect_ai/_eval/task/resolved.py +35 -0
  18. inspect_ai/_eval/task/run.py +4 -0
  19. inspect_ai/_eval/task/task.py +50 -69
  20. inspect_ai/_eval/task/tasks.py +30 -0
  21. inspect_ai/_util/constants.py +3 -0
  22. inspect_ai/_util/dotenv.py +17 -0
  23. inspect_ai/_util/logger.py +3 -0
  24. inspect_ai/_util/registry.py +43 -2
  25. inspect_ai/_view/server.py +28 -10
  26. inspect_ai/_view/www/dist/assets/index.css +32 -19
  27. inspect_ai/_view/www/dist/assets/index.js +17682 -29989
  28. inspect_ai/_view/www/log-schema.json +79 -9
  29. inspect_ai/_view/www/package.json +2 -2
  30. inspect_ai/_view/www/src/appearance/styles.ts +6 -5
  31. inspect_ai/_view/www/src/components/AnsiDisplay.tsx +2 -2
  32. inspect_ai/_view/www/src/constants.ts +3 -0
  33. inspect_ai/_view/www/src/logfile/remoteZipFile.ts +141 -20
  34. inspect_ai/_view/www/src/plan/PlanDetailView.tsx +2 -1
  35. inspect_ai/_view/www/src/samples/SampleSummaryView.tsx +1 -1
  36. inspect_ai/_view/www/src/samples/chat/tools/tool.ts +7 -5
  37. inspect_ai/_view/www/src/samples/descriptor/score/CategoricalScoreDescriptor.tsx +1 -1
  38. inspect_ai/_view/www/src/samples/descriptor/score/NumericScoreDescriptor.tsx +2 -2
  39. inspect_ai/_view/www/src/samples/error/FlatSampleErrorView.module.css +1 -0
  40. inspect_ai/_view/www/src/samples/error/FlatSampleErrorView.tsx +3 -1
  41. inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +1 -1
  42. inspect_ai/_view/www/src/samples/sample-tools/sample-filter/SampleFilter.tsx +5 -2
  43. inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +2 -2
  44. inspect_ai/_view/www/src/samples/transcript/state/StateEventView.tsx +5 -1
  45. inspect_ai/_view/www/src/types/log.d.ts +11 -5
  46. inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.tsx +17 -12
  47. inspect_ai/_view/www/src/workspace/sidebar/SidebarLogEntry.tsx +2 -1
  48. inspect_ai/_view/www/yarn.lock +12 -5
  49. inspect_ai/log/_log.py +10 -1
  50. inspect_ai/log/_recorders/eval.py +27 -8
  51. inspect_ai/log/_recorders/json.py +10 -2
  52. inspect_ai/log/_transcript.py +13 -4
  53. inspect_ai/model/_call_tools.py +13 -4
  54. inspect_ai/model/_chat_message.py +15 -1
  55. inspect_ai/model/_model.py +30 -12
  56. inspect_ai/model/_model_output.py +6 -1
  57. inspect_ai/model/_openai.py +11 -6
  58. inspect_ai/model/_providers/anthropic.py +167 -77
  59. inspect_ai/model/_providers/google.py +6 -2
  60. inspect_ai/model/_providers/none.py +31 -0
  61. inspect_ai/model/_providers/openai.py +11 -8
  62. inspect_ai/model/_providers/providers.py +7 -0
  63. inspect_ai/model/_providers/vertex.py +5 -2
  64. inspect_ai/solver/_bridge/bridge.py +1 -1
  65. inspect_ai/solver/_chain.py +7 -6
  66. inspect_ai/tool/__init__.py +4 -0
  67. inspect_ai/tool/_tool_call.py +5 -2
  68. inspect_ai/tool/_tool_support_helpers.py +200 -0
  69. inspect_ai/tool/_tools/_bash_session.py +119 -0
  70. inspect_ai/tool/_tools/_computer/_computer.py +1 -1
  71. inspect_ai/tool/_tools/_text_editor.py +121 -0
  72. inspect_ai/tool/_tools/_web_browser/_back_compat.py +150 -0
  73. inspect_ai/tool/_tools/_web_browser/_web_browser.py +75 -130
  74. inspect_ai/tool/_tools/_web_search.py +2 -2
  75. inspect_ai/util/_json.py +28 -0
  76. inspect_ai/util/_sandbox/context.py +18 -8
  77. inspect_ai/util/_sandbox/docker/config.py +1 -1
  78. inspect_ai/util/_sandbox/docker/internal.py +3 -3
  79. inspect_ai/util/_sandbox/environment.py +17 -2
  80. {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info}/METADATA +8 -5
  81. {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info}/RECORD +85 -108
  82. {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info}/WHEEL +1 -1
  83. inspect_ai/tool/_tools/_web_browser/_resources/.pylintrc +0 -8
  84. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/launch.json +0 -24
  85. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/settings.json +0 -25
  86. inspect_ai/tool/_tools/_web_browser/_resources/Dockerfile +0 -22
  87. inspect_ai/tool/_tools/_web_browser/_resources/README.md +0 -63
  88. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree.py +0 -71
  89. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree_node.py +0 -323
  90. inspect_ai/tool/_tools/_web_browser/_resources/cdp/__init__.py +0 -5
  91. inspect_ai/tool/_tools/_web_browser/_resources/cdp/a11y.py +0 -279
  92. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom.py +0 -9
  93. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom_snapshot.py +0 -293
  94. inspect_ai/tool/_tools/_web_browser/_resources/cdp/page.py +0 -94
  95. inspect_ai/tool/_tools/_web_browser/_resources/constants.py +0 -2
  96. inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.svg +0 -2
  97. inspect_ai/tool/_tools/_web_browser/_resources/mock_environment.py +0 -45
  98. inspect_ai/tool/_tools/_web_browser/_resources/playwright_browser.py +0 -50
  99. inspect_ai/tool/_tools/_web_browser/_resources/playwright_crawler.py +0 -48
  100. inspect_ai/tool/_tools/_web_browser/_resources/playwright_page_crawler.py +0 -280
  101. inspect_ai/tool/_tools/_web_browser/_resources/pyproject.toml +0 -65
  102. inspect_ai/tool/_tools/_web_browser/_resources/rectangle.py +0 -64
  103. inspect_ai/tool/_tools/_web_browser/_resources/rpc_client_helpers.py +0 -146
  104. inspect_ai/tool/_tools/_web_browser/_resources/scale_factor.py +0 -64
  105. inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_tree_node.py +0 -180
  106. inspect_ai/tool/_tools/_web_browser/_resources/test_playwright_crawler.py +0 -99
  107. inspect_ai/tool/_tools/_web_browser/_resources/test_rectangle.py +0 -15
  108. inspect_ai/tool/_tools/_web_browser/_resources/test_web_client.py +0 -44
  109. inspect_ai/tool/_tools/_web_browser/_resources/web_browser_rpc_types.py +0 -39
  110. inspect_ai/tool/_tools/_web_browser/_resources/web_client.py +0 -214
  111. inspect_ai/tool/_tools/_web_browser/_resources/web_client_new_session.py +0 -35
  112. inspect_ai/tool/_tools/_web_browser/_resources/web_server.py +0 -192
  113. {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info}/entry_points.txt +0 -0
  114. {inspect_ai-0.3.74.dist-info → inspect_ai-0.3.76.dist-info/licenses}/LICENSE +0 -0
  115. {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,d09GMgABAAAAABhoAA4AAAAAKaQAABgSAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiIbiWAcNAZgAIEMEQgKvmyuZQtQAAE2AiQDgRwEIAWDGAcgG1wgIwP1idKKQ/ZXB7wh9dEX4KmqTgg7FOEkMSxRPhYqDkW8PvZlHUXsYC5T5u+8aYjmrNnd7CZONmJIhATRQKAEjxiaENSCW+tUhYopvV7FqD0Vo3Xaq5j/Hc/DH+7Pu6/Etwa+QfI5PLCAI/0Ma2i69HEH24ONtWgl26cWIFCEkjzu/6qr7NZqvySHCQkMsykkzKaK67e/cqZlW8sRmOQETHo3G2KQrXeIg3sZJmoR1PbT+fX9rovbwCK2BwAVZ3MtNv1MK7XSyJCwIeED5y8Q3Izt87+fq7zE6htWxeNCBc+e4tv87s2t/iXRiHglUhqhqf7hGlmD0ymNlgiFWKm0ws7UnATk3p0D0i1RnC2EO7az3GeaAAgANAKiMAQBFoVhgDB5gMSnoVDSIExmiWlAtO1qrgWiY3NZDRDdilvrgQgBACCFeK775jZIQGFAgOsHmCckkKzeTfk5Ic8aD8cH/nsTZFn7PcLFI48/spCubulAfaCLofBJbf7oh8G0S0OHAyZ9g/BnOqA6GiPFaVRaTQRn+u/k5KFuOEP7WKGWFDAU1fXp3c438ah1p4VwoIhIyOgYmHBsHHwCIq5yspg7OcWiBLIHrgPFm5nT0khmAcGDkZcALQU2GC8j1PkxRDQcLHRcWozBCQQlgwT5ITBzUAJoGXBwRapjEwqnetDISNdxxjE4c5ELsIUoTq8uZcAEGjBK2y8CcoxAKIzURASUALEqhSgsmIUsgoUNxaFkXErgSlESSqRQ6PwEUAbangDOqgHOqECUIrUhmFRZS24EqNObym+ZtDILipQpyQJAAs8FWgj2C3aGrGmYm5NBH5Doqu1/AHWLlvYWACsAAKDHccyStAnEgVNwiClBJxfdMyiKfL0FMIiTxCZdoTKNuv7/C4ZXf4WGpbJYheYvwBp0wynHHfXcY6tWLFsKFAvJP2VGPQGSXKkCKHcnbSi7SJBDRGCOsLqudsWwlof36uxGBnggq4SdztXx2PF6t4JSR7g0LUjF8eCHRjG8ORl6kQtPkUhn1gvdfJxCNbkJZmtunJnwW/aGvArjpVX2f4w49oHwZiH59gts4xfLveFrv8u6eNUeoMwCpyd72nWpPP1pbES9VFQQhLF/52Da0GmKtr/zfIvztS5fb5mUs+EBXey7X2Sf4XD3tAH1UrfCeGX511TQuz7P+Uh38dWjclGMBYtZdA+KYkgLevnnjqrPnnIy1BQ+yFLQRUivi62lgFcsrL8SPv4c5/iuiUBYQuZJtD3h24tb+UtjYgg1ionsCGwjWLoDxGxsmgWidaM0Ez6RKEBq9OwZ0DcAub+pczveJGJlbc3QM2RhtrCr/kKzHoxmucQIY8YYZhtoNwHlErd9D+gffJg+TFPaOu6dK+Kw5ECsAXUoSJb0yNjMEZ74zu/XdVGIYaGnatBsOo5DsodXONSt0/x9nlcdRrxGNnZvN/qb16zl6cf06YpRKvtQlGE75ViB+8oOjGN6P953ub4udt6T429QHLxUDL20NS7veMvHmPoS0lxjSsY3owOesvtKxPbHImJ6CcC9KLMXwdKRMYori3uPeQqmzXmTjdE6SR+i0yk+3XlDTSBmOShkry+GTJ82m2VY1mOEuJFi97dc3zXIy0hiP3o3cMfx7V9+2alMXlKR9/WLFwdL1RiHuerosqGqoP7bLUWFXqJOtg9ul5TnxnjYW1MTun3akgWqF2RPd2S9YubVcAqItzTFEH4s3Z/nVGglXYrablAq8QFlS8FsiwPP+S2YpL9Fu9h4vJPTWBhVyyhwTyEFSDZkXpp5yc+p9GmOfUldl2E/XVIpimy72VRgAuAEKWBLXT5As56mGK7mGLOYYfYgqQG8SUpRqZ1K3t2hhGTL8yQqdFPomkRKsMphuv1S3Sz/R163sCBct2k7kG2NjMUxESvCiO0rIsRpV2OGG22FRdD/rpuwTzHGDe5iznE4pTnya8yZdUmubCCMST0jcyN6GZTIJUYpcq/rrnzmwXL3BT5R7LzTISDPV4cb2LY9uHI7F6akbppSmX7NJ0vIblGGxAZqAXHNxwbsLA/X6n1haCF7wpB+JWJeUmm1NZjrzWmr56UUYZftYfqo5Baxhn3kxrruyNG+81XL8sS5KVXx9RTxm4MbIAt6sqaf+C0s+X6afjTPi+v4kPqM32O8Y69fKvBC+SmK1iB+j09AZCXFpHECYAHiB+LlPqHXEVocM2ikmC3O8Qo31hzLToObBqQZ1a2ro/n4eLuuZ+ariG+L66cbZo07KlXWO7EYlwR0VwgHu1y1XOy6n8Qz23wgXM0qVlU7flPOFDhPNiY2xfBPgu+m4V7zASkv6+uDt4euix3542419NUAIHJIn5gHUPxar1o+Ds+kudnhrqR2FxZMGhIVhlfF1jbXWoc7h2UhnKGxkGOTH+Nw9BdVC7i6o20Jl3IcAilQvla/0bGvO+o+R0gro0Qpb9bG4lhlrRzf9aMjfjxR4OypQBrlp7bnTuencm3n2fYTdmFVWoqYxXfvovylEZ89BY+yyp6/HilLenrfXCDE/LpbQpqviQmvuwNnOnmpGJqIfZcRHlGgFCDmZPZosUJANsOCxSlC8dOoln66odHscdWiu7p+OUENKK8Lvewl3zIOy92kwEYj62DapQMxX+iTYvy4cV2XMcSMYQeVj1SWI6xezUddRX1tcZuSLyTrGPHTErssOyfmA9yCX5PJOTFFQSIj3sGaavtzWLU8KSAhK8il8c19eLrM/yX+fCr7F87u1rNlI3YFgHkJuyQak4JERaGbPr446D6j9RduXxamPHVThCVeArLtCJ5k2P522pHNHM9q052lTI/oaXdmtTtR6OthCUNZOvJr/ULoUsq3qDzXQqnN1jr3JjrBvDwRw2MxFiOsKobbLA05l8K83dYYyCp0E8hejLCL9wajt+sL/XIp3bDTF8bG3Fb/sYJ2G9aiiGS6CTrOvBwplGEE6h/WeSMbkoQY2asZsKDGyRYgZct0iOkp0PyQXA3KH3uxoFo3su5j1EJJtSRZWaHS79PFLomXmOaitmpLIVBk3fs9WR7qmGl6+fplhPkRN7Kge9GH6yjEsAkCp0fKBEAClMEFlby2xEs4vUm24ziklC6mRYqx/T33+F7f8SW6/GRRHKwpLlmG6vQ2bqo0/3D8Qi3cc3FHevu/fddP3V/PJAVTBRJSKypnlkCLMNTYt72M9URe6Of6dFYjNiXGzCRldSSkd/d8/378EaEpIckKEpFlAtLf931lGN+dJHfXq8A7+14UAXn4Yl51kardCXt3G7uT5r+OUbbjoYgkAPfImnBmM0M/OH+q5S12zDTO8cn8Y7rjre3AFJJ7HGB7fdlmqQ0cm4KgepFxSZi2dwjL7NtputeAJCEmcF6SxcKL6jkvSDQfCFlkz4xLn8p9dKMb1ZfScAPHDOG6yg4P0owFST1/PBtQ0J3KB3sVssAh8zcby1p0tG+c6YwLU55rjntDv4uZykt3xe2HNB7+b52UGCUT/UC89RlJgexos2lBdJDcrdH8vL5PB/KX35l2X/paXbwjhYOMpL9CW/FaFjmFzIe88Epj7fTirTBkmxwSN6azgTfQmo+UZ7Kk8trwZJ+cEzOvCXrkVsmTdYLQes5yXuSilghT+m6KUlPYBpLybZhOWBBuc+n9lxiCmjBRErOTmKaSZM+AfkHyf5OsZW9zPRbDN5wCk05sNL0gjAFCPFOpMrTC5yVJ++HKqhSV0HXpSvEvQbPDv5AnEYcu30y27alB+0ye3K+7/cwy/1s6JQGfEHvInrxAq1DgBd3RSDTHkmFoLazdH2OcStbRiwzrvu5kHk88z5x/atxk2hjQ0E/8irFJkHkKJF9e/XCj8yqiDl8izmjKmHLY8AfgzPv7hQCrr4IaBlX5roZaaalwPjnLNXDrrrQKMFUxenmdjJcAdZU+8+ixUgsLT4DnF/RJnDGSuu4Q1ty3rkuvTKbKviXQmraX6DU5nvWg4dksAfrjIRRiOrm+8XF2YvOX6MRkX9+6l+KdjWbsaMAtJ/PHGgxVYxg5kEb9Huu03i8sp54T4HCd05AmjeOlsedQT7iknU5kNI3uoX800FmePyiFvvD0/O7hoqCLu0NmfZw2+fiyH1ws9dSc8ynxviuHXQFCFX3P69Y42oALZX46NNcwibeIQ6U9xoIPOBwbmBvd2barBZnf2lZ9We5v1P/k5EHdUEPjTs5BQKxycnfDIAguES1iwKzZJyYqICq06ZlSlYTHoaQ09uIz67CvWKNE13LzL4cyAGQG/JTq7yqzG5cBPdBpEg0Y0YhqpG2NiWePRvHI07qQ44w+KjIdmAj1DjH3GgVhkR7MPqn3R1XwPW0CZAtupnflsM3ir7VpmGFsXJ7WjemB61gH1GlL9jUadKiNziwYM8frnTyYy9SXc7o6zIiMvETIXKx24TiQYfHfx8A9duVJuzB2ItfUlhvxK7aVTLXAt8E8KGq818A+XfpyblXuHF2O1qWiRYLB7UwC/Fhq/FE/ynPEaSt8VFSpx5kAuvqXsTJtav4ATsl78/6bMWdhsi1EW+/SAxA7NhdhM0zpXcLOlNEJAhAn/UjaP8FxrRfeaWuy796NoXjbr5wKigmm8EnHzeSTEHBP5Ns8e8P/caau+aGXTPJgTEpJN9lN7pr9hbfND67cLjr1SCt24NOc5B2mt8NWUMpjglmmdCKaEhSfWToht96FaBhyCNLGQDkEHTuuPt6DnHoI7MzoJtsUSFGcDh0qP2+icEiY5nJLbKWc241W+gpsnUxZXkxWIDUbssN9CHst85uwtpnEIJulbpABou9jkXRc8HOjywjEwg/MrbLJwxxC3pZ+CIEzG5whK4NsdPa2slt7JSBbGDZ8doUwzX8jvt/o1GR5yiAZKK0M/K+kEFKIb/GoRC5gZjTuWu63Ki3o8qwHgrh79usVit4PGBsRBSNMPROBDFC35HUjSYMR4NO2WjoOzTkeupnoNaV4sJmDKhElyi0Rzwc+4Kxl4PG5eNt39WZ28SJ/YkYrIHojow18Y2F9S5ZMOyQwAiI7ZTjbolbJTOCA9AxyI7iIO2Z+16rkBPdUEteQp2f3HHlMJV43T/ofPFyuiHmCNG8WZ+YuwLJ4jTxAaJ69irnWuJesgcCubmJN2AoeATVHgnJ2vj+ko+TEO89IfAaneu3vUqtDRfQ96UvTsaVYHbtEPtPXVMDdnQAyTT8enxE4GnTy6SpBYiQfSOhYbMJsGOyaxGrBH2u+nmjnjwdUYEqJ2oHL5wSuIWgfhro/ADRRCUxxBgGYy/FkgctRdk4RNqUOUTR6IN5DVFPe931rD+LqfvxDJlJs4vgJivqX57xlL3poQ+o7y8vvec9c4z4ing9bfdg43Ics9CY0TfYWMtIDivy8ZrZN/1oxmaHvbefvAf41tC6cf14FKVEPqvTBwGfNlTEMC4omlLmtgcOYETRrPNObmo5GwV/huMtV2Ock+mnUAxzHHmsFHtOrb1nTuT6Qj3G/n0M22+JkJeJVi9Xn83Jn8JP5slzBdIaV8BugviUbTSfkIZt4Wk99U5ouH78JDS+zIJ3EkHiCqidrdyOhSgQoVIfZOz6AnAeljsDSnLnv6Nzqu/gCissH1Gmii9M0xDZa/sLNslgcYJOpGycvSAwJys5g9g893Z+Gm7+8Wu4nt5KPmu6ln8NzQB69T5TyqZR55xdmqp/ZzSv0fKHpl5YPxCr6+5ArX8OvzHs5+6NXyEFYF7n2c/DvJKi3Bm0OIQo688F6VtCZwe0/sZRoItk6dmgy7WG8VwxCRLqImJLnSGYxC+CofR/szgtCgE1Bq5GLk+Sa4sBMF2dNvNvrUCKn7p2wvt7JHHeiP2Rt4x4p/3umX/Q87irYTE4rUwHPjJPy8uqYlETBp9K+ppRYD0JKNsLjqmrvnWFsKEkcyx7Tf0ffrJBxHk2w8VKsBJ23wT3nVQfa8lVLcYVTn6hmNHE554IrVgFOO9sUllO3RCNMLuazDhrmfJchYLCbnFTAIqTAM4YERU6MsFBCsyq/J4eZAUpVUtGOv422/3+qMckMkZFxm8sP4HfkEh2nUExqX4ZsmCAHzuqlVnEVo0PiWHLX7wX4iEke13yb+U76Fwe9a0LiKum0G1T+JBlACPRAlRJOYCG5MI/xx5uNqnWzi3ZC4L+LqGgU4XMssdiU02hX8XhSC0ectcIlywcKYxmbwMxjj366Sijru4vZuM0+TzMpeKh7FxSOZ+ITBefXArqrM9FGzCKKXF96nLrErWu46k+SBUJwoHc12L5ca/INKpxv44lVadrNfQJ5f12rgNkN6dneJ5OSg7OTLSn7eYzxWXTIMCRYvGeq5zeGip7HsGxl/HiwWBV4Q8Czvs4BG5xvlxqnaZXP1tNwrALBTD+Ot9pOjAVv+qOLcuJcj28bGGlk6Abhds9ih8xnHwSTMDg/6IafsX/iv0iNF4vURXQNRWohE2WG8m85CluxKkWuO7TaAsaw+f33oBECOpFCPS2CqRf9unguYngUipgpdH/6fuK8OAgxSnasoQ/tB0PB2iL5cvhzBI82O7cwdOnvx1ne+75ywlrY0akPb+2YG99HyoYyj7ou8aB1HE/7r345xX84XEoOSjV04susy1SJb3vgu7GZ1BY4CAxJVBVOBbthipBVXsHS5nlst/BlGEmqDQu1swOSm1YZDonjqPNc0SSEzNR/kJyo9mWEQfre6+4ZDe6DGuma6lk1vW7mdFYKcgwSvZJUgrsEn6ojcb8JVdU+Vdu1Xgtj4yfF9qtM22LrRrh8LWtUShK8MnhxahNVu7ICkqUF+IlPHcDTZhtInr5UiFL3obcGLG1ubBzMOIx7mikbSKBcBNnzyIZRH86wjXRiT5ub0vJ/2sh39TGyQXvIMgIZniAlVkc45iwaMv9IlDM3zmJK2674Rk1wWjGCMRTMdO9hGDjoLhmNr5LRoTUr+eum5XYXclCyaLqFQ9toHjGZwlZMAc08+rTwyHUPB6IZSBzCgw1Aa2RfV6iDX7uu3aWhupztCW33X56kSxdpQ00lV5yxuud58lQUliIfg3Yb2i1c+3g05cNfbb9vXhv//f3u9I9ZYbqCR0S8STpyJOCoB3IVV/5WUId5cCwZ+uO3yK67Su480Iezs2h96op9lby1AZ5IY7kiaHCXdmDdFjCm+LuJuYufb3JSU6w7RP+WhSg/23Zrxxk7qoabCW2c7X0D/6RoioaFJdmaJCPBIA0jRmgnvxhVJrDDMcO6xJK1vRNCVRtnCyAjNZ2OBXxjy4LGmBFJACxXRATaL5V4nSC7Hoovd4n3Ul/YRkQIRWsY0kBfDzUhmm7+9IeVDe4R8RP7z6jzhbYdHXzXMUAGSYHc9hUEI3phzcSJ+5Z/fvxHJshcuUx0I/fas1Rs4S4JoS/RifhAre00pBnzp4X9o12PO6R6pRYjgv8xEAn1qu5h5YLxDapIaCmMedBxXxmeOZrYjqflu44wofN89JkJ2ubyRx5InF6agPmyXE8FlC0NSu0NfwKtSP9YrJm13dwUEi/TxZguPuR2ONsjKoElepV1cQ2hKqHbWq5Tz83+WJbxoMef15FBIjT1MCdtiUot1iArW+26cP28+SO415m4XWJkDYEQ2Mewrjw3WmPk8ixqvEtSck31y0/0sy+le+7MbN0rJgXmnZqiTxfcCQT3WiTzFFJaQfiMrdFzrtwyhUuFpdedc2u1xRQLf7QX/n20GfP9gBQx/MyXhy56cDPzT7i1gmCImp46XOAiSxoa07R0yLts+o2CnTHbgsMiyDFdEQWgAfoQ1b9WVV3oFPVKQsgLAICHXWPgTR6tfj3MuB9GeNVYA5ss6gDVVoKlyRIk90fRGopv07OWMJZZAPTCr8zhVVh0NZoyawH4EzVt+JUG6InmGym3/IuTA7QZFJDlKNHwiK/Q8oMk+bwj5OxewzdhQn3BLJb/9c3l7yEIyQJRYYDwZFFGKKwsFRQxr3iDIveb0QE9e/jHyikswVda4+kmwABbDrVdGrgdUQ4E1CADAGpd8XUQjiN1MIaddQgfE+oI5ErqUDHkwOC0ABh1KlWsVpVyDeq18mdXpkJbj1CsWUbN26xFVECyRJAASvJGMstCl0aVOeslKgQzvCwJnj2M99GbkQw9WqxBKrPkfOSgoTCyCD17G3UJqFLxyjPBSwFv9gZT/iCkRIk4VRIKh5oV3g4BDtoWarNB4bylhjImZWgHOFRGZCO023+hAAA=); }</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
- )