Skip to content

Commit c245234

Browse files
committed
- Add agent cancellation support and update version to 0.5.0
- feat: enhance proxy handling with AuthorizationRequiredError for unauthenticated requests - Add blueprint_id to TaskFetchResponse, TaskUpdateRequest, and TaskCreateRequest for enhanced task management - Clarify comments on message handling in main function for better understanding of execute behavior
1 parent dc14293 commit c245234

10 files changed

Lines changed: 496 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ All notable changes to the ChatBotKit Python SDK are documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.5.0] - 2026-07-22
8+
9+
### Added
10+
11+
- Agent cancellation. `agent.execute(...)` and `agent.complete(...)` now accept
12+
an `abort_signal` (`asyncio.Event`); setting it stops the loop from the outside
13+
(timeout, shutdown, user stop) and exits with code `1` at the next event
14+
boundary. The built-in `abort` tool's `hard=True` option now cancels the
15+
in-flight iteration immediately instead of being a no-op, bringing the Python
16+
agent to parity with the Node and Go SDKs.
17+
18+
### Changed
19+
20+
- The agent system instruction now includes the "Be Responsive" guideline
21+
(prioritise new user input mid-run), matching the Node and Go SDKs.
22+
723
## [0.4.0] - 2026-06-27
824

925
### Added
@@ -13,8 +29,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1329
(`oauth`/`jwt` secrets only; owner-only) and return `{ token, expiresAt }`.
1430
`client.secret.proxy(...)` / `client.contact.secret.proxy(...)` proxy a request
1531
through a secret — the credential is injected server-side (it never leaves the
16-
platform) and the raw upstream `httpx.Response` is returned verbatim. A non-2xx
17-
status (including `409 authorization_required`) is returned, not raised.
32+
platform) and the upstream `httpx.Response` is returned as-is, success or error.
33+
- `AuthorizationRequiredError` (exported from `chatbotkit`; a subclass of
34+
`APIError`) carrying the `url` the user must visit to authorize. It is raised
35+
when a secret or connection has not been authenticated yet
36+
(`409 authorization_required`) — by `mint`, by any normal route, and by `proxy`
37+
(which otherwise passes every genuine upstream response through untouched).
38+
`APIError` now also carries `status_code` and the parsed `data` body.
1839

1940
## [0.3.0] - 2026-06-26
2041

chatbotkit/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
from ._client import ChatBotKit
2-
from ._transport import APIError, Client, ClientOptions, Response
2+
from ._transport import (
3+
APIError,
4+
AuthorizationRequiredError,
5+
Client,
6+
ClientOptions,
7+
Response,
8+
)
39

410
__all__ = [
511
"APIError",
12+
"AuthorizationRequiredError",
613
"ChatBotKit",
714
"Client",
815
"ClientOptions",
916
"Response",
1017
]
1118

12-
__version__ = "0.4.0"
19+
__version__ = "0.5.0"

chatbotkit/_transport.py

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,66 @@ def __init__(
8888
code: str | None = None,
8989
status_code: int | None = None,
9090
url: str | None = None,
91+
data: Any = None,
9192
) -> None:
9293
super().__init__(message)
9394
self.message = message
9495
self.code = code
9596
self.status_code = status_code
9697
self.url = url
98+
self.data = data
99+
100+
101+
class AuthorizationRequiredError(APIError):
102+
"""Raised when a secret or connection has not been authenticated yet.
103+
104+
``url`` is the address the user must visit to authorize.
105+
"""
106+
107+
def __init__(
108+
self,
109+
message: str,
110+
*,
111+
status_code: int | None = None,
112+
url: str | None = None,
113+
data: Any = None,
114+
) -> None:
115+
super().__init__(
116+
message,
117+
code="AUTHORIZATION_REQUIRED",
118+
status_code=status_code,
119+
url=url,
120+
data=data,
121+
)
122+
123+
124+
def _handle_proxy_response(response: httpx.Response) -> httpx.Response:
125+
"""Surface a CBK control response from a proxied request.
126+
127+
Successful and non-JSON error responses are returned untouched; a CBK
128+
``authorization_required`` signal is raised as an AuthorizationRequiredError
129+
carrying the authorize URL, while a genuine upstream error is returned as-is.
130+
"""
131+
if response.status_code < 400:
132+
return response
133+
134+
if "application/json" not in response.headers.get("content-type", ""):
135+
return response
136+
137+
try:
138+
data = response.json()
139+
except ValueError:
140+
return response
141+
142+
if isinstance(data, dict) and data.get("error") == "authorization_required":
143+
raise AuthorizationRequiredError(
144+
data.get("message") or "authorization required",
145+
status_code=response.status_code,
146+
url=data.get("url"),
147+
data=data,
148+
)
149+
150+
return response
97151

98152

99153
class Response(Generic[T, U]):
@@ -272,6 +326,34 @@ async def request(
272326

273327
return response
274328

329+
async def proxy(
330+
self,
331+
path: str,
332+
*,
333+
method: str | None = None,
334+
query: Any = None,
335+
record: Any = None,
336+
headers: Mapping[str, str] | None = None,
337+
endpoint: str | None = None,
338+
) -> httpx.Response:
339+
"""Proxy a request and return the upstream response.
340+
341+
Successful and upstream-error responses pass through untouched; a CBK
342+
``authorization_required`` signal is raised as an
343+
AuthorizationRequiredError carrying the URL the user must visit.
344+
"""
345+
response = await self.request(
346+
path,
347+
method=method,
348+
query=query,
349+
record=record,
350+
headers=headers,
351+
endpoint=endpoint,
352+
raw=True,
353+
)
354+
355+
return _handle_proxy_response(response)
356+
275357
def stream(
276358
self,
277359
path: str,
@@ -371,18 +453,29 @@ async def raise_for_status(self, response: httpx.Response) -> None:
371453

372454
message = f"HTTP Error: {response.reason_phrase}"
373455
code = f"ERROR_{response.status_code}"
456+
data: Any = None
374457

375458
try:
376459
data = response.json()
377-
message = data.get("message") or message
378-
code = data.get("code") or code
460+
if isinstance(data, dict):
461+
message = data.get("message") or message
462+
code = data.get("code") or code
379463
except ValueError:
380464
body = await response.aread()
381465
message = body.decode() or f"HTTP Error: {response.status_code}"
382466

467+
if isinstance(data, dict) and data.get("error") == "authorization_required":
468+
raise AuthorizationRequiredError(
469+
message,
470+
status_code=response.status_code,
471+
url=data.get("url"),
472+
data=data,
473+
)
474+
383475
raise APIError(
384476
message,
385477
code=code,
386478
status_code=response.status_code,
387479
url=str(response.url),
480+
data=data,
388481
)

0 commit comments

Comments
 (0)