agnt5 0.3.2a1__cp310-abi3-manylinux_2_34_aarch64.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.
Potentially problematic release.
This version of agnt5 might be problematic. Click here for more details.
- agnt5/__init__.py +119 -0
- agnt5/_compat.py +16 -0
- agnt5/_core.abi3.so +0 -0
- agnt5/_retry_utils.py +196 -0
- agnt5/_schema_utils.py +312 -0
- agnt5/_sentry.py +515 -0
- agnt5/_telemetry.py +279 -0
- agnt5/agent/__init__.py +48 -0
- agnt5/agent/context.py +581 -0
- agnt5/agent/core.py +1782 -0
- agnt5/agent/decorator.py +112 -0
- agnt5/agent/handoff.py +105 -0
- agnt5/agent/registry.py +68 -0
- agnt5/agent/result.py +39 -0
- agnt5/checkpoint.py +246 -0
- agnt5/client.py +1556 -0
- agnt5/context.py +288 -0
- agnt5/emit.py +197 -0
- agnt5/entity.py +1230 -0
- agnt5/events.py +567 -0
- agnt5/exceptions.py +110 -0
- agnt5/function.py +330 -0
- agnt5/journal.py +212 -0
- agnt5/lm.py +1266 -0
- agnt5/memoization.py +379 -0
- agnt5/memory.py +521 -0
- agnt5/tool.py +721 -0
- agnt5/tracing.py +300 -0
- agnt5/types.py +111 -0
- agnt5/version.py +19 -0
- agnt5/worker.py +2094 -0
- agnt5/workflow.py +1632 -0
- agnt5-0.3.2a1.dist-info/METADATA +26 -0
- agnt5-0.3.2a1.dist-info/RECORD +35 -0
- agnt5-0.3.2a1.dist-info/WHEEL +4 -0
agnt5/client.py
ADDED
|
@@ -0,0 +1,1556 @@
|
|
|
1
|
+
"""AGNT5 Client SDK for invoking components."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, AsyncIterator, Dict, Iterator, Optional, TYPE_CHECKING
|
|
6
|
+
from urllib.parse import urljoin
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .events import Event, EventType
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .client import RunError
|
|
14
|
+
|
|
15
|
+
# Environment variable for API key
|
|
16
|
+
AGNT5_API_KEY_ENV = "AGNT5_API_KEY"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_error_response(error_data: Dict[str, Any], run_id: Optional[str] = None) -> "RunError":
|
|
20
|
+
"""Parse error response from platform and create RunError with structured fields.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
error_data: Error response dict from platform (contains error, error_code, metadata, etc.)
|
|
24
|
+
run_id: Optional run ID if not in error_data
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
RunError with structured fields populated from the response
|
|
28
|
+
"""
|
|
29
|
+
# Import here to avoid circular import
|
|
30
|
+
from .client import RunError
|
|
31
|
+
|
|
32
|
+
message = error_data.get("error", "Unknown error")
|
|
33
|
+
run_id = error_data.get("runId") or run_id
|
|
34
|
+
error_code = error_data.get("error_code")
|
|
35
|
+
|
|
36
|
+
# Extract retry metadata if present
|
|
37
|
+
metadata = error_data.get("metadata")
|
|
38
|
+
attempts = None
|
|
39
|
+
max_attempts = None
|
|
40
|
+
|
|
41
|
+
if metadata:
|
|
42
|
+
if isinstance(metadata, dict):
|
|
43
|
+
attempts = metadata.get("attempts")
|
|
44
|
+
max_attempts = metadata.get("max_attempts")
|
|
45
|
+
elif isinstance(metadata, str):
|
|
46
|
+
# Parse JSON string metadata
|
|
47
|
+
try:
|
|
48
|
+
parsed = json.loads(metadata)
|
|
49
|
+
attempts = parsed.get("attempts")
|
|
50
|
+
max_attempts = parsed.get("max_attempts")
|
|
51
|
+
metadata = parsed
|
|
52
|
+
except (json.JSONDecodeError, TypeError):
|
|
53
|
+
metadata = {"raw": metadata}
|
|
54
|
+
|
|
55
|
+
return RunError(
|
|
56
|
+
message,
|
|
57
|
+
run_id=run_id,
|
|
58
|
+
error_code=error_code,
|
|
59
|
+
attempts=attempts,
|
|
60
|
+
max_attempts=max_attempts,
|
|
61
|
+
metadata=metadata if isinstance(metadata, dict) else None,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _parse_sse_to_event(event_type_str: str, data: Dict[str, Any]) -> Event:
|
|
66
|
+
"""Convert SSE event type and data to typed Event object.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
event_type_str: The event type string from SSE (e.g., "agent.started")
|
|
70
|
+
data: The parsed JSON data from the SSE data field
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Event object with typed event_type and data payload
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
event_type = EventType(event_type_str)
|
|
77
|
+
except ValueError:
|
|
78
|
+
# Unknown event type - store as-is with a generic type
|
|
79
|
+
# This allows forward compatibility with new event types
|
|
80
|
+
return Event(
|
|
81
|
+
event_type=EventType.PROGRESS_UPDATE,
|
|
82
|
+
data={"_raw_event_type": event_type_str, **data},
|
|
83
|
+
content_index=data.get("index", 0),
|
|
84
|
+
sequence=data.get("sequence", 0),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return Event(
|
|
88
|
+
event_type=event_type,
|
|
89
|
+
data=data,
|
|
90
|
+
content_index=data.get("index", 0),
|
|
91
|
+
sequence=data.get("sequence", 0),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Client:
|
|
96
|
+
"""Client for invoking AGNT5 components.
|
|
97
|
+
|
|
98
|
+
This client provides a simple interface for calling functions, workflows,
|
|
99
|
+
and other components deployed on AGNT5.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
```python
|
|
103
|
+
from agnt5 import Client
|
|
104
|
+
|
|
105
|
+
# Local development (no auth needed)
|
|
106
|
+
client = Client("http://localhost:34181")
|
|
107
|
+
result = client.run("greet", {"name": "Alice"})
|
|
108
|
+
print(result) # {"message": "Hello, Alice!"}
|
|
109
|
+
|
|
110
|
+
# Production with API key
|
|
111
|
+
client = Client(
|
|
112
|
+
gateway_url="https://api.agnt5.com",
|
|
113
|
+
api_key="agnt5_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Or use AGNT5_API_KEY environment variable
|
|
117
|
+
# export AGNT5_API_KEY=agnt5_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
118
|
+
client = Client(gateway_url="https://api.agnt5.com")
|
|
119
|
+
```
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
gateway_url: str = "http://localhost:34181",
|
|
125
|
+
timeout: float = 30.0,
|
|
126
|
+
api_key: Optional[str] = None,
|
|
127
|
+
):
|
|
128
|
+
"""Initialize the AGNT5 client.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
gateway_url: Base URL of the AGNT5 gateway (default: http://localhost:34181)
|
|
132
|
+
timeout: Request timeout in seconds (default: 30.0)
|
|
133
|
+
api_key: Service key for authentication. If not provided, falls back to
|
|
134
|
+
AGNT5_API_KEY environment variable. Keys start with "agnt5_sk_".
|
|
135
|
+
"""
|
|
136
|
+
self.gateway_url = gateway_url.rstrip("/")
|
|
137
|
+
self.timeout = timeout
|
|
138
|
+
# Use provided api_key or fallback to environment variable
|
|
139
|
+
self.api_key = api_key or os.environ.get(AGNT5_API_KEY_ENV)
|
|
140
|
+
self._client = httpx.Client(timeout=timeout)
|
|
141
|
+
|
|
142
|
+
def _build_headers(
|
|
143
|
+
self,
|
|
144
|
+
session_id: Optional[str] = None,
|
|
145
|
+
user_id: Optional[str] = None,
|
|
146
|
+
) -> Dict[str, str]:
|
|
147
|
+
"""Build request headers with authentication and optional session/user context.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
session_id: Session identifier for multi-turn conversations
|
|
151
|
+
user_id: User identifier for user-scoped memory
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Dictionary of HTTP headers
|
|
155
|
+
"""
|
|
156
|
+
headers = {"Content-Type": "application/json"}
|
|
157
|
+
if self.api_key:
|
|
158
|
+
headers["X-API-KEY"] = self.api_key
|
|
159
|
+
if session_id:
|
|
160
|
+
headers["X-Session-ID"] = session_id
|
|
161
|
+
if user_id:
|
|
162
|
+
headers["X-User-ID"] = user_id
|
|
163
|
+
return headers
|
|
164
|
+
|
|
165
|
+
def run(
|
|
166
|
+
self,
|
|
167
|
+
component: str,
|
|
168
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
169
|
+
component_type: str = "function",
|
|
170
|
+
session_id: Optional[str] = None,
|
|
171
|
+
user_id: Optional[str] = None,
|
|
172
|
+
) -> Dict[str, Any]:
|
|
173
|
+
"""Execute a component synchronously and wait for the result.
|
|
174
|
+
|
|
175
|
+
This is a blocking call that waits for the component to complete execution.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
component: Name of the component to execute
|
|
179
|
+
input_data: Input data for the component (will be sent as JSON body)
|
|
180
|
+
component_type: Type of component - "function", "workflow", "agent", "tool" (default: "function")
|
|
181
|
+
session_id: Session identifier for multi-turn conversations (optional)
|
|
182
|
+
user_id: User identifier for user-scoped memory (optional)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Dictionary containing the component's output
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
RunError: If the component execution fails
|
|
189
|
+
httpx.HTTPError: If the HTTP request fails
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
```python
|
|
193
|
+
# Simple function call (default)
|
|
194
|
+
result = client.run("greet", {"name": "Alice"})
|
|
195
|
+
|
|
196
|
+
# Workflow execution (explicit)
|
|
197
|
+
result = client.run("order_fulfillment", {"order_id": "123"}, component_type="workflow")
|
|
198
|
+
|
|
199
|
+
# Multi-turn conversation with session
|
|
200
|
+
result = client.run("chat", {"message": "Hello"}, session_id="session-123")
|
|
201
|
+
|
|
202
|
+
# User-scoped memory
|
|
203
|
+
result = client.run("assistant", {"message": "Help me"}, user_id="user-456")
|
|
204
|
+
|
|
205
|
+
# No input data
|
|
206
|
+
result = client.run("get_status")
|
|
207
|
+
```
|
|
208
|
+
"""
|
|
209
|
+
if input_data is None:
|
|
210
|
+
input_data = {}
|
|
211
|
+
|
|
212
|
+
# Build URL with component type
|
|
213
|
+
url = urljoin(self.gateway_url + "/", f"v1/run/{component_type}/{component}")
|
|
214
|
+
|
|
215
|
+
# Make request with auth and session headers
|
|
216
|
+
response = self._client.post(
|
|
217
|
+
url,
|
|
218
|
+
json=input_data,
|
|
219
|
+
headers=self._build_headers(session_id=session_id, user_id=user_id),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Handle errors
|
|
223
|
+
if response.status_code == 404:
|
|
224
|
+
try:
|
|
225
|
+
error_data = response.json()
|
|
226
|
+
raise RunError(
|
|
227
|
+
error_data.get("error", "Component not found"),
|
|
228
|
+
run_id=error_data.get("runId"),
|
|
229
|
+
)
|
|
230
|
+
except ValueError:
|
|
231
|
+
# JSON parsing failed
|
|
232
|
+
raise RunError(f"Component '{component}' not found")
|
|
233
|
+
|
|
234
|
+
if response.status_code == 503:
|
|
235
|
+
error_data = response.json()
|
|
236
|
+
raise RunError(
|
|
237
|
+
f"Service unavailable: {error_data.get('error', 'Unknown error')}",
|
|
238
|
+
run_id=error_data.get("runId"),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if response.status_code == 504:
|
|
242
|
+
error_data = response.json()
|
|
243
|
+
raise RunError(
|
|
244
|
+
"Execution timeout",
|
|
245
|
+
run_id=error_data.get("runId"),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Handle 500 errors with our RunResponse format
|
|
249
|
+
if response.status_code == 500:
|
|
250
|
+
try:
|
|
251
|
+
error_data = response.json()
|
|
252
|
+
raise _parse_error_response(error_data)
|
|
253
|
+
except ValueError:
|
|
254
|
+
# JSON parsing failed, fall through to raise_for_status
|
|
255
|
+
response.raise_for_status()
|
|
256
|
+
else:
|
|
257
|
+
# For other error codes, use standard HTTP error handling
|
|
258
|
+
response.raise_for_status()
|
|
259
|
+
|
|
260
|
+
# Parse response
|
|
261
|
+
data = response.json()
|
|
262
|
+
|
|
263
|
+
# Check execution status
|
|
264
|
+
if data.get("status") == "failed":
|
|
265
|
+
raise _parse_error_response(data)
|
|
266
|
+
|
|
267
|
+
# Return output
|
|
268
|
+
return data.get("output", {})
|
|
269
|
+
|
|
270
|
+
def submit(
|
|
271
|
+
self,
|
|
272
|
+
component: str,
|
|
273
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
274
|
+
component_type: str = "function",
|
|
275
|
+
) -> str:
|
|
276
|
+
"""Submit a component for async execution and return immediately.
|
|
277
|
+
|
|
278
|
+
This is a non-blocking call that returns a run ID immediately.
|
|
279
|
+
Use get_status() to check progress and get_result() to retrieve the output.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
component: Name of the component to execute
|
|
283
|
+
input_data: Input data for the component (will be sent as JSON body)
|
|
284
|
+
component_type: Type of component - "function", "workflow", "agent", "tool" (default: "function")
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
String containing the run ID
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
httpx.HTTPError: If the HTTP request fails
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
```python
|
|
294
|
+
# Submit async function (default)
|
|
295
|
+
run_id = client.submit("process_video", {"url": "https://..."})
|
|
296
|
+
print(f"Submitted: {run_id}")
|
|
297
|
+
|
|
298
|
+
# Submit workflow
|
|
299
|
+
run_id = client.submit("order_fulfillment", {"order_id": "123"}, component_type="workflow")
|
|
300
|
+
|
|
301
|
+
# Check status later
|
|
302
|
+
status = client.get_status(run_id)
|
|
303
|
+
if status["status"] == "completed":
|
|
304
|
+
result = client.get_result(run_id)
|
|
305
|
+
```
|
|
306
|
+
"""
|
|
307
|
+
if input_data is None:
|
|
308
|
+
input_data = {}
|
|
309
|
+
|
|
310
|
+
# Build URL with component type
|
|
311
|
+
url = urljoin(self.gateway_url + "/", f"v1/submit/{component_type}/{component}")
|
|
312
|
+
|
|
313
|
+
# Make request with auth headers
|
|
314
|
+
response = self._client.post(
|
|
315
|
+
url,
|
|
316
|
+
json=input_data,
|
|
317
|
+
headers=self._build_headers(),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Handle errors
|
|
321
|
+
response.raise_for_status()
|
|
322
|
+
|
|
323
|
+
# Parse response and extract run ID
|
|
324
|
+
data = response.json()
|
|
325
|
+
return data.get("runId", "")
|
|
326
|
+
|
|
327
|
+
def get_status(self, run_id: str) -> Dict[str, Any]:
|
|
328
|
+
"""Get the current status of a run.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
run_id: The run ID returned from submit()
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Dictionary containing status information:
|
|
335
|
+
{
|
|
336
|
+
"runId": "...",
|
|
337
|
+
"status": "pending|running|completed|failed|cancelled",
|
|
338
|
+
"submittedAt": 1234567890,
|
|
339
|
+
"startedAt": 1234567891, // optional
|
|
340
|
+
"completedAt": 1234567892 // optional
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
Raises:
|
|
344
|
+
httpx.HTTPError: If the HTTP request fails
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
```python
|
|
348
|
+
status = client.get_status(run_id)
|
|
349
|
+
print(f"Status: {status['status']}")
|
|
350
|
+
```
|
|
351
|
+
"""
|
|
352
|
+
url = urljoin(self.gateway_url + "/", f"v1/status/{run_id}")
|
|
353
|
+
|
|
354
|
+
response = self._client.get(url, headers=self._build_headers())
|
|
355
|
+
response.raise_for_status()
|
|
356
|
+
|
|
357
|
+
return response.json()
|
|
358
|
+
|
|
359
|
+
def get_result(self, run_id: str) -> Dict[str, Any]:
|
|
360
|
+
"""Get the result of a completed run.
|
|
361
|
+
|
|
362
|
+
This will raise an error if the run is not yet complete.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
run_id: The run ID returned from submit()
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Dictionary containing the component's output
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
RunError: If the run failed or is not yet complete
|
|
372
|
+
httpx.HTTPError: If the HTTP request fails
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
```python
|
|
376
|
+
try:
|
|
377
|
+
result = client.get_result(run_id)
|
|
378
|
+
print(result)
|
|
379
|
+
except RunError as e:
|
|
380
|
+
if "not complete" in str(e):
|
|
381
|
+
print("Run is still in progress")
|
|
382
|
+
else:
|
|
383
|
+
print(f"Run failed: {e}")
|
|
384
|
+
```
|
|
385
|
+
"""
|
|
386
|
+
url = urljoin(self.gateway_url + "/", f"v1/result/{run_id}")
|
|
387
|
+
|
|
388
|
+
response = self._client.get(url, headers=self._build_headers())
|
|
389
|
+
|
|
390
|
+
# Handle 404 - run not complete or not found
|
|
391
|
+
if response.status_code == 404:
|
|
392
|
+
error_data = response.json()
|
|
393
|
+
error_msg = error_data.get("error", "Run not found or not complete")
|
|
394
|
+
current_status = error_data.get("status", "unknown")
|
|
395
|
+
raise RunError(f"{error_msg} (status: {current_status})", run_id=run_id)
|
|
396
|
+
|
|
397
|
+
# Handle other errors
|
|
398
|
+
response.raise_for_status()
|
|
399
|
+
|
|
400
|
+
# Parse response
|
|
401
|
+
data = response.json()
|
|
402
|
+
|
|
403
|
+
# Check if run failed
|
|
404
|
+
if data.get("status") == "failed":
|
|
405
|
+
raise _parse_error_response(data, run_id=run_id)
|
|
406
|
+
|
|
407
|
+
# Return output
|
|
408
|
+
return data.get("output", {})
|
|
409
|
+
|
|
410
|
+
def wait_for_result(
|
|
411
|
+
self,
|
|
412
|
+
run_id: str,
|
|
413
|
+
timeout: float = 300.0,
|
|
414
|
+
poll_interval: float = 1.0,
|
|
415
|
+
) -> Dict[str, Any]:
|
|
416
|
+
"""Wait for a run to complete and return the result.
|
|
417
|
+
|
|
418
|
+
This polls the status endpoint until the run completes or times out.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
run_id: The run ID returned from submit()
|
|
422
|
+
timeout: Maximum time to wait in seconds (default: 300)
|
|
423
|
+
poll_interval: How often to check status in seconds (default: 1.0)
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Dictionary containing the component's output
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
RunError: If the run fails or times out
|
|
430
|
+
httpx.HTTPError: If the HTTP request fails
|
|
431
|
+
|
|
432
|
+
Example:
|
|
433
|
+
```python
|
|
434
|
+
# Submit and wait for result
|
|
435
|
+
run_id = client.submit("long_task", {"data": "..."})
|
|
436
|
+
try:
|
|
437
|
+
result = client.wait_for_result(run_id, timeout=600)
|
|
438
|
+
print(result)
|
|
439
|
+
except RunError as e:
|
|
440
|
+
print(f"Failed: {e}")
|
|
441
|
+
```
|
|
442
|
+
"""
|
|
443
|
+
import time
|
|
444
|
+
|
|
445
|
+
start_time = time.time()
|
|
446
|
+
|
|
447
|
+
while True:
|
|
448
|
+
# Check timeout
|
|
449
|
+
elapsed = time.time() - start_time
|
|
450
|
+
if elapsed >= timeout:
|
|
451
|
+
raise RunError(
|
|
452
|
+
f"Timeout waiting for run to complete after {timeout}s",
|
|
453
|
+
run_id=run_id,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Get current status
|
|
457
|
+
status = self.get_status(run_id)
|
|
458
|
+
current_status = status.get("status", "")
|
|
459
|
+
|
|
460
|
+
# Check if complete
|
|
461
|
+
if current_status in ("completed", "failed", "cancelled"):
|
|
462
|
+
# Get result (will raise if failed)
|
|
463
|
+
return self.get_result(run_id)
|
|
464
|
+
|
|
465
|
+
# Wait before next poll
|
|
466
|
+
time.sleep(poll_interval)
|
|
467
|
+
|
|
468
|
+
def stream(
|
|
469
|
+
self,
|
|
470
|
+
component: str,
|
|
471
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
472
|
+
component_type: str = "function",
|
|
473
|
+
):
|
|
474
|
+
"""Stream responses from a component using Server-Sent Events (SSE).
|
|
475
|
+
|
|
476
|
+
This method yields chunks as they arrive from the component.
|
|
477
|
+
Perfect for LLM token streaming and incremental responses.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
component: Name of the component to execute
|
|
481
|
+
input_data: Input data for the component (will be sent as JSON body)
|
|
482
|
+
component_type: Type of component - "function", "workflow", "agent", "tool" (default: "function")
|
|
483
|
+
|
|
484
|
+
Yields:
|
|
485
|
+
String chunks as they arrive from the component
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
RunError: If the component execution fails
|
|
489
|
+
httpx.HTTPError: If the HTTP request fails
|
|
490
|
+
|
|
491
|
+
Example:
|
|
492
|
+
```python
|
|
493
|
+
# Stream LLM tokens
|
|
494
|
+
for chunk in client.stream("generate_text", {"prompt": "Write a story"}):
|
|
495
|
+
print(chunk, end="", flush=True)
|
|
496
|
+
```
|
|
497
|
+
"""
|
|
498
|
+
if input_data is None:
|
|
499
|
+
input_data = {}
|
|
500
|
+
|
|
501
|
+
# Build URL with component type
|
|
502
|
+
url = urljoin(self.gateway_url + "/", f"v1/stream/{component_type}/{component}")
|
|
503
|
+
|
|
504
|
+
# Use streaming request with auth headers
|
|
505
|
+
with self._client.stream(
|
|
506
|
+
"POST",
|
|
507
|
+
url,
|
|
508
|
+
json=input_data,
|
|
509
|
+
headers=self._build_headers(),
|
|
510
|
+
timeout=300.0, # 5 minute timeout for streaming
|
|
511
|
+
) as response:
|
|
512
|
+
# Check for errors
|
|
513
|
+
if response.status_code != 200:
|
|
514
|
+
# For streaming responses, we can't read the full text
|
|
515
|
+
# Just raise an HTTP error
|
|
516
|
+
raise RunError(
|
|
517
|
+
f"HTTP {response.status_code}: Streaming request failed",
|
|
518
|
+
run_id=None,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# Parse SSE stream
|
|
522
|
+
current_event = None
|
|
523
|
+
for line in response.iter_lines():
|
|
524
|
+
line = line.strip()
|
|
525
|
+
|
|
526
|
+
# Skip empty lines and comments
|
|
527
|
+
if not line or line.startswith(":"):
|
|
528
|
+
continue
|
|
529
|
+
|
|
530
|
+
# Parse event type: "event: output.delta"
|
|
531
|
+
if line.startswith("event: "):
|
|
532
|
+
current_event = line[7:] # Remove "event: " prefix
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
# Parse SSE format: "data: {...}"
|
|
536
|
+
if line.startswith("data: "):
|
|
537
|
+
data_str = line[6:] # Remove "data: " prefix
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
data = json.loads(data_str)
|
|
541
|
+
|
|
542
|
+
# Check for completion
|
|
543
|
+
if data.get("done") or current_event == "done":
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
# Check for error
|
|
547
|
+
if "error" in data:
|
|
548
|
+
raise RunError(
|
|
549
|
+
data.get("error"),
|
|
550
|
+
run_id=data.get("runId"),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# Yield chunk from output.delta events
|
|
554
|
+
if current_event == "output.delta":
|
|
555
|
+
# Try different content field formats
|
|
556
|
+
if "content" in data:
|
|
557
|
+
yield data["content"]
|
|
558
|
+
elif "output_data" in data:
|
|
559
|
+
# output_data is proper JSON (string, number, object, etc.)
|
|
560
|
+
output = data["output_data"]
|
|
561
|
+
if isinstance(output, str):
|
|
562
|
+
yield output
|
|
563
|
+
elif output is not None:
|
|
564
|
+
# For non-string types, yield JSON string representation
|
|
565
|
+
yield json.dumps(output)
|
|
566
|
+
# Also support legacy "chunk" format
|
|
567
|
+
elif "chunk" in data:
|
|
568
|
+
yield data["chunk"]
|
|
569
|
+
|
|
570
|
+
except json.JSONDecodeError:
|
|
571
|
+
# Skip malformed JSON
|
|
572
|
+
continue
|
|
573
|
+
|
|
574
|
+
def stream_events(
|
|
575
|
+
self,
|
|
576
|
+
component: str,
|
|
577
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
578
|
+
component_type: str = "function",
|
|
579
|
+
session_id: Optional[str] = None,
|
|
580
|
+
user_id: Optional[str] = None,
|
|
581
|
+
timeout: float = 300.0,
|
|
582
|
+
) -> Iterator[Event]:
|
|
583
|
+
"""Stream typed Event objects from a component execution.
|
|
584
|
+
|
|
585
|
+
This method yields Event objects as they arrive from the component,
|
|
586
|
+
providing full access to the event taxonomy including agent lifecycle,
|
|
587
|
+
LM streaming, tool calls, and workflow events.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
component: Name of the component to execute
|
|
591
|
+
input_data: Input data for the component (will be sent as JSON body)
|
|
592
|
+
component_type: Type of component - "function", "workflow", "agent", "tool"
|
|
593
|
+
session_id: Session identifier for multi-turn conversations (optional)
|
|
594
|
+
user_id: User identifier for user-scoped memory (optional)
|
|
595
|
+
timeout: Stream timeout in seconds (default: 300.0 / 5 minutes)
|
|
596
|
+
|
|
597
|
+
Yields:
|
|
598
|
+
Event objects as they arrive from the stream
|
|
599
|
+
|
|
600
|
+
Raises:
|
|
601
|
+
RunError: If the component execution fails
|
|
602
|
+
httpx.HTTPError: If the HTTP request fails
|
|
603
|
+
|
|
604
|
+
Example:
|
|
605
|
+
```python
|
|
606
|
+
from agnt5 import Client, EventType
|
|
607
|
+
|
|
608
|
+
client = Client()
|
|
609
|
+
|
|
610
|
+
# Stream agent events
|
|
611
|
+
for event in client.stream_events("my_agent", {"message": "Hi"}, "agent"):
|
|
612
|
+
if event.event_type == EventType.AGENT_STARTED:
|
|
613
|
+
print(f"Agent started: {event.data['agent_name']}")
|
|
614
|
+
elif event.event_type == EventType.LM_MESSAGE_DELTA:
|
|
615
|
+
print(event.data['content'], end='', flush=True)
|
|
616
|
+
elif event.event_type == EventType.AGENT_COMPLETED:
|
|
617
|
+
print(f"\\nDone: {event.data['output']}")
|
|
618
|
+
```
|
|
619
|
+
"""
|
|
620
|
+
if timeout <= 0:
|
|
621
|
+
raise ValueError("timeout must be a positive number")
|
|
622
|
+
|
|
623
|
+
if input_data is None:
|
|
624
|
+
input_data = {}
|
|
625
|
+
|
|
626
|
+
# Build URL with component type (using streaming endpoint)
|
|
627
|
+
url = urljoin(self.gateway_url + "/", f"v1/stream/{component_type}/{component}")
|
|
628
|
+
|
|
629
|
+
# Use streaming request with auth and session headers
|
|
630
|
+
with self._client.stream(
|
|
631
|
+
"POST",
|
|
632
|
+
url,
|
|
633
|
+
json=input_data,
|
|
634
|
+
headers=self._build_headers(session_id=session_id, user_id=user_id),
|
|
635
|
+
timeout=timeout,
|
|
636
|
+
) as response:
|
|
637
|
+
# Check for errors
|
|
638
|
+
if response.status_code != 200:
|
|
639
|
+
# Try to get error details from response body
|
|
640
|
+
try:
|
|
641
|
+
error_body = response.read().decode("utf-8")
|
|
642
|
+
error_data = json.loads(error_body)
|
|
643
|
+
error_msg = error_data.get("error", f"HTTP {response.status_code}")
|
|
644
|
+
run_id = error_data.get("runId")
|
|
645
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
646
|
+
error_msg = f"HTTP {response.status_code}: Streaming request failed"
|
|
647
|
+
run_id = None
|
|
648
|
+
raise RunError(error_msg, run_id=run_id)
|
|
649
|
+
|
|
650
|
+
# Parse SSE stream
|
|
651
|
+
current_event_type: Optional[str] = None
|
|
652
|
+
for line in response.iter_lines():
|
|
653
|
+
line = line.strip()
|
|
654
|
+
|
|
655
|
+
# Skip empty lines and comments (keep-alive)
|
|
656
|
+
if not line or line.startswith(":"):
|
|
657
|
+
continue
|
|
658
|
+
|
|
659
|
+
# Parse event type: "event: agent.started"
|
|
660
|
+
if line.startswith("event: "):
|
|
661
|
+
current_event_type = line[7:] # Remove "event: " prefix
|
|
662
|
+
continue
|
|
663
|
+
|
|
664
|
+
# Parse SSE data: "data: {...}"
|
|
665
|
+
if line.startswith("data: "):
|
|
666
|
+
data_str = line[6:] # Remove "data: " prefix
|
|
667
|
+
|
|
668
|
+
try:
|
|
669
|
+
data = json.loads(data_str)
|
|
670
|
+
|
|
671
|
+
# Check for completion signal
|
|
672
|
+
if data.get("done") or current_event_type == "done":
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
# Check for error event
|
|
676
|
+
if current_event_type == "error" or "error" in data:
|
|
677
|
+
error_msg = data.get("error", "Unknown streaming error")
|
|
678
|
+
raise RunError(error_msg, run_id=data.get("runId"))
|
|
679
|
+
|
|
680
|
+
# Yield typed Event object
|
|
681
|
+
if current_event_type:
|
|
682
|
+
yield _parse_sse_to_event(current_event_type, data)
|
|
683
|
+
|
|
684
|
+
except json.JSONDecodeError:
|
|
685
|
+
# Skip malformed JSON
|
|
686
|
+
continue
|
|
687
|
+
|
|
688
|
+
def entity(self, entity_type: str, key: str) -> "EntityProxy":
|
|
689
|
+
"""Get a proxy for calling methods on a durable entity.
|
|
690
|
+
|
|
691
|
+
This provides a fluent API for entity method invocations with key-based routing.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
entity_type: The entity class name (e.g., "Counter", "ShoppingCart")
|
|
695
|
+
key: The entity instance key (e.g., "user-123", "cart-alice")
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
EntityProxy that allows method calls on the entity
|
|
699
|
+
|
|
700
|
+
Example:
|
|
701
|
+
```python
|
|
702
|
+
# Call entity method
|
|
703
|
+
result = client.entity("Counter", "user-123").increment(amount=5)
|
|
704
|
+
print(result) # 5
|
|
705
|
+
|
|
706
|
+
# Shopping cart
|
|
707
|
+
result = client.entity("ShoppingCart", "user-alice").add_item(
|
|
708
|
+
item_id="item-123",
|
|
709
|
+
quantity=2,
|
|
710
|
+
price=29.99
|
|
711
|
+
)
|
|
712
|
+
```
|
|
713
|
+
"""
|
|
714
|
+
return EntityProxy(self, entity_type, key)
|
|
715
|
+
|
|
716
|
+
def workflow(self, workflow_name: str) -> "WorkflowProxy":
|
|
717
|
+
"""Get a proxy for invoking a workflow with fluent API.
|
|
718
|
+
|
|
719
|
+
This provides a convenient API for workflow invocations, including
|
|
720
|
+
a chat() method for multi-turn conversation workflows.
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
workflow_name: Name of the workflow to invoke
|
|
724
|
+
|
|
725
|
+
Returns:
|
|
726
|
+
WorkflowProxy that provides workflow-specific methods
|
|
727
|
+
|
|
728
|
+
Example:
|
|
729
|
+
```python
|
|
730
|
+
# Standard workflow execution
|
|
731
|
+
result = client.workflow("order_process").run(order_id="123")
|
|
732
|
+
|
|
733
|
+
# Chat workflow with session
|
|
734
|
+
response = client.workflow("support_bot").chat(
|
|
735
|
+
message="My order hasn't arrived",
|
|
736
|
+
session_id="user-123",
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
# Continue conversation
|
|
740
|
+
response = client.workflow("support_bot").chat(
|
|
741
|
+
message="Can you track it?",
|
|
742
|
+
session_id="user-123",
|
|
743
|
+
)
|
|
744
|
+
```
|
|
745
|
+
"""
|
|
746
|
+
return WorkflowProxy(self, workflow_name)
|
|
747
|
+
|
|
748
|
+
def session(self, session_type: str, key: str) -> "SessionProxy":
|
|
749
|
+
"""Get a proxy for a session entity (OpenAI/ADK-style API).
|
|
750
|
+
|
|
751
|
+
This is a convenience wrapper around entity() specifically for SessionEntity subclasses,
|
|
752
|
+
providing a familiar API for developers coming from OpenAI Agents SDK or Google ADK.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
session_type: The session entity class name (e.g., "Conversation", "ChatSession")
|
|
756
|
+
key: The session instance key (typically user ID or session ID)
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
SessionProxy that provides session-specific methods
|
|
760
|
+
|
|
761
|
+
Example:
|
|
762
|
+
```python
|
|
763
|
+
# Create a conversation session
|
|
764
|
+
session = client.session("Conversation", "user-alice")
|
|
765
|
+
|
|
766
|
+
# Chat with the session
|
|
767
|
+
response = session.chat("Hello! How are you?")
|
|
768
|
+
print(response)
|
|
769
|
+
|
|
770
|
+
# Get conversation history
|
|
771
|
+
history = session.get_history()
|
|
772
|
+
for msg in history:
|
|
773
|
+
print(f"{msg['role']}: {msg['content']}")
|
|
774
|
+
```
|
|
775
|
+
"""
|
|
776
|
+
return SessionProxy(self, session_type, key)
|
|
777
|
+
|
|
778
|
+
def close(self):
|
|
779
|
+
"""Close the underlying HTTP client."""
|
|
780
|
+
self._client.close()
|
|
781
|
+
|
|
782
|
+
def __enter__(self):
|
|
783
|
+
"""Context manager entry."""
|
|
784
|
+
return self
|
|
785
|
+
|
|
786
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
787
|
+
"""Context manager exit."""
|
|
788
|
+
self.close()
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
class EntityProxy:
|
|
792
|
+
"""Proxy for calling methods on a durable entity instance.
|
|
793
|
+
|
|
794
|
+
This class enables fluent method calls on entities using Python's
|
|
795
|
+
attribute access. Any method call is translated to an HTTP request
|
|
796
|
+
to /entity/:type/:key/:method.
|
|
797
|
+
|
|
798
|
+
Example:
|
|
799
|
+
```python
|
|
800
|
+
counter = client.entity("Counter", "user-123")
|
|
801
|
+
result = counter.increment(amount=5) # Calls /entity/Counter/user-123/increment
|
|
802
|
+
```
|
|
803
|
+
"""
|
|
804
|
+
|
|
805
|
+
def __init__(self, client: "Client", entity_type: str, key: str):
|
|
806
|
+
"""Initialize entity proxy.
|
|
807
|
+
|
|
808
|
+
Args:
|
|
809
|
+
client: The AGNT5 client instance
|
|
810
|
+
entity_type: The entity class name
|
|
811
|
+
key: The entity instance key
|
|
812
|
+
"""
|
|
813
|
+
self._client = client
|
|
814
|
+
self._entity_type = entity_type
|
|
815
|
+
self._key = key
|
|
816
|
+
|
|
817
|
+
def __getattr__(self, method_name: str):
|
|
818
|
+
"""Dynamic method lookup that creates entity method callers.
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
method_name: The entity method to call
|
|
822
|
+
|
|
823
|
+
Returns:
|
|
824
|
+
Callable that executes the entity method
|
|
825
|
+
"""
|
|
826
|
+
|
|
827
|
+
def method_caller(*args, **kwargs) -> Any:
|
|
828
|
+
"""Call an entity method with the given parameters.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
*args: Positional arguments (not recommended, use kwargs)
|
|
832
|
+
**kwargs: Method parameters as keyword arguments
|
|
833
|
+
|
|
834
|
+
Returns:
|
|
835
|
+
The method's return value
|
|
836
|
+
|
|
837
|
+
Raises:
|
|
838
|
+
RunError: If the method execution fails
|
|
839
|
+
ValueError: If both positional and keyword arguments are provided
|
|
840
|
+
"""
|
|
841
|
+
# Convert positional args to kwargs if provided
|
|
842
|
+
if args and kwargs:
|
|
843
|
+
raise ValueError(
|
|
844
|
+
f"Cannot mix positional and keyword arguments when calling entity method '{method_name}'. "
|
|
845
|
+
"Please use keyword arguments only."
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
# If positional args provided, we can't convert them without knowing parameter names
|
|
849
|
+
# Raise helpful error
|
|
850
|
+
if args:
|
|
851
|
+
raise ValueError(
|
|
852
|
+
f"Entity method '{method_name}' requires keyword arguments, but got {len(args)} positional arguments. "
|
|
853
|
+
f"Example: .{method_name}(param1=value1, param2=value2)"
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
# Build URL: /v1/entity/:entityType/:key/:method
|
|
857
|
+
url = urljoin(
|
|
858
|
+
self._client.gateway_url + "/",
|
|
859
|
+
f"v1/entity/{self._entity_type}/{self._key}/{method_name}",
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
# Make request with method parameters as JSON body and auth headers
|
|
863
|
+
response = self._client._client.post(
|
|
864
|
+
url,
|
|
865
|
+
json=kwargs,
|
|
866
|
+
headers=self._client._build_headers(),
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
# Handle errors
|
|
870
|
+
if response.status_code == 504:
|
|
871
|
+
error_data = response.json()
|
|
872
|
+
raise RunError(
|
|
873
|
+
"Execution timeout",
|
|
874
|
+
run_id=error_data.get("run_id"),
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
if response.status_code == 500:
|
|
878
|
+
try:
|
|
879
|
+
error_data = response.json()
|
|
880
|
+
raise _parse_error_response(error_data)
|
|
881
|
+
except ValueError:
|
|
882
|
+
response.raise_for_status()
|
|
883
|
+
else:
|
|
884
|
+
response.raise_for_status()
|
|
885
|
+
|
|
886
|
+
# Parse response
|
|
887
|
+
data = response.json()
|
|
888
|
+
|
|
889
|
+
# Check execution status
|
|
890
|
+
if data.get("status") == "failed":
|
|
891
|
+
raise _parse_error_response(data)
|
|
892
|
+
|
|
893
|
+
# Return output
|
|
894
|
+
return data.get("output")
|
|
895
|
+
|
|
896
|
+
return method_caller
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
class SessionProxy(EntityProxy):
|
|
900
|
+
"""Proxy for session entities with conversation-specific helper methods.
|
|
901
|
+
|
|
902
|
+
This extends EntityProxy to provide familiar APIs for session-based
|
|
903
|
+
conversations, similar to OpenAI Agents SDK and Google ADK.
|
|
904
|
+
|
|
905
|
+
Example:
|
|
906
|
+
```python
|
|
907
|
+
# Create a session
|
|
908
|
+
session = client.session("Conversation", "user-alice")
|
|
909
|
+
|
|
910
|
+
# Chat
|
|
911
|
+
response = session.chat("Tell me about AI")
|
|
912
|
+
|
|
913
|
+
# Get history
|
|
914
|
+
history = session.get_history()
|
|
915
|
+
```
|
|
916
|
+
"""
|
|
917
|
+
|
|
918
|
+
def chat(self, message: str, **kwargs) -> str:
|
|
919
|
+
"""Send a message to the conversation session.
|
|
920
|
+
|
|
921
|
+
This is a convenience method that calls the `chat` method on the
|
|
922
|
+
underlying SessionEntity and returns just the response text.
|
|
923
|
+
|
|
924
|
+
Args:
|
|
925
|
+
message: The user's message
|
|
926
|
+
**kwargs: Additional parameters to pass to the chat method
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
The assistant's response as a string
|
|
930
|
+
|
|
931
|
+
Example:
|
|
932
|
+
```python
|
|
933
|
+
response = session.chat("What is the weather today?")
|
|
934
|
+
print(response)
|
|
935
|
+
```
|
|
936
|
+
"""
|
|
937
|
+
# Call the chat method via the entity proxy
|
|
938
|
+
result = self.__getattr__("chat")(message=message, **kwargs)
|
|
939
|
+
|
|
940
|
+
# SessionEntity.chat() returns a dict with 'response' key
|
|
941
|
+
if isinstance(result, dict) and "response" in result:
|
|
942
|
+
return result["response"]
|
|
943
|
+
|
|
944
|
+
# If it's already a string, return as-is
|
|
945
|
+
return str(result)
|
|
946
|
+
|
|
947
|
+
def get_history(self) -> list:
|
|
948
|
+
"""Get the conversation history for this session.
|
|
949
|
+
|
|
950
|
+
Returns:
|
|
951
|
+
List of message dictionaries with 'role' and 'content' keys
|
|
952
|
+
|
|
953
|
+
Example:
|
|
954
|
+
```python
|
|
955
|
+
history = session.get_history()
|
|
956
|
+
for msg in history:
|
|
957
|
+
print(f"{msg['role']}: {msg['content']}")
|
|
958
|
+
```
|
|
959
|
+
"""
|
|
960
|
+
return self.__getattr__("get_history")()
|
|
961
|
+
|
|
962
|
+
def add_message(self, role: str, content: str) -> dict:
|
|
963
|
+
"""Add a message to the conversation history.
|
|
964
|
+
|
|
965
|
+
Args:
|
|
966
|
+
role: Message role ('user', 'assistant', or 'system')
|
|
967
|
+
content: Message content
|
|
968
|
+
|
|
969
|
+
Returns:
|
|
970
|
+
Dictionary confirming the message was added
|
|
971
|
+
|
|
972
|
+
Example:
|
|
973
|
+
```python
|
|
974
|
+
session.add_message("system", "You are a helpful assistant")
|
|
975
|
+
session.add_message("user", "Hello!")
|
|
976
|
+
```
|
|
977
|
+
"""
|
|
978
|
+
return self.__getattr__("add_message")(role=role, content=content)
|
|
979
|
+
|
|
980
|
+
def clear_history(self) -> dict:
|
|
981
|
+
"""Clear the conversation history for this session.
|
|
982
|
+
|
|
983
|
+
Returns:
|
|
984
|
+
Dictionary confirming the history was cleared
|
|
985
|
+
|
|
986
|
+
Example:
|
|
987
|
+
```python
|
|
988
|
+
session.clear_history()
|
|
989
|
+
```
|
|
990
|
+
"""
|
|
991
|
+
return self.__getattr__("clear_history")()
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
class WorkflowProxy:
|
|
995
|
+
"""Proxy for invoking workflows with a fluent API.
|
|
996
|
+
|
|
997
|
+
Provides convenient methods for workflow execution, including
|
|
998
|
+
a chat() method for multi-turn conversation workflows.
|
|
999
|
+
|
|
1000
|
+
Example:
|
|
1001
|
+
```python
|
|
1002
|
+
# Standard workflow
|
|
1003
|
+
result = client.workflow("order_process").run(order_id="123")
|
|
1004
|
+
|
|
1005
|
+
# Chat workflow
|
|
1006
|
+
response = client.workflow("support_bot").chat(
|
|
1007
|
+
message="Help me",
|
|
1008
|
+
session_id="user-123",
|
|
1009
|
+
)
|
|
1010
|
+
```
|
|
1011
|
+
"""
|
|
1012
|
+
|
|
1013
|
+
def __init__(self, client: "Client", workflow_name: str):
|
|
1014
|
+
"""Initialize workflow proxy.
|
|
1015
|
+
|
|
1016
|
+
Args:
|
|
1017
|
+
client: The AGNT5 client instance
|
|
1018
|
+
workflow_name: Name of the workflow
|
|
1019
|
+
"""
|
|
1020
|
+
self._client = client
|
|
1021
|
+
self._workflow_name = workflow_name
|
|
1022
|
+
|
|
1023
|
+
def run(
|
|
1024
|
+
self,
|
|
1025
|
+
session_id: Optional[str] = None,
|
|
1026
|
+
user_id: Optional[str] = None,
|
|
1027
|
+
**kwargs,
|
|
1028
|
+
) -> Dict[str, Any]:
|
|
1029
|
+
"""Execute the workflow synchronously.
|
|
1030
|
+
|
|
1031
|
+
Args:
|
|
1032
|
+
session_id: Session identifier for multi-turn workflows (optional)
|
|
1033
|
+
user_id: User identifier for user-scoped memory (optional)
|
|
1034
|
+
**kwargs: Input parameters for the workflow
|
|
1035
|
+
|
|
1036
|
+
Returns:
|
|
1037
|
+
Dictionary containing the workflow's output
|
|
1038
|
+
|
|
1039
|
+
Example:
|
|
1040
|
+
```python
|
|
1041
|
+
result = client.workflow("order_process").run(
|
|
1042
|
+
order_id="123",
|
|
1043
|
+
customer_id="cust-456",
|
|
1044
|
+
)
|
|
1045
|
+
```
|
|
1046
|
+
"""
|
|
1047
|
+
return self._client.run(
|
|
1048
|
+
component=self._workflow_name,
|
|
1049
|
+
input_data=kwargs,
|
|
1050
|
+
component_type="workflow",
|
|
1051
|
+
session_id=session_id,
|
|
1052
|
+
user_id=user_id,
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
def chat(
|
|
1056
|
+
self,
|
|
1057
|
+
message: str,
|
|
1058
|
+
session_id: Optional[str] = None,
|
|
1059
|
+
user_id: Optional[str] = None,
|
|
1060
|
+
**kwargs,
|
|
1061
|
+
) -> Dict[str, Any]:
|
|
1062
|
+
"""Send a message to a chat-enabled workflow.
|
|
1063
|
+
|
|
1064
|
+
This is a convenience method for multi-turn conversation workflows.
|
|
1065
|
+
The message is passed as the 'message' input parameter.
|
|
1066
|
+
|
|
1067
|
+
Args:
|
|
1068
|
+
message: The user's message
|
|
1069
|
+
session_id: Session identifier for conversation continuity (recommended)
|
|
1070
|
+
user_id: User identifier for user-scoped memory (optional)
|
|
1071
|
+
**kwargs: Additional input parameters for the workflow
|
|
1072
|
+
|
|
1073
|
+
Returns:
|
|
1074
|
+
Dictionary containing the workflow's response (typically has 'response' key)
|
|
1075
|
+
|
|
1076
|
+
Example:
|
|
1077
|
+
```python
|
|
1078
|
+
# First message
|
|
1079
|
+
result = client.workflow("support_bot").chat(
|
|
1080
|
+
message="My order hasn't arrived",
|
|
1081
|
+
session_id="session-123",
|
|
1082
|
+
)
|
|
1083
|
+
print(result.get("response"))
|
|
1084
|
+
|
|
1085
|
+
# Continue conversation
|
|
1086
|
+
result = client.workflow("support_bot").chat(
|
|
1087
|
+
message="Can you track it?",
|
|
1088
|
+
session_id="session-123",
|
|
1089
|
+
)
|
|
1090
|
+
```
|
|
1091
|
+
"""
|
|
1092
|
+
# Merge message into kwargs
|
|
1093
|
+
input_data = {"message": message, **kwargs}
|
|
1094
|
+
|
|
1095
|
+
return self._client.run(
|
|
1096
|
+
component=self._workflow_name,
|
|
1097
|
+
input_data=input_data,
|
|
1098
|
+
component_type="workflow",
|
|
1099
|
+
session_id=session_id,
|
|
1100
|
+
user_id=user_id,
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
def stream_events(
|
|
1104
|
+
self,
|
|
1105
|
+
session_id: Optional[str] = None,
|
|
1106
|
+
user_id: Optional[str] = None,
|
|
1107
|
+
timeout: float = 300.0,
|
|
1108
|
+
**kwargs,
|
|
1109
|
+
) -> Iterator[Event]:
|
|
1110
|
+
"""Stream typed Event objects from workflow execution.
|
|
1111
|
+
|
|
1112
|
+
This method yields Event objects as they arrive from the workflow,
|
|
1113
|
+
including nested events from agents and functions called within the workflow.
|
|
1114
|
+
|
|
1115
|
+
Args:
|
|
1116
|
+
session_id: Session identifier for multi-turn workflows (optional)
|
|
1117
|
+
user_id: User identifier for user-scoped memory (optional)
|
|
1118
|
+
timeout: Stream timeout in seconds (default: 300.0 / 5 minutes)
|
|
1119
|
+
**kwargs: Input parameters for the workflow
|
|
1120
|
+
|
|
1121
|
+
Yields:
|
|
1122
|
+
Event objects as they arrive from the stream
|
|
1123
|
+
|
|
1124
|
+
Example:
|
|
1125
|
+
```python
|
|
1126
|
+
from agnt5 import Client, EventType
|
|
1127
|
+
|
|
1128
|
+
# Stream workflow events
|
|
1129
|
+
for event in client.workflow("research_workflow").stream_events(query="AI"):
|
|
1130
|
+
if event.event_type == EventType.WORKFLOW_STEP_STARTED:
|
|
1131
|
+
print(f"Step started: {event.data.get('step_name')}")
|
|
1132
|
+
elif event.event_type == EventType.LM_MESSAGE_DELTA:
|
|
1133
|
+
print(event.data['content'], end='', flush=True)
|
|
1134
|
+
elif event.event_type == EventType.WORKFLOW_STEP_COMPLETED:
|
|
1135
|
+
print(f"\\nStep done: {event.data.get('step_name')}")
|
|
1136
|
+
```
|
|
1137
|
+
"""
|
|
1138
|
+
return self._client.stream_events(
|
|
1139
|
+
component=self._workflow_name,
|
|
1140
|
+
input_data=kwargs,
|
|
1141
|
+
component_type="workflow",
|
|
1142
|
+
session_id=session_id,
|
|
1143
|
+
user_id=user_id,
|
|
1144
|
+
timeout=timeout,
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
def submit(self, **kwargs) -> str:
|
|
1148
|
+
"""Submit the workflow for async execution.
|
|
1149
|
+
|
|
1150
|
+
Args:
|
|
1151
|
+
**kwargs: Input parameters for the workflow
|
|
1152
|
+
|
|
1153
|
+
Returns:
|
|
1154
|
+
Run ID for tracking the execution
|
|
1155
|
+
|
|
1156
|
+
Example:
|
|
1157
|
+
```python
|
|
1158
|
+
run_id = client.workflow("long_process").submit(data="...")
|
|
1159
|
+
# Check status later
|
|
1160
|
+
status = client.get_status(run_id)
|
|
1161
|
+
```
|
|
1162
|
+
"""
|
|
1163
|
+
return self._client.submit(
|
|
1164
|
+
component=self._workflow_name,
|
|
1165
|
+
input_data=kwargs,
|
|
1166
|
+
component_type="workflow",
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
class AsyncClient:
|
|
1171
|
+
"""Async client for invoking AGNT5 components.
|
|
1172
|
+
|
|
1173
|
+
This client provides an async interface for calling functions, workflows,
|
|
1174
|
+
and other components deployed on AGNT5. Use this when you need to stream
|
|
1175
|
+
events in an async context or integrate with async frameworks.
|
|
1176
|
+
|
|
1177
|
+
Example:
|
|
1178
|
+
```python
|
|
1179
|
+
import asyncio
|
|
1180
|
+
from agnt5 import AsyncClient, EventType
|
|
1181
|
+
|
|
1182
|
+
async def main():
|
|
1183
|
+
async with AsyncClient() as client:
|
|
1184
|
+
# Stream agent events asynchronously
|
|
1185
|
+
async for event in client.stream_events("my_agent", {"msg": "Hi"}, "agent"):
|
|
1186
|
+
if event.event_type == EventType.LM_MESSAGE_DELTA:
|
|
1187
|
+
print(event.data['content'], end='', flush=True)
|
|
1188
|
+
|
|
1189
|
+
asyncio.run(main())
|
|
1190
|
+
```
|
|
1191
|
+
"""
|
|
1192
|
+
|
|
1193
|
+
def __init__(
|
|
1194
|
+
self,
|
|
1195
|
+
gateway_url: str = "http://localhost:34181",
|
|
1196
|
+
timeout: float = 30.0,
|
|
1197
|
+
api_key: Optional[str] = None,
|
|
1198
|
+
):
|
|
1199
|
+
"""Initialize the async AGNT5 client.
|
|
1200
|
+
|
|
1201
|
+
Args:
|
|
1202
|
+
gateway_url: Base URL of the AGNT5 gateway (default: http://localhost:34181)
|
|
1203
|
+
timeout: Request timeout in seconds (default: 30.0)
|
|
1204
|
+
api_key: Service key for authentication. If not provided, falls back to
|
|
1205
|
+
AGNT5_API_KEY environment variable. Keys start with "agnt5_sk_".
|
|
1206
|
+
"""
|
|
1207
|
+
self.gateway_url = gateway_url.rstrip("/")
|
|
1208
|
+
self.timeout = timeout
|
|
1209
|
+
# Use provided api_key or fallback to environment variable
|
|
1210
|
+
self.api_key = api_key or os.environ.get(AGNT5_API_KEY_ENV)
|
|
1211
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
1212
|
+
|
|
1213
|
+
def _build_headers(
|
|
1214
|
+
self,
|
|
1215
|
+
session_id: Optional[str] = None,
|
|
1216
|
+
user_id: Optional[str] = None,
|
|
1217
|
+
) -> Dict[str, str]:
|
|
1218
|
+
"""Build request headers with authentication and optional session/user context.
|
|
1219
|
+
|
|
1220
|
+
Args:
|
|
1221
|
+
session_id: Session identifier for multi-turn conversations
|
|
1222
|
+
user_id: User identifier for user-scoped memory
|
|
1223
|
+
|
|
1224
|
+
Returns:
|
|
1225
|
+
Dictionary of HTTP headers
|
|
1226
|
+
"""
|
|
1227
|
+
headers = {"Content-Type": "application/json"}
|
|
1228
|
+
if self.api_key:
|
|
1229
|
+
headers["X-API-KEY"] = self.api_key
|
|
1230
|
+
if session_id:
|
|
1231
|
+
headers["X-Session-ID"] = session_id
|
|
1232
|
+
if user_id:
|
|
1233
|
+
headers["X-User-ID"] = user_id
|
|
1234
|
+
return headers
|
|
1235
|
+
|
|
1236
|
+
async def __aenter__(self) -> "AsyncClient":
|
|
1237
|
+
"""Async context manager entry."""
|
|
1238
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
1239
|
+
return self
|
|
1240
|
+
|
|
1241
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
1242
|
+
"""Async context manager exit."""
|
|
1243
|
+
if self._client:
|
|
1244
|
+
await self._client.aclose()
|
|
1245
|
+
self._client = None
|
|
1246
|
+
|
|
1247
|
+
async def _ensure_client(self) -> httpx.AsyncClient:
|
|
1248
|
+
"""Ensure async client is available."""
|
|
1249
|
+
if self._client is None:
|
|
1250
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
1251
|
+
return self._client
|
|
1252
|
+
|
|
1253
|
+
async def close(self) -> None:
|
|
1254
|
+
"""Close the underlying async HTTP client."""
|
|
1255
|
+
if self._client:
|
|
1256
|
+
await self._client.aclose()
|
|
1257
|
+
self._client = None
|
|
1258
|
+
|
|
1259
|
+
async def run(
|
|
1260
|
+
self,
|
|
1261
|
+
component: str,
|
|
1262
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
1263
|
+
component_type: str = "function",
|
|
1264
|
+
session_id: Optional[str] = None,
|
|
1265
|
+
user_id: Optional[str] = None,
|
|
1266
|
+
) -> Dict[str, Any]:
|
|
1267
|
+
"""Execute a component asynchronously and wait for the result.
|
|
1268
|
+
|
|
1269
|
+
Args:
|
|
1270
|
+
component: Name of the component to execute
|
|
1271
|
+
input_data: Input data for the component
|
|
1272
|
+
component_type: Type of component - "function", "workflow", "agent", "tool"
|
|
1273
|
+
session_id: Session identifier for multi-turn conversations
|
|
1274
|
+
user_id: User identifier for user-scoped memory
|
|
1275
|
+
|
|
1276
|
+
Returns:
|
|
1277
|
+
Dictionary containing the component's output
|
|
1278
|
+
|
|
1279
|
+
Raises:
|
|
1280
|
+
RunError: If the component execution fails
|
|
1281
|
+
httpx.HTTPError: If the HTTP request fails
|
|
1282
|
+
"""
|
|
1283
|
+
if input_data is None:
|
|
1284
|
+
input_data = {}
|
|
1285
|
+
|
|
1286
|
+
client = await self._ensure_client()
|
|
1287
|
+
url = urljoin(self.gateway_url + "/", f"v1/run/{component_type}/{component}")
|
|
1288
|
+
|
|
1289
|
+
response = await client.post(
|
|
1290
|
+
url,
|
|
1291
|
+
json=input_data,
|
|
1292
|
+
headers=self._build_headers(session_id=session_id, user_id=user_id),
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
if response.status_code == 404:
|
|
1296
|
+
try:
|
|
1297
|
+
error_data = response.json()
|
|
1298
|
+
raise RunError(
|
|
1299
|
+
error_data.get("error", "Component not found"),
|
|
1300
|
+
run_id=error_data.get("runId"),
|
|
1301
|
+
)
|
|
1302
|
+
except ValueError:
|
|
1303
|
+
raise RunError(f"Component '{component}' not found")
|
|
1304
|
+
|
|
1305
|
+
if response.status_code in (500, 503, 504):
|
|
1306
|
+
try:
|
|
1307
|
+
error_data = response.json()
|
|
1308
|
+
raise _parse_error_response(error_data)
|
|
1309
|
+
except ValueError:
|
|
1310
|
+
response.raise_for_status()
|
|
1311
|
+
else:
|
|
1312
|
+
response.raise_for_status()
|
|
1313
|
+
|
|
1314
|
+
data = response.json()
|
|
1315
|
+
if data.get("status") == "failed":
|
|
1316
|
+
raise _parse_error_response(data)
|
|
1317
|
+
|
|
1318
|
+
return data.get("output", {})
|
|
1319
|
+
|
|
1320
|
+
async def stream_events(
|
|
1321
|
+
self,
|
|
1322
|
+
component: str,
|
|
1323
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
1324
|
+
component_type: str = "function",
|
|
1325
|
+
session_id: Optional[str] = None,
|
|
1326
|
+
user_id: Optional[str] = None,
|
|
1327
|
+
timeout: float = 300.0,
|
|
1328
|
+
) -> AsyncIterator[Event]:
|
|
1329
|
+
"""Async stream typed Event objects from a component execution.
|
|
1330
|
+
|
|
1331
|
+
This method yields Event objects as they arrive from the component,
|
|
1332
|
+
providing full access to the event taxonomy including agent lifecycle,
|
|
1333
|
+
LM streaming, tool calls, and workflow events.
|
|
1334
|
+
|
|
1335
|
+
Args:
|
|
1336
|
+
component: Name of the component to execute
|
|
1337
|
+
input_data: Input data for the component
|
|
1338
|
+
component_type: Type of component - "function", "workflow", "agent", "tool"
|
|
1339
|
+
session_id: Session identifier for multi-turn conversations
|
|
1340
|
+
user_id: User identifier for user-scoped memory
|
|
1341
|
+
timeout: Stream timeout in seconds (default: 300.0 / 5 minutes)
|
|
1342
|
+
|
|
1343
|
+
Yields:
|
|
1344
|
+
Event objects as they arrive from the stream
|
|
1345
|
+
|
|
1346
|
+
Raises:
|
|
1347
|
+
RunError: If the component execution fails
|
|
1348
|
+
httpx.HTTPError: If the HTTP request fails
|
|
1349
|
+
|
|
1350
|
+
Example:
|
|
1351
|
+
```python
|
|
1352
|
+
async with AsyncClient() as client:
|
|
1353
|
+
async for event in client.stream_events("my_agent", {"msg": "Hi"}, "agent"):
|
|
1354
|
+
if event.event_type == EventType.AGENT_STARTED:
|
|
1355
|
+
print(f"Agent started: {event.data['agent_name']}")
|
|
1356
|
+
elif event.event_type == EventType.LM_MESSAGE_DELTA:
|
|
1357
|
+
print(event.data['content'], end='', flush=True)
|
|
1358
|
+
```
|
|
1359
|
+
"""
|
|
1360
|
+
if timeout <= 0:
|
|
1361
|
+
raise ValueError("timeout must be a positive number")
|
|
1362
|
+
|
|
1363
|
+
if input_data is None:
|
|
1364
|
+
input_data = {}
|
|
1365
|
+
|
|
1366
|
+
client = await self._ensure_client()
|
|
1367
|
+
url = urljoin(self.gateway_url + "/", f"v1/stream/{component_type}/{component}")
|
|
1368
|
+
|
|
1369
|
+
async with client.stream(
|
|
1370
|
+
"POST",
|
|
1371
|
+
url,
|
|
1372
|
+
json=input_data,
|
|
1373
|
+
headers=self._build_headers(session_id=session_id, user_id=user_id),
|
|
1374
|
+
timeout=timeout,
|
|
1375
|
+
) as response:
|
|
1376
|
+
if response.status_code != 200:
|
|
1377
|
+
# Try to get error details from response body
|
|
1378
|
+
try:
|
|
1379
|
+
error_body = (await response.aread()).decode("utf-8")
|
|
1380
|
+
error_data = json.loads(error_body)
|
|
1381
|
+
error_msg = error_data.get("error", f"HTTP {response.status_code}")
|
|
1382
|
+
run_id = error_data.get("runId")
|
|
1383
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
1384
|
+
error_msg = f"HTTP {response.status_code}: Streaming request failed"
|
|
1385
|
+
run_id = None
|
|
1386
|
+
raise RunError(error_msg, run_id=run_id)
|
|
1387
|
+
|
|
1388
|
+
current_event_type: Optional[str] = None
|
|
1389
|
+
async for line in response.aiter_lines():
|
|
1390
|
+
line = line.strip()
|
|
1391
|
+
|
|
1392
|
+
# Skip empty lines and comments (keep-alive)
|
|
1393
|
+
if not line or line.startswith(":"):
|
|
1394
|
+
continue
|
|
1395
|
+
|
|
1396
|
+
# Parse event type: "event: agent.started"
|
|
1397
|
+
if line.startswith("event: "):
|
|
1398
|
+
current_event_type = line[7:]
|
|
1399
|
+
continue
|
|
1400
|
+
|
|
1401
|
+
# Parse SSE data: "data: {...}"
|
|
1402
|
+
if line.startswith("data: "):
|
|
1403
|
+
data_str = line[6:]
|
|
1404
|
+
|
|
1405
|
+
try:
|
|
1406
|
+
data = json.loads(data_str)
|
|
1407
|
+
|
|
1408
|
+
# Check for completion signal
|
|
1409
|
+
if data.get("done") or current_event_type == "done":
|
|
1410
|
+
return
|
|
1411
|
+
|
|
1412
|
+
# Check for error event
|
|
1413
|
+
if current_event_type == "error" or "error" in data:
|
|
1414
|
+
error_msg = data.get("error", "Unknown streaming error")
|
|
1415
|
+
raise RunError(error_msg, run_id=data.get("runId"))
|
|
1416
|
+
|
|
1417
|
+
# Yield typed Event object
|
|
1418
|
+
if current_event_type:
|
|
1419
|
+
yield _parse_sse_to_event(current_event_type, data)
|
|
1420
|
+
|
|
1421
|
+
except json.JSONDecodeError:
|
|
1422
|
+
continue
|
|
1423
|
+
|
|
1424
|
+
async def submit(
|
|
1425
|
+
self,
|
|
1426
|
+
component: str,
|
|
1427
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
1428
|
+
component_type: str = "function",
|
|
1429
|
+
) -> str:
|
|
1430
|
+
"""Submit a component for async execution and return immediately.
|
|
1431
|
+
|
|
1432
|
+
Args:
|
|
1433
|
+
component: Name of the component to execute
|
|
1434
|
+
input_data: Input data for the component
|
|
1435
|
+
component_type: Type of component
|
|
1436
|
+
|
|
1437
|
+
Returns:
|
|
1438
|
+
String containing the run ID
|
|
1439
|
+
"""
|
|
1440
|
+
if input_data is None:
|
|
1441
|
+
input_data = {}
|
|
1442
|
+
|
|
1443
|
+
client = await self._ensure_client()
|
|
1444
|
+
url = urljoin(self.gateway_url + "/", f"v1/submit/{component_type}/{component}")
|
|
1445
|
+
|
|
1446
|
+
response = await client.post(
|
|
1447
|
+
url,
|
|
1448
|
+
json=input_data,
|
|
1449
|
+
headers=self._build_headers(),
|
|
1450
|
+
)
|
|
1451
|
+
response.raise_for_status()
|
|
1452
|
+
|
|
1453
|
+
data = response.json()
|
|
1454
|
+
return data.get("runId", "")
|
|
1455
|
+
|
|
1456
|
+
async def get_status(self, run_id: str) -> Dict[str, Any]:
|
|
1457
|
+
"""Get the current status of a run.
|
|
1458
|
+
|
|
1459
|
+
Args:
|
|
1460
|
+
run_id: The run ID returned from submit()
|
|
1461
|
+
|
|
1462
|
+
Returns:
|
|
1463
|
+
Dictionary containing status information
|
|
1464
|
+
"""
|
|
1465
|
+
client = await self._ensure_client()
|
|
1466
|
+
url = urljoin(self.gateway_url + "/", f"v1/status/{run_id}")
|
|
1467
|
+
|
|
1468
|
+
response = await client.get(url, headers=self._build_headers())
|
|
1469
|
+
response.raise_for_status()
|
|
1470
|
+
|
|
1471
|
+
return response.json()
|
|
1472
|
+
|
|
1473
|
+
async def get_result(self, run_id: str) -> Dict[str, Any]:
|
|
1474
|
+
"""Get the result of a completed run.
|
|
1475
|
+
|
|
1476
|
+
Args:
|
|
1477
|
+
run_id: The run ID returned from submit()
|
|
1478
|
+
|
|
1479
|
+
Returns:
|
|
1480
|
+
Dictionary containing the component's output
|
|
1481
|
+
|
|
1482
|
+
Raises:
|
|
1483
|
+
RunError: If the run failed or is not yet complete
|
|
1484
|
+
"""
|
|
1485
|
+
client = await self._ensure_client()
|
|
1486
|
+
url = urljoin(self.gateway_url + "/", f"v1/result/{run_id}")
|
|
1487
|
+
|
|
1488
|
+
response = await client.get(url, headers=self._build_headers())
|
|
1489
|
+
|
|
1490
|
+
if response.status_code == 404:
|
|
1491
|
+
error_data = response.json()
|
|
1492
|
+
error_msg = error_data.get("error", "Run not found or not complete")
|
|
1493
|
+
current_status = error_data.get("status", "unknown")
|
|
1494
|
+
raise RunError(f"{error_msg} (status: {current_status})", run_id=run_id)
|
|
1495
|
+
|
|
1496
|
+
response.raise_for_status()
|
|
1497
|
+
data = response.json()
|
|
1498
|
+
|
|
1499
|
+
if data.get("status") == "failed":
|
|
1500
|
+
raise _parse_error_response(data, run_id=run_id)
|
|
1501
|
+
|
|
1502
|
+
return data.get("output", {})
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
class RunError(Exception):
|
|
1506
|
+
"""Raised when a component run fails on AGNT5.
|
|
1507
|
+
|
|
1508
|
+
Attributes:
|
|
1509
|
+
message: Error message describing what went wrong
|
|
1510
|
+
run_id: The unique run ID associated with this execution (if available)
|
|
1511
|
+
error_code: Structured error code (e.g., "EXECUTION_FAILED", "GRPC_ERROR")
|
|
1512
|
+
attempts: Number of execution attempts made (1-indexed)
|
|
1513
|
+
max_attempts: Maximum attempts configured for this component
|
|
1514
|
+
metadata: Full metadata dict from platform response
|
|
1515
|
+
"""
|
|
1516
|
+
|
|
1517
|
+
def __init__(
|
|
1518
|
+
self,
|
|
1519
|
+
message: str,
|
|
1520
|
+
run_id: Optional[str] = None,
|
|
1521
|
+
error_code: Optional[str] = None,
|
|
1522
|
+
attempts: Optional[int] = None,
|
|
1523
|
+
max_attempts: Optional[int] = None,
|
|
1524
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1525
|
+
):
|
|
1526
|
+
super().__init__(message)
|
|
1527
|
+
self.message = message
|
|
1528
|
+
self.run_id = run_id
|
|
1529
|
+
self.error_code = error_code
|
|
1530
|
+
self.attempts = attempts
|
|
1531
|
+
self.max_attempts = max_attempts
|
|
1532
|
+
self.metadata = metadata or {}
|
|
1533
|
+
|
|
1534
|
+
@property
|
|
1535
|
+
def was_retried(self) -> bool:
|
|
1536
|
+
"""Returns True if execution was retried at least once."""
|
|
1537
|
+
return self.attempts is not None and self.attempts > 1
|
|
1538
|
+
|
|
1539
|
+
@property
|
|
1540
|
+
def exhausted_retries(self) -> bool:
|
|
1541
|
+
"""Returns True if all retry attempts were exhausted."""
|
|
1542
|
+
if self.attempts is None or self.max_attempts is None:
|
|
1543
|
+
return False
|
|
1544
|
+
return self.attempts >= self.max_attempts
|
|
1545
|
+
|
|
1546
|
+
def __str__(self):
|
|
1547
|
+
parts = [self.message]
|
|
1548
|
+
if self.run_id:
|
|
1549
|
+
parts.append(f"run_id: {self.run_id}")
|
|
1550
|
+
if self.attempts is not None and self.max_attempts is not None:
|
|
1551
|
+
parts.append(f"attempts: {self.attempts}/{self.max_attempts}")
|
|
1552
|
+
if self.error_code:
|
|
1553
|
+
parts.append(f"error_code: {self.error_code}")
|
|
1554
|
+
if len(parts) > 1:
|
|
1555
|
+
return f"{parts[0]} ({', '.join(parts[1:])})"
|
|
1556
|
+
return self.message
|