Repository: huggingface/smolagents

Issue:
Import of matplotlib.pyplot is not allowed.
It would be nice if you could add this import so the plot outputted can be saved as an image for example. This could turn the code Agent into a data visualisation Agent. 
No module named "_gdbm"
Code:
qwen_model = HfApiModel("Qwen/Qwen2.5-Coder-32B-Instruct", token = hf_token)
data_analyst_agent_qwen = CodeAgent(
    tools=[],
    model=qwen_model,
    additional_authorized_imports=["numpy", "pandas", "matplotlib.pyplot", "seaborn"],
    max_steps=10,
)
data_analyst_agent_qwen.run(data_analyst_prompt, additional_args = dict(source_file_path = input_file_path))

Result:
Error when smolagent is executing the following code during one of the steps. This code is generated by the llm during step 1. The following code runs without any error when I run it in a separate notebook. Logs are not showing any additional information. Error message: No module named "_gdbm". The error seems to be generated by the code for the plots. Agent works without any issues if I "EXCLUDE" the requirement about generating charts.

import matplotlib.pyplot as plt                                                                                  
  import seaborn as sns                                                                                            
                                                                                                                   
                          
  plt.figure(figsize=(15, 10))                                                                                     
                                                                                                                   
                                                                     
  plt.subplot(2, 2, 1)                                                                                             
  sns.boxplot(x='Product_3', y='Stroke_Length', data=df)                                                           
  plt.title('Distribution of Stroke_Length by Product_3')                                                          
  plt.xticks(rotation=45)                                                                                          
                                                                                                                   
                                                           
  plt.subplot(2, 2, 2)                                                                                             
  sns.boxplot(x='Cylinder_Function', y='Closed_Length_mm', data=df)                                                
  plt.title('Distribution of Closed_Length_mm by Cylinder_Function')                                               
  plt.xticks(rotation=90)                                                                                          
                                                                                                                   
                                                                    
  plt.subplot(2, 2, 3)                                                                                             
  sns.boxplot(x='Type_2', y='Attributes_Rod_ID', data=df)                                                          
  plt.title('Distribution of Attributes_Rod_ID by Type_2')                                                         
                                                                                                                   
                                                             
  plt.subplot(2, 2, 4)                                                                                             
  sns.boxplot(x='Type_2', y='Attributes_Internal_Bore', data=df)                                                   
  plt.title('Distribution of Attributes_Internal_Bore by Type_2')                                                  
                                                                                                                   
  plt.tight_layout()                                                                                               
  plt.show()                                                                                                       
                                                                                                                   
                                                                           
  question1 = "What is the average Stroke_Length for each Product_3 category?"                                     
  question2 = "What is the maximum Closed_Length_mm for each Cylinder_Function category?"                          
  question3 = "What is the average Attributes_Rod_ID for each Type_2 category?"                                    
                                                                                                                   
                                                                                        
  answer1 = df.groupby('Product_3')['Stroke_Length'].mean()                                                        
  answer2 = df.groupby('Cylinder_Function')['Closed_Length_mm'].max()                                              
  answer3 = df.groupby('Type_2')['Attributes_Rod_ID'].mean()                                                       
                                                                                                                   
  print("Question 1:", question1)                                                                                  
  print("Answer 1:", answer1)                                                                                      
  print("\nQuestion 2:", question2)                                                                                
  print("Answer 2:", answer2)                                                                                      
  print("\nQuestion 3:", question3)                                                                                
  print("Answer 3:", answer3)        





Discussion:
You can customize which libraries can be imported by passing `additional_authorized_imports`.
You can add which libraries can be imported in `CodeAgent` by passing the libraries to `additional_authorized_imports` 

Example:
```
from smolagents import CodeAgent, HfApiModel

agent = CodeAgent(
    tools=[],
    model=HfApiModel(),
    additional_authorized_imports=["matplotlib"], # or any other you want 
)

agent_output = agent.run(
    "how to plot a graph in python"
)  
```

just make sure to first install `matplotlib` i-e `pip install matplotlib`
see https://github.com/huggingface/smolagents/issues/339 

I think this issue is not related to the `smolagents` library, but to your local Python environment.
@albertvillanova 
I am not certain that the problem is with the local python environment. If I copy the code generated by the llm and run it in a notebook within my local environment, the code works just fine. However, when the agent is trying to run it within the python-interpretor, it encounters the "Missing _gdbm module" error. Can you pls provide some suggestions to debug this further?
I think the `LocalPythonInterpreter` class doesn't interact with Python the same way a typical system or virtual environment does. It’s more of a parser and executor for Python code, lacking the dynamic runtime configuration you'd expect from a full Python environment.

Modules like `_gdbm` (which `matplotlib` relies on) need specific shared libraries and environment variables, which might not be properly initialized in the custom interpreter. This could be a tradeoff in smolagents, where the focus is on safely executing untrusted code—possibly at the cost of more complex functionality like rendering a GUI, which `_gdbm` might be involved in and requires system tools.

Also some code insights, future fixes (?):

- The `# TODO: assert self.authorized imports are all installed locally` in the `LocalPythonInterpreter` class initialization shows that there’s no mechanism to check if the imports in `self.authorized_imports` are actually available in the current runtime, leading to  runtime errors, like the one discussed.

- There’s also no way to ensure the interpreter inherits critical configurations, like `sys.path` or environment variables, from the actual Python runtime. This is what I'm referring about for the design choice, that seems to prioritize security and predictability, especially when running untrusted code. Which is a good thing imo.
I have the same issue on anaconda on Windows 10. I can import locally matplotlib and plot some plots, but it is impossible to do via smolagents as described above. 
I don't get the real reason for the problem, but I was able to locate the error via the code:
```
import smolagents
inter = smolagents.LocalPythonInterpreter(additional_authorized_imports=[ 'pandas', 'matplotlib', 'numpy'], tools={})
inter('import matplotlib.pyplot as plt', {})
```
This results in:
```
ModuleNotFoundError                       Traceback (most recent call last)
File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\smolagents\local_python_executor.py:1294](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/smolagents/local_python_executor.py#line=1293), in evaluate_python_code(code, static_tools, custom_tools, state, authorized_imports, max_print_outputs_length)
   1293 for node in expression.body:
-> 1294     result = evaluate_ast(node, state, static_tools, custom_tools, authorized_imports)
   1295 state["print_outputs"] = truncate_content(PRINT_OUTPUTS, max_length=max_print_outputs_length)

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\smolagents\local_python_executor.py:1208](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/smolagents/local_python_executor.py#line=1207), in evaluate_ast(expression, state, static_tools, custom_tools, authorized_imports)
   1207 elif isinstance(expression, (ast.Import, ast.ImportFrom)):
-> 1208     return import_modules(expression, state, authorized_imports)
   1209 elif isinstance(expression, ast.ClassDef):

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\smolagents\local_python_executor.py:1008](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/smolagents/local_python_executor.py#line=1007), in import_modules(expression, state, authorized_imports)
   1007     raw_module = import_module(alias.name)
-> 1008     state[alias.asname or alias.name] = get_safe_module(raw_module, dangerous_patterns)
   1009 else:

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\smolagents\local_python_executor.py:966](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/smolagents/local_python_executor.py#line=965), in get_safe_module(unsafe_module, dangerous_patterns, visited)
    965 if isinstance(attr_value, ModuleType):
--> 966     attr_value = get_safe_module(attr_value, dangerous_patterns, visited=visited)
    968 setattr(safe_module, attr_name, attr_value)

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\smolagents\local_python_executor.py:966](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/smolagents/local_python_executor.py#line=965), in get_safe_module(unsafe_module, dangerous_patterns, visited)
    965 if isinstance(attr_value, ModuleType):
--> 966     attr_value = get_safe_module(attr_value, dangerous_patterns, visited=visited)
    968 setattr(safe_module, attr_name, attr_value)

    [... skipping similar frames: get_safe_module at line 966 (5 times)]

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\smolagents\local_python_executor.py:966](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/smolagents/local_python_executor.py#line=965), in get_safe_module(unsafe_module, dangerous_patterns, visited)
    965 if isinstance(attr_value, ModuleType):
--> 966     attr_value = get_safe_module(attr_value, dangerous_patterns, visited=visited)
    968 setattr(safe_module, attr_name, attr_value)

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\smolagents\local_python_executor.py:962](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/smolagents/local_python_executor.py#line=961), in get_safe_module(unsafe_module, dangerous_patterns, visited)
    960     continue
--> 962 attr_value = getattr(unsafe_module, attr_name)
    964 # Recursively process nested modules, passing visited set

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\six.py:97](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/six.py#line=96), in _LazyDescr.__get__(self, obj, tp)
     96 def __get__(self, obj, tp):
---> 97     result = self._resolve()
     98     setattr(obj, self.name, result)  # Invokes __set__.

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\six.py:120](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/six.py#line=119), in MovedModule._resolve(self)
    119 def _resolve(self):
--> 120     return _import_module(self.mod)

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\six.py:87](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/six.py#line=86), in _import_module(name)
     86 """Import module, returning the module after the last dot."""
---> 87 __import__(name)
     88 return sys.modules[name]

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\dbm\gnu.py:3](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/dbm/gnu.py#line=2)
      1 """Provide the _gdbm module as a dbm submodule."""
----> 3 from _gdbm import *

ModuleNotFoundError: No module named '_gdbm'

During handling of the above exception, another exception occurred:

InterpreterError                          Traceback (most recent call last)
Cell In[3], line 4
      2 inter = smolagents.LocalPythonInterpreter(additional_authorized_imports=[ 'pandas', 'matplotlib', 'numpy'], tools={})
      3 inter('import numpy as np', {})
----> 4 inter('import matplotlib.pyplot as plt', {})

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\smolagents\local_python_executor.py:1334](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/smolagents/local_python_executor.py#line=1333), in LocalPythonInterpreter.__call__(self, code_action, additional_variables)
   1332 def __call__(self, code_action: str, additional_variables: Dict) -> Tuple[Any, str, bool]:
   1333     self.state.update(additional_variables)
-> 1334     output, is_final_answer = evaluate_python_code(
   1335         code_action,
   1336         static_tools=self.static_tools,
   1337         custom_tools=self.custom_tools,
   1338         state=self.state,
   1339         authorized_imports=self.authorized_imports,
   1340         max_print_outputs_length=self.max_print_outputs_length,
   1341     )
   1342     logs = self.state["print_outputs"]
   1343     return output, logs, is_final_answer

File [D:\Documents\anaconda3\envs\new_sandbox\Lib\site-packages\smolagents\local_python_executor.py:1308](file:///D:/Documents/anaconda3/envs/new_sandbox/Lib/site-packages/smolagents/local_python_executor.py#line=1307), in evaluate_python_code(code, static_tools, custom_tools, state, authorized_imports, max_print_outputs_length)
   1304 error_msg = truncate_content(PRINT_OUTPUTS, max_length=max_print_outputs_length)
   1305 error_msg = (
   1306     f"Code execution failed at line '{ast.get_source_segment(code, node)}' due to: {exception_type}:{str(e)}"
   1307 )
-> 1308 raise InterpreterError(error_msg)

InterpreterError: Code execution failed at line 'import matplotlib.pyplot as plt' due to: ModuleNotFoundError:No module named '_gdbm'
```

This lead me to an idea of adding `dbm` to `dangerous_modules`,  which solved the issue.  There can be a better solution, perhaps. Tagging @aymeric-roucher 
`smolagents` traverses all the imports of allowed dependencies recursively.

`matplotlib.pyplot` depends on `six` which heavily make use of lazy, conditional imports to handle different python versions (py2/py3)

`smolagents` due to l948 and l953, triggers lazy imports by accessing the `dbm_gnu` attribute which exists in `dir(unsafe_module)` (where `unsafe_module` is the `six` module) but not supposed to get accessed yet

```python
    for attr_name in dir(unsafe_module):  # attr_name == dbm_genu
...
        attr_value = getattr(unsafe_module, attr_name)  # raise!

```

I will try to propose a fix in the evening, if not already managed :)

In the meantime, @Bilokin's work around is 💯  and should monkey patch the issue @prasiyer 



Patch:
diff --git a/src/smolagents/local_python_executor.py b/src/smolagents/local_python_executor.py
index 6f3e1eb8d..7288640df 100644
--- a/src/smolagents/local_python_executor.py
+++ b/src/smolagents/local_python_executor.py
@@ -18,6 +18,7 @@
 import builtins
 import difflib
 import inspect
+import logging
 import math
 import re
 from collections.abc import Mapping
@@ -31,6 +32,9 @@
 from .utils import BASE_BUILTIN_MODULES, truncate_content
 
 
+logger = logging.getLogger(__name__)
+
+
 class InterpreterError(ValueError):
     """
     An error raised when the interpreter cannot evaluate a Python expression, due to syntax error or unsupported
@@ -960,10 +964,17 @@ def get_safe_module(raw_module, dangerous_patterns, authorized_imports, visited=
             pattern in raw_module.__name__.split(".") + [attr_name] and pattern not in authorized_imports
             for pattern in dangerous_patterns
         ):
+            logger.info(f"Skipping dangerous attribute {raw_module.__name__}.{attr_name}")
             continue
 
-        attr_value = getattr(raw_module, attr_name)
-
+        try:
+            attr_value = getattr(raw_module, attr_name)
+        except ImportError as e:
+            # lazy / dynamic loading module -> INFO log and skip
+            logger.info(
+                f"Skipping import error while copying {raw_module.__name__}.{attr_name}: {type(e).__name__} - {e}"
+            )
+            continue
         # Recursively process nested modules, passing visited set
         if isinstance(attr_value, ModuleType):
             attr_value = get_safe_module(attr_value, dangerous_patterns, authorized_imports, visited=visited)


Test patch:
diff --git a/tests/test_python_interpreter.py b/tests/test_python_interpreter.py
index a44fbeaff..ad8b99d41 100644
--- a/tests/test_python_interpreter.py
+++ b/tests/test_python_interpreter.py
@@ -13,6 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import types
 import unittest
 from textwrap import dedent
 
@@ -24,6 +25,7 @@
     InterpreterError,
     evaluate_python_code,
     fix_final_answer_code,
+    get_safe_module,
 )
 
 
@@ -1069,3 +1071,23 @@ def __{operator_name}__(self, other):
     state = {}
     result, _ = evaluate_python_code(code, {}, state=state)
     assert result == expected_result
+
+
+def test_get_safe_module_handle_lazy_imports():
+    class FakeModule(types.ModuleType):
+        def __init__(self, name):
+            super().__init__(name)
+            self.non_lazy_attribute = "ok"
+
+        def __getattr__(self, name):
+            if name == "lazy_attribute":
+                raise ImportError("lazy import failure")
+            return super().__getattr__(name)
+
+        def __dir__(self):
+            return super().__dir__() + ["lazy_attribute"]
+
+    fake_module = FakeModule("fake_module")
+    safe_module = get_safe_module(fake_module, dangerous_patterns=[], authorized_imports=set())
+    assert not hasattr(safe_module, "lazy_attribute")
+    assert getattr(safe_module, "non_lazy_attribute") == "ok"