agi-python 0.0.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.
- agi/__init__.py +70 -0
- agi/_http.py +135 -0
- agi/_session_context.py +345 -0
- agi/_sse.py +77 -0
- agi/client.py +142 -0
- agi/exceptions.py +43 -0
- agi/py.typed +0 -0
- agi/resources/__init__.py +5 -0
- agi/resources/sessions.py +358 -0
- agi/types/__init__.py +39 -0
- agi/types/results.py +183 -0
- agi/types/sessions.py +119 -0
- agi/types/shared.py +26 -0
- agi_python-0.0.1.dist-info/METADATA +363 -0
- agi_python-0.0.1.dist-info/RECORD +18 -0
- agi_python-0.0.1.dist-info/WHEEL +5 -0
- agi_python-0.0.1.dist-info/licenses/LICENSE +21 -0
- agi_python-0.0.1.dist-info/top_level.txt +1 -0
agi/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Official Python SDK for AGI.tech API.
|
|
2
|
+
|
|
3
|
+
The agi package provides a complete Python SDK for the AGI.tech API,
|
|
4
|
+
enabling developers to create and manage AI agent sessions that can perform
|
|
5
|
+
complex web tasks.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from agi import AGIClient
|
|
9
|
+
>>>
|
|
10
|
+
>>> client = AGIClient(api_key="your_api_key")
|
|
11
|
+
>>>
|
|
12
|
+
>>> with client.session("agi-0") as session:
|
|
13
|
+
... result = session.run_task(
|
|
14
|
+
... "Find three nonstop SFO→JFK flights next month under $450"
|
|
15
|
+
... )
|
|
16
|
+
... print(result)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from agi.client import AGIClient
|
|
20
|
+
from agi.exceptions import (
|
|
21
|
+
AgentExecutionError,
|
|
22
|
+
AGIError,
|
|
23
|
+
APIError,
|
|
24
|
+
AuthenticationError,
|
|
25
|
+
NotFoundError,
|
|
26
|
+
PermissionError,
|
|
27
|
+
RateLimitError,
|
|
28
|
+
)
|
|
29
|
+
from agi.types.results import Screenshot, TaskMetadata, TaskResult
|
|
30
|
+
from agi.types.sessions import (
|
|
31
|
+
DeleteResponse,
|
|
32
|
+
ExecuteStatusResponse,
|
|
33
|
+
MessageResponse,
|
|
34
|
+
MessagesResponse,
|
|
35
|
+
NavigateResponse,
|
|
36
|
+
ScreenshotResponse,
|
|
37
|
+
SessionResponse,
|
|
38
|
+
SSEEvent,
|
|
39
|
+
SuccessResponse,
|
|
40
|
+
)
|
|
41
|
+
from agi.types.shared import EventType, MessageType, SessionStatus, SnapshotMode
|
|
42
|
+
|
|
43
|
+
__version__ = "0.0.1"
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"AGIClient",
|
|
47
|
+
"AGIError",
|
|
48
|
+
"APIError",
|
|
49
|
+
"AgentExecutionError",
|
|
50
|
+
"AuthenticationError",
|
|
51
|
+
"NotFoundError",
|
|
52
|
+
"PermissionError",
|
|
53
|
+
"RateLimitError",
|
|
54
|
+
"SessionResponse",
|
|
55
|
+
"SSEEvent",
|
|
56
|
+
"MessageResponse",
|
|
57
|
+
"MessagesResponse",
|
|
58
|
+
"ExecuteStatusResponse",
|
|
59
|
+
"DeleteResponse",
|
|
60
|
+
"NavigateResponse",
|
|
61
|
+
"Screenshot",
|
|
62
|
+
"ScreenshotResponse",
|
|
63
|
+
"SuccessResponse",
|
|
64
|
+
"TaskResult",
|
|
65
|
+
"TaskMetadata",
|
|
66
|
+
"EventType",
|
|
67
|
+
"MessageType",
|
|
68
|
+
"SessionStatus",
|
|
69
|
+
"SnapshotMode",
|
|
70
|
+
]
|
agi/_http.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""HTTP client for AGI API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from agi.exceptions import (
|
|
11
|
+
AGIError,
|
|
12
|
+
APIError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
PermissionError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HTTPClient:
|
|
21
|
+
"""HTTP client with retry logic and error handling."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
api_key: str,
|
|
26
|
+
base_url: str = "https://api.agi.tech",
|
|
27
|
+
timeout: int = 60,
|
|
28
|
+
max_retries: int = 3,
|
|
29
|
+
):
|
|
30
|
+
"""Initialize HTTP client.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
api_key: API key for authentication
|
|
34
|
+
base_url: Base URL for API
|
|
35
|
+
timeout: Request timeout in seconds
|
|
36
|
+
max_retries: Maximum number of retry attempts for 5xx errors
|
|
37
|
+
"""
|
|
38
|
+
self._client = httpx.Client(
|
|
39
|
+
base_url=base_url,
|
|
40
|
+
headers={
|
|
41
|
+
"Authorization": f"Bearer {api_key}",
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
},
|
|
44
|
+
timeout=timeout,
|
|
45
|
+
)
|
|
46
|
+
self._max_retries = max_retries
|
|
47
|
+
|
|
48
|
+
def request(
|
|
49
|
+
self,
|
|
50
|
+
method: str,
|
|
51
|
+
path: str,
|
|
52
|
+
**kwargs: Any,
|
|
53
|
+
) -> httpx.Response:
|
|
54
|
+
"""Make HTTP request with retry logic.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
method: HTTP method (GET, POST, etc.)
|
|
58
|
+
path: Request path
|
|
59
|
+
**kwargs: Additional arguments for httpx.request()
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
HTTP response
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
AGIError: On API errors
|
|
66
|
+
"""
|
|
67
|
+
last_exception: httpx.HTTPStatusError | None = None
|
|
68
|
+
|
|
69
|
+
for attempt in range(self._max_retries):
|
|
70
|
+
try:
|
|
71
|
+
response = self._client.request(method, path, **kwargs)
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
return response
|
|
74
|
+
|
|
75
|
+
except httpx.HTTPStatusError as e:
|
|
76
|
+
if e.response.status_code >= 500 and attempt < self._max_retries - 1:
|
|
77
|
+
wait_time = 2**attempt
|
|
78
|
+
time.sleep(wait_time)
|
|
79
|
+
last_exception = e
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
self._handle_error(e.response)
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
except (httpx.RequestError, httpx.TimeoutException) as e:
|
|
86
|
+
if attempt < self._max_retries - 1:
|
|
87
|
+
wait_time = 2**attempt
|
|
88
|
+
time.sleep(wait_time)
|
|
89
|
+
continue
|
|
90
|
+
raise APIError(f"Request failed: {str(e)}") from e
|
|
91
|
+
|
|
92
|
+
if last_exception:
|
|
93
|
+
raise APIError(f"Max retries exceeded: {str(last_exception)}") from last_exception
|
|
94
|
+
|
|
95
|
+
raise APIError("Request failed")
|
|
96
|
+
|
|
97
|
+
def _handle_error(self, response: httpx.Response) -> None:
|
|
98
|
+
"""Map HTTP errors to SDK exceptions.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
response: HTTP response with error status
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
Specific AGIError subclass based on status code
|
|
105
|
+
"""
|
|
106
|
+
status_code = response.status_code
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
error_data = response.json()
|
|
110
|
+
error_message = error_data.get("detail", response.text)
|
|
111
|
+
except Exception:
|
|
112
|
+
error_message = response.text
|
|
113
|
+
|
|
114
|
+
if status_code == 401:
|
|
115
|
+
raise AuthenticationError(f"Authentication failed: {error_message}")
|
|
116
|
+
elif status_code == 403:
|
|
117
|
+
raise PermissionError(f"Permission denied: {error_message}")
|
|
118
|
+
elif status_code == 404:
|
|
119
|
+
raise NotFoundError(f"Resource not found: {error_message}")
|
|
120
|
+
elif status_code == 429:
|
|
121
|
+
raise RateLimitError(f"Rate limit exceeded: {error_message}")
|
|
122
|
+
elif status_code >= 500:
|
|
123
|
+
raise APIError(f"Server error ({status_code}): {error_message}")
|
|
124
|
+
else:
|
|
125
|
+
raise AGIError(f"API error ({status_code}): {error_message}")
|
|
126
|
+
|
|
127
|
+
def close(self) -> None:
|
|
128
|
+
"""Close the HTTP client."""
|
|
129
|
+
self._client.close()
|
|
130
|
+
|
|
131
|
+
def __enter__(self) -> HTTPClient:
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
def __exit__(self, *args: Any) -> None:
|
|
135
|
+
self.close()
|
agi/_session_context.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Session context manager for high-level API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from agi.exceptions import AgentExecutionError
|
|
11
|
+
from agi.types.results import Screenshot, TaskMetadata, TaskResult
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from agi.client import AGIClient
|
|
15
|
+
from agi.types.sessions import (
|
|
16
|
+
ExecuteStatusResponse,
|
|
17
|
+
MessagesResponse,
|
|
18
|
+
NavigateResponse,
|
|
19
|
+
SSEEvent,
|
|
20
|
+
SuccessResponse,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SessionContext:
|
|
25
|
+
"""High-level session context manager matching docs pattern.
|
|
26
|
+
|
|
27
|
+
This provides the simple API shown in documentation:
|
|
28
|
+
with client.session("agi-0") as session:
|
|
29
|
+
result = session.run_task("Find cheapest iPhone 15...")
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> from agi import AGIClient
|
|
33
|
+
>>> client = AGIClient(api_key="...")
|
|
34
|
+
>>>
|
|
35
|
+
>>> with client.session("agi-0") as session:
|
|
36
|
+
... result = session.run_task("Find flights SFO→JFK under $450")
|
|
37
|
+
... print(result)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
client: AGIClient,
|
|
43
|
+
agent_name: str = "agi-0",
|
|
44
|
+
**create_kwargs: Any,
|
|
45
|
+
):
|
|
46
|
+
"""Initialize session context.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
client: AGIClient instance
|
|
50
|
+
agent_name: Agent model to use
|
|
51
|
+
**create_kwargs: Additional arguments for session creation
|
|
52
|
+
(webhook_url, goal, max_steps, restore_from_environment_id)
|
|
53
|
+
"""
|
|
54
|
+
self._client = client
|
|
55
|
+
self._agent_name = agent_name
|
|
56
|
+
self._create_kwargs = create_kwargs
|
|
57
|
+
self.session_id: str | None = None
|
|
58
|
+
self.vnc_url: str | None = None
|
|
59
|
+
self.agent_url: str | None = None
|
|
60
|
+
|
|
61
|
+
def __enter__(self) -> SessionContext:
|
|
62
|
+
"""Create session on context entry.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
SessionContext instance
|
|
66
|
+
"""
|
|
67
|
+
response = self._client.sessions.create(
|
|
68
|
+
agent_name=self._agent_name,
|
|
69
|
+
**self._create_kwargs,
|
|
70
|
+
)
|
|
71
|
+
self.session_id = response.session_id
|
|
72
|
+
self.vnc_url = response.vnc_url
|
|
73
|
+
self.agent_url = response.agent_url
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
def __exit__(self, *args: Any) -> None:
|
|
77
|
+
"""Delete session on context exit."""
|
|
78
|
+
if self.session_id:
|
|
79
|
+
try:
|
|
80
|
+
self._client.sessions.delete(self.session_id)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def run_task(
|
|
85
|
+
self,
|
|
86
|
+
task: str,
|
|
87
|
+
start_url: str | None = None,
|
|
88
|
+
timeout: int = 600,
|
|
89
|
+
poll_interval: float = 3.0,
|
|
90
|
+
) -> TaskResult:
|
|
91
|
+
"""Send task and wait for completion using polling.
|
|
92
|
+
|
|
93
|
+
This is the primary method matching the docs example.
|
|
94
|
+
It sends the task message and polls for completion status.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
task: Natural language task description
|
|
98
|
+
start_url: Optional starting URL for the task
|
|
99
|
+
timeout: Maximum time to wait in seconds (default: 600)
|
|
100
|
+
poll_interval: Polling interval in seconds (default: 3.0)
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
TaskResult with data and metadata
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
AgentExecutionError: If task fails or times out
|
|
107
|
+
ValueError: If session not created
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
>>> with client.session("agi-0") as session:
|
|
111
|
+
... result = session.run_task(
|
|
112
|
+
... "Find three nonstop SFO→JFK flights next month under $450"
|
|
113
|
+
... )
|
|
114
|
+
... print(result.data)
|
|
115
|
+
... print(f"Duration: {result.metadata.duration}s")
|
|
116
|
+
"""
|
|
117
|
+
if not self.session_id:
|
|
118
|
+
raise ValueError("Session not created. Use context manager 'with' statement.")
|
|
119
|
+
|
|
120
|
+
self._client.sessions.send_message(
|
|
121
|
+
self.session_id,
|
|
122
|
+
message=task,
|
|
123
|
+
start_url=start_url,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
start_time = time.time()
|
|
127
|
+
|
|
128
|
+
while True:
|
|
129
|
+
elapsed = time.time() - start_time
|
|
130
|
+
if elapsed > timeout:
|
|
131
|
+
raise AgentExecutionError(
|
|
132
|
+
f"Task exceeded timeout of {timeout}s (elapsed: {elapsed:.1f}s)"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
status_response = self._client.sessions.get_status(self.session_id)
|
|
136
|
+
|
|
137
|
+
if status_response.status in ("finished", "waiting_for_input"):
|
|
138
|
+
messages_response = self._client.sessions.get_messages(self.session_id)
|
|
139
|
+
messages = messages_response.messages
|
|
140
|
+
|
|
141
|
+
# Find DONE or QUESTION message
|
|
142
|
+
done_msg = None
|
|
143
|
+
for msg in messages:
|
|
144
|
+
if msg.type in ("DONE", "QUESTION"):
|
|
145
|
+
done_msg = msg
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
if not done_msg:
|
|
149
|
+
raise AgentExecutionError(
|
|
150
|
+
f"Task status '{status_response.status}' but no DONE/QUESTION message found."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Extract task data
|
|
154
|
+
content = done_msg.content
|
|
155
|
+
if isinstance(content, dict):
|
|
156
|
+
data = content
|
|
157
|
+
else:
|
|
158
|
+
data = {"content": content} if content else {}
|
|
159
|
+
|
|
160
|
+
duration = time.time() - start_time
|
|
161
|
+
steps = sum(1 for msg in messages if msg.type in ("THOUGHT", "QUESTION", "DONE"))
|
|
162
|
+
|
|
163
|
+
metadata = TaskMetadata(
|
|
164
|
+
task_id=done_msg.id,
|
|
165
|
+
session_id=self.session_id,
|
|
166
|
+
duration=duration,
|
|
167
|
+
cost=0.0,
|
|
168
|
+
timestamp=datetime.now(),
|
|
169
|
+
steps=steps,
|
|
170
|
+
success=True,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return TaskResult(data=data, metadata=metadata)
|
|
174
|
+
|
|
175
|
+
if status_response.status == "error":
|
|
176
|
+
messages_response = self._client.sessions.get_messages(self.session_id)
|
|
177
|
+
|
|
178
|
+
error_details = "Unknown error"
|
|
179
|
+
for msg in messages_response.messages:
|
|
180
|
+
if msg.type == "ERROR":
|
|
181
|
+
if isinstance(msg.content, str):
|
|
182
|
+
error_details = msg.content if msg.content else "Unknown error"
|
|
183
|
+
else:
|
|
184
|
+
error_details = str(msg.content)
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
raise AgentExecutionError(f"Task failed: {error_details}")
|
|
188
|
+
|
|
189
|
+
time.sleep(poll_interval)
|
|
190
|
+
|
|
191
|
+
def pause(self) -> SuccessResponse:
|
|
192
|
+
"""Pause task execution.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
SuccessResponse confirming pause
|
|
196
|
+
"""
|
|
197
|
+
if not self.session_id:
|
|
198
|
+
raise ValueError("Session not created")
|
|
199
|
+
return self._client.sessions.pause(self.session_id)
|
|
200
|
+
|
|
201
|
+
def resume(self) -> SuccessResponse:
|
|
202
|
+
"""Resume paused task.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
SuccessResponse confirming resume
|
|
206
|
+
"""
|
|
207
|
+
if not self.session_id:
|
|
208
|
+
raise ValueError("Session not created")
|
|
209
|
+
return self._client.sessions.resume(self.session_id)
|
|
210
|
+
|
|
211
|
+
def cancel(self) -> SuccessResponse:
|
|
212
|
+
"""Cancel task execution.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
SuccessResponse confirming cancellation
|
|
216
|
+
"""
|
|
217
|
+
if not self.session_id:
|
|
218
|
+
raise ValueError("Session not created")
|
|
219
|
+
return self._client.sessions.cancel(self.session_id)
|
|
220
|
+
|
|
221
|
+
def navigate(self, url: str) -> NavigateResponse:
|
|
222
|
+
"""Navigate browser to URL.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
url: URL to navigate to
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
NavigateResponse with current URL
|
|
229
|
+
"""
|
|
230
|
+
if not self.session_id:
|
|
231
|
+
raise ValueError("Session not created")
|
|
232
|
+
return self._client.sessions.navigate(self.session_id, url)
|
|
233
|
+
|
|
234
|
+
def screenshot(self) -> Screenshot:
|
|
235
|
+
"""Get browser screenshot.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Screenshot with decoded image data and save() method
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
>>> screenshot = session.screenshot()
|
|
242
|
+
>>> screenshot.save("page.png")
|
|
243
|
+
>>> print(f"Size: {screenshot.width}x{screenshot.height}")
|
|
244
|
+
"""
|
|
245
|
+
if not self.session_id:
|
|
246
|
+
raise ValueError("Session not created")
|
|
247
|
+
|
|
248
|
+
response = self._client.sessions.screenshot(self.session_id)
|
|
249
|
+
return Screenshot.from_base64(
|
|
250
|
+
base64_data=response.screenshot, url=response.url, title=response.title
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def send_message(
|
|
254
|
+
self,
|
|
255
|
+
message: str,
|
|
256
|
+
start_url: str | None = None,
|
|
257
|
+
config_updates: dict[str, Any] | None = None,
|
|
258
|
+
) -> SuccessResponse:
|
|
259
|
+
"""Send message to agent to start a task or respond to questions.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
message: Message content (task instruction or response)
|
|
263
|
+
start_url: Optional starting URL for the task
|
|
264
|
+
config_updates: Optional configuration updates
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
SuccessResponse confirming message sent
|
|
268
|
+
|
|
269
|
+
Example:
|
|
270
|
+
>>> with client.session("agi-0") as session:
|
|
271
|
+
... session.send_message("Find flights from SFO to JFK under $450")
|
|
272
|
+
"""
|
|
273
|
+
if not self.session_id:
|
|
274
|
+
raise ValueError("Session not created")
|
|
275
|
+
return self._client.sessions.send_message(
|
|
276
|
+
self.session_id, message, start_url, config_updates
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def get_status(self) -> ExecuteStatusResponse:
|
|
280
|
+
"""Get current execution status of the session.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
ExecuteStatusResponse with status ("running", "finished", etc.)
|
|
284
|
+
|
|
285
|
+
Example:
|
|
286
|
+
>>> with client.session("agi-0") as session:
|
|
287
|
+
... session.send_message("Research topic...")
|
|
288
|
+
... status = session.get_status()
|
|
289
|
+
... print(status.status)
|
|
290
|
+
"""
|
|
291
|
+
if not self.session_id:
|
|
292
|
+
raise ValueError("Session not created")
|
|
293
|
+
return self._client.sessions.get_status(self.session_id)
|
|
294
|
+
|
|
295
|
+
def get_messages(self, after_id: int = 0, sanitize: bool = True) -> MessagesResponse:
|
|
296
|
+
"""Get messages from the session.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
after_id: Return messages with ID > after_id (for polling)
|
|
300
|
+
sanitize: Filter out system messages, prompts, and images
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
MessagesResponse with messages list and status
|
|
304
|
+
|
|
305
|
+
Example:
|
|
306
|
+
>>> with client.session("agi-0") as session:
|
|
307
|
+
... messages = session.get_messages(after_id=0)
|
|
308
|
+
... for msg in messages.messages:
|
|
309
|
+
... print(f"[{msg.type}] {msg.content}")
|
|
310
|
+
"""
|
|
311
|
+
if not self.session_id:
|
|
312
|
+
raise ValueError("Session not created")
|
|
313
|
+
return self._client.sessions.get_messages(self.session_id, after_id, sanitize)
|
|
314
|
+
|
|
315
|
+
def stream_events(
|
|
316
|
+
self,
|
|
317
|
+
event_types: list[str] | None = None,
|
|
318
|
+
sanitize: bool = True,
|
|
319
|
+
include_history: bool = True,
|
|
320
|
+
) -> Iterator[SSEEvent]:
|
|
321
|
+
"""Stream real-time events from the session via Server-Sent Events.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
event_types: Filter specific event types (e.g., ["thought", "done"])
|
|
325
|
+
sanitize: Filter out system messages
|
|
326
|
+
include_history: Include historical messages on connection
|
|
327
|
+
|
|
328
|
+
Yields:
|
|
329
|
+
SSEEvent objects with id, event type, and data
|
|
330
|
+
|
|
331
|
+
Example:
|
|
332
|
+
>>> with client.session("agi-0") as session:
|
|
333
|
+
... session.send_message("Research company XYZ")
|
|
334
|
+
... for event in session.stream_events():
|
|
335
|
+
... if event.event == "thought":
|
|
336
|
+
... print(f"Agent: {event.data}")
|
|
337
|
+
... elif event.event == "done":
|
|
338
|
+
... print(f"Result: {event.data}")
|
|
339
|
+
... break
|
|
340
|
+
"""
|
|
341
|
+
if not self.session_id:
|
|
342
|
+
raise ValueError("Session not created")
|
|
343
|
+
yield from self._client.sessions.stream_events(
|
|
344
|
+
self.session_id, event_types, sanitize, include_history
|
|
345
|
+
)
|
agi/_sse.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Server-Sent Events (SSE) streaming client."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from agi.types.sessions import SSEEvent
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from agi._http import HTTPClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SSEClient:
|
|
14
|
+
"""Client for handling Server-Sent Events streams."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, http: "HTTPClient"):
|
|
17
|
+
"""Initialize SSE client.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
http: HTTP client instance
|
|
21
|
+
"""
|
|
22
|
+
self._http = http
|
|
23
|
+
|
|
24
|
+
def stream(self, path: str, params: dict[str, Any] | None = None) -> Iterator[SSEEvent]:
|
|
25
|
+
"""Connect to SSE endpoint and yield events.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
path: API endpoint path
|
|
29
|
+
params: Query parameters
|
|
30
|
+
|
|
31
|
+
Yields:
|
|
32
|
+
SSEEvent objects
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> for event in sse.stream("/v1/sessions/123/events"):
|
|
36
|
+
... if event.event == "done":
|
|
37
|
+
... break
|
|
38
|
+
"""
|
|
39
|
+
clean_params = {k: v for k, v in (params or {}).items() if v is not None}
|
|
40
|
+
|
|
41
|
+
with self._http._client.stream("GET", path, params=clean_params) as response:
|
|
42
|
+
response.raise_for_status()
|
|
43
|
+
|
|
44
|
+
event_id: str | None = None
|
|
45
|
+
event_type: str | None = None
|
|
46
|
+
data_lines: list[str] = []
|
|
47
|
+
|
|
48
|
+
for line in response.iter_lines():
|
|
49
|
+
line = line.strip()
|
|
50
|
+
|
|
51
|
+
if not line:
|
|
52
|
+
if event_type and data_lines:
|
|
53
|
+
data_str = "".join(data_lines)
|
|
54
|
+
try:
|
|
55
|
+
data = json.loads(data_str)
|
|
56
|
+
except json.JSONDecodeError:
|
|
57
|
+
data = {"content": data_str}
|
|
58
|
+
|
|
59
|
+
yield SSEEvent(
|
|
60
|
+
id=event_id,
|
|
61
|
+
event=event_type, # type: ignore
|
|
62
|
+
data=data,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
event_id = None
|
|
66
|
+
event_type = None
|
|
67
|
+
data_lines = []
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if line.startswith("id:"):
|
|
71
|
+
event_id = line[3:].strip()
|
|
72
|
+
elif line.startswith("event:"):
|
|
73
|
+
event_type = line[6:].strip()
|
|
74
|
+
elif line.startswith("data:"):
|
|
75
|
+
data_lines.append(line[5:].strip())
|
|
76
|
+
elif line.startswith(":"):
|
|
77
|
+
continue
|