diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 10f3091..6b7b74c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.0" + ".": "0.3.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index eeba574..808f944 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 26 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browser-use%2Fbrowser-use-ce018db4d6891d645cfb220c86d17ac1d431e1ba2f604e8015876b17a5a11149.yml -openapi_spec_hash: e9a00924682ab214ca5d8b6b5c84430e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browser-use%2Fbrowser-use-814bdd9f98b750d42a2b713a0a12b14fc5a0241ff820b2fbc7666ab2e9a5443f.yml +openapi_spec_hash: 0dae4d4d33a3ec93e470f9546e43fad3 config_hash: dd3e22b635fa0eb9a7c741a8aaca2a7f diff --git a/CHANGELOG.md b/CHANGELOG.md index 31ba976..211f6e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.3.0 (2025-08-20) + +Full Changelog: [v0.2.0...v0.3.0](https://github.com/browser-use/browser-use-python/compare/v0.2.0...v0.3.0) + +### Features + +* LLM key strings over LLM model enum ([0f5930a](https://github.com/browser-use/browser-use-python/commit/0f5930a7760190523f1d3e969c66e0a34ff075b3)) + ## 0.2.0 (2025-08-19) Full Changelog: [v0.1.0...v0.2.0](https://github.com/browser-use/browser-use-python/compare/v0.1.0...v0.2.0) diff --git a/api.md b/api.md index bddad6f..9c56e23 100644 --- a/api.md +++ b/api.md @@ -31,7 +31,6 @@ Types: ```python from browser_use_sdk.types import ( FileView, - LlmModel, TaskItemView, TaskStatus, TaskStepView, diff --git a/examples/async_create.py b/examples/async_create.py new file mode 100755 index 0000000..311dd01 --- /dev/null +++ b/examples/async_create.py @@ -0,0 +1,55 @@ +#!/usr/bin/env -S rye run python + +import asyncio +from typing import List + +from pydantic import BaseModel + +from browser_use_sdk import AsyncBrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = AsyncBrowserUse() + + +# Regular Task +async def create_regular_task() -> None: + res = await client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """ + ) + + print(f"Regular Task ID: {res.id}") + + +# Structured Output +async def create_structured_task() -> None: + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + res = await client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + structured_output_json=SearchResult, + ) + + print(f"Structured Task ID: {res.id}") + + +# Main + + +async def main() -> None: + await asyncio.gather( + # + create_regular_task(), + create_structured_task(), + ) + + +asyncio.run(main()) diff --git a/examples/async_retrieve.py b/examples/async_retrieve.py new file mode 100755 index 0000000..60e46c0 --- /dev/null +++ b/examples/async_retrieve.py @@ -0,0 +1,95 @@ +#!/usr/bin/env -S rye run python + +import asyncio +from typing import List + +from pydantic import BaseModel + +from browser_use_sdk import AsyncBrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = AsyncBrowserUse() + + +# Regular Task +async def retrieve_regular_task() -> None: + """ + Retrieves a regular task and waits for it to finish. + """ + + print("Retrieving regular task...") + + regular_task = await client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """ + ) + + print(f"Regular Task ID: {regular_task.id}") + + while True: + regular_status = await client.tasks.retrieve(regular_task.id) + print(f"Regular Task Status: {regular_status.status}") + if regular_status.status == "finished": + print(f"Regular Task Output: {regular_status.done_output}") + break + + await asyncio.sleep(1) + + print("Done") + + +async def retrieve_structured_task() -> None: + """ + Retrieves a structured task and waits for it to finish. + """ + + print("Retrieving structured task...") + + # Structured Output + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + structured_task = await client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + structured_output_json=SearchResult, + ) + + print(f"Structured Task ID: {structured_task.id}") + + while True: + structured_status = await client.tasks.retrieve(task_id=structured_task.id, structured_output_json=SearchResult) + print(f"Structured Task Status: {structured_status.status}") + + if structured_status.status == "finished": + if structured_status.parsed_output is None: + print("Structured Task No output") + else: + for post in structured_status.parsed_output.posts: + print(f" - {post.title} - {post.url}") + + break + + await asyncio.sleep(1) + + print("Done") + + +# Main + + +async def main() -> None: + await asyncio.gather( + # + retrieve_regular_task(), + retrieve_structured_task(), + ) + + +asyncio.run(main()) diff --git a/examples/async_run.py b/examples/async_run.py new file mode 100755 index 0000000..3f12f9d --- /dev/null +++ b/examples/async_run.py @@ -0,0 +1,63 @@ +#!/usr/bin/env -S rye run python + +import asyncio +from typing import List + +from pydantic import BaseModel + +from browser_use_sdk import AsyncBrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = AsyncBrowserUse() + + +# Regular Task +async def run_regular_task() -> None: + regular_result = await client.tasks.run( + task=""" + Find top 10 Hacker News articles and return the title and url. + """ + ) + + print(f"Regular Task ID: {regular_result.id}") + + print(f"Regular Task Output: {regular_result.done_output}") + + print("Done") + + +# Structured Output +async def run_structured_task() -> None: + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + structured_result = await client.tasks.run( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + structured_output_json=SearchResult, + ) + + print(f"Structured Task ID: {structured_result.id}") + + if structured_result.parsed_output is not None: + print("Structured Task Output:") + for post in structured_result.parsed_output.posts: + print(f" - {post.title} - {post.url}") + + print("Structured Task Done") + + +async def main() -> None: + await asyncio.gather( + # + run_regular_task(), + run_structured_task(), + ) + + +asyncio.run(main()) diff --git a/examples/async_stream.py b/examples/async_stream.py new file mode 100755 index 0000000..0dc923d --- /dev/null +++ b/examples/async_stream.py @@ -0,0 +1,81 @@ +#!/usr/bin/env -S rye run python + +import asyncio +from typing import List + +from pydantic import BaseModel + +from browser_use_sdk import AsyncBrowserUse +from browser_use_sdk.types.task_create_params import AgentSettings + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = AsyncBrowserUse() + + +# Regular Task +async def stream_regular_task() -> None: + regular_task = await client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + agent_settings=AgentSettings(llm="gemini-2.5-flash"), + ) + + print(f"Regular Task ID: {regular_task.id}") + + async for res in client.tasks.stream(regular_task.id): + print(f"Regular Task Status: {res.status}") + + if len(res.steps) > 0: + last_step = res.steps[-1] + print(f"Regular Task Step: {last_step.url} ({last_step.next_goal})") + for action in last_step.actions: + print(f" - Regular Task Action: {action}") + + print("Regular Task Done") + + +# Structured Output +async def stream_structured_task() -> None: + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + structured_task = await client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + structured_output_json=SearchResult, + ) + + print(f"Structured Task ID: {structured_task.id}") + + async for res in client.tasks.stream(structured_task.id, structured_output_json=SearchResult): + print(f"Structured Task Status: {res.status}") + + if res.status == "finished": + if res.parsed_output is None: + print("Structured Task No output") + else: + for post in res.parsed_output.posts: + print(f" - Structured Task Post: {post.title} - {post.url}") + break + + print("Structured Task Done") + + +# Main + + +async def main() -> None: + await asyncio.gather( + # + stream_regular_task(), + stream_structured_task(), + ) + + +asyncio.run(main()) diff --git a/examples/create.py b/examples/create.py new file mode 100755 index 0000000..35da8f6 --- /dev/null +++ b/examples/create.py @@ -0,0 +1,46 @@ +#!/usr/bin/env -S rye run python + +from typing import List + +from pydantic import BaseModel + +from browser_use_sdk import BrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = BrowserUse() + + +# Regular Task +def create_regular_task() -> None: + res = client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """ + ) + + print(res.id) + + +create_regular_task() + + +# Structured Output +def create_structured_task() -> None: + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + res = client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + structured_output_json=SearchResult, + ) + + print(res.id) + + +create_structured_task() diff --git a/examples/retrieve.py b/examples/retrieve.py new file mode 100755 index 0000000..6569a5f --- /dev/null +++ b/examples/retrieve.py @@ -0,0 +1,87 @@ +#!/usr/bin/env -S rye run python + +import time +from typing import List + +from pydantic import BaseModel + +from browser_use_sdk import BrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = BrowserUse() + + +# Regular Task +def retrieve_regular_task() -> None: + """ + Retrieves a regular task and waits for it to finish. + """ + + print("Retrieving regular task...") + + regular_task = client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """ + ) + + print(f"Task ID: {regular_task.id}") + + while True: + regular_status = client.tasks.retrieve(regular_task.id) + print(regular_status.status) + if regular_status.status == "finished": + print(regular_status.done_output) + break + + time.sleep(1) + + print("Done") + + +retrieve_regular_task() + + +def retrieve_structured_task() -> None: + """ + Retrieves a structured task and waits for it to finish. + """ + + print("Retrieving structured task...") + + # Structured Output + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + structured_task = client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + structured_output_json=SearchResult, + ) + + print(f"Task ID: {structured_task.id}") + + while True: + structured_status = client.tasks.retrieve(task_id=structured_task.id, structured_output_json=SearchResult) + print(structured_status.status) + + if structured_status.status == "finished": + if structured_status.parsed_output is None: + print("No output") + else: + for post in structured_status.parsed_output.posts: + print(f" - {post.title} - {post.url}") + + break + + time.sleep(1) + + print("Done") + + +retrieve_structured_task() diff --git a/examples/run.py b/examples/run.py new file mode 100755 index 0000000..dfe95ce --- /dev/null +++ b/examples/run.py @@ -0,0 +1,56 @@ +#!/usr/bin/env -S rye run python + +from typing import List + +from pydantic import BaseModel + +from browser_use_sdk import BrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = BrowserUse() + + +# Regular Task +def run_regular_task() -> None: + regular_result = client.tasks.run( + task=""" + Find top 10 Hacker News articles and return the title and url. + """ + ) + + print(f"Task ID: {regular_result.id}") + + print(regular_result.done_output) + + print("Done") + + +run_regular_task() + + +# Structured Output +def run_structured_task() -> None: + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + structured_result = client.tasks.run( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + structured_output_json=SearchResult, + ) + + print(f"Task ID: {structured_result.id}") + + if structured_result.parsed_output is not None: + for post in structured_result.parsed_output.posts: + print(f" - {post.title} - {post.url}") + + print("Done") + + +run_structured_task() diff --git a/examples/stream.py b/examples/stream.py new file mode 100755 index 0000000..eea579b --- /dev/null +++ b/examples/stream.py @@ -0,0 +1,72 @@ +#!/usr/bin/env -S rye run python + +from typing import List + +from pydantic import BaseModel + +from browser_use_sdk import BrowserUse +from browser_use_sdk.types.task_create_params import AgentSettings + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = BrowserUse() + + +# Regular Task +def stream_regular_task() -> None: + regular_task = client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + agent_settings=AgentSettings(llm="gemini-2.5-flash"), + ) + + print(f"Task ID: {regular_task.id}") + + for res in client.tasks.stream(regular_task.id): + print(res.status) + + if len(res.steps) > 0: + last_step = res.steps[-1] + print(f"{last_step.url} ({last_step.next_goal})") + for action in last_step.actions: + print(f" - {action}") + + print("Done") + + +stream_regular_task() + + +# Structured Output +def stream_structured_task() -> None: + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + structured_task = client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + structured_output_json=SearchResult, + ) + + print(f"Task ID: {structured_task.id}") + + for res in client.tasks.stream(structured_task.id, structured_output_json=SearchResult): + print(res.status) + + if res.status == "finished": + if res.parsed_output is None: + print("No output") + else: + for post in res.parsed_output.posts: + print(f" - {post.title} - {post.url}") + break + + print("Done") + + +stream_structured_task() diff --git a/pyproject.toml b/pyproject.toml index 3617055..7c9b988 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browser-use-sdk" -version = "0.2.0" +version = "0.3.0" description = "The official Python library for the browser-use API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browser_use/lib/.keep b/src/browser_use/lib/.keep deleted file mode 100644 index 5e2c99f..0000000 --- a/src/browser_use/lib/.keep +++ /dev/null @@ -1,4 +0,0 @@ -File generated from our OpenAPI spec by Stainless. - -This directory can be used to store custom files to expand the SDK. -It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/browser_use_sdk/_version.py b/src/browser_use_sdk/_version.py index 052155b..4e9d259 100644 --- a/src/browser_use_sdk/_version.py +++ b/src/browser_use_sdk/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browser_use_sdk" -__version__ = "0.2.0" # x-release-please-version +__version__ = "0.3.0" # x-release-please-version diff --git a/src/browser_use_sdk/lib/parse.py b/src/browser_use_sdk/lib/parse.py new file mode 100644 index 0000000..b11e44e --- /dev/null +++ b/src/browser_use_sdk/lib/parse.py @@ -0,0 +1,35 @@ +import json +import hashlib +from typing import Any, Union, Generic, TypeVar +from datetime import datetime + +from pydantic import BaseModel + +from browser_use_sdk.types.task_view import TaskView + +T = TypeVar("T", bound=BaseModel) + + +class TaskViewWithOutput(TaskView, Generic[T]): + """ + TaskView with structured output. + """ + + parsed_output: Union[T, None] + + +class CustomJSONEncoder(json.JSONEncoder): + """Custom JSON encoder to handle datetime objects.""" + + # NOTE: Python doesn't have the override decorator in 3.8, that's why we ignore it. + def default(self, o: Any) -> Any: # type: ignore[override] + if isinstance(o, datetime): + return o.isoformat() + return super().default(o) + + +def hash_task_view(task_view: TaskView) -> str: + """Hashes the task view to detect changes.""" + return hashlib.sha256( + json.dumps(task_view.model_dump(), sort_keys=True, cls=CustomJSONEncoder).encode() + ).hexdigest() diff --git a/src/browser_use_sdk/resources/tasks.py b/src/browser_use_sdk/resources/tasks.py index fab70a6..5cf36c8 100644 --- a/src/browser_use_sdk/resources/tasks.py +++ b/src/browser_use_sdk/resources/tasks.py @@ -2,11 +2,15 @@ from __future__ import annotations -from typing import Dict, List, Union, Optional +import json +import time +import asyncio +from typing import Dict, List, Union, TypeVar, Iterator, Optional, AsyncIterator, overload from datetime import datetime from typing_extensions import Literal import httpx +from pydantic import BaseModel from ..types import TaskStatus, task_list_params, task_create_params, task_update_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven @@ -19,6 +23,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from ..lib.parse import TaskViewWithOutput, hash_task_view from .._base_client import make_request_options from ..types.task_view import TaskView from ..types.task_status import TaskStatus @@ -30,6 +35,8 @@ __all__ = ["TasksResource", "AsyncTasksResource"] +T = TypeVar("T", bound=BaseModel) + class TasksResource(SyncAPIResource): @cached_property @@ -51,6 +58,107 @@ def with_streaming_response(self) -> TasksResourceWithStreamingResponse: """ return TasksResourceWithStreamingResponse(self) + @overload + def run( + self, + *, + task: str, + agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, + browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + structured_output_json: Optional[str] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TaskView: ... + + @overload + def run( + self, + *, + task: str, + agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, + browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + structured_output_json: type[T], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TaskViewWithOutput[T]: ... + + def run( + self, + *, + task: str, + agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, + browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + structured_output_json: Optional[Union[type[BaseModel], str]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: + """ + Run a new task and return the task view. + """ + if structured_output_json is not None and isinstance(structured_output_json, type): + create_task_res = self.create( + task=task, + agent_settings=agent_settings, + browser_settings=browser_settings, + included_file_names=included_file_names, + metadata=metadata, + secrets=secrets, + structured_output_json=structured_output_json, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + + for structured_msg in self.stream(create_task_res.id, structured_output_json=structured_output_json): + if structured_msg.status == "finished": + return structured_msg + + raise ValueError("Task did not finish") + + else: + create_task_res = self.create( + task=task, + agent_settings=agent_settings, + browser_settings=browser_settings, + included_file_names=included_file_names, + metadata=metadata, + secrets=secrets, + structured_output_json=structured_output_json, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + + for msg in self.stream(create_task_res.id): + if msg.status == "finished": + return msg + + raise ValueError("Task did not finish") + + @overload def create( self, *, @@ -67,6 +175,43 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TaskCreateResponse: ... + + @overload + def create( + self, + *, + task: str, + agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, + browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + structured_output_json: type[BaseModel], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TaskCreateResponse: ... + + def create( + self, + *, + task: str, + agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, + browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + structured_output_json: Optional[Union[type[BaseModel], str]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> TaskCreateResponse: """ Create and start a new Browser Use Agent task. @@ -134,6 +279,13 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ + if ( + structured_output_json is not None + and not isinstance(structured_output_json, str) + and isinstance(structured_output_json, type) + ): + structured_output_json = json.dumps(structured_output_json.model_json_schema()) + return self._post( "/tasks", body=maybe_transform( @@ -154,9 +306,11 @@ def create( cast_to=TaskCreateResponse, ) + @overload def retrieve( self, task_id: str, + structured_output_json: type[T], *, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -164,7 +318,33 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskView: + ) -> TaskViewWithOutput[T]: ... + + @overload + def retrieve( + self, + task_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TaskView: ... + + def retrieve( + self, + task_id: str, + structured_output_json: Optional[type[BaseModel]] | NotGiven = NOT_GIVEN, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: """ Get detailed information about a specific AI agent task. @@ -202,6 +382,29 @@ def retrieve( """ if not task_id: raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") + + if structured_output_json is not None and isinstance(structured_output_json, type): + res = self._get( + f"/tasks/{task_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=TaskView, + ) + + if res.done_output is None: + return TaskViewWithOutput[BaseModel]( + **res.model_dump(), + parsed_output=None, + ) + + parsed_output = structured_output_json.model_validate_json(res.done_output) + + return TaskViewWithOutput[BaseModel]( + **res.model_dump(), + parsed_output=parsed_output, + ) + return self._get( f"/tasks/{task_id}", options=make_request_options( @@ -210,6 +413,110 @@ def retrieve( cast_to=TaskView, ) + @overload + def stream( + self, + task_id: str, + structured_output_json: type[T], + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Iterator[TaskViewWithOutput[T]]: ... + + @overload + def stream( + self, + task_id: str, + structured_output_json: None | NotGiven = NOT_GIVEN, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Iterator[TaskView]: ... + + def stream( + self, + task_id: str, + structured_output_json: type[T] | None | NotGiven = NOT_GIVEN, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Iterator[TaskView | TaskViewWithOutput[T]]: + """ + Stream the task view as it is updated until the task is finished. + """ + + for res in self._watch( + task_id=task_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ): + if structured_output_json is not None and isinstance(structured_output_json, type): + if res.done_output is None: + yield TaskViewWithOutput[T]( + **res.model_dump(), + parsed_output=None, + ) + else: + schema: type[T] = structured_output_json + parsed_output: T = schema.model_validate_json(res.done_output) + + yield TaskViewWithOutput[T]( + **res.model_dump(), + parsed_output=parsed_output, + ) + + else: + yield res + + def _watch( + self, + task_id: str, + interval: float = 1, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Iterator[TaskView]: + """Converts a polling loop into a generator loop.""" + hash: str | None = None + + while True: + res = self.retrieve( + task_id=task_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + + res_hash = hash_task_view(res) + + if hash is None or res_hash != hash: + hash = res_hash + yield res + + if res.status == "finished": + break + + time.sleep(interval) + def update( self, task_id: str, @@ -545,6 +852,107 @@ def with_streaming_response(self) -> AsyncTasksResourceWithStreamingResponse: """ return AsyncTasksResourceWithStreamingResponse(self) + @overload + async def run( + self, + *, + task: str, + agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, + browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + structured_output_json: Optional[str] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TaskView: ... + + @overload + async def run( + self, + *, + task: str, + agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, + browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + structured_output_json: type[T], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TaskViewWithOutput[T]: ... + + async def run( + self, + *, + task: str, + agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, + browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + structured_output_json: Optional[Union[type[BaseModel], str]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: + """ + Run a new Browser Use Agent task. + """ + if structured_output_json is not None and isinstance(structured_output_json, type): + create_task_res = await self.create( + task=task, + agent_settings=agent_settings, + browser_settings=browser_settings, + included_file_names=included_file_names, + metadata=metadata, + secrets=secrets, + structured_output_json=structured_output_json, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + + async for structured_msg in self.stream(create_task_res.id, structured_output_json=structured_output_json): + if structured_msg.status == "finished": + return structured_msg + + raise ValueError("Task did not finish") + + else: + create_task_res = await self.create( + task=task, + agent_settings=agent_settings, + browser_settings=browser_settings, + included_file_names=included_file_names, + metadata=metadata, + secrets=secrets, + structured_output_json=structured_output_json, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + + async for msg in self.stream(create_task_res.id): + if msg.status == "finished": + return msg + + raise ValueError("Task did not finish") + + @overload async def create( self, *, @@ -561,6 +969,43 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TaskCreateResponse: ... + + @overload + async def create( + self, + *, + task: str, + agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, + browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + structured_output_json: type[BaseModel], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TaskCreateResponse: ... + + async def create( + self, + *, + task: str, + agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, + browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, + included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + structured_output_json: Optional[Union[type[BaseModel], str]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> TaskCreateResponse: """ Create and start a new Browser Use Agent task. @@ -628,6 +1073,14 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ + + if ( + structured_output_json is not None + and not isinstance(structured_output_json, str) + and isinstance(structured_output_json, type) + ): + structured_output_json = json.dumps(structured_output_json.model_json_schema()) + return await self._post( "/tasks", body=await async_maybe_transform( @@ -648,9 +1101,11 @@ async def create( cast_to=TaskCreateResponse, ) + @overload async def retrieve( self, task_id: str, + structured_output_json: type[T], *, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -658,7 +1113,33 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskView: + ) -> TaskViewWithOutput[T]: ... + + @overload + async def retrieve( + self, + task_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TaskView: ... + + async def retrieve( + self, + task_id: str, + structured_output_json: Optional[type[BaseModel]] | NotGiven = NOT_GIVEN, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: """ Get detailed information about a specific AI agent task. @@ -696,6 +1177,29 @@ async def retrieve( """ if not task_id: raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") + + if structured_output_json is not None and isinstance(structured_output_json, type): + res = await self._get( + f"/tasks/{task_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=TaskView, + ) + + if res.done_output is None: + return TaskViewWithOutput[BaseModel]( + **res.model_dump(), + parsed_output=None, + ) + + parsed_output = structured_output_json.model_validate_json(res.done_output) + + return TaskViewWithOutput[BaseModel]( + **res.model_dump(), + parsed_output=parsed_output, + ) + return await self._get( f"/tasks/{task_id}", options=make_request_options( @@ -704,6 +1208,108 @@ async def retrieve( cast_to=TaskView, ) + @overload + def stream( + self, + task_id: str, + structured_output_json: type[T], + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncIterator[TaskViewWithOutput[T]]: ... + + @overload + def stream( + self, + task_id: str, + structured_output_json: None | NotGiven = NOT_GIVEN, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncIterator[TaskView]: ... + + async def stream( + self, + task_id: str, + structured_output_json: type[T] | None | NotGiven = NOT_GIVEN, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncIterator[TaskView | TaskViewWithOutput[T]]: + """ + Stream the task view as it is updated until the task is finished. + """ + + async for res in self._watch( + task_id=task_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ): + if structured_output_json is not None and isinstance(structured_output_json, type): + if res.done_output is None: + yield TaskViewWithOutput[T]( + **res.model_dump(), + parsed_output=None, + ) + else: + schema: type[T] = structured_output_json + # pydantic returns the model instance, but the type checker can’t infer it. + parsed_output: T = schema.model_validate_json(res.done_output) + yield TaskViewWithOutput[T]( + **res.model_dump(), + parsed_output=parsed_output, + ) + else: + yield res + + async def _watch( + self, + task_id: str, + interval: float = 1, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncIterator[TaskView]: + """Converts a polling loop into a generator loop.""" + prev_hash: str | None = None + + while True: + res = await self.retrieve( + task_id=task_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + + res_hash = hash_task_view(res) + if prev_hash is None or res_hash != prev_hash: + prev_hash = res_hash + yield res + + if res.status == "finished": + break + + await asyncio.sleep(interval) + async def update( self, task_id: str, diff --git a/src/browser_use_sdk/types/__init__.py b/src/browser_use_sdk/types/__init__.py index 105b042..191c708 100644 --- a/src/browser_use_sdk/types/__init__.py +++ b/src/browser_use_sdk/types/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from .file_view import FileView as FileView -from .llm_model import LlmModel as LlmModel from .task_view import TaskView as TaskView from .task_status import TaskStatus as TaskStatus from .session_view import SessionView as SessionView diff --git a/src/browser_use_sdk/types/llm_model.py b/src/browser_use_sdk/types/llm_model.py deleted file mode 100644 index fd232e7..0000000 --- a/src/browser_use_sdk/types/llm_model.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["LlmModel"] - -LlmModel: TypeAlias = Literal[ - "gpt-4o", - "gpt-4o-mini", - "gpt-4.1", - "gpt-4.1-mini", - "o4-mini", - "o3", - "gemini-2.0-flash", - "gemini-2.0-flash-lite", - "gemini-2.5-flash-preview-04-17", - "gemini-2.5-flash", - "gemini-2.5-pro", - "claude-3-7-sonnet-20250219", - "claude-sonnet-4-20250514", - "llama-4-maverick-17b-128e-instruct", -] diff --git a/src/browser_use_sdk/types/task_create_params.py b/src/browser_use_sdk/types/task_create_params.py index 7943846..0585011 100644 --- a/src/browser_use_sdk/types/task_create_params.py +++ b/src/browser_use_sdk/types/task_create_params.py @@ -3,10 +3,9 @@ from __future__ import annotations from typing import Dict, List, Optional -from typing_extensions import Required, Annotated, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo -from .llm_model import LlmModel __all__ = ["TaskCreateParams", "AgentSettings", "BrowserSettings"] @@ -40,7 +39,19 @@ class TaskCreateParams(TypedDict, total=False): class AgentSettings(TypedDict, total=False): - llm: LlmModel + llm: Literal[ + "gpt-4.1", + "gpt-4.1-mini", + "o4-mini", + "o3", + "gemini-2.5-flash", + "gemini-2.5-pro", + "claude-sonnet-4-20250514", + "gpt-4o", + "gpt-4o-mini", + "llama-4-maverick-17b-128e-instruct", + "claude-3-7-sonnet-20250219", + ] profile_id: Annotated[Optional[str], PropertyInfo(alias="profileId")] diff --git a/src/browser_use_sdk/types/task_item_view.py b/src/browser_use_sdk/types/task_item_view.py index ff7c731..695e846 100644 --- a/src/browser_use_sdk/types/task_item_view.py +++ b/src/browser_use_sdk/types/task_item_view.py @@ -6,7 +6,6 @@ from pydantic import Field as FieldInfo from .._models import BaseModel -from .llm_model import LlmModel from .task_status import TaskStatus __all__ = ["TaskItemView"] @@ -17,7 +16,7 @@ class TaskItemView(BaseModel): is_scheduled: bool = FieldInfo(alias="isScheduled") - llm: LlmModel + llm: str session_id: str = FieldInfo(alias="sessionId") diff --git a/src/browser_use_sdk/types/task_view.py b/src/browser_use_sdk/types/task_view.py index 2bc4812..6eef227 100644 --- a/src/browser_use_sdk/types/task_view.py +++ b/src/browser_use_sdk/types/task_view.py @@ -8,7 +8,6 @@ from .._models import BaseModel from .file_view import FileView -from .llm_model import LlmModel from .task_status import TaskStatus from .task_step_view import TaskStepView @@ -37,7 +36,7 @@ class TaskView(BaseModel): is_scheduled: bool = FieldInfo(alias="isScheduled") - llm: LlmModel + llm: str output_files: List[FileView] = FieldInfo(alias="outputFiles") diff --git a/tests/api_resources/test_tasks.py b/tests/api_resources/test_tasks.py index d03ca35..d296a63 100644 --- a/tests/api_resources/test_tasks.py +++ b/tests/api_resources/test_tasks.py @@ -39,7 +39,7 @@ def test_method_create_with_all_params(self, client: BrowserUse) -> None: task = client.tasks.create( task="x", agent_settings={ - "llm": "gpt-4o", + "llm": "gpt-4.1", "profile_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "start_url": "startUrl", }, @@ -375,7 +375,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserUse task = await async_client.tasks.create( task="x", agent_settings={ - "llm": "gpt-4o", + "llm": "gpt-4.1", "profile_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "start_url": "startUrl", },