豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
28 changes: 24 additions & 4 deletions src/agents/function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ def function_schema(
takes_context = False
filtered_params = []

# Track whether the first real (non-self/cls) parameter has been processed for context check
self_or_cls_skipped = False

if params:
first_name, first_param = params[0]
# Prefer the evaluated type hint if available
Expand All @@ -297,15 +300,24 @@ 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"):
self_or_cls_skipped = True # Skip bound method receiver parameter
Comment thread
seratch marked this conversation as resolved.
Outdated
else:
filtered_params.append((first_name, first_param))

# For parameters other than the first, raise error if any use RunContextWrapper or ToolContext.
for name, param in params[1:]:
# For parameters other than the first, raise error if any use RunContextWrapper or ToolContext
# (unless self/cls was skipped, in which case ONLY the param immediately after self/cls is
# treated as the effective first param).
for idx, (name, param) in enumerate(params[1:]):
ann = type_hints.get(name, param.annotation)
if ann != inspect._empty:
origin = get_origin(ann) or ann
if origin is RunContextWrapper or origin is ToolContext:
if self_or_cls_skipped and not takes_context and idx == 0:
# self/cls was the first param and this is immediately after it
takes_context = True
self_or_cls_skipped = False
continue
raise UserError(
f"RunContextWrapper/ToolContext param found at non-first position in function"
f" {func.__name__}"
Expand Down Expand Up @@ -409,14 +421,22 @@ def function_schema(
if strict_json_schema:
json_schema = ensure_strict_json_schema(json_schema)

# 5. Return as a FuncSchema dataclass
# 5. Build a signature that excludes self/cls so to_call_args iterates
# only the parameters the caller actually needs to pass.
if self_or_cls_skipped or (takes_context and params and params[0][0] in ("self", "cls")):
remaining = [p for name, p in params if name not in ("self", "cls")]
Comment thread
seratch marked this conversation as resolved.
Outdated
call_sig = sig.replace(parameters=remaining)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drop only the leading receiver from call signature

This filter removes every parameter named self or cls, not just the first receiver that was skipped. For valid signatures like def f(self, cls: int, value: str), the generated schema still includes cls, but to_call_args iterates a signature where cls was deleted, so the provided value is ignored and argument mapping becomes incorrect. The replacement signature should remove only the single leading receiver parameter.

Useful? React with 👍 / 👎.

else:
call_sig = sig

# 6. Return as a FuncSchema dataclass
return FuncSchema(
name=func_name,
# Ensure description_override takes precedence even if docstring info is disabled.
description=description_override or (doc_info.description if doc_info else None),
params_pydantic_model=dynamic_model,
params_json_schema=json_schema,
signature=sig,
signature=call_sig,
takes_context=takes_context,
strict_json_schema=strict_json_schema,
)
94 changes: 94 additions & 0 deletions tests/test_function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,3 +885,97 @@ def func_with_annotated_multiple_field_constraints(

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


def test_method_self_param_skipped():
"""Test that self parameter is skipped for class methods."""

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_classmethod_cls_param_skipped():
"""Test that cls parameter is skipped for classmethods passed as unbound."""

# 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}

fs = function_schema(fn, use_docstring_info=False)
props = fs.params_json_schema.get("properties", {})
assert "cls" not in props
assert "name" in props


def test_method_self_with_context_second_param():
"""Test that self is skipped and RunContextWrapper as second param is recognized."""

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", {})
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_method_self_excluded_from_call_signature():
"""Test that self/cls is excluded from the stored signature used by to_call_args."""

# 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}

fs = function_schema(fn, use_docstring_info=False)
# self should not be in the signature used by to_call_args
assert "self" not in fs.signature.parameters
assert "name" in fs.signature.parameters
assert fs.takes_context is True

# to_call_args should produce only the non-context, non-self params
parsed = fs.params_pydantic_model(name="world")
args, kwargs = fs.to_call_args(parsed)
assert args == ["world"]


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