refactor: Calculator Component updating tool implementation and fix deprecation warnings (#5442)
* refactor(calculator): update tool implementation Replace legacy tool mode implementation using CalculatorToolSchema with simplified tool_mode=True approach. * refactor(calculator): fix deprecation warnings Fix ast.Num deprecation warnings by supporting ast.Constant while maintaining backwards compatibility. * Update isinstance check to use Python 3.10+ union operator (|) instead of tuple syntax * Update calculator.py Component name required; if not it would get None in Toolset * [autofix.ci] apply automated fixes * test(calculator): add unit tests for CalculatorToolComponent * revert(tools): restore Calculator component to its original implementation Due to potential breaking changes in the repository, reverting the Calculator component to its initial PR state to maintain compatibility and stability. * feat(tools): mark Calculator component as legacy and update display name - Set legacy flag to true for Calculator component - Update display name to "Calculator (Deprecated)" to clearly indicate deprecation status - Maintain backward compatibility by preserving class name and internal name * feat(tools)!: add new calculator core component BREAKING CHANGE: Introduces calculator_core.py as a replacement for the deprecated calculator.py * refactor(tools): rename calculator classes for better distinction * refactor(tools): update __init__.py to reflect new class names * [autofix.ci] apply automated fixes * test(tools): update calculator tests for core component --------- Co-authored-by: Edwin Jose <edwin.jose@datastax.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
7cf77d30ae
commit
3474259ca4
4 changed files with 176 additions and 1 deletions
|
|
@ -4,6 +4,7 @@ from langchain_core._api.deprecation import LangChainDeprecationWarning
|
||||||
|
|
||||||
from .bing_search_api import BingSearchAPIComponent
|
from .bing_search_api import BingSearchAPIComponent
|
||||||
from .calculator import CalculatorToolComponent
|
from .calculator import CalculatorToolComponent
|
||||||
|
from .calculator_core import CalculatorComponent
|
||||||
from .duck_duck_go_search_run import DuckDuckGoSearchComponent
|
from .duck_duck_go_search_run import DuckDuckGoSearchComponent
|
||||||
from .exa_search import ExaSearchToolkit
|
from .exa_search import ExaSearchToolkit
|
||||||
from .glean_search_api import GleanSearchAPIComponent
|
from .glean_search_api import GleanSearchAPIComponent
|
||||||
|
|
@ -31,6 +32,7 @@ __all__ = [
|
||||||
"AstraDBCQLToolComponent",
|
"AstraDBCQLToolComponent",
|
||||||
"AstraDBToolComponent",
|
"AstraDBToolComponent",
|
||||||
"BingSearchAPIComponent",
|
"BingSearchAPIComponent",
|
||||||
|
"CalculatorComponent",
|
||||||
"CalculatorToolComponent",
|
"CalculatorToolComponent",
|
||||||
"DuckDuckGoSearchComponent",
|
"DuckDuckGoSearchComponent",
|
||||||
"ExaSearchToolkit",
|
"ExaSearchToolkit",
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,11 @@ from langflow.schema import Data
|
||||||
|
|
||||||
|
|
||||||
class CalculatorToolComponent(LCToolComponent):
|
class CalculatorToolComponent(LCToolComponent):
|
||||||
display_name = "Calculator"
|
display_name = "Calculator [DEPRECATED]"
|
||||||
description = "Perform basic arithmetic operations on a given expression."
|
description = "Perform basic arithmetic operations on a given expression."
|
||||||
icon = "calculator"
|
icon = "calculator"
|
||||||
name = "CalculatorTool"
|
name = "CalculatorTool"
|
||||||
|
legacy = True
|
||||||
|
|
||||||
inputs = [
|
inputs = [
|
||||||
MessageTextInput(
|
MessageTextInput(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import ast
|
||||||
|
import operator
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from langflow.custom import Component
|
||||||
|
from langflow.inputs import MessageTextInput
|
||||||
|
from langflow.io import Output
|
||||||
|
from langflow.schema import Data
|
||||||
|
|
||||||
|
|
||||||
|
class CalculatorComponent(Component):
|
||||||
|
display_name = "Calculator"
|
||||||
|
description = "Perform basic arithmetic operations on a given expression."
|
||||||
|
icon = "calculator"
|
||||||
|
|
||||||
|
# Cache operators dictionary as a class variable
|
||||||
|
OPERATORS: dict[type[ast.operator], Callable] = {
|
||||||
|
ast.Add: operator.add,
|
||||||
|
ast.Sub: operator.sub,
|
||||||
|
ast.Mult: operator.mul,
|
||||||
|
ast.Div: operator.truediv,
|
||||||
|
ast.Pow: operator.pow,
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
MessageTextInput(
|
||||||
|
name="expression",
|
||||||
|
display_name="Expression",
|
||||||
|
info="The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').",
|
||||||
|
tool_mode=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
outputs = [
|
||||||
|
Output(display_name="Data", name="result", type_=Data, method="evaluate_expression"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _eval_expr(self, node: ast.AST) -> float:
|
||||||
|
"""Evaluate an AST node recursively."""
|
||||||
|
if isinstance(node, ast.Constant):
|
||||||
|
if isinstance(node.value, int | float):
|
||||||
|
return float(node.value)
|
||||||
|
error_msg = f"Unsupported constant type: {type(node.value).__name__}"
|
||||||
|
raise TypeError(error_msg)
|
||||||
|
if isinstance(node, ast.Num): # For backwards compatibility
|
||||||
|
if isinstance(node.n, int | float):
|
||||||
|
return float(node.n)
|
||||||
|
error_msg = f"Unsupported number type: {type(node.n).__name__}"
|
||||||
|
raise TypeError(error_msg)
|
||||||
|
|
||||||
|
if isinstance(node, ast.BinOp):
|
||||||
|
op_type = type(node.op)
|
||||||
|
if op_type not in self.OPERATORS:
|
||||||
|
error_msg = f"Unsupported binary operator: {op_type.__name__}"
|
||||||
|
raise TypeError(error_msg)
|
||||||
|
|
||||||
|
left = self._eval_expr(node.left)
|
||||||
|
right = self._eval_expr(node.right)
|
||||||
|
return self.OPERATORS[op_type](left, right)
|
||||||
|
|
||||||
|
error_msg = f"Unsupported operation or expression type: {type(node).__name__}"
|
||||||
|
raise TypeError(error_msg)
|
||||||
|
|
||||||
|
def evaluate_expression(self) -> Data:
|
||||||
|
"""Evaluate the mathematical expression and return the result."""
|
||||||
|
try:
|
||||||
|
tree = ast.parse(self.expression, mode="eval")
|
||||||
|
result = self._eval_expr(tree.body)
|
||||||
|
|
||||||
|
formatted_result = f"{float(result):.6f}".rstrip("0").rstrip(".")
|
||||||
|
self.log(f"Calculation result: {formatted_result}")
|
||||||
|
|
||||||
|
self.status = formatted_result
|
||||||
|
return Data(data={"result": formatted_result})
|
||||||
|
|
||||||
|
except ZeroDivisionError:
|
||||||
|
error_message = "Error: Division by zero"
|
||||||
|
self.status = error_message
|
||||||
|
return Data(data={"error": error_message, "input": self.expression})
|
||||||
|
|
||||||
|
except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:
|
||||||
|
error_message = f"Invalid expression: {e!s}"
|
||||||
|
self.status = error_message
|
||||||
|
return Data(data={"error": error_message, "input": self.expression})
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
"""Return the main evaluation function."""
|
||||||
|
return self.evaluate_expression
|
||||||
84
src/backend/tests/unit/components/tools/test_calculator.py
Normal file
84
src/backend/tests/unit/components/tools/test_calculator.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import pytest
|
||||||
|
from langflow.components.tools.calculator_core import CalculatorComponent
|
||||||
|
|
||||||
|
from tests.base import ComponentTestBaseWithoutClient
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalculatorComponent(ComponentTestBaseWithoutClient):
|
||||||
|
@pytest.fixture
|
||||||
|
def component_class(self):
|
||||||
|
return CalculatorComponent
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def default_kwargs(self):
|
||||||
|
return {"expression": "2 + 2", "_session_id": "test_session"}
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def file_names_mapping(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def test_basic_calculation(self, component_class, default_kwargs):
|
||||||
|
# Arrange
|
||||||
|
component = component_class(**default_kwargs)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = component.evaluate_expression()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result.data["result"] == "4"
|
||||||
|
|
||||||
|
def test_complex_calculation(self, component_class):
|
||||||
|
# Arrange
|
||||||
|
component = component_class(expression="4*4*(33/22)+12-20", _session_id="test_session")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = component.evaluate_expression()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert float(result.data["result"]) == pytest.approx(16)
|
||||||
|
|
||||||
|
def test_division_by_zero(self, component_class):
|
||||||
|
# Arrange
|
||||||
|
component = component_class(expression="1/0", _session_id="test_session")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = component.evaluate_expression()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert "error" in result.data
|
||||||
|
assert result.data["error"] == "Error: Division by zero"
|
||||||
|
|
||||||
|
def test_invalid_expression(self, component_class):
|
||||||
|
# Arrange
|
||||||
|
component = component_class(expression="2 + *", _session_id="test_session")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = component.evaluate_expression()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert "error" in result.data
|
||||||
|
assert "Invalid expression" in result.data["error"]
|
||||||
|
|
||||||
|
def test_unsupported_operation(self, component_class):
|
||||||
|
# Arrange
|
||||||
|
component = component_class(expression="sqrt(16)", _session_id="test_session")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = component.evaluate_expression()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert "error" in result.data
|
||||||
|
assert "Unsupported operation" in result.data["error"]
|
||||||
|
|
||||||
|
def test_component_frontend_node(self, component_class, default_kwargs):
|
||||||
|
# Arrange
|
||||||
|
component = component_class(**default_kwargs)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
frontend_node = component.to_frontend_node()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
node_data = frontend_node["data"]["node"]
|
||||||
|
assert node_data["display_name"] == "Calculator"
|
||||||
|
assert node_data["description"] == "Perform basic arithmetic operations on a given expression."
|
||||||
|
assert node_data["icon"] == "calculator"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue