Coverage for src / mcp_server_langgraph / tools / calculator_tools.py: 75%

100 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-03 00:43 +0000

1""" 

2Calculator tools for mathematical operations 

3 

4Provides basic arithmetic and evaluation tools for the agent. 

5""" 

6 

7import ast 

8import operator 

9from typing import Any 

10 

11from langchain_core.tools import tool 

12from pydantic import Field 

13 

14 

15def _safe_log(level: str, message: str, **kwargs: Any) -> None: 

16 """Safely log message, handling cases where observability isn't initialized""" 

17 try: 

18 from mcp_server_langgraph.observability.telemetry import logger 

19 

20 getattr(logger, level)(message, **kwargs) 

21 except (ImportError, RuntimeError): 

22 # Observability not available - silently skip 

23 pass 

24 

25 

26def _safe_metric(metric_name: str, value: float, attributes: dict[str, Any]) -> None: 

27 """Safely record metric, handling cases where observability isn't initialized""" 

28 try: 

29 from mcp_server_langgraph.observability.telemetry import metrics 

30 

31 getattr(metrics, metric_name).add(value, attributes) 

32 except (ImportError, RuntimeError): 

33 # Observability not available - silently skip 

34 pass 

35 

36 

37# Safe operators for calculator evaluation 

38SAFE_OPERATORS = { 

39 ast.Add: operator.add, 

40 ast.Sub: operator.sub, 

41 ast.Mult: operator.mul, 

42 ast.Div: operator.truediv, 

43 ast.Pow: operator.pow, 

44 ast.USub: operator.neg, 

45 ast.UAdd: operator.pos, 

46 ast.Mod: operator.mod, 

47} 

48 

49 

50def _safe_eval(expression: str) -> float: 

51 """ 

52 Safely evaluate a mathematical expression. 

53 

54 Args: 

55 expression: Mathematical expression (e.g., "2 + 2", "10 * 3") 

56 

57 Returns: 

58 Result of evaluation 

59 

60 Raises: 

61 ValueError: If expression contains unsafe operations 

62 SyntaxError: If expression is malformed 

63 """ 

64 

65 def _eval(node: Any) -> float: 

66 if isinstance(node, ast.Constant): # Number (Python 3.8+) 

67 if isinstance(node.value, (int, float)): 67 ↛ 70line 67 didn't jump to line 70 because the condition on line 67 was always true

68 return float(node.value) 

69 else: 

70 msg = f"Invalid constant type: {type(node.value).__name__}" 

71 raise ValueError(msg) 

72 elif isinstance(node, ast.BinOp): # Binary operation 

73 op_func = SAFE_OPERATORS.get(type(node.op)) 

74 if op_func is None: 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true

75 msg = f"Unsafe operator: {type(node.op).__name__}" 

76 raise ValueError(msg) 

77 return float(op_func(_eval(node.left), _eval(node.right))) # type: ignore[operator] 

78 elif isinstance(node, ast.UnaryOp): # Unary operation 78 ↛ 85line 78 didn't jump to line 85 because the condition on line 78 was always true

79 op_func = SAFE_OPERATORS.get(type(node.op)) 

80 if op_func is None: 80 ↛ 81line 80 didn't jump to line 81 because the condition on line 80 was never true

81 msg = f"Unsafe operator: {type(node.op).__name__}" 

82 raise ValueError(msg) 

83 return float(op_func(_eval(node.operand))) # type: ignore[operator] 

84 else: 

85 msg = f"Unsafe node type: {type(node).__name__}" 

86 raise ValueError(msg) 

87 

88 try: 

89 tree = ast.parse(expression, mode="eval") 

90 result = _eval(tree.body) 

91 return result 

92 except (ValueError, SyntaxError, ZeroDivisionError) as e: 

93 msg = f"Invalid expression: {e}" 

94 raise ValueError(msg) from e 

95 

96 

97@tool 

98def calculator(expression: str = Field(description="Mathematical expression to evaluate (e.g., '2 + 2', '10 * 3')")) -> str: 

99 """ 

100 Evaluate a mathematical expression safely. 

101 

102 Supports: +, -, *, /, **, %, and parentheses. 

103 Does NOT support: functions, variables, or imports for security. 

104 

105 Examples: 

106 - "2 + 2" → 4.0 

107 - "10 * 3 - 5" → 25.0 

108 - "(8 / 2) ** 2" → 16.0 

109 """ 

110 try: 

111 _safe_log("info", "Calculator tool invoked", extra={"expression": expression}) 

112 _safe_metric("tool_calls", 1, {"tool": "calculator"}) 

113 

114 result = _safe_eval(expression) 

115 

116 _safe_log("info", "Calculator result", extra={"expression": expression, "result": result}) 

117 return f"{result}" 

118 

119 except Exception as e: 

120 error_msg = f"Error evaluating expression '{expression}': {e}" 

121 _safe_log("error", error_msg, exc_info=True) 

122 return f"Error: {e}" 

123 

124 

125@tool 

126def add(a: float = Field(description="First number"), b: float = Field(description="Second number")) -> str: 

127 """Add two numbers together.""" 

128 try: 

129 _safe_log("info", "Add tool invoked", extra={"a": a, "b": b}) 

130 _safe_metric("tool_calls", 1, {"tool": "add"}) 

131 

132 result = a + b 

133 return f"{result}" 

134 

135 except Exception as e: 

136 _safe_log("error", f"Error in add tool: {e}", exc_info=True) 

137 return f"Error: {e}" 

138 

139 

140@tool 

141def subtract(a: float = Field(description="First number"), b: float = Field(description="Second number")) -> str: 

142 """Subtract second number from first number.""" 

143 try: 

144 _safe_log("info", "Subtract tool invoked", extra={"a": a, "b": b}) 

145 _safe_metric("tool_calls", 1, {"tool": "subtract"}) 

146 

147 result = a - b 

148 return f"{result}" 

149 

150 except Exception as e: 

151 _safe_log("error", f"Error in subtract tool: {e}", exc_info=True) 

152 return f"Error: {e}" 

153 

154 

155@tool 

156def multiply(a: float = Field(description="First number"), b: float = Field(description="Second number")) -> str: 

157 """Multiply two numbers together.""" 

158 try: 

159 _safe_log("info", "Multiply tool invoked", extra={"a": a, "b": b}) 

160 _safe_metric("tool_calls", 1, {"tool": "multiply"}) 

161 

162 result = a * b 

163 return f"{result}" 

164 

165 except Exception as e: 

166 _safe_log("error", f"Error in multiply tool: {e}", exc_info=True) 

167 return f"Error: {e}" 

168 

169 

170@tool 

171def divide(a: float = Field(description="Numerator"), b: float = Field(description="Denominator (cannot be zero)")) -> str: 

172 """Divide first number by second number.""" 

173 try: 

174 _safe_log("info", "Divide tool invoked", extra={"a": a, "b": b}) 

175 _safe_metric("tool_calls", 1, {"tool": "divide"}) 

176 

177 if b == 0: 

178 return "Error: Division by zero" 

179 

180 result = a / b 

181 return f"{result}" 

182 

183 except Exception as e: 

184 _safe_log("error", f"Error in divide tool: {e}", exc_info=True) 

185 return f"Error: {e}"