Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(google_serper_api): migrate to new tool mode implementation #5446

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/backend/base/langflow/components/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .glean_search_api import GleanSearchAPIComponent
from .google_search_api import GoogleSearchAPIComponent
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
Expand Down Expand Up @@ -40,6 +41,7 @@
"GleanSearchAPIComponent",
"GoogleSearchAPIComponent",
"GoogleSerperAPIComponent",
"GoogleSerperAPICore",
"MCPStdio",
"PythonCodeStructuredTool",
"PythonREPLToolComponent",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@


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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
114 changes: 114 additions & 0 deletions src/backend/tests/unit/components/tools/test_google_serper_api_core.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 15 additions & 6 deletions src/frontend/src/icons/Serper/Serper.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
const SvgSerper = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
viewBox="0 0 70 70"
width="1em"
height="1em"
viewBox="0 0 48 48"
preserveAspectRatio="xMidYMid meet"
fill="none"
{...props}
>
<image
width={48}
height={48}
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAABGdBTUEAALGPC/xhBQAAACBjSFJN AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACGVBMVEUAAACVyvSRzvOPzfSQ zfSRzPSQzfSQzfSQzfSQyPSUyfKRzvOQzfSRzvSQzfOOxvGO0PaQzfSQzfSQzfSTzvWZzP+RzPSQ zfSPzfOA1f+Qy/KPzfSQzfSQzfSPzfSRzfSPzPWPy/iRzfWQzfSRz/GPzfORzvWSzfaPzvaQzPOP zPWQzfWQzfSQzfSQzvSMzPKSzvOPzfSQzfWRzvWQzvWN0PKTzPKQzfOQzfSQzfSAv/+Rz/WQzfWQ zfSPzfOQzfSQzPORzPSPzfSRzPSqqv+QzfWQzfSQzvSQzfSRzfWPzPX///+RzvSQzfSQzPSQzvSL 0f+QzvWSy/WQzfWQzfSQzfSPzfWRzfWRzPWRzPWT0feQzPOQzfOQzPOL0fOPzPORzfSPzfOQzPaQ zfSQzfSPy/KQzfWQzfSQzfSPzPOPz/eQzPSQzvSQzfSQzPSPzvSQzvSQzfWPzvWQzfOQzPSJxOuP zPWQzfSQzvSPz/SV1eqQzPSQzfWPy/OQzfSOzfGQzvSRzfOfv/+PzfSQzfSQzvWQzfWSyO2PzvOQ zfSRzfSRzfSSzvOA//+PzvSOxv+OzfKPzPWQzfSRzPKSzPSQzfSQzfOQzfSOzvWPzfOQzfSSzvSR zfSRzPSW0vCRzPSSzvOQzfOQzvSQzfSIzO6OzfWS2/+RzfSSzPCQzfWQzvWQzfORz/KQzfSRy/WQ zfSQzfSQzfT////309j7AAAAsXRSTlMAGFiQuNnu+Y8XE23KyWwSG/X0jhoFb/KABifQ5aNwSDIi M84lUpY4OZdQZ/z4hxQViWVoxCYox/6KBE/z/WvxbniLdAOVy+jkfxkBXeGmjAuqMa3snntmfash aldVFrC0mTfBqDt64udpIHOl0dRytdxJheMNS9rtMAzPw0DrJL9WCGC8fNsOgkdhpD8CWQk9kuo8 RvvFnzQp+i+7jRFfKsaRdQ9NB/AjTGOYOvZKttg9JvjTAAAAAWJLR0RLaQuFUAAAAAd0SU1FB+cF HgMFK2w+nRoAAANfSURBVEjHhVX5QxJREH6eKGQmiIQheJflgXiRYhalmCRlYWqGZZdHB6WllZra aZaVmZUWppZJduj+h8089mIXcH7Ynfnmm7fvzcybJUQqUdExsXHxCkV8XGxCdCLZRpSqHUyQJO1M jkDfFZPCyEStSQ1D16bpmJCSslsfip++h/UbMoymzKysbJMxw8BCOblyfl5geXX+XjG6ryCwSd1+ Kf9AIeJFxSVSh7lUgZ5CSzBcRtcprwi118oq9FmDIvLo+gerw2RDQ79RIzqvDYDaQ+HzraoFQh1/ cj3NTwQ+IYdprrSsdQQtO4koCcg5ytZXjecV7f+Yqb7BoW4sPy7KgRZPbmuiegyoikre5TzRzJW4 1mXm4YoiAE6ipsTSFPOOUy3irjCc5h12LCt2ogq7hV/JeQZ57tazbe0dWDHHOc5TgjvvBAX7OZ9f 5zxYzZ5As3VdAOMi7+oGqwpQK7wv8ehl2Abfzs4r4EvnrKtYbzOJhlejkA03w1wTLGUPoxZ22wtU D81RhkCBDPSJ0t9/XTg1uQFUDYmFZ5oA3mSYW11hiucF6m1yB54DAjgI5l3LvZABQ+AbJvfh+UAA H47QezGqaRuTTYxM8DwimOxxEZo8IVStY/KxOCAXl5IFEOeTEaHSjR5ZgGRLVNIt9U972JBn0i09 Dz60IC/GpoaxVC8lh5akNUic09CiE5K0YuFeCZzE10FDawo6f4YzXLRw2G0GJ4eZHzGjM6KAN+B9 y33PQVuDNh8/vN6BMS0KmGWY95w+F2g+kgTvAg7Ux8HlEFL5YV60X2zvj0R6gQZgoFg/fab6QgEY i1+40+HNLANFiTepNCgVDNMz2vKVTmJrG+fw4RRYQg3nmmKBj/hmE91p3SQHLy/ibKRqKjJWtHxE f3cRS49f5RtQj0fVRQWM7+j0iXLzY22wtaW13fRTgFaR42UNbQ5aKhJBjMho4Iddbh1OrQgRRhzG 634BqKHjXqMNTdfPonf+lxizYL2ZqoVQ/OUkmt+NYNRCv6GwywZAom+Rrr8hddTUBX6xv/84BdA5 1/2Xwutr8i+P57DZ73V5/2X7/dlDXpeDhRr8JIRUh/uxq0P/2EGafGo53WaPilCg5M4Vq5htXelb ItuIeVKzueVWKNxbmz6P7D9P/gNwmex8k7QhUQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0wNS0z MFQwMzowNTo0MyswMDowMLa1rmEAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMDUtMzBUMDM6MDU6 NDMrMDA6MDDH6BbdAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDIzLTA1LTMwVDAzOjA1OjQzKzAw OjAwkP03AgAAAABJRU5ErkJggg=="
<circle
cx="35"
cy="35"
r="32"
stroke="#90CDF4"
strokeWidth="6"
fill="transparent"
/>
<path
d="M35.6992 17.5264C37.571 17.5264 39.3369 17.8438 40.9971 18.4785C42.6735 19.1133 44.1383 19.9922 45.3916 21.1152C46.6449 22.2383 47.597 23.5404 48.248 25.0215C48.3945 25.347 48.4678 25.6807 48.4678 26.0225C48.4678 26.6898 48.2236 27.2676 47.7354 27.7559C47.2633 28.2279 46.6937 28.4639 46.0264 28.4639C45.5869 28.4639 45.1475 28.3255 44.708 28.0488C44.2686 27.7559 43.9593 27.4059 43.7803 26.999C43.1618 25.5993 42.1283 24.4844 40.6797 23.6543C39.2474 22.8242 37.5872 22.4092 35.6992 22.4092C34.7715 22.4092 33.7868 22.5231 32.7451 22.751C31.7197 22.9788 30.7513 23.3288 29.8398 23.8008C28.9284 24.2565 28.1878 24.8343 27.6182 25.5342C27.0485 26.234 26.7637 27.0479 26.7637 27.9756C26.7637 28.7243 27.0078 29.3753 27.4961 29.9287C27.9844 30.4658 28.5296 30.889 29.1318 31.1982C30.5479 31.8981 32.1104 32.3864 33.8193 32.6631C35.5446 32.9398 37.2861 33.2083 39.0439 33.4688C40.8018 33.7292 42.4375 34.1849 43.9512 34.8359C44.9115 35.2428 45.8311 35.8044 46.71 36.5205C47.5889 37.2367 48.305 38.1074 48.8584 39.1328C49.4118 40.1582 49.6885 41.3626 49.6885 42.7461C49.6885 44.5365 49.2572 46.1071 48.3945 47.458C47.5319 48.8089 46.4007 49.932 45.001 50.8271C43.6012 51.7223 42.0794 52.3978 40.4355 52.8535C38.8079 53.293 37.2129 53.5127 35.6504 53.5127C33.5345 53.5127 31.5488 53.179 29.6934 52.5117C27.8379 51.8281 26.2184 50.8678 24.835 49.6309C23.4515 48.3776 22.3936 46.9128 21.6611 45.2363C21.5146 44.9108 21.4414 44.5853 21.4414 44.2598C21.4414 43.5924 21.6774 43.0228 22.1494 42.5508C22.6377 42.0625 23.2155 41.8184 23.8828 41.8184C24.3385 41.8184 24.7861 41.9648 25.2256 42.2578C25.665 42.5345 25.9661 42.8844 26.1289 43.3076C26.8451 44.9515 28.0576 46.2536 29.7666 47.2139C31.4756 48.1579 33.4368 48.6299 35.6504 48.6299C37.0339 48.6299 38.4255 48.4264 39.8252 48.0195C41.2249 47.5964 42.3968 46.9535 43.3408 46.0908C44.3011 45.2119 44.7812 44.097 44.7812 42.7461C44.7812 41.8835 44.4883 41.1755 43.9023 40.6221C43.3327 40.0524 42.7061 39.6211 42.0225 39.3281C40.4762 38.6608 38.8242 38.2051 37.0664 37.9609C35.3086 37.7005 33.5589 37.432 31.8174 37.1553C30.0758 36.8623 28.4482 36.3333 26.9346 35.5684C25.6488 34.9173 24.4769 33.9733 23.4189 32.7363C22.3773 31.4831 21.8564 29.8962 21.8564 27.9756C21.8564 26.2829 22.2633 24.7936 23.0771 23.5078C23.9072 22.2057 24.9977 21.1152 26.3486 20.2363C27.7158 19.3411 29.2132 18.6657 30.8408 18.21C32.4684 17.7542 34.0879 17.5264 35.6992 17.5264Z"
fill="#90CDF4"
/>
</svg>
);

export default SvgSerper;
41 changes: 4 additions & 37 deletions src/frontend/src/icons/Serper/serper.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/frontend/src/utils/styleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ import { QianFanChatIcon } from "../icons/QianFanChat";
import { RedisIcon } from "../icons/Redis";
import { SambaNovaIcon } from "../icons/SambaNova";
import { SearxIcon } from "../icons/Searx";
import { SerperIcon } from "../icons/Serper";
import { ShareIcon } from "../icons/Share";
import { Share2Icon } from "../icons/Share2";
import SvgSlackIcon from "../icons/Slack/SlackIcon";
Expand Down Expand Up @@ -942,4 +943,5 @@ export const nodeIconsLucide: iconsType = {
ThumbsDown,
ThumbDownIconCustom,
ThumbUpIconCustom,
Serper: SerperIcon,
};
Loading