Improve python executor's error logging (#275)
* Improve python executor's error logging
This commit is contained in:
		
							parent
							
								
									3c18d4d588
								
							
						
					
					
						commit
						7a91123729
					
				|  | @ -972,16 +972,8 @@ class CodeAgent(MultiStepAgent): | |||
|                 ] | ||||
|             observation += "Execution logs:\n" + execution_logs | ||||
|         except Exception as e: | ||||
|             if isinstance(e, SyntaxError): | ||||
|                 error_msg = ( | ||||
|                     f"Code execution failed on line {e.lineno} due to: {type(e).__name__}\n" | ||||
|                     f"{e.text}" | ||||
|                     f"{' ' * (e.offset or 0)}^\n" | ||||
|                     f"Error: {str(e)}" | ||||
|                 ) | ||||
|             else: | ||||
|                 error_msg = str(e) | ||||
|             if "Import of " in str(e) and " is not allowed" in str(e): | ||||
|             error_msg = str(e) | ||||
|             if "Import of " in error_msg and " is not allowed" in error_msg: | ||||
|                 self.logger.log( | ||||
|                     "[bold red]Warning to user: Code execution failed due to an unauthorized import - Consider passing said import under `additional_authorized_imports` when initializing your CodeAgent.", | ||||
|                     level=LogLevel.INFO, | ||||
|  |  | |||
|  | @ -554,7 +554,7 @@ def evaluate_call( | |||
|             func = ERRORS[func_name] | ||||
|         else: | ||||
|             raise InterpreterError( | ||||
|                 f"It is not permitted to evaluate other functions than the provided tools or functions defined in previous code (tried to execute {call.func.id})." | ||||
|                 f"It is not permitted to evaluate other functions than the provided tools or functions defined/imported in previous code (tried to execute {call.func.id})." | ||||
|             ) | ||||
| 
 | ||||
|     elif isinstance(call.func, ast.Subscript): | ||||
|  | @ -1245,7 +1245,16 @@ def evaluate_python_code( | |||
|             updated by this function to contain all variables as they are evaluated. | ||||
|             The print outputs will be stored in the state under the key 'print_outputs'. | ||||
|     """ | ||||
|     expression = ast.parse(code) | ||||
|     try: | ||||
|         expression = ast.parse(code) | ||||
|     except SyntaxError as e: | ||||
|         raise InterpreterError( | ||||
|             f"Code execution failed on line {e.lineno} due to: {type(e).__name__}\n" | ||||
|             f"{e.text}" | ||||
|             f"{' ' * (e.offset or 0)}^\n" | ||||
|             f"Error: {str(e)}" | ||||
|         ) | ||||
| 
 | ||||
|     if state is None: | ||||
|         state = {} | ||||
|     if static_tools is None: | ||||
|  | @ -1273,10 +1282,13 @@ def evaluate_python_code( | |||
|         state["print_outputs"] = truncate_content(PRINT_OUTPUTS, max_length=max_print_outputs_length) | ||||
|         is_final_answer = True | ||||
|         return e.value, is_final_answer | ||||
|     except InterpreterError as e: | ||||
|         msg = truncate_content(PRINT_OUTPUTS, max_length=max_print_outputs_length) | ||||
|         msg += f"Code execution failed at line '{ast.get_source_segment(code, node)}' because of the following error:\n{e}" | ||||
|         raise InterpreterError(msg) | ||||
|     except Exception as e: | ||||
|         exception_type = type(e).__name__ | ||||
|         error_msg = truncate_content(PRINT_OUTPUTS, max_length=max_print_outputs_length) | ||||
|         error_msg = ( | ||||
|             f"Code execution failed at line '{ast.get_source_segment(code, node)}' due to: {exception_type}:{str(e)}" | ||||
|         ) | ||||
|         raise InterpreterError(error_msg) | ||||
| 
 | ||||
| 
 | ||||
| class LocalPythonInterpreter: | ||||
|  |  | |||
|  | @ -168,7 +168,9 @@ class Tool: | |||
|                     "Tool's 'forward' method should take 'self' as its first argument, then its next arguments should match the keys of tool attribute 'inputs'." | ||||
|                 ) | ||||
| 
 | ||||
|             json_schema = _convert_type_hints_to_json_schema(self.forward) | ||||
|             json_schema = _convert_type_hints_to_json_schema( | ||||
|                 self.forward | ||||
|             )  # This function will raise an error on missing docstrings, contrary to get_json_schema | ||||
|             for key, value in self.inputs.items(): | ||||
|                 if "nullable" in value: | ||||
|                     assert key in json_schema and "nullable" in json_schema[key], ( | ||||
|  | @ -885,6 +887,16 @@ class ToolCollection: | |||
|             yield cls(tools) | ||||
| 
 | ||||
| 
 | ||||
| def get_tool_json_schema(tool_function): | ||||
|     tool_json_schema = get_json_schema(tool_function)["function"] | ||||
|     tool_parameters = tool_json_schema["parameters"] | ||||
|     inputs_schema = tool_parameters["properties"] | ||||
|     for input_name in inputs_schema: | ||||
|         if "required" not in tool_parameters or input_name not in tool_parameters["required"]: | ||||
|             inputs_schema[input_name]["nullable"] = True | ||||
|     return tool_json_schema | ||||
| 
 | ||||
| 
 | ||||
| def tool(tool_function: Callable) -> Tool: | ||||
|     """ | ||||
|     Converts a function into an instance of a Tool subclass. | ||||
|  | @ -893,12 +905,19 @@ def tool(tool_function: Callable) -> Tool: | |||
|         tool_function: Your function. Should have type hints for each input and a type hint for the output. | ||||
|         Should also have a docstring description including an 'Args:' part where each argument is described. | ||||
|     """ | ||||
|     parameters = get_json_schema(tool_function)["function"] | ||||
|     if "return" not in parameters: | ||||
|     tool_json_schema = get_tool_json_schema(tool_function) | ||||
|     if "return" not in tool_json_schema: | ||||
|         raise TypeHintParsingException("Tool return type not found: make sure your function has a return type hint!") | ||||
| 
 | ||||
|     class SimpleTool(Tool): | ||||
|         def __init__(self, name, description, inputs, output_type, function): | ||||
|         def __init__( | ||||
|             self, | ||||
|             name: str, | ||||
|             description: str, | ||||
|             inputs: Dict[str, Dict[str, str]], | ||||
|             output_type: str, | ||||
|             function: Callable, | ||||
|         ): | ||||
|             self.name = name | ||||
|             self.description = description | ||||
|             self.inputs = inputs | ||||
|  | @ -907,10 +926,10 @@ def tool(tool_function: Callable) -> Tool: | |||
|             self.is_initialized = True | ||||
| 
 | ||||
|     simple_tool = SimpleTool( | ||||
|         parameters["name"], | ||||
|         parameters["description"], | ||||
|         parameters["parameters"]["properties"], | ||||
|         parameters["return"]["type"], | ||||
|         name=tool_json_schema["name"], | ||||
|         description=tool_json_schema["description"], | ||||
|         inputs=tool_json_schema["parameters"]["properties"], | ||||
|         output_type=tool_json_schema["return"]["type"], | ||||
|         function=tool_function, | ||||
|     ) | ||||
|     original_signature = inspect.signature(tool_function) | ||||
|  |  | |||
|  | @ -332,7 +332,7 @@ class AgentTests(unittest.TestCase): | |||
|         output = agent.run("What is 2 multiplied by 3.6452?") | ||||
|         assert isinstance(output, AgentText) | ||||
|         assert output == "got an error" | ||||
|         assert "Code execution failed at line 'print = 2' because of" in str(agent.logs) | ||||
|         assert "Code execution failed at line 'print = 2' due to: InterpreterError" in str(agent.logs) | ||||
| 
 | ||||
|     def test_code_agent_syntax_error_show_offending_lines(self): | ||||
|         agent = CodeAgent(tools=[PythonInterpreterTool()], model=fake_code_model_syntax_error) | ||||
|  | @ -426,7 +426,7 @@ class AgentTests(unittest.TestCase): | |||
|         with console.capture() as capture: | ||||
|             agent.run("Count to 3") | ||||
|         str_output = capture.get() | ||||
|         assert "import under `additional_authorized_imports`" in str_output | ||||
|         assert "Consider passing said import under" in str_output.replace("\n", "") | ||||
| 
 | ||||
|     def test_multiagents(self): | ||||
|         class FakeModelMultiagentsManagerAgent: | ||||
|  |  | |||
|  | @ -630,12 +630,9 @@ counts += 1""" | |||
|         assert "Cannot add non-list value 1 to a list." in str(e) | ||||
| 
 | ||||
|     def test_error_highlights_correct_line_of_code(self): | ||||
|         code = """# Ok this is a very long code | ||||
| # It has many commented lines | ||||
| a = 1 | ||||
|         code = """a = 1 | ||||
| b = 2 | ||||
| 
 | ||||
| # Here is another piece | ||||
| counts = [1, 2, 3] | ||||
| counts += 1 | ||||
| b += 1""" | ||||
|  | @ -643,12 +640,22 @@ b += 1""" | |||
|             evaluate_python_code(code, BASE_PYTHON_TOOLS, state={}) | ||||
|         assert "Code execution failed at line 'counts += 1" in str(e) | ||||
| 
 | ||||
|     def test_error_type_returned_in_function_call(self): | ||||
|         code = """def error_function(): | ||||
|     raise ValueError("error") | ||||
| 
 | ||||
| error_function()""" | ||||
|         with pytest.raises(InterpreterError) as e: | ||||
|             evaluate_python_code(code) | ||||
|         assert "error" in str(e) | ||||
|         assert "ValueError" in str(e) | ||||
| 
 | ||||
|     def test_assert(self): | ||||
|         code = """ | ||||
| assert 1 == 1 | ||||
| assert 1 == 2 | ||||
| """ | ||||
|         with pytest.raises(AssertionError) as e: | ||||
|         with pytest.raises(InterpreterError) as e: | ||||
|             evaluate_python_code(code, BASE_PYTHON_TOOLS, state={}) | ||||
|         assert "1 == 2" in str(e) and "1 == 1" not in str(e) | ||||
| 
 | ||||
|  | @ -845,6 +852,13 @@ shift_intervals | |||
|         result, _ = evaluate_python_code(code, {"print": print, "map": map}, state={}) | ||||
|         assert result == {"Worker A": "8:00 pm", "Worker B": "11:45 am"} | ||||
| 
 | ||||
|     def test_syntax_error_points_error(self): | ||||
|         code = "a = ;" | ||||
|         with pytest.raises(InterpreterError) as e: | ||||
|             evaluate_python_code(code) | ||||
|         assert "SyntaxError" in str(e) | ||||
|         assert "     ^" in str(e) | ||||
| 
 | ||||
|     def test_fix_final_answer_code(self): | ||||
|         test_cases = [ | ||||
|             ( | ||||
|  | @ -890,18 +904,16 @@ shift_intervals | |||
| 
 | ||||
|         # Import of whitelisted modules should succeed but dangerous submodules should not exist | ||||
|         code = "import random;random._os.system('echo bad command passed')" | ||||
|         with pytest.raises(AttributeError) as e: | ||||
|         with pytest.raises(InterpreterError) as e: | ||||
|             evaluate_python_code(code) | ||||
|         assert "module 'random' has no attribute '_os'" in str(e) | ||||
|         assert "AttributeError:module 'random' has no attribute '_os'" in str(e) | ||||
| 
 | ||||
|         code = "import doctest;doctest.inspect.os.system('echo bad command passed')" | ||||
|         with pytest.raises(AttributeError): | ||||
|         with pytest.raises(InterpreterError): | ||||
|             evaluate_python_code(code, authorized_imports=["doctest"]) | ||||
| 
 | ||||
|     def test_close_matches_subscript(self): | ||||
|         code = 'capitals = {"Czech Republic": "Prague", "Monaco": "Monaco", "Bhutan": "Thimphu"};capitals["Butan"]' | ||||
|         with pytest.raises(Exception) as e: | ||||
|             evaluate_python_code(code) | ||||
|         assert "Maybe you meant one of these indexes instead" in str( | ||||
|             e | ||||
|         ) and "['Bhutan']" in str(e).replace("\\", "") | ||||
|         assert "Maybe you meant one of these indexes instead" in str(e) and "['Bhutan']" in str(e).replace("\\", "") | ||||
|  |  | |||
|  | @ -374,6 +374,20 @@ class ToolTests(unittest.TestCase): | |||
|             GetWeatherTool3() | ||||
|         assert "Nullable" in str(e) | ||||
| 
 | ||||
|     def test_tool_default_parameters_is_nullable(self): | ||||
|         @tool | ||||
|         def get_weather(location: str, celsius: bool = False) -> str: | ||||
|             """ | ||||
|             Get weather in the next days at given location. | ||||
| 
 | ||||
|             Args: | ||||
|                 location: the location | ||||
|                 celsius: is the temperature given in celsius | ||||
|             """ | ||||
|             return "The weather is UNGODLY with torrential rains and temperatures below -10°C" | ||||
| 
 | ||||
|         assert get_weather.inputs["celsius"]["nullable"] | ||||
| 
 | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def mock_server_parameters(): | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue