Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions agentrun/integration/utils/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,23 @@ def to_pydanticai(self) -> Any:

return func

def __get__(self, obj: Any, objtype: Any = None) -> "Tool":
"""实现描述符协议,使 Tool 在类属性访问时自动绑定到实例

这允许在工具方法内部调用其他 @tool 装饰的方法时正常工作。
例如:goto 方法调用 self.browser_navigate() 时,会自动获取绑定版本。
"""
if obj is None:
# 通过类访问(如 BrowserToolSet.browser_navigate),返回未绑定的 Tool
return self

# 通过实例访问,返回绑定到该实例的 Tool
# 使用实例的 __dict__ 作为缓存,避免每次访问都创建新的 Tool 对象
cache_key = f"_bound_tool_{id(self)}"
if cache_key not in obj.__dict__:
obj.__dict__[cache_key] = self.bind(obj)
return obj.__dict__[cache_key]

def bind(self, instance: Any) -> "Tool":
"""绑定工具到实例,便于在类中定义工具方法"""

Expand Down
1 change: 1 addition & 0 deletions agentrun/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class ProxyConfigTokenRateLimiter(BaseModel):

class ProxyConfigAIGuardrailConfig(BaseModel):
"""AI 防护配置"""

check_request: Optional[bool] = None
check_response: Optional[bool] = None

Expand Down
8 changes: 5 additions & 3 deletions agentrun/utils/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,11 @@ def to_resource_error(
"does not exist" in self.message or "not found" in self.message
):
return ResourceNotExistError(resource_type, resource_id)
elif (self.status_code == 400 or self.status_code == 500) and (
"already exists" in self.message
):
elif (
self.status_code == 400
or self.status_code == 409
or self.status_code == 500
) and ("already exists" in self.message):
# TODO: ModelProxy already exists returns 500
return ResourceAlreadyExistError(resource_type, resource_id)
else:
Expand Down
15 changes: 10 additions & 5 deletions examples/agent_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,16 @@ def create_or_get_agentruntime():
),
)
)
except ResourceAlreadyExistError:
logger.info("已存在,获取已有资源")

ar = client.list(
AgentRuntimeListInput(agent_runtime_name=agent_runtime_name)
except ResourceAlreadyExistError as e:
logger.info("已存在,获取已有资源", e)

ar = list(
filter(
lambda a: a.agent_runtime_name == agent_runtime_name,
client.list(
AgentRuntimeListInput(agent_runtime_name=agent_runtime_name)
),
)
)[0]

ar.wait_until_ready_or_failed()
Expand Down
18 changes: 15 additions & 3 deletions examples/model.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from logging import config
import os
import re
import time
Expand Down Expand Up @@ -135,21 +136,25 @@ def create_or_get_model_proxy():
"""
logger.info("创建或获取已有的资源")

from agentrun.utils.config import Config

cfg = Config()

try:
cred = client.create(
ModelProxyCreateInput(
model_proxy_name=model_proxy_name,
description="测试模型治理",
model_type=model.ModelType.LLM,
execution_role_arn=f"acs:ram::{cfg.get_account_id()}:role/aliyunagentrundefaultrole",
proxy_config=model.ProxyConfig(
endpoints=[
model.ProxyConfigEndpoint(
model_names=[model_name],
model_service_name="test-model-service",
model_service_name=model_service_name,
)
for model_name in model_names
],
policies={},
),
)
)
Expand All @@ -172,9 +177,16 @@ def update_model_proxy(mp: ModelProxy):
"""
logger.info("更新描述为当前时间")

from agentrun.utils.config import Config

cfg = Config()

# 也可以使用 client.update
mp.update(
ModelProxyUpdateInput(description=f"当前时间戳:{time.time()}"),
ModelProxyUpdateInput(
execution_role_arn=f"acs:ram::{cfg.get_account_id()}:role/aliyunagentrundefaultrole",
description=f"当前时间戳:{time.time()}",
),
)
mp.wait_until_ready_or_failed()
if mp.status != Status.READY:
Expand Down
106 changes: 106 additions & 0 deletions tests/unittests/integration/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,112 @@ def _assert_tools(self, tools_payload):
)


class TestToolDescriptorProtocol:
"""测试 Tool 类的描述符协议实现

确保工具方法内部调用其他 @tool 装饰的方法时能正常工作。
这是修复 BrowserToolSet.goto() 调用 browser_navigate() 时缺少 self 参数问题的测试。
"""

def test_tool_internal_call_works(self):
"""测试工具内部调用其他工具时能正常工作"""
from agentrun.integration.utils.tool import CommonToolSet, tool

class TestToolSet(CommonToolSet):

def __init__(self):
self.call_log: List[str] = []
super().__init__()

@tool(name="main_tool", description="主工具,会调用子工具")
def main_tool(self, value: str) -> str:
"""主工具,内部调用 sub_tool"""
self.call_log.append(f"main_tool({value})")
# 这里调用另一个 @tool 装饰的方法
# 修复前会报错:TypeError: ... missing 1 required positional argument: 'self'
result = self.sub_tool(value=f"from_main:{value}")
return f"main_result:{result}"

@tool(name="sub_tool", description="子工具")
def sub_tool(self, value: str) -> str:
"""子工具"""
self.call_log.append(f"sub_tool({value})")
return f"sub_result:{value}"

ts = TestToolSet()

# 直接调用 main_tool,它内部会调用 sub_tool
result = ts.main_tool(value="test_input")

# 验证两个工具都被正确调用
assert ts.call_log == [
"main_tool(test_input)",
"sub_tool(from_main:test_input)",
]
assert result == "main_result:sub_result:from_main:test_input"

def test_tool_descriptor_returns_bound_tool(self):
"""测试 Tool.__get__ 返回绑定到实例的 Tool"""
from agentrun.integration.utils.tool import CommonToolSet, Tool, tool

class TestToolSet(CommonToolSet):

def __init__(self):
super().__init__()

@tool(name="my_tool", description="测试工具")
def my_tool(self, x: int) -> int:
return x * 2

ts = TestToolSet()

# 通过实例访问应该返回绑定的 Tool
bound_tool = ts.my_tool
assert isinstance(bound_tool, Tool)

# 绑定的 Tool 应该可以直接调用,不需要传入 self
result = bound_tool(x=5)
assert result == 10

def test_tool_descriptor_class_access(self):
"""测试通过类访问 Tool 时返回未绑定的 Tool"""
from agentrun.integration.utils.tool import CommonToolSet, Tool, tool

class TestToolSet(CommonToolSet):

@tool(name="class_tool", description="类工具")
def class_tool(self, x: int) -> int:
return x * 2

# 通过类访问应该返回未绑定的 Tool
unbound_tool = TestToolSet.class_tool
assert isinstance(unbound_tool, Tool)

# 未绑定的 Tool 调用时需要手动传入实例
instance = TestToolSet()
# 通过实例访问会自动绑定
bound_tool = instance.class_tool
assert bound_tool(x=3) == 6

def test_tool_descriptor_caching(self):
"""测试 Tool.__get__ 的缓存机制"""
from agentrun.integration.utils.tool import CommonToolSet, tool

class TestToolSet(CommonToolSet):

@tool(name="cached_tool", description="缓存测试工具")
def cached_tool(self) -> str:
return "cached"

ts = TestToolSet()

# 多次访问应该返回同一个绑定的 Tool 对象(缓存)
tool1 = ts.cached_tool
tool2 = ts.cached_tool

assert tool1 is tool2 # 应该是同一个对象


class TestIntegration:

def get_mocked_toolset(self, timezone="UTC"):
Expand Down
Loading