diff --git a/docs/docs.json b/docs/docs.json index 22a2c963fe..ef874720e9 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -259,6 +259,7 @@ "pages": [ "edge/en/tools/search-research/overview", "edge/en/tools/search-research/serperdevtool", + "edge/en/tools/search-research/searchapitool", "edge/en/tools/search-research/bravesearchtool", "edge/en/tools/search-research/exasearchtool", "edge/en/tools/search-research/linkupsearchtool", @@ -13912,6 +13913,7 @@ "pages": [ "edge/pt-BR/tools/search-research/overview", "edge/pt-BR/tools/search-research/serperdevtool", + "edge/pt-BR/tools/search-research/searchapitool", "edge/pt-BR/tools/search-research/bravesearchtool", "edge/pt-BR/tools/search-research/exasearchtool", "edge/pt-BR/tools/search-research/linkupsearchtool", @@ -26630,6 +26632,7 @@ "pages": [ "edge/ko/tools/search-research/overview", "edge/ko/tools/search-research/serperdevtool", + "edge/ko/tools/search-research/searchapitool", "edge/ko/tools/search-research/bravesearchtool", "edge/ko/tools/search-research/exasearchtool", "edge/ko/tools/search-research/linkupsearchtool", @@ -39777,6 +39780,7 @@ "pages": [ "edge/ar/tools/search-research/overview", "edge/ar/tools/search-research/serperdevtool", + "edge/ar/tools/search-research/searchapitool", "edge/ar/tools/search-research/bravesearchtool", "edge/ar/tools/search-research/exasearchtool", "edge/ar/tools/search-research/linkupsearchtool", diff --git a/docs/edge/ar/tools/search-research/overview.mdx b/docs/edge/ar/tools/search-research/overview.mdx index 6b73d12b5d..4d57d32b12 100644 --- a/docs/edge/ar/tools/search-research/overview.mdx +++ b/docs/edge/ar/tools/search-research/overview.mdx @@ -14,6 +14,10 @@ mode: "wide" تكامل مع Google search API لقدرات بحث شاملة على الويب. + + عدة محركات بحث خلف نقطة نهاية واحدة: بحث Google على الويب والأخبار والأبحاث الأكاديمية والوظائف، إضافة إلى Bing و YouTube وغيرها. + + بحث يركز على الخصوصية مع فهرس بحث Brave المستقل. diff --git a/docs/edge/ar/tools/search-research/searchapitool.mdx b/docs/edge/ar/tools/search-research/searchapitool.mdx new file mode 100644 index 0000000000..6f10ea92b0 --- /dev/null +++ b/docs/edge/ar/tools/search-research/searchapitool.mdx @@ -0,0 +1,74 @@ +--- +title: بحث SearchApi +description: أداة `SearchApiTool` تبحث في الإنترنت عبر SearchApi، الذي يجمع عدة محركات بحث خلف نقطة نهاية واحدة. +icon: magnifying-glass +mode: "wide" +--- + +# `SearchApiTool` + +## الوصف + +أداة `SearchApiTool` تبحث في الإنترنت عبر [SearchApi](https://www.searchapi.io). نقطة نهاية واحدة تجمع عدة محركات، تُختار بالمعامل `engine`، لذا تغطي أداة واحدة بحث Google على الويب والأخبار والأبحاث الأكاديمية والوظائف، إضافة إلى Bing و YouTube و Baidu وبقية [المحركات المدعومة](https://www.searchapi.io/docs). تُرجع الأداة JSON الخاص بالمحرك نفسه، لذا تتطابق النتائج مع توثيق ذلك المحرك. + +يحدث أمران للاستجابة قبل أن يراها الـ Agent: + +- تُحذف روابط `data:` المضمّنة. يُرجع SearchApi الأيقونات والصور المصغّرة كسلاسل base64، وقد تبلغ الواحدة منها عشرات الكيلوبايتات من السياق الذي لا يعني شيئًا للـ Agent. +- تُقتطع السلاسل الطويلة عند `max_string_length`، وتُحدَّد كل قائمة `*_results` بعدد `n_results`. + +## التثبيت + +1. **تثبيت الحزمة**: تأكد من تثبيت حزمة `crewai[tools]` في بيئة Python لديك. +2. **الحصول على مفتاح API**: احصل على مفتاح SearchApi من https://www.searchapi.io (تتوفر خطة مجانية). +3. **إعداد البيئة**: احفظ المفتاح في متغير بيئة باسم `SEARCHAPI_API_KEY`. + +```shell +pip install 'crewai[tools]' +``` + +يُرسَل المفتاح في ترويسة `Authorization` بدلًا من سلسلة الاستعلام، لذا يبقى خارج سجلات الطلبات وخارج `request_url` الذي يعيده SearchApi في `search_metadata`. + +## مثال + +```python Code +from crewai_tools import SearchApiTool + +# Initialize the tool for internet searching capabilities +tool = SearchApiTool() +``` + +## المعاملات + +- **engine**: محرك SearchApi المطلوب، مثل `google` أو `google_news` أو `google_scholar` أو `google_jobs` أو `bing` أو `youtube` أو `baidu`. القيمة الافتراضية `google`. ويمكن تمريره مع كل استدعاء. +- **n_results**: الحد الأقصى لطول كل قائمة `*_results` في الاستجابة. القيمة الافتراضية `10`. +- **country**: اختياري. بلد البحث، يُرسَل باسم `gl` (مثل `uk`). +- **locale**: اختياري. لغة الواجهة، تُرسَل باسم `hl` (مثل `en`). +- **location**: اختياري. الموقع المعياري للبحث، مثل `London,England`. +- **max_string_length**: أطول سلسلة تبقى كما هي في الاستجابة. القيمة الافتراضية `1000`. +- **timeout**: مهلة الطلب بالثواني. القيمة الافتراضية `30`. +- **api_key**: اختياري. مفتاح SearchApi الخاص بك. وإن لم يُحدَّد، يُقرأ من متغير البيئة `SEARCHAPI_API_KEY`. +- **search_url**: نقطة النهاية التي تُستدعى. القيمة الافتراضية `https://www.searchapi.io/api/v1/search`. + +## مثال مع المعاملات + +```python Code +from crewai_tools import SearchApiTool + +# Recent news, localized to the UK +news_tool = SearchApiTool( + engine="google_news", + country="uk", + locale="en", + location="London,England", + n_results=5, +) + +# Academic search through the same tool +scholar_tool = SearchApiTool(engine="google_scholar") +``` + +## الأخطاء + +يؤدي فشل الطلب إلى إطلاق `RuntimeError` يحمل رسالة SearchApi نفسها، مثل `SearchApi request failed (HTTP 401): Invalid API key.`. + +أما البحث الناجح الذي لم يجد شيئًا فليس خطأً: يُرجع SearchApi استجابة HTTP 200 مع رسالة `error` مثل `"Google didn't return any results."`، وتمرّر الأداة هذه الرسالة كما هي ليقرأ الـ Agent سبب خلو الصفحة. diff --git a/docs/edge/en/tools/search-research/overview.mdx b/docs/edge/en/tools/search-research/overview.mdx index 6fdd8410c4..9d73c9599d 100644 --- a/docs/edge/en/tools/search-research/overview.mdx +++ b/docs/edge/en/tools/search-research/overview.mdx @@ -14,6 +14,10 @@ These tools enable your agents to search the web, research topics, and find info Google search API integration for comprehensive web search capabilities. + + Many search engines behind one endpoint: Google web, news, scholar and jobs, plus Bing, YouTube and more. + + Privacy-focused search with Brave's independent search index. diff --git a/docs/edge/en/tools/search-research/searchapitool.mdx b/docs/edge/en/tools/search-research/searchapitool.mdx new file mode 100644 index 0000000000..77ce8bcfe1 --- /dev/null +++ b/docs/edge/en/tools/search-research/searchapitool.mdx @@ -0,0 +1,74 @@ +--- +title: SearchApi Search +description: The `SearchApiTool` searches the internet through SearchApi, which fronts many search engines behind one endpoint. +icon: magnifying-glass +mode: "wide" +--- + +# `SearchApiTool` + +## Description + +The `SearchApiTool` searches the internet through [SearchApi](https://www.searchapi.io). One endpoint fronts many engines, chosen with the `engine` parameter, so a single tool covers Google web search, news, scholar and jobs, plus Bing, YouTube, Baidu and the rest of the [supported engines](https://www.searchapi.io/docs). The tool returns the engine's own JSON, so results line up with the engine's documentation. + +Two things happen to the response before an agent sees it: + +- Inline `data:` URIs are dropped. SearchApi returns favicons and thumbnails as base64 strings, and a single one can run to tens of kilobytes of context that means nothing to an agent. +- Long strings are truncated at `max_string_length`, and every `*_results` list is capped at `n_results`. + +## Installation + +1. **Package Installation**: Confirm that the `crewai[tools]` package is installed in your Python environment. +2. **API Key Acquisition**: Get a SearchApi key at https://www.searchapi.io (free tier available). +3. **Environment Configuration**: Store the key in an environment variable named `SEARCHAPI_API_KEY`. + +```shell +pip install 'crewai[tools]' +``` + +The key is sent in the `Authorization` header rather than the query string, so it stays out of request logs and out of the `request_url` SearchApi echoes back in `search_metadata`. + +## Example + +```python Code +from crewai_tools import SearchApiTool + +# Initialize the tool for internet searching capabilities +tool = SearchApiTool() +``` + +## Parameters + +- **engine**: The SearchApi engine to query, such as `google`, `google_news`, `google_scholar`, `google_jobs`, `bing`, `youtube` or `baidu`. Default is `google`. Can also be passed per call. +- **n_results**: Cap on the length of each `*_results` list in the response. Default is `10`. +- **country**: Optional. Country of the search, sent as `gl` (for example `uk`). +- **locale**: Optional. Interface language, sent as `hl` (for example `en`). +- **location**: Optional. Canonical location of the search, for example `London,England`. +- **max_string_length**: Longest string kept intact in the response. Default is `1000`. +- **timeout**: Request timeout in seconds. Default is `30`. +- **api_key**: Optional. Your SearchApi key. Falls back to the `SEARCHAPI_API_KEY` environment variable. +- **search_url**: The endpoint to call. Default is `https://www.searchapi.io/api/v1/search`. + +## Example with Parameters + +```python Code +from crewai_tools import SearchApiTool + +# Recent news, localized to the UK +news_tool = SearchApiTool( + engine="google_news", + country="uk", + locale="en", + location="London,England", + n_results=5, +) + +# Academic search through the same tool +scholar_tool = SearchApiTool(engine="google_scholar") +``` + +## Errors + +A failed request raises a `RuntimeError` carrying SearchApi's own message, for example `SearchApi request failed (HTTP 401): Invalid API key.`. + +A successful search that found nothing is not an error: SearchApi returns HTTP 200 with an `error` message such as `"Google didn't return any results."`, and the tool passes that through so the agent can read why the page was empty. diff --git a/docs/edge/ko/tools/search-research/overview.mdx b/docs/edge/ko/tools/search-research/overview.mdx index ff59c38971..d565e32c10 100644 --- a/docs/edge/ko/tools/search-research/overview.mdx +++ b/docs/edge/ko/tools/search-research/overview.mdx @@ -14,6 +14,10 @@ mode: "wide" 종합적인 웹 검색 기능을 위한 Google 검색 API 통합. + + 하나의 엔드포인트로 여러 검색 엔진을 제공합니다: Google 웹, 뉴스, 학술, 채용 정보에 더해 Bing, YouTube 등. + + Brave의 독립적인 검색 인덱스를 활용한 프라이버시 중심의 검색. diff --git a/docs/edge/ko/tools/search-research/searchapitool.mdx b/docs/edge/ko/tools/search-research/searchapitool.mdx new file mode 100644 index 0000000000..bc0a5471aa --- /dev/null +++ b/docs/edge/ko/tools/search-research/searchapitool.mdx @@ -0,0 +1,74 @@ +--- +title: SearchApi 검색 +description: SearchApiTool은(는) 여러 검색 엔진을 하나의 엔드포인트로 제공하는 SearchApi를 통해 인터넷을 검색합니다. +icon: magnifying-glass +mode: "wide" +--- + +# `SearchApiTool` + +## 설명 + +`SearchApiTool`은 [SearchApi](https://www.searchapi.io)를 통해 인터넷을 검색합니다. 하나의 엔드포인트가 여러 엔진을 제공하며 `engine` 파라미터로 선택하므로, 단일 도구로 Google 웹 검색, 뉴스, 학술, 채용 정보는 물론 Bing, YouTube, Baidu 및 나머지 [지원 엔진](https://www.searchapi.io/docs)까지 다룰 수 있습니다. 이 도구는 엔진의 JSON을 그대로 반환하므로 결과는 해당 엔진의 문서와 일치합니다. + +에이전트가 응답을 보기 전에 두 가지 처리가 이루어집니다: + +- 인라인 `data:` URI가 제거됩니다. SearchApi는 파비콘과 썸네일을 base64 문자열로 반환하는데, 하나만으로도 수십 킬로바이트에 이를 수 있으며 에이전트에게는 아무런 의미가 없습니다. +- 긴 문자열은 `max_string_length`에서 잘리고, 모든 `*_results` 목록은 `n_results` 개수로 제한됩니다. + +## 설치 + +1. **패키지 설치**: Python 환경에 `crewai[tools]` 패키지가 설치되어 있는지 확인하세요. +2. **API 키 발급**: https://www.searchapi.io 에서 SearchApi 키를 발급받으세요(무료 티어 제공). +3. **환경 설정**: 발급받은 키를 `SEARCHAPI_API_KEY`라는 환경 변수에 저장하세요. + +```shell +pip install 'crewai[tools]' +``` + +키는 쿼리 문자열이 아니라 `Authorization` 헤더로 전송되므로, 요청 로그와 SearchApi가 `search_metadata`에 되돌려주는 `request_url`에 노출되지 않습니다. + +## 예제 + +```python Code +from crewai_tools import SearchApiTool + +# Initialize the tool for internet searching capabilities +tool = SearchApiTool() +``` + +## 파라미터 + +- **engine**: 호출할 SearchApi 엔진입니다. 예: `google`, `google_news`, `google_scholar`, `google_jobs`, `bing`, `youtube`, `baidu`. 기본값은 `google`입니다. 호출할 때마다 전달할 수도 있습니다. +- **n_results**: 응답에서 각 `*_results` 목록의 최대 길이입니다. 기본값은 `10`입니다. +- **country**: 선택 사항. 검색 대상 국가이며 `gl`로 전송됩니다(예: `uk`). +- **locale**: 선택 사항. 인터페이스 언어이며 `hl`로 전송됩니다(예: `en`). +- **location**: 선택 사항. 검색의 표준 위치입니다(예: `London,England`). +- **max_string_length**: 응답에서 그대로 유지되는 문자열의 최대 길이입니다. 기본값은 `1000`입니다. +- **timeout**: 요청 타임아웃(초)입니다. 기본값은 `30`입니다. +- **api_key**: 선택 사항. SearchApi 키입니다. 지정하지 않으면 `SEARCHAPI_API_KEY` 환경 변수를 사용합니다. +- **search_url**: 호출할 엔드포인트입니다. 기본값은 `https://www.searchapi.io/api/v1/search`입니다. + +## 파라미터를 사용한 예제 + +```python Code +from crewai_tools import SearchApiTool + +# Recent news, localized to the UK +news_tool = SearchApiTool( + engine="google_news", + country="uk", + locale="en", + location="London,England", + n_results=5, +) + +# Academic search through the same tool +scholar_tool = SearchApiTool(engine="google_scholar") +``` + +## 오류 + +요청이 실패하면 SearchApi의 메시지를 담은 `RuntimeError`가 발생합니다. 예: `SearchApi request failed (HTTP 401): Invalid API key.` + +검색은 성공했지만 결과가 없는 경우는 오류가 아닙니다. SearchApi는 HTTP 200과 함께 `"Google didn't return any results."` 같은 `error` 메시지를 반환하며, 이 도구는 에이전트가 그 이유를 읽을 수 있도록 해당 메시지를 그대로 전달합니다. diff --git a/docs/edge/pt-BR/tools/search-research/overview.mdx b/docs/edge/pt-BR/tools/search-research/overview.mdx index bece4353eb..ab97a08485 100644 --- a/docs/edge/pt-BR/tools/search-research/overview.mdx +++ b/docs/edge/pt-BR/tools/search-research/overview.mdx @@ -14,6 +14,10 @@ Essas ferramentas permitem que seus agentes pesquisem na web, explorem tópicos Integração com a API de busca do Google para capacidades abrangentes de pesquisa na web. + + Vários motores de busca em um único endpoint: web, notícias, artigos acadêmicos e vagas do Google, além de Bing, YouTube e outros. + + Pesquisa voltada para privacidade com o índice independente de busca do Brave. diff --git a/docs/edge/pt-BR/tools/search-research/searchapitool.mdx b/docs/edge/pt-BR/tools/search-research/searchapitool.mdx new file mode 100644 index 0000000000..3dad234ebe --- /dev/null +++ b/docs/edge/pt-BR/tools/search-research/searchapitool.mdx @@ -0,0 +1,74 @@ +--- +title: Pesquisa SearchApi +description: O `SearchApiTool` pesquisa na internet através do SearchApi, que reúne vários motores de busca em um único endpoint. +icon: magnifying-glass +mode: "wide" +--- + +# `SearchApiTool` + +## Descrição + +O `SearchApiTool` pesquisa na internet através do [SearchApi](https://www.searchapi.io). Um único endpoint reúne vários motores, escolhidos com o parâmetro `engine`, de modo que uma só ferramenta cobre a busca web do Google, notícias, artigos acadêmicos e vagas, além de Bing, YouTube, Baidu e os demais [motores suportados](https://www.searchapi.io/docs). A ferramenta retorna o JSON do próprio motor, então os resultados correspondem à documentação dele. + +Duas coisas acontecem com a resposta antes que um agente a veja: + +- URIs `data:` embutidas são descartadas. O SearchApi retorna favicons e miniaturas como strings base64, e uma única delas pode chegar a dezenas de kilobytes de contexto que não significam nada para um agente. +- Strings longas são truncadas em `max_string_length`, e cada lista `*_results` é limitada a `n_results`. + +## Instalação + +1. **Instalação do pacote**: confirme que o pacote `crewai[tools]` está instalado no seu ambiente Python. +2. **Obtenção da chave de API**: obtenha uma chave do SearchApi em https://www.searchapi.io (há um plano gratuito). +3. **Configuração do ambiente**: guarde a chave em uma variável de ambiente chamada `SEARCHAPI_API_KEY`. + +```shell +pip install 'crewai[tools]' +``` + +A chave é enviada no cabeçalho `Authorization` em vez da query string, portanto não aparece nos logs de requisição nem no `request_url` que o SearchApi devolve em `search_metadata`. + +## Exemplo + +```python Code +from crewai_tools import SearchApiTool + +# Initialize the tool for internet searching capabilities +tool = SearchApiTool() +``` + +## Parâmetros + +- **engine**: o motor do SearchApi a consultar, como `google`, `google_news`, `google_scholar`, `google_jobs`, `bing`, `youtube` ou `baidu`. O padrão é `google`. Também pode ser passado a cada chamada. +- **n_results**: limite do tamanho de cada lista `*_results` na resposta. O padrão é `10`. +- **country**: opcional. País da pesquisa, enviado como `gl` (por exemplo, `uk`). +- **locale**: opcional. Idioma da interface, enviado como `hl` (por exemplo, `en`). +- **location**: opcional. Localização canônica da pesquisa, por exemplo `London,England`. +- **max_string_length**: maior string mantida intacta na resposta. O padrão é `1000`. +- **timeout**: tempo limite da requisição em segundos. O padrão é `30`. +- **api_key**: opcional. Sua chave do SearchApi. Na ausência dela, usa a variável de ambiente `SEARCHAPI_API_KEY`. +- **search_url**: o endpoint chamado. O padrão é `https://www.searchapi.io/api/v1/search`. + +## Exemplo com parâmetros + +```python Code +from crewai_tools import SearchApiTool + +# Recent news, localized to the UK +news_tool = SearchApiTool( + engine="google_news", + country="uk", + locale="en", + location="London,England", + n_results=5, +) + +# Academic search through the same tool +scholar_tool = SearchApiTool(engine="google_scholar") +``` + +## Erros + +Uma requisição malsucedida levanta um `RuntimeError` com a mensagem do próprio SearchApi, por exemplo `SearchApi request failed (HTTP 401): Invalid API key.`. + +Uma pesquisa bem-sucedida que não encontrou nada não é um erro: o SearchApi retorna HTTP 200 com uma mensagem `error` como `"Google didn't return any results."`, e a ferramenta repassa isso para que o agente possa ler por que a página veio vazia. diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index 2db6be89bc..5eba03badc 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -161,6 +161,7 @@ from crewai_tools.tools.scrapfly_scrape_website_tool.scrapfly_scrape_website_tool import ( ScrapflyScrapeWebsiteTool, ) +from crewai_tools.tools.searchapi_tool.searchapi_tool import SearchApiTool from crewai_tools.tools.selenium_scraping_tool.selenium_scraping_tool import ( SeleniumScrapingTool, ) @@ -308,6 +309,7 @@ "ScrapegraphScrapeTool", "ScrapegraphScrapeToolSchema", "ScrapflyScrapeWebsiteTool", + "SearchApiTool", "SeleniumScrapingTool", "SerpApiGoogleSearchTool", "SerpApiGoogleShoppingTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 2653490f76..8eb0ec6da9 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -149,6 +149,7 @@ from crewai_tools.tools.scrapfly_scrape_website_tool.scrapfly_scrape_website_tool import ( ScrapflyScrapeWebsiteTool, ) +from crewai_tools.tools.searchapi_tool.searchapi_tool import SearchApiTool from crewai_tools.tools.selenium_scraping_tool.selenium_scraping_tool import ( SeleniumScrapingTool, ) @@ -290,6 +291,7 @@ "ScrapegraphScrapeTool", "ScrapegraphScrapeToolSchema", "ScrapflyScrapeWebsiteTool", + "SearchApiTool", "SeleniumScrapingTool", "SerpApiGoogleSearchTool", "SerpApiGoogleShoppingTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md new file mode 100644 index 0000000000..50658a9fa6 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md @@ -0,0 +1,81 @@ +# SearchApi Tool + +## Description + +The `SearchApiTool` searches the internet through [SearchApi](https://www.searchapi.io). SearchApi puts many engines behind one endpoint, so a single tool covers Google web search, news, scholar, jobs, Bing, YouTube, Baidu and the rest of the [supported engines](https://www.searchapi.io/docs) by changing the `engine` argument. The tool returns the engine's own JSON, so results line up with the engine's documentation. + +Two things happen to the response before an agent sees it: + +- Inline `data:` URIs are dropped. SearchApi returns favicons and thumbnails as base64 strings, and a single one can run to tens of kilobytes of context that means nothing to an agent. +- Long strings are truncated at `max_string_length`, and every `*_results` list is capped at `n_results`. + +## Installation + +No extra package is needed: + +```shell +uv add 'crewai[tools]' +``` + +## Environment Variables + +Set your SearchApi key: + +```bash +export SEARCHAPI_API_KEY='your_searchapi_key' +``` + +The key is sent in the `Authorization` header rather than the query string, so it stays out of request logs and out of the `request_url` SearchApi echoes back in `search_metadata`. + +## Example + +```python +from crewai import Agent, Crew, Task +from crewai_tools import SearchApiTool + +search_tool = SearchApiTool() + +researcher = Agent( + role="Market Researcher", + goal="Find what people are saying about a company right now", + backstory="An analyst who checks the record before forming a view.", + tools=[search_tool], + verbose=True, +) + +research_task = Task( + description="Search for recent coverage of CrewAI.", + expected_output="A short report on what the search returned.", + agent=researcher, +) + +crew = Crew(agents=[researcher], tasks=[research_task], verbose=True) +result = crew.kickoff() +print(result) +``` + +Point the same tool at a different engine: + +```python +news_tool = SearchApiTool(engine="google_news") +scholar_tool = SearchApiTool(engine="google_scholar", n_results=5) +uk_tool = SearchApiTool(country="uk", locale="en", location="London,England") +``` + +## Arguments + +- `engine` (str, optional): The SearchApi engine to query, such as `"google"`, `"google_news"`, `"google_scholar"`, `"google_jobs"`, `"bing"`, `"youtube"` or `"baidu"`. Defaults to `"google"`. Can also be passed per call. +- `n_results` (int, optional): Cap on the length of each `*_results` list in the response. Defaults to `10`. +- `country` (str, optional): Country of the search, sent as `gl` (for example `"uk"`). Defaults to `None`. +- `locale` (str, optional): Interface language, sent as `hl` (for example `"en"`). Defaults to `None`. +- `location` (str, optional): Canonical location of the search, for example `"London,England"`. Defaults to `None`. +- `max_string_length` (int, optional): Longest string kept intact in the response. Defaults to `1000`. +- `timeout` (int, optional): Request timeout in seconds. Defaults to `30`. +- `api_key` (str, optional): Your SearchApi key. Falls back to the `SEARCHAPI_API_KEY` environment variable. +- `search_url` (str, optional): The endpoint to call. Defaults to `https://www.searchapi.io/api/v1/search`. + +## Errors + +A failed request raises a `RuntimeError` carrying SearchApi's own message, for example `SearchApi request failed (HTTP 401): Invalid API key.`. + +A successful search that found nothing is not an error: SearchApi returns HTTP 200 with an `error` message such as `"Google didn't return any results."`, and the tool passes that through so the agent can read why the page was empty. diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_tool.py b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_tool.py new file mode 100644 index 0000000000..01acdb78dd --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_tool.py @@ -0,0 +1,188 @@ +"""Search the web through SearchApi (https://www.searchapi.io).""" + +import json +import os +from typing import Any + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field +import requests + + +DEFAULT_SEARCH_URL = "https://www.searchapi.io/api/v1/search" + +_RESULT_LIST_SUFFIX = "_results" + + +def _is_data_uri(value: Any) -> bool: + """Return True for an inline ``data:`` URI, which is never useful to an agent.""" + return isinstance(value, str) and value.startswith("data:") + + +def _sanitize(value: Any, max_string_length: int) -> Any: + """Drop inline data URIs and truncate long strings, recursively. + + SearchApi ships favicons and thumbnails as ``data:image/png;base64,...`` + strings on most result items. They mean nothing to an agent and each one + can run to tens of kilobytes, so they are dropped rather than truncated. + + Args: + value: Any part of a decoded JSON response. + max_string_length: Longest string kept intact; longer ones are cut. + + Returns: + The same structure with data URIs removed and long strings truncated. + """ + if isinstance(value, dict): + return { + key: _sanitize(item, max_string_length) + for key, item in value.items() + if not _is_data_uri(item) + } + if isinstance(value, list): + return [ + _sanitize(item, max_string_length) + for item in value + if not _is_data_uri(item) + ] + if isinstance(value, str) and len(value) > max_string_length: + return value[:max_string_length] + "..." + return value + + +def _error_detail(response: requests.Response) -> str: + """Pull SearchApi's error message out of a failed response. + + Args: + response: A response whose status code is not a success. + + Returns: + The API's own error message, or the start of the raw body when the + response is not the documented ``{"error": ...}`` shape. + """ + try: + body = response.json() + except ValueError: + return response.text[:500] + error = body.get("error") if isinstance(body, dict) else None + if isinstance(error, str): + return error + return json.dumps(body)[:500] + + +class SearchApiToolSchema(BaseModel): + """Input for SearchApiTool.""" + + search_query: str = Field( + ..., description="Mandatory search query you want to use to search the internet" + ) + + +class SearchApiTool(BaseTool): + """Search the internet through SearchApi's unified search endpoint. + + One endpoint fronts many engines, selected with ``engine``, so the same + tool covers web search, news, scholar, jobs and the rest. The response is + the engine's own JSON, so its shape follows the engine's documentation at + https://www.searchapi.io/docs. + """ + + name: str = "Search the internet with SearchApi" + description: str = ( + "A tool that can be used to search the internet with a search_query using " + "SearchApi. One endpoint fronts many engines, set with 'engine': 'google' " + "(default), 'google_news', 'google_scholar', 'google_jobs', 'bing', " + "'youtube', 'baidu' and others. Returns the engine's structured JSON results." + ) + args_schema: type[BaseModel] = SearchApiToolSchema + search_url: str = DEFAULT_SEARCH_URL + engine: str = "google" + n_results: int = 10 + country: str | None = None + locale: str | None = None + location: str | None = None + max_string_length: int = 1000 + timeout: int = 30 + api_key: str | None = None + env_vars: list[EnvVar] = Field( + default_factory=lambda: [ + EnvVar( + name="SEARCHAPI_API_KEY", + description="API key for SearchApi", + required=True, + ), + ] + ) + + def _run(self, **kwargs: Any) -> dict[str, Any]: + """Execute the search operation. + + Args: + **kwargs: ``search_query`` (or ``query``) to search for, and an + optional ``engine`` overriding the configured one for this call. + + Returns: + The engine's JSON response, with data URIs dropped, long strings + truncated and result lists capped at ``n_results``. A response that + carries an ``error`` message instead of results, which is how + SearchApi reports a page with nothing on it, is passed through so + the agent can read why. + + Raises: + ValueError: No search query was given, or no API key is configured. + RuntimeError: SearchApi answered with an error status. + """ + search_query: str | None = kwargs.get("search_query") or kwargs.get("query") + if not search_query: + raise ValueError("search_query is required") + + api_key = self.api_key or os.getenv("SEARCHAPI_API_KEY") + if not api_key: + raise ValueError( + "SEARCHAPI_API_KEY environment variable is required for SearchApiTool" + ) + + params: dict[str, Any] = { + "engine": kwargs.get("engine", self.engine), + "q": search_query, + } + if self.country: + params["gl"] = self.country + if self.locale: + params["hl"] = self.locale + if self.location: + params["location"] = self.location + + # The key travels in the Authorization header rather than the query + # string, so it stays out of request logs and out of the request_url + # SearchApi echoes back in search_metadata. + response = requests.get( + self.search_url, + headers={"Authorization": f"Bearer {api_key}"}, + params=params, + timeout=self.timeout, + ) + if not response.ok: + raise RuntimeError( + f"SearchApi request failed (HTTP {response.status_code}): " + f"{_error_detail(response)}" + ) + + return self._format_results(response.json()) + + def _format_results(self, results: dict[str, Any]) -> dict[str, Any]: + """Cap result lists and strip payload that only burns context. + + Args: + results: The decoded JSON body of a successful search. + + Returns: + The same body with every ``*_results`` list capped at + ``n_results`` and every value sanitized. + """ + formatted: dict[str, Any] = {} + for key, value in results.items(): + if key.endswith(_RESULT_LIST_SUFFIX) and isinstance(value, list): + value = value[: self.n_results] + formatted[key] = _sanitize(value, self.max_string_length) + return formatted diff --git a/lib/crewai-tools/tests/tools/searchapi_tool_test.py b/lib/crewai-tools/tests/tools/searchapi_tool_test.py new file mode 100644 index 0000000000..78a68312a0 --- /dev/null +++ b/lib/crewai-tools/tests/tools/searchapi_tool_test.py @@ -0,0 +1,193 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest +import requests as requests_lib + +from crewai_tools.tools.searchapi_tool.searchapi_tool import SearchApiTool + + +def _mock_response( + status_code: int = 200, + json_data: dict | None = None, + text: str = "", +) -> MagicMock: + """Build a ``requests.Response``-like mock with the attributes ``_run`` uses.""" + resp = MagicMock(spec=requests_lib.Response) + resp.status_code = status_code + resp.ok = 200 <= status_code < 400 + resp.text = text or (str(json_data) if json_data else "") + resp.json.return_value = json_data if json_data is not None else {} + return resp + + +@pytest.fixture(autouse=True) +def _searchapi_env(): + with patch.dict(os.environ, {"SEARCHAPI_API_KEY": "test-api-key"}): + yield + + +@pytest.fixture +def tool(): + return SearchApiTool() + + +def test_default_attributes(tool): + assert tool.search_url == "https://www.searchapi.io/api/v1/search" + assert tool.engine == "google" + assert tool.n_results == 10 + assert tool.env_vars[0].name == "SEARCHAPI_API_KEY" + + +def test_missing_query_raises(tool): + with pytest.raises(ValueError, match="search_query is required"): + tool._run() + + +def test_missing_api_key_raises(): + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="SEARCHAPI_API_KEY"): + SearchApiTool()._run(search_query="test") + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_key_is_sent_as_bearer_header_not_query_param(mock_get, tool): + """The key must stay out of the query string, which SearchApi echoes back.""" + mock_get.return_value = _mock_response(json_data={"organic_results": []}) + + tool._run(search_query="crewai") + + kwargs = mock_get.call_args.kwargs + assert kwargs["headers"]["Authorization"] == "Bearer test-api-key" + assert "api_key" not in kwargs["params"] + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_explicit_api_key_takes_precedence_over_env(mock_get): + mock_get.return_value = _mock_response(json_data={}) + + SearchApiTool(api_key="explicit-key")._run(search_query="crewai") + + assert mock_get.call_args.kwargs["headers"]["Authorization"] == "Bearer explicit-key" + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_localization_params_are_only_sent_when_set(mock_get): + mock_get.return_value = _mock_response(json_data={}) + + SearchApiTool(country="uk", locale="en", location="London,England")._run( + search_query="fish" + ) + + params = mock_get.call_args.kwargs["params"] + assert params == { + "engine": "google", + "q": "fish", + "gl": "uk", + "hl": "en", + "location": "London,England", + } + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_engine_can_be_overridden_per_call(mock_get, tool): + mock_get.return_value = _mock_response(json_data={}) + + tool._run(search_query="crewai", engine="google_news") + + assert mock_get.call_args.kwargs["params"]["engine"] == "google_news" + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_query_alias_is_accepted(mock_get, tool): + mock_get.return_value = _mock_response(json_data={}) + + tool._run(query="crewai") + + assert mock_get.call_args.kwargs["params"]["q"] == "crewai" + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_result_lists_are_capped_at_n_results(mock_get): + mock_get.return_value = _mock_response( + json_data={ + "organic_results": [{"position": i} for i in range(10)], + "related_questions": [{"question": f"q{i}"} for i in range(10)], + } + ) + + result = SearchApiTool(n_results=3)._run(search_query="crewai") + + assert len(result["organic_results"]) == 3 + # Only lists named *_results are capped; everything else passes through. + assert len(result["related_questions"]) == 10 + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_data_uris_are_dropped_from_results(mock_get, tool): + mock_get.return_value = _mock_response( + json_data={ + "organic_results": [ + { + "title": "CrewAI", + "link": "https://crewai.com", + "favicon": "data:image/png;base64," + "A" * 20000, + } + ], + "inline_images": ["data:image/png;base64,AAAA", "https://img.co/a.png"], + } + ) + + result = tool._run(search_query="crewai") + + assert result["organic_results"][0] == { + "title": "CrewAI", + "link": "https://crewai.com", + } + assert result["inline_images"] == ["https://img.co/a.png"] + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_long_strings_are_truncated(mock_get): + mock_get.return_value = _mock_response( + json_data={"organic_results": [{"snippet": "x" * 500}]} + ) + + result = SearchApiTool(max_string_length=100)._run(search_query="crewai") + + assert result["organic_results"][0]["snippet"] == "x" * 100 + "..." + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_empty_result_page_is_returned_not_raised(mock_get, tool): + """A 200 carrying `error` is SearchApi reporting no results, not a failure.""" + mock_get.return_value = _mock_response( + json_data={ + "search_metadata": {"id": "search_1", "status": "Success"}, + "error": "Google didn't return any results.", + } + ) + + result = tool._run(search_query="asdkjhaskdjh") + + assert result["error"] == "Google didn't return any results." + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_error_status_raises_with_api_message(mock_get, tool): + mock_get.return_value = _mock_response( + status_code=401, json_data={"error": "Invalid API key."} + ) + + with pytest.raises(RuntimeError, match="HTTP 401.*Invalid API key."): + tool._run(search_query="crewai") + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get") +def test_non_json_error_body_falls_back_to_text(mock_get, tool): + resp = _mock_response(status_code=502, text="Bad gateway") + resp.json.side_effect = ValueError("not json") + mock_get.return_value = resp + + with pytest.raises(RuntimeError, match="HTTP 502.*Bad gateway"): + tool._run(search_query="crewai") diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index 2e540f8c13..4fea757151 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -21130,6 +21130,163 @@ "type": "object" } }, + { + "description": "A tool that can be used to search the internet with a search_query using SearchApi. One endpoint fronts many engines, set with 'engine': 'google' (default), 'google_news', 'google_scholar', 'google_jobs', 'bing', 'youtube', 'baidu' and others. Returns the engine's structured JSON results.", + "env_vars": [ + { + "default": null, + "description": "API key for SearchApi", + "name": "SEARCHAPI_API_KEY", + "required": true + } + ], + "humanized_name": "Search the internet with SearchApi", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + }, + "ToolFailurePolicy": { + "description": "How an agent reacts when one of its tools reports a failure.", + "enum": [ + "ignore", + "warn", + "raise" + ], + "title": "ToolFailurePolicy", + "type": "string" + } + }, + "description": "Search the internet through SearchApi's unified search endpoint.\n\nOne endpoint fronts many engines, selected with ``engine``, so the same\ntool covers web search, news, scholar, jobs and the rest. The response is\nthe engine's own JSON, so its shape follows the engine's documentation at\nhttps://www.searchapi.io/docs.", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "country": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Country" + }, + "engine": { + "default": "google", + "title": "Engine", + "type": "string" + }, + "locale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Locale" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Location" + }, + "max_string_length": { + "default": 1000, + "title": "Max String Length", + "type": "integer" + }, + "n_results": { + "default": 10, + "title": "N Results", + "type": "integer" + }, + "search_url": { + "default": "https://www.searchapi.io/api/v1/search", + "title": "Search Url", + "type": "string" + }, + "timeout": { + "default": 30, + "title": "Timeout", + "type": "integer" + } + }, + "required": [], + "title": "SearchApiTool", + "type": "object" + }, + "name": "SearchApiTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for SearchApiTool.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to search the internet", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SearchApiToolSchema", + "type": "object" + } + }, { "description": "A tool that can be used to read a website content.", "env_vars": [],