From 16aafaf7adf9458e4f2d218bcfc31c86fc923915 Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Tue, 10 Feb 2026 13:35:50 -0800 Subject: [PATCH] refactor: move LangSmithProvider into open-swe, drop deepagents-cli dependency --- apps/agent/agent/integrations/__init__.py | 5 + apps/agent/agent/integrations/langsmith.py | 216 +++++++++++++++++++++ apps/agent/agent/protocol.py | 96 --------- apps/agent/agent/server.py | 4 +- apps/agent/agent/utils/__init__.py | 0 apps/agent/langgraph.json | 2 +- apps/agent/pyproject.toml | 2 +- apps/agent/uv.lock | 20 +- 8 files changed, 235 insertions(+), 110 deletions(-) create mode 100644 apps/agent/agent/integrations/__init__.py create mode 100644 apps/agent/agent/integrations/langsmith.py delete mode 100644 apps/agent/agent/protocol.py delete mode 100644 apps/agent/agent/utils/__init__.py diff --git a/apps/agent/agent/integrations/__init__.py b/apps/agent/agent/integrations/__init__.py new file mode 100644 index 00000000..6b5fa514 --- /dev/null +++ b/apps/agent/agent/integrations/__init__.py @@ -0,0 +1,5 @@ +"""Sandbox provider integrations.""" + +from agent.integrations.langsmith import LangSmithBackend, LangSmithProvider + +__all__ = ["LangSmithBackend", "LangSmithProvider"] diff --git a/apps/agent/agent/integrations/langsmith.py b/apps/agent/agent/integrations/langsmith.py new file mode 100644 index 00000000..030982e9 --- /dev/null +++ b/apps/agent/agent/integrations/langsmith.py @@ -0,0 +1,216 @@ +"""LangSmith sandbox backend implementation. + +Copied from deepagents-cli to avoid requiring deepagents-cli as a dependency. +""" + +from __future__ import annotations + +import contextlib +import os +import time +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +from deepagents.backends.protocol import ( + ExecuteResponse, + FileDownloadResponse, + FileUploadResponse, + SandboxBackendProtocol, +) +from deepagents.backends.sandbox import BaseSandbox + +if TYPE_CHECKING: + from langsmith.sandbox import Sandbox, SandboxClient, SandboxTemplate + + + +class SandboxProvider(ABC): + """Interface for creating and deleting sandbox backends.""" + + @abstractmethod + def get_or_create( + self, + *, + sandbox_id: str | None = None, + **kwargs: Any, + ) -> SandboxBackendProtocol: + """Get an existing sandbox, or create one if needed.""" + raise NotImplementedError + + @abstractmethod + def delete( + self, + *, + sandbox_id: str, + **kwargs: Any, + ) -> None: + """Delete a sandbox by id.""" + raise NotImplementedError + + +# Default template configuration +DEFAULT_TEMPLATE_NAME = "deepagents-cli" +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._timeout: int = 30 * 60 # 30 mins default + + @property + def id(self) -> str: + """Unique identifier for the sandbox backend.""" + return self._sandbox.name + + def execute(self, command: str) -> ExecuteResponse: + """Execute a command in the sandbox and return ExecuteResponse.""" + result = self._sandbox.run(command, timeout=self._timeout) + + 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 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. + """ + + def __init__(self, api_key: str | None = None) -> None: + from langsmith import sandbox + + self._api_key = api_key or os.environ.get("LANGSMITH_API_KEY") + if not self._api_key: + msg = "LANGSMITH_API_KEY environment variable not set" + raise ValueError(msg) + self._client: SandboxClient = sandbox.SandboxClient(api_key=self._api_key) + + def get_or_create( + self, + *, + sandbox_id: str | None = None, + timeout: int = 180, + template: str | None = None, + template_image: str | None = None, + **kwargs: Any, + ) -> SandboxBackendProtocol: + """Get existing or create new LangSmith sandbox.""" + if kwargs: + msg = f"Received unsupported arguments: {list(kwargs.keys())}" + raise TypeError(msg) + if sandbox_id: + try: + sandbox = self._client.get_sandbox(name=sandbox_id) + except Exception as e: + msg = f"Failed to connect to existing sandbox '{sandbox_id}': {e}" + raise RuntimeError(msg) from e + return LangSmithBackend(sandbox) + + resolved_template_name, resolved_image_name = self._resolve_template( + template, template_image + ) + + self._ensure_template(resolved_template_name, resolved_image_name) + + try: + sandbox = self._client.create_sandbox( + template_name=resolved_template_name, timeout=timeout + ) + except Exception as e: + msg = ( + f"Failed to create sandbox from template " + f"'{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) + if result.exit_code == 0: + break + except Exception: + pass + time.sleep(2) + else: + with contextlib.suppress(Exception): + self._client.delete_sandbox(sandbox.name) + msg = f"LangSmith sandbox failed to start within {timeout} seconds" + raise RuntimeError(msg) + + return LangSmithBackend(sandbox) + + def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: + """Delete a LangSmith sandbox.""" + self._client.delete_sandbox(sandbox_id) + + @staticmethod + def _resolve_template( + template: SandboxTemplate | str | None, + template_image: str | None = None, + ) -> tuple[str, str]: + """Resolve template name and image from kwargs.""" + resolved_image = template_image or DEFAULT_TEMPLATE_IMAGE + if template is None: + 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 + + def _ensure_template( + self, + template_name: str, + template_image: str, + ) -> None: + """Ensure template exists, creating it if needed.""" + from langsmith.sandbox import ResourceNotFoundError + + try: + self._client.get_template(template_name) + except ResourceNotFoundError as e: + if e.resource_type != "template": + msg = f"Unexpected resource not found: {e}" + raise RuntimeError(msg) from e + try: + self._client.create_template(name=template_name, image=template_image) + except Exception as create_err: + msg = f"Failed to create template '{template_name}': {create_err}" + raise RuntimeError(msg) from create_err + except Exception as e: + msg = f"Failed to check template '{template_name}': {e}" + raise RuntimeError(msg) from e diff --git a/apps/agent/agent/protocol.py b/apps/agent/agent/protocol.py deleted file mode 100644 index ed780acd..00000000 --- a/apps/agent/agent/protocol.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Protocol definitions for sandbox backends. - -This module defines the SandboxBackendProtocol that sandbox implementations -must follow. Copied from deepagents to reduce coupling. -""" - -from dataclasses import dataclass -from typing import Protocol, runtime_checkable - - -@dataclass -class ExecuteResponse: - """Result of code execution. - - Simplified schema optimized for LLM consumption. - """ - - output: str - """Combined stdout and stderr output of the executed command.""" - - exit_code: int | None = None - """The process exit code. 0 indicates success, non-zero indicates failure.""" - - truncated: bool = False - """Whether the output was truncated due to backend limitations.""" - - -@runtime_checkable -class SandboxBackendProtocol(Protocol): - """Protocol for sandbox backends that support shell command execution. - - Designed for backends running in isolated environments (containers, VMs, - remote hosts). - - This is a minimal protocol for type checking - actual implementations - come from deepagents. - """ - - @property - def id(self) -> str: - """Unique identifier for the sandbox backend instance.""" - ... - - def execute(self, command: str) -> ExecuteResponse: - """Execute a command in the sandbox. - - Args: - command: Full shell command string to execute. - - Returns: - ExecuteResponse with combined output, exit code, and truncation flag. - """ - ... - - async def aexecute(self, command: str) -> ExecuteResponse: - """Async version of execute.""" - ... - - def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> str: - """Read file content with line numbers.""" - ... - - async def aread(self, file_path: str, offset: int = 0, limit: int = 2000) -> str: - """Async version of read.""" - ... - - def write(self, file_path: str, content: str) -> "WriteResult": - """Write content to a new file.""" - ... - - def edit( - self, - file_path: str, - old_string: str, - new_string: str, - replace_all: bool = False, - ) -> "EditResult": - """Perform exact string replacements in an existing file.""" - ... - - -@dataclass -class WriteResult: - """Result from backend write operations.""" - - error: str | None = None - path: str | None = None - - -@dataclass -class EditResult: - """Result from backend edit operations.""" - - error: str | None = None - path: str | None = None - occurrences: int | None = None diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index 31dd5fe1..18fce83e 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -34,7 +34,7 @@ from .middleware import ( post_to_linear_after_model, ) from .prompt import construct_system_prompt -from .protocol import SandboxBackendProtocol +from deepagents.backends.protocol import SandboxBackendProtocol from .tools import commit_and_open_pr, fetch_url, http_request @@ -75,7 +75,7 @@ def _create_langsmith_sandbox( Returns: SandboxBackendProtocol instance """ - from deepagents_cli.integrations.langsmith import LangSmithProvider + from .integrations.langsmith import LangSmithProvider api_key = _get_langsmith_api_key() template_name, template_image = _get_sandbox_template_config() diff --git a/apps/agent/agent/utils/__init__.py b/apps/agent/agent/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/agent/langgraph.json b/apps/agent/langgraph.json index 67fa18f2..b4f46276 100644 --- a/apps/agent/langgraph.json +++ b/apps/agent/langgraph.json @@ -4,7 +4,7 @@ "graphs": { "agent": "agent.server:get_agent" }, - "dependencies": ["."], + "dependencies": [".", "../../../deepagents/libs/deepagents"], "http": { "app": "agent.webapp:app" }, diff --git a/apps/agent/pyproject.toml b/apps/agent/pyproject.toml index a894e23c..01d10332 100644 --- a/apps/agent/pyproject.toml +++ b/apps/agent/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" license = { text = "MIT" } dependencies = [ # Core deepagents library - "deepagents>=0.3.12", + "deepagents>=0.4.0", # FastAPI for webhook handling "fastapi>=0.104.0", "uvicorn>=0.24.0", diff --git a/apps/agent/uv.lock b/apps/agent/uv.lock index 7f3bd464..82e476f4 100644 --- a/apps/agent/uv.lock +++ b/apps/agent/uv.lock @@ -346,7 +346,7 @@ wheels = [ [[package]] name = "deepagents" -version = "0.3.12" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain" }, @@ -355,9 +355,9 @@ dependencies = [ { name = "langchain-google-genai" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/9d3512327d48e619567797dffb34c356b2e0c7b0aa505fd3aaef342903d3/deepagents-0.3.12.tar.gz", hash = "sha256:ab2d7e7c47040d364a20cc19cc775294c1e942456652d6c12e0f21011068633c", size = 77962, upload-time = "2026-02-06T21:20:43.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/ac/895c5efe77ee64f38af64146509b97220867fc149a9a376b2c74f266fd45/deepagents-0.4.0.tar.gz", hash = "sha256:ccfbb2394d2c50a3cf6f61457c5c1d3868354beee2177a57b8497c72c38f8e9b", size = 77614, upload-time = "2026-02-10T16:31:16.99Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/0a/2b8542a19bb22cf49827a38d04e045c6dba97d03dddc721d3ed2e7be0e5e/deepagents-0.3.12-py3-none-any.whl", hash = "sha256:42e707a1be48db3bc445fbe3243b6dc19333565cac4eab8cdc0e37d780c6cfe7", size = 88553, upload-time = "2026-02-06T21:20:42.575Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c8/cbedac42e011889f151047cf22dbecef9a38330c2e90e517c2bd1b1e636d/deepagents-0.4.0-py3-none-any.whl", hash = "sha256:475af99429c7b6abe4c53d476d6b48f01960dc1bcaec7fa21093189acdfc2864", size = 87831, upload-time = "2026-02-10T16:31:15.596Z" }, ] [[package]] @@ -782,16 +782,16 @@ wheels = [ [[package]] name = "langchain" -version = "1.2.9" +version = "1.2.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/d5/e7c8d18bf1ee2d37839dde161d523049fd0a5b172cf4c62f17090e1b4dcb/langchain-1.2.9.tar.gz", hash = "sha256:ae266c640b63c38f16b6d996a50aea575940b29b63cbc652c5d12f0111357f01", size = 569621, upload-time = "2026-02-06T12:39:41.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/22/a4d4ac98fc2e393537130bbfba0d71a8113e6f884d96f935923e247397fe/langchain-1.2.10.tar.gz", hash = "sha256:bdcd7218d9c79a413cf15e106e4eb94408ac0963df9333ccd095b9ed43bf3be7", size = 570071, upload-time = "2026-02-10T14:56:49.74Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/d9/ee07b79f8f1cfd87a6b147879149bdb03c04656e83e5a8c97f38d8915d07/langchain-1.2.9-py3-none-any.whl", hash = "sha256:c1af39d22b7f0415a6f8fa63b37f692335601d3333592c481b899166c55f3fcb", size = 111240, upload-time = "2026-02-06T12:39:39.833Z" }, + { url = "https://files.pythonhosted.org/packages/7c/06/c3394327f815fade875724c0f6cff529777c96a1e17fea066deb997f8cf5/langchain-1.2.10-py3-none-any.whl", hash = "sha256:e07a377204451fffaed88276b8193e894893b1003e25c5bca6539288ccca3698", size = 111738, upload-time = "2026-02-10T14:56:47.985Z" }, ] [[package]] @@ -810,7 +810,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.2.9" +version = "1.2.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -822,9 +822,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/85/f501592b5d76b27a198f1102bafe365151a0a6f69444122fad6d10e6f4bf/langchain_core-1.2.9.tar.gz", hash = "sha256:a3768febc762307241d153b0f8bc58fd4b70c0ff077fda3274606741fca3f5a7", size = 815900, upload-time = "2026-02-05T14:21:43.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/12/17/1943cedfc118e04b8128e4c3e1dbf0fa0ea58eefddbb6198cfd699d19f01/langchain_core-1.2.11.tar.gz", hash = "sha256:f164bb36602dd74a3a50c1334fca75309ad5ed95767acdfdbb9fa95ce28a1e01", size = 831211, upload-time = "2026-02-10T20:35:28.35Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/46/77846a98913e444d0d564070a9056bd999daada52bd099dc1e8812272810/langchain_core-1.2.9-py3-none-any.whl", hash = "sha256:7e5ecba5ed7a65852e8d5288e9ceeba05340fa9baf32baf672818b497bbaea8f", size = 496296, upload-time = "2026-02-05T14:21:42.816Z" }, + { url = "https://files.pythonhosted.org/packages/10/30/1f80e3fc674353cad975ed5294353d42512535d2094ef032c06454c2c873/langchain_core-1.2.11-py3-none-any.whl", hash = "sha256:ae11ceb8dda60d0b9d09e763116e592f1683327c17be5b715f350fd29aee65d3", size = 500062, upload-time = "2026-02-10T20:35:26.698Z" }, ] [[package]] @@ -1043,7 +1043,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "cryptography", specifier = ">=41.0.0" }, - { name = "deepagents", specifier = ">=0.3.12" }, + { name = "deepagents", specifier = ">=0.4.0" }, { name = "fastapi", specifier = ">=0.104.0" }, { name = "httpx", specifier = ">=0.25.0" }, { name = "langchain", specifier = ">=1.2.9" },