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 .python_repl import PythonREPLToolComponent
|
||||||
from .search_api import SearchAPIComponent
|
from .search_api import SearchAPIComponent
|
||||||
from .searxng import SearXNGToolComponent
|
from .searxng import SearXNGToolComponent
|
||||||
|
from .serp import SerpComponent
|
||||||
from .serp_api import SerpAPIComponent
|
from .serp_api import SerpAPIComponent
|
||||||
from .tavily import TavilySearchComponent
|
from .tavily import TavilySearchComponent
|
||||||
from .tavily_search import TavilySearchToolComponent
|
from .tavily_search import TavilySearchToolComponent
|
||||||
|
|
@ -50,6 +51,7 @@ __all__ = [
|
||||||
"SearXNGToolComponent",
|
"SearXNGToolComponent",
|
||||||
"SearchAPIComponent",
|
"SearchAPIComponent",
|
||||||
"SerpAPIComponent",
|
"SerpAPIComponent",
|
||||||
|
"SerpComponent",
|
||||||
"TavilySearchComponent",
|
"TavilySearchComponent",
|
||||||
"TavilySearchToolComponent",
|
"TavilySearchToolComponent",
|
||||||
"WikidataAPIComponent",
|
"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):
|
class SerpAPIComponent(LCToolComponent):
|
||||||
display_name = "Serp Search API"
|
display_name = "Serp Search API [DEPRECATED]"
|
||||||
description = "Call Serp Search API with result limiting"
|
description = "Call Serp Search API with result limiting"
|
||||||
name = "SerpAPI"
|
name = "SerpAPI"
|
||||||
icon = "SerpSearch"
|
icon = "SerpSearch"
|
||||||
|
legacy = True
|
||||||
|
|
||||||
inputs = [
|
inputs = [
|
||||||
SecretStrInput(name="serpapi_api_key", display_name="SerpAPI API Key", required=True),
|
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