Skip to content

Commit 7704951

Browse files
committed
Merge branch 'slow-workflow' of https://github.com/deepset-ai/haystack into slow-workflow
2 parents 8a1b5bc + c620c1c commit 7704951

File tree

9 files changed

+219
-119
lines changed

9 files changed

+219
-119
lines changed

haystack/components/agents/agent.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import inspect
66
from copy import deepcopy
7-
from typing import Any, Dict, List, Optional
7+
from typing import Any, Dict, List, Optional, Union
88

99
from haystack import component, default_from_dict, default_to_dict, logging, tracing
1010
from haystack.components.generators.chat.types import ChatGenerator
@@ -16,7 +16,7 @@
1616
from haystack.dataclasses.state import State, _schema_from_dict, _schema_to_dict, _validate_schema
1717
from haystack.dataclasses.state_utils import merge_lists
1818
from haystack.dataclasses.streaming_chunk import StreamingCallbackT
19-
from haystack.tools import Tool, deserialize_tools_or_toolset_inplace
19+
from haystack.tools import Tool, Toolset, deserialize_tools_or_toolset_inplace, serialize_tools_or_toolset
2020
from haystack.utils.callable_serialization import deserialize_callable, serialize_callable
2121
from haystack.utils.deserialization import deserialize_chatgenerator_inplace
2222

@@ -61,7 +61,7 @@ def __init__(
6161
self,
6262
*,
6363
chat_generator: ChatGenerator,
64-
tools: Optional[List[Tool]] = None,
64+
tools: Optional[Union[List[Tool], Toolset]] = None,
6565
system_prompt: Optional[str] = None,
6666
exit_conditions: Optional[List[str]] = None,
6767
state_schema: Optional[Dict[str, Any]] = None,
@@ -73,7 +73,7 @@ def __init__(
7373
Initialize the agent component.
7474
7575
:param chat_generator: An instance of the chat generator that your agent should use. It must support tools.
76-
:param tools: List of Tool objects available to the agent
76+
:param tools: List of Tool objects or a Toolset that the agent can use.
7777
:param system_prompt: System prompt for the agent.
7878
:param exit_conditions: List of conditions that will cause the agent to return.
7979
Can include "text" if the agent should return when it generates a message without tool calls,
@@ -166,7 +166,7 @@ def to_dict(self) -> Dict[str, Any]:
166166
return default_to_dict(
167167
self,
168168
chat_generator=component_to_dict(obj=self.chat_generator, name="chat_generator"),
169-
tools=[t.to_dict() for t in self.tools],
169+
tools=serialize_tools_or_toolset(self.tools),
170170
system_prompt=self.system_prompt,
171171
exit_conditions=self.exit_conditions,
172172
# We serialize the original state schema, not the resolved one to reflect the original user input

haystack/components/connectors/openapi.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class OpenAPIConnector:
3535
)
3636
response = connector.run(
3737
operation_id="search",
38-
parameters={"q": "Who was Nikola Tesla?"}
38+
arguments={"q": "Who was Nikola Tesla?"}
3939
)
4040
```
4141
Note:
@@ -91,7 +91,7 @@ def run(self, operation_id: str, arguments: Optional[Dict[str, Any]] = None) ->
9191
Invokes a REST endpoint specified in the OpenAPI specification.
9292
9393
:param operation_id: The operationId from the OpenAPI spec to invoke
94-
:param parameters: Optional parameters for the endpoint (query, path, or body parameters)
94+
:param arguments: Optional parameters for the endpoint (query, path, or body parameters)
9595
:return: Dictionary containing the service response
9696
"""
9797
payload = {"name": operation_id, "arguments": arguments or {}}

haystack/components/tools/tool_invoker.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,17 @@ def __init__(
184184

185185
# Convert Toolset to list for internal use
186186
if isinstance(tools, Toolset):
187-
tools = list(tools)
187+
converted_tools = list(tools)
188+
else:
189+
converted_tools = tools
188190

189-
_check_duplicate_tool_names(tools)
190-
tool_names = [tool.name for tool in tools]
191+
_check_duplicate_tool_names(converted_tools)
192+
tool_names = [tool.name for tool in converted_tools]
191193
duplicates = {name for name in tool_names if tool_names.count(name) > 1}
192194
if duplicates:
193195
raise ValueError(f"Duplicate tool names found: {duplicates}")
194196

195-
self._tools_with_names = dict(zip(tool_names, tools))
197+
self._tools_with_names = dict(zip(tool_names, converted_tools))
196198
self.raise_on_failure = raise_on_failure
197199
self.convert_result_to_json_string = convert_result_to_json_string
198200

haystack/tracing/logging_tracer.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from haystack import logging
1010
from haystack.tracing import Span, Tracer
11+
from haystack.tracing.utils import coerce_tag_value
1112

1213
logger = logging.getLogger(__name__)
1314

@@ -75,8 +76,9 @@ def trace(
7576
logger.debug("Operation: {operation_name}", operation_name=operation_name)
7677
for tag_name, tag_value in tags.items():
7778
color_string = self.tags_color_strings.get(tag_name, "")
79+
coerced_value = coerce_tag_value(tag_value)
7880
logger.debug(
79-
color_string + "{tag_name}={tag_value}" + RESET_COLOR, tag_name=tag_name, tag_value=tag_value
81+
color_string + "{tag_name}={tag_value}" + RESET_COLOR, tag_name=tag_name, tag_value=coerced_value
8082
)
8183

8284
def current_span(self) -> Optional[Span]:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
features:
3+
- |
4+
Agent now supports a List of Tools or a Toolset as input.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
use coerce_tag_value in LoggingTracer to serialize tag values

test/components/agents/test_agent.py

+137-102
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,62 @@ def test_to_dict(self, weather_tool, component_tool, monkeypatch):
239239
},
240240
}
241241

242+
def test_to_dict_with_toolset(self, monkeypatch, weather_tool):
243+
monkeypatch.setenv("OPENAI_API_KEY", "fake-key")
244+
toolset = Toolset(tools=[weather_tool])
245+
agent = Agent(chat_generator=OpenAIChatGenerator(), tools=toolset)
246+
serialized_agent = agent.to_dict()
247+
assert serialized_agent == {
248+
"type": "haystack.components.agents.agent.Agent",
249+
"init_parameters": {
250+
"chat_generator": {
251+
"type": "haystack.components.generators.chat.openai.OpenAIChatGenerator",
252+
"init_parameters": {
253+
"model": "gpt-4o-mini",
254+
"streaming_callback": None,
255+
"api_base_url": None,
256+
"organization": None,
257+
"generation_kwargs": {},
258+
"api_key": {"type": "env_var", "env_vars": ["OPENAI_API_KEY"], "strict": True},
259+
"timeout": None,
260+
"max_retries": None,
261+
"tools": None,
262+
"tools_strict": False,
263+
"http_client_kwargs": None,
264+
},
265+
},
266+
"tools": {
267+
"type": "haystack.tools.toolset.Toolset",
268+
"data": {
269+
"tools": [
270+
{
271+
"type": "haystack.tools.tool.Tool",
272+
"data": {
273+
"name": "weather_tool",
274+
"description": "Provides weather information for a given location.",
275+
"parameters": {
276+
"type": "object",
277+
"properties": {"location": {"type": "string"}},
278+
"required": ["location"],
279+
},
280+
"function": "test_agent.weather_function",
281+
"outputs_to_string": None,
282+
"inputs_from_state": None,
283+
"outputs_to_state": None,
284+
},
285+
}
286+
]
287+
},
288+
},
289+
"system_prompt": None,
290+
"exit_conditions": ["text"],
291+
"state_schema": {},
292+
"max_agent_steps": 100,
293+
"raise_on_tool_invocation_failure": False,
294+
"streaming_callback": None,
295+
},
296+
}
297+
242298
def test_from_dict(self, weather_tool, component_tool, monkeypatch):
243299
monkeypatch.setenv("OPENAI_API_KEY", "fake-key")
244300
data = {
@@ -318,6 +374,67 @@ def test_from_dict(self, weather_tool, component_tool, monkeypatch):
318374
"messages": {"handler": merge_lists, "type": List[ChatMessage]},
319375
}
320376

377+
def test_from_dict_with_toolset(self, monkeypatch):
378+
monkeypatch.setenv("OPENAI_API_KEY", "fake-key")
379+
data = {
380+
"type": "haystack.components.agents.agent.Agent",
381+
"init_parameters": {
382+
"chat_generator": {
383+
"type": "haystack.components.generators.chat.openai.OpenAIChatGenerator",
384+
"init_parameters": {
385+
"model": "gpt-4o-mini",
386+
"streaming_callback": None,
387+
"api_base_url": None,
388+
"organization": None,
389+
"generation_kwargs": {},
390+
"api_key": {"type": "env_var", "env_vars": ["OPENAI_API_KEY"], "strict": True},
391+
"timeout": None,
392+
"max_retries": None,
393+
"tools": None,
394+
"tools_strict": False,
395+
"http_client_kwargs": None,
396+
},
397+
},
398+
"tools": {
399+
"type": "haystack.tools.toolset.Toolset",
400+
"data": {
401+
"tools": [
402+
{
403+
"type": "haystack.tools.tool.Tool",
404+
"data": {
405+
"name": "weather_tool",
406+
"description": "Provides weather information for a given location.",
407+
"parameters": {
408+
"type": "object",
409+
"properties": {"location": {"type": "string"}},
410+
"required": ["location"],
411+
},
412+
"function": "test_agent.weather_function",
413+
"outputs_to_string": None,
414+
"inputs_from_state": None,
415+
"outputs_to_state": None,
416+
},
417+
}
418+
]
419+
},
420+
},
421+
"system_prompt": None,
422+
"exit_conditions": ["text"],
423+
"state_schema": {},
424+
"max_agent_steps": 100,
425+
"raise_on_tool_invocation_failure": False,
426+
"streaming_callback": None,
427+
},
428+
}
429+
agent = Agent.from_dict(data)
430+
assert isinstance(agent, Agent)
431+
assert isinstance(agent.chat_generator, OpenAIChatGenerator)
432+
assert agent.chat_generator.model == "gpt-4o-mini"
433+
assert agent.chat_generator.api_key == Secret.from_env_var("OPENAI_API_KEY")
434+
assert isinstance(agent.tools, Toolset)
435+
assert agent.tools[0].function is weather_function
436+
assert agent.exit_conditions == ["text"]
437+
321438
def test_serde(self, weather_tool, component_tool, monkeypatch):
322439
monkeypatch.setenv("FAKE_OPENAI_KEY", "fake-key")
323440
generator = OpenAIChatGenerator(api_key=Secret.from_env_var("FAKE_OPENAI_KEY"))
@@ -700,59 +817,18 @@ def test_agent_tracing_span_run(self, caplog, monkeypatch, weather_tool):
700817
expected_tag_values = [
701818
"chat_generator",
702819
"MockChatGeneratorWithoutRunAsync",
703-
{"messages": "list", "tools": "list"},
704-
{},
705-
{},
706-
{
707-
"messages": [ChatMessage.from_user(text="What's the weather in Paris?")],
708-
"tools": [
709-
Tool(
710-
name="weather_tool",
711-
description="Provides weather information for a given location.",
712-
parameters={
713-
"type": "object",
714-
"properties": {"location": {"type": "string"}},
715-
"required": ["location"],
716-
},
717-
function=weather_function,
718-
outputs_to_string=None,
719-
inputs_from_state=None,
720-
outputs_to_state=None,
721-
)
722-
],
723-
},
820+
'{"messages": "list", "tools": "list"}',
821+
"{}",
822+
"{}",
823+
'{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}], "tools": [{"type": "haystack.tools.tool.Tool", "data": {"name": "weather_tool", "description": "Provides weather information for a given location.", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}, "function": "test_agent.weather_function", "outputs_to_string": null, "inputs_from_state": null, "outputs_to_state": null}}]}',
724824
1,
725-
{"replies": [ChatMessage.from_assistant(text="Hello")]},
825+
'{"replies": [{"role": "assistant", "meta": {}, "name": null, "content": [{"text": "Hello"}]}]}',
726826
100,
727-
[
728-
Tool(
729-
name="weather_tool",
730-
description="Provides weather information for a given location.",
731-
parameters={
732-
"type": "object",
733-
"properties": {"location": {"type": "string"}},
734-
"required": ["location"],
735-
},
736-
function=weather_function,
737-
outputs_to_string=None,
738-
inputs_from_state=None,
739-
outputs_to_state=None,
740-
)
741-
],
742-
["text"],
743-
{
744-
"messages": {
745-
"type": "typing.List[haystack.dataclasses.chat_message.ChatMessage]",
746-
"handler": "haystack.dataclasses.state_utils.merge_lists",
747-
}
748-
},
749-
{"messages": [ChatMessage.from_user(text="What's the weather in Paris?")], "streaming_callback": None},
750-
{
751-
"messages": [
752-
ChatMessage.from_user(text="What's the weather in Paris?"),
753-
ChatMessage.from_assistant(text="Hello"),
754-
]
755-
},
827+
'[{"type": "haystack.tools.tool.Tool", "data": {"name": "weather_tool", "description": "Provides weather information for a given location.", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}, "function": "test_agent.weather_function", "outputs_to_string": null, "inputs_from_state": null, "outputs_to_state": null}}]',
828+
'["text"]',
829+
'{"messages": {"type": "typing.List[haystack.dataclasses.chat_message.ChatMessage]", "handler": "haystack.dataclasses.state_utils.merge_lists"}}',
830+
'{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}], "streaming_callback": null}',
831+
'{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}, {"role": "assistant", "meta": {}, "name": null, "content": [{"text": "Hello"}]}]}',
756832
1,
757833
]
758834
for idx, record in enumerate(tags_records):
@@ -801,59 +877,18 @@ async def test_agent_tracing_span_async_run(self, caplog, monkeypatch, weather_t
801877
expected_tag_values = [
802878
"chat_generator",
803879
"MockChatGeneratorWithRunAsync",
804-
{"messages": "list", "tools": "list"},
805-
{},
806-
{},
807-
{
808-
"messages": [ChatMessage.from_user(text="What's the weather in Paris?")],
809-
"tools": [
810-
Tool(
811-
name="weather_tool",
812-
description="Provides weather information for a given location.",
813-
parameters={
814-
"type": "object",
815-
"properties": {"location": {"type": "string"}},
816-
"required": ["location"],
817-
},
818-
function=weather_function,
819-
outputs_to_string=None,
820-
inputs_from_state=None,
821-
outputs_to_state=None,
822-
)
823-
],
824-
},
880+
'{"messages": "list", "tools": "list"}',
881+
"{}",
882+
"{}",
883+
'{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}], "tools": [{"type": "haystack.tools.tool.Tool", "data": {"name": "weather_tool", "description": "Provides weather information for a given location.", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}, "function": "test_agent.weather_function", "outputs_to_string": null, "inputs_from_state": null, "outputs_to_state": null}}]}',
825884
1,
826-
{"replies": [ChatMessage.from_assistant(text="Hello from run_async")]},
885+
'{"replies": [{"role": "assistant", "meta": {}, "name": null, "content": [{"text": "Hello from run_async"}]}]}',
827886
100,
828-
[
829-
Tool(
830-
name="weather_tool",
831-
description="Provides weather information for a given location.",
832-
parameters={
833-
"type": "object",
834-
"properties": {"location": {"type": "string"}},
835-
"required": ["location"],
836-
},
837-
function=weather_function,
838-
outputs_to_string=None,
839-
inputs_from_state=None,
840-
outputs_to_state=None,
841-
)
842-
],
843-
["text"],
844-
{
845-
"messages": {
846-
"type": "typing.List[haystack.dataclasses.chat_message.ChatMessage]",
847-
"handler": "haystack.dataclasses.state_utils.merge_lists",
848-
}
849-
},
850-
{"messages": [ChatMessage.from_user(text="What's the weather in Paris?")], "streaming_callback": None},
851-
{
852-
"messages": [
853-
ChatMessage.from_user(text="What's the weather in Paris?"),
854-
ChatMessage.from_assistant(text="Hello from run_async"),
855-
]
856-
},
887+
'[{"type": "haystack.tools.tool.Tool", "data": {"name": "weather_tool", "description": "Provides weather information for a given location.", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}, "function": "test_agent.weather_function", "outputs_to_string": null, "inputs_from_state": null, "outputs_to_state": null}}]',
888+
'["text"]',
889+
'{"messages": {"type": "typing.List[haystack.dataclasses.chat_message.ChatMessage]", "handler": "haystack.dataclasses.state_utils.merge_lists"}}',
890+
'{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}], "streaming_callback": null}',
891+
'{"messages": [{"role": "user", "meta": {}, "name": null, "content": [{"text": "What\'s the weather in Paris?"}]}, {"role": "assistant", "meta": {}, "name": null, "content": [{"text": "Hello from run_async"}]}]}',
857892
1,
858893
]
859894
for idx, record in enumerate(tags_records):

0 commit comments

Comments
 (0)