agentrun-inner-test 0.0.46__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.
- agentrun/__init__.py +325 -0
- agentrun/agent_runtime/__client_async_template.py +466 -0
- agentrun/agent_runtime/__endpoint_async_template.py +345 -0
- agentrun/agent_runtime/__init__.py +53 -0
- agentrun/agent_runtime/__runtime_async_template.py +477 -0
- agentrun/agent_runtime/api/__data_async_template.py +58 -0
- agentrun/agent_runtime/api/__init__.py +6 -0
- agentrun/agent_runtime/api/control.py +1362 -0
- agentrun/agent_runtime/api/data.py +98 -0
- agentrun/agent_runtime/client.py +868 -0
- agentrun/agent_runtime/endpoint.py +649 -0
- agentrun/agent_runtime/model.py +362 -0
- agentrun/agent_runtime/runtime.py +904 -0
- agentrun/credential/__client_async_template.py +177 -0
- agentrun/credential/__credential_async_template.py +216 -0
- agentrun/credential/__init__.py +28 -0
- agentrun/credential/api/__init__.py +5 -0
- agentrun/credential/api/control.py +606 -0
- agentrun/credential/client.py +319 -0
- agentrun/credential/credential.py +381 -0
- agentrun/credential/model.py +248 -0
- agentrun/integration/__init__.py +21 -0
- agentrun/integration/agentscope/__init__.py +12 -0
- agentrun/integration/agentscope/adapter.py +17 -0
- agentrun/integration/agentscope/builtin.py +65 -0
- agentrun/integration/agentscope/message_adapter.py +185 -0
- agentrun/integration/agentscope/model_adapter.py +60 -0
- agentrun/integration/agentscope/tool_adapter.py +59 -0
- agentrun/integration/builtin/__init__.py +16 -0
- agentrun/integration/builtin/model.py +93 -0
- agentrun/integration/builtin/sandbox.py +1234 -0
- agentrun/integration/builtin/toolset.py +47 -0
- agentrun/integration/crewai/__init__.py +12 -0
- agentrun/integration/crewai/adapter.py +9 -0
- agentrun/integration/crewai/builtin.py +65 -0
- agentrun/integration/crewai/model_adapter.py +31 -0
- agentrun/integration/crewai/tool_adapter.py +26 -0
- agentrun/integration/google_adk/__init__.py +12 -0
- agentrun/integration/google_adk/adapter.py +15 -0
- agentrun/integration/google_adk/builtin.py +65 -0
- agentrun/integration/google_adk/message_adapter.py +144 -0
- agentrun/integration/google_adk/model_adapter.py +46 -0
- agentrun/integration/google_adk/tool_adapter.py +235 -0
- agentrun/integration/langchain/__init__.py +30 -0
- agentrun/integration/langchain/adapter.py +15 -0
- agentrun/integration/langchain/builtin.py +71 -0
- agentrun/integration/langchain/message_adapter.py +141 -0
- agentrun/integration/langchain/model_adapter.py +37 -0
- agentrun/integration/langchain/tool_adapter.py +50 -0
- agentrun/integration/langgraph/__init__.py +35 -0
- agentrun/integration/langgraph/adapter.py +20 -0
- agentrun/integration/langgraph/agent_converter.py +1073 -0
- agentrun/integration/langgraph/builtin.py +65 -0
- agentrun/integration/pydantic_ai/__init__.py +12 -0
- agentrun/integration/pydantic_ai/adapter.py +13 -0
- agentrun/integration/pydantic_ai/builtin.py +65 -0
- agentrun/integration/pydantic_ai/model_adapter.py +44 -0
- agentrun/integration/pydantic_ai/tool_adapter.py +19 -0
- agentrun/integration/utils/__init__.py +112 -0
- agentrun/integration/utils/adapter.py +560 -0
- agentrun/integration/utils/canonical.py +164 -0
- agentrun/integration/utils/converter.py +134 -0
- agentrun/integration/utils/model.py +110 -0
- agentrun/integration/utils/tool.py +1759 -0
- agentrun/model/__client_async_template.py +357 -0
- agentrun/model/__init__.py +57 -0
- agentrun/model/__model_proxy_async_template.py +270 -0
- agentrun/model/__model_service_async_template.py +267 -0
- agentrun/model/api/__init__.py +6 -0
- agentrun/model/api/control.py +1173 -0
- agentrun/model/api/data.py +196 -0
- agentrun/model/client.py +674 -0
- agentrun/model/model.py +235 -0
- agentrun/model/model_proxy.py +439 -0
- agentrun/model/model_service.py +438 -0
- agentrun/sandbox/__aio_sandbox_async_template.py +523 -0
- agentrun/sandbox/__browser_sandbox_async_template.py +110 -0
- agentrun/sandbox/__client_async_template.py +491 -0
- agentrun/sandbox/__code_interpreter_sandbox_async_template.py +463 -0
- agentrun/sandbox/__init__.py +69 -0
- agentrun/sandbox/__sandbox_async_template.py +463 -0
- agentrun/sandbox/__template_async_template.py +152 -0
- agentrun/sandbox/aio_sandbox.py +905 -0
- agentrun/sandbox/api/__aio_data_async_template.py +335 -0
- agentrun/sandbox/api/__browser_data_async_template.py +140 -0
- agentrun/sandbox/api/__code_interpreter_data_async_template.py +206 -0
- agentrun/sandbox/api/__init__.py +19 -0
- agentrun/sandbox/api/__sandbox_data_async_template.py +107 -0
- agentrun/sandbox/api/aio_data.py +551 -0
- agentrun/sandbox/api/browser_data.py +172 -0
- agentrun/sandbox/api/code_interpreter_data.py +396 -0
- agentrun/sandbox/api/control.py +1051 -0
- agentrun/sandbox/api/playwright_async.py +492 -0
- agentrun/sandbox/api/playwright_sync.py +492 -0
- agentrun/sandbox/api/sandbox_data.py +154 -0
- agentrun/sandbox/browser_sandbox.py +185 -0
- agentrun/sandbox/client.py +925 -0
- agentrun/sandbox/code_interpreter_sandbox.py +823 -0
- agentrun/sandbox/model.py +397 -0
- agentrun/sandbox/sandbox.py +848 -0
- agentrun/sandbox/template.py +217 -0
- agentrun/server/__init__.py +191 -0
- agentrun/server/agui_normalizer.py +180 -0
- agentrun/server/agui_protocol.py +797 -0
- agentrun/server/invoker.py +309 -0
- agentrun/server/model.py +427 -0
- agentrun/server/openai_protocol.py +535 -0
- agentrun/server/protocol.py +140 -0
- agentrun/server/server.py +208 -0
- agentrun/toolset/__client_async_template.py +62 -0
- agentrun/toolset/__init__.py +51 -0
- agentrun/toolset/__toolset_async_template.py +204 -0
- agentrun/toolset/api/__init__.py +17 -0
- agentrun/toolset/api/control.py +262 -0
- agentrun/toolset/api/mcp.py +100 -0
- agentrun/toolset/api/openapi.py +1251 -0
- agentrun/toolset/client.py +102 -0
- agentrun/toolset/model.py +321 -0
- agentrun/toolset/toolset.py +270 -0
- agentrun/utils/__data_api_async_template.py +720 -0
- agentrun/utils/__init__.py +5 -0
- agentrun/utils/__resource_async_template.py +158 -0
- agentrun/utils/config.py +258 -0
- agentrun/utils/control_api.py +78 -0
- agentrun/utils/data_api.py +1120 -0
- agentrun/utils/exception.py +151 -0
- agentrun/utils/helper.py +108 -0
- agentrun/utils/log.py +77 -0
- agentrun/utils/model.py +168 -0
- agentrun/utils/resource.py +291 -0
- agentrun_inner_test-0.0.46.dist-info/METADATA +263 -0
- agentrun_inner_test-0.0.46.dist-info/RECORD +135 -0
- agentrun_inner_test-0.0.46.dist-info/WHEEL +5 -0
- agentrun_inner_test-0.0.46.dist-info/licenses/LICENSE +201 -0
- agentrun_inner_test-0.0.46.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1759 @@
|
|
|
1
|
+
"""通用工具定义和转换模块
|
|
2
|
+
|
|
3
|
+
提供跨框架的通用工具定义和转换功能。
|
|
4
|
+
|
|
5
|
+
支持多种定义方式:
|
|
6
|
+
1. 使用 @tool 装饰器 - 最简单,推荐
|
|
7
|
+
2. 使用 Pydantic BaseModel - 类型安全
|
|
8
|
+
3. 使用 ToolParameter 列表 - 灵活但繁琐
|
|
9
|
+
|
|
10
|
+
支持转换为以下框架格式:
|
|
11
|
+
- OpenAI Function Calling
|
|
12
|
+
- Anthropic Claude Tools
|
|
13
|
+
- LangChain Tools
|
|
14
|
+
- Google ADK Tools
|
|
15
|
+
- AgentScope Tools
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from copy import deepcopy
|
|
21
|
+
from functools import wraps
|
|
22
|
+
import hashlib
|
|
23
|
+
import inspect
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
from typing import (
|
|
27
|
+
Any,
|
|
28
|
+
Callable,
|
|
29
|
+
Dict,
|
|
30
|
+
get_args,
|
|
31
|
+
get_origin,
|
|
32
|
+
get_type_hints,
|
|
33
|
+
List,
|
|
34
|
+
Optional,
|
|
35
|
+
Set,
|
|
36
|
+
Tuple,
|
|
37
|
+
Type,
|
|
38
|
+
TYPE_CHECKING,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
from pydantic import (
|
|
42
|
+
AliasChoices,
|
|
43
|
+
BaseModel,
|
|
44
|
+
create_model,
|
|
45
|
+
Field,
|
|
46
|
+
ValidationError,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
from agentrun.toolset import ToolSet
|
|
51
|
+
|
|
52
|
+
from agentrun.utils.log import logger
|
|
53
|
+
|
|
54
|
+
# Tool name constraints for external providers like OpenAI
|
|
55
|
+
MAX_TOOL_NAME_LEN = 64
|
|
56
|
+
TOOL_NAME_HEAD_LEN = 32
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def normalize_tool_name(name: str) -> str:
|
|
60
|
+
"""Normalize a tool name to fit provider limits.
|
|
61
|
+
|
|
62
|
+
If `name` length is <= MAX_TOOL_NAME_LEN, return it unchanged.
|
|
63
|
+
Otherwise, return the first TOOL_NAME_HEAD_LEN characters + md5(full_name)
|
|
64
|
+
(32 hex chars), resulting in a 64-char string.
|
|
65
|
+
"""
|
|
66
|
+
if not isinstance(name, str):
|
|
67
|
+
name = str(name)
|
|
68
|
+
if len(name) <= MAX_TOOL_NAME_LEN:
|
|
69
|
+
return name
|
|
70
|
+
digest = hashlib.md5(name.encode("utf-8")).hexdigest()
|
|
71
|
+
return name[:TOOL_NAME_HEAD_LEN] + digest
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ToolParameter:
|
|
75
|
+
"""工具参数定义
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
name: 参数名称
|
|
79
|
+
param_type: 参数类型(string, integer, number, boolean, array, object)
|
|
80
|
+
description: 参数描述
|
|
81
|
+
required: 是否必填
|
|
82
|
+
default: 默认值
|
|
83
|
+
enum: 枚举值列表
|
|
84
|
+
items: 数组元素类型(当 param_type 为 array 时使用)
|
|
85
|
+
properties: 对象属性(当 param_type 为 object 时使用)
|
|
86
|
+
format: 额外的格式限定(如 int32、int64 等)
|
|
87
|
+
nullable: 是否允许为空
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
name: str,
|
|
93
|
+
param_type: str,
|
|
94
|
+
description: str = "",
|
|
95
|
+
required: bool = False,
|
|
96
|
+
default: Any = None,
|
|
97
|
+
enum: Optional[List[Any]] = None,
|
|
98
|
+
items: Optional[Dict[str, Any]] = None,
|
|
99
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
100
|
+
format: Optional[str] = None,
|
|
101
|
+
nullable: bool = False,
|
|
102
|
+
):
|
|
103
|
+
self.name = name
|
|
104
|
+
self.param_type = param_type
|
|
105
|
+
self.description = description
|
|
106
|
+
self.required = required
|
|
107
|
+
self.default = default
|
|
108
|
+
self.enum = enum
|
|
109
|
+
self.items = items
|
|
110
|
+
self.properties = properties
|
|
111
|
+
self.format = format
|
|
112
|
+
self.nullable = nullable
|
|
113
|
+
|
|
114
|
+
def to_json_schema(self) -> Dict[str, Any]:
|
|
115
|
+
"""转换为 JSON Schema 格式"""
|
|
116
|
+
schema: Dict[str, Any] = {
|
|
117
|
+
"type": self.param_type,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if self.description:
|
|
121
|
+
schema["description"] = self.description
|
|
122
|
+
|
|
123
|
+
if self.default is not None:
|
|
124
|
+
schema["default"] = self.default
|
|
125
|
+
|
|
126
|
+
if self.enum is not None:
|
|
127
|
+
schema["enum"] = self.enum
|
|
128
|
+
|
|
129
|
+
if self.format:
|
|
130
|
+
schema["format"] = self.format
|
|
131
|
+
|
|
132
|
+
if self.nullable:
|
|
133
|
+
schema["nullable"] = True
|
|
134
|
+
|
|
135
|
+
if self.param_type == "array" and self.items:
|
|
136
|
+
schema["items"] = deepcopy(self.items)
|
|
137
|
+
|
|
138
|
+
if self.param_type == "object" and self.properties:
|
|
139
|
+
schema["properties"] = deepcopy(self.properties)
|
|
140
|
+
|
|
141
|
+
return schema
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _merge_schema_dicts(
|
|
145
|
+
base: Dict[str, Any], override: Dict[str, Any]
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
"""递归合并 JSON Schema 片段,override 拥有更高优先级"""
|
|
148
|
+
|
|
149
|
+
if not base:
|
|
150
|
+
return deepcopy(override)
|
|
151
|
+
if not override:
|
|
152
|
+
return deepcopy(base)
|
|
153
|
+
|
|
154
|
+
merged: Dict[str, Any] = deepcopy(base)
|
|
155
|
+
for key, value in override.items():
|
|
156
|
+
if (
|
|
157
|
+
key in merged
|
|
158
|
+
and isinstance(merged[key], dict)
|
|
159
|
+
and isinstance(value, dict)
|
|
160
|
+
):
|
|
161
|
+
merged[key] = _merge_schema_dicts(merged[key], value)
|
|
162
|
+
else:
|
|
163
|
+
merged[key] = deepcopy(value)
|
|
164
|
+
return merged
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _resolve_ref_schema(
|
|
168
|
+
ref: Optional[str], root_schema: Dict[str, Any]
|
|
169
|
+
) -> Optional[Dict[str, Any]]:
|
|
170
|
+
"""解析 $ref 指针"""
|
|
171
|
+
|
|
172
|
+
if not ref or not isinstance(ref, str) or not ref.startswith("#/"):
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
target: Any = root_schema
|
|
176
|
+
# 处理 JSON Pointer 转义
|
|
177
|
+
parts = ref[2:].split("/")
|
|
178
|
+
for raw_part in parts:
|
|
179
|
+
part = raw_part.replace("~1", "/").replace("~0", "~")
|
|
180
|
+
if not isinstance(target, dict) or part not in target:
|
|
181
|
+
return None
|
|
182
|
+
target = target[part]
|
|
183
|
+
|
|
184
|
+
if isinstance(target, dict):
|
|
185
|
+
return deepcopy(target)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _extract_core_schema(
|
|
190
|
+
field_schema: Dict[str, Any],
|
|
191
|
+
root_schema: Dict[str, Any],
|
|
192
|
+
seen_refs: Optional[Set[str]] = None,
|
|
193
|
+
) -> Tuple[Dict[str, Any], bool]:
|
|
194
|
+
"""展开 anyOf/oneOf/allOf/$ref,返回核心 schema 及 nullable 标记"""
|
|
195
|
+
|
|
196
|
+
schema = deepcopy(field_schema)
|
|
197
|
+
nullable = False
|
|
198
|
+
|
|
199
|
+
if seen_refs is None:
|
|
200
|
+
seen_refs = set()
|
|
201
|
+
|
|
202
|
+
def _pick_from_union(options: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
203
|
+
nonlocal nullable
|
|
204
|
+
chosen: Optional[Dict[str, Any]] = None
|
|
205
|
+
for option in options:
|
|
206
|
+
if not isinstance(option, dict):
|
|
207
|
+
continue
|
|
208
|
+
if option.get("type") == "null":
|
|
209
|
+
nullable = True
|
|
210
|
+
continue
|
|
211
|
+
resolved, is_nullable = _extract_core_schema(
|
|
212
|
+
option, root_schema, seen_refs
|
|
213
|
+
)
|
|
214
|
+
nullable = nullable or is_nullable
|
|
215
|
+
if chosen is None:
|
|
216
|
+
chosen = resolved
|
|
217
|
+
return chosen or {}
|
|
218
|
+
|
|
219
|
+
union_key = None
|
|
220
|
+
if "anyOf" in schema:
|
|
221
|
+
union_key = "anyOf"
|
|
222
|
+
elif "oneOf" in schema:
|
|
223
|
+
union_key = "oneOf"
|
|
224
|
+
|
|
225
|
+
if union_key:
|
|
226
|
+
options = schema.pop(union_key, [])
|
|
227
|
+
schema = _pick_from_union(options)
|
|
228
|
+
|
|
229
|
+
if "allOf" in schema:
|
|
230
|
+
merged: Dict[str, Any] = {}
|
|
231
|
+
for option in schema.pop("allOf", []):
|
|
232
|
+
if not isinstance(option, dict):
|
|
233
|
+
continue
|
|
234
|
+
resolved, is_nullable = _extract_core_schema(
|
|
235
|
+
option, root_schema, seen_refs
|
|
236
|
+
)
|
|
237
|
+
nullable = nullable or is_nullable
|
|
238
|
+
merged = _merge_schema_dicts(merged, resolved)
|
|
239
|
+
schema = _merge_schema_dicts(merged, schema)
|
|
240
|
+
|
|
241
|
+
ref = schema.get("$ref")
|
|
242
|
+
ref_schema = _resolve_ref_schema(ref, root_schema)
|
|
243
|
+
if ref_schema and isinstance(ref, str) and ref not in seen_refs:
|
|
244
|
+
seen_refs.add(ref)
|
|
245
|
+
schema.pop("$ref", None)
|
|
246
|
+
resolved, is_nullable = _extract_core_schema(
|
|
247
|
+
ref_schema, root_schema, seen_refs
|
|
248
|
+
)
|
|
249
|
+
seen_refs.discard(ref)
|
|
250
|
+
nullable = nullable or is_nullable
|
|
251
|
+
schema = _merge_schema_dicts(resolved, schema)
|
|
252
|
+
|
|
253
|
+
return schema, nullable
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class Tool:
|
|
257
|
+
"""通用工具定义
|
|
258
|
+
|
|
259
|
+
支持多种参数定义方式:
|
|
260
|
+
1. 使用 parameters 列表(ToolParameter)
|
|
261
|
+
2. 使用 args_schema(Pydantic BaseModel)
|
|
262
|
+
|
|
263
|
+
Attributes:
|
|
264
|
+
name: 工具名称
|
|
265
|
+
description: 工具描述
|
|
266
|
+
parameters: 参数列表(使用 ToolParameter 定义)
|
|
267
|
+
args_schema: 参数模型(使用 Pydantic BaseModel 定义)
|
|
268
|
+
func: 工具执行函数
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(
|
|
272
|
+
self,
|
|
273
|
+
name: str,
|
|
274
|
+
description: str = "",
|
|
275
|
+
parameters: Optional[List[ToolParameter]] = None,
|
|
276
|
+
args_schema: Optional[Type[BaseModel]] = None,
|
|
277
|
+
func: Optional[Callable] = None,
|
|
278
|
+
):
|
|
279
|
+
# Normalize tool name to avoid external provider limits (e.g. OpenAI 64 chars)
|
|
280
|
+
# If name length > 64, keep first 32 chars and append 32-char md5 sum of full name.
|
|
281
|
+
self.name = normalize_tool_name(name)
|
|
282
|
+
self.description = description
|
|
283
|
+
self.parameters = list(parameters or [])
|
|
284
|
+
self.args_schema = args_schema or _build_args_model_from_parameters(
|
|
285
|
+
name, self.parameters
|
|
286
|
+
)
|
|
287
|
+
if self.args_schema is None:
|
|
288
|
+
model_name = f"{self.name.title().replace('_', '')}Args"
|
|
289
|
+
self.args_schema = create_model(model_name) # type: ignore
|
|
290
|
+
elif not self.parameters:
|
|
291
|
+
self.parameters = self._generate_parameters_from_schema(
|
|
292
|
+
self.args_schema
|
|
293
|
+
)
|
|
294
|
+
self.func = func
|
|
295
|
+
|
|
296
|
+
def _generate_parameters_from_schema(
|
|
297
|
+
self, schema: Type[BaseModel]
|
|
298
|
+
) -> List[ToolParameter]:
|
|
299
|
+
"""从 Pydantic schema 生成参数列表"""
|
|
300
|
+
parameters = []
|
|
301
|
+
json_schema = schema.model_json_schema()
|
|
302
|
+
|
|
303
|
+
properties = json_schema.get("properties", {})
|
|
304
|
+
required_fields = set(json_schema.get("required", []))
|
|
305
|
+
|
|
306
|
+
for field_name, field_info in properties.items():
|
|
307
|
+
normalized_schema, nullable = _extract_core_schema(
|
|
308
|
+
field_info, json_schema
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
param_type = normalized_schema.get("type")
|
|
312
|
+
if not param_type:
|
|
313
|
+
if "properties" in normalized_schema:
|
|
314
|
+
param_type = "object"
|
|
315
|
+
elif "items" in normalized_schema:
|
|
316
|
+
param_type = "array"
|
|
317
|
+
elif "enum" in normalized_schema:
|
|
318
|
+
param_type = "string"
|
|
319
|
+
else:
|
|
320
|
+
param_type = field_info.get("type", "string")
|
|
321
|
+
|
|
322
|
+
description = (
|
|
323
|
+
field_info.get("description")
|
|
324
|
+
or normalized_schema.get("description")
|
|
325
|
+
or ""
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if "default" in field_info:
|
|
329
|
+
default = field_info.get("default")
|
|
330
|
+
else:
|
|
331
|
+
default = normalized_schema.get("default")
|
|
332
|
+
|
|
333
|
+
if "enum" in field_info:
|
|
334
|
+
enum = field_info.get("enum")
|
|
335
|
+
else:
|
|
336
|
+
enum = normalized_schema.get("enum")
|
|
337
|
+
|
|
338
|
+
items = normalized_schema.get("items")
|
|
339
|
+
if items is not None:
|
|
340
|
+
items = deepcopy(items)
|
|
341
|
+
|
|
342
|
+
properties_schema = normalized_schema.get("properties")
|
|
343
|
+
if properties_schema is not None:
|
|
344
|
+
properties_schema = deepcopy(properties_schema)
|
|
345
|
+
|
|
346
|
+
fmt = normalized_schema.get("format")
|
|
347
|
+
|
|
348
|
+
if (
|
|
349
|
+
not nullable
|
|
350
|
+
and field_name not in required_fields
|
|
351
|
+
and "default" in field_info
|
|
352
|
+
and field_info.get("default") is None
|
|
353
|
+
):
|
|
354
|
+
nullable = True
|
|
355
|
+
|
|
356
|
+
parameters.append(
|
|
357
|
+
ToolParameter(
|
|
358
|
+
name=field_name,
|
|
359
|
+
param_type=param_type,
|
|
360
|
+
description=description,
|
|
361
|
+
required=field_name in required_fields,
|
|
362
|
+
default=default,
|
|
363
|
+
enum=enum,
|
|
364
|
+
items=items,
|
|
365
|
+
properties=properties_schema,
|
|
366
|
+
format=fmt,
|
|
367
|
+
nullable=nullable,
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return parameters
|
|
372
|
+
|
|
373
|
+
def get_parameters_schema(self) -> Dict[str, Any]:
|
|
374
|
+
"""获取参数的 JSON Schema"""
|
|
375
|
+
if self.args_schema is None:
|
|
376
|
+
return {"type": "object", "properties": {}}
|
|
377
|
+
raw_schema = self.args_schema.model_json_schema()
|
|
378
|
+
schema = deepcopy(raw_schema)
|
|
379
|
+
|
|
380
|
+
properties = raw_schema.get("properties")
|
|
381
|
+
if not isinstance(properties, dict):
|
|
382
|
+
return schema
|
|
383
|
+
|
|
384
|
+
normalized_properties: Dict[str, Any] = {}
|
|
385
|
+
required_fields = set(schema.get("required", []))
|
|
386
|
+
meta_keys = (
|
|
387
|
+
"default",
|
|
388
|
+
"description",
|
|
389
|
+
"examples",
|
|
390
|
+
"title",
|
|
391
|
+
"deprecated",
|
|
392
|
+
"enum",
|
|
393
|
+
"const",
|
|
394
|
+
"format",
|
|
395
|
+
)
|
|
396
|
+
for field_name, field_schema in properties.items():
|
|
397
|
+
normalized, nullable = _extract_core_schema(
|
|
398
|
+
field_schema, raw_schema
|
|
399
|
+
)
|
|
400
|
+
enriched = deepcopy(normalized) if normalized else {}
|
|
401
|
+
|
|
402
|
+
for meta_key in meta_keys:
|
|
403
|
+
if meta_key in field_schema:
|
|
404
|
+
enriched[meta_key] = deepcopy(field_schema[meta_key])
|
|
405
|
+
|
|
406
|
+
# 确保 description 字段总是存在(即使为空字符串)
|
|
407
|
+
if "description" not in enriched:
|
|
408
|
+
enriched["description"] = ""
|
|
409
|
+
|
|
410
|
+
default_is_none = (
|
|
411
|
+
field_name not in required_fields
|
|
412
|
+
and "default" in field_schema
|
|
413
|
+
and field_schema.get("default") is None
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if nullable or field_schema.get("nullable") or default_is_none:
|
|
417
|
+
enriched["nullable"] = True
|
|
418
|
+
|
|
419
|
+
normalized_properties[field_name] = enriched
|
|
420
|
+
|
|
421
|
+
schema["properties"] = normalized_properties
|
|
422
|
+
return schema
|
|
423
|
+
|
|
424
|
+
def to_openai_function(self) -> Dict[str, Any]:
|
|
425
|
+
"""转换为 OpenAI Function Calling 格式"""
|
|
426
|
+
return {
|
|
427
|
+
"name": self.name,
|
|
428
|
+
"description": self.description,
|
|
429
|
+
"parameters": self.get_parameters_schema(),
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
def to_anthropic_tool(self) -> Dict[str, Any]:
|
|
433
|
+
"""转换为 Anthropic Claude Tools 格式"""
|
|
434
|
+
return {
|
|
435
|
+
"name": self.name,
|
|
436
|
+
"description": self.description,
|
|
437
|
+
"input_schema": self.get_parameters_schema(),
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
def to_langchain(self) -> Any:
|
|
441
|
+
"""转换为 LangChain Tool 格式
|
|
442
|
+
|
|
443
|
+
优先使用适配器模式,如果适配器未注册则回退到旧实现。
|
|
444
|
+
"""
|
|
445
|
+
# 尝试使用适配器模式
|
|
446
|
+
try:
|
|
447
|
+
from agentrun.integration.utils.canonical import CanonicalTool
|
|
448
|
+
from agentrun.integration.utils.converter import get_converter
|
|
449
|
+
|
|
450
|
+
converter = get_converter()
|
|
451
|
+
adapter = converter._tool_adapters.get("langchain")
|
|
452
|
+
if adapter is not None:
|
|
453
|
+
# 转换为中间格式
|
|
454
|
+
canonical_tool = CanonicalTool(
|
|
455
|
+
name=self.name,
|
|
456
|
+
description=self.description,
|
|
457
|
+
parameters=self.get_parameters_schema(),
|
|
458
|
+
func=self.func,
|
|
459
|
+
)
|
|
460
|
+
# 转换为 LangChain 格式
|
|
461
|
+
result = adapter.from_canonical([canonical_tool])
|
|
462
|
+
return result[0] if result else None
|
|
463
|
+
except (ImportError, AttributeError, KeyError):
|
|
464
|
+
pass
|
|
465
|
+
|
|
466
|
+
# 回退到旧实现(保持向后兼容)
|
|
467
|
+
try:
|
|
468
|
+
from langchain_core.tools import StructuredTool # type: ignore
|
|
469
|
+
except ImportError as e:
|
|
470
|
+
raise ImportError(
|
|
471
|
+
"LangChain is not installed. "
|
|
472
|
+
"Install it with: pip install langchain-core"
|
|
473
|
+
) from e
|
|
474
|
+
|
|
475
|
+
return StructuredTool.from_function(
|
|
476
|
+
func=self.func,
|
|
477
|
+
name=self.name,
|
|
478
|
+
description=self.description,
|
|
479
|
+
args_schema=self.args_schema,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def to_google_adk(self) -> Callable:
|
|
483
|
+
"""转换为 Google ADK Tool 格式
|
|
484
|
+
|
|
485
|
+
优先使用适配器模式,如果适配器未注册则回退到旧实现。
|
|
486
|
+
Google ADK 直接使用 Python 函数作为工具。
|
|
487
|
+
"""
|
|
488
|
+
# 尝试使用适配器模式
|
|
489
|
+
try:
|
|
490
|
+
from agentrun.integration.utils.canonical import CanonicalTool
|
|
491
|
+
from agentrun.integration.utils.converter import get_converter
|
|
492
|
+
|
|
493
|
+
converter = get_converter()
|
|
494
|
+
adapter = converter._tool_adapters.get("google_adk")
|
|
495
|
+
if adapter is not None:
|
|
496
|
+
# 转换为中间格式
|
|
497
|
+
canonical_tool = CanonicalTool(
|
|
498
|
+
name=self.name,
|
|
499
|
+
description=self.description,
|
|
500
|
+
parameters=self.get_parameters_schema(),
|
|
501
|
+
func=self.func,
|
|
502
|
+
)
|
|
503
|
+
# 转换为 Google ADK 格式
|
|
504
|
+
result = adapter.from_canonical([canonical_tool])
|
|
505
|
+
if result:
|
|
506
|
+
return result[0] # type: ignore
|
|
507
|
+
except (ImportError, AttributeError, KeyError):
|
|
508
|
+
pass
|
|
509
|
+
|
|
510
|
+
# 回退到旧实现(保持向后兼容)
|
|
511
|
+
if self.func is None:
|
|
512
|
+
raise ValueError(
|
|
513
|
+
f"Tool '{self.name}' has no function implementation"
|
|
514
|
+
)
|
|
515
|
+
return self.func
|
|
516
|
+
|
|
517
|
+
def to_agentscope(self) -> Dict[str, Any]:
|
|
518
|
+
"""转换为 AgentScope Tool 格式"""
|
|
519
|
+
try:
|
|
520
|
+
from agentrun.integration.utils.canonical import CanonicalTool
|
|
521
|
+
from agentrun.integration.utils.converter import get_converter
|
|
522
|
+
|
|
523
|
+
converter = get_converter()
|
|
524
|
+
adapter = converter._tool_adapters.get("agentscope")
|
|
525
|
+
if adapter is not None:
|
|
526
|
+
canonical_tool = CanonicalTool(
|
|
527
|
+
name=self.name,
|
|
528
|
+
description=self.description,
|
|
529
|
+
parameters=self.get_parameters_schema(),
|
|
530
|
+
func=self.func,
|
|
531
|
+
)
|
|
532
|
+
result = adapter.from_canonical([canonical_tool])
|
|
533
|
+
return result[0] if result else {}
|
|
534
|
+
except (ImportError, AttributeError, KeyError):
|
|
535
|
+
pass
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
"type": "function",
|
|
539
|
+
"function": {
|
|
540
|
+
"name": self.name,
|
|
541
|
+
"description": self.description,
|
|
542
|
+
"parameters": self.get_parameters_schema(),
|
|
543
|
+
},
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
def to_langgraph(self) -> Any:
|
|
547
|
+
"""转换为 LangGraph Tool 格式
|
|
548
|
+
|
|
549
|
+
LangGraph 与 LangChain 完全兼容,因此使用相同的接口。
|
|
550
|
+
"""
|
|
551
|
+
return self.to_langchain()
|
|
552
|
+
|
|
553
|
+
def to_crewai(self) -> Any:
|
|
554
|
+
"""转换为 CrewAI Tool 格式
|
|
555
|
+
|
|
556
|
+
CrewAI 内部使用 LangChain,因此使用相同的接口。
|
|
557
|
+
"""
|
|
558
|
+
return self.to_langchain()
|
|
559
|
+
|
|
560
|
+
def to_pydanticai(self) -> Any:
|
|
561
|
+
"""转换为 PydanticAI Tool 格式"""
|
|
562
|
+
# PydanticAI 使用函数装饰器定义工具
|
|
563
|
+
# 返回一个包装函数,使其符合 PydanticAI 的工具接口
|
|
564
|
+
if self.func is None:
|
|
565
|
+
raise ValueError(
|
|
566
|
+
f"Tool '{self.name}' has no function implementation"
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# 创建一个包装函数,添加必要的元数据
|
|
570
|
+
func = self.func
|
|
571
|
+
|
|
572
|
+
# 添加函数文档字符串
|
|
573
|
+
if not func.__doc__:
|
|
574
|
+
# 使用 setattr 绕过类型检查
|
|
575
|
+
object.__setattr__(func, "__doc__", self.description)
|
|
576
|
+
|
|
577
|
+
# 添加函数名称
|
|
578
|
+
if not hasattr(func, "__name__"):
|
|
579
|
+
object.__setattr__(func, "__name__", self.name)
|
|
580
|
+
|
|
581
|
+
# 为 PydanticAI 添加参数 schema 信息
|
|
582
|
+
# 使用 setattr 绕过类型检查
|
|
583
|
+
object.__setattr__(
|
|
584
|
+
func,
|
|
585
|
+
"_tool_schema",
|
|
586
|
+
{
|
|
587
|
+
"name": self.name,
|
|
588
|
+
"description": self.description,
|
|
589
|
+
"parameters": self.get_parameters_schema(),
|
|
590
|
+
},
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
return func
|
|
594
|
+
|
|
595
|
+
def __get__(self, obj: Any, objtype: Any = None) -> "Tool":
|
|
596
|
+
"""实现描述符协议,使 Tool 在类属性访问时自动绑定到实例
|
|
597
|
+
|
|
598
|
+
这允许在工具方法内部调用其他 @tool 装饰的方法时正常工作。
|
|
599
|
+
例如:goto 方法调用 self.browser_navigate() 时,会自动获取绑定版本。
|
|
600
|
+
"""
|
|
601
|
+
if obj is None:
|
|
602
|
+
# 通过类访问(如 BrowserToolSet.browser_navigate),返回未绑定的 Tool
|
|
603
|
+
return self
|
|
604
|
+
|
|
605
|
+
# 通过实例访问,返回绑定到该实例的 Tool
|
|
606
|
+
# 使用实例的 __dict__ 作为缓存,避免每次访问都创建新的 Tool 对象
|
|
607
|
+
cache_key = f"_bound_tool_{id(self)}"
|
|
608
|
+
if cache_key not in obj.__dict__:
|
|
609
|
+
obj.__dict__[cache_key] = self.bind(obj)
|
|
610
|
+
return obj.__dict__[cache_key]
|
|
611
|
+
|
|
612
|
+
def bind(self, instance: Any) -> "Tool":
|
|
613
|
+
"""绑定工具到实例,便于在类中定义工具方法"""
|
|
614
|
+
|
|
615
|
+
if self.func is None:
|
|
616
|
+
raise ValueError(
|
|
617
|
+
f"Tool '{self.name}' has no function implementation"
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# 复制当前工具的定义,覆写执行函数为绑定后的函数
|
|
621
|
+
parameters = deepcopy(self.parameters) if self.parameters else None
|
|
622
|
+
original_func = self.func
|
|
623
|
+
|
|
624
|
+
@wraps(original_func)
|
|
625
|
+
def bound(*args, **kwargs):
|
|
626
|
+
return original_func(instance, *args, **kwargs)
|
|
627
|
+
|
|
628
|
+
# 更新函数签名,移除 self 参数
|
|
629
|
+
try:
|
|
630
|
+
original_sig = inspect.signature(original_func)
|
|
631
|
+
params = list(original_sig.parameters.values())
|
|
632
|
+
if params and params[0].name == "self":
|
|
633
|
+
params = params[1:]
|
|
634
|
+
setattr(
|
|
635
|
+
bound, "__signature__", original_sig.replace(parameters=params)
|
|
636
|
+
)
|
|
637
|
+
except (TypeError, ValueError):
|
|
638
|
+
# 某些内置函数不支持签名操作,忽略即可
|
|
639
|
+
pass
|
|
640
|
+
|
|
641
|
+
# 清理 self 的注解,确保外部框架解析一致
|
|
642
|
+
original_annotations = getattr(original_func, "__annotations__", {})
|
|
643
|
+
if original_annotations:
|
|
644
|
+
setattr(
|
|
645
|
+
bound,
|
|
646
|
+
"__annotations__",
|
|
647
|
+
{
|
|
648
|
+
key: value
|
|
649
|
+
for key, value in original_annotations.items()
|
|
650
|
+
if key != "self"
|
|
651
|
+
},
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
return Tool(
|
|
655
|
+
name=self.name,
|
|
656
|
+
description=self.description,
|
|
657
|
+
parameters=parameters,
|
|
658
|
+
args_schema=self.args_schema,
|
|
659
|
+
func=bound,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
def openai(self) -> Dict[str, Any]:
|
|
663
|
+
"""to_openai_function 的别名"""
|
|
664
|
+
return self.to_openai_function()
|
|
665
|
+
|
|
666
|
+
def langchain(self) -> Any:
|
|
667
|
+
"""to_langchain 的别名"""
|
|
668
|
+
return self.to_langchain()
|
|
669
|
+
|
|
670
|
+
def google_adk(self) -> Callable:
|
|
671
|
+
"""to_google_adk 的别名"""
|
|
672
|
+
return self.to_google_adk()
|
|
673
|
+
|
|
674
|
+
def pydanticai(self) -> Any:
|
|
675
|
+
"""to_pydanticai 的别名(暂未实现)"""
|
|
676
|
+
# TODO: 实现 PydanticAI 转换
|
|
677
|
+
raise NotImplementedError("PydanticAI conversion not implemented yet")
|
|
678
|
+
|
|
679
|
+
def __call__(self, *args, **kwargs) -> Any:
|
|
680
|
+
"""直接调用工具函数"""
|
|
681
|
+
if self.func is None:
|
|
682
|
+
raise ValueError(
|
|
683
|
+
f"Tool '{self.name}' has no function implementation"
|
|
684
|
+
)
|
|
685
|
+
return self.func(*args, **kwargs)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class CommonToolSet:
|
|
689
|
+
"""工具集
|
|
690
|
+
|
|
691
|
+
管理多个工具,提供批量转换和过滤功能。
|
|
692
|
+
|
|
693
|
+
默认会收集子类中定义的 `Tool` 属性,因此简单的继承即可完成
|
|
694
|
+
工具集的声明。也可以通过传入 ``tools_list`` 或调用 ``register``
|
|
695
|
+
方法来自定义工具集合。
|
|
696
|
+
|
|
697
|
+
Attributes:
|
|
698
|
+
tools_list: 工具列表
|
|
699
|
+
"""
|
|
700
|
+
|
|
701
|
+
def __init__(self, tools_list: Optional[List[Tool]] = None):
|
|
702
|
+
if tools_list is None:
|
|
703
|
+
tools_list = self._collect_declared_tools()
|
|
704
|
+
self._tools: List[Tool] = list(tools_list)
|
|
705
|
+
|
|
706
|
+
def _collect_declared_tools(self) -> List[Tool]:
|
|
707
|
+
"""收集并绑定子类中定义的 Tool 对象"""
|
|
708
|
+
|
|
709
|
+
tools: List[Tool] = []
|
|
710
|
+
seen: set[str] = set()
|
|
711
|
+
for cls in type(self).mro():
|
|
712
|
+
if cls in {CommonToolSet, object}:
|
|
713
|
+
continue
|
|
714
|
+
class_dict = getattr(cls, "__dict__", {})
|
|
715
|
+
for attr_name, attr_value in class_dict.items():
|
|
716
|
+
if attr_name.startswith("_") or attr_name in seen:
|
|
717
|
+
continue
|
|
718
|
+
if isinstance(attr_value, Tool):
|
|
719
|
+
seen.add(attr_name)
|
|
720
|
+
tools.append(attr_value.bind(self))
|
|
721
|
+
return tools
|
|
722
|
+
|
|
723
|
+
# def register(self, *tools: Tool) -> None:
|
|
724
|
+
# """动态注册工具"""
|
|
725
|
+
|
|
726
|
+
# for tool in tools:
|
|
727
|
+
# if not isinstance(tool, Tool):
|
|
728
|
+
# raise TypeError(
|
|
729
|
+
# f"register() 只接受 Tool 实例, 收到类型: {type(tool)!r}"
|
|
730
|
+
# )
|
|
731
|
+
# self._tools.append(tool)
|
|
732
|
+
|
|
733
|
+
def tools(
|
|
734
|
+
self,
|
|
735
|
+
prefix: Optional[str] = None,
|
|
736
|
+
modify_tool_name: Optional[Callable[[Tool], Tool]] = None,
|
|
737
|
+
filter_tools_by_name: Optional[Callable[[str], bool]] = None,
|
|
738
|
+
):
|
|
739
|
+
"""获取工具列表
|
|
740
|
+
|
|
741
|
+
Args:
|
|
742
|
+
prefix: 为所有工具名称添加前缀
|
|
743
|
+
modify_tool_name: 自定义工具修改函数
|
|
744
|
+
filter_tools_by_name: 工具名称过滤函数
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
处理后的工具列表
|
|
748
|
+
"""
|
|
749
|
+
tools = list(self._tools)
|
|
750
|
+
|
|
751
|
+
# 应用过滤器
|
|
752
|
+
if filter_tools_by_name is not None:
|
|
753
|
+
tools = [t for t in tools if filter_tools_by_name(t.name)]
|
|
754
|
+
|
|
755
|
+
# 应用前缀
|
|
756
|
+
if prefix is not None:
|
|
757
|
+
tools = [
|
|
758
|
+
Tool(
|
|
759
|
+
name=f"{prefix}{t.name}",
|
|
760
|
+
description=t.description,
|
|
761
|
+
parameters=t.parameters,
|
|
762
|
+
args_schema=t.args_schema,
|
|
763
|
+
func=t.func,
|
|
764
|
+
)
|
|
765
|
+
for t in tools
|
|
766
|
+
]
|
|
767
|
+
|
|
768
|
+
# 应用自定义修改函数
|
|
769
|
+
if modify_tool_name is not None:
|
|
770
|
+
tools = [modify_tool_name(t) for t in tools]
|
|
771
|
+
|
|
772
|
+
from agentrun.integration.utils.canonical import CanonicalTool
|
|
773
|
+
|
|
774
|
+
return [
|
|
775
|
+
CanonicalTool(
|
|
776
|
+
name=tool.name,
|
|
777
|
+
description=tool.description,
|
|
778
|
+
parameters=tool.get_parameters_schema(),
|
|
779
|
+
func=tool.func,
|
|
780
|
+
)
|
|
781
|
+
for tool in tools
|
|
782
|
+
]
|
|
783
|
+
|
|
784
|
+
def __convert_tools(
|
|
785
|
+
self,
|
|
786
|
+
adapter_name: str,
|
|
787
|
+
prefix: Optional[str] = None,
|
|
788
|
+
modify_tool_name: Optional[Callable[[Tool], Tool]] = None,
|
|
789
|
+
filter_tools_by_name: Optional[Callable[[str], bool]] = None,
|
|
790
|
+
):
|
|
791
|
+
tools = self.tools(prefix, modify_tool_name, filter_tools_by_name)
|
|
792
|
+
|
|
793
|
+
# 尝试使用适配器模式进行批量转换
|
|
794
|
+
try:
|
|
795
|
+
from agentrun.integration.utils.converter import get_converter
|
|
796
|
+
|
|
797
|
+
converter = get_converter()
|
|
798
|
+
adapter = converter._tool_adapters.get(adapter_name)
|
|
799
|
+
if adapter is not None:
|
|
800
|
+
return adapter.from_canonical(tools)
|
|
801
|
+
except (ImportError, AttributeError, KeyError):
|
|
802
|
+
pass
|
|
803
|
+
|
|
804
|
+
logger.warning(
|
|
805
|
+
f"adapter {adapter_name} not found, returning empty list"
|
|
806
|
+
)
|
|
807
|
+
return []
|
|
808
|
+
|
|
809
|
+
@classmethod
|
|
810
|
+
def from_agentrun_toolset(
|
|
811
|
+
cls,
|
|
812
|
+
toolset: ToolSet,
|
|
813
|
+
config: Optional[Any] = None,
|
|
814
|
+
refresh: bool = False,
|
|
815
|
+
) -> "CommonToolSet":
|
|
816
|
+
"""从 AgentRun ToolSet 创建通用工具集
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
toolset: agentrun.toolset.toolset.ToolSet 实例
|
|
820
|
+
config: 额外的请求配置,调用工具时会自动合并
|
|
821
|
+
refresh: 是否先刷新最新信息
|
|
822
|
+
|
|
823
|
+
Returns:
|
|
824
|
+
通用 ToolSet 实例,可直接调用 .to_openai_function()、.to_langchain() 等
|
|
825
|
+
|
|
826
|
+
Example:
|
|
827
|
+
>>> from agentrun.toolset.client import ToolSetClient
|
|
828
|
+
>>> from agentrun.integration.common import from_agentrun_toolset
|
|
829
|
+
>>>
|
|
830
|
+
>>> client = ToolSetClient()
|
|
831
|
+
>>> remote_toolset = client.get(name="my-toolset")
|
|
832
|
+
>>> common_toolset = from_agentrun_toolset(remote_toolset)
|
|
833
|
+
>>>
|
|
834
|
+
>>> # 使用已有的转换方法
|
|
835
|
+
>>> openai_tools = common_toolset.to_openai_function()
|
|
836
|
+
>>> google_adk_tools = common_toolset.to_google_adk()
|
|
837
|
+
"""
|
|
838
|
+
|
|
839
|
+
if refresh:
|
|
840
|
+
toolset = toolset.get(config=config)
|
|
841
|
+
|
|
842
|
+
# 获取所有工具元数据
|
|
843
|
+
tools_meta = toolset.list_tools(config=config) or []
|
|
844
|
+
integration_tools: List[Tool] = []
|
|
845
|
+
seen_names = set()
|
|
846
|
+
|
|
847
|
+
for meta in tools_meta:
|
|
848
|
+
tool = _build_tool_from_meta(toolset, meta, config)
|
|
849
|
+
if tool:
|
|
850
|
+
# 检测工具名冲突
|
|
851
|
+
if tool.name in seen_names:
|
|
852
|
+
logger.warning(
|
|
853
|
+
f"Duplicate tool name '{tool.name}' detected, "
|
|
854
|
+
"second occurrence will be skipped"
|
|
855
|
+
)
|
|
856
|
+
continue
|
|
857
|
+
seen_names.add(tool.name)
|
|
858
|
+
integration_tools.append(tool)
|
|
859
|
+
|
|
860
|
+
return CommonToolSet(integration_tools)
|
|
861
|
+
|
|
862
|
+
def to_openai_function(
|
|
863
|
+
self,
|
|
864
|
+
prefix: Optional[str] = None,
|
|
865
|
+
modify_tool_name: Optional[Callable[[Tool], Tool]] = None,
|
|
866
|
+
filter_tools_by_name: Optional[Callable[[str], bool]] = None,
|
|
867
|
+
) -> List[Dict[str, Any]]:
|
|
868
|
+
"""批量转换为 OpenAI Function Calling 格式"""
|
|
869
|
+
tools = self.tools(prefix, modify_tool_name, filter_tools_by_name)
|
|
870
|
+
return [
|
|
871
|
+
{
|
|
872
|
+
"name": tool.name,
|
|
873
|
+
"description": tool.description,
|
|
874
|
+
"parameters": tool.parameters,
|
|
875
|
+
}
|
|
876
|
+
for tool in tools
|
|
877
|
+
]
|
|
878
|
+
|
|
879
|
+
def to_anthropic_tool(
|
|
880
|
+
self,
|
|
881
|
+
prefix: Optional[str] = None,
|
|
882
|
+
modify_tool_name: Optional[Callable[[Tool], Tool]] = None,
|
|
883
|
+
filter_tools_by_name: Optional[Callable[[str], bool]] = None,
|
|
884
|
+
) -> List[Dict[str, Any]]:
|
|
885
|
+
"""批量转换为 Anthropic Claude Tools 格式"""
|
|
886
|
+
tools = self.tools(prefix, modify_tool_name, filter_tools_by_name)
|
|
887
|
+
return [
|
|
888
|
+
{
|
|
889
|
+
"name": tool.name,
|
|
890
|
+
"description": tool.description,
|
|
891
|
+
"input_schema": tool.parameters,
|
|
892
|
+
}
|
|
893
|
+
for tool in tools
|
|
894
|
+
]
|
|
895
|
+
|
|
896
|
+
def to_langchain(
|
|
897
|
+
self,
|
|
898
|
+
prefix: Optional[str] = None,
|
|
899
|
+
modify_tool_name: Optional[Callable[[Tool], Tool]] = None,
|
|
900
|
+
filter_tools_by_name: Optional[Callable[[str], bool]] = None,
|
|
901
|
+
) -> List[Any]:
|
|
902
|
+
"""批量转换为 LangChain Tool 格式
|
|
903
|
+
|
|
904
|
+
优先使用适配器模式进行批量转换,提高效率。
|
|
905
|
+
"""
|
|
906
|
+
return self.__convert_tools(
|
|
907
|
+
"langchain", prefix, modify_tool_name, filter_tools_by_name
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
def to_google_adk(
|
|
911
|
+
self,
|
|
912
|
+
prefix: Optional[str] = None,
|
|
913
|
+
modify_tool_name: Optional[Callable[[Tool], Tool]] = None,
|
|
914
|
+
filter_tools_by_name: Optional[Callable[[str], bool]] = None,
|
|
915
|
+
):
|
|
916
|
+
"""批量转换为 Google ADK Tool 格式
|
|
917
|
+
|
|
918
|
+
优先使用适配器模式进行批量转换,提高效率。
|
|
919
|
+
"""
|
|
920
|
+
return self.__convert_tools(
|
|
921
|
+
"google_adk", prefix, modify_tool_name, filter_tools_by_name
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
def to_agentscope(
|
|
925
|
+
self,
|
|
926
|
+
prefix: Optional[str] = None,
|
|
927
|
+
modify_tool_name: Optional[Callable[[Tool], Tool]] = None,
|
|
928
|
+
filter_tools_by_name: Optional[Callable[[str], bool]] = None,
|
|
929
|
+
):
|
|
930
|
+
"""批量转换为 AgentScope Tool 格式"""
|
|
931
|
+
return self.__convert_tools(
|
|
932
|
+
"agentscope", prefix, modify_tool_name, filter_tools_by_name
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
def to_langgraph(
|
|
936
|
+
self,
|
|
937
|
+
prefix: Optional[str] = None,
|
|
938
|
+
modify_tool_name: Optional[Callable[[Tool], Tool]] = None,
|
|
939
|
+
filter_tools_by_name: Optional[Callable[[str], bool]] = None,
|
|
940
|
+
) -> List[Any]:
|
|
941
|
+
"""批量转换为 LangGraph Tool 格式
|
|
942
|
+
|
|
943
|
+
LangGraph 与 LangChain 完全兼容,因此使用相同的接口。
|
|
944
|
+
"""
|
|
945
|
+
return self.__convert_tools(
|
|
946
|
+
"langgraph", prefix, modify_tool_name, filter_tools_by_name
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
def to_crewai(
|
|
950
|
+
self,
|
|
951
|
+
prefix: Optional[str] = None,
|
|
952
|
+
modify_tool_name: Optional[Callable[[Tool], Tool]] = None,
|
|
953
|
+
filter_tools_by_name: Optional[Callable[[str], bool]] = None,
|
|
954
|
+
) -> List[Any]:
|
|
955
|
+
"""批量转换为 CrewAI Tool 格式
|
|
956
|
+
|
|
957
|
+
CrewAI 内部使用 LangChain,因此使用相同的接口。
|
|
958
|
+
"""
|
|
959
|
+
return self.__convert_tools(
|
|
960
|
+
"crewai", prefix, modify_tool_name, filter_tools_by_name
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
def to_pydantic_ai(
|
|
964
|
+
self,
|
|
965
|
+
prefix: Optional[str] = None,
|
|
966
|
+
modify_tool_name: Optional[Callable[[Tool], Tool]] = None,
|
|
967
|
+
filter_tools_by_name: Optional[Callable[[str], bool]] = None,
|
|
968
|
+
) -> List[Any]:
|
|
969
|
+
"""批量转换为 PydanticAI Tool 格式"""
|
|
970
|
+
return self.__convert_tools(
|
|
971
|
+
"pydantic_ai", prefix, modify_tool_name, filter_tools_by_name
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _extract_type_hints_from_function(
|
|
976
|
+
func: Callable,
|
|
977
|
+
) -> Dict[str, Any]:
|
|
978
|
+
"""从函数签名提取类型提示"""
|
|
979
|
+
try:
|
|
980
|
+
hints = get_type_hints(func, include_extras=True)
|
|
981
|
+
except TypeError:
|
|
982
|
+
hints = get_type_hints(func)
|
|
983
|
+
except Exception:
|
|
984
|
+
# 如果无法获取类型提示,使用空字典
|
|
985
|
+
hints = {}
|
|
986
|
+
|
|
987
|
+
sig = inspect.signature(func)
|
|
988
|
+
result = {}
|
|
989
|
+
|
|
990
|
+
for param_name, param in sig.parameters.items():
|
|
991
|
+
if param_name == "self":
|
|
992
|
+
continue
|
|
993
|
+
if param_name in hints:
|
|
994
|
+
result[param_name] = hints[param_name]
|
|
995
|
+
else:
|
|
996
|
+
result[param_name] = str
|
|
997
|
+
|
|
998
|
+
return result
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def tool(
|
|
1002
|
+
name: Optional[str] = None,
|
|
1003
|
+
description: Optional[str] = None,
|
|
1004
|
+
) -> Callable[[Callable], Tool]:
|
|
1005
|
+
"""工具装饰器
|
|
1006
|
+
|
|
1007
|
+
从函数自动创建 Tool 实例。
|
|
1008
|
+
|
|
1009
|
+
Args:
|
|
1010
|
+
name: 工具名称,默认使用函数名
|
|
1011
|
+
description: 工具描述,默认使用函数文档字符串
|
|
1012
|
+
|
|
1013
|
+
Returns:
|
|
1014
|
+
装饰后的 Tool 实例
|
|
1015
|
+
|
|
1016
|
+
Example:
|
|
1017
|
+
>>> @tool()
|
|
1018
|
+
... def calculator(operation: Literal["add", "subtract"], a: float, b: float) -> float:
|
|
1019
|
+
... '''执行数学运算'''
|
|
1020
|
+
... if operation == "add":
|
|
1021
|
+
... return a + b
|
|
1022
|
+
... return a - b
|
|
1023
|
+
"""
|
|
1024
|
+
|
|
1025
|
+
def decorator(func: Callable) -> Tool:
|
|
1026
|
+
tool_name = name or func.__name__
|
|
1027
|
+
# ensure tool name is normalized
|
|
1028
|
+
tool_name = normalize_tool_name(tool_name)
|
|
1029
|
+
tool_description = description or (
|
|
1030
|
+
func.__doc__.strip() if func.__doc__ else ""
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
# 获取函数签名
|
|
1034
|
+
sig = inspect.signature(func)
|
|
1035
|
+
type_hints = _extract_type_hints_from_function(func)
|
|
1036
|
+
|
|
1037
|
+
# 构建 Pydantic 字段
|
|
1038
|
+
fields: Dict[str, Any] = {}
|
|
1039
|
+
|
|
1040
|
+
for param_name, param in sig.parameters.items():
|
|
1041
|
+
if param_name == "self":
|
|
1042
|
+
continue
|
|
1043
|
+
param_type = type_hints.get(param_name, str)
|
|
1044
|
+
|
|
1045
|
+
# 检查是否使用了 Annotated 类型
|
|
1046
|
+
origin = get_origin(param_type)
|
|
1047
|
+
if origin is not None:
|
|
1048
|
+
# 处理 Annotated[T, Field(...)] 形式
|
|
1049
|
+
args = get_args(param_type)
|
|
1050
|
+
if args:
|
|
1051
|
+
actual_type = args[0]
|
|
1052
|
+
# 查找 Field 对象
|
|
1053
|
+
field_obj = None
|
|
1054
|
+
for arg in args[1:]:
|
|
1055
|
+
if isinstance(arg, type(Field())):
|
|
1056
|
+
field_obj = arg
|
|
1057
|
+
break
|
|
1058
|
+
|
|
1059
|
+
if field_obj is not None:
|
|
1060
|
+
# 如果有默认值
|
|
1061
|
+
if param.default is not inspect.Parameter.empty:
|
|
1062
|
+
fields[param_name] = (
|
|
1063
|
+
actual_type,
|
|
1064
|
+
field_obj,
|
|
1065
|
+
)
|
|
1066
|
+
else:
|
|
1067
|
+
fields[param_name] = (actual_type, field_obj)
|
|
1068
|
+
else:
|
|
1069
|
+
# 没有 Field,使用基本类型
|
|
1070
|
+
if param.default is not inspect.Parameter.empty:
|
|
1071
|
+
fields[param_name] = (
|
|
1072
|
+
actual_type,
|
|
1073
|
+
Field(default=param.default),
|
|
1074
|
+
)
|
|
1075
|
+
else:
|
|
1076
|
+
fields[param_name] = (actual_type, ...)
|
|
1077
|
+
else:
|
|
1078
|
+
# 无法解析,使用字符串
|
|
1079
|
+
if param.default is not inspect.Parameter.empty:
|
|
1080
|
+
fields[param_name] = (str, Field(default=param.default))
|
|
1081
|
+
else:
|
|
1082
|
+
fields[param_name] = (str, ...)
|
|
1083
|
+
else:
|
|
1084
|
+
# 普通类型
|
|
1085
|
+
if param.default is not inspect.Parameter.empty:
|
|
1086
|
+
fields[param_name] = (
|
|
1087
|
+
param_type,
|
|
1088
|
+
Field(default=param.default),
|
|
1089
|
+
)
|
|
1090
|
+
else:
|
|
1091
|
+
fields[param_name] = (param_type, ...)
|
|
1092
|
+
|
|
1093
|
+
# 创建 Pydantic 模型
|
|
1094
|
+
# 模型名称与工具名保持一致
|
|
1095
|
+
model_name = tool_name
|
|
1096
|
+
args_schema = create_model(model_name, **fields) # type: ignore
|
|
1097
|
+
|
|
1098
|
+
def _func(*args, **kwargs):
|
|
1099
|
+
logger.debug(
|
|
1100
|
+
"invoke tool %s with arguments %s %s", tool_name, args, kwargs
|
|
1101
|
+
)
|
|
1102
|
+
results = func(*args, **kwargs)
|
|
1103
|
+
logger.debug(
|
|
1104
|
+
"invoke tool %s finished with result %s", tool_name, results
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
# if type(results) is str:
|
|
1108
|
+
# return {"result": results}
|
|
1109
|
+
|
|
1110
|
+
return results
|
|
1111
|
+
|
|
1112
|
+
return Tool(
|
|
1113
|
+
name=tool_name,
|
|
1114
|
+
description=tool_description,
|
|
1115
|
+
args_schema=args_schema,
|
|
1116
|
+
func=_func,
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
return decorator
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def from_pydantic(
|
|
1123
|
+
name: str,
|
|
1124
|
+
description: str,
|
|
1125
|
+
args_schema: Type[BaseModel],
|
|
1126
|
+
func: Callable,
|
|
1127
|
+
) -> Tool:
|
|
1128
|
+
"""从 Pydantic BaseModel 创建 Tool
|
|
1129
|
+
|
|
1130
|
+
Args:
|
|
1131
|
+
name: 工具名称
|
|
1132
|
+
description: 工具描述
|
|
1133
|
+
args_schema: Pydantic BaseModel 类
|
|
1134
|
+
func: 工具执行函数
|
|
1135
|
+
|
|
1136
|
+
Returns:
|
|
1137
|
+
Tool 实例
|
|
1138
|
+
|
|
1139
|
+
Example:
|
|
1140
|
+
>>> class SearchArgs(BaseModel):
|
|
1141
|
+
... query: str = Field(description="搜索关键词")
|
|
1142
|
+
... limit: int = Field(description="结果数量", default=10)
|
|
1143
|
+
>>>
|
|
1144
|
+
>>> search_tool = from_pydantic(
|
|
1145
|
+
... name="search",
|
|
1146
|
+
... description="搜索网络信息",
|
|
1147
|
+
... args_schema=SearchArgs,
|
|
1148
|
+
... func=lambda query, limit: f"搜索: {query}"
|
|
1149
|
+
... )
|
|
1150
|
+
"""
|
|
1151
|
+
return Tool(
|
|
1152
|
+
name=name,
|
|
1153
|
+
description=description,
|
|
1154
|
+
args_schema=args_schema,
|
|
1155
|
+
func=func,
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
"""AgentRun ToolSet 适配器
|
|
1160
|
+
|
|
1161
|
+
将 AgentRun 控制面的 ToolSet 资源适配为通用的 Tool/ToolSet 抽象,
|
|
1162
|
+
从而无缝集成到各个 AI 框架中。
|
|
1163
|
+
|
|
1164
|
+
核心思路:
|
|
1165
|
+
1. 从 toolset.list_tools() 获取工具元数据
|
|
1166
|
+
2. 为每个工具构建 Pydantic schema
|
|
1167
|
+
3. 使用 from_pydantic 创建 Tool(自动处理所有转换逻辑)
|
|
1168
|
+
"""
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def _build_tool_from_meta(
|
|
1172
|
+
toolset: Any,
|
|
1173
|
+
meta: Any,
|
|
1174
|
+
config: Optional[Any],
|
|
1175
|
+
) -> Optional[Tool]:
|
|
1176
|
+
"""从工具元数据构建 Tool - 使用 from_pydantic 复用已有逻辑"""
|
|
1177
|
+
meta_dict = _to_dict(meta)
|
|
1178
|
+
|
|
1179
|
+
# 提取工具名称
|
|
1180
|
+
tool_name = (
|
|
1181
|
+
meta_dict.get("name")
|
|
1182
|
+
or meta_dict.get("operationId")
|
|
1183
|
+
or meta_dict.get("tool_id")
|
|
1184
|
+
)
|
|
1185
|
+
if not tool_name:
|
|
1186
|
+
return None
|
|
1187
|
+
|
|
1188
|
+
# 提取描述(注意:description 可能是 None,需要显式转换)
|
|
1189
|
+
description = (
|
|
1190
|
+
meta_dict.get("description")
|
|
1191
|
+
or meta_dict.get("summary")
|
|
1192
|
+
or f"{meta_dict.get('method', '')} {meta_dict.get('path', '')}".strip()
|
|
1193
|
+
or ""
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
# 构建 Pydantic schema
|
|
1197
|
+
args_schema = _build_args_schema(tool_name, meta_dict)
|
|
1198
|
+
if not args_schema:
|
|
1199
|
+
# 无参数工具或 schema 解析失败
|
|
1200
|
+
if (
|
|
1201
|
+
"input_schema" in meta_dict
|
|
1202
|
+
or "parameters" in meta_dict
|
|
1203
|
+
or "request_body" in meta_dict
|
|
1204
|
+
):
|
|
1205
|
+
logger.debug(
|
|
1206
|
+
f"Failed to parse schema for tool '{tool_name}', using empty"
|
|
1207
|
+
" schema"
|
|
1208
|
+
)
|
|
1209
|
+
args_schema = create_model(f"{tool_name}_Args")
|
|
1210
|
+
|
|
1211
|
+
# 创建带有正确签名的调用函数
|
|
1212
|
+
call_tool_func = _create_function_with_signature(
|
|
1213
|
+
tool_name, args_schema, toolset, config
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
# 设置函数的 docstring(Google ADK 可能会读取)
|
|
1217
|
+
if description:
|
|
1218
|
+
call_tool_func.__doc__ = description
|
|
1219
|
+
|
|
1220
|
+
# 使用 from_pydantic 创建 Tool (利用已有的转换逻辑)
|
|
1221
|
+
return from_pydantic(
|
|
1222
|
+
name=tool_name,
|
|
1223
|
+
description=description,
|
|
1224
|
+
args_schema=args_schema,
|
|
1225
|
+
func=call_tool_func,
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def _normalize_tool_arguments(
|
|
1230
|
+
raw_kwargs: Dict[str, Any],
|
|
1231
|
+
args_schema: Optional[Type[BaseModel]],
|
|
1232
|
+
) -> Dict[str, Any]:
|
|
1233
|
+
"""根据 schema 对传入参数做简单归一化,提升 LLM 容错能力"""
|
|
1234
|
+
|
|
1235
|
+
if not raw_kwargs or args_schema is None:
|
|
1236
|
+
return raw_kwargs
|
|
1237
|
+
|
|
1238
|
+
normalized = dict(raw_kwargs)
|
|
1239
|
+
model_fields = list(args_schema.model_fields.keys())
|
|
1240
|
+
if not model_fields:
|
|
1241
|
+
return normalized
|
|
1242
|
+
|
|
1243
|
+
alias_map: Dict[str, str] = getattr(
|
|
1244
|
+
args_schema, "__agentrun_argument_aliases__", {}
|
|
1245
|
+
)
|
|
1246
|
+
for alias, canonical in alias_map.items():
|
|
1247
|
+
if alias in normalized and canonical not in normalized:
|
|
1248
|
+
normalized[canonical] = normalized.pop(alias)
|
|
1249
|
+
|
|
1250
|
+
if (
|
|
1251
|
+
len(model_fields) == 1
|
|
1252
|
+
and model_fields[0] not in normalized
|
|
1253
|
+
and len(normalized) == 1
|
|
1254
|
+
):
|
|
1255
|
+
_, value = normalized.popitem()
|
|
1256
|
+
normalized[model_fields[0]] = value
|
|
1257
|
+
return normalized
|
|
1258
|
+
|
|
1259
|
+
# 次要策略:忽略大小写和分隔符进行匹配
|
|
1260
|
+
def _simplify(name: str) -> str:
|
|
1261
|
+
return re.sub(r"[^a-z0-9]", "", name.lower())
|
|
1262
|
+
|
|
1263
|
+
field_map = {_simplify(name): name for name in model_fields}
|
|
1264
|
+
|
|
1265
|
+
for key in list(normalized.keys()):
|
|
1266
|
+
if key in args_schema.model_fields:
|
|
1267
|
+
continue
|
|
1268
|
+
simplified = _simplify(key)
|
|
1269
|
+
target = field_map.get(simplified)
|
|
1270
|
+
if target and target not in normalized:
|
|
1271
|
+
normalized[target] = normalized.pop(key)
|
|
1272
|
+
|
|
1273
|
+
return normalized
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def _maybe_add_body_alias(
|
|
1277
|
+
field_name: str, field_schema: Dict[str, Any], total_fields: int
|
|
1278
|
+
) -> Tuple[str, ...]:
|
|
1279
|
+
"""为展开的 body 字段添加常见别名,方便 LLM 调用"""
|
|
1280
|
+
|
|
1281
|
+
aliases: List[str] = []
|
|
1282
|
+
name_lower = field_name.lower()
|
|
1283
|
+
|
|
1284
|
+
if total_fields == 1 and "query" not in name_lower:
|
|
1285
|
+
if "search" in name_lower or name_lower.endswith("_input"):
|
|
1286
|
+
aliases.append("query")
|
|
1287
|
+
|
|
1288
|
+
if not aliases:
|
|
1289
|
+
return tuple()
|
|
1290
|
+
|
|
1291
|
+
extra_key = "x-aliases"
|
|
1292
|
+
existing = field_schema.get(extra_key, [])
|
|
1293
|
+
if not isinstance(existing, list):
|
|
1294
|
+
existing = []
|
|
1295
|
+
|
|
1296
|
+
for alias in aliases:
|
|
1297
|
+
if not alias or alias == field_name or alias in existing:
|
|
1298
|
+
continue
|
|
1299
|
+
existing.append(alias)
|
|
1300
|
+
|
|
1301
|
+
if existing:
|
|
1302
|
+
field_schema[extra_key] = existing
|
|
1303
|
+
|
|
1304
|
+
return tuple(alias for alias in aliases if alias)
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _create_function_with_signature(
|
|
1308
|
+
tool_name: str,
|
|
1309
|
+
args_schema: Type[BaseModel],
|
|
1310
|
+
toolset: Any,
|
|
1311
|
+
config: Optional[Any],
|
|
1312
|
+
) -> Callable:
|
|
1313
|
+
"""创建一个带有正确签名的函数
|
|
1314
|
+
|
|
1315
|
+
从 Pydantic schema 提取参数信息,动态构建函数签名
|
|
1316
|
+
"""
|
|
1317
|
+
from functools import wraps
|
|
1318
|
+
import inspect
|
|
1319
|
+
|
|
1320
|
+
# 从 Pydantic model 构建参数列表
|
|
1321
|
+
parameters = []
|
|
1322
|
+
for field_name, field_info in args_schema.model_fields.items():
|
|
1323
|
+
annotation = field_info.annotation
|
|
1324
|
+
|
|
1325
|
+
# 处理默认值
|
|
1326
|
+
from pydantic_core import PydanticUndefined
|
|
1327
|
+
|
|
1328
|
+
if field_info.default is not PydanticUndefined:
|
|
1329
|
+
default = field_info.default
|
|
1330
|
+
elif not field_info.is_required():
|
|
1331
|
+
default = None
|
|
1332
|
+
else:
|
|
1333
|
+
default = inspect.Parameter.empty
|
|
1334
|
+
|
|
1335
|
+
param = inspect.Parameter(
|
|
1336
|
+
field_name,
|
|
1337
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
1338
|
+
default=default,
|
|
1339
|
+
annotation=annotation,
|
|
1340
|
+
)
|
|
1341
|
+
parameters.append(param)
|
|
1342
|
+
|
|
1343
|
+
alias_map: Dict[str, str] = getattr(
|
|
1344
|
+
args_schema, "__agentrun_argument_aliases__", {}
|
|
1345
|
+
)
|
|
1346
|
+
if alias_map:
|
|
1347
|
+
for alias, canonical in alias_map.items():
|
|
1348
|
+
canonical_field = args_schema.model_fields.get(canonical)
|
|
1349
|
+
alias_annotation = (
|
|
1350
|
+
canonical_field.annotation
|
|
1351
|
+
if canonical_field
|
|
1352
|
+
else inspect._empty
|
|
1353
|
+
)
|
|
1354
|
+
if (
|
|
1355
|
+
alias_annotation is not inspect._empty
|
|
1356
|
+
and alias_annotation is not None
|
|
1357
|
+
):
|
|
1358
|
+
alias_annotation = Optional[alias_annotation]
|
|
1359
|
+
parameters.append(
|
|
1360
|
+
inspect.Parameter(
|
|
1361
|
+
alias,
|
|
1362
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
1363
|
+
default=None,
|
|
1364
|
+
annotation=alias_annotation,
|
|
1365
|
+
)
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
# 创建实际执行函数
|
|
1369
|
+
def impl(**kwargs):
|
|
1370
|
+
# 使用 Pydantic 进行参数校验,确保 LLM 传入的字段合法
|
|
1371
|
+
normalized_kwargs = _normalize_tool_arguments(kwargs, args_schema)
|
|
1372
|
+
|
|
1373
|
+
if args_schema is not None:
|
|
1374
|
+
try:
|
|
1375
|
+
parsed = args_schema(**normalized_kwargs)
|
|
1376
|
+
payload = parsed.model_dump(mode="python", exclude_unset=True)
|
|
1377
|
+
except ValidationError as exc:
|
|
1378
|
+
raise ValueError(
|
|
1379
|
+
f"Invalid arguments for tool '{tool_name}': {exc}"
|
|
1380
|
+
) from exc
|
|
1381
|
+
else:
|
|
1382
|
+
payload = normalized_kwargs
|
|
1383
|
+
|
|
1384
|
+
# 转换类型并映射字段名(body -> json)
|
|
1385
|
+
converted_kwargs = {
|
|
1386
|
+
("json" if k == "body" else k): _convert_to_native(v)
|
|
1387
|
+
for k, v in payload.items()
|
|
1388
|
+
}
|
|
1389
|
+
body_fields = getattr(args_schema, "__agentrun_body_fields__", None)
|
|
1390
|
+
if body_fields:
|
|
1391
|
+
body_payload = {}
|
|
1392
|
+
for field in body_fields:
|
|
1393
|
+
if field in converted_kwargs:
|
|
1394
|
+
body_payload[field] = converted_kwargs.pop(field)
|
|
1395
|
+
if body_payload and "json" not in converted_kwargs:
|
|
1396
|
+
converted_kwargs["json"] = body_payload
|
|
1397
|
+
|
|
1398
|
+
return toolset.call_tool(
|
|
1399
|
+
name=tool_name, arguments=converted_kwargs, config=config
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
# 创建包装函数并设置签名
|
|
1403
|
+
@wraps(impl)
|
|
1404
|
+
def wrapper(**kwargs):
|
|
1405
|
+
return impl(**kwargs)
|
|
1406
|
+
|
|
1407
|
+
# 设置函数属性(清理特殊字符,确保是有效的 Python 标识符)
|
|
1408
|
+
clean_name = re.sub(r"[^0-9a-zA-Z_]", "_", tool_name)
|
|
1409
|
+
clean_name = normalize_tool_name(clean_name)
|
|
1410
|
+
wrapper.__name__ = clean_name
|
|
1411
|
+
wrapper.__qualname__ = clean_name
|
|
1412
|
+
if parameters:
|
|
1413
|
+
wrapper.__signature__ = inspect.Signature(parameters) # type: ignore
|
|
1414
|
+
|
|
1415
|
+
return wrapper
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
def _convert_to_native(value: Any) -> Any:
|
|
1419
|
+
"""转换框架特定类型为 Python 原生类型
|
|
1420
|
+
|
|
1421
|
+
主要处理 Google ADK 的 Nestedobject 等特殊类型
|
|
1422
|
+
"""
|
|
1423
|
+
# 如果是基本类型,直接返回
|
|
1424
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
1425
|
+
return value
|
|
1426
|
+
|
|
1427
|
+
# 如果是列表,递归转换
|
|
1428
|
+
if isinstance(value, list):
|
|
1429
|
+
return [_convert_to_native(item) for item in value]
|
|
1430
|
+
|
|
1431
|
+
# 如果是字典,递归转换
|
|
1432
|
+
if isinstance(value, dict):
|
|
1433
|
+
return {k: _convert_to_native(v) for k, v in value.items()}
|
|
1434
|
+
|
|
1435
|
+
# 检查类型名称,处理 Pydantic BaseModel 或类似的对象
|
|
1436
|
+
type_name = type(value).__name__
|
|
1437
|
+
if "Object" in type_name or hasattr(value, "model_dump"):
|
|
1438
|
+
# Pydantic BaseModel 或 Google ADK NestedObject
|
|
1439
|
+
if hasattr(value, "model_dump"):
|
|
1440
|
+
try:
|
|
1441
|
+
return _convert_to_native(value.model_dump())
|
|
1442
|
+
except Exception:
|
|
1443
|
+
pass
|
|
1444
|
+
|
|
1445
|
+
# Google ADK NestedObject 等有 to_dict 方法的对象
|
|
1446
|
+
if hasattr(value, "to_dict"):
|
|
1447
|
+
try:
|
|
1448
|
+
return _convert_to_native(value.to_dict())
|
|
1449
|
+
except Exception:
|
|
1450
|
+
pass
|
|
1451
|
+
|
|
1452
|
+
# 尝试转换为字典(处理其他特殊类型)
|
|
1453
|
+
if hasattr(value, "to_dict"):
|
|
1454
|
+
try:
|
|
1455
|
+
return _convert_to_native(value.to_dict())
|
|
1456
|
+
except Exception:
|
|
1457
|
+
pass
|
|
1458
|
+
|
|
1459
|
+
# 尝试使用 __dict__ 属性
|
|
1460
|
+
if hasattr(value, "__dict__"):
|
|
1461
|
+
try:
|
|
1462
|
+
return _convert_to_native(value.__dict__)
|
|
1463
|
+
except Exception:
|
|
1464
|
+
pass
|
|
1465
|
+
|
|
1466
|
+
# 如果是 Pydantic BaseModel,尝试提取字段值
|
|
1467
|
+
if hasattr(value, "__fields__") or hasattr(value, "model_fields"):
|
|
1468
|
+
try:
|
|
1469
|
+
result = {}
|
|
1470
|
+
# 新版本 Pydantic v2
|
|
1471
|
+
if hasattr(value, "model_fields"):
|
|
1472
|
+
for field_name in value.model_fields:
|
|
1473
|
+
if hasattr(value, field_name):
|
|
1474
|
+
result[field_name] = _convert_to_native(
|
|
1475
|
+
getattr(value, field_name)
|
|
1476
|
+
)
|
|
1477
|
+
# 旧版本 Pydantic v1
|
|
1478
|
+
elif hasattr(value, "__fields__"):
|
|
1479
|
+
for field_name in value.__fields__:
|
|
1480
|
+
if hasattr(value, field_name):
|
|
1481
|
+
result[field_name] = _convert_to_native(
|
|
1482
|
+
getattr(value, field_name)
|
|
1483
|
+
)
|
|
1484
|
+
return result
|
|
1485
|
+
except Exception:
|
|
1486
|
+
pass
|
|
1487
|
+
|
|
1488
|
+
# 如果无法转换,返回原值(可能会导致序列化错误)
|
|
1489
|
+
logger.debug(
|
|
1490
|
+
f"Unable to convert object of type {type(value)} to native Python type:"
|
|
1491
|
+
f" {value}"
|
|
1492
|
+
)
|
|
1493
|
+
return value
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
def _build_args_schema(
|
|
1497
|
+
tool_name: str,
|
|
1498
|
+
meta: Dict[str, Any],
|
|
1499
|
+
) -> Optional[Type[BaseModel]]:
|
|
1500
|
+
"""构建参数 schema"""
|
|
1501
|
+
# 新的统一格式:ToolInfo.parameters (ToolSchema 对象)
|
|
1502
|
+
# 当 meta 是从 toolset.list_tools() 返回的 ToolInfo 时
|
|
1503
|
+
if "parameters" in meta and isinstance(meta.get("parameters"), dict):
|
|
1504
|
+
# 检查是否是 ToolSchema 格式(有 type/properties/required)
|
|
1505
|
+
params = meta.get("parameters")
|
|
1506
|
+
if params and "type" in params:
|
|
1507
|
+
# 这是 ToolSchema 的 JSON 表示,直接使用
|
|
1508
|
+
return _json_schema_to_pydantic(f"{tool_name}_Args", params)
|
|
1509
|
+
|
|
1510
|
+
# MCP 格式(旧格式,保持兼容)
|
|
1511
|
+
if "input_schema" in meta:
|
|
1512
|
+
schema = _load_json(meta.get("input_schema"))
|
|
1513
|
+
return _json_schema_to_pydantic(f"{tool_name}_Args", schema)
|
|
1514
|
+
|
|
1515
|
+
# OpenAPI 格式(旧格式,保持兼容)
|
|
1516
|
+
# 注意:这里的 parameters 是数组格式,不同于上面的 ToolSchema
|
|
1517
|
+
if "parameters" in meta and isinstance(meta.get("parameters"), list):
|
|
1518
|
+
schema, body_info, alias_map = _build_openapi_schema(meta)
|
|
1519
|
+
model = _json_schema_to_pydantic(f"{tool_name}_Args", schema)
|
|
1520
|
+
if model is not None and body_info:
|
|
1521
|
+
setattr(model, "__agentrun_body_fields__", tuple(body_info))
|
|
1522
|
+
if model is not None and alias_map:
|
|
1523
|
+
setattr(model, "__agentrun_argument_aliases__", dict(alias_map))
|
|
1524
|
+
return model
|
|
1525
|
+
|
|
1526
|
+
# OpenAPI 格式(有 request_body)
|
|
1527
|
+
if "request_body" in meta:
|
|
1528
|
+
schema, body_info, alias_map = _build_openapi_schema(meta)
|
|
1529
|
+
model = _json_schema_to_pydantic(f"{tool_name}_Args", schema)
|
|
1530
|
+
if model is not None and body_info:
|
|
1531
|
+
setattr(model, "__agentrun_body_fields__", tuple(body_info))
|
|
1532
|
+
if model is not None and alias_map:
|
|
1533
|
+
setattr(model, "__agentrun_argument_aliases__", dict(alias_map))
|
|
1534
|
+
return model
|
|
1535
|
+
|
|
1536
|
+
return None
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
def _build_openapi_schema(
|
|
1540
|
+
meta: Dict[str, Any],
|
|
1541
|
+
) -> Tuple[Dict[str, Any], Tuple[str, ...], Dict[str, str]]:
|
|
1542
|
+
"""从 OpenAPI 元数据构建 JSON Schema
|
|
1543
|
+
|
|
1544
|
+
Returns:
|
|
1545
|
+
schema: JSON Schema dict
|
|
1546
|
+
body_fields: 若 requestBody 为对象并被展开, 返回其字段名称元组
|
|
1547
|
+
"""
|
|
1548
|
+
properties = {}
|
|
1549
|
+
required = []
|
|
1550
|
+
body_field_names: List[str] = []
|
|
1551
|
+
alias_map: Dict[str, str] = {}
|
|
1552
|
+
|
|
1553
|
+
# 处理 parameters
|
|
1554
|
+
for param in meta.get("parameters", []):
|
|
1555
|
+
if not isinstance(param, dict):
|
|
1556
|
+
continue
|
|
1557
|
+
name = param.get("name")
|
|
1558
|
+
if not name:
|
|
1559
|
+
continue
|
|
1560
|
+
|
|
1561
|
+
schema = param.get("schema", {})
|
|
1562
|
+
if isinstance(schema, dict):
|
|
1563
|
+
properties[name] = {
|
|
1564
|
+
**schema,
|
|
1565
|
+
"description": param.get("description") or schema.get(
|
|
1566
|
+
"description", ""
|
|
1567
|
+
),
|
|
1568
|
+
}
|
|
1569
|
+
if param.get("required"):
|
|
1570
|
+
required.append(name)
|
|
1571
|
+
|
|
1572
|
+
# 处理 requestBody
|
|
1573
|
+
request_body = meta.get("request_body", {})
|
|
1574
|
+
if isinstance(request_body, dict):
|
|
1575
|
+
content = request_body.get("content", {})
|
|
1576
|
+
for content_type in [
|
|
1577
|
+
"application/json",
|
|
1578
|
+
"application/x-www-form-urlencoded",
|
|
1579
|
+
]:
|
|
1580
|
+
if content_type in content:
|
|
1581
|
+
body_schema = content[content_type].get("schema", {})
|
|
1582
|
+
if body_schema:
|
|
1583
|
+
if (
|
|
1584
|
+
isinstance(body_schema, dict)
|
|
1585
|
+
and body_schema.get("type") == "object"
|
|
1586
|
+
and body_schema.get("properties")
|
|
1587
|
+
and not any(
|
|
1588
|
+
name in properties
|
|
1589
|
+
for name in body_schema["properties"].keys()
|
|
1590
|
+
)
|
|
1591
|
+
):
|
|
1592
|
+
# 将 body 属性展开,提升 LLM 可用性
|
|
1593
|
+
nested_required = set(body_schema.get("required", []))
|
|
1594
|
+
total_body_fields = len(body_schema["properties"])
|
|
1595
|
+
for field_name, field_schema in body_schema[
|
|
1596
|
+
"properties"
|
|
1597
|
+
].items():
|
|
1598
|
+
aliases = _maybe_add_body_alias(
|
|
1599
|
+
field_name,
|
|
1600
|
+
field_schema,
|
|
1601
|
+
total_body_fields,
|
|
1602
|
+
)
|
|
1603
|
+
for alias in aliases:
|
|
1604
|
+
alias_map[alias] = field_name
|
|
1605
|
+
properties[field_name] = field_schema
|
|
1606
|
+
if field_name in nested_required:
|
|
1607
|
+
required.append(field_name)
|
|
1608
|
+
body_field_names.append(field_name)
|
|
1609
|
+
else:
|
|
1610
|
+
# 使用 body 字段
|
|
1611
|
+
properties["body"] = body_schema
|
|
1612
|
+
if request_body.get("required"):
|
|
1613
|
+
required.append("body")
|
|
1614
|
+
break
|
|
1615
|
+
|
|
1616
|
+
schema = {
|
|
1617
|
+
"type": "object",
|
|
1618
|
+
"properties": properties,
|
|
1619
|
+
"required": list(dict.fromkeys(required)) if required else [],
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
return schema, tuple(body_field_names), alias_map
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
def _json_schema_to_pydantic(
|
|
1626
|
+
name: str,
|
|
1627
|
+
schema: Optional[Dict[str, Any]],
|
|
1628
|
+
) -> Optional[Type[BaseModel]]:
|
|
1629
|
+
"""将 JSON Schema 转换为 Pydantic 模型"""
|
|
1630
|
+
if not schema or not isinstance(schema, dict):
|
|
1631
|
+
return None
|
|
1632
|
+
|
|
1633
|
+
properties = schema.get("properties", {})
|
|
1634
|
+
if not properties:
|
|
1635
|
+
return None
|
|
1636
|
+
|
|
1637
|
+
required_fields = set(schema.get("required", []))
|
|
1638
|
+
fields = {}
|
|
1639
|
+
|
|
1640
|
+
for field_name, field_schema in properties.items():
|
|
1641
|
+
if not isinstance(field_schema, dict):
|
|
1642
|
+
continue
|
|
1643
|
+
|
|
1644
|
+
# 映射类型
|
|
1645
|
+
field_type = _json_type_to_python(field_schema)
|
|
1646
|
+
description = field_schema.get("description", "")
|
|
1647
|
+
default = field_schema.get("default")
|
|
1648
|
+
aliases = field_schema.get("x-aliases")
|
|
1649
|
+
field_kwargs: Dict[str, Any] = {"description": description}
|
|
1650
|
+
if aliases:
|
|
1651
|
+
if not isinstance(aliases, (list, tuple)):
|
|
1652
|
+
aliases = [aliases]
|
|
1653
|
+
field_kwargs["validation_alias"] = AliasChoices(
|
|
1654
|
+
field_name, *aliases
|
|
1655
|
+
)
|
|
1656
|
+
|
|
1657
|
+
# 构建字段定义
|
|
1658
|
+
if field_name in required_fields:
|
|
1659
|
+
# 必填字段
|
|
1660
|
+
fields[field_name] = (field_type, Field(**field_kwargs))
|
|
1661
|
+
else:
|
|
1662
|
+
# 可选字段
|
|
1663
|
+
from typing import Optional as TypingOptional
|
|
1664
|
+
|
|
1665
|
+
fields[field_name] = (
|
|
1666
|
+
TypingOptional[field_type],
|
|
1667
|
+
Field(default=default, **field_kwargs),
|
|
1668
|
+
)
|
|
1669
|
+
|
|
1670
|
+
# 创建模型,清理名称
|
|
1671
|
+
model_name = re.sub(r"[^0-9a-zA-Z]", "", name.title())
|
|
1672
|
+
return create_model(model_name or "Args", **fields) # type: ignore
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def _json_type_to_python(field_schema: Dict[str, Any]) -> type:
|
|
1676
|
+
"""映射 JSON Schema 类型到 Python 类型"""
|
|
1677
|
+
schema_type = field_schema.get("type", "string")
|
|
1678
|
+
|
|
1679
|
+
# 处理嵌套对象和数组
|
|
1680
|
+
if schema_type == "object":
|
|
1681
|
+
# 如果有 properties,递归创建嵌套模型
|
|
1682
|
+
properties = field_schema.get("properties")
|
|
1683
|
+
if properties:
|
|
1684
|
+
nested_model = _json_schema_to_pydantic(
|
|
1685
|
+
"NestedObject", field_schema
|
|
1686
|
+
)
|
|
1687
|
+
return nested_model if nested_model else dict
|
|
1688
|
+
return dict
|
|
1689
|
+
|
|
1690
|
+
if schema_type == "array":
|
|
1691
|
+
items_schema = field_schema.get("items", {})
|
|
1692
|
+
if isinstance(items_schema, dict):
|
|
1693
|
+
item_type = _json_type_to_python(items_schema)
|
|
1694
|
+
from typing import List as TypingList
|
|
1695
|
+
|
|
1696
|
+
return TypingList[item_type] # type: ignore
|
|
1697
|
+
return list
|
|
1698
|
+
|
|
1699
|
+
# 基本类型映射
|
|
1700
|
+
mapping = {
|
|
1701
|
+
"string": str,
|
|
1702
|
+
"integer": int,
|
|
1703
|
+
"number": float,
|
|
1704
|
+
"boolean": bool,
|
|
1705
|
+
}
|
|
1706
|
+
return mapping.get(schema_type, str)
|
|
1707
|
+
|
|
1708
|
+
|
|
1709
|
+
def _build_args_model_from_parameters(
|
|
1710
|
+
tool_name: str, parameters: List[ToolParameter]
|
|
1711
|
+
) -> Optional[Type[BaseModel]]:
|
|
1712
|
+
"""根据 ToolParameter 定义构建 Pydantic 模型"""
|
|
1713
|
+
|
|
1714
|
+
if not parameters:
|
|
1715
|
+
return None
|
|
1716
|
+
|
|
1717
|
+
schema = {
|
|
1718
|
+
"type": "object",
|
|
1719
|
+
"properties": {
|
|
1720
|
+
param.name: param.to_json_schema() for param in parameters
|
|
1721
|
+
},
|
|
1722
|
+
"required": [
|
|
1723
|
+
param.name
|
|
1724
|
+
for param in parameters
|
|
1725
|
+
if getattr(param, "required", False)
|
|
1726
|
+
],
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
return _json_schema_to_pydantic(f"{tool_name}_Args", schema)
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
def _load_json(data: Any) -> Optional[Dict[str, Any]]:
|
|
1733
|
+
"""加载 JSON 数据"""
|
|
1734
|
+
if isinstance(data, dict):
|
|
1735
|
+
return data
|
|
1736
|
+
if isinstance(data, (bytes, bytearray)):
|
|
1737
|
+
data = data.decode("utf-8")
|
|
1738
|
+
if isinstance(data, str):
|
|
1739
|
+
try:
|
|
1740
|
+
return json.loads(data)
|
|
1741
|
+
except json.JSONDecodeError:
|
|
1742
|
+
return None
|
|
1743
|
+
return None
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
def _to_dict(obj: Any) -> Dict[str, Any]:
|
|
1747
|
+
"""转换为字典"""
|
|
1748
|
+
if obj is None:
|
|
1749
|
+
return {}
|
|
1750
|
+
if isinstance(obj, dict):
|
|
1751
|
+
return obj
|
|
1752
|
+
if hasattr(obj, "model_dump"):
|
|
1753
|
+
return obj.model_dump(exclude_none=True)
|
|
1754
|
+
|
|
1755
|
+
# 使用 __dict__ 更安全(避免获取方法和属性)
|
|
1756
|
+
if hasattr(obj, "__dict__"):
|
|
1757
|
+
return {k: v for k, v in obj.__dict__.items() if not k.startswith("_")}
|
|
1758
|
+
|
|
1759
|
+
return {}
|