specstream-haystack 0.1.0__tar.gz → 0.1.2__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.
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/PKG-INFO +6 -6
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/README.md +1 -1
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/pyproject.toml +5 -5
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/src/haystack_integrations/components/fetchers/specstream/_api.py +21 -11
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/tests/test_specstream_haystack.py +16 -2
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/.gitignore +0 -0
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/LICENSE +0 -0
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/src/haystack_integrations/components/fetchers/specstream/__init__.py +0 -0
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/src/haystack_integrations/components/fetchers/specstream/list_fetcher.py +0 -0
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/src/haystack_integrations/components/fetchers/specstream/py.typed +0 -0
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/src/haystack_integrations/tools/specstream/__init__.py +0 -0
- {specstream_haystack-0.1.0 → specstream_haystack-0.1.2}/src/haystack_integrations/tools/specstream/list_tool.py +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: specstream-haystack
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: Give a Haystack pipeline or agent
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Give a Haystack pipeline or agent lead lists, company lists and grants from the live web. Every row comes with its source, priced per match in code.
|
|
5
5
|
Project-URL: Homepage, https://specstream.ai
|
|
6
6
|
Project-URL: Documentation, https://specstream.ai/docs
|
|
7
|
-
Project-URL: Repository, https://github.com/
|
|
8
|
-
Project-URL: Issues, https://github.com/
|
|
7
|
+
Project-URL: Repository, https://github.com/specstream/specstream-tools/tree/main/packages/specstream-haystack
|
|
8
|
+
Project-URL: Issues, https://github.com/specstream/specstream-tools/issues
|
|
9
9
|
Project-URL: Pricing, https://specstream.ai/pricing
|
|
10
10
|
Author-email: SpecStream <support@specstream.ai>
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Keywords: agents,haystack,haystack-ai,lead-lists,live-web,research,specstream,tools,web-data
|
|
13
|
+
Keywords: agents,company-list,grants,haystack,haystack-ai,lead-generation,lead-list,lead-lists,live-web,research,specstream,tools,web-data
|
|
14
14
|
Classifier: Development Status :: 4 - Beta
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
@@ -80,4 +80,4 @@ The agent writes the request and picks how many rows. You decide what one list m
|
|
|
80
80
|
|
|
81
81
|
The same request with the same limits, in the same hour, reaches the run it already started instead of starting another. A retried step never pays for the list twice.
|
|
82
82
|
|
|
83
|
-
MIT licensed. Source: [
|
|
83
|
+
MIT licensed. Source: [specstream/specstream-tools](https://github.com/specstream/specstream-tools/tree/main/packages/specstream-haystack).
|
|
@@ -47,4 +47,4 @@ The agent writes the request and picks how many rows. You decide what one list m
|
|
|
47
47
|
|
|
48
48
|
The same request with the same limits, in the same hour, reaches the run it already started instead of starting another. A retried step never pays for the list twice.
|
|
49
49
|
|
|
50
|
-
MIT licensed. Source: [
|
|
50
|
+
MIT licensed. Source: [specstream/specstream-tools](https://github.com/specstream/specstream-tools/tree/main/packages/specstream-haystack).
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "specstream-haystack"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "Give a Haystack pipeline or agent
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "Give a Haystack pipeline or agent lead lists, company lists and grants from the live web. Every row comes with its source, priced per match in code."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
9
|
authors = [{ name = "SpecStream", email = "support@specstream.ai" }]
|
|
10
|
-
keywords = ["haystack", "haystack-ai", "specstream", "agents", "tools", "web-data", "live-web", "lead-lists", "research"]
|
|
10
|
+
keywords = ["haystack", "haystack-ai", "specstream", "agents", "tools", "web-data", "live-web", "lead-lists", "research", "lead-generation", "lead-list", "company-list", "grants"]
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Development Status :: 4 - Beta",
|
|
13
13
|
"Intended Audience :: Developers",
|
|
@@ -30,8 +30,8 @@ test = ["pytest>=8", "ruff"]
|
|
|
30
30
|
[project.urls]
|
|
31
31
|
Homepage = "https://specstream.ai"
|
|
32
32
|
Documentation = "https://specstream.ai/docs"
|
|
33
|
-
Repository = "https://github.com/
|
|
34
|
-
Issues = "https://github.com/
|
|
33
|
+
Repository = "https://github.com/specstream/specstream-tools/tree/main/packages/specstream-haystack"
|
|
34
|
+
Issues = "https://github.com/specstream/specstream-tools/issues"
|
|
35
35
|
Pricing = "https://specstream.ai/pricing"
|
|
36
36
|
|
|
37
37
|
[build-system]
|
|
@@ -12,7 +12,7 @@ from dataclasses import dataclass
|
|
|
12
12
|
from typing import Any, Callable
|
|
13
13
|
|
|
14
14
|
DEFAULT_BASE_URL = "https://api.specstream.ai"
|
|
15
|
-
USER_AGENT = "
|
|
15
|
+
USER_AGENT = "specstream-haystack/0.1.2"
|
|
16
16
|
# A list runs for up to about five minutes, so the tools wait a little longer than that by default.
|
|
17
17
|
DEFAULT_WAIT_SECONDS = 420.0
|
|
18
18
|
FINISHED = {"completed", "partial"}
|
|
@@ -35,15 +35,23 @@ def list_link(find_id: str) -> str:
|
|
|
35
35
|
return f"https://specstream.ai/workspace/collections/{urllib.parse.quote(find_id, safe='')}"
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
def
|
|
39
|
-
"""
|
|
38
|
+
def _sources(flat: dict[str, Any]) -> dict[str, Any]:
|
|
39
|
+
"""Each value's page in the API's flat row: <field>_source beside the <field> it belongs to."""
|
|
40
|
+
return {key: value for key, value in flat.items() if key.endswith("_source") and key[: -len("_source")] in flat}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def flatten_row(find_id: str, record: dict[str, Any], flat: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
44
|
+
"""One row as flat fields: each column's label holds what the list shows. The page of each value
|
|
45
|
+
(<field>_source) and of the fact that put the row in the list (source_url, checked_at) come from
|
|
46
|
+
the API's flat row, so every tool shows the same pages."""
|
|
47
|
+
flat = flat or {}
|
|
48
|
+
sources = _sources(flat)
|
|
40
49
|
row: dict[str, Any] = {"list_id": find_id, "position": record.get("ordinal")}
|
|
41
|
-
source: dict[str, Any] | None = None
|
|
42
50
|
for cell in record.get("details") or record.get("cells") or []:
|
|
43
51
|
label = str(cell.get("label") or cell.get("column_id") or "").strip()
|
|
44
52
|
if not label:
|
|
45
53
|
continue
|
|
46
|
-
if label in RESERVED:
|
|
54
|
+
if label in RESERVED or label in sources:
|
|
47
55
|
label = f"{label} (column)"
|
|
48
56
|
if cell.get("absence"):
|
|
49
57
|
value: Any = ""
|
|
@@ -52,11 +60,9 @@ def flatten_row(find_id: str, record: dict[str, Any]) -> dict[str, Any]:
|
|
|
52
60
|
else:
|
|
53
61
|
value = cell.get("value") if cell.get("value") is not None else ""
|
|
54
62
|
row[label] = value
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
row["source_url"] = source.get("source_url", "") if source else ""
|
|
59
|
-
row["checked_at"] = source.get("observed_at", "") if source else ""
|
|
63
|
+
row.update(sources)
|
|
64
|
+
row["source_url"] = flat.get("source_url") or ""
|
|
65
|
+
row["checked_at"] = flat.get("checked_at") or ""
|
|
60
66
|
row["list_link"] = list_link(find_id)
|
|
61
67
|
return row
|
|
62
68
|
|
|
@@ -115,7 +121,11 @@ class SpecStreamAPI:
|
|
|
115
121
|
query = {"limit": "100", **({"cursor": cursor} if cursor else {})}
|
|
116
122
|
page = self.request("GET", f"/v1/finds/{urllib.parse.quote(find_id, safe='')}/rows?{urllib.parse.urlencode(query)}") or {}
|
|
117
123
|
status = page.get("status") or status
|
|
118
|
-
|
|
124
|
+
flat = {str(item.get("id")): item for item in page.get("flat_rows") or []}
|
|
125
|
+
rows.extend(
|
|
126
|
+
flatten_row(find_id, record, flat.get(f"{find_id}:{record.get('entity_id')}"))
|
|
127
|
+
for record in page.get("rows") or []
|
|
128
|
+
)
|
|
119
129
|
cursor = page.get("next_cursor")
|
|
120
130
|
if not cursor:
|
|
121
131
|
break
|
|
@@ -13,6 +13,7 @@ FIND_ID = "fnd_test123"
|
|
|
13
13
|
|
|
14
14
|
def _row(ordinal, name, url):
|
|
15
15
|
return {
|
|
16
|
+
"entity_id": f"e{ordinal}",
|
|
16
17
|
"ordinal": ordinal,
|
|
17
18
|
"details": [
|
|
18
19
|
{"label": "Organization", "display": name, "provenance": {"source_url": url, "observed_at": "2026-10-14T19:56:02Z"}},
|
|
@@ -22,6 +23,18 @@ def _row(ordinal, name, url):
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
def _flat(row):
|
|
27
|
+
"""The API's flat row: the page of each value, and of the fact that put the row in the list."""
|
|
28
|
+
name = row["details"][0]
|
|
29
|
+
return {
|
|
30
|
+
"id": f"{FIND_ID}:{row['entity_id']}",
|
|
31
|
+
"name": name["display"],
|
|
32
|
+
"name_source": name["provenance"]["source_url"],
|
|
33
|
+
"source_url": f"{name['provenance']['source_url']}/filing",
|
|
34
|
+
"checked_at": "2026-10-14T19:56:02Z",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
25
38
|
class FakeApi:
|
|
26
39
|
"""Stands in for the SpecStream REST API and records the calls."""
|
|
27
40
|
|
|
@@ -43,7 +56,8 @@ class FakeApi:
|
|
|
43
56
|
if path == "/v1/capacity/executions":
|
|
44
57
|
return {"find_id": FIND_ID}
|
|
45
58
|
if "/rows" in path:
|
|
46
|
-
|
|
59
|
+
page = self.pages.pop(0) if len(self.pages) > 1 else self.pages[0]
|
|
60
|
+
return {**page, "flat_rows": [_flat(row) for row in page["rows"]]}
|
|
47
61
|
raise AssertionError(path)
|
|
48
62
|
|
|
49
63
|
def paths(self):
|
|
@@ -71,7 +85,7 @@ def test_rows_become_documents_with_source_in_meta(fake):
|
|
|
71
85
|
fake()
|
|
72
86
|
result = _fetcher().run(request="Breweries in Portland")
|
|
73
87
|
docs = result["documents"]
|
|
74
|
-
assert [d.meta["source_url"] for d in docs] == ["https://src/a", "https://src/b"]
|
|
88
|
+
assert [d.meta["source_url"] for d in docs] == ["https://src/a/filing", "https://src/b/filing"]
|
|
75
89
|
first = docs[0]
|
|
76
90
|
assert json.loads(first.content)["Organization"] == "Alpha"
|
|
77
91
|
assert first.meta["checked_at"] == "2026-10-14T19:56:02Z"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|