asteroid-odyssey 1.1.0__py3-none-any.whl → 1.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.
Files changed (76) hide show
  1. asteroid_odyssey/__init__.py +10 -2
  2. asteroid_odyssey/agents_v1_gen/__init__.py +87 -0
  3. asteroid_odyssey/agents_v1_gen/api/__init__.py +7 -0
  4. asteroid_odyssey/agents_v1_gen/api/agent_profile_api.py +1696 -0
  5. asteroid_odyssey/{openapi_client → agents_v1_gen}/api/api_api.py +4 -4
  6. asteroid_odyssey/{openapi_client → agents_v1_gen}/api/execution_api.py +10 -10
  7. asteroid_odyssey/{openapi_client → agents_v1_gen}/api_client.py +6 -6
  8. asteroid_odyssey/{openapi_client → agents_v1_gen}/configuration.py +2 -2
  9. asteroid_odyssey/agents_v1_gen/models/__init__.py +34 -0
  10. asteroid_odyssey/agents_v1_gen/models/agent_profile.py +118 -0
  11. asteroid_odyssey/agents_v1_gen/models/country_code.py +44 -0
  12. asteroid_odyssey/agents_v1_gen/models/create_agent_profile_request.py +112 -0
  13. asteroid_odyssey/agents_v1_gen/models/credential.py +95 -0
  14. asteroid_odyssey/agents_v1_gen/models/delete_agent_profile200_response.py +87 -0
  15. asteroid_odyssey/{openapi_client → agents_v1_gen}/models/execution_result_response.py +2 -2
  16. asteroid_odyssey/{openapi_client → agents_v1_gen}/models/execution_status_response.py +1 -1
  17. asteroid_odyssey/agents_v1_gen/models/proxy_type.py +37 -0
  18. asteroid_odyssey/agents_v1_gen/models/update_agent_profile_request.py +112 -0
  19. asteroid_odyssey/{openapi_client → agents_v1_gen}/rest.py +1 -1
  20. asteroid_odyssey/agents_v2_gen/__init__.py +99 -0
  21. asteroid_odyssey/agents_v2_gen/api/__init__.py +5 -0
  22. asteroid_odyssey/agents_v2_gen/api/execution_api.py +625 -0
  23. asteroid_odyssey/agents_v2_gen/api_client.py +801 -0
  24. asteroid_odyssey/agents_v2_gen/api_response.py +21 -0
  25. asteroid_odyssey/agents_v2_gen/configuration.py +612 -0
  26. asteroid_odyssey/agents_v2_gen/exceptions.py +216 -0
  27. asteroid_odyssey/agents_v2_gen/models/__init__.py +42 -0
  28. asteroid_odyssey/agents_v2_gen/models/activity_payload_union_action_completed.py +100 -0
  29. asteroid_odyssey/agents_v2_gen/models/activity_payload_union_action_failed.py +100 -0
  30. asteroid_odyssey/agents_v2_gen/models/activity_payload_union_action_started.py +100 -0
  31. asteroid_odyssey/agents_v2_gen/models/activity_payload_union_generic.py +100 -0
  32. asteroid_odyssey/agents_v2_gen/models/activity_payload_union_status_changed.py +100 -0
  33. asteroid_odyssey/agents_v2_gen/models/activity_payload_union_step_completed.py +100 -0
  34. asteroid_odyssey/agents_v2_gen/models/activity_payload_union_step_started.py +100 -0
  35. asteroid_odyssey/agents_v2_gen/models/activity_payload_union_terminal.py +100 -0
  36. asteroid_odyssey/agents_v2_gen/models/activity_payload_union_transitioned_node.py +100 -0
  37. asteroid_odyssey/agents_v2_gen/models/activity_payload_union_user_message_received.py +100 -0
  38. asteroid_odyssey/agents_v2_gen/models/error.py +89 -0
  39. asteroid_odyssey/agents_v2_gen/models/execution_activity.py +98 -0
  40. asteroid_odyssey/agents_v2_gen/models/execution_activity_action_completed_payload.py +87 -0
  41. asteroid_odyssey/agents_v2_gen/models/execution_activity_action_failed_payload.py +87 -0
  42. asteroid_odyssey/agents_v2_gen/models/execution_activity_action_started_payload.py +87 -0
  43. asteroid_odyssey/agents_v2_gen/models/execution_activity_generic_payload.py +87 -0
  44. asteroid_odyssey/agents_v2_gen/models/execution_activity_payload_union.py +252 -0
  45. asteroid_odyssey/agents_v2_gen/models/execution_activity_status_changed_payload.py +88 -0
  46. asteroid_odyssey/agents_v2_gen/models/execution_activity_step_completed_payload.py +87 -0
  47. asteroid_odyssey/agents_v2_gen/models/execution_activity_step_started_payload.py +87 -0
  48. asteroid_odyssey/agents_v2_gen/models/execution_activity_transitioned_node_payload.py +89 -0
  49. asteroid_odyssey/agents_v2_gen/models/execution_activity_user_message_received_payload.py +89 -0
  50. asteroid_odyssey/agents_v2_gen/models/execution_status.py +43 -0
  51. asteroid_odyssey/agents_v2_gen/models/execution_terminal_payload.py +96 -0
  52. asteroid_odyssey/agents_v2_gen/models/execution_user_messages_add_text_body.py +87 -0
  53. asteroid_odyssey/agents_v2_gen/models/versions.py +37 -0
  54. asteroid_odyssey/agents_v2_gen/py.typed +0 -0
  55. asteroid_odyssey/agents_v2_gen/rest.py +258 -0
  56. asteroid_odyssey/client.py +635 -123
  57. {asteroid_odyssey-1.1.0.dist-info → asteroid_odyssey-1.2.0.dist-info}/METADATA +2 -1
  58. asteroid_odyssey-1.2.0.dist-info/RECORD +72 -0
  59. asteroid_odyssey/openapi_client/__init__.py +0 -71
  60. asteroid_odyssey/openapi_client/api/__init__.py +0 -6
  61. asteroid_odyssey/openapi_client/models/__init__.py +0 -27
  62. asteroid_odyssey-1.1.0.dist-info/RECORD +0 -28
  63. /asteroid_odyssey/{openapi_client → agents_v1_gen}/api_response.py +0 -0
  64. /asteroid_odyssey/{openapi_client → agents_v1_gen}/exceptions.py +0 -0
  65. /asteroid_odyssey/{openapi_client → agents_v1_gen}/models/browser_session_recording_response.py +0 -0
  66. /asteroid_odyssey/{openapi_client → agents_v1_gen}/models/error_response.py +0 -0
  67. /asteroid_odyssey/{openapi_client → agents_v1_gen}/models/execution_response.py +0 -0
  68. /asteroid_odyssey/{openapi_client → agents_v1_gen}/models/execution_result.py +0 -0
  69. /asteroid_odyssey/{openapi_client → agents_v1_gen}/models/health_check200_response.py +0 -0
  70. /asteroid_odyssey/{openapi_client → agents_v1_gen}/models/health_check500_response.py +0 -0
  71. /asteroid_odyssey/{openapi_client → agents_v1_gen}/models/status.py +0 -0
  72. /asteroid_odyssey/{openapi_client → agents_v1_gen}/models/structured_agent_execution_request.py +0 -0
  73. /asteroid_odyssey/{openapi_client → agents_v1_gen}/models/upload_execution_files200_response.py +0 -0
  74. /asteroid_odyssey/{openapi_client → agents_v1_gen}/py.typed +0 -0
  75. {asteroid_odyssey-1.1.0.dist-info → asteroid_odyssey-1.2.0.dist-info}/WHEEL +0 -0
  76. {asteroid_odyssey-1.1.0.dist-info → asteroid_odyssey-1.2.0.dist-info}/top_level.txt +0 -0
@@ -11,69 +11,149 @@ without modifying any generated files.
11
11
  import time
12
12
  import os
13
13
  import logging
14
+ import base64
14
15
  from typing import Dict, Any, Optional, List, Union, Tuple
15
- from .openapi_client import (
16
- Configuration,
17
- ApiClient,
18
- APIApi,
19
- ExecutionApi,
16
+ from cryptography.hazmat.primitives import serialization
17
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
18
+ from .agents_v1_gen import (
19
+ Configuration as AgentsV1Configuration,
20
+ ApiClient as AgentsV1ApiClient,
21
+ APIApi as AgentsV1APIApi,
22
+ ExecutionApi as AgentsV1ExecutionApi,
23
+ AgentProfileApi as AgentsV1AgentProfileApi,
20
24
  ExecutionStatusResponse,
21
- ExecutionResultResponse,
22
- BrowserSessionRecordingResponse,
25
+ ExecutionResult,
23
26
  UploadExecutionFiles200Response,
24
27
  Status,
25
- StructuredAgentExecutionRequest
28
+ StructuredAgentExecutionRequest,
29
+ CreateAgentProfileRequest,
30
+ UpdateAgentProfileRequest,
31
+ DeleteAgentProfile200Response,
32
+ AgentProfile,
33
+ Credential,
26
34
  )
27
- from .openapi_client.exceptions import ApiException
35
+ from .agents_v1_gen.exceptions import ApiException
36
+ from .agents_v2_gen import (
37
+ Configuration as AgentsV2Configuration,
38
+ ApiClient as AgentsV2ApiClient,
39
+ ExecutionApi as AgentsV2ExecutionApi,
40
+ ExecutionActivity,
41
+ ExecutionUserMessagesAddTextBody,
42
+ )
43
+
44
+
45
+ class AsteroidAPIError(Exception):
46
+ """Base exception for all Asteroid API related errors."""
47
+ pass
48
+
49
+
50
+ class ExecutionError(AsteroidAPIError):
51
+ """Raised when an execution fails or is cancelled."""
52
+ def __init__(self, message: str, execution_result: Optional[ExecutionResult] = None):
53
+ super().__init__(message)
54
+ self.execution_result = execution_result
55
+
56
+
57
+ class TimeoutError(AsteroidAPIError):
58
+ """Raised when an execution times out."""
59
+ def __init__(self, message: str):
60
+ super().__init__(message)
61
+
62
+
63
+ def encrypt_with_public_key(plaintext: str, pem_public_key: str) -> str:
64
+ """
65
+ Encrypt plaintext using RSA public key with PKCS1v15 padding.
66
+
67
+ Args:
68
+ plaintext: The string to encrypt
69
+ pem_public_key: PEM-formatted RSA public key
70
+
71
+ Returns:
72
+ Base64-encoded encrypted string
73
+
74
+ Raises:
75
+ ValueError: If encryption fails or key is invalid
76
+
77
+ Example:
78
+ encrypted = encrypt_with_public_key("my_password", public_key_pem)
79
+ """
80
+ try:
81
+ # Load the PEM public key (matches node-forge behavior)
82
+ public_key = serialization.load_pem_public_key(pem_public_key.encode('utf-8'))
83
+
84
+ if not isinstance(public_key, rsa.RSAPublicKey):
85
+ raise ValueError("Invalid RSA public key")
86
+
87
+ # Encrypt using PKCS1v15 padding (matches "RSAES-PKCS1-V1_5" from TypeScript)
88
+ encrypted_bytes = public_key.encrypt(
89
+ plaintext.encode('utf-8'),
90
+ padding.PKCS1v15()
91
+ )
92
+
93
+ # Encode as base64 (matches forge.util.encode64)
94
+ return base64.b64encode(encrypted_bytes).decode('utf-8')
95
+
96
+ except Exception as e:
97
+ raise ValueError(f"Failed to encrypt: {str(e)}") from e
28
98
 
29
99
 
30
100
  class AsteroidClient:
31
101
  """
32
102
  High-level client for the Asteroid Agents API.
33
-
103
+
34
104
  This class provides a convenient interface for executing agents and managing
35
105
  their execution lifecycle, similar to the TypeScript SDK.
36
106
  """
37
-
107
+
38
108
  def __init__(self, api_key: str, base_url: Optional[str] = None):
39
109
  """
40
110
  Create an API client with the provided API key.
41
-
111
+
42
112
  Args:
43
113
  api_key: Your API key for authentication
44
114
  base_url: Optional base URL (defaults to https://odyssey.asteroid.ai/api/v1)
45
-
115
+
46
116
  Example:
47
117
  client = AsteroidClient('your-api-key')
48
118
  """
49
119
  if api_key is None:
50
120
  raise TypeError("API key cannot be None")
51
-
121
+
52
122
  # Configure the API client
53
- config = Configuration(
123
+ config = AgentsV1Configuration(
54
124
  host=base_url or "https://odyssey.asteroid.ai/api/v1",
55
125
  api_key={'ApiKeyAuth': api_key}
56
126
  )
57
-
58
- self.api_client = ApiClient(config)
59
- self.api_api = APIApi(self.api_client)
60
- self.execution_api = ExecutionApi(self.api_client)
61
-
62
- def execute_agent(self, agent_id: str, execution_data: Dict[str, Any], agent_profile_id: Optional[str] = None) -> str:
127
+
128
+ self.api_client = AgentsV1ApiClient(config)
129
+ self.api_api = AgentsV1APIApi(self.api_client)
130
+ self.execution_api = AgentsV1ExecutionApi(self.api_client)
131
+ self.agent_profile_api = AgentsV1AgentProfileApi(self.api_client)
132
+
133
+ self.agents_v2_config = AgentsV2Configuration(
134
+ host=base_url or "https://odyssey.asteroid.ai/agents/v2",
135
+ api_key={'ApiKeyAuth': api_key}
136
+ )
137
+ self.agents_v2_api_client = AgentsV2ApiClient(self.agents_v2_config)
138
+ self.agents_v2_execution_api = AgentsV2ExecutionApi(self.agents_v2_api_client)
139
+
140
+ # --- V1 ---
141
+
142
+ def execute_agent(self, agent_id: str, execution_data: Dict[str, Any], agent_profile_id: Optional[str] = None) -> str:
63
143
  """
64
144
  Execute an agent with the provided parameters.
65
-
145
+
66
146
  Args:
67
147
  agent_id: The ID of the agent to execute
68
148
  execution_data: The execution parameters
69
149
  agent_profile_id: Optional ID of the agent profile
70
-
150
+
71
151
  Returns:
72
152
  The execution ID
73
-
153
+
74
154
  Raises:
75
- Exception: If the execution request fails
76
-
155
+ AsteroidAPIError: If the execution request fails
156
+
77
157
  Example:
78
158
  execution_id = client.execute_agent('my-agent-id', {'input': 'some dynamic value'}, 'agent-profile-id')
79
159
  """
@@ -82,21 +162,21 @@ class AsteroidClient:
82
162
  response = self.execution_api.execute_agent_structured(agent_id, req)
83
163
  return response.execution_id
84
164
  except ApiException as e:
85
- raise Exception(f"Failed to execute agent: {e}")
86
-
165
+ raise AsteroidAPIError(f"Failed to execute agent: {e}") from e
166
+
87
167
  def get_execution_status(self, execution_id: str) -> ExecutionStatusResponse:
88
168
  """
89
169
  Get the current status for an execution.
90
-
170
+
91
171
  Args:
92
172
  execution_id: The execution identifier
93
-
173
+
94
174
  Returns:
95
175
  The execution status details
96
-
176
+
97
177
  Raises:
98
- Exception: If the status request fails
99
-
178
+ AsteroidAPIError: If the status request fails
179
+
100
180
  Example:
101
181
  status = client.get_execution_status(execution_id)
102
182
  print(status.status)
@@ -104,89 +184,123 @@ class AsteroidClient:
104
184
  try:
105
185
  return self.execution_api.get_execution_status(execution_id)
106
186
  except ApiException as e:
107
- raise Exception(f"Failed to get execution status: {e}")
108
-
109
- def get_execution_result(self, execution_id: str) -> Dict[str, Any]:
187
+ raise AsteroidAPIError(f"Failed to get execution status: {e}") from e
188
+
189
+ def get_execution_result(self, execution_id: str) -> ExecutionResult:
110
190
  """
111
191
  Get the final result of an execution.
112
-
192
+
113
193
  Args:
114
194
  execution_id: The execution identifier
115
-
195
+
116
196
  Returns:
117
- The result object of the execution
118
-
197
+ The execution result object
198
+
119
199
  Raises:
120
- Exception: If the result request fails or execution failed
121
-
200
+ AsteroidAPIError: If the result request fails or execution failed
201
+
122
202
  Example:
123
203
  result = client.get_execution_result(execution_id)
124
- print(result)
204
+ print(result.outcome, result.reasoning)
125
205
  """
126
206
  try:
127
207
  response = self.execution_api.get_execution_result(execution_id)
128
-
208
+
129
209
  if response.error:
130
- raise Exception(response.error)
210
+ raise AsteroidAPIError(response.error)
211
+
212
+ # Handle case where execution_result might be None or have invalid data
213
+ if response.execution_result is None:
214
+ raise AsteroidAPIError("Execution result is not available yet")
131
215
 
132
- return response.execution_result or {}
216
+ return response.execution_result
133
217
  except ApiException as e:
134
- raise Exception(f"Failed to get execution result: {e}")
135
-
218
+ raise AsteroidAPIError(f"Failed to get execution result: {e}") from e
219
+ except Exception as e:
220
+ # Handle validation errors from ExecutionResult model
221
+ if "must be one of enum values" in str(e):
222
+ raise AsteroidAPIError("Execution result is not available yet - execution may still be running") from e
223
+ raise e
224
+
136
225
  def wait_for_execution_result(
137
- self,
138
- execution_id: str,
139
- interval: float = 1.0,
226
+ self,
227
+ execution_id: str,
228
+ interval: float = 1.0,
140
229
  timeout: float = 3600.0
141
- ) -> Dict[str, Any]:
230
+ ) -> ExecutionResult:
142
231
  """
143
232
  Wait for an execution to reach a terminal state and return the result.
144
-
145
- Continuously polls the execution status until it's either "completed",
233
+
234
+ Continuously polls the execution status until it's either "completed",
146
235
  "cancelled", or "failed".
147
-
236
+
148
237
  Args:
149
238
  execution_id: The execution identifier
150
239
  interval: Polling interval in seconds (default is 1.0)
151
240
  timeout: Maximum wait time in seconds (default is 3600 - 1 hour)
152
-
241
+
153
242
  Returns:
154
- The execution result if completed
155
-
243
+ The execution result object
244
+
156
245
  Raises:
157
- Exception: If the execution ends as "cancelled" or "failed", or times out
158
-
246
+ ValueError: If interval or timeout parameters are invalid
247
+ TimeoutError: If the execution times out
248
+ ExecutionError: If the execution ends as "cancelled" or "failed"
249
+
159
250
  Example:
160
251
  result = client.wait_for_execution_result(execution_id, interval=2.0)
252
+ print(result.outcome, result.reasoning)
161
253
  """
254
+ # Validate input parameters
255
+ if interval <= 0:
256
+ raise ValueError("interval must be positive")
257
+ if timeout <= 0:
258
+ raise ValueError("timeout must be positive")
162
259
  start_time = time.time()
163
-
260
+
164
261
  while True:
165
262
  elapsed_time = time.time() - start_time
166
263
  if elapsed_time >= timeout:
167
- raise Exception(f"Execution {execution_id} timed out after {timeout}s")
168
-
264
+ raise TimeoutError(f"Execution {execution_id} timed out after {timeout}s")
265
+
169
266
  status_response = self.get_execution_status(execution_id)
170
267
  current_status = status_response.status
171
-
268
+
172
269
  if current_status == Status.COMPLETED:
173
- return self.get_execution_result(execution_id)
270
+ try:
271
+ return self.get_execution_result(execution_id)
272
+ except Exception as e:
273
+ if "not available yet" in str(e):
274
+ # Execution completed but result not ready yet, wait a bit more
275
+ time.sleep(interval)
276
+ continue
277
+ raise e
174
278
  elif current_status in [Status.FAILED, Status.CANCELLED]:
175
- reason = f" - {status_response.reason}" if status_response.reason else ""
176
- raise Exception(f"Execution {execution_id} ended with status: {current_status.value}{reason}")
177
-
279
+ # Get the execution result to provide outcome and reasoning
280
+ try:
281
+ execution_result = self.get_execution_result(execution_id)
282
+ reason = f" - {status_response.reason}" if status_response.reason else ""
283
+ raise ExecutionError(
284
+ f"Execution {execution_id} ended with status: {current_status.value}{reason}",
285
+ execution_result
286
+ )
287
+ except Exception as e:
288
+ # If we can't get the execution result, fall back to the original behavior
289
+ reason = f" - {status_response.reason}" if status_response.reason else ""
290
+ raise ExecutionError(f"Execution {execution_id} ended with status: {current_status.value}{reason}") from e
291
+
178
292
  # Wait for the specified interval before polling again
179
293
  time.sleep(interval)
180
-
294
+
181
295
  def upload_execution_files(
182
- self,
183
- execution_id: str,
296
+ self,
297
+ execution_id: str,
184
298
  files: List[Union[bytes, str, Tuple[str, bytes]]],
185
299
  default_filename: str = "file.txt"
186
300
  ) -> UploadExecutionFiles200Response:
187
301
  """
188
302
  Upload files to an execution.
189
-
303
+
190
304
  Args:
191
305
  execution_id: The execution identifier
192
306
  files: List of files to upload. Each file can be:
@@ -194,25 +308,25 @@ class AsteroidClient:
194
308
  - str: File path as string (will read file and use filename)
195
309
  - Tuple[str, bytes]: (filename, file_content) tuple
196
310
  default_filename: Default filename to use when file is provided as bytes
197
-
311
+
198
312
  Returns:
199
313
  The upload response containing message and file IDs
200
-
314
+
201
315
  Raises:
202
316
  Exception: If the upload request fails
203
-
317
+
204
318
  Example:
205
319
  # Upload with file content (file should be in your current working directory)
206
320
  with open('hello.txt', 'r') as f:
207
321
  file_content = f.read()
208
-
322
+
209
323
  response = client.upload_execution_files(execution_id, [file_content.encode()])
210
324
  print(f"Uploaded files: {response.file_ids}")
211
-
325
+
212
326
  # Upload with filename and content
213
327
  files = [('hello.txt', file_content.encode())]
214
328
  response = client.upload_execution_files(execution_id, files)
215
-
329
+
216
330
  # Or create content directly
217
331
  hello_content = "Hello World!".encode()
218
332
  response = client.upload_execution_files(execution_id, [hello_content])
@@ -246,25 +360,25 @@ class AsteroidClient:
246
360
  # Other types - convert to string content and encode
247
361
  content = str(file_item).encode()
248
362
  processed_files.append((default_filename, content))
249
-
363
+
250
364
  response = self.execution_api.upload_execution_files(execution_id, files=processed_files)
251
365
  return response
252
366
  except ApiException as e:
253
- raise Exception(f"Failed to upload execution files: {e}")
254
-
367
+ raise AsteroidAPIError(f"Failed to upload execution files: {e}") from e
368
+
255
369
  def get_browser_session_recording(self, execution_id: str) -> str:
256
370
  """
257
371
  Get the browser session recording URL for a completed execution.
258
-
372
+
259
373
  Args:
260
374
  execution_id: The execution identifier
261
-
375
+
262
376
  Returns:
263
377
  The URL of the browser session recording
264
-
378
+
265
379
  Raises:
266
380
  Exception: If the recording request fails
267
-
381
+
268
382
  Example:
269
383
  recording_url = client.get_browser_session_recording(execution_id)
270
384
  print(f"Recording available at: {recording_url}")
@@ -273,12 +387,199 @@ class AsteroidClient:
273
387
  response = self.execution_api.get_browser_session_recording(execution_id)
274
388
  return response.recording_url
275
389
  except ApiException as e:
276
- raise Exception(f"Failed to get browser session recording: {e}")
277
-
390
+ raise AsteroidAPIError(f"Failed to get browser session recording: {e}") from e
391
+
392
+ def get_agent_profiles(self, organization_id: str) -> List[AgentProfile]:
393
+ """
394
+ Get a list of agent profiles for a specific organization.
395
+
396
+ Args:
397
+ organization_id: The organization identifier (required)
398
+ Returns:
399
+ A list of agent profiles
400
+ Raises:
401
+ Exception: If the agent profiles request fails
402
+ Example:
403
+ profiles = client.get_agent_profiles("org-123")
404
+ """
405
+ try:
406
+ response = self.agent_profile_api.get_agent_profiles(organization_id=organization_id)
407
+ return response # response is already a List[AgentProfile]
408
+ except ApiException as e:
409
+ raise AsteroidAPIError(f"Failed to get agent profiles: {e}") from e
410
+ def get_agent_profile(self, profile_id: str) -> AgentProfile:
411
+ """
412
+ Get an agent profile by ID.
413
+ Args:
414
+ profile_id: The ID of the agent profile
415
+ Returns:
416
+ The agent profile
417
+ Raises:
418
+ Exception: If the agent profile request fails
419
+ Example:
420
+ profile = client.get_agent_profile("profile_id")
421
+ """
422
+ try:
423
+ response = self.agent_profile_api.get_agent_profile(profile_id)
424
+ return response
425
+ except ApiException as e:
426
+ raise AsteroidAPIError(f"Failed to get agent profile: {e}") from e
427
+
428
+ def create_agent_profile(self, request: CreateAgentProfileRequest) -> AgentProfile:
429
+ """
430
+ Create an agent profile with automatic credential encryption.
431
+
432
+ Args:
433
+ request: The request object
434
+ Returns:
435
+ The agent profile
436
+ Raises:
437
+ Exception: If the agent profile creation fails
438
+ Example:
439
+ request = CreateAgentProfileRequest(
440
+ name="My Agent Profile",
441
+ description="This is my agent profile",
442
+ organization_id="org-123",
443
+ proxy_cc=CountryCode.US,
444
+ proxy_type=ProxyType.RESIDENTIAL,
445
+ captcha_solver_active=True,
446
+ sticky_ip=True,
447
+ credentials=[Credential(name="user", data="password")]
448
+ )
449
+ profile = client.create_agent_profile(request)
450
+ """
451
+ try:
452
+ # Create a copy to avoid modifying the original request
453
+ processed_request = request
454
+
455
+ # If credentials are provided, encrypt them before sending
456
+ if request.credentials and len(request.credentials) > 0:
457
+ # Get the public key for encryption
458
+ public_key = self.get_credentials_public_key()
459
+
460
+ # Encrypt each credential's data field
461
+ encrypted_credentials = []
462
+ for credential in request.credentials:
463
+ encrypted_credential = Credential(
464
+ name=credential.name,
465
+ data=encrypt_with_public_key(credential.data, public_key),
466
+ id=credential.id,
467
+ created_at=credential.created_at
468
+ )
469
+ encrypted_credentials.append(encrypted_credential)
470
+
471
+ # Create new request with encrypted credentials
472
+ processed_request = CreateAgentProfileRequest(
473
+ name=request.name,
474
+ description=request.description,
475
+ organization_id=request.organization_id,
476
+ proxy_cc=request.proxy_cc,
477
+ proxy_type=request.proxy_type,
478
+ captcha_solver_active=request.captcha_solver_active,
479
+ sticky_ip=request.sticky_ip,
480
+ credentials=encrypted_credentials
481
+ )
482
+
483
+ response = self.agent_profile_api.create_agent_profile(processed_request)
484
+ return response
485
+ except ApiException as e:
486
+ raise AsteroidAPIError(f"Failed to create agent profile: {e}") from e
487
+ def update_agent_profile(self, profile_id: str, request: UpdateAgentProfileRequest) -> AgentProfile:
488
+ """
489
+ Update an agent profile with automatic credential encryption.
490
+
491
+ Args:
492
+ profile_id: The ID of the agent profile
493
+ request: The request object
494
+ Returns:
495
+ The agent profile
496
+ Raises:
497
+ Exception: If the agent profile update fails
498
+ Example:
499
+ request = UpdateAgentProfileRequest(
500
+ name="My Agent Profile",
501
+ description="This is my agent profile",
502
+ credentials_to_add=[Credential(name="api_key", data="secret")]
503
+ )
504
+ profile = client.update_agent_profile("profile_id", request)
505
+ """
506
+ try:
507
+ # Create a copy to avoid modifying the original request
508
+ processed_request = request
509
+
510
+ # If credentials_to_add are provided, encrypt them before sending
511
+ if request.credentials_to_add and len(request.credentials_to_add) > 0:
512
+ # Get the public key for encryption
513
+ public_key = self.get_credentials_public_key()
514
+
515
+ # Encrypt the data field of each credential to add
516
+ encrypted_credentials_to_add = []
517
+ for credential in request.credentials_to_add:
518
+ encrypted_credential = Credential(
519
+ name=credential.name,
520
+ data=encrypt_with_public_key(credential.data, public_key),
521
+ id=credential.id,
522
+ created_at=credential.created_at
523
+ )
524
+ encrypted_credentials_to_add.append(encrypted_credential)
525
+
526
+ # Create new request with encrypted credentials
527
+ processed_request = UpdateAgentProfileRequest(
528
+ name=request.name,
529
+ description=request.description,
530
+ proxy_cc=request.proxy_cc,
531
+ proxy_type=request.proxy_type,
532
+ captcha_solver_active=request.captcha_solver_active,
533
+ sticky_ip=request.sticky_ip,
534
+ credentials_to_add=encrypted_credentials_to_add,
535
+ credentials_to_delete=request.credentials_to_delete
536
+ )
537
+
538
+ response = self.agent_profile_api.update_agent_profile(profile_id, processed_request)
539
+ return response
540
+ except ApiException as e:
541
+ raise AsteroidAPIError(f"Failed to update agent profile: {e}") from e
542
+ def delete_agent_profile(self, profile_id: str) -> DeleteAgentProfile200Response:
543
+ """
544
+ Delete an agent profile.
545
+ Args:
546
+ profile_id: The ID of the agent profile
547
+ Returns:
548
+ Confirmation message from the server
549
+ Raises:
550
+ Exception: If the agent profile deletion fails
551
+ Example:
552
+ response = client.delete_agent_profile("profile_id")
553
+ """
554
+ try:
555
+ response = self.agent_profile_api.delete_agent_profile(profile_id)
556
+ return response
557
+ except ApiException as e:
558
+ raise AsteroidAPIError(f"Failed to delete agent profile: {e}") from e
559
+
560
+ def get_credentials_public_key(self) -> str:
561
+ """
562
+ Get the public key for encrypting credentials.
563
+
564
+ Returns:
565
+ PEM-formatted RSA public key string
566
+
567
+ Raises:
568
+ Exception: If the public key request fails
569
+
570
+ Example:
571
+ public_key = client.get_credentials_public_key()
572
+ """
573
+ try:
574
+ response = self.agent_profile_api.get_credentials_public_key()
575
+ return response
576
+ except ApiException as e:
577
+ raise AsteroidAPIError(f"Failed to get credentials public key: {e}") from e
578
+
278
579
  def __enter__(self):
279
580
  """Context manager entry."""
280
581
  return self
281
-
582
+
282
583
  def __exit__(self, exc_type, exc_value, tb):
283
584
  """Context manager exit: clean up API client connection pool."""
284
585
  try:
@@ -297,39 +598,98 @@ class AsteroidClient:
297
598
  # Returning False allows any exception in the 'with' block to propagate
298
599
  return False
299
600
 
601
+ # Utility methods for nicer response formatting
602
+ def format_agent_profile(self, profile: AgentProfile) -> str:
603
+ """Format an agent profile for nice display."""
604
+ credentials_info = f"{[(credential.name, credential.id) for credential in profile.credentials]}" if profile.credentials else "No credentials"
605
+ return f"""Agent Profile:
606
+ ID: {profile.id}
607
+ Name: {profile.name}
608
+ Description: {profile.description}
609
+ Organization: {profile.organization_id}
610
+ Proxy: {profile.proxy_cc.value} ({profile.proxy_type.value})
611
+ Captcha Solver: {'Enabled' if profile.captcha_solver_active else 'Disabled'}
612
+ Sticky IP: {'Yes' if profile.sticky_ip else 'No'}
613
+ Credentials: {credentials_info}
614
+ Created: {profile.created_at.strftime('%Y-%m-%d %H:%M:%S')}
615
+ Updated: {profile.updated_at.strftime('%Y-%m-%d %H:%M:%S')}"""
616
+
617
+ def format_agent_profiles_list(self, profiles: List[AgentProfile]) -> str:
618
+ """Format a list of agent profiles for nice display."""
619
+ if not profiles:
620
+ return "No agent profiles found."
621
+
622
+ result = f"Found {len(profiles)} agent profile(s):\n"
623
+ for i, profile in enumerate(profiles, 1):
624
+ credentials_count = len(profile.credentials) if profile.credentials else 0
625
+ result += f" {i}. {profile.name} (ID: {profile.id[:8]}...) - {credentials_count} credentials\n"
626
+ return result.rstrip()
627
+
628
+ # --- V2 ---
629
+
630
+ def get_last_n_execution_activities(self, execution_id: str, n: int) -> List[ExecutionActivity]:
631
+ """
632
+ Get the last N execution activities for a given execution ID, sorted by their timestamp in descending order.
633
+ Args:
634
+ execution_id: The execution identifier
635
+ n: The number of activities to return
636
+ Returns:
637
+ A list of execution activities
638
+ Raises:
639
+ Exception: If the execution activities request fails
640
+ """
641
+ return self.agents_v2_execution_api.activities_get(execution_id, order="desc", limit=n)
642
+ def add_message_to_execution(self, execution_id: str, message: str) -> None:
643
+ """
644
+ Add a message to an execution.
645
+ Args:
646
+ execution_id: The execution identifier
647
+ message: The message to add
648
+ Returns:
649
+ None
650
+ Raises:
651
+ Exception: If the message addition fails
652
+ Example:
653
+ add_message_to_execution(client, "execution_id", "Hello, world!")
654
+ """
655
+ message_body = ExecutionUserMessagesAddTextBody(message=message)
656
+ return self.agents_v2_execution_api.user_messages_add(execution_id, message_body)
657
+
300
658
 
301
659
  # Convenience functions that mirror the TypeScript SDK pattern
302
660
  def create_client(api_key: str, base_url: Optional[str] = None) -> AsteroidClient:
303
661
  """
304
662
  Create an API client with a provided API key.
305
-
663
+
306
664
  This is a convenience function that creates an AsteroidClient instance.
307
-
665
+
308
666
  Args:
309
667
  api_key: Your API key
310
668
  base_url: Optional base URL
311
-
669
+
312
670
  Returns:
313
671
  A configured AsteroidClient instance
314
-
672
+
315
673
  Example:
316
674
  client = create_client('your-api-key')
317
675
  """
318
676
  return AsteroidClient(api_key, base_url)
319
677
 
678
+ # --- V1 ---
679
+
320
680
  def execute_agent(client: AsteroidClient, agent_id: str, execution_data: Dict[str, Any], agent_profile_id: Optional[str] = None) -> str:
321
681
  """
322
682
  Execute an agent with the provided parameters.
323
-
683
+
324
684
  Args:
325
685
  client: The AsteroidClient instance
326
686
  agent_id: The ID of the agent to execute
327
687
  execution_data: The execution parameters
328
688
  agent_profile_id: Optional ID of the agent profile
329
-
689
+
330
690
  Returns:
331
691
  The execution ID
332
-
692
+
333
693
  Example:
334
694
  execution_id = execute_agent(client, 'my-agent-id', {'input': 'some dynamic value'}, 'agent-profile-id')
335
695
  """
@@ -340,14 +700,14 @@ def execute_agent(client: AsteroidClient, agent_id: str, execution_data: Dict[st
340
700
  def get_execution_status(client: AsteroidClient, execution_id: str) -> ExecutionStatusResponse:
341
701
  """
342
702
  Get the current status for an execution.
343
-
703
+
344
704
  Args:
345
705
  client: The AsteroidClient instance
346
706
  execution_id: The execution identifier
347
-
707
+
348
708
  Returns:
349
709
  The execution status details
350
-
710
+
351
711
  Example:
352
712
  status = get_execution_status(client, execution_id)
353
713
  print(status.status)
@@ -355,72 +715,80 @@ def get_execution_status(client: AsteroidClient, execution_id: str) -> Execution
355
715
  return client.get_execution_status(execution_id)
356
716
 
357
717
 
358
- def get_execution_result(client: AsteroidClient, execution_id: str) -> Dict[str, Any]:
718
+ def get_execution_result(client: AsteroidClient, execution_id: str) -> ExecutionResult:
359
719
  """
360
720
  Get the final result of an execution.
361
-
721
+
362
722
  Args:
363
723
  client: The AsteroidClient instance
364
724
  execution_id: The execution identifier
365
-
725
+
366
726
  Returns:
367
- The result object of the execution
368
-
727
+ The execution result object
728
+
729
+ Raises:
730
+ Exception: If the result is not available yet or execution failed
731
+
369
732
  Example:
370
733
  result = get_execution_result(client, execution_id)
371
- print(result)
734
+ print(result.outcome, result.reasoning)
372
735
  """
373
736
  return client.get_execution_result(execution_id)
374
737
 
375
738
 
376
739
  def wait_for_execution_result(
377
- client: AsteroidClient,
378
- execution_id: str,
379
- interval: float = 1.0,
740
+ client: AsteroidClient,
741
+ execution_id: str,
742
+ interval: float = 1.0,
380
743
  timeout: float = 3600.0
381
- ) -> Dict[str, Any]:
744
+ ) -> ExecutionResult:
382
745
  """
383
746
  Wait for an execution to reach a terminal state and return the result.
384
-
747
+
385
748
  Args:
386
749
  client: The AsteroidClient instance
387
750
  execution_id: The execution identifier
388
751
  interval: Polling interval in seconds (default is 1.0)
389
752
  timeout: Maximum wait time in seconds (default is 3600 - 1 hour)
390
-
753
+
391
754
  Returns:
392
- The execution result if completed
393
-
755
+ The execution result object
756
+
757
+ Raises:
758
+ TimeoutError: If the execution times out
759
+ ExecutionError: If the execution ends as "cancelled" or "failed"
760
+
394
761
  Example:
395
762
  result = wait_for_execution_result(client, execution_id, interval=2.0)
763
+ print(result.outcome, result.reasoning)
396
764
  """
397
765
  return client.wait_for_execution_result(execution_id, interval, timeout)
398
766
 
399
767
 
400
768
  def upload_execution_files(
401
- client: AsteroidClient,
402
- execution_id: str,
769
+ client: AsteroidClient,
770
+ execution_id: str,
403
771
  files: List[Union[bytes, str, Tuple[str, bytes]]],
404
772
  default_filename: str = "file.txt"
405
773
  ) -> UploadExecutionFiles200Response:
406
774
  """
407
775
  Upload files to an execution.
408
-
776
+
409
777
  Args:
410
778
  client: The AsteroidClient instance
411
779
  execution_id: The execution identifier
412
780
  files: List of files to upload
413
781
  default_filename: Default filename to use when file is provided as bytes
414
-
782
+
415
783
  Returns:
416
784
  The upload response containing message and file IDs
417
-
785
+
418
786
  Example:
419
787
  # Create a simple text file with "Hello World!" content
420
788
  hello_content = "Hello World!".encode()
421
789
  response = upload_execution_files(client, execution_id, [hello_content])
422
790
  print(f"Uploaded files: {response.file_ids}")
423
-
791
+
424
792
  # Or specify filename with content
425
793
  files = [('hello.txt', "Hello World!".encode())]
426
794
  response = upload_execution_files(client, execution_id, files)
@@ -431,20 +799,155 @@ def upload_execution_files(
431
799
  def get_browser_session_recording(client: AsteroidClient, execution_id: str) -> str:
432
800
  """
433
801
  Get the browser session recording URL for a completed execution.
434
-
802
+
435
803
  Args:
436
804
  client: The AsteroidClient instance
437
805
  execution_id: The execution identifier
438
-
806
+
439
807
  Returns:
440
808
  The URL of the browser session recording
441
-
809
+
442
810
  Example:
443
811
  recording_url = get_browser_session_recording(client, execution_id)
444
812
  print(f"Recording available at: {recording_url}")
445
813
  """
446
814
  return client.get_browser_session_recording(execution_id)
447
815
 
816
+ def get_agent_profiles(client: AsteroidClient, organization_id: Optional[str] = None) -> List[AgentProfile]:
817
+ """
818
+ Get a list of agent profiles.
819
+ Args:
820
+ client: The AsteroidClient instance
821
+ organization_id: The organization identifier (optional) Returns all agent profiles if no organization_id is provided.
822
+ Returns:
823
+ A list of agent profiles
824
+ Raises:
825
+ Exception: If the agent profiles request fails
826
+ Example:
827
+ profiles = get_agent_profiles(client, "org-123")
828
+ """
829
+ return client.get_agent_profiles(organization_id)
830
+ def get_agent_profile(client: AsteroidClient, profile_id: str) -> AgentProfile:
831
+ """
832
+ Get an agent profile by ID.
833
+ Args:
834
+ client: The AsteroidClient instance
835
+ profile_id: The ID of the agent profile
836
+ Returns:
837
+ The agent profile
838
+ Raises:
839
+ Exception: If the agent profile request fails
840
+ Example:
841
+ profile = get_agent_profile(client, "profile_id")
842
+ """
843
+ return client.get_agent_profile(profile_id)
844
+ def create_agent_profile(client: AsteroidClient, request: CreateAgentProfileRequest) -> AgentProfile:
845
+ """
846
+ Create an agent profile.
847
+ Args:
848
+ client: The AsteroidClient instance
849
+ request: The request object
850
+ Returns:
851
+ The agent profile
852
+ Raises:
853
+ Exception: If the agent profile creation fails
854
+ Example:
855
+ request = CreateAgentProfileRequest(
856
+ name="My Agent Profile",
857
+ description="This is my agent profile",
858
+ organization_id="org-123",
859
+ proxy_cc=CountryCode.US,
860
+ proxy_type=ProxyType.RESIDENTIAL,
861
+ captcha_solver_active=True,
862
+ sticky_ip=True,
863
+ credentials=[Credential(name="user", data="password")]
864
+ )
865
+ profile = create_agent_profile(client, request)
866
+ """
867
+ return client.create_agent_profile(request)
868
+ def update_agent_profile(client: AsteroidClient, profile_id: str, request: UpdateAgentProfileRequest) -> AgentProfile:
869
+ """
870
+ Update an agent profile with the provided request.
871
+ Args:
872
+ client: The AsteroidClient instance
873
+ profile_id: The ID of the agent profile
874
+ request: The request object
875
+ Returns:
876
+ The agent profile
877
+ Raises:
878
+ Exception: If the agent profile update fails
879
+ Example:
880
+ request = UpdateAgentProfileRequest(
881
+ name="My Agent Profile",
882
+ description="This is my agent profile",
883
+ organization_id="org-123",
884
+ )
885
+ profile = update_agent_profile(client, "profile_id", request)
886
+ """
887
+ return client.update_agent_profile(profile_id, request)
888
+ def delete_agent_profile(client: AsteroidClient, profile_id: str) -> DeleteAgentProfile200Response:
889
+ """
890
+ Delete an agent profile.
891
+ Args:
892
+ client: The AsteroidClient instance
893
+ profile_id: The ID of the agent profile
894
+ Returns:
895
+ The agent profile
896
+ Raises:
897
+ Exception: If the agent profile deletion fails
898
+ Example:
899
+ profile_deleted =delete_agent_profile(client, "profile_id")
900
+ """
901
+ return client.delete_agent_profile(profile_id)
902
+
903
+ def get_credentials_public_key(client: AsteroidClient) -> str:
904
+ """
905
+ Get the public key for encrypting credentials.
906
+
907
+ Args:
908
+ client: The AsteroidClient instance
909
+
910
+ Returns:
911
+ PEM-formatted RSA public key string
912
+
913
+ Example:
914
+ public_key = get_credentials_public_key(client)
915
+ """
916
+ return client.get_credentials_public_key()
917
+
918
+ # --- V2 ---
919
+
920
+ def get_last_n_execution_activities(client: AsteroidClient, execution_id: str, n: int) -> List[ExecutionActivity]:
921
+ """
922
+ Get the last N execution activities for a given execution ID, sorted by their timestamp in descending order.
923
+ Args:
924
+ client: The AsteroidClient instance
925
+ execution_id: The execution identifier
926
+ n: The number of activities to return
927
+ Returns:
928
+ A list of execution activities
929
+ Raises:
930
+ Exception: If the execution activities request fails
931
+ Example:
932
+ activities = get_last_n_execution_activities(client, "execution_id", 10)
933
+ """
934
+ return client.get_last_n_execution_activities(execution_id, n)
935
+
936
+ def add_message_to_execution(client: AsteroidClient, execution_id: str, message: str) -> None:
937
+ """
938
+ Add a message to an execution.
939
+ Args:
940
+ client: The AsteroidClient instance
941
+ execution_id: The execution identifier
942
+ message: The message to add
943
+ Returns:
944
+ None
945
+ Raises:
946
+ Exception: If the message addition fails
947
+ Example:
948
+ add_message_to_execution(client, "execution_id", "Hello, world!")
949
+ """
950
+ return client.add_message_to_execution(execution_id, message)
448
951
 
449
952
  # Re-export common types for convenience
450
953
  __all__ = [
@@ -455,5 +958,14 @@ __all__ = [
455
958
  'get_execution_result',
456
959
  'wait_for_execution_result',
457
960
  'upload_execution_files',
458
- 'get_browser_session_recording'
459
- ]
961
+ 'get_browser_session_recording',
962
+ 'get_agent_profiles',
963
+ 'get_agent_profile',
964
+ 'create_agent_profile',
965
+ 'update_agent_profile',
966
+ 'delete_agent_profile',
967
+ 'get_credentials_public_key',
968
+ 'AsteroidAPIError',
969
+ 'ExecutionError',
970
+ 'TimeoutError'
971
+ ]