From 12cf9f04c9c379e2a640115a4c452d3a63704876 Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee Date: Wed, 8 Jul 2026 12:17:14 +0200 Subject: [PATCH 1/2] Fix component tool adding unnecessary descriptions field to parameters schema --- haystack/tools/component_tool.py | 6 ++-- haystack/tools/parameters_schema_utils.py | 31 +++---------------- ...l-schema-description-b3e47b8bc4742f9c.yaml | 9 ++++++ test/tools/test_component_tool.py | 25 +++------------ test/tools/test_pipeline_tool.py | 6 ---- 5 files changed, 22 insertions(+), 55 deletions(-) create mode 100644 releasenotes/notes/component-tool-schema-description-b3e47b8bc4742f9c.yaml diff --git a/haystack/tools/component_tool.py b/haystack/tools/component_tool.py index 7609b5600d5..3afe8e399c3 100644 --- a/haystack/tools/component_tool.py +++ b/haystack/tools/component_tool.py @@ -337,7 +337,7 @@ def _create_tool_parameters_schema(self, component: Component, inputs_from_state :raises SchemaGenerationError: If schema generation fails :returns: OpenAI tools schema for the component's run method parameters. """ - component_run_description, param_descriptions = _get_component_param_descriptions(component) + _, param_descriptions = _get_component_param_descriptions(component) # collect fields (types and defaults) and descriptions from function parameters fields: dict[str, Any] = {} @@ -365,7 +365,9 @@ def _create_tool_parameters_schema(self, component: Component, inputs_from_state parameters_schema: dict[str, Any] = {} try: - model = create_model(component.run.__name__, __doc__=component_run_description, **fields) + # No `__doc__`: it would surface as a top-level `description` on the parameters schema, + # which LLM providers ignore. The component description feeds the tool-level description. + model = create_model(component.run.__name__, **fields) parameters_schema = model.model_json_schema() except Exception as e: raise SchemaGenerationError( diff --git a/haystack/tools/parameters_schema_utils.py b/haystack/tools/parameters_schema_utils.py index 07608b5ad16..88445b469e1 100644 --- a/haystack/tools/parameters_schema_utils.py +++ b/haystack/tools/parameters_schema_utils.py @@ -117,32 +117,19 @@ def _get_component_param_descriptions(component: Any) -> tuple[str, dict[str, st # Get descriptions from the component's run method short_desc, param_descriptions = _get_param_descriptions(component.run) - # If it's a SuperComponent, enhance the descriptions from the original components + # If it's a SuperComponent, enhance the parameter descriptions from the original components if isinstance(component, _SuperComponent): - # Collect descriptions from components in the pipeline - component_descriptions = [] - processed_components = set() - - # First gather descriptions from all components that have inputs mapped for super_param_name, pipeline_paths in component.input_mapping.items(): # Collect descriptions from all mapped components descriptions = [] for path in pipeline_paths: try: - # Get the component and socket this input is mapped fromq + # Get the component and socket this input is mapped from comp_name, socket_name = component._split_component_path(path) pipeline_component = component.pipeline.get_component(comp_name) - # Get run method descriptions for this component - run_desc, run_param_descriptions = _get_param_descriptions(pipeline_component.run) - - # Don't add the same component description multiple times - if comp_name not in processed_components: - processed_components.add(comp_name) - if run_desc: - component_descriptions.append(f"'{comp_name}': {run_desc}") - # Add parameter description if available + _, run_param_descriptions = _get_param_descriptions(pipeline_component.run) if input_param_mapping := run_param_descriptions.get(socket_name): descriptions.append(f"Provided to the '{comp_name}' component as: '{input_param_mapping}'") except Exception as e: @@ -153,19 +140,11 @@ def _get_component_param_descriptions(component: Any) -> tuple[str, dict[str, st e=str(e), ) - # We don't only handle a one to one description mapping of input parameters, but a one to many mapping. - # i.e. for a combined_input parameter description: - # super_comp = SuperComponent( - # pipeline=pipeline, - # input_mapping={"combined_input": ["comp_a.query", "comp_b.text"]}, - # ) + # A single SuperComponent input can map to multiple pipeline components, e.g. + # input_mapping={"combined_input": ["comp_a.query", "comp_b.text"]} if descriptions: param_descriptions[super_param_name] = ", and ".join(descriptions) + "." - # We also create a combined description for the SuperComponent based on its components - if component_descriptions: - short_desc = f"A component that combines: {', '.join(component_descriptions)}" - return short_desc, param_descriptions diff --git a/releasenotes/notes/component-tool-schema-description-b3e47b8bc4742f9c.yaml b/releasenotes/notes/component-tool-schema-description-b3e47b8bc4742f9c.yaml new file mode 100644 index 00000000000..66173372555 --- /dev/null +++ b/releasenotes/notes/component-tool-schema-description-b3e47b8bc4742f9c.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + ``ComponentTool`` no longer adds a top-level ``description`` key to the generated ``parameters`` + JSON schema. The wrapped component's docstring was emitted inside the parameters schema, where + LLM providers (OpenAI, Anthropic, Google) do not use it, and for tools wrapping a + ``SuperComponent`` it produced a confusing auto-generated summary. The parameters schema now only + carries per-parameter descriptions, matching plain ``Tool`` and function-based tools. The + tool-level ``description`` is unchanged. diff --git a/test/tools/test_component_tool.py b/test/tools/test_component_tool.py index a20ebe7eec3..dd0c42bc5ba 100644 --- a/test/tools/test_component_tool.py +++ b/test/tools/test_component_tool.py @@ -179,7 +179,6 @@ def test_from_component_basic(self): assert tool.description == "A simple component that generates text." assert tool.parameters == { "type": "object", - "description": "A simple component that generates text.", "properties": {"text": {"type": "string", "description": "user's name"}}, "required": ["text"], } @@ -198,21 +197,13 @@ def test_from_component_with_inputs_from_state(self): tool = ComponentTool(component=SimpleComponent(), inputs_from_state={"text": "text"}) assert tool.inputs_from_state == {"text": "text"} # Inputs should be excluded from schema generation - assert tool.parameters == { - "type": "object", - "properties": {}, - "description": "A simple component that generates text.", - } + assert tool.parameters == {"type": "object", "properties": {}} def test_from_component_with_inputs_from_state_different_names(self): tool = ComponentTool(component=SimpleComponent(), inputs_from_state={"state_text": "text"}) assert tool.inputs_from_state == {"state_text": "text"} # Inputs should be excluded from schema generation - assert tool.parameters == { - "type": "object", - "properties": {}, - "description": "A simple component that generates text.", - } + assert tool.parameters == {"type": "object", "properties": {}} def test_from_component_with_invalid_inputs_from_state_nested_dict(self): """Test that ComponentTool rejects nested dict format for inputs_from_state""" @@ -240,7 +231,6 @@ def test_from_component_with_dataclass(self): "type": "object", } }, - "description": "A simple component that processes a User.", "properties": {"user": {"$ref": "#/$defs/User", "description": "The User object to process."}}, "required": ["user"], "type": "object", @@ -262,7 +252,6 @@ def test_from_component_with_list_input(self): assert tool.parameters == { "type": "object", - "description": "Concatenates a list of strings into a single string.", "properties": { "texts": { "type": "array", @@ -303,7 +292,6 @@ def test_from_component_with_nested_dataclass(self): "type": "object", }, }, - "description": "Creates information about the person.", "properties": {"person": {"$ref": "#/$defs/Person", "description": "The Person to process."}}, "required": ["person"], "type": "object", @@ -328,7 +316,6 @@ def test_from_component_with_list_of_documents(self): "Document": DOCUMENT_SCHEMA, "SparseEmbedding": SPARSE_EMBEDDING_SCHEMA, }, - "description": "Concatenates the content of multiple documents with newlines.", "properties": { "documents": { "description": "List of Documents whose content will be concatenated", @@ -351,7 +338,6 @@ def test_from_component_with_dynamic_input_types(self): builder = PromptBuilder(template="Hello, {{name}}!") tool = ComponentTool(component=builder, name="prompt_builder_tool") assert tool.parameters == { - "description": "Renders the prompt template with the provided variables.", "properties": { "name": {"description": "Input 'name' for the component."}, "template": { @@ -417,10 +403,9 @@ def run(self, text: str, number: int = 42) -> dict[str, str]: # Create ComponentTool from SuperComponent tool = ComponentTool(component=super_comp, name="text_processor") - # Verify that schema includes the docstrings from the original component + # Verify that schema includes the per-parameter docstrings from the original component assert tool.parameters == { "type": "object", - "description": "A component that combines: 'processor': Process inputs and return result.", "properties": { "input_text": { "type": "string", @@ -484,11 +469,9 @@ def run(self, text: str) -> dict[str, str]: # Create ComponentTool from SuperComponent tool = ComponentTool(component=super_comp, name="combined_processor") - # Verify that schema includes combined docstrings from both components + # Verify that schema includes combined per-parameter docstrings from both components assert tool.parameters == { "type": "object", - "description": "A component that combines: 'comp_a': Process query in component A., 'comp_b': Process " - "text in component B.", "properties": { "combined_input": { "type": "string", diff --git a/test/tools/test_pipeline_tool.py b/test/tools/test_pipeline_tool.py index 27b70916655..344c7d34f35 100644 --- a/test/tools/test_pipeline_tool.py +++ b/test/tools/test_pipeline_tool.py @@ -164,9 +164,6 @@ def test_auto_generated_tool_params(self, sample_pipeline): ) assert tool.parameters == { - "description": "A component that combines: 'bm25_retriever': Run the InMemoryBM25Retriever on the " - "given input data., 'ranker': Returns a list of documents ranked by their similarity " - "to the given query.", "properties": { "query": { "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): def test_auto_generated_tool_params_no_mappings(self, sample_pipeline): tool = PipelineTool(pipeline=sample_pipeline, name="test_tool", description="A test tool") assert tool.parameters == { - "description": "A component that combines: 'bm25_retriever': Run the InMemoryBM25Retriever on the given " - "input data., 'ranker': Returns a list of documents ranked by their similarity to the " - "given query.", "properties": { "query": { "description": "Provided to the 'bm25_retriever' component as: 'The query string for the " From 1ca518d14ecf16889e0484cd7ad184bda3de7683 Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee Date: Wed, 8 Jul 2026 12:19:05 +0200 Subject: [PATCH 2/2] more changes --- haystack/tools/component_tool.py | 2 +- haystack/tools/parameters_schema_utils.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/haystack/tools/component_tool.py b/haystack/tools/component_tool.py index 3afe8e399c3..2e1682a44c3 100644 --- a/haystack/tools/component_tool.py +++ b/haystack/tools/component_tool.py @@ -337,7 +337,7 @@ def _create_tool_parameters_schema(self, component: Component, inputs_from_state :raises SchemaGenerationError: If schema generation fails :returns: OpenAI tools schema for the component's run method parameters. """ - _, param_descriptions = _get_component_param_descriptions(component) + param_descriptions = _get_component_param_descriptions(component) # collect fields (types and defaults) and descriptions from function parameters fields: dict[str, Any] = {} diff --git a/haystack/tools/parameters_schema_utils.py b/haystack/tools/parameters_schema_utils.py index 88445b469e1..fcaac47c6ad 100644 --- a/haystack/tools/parameters_schema_utils.py +++ b/haystack/tools/parameters_schema_utils.py @@ -102,7 +102,7 @@ def _get_param_descriptions(method: Callable) -> tuple[str, dict[str, str]]: return parsed_doc.short_description or "", param_descriptions -def _get_component_param_descriptions(component: Any) -> tuple[str, dict[str, str]]: +def _get_component_param_descriptions(component: Any) -> dict[str, str]: """ Get parameter descriptions from a component, handling both regular Components and SuperComponents. @@ -110,12 +110,12 @@ def _get_component_param_descriptions(component: Any) -> tuple[str, dict[str, st For SuperComponents, this extracts descriptions from the underlying pipeline components. :param component: The component to extract parameter descriptions from - :returns: A tuple of (short_description, param_descriptions) + :returns: A dictionary mapping parameter names to their descriptions """ from haystack.core.super_component.super_component import _SuperComponent # Get descriptions from the component's run method - short_desc, param_descriptions = _get_param_descriptions(component.run) + _, param_descriptions = _get_param_descriptions(component.run) # If it's a SuperComponent, enhance the parameter descriptions from the original components if isinstance(component, _SuperComponent): @@ -145,7 +145,7 @@ def _get_component_param_descriptions(component: Any) -> tuple[str, dict[str, st if descriptions: param_descriptions[super_param_name] = ", and ".join(descriptions) + "." - return short_desc, param_descriptions + return param_descriptions def _dataclass_to_pydantic_model(dc_type: Any) -> type[BaseModel]: