langchain-oxylabs 0.1.0__tar.gz
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.
- langchain_oxylabs-0.1.0/LICENSE +21 -0
- langchain_oxylabs-0.1.0/PKG-INFO +77 -0
- langchain_oxylabs-0.1.0/README.md +56 -0
- langchain_oxylabs-0.1.0/langchain_oxylabs/__init__.py +10 -0
- langchain_oxylabs-0.1.0/langchain_oxylabs/_version.py +8 -0
- langchain_oxylabs-0.1.0/langchain_oxylabs/py.typed +0 -0
- langchain_oxylabs-0.1.0/langchain_oxylabs/tools.py +323 -0
- langchain_oxylabs-0.1.0/langchain_oxylabs/utilities.py +688 -0
- langchain_oxylabs-0.1.0/pyproject.toml +69 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Oxylabs, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: langchain-oxylabs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Oxylabs integration package for LangChain
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9,<4.0
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: langchain-core (>=0.3.15,<0.4.0)
|
|
15
|
+
Requires-Dist: oxylabs (>=2.0.0,<3.0.0)
|
|
16
|
+
Project-URL: Repository, https://github.com/oxylabs/langchain-oxylabs
|
|
17
|
+
Project-URL: Release Notes, https://github.com/oxylabs/langchain-oxylabs/releases
|
|
18
|
+
Project-URL: Source Code, https://github.com/oxylabs/langchain-oxylabs
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# langchain-oxylabs
|
|
22
|
+
|
|
23
|
+
This package contains the LangChain integration with Oxylabs, providing tools to scrape Google search results
|
|
24
|
+
with Oxylabs Web Scraper API using LangChain's framework.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install -U langchain-oxylabs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Credentials
|
|
33
|
+
Create your API user credentials: Sign up for a free trial or purchase the product
|
|
34
|
+
in the [Oxylabs dashboard](https://dashboard.oxylabs.io/en/registration)
|
|
35
|
+
to create your API user credentials.
|
|
36
|
+
|
|
37
|
+
Configure your Oxylabs credentials by setting the following environment variables:
|
|
38
|
+
- `OXYLABS_USERNAME` - Your Oxylabs API username
|
|
39
|
+
- `OXYLABS_PASSWORD` - Your Oxylabs API password
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
`langchain_oxylabs` package provides the following classes:
|
|
43
|
+
- `OxylabsSearchRun` - A tool that returns scraped Google search results in a formatted text
|
|
44
|
+
- `OxylabsSearchResults` - A tool that returns scraped Google search results in a JSON format
|
|
45
|
+
- `OxylabsSearchAPIWrapper` - An API wrapper for initializing Oxylabs API
|
|
46
|
+
|
|
47
|
+
Here is an example usage of these classes:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import json
|
|
51
|
+
from langchain_oxylabs import OxylabsSearchRun, OxylabsSearchResults, OxylabsSearchAPIWrapper
|
|
52
|
+
|
|
53
|
+
# Initialize the API wrapper
|
|
54
|
+
oxylabs_wrapper = OxylabsSearchAPIWrapper()
|
|
55
|
+
|
|
56
|
+
# Initialize the search run tool
|
|
57
|
+
run_tool = OxylabsSearchRun(wrapper=oxylabs_wrapper)
|
|
58
|
+
|
|
59
|
+
# Invoke the tool and print results
|
|
60
|
+
results_text = run_tool.invoke({"query": "Visit restaurants in Vilnius."})
|
|
61
|
+
print(results_text)
|
|
62
|
+
|
|
63
|
+
# Initialize the search results tool
|
|
64
|
+
results_tool = OxylabsSearchResults(wrapper=oxylabs_wrapper)
|
|
65
|
+
|
|
66
|
+
# Invoke the tool and print results
|
|
67
|
+
response_results = results_tool.invoke({"query": "Visit restaurants in Paris."})
|
|
68
|
+
response_results = json.loads(response_results)
|
|
69
|
+
for result in response_results:
|
|
70
|
+
for key, value in result.items():
|
|
71
|
+
print(f"{key}: {value}")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
76
|
+
|
|
77
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# langchain-oxylabs
|
|
2
|
+
|
|
3
|
+
This package contains the LangChain integration with Oxylabs, providing tools to scrape Google search results
|
|
4
|
+
with Oxylabs Web Scraper API using LangChain's framework.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install -U langchain-oxylabs
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Credentials
|
|
13
|
+
Create your API user credentials: Sign up for a free trial or purchase the product
|
|
14
|
+
in the [Oxylabs dashboard](https://dashboard.oxylabs.io/en/registration)
|
|
15
|
+
to create your API user credentials.
|
|
16
|
+
|
|
17
|
+
Configure your Oxylabs credentials by setting the following environment variables:
|
|
18
|
+
- `OXYLABS_USERNAME` - Your Oxylabs API username
|
|
19
|
+
- `OXYLABS_PASSWORD` - Your Oxylabs API password
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
`langchain_oxylabs` package provides the following classes:
|
|
23
|
+
- `OxylabsSearchRun` - A tool that returns scraped Google search results in a formatted text
|
|
24
|
+
- `OxylabsSearchResults` - A tool that returns scraped Google search results in a JSON format
|
|
25
|
+
- `OxylabsSearchAPIWrapper` - An API wrapper for initializing Oxylabs API
|
|
26
|
+
|
|
27
|
+
Here is an example usage of these classes:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import json
|
|
31
|
+
from langchain_oxylabs import OxylabsSearchRun, OxylabsSearchResults, OxylabsSearchAPIWrapper
|
|
32
|
+
|
|
33
|
+
# Initialize the API wrapper
|
|
34
|
+
oxylabs_wrapper = OxylabsSearchAPIWrapper()
|
|
35
|
+
|
|
36
|
+
# Initialize the search run tool
|
|
37
|
+
run_tool = OxylabsSearchRun(wrapper=oxylabs_wrapper)
|
|
38
|
+
|
|
39
|
+
# Invoke the tool and print results
|
|
40
|
+
results_text = run_tool.invoke({"query": "Visit restaurants in Vilnius."})
|
|
41
|
+
print(results_text)
|
|
42
|
+
|
|
43
|
+
# Initialize the search results tool
|
|
44
|
+
results_tool = OxylabsSearchResults(wrapper=oxylabs_wrapper)
|
|
45
|
+
|
|
46
|
+
# Invoke the tool and print results
|
|
47
|
+
response_results = results_tool.invoke({"query": "Visit restaurants in Paris."})
|
|
48
|
+
response_results = json.loads(response_results)
|
|
49
|
+
for result in response_results:
|
|
50
|
+
for key, value in result.items():
|
|
51
|
+
print(f"{key}: {value}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
56
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from langchain_oxylabs.tools import OxylabsSearchRun, OxylabsSearchResults
|
|
2
|
+
from langchain_oxylabs.utilities import OxylabsSearchAPIWrapper
|
|
3
|
+
from langchain_oxylabs._version import __version__
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"OxylabsSearchRun",
|
|
7
|
+
"OxylabsSearchResults",
|
|
8
|
+
"OxylabsSearchAPIWrapper",
|
|
9
|
+
"__version__",
|
|
10
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from importlib import metadata
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
__version__ = metadata.version(__package__)
|
|
5
|
+
except metadata.PackageNotFoundError:
|
|
6
|
+
# Case where package metadata is not available.
|
|
7
|
+
__version__ = ""
|
|
8
|
+
del metadata # optional, avoids polluting the results of dir(__package__)
|
|
File without changes
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Tool for the Oxylabs Search API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional, Type
|
|
5
|
+
from langchain_core.tools import BaseTool
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
from langchain_oxylabs.utilities import OxylabsSearchAPIWrapper
|
|
8
|
+
from langchain_core.callbacks import (
|
|
9
|
+
AsyncCallbackManagerForToolRun,
|
|
10
|
+
CallbackManagerForToolRun,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OxylabsSearchQueryInput(BaseModel):
|
|
15
|
+
"""Input for the OxylabsSearch tool."""
|
|
16
|
+
|
|
17
|
+
query: str = Field(description="query to retrieve on Oxylabs Search API")
|
|
18
|
+
geo_location: Optional[str] = Field(
|
|
19
|
+
default="California,United States",
|
|
20
|
+
description="Geographic location for the search;"
|
|
21
|
+
" adjust if location-specific information is requested.",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OxylabsSearchRun(BaseTool): # type: ignore[override, override]
|
|
26
|
+
"""Oxylabs Search Run tool.
|
|
27
|
+
|
|
28
|
+
Setup:
|
|
29
|
+
Install ``langchain-oxylabs``,
|
|
30
|
+
and set environment variables ``OXYLABS_USERNAME`` and ``OXYLABS_PASSWORD``.
|
|
31
|
+
|
|
32
|
+
.. code-block:: bash
|
|
33
|
+
|
|
34
|
+
pip install langchain-oxylabs
|
|
35
|
+
export OXYLABS_USERNAME="your-oxylabs-username"
|
|
36
|
+
export OXYLABS_PASSWORD="your-oxylabs-password"
|
|
37
|
+
|
|
38
|
+
Instantiation:
|
|
39
|
+
.. code-block:: python
|
|
40
|
+
|
|
41
|
+
from langchain_oxylabs import OxylabsSearchRun, OxylabsSearchAPIWrapper
|
|
42
|
+
|
|
43
|
+
oxylabs_wrapper = OxylabsSearchAPIWrapper()
|
|
44
|
+
tool = OxylabsSearchRun(wrapper=oxylabs_wrapper)
|
|
45
|
+
|
|
46
|
+
Invocation with args:
|
|
47
|
+
.. code-block:: python
|
|
48
|
+
|
|
49
|
+
tool_.invoke({"query": "Visit restaurants in Vilnius."})
|
|
50
|
+
|
|
51
|
+
Invocation with ToolCall:
|
|
52
|
+
|
|
53
|
+
.. code-block:: python
|
|
54
|
+
|
|
55
|
+
tool = OxylabsSearchRun(wrapper=oxylabs_wrapper, kwargs={"result_categories": ["local_information", "combined_search_result"]})
|
|
56
|
+
model_generated_tool_call = {"args": {"query": "Visit restaurants in Vilnius.", "geo_location": "Vilnius,Lithuania"}, "id": "1", "name": "oxylabs_search", "type": "tool_call",}
|
|
57
|
+
tool.invoke(model_generated_tool_call)
|
|
58
|
+
|
|
59
|
+
.. code-block:: python
|
|
60
|
+
|
|
61
|
+
ToolMessage(
|
|
62
|
+
(' LOCAL PACK: \n'
|
|
63
|
+
' ITEMS ITEMS: \n'
|
|
64
|
+
' ITEMS-ITEM-1: \n'
|
|
65
|
+
' CID: 13950149882539119249\n'
|
|
66
|
+
' POS: 1\n'
|
|
67
|
+
' TITLE: Etno Dvaras\n'
|
|
68
|
+
' RATING: 4.5\n'
|
|
69
|
+
' ADDRESS: Lithuanian\n'
|
|
70
|
+
' ITEMS-ITEM-2: \n'
|
|
71
|
+
' CID: 711702509070991018\n'
|
|
72
|
+
' POS: 2\n'
|
|
73
|
+
' TITLE: Lokys\n'
|
|
74
|
+
' RATING: 4.5\n'
|
|
75
|
+
' ADDRESS: Lithuanian\n'
|
|
76
|
+
' ITEMS-ITEM-3: \n'
|
|
77
|
+
' CID: 7630589473191639738\n'
|
|
78
|
+
' POS: 3\n'
|
|
79
|
+
' TITLE: Senoji trobelė\n'
|
|
80
|
+
' RATING: 4.4\n'
|
|
81
|
+
' ADDRESS: Lithuanian\n'
|
|
82
|
+
'\n'
|
|
83
|
+
' ORGANIC RESULTS ITEMS: \n'
|
|
84
|
+
' ORGANIC-ITEM-1: \n'
|
|
85
|
+
' POS: 1\n'
|
|
86
|
+
' URL: '
|
|
87
|
+
'https://www.tripadvisor.com/Restaurants-g274951-Vilnius_Vilnius_County.html\n'
|
|
88
|
+
' DESC: Some of the best restaurants in Vilnius for families with children '
|
|
89
|
+
'include: Momo grill Vilnius · Jurgis ir Drakonas Ogmios · RoseHip Vegan\xa0'
|
|
90
|
+
'...\n'
|
|
91
|
+
' TITLE: THE 10 BEST Restaurants in Vilnius (Updated November ...\n'
|
|
92
|
+
' SITELINKS: \n'
|
|
93
|
+
' SITELINKS: \n'
|
|
94
|
+
' INLINE ITEMS: \n'
|
|
95
|
+
' INLINE-ITEM-1: \n'
|
|
96
|
+
' URL: '
|
|
97
|
+
'https://www.tripadvisor.com/Restaurants-g274951-zfp58-Vilnius_Vilnius_County.html\n'
|
|
98
|
+
' TITLE: Vilnius Dinner Restaurants\n'
|
|
99
|
+
' INLINE-ITEM-2: \n'
|
|
100
|
+
' URL: '
|
|
101
|
+
'https://www.tripadvisor.com/Restaurants-g274951-zfp2-Vilnius_Vilnius_County.html\n'
|
|
102
|
+
' TITLE: Vilnius Breakfast Restaurants\n'
|
|
103
|
+
' INLINE-ITEM-3: \n'
|
|
104
|
+
' URL: '
|
|
105
|
+
'https://www.tripadvisor.com/Restaurants-g274951-c8-Vilnius_Vilnius_County.html\n'
|
|
106
|
+
' TITLE: Cafés in Vilnius\n'
|
|
107
|
+
' URL_SHOWN: https://www.tripadvisor.com› ... › Vilnius\n'
|
|
108
|
+
' FAVICON_TEXT: Tripadvisor\n'
|
|
109
|
+
' ORGANIC-ITEM-2: \n'
|
|
110
|
+
' POS: 2\n'
|
|
111
|
+
' URL: '
|
|
112
|
+
'https://theweek.com/culture-life/food-drink/foodie-guide-to-vilnius-lithuania\n'
|
|
113
|
+
" DESC: Jun 24, 2024 — Lithuania's capital has established itself as an "
|
|
114
|
+
"affordable culinary hotspot as four of the city's restaurants awarded "
|
|
115
|
+
'Michelin stars.\n'
|
|
116
|
+
' TITLE: Star quality: a foodie guide to Vilnius\n'
|
|
117
|
+
' URL_SHOWN: https://theweek.com› Culture & Life › Food & Drink\n'
|
|
118
|
+
' FAVICON_TEXT: The Week\n'
|
|
119
|
+
' ...
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
""" # noqa: E501
|
|
123
|
+
|
|
124
|
+
name: str = "oxylabs_search"
|
|
125
|
+
description: str = (
|
|
126
|
+
"A meta search engine."
|
|
127
|
+
"Ideal for situations where you need to answer questions about current events,"
|
|
128
|
+
"facts, products, recipes, local information, and other topics "
|
|
129
|
+
"that can be explored via web browsing. "
|
|
130
|
+
"The input should be a search query and, if applicable,"
|
|
131
|
+
" a geo_location string to enhance result accuracy. "
|
|
132
|
+
"The output is a compiled, formatted summary of query results. "
|
|
133
|
+
)
|
|
134
|
+
wrapper: OxylabsSearchAPIWrapper
|
|
135
|
+
kwargs: dict = Field(default_factory=dict)
|
|
136
|
+
args_schema: Type[BaseModel] = OxylabsSearchQueryInput
|
|
137
|
+
|
|
138
|
+
def _run(
|
|
139
|
+
self,
|
|
140
|
+
query: str,
|
|
141
|
+
geo_location: Optional[str] = "",
|
|
142
|
+
# run_manager is added to enable tracing
|
|
143
|
+
run_manager: Optional[CallbackManagerForToolRun] = None
|
|
144
|
+
) -> str:
|
|
145
|
+
"""Use the tool."""
|
|
146
|
+
|
|
147
|
+
kwargs_ = {**self.kwargs}
|
|
148
|
+
kwargs_.update(
|
|
149
|
+
{
|
|
150
|
+
"geo_location": geo_location,
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return self.wrapper.run(query, **kwargs_)
|
|
155
|
+
|
|
156
|
+
async def _arun(
|
|
157
|
+
self,
|
|
158
|
+
query: str,
|
|
159
|
+
geo_location: Optional[str] = "",
|
|
160
|
+
# run_manager is added to enable tracing
|
|
161
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None
|
|
162
|
+
) -> str:
|
|
163
|
+
"""Use the tool asynchronously."""
|
|
164
|
+
|
|
165
|
+
kwargs_ = {**self.kwargs}
|
|
166
|
+
kwargs_.update(
|
|
167
|
+
{
|
|
168
|
+
"geo_location": geo_location,
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return await self.wrapper.arun(query, **kwargs_)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class OxylabsSearchResults(BaseTool): # type: ignore[override, override]
|
|
176
|
+
"""Oxylabs Search Results tool.
|
|
177
|
+
|
|
178
|
+
Setup:
|
|
179
|
+
Install ``langchain-oxylabs``,
|
|
180
|
+
and set environment variables ``OXYLABS_USERNAME`` and ``OXYLABS_PASSWORD``.
|
|
181
|
+
|
|
182
|
+
.. code-block:: bash
|
|
183
|
+
|
|
184
|
+
pip install langchain-oxylabs
|
|
185
|
+
export OXYLABS_USERNAME="your-oxylabs-username"
|
|
186
|
+
export OXYLABS_PASSWORD="your-oxylabs-password"
|
|
187
|
+
|
|
188
|
+
Instantiation:
|
|
189
|
+
.. code-block:: python
|
|
190
|
+
|
|
191
|
+
from langchain_oxylabs import OxylabsSearchRun, OxylabsSearchAPIWrapper
|
|
192
|
+
|
|
193
|
+
oxylabs_wrapper = OxylabsSearchAPIWrapper()
|
|
194
|
+
tool = OxylabsSearchResults(wrapper=oxylabs_wrapper)
|
|
195
|
+
|
|
196
|
+
Invocation with args:
|
|
197
|
+
.. code-block:: python
|
|
198
|
+
|
|
199
|
+
response_results = tool.invoke({"query": "Visit restaurants in Vilnius."})
|
|
200
|
+
response_results = json.loads(response_results)
|
|
201
|
+
for item in response_results:
|
|
202
|
+
print(item)
|
|
203
|
+
|
|
204
|
+
Invocation with ToolCall:
|
|
205
|
+
|
|
206
|
+
.. code-block:: python
|
|
207
|
+
|
|
208
|
+
model_generated_tool_call = {"args": {"query": "Visit restaurants in Vilnius.", "geo_location": "Vilnius,Lithuania"}, "id": "1", "name": "oxylabs_search", "type": "tool_call",}
|
|
209
|
+
tool.invoke(model_generated_tool_call)
|
|
210
|
+
|
|
211
|
+
.. code-block:: python
|
|
212
|
+
|
|
213
|
+
ToolMessage('[{"paid": [], "organic": [{"pos": 1, "url": '
|
|
214
|
+
'"https://www.tripadvisor.com/Restaurants-g274951-Vilnius_Vilnius_County.html", '
|
|
215
|
+
'"desc": "Some of the best restaurants in Vilnius for families with children '
|
|
216
|
+
'include: Momo grill Vilnius \\u00b7 Jurgis ir Drakonas Ogmios \\u00b7 '
|
|
217
|
+
'RoseHip Vegan\\u00a0...", "title": "THE 10 BEST Restaurants in Vilnius '
|
|
218
|
+
'(Updated November ...", "sitelinks": {"inline": [{"url": '
|
|
219
|
+
'"https://www.tripadvisor.com/Restaurants-g274951-zfp58-Vilnius_Vilnius_County.html", '
|
|
220
|
+
'"title": "Vilnius Dinner Restaurants"}, {"url": '
|
|
221
|
+
'"https://www.tripadvisor.com/Restaurants-g274951-zfp2-Vilnius_Vilnius_County.html", '
|
|
222
|
+
'"title": "Vilnius Breakfast Restaurants"}, {"url": '
|
|
223
|
+
'"https://www.tripadvisor.com/Restaurants-g274951-c8-Vilnius_Vilnius_County.html", '
|
|
224
|
+
'"title": "Caf\\u00e9s in Vilnius"}]}, "url_shown": '
|
|
225
|
+
'"https://www.tripadvisor.com\\u203a ... \\u203a Vilnius", "pos_overall": 1, '
|
|
226
|
+
'"favicon_text": "Tripadvisor"}, {"pos": 2, "url": '
|
|
227
|
+
'"https://www.amsterdamfoodie.nl/2022/foodie-guide-to-vilnius-lithuania/", '
|
|
228
|
+
'"desc": "Dec 2, 2022 \\u2014 Vilnius takes the top spot for most creative '
|
|
229
|
+
'dining with the fabulous restaurant Amandus. Modern gastronomy, with a nod '
|
|
230
|
+
'to Lithuanian tradition, goes hand in\\u00a0...", "title": "A Foodie\'s '
|
|
231
|
+
'Guide to Vilnius, Lithuania", "url_shown": '
|
|
232
|
+
'"https://www.amsterdamfoodie.nl\\u203a blog", "pos_overall": 2, '
|
|
233
|
+
'"favicon_text": "Amsterdam Foodie"}, {"pos": 3, "url": '
|
|
234
|
+
'"https://www.reddit.com/r/Vilnius/comments/uudd1p/good_restaurants_street_food_in_vilnius/", '
|
|
235
|
+
'"desc": "- Alin\\u0117 Lei\\u010diai - also traditional peasant food of '
|
|
236
|
+
'history but in a more upscale environment, and more expensive, table service '
|
|
237
|
+
'and more diverse\\u00a0...", "title": "Good restaurants & street food in '
|
|
238
|
+
'Vilnius?", "url_shown": "10+ comments \\u00b7 2 years ago", "pos_overall": '
|
|
239
|
+
'3, "favicon_text": "Reddit\\u00a0\\u00b7\\u00a0r/Vilnius"}, {"pos": 4, '
|
|
240
|
+
'"url": '
|
|
241
|
+
'"https://theweek.com/culture-life/food-drink/foodie-guide-to-vilnius-lithuania", '
|
|
242
|
+
'"desc": "Jun 24, 2024 \\u2014 Lithuania\'s capital has established itself as '
|
|
243
|
+
"an affordable culinary hotspot as four of the city's restaurants awarded "
|
|
244
|
+
'Michelin stars.", "title": "Star quality: a foodie guide to Vilnius", '
|
|
245
|
+
'"sitelinks": {"inline": [{"url": '
|
|
246
|
+
'"https://theweek.com/culture-life/food-drink/foodie-guide-to-vilnius-lithuania#:~:text=Nineteen18,-With%20its%20%60%60sleek%20and", '
|
|
247
|
+
'"title": "Nineteen18"}, {"url": '
|
|
248
|
+
'"https://theweek.com/culture-life/food-drink/foodie-guide-to-vilnius-lithuania#:~:text=D%C5%BEiaugsmas,-D%C5%BEiaugsmas", '
|
|
249
|
+
'"title": "D\\u017eiaugsmas"}, {"url": '
|
|
250
|
+
'"https://theweek.com/culture-life/food-drink/foodie-guide-to-vilnius-lithuania#:~:text=Gaspar%27s", '
|
|
251
|
+
'"title": "Gaspar\'s"}]}, "url_shown": "https://theweek.com\\u203a Culture & '
|
|
252
|
+
'Life \\u203a Food & Drink", "pos_overall": 4, "favicon_text": "The Week"}], '
|
|
253
|
+
'"local_pack": {"items": [{"cid": "13950149882539119249", "pos": 1, "title": '
|
|
254
|
+
'"Etno Dvaras", "rating": 4.5, "address": "Lithuanian", "rating_count": 0}, '
|
|
255
|
+
'{"cid": "7630589473191639738", "pos": 2, "title": "Senoji trobel\\u0117", '
|
|
256
|
+
'"rating": 4.4, "address": "Lithuanian", "rating_count": 0}, {"cid": '
|
|
257
|
+
'"6362402554387935672", "pos": 3, "title": "Ertlio Namas", "rating": 4.7, '
|
|
258
|
+
'"address": "\\u0160v. Jono g. 7", "subtitle": "Lithuanian", "rating_count": '
|
|
259
|
+
'0}], "pos_overall": 5}, "related_searches": {"pos_overall": 6, '
|
|
260
|
+
'"related_searches": ["Best restaurants in Vilnius old town", "Unique places '
|
|
261
|
+
'to eat in Vilnius", "Best visit restaurants in vilnius", "Best Lithuanian '
|
|
262
|
+
'restaurants in Vilnius", "Restaurants Vilnius old Town", "Vilnius '
|
|
263
|
+
'restaurants Michelin", "Fine dining Vilnius", "Tripadvisor Vilnius '
|
|
264
|
+
'Restaurants"]}, "search_information": {"query": "Visit restaurants in '
|
|
265
|
+
'Vilnius.", "geo_location": "United States", "showing_results_for": "Visit '
|
|
266
|
+
'restaurants in Vilnius.", "total_results_count": 1260000}, '
|
|
267
|
+
'"total_results_count": 1260000}]'
|
|
268
|
+
)
|
|
269
|
+
""" # noqa: E501
|
|
270
|
+
|
|
271
|
+
name: str = "oxylabs_search_results"
|
|
272
|
+
description: str = (
|
|
273
|
+
"A meta search engine."
|
|
274
|
+
"Ideal for situations where you need to answer questions about current events,"
|
|
275
|
+
"facts, products, recipes, local information, and other topics "
|
|
276
|
+
"that can be explored via web browsing. "
|
|
277
|
+
"The input should be a search query and, if applicable,"
|
|
278
|
+
" a geo_location string to enhance result accuracy. "
|
|
279
|
+
"The output is a JSON array of response page objects. "
|
|
280
|
+
)
|
|
281
|
+
wrapper: OxylabsSearchAPIWrapper
|
|
282
|
+
kwargs: dict = Field(default_factory=dict)
|
|
283
|
+
args_schema: Type[BaseModel] = OxylabsSearchQueryInput
|
|
284
|
+
|
|
285
|
+
model_config = ConfigDict(
|
|
286
|
+
extra="allow",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def _run(
|
|
290
|
+
self,
|
|
291
|
+
query: str,
|
|
292
|
+
geo_location: Optional[str] = "",
|
|
293
|
+
# run_manager is added to enable tracing
|
|
294
|
+
run_manager: Optional[CallbackManagerForToolRun] = None
|
|
295
|
+
) -> str:
|
|
296
|
+
"""Use the tool."""
|
|
297
|
+
|
|
298
|
+
kwargs_ = {**self.kwargs}
|
|
299
|
+
kwargs_.update(
|
|
300
|
+
{
|
|
301
|
+
"geo_location": geo_location,
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return json.dumps(self.wrapper.results(query, **kwargs_))
|
|
306
|
+
|
|
307
|
+
async def _arun(
|
|
308
|
+
self,
|
|
309
|
+
query: str,
|
|
310
|
+
geo_location: Optional[str] = "",
|
|
311
|
+
# run_manager is added to enable tracing
|
|
312
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None
|
|
313
|
+
) -> str:
|
|
314
|
+
"""Use the tool asynchronously."""
|
|
315
|
+
|
|
316
|
+
kwargs_ = {**self.kwargs}
|
|
317
|
+
kwargs_.update(
|
|
318
|
+
{
|
|
319
|
+
"geo_location": geo_location,
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return json.dumps(await self.wrapper.aresults(query, **kwargs_))
|
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
"""Chain that calls Oxylabs API for Google Search."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
from platform import python_version, architecture
|
|
7
|
+
from oxylabs import RealtimeClient # type: ignore[import-untyped]
|
|
8
|
+
from langchain_core.utils import get_from_dict_or_env
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
10
|
+
from langchain_oxylabs._version import __version__
|
|
11
|
+
|
|
12
|
+
RESULT_CATEGORIES = [
|
|
13
|
+
"knowledge_graph",
|
|
14
|
+
"combined_search_result",
|
|
15
|
+
"product_information",
|
|
16
|
+
"local_information",
|
|
17
|
+
"search_information",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
IMAGE_BINARY_CONTENT_ARRAY_ATTRIBUTE = "images"
|
|
21
|
+
BINARY_CONTENT_REPLACEMENT = "Redacted base64 image string..."
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ResponseElement:
|
|
26
|
+
tag: str
|
|
27
|
+
display_tag: str
|
|
28
|
+
path_: str
|
|
29
|
+
python_type: str
|
|
30
|
+
parent: Optional["ResponseElement"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_default_excluded_result_attributes() -> List[str]:
|
|
34
|
+
return ["pos_overall"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_default_image_content_attributes() -> List[str]:
|
|
38
|
+
return ["image_data", "data"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_default_params() -> dict:
|
|
42
|
+
"""Provide default parameters for OxylabsSearchAPIWrapper.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
dict: Default parameters, including the following keys:
|
|
46
|
+
- source (str): Source of the search engine, e.g., "google_search".
|
|
47
|
+
- user_agent_type (str): User agent type, e.g.,
|
|
48
|
+
"desktop". Can be set using values from `oxylabs.utils.types`.
|
|
49
|
+
- render (str): Render type for the search results, e.g.,
|
|
50
|
+
"html". Can be set using values from `oxylabs.utils.types`.
|
|
51
|
+
- domain (str): Domain for the search engine, e.g., "com".
|
|
52
|
+
- start_page (int): Starting page number for search results.
|
|
53
|
+
- pages (int): Number of pages to retrieve.
|
|
54
|
+
- limit (int): Maximum number of results to return.
|
|
55
|
+
- parse (bool): Whether to enable result parsing into an object.
|
|
56
|
+
- locale (str): Locale or location for the search.
|
|
57
|
+
- geo_location (str): Geographic location for the search.
|
|
58
|
+
- parsing_instructions (dict): Additional instructions for parsing
|
|
59
|
+
the search results.
|
|
60
|
+
- context (list): Search context information.
|
|
61
|
+
- request_timeout (int): Timeout for the Oxylabs service, in seconds.
|
|
62
|
+
- result_categories (list): Specifies the desired categories for the
|
|
63
|
+
results from the available:
|
|
64
|
+
`knowledge_graph` which includes Oxylabs results categories:
|
|
65
|
+
`knowledge`, `recipes`, `carousel`, `apps`.
|
|
66
|
+
`combined_search_result` which includes Oxylabs results categories:
|
|
67
|
+
`organic`, `paid`, `organic_videos`, `featured_snippet`,
|
|
68
|
+
`top_stories`, `finance`, `sports_games`, `twitter`,
|
|
69
|
+
`discussions_and_forums`, `images`, `videos`, `video_box`.
|
|
70
|
+
`product_information` which includes Oxylabs results categories:
|
|
71
|
+
`popular_products`, `pla`.
|
|
72
|
+
`local_information` which includes Oxylabs results categories:
|
|
73
|
+
`top_sights`, `flights`, `hotels`, `local_pack`,
|
|
74
|
+
`local_service_ads`, `jobs`.
|
|
75
|
+
`search_information` which includes Oxylabs results categories:
|
|
76
|
+
`search_information`, `related_searches`,
|
|
77
|
+
`related_searches_categorized`, `related_questions`.
|
|
78
|
+
The list preserves the order of the categories,
|
|
79
|
+
allowing prioritized filtering. If left empty (default),
|
|
80
|
+
results are returned from all available categories without filtering.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"source": "google_search",
|
|
85
|
+
"user_agent_type": "desktop",
|
|
86
|
+
"render": "html",
|
|
87
|
+
"domain": "com",
|
|
88
|
+
"start_page": 1,
|
|
89
|
+
"pages": 1,
|
|
90
|
+
"limit": 5,
|
|
91
|
+
"parse": True,
|
|
92
|
+
"locale": "",
|
|
93
|
+
"geo_location": "",
|
|
94
|
+
"parsing_instructions": {},
|
|
95
|
+
"context": [],
|
|
96
|
+
"request_timeout": 165,
|
|
97
|
+
"result_categories": [],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class OxylabsSearchAPIWrapper(BaseModel):
|
|
102
|
+
"""Wrapper class for OxylabsSearchAPI.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
```python
|
|
106
|
+
from langchain_oxylabs import OxylabsSearchAPIWrapper
|
|
107
|
+
|
|
108
|
+
oxylabs_api = OxylabsSearchAPIWrapper(
|
|
109
|
+
"oxylabs_username": <OXYLABS_USERNAME>,
|
|
110
|
+
"oxylabs_password": <OXYLABS_PASSWORD>,
|
|
111
|
+
params={
|
|
112
|
+
"source": "google_search",
|
|
113
|
+
"user_agent_type": "desktop",
|
|
114
|
+
"render": "html",
|
|
115
|
+
"domain": "com",
|
|
116
|
+
"start_page": 1,
|
|
117
|
+
"pages": 1,
|
|
118
|
+
"limit": 5,
|
|
119
|
+
"parse": True,
|
|
120
|
+
"locale": "",
|
|
121
|
+
"geo_location": "",
|
|
122
|
+
"parsing_instructions": {},
|
|
123
|
+
"context": [],
|
|
124
|
+
"request_timeout": 165,
|
|
125
|
+
"result_categories": [],
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
search_query = "Oxylabs"
|
|
129
|
+
result = oxylabs_api.run(search_query)
|
|
130
|
+
print(result)
|
|
131
|
+
```
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
include_binary_image_data: Optional[bool] = Field(default=False)
|
|
135
|
+
parsing_recursion_depth: int = Field(default=5)
|
|
136
|
+
|
|
137
|
+
search_engine: Any = None
|
|
138
|
+
params: dict = Field(default_factory=_get_default_params)
|
|
139
|
+
result_categories: Optional[list] = Field(default=[])
|
|
140
|
+
|
|
141
|
+
excluded_result_attributes: List[str] = Field(
|
|
142
|
+
default_factory=_get_default_excluded_result_attributes
|
|
143
|
+
)
|
|
144
|
+
image_binary_content_attributes: List[str] = Field(
|
|
145
|
+
default_factory=_get_default_image_content_attributes
|
|
146
|
+
)
|
|
147
|
+
image_binary_content_array_attribute: str = Field(
|
|
148
|
+
default=IMAGE_BINARY_CONTENT_ARRAY_ATTRIBUTE
|
|
149
|
+
)
|
|
150
|
+
binary_content_replacement: str = Field(default=BINARY_CONTENT_REPLACEMENT)
|
|
151
|
+
|
|
152
|
+
oxylabs_username: Optional[str] = None
|
|
153
|
+
oxylabs_password: Optional[str] = None
|
|
154
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
|
|
155
|
+
|
|
156
|
+
@model_validator(mode="before")
|
|
157
|
+
@classmethod
|
|
158
|
+
def validate_environment(cls, values: Dict) -> Any:
|
|
159
|
+
"""
|
|
160
|
+
Validate that oxylabs username,
|
|
161
|
+
password and python package exists in environment.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
default_params = _get_default_params()
|
|
165
|
+
default_params.update(**values.get("params", {}))
|
|
166
|
+
current_params = default_params
|
|
167
|
+
oxylabs_username = get_from_dict_or_env(
|
|
168
|
+
values, "oxylabs_username", "OXYLABS_USERNAME"
|
|
169
|
+
)
|
|
170
|
+
oxylabs_password = get_from_dict_or_env(
|
|
171
|
+
values, "oxylabs_password", "OXYLABS_PASSWORD"
|
|
172
|
+
)
|
|
173
|
+
if not (oxylabs_username and oxylabs_password):
|
|
174
|
+
raise RuntimeError(
|
|
175
|
+
"Please set up environment variables:"
|
|
176
|
+
" `OXYLABS_USERNAME` and `OXYLABS_PASSWORD`"
|
|
177
|
+
" or provide credentials in the parameters"
|
|
178
|
+
" as `oxylabs_username` and `oxylabs_password`."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
formed_values: Dict[str, Any] = {
|
|
182
|
+
"oxylabs_username": oxylabs_username,
|
|
183
|
+
"oxylabs_password": oxylabs_password,
|
|
184
|
+
"params": dict(current_params),
|
|
185
|
+
"include_binary_image_data": values.get(
|
|
186
|
+
"include_binary_image_data", False
|
|
187
|
+
),
|
|
188
|
+
"parsing_recursion_depth": values.get(
|
|
189
|
+
"parsing_recursion_depth", 5
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
if "result_categories" in formed_values["params"]:
|
|
193
|
+
validated_categories = cls.validate_response_categories(
|
|
194
|
+
formed_values["params"]["result_categories"]
|
|
195
|
+
)
|
|
196
|
+
if validated_categories:
|
|
197
|
+
formed_values["result_categories"] = validated_categories
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
bits, _ = architecture()
|
|
201
|
+
oxylabs_realtime_client = RealtimeClient(
|
|
202
|
+
username=oxylabs_username,
|
|
203
|
+
password=oxylabs_password,
|
|
204
|
+
sdk_type=f"oxylabs-sdk-langchain/{__version__} ({python_version()}; {bits})",
|
|
205
|
+
)
|
|
206
|
+
# The process to set any available provider
|
|
207
|
+
source_ = formed_values["params"]["source"]
|
|
208
|
+
source_provider_map = {
|
|
209
|
+
"google": ["google_search"],
|
|
210
|
+
}
|
|
211
|
+
engine_ = ""
|
|
212
|
+
for engine, sources_ in source_provider_map.items():
|
|
213
|
+
if source_ in sources_:
|
|
214
|
+
engine_ = engine
|
|
215
|
+
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
if engine_:
|
|
219
|
+
oxylabs_realtime_client_by_provider = getattr(
|
|
220
|
+
oxylabs_realtime_client, engine_
|
|
221
|
+
)
|
|
222
|
+
formed_values["search_engine"] = oxylabs_realtime_client_by_provider
|
|
223
|
+
else:
|
|
224
|
+
supported_sources = ", ".join(sum(source_provider_map.values(), []))
|
|
225
|
+
raise NotImplementedError(
|
|
226
|
+
f"Source: `{source_}` is not supported."
|
|
227
|
+
f" Supported sources: {supported_sources}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
except NotImplementedError as exc:
|
|
231
|
+
raise NotImplementedError(f"{exc}") from exc
|
|
232
|
+
|
|
233
|
+
except Exception as exc:
|
|
234
|
+
raise RuntimeError(f"Unknown Oxylabs Python SDK integration error: {exc}") from exc
|
|
235
|
+
|
|
236
|
+
return formed_values
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def validate_response_categories(result_categories: list) -> list:
|
|
240
|
+
validated_categories = []
|
|
241
|
+
for result_category in result_categories:
|
|
242
|
+
if result_category in RESULT_CATEGORIES:
|
|
243
|
+
validated_categories.append(result_category)
|
|
244
|
+
|
|
245
|
+
return validated_categories
|
|
246
|
+
|
|
247
|
+
async def arun(self, query: str, **kwargs: Any) -> str:
|
|
248
|
+
"""
|
|
249
|
+
Run query through OxylabsSearchAPI and parse result async.
|
|
250
|
+
"""
|
|
251
|
+
return self._process_response(await self.aresults(query, **kwargs), **kwargs)
|
|
252
|
+
|
|
253
|
+
def run(self, query: str, **kwargs: Any) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Run query through OxylabsSearchAPI and parse result.
|
|
256
|
+
"""
|
|
257
|
+
return self._process_response(self.results(query, **kwargs), **kwargs)
|
|
258
|
+
|
|
259
|
+
def results(self, query: str, **kwargs: Any) -> List[Dict[str, Any]]:
|
|
260
|
+
"""
|
|
261
|
+
Run query through Oxylabs Web Scrapper API and return SERPResponse object.
|
|
262
|
+
"""
|
|
263
|
+
params_ = self.get_params(**kwargs)
|
|
264
|
+
search_client = self.search_engine
|
|
265
|
+
search_result = search_client.scrape_search(query, **params_)
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
validated_responses = self._validate_response(search_result)
|
|
269
|
+
|
|
270
|
+
except RuntimeError:
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
return validated_responses
|
|
274
|
+
|
|
275
|
+
async def aresults(self, query: str, **kwargs: Any) -> List[Dict[str, Any]]:
|
|
276
|
+
"""
|
|
277
|
+
Run query through Oxylabs Web Scrapper API and return SERPResponse object async.
|
|
278
|
+
"""
|
|
279
|
+
params_ = self.get_params(**kwargs)
|
|
280
|
+
|
|
281
|
+
search_client = self.search_engine
|
|
282
|
+
search_result = await asyncio.to_thread(
|
|
283
|
+
search_client.scrape_search,
|
|
284
|
+
query,
|
|
285
|
+
**params_,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
validated_responses = self._validate_response(search_result)
|
|
290
|
+
|
|
291
|
+
except RuntimeError:
|
|
292
|
+
return []
|
|
293
|
+
|
|
294
|
+
return validated_responses
|
|
295
|
+
|
|
296
|
+
def get_params(self, **kwargs: Any) -> Dict[str, Any]:
|
|
297
|
+
"""
|
|
298
|
+
Get default configuration parameters for OxylabsSearchAPI for scrape_search().
|
|
299
|
+
"""
|
|
300
|
+
wrapper_params_ = ["result_categories"]
|
|
301
|
+
|
|
302
|
+
_params = {
|
|
303
|
+
f"{p_key}": self.params[p_key]
|
|
304
|
+
for p_key in self.params
|
|
305
|
+
if self.params[p_key] and p_key not in wrapper_params_
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for key, value in kwargs.items():
|
|
309
|
+
if key in _params:
|
|
310
|
+
_params[key] = value
|
|
311
|
+
|
|
312
|
+
return _params
|
|
313
|
+
|
|
314
|
+
@staticmethod
|
|
315
|
+
def _validate_response(response: Any) -> List[Dict[Any, Any]]:
|
|
316
|
+
"""
|
|
317
|
+
Validate Oxylabs SERPResponse format and unpack data.
|
|
318
|
+
"""
|
|
319
|
+
validated_results = []
|
|
320
|
+
try:
|
|
321
|
+
result_pages = response.raw["results"]
|
|
322
|
+
if not isinstance(result_pages, list) or not result_pages:
|
|
323
|
+
raise ValueError("No results returned!")
|
|
324
|
+
|
|
325
|
+
for result_page in result_pages:
|
|
326
|
+
result_page = dict(result_page)
|
|
327
|
+
content = result_page["content"]
|
|
328
|
+
if not isinstance(content, dict):
|
|
329
|
+
raise ValueError(
|
|
330
|
+
"Result `content` format error,"
|
|
331
|
+
" try setting parameter `parse` to True"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
unpacked_results = content["results"]
|
|
335
|
+
|
|
336
|
+
if not isinstance(unpacked_results, dict):
|
|
337
|
+
raise ValueError("Response format Error!")
|
|
338
|
+
|
|
339
|
+
if unpacked_results:
|
|
340
|
+
validated_results.append(unpacked_results)
|
|
341
|
+
|
|
342
|
+
return validated_results
|
|
343
|
+
|
|
344
|
+
except (KeyError, IndexError, TypeError, ValueError) as exc:
|
|
345
|
+
raise RuntimeError(f"Response Validation Error: {str(exc)}") from exc
|
|
346
|
+
|
|
347
|
+
def _process_response(self, res: Any, **kwargs: Any) -> str:
|
|
348
|
+
"""
|
|
349
|
+
Process Oxylabs SERPResponse and serialize search results to string.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
result_ = "No good search result found"
|
|
353
|
+
|
|
354
|
+
result_category_processing_map = {
|
|
355
|
+
"knowledge_graph": self._create_knowledge_graph_snippets,
|
|
356
|
+
"combined_search_result": self._create_combined_search_result_snippets,
|
|
357
|
+
"product_information": self._create_product_information_snippets,
|
|
358
|
+
"local_information": self._create_local_information_snippets,
|
|
359
|
+
"search_information": self._create_search_information_snippets,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
snippets: List[str] = []
|
|
363
|
+
validated_categories = self.validate_response_categories(
|
|
364
|
+
kwargs.get("result_categories", [])
|
|
365
|
+
)
|
|
366
|
+
result_categories_ = validated_categories or self.result_categories or []
|
|
367
|
+
|
|
368
|
+
for _, validated_response in enumerate(res):
|
|
369
|
+
if result_categories_:
|
|
370
|
+
for result_category in result_categories_:
|
|
371
|
+
result_category_processing_map[result_category](
|
|
372
|
+
validated_response, snippets
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
for result_category in result_category_processing_map:
|
|
376
|
+
result_category_processing_map[result_category](
|
|
377
|
+
validated_response, snippets
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Combine all snippets
|
|
381
|
+
if snippets:
|
|
382
|
+
result_ = "\n\n".join(snippets)
|
|
383
|
+
|
|
384
|
+
return result_
|
|
385
|
+
|
|
386
|
+
def recursive_snippet_collector(
|
|
387
|
+
self,
|
|
388
|
+
target_structure: Any,
|
|
389
|
+
max_depth: int,
|
|
390
|
+
current_depth: int,
|
|
391
|
+
parent_: ResponseElement,
|
|
392
|
+
) -> str:
|
|
393
|
+
target_snippets: List[str] = []
|
|
394
|
+
|
|
395
|
+
padding_multiplier = current_depth + 1
|
|
396
|
+
recursion_padding = " " * padding_multiplier
|
|
397
|
+
|
|
398
|
+
if current_depth >= max_depth:
|
|
399
|
+
return "\n".join(target_snippets)
|
|
400
|
+
|
|
401
|
+
if isinstance(target_structure, (str, float, int)):
|
|
402
|
+
self.recursion_process_simple_types(
|
|
403
|
+
parent_, recursion_padding, target_snippets, target_structure
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
elif isinstance(target_structure, dict):
|
|
407
|
+
self.recursion_process_dict(
|
|
408
|
+
current_depth,
|
|
409
|
+
max_depth,
|
|
410
|
+
parent_,
|
|
411
|
+
recursion_padding,
|
|
412
|
+
target_snippets,
|
|
413
|
+
target_structure,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
elif isinstance(target_structure, (list, tuple)):
|
|
417
|
+
self.recursion_process_array(
|
|
418
|
+
current_depth,
|
|
419
|
+
max_depth,
|
|
420
|
+
parent_,
|
|
421
|
+
recursion_padding,
|
|
422
|
+
target_snippets,
|
|
423
|
+
target_structure,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
return "\n".join(target_snippets)
|
|
427
|
+
|
|
428
|
+
def recursion_process_array(
|
|
429
|
+
self,
|
|
430
|
+
current_depth: int,
|
|
431
|
+
max_depth: int,
|
|
432
|
+
parent_: ResponseElement,
|
|
433
|
+
recursion_padding: str,
|
|
434
|
+
target_snippets: list,
|
|
435
|
+
target_structure: Any,
|
|
436
|
+
) -> None:
|
|
437
|
+
if target_structure:
|
|
438
|
+
target_snippets.append(
|
|
439
|
+
f"{recursion_padding}{parent_.display_tag.upper()} ITEMS: "
|
|
440
|
+
)
|
|
441
|
+
for nr_, element_ in enumerate(target_structure):
|
|
442
|
+
target_snippets.append(
|
|
443
|
+
self.recursive_snippet_collector(
|
|
444
|
+
element_,
|
|
445
|
+
max_depth=max_depth,
|
|
446
|
+
current_depth=current_depth + 1,
|
|
447
|
+
parent_=ResponseElement(
|
|
448
|
+
path_=f"{parent_.path_.upper()}-ITEM-{nr_ + 1}",
|
|
449
|
+
tag=parent_.tag.upper(),
|
|
450
|
+
display_tag=f"{parent_.tag.upper()}-ITEM-{nr_ + 1}",
|
|
451
|
+
python_type=str(type(target_structure)),
|
|
452
|
+
parent=parent_,
|
|
453
|
+
),
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def recursion_process_dict(
|
|
458
|
+
self,
|
|
459
|
+
current_depth: int,
|
|
460
|
+
max_depth: int,
|
|
461
|
+
parent_: ResponseElement,
|
|
462
|
+
recursion_padding: str,
|
|
463
|
+
target_snippets: list,
|
|
464
|
+
target_structure: Any,
|
|
465
|
+
) -> None:
|
|
466
|
+
if target_structure:
|
|
467
|
+
target_snippets.append(
|
|
468
|
+
f"{recursion_padding}{parent_.display_tag.upper()}: "
|
|
469
|
+
)
|
|
470
|
+
for key_, value_ in target_structure.items():
|
|
471
|
+
if isinstance(value_, dict):
|
|
472
|
+
if value_:
|
|
473
|
+
target_snippets.append(f"{recursion_padding}{key_.upper()}: ")
|
|
474
|
+
target_snippets.append(
|
|
475
|
+
self.recursive_snippet_collector(
|
|
476
|
+
value_,
|
|
477
|
+
max_depth=max_depth,
|
|
478
|
+
current_depth=current_depth + 1,
|
|
479
|
+
parent_=ResponseElement(
|
|
480
|
+
path_=f"{parent_.path_.upper()}-{key_.upper()}",
|
|
481
|
+
tag=key_.upper(),
|
|
482
|
+
display_tag=key_.upper(),
|
|
483
|
+
python_type=str(type(value_)),
|
|
484
|
+
parent=parent_,
|
|
485
|
+
),
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
elif isinstance(value_, (list, tuple)):
|
|
490
|
+
if value_:
|
|
491
|
+
target_snippets.append(
|
|
492
|
+
f"{recursion_padding}{key_.upper()} ITEMS: "
|
|
493
|
+
)
|
|
494
|
+
for nr_, _element in enumerate(value_):
|
|
495
|
+
target_snippets.append(
|
|
496
|
+
self.recursive_snippet_collector(
|
|
497
|
+
_element,
|
|
498
|
+
max_depth=max_depth,
|
|
499
|
+
current_depth=current_depth + 1,
|
|
500
|
+
parent_=ResponseElement(
|
|
501
|
+
path_=f"{parent_.path_.upper()}"
|
|
502
|
+
f"-{key_.upper()}-ITEM-{nr_ + 1}",
|
|
503
|
+
tag=key_.upper(),
|
|
504
|
+
display_tag=f"{key_.upper()}-ITEM-{nr_ + 1}",
|
|
505
|
+
python_type=str(type(value_)),
|
|
506
|
+
parent=parent_,
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
elif isinstance(value_, (str, float, int)):
|
|
512
|
+
if value_:
|
|
513
|
+
if (
|
|
514
|
+
key_ in self.image_binary_content_attributes
|
|
515
|
+
and not self.include_binary_image_data
|
|
516
|
+
):
|
|
517
|
+
value_ = self.binary_content_replacement
|
|
518
|
+
|
|
519
|
+
if key_ not in self.excluded_result_attributes:
|
|
520
|
+
target_snippets.append(
|
|
521
|
+
f"{recursion_padding}{key_.upper()}: {str(value_)}"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def recursion_process_simple_types(
|
|
525
|
+
self,
|
|
526
|
+
parent_: ResponseElement,
|
|
527
|
+
recursion_padding: str,
|
|
528
|
+
target_snippets: list,
|
|
529
|
+
target_structure: Any,
|
|
530
|
+
) -> None:
|
|
531
|
+
if target_structure:
|
|
532
|
+
if parent_.python_type == str(type([])):
|
|
533
|
+
if (
|
|
534
|
+
self.image_binary_content_array_attribute.upper()
|
|
535
|
+
in parent_.path_.split("-")[-3:]
|
|
536
|
+
or parent_.tag.lower() in self.image_binary_content_attributes
|
|
537
|
+
) and not self.include_binary_image_data:
|
|
538
|
+
target_structure = self.binary_content_replacement
|
|
539
|
+
|
|
540
|
+
target_snippets.append(
|
|
541
|
+
f"{recursion_padding}{parent_.display_tag}: {str(target_structure)}"
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
elif parent_.python_type == str(type({})):
|
|
545
|
+
if (
|
|
546
|
+
parent_.tag.lower() in self.image_binary_content_attributes
|
|
547
|
+
and not self.include_binary_image_data
|
|
548
|
+
):
|
|
549
|
+
target_structure = self.binary_content_replacement
|
|
550
|
+
|
|
551
|
+
if parent_.tag.lower() not in self.excluded_result_attributes:
|
|
552
|
+
target_snippets.append(
|
|
553
|
+
f"{recursion_padding}{parent_.display_tag}:"
|
|
554
|
+
f" {str(target_structure)}"
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
def process_tags(
|
|
558
|
+
self, snippets_: list, tags_: list, results: dict, group_name: str = ""
|
|
559
|
+
) -> None:
|
|
560
|
+
check_tags = [tag_[0] in results for tag_ in tags_]
|
|
561
|
+
if any(check_tags):
|
|
562
|
+
for tag in tags_:
|
|
563
|
+
tag_content = results.get(tag[0], {}) or {}
|
|
564
|
+
if tag_content:
|
|
565
|
+
collected_snippets = self.recursive_snippet_collector(
|
|
566
|
+
tag_content,
|
|
567
|
+
max_depth=self.parsing_recursion_depth,
|
|
568
|
+
current_depth=0,
|
|
569
|
+
parent_=ResponseElement(
|
|
570
|
+
path_=f"{group_name}-{tag[0]}",
|
|
571
|
+
tag=tag[0],
|
|
572
|
+
display_tag=tag[1],
|
|
573
|
+
python_type=str((type(tag_content))),
|
|
574
|
+
parent=None,
|
|
575
|
+
),
|
|
576
|
+
)
|
|
577
|
+
if collected_snippets:
|
|
578
|
+
snippets_.append(collected_snippets)
|
|
579
|
+
|
|
580
|
+
def _create_knowledge_graph_snippets(
|
|
581
|
+
self, results: dict, knowledge_graph_snippets: list
|
|
582
|
+
) -> None:
|
|
583
|
+
"""
|
|
584
|
+
Create knowledge graph snippets
|
|
585
|
+
from Oxylabs SERPResponse data search_information tag.
|
|
586
|
+
"""
|
|
587
|
+
|
|
588
|
+
knowledge_graph_tags = [
|
|
589
|
+
("knowledge", "Knowledge Graph"),
|
|
590
|
+
("recipes", "Recipes"),
|
|
591
|
+
("item_carousel", "Item Carousel"),
|
|
592
|
+
("apps", "Apps"),
|
|
593
|
+
]
|
|
594
|
+
self.process_tags(
|
|
595
|
+
knowledge_graph_snippets, knowledge_graph_tags, results, "Knowledge"
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def _create_combined_search_result_snippets(
|
|
599
|
+
self, results: dict, combined_search_result_snippets: list
|
|
600
|
+
) -> None:
|
|
601
|
+
"""
|
|
602
|
+
Create combined search result snippets
|
|
603
|
+
from Oxylabs SERPResponse data search_information tag.
|
|
604
|
+
"""
|
|
605
|
+
|
|
606
|
+
combined_search_result_tags = [
|
|
607
|
+
("organic", "Organic Results"),
|
|
608
|
+
("organic_videos", "Organic Videos"),
|
|
609
|
+
("paid", "Paid Results"),
|
|
610
|
+
("featured_snipped", "Feature Snipped"),
|
|
611
|
+
("top_stories", "Top Stories"),
|
|
612
|
+
("finance", "Finance"),
|
|
613
|
+
("sports_games", "Sports Games"),
|
|
614
|
+
("twitter", "Twitter"),
|
|
615
|
+
("discussions_and_forums", "Discussions and Forums"),
|
|
616
|
+
("images", "Images"),
|
|
617
|
+
("videos", "Videos"),
|
|
618
|
+
("video_box", "Video box"),
|
|
619
|
+
]
|
|
620
|
+
self.process_tags(
|
|
621
|
+
combined_search_result_snippets,
|
|
622
|
+
combined_search_result_tags,
|
|
623
|
+
results,
|
|
624
|
+
"Combined Search Results",
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
def _create_product_information_snippets(
|
|
628
|
+
self, results: dict, product_information_snippets: list
|
|
629
|
+
) -> None:
|
|
630
|
+
"""
|
|
631
|
+
Create product information snippets from
|
|
632
|
+
Oxylabs SERPResponse data search_information tag.
|
|
633
|
+
"""
|
|
634
|
+
|
|
635
|
+
product_information_tags = [
|
|
636
|
+
("popular_products", "Popular Products"),
|
|
637
|
+
("pla", "Product Listing Ads (PLA)"),
|
|
638
|
+
]
|
|
639
|
+
self.process_tags(
|
|
640
|
+
product_information_snippets,
|
|
641
|
+
product_information_tags,
|
|
642
|
+
results,
|
|
643
|
+
"Product Information",
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
def _create_local_information_snippets(
|
|
647
|
+
self, results: dict, local_information_snippets: list
|
|
648
|
+
) -> None:
|
|
649
|
+
"""
|
|
650
|
+
Create local group information snippets
|
|
651
|
+
from Oxylabs SERPResponse data search_information tag.
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
local_information_tags = [
|
|
655
|
+
("top_sights", "Top Sights"),
|
|
656
|
+
("flights", "Flights"),
|
|
657
|
+
("hotels", "Hotels"),
|
|
658
|
+
("local_pack", "Local Pack"),
|
|
659
|
+
("local_service_ads", "Local Service Ads"),
|
|
660
|
+
("jobs", "Jobs"),
|
|
661
|
+
]
|
|
662
|
+
self.process_tags(
|
|
663
|
+
local_information_snippets,
|
|
664
|
+
local_information_tags,
|
|
665
|
+
results,
|
|
666
|
+
"Local Information",
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
def _create_search_information_snippets(
|
|
670
|
+
self, results: dict, search_information_snippets: list
|
|
671
|
+
) -> None:
|
|
672
|
+
"""
|
|
673
|
+
Create search information snippets
|
|
674
|
+
from Oxylabs SERPResponse data search_information tag.
|
|
675
|
+
"""
|
|
676
|
+
|
|
677
|
+
search_information_tags = [
|
|
678
|
+
("search_information", "Search Information"),
|
|
679
|
+
("related_searches", "Related Searches"),
|
|
680
|
+
("related_searches_categorized", "Related Searches Categorized"),
|
|
681
|
+
("related_questions", "Related Questions"),
|
|
682
|
+
]
|
|
683
|
+
self.process_tags(
|
|
684
|
+
search_information_snippets,
|
|
685
|
+
search_information_tags,
|
|
686
|
+
results,
|
|
687
|
+
"Search Information",
|
|
688
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=1.0.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "langchain-oxylabs"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Oxylabs integration package for LangChain"
|
|
9
|
+
authors = []
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
repository = "https://github.com/oxylabs/langchain-oxylabs"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
|
|
14
|
+
[tool.mypy]
|
|
15
|
+
disallow_untyped_defs = "True"
|
|
16
|
+
|
|
17
|
+
[tool.poetry.urls]
|
|
18
|
+
"Source Code" = "https://github.com/oxylabs/langchain-oxylabs"
|
|
19
|
+
"Release Notes" = "https://github.com/oxylabs/langchain-oxylabs/releases"
|
|
20
|
+
|
|
21
|
+
[tool.poetry.dependencies]
|
|
22
|
+
python = ">=3.9,<4.0"
|
|
23
|
+
langchain-core = "^0.3.15"
|
|
24
|
+
oxylabs = "^2.0.0"
|
|
25
|
+
|
|
26
|
+
[tool.ruff.lint]
|
|
27
|
+
select = ["E", "F", "I", "T201"]
|
|
28
|
+
|
|
29
|
+
[tool.coverage.run]
|
|
30
|
+
omit = ["tests/*"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
addopts = "--strict-markers --strict-config --durations=5"
|
|
34
|
+
asyncio_mode = "auto"
|
|
35
|
+
|
|
36
|
+
[tool.poetry.group.test]
|
|
37
|
+
optional = true
|
|
38
|
+
|
|
39
|
+
[tool.poetry.group.codespell]
|
|
40
|
+
optional = true
|
|
41
|
+
|
|
42
|
+
[tool.poetry.group.test_integration]
|
|
43
|
+
optional = true
|
|
44
|
+
|
|
45
|
+
[tool.poetry.group.lint]
|
|
46
|
+
optional = true
|
|
47
|
+
|
|
48
|
+
[tool.poetry.group.dev]
|
|
49
|
+
optional = true
|
|
50
|
+
|
|
51
|
+
[tool.poetry.group.dev.dependencies]
|
|
52
|
+
|
|
53
|
+
[tool.poetry.group.test.dependencies]
|
|
54
|
+
pytest = "^7.4.3"
|
|
55
|
+
pytest-asyncio = "^0.23.2"
|
|
56
|
+
pytest-socket = "^0.7.0"
|
|
57
|
+
pytest-watcher = "^0.3.4"
|
|
58
|
+
langchain-tests = "^0.3.5"
|
|
59
|
+
|
|
60
|
+
[tool.poetry.group.codespell.dependencies]
|
|
61
|
+
codespell = "^2.2.6"
|
|
62
|
+
|
|
63
|
+
[tool.poetry.group.test_integration.dependencies]
|
|
64
|
+
|
|
65
|
+
[tool.poetry.group.lint.dependencies]
|
|
66
|
+
ruff = "^0.5"
|
|
67
|
+
|
|
68
|
+
[tool.poetry.group.typing.dependencies]
|
|
69
|
+
mypy = "^1.10"
|