mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 11:33:14 +00:00
refactor: Remove all imports from deepagents_cli & pull in custom tools (#894)
This commit is contained in:
parent
841aa536f5
commit
78542b226f
6 changed files with 160 additions and 102 deletions
14
apps/agent/agent/prompt.py
Normal file
14
apps/agent/agent/prompt.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
SYSTEM_PROMPT = """### Current Working Directory
|
||||
|
||||
You are operating in a **remote Linux sandbox** at `{working_dir}`.
|
||||
|
||||
All code execution and file operations happen in this sandbox environment.
|
||||
|
||||
**Important:**
|
||||
- Use `{working_dir}` as your working directory for all operations
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def construct_system_prompt(working_dir: str) -> str:
|
||||
return SYSTEM_PROMPT.format(working_dir=working_dir)
|
||||
|
|
@ -6,19 +6,16 @@
|
|||
import logging
|
||||
import os
|
||||
import warnings
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from langchain.agents.middleware import AgentState, after_agent, after_model, before_model
|
||||
from langchain.agents.middleware.types import AgentMiddleware
|
||||
from langchain.tools import BaseTool
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langgraph.config import get_config, get_store
|
||||
from langgraph.graph.state import RunnableConfig
|
||||
from langgraph.pregel import Pregel
|
||||
from langgraph.runtime import Runtime
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
warnings.filterwarnings("ignore", module="langchain_core._api.deprecation")
|
||||
|
||||
|
|
@ -30,12 +27,11 @@ warnings.filterwarnings("ignore", message=".*Pydantic V1.*", category=UserWarnin
|
|||
# Now safe to import agent (which imports LangChain modules)
|
||||
from deepagents import create_deep_agent
|
||||
from deepagents.backends.sandbox import SandboxBackendProtocol
|
||||
from deepagents_cli.agent import get_system_prompt
|
||||
from deepagents_cli.config import config, settings
|
||||
from deepagents_cli.tools import fetch_url, http_request, web_search
|
||||
|
||||
# Local import for encryption
|
||||
from .encryption import decrypt_token
|
||||
from .prompt import construct_system_prompt
|
||||
from .tools import fetch_url, http_request
|
||||
|
||||
|
||||
def _get_langsmith_api_key() -> str | None:
|
||||
|
|
@ -88,69 +84,6 @@ def _create_langsmith_sandbox(
|
|||
)
|
||||
|
||||
|
||||
def create_server_agent(
|
||||
model: str | BaseChatModel | None,
|
||||
assistant_id: str,
|
||||
*,
|
||||
tools: list[BaseTool] | None = None,
|
||||
sandbox: SandboxBackendProtocol | None = None,
|
||||
sandbox_type: str | None = None,
|
||||
system_prompt: str | None = None,
|
||||
auto_approve: bool = True, # noqa: ARG001 - Always True for Open SWE
|
||||
working_dir: str | None = None,
|
||||
middleware: Sequence[AgentMiddleware] = (),
|
||||
) -> Pregel:
|
||||
"""Create a server-mode agent for Open SWE.
|
||||
|
||||
This creates an agent configured for server/cloud deployment with sandbox
|
||||
support and custom middleware. Always runs with auto_approve=True.
|
||||
|
||||
Args:
|
||||
model: LLM model to use. Can be None for introspection-only mode.
|
||||
assistant_id: Agent identifier for memory/state storage
|
||||
tools: Additional tools to provide to agent
|
||||
sandbox: Optional sandbox backend for remote execution (e.g., LangSmithBackend).
|
||||
sandbox_type: Type of sandbox provider ("langsmith").
|
||||
Used for system prompt generation.
|
||||
system_prompt: Override the default system prompt. If None, generates one
|
||||
based on sandbox_type and assistant_id.
|
||||
working_dir: Override the default working directory (e.g., cloned repo path).
|
||||
Used in system prompt to tell the agent where to operate.
|
||||
middleware: Sequence of middleware to apply to the agent.
|
||||
|
||||
Returns:
|
||||
Configured LangGraph Pregel instance ready for execution
|
||||
"""
|
||||
agent_tools = tools or []
|
||||
|
||||
# Get or use custom system prompt
|
||||
if system_prompt is None:
|
||||
if sandbox_type is not None:
|
||||
system_prompt = get_system_prompt(
|
||||
assistant_id=assistant_id,
|
||||
sandbox_type=sandbox_type,
|
||||
working_dir=working_dir,
|
||||
)
|
||||
else:
|
||||
# Only happens when thread_id is None / not actually running
|
||||
system_prompt = ""
|
||||
|
||||
return create_deep_agent(
|
||||
model=model,
|
||||
system_prompt=system_prompt,
|
||||
tools=agent_tools,
|
||||
backend=sandbox,
|
||||
middleware=middleware,
|
||||
interrupt_on={}, # Always auto-approve for Open SWE
|
||||
).with_config(config)
|
||||
|
||||
|
||||
tools = [http_request, fetch_url]
|
||||
if settings.has_tavily:
|
||||
tools.append(web_search)
|
||||
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client()
|
||||
|
||||
SANDBOX_CREATING = "__creating__"
|
||||
|
|
@ -867,11 +800,16 @@ def graph_loaded_for_execution(config: RunnableConfig) -> bool:
|
|||
)
|
||||
|
||||
|
||||
DEFAULT_RECURSION_LIMIT = 1_000
|
||||
|
||||
|
||||
async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
||||
"""Get or create an agent with a sandbox for the given thread."""
|
||||
thread_id = config["configurable"].get("thread_id", None)
|
||||
logger.info("get_agent called for thread %s", thread_id)
|
||||
|
||||
config["recursion_limit"] = DEFAULT_RECURSION_LIMIT
|
||||
|
||||
repo_config = config["configurable"].get("repo", {})
|
||||
repo_owner = repo_config.get("owner")
|
||||
repo_name = repo_config.get("name")
|
||||
|
|
@ -883,14 +821,10 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
|||
|
||||
if thread_id is None or not graph_loaded_for_execution(config):
|
||||
logger.info("No thread_id or not for execution, returning agent without sandbox")
|
||||
return create_server_agent(
|
||||
model=None,
|
||||
assistant_id="agent",
|
||||
tools=tools,
|
||||
sandbox=None,
|
||||
sandbox_type=None,
|
||||
auto_approve=True,
|
||||
)
|
||||
return create_deep_agent(
|
||||
system_prompt="",
|
||||
tools=[],
|
||||
).with_config(config)
|
||||
|
||||
sandbox_id = await _get_sandbox_id_from_metadata(thread_id)
|
||||
|
||||
|
|
@ -938,14 +872,10 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
|||
logger.info("Connecting to existing sandbox %s", sandbox_id)
|
||||
try:
|
||||
# Connect to existing sandbox without context manager cleanup
|
||||
sandbox_backend = await asyncio.to_thread(
|
||||
_create_langsmith_sandbox, sandbox_id
|
||||
)
|
||||
sandbox_backend = await asyncio.to_thread(_create_langsmith_sandbox, sandbox_id)
|
||||
logger.info("Connected to existing sandbox %s", sandbox_id)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to connect to existing sandbox %s, creating new one", sandbox_id
|
||||
)
|
||||
logger.warning("Failed to connect to existing sandbox %s, creating new one", sandbox_id)
|
||||
# Reset sandbox_id and create a new sandbox
|
||||
await client.threads.update(
|
||||
thread_id=thread_id,
|
||||
|
|
@ -962,9 +892,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
|||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to create replacement sandbox")
|
||||
await client.threads.update(
|
||||
thread_id=thread_id, metadata={"sandbox_id": None}
|
||||
)
|
||||
await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None})
|
||||
raise
|
||||
|
||||
thread = await client.threads.get(thread_id=thread_id)
|
||||
|
|
@ -983,17 +911,14 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
|||
_SANDBOX_BACKENDS[thread_id] = sandbox_backend
|
||||
|
||||
logger.info("Returning agent with sandbox for thread %s", thread_id)
|
||||
return create_server_agent(
|
||||
model=None,
|
||||
assistant_id="agent",
|
||||
tools=tools,
|
||||
sandbox=sandbox_backend,
|
||||
sandbox_type="langsmith",
|
||||
auto_approve=True,
|
||||
working_dir=repo_dir,
|
||||
return create_deep_agent(
|
||||
model=None, # TODO: Actually pass a model here
|
||||
system_prompt=construct_system_prompt(repo_dir),
|
||||
tools=[http_request, fetch_url],
|
||||
backend=sandbox_backend,
|
||||
middleware=[
|
||||
check_message_queue_before_model,
|
||||
post_to_linear_after_model,
|
||||
open_pr_if_needed,
|
||||
],
|
||||
)
|
||||
).with_config(config)
|
||||
|
|
|
|||
4
apps/agent/agent/tools/__init__.py
Normal file
4
apps/agent/agent/tools/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .fetch_url import fetch_url
|
||||
from .http_request import http_request
|
||||
|
||||
__all__ = ["fetch_url", "http_request"]
|
||||
50
apps/agent/agent/tools/fetch_url.py
Normal file
50
apps/agent/agent/tools/fetch_url.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from typing import Any
|
||||
|
||||
import requests
|
||||
from markdownify import markdownify
|
||||
|
||||
|
||||
def fetch_url(url: str, timeout: int = 30) -> dict[str, Any]:
|
||||
"""Fetch content from a URL and convert HTML to markdown format.
|
||||
|
||||
This tool fetches web page content and converts it to clean markdown text,
|
||||
making it easy to read and process HTML content. After receiving the markdown,
|
||||
you MUST synthesize the information into a natural, helpful response for the user.
|
||||
|
||||
Args:
|
||||
url: The URL to fetch (must be a valid HTTP/HTTPS URL)
|
||||
timeout: Request timeout in seconds (default: 30)
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- success: Whether the request succeeded
|
||||
- url: The final URL after redirects
|
||||
- markdown_content: The page content converted to markdown
|
||||
- status_code: HTTP status code
|
||||
- content_length: Length of the markdown content in characters
|
||||
|
||||
IMPORTANT: After using this tool:
|
||||
1. Read through the markdown content
|
||||
2. Extract relevant information that answers the user's question
|
||||
3. Synthesize this into a clear, natural language response
|
||||
4. NEVER show the raw markdown to the user unless specifically requested
|
||||
"""
|
||||
try:
|
||||
response = requests.get(
|
||||
url,
|
||||
timeout=timeout,
|
||||
headers={"User-Agent": "Mozilla/5.0 (compatible; DeepAgents/1.0)"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# Convert HTML content to markdown
|
||||
markdown_content = markdownify(response.text)
|
||||
|
||||
return {
|
||||
"url": str(response.url),
|
||||
"markdown_content": markdown_content,
|
||||
"status_code": response.status_code,
|
||||
"content_length": len(markdown_content),
|
||||
}
|
||||
except requests.exceptions.RequestException as e:
|
||||
return {"error": f"Fetch URL error: {e!s}", "url": url}
|
||||
70
apps/agent/agent/tools/http_request.py
Normal file
70
apps/agent/agent/tools/http_request.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def http_request(
|
||||
url: str,
|
||||
method: str = "GET",
|
||||
headers: dict[str, str] | None = None,
|
||||
data: str | dict | None = None,
|
||||
params: dict[str, str] | None = None,
|
||||
timeout: int = 30,
|
||||
) -> dict[str, Any]:
|
||||
"""Make HTTP requests to APIs and web services.
|
||||
|
||||
Args:
|
||||
url: Target URL
|
||||
method: HTTP method (GET, POST, PUT, DELETE, etc.)
|
||||
headers: HTTP headers to include
|
||||
data: Request body data (string or dict)
|
||||
params: URL query parameters
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
Dictionary with response data including status, headers, and content
|
||||
"""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {}
|
||||
|
||||
if headers:
|
||||
kwargs["headers"] = headers
|
||||
if params:
|
||||
kwargs["params"] = params
|
||||
if data:
|
||||
if isinstance(data, dict):
|
||||
kwargs["json"] = data
|
||||
else:
|
||||
kwargs["data"] = data
|
||||
|
||||
response = requests.request(method.upper(), url, timeout=timeout, **kwargs)
|
||||
|
||||
try:
|
||||
content = response.json()
|
||||
except (ValueError, requests.exceptions.JSONDecodeError):
|
||||
content = response.text
|
||||
|
||||
return {
|
||||
"success": response.status_code < 400,
|
||||
"status_code": response.status_code,
|
||||
"headers": dict(response.headers),
|
||||
"content": content,
|
||||
"url": response.url,
|
||||
}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return {
|
||||
"success": False,
|
||||
"status_code": 0,
|
||||
"headers": {},
|
||||
"content": f"Request timed out after {timeout} seconds",
|
||||
"url": url,
|
||||
}
|
||||
except requests.exceptions.RequestException as e:
|
||||
return {
|
||||
"success": False,
|
||||
"status_code": 0,
|
||||
"headers": {},
|
||||
"content": f"Request error: {e!s}",
|
||||
"url": url,
|
||||
}
|
||||
|
|
@ -8,26 +8,21 @@ license = { text = "MIT" }
|
|||
dependencies = [
|
||||
# Core deepagents CLI - points to branch with LangSmith sandbox and working_dir support
|
||||
"deepagents-cli @ git+https://github.com/langchain-ai/deepagents.git@yogesh/working_dir_and_template_config#subdirectory=libs/cli",
|
||||
|
||||
# FastAPI for webhook handling
|
||||
"fastapi>=0.104.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
# HTTP client
|
||||
"httpx>=0.25.0",
|
||||
|
||||
# JWT for service authentication
|
||||
"PyJWT>=2.8.0",
|
||||
|
||||
# Encryption
|
||||
"cryptography>=41.0.0",
|
||||
|
||||
# LangGraph SDK for thread management
|
||||
"langgraph-sdk>=0.1.0",
|
||||
|
||||
# LangChain dependencies (will be pulled in by deepagents-cli but listing for clarity)
|
||||
"langchain>=0.2.0",
|
||||
"langgraph>=0.2.0",
|
||||
"markdownify>=1.2.2",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue