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.
@@ -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"