From 7ab7a06c4b66718f72a789855ef185911d100dd2 Mon Sep 17 00:00:00 2001 From: workforcloudy-glitch Date: Wed, 8 Jul 2026 19:42:08 +0800 Subject: [PATCH] feat(tools): add querit search tool --- docs/docs.json | 1 + .../search-research/queritsearchtool.mdx | 98 ++++++ lib/crewai-tools/src/crewai_tools/__init__.py | 2 + .../src/crewai_tools/tools/__init__.py | 2 + .../tools/querit_search_tool/README.md | 95 ++++++ .../tools/querit_search_tool/__init__.py | 6 + .../querit_search_tool/querit_search_tool.py | 211 ++++++++++++ .../tests/tools/querit_search_tool_test.py | 267 +++++++++++++++ lib/crewai-tools/tool.specs.json | 304 ++++++++++++++++++ 9 files changed, 986 insertions(+) create mode 100644 docs/edge/en/tools/search-research/queritsearchtool.mdx create mode 100644 lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/README.md create mode 100644 lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/__init__.py create mode 100644 lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/querit_search_tool.py create mode 100644 lib/crewai-tools/tests/tools/querit_search_tool_test.py diff --git a/docs/docs.json b/docs/docs.json index f6d122bb3a..ee581952bc 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -244,6 +244,7 @@ "edge/en/tools/search-research/bravesearchtool", "edge/en/tools/search-research/exasearchtool", "edge/en/tools/search-research/linkupsearchtool", + "edge/en/tools/search-research/queritsearchtool", "edge/en/tools/search-research/githubsearchtool", "edge/en/tools/search-research/websitesearchtool", "edge/en/tools/search-research/codedocssearchtool", diff --git a/docs/edge/en/tools/search-research/queritsearchtool.mdx b/docs/edge/en/tools/search-research/queritsearchtool.mdx new file mode 100644 index 0000000000..dff253cdff --- /dev/null +++ b/docs/edge/en/tools/search-research/queritsearchtool.mdx @@ -0,0 +1,98 @@ +--- +title: "Querit Search Tool" +description: "Perform real-time web searches using the Querit Search API" +icon: "magnifying-glass" +mode: "wide" +--- + +The `QueritSearchTool` provides an interface to the Querit Search API, enabling CrewAI agents to retrieve real-time web results from Querit's search infrastructure for LLM applications. + +## Installation + +```shell +uv add 'crewai[tools]' +``` + +## Environment Variables + +Set your Querit API key as an environment variable: + +```bash +export QUERIT_API_KEY='your_querit_api_key' +``` + +You can create and manage API keys from the Querit dashboard: [querit.ai/en/dashboard/api-keys](https://www.querit.ai/en/dashboard/api-keys). + +## Example Usage + +```python +from crewai import Agent, Crew, Task +from crewai_tools import QueritSearchTool + +querit_tool = QueritSearchTool(count=10) + +researcher = Agent( + role="Researcher", + goal="Find current information from the web", + backstory="An expert researcher who verifies information with live sources.", + tools=[querit_tool], +) + +research_task = Task( + description="Search for recent developments in large language models.", + expected_output="A concise summary with cited sources.", + agent=researcher, +) + +crew = Crew(agents=[researcher], tasks=[research_task]) +result = crew.kickoff() +print(result) +``` + +## Direct Usage + +```python +from crewai_tools import QueritSearchTool + +tool = QueritSearchTool(count=10) +result = tool.run(query="What is the latest progress on LLMs?") +print(result) +``` + +## Configuration Options + +The `QueritSearchTool` accepts the following arguments: + +- `query` (str): Required. The search query string. +- `count` (int): Maximum number of search results to return. Defaults to `10`. +- `chunksPerDoc` (int): Number of summary chunks to return per document. Defaults to `3` and supports values from `1` to `3`. +- `site_include` (list[str]): Optional websites to include in the search results. +- `site_exclude` (list[str]): Optional websites to exclude from the search results. +- `time_range` (str): Optional time range value passed to Querit as `timeRange.date`. Use `d[number]`, `w[number]`, `m[number]`, `y[number]`, or `YYYY-MM-DDtoYYYY-MM-DD`. +- `country_include` (list[str]): Optional countries to include in the search results. +- `language_include` (list[str]): Optional languages to include in the search results. +- `timeout` (int): Request timeout in seconds. Defaults to `30`. +- `api_key` (str): Optional Querit API key. If omitted, `QUERIT_API_KEY` is used. +- `search_url` (str): Optional Querit API endpoint. Defaults to `https://api.querit.ai/v1/search`. + +## Filter Parameters Example + +Use the flat filter parameters for common Querit filters. Country and language values are passed through to Querit without restricting their allowed values in this tool. `time_range` must use a Querit time range format such as `d7`, `w1`, `m3`, `y1`, or `2022-04-01to2022-07-30`. + +```python +from crewai_tools import QueritSearchTool + +tool = QueritSearchTool( + count=10, + chunksPerDoc=3, + site_include=["example.com"], + site_exclude=["archive.example.com"], + time_range="w1", + country_include=["united states"], + language_include=["english"], +) +``` + +## Response Format + +The tool returns the original Querit API JSON response. diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index a7c7ba9678..9e46c3de68 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -143,6 +143,7 @@ from crewai_tools.tools.qdrant_vector_search_tool.qdrant_search_tool import ( QdrantVectorSearchTool, ) +from crewai_tools.tools.querit_search_tool.querit_search_tool import QueritSearchTool from crewai_tools.tools.rag.rag_tool import RagTool from crewai_tools.tools.scrape_element_from_website.scrape_element_from_website import ( ScrapeElementFromWebsiteTool, @@ -292,6 +293,7 @@ "PatronusLocalEvaluatorTool", "PatronusPredefinedCriteriaEvalTool", "QdrantVectorSearchTool", + "QueritSearchTool", "RagTool", "S3ReaderTool", "S3WriterTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 18bf4e5638..0e8c9c9248 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -131,6 +131,7 @@ from crewai_tools.tools.qdrant_vector_search_tool.qdrant_search_tool import ( QdrantVectorSearchTool, ) +from crewai_tools.tools.querit_search_tool.querit_search_tool import QueritSearchTool from crewai_tools.tools.rag.rag_tool import RagTool from crewai_tools.tools.scrape_element_from_website.scrape_element_from_website import ( ScrapeElementFromWebsiteTool, @@ -276,6 +277,7 @@ "PatronusLocalEvaluatorTool", "PatronusPredefinedCriteriaEvalTool", "QdrantVectorSearchTool", + "QueritSearchTool", "RagTool", "ScrapeElementFromWebsiteTool", "ScrapeWebsiteTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/README.md new file mode 100644 index 0000000000..fec071acc6 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/README.md @@ -0,0 +1,95 @@ +# Querit Search Tool + +## Description + +The `QueritSearchTool` provides an interface to the Querit Search API, enabling CrewAI agents to perform real-time web searches through Querit's API for LLM applications. It supports result limits and Querit filters for sites, time ranges, countries, and languages. + +## Installation + +```shell +uv add 'crewai[tools]' +``` + +## Environment Variables + +Set your Querit API key as an environment variable: + +```bash +export QUERIT_API_KEY='your_querit_api_key' +``` + +You can create or manage API keys from the Querit dashboard: + +```text +https://www.querit.ai/en/dashboard/api-keys +``` + +## Example + +```python +from crewai import Agent, Crew, Task +from crewai_tools import QueritSearchTool + +querit_tool = QueritSearchTool(count=10) + +researcher = Agent( + role="Researcher", + goal="Find current information from the web", + backstory="An expert researcher who verifies information with live sources.", + tools=[querit_tool], +) + +research_task = Task( + description="Search for recent developments in large language models.", + expected_output="A concise summary with cited sources.", + agent=researcher, +) + +crew = Crew(agents=[researcher], tasks=[research_task]) +result = crew.kickoff() +print(result) +``` + +## Direct Usage + +```python +from crewai_tools import QueritSearchTool + +tool = QueritSearchTool(count=10) +result = tool.run(query="What is the latest progress on LLMs?") +print(result) +``` + +## Arguments + +- `query` (str): Required. The search query string. +- `count` (int): Maximum number of search results to return. Defaults to `10`. +- `chunksPerDoc` (int): Number of summary chunks to return per document. Defaults to `3` and supports values from `1` to `3`. +- `site_include` (list[str]): Optional websites to include in the search results. +- `site_exclude` (list[str]): Optional websites to exclude from the search results. +- `time_range` (str): Optional time range value passed to Querit as `timeRange.date`. Use `d[number]`, `w[number]`, `m[number]`, `y[number]`, or `YYYY-MM-DDtoYYYY-MM-DD`. +- `country_include` (list[str]): Optional countries to include in the search results. +- `language_include` (list[str]): Optional languages to include in the search results. +- `timeout` (int): Request timeout in seconds. Defaults to `30`. +- `api_key` (str): Optional Querit API key. If omitted, `QUERIT_API_KEY` is used. +- `search_url` (str): Optional Querit API endpoint. Defaults to `https://api.querit.ai/v1/search`. + +## Filter Parameters Example + +Use the flat filter parameters for common Querit filters. Country and language values are passed through to Querit without restricting their allowed values in this tool. `time_range` must use a Querit time range format such as `d7`, `w1`, `m3`, `y1`, or `2022-04-01to2022-07-30`. + +```python +tool = QueritSearchTool( + count=10, + chunksPerDoc=3, + site_include=["example.com"], + site_exclude=["archive.example.com"], + time_range="w1", + country_include=["united states"], + language_include=["english"], +) +``` + +## Response Format + +The tool returns the original Querit API JSON response. diff --git a/lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/__init__.py new file mode 100644 index 0000000000..3242ed2c22 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/__init__.py @@ -0,0 +1,6 @@ +"""Public exports for the Querit search tool package.""" + +from crewai_tools.tools.querit_search_tool.querit_search_tool import QueritSearchTool + + +__all__ = ["QueritSearchTool"] diff --git a/lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/querit_search_tool.py b/lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/querit_search_tool.py new file mode 100644 index 0000000000..721406eb86 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/querit_search_tool/querit_search_tool.py @@ -0,0 +1,211 @@ +"""Tool for searching the web with the Querit Search API.""" + +import os +from typing import Any, TypeAlias + +from crewai.tools import BaseTool, EnvVar +from dotenv import load_dotenv +from pydantic import BaseModel, ConfigDict, Field +import requests + + +load_dotenv() + +QueritSearchResponse: TypeAlias = dict[str, Any] +QueritFilters: TypeAlias = dict[str, Any] +TIME_RANGE_PATTERN = r"^([dwmy][1-9][0-9]*|\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2})$" + + +class QueritSearchToolSchema(BaseModel): + """Input for QueritSearchTool.""" + + model_config = ConfigDict(populate_by_name=True) + + query: str = Field(..., description="The search query string.") + count: int | None = Field( + default=None, ge=1, description="The maximum number of results to return." + ) + chunks_per_doc: int | None = Field( + default=None, + ge=1, + le=3, + alias="chunksPerDoc", + description="The number of summary chunks to return per document.", + ) + site_include: list[str] | None = Field( + default=None, + description="Websites to include in the search results.", + ) + site_exclude: list[str] | None = Field( + default=None, + description="Websites to exclude from the search results.", + ) + time_range: str | None = Field( + default=None, + pattern=TIME_RANGE_PATTERN, + description="Time range filter, such as d7, w1, m3, y1, or a date range.", + ) + country_include: list[str] | None = Field( + default=None, + description="Countries to include in the search results.", + ) + language_include: list[str] | None = Field( + default=None, + description="Languages to include in the search results.", + ) + + +class QueritSearchTool(BaseTool): + """Tool that searches the web using the Querit Search API.""" + + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + name: str = "Querit Search" + description: str = ( + "A tool that performs web searches using the Querit Search API. " + "It returns the original Querit API JSON response." + ) + args_schema: type[BaseModel] = QueritSearchToolSchema + search_url: str = "https://api.querit.ai/v1/search" + api_key: str | None = Field( + default_factory=lambda: os.getenv("QUERIT_API_KEY"), + description="The Querit API key. If not provided, it will be loaded from QUERIT_API_KEY.", + ) + count: int = Field( + default=10, ge=1, description="The maximum number of results to return." + ) + chunks_per_doc: int | None = Field( + default=3, + ge=1, + le=3, + alias="chunksPerDoc", + description="The number of summary chunks to return per document.", + ) + timeout: int = Field(default=30, description="The request timeout in seconds.") + site_include: list[str] | None = Field( + default=None, + description="Websites to include in the search results.", + ) + site_exclude: list[str] | None = Field( + default=None, + description="Websites to exclude from the search results.", + ) + time_range: str | None = Field( + default=None, + pattern=TIME_RANGE_PATTERN, + description="Time range filter, such as d7, w1, m3, y1, or a date range.", + ) + country_include: list[str] | None = Field( + default=None, + description="Countries to include in the search results.", + ) + language_include: list[str] | None = Field( + default=None, + description="Languages to include in the search results.", + ) + env_vars: list[EnvVar] = Field( + default_factory=lambda: [ + EnvVar( + name="QUERIT_API_KEY", + description="API key for the Querit search service", + required=True, + ), + ] + ) + + def __init__(self, **kwargs: Any) -> None: + """Initialize the tool and ensure the Querit API key is configured.""" + super().__init__(**kwargs) + if not self.api_key: + raise ValueError( + "QUERIT_API_KEY environment variable is required for QueritSearchTool" + ) + + def _run(self, query: str, **kwargs: Any) -> QueritSearchResponse: + """Execute a Querit web search. + + Args: + query: Search query string. + **kwargs: Optional runtime overrides for search parameters. + + Returns: + The raw Querit API JSON response. + """ + count = kwargs.get("count") or self.count + chunks_per_doc = kwargs.get("chunks_per_doc") or kwargs.get("chunksPerDoc") + if chunks_per_doc is None: + chunks_per_doc = self.chunks_per_doc + payload: QueritSearchResponse = {"query": query, "count": count} + if chunks_per_doc is not None: + payload["chunksPerDoc"] = chunks_per_doc + + filters = self._build_filters(kwargs) + if filters: + payload["filters"] = filters + + response: requests.Response | None = None + last_error: requests.RequestException | None = None + for _ in range(3): + try: + response = requests.post( + self.search_url, + headers={ + "Accept": "application/json", + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + }, + json=payload, + timeout=self.timeout, + ) + response.raise_for_status() + break + except requests.HTTPError as error: + last_error = error + status_code = error.response.status_code if error.response else None + if status_code not in {429, 500, 502, 503, 504}: + raise + except requests.RequestException as error: + last_error = error + if response is None: + if last_error is not None: + raise last_error + raise RuntimeError("Querit request failed without an exception") + search_response = response.json() + if not isinstance(search_response, dict): + raise ValueError("Querit API response must be a JSON object") + + return dict(search_response) + + def _build_filters(self, kwargs: dict[str, Any]) -> QueritFilters: + """Build the Querit API filters payload from flat tool parameters. + + Args: + kwargs: Runtime parameter overrides passed to the tool. + + Returns: + A Querit API filters mapping, or an empty mapping when no filters are set. + """ + filters: QueritFilters = {} + site_include = kwargs.get("site_include") or self.site_include + site_exclude = kwargs.get("site_exclude") or self.site_exclude + time_range = kwargs.get("time_range") or self.time_range + country_include = kwargs.get("country_include") or self.country_include + language_include = kwargs.get("language_include") or self.language_include + + if site_include is not None or site_exclude is not None: + filters["sites"] = { + key: value + for key, value in { + "include": site_include, + "exclude": site_exclude, + }.items() + if value is not None + } + if time_range is not None: + filters["timeRange"] = {"date": time_range} + if country_include is not None: + filters["geo"] = {"countries": {"include": country_include}} + if language_include is not None: + filters["languages"] = {"include": language_include} + + return filters diff --git a/lib/crewai-tools/tests/tools/querit_search_tool_test.py b/lib/crewai-tools/tests/tools/querit_search_tool_test.py new file mode 100644 index 0000000000..486fbcc343 --- /dev/null +++ b/lib/crewai-tools/tests/tools/querit_search_tool_test.py @@ -0,0 +1,267 @@ +"""Tests for the Querit search tool.""" + +from collections.abc import Generator +import os +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from crewai_tools.tools.querit_search_tool import QueritSearchTool + + +@pytest.fixture(autouse=True) +def querit_api_key() -> Generator[None, None, None]: + """Provide a Querit API key for tests that instantiate the tool.""" + with patch.dict(os.environ, {"QUERIT_API_KEY": "test-api-key"}): + yield + + +def _mock_response(json_data: Any) -> MagicMock: + """Create a mocked requests response returning the given JSON payload.""" + response = MagicMock() + response.json.return_value = json_data + response.raise_for_status.return_value = None + return response + + +def _mock_http_error_response(status_code: int) -> MagicMock: + """Create a mocked requests response that raises an HTTP status error.""" + response = MagicMock() + response.status_code = status_code + error = requests.HTTPError(f"{status_code} error") + error.response = response + response.raise_for_status.side_effect = error + return response + + +def test_missing_api_key_raises() -> None: + """Verify that the tool requires a Querit API key.""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="QUERIT_API_KEY"): + QueritSearchTool() + + +def test_querit_search_tool_posts_expected_payload() -> None: + """Verify that the tool posts the expected request and returns raw results.""" + tool = QueritSearchTool(count=2, chunksPerDoc=None) + response_data = { + "took": "300ms", + "error_code": 200, + "error_msg": "", + "search_id": 11099848653006015581, + "query_context": {"query": "expanded query", "count": 99}, + "results": { + "result": [ + { + "url": "https://example.com/llm", + "title": "LLM progress", + "snippet": "Recent LLM progress summary", + "site_name": "example.com", + "site_icon": "https://example.com/favicon.ico", + "page_age": "2025-07-20T16:00:00Z", + } + ] + }, + } + + with patch( + "crewai_tools.tools.querit_search_tool.querit_search_tool.requests.post", + return_value=_mock_response(response_data), + ) as post: + result = tool.run(query="latest LLM progress") + + post.assert_called_once_with( + "https://api.querit.ai/v1/search", + headers={ + "Accept": "application/json", + "Authorization": "Bearer test-api-key", + "Content-Type": "application/json", + }, + json={"query": "latest LLM progress", "count": 2}, + timeout=30, + ) + assert result == response_data + + +def test_querit_search_tool_uses_default_count_and_chunks_per_doc() -> None: + """Verify that default result count and chunks per document are sent.""" + tool = QueritSearchTool() + + with patch( + "crewai_tools.tools.querit_search_tool.querit_search_tool.requests.post", + return_value=_mock_response({"results": {"result": []}}), + ) as post: + tool.run(query="AI news") + + assert post.call_args.kwargs["json"] == { + "query": "AI news", + "count": 10, + "chunksPerDoc": 3, + } + + +def test_querit_search_tool_accepts_runtime_count() -> None: + """Verify that runtime count overrides the tool default.""" + tool = QueritSearchTool(count=5, chunksPerDoc=None) + + response_data = {"results": {"result": []}} + with patch( + "crewai_tools.tools.querit_search_tool.querit_search_tool.requests.post", + return_value=_mock_response(response_data), + ) as post: + result = tool.run(query="AI news", count=1) + + assert post.call_args.kwargs["json"] == { + "query": "AI news", + "count": 1, + } + assert result == response_data + + +def test_querit_search_tool_posts_flat_filter_parameters() -> None: + """Verify that flat filter parameters are mapped to Querit filters.""" + tool = QueritSearchTool( + count=5, + chunksPerDoc=1, + site_include=["example.com"], + site_exclude=["archive.example.com"], + time_range="w1", + country_include=["US", "CA"], + language_include=["en", "fr"], + ) + + with patch( + "crewai_tools.tools.querit_search_tool.querit_search_tool.requests.post", + return_value=_mock_response({"results": {"result": []}}), + ) as post: + tool.run(query="AI search news") + + assert post.call_args.kwargs["json"] == { + "query": "AI search news", + "count": 5, + "chunksPerDoc": 1, + "filters": { + "sites": { + "include": ["example.com"], + "exclude": ["archive.example.com"], + }, + "timeRange": {"date": "w1"}, + "geo": {"countries": {"include": ["US", "CA"]}}, + "languages": {"include": ["en", "fr"]}, + }, + } + + +def test_querit_search_tool_serializes_time_range_filter() -> None: + """Verify that time range input is serialized into the Querit filter shape.""" + tool = QueritSearchTool(count=5, chunksPerDoc=None) + + with patch( + "crewai_tools.tools.querit_search_tool.querit_search_tool.requests.post", + return_value=_mock_response({"results": {"result": []}}), + ) as post: + tool.run(query="AI news", time_range="2026-01-01to2026-01-31") + + assert post.call_args.kwargs["json"] == { + "query": "AI news", + "count": 5, + "filters": {"timeRange": {"date": "2026-01-01to2026-01-31"}}, + } + + +def test_querit_search_tool_rejects_invalid_time_range_format() -> None: + """Verify that unsupported time range values fail validation.""" + tool = QueritSearchTool() + + with pytest.raises(ValueError, match="String should match pattern"): + tool.run(query="AI news", time_range="past_week") + + +def test_querit_search_tool_retries_request_exceptions_three_times() -> None: + """Verify that transient request exceptions are retried up to three times.""" + tool = QueritSearchTool() + response_data = {"results": {"result": []}} + response = _mock_response(response_data) + + with patch( + "crewai_tools.tools.querit_search_tool.querit_search_tool.requests.post", + side_effect=[ + requests.ConnectTimeout("first timeout"), + requests.ConnectTimeout("second timeout"), + response, + ], + ) as post: + result = tool.run(query="AI news") + + assert post.call_count == 3 + assert result == response_data + + +def test_querit_search_tool_retries_transient_http_errors_three_times() -> None: + """Verify that transient HTTP status errors are retried up to three times.""" + tool = QueritSearchTool() + response_data = {"results": {"result": []}} + response = _mock_response(response_data) + + with patch( + "crewai_tools.tools.querit_search_tool.querit_search_tool.requests.post", + side_effect=[ + _mock_http_error_response(429), + _mock_http_error_response(500), + response, + ], + ) as post: + result = tool.run(query="AI news") + + assert post.call_count == 3 + assert result == response_data + + +def test_querit_search_tool_does_not_retry_auth_http_errors() -> None: + """Verify that non-transient HTTP status errors are not retried.""" + tool = QueritSearchTool() + + with patch( + "crewai_tools.tools.querit_search_tool.querit_search_tool.requests.post", + return_value=_mock_http_error_response(401), + ) as post: + with pytest.raises(requests.HTTPError): + tool.run(query="AI news") + + post.assert_called_once() + + +def test_querit_search_tool_rejects_invalid_chunks_per_doc() -> None: + """Verify that chunks per document cannot exceed the Querit limit.""" + tool = QueritSearchTool() + + with pytest.raises(ValueError, match="less than or equal to 3"): + tool.run(query="AI news", chunksPerDoc=4) + + +def test_querit_search_tool_returns_raw_response() -> None: + """Verify that the tool returns the original Querit API response.""" + raw_response = {"results": {"result": {"url": "https://example.com"}}} + tool = QueritSearchTool() + + with patch( + "crewai_tools.tools.querit_search_tool.querit_search_tool.requests.post", + return_value=_mock_response(raw_response), + ): + result = tool.run(query="AI news") + + assert result == raw_response + + +def test_querit_search_tool_rejects_non_object_response() -> None: + """Verify that non-object Querit API responses are rejected.""" + tool = QueritSearchTool() + + with patch( + "crewai_tools.tools.querit_search_tool.querit_search_tool.requests.post", + return_value=_mock_response(["unexpected"]), + ): + with pytest.raises(ValueError, match="Querit API response"): + tool.run(query="AI news") diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index 795fa932c4..4785b2cf06 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -18629,6 +18629,310 @@ "type": "object" } }, + { + "description": "A tool that performs web searches using the Querit Search API. It returns the original Querit API JSON response.", + "env_vars": [ + { + "default": null, + "description": "API key for the Querit search service", + "name": "QUERIT_API_KEY", + "required": true + } + ], + "humanized_name": "Querit Search", + "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" + } + }, + "description": "Tool that searches the web using the Querit Search API.", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The Querit API key. If not provided, it will be loaded from QUERIT_API_KEY.", + "title": "Api Key" + }, + "chunksPerDoc": { + "anyOf": [ + { + "maximum": 3, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 3, + "description": "The number of summary chunks to return per document.", + "title": "Chunksperdoc" + }, + "count": { + "default": 10, + "description": "The maximum number of results to return.", + "minimum": 1, + "title": "Count", + "type": "integer" + }, + "country_include": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Countries to include in the search results.", + "title": "Country Include" + }, + "language_include": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Languages to include in the search results.", + "title": "Language Include" + }, + "search_url": { + "default": "https://api.querit.ai/v1/search", + "title": "Search Url", + "type": "string" + }, + "site_exclude": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Websites to exclude from the search results.", + "title": "Site Exclude" + }, + "site_include": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Websites to include in the search results.", + "title": "Site Include" + }, + "time_range": { + "anyOf": [ + { + "pattern": "^([dwmy][1-9][0-9]*|\\d{4}-\\d{2}-\\d{2}to\\d{4}-\\d{2}-\\d{2})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Time range filter, such as d7, w1, m3, y1, or a date range.", + "title": "Time Range" + }, + "timeout": { + "default": 30, + "description": "The request timeout in seconds.", + "title": "Timeout", + "type": "integer" + } + }, + "required": [], + "title": "QueritSearchTool", + "type": "object" + }, + "name": "QueritSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for QueritSearchTool.", + "properties": { + "chunksPerDoc": { + "anyOf": [ + { + "maximum": 3, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The number of summary chunks to return per document.", + "title": "Chunksperdoc" + }, + "count": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The maximum number of results to return.", + "title": "Count" + }, + "country_include": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Countries to include in the search results.", + "title": "Country Include" + }, + "language_include": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Languages to include in the search results.", + "title": "Language Include" + }, + "query": { + "description": "The search query string.", + "title": "Query", + "type": "string" + }, + "site_exclude": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Websites to exclude from the search results.", + "title": "Site Exclude" + }, + "site_include": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Websites to include in the search results.", + "title": "Site Include" + }, + "time_range": { + "anyOf": [ + { + "pattern": "^([dwmy][1-9][0-9]*|\\d{4}-\\d{2}-\\d{2}to\\d{4}-\\d{2}-\\d{2})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Time range filter, such as d7, w1, m3, y1, or a date range.", + "title": "Time Range" + } + }, + "required": [ + "query" + ], + "title": "QueritSearchToolSchema", + "type": "object" + } + }, { "description": "A knowledge base that can be used to answer questions.", "env_vars": [],