Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 33 additions & 36 deletions haystack/tools/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,20 +324,41 @@ def _check_duplicate_tool_names(tools: list[Tool] | None) -> None:
raise ValueError(f"Duplicate tool names found: {duplicate_tool_names}")


def _convert_handler(config: dict[str, Any], converter: Callable[[Any], Any]) -> dict[str, Any]:
"""
Copies a single output config, converting its "handler" entry (if present) via `converter`.

:param config: A single output configuration dictionary that may contain a "handler" key.
:param converter: `serialize_callable` or `deserialize_callable`, applied to the "handler" value.
:returns: A copy of `config` with the "handler" value converted, if present.
"""
new_config = config.copy()
if "handler" in config:
new_config["handler"] = converter(config["handler"])
return new_config


def _convert_handler_in_configs(
configs: dict[str, dict[str, Any]], converter: Callable[[Any], Any]
) -> dict[str, dict[str, Any]]:
"""
Applies `_convert_handler` to every config in a dictionary of named output configs.

:param configs: A mapping of keys to output configuration dictionaries.
:param converter: `serialize_callable` or `deserialize_callable`, applied to each "handler" value.
:returns: A new mapping with the same keys, each config converted via `_convert_handler`.
"""
return {key: _convert_handler(config, converter) for key, config in configs.items()}


def _serialize_outputs_to_state(outputs_to_state: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""
Serializes the outputs_to_state dictionary, converting any callable handlers to their string representation.

:param outputs_to_state: The outputs_to_state dictionary to serialize.
:returns: The serialized outputs_to_state dictionary.
"""
serialized_outputs = {}
for key, config in outputs_to_state.items():
serialized_config = config.copy()
if "handler" in config:
serialized_config["handler"] = serialize_callable(config["handler"])
serialized_outputs[key] = serialized_config
return serialized_outputs
return _convert_handler_in_configs(outputs_to_state, serialize_callable)


def _deserialize_outputs_to_state(outputs_to_state: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
Expand All @@ -347,13 +368,7 @@ def _deserialize_outputs_to_state(outputs_to_state: dict[str, dict[str, Any]]) -
:param outputs_to_state: The outputs_to_state dictionary to deserialize.
:returns: The deserialized outputs_to_state dictionary.
"""
deserialized_outputs = {}
for key, config in outputs_to_state.items():
deserialized_config = config.copy()
if "handler" in config:
deserialized_config["handler"] = deserialize_callable(config["handler"])
deserialized_outputs[key] = deserialized_config
return deserialized_outputs
return _convert_handler_in_configs(outputs_to_state, deserialize_callable)


def _serialize_outputs_to_string(outputs_to_string: dict[str, Any]) -> dict[str, Any]:
Expand All @@ -365,19 +380,10 @@ def _serialize_outputs_to_string(outputs_to_string: dict[str, Any]) -> dict[str,
"""
if "source" in outputs_to_string or "handler" in outputs_to_string or "raw_result" in outputs_to_string:
# Single output configuration
serialized_outputs = outputs_to_string.copy()
if "handler" in outputs_to_string:
serialized_outputs["handler"] = serialize_callable(outputs_to_string["handler"])
return serialized_outputs
return _convert_handler(outputs_to_string, serialize_callable)

# Multiple outputs configuration
serialized_outputs = {}
for key, config in outputs_to_string.items():
serialized_config = config.copy()
if "handler" in config:
serialized_config["handler"] = serialize_callable(config["handler"])
serialized_outputs[key] = serialized_config
return serialized_outputs
return _convert_handler_in_configs(outputs_to_string, serialize_callable)


def _deserialize_outputs_to_string(outputs_to_string: dict[str, Any]) -> dict[str, Any]:
Expand All @@ -389,16 +395,7 @@ def _deserialize_outputs_to_string(outputs_to_string: dict[str, Any]) -> dict[st
"""
if "source" in outputs_to_string or "handler" in outputs_to_string or "raw_result" in outputs_to_string:
# Single output configuration
deserialized_outputs = outputs_to_string.copy()
if "handler" in outputs_to_string:
deserialized_outputs["handler"] = deserialize_callable(outputs_to_string["handler"])
return deserialized_outputs
return _convert_handler(outputs_to_string, deserialize_callable)

# Multiple outputs configuration
deserialized_outputs = {}
for key, config in outputs_to_string.items():
deserialized_config = config.copy()
if "handler" in config:
deserialized_config["handler"] = deserialize_callable(config["handler"])
deserialized_outputs[key] = deserialized_config
return deserialized_outputs
return _convert_handler_in_configs(outputs_to_string, deserialize_callable)
34 changes: 33 additions & 1 deletion test/tools/test_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
# SPDX-License-Identifier: Apache-2.0

import re
from typing import Any

import pytest

from haystack.dataclasses import TextContent
from haystack.tools import Tool, _check_duplicate_tool_names
from haystack.tools.errors import ToolInvocationError
from haystack.tools.tool import _deserialize_outputs_to_string, _serialize_outputs_to_string
from haystack.tools.tool import (
_deserialize_outputs_to_state,
_deserialize_outputs_to_string,
_serialize_outputs_to_state,
_serialize_outputs_to_string,
)


def get_weather_report(city: str) -> str:
Expand Down Expand Up @@ -234,6 +240,32 @@ def test_deserialize_outputs_to_string(self):
"temp": {"source": "temperature", "handler": format_string},
}

def test_serialize_outputs_to_state(self):
config: dict[str, dict[str, Any]] = {
"documents": {"source": "docs", "handler": format_string},
"summary": {"source": "docs", "handler": get_weather_report},
"raw_docs": {"source": "docs"},
}
serialized = _serialize_outputs_to_state(config)
assert serialized == {
"documents": {"source": "docs", "handler": "test_tool.format_string"},
"summary": {"source": "docs", "handler": "test_tool.get_weather_report"},
"raw_docs": {"source": "docs"},
}

def test_deserialize_outputs_to_state(self):
serialized = {
"documents": {"source": "docs", "handler": "test_tool.format_string"},
"summary": {"source": "docs", "handler": "test_tool.get_weather_report"},
"raw_docs": {"source": "docs"},
}
deserialized = _deserialize_outputs_to_state(serialized)
assert deserialized == {
"documents": {"source": "docs", "handler": format_string},
"summary": {"source": "docs", "handler": get_weather_report},
"raw_docs": {"source": "docs"},
}

def test_inputs_from_state_validation_with_invalid_parameter(self):
"""Test that inputs_from_state is validated against the parameters schema"""
with pytest.raises(
Expand Down
Loading