From d650b21f1796ee56cbd6fdf66a1888d69521cb55 Mon Sep 17 00:00:00 2001 From: VICTOR CORREA GOMES <112295415+Vigtu@users.noreply.github.com> Date: Fri, 17 Jan 2025 22:25:34 -0300 Subject: [PATCH] refactor(google_serper_api): migrate to new tool mode implementation (#5446) * # refactor(google_serper_api): migrate to new tool mode implementation BREAKING CHANGE: Replace legacy LCToolComponent implementation with new Component base class - Migrate from LCToolComponent to Component base class - Add tool_mode flag to MultilineInput - Update output configuration to use DataFrame type - Implement structured error handling with DataFrame responses - Remove legacy tool mode implementation * test(google-serper): add unit tests for GoogleSerperAPIComponent - Add comprehensive test suite for GoogleSerperAPIComponent - Mock HTTP requests to test search functionality - Test component initialization and configuration - Add error handling test cases - Test text search and wrapper building methods - Ensure proper DataFrame output structure This change improves test coverage for the Google Serper API integration, following existing test patterns in the project. * [autofix.ci] apply automated fixes * style(tests): remove unused fixture argument in google-serper test - Remove unused mock_search_results fixture from test_text_search_serper - Fix linting error ARG001 (unused function argument) * revert(tools): restore Google Search API component to original implementation Due to backward compatibility concerns, reverting the Google Search API component to its initial implementation state to maintain stability and prevent breaking changes. * refactor(components): mark GoogleSerperAPI component as deprecated & Legacy * feat(components): add GoogleSerperAPICore component * refactor(google-serper): fix filename typo from 'gloogle' to 'google' * test(google-serper): add unit tests following component test guide * feat(icons): improve Serper icon quality and add to icon list * [autofix.ci] apply automated fixes * Update test_google_serper_api_core.py test update --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Edwin Jose --- .../langflow/components/tools/__init__.py | 2 + .../components/tools/google_serper_api.py | 3 +- .../tools/google_serper_api_core.py | 74 ++++++++++++ .../tools/test_google_serper_api_core.py | 114 ++++++++++++++++++ src/frontend/src/icons/Serper/Serper.jsx | 21 +++- src/frontend/src/icons/Serper/serper.svg | 41 +------ src/frontend/src/utils/styleUtils.ts | 2 + 7 files changed, 213 insertions(+), 44 deletions(-) create mode 100644 src/backend/base/langflow/components/tools/google_serper_api_core.py create mode 100644 src/backend/tests/unit/components/tools/test_google_serper_api_core.py 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, };