-
Notifications
You must be signed in to change notification settings - Fork 58
feat(sdk): implement LangChain Code Interpreter sandbox provider #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
638736f
a75dc32
53eaa39
c441064
ab60921
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
+21
to
22
|
||
| self.stdout = stdout | ||
| self.stderr = stderr | ||
| self.command = command | ||
|
Comment on lines
+21
to
25
|
||
| 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""" | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.""" |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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')") | ||||||
|
||||||
| response = sandbox.execute("print('hello world')") | |
| response = sandbox.execute("python3 -c \"print('hello world')\"") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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") | ||||||||||
|
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") |
Copilot
AI
Apr 27, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 surroundingifblock, which is easy to miss in reviews and can trip formatters/lint rules. Align the indentation to a single level inside theif result["exit_code"] != 0:block.