mirror of
https://github.com/deepset-ai/haystack.git
synced 2026-10-12 02:12:50 +00:00
fix: component tool schema description (#11909)
Co-authored-by: David S. Batista <[email protected]>
This commit is contained in:
co-authored by
David S. Batista
parent
50671f7ca6
commit
04435add0b
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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 "
|
||||
|
||||
Reference in New Issue
Block a user