fix: add Exa web search tool [closes: OPE-29] (#1133)

* fix: add Exa web search tool

* chore: update uv.lock for exa-py dependency

* linting

* chore: remove web_search from system prompt

* chore: drop search_type and category params from web_search
This commit is contained in:
Aran Yogesh 2026-03-25 16:24:31 -07:00 • committed by GitHub
parent 87968ab813
commit 7d1004ad66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 91 additions and 0 deletions

View file

@ -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)

View file

@ -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,

View file

@ -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",
]

63
agent/tools/web_search.py Normal file
View file

@ -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}"}

View file

@ -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]

20
uv.lock generated
View file

@ -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" },