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
20 changes: 12 additions & 8 deletions src/agents/function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,20 @@ def _suppress_griffe_logging():

def _ensure_blank_line_before_google_sections(doc: str) -> str:
"""Insert a blank line before a Google-style parameter section header (``Args:`` or an
alias) that directly follows non-indented text (for example a summary line).
alias) that directly follows a non-blank line, such as a summary line or the indented body
of a preceding section.

griffe's Google parser silently skips a section header when there is no blank line above
it and the following line is indented (it logs "Missing blank line above section"). That
drops every parameter description and leaks the raw ``Args:`` block into the description.
numpy/sphinx parsing already tolerates the missing blank line, so this normalizes the
Google case to match. Only the parameter section is normalized because
generate_func_documentation only consumes parameter sections (plus the first text block);
other griffe sections are intentionally left alone. The string is returned unchanged when
no insertion is needed, which keeps well-formed docstrings byte-identical.
griffe applies that gate no matter how the line above is indented, so a header that follows
another section's indented body (for example ``Note:`` or ``Example:``) needs the same
normalization as one that follows the summary. numpy/sphinx parsing already tolerates the
missing blank line, so this normalizes the Google case to match. Only the parameter section
is normalized because generate_func_documentation only consumes parameter sections (plus
the first text block); other griffe sections are intentionally left alone. The string is
returned unchanged when no insertion is needed, which keeps well-formed docstrings
byte-identical.
"""
lines = doc.splitlines()
output: list[str] = []
Expand All @@ -177,10 +181,10 @@ def _ensure_blank_line_before_google_sections(doc: str) -> str:
if (
index > 0
and _GOOGLE_SECTION_HEADER_RE.match(line)
# Preceding line is non-blank top-level text (summary), not an indented section body.
# Preceding line is non-blank, so griffe would skip the header. Its indentation does
# not matter, because the header itself is anchored at column 0 by the regex above.
and output
and output[-1].strip()
and not output[-1].startswith((" ", "\t"))
# Following line is an indented block, matching griffe's "indented line below" gate.
and index + 1 < len(lines)
and lines[index + 1].startswith((" ", "\t"))
Expand Down
50 changes: 50 additions & 0 deletions tests/test_function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -958,3 +958,53 @@ def weather(city: str, units: str) -> str:
assert properties["city"]["description"] == "The city to get weather for."
assert properties["units"]["description"] == "Temperature units to use."
assert "Args:" not in (weather.description or "")


def section_body_before_args_google_function(city: str, units: str):
"""Get the weather for a city.

Note:
Results are cached.
Args:
city: The city to get weather for.
units: Temperature units to use.
"""
return f"{city} {units}"


def section_body_before_args_blank_line_google_function(city: str, units: str):
"""Get the weather for a city.

Note:
Results are cached.

Args:
city: The city to get weather for.
units: Temperature units to use.
"""
return f"{city} {units}"


def test_google_docstring_missing_blank_line_after_section_body():
"""A Google docstring whose ``Args:`` directly follows another section's indented body
(no blank line) should still yield parameter descriptions. griffe skips the header
whenever no blank line sits above it, regardless of how the preceding line is indented."""
fs = function_schema(section_body_before_args_google_function, strict_json_schema=False)

properties = fs.params_json_schema.get("properties", {})
assert properties["city"]["description"] == "The city to get weather for."
assert properties["units"]["description"] == "Temperature units to use."

assert "Args:" not in (fs.description or "")


def test_google_docstring_after_section_body_matches_blank_line_form():
"""The variant missing the blank line after a preceding section's body must produce the
exact same schema as the well-formed variant that includes it."""
fixed = function_schema(section_body_before_args_google_function, strict_json_schema=False)
control = function_schema(
section_body_before_args_blank_line_google_function, strict_json_schema=False
)

assert fixed.description == control.description
assert fixed.params_json_schema["properties"] == control.params_json_schema["properties"]