olbrain-python-sdk 0.3.2__py3-none-any.whl → 0.5.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.
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,37 @@ 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 `https://webhook.olbrain.com`.
47
60
 
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()`.
61
+ Chat calls are `POST {agent_url}/api/agents/webhook/{agent_id}` with
62
+ `Authorization: Bearer <api_key>`. Session reads are
63
+ `GET {agent_url}/api/agents/{agent_id}/sessions/{session_id}[/messages]`. The
64
+ client is a context manager, and `__exit__` calls `close()`.
51
65
 
52
66
  ### Methods
53
67
 
54
68
  | Method | Returns | Notes |
55
69
  |---|---|---|
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. |
70
+ | `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
71
  | `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
72
  | `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. |
73
+ | `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
74
  | `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.
75
+ | `close()` | `None` | Stops all listeners. |
76
+ | `get_session(session_id)` | `SessionInfo` | `SessionNotFoundError` if it doesn't exist (or belongs to another agent). |
77
+ | `get_session_stats(session_id)` | `dict` | `message_count`, `token_usage`, `cost`, `created_at`, `last_message_at`. |
78
+ | `get_messages(session_id, limit=20, offset=0)` | `dict` | `{"messages": [...], "limit", "offset"}`, oldest first; `limit` at most 200. |
79
+ | `update_session(...)`, `delete_session(...)` | — | Always raise `OlbrainError`: the API has no endpoint for them. |
72
80
 
73
- ### Streaming callback
81
+ `on_message` gets each message as a raw dict (`role`, `content`, …). It runs
82
+ on the listener thread, so keep it fast and don't raise from it.
74
83
 
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`)
84
+ ### Data classes (`olbrain.session`, re-exported from `olbrain`)
90
85
 
91
86
  ```python
92
87
  @dataclass
@@ -126,12 +121,9 @@ class SessionInfo:
126
121
  `reply.success` too. A 200 response can carry `success=False` and an `error`.
127
122
 
128
123
  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`)
124
+ returns them.
132
125
 
133
- All of them subclass `OlbrainError`, which has `.message`, `.error_code` and
134
- `.details`.
126
+ ### Errors (`AgentClient`)
135
127
 
136
128
  | Condition | Raised |
137
129
  |---|---|
@@ -143,25 +135,130 @@ All of them subclass `OlbrainError`, which has `.message`, `.error_code` and
143
135
  | Any other non-200 (403, 5xx…) | `OlbrainError("Failed to <operation>: <status> - <detail>")` |
144
136
  | Connection error or timeout | `NetworkError` |
145
137
 
138
+ ---
139
+
140
+ ## 2. `Olbrain` — workflow agents, runs, approvals
141
+
142
+ ```python
143
+ olbrain = Olbrain(api_key=os.environ["OLBRAIN_API_KEY"]) # server / script
144
+ # olbrain = Olbrain(get_id_token=lambda: current_user_id_token()) # signed-in Olbrain user (Firebase ID token)
145
+ ```
146
+
147
+ `Olbrain(api_key=None, get_id_token=None, base_url=None, timeout=30)`:
148
+ `api_key` must start `ak_`; `get_id_token` is a callable returning the
149
+ token and is called before **every** request; `base_url` defaults to
150
+ `https://webhook.olbrain.com`; `timeout` is in seconds. Give **exactly one** of
151
+ `api_key` or `get_id_token`, or the constructor raises `ValidationError`.
152
+
153
+ Every method takes the agent id first and returns the API's JSON (snake_case
154
+ dicts and lists).
155
+
156
+ | Method | HTTP | Returns |
157
+ |---|---|---|
158
+ | `workflows.run(agent_id, payload=None, variables=None)` | `POST /api/workflows/{agent_id}/trigger` | `{"run_id", "status"}` |
159
+ | `runs.list(agent_id, status=None, limit=None)` | `GET /api/agents/{agent_id}/runs` | list of runs |
160
+ | `runs.get(agent_id, run_id)` | `GET …/runs/{run_id}` | run |
161
+ | `runs.cancel / pause / retry / resume(agent_id, run_id)` | `POST …/runs/{run_id}/{verb}` | dict |
162
+ | `approvals.list(agent_id, run_id)` | `GET …/runs/{run_id}/approvals` | list of approvals |
163
+ | `approvals.resolve(agent_id, run_id, exception_id, resolution, modified_data=None)` | `POST …/approvals/resolve` | `{"exception_id", "resolution", "all_resolved", "resumed"}` |
164
+
165
+ A run has `run_id`, `status`, and optionally `progress_percent`, `started_at`,
166
+ `completed_at`, `duration_seconds`, `error` and `summary`
167
+ (`total_processed`, `successful`, `failed`, `exceptions`, `skipped`). An
168
+ approval has `exception_id`, `step_id`, `item_id`, `data`, `error`,
169
+ `requires_human_review` and `blocking`. `resolution` is `"approved"`,
170
+ `"rejected"` or `"modified"`; send `modified_data` only with `"modified"`.
171
+
172
+ There is no SDK helper for waiting on a run, so poll `runs.get` yourself:
173
+
146
174
  ```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")
175
+ import os, time
176
+ from olbrain import Olbrain
177
+
178
+ olbrain = Olbrain(api_key=os.environ["OLBRAIN_API_KEY"])
179
+ run_id = olbrain.workflows.run(agent_id, payload={"url": url})["run_id"]
180
+ DONE = {"completed", "failed", "canceled", "cancelled"}
181
+ run = olbrain.runs.get(agent_id, run_id)
182
+ while run["status"] not in DONE:
183
+ for a in olbrain.approvals.list(agent_id, run_id):
184
+ olbrain.approvals.resolve(agent_id, run_id, a["exception_id"], "approved")
185
+ time.sleep(3)
186
+ run = olbrain.runs.get(agent_id, run_id)
159
187
  ```
160
188
 
161
- `ValidationError` and `StreamingError` are exported, but the client never
162
- raises them.
189
+ The terminal status strings in that loop are an assumption. Check the values
190
+ your agent actually returns.
191
+
192
+ ### Errors (`Olbrain`, including `research`)
193
+
194
+ The error code and message come from the response body: `{error:{code,message}}`
195
+ or FastAPI's `{detail}`.
196
+
197
+ | Condition | Raised |
198
+ |---|---|
199
+ | code is `insufficient_funds`, `credit_limit_exceeded`, `subscription_suspended`, `subscription_inactive` or `billing_unavailable` (any status) | `BillingError` (`.status`, `.code`) |
200
+ | 401 | `AuthenticationError` |
201
+ | 404 | `NotFoundError` (`.status = 404`, `.code`, e.g. `agent_not_found`) |
202
+ | 429 | `RateLimitError` (`.retry_after` seconds from the `Retry-After` header, or `None`) |
203
+ | any other non-2xx (403, 409, 422, 5xx…) | `ApiError` (`.status`, `.code`, `.message`, `.detail` = raw `detail`) |
204
+ | connection failure, timeout, or a dropped stream | `NetworkError` |
205
+
206
+ `BillingError` and `NotFoundError` subclass `ApiError`, and every error class
207
+ subclasses `OlbrainError`, so put the narrower `except` clauses first.
208
+
209
+ ---
210
+
211
+ ## 3. `Olbrain.research` — research agents (sign-ins only)
212
+
213
+ ```python
214
+ olbrain = Olbrain(get_id_token=lambda: user_id_token)
215
+ r = olbrain.research
216
+ ```
217
+
218
+ All paths sit under `/api/agents/{agent_id}/research`. The agent must be a
219
+ research agent that is bound to a screen in the user's org. Payloads are
220
+ returned untyped (dicts and lists) because their shape belongs to the
221
+ research-runtime service.
222
+
223
+ | Method | |
224
+ |---|---|
225
+ | `sessions.create(agent_id)` | session (has `session_id`) |
226
+ | `sessions.list(agent_id, include_archived=False)`, `sessions.get / delete(agent_id, session_id)` | |
227
+ | `sessions.patch(agent_id, session_id, patch)` | partial session fields, passed through |
228
+ | `sessions.models(agent_id, session_id)` | available models |
229
+ | `sessions.set_model / set_review_model(agent_id, session_id, model)` | `None` resets to the default |
230
+ | `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 |
231
+ | `plans.approve(agent_id, session_id, message_id, body=None)`, `plans.approve_by_id(agent_id, session_id, plan_id, body=None)` | |
232
+ | `attachments.add(agent_id, session_id, files)` | multipart upload of `(filename, file or bytes[, content_type])` tuples; returns refs to pass as `attachments` |
233
+ | `attachments.upload_url(agent_id, session_id, storage_path)` | `{"url"}` |
234
+ | `runs.cancel / retry(agent_id, run_id)`, `runs.active_job(agent_id, run_id)` (or `None`), `runs.resolve_claims(agent_id, run_id, resolutions)` | |
235
+ | `reports.get(agent_id, run_id, version=None)`, `reports.artifact_url(agent_id, run_id, profile_id, fmt)`, `reports.evidence(agent_id, run_id)` | |
236
+ | `reports.share.get / revoke(agent_id, run_id)`, `reports.share.set(agent_id, run_id, visibility)` | |
237
+ | `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 |
238
+ | `review.stream(agent_id, run_id)`, `review.stream_apply(agent_id, run_id, finding_ids)` | generators of frames |
239
+ | `templates.versions(agent_id)` | |
240
+ | `messages.older(agent_id, session_id, before_ts)` | one page of older messages, oldest first |
241
+
242
+ Stream frames come straight from research-runtime, unchanged: `{"type": "text_delta", "text"}`,
243
+ `{"type": "tool_use_start", …}`, `{"type": "complete", …}` and `{"type": "error", …}`.
244
+ The request is sent when you start iterating, and it can wait up to 600 s for
245
+ its first frame while the agent searches. To cancel, stop iterating (`break`,
246
+ or `.close()` the generator); the connection is closed.
247
+
248
+ ```python
249
+ for ev in r.sessions.stream_message(agent_id, sid, "Size the Indian EV charger market"):
250
+ if ev.get("type") == "text_delta":
251
+ print(ev["text"], end="", flush=True)
252
+ elif ev.get("type") == "error":
253
+ raise RuntimeError(ev.get("message", "research error"))
254
+ ```
255
+
256
+ The JavaScript SDK's `live.*` polling subscriptions are not in Python. Poll
257
+ `sessions.get` / `reports.get` yourself.
258
+
259
+ ---
163
260
 
164
261
  ## Logging
165
262
 
166
- The SDK logs through `logging.getLogger("olbrain")`. Turn on
167
- `logging.basicConfig(level=logging.DEBUG)` to see requests and stream events.
263
+ `AgentClient` logs through `logging.getLogger("olbrain")`. Turn on
264
+ `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.0"
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
  ]
olbrain/client.py CHANGED
@@ -12,6 +12,7 @@ FIXES:
12
12
  import requests
13
13
  import logging
14
14
  import time
15
+ from urllib.parse import quote
15
16
  from typing import Callable, Optional, Dict, Any, List
16
17
 
17
18
  logger = logging.getLogger(__name__)
@@ -148,7 +149,7 @@ class AgentClient:
148
149
 
149
150
  Args:
150
151
  agent_id: Agent identifier
151
- api_key: API key (must start with 'sk_live_', 'org_live_', 'sk_', or 'org_')
152
+ api_key: Access key (ak_…); 'sk_live_', 'org_live_', 'sk_' and 'org_' are also accepted
152
153
  agent_url: Optional agent URL (auto-constructed if not provided)
153
154
 
154
155
  Raises:
@@ -157,10 +158,10 @@ class AgentClient:
157
158
  if not agent_id:
158
159
  raise ValueError("agent_id is required")
159
160
 
160
- valid_prefixes = ('sk_live_', 'org_live_', 'sk_', 'org_')
161
+ valid_prefixes = ('ak_', 'sk_live_', 'org_live_', 'sk_', 'org_')
161
162
  if not api_key or not api_key.startswith(valid_prefixes):
162
163
  raise ValueError(
163
- "Invalid API key - must start with 'sk_live_', 'org_live_', 'sk_', or 'org_'"
164
+ "Invalid API key - must start with 'ak_', 'sk_live_', 'org_live_', 'sk_', or 'org_'"
164
165
  )
165
166
 
166
167
  self.agent_id = agent_id
@@ -170,8 +171,8 @@ class AgentClient:
170
171
  if agent_url:
171
172
  self.agent_url = agent_url.rstrip('/')
172
173
  else:
173
- # Use Olbrain shared agent URL
174
- self.agent_url = "https://olbrain-agent-cloud-768934887465.us-central1.run.app"
174
+ # The Olbrain API. The old run.app host this SDK defaulted to no longer exists.
175
+ self.agent_url = "https://webhook.olbrain.com"
175
176
 
176
177
  self._streams = {} # session_id -> MessageStream
177
178
  self._running = False
@@ -198,7 +199,7 @@ class AgentClient:
198
199
  title: Optional title for the session
199
200
  user_id: Optional user identifier for tracking
200
201
  metadata: Optional metadata dict
201
- mode: Session mode - 'development', 'testing', or 'production' (default)
202
+ mode: Kept for compatibility and not sent: the Olbrain API always serves the published agent
202
203
  description: Optional session description
203
204
 
204
205
  Returns:
@@ -219,8 +220,7 @@ class AgentClient:
219
220
 
220
221
  payload = {
221
222
  'message': title or "New Session",
222
- 'response_mode': 'sync',
223
- 'mode': mode
223
+ 'response_mode': 'sync'
224
224
  }
225
225
  if user_id:
226
226
  payload['user_id'] = user_id
@@ -229,7 +229,7 @@ class AgentClient:
229
229
 
230
230
  # Create session via webhook endpoint
231
231
  response = requests.post(
232
- f"{self.agent_url}/api/agent/webhook",
232
+ self._chat_url(),
233
233
  headers={
234
234
  'Authorization': f'Bearer {self.api_key}',
235
235
  'Content-Type': 'application/json',
@@ -278,7 +278,7 @@ class AgentClient:
278
278
  user_id: Optional user identifier
279
279
  metadata: Optional message metadata
280
280
  model: Optional model override (e.g., 'gpt-4', 'claude-3-opus')
281
- mode: Session mode - 'development', 'testing', or 'production'
281
+ mode: Kept for compatibility and not sent: the Olbrain API always serves the published agent
282
282
 
283
283
  Returns:
284
284
  Response dict with success, response, token_usage, etc.
@@ -294,8 +294,7 @@ class AgentClient:
294
294
  payload = {
295
295
  'session_id': session_id,
296
296
  'message': message.strip(),
297
- 'response_mode': 'sync',
298
- 'mode': mode
297
+ 'response_mode': 'sync'
299
298
  }
300
299
  if user_id:
301
300
  payload['user_id'] = user_id
@@ -305,7 +304,7 @@ class AgentClient:
305
304
  payload['model'] = model
306
305
 
307
306
  response = requests.post(
308
- f"{self.agent_url}/api/agent/webhook",
307
+ self._chat_url(),
309
308
  headers={
310
309
  'Authorization': f'Bearer {self.api_key}',
311
310
  'Content-Type': 'application/json',
@@ -378,7 +377,7 @@ class AgentClient:
378
377
  payload['model'] = model
379
378
 
380
379
  response = requests.post(
381
- f"{self.agent_url}/api/agent/webhook", # FIXED: was /sessions/{id}/messages
380
+ self._chat_url(),
382
381
  headers={
383
382
  'Authorization': f'Bearer {self.api_key}',
384
383
  'Content-Type': 'application/json',
@@ -489,209 +488,52 @@ class AgentClient:
489
488
  """
490
489
  Get session details.
491
490
 
492
- FIXED: Uses webhook to query session info instead of /sessions/{id}
493
-
494
- Args:
495
- session_id: Session identifier
496
-
497
- Returns:
498
- SessionInfo with session details
499
-
500
491
  Raises:
501
- SessionNotFoundError: If session doesn't exist
502
- OlbrainError: If request fails
492
+ SessionNotFoundError: If the session doesn't exist (or belongs to another agent)
503
493
  """
504
- try:
505
- # Query session info via webhook
506
- response = requests.post(
507
- f"{self.agent_url}/api/agent/webhook",
508
- headers={
509
- 'Authorization': f'Bearer {self.api_key}',
510
- 'Content-Type': 'application/json',
511
- **({'X-Agent-ID': self.agent_id} if self.agent_id else {})
512
- },
513
- json={
514
- 'session_id': session_id,
515
- 'action': 'get_session_info'
516
- },
517
- timeout=30
518
- )
519
-
520
- self._handle_response_errors(response, "get session")
494
+ return SessionInfo.from_dict(self._get(self._session_url(session_id), "get session"))
521
495
 
522
- data = response.json()
523
- return SessionInfo.from_dict(data)
524
-
525
- except requests.exceptions.RequestException as e:
526
- raise NetworkError(f"Network error getting session: {e}")
527
-
528
- def update_session(
529
- self,
530
- session_id: str,
531
- title: str = None,
532
- metadata: Dict[str, Any] = None,
533
- status: str = None
534
- ) -> SessionInfo:
535
- """
536
- Update session details via webhook.
537
-
538
- Args:
539
- session_id: Session identifier
540
- title: New session title (optional)
541
- metadata: New metadata dict (optional)
542
- status: New status - 'active' or 'archived' (optional)
543
-
544
- Returns:
545
- Updated SessionInfo
546
- """
547
- try:
548
- update_data = {}
549
- if title is not None:
550
- update_data['title'] = title
551
- if metadata is not None:
552
- update_data['metadata'] = metadata
553
- if status is not None:
554
- if status not in ['active', 'archived']:
555
- raise ValueError("status must be 'active' or 'archived'")
556
- update_data['status'] = status
557
-
558
- response = requests.post(
559
- f"{self.agent_url}/api/agent/webhook",
560
- headers={
561
- 'Authorization': f'Bearer {self.api_key}',
562
- 'Content-Type': 'application/json',
563
- **({'X-Agent-ID': self.agent_id} if self.agent_id else {})
564
- },
565
- json={
566
- 'session_id': session_id,
567
- 'action': 'update_session',
568
- **update_data
569
- },
570
- timeout=30
571
- )
572
-
573
- self._handle_response_errors(response, "update session")
574
-
575
- data = response.json()
576
- return SessionInfo.from_dict(data)
577
-
578
- except requests.exceptions.RequestException as e:
579
- raise NetworkError(f"Network error updating session: {e}")
496
+ def update_session(self, session_id: str, title: str = None, metadata: Dict[str, Any] = None,
497
+ status: str = None) -> SessionInfo:
498
+ """Not supported: the Olbrain API has no endpoint to change a session."""
499
+ raise OlbrainError("update_session is not supported: the Olbrain API has no endpoint to change a session")
580
500
 
581
501
  def delete_session(self, session_id: str) -> Dict[str, Any]:
582
- """
583
- Archive/delete a session via webhook.
584
-
585
- Args:
586
- session_id: Session identifier
587
-
588
- Returns:
589
- Dict with success status
590
- """
591
- try:
592
- # Stop listening if we have an active stream
593
- if session_id in self._streams:
594
- self._streams[session_id].stop()
595
- del self._streams[session_id]
596
-
597
- response = requests.post(
598
- f"{self.agent_url}/api/agent/webhook",
599
- headers={
600
- 'Authorization': f'Bearer {self.api_key}',
601
- 'Content-Type': 'application/json',
602
- **({'X-Agent-ID': self.agent_id} if self.agent_id else {})
603
- },
604
- json={
605
- 'session_id': session_id,
606
- 'action': 'delete_session'
607
- },
608
- timeout=30
609
- )
610
-
611
- self._handle_response_errors(response, "delete session")
612
-
613
- return response.json()
614
-
615
- except requests.exceptions.RequestException as e:
616
- raise NetworkError(f"Network error deleting session: {e}")
502
+ """Not supported: the Olbrain API has no endpoint to delete a session."""
503
+ raise OlbrainError("delete_session is not supported: the Olbrain API has no endpoint to delete a session")
617
504
 
618
505
  def get_session_stats(self, session_id: str) -> Dict[str, Any]:
619
506
  """
620
- Get session statistics via webhook.
621
-
622
- Args:
623
- session_id: Session identifier
624
-
625
- Returns:
626
- Dict with session statistics
507
+ Session statistics: message_count, token_usage, cost, created_at and last_message_at.
627
508
  """
628
- try:
629
- response = requests.post(
630
- f"{self.agent_url}/api/agent/webhook",
631
- headers={
632
- 'Authorization': f'Bearer {self.api_key}',
633
- 'Content-Type': 'application/json',
634
- **({'X-Agent-ID': self.agent_id} if self.agent_id else {})
635
- },
636
- json={
637
- 'session_id': session_id,
638
- 'action': 'get_stats'
639
- },
640
- timeout=30
641
- )
642
-
643
- self._handle_response_errors(response, "get session stats")
644
-
645
- return response.json()
509
+ return self._get(self._session_url(session_id), "get session stats")
646
510
 
647
- except requests.exceptions.RequestException as e:
648
- raise NetworkError(f"Network error getting session stats: {e}")
649
-
650
- def get_messages(
651
- self,
652
- session_id: str,
653
- limit: int = 20,
654
- offset: int = 0
655
- ) -> Dict[str, Any]:
511
+ def get_messages(self, session_id: str, limit: int = 20, offset: int = 0) -> Dict[str, Any]:
656
512
  """
657
- Get message history via webhook.
658
-
659
- Args:
660
- session_id: Session identifier
661
- limit: Maximum messages to return (default: 20)
662
- offset: Number of messages to skip (default: 0)
663
-
664
- Returns:
665
- Dict with messages list and pagination info
513
+ Message history, oldest first: {"messages": [...], "limit", "offset"} (limit at most 200).
666
514
  """
667
- try:
668
- response = requests.post(
669
- f"{self.agent_url}/api/agent/webhook",
670
- headers={
671
- 'Authorization': f'Bearer {self.api_key}',
672
- 'Content-Type': 'application/json',
673
- **({'X-Agent-ID': self.agent_id} if self.agent_id else {})
674
- },
675
- json={
676
- 'session_id': session_id,
677
- 'action': 'get_messages',
678
- 'limit': limit,
679
- 'offset': offset
680
- },
681
- timeout=30
682
- )
683
-
684
- self._handle_response_errors(response, "get messages")
685
-
686
- return response.json()
687
-
688
- except requests.exceptions.RequestException as e:
689
- raise NetworkError(f"Network error getting messages: {e}")
515
+ return self._get(f"{self._session_url(session_id)}/messages", "get messages",
516
+ params={'limit': limit, 'offset': offset})
690
517
 
691
518
  # -------------------------------------------------------------------------
692
519
  # Helper Methods
693
520
  # -------------------------------------------------------------------------
694
521
 
522
+ def _chat_url(self) -> str:
523
+ return f"{self.agent_url}/api/agents/webhook/{quote(self.agent_id, safe='')}"
524
+
525
+ def _session_url(self, session_id: str) -> str:
526
+ return f"{self.agent_url}/api/agents/{quote(self.agent_id, safe='')}/sessions/{quote(session_id, safe='')}"
527
+
528
+ def _get(self, url: str, operation: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
529
+ try:
530
+ response = requests.get(url, headers={'Authorization': f'Bearer {self.api_key}'},
531
+ params=params, timeout=30)
532
+ except requests.exceptions.RequestException as e:
533
+ raise NetworkError(f"Network error ({operation}): {e}")
534
+ self._handle_response_errors(response, operation)
535
+ return response.json()
536
+
695
537
  def _handle_response_errors(self, response: requests.Response, operation: str):
696
538
  """Handle HTTP response errors and raise appropriate exceptions."""
697
539
  if response.status_code == 200: