agent-builder-gateway-sdk 0.7.1__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.
- agent_builder_gateway_sdk-0.7.1.dist-info/METADATA +540 -0
- agent_builder_gateway_sdk-0.7.1.dist-info/RECORD +10 -0
- agent_builder_gateway_sdk-0.7.1.dist-info/WHEEL +4 -0
- agent_builder_gateway_sdk-0.7.1.dist-info/licenses/LICENSE +22 -0
- gateway_sdk/__init__.py +34 -0
- gateway_sdk/auth.py +49 -0
- gateway_sdk/client.py +726 -0
- gateway_sdk/exceptions.py +87 -0
- gateway_sdk/models.py +213 -0
- gateway_sdk/streaming.py +41 -0
gateway_sdk/client.py
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
"""Gateway 客户端(内部版本)
|
|
2
|
+
|
|
3
|
+
用于 Agent/Prefab 内部调用网关
|
|
4
|
+
|
|
5
|
+
架构说明:
|
|
6
|
+
- Agent 从请求头获取 X-Internal-Token(由网关传入)
|
|
7
|
+
- Prefab 从请求头获取 X-Internal-Token(由 Agent 传入)
|
|
8
|
+
- SDK 直接传递 internal token,不做任何转换
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from typing import Any, Dict, List, Optional, Union, Iterator
|
|
13
|
+
from .models import PrefabCall, PrefabResult, BatchResult, CallStatus, StreamEvent
|
|
14
|
+
from .streaming import parse_sse_stream
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
GatewayError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
PrefabNotFoundError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
QuotaExceededError,
|
|
21
|
+
ServiceUnavailableError,
|
|
22
|
+
MissingSecretError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Gateway 地址 (在构建时会根据环境动态替换)
|
|
27
|
+
DEFAULT_GATEWAY_URL = "http://agent-builder-gateway.sensedeal.vip"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GatewayClient:
|
|
31
|
+
"""Gateway SDK 主客户端(内部版本)"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
internal_token: Optional[str] = None,
|
|
36
|
+
base_url: str = DEFAULT_GATEWAY_URL,
|
|
37
|
+
timeout: int = 1200,
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
初始化客户端
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
internal_token: 内部 token(从请求头 X-Internal-Token 获取)
|
|
44
|
+
可选,如果不提供则使用白名单模式(适用于白名单环境)
|
|
45
|
+
base_url: Gateway 地址(默认使用测试环境)
|
|
46
|
+
timeout: 请求超时时间(秒),默认 1200 秒(20 分钟)
|
|
47
|
+
|
|
48
|
+
Note:
|
|
49
|
+
- Agent/Prefab 内部调用:传入 internal_token
|
|
50
|
+
- 白名单环境:无需传入 token,由 Gateway 基于 IP 白名单验证
|
|
51
|
+
- 外部用户请使用 from_api_key() 或外部端点 /v1/external/invoke_*
|
|
52
|
+
"""
|
|
53
|
+
self.base_url = base_url.rstrip("/")
|
|
54
|
+
self.internal_token = internal_token
|
|
55
|
+
self.timeout = timeout
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_api_key(cls, api_key: str, base_url: str = DEFAULT_GATEWAY_URL, timeout: int = 1200):
|
|
59
|
+
"""
|
|
60
|
+
从 API Key 创建客户端(第三方集成使用)
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
api_key: API Key(sk-xxx)
|
|
64
|
+
base_url: Gateway 地址
|
|
65
|
+
timeout: 请求超时时间(秒)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
GatewayClient 实例
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
AuthenticationError: API Key 无效
|
|
72
|
+
ServiceUnavailableError: 网络请求失败
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
```python
|
|
76
|
+
# 第三方集成
|
|
77
|
+
client = GatewayClient.from_api_key("sk-xxx")
|
|
78
|
+
|
|
79
|
+
# 上传输入文件
|
|
80
|
+
s3_url = client.upload_input_file("/tmp/video.mp4")
|
|
81
|
+
|
|
82
|
+
# 调用 Prefab
|
|
83
|
+
result = client.run("video-processor", "1.0.0", "extract_audio", files={"video": [s3_url]})
|
|
84
|
+
```
|
|
85
|
+
"""
|
|
86
|
+
from .exceptions import AuthenticationError, ServiceUnavailableError
|
|
87
|
+
|
|
88
|
+
url = f"{base_url.rstrip('/')}/v1/auth/convert_to_internal_token"
|
|
89
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
with httpx.Client(timeout=timeout) as http_client:
|
|
93
|
+
response = http_client.post(url, headers=headers)
|
|
94
|
+
|
|
95
|
+
if response.status_code == 401:
|
|
96
|
+
raise AuthenticationError("Invalid or expired API Key")
|
|
97
|
+
|
|
98
|
+
response.raise_for_status()
|
|
99
|
+
data = response.json()
|
|
100
|
+
internal_token = data["internal_token"]
|
|
101
|
+
|
|
102
|
+
return cls(internal_token=internal_token, base_url=base_url, timeout=timeout)
|
|
103
|
+
|
|
104
|
+
except httpx.TimeoutException:
|
|
105
|
+
raise ServiceUnavailableError("请求超时")
|
|
106
|
+
except httpx.RequestError as e:
|
|
107
|
+
raise ServiceUnavailableError(f"网络请求失败: {str(e)}")
|
|
108
|
+
|
|
109
|
+
def _build_headers(self, content_type: str = "application/json") -> Dict[str, str]:
|
|
110
|
+
"""
|
|
111
|
+
构建请求头(白名单模式支持)
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
content_type: 内容类型
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
请求头字典
|
|
118
|
+
"""
|
|
119
|
+
headers = {"Content-Type": content_type}
|
|
120
|
+
if self.internal_token:
|
|
121
|
+
headers["X-Internal-Token"] = self.internal_token
|
|
122
|
+
return headers
|
|
123
|
+
|
|
124
|
+
def _build_auth_headers(self) -> Dict[str, str]:
|
|
125
|
+
"""
|
|
126
|
+
构建认证请求头(仅包含认证信息,白名单模式支持)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
请求头字典
|
|
130
|
+
"""
|
|
131
|
+
headers = {}
|
|
132
|
+
if self.internal_token:
|
|
133
|
+
headers["X-Internal-Token"] = self.internal_token
|
|
134
|
+
return headers
|
|
135
|
+
|
|
136
|
+
def run(
|
|
137
|
+
self,
|
|
138
|
+
prefab_id: str,
|
|
139
|
+
version: str,
|
|
140
|
+
function_name: str,
|
|
141
|
+
parameters: Dict[str, Any],
|
|
142
|
+
files: Optional[Dict[str, List[str]]] = None,
|
|
143
|
+
stream: bool = False,
|
|
144
|
+
) -> Union[PrefabResult, Iterator[StreamEvent]]:
|
|
145
|
+
"""
|
|
146
|
+
执行单个预制件
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
prefab_id: 预制件 ID
|
|
150
|
+
version: 版本号
|
|
151
|
+
function_name: 函数名
|
|
152
|
+
parameters: 参数字典
|
|
153
|
+
files: 文件输入(可选)
|
|
154
|
+
stream: 是否流式返回
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
PrefabResult 或 StreamEvent 迭代器
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
AuthenticationError: 认证失败
|
|
161
|
+
PrefabNotFoundError: 预制件不存在
|
|
162
|
+
ValidationError: 参数验证失败
|
|
163
|
+
QuotaExceededError: 配额超限
|
|
164
|
+
ServiceUnavailableError: 服务不可用
|
|
165
|
+
MissingSecretError: 缺少必需的密钥
|
|
166
|
+
"""
|
|
167
|
+
call = PrefabCall(
|
|
168
|
+
prefab_id=prefab_id,
|
|
169
|
+
version=version,
|
|
170
|
+
function_name=function_name,
|
|
171
|
+
parameters=parameters,
|
|
172
|
+
files=files,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if stream:
|
|
176
|
+
return self._run_streaming(call)
|
|
177
|
+
else:
|
|
178
|
+
result = self.run_batch([call])
|
|
179
|
+
return result.results[0]
|
|
180
|
+
|
|
181
|
+
def run_batch(self, calls: List[PrefabCall]) -> BatchResult:
|
|
182
|
+
"""
|
|
183
|
+
批量执行预制件
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
calls: 预制件调用列表
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
BatchResult
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
同 run() 方法
|
|
193
|
+
"""
|
|
194
|
+
url = f"{self.base_url}/v1/internal/run"
|
|
195
|
+
headers = self._build_headers()
|
|
196
|
+
|
|
197
|
+
payload = {"calls": [call.to_dict() for call in calls]}
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
201
|
+
response = client.post(url, json=payload, headers=headers)
|
|
202
|
+
self._handle_error_response(response)
|
|
203
|
+
|
|
204
|
+
data = response.json()
|
|
205
|
+
results = [
|
|
206
|
+
PrefabResult(
|
|
207
|
+
status=CallStatus(r["status"]),
|
|
208
|
+
output=r.get("output"),
|
|
209
|
+
error=r.get("error"),
|
|
210
|
+
job_id=data.get("job_id"),
|
|
211
|
+
)
|
|
212
|
+
for r in data["results"]
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
return BatchResult(job_id=data["job_id"], status=data["status"], results=results)
|
|
216
|
+
|
|
217
|
+
except httpx.TimeoutException:
|
|
218
|
+
raise ServiceUnavailableError("请求超时")
|
|
219
|
+
except httpx.RequestError as e:
|
|
220
|
+
raise ServiceUnavailableError(f"网络请求失败: {str(e)}")
|
|
221
|
+
|
|
222
|
+
def _run_streaming(self, call: PrefabCall) -> Iterator[StreamEvent]:
|
|
223
|
+
"""
|
|
224
|
+
流式执行预制件
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
call: 预制件调用
|
|
228
|
+
|
|
229
|
+
Yields:
|
|
230
|
+
StreamEvent
|
|
231
|
+
"""
|
|
232
|
+
url = f"{self.base_url}/v1/internal/run"
|
|
233
|
+
headers = self._build_headers()
|
|
234
|
+
|
|
235
|
+
payload = {"calls": [call.to_dict()]}
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
239
|
+
with client.stream("POST", url, json=payload, headers=headers) as response:
|
|
240
|
+
self._handle_error_response(response)
|
|
241
|
+
|
|
242
|
+
# 解析 SSE 流
|
|
243
|
+
yield from parse_sse_stream(response.iter_bytes())
|
|
244
|
+
|
|
245
|
+
except httpx.TimeoutException:
|
|
246
|
+
raise ServiceUnavailableError("请求超时")
|
|
247
|
+
except httpx.RequestError as e:
|
|
248
|
+
raise ServiceUnavailableError(f"网络请求失败: {str(e)}")
|
|
249
|
+
|
|
250
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
251
|
+
"""
|
|
252
|
+
处理错误响应
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
response: HTTP 响应
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
对应的异常
|
|
259
|
+
"""
|
|
260
|
+
if response.status_code < 400:
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
error_data = response.json()
|
|
265
|
+
detail = error_data.get("detail", "Unknown error")
|
|
266
|
+
|
|
267
|
+
# 解析错误详情
|
|
268
|
+
if isinstance(detail, dict):
|
|
269
|
+
error_code = detail.get("error_code", "UNKNOWN_ERROR")
|
|
270
|
+
message = detail.get("message", str(detail))
|
|
271
|
+
else:
|
|
272
|
+
error_code = "UNKNOWN_ERROR"
|
|
273
|
+
message = str(detail)
|
|
274
|
+
|
|
275
|
+
except Exception:
|
|
276
|
+
error_code = "UNKNOWN_ERROR"
|
|
277
|
+
# 对于流式响应,需要先读取内容
|
|
278
|
+
try:
|
|
279
|
+
error_text = response.text
|
|
280
|
+
except Exception:
|
|
281
|
+
# 如果无法读取,先读取响应再获取文本
|
|
282
|
+
try:
|
|
283
|
+
response.read()
|
|
284
|
+
error_text = response.text
|
|
285
|
+
except Exception:
|
|
286
|
+
error_text = "Unable to read error response"
|
|
287
|
+
message = f"HTTP {response.status_code}: {error_text}"
|
|
288
|
+
|
|
289
|
+
# 根据状态码和错误码抛出对应异常
|
|
290
|
+
if response.status_code == 401 or response.status_code == 403:
|
|
291
|
+
raise AuthenticationError(message)
|
|
292
|
+
elif response.status_code == 404:
|
|
293
|
+
raise PrefabNotFoundError("unknown", "unknown", message)
|
|
294
|
+
elif response.status_code == 422:
|
|
295
|
+
raise ValidationError(message)
|
|
296
|
+
elif response.status_code == 429:
|
|
297
|
+
# 配额超限
|
|
298
|
+
if isinstance(detail, dict):
|
|
299
|
+
raise QuotaExceededError(
|
|
300
|
+
message,
|
|
301
|
+
limit=detail.get("limit", 0),
|
|
302
|
+
used=detail.get("used", 0),
|
|
303
|
+
quota_type=detail.get("quota_type", "unknown"),
|
|
304
|
+
)
|
|
305
|
+
else:
|
|
306
|
+
raise QuotaExceededError(message, 0, 0, "unknown")
|
|
307
|
+
elif response.status_code == 400 and error_code == "MISSING_SECRET":
|
|
308
|
+
# 缺少密钥
|
|
309
|
+
if isinstance(detail, dict):
|
|
310
|
+
raise MissingSecretError(
|
|
311
|
+
prefab_id=detail.get("prefab_id", "unknown"),
|
|
312
|
+
secret_name=detail.get("secret_name", "unknown"),
|
|
313
|
+
instructions=detail.get("instructions"),
|
|
314
|
+
)
|
|
315
|
+
else:
|
|
316
|
+
raise MissingSecretError("unknown", "unknown")
|
|
317
|
+
elif response.status_code == 400 and error_code == "MISSING_AGENT_CONTEXT":
|
|
318
|
+
# 缺少 Agent 上下文(文件操作需要)
|
|
319
|
+
from .exceptions import AgentContextRequiredError
|
|
320
|
+
raise AgentContextRequiredError(message)
|
|
321
|
+
elif response.status_code >= 500:
|
|
322
|
+
raise ServiceUnavailableError(message)
|
|
323
|
+
else:
|
|
324
|
+
raise GatewayError(message, {"error_code": error_code})
|
|
325
|
+
|
|
326
|
+
# ========== 文件操作 API(新增)==========
|
|
327
|
+
|
|
328
|
+
def upload_input_file(self, file_path: str, content_type: Optional[str] = None) -> str:
|
|
329
|
+
"""
|
|
330
|
+
上传输入文件(第三方集成使用)
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
file_path: 本地文件路径
|
|
334
|
+
content_type: 内容类型(可选,如 "video/mp4")
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
s3_url: S3 文件地址,可用于调用 Prefab
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
GatewayError: 上传失败
|
|
341
|
+
AgentContextRequiredError: 不会抛出(此方法不需要 agent context)
|
|
342
|
+
|
|
343
|
+
Example:
|
|
344
|
+
```python
|
|
345
|
+
client = GatewayClient.from_api_key("sk-xxx")
|
|
346
|
+
|
|
347
|
+
# 上传输入文件
|
|
348
|
+
s3_url = client.upload_input_file("/tmp/video.mp4", content_type="video/mp4")
|
|
349
|
+
|
|
350
|
+
# 调用 Prefab
|
|
351
|
+
result = client.run(
|
|
352
|
+
"video-processor",
|
|
353
|
+
"1.0.0",
|
|
354
|
+
"extract_audio",
|
|
355
|
+
files={"video": [s3_url]}
|
|
356
|
+
)
|
|
357
|
+
```
|
|
358
|
+
"""
|
|
359
|
+
import os
|
|
360
|
+
from pathlib import Path
|
|
361
|
+
|
|
362
|
+
if not os.path.exists(file_path):
|
|
363
|
+
raise ValueError(f"File not found: {file_path}")
|
|
364
|
+
|
|
365
|
+
filename = Path(file_path).name
|
|
366
|
+
|
|
367
|
+
# 1. 获取预签名上传 URL
|
|
368
|
+
url = f"{self.base_url}/internal/files/generate_upload_url"
|
|
369
|
+
headers = self._build_headers()
|
|
370
|
+
payload = {
|
|
371
|
+
"filename": filename,
|
|
372
|
+
"content_type": content_type
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
377
|
+
response = client.post(url, json=payload, headers=headers)
|
|
378
|
+
self._handle_error_response(response)
|
|
379
|
+
|
|
380
|
+
data = response.json()
|
|
381
|
+
upload_url = data["upload_url"]
|
|
382
|
+
s3_url = data["s3_url"]
|
|
383
|
+
|
|
384
|
+
# 2. 使用预签名 URL 上传到 S3
|
|
385
|
+
with open(file_path, "rb") as f:
|
|
386
|
+
file_data = f.read()
|
|
387
|
+
|
|
388
|
+
upload_headers = {}
|
|
389
|
+
if content_type:
|
|
390
|
+
upload_headers["Content-Type"] = content_type
|
|
391
|
+
|
|
392
|
+
with httpx.Client(timeout=self.timeout * 2) as client: # 上传超时时间加倍
|
|
393
|
+
upload_response = client.put(upload_url, content=file_data, headers=upload_headers)
|
|
394
|
+
upload_response.raise_for_status()
|
|
395
|
+
|
|
396
|
+
return s3_url
|
|
397
|
+
|
|
398
|
+
except httpx.TimeoutException:
|
|
399
|
+
raise ServiceUnavailableError("上传超时")
|
|
400
|
+
except httpx.RequestError as e:
|
|
401
|
+
raise ServiceUnavailableError(f"上传失败: {str(e)}")
|
|
402
|
+
|
|
403
|
+
def upload_file(self, file_path: str) -> Dict[str, Any]:
|
|
404
|
+
"""
|
|
405
|
+
上传永久文件到 agent-outputs
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
file_path: 本地文件路径
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
{
|
|
412
|
+
"success": bool,
|
|
413
|
+
"s3_url": str, # S3 地址
|
|
414
|
+
"filename": str,
|
|
415
|
+
"size": int
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
GatewayError: 上传失败
|
|
420
|
+
|
|
421
|
+
Example:
|
|
422
|
+
result = client.upload_file("/tmp/result.pdf")
|
|
423
|
+
s3_url = result["s3_url"] # s3://bucket/agent-outputs/{user_id}/{agent_id}/...
|
|
424
|
+
"""
|
|
425
|
+
import os
|
|
426
|
+
|
|
427
|
+
if not os.path.exists(file_path):
|
|
428
|
+
raise ValueError(f"File not found: {file_path}")
|
|
429
|
+
|
|
430
|
+
url = f"{self.base_url}/internal/files/upload"
|
|
431
|
+
headers = self._build_auth_headers()
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
with open(file_path, "rb") as f:
|
|
435
|
+
files = {"file": (os.path.basename(file_path), f)}
|
|
436
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
437
|
+
response = client.post(url, files=files, headers=headers)
|
|
438
|
+
self._handle_error_response(response)
|
|
439
|
+
return response.json()
|
|
440
|
+
|
|
441
|
+
except httpx.TimeoutException:
|
|
442
|
+
raise ServiceUnavailableError("上传超时")
|
|
443
|
+
except httpx.RequestError as e:
|
|
444
|
+
raise ServiceUnavailableError(f"上传失败: {str(e)}")
|
|
445
|
+
|
|
446
|
+
def upload_temp_file(
|
|
447
|
+
self,
|
|
448
|
+
file_path: str,
|
|
449
|
+
ttl: int = 86400,
|
|
450
|
+
session_id: Optional[str] = None
|
|
451
|
+
) -> Dict[str, Any]:
|
|
452
|
+
"""
|
|
453
|
+
上传临时文件到 agent-workspace(带 TTL)
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
file_path: 本地文件路径
|
|
457
|
+
ttl: 生存时间(秒),默认 86400(24 小时)
|
|
458
|
+
session_id: 会话 ID(可选),用于批量管理
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
{
|
|
462
|
+
"success": bool,
|
|
463
|
+
"s3_url": str,
|
|
464
|
+
"filename": str,
|
|
465
|
+
"size": int
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
Raises:
|
|
469
|
+
GatewayError: 上传失败
|
|
470
|
+
|
|
471
|
+
Example:
|
|
472
|
+
# 默认 24 小时后删除
|
|
473
|
+
result = client.upload_temp_file("/tmp/intermediate.jpg")
|
|
474
|
+
|
|
475
|
+
# 1 小时后删除
|
|
476
|
+
result = client.upload_temp_file("/tmp/temp.dat", ttl=3600)
|
|
477
|
+
|
|
478
|
+
# 关联到 session
|
|
479
|
+
session_id = str(uuid.uuid4())
|
|
480
|
+
result = client.upload_temp_file("/tmp/temp.jpg", session_id=session_id)
|
|
481
|
+
"""
|
|
482
|
+
import os
|
|
483
|
+
|
|
484
|
+
if not os.path.exists(file_path):
|
|
485
|
+
raise ValueError(f"File not found: {file_path}")
|
|
486
|
+
|
|
487
|
+
url = f"{self.base_url}/internal/files/upload_temp"
|
|
488
|
+
headers = self._build_auth_headers()
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
with open(file_path, "rb") as f:
|
|
492
|
+
files = {"file": (os.path.basename(file_path), f)}
|
|
493
|
+
data = {"ttl": ttl}
|
|
494
|
+
if session_id:
|
|
495
|
+
data["session_id"] = session_id
|
|
496
|
+
|
|
497
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
498
|
+
response = client.post(url, files=files, data=data, headers=headers)
|
|
499
|
+
self._handle_error_response(response)
|
|
500
|
+
return response.json()
|
|
501
|
+
|
|
502
|
+
except httpx.TimeoutException:
|
|
503
|
+
raise ServiceUnavailableError("上传超时")
|
|
504
|
+
except httpx.RequestError as e:
|
|
505
|
+
raise ServiceUnavailableError(f"上传失败: {str(e)}")
|
|
506
|
+
|
|
507
|
+
def download_file(
|
|
508
|
+
self,
|
|
509
|
+
s3_url: str,
|
|
510
|
+
local_path: str,
|
|
511
|
+
mode: str = "presigned"
|
|
512
|
+
) -> None:
|
|
513
|
+
"""
|
|
514
|
+
下载文件
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
s3_url: S3 文件 URL
|
|
518
|
+
local_path: 本地保存路径
|
|
519
|
+
mode: 下载模式("presigned" 推荐,"stream" 暂不支持)
|
|
520
|
+
|
|
521
|
+
Raises:
|
|
522
|
+
GatewayError: 下载失败
|
|
523
|
+
|
|
524
|
+
Example:
|
|
525
|
+
client.download_file("s3://bucket/agent-outputs/...", "/tmp/result.pdf")
|
|
526
|
+
"""
|
|
527
|
+
import os
|
|
528
|
+
|
|
529
|
+
if mode != "presigned":
|
|
530
|
+
raise ValueError("目前仅支持 presigned 模式")
|
|
531
|
+
|
|
532
|
+
# 获取预签名 URL
|
|
533
|
+
presigned_url = self.get_presigned_url(s3_url)
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
# 直接从 S3 下载
|
|
537
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
538
|
+
response = client.get(presigned_url)
|
|
539
|
+
response.raise_for_status()
|
|
540
|
+
|
|
541
|
+
# 保存到本地
|
|
542
|
+
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
543
|
+
with open(local_path, "wb") as f:
|
|
544
|
+
f.write(response.content)
|
|
545
|
+
|
|
546
|
+
except httpx.TimeoutException:
|
|
547
|
+
raise ServiceUnavailableError("下载超时")
|
|
548
|
+
except httpx.RequestError as e:
|
|
549
|
+
raise ServiceUnavailableError(f"下载失败: {str(e)}")
|
|
550
|
+
|
|
551
|
+
def get_presigned_url(
|
|
552
|
+
self,
|
|
553
|
+
s3_url: str,
|
|
554
|
+
expires_in: int = 3600
|
|
555
|
+
) -> str:
|
|
556
|
+
"""
|
|
557
|
+
获取预签名 URL(用于直接下载)
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
s3_url: S3 文件 URL
|
|
561
|
+
expires_in: 有效期(秒),默认 3600(1 小时)
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
预签名 URL(HTTPS)
|
|
565
|
+
|
|
566
|
+
Raises:
|
|
567
|
+
GatewayError: 获取失败
|
|
568
|
+
|
|
569
|
+
Example:
|
|
570
|
+
url = client.get_presigned_url("s3://bucket/agent-outputs/...")
|
|
571
|
+
# 可以直接用浏览器访问这个 URL 下载文件
|
|
572
|
+
"""
|
|
573
|
+
url = f"{self.base_url}/internal/files/download"
|
|
574
|
+
headers = self._build_auth_headers()
|
|
575
|
+
params = {
|
|
576
|
+
"s3_url": s3_url,
|
|
577
|
+
"mode": "presigned",
|
|
578
|
+
"expires_in": expires_in
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
try:
|
|
582
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
583
|
+
response = client.get(url, params=params, headers=headers)
|
|
584
|
+
self._handle_error_response(response)
|
|
585
|
+
data = response.json()
|
|
586
|
+
return data["presigned_url"]
|
|
587
|
+
|
|
588
|
+
except httpx.TimeoutException:
|
|
589
|
+
raise ServiceUnavailableError("请求超时")
|
|
590
|
+
except httpx.RequestError as e:
|
|
591
|
+
raise ServiceUnavailableError(f"请求失败: {str(e)}")
|
|
592
|
+
|
|
593
|
+
def list_files(
|
|
594
|
+
self,
|
|
595
|
+
limit: int = 100,
|
|
596
|
+
continuation_token: Optional[str] = None
|
|
597
|
+
) -> Dict[str, Any]:
|
|
598
|
+
"""
|
|
599
|
+
列出永久文件(agent-outputs)
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
limit: 最大返回数量,默认 100
|
|
603
|
+
continuation_token: 分页 token(可选)
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
{
|
|
607
|
+
"files": [
|
|
608
|
+
{
|
|
609
|
+
"key": str,
|
|
610
|
+
"size": int,
|
|
611
|
+
"last_modified": str,
|
|
612
|
+
"s3_url": str
|
|
613
|
+
}
|
|
614
|
+
],
|
|
615
|
+
"next_token": str (optional)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
Raises:
|
|
619
|
+
GatewayError: 获取失败
|
|
620
|
+
|
|
621
|
+
Example:
|
|
622
|
+
result = client.list_files(limit=50)
|
|
623
|
+
for file in result["files"]:
|
|
624
|
+
print(file["s3_url"])
|
|
625
|
+
|
|
626
|
+
# 翻页
|
|
627
|
+
if "next_token" in result:
|
|
628
|
+
next_page = client.list_files(limit=50, continuation_token=result["next_token"])
|
|
629
|
+
"""
|
|
630
|
+
url = f"{self.base_url}/internal/files/list"
|
|
631
|
+
headers = self._build_auth_headers()
|
|
632
|
+
params = {"limit": limit}
|
|
633
|
+
if continuation_token:
|
|
634
|
+
params["continuation_token"] = continuation_token
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
638
|
+
response = client.get(url, params=params, headers=headers)
|
|
639
|
+
self._handle_error_response(response)
|
|
640
|
+
return response.json()
|
|
641
|
+
|
|
642
|
+
except httpx.TimeoutException:
|
|
643
|
+
raise ServiceUnavailableError("请求超时")
|
|
644
|
+
except httpx.RequestError as e:
|
|
645
|
+
raise ServiceUnavailableError(f"请求失败: {str(e)}")
|
|
646
|
+
|
|
647
|
+
def list_temp_files(
|
|
648
|
+
self,
|
|
649
|
+
session_id: Optional[str] = None,
|
|
650
|
+
limit: int = 100,
|
|
651
|
+
continuation_token: Optional[str] = None
|
|
652
|
+
) -> Dict[str, Any]:
|
|
653
|
+
"""
|
|
654
|
+
列出临时文件(agent-workspace)
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
session_id: 会话 ID(可选),不指定则列出所有临时文件
|
|
658
|
+
limit: 最大返回数量,默认 100
|
|
659
|
+
continuation_token: 分页 token(可选)
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
同 list_files()
|
|
663
|
+
|
|
664
|
+
Raises:
|
|
665
|
+
GatewayError: 获取失败
|
|
666
|
+
|
|
667
|
+
Example:
|
|
668
|
+
# 列出所有临时文件
|
|
669
|
+
result = client.list_temp_files()
|
|
670
|
+
|
|
671
|
+
# 列出指定 session 的临时文件
|
|
672
|
+
result = client.list_temp_files(session_id="abc123")
|
|
673
|
+
"""
|
|
674
|
+
url = f"{self.base_url}/internal/files/list_temp"
|
|
675
|
+
headers = self._build_auth_headers()
|
|
676
|
+
params = {"limit": limit}
|
|
677
|
+
if session_id:
|
|
678
|
+
params["session_id"] = session_id
|
|
679
|
+
if continuation_token:
|
|
680
|
+
params["continuation_token"] = continuation_token
|
|
681
|
+
|
|
682
|
+
try:
|
|
683
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
684
|
+
response = client.get(url, params=params, headers=headers)
|
|
685
|
+
self._handle_error_response(response)
|
|
686
|
+
return response.json()
|
|
687
|
+
|
|
688
|
+
except httpx.TimeoutException:
|
|
689
|
+
raise ServiceUnavailableError("请求超时")
|
|
690
|
+
except httpx.RequestError as e:
|
|
691
|
+
raise ServiceUnavailableError(f"请求失败: {str(e)}")
|
|
692
|
+
|
|
693
|
+
def cleanup_temp_files(self, session_id: str) -> int:
|
|
694
|
+
"""
|
|
695
|
+
立即清理指定 session 的所有临时文件
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
session_id: 会话 ID
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
删除的文件数量
|
|
702
|
+
|
|
703
|
+
Raises:
|
|
704
|
+
GatewayError: 清理失败
|
|
705
|
+
|
|
706
|
+
Example:
|
|
707
|
+
# Agent 任务完成后立即清理
|
|
708
|
+
count = client.cleanup_temp_files(session_id="abc123")
|
|
709
|
+
print(f"Cleaned up {count} temporary files")
|
|
710
|
+
"""
|
|
711
|
+
url = f"{self.base_url}/internal/files/cleanup_temp"
|
|
712
|
+
headers = self._build_auth_headers()
|
|
713
|
+
params = {"session_id": session_id}
|
|
714
|
+
|
|
715
|
+
try:
|
|
716
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
717
|
+
response = client.delete(url, params=params, headers=headers)
|
|
718
|
+
self._handle_error_response(response)
|
|
719
|
+
data = response.json()
|
|
720
|
+
return data["deleted_count"]
|
|
721
|
+
|
|
722
|
+
except httpx.TimeoutException:
|
|
723
|
+
raise ServiceUnavailableError("请求超时")
|
|
724
|
+
except httpx.RequestError as e:
|
|
725
|
+
raise ServiceUnavailableError(f"请求失败: {str(e)}")
|
|
726
|
+
|