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.
Files changed (135) hide show
  1. agentrun/__init__.py +325 -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 +93 -0
  31. agentrun/integration/builtin/sandbox.py +1234 -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 +31 -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 +46 -0
  43. agentrun/integration/google_adk/tool_adapter.py +235 -0
  44. agentrun/integration/langchain/__init__.py +30 -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 +35 -0
  51. agentrun/integration/langgraph/adapter.py +20 -0
  52. agentrun/integration/langgraph/agent_converter.py +1073 -0
  53. agentrun/integration/langgraph/builtin.py +65 -0
  54. agentrun/integration/pydantic_ai/__init__.py +12 -0
  55. agentrun/integration/pydantic_ai/adapter.py +13 -0
  56. agentrun/integration/pydantic_ai/builtin.py +65 -0
  57. agentrun/integration/pydantic_ai/model_adapter.py +44 -0
  58. agentrun/integration/pydantic_ai/tool_adapter.py +19 -0
  59. agentrun/integration/utils/__init__.py +112 -0
  60. agentrun/integration/utils/adapter.py +560 -0
  61. agentrun/integration/utils/canonical.py +164 -0
  62. agentrun/integration/utils/converter.py +134 -0
  63. agentrun/integration/utils/model.py +110 -0
  64. agentrun/integration/utils/tool.py +1759 -0
  65. agentrun/model/__client_async_template.py +357 -0
  66. agentrun/model/__init__.py +57 -0
  67. agentrun/model/__model_proxy_async_template.py +270 -0
  68. agentrun/model/__model_service_async_template.py +267 -0
  69. agentrun/model/api/__init__.py +6 -0
  70. agentrun/model/api/control.py +1173 -0
  71. agentrun/model/api/data.py +196 -0
  72. agentrun/model/client.py +674 -0
  73. agentrun/model/model.py +235 -0
  74. agentrun/model/model_proxy.py +439 -0
  75. agentrun/model/model_service.py +438 -0
  76. agentrun/sandbox/__aio_sandbox_async_template.py +523 -0
  77. agentrun/sandbox/__browser_sandbox_async_template.py +110 -0
  78. agentrun/sandbox/__client_async_template.py +491 -0
  79. agentrun/sandbox/__code_interpreter_sandbox_async_template.py +463 -0
  80. agentrun/sandbox/__init__.py +69 -0
  81. agentrun/sandbox/__sandbox_async_template.py +463 -0
  82. agentrun/sandbox/__template_async_template.py +152 -0
  83. agentrun/sandbox/aio_sandbox.py +905 -0
  84. agentrun/sandbox/api/__aio_data_async_template.py +335 -0
  85. agentrun/sandbox/api/__browser_data_async_template.py +140 -0
  86. agentrun/sandbox/api/__code_interpreter_data_async_template.py +206 -0
  87. agentrun/sandbox/api/__init__.py +19 -0
  88. agentrun/sandbox/api/__sandbox_data_async_template.py +107 -0
  89. agentrun/sandbox/api/aio_data.py +551 -0
  90. agentrun/sandbox/api/browser_data.py +172 -0
  91. agentrun/sandbox/api/code_interpreter_data.py +396 -0
  92. agentrun/sandbox/api/control.py +1051 -0
  93. agentrun/sandbox/api/playwright_async.py +492 -0
  94. agentrun/sandbox/api/playwright_sync.py +492 -0
  95. agentrun/sandbox/api/sandbox_data.py +154 -0
  96. agentrun/sandbox/browser_sandbox.py +185 -0
  97. agentrun/sandbox/client.py +925 -0
  98. agentrun/sandbox/code_interpreter_sandbox.py +823 -0
  99. agentrun/sandbox/model.py +397 -0
  100. agentrun/sandbox/sandbox.py +848 -0
  101. agentrun/sandbox/template.py +217 -0
  102. agentrun/server/__init__.py +191 -0
  103. agentrun/server/agui_normalizer.py +180 -0
  104. agentrun/server/agui_protocol.py +797 -0
  105. agentrun/server/invoker.py +309 -0
  106. agentrun/server/model.py +427 -0
  107. agentrun/server/openai_protocol.py +535 -0
  108. agentrun/server/protocol.py +140 -0
  109. agentrun/server/server.py +208 -0
  110. agentrun/toolset/__client_async_template.py +62 -0
  111. agentrun/toolset/__init__.py +51 -0
  112. agentrun/toolset/__toolset_async_template.py +204 -0
  113. agentrun/toolset/api/__init__.py +17 -0
  114. agentrun/toolset/api/control.py +262 -0
  115. agentrun/toolset/api/mcp.py +100 -0
  116. agentrun/toolset/api/openapi.py +1251 -0
  117. agentrun/toolset/client.py +102 -0
  118. agentrun/toolset/model.py +321 -0
  119. agentrun/toolset/toolset.py +270 -0
  120. agentrun/utils/__data_api_async_template.py +720 -0
  121. agentrun/utils/__init__.py +5 -0
  122. agentrun/utils/__resource_async_template.py +158 -0
  123. agentrun/utils/config.py +258 -0
  124. agentrun/utils/control_api.py +78 -0
  125. agentrun/utils/data_api.py +1120 -0
  126. agentrun/utils/exception.py +151 -0
  127. agentrun/utils/helper.py +108 -0
  128. agentrun/utils/log.py +77 -0
  129. agentrun/utils/model.py +168 -0
  130. agentrun/utils/resource.py +291 -0
  131. agentrun_inner_test-0.0.46.dist-info/METADATA +263 -0
  132. agentrun_inner_test-0.0.46.dist-info/RECORD +135 -0
  133. agentrun_inner_test-0.0.46.dist-info/WHEEL +5 -0
  134. agentrun_inner_test-0.0.46.dist-info/licenses/LICENSE +201 -0
  135. 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 {}