apify-haystack 0.1.8b1__tar.gz → 0.1.8b2__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.
@@ -28,6 +28,7 @@ All notable changes to this project will be documented in this file.
28
28
  ### 🚜 Refactor
29
29
 
30
30
  - [**breaking**] Make the constants module private ([#68](https://github.com/apify/apify-haystack/pull/68)) ([8f5dbbd](https://github.com/apify/apify-haystack/commit/8f5dbbd7a4fa0e8c3c7c8036799d40744ee47624)) by [@vdusek](https://github.com/vdusek)
31
+ - [**breaking**] Make all modules private and export the public API from the package root ([#77](https://github.com/apify/apify-haystack/pull/77)) ([4e6ccca](https://github.com/apify/apify-haystack/commit/4e6cccae287999f9ceb72fe9b67c2305b1206113)) by [@vdusek](https://github.com/vdusek)
31
32
 
32
33
  ### ⚙️ Miscellaneous Tasks
33
34
 
@@ -32,6 +32,8 @@ We use [ruff](https://docs.astral.sh/ruff/) for linting and formatting, [ty](htt
32
32
 
33
33
  Unit tests (`tests/unit`) are isolated from the Apify API. Run them with `uv run poe unit-tests`, or a single test with `uv run pytest tests/unit/<path>.py::<test_name>`.
34
34
 
35
+ Integration tests (`tests/integration`) run the components against the Apify platform, so they need an Apify API token in the `APIFY_TOKEN` environment variable and are skipped without one. Run them with `uv run poe integration-tests`. They run real Actors on that account, which costs a little. CI runs them on pull requests from this repository and on pushes to `main`, with the test user's token.
36
+
35
37
  ## Documentation
36
38
 
37
39
  We adhere to the [Google docstring format](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) for documenting our codebase. Every user-facing class or method is documented. Documentation standards are enforced using [Ruff](https://docs.astral.sh/ruff/).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apify_haystack
3
- Version: 0.1.8b1
3
+ Version: 0.1.8b2
4
4
  Summary: Apify-haystack integration
5
5
  Keywords: apify,crawler,haystack,rag,scraper,scraping
6
6
  Author: Apify Technologies s.r.o.
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "apify_haystack"
7
- version = "0.1.8b1"
7
+ version = "0.1.8b2"
8
8
  description = "Apify-haystack integration"
9
9
  license = "Apache-2.0"
10
10
  license-files = ["LICENSE"]
@@ -191,6 +191,14 @@ help = "Run unit tests"
191
191
  cmd = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify_haystack --cov-report=xml:coverage-unit.xml tests/unit"
192
192
  help = "Run unit tests with an XML coverage report"
193
193
 
194
+ [tool.poe.tasks.integration-tests]
195
+ cmd = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} tests/integration"
196
+ help = "Run integration tests against the Apify platform"
197
+
198
+ [tool.poe.tasks.integration-tests-cov]
199
+ cmd = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify_haystack --cov-report=xml:coverage-integration.xml tests/integration"
200
+ help = "Run integration tests with an XML coverage report"
201
+
194
202
  [tool.poe.tasks.check-code]
195
203
  sequence = [
196
204
  "lint",
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "apify_haystack"
7
- version = "0.1.8b1"
7
+ version = "0.1.8b2"
8
8
  description = "Apify-haystack integration"
9
9
  authors = [{ name = "Apify Technologies s.r.o.", email = "support@apify.com" }]
10
10
  license = "Apache-2.0"
@@ -156,6 +156,8 @@ build = { cmd = "uv build", help = "Build the package" }
156
156
  type-check = { cmd = "uv run ty check", help = "Run the type checker" }
157
157
  unit-tests = { cmd = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} tests/unit", help = "Run unit tests" }
158
158
  unit-tests-cov = { cmd = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify_haystack --cov-report=xml:coverage-unit.xml tests/unit", help = "Run unit tests with an XML coverage report" }
159
+ integration-tests = { cmd = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} tests/integration", help = "Run integration tests against the Apify platform" }
160
+ integration-tests-cov = { cmd = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify_haystack --cov-report=xml:coverage-integration.xml tests/integration", help = "Run integration tests with an XML coverage report" }
159
161
  check-code = { sequence = ["lint", "type-check", "unit-tests"], help = "Run lint, type check, and unit tests" }
160
162
 
161
163
  [tool.poe.tasks.install-dev]
@@ -2,14 +2,14 @@ from importlib import metadata
2
2
 
3
3
  __version__ = metadata.version('apify-haystack')
4
4
 
5
- from apify_haystack.actor_tool import ApifyActorTool
6
- from apify_haystack.apify_dataset import (
5
+ from apify_haystack._actor_tool import ApifyActorTool
6
+ from apify_haystack._dataset import (
7
7
  ApifyDatasetFromActorCall,
8
8
  ApifyDatasetFromTaskCall,
9
9
  ApifyDatasetLoader,
10
10
  default_dataset_mapping_function,
11
11
  )
12
- from apify_haystack.web_search import ApifyWebSearch
12
+ from apify_haystack._web_search import ApifyWebSearch
13
13
 
14
14
  __all__ = [
15
15
  'ApifyActorTool',
@@ -10,11 +10,11 @@ from haystack.tools import Tool
10
10
  from haystack.utils import Secret, deserialize_secrets_inplace
11
11
 
12
12
  from apify_haystack._client import (
13
- _ApifyClientComponent,
14
- _create_client,
15
- _get_dataset_id,
16
- _to_decimal,
17
- _to_timedelta,
13
+ ApifyClientComponent,
14
+ create_client,
15
+ get_dataset_id,
16
+ to_decimal,
17
+ to_timedelta,
18
18
  )
19
19
 
20
20
  # OpenAI limits a function name to 64 characters and a description to 1024.
@@ -30,15 +30,16 @@ UNTRUSTED_CONTENT_END = '<<<END_EXTERNAL_UNTRUSTED_CONTENT>>>'
30
30
  _PROPERTY_SCHEMA_KEYS = ('type', 'description', 'enum', 'default', 'items', 'minimum', 'maximum')
31
31
 
32
32
 
33
- class ApifyActorTool(_ApifyClientComponent, Tool):
33
+ class ApifyActorTool(ApifyClientComponent, Tool):
34
34
  """A Haystack tool that runs an Apify Actor and returns the items of its default dataset.
35
35
 
36
36
  Use it to give a Haystack `Agent` access to any Actor from Apify Store. The tool takes its parameters from the
37
37
  Actor's input schema and its description from the Actor's definition, so the LLM sees the same inputs as a user
38
38
  of the Actor. Reading them needs one Apify API request when the tool is created without `parameters`.
39
39
 
40
- The LLM gets the dataset items as JSON between the `UNTRUSTED_CONTENT_START` and `UNTRUSTED_CONTENT_END` markers,
41
- since scraped content can carry prompt injections. `invoke` and `invoke_async` return the items unwrapped.
40
+ The LLM gets the dataset items as JSON between the `<<<EXTERNAL_UNTRUSTED_CONTENT>>>` and
41
+ `<<<END_EXTERNAL_UNTRUSTED_CONTENT>>>` markers, since scraped content can carry prompt injections. `invoke` and
42
+ `invoke_async` return the items unwrapped.
42
43
  For details, see https://docs.apify.com/platform/integrations/haystack
43
44
 
44
45
  Example:
@@ -53,6 +54,9 @@ class ApifyActorTool(_ApifyClientComponent, Tool):
53
54
  agent = Agent(chat_generator=OpenAIChatGenerator(), tools=[tool])
54
55
  """
55
56
 
57
+ # Serialized pipelines store the class by this public path, so it does not depend on the module layout.
58
+ __module__ = 'apify_haystack'
59
+
56
60
  def __init__(
57
61
  self,
58
62
  actor_id: str,
@@ -152,10 +156,10 @@ class ApifyActorTool(_ApifyClientComponent, Tool):
152
156
  run_input=run_input,
153
157
  build=self.build,
154
158
  memory_mbytes=self.memory_mbytes,
155
- run_timeout=_to_timedelta(self.timeout_secs),
156
- max_total_charge_usd=_to_decimal(self.max_total_charge_usd),
159
+ run_timeout=to_timedelta(self.timeout_secs),
160
+ max_total_charge_usd=to_decimal(self.max_total_charge_usd),
157
161
  )
158
- dataset_id = _get_dataset_id(run, f'Actor {self.actor_id}')
162
+ dataset_id = get_dataset_id(run, f'Actor {self.actor_id}')
159
163
  return client.dataset(dataset_id).list_items(clean=True, limit=self.dataset_items_limit).items
160
164
 
161
165
  async def _run_actor_async(self, **run_input: Any) -> list[dict]:
@@ -164,15 +168,15 @@ class ApifyActorTool(_ApifyClientComponent, Tool):
164
168
  run_input=run_input,
165
169
  build=self.build,
166
170
  memory_mbytes=self.memory_mbytes,
167
- run_timeout=_to_timedelta(self.timeout_secs),
168
- max_total_charge_usd=_to_decimal(self.max_total_charge_usd),
171
+ run_timeout=to_timedelta(self.timeout_secs),
172
+ max_total_charge_usd=to_decimal(self.max_total_charge_usd),
169
173
  )
170
- dataset_id = _get_dataset_id(run, f'Actor {self.actor_id}')
174
+ dataset_id = get_dataset_id(run, f'Actor {self.actor_id}')
171
175
  return (await client.dataset(dataset_id).list_items(clean=True, limit=self.dataset_items_limit)).items
172
176
 
173
177
 
174
178
  def _get_actor_definition(apify_token: Secret, actor_id: str) -> dict[str, Any]:
175
- client = _create_client(ApifyClient, apify_token.resolve_value())
179
+ client = create_client(ApifyClient, apify_token.resolve_value())
176
180
  try:
177
181
  build = client.actor(actor_id).default_build().get()
178
182
  finally:
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  _ClientT = TypeVar('_ClientT', ApifyClient, ApifyClientAsync)
16
16
 
17
17
 
18
- class _ApifyClientComponent:
18
+ class ApifyClientComponent:
19
19
  """Base for the components that call the Apify API.
20
20
 
21
21
  The sync client lives between `warm_up` and `close`, the async one between `warm_up_async` and `close_async`.
@@ -47,16 +47,16 @@ class _ApifyClientComponent:
47
47
 
48
48
  def _get_client(self) -> ApifyClient:
49
49
  if self.client is None:
50
- self.client = _create_client(ApifyClient, self.apify_token.resolve_value())
50
+ self.client = create_client(ApifyClient, self.apify_token.resolve_value())
51
51
  return self.client
52
52
 
53
53
  def _get_async_client(self) -> ApifyClientAsync:
54
54
  if self.async_client is None:
55
- self.async_client = _create_client(ApifyClientAsync, self.apify_token.resolve_value())
55
+ self.async_client = create_client(ApifyClientAsync, self.apify_token.resolve_value())
56
56
  return self.async_client
57
57
 
58
58
 
59
- def _create_client(client_class: type[_ClientT], token: str | None) -> _ClientT:
59
+ def create_client(client_class: type[_ClientT], token: str | None) -> _ClientT:
60
60
  """Create an Apify client that identifies its requests as coming from this integration."""
61
61
  client = client_class(token, headers={'x-apify-integration-platform': HAYSTACK_INTEGRATION_PLATFORM})
62
62
 
@@ -66,7 +66,7 @@ def _create_client(client_class: type[_ClientT], token: str | None) -> _ClientT:
66
66
  return client
67
67
 
68
68
 
69
- def _get_dataset_id(run: Run | None, label: str) -> str:
69
+ def get_dataset_id(run: Run | None, label: str) -> str:
70
70
  """Return the default dataset of a finished run, raising unless the run succeeded."""
71
71
  if run is None:
72
72
  raise ValueError(f'No response found in the {label} call')
@@ -75,10 +75,10 @@ def _get_dataset_id(run: Run | None, label: str) -> str:
75
75
  return run.default_dataset_id
76
76
 
77
77
 
78
- def _to_timedelta(seconds: int | None) -> timedelta | None:
78
+ def to_timedelta(seconds: int | None) -> timedelta | None:
79
79
  return None if seconds is None else timedelta(seconds=seconds)
80
80
 
81
81
 
82
- def _to_decimal(value: float | None) -> Decimal | None:
82
+ def to_decimal(value: float | None) -> Decimal | None:
83
83
  # `str` keeps the amount as written; `Decimal(0.1)` would carry the binary float error.
84
84
  return None if value is None else Decimal(str(value))
@@ -7,7 +7,7 @@ from haystack import component, default_from_dict, default_to_dict
7
7
  from haystack.dataclasses import Document
8
8
  from haystack.utils import Secret, deserialize_callable, serialize_callable
9
9
 
10
- from apify_haystack._client import _ApifyClientComponent, _get_dataset_id, _to_decimal, _to_timedelta
10
+ from apify_haystack._client import ApifyClientComponent, get_dataset_id, to_decimal, to_timedelta
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from collections.abc import Callable
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  from apify_client import ApifyClient, ApifyClientAsync
16
16
 
17
17
 
18
- class _ApifyDatasetComponent(_ApifyClientComponent):
18
+ class ApifyDatasetComponent(ApifyClientComponent):
19
19
  """Base for the components that convert dataset items to Documents with a mapping function."""
20
20
 
21
21
  @classmethod
@@ -29,7 +29,7 @@ class _ApifyDatasetComponent(_ApifyClientComponent):
29
29
 
30
30
 
31
31
  @component
32
- class ApifyDatasetLoader(_ApifyDatasetComponent):
32
+ class ApifyDatasetLoader(ApifyDatasetComponent):
33
33
  """Load datasets produced by Apify Actors.
34
34
 
35
35
  Actors are serverless functions that can be used to scrape, crawl, and extract data from the web.
@@ -49,6 +49,9 @@ class ApifyDatasetLoader(_ApifyDatasetComponent):
49
49
  dataset = loader.run()
50
50
  """
51
51
 
52
+ # Serialized pipelines store the class by this public path, so it does not depend on the module layout.
53
+ __module__ = 'apify_haystack'
54
+
52
55
  def __init__(
53
56
  self,
54
57
  dataset_id: str,
@@ -95,7 +98,7 @@ class ApifyDatasetLoader(_ApifyDatasetComponent):
95
98
 
96
99
 
97
100
  @component
98
- class ApifyDatasetFromActorCall(_ApifyDatasetComponent):
101
+ class ApifyDatasetFromActorCall(ApifyDatasetComponent):
99
102
  """Get an Apify dataset by calling an Apify Actor.
100
103
 
101
104
  Perform a call of an Apify Actor and load the produced dataset.
@@ -105,6 +108,9 @@ class ApifyDatasetFromActorCall(_ApifyDatasetComponent):
105
108
  `apify_token`.
106
109
  """
107
110
 
111
+ # Serialized pipelines store the class by this public path, so it does not depend on the module layout.
112
+ __module__ = 'apify_haystack'
113
+
108
114
  def __init__(
109
115
  self,
110
116
  actor_id: str,
@@ -174,10 +180,10 @@ class ApifyDatasetFromActorCall(_ApifyDatasetComponent):
174
180
  run_input=run_input,
175
181
  build=self.build,
176
182
  memory_mbytes=self.memory_mbytes,
177
- run_timeout=_to_timedelta(self.timeout_secs),
178
- max_total_charge_usd=_to_decimal(self.max_total_charge_usd),
183
+ run_timeout=to_timedelta(self.timeout_secs),
184
+ max_total_charge_usd=to_decimal(self.max_total_charge_usd),
179
185
  )
180
- dataset_id = _get_dataset_id(run, f'Actor {self.actor_id}')
186
+ dataset_id = get_dataset_id(run, f'Actor {self.actor_id}')
181
187
  return {'documents': _load_documents(client, dataset_id, self.dataset_mapping_function)}
182
188
 
183
189
  @component.output_types(documents=list[Document])
@@ -197,10 +203,10 @@ class ApifyDatasetFromActorCall(_ApifyDatasetComponent):
197
203
  run_input=run_input,
198
204
  build=self.build,
199
205
  memory_mbytes=self.memory_mbytes,
200
- run_timeout=_to_timedelta(self.timeout_secs),
201
- max_total_charge_usd=_to_decimal(self.max_total_charge_usd),
206
+ run_timeout=to_timedelta(self.timeout_secs),
207
+ max_total_charge_usd=to_decimal(self.max_total_charge_usd),
202
208
  )
203
- dataset_id = _get_dataset_id(run, f'Actor {self.actor_id}')
209
+ dataset_id = get_dataset_id(run, f'Actor {self.actor_id}')
204
210
  documents = await _load_documents_async(client, dataset_id, self.dataset_mapping_function)
205
211
  return {'documents': documents}
206
212
 
@@ -220,7 +226,7 @@ class ApifyDatasetFromActorCall(_ApifyDatasetComponent):
220
226
 
221
227
 
222
228
  @component
223
- class ApifyDatasetFromTaskCall(_ApifyDatasetComponent):
229
+ class ApifyDatasetFromTaskCall(ApifyDatasetComponent):
224
230
  """Get an Apify dataset by calling an Apify task.
225
231
 
226
232
  Perform a call of an Apify task and load the produced dataset.
@@ -230,6 +236,9 @@ class ApifyDatasetFromTaskCall(_ApifyDatasetComponent):
230
236
  `apify_token`.
231
237
  """
232
238
 
239
+ # Serialized pipelines store the class by this public path, so it does not depend on the module layout.
240
+ __module__ = 'apify_haystack'
241
+
233
242
  def __init__(
234
243
  self,
235
244
  task_id: str,
@@ -289,10 +298,10 @@ class ApifyDatasetFromTaskCall(_ApifyDatasetComponent):
289
298
  task_input=task_input,
290
299
  build=self.build,
291
300
  memory_mbytes=self.memory_mbytes,
292
- run_timeout=_to_timedelta(self.timeout_secs),
293
- max_total_charge_usd=_to_decimal(self.max_total_charge_usd),
301
+ run_timeout=to_timedelta(self.timeout_secs),
302
+ max_total_charge_usd=to_decimal(self.max_total_charge_usd),
294
303
  )
295
- dataset_id = _get_dataset_id(run, f'task {self.task_id}')
304
+ dataset_id = get_dataset_id(run, f'task {self.task_id}')
296
305
  return {'documents': _load_documents(client, dataset_id, self.dataset_mapping_function)}
297
306
 
298
307
  @component.output_types(documents=list[Document])
@@ -312,10 +321,10 @@ class ApifyDatasetFromTaskCall(_ApifyDatasetComponent):
312
321
  task_input=task_input,
313
322
  build=self.build,
314
323
  memory_mbytes=self.memory_mbytes,
315
- run_timeout=_to_timedelta(self.timeout_secs),
316
- max_total_charge_usd=_to_decimal(self.max_total_charge_usd),
324
+ run_timeout=to_timedelta(self.timeout_secs),
325
+ max_total_charge_usd=to_decimal(self.max_total_charge_usd),
317
326
  )
318
- dataset_id = _get_dataset_id(run, f'task {self.task_id}')
327
+ dataset_id = get_dataset_id(run, f'task {self.task_id}')
319
328
  documents = await _load_documents_async(client, dataset_id, self.dataset_mapping_function)
320
329
  return {'documents': documents}
321
330
 
@@ -6,8 +6,8 @@ from haystack import component, default_from_dict, default_to_dict
6
6
  from haystack.dataclasses import Document
7
7
  from haystack.utils import Secret
8
8
 
9
- from apify_haystack._client import _ApifyClientComponent, _get_dataset_id, _to_decimal, _to_timedelta
10
- from apify_haystack.apify_dataset import default_dataset_mapping_function
9
+ from apify_haystack._client import ApifyClientComponent, get_dataset_id, to_decimal, to_timedelta
10
+ from apify_haystack._dataset import default_dataset_mapping_function
11
11
 
12
12
  RAG_WEB_BROWSER_ACTOR_ID = 'apify/rag-web-browser'
13
13
 
@@ -16,7 +16,7 @@ _MAX_TOP_K = 100
16
16
 
17
17
 
18
18
  @component
19
- class ApifyWebSearch(_ApifyClientComponent):
19
+ class ApifyWebSearch(ApifyClientComponent):
20
20
  """Search the web with the Apify RAG Web Browser Actor and return the result pages as Documents.
21
21
 
22
22
  The Actor searches Google, scrapes the top results, and returns their content as Markdown. Each result becomes
@@ -33,6 +33,9 @@ class ApifyWebSearch(_ApifyClientComponent):
33
33
  documents, links = result['documents'], result['links']
34
34
  """
35
35
 
36
+ # Serialized pipelines store the class by this public path, so it does not depend on the module layout.
37
+ __module__ = 'apify_haystack'
38
+
36
39
  def __init__(
37
40
  self,
38
41
  top_k: int = 5,
@@ -75,10 +78,10 @@ class ApifyWebSearch(_ApifyClientComponent):
75
78
  client = self._get_client()
76
79
  run = client.actor(RAG_WEB_BROWSER_ACTOR_ID).call(
77
80
  run_input=self._run_input(query),
78
- run_timeout=_to_timedelta(self.timeout_secs),
79
- max_total_charge_usd=_to_decimal(self.max_total_charge_usd),
81
+ run_timeout=to_timedelta(self.timeout_secs),
82
+ max_total_charge_usd=to_decimal(self.max_total_charge_usd),
80
83
  )
81
- dataset_id = _get_dataset_id(run, f'Actor {RAG_WEB_BROWSER_ACTOR_ID}')
84
+ dataset_id = get_dataset_id(run, f'Actor {RAG_WEB_BROWSER_ACTOR_ID}')
82
85
  items = client.dataset(dataset_id).list_items(clean=True).items
83
86
  return _to_output(items)
84
87
 
@@ -92,10 +95,10 @@ class ApifyWebSearch(_ApifyClientComponent):
92
95
  client = self._get_async_client()
93
96
  run = await client.actor(RAG_WEB_BROWSER_ACTOR_ID).call(
94
97
  run_input=self._run_input(query),
95
- run_timeout=_to_timedelta(self.timeout_secs),
96
- max_total_charge_usd=_to_decimal(self.max_total_charge_usd),
98
+ run_timeout=to_timedelta(self.timeout_secs),
99
+ max_total_charge_usd=to_decimal(self.max_total_charge_usd),
97
100
  )
98
- dataset_id = _get_dataset_id(run, f'Actor {RAG_WEB_BROWSER_ACTOR_ID}')
101
+ dataset_id = get_dataset_id(run, f'Actor {RAG_WEB_BROWSER_ACTOR_ID}')
99
102
  items = (await client.dataset(dataset_id).list_items(clean=True)).items
100
103
  return _to_output(items)
101
104