From 55e6f0c9bd4ad6b206c3c7a06554ad7c4baa23a7 Mon Sep 17 00:00:00 2001 From: Vitor Coelho <6161176+VitorCoelho@users.noreply.github.com> Date: Wed, 4 Nov 2026 21:50:49 -0300 Subject: [PATCH] fix: describe custom actions in HighLevelActionSet.to_tool_description to_tool_description() looked up each action function with globals()[tool_name], i.e. in the highlevel module namespace, which only contains the built-in actions. As a result: - an action set with custom_actions raised KeyError for the first custom action, so tool-use agents could not use custom actions at all; - a custom action reusing a built-in name (e.g. its own click()) was described with the built-in signature instead of its own. Keep the functions registered in the action set and read the signature from them. Add a test covering both cases. --- .../src/browsergym/core/action/highlevel.py | 6 ++- tests/core/test_actions_highlevel.py | 46 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/browsergym/core/src/browsergym/core/action/highlevel.py b/browsergym/core/src/browsergym/core/action/highlevel.py index c7374537e..f9bbeca3d 100644 --- a/browsergym/core/src/browsergym/core/action/highlevel.py +++ b/browsergym/core/src/browsergym/core/action/highlevel.py @@ -372,6 +372,7 @@ def __init__( # parse the actions and build the action space self.action_set: dict[str, HighLevelAction] = {} + self._action_functions: dict[str, callable] = {} self.python_includes = "" # include playwright imports @@ -427,6 +428,7 @@ def __init__( if func.__name__ in self.action_set: raise ValueError(f"Duplicated action '{func.__name__}'") + self._action_functions[func.__name__] = func self.action_set[func.__name__] = HighLevelAction( # entrypoint=func, signature=signature, @@ -574,7 +576,9 @@ def to_tool_description(self, api="openai", add_examples=True) -> list[dict]: for tool_name, action in self.action_set.items(): # Parse the signature to extract parameter names and types parameters = {"type": "object", "properties": {}, "required": []} - signature = inspect.signature(globals()[tool_name]) + # use the function registered in this action set, so that custom actions + # (including ones that reuse a built-in action name) are described correctly + signature = inspect.signature(self._action_functions[tool_name]) for param_name, param in signature.parameters.items(): param_type = "string" # Default to string if type is not specified if param.annotation != inspect.Parameter.empty: diff --git a/tests/core/test_actions_highlevel.py b/tests/core/test_actions_highlevel.py index 4f0c08523..806edba82 100644 --- a/tests/core/test_actions_highlevel.py +++ b/tests/core/test_actions_highlevel.py @@ -110,6 +110,52 @@ def test_action_parser(): ] +def _custom_click_twice(bid: str, times: int = 2): + """ + Clicks an element several times. + + Examples: + _custom_click_twice('a51', 3) + """ + pass + + +def click(bid: str, note: str = ""): + """ + A custom action that reuses the name of a built-in action. + + Examples: + click('a51', 'submit') + """ + pass + + +def test_tool_description_custom_actions(): + action_set = HighLevelActionSet(subsets=["custom"], custom_actions=[_custom_click_twice]) + tools = {tool["name"]: tool for tool in action_set.to_tool_description()} + assert set(tools) == {"noop", "_custom_click_twice"} + assert tools["_custom_click_twice"]["parameters"] == { + "type": "object", + "properties": { + "bid": {"type": "string"}, + "times": {"type": "number", "default": 2}, + }, + "required": ["bid"], + } + + # a custom action named like a built-in one must be described with its own signature + action_set = HighLevelActionSet(subsets=["custom"], custom_actions=[click]) + tools = {tool["name"]: tool for tool in action_set.to_tool_description(api="anthropic")} + assert tools["click"]["input_schema"] == { + "type": "object", + "properties": { + "bid": {"type": "string"}, + "note": {"type": "string", "default": ""}, + }, + "required": ["bid"], + } + + def test_valid_action(): action_set = HighLevelActionSet()