serpex-haystack 1.0.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,56 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+
5
+ # Distribution / packaging
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ pip-wheel-metadata/
20
+ share/python-wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # Virtual environments
27
+ venv/
28
+ ENV/
29
+ env/
30
+ .venv
31
+
32
+ # IDE
33
+ .vscode/
34
+ .idea/
35
+ *.swp
36
+ *.swo
37
+ *~
38
+
39
+ # Testing
40
+ .pytest_cache/
41
+ .coverage
42
+ htmlcov/
43
+ .tox/
44
+
45
+ # Mypy
46
+ .mypy_cache/
47
+ .dmypy.json
48
+ dmypy.json
49
+
50
+ # Environment variables
51
+ .env
52
+ .env.local
53
+
54
+ # OS
55
+ .DS_Store
56
+ Thumbs.db
@@ -0,0 +1,176 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Support. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
@@ -0,0 +1,285 @@
1
+ Metadata-Version: 2.4
2
+ Name: serpex-haystack
3
+ Version: 1.0.0
4
+ Summary: Haystack integration for Serpex web search - supporting Google, Bing, DuckDuckGo, Brave, Yahoo, and Yandex
5
+ Project-URL: Homepage, https://serpex.dev
6
+ Project-URL: Documentation, https://docs.serpex.dev
7
+ Project-URL: Repository, https://github.com/divyeshradadiya/serpex-haystack
8
+ Project-URL: Issues, https://github.com/divyeshradadiya/serpex-haystack/issues
9
+ Author-email: Divyesh Radadiya <divyeshradadiya0@gmail.com>
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: ai,bing-search,duckduckgo,google-search,haystack,llm,nlp,search-api,serpex,web-search
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Requires-Python: >=3.8
26
+ Requires-Dist: haystack-ai>=2.0.0
27
+ Requires-Dist: httpx>=0.24.0
28
+ Requires-Dist: tenacity>=8.0.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: black>=23.0.0; extra == 'dev'
31
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
32
+ Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
35
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
36
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # Serpex Haystack Integration
40
+
41
+ [![PyPI - Version](https://img.shields.io/pypi/v/serpex-haystack.svg)](https://pypi.org/project/serpex-haystack)
42
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/serpex-haystack.svg)](https://pypi.org/project/serpex-haystack)
43
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
44
+ [![CI Tests](https://github.com/divyeshradadiya/serpex-haystack/actions/workflows/ci.yml/badge.svg)](https://github.com/divyeshradadiya/serpex-haystack/actions/workflows/ci.yml)
45
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
46
+
47
+ [Serpex](https://serpex.dev) integration for [Haystack](https://haystack.deepset.ai/) - bringing powerful multi-engine web search capabilities to your Haystack pipelines.
48
+
49
+ ## Overview
50
+
51
+ Serpex is a unified web search API that provides access to multiple search engines including Google, Bing, DuckDuckGo, Brave, Yahoo, and Yandex. This integration allows you to seamlessly incorporate web search results into your Haystack RAG (Retrieval-Augmented Generation) pipelines and AI applications.
52
+
53
+ ### Key Features
54
+
55
+ - 🔍 **Multi-Engine Support**: Switch between Google, Bing, DuckDuckGo, Brave, Yahoo, and Yandex
56
+ - ⚡ **High Performance**: Fast and reliable API with automatic retries
57
+ - 🎯 **Rich Results**: Get organic search results with titles, snippets, and URLs
58
+ - 🕒 **Time Filters**: Filter results by day, week, month, or year
59
+ - 🔒 **Type-Safe**: Fully typed with comprehensive type hints
60
+ - 📝 **Haystack Native**: Seamless integration with Haystack 2.0+ components
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install serpex-haystack
66
+ ```
67
+
68
+ ## Quick Start
69
+
70
+ ### Get Your API Key
71
+
72
+ Sign up at [Serpex.dev](https://serpex.dev) to get your free API key.
73
+
74
+ ### Basic Usage
75
+
76
+ ```python
77
+ from haystack import Pipeline
78
+ from haystack.components.builders import PromptBuilder
79
+ from haystack.components.generators import OpenAIGenerator
80
+ from haystack.utils import Secret
81
+ from haystack_integrations.components.websearch.serpex import SerpexWebSearch
82
+
83
+ # Create a web search component
84
+ web_search = SerpexWebSearch(
85
+ api_key=Secret.from_env_var("SERPEX_API_KEY"),
86
+ engine="google", # or "bing", "duckduckgo", "brave", "yahoo", "yandex"
87
+ )
88
+
89
+ # Use it standalone
90
+ results = web_search.run(query="What is Haystack AI?")
91
+ for doc in results["documents"]:
92
+ print(f"Title: {doc.meta['title']}")
93
+ print(f"URL: {doc.meta['url']}")
94
+ print(f"Snippet: {doc.content}\n")
95
+ ```
96
+
97
+ ### RAG Pipeline Example
98
+
99
+ ```python
100
+ from haystack import Pipeline
101
+ from haystack.components.builders import PromptBuilder
102
+ from haystack.components.generators import OpenAIGenerator
103
+ from haystack.utils import Secret
104
+ from haystack_integrations.components.websearch.serpex import SerpexWebSearch
105
+
106
+ # Create a simple RAG pipeline with web search
107
+ prompt_template = """
108
+ Based on the following search results, answer the question.
109
+
110
+ Search Results:
111
+ {% for doc in documents %}
112
+ - {{ doc.meta.title }}: {{ doc.content }}
113
+ Source: {{ doc.meta.url }}
114
+ {% endfor %}
115
+
116
+ Question: {{ query }}
117
+
118
+ Answer:
119
+ """
120
+
121
+ pipe = Pipeline()
122
+ pipe.add_component("search", SerpexWebSearch(api_key=Secret.from_env_var("SERPEX_API_KEY")))
123
+ pipe.add_component("prompt", PromptBuilder(template=prompt_template))
124
+ pipe.add_component("llm", OpenAIGenerator(api_key=Secret.from_env_var("OPENAI_API_KEY")))
125
+
126
+ pipe.connect("search.documents", "prompt.documents")
127
+ pipe.connect("prompt", "llm")
128
+
129
+ # Run the pipeline
130
+ result = pipe.run({
131
+ "search": {"query": "Latest developments in AI agents"},
132
+ "prompt": {"query": "Latest developments in AI agents"}
133
+ })
134
+
135
+ print(result["llm"]["replies"][0])
136
+ ```
137
+
138
+ ## Advanced Features
139
+
140
+ ### Multiple Search Engines
141
+
142
+ ```python
143
+ # Use different engines for different queries
144
+ google_search = SerpexWebSearch(engine="google")
145
+ bing_search = SerpexWebSearch(engine="bing")
146
+ duckduckgo_search = SerpexWebSearch(engine="duckduckgo")
147
+ ```
148
+
149
+ ### Time Range Filtering
150
+
151
+ ```python
152
+ # Get only recent results
153
+ recent_results = web_search.run(
154
+ query="AI news",
155
+ time_range="week" # Options: "day", "week", "month", "year", "all"
156
+ )
157
+ ```
158
+
159
+ ### Runtime Configuration Override
160
+
161
+ ```python
162
+ # Override settings at runtime
163
+ results = web_search.run(
164
+ query="Python tutorials",
165
+ engine="duckduckgo", # Override default engine
166
+ )
167
+ ```
168
+
169
+ ### Error Handling with Retries
170
+
171
+ The component includes built-in retry logic with exponential backoff:
172
+
173
+ ```python
174
+ web_search = SerpexWebSearch(
175
+ api_key=Secret.from_env_var("SERPEX_API_KEY"),
176
+ timeout=10.0, # Request timeout in seconds
177
+ retry_attempts=3 # Number of retry attempts
178
+ )
179
+ ```
180
+
181
+ ## Component Reference
182
+
183
+ ### SerpexWebSearch
184
+
185
+ A Haystack component for fetching web search results via the Serpex API.
186
+
187
+ #### Parameters
188
+
189
+ - **api_key** (`Secret`, optional): Serpex API key. Defaults to `SERPEX_API_KEY` environment variable.
190
+ - **engine** (`str`, optional): Search engine to use. Options: `"auto"`, `"google"`, `"bing"`, `"duckduckgo"`, `"brave"`, `"yahoo"`, `"yandex"`. Defaults to `"google"`.
191
+ - **timeout** (`float`, optional): Request timeout in seconds. Defaults to `10.0`.
192
+ - **retry_attempts** (`int`, optional): Number of retry attempts. Defaults to `2`.
193
+
194
+ #### Inputs
195
+
196
+ - **query** (`str`): The search query string.
197
+ - **engine** (`str`, optional): Override the default search engine.
198
+ - **time_range** (`str`, optional): Filter by time range (`"all"`, `"day"`, `"week"`, `"month"`, `"year"`).
199
+
200
+ #### Outputs
201
+
202
+ - **documents** (`List[Document]`): List of Haystack Document objects containing search results.
203
+
204
+ Each document includes:
205
+ - **content**: The search result snippet
206
+ - **meta**:
207
+ - `title`: Result title
208
+ - `url`: Result URL
209
+ - `position`: Position in search results
210
+ - `query`: Original search query
211
+ - `engine`: Search engine used
212
+
213
+ ## Examples
214
+
215
+ Check out the [examples](examples/) directory for more use cases:
216
+
217
+ - [Basic Search](examples/basic_search.py)
218
+ - [RAG Pipeline](examples/rag_pipeline.py)
219
+ - [Multi-Engine Comparison](examples/multi_engine.py)
220
+ - [Agent with Web Search](examples/agent_example.py)
221
+
222
+ ## Why Serpex?
223
+
224
+ - **🌐 Multi-Engine Access**: One API for all major search engines
225
+ - **⚡ Fast & Reliable**: Optimized infrastructure with 99.9% uptime
226
+ - **💰 Cost-Effective**: Competitive pricing with generous free tier
227
+ - **📊 Rich Metadata**: Comprehensive result data including positions, timestamps, and more
228
+ - **🔒 Secure**: Enterprise-grade security and data privacy
229
+ - **🚀 Scalable**: Handle thousands of requests per second
230
+
231
+ ## Documentation
232
+
233
+ - [Serpex API Documentation](https://docs.serpex.dev)
234
+ - [Haystack Documentation](https://docs.haystack.deepset.ai)
235
+ - [Integration Examples](examples/)
236
+
237
+ ## Contributing
238
+
239
+ Contributions are welcome! Please feel free to submit a Pull Request.
240
+
241
+ 1. Fork the repository
242
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
243
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
244
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
245
+ 5. Open a Pull Request
246
+
247
+ ## Development Setup
248
+
249
+ ```bash
250
+ # Clone the repository
251
+ git clone https://github.com/divyeshradadiya/serpex-haystack.git
252
+ cd serpex-haystack
253
+
254
+ # Install with development dependencies
255
+ pip install -e ".[dev]"
256
+
257
+ # Run tests
258
+ pytest
259
+
260
+ # Run linting
261
+ ruff check .
262
+ black --check .
263
+
264
+ # Run type checking
265
+ mypy src/
266
+ ```
267
+
268
+ ## License
269
+
270
+ This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.
271
+
272
+ ## Support
273
+
274
+ - 📧 Email: support@serpex.dev
275
+ - 💬 Discord: [Join our community](https://discord.com/channels/1417759329385316383/1421004675343319102)
276
+ - 🐛 Issues: [GitHub Issues](https://github.com/divyeshradadiya/serpex-haystack/issues)
277
+ - 📖 Docs: [docs.serpex.dev](https://serpex.dev/docs)
278
+
279
+ ## Acknowledgments
280
+
281
+ Built with ❤️ for the Haystack community by [Divyesh Radadiya](https://github.com/divyeshradadiya)
282
+
283
+ ---
284
+
285
+ **Note**: This is a community-maintained integration. For Serpex API support, visit [serpex.dev](https://serpex.dev).
@@ -0,0 +1,247 @@
1
+ # Serpex Haystack Integration
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/serpex-haystack.svg)](https://pypi.org/project/serpex-haystack)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/serpex-haystack.svg)](https://pypi.org/project/serpex-haystack)
5
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
+ [![CI Tests](https://github.com/divyeshradadiya/serpex-haystack/actions/workflows/ci.yml/badge.svg)](https://github.com/divyeshradadiya/serpex-haystack/actions/workflows/ci.yml)
7
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
8
+
9
+ [Serpex](https://serpex.dev) integration for [Haystack](https://haystack.deepset.ai/) - bringing powerful multi-engine web search capabilities to your Haystack pipelines.
10
+
11
+ ## Overview
12
+
13
+ Serpex is a unified web search API that provides access to multiple search engines including Google, Bing, DuckDuckGo, Brave, Yahoo, and Yandex. This integration allows you to seamlessly incorporate web search results into your Haystack RAG (Retrieval-Augmented Generation) pipelines and AI applications.
14
+
15
+ ### Key Features
16
+
17
+ - 🔍 **Multi-Engine Support**: Switch between Google, Bing, DuckDuckGo, Brave, Yahoo, and Yandex
18
+ - ⚡ **High Performance**: Fast and reliable API with automatic retries
19
+ - 🎯 **Rich Results**: Get organic search results with titles, snippets, and URLs
20
+ - 🕒 **Time Filters**: Filter results by day, week, month, or year
21
+ - 🔒 **Type-Safe**: Fully typed with comprehensive type hints
22
+ - 📝 **Haystack Native**: Seamless integration with Haystack 2.0+ components
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install serpex-haystack
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ### Get Your API Key
33
+
34
+ Sign up at [Serpex.dev](https://serpex.dev) to get your free API key.
35
+
36
+ ### Basic Usage
37
+
38
+ ```python
39
+ from haystack import Pipeline
40
+ from haystack.components.builders import PromptBuilder
41
+ from haystack.components.generators import OpenAIGenerator
42
+ from haystack.utils import Secret
43
+ from haystack_integrations.components.websearch.serpex import SerpexWebSearch
44
+
45
+ # Create a web search component
46
+ web_search = SerpexWebSearch(
47
+ api_key=Secret.from_env_var("SERPEX_API_KEY"),
48
+ engine="google", # or "bing", "duckduckgo", "brave", "yahoo", "yandex"
49
+ )
50
+
51
+ # Use it standalone
52
+ results = web_search.run(query="What is Haystack AI?")
53
+ for doc in results["documents"]:
54
+ print(f"Title: {doc.meta['title']}")
55
+ print(f"URL: {doc.meta['url']}")
56
+ print(f"Snippet: {doc.content}\n")
57
+ ```
58
+
59
+ ### RAG Pipeline Example
60
+
61
+ ```python
62
+ from haystack import Pipeline
63
+ from haystack.components.builders import PromptBuilder
64
+ from haystack.components.generators import OpenAIGenerator
65
+ from haystack.utils import Secret
66
+ from haystack_integrations.components.websearch.serpex import SerpexWebSearch
67
+
68
+ # Create a simple RAG pipeline with web search
69
+ prompt_template = """
70
+ Based on the following search results, answer the question.
71
+
72
+ Search Results:
73
+ {% for doc in documents %}
74
+ - {{ doc.meta.title }}: {{ doc.content }}
75
+ Source: {{ doc.meta.url }}
76
+ {% endfor %}
77
+
78
+ Question: {{ query }}
79
+
80
+ Answer:
81
+ """
82
+
83
+ pipe = Pipeline()
84
+ pipe.add_component("search", SerpexWebSearch(api_key=Secret.from_env_var("SERPEX_API_KEY")))
85
+ pipe.add_component("prompt", PromptBuilder(template=prompt_template))
86
+ pipe.add_component("llm", OpenAIGenerator(api_key=Secret.from_env_var("OPENAI_API_KEY")))
87
+
88
+ pipe.connect("search.documents", "prompt.documents")
89
+ pipe.connect("prompt", "llm")
90
+
91
+ # Run the pipeline
92
+ result = pipe.run({
93
+ "search": {"query": "Latest developments in AI agents"},
94
+ "prompt": {"query": "Latest developments in AI agents"}
95
+ })
96
+
97
+ print(result["llm"]["replies"][0])
98
+ ```
99
+
100
+ ## Advanced Features
101
+
102
+ ### Multiple Search Engines
103
+
104
+ ```python
105
+ # Use different engines for different queries
106
+ google_search = SerpexWebSearch(engine="google")
107
+ bing_search = SerpexWebSearch(engine="bing")
108
+ duckduckgo_search = SerpexWebSearch(engine="duckduckgo")
109
+ ```
110
+
111
+ ### Time Range Filtering
112
+
113
+ ```python
114
+ # Get only recent results
115
+ recent_results = web_search.run(
116
+ query="AI news",
117
+ time_range="week" # Options: "day", "week", "month", "year", "all"
118
+ )
119
+ ```
120
+
121
+ ### Runtime Configuration Override
122
+
123
+ ```python
124
+ # Override settings at runtime
125
+ results = web_search.run(
126
+ query="Python tutorials",
127
+ engine="duckduckgo", # Override default engine
128
+ )
129
+ ```
130
+
131
+ ### Error Handling with Retries
132
+
133
+ The component includes built-in retry logic with exponential backoff:
134
+
135
+ ```python
136
+ web_search = SerpexWebSearch(
137
+ api_key=Secret.from_env_var("SERPEX_API_KEY"),
138
+ timeout=10.0, # Request timeout in seconds
139
+ retry_attempts=3 # Number of retry attempts
140
+ )
141
+ ```
142
+
143
+ ## Component Reference
144
+
145
+ ### SerpexWebSearch
146
+
147
+ A Haystack component for fetching web search results via the Serpex API.
148
+
149
+ #### Parameters
150
+
151
+ - **api_key** (`Secret`, optional): Serpex API key. Defaults to `SERPEX_API_KEY` environment variable.
152
+ - **engine** (`str`, optional): Search engine to use. Options: `"auto"`, `"google"`, `"bing"`, `"duckduckgo"`, `"brave"`, `"yahoo"`, `"yandex"`. Defaults to `"google"`.
153
+ - **timeout** (`float`, optional): Request timeout in seconds. Defaults to `10.0`.
154
+ - **retry_attempts** (`int`, optional): Number of retry attempts. Defaults to `2`.
155
+
156
+ #### Inputs
157
+
158
+ - **query** (`str`): The search query string.
159
+ - **engine** (`str`, optional): Override the default search engine.
160
+ - **time_range** (`str`, optional): Filter by time range (`"all"`, `"day"`, `"week"`, `"month"`, `"year"`).
161
+
162
+ #### Outputs
163
+
164
+ - **documents** (`List[Document]`): List of Haystack Document objects containing search results.
165
+
166
+ Each document includes:
167
+ - **content**: The search result snippet
168
+ - **meta**:
169
+ - `title`: Result title
170
+ - `url`: Result URL
171
+ - `position`: Position in search results
172
+ - `query`: Original search query
173
+ - `engine`: Search engine used
174
+
175
+ ## Examples
176
+
177
+ Check out the [examples](examples/) directory for more use cases:
178
+
179
+ - [Basic Search](examples/basic_search.py)
180
+ - [RAG Pipeline](examples/rag_pipeline.py)
181
+ - [Multi-Engine Comparison](examples/multi_engine.py)
182
+ - [Agent with Web Search](examples/agent_example.py)
183
+
184
+ ## Why Serpex?
185
+
186
+ - **🌐 Multi-Engine Access**: One API for all major search engines
187
+ - **⚡ Fast & Reliable**: Optimized infrastructure with 99.9% uptime
188
+ - **💰 Cost-Effective**: Competitive pricing with generous free tier
189
+ - **📊 Rich Metadata**: Comprehensive result data including positions, timestamps, and more
190
+ - **🔒 Secure**: Enterprise-grade security and data privacy
191
+ - **🚀 Scalable**: Handle thousands of requests per second
192
+
193
+ ## Documentation
194
+
195
+ - [Serpex API Documentation](https://docs.serpex.dev)
196
+ - [Haystack Documentation](https://docs.haystack.deepset.ai)
197
+ - [Integration Examples](examples/)
198
+
199
+ ## Contributing
200
+
201
+ Contributions are welcome! Please feel free to submit a Pull Request.
202
+
203
+ 1. Fork the repository
204
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
205
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
206
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
207
+ 5. Open a Pull Request
208
+
209
+ ## Development Setup
210
+
211
+ ```bash
212
+ # Clone the repository
213
+ git clone https://github.com/divyeshradadiya/serpex-haystack.git
214
+ cd serpex-haystack
215
+
216
+ # Install with development dependencies
217
+ pip install -e ".[dev]"
218
+
219
+ # Run tests
220
+ pytest
221
+
222
+ # Run linting
223
+ ruff check .
224
+ black --check .
225
+
226
+ # Run type checking
227
+ mypy src/
228
+ ```
229
+
230
+ ## License
231
+
232
+ This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.
233
+
234
+ ## Support
235
+
236
+ - 📧 Email: support@serpex.dev
237
+ - 💬 Discord: [Join our community](https://discord.com/channels/1417759329385316383/1421004675343319102)
238
+ - 🐛 Issues: [GitHub Issues](https://github.com/divyeshradadiya/serpex-haystack/issues)
239
+ - 📖 Docs: [docs.serpex.dev](https://serpex.dev/docs)
240
+
241
+ ## Acknowledgments
242
+
243
+ Built with ❤️ for the Haystack community by [Divyesh Radadiya](https://github.com/divyeshradadiya)
244
+
245
+ ---
246
+
247
+ **Note**: This is a community-maintained integration. For Serpex API support, visit [serpex.dev](https://serpex.dev).
@@ -0,0 +1,120 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "serpex-haystack"
7
+ version = "1.0.0"
8
+ description = "Haystack integration for Serpex web search - supporting Google, Bing, DuckDuckGo, Brave, Yahoo, and Yandex"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "Apache-2.0"
12
+ keywords = [
13
+ "haystack",
14
+ "serpex",
15
+ "web-search",
16
+ "search-api",
17
+ "google-search",
18
+ "bing-search",
19
+ "duckduckgo",
20
+ "nlp",
21
+ "ai",
22
+ "llm",
23
+ ]
24
+ authors = [
25
+ { name = "Divyesh Radadiya", email = "divyeshradadiya0@gmail.com" },
26
+ ]
27
+ classifiers = [
28
+ "Development Status :: 5 - Production/Stable",
29
+ "Intended Audience :: Developers",
30
+ "License :: OSI Approved :: Apache Software License",
31
+ "Operating System :: OS Independent",
32
+ "Programming Language :: Python :: 3",
33
+ "Programming Language :: Python :: 3.8",
34
+ "Programming Language :: Python :: 3.9",
35
+ "Programming Language :: Python :: 3.10",
36
+ "Programming Language :: Python :: 3.11",
37
+ "Programming Language :: Python :: 3.12",
38
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
39
+ "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
40
+ ]
41
+
42
+ dependencies = [
43
+ "haystack-ai>=2.0.0",
44
+ "httpx>=0.24.0",
45
+ "tenacity>=8.0.0",
46
+ ]
47
+
48
+ [project.optional-dependencies]
49
+ dev = [
50
+ "pytest>=7.0.0",
51
+ "pytest-cov>=4.0.0",
52
+ "pytest-asyncio>=0.21.0",
53
+ "black>=23.0.0",
54
+ "ruff>=0.1.0",
55
+ "mypy>=1.0.0",
56
+ "pre-commit>=3.0.0",
57
+ ]
58
+
59
+ [project.urls]
60
+ Homepage = "https://serpex.dev"
61
+ Documentation = "https://docs.serpex.dev"
62
+ Repository = "https://github.com/divyeshradadiya/serpex-haystack"
63
+ Issues = "https://github.com/divyeshradadiya/serpex-haystack/issues"
64
+
65
+ [tool.hatch.build.targets.wheel]
66
+ packages = ["src/haystack_integrations"]
67
+
68
+ [tool.hatch.build.targets.sdist]
69
+ include = [
70
+ "/src",
71
+ "/tests",
72
+ "/README.md",
73
+ "/LICENSE",
74
+ ]
75
+
76
+ [tool.black]
77
+ line-length = 120
78
+ target-version = ["py38"]
79
+
80
+ [tool.ruff]
81
+ line-length = 120
82
+ target-version = "py38"
83
+
84
+ [tool.ruff.lint]
85
+ select = [
86
+ "E", # pycodestyle errors
87
+ "W", # pycodestyle warnings
88
+ "F", # pyflakes
89
+ "I", # isort
90
+ "C", # flake8-comprehensions
91
+ "B", # flake8-bugbear
92
+ ]
93
+ ignore = [
94
+ "E501", # line too long (handled by black)
95
+ "B008", # do not perform function calls in argument defaults
96
+ ]
97
+
98
+ [tool.mypy]
99
+ python_version = "3.9"
100
+ warn_return_any = true
101
+ warn_unused_configs = true
102
+ ignore_missing_imports = true
103
+
104
+ [tool.pytest.ini_options]
105
+ minversion = "7.0"
106
+ addopts = "-v --cov=src/haystack_integrations --cov-report=term-missing --cov-report=html"
107
+ testpaths = ["tests"]
108
+
109
+ [tool.coverage.run]
110
+ source = ["src/haystack_integrations"]
111
+ omit = ["*/tests/*"]
112
+
113
+ [tool.coverage.report]
114
+ exclude_lines = [
115
+ "pragma: no cover",
116
+ "def __repr__",
117
+ "raise AssertionError",
118
+ "raise NotImplementedError",
119
+ "if __name__ == .__main__.:",
120
+ ]
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2024-present Divyesh Radadiya <divyeshradadiya0@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2024-present Divyesh Radadiya <divyeshradadiya0@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2024-present Divyesh Radadiya <divyeshradadiya0@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from haystack_integrations.components.websearch.serpex import SerpexWebSearch
6
+
7
+ __all__ = ["SerpexWebSearch"]
@@ -0,0 +1,209 @@
1
+ # SPDX-FileCopyrightText: 2024-present Divyesh Radadiya <divyeshradadiya0@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from typing import Any, Dict, List, Literal, Optional, cast
6
+
7
+ import httpx
8
+ from haystack import component, default_from_dict, default_to_dict, logging
9
+ from haystack.dataclasses import Document
10
+ from haystack.utils import Secret, deserialize_secrets_inplace
11
+ from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @component
17
+ class SerpexWebSearch:
18
+ """
19
+ Fetches web search results from the Serpex API.
20
+
21
+ Serpex provides web search results from multiple search engines including Google, Bing, DuckDuckGo, and more.
22
+ Use it to retrieve organic search results, snippets, and metadata for search queries.
23
+
24
+ ### Usage example
25
+
26
+ ```python
27
+ from haystack_integrations.components.websearch.serpex import SerpexWebSearch
28
+ from haystack.utils import Secret
29
+
30
+ fetcher = SerpexWebSearch(api_key=Secret.from_token("your-serpex-api-key"))
31
+ results = fetcher.run(query="What is Haystack?")
32
+
33
+ documents = results["documents"]
34
+ for doc in documents:
35
+ print(f"Title: {doc.meta['title']}")
36
+ print(f"URL: {doc.meta['url']}")
37
+ print(f"Snippet: {doc.content}")
38
+ ```
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ *,
44
+ api_key: Secret = Secret.from_env_var("SERPEX_API_KEY"),
45
+ engine: Literal["auto", "google", "bing", "duckduckgo", "brave", "yahoo", "yandex"] = "google",
46
+ timeout: float = 10.0,
47
+ retry_attempts: int = 2,
48
+ ) -> None:
49
+ """
50
+ Initializes the SerpexWebSearch component.
51
+
52
+ :param api_key: Serpex API key for authentication. Get yours at https://serpex.dev
53
+ :param engine: Search engine to use. Options: "auto", "google", "bing", "duckduckgo",
54
+ "brave", "yahoo", "yandex". Defaults to "google".
55
+ :param timeout: Timeout in seconds for the API request. Defaults to 10.0.
56
+ :param retry_attempts: Number of retry attempts for failed requests. Defaults to 2.
57
+ """
58
+ self.api_key = api_key
59
+ self.engine = engine
60
+ self.timeout = timeout
61
+ self.retry_attempts = retry_attempts
62
+
63
+ # Create httpx client
64
+ self._client = httpx.Client(timeout=timeout, follow_redirects=True)
65
+
66
+ # Define retry decorator
67
+ @retry(
68
+ reraise=True,
69
+ stop=stop_after_attempt(self.retry_attempts),
70
+ wait=wait_exponential(multiplier=1, min=2, max=10),
71
+ retry=retry_if_exception_type((httpx.HTTPStatusError, httpx.RequestError)),
72
+ )
73
+ def make_request(url: str, headers: Dict[str, str], params: Dict[str, Any]) -> httpx.Response:
74
+ response = self._client.get(url, headers=headers, params=params)
75
+ response.raise_for_status()
76
+ return response
77
+
78
+ self._make_request = make_request
79
+
80
+ def __del__(self):
81
+ """
82
+ Clean up resources when the component is deleted.
83
+
84
+ Closes the HTTP client to prevent resource leaks.
85
+ """
86
+ try:
87
+ if hasattr(self, "_client"):
88
+ self._client.close()
89
+ except Exception:
90
+ pass
91
+
92
+ def to_dict(self) -> Dict[str, Any]:
93
+ """
94
+ Serializes the component to a dictionary.
95
+
96
+ :returns: Dictionary with serialized data.
97
+ """
98
+ return cast(
99
+ Dict[str, Any],
100
+ default_to_dict(
101
+ self,
102
+ api_key=self.api_key.to_dict(),
103
+ engine=self.engine,
104
+ timeout=self.timeout,
105
+ retry_attempts=self.retry_attempts,
106
+ ),
107
+ )
108
+
109
+ @classmethod
110
+ def from_dict(cls, data: Dict[str, Any]) -> "SerpexWebSearch":
111
+ """
112
+ Deserializes the component from a dictionary.
113
+
114
+ :param data: Dictionary to deserialize from.
115
+ :returns: Deserialized component.
116
+ """
117
+ deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
118
+ return cast("SerpexWebSearch", default_from_dict(cls, data))
119
+
120
+ @component.output_types(documents=List[Document])
121
+ def run(
122
+ self,
123
+ query: str,
124
+ *,
125
+ engine: Optional[Literal["auto", "google", "bing", "duckduckgo", "brave", "yahoo", "yandex"]] = None,
126
+ time_range: Optional[Literal["all", "day", "week", "month", "year"]] = None,
127
+ ) -> Dict[str, List[Document]]:
128
+ """
129
+ Fetches web search results for the given query.
130
+
131
+ :param query: The search query string.
132
+ :param engine: Override the default search engine. If None, uses the engine from initialization.
133
+ :param time_range: Time range filter for results. Options: "all", "day", "week", "month", "year".
134
+ Defaults to None (all time).
135
+ :returns: Dictionary containing a list of Document objects with search results.
136
+ """
137
+ documents: List[Document] = []
138
+
139
+ try:
140
+ # Prepare request parameters
141
+ params: Dict[str, Any] = {
142
+ "q": query,
143
+ "engine": engine or self.engine,
144
+ "category": "web",
145
+ }
146
+
147
+ if time_range:
148
+ params["time_range"] = time_range
149
+
150
+ headers = {
151
+ "Authorization": f"Bearer {self.api_key.resolve_value()}",
152
+ "Content-Type": "application/json",
153
+ }
154
+
155
+ # Make API request
156
+ response = self._make_request("https://api.serpex.dev/api/search", headers, params)
157
+ data = response.json()
158
+
159
+ # Parse search results
160
+ if "results" in data and isinstance(data["results"], list):
161
+ for result in data["results"]:
162
+ # Extract result data
163
+ title = result.get("title", "")
164
+ url = result.get("url", "") # API uses 'url' not 'link'
165
+ snippet = result.get("snippet", "")
166
+ position = result.get("position", 0)
167
+
168
+ # Create Document object
169
+ doc = Document(
170
+ content=snippet,
171
+ meta={
172
+ "title": title,
173
+ "url": url,
174
+ "position": position,
175
+ "query": query,
176
+ "engine": engine or self.engine,
177
+ },
178
+ )
179
+ documents.append(doc)
180
+
181
+ logger.info(
182
+ "Successfully fetched {count} search results for query: {query}",
183
+ count=len(documents),
184
+ query=query,
185
+ )
186
+ else:
187
+ logger.warning(
188
+ "No results found in Serpex API response for query: {query}",
189
+ query=query,
190
+ )
191
+
192
+ except httpx.HTTPStatusError as e:
193
+ logger.error(
194
+ "HTTP error occurred while fetching Serpex results: {status} - {detail}",
195
+ status=e.response.status_code,
196
+ detail=str(e),
197
+ )
198
+ raise
199
+ except httpx.RequestError as e:
200
+ logger.error("Request error occurred while fetching Serpex results: {error}", error=e)
201
+ raise
202
+ except Exception as e:
203
+ logger.error(
204
+ "Unexpected error occurred while fetching Serpex results: {error}",
205
+ error=e,
206
+ )
207
+ raise
208
+
209
+ return {"documents": documents}
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2024-present Divyesh Radadiya <divyeshradadiya0@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,148 @@
1
+ """
2
+ Tests for SerpexWebSearch component
3
+ """
4
+
5
+ import os
6
+ from unittest.mock import Mock, patch
7
+
8
+ from haystack.dataclasses import Document
9
+ from haystack.utils import Secret
10
+
11
+ from haystack_integrations.components.websearch.serpex import SerpexWebSearch
12
+
13
+
14
+ class TestSerpexWebSearch:
15
+ def test_init_default(self):
16
+ """Test initialization with default parameters"""
17
+ with patch.dict(os.environ, {"SERPEX_API_KEY": "test_key"}):
18
+ component = SerpexWebSearch()
19
+ assert component.engine == "google"
20
+ assert component.timeout == 10.0
21
+ assert component.retry_attempts == 2
22
+
23
+ def test_init_custom(self):
24
+ """Test initialization with custom parameters"""
25
+ component = SerpexWebSearch(
26
+ api_key=Secret.from_token("custom_key"),
27
+ engine="bing",
28
+ timeout=15.0,
29
+ retry_attempts=3,
30
+ )
31
+ assert component.engine == "bing"
32
+ assert component.timeout == 15.0
33
+ assert component.retry_attempts == 3
34
+
35
+ def test_to_dict(self):
36
+ """Test serialization to dictionary"""
37
+ with patch.dict(os.environ, {"SERPEX_API_KEY": "test_key"}):
38
+ component = SerpexWebSearch(
39
+ api_key=Secret.from_env_var("SERPEX_API_KEY"),
40
+ engine="duckduckgo",
41
+ )
42
+ data = component.to_dict()
43
+
44
+ assert data["type"] == "haystack_integrations.components.websearch.serpex.SerpexWebSearch"
45
+ assert data["init_parameters"]["engine"] == "duckduckgo"
46
+
47
+ def test_from_dict(self):
48
+ """Test deserialization from dictionary"""
49
+ data = {
50
+ "type": "haystack_integrations.components.websearch.serpex.SerpexWebSearch",
51
+ "init_parameters": {
52
+ "api_key": {"type": "env_var", "env_vars": ["SERPEX_API_KEY"], "strict": True},
53
+ "engine": "brave",
54
+ "timeout": 20.0,
55
+ "retry_attempts": 4,
56
+ },
57
+ }
58
+
59
+ with patch.dict(os.environ, {"SERPEX_API_KEY": "test_key"}):
60
+ component = SerpexWebSearch.from_dict(data)
61
+ assert component.engine == "brave"
62
+ assert component.timeout == 20.0
63
+ assert component.retry_attempts == 4
64
+
65
+ @patch("haystack_integrations.components.websearch.serpex.httpx.Client")
66
+ def test_run_success(self, mock_client):
67
+ """Test successful search"""
68
+ # Mock response
69
+ mock_response = Mock()
70
+ mock_response.json.return_value = {
71
+ "results": [
72
+ {
73
+ "title": "Test Title 1",
74
+ "url": "https://example.com/1",
75
+ "snippet": "Test snippet 1",
76
+ "position": 1,
77
+ },
78
+ {
79
+ "title": "Test Title 2",
80
+ "url": "https://example.com/2",
81
+ "snippet": "Test snippet 2",
82
+ "position": 2,
83
+ },
84
+ ]
85
+ }
86
+
87
+ # Setup mock client
88
+ mock_instance = Mock()
89
+ mock_instance.get.return_value = mock_response
90
+ mock_client.return_value = mock_instance
91
+
92
+ component = SerpexWebSearch(api_key=Secret.from_token("test_key"))
93
+ result = component.run(query="test query")
94
+
95
+ assert "documents" in result
96
+ assert len(result["documents"]) == 2
97
+
98
+ doc1 = result["documents"][0]
99
+ assert isinstance(doc1, Document)
100
+ assert doc1.content == "Test snippet 1"
101
+ assert doc1.meta["title"] == "Test Title 1"
102
+ assert doc1.meta["url"] == "https://example.com/1"
103
+ assert doc1.meta["position"] == 1
104
+ assert doc1.meta["query"] == "test query"
105
+
106
+ @patch("haystack_integrations.components.websearch.serpex.httpx.Client")
107
+ def test_run_with_overrides(self, mock_client):
108
+ """Test search with runtime parameter overrides"""
109
+ mock_response = Mock()
110
+ mock_response.json.return_value = {"results": []}
111
+
112
+ mock_instance = Mock()
113
+ mock_instance.get.return_value = mock_response
114
+ mock_client.return_value = mock_instance
115
+
116
+ component = SerpexWebSearch(api_key=Secret.from_token("test_key"))
117
+ component.run(query="test", engine="bing", time_range="week")
118
+
119
+ # Check that parameters were passed correctly
120
+ call_args = mock_instance.get.call_args
121
+ params = call_args[1]["params"]
122
+ assert params["engine"] == "bing"
123
+ assert params["time_range"] == "week"
124
+
125
+ @patch("haystack_integrations.components.websearch.serpex.httpx.Client")
126
+ def test_run_empty_results(self, mock_client):
127
+ """Test handling of empty results"""
128
+ mock_response = Mock()
129
+ mock_response.json.return_value = {"results": []}
130
+
131
+ mock_instance = Mock()
132
+ mock_instance.get.return_value = mock_response
133
+ mock_client.return_value = mock_instance
134
+
135
+ component = SerpexWebSearch(api_key=Secret.from_token("test_key"))
136
+ result = component.run(query="test query")
137
+
138
+ assert "documents" in result
139
+ assert len(result["documents"]) == 0
140
+
141
+ def test_cleanup(self):
142
+ """Test resource cleanup"""
143
+ component = SerpexWebSearch(api_key=Secret.from_token("test_key"))
144
+ assert hasattr(component, "_client")
145
+
146
+ # Trigger cleanup
147
+ component.__del__()
148
+ # Should not raise any exceptions