firecrawl-haystack 1.0.0__tar.gz → 1.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.
Files changed (17) hide show
  1. firecrawl_haystack-1.1.0/CHANGELOG.md +15 -0
  2. {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/PKG-INFO +2 -2
  3. {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/pydoc/config_docusaurus.yml +2 -1
  4. {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/pyproject.toml +2 -1
  5. firecrawl_haystack-1.1.0/src/haystack_integrations/components/websearch/firecrawl/__init__.py +7 -0
  6. firecrawl_haystack-1.1.0/src/haystack_integrations/components/websearch/firecrawl/firecrawl_websearch.py +193 -0
  7. firecrawl_haystack-1.1.0/src/haystack_integrations/components/websearch/py.typed +0 -0
  8. firecrawl_haystack-1.1.0/tests/__init__.py +3 -0
  9. firecrawl_haystack-1.1.0/tests/test_firecrawl_websearch.py +276 -0
  10. {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/.gitignore +0 -0
  11. {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/LICENSE +0 -0
  12. {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/README.md +0 -0
  13. {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/src/haystack_integrations/components/fetchers/firecrawl/__init__.py +0 -0
  14. {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/src/haystack_integrations/components/fetchers/firecrawl/firecrawl_crawler.py +0 -0
  15. {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/src/haystack_integrations/components/fetchers/py.typed +0 -0
  16. {firecrawl_haystack-1.0.0/tests → firecrawl_haystack-1.1.0/src/haystack_integrations/components/websearch}/__init__.py +0 -0
  17. {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/tests/test_firecrawl_crawler.py +0 -0
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## [integrations/firecrawl-v1.0.1] - 2026-02-24
4
+
5
+ ### 🌀 Miscellaneous
6
+
7
+ - Docs: Update Firecrawl module path in Docusaurus configuration (#2869)
8
+
9
+ ## [integrations/firecrawl-v1.0.0] - 2026-02-24
10
+
11
+ ### 🚀 Features
12
+
13
+ - Add FirecrawlCrawler (#2859)
14
+
15
+ <!-- generated by git-cliff -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firecrawl-haystack
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Haystack integration for Firecrawl
5
5
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/firecrawl#readme
6
6
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
@@ -8,7 +8,7 @@ Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tr
8
8
  Author-email: deepset GmbH <info@deepset.ai>
9
9
  License-Expression: Apache-2.0
10
10
  License-File: LICENSE
11
- Keywords: Firecrawl,Haystack,Web Crawler,Web Scraping
11
+ Keywords: Firecrawl,Haystack,Web Crawler,Web Scraping,Web Search
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: License :: OSI Approved :: Apache Software License
14
14
  Classifier: Programming Language :: Python
@@ -1,6 +1,7 @@
1
1
  loaders:
2
2
  - modules:
3
- - haystack_integrations.components.fetchers.firecrawl
3
+ - haystack_integrations.components.fetchers.firecrawl.firecrawl_crawler
4
+ - haystack_integrations.components.websearch.firecrawl.firecrawl_websearch
4
5
  search_path: [../src]
5
6
  processors:
6
7
  - type: filter
@@ -14,6 +14,7 @@ keywords = [
14
14
  "Haystack",
15
15
  "Web Crawler",
16
16
  "Web Scraping",
17
+ "Web Search",
17
18
  ]
18
19
  authors = [
19
20
  { name = "deepset GmbH", email = "info@deepset.ai" },
@@ -73,7 +74,7 @@ unit = 'pytest -m "not integration" {args:tests}'
73
74
  integration = 'pytest -m "integration" {args:tests}'
74
75
  all = 'pytest {args:tests}'
75
76
  cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
76
- types = "mypy -p haystack_integrations.components.fetchers.firecrawl {args}"
77
+ types = "mypy -p haystack_integrations.components.fetchers.firecrawl -p haystack_integrations.components.websearch.firecrawl {args}"
77
78
 
78
79
  [tool.mypy]
79
80
  install_types = true
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2026-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from haystack_integrations.components.websearch.firecrawl.firecrawl_websearch import FirecrawlWebSearch
6
+
7
+ __all__ = ["FirecrawlWebSearch"]
@@ -0,0 +1,193 @@
1
+ # SPDX-FileCopyrightText: 2026-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from typing import Any
6
+
7
+ from firecrawl import AsyncFirecrawl, Firecrawl # type: ignore[import-untyped]
8
+ from firecrawl.types import SearchData # type: ignore[import-untyped]
9
+ from haystack import Document, component, logging
10
+ from haystack.utils import Secret
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @component
16
+ class FirecrawlWebSearch:
17
+ """
18
+ A component that uses Firecrawl to search the web and return results as Haystack Documents.
19
+
20
+ This component wraps the Firecrawl Search API, enabling web search queries that return
21
+ structured documents with content and links. It follows the standard Haystack WebSearch
22
+ component interface.
23
+
24
+ Firecrawl is a service that crawls and scrapes websites, returning content in formats suitable
25
+ for LLMs. You need a Firecrawl API key from [firecrawl.dev](https://firecrawl.dev).
26
+
27
+ ### Usage example
28
+
29
+ ```python
30
+ from haystack_integrations.components.websearch.firecrawl import FirecrawlWebSearch
31
+ from haystack.utils import Secret
32
+
33
+ websearch = FirecrawlWebSearch(
34
+ api_key=Secret.from_env_var("FIRECRAWL_API_KEY"),
35
+ top_k=5,
36
+ )
37
+ result = websearch.run(query="What is Haystack by deepset?")
38
+ documents = result["documents"]
39
+ links = result["links"]
40
+ ```
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ api_key: Secret = Secret.from_env_var("FIRECRAWL_API_KEY"),
46
+ top_k: int | None = 10,
47
+ search_params: dict[str, Any] | None = None,
48
+ ) -> None:
49
+ """
50
+ Initialize the FirecrawlWebSearch component.
51
+
52
+ :param api_key:
53
+ API key for Firecrawl.
54
+ Defaults to the `FIRECRAWL_API_KEY` environment variable.
55
+ :param top_k:
56
+ Maximum number of documents to return.
57
+ Defaults to 10. This can be overridden by the `"limit"` parameter in `search_params`.
58
+ :param search_params:
59
+ Additional parameters passed to the Firecrawl search API.
60
+ See the [Firecrawl API reference](https://docs.firecrawl.dev/api-reference/endpoint/search)
61
+ for available parameters. Supported keys include: `tbs`, `location`,
62
+ `scrape_options`, `sources`, `categories`, `timeout`.
63
+ """
64
+ self.api_key = api_key
65
+ self.top_k = top_k
66
+ self.search_params = search_params
67
+ self._search_params = {} if search_params is None else search_params.copy()
68
+ self._firecrawl_client: Firecrawl | None = None
69
+ self._async_firecrawl_client: AsyncFirecrawl | None = None
70
+
71
+ def warm_up(self) -> None:
72
+ """
73
+ Warm up the Firecrawl clients by initializing the sync and async clients.
74
+ This is useful to avoid cold start delays when performing searches.
75
+ """
76
+ if self._firecrawl_client is None:
77
+ self._firecrawl_client = Firecrawl(api_key=self.api_key.resolve_value())
78
+ if self._async_firecrawl_client is None:
79
+ self._async_firecrawl_client = AsyncFirecrawl(api_key=self.api_key.resolve_value())
80
+
81
+ @component.output_types(documents=list[Document], links=list[str])
82
+ def run(
83
+ self,
84
+ query: str,
85
+ search_params: dict[str, Any] | None = None,
86
+ ) -> dict[str, Any]:
87
+ """
88
+ Search the web using Firecrawl and return results as Documents.
89
+
90
+ :param query: Search query string.
91
+ :param search_params:
92
+ Optional override of search parameters for this run.
93
+ If provided, fully replaces the init-time search_params.
94
+ :returns: A dictionary with the following keys:
95
+ - `documents`: List of documents with search result content.
96
+ - `links`: List of URLs from the search results.
97
+ """
98
+ if self._firecrawl_client is None:
99
+ self.warm_up()
100
+
101
+ current_params = search_params if search_params is not None else self._search_params
102
+ params = current_params.copy()
103
+ if "limit" not in params and self.top_k is not None:
104
+ params["limit"] = self.top_k
105
+
106
+ try:
107
+ search_response = self._firecrawl_client.search( # type: ignore[union-attr]
108
+ query=query,
109
+ **params,
110
+ )
111
+ except Exception as error:
112
+ logger.exception(f"Failed to search for query '{query}': {error}")
113
+ return {"documents": [], "links": []}
114
+
115
+ documents, links = self._parse_search_response(search_response)
116
+ return {"documents": documents, "links": links}
117
+
118
+ @component.output_types(documents=list[Document], links=list[str])
119
+ async def run_async(
120
+ self,
121
+ query: str,
122
+ search_params: dict[str, Any] | None = None,
123
+ ) -> dict[str, Any]:
124
+ """
125
+ Asynchronously search the web using Firecrawl and return results as Documents.
126
+
127
+ :param query: Search query string.
128
+ :param search_params:
129
+ Optional override of search parameters for this run.
130
+ If provided, fully replaces the init-time search_params.
131
+ :returns: A dictionary with the following keys:
132
+ - `documents`: List of documents with search result content.
133
+ - `links`: List of URLs from the search results.
134
+ """
135
+ if self._async_firecrawl_client is None:
136
+ self.warm_up()
137
+
138
+ current_params = search_params if search_params is not None else self._search_params
139
+ params = current_params.copy()
140
+ if "limit" not in params and self.top_k is not None:
141
+ params["limit"] = self.top_k
142
+
143
+ try:
144
+ search_response = await self._async_firecrawl_client.search( # type: ignore[union-attr]
145
+ query=query,
146
+ **params,
147
+ )
148
+ except Exception as error:
149
+ logger.exception(f"Failed to search for query '{query}': {error}")
150
+ return {"documents": [], "links": []}
151
+
152
+ documents, links = self._parse_search_response(search_response)
153
+ return {"documents": documents, "links": links}
154
+
155
+ @staticmethod
156
+ def _parse_search_response(search_response: SearchData) -> tuple[list[Document], list[str]]:
157
+ """
158
+ Convert a Firecrawl search response to Haystack Documents and links.
159
+
160
+ :param search_response: Firecrawl search response object.
161
+ :returns: Tuple of (documents, links).
162
+ """
163
+ documents: list[Document] = []
164
+ links: list[str] = []
165
+
166
+ web_results = search_response.web or []
167
+ for result in web_results:
168
+ url = ""
169
+ title = ""
170
+ content = ""
171
+
172
+ if hasattr(result, "markdown") and result.markdown:
173
+ content = result.markdown
174
+ metadata = result.metadata_dict if hasattr(result, "metadata_dict") else {}
175
+ url = metadata.get("url", getattr(result, "url", ""))
176
+ title = metadata.get("title", "")
177
+ else:
178
+ url = getattr(result, "url", "")
179
+ title = getattr(result, "title", "")
180
+ content = getattr(result, "description", "")
181
+
182
+ doc = Document(
183
+ content=content,
184
+ meta={
185
+ "title": title,
186
+ "url": url,
187
+ },
188
+ )
189
+ documents.append(doc)
190
+ if url:
191
+ links.append(url)
192
+
193
+ return documents, links
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2026-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,276 @@
1
+ # SPDX-FileCopyrightText: 2026-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import os
6
+ from unittest.mock import AsyncMock, MagicMock, patch
7
+
8
+ import pytest
9
+ from haystack import Document
10
+ from haystack.core.serialization import component_from_dict, component_to_dict
11
+ from haystack.utils import Secret
12
+
13
+ from haystack_integrations.components.websearch.firecrawl import FirecrawlWebSearch
14
+
15
+
16
+ class TestFirecrawlWebSearch:
17
+ @pytest.fixture
18
+ def search_result_web(self) -> MagicMock:
19
+ """A SearchResultWeb-like object (no markdown, just search metadata)."""
20
+ result = MagicMock(spec=["url", "title", "description"])
21
+ result.url = "https://example.com"
22
+ result.title = "Example Title"
23
+ result.description = "Example description snippet"
24
+ return result
25
+
26
+ @pytest.fixture
27
+ def search_result_document(self) -> MagicMock:
28
+ """A Document-like object (with markdown, from scrapeOptions)."""
29
+ result = MagicMock(spec=["markdown", "metadata_dict", "url"])
30
+ result.markdown = "# Full page content\nSome markdown text."
31
+ result.metadata_dict = {"url": "https://example.com/page", "title": "Page Title"}
32
+ result.url = "https://example.com/page"
33
+ return result
34
+
35
+ @pytest.fixture
36
+ def search_response(self, search_result_web) -> MagicMock:
37
+ """Standard search response with web results."""
38
+ response = MagicMock()
39
+ response.web = [search_result_web]
40
+ return response
41
+
42
+ @pytest.fixture
43
+ def mock_client(self, search_response) -> MagicMock:
44
+ client = MagicMock()
45
+ client.search.return_value = search_response
46
+ return client
47
+
48
+ @pytest.fixture
49
+ def mock_async_client(self, search_response) -> MagicMock:
50
+ client = MagicMock()
51
+ client.search = AsyncMock(return_value=search_response)
52
+ return client
53
+
54
+ def test_init_default(self, monkeypatch: pytest.MonkeyPatch) -> None:
55
+ monkeypatch.setenv("FIRECRAWL_API_KEY", "test-key")
56
+ ws = FirecrawlWebSearch()
57
+ assert ws.top_k == 10
58
+ assert ws._search_params == {}
59
+ assert ws.api_key.resolve_value() == "test-key"
60
+
61
+ def test_init_with_params(self) -> None:
62
+ ws = FirecrawlWebSearch(
63
+ api_key=Secret.from_token("custom-key"),
64
+ top_k=5,
65
+ search_params={"tbs": "qdr:d", "location": "US"},
66
+ )
67
+ assert ws.top_k == 5
68
+ assert ws._search_params == {"tbs": "qdr:d", "location": "US"}
69
+ assert ws.api_key.resolve_value() == "custom-key"
70
+
71
+ def test_to_dict(self, monkeypatch: pytest.MonkeyPatch) -> None:
72
+ monkeypatch.setenv("FIRECRAWL_API_KEY", "test-key")
73
+ ws = FirecrawlWebSearch(top_k=5, search_params={"tbs": "qdr:d"})
74
+ data = component_to_dict(ws, "FirecrawlWebSearch")
75
+ assert (
76
+ data["type"]
77
+ == "haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.FirecrawlWebSearch"
78
+ )
79
+ assert data["init_parameters"]["top_k"] == 5
80
+ assert data["init_parameters"]["search_params"] == {"tbs": "qdr:d"}
81
+
82
+ def test_from_dict(self, monkeypatch: pytest.MonkeyPatch) -> None:
83
+ monkeypatch.setenv("FIRECRAWL_API_KEY", "test-key")
84
+ data = {
85
+ "type": ("haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.FirecrawlWebSearch"),
86
+ "init_parameters": {
87
+ "top_k": 3,
88
+ "search_params": {"location": "UK"},
89
+ "api_key": {"env_vars": ["FIRECRAWL_API_KEY"], "strict": True, "type": "env_var"},
90
+ },
91
+ }
92
+ ws = component_from_dict(FirecrawlWebSearch, data, "FirecrawlWebSearch")
93
+ assert ws.top_k == 3
94
+ assert ws.search_params == {"location": "UK"}
95
+ assert ws.api_key.resolve_value() == "test-key"
96
+
97
+ def test_run_returns_documents_and_links(self, mock_client) -> None:
98
+ ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"), top_k=10)
99
+ ws._firecrawl_client = mock_client
100
+
101
+ result = ws.run(query="test query")
102
+
103
+ assert "documents" in result
104
+ assert "links" in result
105
+ assert len(result["documents"]) == 1
106
+ assert isinstance(result["documents"][0], Document)
107
+ assert result["documents"][0].content == "Example description snippet"
108
+ assert result["documents"][0].meta["url"] == "https://example.com"
109
+ assert result["documents"][0].meta["title"] == "Example Title"
110
+ assert result["links"] == ["https://example.com"]
111
+ mock_client.search.assert_called_once_with(query="test query", limit=10)
112
+
113
+ def test_run_with_scraped_documents(self, search_result_document, mock_client) -> None:
114
+ """When scrapeOptions are used, results contain markdown content."""
115
+ search_response = MagicMock()
116
+ search_response.web = [search_result_document]
117
+ mock_client.search.return_value = search_response
118
+
119
+ ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"), top_k=10)
120
+ ws._firecrawl_client = mock_client
121
+
122
+ result = ws.run(query="test query")
123
+
124
+ assert result["documents"][0].content == "# Full page content\nSome markdown text."
125
+ assert result["documents"][0].meta["url"] == "https://example.com/page"
126
+ assert result["documents"][0].meta["title"] == "Page Title"
127
+
128
+ def test_run_overrides_init_params_with_runtime_params(self, mock_client) -> None:
129
+ ws = FirecrawlWebSearch(
130
+ api_key=Secret.from_token("test-key"),
131
+ top_k=10,
132
+ search_params={"location": "US"},
133
+ )
134
+ ws._firecrawl_client = mock_client
135
+
136
+ ws.run(query="test", search_params={"location": "UK", "limit": 5})
137
+
138
+ mock_client.search.assert_called_once_with(
139
+ query="test",
140
+ location="UK",
141
+ limit=5,
142
+ )
143
+
144
+ @pytest.mark.asyncio
145
+ async def test_run_async(self, mock_async_client) -> None:
146
+ ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"), top_k=10)
147
+ ws._async_firecrawl_client = mock_async_client
148
+
149
+ result = await ws.run_async(query="test query")
150
+
151
+ assert len(result["documents"]) == 1
152
+ assert isinstance(result["documents"][0], Document)
153
+ assert result["links"] == ["https://example.com"]
154
+ mock_async_client.search.assert_awaited_once()
155
+
156
+ def test_run_returns_empty_on_error(self, mock_client) -> None:
157
+ mock_client.search.side_effect = Exception("API error")
158
+
159
+ ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
160
+ ws._firecrawl_client = mock_client
161
+
162
+ result = ws.run(query="test")
163
+ assert result["documents"] == []
164
+ assert result["links"] == []
165
+
166
+ @pytest.mark.asyncio
167
+ async def test_run_async_returns_empty_on_error(self, mock_async_client) -> None:
168
+ mock_async_client.search = AsyncMock(side_effect=Exception("API error"))
169
+
170
+ ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
171
+ ws._async_firecrawl_client = mock_async_client
172
+
173
+ result = await ws.run_async(query="test")
174
+ assert result["documents"] == []
175
+ assert result["links"] == []
176
+
177
+ def test_run_calls_warm_up(self, search_response) -> None:
178
+ with (
179
+ patch(
180
+ "haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.Firecrawl"
181
+ ) as mock_firecrawl_client,
182
+ patch("haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.AsyncFirecrawl"),
183
+ ):
184
+ mock_firecrawl_client.return_value.search.return_value = search_response
185
+
186
+ ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
187
+ ws.run(query="test")
188
+
189
+ assert ws._firecrawl_client is mock_firecrawl_client.return_value
190
+ mock_firecrawl_client.assert_called_once_with(api_key="test-key")
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_run_async_calls_warm_up(self, search_response) -> None:
194
+ with (
195
+ patch("haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.Firecrawl"),
196
+ patch(
197
+ "haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.AsyncFirecrawl"
198
+ ) as mock_async_firecrawl_client,
199
+ ):
200
+ mock_async_firecrawl_client.return_value.search = AsyncMock(return_value=search_response)
201
+
202
+ ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
203
+ await ws.run_async(query="test")
204
+
205
+ assert ws._async_firecrawl_client is mock_async_firecrawl_client.return_value
206
+ mock_async_firecrawl_client.assert_called_once_with(api_key="test-key")
207
+
208
+ def test_warm_up_initializes_clients(self) -> None:
209
+ ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
210
+ assert ws._firecrawl_client is None
211
+ assert ws._async_firecrawl_client is None
212
+
213
+ ws.warm_up()
214
+
215
+ assert ws._firecrawl_client is not None
216
+ assert ws._async_firecrawl_client is not None
217
+
218
+ def test_run_empty_web_results(self, mock_client) -> None:
219
+ empty_response = MagicMock()
220
+ empty_response.web = []
221
+ mock_client.search.return_value = empty_response
222
+
223
+ ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
224
+ ws._firecrawl_client = mock_client
225
+ result = ws.run(query="obscure query")
226
+ assert result["documents"] == []
227
+ assert result["links"] == []
228
+
229
+ def test_run_none_web_results(self, mock_client) -> None:
230
+ none_response = MagicMock()
231
+ none_response.web = None
232
+ mock_client.search.return_value = none_response
233
+
234
+ ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
235
+ ws._firecrawl_client = mock_client
236
+ result = ws.run(query="obscure query")
237
+ assert result["documents"] == []
238
+ assert result["links"] == []
239
+
240
+ @pytest.mark.skipif(
241
+ not os.environ.get("FIRECRAWL_API_KEY"),
242
+ reason="Export FIRECRAWL_API_KEY to run integration tests.",
243
+ )
244
+ @pytest.mark.integration
245
+ def test_run_integration(self) -> None:
246
+ ws = FirecrawlWebSearch(
247
+ api_key=Secret.from_env_var("FIRECRAWL_API_KEY"),
248
+ top_k=3,
249
+ )
250
+ result = ws.run(query="What is Haystack by deepset?")
251
+
252
+ assert "documents" in result
253
+ assert "links" in result
254
+ assert isinstance(result["documents"], list)
255
+ assert len(result["documents"]) > 0
256
+ assert len(result["links"]) > 0
257
+ assert isinstance(result["documents"][0], Document)
258
+ assert result["documents"][0].content
259
+
260
+ @pytest.mark.skipif(
261
+ not os.environ.get("FIRECRAWL_API_KEY"),
262
+ reason="Export FIRECRAWL_API_KEY to run integration tests.",
263
+ )
264
+ @pytest.mark.integration
265
+ @pytest.mark.asyncio
266
+ async def test_run_async_integration(self) -> None:
267
+ ws = FirecrawlWebSearch(
268
+ api_key=Secret.from_env_var("FIRECRAWL_API_KEY"),
269
+ top_k=3,
270
+ )
271
+ result = await ws.run_async(query="What is Haystack by deepset?")
272
+
273
+ assert "documents" in result
274
+ assert "links" in result
275
+ assert len(result["documents"]) > 0
276
+ assert len(result["links"]) > 0