fix: component tool schema description (#11909)

Co-authored-by: David S. Batista <[email protected]>
This commit is contained in:
Sebastian Husch Lee
2026-07-09 14:27:53 +02:00
committed by GitHub
co-authored by David S. Batista
parent 50671f7ca6
commit 04435add0b
5 changed files with 26 additions and 59 deletions
+4 -2
View File
@@ -337,7 +337,7 @@ class ComponentTool(Tool):
: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 @@ class ComponentTool(Tool):
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(
+9 -30
View File
@@ -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,39 +110,26 @@ 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 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,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]:
@@ -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.
+4 -21
View File
@@ -179,7 +179,6 @@ class TestComponentTool:
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 @@ class TestComponentTool:
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 @@ class TestComponentTool:
"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 @@ class TestComponentTool:
assert tool.parameters == {
"type": "object",
"description": "Concatenates a list of strings into a single string.",
"properties": {
"texts": {
"type": "array",
@@ -303,7 +292,6 @@ class TestComponentTool:
"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 @@ class TestComponentTool:
"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 @@ class TestComponentTool:
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 @@ class TestComponentTool:
# 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 @@ class TestComponentTool:
# 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",
-6
View File
@@ -164,9 +164,6 @@ class TestPipelineTool:
)
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 @@ class TestPipelineTool:
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 "