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.
- firecrawl_haystack-1.1.0/CHANGELOG.md +15 -0
- {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/PKG-INFO +2 -2
- {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/pydoc/config_docusaurus.yml +2 -1
- {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/pyproject.toml +2 -1
- firecrawl_haystack-1.1.0/src/haystack_integrations/components/websearch/firecrawl/__init__.py +7 -0
- firecrawl_haystack-1.1.0/src/haystack_integrations/components/websearch/firecrawl/firecrawl_websearch.py +193 -0
- firecrawl_haystack-1.1.0/src/haystack_integrations/components/websearch/py.typed +0 -0
- firecrawl_haystack-1.1.0/tests/__init__.py +3 -0
- firecrawl_haystack-1.1.0/tests/test_firecrawl_websearch.py +276 -0
- {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/.gitignore +0 -0
- {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/LICENSE +0 -0
- {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/README.md +0 -0
- {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/src/haystack_integrations/components/fetchers/firecrawl/__init__.py +0 -0
- {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/src/haystack_integrations/components/fetchers/firecrawl/firecrawl_crawler.py +0 -0
- {firecrawl_haystack-1.0.0 → firecrawl_haystack-1.1.0}/src/haystack_integrations/components/fetchers/py.typed +0 -0
- {firecrawl_haystack-1.0.0/tests → firecrawl_haystack-1.1.0/src/haystack_integrations/components/websearch}/__init__.py +0 -0
- {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.
|
|
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,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
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|