From 14b50a7fa18632695c16d8cdcac75c31b95a236b Mon Sep 17 00:00:00 2001 From: Vigtu Date: Wed, 25 Dec 2024 18:05:15 -0300 Subject: [PATCH 1/4] # 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 --- .../components/tools/google_serper_api.py | 79 +++++++++++++------ 1 file changed, 56 insertions(+), 23 deletions(-) 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 038c9d2e1bb3..f9a10ae2f117 100644 --- a/src/backend/base/langflow/components/tools/google_serper_api.py +++ b/src/backend/base/langflow/components/tools/google_serper_api.py @@ -1,40 +1,73 @@ from langchain_community.utilities.google_serper import GoogleSerperAPIWrapper -from langflow.base.langchain_utilities.model import LCToolComponent -from langflow.field_typing import Tool -from langflow.inputs import IntInput, MultilineInput, SecretStrInput -from langflow.schema import Data +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 GoogleSerperAPIComponent(LCToolComponent): +class GoogleSerperAPIComponent(Component): display_name = "Google Serper API" description = "Call the Serper.dev Google Search API." - name = "GoogleSerperAPI" icon = "Google" + inputs = [ - SecretStrInput(name="serper_api_key", display_name="Serper API Key", required=True), + 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", ), - IntInput(name="k", display_name="Number of results", value=4, required=True), ] - def run_model(self) -> Data | list[Data]: - wrapper = self._build_wrapper() - results = wrapper.results(query=self.input_value) - list_results = results.get("organic", []) - data = [Data(data=result, text=result["snippet"]) for result in list_results] - self.status = data - return data - - def build_tool(self) -> Tool: - wrapper = self._build_wrapper() - return Tool( - name="google_search", - description="Search Google for recent results.", - func=wrapper.run, - ) + 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({"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 From abcba9c0783220112fff7990eb963f7a4769bb83 Mon Sep 17 00:00:00 2001 From: Vigtu Date: Mon, 6 Jan 2025 16:38:10 -0300 Subject: [PATCH 2/4] 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. --- .../tools/test_google_serper_api_tool.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/backend/tests/unit/components/tools/test_google_serper_api_tool.py diff --git a/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py b/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py new file mode 100644 index 000000000000..05708a7f3e6e --- /dev/null +++ b/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py @@ -0,0 +1,115 @@ +import pytest +from unittest.mock import MagicMock, patch +from langflow.components.tools import GoogleSerperAPIComponent +from langflow.schema import DataFrame + + +@pytest.fixture +def google_serper_component(): + return GoogleSerperAPIComponent() + + +@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 == "Google" + + 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, mock_search_results): + 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 \ No newline at end of file From d30186842cdac5690008f122fc9737aee7f3b3b3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:42:10 +0000 Subject: [PATCH 3/4] [autofix.ci] apply automated fixes --- .../components/tools/test_google_serper_api_tool.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py b/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py index 05708a7f3e6e..45056d4a462d 100644 --- a/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py +++ b/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py @@ -1,5 +1,6 @@ -import pytest from unittest.mock import MagicMock, patch + +import pytest from langflow.components.tools import GoogleSerperAPIComponent from langflow.schema import DataFrame @@ -30,7 +31,7 @@ def mock_search_results(): def test_component_initialization(google_serper_component): assert google_serper_component.display_name == "Google Serper API" assert google_serper_component.icon == "Google" - + input_names = [input_.name for input_ in google_serper_component.inputs] assert "serper_api_key" in input_names assert "input_value" in input_names @@ -88,11 +89,9 @@ def test_search_serper_error_handling(mock_post, mock_get, google_serper_compone def test_text_search_serper(google_serper_component, mock_search_results): - with patch.object(google_serper_component, 'search_serper') as mock_search: + 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"} - ] + [{"title": "Test Title", "link": "https://test.com", "snippet": "Test snippet"}] ) result = google_serper_component.text_search_serper() @@ -112,4 +111,4 @@ def test_build_wrapper(google_serper_component): def test_build_method(google_serper_component): build_result = google_serper_component.build() - assert build_result == google_serper_component.search_serper \ No newline at end of file + assert build_result == google_serper_component.search_serper From b01510b417869f6f5a4d040d0a7c83d85ca741de Mon Sep 17 00:00:00 2001 From: Vigtu Date: Mon, 6 Jan 2025 16:48:44 -0300 Subject: [PATCH 4/4] 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) --- .../tests/unit/components/tools/test_google_serper_api_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py b/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py index 45056d4a462d..265c43130155 100644 --- a/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py +++ b/src/backend/tests/unit/components/tools/test_google_serper_api_tool.py @@ -88,7 +88,7 @@ def test_search_serper_error_handling(mock_post, mock_get, google_serper_compone assert "API connection failed" in result.iloc[0]["error"] -def test_text_search_serper(google_serper_component, mock_search_results): +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"}]