refactor(tools): overhaul Python REPL component with modern tool mode (#5463)

* refactor(tools): overhaul Python REPL component with modern tool mode

BREAKING CHANGE: Complete redesign of PythonREPL component architecture

- Replace legacy tool mode with modern tool_mode=true implementation
- Add automatic import detection for both global and from-imports
- Remove manual global_imports field in favor of automatic detection
- Implement CodeInput type support with FieldTypes.CODE
- Update schema to handle new parameters for agent compatibility
- Improve error handling and logging for import failures

The component now automatically handles imports without manual configuration,
supports modern tool mode, and provides better integration with the agent
system through updated schema definitions.

* refactor(python-repl): remove name parameter

* feat(security): add required global imports validation

- Add explicit global imports input for security control
- Set both global_imports and python_code as required fields
- Remove AST-based import analysis in favor of explicit imports

* [autofix.ci] apply automated fixes

* fix: remove generic exception handling in python_repl

Removed overly broad exception handling to comply with linting rules.

* fix

* test(python-repl): update component test to match current implementation

- Remove assertions for deprecated outputs (api_run_model, api_build_tool)
- Add assertions for current 'results' output
- Add detailed input validation tests for global_imports and python_code
- Verify input configurations including type, default values, and required status
- Ensure component template structure matches frontend requirements

This change aligns the test suite with the current PythonREPLToolComponent
implementation, improving test coverage and maintaining component reliability.

* revert(tools): restore Python REPL component to original implementation

Due to backward compatibility concerns, reverting the Python REPL component
to its initial implementation state to maintain stability and prevent breaking changes.

* feat(tools): mark Python REPL component as deprecated & Legacy

* feat(tools): add Python REPL Core component

* [autofix.ci] apply automated fixes

* fix(tests): mark Python REPL Core as unreleased component

* fix(__init__): fix

* feat(python-repl): improve example clarity for printing the results

* fix(tests): resolve KeyError in test_component_versions

* style(python-repl): format description string to follow PEP 8 guidelines

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
VICTOR CORREA GOMES 2025-01-20 08:54:32 -03:00 • committed by GitHub
commit 6c79a3d6aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 153 additions and 27 deletions

View file

@ -15,6 +15,7 @@ from .google_serper_api_core import GoogleSerperAPICore
from .mcp_stdio import MCPStdio from .mcp_stdio import MCPStdio
from .python_code_structured_tool import PythonCodeStructuredTool from .python_code_structured_tool import PythonCodeStructuredTool
from .python_repl import PythonREPLToolComponent from .python_repl import PythonREPLToolComponent
from .python_repl_core import PythonREPLComponent
from .search_api import SearchAPIComponent from .search_api import SearchAPIComponent
from .searxng import SearXNGToolComponent from .searxng import SearXNGToolComponent
from .serp import SerpComponent from .serp import SerpComponent
@ -47,6 +48,7 @@ __all__ = [
"GoogleSerperAPICore", "GoogleSerperAPICore",
"MCPStdio", "MCPStdio",
"PythonCodeStructuredTool", "PythonCodeStructuredTool",
"PythonREPLComponent",
"PythonREPLToolComponent", "PythonREPLToolComponent",
"SearXNGToolComponent", "SearXNGToolComponent",
"SearchAPIComponent", "SearchAPIComponent",

View file

@ -13,10 +13,11 @@ from langflow.schema import Data
class PythonREPLToolComponent(LCToolComponent): class PythonREPLToolComponent(LCToolComponent):
display_name = "Python REPL" display_name = "Python REPL [DEPRECATED]"
description = "A tool for running Python code in a REPL environment." description = "A tool for running Python code in a REPL environment."
name = "PythonREPLTool" name = "PythonREPLTool"
icon = "Python" icon = "Python"
legacy = True
inputs = [ inputs = [
StrInput( StrInput(

View file

@ -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

View file

@ -15,6 +15,7 @@ _convert_field_type_to_type: dict[FieldTypes, type] = {
FieldTypes.TABLE: dict, FieldTypes.TABLE: dict,
FieldTypes.FILE: str, FieldTypes.FILE: str,
FieldTypes.PROMPT: str, FieldTypes.PROMPT: str,
FieldTypes.CODE: str,
FieldTypes.OTHER: str, FieldTypes.OTHER: str,
} }

View file

@ -1,30 +1,53 @@
from langflow.components.tools import PythonREPLToolComponent import pytest
from langflow.custom import Component from langflow.components.tools import PythonREPLComponent
from langflow.custom.utils import build_custom_component_template
from tests.base import DID_NOT_EXIST, ComponentTestBaseWithoutClient
def test_python_repl_tool_template(): class TestPythonREPLComponent(ComponentTestBaseWithoutClient):
python_repl_tool = PythonREPLToolComponent() @pytest.fixture
component = Component(_code=python_repl_tool._code) def component_class(self):
frontend_node, _ = build_custom_component_template(component) return PythonREPLComponent
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"])
# Additional assertions specific to PythonREPLToolComponent @pytest.fixture
input_names = [input_["name"] for input_ in frontend_node["template"].values() if isinstance(input_, dict)] def default_kwargs(self):
# assert "input_value" in input_names return {
assert "name" in input_names "global_imports": "math",
assert "description" in input_names "python_code": "print('Hello, World!')",
assert "global_imports" in input_names }
global_imports_input = next( @pytest.fixture
input_ def file_names_mapping(self):
for input_ in frontend_node["template"].values() # Component not yet released, mark all versions as non-existent
if isinstance(input_, dict) and input_["name"] == "global_imports" return [
) {"version": "1.0.17", "module": "tools", "file_name": DID_NOT_EXIST},
assert global_imports_input["type"] == "str" {"version": "1.0.18", "module": "tools", "file_name": DID_NOT_EXIST},
# assert global_imports_input["combobox"] is True {"version": "1.0.19", "module": "tools", "file_name": DID_NOT_EXIST},
assert global_imports_input["value"] == "math" {"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"]