From f11259cd79c2d6c421b6ca6bd7f6c5378cc1a2e3 Mon Sep 17 00:00:00 2001 From: Tessa Kriesel Date: Mon, 2 Nov 2026 10:53:28 -0600 Subject: [PATCH 1/3] README: add the one-liner and privacy line, lead Usage with a streaming research call --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7dda8e5..497745e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![PyPI version](https://img.shields.io/pypi/v/tabstack.svg?label=pypi%20(stable))](https://pypi.org/project/tabstack/) +Give your model the web without giving up your privacy. Never trained on. Private by default. Built by Mozilla. + The Tabstack Python library provides convenient access to the Tabstack REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -32,9 +34,15 @@ client = Tabstack( api_key=os.environ.get("TABSTACK_API_KEY"), # This is the default and can be omitted ) -automate_event = client.agent.automate( - task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", +stream = client.agent.research( + query="What are the latest developments in quantum computing?", ) + +for event in stream: + if event.event == "complete": + print(event.data.report) + elif event.event == "error": + print(event.data.message) ``` While you can provide an `api_key` keyword argument, From 5372ddecff53b6ef30c1c59eb848dcc54f96f2c4 Mon Sep 17 00:00:00 2001 From: Tessa Kriesel Date: Mon, 2 Nov 2026 10:57:14 -0600 Subject: [PATCH 2/3] README: print research errors to stderr --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 497745e..3b9f327 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The full API of this library can be found in [api.md](api.md). ```python import os +import sys from tabstack import Tabstack client = Tabstack( @@ -42,7 +43,7 @@ for event in stream: if event.event == "complete": print(event.data.report) elif event.event == "error": - print(event.data.message) + print(event.data.message, file=sys.stderr) ``` While you can provide an `api_key` keyword argument, From 5a6e4868cb5761d063919e62a7cb2b90102851d9 Mon Sep 17 00:00:00 2001 From: Tessa Kriesel Date: Mon, 2 Nov 2026 13:05:04 -0600 Subject: [PATCH 3/3] README: show automate as a stream and read the result from the complete event Co-Authored-By: Claude --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3b9f327..ac81de6 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,13 @@ client = AsyncTabstack( async def main() -> None: - automate_event = await client.agent.automate( + stream = await client.agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", + url="https://github.com/trending", ) + async for event in stream: + if event.event == "complete": + print(event.data.final_answer) asyncio.run(main()) @@ -76,6 +80,8 @@ asyncio.run(main()) Functionality between the synchronous and asynchronous clients is otherwise identical. +`client.agent.automate()` and `client.agent.research()` always stream. They return a `Stream` (or `AsyncStream`) of `AutomateEvent` or `ResearchEvent`, and the final result arrives in the `complete` event. Both unions include an `error` event, and `AutomateEvent` also ends with a `done` event. + ### With aiohttp By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. @@ -101,9 +107,13 @@ async def main() -> None: api_key=os.environ.get("TABSTACK_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: - automate_event = await client.agent.automate( + stream = await client.agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", + url="https://github.com/trending", ) + async for event in stream: + if event.event == "complete": + print(event.data.final_answer) asyncio.run(main()) @@ -127,11 +137,14 @@ from tabstack import Tabstack client = Tabstack() -automate_event = client.agent.automate( +stream = client.agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", - geo_target={}, + url="https://github.com/trending", + geo_target={"country": "US"}, ) -print(automate_event.geo_target) +for event in stream: + if event.event == "complete": + print(event.data.final_answer) ``` ## Handling errors @@ -150,9 +163,18 @@ from tabstack import Tabstack client = Tabstack() try: - client.agent.automate( + stream = client.agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", + url="https://github.com/trending", ) + for event in stream: + if event.event == "complete": + if event.data.success: + print(event.data.final_answer) + elif event.data.error is not None: + print(event.data.error.message) + elif event.event == "error": + print(event.data.error.message) except tabstack.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. @@ -164,6 +186,8 @@ except tabstack.APIStatusError as e: print(e.response) ``` +Errors that happen after the stream opens arrive as events in the stream (an `error` event, or a `complete` event with `success: false`), not as a raised `APIError`. + Error codes are as follows: | Status Code | Error Type | @@ -195,17 +219,21 @@ client = Tabstack( ) # Or, configure per-request: -client.with_options(max_retries=5).agent.automate( +stream = client.with_options(max_retries=5).agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", ) +for event in stream: + if event.event == "complete": + print(event.data.final_answer) ``` ### Timeouts -By default requests time out after 1 minute. You can configure this with a `timeout` option, +By default requests time out after 1 minute. `client.agent.automate()` and `client.agent.research()` default to 10 minutes unless you change the client-level `timeout`. You can configure this with a `timeout` option, which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python +import httpx from tabstack import Tabstack # Configure the default for all requests: @@ -220,9 +248,12 @@ client = Tabstack( ) # Override per-request: -client.with_options(timeout=5.0).agent.automate( +stream = client.with_options(timeout=5.0).agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", ) +for event in stream: + if event.event == "complete": + print(event.data.final_answer) ``` On timeout, an `APITimeoutError` is thrown. @@ -268,8 +299,10 @@ response = client.agent.with_raw_response.automate( ) print(response.headers.get('X-My-Header')) -agent = response.parse() # get the object that `agent.automate()` would have returned -print(agent) +stream = response.parse() # get the object that `agent.automate()` would have returned +for event in stream: + if event.event == "complete": + print(event.data.final_answer) ``` These methods return an [`APIResponse`](https://github.com/Mozilla-Ocho/tabstack-python/tree/main/src/tabstack/_response.py) object. @@ -278,7 +311,7 @@ The async client returns an [`AsyncAPIResponse`](https://github.com/Mozilla-Ocho #### `.with_streaming_response` -The above interface eagerly reads the full response body when you make the request, which may not always be what you want. +The above interface eagerly reads the full response body when you make the request, which may not always be what you want. Streaming methods are the exception: for `client.agent.automate()` and `client.agent.research()`, `parse()` returns the event `Stream`, which reads the body as you iterate it. To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods.