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 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()
@@ -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