Coverage for haystack/tools/agent_tool.py: 100%
46 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5import json
6from collections.abc import Callable
7from typing import Any
9from haystack.components.agents import Agent
10from haystack.components.agents.agent import _EXIT_REASON_MAX_STEPS
11from haystack.tools.component_tool import ComponentTool
12from haystack.tools.tool import _deserialize_outputs_to_state, _deserialize_outputs_to_string
13from haystack.utils.deserialization import deserialize_component_inplace
16def _required_tool_parameters(agent: Agent, inputs_from_state: dict[str, Any] | None) -> list[str]:
17 """
18 Return the additional required Tool parameters for the wrapped Agent.
20 These are mandatory Agent inputs that are not supplied through `inputs_from_state`.
21 `messages` is excluded because AgentTool always adds it to the generated schema.
23 :param agent: The wrapped Agent.
24 :param inputs_from_state: Maps the calling Agent's state keys to Agent inputs.
25 :returns: Additional required Tool parameters, sorted by name.
26 """
27 covered = {"messages", *(inputs_from_state or {}).values()}
28 # prompt variables are registered as input sockets in a non-deterministic order, so sort to keep the schema stable
29 return sorted(
30 name
31 # __haystack_input__ is attached to the instance by the @component decorator, so mypy cannot see it
32 for name, socket in agent.__haystack_input__._sockets_dict.items() # type: ignore[attr-defined]
33 if socket.is_mandatory and name not in covered
34 )
37def agent_result_to_string(result: dict[str, Any]) -> str:
38 """Default `outputs_to_string` handler"""
39 last_message = result["last_message"]
40 text = last_message.text or json.dumps(last_message.to_dict())
41 if result["exit_reason"] == _EXIT_REASON_MAX_STEPS:
42 text += "\n\n[The Agent reached max_agent_steps and stopped, so this result may be incomplete.]"
43 return text
46class AgentTool(ComponentTool):
47 """
48 A Tool that wraps a Haystack Agent, allowing it to be used as a tool by another Agent.
50 AgentTool is a building block for multi-agent systems: an Agent specialized in one task becomes a tool that
51 other Agents can delegate to. The calling Agent only sees the final reply, so all the steps the wrapped Agent
52 takes stay out of its context. Sensible defaults make this work out of the box: the task is delegated as a
53 single user message and comes back as text.
55 To use AgentTool, you first need a Haystack Agent. Below is an example of creating an AgentTool from an Agent
56 that searches the web with a SerperDevWebSearch component from the `serperdev-haystack` integration package
57 (`pip install serperdev-haystack`).
59 ## Usage Example:
60 <!-- test-ignore -->
61 ```python
62 from haystack.components.agents import Agent
63 from haystack.components.generators.chat import OpenAIResponsesChatGenerator
64 from haystack.dataclasses import ChatMessage
65 from haystack.tools import AgentTool, ComponentTool
66 from haystack_integrations.components.websearch.serperdev import SerperDevWebSearch
68 researcher = Agent(
69 chat_generator=OpenAIResponsesChatGenerator(model="gpt-5.4-mini"),
70 system_prompt="You are a research specialist. Investigate the task and report your findings.",
71 tools=[
72 ComponentTool(
73 component=SerperDevWebSearch(
74 top_k=3,
75 ),
76 name="web_search",
77 description="Search the web for current information on any topic",
78 ),
79 ],
80 )
82 research = AgentTool(
83 agent=researcher,
84 name="research",
85 description="Research a question on the web and report the findings",
86 )
88 coordinator = Agent(
89 chat_generator=OpenAIResponsesChatGenerator(model="gpt-5.4"),
90 tools=[research],
91 system_prompt="You coordinate specialists. Delegate research questions, then answer the user.",
92 )
94 result = coordinator.run([ChatMessage.from_user("What are the latest developments in the Haystack framework?")])
95 print(result["last_message"].text)
96 ```
97 """
99 def __init__(
100 self,
101 agent: Agent,
102 *,
103 name: str,
104 description: str,
105 parameters: dict[str, Any] | None = None,
106 outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
107 inputs_from_state: dict[str, str] | None = None,
108 outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
109 ) -> None:
110 """
111 Create a Tool instance from a Haystack Agent.
113 :param agent: The Haystack Agent to wrap as a tool.
114 :param name: Name of the tool.
115 :param description: Description of the tool. It should tell the calling LLM what the Agent is specialized in
116 and when to delegate to it.
117 :param parameters:
118 A JSON schema defining the parameters expected by the Tool.
119 Will fall back to a schema with the task to delegate as a single user message, plus one string parameter
120 for every other mandatory input of the Agent, if not provided.
121 :param outputs_to_string:
122 Optional dictionary defining how tool outputs should be converted into string(s) or results.
123 If not provided, the tool result is the text of the Agent's final reply, or the serialized message if
124 the reply has no text. A warning is appended if the Agent stopped because it reached `max_agent_steps`.
126 `outputs_to_string` supports two formats:
128 1. Single output format - use "source", "handler", and/or "raw_result" at the root level:
129 ```python
130 {
131 "source": "last_message", "handler": format_reply, "raw_result": False
132 }
133 ```
134 - `source`: If provided, only the specified output key is sent to the handler.
135 - `handler`: A function that takes the tool output (or the extracted source value) and returns the
136 final result.
137 - `raw_result`: If `True`, the result is returned raw without string conversion, but applying the
138 `handler` if provided. This is intended for tools that return images. In this mode, the `handler`
139 is required, since the Agent returns a dictionary, and it must return a list of
140 `TextContent`/`ImageContent` objects to ensure compatibility with Chat Generators.
142 2. Multiple output format - map keys to individual configurations:
143 ```python
144 {
145 "reply": {"source": "last_message", "handler": format_reply},
146 "steps": {"source": "step_count", "handler": str}
147 }
148 ```
149 Each key maps to a dictionary that can contain "source" and/or "handler".
150 Note that `raw_result` is not supported in the multiple output format.
151 :param inputs_from_state:
152 Optional dictionary mapping the calling Agent's state keys to Agent input names.
153 Example: `{"subject": "topic"}` maps state's "subject" to the Agent's "topic" input.
154 Inputs mapped this way are not added to the generated `parameters` schema, since the calling Agent
155 provides them.
156 :param outputs_to_state:
157 Optional dictionary defining how tool outputs map to keys within state as well as optional handlers.
158 The keys must be declared in the `state_schema` of the calling Agent.
159 Handlers merge the tool output into the state and are called as `handler(current_value, tool_output)`.
160 If the source is provided only the specified output key is sent to the handler.
161 Example:
162 ```python
163 {
164 "notes": {"source": "last_message", "handler": custom_handler}
165 }
166 ```
167 If the source is omitted the whole tool result is sent to the handler.
168 Example:
169 ```python
170 {
171 "notes": {"handler": custom_handler}
172 }
173 ```
174 :raises TypeError: If the object passed is not a Haystack Agent instance.
175 :raises ValueError: If `parameters` is provided but does not cover all the mandatory inputs of the Agent.
176 """
177 if not isinstance(agent, Agent):
178 raise TypeError(f"The 'agent' parameter must be an instance of Agent. Got {type(agent)} instead.")
180 if parameters is not None:
181 required_tool_parameters = _required_tool_parameters(agent=agent, inputs_from_state=inputs_from_state)
182 missing_required_parameters = [
183 name for name in required_tool_parameters if name not in parameters.get("properties", {})
184 ]
185 if missing_required_parameters:
186 raise ValueError(
187 f"The Agent wrapped by this tool requires the inputs {missing_required_parameters}, but this tool "
188 f"does not supply them, so it can never run. Add them to 'parameters', the schema that the calling "
189 f"LLM fills in, or to 'inputs_from_state', which takes them from the calling Agent's state."
190 )
192 super().__init__(
193 component=agent,
194 name=name,
195 description=description,
196 parameters=parameters,
197 outputs_to_string=outputs_to_string or {"handler": agent_result_to_string},
198 inputs_from_state=inputs_from_state,
199 outputs_to_state=outputs_to_state,
200 )
202 def _create_tool_parameters_schema(self, component: Any, inputs_from_state: dict[str, Any]) -> dict[str, Any]:
203 """
204 Override ComponentTool schema generation for AgentTool defaults.
206 ComponentTool calls this when users do not provide an explicit `parameters` schema. The generated schema always
207 includes `messages` for the delegated task, plus one string parameter for each mandatory Agent input not
208 supplied through `inputs_from_state`.
209 """
210 additional_required_parameters = _required_tool_parameters(agent=component, inputs_from_state=inputs_from_state)
211 additional_properties = {name: {"type": "string"} for name in additional_required_parameters}
212 return {
213 "type": "object",
214 "properties": {
215 "messages": {
216 "type": "array",
217 "description": "Exactly one user message.",
218 "minItems": 1,
219 "maxItems": 1,
220 "items": {
221 "type": "object",
222 "properties": {
223 "role": {"type": "string", "enum": ["user"]},
224 "content": {"type": "string", "description": "The task to delegate to this tool."},
225 },
226 "required": ["role", "content"],
227 },
228 },
229 **additional_properties,
230 },
231 "required": ["messages", *additional_required_parameters],
232 }
234 def to_dict(self) -> dict[str, Any]:
235 """
236 Serializes the AgentTool to a dictionary.
238 :returns:
239 The serialized dictionary representation of AgentTool.
240 """
241 serialized = super().to_dict()
242 serialized["data"]["agent"] = serialized["data"].pop("component")
243 return serialized
245 @classmethod
246 def from_dict(cls, data: dict[str, Any]) -> "AgentTool":
247 """
248 Deserializes the AgentTool from a dictionary.
250 :param data: The dictionary representation of AgentTool.
251 :returns:
252 The deserialized AgentTool instance.
253 """
254 inner_data = data["data"]
255 deserialize_component_inplace(data=inner_data, key="agent")
257 outputs_to_state = inner_data.get("outputs_to_state")
258 if outputs_to_state:
259 outputs_to_state = _deserialize_outputs_to_state(outputs_to_state=outputs_to_state)
261 outputs_to_string = inner_data.get("outputs_to_string")
262 if outputs_to_string is not None:
263 outputs_to_string = _deserialize_outputs_to_string(outputs_to_string=outputs_to_string)
265 return cls(
266 agent=inner_data["agent"],
267 name=inner_data["name"],
268 description=inner_data["description"],
269 parameters=inner_data.get("parameters"),
270 outputs_to_string=outputs_to_string,
271 inputs_from_state=inner_data.get("inputs_from_state"),
272 outputs_to_state=outputs_to_state,
273 )