diff --git a/src/backend/base/langflow/components/tools/__init__.py b/src/backend/base/langflow/components/tools/__init__.py index 90aee6c3d..f2f5db6dc 100644 --- a/src/backend/base/langflow/components/tools/__init__.py +++ b/src/backend/base/langflow/components/tools/__init__.py @@ -9,6 +9,7 @@ from .duck_duck_go_search_run import DuckDuckGoSearchComponent from .exa_search import ExaSearchToolkit 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 .mcp_stdio import MCPStdio from .python_code_structured_tool import PythonCodeStructuredTool @@ -39,6 +40,7 @@ __all__ = [ "ExaSearchToolkit", "GleanSearchAPIComponent", "GoogleSearchAPIComponent", + "GoogleSearchAPICore", "GoogleSerperAPIComponent", "MCPStdio", "PythonCodeStructuredTool", diff --git a/src/backend/base/langflow/components/tools/google_search_api.py b/src/backend/base/langflow/components/tools/google_search_api.py index a57ebb346..2ede883af 100644 --- a/src/backend/base/langflow/components/tools/google_search_api.py +++ b/src/backend/base/langflow/components/tools/google_search_api.py @@ -6,10 +6,11 @@ from langflow.schema import Data class GoogleSearchAPIComponent(LCToolComponent): - display_name = "Google Search API" + display_name = "Google Search API [DEPRECATED]" description = "Call Google Search API." name = "GoogleSearchAPI" icon = "Google" + legacy = True inputs = [ SecretStrInput(name="google_api_key", display_name="Google API Key", required=True), SecretStrInput(name="google_cse_id", display_name="Google CSE ID", required=True), diff --git a/src/backend/base/langflow/components/tools/google_search_api_core.py b/src/backend/base/langflow/components/tools/google_search_api_core.py new file mode 100644 index 000000000..fda4d6adf --- /dev/null +++ b/src/backend/base/langflow/components/tools/google_search_api_core.py @@ -0,0 +1,68 @@ +from langchain_google_community import GoogleSearchAPIWrapper + +from langflow.custom import Component +from langflow.io import IntInput, MultilineInput, Output, SecretStrInput +from langflow.schema import DataFrame + + +class GoogleSearchAPICore(Component): + display_name = "Google Search API" + description = "Call Google Search API and return results as a DataFrame." + icon = "Google" + + inputs = [ + SecretStrInput( + name="google_api_key", + display_name="Google API Key", + required=True, + ), + SecretStrInput( + name="google_cse_id", + display_name="Google CSE ID", + 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_google", + ), + ] + + def search_google(self) -> DataFrame: + """Search Google using the provided query.""" + if not self.google_api_key: + return DataFrame([{"error": "Invalid Google API Key"}]) + + if not self.google_cse_id: + return DataFrame([{"error": "Invalid Google CSE ID"}]) + + try: + wrapper = GoogleSearchAPIWrapper( + google_api_key=self.google_api_key, google_cse_id=self.google_cse_id, k=self.k + ) + results = wrapper.results(query=self.input_value, num_results=self.k) + return DataFrame(results) + except (ValueError, KeyError) as e: + return DataFrame([{"error": f"Invalid configuration: {e!s}"}]) + except ConnectionError as e: + return DataFrame([{"error": f"Connection error: {e!s}"}]) + except RuntimeError as e: + return DataFrame([{"error": f"Error occurred while searching: {e!s}"}]) + + def build(self): + return self.search_google diff --git a/src/backend/tests/unit/components/tools/test_google_search_api.py b/src/backend/tests/unit/components/tools/test_google_search_api.py new file mode 100644 index 000000000..1a500c7f3 --- /dev/null +++ b/src/backend/tests/unit/components/tools/test_google_search_api.py @@ -0,0 +1,113 @@ +from unittest.mock import patch + +import pandas as pd +import pytest +from langflow.components.tools import GoogleSearchAPICore +from langflow.schema import DataFrame + +from tests.base import ComponentTestBaseWithoutClient + + +class TestGoogleSearchAPICore(ComponentTestBaseWithoutClient): + @pytest.fixture + def component_class(self): + return GoogleSearchAPICore + + @pytest.fixture + def default_kwargs(self): + return { + "google_api_key": "test_api_key", + "google_cse_id": "test_cse_id", + "input_value": "test query", + "k": 2, + } + + @pytest.fixture + def file_names_mapping(self): + # New component, no previous versions + return [] + + @pytest.fixture + def mock_search_results(self): + return pd.DataFrame( + [ + { + "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(self, component_class): + component = component_class() + + frontend_node = component.to_frontend_node() + node_data = frontend_node["data"]["node"] + + # Test basic component attributes + assert node_data["display_name"] == "Google Search API" + assert node_data["icon"] == "Google" + + # Test inputs configuration + template = node_data["template"] + assert "google_api_key" in template + assert "google_cse_id" in template + assert "input_value" in template + assert "k" in template + + @patch("langchain_google_community.GoogleSearchAPIWrapper.results") + def test_search_google_success(self, mock_results, component_class, default_kwargs, mock_search_results): + component = component_class(**default_kwargs) + mock_results.return_value = mock_search_results.to_dict("records") + + result = component.search_google() + + assert isinstance(result, DataFrame) + assert len(result) == 2 + assert result.iloc[0]["title"] == "Test Title 1" + assert result.iloc[1]["link"] == "https://test2.com" + mock_results.assert_called_once_with(query="test query", num_results=2) + + def test_search_google_invalid_api_key(self, component_class): + component = component_class(google_api_key=None) + result = component.search_google() + + assert isinstance(result, DataFrame) + assert "error" in result.columns + assert "Invalid Google API Key" in result.iloc[0]["error"] + + def test_search_google_invalid_cse_id(self, component_class): + component = component_class(google_api_key="valid_key", google_cse_id=None) + result = component.search_google() + + assert isinstance(result, DataFrame) + assert "error" in result.columns + assert "Invalid Google CSE ID" in result.iloc[0]["error"] + + @patch("langchain_google_community.GoogleSearchAPIWrapper.results") + def test_search_google_error_handling(self, mock_results, component_class, default_kwargs): + component = component_class(**default_kwargs) + mock_results.side_effect = ConnectionError("API connection failed") + + result = component.search_google() + + assert isinstance(result, DataFrame) + assert "error" in result.columns + assert "Connection error: API connection failed" in result.iloc[0]["error"] + + def test_build_method(self, component_class, default_kwargs): + component = component_class(**default_kwargs) + build_result = component.build() + assert build_result == component.search_google + + @pytest.mark.asyncio + async def test_latest_version(self, component_class, default_kwargs): + """Override test_latest_version to skip API call.""" + component = component_class(**default_kwargs) + assert component is not None