creativai 0.1.0__py3-none-any.whl

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.
creativai/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """
2
+ CreativAI — Official Python SDK for the CreativAI Video Intelligence Platform.
3
+
4
+ Quickstart::
5
+
6
+ import creativai
7
+
8
+ client = creativai.CreativAI(api_key="sk_live_...")
9
+
10
+ # Or use the environment variable CREATIVAI_API_KEY:
11
+ client = creativai.CreativAI()
12
+
13
+ # Health check
14
+ client.health.check()
15
+
16
+ # Account info
17
+ info = client.users.get_users_info()
18
+ print(info["credits"])
19
+
20
+ # Create a collection and upload a video
21
+ collection = client.collections.create("my-collection")
22
+ cid = collection["collection_id"]
23
+ client.media.upload_file(cid, "/path/to/video.mp4")
24
+
25
+ # Start indexing
26
+ job = client.indexing.start(cid)
27
+
28
+ # Search
29
+ results = client.search.query(cid, "person near forklift")
30
+ """
31
+
32
+ from .client import CreativAI
33
+ from .exceptions import (
34
+ APIError,
35
+ AuthenticationError,
36
+ CreativAIError,
37
+ InsufficientCreditsError,
38
+ NotFoundError,
39
+ PermissionError,
40
+ RateLimitError,
41
+ ServerError,
42
+ StreamingError,
43
+ TimeoutError,
44
+ ValidationError,
45
+ )
46
+
47
+ __all__ = [
48
+ "CreativAI",
49
+ # Exceptions
50
+ "CreativAIError",
51
+ "APIError",
52
+ "AuthenticationError",
53
+ "PermissionError",
54
+ "NotFoundError",
55
+ "RateLimitError",
56
+ "InsufficientCreditsError",
57
+ "ValidationError",
58
+ "ServerError",
59
+ "TimeoutError",
60
+ "StreamingError",
61
+ ]
62
+
63
+ __version__ = "0.1.0"
creativai/_http.py ADDED
@@ -0,0 +1,208 @@
1
+ """
2
+ CreativAI Python SDK — internal HTTP layer
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ from typing import Any, Generator, Iterator
9
+
10
+ import httpx
11
+
12
+ from .exceptions import (
13
+ APIError,
14
+ AuthenticationError,
15
+ InsufficientCreditsError,
16
+ NotFoundError,
17
+ PermissionError,
18
+ RateLimitError,
19
+ ServerError,
20
+ StreamingError,
21
+ ValidationError,
22
+ )
23
+
24
+ _DEFAULT_BASE_URL = "https://creativai-apis.com"
25
+ _DEFAULT_TIMEOUT = 120.0 # seconds
26
+
27
+
28
+ class _HTTPClient:
29
+ """Thin wrapper around httpx that handles auth, error parsing, and SSE."""
30
+
31
+ def __init__(
32
+ self,
33
+ api_key: str,
34
+ base_url: str = _DEFAULT_BASE_URL,
35
+ timeout: float = _DEFAULT_TIMEOUT,
36
+ ) -> None:
37
+ self._api_key = api_key
38
+ self._base_url = base_url.rstrip("/")
39
+ try:
40
+ from importlib.metadata import version as _pkg_version
41
+ _sdk_version = _pkg_version("creativai")
42
+ except Exception:
43
+ _sdk_version = "0.1.0"
44
+ self._client = httpx.Client(
45
+ base_url=self._base_url,
46
+ headers={
47
+ "X-API-Key": api_key,
48
+ "Accept": "application/json",
49
+ "User-Agent": f"creativai-python/{_sdk_version}",
50
+ },
51
+ timeout=timeout,
52
+ )
53
+
54
+ # ------------------------------------------------------------------
55
+ # Public request helpers
56
+ # ------------------------------------------------------------------
57
+
58
+ def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any:
59
+ response = self._client.get(path, params=params)
60
+ return self._parse(response)
61
+
62
+ def post(
63
+ self,
64
+ path: str,
65
+ *,
66
+ json: Any = None,
67
+ data: dict[str, Any] | None = None,
68
+ files: Any = None,
69
+ params: dict[str, Any] | None = None,
70
+ ) -> Any:
71
+ response = self._client.post(path, json=json, data=data, files=files, params=params)
72
+ return self._parse(response)
73
+
74
+ def patch(self, path: str, *, json: Any = None) -> Any:
75
+ response = self._client.patch(path, json=json)
76
+ return self._parse(response)
77
+
78
+ def delete(self, path: str, *, json: Any = None, params: dict[str, Any] | None = None) -> Any:
79
+ response = self._client.delete(path, json=json, params=params)
80
+ return self._parse(response)
81
+
82
+ def put(self, path: str, *, json: Any = None, content: bytes | None = None, headers: dict[str, str] | None = None) -> Any:
83
+ """Used for presigned S3 PUT uploads — does NOT send API key."""
84
+ extra_headers = headers or {}
85
+ response = httpx.put(path, json=json, content=content, headers=extra_headers, timeout=_DEFAULT_TIMEOUT)
86
+ if response.status_code >= 400:
87
+ raise APIError(
88
+ f"S3 upload failed: HTTP {response.status_code}",
89
+ status_code=response.status_code,
90
+ )
91
+ return response
92
+
93
+ # ------------------------------------------------------------------
94
+ # SSE streaming
95
+ # ------------------------------------------------------------------
96
+
97
+ def stream_post(
98
+ self, path: str, *, json: Any = None
99
+ ) -> Iterator[dict[str, str]]:
100
+ """
101
+ Opens an SSE connection via POST and yields raw SSE event dicts
102
+ with keys ``event`` and ``data``.
103
+
104
+ Usage::
105
+
106
+ for event in client.stream_post("/api/v2/agentic-chat/sessions/xxx/chat", json={...}):
107
+ print(event["event"], event["data"])
108
+ """
109
+ try:
110
+ from httpx_sse import connect_sse
111
+ except ImportError as exc:
112
+ raise StreamingError(
113
+ "httpx-sse is required for streaming. "
114
+ "Install it with: pip install httpx-sse"
115
+ ) from exc
116
+
117
+ headers = {
118
+ "X-API-Key": self._api_key,
119
+ "Content-Type": "application/json",
120
+ "Accept": "text/event-stream",
121
+ "User-Agent": "creativai-python/0.1.0",
122
+ }
123
+ try:
124
+ with httpx.Client(base_url=self._base_url, timeout=None) as client:
125
+ with connect_sse(client, "POST", path, json=json, headers=headers) as event_source:
126
+ for sse in event_source.iter_sse():
127
+ yield {"event": sse.event, "data": sse.data}
128
+ except httpx.HTTPStatusError as exc:
129
+ self._raise_for_status(exc.response)
130
+ except httpx.TimeoutException as exc:
131
+ from .exceptions import TimeoutError as _TimeoutError
132
+ raise _TimeoutError(str(exc)) from exc
133
+
134
+ def stream_get(self, path: str, *, params: dict[str, Any] | None = None) -> Iterator[dict[str, str]]:
135
+ """Opens an SSE connection via GET and yields raw SSE event dicts."""
136
+ try:
137
+ from httpx_sse import connect_sse
138
+ except ImportError as exc:
139
+ raise StreamingError(
140
+ "httpx-sse is required for streaming. "
141
+ "Install it with: pip install httpx-sse"
142
+ ) from exc
143
+
144
+ headers = {
145
+ "X-API-Key": self._api_key,
146
+ "Accept": "text/event-stream",
147
+ "User-Agent": "creativai-python/0.1.0",
148
+ }
149
+ try:
150
+ with httpx.Client(base_url=self._base_url, timeout=None) as client:
151
+ with connect_sse(client, "GET", path, params=params, headers=headers) as event_source:
152
+ for sse in event_source.iter_sse():
153
+ yield {"event": sse.event, "data": sse.data}
154
+ except httpx.HTTPStatusError as exc:
155
+ self._raise_for_status(exc.response)
156
+ except httpx.TimeoutException as exc:
157
+ from .exceptions import TimeoutError as _TimeoutError
158
+ raise _TimeoutError(str(exc)) from exc
159
+
160
+ # ------------------------------------------------------------------
161
+ # Internal helpers
162
+ # ------------------------------------------------------------------
163
+
164
+ def _parse(self, response: httpx.Response) -> Any:
165
+ if response.status_code >= 400:
166
+ self._raise_for_status(response)
167
+ try:
168
+ body = response.json()
169
+ except Exception:
170
+ return response.content
171
+ # Unwrap the standard {"success": true, "data": ..., "error": ...} envelope
172
+ if isinstance(body, dict) and "data" in body:
173
+ return body["data"]
174
+ return body
175
+
176
+ @staticmethod
177
+ def _raise_for_status(response: httpx.Response) -> None:
178
+ status = response.status_code
179
+ try:
180
+ body = response.json()
181
+ err = body.get("error") or {}
182
+ message = err.get("message") or body.get("detail") or response.text
183
+ code = err.get("code")
184
+ details = err.get("details")
185
+ except Exception:
186
+ message = response.text
187
+ code = None
188
+ details = None
189
+
190
+ exc_map = {
191
+ 401: AuthenticationError,
192
+ 402: InsufficientCreditsError,
193
+ 403: PermissionError,
194
+ 404: NotFoundError,
195
+ 422: ValidationError,
196
+ 429: RateLimitError,
197
+ }
198
+ cls = exc_map.get(status) or (ServerError if status >= 500 else ValidationError if status == 400 else APIError)
199
+ raise cls(message, status_code=status, code=code, details=details)
200
+
201
+ def close(self) -> None:
202
+ self._client.close()
203
+
204
+ def __enter__(self) -> "_HTTPClient":
205
+ return self
206
+
207
+ def __exit__(self, *args: Any) -> None:
208
+ self.close()
creativai/client.py ADDED
@@ -0,0 +1,161 @@
1
+ """
2
+ CreativAI Python SDK — Main client
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ from typing import Any
9
+
10
+ from ._http import _HTTPClient, _DEFAULT_BASE_URL, _DEFAULT_TIMEOUT
11
+ from .resources import (
12
+ AgenticChat,
13
+ Chat,
14
+ Collections,
15
+ DataPlates,
16
+ Health,
17
+ Indexing,
18
+ Invoices,
19
+ Jobs,
20
+ KnowledgeExtraction,
21
+ LiveStream,
22
+ Media,
23
+ OnlineSearch,
24
+ Organizations,
25
+ Projects,
26
+ Search,
27
+ Sharing,
28
+ SubPlates,
29
+ Subscriptions,
30
+ Tasks,
31
+ Transactions,
32
+ Transfers,
33
+ UploadIntegrations,
34
+ Uploads,
35
+ Users,
36
+ YouTubeSearch,
37
+ )
38
+
39
+
40
+ class CreativAI:
41
+ """The main entry point for the CreativAI Python SDK.
42
+
43
+ Instantiate with your API key (or set the ``CREATIVAI_API_KEY`` environment
44
+ variable) and start using the namespaced resource clients.
45
+
46
+ Example::
47
+
48
+ import creativai
49
+
50
+ client = creativai.CreativAI(api_key="sk_live_...")
51
+
52
+ # Create a collection
53
+ collection = client.collections.create("my-videos")
54
+ cid = collection["collection_id"]
55
+
56
+ # Upload a local file
57
+ client.media.upload_file(cid, "clip.mp4")
58
+
59
+ # Index it
60
+ job = client.indexing.start(cid)
61
+ print(job["indexing_id"])
62
+
63
+ # Search
64
+ results = client.search.query(cid, "person walking near forklift")
65
+
66
+ # Agentic chat
67
+ session = client.agentic_chat.create_session(cid)
68
+ for event in client.agentic_chat.chat(session["session_id"], "What are the most common activities?"):
69
+ if event["event"] == "answer":
70
+ print(event["data"].get("text", ""))
71
+
72
+ Args:
73
+ api_key: Your CreativAI API key (``sk_live_...``). Falls back to the
74
+ ``CREATIVAI_API_KEY`` environment variable.
75
+ base_url: Override the default API base URL.
76
+ timeout: Request timeout in seconds (default 120).
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ api_key: str | None = None,
82
+ *,
83
+ base_url: str = _DEFAULT_BASE_URL,
84
+ timeout: float = _DEFAULT_TIMEOUT,
85
+ ) -> None:
86
+ resolved_key = api_key or os.environ.get("CREATIVAI_API_KEY")
87
+ if not resolved_key:
88
+ raise ValueError(
89
+ "An API key is required. Pass api_key= or set the CREATIVAI_API_KEY "
90
+ "environment variable. Keys begin with 'sk_live_'."
91
+ )
92
+ self._http = _HTTPClient(resolved_key, base_url=base_url, timeout=timeout)
93
+
94
+ # Resource clients — each maps to a logical section of the API
95
+ self.health = Health(self._http)
96
+ self.organizations = Organizations(self._http)
97
+ self.projects = Projects(self._http)
98
+ self.collections = Collections(self._http)
99
+ self.media = Media(self._http)
100
+ self.uploads = Uploads(self._http)
101
+ self.transfers = Transfers(self._http)
102
+ self.indexing = Indexing(self._http)
103
+ self.search = Search(self._http)
104
+ self.data_plates = DataPlates(self._http)
105
+ self.sub_plates = SubPlates(self._http)
106
+ self.knowledge_extraction = KnowledgeExtraction(self._http)
107
+ self.chat = Chat(self._http)
108
+ self.agentic_chat = AgenticChat(self._http)
109
+ self.jobs = Jobs(self._http)
110
+ self.sharing = Sharing(self._http)
111
+ self.tasks = Tasks(self._http)
112
+ self.live_stream = LiveStream(self._http)
113
+ self.online_search = OnlineSearch(self._http)
114
+ self.youtube_search = YouTubeSearch(self._http)
115
+ self.transactions = Transactions(self._http)
116
+ self.users = Users(self._http)
117
+ self.subscriptions = Subscriptions(self._http)
118
+ self.invoices = Invoices(self._http)
119
+ self.upload_integrations = UploadIntegrations(self._http)
120
+
121
+ def close(self) -> None:
122
+ """Close the underlying HTTP connection pool."""
123
+ self._http.close()
124
+
125
+ def as_mcp_server(self) -> Any:
126
+ """Return a FastMCP server instance pre-wired to this client.
127
+
128
+ Requires the ``creativai-mcp`` package to be installed::
129
+
130
+ pip install creativai-mcp
131
+
132
+ Example::
133
+
134
+ import creativai, uvicorn
135
+ from starlette.applications import Starlette
136
+
137
+ client = creativai.CreativAI()
138
+ mcp_server = client.as_mcp_server()
139
+ # Run stdio (for Claude Desktop / Cursor):
140
+ mcp_server.run(transport="stdio")
141
+ # Or mount as ASGI for HTTP/SSE:
142
+ app = mcp_server.sse_app()
143
+ uvicorn.run(app, port=8090)
144
+ """
145
+ try:
146
+ from creativai_mcp.server import build_server # type: ignore[import]
147
+ except ImportError as exc:
148
+ raise ImportError(
149
+ "The 'creativai-mcp' package is required to use as_mcp_server(). "
150
+ "Install it with: pip install creativai-mcp"
151
+ ) from exc
152
+ return build_server(api_key=self._http._api_key)
153
+
154
+ def __enter__(self) -> "CreativAI":
155
+ return self
156
+
157
+ def __exit__(self, *args: Any) -> None:
158
+ self.close()
159
+
160
+ def __repr__(self) -> str:
161
+ return f"CreativAI(base_url={self._http._base_url!r})"
@@ -0,0 +1,78 @@
1
+ """
2
+ CreativAI Python SDK — exceptions
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from typing import Any
7
+
8
+
9
+ class CreativAIError(Exception):
10
+ """Base exception for all CreativAI SDK errors."""
11
+
12
+ def __init__(self, message: str, *, code: str | None = None, details: Any = None) -> None:
13
+ super().__init__(message)
14
+ self.message = message
15
+ self.code = code
16
+ self.details = details
17
+
18
+ def __repr__(self) -> str:
19
+ return f"{self.__class__.__name__}(message={self.message!r}, code={self.code!r})"
20
+
21
+
22
+ class APIError(CreativAIError):
23
+ """Raised when the API returns an error response."""
24
+
25
+ def __init__(
26
+ self,
27
+ message: str,
28
+ *,
29
+ status_code: int,
30
+ code: str | None = None,
31
+ details: Any = None,
32
+ ) -> None:
33
+ super().__init__(message, code=code, details=details)
34
+ self.status_code = status_code
35
+
36
+ def __repr__(self) -> str:
37
+ return (
38
+ f"{self.__class__.__name__}("
39
+ f"status_code={self.status_code}, "
40
+ f"message={self.message!r}, "
41
+ f"code={self.code!r})"
42
+ )
43
+
44
+
45
+ class AuthenticationError(APIError):
46
+ """Raised for 401 Unauthorized — invalid or missing API key."""
47
+
48
+
49
+ class PermissionError(APIError):
50
+ """Raised for 403 Forbidden — caller lacks permission."""
51
+
52
+
53
+ class NotFoundError(APIError):
54
+ """Raised for 404 Not Found."""
55
+
56
+
57
+ class RateLimitError(APIError):
58
+ """Raised for 429 Too Many Requests."""
59
+
60
+
61
+ class InsufficientCreditsError(APIError):
62
+ """Raised for 402 Payment Required — not enough credits."""
63
+
64
+
65
+ class ValidationError(APIError):
66
+ """Raised for 400/422 validation failures."""
67
+
68
+
69
+ class ServerError(APIError):
70
+ """Raised for 5xx server-side errors."""
71
+
72
+
73
+ class TimeoutError(CreativAIError):
74
+ """Raised when a request times out."""
75
+
76
+
77
+ class StreamingError(CreativAIError):
78
+ """Raised when an SSE streaming connection fails."""
@@ -0,0 +1,53 @@
1
+ from .health import Health
2
+ from .organizations import Organizations
3
+ from .projects import Projects
4
+ from .collections import Collections
5
+ from .media import Media
6
+ from .uploads import Uploads
7
+ from .transfers import Transfers
8
+ from .indexing import Indexing
9
+ from .search import Search
10
+ from .data_plates import DataPlates
11
+ from .sub_plates import SubPlates
12
+ from .knowledge_extraction import KnowledgeExtraction
13
+ from .chat import Chat
14
+ from .agentic_chat import AgenticChat
15
+ from .jobs import Jobs
16
+ from .sharing import Sharing
17
+ from .tasks import Tasks
18
+ from .live_stream import LiveStream
19
+ from .online_search import OnlineSearch
20
+ from .youtube_search import YouTubeSearch
21
+ from .transactions import Transactions
22
+ from .users import Users
23
+ from .subscriptions import Subscriptions
24
+ from .invoices import Invoices
25
+ from .upload_integrations import UploadIntegrations
26
+
27
+ __all__ = [
28
+ "Health",
29
+ "Organizations",
30
+ "Projects",
31
+ "Collections",
32
+ "Media",
33
+ "Uploads",
34
+ "Transfers",
35
+ "Indexing",
36
+ "Search",
37
+ "DataPlates",
38
+ "SubPlates",
39
+ "KnowledgeExtraction",
40
+ "Chat",
41
+ "AgenticChat",
42
+ "Jobs",
43
+ "Sharing",
44
+ "Tasks",
45
+ "LiveStream",
46
+ "OnlineSearch",
47
+ "YouTubeSearch",
48
+ "Transactions",
49
+ "Users",
50
+ "Subscriptions",
51
+ "Invoices",
52
+ "UploadIntegrations",
53
+ ]
@@ -0,0 +1,122 @@
1
+ """
2
+ CreativAI SDK — Agentic Chat resource (SSE streaming)
3
+ """
4
+
5
+ from __future__ import annotations
6
+ import json
7
+ from typing import Any, Iterator
8
+ from .._http import _HTTPClient
9
+
10
+ _PREFIX = "/api/v2/agentic-chat"
11
+
12
+
13
+ class AgenticChat:
14
+ def __init__(self, http: _HTTPClient) -> None:
15
+ self._http = http
16
+
17
+ # ------------------------------------------------------------------
18
+ # Session management
19
+ # ------------------------------------------------------------------
20
+
21
+ def create_session(self, collection_id: str, *, title: str | None = None, **kwargs: Any) -> Any:
22
+ """POST /api/v2/agentic-chat/sessions — Create a new agentic chat session (HTTP 201)."""
23
+ body: dict[str, Any] = {"collection_id": collection_id, **kwargs}
24
+ if title is not None:
25
+ body["title"] = title
26
+ return self._http.post(f"{_PREFIX}/sessions", json=body)
27
+
28
+ def list_sessions(self, *, collection_id: str | None = None, limit: int = 20, **kwargs: Any) -> Any:
29
+ """GET /api/v2/agentic-chat/sessions — List sessions, optionally filtered by collection."""
30
+ params: dict[str, Any] = {"limit": limit, **kwargs}
31
+ if collection_id is not None:
32
+ params["collection_id"] = collection_id
33
+ return self._http.get(f"{_PREFIX}/sessions", params=params)
34
+
35
+ def get_session(self, session_id: str) -> Any:
36
+ """GET /api/v2/agentic-chat/sessions/{session_id} — Get session details."""
37
+ return self._http.get(f"{_PREFIX}/sessions/{session_id}")
38
+
39
+ def delete_session(self, session_id: str) -> Any:
40
+ """DELETE /api/v2/agentic-chat/sessions/{session_id} — Delete a session."""
41
+ return self._http.delete(f"{_PREFIX}/sessions/{session_id}")
42
+
43
+ def get_messages(self, session_id: str) -> Any:
44
+ """GET /api/v2/agentic-chat/sessions/{session_id}/messages — Get full message history and session state."""
45
+ return self._http.get(f"{_PREFIX}/sessions/{session_id}/messages")
46
+
47
+ def get_status(self, session_id: str) -> Any:
48
+ """GET /api/v2/agentic-chat/sessions/{session_id}/status — Check current agent status."""
49
+ return self._http.get(f"{_PREFIX}/sessions/{session_id}/status")
50
+
51
+ def get_events(self, session_id: str, **params: Any) -> Any:
52
+ """GET /api/v2/agentic-chat/sessions/{session_id}/events — Poll new agent events since last call."""
53
+ return self._http.get(f"{_PREFIX}/sessions/{session_id}/events", params=params or None)
54
+
55
+ def stop(self, session_id: str, **kwargs: Any) -> Any:
56
+ """POST /api/v2/agentic-chat/sessions/{session_id}/stop — Stop the agent at the current step."""
57
+ return self._http.post(f"{_PREFIX}/sessions/{session_id}/stop", json=kwargs or None)
58
+
59
+ def resume(self, session_id: str, **kwargs: Any) -> Any:
60
+ """POST /api/v2/agentic-chat/sessions/{session_id}/resume — Resume an interrupted agent."""
61
+ return self._http.post(f"{_PREFIX}/sessions/{session_id}/resume", json=kwargs or None)
62
+
63
+ def search_feedback(self, session_id: str, feedback: Any, **kwargs: Any) -> Any:
64
+ """POST .../search-feedback — Respond to a ``search_feedback_required`` interrupt."""
65
+ return self._http.post(
66
+ f"{_PREFIX}/sessions/{session_id}/search-feedback",
67
+ json={"feedback": feedback, **kwargs},
68
+ )
69
+
70
+ # ------------------------------------------------------------------
71
+ # Streaming chat
72
+ # ------------------------------------------------------------------
73
+
74
+ def chat(
75
+ self,
76
+ session_id: str,
77
+ message: str,
78
+ **kwargs: Any,
79
+ ) -> Iterator[dict[str, Any]]:
80
+ """POST .../chat — Send a message and stream SSE events from the agent.
81
+
82
+ Yields parsed event dicts with at minimum ``event`` (event type) and
83
+ ``data`` (parsed JSON payload) keys.
84
+
85
+ SSE event types (non-exhaustive):
86
+ ``thinking``, ``search``, ``create_plate``, ``extract_knowledge``,
87
+ ``web_search``, ``youtube_search``, ``synthesize``,
88
+ ``answer``, ``done``, ``error``, ``search_feedback_required``
89
+
90
+ Example::
91
+
92
+ for event in client.agentic_chat.chat(session_id, "What activities are common?"):
93
+ if event["event"] == "answer":
94
+ print(event["data"]["text"])
95
+
96
+ Args:
97
+ session_id: Active session identifier.
98
+ message: User message text.
99
+ """
100
+ body: dict[str, Any] = {"message": message, **kwargs}
101
+ for raw in self._http.stream_post(f"{_PREFIX}/sessions/{session_id}/chat", json=body):
102
+ event_type = raw["event"]
103
+ raw_data = raw["data"]
104
+ try:
105
+ data = json.loads(raw_data) if raw_data else {}
106
+ except json.JSONDecodeError:
107
+ data = {"text": raw_data}
108
+ yield {"event": event_type, "data": data}
109
+
110
+ def subscribe_stream(self, session_id: str) -> Iterator[dict[str, Any]]:
111
+ """GET .../stream — Subscribe to a running agent's SSE stream (reconnect-safe).
112
+
113
+ Yields the same parsed event dicts as :meth:`chat`.
114
+ """
115
+ for raw in self._http.stream_get(f"{_PREFIX}/sessions/{session_id}/stream"):
116
+ event_type = raw["event"]
117
+ raw_data = raw["data"]
118
+ try:
119
+ data = json.loads(raw_data) if raw_data else {}
120
+ except json.JSONDecodeError:
121
+ data = {"text": raw_data}
122
+ yield {"event": event_type, "data": data}