Repository: microsoft/autogen
Pull request: set use_docker to default to True
## Why are these changes needed?

<!-- Please give a short summary of the change and the problem this solves. -->

## Related issue number

Resolves #1103 

Comments:

- If we are setting `use_docker` to default to `True` then `docker` should be a required dependency
- It is more user friendly to check and warn for docker running/not running when the agent is setup rather than when it tries to execute, the message gets lost in the wall of text that is outputted 


TODOs:

- ~~documentation fixes after #1111 is merged~~
- check that this works with sample app

## Checks

- [x] I've included any doc changes needed for https://microsoft.github.io/autogen/. See https://microsoft.github.io/autogen/docs/Contribute#documentation to build and test documentation locally.
- [x] I've added tests (if relevant) corresponding to the changes introduced in this PR.
- [x] I've made sure all auto checks have passed.


Problem statement:
[Feature Request]: Set use_docker to True if unspecified
### Is your feature request related to a problem? Please describe.

Currently we output a warning if use_docker is not specified and docker is not installed. It will be more secure if we set the default value to True.

### Describe the solution you'd like

[ ] Change the default value in `execute_code` to True
[ ] Make sure instructions for using docker is complete and visible (e.g., via a link) in every example (README, docs, notebooks)
[ ] Add a comment that to disable docker use_docker must be explicitly set to False in every example code snippet involving code execution
[ ] Check the sample app to make sure it works after these changes.

### Additional context

_No response_

Hints from issue discussion:

Commit message: set docker env flag for contrib tests

Patch to review:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2f1af9ab744c..f296a7971608 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -41,6 +41,14 @@ jobs:
           pip install -e .
           python -c "import autogen"
           pip install -e. pytest mock
+      - name: Set AUTOGEN_USE_DOCKER based on OS
+        shell: bash
+        run: |
+          if [[ ${{ matrix.os }} == windows-2019 ]]; then
+            echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV
+          elif [[ ${{ matrix.os }} == macos-latest ]]; then
+            echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV
+          fi
       - name: Test with pytest
         if: matrix.python-version != '3.10'
         run: |
diff --git a/.github/workflows/contrib-tests.yml b/.github/workflows/contrib-tests.yml
index 27a616b77190..d28b4dbb0402 100644
--- a/.github/workflows/contrib-tests.yml
+++ b/.github/workflows/contrib-tests.yml
@@ -192,6 +192,14 @@ jobs:
       - name: Install packages and dependencies for LMM
         run: |
           pip install -e .[lmm]
+      - name: Set AUTOGEN_USE_DOCKER based on OS
+        shell: bash
+        run: |
+          if [[ ${{ matrix.os }} == windows-2019 ]]; then
+            echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV
+          elif [[ ${{ matrix.os }} == macos-latest ]]; then
+            echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV
+          fi
       - name: Test LMM and LLaVA
         run: |
           pytest test/agentchat/contrib/test_img_utils.py test/agentchat/contrib/test_lmm.py test/agentchat/contrib/test_llava.py --skip-openai
diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py
index 1b08ade80ebf..92577d55f601 100644
--- a/autogen/agentchat/conversable_agent.py
+++ b/autogen/agentchat/conversable_agent.py
@@ -9,7 +9,17 @@
 from typing import Any, Awaitable, Callable, Dict, List, Literal, Optional, Tuple, Type, TypeVar, Union
 
 from .. import OpenAIWrapper
-from ..code_utils import DEFAULT_MODEL, UNKNOWN, content_str, execute_code, extract_code, infer_lang
+from ..code_utils import (
+    DEFAULT_MODEL,
+    UNKNOWN,
+    content_str,
+    check_use_docker,
+    decide_use_docker,
+    execute_code,
+    extract_code,
+    infer_lang,
+)
+
 from ..function_utils import get_function_schema, load_basemodels_if_needed, serialize_to_str
 from .agent import Agent
 from .._pydantic import model_dump
@@ -128,6 +138,13 @@ def __init__(
         self._code_execution_config: Union[Dict, Literal[False]] = (
             {} if code_execution_config is None else code_execution_config
         )
+
+        if isinstance(self._code_execution_config, Dict):
+            use_docker = self._code_execution_config.get("use_docker", None)
+            use_docker = decide_use_docker(use_docker)
+            check_use_docker(use_docker)
+            self._code_execution_config["use_docker"] = use_docker
+
         self.human_input_mode = human_input_mode
         self._max_consecutive_auto_reply = (
             max_consecutive_auto_reply if max_consecutive_auto_reply is not None else self.MAX_CONSECUTIVE_AUTO_REPLY
diff --git a/autogen/code_utils.py b/autogen/code_utils.py
index 299a39fba9d0..746237d9a855 100644
--- a/autogen/code_utils.py
+++ b/autogen/code_utils.py
@@ -16,6 +16,7 @@
 except ImportError:
     docker = None
 
+SENTINEL = object()
 DEFAULT_MODEL = "gpt-4"
 FAST_MODEL = "gpt-3.5-turbo"
 # Regular expression for finding a code block
@@ -224,12 +225,76 @@ def _cmd(lang):
     raise NotImplementedError(f"{lang} not recognized in code execution")
 
 
+def is_docker_running():
+    """Check if docker is running.
+
+    Returns:
+        bool: True if docker is running; False otherwise.
+    """
+    if docker is None:
+        return False
+    try:
+        client = docker.from_env()
+        client.ping()
+        return True
+    except docker.errors.DockerException:
+        return False
+
+
+def in_docker_container():
+    """Check if the code is running in a docker container.
+
+    Returns:
+        bool: True if the code is running in a docker container; False otherwise.
+    """
+    return os.path.exists("/.dockerenv")
+
+
+def decide_use_docker(use_docker) -> bool:
+    if use_docker is None:
+        env_var_use_docker = os.environ.get("AUTOGEN_USE_DOCKER", "True")
+
+        truthy_values = {"1", "true", "yes", "t"}
+        falsy_values = {"0", "false", "no", "f"}
+
+        # Convert the value to lowercase for case-insensitive comparison
+        env_var_use_docker_lower = env_var_use_docker.lower()
+
+        # Determine the boolean value based on the environment variable
+        if env_var_use_docker_lower in truthy_values:
+            use_docker = True
+        elif env_var_use_docker_lower in falsy_values:
+            use_docker = False
+        elif env_var_use_docker_lower == "none":  # Special case for 'None' as a string
+            use_docker = None
+        else:
+            # Raise an error for any unrecognized value
+            raise ValueError(
+                f'Invalid value for AUTOGEN_USE_DOCKER: {env_var_use_docker}. Please set AUTOGEN_USE_DOCKER to "1/True/yes", "0/False/no", or "None".'
+            )
+    return use_docker
+
+
+def check_use_docker(use_docker) -> None:
+    if use_docker is not None:
+        inside_docker = in_docker_container()
+        docker_installed_and_running = is_docker_running()
+        if use_docker and not inside_docker and not docker_installed_and_running:
+            raise RuntimeError(
+                'Docker is not running, please make sure docker is running (advised approach for code execution) or set "use_docker":False.'
+            )
+        if not use_docker:
+            logger.warning(
+                'use_docker was set to False. Any code execution will be run natively but we strongly advise to set "use_docker":True. Set "use_docker":None to silence this message.'
+            )
+
+
 def execute_code(
     code: Optional[str] = None,
     timeout: Optional[int] = None,
     filename: Optional[str] = None,
     work_dir: Optional[str] = None,
-    use_docker: Optional[Union[List[str], str, bool]] = None,
+    use_docker: Union[List[str], str, bool] = SENTINEL,
     lang: Optional[str] = "python",
 ) -> Tuple[int, str, str]:
     """Execute code in a docker container.
@@ -249,15 +314,15 @@ def execute_code(
             If None, a default working directory will be used.
             The default working directory is the "extensions" directory under
             "path_to_autogen".
-        use_docker (Optional, list, str or bool): The docker image to use for code execution.
+        use_docker (list, str or bool): The docker image to use for code execution.
             If a list or a str of image name(s) is provided, the code will be executed in a docker container
             with the first image successfully pulled.
             If None, False or empty, the code will be executed in the current environment.
-            Default is None, which will be converted into an empty list when docker package is available.
+            Default is True.
             Expected behaviour:
-                - If `use_docker` is explicitly set to True and the docker package is available, the code will run in a Docker container.
-                - If `use_docker` is explicitly set to True but the Docker package is missing, an error will be raised.
-                - If `use_docker` is not set (i.e., left default to None) and the Docker package is not available, a warning will be displayed, but the code will run natively.
+                - If `use_docker` is not set (i.e. left default to True) or is explicitly set to True and the docker package is available, the code will run in a Docker container.
+                - If `use_docker` is not set (i.e. left default to True) or is explicitly set to True but the Docker package is missing, an error will be raised.
+                - If `use_docker` is explicitly set to False, a warning will be displayed, but the code will run natively.
             If the code is executed in the current environment,
             the code must be trusted.
         lang (Optional, str): The language of the code. Default is "python".
@@ -272,23 +337,12 @@ def execute_code(
         logger.error(error_msg)
         raise AssertionError(error_msg)
 
-    if use_docker and docker is None:
-        error_msg = "Cannot use docker because the python docker package is not available."
-        logger.error(error_msg)
-        raise AssertionError(error_msg)
+    running_inside_docker = in_docker_container()
+    docker_running = is_docker_running()
 
-    # Warn if use_docker was unspecified (or None), and cannot be provided (the default).
-    # In this case the current behavior is to fall back to run natively, but this behavior
-    # is subject to change.
-    if use_docker is None:
-        if docker is None:
-            use_docker = False
-            logger.warning(
-                "execute_code was called without specifying a value for use_docker. Since the python docker package is not available, code will be run natively. Note: this fallback behavior is subject to change"
-            )
-        else:
-            # Default to true
-            use_docker = True
+    if use_docker is SENTINEL:
+        use_docker = decide_use_docker(use_docker=None)
+    check_use_docker(use_docker)
 
     timeout = timeout or DEFAULT_TIMEOUT
     original_filename = filename
@@ -300,15 +354,16 @@ def execute_code(
         filename = f"tmp_code_{code_hash}.{'py' if lang.startswith('python') else lang}"
     if work_dir is None:
         work_dir = WORKING_DIR
+
     filepath = os.path.join(work_dir, filename)
     file_dir = os.path.dirname(filepath)
     os.makedirs(file_dir, exist_ok=True)
+
     if code is not None:
         with open(filepath, "w", encoding="utf-8") as fout:
             fout.write(code)
-    # check if already running in a docker container
-    in_docker_container = os.path.exists("/.dockerenv")
-    if not use_docker or in_docker_container:
+
+    if not use_docker or running_inside_docker:
         # already running in a docker container
         cmd = [
             sys.executable if lang.startswith("python") else _cmd(lang),
@@ -352,7 +407,13 @@ def execute_code(
         return result.returncode, logs, None
 
     # create a docker client
+    if use_docker and not docker_running:
+        raise RuntimeError(
+            "Docker package is missing or docker is not running. Please make sure docker is running or set use_docker=False."
+        )
+
     client = docker.from_env()
+
     image_list = (
         ["python:3-alpine", "python:3", "python:3-windowsservercore"]
         if use_docker is True
diff --git a/setup.py b/setup.py
index 215e88980b63..c2435c15a418 100644
--- a/setup.py
+++ b/setup.py
@@ -21,6 +21,7 @@
     "python-dotenv",
     "tiktoken",
     "pydantic>=1.10,<3",  # could be both V1 and V2
+    "docker",
 ]
 
 setuptools.setup(
diff --git a/test/test_code.py b/test/test_code.py
index ec8ada2c9062..801f6cddf42b 100644
--- a/test/test_code.py
+++ b/test/test_code.py
@@ -294,8 +294,8 @@ def scrape(url):
 
 
 @pytest.mark.skipif(
-    sys.platform in ["darwin"],
-    reason="do not run on MacOS",
+    sys.platform in ["darwin", "win32"],
+    reason="do not run on MacOS or Windows",
 )
 def test_execute_code(use_docker=None):
     try:


Code review comments:
diff_hunk: @@ -41,6 +41,14 @@ jobs:
           pip install -e .
           python -c "import autogen"
           pip install -e. pytest mock
+      - name: Set AUTOGEN_USE_DOCKER based on OS
+        shell: bash
+        run: |
+          if [[ ${{ matrix.os }} == windows-2019 ]]; then
path: .github/workflows/build.yml
text: @user1:
Maybe:
```python
if [[ ${{ matrix.os }} != ubuntu-latest ]]; then
            echo "AUTOGEN_USE_DOCKER=False" >> $GITHUB_ENV
```

@author:
sure, updated :)

diff_hunk: @@ -128,6 +138,13 @@ def __init__(
         self._code_execution_config: Union[Dict, Literal[False]] = (
             {} if code_execution_config is None else code_execution_config
         )
+
+        if isinstance(self._code_execution_config, Dict):
path: autogen/agentchat/conversable_agent.py
text: @user1:
This is incorrect, should be:
```python
if isinstance(self._code_execution_config, dict):
```

Please see (https://docs.python.org/3.8/library/typing.html#classes-functions-and-decorators):

> At runtime, isinstance(x, T) will raise [TypeError](https://docs.python.org/3.8/library/exceptions.html#TypeError). In general, [isinstance()](https://docs.python.org/3.8/library/functions.html#isinstance) and [issubclass()](https://docs.python.org/3.8/library/functions.html#issubclass) should not be used with types.

@author:
true, fixed