cominty-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .pytest_cache/
10
+ .env
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: cominty-sdk
3
+ Version: 0.1.0
4
+ Summary: Official async Python client for the Cominty managed agent chat API
5
+ Project-URL: Homepage, https://github.com/cominty/python-sdk
6
+ Project-URL: Repository, https://github.com/cominty/python-sdk
7
+ Author: Cominty
8
+ License: MIT
9
+ Keywords: agent,chat,cominty,sdk
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: pydantic-settings>=2
21
+ Requires-Dist: pydantic>=2
22
+ Provides-Extra: dev
23
+ Requires-Dist: mypy>=1.11; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Requires-Dist: respx>=0.21; extra == 'dev'
27
+ Requires-Dist: ruff>=0.6; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Cominty Python SDK
31
+
32
+ Official async Python client for the Cominty managed agent chat API.
33
+
34
+ ## Requirements
35
+
36
+ - Python 3.11+
37
+ - A Cominty API key
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install cominty-sdk
43
+ ```
44
+
45
+ Or with [uv](https://docs.astral.sh/uv/):
46
+
47
+ ```bash
48
+ uv add cominty-sdk
49
+ ```
50
+
51
+ ## Configuration
52
+
53
+ | Variable | Description |
54
+ |----------|-------------|
55
+ | `COMINTY_API_KEY` | API key (required) |
56
+ | `COMINTY_API_URL` | Override base URL (optional, takes priority) |
57
+ | `COMINTY_ENVIRONMENT` | `dev`, `staging`, or `production` (default: `production`) |
58
+ | `COMINTY_AGENT_ID` | Default agent ID for chat operations |
59
+ | `COMINTY_MAX_RETRIES` | Max retries on transient errors (default: `3`) |
60
+ | `COMINTY_TIMEOUT` | Request timeout in seconds (default: `60`) |
61
+
62
+ Default base URLs are placeholders and can be overridden with `COMINTY_API_URL`:
63
+
64
+ - `dev`: `https://api.dev.cominty.com`
65
+ - `staging`: `https://api.staging.cominty.com`
66
+ - `production`: `https://api.cominty.com`
67
+
68
+ Authentication uses the `x-cominty-token` header.
69
+
70
+ ## Quick start
71
+
72
+ ```python
73
+ import asyncio
74
+
75
+ from cominty_sdk import AsyncCominty, HumanMessage
76
+
77
+
78
+ async def main() -> None:
79
+ async with AsyncCominty() as client:
80
+ thread, reply = await client.chat.start_and_wait(
81
+ HumanMessage(content="What is Cominty?"),
82
+ agent_id="your-agent-id",
83
+ )
84
+ print(reply.content)
85
+ print(reply.tool_names)
86
+
87
+
88
+ asyncio.run(main())
89
+ ```
90
+
91
+ ## Send a message in an existing thread
92
+
93
+ ```python
94
+ message = await client.messages.send_and_wait(
95
+ thread_id=thread.id,
96
+ message=HumanMessage(
97
+ content="Search our docs for onboarding steps",
98
+ source_ids=[42],
99
+ disabled_tools=["web"],
100
+ ),
101
+ agent_id="your-agent-id",
102
+ )
103
+ ```
104
+
105
+ ## Upload a file
106
+
107
+ Upload is a single high-level call that performs the 3-step S3 flow internally:
108
+
109
+ ```python
110
+ file_id = await client.files.upload("report.pdf")
111
+
112
+ await client.messages.send_and_wait(
113
+ thread_id=thread.id,
114
+ message=HumanMessage(content="Summarize this file", file_ids=[file_id]),
115
+ agent_id="your-agent-id",
116
+ )
117
+ ```
118
+
119
+ ## Streaming
120
+
121
+ The API returns JSONL events on the stream endpoint:
122
+
123
+ ```python
124
+ async for event in client.messages.stream(message.id):
125
+ print(event)
126
+ ```
127
+
128
+ ## QA helpers
129
+
130
+ `MessageOut` exposes convenience accessors for automated QA:
131
+
132
+ ```python
133
+ reply.tool_names # tools invoked (from events)
134
+ reply.cite_tags # raw <cite .../> tags
135
+ reply.document_citations # parsed document citations
136
+ reply.web_citations # parsed web citations
137
+ ```
138
+
139
+ ## Covered endpoints
140
+
141
+ | Resource | Methods |
142
+ |----------|---------|
143
+ | Threads | `list`, `get`, `update`, `archive` |
144
+ | Chat | `start`, `start_and_wait` |
145
+ | Messages | `send`, `send_and_wait`, `wait_until_done`, `cancel`, `export`, `stream` |
146
+ | Files | `upload`, `download` |
147
+ | Usage | `get` |
148
+
149
+ ## Development
150
+
151
+ ```bash
152
+ uv sync --all-extras --dev
153
+ uv run pytest
154
+ uv run ruff check .
155
+ uv run mypy
156
+ ```
157
+
158
+ Integration tests are opt-in:
159
+
160
+ ```bash
161
+ COMINTY_API_KEY=... COMINTY_AGENT_ID=... uv run pytest -m integration
162
+ ```
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,137 @@
1
+ # Cominty Python SDK
2
+
3
+ Official async Python client for the Cominty managed agent chat API.
4
+
5
+ ## Requirements
6
+
7
+ - Python 3.11+
8
+ - A Cominty API key
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install cominty-sdk
14
+ ```
15
+
16
+ Or with [uv](https://docs.astral.sh/uv/):
17
+
18
+ ```bash
19
+ uv add cominty-sdk
20
+ ```
21
+
22
+ ## Configuration
23
+
24
+ | Variable | Description |
25
+ |----------|-------------|
26
+ | `COMINTY_API_KEY` | API key (required) |
27
+ | `COMINTY_API_URL` | Override base URL (optional, takes priority) |
28
+ | `COMINTY_ENVIRONMENT` | `dev`, `staging`, or `production` (default: `production`) |
29
+ | `COMINTY_AGENT_ID` | Default agent ID for chat operations |
30
+ | `COMINTY_MAX_RETRIES` | Max retries on transient errors (default: `3`) |
31
+ | `COMINTY_TIMEOUT` | Request timeout in seconds (default: `60`) |
32
+
33
+ Default base URLs are placeholders and can be overridden with `COMINTY_API_URL`:
34
+
35
+ - `dev`: `https://api.dev.cominty.com`
36
+ - `staging`: `https://api.staging.cominty.com`
37
+ - `production`: `https://api.cominty.com`
38
+
39
+ Authentication uses the `x-cominty-token` header.
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ import asyncio
45
+
46
+ from cominty_sdk import AsyncCominty, HumanMessage
47
+
48
+
49
+ async def main() -> None:
50
+ async with AsyncCominty() as client:
51
+ thread, reply = await client.chat.start_and_wait(
52
+ HumanMessage(content="What is Cominty?"),
53
+ agent_id="your-agent-id",
54
+ )
55
+ print(reply.content)
56
+ print(reply.tool_names)
57
+
58
+
59
+ asyncio.run(main())
60
+ ```
61
+
62
+ ## Send a message in an existing thread
63
+
64
+ ```python
65
+ message = await client.messages.send_and_wait(
66
+ thread_id=thread.id,
67
+ message=HumanMessage(
68
+ content="Search our docs for onboarding steps",
69
+ source_ids=[42],
70
+ disabled_tools=["web"],
71
+ ),
72
+ agent_id="your-agent-id",
73
+ )
74
+ ```
75
+
76
+ ## Upload a file
77
+
78
+ Upload is a single high-level call that performs the 3-step S3 flow internally:
79
+
80
+ ```python
81
+ file_id = await client.files.upload("report.pdf")
82
+
83
+ await client.messages.send_and_wait(
84
+ thread_id=thread.id,
85
+ message=HumanMessage(content="Summarize this file", file_ids=[file_id]),
86
+ agent_id="your-agent-id",
87
+ )
88
+ ```
89
+
90
+ ## Streaming
91
+
92
+ The API returns JSONL events on the stream endpoint:
93
+
94
+ ```python
95
+ async for event in client.messages.stream(message.id):
96
+ print(event)
97
+ ```
98
+
99
+ ## QA helpers
100
+
101
+ `MessageOut` exposes convenience accessors for automated QA:
102
+
103
+ ```python
104
+ reply.tool_names # tools invoked (from events)
105
+ reply.cite_tags # raw <cite .../> tags
106
+ reply.document_citations # parsed document citations
107
+ reply.web_citations # parsed web citations
108
+ ```
109
+
110
+ ## Covered endpoints
111
+
112
+ | Resource | Methods |
113
+ |----------|---------|
114
+ | Threads | `list`, `get`, `update`, `archive` |
115
+ | Chat | `start`, `start_and_wait` |
116
+ | Messages | `send`, `send_and_wait`, `wait_until_done`, `cancel`, `export`, `stream` |
117
+ | Files | `upload`, `download` |
118
+ | Usage | `get` |
119
+
120
+ ## Development
121
+
122
+ ```bash
123
+ uv sync --all-extras --dev
124
+ uv run pytest
125
+ uv run ruff check .
126
+ uv run mypy
127
+ ```
128
+
129
+ Integration tests are opt-in:
130
+
131
+ ```bash
132
+ COMINTY_API_KEY=... COMINTY_AGENT_ID=... uv run pytest -m integration
133
+ ```
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,70 @@
1
+ [project]
2
+ name = "cominty-sdk"
3
+ version = "0.1.0"
4
+ description = "Official async Python client for the Cominty managed agent chat API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Cominty" }]
9
+ keywords = ["cominty", "sdk", "agent", "chat"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = [
21
+ "httpx>=0.27",
22
+ "pydantic>=2",
23
+ "pydantic-settings>=2",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = [
28
+ "mypy>=1.11",
29
+ "pytest>=8",
30
+ "pytest-asyncio>=0.24",
31
+ "respx>=0.21",
32
+ "ruff>=0.6",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/cominty/python-sdk"
37
+ Repository = "https://github.com/cominty/python-sdk"
38
+
39
+ [build-system]
40
+ requires = ["hatchling"]
41
+ build-backend = "hatchling.build"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/cominty_sdk"]
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ only-include = ["src/cominty_sdk", "README.md", "pyproject.toml"]
48
+
49
+ [tool.pytest.ini_options]
50
+ asyncio_mode = "auto"
51
+ asyncio_default_fixture_loop_scope = "function"
52
+ testpaths = ["tests"]
53
+ markers = [
54
+ "integration: opt-in tests requiring COMINTY_API_KEY",
55
+ ]
56
+
57
+ [tool.ruff]
58
+ target-version = "py311"
59
+ line-length = 100
60
+ src = ["src", "tests"]
61
+
62
+ [tool.ruff.lint]
63
+ select = ["E", "F", "I", "UP", "B", "SIM"]
64
+
65
+ [tool.mypy]
66
+ python_version = "3.11"
67
+ strict = true
68
+ files = ["src/cominty_sdk"]
69
+ warn_return_any = true
70
+ warn_unused_configs = true
@@ -0,0 +1,50 @@
1
+ """Cominty SDK — async Python client for the managed agent chat API."""
2
+
3
+ from cominty_sdk.client import AsyncCominty
4
+ from cominty_sdk.config import ComintyEnvironment
5
+ from cominty_sdk.exceptions import (
6
+ AuthenticationError,
7
+ ComintyAPIError,
8
+ ComintyError,
9
+ ComintyServerShuttingDownError,
10
+ ComintyTimeoutError,
11
+ NotFoundError,
12
+ RateLimitError,
13
+ ServerError,
14
+ ValidationError,
15
+ )
16
+ from cominty_sdk.models.files import ConversationFileOut
17
+ from cominty_sdk.models.messages import (
18
+ DocumentCitation,
19
+ HumanMessage,
20
+ MessageOut,
21
+ Question,
22
+ WebCitation,
23
+ )
24
+ from cominty_sdk.models.threads import ThreadOut, ThreadSummaryOut
25
+ from cominty_sdk.models.usage import UsageReport
26
+
27
+ __all__ = [
28
+ "AsyncCominty",
29
+ "AuthenticationError",
30
+ "ComintyAPIError",
31
+ "ComintyEnvironment",
32
+ "ComintyError",
33
+ "ComintyServerShuttingDownError",
34
+ "ComintyTimeoutError",
35
+ "ConversationFileOut",
36
+ "DocumentCitation",
37
+ "HumanMessage",
38
+ "MessageOut",
39
+ "NotFoundError",
40
+ "Question",
41
+ "RateLimitError",
42
+ "ServerError",
43
+ "ThreadOut",
44
+ "ThreadSummaryOut",
45
+ "UsageReport",
46
+ "ValidationError",
47
+ "WebCitation",
48
+ ]
49
+
50
+ __version__ = "0.1.0"
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, TypeVar
5
+
6
+ import httpx
7
+ from pydantic import BaseModel
8
+
9
+ from cominty_sdk.config import AUTH_HEADER, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT
10
+ from cominty_sdk.exceptions import ComintyTimeoutError, raise_for_status
11
+ from cominty_sdk.retry import compute_backoff, is_retryable_exception, maybe_raise_for_status
12
+
13
+ T = TypeVar("T", bound=BaseModel)
14
+
15
+
16
+ class AsyncHTTPClient:
17
+ """Internal async HTTP client with auth, retries, and error mapping."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ base_url: str,
23
+ api_key: str,
24
+ max_retries: int = DEFAULT_MAX_RETRIES,
25
+ timeout: float = DEFAULT_TIMEOUT,
26
+ stream_timeout: float | None = None,
27
+ ) -> None:
28
+ self.base_url = base_url.rstrip("/")
29
+ self.api_key = api_key
30
+ self.max_retries = max_retries
31
+ self.timeout = timeout
32
+ self.stream_timeout = stream_timeout or timeout
33
+ self._client = httpx.AsyncClient(
34
+ base_url=self.base_url,
35
+ timeout=httpx.Timeout(timeout),
36
+ headers={AUTH_HEADER: api_key},
37
+ )
38
+
39
+ async def close(self) -> None:
40
+ await self._client.aclose()
41
+
42
+ async def request(
43
+ self,
44
+ method: str,
45
+ path: str,
46
+ *,
47
+ params: dict[str, Any] | None = None,
48
+ json: dict[str, Any] | None = None,
49
+ headers: dict[str, str] | None = None,
50
+ parse_json: bool = True,
51
+ ) -> Any:
52
+ """Send a request with retries and return parsed JSON or raw response."""
53
+ last_exc: BaseException | None = None
54
+ for attempt in range(self.max_retries + 1):
55
+ try:
56
+ response = await self._client.request(
57
+ method,
58
+ path,
59
+ params=params,
60
+ json=json,
61
+ headers=headers,
62
+ )
63
+ body: Any
64
+ if parse_json:
65
+ body = response.json() if response.content else None
66
+ else:
67
+ body = response.content
68
+
69
+ if response.status_code >= 400:
70
+ maybe_raise_for_status(
71
+ response.status_code,
72
+ body,
73
+ f"{method} {path} failed",
74
+ )
75
+ return body
76
+ except Exception as exc:
77
+ if not is_retryable_exception(exc) or attempt >= self.max_retries:
78
+ if isinstance(exc, httpx.TimeoutException):
79
+ raise ComintyTimeoutError(str(exc)) from exc
80
+ raise
81
+ last_exc = exc
82
+ await asyncio.sleep(compute_backoff(attempt))
83
+ assert last_exc is not None
84
+ raise last_exc
85
+
86
+ async def request_model(
87
+ self,
88
+ method: str,
89
+ path: str,
90
+ model: type[T],
91
+ *,
92
+ params: dict[str, Any] | None = None,
93
+ json: dict[str, Any] | None = None,
94
+ headers: dict[str, str] | None = None,
95
+ ) -> T:
96
+ data = await self.request(method, path, params=params, json=json, headers=headers)
97
+ return model.model_validate(data)
98
+
99
+ async def request_bytes(
100
+ self,
101
+ method: str,
102
+ path: str,
103
+ *,
104
+ params: dict[str, Any] | None = None,
105
+ headers: dict[str, str] | None = None,
106
+ ) -> bytes:
107
+ last_exc: BaseException | None = None
108
+ for attempt in range(self.max_retries + 1):
109
+ try:
110
+ response = await self._client.request(
111
+ method,
112
+ path,
113
+ params=params,
114
+ headers=headers,
115
+ )
116
+ if response.status_code >= 400:
117
+ body: Any = None
118
+ if response.content:
119
+ try:
120
+ body = response.json()
121
+ except Exception:
122
+ body = response.text
123
+ raise_for_status(
124
+ response.status_code,
125
+ body,
126
+ f"{method} {path} failed",
127
+ )
128
+ return response.content
129
+ except Exception as exc:
130
+ if not is_retryable_exception(exc) or attempt >= self.max_retries:
131
+ if isinstance(exc, httpx.TimeoutException):
132
+ raise ComintyTimeoutError(str(exc)) from exc
133
+ raise
134
+ last_exc = exc
135
+ await asyncio.sleep(compute_backoff(attempt))
136
+ assert last_exc is not None
137
+ raise last_exc
138
+
139
+ async def stream_request(
140
+ self,
141
+ method: str,
142
+ path: str,
143
+ *,
144
+ params: dict[str, Any] | None = None,
145
+ headers: dict[str, str] | None = None,
146
+ ) -> httpx.Response:
147
+ """Open a streaming HTTP response (caller must close context)."""
148
+ response = await self._client.stream(
149
+ method,
150
+ path,
151
+ params=params,
152
+ headers=headers,
153
+ timeout=httpx.Timeout(self.stream_timeout),
154
+ ).__aenter__()
155
+ if response.status_code >= 400:
156
+ await response.aread()
157
+ body: Any = None
158
+ if response.content:
159
+ try:
160
+ body = response.json()
161
+ except Exception:
162
+ body = response.text
163
+ raise_for_status(response.status_code, body, f"{method} {path} failed")
164
+ return response
165
+
166
+ @property
167
+ def raw_client(self) -> httpx.AsyncClient:
168
+ """Expose the underlying httpx client for non-API requests (e.g. S3 upload)."""
169
+ return self._client
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from collections.abc import AsyncIterator
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ StreamEvent = dict[str, Any]
11
+
12
+ DOCUMENT_CITE_PATTERN = re.compile(
13
+ r'<cite\s+document_id="([^"]+)"\s+pages="([^"]+)"\s+name="([^"]+)"\s*/>',
14
+ )
15
+ WEB_CITE_PATTERN = re.compile(r'<cite\s+url="(https?://[^"]+)"\s*/>')
16
+ CITE_TAG_PATTERN = re.compile(r"<cite[^>]*/>")
17
+
18
+
19
+ def extract_tool_names(events: list[dict[str, Any]] | None) -> list[str]:
20
+ """Extract tool names from message events, deduplicated preserving order."""
21
+ if not events:
22
+ return []
23
+ names: list[str] = []
24
+ seen: set[str] = set()
25
+ for event in events:
26
+ name = _extract_tool_name_from_event(event)
27
+ if name and name not in seen:
28
+ seen.add(name)
29
+ names.append(name)
30
+ return names
31
+
32
+
33
+ def _extract_tool_name_from_event(event: dict[str, Any]) -> str | None:
34
+ for key in ("tool_name", "tool", "name"):
35
+ value = event.get(key)
36
+ if isinstance(value, str) and value:
37
+ return value
38
+ event_type = event.get("type") or event.get("event")
39
+ if event_type in ("tool_call", "tool_use", "tool"):
40
+ for key in ("tool_name", "tool", "name"):
41
+ value = event.get(key)
42
+ if isinstance(value, str) and value:
43
+ return value
44
+ data = event.get("data")
45
+ if isinstance(data, dict):
46
+ return _extract_tool_name_from_event(data)
47
+ return None
48
+
49
+
50
+ def extract_cite_tags(content: str) -> list[str]:
51
+ """Return raw <cite .../> tags found in message content."""
52
+ return CITE_TAG_PATTERN.findall(content)
53
+
54
+
55
+ def parse_document_citations(content: str) -> list[dict[str, str]]:
56
+ """Parse document citation tags from content."""
57
+ return [
58
+ {"document_id": m.group(1), "pages": m.group(2), "name": m.group(3)}
59
+ for m in DOCUMENT_CITE_PATTERN.finditer(content)
60
+ ]
61
+
62
+
63
+ def parse_web_citations(content: str) -> list[dict[str, str]]:
64
+ """Parse web citation tags from content."""
65
+ return [{"url": m.group(1)} for m in WEB_CITE_PATTERN.finditer(content)]
66
+
67
+
68
+ async def iter_jsonl_events(response: httpx.Response) -> AsyncIterator[StreamEvent]:
69
+ """Parse a JSONL stream into async event dicts."""
70
+ async for line in response.aiter_lines():
71
+ stripped = line.strip()
72
+ if not stripped:
73
+ continue
74
+ yield json.loads(stripped)
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator
4
+ from typing import TYPE_CHECKING
5
+
6
+ from cominty_sdk._qa import StreamEvent, iter_jsonl_events
7
+
8
+ if TYPE_CHECKING:
9
+ from cominty_sdk._http import AsyncHTTPClient
10
+
11
+
12
+ async def stream_message_events(
13
+ http: AsyncHTTPClient,
14
+ message_id: str,
15
+ *,
16
+ last_event_id: str | None = None,
17
+ ) -> AsyncIterator[StreamEvent]:
18
+ """Stream JSONL events for a message."""
19
+ headers: dict[str, str] = {}
20
+ if last_event_id is not None:
21
+ headers["last-event-id"] = last_event_id
22
+
23
+ response = await http.stream_request(
24
+ "GET",
25
+ f"/chat/messages/{message_id}/stream",
26
+ headers=headers or None,
27
+ )
28
+ try:
29
+ async for event in iter_jsonl_events(response):
30
+ yield event
31
+ finally:
32
+ await response.aclose()