specstream-haystack 0.1.2__tar.gz → 0.2.1__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,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: specstream-haystack
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.
3
+ Version: 0.2.1
4
+ Summary: The live web data engine for Haystack: Search API ($5 per 1,000 searches), Extract API ($0.80 per 1,000 pages) and Deep Search.
5
5
  Project-URL: Homepage, https://specstream.ai
6
6
  Project-URL: Documentation, https://specstream.ai/docs
7
7
  Project-URL: Repository, https://github.com/specstream/specstream-tools/tree/main/packages/specstream-haystack
@@ -33,14 +33,18 @@ Description-Content-Type: text/markdown
33
33
 
34
34
  # specstream-haystack
35
35
 
36
- Give your Haystack pipeline or agent any list from the live web. Search components hand a pipeline pages to read; SpecStream hands it the finished list. Ask in plain words – "dental clinics in Lisbon with online booking", "US startups that filed a $5M+ Form D this week" – and get back every matching row as a `Document`, each with the page it was read from. The list starts only within a price you set in code, and the model can't raise it.
36
+ [SpecStream](https://specstream.ai) is the live web data engine. One platform to build, search, extract, enrich, map, and monitor data across the entire web.
37
+
38
+ Ask for any dataset in plain words, and SpecStream builds it from the entire live web: every match found, extracted and enriched into structured records, then monitored for change. This package gives your Haystack pipeline or agent a fetcher component and a tool for it, with one `Document` per row. A run starts only within a price you set in code, and the model can't raise it.
39
+
40
+ Source URL and read time on every value.
37
41
 
38
42
  ```bash
39
43
  pip install -U specstream-haystack
40
44
  export SPECSTREAM_API_KEY=ss_live_... # Agents & API in your SpecStream workspace
41
45
  ```
42
46
 
43
- A free account comes with $10 of work to use within 30 days and needs no card: [specstream.ai](https://specstream.ai/signup).
47
+ A free account comes with $10 of credit and needs no card: [specstream.ai](https://specstream.ai/signup).
44
48
 
45
49
  ## In a pipeline
46
50
 
@@ -1,13 +1,17 @@
1
1
  # specstream-haystack
2
2
 
3
- Give your Haystack pipeline or agent any list from the live web. Search components hand a pipeline pages to read; SpecStream hands it the finished list. Ask in plain words – "dental clinics in Lisbon with online booking", "US startups that filed a $5M+ Form D this week" – and get back every matching row as a `Document`, each with the page it was read from. The list starts only within a price you set in code, and the model can't raise it.
3
+ [SpecStream](https://specstream.ai) is the live web data engine. One platform to build, search, extract, enrich, map, and monitor data across the entire web.
4
+
5
+ Ask for any dataset in plain words, and SpecStream builds it from the entire live web: every match found, extracted and enriched into structured records, then monitored for change. This package gives your Haystack pipeline or agent a fetcher component and a tool for it, with one `Document` per row. A run starts only within a price you set in code, and the model can't raise it.
6
+
7
+ Source URL and read time on every value.
4
8
 
5
9
  ```bash
6
10
  pip install -U specstream-haystack
7
11
  export SPECSTREAM_API_KEY=ss_live_... # Agents & API in your SpecStream workspace
8
12
  ```
9
13
 
10
- A free account comes with $10 of work to use within 30 days and needs no card: [specstream.ai](https://specstream.ai/signup).
14
+ A free account comes with $10 of credit and needs no card: [specstream.ai](https://specstream.ai/signup).
11
15
 
12
16
  ## In a pipeline
13
17
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "specstream-haystack"
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."
3
+ version = "0.2.1"
4
+ description = "The live web data engine for Haystack: Search API ($5 per 1,000 searches), Extract API ($0.80 per 1,000 pages) and Deep Search."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  license = "MIT"
@@ -0,0 +1,8 @@
1
+ from haystack_integrations.components.fetchers.specstream._api import SpecStreamError
2
+ from haystack_integrations.components.fetchers.specstream.list_fetcher import (
3
+ SpecStreamExtractor,
4
+ SpecStreamListFetcher,
5
+ SpecStreamWebSearch,
6
+ )
7
+
8
+ __all__ = ["SpecStreamError", "SpecStreamExtractor", "SpecStreamListFetcher", "SpecStreamWebSearch"]
@@ -112,6 +112,24 @@ class SpecStreamAPI:
112
112
  raise SpecStreamError("SpecStream returned a response this package couldn't read.")
113
113
  return payload["data"]
114
114
 
115
+
116
+ def web_search(self, q: str, *, passages: bool = False) -> dict[str, Any]:
117
+ """Search the live web with GET /v1/web-search ($5 per 1,000 searches, no quote needed)."""
118
+ query = (q or "").strip()
119
+ if not query or len(query) > 2000:
120
+ raise ValueError("q must be between 1 and 2,000 characters.")
121
+ params: dict[str, str] = {"q": query}
122
+ if passages:
123
+ params["passages"] = "true"
124
+ return self.request("GET", f"/v1/web-search?{urllib.parse.urlencode(params)}") or {}
125
+
126
+ def extract_pages(self, urls: list[str], *, format: str = "markdown") -> dict[str, Any]:
127
+ """Extract the main text of 1 to 20 URLs live with POST /v1/extract ($0.80 per 1,000 pages)."""
128
+ cleaned = [str(u or "").strip() for u in (urls or []) if str(u or "").strip()]
129
+ if not 1 <= len(cleaned) <= 20:
130
+ raise ValueError("Provide 1 to 20 http or https URLs.")
131
+ fmt = "text" if format == "text" else "markdown"
132
+ return self.request("POST", "/v1/extract", {"urls": cleaned, "format": fmt}) or {}
115
133
  def results(self, find_id: str) -> dict[str, Any]:
116
134
  """Every row found so far for a list, following the pages."""
117
135
  rows: list[dict[str, Any]] = []
@@ -119,3 +119,86 @@ class SpecStreamListFetcher:
119
119
  meta["request"] = request
120
120
  meta["fields"] = fields
121
121
  return Document(content=json.dumps(fields, ensure_ascii=False, default=str), meta=meta)
122
+
123
+
124
+ @component
125
+ class SpecStreamWebSearch:
126
+ """Searches the live web in seconds with the SpecStream Search API (`GET /v1/web-search`, $5 per 1,000 searches)."""
127
+
128
+ def __init__(
129
+ self,
130
+ api_key: Secret = Secret.from_env_var("SPECSTREAM_API_KEY"),
131
+ base_url: str = DEFAULT_BASE_URL,
132
+ ):
133
+ self.api_key = api_key
134
+ self.base_url = base_url
135
+
136
+ def to_dict(self) -> dict[str, Any]:
137
+ return default_to_dict(self, api_key=self.api_key.to_dict(), base_url=self.base_url)
138
+
139
+ @classmethod
140
+ def from_dict(cls, data: dict[str, Any]) -> "SpecStreamWebSearch":
141
+ deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
142
+ return default_from_dict(cls, data)
143
+
144
+ @component.output_types(documents=list[Document], answer=dict[str, Any])
145
+ def run(self, q: str, passages: bool = False) -> dict[str, Any]:
146
+ data = SpecStreamAPI(self.api_key.resolve_value() or "", self.base_url).web_search(q, passages=passages)
147
+ docs: list[Document] = []
148
+ for row in data.get("rows") or []:
149
+ if isinstance(row, dict):
150
+ docs.append(
151
+ Document(
152
+ content=str(row.get("snippet") or row.get("name") or ""),
153
+ meta={"title": row.get("name"), "source_url": row.get("page"), "fields": row.get("fields")},
154
+ )
155
+ )
156
+ for src in data.get("sources") or []:
157
+ if isinstance(src, dict):
158
+ docs.append(
159
+ Document(
160
+ content=str(src.get("snippet") or src.get("title") or ""),
161
+ meta={"title": src.get("title"), "source_url": src.get("url"), "published": src.get("published")},
162
+ )
163
+ )
164
+ return {"documents": docs, "answer": data.get("answer") or {}}
165
+
166
+
167
+ @component
168
+ class SpecStreamExtractor:
169
+ """Reads the main text of 1 to 20 URLs live with the SpecStream Extract API (`POST /v1/extract`, $0.80 per 1,000 pages)."""
170
+
171
+ def __init__(
172
+ self,
173
+ api_key: Secret = Secret.from_env_var("SPECSTREAM_API_KEY"),
174
+ base_url: str = DEFAULT_BASE_URL,
175
+ ):
176
+ self.api_key = api_key
177
+ self.base_url = base_url
178
+
179
+ def to_dict(self) -> dict[str, Any]:
180
+ return default_to_dict(self, api_key=self.api_key.to_dict(), base_url=self.base_url)
181
+
182
+ @classmethod
183
+ def from_dict(cls, data: dict[str, Any]) -> "SpecStreamExtractor":
184
+ deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
185
+ return default_from_dict(cls, data)
186
+
187
+ @component.output_types(documents=list[Document])
188
+ def run(self, urls: list[str], format: str = "markdown") -> dict[str, Any]:
189
+ data = SpecStreamAPI(self.api_key.resolve_value() or "", self.base_url).extract_pages(urls, format=format)
190
+ docs = [
191
+ Document(
192
+ content=str(item.get("text") or ""),
193
+ meta={
194
+ "source_url": item.get("url"),
195
+ "title": item.get("title"),
196
+ "published": item.get("published"),
197
+ "status": item.get("status"),
198
+ "read_from": item.get("read_from"),
199
+ },
200
+ )
201
+ for item in (data.get("results") or [])
202
+ if isinstance(item, dict)
203
+ ]
204
+ return {"documents": docs}
@@ -1,4 +0,0 @@
1
- from haystack_integrations.components.fetchers.specstream._api import SpecStreamError
2
- from haystack_integrations.components.fetchers.specstream.list_fetcher import SpecStreamListFetcher
3
-
4
- __all__ = ["SpecStreamError", "SpecStreamListFetcher"]