diff --git a/haystack_experimental/dataclasses/tool.py b/haystack_experimental/dataclasses/tool.py index 6c3a026d..070cd70f 100644 --- a/haystack_experimental/dataclasses/tool.py +++ b/haystack_experimental/dataclasses/tool.py @@ -12,6 +12,8 @@ from haystack.utils import deserialize_callable, serialize_callable from pydantic import TypeAdapter, create_model +from haystack_experimental.tools import extract_component_parameters + with LazyImport(message="Run 'pip install jsonschema'") as jsonschema_import: from jsonschema import Draft202012Validator from jsonschema.exceptions import SchemaError @@ -216,18 +218,11 @@ def from_component(cls, component: Component, name: str, description: str) -> "T """ if not isinstance(component, Component): - raise ValueError( - f"{component} is not a Haystack component!" "Can only create a Tool from a Haystack component instance." - ) - - if getattr(component, "__haystack_added_to_pipeline__", None): - msg = ( - "Component has been added in a Pipeline and can't be used to create a Tool. " - "Create Tool from a non-pipeline component instead." + message = ( + f"Object {component!r} is not a Haystack component. " + "Use this method to create a Tool only with Haystack component instances." ) - raise ValueError(msg) - - from haystack_experimental.components.tools.openai.component_caller import extract_component_parameters + raise ValueError(message) # Extract the parameters schema from the component parameters = extract_component_parameters(component) diff --git a/haystack_experimental/tools/__init__.py b/haystack_experimental/tools/__init__.py new file mode 100644 index 00000000..826ea101 --- /dev/null +++ b/haystack_experimental/tools/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from .tool_component_descriptor import extract_component_parameters + +__all__ = ["extract_component_parameters"] diff --git a/haystack_experimental/components/tools/openai/component_caller.py b/haystack_experimental/tools/tool_component_descriptor.py similarity index 97% rename from haystack_experimental/components/tools/openai/component_caller.py rename to haystack_experimental/tools/tool_component_descriptor.py index d4f20070..b3aae0ce 100644 --- a/haystack_experimental/components/tools/openai/component_caller.py +++ b/haystack_experimental/tools/tool_component_descriptor.py @@ -2,14 +2,13 @@ # # SPDX-License-Identifier: Apache-2.0 -from dataclasses import MISSING, fields, is_dataclass +from dataclasses import fields, is_dataclass from inspect import getdoc from typing import Any, Callable, Dict, Union, get_args, get_origin from docstring_parser import parse from haystack import logging from haystack.core.component import Component -from pydantic.fields import FieldInfo from haystack_experimental.util.utils import is_pydantic_v2_model @@ -77,6 +76,25 @@ def get_param_descriptions(method: Callable) -> Dict[str, str]: return param_descriptions +class UnsupportedTypeError(Exception): + """Raised when a type is not supported for schema generation.""" + + pass + + +def is_nullable_type(python_type: Any) -> bool: + """ + Checks if the type is a Union with NoneType (i.e., Optional). + + :param python_type: The Python type to check. + :returns: True if the type is a Union with NoneType, False otherwise. + """ + origin = get_origin(python_type) + if origin is Union: + return type(None) in get_args(python_type) + return False + + # ruff: noqa: PLR0912 def create_property_schema(python_type: Any, description: str, default: Any = None) -> Dict[str, Any]: """ @@ -130,16 +148,3 @@ def create_property_schema(python_type: Any, description: str, default: Any = No schema["default"] = default return schema - - -def is_nullable_type(python_type: Any) -> bool: - """ - Checks if the type is a Union with NoneType (i.e., Optional). - - :param python_type: The Python type to check. - :returns: True if the type is a Union with NoneType, False otherwise. - """ - origin = get_origin(python_type) - if origin is Union: - return type(None) in get_args(python_type) - return False diff --git a/test/components/tools/test_tool_component.py b/test/components/tools/test_tool_component.py index 9cb26ca0..9866a278 100644 --- a/test/components/tools/test_tool_component.py +++ b/test/components/tools/test_tool_component.py @@ -30,7 +30,7 @@ def run(self, text: str) -> Dict[str, str]: """ A simple component that generates text. - :param text: user's introductory message + :param text: user's name :return: A dictionary with the generated text. """ return {"reply": f"Hello, {text}!"} @@ -160,7 +160,7 @@ def test_from_component_basic(self): "properties": { "text": { "type": "string", - "description": "user's introductory message" + "description": "user's name" } }, "required": ["text"] @@ -436,18 +436,6 @@ def foo(self, text: str): description="This should fail" ) - def test_from_component_for_pipeline_component(self): - pipeline = Pipeline() - component = SimpleComponent() - pipeline.add_component("component", component) - - with pytest.raises(ValueError): - Tool.from_component( - component=component, - name="invalid_tool", - description="This should fail" - ) - ## Integration tests class TestToolComponentInPipelineWithOpenAI: @@ -605,7 +593,7 @@ def test_document_processor_in_pipeline(self): pipeline.connect("llm.replies", "tool_invoker.messages") message = ChatMessage.from_user( - text="I have two documents. First one says 'Hello world' and second one says 'Goodbye world'. Can you concatenate them?" + text="Concatenate these documents: First one says 'Hello world' and second one says 'Goodbye world'. Set only content field of the document only. Do not set id, meta, score, embedding, sparse_embedding, dataframe, blob fields." ) result = pipeline.run({"llm": {"messages": [message]}})