-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Calculator Component updating tool implementation and fix d…
…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
1 parent
7cf77d3
commit 3474259
Showing
4 changed files
with
176 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
src/backend/base/langflow/components/tools/calculator_core.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
84
src/backend/tests/unit/components/tools/test_calculator.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |