olbrain-python-sdk 0.2.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/__init__.py +58 -0
- olbrain/client.py +627 -0
- olbrain/exceptions.py +64 -0
- olbrain/session.py +202 -0
- olbrain/streaming.py +137 -0
- olbrain/utils.py +156 -0
- olbrain_python_sdk-0.2.0.dist-info/METADATA +357 -0
- olbrain_python_sdk-0.2.0.dist-info/RECORD +15 -0
- olbrain_python_sdk-0.2.0.dist-info/WHEEL +5 -0
- olbrain_python_sdk-0.2.0.dist-info/licenses/LICENSE +21 -0
- olbrain_python_sdk-0.2.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_client.py +91 -0
- tests/test_exceptions.py +50 -0
- tests/test_session.py +89 -0
olbrain/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Olbrain Python SDK
|
|
3
|
+
Simple client for Olbrain AI agents with real-time message streaming
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
>>> from olbrain import AgentClient
|
|
7
|
+
>>>
|
|
8
|
+
>>> client = AgentClient(agent_id="agent-123", api_key="ak_...")
|
|
9
|
+
>>>
|
|
10
|
+
>>> # Create session and send messages
|
|
11
|
+
>>> session_id = client.create_session(title="My Chat")
|
|
12
|
+
>>> response = client.send_and_wait(session_id, "Hello!")
|
|
13
|
+
>>> print(response.text)
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Or with real-time streaming
|
|
16
|
+
>>> def on_message(msg):
|
|
17
|
+
... print(f"{msg['role']}: {msg['content']}")
|
|
18
|
+
>>> session_id = client.create_session(on_message=on_message, title="Streaming Chat")
|
|
19
|
+
>>> client.send(session_id, "Hello!")
|
|
20
|
+
>>> client.run() # Blocks, receives all messages
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .client import AgentClient
|
|
24
|
+
from .session import ChatResponse, SessionInfo, TokenUsage, Message, Session
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
OlbrainError,
|
|
27
|
+
AuthenticationError,
|
|
28
|
+
NetworkError,
|
|
29
|
+
SessionError,
|
|
30
|
+
SessionNotFoundError,
|
|
31
|
+
RateLimitError,
|
|
32
|
+
ValidationError,
|
|
33
|
+
StreamingError
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__version__ = "0.2.0"
|
|
37
|
+
__author__ = "Olbrain Team"
|
|
38
|
+
__email__ = "support@olbrain.com"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Main client
|
|
42
|
+
'AgentClient',
|
|
43
|
+
# Data classes
|
|
44
|
+
'ChatResponse',
|
|
45
|
+
'SessionInfo',
|
|
46
|
+
'TokenUsage',
|
|
47
|
+
'Message',
|
|
48
|
+
'Session',
|
|
49
|
+
# Exceptions
|
|
50
|
+
'OlbrainError',
|
|
51
|
+
'AuthenticationError',
|
|
52
|
+
'NetworkError',
|
|
53
|
+
'SessionError',
|
|
54
|
+
'SessionNotFoundError',
|
|
55
|
+
'RateLimitError',
|
|
56
|
+
'ValidationError',
|
|
57
|
+
'StreamingError'
|
|
58
|
+
]
|
olbrain/client.py
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Olbrain Python SDK - Simple Client
|
|
3
|
+
Clean API for agent interaction with real-time message streaming
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from typing import Callable, Optional, Dict, Any, List
|
|
10
|
+
|
|
11
|
+
from .streaming import MessageStream
|
|
12
|
+
from .session import ChatResponse, SessionInfo, TokenUsage
|
|
13
|
+
from .exceptions import (
|
|
14
|
+
OlbrainError,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
NetworkError,
|
|
17
|
+
SessionNotFoundError,
|
|
18
|
+
RateLimitError
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AgentClient:
|
|
25
|
+
"""
|
|
26
|
+
Simple client for interacting with Olbrain agents
|
|
27
|
+
|
|
28
|
+
Provides unified message handling - all messages (responses + scheduled)
|
|
29
|
+
come through the same callback stream.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
agent_id: Agent identifier
|
|
33
|
+
api_key: API key (starts with 'ak_')
|
|
34
|
+
agent_url: Optional custom agent URL (auto-constructed if not provided)
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> from olbrain import AgentClient
|
|
38
|
+
>>>
|
|
39
|
+
>>> client = AgentClient(agent_id="agent-123", api_key="ak_...")
|
|
40
|
+
>>>
|
|
41
|
+
>>> def on_message(msg):
|
|
42
|
+
... print(f"{msg['role']}: {msg['content']}")
|
|
43
|
+
>>>
|
|
44
|
+
>>> session = client.create_session(on_message=on_message)
|
|
45
|
+
>>> client.send(session, "Hello!")
|
|
46
|
+
>>> client.run() # Blocks, receives all messages
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
agent_id: str,
|
|
52
|
+
api_key: str,
|
|
53
|
+
agent_url: Optional[str] = None
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initialize Olbrain client
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
agent_id: Agent identifier
|
|
60
|
+
api_key: API key (must start with 'ak_')
|
|
61
|
+
agent_url: Optional agent URL (auto-constructed if not provided)
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ValueError: If agent_id or api_key is invalid
|
|
65
|
+
"""
|
|
66
|
+
if not agent_id:
|
|
67
|
+
raise ValueError("agent_id is required")
|
|
68
|
+
|
|
69
|
+
if not api_key or not api_key.startswith('ak_'):
|
|
70
|
+
raise ValueError("Invalid API key - must start with 'ak_'")
|
|
71
|
+
|
|
72
|
+
self.agent_id = agent_id
|
|
73
|
+
self.api_key = api_key
|
|
74
|
+
|
|
75
|
+
# Auto-construct agent URL if not provided
|
|
76
|
+
if agent_url:
|
|
77
|
+
self.agent_url = agent_url.rstrip('/')
|
|
78
|
+
else:
|
|
79
|
+
# Use default Cloud Run URL pattern
|
|
80
|
+
self.agent_url = f"https://agent-{agent_id}-768934887465.us-central1.run.app"
|
|
81
|
+
|
|
82
|
+
self._streams = {} # session_id -> MessageStream
|
|
83
|
+
self._running = False
|
|
84
|
+
|
|
85
|
+
logger.info(f"AgentClient initialized for {agent_id}")
|
|
86
|
+
|
|
87
|
+
def create_session(
|
|
88
|
+
self,
|
|
89
|
+
on_message: Callable = None,
|
|
90
|
+
title: str = None,
|
|
91
|
+
user_id: str = None,
|
|
92
|
+
metadata: Dict[str, Any] = None,
|
|
93
|
+
mode: str = "production",
|
|
94
|
+
description: str = None
|
|
95
|
+
) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Create new session and optionally start listening for messages.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
on_message: Optional callback function(message_dict) for real-time messages.
|
|
101
|
+
If provided, starts listening automatically.
|
|
102
|
+
message_dict = {
|
|
103
|
+
'role': 'user' | 'assistant',
|
|
104
|
+
'content': str,
|
|
105
|
+
'timestamp': str,
|
|
106
|
+
'token_usage': {...}
|
|
107
|
+
}
|
|
108
|
+
title: Optional title for the session
|
|
109
|
+
user_id: Optional user identifier for tracking
|
|
110
|
+
metadata: Optional metadata dict (can include 'session_description')
|
|
111
|
+
mode: Session mode - 'development', 'testing', or 'production' (default)
|
|
112
|
+
description: Optional session description
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
session_id (string)
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
OlbrainError: If session creation fails
|
|
119
|
+
AuthenticationError: If API key is invalid
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
>>> # Create session with streaming
|
|
123
|
+
>>> def handle_msg(msg):
|
|
124
|
+
... print(msg['content'])
|
|
125
|
+
>>> session = client.create_session(on_message=handle_msg, title="My Chat")
|
|
126
|
+
|
|
127
|
+
>>> # Create session without streaming (for sync usage)
|
|
128
|
+
>>> session = client.create_session(title="Support Chat", user_id="user-123")
|
|
129
|
+
"""
|
|
130
|
+
if mode not in ['development', 'testing', 'production']:
|
|
131
|
+
raise ValueError("mode must be 'development', 'testing', or 'production'")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# Build request payload
|
|
135
|
+
request_metadata = metadata.copy() if metadata else {}
|
|
136
|
+
if description:
|
|
137
|
+
request_metadata['session_description'] = description
|
|
138
|
+
|
|
139
|
+
payload = {
|
|
140
|
+
'message': title or "New Session",
|
|
141
|
+
'response_mode': 'sync',
|
|
142
|
+
'mode': mode
|
|
143
|
+
}
|
|
144
|
+
if user_id:
|
|
145
|
+
payload['user_id'] = user_id
|
|
146
|
+
if request_metadata:
|
|
147
|
+
payload['metadata'] = request_metadata
|
|
148
|
+
|
|
149
|
+
# Create session via webhook endpoint
|
|
150
|
+
response = requests.post(
|
|
151
|
+
f"{self.agent_url}/api/agent/webhook",
|
|
152
|
+
headers={
|
|
153
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
154
|
+
'Content-Type': 'application/json'
|
|
155
|
+
},
|
|
156
|
+
json=payload,
|
|
157
|
+
timeout=30
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
self._handle_response_errors(response, "create session")
|
|
161
|
+
|
|
162
|
+
data = response.json()
|
|
163
|
+
session_id = data.get('session_id')
|
|
164
|
+
|
|
165
|
+
if not session_id:
|
|
166
|
+
raise OlbrainError("No session_id in response")
|
|
167
|
+
|
|
168
|
+
logger.info(f"Created session {session_id}")
|
|
169
|
+
|
|
170
|
+
# Start listening for messages if callback provided
|
|
171
|
+
if on_message:
|
|
172
|
+
self.listen(session_id, on_message)
|
|
173
|
+
|
|
174
|
+
return session_id
|
|
175
|
+
|
|
176
|
+
except requests.exceptions.RequestException as e:
|
|
177
|
+
raise NetworkError(f"Network error creating session: {e}")
|
|
178
|
+
|
|
179
|
+
def send(
|
|
180
|
+
self,
|
|
181
|
+
session_id: str,
|
|
182
|
+
message: str,
|
|
183
|
+
user_id: str = None,
|
|
184
|
+
metadata: Dict[str, Any] = None,
|
|
185
|
+
model: str = None,
|
|
186
|
+
mode: str = "production"
|
|
187
|
+
) -> Dict[str, Any]:
|
|
188
|
+
"""
|
|
189
|
+
Send message to agent. Response comes via callback if listening,
|
|
190
|
+
or returned directly in sync mode.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
session_id: Session identifier
|
|
194
|
+
message: Message text to send
|
|
195
|
+
user_id: Optional user identifier
|
|
196
|
+
metadata: Optional message metadata
|
|
197
|
+
model: Optional model override (e.g., 'gpt-4', 'claude-3-opus')
|
|
198
|
+
mode: Session mode - 'development', 'testing', or 'production'
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Response dict with success, response, token_usage, etc.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValueError: If message is empty
|
|
205
|
+
OlbrainError: If send fails
|
|
206
|
+
SessionNotFoundError: If session doesn't exist
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
>>> response = client.send(session_id, "What's 2+2?")
|
|
210
|
+
>>> print(response['response'])
|
|
211
|
+
"""
|
|
212
|
+
if not message or not message.strip():
|
|
213
|
+
raise ValueError("Message cannot be empty")
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
payload = {
|
|
217
|
+
'session_id': session_id,
|
|
218
|
+
'message': message.strip(),
|
|
219
|
+
'response_mode': 'sync',
|
|
220
|
+
'mode': mode
|
|
221
|
+
}
|
|
222
|
+
if user_id:
|
|
223
|
+
payload['user_id'] = user_id
|
|
224
|
+
if metadata:
|
|
225
|
+
payload['metadata'] = metadata
|
|
226
|
+
if model:
|
|
227
|
+
payload['model'] = model
|
|
228
|
+
|
|
229
|
+
response = requests.post(
|
|
230
|
+
f"{self.agent_url}/api/agent/webhook",
|
|
231
|
+
headers={
|
|
232
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
233
|
+
'Content-Type': 'application/json'
|
|
234
|
+
},
|
|
235
|
+
json=payload,
|
|
236
|
+
timeout=120
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
self._handle_response_errors(response, "send message")
|
|
240
|
+
|
|
241
|
+
data = response.json()
|
|
242
|
+
logger.debug(f"Sent message to session {session_id}")
|
|
243
|
+
return data
|
|
244
|
+
|
|
245
|
+
except requests.exceptions.RequestException as e:
|
|
246
|
+
raise NetworkError(f"Network error sending message: {e}")
|
|
247
|
+
|
|
248
|
+
def send_and_wait(
|
|
249
|
+
self,
|
|
250
|
+
session_id: str,
|
|
251
|
+
message: str,
|
|
252
|
+
user_id: str = None,
|
|
253
|
+
metadata: Dict[str, Any] = None,
|
|
254
|
+
model: str = None,
|
|
255
|
+
timeout: int = 120
|
|
256
|
+
) -> ChatResponse:
|
|
257
|
+
"""
|
|
258
|
+
Send message and wait for response (synchronous).
|
|
259
|
+
|
|
260
|
+
This method sends a message and returns a ChatResponse object
|
|
261
|
+
with the agent's reply. Use this for simple request-response patterns.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
session_id: Session identifier
|
|
265
|
+
message: Message text to send
|
|
266
|
+
user_id: Optional user identifier
|
|
267
|
+
metadata: Optional message metadata
|
|
268
|
+
model: Optional model override
|
|
269
|
+
timeout: Request timeout in seconds (default: 120)
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
ChatResponse with text, token_usage, model_used, etc.
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
ValueError: If message is empty
|
|
276
|
+
OlbrainError: If send fails
|
|
277
|
+
SessionNotFoundError: If session doesn't exist
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
>>> response = client.send_and_wait(session_id, "Explain quantum computing")
|
|
281
|
+
>>> print(response.text)
|
|
282
|
+
>>> print(f"Tokens used: {response.token_usage.total_tokens}")
|
|
283
|
+
"""
|
|
284
|
+
if not message or not message.strip():
|
|
285
|
+
raise ValueError("Message cannot be empty")
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
payload = {
|
|
289
|
+
'message': message.strip()
|
|
290
|
+
}
|
|
291
|
+
if user_id:
|
|
292
|
+
payload['user_id'] = user_id
|
|
293
|
+
if metadata:
|
|
294
|
+
payload['metadata'] = metadata
|
|
295
|
+
|
|
296
|
+
response = requests.post(
|
|
297
|
+
f"{self.agent_url}/sessions/{session_id}/messages",
|
|
298
|
+
headers={
|
|
299
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
300
|
+
'Content-Type': 'application/json'
|
|
301
|
+
},
|
|
302
|
+
json=payload,
|
|
303
|
+
timeout=timeout
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
self._handle_response_errors(response, "send message")
|
|
307
|
+
|
|
308
|
+
data = response.json()
|
|
309
|
+
logger.debug(f"Received response for session {session_id}")
|
|
310
|
+
return ChatResponse.from_dict(data, session_id)
|
|
311
|
+
|
|
312
|
+
except requests.exceptions.RequestException as e:
|
|
313
|
+
raise NetworkError(f"Network error sending message: {e}")
|
|
314
|
+
|
|
315
|
+
def listen(self, session_id: str, on_message: Callable):
|
|
316
|
+
"""
|
|
317
|
+
Start listening for messages on existing session
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
session_id: Session to listen to
|
|
321
|
+
on_message: Callback function(message_dict) for messages
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
>>> def handle_msg(msg):
|
|
325
|
+
... print(msg['content'])
|
|
326
|
+
>>>
|
|
327
|
+
>>> client.listen("existing-session-id", on_message=handle_msg)
|
|
328
|
+
"""
|
|
329
|
+
if session_id in self._streams:
|
|
330
|
+
logger.warning(f"Already listening to session {session_id}")
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
stream = MessageStream(
|
|
334
|
+
agent_url=self.agent_url,
|
|
335
|
+
api_key=self.api_key,
|
|
336
|
+
session_id=session_id,
|
|
337
|
+
on_message=on_message
|
|
338
|
+
)
|
|
339
|
+
stream.start()
|
|
340
|
+
self._streams[session_id] = stream
|
|
341
|
+
|
|
342
|
+
logger.info(f"Started listening to session {session_id}")
|
|
343
|
+
|
|
344
|
+
def run(self):
|
|
345
|
+
"""
|
|
346
|
+
Block forever and process message callbacks
|
|
347
|
+
|
|
348
|
+
Call this to keep your program running and receive messages.
|
|
349
|
+
Press Ctrl+C to exit gracefully.
|
|
350
|
+
|
|
351
|
+
Example:
|
|
352
|
+
>>> client.run() # Blocks until Ctrl+C
|
|
353
|
+
"""
|
|
354
|
+
self._running = True
|
|
355
|
+
logger.info("Client running - press Ctrl+C to exit")
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
while self._running:
|
|
359
|
+
time.sleep(1)
|
|
360
|
+
except KeyboardInterrupt:
|
|
361
|
+
logger.info("Received interrupt signal, shutting down...")
|
|
362
|
+
self.close()
|
|
363
|
+
|
|
364
|
+
def close(self):
|
|
365
|
+
"""
|
|
366
|
+
Stop all streams and cleanup resources
|
|
367
|
+
|
|
368
|
+
Automatically called on Ctrl+C or when using context manager.
|
|
369
|
+
"""
|
|
370
|
+
logger.info("Closing client...")
|
|
371
|
+
self._running = False
|
|
372
|
+
|
|
373
|
+
# Stop all streams
|
|
374
|
+
for session_id, stream in list(self._streams.items()):
|
|
375
|
+
stream.stop()
|
|
376
|
+
|
|
377
|
+
self._streams.clear()
|
|
378
|
+
logger.info("Client closed")
|
|
379
|
+
|
|
380
|
+
def __enter__(self):
|
|
381
|
+
"""Context manager entry"""
|
|
382
|
+
return self
|
|
383
|
+
|
|
384
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
385
|
+
"""Context manager exit"""
|
|
386
|
+
self.close()
|
|
387
|
+
|
|
388
|
+
def __repr__(self) -> str:
|
|
389
|
+
return f"AgentClient(agent_id={self.agent_id})"
|
|
390
|
+
|
|
391
|
+
# -------------------------------------------------------------------------
|
|
392
|
+
# Session Management Methods
|
|
393
|
+
# -------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
def get_session(self, session_id: str) -> SessionInfo:
|
|
396
|
+
"""
|
|
397
|
+
Get session details.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
session_id: Session identifier
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
SessionInfo with session details
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
SessionNotFoundError: If session doesn't exist
|
|
407
|
+
OlbrainError: If request fails
|
|
408
|
+
|
|
409
|
+
Example:
|
|
410
|
+
>>> info = client.get_session(session_id)
|
|
411
|
+
>>> print(f"Title: {info.title}, Messages: {info.message_count}")
|
|
412
|
+
"""
|
|
413
|
+
try:
|
|
414
|
+
response = requests.get(
|
|
415
|
+
f"{self.agent_url}/sessions/{session_id}",
|
|
416
|
+
headers={'Authorization': f'Bearer {self.api_key}'},
|
|
417
|
+
timeout=30
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
self._handle_response_errors(response, "get session")
|
|
421
|
+
|
|
422
|
+
data = response.json()
|
|
423
|
+
return SessionInfo.from_dict(data)
|
|
424
|
+
|
|
425
|
+
except requests.exceptions.RequestException as e:
|
|
426
|
+
raise NetworkError(f"Network error getting session: {e}")
|
|
427
|
+
|
|
428
|
+
def update_session(
|
|
429
|
+
self,
|
|
430
|
+
session_id: str,
|
|
431
|
+
title: str = None,
|
|
432
|
+
metadata: Dict[str, Any] = None,
|
|
433
|
+
status: str = None
|
|
434
|
+
) -> SessionInfo:
|
|
435
|
+
"""
|
|
436
|
+
Update session details.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
session_id: Session identifier
|
|
440
|
+
title: New session title (optional)
|
|
441
|
+
metadata: New metadata dict (optional)
|
|
442
|
+
status: New status - 'active' or 'archived' (optional)
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Updated SessionInfo
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
SessionNotFoundError: If session doesn't exist
|
|
449
|
+
OlbrainError: If update fails
|
|
450
|
+
|
|
451
|
+
Example:
|
|
452
|
+
>>> updated = client.update_session(session_id, title="Renamed Chat")
|
|
453
|
+
"""
|
|
454
|
+
try:
|
|
455
|
+
payload = {}
|
|
456
|
+
if title is not None:
|
|
457
|
+
payload['title'] = title
|
|
458
|
+
if metadata is not None:
|
|
459
|
+
payload['metadata'] = metadata
|
|
460
|
+
if status is not None:
|
|
461
|
+
if status not in ['active', 'archived']:
|
|
462
|
+
raise ValueError("status must be 'active' or 'archived'")
|
|
463
|
+
payload['status'] = status
|
|
464
|
+
|
|
465
|
+
response = requests.put(
|
|
466
|
+
f"{self.agent_url}/sessions/{session_id}",
|
|
467
|
+
headers={
|
|
468
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
469
|
+
'Content-Type': 'application/json'
|
|
470
|
+
},
|
|
471
|
+
json=payload,
|
|
472
|
+
timeout=30
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
self._handle_response_errors(response, "update session")
|
|
476
|
+
|
|
477
|
+
data = response.json()
|
|
478
|
+
return SessionInfo.from_dict(data)
|
|
479
|
+
|
|
480
|
+
except requests.exceptions.RequestException as e:
|
|
481
|
+
raise NetworkError(f"Network error updating session: {e}")
|
|
482
|
+
|
|
483
|
+
def delete_session(self, session_id: str) -> Dict[str, Any]:
|
|
484
|
+
"""
|
|
485
|
+
Archive/delete a session.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
session_id: Session identifier
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
Dict with success status and archived_at timestamp
|
|
492
|
+
|
|
493
|
+
Raises:
|
|
494
|
+
SessionNotFoundError: If session doesn't exist
|
|
495
|
+
OlbrainError: If delete fails
|
|
496
|
+
|
|
497
|
+
Example:
|
|
498
|
+
>>> result = client.delete_session(session_id)
|
|
499
|
+
>>> print(f"Archived at: {result['archived_at']}")
|
|
500
|
+
"""
|
|
501
|
+
try:
|
|
502
|
+
# Stop listening if we have an active stream
|
|
503
|
+
if session_id in self._streams:
|
|
504
|
+
self._streams[session_id].stop()
|
|
505
|
+
del self._streams[session_id]
|
|
506
|
+
|
|
507
|
+
response = requests.delete(
|
|
508
|
+
f"{self.agent_url}/sessions/{session_id}",
|
|
509
|
+
headers={'Authorization': f'Bearer {self.api_key}'},
|
|
510
|
+
timeout=30
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
self._handle_response_errors(response, "delete session")
|
|
514
|
+
|
|
515
|
+
return response.json()
|
|
516
|
+
|
|
517
|
+
except requests.exceptions.RequestException as e:
|
|
518
|
+
raise NetworkError(f"Network error deleting session: {e}")
|
|
519
|
+
|
|
520
|
+
def get_session_stats(self, session_id: str) -> Dict[str, Any]:
|
|
521
|
+
"""
|
|
522
|
+
Get session statistics (token usage, message counts, etc.).
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
session_id: Session identifier
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Dict with session statistics
|
|
529
|
+
|
|
530
|
+
Raises:
|
|
531
|
+
SessionNotFoundError: If session doesn't exist
|
|
532
|
+
OlbrainError: If request fails
|
|
533
|
+
|
|
534
|
+
Example:
|
|
535
|
+
>>> stats = client.get_session_stats(session_id)
|
|
536
|
+
>>> print(f"Total tokens: {stats['stats'].get('total_tokens', 0)}")
|
|
537
|
+
"""
|
|
538
|
+
try:
|
|
539
|
+
response = requests.get(
|
|
540
|
+
f"{self.agent_url}/sessions/{session_id}/stats",
|
|
541
|
+
headers={'Authorization': f'Bearer {self.api_key}'},
|
|
542
|
+
timeout=30
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
self._handle_response_errors(response, "get session stats")
|
|
546
|
+
|
|
547
|
+
return response.json()
|
|
548
|
+
|
|
549
|
+
except requests.exceptions.RequestException as e:
|
|
550
|
+
raise NetworkError(f"Network error getting session stats: {e}")
|
|
551
|
+
|
|
552
|
+
def get_messages(
|
|
553
|
+
self,
|
|
554
|
+
session_id: str,
|
|
555
|
+
limit: int = 20,
|
|
556
|
+
offset: int = 0
|
|
557
|
+
) -> Dict[str, Any]:
|
|
558
|
+
"""
|
|
559
|
+
Get message history for a session.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
session_id: Session identifier
|
|
563
|
+
limit: Maximum messages to return (default: 20)
|
|
564
|
+
offset: Number of messages to skip (default: 0)
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
Dict with messages list and pagination info
|
|
568
|
+
|
|
569
|
+
Raises:
|
|
570
|
+
SessionNotFoundError: If session doesn't exist
|
|
571
|
+
OlbrainError: If request fails
|
|
572
|
+
|
|
573
|
+
Example:
|
|
574
|
+
>>> result = client.get_messages(session_id, limit=50)
|
|
575
|
+
>>> for msg in result['messages']:
|
|
576
|
+
... print(f"{msg['role']}: {msg['content']}")
|
|
577
|
+
"""
|
|
578
|
+
try:
|
|
579
|
+
response = requests.get(
|
|
580
|
+
f"{self.agent_url}/sessions/{session_id}/messages",
|
|
581
|
+
headers={'Authorization': f'Bearer {self.api_key}'},
|
|
582
|
+
params={'limit': limit, 'offset': offset},
|
|
583
|
+
timeout=30
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
self._handle_response_errors(response, "get messages")
|
|
587
|
+
|
|
588
|
+
return response.json()
|
|
589
|
+
|
|
590
|
+
except requests.exceptions.RequestException as e:
|
|
591
|
+
raise NetworkError(f"Network error getting messages: {e}")
|
|
592
|
+
|
|
593
|
+
# -------------------------------------------------------------------------
|
|
594
|
+
# Helper Methods
|
|
595
|
+
# -------------------------------------------------------------------------
|
|
596
|
+
|
|
597
|
+
def _handle_response_errors(self, response: requests.Response, operation: str):
|
|
598
|
+
"""Handle HTTP response errors and raise appropriate exceptions."""
|
|
599
|
+
if response.status_code == 200:
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
if response.status_code == 401:
|
|
603
|
+
raise AuthenticationError("Invalid API key")
|
|
604
|
+
|
|
605
|
+
if response.status_code == 404:
|
|
606
|
+
raise SessionNotFoundError(f"Session not found")
|
|
607
|
+
|
|
608
|
+
if response.status_code == 429:
|
|
609
|
+
try:
|
|
610
|
+
data = response.json()
|
|
611
|
+
retry_after = data.get('detail', {}).get('retry_after')
|
|
612
|
+
raise RateLimitError(
|
|
613
|
+
f"Rate limit exceeded",
|
|
614
|
+
retry_after=retry_after
|
|
615
|
+
)
|
|
616
|
+
except (ValueError, KeyError):
|
|
617
|
+
raise RateLimitError("Rate limit exceeded")
|
|
618
|
+
|
|
619
|
+
# Generic error
|
|
620
|
+
try:
|
|
621
|
+
error_detail = response.json().get('detail', response.text)
|
|
622
|
+
except ValueError:
|
|
623
|
+
error_detail = response.text
|
|
624
|
+
|
|
625
|
+
raise OlbrainError(
|
|
626
|
+
f"Failed to {operation}: {response.status_code} - {error_detail}"
|
|
627
|
+
)
|