Skip to content

Commit 28fea41

Browse files
authored
fix: Strip argument sections out of inputSpec top-level description (#1142)
Per #1067 including the args in the description is redundant as it's already included in the parameter docs which can increase the token counts. Strip args from the description strings for inputSpecs --------- Co-authored-by: Mackenzie Zastrow <[email protected]>
1 parent 1df45be commit 28fea41

File tree

2 files changed

+222
-15
lines changed

2 files changed

+222
-15
lines changed

src/strands/tools/decorator.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,56 @@ def _create_input_model(self) -> Type[BaseModel]:
164164
# Handle case with no parameters
165165
return create_model(model_name)
166166

167+
def _extract_description_from_docstring(self) -> str:
168+
"""Extract the docstring excluding only the Args section.
169+
170+
This method uses the parsed docstring to extract everything except
171+
the Args/Arguments/Parameters section, preserving Returns, Raises,
172+
Examples, and other sections.
173+
174+
Returns:
175+
The description text, or the function name if no description is available.
176+
"""
177+
func_name = self.func.__name__
178+
179+
# Fallback: try to extract manually from raw docstring
180+
raw_docstring = inspect.getdoc(self.func)
181+
if raw_docstring:
182+
lines = raw_docstring.strip().split("\n")
183+
result_lines = []
184+
skip_args_section = False
185+
186+
for line in lines:
187+
stripped_line = line.strip()
188+
189+
# Check if we're starting the Args section
190+
if stripped_line.lower().startswith(("args:", "arguments:", "parameters:", "param:", "params:")):
191+
skip_args_section = True
192+
continue
193+
194+
# Check if we're starting a new section (not Args)
195+
elif (
196+
stripped_line.lower().startswith(("returns:", "return:", "yields:", "yield:"))
197+
or stripped_line.lower().startswith(("raises:", "raise:", "except:", "exceptions:"))
198+
or stripped_line.lower().startswith(("examples:", "example:", "note:", "notes:"))
199+
or stripped_line.lower().startswith(("see also:", "seealso:", "references:", "ref:"))
200+
):
201+
skip_args_section = False
202+
result_lines.append(line)
203+
continue
204+
205+
# If we're not in the Args section, include the line
206+
if not skip_args_section:
207+
result_lines.append(line)
208+
209+
# Join and clean up the description
210+
description = "\n".join(result_lines).strip()
211+
if description:
212+
return description
213+
214+
# Final fallback: use function name
215+
return func_name
216+
167217
def extract_metadata(self) -> ToolSpec:
168218
"""Extract metadata from the function to create a tool specification.
169219
@@ -173,20 +223,16 @@ def extract_metadata(self) -> ToolSpec:
173223
The specification includes:
174224
175225
- name: The function name (or custom override)
176-
- description: The function's docstring
226+
- description: The function's docstring description (excluding Args)
177227
- inputSchema: A JSON schema describing the expected parameters
178228
179229
Returns:
180230
A dictionary containing the tool specification.
181231
"""
182232
func_name = self.func.__name__
183233

184-
# Extract function description from docstring, preserving paragraph breaks
185-
description = inspect.getdoc(self.func)
186-
if description:
187-
description = description.strip()
188-
else:
189-
description = func_name
234+
# Extract function description from parsed docstring, excluding Args section and beyond
235+
description = self._extract_description_from_docstring()
190236

191237
# Get schema directly from the Pydantic model
192238
input_schema = self.input_model.model_json_schema()

tests/strands/tools/test_decorator.py

Lines changed: 169 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -221,14 +221,7 @@ def test_tool(param1: str, param2: int) -> str:
221221

222222
# Check basic spec properties
223223
assert spec["name"] == "test_tool"
224-
assert (
225-
spec["description"]
226-
== """Test tool function.
227-
228-
Args:
229-
param1: First parameter
230-
param2: Second parameter"""
231-
)
224+
assert spec["description"] == "Test tool function."
232225

233226
# Check input schema
234227
schema = spec["inputSchema"]["json"]
@@ -310,6 +303,174 @@ def test_tool(required: str, optional: Optional[int] = None) -> str:
310303
exp_events = [
311304
ToolResultEvent({"toolUseId": "test-id", "status": "success", "content": [{"text": "Result: hello 42"}]})
312305
]
306+
assert tru_events == exp_events
307+
308+
309+
@pytest.mark.asyncio
310+
async def test_docstring_description_extraction():
311+
"""Test that docstring descriptions are extracted correctly, excluding Args section."""
312+
313+
@strands.tool
314+
def tool_with_full_docstring(param1: str, param2: int) -> str:
315+
"""This is the main description.
316+
317+
This is more description text.
318+
319+
Args:
320+
param1: First parameter
321+
param2: Second parameter
322+
323+
Returns:
324+
A string result
325+
326+
Raises:
327+
ValueError: If something goes wrong
328+
"""
329+
return f"{param1} {param2}"
330+
331+
spec = tool_with_full_docstring.tool_spec
332+
assert (
333+
spec["description"]
334+
== """This is the main description.
335+
336+
This is more description text.
337+
338+
Returns:
339+
A string result
340+
341+
Raises:
342+
ValueError: If something goes wrong"""
343+
)
344+
345+
346+
def test_docstring_args_variations():
347+
"""Test that various Args section formats are properly excluded."""
348+
349+
@strands.tool
350+
def tool_with_args(param: str) -> str:
351+
"""Main description.
352+
353+
Args:
354+
param: Parameter description
355+
"""
356+
return param
357+
358+
@strands.tool
359+
def tool_with_arguments(param: str) -> str:
360+
"""Main description.
361+
362+
Arguments:
363+
param: Parameter description
364+
"""
365+
return param
366+
367+
@strands.tool
368+
def tool_with_parameters(param: str) -> str:
369+
"""Main description.
370+
371+
Parameters:
372+
param: Parameter description
373+
"""
374+
return param
375+
376+
@strands.tool
377+
def tool_with_params(param: str) -> str:
378+
"""Main description.
379+
380+
Params:
381+
param: Parameter description
382+
"""
383+
return param
384+
385+
for tool in [tool_with_args, tool_with_arguments, tool_with_parameters, tool_with_params]:
386+
spec = tool.tool_spec
387+
assert spec["description"] == "Main description."
388+
389+
390+
def test_docstring_no_args_section():
391+
"""Test docstring extraction when there's no Args section."""
392+
393+
@strands.tool
394+
def tool_no_args(param: str) -> str:
395+
"""This is the complete description.
396+
397+
Returns:
398+
A string result
399+
"""
400+
return param
401+
402+
spec = tool_no_args.tool_spec
403+
expected_desc = """This is the complete description.
404+
405+
Returns:
406+
A string result"""
407+
assert spec["description"] == expected_desc
408+
409+
410+
def test_docstring_only_args_section():
411+
"""Test docstring extraction when there's only an Args section."""
412+
413+
@strands.tool
414+
def tool_only_args(param: str) -> str:
415+
"""Args:
416+
param: Parameter description
417+
"""
418+
return param
419+
420+
spec = tool_only_args.tool_spec
421+
# Should fall back to function name when no description remains
422+
assert spec["description"] == "tool_only_args"
423+
424+
425+
def test_docstring_empty():
426+
"""Test docstring extraction when docstring is empty."""
427+
428+
@strands.tool
429+
def tool_empty_docstring(param: str) -> str:
430+
return param
431+
432+
spec = tool_empty_docstring.tool_spec
433+
# Should fall back to function name
434+
assert spec["description"] == "tool_empty_docstring"
435+
436+
437+
def test_docstring_preserves_other_sections():
438+
"""Test that non-Args sections are preserved in the description."""
439+
440+
@strands.tool
441+
def tool_multiple_sections(param: str) -> str:
442+
"""Main description here.
443+
444+
Args:
445+
param: This should be excluded
446+
447+
Returns:
448+
This should be included
449+
450+
Raises:
451+
ValueError: This should be included
452+
453+
Examples:
454+
This should be included
455+
456+
Note:
457+
This should be included
458+
"""
459+
return param
460+
461+
spec = tool_multiple_sections.tool_spec
462+
description = spec["description"]
463+
464+
# Should include main description and other sections
465+
assert "Main description here." in description
466+
assert "Returns:" in description
467+
assert "This should be included" in description
468+
assert "Raises:" in description
469+
assert "Examples:" in description
470+
assert "Note:" in description
471+
472+
# Should exclude Args section
473+
assert "This should be excluded" not in description
313474

314475

315476
@pytest.mark.asyncio

0 commit comments

Comments
 (0)