mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-06 07:42:11 +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 logging
|
||||||
import os
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Sequence
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from langchain.agents.middleware import AgentState, after_agent, after_model, before_model
|
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.config import get_config, get_store
|
||||||
from langgraph.graph.state import RunnableConfig
|
from langgraph.graph.state import RunnableConfig
|
||||||
from langgraph.pregel import Pregel
|
from langgraph.pregel import Pregel
|
||||||
from langgraph.runtime import Runtime
|
from langgraph.runtime import Runtime
|
||||||
|
from langgraph_sdk import get_client
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", module="langchain_core._api.deprecation")
|
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)
|
# Now safe to import agent (which imports LangChain modules)
|
||||||
from deepagents import create_deep_agent
|
from deepagents import create_deep_agent
|
||||||
from deepagents.backends.sandbox import SandboxBackendProtocol
|
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
|
# Local import for encryption
|
||||||
from .encryption import decrypt_token
|
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:
|
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()
|
client = get_client()
|
||||||
|
|
||||||
SANDBOX_CREATING = "__creating__"
|
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
|
async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
||||||
"""Get or create an agent with a sandbox for the given thread."""
|
"""Get or create an agent with a sandbox for the given thread."""
|
||||||
thread_id = config["configurable"].get("thread_id", None)
|
thread_id = config["configurable"].get("thread_id", None)
|
||||||
logger.info("get_agent called for thread %s", thread_id)
|
logger.info("get_agent called for thread %s", thread_id)
|
||||||
|
|
||||||
|
config["recursion_limit"] = DEFAULT_RECURSION_LIMIT
|
||||||
|
|
||||||
repo_config = config["configurable"].get("repo", {})
|
repo_config = config["configurable"].get("repo", {})
|
||||||
repo_owner = repo_config.get("owner")
|
repo_owner = repo_config.get("owner")
|
||||||
repo_name = repo_config.get("name")
|
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):
|
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")
|
logger.info("No thread_id or not for execution, returning agent without sandbox")
|
||||||
return create_server_agent(
|
return create_deep_agent(
|
||||||
model=None,
|
system_prompt="",
|
||||||
assistant_id="agent",
|
tools=[],
|
||||||
tools=tools,
|
).with_config(config)
|
||||||
sandbox=None,
|
|
||||||
sandbox_type=None,
|
|
||||||
auto_approve=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
sandbox_id = await _get_sandbox_id_from_metadata(thread_id)
|
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)
|
logger.info("Connecting to existing sandbox %s", sandbox_id)
|
||||||
try:
|
try:
|
||||||
# Connect to existing sandbox without context manager cleanup
|
# Connect to existing sandbox without context manager cleanup
|
||||||
sandbox_backend = await asyncio.to_thread(
|
sandbox_backend = await asyncio.to_thread(_create_langsmith_sandbox, sandbox_id)
|
||||||
_create_langsmith_sandbox, sandbox_id
|
|
||||||
)
|
|
||||||
logger.info("Connected to existing sandbox %s", sandbox_id)
|
logger.info("Connected to existing sandbox %s", sandbox_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(
|
logger.warning("Failed to connect to existing sandbox %s, creating new one", sandbox_id)
|
||||||
"Failed to connect to existing sandbox %s, creating new one", sandbox_id
|
|
||||||
)
|
|
||||||
# Reset sandbox_id and create a new sandbox
|
# Reset sandbox_id and create a new sandbox
|
||||||
await client.threads.update(
|
await client.threads.update(
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
|
|
@ -962,9 +892,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to create replacement sandbox")
|
logger.exception("Failed to create replacement sandbox")
|
||||||
await client.threads.update(
|
await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None})
|
||||||
thread_id=thread_id, metadata={"sandbox_id": None}
|
|
||||||
)
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
thread = await client.threads.get(thread_id=thread_id)
|
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
|
_SANDBOX_BACKENDS[thread_id] = sandbox_backend
|
||||||
|
|
||||||
logger.info("Returning agent with sandbox for thread %s", thread_id)
|
logger.info("Returning agent with sandbox for thread %s", thread_id)
|
||||||
return create_server_agent(
|
return create_deep_agent(
|
||||||
model=None,
|
model=None, # TODO: Actually pass a model here
|
||||||
assistant_id="agent",
|
system_prompt=construct_system_prompt(repo_dir),
|
||||||
tools=tools,
|
tools=[http_request, fetch_url],
|
||||||
sandbox=sandbox_backend,
|
backend=sandbox_backend,
|
||||||
sandbox_type="langsmith",
|
|
||||||
auto_approve=True,
|
|
||||||
working_dir=repo_dir,
|
|
||||||
middleware=[
|
middleware=[
|
||||||
check_message_queue_before_model,
|
check_message_queue_before_model,
|
||||||
post_to_linear_after_model,
|
post_to_linear_after_model,
|
||||||
open_pr_if_needed,
|
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 = [
|
dependencies = [
|
||||||
# Core deepagents CLI - points to branch with LangSmith sandbox and working_dir support
|
# 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",
|
"deepagents-cli @ git+https://github.com/langchain-ai/deepagents.git@yogesh/working_dir_and_template_config#subdirectory=libs/cli",
|
||||||
|
|
||||||
# FastAPI for webhook handling
|
# FastAPI for webhook handling
|
||||||
"fastapi>=0.104.0",
|
"fastapi>=0.104.0",
|
||||||
"uvicorn>=0.24.0",
|
"uvicorn>=0.24.0",
|
||||||
|
|
||||||
# HTTP client
|
# HTTP client
|
||||||
"httpx>=0.25.0",
|
"httpx>=0.25.0",
|
||||||
|
|
||||||
# JWT for service authentication
|
# JWT for service authentication
|
||||||
"PyJWT>=2.8.0",
|
"PyJWT>=2.8.0",
|
||||||
|
|
||||||
# Encryption
|
# Encryption
|
||||||
"cryptography>=41.0.0",
|
"cryptography>=41.0.0",
|
||||||
|
|
||||||
# LangGraph SDK for thread management
|
# LangGraph SDK for thread management
|
||||||
"langgraph-sdk>=0.1.0",
|
"langgraph-sdk>=0.1.0",
|
||||||
|
|
||||||
# LangChain dependencies (will be pulled in by deepagents-cli but listing for clarity)
|
# LangChain dependencies (will be pulled in by deepagents-cli but listing for clarity)
|
||||||
"langchain>=0.2.0",
|
"langchain>=0.2.0",
|
||||||
"langgraph>=0.2.0",
|
"langgraph>=0.2.0",
|
||||||
|
"markdownify>=1.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue