mirror of
https://github.com/microsoft/autogen.git
synced 2025-10-30 09:20:18 +00:00
Run LocalCommandLineCodeExecutor within venv (#3977)
* Run LocalCommandLineCodeExecutor within venv * Remove create_virtual_env func and add docstring * Add explanation for LocalCommandLineExecutor docstring example * Enhance docstring example explanation --------- Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
parent
eb4b1f856e
commit
93733dbd65
@ -136,6 +136,76 @@
|
|||||||
" )\n",
|
" )\n",
|
||||||
")"
|
")"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Local within a Virtual Environment\n",
|
||||||
|
"\n",
|
||||||
|
"If you want the code to run within a virtual environment created as part of the application’s setup, you can specify a directory for the newly created environment and pass its context to {py:class}`~autogen_core.components.code_executor.LocalCommandLineCodeExecutor`. This setup allows the executor to use the specified virtual environment consistently throughout the application's lifetime, ensuring isolated dependencies and a controlled runtime environment."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"CommandLineCodeResult(exit_code=0, output='', code_file='/Users/gziz/Dev/autogen/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/coding/tmp_code_d2a7db48799db3cc785156a11a38822a45c19f3956f02ec69b92e4169ecbf2ca.bash')"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 3,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"import venv\n",
|
||||||
|
"from pathlib import Path\n",
|
||||||
|
"\n",
|
||||||
|
"from autogen_core.base import CancellationToken\n",
|
||||||
|
"from autogen_core.components.code_executor import CodeBlock, LocalCommandLineCodeExecutor\n",
|
||||||
|
"\n",
|
||||||
|
"work_dir = Path(\"coding\")\n",
|
||||||
|
"work_dir.mkdir(exist_ok=True)\n",
|
||||||
|
"\n",
|
||||||
|
"venv_dir = work_dir / \".venv\"\n",
|
||||||
|
"venv_builder = venv.EnvBuilder(with_pip=True)\n",
|
||||||
|
"venv_builder.create(venv_dir)\n",
|
||||||
|
"venv_context = venv_builder.ensure_directories(venv_dir)\n",
|
||||||
|
"\n",
|
||||||
|
"local_executor = LocalCommandLineCodeExecutor(work_dir=work_dir, virtual_env_context=venv_context)\n",
|
||||||
|
"await local_executor.execute_code_blocks(\n",
|
||||||
|
" code_blocks=[\n",
|
||||||
|
" CodeBlock(language=\"bash\", code=\"pip install matplotlib\"),\n",
|
||||||
|
" ],\n",
|
||||||
|
" cancellation_token=CancellationToken(),\n",
|
||||||
|
")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"As we can see, the code has executed successfully, and the installation has been isolated to the newly created virtual environment, without affecting our global environment."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@ -154,7 +224,7 @@
|
|||||||
"name": "python",
|
"name": "python",
|
||||||
"nbconvert_exporter": "python",
|
"nbconvert_exporter": "python",
|
||||||
"pygments_lexer": "ipython3",
|
"pygments_lexer": "ipython3",
|
||||||
"version": "3.11.9"
|
"version": "3.12.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
|
|||||||
@ -3,12 +3,14 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from string import Template
|
from string import Template
|
||||||
from typing import Any, Callable, ClassVar, List, Sequence, Union
|
from types import SimpleNamespace
|
||||||
|
from typing import Any, Callable, ClassVar, List, Optional, Sequence, Union
|
||||||
|
|
||||||
from typing_extensions import ParamSpec
|
from typing_extensions import ParamSpec
|
||||||
|
|
||||||
@ -54,6 +56,36 @@ class LocalCommandLineCodeExecutor(CodeExecutor):
|
|||||||
directory is the current directory ".".
|
directory is the current directory ".".
|
||||||
functions (List[Union[FunctionWithRequirements[Any, A], Callable[..., Any]]]): A list of functions that are available to the code executor. Default is an empty list.
|
functions (List[Union[FunctionWithRequirements[Any, A], Callable[..., Any]]]): A list of functions that are available to the code executor. Default is an empty list.
|
||||||
functions_module (str, optional): The name of the module that will be created to store the functions. Defaults to "functions".
|
functions_module (str, optional): The name of the module that will be created to store the functions. Defaults to "functions".
|
||||||
|
virtual_env_context (Optional[SimpleNamespace], optional): The virtual environment context. Defaults to None.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
How to use `LocalCommandLineCodeExecutor` with a virtual environment different from the one used to run the autogen application:
|
||||||
|
Set up a virtual environment using the `venv` module, and pass its context to the initializer of `LocalCommandLineCodeExecutor`. This way, the executor will run code within the new environment.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import venv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from autogen_core.base import CancellationToken
|
||||||
|
from autogen_core.components.code_executor import CodeBlock, LocalCommandLineCodeExecutor
|
||||||
|
|
||||||
|
work_dir = Path("coding")
|
||||||
|
work_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
venv_dir = work_dir / ".venv"
|
||||||
|
venv_builder = venv.EnvBuilder(with_pip=True)
|
||||||
|
venv_builder.create(venv_dir)
|
||||||
|
venv_context = venv_builder.ensure_directories(venv_dir)
|
||||||
|
|
||||||
|
local_executor = LocalCommandLineCodeExecutor(work_dir=work_dir, virtual_env_context=venv_context)
|
||||||
|
await local_executor.execute_code_blocks(
|
||||||
|
code_blocks=[
|
||||||
|
CodeBlock(language="bash", code="pip install matplotlib"),
|
||||||
|
],
|
||||||
|
cancellation_token=CancellationToken(),
|
||||||
|
)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -86,6 +118,7 @@ $functions"""
|
|||||||
]
|
]
|
||||||
] = [],
|
] = [],
|
||||||
functions_module: str = "functions",
|
functions_module: str = "functions",
|
||||||
|
virtual_env_context: Optional[SimpleNamespace] = None,
|
||||||
):
|
):
|
||||||
if timeout < 1:
|
if timeout < 1:
|
||||||
raise ValueError("Timeout must be greater than or equal to 1.")
|
raise ValueError("Timeout must be greater than or equal to 1.")
|
||||||
@ -110,6 +143,8 @@ $functions"""
|
|||||||
else:
|
else:
|
||||||
self._setup_functions_complete = True
|
self._setup_functions_complete = True
|
||||||
|
|
||||||
|
self._virtual_env_context: Optional[SimpleNamespace] = virtual_env_context
|
||||||
|
|
||||||
def format_functions_for_prompt(self, prompt_template: str = FUNCTION_PROMPT_TEMPLATE) -> str:
|
def format_functions_for_prompt(self, prompt_template: str = FUNCTION_PROMPT_TEMPLATE) -> str:
|
||||||
"""(Experimental) Format the functions for a prompt.
|
"""(Experimental) Format the functions for a prompt.
|
||||||
|
|
||||||
@ -164,9 +199,14 @@ $functions"""
|
|||||||
cmd_args = ["-m", "pip", "install"]
|
cmd_args = ["-m", "pip", "install"]
|
||||||
cmd_args.extend(required_packages)
|
cmd_args.extend(required_packages)
|
||||||
|
|
||||||
|
if self._virtual_env_context:
|
||||||
|
py_executable = self._virtual_env_context.env_exe
|
||||||
|
else:
|
||||||
|
py_executable = sys.executable
|
||||||
|
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
asyncio.create_subprocess_exec(
|
asyncio.create_subprocess_exec(
|
||||||
sys.executable,
|
py_executable,
|
||||||
*cmd_args,
|
*cmd_args,
|
||||||
cwd=self._work_dir,
|
cwd=self._work_dir,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
@ -253,7 +293,17 @@ $functions"""
|
|||||||
f.write(code)
|
f.write(code)
|
||||||
file_names.append(written_file)
|
file_names.append(written_file)
|
||||||
|
|
||||||
program = sys.executable if lang.startswith("python") else lang_to_cmd(lang)
|
env = os.environ.copy()
|
||||||
|
|
||||||
|
if self._virtual_env_context:
|
||||||
|
virtual_env_exe_abs_path = os.path.abspath(self._virtual_env_context.env_exe)
|
||||||
|
virtual_env_bin_abs_path = os.path.abspath(self._virtual_env_context.bin_path)
|
||||||
|
env["PATH"] = f"{virtual_env_bin_abs_path}{os.pathsep}{env['PATH']}"
|
||||||
|
|
||||||
|
program = virtual_env_exe_abs_path if lang.startswith("python") else lang_to_cmd(lang)
|
||||||
|
else:
|
||||||
|
program = sys.executable if lang.startswith("python") else lang_to_cmd(lang)
|
||||||
|
|
||||||
# Wrap in a task to make it cancellable
|
# Wrap in a task to make it cancellable
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
asyncio.create_subprocess_exec(
|
asyncio.create_subprocess_exec(
|
||||||
@ -262,6 +312,7 @@ $functions"""
|
|||||||
cwd=self._work_dir,
|
cwd=self._work_dir,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
env=env,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
cancellation_token.link_future(task)
|
cancellation_token.link_future(task)
|
||||||
|
|||||||
@ -2,8 +2,11 @@
|
|||||||
# Credit to original authors
|
# Credit to original authors
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import venv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AsyncGenerator, TypeAlias
|
from typing import AsyncGenerator, TypeAlias
|
||||||
|
|
||||||
@ -143,3 +146,51 @@ print("hello world")
|
|||||||
assert "test.py" in result.code_file
|
assert "test.py" in result.code_file
|
||||||
assert (temp_dir / Path("test.py")).resolve() == Path(result.code_file).resolve()
|
assert (temp_dir / Path("test.py")).resolve() == Path(result.code_file).resolve()
|
||||||
assert (temp_dir / Path("test.py")).exists()
|
assert (temp_dir / Path("test.py")).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_local_executor_with_custom_venv() -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
env_builder = venv.EnvBuilder(with_pip=True)
|
||||||
|
env_builder.create(temp_dir)
|
||||||
|
env_builder_context = env_builder.ensure_directories(temp_dir)
|
||||||
|
|
||||||
|
executor = LocalCommandLineCodeExecutor(work_dir=temp_dir, virtual_env_context=env_builder_context)
|
||||||
|
code_blocks = [
|
||||||
|
# https://stackoverflow.com/questions/1871549/how-to-determine-if-python-is-running-inside-a-virtualenv
|
||||||
|
CodeBlock(code="import sys; print(sys.prefix != sys.base_prefix)", language="python"),
|
||||||
|
]
|
||||||
|
cancellation_token = CancellationToken()
|
||||||
|
result = await executor.execute_code_blocks(code_blocks, cancellation_token=cancellation_token)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output.strip() == "True"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_local_executor_with_custom_venv_in_local_relative_path() -> None:
|
||||||
|
relative_folder_path = "tmp_dir"
|
||||||
|
try:
|
||||||
|
if not os.path.isdir(relative_folder_path):
|
||||||
|
os.mkdir(relative_folder_path)
|
||||||
|
|
||||||
|
env_path = os.path.join(relative_folder_path, ".venv")
|
||||||
|
env_builder = venv.EnvBuilder(with_pip=True)
|
||||||
|
env_builder.create(env_path)
|
||||||
|
env_builder_context = env_builder.ensure_directories(env_path)
|
||||||
|
|
||||||
|
executor = LocalCommandLineCodeExecutor(work_dir=relative_folder_path, virtual_env_context=env_builder_context)
|
||||||
|
code_blocks = [
|
||||||
|
CodeBlock(code="import sys; print(sys.executable)", language="python"),
|
||||||
|
]
|
||||||
|
cancellation_token = CancellationToken()
|
||||||
|
result = await executor.execute_code_blocks(code_blocks, cancellation_token=cancellation_token)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Check if the expected venv has been used
|
||||||
|
bin_path = os.path.abspath(env_builder_context.bin_path)
|
||||||
|
assert Path(result.output.strip()).parent.samefile(bin_path)
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(relative_folder_path):
|
||||||
|
shutil.rmtree(relative_folder_path)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user