alphai-haystack 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,12 @@
1
+ """Haystack components for the AlphaAI financial-news API (alphai.io)."""
2
+
3
+ from ._version import VERSION
4
+ from .fetchers import AlphaAIInsiderNewsFetcher, AlphaAINewsFetcher
5
+
6
+ __version__ = VERSION
7
+
8
+ __all__ = [
9
+ "AlphaAIInsiderNewsFetcher",
10
+ "AlphaAINewsFetcher",
11
+ "__version__",
12
+ ]
@@ -0,0 +1 @@
1
+ VERSION = "0.1.0"
@@ -0,0 +1,252 @@
1
+ """Haystack components that turn AlphaAI's scored news feed into Documents.
2
+
3
+ Every article on the feed already carries an AI enrichment layer (per-ticker
4
+ impact analysis, a category, a 1-10 relevance score), so the components here
5
+ do no scoring of their own — they fetch, filter, and map articles into
6
+ :class:`haystack.Document` objects with the enrichment exposed as metadata.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import date, datetime
12
+ from decimal import Decimal
13
+ from typing import Any
14
+
15
+ from alphai import Client
16
+ from alphai.models import RichNewsArticle
17
+ from haystack import Document, component, default_from_dict, default_to_dict
18
+ from haystack.utils import Secret, deserialize_secrets_inplace
19
+
20
+ DEFAULT_TOP_K = 10
21
+
22
+
23
+ def _plain(value: object) -> object:
24
+ """Convert SDK field values into JSON-friendly meta values."""
25
+ if isinstance(value, Decimal):
26
+ return str(value)
27
+ if isinstance(value, (datetime, date)):
28
+ return value.isoformat()
29
+ return value
30
+
31
+
32
+ def _category_str(category: object) -> str:
33
+ return str(getattr(category, "value", category))
34
+
35
+
36
+ def _to_document(article: RichNewsArticle) -> Document:
37
+ original = article.original
38
+ enrichment = article.enrichment
39
+ parts = [original.title.strip(), original.summary.strip()]
40
+ content = "\n\n".join(part for part in parts if part)
41
+ meta: dict[str, Any] = {
42
+ "uid": original.uid,
43
+ "url": original.url,
44
+ "title": original.title,
45
+ "source": original.source,
46
+ "source_domain": original.source_domain,
47
+ "published_at": _plain(original.time_published),
48
+ "tickers": list(enrichment.tickers),
49
+ "category": _category_str(enrichment.category),
50
+ "relevance_score": enrichment.relevance_score,
51
+ }
52
+ if article.sources_count is not None:
53
+ meta["sources_count"] = article.sources_count
54
+ if article.insider is not None:
55
+ event = article.insider
56
+ meta["insider"] = {
57
+ "side": event.side,
58
+ "transaction_code": event.transaction_code,
59
+ "shares": _plain(event.shares),
60
+ "avg_price_usd": _plain(event.avg_price_usd),
61
+ "total_value_usd": _plain(event.total_value_usd),
62
+ "is_10b5_1": event.is_10b5_1,
63
+ "insider_name": event.insider_name,
64
+ "insider_title": event.insider_title,
65
+ "is_officer": event.is_officer,
66
+ "is_director": event.is_director,
67
+ "is_ten_percent_owner": event.is_ten_percent_owner,
68
+ "transaction_date": _plain(event.transaction_date),
69
+ }
70
+ return Document(content=content, meta=meta)
71
+
72
+
73
+ @component
74
+ class AlphaAINewsFetcher:
75
+ """Fetches AI-scored financial news from AlphaAI as Haystack Documents.
76
+
77
+ Each Document's ``content`` is the article title plus summary; ``meta``
78
+ carries the enrichment (tickers, category, 1-10 ``relevance_score``,
79
+ source, url, publish time). Filters set in ``__init__`` are defaults and
80
+ can be overridden per ``run()`` call.
81
+
82
+ Requires an AlphaAI API key (free tier available, no card) — see
83
+ https://alphai.io/developers. The key is read from the ``ALPHAI_API_KEY``
84
+ environment variable by default.
85
+
86
+ ```python
87
+ from alphai_haystack import AlphaAINewsFetcher
88
+
89
+ fetcher = AlphaAINewsFetcher(symbol="NVDA", min_relevance=7)
90
+ documents = fetcher.run()["documents"]
91
+ ```
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ api_key: Secret = Secret.from_env_var("ALPHAI_API_KEY"),
97
+ symbol: str | None = None,
98
+ category: str | None = None,
99
+ min_relevance: int | None = None,
100
+ collapse_stories: bool = False,
101
+ top_k: int = DEFAULT_TOP_K,
102
+ ) -> None:
103
+ """
104
+ :param api_key: AlphaAI API key. Defaults to the ``ALPHAI_API_KEY`` env var.
105
+ :param symbol: Only articles tagged with this ticker (e.g. ``"NVDA"``).
106
+ :param category: Only articles in this category (e.g. ``"earnings"``,
107
+ ``"mergers_acquisitions"``, ``"insider"``).
108
+ :param min_relevance: Only articles scored at or above this 1-10 floor.
109
+ :param collapse_stories: Collapse same-story coverage into one item.
110
+ :param top_k: Maximum number of Documents to return per run.
111
+ """
112
+ if top_k < 1:
113
+ raise ValueError("top_k must be at least 1")
114
+ self.api_key = api_key
115
+ self.symbol = symbol
116
+ self.category = category
117
+ self.min_relevance = min_relevance
118
+ self.collapse_stories = collapse_stories
119
+ self.top_k = top_k
120
+ self._client: Client | None = None
121
+
122
+ def warm_up(self) -> None:
123
+ """Create the underlying API client once."""
124
+ if self._client is None:
125
+ self._client = Client(api_key=self.api_key.resolve_value())
126
+
127
+ def to_dict(self) -> dict[str, Any]:
128
+ return default_to_dict(
129
+ self,
130
+ api_key=self.api_key.to_dict(),
131
+ symbol=self.symbol,
132
+ category=self.category,
133
+ min_relevance=self.min_relevance,
134
+ collapse_stories=self.collapse_stories,
135
+ top_k=self.top_k,
136
+ )
137
+
138
+ @classmethod
139
+ def from_dict(cls, data: dict[str, Any]) -> AlphaAINewsFetcher:
140
+ deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
141
+ result: AlphaAINewsFetcher = default_from_dict(cls, data)
142
+ return result
143
+
144
+ @component.output_types(documents=list[Document])
145
+ def run(
146
+ self,
147
+ symbol: str | None = None,
148
+ category: str | None = None,
149
+ min_relevance: int | None = None,
150
+ top_k: int | None = None,
151
+ ) -> dict[str, list[Document]]:
152
+ """Fetch the newest matching articles.
153
+
154
+ :param symbol: Overrides the ticker filter set in ``__init__``.
155
+ :param category: Overrides the category filter set in ``__init__``.
156
+ :param min_relevance: Overrides the relevance floor set in ``__init__``.
157
+ :param top_k: Overrides the maximum number of Documents.
158
+ :returns: ``{"documents": [...]}`` — newest first.
159
+ """
160
+ self.warm_up()
161
+ assert self._client is not None
162
+ articles = self._client.news.iter(
163
+ symbol=symbol if symbol is not None else self.symbol,
164
+ category=category if category is not None else self.category,
165
+ min_relevance=min_relevance if min_relevance is not None else self.min_relevance,
166
+ collapse_stories=self.collapse_stories,
167
+ max_items=top_k if top_k is not None else self.top_k,
168
+ )
169
+ return {"documents": [_to_document(article) for article in articles]}
170
+
171
+
172
+ @component
173
+ class AlphaAIInsiderNewsFetcher:
174
+ """Fetches SEC Form 4 insider-trading events from AlphaAI as Documents.
175
+
176
+ Every item is one insider event (a filing's grouped buy/sell transactions)
177
+ with a structured ``meta["insider"]`` block: side, shares, average price,
178
+ total value, who traded, and whether it was a pre-planned 10b5-1 sale.
179
+ Relevance scores on this feed are deterministic from the event's summed
180
+ value, so ``min_relevance`` works as an "only large trades" dial.
181
+
182
+ ```python
183
+ from alphai_haystack import AlphaAIInsiderNewsFetcher
184
+
185
+ fetcher = AlphaAIInsiderNewsFetcher(min_relevance=7)
186
+ documents = fetcher.run()["documents"]
187
+ ```
188
+ """
189
+
190
+ def __init__(
191
+ self,
192
+ api_key: Secret = Secret.from_env_var("ALPHAI_API_KEY"),
193
+ symbol: str | None = None,
194
+ min_relevance: int | None = None,
195
+ top_k: int = DEFAULT_TOP_K,
196
+ ) -> None:
197
+ """
198
+ :param api_key: AlphaAI API key. Defaults to the ``ALPHAI_API_KEY`` env var.
199
+ :param symbol: Only events for this ticker (share-class siblings included).
200
+ :param min_relevance: 1-10 floor; higher means larger trades only.
201
+ :param top_k: Maximum number of Documents to return per run.
202
+ """
203
+ if top_k < 1:
204
+ raise ValueError("top_k must be at least 1")
205
+ self.api_key = api_key
206
+ self.symbol = symbol
207
+ self.min_relevance = min_relevance
208
+ self.top_k = top_k
209
+ self._client: Client | None = None
210
+
211
+ def warm_up(self) -> None:
212
+ """Create the underlying API client once."""
213
+ if self._client is None:
214
+ self._client = Client(api_key=self.api_key.resolve_value())
215
+
216
+ def to_dict(self) -> dict[str, Any]:
217
+ return default_to_dict(
218
+ self,
219
+ api_key=self.api_key.to_dict(),
220
+ symbol=self.symbol,
221
+ min_relevance=self.min_relevance,
222
+ top_k=self.top_k,
223
+ )
224
+
225
+ @classmethod
226
+ def from_dict(cls, data: dict[str, Any]) -> AlphaAIInsiderNewsFetcher:
227
+ deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
228
+ result: AlphaAIInsiderNewsFetcher = default_from_dict(cls, data)
229
+ return result
230
+
231
+ @component.output_types(documents=list[Document])
232
+ def run(
233
+ self,
234
+ symbol: str | None = None,
235
+ min_relevance: int | None = None,
236
+ top_k: int | None = None,
237
+ ) -> dict[str, list[Document]]:
238
+ """Fetch the newest matching insider events.
239
+
240
+ :param symbol: Overrides the ticker filter set in ``__init__``.
241
+ :param min_relevance: Overrides the relevance floor set in ``__init__``.
242
+ :param top_k: Overrides the maximum number of Documents.
243
+ :returns: ``{"documents": [...]}`` — newest first.
244
+ """
245
+ self.warm_up()
246
+ assert self._client is not None
247
+ articles = self._client.news.insider_iter(
248
+ symbol=symbol if symbol is not None else self.symbol,
249
+ min_relevance=min_relevance if min_relevance is not None else self.min_relevance,
250
+ max_items=top_k if top_k is not None else self.top_k,
251
+ )
252
+ return {"documents": [_to_document(article) for article in articles]}
File without changes
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: alphai-haystack
3
+ Version: 0.1.0
4
+ Summary: Haystack components for AlphaAI: AI-scored financial news and SEC Form 4 insider events as Documents.
5
+ Project-URL: Homepage, https://alphai.io
6
+ Project-URL: Documentation, https://alphai.io/developers
7
+ Project-URL: Repository, https://github.com/makeev/alphai-haystack
8
+ Project-URL: API Reference, https://api.alphai.io/api/schema/
9
+ Author-email: AlphaAI <support@alphai.io>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,alphai,financial-news,haystack,insider,llm,rag,sec,stocks
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Office/Business :: Financial :: Investment
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: alphai-sdk<1,>=0.4.1
27
+ Requires-Dist: haystack-ai<3,>=2.6
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy>=1.10; extra == 'dev'
30
+ Requires-Dist: pytest>=8; extra == 'dev'
31
+ Requires-Dist: ruff<0.17,>=0.6; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # alphai-haystack
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/alphai-haystack)](https://pypi.org/project/alphai-haystack/)
37
+ [![CI](https://github.com/makeev/alphai-haystack/actions/workflows/ci.yml/badge.svg)](https://github.com/makeev/alphai-haystack/actions/workflows/ci.yml)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
39
+
40
+ [Haystack](https://haystack.deepset.ai) components for [AlphaAI](https://alphai.io) — AI-scored
41
+ financial news and SEC Form 4 insider events, delivered as Haystack `Document` objects.
42
+
43
+ Every article on the AlphaAI feed is enriched at ingest: per-ticker impact analysis, a category,
44
+ and a 1-10 relevance score. SEC Form 4 filings become structured insider events about 6 minutes
45
+ after they hit EDGAR. These components fetch that feed so your pipelines and agents can reason
46
+ over pre-scored market news instead of raw headlines.
47
+
48
+ ## Components
49
+
50
+ - **`AlphaAINewsFetcher`** — the main news feed. Filter by ticker, category, and a relevance
51
+ floor; optionally collapse same-story coverage into one item.
52
+ - **`AlphaAIInsiderNewsFetcher`** — SEC Form 4 insider events with a structured
53
+ `meta["insider"]` block: side, shares, average price, total value, who traded, and whether it
54
+ was a pre-planned 10b5-1 sale.
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install alphai-haystack
60
+ ```
61
+
62
+ ## API key
63
+
64
+ Get a free key at [alphai.io/developers](https://alphai.io/developers) (free tier: 20 requests
65
+ per minute, 100 per day, no card). The components read it from the `ALPHAI_API_KEY` environment
66
+ variable by default:
67
+
68
+ ```bash
69
+ export ALPHAI_API_KEY="ak_..."
70
+ ```
71
+
72
+ ## Usage
73
+
74
+ ### Standalone
75
+
76
+ ```python
77
+ from alphai_haystack import AlphaAINewsFetcher
78
+
79
+ fetcher = AlphaAINewsFetcher(symbol="NVDA", min_relevance=7)
80
+ documents = fetcher.run()["documents"]
81
+
82
+ for doc in documents:
83
+ print(doc.meta["relevance_score"], doc.meta["title"])
84
+ ```
85
+
86
+ ### Insider events
87
+
88
+ ```python
89
+ from alphai_haystack import AlphaAIInsiderNewsFetcher
90
+
91
+ fetcher = AlphaAIInsiderNewsFetcher(min_relevance=7) # higher floor = larger trades
92
+ for doc in fetcher.run()["documents"]:
93
+ insider = doc.meta["insider"]
94
+ print(insider["insider_name"], insider["side"], insider["total_value_usd"], doc.meta["tickers"])
95
+ ```
96
+
97
+ ### In a pipeline
98
+
99
+ A minimal market-brief pipeline: fetch scored news for a ticker, hand it to an LLM.
100
+
101
+ ```python
102
+ from haystack import Pipeline
103
+ from haystack.components.builders import PromptBuilder
104
+ from haystack.components.generators import OpenAIGenerator
105
+
106
+ from alphai_haystack import AlphaAINewsFetcher
107
+
108
+ template = """Summarize what moved {{ symbol }} today, using only these articles:
109
+ {% for doc in documents %}
110
+ - {{ doc.content }} (relevance {{ doc.meta.relevance_score }}/10)
111
+ {% endfor %}
112
+ """
113
+
114
+ pipeline = Pipeline()
115
+ pipeline.add_component("news", AlphaAINewsFetcher(min_relevance=6, collapse_stories=True))
116
+ pipeline.add_component("prompt", PromptBuilder(template=template))
117
+ pipeline.add_component("llm", OpenAIGenerator(model="gpt-4o-mini"))
118
+ pipeline.connect("news.documents", "prompt.documents")
119
+ pipeline.connect("prompt", "llm")
120
+
121
+ result = pipeline.run({"news": {"symbol": "NVDA"}, "prompt": {"symbol": "NVDA"}})
122
+ print(result["llm"]["replies"][0])
123
+ ```
124
+
125
+ Both components implement `to_dict`/`from_dict`, so pipelines serialize to YAML and back; the
126
+ API key is stored as an environment-variable reference, never as the raw value.
127
+
128
+ ## Document shape
129
+
130
+ `content` is the article title plus summary. `meta` carries:
131
+
132
+ | Key | Type | Notes |
133
+ |---|---|---|
134
+ | `uid` | str | Stable article id (use with the AlphaAI article endpoint) |
135
+ | `url` | str | Original article URL |
136
+ | `title`, `source`, `source_domain` | str | |
137
+ | `published_at` | str | ISO 8601 |
138
+ | `tickers` | list[str] | Tickers the article affects |
139
+ | `category` | str | One of 14 categories (`earnings`, `insider`, `crypto`, ...) |
140
+ | `relevance_score` | int | 1-10, assigned at ingest |
141
+ | `sources_count` | int | Only when `collapse_stories=True` |
142
+ | `insider` | dict | Insider feed only: side, shares, avg price, total value, who |
143
+
144
+ ## Run parameters
145
+
146
+ `run()` accepts per-call overrides for the filters set in `__init__`: `symbol`, `category`
147
+ (news fetcher only), `min_relevance`, and `top_k`.
148
+
149
+ ## Development
150
+
151
+ ```bash
152
+ pip install -e ".[dev]"
153
+ ruff check . && ruff format --check .
154
+ mypy src/alphai_haystack
155
+ pytest
156
+ ```
157
+
158
+ Tests run fully offline against a fake client.
159
+
160
+ ## Links
161
+
162
+ - [AlphaAI developer docs](https://alphai.io/developers)
163
+ - [OpenAPI schema](https://api.alphai.io/api/schema/)
164
+ - [Python SDK (`alphai-sdk`)](https://github.com/makeev/alphai-sdk) — this package is a thin
165
+ Haystack layer over it
166
+ - [MCP server](https://alphai.io/mcp) — the same feed for MCP-speaking agents
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,8 @@
1
+ alphai_haystack/__init__.py,sha256=tl0EDjXAX8rBYpym-4XGNIOINFaCHz7vzc40R1cA-Yw,289
2
+ alphai_haystack/_version.py,sha256=CpXi3jGlx23RvRyU7iytOMZrnspdWw4yofS8lpP1AJU,18
3
+ alphai_haystack/fetchers.py,sha256=P2O8CZ12lpYRfQyAS6vjsjTF5zF0SN4rtySausYG2NM,9806
4
+ alphai_haystack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ alphai_haystack-0.1.0.dist-info/METADATA,sha256=FLsaFh3CVcEvUbm5xfn_EsXWiwSG4zAnScxNJGEdjIM,6120
6
+ alphai_haystack-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
7
+ alphai_haystack-0.1.0.dist-info/licenses/LICENSE,sha256=4T4cfri1G0YoudJcTv8EiNmTf2ElLYTybG22ZrMfCVs,1064
8
+ alphai_haystack-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AlphaAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.