From 7f6b924f9cffa60f3e8d49a60733f3b2ad6a86fa Mon Sep 17 00:00:00 2001 From: Raphael Valdetaro <79842132+raphaelchristi@users.noreply.github.com> Date: Mon, 20 Jan 2025 04:17:43 -0300 Subject: [PATCH] refactor: Update Serp API component to standard output pattern (#5437) * refactor: Update Serp API component to standard output pattern * test: add unit tests for SerpAPIComponent * [autofix.ci] apply automated fixes * fix: rename component class to avoid conflict with legacy version * [autofix.ci] apply automated fixes * fix: update SerpAPI to extend LCToolComponent * [autofix.ci] apply automated fixes * deprecate: mark SerpAPIComponent as legacy and deprecated * refactor: update Serp API component tests to use new SerpComponent --------- 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 + .../base/langflow/components/tools/serp.py | 115 ++++++++++++++++++ .../langflow/components/tools/serp_api.py | 3 +- .../unit/components/tools/test_serp_api.py | 90 ++++++++++++++ 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 src/backend/base/langflow/components/tools/serp.py create mode 100644 src/backend/tests/unit/components/tools/test_serp_api.py diff --git a/src/backend/base/langflow/components/tools/__init__.py b/src/backend/base/langflow/components/tools/__init__.py index 53a61e1cb..7f6ffd042 100644 --- a/src/backend/base/langflow/components/tools/__init__.py +++ b/src/backend/base/langflow/components/tools/__init__.py @@ -17,6 +17,7 @@ from .python_code_structured_tool import PythonCodeStructuredTool from .python_repl import PythonREPLToolComponent from .search_api import SearchAPIComponent from .searxng import SearXNGToolComponent +from .serp import SerpComponent from .serp_api import SerpAPIComponent from .tavily import TavilySearchComponent from .tavily_search import TavilySearchToolComponent @@ -50,6 +51,7 @@ __all__ = [ "SearXNGToolComponent", "SearchAPIComponent", "SerpAPIComponent", + "SerpComponent", "TavilySearchComponent", "TavilySearchToolComponent", "WikidataAPIComponent", diff --git a/src/backend/base/langflow/components/tools/serp.py b/src/backend/base/langflow/components/tools/serp.py new file mode 100644 index 000000000..51e1f981e --- /dev/null +++ b/src/backend/base/langflow/components/tools/serp.py @@ -0,0 +1,115 @@ +from typing import Any + +from langchain_community.utilities.serpapi import SerpAPIWrapper +from langchain_core.tools import ToolException +from loguru import logger +from pydantic import BaseModel, Field + +from langflow.custom import Component +from langflow.inputs import DictInput, IntInput, MultilineInput, SecretStrInput +from langflow.io import Output +from langflow.schema import Data +from langflow.schema.message import Message + + +class SerpAPISchema(BaseModel): + """Schema for SerpAPI search parameters.""" + + query: str = Field(..., description="The search query") + params: dict[str, Any] | None = Field( + default={ + "engine": "google", + "google_domain": "google.com", + "gl": "us", + "hl": "en", + }, + description="Additional search parameters", + ) + max_results: int = Field(5, description="Maximum number of results to return") + max_snippet_length: int = Field(100, description="Maximum length of each result snippet") + + +class SerpComponent(Component): + display_name = "Serp Search API" + description = "Call Serp Search API with result limiting" + name = "Serp" + icon = "SerpSearch" + + inputs = [ + SecretStrInput(name="serpapi_api_key", display_name="SerpAPI API Key", required=True), + MultilineInput( + name="input_value", + display_name="Input", + tool_mode=True, + ), + DictInput(name="search_params", display_name="Parameters", advanced=True, is_list=True), + IntInput(name="max_results", display_name="Max Results", value=5, advanced=True), + IntInput(name="max_snippet_length", display_name="Max Snippet Length", value=100, advanced=True), + ] + + outputs = [ + Output(display_name="Data", name="data", method="fetch_content"), + Output(display_name="Text", name="text", method="fetch_content_text"), + ] + + def _build_wrapper(self, params: dict[str, Any] | None = None) -> SerpAPIWrapper: + """Build a SerpAPIWrapper with the provided parameters.""" + params = params or {} + if params: + return SerpAPIWrapper( + serpapi_api_key=self.serpapi_api_key, + params=params, + ) + return SerpAPIWrapper(serpapi_api_key=self.serpapi_api_key) + + def run_model(self) -> list[Data]: + return self.fetch_content() + + def fetch_content(self) -> list[Data]: + wrapper = self._build_wrapper(self.search_params) + + def search_func( + query: str, params: dict[str, Any] | None = None, max_results: int = 5, max_snippet_length: int = 100 + ) -> list[Data]: + try: + local_wrapper = wrapper + if params: + local_wrapper = self._build_wrapper(params) + + full_results = local_wrapper.results(query) + organic_results = full_results.get("organic_results", [])[:max_results] + + limited_results = [ + Data( + text=result.get("snippet", ""), + data={ + "title": result.get("title", "")[:max_snippet_length], + "link": result.get("link", ""), + "snippet": result.get("snippet", "")[:max_snippet_length], + }, + ) + for result in organic_results + ] + + except Exception as e: + error_message = f"Error in SerpAPI search: {e!s}" + logger.debug(error_message) + raise ToolException(error_message) from e + return limited_results + + results = search_func( + self.input_value, + params=self.search_params, + max_results=self.max_results, + max_snippet_length=self.max_snippet_length, + ) + self.status = results + return results + + def fetch_content_text(self) -> Message: + data = self.fetch_content() + result_string = "" + for item in data: + result_string += item.text + "\n" + self.status = result_string + return Message(text=result_string) diff --git a/src/backend/base/langflow/components/tools/serp_api.py b/src/backend/base/langflow/components/tools/serp_api.py index faf5ad038..ba9913c57 100644 --- a/src/backend/base/langflow/components/tools/serp_api.py +++ b/src/backend/base/langflow/components/tools/serp_api.py @@ -30,10 +30,11 @@ class SerpAPISchema(BaseModel): class SerpAPIComponent(LCToolComponent): - display_name = "Serp Search API" + display_name = "Serp Search API [DEPRECATED]" description = "Call Serp Search API with result limiting" name = "SerpAPI" icon = "SerpSearch" + legacy = True inputs = [ SecretStrInput(name="serpapi_api_key", display_name="SerpAPI API Key", required=True), diff --git a/src/backend/tests/unit/components/tools/test_serp_api.py b/src/backend/tests/unit/components/tools/test_serp_api.py new file mode 100644 index 000000000..38750538c --- /dev/null +++ b/src/backend/tests/unit/components/tools/test_serp_api.py @@ -0,0 +1,90 @@ +from unittest.mock import MagicMock, patch + +import pytest +from langchain_core.tools import ToolException +from langflow.components.tools import SerpComponent +from langflow.custom import Component +from langflow.custom.utils import build_custom_component_template +from langflow.schema import Data +from langflow.schema.message import Message + + +def test_serpapi_initialization(): + component = SerpComponent() + assert component.display_name == "Serp Search API" + assert component.description == "Call Serp Search API with result limiting" + assert component.icon == "SerpSearch" + + +def test_serpapi_template(): + serpapi = SerpComponent() + component = Component(_code=serpapi._code) + frontend_node, _ = build_custom_component_template(component) + + # Verify basic structure + assert isinstance(frontend_node, dict) + + # Verify inputs + assert "template" in frontend_node + input_names = [input_["name"] for input_ in frontend_node["template"].values() if isinstance(input_, dict)] + + expected_inputs = ["serpapi_api_key", "input_value", "search_params", "max_results", "max_snippet_length"] + + for input_name in expected_inputs: + assert input_name in input_names + + +@patch("langflow.components.tools.serp.SerpAPIWrapper") +def test_fetch_content(mock_serpapi_wrapper): + component = SerpComponent() + component.serpapi_api_key = "test-key" + component.input_value = "test query" + component.max_results = 3 + component.max_snippet_length = 100 + + # Mock the SerpAPIWrapper and its results method + mock_instance = MagicMock() + mock_serpapi_wrapper.return_value = mock_instance + mock_instance.results.return_value = { + "organic_results": [ + {"title": "Test Result 1", "link": "https://test.com", "snippet": "This is a test result 1"}, + {"title": "Test Result 2", "link": "https://test2.com", "snippet": "This is a test result 2"}, + ] + } + + result = component.fetch_content() + + assert isinstance(result, list) + assert len(result) == 2 + assert result[0].text == "This is a test result 1" + assert result[0].data["title"] == "Test Result 1" + assert result[0].data["link"] == "https://test.com" + + +def test_fetch_content_text(): + component = SerpComponent() + component.fetch_content = MagicMock( + return_value=[ + Data(text="First result", data={"title": "Title 1"}), + Data(text="Second result", data={"title": "Title 2"}), + ] + ) + + result = component.fetch_content_text() + + assert isinstance(result, Message) + assert result.text == "First result\nSecond result\n" + + +def test_error_handling(): + component = SerpComponent() + component.serpapi_api_key = "test-key" + component.input_value = "test query" + + with patch("langflow.components.tools.serp.SerpAPIWrapper") as mock_serpapi: + mock_instance = MagicMock() + mock_serpapi.return_value = mock_instance + mock_instance.results.side_effect = Exception("API Error") + + with pytest.raises(ToolException): + component.fetch_content()