From 8322291e81168724eb0a7ce41ff46e9b81cd1edd Mon Sep 17 00:00:00 2001 From: rbatista191 Date: Sun, 7 Dec 2025 10:16:52 +0100 Subject: [PATCH 1/7] feat: added cloro API as new tool --- .../src/crewai_tools/tools/__init__.py | 2 + .../tools/cloro_dev_tool/README.md | 67 ++++++ .../tools/cloro_dev_tool/__init__.py | 0 .../tools/cloro_dev_tool/cloro_dev_tool.py | 219 ++++++++++++++++++ .../tests/tools/cloro_dev_tool_test.py | 182 +++++++++++++++ 5 files changed, 470 insertions(+) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md create mode 100644 lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/__init__.py create mode 100644 lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py create mode 100644 lib/crewai-tools/tests/tools/cloro_dev_tool_test.py diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 51d32ddc25..621b10fd64 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -10,6 +10,7 @@ from crewai_tools.tools.browserbase_load_tool.browserbase_load_tool import ( BrowserbaseLoadTool, ) +from crewai_tools.tools.cloro_dev_tool.cloro_dev_tool import CloroDevTool from crewai_tools.tools.code_docs_search_tool.code_docs_search_tool import ( CodeDocsSearchTool, ) @@ -190,6 +191,7 @@ "BrightDataSearchTool", "BrightDataWebUnlockerTool", "BrowserbaseLoadTool", + "CloroDevTool", "CSVSearchTool", "CodeDocsSearchTool", "CodeInterpreterTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md new file mode 100644 index 0000000000..1b2c8672fc --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md @@ -0,0 +1,67 @@ +# CloroDevTool + +Use the `CloroDevTool` to search the web or query AI models using the cloro API. + +## Installation + +```shell +pip install 'crewai[tools]' +``` + +## Example + +```python +from crewai_tools import CloroDevTool + +# make sure CLORO_API_KEY variable is set +tool = CloroDevTool() + +result = tool.run(search_query="latest news about AI agents") + +print(result) +``` + +## Arguments + +- `api_key` (str, optional): cloro API key. +- `engine` (str, optional): The engine to use for the query. Options are `google`, `chatgpt`, `gemini`, `copilot`, `perplexity`, `aimode`. Defaults to `google`. +- `country` (str, optional): The ISO 3166-1 alpha-2 country code for localized results (e.g., "US", "BR"). For a full list of supported country codes, refer to the [cloro API /v1/countries endpoint](https://docs.cloro.dev/api-reference/endpoint/countries). Defaults to "US". +- `device` (str, optional): The device type for Google search results (`desktop` or `mobile`). Defaults to "desktop". +- `pages` (int, optional): The number of pages to retrieve for Google search results. Defaults to 1. +- `save_file` (bool, optional): Whether to save the search results to a file. Defaults to `False`. + +Get the credentials by creating a [cloro account](https://dashboard.cloro.dev). + +## Response Format + +The tool returns a structured dictionary containing different fields depending on the selected engine. + +### Google Engine +- `organic`: List of organic search results with title, link, snippet, etc. +- `peopleAlsoAsk`: List of related questions. +- `relatedSearches`: List of related search queries. +- `knowledgeGraph`: Knowledge graph data (if available). +- `ai_overview`: Google AI Overview data (if available). + +### LLM Engines (ChatGPT, Gemini, etc.) +- `text`: The main response text from the model. +- `sources`: List of sources cited by the model (if available). + +## Advanced example + +Check out the cloro [documentation](https://docs.cloro.dev/api-reference/introduction) to get the full list of parameters. + +```python +from crewai_tools import CloroDevTool + +# make sure CLORO_API_KEY variable is set +tool = CloroDevTool( + engine="chatgpt", + country="BR", + save_file=True +) + +result = tool.run(search_query="Say 'Hello, Brazil!'") + +print(result) +``` \ No newline at end of file diff --git a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py new file mode 100644 index 0000000000..3a4b0a642d --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py @@ -0,0 +1,219 @@ +import datetime +import json +import logging +import os +from typing import Any, Literal, TypedDict + +import requests +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + + +class OrganicResult(TypedDict, total=False): + """Organic search result data.""" + + title: str + link: str + snippet: str + position: int | None + date: str + sitelinks: list[dict[str, Any]] + + +class PeopleAlsoAskResult(TypedDict): + """People Also Ask result data.""" + + question: str + snippet: str + title: str + link: str + + +class RelatedSearchResult(TypedDict): + """Related search result data.""" + + query: str + link: str + + +class KnowledgeGraph(TypedDict, total=False): + """Knowledge graph data.""" + + title: str + type: str + description: str + source: dict[str, Any] + attributes: dict[str, Any] + + +class FormattedResults(TypedDict, total=False): + """Formatted search results from Cloro API.""" + + organic: list[OrganicResult] + peopleAlsoAsk: list[PeopleAlsoAskResult] + relatedSearches: list[RelatedSearchResult] + knowledgeGraph: KnowledgeGraph + ai_overview: dict[str, Any] + text: str # For LLM responses + sources: list[dict[str, Any]] # For LLM sources + credits: int + + +def _save_results_to_file(content: str) -> None: + """Saves the search results to a file.""" + try: + filename = f"cloro_results_{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.json" + with open(filename, "w") as file: + file.write(content) + logger.info(f"Results saved to {filename}") + except IOError as e: + logger.error(f"Failed to save results to file: {e}") + raise + + +class CloroDevToolSchema(BaseModel): + """Input for CloroDevTool.""" + + search_query: str = Field( + ..., description="Mandatory query/prompt you want to use to search/query the model" + ) + + +class CloroDevTool(BaseTool): + name: str = "Search/Query with Cloro" + description: str = ( + "A tool that can be used to search the internet or query LLMs using cloro API. " + "Supports engines: google, chatgpt, gemini, copilot, perplexity, aimode." + ) + args_schema: type[BaseModel] = CloroDevToolSchema + base_url: str = "https://api.cloro.dev/v1/monitor" + engine: Literal[ + "google", + "chatgpt", + "gemini", + "copilot", + "perplexity", + "aimode", + ] = "google" + country: str = "US" + device: str = "desktop" + pages: int = 1 + api_key: str | None = Field(None, description="cloro API key") + env_vars: list[EnvVar] = Field( + default_factory=lambda: [ + EnvVar( + name="CLORO_API_KEY", description="API key for cloro", required=True + ), + ] + ) + + def __init__(self, api_key: str | None = None, **kwargs): + super().__init__(**kwargs) + if api_key: + self.api_key = api_key + + # Validation + if not self.api_key and not os.environ.get("CLORO_API_KEY"): + pass + + def _get_api_key(self) -> str: + if self.api_key: + return self.api_key + env_key = os.environ.get("CLORO_API_KEY") + if env_key: + return env_key + raise ValueError("cloro API key not found. Set CLORO_API_KEY environment variable or pass 'api_key' to constructor.") + + def _get_endpoint(self) -> str: + return f"{self.base_url}/{self.engine}" + + def _make_api_request(self, query: str) -> dict[str, Any]: + endpoint = self._get_endpoint() + + payload: dict[str, Any] = { + "country": self.country, + } + + if self.engine == "google": + payload["query"] = query + payload["device"] = self.device + payload["pages"] = self.pages + payload["include"] = { + "html": False, + "aioverview": {"markdown": True} + } + else: + payload["prompt"] = query + + if self.engine in ["chatgpt", "gemini", "copilot", "perplexity", "aimode"]: + payload["include"] = {"markdown": True} + + headers = { + "Authorization": f"Bearer {self._get_api_key()}", + "Content-Type": "application/json", + } + + response = None + try: + response = requests.post( + endpoint, headers=headers, json=payload, timeout=60 + ) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + error_msg = f"Error making request to cloro API ({self.engine}): {e}" + if response is not None and hasattr(response, "content"): + error_msg += f"\nResponse content: {response.content.decode('utf-8', errors='replace')}" + logger.error(error_msg) + raise + + def _run(self, **kwargs: Any) -> FormattedResults: + """Execute the search/query operation.""" + search_query: str | None = kwargs.get("search_query") or kwargs.get("query") + save_file = kwargs.get("save_file", self.save_file) + + if not search_query: + raise ValueError("search_query is required") + + api_response = self._make_api_request(search_query) + + if not api_response.get("success"): + raise ValueError(f"cloro API returned unsuccessful response: {api_response}") + + result = api_response.get("result", {}) + formatted_results: FormattedResults = {} # type: ignore + + # Process Google Search Results + if self.engine == "google": + if "organicResults" in result: + formatted_results["organic"] = self._process_organic_results( + result["organicResults"] + ) + if "peopleAlsoAsk" in result: + formatted_results["peopleAlsoAsk"] = self._process_people_also_ask( + result["peopleAlsoAsk"] + ) + if "relatedSearches" in result: + formatted_results["relatedSearches"] = self._process_related_searches( + result["relatedSearches"] + ) + if "knowledgeGraph" in result: + formatted_results["knowledgeGraph"] = result["knowledgeGraph"] # Keep raw for now or define schema + if "aioverview" in result: + formatted_results["ai_overview"] = result["aioverview"] + + # Process LLM Results + else: + if "text" in result: + formatted_results["text"] = result["text"] + if "sources" in result: + formatted_results["sources"] = result["sources"] + # Pass through other useful fields if needed, or keep it simple + + if save_file: + _save_results_to_file(json.dumps(formatted_results, indent=2)) + + return formatted_results + \ No newline at end of file diff --git a/lib/crewai-tools/tests/tools/cloro_dev_tool_test.py b/lib/crewai-tools/tests/tools/cloro_dev_tool_test.py new file mode 100644 index 0000000000..4ca64bce38 --- /dev/null +++ b/lib/crewai-tools/tests/tools/cloro_dev_tool_test.py @@ -0,0 +1,182 @@ +import os +import pytest +from unittest.mock import patch, MagicMock +from crewai_tools.tools.cloro_dev_tool.cloro_dev_tool import CloroDevTool + +@pytest.fixture(autouse=True) +def mock_cloro_api_key(): + with patch.dict(os.environ, {"CLORO_API_KEY": "test_key"}): + yield + +@patch("requests.post") +def test_cloro_tool_google_search(mock_post): + tool = CloroDevTool(engine="google") + mock_response = { + "success": True, + "result": { + "organicResults": [ + { + "title": "Test Title", + "link": "http://test.com", + "snippet": "Test Snippet" + } + ], + "aioverview": {"markdown": "**AI Overview**"} + } + } + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + result = tool.run(search_query="test query") + + assert "organic" in result + assert result["organic"][0]["title"] == "Test Title" + assert "ai_overview" in result + assert result["ai_overview"]["markdown"] == "**AI Overview**" + + # Check payload + called_payload = mock_post.call_args.kwargs["json"] + assert "query" in called_payload + assert called_payload["query"] == "test query" + assert "include" in called_payload + assert called_payload["include"].get("aioverview", {}).get("markdown") is True + + +@patch("requests.post") +def test_cloro_tool_chatgpt_query(mock_post): + tool = CloroDevTool(engine="chatgpt") + mock_response = { + "success": True, + "result": { + "text": "ChatGPT response", + "markdown": "**ChatGPT response**" + } + } + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + result = tool.run(search_query="test prompt") + + assert "text" in result + assert result["text"] == "ChatGPT response" + + # Check payload + called_payload = mock_post.call_args.kwargs["json"] + assert "prompt" in called_payload + assert called_payload["prompt"] == "test prompt" + + +@patch("requests.post") +def test_cloro_tool_gemini_query(mock_post): + tool = CloroDevTool(engine="gemini") + mock_response = { + "success": True, + "result": { + "text": "Gemini response", + } + } + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + result = tool.run(search_query="gemini prompt") + + assert "text" in result + assert result["text"] == "Gemini response" + + +@patch("requests.post") +def test_cloro_tool_copilot_query(mock_post): + tool = CloroDevTool(engine="copilot") + mock_response = { + "success": True, + "result": { + "text": "Copilot response", + "sources": [{"title": "Source 1", "link": "http://source1.com"}] + } + } + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + result = tool.run(search_query="copilot prompt") + + assert "text" in result + assert "sources" in result + assert result["sources"][0]["title"] == "Source 1" + + +@patch("requests.post") +def test_cloro_tool_perplexity_query(mock_post): + tool = CloroDevTool(engine="perplexity") + mock_response = { + "success": True, + "result": { + "text": "Perplexity response", + } + } + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + result = tool.run(search_query="perplexity prompt") + + assert "text" in result + + +@patch("requests.post") +def test_cloro_tool_aimode_query(mock_post): + tool = CloroDevTool(engine="aimode") + mock_response = { + "success": True, + "result": { + "text": "AI Mode response" + } + } + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + result = tool.run(search_query="aimode prompt") + + assert "text" in result + + +@patch("requests.post") +def test_api_error_handling(mock_post): + tool = CloroDevTool() + mock_post.side_effect = Exception("API Error") + + with pytest.raises(Exception) as exc_info: + tool.run(search_query="test") + assert "API Error" in str(exc_info.value) + +@patch("requests.post") +def test_unsuccessful_response(mock_post): + tool = CloroDevTool() + mock_response = {"success": False} + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + with pytest.raises(ValueError) as exc_info: + tool.run(search_query="test") + assert "cloro API returned unsuccessful response" in str(exc_info.value) + +def test_save_file(): + tool = CloroDevTool(save_file=True) + + with patch("requests.post") as mock_post, \ + patch("builtins.open", new_callable=MagicMock) as mock_open: + + mock_response = { + "success": True, + "result": {"organicResults": []} + } + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + tool.run(search_query="test") + + # Verify open was called + mock_open.assert_called() + + # Verify write was called on the file handle + # open() returns a context manager, __enter__ returns the file handle + mock_file_handle = mock_open.return_value.__enter__.return_value + mock_file_handle.write.assert_called() \ No newline at end of file From 1e4d2b4d67c34f707ede5cce388f41be3e31eb51 Mon Sep 17 00:00:00 2001 From: rbatista191 Date: Sun, 7 Dec 2025 11:06:47 +0100 Subject: [PATCH 2/7] chore: more optimizations to cloro tool --- .../tools/cloro_dev_tool/README.md | 14 +- .../tools/cloro_dev_tool/cloro_dev_tool.py | 337 +++++++++--------- .../tests/tools/cloro_dev_tool_test.py | 17 +- 3 files changed, 189 insertions(+), 179 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md index 1b2c8672fc..1c29c93c3b 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md +++ b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md @@ -37,15 +37,23 @@ Get the credentials by creating a [cloro account](https://dashboard.cloro.dev). The tool returns a structured dictionary containing different fields depending on the selected engine. ### Google Engine + - `organic`: List of organic search results with title, link, snippet, etc. - `peopleAlsoAsk`: List of related questions. - `relatedSearches`: List of related search queries. -- `knowledgeGraph`: Knowledge graph data (if available). - `ai_overview`: Google AI Overview data (if available). -### LLM Engines (ChatGPT, Gemini, etc.) +### LLM Engines (ChatGPT, Perplexity, Gemini, etc.) + - `text`: The main response text from the model. - `sources`: List of sources cited by the model (if available). +- `shopping_cards`: List of product/shopping cards with prices and offers (if available). +- `hotels`: List of hotel results (if available). +- `places`: List of places/locations (if available). +- `videos`: List of video results (if available). +- `images`: List of image results (if available). +- `related_queries`: List of related follow-up queries (if available). +- `entities`: List of extracted entities (if available). ## Advanced example @@ -64,4 +72,4 @@ tool = CloroDevTool( result = tool.run(search_query="Say 'Hello, Brazil!'") print(result) -``` \ No newline at end of file +``` diff --git a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py index 3a4b0a642d..f221fae55f 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py @@ -11,53 +11,28 @@ logger = logging.getLogger(__name__) -class OrganicResult(TypedDict, total=False): - """Organic search result data.""" - - title: str - link: str - snippet: str - position: int | None - date: str - sitelinks: list[dict[str, Any]] - - -class PeopleAlsoAskResult(TypedDict): - """People Also Ask result data.""" - - question: str - snippet: str - title: str - link: str - - -class RelatedSearchResult(TypedDict): - """Related search result data.""" - - query: str - link: str - - -class KnowledgeGraph(TypedDict, total=False): - """Knowledge graph data.""" - - title: str - type: str - description: str - source: dict[str, Any] - attributes: dict[str, Any] - - class FormattedResults(TypedDict, total=False): """Formatted search results from Cloro API.""" - organic: list[OrganicResult] - peopleAlsoAsk: list[PeopleAlsoAskResult] - relatedSearches: list[RelatedSearchResult] - knowledgeGraph: KnowledgeGraph + # Google / Search + organic: list[dict[str, Any]] + peopleAlsoAsk: list[dict[str, Any]] + relatedSearches: list[dict[str, Any]] ai_overview: dict[str, Any] - text: str # For LLM responses - sources: list[dict[str, Any]] # For LLM sources + + # LLM / Common + text: str + sources: list[dict[str, Any]] + + # Rich Content (Perplexity / ChatGPT) + shopping_cards: list[dict[str, Any]] + hotels: list[dict[str, Any]] + places: list[dict[str, Any]] + videos: list[dict[str, Any]] + images: list[dict[str, Any]] + related_queries: list[str] + entities: list[dict[str, Any]] + credits: int @@ -84,136 +59,150 @@ class CloroDevToolSchema(BaseModel): class CloroDevTool(BaseTool): name: str = "Search/Query with Cloro" description: str = ( - "A tool that can be used to search the internet or query LLMs using cloro API. " - "Supports engines: google, chatgpt, gemini, copilot, perplexity, aimode." - ) - args_schema: type[BaseModel] = CloroDevToolSchema - base_url: str = "https://api.cloro.dev/v1/monitor" - engine: Literal[ - "google", - "chatgpt", - "gemini", - "copilot", - "perplexity", - "aimode", - ] = "google" - country: str = "US" - device: str = "desktop" - pages: int = 1 - api_key: str | None = Field(None, description="cloro API key") - env_vars: list[EnvVar] = Field( - default_factory=lambda: [ - EnvVar( - name="CLORO_API_KEY", description="API key for cloro", required=True - ), - ] + "A tool that can be used to search the internet or query LLMs using cloro API. " + "Supports engines: google, chatgpt, gemini, copilot, perplexity, aimode." + ) + args_schema: type[BaseModel] = CloroDevToolSchema + base_url: str = "https://api.cloro.dev/v1/monitor" + engine: Literal[ + "google", + "chatgpt", + "gemini", + "copilot", + "perplexity", + "aimode", + ] = "google" + country: str = "US" + device: str = "desktop" + pages: int = 1 + save_file: bool = False + api_key: str | None = Field(None, description="cloro API key") + env_vars: list[EnvVar] = Field( + default_factory=lambda: [ + EnvVar( + name="CLORO_API_KEY", description="API key for cloro", required=True + ), + ] + ) + + def __init__(self, api_key: str | None = None, **kwargs): + super().__init__(**kwargs) + if api_key: + self.api_key = api_key + + # Validation + if not self.api_key and not os.environ.get("CLORO_API_KEY"): + pass + + def _get_api_key(self) -> str: + if self.api_key: + return self.api_key + env_key = os.environ.get("CLORO_API_KEY") + if env_key: + return env_key + raise ValueError("cloro API key not found. Set CLORO_API_KEY environment variable or pass 'api_key' to constructor.") + + def _get_endpoint(self) -> str: + return f"{self.base_url}/{self.engine}" + + def _make_api_request(self, query: str) -> dict[str, Any]: + endpoint = self._get_endpoint() + + payload: dict[str, Any] = { + "country": self.country, + } + + if self.engine == "google": + payload["query"] = query + payload["device"] = self.device + payload["pages"] = self.pages + payload["include"] = { + "html": False, + "aioverview": {"markdown": True} + } + else: + payload["prompt"] = query + + if self.engine in ["chatgpt", "gemini", "copilot", "perplexity", "aimode"]: + payload["include"] = {"markdown": True} + + headers = { + "Authorization": f"Bearer {self._get_api_key()}", + "Content-Type": "application/json", + } + + response = None + try: + response = requests.post( + endpoint, headers=headers, json=payload, timeout=60 ) - - def __init__(self, api_key: str | None = None, **kwargs): - super().__init__(**kwargs) - if api_key: - self.api_key = api_key - - # Validation - if not self.api_key and not os.environ.get("CLORO_API_KEY"): - pass - - def _get_api_key(self) -> str: - if self.api_key: - return self.api_key - env_key = os.environ.get("CLORO_API_KEY") - if env_key: - return env_key - raise ValueError("cloro API key not found. Set CLORO_API_KEY environment variable or pass 'api_key' to constructor.") - - def _get_endpoint(self) -> str: - return f"{self.base_url}/{self.engine}" - - def _make_api_request(self, query: str) -> dict[str, Any]: - endpoint = self._get_endpoint() - - payload: dict[str, Any] = { - "country": self.country, - } - - if self.engine == "google": - payload["query"] = query - payload["device"] = self.device - payload["pages"] = self.pages - payload["include"] = { - "html": False, - "aioverview": {"markdown": True} - } - else: - payload["prompt"] = query - - if self.engine in ["chatgpt", "gemini", "copilot", "perplexity", "aimode"]: - payload["include"] = {"markdown": True} - - headers = { - "Authorization": f"Bearer {self._get_api_key()}", - "Content-Type": "application/json", - } - - response = None - try: - response = requests.post( - endpoint, headers=headers, json=payload, timeout=60 - ) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - error_msg = f"Error making request to cloro API ({self.engine}): {e}" - if response is not None and hasattr(response, "content"): - error_msg += f"\nResponse content: {response.content.decode('utf-8', errors='replace')}" - logger.error(error_msg) - raise - - def _run(self, **kwargs: Any) -> FormattedResults: - """Execute the search/query operation.""" - search_query: str | None = kwargs.get("search_query") or kwargs.get("query") - save_file = kwargs.get("save_file", self.save_file) - - if not search_query: - raise ValueError("search_query is required") - - api_response = self._make_api_request(search_query) - - if not api_response.get("success"): - raise ValueError(f"cloro API returned unsuccessful response: {api_response}") + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + error_msg = f"Error making request to cloro API ({self.engine}): {e}" + if response is not None and hasattr(response, "content"): + error_msg += f"\nResponse content: {response.content.decode('utf-8', errors='replace')}" + logger.error(error_msg) + raise + + def _run(self, **kwargs: Any) -> FormattedResults: + """Execute the search/query operation.""" + search_query: str | None = kwargs.get("search_query") or kwargs.get("query") + save_file = kwargs.get("save_file", self.save_file) + + if not search_query: + raise ValueError("search_query is required") + + api_response = self._make_api_request(search_query) + + if not api_response.get("success"): + raise ValueError(f"cloro API returned unsuccessful response: {api_response}") + + result = api_response.get("result", {}) + formatted_results: FormattedResults = {} # type: ignore + + # Process Google Search Results + if self.engine == "google": + if "organicResults" in result: + formatted_results["organic"] = result["organicResults"] + if "peopleAlsoAsk" in result: + formatted_results["peopleAlsoAsk"] = result["peopleAlsoAsk"] + if "relatedSearches" in result: + formatted_results["relatedSearches"] = result["relatedSearches"] + if "aioverview" in result: + formatted_results["ai_overview"] = result["aioverview"] + + # Process LLM Results + else: + if "text" in result: + formatted_results["text"] = result["text"] + if "sources" in result: + formatted_results["sources"] = result["sources"] + + # Map rich content if available + if "shopping_cards" in result: + formatted_results["shopping_cards"] = result["shopping_cards"] + elif "shoppingCards" in result: + formatted_results["shopping_cards"] = result["shoppingCards"] + + if "hotels" in result: + formatted_results["hotels"] = result["hotels"] + if "places" in result: + formatted_results["places"] = result["places"] + if "videos" in result: + formatted_results["videos"] = result["videos"] + if "images" in result: + formatted_results["images"] = result["images"] + + if "related_queries" in result: + formatted_results["related_queries"] = result["related_queries"] + elif "relatedQueries" in result: + formatted_results["related_queries"] = result["relatedQueries"] - result = api_response.get("result", {}) - formatted_results: FormattedResults = {} # type: ignore - - # Process Google Search Results - if self.engine == "google": - if "organicResults" in result: - formatted_results["organic"] = self._process_organic_results( - result["organicResults"] - ) - if "peopleAlsoAsk" in result: - formatted_results["peopleAlsoAsk"] = self._process_people_also_ask( - result["peopleAlsoAsk"] - ) - if "relatedSearches" in result: - formatted_results["relatedSearches"] = self._process_related_searches( - result["relatedSearches"] - ) - if "knowledgeGraph" in result: - formatted_results["knowledgeGraph"] = result["knowledgeGraph"] # Keep raw for now or define schema - if "aioverview" in result: - formatted_results["ai_overview"] = result["aioverview"] - - # Process LLM Results - else: - if "text" in result: - formatted_results["text"] = result["text"] - if "sources" in result: - formatted_results["sources"] = result["sources"] - # Pass through other useful fields if needed, or keep it simple - - if save_file: - _save_results_to_file(json.dumps(formatted_results, indent=2)) - - return formatted_results - \ No newline at end of file + if "entities" in result: + formatted_results["entities"] = result["entities"] + + if save_file: + _save_results_to_file(json.dumps(formatted_results, indent=2)) + + return formatted_results \ No newline at end of file diff --git a/lib/crewai-tools/tests/tools/cloro_dev_tool_test.py b/lib/crewai-tools/tests/tools/cloro_dev_tool_test.py index 4ca64bce38..2a2e6a8d46 100644 --- a/lib/crewai-tools/tests/tools/cloro_dev_tool_test.py +++ b/lib/crewai-tools/tests/tools/cloro_dev_tool_test.py @@ -49,7 +49,8 @@ def test_cloro_tool_chatgpt_query(mock_post): "success": True, "result": { "text": "ChatGPT response", - "markdown": "**ChatGPT response**" + "markdown": "**ChatGPT response**", + "shoppingCards": [{"title": "Product 1", "price": "$10"}] } } mock_post.return_value.json.return_value = mock_response @@ -60,6 +61,10 @@ def test_cloro_tool_chatgpt_query(mock_post): assert "text" in result assert result["text"] == "ChatGPT response" + # Verify rich content processing (camelCase normalization) + assert "shopping_cards" in result + assert result["shopping_cards"][0]["title"] == "Product 1" + # Check payload called_payload = mock_post.call_args.kwargs["json"] assert "prompt" in called_payload @@ -111,6 +116,8 @@ def test_cloro_tool_perplexity_query(mock_post): "success": True, "result": { "text": "Perplexity response", + "shopping_cards": [{"title": "Product 2", "price": "$20"}], + "related_queries": ["query 1", "query 2"] } } mock_post.return_value.json.return_value = mock_response @@ -119,6 +126,12 @@ def test_cloro_tool_perplexity_query(mock_post): result = tool.run(search_query="perplexity prompt") assert "text" in result + + # Verify rich content processing (snake_case) + assert "shopping_cards" in result + assert result["shopping_cards"][0]["title"] == "Product 2" + assert "related_queries" in result + assert len(result["related_queries"]) == 2 @patch("requests.post") @@ -179,4 +192,4 @@ def test_save_file(): # Verify write was called on the file handle # open() returns a context manager, __enter__ returns the file handle mock_file_handle = mock_open.return_value.__enter__.return_value - mock_file_handle.write.assert_called() \ No newline at end of file + mock_file_handle.write.assert_called() From d64b6be0dd2b44605d8bcfe473da4e9860760262 Mon Sep 17 00:00:00 2001 From: rbatista191 Date: Sun, 7 Dec 2025 11:24:30 +0100 Subject: [PATCH 3/7] fix: bugs identified by Codex --- .../crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py index f221fae55f..02038ac6a3 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py @@ -89,10 +89,6 @@ def __init__(self, api_key: str | None = None, **kwargs): super().__init__(**kwargs) if api_key: self.api_key = api_key - - # Validation - if not self.api_key and not os.environ.get("CLORO_API_KEY"): - pass def _get_api_key(self) -> str: if self.api_key: @@ -124,7 +120,7 @@ def _make_api_request(self, query: str) -> dict[str, Any]: payload["prompt"] = query if self.engine in ["chatgpt", "gemini", "copilot", "perplexity", "aimode"]: - payload["include"] = {"markdown": True} + payload["include"] = {"markdown": True} headers = { "Authorization": f"Bearer {self._get_api_key()}", @@ -156,7 +152,7 @@ def _run(self, **kwargs: Any) -> FormattedResults: api_response = self._make_api_request(search_query) if not api_response.get("success"): - raise ValueError(f"cloro API returned unsuccessful response: {api_response}") + raise ValueError(f"cloro API returned unsuccessful response: {api_response}") result = api_response.get("result", {}) formatted_results: FormattedResults = {} # type: ignore From 8867738623bf0abb125c5f012fe3b3c3c504f06d Mon Sep 17 00:00:00 2001 From: rbatista191 Date: Sun, 7 Dec 2025 11:37:02 +0100 Subject: [PATCH 4/7] fix: bugs identified by Cursor bot --- lib/crewai-tools/src/crewai_tools/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index df69905734..0001611ac8 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -21,6 +21,7 @@ from crewai_tools.tools.browserbase_load_tool.browserbase_load_tool import ( BrowserbaseLoadTool, ) +from crewai_tools.tools.cloro_dev_tool.cloro_dev_tool import CloroDevTool from crewai_tools.tools.code_docs_search_tool.code_docs_search_tool import ( CodeDocsSearchTool, ) @@ -205,6 +206,7 @@ "BrightDataSearchTool", "BrightDataWebUnlockerTool", "BrowserbaseLoadTool", + "CloroDevTool", "CSVSearchTool", "CodeDocsSearchTool", "CodeInterpreterTool", From b9012405b6cf0deee423c40309d1ce6b38ab138b Mon Sep 17 00:00:00 2001 From: rbatista191 Date: Sun, 7 Dec 2025 12:09:20 +0100 Subject: [PATCH 5/7] feat: added docs for cloro, renamed CloroDev --- docs/en/tools/web-scraping/cloro-tool.mdx | 88 ++++++++++ docs/en/tools/web-scraping/overview.mdx | 150 ++++++++++++------ lib/crewai-tools/src/crewai_tools/__init__.py | 4 +- .../src/crewai_tools/tools/__init__.py | 2 +- .../{cloro_dev_tool => cloro_tool}/README.md | 12 +- .../__init__.py | 0 .../cloro_tool.py} | 2 +- ...ro_dev_tool_test.py => cloro_tool_test.py} | 18 +-- 8 files changed, 210 insertions(+), 66 deletions(-) create mode 100644 docs/en/tools/web-scraping/cloro-tool.mdx rename lib/crewai-tools/src/crewai_tools/tools/{cloro_dev_tool => cloro_tool}/README.md (91%) rename lib/crewai-tools/src/crewai_tools/tools/{cloro_dev_tool => cloro_tool}/__init__.py (100%) rename lib/crewai-tools/src/crewai_tools/tools/{cloro_dev_tool/cloro_dev_tool.py => cloro_tool/cloro_tool.py} (99%) rename lib/crewai-tools/tests/tools/{cloro_dev_tool_test.py => cloro_tool_test.py} (93%) diff --git a/docs/en/tools/web-scraping/cloro-tool.mdx b/docs/en/tools/web-scraping/cloro-tool.mdx new file mode 100644 index 0000000000..13a41ec225 --- /dev/null +++ b/docs/en/tools/web-scraping/cloro-tool.mdx @@ -0,0 +1,88 @@ +--- +title: CloroTool +description: Use the `CloroTool` to scrape AI models using the cloro API. +icon: flask +mode: "wide" +--- + +# `CloroTool` + +## Description + +Use the `CloroTool` to scrape AI models using the cloro API. Supports engines: google, chatgpt, gemini, copilot, perplexity, aimode. + +## Installation + +```shell +pip install 'crewai[tools]' +``` + +## Environment Variables + +- `CLORO_API_KEY` (required) + +Get the credentials by creating a [cloro account](https://dashboard.cloro.dev). + +## Example + +```python Code +from crewai_tools import CloroTool + +# make sure CLORO_API_KEY variable is set +tool = CloroTool() + +result = tool.run(search_query="latest news about AI agents") + +print(result) +``` + +## Arguments + +- `api_key` (str, optional): cloro API key. +- `engine` (str, optional): The engine to use for the query. Options are `google`, `chatgpt`, `gemini`, `copilot`, `perplexity`, `aimode`. Defaults to `google`. +- `country` (str, optional): The ISO 3166-1 alpha-2 country code for localized results (e.g., "US", "BR"). For a full list of supported country codes, refer to the [cloro API /v1/countries endpoint](https://docs.cloro.dev/api-reference/endpoint/countries). Defaults to "US". +- `device` (str, optional): The device type for Google search results (`desktop` or `mobile`). Defaults to "desktop". +- `pages` (int, optional): The number of pages to retrieve for Google search results. Defaults to 1. +- `save_file` (bool, optional): Whether to save the search results to a file. Defaults to `False`. + +## Response Format + +The tool returns a structured dictionary containing different fields depending on the selected engine. + +### Google Engine + +- `organic`: List of organic search results with title, link, snippet, etc. +- `peopleAlsoAsk`: List of related questions. +- `relatedSearches`: List of related search queries. +- `ai_overview`: Google AI Overview data (if available). + +### LLM Engines (ChatGPT, Perplexity, Gemini, etc.) + +- `text`: The main response text from the model. +- `sources`: List of sources cited by the model (if available). +- `shopping_cards`: List of product/shopping cards with prices and offers (if available). +- `hotels`: List of hotel results (if available). +- `places`: List of places/locations (if available). +- `videos`: List of video results (if available). +- `images`: List of image results (if available). +- `related_queries`: List of related follow-up queries (if available). +- `entities`: List of extracted entities (if available). + +## Advanced example + +Check out the cloro [documentation](https://docs.cloro.dev/api-reference/introduction) to get the full list of parameters. + +```python Code +from crewai_tools import CloroTool + +# make sure CLORO_API_KEY variable is set +tool = CloroTool( + engine="chatgpt", + country="BR", + save_file=True +) + +result = tool.run(search_query="Say 'Hello, Brazil!'") + +print(result) +``` diff --git a/docs/en/tools/web-scraping/overview.mdx b/docs/en/tools/web-scraping/overview.mdx index 0031cf33e9..7ef288d6ac 100644 --- a/docs/en/tools/web-scraping/overview.mdx +++ b/docs/en/tools/web-scraping/overview.mdx @@ -14,53 +14,109 @@ These tools enable your agents to interact with the web, extract data from websi General-purpose web scraping tool for extracting content from any website. - - Target specific elements on web pages with precision scraping capabilities. - - - - Crawl entire websites systematically with Firecrawl's powerful engine. - - - - High-performance web scraping with Firecrawl's advanced capabilities. - - - - Search and extract specific content using Firecrawl's search features. - - - - Browser automation and scraping with Selenium WebDriver capabilities. - - - - Professional web scraping with ScrapFly's premium scraping service. - - - - Graph-based web scraping for complex data relationships. - - - - Comprehensive web crawling and data extraction capabilities. - - - - Cloud-based browser automation with BrowserBase infrastructure. - - - - Fast browser interactions with HyperBrowser's optimized engine. - - - - Intelligent browser automation with natural language commands. - - - - Access web data at scale with Oxylabs. - +{" "} + + Target specific elements on web pages with precision scraping capabilities. + + +{" "} + + Crawl entire websites systematically with Firecrawl's powerful engine. + + +{" "} + + High-performance web scraping with Firecrawl's advanced capabilities. + + +{" "} + + Search and extract specific content using Firecrawl's search features. + + +{" "} + + Browser automation and scraping with Selenium WebDriver capabilities. + + +{" "} + + Professional web scraping with ScrapFly's premium scraping service. + + +{" "} + + Graph-based web scraping for complex data relationships. + + +{" "} + + Comprehensive web crawling and data extraction capabilities. + + +{" "} + + LLM scraping via cloro API. + + +{" "} + + Fast browser interactions with HyperBrowser's optimized engine. + + +{" "} + + Intelligent browser automation with natural language commands. + + +{" "} + + Access web data at scale with Oxylabs. + SERP search, Web Unlocker, and Dataset API integrations. diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index 0001611ac8..7c5a455345 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -21,7 +21,7 @@ from crewai_tools.tools.browserbase_load_tool.browserbase_load_tool import ( BrowserbaseLoadTool, ) -from crewai_tools.tools.cloro_dev_tool.cloro_dev_tool import CloroDevTool +from crewai_tools.tools.cloro_tool.cloro_tool import CloroTool from crewai_tools.tools.code_docs_search_tool.code_docs_search_tool import ( CodeDocsSearchTool, ) @@ -206,7 +206,7 @@ "BrightDataSearchTool", "BrightDataWebUnlockerTool", "BrowserbaseLoadTool", - "CloroDevTool", + "CloroTool", "CSVSearchTool", "CodeDocsSearchTool", "CodeInterpreterTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 621b10fd64..9fe3c251b4 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -10,7 +10,7 @@ from crewai_tools.tools.browserbase_load_tool.browserbase_load_tool import ( BrowserbaseLoadTool, ) -from crewai_tools.tools.cloro_dev_tool.cloro_dev_tool import CloroDevTool +from crewai_tools.tools.cloro_tool.cloro_tool import CloroTool from crewai_tools.tools.code_docs_search_tool.code_docs_search_tool import ( CodeDocsSearchTool, ) diff --git a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/cloro_tool/README.md similarity index 91% rename from lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md rename to lib/crewai-tools/src/crewai_tools/tools/cloro_tool/README.md index 1c29c93c3b..9f326be576 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/README.md +++ b/lib/crewai-tools/src/crewai_tools/tools/cloro_tool/README.md @@ -1,6 +1,6 @@ -# CloroDevTool +# CloroTool -Use the `CloroDevTool` to search the web or query AI models using the cloro API. +Use the `CloroTool` to search the web or query AI models using the cloro API. ## Installation @@ -11,10 +11,10 @@ pip install 'crewai[tools]' ## Example ```python -from crewai_tools import CloroDevTool +from crewai_tools import CloroTool # make sure CLORO_API_KEY variable is set -tool = CloroDevTool() +tool = CloroTool() result = tool.run(search_query="latest news about AI agents") @@ -60,10 +60,10 @@ The tool returns a structured dictionary containing different fields depending o Check out the cloro [documentation](https://docs.cloro.dev/api-reference/introduction) to get the full list of parameters. ```python -from crewai_tools import CloroDevTool +from crewai_tools import CloroTool # make sure CLORO_API_KEY variable is set -tool = CloroDevTool( +tool = CloroTool( engine="chatgpt", country="BR", save_file=True diff --git a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/cloro_tool/__init__.py similarity index 100% rename from lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/__init__.py rename to lib/crewai-tools/src/crewai_tools/tools/cloro_tool/__init__.py diff --git a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py b/lib/crewai-tools/src/crewai_tools/tools/cloro_tool/cloro_tool.py similarity index 99% rename from lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py rename to lib/crewai-tools/src/crewai_tools/tools/cloro_tool/cloro_tool.py index 02038ac6a3..6a4d67a4ef 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/cloro_dev_tool/cloro_dev_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/cloro_tool/cloro_tool.py @@ -56,7 +56,7 @@ class CloroDevToolSchema(BaseModel): ) -class CloroDevTool(BaseTool): +class CloroTool(BaseTool): name: str = "Search/Query with Cloro" description: str = ( "A tool that can be used to search the internet or query LLMs using cloro API. " diff --git a/lib/crewai-tools/tests/tools/cloro_dev_tool_test.py b/lib/crewai-tools/tests/tools/cloro_tool_test.py similarity index 93% rename from lib/crewai-tools/tests/tools/cloro_dev_tool_test.py rename to lib/crewai-tools/tests/tools/cloro_tool_test.py index 2a2e6a8d46..7150e254af 100644 --- a/lib/crewai-tools/tests/tools/cloro_dev_tool_test.py +++ b/lib/crewai-tools/tests/tools/cloro_tool_test.py @@ -1,7 +1,7 @@ import os import pytest from unittest.mock import patch, MagicMock -from crewai_tools.tools.cloro_dev_tool.cloro_dev_tool import CloroDevTool +from crewai_tools.tools.cloro_tool.cloro_tool import CloroTool @pytest.fixture(autouse=True) def mock_cloro_api_key(): @@ -10,7 +10,7 @@ def mock_cloro_api_key(): @patch("requests.post") def test_cloro_tool_google_search(mock_post): - tool = CloroDevTool(engine="google") + tool = CloroTool(engine="google") mock_response = { "success": True, "result": { @@ -44,7 +44,7 @@ def test_cloro_tool_google_search(mock_post): @patch("requests.post") def test_cloro_tool_chatgpt_query(mock_post): - tool = CloroDevTool(engine="chatgpt") + tool = CloroTool(engine="chatgpt") mock_response = { "success": True, "result": { @@ -73,7 +73,7 @@ def test_cloro_tool_chatgpt_query(mock_post): @patch("requests.post") def test_cloro_tool_gemini_query(mock_post): - tool = CloroDevTool(engine="gemini") + tool = CloroTool(engine="gemini") mock_response = { "success": True, "result": { @@ -91,7 +91,7 @@ def test_cloro_tool_gemini_query(mock_post): @patch("requests.post") def test_cloro_tool_copilot_query(mock_post): - tool = CloroDevTool(engine="copilot") + tool = CloroTool(engine="copilot") mock_response = { "success": True, "result": { @@ -111,7 +111,7 @@ def test_cloro_tool_copilot_query(mock_post): @patch("requests.post") def test_cloro_tool_perplexity_query(mock_post): - tool = CloroDevTool(engine="perplexity") + tool = CloroTool(engine="perplexity") mock_response = { "success": True, "result": { @@ -136,7 +136,7 @@ def test_cloro_tool_perplexity_query(mock_post): @patch("requests.post") def test_cloro_tool_aimode_query(mock_post): - tool = CloroDevTool(engine="aimode") + tool = CloroTool(engine="aimode") mock_response = { "success": True, "result": { @@ -153,7 +153,7 @@ def test_cloro_tool_aimode_query(mock_post): @patch("requests.post") def test_api_error_handling(mock_post): - tool = CloroDevTool() + tool = CloroTool() mock_post.side_effect = Exception("API Error") with pytest.raises(Exception) as exc_info: @@ -162,7 +162,7 @@ def test_api_error_handling(mock_post): @patch("requests.post") def test_unsuccessful_response(mock_post): - tool = CloroDevTool() + tool = CloroTool() mock_response = {"success": False} mock_post.return_value.json.return_value = mock_response mock_post.return_value.status_code = 200 From 6fa2ba19b880ead7b60925e7aab16afdab763780 Mon Sep 17 00:00:00 2001 From: rbatista191 Date: Sun, 7 Dec 2025 12:14:30 +0100 Subject: [PATCH 6/7] fix: bugs identified by Cursor --- lib/crewai-tools/src/crewai_tools/tools/__init__.py | 2 +- lib/crewai-tools/tests/tools/cloro_tool_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 9fe3c251b4..95d9e89fd0 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -191,7 +191,7 @@ "BrightDataSearchTool", "BrightDataWebUnlockerTool", "BrowserbaseLoadTool", - "CloroDevTool", + "CloroTool", "CSVSearchTool", "CodeDocsSearchTool", "CodeInterpreterTool", diff --git a/lib/crewai-tools/tests/tools/cloro_tool_test.py b/lib/crewai-tools/tests/tools/cloro_tool_test.py index 7150e254af..75fdc2db95 100644 --- a/lib/crewai-tools/tests/tools/cloro_tool_test.py +++ b/lib/crewai-tools/tests/tools/cloro_tool_test.py @@ -172,7 +172,7 @@ def test_unsuccessful_response(mock_post): assert "cloro API returned unsuccessful response" in str(exc_info.value) def test_save_file(): - tool = CloroDevTool(save_file=True) + tool = CloroTool(save_file=True) with patch("requests.post") as mock_post, \ patch("builtins.open", new_callable=MagicMock) as mock_open: From 035f1bebe7522fd592552f694b88d5124cedf03f Mon Sep 17 00:00:00 2001 From: rbatista191 Date: Sun, 7 Dec 2025 17:23:12 +0100 Subject: [PATCH 7/7] chore: few more missing additions --- .env.test | 1 + docs/en/tools/web-scraping/overview.mdx | 2 +- lib/crewai-tools/tool.specs.json | 113 ++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/.env.test b/.env.test index 4ef1b503cf..d83bacc87c 100644 --- a/.env.test +++ b/.env.test @@ -48,6 +48,7 @@ OPENAI_API_BASE=https://api.openai.com/v1 # Search & Scraping Tool API Keys # ----------------------------------------------------------------------------- SERPER_API_KEY=fake-serper-key +CLORO_API_KEY=fake-cloro-key EXA_API_KEY=fake-exa-key BRAVE_API_KEY=fake-brave-key FIRECRAWL_API_KEY=fake-firecrawl-key diff --git a/docs/en/tools/web-scraping/overview.mdx b/docs/en/tools/web-scraping/overview.mdx index 7ef288d6ac..2d813eef89 100644 --- a/docs/en/tools/web-scraping/overview.mdx +++ b/docs/en/tools/web-scraping/overview.mdx @@ -88,7 +88,7 @@ These tools enable your agents to interact with the web, extract data from websi {" "} - LLM scraping via cloro API. + Scrape the user interface of major LLMs via cloro API. {" "} diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index ea2cef07a3..325fe57f08 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -940,6 +940,119 @@ "type": "object" } }, + { + "description": "A tool that scrape the user interface LLMs using cloro API. Support Google, ChatGPT, Gemini, Copilot, Perplexity and AI Mode engines", + "env_vars": [ + { + "default": null, + "description": "API key for cloro", + "name": "CLORO_API_KEY", + "required": true + } + ], + "humanized_name": "Prompt with cloro", + "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": "CloroTool - A tool for scraping LLMs via the cloro API.\n\nAttributes:\n name (str): Tool name.\n description (str): Tool description.\n args_schema (Type[BaseModel]): Pydantic schema for input arguments.\n api_key (Optional[str]): cloro API key.", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "country": { + "default": "US", + "title": "Country", + "type": "string" + }, + "device": { + "default": "desktop", + "title": "Device", + "type": "string" + }, + "engine": { + "default": "google", + "enum": [ + "google", + "chatgpt", + "gemini", + "copilot", + "perplexity", + "aimode" + ], + "title": "Engine", + "type": "string" + }, + "pages": { + "default": 1, + "title": "Pages", + "type": "integer" + }, + "save_file": { + "default": false, + "title": "Save File", + "type": "boolean" + } + }, + "title": "CloroTool", + "type": "object" + }, + "name": "CloroTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for CloroDevTool.", + "properties": { + "search_query": { + "description": "Mandatory query/prompt you want to use to search/query the model", + "title": "Search Query", + "type": "string" + } + }, + "required": ["search_query"], + "title": "CloroDevToolSchema", + "type": "object" + } + }, { "description": "A tool that can be used to semantic search a query from a CSV's content.", "env_vars": [],