Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/langgraph-integration/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
OPENAI_API_KEY="<your-api-key>"

# Pick ONE OpenAI-compatible endpoint:
# DeepSeek:
OPENAI_BASE_URL="https://api.deepseek.com/v1"
# TokenHub:
# OPENAI_BASE_URL="https://tokenhub.tencentmaas.com/v1"

E2B_API_URL="http://<your-cube-sandbox-ip>:3000"
E2B_API_KEY="<your-e2b-api-key>"
CUBE_TEMPLATE_ID="<your-template-id>"

# mkcert root CA for E2B data-plane HTTPS (*.cube.app)
CUBE_SSL_CERT_FILE="/etc/pki/ca-trust/source/anchors/mkcert_<id>.pem"
3 changes: 3 additions & 0 deletions examples/langgraph-integration/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.venv/
.env
__pycache__/
97 changes: 97 additions & 0 deletions examples/langgraph-integration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# LangGraph + CubeSandbox Example

[中文](README_zh.md)

This directory shows how to connect a [LangGraph](https://github.com/langchain-ai/langgraph) agent to [CubeSandbox](https://github.com/TencentCloud/CubeSandbox).

It uses [Deep Agents](https://github.com/langchain-ai/deepagents) `create_deep_agent()` with the E2B-compatible API so tool calls run inside CubeSandbox MicroVMs.

```text
LangGraph Agent (Deep Agents)
└── langchain-e2b.E2BSandbox
└── e2b.Sandbox ──► CubeAPI (:3000)
CubeSandbox MicroVM (envd)
```

## Prerequisites

- Python 3.11+
- Running CubeSandbox with CubeAPI reachable
- A sandbox template
- An OpenAI-compatible LLM API key

## Quick start

```bash
pip install -r requirements.txt
cp .env.example .env # API keys, CubeAPI URL, template ID, mkcert CA path

# Sandbox connectivity only (no LLM)
python e2b_demo.py --sandbox-only

# Full agent
python e2b_demo.py
python e2b_demo.py --model deepseek-chat --question "What Linux distro is this?"
```

## Environment variables

| Variable | Description |
|----------|-------------|
| `OPENAI_API_KEY` | LLM API key |
| `OPENAI_BASE_URL` | LLM endpoint (e.g. DeepSeek) |
| `E2B_API_URL` | CubeAPI URL |
| `E2B_API_KEY` | CubeAPI auth key |
| `CUBE_TEMPLATE_ID` | Sandbox template ID |
| `CUBE_SSL_CERT_FILE` | mkcert root CA for E2B data-plane HTTPS |

## Files

| File | Description |
|------|-------------|
| `e2b_demo.py` | Entry script |
| `demo_common.py` | Agent and sandbox connectivity helpers |
| `cube_patches.py` | envd patches (root user, stdin) |
| `env_utils.py` | Load `.env`, validate variables |
| `llm_utils.py` | OpenAI-compatible LLM client |

## CubeSandbox adaptations

`cube_patches.py` runs before the E2B SDK is used:

| Patch | Reason |
|-------|--------|
| `default_username = "root"` | Cube envd only supports root |
| Drop `stdin` kwarg | Older envd rejects it |
| `workdir = "/root"` | Cube images use root |
| `CUBE_SSL_CERT_FILE` → `SSL_CERT_FILE` | Trust mkcert CA for E2B HTTPS (same as openai-agents-example) |

The LLM client uses certifi + `trust_env=False` so `SSL_CERT_FILE` does not affect DeepSeek and other public HTTPS APIs.

Point `CUBE_SSL_CERT_FILE` at the **mkcert root CA**, not `cube-root-ca.crt`.

## Core code

```python
from cube_patches import apply_cube_envd_patches
apply_cube_envd_patches()

from deepagents import create_deep_agent
from e2b import Sandbox
from langchain_e2b import E2BSandbox

sandbox = Sandbox.create(template=template_id, timeout=300)
backend = E2BSandbox(sandbox=sandbox, workdir="/root", timeout=300)

agent = create_deep_agent(model=chat_model, backend=backend, system_prompt="...")
result = agent.invoke({"messages": [{"role": "user", "content": question}]})
```

## Related docs

- [LangGraph docs](https://docs.langchain.com/oss/python/langgraph/overview)
- [Deep Agents sandboxes](https://docs.langchain.com/oss/python/deepagents/sandboxes)
- [OpenAI Agents example](../openai-agents-example/README.md)
97 changes: 97 additions & 0 deletions examples/langgraph-integration/README_zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# LangGraph + CubeSandbox 示例

[English](README.md)

本目录演示如何将 [LangGraph](https://github.com/langchain-ai/langgraph) Agent 接入 [CubeSandbox](https://github.com/TencentCloud/CubeSandbox) 沙箱。

使用 [Deep Agents](https://github.com/langchain-ai/deepagents) 的 `create_deep_agent()` 构建 LangGraph Agent,通过 E2B 兼容 API 在 CubeSandbox MicroVM 内执行工具。

```text
LangGraph Agent (Deep Agents)
└── langchain-e2b.E2BSandbox
└── e2b.Sandbox ──► CubeAPI (:3000)
CubeSandbox MicroVM (envd)
```

## 前置条件

- Python 3.11+
- CubeSandbox 已部署,CubeAPI 可访问
- 已创建沙箱模板
- OpenAI 兼容 LLM API Key

## 快速开始

```bash
pip install -r requirements.txt
cp .env.example .env # 填入 API Key、CubeAPI 地址、模板 ID、mkcert 证书路径

# 仅测沙箱(无需 LLM)
python e2b_demo.py --sandbox-only

# 完整 Agent
python e2b_demo.py
python e2b_demo.py --model deepseek-chat --question "What Linux distro is this?"
```

## 环境变量

| 变量 | 说明 |
|------|------|
| `OPENAI_API_KEY` | LLM API Key |
| `OPENAI_BASE_URL` | LLM 地址(如 DeepSeek) |
| `E2B_API_URL` | CubeAPI 地址 |
| `E2B_API_KEY` | CubeAPI 鉴权 Key |
| `CUBE_TEMPLATE_ID` | 沙箱模板 ID |
| `CUBE_SSL_CERT_FILE` | E2B 数据面 HTTPS 所需 mkcert 根证书 |

## 文件说明

| 文件 | 说明 |
|------|------|
| `e2b_demo.py` | 入口脚本 |
| `demo_common.py` | Agent / 沙箱连通性测试 |
| `cube_patches.py` | envd 兼容补丁(root 用户、stdin) |
| `env_utils.py` | 加载 `.env`、校验环境变量 |
| `llm_utils.py` | 构建 OpenAI 兼容 LLM 客户端 |

## CubeSandbox 适配

`cube_patches.py` 在导入 E2B SDK 前应用:

| 补丁 | 原因 |
|------|------|
| `default_username = "root"` | Cube envd 只支持 root |
| 移除 `stdin` 参数 | 旧版 envd 不支持 |
| `workdir = "/root"` | Cube 镜像以 root 为主 |
| `CUBE_SSL_CERT_FILE` → `SSL_CERT_FILE` | E2B 数据面 HTTPS 信任 mkcert CA(同 openai-agents-example) |

LLM 客户端使用 certifi 公网 CA + `trust_env=False`,避免 `SSL_CERT_FILE` 污染 DeepSeek 等 HTTPS 请求。

`CUBE_SSL_CERT_FILE` 须指向 **mkcert 根证书**,不要用 `cube-root-ca.crt`。

## 核心代码

```python
from cube_patches import apply_cube_envd_patches
apply_cube_envd_patches()

from deepagents import create_deep_agent
from e2b import Sandbox
from langchain_e2b import E2BSandbox

sandbox = Sandbox.create(template=template_id, timeout=300)
backend = E2BSandbox(sandbox=sandbox, workdir="/root", timeout=300)

agent = create_deep_agent(model=chat_model, backend=backend, system_prompt="...")
result = agent.invoke({"messages": [{"role": "user", "content": question}]})
```

## 相关文档

- [LangGraph 文档](https://docs.langchain.com/oss/python/langgraph/overview)
- [Deep Agents 沙箱文档](https://docs.langchain.com/oss/python/deepagents/sandboxes)
- [OpenAI Agents 集成示例](../openai-agents-example/README_zh.md)
85 changes: 85 additions & 0 deletions examples/langgraph-integration/cube_patches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright (c) 2026 Tencent Inc.
# SPDX-License-Identifier: Apache-2.0

"""CubeSandbox envd compatibility patches for the E2B Python SDK.

Cube envd only supports the ``root`` user, and older envd builds reject the
``stdin`` keyword on ``commands.run()``. Apply these patches before creating
sandboxes or importing langchain-e2b backends.
"""

from __future__ import annotations

import functools
import inspect
from typing import Any, Callable

_PATCHED = False


def _patch_filesystem(fs_cls: type) -> None:
for name in (
"read",
"write",
"write_files",
"list",
"exists",
"get_info",
"remove",
"rename",
"make_dir",
"watch_dir",
):
original = getattr(fs_cls, name, None)
if original is None:
continue

params = list(inspect.signature(original).parameters.keys())
user_pos = params.index("user") - 1 if "user" in params else None

def _make(fn: Callable[..., Any], user_index: int | None = user_pos) -> Callable[..., Any]:
@functools.wraps(fn)
def _wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
if user_index is not None and len(args) > user_index:
args_list = list(args)
if args_list[user_index] is None:
args_list[user_index] = "root"
args = tuple(args_list)
else:
kwargs.setdefault("user", "root")
return fn(self, *args, **kwargs)

return _wrapper

setattr(fs_cls, name, _make(original))


def _patch_commands(cmd_cls: type) -> None:
original_run = cmd_cls.run

@functools.wraps(original_run)
def _patched_run(self: Any, *args: Any, **kwargs: Any) -> Any:
if hasattr(self, "_envd_version"):
from e2b.sandbox_sync.commands.command import ENVD_COMMANDS_STDIN

if self._envd_version < ENVD_COMMANDS_STDIN:
kwargs.pop("stdin", None)
return original_run(self, *args, **kwargs)

cmd_cls.run = _patched_run


def apply_cube_envd_patches() -> None:
"""Patch sync E2B SDK classes for CubeSandbox envd."""
global _PATCHED
if _PATCHED:
return

import e2b.envd.rpc as e2b_rpc
from e2b.sandbox_sync.commands.command import Commands as SyncCommands
from e2b.sandbox_sync.filesystem.filesystem import Filesystem as SyncFilesystem

e2b_rpc.default_username = "root"
_patch_filesystem(SyncFilesystem)
_patch_commands(SyncCommands)
_PATCHED = True
85 changes: 85 additions & 0 deletions examples/langgraph-integration/demo_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright (c) 2026 Tencent Inc.
# SPDX-License-Identifier: Apache-2.0

"""Shared helpers for LangGraph / Deep Agents CubeSandbox demos."""

from __future__ import annotations

import os
import time
from typing import Protocol

from deepagents import create_deep_agent
from deepagents.backends.sandbox import BaseSandbox

from llm_utils import make_chat_model

CUBE_WORKDIR = "/root"
MARKER_PATH = "/tmp/langgraph-cube-marker.txt"
MARKER_CONTENT = "langgraph + cubesandbox\n"

SYSTEM_PROMPT = (
"You are a helpful assistant running inside a CubeSandbox MicroVM. "
"Use the execute tool and filesystem tools to inspect the environment. "
"Be concise and show command output when relevant."
)


class SandboxHandle(Protocol):
def kill(self) -> None: ...


def run_sandbox_check(*, backend: BaseSandbox, sandbox: SandboxHandle, label: str) -> None:
print(f"Backend: {label}")
print(f"CubeAPI: {os.environ['E2B_API_URL']}\n")

t0 = time.monotonic()
print(f"[create] sandbox_id={backend.id}")

result = backend.execute("uname -a && cat /etc/os-release | head -3")
print(f"[execute] exit={result.exit_code} {(time.monotonic() - t0) * 1000:.0f} ms")
print(result.output.strip())

backend.upload_files([(MARKER_PATH, MARKER_CONTENT.encode("utf-8"))])
read_back = backend.download_files([MARKER_PATH])[0]
if read_back.content is None:
raise RuntimeError(f"failed to read marker file: {read_back.error}")

text = read_back.content.decode("utf-8")
if text.strip() != MARKER_CONTENT.strip():
raise RuntimeError(f"marker mismatch: {text!r}")

print(f"\n{'=' * 60}")
print("PASS: CubeSandbox backend is reachable.")


def run_agent_demo(
*,
backend: BaseSandbox,
sandbox: SandboxHandle,
label: str,
model: str,
question: str,
) -> None:
print(f"Backend: {label}")
print(f"Model: {model}")
print(f"CubeAPI: {os.environ['E2B_API_URL']}")
print(f"Question: {question}\n")
print(f"[sandbox] sandbox_id={backend.id}")

agent = create_deep_agent(
model=make_chat_model(model),
backend=backend,
system_prompt=SYSTEM_PROMPT,
)

print("[agent] running LangGraph agent (Deep Agents harness) ...\n")
t1 = time.monotonic()
result = agent.invoke({"messages": [{"role": "user", "content": question}]})
elapsed_ms = (time.monotonic() - t1) * 1000

messages = result.get("messages", [])
answer = messages[-1].content if messages else result
Comment thread
FAUST-BENCHOU marked this conversation as resolved.
print(f"{'=' * 60}")
print(answer)
print(f"\n[done] {elapsed_ms:.0f} ms")
Loading
Loading