import esprima from typing import Any, Callable, Dict, Optional import types import ast import inspect from smolagents.tools import Tool class JavaScriptMethodDispatcher(Tool): name = "javascript_as_python_caller" description = "turns java script code blocks into callable python functions" inputs = {'jsContent':{'type':'string', 'description':'content of a regular java script code block of a function definition'}} output_type = "callable" def __init__(self): self._method_cache: Dict[str, Callable] = {} def register_js_method(self, js_code: str, method_name: Optional[str] = None) -> Callable: """ Convert JavaScript code to a Python callable and register it in the dispatcher. Args: js_code (str): JavaScript code containing the function method_name (str, optional): Name to register the method under. If None, tries to extract from the JS code. Returns: Callable: Python function that can be called directly """ try: # Parse JavaScript code parsed = esprima.parseScript(js_code) # Find the function declaration func_decl = None for node in parsed.body: if node.type == 'FunctionDeclaration': func_decl = node break if not func_decl: raise ValueError("No function declaration found in JavaScript code") # Get the method name if method_name is None: method_name = func_decl.id.name # Convert JavaScript parameters to Python params = [param.name for param in func_decl.params] # Convert the function body body_lines = self._convert_js_body(func_decl.body) # Create the Python function source py_source = f"def {method_name}({', '.join(params)}):\n" py_source += "\n".join(f" {line}" for line in body_lines) # Create function namespace namespace = { 'print': print, # Common built-ins 'str': str, 'int': int, 'float': float, 'bool': bool, 'list': list, 'dict': dict, 'None': None, 'True': True, 'False': False } # Compile and execute the Python code compiled_code = compile(py_source, '', 'exec') exec(compiled_code, namespace) # Get the compiled function py_func = namespace[method_name] # Store in cache self._method_cache[method_name] = py_func return py_func except Exception as e: raise ValueError(f"Failed to convert JavaScript method: {str(e)}") def get_method(self, method_name: str) -> Optional[Callable]: """ Get a registered method by name. Args: method_name (str): Name of the registered method Returns: Optional[Callable]: The registered Python function, or None if not found """ return self._method_cache.get(method_name) def call_method(self, method_name: str, *args, **kwargs) -> Any: """ Call a registered method by name with given arguments. Args: method_name (str): Name of the registered method *args: Positional arguments to pass to the method **kwargs: Keyword arguments to pass to the method Returns: Any: Result of the method call Raises: ValueError: If method is not found or call fails """ method = self.get_method(method_name) if method is None: raise ValueError(f"Method '{method_name}' not found") try: return method(*args, **kwargs) except Exception as e: raise ValueError(f"Failed to call method '{method_name}': {str(e)}") def _convert_js_body(self, body_node: Any) -> list[str]: """Convert JavaScript function body to Python code lines.""" # This is a simplified conversion - you'd want to expand this # based on your specific needs lines = [] # Handle different types of statements if body_node.type == 'BlockStatement': for statement in body_node.body: if statement.type == 'ReturnStatement': return_value = self._convert_js_expression(statement.argument) lines.append(f"return {return_value}") elif statement.type == 'ExpressionStatement': expr = self._convert_js_expression(statement.expression) lines.append(expr) elif statement.type == 'IfStatement': condition = self._convert_js_expression(statement.test) lines.append(f"if {condition}:") then_lines = self._convert_js_body(statement.consequent) lines.extend(f" {line}" for line in then_lines) if statement.alternate: lines.append("else:") else_lines = self._convert_js_body(statement.alternate) lines.extend(f" {line}" for line in else_lines) return lines def _convert_js_expression(self, node: Any) -> str: """Convert JavaScript expression to Python code string.""" if node.type == 'BinaryExpression': left = self._convert_js_expression(node.left) right = self._convert_js_expression(node.right) op = node.operator # Convert JavaScript operators to Python op_map = { '===': '==', '!==': '!=', '&&': 'and', '||': 'or' } op = op_map.get(op, op) return f"({left} {op} {right})" elif node.type == 'Literal': if isinstance(node.value, str): return f"'{node.value}'" return str(node.value) elif node.type == 'Identifier': # Handle JavaScript built-ins js_to_py = { 'undefined': 'None', 'null': 'None', 'true': 'True', 'false': 'False' } return js_to_py.get(node.name, node.name) elif node.type == 'CallExpression': func = self._convert_js_expression(node.callee) args = [self._convert_js_expression(arg) for arg in node.arguments] # Handle special cases if func == 'console.log': func = 'print' return f"{func}({', '.join(args)})" raise ValueError(f"Unsupported expression type: {node.type}") def forward(self, jsContent: str) -> callable: return self.register_js_method(jsContent)