From 78542b226faa369c6c75b8fe973c9af39ef4d2af Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Fri, 6 Feb 2026 13:23:09 -0800 Subject: [PATCH] refactor: Remove all imports from deepagents_cli & pull in custom tools (#894) --- apps/agent/agent/prompt.py | 14 +++ apps/agent/agent/server.py | 117 +++++-------------------- apps/agent/agent/tools/__init__.py | 4 + apps/agent/agent/tools/fetch_url.py | 50 +++++++++++ apps/agent/agent/tools/http_request.py | 70 +++++++++++++++ apps/agent/pyproject.toml | 7 +- 6 files changed, 160 insertions(+), 102 deletions(-) create mode 100644 apps/agent/agent/prompt.py create mode 100644 apps/agent/agent/tools/__init__.py create mode 100644 apps/agent/agent/tools/fetch_url.py create mode 100644 apps/agent/agent/tools/http_request.py diff --git a/apps/agent/agent/prompt.py b/apps/agent/agent/prompt.py new file mode 100644 index 00000000..adf03a5b --- /dev/null +++ b/apps/agent/agent/prompt.py @@ -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) diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index 4f3dbd63..430a9f5b 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -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) diff --git a/apps/agent/agent/tools/__init__.py b/apps/agent/agent/tools/__init__.py new file mode 100644 index 00000000..4e988eca --- /dev/null +++ b/apps/agent/agent/tools/__init__.py @@ -0,0 +1,4 @@ +from .fetch_url import fetch_url +from .http_request import http_request + +__all__ = ["fetch_url", "http_request"] diff --git a/apps/agent/agent/tools/fetch_url.py b/apps/agent/agent/tools/fetch_url.py new file mode 100644 index 00000000..4ed35040 --- /dev/null +++ b/apps/agent/agent/tools/fetch_url.py @@ -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} diff --git a/apps/agent/agent/tools/http_request.py b/apps/agent/agent/tools/http_request.py new file mode 100644 index 00000000..1969351a --- /dev/null +++ b/apps/agent/agent/tools/http_request.py @@ -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, + } diff --git a/apps/agent/pyproject.toml b/apps/agent/pyproject.toml index 8d8da6a7..92eaa83c 100644 --- a/apps/agent/pyproject.toml +++ b/apps/agent/pyproject.toml @@ -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]