Skip to content

fix: Fix invoker to work when using dataclass with from_dict but dataclass… #9434

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 26, 2025
Merged
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
12 changes: 8 additions & 4 deletions haystack/tools/component_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,19 @@ def component_invoker(**kwargs):
target_type = get_args(param_type)[0] if get_origin(param_type) is list else param_type
if hasattr(target_type, "from_dict"):
if isinstance(param_value, list):
param_value = [target_type.from_dict(item) for item in param_value if isinstance(item, dict)]
resolved_param_value = [
target_type.from_dict(item) if isinstance(item, dict) else item for item in param_value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sjrl - just one clarification - so the only difference to previous code state is that we now include other items in the list - that are not dicts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we keep other items in the list (or really all items in a list) that aren't dicts. If we don't do this and a user passes this

agent_tool.invoke(messages=[ChatMessage.from_user("Tell me the latest news about gpus")])

we actually run the agent_tool with

messages=[ChatMessage.from_user("Tell me the latest news about gpus")]
# turns into 
messages=[]
# then we run Agent Tool with an empty list of messages

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is relevant in two cases:

  • If a user is trying to use a tool manually with invoke or within a custom component
  • If we pass inputs_from_state to a Tool which are already fully formed dataclasses. E.g. If I pass the messages from State into the input of a Tool that accepts ChatMessages. I wouldn't expect the invoke function to then remove all the messages.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now it makes total sense 🙏

]
elif isinstance(param_value, dict):
param_value = target_type.from_dict(param_value)
resolved_param_value = target_type.from_dict(param_value)
else:
resolved_param_value = param_value
else:
# Let TypeAdapter handle both single values and lists
type_adapter = TypeAdapter(param_type)
param_value = type_adapter.validate_python(param_value)
resolved_param_value = type_adapter.validate_python(param_value)

converted_kwargs[param_name] = param_value
converted_kwargs[param_name] = resolved_param_value
logger.debug(f"Invoking component {type(component)} with kwargs: {converted_kwargs}")
return component.run(**converted_kwargs)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
fixes:
- |
Fix component_invoker used by ComponentTool to work when a dataclass like ChatMessage is directly passed to `component_tool.invoke(...)`.
Previously this would either cause an error or silently skip your input.
22 changes: 22 additions & 0 deletions test/tools/test_component_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@
# Component and Model Definitions


@component
class SimpleComponentUsingChatMessages:
"""A simple component that generates text."""

@component.output_types(reply=str)
def run(self, messages: List[ChatMessage]) -> Dict[str, str]:
"""
A simple component that generates text.

:param messages: Users messages
:return: A dictionary with the generated text.
"""
return {"reply": f"Hello, {messages[0].text}!"}


@component
class SimpleComponent:
"""A simple component that generates text."""
Expand Down Expand Up @@ -306,6 +321,13 @@ def foo(self, text: str):
with pytest.raises(ValueError):
ComponentTool(component=not_a_component, name="invalid_tool", description="This should fail")

def test_component_invoker_with_chat_message_input(self):
tool = ComponentTool(
component=SimpleComponentUsingChatMessages(), name="simple_tool", description="A simple tool"
)
result = tool.invoke(messages=[ChatMessage.from_user(text="world")])
assert result == {"reply": "Hello, world!"}


# Integration tests
class TestToolComponentInPipelineWithOpenAI:
Expand Down
Loading