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 +161 -64
- olbrain/__init__.py +12 -4
- olbrain/client.py +42 -200
- olbrain/exceptions.py +22 -0
- olbrain/olbrain.py +241 -0
- olbrain/research.py +200 -0
- olbrain/streaming.py +57 -105
- {olbrain_python_sdk-0.3.2.dist-info → olbrain_python_sdk-0.5.0.dist-info}/METADATA +35 -29
- olbrain_python_sdk-0.5.0.dist-info/RECORD +14 -0
- {olbrain_python_sdk-0.3.2.dist-info → olbrain_python_sdk-0.5.0.dist-info}/WHEEL +1 -1
- olbrain_python_sdk-0.3.2.dist-info/RECORD +0 -12
- {olbrain_python_sdk-0.3.2.dist-info → olbrain_python_sdk-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {olbrain_python_sdk-0.3.2.dist-info → olbrain_python_sdk-0.5.0.dist-info}/top_level.txt +0 -0
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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,
|
|
32
|
+
from olbrain import AgentClient, Olbrain, OlbrainError
|
|
22
33
|
```
|
|
23
34
|
|
|
24
|
-
|
|
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
|
-
|
|
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_`,
|
|
45
|
-
constructor raises `ValueError`.
|
|
46
|
-
- `agent_url`: optional. Defaults to `https://olbrain
|
|
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
|
-
|
|
49
|
-
`Authorization: Bearer <api_key
|
|
50
|
-
|
|
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
|
|
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
|
|
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
|
|
62
|
-
| `get_session(session_id)` | `SessionInfo` |
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
130
|
-
|
|
131
|
-
## Errors (`olbrain.exceptions`)
|
|
124
|
+
returns them.
|
|
132
125
|
|
|
133
|
-
|
|
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
|
-
|
|
148
|
-
import
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
167
|
-
`logging.basicConfig(level=logging.DEBUG)` to see requests and
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
-
#
|
|
174
|
-
self.agent_url = "https://olbrain
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
668
|
-
|
|
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:
|