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 <edwin.jose@datastax.com>
This commit is contained in:
Raphael Valdetaro 2025-01-20 04:17:43 -03:00 committed by GitHub
commit 7f6b924f9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 209 additions and 1 deletions

View file

@ -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",

View file

@ -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)

View file

@ -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),

View file

@ -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()