featur: Add CalculatorToolComponent (#3772)
* feat: Add CalculatorToolComponent to langflow/components/tools This commit adds the CalculatorToolComponent to the langflow/components/tools directory. The CalculatorToolComponent allows users to perform basic arithmetic operations on a given expression. It includes an input for the expression and returns the result or an error message. Ref: #3757 * lint * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
2652f44d31
commit
92f59ab9e0
5 changed files with 92 additions and 17 deletions
5
poetry.lock
generated
5
poetry.lock
generated
|
|
@ -10605,11 +10605,6 @@ files = [
|
||||||
{file = "triton-3.0.0-1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e509deb77f1c067d8640725ef00c5cbfcb2052a1a3cb6a6d343841f92624eb"},
|
{file = "triton-3.0.0-1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e509deb77f1c067d8640725ef00c5cbfcb2052a1a3cb6a6d343841f92624eb"},
|
||||||
{file = "triton-3.0.0-1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bcbf3b1c48af6a28011a5c40a5b3b9b5330530c3827716b5fbf6d7adcc1e53e9"},
|
{file = "triton-3.0.0-1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bcbf3b1c48af6a28011a5c40a5b3b9b5330530c3827716b5fbf6d7adcc1e53e9"},
|
||||||
{file = "triton-3.0.0-1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6e5727202f7078c56f91ff13ad0c1abab14a0e7f2c87e91b12b6f64f3e8ae609"},
|
{file = "triton-3.0.0-1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6e5727202f7078c56f91ff13ad0c1abab14a0e7f2c87e91b12b6f64f3e8ae609"},
|
||||||
{file = "triton-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39b052da883351fdf6be3d93cedae6db3b8e3988d3b09ed221bccecfa9612230"},
|
|
||||||
{file = "triton-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd34f19a8582af96e6291d4afce25dac08cb2a5d218c599163761e8e0827208e"},
|
|
||||||
{file = "triton-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d5e10de8c011adeb7c878c6ce0dd6073b14367749e34467f1cff2bde1b78253"},
|
|
||||||
{file = "triton-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8903767951bf86ec960b4fe4e21bc970055afc65e9d57e916d79ae3c93665e3"},
|
|
||||||
{file = "triton-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41004fb1ae9a53fcb3e970745feb87f0e3c94c6ce1ba86e95fa3b8537894bef7"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
|
||||||
82
src/backend/base/langflow/components/tools/Calculator.py
Normal file
82
src/backend/base/langflow/components/tools/Calculator.py
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import ast
|
||||||
|
import operator
|
||||||
|
from typing import List
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from langflow.base.langchain_utilities.model import LCToolComponent
|
||||||
|
from langflow.inputs import MessageTextInput
|
||||||
|
from langflow.schema import Data
|
||||||
|
from langflow.field_typing import Tool
|
||||||
|
from langchain.tools import StructuredTool
|
||||||
|
|
||||||
|
|
||||||
|
class CalculatorToolComponent(LCToolComponent):
|
||||||
|
display_name = "Calculator"
|
||||||
|
description = "Perform basic arithmetic operations on a given expression."
|
||||||
|
icon = "calculator"
|
||||||
|
name = "CalculatorTool"
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
MessageTextInput(
|
||||||
|
name="expression",
|
||||||
|
display_name="Expression",
|
||||||
|
info="The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
class CalculatorToolSchema(BaseModel):
|
||||||
|
expression: str = Field(..., description="The arithmetic expression to evaluate.")
|
||||||
|
|
||||||
|
def run_model(self) -> List[Data]:
|
||||||
|
return self._evaluate_expression(self.expression)
|
||||||
|
|
||||||
|
def build_tool(self) -> Tool:
|
||||||
|
return StructuredTool.from_function(
|
||||||
|
name="calculator",
|
||||||
|
description="Evaluate basic arithmetic expressions. Input should be a string containing the expression.",
|
||||||
|
func=self._evaluate_expression,
|
||||||
|
args_schema=self.CalculatorToolSchema,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _evaluate_expression(self, expression: str) -> List[Data]:
|
||||||
|
try:
|
||||||
|
# Define the allowed operators
|
||||||
|
operators = {
|
||||||
|
ast.Add: operator.add,
|
||||||
|
ast.Sub: operator.sub,
|
||||||
|
ast.Mult: operator.mul,
|
||||||
|
ast.Div: operator.truediv,
|
||||||
|
ast.Pow: operator.pow,
|
||||||
|
}
|
||||||
|
|
||||||
|
def eval_expr(node):
|
||||||
|
if isinstance(node, ast.Num):
|
||||||
|
return node.n
|
||||||
|
elif isinstance(node, ast.BinOp):
|
||||||
|
return operators[type(node.op)](eval_expr(node.left), eval_expr(node.right))
|
||||||
|
elif isinstance(node, ast.UnaryOp):
|
||||||
|
return operators[type(node.op)](eval_expr(node.operand))
|
||||||
|
else:
|
||||||
|
raise TypeError(node)
|
||||||
|
|
||||||
|
# Parse the expression and evaluate it
|
||||||
|
tree = ast.parse(expression, mode="eval")
|
||||||
|
result = eval_expr(tree.body)
|
||||||
|
|
||||||
|
# Format the result to a reasonable number of decimal places
|
||||||
|
formatted_result = f"{result:.6f}".rstrip("0").rstrip(".")
|
||||||
|
|
||||||
|
self.status = formatted_result
|
||||||
|
return [Data(data={"result": formatted_result})]
|
||||||
|
|
||||||
|
except (SyntaxError, TypeError, KeyError) as e:
|
||||||
|
error_message = f"Invalid expression: {str(e)}"
|
||||||
|
self.status = error_message
|
||||||
|
return [Data(data={"error": error_message})]
|
||||||
|
except ZeroDivisionError:
|
||||||
|
error_message = "Error: Division by zero"
|
||||||
|
self.status = error_message
|
||||||
|
return [Data(data={"error": error_message})]
|
||||||
|
except Exception as e:
|
||||||
|
error_message = f"Error: {str(e)}"
|
||||||
|
self.status = error_message
|
||||||
|
return [Data(data={"error": error_message})]
|
||||||
|
|
@ -10,6 +10,7 @@ from .SearXNGTool import SearXNGToolComponent
|
||||||
from .SerpAPI import SerpAPIComponent
|
from .SerpAPI import SerpAPIComponent
|
||||||
from .WikipediaAPI import WikipediaAPIComponent
|
from .WikipediaAPI import WikipediaAPIComponent
|
||||||
from .WolframAlphaAPI import WolframAlphaAPIComponent
|
from .WolframAlphaAPI import WolframAlphaAPIComponent
|
||||||
|
from .Calculator import CalculatorToolComponent
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|
@ -25,4 +26,5 @@ __all__ = [
|
||||||
"SerpAPIComponent",
|
"SerpAPIComponent",
|
||||||
"WikipediaAPIComponent",
|
"WikipediaAPIComponent",
|
||||||
"WolframAlphaAPIComponent",
|
"WolframAlphaAPIComponent",
|
||||||
|
"CalculatorToolComponent",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1159,7 +1159,7 @@
|
||||||
"description": "Perform basic arithmetic operations on a given expression.",
|
"description": "Perform basic arithmetic operations on a given expression.",
|
||||||
"display_name": "Calculator",
|
"display_name": "Calculator",
|
||||||
"documentation": "",
|
"documentation": "",
|
||||||
"edited": true,
|
"edited": false,
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"expression"
|
"expression"
|
||||||
],
|
],
|
||||||
|
|
@ -1176,8 +1176,7 @@
|
||||||
"name": "api_run_model",
|
"name": "api_run_model",
|
||||||
"selected": "Data",
|
"selected": "Data",
|
||||||
"types": [
|
"types": [
|
||||||
"Data",
|
"Data"
|
||||||
"list"
|
|
||||||
],
|
],
|
||||||
"value": "__UNDEFINED__"
|
"value": "__UNDEFINED__"
|
||||||
},
|
},
|
||||||
|
|
@ -1188,8 +1187,7 @@
|
||||||
"name": "api_build_tool",
|
"name": "api_build_tool",
|
||||||
"selected": "Tool",
|
"selected": "Tool",
|
||||||
"types": [
|
"types": [
|
||||||
"Tool",
|
"Tool"
|
||||||
"Sequence"
|
|
||||||
],
|
],
|
||||||
"value": "__UNDEFINED__"
|
"value": "__UNDEFINED__"
|
||||||
}
|
}
|
||||||
|
|
@ -1213,7 +1211,7 @@
|
||||||
"show": true,
|
"show": true,
|
||||||
"title_case": false,
|
"title_case": false,
|
||||||
"type": "code",
|
"type": "code",
|
||||||
"value": "import ast\r\nimport operator\r\nfrom typing import List\r\nfrom pydantic import BaseModel, Field\r\nfrom langflow.base.langchain_utilities.model import LCToolComponent\r\nfrom langflow.inputs import MessageTextInput\r\nfrom langflow.schema import Data\r\nfrom langflow.field_typing import Tool\r\nfrom langchain.tools import StructuredTool\r\n\r\nclass CalculatorToolComponent(LCToolComponent):\r\n display_name = \"Calculator\"\r\n description = \"Perform basic arithmetic operations on a given expression.\"\r\n icon = \"calculator\"\r\n name = \"CalculatorTool\"\r\n\r\n inputs = [\r\n MessageTextInput(\r\n name=\"expression\",\r\n display_name=\"Expression\",\r\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\r\n ),\r\n ]\r\n\r\n class CalculatorToolSchema(BaseModel):\r\n expression: str = Field(..., description=\"The arithmetic expression to evaluate.\")\r\n\r\n def run_model(self) -> List[Data]:\r\n return self._evaluate_expression(self.expression)\r\n\r\n def build_tool(self) -> Tool:\r\n return StructuredTool.from_function(\r\n name=\"calculator\",\r\n description=\"Evaluate basic arithmetic expressions. Input should be a string containing the expression.\",\r\n func=self._evaluate_expression,\r\n args_schema=self.CalculatorToolSchema,\r\n )\r\n\r\n def _evaluate_expression(self, expression: str) -> List[Data]:\r\n try:\r\n # Define the allowed operators\r\n operators = {\r\n ast.Add: operator.add,\r\n ast.Sub: operator.sub,\r\n ast.Mult: operator.mul,\r\n ast.Div: operator.truediv,\r\n ast.Pow: operator.pow,\r\n }\r\n\r\n def eval_expr(node):\r\n if isinstance(node, ast.Num):\r\n return node.n\r\n elif isinstance(node, ast.BinOp):\r\n return operators[type(node.op)](eval_expr(node.left), eval_expr(node.right))\r\n elif isinstance(node, ast.UnaryOp):\r\n return operators[type(node.op)](eval_expr(node.operand))\r\n else:\r\n raise TypeError(node)\r\n\r\n # Parse the expression and evaluate it\r\n tree = ast.parse(expression, mode='eval')\r\n result = eval_expr(tree.body)\r\n\r\n # Format the result to a reasonable number of decimal places\r\n formatted_result = f\"{result:.6f}\".rstrip('0').rstrip('.')\r\n\r\n self.status = formatted_result\r\n return [Data(data={\"result\": formatted_result})]\r\n\r\n except (SyntaxError, TypeError, KeyError) as e:\r\n error_message = f\"Invalid expression: {str(e)}\"\r\n self.status = error_message\r\n return [Data(data={\"error\": error_message})]\r\n except ZeroDivisionError:\r\n error_message = \"Error: Division by zero\"\r\n self.status = error_message\r\n return [Data(data={\"error\": error_message})]\r\n except Exception as e:\r\n error_message = f\"Error: {str(e)}\"\r\n self.status = error_message\r\n return [Data(data={\"error\": error_message})]"
|
"value": "import ast\nimport operator\nfrom typing import List\nfrom pydantic import BaseModel, Field\nfrom langflow.base.langchain_utilities.model import LCToolComponent\nfrom langflow.inputs import MessageTextInput\nfrom langflow.schema import Data\nfrom langflow.field_typing import Tool\nfrom langchain.tools import StructuredTool\n\n\nclass CalculatorToolComponent(LCToolComponent):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n icon = \"calculator\"\n name = \"CalculatorTool\"\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n ),\n ]\n\n class CalculatorToolSchema(BaseModel):\n expression: str = Field(..., description=\"The arithmetic expression to evaluate.\")\n\n def run_model(self) -> List[Data]:\n return self._evaluate_expression(self.expression)\n\n def build_tool(self) -> Tool:\n return StructuredTool.from_function(\n name=\"calculator\",\n description=\"Evaluate basic arithmetic expressions. Input should be a string containing the expression.\",\n func=self._evaluate_expression,\n args_schema=self.CalculatorToolSchema,\n )\n\n def _evaluate_expression(self, expression: str) -> List[Data]:\n try:\n # Define the allowed operators\n operators = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n def eval_expr(node):\n if isinstance(node, ast.Num):\n return node.n\n elif isinstance(node, ast.BinOp):\n return operators[type(node.op)](eval_expr(node.left), eval_expr(node.right))\n elif isinstance(node, ast.UnaryOp):\n return operators[type(node.op)](eval_expr(node.operand))\n else:\n raise TypeError(node)\n\n # Parse the expression and evaluate it\n tree = ast.parse(expression, mode=\"eval\")\n result = eval_expr(tree.body)\n\n # Format the result to a reasonable number of decimal places\n formatted_result = f\"{result:.6f}\".rstrip(\"0\").rstrip(\".\")\n\n self.status = formatted_result\n return [Data(data={\"result\": formatted_result})]\n\n except (SyntaxError, TypeError, KeyError) as e:\n error_message = f\"Invalid expression: {str(e)}\"\n self.status = error_message\n return [Data(data={\"error\": error_message})]\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return [Data(data={\"error\": error_message})]\n except Exception as e:\n error_message = f\"Error: {str(e)}\"\n self.status = error_message\n return [Data(data={\"error\": error_message})]\n"
|
||||||
},
|
},
|
||||||
"expression": {
|
"expression": {
|
||||||
"_input_type": "MessageTextInput",
|
"_input_type": "MessageTextInput",
|
||||||
|
|
|
||||||
|
|
@ -2258,7 +2258,7 @@
|
||||||
"description": "Perform basic arithmetic operations on a given expression.",
|
"description": "Perform basic arithmetic operations on a given expression.",
|
||||||
"display_name": "Calculator",
|
"display_name": "Calculator",
|
||||||
"documentation": "",
|
"documentation": "",
|
||||||
"edited": true,
|
"edited": false,
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"expression"
|
"expression"
|
||||||
],
|
],
|
||||||
|
|
@ -2275,8 +2275,7 @@
|
||||||
"name": "api_run_model",
|
"name": "api_run_model",
|
||||||
"selected": "Data",
|
"selected": "Data",
|
||||||
"types": [
|
"types": [
|
||||||
"Data",
|
"Data"
|
||||||
"list"
|
|
||||||
],
|
],
|
||||||
"value": "__UNDEFINED__"
|
"value": "__UNDEFINED__"
|
||||||
},
|
},
|
||||||
|
|
@ -2287,8 +2286,7 @@
|
||||||
"name": "api_build_tool",
|
"name": "api_build_tool",
|
||||||
"selected": "Tool",
|
"selected": "Tool",
|
||||||
"types": [
|
"types": [
|
||||||
"Tool",
|
"Tool"
|
||||||
"Sequence"
|
|
||||||
],
|
],
|
||||||
"value": "__UNDEFINED__"
|
"value": "__UNDEFINED__"
|
||||||
}
|
}
|
||||||
|
|
@ -2312,7 +2310,7 @@
|
||||||
"show": true,
|
"show": true,
|
||||||
"title_case": false,
|
"title_case": false,
|
||||||
"type": "code",
|
"type": "code",
|
||||||
"value": "import ast\r\nimport operator\r\nfrom typing import List\r\nfrom pydantic import BaseModel, Field\r\nfrom langflow.base.langchain_utilities.model import LCToolComponent\r\nfrom langflow.inputs import MessageTextInput\r\nfrom langflow.schema import Data\r\nfrom langflow.field_typing import Tool\r\nfrom langchain.tools import StructuredTool\r\n\r\nclass CalculatorToolComponent(LCToolComponent):\r\n display_name = \"Calculator\"\r\n description = \"Perform basic arithmetic operations on a given expression.\"\r\n icon = \"calculator\"\r\n name = \"CalculatorTool\"\r\n\r\n inputs = [\r\n MessageTextInput(\r\n name=\"expression\",\r\n display_name=\"Expression\",\r\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\r\n ),\r\n ]\r\n\r\n class CalculatorToolSchema(BaseModel):\r\n expression: str = Field(..., description=\"The arithmetic expression to evaluate.\")\r\n\r\n def run_model(self) -> List[Data]:\r\n return self._evaluate_expression(self.expression)\r\n\r\n def build_tool(self) -> Tool:\r\n return StructuredTool.from_function(\r\n name=\"calculator\",\r\n description=\"Evaluate basic arithmetic expressions. Input should be a string containing the expression.\",\r\n func=self._evaluate_expression,\r\n args_schema=self.CalculatorToolSchema,\r\n )\r\n\r\n def _evaluate_expression(self, expression: str) -> List[Data]:\r\n try:\r\n # Define the allowed operators\r\n operators = {\r\n ast.Add: operator.add,\r\n ast.Sub: operator.sub,\r\n ast.Mult: operator.mul,\r\n ast.Div: operator.truediv,\r\n ast.Pow: operator.pow,\r\n }\r\n\r\n def eval_expr(node):\r\n if isinstance(node, ast.Num):\r\n return node.n\r\n elif isinstance(node, ast.BinOp):\r\n return operators[type(node.op)](eval_expr(node.left), eval_expr(node.right))\r\n elif isinstance(node, ast.UnaryOp):\r\n return operators[type(node.op)](eval_expr(node.operand))\r\n else:\r\n raise TypeError(node)\r\n\r\n # Parse the expression and evaluate it\r\n tree = ast.parse(expression, mode='eval')\r\n result = eval_expr(tree.body)\r\n\r\n # Format the result to a reasonable number of decimal places\r\n formatted_result = f\"{result:.6f}\".rstrip('0').rstrip('.')\r\n\r\n self.status = formatted_result\r\n return [Data(data={\"result\": formatted_result})]\r\n\r\n except (SyntaxError, TypeError, KeyError) as e:\r\n error_message = f\"Invalid expression: {str(e)}\"\r\n self.status = error_message\r\n return [Data(data={\"error\": error_message})]\r\n except ZeroDivisionError:\r\n error_message = \"Error: Division by zero\"\r\n self.status = error_message\r\n return [Data(data={\"error\": error_message})]\r\n except Exception as e:\r\n error_message = f\"Error: {str(e)}\"\r\n self.status = error_message\r\n return [Data(data={\"error\": error_message})]"
|
"value": "import ast\nimport operator\nfrom typing import List\nfrom pydantic import BaseModel, Field\nfrom langflow.base.langchain_utilities.model import LCToolComponent\nfrom langflow.inputs import MessageTextInput\nfrom langflow.schema import Data\nfrom langflow.field_typing import Tool\nfrom langchain.tools import StructuredTool\n\n\nclass CalculatorToolComponent(LCToolComponent):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n icon = \"calculator\"\n name = \"CalculatorTool\"\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n ),\n ]\n\n class CalculatorToolSchema(BaseModel):\n expression: str = Field(..., description=\"The arithmetic expression to evaluate.\")\n\n def run_model(self) -> List[Data]:\n return self._evaluate_expression(self.expression)\n\n def build_tool(self) -> Tool:\n return StructuredTool.from_function(\n name=\"calculator\",\n description=\"Evaluate basic arithmetic expressions. Input should be a string containing the expression.\",\n func=self._evaluate_expression,\n args_schema=self.CalculatorToolSchema,\n )\n\n def _evaluate_expression(self, expression: str) -> List[Data]:\n try:\n # Define the allowed operators\n operators = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n def eval_expr(node):\n if isinstance(node, ast.Num):\n return node.n\n elif isinstance(node, ast.BinOp):\n return operators[type(node.op)](eval_expr(node.left), eval_expr(node.right))\n elif isinstance(node, ast.UnaryOp):\n return operators[type(node.op)](eval_expr(node.operand))\n else:\n raise TypeError(node)\n\n # Parse the expression and evaluate it\n tree = ast.parse(expression, mode=\"eval\")\n result = eval_expr(tree.body)\n\n # Format the result to a reasonable number of decimal places\n formatted_result = f\"{result:.6f}\".rstrip(\"0\").rstrip(\".\")\n\n self.status = formatted_result\n return [Data(data={\"result\": formatted_result})]\n\n except (SyntaxError, TypeError, KeyError) as e:\n error_message = f\"Invalid expression: {str(e)}\"\n self.status = error_message\n return [Data(data={\"error\": error_message})]\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return [Data(data={\"error\": error_message})]\n except Exception as e:\n error_message = f\"Error: {str(e)}\"\n self.status = error_message\n return [Data(data={\"error\": error_message})]\n"
|
||||||
},
|
},
|
||||||
"expression": {
|
"expression": {
|
||||||
"_input_type": "MessageTextInput",
|
"_input_type": "MessageTextInput",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue