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