diff --git a/examples/langgraph-integration/.env.example b/examples/langgraph-integration/.env.example new file mode 100644 index 000000000..eb7f7a678 --- /dev/null +++ b/examples/langgraph-integration/.env.example @@ -0,0 +1,14 @@ +OPENAI_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://:3000" +E2B_API_KEY="" +CUBE_TEMPLATE_ID="" + +# mkcert root CA for E2B data-plane HTTPS (*.cube.app) +CUBE_SSL_CERT_FILE="/etc/pki/ca-trust/source/anchors/mkcert_.pem" diff --git a/examples/langgraph-integration/.gitignore b/examples/langgraph-integration/.gitignore new file mode 100644 index 000000000..67b5e38af --- /dev/null +++ b/examples/langgraph-integration/.gitignore @@ -0,0 +1,3 @@ +.venv/ +.env +__pycache__/ diff --git a/examples/langgraph-integration/README.md b/examples/langgraph-integration/README.md new file mode 100644 index 000000000..51a9e986e --- /dev/null +++ b/examples/langgraph-integration/README.md @@ -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) diff --git a/examples/langgraph-integration/README_zh.md b/examples/langgraph-integration/README_zh.md new file mode 100644 index 000000000..36ed1e557 --- /dev/null +++ b/examples/langgraph-integration/README_zh.md @@ -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) diff --git a/examples/langgraph-integration/cube_patches.py b/examples/langgraph-integration/cube_patches.py new file mode 100644 index 000000000..2ec889475 --- /dev/null +++ b/examples/langgraph-integration/cube_patches.py @@ -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 diff --git a/examples/langgraph-integration/demo_common.py b/examples/langgraph-integration/demo_common.py new file mode 100644 index 000000000..87743ee7c --- /dev/null +++ b/examples/langgraph-integration/demo_common.py @@ -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 + print(f"{'=' * 60}") + print(answer) + print(f"\n[done] {elapsed_ms:.0f} ms") diff --git a/examples/langgraph-integration/e2b_demo.py b/examples/langgraph-integration/e2b_demo.py new file mode 100644 index 000000000..e20f1dcbf --- /dev/null +++ b/examples/langgraph-integration/e2b_demo.py @@ -0,0 +1,74 @@ +# Copyright (c) 2026 Tencent Inc. +# SPDX-License-Identifier: Apache-2.0 + +""" +LangGraph + CubeSandbox demo (E2B-compatible SDK). + +Usage: + cp .env.example .env + pip install -r requirements.txt + python e2b_demo.py + python e2b_demo.py --sandbox-only +""" + +from __future__ import annotations + +import argparse +import os + +from cube_patches import apply_cube_envd_patches + +apply_cube_envd_patches() + +from e2b import Sandbox # noqa: E402 +from langchain_e2b import E2BSandbox # noqa: E402 + +from demo_common import CUBE_WORKDIR, run_agent_demo, run_sandbox_check # noqa: E402 +from env_utils import load_cube_env # noqa: E402 + + +def create_backend(*, template: str, timeout: int) -> tuple[Sandbox, E2BSandbox]: + sandbox = Sandbox.create(template=template, timeout=timeout) + backend = E2BSandbox(sandbox=sandbox, workdir=CUBE_WORKDIR, timeout=timeout) + return sandbox, backend + + +def main() -> None: + parser = argparse.ArgumentParser(description="LangGraph + CubeSandbox (E2B SDK)") + parser.add_argument("--model", default="deepseek-chat") + parser.add_argument( + "--question", + default="What OS is running? Show uname and the first 3 lines of /etc/os-release.", + ) + parser.add_argument("--template", default=None) + parser.add_argument("--timeout", type=int, default=300) + parser.add_argument("--sandbox-only", action="store_true") + args = parser.parse_args() + + load_cube_env(need_llm=not args.sandbox_only) + + template = args.template or os.environ.get("CUBE_TEMPLATE_ID") + if not template: + raise SystemExit("Missing template: set CUBE_TEMPLATE_ID or pass --template") + + sandbox = None + try: + sandbox, backend = create_backend(template=template, timeout=args.timeout) + if args.sandbox_only: + run_sandbox_check(backend=backend, sandbox=sandbox, label="E2B SDK -> CubeAPI") + else: + run_agent_demo( + backend=backend, + sandbox=sandbox, + label="E2B SDK -> CubeAPI", + model=args.model, + question=args.question, + ) + finally: + if sandbox is not None: + print("\n[cleanup] destroying sandbox ...") + sandbox.kill() + + +if __name__ == "__main__": + main() diff --git a/examples/langgraph-integration/env_utils.py b/examples/langgraph-integration/env_utils.py new file mode 100644 index 000000000..5aa47df36 --- /dev/null +++ b/examples/langgraph-integration/env_utils.py @@ -0,0 +1,35 @@ +# Copyright (c) 2026 Tencent Inc. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import os +from pathlib import Path + +from dotenv import load_dotenv + + +def load_local_dotenv() -> None: + """Load a nearby ``.env`` without overriding existing environment variables.""" + for path in (Path(__file__).with_name(".env"), Path.cwd() / ".env"): + if path.is_file(): + load_dotenv(dotenv_path=path, override=False) + return + + +def load_cube_env(*, need_llm: bool = True) -> None: + """Validate CubeSandbox and optional LLM environment variables.""" + load_local_dotenv() + + required = ("E2B_API_KEY", "E2B_API_URL", "CUBE_TEMPLATE_ID") + if need_llm: + required = ("OPENAI_API_KEY", "OPENAI_BASE_URL", *required) + + for key in required: + if not os.environ.get(key): + raise SystemExit(f"Missing env var: {key}") + + cube_ssl = os.environ.get("CUBE_SSL_CERT_FILE") + if cube_ssl and os.path.isfile(cube_ssl): + os.environ["SSL_CERT_FILE"] = cube_ssl + print(f"[ssl] SSL_CERT_FILE={cube_ssl} (for E2B data-plane HTTPS)") diff --git a/examples/langgraph-integration/llm_utils.py b/examples/langgraph-integration/llm_utils.py new file mode 100644 index 000000000..02742f10c --- /dev/null +++ b/examples/langgraph-integration/llm_utils.py @@ -0,0 +1,30 @@ +# Copyright (c) 2026 Tencent Inc. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import os +import ssl + +import certifi +import httpx +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_openai import ChatOpenAI + + +def make_chat_model(model_name: str) -> BaseChatModel: + """Build an OpenAI-compatible chat model (DeepSeek, TokenHub, etc.).""" + bare = model_name.split("/", 1)[-1] if "/" in model_name else model_name + bare = bare.split(":", 1)[-1] if ":" in bare else bare + + ssl_ctx = ssl.create_default_context(cafile=certifi.where()) + http_client = httpx.Client(verify=ssl_ctx, trust_env=False) + print("[ssl] LLM client: certifi CA bundle, trust_env=False") + + return ChatOpenAI( + model=bare, + base_url=os.environ.get("OPENAI_BASE_URL"), + api_key=os.environ.get("OPENAI_API_KEY"), + timeout=120, + http_client=http_client, + ) diff --git a/examples/langgraph-integration/requirements.txt b/examples/langgraph-integration/requirements.txt new file mode 100644 index 000000000..0609f391a --- /dev/null +++ b/examples/langgraph-integration/requirements.txt @@ -0,0 +1,5 @@ +deepagents>=0.6.0,<0.7.0 +python-dotenv +langchain-openai +e2b>=2.25.1,<3.0.0 +langchain-e2b>=0.0.5