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.
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: specstream-haystack
3
- Version: 0.1.0
4
- Summary: Give a Haystack pipeline or agent any list from the live web: ask in plain words, get every matching row as a Document with the page it came from, within a price you set in code.
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/Specstreamai/specstream-tools/tree/main/packages/specstream-haystack
8
- Project-URL: Issues, https://github.com/Specstreamai/specstream-tools/issues
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: [Specstreamai/specstream-tools](https://github.com/Specstreamai/specstream-tools/tree/main/packages/specstream-haystack).
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: [Specstreamai/specstream-tools](https://github.com/Specstreamai/specstream-tools/tree/main/packages/specstream-haystack).
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.0"
4
- description = "Give a Haystack pipeline or agent any list from the live web: ask in plain words, get every matching row as a Document with the page it came from, within a price you set in code."
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/Specstreamai/specstream-tools/tree/main/packages/specstream-haystack"
34
- Issues = "https://github.com/Specstreamai/specstream-tools/issues"
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 = "langchain-specstream/0.1.2"
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 flatten_row(find_id: str, record: dict[str, Any]) -> dict[str, Any]:
39
- """One row as flat fields: each column's label holds what the list shows, plus where the row was read."""
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
- provenance = cell.get("provenance") or {}
56
- if source is None and str(provenance.get("source_url") or "").startswith("http"):
57
- source = provenance
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
- rows.extend(flatten_row(find_id, record) for record in page.get("rows") or [])
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
- return self.pages.pop(0) if len(self.pages) > 1 else self.pages[0]
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"