diff --git a/.fernignore b/.fernignore
index 39199a1..17fad2b 100644
--- a/.fernignore
+++ b/.fernignore
@@ -2,5 +2,7 @@
src/scrapybara/client.py
src/scrapybara/anthropic/
+src/scrapybara/tools/
+src/scrapybara/types/act.py
tests/custom/test_client.py
.github/workflows/ci.yml
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 2daf4a8..659e929 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -107,6 +107,92 @@ files = [
[package.extras]
test = ["pytest (>=6)"]
+[[package]]
+name = "greenlet"
+version = "3.1.1"
+description = "Lightweight in-process concurrent programming"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"},
+ {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"},
+ {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"},
+ {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"},
+ {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"},
+ {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"},
+ {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"},
+ {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"},
+ {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"},
+ {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"},
+ {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"},
+ {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"},
+ {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"},
+ {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"},
+ {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"},
+ {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"},
+ {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"},
+ {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"},
+ {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"},
+ {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"},
+ {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"},
+ {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"},
+ {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"},
+ {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"},
+ {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"},
+ {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"},
+ {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"},
+ {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"},
+ {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"},
+ {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"},
+ {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"},
+ {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"},
+ {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"},
+ {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"},
+ {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"},
+ {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"},
+ {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"},
+ {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"},
+ {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"},
+ {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"},
+ {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"},
+ {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"},
+ {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"},
+ {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"},
+ {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"},
+ {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"},
+ {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"},
+ {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"},
+ {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"},
+ {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"},
+ {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"},
+ {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"},
+ {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"},
+ {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"},
+ {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"},
+ {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"},
+ {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"},
+ {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"},
+ {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"},
+ {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"},
+ {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"},
+ {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"},
+ {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"},
+ {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"},
+ {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"},
+ {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"},
+ {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"},
+ {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"},
+ {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"},
+ {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"},
+ {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"},
+ {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"},
+ {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"},
+]
+
+[package.extras]
+docs = ["Sphinx", "furo"]
+test = ["objgraph", "psutil"]
+
[[package]]
name = "h11"
version = "0.14.0"
@@ -341,6 +427,26 @@ files = [
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
+[[package]]
+name = "playwright"
+version = "1.48.0"
+description = "A high-level API to automate web browsers"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "playwright-1.48.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:082bce2739f1078acc7d0734da8cc0e23eb91b7fae553f3316d733276f09a6b1"},
+ {file = "playwright-1.48.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7da2eb51a19c7f3b523e9faa9d98e7af92e52eb983a099979ea79c9668e3cbf7"},
+ {file = "playwright-1.48.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:115b988d1da322358b77bc3bf2d3cc90f8c881e691461538e7df91614c4833c9"},
+ {file = "playwright-1.48.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:8dabb80e62f667fe2640a8b694e26a7b884c0b4803f7514a3954fc849126227b"},
+ {file = "playwright-1.48.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ff8303409ebed76bed4c3d655340320b768817d900ba208b394fdd7d7939a5c"},
+ {file = "playwright-1.48.0-py3-none-win32.whl", hash = "sha256:85598c360c590076d4f435525be991246d74a905b654ac19d26eab7ed9b98b2d"},
+ {file = "playwright-1.48.0-py3-none-win_amd64.whl", hash = "sha256:e0e87b0c4dc8fce83c725dd851aec37bc4e882bb225ec8a96bd83cf32d4f1623"},
+]
+
+[package.dependencies]
+greenlet = "3.1.1"
+pyee = "12.0.0"
+
[[package]]
name = "pluggy"
version = "1.5.0"
@@ -358,13 +464,13 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pydantic"
-version = "2.10.4"
+version = "2.10.5"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"},
- {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"},
+ {file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"},
+ {file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"},
]
[package.dependencies]
@@ -488,6 +594,23 @@ files = [
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+[[package]]
+name = "pyee"
+version = "12.0.0"
+description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pyee-12.0.0-py3-none-any.whl", hash = "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990"},
+ {file = "pyee-12.0.0.tar.gz", hash = "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145"},
+]
+
+[package.dependencies]
+typing-extensions = "*"
+
+[package.extras]
+dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"]
+
[[package]]
name = "pytest"
version = "7.4.4"
@@ -657,4 +780,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "13de834176e26d7becf87fce47d73d025fed246396db1afb1ab21e59e69350b4"
+content-hash = "8130552a641501ad4537fd8c8eec56deb0b111b9da63b6460974bd12d106ff5a"
diff --git a/pyproject.toml b/pyproject.toml
index 6a6900a..178562f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "scrapybara"
-version = "2.0.7"
+version = "2.1.0"
description = ""
readme = "README.md"
authors = []
@@ -34,6 +34,7 @@ Repository = 'https://github.com/scrapybara/scrapybara-python'
python = "^3.8"
anthropic = "^0.39.0"
httpx = ">=0.21.2"
+playwright = "^1.48.0"
pydantic = ">= 1.9.2"
pydantic-core = "^2.18.2"
typing_extensions = ">= 4.0.0"
diff --git a/reference.md b/reference.md
index 778f83f..a16e22d 100644
--- a/reference.md
+++ b/reference.md
@@ -761,177 +761,6 @@ client.instance.resume(
-
-
-
-
-## Agent
-client.agent.act(...)
-
--
-
-#### 🔌 Usage
-
-
--
-
-
--
-
-```python
-from scrapybara import Scrapybara
-
-client = Scrapybara(
- api_key="YOUR_API_KEY",
-)
-client.agent.act(
- instance_id="instance_id",
- cmd="cmd",
-)
-
-```
-
-
-
-
-
-#### ⚙️ Parameters
-
-
--
-
-
--
-
-**instance_id:** `str`
-
-
-
-
-
--
-
-**cmd:** `str`
-
-
-
-
-
--
-
-**include_screenshot:** `typing.Optional[bool]`
-
-
-
-
-
--
-
-**model:** `typing.Optional[Model]`
-
-
-
-
-
--
-
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
-
-
-
-
-
-
-
-
-
-
-
-client.agent.scrape(...)
-
--
-
-#### 🔌 Usage
-
-
--
-
-
--
-
-```python
-from scrapybara import Scrapybara
-
-client = Scrapybara(
- api_key="YOUR_API_KEY",
-)
-client.agent.scrape(
- instance_id="instance_id",
- cmd="cmd",
-)
-
-```
-
-
-
-
-
-#### ⚙️ Parameters
-
-
--
-
-
--
-
-**instance_id:** `str`
-
-
-
-
-
--
-
-**cmd:** `str`
-
-
-
-
-
--
-
-**schema:** `typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]]`
-
-
-
-
-
--
-
-**include_screenshot:** `typing.Optional[bool]`
-
-
-
-
-
--
-
-**model:** `typing.Optional[Model]`
-
-
-
-
-
--
-
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
-
-
-
-
-
-
-
diff --git a/src/scrapybara/__init__.py b/src/scrapybara/__init__.py
index 91c8291..03f549c 100644
--- a/src/scrapybara/__init__.py
+++ b/src/scrapybara/__init__.py
@@ -1,7 +1,6 @@
# This file was auto-generated by Fern from our API Definition.
from .types import (
- ActResponse,
AuthStateResponse,
BrowserAuthenticateResponse,
BrowserGetCdpUrlResponse,
@@ -20,7 +19,6 @@
Notebook,
NotebookCell,
SaveBrowserAuthResponse,
- ScrapeResponse,
StartBrowserResponse,
Status,
StopBrowserResponse,
@@ -29,15 +27,13 @@
ValidationErrorLocItem,
)
from .errors import UnprocessableEntityError
-from . import agent, browser, code, env, file, instance, notebook
-from .agent import Model
+from . import browser, code, env, file, instance, notebook
from .client import AsyncScrapybara, Scrapybara
from .environment import ScrapybaraEnvironment
from .instance import Action, Command
from .version import __version__
__all__ = [
- "ActResponse",
"Action",
"AsyncScrapybara",
"AuthStateResponse",
@@ -56,11 +52,9 @@
"InstanceGetStreamUrlResponse",
"InstanceScreenshotResponse",
"KernelInfo",
- "Model",
"Notebook",
"NotebookCell",
"SaveBrowserAuthResponse",
- "ScrapeResponse",
"Scrapybara",
"ScrapybaraEnvironment",
"StartBrowserResponse",
@@ -71,7 +65,6 @@
"ValidationError",
"ValidationErrorLocItem",
"__version__",
- "agent",
"browser",
"code",
"env",
diff --git a/src/scrapybara/agent/__init__.py b/src/scrapybara/agent/__init__.py
deleted file mode 100644
index b1f769f..0000000
--- a/src/scrapybara/agent/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-from .types import Model
-
-__all__ = ["Model"]
diff --git a/src/scrapybara/agent/client.py b/src/scrapybara/agent/client.py
deleted file mode 100644
index fb3ac5f..0000000
--- a/src/scrapybara/agent/client.py
+++ /dev/null
@@ -1,364 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-from ..core.client_wrapper import SyncClientWrapper
-from .types.model import Model
-from ..core.request_options import RequestOptions
-from ..types.act_response import ActResponse
-from ..core.jsonable_encoder import jsonable_encoder
-from ..core.pydantic_utilities import parse_obj_as
-from ..errors.unprocessable_entity_error import UnprocessableEntityError
-from ..types.http_validation_error import HttpValidationError
-from json.decoder import JSONDecodeError
-from ..core.api_error import ApiError
-from ..types.scrape_response import ScrapeResponse
-from ..core.client_wrapper import AsyncClientWrapper
-
-# this is used as the default value for optional parameters
-OMIT = typing.cast(typing.Any, ...)
-
-
-class AgentClient:
- def __init__(self, *, client_wrapper: SyncClientWrapper):
- self._client_wrapper = client_wrapper
-
- def act(
- self,
- instance_id: str,
- *,
- cmd: str,
- include_screenshot: typing.Optional[bool] = OMIT,
- model: typing.Optional[Model] = OMIT,
- request_options: typing.Optional[RequestOptions] = None,
- ) -> ActResponse:
- """
- Parameters
- ----------
- instance_id : str
-
- cmd : str
-
- include_screenshot : typing.Optional[bool]
-
- model : typing.Optional[Model]
-
- request_options : typing.Optional[RequestOptions]
- Request-specific configuration.
-
- Returns
- -------
- ActResponse
- Successful Response
-
- Examples
- --------
- from scrapybara import Scrapybara
-
- client = Scrapybara(
- api_key="YOUR_API_KEY",
- )
- client.agent.act(
- instance_id="instance_id",
- cmd="cmd",
- )
- """
- _response = self._client_wrapper.httpx_client.request(
- f"v1/instance/{jsonable_encoder(instance_id)}/act",
- method="POST",
- json={
- "cmd": cmd,
- "include_screenshot": include_screenshot,
- "model": model,
- },
- headers={
- "content-type": "application/json",
- },
- request_options=request_options,
- omit=OMIT,
- )
- try:
- if 200 <= _response.status_code < 300:
- return typing.cast(
- ActResponse,
- parse_obj_as(
- type_=ActResponse, # type: ignore
- object_=_response.json(),
- ),
- )
- if _response.status_code == 422:
- raise UnprocessableEntityError(
- typing.cast(
- HttpValidationError,
- parse_obj_as(
- type_=HttpValidationError, # type: ignore
- object_=_response.json(),
- ),
- )
- )
- _response_json = _response.json()
- except JSONDecodeError:
- raise ApiError(status_code=_response.status_code, body=_response.text)
- raise ApiError(status_code=_response.status_code, body=_response_json)
-
- def scrape(
- self,
- instance_id: str,
- *,
- cmd: str,
- schema: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT,
- include_screenshot: typing.Optional[bool] = OMIT,
- model: typing.Optional[Model] = OMIT,
- request_options: typing.Optional[RequestOptions] = None,
- ) -> ScrapeResponse:
- """
- Parameters
- ----------
- instance_id : str
-
- cmd : str
-
- schema : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]]
-
- include_screenshot : typing.Optional[bool]
-
- model : typing.Optional[Model]
-
- request_options : typing.Optional[RequestOptions]
- Request-specific configuration.
-
- Returns
- -------
- ScrapeResponse
- Successful Response
-
- Examples
- --------
- from scrapybara import Scrapybara
-
- client = Scrapybara(
- api_key="YOUR_API_KEY",
- )
- client.agent.scrape(
- instance_id="instance_id",
- cmd="cmd",
- )
- """
- _response = self._client_wrapper.httpx_client.request(
- f"v1/instance/{jsonable_encoder(instance_id)}/scrape",
- method="POST",
- json={
- "cmd": cmd,
- "schema": schema,
- "include_screenshot": include_screenshot,
- "model": model,
- },
- headers={
- "content-type": "application/json",
- },
- request_options=request_options,
- omit=OMIT,
- )
- try:
- if 200 <= _response.status_code < 300:
- return typing.cast(
- ScrapeResponse,
- parse_obj_as(
- type_=ScrapeResponse, # type: ignore
- object_=_response.json(),
- ),
- )
- if _response.status_code == 422:
- raise UnprocessableEntityError(
- typing.cast(
- HttpValidationError,
- parse_obj_as(
- type_=HttpValidationError, # type: ignore
- object_=_response.json(),
- ),
- )
- )
- _response_json = _response.json()
- except JSONDecodeError:
- raise ApiError(status_code=_response.status_code, body=_response.text)
- raise ApiError(status_code=_response.status_code, body=_response_json)
-
-
-class AsyncAgentClient:
- def __init__(self, *, client_wrapper: AsyncClientWrapper):
- self._client_wrapper = client_wrapper
-
- async def act(
- self,
- instance_id: str,
- *,
- cmd: str,
- include_screenshot: typing.Optional[bool] = OMIT,
- model: typing.Optional[Model] = OMIT,
- request_options: typing.Optional[RequestOptions] = None,
- ) -> ActResponse:
- """
- Parameters
- ----------
- instance_id : str
-
- cmd : str
-
- include_screenshot : typing.Optional[bool]
-
- model : typing.Optional[Model]
-
- request_options : typing.Optional[RequestOptions]
- Request-specific configuration.
-
- Returns
- -------
- ActResponse
- Successful Response
-
- Examples
- --------
- import asyncio
-
- from scrapybara import AsyncScrapybara
-
- client = AsyncScrapybara(
- api_key="YOUR_API_KEY",
- )
-
-
- async def main() -> None:
- await client.agent.act(
- instance_id="instance_id",
- cmd="cmd",
- )
-
-
- asyncio.run(main())
- """
- _response = await self._client_wrapper.httpx_client.request(
- f"v1/instance/{jsonable_encoder(instance_id)}/act",
- method="POST",
- json={
- "cmd": cmd,
- "include_screenshot": include_screenshot,
- "model": model,
- },
- headers={
- "content-type": "application/json",
- },
- request_options=request_options,
- omit=OMIT,
- )
- try:
- if 200 <= _response.status_code < 300:
- return typing.cast(
- ActResponse,
- parse_obj_as(
- type_=ActResponse, # type: ignore
- object_=_response.json(),
- ),
- )
- if _response.status_code == 422:
- raise UnprocessableEntityError(
- typing.cast(
- HttpValidationError,
- parse_obj_as(
- type_=HttpValidationError, # type: ignore
- object_=_response.json(),
- ),
- )
- )
- _response_json = _response.json()
- except JSONDecodeError:
- raise ApiError(status_code=_response.status_code, body=_response.text)
- raise ApiError(status_code=_response.status_code, body=_response_json)
-
- async def scrape(
- self,
- instance_id: str,
- *,
- cmd: str,
- schema: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT,
- include_screenshot: typing.Optional[bool] = OMIT,
- model: typing.Optional[Model] = OMIT,
- request_options: typing.Optional[RequestOptions] = None,
- ) -> ScrapeResponse:
- """
- Parameters
- ----------
- instance_id : str
-
- cmd : str
-
- schema : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]]
-
- include_screenshot : typing.Optional[bool]
-
- model : typing.Optional[Model]
-
- request_options : typing.Optional[RequestOptions]
- Request-specific configuration.
-
- Returns
- -------
- ScrapeResponse
- Successful Response
-
- Examples
- --------
- import asyncio
-
- from scrapybara import AsyncScrapybara
-
- client = AsyncScrapybara(
- api_key="YOUR_API_KEY",
- )
-
-
- async def main() -> None:
- await client.agent.scrape(
- instance_id="instance_id",
- cmd="cmd",
- )
-
-
- asyncio.run(main())
- """
- _response = await self._client_wrapper.httpx_client.request(
- f"v1/instance/{jsonable_encoder(instance_id)}/scrape",
- method="POST",
- json={
- "cmd": cmd,
- "schema": schema,
- "include_screenshot": include_screenshot,
- "model": model,
- },
- headers={
- "content-type": "application/json",
- },
- request_options=request_options,
- omit=OMIT,
- )
- try:
- if 200 <= _response.status_code < 300:
- return typing.cast(
- ScrapeResponse,
- parse_obj_as(
- type_=ScrapeResponse, # type: ignore
- object_=_response.json(),
- ),
- )
- if _response.status_code == 422:
- raise UnprocessableEntityError(
- typing.cast(
- HttpValidationError,
- parse_obj_as(
- type_=HttpValidationError, # type: ignore
- object_=_response.json(),
- ),
- )
- )
- _response_json = _response.json()
- except JSONDecodeError:
- raise ApiError(status_code=_response.status_code, body=_response.text)
- raise ApiError(status_code=_response.status_code, body=_response_json)
diff --git a/src/scrapybara/agent/types/__init__.py b/src/scrapybara/agent/types/__init__.py
deleted file mode 100644
index dce9230..0000000
--- a/src/scrapybara/agent/types/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-from .model import Model
-
-__all__ = ["Model"]
diff --git a/src/scrapybara/agent/types/model.py b/src/scrapybara/agent/types/model.py
deleted file mode 100644
index 5949ec8..0000000
--- a/src/scrapybara/agent/types/model.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-Model = typing.Union[typing.Literal["claude", "gemini"], typing.Any]
diff --git a/src/scrapybara/anthropic/__init__.py b/src/scrapybara/anthropic/__init__.py
index 052df21..b99c7c2 100644
--- a/src/scrapybara/anthropic/__init__.py
+++ b/src/scrapybara/anthropic/__init__.py
@@ -1,16 +1,30 @@
-from typing import Literal, Optional, TypedDict, Any
+from typing import Literal, Optional, TypedDict, Any, Dict
from anthropic.types.beta import (
BetaToolComputerUse20241022Param,
BetaToolTextEditor20241022Param,
BetaToolBash20241022Param,
)
import asyncio
+from pydantic import Field
from ..client import Instance
-
+from ..types.act import Model
from .base import BaseAnthropicTool, CLIResult, ToolError, ToolResult
+# New: universal act API
+class Anthropic(Model):
+ provider: Literal["anthropic"] = Field(default="anthropic", frozen=True)
+
+ def __init__(
+ self,
+ name: Optional[str] = "claude-3-5-sonnet-20241022",
+ api_key: Optional[str] = None,
+ ) -> None:
+ super().__init__(provider="anthropic", name=name, api_key=api_key)
+
+
+# Legacy: Anthropic SDK-compatible tools
class ComputerToolOptions(TypedDict):
display_height_px: int
display_width_px: int
@@ -162,3 +176,25 @@ async def __call__(self, **kwargs: Any) -> ToolResult:
)
except Exception as e:
raise ToolError(str(e)) from None
+
+
+class ToolCollection:
+ """A collection of anthropic-defined tools."""
+
+ def __init__(self, *tools):
+ self.tools = tools
+ self.tool_map = {tool.to_params()["name"]: tool for tool in tools}
+
+ def to_params(self) -> list:
+ return [tool.to_params() for tool in self.tools]
+
+ async def run(self, *, name: str, tool_input: Dict[str, Any]) -> ToolResult:
+ tool = self.tool_map.get(name)
+ if not tool:
+ return ToolResult(error=f"Tool {name} not found")
+ try:
+ r = await tool(**tool_input)
+ return r if r else ToolResult()
+ except Exception as e:
+ print(f"Error running tool {name}: {e}")
+ return ToolResult(error=str(e))
diff --git a/src/scrapybara/base_client.py b/src/scrapybara/base_client.py
index c2e37de..44799b9 100644
--- a/src/scrapybara/base_client.py
+++ b/src/scrapybara/base_client.py
@@ -7,7 +7,6 @@
from .core.api_error import ApiError
from .core.client_wrapper import SyncClientWrapper
from .instance.client import InstanceClient
-from .agent.client import AgentClient
from .browser.client import BrowserClient
from .code.client import CodeClient
from .notebook.client import NotebookClient
@@ -24,7 +23,6 @@
from .types.auth_state_response import AuthStateResponse
from .core.client_wrapper import AsyncClientWrapper
from .instance.client import AsyncInstanceClient
-from .agent.client import AsyncAgentClient
from .browser.client import AsyncBrowserClient
from .code.client import AsyncCodeClient
from .notebook.client import AsyncNotebookClient
@@ -98,7 +96,6 @@ def __init__(
timeout=_defaulted_timeout,
)
self.instance = InstanceClient(client_wrapper=self._client_wrapper)
- self.agent = AgentClient(client_wrapper=self._client_wrapper)
self.browser = BrowserClient(client_wrapper=self._client_wrapper)
self.code = CodeClient(client_wrapper=self._client_wrapper)
self.notebook = NotebookClient(client_wrapper=self._client_wrapper)
@@ -375,7 +372,6 @@ def __init__(
timeout=_defaulted_timeout,
)
self.instance = AsyncInstanceClient(client_wrapper=self._client_wrapper)
- self.agent = AsyncAgentClient(client_wrapper=self._client_wrapper)
self.browser = AsyncBrowserClient(client_wrapper=self._client_wrapper)
self.code = AsyncCodeClient(client_wrapper=self._client_wrapper)
self.notebook = AsyncNotebookClient(client_wrapper=self._client_wrapper)
diff --git a/src/scrapybara/client.py b/src/scrapybara/client.py
index d205c1c..7db66c6 100644
--- a/src/scrapybara/client.py
+++ b/src/scrapybara/client.py
@@ -1,15 +1,27 @@
from datetime import datetime
-from typing import Optional, Any, Dict, List, Sequence, Union, Literal
+from typing import (
+ Optional,
+ Any,
+ Dict,
+ List,
+ Sequence,
+ Union,
+ Literal,
+ Generator,
+ Callable,
+ AsyncGenerator,
+)
import typing
-import httpx
import os
+import asyncio
+
+import httpx
-from pydantic import BaseModel, ValidationError
-from scrapybara.agent.types.model import Model
+from scrapybara.core.http_client import AsyncHttpClient, HttpClient
from scrapybara.environment import ScrapybaraEnvironment
from .core.request_options import RequestOptions
+from .core.api_error import ApiError
from .types import (
- ActResponse,
AuthStateResponse,
BrowserAuthenticateResponse,
BrowserGetCdpUrlResponse,
@@ -26,140 +38,29 @@
Notebook as NotebookType,
NotebookCell,
SaveBrowserAuthResponse,
- ScrapeResponse,
StartBrowserResponse,
StopBrowserResponse,
StopInstanceResponse,
)
+from .types.act import (
+ ActRequest,
+ ActResponse,
+ Message,
+ Model,
+ TextPart,
+ Tool,
+ ToolCallPart,
+ ToolMessage,
+ ToolResultPart,
+ UserMessage,
+ AssistantMessage,
+ Step,
+)
from .base_client import BaseClient, AsyncBaseClient
from .instance.types import Action, Command
OMIT = typing.cast(typing.Any, ...)
-PydanticModelT = typing.TypeVar("PydanticModelT", bound=BaseModel)
-
-
-class Agent:
- def __init__(self, instance_id: str, client: BaseClient):
- self.instance_id = instance_id
- self._client = client
-
- def act(
- self,
- *,
- cmd: str,
- include_screenshot: Optional[bool] = OMIT,
- model: Optional[Model] = OMIT,
- request_options: Optional[RequestOptions] = None,
- ) -> ActResponse:
- return self._client.agent.act(
- self.instance_id,
- cmd=cmd,
- include_screenshot=include_screenshot,
- model=model,
- request_options=request_options,
- )
-
- def scrape(
- self,
- *,
- cmd: str,
- schema: Optional[Dict[str, Optional[Any]]] = OMIT,
- include_screenshot: Optional[bool] = OMIT,
- model: Optional[Model] = OMIT,
- request_options: Optional[RequestOptions] = None,
- ) -> ScrapeResponse:
- return self._client.agent.scrape(
- self.instance_id,
- cmd=cmd,
- schema=schema,
- include_screenshot=include_screenshot,
- model=model,
- request_options=request_options,
- )
-
- def scrape_to_pydantic(
- self,
- *,
- cmd: str,
- schema: PydanticModelT,
- model: typing.Optional[Model] = OMIT,
- request_options: typing.Optional[RequestOptions] = None,
- ) -> PydanticModelT:
- response = self._client.agent.scrape(
- self.instance_id,
- cmd=cmd,
- schema=schema.model_json_schema(),
- model=model,
- request_options=request_options,
- )
-
- try:
- return schema.model_validate(response.data)
- except ValidationError as e:
- raise ValidationError(f"Validation error at client side: {e}") from e
-
-
-class AsyncAgent:
- def __init__(self, instance_id: str, client: AsyncBaseClient):
- self.instance_id = instance_id
- self._client = client
-
- async def act(
- self,
- *,
- cmd: str,
- include_screenshot: Optional[bool] = OMIT,
- model: Optional[Model] = OMIT,
- request_options: Optional[RequestOptions] = None,
- ) -> ActResponse:
- return await self._client.agent.act(
- self.instance_id,
- cmd=cmd,
- include_screenshot=include_screenshot,
- model=model,
- request_options=request_options,
- )
-
- async def scrape(
- self,
- *,
- cmd: str,
- schema: Optional[Dict[str, Optional[Any]]] = OMIT,
- include_screenshot: Optional[bool] = OMIT,
- model: Optional[Model] = OMIT,
- request_options: Optional[RequestOptions] = None,
- ) -> ScrapeResponse:
- return await self._client.agent.scrape(
- self.instance_id,
- cmd=cmd,
- schema=schema,
- include_screenshot=include_screenshot,
- model=model,
- request_options=request_options,
- )
-
- async def scrape_to_pydantic(
- self,
- *,
- cmd: str,
- schema: PydanticModelT,
- model: typing.Optional[Model] = OMIT,
- request_options: typing.Optional[RequestOptions] = None,
- ) -> PydanticModelT:
- response = await self._client.agent.scrape(
- self.instance_id,
- cmd=cmd,
- schema=schema.model_json_schema(),
- model=model,
- request_options=request_options,
- )
-
- try:
- return schema.model_validate(response.data)
- except ValidationError as e:
- raise ValidationError(f"Validation error at client side: {e}") from e
-
class Browser:
def __init__(self, instance_id: str, client: BaseClient):
@@ -659,7 +560,6 @@ def __init__(
self.instance_type = instance_type
self.status = status
self._client = client
- self.agent = Agent(self.id, self._client)
self.browser = Browser(self.id, self._client)
self.code = Code(self.id, self._client)
self.notebook = Notebook(self.id, self._client)
@@ -768,7 +668,6 @@ def __init__(
self.instance_type = instance_type
self.status = status
self._client = client
- self.agent = AsyncAgent(self.id, self._client)
self.browser = AsyncBrowser(self.id, self._client)
self.code = AsyncCode(self.id, self._client)
self.notebook = AsyncNotebook(self.id, self._client)
@@ -887,6 +786,10 @@ def __init__(
httpx_client=httpx_client,
)
+ @property
+ def httpx_client(self) -> HttpClient:
+ return self._base_client._client_wrapper.httpx_client
+
def start(
self,
*,
@@ -946,6 +849,181 @@ def get_auth_states(
response = self._base_client.get_auth_states(request_options=request_options)
return [AuthStateResponse(id=state.id, name=state.name) for state in response]
+ def act(
+ self,
+ *,
+ model: Model,
+ system: Optional[str] = None,
+ prompt: Optional[str] = None,
+ messages: Optional[List[Message]] = None,
+ tools: Optional[List[Tool]] = None,
+ on_step: Optional[Callable[[Step], None]] = None,
+ temperature: Optional[float] = None,
+ max_tokens: Optional[int] = None,
+ request_options: Optional[RequestOptions] = None,
+ ) -> List[Message]:
+ """
+ Run an agent loop with the given tools and model, returning all messages at the end.
+
+ Args:
+ tools: List of tools available to the agent
+ model: The model to use for generating responses
+ system: System prompt for the agent
+ prompt: Initial user prompt
+ messages: List of messages to start with
+ on_step: Callback for each step of the conversation
+ temperature: Optional temperature parameter for the model
+ max_tokens: Optional max tokens parameter for the model
+ request_options: Optional request configuration
+
+ Returns:
+ List of all messages from the conversation
+ """
+ result_messages: List[Message] = []
+ if messages:
+ result_messages.extend(messages)
+ for step in self.act_stream(
+ tools=tools,
+ model=model,
+ system=system,
+ prompt=prompt,
+ messages=messages,
+ temperature=temperature,
+ max_tokens=max_tokens,
+ on_step=on_step,
+ request_options=request_options,
+ ):
+ assistant_msg = AssistantMessage(
+ content=[TextPart(text=step.text)] + (step.tool_calls or [])
+ )
+ result_messages.append(assistant_msg)
+ if step.tool_results:
+ tool_msg = ToolMessage(content=step.tool_results)
+ result_messages.append(tool_msg)
+ return result_messages
+
+ def act_stream(
+ self,
+ *,
+ model: Model,
+ system: Optional[str] = None,
+ prompt: Optional[str] = None,
+ messages: Optional[List[Message]] = None,
+ tools: Optional[List[Tool]] = None,
+ on_step: Optional[Callable[[Step], None]] = None,
+ temperature: Optional[float] = None,
+ max_tokens: Optional[int] = None,
+ request_options: Optional[RequestOptions] = None,
+ ) -> Generator[Step, None, None]:
+ """
+ Run an interactive agent loop with the given tools and model.
+
+ Args:
+ tools: List of tools available to the agent
+ model: The model to use for generating responses
+ system: System prompt for the agent
+ prompt: Initial user prompt
+ messages: List of messages to start with
+ on_step: Callback for each step of the conversation
+ temperature: Optional temperature parameter for the model
+ max_tokens: Optional max tokens parameter for the model
+ request_options: Optional request configuration
+
+ Yields:
+ Steps from the conversation, including tool results
+ """
+ current_messages: List[Message] = []
+ if messages is None:
+ if prompt is None:
+ raise ValueError("prompt or messages must be provided")
+ current_messages = [UserMessage(content=[TextPart(text=prompt)])]
+ else:
+ current_messages = list(messages)
+
+ current_tools = [] if tools is None else list(tools)
+
+ while True:
+ request = ActRequest(
+ model=model,
+ system=system,
+ messages=current_messages,
+ tools=current_tools,
+ temperature=temperature,
+ max_tokens=max_tokens,
+ )
+
+ response = self.httpx_client.request(
+ "v1/act",
+ method="POST",
+ json=request.model_dump(exclude_none=True),
+ headers={"content-type": "application/json"},
+ request_options=request_options,
+ )
+
+ if not 200 <= response.status_code < 300:
+ raise ApiError(status_code=response.status_code, body=response.json())
+
+ act_response = ActResponse.model_validate(response.json())
+ current_messages.append(act_response.message)
+
+ # Extract text from assistant message
+ text = "\n".join(
+ part.text
+ for part in act_response.message.content
+ if isinstance(part, TextPart)
+ )
+
+ # Extract tool calls
+ tool_calls = [
+ part
+ for part in act_response.message.content
+ if isinstance(part, ToolCallPart)
+ ]
+
+ # Create initial step
+ step = Step(
+ text=text,
+ tool_calls=tool_calls if tool_calls else None,
+ finish_reason=act_response.finish_reason,
+ usage=act_response.usage,
+ )
+
+ # Check if we should continue the loop
+ has_tool_calls = bool(tool_calls)
+
+ if has_tool_calls:
+ tool_results: List[ToolResultPart] = []
+ for part in tool_calls:
+ tool = next(t for t in current_tools if t.name == part.tool_name)
+ try:
+ result = tool(**part.args)
+ tool_results.append(
+ ToolResultPart(
+ tool_call_id=part.tool_call_id,
+ tool_name=part.tool_name,
+ result=result,
+ )
+ )
+ except Exception as e:
+ tool_results.append(
+ ToolResultPart(
+ tool_call_id=part.tool_call_id,
+ tool_name=part.tool_name,
+ result=str(e),
+ is_error=True,
+ )
+ )
+ step.tool_results = tool_results
+ tool_message = ToolMessage(content=tool_results)
+ current_messages.append(tool_message)
+
+ if on_step:
+ on_step(step)
+ yield step
+
+ if not has_tool_calls:
+ break
+
class AsyncScrapybara:
def __init__(
@@ -967,6 +1045,10 @@ def __init__(
httpx_client=httpx_client,
)
+ @property
+ def httpx_client(self) -> AsyncHttpClient:
+ return self._base_client._client_wrapper.httpx_client
+
async def start(
self,
*,
@@ -1031,3 +1113,181 @@ async def get_auth_states(
request_options=request_options
)
return [AuthStateResponse(id=state.id, name=state.name) for state in response]
+
+ async def act(
+ self,
+ *,
+ model: Model,
+ system: Optional[str] = None,
+ prompt: Optional[str] = None,
+ messages: Optional[List[Message]] = None,
+ tools: Optional[List[Tool]] = None,
+ on_step: Optional[Callable[[Step], None]] = None,
+ temperature: Optional[float] = None,
+ max_tokens: Optional[int] = None,
+ request_options: Optional[RequestOptions] = None,
+ ) -> List[Message]:
+ """
+ Run an agent loop with the given tools and model, returning all messages at the end.
+
+ Args:
+ tools: List of tools available to the agent
+ model: The model to use for generating responses
+ system: System prompt for the agent
+ prompt: Initial user prompt
+ messages: List of messages to start with
+ on_step: Callback for each step of the conversation
+ temperature: Optional temperature parameter for the model
+ max_tokens: Optional max tokens parameter for the model
+ request_options: Optional request configuration
+
+ Returns:
+ List of all messages from the conversation
+ """
+ result_messages: List[Message] = []
+ if messages:
+ result_messages.extend(messages)
+ async for step in self.act_stream(
+ tools=tools,
+ model=model,
+ system=system,
+ prompt=prompt,
+ messages=messages,
+ temperature=temperature,
+ max_tokens=max_tokens,
+ on_step=on_step,
+ request_options=request_options,
+ ):
+ assistant_msg = AssistantMessage(
+ content=[TextPart(text=step.text)] + (step.tool_calls or [])
+ )
+ result_messages.append(assistant_msg)
+ if step.tool_results:
+ tool_msg = ToolMessage(content=step.tool_results)
+ result_messages.append(tool_msg)
+ return result_messages
+
+ async def act_stream(
+ self,
+ *,
+ model: Model,
+ system: Optional[str] = None,
+ prompt: Optional[str] = None,
+ messages: Optional[List[Message]] = None,
+ tools: Optional[List[Tool]] = None,
+ on_step: Optional[Callable[[Step], None]] = None,
+ temperature: Optional[float] = None,
+ max_tokens: Optional[int] = None,
+ request_options: Optional[RequestOptions] = None,
+ ) -> AsyncGenerator[Step, None]:
+ """
+ Run an interactive agent loop with the given tools and model.
+
+ Args:
+ tools: List of tools available to the agent
+ model: The model to use for generating responses
+ system: System prompt for the agent
+ prompt: Initial user prompt
+ messages: List of messages to start with
+ on_step: Callback for each step of the conversation
+ temperature: Optional temperature parameter for the model
+ max_tokens: Optional max tokens parameter for the model
+ request_options: Optional request configuration
+
+ Yields:
+ Steps from the conversation, including tool results
+ """
+ current_messages: List[Message] = []
+ if messages is None:
+ if prompt is None:
+ raise ValueError("prompt or messages must be provided")
+ current_messages = [UserMessage(content=[TextPart(text=prompt)])]
+ else:
+ current_messages = list(messages)
+
+ current_tools = [] if tools is None else list(tools)
+
+ while True:
+ request = ActRequest(
+ model=model,
+ system=system,
+ messages=current_messages,
+ tools=current_tools,
+ temperature=temperature,
+ max_tokens=max_tokens,
+ )
+
+ response = await self.httpx_client.request(
+ "v1/act",
+ method="POST",
+ json=request.model_dump(exclude_none=True),
+ headers={"content-type": "application/json"},
+ request_options=request_options,
+ )
+
+ if not 200 <= response.status_code < 300:
+ raise ApiError(status_code=response.status_code, body=response.json())
+
+ act_response = ActResponse.model_validate(response.json())
+ current_messages.append(act_response.message)
+
+ # Extract text from assistant message
+ text = "\n".join(
+ part.text
+ for part in act_response.message.content
+ if isinstance(part, TextPart)
+ )
+
+ # Extract tool calls
+ tool_calls = [
+ part
+ for part in act_response.message.content
+ if isinstance(part, ToolCallPart)
+ ]
+
+ # Create initial step
+ step = Step(
+ text=text,
+ tool_calls=tool_calls if tool_calls else None,
+ finish_reason=act_response.finish_reason,
+ usage=act_response.usage,
+ )
+
+ # Check if we should continue the loop
+ has_tool_calls = bool(tool_calls)
+
+ if has_tool_calls:
+ tool_results: List[ToolResultPart] = []
+ for part in tool_calls:
+ tool = next(t for t in current_tools if t.name == part.tool_name)
+ try:
+ loop = asyncio.get_event_loop()
+ result = await loop.run_in_executor(
+ None, lambda: tool(**part.args)
+ )
+ tool_results.append(
+ ToolResultPart(
+ tool_call_id=part.tool_call_id,
+ tool_name=part.tool_name,
+ result=result,
+ )
+ )
+ except Exception as e:
+ tool_results.append(
+ ToolResultPart(
+ tool_call_id=part.tool_call_id,
+ tool_name=part.tool_name,
+ result=str(e),
+ is_error=True,
+ )
+ )
+ step.tool_results = tool_results
+ tool_message = ToolMessage(content=tool_results)
+ current_messages.append(tool_message)
+
+ if on_step:
+ on_step(step)
+ yield step
+
+ if not has_tool_calls:
+ break
diff --git a/src/scrapybara/core/client_wrapper.py b/src/scrapybara/core/client_wrapper.py
index f59b9a3..fe993b6 100644
--- a/src/scrapybara/core/client_wrapper.py
+++ b/src/scrapybara/core/client_wrapper.py
@@ -16,7 +16,7 @@ def get_headers(self) -> typing.Dict[str, str]:
headers: typing.Dict[str, str] = {
"X-Fern-Language": "Python",
"X-Fern-SDK-Name": "scrapybara",
- "X-Fern-SDK-Version": "2.0.7",
+ "X-Fern-SDK-Version": "2.1.0",
}
headers["x-api-key"] = self.api_key
return headers
diff --git a/src/scrapybara/tools/__init__.py b/src/scrapybara/tools/__init__.py
new file mode 100644
index 0000000..31e6110
--- /dev/null
+++ b/src/scrapybara/tools/__init__.py
@@ -0,0 +1,203 @@
+from typing import Any
+from playwright.sync_api import sync_playwright
+
+from ..types.tool import Tool
+from ..client import Instance
+
+
+class ComputerTool(Tool):
+ """A computer interaction tool that allows the agent to control mouse and keyboard."""
+
+ _instance: Instance
+
+ def __init__(self, instance: Instance) -> None:
+ super().__init__(
+ name="computer",
+ )
+ self._instance = instance
+
+ def __call__(self, **kwargs: Any) -> Any:
+ action = kwargs.pop("action")
+ coordinate = kwargs.pop("coordinate", None)
+ text = kwargs.pop("text", None)
+
+ return self._instance.computer(
+ action=action,
+ coordinate=tuple(coordinate) if coordinate else None,
+ text=text,
+ )
+
+
+class EditTool(Tool):
+ """A filesystem editor tool that allows the agent to view, create, and edit files."""
+
+ _instance: Instance
+
+ def __init__(self, instance: Instance) -> None:
+ super().__init__(
+ name="str_replace_editor",
+ )
+ self._instance = instance
+
+ def __call__(self, **kwargs: Any) -> Any:
+ command = kwargs.pop("command")
+ path = kwargs.pop("path")
+ file_text = kwargs.pop("file_text", None)
+ view_range = kwargs.pop("view_range", None)
+ old_str = kwargs.pop("old_str", None)
+ new_str = kwargs.pop("new_str", None)
+ insert_line = kwargs.pop("insert_line", None)
+
+ return self._instance.edit(
+ command=command,
+ path=path,
+ file_text=file_text,
+ view_range=view_range,
+ old_str=old_str,
+ new_str=new_str,
+ insert_line=insert_line,
+ )
+
+
+class BashTool(Tool):
+ """A shell execution tool that allows the agent to run bash commands."""
+
+ _instance: Instance
+
+ def __init__(self, instance: Instance) -> None:
+ super().__init__(
+ name="bash",
+ )
+ self._instance = instance
+
+ def __call__(self, **kwargs: Any) -> Any:
+ command = kwargs.pop("command")
+ restart = kwargs.pop("restart", False)
+
+ return self._instance.bash(command=command, restart=restart)
+
+
+class BrowserTool(Tool):
+ """A browser interaction tool that allows the agent to interact with a browser."""
+
+ _instance: Instance
+
+ def __init__(self, instance: Instance) -> None:
+ super().__init__(
+ name="browser",
+ description="Interact with a browser for web scraping and automation",
+ parameters={
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "enum": [
+ "go_to", # Navigate to a URL
+ "get_html", # Get current page HTML
+ "evaluate", # Run JavaScript code
+ "click", # Click on an element
+ "type", # Type into an element
+ "screenshot", # Take a screenshot
+ "get_text", # Get text content of element
+ "get_attribute", # Get attribute of element
+ ],
+ "description": "The browser command to execute. Required parameters per command:\n- go_to: requires 'url'\n- evaluate: requires 'code'\n- click: requires 'selector'\n- type: requires 'selector' and 'text'\n- get_text: requires 'selector'\n- get_attribute: requires 'selector' and 'attribute'\n- get_html: no additional parameters\n- screenshot: no additional parameters",
+ },
+ "url": {
+ "type": "string",
+ "description": "URL for go_to command (required for go_to)",
+ },
+ "selector": {
+ "type": "string",
+ "description": "CSS selector for element operations (required for click, type, get_text, get_attribute)",
+ },
+ "code": {
+ "type": "string",
+ "description": "JavaScript code for evaluate command (required for evaluate)",
+ },
+ "text": {
+ "type": "string",
+ "description": "Text to type for type command (required for type)",
+ },
+ "timeout": {
+ "type": "integer",
+ "description": "Timeout in milliseconds for operations",
+ "default": 30000,
+ },
+ "attribute": {
+ "type": "string",
+ "description": "Attribute name for get_attribute command (required for get_attribute)",
+ },
+ },
+ "required": ["command"],
+ },
+ )
+ self._instance = instance
+
+ def __call__(self, **kwargs: Any) -> Any:
+ command = kwargs.pop("command")
+ url = kwargs.pop("url", None)
+ selector = kwargs.pop("selector", None)
+ code = kwargs.pop("code", None)
+ text = kwargs.pop("text", None)
+ timeout = kwargs.pop("timeout", 30000)
+ attribute = kwargs.pop("attribute", None)
+
+ cdp_url = self._instance.browser.get_cdp_url().cdp_url
+ if cdp_url is None:
+ raise ValueError("CDP URL is not available, start the browser first")
+
+ with sync_playwright() as playwright:
+ browser = playwright.chromium.connect_over_cdp(cdp_url)
+ context = browser.contexts[0]
+ if not context.pages:
+ page = context.new_page()
+ else:
+ page = context.pages[0]
+
+ try:
+ if command == "go_to":
+ page.goto(url, timeout=timeout)
+ return True
+
+ elif command == "get_html":
+ try:
+ return page.evaluate("() => document.documentElement.outerHTML")
+ except Exception:
+ # If page is navigating, just return what we can get
+ return page.evaluate("() => document.documentElement.innerHTML")
+
+ elif command == "evaluate":
+ return page.evaluate(code)
+
+ elif command == "click":
+ page.click(selector, timeout=timeout)
+ return True
+
+ elif command == "type":
+ page.type(selector, text, timeout=timeout)
+ return True
+
+ elif command == "screenshot":
+ return page.screenshot(type="png")
+
+ elif command == "get_text":
+ element = page.wait_for_selector(selector, timeout=timeout)
+ if element is None:
+ raise ValueError(f"Element not found: {selector}")
+ return element.text_content()
+
+ elif command == "get_attribute":
+ element = page.wait_for_selector(selector, timeout=timeout)
+ if element is None:
+ raise ValueError(f"Element not found: {selector}")
+ return element.get_attribute(attribute)
+
+ else:
+ raise ValueError(f"Unknown command: {command}")
+
+ except Exception as e:
+ raise ValueError(f"Browser command failed: {str(e)}")
+
+ finally:
+ browser.close()
diff --git a/src/scrapybara/types/__init__.py b/src/scrapybara/types/__init__.py
index b83f133..40bc910 100644
--- a/src/scrapybara/types/__init__.py
+++ b/src/scrapybara/types/__init__.py
@@ -1,6 +1,5 @@
# This file was auto-generated by Fern from our API Definition.
-from .act_response import ActResponse
from .auth_state_response import AuthStateResponse
from .browser_authenticate_response import BrowserAuthenticateResponse
from .browser_get_cdp_url_response import BrowserGetCdpUrlResponse
@@ -19,7 +18,6 @@
from .notebook import Notebook
from .notebook_cell import NotebookCell
from .save_browser_auth_response import SaveBrowserAuthResponse
-from .scrape_response import ScrapeResponse
from .start_browser_response import StartBrowserResponse
from .status import Status
from .stop_browser_response import StopBrowserResponse
@@ -28,7 +26,6 @@
from .validation_error_loc_item import ValidationErrorLocItem
__all__ = [
- "ActResponse",
"AuthStateResponse",
"BrowserAuthenticateResponse",
"BrowserGetCdpUrlResponse",
@@ -47,7 +44,6 @@
"Notebook",
"NotebookCell",
"SaveBrowserAuthResponse",
- "ScrapeResponse",
"StartBrowserResponse",
"Status",
"StopBrowserResponse",
diff --git a/src/scrapybara/types/act.py b/src/scrapybara/types/act.py
new file mode 100644
index 0000000..c70899b
--- /dev/null
+++ b/src/scrapybara/types/act.py
@@ -0,0 +1,97 @@
+from typing import Any, Dict, List, Literal, Optional, Union
+from pydantic import BaseModel
+from .tool import Tool
+
+
+# Message part types
+class TextPart(BaseModel):
+ type: Literal["text"] = "text"
+ text: str
+
+
+class ImagePart(BaseModel):
+ type: Literal["image"] = "image"
+ image: str # Base64 encoded image or URL
+ mime_type: Optional[str] = None
+
+
+class ToolCallPart(BaseModel):
+ type: Literal["tool-call"] = "tool-call"
+ tool_call_id: str
+ tool_name: str
+ args: Dict[str, Any]
+
+
+class ToolResultPart(BaseModel):
+ type: Literal["tool-result"] = "tool-result"
+ tool_call_id: str
+ tool_name: str
+ result: Any
+ is_error: Optional[bool] = False
+
+
+class UserMessage(BaseModel):
+ role: Literal["user"] = "user"
+ content: List[Union[TextPart, ImagePart]]
+
+
+class AssistantMessage(BaseModel):
+ role: Literal["assistant"] = "assistant"
+ content: List[Union[TextPart, ToolCallPart]]
+
+
+class ToolMessage(BaseModel):
+ role: Literal["tool"] = "tool"
+ content: List[ToolResultPart]
+
+
+Message = Union[UserMessage, AssistantMessage, ToolMessage]
+
+
+# Request/Response models
+class Model(BaseModel):
+ provider: Literal["anthropic"]
+ name: str
+ api_key: Optional[str] = None
+
+
+class ActRequest(BaseModel):
+ model: Model
+ system: Optional[str] = None
+ messages: Optional[List[Message]] = None
+ tools: Optional[List[Tool]] = None
+ temperature: Optional[float] = None
+ max_tokens: Optional[int] = None
+
+
+class TokenUsage(BaseModel):
+ prompt_tokens: int
+ completion_tokens: int
+ total_tokens: int
+
+
+class ActResponse(BaseModel):
+ message: AssistantMessage
+ finish_reason: Literal[
+ "stop", "length", "content-filter", "tool-calls", "error", "other", "unknown"
+ ]
+ usage: Optional[TokenUsage] = None
+
+
+# Step definition
+class Step(BaseModel):
+ text: str
+ tool_calls: Optional[List[ToolCallPart]] = None
+ tool_results: Optional[List[ToolResultPart]] = None
+ finish_reason: Optional[
+ Literal[
+ "stop",
+ "length",
+ "content-filter",
+ "tool-calls",
+ "error",
+ "other",
+ "unknown",
+ ]
+ ] = None
+ usage: Optional[TokenUsage] = None
diff --git a/src/scrapybara/types/act_response.py b/src/scrapybara/types/act_response.py
deleted file mode 100644
index f808f43..0000000
--- a/src/scrapybara/types/act_response.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-from ..core.pydantic_utilities import UniversalBaseModel
-import typing
-from ..core.pydantic_utilities import IS_PYDANTIC_V2
-import pydantic
-
-
-class ActResponse(UniversalBaseModel):
- output: str
- screenshot: typing.Optional[str] = None
-
- if IS_PYDANTIC_V2:
- model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
- else:
-
- class Config:
- frozen = True
- smart_union = True
- extra = pydantic.Extra.allow
diff --git a/src/scrapybara/types/scrape_response.py b/src/scrapybara/types/scrape_response.py
deleted file mode 100644
index 1b4799d..0000000
--- a/src/scrapybara/types/scrape_response.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-from ..core.pydantic_utilities import UniversalBaseModel
-import typing
-from ..core.pydantic_utilities import IS_PYDANTIC_V2
-import pydantic
-
-
-class ScrapeResponse(UniversalBaseModel):
- data: typing.Dict[str, typing.Optional[typing.Any]]
- screenshot: typing.Optional[str] = None
-
- if IS_PYDANTIC_V2:
- model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
- else:
-
- class Config:
- frozen = True
- smart_union = True
- extra = pydantic.Extra.allow
diff --git a/src/scrapybara/types/tool.py b/src/scrapybara/types/tool.py
new file mode 100644
index 0000000..0a79b94
--- /dev/null
+++ b/src/scrapybara/types/tool.py
@@ -0,0 +1,11 @@
+from typing import Any, Dict, Optional
+from pydantic import BaseModel
+
+
+class Tool(BaseModel):
+ name: str
+ description: Optional[str] = None
+ parameters: Optional[Dict[str, Any]] = None
+
+ def __call__(self, **kwargs: Any) -> Any:
+ raise NotImplementedError("Tool.__call__ must be implemented by subclasses")
diff --git a/tests/custom/test_client.py b/tests/custom/test_client.py
index 6f81cab..3e3208d 100644
--- a/tests/custom/test_client.py
+++ b/tests/custom/test_client.py
@@ -1,6 +1,52 @@
from scrapybara import Scrapybara
import os
+from scrapybara.anthropic import Anthropic
+from scrapybara.tools import BashTool, BrowserTool, ComputerTool, EditTool
+from datetime import datetime
+
+SYSTEM_PROMPT = f"""
+* You have access to an Ubuntu virtual machine with internet connectivity
+* You can install Ubuntu applications using the bash tool (use curl over wget)
+* To run GUI applications with the bash tool:
+ - Use a subshell, e.g. "(DISPLAY=:1 xterm &)", make sure to include the parantheses
+ - GUI apps will appear but may take time to load - confirm with an extra screenshot
+* Start Chromium (default browser) via the bash tool "(DISPLAY=:1 chromium &)", but interact with it visually via the computer tool
+* In Chromium, click the address bar directly to enter URLs/searches
+* If you need to read a full PDF after initial screenshot
+ - Download with curl
+ - Convert to text using pdftotext
+ - Read the text file with StrReplaceEditTool
+* If you need to read a HTML file:
+ - Open with the address bar in Chromium
+* For commands with large text output:
+ - Redirect to a temp file
+ - Use str_replace_editor or grep with context (-B and -A flags) to view output
+* When viewing pages:
+ - Zoom out to see full content, or
+ - Scroll to ensure you see everything
+* Computer function calls take time, string together calls when possible
+* You are allowed to take actions on behalf of the user on sites that are authenticated
+* If the user asks you to access a site, assume that the user has already authenticated
+* For accessibility, you can use and create accounts for communication tools such as email, Discord, Telegram on behalf of the user - e.g. join Discord channels, send emails, etc.
+* To login additional sites, ask the user to use Auth Contexts or the Interactive Desktop
+* Today's date is {datetime.today().strftime('%A, %B %-d, %Y')}
+
+
+
+* If first screenshot shows black screen:
+ - Click mouse in screen center
+ - Take another screenshot
+* When interacting with a field, always clear the field first using "ctrl+A" and "delete"
+ - Take an extra screenshot after clicking "enter" to confirm the field is properly submitted and move the mouse to the next field
+* If given a complex task, break down into smaller steps and ask the user for details only if necessary
+* Research facts with Google searches in Chromium
+* Read through web pages thoroughly by scrolling down till the end
+* Use more generalized websites during research, e.g. use Google Flights instead of United when searching for flights, only use United when finalizing bookings
+* Wait for actions to complete (examine previous screenshots) before taking another action
+* Be concise!
+"""
+
def test_client() -> None:
if os.getenv("SCRAPYBARA_API_KEY") is None:
@@ -15,5 +61,24 @@ def test_client() -> None:
instance.browser.start()
cdp_url = instance.browser.get_cdp_url()
assert cdp_url is not None
+
+ messages = client.act(
+ model=Anthropic(),
+ system=SYSTEM_PROMPT,
+ prompt="Go to the YC website and fetch the HTML",
+ tools=[
+ ComputerTool(instance),
+ BashTool(instance),
+ EditTool(instance),
+ BrowserTool(instance),
+ ],
+ on_step=lambda step: print(f"{step}\n"),
+ )
+ assert len(messages) > 0
+
instance.browser.stop()
instance.stop()
+
+
+if __name__ == "__main__":
+ test_client()