Skip to content

feat(sdk): implement LangChain Code Interpreter sandbox provider#290

Open
yashisrani wants to merge 5 commits intovolcano-sh:mainfrom
yashisrani:implement/sandboxProvider
Open

feat(sdk): implement LangChain Code Interpreter sandbox provider#290
yashisrani wants to merge 5 commits intovolcano-sh:mainfrom
yashisrani:implement/sandboxProvider

Conversation

@yashisrani
Copy link
Copy Markdown
Contributor

What type of PR is this?

What this PR does / why we need it:

  • This PR Adds AgentCube Code Interpreter as a LangChain-compatible sandbox provider by implementing the BaseSandbox interface. Enables secure, isolated code execution for LangChain agents.

Key Changes

LangChain Integration

  • Added agentcube.integrations.langchain with AgentCubeSandbox
  • Implemented required methods: execute, upload_files, download_files
  • Added async support (aexecute, etc.)
  • Optional dependency handling for LangChain/DeepAgents

SDK Enhancements

  • write_file now supports binary (bytes) data
  • Data Plane updated for base64 encoding of raw bytes

Testing

  • Added integration tests for full sandbox lifecycle
  • Verified execution output, exit codes, and binary file transfers

Testing Performed

  • All tests passed (5/5) via pytest
  • Linting passed with ruff
  • Confirmed correct error mapping to ExecuteResponse

Which issue(s) this PR fixes:
Fixes #278

Special notes for your reviewer:

Does this PR introduce a user-facing change?:

NONE

Signed-off-by: Yash Israni <118755067+yashisrani@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 19, 2026 15:37
@volcano-sh-bot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign acsoto for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces LangChain integration for the AgentCube SDK through the new AgentCubeSandbox class and associated unit tests. Additionally, the write_file method has been updated to support both string and binary content. Feedback was provided regarding the implementation of asynchronous methods in the LangChain integration, which currently execute blocking synchronous calls and should be offloaded to separate threads to avoid blocking the event loop.

Comment on lines +174 to +192
async def aexecute(
self,
command: str,
*,
timeout: int | None = None,
) -> ExecuteResponse:
"""Async version of execute. Currently wraps synchronous call."""
return self.execute(command, timeout=timeout)

async def aupload_files(
self,
files: list[tuple[str, bytes]],
) -> list[FileUploadResponse]:
"""Async version of upload_files. Currently wraps synchronous call."""
return self.upload_files(files)

async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
"""Async version of download_files. Currently wraps synchronous call."""
return self.download_files(paths)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The async methods aexecute, aupload_files, and adownload_files are currently implemented as synchronous blocking calls. Since the underlying CodeInterpreterClient uses the requests library for network I/O, these methods will block the entire event loop when called in an asynchronous context. This can lead to performance issues in applications that rely on non-blocking behavior (e.g., LangChain agents running multiple tasks concurrently).

Consider using asyncio.to_thread (available in Python 3.9+) to offload these blocking calls to a separate thread. This ensures that the event loop remains responsive.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Python SDK integration so AgentCube Code Interpreter sessions can be used as a LangChain/DeepAgents-compatible sandbox provider, including binary file transfer support.

Changes:

  • Introduces AgentCubeSandbox implementing the (DeepAgents) BaseSandbox interface with sync + async methods.
  • Extends write_file to accept bytes end-to-end and base64-encode raw bytes in the data-plane client.
  • Adds a unit test suite for the LangChain sandbox wrapper.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
sdk-python/agentcube/integrations/langchain.py Adds AgentCubeSandbox adapter with optional DeepAgents dependency handling, execute/upload/download, and async wrappers
sdk-python/agentcube/integrations/init.py Introduces the integrations package
sdk-python/agentcube/code_interpreter.py Broadens write_file to accept `str
sdk-python/agentcube/clients/code_interpreter_data_plane.py Updates write_file to base64-encode either text or raw bytes
sdk-python/tests/test_langchain_integration.py Adds unit tests for AgentCubeSandbox behavior
Comments suppressed due to low confidence (1)

sdk-python/agentcube/code_interpreter.py:206

  • CodeInterpreterClient.write_file now accepts bytes, but the existing unit tests for CodeInterpreterClient don't cover passing binary content. Adding a test that passes bytes and asserts the data-plane write_file call (and/or correct request payload) would prevent regressions in binary transfer support.
    def write_file(self, content: Union[str, bytes], remote_path: str):
        """
        Write content to a file in the remote environment.

        Args:
            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.
        """
        self.dp_client.write_file(content, remote_path)

Comment on lines +76 to +78
@patch("os.remove")
@patch("builtins.open", new_callable=MagicMock)
def test_download_files(self, mock_open, mock_remove):
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test, patching os.remove prevents the temp file created by NamedTemporaryFile(delete=False) inside download_files() from actually being deleted, so the test leaks a real file into the system temp directory. Consider not mocking os.remove, or alternatively mock tempfile.NamedTemporaryFile/os.path.exists so no real file is created.

Suggested change
@patch("os.remove")
@patch("builtins.open", new_callable=MagicMock)
def test_download_files(self, mock_open, mock_remove):
@patch("builtins.open", new_callable=MagicMock)
def test_download_files(self, mock_open):

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +35
# Internal types for base class compliance
try:
from deepagents.backends.protocol import (
ExecuteResponse,
FileDownloadResponse,
FileUploadResponse,
)
from deepagents.backends.sandbox import BaseSandbox
except ImportError:
# Define fallback classes if deepagents is not installed
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching a broad ImportError here can unintentionally mask real import-time failures inside deepagents (e.g., transitive dependency issues or API changes) and silently fall back to the stub types, making debugging difficult. Prefer catching ModuleNotFoundError specifically for the missing optional dependency (and/or checking e.name), and re-raising other import errors with a helpful message.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you fix this?

Comment on lines +77 to +81

@property
def id(self) -> str:
"""Return the unique session ID of the sandbox instance."""
return self._client.session_id or "unknown"
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id can change from a real session ID to the literal string "unknown" after the underlying CodeInterpreterClient.stop() clears session_id, which can break callers that rely on sandbox IDs being stable. Consider capturing the initial session ID in __init__ and always returning that, or raising if the sandbox has been stopped instead of returning a placeholder.

Suggested change
@property
def id(self) -> str:
"""Return the unique session ID of the sandbox instance."""
return self._client.session_id or "unknown"
self._id = client.session_id or "unknown"
@property
def id(self) -> str:
"""Return the unique session ID of the sandbox instance."""
return self._id

Copilot uses AI. Check for mistakes.
Comment on lines +174 to +192
async def aexecute(
self,
command: str,
*,
timeout: int | None = None,
) -> ExecuteResponse:
"""Async version of execute. Currently wraps synchronous call."""
return self.execute(command, timeout=timeout)

async def aupload_files(
self,
files: list[tuple[str, bytes]],
) -> list[FileUploadResponse]:
"""Async version of upload_files. Currently wraps synchronous call."""
return self.upload_files(files)

async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
"""Async version of download_files. Currently wraps synchronous call."""
return self.download_files(paths)
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The async methods (aexecute/aupload_files/adownload_files) currently call the synchronous implementations directly, which will block the event loop when used from async LangChain/DeepAgents code. If these methods are part of the expected async interface, run the sync work in a thread (e.g., via asyncio.to_thread/executor) or provide a truly async client implementation.

Copilot uses AI. Check for mistakes.
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 19, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 47.57%. Comparing base (57f6d84) to head (ab60921).
⚠️ Report is 70 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #290      +/-   ##
==========================================
+ Coverage   43.37%   47.57%   +4.19%     
==========================================
  Files          30       30              
  Lines        2610     2819     +209     
==========================================
+ Hits         1132     1341     +209     
+ Misses       1355     1338      -17     
- Partials      123      140      +17     
Flag Coverage Δ
unittests 47.57% <ø> (+4.19%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines +101 to +112
return ExecuteResponse(
output=stdout,
exit_code=0,
truncated=False,
)
except CommandExecutionError as e:
# Map AgentCube execution error
return ExecuteResponse(
output=e.stderr or "",
exit_code=e.exit_code,
truncated=False,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve combined stdout/stderr in ExecuteResponse instead of returning stdout on success and stderr on failure separately?
It's the useful context for the agent

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to add the standard LangChain sandbox integration tests as well?
https://docs.langchain.com/oss/python/contributing/implement-langchain

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's necessary to provide a demo

Comment on lines +26 to +35
# Internal types for base class compliance
try:
from deepagents.backends.protocol import (
ExecuteResponse,
FileDownloadResponse,
FileUploadResponse,
)
from deepagents.backends.sandbox import BaseSandbox
except ImportError:
# Define fallback classes if deepagents is not installed
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you fix this?

Copy link
Copy Markdown
Member

@hzxuzhonghu hzxuzhonghu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this is a clean LangChain integration, but there are a few correctness issues to address before merging.

Notable omission: pyproject.toml has no [project.optional-dependencies] section for deepagents / langchain. Since the import is guarded with a try/except, pip install agentcube-sdk silently installs a non-functional integration. Users have no documented way to install the required extras (e.g. pip install agentcube-sdk[langchain]). Please add:

[project.optional-dependencies]
langchain = ["deepagents", "langchain-core"]

exit_code=e.exit_code,
truncated=False,
)
except Exception as e:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This except Exception catch-all silently converts any unexpected error — AttributeError, TypeError, transient network glitches, programming bugs — to exit_code=1. This makes production debugging very hard: a typo in the _client plumbing would surface as a successful-looking exit_code=1 response instead of a traceback.

Only catch errors you can meaningfully translate here. Re-raise anything else:

except CommandExecutionError as e:
    return ExecuteResponse(output=e.stderr or "", exit_code=e.exit_code, truncated=False)
# Let unexpected exceptions propagate — don't swallow them

If you need a safety net, at minimum log the unexpected exception before returning.

timeout: int | None = None,
) -> ExecuteResponse:
"""Async version of execute. Currently wraps synchronous call."""
return self.execute(command, timeout=timeout)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three async methods (aexecute, aupload_files, adownload_files) call blocking synchronous I/O directly on the event-loop thread. When a LangChain agent awaits any of these, the entire asyncio event loop stalls for the full round-trip HTTP latency, blocking every other coroutine.

Use asyncio.to_thread (Python ≥ 3.9) to offload to a thread pool:

import asyncio

async def aexecute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse:
    return await asyncio.to_thread(self.execute, command, timeout=timeout)

async def aupload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
    return await asyncio.to_thread(self.upload_files, files)

async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
    return await asyncio.to_thread(self.download_files, paths)

For Python < 3.9 compatibility use loop.run_in_executor(None, ...) instead.

self.mock_client.write_file.assert_any_call(b"world", "test2.txt")

@patch("os.remove")
@patch("builtins.open", new_callable=MagicMock)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two patches are missing here, causing the test to touch the real filesystem:

  1. tempfile.NamedTemporaryFile is not patched → the test creates a real temp file in /tmp. Because os.remove is mocked to a no-op, that file is never cleaned up and accumulates across test runs.
  2. os.path.exists is not patched → in the finally block, the real os.path.exists is called on the real temp path; if the cleanup ever does run it would hit the live filesystem.

Fix by also patching these in the decorator chain:

@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):
    mock_tmpfile.return_value.__enter__.return_value.name = "/tmp/fake_path"
    ...

if isinstance(content, str):
content_bytes = content.encode('utf-8')
else:
content_bytes = content
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make this change separately in another pr?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the sandboxer provider in a separate dir. Utimately, i would like to see it is accepted in langchain similar as aws agent core.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# See the License for the specific language governing permissions and
# limitations under the License.

"""LangChain integration for AgentCube Code Interpreter."""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""LangChain integration for AgentCube Code Interpreter."""
"""AgentCube Code Interpreter sandbox integration for Langchain."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's necessary to provide a demo

Signed-off-by: Yash Israni <118755067+yashisrani@users.noreply.github.com>
Signed-off-by: Yash Israni <118755067+yashisrani@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 24, 2026 06:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (1)

sdk-python/agentcube/code_interpreter.py:206

  • CodeInterpreterClient.write_file now accepts bytes, but it forwards directly to dp_client.write_file(...). The current data-plane implementation still treats content as str and calls .encode(...), which will fail for bytes input. Either update the data-plane write_file to handle bytes (base64 encode bytes directly) or convert bytes to the expected wire format here before delegating.
    def write_file(self, content: Union[str, bytes], remote_path: str):
        """
        Write content to a file in the remote environment.

        Args:
            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.
        """
        self.dp_client.write_file(content, remote_path)

Comment on lines 156 to 162
if result["exit_code"] != 0:
raise CommandExecutionError(
exit_code=result["exit_code"],
stderr=result["stderr"],
stdout=stdout,
stderr=stderr,
command=command
)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The raise CommandExecutionError(...) line is over-indented relative to the surrounding if block, which is easy to miss in reviews and can trip formatters/lint rules. Align the indentation to a single level inside the if result["exit_code"] != 0: block.

Copilot uses AI. Check for mistakes.
client = CodeInterpreterClient()
sandbox = AgentCubeSandbox(client)

response = sandbox.execute("print('hello world')")
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage example calls sandbox.execute("print('hello world')"), but execute runs a shell command in the sandbox. As written, this will fail on most systems unless there happens to be a print(...) executable. Use a shell command (e.g., python3 -c ... or echo ...) in the example to match the implementation.

Suggested change
response = sandbox.execute("print('hello world')")
response = sandbox.execute("python3 -c \"print('hello world')\"")

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +58
# 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"}')
]
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says the SDK "only supports str for now", but the code passes bytes and this PR also updates write_file to accept bytes. Please update the comment (and/or the example payload type) to reflect the actual behavior so users don't get conflicting guidance.

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +147
# 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")
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upload_files decodes bytes to UTF-8 strings before calling write_file, but this PR updates CodeInterpreterClient.write_file to accept raw bytes and the LangChain file API is explicitly bytes. Decoding will corrupt binary data and can raise UnicodeDecodeError, and it also contradicts the method type hint (list[tuple[str, bytes]]). Pass bytes through unchanged (and only accept/convert str if needed).

Suggested change
# 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")

Copilot uses AI. Check for mistakes.
self.assertEqual(responses[0].content, file_content)
self.assertIsNone(responses[0].error)

self.mock_client.download_file.assert_called_once()
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts download_file was called with no arguments, but AgentCubeSandbox.download_files calls self._client.download_file(path, tmp_path). Update the assertion to check the expected arguments (including the temp path) so the test actually validates the integration.

Suggested change
self.mock_client.download_file.assert_called_once()
self.mock_client.download_file.assert_called_once_with("remote1.txt", "/tmp/fake_path")

Copilot uses AI. Check for mistakes.
Comment on lines +21 to 22
def __init__(self, exit_code: int, stdout: str, stderr: str, command: str = None):
self.exit_code = exit_code
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

command is annotated as str = None, which is an invalid type/default pairing and will be flagged by type checkers. Use an optional type (e.g., command: str | None = None) and consider widening it to match callers (the data-plane client can pass a list of argv).

Copilot uses AI. Check for mistakes.
Signed-off-by: Yash Israni <118755067+yashisrani@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

u can also add a e2e test based on it

Signed-off-by: Yash Israni <118755067+yashisrani@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 27, 2026 12:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

sdk-python/agentcube/code_interpreter.py:206

  • CodeInterpreterClient.write_file now advertises Union[str, bytes], but it forwards directly to dp_client.write_file(...) which (currently) expects str and calls .encode('utf-8') on the content. Passing bytes will raise an AttributeError at runtime. Either update the data-plane client's write_file to accept bytes (base64-encode raw bytes) or coerce bytes to the required form here before delegating.
    def write_file(self, content: Union[str, bytes], remote_path: str):
        """
        Write content to a file in the remote environment.

        Args:
            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.
        """
        self.dp_client.write_file(content, remote_path)

Comment on lines +59 to +75
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")
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test currently asserts write_file is called with decoded strings ("hello"/"world"), but the integration is intended to support binary transfers (bytes). Once AgentCubeSandbox.upload_files passes bytes through (instead of decoding), these assertions will fail and the test won't validate binary behavior. Update the test to assert bytes are forwarded and (optionally) add a case that includes non-UTF8 bytes to ensure true binary support.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +55
@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)
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SandboxIntegrationTests are only enabled when langchain-tests is installed, but the provided sandbox fixture uses a very minimal MagicMock backend. If langchain-tests is present, the upstream standard suite is likely to exercise more behaviors than execute_command/list_files, and even if it passes it won't validate real AgentCube integration. Consider skipping these tests unless a real backend is configured via env vars, or provide a more complete fake that matches what SandboxIntegrationTests expects.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +36
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):
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file defines async def tests/fixtures with @pytest.mark.asyncio, but the repo's Python SDK CI workflow installs only pytest (no pytest-asyncio). Without the plugin, pytest will error at collection before the skipif can take effect. Either add a module-level pytest.importorskip("pytest_asyncio") (and use pytest_asyncio.fixture), or ensure pytest-asyncio is installed in the SDK test job so these tests can be safely collected.

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +148
# 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)
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upload_files unconditionally decodes bytes as UTF-8 before calling write_file. This will corrupt arbitrary binary uploads and can raise UnicodeDecodeError for non-text content. Since the SDK is being updated to support bytes, prefer passing bytes through to self._client.write_file (and broaden the type to accept str | bytes if needed) rather than decoding here.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to 25
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
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type hint mismatch: command defaults to None but is annotated as str. This should be Optional[str] (or str | None) to match the actual allowed value and avoid type-checker errors.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

@hzxuzhonghu hzxuzhonghu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement as langchain codeinterpreter sandbox provider

7 participants