ddgs-haystack 1.0.0__tar.gz → 2.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,16 @@
1
+ # Changelog
2
+
3
+ ## [integrations/ddgs-v1.1.0] - 2026-07-22
4
+
5
+ ### 🚀 Features
6
+
7
+ - Expose per-run parameter overrides on DDGSWebSearch (#3634)
8
+
9
+
10
+ ## [integrations/ddgs-v1.0.0] - 2026-07-21
11
+
12
+ ### 🚀 Features
13
+
14
+ - Add ddgs (Dux Distributed Global Search) web search integration (#3623)
15
+
16
+ <!-- generated by git-cliff -->
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: ddgs-haystack
3
- Version: 1.0.0
3
+ Version: 2.0.0
4
4
  Summary: Haystack integration for ddgs
5
5
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/ddgs#readme
6
6
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
@@ -12,14 +12,14 @@ Keywords: agent tool,ddgs,duckduckgo,haystack,metasearch,web search,websearch
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: License :: OSI Approved :: Apache Software License
14
14
  Classifier: Programming Language :: Python
15
- Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
19
18
  Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
20
20
  Classifier: Programming Language :: Python :: Implementation :: CPython
21
21
  Classifier: Programming Language :: Python :: Implementation :: PyPy
22
- Requires-Python: >=3.10
22
+ Requires-Python: >=3.11
23
23
  Requires-Dist: ddgs>=9.14.3
24
24
  Requires-Dist: haystack-ai>=2.24.1
25
25
  Description-Content-Type: text/markdown
@@ -29,6 +29,7 @@ Description-Content-Type: text/markdown
29
29
  [![PyPI - Version](https://img.shields.io/pypi/v/ddgs-haystack.svg)](https://pypi.org/project/ddgs-haystack)
30
30
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ddgs-haystack.svg)](https://pypi.org/project/ddgs-haystack)
31
31
 
32
+ - [Integration page](https://haystack.deepset.ai/integrations/ddgs)
32
33
  - [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/ddgs/CHANGELOG.md)
33
34
 
34
35
  ---
@@ -36,5 +37,3 @@ Description-Content-Type: text/markdown
36
37
  ## Contributing
37
38
 
38
39
  Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
39
-
40
- `ddgs` requires no API key, so the integration tests run without any environment variables.
@@ -3,6 +3,7 @@
3
3
  [![PyPI - Version](https://img.shields.io/pypi/v/ddgs-haystack.svg)](https://pypi.org/project/ddgs-haystack)
4
4
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ddgs-haystack.svg)](https://pypi.org/project/ddgs-haystack)
5
5
 
6
+ - [Integration page](https://haystack.deepset.ai/integrations/ddgs)
6
7
  - [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/ddgs/CHANGELOG.md)
7
8
 
8
9
  ---
@@ -10,5 +11,3 @@
10
11
  ## Contributing
11
12
 
12
13
  Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
13
-
14
- `ddgs` requires no API key, so the integration tests run without any environment variables.
@@ -7,7 +7,7 @@ name = "ddgs-haystack"
7
7
  dynamic = ["version"]
8
8
  description = "Haystack integration for ddgs"
9
9
  readme = "README.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.11"
11
11
  license = "Apache-2.0"
12
12
  keywords = ["haystack", "web search", "websearch", "ddgs", "duckduckgo", "metasearch", "agent tool"]
13
13
  authors = [{ name = "deepset GmbH", email = "info@deepset.ai" }]
@@ -15,11 +15,11 @@ classifiers = [
15
15
  "License :: OSI Approved :: Apache Software License",
16
16
  "Development Status :: 4 - Beta",
17
17
  "Programming Language :: Python",
18
- "Programming Language :: Python :: 3.10",
19
18
  "Programming Language :: Python :: 3.11",
20
19
  "Programming Language :: Python :: 3.12",
21
20
  "Programming Language :: Python :: 3.13",
22
21
  "Programming Language :: Python :: 3.14",
22
+ "Programming Language :: Python :: 3.15",
23
23
  "Programming Language :: Python :: Implementation :: CPython",
24
24
  "Programming Language :: Python :: Implementation :: PyPy",
25
25
  ]
@@ -127,6 +127,7 @@ ignore = [
127
127
  "PLR0912",
128
128
  "PLR0913",
129
129
  "PLR0915",
130
+ "PLR0917",
130
131
  # Allow `Any` type - used legitimately for dynamic types and SDK boundaries
131
132
  "ANN401",
132
133
  ]
@@ -0,0 +1,217 @@
1
+ # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ from haystack import Document, component, logging
9
+
10
+ from ddgs import DDGS
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @component
16
+ class DDGSWebSearch:
17
+ """
18
+ Searches the web with ddgs (Dux Distributed Global Search) and returns results as Haystack Documents.
19
+
20
+ [ddgs](https://github.com/deedy5/ddgs) is a free, **keyless** metasearch library that aggregates
21
+ results from multiple backends (DuckDuckGo, Google, Bing, Brave, Yahoo, Yandex, Mullvad, and more),
22
+ so no API key is required.
23
+
24
+ ### Usage example
25
+
26
+ ```python
27
+ from haystack_integrations.components.websearch.ddgs import DDGSWebSearch
28
+
29
+ websearch = DDGSWebSearch(top_k=5)
30
+ result = websearch.run(query="What is Haystack by deepset?")
31
+
32
+ documents = result["documents"]
33
+ links = result["links"]
34
+ ```
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ top_k: int = 10,
40
+ backend: str = "auto",
41
+ region: str = "us-en",
42
+ safesearch: str = "moderate",
43
+ search_params: dict[str, Any] | None = None,
44
+ ) -> None:
45
+ """
46
+ Initialize the DDGSWebSearch component.
47
+
48
+ :param top_k:
49
+ Maximum number of results to return.
50
+ :param backend:
51
+ Comma-separated ddgs backends to query, or `"auto"` to let ddgs choose
52
+ (for example `"duckduckgo, google, brave"`). See the ddgs docs for the full list.
53
+ :param region:
54
+ Region/locale for the search, for example `"us-en"`, `"de-de"`, or `"wt-wt"` (no region).
55
+ :param safesearch:
56
+ Safe-search level: `"on"`, `"moderate"`, or `"off"`.
57
+ :param search_params:
58
+ Additional keyword arguments forwarded to `DDGS().text()` (for example `page` or
59
+ `timelimit`). Values here override `backend`, `region`, `safesearch`, and `top_k`
60
+ on conflict.
61
+ """
62
+ self.top_k = top_k
63
+ self.backend = backend
64
+ self.region = region
65
+ self.safesearch = safesearch
66
+ self.search_params = search_params or {}
67
+ self._client: DDGS | None = None
68
+
69
+ def warm_up(self) -> None:
70
+ """
71
+ Initialize the ddgs client.
72
+
73
+ Called automatically on first use. Can be called explicitly to avoid cold-start latency.
74
+ """
75
+ if self._client is None:
76
+ self._client = DDGS()
77
+
78
+ def _search(
79
+ self,
80
+ query: str,
81
+ top_k: int | None = None,
82
+ backend: str | None = None,
83
+ region: str | None = None,
84
+ safesearch: str | None = None,
85
+ search_params: dict[str, Any] | None = None,
86
+ ) -> dict[str, list[Document] | list[str]]:
87
+ """
88
+ Run the blocking ddgs search and normalise the results.
89
+
90
+ :param query:
91
+ Search query.
92
+ :returns:
93
+ A dictionary with `documents` and `links` keys.
94
+ """
95
+ if self._client is None:
96
+ self.warm_up()
97
+ if self._client is None:
98
+ msg = "DDGSWebSearch client failed to initialize."
99
+ raise RuntimeError(msg)
100
+
101
+ params: dict[str, Any] = {
102
+ "region": region if region is not None else self.region,
103
+ "safesearch": safesearch if safesearch is not None else self.safesearch,
104
+ "backend": backend if backend is not None else self.backend,
105
+ "max_results": top_k if top_k is not None else self.top_k,
106
+ }
107
+ params.update(search_params if search_params is not None else self.search_params)
108
+
109
+ results = self._client.text(query, **params)
110
+ documents, links = self._parse_response(results)
111
+
112
+ logger.debug(
113
+ "ddgs returned {number_documents} documents for the query '{query}'",
114
+ number_documents=len(documents),
115
+ query=query,
116
+ )
117
+ return {"documents": documents, "links": links}
118
+
119
+ @component.output_types(documents=list[Document], links=list[str])
120
+ def run(
121
+ self,
122
+ query: str,
123
+ top_k: int | None = None,
124
+ *,
125
+ backend: str | None = None,
126
+ region: str | None = None,
127
+ safesearch: str | None = None,
128
+ search_params: dict[str, Any] | None = None,
129
+ ) -> dict[str, list[Document] | list[str]]:
130
+ """
131
+ Use ddgs to search the web.
132
+
133
+ :param query:
134
+ Search query.
135
+ :param top_k:
136
+ Optional per-run override of the maximum number of results. If not provided, the
137
+ init-time `top_k` is used.
138
+ :param backend:
139
+ Optional per-run override of the ddgs backends. If not provided, the init-time
140
+ `backend` is used.
141
+ :param region:
142
+ Optional per-run override of the region/locale. If not provided, the init-time
143
+ `region` is used.
144
+ :param safesearch:
145
+ Optional per-run override of the safe-search level. If not provided, the init-time
146
+ `safesearch` is used.
147
+ :param search_params:
148
+ Optional per-run override of the extra `DDGS().text()` arguments. If provided, fully
149
+ replaces the init-time `search_params`.
150
+ :returns:
151
+ A dictionary with the following keys:
152
+ - `documents`: List of documents returned by the search backends.
153
+ - `links`: List of links returned by the search backends.
154
+ """
155
+ return self._search(query, top_k, backend, region, safesearch, search_params)
156
+
157
+ @component.output_types(documents=list[Document], links=list[str])
158
+ async def run_async(
159
+ self,
160
+ query: str,
161
+ top_k: int | None = None,
162
+ *,
163
+ backend: str | None = None,
164
+ region: str | None = None,
165
+ safesearch: str | None = None,
166
+ search_params: dict[str, Any] | None = None,
167
+ ) -> dict[str, list[Document] | list[str]]:
168
+ """
169
+ Asynchronously use ddgs to search the web.
170
+
171
+ ddgs has no native async API, so the blocking search runs in a worker thread. Same parameters
172
+ and return values as :meth:`run`.
173
+
174
+ :param query:
175
+ Search query.
176
+ :param top_k:
177
+ Optional per-run override of the maximum number of results. If not provided, the
178
+ init-time `top_k` is used.
179
+ :param backend:
180
+ Optional per-run override of the ddgs backends. If not provided, the init-time
181
+ `backend` is used.
182
+ :param region:
183
+ Optional per-run override of the region/locale. If not provided, the init-time
184
+ `region` is used.
185
+ :param safesearch:
186
+ Optional per-run override of the safe-search level. If not provided, the init-time
187
+ `safesearch` is used.
188
+ :param search_params:
189
+ Optional per-run override of the extra `DDGS().text()` arguments. If provided, fully
190
+ replaces the init-time `search_params`.
191
+ :returns:
192
+ A dictionary with `documents` and `links` keys.
193
+ """
194
+ return await asyncio.to_thread(self._search, query, top_k, backend, region, safesearch, search_params)
195
+
196
+ @staticmethod
197
+ def _parse_response(results: list[dict[str, Any]]) -> tuple[list[Document], list[str]]:
198
+ """
199
+ Convert ddgs text results into Haystack Documents and links.
200
+
201
+ :param results:
202
+ The list of result dictionaries returned by `DDGS().text()`.
203
+ :returns:
204
+ A tuple of `(documents, links)`.
205
+ """
206
+ documents: list[Document] = []
207
+ links: list[str] = []
208
+
209
+ for result in results:
210
+ url = result.get("href", "")
211
+ title = result.get("title", "")
212
+ content = result.get("body", "")
213
+ documents.append(Document(content=content, meta={"title": title, "url": url}))
214
+ if url:
215
+ links.append(url)
216
+
217
+ return documents, links
@@ -109,6 +109,25 @@ class TestDDGSWebSearch:
109
109
  assert kwargs["region"] == "fr-fr"
110
110
  assert kwargs["timelimit"] == "d"
111
111
 
112
+ @patch(DDGS_PATH)
113
+ def test_run_overrides_params_at_runtime(self, mock_ddgs):
114
+ mock_ddgs.return_value.text.return_value = []
115
+ ws = DDGSWebSearch(top_k=10, backend="auto", region="us-en", safesearch="moderate")
116
+ ws.run(
117
+ query="q",
118
+ top_k=3,
119
+ backend="duckduckgo, brave",
120
+ region="de-de",
121
+ safesearch="off",
122
+ search_params={"timelimit": "w"},
123
+ )
124
+ _, kwargs = mock_ddgs.return_value.text.call_args
125
+ assert kwargs["max_results"] == 3
126
+ assert kwargs["backend"] == "duckduckgo, brave"
127
+ assert kwargs["region"] == "de-de"
128
+ assert kwargs["safesearch"] == "off"
129
+ assert kwargs["timelimit"] == "w"
130
+
112
131
  @pytest.mark.asyncio
113
132
  @patch(DDGS_PATH)
114
133
  async def test_run_async(self, mock_ddgs):
@@ -117,9 +136,19 @@ class TestDDGSWebSearch:
117
136
  assert len(result["documents"]) == 2
118
137
  assert result["links"] == ["https://haystack.deepset.ai", "https://www.deepset.ai"]
119
138
 
139
+ @pytest.mark.asyncio
140
+ @patch(DDGS_PATH)
141
+ async def test_run_async_overrides_params_at_runtime(self, mock_ddgs):
142
+ mock_ddgs.return_value.text.return_value = []
143
+ ws = DDGSWebSearch(top_k=10, region="us-en")
144
+ await ws.run_async(query="q", top_k=3, region="de-de")
145
+ _, kwargs = mock_ddgs.return_value.text.call_args
146
+ assert kwargs["max_results"] == 3
147
+ assert kwargs["region"] == "de-de"
148
+
120
149
  @pytest.mark.integration
121
150
  def test_web_search_integration(self):
122
- result = DDGSWebSearch(top_k=3).run(query="What is the deepset Haystack framework?")
151
+ result = DDGSWebSearch(top_k=3).run(query="Python programming language")
123
152
  assert len(result["documents"]) > 0
124
153
  assert all(isinstance(doc, Document) for doc in result["documents"])
125
154
  assert all(doc.meta.get("url") for doc in result["documents"])
@@ -128,6 +157,6 @@ class TestDDGSWebSearch:
128
157
  @pytest.mark.asyncio
129
158
  @pytest.mark.integration
130
159
  async def test_web_search_async_integration(self):
131
- result = await DDGSWebSearch(top_k=3).run_async(query="What is the deepset Haystack framework?")
160
+ result = await DDGSWebSearch(top_k=3).run_async(query="Python programming language")
132
161
  assert len(result["documents"]) > 0
133
162
  assert result["links"]
@@ -1,161 +0,0 @@
1
- # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- import asyncio
6
- from typing import Any
7
-
8
- from haystack import Document, component, logging
9
-
10
- from ddgs import DDGS
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
-
15
- @component
16
- class DDGSWebSearch:
17
- """
18
- Searches the web with ddgs (Dux Distributed Global Search) and returns results as Haystack Documents.
19
-
20
- [ddgs](https://github.com/deedy5/ddgs) is a free, **keyless** metasearch library that aggregates
21
- results from multiple backends (DuckDuckGo, Google, Bing, Brave, Yahoo, Yandex, Mullvad, and more),
22
- so no API key is required.
23
-
24
- ### Usage example
25
-
26
- ```python
27
- from haystack_integrations.components.websearch.ddgs import DDGSWebSearch
28
-
29
- websearch = DDGSWebSearch(top_k=5)
30
- result = websearch.run(query="What is Haystack by deepset?")
31
-
32
- documents = result["documents"]
33
- links = result["links"]
34
- ```
35
- """
36
-
37
- def __init__(
38
- self,
39
- top_k: int = 10,
40
- backend: str = "auto",
41
- region: str = "us-en",
42
- safesearch: str = "moderate",
43
- search_params: dict[str, Any] | None = None,
44
- ) -> None:
45
- """
46
- Initialize the DDGSWebSearch component.
47
-
48
- :param top_k:
49
- Maximum number of results to return.
50
- :param backend:
51
- Comma-separated ddgs backends to query, or ``"auto"`` to let ddgs choose
52
- (for example ``"duckduckgo, google, brave"``). See the ddgs docs for the full list.
53
- :param region:
54
- Region/locale for the search, for example ``"us-en"``, ``"de-de"``, or ``"wt-wt"`` (no region).
55
- :param safesearch:
56
- Safe-search level: ``"on"``, ``"moderate"``, or ``"off"``.
57
- :param search_params:
58
- Additional keyword arguments forwarded to ``DDGS().text()`` (for example ``page`` or
59
- ``timelimit``). Values here override ``backend``, ``region``, ``safesearch``, and ``top_k``
60
- on conflict.
61
- """
62
- self.top_k = top_k
63
- self.backend = backend
64
- self.region = region
65
- self.safesearch = safesearch
66
- self.search_params = search_params or {}
67
- self._client: DDGS | None = None
68
-
69
- def warm_up(self) -> None:
70
- """
71
- Initialize the ddgs client.
72
-
73
- Called automatically on first use. Can be called explicitly to avoid cold-start latency.
74
- """
75
- if self._client is None:
76
- self._client = DDGS()
77
-
78
- def _search(self, query: str) -> dict[str, list[Document] | list[str]]:
79
- """
80
- Run the blocking ddgs search and normalise the results.
81
-
82
- :param query:
83
- Search query.
84
- :returns:
85
- A dictionary with ``documents`` and ``links`` keys.
86
- """
87
- if self._client is None:
88
- self.warm_up()
89
- if self._client is None:
90
- msg = "DDGSWebSearch client failed to initialize."
91
- raise RuntimeError(msg)
92
-
93
- params: dict[str, Any] = {
94
- "region": self.region,
95
- "safesearch": self.safesearch,
96
- "backend": self.backend,
97
- "max_results": self.top_k,
98
- }
99
- params.update(self.search_params)
100
-
101
- results = self._client.text(query, **params)
102
- documents, links = self._parse_response(results)
103
-
104
- logger.debug(
105
- "ddgs returned {number_documents} documents for the query '{query}'",
106
- number_documents=len(documents),
107
- query=query,
108
- )
109
- return {"documents": documents, "links": links}
110
-
111
- @component.output_types(documents=list[Document], links=list[str])
112
- def run(self, query: str) -> dict[str, list[Document] | list[str]]:
113
- """
114
- Use ddgs to search the web.
115
-
116
- :param query:
117
- Search query.
118
- :returns:
119
- A dictionary with the following keys:
120
- - ``documents``: List of documents returned by the search backends.
121
- - ``links``: List of links returned by the search backends.
122
- """
123
- return self._search(query)
124
-
125
- @component.output_types(documents=list[Document], links=list[str])
126
- async def run_async(self, query: str) -> dict[str, list[Document] | list[str]]:
127
- """
128
- Asynchronously use ddgs to search the web.
129
-
130
- ddgs has no native async API, so the blocking search runs in a worker thread. Same parameters
131
- and return values as :meth:`run`.
132
-
133
- :param query:
134
- Search query.
135
- :returns:
136
- A dictionary with ``documents`` and ``links`` keys.
137
- """
138
- return await asyncio.to_thread(self._search, query)
139
-
140
- @staticmethod
141
- def _parse_response(results: list[dict[str, Any]]) -> tuple[list[Document], list[str]]:
142
- """
143
- Convert ddgs text results into Haystack Documents and links.
144
-
145
- :param results:
146
- The list of result dictionaries returned by ``DDGS().text()``.
147
- :returns:
148
- A tuple of ``(documents, links)``.
149
- """
150
- documents: list[Document] = []
151
- links: list[str] = []
152
-
153
- for result in results:
154
- url = result.get("href", "")
155
- title = result.get("title", "")
156
- content = result.get("body", "")
157
- documents.append(Document(content=content, meta={"title": title, "url": url}))
158
- if url:
159
- links.append(url)
160
-
161
- return documents, links
File without changes
File without changes