olbrain-python-sdk 0.3.2__py3-none-any.whl → 0.5.1__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.
olbrain/AGENTS.md CHANGED
@@ -7,9 +7,20 @@ taken from the source. If it disagrees with the web docs, this file is right.
7
7
  Find it from Python with:
8
8
  `python -c "import olbrain, os; print(os.path.join(os.path.dirname(olbrain.__file__), 'AGENTS.md'))"`
9
9
 
10
- The Python SDK covers **conversational agents** only (`AgentClient`). Workflow
11
- runs, approvals and research agents are available only in the JavaScript SDK
12
- (`@olbrain/js-sdk`, class `Olbrain`).
10
+ ## Pick the right client
11
+
12
+ | You want to… | Use | Auth |
13
+ |---|---|---|
14
+ | Chat with a conversational agent (send a message, get a reply) | `AgentClient` | `api_key="ak_…"` (the only kind issued today; `sk_live_`, `org_live_`, `sk_` and `org_` still pass validation) |
15
+ | Trigger workflow agents, read/control runs, resolve approvals | `Olbrain` | `api_key="ak_…"` **or** `get_id_token` (Olbrain sign-in) |
16
+ | Drive a research agent (sessions, streamed turns, plans, reports) | `Olbrain.research` | `get_id_token` only. An `ak_` key gets `NotFoundError` (`agent_not_found`) |
17
+
18
+ These are two separate HTTP clients with different error rules. Both default
19
+ to `https://webhook.olbrain.com`. The JavaScript SDK (`@olbrain/js-sdk`) has
20
+ the same two clients plus a browser `ChatWidget`.
21
+
22
+ Keep the `ak_` key on your server (an environment variable such as
23
+ `OLBRAIN_API_KEY`). Never send it to a browser.
13
24
 
14
25
  ## Install and import
15
26
 
@@ -18,10 +29,14 @@ pip install olbrain-python-sdk # import name: olbrain. Requires Python 3.7+ an
18
29
  ```
19
30
 
20
31
  ```python
21
- from olbrain import AgentClient, ChatResponse, OlbrainError
32
+ from olbrain import AgentClient, Olbrain, OlbrainError
22
33
  ```
23
34
 
24
- ## Quick start
35
+ Both clients are synchronous and use `requests`.
36
+
37
+ ---
38
+
39
+ ## 1. `AgentClient` — conversational agents
25
40
 
26
41
  ```python
27
42
  import os
@@ -36,57 +51,38 @@ with AgentClient(agent_id="your-agent-id", api_key=os.environ["OLBRAIN_API_KEY"]
36
51
  print("agent error:", reply.error)
37
52
  ```
38
53
 
39
- The client is synchronous and uses `requests`.
40
-
41
- ## `AgentClient(agent_id, api_key, agent_url=None)`
54
+ ### `AgentClient(agent_id, api_key, agent_url=None)`
42
55
 
43
56
  - `agent_id`: required. An empty value raises `ValueError`.
44
- - `api_key`: must start with `sk_live_`, `org_live_`, `sk_` or `org_`, or the
45
- constructor raises `ValueError`. `ak_` keys are **not** accepted here.
46
- - `agent_url`: optional. Defaults to `https://olbrain-agent-cloud-768934887465.us-central1.run.app`.
57
+ - `api_key`: must start with `ak_`, `sk_live_`, `org_live_`, `sk_` or `org_`,
58
+ or the constructor raises `ValueError`.
59
+ - `agent_url`: optional. Defaults to the `OLBRAIN_AGENT_URL` environment
60
+ variable if set, otherwise `https://webhook.olbrain.com`.
47
61
 
48
- Every call is a `POST {agent_url}/api/agent/webhook` with headers
49
- `Authorization: Bearer <api_key>` and `X-Agent-ID: <agent_id>`. The client is a
50
- context manager, and `__exit__` calls `close()`.
62
+ Chat calls are `POST {agent_url}/api/agents/webhook/{agent_id}` with
63
+ `Authorization: Bearer <api_key>`. Session reads are
64
+ `GET {agent_url}/api/agents/{agent_id}/sessions/{session_id}[/messages]`. The
65
+ client is a context manager, and `__exit__` calls `close()`.
51
66
 
52
67
  ### Methods
53
68
 
54
69
  | Method | Returns | Notes |
55
70
  |---|---|---|
56
- | `create_session(on_message=None, title=None, user_id=None, metadata=None, mode="production", description=None)` | `str` session id | Sends `title` (default `"New Session"`) to the agent **as the first message**. `mode` must be `"development"`, `"testing"` or `"production"`, otherwise `ValueError`. `description` is stored as `metadata["session_description"]`. If you pass `on_message`, it also calls `listen()`. Timeout 30 s. |
71
+ | `create_session(on_message=None, title=None, user_id=None, metadata=None, mode="production", description=None)` | `str` session id | Sends `title` (default `"New Session"`) to the agent **as the first message**. `mode` must be `"development"`, `"testing"` or `"production"`, otherwise `ValueError`, but it is not sent: the API always serves the published agent. `description` is stored as `metadata["session_description"]`. If you pass `on_message`, it also calls `listen()`. Timeout 30 s. |
57
72
  | `send_and_wait(session_id, message, user_id=None, metadata=None, model=None, timeout=120)` | `ChatResponse` | Waits for the reply. `timeout` is in **seconds**. `model` overrides the agent's model. Empty or whitespace-only messages raise `ValueError`. |
58
73
  | `send(session_id, message, user_id=None, metadata=None, model=None, mode="production")` | `dict` (raw JSON) | Same request, but returns the raw response dict (`response`, `token_usage`, `success`, …). It still blocks, with a 120 s timeout. |
59
- | `listen(session_id, on_message)` | `None` | Starts a background daemon thread that reads SSE from `GET {agent_url}/sessions/{id}/stream`. It reconnects with backoff from 5 s up to 60 s. Calling it again for the same session does nothing. |
74
+ | `listen(session_id, on_message)` | `None` | Starts a background daemon thread that polls the session's messages every 2 s and calls `on_message` once per **new** message (messages already there are skipped). Calling it again for the same session does nothing. |
60
75
  | `run()` | `None` | Blocks until Ctrl+C, then calls `close()`. Use it to keep a script alive for `listen` callbacks. |
61
- | `close()` | `None` | Stops all streams. |
62
- | `get_session(session_id)` | `SessionInfo` | Needs backend support (webhook `action: get_session_info`). |
63
- | `update_session(session_id, title=None, metadata=None, status=None)` | `SessionInfo` | `status` must be `"active"` or `"archived"`. |
64
- | `delete_session(session_id)` | `dict` | Also stops that session's stream. |
65
- | `get_session_stats(session_id)` | `dict` | |
66
- | `get_messages(session_id, limit=20, offset=0)` | `dict` | The messages list and pagination info, as the backend returns them. |
67
-
68
- The five session-management methods (`get_session` through `get_messages`)
69
- depend on webhook `action`s that not every backend implements. They raise if
70
- the backend refuses. Only `create_session`, `send_and_wait` and `send` are
71
- guaranteed to work.
76
+ | `close()` | `None` | Stops all listeners. |
77
+ | `get_session(session_id)` | `SessionInfo` | `SessionNotFoundError` if it doesn't exist (or belongs to another agent). |
78
+ | `get_session_stats(session_id)` | `dict` | `message_count`, `token_usage`, `cost`, `created_at`, `last_message_at`. |
79
+ | `get_messages(session_id, limit=20, offset=0)` | `dict` | `{"messages": [...], "limit", "offset"}`, oldest first; `limit` at most 200. |
80
+ | `update_session(...)`, `delete_session(...)` | — | Always raise `OlbrainError`: the API has no endpoint for them. |
72
81
 
73
- ### Streaming callback
82
+ `on_message` gets each message as a raw dict (`role`, `content`, …). It runs
83
+ on the listener thread, so keep it fast and don't raise from it.
74
84
 
75
- `on_message` gets the **raw dict** from each SSE frame that has a `content`
76
- key. Frames whose `type` is `ping` or `connected` are skipped. The callback
77
- runs on the stream thread, so keep it fast and don't raise from it.
78
-
79
- ```python
80
- def on_message(msg: dict):
81
- print(f"{msg.get('role')}: {msg['content']}")
82
-
83
- client = AgentClient(agent_id="…", api_key="sk_live_…")
84
- sid = client.create_session(on_message=on_message, title="Streaming chat")
85
- client.send(sid, "Tell me a story")
86
- client.run() # blocks; Ctrl+C to stop
87
- ```
88
-
89
- ## Data classes (`olbrain.session`, re-exported from `olbrain`)
85
+ ### Data classes (`olbrain.session`, re-exported from `olbrain`)
90
86
 
91
87
  ```python
92
88
  @dataclass
@@ -126,12 +122,9 @@ class SessionInfo:
126
122
  `reply.success` too. A 200 response can carry `success=False` and an `error`.
127
123
 
128
124
  Avoid the exported `Message` and `Session` classes. Nothing in the client
129
- returns them, and `Session.get_stats()` always falls back to local data.
130
-
131
- ## Errors (`olbrain.exceptions`)
125
+ returns them.
132
126
 
133
- All of them subclass `OlbrainError`, which has `.message`, `.error_code` and
134
- `.details`.
127
+ ### Errors (`AgentClient`)
135
128
 
136
129
  | Condition | Raised |
137
130
  |---|---|
@@ -143,25 +136,131 @@ All of them subclass `OlbrainError`, which has `.message`, `.error_code` and
143
136
  | Any other non-200 (403, 5xx…) | `OlbrainError("Failed to <operation>: <status> - <detail>")` |
144
137
  | Connection error or timeout | `NetworkError` |
145
138
 
139
+ ---
140
+
141
+ ## 2. `Olbrain` — workflow agents, runs, approvals
142
+
143
+ ```python
144
+ olbrain = Olbrain(api_key=os.environ["OLBRAIN_API_KEY"]) # server / script
145
+ # olbrain = Olbrain(get_id_token=lambda: current_user_id_token()) # signed-in Olbrain user (Firebase ID token)
146
+ ```
147
+
148
+ `Olbrain(api_key=None, get_id_token=None, base_url=None, timeout=30)`:
149
+ `api_key` must start `ak_`; `get_id_token` is a callable returning the
150
+ token and is called before **every** request; `base_url` defaults to the
151
+ `OLBRAIN_AGENT_URL` environment variable if set, otherwise
152
+ `https://webhook.olbrain.com`; `timeout` is in seconds. Give **exactly one** of
153
+ `api_key` or `get_id_token`, or the constructor raises `ValidationError`.
154
+
155
+ Every method takes the agent id first and returns the API's JSON (snake_case
156
+ dicts and lists).
157
+
158
+ | Method | HTTP | Returns |
159
+ |---|---|---|
160
+ | `workflows.run(agent_id, payload=None, variables=None)` | `POST /api/workflows/{agent_id}/trigger` | `{"run_id", "status"}` |
161
+ | `runs.list(agent_id, status=None, limit=None)` | `GET /api/agents/{agent_id}/runs` | list of runs |
162
+ | `runs.get(agent_id, run_id)` | `GET …/runs/{run_id}` | run |
163
+ | `runs.cancel / pause / retry / resume(agent_id, run_id)` | `POST …/runs/{run_id}/{verb}` | dict |
164
+ | `approvals.list(agent_id, run_id)` | `GET …/runs/{run_id}/approvals` | list of approvals |
165
+ | `approvals.resolve(agent_id, run_id, exception_id, resolution, modified_data=None)` | `POST …/approvals/resolve` | `{"exception_id", "resolution", "all_resolved", "resumed"}` |
166
+
167
+ A run has `run_id`, `status`, and optionally `progress_percent`, `started_at`,
168
+ `completed_at`, `duration_seconds`, `error` and `summary`
169
+ (`total_processed`, `successful`, `failed`, `exceptions`, `skipped`). An
170
+ approval has `exception_id`, `step_id`, `item_id`, `data`, `error`,
171
+ `requires_human_review` and `blocking`. `resolution` is `"approved"`,
172
+ `"rejected"` or `"modified"`; send `modified_data` only with `"modified"`.
173
+
174
+ There is no SDK helper for waiting on a run, so poll `runs.get` yourself:
175
+
146
176
  ```python
147
- from olbrain import AgentClient, AuthenticationError, RateLimitError, NetworkError, OlbrainError
148
- import time
149
-
150
- def ask(client, sid, text, tries=3):
151
- for i in range(tries):
152
- try:
153
- return client.send_and_wait(sid, text)
154
- except RateLimitError as e:
155
- time.sleep(e.retry_after or 2 ** i)
156
- except NetworkError:
157
- time.sleep(2 ** i)
158
- raise OlbrainError("gave up")
177
+ import os, time
178
+ from olbrain import Olbrain
179
+
180
+ olbrain = Olbrain(api_key=os.environ["OLBRAIN_API_KEY"])
181
+ run_id = olbrain.workflows.run(agent_id, payload={"url": url})["run_id"]
182
+ DONE = {"completed", "failed", "canceled", "cancelled"}
183
+ run = olbrain.runs.get(agent_id, run_id)
184
+ while run["status"] not in DONE:
185
+ for a in olbrain.approvals.list(agent_id, run_id):
186
+ olbrain.approvals.resolve(agent_id, run_id, a["exception_id"], "approved")
187
+ time.sleep(3)
188
+ run = olbrain.runs.get(agent_id, run_id)
159
189
  ```
160
190
 
161
- `ValidationError` and `StreamingError` are exported, but the client never
162
- raises them.
191
+ The terminal status strings in that loop are an assumption. Check the values
192
+ your agent actually returns.
193
+
194
+ ### Errors (`Olbrain`, including `research`)
195
+
196
+ The error code and message come from the response body: `{error:{code,message}}`
197
+ or FastAPI's `{detail}`.
198
+
199
+ | Condition | Raised |
200
+ |---|---|
201
+ | code is `insufficient_funds`, `credit_limit_exceeded`, `subscription_suspended`, `subscription_inactive` or `billing_unavailable` (any status) | `BillingError` (`.status`, `.code`) |
202
+ | 401 | `AuthenticationError` |
203
+ | 404 | `NotFoundError` (`.status = 404`, `.code`, e.g. `agent_not_found`) |
204
+ | 429 | `RateLimitError` (`.retry_after` seconds from the `Retry-After` header, or `None`) |
205
+ | any other non-2xx (403, 409, 422, 5xx…) | `ApiError` (`.status`, `.code`, `.message`, `.detail` = raw `detail`) |
206
+ | connection failure, timeout, or a dropped stream | `NetworkError` |
207
+
208
+ `BillingError` and `NotFoundError` subclass `ApiError`, and every error class
209
+ subclasses `OlbrainError`, so put the narrower `except` clauses first.
210
+
211
+ ---
212
+
213
+ ## 3. `Olbrain.research` — research agents (sign-ins only)
214
+
215
+ ```python
216
+ olbrain = Olbrain(get_id_token=lambda: user_id_token)
217
+ r = olbrain.research
218
+ ```
219
+
220
+ All paths sit under `/api/agents/{agent_id}/research`. The agent must be a
221
+ research agent that is bound to a screen in the user's org. Payloads are
222
+ returned untyped (dicts and lists) because their shape belongs to the
223
+ research-runtime service.
224
+
225
+ | Method | |
226
+ |---|---|
227
+ | `sessions.create(agent_id)` | session (has `session_id`) |
228
+ | `sessions.list(agent_id, include_archived=False)`, `sessions.get / delete(agent_id, session_id)` | |
229
+ | `sessions.patch(agent_id, session_id, patch)` | partial session fields, passed through |
230
+ | `sessions.models(agent_id, session_id)` | available models |
231
+ | `sessions.set_model / set_review_model(agent_id, session_id, model)` | `None` resets to the default |
232
+ | `sessions.stream_message(agent_id, session_id, content, attachments=None, effort=None, escalates_run_id=None, command=None, target_run_id=None)` | generator of frames |
233
+ | `plans.approve(agent_id, session_id, message_id, body=None)`, `plans.approve_by_id(agent_id, session_id, plan_id, body=None)` | |
234
+ | `attachments.add(agent_id, session_id, files)` | multipart upload of `(filename, file or bytes[, content_type])` tuples; returns refs to pass as `attachments` |
235
+ | `attachments.upload_url(agent_id, session_id, storage_path)` | `{"url"}` |
236
+ | `runs.cancel / retry(agent_id, run_id)`, `runs.active_job(agent_id, run_id)` (or `None`), `runs.resolve_claims(agent_id, run_id, resolutions)` | |
237
+ | `reports.get(agent_id, run_id, version=None)`, `reports.artifact_url(agent_id, run_id, profile_id, fmt)`, `reports.evidence(agent_id, run_id)` | |
238
+ | `reports.share.get / revoke(agent_id, run_id)`, `reports.share.set(agent_id, run_id, visibility)` | |
239
+ | `review.get(agent_id, run_id)`; `review.run(agent_id, run_id)`, `review.apply(agent_id, run_id, finding_ids)` | the last two block, up to 600 s |
240
+ | `review.stream(agent_id, run_id)`, `review.stream_apply(agent_id, run_id, finding_ids)` | generators of frames |
241
+ | `templates.versions(agent_id)` | |
242
+ | `messages.older(agent_id, session_id, before_ts)` | one page of older messages, oldest first |
243
+
244
+ Stream frames come straight from research-runtime, unchanged: `{"type": "text_delta", "text"}`,
245
+ `{"type": "tool_use_start", …}`, `{"type": "complete", …}` and `{"type": "error", …}`.
246
+ The request is sent when you start iterating, and it can wait up to 600 s for
247
+ its first frame while the agent searches. To cancel, stop iterating (`break`,
248
+ or `.close()` the generator); the connection is closed.
249
+
250
+ ```python
251
+ for ev in r.sessions.stream_message(agent_id, sid, "Size the Indian EV charger market"):
252
+ if ev.get("type") == "text_delta":
253
+ print(ev["text"], end="", flush=True)
254
+ elif ev.get("type") == "error":
255
+ raise RuntimeError(ev.get("message", "research error"))
256
+ ```
257
+
258
+ The JavaScript SDK's `live.*` polling subscriptions are not in Python. Poll
259
+ `sessions.get` / `reports.get` yourself.
260
+
261
+ ---
163
262
 
164
263
  ## Logging
165
264
 
166
- The SDK logs through `logging.getLogger("olbrain")`. Turn on
167
- `logging.basicConfig(level=logging.DEBUG)` to see requests and stream events.
265
+ `AgentClient` logs through `logging.getLogger("olbrain")`. Turn on
266
+ `logging.basicConfig(level=logging.DEBUG)` to see requests and listener events.
olbrain/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Olbrain Python SDK
3
- Simple client for Olbrain AI agents with real-time message streaming
3
+ AgentClient for conversational agents; Olbrain for workflow runs, approvals and research agents
4
4
 
5
5
  Full offline API reference: AGENTS.md next to this file.
6
6
 
@@ -23,6 +23,7 @@ Example:
23
23
  """
24
24
 
25
25
  from .client import AgentClient
26
+ from .olbrain import Olbrain
26
27
  from .session import ChatResponse, SessionInfo, TokenUsage, Message, Session
27
28
  from .exceptions import (
28
29
  OlbrainError,
@@ -32,16 +33,20 @@ from .exceptions import (
32
33
  SessionNotFoundError,
33
34
  RateLimitError,
34
35
  ValidationError,
35
- StreamingError
36
+ StreamingError,
37
+ ApiError,
38
+ NotFoundError,
39
+ BillingError,
36
40
  )
37
41
 
38
- __version__ = "0.3.2"
42
+ __version__ = "0.5.1"
39
43
  __author__ = "Olbrain Team"
40
44
  __email__ = "support@olbrain.com"
41
45
 
42
46
  __all__ = [
43
47
  # Main client
44
48
  'AgentClient',
49
+ 'Olbrain',
45
50
  # Data classes
46
51
  'ChatResponse',
47
52
  'SessionInfo',
@@ -56,5 +61,8 @@ __all__ = [
56
61
  'SessionNotFoundError',
57
62
  'RateLimitError',
58
63
  'ValidationError',
59
- 'StreamingError'
64
+ 'StreamingError',
65
+ 'ApiError',
66
+ 'NotFoundError',
67
+ 'BillingError',
60
68
  ]