Skip to content

Commit a155f15

Browse files
committed
feat: 添加工具名称标准化功能,确保符合外部提供者限制并增加单元测试
Change-Id: Ic4a12c5033dd68a37a256a4ee0ad2dfafc804793 Signed-off-by: OhYee <oyohyee@oyohyee.com>
1 parent 36f580f commit a155f15

File tree

6 files changed

+121
-7
lines changed

6 files changed

+121
-7
lines changed

agentrun/integration/google_adk/tool_adapter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from agentrun.integration.utils.adapter import ToolAdapter
88
from agentrun.integration.utils.canonical import CanonicalTool
9+
from agentrun.integration.utils.tool import normalize_tool_name
910

1011

1112
def _json_schema_to_google_schema(
@@ -179,7 +180,7 @@ class GoogleADKToolAdapter(ToolAdapter):
179180

180181
def get_registered_tool(self, name: str) -> Optional[CanonicalTool]:
181182
"""根据名称获取最近注册的工具定义 / Google ADK Tool Adapter"""
182-
return self._registered_tools.get(name)
183+
return self._registered_tools.get(normalize_tool_name(name))
183184

184185
def from_canonical(self, tools: List[CanonicalTool]):
185186
"""将标准格式转换为 Google ADK 工具 / Google ADK Tool Adapter
@@ -207,7 +208,7 @@ def from_canonical(self, tools: List[CanonicalTool]):
207208

208209
# 创建 FunctionDeclaration
209210
declaration = types.FunctionDeclaration(
210-
name=tool.name,
211+
name=normalize_tool_name(tool.name),
211212
description=tool.description or "",
212213
parameters=google_schema,
213214
)

agentrun/integration/utils/adapter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from agentrun.integration.utils.canonical import CanonicalMessage, CanonicalTool
1818
from agentrun.integration.utils.model import CommonModel
19+
from agentrun.integration.utils.tool import normalize_tool_name
1920

2021
# 用于缓存动态创建的 Pydantic 模型,避免重复创建
2122
_dynamic_models_cache: Dict[str, type] = {}
@@ -510,8 +511,8 @@ def tool_func(**kwargs):
510511

511512
return canonical_tool.func(**kwargs)
512513

513-
# 设置函数元数据
514-
tool_func.__name__ = canonical_tool.name
514+
# 设置函数元数据(函数名也需要标准化以防第三方工具名限制)
515+
tool_func.__name__ = normalize_tool_name(canonical_tool.name)
515516

516517
# 生成文档字符串
517518
base_doc = canonical_tool.description or ""

agentrun/integration/utils/canonical.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
from enum import Enum
2020

21+
from agentrun.integration.utils.tool import normalize_tool_name
22+
2123

2224
class MessageRole(str, Enum):
2325
"""统一的消息角色枚举"""
@@ -102,6 +104,11 @@ class CanonicalTool:
102104
parameters: Dict[str, Any] # JSON Schema 格式
103105
func: Optional[Callable] = None
104106

107+
def __post_init__(self):
108+
# Normalize canonical tool name to avoid exceeding provider limits
109+
if self.name:
110+
self.name = normalize_tool_name(self.name)
111+
105112
def to_openai_function(self) -> Dict[str, Any]:
106113
"""转换为 OpenAI Function Calling 格式"""
107114
return {

agentrun/integration/utils/tool.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from copy import deepcopy
2121
from functools import wraps
22+
import hashlib
2223
import inspect
2324
import json
2425
import re
@@ -47,6 +48,25 @@
4748
from agentrun.toolset import ToolSet
4849
from agentrun.utils.log import logger
4950

51+
# Tool name constraints for external providers like OpenAI
52+
MAX_TOOL_NAME_LEN = 64
53+
TOOL_NAME_HEAD_LEN = 32
54+
55+
56+
def normalize_tool_name(name: str) -> str:
57+
"""Normalize a tool name to fit provider limits.
58+
59+
If `name` length is <= MAX_TOOL_NAME_LEN, return it unchanged.
60+
Otherwise, return the first TOOL_NAME_HEAD_LEN characters + md5(full_name)
61+
(32 hex chars), resulting in a 64-char string.
62+
"""
63+
if not isinstance(name, str):
64+
name = str(name)
65+
if len(name) <= MAX_TOOL_NAME_LEN:
66+
return name
67+
digest = hashlib.md5(name.encode("utf-8")).hexdigest()
68+
return name[:TOOL_NAME_HEAD_LEN] + digest
69+
5070

5171
class ToolParameter:
5272
"""工具参数定义
@@ -253,7 +273,9 @@ def __init__(
253273
args_schema: Optional[Type[BaseModel]] = None,
254274
func: Optional[Callable] = None,
255275
):
256-
self.name = name
276+
# Normalize tool name to avoid external provider limits (e.g. OpenAI 64 chars)
277+
# If name length > 64, keep first 32 chars and append 32-char md5 sum of full name.
278+
self.name = normalize_tool_name(name)
257279
self.description = description
258280
self.parameters = list(parameters or [])
259281
self.args_schema = args_schema or _build_args_model_from_parameters(
@@ -982,6 +1004,8 @@ def tool(
9821004

9831005
def decorator(func: Callable) -> Tool:
9841006
tool_name = name or func.__name__
1007+
# ensure tool name is normalized
1008+
tool_name = normalize_tool_name(tool_name)
9851009
tool_description = description or (
9861010
func.__doc__.strip() if func.__doc__ else ""
9871011
)
@@ -1362,6 +1386,7 @@ def wrapper(**kwargs):
13621386

13631387
# 设置函数属性(清理特殊字符,确保是有效的 Python 标识符)
13641388
clean_name = re.sub(r"[^0-9a-zA-Z_]", "_", tool_name)
1389+
clean_name = normalize_tool_name(clean_name)
13651390
wrapper.__name__ = clean_name
13661391
wrapper.__qualname__ = clean_name
13671392
if parameters:

agentrun/toolset/api/openapi.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import httpx
1212
from pydash import get as pg
1313

14+
from agentrun.integration.utils.tool import normalize_tool_name
1415
from agentrun.utils.config import Config
1516
from agentrun.utils.log import logger
1617
from agentrun.utils.model import BaseModel
@@ -322,6 +323,8 @@ def wrapper(**kwargs):
322323

323324
# 设置函数属性
324325
clean_name = re.sub(r"[^0-9a-zA-Z_]", "_", name)
326+
# Normalize for provider limits / external frameworks
327+
clean_name = normalize_tool_name(clean_name)
325328
wrapper.__name__ = clean_name
326329
wrapper.__qualname__ = clean_name
327330
wrapper.__doc__ = "\n".join(doc_parts)
@@ -609,7 +612,7 @@ def from_openapi_schema(
609612

610613
tools.append(
611614
ToolInfo(
612-
name=operation_id,
615+
name=normalize_tool_name(operation_id),
613616
description=description,
614617
parameters=parameters,
615618
)
@@ -682,7 +685,7 @@ def from_mcp_tools(
682685

683686
tool_infos.append(
684687
ToolInfo(
685-
name=tool_name,
688+
name=normalize_tool_name(tool_name),
686689
description=tool_description,
687690
parameters=parameters
688691
or ToolSchema(type="object", properties={}),
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import hashlib
2+
import json
3+
4+
from agentrun.integration.utils.canonical import CanonicalTool
5+
from agentrun.integration.utils.tool import normalize_tool_name, Tool
6+
from agentrun.toolset.api.openapi import ApiSet
7+
8+
9+
def test_normalize_tool_name_short():
10+
name = "short_name"
11+
assert normalize_tool_name(name) == name
12+
13+
14+
def test_normalize_tool_name_long():
15+
long_name = "a" * 80
16+
normalized = normalize_tool_name(long_name)
17+
assert len(normalized) == 64
18+
assert normalized[:32] == long_name[:32]
19+
expected_md5 = hashlib.md5(long_name.encode("utf-8")).hexdigest()
20+
assert normalized[32:] == expected_md5
21+
22+
23+
def test_tool_normalizes_in_tool_class():
24+
long_name = "tool_" + "x" * 80
25+
t = Tool(name=long_name, func=lambda: None)
26+
assert len(t.name) <= 64
27+
assert t.name == normalize_tool_name(long_name)
28+
29+
30+
def test_canonical_tool_normalizes():
31+
long_name = "canonical_" + "y" * 80
32+
c = CanonicalTool(name=long_name, description="desc", parameters={})
33+
assert c.name == normalize_tool_name(long_name)
34+
35+
36+
def test_openapi_from_schema_tool_name_truncation():
37+
long_name = "op_" + "z" * 100
38+
schema = {
39+
"openapi": "3.0.0",
40+
"paths": {
41+
"/test": {
42+
"get": {
43+
"operationId": long_name,
44+
"responses": {
45+
"200": {
46+
"content": {
47+
"application/json": {
48+
"schema": {"type": "string"}
49+
}
50+
}
51+
}
52+
},
53+
}
54+
}
55+
},
56+
}
57+
58+
apiset = ApiSet.from_openapi_schema(schema=json.dumps(schema))
59+
names = [t.name for t in apiset.tools()]
60+
assert normalize_tool_name(long_name) in names
61+
62+
63+
def test_google_adk_declaration_name_normalized():
64+
try:
65+
from agentrun.integration.google_adk.tool_adapter import (
66+
GoogleADKToolAdapter,
67+
)
68+
except Exception:
69+
# google.genai not installed — skip
70+
return
71+
72+
long_name = "gtool_" + "x" * 100
73+
ct = CanonicalTool(name=long_name, description="d", parameters={})
74+
adapter = GoogleADKToolAdapter()
75+
tools = adapter.from_canonical([ct])
76+
decl = tools[0]._get_declaration()
77+
assert decl.name == normalize_tool_name(long_name)

0 commit comments

Comments
 (0)