diff --git a/src/backend/base/langflow/components/tools/__init__.py b/src/backend/base/langflow/components/tools/__init__.py index f2f5db6dc..53a61e1cb 100644 --- a/src/backend/base/langflow/components/tools/__init__.py +++ b/src/backend/base/langflow/components/tools/__init__.py @@ -11,6 +11,7 @@ from .glean_search_api import GleanSearchAPIComponent from .google_search_api import GoogleSearchAPIComponent from .google_search_api_core import GoogleSearchAPICore from .google_serper_api import GoogleSerperAPIComponent +from .google_serper_api_core import GoogleSerperAPICore from .mcp_stdio import MCPStdio from .python_code_structured_tool import PythonCodeStructuredTool from .python_repl import PythonREPLToolComponent @@ -42,6 +43,7 @@ __all__ = [ "GoogleSearchAPIComponent", "GoogleSearchAPICore", "GoogleSerperAPIComponent", + "GoogleSerperAPICore", "MCPStdio", "PythonCodeStructuredTool", "PythonREPLToolComponent", diff --git a/src/backend/base/langflow/components/tools/google_serper_api.py b/src/backend/base/langflow/components/tools/google_serper_api.py index 038c9d2e1..e9e587c95 100644 --- a/src/backend/base/langflow/components/tools/google_serper_api.py +++ b/src/backend/base/langflow/components/tools/google_serper_api.py @@ -7,10 +7,11 @@ from langflow.schema import Data class GoogleSerperAPIComponent(LCToolComponent): - display_name = "Google Serper API" + display_name = "Google Serper API [DEPRECATED]" description = "Call the Serper.dev Google Search API." name = "GoogleSerperAPI" icon = "Google" + legacy = True inputs = [ SecretStrInput(name="serper_api_key", display_name="Serper API Key", required=True), MultilineInput( diff --git a/src/backend/base/langflow/components/tools/google_serper_api_core.py b/src/backend/base/langflow/components/tools/google_serper_api_core.py new file mode 100644 index 000000000..a46e19057 --- /dev/null +++ b/src/backend/base/langflow/components/tools/google_serper_api_core.py @@ -0,0 +1,74 @@ +from langchain_community.utilities.google_serper import GoogleSerperAPIWrapper + +from langflow.custom import Component +from langflow.io import IntInput, MultilineInput, Output, SecretStrInput +from langflow.schema import DataFrame +from langflow.schema.message import Message + + +class GoogleSerperAPICore(Component): + display_name = "Google Serper API" + description = "Call the Serper.dev Google Search API." + icon = "Serper" + + inputs = [ + SecretStrInput( + name="serper_api_key", + display_name="Serper API Key", + required=True, + ), + MultilineInput( + name="input_value", + display_name="Input", + tool_mode=True, + ), + IntInput( + name="k", + display_name="Number of results", + value=4, + required=True, + ), + ] + + outputs = [ + Output( + display_name="Results", + name="results", + type_=DataFrame, + method="search_serper", + ), + ] + + def search_serper(self) -> DataFrame: + try: + wrapper = self._build_wrapper() + results = wrapper.results(query=self.input_value) + list_results = results.get("organic", []) + + # Convert results to DataFrame using list comprehension + df_data = [ + { + "title": result.get("title", ""), + "link": result.get("link", ""), + "snippet": result.get("snippet", ""), + } + for result in list_results + ] + + return DataFrame(df_data) + except (ValueError, KeyError, ConnectionError) as e: + error_message = f"Error occurred while searching: {e!s}" + self.status = error_message + # Return DataFrame with error as a list of dictionaries + return DataFrame([{"error": error_message}]) + + def text_search_serper(self) -> Message: + search_results = self.search_serper() + text_result = search_results.to_string(index=False) if not search_results.empty else "No results found." + return Message(text=text_result) + + def _build_wrapper(self): + return GoogleSerperAPIWrapper(serper_api_key=self.serper_api_key, k=self.k) + + def build(self): + return self.search_serper diff --git a/src/backend/tests/unit/components/tools/test_google_serper_api_core.py b/src/backend/tests/unit/components/tools/test_google_serper_api_core.py new file mode 100644 index 000000000..1f09e369d --- /dev/null +++ b/src/backend/tests/unit/components/tools/test_google_serper_api_core.py @@ -0,0 +1,114 @@ +from unittest.mock import MagicMock, patch + +import pytest +from langflow.components.tools import GoogleSerperAPICore +from langflow.schema import DataFrame + + +@pytest.fixture +def google_serper_component(): + return GoogleSerperAPICore() + + +@pytest.fixture +def mock_search_results(): + return { + "organic": [ + { + "title": "Test Title 1", + "link": "https://test1.com", + "snippet": "Test snippet 1", + }, + { + "title": "Test Title 2", + "link": "https://test2.com", + "snippet": "Test snippet 2", + }, + ] + } + + +def test_component_initialization(google_serper_component): + assert google_serper_component.display_name == "Google Serper API" + assert google_serper_component.icon == "Serper" + + input_names = [input_.name for input_ in google_serper_component.inputs] + assert "serper_api_key" in input_names + assert "input_value" in input_names + assert "k" in input_names + + +@patch("langchain_community.utilities.google_serper.requests.get") +@patch("langchain_community.utilities.google_serper.requests.post") +def test_search_serper_success(mock_post, mock_get, google_serper_component, mock_search_results): + # Configure mocks + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = mock_search_results + mock_post.return_value = mock_response + mock_get.return_value = mock_response + + # Configure component + google_serper_component.serper_api_key = "test_api_key" + google_serper_component.input_value = "test query" + google_serper_component.k = 2 + + # Execute search + result = google_serper_component.search_serper() + + # Verify results + assert isinstance(result, DataFrame) + assert len(result) == 2 + assert list(result.columns) == ["title", "link", "snippet"] + assert result.iloc[0]["title"] == "Test Title 1" + assert result.iloc[1]["link"] == "https://test2.com" + + +@patch("langchain_community.utilities.google_serper.requests.get") +@patch("langchain_community.utilities.google_serper.requests.post") +def test_search_serper_error_handling(mock_post, mock_get, google_serper_component): + # Configure mocks to simulate error + mock_response = MagicMock() + mock_response.status_code = 403 + mock_response.raise_for_status.side_effect = ConnectionError("API connection failed") + mock_post.return_value = mock_response + mock_get.return_value = mock_response + + # Configure component + google_serper_component.serper_api_key = "test_api_key" + google_serper_component.input_value = "test query" + google_serper_component.k = 2 + + # Execute search + result = google_serper_component.search_serper() + + # Verify error handling + assert isinstance(result, DataFrame) + assert "error" in result.columns + assert "API connection failed" in result.iloc[0]["error"] + + +def test_text_search_serper(google_serper_component): + with patch.object(google_serper_component, "search_serper") as mock_search: + mock_search.return_value = DataFrame( + [{"title": "Test Title", "link": "https://test.com", "snippet": "Test snippet"}] + ) + + result = google_serper_component.text_search_serper() + assert result.text is not None + assert "Test Title" in result.text + assert "https://test.com" in result.text + + +def test_build_wrapper(google_serper_component): + google_serper_component.serper_api_key = "test_api_key" + google_serper_component.k = 2 + + wrapper = google_serper_component._build_wrapper() + assert wrapper.serper_api_key == "test_api_key" + assert wrapper.k == 2 + + +def test_build_method(google_serper_component): + build_result = google_serper_component.build() + assert build_result == google_serper_component.search_serper diff --git a/src/frontend/src/icons/Serper/Serper.jsx b/src/frontend/src/icons/Serper/Serper.jsx index ff0bb9e04..2f6f9f9f7 100644 --- a/src/frontend/src/icons/Serper/Serper.jsx +++ b/src/frontend/src/icons/Serper/Serper.jsx @@ -1,17 +1,26 @@ const SvgSerper = (props) => ( - + ); + export default SvgSerper; diff --git a/src/frontend/src/icons/Serper/serper.svg b/src/frontend/src/icons/Serper/serper.svg index a984dbfe8..0ada1892e 100644 --- a/src/frontend/src/icons/Serper/serper.svg +++ b/src/frontend/src/icons/Serper/serper.svg @@ -1,38 +1,5 @@ - - - + + + + diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 54099d920..7203dd0b6 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -294,6 +294,7 @@ import { QianFanChatIcon } from "../icons/QianFanChat"; import { RedisIcon } from "../icons/Redis"; import { SambaNovaIcon } from "../icons/SambaNova"; import { SearxIcon } from "../icons/Searx"; +import { SerperIcon } from "../icons/Serper"; import { ShareIcon } from "../icons/Share"; import { Share2Icon } from "../icons/Share2"; import SvgSlackIcon from "../icons/Slack/SlackIcon"; @@ -942,4 +943,5 @@ export const nodeIconsLucide: iconsType = { ThumbsDown, ThumbDownIconCustom, ThumbUpIconCustom, + Serper: SerperIcon, };