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
6 changes: 4 additions & 2 deletions haystack/tools/component_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}
Expand Down Expand Up @@ -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(
Expand Down
39 changes: 9 additions & 30 deletions haystack/tools/parameters_schema_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,47 +102,34 @@ 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.

For regular components, this extracts descriptions from the run method's docstring.
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 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:
Expand All @@ -153,20 +140,12 @@ 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
return param_descriptions


def _dataclass_to_pydantic_model(dc_type: Any) -> type[BaseModel]:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 4 additions & 21 deletions test/tools/test_component_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
}
Expand All @@ -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"""
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 0 additions & 6 deletions test/tools/test_pipeline_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand All @@ -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 "
Expand Down
Loading