• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

deepset-ai / haystack / 16143753132

08 Jul 2025 12:51PM UTC coverage: 90.433%. Remained the same
16143753132

push

github

web-flow
docs: fix curly brackets (#9599)

11684 of 12920 relevant lines covered (90.43%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.74
haystack/tools/component_tool.py
1
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2
#
3
# SPDX-License-Identifier: Apache-2.0
4

5
from typing import Any, Callable, Dict, Optional, Union, get_args, get_origin
1✔
6

7
from pydantic import Field, TypeAdapter, create_model
1✔
8

9
from haystack import logging
1✔
10
from haystack.core.component import Component
1✔
11
from haystack.core.serialization import (
1✔
12
    component_from_dict,
13
    component_to_dict,
14
    generate_qualified_class_name,
15
    import_class_by_name,
16
)
17
from haystack.tools import Tool
1✔
18
from haystack.tools.errors import SchemaGenerationError
1✔
19
from haystack.tools.from_function import _remove_title_from_schema
1✔
20
from haystack.tools.parameters_schema_utils import _get_component_param_descriptions, _resolve_type
1✔
21
from haystack.utils.callable_serialization import deserialize_callable, serialize_callable
1✔
22

23
logger = logging.getLogger(__name__)
1✔
24

25

26
class ComponentTool(Tool):
1✔
27
    """
28
    A Tool that wraps Haystack components, allowing them to be used as tools by LLMs.
29

30
    ComponentTool automatically generates LLM-compatible tool schemas from component input sockets,
31
    which are derived from the component's `run` method signature and type hints.
32

33

34
    Key features:
35
    - Automatic LLM tool calling schema generation from component input sockets
36
    - Type conversion and validation for component inputs
37
    - Support for types:
38
        - Dataclasses
39
        - Lists of dataclasses
40
        - Basic types (str, int, float, bool, dict)
41
        - Lists of basic types
42
    - Automatic name generation from component class name
43
    - Description extraction from component docstrings
44

45
    To use ComponentTool, you first need a Haystack component - either an existing one or a new one you create.
46
    You can create a ComponentTool from the component by passing the component to the ComponentTool constructor.
47
    Below is an example of creating a ComponentTool from an existing SerperDevWebSearch component.
48

49
    ```python
50
    from haystack import component, Pipeline
51
    from haystack.tools import ComponentTool
52
    from haystack.components.websearch import SerperDevWebSearch
53
    from haystack.utils import Secret
54
    from haystack.components.tools.tool_invoker import ToolInvoker
55
    from haystack.components.generators.chat import OpenAIChatGenerator
56
    from haystack.dataclasses import ChatMessage
57

58
    # Create a SerperDev search component
59
    search = SerperDevWebSearch(api_key=Secret.from_env_var("SERPERDEV_API_KEY"), top_k=3)
60

61
    # Create a tool from the component
62
    tool = ComponentTool(
63
        component=search,
64
        name="web_search",  # Optional: defaults to "serper_dev_web_search"
65
        description="Search the web for current information on any topic"  # Optional: defaults to component docstring
66
    )
67

68
    # Create pipeline with OpenAIChatGenerator and ToolInvoker
69
    pipeline = Pipeline()
70
    pipeline.add_component("llm", OpenAIChatGenerator(model="gpt-4o-mini", tools=[tool]))
71
    pipeline.add_component("tool_invoker", ToolInvoker(tools=[tool]))
72

73
    # Connect components
74
    pipeline.connect("llm.replies", "tool_invoker.messages")
75

76
    message = ChatMessage.from_user("Use the web search tool to find information about Nikola Tesla")
77

78
    # Run pipeline
79
    result = pipeline.run({"llm": {"messages": [message]}})
80

81
    print(result)
82
    ```
83

84
    """
85

86
    def __init__(
1✔
87
        self,
88
        component: Component,
89
        name: Optional[str] = None,
90
        description: Optional[str] = None,
91
        parameters: Optional[Dict[str, Any]] = None,
92
        *,
93
        outputs_to_string: Optional[Dict[str, Union[str, Callable[[Any], str]]]] = None,
94
        inputs_from_state: Optional[Dict[str, str]] = None,
95
        outputs_to_state: Optional[Dict[str, Dict[str, Union[str, Callable]]]] = None,
96
    ):
97
        """
98
        Create a Tool instance from a Haystack component.
99

100
        :param component: The Haystack component to wrap as a tool.
101
        :param name: Optional name for the tool (defaults to snake_case of component class name).
102
        :param description: Optional description (defaults to component's docstring).
103
        :param parameters:
104
            A JSON schema defining the parameters expected by the Tool.
105
            Will fall back to the parameters defined in the component's run method signature if not provided.
106
        :param outputs_to_string:
107
            Optional dictionary defining how a tool outputs should be converted into a string.
108
            If the source is provided only the specified output key is sent to the handler.
109
            If the source is omitted the whole tool result is sent to the handler.
110
            Example:
111
            ```python
112
            {
113
                "source": "docs", "handler": format_documents
114
            }
115
            ```
116
        :param inputs_from_state:
117
            Optional dictionary mapping state keys to tool parameter names.
118
            Example: `{"repository": "repo"}` maps state's "repository" to tool's "repo" parameter.
119
        :param outputs_to_state:
120
            Optional dictionary defining how tool outputs map to keys within state as well as optional handlers.
121
            If the source is provided only the specified output key is sent to the handler.
122
            Example:
123
            ```python
124
            {
125
                "documents": {"source": "docs", "handler": custom_handler}
126
            }
127
            ```
128
            If the source is omitted the whole tool result is sent to the handler.
129
            Example:
130
            ```python
131
            {
132
                "documents": {"handler": custom_handler}
133
            }
134
            ```
135
        :raises ValueError: If the component is invalid or schema generation fails.
136
        """
137
        if not isinstance(component, Component):
1✔
138
            message = (
1✔
139
                f"Object {component!r} is not a Haystack component. "
140
                "Use ComponentTool only with Haystack component instances."
141
            )
142
            raise ValueError(message)
1✔
143

144
        if getattr(component, "__haystack_added_to_pipeline__", None):
1✔
145
            msg = (
1✔
146
                "Component has been added to a pipeline and can't be used to create a ComponentTool. "
147
                "Create ComponentTool from a non-pipeline component instead."
148
            )
149
            raise ValueError(msg)
1✔
150

151
        self._unresolved_parameters = parameters
1✔
152
        # Create the tools schema from the component run method parameters
153
        tool_schema = parameters or self._create_tool_parameters_schema(component, inputs_from_state or {})
1✔
154

155
        def component_invoker(**kwargs):
1✔
156
            """
157
            Invokes the component using keyword arguments provided by the LLM function calling/tool-generated response.
158

159
            :param kwargs: The keyword arguments to invoke the component with.
160
            :returns: The result of the component invocation.
161
            """
162
            converted_kwargs = {}
1✔
163
            input_sockets = component.__haystack_input__._sockets_dict  # type: ignore[attr-defined]
1✔
164
            for param_name, param_value in kwargs.items():
1✔
165
                param_type = input_sockets[param_name].type
1✔
166

167
                # Check if the type (or list element type) has from_dict
168
                target_type = get_args(param_type)[0] if get_origin(param_type) is list else param_type
1✔
169
                if hasattr(target_type, "from_dict"):
1✔
170
                    if isinstance(param_value, list):
1✔
171
                        resolved_param_value = [
1✔
172
                            target_type.from_dict(item) if isinstance(item, dict) else item for item in param_value
173
                        ]
174
                    elif isinstance(param_value, dict):
×
175
                        resolved_param_value = target_type.from_dict(param_value)
×
176
                    else:
177
                        resolved_param_value = param_value
×
178
                else:
179
                    # Let TypeAdapter handle both single values and lists
180
                    type_adapter = TypeAdapter(param_type)
1✔
181
                    resolved_param_value = type_adapter.validate_python(param_value)
1✔
182

183
                converted_kwargs[param_name] = resolved_param_value
1✔
184
            logger.debug(f"Invoking component {type(component)} with kwargs: {converted_kwargs}")
1✔
185
            return component.run(**converted_kwargs)
1✔
186

187
        # Generate a name for the tool if not provided
188
        if not name:
1✔
189
            class_name = component.__class__.__name__
1✔
190
            # Convert camelCase/PascalCase to snake_case
191
            name = "".join(
1✔
192
                [
193
                    "_" + c.lower() if c.isupper() and i > 0 and not class_name[i - 1].isupper() else c.lower()
194
                    for i, c in enumerate(class_name)
195
                ]
196
            ).lstrip("_")
197

198
        description = description or component.__doc__ or name
1✔
199

200
        # Create the Tool instance with the component invoker as the function to be called and the schema
201
        super().__init__(
1✔
202
            name=name,
203
            description=description,
204
            parameters=tool_schema,
205
            function=component_invoker,
206
            inputs_from_state=inputs_from_state,
207
            outputs_to_state=outputs_to_state,
208
            outputs_to_string=outputs_to_string,
209
        )
210
        self._component = component
1✔
211

212
    def to_dict(self) -> Dict[str, Any]:
1✔
213
        """
214
        Serializes the ComponentTool to a dictionary.
215
        """
216
        serialized_component = component_to_dict(obj=self._component, name=self.name)
1✔
217

218
        serialized: Dict[str, Any] = {
1✔
219
            "component": serialized_component,
220
            "name": self.name,
221
            "description": self.description,
222
            "parameters": self._unresolved_parameters,
223
            "inputs_from_state": self.inputs_from_state,
224
            # This is soft-copied as to not modify the attributes in place
225
            "outputs_to_state": self.outputs_to_state.copy() if self.outputs_to_state else None,
226
        }
227

228
        if self.outputs_to_state is not None:
1✔
229
            serialized_outputs = {}
1✔
230
            for key, config in self.outputs_to_state.items():
1✔
231
                serialized_config = config.copy()
1✔
232
                if "handler" in config:
1✔
233
                    serialized_config["handler"] = serialize_callable(config["handler"])
1✔
234
                serialized_outputs[key] = serialized_config
1✔
235
            serialized["outputs_to_state"] = serialized_outputs
1✔
236

237
        if self.outputs_to_string is not None and self.outputs_to_string.get("handler") is not None:
1✔
238
            # This is soft-copied as to not modify the attributes in place
239
            serialized["outputs_to_string"] = self.outputs_to_string.copy()
1✔
240
            serialized["outputs_to_string"]["handler"] = serialize_callable(self.outputs_to_string["handler"])
1✔
241
        else:
242
            serialized["outputs_to_string"] = None
1✔
243

244
        return {"type": generate_qualified_class_name(type(self)), "data": serialized}
1✔
245

246
    @classmethod
1✔
247
    def from_dict(cls, data: Dict[str, Any]) -> "Tool":
1✔
248
        """
249
        Deserializes the ComponentTool from a dictionary.
250
        """
251
        inner_data = data["data"]
1✔
252
        component_class = import_class_by_name(inner_data["component"]["type"])
1✔
253
        component = component_from_dict(cls=component_class, data=inner_data["component"], name=inner_data["name"])
1✔
254

255
        if "outputs_to_state" in inner_data and inner_data["outputs_to_state"]:
1✔
256
            deserialized_outputs = {}
1✔
257
            for key, config in inner_data["outputs_to_state"].items():
1✔
258
                deserialized_config = config.copy()
1✔
259
                if "handler" in config:
1✔
260
                    deserialized_config["handler"] = deserialize_callable(config["handler"])
1✔
261
                deserialized_outputs[key] = deserialized_config
1✔
262
            inner_data["outputs_to_state"] = deserialized_outputs
1✔
263

264
        if (
1✔
265
            inner_data.get("outputs_to_string") is not None
266
            and inner_data["outputs_to_string"].get("handler") is not None
267
        ):
268
            inner_data["outputs_to_string"]["handler"] = deserialize_callable(
1✔
269
                inner_data["outputs_to_string"]["handler"]
270
            )
271

272
        return cls(
1✔
273
            component=component,
274
            name=inner_data["name"],
275
            description=inner_data["description"],
276
            parameters=inner_data.get("parameters", None),
277
            outputs_to_string=inner_data.get("outputs_to_string", None),
278
            inputs_from_state=inner_data.get("inputs_from_state", None),
279
            outputs_to_state=inner_data.get("outputs_to_state", None),
280
        )
281

282
    def _create_tool_parameters_schema(self, component: Component, inputs_from_state: Dict[str, Any]) -> Dict[str, Any]:
1✔
283
        """
284
        Creates an OpenAI tools schema from a component's run method parameters.
285

286
        :param component: The component to create the schema from.
287
        :raises SchemaGenerationError: If schema generation fails
288
        :returns: OpenAI tools schema for the component's run method parameters.
289
        """
290
        component_run_description, param_descriptions = _get_component_param_descriptions(component)
1✔
291

292
        # collect fields (types and defaults) and descriptions from function parameters
293
        fields: Dict[str, Any] = {}
1✔
294

295
        for input_name, socket in component.__haystack_input__._sockets_dict.items():  # type: ignore[attr-defined]
1✔
296
            if inputs_from_state is not None and input_name in inputs_from_state:
1✔
297
                continue
1✔
298
            input_type = socket.type
1✔
299
            description = param_descriptions.get(input_name, f"Input '{input_name}' for the component.")
1✔
300

301
            # if the parameter has not a default value, Pydantic requires an Ellipsis (...)
302
            # to explicitly indicate that the parameter is required
303
            default = ... if socket.is_mandatory else socket.default_value
1✔
304
            resolved_type = _resolve_type(input_type)
1✔
305
            fields[input_name] = (resolved_type, Field(default=default, description=description))
1✔
306

307
        parameters_schema: Dict[str, Any] = {}
1✔
308
        try:
1✔
309
            model = create_model(component.run.__name__, __doc__=component_run_description, **fields)
1✔
310
            parameters_schema = model.model_json_schema()
1✔
311
        except Exception as e:
×
312
            raise SchemaGenerationError(
×
313
                f"Failed to create JSON schema for the run method of Component '{component.__class__.__name__}'"
314
            ) from e
315

316
        # we don't want to include title keywords in the schema, as they contain redundant information
317
        # there is no programmatic way to prevent Pydantic from adding them, so we remove them later
318
        # see https://github.com/pydantic/pydantic/discussions/8504
319
        _remove_title_from_schema(parameters_schema)
1✔
320

321
        return parameters_schema
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc