diff --git a/INSTALLATION.md b/INSTALLATION.md index 3013949e..129f9d6f 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -343,6 +343,9 @@ SLACK_BOT_USER_ID="" SLACK_BOT_USERNAME="" SLACK_SIGNING_SECRET="" +# === Exa (optional — enables web search tool) === +EXA_API_KEY="" # From https://dashboard.exa.ai + # === Sandbox (optional) === DEFAULT_SANDBOX_TEMPLATE_NAME="" # Custom sandbox template name (default: deepagents-cli) DEFAULT_SANDBOX_TEMPLATE_IMAGE="" # Custom Docker image (default: python:3) diff --git a/agent/server.py b/agent/server.py index 10b4e714..dae949d8 100644 --- a/agent/server.py +++ b/agent/server.py @@ -54,6 +54,7 @@ from .tools import ( slack_thread_reply, submit_pr_review, update_pr_review, + web_search, ) from .utils.auth import resolve_github_token from .utils.model import make_model @@ -416,6 +417,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 tools=[ http_request, fetch_url, + web_search, commit_and_open_pr, linear_comment, linear_create_issue, diff --git a/agent/tools/__init__.py b/agent/tools/__init__.py index e639d11f..fc9bce1f 100644 --- a/agent/tools/__init__.py +++ b/agent/tools/__init__.py @@ -19,6 +19,7 @@ from .linear_get_issue_comments import linear_get_issue_comments from .linear_list_teams import linear_list_teams from .linear_update_issue import linear_update_issue from .slack_thread_reply import slack_thread_reply +from .web_search import web_search __all__ = [ "commit_and_open_pr", @@ -40,4 +41,5 @@ __all__ = [ "slack_thread_reply", "submit_pr_review", "update_pr_review", + "web_search", ] diff --git a/agent/tools/web_search.py b/agent/tools/web_search.py new file mode 100644 index 00000000..e5ceb02e --- /dev/null +++ b/agent/tools/web_search.py @@ -0,0 +1,63 @@ +import asyncio +import logging +import os +from typing import Any + +from exa_py import Exa + +logger = logging.getLogger(__name__) + + +def web_search( + query: str, + num_results: int = 5, + include_contents: bool = True, +) -> dict[str, Any]: + """Search the web using Exa to find relevant information. + + Use this tool when you need to find documentation, code examples, GitHub repos, + news, or research papers to help complete a task. + + Args: + query: The search query + num_results: Number of results to return (default: 5) + include_contents: Whether to include full page contents (default: True) + + Returns: + Dictionary containing: + - success: Whether the search succeeded + - results: Search results from Exa + - error: Error message if something failed + """ + api_key = os.environ.get("EXA_API_KEY") + if not api_key: + logger.warning("exa_api_key_missing") + return { + "success": False, + "error": "EXA_API_KEY is not configured. Please add it to your environment variables.", + } + + async def _search() -> dict[str, Any]: + client = Exa(api_key=api_key) + if include_contents: + result = await asyncio.to_thread( + client.search_and_contents, + query, + text=True, + num_results=num_results, + type="auto", + ) + else: + result = await asyncio.to_thread( + client.search, + query, + num_results=num_results, + type="auto", + ) + return {"success": True, "results": str(result), "error": None} + + try: + return asyncio.run(_search()) + except Exception as e: + logger.exception("web_search failed") + return {"success": False, "results": None, "error": f"{type(e).__name__}: {e}"} diff --git a/pyproject.toml b/pyproject.toml index 67e3b57c..fef36328 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "langchain-daytona>=0.0.3", "langchain-modal>=0.0.2", "langchain-runloop>=0.0.3", + "exa-py>=2.10.1", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index a07af24e..d603a650 100644 --- a/uv.lock +++ b/uv.lock @@ -686,6 +686,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/a8/c070e1340636acb38d4e6a7e45c46d168a462b48b9b3257e14ca0e5af79b/environs-14.6.0-py3-none-any.whl", hash = "sha256:f8fb3d6c6a55872b0c6db077a28f5a8c7b8984b7c32029613d44cef95cfc0812", size = 17205, upload-time = "2026-02-20T04:02:07.299Z" }, ] +[[package]] +name = "exa-py" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpcore" }, + { name = "httpx" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/bb/23c9f78edbf0e0d656839be7346a2f77b9caaae8cc3cb301012c46fd7dc5/exa_py-2.10.1.tar.gz", hash = "sha256:731958c2befc5fc82f031c93cfe7b3d55dc3b0e1bf32f83ec34d32a65ee31ba1", size = 53826, upload-time = "2026-03-25T00:50:49.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/8d/0665263aa8d51ef8e2a3955e2b56496add4879730451961b09610bbc7036/exa_py-2.10.1-py3-none-any.whl", hash = "sha256:e2174c932764fff747e84e9e6d0637eaa4a6503556014df73a3427f42cc9d6a7", size = 72270, upload-time = "2026-03-25T00:50:47.721Z" }, +] + [[package]] name = "fastapi" version = "0.128.3" @@ -1765,6 +1783,7 @@ source = { editable = "." } dependencies = [ { name = "cryptography" }, { name = "deepagents" }, + { name = "exa-py" }, { name = "fastapi" }, { name = "httpx" }, { name = "langchain" }, @@ -1793,6 +1812,7 @@ dev = [ requires-dist = [ { name = "cryptography", specifier = ">=41.0.0" }, { name = "deepagents", specifier = ">=0.4.3" }, + { name = "exa-py", specifier = ">=2.10.1" }, { name = "fastapi", specifier = ">=0.104.0" }, { name = "httpx", specifier = ">=0.25.0" }, { name = "langchain", specifier = ">=1.2.9" },