Skip to content

Commit 04435ad

Browse files
sjrldavidsbatista
andauthored
fix: component tool schema description (#11909)
Co-authored-by: David S. Batista <dsbatista@gmail.com>
1 parent 50671f7 commit 04435ad

5 files changed

Lines changed: 26 additions & 59 deletions

File tree

haystack/tools/component_tool.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ def _create_tool_parameters_schema(self, component: Component, inputs_from_state
337337
:raises SchemaGenerationError: If schema generation fails
338338
:returns: OpenAI tools schema for the component's run method parameters.
339339
"""
340-
component_run_description, param_descriptions = _get_component_param_descriptions(component)
340+
param_descriptions = _get_component_param_descriptions(component)
341341

342342
# collect fields (types and defaults) and descriptions from function parameters
343343
fields: dict[str, Any] = {}
@@ -365,7 +365,9 @@ def _create_tool_parameters_schema(self, component: Component, inputs_from_state
365365

366366
parameters_schema: dict[str, Any] = {}
367367
try:
368-
model = create_model(component.run.__name__, __doc__=component_run_description, **fields)
368+
# No `__doc__`: it would surface as a top-level `description` on the parameters schema,
369+
# which LLM providers ignore. The component description feeds the tool-level description.
370+
model = create_model(component.run.__name__, **fields)
369371
parameters_schema = model.model_json_schema()
370372
except Exception as e:
371373
raise SchemaGenerationError(

haystack/tools/parameters_schema_utils.py

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -102,47 +102,34 @@ def _get_param_descriptions(method: Callable) -> tuple[str, dict[str, str]]:
102102
return parsed_doc.short_description or "", param_descriptions
103103

104104

105-
def _get_component_param_descriptions(component: Any) -> tuple[str, dict[str, str]]:
105+
def _get_component_param_descriptions(component: Any) -> dict[str, str]:
106106
"""
107107
Get parameter descriptions from a component, handling both regular Components and SuperComponents.
108108
109109
For regular components, this extracts descriptions from the run method's docstring.
110110
For SuperComponents, this extracts descriptions from the underlying pipeline components.
111111
112112
:param component: The component to extract parameter descriptions from
113-
:returns: A tuple of (short_description, param_descriptions)
113+
:returns: A dictionary mapping parameter names to their descriptions
114114
"""
115115
from haystack.core.super_component.super_component import _SuperComponent
116116

117117
# Get descriptions from the component's run method
118-
short_desc, param_descriptions = _get_param_descriptions(component.run)
118+
_, param_descriptions = _get_param_descriptions(component.run)
119119

120-
# If it's a SuperComponent, enhance the descriptions from the original components
120+
# If it's a SuperComponent, enhance the parameter descriptions from the original components
121121
if isinstance(component, _SuperComponent):
122-
# Collect descriptions from components in the pipeline
123-
component_descriptions = []
124-
processed_components = set()
125-
126-
# First gather descriptions from all components that have inputs mapped
127122
for super_param_name, pipeline_paths in component.input_mapping.items():
128123
# Collect descriptions from all mapped components
129124
descriptions = []
130125
for path in pipeline_paths:
131126
try:
132-
# Get the component and socket this input is mapped fromq
127+
# Get the component and socket this input is mapped from
133128
comp_name, socket_name = component._split_component_path(path)
134129
pipeline_component = component.pipeline.get_component(comp_name)
135130

136-
# Get run method descriptions for this component
137-
run_desc, run_param_descriptions = _get_param_descriptions(pipeline_component.run)
138-
139-
# Don't add the same component description multiple times
140-
if comp_name not in processed_components:
141-
processed_components.add(comp_name)
142-
if run_desc:
143-
component_descriptions.append(f"'{comp_name}': {run_desc}")
144-
145131
# Add parameter description if available
132+
_, run_param_descriptions = _get_param_descriptions(pipeline_component.run)
146133
if input_param_mapping := run_param_descriptions.get(socket_name):
147134
descriptions.append(f"Provided to the '{comp_name}' component as: '{input_param_mapping}'")
148135
except Exception as e:
@@ -153,20 +140,12 @@ def _get_component_param_descriptions(component: Any) -> tuple[str, dict[str, st
153140
e=str(e),
154141
)
155142

156-
# We don't only handle a one to one description mapping of input parameters, but a one to many mapping.
157-
# i.e. for a combined_input parameter description:
158-
# super_comp = SuperComponent(
159-
# pipeline=pipeline,
160-
# input_mapping={"combined_input": ["comp_a.query", "comp_b.text"]},
161-
# )
143+
# A single SuperComponent input can map to multiple pipeline components, e.g.
144+
# input_mapping={"combined_input": ["comp_a.query", "comp_b.text"]}
162145
if descriptions:
163146
param_descriptions[super_param_name] = ", and ".join(descriptions) + "."
164147

165-
# We also create a combined description for the SuperComponent based on its components
166-
if component_descriptions:
167-
short_desc = f"A component that combines: {', '.join(component_descriptions)}"
168-
169-
return short_desc, param_descriptions
148+
return param_descriptions
170149

171150

172151
def _dataclass_to_pydantic_model(dc_type: Any) -> type[BaseModel]:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
fixes:
3+
- |
4+
``ComponentTool`` no longer adds a top-level ``description`` key to the generated ``parameters``
5+
JSON schema. The wrapped component's docstring was emitted inside the parameters schema, where
6+
LLM providers (OpenAI, Anthropic, Google) do not use it, and for tools wrapping a
7+
``SuperComponent`` it produced a confusing auto-generated summary. The parameters schema now only
8+
carries per-parameter descriptions, matching plain ``Tool`` and function-based tools. The
9+
tool-level ``description`` is unchanged.

test/tools/test_component_tool.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@ def test_from_component_basic(self):
179179
assert tool.description == "A simple component that generates text."
180180
assert tool.parameters == {
181181
"type": "object",
182-
"description": "A simple component that generates text.",
183182
"properties": {"text": {"type": "string", "description": "user's name"}},
184183
"required": ["text"],
185184
}
@@ -198,21 +197,13 @@ def test_from_component_with_inputs_from_state(self):
198197
tool = ComponentTool(component=SimpleComponent(), inputs_from_state={"text": "text"})
199198
assert tool.inputs_from_state == {"text": "text"}
200199
# Inputs should be excluded from schema generation
201-
assert tool.parameters == {
202-
"type": "object",
203-
"properties": {},
204-
"description": "A simple component that generates text.",
205-
}
200+
assert tool.parameters == {"type": "object", "properties": {}}
206201

207202
def test_from_component_with_inputs_from_state_different_names(self):
208203
tool = ComponentTool(component=SimpleComponent(), inputs_from_state={"state_text": "text"})
209204
assert tool.inputs_from_state == {"state_text": "text"}
210205
# Inputs should be excluded from schema generation
211-
assert tool.parameters == {
212-
"type": "object",
213-
"properties": {},
214-
"description": "A simple component that generates text.",
215-
}
206+
assert tool.parameters == {"type": "object", "properties": {}}
216207

217208
def test_from_component_with_invalid_inputs_from_state_nested_dict(self):
218209
"""Test that ComponentTool rejects nested dict format for inputs_from_state"""
@@ -240,7 +231,6 @@ def test_from_component_with_dataclass(self):
240231
"type": "object",
241232
}
242233
},
243-
"description": "A simple component that processes a User.",
244234
"properties": {"user": {"$ref": "#/$defs/User", "description": "The User object to process."}},
245235
"required": ["user"],
246236
"type": "object",
@@ -262,7 +252,6 @@ def test_from_component_with_list_input(self):
262252

263253
assert tool.parameters == {
264254
"type": "object",
265-
"description": "Concatenates a list of strings into a single string.",
266255
"properties": {
267256
"texts": {
268257
"type": "array",
@@ -303,7 +292,6 @@ def test_from_component_with_nested_dataclass(self):
303292
"type": "object",
304293
},
305294
},
306-
"description": "Creates information about the person.",
307295
"properties": {"person": {"$ref": "#/$defs/Person", "description": "The Person to process."}},
308296
"required": ["person"],
309297
"type": "object",
@@ -328,7 +316,6 @@ def test_from_component_with_list_of_documents(self):
328316
"Document": DOCUMENT_SCHEMA,
329317
"SparseEmbedding": SPARSE_EMBEDDING_SCHEMA,
330318
},
331-
"description": "Concatenates the content of multiple documents with newlines.",
332319
"properties": {
333320
"documents": {
334321
"description": "List of Documents whose content will be concatenated",
@@ -351,7 +338,6 @@ def test_from_component_with_dynamic_input_types(self):
351338
builder = PromptBuilder(template="Hello, {{name}}!")
352339
tool = ComponentTool(component=builder, name="prompt_builder_tool")
353340
assert tool.parameters == {
354-
"description": "Renders the prompt template with the provided variables.",
355341
"properties": {
356342
"name": {"description": "Input 'name' for the component."},
357343
"template": {
@@ -417,10 +403,9 @@ def run(self, text: str, number: int = 42) -> dict[str, str]:
417403
# Create ComponentTool from SuperComponent
418404
tool = ComponentTool(component=super_comp, name="text_processor")
419405

420-
# Verify that schema includes the docstrings from the original component
406+
# Verify that schema includes the per-parameter docstrings from the original component
421407
assert tool.parameters == {
422408
"type": "object",
423-
"description": "A component that combines: 'processor': Process inputs and return result.",
424409
"properties": {
425410
"input_text": {
426411
"type": "string",
@@ -484,11 +469,9 @@ def run(self, text: str) -> dict[str, str]:
484469
# Create ComponentTool from SuperComponent
485470
tool = ComponentTool(component=super_comp, name="combined_processor")
486471

487-
# Verify that schema includes combined docstrings from both components
472+
# Verify that schema includes combined per-parameter docstrings from both components
488473
assert tool.parameters == {
489474
"type": "object",
490-
"description": "A component that combines: 'comp_a': Process query in component A., 'comp_b': Process "
491-
"text in component B.",
492475
"properties": {
493476
"combined_input": {
494477
"type": "string",

test/tools/test_pipeline_tool.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,6 @@ def test_auto_generated_tool_params(self, sample_pipeline):
164164
)
165165

166166
assert tool.parameters == {
167-
"description": "A component that combines: 'bm25_retriever': Run the InMemoryBM25Retriever on the "
168-
"given input data., 'ranker': Returns a list of documents ranked by their similarity "
169-
"to the given query.",
170167
"properties": {
171168
"query": {
172169
"description": "Provided to the 'bm25_retriever' component as: 'The query string for the Retriever."
@@ -182,9 +179,6 @@ def test_auto_generated_tool_params(self, sample_pipeline):
182179
def test_auto_generated_tool_params_no_mappings(self, sample_pipeline):
183180
tool = PipelineTool(pipeline=sample_pipeline, name="test_tool", description="A test tool")
184181
assert tool.parameters == {
185-
"description": "A component that combines: 'bm25_retriever': Run the InMemoryBM25Retriever on the given "
186-
"input data., 'ranker': Returns a list of documents ranked by their similarity to the "
187-
"given query.",
188182
"properties": {
189183
"query": {
190184
"description": "Provided to the 'bm25_retriever' component as: 'The query string for the "

0 commit comments

Comments
 (0)