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,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""definition 的持久化与环境变量展开。"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from .definitions import AgentDefinition
|
|
14
|
+
from ..exceptions import DefinitionLoadError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_ENV_PATTERN = re.compile(r"\$\{(?P<name>[A-Z0-9_]+)(?::-(?P<default>[^}]*))?\}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _resolve_env_placeholders(value: Any) -> Any:
|
|
23
|
+
"""递归展开 YAML 中的 ${ENV} / ${ENV:-default} 占位符。"""
|
|
24
|
+
if isinstance(value, dict):
|
|
25
|
+
return {k: _resolve_env_placeholders(v) for k, v in value.items()}
|
|
26
|
+
if isinstance(value, list):
|
|
27
|
+
return [_resolve_env_placeholders(v) for v in value]
|
|
28
|
+
if not isinstance(value, str):
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
def _replace(match: re.Match[str]) -> str:
|
|
32
|
+
name = match.group("name")
|
|
33
|
+
default = match.group("default")
|
|
34
|
+
return os.getenv(name, default or "")
|
|
35
|
+
|
|
36
|
+
return _ENV_PATTERN.sub(_replace, value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _default_base_root(definition_path: Path) -> Path:
|
|
40
|
+
"""约定:如果 definition 放在 agent_app/ 下,默认相对项目根目录解析。"""
|
|
41
|
+
if definition_path.parent.name == "agent_app":
|
|
42
|
+
return definition_path.parent.parent
|
|
43
|
+
return definition_path.parent
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AgentDefinitionStore:
|
|
47
|
+
"""面向单机文件系统的 definition 存储。
|
|
48
|
+
|
|
49
|
+
当前实现很轻,只负责 YAML 读写;真正的校验交给 AgentDefinition。
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, root: str | Path):
|
|
53
|
+
self.root = Path(root)
|
|
54
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
def definition_path(self, agent_id: str) -> Path:
|
|
57
|
+
return self.root / f"{agent_id}.yaml"
|
|
58
|
+
|
|
59
|
+
def list_paths(self) -> list[Path]:
|
|
60
|
+
return sorted(self.root.glob("*.yaml"))
|
|
61
|
+
|
|
62
|
+
def list_definitions(self) -> list[AgentDefinition]:
|
|
63
|
+
return [self.load_path(path) for path in self.list_paths()]
|
|
64
|
+
|
|
65
|
+
def load(self, agent_id: str) -> AgentDefinition:
|
|
66
|
+
return self.load_path(self.definition_path(agent_id))
|
|
67
|
+
|
|
68
|
+
def load_path(self, path: str | Path) -> AgentDefinition:
|
|
69
|
+
# 读取后先做环境变量替换,再交给 pydantic 做结构校验。
|
|
70
|
+
resolved_path = Path(path).resolve()
|
|
71
|
+
logger.debug("Loading agent definition path=%s", resolved_path)
|
|
72
|
+
try:
|
|
73
|
+
with resolved_path.open("r", encoding="utf-8") as handle:
|
|
74
|
+
raw = yaml.safe_load(handle) or {}
|
|
75
|
+
except FileNotFoundError as exc:
|
|
76
|
+
logger.exception("Definition file not found path=%s", resolved_path)
|
|
77
|
+
raise DefinitionLoadError(
|
|
78
|
+
f"definition file not found: '{resolved_path}'",
|
|
79
|
+
user_message="Agent 配置文件不存在,请检查 agent_app/agent.yaml 路径",
|
|
80
|
+
) from exc
|
|
81
|
+
except yaml.YAMLError as exc:
|
|
82
|
+
logger.exception("Invalid YAML in definition path=%s", resolved_path)
|
|
83
|
+
raise DefinitionLoadError(
|
|
84
|
+
f"invalid YAML in definition file '{resolved_path}': {exc}",
|
|
85
|
+
user_message="Agent 配置文件格式错误,请检查 YAML 语法",
|
|
86
|
+
) from exc
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
resolved_raw = _resolve_env_placeholders(raw)
|
|
90
|
+
runtime_raw = resolved_raw.get("runtime")
|
|
91
|
+
if not isinstance(runtime_raw, dict):
|
|
92
|
+
runtime_raw = {}
|
|
93
|
+
raw_base_path = runtime_raw.get("base_path", ".")
|
|
94
|
+
base_path = Path(raw_base_path)
|
|
95
|
+
if not base_path.is_absolute():
|
|
96
|
+
base_path = (_default_base_root(resolved_path) / base_path).resolve()
|
|
97
|
+
resolved_raw["runtime"] = {**runtime_raw, "base_path": base_path.as_posix()}
|
|
98
|
+
definition = AgentDefinition.model_validate(resolved_raw)
|
|
99
|
+
logger.debug("Loaded agent definition agent_id=%s path=%s", definition.agent.agent_id, resolved_path)
|
|
100
|
+
return definition
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
logger.exception("Failed to validate definition path=%s", resolved_path)
|
|
103
|
+
raise DefinitionLoadError(
|
|
104
|
+
f"failed to validate definition file '{resolved_path}': {exc}",
|
|
105
|
+
user_message="Agent 配置校验失败,请检查 agent_app/agent.yaml 必填项和字段格式",
|
|
106
|
+
) from exc
|
|
107
|
+
|
|
108
|
+
def save(self, definition: AgentDefinition) -> Path:
|
|
109
|
+
path = self.definition_path(definition.agent.agent_id)
|
|
110
|
+
with path.open("w", encoding="utf-8") as handle:
|
|
111
|
+
yaml.safe_dump(definition.model_dump(mode="python"), handle, allow_unicode=True, sort_keys=False)
|
|
112
|
+
return path
|
|
113
|
+
|
|
114
|
+
def delete(self, agent_id: str) -> None:
|
|
115
|
+
path = self.definition_path(agent_id)
|
|
116
|
+
if path.exists():
|
|
117
|
+
path.unlink()
|
agent_runtime/context.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Public facade for configuration models."""
|
|
2
|
+
|
|
3
|
+
from .config.definitions import (
|
|
4
|
+
DEFAULT_PASS_THROUGH_HEADERS,
|
|
5
|
+
DEFAULT_PLUGIN_CLASS_NAME,
|
|
6
|
+
DEFAULT_PLUGIN_MODULE_PATH,
|
|
7
|
+
A2ASettings,
|
|
8
|
+
AgentConfig,
|
|
9
|
+
AgentDefinition,
|
|
10
|
+
InputFieldDefinition,
|
|
11
|
+
MCPSettings,
|
|
12
|
+
ModelSettings,
|
|
13
|
+
PluginSettings,
|
|
14
|
+
RuntimeConfig,
|
|
15
|
+
SkillDefinition,
|
|
16
|
+
ToolPolicy,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DEFAULT_PASS_THROUGH_HEADERS",
|
|
21
|
+
"DEFAULT_PLUGIN_CLASS_NAME",
|
|
22
|
+
"DEFAULT_PLUGIN_MODULE_PATH",
|
|
23
|
+
"A2ASettings",
|
|
24
|
+
"AgentConfig",
|
|
25
|
+
"AgentDefinition",
|
|
26
|
+
"InputFieldDefinition",
|
|
27
|
+
"MCPSettings",
|
|
28
|
+
"ModelSettings",
|
|
29
|
+
"PluginSettings",
|
|
30
|
+
"RuntimeConfig",
|
|
31
|
+
"SkillDefinition",
|
|
32
|
+
"ToolPolicy",
|
|
33
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""对外暴露的 MCP discovery 辅助入口。"""
|
|
2
|
+
|
|
3
|
+
from .mcp.discovery import (
|
|
4
|
+
default_discover_tools,
|
|
5
|
+
load_mcp_client_and_tools,
|
|
6
|
+
load_mcp_tools,
|
|
7
|
+
)
|
|
8
|
+
from .mcp.metadata import DiscoveredTool, discovered_tool_from_tool
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"DiscoveredTool",
|
|
12
|
+
"default_discover_tools",
|
|
13
|
+
"discovered_tool_from_tool",
|
|
14
|
+
"load_mcp_client_and_tools",
|
|
15
|
+
"load_mcp_tools",
|
|
16
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""框架统一异常类型。
|
|
4
|
+
|
|
5
|
+
原则:
|
|
6
|
+
- 日志里保留内部真实异常
|
|
7
|
+
- 对用户返回稳定、可预期的文案
|
|
8
|
+
- 启动期和请求期都尽量用语义明确的异常类型,而不是到处混用 RuntimeError
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentRuntimeError(Exception):
|
|
13
|
+
"""框架异常基类,附带稳定的用户可见文案。"""
|
|
14
|
+
|
|
15
|
+
default_user_message = "任务执行失败,请稍后重试"
|
|
16
|
+
default_code = "agent_runtime_error"
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
message: str,
|
|
21
|
+
*,
|
|
22
|
+
user_message: str | None = None,
|
|
23
|
+
code: str | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.user_message = user_message or self.default_user_message
|
|
27
|
+
self.code = code or self.default_code
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DefinitionLoadError(AgentRuntimeError):
|
|
31
|
+
default_user_message = "Agent 配置加载失败,请检查 agent_app/agent.yaml"
|
|
32
|
+
default_code = "definition_load_error"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PluginLoadError(AgentRuntimeError):
|
|
36
|
+
default_user_message = "Agent 插件加载失败,请检查 plugin 配置"
|
|
37
|
+
default_code = "plugin_load_error"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MCPToolLoadError(AgentRuntimeError):
|
|
41
|
+
default_user_message = "MCP 工具加载失败,请检查 MCP 服务和网络连通性"
|
|
42
|
+
default_code = "mcp_tool_load_error"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AgentBuildError(AgentRuntimeError):
|
|
46
|
+
default_user_message = "Agent 构建失败,请检查模型、插件和工具配置"
|
|
47
|
+
default_code = "agent_build_error"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TaskWaitTimeoutError(AgentRuntimeError):
|
|
51
|
+
default_user_message = "任务等待超时,已自动结束"
|
|
52
|
+
default_code = "task_wait_timeout"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TaskExecutionTimeoutError(AgentRuntimeError):
|
|
56
|
+
default_user_message = "任务执行超时,已自动取消"
|
|
57
|
+
default_code = "task_execution_timeout"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UserCancelledError(AgentRuntimeError):
|
|
61
|
+
default_user_message = "用户已取消操作"
|
|
62
|
+
default_code = "user_cancelled"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TaskCancelledError(AgentRuntimeError):
|
|
66
|
+
default_user_message = "任务已取消"
|
|
67
|
+
default_code = "task_cancelled"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def user_message_for_error(error: Exception) -> str:
|
|
71
|
+
"""把内部异常映射成稳定的用户可见文案。"""
|
|
72
|
+
if isinstance(error, AgentRuntimeError):
|
|
73
|
+
return error.user_message
|
|
74
|
+
return AgentRuntimeError.default_user_message
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""MCP 相关内部实现。
|
|
2
|
+
|
|
3
|
+
模块划分:
|
|
4
|
+
|
|
5
|
+
- `metadata.py`: 工具描述对象与规整逻辑
|
|
6
|
+
- `utils.py`: MCP 共用规则
|
|
7
|
+
- `discovery.py`: discover/load 主流程
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .discovery import default_discover_tools, load_mcp_client_and_tools, load_mcp_tools
|
|
11
|
+
from .metadata import (
|
|
12
|
+
DiscoveredTool,
|
|
13
|
+
discovered_tool_from_tool,
|
|
14
|
+
normalize_discovered_tools,
|
|
15
|
+
)
|
|
16
|
+
from .utils import index_tool_sources, merge_mcp_headers, register_tool_source
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DiscoveredTool",
|
|
20
|
+
"default_discover_tools",
|
|
21
|
+
"discovered_tool_from_tool",
|
|
22
|
+
"index_tool_sources",
|
|
23
|
+
"load_mcp_client_and_tools",
|
|
24
|
+
"load_mcp_tools",
|
|
25
|
+
"merge_mcp_headers",
|
|
26
|
+
"normalize_discovered_tools",
|
|
27
|
+
"register_tool_source",
|
|
28
|
+
]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""MCP 工具发现与加载。
|
|
4
|
+
|
|
5
|
+
这个文件只保留 discover/load 主流程,不再承载 descriptor 模型和共用工具函数。
|
|
6
|
+
|
|
7
|
+
这里区分两类动作:
|
|
8
|
+
|
|
9
|
+
1. discover:只拿工具元数据,给管理面、prompt、agent card 使用
|
|
10
|
+
2. load:真正创建 MCPClient,并返回可执行的 tool 对象
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from ..config.definitions import MCPSettings
|
|
18
|
+
from ..exceptions import MCPToolLoadError
|
|
19
|
+
from .metadata import (
|
|
20
|
+
DiscoveredTool,
|
|
21
|
+
discovered_tool_from_tool,
|
|
22
|
+
normalize_discovered_tools,
|
|
23
|
+
)
|
|
24
|
+
from .utils import merge_mcp_headers
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def default_discover_tools(
|
|
30
|
+
mcp: MCPSettings, headers: dict[str, str] | None = None
|
|
31
|
+
) -> list[DiscoveredTool]:
|
|
32
|
+
"""默认的工具发现流程。
|
|
33
|
+
|
|
34
|
+
这里只做“看工具”(list_tools) 拿工具 description ,因此 client 用完即关,不把连接留到运行时。
|
|
35
|
+
"""
|
|
36
|
+
client, tools = load_mcp_client_and_tools(mcp, headers=headers)
|
|
37
|
+
try:
|
|
38
|
+
logger.debug(
|
|
39
|
+
"Discovered MCP tools mcp=%s url=%s tool_count=%s",
|
|
40
|
+
mcp.name,
|
|
41
|
+
mcp.url,
|
|
42
|
+
len(tools),
|
|
43
|
+
)
|
|
44
|
+
return normalize_discovered_tools(
|
|
45
|
+
(discovered_tool_from_tool(tool) for tool in tools), mcp.name
|
|
46
|
+
)
|
|
47
|
+
finally:
|
|
48
|
+
try:
|
|
49
|
+
client.disconnect()
|
|
50
|
+
except Exception:
|
|
51
|
+
logger.warning(
|
|
52
|
+
"Failed to disconnect discovery MCP client mcp=%s url=%s",
|
|
53
|
+
mcp.name,
|
|
54
|
+
mcp.url,
|
|
55
|
+
exc_info=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_mcp_tools(
|
|
60
|
+
mcp: MCPSettings, headers: dict[str, str] | None = None
|
|
61
|
+
) -> list[Any]:
|
|
62
|
+
"""兼容旧用法:仅返回 tools,但把 client 挂到 tool 上,避免生命周期丢失。"""
|
|
63
|
+
client, tools = load_mcp_client_and_tools(mcp, headers=headers)
|
|
64
|
+
for tool in tools:
|
|
65
|
+
try:
|
|
66
|
+
tool._runtime_mcp_client = client
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
return tools
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load_mcp_client_and_tools(
|
|
73
|
+
mcp: MCPSettings, headers: dict[str, str] | None = None
|
|
74
|
+
) -> tuple[Any, list[Any]]:
|
|
75
|
+
"""创建一个 MCPClient,并把这个 MCP 暴露的工具全部取出来。
|
|
76
|
+
|
|
77
|
+
这里是运行时真正执行工具前的入口,所以失败时要负责把刚创建的 client 清干净。
|
|
78
|
+
"""
|
|
79
|
+
from smolagents import MCPClient
|
|
80
|
+
|
|
81
|
+
merged_headers = merge_mcp_headers(mcp, headers)
|
|
82
|
+
total_attempts = mcp.retry_count + 1
|
|
83
|
+
last_exc: Exception | None = None
|
|
84
|
+
|
|
85
|
+
for attempt in range(1, total_attempts + 1):
|
|
86
|
+
client = None
|
|
87
|
+
try:
|
|
88
|
+
client = MCPClient(
|
|
89
|
+
{"url": mcp.url, "transport": mcp.transport, "headers": merged_headers},
|
|
90
|
+
structured_output=False,
|
|
91
|
+
)
|
|
92
|
+
tools = client.get_tools()
|
|
93
|
+
logger.debug(
|
|
94
|
+
"Loaded MCP client and tools mcp=%s url=%s tool_count=%s attempt=%s/%s",
|
|
95
|
+
mcp.name,
|
|
96
|
+
mcp.url,
|
|
97
|
+
len(tools),
|
|
98
|
+
attempt,
|
|
99
|
+
total_attempts,
|
|
100
|
+
)
|
|
101
|
+
return client, tools
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
last_exc = exc
|
|
104
|
+
logger.exception(
|
|
105
|
+
"Failed to create/load MCP client mcp=%s url=%s attempt=%s/%s",
|
|
106
|
+
mcp.name,
|
|
107
|
+
mcp.url,
|
|
108
|
+
attempt,
|
|
109
|
+
total_attempts,
|
|
110
|
+
)
|
|
111
|
+
if client is not None:
|
|
112
|
+
try:
|
|
113
|
+
client.disconnect()
|
|
114
|
+
except Exception:
|
|
115
|
+
logger.warning(
|
|
116
|
+
"Failed to disconnect MCP client after tool load failure mcp=%s url=%s attempt=%s/%s",
|
|
117
|
+
mcp.name,
|
|
118
|
+
mcp.url,
|
|
119
|
+
attempt,
|
|
120
|
+
total_attempts,
|
|
121
|
+
exc_info=True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if attempt < total_attempts:
|
|
125
|
+
logger.warning(
|
|
126
|
+
"Retrying MCP client load mcp=%s url=%s in %ss after failure (%s/%s)",
|
|
127
|
+
mcp.name,
|
|
128
|
+
mcp.url,
|
|
129
|
+
mcp.retry_delay_seconds,
|
|
130
|
+
attempt,
|
|
131
|
+
total_attempts,
|
|
132
|
+
)
|
|
133
|
+
if mcp.retry_delay_seconds > 0:
|
|
134
|
+
time.sleep(mcp.retry_delay_seconds)
|
|
135
|
+
|
|
136
|
+
retry_note = f"after {total_attempts} attempts ({mcp.retry_count} retries)"
|
|
137
|
+
raise MCPToolLoadError(
|
|
138
|
+
(
|
|
139
|
+
f"failed to create/load MCP client for mcp '{mcp.name}' at '{mcp.url}' "
|
|
140
|
+
f"{retry_note}: {last_exc}"
|
|
141
|
+
),
|
|
142
|
+
user_message=(
|
|
143
|
+
f"MCP 服务 '{mcp.name}' 连接失败,已重试 {mcp.retry_count} 次后仍不可用,"
|
|
144
|
+
"请检查服务状态和网络连通性"
|
|
145
|
+
),
|
|
146
|
+
) from last_exc
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""MCP 工具元数据模型。
|
|
4
|
+
|
|
5
|
+
这个文件只处理“工具长什么样”:
|
|
6
|
+
|
|
7
|
+
- `DiscoveredTool`: discovery 阶段的只读工具摘要
|
|
8
|
+
- `discovered_tool_from_tool`: 把具体 tool 对象压平为摘要
|
|
9
|
+
- `normalize_discovered_tools`: 规整 discoverer 返回值,并补齐 `source_mcp`
|
|
10
|
+
|
|
11
|
+
注意:
|
|
12
|
+
|
|
13
|
+
- 这里的对象不是原生 MCP 可执行 tool
|
|
14
|
+
- 它只服务于管理面、prompt 展示、工具列表和策略定位
|
|
15
|
+
- 真正执行工具时,runtime 仍然使用原生 tool 对象
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Any, Iterable
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DiscoveredTool(BaseModel):
|
|
24
|
+
"""discovery 阶段的只读工具摘要。
|
|
25
|
+
|
|
26
|
+
之所以不直接缓存原生 MCP tool,是因为 discovery 阶段只需要稳定、可序列化、
|
|
27
|
+
可长期持有的元数据;真正执行工具时,会重新拿原生 tool 实例。
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
source_mcp: str | None = None
|
|
32
|
+
description: str = ""
|
|
33
|
+
inputs: dict[str, Any] = Field(default_factory=dict)
|
|
34
|
+
output_type: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def discovered_tool_from_tool(
|
|
38
|
+
tool: Any, source_mcp: str | None = None
|
|
39
|
+
) -> DiscoveredTool:
|
|
40
|
+
"""把具体 tool 对象压平成稳定的 discovered tool 摘要。"""
|
|
41
|
+
|
|
42
|
+
return DiscoveredTool(
|
|
43
|
+
name=getattr(tool, "name", ""),
|
|
44
|
+
source_mcp=source_mcp,
|
|
45
|
+
description=getattr(tool, "description", "") or "",
|
|
46
|
+
inputs=getattr(tool, "inputs", {}) or {},
|
|
47
|
+
output_type=getattr(tool, "output_type", None),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def normalize_discovered_tools(
|
|
52
|
+
items: Iterable[Any], source_mcp: str
|
|
53
|
+
) -> list[DiscoveredTool]:
|
|
54
|
+
"""把 discoverer 返回值统一规整成带 `source_mcp` 的 discovered tools。"""
|
|
55
|
+
|
|
56
|
+
discovered_tools: list[DiscoveredTool] = []
|
|
57
|
+
for item in items:
|
|
58
|
+
discovered_tool = (
|
|
59
|
+
item
|
|
60
|
+
if isinstance(item, DiscoveredTool)
|
|
61
|
+
else DiscoveredTool.model_validate(item)
|
|
62
|
+
)
|
|
63
|
+
discovered_tools.append(
|
|
64
|
+
discovered_tool.model_copy(
|
|
65
|
+
update={"source_mcp": discovered_tool.source_mcp or source_mcp}
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
return discovered_tools
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""MCP 共用工具函数。
|
|
4
|
+
|
|
5
|
+
这个文件只放多处复用的 MCP 规则:
|
|
6
|
+
|
|
7
|
+
- header 合并
|
|
8
|
+
- tool source 建索引
|
|
9
|
+
- 重名工具冲突检测
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
|
|
14
|
+
from ..config.definitions import MCPSettings
|
|
15
|
+
from .metadata import DiscoveredTool
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def merge_mcp_headers(
|
|
19
|
+
mcp: MCPSettings, headers: dict[str, str] | None = None
|
|
20
|
+
) -> dict[str, str]:
|
|
21
|
+
"""合并 MCP 静态头和调用期透传头。"""
|
|
22
|
+
|
|
23
|
+
merged_headers = dict(mcp.static_headers)
|
|
24
|
+
if headers:
|
|
25
|
+
merged_headers.update({key: value for key, value in headers.items() if value})
|
|
26
|
+
return merged_headers
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register_tool_source(
|
|
30
|
+
tool_sources: dict[str, str], tool_name: str, source_mcp: str
|
|
31
|
+
) -> None:
|
|
32
|
+
"""记录工具来源,并在同名冲突时抛出一致错误。"""
|
|
33
|
+
|
|
34
|
+
existing_source = tool_sources.get(tool_name)
|
|
35
|
+
if existing_source is not None:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"duplicate MCP tool name '{tool_name}' from '{existing_source}' and '{source_mcp}'"
|
|
38
|
+
)
|
|
39
|
+
tool_sources[tool_name] = source_mcp
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def index_tool_sources(discovered_tools: Iterable[DiscoveredTool]) -> dict[str, str]:
|
|
43
|
+
"""为已发现工具建立 `name -> source_mcp` 索引。"""
|
|
44
|
+
|
|
45
|
+
tool_sources: dict[str, str] = {}
|
|
46
|
+
for discovered_tool in discovered_tools:
|
|
47
|
+
register_tool_source(
|
|
48
|
+
tool_sources,
|
|
49
|
+
discovered_tool.name,
|
|
50
|
+
discovered_tool.source_mcp or "",
|
|
51
|
+
)
|
|
52
|
+
return tool_sources
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ModelRegistry:
|
|
7
|
+
"""Pluggable LLM model factory.
|
|
8
|
+
|
|
9
|
+
Register model classes by provider name and create instances via
|
|
10
|
+
``ModelRegistry.create(provider, **kwargs)``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
_providers: dict[str, type] = {}
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def register(cls, name: str, model_cls: type) -> None:
|
|
17
|
+
cls._providers[name] = model_cls
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def create(cls, provider: str, **kwargs: Any):
|
|
21
|
+
model_cls = cls._providers.get(provider)
|
|
22
|
+
if model_cls is None:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"Unknown model provider: {provider}. "
|
|
25
|
+
f"Registered: {list(cls._providers.keys())}"
|
|
26
|
+
)
|
|
27
|
+
return model_cls(**kwargs)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def list_providers(cls) -> list[str]:
|
|
31
|
+
return list(cls._providers.keys())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _register_defaults() -> None:
|
|
35
|
+
from .smol import DeepSeekModel
|
|
36
|
+
|
|
37
|
+
ModelRegistry.register("openai_compatible", DeepSeekModel)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_register_defaults()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""插件扩展点定义。
|
|
4
|
+
|
|
5
|
+
框架把“工具如何调用、提示词如何生成、结果如何格式化”这些可变逻辑
|
|
6
|
+
都收敛到 plugin 接口里,避免把业务定制写死在 runtime 中。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Protocol
|
|
11
|
+
|
|
12
|
+
from ..config.definitions import AgentDefinition, InputFieldDefinition
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class BeforeToolDecision:
|
|
17
|
+
"""插件在工具执行前给出的治理决策。"""
|
|
18
|
+
|
|
19
|
+
requires_confirmation: bool | None = None
|
|
20
|
+
input_fields: list[InputFieldDefinition] = field(default_factory=list)
|
|
21
|
+
prompt: str | None = None
|
|
22
|
+
allow_arg_override: bool | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ToolExecutionContext:
|
|
27
|
+
"""传给 plugin 的统一上下文。
|
|
28
|
+
|
|
29
|
+
这里尽量把 plugin 关心的信息收全:当前 agent 定义、工具名、参数、
|
|
30
|
+
来源 MCP、原始 tool_call、执行结果等。
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
agent_definition: AgentDefinition
|
|
34
|
+
tool_name: str
|
|
35
|
+
args: dict[str, Any]
|
|
36
|
+
mcp_name: str | None = None
|
|
37
|
+
task_info: Any = None
|
|
38
|
+
raw_tool_call: Any = None
|
|
39
|
+
observation: Any = None
|
|
40
|
+
result: Any = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AgentPlugin(Protocol):
|
|
44
|
+
"""插件协议:描述一个 plugin 至少要提供哪些扩展点。"""
|
|
45
|
+
|
|
46
|
+
def build_instructions(self, definition: AgentDefinition, tools: list[Any]) -> str:
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def extra_tools(self, definition: AgentDefinition) -> list[Any]:
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def before_tool(self, context: ToolExecutionContext) -> BeforeToolDecision | None:
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
def normalize_args(self, context: ToolExecutionContext) -> dict[str, Any] | None:
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def after_tool(self, context: ToolExecutionContext) -> None:
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def format_result(self, context: ToolExecutionContext) -> str | dict[str, Any] | None:
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class BaseAgentPlugin:
|
|
66
|
+
"""插件基类。
|
|
67
|
+
|
|
68
|
+
所有扩展点默认都是 no-op,这样业务插件只需要覆盖自己关心的那几项。
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
72
|
+
self.config = config or {}
|
|
73
|
+
|
|
74
|
+
def build_instructions(self, definition: AgentDefinition, tools: list[Any]) -> str:
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
def extra_tools(self, definition: AgentDefinition) -> list[Any]:
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
def before_tool(self, context: ToolExecutionContext) -> BeforeToolDecision | None:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def normalize_args(self, context: ToolExecutionContext) -> dict[str, Any] | None:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def after_tool(self, context: ToolExecutionContext) -> None:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def format_result(self, context: ToolExecutionContext) -> str | dict[str, Any] | None:
|
|
90
|
+
return None
|