mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
refactor: move LangSmithProvider into open-swe, drop deepagents-cli dependency
This commit is contained in:
parent
8c9bc67335
commit
16aafaf7ad
8 changed files with 235 additions and 110 deletions
5
apps/agent/agent/integrations/__init__.py
Normal file
5
apps/agent/agent/integrations/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""Sandbox provider integrations."""
|
||||
|
||||
from agent.integrations.langsmith import LangSmithBackend, LangSmithProvider
|
||||
|
||||
__all__ = ["LangSmithBackend", "LangSmithProvider"]
|
||||
216
apps/agent/agent/integrations/langsmith.py
Normal file
216
apps/agent/agent/integrations/langsmith.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"graphs": {
|
||||
"agent": "agent.server:get_agent"
|
||||
},
|
||||
"dependencies": ["."],
|
||||
"dependencies": [".", "../../../deepagents/libs/deepagents"],
|
||||
"http": {
|
||||
"app": "agent.webapp:app"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
20
apps/agent/uv.lock
generated
20
apps/agent/uv.lock
generated
|
|
@ -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" },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue