ai-computer-client 0.3.2__py3-none-any.whl → 0.3.4__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.
ai_computer/models.py ADDED
@@ -0,0 +1,45 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional, Dict
3
+
4
+ @dataclass
5
+ class SandboxResponse:
6
+ """Response from sandbox operations.
7
+
8
+ Attributes:
9
+ success: Whether the operation was successful
10
+ data: Optional response data
11
+ error: Optional error message if operation failed
12
+ """
13
+ success: bool
14
+ data: Optional[Dict] = None
15
+ error: Optional[str] = None
16
+
17
+ @dataclass
18
+ class StreamEvent:
19
+ """Event from streaming code execution.
20
+
21
+ Attributes:
22
+ type: Type of event ('stdout', 'stderr', 'info', 'error', 'completed', 'keepalive')
23
+ data: Event data
24
+ """
25
+ type: str
26
+ data: str
27
+
28
+ @dataclass
29
+ class FileOperationResponse:
30
+ """Response from file operations.
31
+
32
+ Attributes:
33
+ success: Whether the operation was successful
34
+ filename: Name of the file
35
+ size: Size of the file in bytes
36
+ path: Path where the file was saved
37
+ message: Optional status message
38
+ error: Optional error message if operation failed
39
+ """
40
+ success: bool
41
+ filename: Optional[str] = None
42
+ size: Optional[int] = None
43
+ path: Optional[str] = None
44
+ message: Optional[str] = None
45
+ error: Optional[str] = None
@@ -0,0 +1,5 @@
1
+ from .filesystem import FileSystemModule
2
+ from .shell import ShellModule
3
+ from .code import CodeModule
4
+
5
+ __all__ = ["FileSystemModule", "ShellModule", "CodeModule"]
@@ -0,0 +1,81 @@
1
+ from typing import Optional, Dict, Any
2
+ import aiohttp
3
+ from ..models import SandboxResponse
4
+
5
+ class BaseSubmodule:
6
+ """Base class for all submodules.
7
+
8
+ This class provides common functionality for all submodules, including
9
+ access to the parent client's authentication token and sandbox ID.
10
+
11
+ Attributes:
12
+ _client: Reference to the parent SandboxClient
13
+ """
14
+
15
+ def __init__(self, client):
16
+ """Initialize the submodule.
17
+
18
+ Args:
19
+ client: The parent SandboxClient instance
20
+ """
21
+ self._client = client
22
+
23
+ @property
24
+ def base_url(self) -> str:
25
+ """Get the base URL from the parent client."""
26
+ return self._client.base_url
27
+
28
+ @property
29
+ def token(self) -> Optional[str]:
30
+ """Get the authentication token from the parent client."""
31
+ return self._client.token
32
+
33
+ @property
34
+ def sandbox_id(self) -> Optional[str]:
35
+ """Get the sandbox ID from the parent client."""
36
+ return self._client.sandbox_id
37
+
38
+ async def _ensure_ready(self) -> SandboxResponse:
39
+ """Ensure the sandbox is ready for operations.
40
+
41
+ Returns:
42
+ SandboxResponse indicating if the sandbox is ready
43
+ """
44
+ if not self.token or not self.sandbox_id:
45
+ return SandboxResponse(
46
+ success=False,
47
+ error="Client not properly initialized. Call setup() first"
48
+ )
49
+
50
+ # Ensure sandbox is ready
51
+ return await self._client.wait_for_ready()
52
+
53
+ def _get_headers(self, content_type: str = "application/json") -> Dict[str, str]:
54
+ """Get the headers for API requests.
55
+
56
+ Args:
57
+ content_type: The content type for the request
58
+
59
+ Returns:
60
+ Dictionary of headers
61
+ """
62
+ return {
63
+ "Authorization": f"Bearer {self.token}",
64
+ "Content-Type": content_type
65
+ }
66
+
67
+ async def _handle_response(self, response: aiohttp.ClientResponse) -> SandboxResponse:
68
+ """Handle the API response.
69
+
70
+ Args:
71
+ response: The aiohttp response object
72
+
73
+ Returns:
74
+ SandboxResponse with the parsed response data
75
+ """
76
+ if response.status != 200:
77
+ error_text = await response.text()
78
+ return SandboxResponse(success=False, error=error_text)
79
+
80
+ result = await response.json()
81
+ return SandboxResponse(success=True, data=result)
@@ -0,0 +1,295 @@
1
+ import aiohttp
2
+ import asyncio
3
+ from typing import Optional, Dict, AsyncGenerator, Any
4
+
5
+ from .base import BaseSubmodule
6
+ from ..models import SandboxResponse, StreamEvent
7
+
8
+ class CodeModule(BaseSubmodule):
9
+ """Code execution operations for the sandbox environment.
10
+
11
+ This module provides methods for executing Python code in the sandbox.
12
+ """
13
+
14
+ async def execute(
15
+ self,
16
+ code: str,
17
+ timeout: int = 30,
18
+ environment: Optional[Dict[str, str]] = None
19
+ ) -> SandboxResponse:
20
+ """Execute Python code in the sandbox.
21
+
22
+ Args:
23
+ code: The Python code to execute
24
+ timeout: Maximum execution time in seconds
25
+ environment: Optional environment variables for the execution
26
+
27
+ Returns:
28
+ SandboxResponse containing execution results
29
+ """
30
+ ready = await self._ensure_ready()
31
+ if not ready.success:
32
+ return ready
33
+
34
+ headers = self._get_headers()
35
+
36
+ data = {
37
+ "code": code,
38
+ "timeout": timeout
39
+ }
40
+
41
+ if environment:
42
+ data["environment"] = environment
43
+
44
+ try:
45
+ async with aiohttp.ClientSession() as session:
46
+ async with session.post(
47
+ f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute",
48
+ headers=headers,
49
+ json=data
50
+ ) as response:
51
+ return await self._handle_response(response)
52
+
53
+ except Exception as e:
54
+ return SandboxResponse(success=False, error=f"Connection error: {str(e)}")
55
+
56
+ async def execute_stream(
57
+ self,
58
+ code: str,
59
+ timeout: int = 30,
60
+ environment: Optional[Dict[str, str]] = None
61
+ ) -> AsyncGenerator[StreamEvent, None]:
62
+ """Execute Python code in the sandbox with streaming output.
63
+
64
+ Args:
65
+ code: The Python code to execute
66
+ timeout: Maximum execution time in seconds
67
+ environment: Optional environment variables for the execution
68
+
69
+ Yields:
70
+ StreamEvent objects containing execution output
71
+ """
72
+ ready = await self._ensure_ready()
73
+ if not ready.success:
74
+ yield StreamEvent(type="error", data=ready.error or "Sandbox not ready")
75
+ return
76
+
77
+ headers = self._get_headers()
78
+
79
+ data = {
80
+ "code": code,
81
+ "timeout": timeout
82
+ }
83
+
84
+ if environment:
85
+ data["environment"] = environment
86
+
87
+ try:
88
+ async with aiohttp.ClientSession() as session:
89
+ async with session.post(
90
+ f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute/stream",
91
+ headers=headers,
92
+ json=data
93
+ ) as response:
94
+ if response.status != 200:
95
+ error_text = await response.text()
96
+ yield StreamEvent(type="error", data=error_text)
97
+ return
98
+
99
+ # Process the streaming response
100
+ async for line in response.content:
101
+ line = line.strip()
102
+ if not line:
103
+ continue
104
+
105
+ try:
106
+ event_data = line.decode('utf-8')
107
+ # Check if it's a JSON object
108
+ if event_data.startswith('{') and event_data.endswith('}'):
109
+ import json
110
+ event_json = json.loads(event_data)
111
+ event_type = event_json.get('type', 'info')
112
+ event_data = event_json.get('data', '')
113
+ yield StreamEvent(type=event_type, data=event_data)
114
+
115
+ # If execution is complete, break the loop
116
+ if event_type == 'completed':
117
+ break
118
+ else:
119
+ # Treat as stdout if not JSON
120
+ yield StreamEvent(type="stdout", data=event_data)
121
+ except Exception as e:
122
+ yield StreamEvent(type="error", data=f"Failed to parse event: {str(e)}")
123
+ break
124
+
125
+ except Exception as e:
126
+ yield StreamEvent(type="error", data=f"Connection error: {str(e)}")
127
+
128
+ async def execute_file(
129
+ self,
130
+ file_path: str,
131
+ timeout: int = 30,
132
+ environment: Optional[Dict[str, str]] = None
133
+ ) -> SandboxResponse:
134
+ """Execute a Python file in the sandbox.
135
+
136
+ Args:
137
+ file_path: Path to the Python file in the sandbox
138
+ timeout: Maximum execution time in seconds
139
+ environment: Optional environment variables for the execution
140
+
141
+ Returns:
142
+ SandboxResponse containing execution results
143
+ """
144
+ ready = await self._ensure_ready()
145
+ if not ready.success:
146
+ return ready
147
+
148
+ # Execute code to check if the file exists and is a Python file
149
+ check_code = f"""
150
+ import os
151
+
152
+ file_path = "{file_path}"
153
+ if not os.path.exists(file_path):
154
+ result = {{"success": False, "error": f"File not found: {{file_path}}"}}
155
+ elif not os.path.isfile(file_path):
156
+ result = {{"success": False, "error": f"Not a file: {{file_path}}"}}
157
+ elif not file_path.endswith('.py'):
158
+ result = {{"success": False, "error": f"Not a Python file: {{file_path}}"}}
159
+ else:
160
+ result = {{"success": True}}
161
+
162
+ result
163
+ """
164
+ check_response = await self.execute(check_code)
165
+ if not check_response.success:
166
+ return check_response
167
+
168
+ result = check_response.data.get('result', {})
169
+ if not result.get('success', False):
170
+ return SandboxResponse(
171
+ success=False,
172
+ error=result.get('error', 'Unknown error checking file')
173
+ )
174
+
175
+ # Execute the Python file
176
+ headers = self._get_headers()
177
+
178
+ data = {
179
+ "file_path": file_path,
180
+ "timeout": timeout
181
+ }
182
+
183
+ if environment:
184
+ data["environment"] = environment
185
+
186
+ try:
187
+ async with aiohttp.ClientSession() as session:
188
+ async with session.post(
189
+ f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute",
190
+ headers=headers,
191
+ json=data
192
+ ) as response:
193
+ return await self._handle_response(response)
194
+
195
+ except Exception as e:
196
+ return SandboxResponse(success=False, error=f"Connection error: {str(e)}")
197
+
198
+ async def execute_file_stream(
199
+ self,
200
+ file_path: str,
201
+ timeout: int = 30,
202
+ environment: Optional[Dict[str, str]] = None
203
+ ) -> AsyncGenerator[StreamEvent, None]:
204
+ """Execute a Python file in the sandbox with streaming output.
205
+
206
+ Args:
207
+ file_path: Path to the Python file in the sandbox
208
+ timeout: Maximum execution time in seconds
209
+ environment: Optional environment variables for the execution
210
+
211
+ Yields:
212
+ StreamEvent objects containing execution output
213
+ """
214
+ ready = await self._ensure_ready()
215
+ if not ready.success:
216
+ yield StreamEvent(type="error", data=ready.error or "Sandbox not ready")
217
+ return
218
+
219
+ # Execute code to check if the file exists and is a Python file
220
+ check_code = f"""
221
+ import os
222
+
223
+ file_path = "{file_path}"
224
+ if not os.path.exists(file_path):
225
+ result = {{"success": False, "error": f"File not found: {{file_path}}"}}
226
+ elif not os.path.isfile(file_path):
227
+ result = {{"success": False, "error": f"Not a file: {{file_path}}"}}
228
+ elif not file_path.endswith('.py'):
229
+ result = {{"success": False, "error": f"Not a Python file: {{file_path}}"}}
230
+ else:
231
+ result = {{"success": True}}
232
+
233
+ result
234
+ """
235
+ check_response = await self.execute(check_code)
236
+ if not check_response.success:
237
+ yield StreamEvent(type="error", data=check_response.error or "Failed to check file")
238
+ return
239
+
240
+ result = check_response.data.get('result', {})
241
+ if not result.get('success', False):
242
+ yield StreamEvent(type="error", data=result.get('error', 'Unknown error checking file'))
243
+ return
244
+
245
+ # Execute the Python file with streaming output
246
+ headers = self._get_headers()
247
+
248
+ data = {
249
+ "file_path": file_path,
250
+ "timeout": timeout
251
+ }
252
+
253
+ if environment:
254
+ data["environment"] = environment
255
+
256
+ try:
257
+ async with aiohttp.ClientSession() as session:
258
+ async with session.post(
259
+ f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute/file/stream",
260
+ headers=headers,
261
+ json=data
262
+ ) as response:
263
+ if response.status != 200:
264
+ error_text = await response.text()
265
+ yield StreamEvent(type="error", data=error_text)
266
+ return
267
+
268
+ # Process the streaming response
269
+ async for line in response.content:
270
+ line = line.strip()
271
+ if not line:
272
+ continue
273
+
274
+ try:
275
+ event_data = line.decode('utf-8')
276
+ # Check if it's a JSON object
277
+ if event_data.startswith('{') and event_data.endswith('}'):
278
+ import json
279
+ event_json = json.loads(event_data)
280
+ event_type = event_json.get('type', 'info')
281
+ event_data = event_json.get('data', '')
282
+ yield StreamEvent(type=event_type, data=event_data)
283
+
284
+ # If execution is complete, break the loop
285
+ if event_type == 'completed':
286
+ break
287
+ else:
288
+ # Treat as stdout if not JSON
289
+ yield StreamEvent(type="stdout", data=event_data)
290
+ except Exception as e:
291
+ yield StreamEvent(type="error", data=f"Failed to parse event: {str(e)}")
292
+ break
293
+
294
+ except Exception as e:
295
+ yield StreamEvent(type="error", data=f"Connection error: {str(e)}")