lfx-serpingapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lfx_serpingapi/__init__.py +15 -0
- lfx_serpingapi/components/serpingapi/__init__.py +3 -0
- lfx_serpingapi/components/serpingapi/serpingapi_search.py +176 -0
- lfx_serpingapi/extension.json +16 -0
- lfx_serpingapi-0.1.0.dist-info/METADATA +60 -0
- lfx_serpingapi-0.1.0.dist-info/RECORD +8 -0
- lfx_serpingapi-0.1.0.dist-info/WHEEL +4 -0
- lfx_serpingapi-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""lfx-serpingapi: Serping API Search bundle.
|
|
2
|
+
|
|
3
|
+
This package is the distribution unit ``lfx-serpingapi``. At runtime
|
|
4
|
+
Langflow's loader discovers ``extension.json`` shipped alongside this
|
|
5
|
+
``__init__.py`` and registers ``SerpingApiSearchComponent`` under the
|
|
6
|
+
namespaced ID ``ext:serpingapi:SerpingApiSearchComponent@official``.
|
|
7
|
+
|
|
8
|
+
Serping API (https://serpingapi.com) is a Google SERP API; the component
|
|
9
|
+
calls its search endpoint directly with ``httpx`` and needs only a
|
|
10
|
+
user-supplied API key, so the bundle carries no vendor SDK dependency.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from lfx_serpingapi.components.serpingapi.serpingapi_search import SerpingApiSearchComponent
|
|
14
|
+
|
|
15
|
+
__all__ = ["SerpingApiSearchComponent"]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from lfx.custom.custom_component.component import Component
|
|
3
|
+
from lfx.field_typing.range_spec import RangeSpec
|
|
4
|
+
from lfx.inputs.inputs import IntInput, MessageTextInput, SecretStrInput
|
|
5
|
+
from lfx.schema.data import Data
|
|
6
|
+
from lfx.schema.dataframe import DataFrame
|
|
7
|
+
from lfx.template.field.base import Output
|
|
8
|
+
|
|
9
|
+
SEARCH_ENDPOINT = "https://api.serpingapi.com/v1/search"
|
|
10
|
+
DEFAULT_RESULTS = 10
|
|
11
|
+
MIN_RESULTS = 1
|
|
12
|
+
MAX_RESULTS = 100
|
|
13
|
+
FIRST_PAGE = 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _http_error_message(error: httpx.HTTPStatusError) -> str:
|
|
17
|
+
"""Describe a non-2xx Serping API response, keeping the API's own error message.
|
|
18
|
+
|
|
19
|
+
Serping API errors carry ``{"error": {"code": ..., "message": ...}}``.
|
|
20
|
+
"""
|
|
21
|
+
response = error.response
|
|
22
|
+
detail = None
|
|
23
|
+
try:
|
|
24
|
+
payload = response.json()
|
|
25
|
+
body_error = payload.get("error") if isinstance(payload, dict) else None
|
|
26
|
+
if isinstance(body_error, dict):
|
|
27
|
+
detail = body_error.get("message") or body_error.get("code")
|
|
28
|
+
elif isinstance(body_error, str):
|
|
29
|
+
detail = body_error
|
|
30
|
+
except ValueError:
|
|
31
|
+
detail = None
|
|
32
|
+
reason = detail or response.reason_phrase or "request failed"
|
|
33
|
+
return f"Serping API error {response.status_code}: {reason}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SerpingApiSearchComponent(Component):
|
|
37
|
+
"""Component for performing web searches using the Serping API."""
|
|
38
|
+
|
|
39
|
+
display_name = "Serping API Search"
|
|
40
|
+
description = "Search Google organic results with the Serping API."
|
|
41
|
+
documentation = "https://serpingapi.com/docs"
|
|
42
|
+
icon = "search"
|
|
43
|
+
|
|
44
|
+
inputs = [
|
|
45
|
+
MessageTextInput(
|
|
46
|
+
name="input_value",
|
|
47
|
+
display_name="Search Query",
|
|
48
|
+
required=True,
|
|
49
|
+
info="The search query to execute with the Serping API.",
|
|
50
|
+
tool_mode=True,
|
|
51
|
+
),
|
|
52
|
+
SecretStrInput(
|
|
53
|
+
name="serpingapi_api_key",
|
|
54
|
+
display_name="Serping API Key",
|
|
55
|
+
required=True,
|
|
56
|
+
info="Your Serping API key. Get one at https://serpingapi.com.",
|
|
57
|
+
password=True,
|
|
58
|
+
),
|
|
59
|
+
MessageTextInput(
|
|
60
|
+
name="gl",
|
|
61
|
+
display_name="Country",
|
|
62
|
+
required=False,
|
|
63
|
+
advanced=True,
|
|
64
|
+
info="Country code for the results, for example 'us', 'de' or 'jp'.",
|
|
65
|
+
),
|
|
66
|
+
MessageTextInput(
|
|
67
|
+
name="hl",
|
|
68
|
+
display_name="Language",
|
|
69
|
+
required=False,
|
|
70
|
+
advanced=True,
|
|
71
|
+
info="Interface language, for example 'en' or 'es'.",
|
|
72
|
+
),
|
|
73
|
+
MessageTextInput(
|
|
74
|
+
name="location",
|
|
75
|
+
display_name="Location",
|
|
76
|
+
required=False,
|
|
77
|
+
advanced=True,
|
|
78
|
+
info="Locality for the search, for example 'Seattle, Washington, United States'.",
|
|
79
|
+
),
|
|
80
|
+
IntInput(
|
|
81
|
+
name="max_results",
|
|
82
|
+
display_name="Max Results",
|
|
83
|
+
value=DEFAULT_RESULTS,
|
|
84
|
+
required=False,
|
|
85
|
+
advanced=True,
|
|
86
|
+
range_spec=RangeSpec(min=MIN_RESULTS, max=MAX_RESULTS, step=1, step_type="int"),
|
|
87
|
+
info="Maximum number of organic results to return (1-100).",
|
|
88
|
+
),
|
|
89
|
+
IntInput(
|
|
90
|
+
name="page",
|
|
91
|
+
display_name="Page",
|
|
92
|
+
value=FIRST_PAGE,
|
|
93
|
+
required=False,
|
|
94
|
+
advanced=True,
|
|
95
|
+
range_spec=RangeSpec(min=FIRST_PAGE, max=100, step=1, step_type="int"),
|
|
96
|
+
info="Result page to fetch, starting at 1.",
|
|
97
|
+
),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
outputs = [
|
|
101
|
+
# The method name is also the tool name in tool mode. Keep it specific:
|
|
102
|
+
# DuckDuckGo, Tavily, Wikipedia and other search components already expose
|
|
103
|
+
# ``fetch_content_dataframe``, and an Agent cannot hold two tools with the
|
|
104
|
+
# same name.
|
|
105
|
+
Output(display_name="Table", name="dataframe", method="serpingapi_search"),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def _request_body(self) -> dict:
|
|
109
|
+
"""Build the JSON body for the Serping API search endpoint."""
|
|
110
|
+
requested = DEFAULT_RESULTS if self.max_results is None else int(self.max_results)
|
|
111
|
+
num = max(MIN_RESULTS, min(requested, MAX_RESULTS))
|
|
112
|
+
body: dict = {"q": self.input_value or "", "num": num}
|
|
113
|
+
for field in ("gl", "hl", "location"):
|
|
114
|
+
value = getattr(self, field, None)
|
|
115
|
+
if isinstance(value, str) and value.strip():
|
|
116
|
+
body[field] = value.strip()
|
|
117
|
+
page = FIRST_PAGE if self.page is None else int(self.page)
|
|
118
|
+
if page > FIRST_PAGE:
|
|
119
|
+
body["page"] = page
|
|
120
|
+
return body
|
|
121
|
+
|
|
122
|
+
def _search(self) -> dict:
|
|
123
|
+
"""Call the Serping API search endpoint and return the decoded JSON payload."""
|
|
124
|
+
if not self.serpingapi_api_key:
|
|
125
|
+
msg = "Serping API key is required. Set the Serping API Key input."
|
|
126
|
+
raise ValueError(msg)
|
|
127
|
+
|
|
128
|
+
headers = {
|
|
129
|
+
"X-API-Key": self.serpingapi_api_key,
|
|
130
|
+
"Accept": "application/json",
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
# Identify the integration to the API; keeps traffic attributable.
|
|
133
|
+
"User-Agent": "langflow-serpingapi-bundle",
|
|
134
|
+
}
|
|
135
|
+
response = httpx.post(SEARCH_ENDPOINT, json=self._request_body(), headers=headers, timeout=30)
|
|
136
|
+
response.raise_for_status()
|
|
137
|
+
return response.json()
|
|
138
|
+
|
|
139
|
+
def _error_result(self, message: str) -> list[Data]:
|
|
140
|
+
error_data = [Data(text=message, data={"error": message})]
|
|
141
|
+
self.status = error_data
|
|
142
|
+
return error_data
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _result_to_data(result: dict) -> Data:
|
|
146
|
+
snippet = result.get("snippet", "")
|
|
147
|
+
return Data(
|
|
148
|
+
text=snippet,
|
|
149
|
+
data={
|
|
150
|
+
"title": result.get("title", ""),
|
|
151
|
+
"link": result.get("link", ""),
|
|
152
|
+
"snippet": snippet,
|
|
153
|
+
"position": result.get("position"),
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def fetch_content(self) -> list[Data]:
|
|
158
|
+
"""Execute the search and return the organic results as Data objects."""
|
|
159
|
+
try:
|
|
160
|
+
payload = self._search()
|
|
161
|
+
except httpx.HTTPStatusError as e:
|
|
162
|
+
return self._error_result(_http_error_message(e))
|
|
163
|
+
except (httpx.HTTPError, ValueError) as e:
|
|
164
|
+
return self._error_result(str(e))
|
|
165
|
+
|
|
166
|
+
if not isinstance(payload, dict):
|
|
167
|
+
return self._error_result("Serping API returned an unexpected response (expected a JSON object).")
|
|
168
|
+
|
|
169
|
+
results = payload.get("organic") or []
|
|
170
|
+
data_results = [self._result_to_data(result) for result in results if isinstance(result, dict)]
|
|
171
|
+
self.status = data_results
|
|
172
|
+
return data_results
|
|
173
|
+
|
|
174
|
+
def serpingapi_search(self) -> DataFrame:
|
|
175
|
+
"""Run the search and return the organic results as a DataFrame."""
|
|
176
|
+
return DataFrame(self.fetch_content())
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://schemas.langflow.org/extension/v1.json",
|
|
3
|
+
"id": "lfx-serpingapi",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"name": "Serping API",
|
|
6
|
+
"description": "Serping API Google SERP web-search component as a standalone Langflow Extension Bundle.",
|
|
7
|
+
"lfx": {
|
|
8
|
+
"compat": ["1"]
|
|
9
|
+
},
|
|
10
|
+
"bundles": [
|
|
11
|
+
{
|
|
12
|
+
"name": "serpingapi",
|
|
13
|
+
"path": "components/serpingapi"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lfx-serpingapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Serping API Google SERP web-search component as a standalone Langflow Extension Bundle.
|
|
5
|
+
Project-URL: Homepage, https://github.com/langflow-ai/langflow
|
|
6
|
+
Project-URL: Documentation, https://docs.langflow.org/extensions
|
|
7
|
+
Project-URL: Repository, https://github.com/langflow-ai/langflow
|
|
8
|
+
Author-email: Langflow <contact@langflow.org>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: bundle,extension,langflow,lfx,search,serp,serpingapi
|
|
11
|
+
Requires-Python: <3.15,>=3.10
|
|
12
|
+
Requires-Dist: httpx<1.0.0,>=0.24.0
|
|
13
|
+
Requires-Dist: lfx<2.0.0,>=1.13.0.dev0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# lfx-serpingapi
|
|
17
|
+
|
|
18
|
+
Serping API Google SERP web-search component as a standalone Langflow Extension Bundle.
|
|
19
|
+
|
|
20
|
+
The bundle ships a single component, `SerpingApiSearchComponent`, which runs
|
|
21
|
+
a web search through the [Serping API](https://serpingapi.com) and returns
|
|
22
|
+
the organic Google results as a table. It calls the Serping API search
|
|
23
|
+
endpoint directly with `httpx` and needs only a user-supplied API key, so it
|
|
24
|
+
carries no vendor SDK dependency. See the
|
|
25
|
+
[Serping API docs](https://serpingapi.com/docs) for the API details.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install lfx-serpingapi
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The bundle is registered automatically via the `langflow.extensions`
|
|
34
|
+
entry-point. After install, restart your Langflow server; the
|
|
35
|
+
`SerpingApiSearchComponent` will appear in the palette's **Bundles** section
|
|
36
|
+
under **Serping API**.
|
|
37
|
+
|
|
38
|
+
## Configure
|
|
39
|
+
|
|
40
|
+
Set the **Serping API Key** input to your own key from
|
|
41
|
+
[serpingapi.com](https://serpingapi.com). The component is optional and does
|
|
42
|
+
nothing until a key is supplied, so it changes nothing for anyone who does
|
|
43
|
+
not use it. Country (`gl`), language (`hl`), location, max results (1-100)
|
|
44
|
+
and page are optional advanced inputs.
|
|
45
|
+
|
|
46
|
+
In tool mode, the component exposes a single tool named `serpingapi_search`.
|
|
47
|
+
|
|
48
|
+
## Develop
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd src/bundles/serpingapi
|
|
52
|
+
pip install -e .
|
|
53
|
+
lfx extension validate src/lfx_serpingapi
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Manifest
|
|
57
|
+
|
|
58
|
+
The extension manifest is shipped at `src/lfx_serpingapi/extension.json` and
|
|
59
|
+
points at the bundle at `components/serpingapi`. The component registers under
|
|
60
|
+
the canonical namespaced ID `ext:serpingapi:SerpingApiSearchComponent@official`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
lfx_serpingapi/__init__.py,sha256=V21xnObdW36K33lDOBtxd76w8_VSu21Qh3Q5VVb-FJ8,677
|
|
2
|
+
lfx_serpingapi/extension.json,sha256=xVpiUGUFfItir6z60HPj2InWGuGaY_EebszTTziu9yY,381
|
|
3
|
+
lfx_serpingapi/components/serpingapi/__init__.py,sha256=W0iZQhQxVWg2P_OOGpMw-zdSkFMI6hW0paFhxWJJ4KA,98
|
|
4
|
+
lfx_serpingapi/components/serpingapi/serpingapi_search.py,sha256=QXobb-9CkGsf-mAGPdl0COLgxjEJxpO6uHk9pXpcOK4,6671
|
|
5
|
+
lfx_serpingapi-0.1.0.dist-info/METADATA,sha256=xGXCdAWMZkYmgZy_BoRGENSN99U60izfjN4Vc_tmuuU,2193
|
|
6
|
+
lfx_serpingapi-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
7
|
+
lfx_serpingapi-0.1.0.dist-info/entry_points.txt,sha256=GhaFOfWCRihPgSutE5n5E7kDmzlxga20O0_o86vMlLA,54
|
|
8
|
+
lfx_serpingapi-0.1.0.dist-info/RECORD,,
|