diff --git a/src/backend/base/langflow/components/prototypes/PythonFunction.py b/src/backend/base/langflow/components/prototypes/PythonFunction.py index bc5576f89..5d572eacb 100644 --- a/src/backend/base/langflow/components/prototypes/PythonFunction.py +++ b/src/backend/base/langflow/components/prototypes/PythonFunction.py @@ -1,27 +1,73 @@ -from typing import Callable +from typing import Callable, List -from langflow.custom import CustomComponent +from langflow.custom import Component from langflow.custom.utils import get_function -from langflow.field_typing import Code +from langflow.io import CodeInput, Output +from langflow.schema import Data, dotdict +from langflow.schema.message import Message -class PythonFunctionComponent(CustomComponent): +class PythonFunctionComponent(Component): display_name = "Python Function" - description = "Define a Python function." + description = "Define and execute a Python function that returns a Data object or a Message." icon = "Python" name = "PythonFunction" beta = True - def build_config(self): - return { - "function_code": { - "display_name": "Code", - "info": "The code for the function.", - "show": True, - }, - } + inputs = [ + CodeInput( + name="function_code", + display_name="Function Code", + info="The code for the function.", + ), + ] - def build(self, function_code: Code) -> Callable: + outputs = [ + Output( + name="function_output", + display_name="Function Callable", + method="get_function_callable", + ), + Output( + name="function_output_data", + display_name="Function Output (Data)", + method="execute_function_data", + ), + Output( + name="function_output_str", + display_name="Function Output (Message)", + method="execute_function_message", + ), + ] + + def get_function_callable(self) -> Callable: + function_code = self.function_code self.status = function_code func = get_function(function_code) return func + + def execute_function(self) -> List[dotdict | str] | dotdict | str: + function_code = self.function_code + + if not function_code: + return "No function code provided." + + try: + func = get_function(function_code) + return func() + except Exception as e: + return f"Error executing function: {str(e)}" + + def execute_function_data(self) -> List[Data]: + results = self.execute_function() + results = results if isinstance(results, list) else [results] + data = [(Data(text=x) if isinstance(x, str) else Data(**x)) for x in results] + return data + + def execute_function_message(self) -> Message: + results = self.execute_function() + results = results if isinstance(results, list) else [results] + results_list = [str(x) for x in results] + results_str = "\n".join(results_list) + data = Message(text=results_str) + return data diff --git a/src/backend/base/langflow/inputs/__init__.py b/src/backend/base/langflow/inputs/__init__.py index a68597da9..c23515cbb 100644 --- a/src/backend/base/langflow/inputs/__init__.py +++ b/src/backend/base/langflow/inputs/__init__.py @@ -16,6 +16,7 @@ from .inputs import ( MultiselectInput, NestedDictInput, PromptInput, + CodeInput, SecretStrInput, StrInput, TableInput, @@ -36,6 +37,7 @@ __all__ = [ "MultilineSecretInput", "NestedDictInput", "PromptInput", + "CodeInput", "SecretStrInput", "StrInput", "MessageTextInput", diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 82e315da1..f07c238a3 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -1,7 +1,14 @@ from enum import Enum from typing import Annotated, Any -from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, field_validator, model_serializer +from pydantic import ( + BaseModel, + ConfigDict, + Field, + PlainSerializer, + field_validator, + model_serializer, +) from langflow.field_typing.range_spec import RangeSpec from langflow.inputs.validators import CoalesceBool @@ -18,6 +25,7 @@ class FieldTypes(str, Enum): NESTED_DICT = "NestedDict" FILE = "file" PROMPT = "prompt" + CODE = "code" OTHER = "other" TABLE = "table" diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index 8d48e3c6c..75d31479d 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -74,6 +74,10 @@ class PromptInput(BaseInputMixin, ListableInputMixin, InputTraceMixin): field_type: SerializableFieldTypes = FieldTypes.PROMPT +class CodeInput(BaseInputMixin, ListableInputMixin, InputTraceMixin): + field_type: SerializableFieldTypes = FieldTypes.CODE + + # Applying mixins to a specific input type class StrInput(BaseInputMixin, ListableInputMixin, DatabaseLoadMixin, MetadataTraceMixin): field_type: SerializableFieldTypes = FieldTypes.TEXT @@ -493,6 +497,7 @@ InputTypes = Union[ MultilineSecretInput, NestedDictInput, PromptInput, + CodeInput, SecretStrInput, StrInput, MessageTextInput, diff --git a/src/backend/base/langflow/io/__init__.py b/src/backend/base/langflow/io/__init__.py index 8d26e7ed7..268943a5a 100644 --- a/src/backend/base/langflow/io/__init__.py +++ b/src/backend/base/langflow/io/__init__.py @@ -14,6 +14,7 @@ from langflow.inputs import ( MultilineSecretInput, NestedDictInput, PromptInput, + CodeInput, SecretStrInput, StrInput, TableInput, @@ -36,6 +37,7 @@ __all__ = [ "MultilineSecretInput", "NestedDictInput", "PromptInput", + "CodeInput", "SecretStrInput", "StrInput", "MessageTextInput", diff --git a/src/backend/tests/unit/inputs/test_inputs.py b/src/backend/tests/unit/inputs/test_inputs.py index b409e82ca..f4057a54b 100644 --- a/src/backend/tests/unit/inputs/test_inputs.py +++ b/src/backend/tests/unit/inputs/test_inputs.py @@ -3,6 +3,7 @@ from pydantic import ValidationError from langflow.inputs.inputs import ( BoolInput, + CodeInput, DataInput, DictInput, DropdownInput, @@ -99,6 +100,11 @@ def test_prompt_input_valid(): assert prompt_input.value == "Enter your name" +def test_code_input_valid(): + code_input = CodeInput(name="valid_code", value="def hello():\n print('Hello, World!')") + assert code_input.value == "def hello():\n print('Hello, World!')" + + def test_multiline_input_valid(): multiline_input = MultilineInput(name="valid_multiline", value="This is a\nmultiline input") assert multiline_input.value == "This is a\nmultiline input" @@ -214,7 +220,10 @@ def test_instantiate_input_comprehensive(): "FloatInput": {"name": "float_input", "value": 10.5}, "BoolInput": {"name": "bool_input", "value": True}, "DictInput": {"name": "dict_input", "value": {"key": "value"}}, - "MultiselectInput": {"name": "multiselect_input", "value": ["option1", "option2"]}, + "MultiselectInput": { + "name": "multiselect_input", + "value": ["option1", "option2"], + }, } for input_type, data in valid_data.items(): diff --git a/src/backend/tests/unit/test_experimental_components.py b/src/backend/tests/unit/test_experimental_components.py index 7495cf47c..50a363f4a 100644 --- a/src/backend/tests/unit/test_experimental_components.py +++ b/src/backend/tests/unit/test_experimental_components.py @@ -14,8 +14,13 @@ def test_python_function_component(): # Act # function must be a string representation function = "def function():\n return 'Hello, World!'" + python_function_component.function_code = function # result is the callable function - result = python_function_component.build(function) + result = python_function_component.get_function_callable() + result_message = python_function_component.execute_function_message() + result_data = python_function_component.execute_function_data() # Assert assert result() == "Hello, World!" + assert result_message.text == "Hello, World!" + assert result_data[0].text == "Hello, World!"