Skip to content

Commit 4d1e6a9

Browse files
authored
fix: docstring enhancement with decorators (#169)
* feat: improve programmatic use * chore: remove unused param * chore: simplify combined decorator * chore: format * fix: api key pass through * chore: format
1 parent 18854f4 commit 4d1e6a9

File tree

5 files changed

+134
-97
lines changed

5 files changed

+134
-97
lines changed

src/deepset_mcp/tool_factory.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ def build_tool(
227227
base_func: Callable[..., Any],
228228
config: ToolConfig,
229229
workspace_mode: WorkspaceMode,
230+
api_key: str | None = None,
230231
workspace: str | None = None,
231232
use_request_context: bool = True,
232233
base_url: str | None = None,
@@ -240,6 +241,7 @@ def build_tool(
240241
:param base_func: The base tool function.
241242
:param config: Tool configuration specifying dependencies and custom arguments.
242243
:param workspace_mode: How the workspace should be handled.
244+
:param api_key: The deepset API key to use.
243245
:param workspace: The workspace to use when using a static workspace.
244246
:param use_request_context: Whether to collect the API key from the request context.
245247
:param base_url: Base URL for the deepset API.
@@ -258,7 +260,9 @@ def build_tool(
258260
enhanced_func = apply_workspace(enhanced_func, config, workspace_mode, workspace)
259261

260262
# Apply client injection (adds ctx parameter if needed)
261-
enhanced_func = apply_client(enhanced_func, config, use_request_context=use_request_context, base_url=base_url)
263+
enhanced_func = apply_client(
264+
enhanced_func, config, use_request_context=use_request_context, base_url=base_url, api_key=api_key
265+
)
262266

263267
# Create final async wrapper if needed
264268
if not inspect.iscoroutinefunction(enhanced_func):
@@ -354,13 +358,14 @@ def register_tools(
354358
enhanced_tool = base_func(explorer=explorer)
355359
else:
356360
enhanced_tool = build_tool(
357-
base_func,
358-
config,
359-
workspace_mode,
360-
workspace,
361-
get_api_key_from_authorization_header,
362-
base_url,
363-
object_store,
361+
base_func=base_func,
362+
config=config,
363+
workspace_mode=workspace_mode,
364+
workspace=workspace,
365+
use_request_context=get_api_key_from_authorization_header,
366+
base_url=base_url,
367+
object_store=object_store,
368+
api_key=api_key,
364369
)
365370

366371
mcp_server_instance.add_tool(enhanced_tool, name=tool_name)

src/deepset_mcp/tools/tokonomics/decorators.py

Lines changed: 38 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -49,75 +49,54 @@ def _add_str_to_type(annotation: Any) -> Any:
4949
return annotation | str
5050

5151

52-
def _enhance_docstring_for_references(original: str, param_info: dict[str, dict[str, Any]], func_name: str) -> str:
53-
"""Add reference documentation to function docstring.
52+
def _enhance_docstring_for_references(original: str, func_name: str) -> str:
53+
"""Create complete docstring for LLM tool with reference support.
5454
5555
:param original: Original docstring.
56-
:param param_info: Parameter modification info.
5756
:param func_name: Function name for examples.
58-
:return: Enhanced docstring.
57+
:return: Complete docstring for LLM tool.
5958
"""
6059
if not original:
61-
original = f"{func_name} function with reference support."
60+
original = f"{func_name} function."
6261

63-
# Build the reference section
64-
ref_section = [
65-
"",
66-
"**Reference Support**",
62+
enhancement = [
6763
"",
6864
"All parameters accept object references in the form ``@obj_id`` or ``@obj_id.path.to.value``.",
6965
"",
66+
"Examples::",
67+
"",
68+
" # Direct call with values",
69+
f" {func_name}(data={{'key': 'value'}}, threshold=10)",
70+
"",
71+
" # Call with references",
72+
f" {func_name}(data='@obj_123', threshold='@obj_456.config.threshold')",
73+
"",
74+
" # Mixed call",
75+
f" {func_name}(data='@obj_123.items', threshold=10)",
7076
]
7177

72-
if param_info:
73-
ref_section.append("Parameter types after decoration:")
74-
ref_section.append("")
75-
for name, info in param_info.items():
76-
if info["accepts_str"]:
77-
ref_section.append(f"- ``{name}``: {info['original']} (already accepts strings)")
78-
else:
79-
ref_section.append(f"- ``{name}``: {info['original']}{info['modified']} (now accepts references)")
80-
ref_section.append("")
81-
82-
ref_section.extend(
83-
[
84-
"Examples::",
85-
"",
86-
" # Direct call with values",
87-
f" {func_name}(data={{'key': 'value'}}, threshold=10)",
88-
"",
89-
" # Call with references",
90-
f" {func_name}(data='@obj_123', threshold='@obj_456.config.threshold')",
91-
"",
92-
" # Mixed call",
93-
f" {func_name}(data='@obj_123.items', threshold=10)",
94-
]
95-
)
96-
97-
return original.rstrip() + "\n" + "\n".join(ref_section)
78+
return original.rstrip() + "\n" + "\n".join(enhancement)
9879

9980

10081
def _enhance_docstring_for_explorable(original: str, func_name: str) -> str:
101-
"""Add explorable documentation to function docstring.
82+
"""Create complete docstring for LLM tool with output storage.
10283
10384
:param original: Original docstring.
10485
:param func_name: Function name.
105-
:return: Enhanced docstring.
86+
:return: Complete docstring for LLM tool.
10687
"""
10788
if not original:
108-
original = f"{func_name} function with stored output."
89+
original = f"{func_name} function."
10990

110-
section = [
111-
"",
112-
"**Output Storage**",
91+
enhancement = [
11392
"",
114-
"The output of this function is automatically stored and can be referenced in other functions.",
115-
"The function returns a formatted preview of the result along with an object ID (e.g., ``@obj_123``).",
116-
"",
117-
"Use the returned object ID to pass this result to other functions that accept references.",
93+
"The output is automatically stored and can be referenced in other functions.",
94+
"Returns a formatted preview with an object ID (e.g., ``@obj_123``).",
95+
"Use the object store tools in combination with the object ID to view nested properties of the object.",
96+
"Use the returned object ID to pass this result to other functions.",
11897
]
11998

120-
return original.rstrip() + "\n" + "\n".join(section)
99+
return original.rstrip() + "\n" + "\n".join(enhancement)
121100

122101

123102
def explorable(
@@ -290,7 +269,7 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
290269

291270
# Update signature and docstring
292271
async_wrapper.__signature__ = new_sig # type: ignore[attr-defined]
293-
async_wrapper.__doc__ = _enhance_docstring_for_references(func.__doc__ or "", param_info, func.__name__)
272+
async_wrapper.__doc__ = _enhance_docstring_for_references(func.__doc__ or "", func.__name__)
294273
return async_wrapper # type: ignore[return-value]
295274
else:
296275

@@ -333,7 +312,7 @@ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
333312

334313
# Update signature and docstring
335314
sync_wrapper.__signature__ = new_sig # type: ignore[attr-defined]
336-
sync_wrapper.__doc__ = _enhance_docstring_for_references(func.__doc__ or "", param_info, func.__name__)
315+
sync_wrapper.__doc__ = _enhance_docstring_for_references(func.__doc__ or "", func.__name__)
337316
return sync_wrapper # type: ignore[return-value]
338317

339318
return decorator
@@ -375,26 +354,19 @@ def decorator(func: F) -> F:
375354

376355
# Combine docstrings (remove duplicate function name line)
377356
if ref_func.__doc__ and exp_func.__doc__:
378-
# Take the reference part from ref_func and explorable part from exp_func
379-
ref_lines = ref_func.__doc__.split("\n")
380357
exp_lines = exp_func.__doc__.split("\n")
381-
382-
# Find where the reference section starts
383-
ref_start = next((i for i, line in enumerate(ref_lines) if "**Reference Support**" in line), len(ref_lines))
384358
# Find where the explorable section starts
385-
exp_start = next((i for i, line in enumerate(exp_lines) if "**Output Storage**" in line), 0)
386-
387-
# Combine: original + reference section + explorable section
388-
# Take everything from ref_func including reference section but excluding examples
389-
# Find end of reference section (before examples)
390-
ref_end = len(ref_lines)
391-
for i in range(ref_start, len(ref_lines)):
392-
if "Examples::" in ref_lines[i]:
393-
ref_end = i
394-
break
395-
396-
combined = ref_lines[:ref_end] + exp_lines[exp_start:]
397-
exp_func.__doc__ = "\n".join(combined)
359+
exp_start = next(
360+
(
361+
i
362+
for i, line in enumerate(exp_lines)
363+
if "The output is automatically stored and can be referenced" in line
364+
),
365+
0,
366+
)
367+
368+
combined = ref_func.__doc__ + "\n".join(exp_lines[exp_start:])
369+
exp_func.__doc__ = combined
398370

399371
return exp_func
400372

test/unit/test_tool_factory.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ async def sample_func(client: AsyncClientProtocol, workspace: str, a: int, custo
549549
custom_args={"custom_arg": "injected"},
550550
)
551551

552-
result = build_tool(sample_func, config, WorkspaceMode.STATIC, "test-workspace")
552+
result = build_tool(sample_func, config, WorkspaceMode.STATIC, workspace="test-workspace")
553553

554554
# Check final signature
555555
sig = inspect.signature(result)
@@ -578,7 +578,9 @@ async def sample_func(client: AsyncClientProtocol, workspace: str, a: int) -> st
578578
needs_workspace=True,
579579
)
580580

581-
result = build_tool(sample_func, config, WorkspaceMode.STATIC, "test-workspace")
581+
result = build_tool(
582+
sample_func, config, WorkspaceMode.STATIC, workspace="test-workspace", use_request_context=True
583+
)
582584

583585
# Mock the context and use FakeClient
584586
mock_ctx = MagicMock()

test/unit/tools/tokonomics/test_decorators.py

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -90,26 +90,24 @@ def test_add_str_to_type_other_type(self) -> None:
9090
def test_enhance_docstring_for_references(self) -> None:
9191
"""Test docstring enhancement for referenceable functions."""
9292
original = "Original docstring."
93-
param_info = {
94-
"data": {"original": "dict", "modified": "dict | str", "accepts_str": False},
95-
"text": {"original": "str", "modified": "str", "accepts_str": True},
96-
}
9793

98-
result = _enhance_docstring_for_references(original, param_info, "test_func")
94+
result = _enhance_docstring_for_references(original, "test_func")
9995

10096
assert "Original docstring." in result
101-
assert "**Reference Support**" in result
10297
assert "All parameters accept object references" in result
103-
assert "``data``: dict → dict | str" in result
104-
assert "``text``: str (already accepts strings)" in result
10598
assert "test_func(data='@obj_123'" in result
99+
# Should not contain the old reference support formatting
100+
assert "**Reference Support**" not in result
101+
assert "dict → dict | str" not in result
106102

107103
def test_enhance_docstring_for_references_empty_original(self) -> None:
108104
"""Test docstring enhancement with empty original docstring."""
109-
result = _enhance_docstring_for_references("", {}, "test_func")
105+
result = _enhance_docstring_for_references("", "test_func")
110106

111-
assert "test_func function with reference support." in result
112-
assert "**Reference Support**" in result
107+
assert "test_func function." in result
108+
assert "All parameters accept object references" in result
109+
# Should not contain the old reference support formatting
110+
assert "**Reference Support**" not in result
113111

114112
def test_enhance_docstring_for_explorable(self) -> None:
115113
"""Test docstring enhancement for explorable functions."""
@@ -118,16 +116,69 @@ def test_enhance_docstring_for_explorable(self) -> None:
118116
result = _enhance_docstring_for_explorable(original, "test_func")
119117

120118
assert "Original docstring." in result
121-
assert "**Output Storage**" in result
122119
assert "automatically stored and can be referenced" in result
123120
assert "object ID (e.g., ``@obj_123``)" in result
121+
# Should not contain the old output storage formatting
122+
assert "**Output Storage**" not in result
124123

125124
def test_enhance_docstring_for_explorable_empty_original(self) -> None:
126125
"""Test docstring enhancement with empty original docstring."""
127126
result = _enhance_docstring_for_explorable("", "test_func")
128127

129-
assert "test_func function with stored output." in result
130-
assert "**Output Storage**" in result
128+
assert "test_func function." in result
129+
assert "automatically stored and can be referenced" in result
130+
# Should not contain the old output storage formatting
131+
assert "**Output Storage**" not in result
132+
133+
def test_enhance_docstring_for_references_preserves_original(self) -> None:
134+
"""Test that _enhance_docstring_for_references preserves all original content."""
135+
original = """Process data and return results.
136+
137+
This function processes the input data and returns processed results.
138+
It may raise exceptions if the data is invalid.
139+
140+
:param data: Input data to process
141+
:param options: Processing options
142+
:return: Processed results
143+
:raises ValueError: If data is invalid
144+
:raises RuntimeError: If processing fails
145+
"""
146+
147+
result = _enhance_docstring_for_references(original, "test_func")
148+
149+
# Check that all original content is preserved exactly
150+
assert "Process data and return results." in result
151+
assert "This function processes the input data" in result
152+
assert ":param data: Input data to process" in result
153+
assert ":param options: Processing options" in result
154+
assert ":return: Processed results" in result
155+
assert ":raises ValueError: If data is invalid" in result
156+
assert ":raises RuntimeError: If processing fails" in result
157+
158+
# Check that enhancement is added
159+
assert "All parameters accept object references" in result
160+
assert "test_func(data='@obj_123'" in result
161+
162+
def test_enhance_docstring_for_explorable_preserves_original(self) -> None:
163+
"""Test that _enhance_docstring_for_explorable preserves all original content."""
164+
original = """Calculate statistics from data.
165+
166+
:param values: List of numbers
167+
:return: Statistical summary dict
168+
:raises ValueError: If values is empty
169+
"""
170+
171+
result = _enhance_docstring_for_explorable(original, "calc_stats")
172+
173+
# Check that all original content is preserved exactly
174+
assert "Calculate statistics from data." in result
175+
assert ":param values: List of numbers" in result
176+
assert ":return: Statistical summary dict" in result
177+
assert ":raises ValueError: If values is empty" in result
178+
179+
# Check that enhancement is added
180+
assert "automatically stored and can be referenced" in result
181+
assert "object ID (e.g., ``@obj_123``)" in result
131182

132183

133184
class TestExplorableDecorator:
@@ -205,8 +256,9 @@ def test_func() -> dict:
205256
return {}
206257

207258
assert "Original docstring." in test_func.__doc__
208-
assert "**Output Storage**" in test_func.__doc__
209259
assert "automatically stored" in test_func.__doc__
260+
# Should not contain the old output storage formatting
261+
assert "**Output Storage**" not in test_func.__doc__
210262

211263
def test_explorable_with_args(self, store: ObjectStore, explorer: RichExplorer) -> None:
212264
"""Test @explorable decorated function with arguments."""
@@ -390,8 +442,9 @@ def test_func(data: dict) -> str:
390442
return "result"
391443

392444
assert "Original docstring." in test_func.__doc__
393-
assert "**Reference Support**" in test_func.__doc__
394445
assert "All parameters accept object references" in test_func.__doc__
446+
# Should not contain the old reference support formatting
447+
assert "**Reference Support**" not in test_func.__doc__
395448

396449

397450
class TestExplorableAndReferenceableDecorator:
@@ -465,8 +518,11 @@ def test_func(data: dict) -> dict:
465518

466519
# Should contain both reference and explorable documentation
467520
assert "Original docstring." in docstring
468-
assert "**Reference Support**" in docstring
469-
assert "**Output Storage**" in docstring
521+
assert "All parameters accept object references" in docstring
522+
assert "automatically stored" in docstring
523+
# Should not contain the old section formatting
524+
assert "**Reference Support**" not in docstring
525+
assert "**Output Storage**" not in docstring
470526

471527
def test_combined_decorator_signature(self, store: ObjectStore, explorer: RichExplorer) -> None:
472528
"""Test that combined decorator modifies signature correctly."""

test/unit/tools/tokonomics/test_integration.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,14 +438,16 @@ def documented_referenceable(stats: dict, precision: int = 2) -> str:
438438
# Check enhanced docstrings
439439
explorable_doc = documented_explorable.__doc__
440440
assert "Process a list of items" in explorable_doc
441-
assert "**Output Storage**" in explorable_doc
442441
assert "automatically stored" in explorable_doc
442+
# Should not contain the old output storage formatting
443+
assert "**Output Storage**" not in explorable_doc
443444

444445
referenceable_doc = documented_referenceable.__doc__
445446
assert "Format statistics" in referenceable_doc
446-
assert "**Reference Support**" in referenceable_doc
447447
assert "All parameters accept object references" in referenceable_doc
448-
assert "``stats``: <class 'dict'> → dict | str" in referenceable_doc
448+
# Should not contain the old reference support formatting
449+
assert "**Reference Support**" not in referenceable_doc
450+
assert "dict | str" not in referenceable_doc
449451

450452
# Test functionality with enhanced docs
451453
result1 = documented_explorable([10, 20, 30, 40, 50])

0 commit comments

Comments
 (0)