refactor(google_serper_api): migrate to new tool mode implementation (#5446)
* # refactor(google_serper_api): migrate to new tool mode implementation BREAKING CHANGE: Replace legacy LCToolComponent implementation with new Component base class - Migrate from LCToolComponent to Component base class - Add tool_mode flag to MultilineInput - Update output configuration to use DataFrame type - Implement structured error handling with DataFrame responses - Remove legacy tool mode implementation * test(google-serper): add unit tests for GoogleSerperAPIComponent - Add comprehensive test suite for GoogleSerperAPIComponent - Mock HTTP requests to test search functionality - Test component initialization and configuration - Add error handling test cases - Test text search and wrapper building methods - Ensure proper DataFrame output structure This change improves test coverage for the Google Serper API integration, following existing test patterns in the project. * [autofix.ci] apply automated fixes * style(tests): remove unused fixture argument in google-serper test - Remove unused mock_search_results fixture from test_text_search_serper - Fix linting error ARG001 (unused function argument) * revert(tools): restore Google Search API component to original implementation Due to backward compatibility concerns, reverting the Google Search API component to its initial implementation state to maintain stability and prevent breaking changes. * refactor(components): mark GoogleSerperAPI component as deprecated & Legacy * feat(components): add GoogleSerperAPICore component * refactor(google-serper): fix filename typo from 'gloogle' to 'google' * test(google-serper): add unit tests following component test guide * feat(icons): improve Serper icon quality and add to icon list * [autofix.ci] apply automated fixes * Update test_google_serper_api_core.py test update --------- 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
f65d3aa166
commit
d650b21f17
7 changed files with 213 additions and 44 deletions
|
|
@ -11,6 +11,7 @@ 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 .google_serper_api_core import GoogleSerperAPICore
|
||||
from .mcp_stdio import MCPStdio
|
||||
from .python_code_structured_tool import PythonCodeStructuredTool
|
||||
from .python_repl import PythonREPLToolComponent
|
||||
|
|
@ -42,6 +43,7 @@ __all__ = [
|
|||
"GoogleSearchAPIComponent",
|
||||
"GoogleSearchAPICore",
|
||||
"GoogleSerperAPIComponent",
|
||||
"GoogleSerperAPICore",
|
||||
"MCPStdio",
|
||||
"PythonCodeStructuredTool",
|
||||
"PythonREPLToolComponent",
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ from langflow.schema import Data
|
|||
|
||||
|
||||
class GoogleSerperAPIComponent(LCToolComponent):
|
||||
display_name = "Google Serper API"
|
||||
display_name = "Google Serper API [DEPRECATED]"
|
||||
description = "Call the Serper.dev Google Search API."
|
||||
name = "GoogleSerperAPI"
|
||||
icon = "Google"
|
||||
legacy = True
|
||||
inputs = [
|
||||
SecretStrInput(name="serper_api_key", display_name="Serper API Key", required=True),
|
||||
MultilineInput(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
from langchain_community.utilities.google_serper import GoogleSerperAPIWrapper
|
||||
|
||||
from langflow.custom import Component
|
||||
from langflow.io import IntInput, MultilineInput, Output, SecretStrInput
|
||||
from langflow.schema import DataFrame
|
||||
from langflow.schema.message import Message
|
||||
|
||||
|
||||
class GoogleSerperAPICore(Component):
|
||||
display_name = "Google Serper API"
|
||||
description = "Call the Serper.dev Google Search API."
|
||||
icon = "Serper"
|
||||
|
||||
inputs = [
|
||||
SecretStrInput(
|
||||
name="serper_api_key",
|
||||
display_name="Serper API Key",
|
||||
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_serper",
|
||||
),
|
||||
]
|
||||
|
||||
def search_serper(self) -> DataFrame:
|
||||
try:
|
||||
wrapper = self._build_wrapper()
|
||||
results = wrapper.results(query=self.input_value)
|
||||
list_results = results.get("organic", [])
|
||||
|
||||
# Convert results to DataFrame using list comprehension
|
||||
df_data = [
|
||||
{
|
||||
"title": result.get("title", ""),
|
||||
"link": result.get("link", ""),
|
||||
"snippet": result.get("snippet", ""),
|
||||
}
|
||||
for result in list_results
|
||||
]
|
||||
|
||||
return DataFrame(df_data)
|
||||
except (ValueError, KeyError, ConnectionError) as e:
|
||||
error_message = f"Error occurred while searching: {e!s}"
|
||||
self.status = error_message
|
||||
# Return DataFrame with error as a list of dictionaries
|
||||
return DataFrame([{"error": error_message}])
|
||||
|
||||
def text_search_serper(self) -> Message:
|
||||
search_results = self.search_serper()
|
||||
text_result = search_results.to_string(index=False) if not search_results.empty else "No results found."
|
||||
return Message(text=text_result)
|
||||
|
||||
def _build_wrapper(self):
|
||||
return GoogleSerperAPIWrapper(serper_api_key=self.serper_api_key, k=self.k)
|
||||
|
||||
def build(self):
|
||||
return self.search_serper
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from langflow.components.tools import GoogleSerperAPICore
|
||||
from langflow.schema import DataFrame
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def google_serper_component():
|
||||
return GoogleSerperAPICore()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_search_results():
|
||||
return {
|
||||
"organic": [
|
||||
{
|
||||
"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(google_serper_component):
|
||||
assert google_serper_component.display_name == "Google Serper API"
|
||||
assert google_serper_component.icon == "Serper"
|
||||
|
||||
input_names = [input_.name for input_ in google_serper_component.inputs]
|
||||
assert "serper_api_key" in input_names
|
||||
assert "input_value" in input_names
|
||||
assert "k" in input_names
|
||||
|
||||
|
||||
@patch("langchain_community.utilities.google_serper.requests.get")
|
||||
@patch("langchain_community.utilities.google_serper.requests.post")
|
||||
def test_search_serper_success(mock_post, mock_get, google_serper_component, mock_search_results):
|
||||
# Configure mocks
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_search_results
|
||||
mock_post.return_value = mock_response
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# Configure component
|
||||
google_serper_component.serper_api_key = "test_api_key"
|
||||
google_serper_component.input_value = "test query"
|
||||
google_serper_component.k = 2
|
||||
|
||||
# Execute search
|
||||
result = google_serper_component.search_serper()
|
||||
|
||||
# Verify results
|
||||
assert isinstance(result, DataFrame)
|
||||
assert len(result) == 2
|
||||
assert list(result.columns) == ["title", "link", "snippet"]
|
||||
assert result.iloc[0]["title"] == "Test Title 1"
|
||||
assert result.iloc[1]["link"] == "https://test2.com"
|
||||
|
||||
|
||||
@patch("langchain_community.utilities.google_serper.requests.get")
|
||||
@patch("langchain_community.utilities.google_serper.requests.post")
|
||||
def test_search_serper_error_handling(mock_post, mock_get, google_serper_component):
|
||||
# Configure mocks to simulate error
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 403
|
||||
mock_response.raise_for_status.side_effect = ConnectionError("API connection failed")
|
||||
mock_post.return_value = mock_response
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# Configure component
|
||||
google_serper_component.serper_api_key = "test_api_key"
|
||||
google_serper_component.input_value = "test query"
|
||||
google_serper_component.k = 2
|
||||
|
||||
# Execute search
|
||||
result = google_serper_component.search_serper()
|
||||
|
||||
# Verify error handling
|
||||
assert isinstance(result, DataFrame)
|
||||
assert "error" in result.columns
|
||||
assert "API connection failed" in result.iloc[0]["error"]
|
||||
|
||||
|
||||
def test_text_search_serper(google_serper_component):
|
||||
with patch.object(google_serper_component, "search_serper") as mock_search:
|
||||
mock_search.return_value = DataFrame(
|
||||
[{"title": "Test Title", "link": "https://test.com", "snippet": "Test snippet"}]
|
||||
)
|
||||
|
||||
result = google_serper_component.text_search_serper()
|
||||
assert result.text is not None
|
||||
assert "Test Title" in result.text
|
||||
assert "https://test.com" in result.text
|
||||
|
||||
|
||||
def test_build_wrapper(google_serper_component):
|
||||
google_serper_component.serper_api_key = "test_api_key"
|
||||
google_serper_component.k = 2
|
||||
|
||||
wrapper = google_serper_component._build_wrapper()
|
||||
assert wrapper.serper_api_key == "test_api_key"
|
||||
assert wrapper.k == 2
|
||||
|
||||
|
||||
def test_build_method(google_serper_component):
|
||||
build_result = google_serper_component.build()
|
||||
assert build_result == google_serper_component.search_serper
|
||||
Loading…
Add table
Add a link
Reference in a new issue