agnt5 0.2.8a10__cp310-abi3-manylinux_2_34_x86_64.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 +91 -0
- agnt5/_compat.py +16 -0
- agnt5/_core.abi3.so +0 -0
- agnt5/_retry_utils.py +169 -0
- agnt5/_schema_utils.py +312 -0
- agnt5/_telemetry.py +182 -0
- agnt5/agent.py +1685 -0
- agnt5/client.py +741 -0
- agnt5/context.py +178 -0
- agnt5/entity.py +795 -0
- agnt5/exceptions.py +102 -0
- agnt5/function.py +321 -0
- agnt5/lm.py +813 -0
- agnt5/tool.py +648 -0
- agnt5/tracing.py +196 -0
- agnt5/types.py +110 -0
- agnt5/version.py +19 -0
- agnt5/worker.py +1619 -0
- agnt5/workflow.py +1048 -0
- agnt5-0.2.8a10.dist-info/METADATA +25 -0
- agnt5-0.2.8a10.dist-info/RECORD +22 -0
- agnt5-0.2.8a10.dist-info/WHEEL +4 -0
agnt5/client.py
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
"""AGNT5 Client SDK for invoking components."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Client:
|
|
11
|
+
"""Client for invoking AGNT5 components.
|
|
12
|
+
|
|
13
|
+
This client provides a simple interface for calling functions, workflows,
|
|
14
|
+
and other components deployed on AGNT5.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
```python
|
|
18
|
+
from agnt5 import Client
|
|
19
|
+
|
|
20
|
+
client = Client("http://localhost:34181")
|
|
21
|
+
result = client.run("greet", {"name": "Alice"})
|
|
22
|
+
print(result) # {"message": "Hello, Alice!"}
|
|
23
|
+
```
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
gateway_url: str = "http://localhost:34181",
|
|
29
|
+
timeout: float = 30.0,
|
|
30
|
+
):
|
|
31
|
+
"""Initialize the AGNT5 client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
gateway_url: Base URL of the AGNT5 gateway (default: http://localhost:34181)
|
|
35
|
+
timeout: Request timeout in seconds (default: 30.0)
|
|
36
|
+
"""
|
|
37
|
+
self.gateway_url = gateway_url.rstrip("/")
|
|
38
|
+
self.timeout = timeout
|
|
39
|
+
self._client = httpx.Client(timeout=timeout)
|
|
40
|
+
|
|
41
|
+
def run(
|
|
42
|
+
self,
|
|
43
|
+
component: str,
|
|
44
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
45
|
+
component_type: str = "function",
|
|
46
|
+
session_id: Optional[str] = None,
|
|
47
|
+
user_id: Optional[str] = None,
|
|
48
|
+
) -> Dict[str, Any]:
|
|
49
|
+
"""Execute a component synchronously and wait for the result.
|
|
50
|
+
|
|
51
|
+
This is a blocking call that waits for the component to complete execution.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
component: Name of the component to execute
|
|
55
|
+
input_data: Input data for the component (will be sent as JSON body)
|
|
56
|
+
component_type: Type of component - "function", "workflow", "agent", "tool" (default: "function")
|
|
57
|
+
session_id: Session identifier for multi-turn conversations (optional)
|
|
58
|
+
user_id: User identifier for user-scoped memory (optional)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary containing the component's output
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
RunError: If the component execution fails
|
|
65
|
+
httpx.HTTPError: If the HTTP request fails
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
```python
|
|
69
|
+
# Simple function call (default)
|
|
70
|
+
result = client.run("greet", {"name": "Alice"})
|
|
71
|
+
|
|
72
|
+
# Workflow execution (explicit)
|
|
73
|
+
result = client.run("order_fulfillment", {"order_id": "123"}, component_type="workflow")
|
|
74
|
+
|
|
75
|
+
# Multi-turn conversation with session
|
|
76
|
+
result = client.run("chat", {"message": "Hello"}, session_id="session-123")
|
|
77
|
+
|
|
78
|
+
# User-scoped memory
|
|
79
|
+
result = client.run("assistant", {"message": "Help me"}, user_id="user-456")
|
|
80
|
+
|
|
81
|
+
# No input data
|
|
82
|
+
result = client.run("get_status")
|
|
83
|
+
```
|
|
84
|
+
"""
|
|
85
|
+
if input_data is None:
|
|
86
|
+
input_data = {}
|
|
87
|
+
|
|
88
|
+
# Build URL with component type
|
|
89
|
+
url = urljoin(self.gateway_url + "/", f"v1/run/{component_type}/{component}")
|
|
90
|
+
|
|
91
|
+
# Build headers with memory scoping identifiers
|
|
92
|
+
headers = {"Content-Type": "application/json"}
|
|
93
|
+
if session_id:
|
|
94
|
+
headers["X-Session-ID"] = session_id
|
|
95
|
+
if user_id:
|
|
96
|
+
headers["X-User-ID"] = user_id
|
|
97
|
+
|
|
98
|
+
# Make request
|
|
99
|
+
response = self._client.post(
|
|
100
|
+
url,
|
|
101
|
+
json=input_data,
|
|
102
|
+
headers=headers,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Handle errors
|
|
106
|
+
if response.status_code == 404:
|
|
107
|
+
try:
|
|
108
|
+
error_data = response.json()
|
|
109
|
+
raise RunError(
|
|
110
|
+
error_data.get("error", "Component not found"),
|
|
111
|
+
run_id=error_data.get("runId"),
|
|
112
|
+
)
|
|
113
|
+
except ValueError:
|
|
114
|
+
# JSON parsing failed
|
|
115
|
+
raise RunError(f"Component '{component}' not found")
|
|
116
|
+
|
|
117
|
+
if response.status_code == 503:
|
|
118
|
+
error_data = response.json()
|
|
119
|
+
raise RunError(
|
|
120
|
+
f"Service unavailable: {error_data.get('error', 'Unknown error')}",
|
|
121
|
+
run_id=error_data.get("runId"),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if response.status_code == 504:
|
|
125
|
+
error_data = response.json()
|
|
126
|
+
raise RunError(
|
|
127
|
+
"Execution timeout",
|
|
128
|
+
run_id=error_data.get("runId"),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Handle 500 errors with our RunResponse format
|
|
132
|
+
if response.status_code == 500:
|
|
133
|
+
try:
|
|
134
|
+
error_data = response.json()
|
|
135
|
+
raise RunError(
|
|
136
|
+
error_data.get("error", "Unknown error"),
|
|
137
|
+
run_id=error_data.get("runId"),
|
|
138
|
+
)
|
|
139
|
+
except ValueError:
|
|
140
|
+
# JSON parsing failed, fall through to raise_for_status
|
|
141
|
+
response.raise_for_status()
|
|
142
|
+
else:
|
|
143
|
+
# For other error codes, use standard HTTP error handling
|
|
144
|
+
response.raise_for_status()
|
|
145
|
+
|
|
146
|
+
# Parse response
|
|
147
|
+
data = response.json()
|
|
148
|
+
|
|
149
|
+
# Check execution status
|
|
150
|
+
if data.get("status") == "failed":
|
|
151
|
+
raise RunError(
|
|
152
|
+
data.get("error", "Unknown error"),
|
|
153
|
+
run_id=data.get("runId"),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Return output
|
|
157
|
+
return data.get("output", {})
|
|
158
|
+
|
|
159
|
+
def submit(
|
|
160
|
+
self,
|
|
161
|
+
component: str,
|
|
162
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
163
|
+
component_type: str = "function",
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Submit a component for async execution and return immediately.
|
|
166
|
+
|
|
167
|
+
This is a non-blocking call that returns a run ID immediately.
|
|
168
|
+
Use get_status() to check progress and get_result() to retrieve the output.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
component: Name of the component to execute
|
|
172
|
+
input_data: Input data for the component (will be sent as JSON body)
|
|
173
|
+
component_type: Type of component - "function", "workflow", "agent", "tool" (default: "function")
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
String containing the run ID
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
httpx.HTTPError: If the HTTP request fails
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
```python
|
|
183
|
+
# Submit async function (default)
|
|
184
|
+
run_id = client.submit("process_video", {"url": "https://..."})
|
|
185
|
+
print(f"Submitted: {run_id}")
|
|
186
|
+
|
|
187
|
+
# Submit workflow
|
|
188
|
+
run_id = client.submit("order_fulfillment", {"order_id": "123"}, component_type="workflow")
|
|
189
|
+
|
|
190
|
+
# Check status later
|
|
191
|
+
status = client.get_status(run_id)
|
|
192
|
+
if status["status"] == "completed":
|
|
193
|
+
result = client.get_result(run_id)
|
|
194
|
+
```
|
|
195
|
+
"""
|
|
196
|
+
if input_data is None:
|
|
197
|
+
input_data = {}
|
|
198
|
+
|
|
199
|
+
# Build URL with component type
|
|
200
|
+
url = urljoin(self.gateway_url + "/", f"v1/submit/{component_type}/{component}")
|
|
201
|
+
|
|
202
|
+
# Make request
|
|
203
|
+
response = self._client.post(
|
|
204
|
+
url,
|
|
205
|
+
json=input_data,
|
|
206
|
+
headers={"Content-Type": "application/json"},
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Handle errors
|
|
210
|
+
response.raise_for_status()
|
|
211
|
+
|
|
212
|
+
# Parse response and extract run ID
|
|
213
|
+
data = response.json()
|
|
214
|
+
return data.get("runId", "")
|
|
215
|
+
|
|
216
|
+
def get_status(self, run_id: str) -> Dict[str, Any]:
|
|
217
|
+
"""Get the current status of a run.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
run_id: The run ID returned from submit()
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Dictionary containing status information:
|
|
224
|
+
{
|
|
225
|
+
"runId": "...",
|
|
226
|
+
"status": "pending|running|completed|failed|cancelled",
|
|
227
|
+
"submittedAt": 1234567890,
|
|
228
|
+
"startedAt": 1234567891, // optional
|
|
229
|
+
"completedAt": 1234567892 // optional
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
httpx.HTTPError: If the HTTP request fails
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
```python
|
|
237
|
+
status = client.get_status(run_id)
|
|
238
|
+
print(f"Status: {status['status']}")
|
|
239
|
+
```
|
|
240
|
+
"""
|
|
241
|
+
url = urljoin(self.gateway_url + "/", f"v1/status/{run_id}")
|
|
242
|
+
|
|
243
|
+
response = self._client.get(url)
|
|
244
|
+
response.raise_for_status()
|
|
245
|
+
|
|
246
|
+
return response.json()
|
|
247
|
+
|
|
248
|
+
def get_result(self, run_id: str) -> Dict[str, Any]:
|
|
249
|
+
"""Get the result of a completed run.
|
|
250
|
+
|
|
251
|
+
This will raise an error if the run is not yet complete.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
run_id: The run ID returned from submit()
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Dictionary containing the component's output
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
RunError: If the run failed or is not yet complete
|
|
261
|
+
httpx.HTTPError: If the HTTP request fails
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
```python
|
|
265
|
+
try:
|
|
266
|
+
result = client.get_result(run_id)
|
|
267
|
+
print(result)
|
|
268
|
+
except RunError as e:
|
|
269
|
+
if "not complete" in str(e):
|
|
270
|
+
print("Run is still in progress")
|
|
271
|
+
else:
|
|
272
|
+
print(f"Run failed: {e}")
|
|
273
|
+
```
|
|
274
|
+
"""
|
|
275
|
+
url = urljoin(self.gateway_url + "/", f"v1/result/{run_id}")
|
|
276
|
+
|
|
277
|
+
response = self._client.get(url)
|
|
278
|
+
|
|
279
|
+
# Handle 404 - run not complete or not found
|
|
280
|
+
if response.status_code == 404:
|
|
281
|
+
error_data = response.json()
|
|
282
|
+
error_msg = error_data.get("error", "Run not found or not complete")
|
|
283
|
+
current_status = error_data.get("status", "unknown")
|
|
284
|
+
raise RunError(f"{error_msg} (status: {current_status})", run_id=run_id)
|
|
285
|
+
|
|
286
|
+
# Handle other errors
|
|
287
|
+
response.raise_for_status()
|
|
288
|
+
|
|
289
|
+
# Parse response
|
|
290
|
+
data = response.json()
|
|
291
|
+
|
|
292
|
+
# Check if run failed
|
|
293
|
+
if data.get("status") == "failed":
|
|
294
|
+
raise RunError(
|
|
295
|
+
data.get("error", "Unknown error"),
|
|
296
|
+
run_id=run_id,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Return output
|
|
300
|
+
return data.get("output", {})
|
|
301
|
+
|
|
302
|
+
def wait_for_result(
|
|
303
|
+
self,
|
|
304
|
+
run_id: str,
|
|
305
|
+
timeout: float = 300.0,
|
|
306
|
+
poll_interval: float = 1.0,
|
|
307
|
+
) -> Dict[str, Any]:
|
|
308
|
+
"""Wait for a run to complete and return the result.
|
|
309
|
+
|
|
310
|
+
This polls the status endpoint until the run completes or times out.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
run_id: The run ID returned from submit()
|
|
314
|
+
timeout: Maximum time to wait in seconds (default: 300)
|
|
315
|
+
poll_interval: How often to check status in seconds (default: 1.0)
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Dictionary containing the component's output
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
RunError: If the run fails or times out
|
|
322
|
+
httpx.HTTPError: If the HTTP request fails
|
|
323
|
+
|
|
324
|
+
Example:
|
|
325
|
+
```python
|
|
326
|
+
# Submit and wait for result
|
|
327
|
+
run_id = client.submit("long_task", {"data": "..."})
|
|
328
|
+
try:
|
|
329
|
+
result = client.wait_for_result(run_id, timeout=600)
|
|
330
|
+
print(result)
|
|
331
|
+
except RunError as e:
|
|
332
|
+
print(f"Failed: {e}")
|
|
333
|
+
```
|
|
334
|
+
"""
|
|
335
|
+
import time
|
|
336
|
+
|
|
337
|
+
start_time = time.time()
|
|
338
|
+
|
|
339
|
+
while True:
|
|
340
|
+
# Check timeout
|
|
341
|
+
elapsed = time.time() - start_time
|
|
342
|
+
if elapsed >= timeout:
|
|
343
|
+
raise RunError(
|
|
344
|
+
f"Timeout waiting for run to complete after {timeout}s",
|
|
345
|
+
run_id=run_id,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Get current status
|
|
349
|
+
status = self.get_status(run_id)
|
|
350
|
+
current_status = status.get("status", "")
|
|
351
|
+
|
|
352
|
+
# Check if complete
|
|
353
|
+
if current_status in ("completed", "failed", "cancelled"):
|
|
354
|
+
# Get result (will raise if failed)
|
|
355
|
+
return self.get_result(run_id)
|
|
356
|
+
|
|
357
|
+
# Wait before next poll
|
|
358
|
+
time.sleep(poll_interval)
|
|
359
|
+
|
|
360
|
+
def stream(
|
|
361
|
+
self,
|
|
362
|
+
component: str,
|
|
363
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
364
|
+
):
|
|
365
|
+
"""Stream responses from a component using Server-Sent Events (SSE).
|
|
366
|
+
|
|
367
|
+
This method yields chunks as they arrive from the component.
|
|
368
|
+
Perfect for LLM token streaming and incremental responses.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
component: Name of the component to execute
|
|
372
|
+
input_data: Input data for the component (will be sent as JSON body)
|
|
373
|
+
|
|
374
|
+
Yields:
|
|
375
|
+
String chunks as they arrive from the component
|
|
376
|
+
|
|
377
|
+
Raises:
|
|
378
|
+
RunError: If the component execution fails
|
|
379
|
+
httpx.HTTPError: If the HTTP request fails
|
|
380
|
+
|
|
381
|
+
Example:
|
|
382
|
+
```python
|
|
383
|
+
# Stream LLM tokens
|
|
384
|
+
for chunk in client.stream("generate_text", {"prompt": "Write a story"}):
|
|
385
|
+
print(chunk, end="", flush=True)
|
|
386
|
+
```
|
|
387
|
+
"""
|
|
388
|
+
if input_data is None:
|
|
389
|
+
input_data = {}
|
|
390
|
+
|
|
391
|
+
# Build URL
|
|
392
|
+
url = urljoin(self.gateway_url + "/", f"v1/stream/{component}")
|
|
393
|
+
|
|
394
|
+
# Use streaming request
|
|
395
|
+
with self._client.stream(
|
|
396
|
+
"POST",
|
|
397
|
+
url,
|
|
398
|
+
json=input_data,
|
|
399
|
+
headers={"Content-Type": "application/json"},
|
|
400
|
+
timeout=300.0, # 5 minute timeout for streaming
|
|
401
|
+
) as response:
|
|
402
|
+
# Check for errors
|
|
403
|
+
if response.status_code != 200:
|
|
404
|
+
# For streaming responses, we can't read the full text
|
|
405
|
+
# Just raise an HTTP error
|
|
406
|
+
raise RunError(
|
|
407
|
+
f"HTTP {response.status_code}: Streaming request failed",
|
|
408
|
+
run_id=None,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Parse SSE stream
|
|
412
|
+
for line in response.iter_lines():
|
|
413
|
+
line = line.strip()
|
|
414
|
+
|
|
415
|
+
# Skip empty lines and comments
|
|
416
|
+
if not line or line.startswith(":"):
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
# Parse SSE format: "data: {...}"
|
|
420
|
+
if line.startswith("data: "):
|
|
421
|
+
data_str = line[6:] # Remove "data: " prefix
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
data = json.loads(data_str)
|
|
425
|
+
|
|
426
|
+
# Check for completion
|
|
427
|
+
if data.get("done"):
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
# Check for error
|
|
431
|
+
if "error" in data:
|
|
432
|
+
raise RunError(
|
|
433
|
+
data.get("error"),
|
|
434
|
+
run_id=data.get("runId"),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Yield chunk
|
|
438
|
+
if "chunk" in data:
|
|
439
|
+
yield data["chunk"]
|
|
440
|
+
|
|
441
|
+
except json.JSONDecodeError:
|
|
442
|
+
# Skip malformed JSON
|
|
443
|
+
continue
|
|
444
|
+
|
|
445
|
+
def entity(self, entity_type: str, key: str) -> "EntityProxy":
|
|
446
|
+
"""Get a proxy for calling methods on a durable entity.
|
|
447
|
+
|
|
448
|
+
This provides a fluent API for entity method invocations with key-based routing.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
entity_type: The entity class name (e.g., "Counter", "ShoppingCart")
|
|
452
|
+
key: The entity instance key (e.g., "user-123", "cart-alice")
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
EntityProxy that allows method calls on the entity
|
|
456
|
+
|
|
457
|
+
Example:
|
|
458
|
+
```python
|
|
459
|
+
# Call entity method
|
|
460
|
+
result = client.entity("Counter", "user-123").increment(amount=5)
|
|
461
|
+
print(result) # 5
|
|
462
|
+
|
|
463
|
+
# Shopping cart
|
|
464
|
+
result = client.entity("ShoppingCart", "user-alice").add_item(
|
|
465
|
+
item_id="item-123",
|
|
466
|
+
quantity=2,
|
|
467
|
+
price=29.99
|
|
468
|
+
)
|
|
469
|
+
```
|
|
470
|
+
"""
|
|
471
|
+
return EntityProxy(self, entity_type, key)
|
|
472
|
+
|
|
473
|
+
def session(self, session_type: str, key: str) -> "SessionProxy":
|
|
474
|
+
"""Get a proxy for a session entity (OpenAI/ADK-style API).
|
|
475
|
+
|
|
476
|
+
This is a convenience wrapper around entity() specifically for SessionEntity subclasses,
|
|
477
|
+
providing a familiar API for developers coming from OpenAI Agents SDK or Google ADK.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
session_type: The session entity class name (e.g., "Conversation", "ChatSession")
|
|
481
|
+
key: The session instance key (typically user ID or session ID)
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
SessionProxy that provides session-specific methods
|
|
485
|
+
|
|
486
|
+
Example:
|
|
487
|
+
```python
|
|
488
|
+
# Create a conversation session
|
|
489
|
+
session = client.session("Conversation", "user-alice")
|
|
490
|
+
|
|
491
|
+
# Chat with the session
|
|
492
|
+
response = session.chat("Hello! How are you?")
|
|
493
|
+
print(response)
|
|
494
|
+
|
|
495
|
+
# Get conversation history
|
|
496
|
+
history = session.get_history()
|
|
497
|
+
for msg in history:
|
|
498
|
+
print(f"{msg['role']}: {msg['content']}")
|
|
499
|
+
```
|
|
500
|
+
"""
|
|
501
|
+
return SessionProxy(self, session_type, key)
|
|
502
|
+
|
|
503
|
+
def close(self):
|
|
504
|
+
"""Close the underlying HTTP client."""
|
|
505
|
+
self._client.close()
|
|
506
|
+
|
|
507
|
+
def __enter__(self):
|
|
508
|
+
"""Context manager entry."""
|
|
509
|
+
return self
|
|
510
|
+
|
|
511
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
512
|
+
"""Context manager exit."""
|
|
513
|
+
self.close()
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class EntityProxy:
|
|
517
|
+
"""Proxy for calling methods on a durable entity instance.
|
|
518
|
+
|
|
519
|
+
This class enables fluent method calls on entities using Python's
|
|
520
|
+
attribute access. Any method call is translated to an HTTP request
|
|
521
|
+
to /entity/:type/:key/:method.
|
|
522
|
+
|
|
523
|
+
Example:
|
|
524
|
+
```python
|
|
525
|
+
counter = client.entity("Counter", "user-123")
|
|
526
|
+
result = counter.increment(amount=5) # Calls /entity/Counter/user-123/increment
|
|
527
|
+
```
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
def __init__(self, client: "Client", entity_type: str, key: str):
|
|
531
|
+
"""Initialize entity proxy.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
client: The AGNT5 client instance
|
|
535
|
+
entity_type: The entity class name
|
|
536
|
+
key: The entity instance key
|
|
537
|
+
"""
|
|
538
|
+
self._client = client
|
|
539
|
+
self._entity_type = entity_type
|
|
540
|
+
self._key = key
|
|
541
|
+
|
|
542
|
+
def __getattr__(self, method_name: str):
|
|
543
|
+
"""Dynamic method lookup that creates entity method callers.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
method_name: The entity method to call
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Callable that executes the entity method
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
def method_caller(*args, **kwargs) -> Any:
|
|
553
|
+
"""Call an entity method with the given parameters.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
*args: Positional arguments (not recommended, use kwargs)
|
|
557
|
+
**kwargs: Method parameters as keyword arguments
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
The method's return value
|
|
561
|
+
|
|
562
|
+
Raises:
|
|
563
|
+
RunError: If the method execution fails
|
|
564
|
+
ValueError: If both positional and keyword arguments are provided
|
|
565
|
+
"""
|
|
566
|
+
# Convert positional args to kwargs if provided
|
|
567
|
+
if args and kwargs:
|
|
568
|
+
raise ValueError(
|
|
569
|
+
f"Cannot mix positional and keyword arguments when calling entity method '{method_name}'. "
|
|
570
|
+
"Please use keyword arguments only."
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# If positional args provided, we can't convert them without knowing parameter names
|
|
574
|
+
# Raise helpful error
|
|
575
|
+
if args:
|
|
576
|
+
raise ValueError(
|
|
577
|
+
f"Entity method '{method_name}' requires keyword arguments, but got {len(args)} positional arguments. "
|
|
578
|
+
f"Example: .{method_name}(param1=value1, param2=value2)"
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# Build URL: /v1/entity/:entityType/:key/:method
|
|
582
|
+
url = urljoin(
|
|
583
|
+
self._client.gateway_url + "/",
|
|
584
|
+
f"v1/entity/{self._entity_type}/{self._key}/{method_name}",
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Make request with method parameters as JSON body
|
|
588
|
+
response = self._client._client.post(
|
|
589
|
+
url,
|
|
590
|
+
json=kwargs,
|
|
591
|
+
headers={"Content-Type": "application/json"},
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# Handle errors
|
|
595
|
+
if response.status_code == 504:
|
|
596
|
+
error_data = response.json()
|
|
597
|
+
raise RunError(
|
|
598
|
+
"Execution timeout",
|
|
599
|
+
run_id=error_data.get("run_id"),
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
if response.status_code == 500:
|
|
603
|
+
try:
|
|
604
|
+
error_data = response.json()
|
|
605
|
+
raise RunError(
|
|
606
|
+
error_data.get("error", "Unknown error"),
|
|
607
|
+
run_id=error_data.get("run_id"),
|
|
608
|
+
)
|
|
609
|
+
except ValueError:
|
|
610
|
+
response.raise_for_status()
|
|
611
|
+
else:
|
|
612
|
+
response.raise_for_status()
|
|
613
|
+
|
|
614
|
+
# Parse response
|
|
615
|
+
data = response.json()
|
|
616
|
+
|
|
617
|
+
# Check execution status
|
|
618
|
+
if data.get("status") == "failed":
|
|
619
|
+
raise RunError(
|
|
620
|
+
data.get("error", "Unknown error"),
|
|
621
|
+
run_id=data.get("run_id"),
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# Return output
|
|
625
|
+
return data.get("output")
|
|
626
|
+
|
|
627
|
+
return method_caller
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class SessionProxy(EntityProxy):
|
|
631
|
+
"""Proxy for session entities with conversation-specific helper methods.
|
|
632
|
+
|
|
633
|
+
This extends EntityProxy to provide familiar APIs for session-based
|
|
634
|
+
conversations, similar to OpenAI Agents SDK and Google ADK.
|
|
635
|
+
|
|
636
|
+
Example:
|
|
637
|
+
```python
|
|
638
|
+
# Create a session
|
|
639
|
+
session = client.session("Conversation", "user-alice")
|
|
640
|
+
|
|
641
|
+
# Chat
|
|
642
|
+
response = session.chat("Tell me about AI")
|
|
643
|
+
|
|
644
|
+
# Get history
|
|
645
|
+
history = session.get_history()
|
|
646
|
+
```
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
def chat(self, message: str, **kwargs) -> str:
|
|
650
|
+
"""Send a message to the conversation session.
|
|
651
|
+
|
|
652
|
+
This is a convenience method that calls the `chat` method on the
|
|
653
|
+
underlying SessionEntity and returns just the response text.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
message: The user's message
|
|
657
|
+
**kwargs: Additional parameters to pass to the chat method
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
The assistant's response as a string
|
|
661
|
+
|
|
662
|
+
Example:
|
|
663
|
+
```python
|
|
664
|
+
response = session.chat("What is the weather today?")
|
|
665
|
+
print(response)
|
|
666
|
+
```
|
|
667
|
+
"""
|
|
668
|
+
# Call the chat method via the entity proxy
|
|
669
|
+
result = self.__getattr__("chat")(message=message, **kwargs)
|
|
670
|
+
|
|
671
|
+
# SessionEntity.chat() returns a dict with 'response' key
|
|
672
|
+
if isinstance(result, dict) and "response" in result:
|
|
673
|
+
return result["response"]
|
|
674
|
+
|
|
675
|
+
# If it's already a string, return as-is
|
|
676
|
+
return str(result)
|
|
677
|
+
|
|
678
|
+
def get_history(self) -> list:
|
|
679
|
+
"""Get the conversation history for this session.
|
|
680
|
+
|
|
681
|
+
Returns:
|
|
682
|
+
List of message dictionaries with 'role' and 'content' keys
|
|
683
|
+
|
|
684
|
+
Example:
|
|
685
|
+
```python
|
|
686
|
+
history = session.get_history()
|
|
687
|
+
for msg in history:
|
|
688
|
+
print(f"{msg['role']}: {msg['content']}")
|
|
689
|
+
```
|
|
690
|
+
"""
|
|
691
|
+
return self.__getattr__("get_history")()
|
|
692
|
+
|
|
693
|
+
def add_message(self, role: str, content: str) -> dict:
|
|
694
|
+
"""Add a message to the conversation history.
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
role: Message role ('user', 'assistant', or 'system')
|
|
698
|
+
content: Message content
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
Dictionary confirming the message was added
|
|
702
|
+
|
|
703
|
+
Example:
|
|
704
|
+
```python
|
|
705
|
+
session.add_message("system", "You are a helpful assistant")
|
|
706
|
+
session.add_message("user", "Hello!")
|
|
707
|
+
```
|
|
708
|
+
"""
|
|
709
|
+
return self.__getattr__("add_message")(role=role, content=content)
|
|
710
|
+
|
|
711
|
+
def clear_history(self) -> dict:
|
|
712
|
+
"""Clear the conversation history for this session.
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
Dictionary confirming the history was cleared
|
|
716
|
+
|
|
717
|
+
Example:
|
|
718
|
+
```python
|
|
719
|
+
session.clear_history()
|
|
720
|
+
```
|
|
721
|
+
"""
|
|
722
|
+
return self.__getattr__("clear_history")()
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class RunError(Exception):
|
|
726
|
+
"""Raised when a component run fails on AGNT5.
|
|
727
|
+
|
|
728
|
+
Attributes:
|
|
729
|
+
message: Error message describing what went wrong
|
|
730
|
+
run_id: The unique run ID associated with this execution (if available)
|
|
731
|
+
"""
|
|
732
|
+
|
|
733
|
+
def __init__(self, message: str, run_id: Optional[str] = None):
|
|
734
|
+
super().__init__(message)
|
|
735
|
+
self.run_id = run_id
|
|
736
|
+
self.message = message
|
|
737
|
+
|
|
738
|
+
def __str__(self):
|
|
739
|
+
if self.run_id:
|
|
740
|
+
return f"{self.message} (run_id: {self.run_id})"
|
|
741
|
+
return self.message
|