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/__init__.py +13 -3
- ai_computer/client.py +188 -549
- ai_computer/models.py +45 -0
- ai_computer/submodules/__init__.py +5 -0
- ai_computer/submodules/base.py +81 -0
- ai_computer/submodules/code.py +295 -0
- ai_computer/submodules/filesystem.py +438 -0
- ai_computer/submodules/shell.py +52 -0
- {ai_computer_client-0.3.2.dist-info → ai_computer_client-0.3.4.dist-info}/METADATA +28 -5
- ai_computer_client-0.3.4.dist-info/RECORD +12 -0
- ai_computer_client-0.3.2.dist-info/RECORD +0 -6
- {ai_computer_client-0.3.2.dist-info → ai_computer_client-0.3.4.dist-info}/WHEEL +0 -0
- {ai_computer_client-0.3.2.dist-info → ai_computer_client-0.3.4.dist-info}/licenses/LICENSE +0 -0
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,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)}")
|