From a30f2a1b1c157470556c90a8a4129f8e1549d883 Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Wed, 20 Aug 2025 10:16:56 +0100 Subject: [PATCH 1/9] Add Sync Methods --- examples/create.py | 39 +++ examples/retrieve.py | 64 +++++ examples/run.py | 45 ++++ examples/stream.py | 61 +++++ src/browser_use/lib/.keep | 4 - src/browser_use_sdk/lib/parse.py | 15 ++ src/browser_use_sdk/resources/tasks.py | 319 ++++++++++++++++++++++++- 7 files changed, 542 insertions(+), 5 deletions(-) create mode 100755 examples/create.py create mode 100755 examples/retrieve.py create mode 100755 examples/run.py create mode 100755 examples/stream.py delete mode 100644 src/browser_use/lib/.keep create mode 100644 src/browser_use_sdk/lib/parse.py diff --git a/examples/create.py b/examples/create.py new file mode 100755 index 0000000..a5045cd --- /dev/null +++ b/examples/create.py @@ -0,0 +1,39 @@ +#!/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 +res = client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """ +) + +print(res.id) + + +# Structured Output +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) diff --git a/examples/retrieve.py b/examples/retrieve.py new file mode 100755 index 0000000..9b9c910 --- /dev/null +++ b/examples/retrieve.py @@ -0,0 +1,64 @@ +#!/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 +task = client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """ +) + +print(f"Task ID: {task.id}") + +while True: + regular = client.tasks.retrieve(task.id) + print(regular.status) + if regular.status == "finished": + print(regular.done_output) + break + + time.sleep(1) + + +# Structured Output +class HackerNewsPost(BaseModel): + title: str + url: str + + +class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + +structured = 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.id}") + +while True: + structured = client.tasks.retrieve(task_id=structured.id, structured_output_json=SearchResult) + print(structured.status) + + if structured.status == "finished": + if structured.parsed_output is None: + print("No output") + else: + for post in structured.parsed_output.posts: + print(f" - {post.title} - {post.url}") + + break + + time.sleep(1) diff --git a/examples/run.py b/examples/run.py new file mode 100755 index 0000000..df3184c --- /dev/null +++ b/examples/run.py @@ -0,0 +1,45 @@ +#!/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 +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) + + +# Structured Output +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}") diff --git a/examples/stream.py b/examples/stream.py new file mode 100755 index 0000000..7a11490 --- /dev/null +++ b/examples/stream.py @@ -0,0 +1,61 @@ +#!/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 +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}") + + +# 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}") + +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 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/lib/parse.py b/src/browser_use_sdk/lib/parse.py new file mode 100644 index 0000000..a3b4313 --- /dev/null +++ b/src/browser_use_sdk/lib/parse.py @@ -0,0 +1,15 @@ +from typing import Union, Generic, TypeVar + +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] diff --git a/src/browser_use_sdk/resources/tasks.py b/src/browser_use_sdk/resources/tasks.py index fab70a6..1a64eec 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 hashlib +from typing import Any, Dict, List, Union, TypeVar, Optional, Generator, 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 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,104 @@ 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]]: + 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 msg in self.stream(create_task_res.id, structured_output_json=structured_output_json): + if msg.status == "finished": + return 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 +172,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 +276,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 +303,37 @@ 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. + 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]: ... + + @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. @@ -202,6 +379,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[structured_output_json]( + **res.model_dump(), + parsed_output=None, + ) + + parsed_output = structured_output_json.model_validate_json(res.done_output) + + return TaskViewWithOutput[structured_output_json]( + **res.model_dump(), + parsed_output=parsed_output, + ) + return self._get( f"/tasks/{task_id}", options=make_request_options( @@ -210,6 +410,123 @@ def retrieve( cast_to=TaskView, ) + @overload + def stream( + 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, + ) -> Generator[TaskView, None]: ... + + @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, + ) -> Generator[TaskViewWithOutput[T], None]: ... + + def stream( + 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, + ) -> Generator[Union[TaskView, TaskViewWithOutput[BaseModel]], None]: + """ + 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[structured_output_json]( + **res.model_dump(), + parsed_output=None, + ) + else: + parsed_output = structured_output_json.model_validate_json(res.done_output) + + yield TaskViewWithOutput[structured_output_json]( + **res.model_dump(), + parsed_output=parsed_output, + ) + + else: + yield res + + 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(self, task_view: TaskView) -> str: + """Hashes the task view to detect changes.""" + return hashlib.sha256( + json.dumps(task_view.model_dump(), sort_keys=True, cls=self.CustomJSONEncoder).encode() + ).hexdigest() + + 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, + ) -> Generator[Union[TaskView, TaskViewWithOutput[BaseModel]], None]: + """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 = self._hash(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, From c5c21eae02017b54d85ffb3337356338499dec2d Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Wed, 20 Aug 2025 11:15:51 +0100 Subject: [PATCH 2/9] fixes --- examples/retrieve.py | 26 +++++++++++++------------- src/browser_use_sdk/resources/tasks.py | 19 +++++++++++-------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/examples/retrieve.py b/examples/retrieve.py index 9b9c910..3152768 100755 --- a/examples/retrieve.py +++ b/examples/retrieve.py @@ -11,19 +11,19 @@ client = BrowserUse() # Regular Task -task = client.tasks.create( +regular_task = client.tasks.create( task=""" Find top 10 Hacker News articles and return the title and url. """ ) -print(f"Task ID: {task.id}") +print(f"Task ID: {regular_task.id}") while True: - regular = client.tasks.retrieve(task.id) - print(regular.status) - if regular.status == "finished": - print(regular.done_output) + 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) @@ -39,24 +39,24 @@ class SearchResult(BaseModel): posts: List[HackerNewsPost] -structured = client.tasks.create( +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.id}") +print(f"Task ID: {structured_task.id}") while True: - structured = client.tasks.retrieve(task_id=structured.id, structured_output_json=SearchResult) - print(structured.status) + structured_status = client.tasks.retrieve(task_id=structured_task.id, structured_output_json=SearchResult) + print(structured_status.status) - if structured.status == "finished": - if structured.parsed_output is None: + if structured_status.status == "finished": + if structured_status.parsed_output is None: print("No output") else: - for post in structured.parsed_output.posts: + for post in structured_status.parsed_output.posts: print(f" - {post.title} - {post.url}") break diff --git a/src/browser_use_sdk/resources/tasks.py b/src/browser_use_sdk/resources/tasks.py index 1a64eec..8349081 100644 --- a/src/browser_use_sdk/resources/tasks.py +++ b/src/browser_use_sdk/resources/tasks.py @@ -113,6 +113,9 @@ def run( 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, @@ -128,9 +131,9 @@ def run( timeout=timeout, ) - for msg in self.stream(create_task_res.id, structured_output_json=structured_output_json): - if msg.status == "finished": - return msg + 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") @@ -341,7 +344,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskView: + ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: """ Get detailed information about a specific AI agent task. @@ -390,14 +393,14 @@ def retrieve( ) if res.done_output is None: - return TaskViewWithOutput[structured_output_json]( + return TaskViewWithOutput[BaseModel]( **res.model_dump(), parsed_output=None, ) parsed_output = structured_output_json.model_validate_json(res.done_output) - return TaskViewWithOutput[structured_output_json]( + return TaskViewWithOutput[BaseModel]( **res.model_dump(), parsed_output=parsed_output, ) @@ -462,14 +465,14 @@ def stream( ): if structured_output_json is not None and isinstance(structured_output_json, type): if res.done_output is None: - yield TaskViewWithOutput[structured_output_json]( + yield TaskViewWithOutput[BaseModel]( **res.model_dump(), parsed_output=None, ) else: parsed_output = structured_output_json.model_validate_json(res.done_output) - yield TaskViewWithOutput[structured_output_json]( + yield TaskViewWithOutput[BaseModel]( **res.model_dump(), parsed_output=parsed_output, ) From 917c3a4d6a39af18ef300474fb3e6dd92ca3baa9 Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Wed, 20 Aug 2025 11:42:44 +0100 Subject: [PATCH 3/9] Add Async --- examples/async_create.py | 55 +++++ examples/async_retrieve.py | 95 ++++++++ examples/async_run.py | 63 +++++ examples/async_stream.py | 81 +++++++ examples/create.py | 43 ++-- examples/retrieve.py | 95 +++++--- examples/run.py | 55 +++-- examples/stream.py | 81 ++++--- src/browser_use_sdk/lib/parse.py | 22 +- src/browser_use_sdk/resources/tasks.py | 324 +++++++++++++++++++++++-- 10 files changed, 782 insertions(+), 132 deletions(-) create mode 100755 examples/async_create.py create mode 100755 examples/async_retrieve.py create mode 100755 examples/async_run.py create mode 100755 examples/async_stream.py diff --git a/examples/async_create.py b/examples/async_create.py new file mode 100755 index 0000000..65a73d3 --- /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(): + 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(): + 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(): + 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..584c2c1 --- /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(): + """ + 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(): + """ + 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(): + 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..26184f5 --- /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(): + 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(): + 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(): + 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..503403e --- /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(): + 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(): + 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(): + await asyncio.gather( + # + stream_regular_task(), + stream_structured_task(), + ) + + +asyncio.run(main()) diff --git a/examples/create.py b/examples/create.py index a5045cd..7871035 100755 --- a/examples/create.py +++ b/examples/create.py @@ -9,31 +9,38 @@ # gets API Key from environment variable BROWSER_USE_API_KEY client = BrowserUse() + # Regular Task -res = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """ -) +def create_regular_task(): + res = client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """ + ) + + print(res.id) + -print(res.id) +create_regular_task() # Structured Output -class HackerNewsPost(BaseModel): - title: str - url: str +def create_structured_task(): + class HackerNewsPost(BaseModel): + title: str + url: str + class SearchResult(BaseModel): + posts: List[HackerNewsPost] -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) -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 index 3152768..b5183da 100755 --- a/examples/retrieve.py +++ b/examples/retrieve.py @@ -10,55 +10,78 @@ # gets API Key from environment variable BROWSER_USE_API_KEY client = BrowserUse() + # Regular Task -regular_task = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. +def retrieve_regular_task(): + """ + Retrieves a regular task and waits for it to finish. """ -) -print(f"Task ID: {regular_task.id}") + 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) -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 + print("Done") + + +retrieve_regular_task() + + +def retrieve_structured_task(): + """ + Retrieves a structured task and waits for it to finish. + """ - time.sleep(1) + print("Retrieving structured task...") + # Structured Output + class HackerNewsPost(BaseModel): + title: str + url: str -# 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, + ) -class SearchResult(BaseModel): - posts: List[HackerNewsPost] + 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) -structured_task = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - structured_output_json=SearchResult, -) + 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}") -print(f"Task ID: {structured_task.id}") + break -while True: - structured_status = client.tasks.retrieve(task_id=structured_task.id, structured_output_json=SearchResult) - print(structured_status.status) + time.sleep(1) - 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}") + print("Done") - break - time.sleep(1) +retrieve_structured_task() diff --git a/examples/run.py b/examples/run.py index df3184c..c269a16 100755 --- a/examples/run.py +++ b/examples/run.py @@ -9,37 +9,48 @@ # gets API Key from environment variable BROWSER_USE_API_KEY client = BrowserUse() + # Regular Task -regular_result = client.tasks.run( - task=""" - Find top 10 Hacker News articles and return the title and url. - """ -) +def run_regular_task(): + 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(f"Task ID: {regular_result.id}") + print("Done") -print(regular_result.done_output) + +run_regular_task() # Structured Output -class HackerNewsPost(BaseModel): - title: str - url: str +def run_structured_task(): + 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, + ) -class SearchResult(BaseModel): - posts: List[HackerNewsPost] + 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}") -structured_result = client.tasks.run( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - structured_output_json=SearchResult, -) + print("Done") -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}") +run_structured_task() diff --git a/examples/stream.py b/examples/stream.py index 7a11490..9160758 100755 --- a/examples/stream.py +++ b/examples/stream.py @@ -10,52 +10,63 @@ # gets API Key from environment variable BROWSER_USE_API_KEY client = BrowserUse() + # Regular Task -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"), -) +def stream_regular_task(): + 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) -print(f"Task ID: {regular_task.id}") + 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}") -for res in client.tasks.stream(regular_task.id): - print(res.status) + print("Done") - 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}") + +stream_regular_task() # Structured Output -class HackerNewsPost(BaseModel): - title: str - url: str +def stream_structured_task(): + 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, + ) -class SearchResult(BaseModel): - posts: List[HackerNewsPost] + print(f"Task ID: {structured_task.id}") + for res in client.tasks.stream(structured_task.id, structured_output_json=SearchResult): + print(res.status) -structured_task = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - structured_output_json=SearchResult, -) + 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(f"Task ID: {structured_task.id}") + print("Done") -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 +stream_structured_task() diff --git a/src/browser_use_sdk/lib/parse.py b/src/browser_use_sdk/lib/parse.py index a3b4313..b11e44e 100644 --- a/src/browser_use_sdk/lib/parse.py +++ b/src/browser_use_sdk/lib/parse.py @@ -1,4 +1,7 @@ -from typing import Union, Generic, TypeVar +import json +import hashlib +from typing import Any, Union, Generic, TypeVar +from datetime import datetime from pydantic import BaseModel @@ -13,3 +16,20 @@ class TaskViewWithOutput(TaskView, Generic[T]): """ 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 8349081..b355466 100644 --- a/src/browser_use_sdk/resources/tasks.py +++ b/src/browser_use_sdk/resources/tasks.py @@ -4,8 +4,8 @@ import json import time -import hashlib -from typing import Any, Dict, List, Union, TypeVar, Optional, Generator, overload +import asyncio +from typing import Dict, List, Union, TypeVar, Optional, Generator, AsyncGenerator, overload from datetime import datetime from typing_extensions import Literal @@ -23,7 +23,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..lib.parse import TaskViewWithOutput +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 @@ -480,21 +480,6 @@ def stream( else: yield res - 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(self, task_view: TaskView) -> str: - """Hashes the task view to detect changes.""" - return hashlib.sha256( - json.dumps(task_view.model_dump(), sort_keys=True, cls=self.CustomJSONEncoder).encode() - ).hexdigest() - def _watch( self, task_id: str, @@ -519,7 +504,7 @@ def _watch( timeout=timeout, ) - res_hash = self._hash(res) + res_hash = hash_task_view(res) if hash is None or res_hash != hash: hash = res_hash @@ -865,6 +850,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, *, @@ -881,6 +967,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. @@ -948,6 +1071,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( @@ -968,9 +1099,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. @@ -978,7 +1111,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. @@ -1016,6 +1175,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( @@ -1024,6 +1206,108 @@ async def retrieve( cast_to=TaskView, ) + @overload + async def stream( + 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, + ) -> AsyncGenerator[TaskView, None]: ... + + @overload + async 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, + ) -> AsyncGenerator[TaskViewWithOutput[T], None]: ... + + async def stream( + 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, + ) -> AsyncGenerator[Union[TaskView, TaskViewWithOutput[BaseModel]], None]: + """ + 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[BaseModel]( + **res.model_dump(), + parsed_output=None, + ) + else: + parsed_output = structured_output_json.model_validate_json(res.done_output) + + yield TaskViewWithOutput[BaseModel]( + **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, + ) -> AsyncGenerator[Union[TaskView, TaskViewWithOutput[BaseModel]], None]: + """Converts a polling loop into a generator loop.""" + 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 hash is None or res_hash != hash: + hash = res_hash + yield res + + if res.status == "finished": + break + + await asyncio.sleep(interval) + async def update( self, task_id: str, From 0f5930a7760190523f1d3e969c66e0a34ff075b3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:09:05 +0000 Subject: [PATCH 4/9] feat: LLM key strings over LLM model enum --- .stats.yml | 4 ++-- api.md | 1 - src/browser_use_sdk/types/__init__.py | 1 - src/browser_use_sdk/types/llm_model.py | 22 ------------------- .../types/task_create_params.py | 17 +++++++++++--- src/browser_use_sdk/types/task_item_view.py | 3 +-- src/browser_use_sdk/types/task_view.py | 3 +-- tests/api_resources/test_tasks.py | 4 ++-- 8 files changed, 20 insertions(+), 35 deletions(-) delete mode 100644 src/browser_use_sdk/types/llm_model.py 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/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/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", }, From 64bc58309841b941564569e923895669a301b4fd Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Wed, 20 Aug 2025 17:36:31 +0100 Subject: [PATCH 5/9] Update tasks.py --- src/browser_use_sdk/resources/tasks.py | 87 +++++++++++++------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/src/browser_use_sdk/resources/tasks.py b/src/browser_use_sdk/resources/tasks.py index b355466..c4c5dda 100644 --- a/src/browser_use_sdk/resources/tasks.py +++ b/src/browser_use_sdk/resources/tasks.py @@ -5,7 +5,7 @@ import json import time import asyncio -from typing import Dict, List, Union, TypeVar, Optional, Generator, AsyncGenerator, overload +from typing import Dict, List, Union, TypeVar, Iterator, Optional, AsyncIterator, overload from datetime import datetime from typing_extensions import Literal @@ -417,6 +417,7 @@ def retrieve( 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. @@ -424,7 +425,7 @@ def stream( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Generator[TaskView, None]: ... + ) -> Iterator[TaskView]: ... @overload def stream( @@ -438,12 +439,12 @@ def stream( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Generator[TaskViewWithOutput[T], None]: ... + ) -> Iterator[TaskViewWithOutput[T]]: ... def stream( self, task_id: str, - structured_output_json: Optional[type[BaseModel]] | NotGiven = NOT_GIVEN, + structured_output_json: type[BaseModel] | 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. @@ -451,7 +452,7 @@ def stream( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Generator[Union[TaskView, TaskViewWithOutput[BaseModel]], None]: + ) -> Iterator[Union[TaskView, TaskViewWithOutput[BaseModel]]]: """ Stream the task view as it is updated until the task is finished. """ @@ -491,7 +492,7 @@ def _watch( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Generator[Union[TaskView, TaskViewWithOutput[BaseModel]], None]: + ) -> Iterator[TaskView]: """Converts a polling loop into a generator loop.""" hash: str | None = None @@ -1207,9 +1208,10 @@ async def retrieve( ) @overload - async def stream( + 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. @@ -1217,13 +1219,13 @@ async def stream( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncGenerator[TaskView, None]: ... + ) -> AsyncIterator[TaskViewWithOutput[T]]: ... @overload - async def stream( + def stream( self, task_id: str, - structured_output_json: type[T], + 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. @@ -1231,12 +1233,12 @@ async def stream( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncGenerator[TaskViewWithOutput[T], None]: ... + ) -> AsyncIterator[TaskView]: ... - async def stream( + def stream( self, task_id: str, - structured_output_json: Optional[type[BaseModel]] | NotGiven = NOT_GIVEN, + 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. @@ -1244,49 +1246,51 @@ async def stream( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncGenerator[Union[TaskView, TaskViewWithOutput[BaseModel]], None]: + ) -> AsyncIterator[TaskView] | AsyncIterator[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[BaseModel]( - **res.model_dump(), - parsed_output=None, - ) + async def _gen() -> AsyncIterator[TaskView | TaskViewWithOutput[T]]: + 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 a schema (type[T]) is passed, wrap with parsed_output[T] + 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: - parsed_output = structured_output_json.model_validate_json(res.done_output) + yield res - yield TaskViewWithOutput[BaseModel]( - **res.model_dump(), - parsed_output=parsed_output, - ) - - else: - yield res + return _gen() 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, - ) -> AsyncGenerator[Union[TaskView, TaskViewWithOutput[BaseModel]], None]: + ) -> AsyncIterator[TaskView]: """Converts a polling loop into a generator loop.""" - hash: str | None = None + prev_hash: str | None = None while True: res = await self.retrieve( @@ -1298,9 +1302,8 @@ async def _watch( ) res_hash = hash_task_view(res) - - if hash is None or res_hash != hash: - hash = res_hash + if prev_hash is None or res_hash != prev_hash: + prev_hash = res_hash yield res if res.status == "finished": From 6318ceb111659ec258428299e5d254e8bbcd81d0 Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Wed, 20 Aug 2025 17:41:31 +0100 Subject: [PATCH 6/9] stash --- examples/async_create.py | 6 +++--- examples/async_retrieve.py | 6 +++--- examples/async_run.py | 6 +++--- examples/async_stream.py | 6 +++--- examples/create.py | 4 ++-- examples/retrieve.py | 4 ++-- examples/run.py | 4 ++-- examples/stream.py | 4 ++-- src/browser_use_sdk/resources/tasks.py | 22 +++++++++++----------- 9 files changed, 31 insertions(+), 31 deletions(-) diff --git a/examples/async_create.py b/examples/async_create.py index 65a73d3..311dd01 100755 --- a/examples/async_create.py +++ b/examples/async_create.py @@ -12,7 +12,7 @@ # Regular Task -async def create_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. @@ -23,7 +23,7 @@ async def create_regular_task(): # Structured Output -async def create_structured_task(): +async def create_structured_task() -> None: class HackerNewsPost(BaseModel): title: str url: str @@ -44,7 +44,7 @@ class SearchResult(BaseModel): # Main -async def main(): +async def main() -> None: await asyncio.gather( # create_regular_task(), diff --git a/examples/async_retrieve.py b/examples/async_retrieve.py index 584c2c1..60e46c0 100755 --- a/examples/async_retrieve.py +++ b/examples/async_retrieve.py @@ -12,7 +12,7 @@ # Regular Task -async def retrieve_regular_task(): +async def retrieve_regular_task() -> None: """ Retrieves a regular task and waits for it to finish. """ @@ -39,7 +39,7 @@ async def retrieve_regular_task(): print("Done") -async def retrieve_structured_task(): +async def retrieve_structured_task() -> None: """ Retrieves a structured task and waits for it to finish. """ @@ -84,7 +84,7 @@ class SearchResult(BaseModel): # Main -async def main(): +async def main() -> None: await asyncio.gather( # retrieve_regular_task(), diff --git a/examples/async_run.py b/examples/async_run.py index 26184f5..3f12f9d 100755 --- a/examples/async_run.py +++ b/examples/async_run.py @@ -12,7 +12,7 @@ # Regular Task -async def run_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. @@ -27,7 +27,7 @@ async def run_regular_task(): # Structured Output -async def run_structured_task(): +async def run_structured_task() -> None: class HackerNewsPost(BaseModel): title: str url: str @@ -52,7 +52,7 @@ class SearchResult(BaseModel): print("Structured Task Done") -async def main(): +async def main() -> None: await asyncio.gather( # run_regular_task(), diff --git a/examples/async_stream.py b/examples/async_stream.py index 503403e..0dc923d 100755 --- a/examples/async_stream.py +++ b/examples/async_stream.py @@ -13,7 +13,7 @@ # Regular Task -async def stream_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. @@ -36,7 +36,7 @@ async def stream_regular_task(): # Structured Output -async def stream_structured_task(): +async def stream_structured_task() -> None: class HackerNewsPost(BaseModel): title: str url: str @@ -70,7 +70,7 @@ class SearchResult(BaseModel): # Main -async def main(): +async def main() -> None: await asyncio.gather( # stream_regular_task(), diff --git a/examples/create.py b/examples/create.py index 7871035..35da8f6 100755 --- a/examples/create.py +++ b/examples/create.py @@ -11,7 +11,7 @@ # Regular Task -def create_regular_task(): +def create_regular_task() -> None: res = client.tasks.create( task=""" Find top 10 Hacker News articles and return the title and url. @@ -25,7 +25,7 @@ def create_regular_task(): # Structured Output -def create_structured_task(): +def create_structured_task() -> None: class HackerNewsPost(BaseModel): title: str url: str diff --git a/examples/retrieve.py b/examples/retrieve.py index b5183da..6569a5f 100755 --- a/examples/retrieve.py +++ b/examples/retrieve.py @@ -12,7 +12,7 @@ # Regular Task -def retrieve_regular_task(): +def retrieve_regular_task() -> None: """ Retrieves a regular task and waits for it to finish. """ @@ -42,7 +42,7 @@ def retrieve_regular_task(): retrieve_regular_task() -def retrieve_structured_task(): +def retrieve_structured_task() -> None: """ Retrieves a structured task and waits for it to finish. """ diff --git a/examples/run.py b/examples/run.py index c269a16..dfe95ce 100755 --- a/examples/run.py +++ b/examples/run.py @@ -11,7 +11,7 @@ # Regular Task -def run_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. @@ -29,7 +29,7 @@ def run_regular_task(): # Structured Output -def run_structured_task(): +def run_structured_task() -> None: class HackerNewsPost(BaseModel): title: str url: str diff --git a/examples/stream.py b/examples/stream.py index 9160758..eea579b 100755 --- a/examples/stream.py +++ b/examples/stream.py @@ -12,7 +12,7 @@ # Regular Task -def stream_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. @@ -38,7 +38,7 @@ def stream_regular_task(): # Structured Output -def stream_structured_task(): +def stream_structured_task() -> None: class HackerNewsPost(BaseModel): title: str url: str diff --git a/src/browser_use_sdk/resources/tasks.py b/src/browser_use_sdk/resources/tasks.py index c4c5dda..cab7db1 100644 --- a/src/browser_use_sdk/resources/tasks.py +++ b/src/browser_use_sdk/resources/tasks.py @@ -417,7 +417,7 @@ def retrieve( def stream( self, task_id: str, - structured_output_json: None | 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. @@ -425,13 +425,13 @@ def stream( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Iterator[TaskView]: ... + ) -> Iterator[TaskViewWithOutput[T]]: ... @overload def stream( self, task_id: str, - structured_output_json: type[T], + 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. @@ -439,12 +439,12 @@ def stream( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Iterator[TaskViewWithOutput[T]]: ... + ) -> Iterator[TaskView]: ... def stream( self, task_id: str, - structured_output_json: type[BaseModel] | None | NotGiven = NOT_GIVEN, + 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. @@ -452,7 +452,7 @@ def stream( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Iterator[Union[TaskView, TaskViewWithOutput[BaseModel]]]: + ) -> Iterator[TaskView | TaskViewWithOutput[T]]: """ Stream the task view as it is updated until the task is finished. """ @@ -466,14 +466,15 @@ def stream( ): if structured_output_json is not None and isinstance(structured_output_json, type): if res.done_output is None: - yield TaskViewWithOutput[BaseModel]( + yield TaskViewWithOutput[T]( **res.model_dump(), parsed_output=None, ) else: - parsed_output = structured_output_json.model_validate_json(res.done_output) + schema: type[T] = structured_output_json + parsed_output: T = schema.model_validate_json(res.done_output) - yield TaskViewWithOutput[BaseModel]( + yield TaskViewWithOutput[T]( **res.model_dump(), parsed_output=parsed_output, ) @@ -1246,7 +1247,7 @@ def stream( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncIterator[TaskView] | AsyncIterator[TaskViewWithOutput[T]]: + ) -> AsyncIterator[TaskView | TaskViewWithOutput[T]]: """ Stream the task view as it is updated until the task is finished. """ @@ -1259,7 +1260,6 @@ async def _gen() -> AsyncIterator[TaskView | TaskViewWithOutput[T]]: extra_body=extra_body, timeout=timeout, ): - # If a schema (type[T]) is passed, wrap with parsed_output[T] if structured_output_json is not None and isinstance(structured_output_json, type): if res.done_output is None: yield TaskViewWithOutput[T]( From 492c7a82f2b5e3b21704149caace97a41b14e34f Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Wed, 20 Aug 2025 17:42:35 +0100 Subject: [PATCH 7/9] Update tasks.py --- src/browser_use_sdk/resources/tasks.py | 49 ++++++++++++-------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/browser_use_sdk/resources/tasks.py b/src/browser_use_sdk/resources/tasks.py index cab7db1..8532dbc 100644 --- a/src/browser_use_sdk/resources/tasks.py +++ b/src/browser_use_sdk/resources/tasks.py @@ -1236,7 +1236,7 @@ def stream( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncIterator[TaskView]: ... - def stream( + async def stream( self, task_id: str, structured_output_json: type[T] | None | NotGiven = NOT_GIVEN, @@ -1252,32 +1252,29 @@ def stream( Stream the task view as it is updated until the task is finished. """ - async def _gen() -> AsyncIterator[TaskView | TaskViewWithOutput[T]]: - 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, - ) + 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: - yield res - - return _gen() + 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, From 49f720d5ff07c29c5d4571b3598069cdecfc15ac Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Wed, 20 Aug 2025 17:45:41 +0100 Subject: [PATCH 8/9] Update tasks.py --- src/browser_use_sdk/resources/tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/browser_use_sdk/resources/tasks.py b/src/browser_use_sdk/resources/tasks.py index 8532dbc..5cf36c8 100644 --- a/src/browser_use_sdk/resources/tasks.py +++ b/src/browser_use_sdk/resources/tasks.py @@ -1281,6 +1281,8 @@ async def _watch( 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, From b397d699a41a34b7ee2d291084f5341afb2582b8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:54:10 +0000 Subject: [PATCH 9/9] release: 0.3.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- src/browser_use_sdk/_version.py | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) 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/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/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_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