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,84 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from .builder import AgentBuilder
|
|
4
|
+
from .definitions import (
|
|
5
|
+
A2ASettings,
|
|
6
|
+
AgentConfig,
|
|
7
|
+
AgentDefinition,
|
|
8
|
+
InputFieldDefinition,
|
|
9
|
+
MCPSettings,
|
|
10
|
+
ModelSettings,
|
|
11
|
+
PluginSettings,
|
|
12
|
+
RuntimeConfig,
|
|
13
|
+
SkillDefinition,
|
|
14
|
+
ToolPolicy,
|
|
15
|
+
)
|
|
16
|
+
from .discovery import DiscoveredTool
|
|
17
|
+
from .exceptions import (
|
|
18
|
+
AgentBuildError,
|
|
19
|
+
AgentRuntimeError,
|
|
20
|
+
DefinitionLoadError,
|
|
21
|
+
MCPToolLoadError,
|
|
22
|
+
PluginLoadError,
|
|
23
|
+
TaskCancelledError,
|
|
24
|
+
TaskWaitTimeoutError,
|
|
25
|
+
UserCancelledError,
|
|
26
|
+
)
|
|
27
|
+
from .model_registry import ModelRegistry
|
|
28
|
+
from .plugins.base import BaseAgentPlugin, BeforeToolDecision, ToolExecutionContext
|
|
29
|
+
from .plugins.default import DefaultPlugin
|
|
30
|
+
from .runtime import ManagedAgentRuntime
|
|
31
|
+
from .sdk_agent import (
|
|
32
|
+
BaseSDKAgent,
|
|
33
|
+
ConfiguredSDKAgent,
|
|
34
|
+
DeclarativeSDKAgent,
|
|
35
|
+
OpenAICompatibleSingleMCPAgent,
|
|
36
|
+
SingleMCPAgent,
|
|
37
|
+
)
|
|
38
|
+
from .server.bootstrap import load_single_agent_runtime
|
|
39
|
+
from .standalone import build_single_agent_app, run_single_agent_app
|
|
40
|
+
from .storage import AgentDefinitionStore
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
__version__ = version("agent-runtime-sdk")
|
|
44
|
+
except PackageNotFoundError: # pragma: no cover - source tree without installed metadata
|
|
45
|
+
__version__ = "0.1.0"
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"__version__",
|
|
49
|
+
"A2ASettings",
|
|
50
|
+
"AgentConfig",
|
|
51
|
+
"AgentBuilder",
|
|
52
|
+
"AgentBuildError",
|
|
53
|
+
"AgentDefinition",
|
|
54
|
+
"AgentDefinitionStore",
|
|
55
|
+
"AgentRuntimeError",
|
|
56
|
+
"BaseAgentPlugin",
|
|
57
|
+
"BaseSDKAgent",
|
|
58
|
+
"ConfiguredSDKAgent",
|
|
59
|
+
"DeclarativeSDKAgent",
|
|
60
|
+
"BeforeToolDecision",
|
|
61
|
+
"DefinitionLoadError",
|
|
62
|
+
"DefaultPlugin",
|
|
63
|
+
"InputFieldDefinition",
|
|
64
|
+
"MCPToolLoadError",
|
|
65
|
+
"MCPSettings",
|
|
66
|
+
"ManagedAgentRuntime",
|
|
67
|
+
"ModelSettings",
|
|
68
|
+
"ModelRegistry",
|
|
69
|
+
"OpenAICompatibleSingleMCPAgent",
|
|
70
|
+
"PluginSettings",
|
|
71
|
+
"PluginLoadError",
|
|
72
|
+
"RuntimeConfig",
|
|
73
|
+
"SkillDefinition",
|
|
74
|
+
"SingleMCPAgent",
|
|
75
|
+
"TaskCancelledError",
|
|
76
|
+
"TaskWaitTimeoutError",
|
|
77
|
+
"DiscoveredTool",
|
|
78
|
+
"ToolExecutionContext",
|
|
79
|
+
"ToolPolicy",
|
|
80
|
+
"UserCancelledError",
|
|
81
|
+
"build_single_agent_app",
|
|
82
|
+
"load_single_agent_runtime",
|
|
83
|
+
"run_single_agent_app",
|
|
84
|
+
]
|
agent_runtime/builder.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""面向使用者的代码式构建入口。"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
from .config.definitions import (
|
|
12
|
+
DEFAULT_PASS_THROUGH_HEADERS,
|
|
13
|
+
AgentConfig,
|
|
14
|
+
AgentDefinition,
|
|
15
|
+
InputFieldDefinition,
|
|
16
|
+
MCPSettings,
|
|
17
|
+
ModelSettings,
|
|
18
|
+
PluginSettings,
|
|
19
|
+
RuntimeConfig,
|
|
20
|
+
SkillDefinition,
|
|
21
|
+
ToolPolicy,
|
|
22
|
+
)
|
|
23
|
+
from .mcp.metadata import DiscoveredTool
|
|
24
|
+
from .plugins.base import BaseAgentPlugin
|
|
25
|
+
from .runtime import ManagedAgentRuntime
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AgentBuilder:
|
|
29
|
+
"""代码式配置入口。"""
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self._agent = AgentConfig(
|
|
33
|
+
agent_id="mcp_agent",
|
|
34
|
+
name="",
|
|
35
|
+
description="",
|
|
36
|
+
version="0.1.0",
|
|
37
|
+
)
|
|
38
|
+
self._runtime = RuntimeConfig(
|
|
39
|
+
base_path=".",
|
|
40
|
+
public_base_url=None,
|
|
41
|
+
model=ModelSettings(),
|
|
42
|
+
)
|
|
43
|
+
self._mcps: list[MCPSettings] = []
|
|
44
|
+
|
|
45
|
+
self._plugin_instance: BaseAgentPlugin | None = None
|
|
46
|
+
self._discoverer: Callable[..., list[DiscoveredTool]] | None = None
|
|
47
|
+
self._explicit_model_api_key: str | None = None
|
|
48
|
+
self._auto_agent_id = True
|
|
49
|
+
|
|
50
|
+
def model(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
provider: str | None = None,
|
|
54
|
+
api_base: str | None = None,
|
|
55
|
+
model_id: str | None = None,
|
|
56
|
+
api_key: str | None = None,
|
|
57
|
+
api_key_env: str | None = None,
|
|
58
|
+
max_steps: int | None = None,
|
|
59
|
+
flatten_messages_as_text: bool | None = None,
|
|
60
|
+
) -> AgentBuilder:
|
|
61
|
+
model = self._runtime.model
|
|
62
|
+
if provider is not None:
|
|
63
|
+
model.provider = provider
|
|
64
|
+
if api_base is not None:
|
|
65
|
+
model.api_base = api_base
|
|
66
|
+
if model_id is not None:
|
|
67
|
+
model.model_id = model_id
|
|
68
|
+
if api_key is not None:
|
|
69
|
+
self._explicit_model_api_key = api_key
|
|
70
|
+
if api_key_env is not None:
|
|
71
|
+
model.api_key_env = api_key_env
|
|
72
|
+
if max_steps is not None:
|
|
73
|
+
model.max_steps = max_steps
|
|
74
|
+
if flatten_messages_as_text is not None:
|
|
75
|
+
model.flatten_messages_as_text = flatten_messages_as_text
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def tool_policy(
|
|
79
|
+
self,
|
|
80
|
+
tool_name: str,
|
|
81
|
+
*,
|
|
82
|
+
mcp_name: str | None = None,
|
|
83
|
+
requires_confirmation: bool = False,
|
|
84
|
+
prompt: str | None = None,
|
|
85
|
+
enabled: bool = True,
|
|
86
|
+
allow_arg_override: bool = True,
|
|
87
|
+
input_fields: list[InputFieldDefinition | dict[str, Any]] | None = None,
|
|
88
|
+
) -> AgentBuilder:
|
|
89
|
+
fields: list[InputFieldDefinition] = []
|
|
90
|
+
if input_fields:
|
|
91
|
+
for field in input_fields:
|
|
92
|
+
if isinstance(field, InputFieldDefinition):
|
|
93
|
+
fields.append(field)
|
|
94
|
+
else:
|
|
95
|
+
fields.append(InputFieldDefinition.model_validate(field))
|
|
96
|
+
target_mcp = self._resolve_policy_target_mcp(mcp_name)
|
|
97
|
+
target_mcp.tool_policies[tool_name] = ToolPolicy(
|
|
98
|
+
enabled=enabled,
|
|
99
|
+
requires_confirmation=requires_confirmation,
|
|
100
|
+
input_fields=fields,
|
|
101
|
+
prompt=prompt,
|
|
102
|
+
allow_arg_override=allow_arg_override,
|
|
103
|
+
)
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def plugin(self, plugin: BaseAgentPlugin) -> AgentBuilder:
|
|
107
|
+
self._plugin_instance = plugin
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def instructions(self, text: str) -> AgentBuilder:
|
|
111
|
+
self._agent.extra_instructions = text
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def max_steps(self, n: int) -> AgentBuilder:
|
|
115
|
+
self._runtime.model.max_steps = n
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def agent_info(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
agent_id: str | None = None,
|
|
122
|
+
name: str | None = None,
|
|
123
|
+
description: str | None = None,
|
|
124
|
+
version: str | None = None,
|
|
125
|
+
) -> AgentBuilder:
|
|
126
|
+
if agent_id is not None:
|
|
127
|
+
self._agent.agent_id = agent_id
|
|
128
|
+
self._auto_agent_id = False
|
|
129
|
+
if name is not None:
|
|
130
|
+
self._agent.name = name
|
|
131
|
+
if description is not None:
|
|
132
|
+
self._agent.description = description
|
|
133
|
+
if version is not None:
|
|
134
|
+
self._agent.version = version
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def add_mcp(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
name: str,
|
|
141
|
+
url: str,
|
|
142
|
+
transport: str = "streamable-http",
|
|
143
|
+
enabled: bool = True,
|
|
144
|
+
retry_count: int = 3,
|
|
145
|
+
retry_delay_seconds: float = 1.0,
|
|
146
|
+
static_headers: dict[str, str] | None = None,
|
|
147
|
+
pass_through_headers: list[str] | None = None,
|
|
148
|
+
tool_policies: dict[str, ToolPolicy | dict[str, Any]] | None = None,
|
|
149
|
+
) -> AgentBuilder:
|
|
150
|
+
policies: dict[str, ToolPolicy] = {}
|
|
151
|
+
if tool_policies:
|
|
152
|
+
for tool_name, policy in tool_policies.items():
|
|
153
|
+
policies[tool_name] = (
|
|
154
|
+
policy
|
|
155
|
+
if isinstance(policy, ToolPolicy)
|
|
156
|
+
else ToolPolicy.model_validate(policy)
|
|
157
|
+
)
|
|
158
|
+
self._mcps.append(
|
|
159
|
+
MCPSettings(
|
|
160
|
+
name=name,
|
|
161
|
+
url=url,
|
|
162
|
+
transport=transport,
|
|
163
|
+
enabled=enabled,
|
|
164
|
+
retry_count=retry_count,
|
|
165
|
+
retry_delay_seconds=retry_delay_seconds,
|
|
166
|
+
static_headers=static_headers or {},
|
|
167
|
+
pass_through_headers=pass_through_headers
|
|
168
|
+
or list(DEFAULT_PASS_THROUGH_HEADERS),
|
|
169
|
+
tool_policies=policies,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
def a2a_skill(
|
|
175
|
+
self,
|
|
176
|
+
*,
|
|
177
|
+
id: str,
|
|
178
|
+
name: str,
|
|
179
|
+
description: str,
|
|
180
|
+
tags: list[str] | None = None,
|
|
181
|
+
examples: list[str] | None = None,
|
|
182
|
+
) -> AgentBuilder:
|
|
183
|
+
self._agent.a2a.skills.append(
|
|
184
|
+
SkillDefinition(
|
|
185
|
+
id=id,
|
|
186
|
+
name=name,
|
|
187
|
+
description=description,
|
|
188
|
+
tags=tags or [],
|
|
189
|
+
examples=examples or [],
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def public_base_url(self, url: str) -> AgentBuilder:
|
|
195
|
+
self._runtime.public_base_url = url
|
|
196
|
+
return self
|
|
197
|
+
|
|
198
|
+
def discoverer(self, func: Callable[..., list[DiscoveredTool]]) -> AgentBuilder:
|
|
199
|
+
self._discoverer = func
|
|
200
|
+
return self
|
|
201
|
+
|
|
202
|
+
def build(self) -> ManagedAgentRuntime:
|
|
203
|
+
definition = self.build_definition()
|
|
204
|
+
if self._explicit_model_api_key is not None:
|
|
205
|
+
os.environ[definition.runtime.model.api_key_env] = (
|
|
206
|
+
self._explicit_model_api_key
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
runtime = ManagedAgentRuntime(
|
|
210
|
+
definition=definition,
|
|
211
|
+
public_base_url=definition.runtime.public_base_url or "",
|
|
212
|
+
discoverer=self._discoverer,
|
|
213
|
+
)
|
|
214
|
+
if self._plugin_instance is not None:
|
|
215
|
+
runtime.plugin = self._plugin_instance
|
|
216
|
+
runtime.reload(discover=True, skip_plugin_load=True)
|
|
217
|
+
else:
|
|
218
|
+
runtime.reload(discover=True)
|
|
219
|
+
if runtime.load_error:
|
|
220
|
+
raise RuntimeError(runtime.load_error)
|
|
221
|
+
return runtime
|
|
222
|
+
|
|
223
|
+
def build_a2a_app(
|
|
224
|
+
self, *, public_url: str | None = None, enable_management: bool = True
|
|
225
|
+
):
|
|
226
|
+
from .server.app_factory import build_app_from_runtime
|
|
227
|
+
|
|
228
|
+
if public_url is not None:
|
|
229
|
+
self.public_base_url(public_url)
|
|
230
|
+
runtime = self.build()
|
|
231
|
+
return build_app_from_runtime(runtime, enable_management=enable_management)
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def from_yaml(cls, path: str | Path) -> AgentBuilder:
|
|
235
|
+
from .config.storage import AgentDefinitionStore
|
|
236
|
+
|
|
237
|
+
resolved = Path(path).resolve()
|
|
238
|
+
definition = AgentDefinitionStore(resolved.parent).load_path(resolved)
|
|
239
|
+
return cls.from_definition(definition)
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def from_definition(cls, definition: AgentDefinition) -> AgentBuilder:
|
|
243
|
+
builder = cls()
|
|
244
|
+
builder._agent = definition.agent.model_copy(deep=True)
|
|
245
|
+
builder._runtime = definition.runtime.model_copy(deep=True)
|
|
246
|
+
builder._mcps = [mcp.model_copy(deep=True) for mcp in definition.mcps]
|
|
247
|
+
builder._auto_agent_id = False
|
|
248
|
+
return builder
|
|
249
|
+
|
|
250
|
+
def build_definition(self) -> AgentDefinition:
|
|
251
|
+
"""返回当前 builder 收敛后的 `AgentDefinition`。
|
|
252
|
+
|
|
253
|
+
这个方法适合:
|
|
254
|
+
|
|
255
|
+
- 需要在真正 `build()` 前检查最终配置
|
|
256
|
+
- Python SDK 层希望把 YAML / 类配置统一收敛到同一个 definition 规格
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
if not self._mcps:
|
|
260
|
+
raise ValueError("at least one MCP server must be added before build()")
|
|
261
|
+
|
|
262
|
+
agent = self._agent.model_copy(deep=True)
|
|
263
|
+
runtime = self._runtime.model_copy(deep=True)
|
|
264
|
+
mcps = [mcp.model_copy(deep=True) for mcp in self._mcps]
|
|
265
|
+
|
|
266
|
+
if self._auto_agent_id:
|
|
267
|
+
agent.agent_id = self._auto_agent_id_from_mcps(mcps)
|
|
268
|
+
if not agent.name:
|
|
269
|
+
agent.name = agent.agent_id
|
|
270
|
+
if not agent.description:
|
|
271
|
+
agent.description = agent.name
|
|
272
|
+
|
|
273
|
+
if runtime.model.api_base is None:
|
|
274
|
+
runtime.model.api_base = os.getenv("MODEL_SOURCE_API_BASE")
|
|
275
|
+
if runtime.model.model_id is None:
|
|
276
|
+
runtime.model.model_id = os.getenv("MODEL_SOURCE_MODEL_ID")
|
|
277
|
+
|
|
278
|
+
if self._plugin_instance is not None:
|
|
279
|
+
runtime.plugin = None
|
|
280
|
+
|
|
281
|
+
return AgentDefinition(
|
|
282
|
+
agent=agent,
|
|
283
|
+
runtime=runtime,
|
|
284
|
+
mcps=mcps,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def _build_definition(self) -> AgentDefinition:
|
|
288
|
+
"""兼容旧内部调用;新代码请优先使用 `build_definition()`。"""
|
|
289
|
+
|
|
290
|
+
return self.build_definition()
|
|
291
|
+
|
|
292
|
+
@staticmethod
|
|
293
|
+
def _auto_agent_id_from_mcps(mcps: list[MCPSettings]) -> str:
|
|
294
|
+
parsed = urlparse(mcps[0].url)
|
|
295
|
+
path_part = (
|
|
296
|
+
parsed.path.strip("/").split("/")[-1] if parsed.path.strip("/") else ""
|
|
297
|
+
)
|
|
298
|
+
if path_part:
|
|
299
|
+
normalized = re.sub(r"[^0-9a-zA-Z_]", "_", path_part)
|
|
300
|
+
return f"mcp_{normalized}" if normalized else "mcp_agent"
|
|
301
|
+
return "mcp_agent"
|
|
302
|
+
|
|
303
|
+
def _resolve_policy_target_mcp(self, mcp_name: str | None) -> MCPSettings:
|
|
304
|
+
if mcp_name is not None:
|
|
305
|
+
for mcp in self._mcps:
|
|
306
|
+
if mcp.name == mcp_name:
|
|
307
|
+
return mcp
|
|
308
|
+
raise ValueError(f"mcp '{mcp_name}' not found")
|
|
309
|
+
if len(self._mcps) == 1:
|
|
310
|
+
return self._mcps[0]
|
|
311
|
+
if not self._mcps:
|
|
312
|
+
raise ValueError(
|
|
313
|
+
"add an MCP before tool_policy(), or pass mcp_name after configuring MCPs"
|
|
314
|
+
)
|
|
315
|
+
raise ValueError(
|
|
316
|
+
"multiple MCPs configured; please specify mcp_name when setting tool_policy()"
|
|
317
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""配置层内部实现。"""
|
|
2
|
+
|
|
3
|
+
from .definitions import (
|
|
4
|
+
A2ASettings,
|
|
5
|
+
AgentConfig,
|
|
6
|
+
AgentDefinition,
|
|
7
|
+
InputFieldDefinition,
|
|
8
|
+
MCPSettings,
|
|
9
|
+
ModelSettings,
|
|
10
|
+
PluginSettings,
|
|
11
|
+
RuntimeConfig,
|
|
12
|
+
SkillDefinition,
|
|
13
|
+
ToolPolicy,
|
|
14
|
+
)
|
|
15
|
+
from .storage import AgentDefinitionStore
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"A2ASettings",
|
|
19
|
+
"AgentConfig",
|
|
20
|
+
"AgentDefinition",
|
|
21
|
+
"AgentDefinitionStore",
|
|
22
|
+
"InputFieldDefinition",
|
|
23
|
+
"MCPSettings",
|
|
24
|
+
"ModelSettings",
|
|
25
|
+
"PluginSettings",
|
|
26
|
+
"RuntimeConfig",
|
|
27
|
+
"SkillDefinition",
|
|
28
|
+
"ToolPolicy",
|
|
29
|
+
]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""框架的静态定义层。
|
|
4
|
+
|
|
5
|
+
这个文件只负责“描述一个 agent runtime 长什么样”,不负责真正执行。
|
|
6
|
+
调用链通常是:
|
|
7
|
+
|
|
8
|
+
1. 先把 YAML / Builder 收敛成 AgentDefinition
|
|
9
|
+
2. 再由 ManagedAgentRuntime 读取这些定义构造运行时
|
|
10
|
+
3. 最后通过 standalone / builder 暴露成 A2A 服务
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_PASS_THROUGH_HEADERS = ["X-Session-Id", "accessToken", "user_email"]
|
|
20
|
+
DEFAULT_PLUGIN_MODULE_PATH = "agent_app/plugin.py"
|
|
21
|
+
DEFAULT_PLUGIN_CLASS_NAME = "Plugin"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InputFieldDefinition(BaseModel):
|
|
25
|
+
"""描述某个工具参数缺失时,需要向用户补采的字段。"""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
type: str = "string"
|
|
29
|
+
description: str = ""
|
|
30
|
+
required: bool = True
|
|
31
|
+
default: Any = None
|
|
32
|
+
map_to_arg: str | None = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def target_arg(self) -> str:
|
|
36
|
+
return self.map_to_arg or self.name
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ToolPolicy(BaseModel):
|
|
40
|
+
"""工具治理策略。"""
|
|
41
|
+
|
|
42
|
+
enabled: bool = True
|
|
43
|
+
requires_confirmation: bool = False
|
|
44
|
+
input_fields: list[InputFieldDefinition] = Field(default_factory=list)
|
|
45
|
+
prompt: str | None = None
|
|
46
|
+
allow_arg_override: bool = True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ModelSettings(BaseModel):
|
|
50
|
+
"""模型侧配置。"""
|
|
51
|
+
|
|
52
|
+
provider: str = "openai_compatible"
|
|
53
|
+
api_base: str | None = None
|
|
54
|
+
model_id: str | None = None
|
|
55
|
+
api_key_env: str = "MODEL_SOURCE_API_KEY"
|
|
56
|
+
max_steps: int = 15
|
|
57
|
+
flatten_messages_as_text: bool = True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MCPSettings(BaseModel):
|
|
61
|
+
"""单个 MCP 服务的连接定义。"""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
url: str
|
|
65
|
+
transport: str = "streamable-http"
|
|
66
|
+
enabled: bool = True
|
|
67
|
+
retry_count: int = Field(default=3, ge=0)
|
|
68
|
+
retry_delay_seconds: float = Field(default=1.0, ge=0)
|
|
69
|
+
static_headers: dict[str, str] = Field(default_factory=dict)
|
|
70
|
+
pass_through_headers: list[str] = Field(default_factory=lambda: list(DEFAULT_PASS_THROUGH_HEADERS))
|
|
71
|
+
# TODO: tool policy 后续还可以继续细分,不一定永远只挂在 MCP server 下。
|
|
72
|
+
tool_policies: dict[str, ToolPolicy] = Field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PluginSettings(BaseModel):
|
|
76
|
+
"""插件加载配置。"""
|
|
77
|
+
|
|
78
|
+
module_path: str = DEFAULT_PLUGIN_MODULE_PATH
|
|
79
|
+
class_name: str = DEFAULT_PLUGIN_CLASS_NAME
|
|
80
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SkillDefinition(BaseModel):
|
|
84
|
+
"""A2A agent card 中对外暴露的一个技能描述。"""
|
|
85
|
+
|
|
86
|
+
id: str
|
|
87
|
+
name: str
|
|
88
|
+
description: str
|
|
89
|
+
tags: list[str] = Field(default_factory=list)
|
|
90
|
+
examples: list[str] = Field(default_factory=list)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class A2ASettings(BaseModel):
|
|
94
|
+
"""A2A 展示层配置。"""
|
|
95
|
+
|
|
96
|
+
skills: list[SkillDefinition] = Field(default_factory=list)
|
|
97
|
+
default_input_modes: list[str] = Field(default_factory=lambda: ["text"])
|
|
98
|
+
default_output_modes: list[str] = Field(default_factory=lambda: ["text"])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AgentConfig(BaseModel):
|
|
102
|
+
"""Agent 的身份与展示配置。"""
|
|
103
|
+
|
|
104
|
+
agent_id: str
|
|
105
|
+
name: str
|
|
106
|
+
description: str
|
|
107
|
+
version: str = "0.1.0"
|
|
108
|
+
a2a: A2ASettings = Field(default_factory=A2ASettings)
|
|
109
|
+
extra_instructions: str = ""
|
|
110
|
+
|
|
111
|
+
@field_validator("agent_id")
|
|
112
|
+
@classmethod
|
|
113
|
+
def validate_agent_id(cls, value: str) -> str:
|
|
114
|
+
if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_-]*", value):
|
|
115
|
+
raise ValueError("agent_id must match [a-zA-Z0-9][a-zA-Z0-9_-]*")
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class RuntimeConfig(BaseModel):
|
|
120
|
+
"""运行时必要配置。"""
|
|
121
|
+
|
|
122
|
+
base_path: str = "."
|
|
123
|
+
public_base_url: str | None = None
|
|
124
|
+
model: ModelSettings
|
|
125
|
+
plugin: PluginSettings | None = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class AgentDefinition(BaseModel):
|
|
129
|
+
"""一个 agent runtime 的完整静态定义。"""
|
|
130
|
+
|
|
131
|
+
agent: AgentConfig
|
|
132
|
+
runtime: RuntimeConfig
|
|
133
|
+
mcps: list[MCPSettings] = Field(default_factory=list)
|
|
134
|
+
|
|
135
|
+
@model_validator(mode="after")
|
|
136
|
+
def validate_mcps(self) -> "AgentDefinition":
|
|
137
|
+
# FIXME: 未来不一定要求必须存在 MCP;当前先保持现有约束。
|
|
138
|
+
if not self.mcps:
|
|
139
|
+
raise ValueError("at least one MCP server must be configured in 'mcps'")
|
|
140
|
+
|
|
141
|
+
names = [mcp.name for mcp in self.mcps]
|
|
142
|
+
if len(set(names)) != len(names):
|
|
143
|
+
raise ValueError("mcp names must be unique")
|
|
144
|
+
return self
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .definitions import InputFieldDefinition, ToolPolicy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_user_payload(value: str | dict | None) -> Any:
|
|
10
|
+
if value is None:
|
|
11
|
+
return None
|
|
12
|
+
if isinstance(value, dict):
|
|
13
|
+
return value
|
|
14
|
+
text = str(value).strip()
|
|
15
|
+
if not text:
|
|
16
|
+
return None
|
|
17
|
+
try:
|
|
18
|
+
return json.loads(text)
|
|
19
|
+
except Exception:
|
|
20
|
+
return text
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_missing(value: Any) -> bool:
|
|
24
|
+
if value is None:
|
|
25
|
+
return True
|
|
26
|
+
if isinstance(value, str):
|
|
27
|
+
return value.strip() == ""
|
|
28
|
+
if isinstance(value, (list, dict)):
|
|
29
|
+
return len(value) == 0
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def collect_missing_fields(policy: ToolPolicy, current_args: dict[str, Any]) -> list[InputFieldDefinition]:
|
|
34
|
+
missing: list[InputFieldDefinition] = []
|
|
35
|
+
for field in policy.input_fields:
|
|
36
|
+
target_arg = field.target_arg
|
|
37
|
+
if field.required and is_missing(current_args.get(target_arg)):
|
|
38
|
+
missing.append(field)
|
|
39
|
+
return missing
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def merge_input_fields(
|
|
43
|
+
current_args: dict[str, Any],
|
|
44
|
+
policy: ToolPolicy,
|
|
45
|
+
resume_payload: Any,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
merged = dict(current_args)
|
|
48
|
+
if isinstance(resume_payload, dict):
|
|
49
|
+
for field in policy.input_fields:
|
|
50
|
+
key = field.name
|
|
51
|
+
target_arg = field.target_arg
|
|
52
|
+
if key in resume_payload and not is_missing(resume_payload[key]):
|
|
53
|
+
merged[target_arg] = resume_payload[key]
|
|
54
|
+
elif target_arg in resume_payload and not is_missing(resume_payload[target_arg]):
|
|
55
|
+
merged[target_arg] = resume_payload[target_arg]
|
|
56
|
+
elif field.default is not None and is_missing(merged.get(target_arg)):
|
|
57
|
+
merged[target_arg] = field.default
|
|
58
|
+
return merged
|
|
59
|
+
|
|
60
|
+
if len(policy.input_fields) == 1 and resume_payload is not None:
|
|
61
|
+
field = policy.input_fields[0]
|
|
62
|
+
merged[field.target_arg] = resume_payload
|
|
63
|
+
return merged
|