diff --git a/agentrun/integration/utils/tool.py b/agentrun/integration/utils/tool.py index 8b566ed..bde72a5 100644 --- a/agentrun/integration/utils/tool.py +++ b/agentrun/integration/utils/tool.py @@ -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": """绑定工具到实例,便于在类中定义工具方法""" diff --git a/agentrun/model/model.py b/agentrun/model/model.py index 73a5e2d..cc6157d 100644 --- a/agentrun/model/model.py +++ b/agentrun/model/model.py @@ -131,6 +131,7 @@ class ProxyConfigTokenRateLimiter(BaseModel): class ProxyConfigAIGuardrailConfig(BaseModel): """AI 防护配置""" + check_request: Optional[bool] = None check_response: Optional[bool] = None diff --git a/agentrun/utils/exception.py b/agentrun/utils/exception.py index acf0219..c98d01d 100644 --- a/agentrun/utils/exception.py +++ b/agentrun/utils/exception.py @@ -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: diff --git a/examples/agent_runtime.py b/examples/agent_runtime.py index 301ac77..c36a15e 100644 --- a/examples/agent_runtime.py +++ b/examples/agent_runtime.py @@ -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() diff --git a/examples/model.py b/examples/model.py index cee914f..656cdf4 100644 --- a/examples/model.py +++ b/examples/model.py @@ -1,3 +1,4 @@ +from logging import config import os import re import time @@ -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={}, ), ) ) @@ -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: diff --git a/tests/unittests/integration/test_integration.py b/tests/unittests/integration/test_integration.py index 6156b93..ef3cf83 100644 --- a/tests/unittests/integration/test_integration.py +++ b/tests/unittests/integration/test_integration.py @@ -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"):