Skip to content

feat: add Exa AI-powered search tool#81

Open
tgonzalezc5 wants to merge 1 commit intohuggingface:mainfrom
tgonzalezc5:add-exa-search
Open

feat: add Exa AI-powered search tool#81
tgonzalezc5 wants to merge 1 commit intohuggingface:mainfrom
tgonzalezc5:add-exa-search

Conversation

@tgonzalezc5
Copy link
Copy Markdown

Summary

Adds a general-purpose web_search tool backed by Exa so the agent can ground answers in current web content when HF docs, papers, and GitHub search aren't enough — e.g. recent blog posts, announcements, non-arxiv references, or cross-domain context.

  • New agent/tools/web_search_tool.py with a typed WebSearchResult model and a snippet cascade (summary → highlights → text).
  • Registered through create_builtin_tools only when EXA_API_KEY is set, so the tool stays dark for users who don't configure it.
  • Added to the research sub-agent's read-only tool allow-list so deep literature/context crawls can use it alongside hf_papers and GitHub tools.
  • Surfaces Exa search types (auto / neural / fast), category, include/exclude domain filters, date filters, and content modes (text / highlights / summary).

Usage

# .env
EXA_API_KEY=<your-exa-api-key>

The agent can then call web_search like any other tool:

{
  "query": "state-of-the-art flash attention kernels 2025",
  "num_results": 5,
  "type": "auto",
  "category": "research paper",
  "include_domains": ["arxiv.org", "github.com"],
  "start_published_date": "2024-01-01"
}

Output format is consistent with the other docs/search tools — title, URL, published date, relevance score, and a snippet that falls back through summary → highlights → text.

Files changed

  • agent/tools/web_search_tool.py — new tool module (handler + spec + result type).
  • agent/tools/__init__.py — export the tool spec / handler / enabled-check.
  • agent/core/tools.py — conditionally register the tool when EXA_API_KEY is set.
  • agent/tools/research_tool.py — allow web_search from the research sub-agent.
  • pyproject.toml — add exa-py>=2.0.0 dependency; move pytest-asyncio into the dev extras so tests run out of the box.
  • README.md — document EXA_API_KEY as an optional env var.
  • tests/unit/test_web_search_tool.py — unit tests (19 new).

Test plan

  • uv run python -m pytest tests/30 passed (11 existing + 19 new).
  • Tool does not appear in the registered tool list when EXA_API_KEY is unset.
  • Tool does appear when EXA_API_KEY is set (verified via create_builtin_tools).
  • x-exa-integration header is set on every Exa client instance (asserted in test_handler_happy_path_sets_integration_header).
  • Snippet cascade: summary preferred → highlights → text → empty.
  • Argument validation: invalid type, invalid category, non-list domain filters, missing query.
  • num_results is clamped to [1, 25].
  • Plain search is used when no content modes are requested; search_and_contents when any are.
  • Manual smoke test with a real EXA_API_KEY in a full CLI session (not run here — requires a key).

Adds a general-purpose web_search tool backed by Exa so the agent can
ground answers in current web content when HF docs, papers, and GitHub
search aren't enough.

- New agent/tools/web_search_tool.py with a typed WebSearchResult model
  and a snippet cascade (summary > highlights > text).
- Registered through create_builtin_tools only when EXA_API_KEY is set,
  so the tool stays dark for users who don't configure it.
- Added to the research sub-agent's read-only tool allow-list.
- Surfaces Exa search types, category, domain filters, date filters,
  and content modes (text / highlights / summary).
- Tests cover response parsing, snippet fallbacks, argument validation,
  router gating, and the integration-attribution header.
@akseljoonas
Copy link
Copy Markdown
Collaborator

@claude please review this PR. Focus on correctness, security, LiteLLM/Bedrock routing impact, and whether it's safe to merge against current main. Keep it concise and prefer inline comments where it matters.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

Claude finished @akseljoonas's task in 2m 25s —— View job


PR Review: feat: add Exa AI-powered search tool

Tasks

  • Read context and understand request
  • Read CLAUDE.md for repo guidelines (none present)
  • Read changed files (web_search_tool.py, tools.py, pyproject.toml, research_tool.py, tests)
  • Analyze: correctness, security, LiteLLM/Bedrock routing impact
  • Post review findings

Verdict: NEEDS-CHANGES before merge (2 items) · Logic/tests are solid


Correctness

🔴 1. exa-py is a core dependency, not optional

pyproject.toml:29

# current (wrong)
dependencies = [
    ...
    "exa-py>=2.0.0",   # ← installs on every `uv sync`, key or not
]

Every deployment pays the install cost even when EXA_API_KEY is never set. This contradicts the PR description and the inline comment ("Optional integration: Exa web search"). Fix: move to [project.optional-dependencies] and document uv sync --extra exa in the README. Alternatively, accept the bloat and update the description — but make it a deliberate choice.

Fix this →

🔴 2. pytest-asyncio>=1.3.0 is almost certainly a typo

pyproject.toml:46 — current stable is the 0.x line (0.26.x). >=1.3.0 has no published release and will fail uv sync --extra dev when the constraint can't be satisfied. Tests pass locally only if pytest-asyncio was already installed separately. Should be >=0.23.0 or a specific known-good version.

Fix this →

🟡 3. __init__.py exports un-imported symbols

agent/tools/__init__.py:40-41__all__ references GITHUB_SEARCH_CODE_TOOL_SPEC and github_search_code_handler which are never imported in that file. This is pre-existing but the PR touches __init__.py and worsens the stale exports list. Any from agent.tools import * will ImportError for these names.

🟡 4. Per-call Exa client instantiation

web_search_tool.py:167 — a new Exa(api_key) client (and its underlying HTTP session) is created on every web_search_handler call via asyncio.to_thread. Fine for correctness, but connection-pooling and Exa's own SDK client reuse would be lost. Not a blocker for MVP; worth noting for high-frequency research loops.

🟢 5. Arg validation is thorough and consistent

num_results clamping (line 224), type/category enum checks (228–238), domain list-type checks (242–245), and empty-query guard (217–218) are all correct and match the tool spec. Snippet cascade logic and _coerce_result handles both dict and object SDK responses cleanly.


Security

🟡 Exception message forwarded to agent

web_search_tool.py:278return f"Web search error: {e}", False returns the raw SDK exception string to the LLM context. The Exa SDK can include auth details (e.g. 401 Unauthorized for key sk-...) in its error strings. The key itself isn't returned to users but it will appear in the agent's tool-result history (and logs, since logger.exception is also called). Low risk but worth sanitising to something like "Web search failed (check EXA_API_KEY and request params)." and keeping the full error in the log only.

🟢 No SSRF / injection surface

All HTTP calls go through the Exa SDK to the Exa API. No user-controlled URLs are fetched directly. Domain filter strings (include_domains) are not validated beyond list-type but they're passed only as Exa API filter params — not used in any local filesystem or shell path. Query strings are not interpolated anywhere.

🟢 API key never logged or surfaced in output

The key is read from env at call time, passed only to the Exa() constructor, and never appears in return values or log statements.


LiteLLM / Bedrock Routing Impact

No impact. The web search tool doesn't call acompletion — it calls the Exa REST API synchronously in a thread and returns a plain string. Tool spec format is standard OpenAI JSON Schema (same as every other built-in tool) and is correctly routed through ToolRouter.get_tool_specs_for_llm().

Prompt caching (prompt_caching.py): adding a tool appends to the tools array, invalidating the cache_control breakpoint on the last tool for one turn, then re-hitting cache on subsequent turns. This is the normal cost of any new tool; no special handling needed.

Bedrock: no active Bedrock path exists in main (feat/bedrock-migration is a pending branch). When that merges, this tool's standard JSON Schema spec will be compatible as-is — LiteLLM's Bedrock adapter translates OpenAI-format tool defs. No forward-compatibility concern.

Research sub-agent: web_search is correctly added to RESEARCH_TOOL_NAMES (research_tool.py:42) and filtered from session.tool_router.get_tool_specs_for_llm() via the allow-list in research_handler. The handler signature (session=None) is compatible with the router's inspect.signature dispatch path.


Summary

Item Severity File
exa-py in core deps 🔴 Blocker pyproject.toml:29
pytest-asyncio>=1.3.0 typo 🔴 Blocker pyproject.toml:46
__init__.py stale __all__ exports 🟡 Minor agent/tools/__init__.py:40-41
Per-call Exa client allocation 🟡 Minor web_search_tool.py:167
SDK error string forwarded to agent 🟡 Minor web_search_tool.py:278

Fix the two 🔴 items and this is good to merge. The handler logic, test coverage (19 tests, all isolation patterns correct), and integration pattern with the research sub-agent are well done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants