agent-runtime-sdk 0.1.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.
- agent_runtime/__init__.py +84 -0
- agent_runtime/builder.py +317 -0
- agent_runtime/config/__init__.py +29 -0
- agent_runtime/config/definitions.py +144 -0
- agent_runtime/config/policies.py +63 -0
- agent_runtime/config/storage.py +117 -0
- agent_runtime/context.py +10 -0
- agent_runtime/definitions.py +33 -0
- agent_runtime/discovery.py +16 -0
- agent_runtime/exceptions.py +74 -0
- agent_runtime/mcp/__init__.py +28 -0
- agent_runtime/mcp/discovery.py +146 -0
- agent_runtime/mcp/metadata.py +68 -0
- agent_runtime/mcp/utils.py +52 -0
- agent_runtime/model_registry.py +40 -0
- agent_runtime/plugins/__init__.py +4 -0
- agent_runtime/plugins/base.py +90 -0
- agent_runtime/plugins/default.py +19 -0
- agent_runtime/plugins/instructions.py +38 -0
- agent_runtime/plugins/loader.py +59 -0
- agent_runtime/policies.py +15 -0
- agent_runtime/runtime.py +110 -0
- agent_runtime/runtime_engine/__init__.py +22 -0
- agent_runtime/runtime_engine/a2a_bridge.py +190 -0
- agent_runtime/runtime_engine/a2a_task_io.py +165 -0
- agent_runtime/runtime_engine/agent_build.py +315 -0
- agent_runtime/runtime_engine/context.py +469 -0
- agent_runtime/runtime_engine/loading.py +170 -0
- agent_runtime/runtime_engine/observability.py +154 -0
- agent_runtime/runtime_engine/policy_registry.py +98 -0
- agent_runtime/runtime_engine/protocol_tools.py +94 -0
- agent_runtime/runtime_engine/task_flow.py +897 -0
- agent_runtime/runtime_engine/tool_flow.py +332 -0
- agent_runtime/sdk_agent.py +548 -0
- agent_runtime/server/__init__.py +15 -0
- agent_runtime/server/app_factory.py +37 -0
- agent_runtime/server/bootstrap.py +48 -0
- agent_runtime/server/endpoint_utils.py +37 -0
- agent_runtime/server/management.py +107 -0
- agent_runtime/smol/__init__.py +4 -0
- agent_runtime/smol/agents.py +431 -0
- agent_runtime/smol/llm_models.py +212 -0
- agent_runtime/smol/memory.py +111 -0
- agent_runtime/smol/models.py +69 -0
- agent_runtime/standalone.py +57 -0
- agent_runtime/storage.py +5 -0
- agent_runtime/tools.py +5 -0
- agent_runtime_sdk-0.1.0.dist-info/METADATA +125 -0
- agent_runtime_sdk-0.1.0.dist-info/RECORD +51 -0
- agent_runtime_sdk-0.1.0.dist-info/WHEEL +5 -0
- agent_runtime_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""面向 SDK 使用者的类式 agent 抽象。
|
|
4
|
+
|
|
5
|
+
设计目标:
|
|
6
|
+
|
|
7
|
+
1. YAML 配置方式和 Python 类方式都收敛到同一个规范层:`AgentDefinition`
|
|
8
|
+
2. 业务方既可以直接声明完整 definition,也可以只覆写少量小钩子
|
|
9
|
+
3. 基类数量不追求多,而是提供少量高价值的“半成品基类”
|
|
10
|
+
|
|
11
|
+
建议分层:
|
|
12
|
+
|
|
13
|
+
- `BaseSDKAgent`: 只管 definition / builder / runtime / app 生命周期
|
|
14
|
+
- `DeclarativeSDKAgent`: 适合直接返回完整 definition 的场景
|
|
15
|
+
- `ConfiguredSDKAgent`: 适合用细粒度 hook 组装 definition
|
|
16
|
+
- `SingleMCPAgent`: 单 MCP 场景的高频半成品
|
|
17
|
+
- `OpenAICompatibleSingleMCPAgent`: 单 MCP + OpenAI-compatible 模型的常见半成品
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from typing import Any, Callable
|
|
23
|
+
|
|
24
|
+
from .builder import AgentBuilder
|
|
25
|
+
from .config.definitions import (
|
|
26
|
+
DEFAULT_PASS_THROUGH_HEADERS,
|
|
27
|
+
A2ASettings,
|
|
28
|
+
AgentConfig,
|
|
29
|
+
AgentDefinition,
|
|
30
|
+
MCPSettings,
|
|
31
|
+
ModelSettings,
|
|
32
|
+
PluginSettings,
|
|
33
|
+
RuntimeConfig,
|
|
34
|
+
SkillDefinition,
|
|
35
|
+
ToolPolicy,
|
|
36
|
+
)
|
|
37
|
+
from .mcp.metadata import DiscoveredTool
|
|
38
|
+
from .plugins.base import BaseAgentPlugin
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _coerce_agent_config(value: AgentConfig | dict[str, Any]) -> AgentConfig:
|
|
42
|
+
return value if isinstance(value, AgentConfig) else AgentConfig.model_validate(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _coerce_runtime_config(
|
|
46
|
+
value: RuntimeConfig | dict[str, Any]
|
|
47
|
+
) -> RuntimeConfig:
|
|
48
|
+
return (
|
|
49
|
+
value if isinstance(value, RuntimeConfig) else RuntimeConfig.model_validate(value)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _coerce_model_settings(
|
|
54
|
+
value: ModelSettings | dict[str, Any]
|
|
55
|
+
) -> ModelSettings:
|
|
56
|
+
return value if isinstance(value, ModelSettings) else ModelSettings.model_validate(value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _coerce_plugin_settings(
|
|
60
|
+
value: PluginSettings | dict[str, Any] | None,
|
|
61
|
+
) -> PluginSettings | None:
|
|
62
|
+
if value is None or isinstance(value, PluginSettings):
|
|
63
|
+
return value
|
|
64
|
+
return PluginSettings.model_validate(value)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _coerce_skill_definition(
|
|
68
|
+
value: SkillDefinition | dict[str, Any]
|
|
69
|
+
) -> SkillDefinition:
|
|
70
|
+
return (
|
|
71
|
+
value if isinstance(value, SkillDefinition) else SkillDefinition.model_validate(value)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _coerce_tool_policies(
|
|
76
|
+
policies: dict[str, ToolPolicy | dict[str, Any]] | None,
|
|
77
|
+
) -> dict[str, ToolPolicy]:
|
|
78
|
+
resolved: dict[str, ToolPolicy] = {}
|
|
79
|
+
for tool_name, policy in (policies or {}).items():
|
|
80
|
+
resolved[tool_name] = (
|
|
81
|
+
policy if isinstance(policy, ToolPolicy) else ToolPolicy.model_validate(policy)
|
|
82
|
+
)
|
|
83
|
+
return resolved
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _coerce_mcp_settings(
|
|
87
|
+
items: list[MCPSettings | dict[str, Any]]
|
|
88
|
+
) -> list[MCPSettings]:
|
|
89
|
+
resolved: list[MCPSettings] = []
|
|
90
|
+
for item in items:
|
|
91
|
+
resolved.append(
|
|
92
|
+
item if isinstance(item, MCPSettings) else MCPSettings.model_validate(item)
|
|
93
|
+
)
|
|
94
|
+
return resolved
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _resolve_string_setting(
|
|
98
|
+
*,
|
|
99
|
+
explicit: str | None,
|
|
100
|
+
env_var: str | None = None,
|
|
101
|
+
default: str | None = None,
|
|
102
|
+
) -> str | None:
|
|
103
|
+
"""统一处理“显式值 -> 环境变量 -> 默认值”的解析顺序。"""
|
|
104
|
+
|
|
105
|
+
if explicit is not None:
|
|
106
|
+
return explicit
|
|
107
|
+
if env_var:
|
|
108
|
+
env_value = os.getenv(env_var)
|
|
109
|
+
if env_value:
|
|
110
|
+
return env_value
|
|
111
|
+
return default
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class BaseSDKAgent(ABC):
|
|
115
|
+
"""类方式 agent 的最小生命周期基类。
|
|
116
|
+
|
|
117
|
+
这个类不关心业务字段长什么样,只约束一个核心事实:
|
|
118
|
+
|
|
119
|
+
- Python 类方式最终必须先收敛为 `AgentDefinition`
|
|
120
|
+
|
|
121
|
+
这样:
|
|
122
|
+
- YAML 入口和 Python 入口共用同一个 definition/spec 层
|
|
123
|
+
- `AgentBuilder.from_definition(...)` 成为统一装配路径
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def definition(self) -> AgentDefinition:
|
|
127
|
+
"""返回当前 agent 的规范化 definition。
|
|
128
|
+
|
|
129
|
+
这里把 `build_definition()` 的结果统一转成 `AgentDefinition`,
|
|
130
|
+
并给子类一个最终修改 definition 的扩展点。
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
raw = self.build_definition()
|
|
134
|
+
definition = (
|
|
135
|
+
raw if isinstance(raw, AgentDefinition) else AgentDefinition.model_validate(raw)
|
|
136
|
+
)
|
|
137
|
+
# 统一用深拷贝,避免子类返回共享对象后被后续调用原地改坏。
|
|
138
|
+
definition = definition.model_copy(deep=True)
|
|
139
|
+
customized = self.customize_definition(definition)
|
|
140
|
+
return definition if customized is None else customized
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
def build_definition(self) -> AgentDefinition | dict[str, Any]:
|
|
144
|
+
"""构造当前 agent 的 definition/spec。"""
|
|
145
|
+
|
|
146
|
+
def builder(self) -> AgentBuilder:
|
|
147
|
+
"""把当前类式 agent 转成 `AgentBuilder`。
|
|
148
|
+
|
|
149
|
+
这个方法是类方式和 builder 方式之间的桥接点。
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
builder = AgentBuilder.from_definition(self.definition())
|
|
153
|
+
|
|
154
|
+
explicit_model_api_key = self.model_api_key()
|
|
155
|
+
if explicit_model_api_key is not None:
|
|
156
|
+
builder.model(api_key=explicit_model_api_key)
|
|
157
|
+
|
|
158
|
+
discoverer = self.discoverer()
|
|
159
|
+
if discoverer is not None:
|
|
160
|
+
builder.discoverer(discoverer)
|
|
161
|
+
|
|
162
|
+
plugin = self.plugin_instance()
|
|
163
|
+
if plugin is not None:
|
|
164
|
+
builder.plugin(plugin)
|
|
165
|
+
|
|
166
|
+
self.customize_builder(builder)
|
|
167
|
+
return builder
|
|
168
|
+
|
|
169
|
+
def build_runtime(self):
|
|
170
|
+
"""构造并返回 `ManagedAgentRuntime`。"""
|
|
171
|
+
|
|
172
|
+
return self.builder().build()
|
|
173
|
+
|
|
174
|
+
def build_app(
|
|
175
|
+
self,
|
|
176
|
+
*,
|
|
177
|
+
enable_management: bool = False,
|
|
178
|
+
public_url: str | None = None,
|
|
179
|
+
):
|
|
180
|
+
"""构造并返回可直接挂到 uvicorn 的 A2A app。"""
|
|
181
|
+
|
|
182
|
+
return self.builder().build_a2a_app(
|
|
183
|
+
public_url=public_url,
|
|
184
|
+
enable_management=enable_management,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def model_api_key(self) -> str | None:
|
|
188
|
+
"""可选:返回只存在于运行时的显式 API Key。
|
|
189
|
+
|
|
190
|
+
之所以不放进 `AgentDefinition`,是因为 definition 适合作为配置规格,
|
|
191
|
+
而不是密钥载体。
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def plugin_instance(self) -> BaseAgentPlugin | None:
|
|
197
|
+
"""可选:返回业务 plugin 实例。
|
|
198
|
+
|
|
199
|
+
如果这里返回实例,会覆盖 definition 里的 `runtime.plugin` 加载路径。
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
def discoverer(self) -> Callable[..., list[DiscoveredTool]] | None:
|
|
205
|
+
"""可选:返回自定义 MCP discoverer。"""
|
|
206
|
+
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def customize_definition(
|
|
210
|
+
self, definition: AgentDefinition
|
|
211
|
+
) -> AgentDefinition | None:
|
|
212
|
+
"""可选:在 definition 完成后做最后修正。"""
|
|
213
|
+
|
|
214
|
+
return definition
|
|
215
|
+
|
|
216
|
+
def customize_builder(self, builder: AgentBuilder) -> None:
|
|
217
|
+
"""可选:在 builder 完成后做最后修正。"""
|
|
218
|
+
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class DeclarativeSDKAgent(BaseSDKAgent):
|
|
223
|
+
"""适合“我已经有完整 definition,只想走类入口”的场景。"""
|
|
224
|
+
|
|
225
|
+
@abstractmethod
|
|
226
|
+
def declared_definition(self) -> AgentDefinition | dict[str, Any]:
|
|
227
|
+
"""返回完整 definition。"""
|
|
228
|
+
|
|
229
|
+
def build_definition(self) -> AgentDefinition | dict[str, Any]:
|
|
230
|
+
return self.declared_definition()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class ConfiguredSDKAgent(BaseSDKAgent):
|
|
234
|
+
"""适合通过细粒度 hook 组装 definition 的通用基类。
|
|
235
|
+
|
|
236
|
+
这层的重点不是“业务一定要全部重写”,而是把常见变更点拆小:
|
|
237
|
+
|
|
238
|
+
- 身份信息拆成 `agent_id/name/description/version`
|
|
239
|
+
- 模型拆成 provider/base/model_id/api_key_env/max_steps
|
|
240
|
+
- A2A 展示层拆成 skills 和输入输出模式
|
|
241
|
+
- MCP 拆成一个或多个 `MCPSettings`
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
def build_definition(self) -> AgentDefinition:
|
|
245
|
+
return AgentDefinition(
|
|
246
|
+
agent=_coerce_agent_config(self.agent_config()),
|
|
247
|
+
runtime=_coerce_runtime_config(self.runtime_config()),
|
|
248
|
+
mcps=_coerce_mcp_settings(self.mcp_settings()),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def agent_config(self) -> AgentConfig | dict[str, Any]:
|
|
252
|
+
return AgentConfig(
|
|
253
|
+
agent_id=self.agent_id(),
|
|
254
|
+
name=self.agent_name(),
|
|
255
|
+
description=self.agent_description(),
|
|
256
|
+
version=self.agent_version(),
|
|
257
|
+
a2a=self.a2a_settings(),
|
|
258
|
+
extra_instructions=self.extra_instructions(),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def runtime_config(self) -> RuntimeConfig | dict[str, Any]:
|
|
262
|
+
return RuntimeConfig(
|
|
263
|
+
base_path=self.base_path(),
|
|
264
|
+
public_base_url=self.public_base_url(),
|
|
265
|
+
model=_coerce_model_settings(self.model_settings()),
|
|
266
|
+
plugin=_coerce_plugin_settings(self.plugin_settings()),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def model_settings(self) -> ModelSettings | dict[str, Any]:
|
|
270
|
+
return ModelSettings(
|
|
271
|
+
provider=self.model_provider(),
|
|
272
|
+
api_base=self.model_api_base(),
|
|
273
|
+
model_id=self.model_id(),
|
|
274
|
+
api_key_env=self.model_api_key_env(),
|
|
275
|
+
max_steps=self.model_max_steps(),
|
|
276
|
+
flatten_messages_as_text=self.model_flatten_messages_as_text(),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def a2a_settings(self) -> A2ASettings:
|
|
280
|
+
return A2ASettings(
|
|
281
|
+
skills=[_coerce_skill_definition(skill) for skill in self.a2a_skills()],
|
|
282
|
+
default_input_modes=list(self.a2a_default_input_modes()),
|
|
283
|
+
default_output_modes=list(self.a2a_default_output_modes()),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
@abstractmethod
|
|
287
|
+
def agent_id(self) -> str:
|
|
288
|
+
"""返回稳定的内部 agent 标识。"""
|
|
289
|
+
|
|
290
|
+
def agent_name(self) -> str:
|
|
291
|
+
"""返回展示名称。
|
|
292
|
+
|
|
293
|
+
默认回退到 `agent_id()`,让最小子类只实现一个必填钩子也能工作。
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
return self.agent_id()
|
|
297
|
+
|
|
298
|
+
def agent_description(self) -> str:
|
|
299
|
+
"""返回展示描述。默认回退到 `agent_name()`。"""
|
|
300
|
+
|
|
301
|
+
return self.agent_name()
|
|
302
|
+
|
|
303
|
+
def agent_version(self) -> str:
|
|
304
|
+
return "0.1.0"
|
|
305
|
+
|
|
306
|
+
def extra_instructions(self) -> str:
|
|
307
|
+
return ""
|
|
308
|
+
|
|
309
|
+
def a2a_skills(self) -> list[SkillDefinition | dict[str, Any]]:
|
|
310
|
+
return []
|
|
311
|
+
|
|
312
|
+
def a2a_default_input_modes(self) -> list[str]:
|
|
313
|
+
return ["text"]
|
|
314
|
+
|
|
315
|
+
def a2a_default_output_modes(self) -> list[str]:
|
|
316
|
+
return ["text"]
|
|
317
|
+
|
|
318
|
+
def base_path(self) -> str:
|
|
319
|
+
return "."
|
|
320
|
+
|
|
321
|
+
def public_base_url(self) -> str | None:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
def model_provider(self) -> str:
|
|
325
|
+
return "openai_compatible"
|
|
326
|
+
|
|
327
|
+
def model_api_base(self) -> str | None:
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def model_id(self) -> str | None:
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
def model_api_key_env(self) -> str:
|
|
334
|
+
return "MODEL_SOURCE_API_KEY"
|
|
335
|
+
|
|
336
|
+
def model_max_steps(self) -> int:
|
|
337
|
+
return 15
|
|
338
|
+
|
|
339
|
+
def model_flatten_messages_as_text(self) -> bool:
|
|
340
|
+
return True
|
|
341
|
+
|
|
342
|
+
def plugin_settings(self) -> PluginSettings | dict[str, Any] | None:
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
@abstractmethod
|
|
346
|
+
def mcp_settings(self) -> list[MCPSettings | dict[str, Any]]:
|
|
347
|
+
"""返回一个或多个 MCP 绑定。"""
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class SingleMCPAgent(ConfiguredSDKAgent):
|
|
351
|
+
"""单 MCP 场景的半成品基类。"""
|
|
352
|
+
|
|
353
|
+
def mcp_settings(self) -> list[MCPSettings]:
|
|
354
|
+
return [
|
|
355
|
+
MCPSettings(
|
|
356
|
+
name=self.mcp_name(),
|
|
357
|
+
url=self.mcp_url(),
|
|
358
|
+
transport=self.mcp_transport(),
|
|
359
|
+
enabled=self.mcp_enabled(),
|
|
360
|
+
retry_count=self.mcp_retry_count(),
|
|
361
|
+
retry_delay_seconds=self.mcp_retry_delay_seconds(),
|
|
362
|
+
static_headers=dict(self.mcp_static_headers()),
|
|
363
|
+
pass_through_headers=list(self.mcp_pass_through_headers()),
|
|
364
|
+
tool_policies=_coerce_tool_policies(self.mcp_tool_policies()),
|
|
365
|
+
)
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
@abstractmethod
|
|
369
|
+
def mcp_name(self) -> str:
|
|
370
|
+
"""返回单个 MCP 的名称。"""
|
|
371
|
+
|
|
372
|
+
@abstractmethod
|
|
373
|
+
def mcp_url(self) -> str:
|
|
374
|
+
"""返回单个 MCP 的地址。"""
|
|
375
|
+
|
|
376
|
+
def mcp_transport(self) -> str:
|
|
377
|
+
return "streamable-http"
|
|
378
|
+
|
|
379
|
+
def mcp_enabled(self) -> bool:
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
def mcp_retry_count(self) -> int:
|
|
383
|
+
return 3
|
|
384
|
+
|
|
385
|
+
def mcp_retry_delay_seconds(self) -> float:
|
|
386
|
+
return 1.0
|
|
387
|
+
|
|
388
|
+
def mcp_static_headers(self) -> dict[str, str]:
|
|
389
|
+
return {}
|
|
390
|
+
|
|
391
|
+
def mcp_pass_through_headers(self) -> list[str]:
|
|
392
|
+
return list(DEFAULT_PASS_THROUGH_HEADERS)
|
|
393
|
+
|
|
394
|
+
def mcp_tool_policies(self) -> dict[str, ToolPolicy | dict[str, Any]]:
|
|
395
|
+
return {}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class OpenAICompatibleSingleMCPAgent(SingleMCPAgent):
|
|
399
|
+
"""单 MCP + OpenAI-compatible 模型的常用半成品基类。
|
|
400
|
+
|
|
401
|
+
这个类面向业务开发者的目标是:
|
|
402
|
+
|
|
403
|
+
- 不用关心 `AgentBuilder` 细节
|
|
404
|
+
- 常见配置支持“显式参数 -> 环境变量 -> 默认值”三级覆盖
|
|
405
|
+
- 只需要覆写少量语义化小钩子
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
def __init__(
|
|
409
|
+
self,
|
|
410
|
+
*,
|
|
411
|
+
public_base_url: str | None = None,
|
|
412
|
+
model_api_base: str | None = None,
|
|
413
|
+
model_id: str | None = None,
|
|
414
|
+
model_api_key: str | None = None,
|
|
415
|
+
mcp_url: str | None = None,
|
|
416
|
+
mcp_transport: str | None = None,
|
|
417
|
+
mcp_retry_count: int | None = None,
|
|
418
|
+
mcp_retry_delay_seconds: float | None = None,
|
|
419
|
+
max_steps: int | None = None,
|
|
420
|
+
flatten_messages_as_text: bool | None = None,
|
|
421
|
+
) -> None:
|
|
422
|
+
self._public_base_url_override = public_base_url
|
|
423
|
+
self._model_api_base_override = model_api_base
|
|
424
|
+
self._model_id_override = model_id
|
|
425
|
+
self._model_api_key_override = model_api_key
|
|
426
|
+
self._mcp_url_override = mcp_url
|
|
427
|
+
self._mcp_transport_override = mcp_transport
|
|
428
|
+
self._mcp_retry_count_override = mcp_retry_count
|
|
429
|
+
self._mcp_retry_delay_seconds_override = mcp_retry_delay_seconds
|
|
430
|
+
self._model_max_steps_override = max_steps
|
|
431
|
+
self._flatten_messages_as_text_override = flatten_messages_as_text
|
|
432
|
+
|
|
433
|
+
def public_base_url(self) -> str | None:
|
|
434
|
+
return _resolve_string_setting(
|
|
435
|
+
explicit=self._public_base_url_override,
|
|
436
|
+
env_var=self.public_base_url_env_var(),
|
|
437
|
+
default=self.default_public_base_url(),
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
def model_provider(self) -> str:
|
|
441
|
+
return "openai_compatible"
|
|
442
|
+
|
|
443
|
+
def model_api_base(self) -> str | None:
|
|
444
|
+
return _resolve_string_setting(
|
|
445
|
+
explicit=self._model_api_base_override,
|
|
446
|
+
env_var=self.model_api_base_env_var(),
|
|
447
|
+
default=self.default_model_api_base(),
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def model_id(self) -> str | None:
|
|
451
|
+
return _resolve_string_setting(
|
|
452
|
+
explicit=self._model_id_override,
|
|
453
|
+
env_var=self.model_id_env_var(),
|
|
454
|
+
default=self.default_model_id(),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def model_api_key(self) -> str | None:
|
|
458
|
+
# 这里有意只返回显式传入的 key。
|
|
459
|
+
#
|
|
460
|
+
# 正常情况下,SDK 仍建议通过 `model_api_key_env()` 指定环境变量读取。
|
|
461
|
+
# 只有业务方明确想在代码里塞一个临时 key 时,才走这个运行时 override。
|
|
462
|
+
return self._model_api_key_override
|
|
463
|
+
|
|
464
|
+
def model_max_steps(self) -> int:
|
|
465
|
+
if self._model_max_steps_override is not None:
|
|
466
|
+
return self._model_max_steps_override
|
|
467
|
+
return self.default_model_max_steps()
|
|
468
|
+
|
|
469
|
+
def model_flatten_messages_as_text(self) -> bool:
|
|
470
|
+
if self._flatten_messages_as_text_override is not None:
|
|
471
|
+
return self._flatten_messages_as_text_override
|
|
472
|
+
return self.default_model_flatten_messages_as_text()
|
|
473
|
+
|
|
474
|
+
def mcp_url(self) -> str:
|
|
475
|
+
value = _resolve_string_setting(
|
|
476
|
+
explicit=self._mcp_url_override,
|
|
477
|
+
env_var=self.mcp_url_env_var(),
|
|
478
|
+
default=self.default_mcp_url(),
|
|
479
|
+
)
|
|
480
|
+
if not value:
|
|
481
|
+
raise ValueError(
|
|
482
|
+
"mcp_url is not configured; pass mcp_url=..., "
|
|
483
|
+
f"set env {self.mcp_url_env_var()!r}, or override default_mcp_url()."
|
|
484
|
+
)
|
|
485
|
+
return value
|
|
486
|
+
|
|
487
|
+
def mcp_transport(self) -> str:
|
|
488
|
+
value = _resolve_string_setting(
|
|
489
|
+
explicit=self._mcp_transport_override,
|
|
490
|
+
env_var=self.mcp_transport_env_var(),
|
|
491
|
+
default=self.default_mcp_transport(),
|
|
492
|
+
)
|
|
493
|
+
return value or "streamable-http"
|
|
494
|
+
|
|
495
|
+
def mcp_retry_count(self) -> int:
|
|
496
|
+
if self._mcp_retry_count_override is not None:
|
|
497
|
+
return self._mcp_retry_count_override
|
|
498
|
+
return self.default_mcp_retry_count()
|
|
499
|
+
|
|
500
|
+
def mcp_retry_delay_seconds(self) -> float:
|
|
501
|
+
if self._mcp_retry_delay_seconds_override is not None:
|
|
502
|
+
return self._mcp_retry_delay_seconds_override
|
|
503
|
+
return self.default_mcp_retry_delay_seconds()
|
|
504
|
+
|
|
505
|
+
def public_base_url_env_var(self) -> str | None:
|
|
506
|
+
return "AGENT_PUBLIC_BASE_URL"
|
|
507
|
+
|
|
508
|
+
def default_public_base_url(self) -> str | None:
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
def model_api_base_env_var(self) -> str | None:
|
|
512
|
+
return "MODEL_SOURCE_API_BASE"
|
|
513
|
+
|
|
514
|
+
def default_model_api_base(self) -> str | None:
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
def model_id_env_var(self) -> str | None:
|
|
518
|
+
return "MODEL_SOURCE_MODEL_ID"
|
|
519
|
+
|
|
520
|
+
def default_model_id(self) -> str | None:
|
|
521
|
+
return None
|
|
522
|
+
|
|
523
|
+
def model_api_key_env(self) -> str:
|
|
524
|
+
return "MODEL_SOURCE_API_KEY"
|
|
525
|
+
|
|
526
|
+
def default_model_max_steps(self) -> int:
|
|
527
|
+
return 15
|
|
528
|
+
|
|
529
|
+
def default_model_flatten_messages_as_text(self) -> bool:
|
|
530
|
+
return True
|
|
531
|
+
|
|
532
|
+
def mcp_url_env_var(self) -> str | None:
|
|
533
|
+
return None
|
|
534
|
+
|
|
535
|
+
def default_mcp_url(self) -> str | None:
|
|
536
|
+
return None
|
|
537
|
+
|
|
538
|
+
def mcp_transport_env_var(self) -> str | None:
|
|
539
|
+
return None
|
|
540
|
+
|
|
541
|
+
def default_mcp_transport(self) -> str | None:
|
|
542
|
+
return "streamable-http"
|
|
543
|
+
|
|
544
|
+
def default_mcp_retry_count(self) -> int:
|
|
545
|
+
return 3
|
|
546
|
+
|
|
547
|
+
def default_mcp_retry_delay_seconds(self) -> float:
|
|
548
|
+
return 1.0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""服务装配层内部实现。"""
|
|
2
|
+
|
|
3
|
+
from .app_factory import build_app_from_runtime
|
|
4
|
+
from .bootstrap import DEFAULT_DEFINITION_PATH, load_single_agent_runtime
|
|
5
|
+
from .endpoint_utils import port_from_public_base_url, resolve_public_base_url
|
|
6
|
+
from .management import create_management_router
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"DEFAULT_DEFINITION_PATH",
|
|
10
|
+
"build_app_from_runtime",
|
|
11
|
+
"create_management_router",
|
|
12
|
+
"load_single_agent_runtime",
|
|
13
|
+
"port_from_public_base_url",
|
|
14
|
+
"resolve_public_base_url",
|
|
15
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
|
|
5
|
+
from ..runtime import ManagedAgentRuntime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_app_from_runtime(
|
|
9
|
+
runtime: ManagedAgentRuntime,
|
|
10
|
+
enable_management: bool = True,
|
|
11
|
+
) -> FastAPI:
|
|
12
|
+
"""把已经构造好的 runtime 包成 FastAPI 应用。"""
|
|
13
|
+
|
|
14
|
+
if runtime.load_error:
|
|
15
|
+
if runtime.load_exception is not None:
|
|
16
|
+
raise runtime.load_exception
|
|
17
|
+
raise RuntimeError(runtime.load_error)
|
|
18
|
+
|
|
19
|
+
app = FastAPI(title=runtime.definition.agent.name)
|
|
20
|
+
app.state.runtime = runtime
|
|
21
|
+
|
|
22
|
+
@app.get("/healthz")
|
|
23
|
+
def healthz():
|
|
24
|
+
return {
|
|
25
|
+
"status": runtime.status,
|
|
26
|
+
"agent_id": runtime.definition.agent.agent_id,
|
|
27
|
+
"tool_count": len(runtime.discovered_tools),
|
|
28
|
+
"load_error": runtime.load_error,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if enable_management:
|
|
32
|
+
from .management import create_management_router
|
|
33
|
+
|
|
34
|
+
app.include_router(create_management_router(runtime))
|
|
35
|
+
|
|
36
|
+
app.mount("/", runtime.get_asgi_app())
|
|
37
|
+
return app
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .endpoint_utils import resolve_public_base_url
|
|
7
|
+
from ..config.storage import AgentDefinitionStore
|
|
8
|
+
from ..runtime import ManagedAgentRuntime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_DEFINITION_PATH = Path.cwd() / "agent_app" / "agent.yaml"
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_single_agent_runtime(
|
|
16
|
+
definition_path: str | Path = DEFAULT_DEFINITION_PATH,
|
|
17
|
+
public_base_url: str | None = None,
|
|
18
|
+
bind_port: int | None = None,
|
|
19
|
+
discoverer=None,
|
|
20
|
+
) -> ManagedAgentRuntime:
|
|
21
|
+
"""加载一个 definition,并立即完成 runtime reload。"""
|
|
22
|
+
|
|
23
|
+
logger.info("Loading single-agent runtime definition_path=%s", definition_path)
|
|
24
|
+
definition = AgentDefinitionStore(Path(definition_path).resolve().parent).load_path(definition_path)
|
|
25
|
+
resolved_public_base_url = resolve_public_base_url(
|
|
26
|
+
definition,
|
|
27
|
+
public_base_url=public_base_url,
|
|
28
|
+
bind_port=bind_port,
|
|
29
|
+
)
|
|
30
|
+
runtime = ManagedAgentRuntime(
|
|
31
|
+
definition=definition,
|
|
32
|
+
public_base_url=resolved_public_base_url,
|
|
33
|
+
discoverer=discoverer,
|
|
34
|
+
).reload()
|
|
35
|
+
if runtime.load_error:
|
|
36
|
+
logger.error(
|
|
37
|
+
"Runtime loaded with error agent_id=%s load_error=%s",
|
|
38
|
+
runtime.definition.agent.agent_id,
|
|
39
|
+
runtime.load_error,
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
logger.info(
|
|
43
|
+
"Runtime ready agent_id=%s public_base_url=%s discovered_tools=%s",
|
|
44
|
+
runtime.definition.agent.agent_id,
|
|
45
|
+
runtime.public_url(),
|
|
46
|
+
len(runtime.discovered_tools),
|
|
47
|
+
)
|
|
48
|
+
return runtime
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
from ..config.definitions import AgentDefinition
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def port_from_public_base_url(public_base_url: str | None) -> int | None:
|
|
9
|
+
"""从 public_base_url 中提取显式端口。"""
|
|
10
|
+
|
|
11
|
+
normalized = (public_base_url or "").rstrip("/")
|
|
12
|
+
if not normalized:
|
|
13
|
+
return None
|
|
14
|
+
parsed = urlparse(normalized)
|
|
15
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
16
|
+
return None
|
|
17
|
+
return parsed.port
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_public_base_url(
|
|
21
|
+
definition: AgentDefinition,
|
|
22
|
+
*,
|
|
23
|
+
public_base_url: str | None = None,
|
|
24
|
+
bind_port: int | None = None,
|
|
25
|
+
public_host: str = "127.0.0.1",
|
|
26
|
+
) -> str:
|
|
27
|
+
"""解析运行时最终使用的 public_base_url。"""
|
|
28
|
+
|
|
29
|
+
resolved_public_base_url = (public_base_url or definition.runtime.public_base_url or "").rstrip("/")
|
|
30
|
+
if resolved_public_base_url:
|
|
31
|
+
return resolved_public_base_url
|
|
32
|
+
if bind_port is not None and bind_port > 0:
|
|
33
|
+
return f"http://{public_host}:{bind_port}"
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"public_base_url must be configured in agent_app/agent.yaml or passed explicitly; "
|
|
36
|
+
"if you only want to bind a local port, pass bind_port/run_single_agent_app(..., port=...)."
|
|
37
|
+
)
|