mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-02 13:48:09 +00:00
feat(crewai-tools): add highlights to ExaSearchTool, rename from EXASearchTool
Some checks failed
Some checks failed
* feat(crewai-tools): add highlights to ExaSearchTool, rename from EXASearchTool - Add a highlights init param so agents can get token-efficient excerpts instead of full pages - Rename EXASearchTool to ExaSearchTool; keep EXASearchTool as a deprecated alias so existing imports keep working - Update the docs and example to use highlights as the recommended option - Add a small note that says Exa is the fastest and most accurate web search API - Add tests for the new highlights param and the deprecation alias * fix(crewai-tools): import order and module-level Exa for tests - Reorder std-lib imports so ruff is happy with force-sort-within-sections. - Import Exa at module level (with a fallback) so the existing test mocks resolve. The lazy install prompt still works if exa_py is missing. - Allow content and summary to be a dict, matching highlights. - Trim test file to the cases this PR introduces (highlights param and the EXASearchTool deprecation alias). Existing init-shape tests stay. Co-Authored-By: ishan <ishan@exa.ai> * chore(crewai-tools): drop self-explanatory comment on schema alias Co-Authored-By: ishan <ishan@exa.ai> * docs(crewai-tools): default highlights to True, drop summary from examples Co-Authored-By: ishan <ishan@exa.ai> * docs(crewai-tools): simplify highlights examples to highlights=True Co-Authored-By: ishan <ishan@exa.ai> * feat(crewai-tools): add x-exa-integration header for usage tracking Co-Authored-By: ishan <ishan@exa.ai> * docs(crewai-tools): add Exa MCP section and resources links Co-Authored-By: ishan <ishan@exa.ai> --------- Co-authored-by: ishan <ishan@exa.ai> Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com> Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com>
This commit is contained in:
@@ -26,7 +26,7 @@ CrewAI provides an extensive collection of powerful tools ready to enhance your
|
||||
- **Web Scraping**: `ScrapeWebsiteTool`, `SeleniumScrapingTool`
|
||||
- **Database Integrations**: `MySQLSearchTool`
|
||||
- **Vector Database Integrations**: `MongoDBVectorSearchTool`, `QdrantVectorSearchTool`, `WeaviateVectorSearchTool`
|
||||
- **API Integrations**: `SerperApiTool`, `EXASearchTool`
|
||||
- **API Integrations**: `SerperApiTool`, `ExaSearchTool`
|
||||
- **AI-powered Tools**: `DallETool`, `VisionTool`, `StagehandTool`
|
||||
|
||||
And many more robust tools to simplify your agent integrations.
|
||||
|
||||
@@ -76,7 +76,7 @@ from crewai_tools.tools.e2b_sandbox_tool import (
|
||||
E2BFileTool,
|
||||
E2BPythonTool,
|
||||
)
|
||||
from crewai_tools.tools.exa_tools.exa_search_tool import EXASearchTool
|
||||
from crewai_tools.tools.exa_tools.exa_search_tool import EXASearchTool, ExaSearchTool
|
||||
from crewai_tools.tools.file_read_tool.file_read_tool import FileReadTool
|
||||
from crewai_tools.tools.file_writer_tool.file_writer_tool import FileWriterTool
|
||||
from crewai_tools.tools.files_compressor_tool.files_compressor_tool import (
|
||||
@@ -258,6 +258,7 @@ __all__ = [
|
||||
"E2BPythonTool",
|
||||
"EXASearchTool",
|
||||
"EnterpriseActionTool",
|
||||
"ExaSearchTool",
|
||||
"FileCompressorTool",
|
||||
"FileReadTool",
|
||||
"FileWriterTool",
|
||||
|
||||
@@ -65,7 +65,7 @@ from crewai_tools.tools.e2b_sandbox_tool import (
|
||||
E2BFileTool,
|
||||
E2BPythonTool,
|
||||
)
|
||||
from crewai_tools.tools.exa_tools.exa_search_tool import EXASearchTool
|
||||
from crewai_tools.tools.exa_tools.exa_search_tool import EXASearchTool, ExaSearchTool
|
||||
from crewai_tools.tools.file_read_tool.file_read_tool import FileReadTool
|
||||
from crewai_tools.tools.file_writer_tool.file_writer_tool import FileWriterTool
|
||||
from crewai_tools.tools.files_compressor_tool.files_compressor_tool import (
|
||||
@@ -242,6 +242,7 @@ __all__ = [
|
||||
"E2BFileTool",
|
||||
"E2BPythonTool",
|
||||
"EXASearchTool",
|
||||
"ExaSearchTool",
|
||||
"FileCompressorTool",
|
||||
"FileReadTool",
|
||||
"FileWriterTool",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# EXASearchTool Documentation
|
||||
# ExaSearchTool Documentation
|
||||
|
||||
## Description
|
||||
This tool is designed to perform a semantic search for a specified query from a text's content across the internet. It utilizes the `https://exa.ai/` API to fetch and display the most relevant search results based on the query provided by the user.
|
||||
This tool lets CrewAI agents search the web using [Exa](https://exa.ai/), the fastest and most accurate web search API. By default the tool returns token-efficient highlights of the most relevant results for any query; you can also opt in to full page content.
|
||||
|
||||
## Installation
|
||||
To incorporate this tool into your project, follow the installation instructions below:
|
||||
@@ -10,21 +10,23 @@ uv add crewai[tools] exa_py
|
||||
```
|
||||
|
||||
## Example
|
||||
The following example demonstrates how to initialize the tool and execute a search with a given query:
|
||||
The following example demonstrates how to initialize the tool and run a search:
|
||||
|
||||
```python
|
||||
from crewai_tools import EXASearchTool
|
||||
from crewai_tools import ExaSearchTool
|
||||
|
||||
# Initialize the tool for internet searching capabilities
|
||||
tool = EXASearchTool(api_key="your_api_key")
|
||||
# Default: results with token-efficient highlights
|
||||
tool = ExaSearchTool(api_key="your_api_key", highlights=True)
|
||||
```
|
||||
|
||||
## Steps to Get Started
|
||||
To effectively use the `EXASearchTool`, follow these steps:
|
||||
To effectively use the `ExaSearchTool`, follow these steps:
|
||||
|
||||
1. **Package Installation**: Confirm that the `crewai[tools]` package is installed in your Python environment.
|
||||
2. **API Key Acquisition**: Acquire a `https://exa.ai/` API key by registering for a free account at `https://exa.ai/`.
|
||||
3. **Environment Configuration**: Store your obtained API key in an environment variable named `EXA_API_KEY` to facilitate its use by the tool.
|
||||
2. **API Key Acquisition**: Get an Exa API key from the [Exa dashboard](https://dashboard.exa.ai/api-keys).
|
||||
3. **Environment Configuration**: Store your API key in an environment variable named `EXA_API_KEY` so the tool can pick it up automatically.
|
||||
|
||||
## Conclusion
|
||||
By integrating the `EXASearchTool` into Python projects, users gain the ability to conduct real-time, relevant searches across the internet directly from their applications. By adhering to the setup and usage guidelines provided, incorporating this tool into projects is streamlined and straightforward.
|
||||
For details on choosing between highlights and full content, see the [Exa search best practices](https://exa.ai/docs/reference/search-best-practices).
|
||||
|
||||
## Note
|
||||
`EXASearchTool` is a deprecated alias for `ExaSearchTool`. Existing imports continue to work but emit a deprecation warning; please migrate to `ExaSearchTool`.
|
||||
|
||||
@@ -3,12 +3,19 @@ from __future__ import annotations
|
||||
from builtins import type as type_
|
||||
import os
|
||||
from typing import Any, TypedDict
|
||||
import warnings
|
||||
|
||||
from crewai.tools import BaseTool, EnvVar
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from typing_extensions import Required
|
||||
|
||||
|
||||
try:
|
||||
from exa_py import Exa
|
||||
except ImportError:
|
||||
Exa = None # type: ignore[assignment,misc]
|
||||
|
||||
|
||||
class SearchParams(TypedDict, total=False):
|
||||
"""Parameters for Exa search API."""
|
||||
|
||||
@@ -18,7 +25,7 @@ class SearchParams(TypedDict, total=False):
|
||||
include_domains: list[str]
|
||||
|
||||
|
||||
class EXABaseToolSchema(BaseModel):
|
||||
class ExaBaseToolSchema(BaseModel):
|
||||
search_query: str = Field(
|
||||
..., description="Mandatory search query you want to use to search the internet"
|
||||
)
|
||||
@@ -31,14 +38,20 @@ class EXABaseToolSchema(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class EXASearchTool(BaseTool):
|
||||
EXABaseToolSchema = ExaBaseToolSchema
|
||||
|
||||
|
||||
class ExaSearchTool(BaseTool):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
name: str = "EXASearchTool"
|
||||
description: str = "Search the internet using Exa"
|
||||
args_schema: type_[BaseModel] = EXABaseToolSchema
|
||||
name: str = "ExaSearchTool"
|
||||
description: str = (
|
||||
"Search the web with Exa, the fastest and most accurate web search API."
|
||||
)
|
||||
args_schema: type_[BaseModel] = ExaBaseToolSchema
|
||||
client: Any | None = None
|
||||
content: bool | None = False
|
||||
summary: bool | None = False
|
||||
content: bool | dict[str, Any] | None = False
|
||||
summary: bool | dict[str, Any] | None = False
|
||||
highlights: bool | dict[str, Any] | None = True
|
||||
type: str | None = "auto"
|
||||
package_dependencies: list[str] = Field(default_factory=lambda: ["exa_py"])
|
||||
api_key: str | None = Field(
|
||||
@@ -68,17 +81,17 @@ class EXASearchTool(BaseTool):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: bool | None = False,
|
||||
summary: bool | None = False,
|
||||
content: bool | dict[str, Any] | None = False,
|
||||
summary: bool | dict[str, Any] | None = False,
|
||||
highlights: bool | dict[str, Any] | None = True,
|
||||
type: str | None = "auto",
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
**kwargs,
|
||||
)
|
||||
try:
|
||||
from exa_py import Exa
|
||||
except ImportError as e:
|
||||
global Exa
|
||||
if Exa is None:
|
||||
import click
|
||||
|
||||
if click.confirm(
|
||||
@@ -88,12 +101,13 @@ class EXASearchTool(BaseTool):
|
||||
|
||||
subprocess.run(["uv", "add", "exa_py"], check=True) # noqa: S607
|
||||
|
||||
# Re-import after installation
|
||||
from exa_py import Exa
|
||||
from exa_py import Exa as _Exa
|
||||
|
||||
Exa = _Exa # type: ignore[misc]
|
||||
else:
|
||||
raise ImportError(
|
||||
"You are missing the 'exa_py' package. Would you like to install it?"
|
||||
) from e
|
||||
"You are missing the 'exa_py' package. Please install it to use ExaSearchTool."
|
||||
)
|
||||
|
||||
client_kwargs: dict[str, str] = {}
|
||||
if self.api_key:
|
||||
@@ -101,8 +115,10 @@ class EXASearchTool(BaseTool):
|
||||
if self.base_url:
|
||||
client_kwargs["base_url"] = self.base_url
|
||||
self.client = Exa(**client_kwargs)
|
||||
self.client.headers["x-exa-integration"] = "crewai"
|
||||
self.content = content
|
||||
self.summary = summary
|
||||
self.highlights = highlights
|
||||
self.type = type
|
||||
|
||||
def _run(
|
||||
@@ -126,10 +142,31 @@ class EXASearchTool(BaseTool):
|
||||
if include_domains:
|
||||
search_params["include_domains"] = include_domains
|
||||
|
||||
contents_kwargs: dict[str, Any] = {}
|
||||
if self.content:
|
||||
results = self.client.search_and_contents(
|
||||
search_query, summary=self.summary, **search_params
|
||||
contents_kwargs["text"] = self.content
|
||||
if self.highlights:
|
||||
contents_kwargs["highlights"] = self.highlights
|
||||
if self.summary:
|
||||
contents_kwargs["summary"] = self.summary
|
||||
|
||||
if contents_kwargs:
|
||||
return self.client.search_and_contents(
|
||||
search_query, **contents_kwargs, **search_params
|
||||
)
|
||||
else:
|
||||
results = self.client.search(search_query, **search_params)
|
||||
return results
|
||||
return self.client.search(search_query, **search_params)
|
||||
|
||||
|
||||
class EXASearchTool(ExaSearchTool):
|
||||
"""Deprecated alias for :class:`ExaSearchTool`. Kept for backwards compatibility."""
|
||||
|
||||
name: str = "ExaSearchTool"
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
warnings.warn(
|
||||
"EXASearchTool is deprecated and will be removed in a future release; "
|
||||
"use ExaSearchTool instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from crewai_tools import EXASearchTool
|
||||
from crewai_tools import EXASearchTool, ExaSearchTool
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def exa_search_tool():
|
||||
return EXASearchTool(api_key="test_api_key")
|
||||
return ExaSearchTool(api_key="test_api_key")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -22,11 +22,12 @@ def test_exa_search_tool_initialization():
|
||||
"crewai_tools.tools.exa_tools.exa_search_tool.Exa"
|
||||
) as mock_exa_class:
|
||||
api_key = "test_api_key"
|
||||
tool = EXASearchTool(api_key=api_key)
|
||||
tool = ExaSearchTool(api_key=api_key)
|
||||
|
||||
assert tool.api_key == api_key
|
||||
assert tool.content is False
|
||||
assert tool.summary is False
|
||||
assert tool.highlights is True
|
||||
assert tool.type == "auto"
|
||||
mock_exa_class.assert_called_once_with(api_key=api_key)
|
||||
|
||||
@@ -36,7 +37,7 @@ def test_exa_search_tool_initialization_with_env(mock_exa_api_key):
|
||||
with patch(
|
||||
"crewai_tools.tools.exa_tools.exa_search_tool.Exa"
|
||||
) as mock_exa_class:
|
||||
EXASearchTool()
|
||||
ExaSearchTool()
|
||||
mock_exa_class.assert_called_once_with(api_key="test_key_from_env")
|
||||
|
||||
|
||||
@@ -47,12 +48,13 @@ def test_exa_search_tool_initialization_with_base_url():
|
||||
) as mock_exa_class:
|
||||
api_key = "test_api_key"
|
||||
base_url = "https://custom.exa.api.com"
|
||||
tool = EXASearchTool(api_key=api_key, base_url=base_url)
|
||||
tool = ExaSearchTool(api_key=api_key, base_url=base_url)
|
||||
|
||||
assert tool.api_key == api_key
|
||||
assert tool.base_url == base_url
|
||||
assert tool.content is False
|
||||
assert tool.summary is False
|
||||
assert tool.highlights is True
|
||||
assert tool.type == "auto"
|
||||
mock_exa_class.assert_called_once_with(api_key=api_key, base_url=base_url)
|
||||
|
||||
@@ -67,7 +69,7 @@ def test_exa_search_tool_initialization_with_env_base_url(
|
||||
mock_exa_api_key, mock_exa_base_url
|
||||
):
|
||||
with patch("crewai_tools.tools.exa_tools.exa_search_tool.Exa") as mock_exa_class:
|
||||
EXASearchTool()
|
||||
ExaSearchTool()
|
||||
mock_exa_class.assert_called_once_with(
|
||||
api_key="test_key_from_env", base_url="https://env.exa.api.com"
|
||||
)
|
||||
@@ -79,8 +81,33 @@ def test_exa_search_tool_initialization_without_base_url():
|
||||
"crewai_tools.tools.exa_tools.exa_search_tool.Exa"
|
||||
) as mock_exa_class:
|
||||
api_key = "test_api_key"
|
||||
tool = EXASearchTool(api_key=api_key)
|
||||
tool = ExaSearchTool(api_key=api_key)
|
||||
|
||||
assert tool.api_key == api_key
|
||||
assert tool.base_url is None
|
||||
mock_exa_class.assert_called_once_with(api_key=api_key)
|
||||
|
||||
|
||||
def test_exa_search_tool_highlights_uses_search_and_contents():
|
||||
with patch("crewai_tools.tools.exa_tools.exa_search_tool.Exa") as mock_exa_class:
|
||||
mock_client = MagicMock()
|
||||
mock_exa_class.return_value = mock_client
|
||||
tool = ExaSearchTool(
|
||||
api_key="test_api_key", highlights={"max_characters": 4000}
|
||||
)
|
||||
|
||||
tool._run(search_query="hello world")
|
||||
|
||||
mock_client.search_and_contents.assert_called_once_with(
|
||||
"hello world",
|
||||
highlights={"max_characters": 4000},
|
||||
type="auto",
|
||||
)
|
||||
mock_client.search.assert_not_called()
|
||||
|
||||
|
||||
def test_exasearchtool_alias_is_deprecated():
|
||||
with patch("crewai_tools.tools.exa_tools.exa_search_tool.Exa"):
|
||||
with pytest.warns(DeprecationWarning, match="ExaSearchTool"):
|
||||
tool = EXASearchTool(api_key="test_api_key")
|
||||
assert isinstance(tool, ExaSearchTool)
|
||||
|
||||
@@ -774,7 +774,7 @@ def calculator(expression: str) -> str:
|
||||
```
|
||||
|
||||
### Built-in Tools (install with `uv add crewai-tools`)
|
||||
Web/Search: SerperDevTool, ScrapeWebsiteTool, WebsiteSearchTool, EXASearchTool, FirecrawlSearchTool
|
||||
Web/Search: SerperDevTool, ScrapeWebsiteTool, WebsiteSearchTool, ExaSearchTool, FirecrawlSearchTool
|
||||
Documents: FileReadTool, DirectoryReadTool, PDFSearchTool, DOCXSearchTool, CSVSearchTool, JSONSearchTool, XMLSearchTool, MDXSearchTool
|
||||
Code: CodeInterpreterTool, CodeDocsSearchTool, GithubSearchTool
|
||||
Media: DALL-E Tool, YoutubeChannelSearchTool, YoutubeVideoSearchTool
|
||||
|
||||
Reference in New Issue
Block a user