豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
10 changes: 9 additions & 1 deletion src/agents/function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,18 @@ def function_schema(
takes_context = True # Mark that the function takes context
else:
filtered_params.append((first_name, first_param))
elif first_name in ("self", "cls"):
# An unannotated self/cls means this is an unbound method or classmethod.
# Bound methods (e.g., instance.method) already have self stripped by Python.
raise UserError(
f"Function {func.__name__} has an unbound '{first_name}' parameter. "
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid rejecting method decorators that pass unbound functions

This branch now throws UserError for any unannotated first parameter named self/cls, but @function_tool is applied to unbound functions during class definition, so class T: @function_tool def f(self, x: str) now fails immediately. Because function_tool always calls function_schema(the_func, ...) on that unbound callable, this change blocks the common decorator workflow for method-based tools instead of allowing them to be registered and later invoked as bound methods.

Useful? React with 👍 / 👎.

f"Pass a bound method (e.g., instance.{func.__name__}) instead of the "
f"unbound class method."
)
else:
filtered_params.append((first_name, first_param))

# For parameters other than the first, raise error if any use RunContextWrapper or ToolContext.
# For parameters other than the first, raise error if any use RunContextWrapper or ToolContext
for name, param in params[1:]:
ann = type_hints.get(name, param.annotation)
if ann != inspect._empty:
Expand Down
85 changes: 85 additions & 0 deletions tests/test_function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,3 +885,88 @@ def func_with_annotated_multiple_field_constraints(

with pytest.raises(ValidationError): # zero factor
fs.params_pydantic_model(**{"score": 50, "factor": 0.0})


def test_bound_method_self_not_in_schema():
"""Test that bound methods work normally (Python already strips self)."""

class MyTools:
def greet(self, name: str) -> str:
return f"Hello, {name}"

obj = MyTools()
fs = function_schema(obj.greet, use_docstring_info=False)
props = fs.params_json_schema.get("properties", {})
assert "self" not in props
assert "name" in props
assert fs.params_json_schema.get("required") == ["name"]


def test_unbound_cls_param_raises():
"""Test that unbound classmethods with unannotated cls raise UserError."""

# Simulate a function whose first param is named cls with no annotation
code = compile("def greet(cls, name: str) -> str: ...", "<test>", "exec")
ns: dict[str, Any] = {}
exec(code, ns) # noqa: S102
fn = ns["greet"]
fn.__annotations__ = {"name": str, "return": str}

with pytest.raises(UserError, match="unbound 'cls' parameter"):
function_schema(fn, use_docstring_info=False)


def test_bound_method_with_context_second_param():
"""Test that bound methods with RunContextWrapper as second param work correctly."""

class MyTools:
def greet(self, ctx: RunContextWrapper[None], name: str) -> str:
return f"Hello, {name}"

obj = MyTools()
fs = function_schema(obj.greet, use_docstring_info=False)
props = fs.params_json_schema.get("properties", {})
# self is already stripped by Python for bound methods
assert "self" not in props
assert "ctx" not in props
assert "name" in props
assert fs.takes_context is True


def test_method_context_not_immediately_after_self_raises():
"""Test that RunContextWrapper at position 3+ (not immediately after self) raises UserError."""

class MyTools:
def greet(self, name: str, ctx: RunContextWrapper[None]) -> str:
return f"Hello, {name}"

obj = MyTools()
with pytest.raises(UserError, match="non-first position"):
function_schema(obj.greet, use_docstring_info=False)


def test_unbound_method_with_self_raises():
"""Test that unbound methods with unannotated self raise UserError."""

# Simulate an unbound method with self as first param
code = compile(
"def greet(self, ctx, name: str) -> str: ...", "<test>", "exec"
)
ns: dict[str, Any] = {}
exec(code, ns) # noqa: S102
fn = ns["greet"]
fn.__annotations__ = {"ctx": RunContextWrapper[None], "name": str, "return": str}

with pytest.raises(UserError, match="unbound 'self' parameter"):
function_schema(fn, use_docstring_info=False)


def test_regular_unannotated_first_param_still_included():
"""Test that a regular unannotated first param (not self/cls) is still included."""

def process(data, flag: bool = False) -> str:
return str(data)

fs = function_schema(process, use_docstring_info=False)
props = fs.params_json_schema.get("properties", {})
assert "data" in props