Skip to content

Commit 1ebfe85

Browse files
authored
fix: Jinja2-based ComponentTools in pipelines properly handle deepcopy (#234)
1 parent 8fb22ee commit 1ebfe85

File tree

2 files changed

+47
-0
lines changed

2 files changed

+47
-0
lines changed

haystack_experimental/tools/component_tool.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5+
from copy import copy, deepcopy
56
from dataclasses import fields, is_dataclass
67
from inspect import getdoc
78
from typing import Any, Callable, Dict, Optional, Union, get_args, get_origin
@@ -392,3 +393,29 @@ def _create_property_schema(self, python_type: Any, description: str, default: A
392393
schema["default"] = default
393394

394395
return schema
396+
397+
def __deepcopy__(self, memo: Dict[Any, Any]) -> "ComponentTool":
398+
# Jinja2 templates throw an Exception when we deepcopy them (see https://github.com/pallets/jinja/issues/758)
399+
# When we use a ComponentTool in a pipeline at runtime, we deepcopy the tool
400+
# We overwrite ComponentTool.__deepcopy__ to fix this in experimental until a more comprehensive fix is merged.
401+
# We track the issue here: https://github.com/deepset-ai/haystack/issues/9011
402+
result = copy(self)
403+
404+
# Add the object to the memo dictionary to handle circular references
405+
memo[id(self)] = result
406+
407+
# Deep copy all attributes with exception handling
408+
for key, value in self.__dict__.items():
409+
try:
410+
# Try to deep copy the attribute
411+
setattr(result, key, deepcopy(value, memo))
412+
except TypeError:
413+
# Fall back to using the original attribute for components that use Jinja2-templates
414+
logger.debug(
415+
"deepcopy of ComponentTool {tool_name} failed. Using original attribute '{attribute}' instead.",
416+
tool_name=self.name,
417+
attribute=key,
418+
)
419+
setattr(result, key, getattr(self, key))
420+
421+
return result

test/tools/test_component_tool.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
import json
66
import os
7+
from copy import deepcopy
78
from dataclasses import dataclass
89
from typing import Dict, List
910

1011
import pytest
1112

1213
from haystack import Pipeline, component
14+
from haystack.components.builders import PromptBuilder
1315
from haystack.components.generators.chat import OpenAIChatGenerator
1416
from haystack.components.websearch.serper_dev import SerperDevWebSearch
1517
from haystack.dataclasses import ChatMessage, ChatRole, Document
@@ -609,3 +611,21 @@ def test_pipeline_component_fails(self):
609611
# thus can't be used as tool
610612
with pytest.raises(ValueError, match="Component has been added to a pipeline"):
611613
ComponentTool(component=component)
614+
615+
def test_deepcopy_with_jinja_based_component(self):
616+
# Jinja2 templates throw an Exception when we deepcopy them (see https://github.com/pallets/jinja/issues/758)
617+
# When we use a ComponentTool in a pipeline at runtime, we deepcopy the tool
618+
# We overwrite ComponentTool.__deepcopy__ to fix this in experimental until a more comprehensive fix is merged.
619+
# We track the issue here: https://github.com/deepset-ai/haystack/issues/9011
620+
621+
builder = PromptBuilder("{{query}}")
622+
623+
tool = ComponentTool(component=builder)
624+
result = tool.function(query="Hello")
625+
626+
tool_copy = deepcopy(tool)
627+
628+
result_from_copy = tool_copy.function(query="Hello")
629+
630+
assert "prompt" in result_from_copy
631+
assert result_from_copy["prompt"] == result["prompt"]

0 commit comments

Comments
 (0)