diff --git a/sdk-python/agentcube/clients/code_interpreter_data_plane.py b/sdk-python/agentcube/clients/code_interpreter_data_plane.py index a9350865..2f892bf6 100644 --- a/sdk-python/agentcube/clients/code_interpreter_data_plane.py +++ b/sdk-python/agentcube/clients/code_interpreter_data_plane.py @@ -150,14 +150,22 @@ def execute_command(self, command: Union[str, List[str]], timeout: Optional[floa resp.raise_for_status() result = resp.json() + stdout = result.get("stdout", "") + stderr = result.get("stderr", "") + if result["exit_code"] != 0: raise CommandExecutionError( exit_code=result["exit_code"], - stderr=result["stderr"], + stdout=stdout, + stderr=stderr, command=command ) - return result["stdout"] + # Combine stdout and stderr for the caller if stderr is present + output = stdout + if stderr: + output = f"{stdout}\n{stderr}".strip() if stdout else stderr + return output def run_code(self, language: str, code: str, timeout: Optional[float] = None) -> str: """Run a code snippet (python or bash).""" diff --git a/sdk-python/agentcube/code_interpreter.py b/sdk-python/agentcube/code_interpreter.py index d4f2fe5f..6852fbfc 100644 --- a/sdk-python/agentcube/code_interpreter.py +++ b/sdk-python/agentcube/code_interpreter.py @@ -14,7 +14,7 @@ import os import logging -from typing import Optional +from typing import Optional, Union from agentcube.clients.control_plane import ControlPlaneClient from agentcube.clients.code_interpreter_data_plane import CodeInterpreterDataPlaneClient @@ -194,12 +194,12 @@ def run_code(self, language: str, code: str, timeout: Optional[float] = None) -> """ return self.dp_client.run_code(language, code, timeout) - def write_file(self, content: str, remote_path: str): + def write_file(self, content: Union[str, bytes], remote_path: str): """ Write content to a file in the remote environment. Args: - content: The string content to write to the file. + content: The string or binary content to write to the file. remote_path: The destination path of the file in the remote environment. This path is relative to the session's working directory. """ diff --git a/sdk-python/agentcube/exceptions.py b/sdk-python/agentcube/exceptions.py index 0240584f..8de34bfe 100644 --- a/sdk-python/agentcube/exceptions.py +++ b/sdk-python/agentcube/exceptions.py @@ -18,11 +18,17 @@ class AgentCubeError(Exception): class CommandExecutionError(AgentCubeError): """Raised when a command execution fails (exit code != 0)""" - def __init__(self, exit_code, stderr, command=None): + def __init__(self, exit_code: int, stdout: str, stderr: str, command: str = None): self.exit_code = exit_code + self.stdout = stdout self.stderr = stderr self.command = command - super().__init__(f"Command failed (exit {exit_code}): {stderr}") + + # Combine for the error message + output = stdout + if stderr: + output = f"{stdout}\n{stderr}".strip() if stdout else stderr + super().__init__(f"Command failed (exit {exit_code}): {output}") class SessionError(AgentCubeError): """Raised when session creation or management fails""" diff --git a/sdk-python/agentcube/integrations/__init__.py b/sdk-python/agentcube/integrations/__init__.py new file mode 100644 index 00000000..429557c0 --- /dev/null +++ b/sdk-python/agentcube/integrations/__init__.py @@ -0,0 +1,15 @@ +# Copyright The Volcano Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""AgentCube SDK integrations.""" diff --git a/sdk-python/agentcube/integrations/langchain/README.md b/sdk-python/agentcube/integrations/langchain/README.md new file mode 100644 index 00000000..37c8e0d3 --- /dev/null +++ b/sdk-python/agentcube/integrations/langchain/README.md @@ -0,0 +1,28 @@ +# AgentCube LangChain Integration + +This directory contains the AgentCube sandbox provider for LangChain and the DeepAgents ecosystem. + +## Features + +- **AgentCubeSandbox**: A sandbox provider for executing code in AgentCube sessions. +- **Async Support**: Fully non-blocking async methods using `asyncio.to_thread`. +- **Combined Output**: Merges stdout and stderr for better agent reasoning. + +## Installation + +```bash +pip install agentcube-sdk[langchain] +``` + +## Usage + +```python +from agentcube import CodeInterpreterClient +from agentcube.integrations.langchain import AgentCubeSandbox + +client = CodeInterpreterClient() +sandbox = AgentCubeSandbox(client) + +response = sandbox.execute("print('hello world')") +print(response.output) +``` diff --git a/sdk-python/agentcube/integrations/langchain/__init__.py b/sdk-python/agentcube/integrations/langchain/__init__.py new file mode 100644 index 00000000..85edd1e3 --- /dev/null +++ b/sdk-python/agentcube/integrations/langchain/__init__.py @@ -0,0 +1,17 @@ +# Copyright The Volcano Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .sandbox import AgentCubeSandbox + +__all__ = ["AgentCubeSandbox"] diff --git a/sdk-python/agentcube/integrations/langchain/sandbox.py b/sdk-python/agentcube/integrations/langchain/sandbox.py new file mode 100644 index 00000000..01b9f3da --- /dev/null +++ b/sdk-python/agentcube/integrations/langchain/sandbox.py @@ -0,0 +1,203 @@ +# Copyright The Volcano Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""LangChain integration for AgentCube Code Interpreter.""" + +from __future__ import annotations + +import os +import tempfile +import asyncio +from typing import Optional + +from agentcube.code_interpreter import CodeInterpreterClient +from agentcube.exceptions import CommandExecutionError + +# Internal types for base class compliance +try: + from deepagents.backends.protocol import ( + ExecuteResponse, + FileDownloadResponse, + FileUploadResponse, + ) + from deepagents.backends.sandbox import BaseSandbox +except ModuleNotFoundError as e: + # Catching only if deepagents itself is missing. + # If deepagents is installed but has internal import errors, we should let them bubble up. + if e.name and (e.name.startswith("deepagents") or "deepagents" in e.name): + # Define fallback classes if deepagents is not installed + # This allows the module to be imported even if the optional integration + # dependencies are missing. + class BaseSandbox: # type: ignore + """Fallback BaseSandbox.""" + pass + + class ExecuteResponse: # type: ignore + """Fallback ExecuteResponse.""" + def __init__(self, output: str, exit_code: int, truncated: bool = False): + self.output = output + self.exit_code = exit_code + self.truncated = truncated + + class FileUploadResponse: # type: ignore + """Fallback FileUploadResponse.""" + def __init__(self, path: str, error: Optional[str] = None): + self.path = path + self.error = error + + class FileDownloadResponse: # type: ignore + """Fallback FileDownloadResponse.""" + def __init__(self, path: str, content: bytes, error: Optional[str] = None): + self.path = path + self.content = content + self.error = error + else: + raise +except ImportError as e: + # Re-raise with more context if it's an import error within deepagents + raise ImportError(f"Failed to import deepagents: {e}") from e + + +class AgentCubeSandbox(BaseSandbox): + """AgentCube implementation of the LangChain Sandbox integration. + + This class allows AgentCube to be used as a backend for autonomous agents + and code execution tools within the LangChain / DeepAgents ecosystem. + """ + + def __init__(self, client: CodeInterpreterClient) -> None: + """Initialize the sandbox with an AgentCube CodeInterpreterClient. + + Args: + client: An instance of AgentCube's CodeInterpreterClient. + """ + self._client = client + + @property + def id(self) -> str: + """Return the unique session ID of the sandbox instance.""" + return self._client.session_id or "unknown" + + def execute( + self, + command: str, + *, + timeout: int | None = None, + ) -> ExecuteResponse: + """Execute a shell command in the AgentCube sandbox. + + Args: + command: The command to execute. + timeout: Optional execution timeout in seconds. + + Returns: + An ExecuteResponse containing stdout, exit_code and truncated status. + """ + try: + # Map AgentCube output to ExecuteResponse + # execute_command now returns combined stdout and stderr + output = self._client.execute_command(command, timeout=timeout) + return ExecuteResponse( + output=output, + exit_code=0, + truncated=False, + ) + except CommandExecutionError as e: + # Map AgentCube execution error + # Combine stdout and stderr for the agent + output = e.stdout + if e.stderr: + output = f"{output}\n{e.stderr}".strip() if output else e.stderr + + return ExecuteResponse( + output=output, + exit_code=e.exit_code, + truncated=False, + ) + + def upload_files( + self, + files: list[tuple[str, bytes]], + ) -> list[FileUploadResponse]: + """Upload multiple files to the AgentCube sandbox. + + Args: + files: A list of (remote_path, content_bytes) tuples. + + Returns: + A list of FileUploadResponse objects in the same order as input. + """ + results = [] + for path, content in files: + try: + # If bytes, try to decode to string as write_file currently only supports str + # SDK support for raw bytes will be added in a separate PR. + if isinstance(content, bytes): + content = content.decode("utf-8") + self._client.write_file(content, path) + results.append(FileUploadResponse(path=path, error=None)) + except Exception as e: + results.append(FileUploadResponse(path=path, error=str(e))) + return results + + def download_files(self, paths: list[str]) -> list[FileDownloadResponse]: + """Download multiple files from the AgentCube sandbox. + + Args: + paths: A list of remote file paths to download. + + Returns: + A list of FileDownloadResponse objects containing file contents. + """ + results = [] + for path in paths: + try: + # AgentCube download_file writes to a local path + # We use a temp file to read it into memory for the response + with tempfile.NamedTemporaryFile(delete=False) as tmp: + tmp_path = tmp.name + + try: + self._client.download_file(path, tmp_path) + with open(tmp_path, "rb") as f: + content = f.read() + results.append(FileDownloadResponse(path=path, content=content, error=None)) + finally: + if os.path.exists(tmp_path): + os.remove(tmp_path) + except Exception as e: + results.append(FileDownloadResponse(path=path, content=b"", error=str(e))) + return results + + # --- Async Support --- + + async def aexecute( + self, + command: str, + *, + timeout: int | None = None, + ) -> ExecuteResponse: + """Async version of execute. Offloaded to thread pool.""" + return await asyncio.to_thread(self.execute, command, timeout=timeout) + + async def aupload_files( + self, + files: list[tuple[str, bytes]], + ) -> list[FileUploadResponse]: + """Async version of upload_files. Offloaded to thread pool.""" + return await asyncio.to_thread(self.upload_files, files) + + async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]: + """Async version of download_files. Offloaded to thread pool.""" + return await asyncio.to_thread(self.download_files, paths) diff --git a/sdk-python/examples/test_langchain_sandbox.py b/sdk-python/examples/test_langchain_sandbox.py new file mode 100644 index 00000000..742fbe09 --- /dev/null +++ b/sdk-python/examples/test_langchain_sandbox.py @@ -0,0 +1,92 @@ +# Copyright The Volcano Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import asyncio +from agentcube import CodeInterpreterClient +from agentcube.integrations.langchain import AgentCubeSandbox + +async def test_sandbox_provider(): + """ + Test script to verify AgentCube as a LangChain-compatible sandbox provider. + This demonstrates the BaseSandbox interface compliance. + """ + print("๐Ÿ› ๏ธ Initializing AgentCube LangChain Sandbox Provider...") + + # 1. Setup the client + # Ensure ROUTER_URL is set in your environment + try: + client = CodeInterpreterClient(name="test-sandbox", verbose=False) + except Exception as e: + print(f"โŒ Failed to initialize client: {e}") + return + + # 2. Initialize the Sandbox Provider + # This object implements the LangChain BaseSandbox interface + sandbox = AgentCubeSandbox(client) + print(f"โœ… Sandbox initialized with ID: {sandbox.id}") + + try: + # 3. Test isolated execution (BaseSandbox.execute) + print("\n๐Ÿ“ Testing code execution...") + cmd = "python3 -c \"print('Hello from AgentCube Sandbox!'); import os; print(f'Working dir: {os.getcwd()}')\"" + response = await sandbox.aexecute(cmd) + + print(f"--- Output ---\n{response.output}") + print(f"Exit Code: {response.exit_code}") + + if response.exit_code == 0: + print("โœ… Execution successful.") + + # 4. Test file management (BaseSandbox.upload_files) + print("\n๐Ÿ“‚ Testing file upload...") + # Note: Using text content as SDK only supports str for now (per maintainer request) + files_to_upload = [ + ("greeting.txt", b"Hello LangChain!"), + ("config.json", b'{"status": "isolated"}') + ] + upload_results = await sandbox.aupload_files(files_to_upload) + for res in upload_results: + if not res.error: + print(f"โœ… Uploaded: {res.path}") + else: + print(f"โŒ Upload failed for {res.path}: {res.error}") + + # 5. Verify files exist and download them (BaseSandbox.download_files) + print("\n๐Ÿ” Verifying and downloading files...") + # Check files via execution + ls_res = await sandbox.aexecute("ls -lh greeting.txt config.json") + print(ls_res.output) + + # Download back + download_results = await sandbox.adownload_files(["greeting.txt"]) + for res in download_results: + if not res.error: + print(f"โœ… Downloaded {res.path}: '{res.content.decode()}'") + else: + print(f"โŒ Download failed for {res.path}: {res.error}") + + finally: + # 6. Cleanup the session + print("\n๐Ÿงน Cleaning up session...") + client.stop() + print("โœจ Done.") + +if __name__ == "__main__": + # Check for ROUTER_URL before running + if not os.getenv("ROUTER_URL"): + print("โš ๏ธ Warning: ROUTER_URL environment variable is not set.") + print("Please set it before running (e.g., export ROUTER_URL=http://localhost:8080)") + + asyncio.run(test_sandbox_provider()) diff --git a/sdk-python/pyproject.toml b/sdk-python/pyproject.toml index ddd14737..47cf3fe4 100644 --- a/sdk-python/pyproject.toml +++ b/sdk-python/pyproject.toml @@ -16,6 +16,9 @@ dependencies = [ "cryptography" ] +[project.optional-dependencies] +langchain = ["deepagents", "langchain-core", "langchain-tests", "pytest-asyncio"] + [tool.setuptools.packages.find] where = ["."] include = ["agentcube*"] diff --git a/sdk-python/tests/test_langchain_e2e.py b/sdk-python/tests/test_langchain_e2e.py new file mode 100644 index 00000000..e1e25120 --- /dev/null +++ b/sdk-python/tests/test_langchain_e2e.py @@ -0,0 +1,86 @@ +# Copyright The Volcano Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import pytest +from agentcube import CodeInterpreterClient +from agentcube.integrations.langchain import AgentCubeSandbox + +# Skip these tests unless the E2E environment variables are set +pytestmark = pytest.mark.skipif( + not os.getenv("ROUTER_URL") or not os.getenv("WORKLOAD_MANAGER_URL"), + reason="E2E environment variables (ROUTER_URL, WORKLOAD_MANAGER_URL) not set" +) + +@pytest.fixture +async def sandbox(): + """Fixture to manage the lifecycle of an AgentCubeSandbox during E2E tests.""" + client = CodeInterpreterClient(name="e2e-test-sandbox", verbose=False) + sb = AgentCubeSandbox(client) + yield sb + # Cleanup after test + client.stop() + +@pytest.mark.asyncio +async def test_langchain_sandbox_e2e_flow(sandbox): + """ + E2E test verifying the core BaseSandbox interface against a real backend. + Matches the flow in examples/test_langchain_sandbox.py + """ + + # 1. Test Command Execution + cmd = "python3 -c \"print('e2e_success')\"" + response = await sandbox.aexecute(cmd) + + assert response.exit_code == 0 + assert "e2e_success" in response.output + + # 2. Test File Upload + files_to_upload = [ + ("e2e_test.txt", b"e2e_content"), + ("data.json", b'{"key": "value"}') + ] + upload_results = await sandbox.aupload_files(files_to_upload) + + assert len(upload_results) == 2 + for res in upload_results: + assert res.error is None + assert res.path in ["e2e_test.txt", "data.json"] + + # 3. Verify files exist via remote ls + ls_res = await sandbox.aexecute("ls e2e_test.txt data.json") + assert ls_res.exit_code == 0 + assert "e2e_test.txt" in ls_res.output + assert "data.json" in ls_res.output + + # 4. Test File Download + download_results = await sandbox.adownload_files(["e2e_test.txt"]) + + assert len(download_results) == 1 + assert download_results[0].error is None + assert download_results[0].path == "e2e_test.txt" + assert download_results[0].content == b"e2e_content" + +@pytest.mark.asyncio +async def test_langchain_sandbox_environment_isolation(sandbox): + """Verify that multiple commands share the same stateful environment.""" + + # Set a variable in one command + await sandbox.aexecute("echo 'persisted_state' > state.txt") + + # Read it in another + response = await sandbox.aexecute("cat state.txt") + + assert response.exit_code == 0 + assert "persisted_state" in response.output diff --git a/sdk-python/tests/test_langchain_integration.py b/sdk-python/tests/test_langchain_integration.py new file mode 100644 index 00000000..39f963a0 --- /dev/null +++ b/sdk-python/tests/test_langchain_integration.py @@ -0,0 +1,106 @@ +# Copyright The Volcano Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import MagicMock, patch + +from agentcube.integrations.langchain import AgentCubeSandbox +from agentcube.exceptions import CommandExecutionError + +class TestAgentCubeSandbox(unittest.TestCase): + """Test the LangChain Sandbox integration.""" + + def setUp(self): + self.mock_client = MagicMock() + self.mock_client.session_id = "test-session-123" + self.sandbox = AgentCubeSandbox(self.mock_client) + + def test_id_property(self): + """Test the id property returns session_id.""" + self.assertEqual(self.sandbox.id, "test-session-123") + + def test_execute_success(self): + """Test execute command success.""" + self.mock_client.execute_command.return_value = "hello world" + + response = self.sandbox.execute("echo hello world") + + self.assertEqual(response.output, "hello world") + self.assertEqual(response.exit_code, 0) + self.assertFalse(response.truncated) + self.mock_client.execute_command.assert_called_once_with("echo hello world", timeout=None) + + def test_execute_failure(self): + """Test execute command failure (CommandExecutionError).""" + self.mock_client.execute_command.side_effect = CommandExecutionError( + exit_code=127, stdout="some output", stderr="command not found", command="invalid" + ) + + response = self.sandbox.execute("invalid") + + # Expect combined output + self.assertEqual(response.output, "some output\ncommand not found") + self.assertEqual(response.exit_code, 127) + self.mock_client.execute_command.assert_called_once() + + def test_upload_files(self): + """Test uploading multiple files.""" + files = [ + ("test1.txt", b"hello"), + ("test2.txt", b"world") + ] + + responses = self.sandbox.upload_files(files) + + self.assertEqual(len(responses), 2) + self.assertEqual(responses[0].path, "test1.txt") + self.assertIsNone(responses[0].error) + self.assertEqual(responses[1].path, "test2.txt") + self.assertIsNone(responses[1].error) + + # Verify client calls + self.assertEqual(self.mock_client.write_file.call_count, 2) + self.mock_client.write_file.assert_any_call("hello", "test1.txt") + self.mock_client.write_file.assert_any_call("world", "test2.txt") + + @patch("os.path.exists", return_value=True) + @patch("os.remove") + @patch("builtins.open", new_callable=MagicMock) + @patch("tempfile.NamedTemporaryFile") + def test_download_files(self, mock_tmpfile, mock_open, mock_remove, mock_exists): + """Test downloading files.""" + # Setup mock temp file + mock_tmpfile.return_value.__enter__.return_value.name = "/tmp/fake_path" + + # Mock file content + file_content = b"file content" + mock_open.return_value.__enter__.return_value.read.return_value = file_content + + paths = ["remote1.txt"] + + responses = self.sandbox.download_files(paths) + + self.assertEqual(len(responses), 1) + self.assertEqual(responses[0].path, "remote1.txt") + self.assertEqual(responses[0].content, file_content) + self.assertIsNone(responses[0].error) + + self.mock_client.download_file.assert_called_once() + self.assertTrue(mock_open.called) + # Verify cleanup + mock_exists.assert_called_with("/tmp/fake_path") + mock_remove.assert_called_with("/tmp/fake_path") + +if __name__ == "__main__": + unittest.main() diff --git a/sdk-python/tests/test_langchain_standard.py b/sdk-python/tests/test_langchain_standard.py new file mode 100644 index 00000000..73ab9dad --- /dev/null +++ b/sdk-python/tests/test_langchain_standard.py @@ -0,0 +1,61 @@ +# Copyright The Volcano Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Iterator +import pytest +from unittest.mock import MagicMock + +from agentcube.integrations.langchain import AgentCubeSandbox +from agentcube.code_interpreter import CodeInterpreterClient + +try: + from langchain_tests.integration_tests import SandboxIntegrationTests + HAS_LANGCHAIN_TESTS = True +except ImportError: + # Fallback for CI environments where optional dependencies are not installed + class SandboxIntegrationTests: # type: ignore + pass + HAS_LANGCHAIN_TESTS = False + +@pytest.mark.skipif(not HAS_LANGCHAIN_TESTS, reason="langchain-tests not installed") +class TestAgentCubeSandboxStandard(SandboxIntegrationTests): + """Standard LangChain integration tests for AgentCubeSandbox.""" + + @pytest.fixture(scope="class") + def sandbox(self) -> Iterator[AgentCubeSandbox]: + """Provide a configured AgentCubeSandbox for testing. + + Note: This currently uses a mocked backend to allow CI execution. + To test against a real backend, provide the necessary environment variables + (ROUTER_URL, etc.) and remove the mocking logic. + """ + # For standard integration tests, we provide a mocked client + # that simulates the behavior required by the test suite. + mock_client = MagicMock(spec=CodeInterpreterClient) + mock_client.session_id = "test-session-id" + + # Simulate successful command execution + mock_client.execute_command.return_value = "standard output" + + # Simulate file operations + mock_client.list_files.return_value = [] + + # Return the sandbox with the mocked client + backend = AgentCubeSandbox(client=mock_client) + + try: + yield backend + finally: + # Cleanup + mock_client.stop()