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:
parent
6ac45a8638
commit
7f6b924f9c
4 changed files with 209 additions and 1 deletions
|
|
@ -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",
|
||||
|
|
|
|||
115
src/backend/base/langflow/components/tools/serp.py
Normal file
115
src/backend/base/langflow/components/tools/serp.py
Normal 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)
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
90
src/backend/tests/unit/components/tools/test_serp_api.py
Normal file
90
src/backend/tests/unit/components/tools/test_serp_api.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue