diff --git a/src/backend/base/langflow/components/tools/__init__.py b/src/backend/base/langflow/components/tools/__init__.py index 7f6ffd042..e405ce6f4 100644 --- a/src/backend/base/langflow/components/tools/__init__.py +++ b/src/backend/base/langflow/components/tools/__init__.py @@ -15,6 +15,7 @@ from .google_serper_api_core import GoogleSerperAPICore from .mcp_stdio import MCPStdio from .python_code_structured_tool import PythonCodeStructuredTool from .python_repl import PythonREPLToolComponent +from .python_repl_core import PythonREPLComponent from .search_api import SearchAPIComponent from .searxng import SearXNGToolComponent from .serp import SerpComponent @@ -47,6 +48,7 @@ __all__ = [ "GoogleSerperAPICore", "MCPStdio", "PythonCodeStructuredTool", + "PythonREPLComponent", "PythonREPLToolComponent", "SearXNGToolComponent", "SearchAPIComponent", diff --git a/src/backend/base/langflow/components/tools/python_repl.py b/src/backend/base/langflow/components/tools/python_repl.py index ba11e49b7..0f453e41d 100644 --- a/src/backend/base/langflow/components/tools/python_repl.py +++ b/src/backend/base/langflow/components/tools/python_repl.py @@ -13,10 +13,11 @@ from langflow.schema import Data class PythonREPLToolComponent(LCToolComponent): - display_name = "Python REPL" + display_name = "Python REPL [DEPRECATED]" description = "A tool for running Python code in a REPL environment." name = "PythonREPLTool" icon = "Python" + legacy = True inputs = [ StrInput( diff --git a/src/backend/base/langflow/components/tools/python_repl_core.py b/src/backend/base/langflow/components/tools/python_repl_core.py new file mode 100644 index 000000000..ea88f55e8 --- /dev/null +++ b/src/backend/base/langflow/components/tools/python_repl_core.py @@ -0,0 +1,99 @@ +import importlib + +from langchain_experimental.utilities import PythonREPL + +from langflow.custom import Component +from langflow.io import CodeInput, Output, StrInput +from langflow.schema import Data + + +class PythonREPLComponent(Component): + display_name = "Python REPL" + description = ( + "A Python code executor that lets you run Python code with specific imported modules. " + "Remember to always use print() to see your results. Example: print(df.head())" + ) + icon = "Python" + + inputs = [ + StrInput( + name="global_imports", + display_name="Global Imports", + info="A comma-separated list of modules to import globally, e.g. 'math,numpy,pandas'.", + value="math,pandas", + required=True, + ), + CodeInput( + name="python_code", + display_name="Python Code", + info="The Python code to execute. Only modules specified in Global Imports can be used.", + value="print('Hello, World!')", + tool_mode=True, + required=True, + ), + ] + + outputs = [ + Output( + display_name="Results", + name="results", + type_=Data, + method="run_python_repl", + ), + ] + + def get_globals(self, global_imports: str | list[str]) -> dict: + """Create a globals dictionary with only the specified allowed imports.""" + global_dict = {} + + try: + if isinstance(global_imports, str): + modules = [module.strip() for module in global_imports.split(",")] + elif isinstance(global_imports, list): + modules = global_imports + else: + msg = "global_imports must be either a string or a list" + raise TypeError(msg) + + for module in modules: + try: + imported_module = importlib.import_module(module) + global_dict[imported_module.__name__] = imported_module + except ImportError as e: + msg = f"Could not import module {module}: {e!s}" + raise ImportError(msg) from e + + except Exception as e: + self.log(f"Error in global imports: {e!s}") + raise + else: + self.log(f"Successfully imported modules: {list(global_dict.keys())}") + return global_dict + + def run_python_repl(self) -> Data: + try: + globals_ = self.get_globals(self.global_imports) + python_repl = PythonREPL(_globals=globals_) + result = python_repl.run(self.python_code) + result = result.strip() if result else "" + + self.log("Code execution completed successfully") + return Data(data={"result": result}) + + except ImportError as e: + error_message = f"Import Error: {e!s}" + self.log(error_message) + return Data(data={"error": error_message}) + + except SyntaxError as e: + error_message = f"Syntax Error: {e!s}" + self.log(error_message) + return Data(data={"error": error_message}) + + except (NameError, TypeError, ValueError) as e: + error_message = f"Error during execution: {e!s}" + self.log(error_message) + return Data(data={"error": error_message}) + + def build(self): + return self.run_python_repl diff --git a/src/backend/base/langflow/io/schema.py b/src/backend/base/langflow/io/schema.py index b92c6fe2e..3feded3e8 100644 --- a/src/backend/base/langflow/io/schema.py +++ b/src/backend/base/langflow/io/schema.py @@ -15,6 +15,7 @@ _convert_field_type_to_type: dict[FieldTypes, type] = { FieldTypes.TABLE: dict, FieldTypes.FILE: str, FieldTypes.PROMPT: str, + FieldTypes.CODE: str, FieldTypes.OTHER: str, } diff --git a/src/backend/tests/unit/components/tools/test_python_repl_tool.py b/src/backend/tests/unit/components/tools/test_python_repl_tool.py index d0f0921b7..98f2b7899 100644 --- a/src/backend/tests/unit/components/tools/test_python_repl_tool.py +++ b/src/backend/tests/unit/components/tools/test_python_repl_tool.py @@ -1,30 +1,53 @@ -from langflow.components.tools import PythonREPLToolComponent -from langflow.custom import Component -from langflow.custom.utils import build_custom_component_template +import pytest +from langflow.components.tools import PythonREPLComponent + +from tests.base import DID_NOT_EXIST, ComponentTestBaseWithoutClient -def test_python_repl_tool_template(): - python_repl_tool = PythonREPLToolComponent() - component = Component(_code=python_repl_tool._code) - frontend_node, _ = build_custom_component_template(component) - assert "outputs" in frontend_node - output_names = [output["name"] for output in frontend_node["outputs"]] - assert "api_run_model" in output_names - assert "api_build_tool" in output_names - assert all(output["types"] != [] for output in frontend_node["outputs"]) +class TestPythonREPLComponent(ComponentTestBaseWithoutClient): + @pytest.fixture + def component_class(self): + return PythonREPLComponent - # Additional assertions specific to PythonREPLToolComponent - input_names = [input_["name"] for input_ in frontend_node["template"].values() if isinstance(input_, dict)] - # assert "input_value" in input_names - assert "name" in input_names - assert "description" in input_names - assert "global_imports" in input_names + @pytest.fixture + def default_kwargs(self): + return { + "global_imports": "math", + "python_code": "print('Hello, World!')", + } - global_imports_input = next( - input_ - for input_ in frontend_node["template"].values() - if isinstance(input_, dict) and input_["name"] == "global_imports" - ) - assert global_imports_input["type"] == "str" - # assert global_imports_input["combobox"] is True - assert global_imports_input["value"] == "math" + @pytest.fixture + def file_names_mapping(self): + # Component not yet released, mark all versions as non-existent + return [ + {"version": "1.0.17", "module": "tools", "file_name": DID_NOT_EXIST}, + {"version": "1.0.18", "module": "tools", "file_name": DID_NOT_EXIST}, + {"version": "1.0.19", "module": "tools", "file_name": DID_NOT_EXIST}, + {"version": "1.1.0", "module": "tools", "file_name": DID_NOT_EXIST}, + {"version": "1.1.1", "module": "tools", "file_name": DID_NOT_EXIST}, + ] + + def test_component_initialization(self, component_class, default_kwargs): + component = component_class(**default_kwargs) + frontend_node = component.to_frontend_node() + node_data = frontend_node["data"]["node"] + + # Test template fields + template = node_data["template"] + assert "global_imports" in template + assert "python_code" in template + + # Test global_imports configuration + global_imports = template["global_imports"] + assert global_imports["type"] == "str" + assert global_imports["value"] == "math" + assert global_imports["required"] is True + + # Test python_code configuration + python_code = template["python_code"] + assert python_code["type"] == "code" + assert python_code["value"] == "print('Hello, World!')" + assert python_code["required"] is True + + # Test base configuration + assert "Data" in node_data["base_classes"]