diff --git a/CUSTOMIZATION.md b/CUSTOMIZATION.md index 2573feab..4aba65e6 100644 --- a/CUSTOMIZATION.md +++ b/CUSTOMIZATION.md @@ -84,7 +84,7 @@ The factory must return an object implementing `SandboxBackendProtocol` from `de If none of the built-in providers fit, you can build your own. The agent accepts any backend that implements `SandboxBackendProtocol` from `deepagents`. The protocol requires: -- **File operations**: `ls_info()`, `read()`, `write()`, `edit()`, `glob_info()`, `grep_raw()` +- **File operations**: `ls()`, `read()`, `write()`, `edit()`, `glob()`, `grep()` - **Shell execution**: `execute(command, timeout=None) -> ExecuteResponse` - **Identity**: `id` property returning a unique sandbox identifier @@ -111,7 +111,7 @@ class MySandbox(BaseSandbox): ) ``` -See `agent/integrations/langsmith.py` (`LangSmithBackend` class) for a full reference implementation. +See `deepagents.backends.LangSmithSandbox` and `agent/integrations/langsmith.py` for a full reference implementation. --- diff --git a/agent/integrations/__init__.py b/agent/integrations/__init__.py index 6b5fa514..c46d1b7a 100644 --- a/agent/integrations/__init__.py +++ b/agent/integrations/__init__.py @@ -1,5 +1,7 @@ """Sandbox provider integrations.""" -from agent.integrations.langsmith import LangSmithBackend, LangSmithProvider +from deepagents.backends import LangSmithSandbox -__all__ = ["LangSmithBackend", "LangSmithProvider"] +from agent.integrations.langsmith import LangSmithProvider + +__all__ = ["LangSmithProvider", "LangSmithSandbox"] diff --git a/agent/integrations/langsmith.py b/agent/integrations/langsmith.py index 0f93fb29..602c827a 100644 --- a/agent/integrations/langsmith.py +++ b/agent/integrations/langsmith.py @@ -1,7 +1,4 @@ -"""LangSmith sandbox backend implementation. - -Copied from deepagents-cli to avoid requiring deepagents-cli as a dependency. -""" +"""LangSmith sandbox backend integration.""" from __future__ import annotations @@ -11,15 +8,9 @@ import time from abc import ABC, abstractmethod from typing import Any -from deepagents.backends.protocol import ( - ExecuteResponse, - FileDownloadResponse, - FileUploadResponse, - SandboxBackendProtocol, - WriteResult, -) -from deepagents.backends.sandbox import BaseSandbox -from langsmith.sandbox import Sandbox, SandboxClient, SandboxTemplate +from deepagents.backends import LangSmithSandbox +from deepagents.backends.protocol import SandboxBackendProtocol +from langsmith.sandbox import SandboxClient, SandboxTemplate def _get_langsmith_api_key() -> str | None: @@ -32,12 +23,7 @@ def _get_langsmith_api_key() -> str | None: def _get_sandbox_template_config() -> tuple[str | None, str | None]: - """Get sandbox template configuration from environment. - - Returns: - Tuple of (template_name, template_image) from environment variables. - Values are None if not set in environment. - """ + """Get sandbox template configuration from environment.""" template_name = os.environ.get("DEFAULT_SANDBOX_TEMPLATE_NAME") template_image = os.environ.get("DEFAULT_SANDBOX_TEMPLATE_IMAGE") return template_name, template_image @@ -48,10 +34,6 @@ def create_langsmith_sandbox( ) -> SandboxBackendProtocol: """Create or connect to a LangSmith sandbox without automatic cleanup. - This function directly uses the LangSmithProvider to create/connect to sandboxes - without the context manager cleanup, allowing sandboxes to persist across - multiple agent invocations. - Args: sandbox_id: Optional existing sandbox ID to connect to. If None, creates a new sandbox. @@ -99,7 +81,6 @@ def _update_thread_sandbox_metadata(sandbox_id: str) -> None: else: loop.create_task(_update()) except Exception: - # Best-effort: ignore failures (no config context, client unavailable, etc.) pass @@ -127,87 +108,12 @@ class SandboxProvider(ABC): raise NotImplementedError -# Default template configuration DEFAULT_TEMPLATE_NAME = "open-swe" DEFAULT_TEMPLATE_IMAGE = "python:3" -class LangSmithBackend(BaseSandbox): - """LangSmith backend implementation conforming to SandboxBackendProtocol. - - This implementation inherits all file operation methods from BaseSandbox - and only implements the execute() method using LangSmith's API. - """ - - def __init__(self, sandbox: Sandbox) -> None: - self._sandbox = sandbox - self._default_timeout: int = 30 * 5 # 5 minute default - - @property - def id(self) -> str: - """Unique identifier for the sandbox backend.""" - return self._sandbox.name - - def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse: - """Execute a command in the sandbox and return ExecuteResponse. - - Args: - command: Full shell command string to execute. - timeout: Maximum time in seconds to wait for the command to complete. - If None, uses the default timeout of 5 minutes. - - Returns: - ExecuteResponse with combined output, exit code, and truncation flag. - """ - effective_timeout = timeout if timeout is not None else self._default_timeout - result = self._sandbox.run(command, timeout=effective_timeout) - - # Combine stdout and stderr (matching other backends' approach) - output = result.stdout or "" - if result.stderr: - output += "\n" + result.stderr if output else result.stderr - - return ExecuteResponse( - output=output, - exit_code=result.exit_code, - truncated=False, - ) - - def write(self, file_path: str, content: str) -> WriteResult: - """Write content using the LangSmith SDK to avoid ARG_MAX. - - BaseSandbox.write() sends the full content in a shell command, which - can exceed ARG_MAX for large content. This override uses the SDK's - native write(), which sends content in the HTTP body. - """ - try: - self._sandbox.write(file_path, content.encode("utf-8")) - return WriteResult(path=file_path, files_update=None) - except Exception as e: - return WriteResult(error=f"Failed to write file '{file_path}': {e}") - - def download_files(self, paths: list[str]) -> list[FileDownloadResponse]: - """Download multiple files from the LangSmith sandbox.""" - responses: list[FileDownloadResponse] = [] - for path in paths: - content = self._sandbox.read(path) - responses.append(FileDownloadResponse(path=path, content=content, error=None)) - return responses - - def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]: - """Upload multiple files to the LangSmith sandbox.""" - responses: list[FileUploadResponse] = [] - for path, content in files: - self._sandbox.write(path, content) - responses.append(FileUploadResponse(path=path, error=None)) - return responses - - class LangSmithProvider(SandboxProvider): - """LangSmith sandbox provider implementation. - - Manages LangSmith sandbox lifecycle using the LangSmith SDK. - """ + """LangSmith sandbox provider implementation.""" def __init__(self, api_key: str | None = None) -> None: from langsmith import sandbox @@ -237,7 +143,7 @@ class LangSmithProvider(SandboxProvider): except Exception as e: msg = f"Failed to connect to existing sandbox '{sandbox_id}': {e}" raise RuntimeError(msg) from e - return LangSmithBackend(sandbox) + return LangSmithSandbox(sandbox) resolved_template_name, resolved_image_name = self._resolve_template( template, template_image @@ -253,7 +159,6 @@ class LangSmithProvider(SandboxProvider): msg = f"Failed to create sandbox from template '{resolved_template_name}': {e}" raise RuntimeError(msg) from e - # Verify sandbox is ready by polling for _ in range(timeout // 2): try: result = sandbox.run("echo ready", timeout=5) @@ -268,7 +173,7 @@ class LangSmithProvider(SandboxProvider): msg = f"LangSmith sandbox failed to start within {timeout} seconds" raise RuntimeError(msg) - return LangSmithBackend(sandbox) + return LangSmithSandbox(sandbox) def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: """Delete a LangSmith sandbox.""" @@ -285,7 +190,6 @@ class LangSmithProvider(SandboxProvider): return DEFAULT_TEMPLATE_NAME, resolved_image if isinstance(template, str): return template, resolved_image - # SandboxTemplate object if template_image is None and template.image: resolved_image = template.image return template.name, resolved_image diff --git a/agent/server.py b/agent/server.py index a9f81bb0..52a0a787 100644 --- a/agent/server.py +++ b/agent/server.py @@ -90,7 +90,7 @@ async def _clone_or_pull_repo_in_sandbox( # noqa: PLR0915 """Clone a GitHub repo into the sandbox, or pull if it already exists. Args: - sandbox_backend: The sandbox backend to execute commands in (LangSmithBackend) + sandbox_backend: The sandbox backend to execute commands in owner: GitHub repo owner repo: GitHub repo name github_token: GitHub access token (from agent auth or env var) diff --git a/pyproject.toml b/pyproject.toml index b562b862..648bc8bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.11" license = { text = "MIT" } dependencies = [ - "deepagents>=0.4.12", + "deepagents>=0.5.0a4", "fastapi>=0.104.0", "uvicorn>=0.24.0", "httpx>=0.25.0", diff --git a/uv.lock b/uv.lock index e2f3968b..2403c735 100644 --- a/uv.lock +++ b/uv.lock @@ -171,7 +171,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.78.0" +version = "0.88.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -183,9 +183,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/51/32849a48f9b1cfe80a508fd269b20bd8f0b1357c70ba092890fde5a6a10b/anthropic-0.78.0.tar.gz", hash = "sha256:55fd978ab9b049c61857463f4c4e9e092b24f892519c6d8078cee1713d8af06e", size = 509136, upload-time = "2026-02-05T17:52:04.986Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/68/565f13059c0a6a6fd5f96f306f2a0fb478a0e1174ec18a4df16b5fac9379/anthropic-0.88.0.tar.gz", hash = "sha256:f4c7f6863d08c869913516f08d658fe53caaf8bcc4fbea3218df343d2a876c58", size = 596654, upload-time = "2026-04-01T19:59:05.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/03/2f50931a942e5e13f80e24d83406714672c57964be593fc046d81369335b/anthropic-0.78.0-py3-none-any.whl", hash = "sha256:2a9887d2e99d1b0f9fe08857a1e9fe5d2d4030455dbf9ac65aab052e2efaeac4", size = 405485, upload-time = "2026-02-05T17:52:03.674Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ac/68f646998160c9f2e6f9353a31dd87292ef02b915b455aaf70a52a059a75/anthropic-0.88.0-py3-none-any.whl", hash = "sha256:71898b32332bc75d9739bc10095288d40a29605da6d00da2fe832b1aa036552f", size = 478338, upload-time = "2026-04-01T19:59:03.832Z" }, ] [[package]] @@ -629,18 +629,19 @@ wheels = [ [[package]] name = "deepagents" -version = "0.4.12" +version = "0.5.0a4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain" }, { name = "langchain-anthropic" }, { name = "langchain-core" }, { name = "langchain-google-genai" }, + { name = "langsmith" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/ef/0b2ccd5e4f40c1554145de17a7f3ee41994de1dda3ea36abe28600f1a3cf/deepagents-0.4.12.tar.gz", hash = "sha256:fc24a691e5cba00920ac4fa1d94f8147d6081fe513ed22bdba7da469288681c3", size = 91870, upload-time = "2026-03-20T14:54:29.904Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/52/8fc1fc65fcfa34e54d79ba40cbf0205f68f81f97d716f12c51ef91e91f1f/deepagents-0.5.0a4.tar.gz", hash = "sha256:4f2993fffd7ac3092ac534e72362dca511d4a965fa4669da45502ec2c96cf580", size = 108345, upload-time = "2026-04-03T14:41:18.734Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/77/63a5cb4e3a8871c4a52d600661a232c26b35f52931ee551c3adc38eeacf6/deepagents-0.4.12-py3-none-any.whl", hash = "sha256:76a272bac25607c5ef8c5adc876e391da945f1107b504686964dfdb6afdc1ebb", size = 104455, upload-time = "2026-03-20T14:54:28.786Z" }, + { url = "https://files.pythonhosted.org/packages/ca/79/d997eefa2a2a46e02f64d4efe8321fff61bb8204fda99379c6451b8f1987/deepagents-0.5.0a4-py3-none-any.whl", hash = "sha256:c8d996209d0bb738b7ae02f90c4d4c8905276bcae6778aa26d383d16c5c46b5a", size = 121698, upload-time = "2026-04-03T14:41:17.745Z" }, ] [[package]] @@ -1271,16 +1272,16 @@ wheels = [ [[package]] name = "langchain-anthropic" -version = "1.3.4" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anthropic" }, { name = "langchain-core" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/4e/7c1ffac126f5e62b0b9066f331f91ae69361e73476fd3ca1b19f8d8a3cc3/langchain_anthropic-1.3.4.tar.gz", hash = "sha256:000ed4c2d6fb8842b4ffeed22a74a3e84f9e9bcb63638e4abbb4a1d8ffa07211", size = 671858, upload-time = "2026-02-24T13:54:01.738Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/c7/259d4d805c6ac90c8695714fc15498a4557bb515eb24f692fd611966e383/langchain_anthropic-1.4.0.tar.gz", hash = "sha256:bbf64e99f9149a34ba67813e9582b2160a0968de9e9f54f7ba8d1658f253c2e5", size = 674360, upload-time = "2026-03-17T18:42:20.751Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/cf/b7c7b7270efbb3db2edbf14b09ba9110a41628f3a85a11cae9527a35641c/langchain_anthropic-1.3.4-py3-none-any.whl", hash = "sha256:cd112dcc8049aef09f58b3c4338b2c9db5ee98105e08664954a4e40d8bf120b9", size = 47454, upload-time = "2026-02-24T13:54:00.53Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c0/77f99373276d4f06c38a887ef6023f101cfc7ba3b2bf9af37064cdbadde5/langchain_anthropic-1.4.0-py3-none-any.whl", hash = "sha256:c84f55722336935f7574d5771598e674f3959fdca0b51de14c9788dbf52761be", size = 48463, upload-time = "2026-03-17T18:42:19.742Z" }, ] [[package]] @@ -1317,7 +1318,7 @@ wheels = [ [[package]] name = "langchain-google-genai" -version = "4.2.0" +version = "4.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filetype" }, @@ -1325,9 +1326,9 @@ dependencies = [ { name = "langchain-core" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/eae2305e207574dc633983a8a82a745e0ede1bce1f3a9daff24d2341fadc/langchain_google_genai-4.2.0.tar.gz", hash = "sha256:9a8d9bfc35354983ed29079cefff53c3e7c9c2a44b6ba75cc8f13a0cf8b55c33", size = 277361, upload-time = "2026-01-13T20:41:17.63Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/63/e7d148f903cebfef50109da71378f411166f068d66f79b9e16a62dbacf41/langchain_google_genai-4.2.1.tar.gz", hash = "sha256:7f44487a0337535897e3bba9a1d6605d722629e034f757ffa8755af0aa85daa8", size = 278288, upload-time = "2026-02-19T19:29:19.416Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/51/39942c0083139652494bb354dddf0ed397703a4882302f7b48aeca531c96/langchain_google_genai-4.2.0-py3-none-any.whl", hash = "sha256:856041aaafceff65a4ef0d5acf5731f2db95229ff041132af011aec51e8279d9", size = 66452, upload-time = "2026-01-13T20:41:16.296Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7e/46c5973bd8b10a5c4c8a77136cf536e658796380a17c740246074901b038/langchain_google_genai-4.2.1-py3-none-any.whl", hash = "sha256:a7735289cf94ca3a684d830e09196aac8f6e75e647e3a0a1c3c9dc534ceb985e", size = 66500, upload-time = "2026-02-19T19:29:18.002Z" }, ] [[package]] @@ -1812,7 +1813,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "cryptography", specifier = ">=41.0.0" }, - { name = "deepagents", specifier = ">=0.4.12" }, + { name = "deepagents", specifier = ">=0.5.0a4" }, { name = "exa-py", specifier = ">=2.10.1" }, { name = "fastapi", specifier = ">=0.104.0" }, { name = "httpx", specifier = ">=0.25.0" },