From 90cdb48db017a89863ff35498cd5c4576b0aacee Mon Sep 17 00:00:00 2001 From: Lorenze Jay Date: Thu, 30 Jan 2025 15:09:47 -0800 Subject: [PATCH 1/3] latest version of exa --- .../tools/exa_tools/exa_base_tool.py | 47 ------------ .../tools/exa_tools/exa_search_tool.py | 73 ++++++++++++++----- 2 files changed, 55 insertions(+), 65 deletions(-) delete mode 100644 src/crewai_tools/tools/exa_tools/exa_base_tool.py diff --git a/src/crewai_tools/tools/exa_tools/exa_base_tool.py b/src/crewai_tools/tools/exa_tools/exa_base_tool.py deleted file mode 100644 index 295b283ad..000000000 --- a/src/crewai_tools/tools/exa_tools/exa_base_tool.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Type - -from crewai.tools import BaseTool -from pydantic import BaseModel, Field - - -class EXABaseToolToolSchema(BaseModel): - """Input for EXABaseTool.""" - - search_query: str = Field( - ..., description="Mandatory search query you want to use to search the internet" - ) - - -class EXABaseTool(BaseTool): - name: str = "Search the internet" - description: str = ( - "A tool that can be used to search the internet from a search_query" - ) - args_schema: Type[BaseModel] = EXABaseToolToolSchema - search_url: str = "https://api.exa.ai/search" - n_results: int = None - headers: dict = { - "accept": "application/json", - "content-type": "application/json", - } - - def _parse_results(self, results): - string = [] - for result in results: - try: - string.append( - "\n".join( - [ - f"Title: {result['title']}", - f"Score: {result['score']}", - f"Url: {result['url']}", - f"ID: {result['id']}", - "---", - ] - ) - ) - except KeyError: - continue - - content = "\n".join(string) - return f"\nSearch results: {content}\n" diff --git a/src/crewai_tools/tools/exa_tools/exa_search_tool.py b/src/crewai_tools/tools/exa_tools/exa_search_tool.py index 6724c2417..6681e8d1b 100644 --- a/src/crewai_tools/tools/exa_tools/exa_search_tool.py +++ b/src/crewai_tools/tools/exa_tools/exa_search_tool.py @@ -1,30 +1,67 @@ -import os -from typing import Any +from typing import Any, Optional, Type +from pydantic import BaseModel, Field -import requests +try: + from exa_py import Exa -from .exa_base_tool import EXABaseTool + EXA_INSTALLED = True +except ImportError: + Exa = Any + EXA_INSTALLED = False -class EXASearchTool(EXABaseTool): +class EXABaseToolToolSchema(BaseModel): + search_query: str = Field( + ..., description="Mandatory search query you want to use to search the internet" + ) + + +class EXASearchTool: + args_schema: Type[BaseModel] = EXABaseToolToolSchema + client: Optional["Exa"] = Field(default=None, description="Exa search client") + + def __init__( + self, + api_key: str, + content: bool = False, + highlights: bool = False, + type: str = "keyword", + use_autoprompt: bool = True, + ): + if not EXA_INSTALLED: + raise ImportError("`exa-py` package not found, please run `uv add exa-py`") + self.client = Exa(api_key=api_key) + self.content = content + self.highlights = highlights + self.type = type + self.use_autoprompt = use_autoprompt + def _run( self, - **kwargs: Any, + search_query: str, + start_published_date: Optional[str] = None, + end_published_date: Optional[str] = None, + include_domains: Optional[list[str]] = None, ) -> Any: - search_query = kwargs.get("search_query") - if search_query is None: - search_query = kwargs.get("query") + if self.client is None: + raise ValueError("Client not initialized") - payload = { - "query": search_query, - "type": "magic", + search_params = { + "use_autoprompt": self.use_autoprompt, + "type": self.type, } - headers = self.headers.copy() - headers["x-api-key"] = os.environ["EXA_API_KEY"] + if start_published_date: + search_params["start_published_date"] = start_published_date + if end_published_date: + search_params["end_published_date"] = end_published_date + if include_domains: + search_params["include_domains"] = include_domains - response = requests.post(self.search_url, json=payload, headers=headers) - results = response.json() - if "results" in results: - results = super()._parse_results(results["results"]) + if self.content: + results = self.client.search_and_contents( + search_query, highlights=self.highlights, **search_params + ) + else: + results = self.client.search(search_query, **search_params) return results From bcfe015d9d01ab287ae275234517a110a62e41fb Mon Sep 17 00:00:00 2001 From: Lorenze Jay Date: Thu, 30 Jan 2025 15:53:57 -0800 Subject: [PATCH 2/3] ensure works on agent --- .../tools/exa_tools/exa_search_tool.py | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/crewai_tools/tools/exa_tools/exa_search_tool.py b/src/crewai_tools/tools/exa_tools/exa_search_tool.py index 6681e8d1b..6bf834d6c 100644 --- a/src/crewai_tools/tools/exa_tools/exa_search_tool.py +++ b/src/crewai_tools/tools/exa_tools/exa_search_tool.py @@ -1,5 +1,6 @@ from typing import Any, Optional, Type from pydantic import BaseModel, Field +from crewai.tools import BaseTool try: from exa_py import Exa @@ -10,31 +11,48 @@ except ImportError: EXA_INSTALLED = False -class EXABaseToolToolSchema(BaseModel): +class EXABaseToolSchema(BaseModel): search_query: str = Field( ..., description="Mandatory search query you want to use to search the internet" ) + start_published_date: Optional[str] = Field( + None, description="Start date for the search" + ) + end_published_date: Optional[str] = Field( + None, description="End date for the search" + ) + include_domains: Optional[list[str]] = Field( + None, description="List of domains to include in the search" + ) -class EXASearchTool: - args_schema: Type[BaseModel] = EXABaseToolToolSchema - client: Optional["Exa"] = Field(default=None, description="Exa search client") +class EXASearchTool(BaseTool): + model_config = {"arbitrary_types_allowed": True} + name: str = "EXASearchTool" + description: str = "Search the internet using Exa" + args_schema: Type[BaseModel] = EXABaseToolSchema + client: Optional["Exa"] = None + content: Optional[bool] = False + summary: Optional[bool] = False + type: Optional[str] = "auto" def __init__( self, api_key: str, - content: bool = False, - highlights: bool = False, - type: str = "keyword", - use_autoprompt: bool = True, + content: Optional[bool] = False, + summary: Optional[bool] = False, + type: Optional[str] = "auto", + **kwargs, ): + super().__init__( + **kwargs, + ) if not EXA_INSTALLED: raise ImportError("`exa-py` package not found, please run `uv add exa-py`") self.client = Exa(api_key=api_key) self.content = content - self.highlights = highlights + self.summary = summary self.type = type - self.use_autoprompt = use_autoprompt def _run( self, @@ -47,7 +65,6 @@ class EXASearchTool: raise ValueError("Client not initialized") search_params = { - "use_autoprompt": self.use_autoprompt, "type": self.type, } @@ -60,7 +77,7 @@ class EXASearchTool: if self.content: results = self.client.search_and_contents( - search_query, highlights=self.highlights, **search_params + search_query, summary=self.summary, **search_params ) else: results = self.client.search(search_query, **search_params) From 9a09ea7703821997db911ff4b469d3dcf24d4bde Mon Sep 17 00:00:00 2001 From: Lorenze Jay Date: Thu, 30 Jan 2025 16:04:33 -0800 Subject: [PATCH 3/3] better docs and download missing packaged --- src/crewai_tools/tools/exa_tools/README.md | 4 ++-- .../tools/exa_tools/exa_search_tool.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/crewai_tools/tools/exa_tools/README.md b/src/crewai_tools/tools/exa_tools/README.md index 8d556dab3..1d1d20150 100644 --- a/src/crewai_tools/tools/exa_tools/README.md +++ b/src/crewai_tools/tools/exa_tools/README.md @@ -6,7 +6,7 @@ This tool is designed to perform a semantic search for a specified query from a ## Installation To incorporate this tool into your project, follow the installation instructions below: ```shell -pip install 'crewai[tools]' +uv add crewai[tools] exa_py ``` ## Example @@ -16,7 +16,7 @@ The following example demonstrates how to initialize the tool and execute a sear from crewai_tools import EXASearchTool # Initialize the tool for internet searching capabilities -tool = EXASearchTool() +tool = EXASearchTool(api_key="your_api_key") ``` ## Steps to Get Started diff --git a/src/crewai_tools/tools/exa_tools/exa_search_tool.py b/src/crewai_tools/tools/exa_tools/exa_search_tool.py index 6bf834d6c..f094b0495 100644 --- a/src/crewai_tools/tools/exa_tools/exa_search_tool.py +++ b/src/crewai_tools/tools/exa_tools/exa_search_tool.py @@ -48,7 +48,19 @@ class EXASearchTool(BaseTool): **kwargs, ) if not EXA_INSTALLED: - raise ImportError("`exa-py` package not found, please run `uv add exa-py`") + import click + + if click.confirm( + "You are missing the 'exa_py' package. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "exa_py"], check=True) + + else: + raise ImportError( + "You are missing the 'exa_py' package. Would you like to install it?" + ) self.client = Exa(api_key=api_key) self.content = content self.summary = summary