From da4ad98c238243eb3f76c28c3653da3934b10835 Mon Sep 17 00:00:00 2001 From: Matthew Zhou Date: Thu, 24 Apr 2025 11:13:44 -0700 Subject: [PATCH] feat: Adjust local tool execution to always try using venv if exists (#1864) --- letta/schemas/sandbox_config.py | 4 +- letta/server/server.py | 6 +- .../services/helpers/tool_execution_helper.py | 6 +- .../tool_executor/tool_execution_sandbox.py | 192 ++++++------------ letta/services/tool_sandbox/local_sandbox.py | 6 +- tests/integration_test_async_tool_sandbox.py | 8 +- ...integration_test_tool_execution_sandbox.py | 84 +------- tests/test_v1_routes.py | 2 +- 8 files changed, 78 insertions(+), 230 deletions(-) diff --git a/letta/schemas/sandbox_config.py b/letta/schemas/sandbox_config.py index 80e93c11..d16b9169 100644 --- a/letta/schemas/sandbox_config.py +++ b/letta/schemas/sandbox_config.py @@ -47,14 +47,14 @@ class PipRequirement(BaseModel): class LocalSandboxConfig(BaseModel): sandbox_dir: Optional[str] = Field(None, description="Directory for the sandbox environment.") - use_venv: bool = Field(False, description="Whether or not to use the venv, or run directly in the same run loop.") + force_create_venv: bool = Field(False, description="Whether or not to use the venv, or run directly in the same run loop.") venv_name: str = Field( "venv", description="The name for the venv in the sandbox directory. We first search for an existing venv with this name, otherwise, we make it from the requirements.txt.", ) pip_requirements: List[PipRequirement] = Field( default_factory=list, - description="List of pip packages to install with mandatory name and optional version following semantic versioning. This only is considered when use_venv is True.", + description="List of pip packages to install with mandatory name and optional version following semantic versioning. This only is considered when force_create_venv is True.", ) @property diff --git a/letta/server/server.py b/letta/server/server.py index 6190773b..7dba65db 100644 --- a/letta/server/server.py +++ b/letta/server/server.py @@ -1308,12 +1308,14 @@ class SyncServer(Server): tool_execution_result = ToolExecutionSandbox(tool.name, tool_args, actor, tool_object=tool).run( agent_state=agent_state, additional_env_vars=tool_env_vars ) + status = "error" if tool_execution_result.stderr else "success" + tool_return = str(tool_execution_result.stderr) if tool_execution_result.stderr else str(tool_execution_result.func_return) return ToolReturnMessage( id="null", tool_call_id="null", date=get_utc_time(), - status=tool_execution_result.status, - tool_return=str(tool_execution_result.func_return), + status=status, + tool_return=tool_return, stdout=tool_execution_result.stdout, stderr=tool_execution_result.stderr, ) diff --git a/letta/services/helpers/tool_execution_helper.py b/letta/services/helpers/tool_execution_helper.py index ac86f480..60fdec7f 100644 --- a/letta/services/helpers/tool_execution_helper.py +++ b/letta/services/helpers/tool_execution_helper.py @@ -24,7 +24,7 @@ def find_python_executable(local_configs: LocalSandboxConfig) -> str: """ sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) # Expand tilde - if not local_configs.use_venv: + if not local_configs.force_create_venv: return "python.exe" if platform.system().lower().startswith("win") else "python3" venv_path = os.path.join(sandbox_dir, local_configs.venv_name) @@ -96,7 +96,7 @@ def install_pip_requirements_for_sandbox( python_exec = find_python_executable(local_configs) # If using a virtual environment, upgrade pip before installing dependencies. - if local_configs.use_venv: + if local_configs.force_create_venv: ensure_pip_is_up_to_date(python_exec, env=env) # Construct package list @@ -108,7 +108,7 @@ def install_pip_requirements_for_sandbox( pip_cmd.append("--upgrade") pip_cmd += packages - if user_install_if_no_venv and not local_configs.use_venv: + if user_install_if_no_venv and not local_configs.force_create_venv: pip_cmd.append("--user") run_subprocess(pip_cmd, env=env, fail_msg=f"Failed to install packages: {', '.join(packages)}") diff --git a/letta/services/tool_executor/tool_execution_sandbox.py b/letta/services/tool_executor/tool_execution_sandbox.py index 361a9902..fa5f36cc 100644 --- a/letta/services/tool_executor/tool_execution_sandbox.py +++ b/letta/services/tool_executor/tool_execution_sandbox.py @@ -1,12 +1,10 @@ import ast import base64 -import io import os import pickle import subprocess import sys import tempfile -import traceback import uuid from typing import Any, Dict, Optional @@ -119,89 +117,74 @@ class ToolExecutionSandbox: @trace_method def run_local_dir_sandbox( - self, agent_state: Optional[AgentState] = None, additional_env_vars: Optional[Dict] = None + self, + agent_state: Optional[AgentState] = None, + additional_env_vars: Optional[Dict] = None, ) -> ToolExecutionResult: - sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=self.user) + sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config( + sandbox_type=SandboxType.LOCAL, + actor=self.user, + ) local_configs = sbx_config.get_local_config() + sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) + venv_path = os.path.join(sandbox_dir, local_configs.venv_name) - # Get environment variables for the sandbox + # Aggregate environment variables env = os.environ.copy() - env_vars = self.sandbox_config_manager.get_sandbox_env_vars_as_dict(sandbox_config_id=sbx_config.id, actor=self.user, limit=100) - env.update(env_vars) - - # Get environment variables for this agent specifically + env.update(self.sandbox_config_manager.get_sandbox_env_vars_as_dict(sandbox_config_id=sbx_config.id, actor=self.user, limit=100)) if agent_state: env.update(agent_state.get_agent_env_vars_as_dict()) - - # Finally, get any that are passed explicitly into the `run` function call if additional_env_vars: env.update(additional_env_vars) - # Safety checks - if not os.path.exists(local_configs.sandbox_dir) or not os.path.isdir(local_configs.sandbox_dir): - logger.warning(f"Sandbox directory does not exist, creating: {local_configs.sandbox_dir}") - os.makedirs(local_configs.sandbox_dir) - - # Write the code to a temp file in the sandbox_dir - with tempfile.NamedTemporaryFile(mode="w", dir=local_configs.sandbox_dir, suffix=".py", delete=False) as temp_file: - if local_configs.use_venv: - # If using venv, we need to wrap with special string markers to separate out the output and the stdout (since it is all in stdout) - code = self.generate_execution_script(agent_state=agent_state, wrap_print_with_markers=True) - else: - code = self.generate_execution_script(agent_state=agent_state) + # Ensure sandbox dir exists + if not os.path.exists(sandbox_dir): + logger.warning(f"Sandbox directory does not exist, creating: {sandbox_dir}") + os.makedirs(sandbox_dir) + # Write the code to a temp file + with tempfile.NamedTemporaryFile(mode="w", dir=sandbox_dir, suffix=".py", delete=False) as temp_file: + code = self.generate_execution_script(agent_state=agent_state, wrap_print_with_markers=True) temp_file.write(code) temp_file.flush() temp_file_path = temp_file.name + try: - if local_configs.use_venv: - return self.run_local_dir_sandbox_venv(sbx_config, env, temp_file_path) + # Decide whether to use venv + use_venv = os.path.isdir(venv_path) + + if self.force_recreate_venv or (not use_venv and local_configs.force_create_venv): + logger.warning(f"Virtual environment not found at {venv_path}. Creating one...") + log_event(name="start create_venv_for_local_sandbox", attributes={"venv_path": venv_path}) + create_venv_for_local_sandbox( + sandbox_dir_path=sandbox_dir, + venv_path=venv_path, + env=env, + force_recreate=self.force_recreate_venv, + ) + log_event(name="finish create_venv_for_local_sandbox") + use_venv = True + + if use_venv: + log_event(name="start install_pip_requirements_for_sandbox", attributes={"local_configs": local_configs.model_dump_json()}) + install_pip_requirements_for_sandbox(local_configs, env=env) + log_event(name="finish install_pip_requirements_for_sandbox", attributes={"local_configs": local_configs.model_dump_json()}) + + python_executable = find_python_executable(local_configs) + if not os.path.isfile(python_executable): + logger.warning( + f"Python executable not found at expected venv path: {python_executable}. Falling back to system Python." + ) + python_executable = sys.executable + else: + env = dict(env) + env["VIRTUAL_ENV"] = venv_path + env["PATH"] = os.path.join(venv_path, "bin") + ":" + env.get("PATH", "") else: - return self.run_local_dir_sandbox_directly(sbx_config, env, temp_file_path) - except Exception as e: - logger.error(f"Executing tool {self.tool_name} has an unexpected error: {e}") - logger.error(f"Logging out tool {self.tool_name} auto-generated code for debugging: \n\n{code}") - raise e - finally: - # Clean up the temp file - os.remove(temp_file_path) + python_executable = sys.executable - @trace_method - def run_local_dir_sandbox_venv( - self, - sbx_config: SandboxConfig, - env: Dict[str, str], - temp_file_path: str, - ) -> ToolExecutionResult: - local_configs = sbx_config.get_local_config() - sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) # Expand tilde - venv_path = os.path.join(sandbox_dir, local_configs.venv_name) + env["PYTHONWARNINGS"] = "ignore" - # Recreate venv if required - if self.force_recreate_venv or not os.path.isdir(venv_path): - logger.warning(f"Virtual environment directory does not exist at: {venv_path}, creating one now...") - log_event(name="start create_venv_for_local_sandbox", attributes={"venv_path": venv_path}) - create_venv_for_local_sandbox( - sandbox_dir_path=sandbox_dir, venv_path=venv_path, env=env, force_recreate=self.force_recreate_venv - ) - log_event(name="finish create_venv_for_local_sandbox") - - log_event(name="start install_pip_requirements_for_sandbox", attributes={"local_configs": local_configs.model_dump_json()}) - install_pip_requirements_for_sandbox(local_configs, env=env) - log_event(name="finish install_pip_requirements_for_sandbox", attributes={"local_configs": local_configs.model_dump_json()}) - - # Ensure Python executable exists - python_executable = find_python_executable(local_configs) - if not os.path.isfile(python_executable): - raise FileNotFoundError(f"Python executable not found in virtual environment: {python_executable}") - - # Set up environment variables - env["VIRTUAL_ENV"] = venv_path - env["PATH"] = os.path.join(venv_path, "bin") + ":" + env["PATH"] - env["PYTHONWARNINGS"] = "ignore" - - # Execute the code - try: log_event(name="start subprocess") result = subprocess.run( [python_executable, temp_file_path], @@ -213,19 +196,19 @@ class ToolExecutionSandbox: ) log_event(name="finish subprocess") func_result, stdout = self.parse_out_function_results_markers(result.stdout) - func_return, agent_state = self.parse_best_effort(func_result) + func_return, parsed_agent_state = self.parse_best_effort(func_result) return ToolExecutionResult( status="success", func_return=func_return, - agent_state=agent_state, + agent_state=parsed_agent_state, stdout=[stdout] if stdout else [], stderr=[result.stderr] if result.stderr else [], sandbox_config_fingerprint=sbx_config.fingerprint(), ) except subprocess.CalledProcessError as e: - logger.error(f"Executing tool {self.tool_name} has process error: {e}") + logger.error(f"Tool execution failed: {e}") func_return = get_friendly_error_msg( function_name=self.tool_name, exception_name=type(e).__name__, @@ -245,72 +228,11 @@ class ToolExecutionSandbox: except Exception as e: logger.error(f"Executing tool {self.tool_name} has an unexpected error: {e}") + logger.error(f"Generated script:\n{code}") raise e - @trace_method - def run_local_dir_sandbox_directly( - self, - sbx_config: SandboxConfig, - env: Dict[str, str], - temp_file_path: str, - ) -> ToolExecutionResult: - status = "success" - func_return, agent_state, stderr = None, None, None - - old_stdout = sys.stdout - old_stderr = sys.stderr - captured_stdout, captured_stderr = io.StringIO(), io.StringIO() - - sys.stdout = captured_stdout - sys.stderr = captured_stderr - - try: - with self.temporary_env_vars(env): - - # Read and compile the Python script - with open(temp_file_path, "r", encoding="utf-8") as f: - source = f.read() - code_obj = compile(source, temp_file_path, "exec") - - # Provide a dict for globals - globals_dict = dict(env) # or {} - # If you need to mimic `__main__` behavior: - globals_dict["__name__"] = "__main__" - globals_dict["__file__"] = temp_file_path - - # Execute the compiled code - log_event(name="start exec", attributes={"temp_file_path": temp_file_path}) - exec(code_obj, globals_dict) - log_event(name="finish exec", attributes={"temp_file_path": temp_file_path}) - - # Get result from the global dict - func_result = globals_dict.get(self.LOCAL_SANDBOX_RESULT_VAR_NAME) - func_return, agent_state = self.parse_best_effort(func_result) - - except Exception as e: - func_return = get_friendly_error_msg( - function_name=self.tool_name, - exception_name=type(e).__name__, - exception_message=str(e), - ) - traceback.print_exc(file=sys.stderr) - status = "error" - - # Restore stdout/stderr - sys.stdout = old_stdout - sys.stderr = old_stderr - - stdout_output = [captured_stdout.getvalue()] if captured_stdout.getvalue() else [] - stderr_output = [captured_stderr.getvalue()] if captured_stderr.getvalue() else [] - - return ToolExecutionResult( - status=status, - func_return=func_return, - agent_state=agent_state, - stdout=stdout_output, - stderr=stderr_output, - sandbox_config_fingerprint=sbx_config.fingerprint(), - ) + finally: + os.remove(temp_file_path) def parse_out_function_results_markers(self, text: str): if self.LOCAL_SANDBOX_RESULT_START_MARKER not in text: diff --git a/letta/services/tool_sandbox/local_sandbox.py b/letta/services/tool_sandbox/local_sandbox.py index 11683525..b4c4c1b5 100644 --- a/letta/services/tool_sandbox/local_sandbox.py +++ b/letta/services/tool_sandbox/local_sandbox.py @@ -69,7 +69,7 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase): else: sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=self.user) local_configs = sbx_config.get_local_config() - use_venv = local_configs.use_venv + force_create_venv = local_configs.force_create_venv # Prepare environment variables env = os.environ.copy() @@ -92,7 +92,7 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase): # If using a virtual environment, ensure it's prepared in parallel venv_preparation_task = None - if use_venv: + if force_create_venv: venv_path = str(os.path.join(sandbox_dir, local_configs.venv_name)) venv_preparation_task = asyncio.create_task(self._prepare_venv(local_configs, venv_path, env)) @@ -110,7 +110,7 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase): # Determine the python executable and environment for the subprocess exec_env = env.copy() - if use_venv: + if force_create_venv: venv_path = str(os.path.join(sandbox_dir, local_configs.venv_name)) python_executable = find_python_executable(local_configs) exec_env["VIRTUAL_ENV"] = venv_path diff --git a/tests/integration_test_async_tool_sandbox.py b/tests/integration_test_async_tool_sandbox.py index b85728db..c0680d15 100644 --- a/tests/integration_test_async_tool_sandbox.py +++ b/tests/integration_test_async_tool_sandbox.py @@ -221,7 +221,7 @@ def custom_test_sandbox_config(test_user): external_codebase_path = str(Path(__file__).parent / "test_tool_sandbox" / "restaurant_management_system") # tqdm is used in this codebase, but NOT in the requirements.txt, this tests that we can successfully install pip requirements local_sandbox_config = LocalSandboxConfig( - sandbox_dir=external_codebase_path, use_venv=True, pip_requirements=[PipRequirement(name="tqdm")] + sandbox_dir=external_codebase_path, force_create_venv=True, pip_requirements=[PipRequirement(name="tqdm")] ) # Create the sandbox configuration @@ -366,7 +366,7 @@ async def test_local_sandbox_with_venv_errors(disable_e2b_api_key, custom_test_s async def test_local_sandbox_with_venv_pip_installs_basic(disable_e2b_api_key, cowsay_tool, test_user): manager = SandboxConfigManager() config_create = SandboxConfigCreate( - config=LocalSandboxConfig(use_venv=True, pip_requirements=[PipRequirement(name="cowsay")]).model_dump() + config=LocalSandboxConfig(force_create_venv=True, pip_requirements=[PipRequirement(name="cowsay")]).model_dump() ) config = manager.create_or_update_sandbox_config(config_create, test_user) @@ -385,7 +385,7 @@ async def test_local_sandbox_with_venv_pip_installs_basic(disable_e2b_api_key, c @pytest.mark.e2b_sandbox async def test_local_sandbox_with_venv_pip_installs_with_update(disable_e2b_api_key, cowsay_tool, test_user): manager = SandboxConfigManager() - config_create = SandboxConfigCreate(config=LocalSandboxConfig(use_venv=True).model_dump()) + config_create = SandboxConfigCreate(config=LocalSandboxConfig(force_create_venv=True).model_dump()) config = manager.create_or_update_sandbox_config(config_create, test_user) key = "secret_word" @@ -400,7 +400,7 @@ async def test_local_sandbox_with_venv_pip_installs_with_update(disable_e2b_api_ assert "No module named 'cowsay'" in result.stderr[0] config_create = SandboxConfigCreate( - config=LocalSandboxConfig(use_venv=True, pip_requirements=[PipRequirement(name="cowsay")]).model_dump() + config=LocalSandboxConfig(force_create_venv=True, pip_requirements=[PipRequirement(name="cowsay")]).model_dump() ) manager.create_or_update_sandbox_config(config_create, test_user) diff --git a/tests/integration_test_tool_execution_sandbox.py b/tests/integration_test_tool_execution_sandbox.py index 85aaab97..c11e772d 100644 --- a/tests/integration_test_tool_execution_sandbox.py +++ b/tests/integration_test_tool_execution_sandbox.py @@ -220,7 +220,7 @@ def custom_test_sandbox_config(test_user): external_codebase_path = str(Path(__file__).parent / "test_tool_sandbox" / "restaurant_management_system") # tqdm is used in this codebase, but NOT in the requirements.txt, this tests that we can successfully install pip requirements local_sandbox_config = LocalSandboxConfig( - sandbox_dir=external_codebase_path, use_venv=True, pip_requirements=[PipRequirement(name="tqdm")] + sandbox_dir=external_codebase_path, force_create_venv=True, pip_requirements=[PipRequirement(name="tqdm")] ) # Create the sandbox configuration @@ -382,7 +382,7 @@ def test_local_sandbox_with_venv_errors(disable_e2b_api_key, custom_test_sandbox def test_local_sandbox_with_venv_pip_installs_basic(disable_e2b_api_key, cowsay_tool, test_user): manager = SandboxConfigManager() config_create = SandboxConfigCreate( - config=LocalSandboxConfig(use_venv=True, pip_requirements=[PipRequirement(name="cowsay")]).model_dump() + config=LocalSandboxConfig(force_create_venv=True, pip_requirements=[PipRequirement(name="cowsay")]).model_dump() ) config = manager.create_or_update_sandbox_config(config_create, test_user) @@ -401,7 +401,7 @@ def test_local_sandbox_with_venv_pip_installs_basic(disable_e2b_api_key, cowsay_ @pytest.mark.e2b_sandbox def test_local_sandbox_with_venv_pip_installs_with_update(disable_e2b_api_key, cowsay_tool, test_user): manager = SandboxConfigManager() - config_create = SandboxConfigCreate(config=LocalSandboxConfig(use_venv=True).model_dump()) + config_create = SandboxConfigCreate(config=LocalSandboxConfig(force_create_venv=True).model_dump()) config = manager.create_or_update_sandbox_config(config_create, test_user) # Add an environment variable @@ -421,7 +421,7 @@ def test_local_sandbox_with_venv_pip_installs_with_update(disable_e2b_api_key, c # Now update the SandboxConfig config_create = SandboxConfigCreate( - config=LocalSandboxConfig(use_venv=True, pip_requirements=[PipRequirement(name="cowsay")]).model_dump() + config=LocalSandboxConfig(force_create_venv=True, pip_requirements=[PipRequirement(name="cowsay")]).model_dump() ) manager.create_or_update_sandbox_config(config_create, test_user) @@ -589,79 +589,3 @@ def test_e2b_sandbox_with_list_rv(check_e2b_key_is_set, list_tool, test_user): sandbox = ToolExecutionSandbox(list_tool.name, {}, user=test_user) result = sandbox.run() assert len(result.func_return) == 5 - - -# Core memory integration tests -class TestCoreMemoryTools: - """ - Tests for core memory manipulation tools. - Tests run in both local sandbox and e2b environments. - """ - - # Local sandbox tests - @pytest.mark.local_sandbox - def test_core_memory_replace_local(self, disable_e2b_api_key, core_memory_tools, test_user, agent_state): - """Test successful replacement of content in core memory - local sandbox.""" - new_name = "Charles" - args = {"label": "human", "old_content": "Chad", "new_content": new_name} - sandbox = ToolExecutionSandbox(core_memory_tools["core_memory_replace"].name, args, user=test_user) - - result = sandbox.run(agent_state=agent_state) - assert new_name in result.agent_state.memory.get_block("human").value - assert result.func_return is None - - @pytest.mark.local_sandbox - def test_core_memory_append_local(self, disable_e2b_api_key, core_memory_tools, test_user, agent_state): - """Test successful appending of content to core memory - local sandbox.""" - append_text = "\nLikes coffee" - args = {"label": "human", "content": append_text} - sandbox = ToolExecutionSandbox(core_memory_tools["core_memory_append"].name, args, user=test_user) - - result = sandbox.run(agent_state=agent_state) - assert append_text in result.agent_state.memory.get_block("human").value - assert result.func_return is None - - @pytest.mark.local_sandbox - def test_core_memory_replace_error_local(self, disable_e2b_api_key, core_memory_tools, test_user, agent_state): - """Test error handling when trying to replace non-existent content - local sandbox.""" - nonexistent_name = "Alexander Wang" - args = {"label": "human", "old_content": nonexistent_name, "new_content": "Charles"} - sandbox = ToolExecutionSandbox(core_memory_tools["core_memory_replace"].name, args, user=test_user) - - result = sandbox.run(agent_state=agent_state) - assert len(result.stderr) != 0 - assert f"ValueError: Old content '{nonexistent_name}' not found in memory block 'human'" in result.stderr[0] - - # E2B sandbox tests - @pytest.mark.e2b_sandbox - def test_core_memory_replace_e2b(self, check_e2b_key_is_set, core_memory_tools, test_user, agent_state): - """Test successful replacement of content in core memory - e2b sandbox.""" - new_name = "Charles" - args = {"label": "human", "old_content": "Chad", "new_content": new_name} - sandbox = ToolExecutionSandbox(core_memory_tools["core_memory_replace"].name, args, user=test_user) - - result = sandbox.run(agent_state=agent_state) - assert new_name in result.agent_state.memory.get_block("human").value - assert result.func_return is None - - @pytest.mark.e2b_sandbox - def test_core_memory_append_e2b(self, check_e2b_key_is_set, core_memory_tools, test_user, agent_state): - """Test successful appending of content to core memory - e2b sandbox.""" - append_text = "\nLikes coffee" - args = {"label": "human", "content": append_text} - sandbox = ToolExecutionSandbox(core_memory_tools["core_memory_append"].name, args, user=test_user) - - result = sandbox.run(agent_state=agent_state) - assert append_text in result.agent_state.memory.get_block("human").value - assert result.func_return is None - - @pytest.mark.e2b_sandbox - def test_core_memory_replace_error_e2b(self, check_e2b_key_is_set, core_memory_tools, test_user, agent_state): - """Test error handling when trying to replace non-existent content - e2b sandbox.""" - nonexistent_name = "Alexander Wang" - args = {"label": "human", "old_content": nonexistent_name, "new_content": "Charles"} - sandbox = ToolExecutionSandbox(core_memory_tools["core_memory_replace"].name, args, user=test_user) - - result = sandbox.run(agent_state=agent_state) - assert len(result.stderr) != 0 - assert f"ValueError: Old content '{nonexistent_name}' not found in memory block 'human'" in result.stderr[0] diff --git a/tests/test_v1_routes.py b/tests/test_v1_routes.py index d79741a2..d08ac86d 100644 --- a/tests/test_v1_routes.py +++ b/tests/test_v1_routes.py @@ -493,7 +493,7 @@ def sample_local_sandbox_config(): """Fixture for a sample LocalSandboxConfig object.""" return LocalSandboxConfig( sandbox_dir="/custom/path", - use_venv=True, + force_create_venv=True, venv_name="custom_venv_name", pip_requirements=[ PipRequirement(name="numpy", version="1.23.0"),