agent-builder-gateway-sdk 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of agent-builder-gateway-sdk might be problematic. Click here for more details.
- agent_builder_gateway_sdk-0.2.0.dist-info/METADATA +288 -0
- agent_builder_gateway_sdk-0.2.0.dist-info/RECORD +10 -0
- agent_builder_gateway_sdk-0.2.0.dist-info/WHEEL +4 -0
- agent_builder_gateway_sdk-0.2.0.dist-info/licenses/LICENSE +22 -0
- gateway_sdk/__init__.py +32 -0
- gateway_sdk/auth.py +49 -0
- gateway_sdk/client.py +236 -0
- gateway_sdk/exceptions.py +72 -0
- gateway_sdk/models.py +114 -0
- gateway_sdk/streaming.py +41 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-builder-gateway-sdk
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for Agent Builder Gateway - 用于 AI 构建的程序调用预制件
|
|
5
|
+
Author: Agent Builder Team
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: black>=24.0.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Gateway SDK
|
|
22
|
+
|
|
23
|
+
Python SDK for Gateway - 用于调用预制件
|
|
24
|
+
|
|
25
|
+
## 概述
|
|
26
|
+
|
|
27
|
+
Gateway SDK 是一个用于调用预制件的 Python SDK。
|
|
28
|
+
|
|
29
|
+
### 核心特性
|
|
30
|
+
|
|
31
|
+
- ✅ 简洁的 API
|
|
32
|
+
- ✅ 支持 JWT Token 和 API Key 认证
|
|
33
|
+
- ✅ 流式响应支持(SSE)
|
|
34
|
+
- ✅ 完整的类型提示
|
|
35
|
+
- ✅ 完善的错误处理
|
|
36
|
+
|
|
37
|
+
## 安装
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install agent-builder-gateway-sdk
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 快速开始
|
|
44
|
+
|
|
45
|
+
### 初始化客户端
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from gateway_sdk import GatewayClient
|
|
49
|
+
|
|
50
|
+
# 使用 JWT Token
|
|
51
|
+
client = GatewayClient(jwt_token="your-jwt-token")
|
|
52
|
+
|
|
53
|
+
# 或使用 API Key
|
|
54
|
+
client = GatewayClient(api_key="sk-xxx")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 调用预制件
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
result = client.run(
|
|
61
|
+
prefab_id="llm-client",
|
|
62
|
+
version="1.0.0",
|
|
63
|
+
function_name="chat",
|
|
64
|
+
parameters={"messages": [{"role": "user", "content": "Hello"}]}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if result.is_success():
|
|
68
|
+
print(result.get_result())
|
|
69
|
+
else:
|
|
70
|
+
print(f"Error: {result.error}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 链式调用
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
llm = client.prefab("llm-client", "1.0.0")
|
|
77
|
+
result = llm.call("chat", messages=[...], model="gpt-4")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 流式响应
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
for event in client.run(..., stream=True):
|
|
84
|
+
if event.type == "content":
|
|
85
|
+
print(event.data, end="", flush=True)
|
|
86
|
+
elif event.type == "done":
|
|
87
|
+
print("\n完成")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 批量调用
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from gateway_sdk import PrefabCall
|
|
94
|
+
|
|
95
|
+
calls = [
|
|
96
|
+
PrefabCall(
|
|
97
|
+
prefab_id="translator",
|
|
98
|
+
version="1.0.0",
|
|
99
|
+
function_name="translate",
|
|
100
|
+
parameters={"text": "Hello", "target": "zh"}
|
|
101
|
+
),
|
|
102
|
+
PrefabCall(
|
|
103
|
+
prefab_id="translator",
|
|
104
|
+
version="1.0.0",
|
|
105
|
+
function_name="translate",
|
|
106
|
+
parameters={"text": "World", "target": "zh"}
|
|
107
|
+
)
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
result = client.run_batch(calls)
|
|
111
|
+
for r in result.results:
|
|
112
|
+
if r.is_success():
|
|
113
|
+
print(r.get_result())
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 文件处理
|
|
117
|
+
|
|
118
|
+
**重要**: SDK 只接收 S3 URL,不负责文件上传/下载。
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# 传递 S3 URL 作为文件输入
|
|
122
|
+
result = client.run(
|
|
123
|
+
prefab_id="video-processor",
|
|
124
|
+
version="1.0.0",
|
|
125
|
+
function_name="extract_audio",
|
|
126
|
+
parameters={"format": "mp3"},
|
|
127
|
+
files={"video": ["s3://bucket/input.mp4"]}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# 输出文件也是 S3 URL
|
|
131
|
+
output_files = result.get_files()
|
|
132
|
+
# {"audio": ["s3://bucket/output.mp3"]}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**文件处理流程**:
|
|
136
|
+
1. 📤 使用 S3 客户端上传文件,获取 S3 URL
|
|
137
|
+
2. 📝 将 S3 URL 传递给 SDK
|
|
138
|
+
3. 📥 从返回的 S3 URL 下载结果文件
|
|
139
|
+
|
|
140
|
+
## API 参考
|
|
141
|
+
|
|
142
|
+
### GatewayClient
|
|
143
|
+
|
|
144
|
+
#### 初始化
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
GatewayClient(
|
|
148
|
+
base_url: str = "http://nodeport.sensedeal.vip:30566",
|
|
149
|
+
api_key: Optional[str] = None,
|
|
150
|
+
jwt_token: Optional[str] = None,
|
|
151
|
+
timeout: int = 60
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**参数**:
|
|
156
|
+
- `api_key`: API Key
|
|
157
|
+
- `jwt_token`: JWT Token
|
|
158
|
+
- `timeout`: 请求超时时间(秒)
|
|
159
|
+
|
|
160
|
+
**注意**:必须提供 `api_key` 或 `jwt_token` 之一。
|
|
161
|
+
|
|
162
|
+
#### 方法
|
|
163
|
+
|
|
164
|
+
**run()** - 执行单个预制件
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
run(
|
|
168
|
+
prefab_id: str,
|
|
169
|
+
version: str,
|
|
170
|
+
function_name: str,
|
|
171
|
+
parameters: Dict[str, Any],
|
|
172
|
+
files: Optional[Dict[str, List[str]]] = None, # 仅接受 S3 URL
|
|
173
|
+
stream: bool = False
|
|
174
|
+
) -> Union[PrefabResult, Iterator[StreamEvent]]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
参数:
|
|
178
|
+
- `files`: 文件输入,格式为 `{"参数名": ["s3://url1", "s3://url2"]}`,**仅接受 S3 URL**
|
|
179
|
+
|
|
180
|
+
**run_batch()** - 批量执行
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
run_batch(calls: List[PrefabCall]) -> BatchResult
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**prefab()** - 获取预制件对象
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
prefab(prefab_id: str, version: str) -> Prefab
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**list_prefabs()** - 列出预制件
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
list_prefabs(status: Optional[str] = None) -> List[PrefabInfo]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**get_prefab_spec()** - 获取预制件规格
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
get_prefab_spec(prefab_id: str, version: Optional[str] = None) -> Dict[str, Any]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### PrefabResult
|
|
205
|
+
|
|
206
|
+
预制件执行结果。
|
|
207
|
+
|
|
208
|
+
**属性**:
|
|
209
|
+
- `status`: 调用状态(SUCCESS / FAILED)
|
|
210
|
+
- `output`: 输出数据
|
|
211
|
+
- `error`: 错误信息
|
|
212
|
+
- `job_id`: 任务 ID
|
|
213
|
+
|
|
214
|
+
**方法**:
|
|
215
|
+
- `is_success()`: 判断是否成功
|
|
216
|
+
- `get(key, default)`: 获取输出字段
|
|
217
|
+
- `get_result()`: 获取业务结果
|
|
218
|
+
- `get_files()`: 获取输出文件
|
|
219
|
+
|
|
220
|
+
### StreamEvent
|
|
221
|
+
|
|
222
|
+
流式事件。
|
|
223
|
+
|
|
224
|
+
**属性**:
|
|
225
|
+
- `type`: 事件类型(start / content / progress / done / error)
|
|
226
|
+
- `data`: 事件数据
|
|
227
|
+
|
|
228
|
+
## 错误处理
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from gateway_sdk.exceptions import (
|
|
232
|
+
GatewayError,
|
|
233
|
+
AuthenticationError,
|
|
234
|
+
PrefabNotFoundError,
|
|
235
|
+
ValidationError,
|
|
236
|
+
QuotaExceededError,
|
|
237
|
+
ServiceUnavailableError,
|
|
238
|
+
MissingSecretError,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
result = client.run(...)
|
|
243
|
+
except AuthenticationError as e:
|
|
244
|
+
print(f"认证失败: {e}")
|
|
245
|
+
except PrefabNotFoundError as e:
|
|
246
|
+
print(f"预制件不存在: {e}")
|
|
247
|
+
except MissingSecretError as e:
|
|
248
|
+
print(f"缺少密钥: {e.secret_name}")
|
|
249
|
+
except QuotaExceededError as e:
|
|
250
|
+
print(f"配额超限: {e.used}/{e.limit}")
|
|
251
|
+
except GatewayError as e:
|
|
252
|
+
print(f"错误: {e}")
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## 示例代码
|
|
256
|
+
|
|
257
|
+
- `examples/basic_usage.py` - 基础用法
|
|
258
|
+
- `examples/streaming.py` - 流式响应
|
|
259
|
+
|
|
260
|
+
## 常见问题
|
|
261
|
+
|
|
262
|
+
**Q: 如何处理超时?**
|
|
263
|
+
|
|
264
|
+
A: 设置 `timeout` 参数:
|
|
265
|
+
```python
|
|
266
|
+
client = GatewayClient(jwt_token="...", timeout=120)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Q: 如何调试?**
|
|
270
|
+
|
|
271
|
+
A: 启用日志:
|
|
272
|
+
```python
|
|
273
|
+
import logging
|
|
274
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Q: 如何停止流式响应?**
|
|
278
|
+
|
|
279
|
+
A: 使用 `break` 跳出循环:
|
|
280
|
+
```python
|
|
281
|
+
for event in client.run(..., stream=True):
|
|
282
|
+
if some_condition:
|
|
283
|
+
break
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## 许可证
|
|
287
|
+
|
|
288
|
+
MIT License
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
gateway_sdk/__init__.py,sha256=lgBWm1gkpX8nGx-weoJbpQeHRYygJu5XBhB-ienHq5I,655
|
|
2
|
+
gateway_sdk/auth.py,sha256=Nx0p9FYZHBoQeN8SO2nn7MKH2y02v3UR3M8bYqhQG0w,1165
|
|
3
|
+
gateway_sdk/client.py,sha256=GZh2SyQ_598ex1LebSy6ajN9F3APckn7O1u0TrpmpsU,7708
|
|
4
|
+
gateway_sdk/exceptions.py,sha256=fIrhKC8vIaocXvyK56OEKPGHn8drvGh6nElnyR0Z71A,1897
|
|
5
|
+
gateway_sdk/models.py,sha256=_3MspU7qDNTsCppzWxgr-sNhjdVzu6QVVZHpqw4lVMs,2731
|
|
6
|
+
gateway_sdk/streaming.py,sha256=nwRWxKP5bU8uIMIv_82DyFws9djnEKdAxW6S4L9KtPI,1092
|
|
7
|
+
agent_builder_gateway_sdk-0.2.0.dist-info/METADATA,sha256=tBJr9Lfbo28xXzpyxeTnXRMHqmCR6-VtY7Kx4rgZr8k,5966
|
|
8
|
+
agent_builder_gateway_sdk-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
agent_builder_gateway_sdk-0.2.0.dist-info/licenses/LICENSE,sha256=C3JXUJe4vPtoVe1lp7O4WeGtH71JWYrRaCufEoJ84K0,1076
|
|
10
|
+
agent_builder_gateway_sdk-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Agent Builder Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
gateway_sdk/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Builder Gateway SDK
|
|
3
|
+
用于 AI 构建的程序调用预制件的 Python SDK
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .client import GatewayClient
|
|
7
|
+
from .models import PrefabCall, PrefabResult, BatchResult, StreamEvent
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
GatewayError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
PrefabNotFoundError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
QuotaExceededError,
|
|
14
|
+
ServiceUnavailableError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.2.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"GatewayClient",
|
|
21
|
+
"PrefabCall",
|
|
22
|
+
"PrefabResult",
|
|
23
|
+
"BatchResult",
|
|
24
|
+
"StreamEvent",
|
|
25
|
+
"GatewayError",
|
|
26
|
+
"AuthenticationError",
|
|
27
|
+
"PrefabNotFoundError",
|
|
28
|
+
"ValidationError",
|
|
29
|
+
"QuotaExceededError",
|
|
30
|
+
"ServiceUnavailableError",
|
|
31
|
+
]
|
|
32
|
+
|
gateway_sdk/auth.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""认证管理"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AuthManager:
|
|
7
|
+
"""认证管理器"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, api_key: Optional[str] = None, jwt_token: Optional[str] = None):
|
|
10
|
+
"""
|
|
11
|
+
初始化认证管理器
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
api_key: API Key(优先级高)
|
|
15
|
+
jwt_token: JWT Token
|
|
16
|
+
"""
|
|
17
|
+
self.api_key = api_key
|
|
18
|
+
self.jwt_token = jwt_token
|
|
19
|
+
|
|
20
|
+
def get_headers(self) -> dict[str, str]:
|
|
21
|
+
"""
|
|
22
|
+
获取认证请求头
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
认证请求头字典
|
|
26
|
+
"""
|
|
27
|
+
headers: dict[str, str] = {}
|
|
28
|
+
|
|
29
|
+
# API Key 优先
|
|
30
|
+
if self.api_key:
|
|
31
|
+
headers["X-API-Key"] = self.api_key
|
|
32
|
+
elif self.jwt_token:
|
|
33
|
+
# 确保 Token 格式正确
|
|
34
|
+
token = self.jwt_token
|
|
35
|
+
if not token.startswith("Bearer "):
|
|
36
|
+
token = f"Bearer {token}"
|
|
37
|
+
headers["Authorization"] = token
|
|
38
|
+
|
|
39
|
+
return headers
|
|
40
|
+
|
|
41
|
+
def is_authenticated(self) -> bool:
|
|
42
|
+
"""
|
|
43
|
+
判断是否已配置认证信息
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
是否已配置认证
|
|
47
|
+
"""
|
|
48
|
+
return bool(self.api_key or self.jwt_token)
|
|
49
|
+
|
gateway_sdk/client.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Gateway 客户端"""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union, Iterator
|
|
5
|
+
from .auth import AuthManager
|
|
6
|
+
from .models import PrefabCall, PrefabResult, BatchResult, CallStatus, StreamEvent
|
|
7
|
+
from .streaming import parse_sse_stream
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
GatewayError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
PrefabNotFoundError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
QuotaExceededError,
|
|
14
|
+
ServiceUnavailableError,
|
|
15
|
+
MissingSecretError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Gateway 地址
|
|
20
|
+
DEFAULT_GATEWAY_URL = "http://nodeport.sensedeal.vip:30566"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GatewayClient:
|
|
24
|
+
"""Gateway SDK 主客户端"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
base_url: str = DEFAULT_GATEWAY_URL,
|
|
29
|
+
api_key: Optional[str] = None,
|
|
30
|
+
jwt_token: Optional[str] = None,
|
|
31
|
+
timeout: int = 60,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
初始化客户端
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
base_url: Gateway 地址(默认使用写死的地址)
|
|
38
|
+
api_key: API Key(优先级高)
|
|
39
|
+
jwt_token: JWT Token(用于 AI 构建的程序)
|
|
40
|
+
timeout: 请求超时时间(秒)
|
|
41
|
+
"""
|
|
42
|
+
self.base_url = base_url.rstrip("/")
|
|
43
|
+
self.auth = AuthManager(api_key=api_key, jwt_token=jwt_token)
|
|
44
|
+
self.timeout = timeout
|
|
45
|
+
|
|
46
|
+
if not self.auth.is_authenticated():
|
|
47
|
+
raise AuthenticationError("必须提供 api_key 或 jwt_token")
|
|
48
|
+
|
|
49
|
+
def run(
|
|
50
|
+
self,
|
|
51
|
+
prefab_id: str,
|
|
52
|
+
version: str,
|
|
53
|
+
function_name: str,
|
|
54
|
+
parameters: Dict[str, Any],
|
|
55
|
+
files: Optional[Dict[str, List[str]]] = None,
|
|
56
|
+
stream: bool = False,
|
|
57
|
+
) -> Union[PrefabResult, Iterator[StreamEvent]]:
|
|
58
|
+
"""
|
|
59
|
+
执行单个预制件
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
prefab_id: 预制件 ID
|
|
63
|
+
version: 版本号
|
|
64
|
+
function_name: 函数名
|
|
65
|
+
parameters: 参数字典
|
|
66
|
+
files: 文件输入(可选)
|
|
67
|
+
stream: 是否流式返回
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
PrefabResult 或 StreamEvent 迭代器
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
AuthenticationError: 认证失败
|
|
74
|
+
PrefabNotFoundError: 预制件不存在
|
|
75
|
+
ValidationError: 参数验证失败
|
|
76
|
+
QuotaExceededError: 配额超限
|
|
77
|
+
ServiceUnavailableError: 服务不可用
|
|
78
|
+
MissingSecretError: 缺少必需的密钥
|
|
79
|
+
"""
|
|
80
|
+
call = PrefabCall(
|
|
81
|
+
prefab_id=prefab_id,
|
|
82
|
+
version=version,
|
|
83
|
+
function_name=function_name,
|
|
84
|
+
parameters=parameters,
|
|
85
|
+
files=files,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if stream:
|
|
89
|
+
return self._run_streaming(call)
|
|
90
|
+
else:
|
|
91
|
+
result = self.run_batch([call])
|
|
92
|
+
return result.results[0]
|
|
93
|
+
|
|
94
|
+
def run_batch(self, calls: List[PrefabCall]) -> BatchResult:
|
|
95
|
+
"""
|
|
96
|
+
批量执行预制件
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
calls: 预制件调用列表
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
BatchResult
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
同 run() 方法
|
|
106
|
+
"""
|
|
107
|
+
url = f"{self.base_url}/v1/run"
|
|
108
|
+
headers = self.auth.get_headers()
|
|
109
|
+
headers["Content-Type"] = "application/json"
|
|
110
|
+
|
|
111
|
+
payload = {"calls": [call.to_dict() for call in calls]}
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
115
|
+
response = client.post(url, json=payload, headers=headers)
|
|
116
|
+
self._handle_error_response(response)
|
|
117
|
+
|
|
118
|
+
data = response.json()
|
|
119
|
+
results = [
|
|
120
|
+
PrefabResult(
|
|
121
|
+
status=CallStatus(r["status"]),
|
|
122
|
+
output=r.get("output"),
|
|
123
|
+
error=r.get("error"),
|
|
124
|
+
job_id=data.get("job_id"),
|
|
125
|
+
)
|
|
126
|
+
for r in data["results"]
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
return BatchResult(job_id=data["job_id"], status=data["status"], results=results)
|
|
130
|
+
|
|
131
|
+
except httpx.TimeoutException:
|
|
132
|
+
raise ServiceUnavailableError("请求超时")
|
|
133
|
+
except httpx.RequestError as e:
|
|
134
|
+
raise ServiceUnavailableError(f"网络请求失败: {str(e)}")
|
|
135
|
+
|
|
136
|
+
def _run_streaming(self, call: PrefabCall) -> Iterator[StreamEvent]:
|
|
137
|
+
"""
|
|
138
|
+
流式执行预制件
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
call: 预制件调用
|
|
142
|
+
|
|
143
|
+
Yields:
|
|
144
|
+
StreamEvent
|
|
145
|
+
"""
|
|
146
|
+
url = f"{self.base_url}/v1/run"
|
|
147
|
+
headers = self.auth.get_headers()
|
|
148
|
+
headers["Content-Type"] = "application/json"
|
|
149
|
+
|
|
150
|
+
payload = {"calls": [call.to_dict()]}
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
154
|
+
with client.stream("POST", url, json=payload, headers=headers) as response:
|
|
155
|
+
self._handle_error_response(response)
|
|
156
|
+
|
|
157
|
+
# 解析 SSE 流
|
|
158
|
+
yield from parse_sse_stream(response.iter_bytes())
|
|
159
|
+
|
|
160
|
+
except httpx.TimeoutException:
|
|
161
|
+
raise ServiceUnavailableError("请求超时")
|
|
162
|
+
except httpx.RequestError as e:
|
|
163
|
+
raise ServiceUnavailableError(f"网络请求失败: {str(e)}")
|
|
164
|
+
|
|
165
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
166
|
+
"""
|
|
167
|
+
处理错误响应
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
response: HTTP 响应
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
对应的异常
|
|
174
|
+
"""
|
|
175
|
+
if response.status_code < 400:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
error_data = response.json()
|
|
180
|
+
detail = error_data.get("detail", "Unknown error")
|
|
181
|
+
|
|
182
|
+
# 解析错误详情
|
|
183
|
+
if isinstance(detail, dict):
|
|
184
|
+
error_code = detail.get("error_code", "UNKNOWN_ERROR")
|
|
185
|
+
message = detail.get("message", str(detail))
|
|
186
|
+
else:
|
|
187
|
+
error_code = "UNKNOWN_ERROR"
|
|
188
|
+
message = str(detail)
|
|
189
|
+
|
|
190
|
+
except Exception:
|
|
191
|
+
error_code = "UNKNOWN_ERROR"
|
|
192
|
+
# 对于流式响应,需要先读取内容
|
|
193
|
+
try:
|
|
194
|
+
error_text = response.text
|
|
195
|
+
except Exception:
|
|
196
|
+
# 如果无法读取,先读取响应再获取文本
|
|
197
|
+
try:
|
|
198
|
+
response.read()
|
|
199
|
+
error_text = response.text
|
|
200
|
+
except Exception:
|
|
201
|
+
error_text = "Unable to read error response"
|
|
202
|
+
message = f"HTTP {response.status_code}: {error_text}"
|
|
203
|
+
|
|
204
|
+
# 根据状态码和错误码抛出对应异常
|
|
205
|
+
if response.status_code == 401 or response.status_code == 403:
|
|
206
|
+
raise AuthenticationError(message)
|
|
207
|
+
elif response.status_code == 404:
|
|
208
|
+
raise PrefabNotFoundError("unknown", "unknown", message)
|
|
209
|
+
elif response.status_code == 422:
|
|
210
|
+
raise ValidationError(message)
|
|
211
|
+
elif response.status_code == 429:
|
|
212
|
+
# 配额超限
|
|
213
|
+
if isinstance(detail, dict):
|
|
214
|
+
raise QuotaExceededError(
|
|
215
|
+
message,
|
|
216
|
+
limit=detail.get("limit", 0),
|
|
217
|
+
used=detail.get("used", 0),
|
|
218
|
+
quota_type=detail.get("quota_type", "unknown"),
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
raise QuotaExceededError(message, 0, 0, "unknown")
|
|
222
|
+
elif response.status_code == 400 and error_code == "MISSING_SECRET":
|
|
223
|
+
# 缺少密钥
|
|
224
|
+
if isinstance(detail, dict):
|
|
225
|
+
raise MissingSecretError(
|
|
226
|
+
prefab_id=detail.get("prefab_id", "unknown"),
|
|
227
|
+
secret_name=detail.get("secret_name", "unknown"),
|
|
228
|
+
instructions=detail.get("instructions"),
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
raise MissingSecretError("unknown", "unknown")
|
|
232
|
+
elif response.status_code >= 500:
|
|
233
|
+
raise ServiceUnavailableError(message)
|
|
234
|
+
else:
|
|
235
|
+
raise GatewayError(message, {"error_code": error_code})
|
|
236
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""异常定义"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GatewayError(Exception):
|
|
7
|
+
"""Gateway SDK 基础异常"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
10
|
+
self.message = message
|
|
11
|
+
self.details = details or {}
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthenticationError(GatewayError):
|
|
16
|
+
"""认证失败"""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PrefabNotFoundError(GatewayError):
|
|
22
|
+
"""预制件不存在"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, prefab_id: str, version: str, message: Optional[str] = None):
|
|
25
|
+
self.prefab_id = prefab_id
|
|
26
|
+
self.version = version
|
|
27
|
+
super().__init__(
|
|
28
|
+
message or f"Prefab {prefab_id}@{version} not found",
|
|
29
|
+
{"prefab_id": prefab_id, "version": version},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ValidationError(GatewayError):
|
|
34
|
+
"""参数验证失败"""
|
|
35
|
+
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class QuotaExceededError(GatewayError):
|
|
40
|
+
"""配额超限"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, message: str, limit: int, used: int, quota_type: str):
|
|
43
|
+
self.limit = limit
|
|
44
|
+
self.used = used
|
|
45
|
+
self.quota_type = quota_type
|
|
46
|
+
super().__init__(
|
|
47
|
+
message,
|
|
48
|
+
{"limit": limit, "used": used, "quota_type": quota_type},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ServiceUnavailableError(GatewayError):
|
|
53
|
+
"""服务不可用"""
|
|
54
|
+
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MissingSecretError(GatewayError):
|
|
59
|
+
"""缺少必需的密钥"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, prefab_id: str, secret_name: str, instructions: Optional[str] = None):
|
|
62
|
+
self.prefab_id = prefab_id
|
|
63
|
+
self.secret_name = secret_name
|
|
64
|
+
self.instructions = instructions
|
|
65
|
+
message = f"Missing required secret '{secret_name}' for prefab '{prefab_id}'"
|
|
66
|
+
if instructions:
|
|
67
|
+
message += f"\n配置说明: {instructions}"
|
|
68
|
+
super().__init__(
|
|
69
|
+
message,
|
|
70
|
+
{"prefab_id": prefab_id, "secret_name": secret_name, "instructions": instructions},
|
|
71
|
+
)
|
|
72
|
+
|
gateway_sdk/models.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""数据模型"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CallStatus(str, Enum):
|
|
9
|
+
"""调用状态"""
|
|
10
|
+
|
|
11
|
+
SUCCESS = "SUCCESS"
|
|
12
|
+
FAILED = "FAILED"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StreamEventType(str, Enum):
|
|
16
|
+
"""流式事件类型"""
|
|
17
|
+
|
|
18
|
+
START = "start"
|
|
19
|
+
CONTENT = "content"
|
|
20
|
+
PROGRESS = "progress"
|
|
21
|
+
DONE = "done"
|
|
22
|
+
ERROR = "error"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class PrefabCall:
|
|
27
|
+
"""预制件调用请求"""
|
|
28
|
+
|
|
29
|
+
prefab_id: str
|
|
30
|
+
version: str
|
|
31
|
+
function_name: str
|
|
32
|
+
parameters: Dict[str, Any]
|
|
33
|
+
files: Optional[Dict[str, List[str]]] = None
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
36
|
+
"""转换为字典格式"""
|
|
37
|
+
inputs: Dict[str, Any] = {"parameters": self.parameters}
|
|
38
|
+
if self.files:
|
|
39
|
+
inputs["files"] = self.files
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
"prefab_id": self.prefab_id,
|
|
43
|
+
"version": self.version,
|
|
44
|
+
"function_name": self.function_name,
|
|
45
|
+
"inputs": inputs,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class PrefabResult:
|
|
51
|
+
"""预制件执行结果"""
|
|
52
|
+
|
|
53
|
+
status: CallStatus
|
|
54
|
+
output: Optional[Dict[str, Any]] = None
|
|
55
|
+
error: Optional[Dict[str, Any]] = None
|
|
56
|
+
job_id: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
def is_success(self) -> bool:
|
|
59
|
+
"""判断是否成功"""
|
|
60
|
+
return self.status == CallStatus.SUCCESS
|
|
61
|
+
|
|
62
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
63
|
+
"""便捷获取输出字段"""
|
|
64
|
+
if self.output:
|
|
65
|
+
return self.output.get(key, default)
|
|
66
|
+
return default
|
|
67
|
+
|
|
68
|
+
def get_result(self) -> Any:
|
|
69
|
+
"""获取业务结果"""
|
|
70
|
+
if self.output:
|
|
71
|
+
return self.output.get("result", self.output)
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def get_files(self) -> Dict[str, List[str]]:
|
|
75
|
+
"""获取输出文件"""
|
|
76
|
+
if self.output:
|
|
77
|
+
return self.output.get("files", {})
|
|
78
|
+
return {}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class BatchResult:
|
|
83
|
+
"""批量执行结果"""
|
|
84
|
+
|
|
85
|
+
job_id: str
|
|
86
|
+
status: str
|
|
87
|
+
results: List[PrefabResult]
|
|
88
|
+
|
|
89
|
+
def all_success(self) -> bool:
|
|
90
|
+
"""判断是否全部成功"""
|
|
91
|
+
return all(r.is_success() for r in self.results)
|
|
92
|
+
|
|
93
|
+
def get_failed(self) -> List[PrefabResult]:
|
|
94
|
+
"""获取失败的结果"""
|
|
95
|
+
return [r for r in self.results if not r.is_success()]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class StreamEvent:
|
|
100
|
+
"""流式事件"""
|
|
101
|
+
|
|
102
|
+
type: StreamEventType
|
|
103
|
+
data: Any
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_dict(cls, data: Dict[str, Any]) -> "StreamEvent":
|
|
107
|
+
"""从字典创建"""
|
|
108
|
+
event_type = data.get("type", "content")
|
|
109
|
+
try:
|
|
110
|
+
event_type_enum = StreamEventType(event_type)
|
|
111
|
+
except ValueError:
|
|
112
|
+
event_type_enum = StreamEventType.CONTENT
|
|
113
|
+
|
|
114
|
+
return cls(type=event_type_enum, data=data.get("data"))
|
gateway_sdk/streaming.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""流式响应处理"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Iterator
|
|
5
|
+
from .models import StreamEvent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_sse_stream(response_iter: Iterator[bytes]) -> Iterator[StreamEvent]:
|
|
9
|
+
"""
|
|
10
|
+
解析 SSE 流式响应
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
response_iter: HTTP 响应字节流迭代器
|
|
14
|
+
|
|
15
|
+
Yields:
|
|
16
|
+
StreamEvent: 解析后的流式事件
|
|
17
|
+
"""
|
|
18
|
+
buffer = b""
|
|
19
|
+
|
|
20
|
+
for chunk in response_iter:
|
|
21
|
+
buffer += chunk
|
|
22
|
+
|
|
23
|
+
# 按行分割
|
|
24
|
+
while b"\n" in buffer:
|
|
25
|
+
line_bytes, buffer = buffer.split(b"\n", 1)
|
|
26
|
+
line = line_bytes.decode("utf-8").strip()
|
|
27
|
+
|
|
28
|
+
# 跳过空行和注释
|
|
29
|
+
if not line or line.startswith(":"):
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
# 解析 SSE 格式:data: {...}
|
|
33
|
+
if line.startswith("data: "):
|
|
34
|
+
data_str = line[6:] # 去掉 "data: " 前缀
|
|
35
|
+
try:
|
|
36
|
+
data = json.loads(data_str)
|
|
37
|
+
yield StreamEvent.from_dict(data)
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
# 忽略无法解析的行
|
|
40
|
+
continue
|
|
41
|
+
|