Skip to content

Commit

Permalink
refactor: Calculator Component updating tool implementation and fix d…
Browse files Browse the repository at this point in the history
…eprecation 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 <[email protected]>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 17, 2025
1 parent 7cf77d3 commit 3474259
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/backend/base/langflow/components/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .bing_search_api import BingSearchAPIComponent
from .calculator import CalculatorToolComponent
from .calculator_core import CalculatorComponent
from .duck_duck_go_search_run import DuckDuckGoSearchComponent
from .exa_search import ExaSearchToolkit
from .glean_search_api import GleanSearchAPIComponent
Expand Down Expand Up @@ -31,6 +32,7 @@
"AstraDBCQLToolComponent",
"AstraDBToolComponent",
"BingSearchAPIComponent",
"CalculatorComponent",
"CalculatorToolComponent",
"DuckDuckGoSearchComponent",
"ExaSearchToolkit",
Expand Down
3 changes: 2 additions & 1 deletion src/backend/base/langflow/components/tools/calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@


class CalculatorToolComponent(LCToolComponent):
display_name = "Calculator"
display_name = "Calculator [DEPRECATED]"
description = "Perform basic arithmetic operations on a given expression."
icon = "calculator"
name = "CalculatorTool"
legacy = True

inputs = [
MessageTextInput(
Expand Down
88 changes: 88 additions & 0 deletions src/backend/base/langflow/components/tools/calculator_core.py
Original file line number Diff line number Diff line change
@@ -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 changes: 84 additions & 0 deletions src/backend/tests/unit/components/tools/test_calculator.py
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit 3474259

Please sign in to comment.