jdcloud-agentgrid 0.1.3__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.
- agentgrid/__init__.py +11 -0
- agentgrid/apps/__init__.py +5 -0
- agentgrid/apps/base_app.py +13 -0
- agentgrid/apps/simple_app/__init__.py +5 -0
- agentgrid/apps/simple_app/simple_app.py +63 -0
- agentgrid/apps/simple_app/simple_app_handlers.py +252 -0
- agentgrid/auth/__init__.py +36 -0
- agentgrid/auth/credential_chain.py +129 -0
- agentgrid/auth/providers.py +122 -0
- agentgrid/config.py +504 -0
- agentgrid/errors.py +145 -0
- agentgrid/extensions/__init__.py +4 -0
- agentgrid/extensions/observability/__init__.py +52 -0
- agentgrid/extensions/tools/__init__.py +81 -0
- agentgrid/extensions/tools/browser_client.py +457 -0
- agentgrid/extensions/tools/code_interpreter_client.py +1233 -0
- agentgrid/extensions/tools/code_interpreter_models.py +124 -0
- agentgrid/profile.py +353 -0
- agentgrid/runtime/__init__.py +12 -0
- agentgrid/runtime/client.py +518 -0
- agentgrid/runtime/models.py +52 -0
- agentgrid/session.py +215 -0
- agentgrid/transport/__init__.py +19 -0
- agentgrid/transport/runtime_adapter.py +184 -0
- agentgrid/transport/runtime_data_client.py +228 -0
- agentgrid/version.py +6 -0
- jdcloud_agentgrid-0.1.3.dist-info/METADATA +177 -0
- jdcloud_agentgrid-0.1.3.dist-info/RECORD +29 -0
- jdcloud_agentgrid-0.1.3.dist-info/WHEEL +4 -0
agentgrid/config.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"""AgentGrid SDK 配置模型与默认配置管理。
|
|
2
|
+
|
|
3
|
+
OpenSpec `profile-and-config-core` 要求:
|
|
4
|
+
- 支持 region/endpoint/data_endpoint/timeout/max_retries/user_agent_extra 核心配置。
|
|
5
|
+
- 支持 connect_timeout/read_timeout 细粒度超时。
|
|
6
|
+
- 支持 SDK 默认级与 Profile 级配置合并,Profile 级覆盖默认级。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from dataclasses import dataclass, field, fields, replace
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
from jdcloud_sdk.core.config import Config as JdcloudCoreConfig
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_RUNTIME_ENDPOINT_HOST = "agentgrid.jdcloud-api.com"
|
|
22
|
+
DEFAULT_RUNTIME_ENDPOINT = f"http://{DEFAULT_RUNTIME_ENDPOINT_HOST}"
|
|
23
|
+
DEFAULT_RUNTIME_DATA_ENDPOINT = "https://agentgrid-cn-north-1.jdcloud.com"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True, frozen=True)
|
|
27
|
+
class BasicConfig:
|
|
28
|
+
"""agentgrid.yaml basic 节点配置。"""
|
|
29
|
+
|
|
30
|
+
region: str | None = None
|
|
31
|
+
endpoint: str | None = None
|
|
32
|
+
data_endpoint: str | None = None
|
|
33
|
+
timeout: float | None = None
|
|
34
|
+
connect_timeout: float | None = None
|
|
35
|
+
read_timeout: float | None = None
|
|
36
|
+
max_retries: int | None = None
|
|
37
|
+
user_agent_extra: str | None = None
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
if self.region is not None and not self.region.strip():
|
|
41
|
+
raise ValueError("region must not be empty")
|
|
42
|
+
if self.endpoint is not None and not self.endpoint.strip():
|
|
43
|
+
raise ValueError("endpoint must not be empty")
|
|
44
|
+
if self.data_endpoint is not None and not self.data_endpoint.strip():
|
|
45
|
+
raise ValueError("data_endpoint must not be empty")
|
|
46
|
+
if self.timeout is not None and self.timeout < 0:
|
|
47
|
+
raise ValueError("timeout must be >= 0")
|
|
48
|
+
if self.connect_timeout is not None and self.connect_timeout < 0:
|
|
49
|
+
raise ValueError("connect_timeout must be >= 0")
|
|
50
|
+
if self.read_timeout is not None and self.read_timeout < 0:
|
|
51
|
+
raise ValueError("read_timeout must be >= 0")
|
|
52
|
+
if self.max_retries is not None and self.max_retries < 0:
|
|
53
|
+
raise ValueError("max_retries must be >= 0")
|
|
54
|
+
|
|
55
|
+
def to_overrides(self) -> dict[str, object]:
|
|
56
|
+
"""转换为 AgentGridConfig 覆盖字段。"""
|
|
57
|
+
|
|
58
|
+
overrides: dict[str, object] = {}
|
|
59
|
+
for config_field in fields(BasicConfig):
|
|
60
|
+
value = getattr(self, config_field.name)
|
|
61
|
+
if value is not None:
|
|
62
|
+
overrides[config_field.name] = value
|
|
63
|
+
return overrides
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(slots=True, frozen=True)
|
|
67
|
+
class RuntimeConfig:
|
|
68
|
+
"""agentgrid.yaml runtime 节点配置。"""
|
|
69
|
+
|
|
70
|
+
region: str | None = None
|
|
71
|
+
identifier: str | None = None
|
|
72
|
+
api_key: str | None = None
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
if self.region is not None and not self.region.strip():
|
|
76
|
+
raise ValueError("runtime.region must not be empty")
|
|
77
|
+
if self.identifier is not None and not self.identifier.strip():
|
|
78
|
+
raise ValueError("runtime.identifier must not be empty")
|
|
79
|
+
if self.api_key is not None and not self.api_key.strip():
|
|
80
|
+
raise ValueError("runtime.api_key must not be empty")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(slots=True, frozen=True)
|
|
84
|
+
class ToolConfig:
|
|
85
|
+
"""agentgrid.yaml 单个工具节点配置。"""
|
|
86
|
+
|
|
87
|
+
identifier: str | None = None
|
|
88
|
+
|
|
89
|
+
def __post_init__(self) -> None:
|
|
90
|
+
if self.identifier is not None and not self.identifier.strip():
|
|
91
|
+
raise ValueError("tool.identifier must not be empty")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(slots=True, frozen=True)
|
|
95
|
+
class ToolsConfig:
|
|
96
|
+
"""agentgrid.yaml tools 节点配置。"""
|
|
97
|
+
|
|
98
|
+
code_interpreter: ToolConfig | None = None
|
|
99
|
+
browser: ToolConfig | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(slots=True, frozen=True)
|
|
103
|
+
class Config:
|
|
104
|
+
"""SDK 全局配置聚合对象。"""
|
|
105
|
+
|
|
106
|
+
basic: BasicConfig = field(default_factory=BasicConfig)
|
|
107
|
+
runtime: RuntimeConfig = field(default_factory=RuntimeConfig)
|
|
108
|
+
tools: ToolsConfig = field(default_factory=ToolsConfig)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(slots=True, frozen=True)
|
|
112
|
+
class AgentGridConfig:
|
|
113
|
+
"""AgentGrid SDK 运行时配置。"""
|
|
114
|
+
|
|
115
|
+
region: str | None = None
|
|
116
|
+
endpoint: str | None = None
|
|
117
|
+
data_endpoint: str | None = None
|
|
118
|
+
timeout: float | None = None
|
|
119
|
+
connect_timeout: float | None = None
|
|
120
|
+
read_timeout: float | None = None
|
|
121
|
+
max_retries: int = 2
|
|
122
|
+
user_agent_extra: str | None = None
|
|
123
|
+
|
|
124
|
+
def resolve_endpoint_for_jdcloud(self) -> tuple[str | None, str | None]:
|
|
125
|
+
"""解析并返回 jdcloud core Config 所需 endpoint/scheme。"""
|
|
126
|
+
|
|
127
|
+
if self.endpoint is None:
|
|
128
|
+
return None, None
|
|
129
|
+
|
|
130
|
+
endpoint = self.endpoint.strip()
|
|
131
|
+
if not endpoint:
|
|
132
|
+
raise ValueError("endpoint must not be empty")
|
|
133
|
+
|
|
134
|
+
if "://" not in endpoint:
|
|
135
|
+
return endpoint, None
|
|
136
|
+
|
|
137
|
+
parsed = urlparse(endpoint)
|
|
138
|
+
if not parsed.hostname:
|
|
139
|
+
raise ValueError("endpoint must contain hostname")
|
|
140
|
+
if parsed.path and parsed.path != "/":
|
|
141
|
+
raise ValueError("endpoint must not contain path")
|
|
142
|
+
if parsed.query or parsed.fragment:
|
|
143
|
+
raise ValueError("endpoint must not contain query or fragment")
|
|
144
|
+
|
|
145
|
+
host = parsed.hostname
|
|
146
|
+
if parsed.port is not None:
|
|
147
|
+
host = f"{host}:{parsed.port}"
|
|
148
|
+
return host, parsed.scheme or None
|
|
149
|
+
|
|
150
|
+
def resolve_timeout_for_jdcloud(self) -> float | tuple[float, float] | None:
|
|
151
|
+
"""解析并返回 jdcloud core Config 可用 timeout。"""
|
|
152
|
+
|
|
153
|
+
connect_timeout = self.effective_connect_timeout
|
|
154
|
+
read_timeout = self.effective_read_timeout
|
|
155
|
+
if connect_timeout is not None and read_timeout is not None:
|
|
156
|
+
if connect_timeout == read_timeout:
|
|
157
|
+
return connect_timeout
|
|
158
|
+
return (connect_timeout, read_timeout)
|
|
159
|
+
if connect_timeout is not None:
|
|
160
|
+
return connect_timeout
|
|
161
|
+
if read_timeout is not None:
|
|
162
|
+
return read_timeout
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def to_jdcloud_core_config(self) -> JdcloudCoreConfig:
|
|
166
|
+
"""构建 jdcloud core `Config`,用于真实 AgentgridClient。"""
|
|
167
|
+
|
|
168
|
+
endpoint, scheme = self.resolve_endpoint_for_jdcloud()
|
|
169
|
+
if endpoint is None:
|
|
170
|
+
endpoint = DEFAULT_RUNTIME_ENDPOINT_HOST
|
|
171
|
+
scheme = "http"
|
|
172
|
+
timeout = self.resolve_timeout_for_jdcloud()
|
|
173
|
+
|
|
174
|
+
config = JdcloudCoreConfig(endpoint=endpoint)
|
|
175
|
+
if scheme is not None:
|
|
176
|
+
config.scheme = scheme
|
|
177
|
+
if timeout is not None:
|
|
178
|
+
config.timeout = timeout
|
|
179
|
+
return config
|
|
180
|
+
|
|
181
|
+
def __post_init__(self) -> None:
|
|
182
|
+
if self.endpoint is not None and not self.endpoint.strip():
|
|
183
|
+
raise ValueError("endpoint must not be empty")
|
|
184
|
+
if self.data_endpoint is not None and not self.data_endpoint.strip():
|
|
185
|
+
raise ValueError("data_endpoint must not be empty")
|
|
186
|
+
if self.timeout is not None and self.timeout < 0:
|
|
187
|
+
raise ValueError("timeout must be >= 0")
|
|
188
|
+
if self.connect_timeout is not None and self.connect_timeout < 0:
|
|
189
|
+
raise ValueError("connect_timeout must be >= 0")
|
|
190
|
+
if self.read_timeout is not None and self.read_timeout < 0:
|
|
191
|
+
raise ValueError("read_timeout must be >= 0")
|
|
192
|
+
if self.max_retries < 0:
|
|
193
|
+
raise ValueError("max_retries must be >= 0")
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def effective_connect_timeout(self) -> float | None:
|
|
197
|
+
"""返回请求链路实际生效的连接超时。
|
|
198
|
+
|
|
199
|
+
协同规则:显式 connect_timeout 优先;否则回退到统一 timeout。
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
if self.connect_timeout is not None:
|
|
203
|
+
return self.connect_timeout
|
|
204
|
+
return self.timeout
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def effective_read_timeout(self) -> float | None:
|
|
208
|
+
"""返回请求链路实际生效的读取超时。
|
|
209
|
+
|
|
210
|
+
协同规则:显式 read_timeout 优先;否则回退到统一 timeout。
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
if self.read_timeout is not None:
|
|
214
|
+
return self.read_timeout
|
|
215
|
+
return self.timeout
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def effective_data_endpoint(self) -> str:
|
|
219
|
+
"""返回数据口实际生效 endpoint。
|
|
220
|
+
|
|
221
|
+
OpenSpec 要求控制口与数据口端点分离;当用户未显式配置 data_endpoint
|
|
222
|
+
时,数据口使用固定默认网关。
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
if self.data_endpoint is None:
|
|
226
|
+
return DEFAULT_RUNTIME_DATA_ENDPOINT
|
|
227
|
+
return self.data_endpoint
|
|
228
|
+
|
|
229
|
+
def merged_with(self, override: AgentGridConfig | None) -> AgentGridConfig:
|
|
230
|
+
"""返回合并后的配置对象。
|
|
231
|
+
|
|
232
|
+
合并规则:override 中非 None 字段覆盖 base;max_retries 永远采用 override。
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
if override is None:
|
|
236
|
+
return self
|
|
237
|
+
|
|
238
|
+
return AgentGridConfig(
|
|
239
|
+
region=override.region if override.region is not None else self.region,
|
|
240
|
+
endpoint=override.endpoint
|
|
241
|
+
if override.endpoint is not None
|
|
242
|
+
else self.endpoint,
|
|
243
|
+
data_endpoint=(
|
|
244
|
+
override.data_endpoint
|
|
245
|
+
if override.data_endpoint is not None
|
|
246
|
+
else self.data_endpoint
|
|
247
|
+
),
|
|
248
|
+
timeout=override.timeout if override.timeout is not None else self.timeout,
|
|
249
|
+
connect_timeout=(
|
|
250
|
+
override.connect_timeout
|
|
251
|
+
if override.connect_timeout is not None
|
|
252
|
+
else self.connect_timeout
|
|
253
|
+
),
|
|
254
|
+
read_timeout=(
|
|
255
|
+
override.read_timeout
|
|
256
|
+
if override.read_timeout is not None
|
|
257
|
+
else self.read_timeout
|
|
258
|
+
),
|
|
259
|
+
max_retries=override.max_retries,
|
|
260
|
+
user_agent_extra=(
|
|
261
|
+
override.user_agent_extra
|
|
262
|
+
if override.user_agent_extra is not None
|
|
263
|
+
else self.user_agent_extra
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def with_overrides(self, **kwargs: object) -> AgentGridConfig:
|
|
268
|
+
"""使用 dataclass replace 生成覆盖后的新配置。"""
|
|
269
|
+
|
|
270
|
+
return replace(self, **kwargs)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
_BUILTIN_SDK_DEFAULT_CONFIG = AgentGridConfig(
|
|
274
|
+
endpoint=DEFAULT_RUNTIME_ENDPOINT,
|
|
275
|
+
data_endpoint=DEFAULT_RUNTIME_DATA_ENDPOINT,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
_SDK_DEFAULT_CONFIG = _BUILTIN_SDK_DEFAULT_CONFIG
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _resolve_agentgrid_yaml_path() -> Path | None:
|
|
282
|
+
"""解析入口脚本同级 agentgrid.yaml 绝对路径。"""
|
|
283
|
+
|
|
284
|
+
if not sys.argv or not sys.argv[0]:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
script_path = Path(sys.argv[0])
|
|
288
|
+
if not script_path.is_absolute():
|
|
289
|
+
script_path = Path.cwd() / script_path
|
|
290
|
+
return script_path.resolve().parent / "agentgrid.yaml"
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _validate_mapping_fields(
|
|
294
|
+
*,
|
|
295
|
+
agentgrid_path: Path,
|
|
296
|
+
node_name: str,
|
|
297
|
+
mapping_obj: Mapping[object, object],
|
|
298
|
+
allowed_fields: set[str],
|
|
299
|
+
) -> None:
|
|
300
|
+
"""校验 mapping 节点字段是否都在白名单内。"""
|
|
301
|
+
|
|
302
|
+
unknown_fields = sorted(
|
|
303
|
+
str(field_name)
|
|
304
|
+
for field_name in mapping_obj
|
|
305
|
+
if not isinstance(field_name, str) or field_name not in allowed_fields
|
|
306
|
+
)
|
|
307
|
+
if not unknown_fields:
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
allowed_values = ", ".join(sorted(allowed_fields))
|
|
311
|
+
unknown_values = ", ".join(unknown_fields)
|
|
312
|
+
raise ValueError(
|
|
313
|
+
f"Invalid agentgrid.yaml at '{agentgrid_path}': unknown {node_name} fields "
|
|
314
|
+
f"[{unknown_values}], allowed fields: [{allowed_values}]"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _parse_basic_config(*, agentgrid_path: Path, value: object) -> BasicConfig:
|
|
319
|
+
"""解析 basic 配置。"""
|
|
320
|
+
|
|
321
|
+
if value is None:
|
|
322
|
+
return BasicConfig()
|
|
323
|
+
if not isinstance(value, Mapping):
|
|
324
|
+
raise ValueError(
|
|
325
|
+
f"Invalid agentgrid.yaml at '{agentgrid_path}': basic must be a mapping object"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
allowed_fields = {config_field.name for config_field in fields(BasicConfig)}
|
|
329
|
+
_validate_mapping_fields(
|
|
330
|
+
agentgrid_path=agentgrid_path,
|
|
331
|
+
node_name="basic",
|
|
332
|
+
mapping_obj=value,
|
|
333
|
+
allowed_fields=allowed_fields,
|
|
334
|
+
)
|
|
335
|
+
basic_dict = {str(key): field_value for key, field_value in value.items()}
|
|
336
|
+
try:
|
|
337
|
+
return BasicConfig(**basic_dict)
|
|
338
|
+
except (TypeError, ValueError) as exc:
|
|
339
|
+
raise ValueError(
|
|
340
|
+
f"Invalid agentgrid.yaml at '{agentgrid_path}': invalid basic config: {exc}"
|
|
341
|
+
) from exc
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _parse_runtime_config(*, agentgrid_path: Path, value: object) -> RuntimeConfig:
|
|
345
|
+
"""解析 runtime 配置。"""
|
|
346
|
+
|
|
347
|
+
if value is None:
|
|
348
|
+
return RuntimeConfig()
|
|
349
|
+
if not isinstance(value, Mapping):
|
|
350
|
+
raise ValueError(
|
|
351
|
+
f"Invalid agentgrid.yaml at '{agentgrid_path}': runtime must be a mapping object"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
allowed_fields = {config_field.name for config_field in fields(RuntimeConfig)}
|
|
355
|
+
_validate_mapping_fields(
|
|
356
|
+
agentgrid_path=agentgrid_path,
|
|
357
|
+
node_name="runtime",
|
|
358
|
+
mapping_obj=value,
|
|
359
|
+
allowed_fields=allowed_fields,
|
|
360
|
+
)
|
|
361
|
+
runtime_dict = {str(key): field_value for key, field_value in value.items()}
|
|
362
|
+
try:
|
|
363
|
+
return RuntimeConfig(**runtime_dict)
|
|
364
|
+
except (TypeError, ValueError) as exc:
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"Invalid agentgrid.yaml at '{agentgrid_path}': invalid runtime config: {exc}"
|
|
367
|
+
) from exc
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _parse_tool_config(
|
|
371
|
+
*, agentgrid_path: Path, tool_name: str, value: object
|
|
372
|
+
) -> ToolConfig:
|
|
373
|
+
"""解析单个工具配置。"""
|
|
374
|
+
|
|
375
|
+
if not isinstance(value, Mapping):
|
|
376
|
+
raise ValueError(
|
|
377
|
+
f"Invalid agentgrid.yaml at '{agentgrid_path}': tools.{tool_name} must be a mapping object"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
allowed_fields = {config_field.name for config_field in fields(ToolConfig)}
|
|
381
|
+
_validate_mapping_fields(
|
|
382
|
+
agentgrid_path=agentgrid_path,
|
|
383
|
+
node_name=f"tools.{tool_name}",
|
|
384
|
+
mapping_obj=value,
|
|
385
|
+
allowed_fields=allowed_fields,
|
|
386
|
+
)
|
|
387
|
+
tool_dict = {str(key): field_value for key, field_value in value.items()}
|
|
388
|
+
try:
|
|
389
|
+
return ToolConfig(**tool_dict)
|
|
390
|
+
except (TypeError, ValueError) as exc:
|
|
391
|
+
raise ValueError(
|
|
392
|
+
f"Invalid agentgrid.yaml at '{agentgrid_path}': invalid tools.{tool_name} config: {exc}"
|
|
393
|
+
) from exc
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _parse_tools_config(*, agentgrid_path: Path, value: object) -> ToolsConfig:
|
|
397
|
+
"""解析 tools 配置。"""
|
|
398
|
+
|
|
399
|
+
if value is None:
|
|
400
|
+
return ToolsConfig()
|
|
401
|
+
if not isinstance(value, Mapping):
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"Invalid agentgrid.yaml at '{agentgrid_path}': tools must be a mapping object"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
allowed_fields = {config_field.name for config_field in fields(ToolsConfig)}
|
|
407
|
+
_validate_mapping_fields(
|
|
408
|
+
agentgrid_path=agentgrid_path,
|
|
409
|
+
node_name="tools",
|
|
410
|
+
mapping_obj=value,
|
|
411
|
+
allowed_fields=allowed_fields,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
tools_dict: dict[str, ToolConfig | None] = {}
|
|
415
|
+
for key, field_value in value.items():
|
|
416
|
+
key_name = str(key)
|
|
417
|
+
if field_value is None:
|
|
418
|
+
tools_dict[key_name] = None
|
|
419
|
+
continue
|
|
420
|
+
tools_dict[key_name] = _parse_tool_config(
|
|
421
|
+
agentgrid_path=agentgrid_path,
|
|
422
|
+
tool_name=key_name,
|
|
423
|
+
value=field_value,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
return ToolsConfig(**tools_dict)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def load_config() -> Config:
|
|
430
|
+
"""加载入口脚本同级 agentgrid.yaml 为 Config 对象。"""
|
|
431
|
+
|
|
432
|
+
agentgrid_path = _resolve_agentgrid_yaml_path()
|
|
433
|
+
if agentgrid_path is None or not agentgrid_path.exists():
|
|
434
|
+
return Config()
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
with agentgrid_path.open("r", encoding="utf-8") as yaml_file:
|
|
438
|
+
raw_config = yaml.safe_load(yaml_file)
|
|
439
|
+
except OSError as exc:
|
|
440
|
+
raise ValueError(
|
|
441
|
+
f"Failed to read agentgrid.yaml at '{agentgrid_path}': {exc}"
|
|
442
|
+
) from exc
|
|
443
|
+
except yaml.YAMLError as exc:
|
|
444
|
+
raise ValueError(
|
|
445
|
+
f"Failed to parse agentgrid.yaml at '{agentgrid_path}': {exc}"
|
|
446
|
+
) from exc
|
|
447
|
+
|
|
448
|
+
if raw_config is None:
|
|
449
|
+
return Config()
|
|
450
|
+
if not isinstance(raw_config, Mapping):
|
|
451
|
+
raise ValueError(
|
|
452
|
+
f"Invalid agentgrid.yaml at '{agentgrid_path}': root must be a mapping object"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
allowed_root_fields = {"basic", "runtime", "tools"}
|
|
456
|
+
_validate_mapping_fields(
|
|
457
|
+
agentgrid_path=agentgrid_path,
|
|
458
|
+
node_name="root",
|
|
459
|
+
mapping_obj=raw_config,
|
|
460
|
+
allowed_fields=allowed_root_fields,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
return Config(
|
|
464
|
+
basic=_parse_basic_config(
|
|
465
|
+
agentgrid_path=agentgrid_path,
|
|
466
|
+
value=raw_config.get("basic"),
|
|
467
|
+
),
|
|
468
|
+
runtime=_parse_runtime_config(
|
|
469
|
+
agentgrid_path=agentgrid_path,
|
|
470
|
+
value=raw_config.get("runtime"),
|
|
471
|
+
),
|
|
472
|
+
tools=_parse_tools_config(
|
|
473
|
+
agentgrid_path=agentgrid_path,
|
|
474
|
+
value=raw_config.get("tools"),
|
|
475
|
+
),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# 模块加载时初始化一次,全局可用。
|
|
480
|
+
settings = load_config()
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def get_sdk_default_config() -> AgentGridConfig:
|
|
484
|
+
"""获取 SDK 默认级配置。"""
|
|
485
|
+
|
|
486
|
+
basic_overrides = settings.basic.to_overrides()
|
|
487
|
+
if not basic_overrides:
|
|
488
|
+
return _SDK_DEFAULT_CONFIG
|
|
489
|
+
|
|
490
|
+
return _SDK_DEFAULT_CONFIG.with_overrides(**basic_overrides)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def set_sdk_default_config(config: AgentGridConfig) -> None:
|
|
494
|
+
"""设置 SDK 默认级配置。"""
|
|
495
|
+
|
|
496
|
+
global _SDK_DEFAULT_CONFIG
|
|
497
|
+
_SDK_DEFAULT_CONFIG = config
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def clear_sdk_default_config() -> None:
|
|
501
|
+
"""清空 SDK 默认级配置,回退到内置默认值。"""
|
|
502
|
+
|
|
503
|
+
global _SDK_DEFAULT_CONFIG
|
|
504
|
+
_SDK_DEFAULT_CONFIG = _BUILTIN_SDK_DEFAULT_CONFIG
|
agentgrid/errors.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""AgentGrid 统一异常定义与错误映射。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentGridError(Exception):
|
|
9
|
+
"""统一异常基类。"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
code: str | None = None,
|
|
16
|
+
request_id: str | None = None,
|
|
17
|
+
retryable: bool | None = None,
|
|
18
|
+
raw_error: Any | None = None,
|
|
19
|
+
cause: Exception | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.code = code
|
|
23
|
+
self.request_id = request_id
|
|
24
|
+
self.retryable = retryable
|
|
25
|
+
self.raw_error = raw_error
|
|
26
|
+
self.cause = cause
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AgentGridAuthenticationError(AgentGridError):
|
|
30
|
+
"""认证失败。"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AgentGridAuthorizationError(AgentGridError):
|
|
34
|
+
"""鉴权失败。"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentGridRateLimitError(AgentGridError):
|
|
38
|
+
"""限流错误。"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AgentGridTimeoutError(AgentGridError):
|
|
42
|
+
"""超时错误。"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AgentGridTransportError(AgentGridError):
|
|
46
|
+
"""传输错误。"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AgentGridStreamError(AgentGridError):
|
|
50
|
+
"""流式处理错误。"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AgentGridServerError(AgentGridError):
|
|
54
|
+
"""服务端错误。"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def map_runtime_error_to_agentgrid_error(
|
|
58
|
+
error: Any,
|
|
59
|
+
*,
|
|
60
|
+
default_message: str,
|
|
61
|
+
) -> AgentGridError:
|
|
62
|
+
"""将底层错误 payload 统一映射到 AgentGrid 异常分层。"""
|
|
63
|
+
|
|
64
|
+
code, message, request_id, retryable, status = _extract_error_fields(error)
|
|
65
|
+
error_cls = _resolve_error_class(code=code, status=status)
|
|
66
|
+
return error_cls(
|
|
67
|
+
message or default_message,
|
|
68
|
+
code=code,
|
|
69
|
+
request_id=request_id,
|
|
70
|
+
retryable=retryable,
|
|
71
|
+
raw_error=error,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _extract_error_fields(
|
|
76
|
+
error: Any,
|
|
77
|
+
) -> tuple[str | None, str | None, str | None, bool | None, int | None]:
|
|
78
|
+
if not isinstance(error, dict):
|
|
79
|
+
return None, str(error), None, None, None
|
|
80
|
+
|
|
81
|
+
code = _as_optional_upper(error.get("code"))
|
|
82
|
+
message = _as_optional_str(error.get("message"))
|
|
83
|
+
request_id = _as_optional_str(error.get("request_id"))
|
|
84
|
+
retryable_value = error.get("retryable")
|
|
85
|
+
retryable = retryable_value if isinstance(retryable_value, bool) else None
|
|
86
|
+
status = _as_optional_int(error.get("status") or error.get("status_code"))
|
|
87
|
+
return code, message, request_id, retryable, status
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolve_error_class(*, code: str | None, status: int | None) -> type[AgentGridError]:
|
|
91
|
+
if code in {
|
|
92
|
+
"AUTHENTICATION_FAILED",
|
|
93
|
+
"INVALID_API_KEY",
|
|
94
|
+
"UNAUTHENTICATED",
|
|
95
|
+
"INVALID_SIGNATURE",
|
|
96
|
+
}:
|
|
97
|
+
return AgentGridAuthenticationError
|
|
98
|
+
if code in {"AUTHORIZATION_FAILED", "FORBIDDEN", "PERMISSION_DENIED"}:
|
|
99
|
+
return AgentGridAuthorizationError
|
|
100
|
+
if code in {"RATE_LIMITED", "TOO_MANY_REQUESTS"}:
|
|
101
|
+
return AgentGridRateLimitError
|
|
102
|
+
if code in {"TIMEOUT", "REQUEST_TIMEOUT", "GATEWAY_TIMEOUT"}:
|
|
103
|
+
return AgentGridTimeoutError
|
|
104
|
+
if code in {"TRANSPORT_ERROR", "NETWORK_ERROR", "CONNECTION_ERROR"}:
|
|
105
|
+
return AgentGridTransportError
|
|
106
|
+
if code in {"STREAM_ERROR", "STREAM_INTERRUPTED", "STREAM_METHOD_MISSING"}:
|
|
107
|
+
return AgentGridStreamError
|
|
108
|
+
|
|
109
|
+
if status == 401:
|
|
110
|
+
return AgentGridAuthenticationError
|
|
111
|
+
if status == 403:
|
|
112
|
+
return AgentGridAuthorizationError
|
|
113
|
+
if status == 429:
|
|
114
|
+
return AgentGridRateLimitError
|
|
115
|
+
if status in {408, 504}:
|
|
116
|
+
return AgentGridTimeoutError
|
|
117
|
+
if status is not None and 500 <= status <= 599:
|
|
118
|
+
return AgentGridServerError
|
|
119
|
+
return AgentGridServerError
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _as_optional_str(value: Any) -> str | None:
|
|
123
|
+
if value is None:
|
|
124
|
+
return None
|
|
125
|
+
return str(value)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _as_optional_upper(value: Any) -> str | None:
|
|
129
|
+
parsed = _as_optional_str(value)
|
|
130
|
+
if parsed is None:
|
|
131
|
+
return None
|
|
132
|
+
return parsed.upper()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _as_optional_int(value: Any) -> int | None:
|
|
136
|
+
if value is None:
|
|
137
|
+
return None
|
|
138
|
+
if isinstance(value, bool):
|
|
139
|
+
return None
|
|
140
|
+
if isinstance(value, int):
|
|
141
|
+
return value
|
|
142
|
+
try:
|
|
143
|
+
return int(str(value))
|
|
144
|
+
except ValueError:
|
|
145
|
+
return None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""可观测性扩展点预留。
|
|
2
|
+
|
|
3
|
+
对应 OpenSpec `extension-points-for-tools-and-observability`:
|
|
4
|
+
- 预留 `before_request`、`after_response`、`on_error` 三类 hook。
|
|
5
|
+
- v1 先提供可注册与触发的最小实现,便于后续观测能力平滑接入。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
Hook = Callable[[dict[str, Any]], None]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class ObservabilityHooks:
|
|
18
|
+
"""请求生命周期 hook 容器。"""
|
|
19
|
+
|
|
20
|
+
before_request: list[Hook] = field(default_factory=list)
|
|
21
|
+
after_response: list[Hook] = field(default_factory=list)
|
|
22
|
+
on_error: list[Hook] = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
def register_before_request(self, hook: Hook) -> None:
|
|
25
|
+
self.before_request.append(hook)
|
|
26
|
+
|
|
27
|
+
def register_after_response(self, hook: Hook) -> None:
|
|
28
|
+
self.after_response.append(hook)
|
|
29
|
+
|
|
30
|
+
def register_on_error(self, hook: Hook) -> None:
|
|
31
|
+
self.on_error.append(hook)
|
|
32
|
+
|
|
33
|
+
def emit_before_request(self, payload: dict[str, Any]) -> None:
|
|
34
|
+
self._emit(self.before_request, payload)
|
|
35
|
+
|
|
36
|
+
def emit_after_response(self, payload: dict[str, Any]) -> None:
|
|
37
|
+
self._emit(self.after_response, payload)
|
|
38
|
+
|
|
39
|
+
def emit_on_error(self, payload: dict[str, Any]) -> None:
|
|
40
|
+
self._emit(self.on_error, payload)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _emit(hooks: list[Hook], payload: dict[str, Any]) -> None:
|
|
44
|
+
# hooks 是观测扩展点,任何单个 hook 异常都不应破坏主业务链路。
|
|
45
|
+
for hook in hooks:
|
|
46
|
+
try:
|
|
47
|
+
hook(payload)
|
|
48
|
+
except Exception:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = ["ObservabilityHooks", "Hook"]
|