alphai-haystack 0.1.0__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.
@@ -0,0 +1,29 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .python-version.local
11
+
12
+ # Tooling caches
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # Env / secrets
20
+ .env
21
+ *.local
22
+
23
+ # OS / editor
24
+ .DS_Store
25
+ .idea/
26
+ .vscode/
27
+
28
+ # uv
29
+ uv.lock
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-07-28
4
+
5
+ Initial release.
6
+
7
+ - `AlphaAINewsFetcher`: the scored news feed as Haystack Documents, with `symbol` /
8
+ `category` / `min_relevance` / `collapse_stories` / `top_k` filters and per-run overrides.
9
+ - `AlphaAIInsiderNewsFetcher`: SEC Form 4 insider events with a structured
10
+ `meta["insider"]` block.
11
+ - Pipeline-safe serialization (`to_dict` / `from_dict`, API key as a `Secret`
12
+ env-var reference).
@@ -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.
@@ -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,137 @@
1
+ # alphai-haystack
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/alphai-haystack)](https://pypi.org/project/alphai-haystack/)
4
+ [![CI](https://github.com/makeev/alphai-haystack/actions/workflows/ci.yml/badge.svg)](https://github.com/makeev/alphai-haystack/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
7
+ [Haystack](https://haystack.deepset.ai) components for [AlphaAI](https://alphai.io) — AI-scored
8
+ financial news and SEC Form 4 insider events, delivered as Haystack `Document` objects.
9
+
10
+ Every article on the AlphaAI feed is enriched at ingest: per-ticker impact analysis, a category,
11
+ and a 1-10 relevance score. SEC Form 4 filings become structured insider events about 6 minutes
12
+ after they hit EDGAR. These components fetch that feed so your pipelines and agents can reason
13
+ over pre-scored market news instead of raw headlines.
14
+
15
+ ## Components
16
+
17
+ - **`AlphaAINewsFetcher`** — the main news feed. Filter by ticker, category, and a relevance
18
+ floor; optionally collapse same-story coverage into one item.
19
+ - **`AlphaAIInsiderNewsFetcher`** — SEC Form 4 insider events with a structured
20
+ `meta["insider"]` block: side, shares, average price, total value, who traded, and whether it
21
+ was a pre-planned 10b5-1 sale.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install alphai-haystack
27
+ ```
28
+
29
+ ## API key
30
+
31
+ Get a free key at [alphai.io/developers](https://alphai.io/developers) (free tier: 20 requests
32
+ per minute, 100 per day, no card). The components read it from the `ALPHAI_API_KEY` environment
33
+ variable by default:
34
+
35
+ ```bash
36
+ export ALPHAI_API_KEY="ak_..."
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ### Standalone
42
+
43
+ ```python
44
+ from alphai_haystack import AlphaAINewsFetcher
45
+
46
+ fetcher = AlphaAINewsFetcher(symbol="NVDA", min_relevance=7)
47
+ documents = fetcher.run()["documents"]
48
+
49
+ for doc in documents:
50
+ print(doc.meta["relevance_score"], doc.meta["title"])
51
+ ```
52
+
53
+ ### Insider events
54
+
55
+ ```python
56
+ from alphai_haystack import AlphaAIInsiderNewsFetcher
57
+
58
+ fetcher = AlphaAIInsiderNewsFetcher(min_relevance=7) # higher floor = larger trades
59
+ for doc in fetcher.run()["documents"]:
60
+ insider = doc.meta["insider"]
61
+ print(insider["insider_name"], insider["side"], insider["total_value_usd"], doc.meta["tickers"])
62
+ ```
63
+
64
+ ### In a pipeline
65
+
66
+ A minimal market-brief pipeline: fetch scored news for a ticker, hand it to an LLM.
67
+
68
+ ```python
69
+ from haystack import Pipeline
70
+ from haystack.components.builders import PromptBuilder
71
+ from haystack.components.generators import OpenAIGenerator
72
+
73
+ from alphai_haystack import AlphaAINewsFetcher
74
+
75
+ template = """Summarize what moved {{ symbol }} today, using only these articles:
76
+ {% for doc in documents %}
77
+ - {{ doc.content }} (relevance {{ doc.meta.relevance_score }}/10)
78
+ {% endfor %}
79
+ """
80
+
81
+ pipeline = Pipeline()
82
+ pipeline.add_component("news", AlphaAINewsFetcher(min_relevance=6, collapse_stories=True))
83
+ pipeline.add_component("prompt", PromptBuilder(template=template))
84
+ pipeline.add_component("llm", OpenAIGenerator(model="gpt-4o-mini"))
85
+ pipeline.connect("news.documents", "prompt.documents")
86
+ pipeline.connect("prompt", "llm")
87
+
88
+ result = pipeline.run({"news": {"symbol": "NVDA"}, "prompt": {"symbol": "NVDA"}})
89
+ print(result["llm"]["replies"][0])
90
+ ```
91
+
92
+ Both components implement `to_dict`/`from_dict`, so pipelines serialize to YAML and back; the
93
+ API key is stored as an environment-variable reference, never as the raw value.
94
+
95
+ ## Document shape
96
+
97
+ `content` is the article title plus summary. `meta` carries:
98
+
99
+ | Key | Type | Notes |
100
+ |---|---|---|
101
+ | `uid` | str | Stable article id (use with the AlphaAI article endpoint) |
102
+ | `url` | str | Original article URL |
103
+ | `title`, `source`, `source_domain` | str | |
104
+ | `published_at` | str | ISO 8601 |
105
+ | `tickers` | list[str] | Tickers the article affects |
106
+ | `category` | str | One of 14 categories (`earnings`, `insider`, `crypto`, ...) |
107
+ | `relevance_score` | int | 1-10, assigned at ingest |
108
+ | `sources_count` | int | Only when `collapse_stories=True` |
109
+ | `insider` | dict | Insider feed only: side, shares, avg price, total value, who |
110
+
111
+ ## Run parameters
112
+
113
+ `run()` accepts per-call overrides for the filters set in `__init__`: `symbol`, `category`
114
+ (news fetcher only), `min_relevance`, and `top_k`.
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ pip install -e ".[dev]"
120
+ ruff check . && ruff format --check .
121
+ mypy src/alphai_haystack
122
+ pytest
123
+ ```
124
+
125
+ Tests run fully offline against a fake client.
126
+
127
+ ## Links
128
+
129
+ - [AlphaAI developer docs](https://alphai.io/developers)
130
+ - [OpenAPI schema](https://api.alphai.io/api/schema/)
131
+ - [Python SDK (`alphai-sdk`)](https://github.com/makeev/alphai-sdk) — this package is a thin
132
+ Haystack layer over it
133
+ - [MCP server](https://alphai.io/mcp) — the same feed for MCP-speaking agents
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,87 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "alphai-haystack"
7
+ dynamic = ["version"]
8
+ description = "Haystack components for AlphaAI: AI-scored financial news and SEC Form 4 insider events as Documents."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "AlphaAI", email = "support@alphai.io" }]
14
+ keywords = [
15
+ "haystack",
16
+ "alphai",
17
+ "financial-news",
18
+ "stocks",
19
+ "sec",
20
+ "insider",
21
+ "rag",
22
+ "llm",
23
+ "agents",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Developers",
28
+ "Intended Audience :: Financial and Insurance Industry",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Operating System :: OS Independent",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
36
+ "Topic :: Office/Business :: Financial :: Investment",
37
+ "Typing :: Typed",
38
+ ]
39
+ dependencies = [
40
+ "haystack-ai>=2.6,<3",
41
+ "alphai-sdk>=0.4.1,<1",
42
+ ]
43
+
44
+ [project.urls]
45
+ Homepage = "https://alphai.io"
46
+ Documentation = "https://alphai.io/developers"
47
+ Repository = "https://github.com/makeev/alphai-haystack"
48
+ "API Reference" = "https://api.alphai.io/api/schema/"
49
+
50
+ [project.optional-dependencies]
51
+ dev = [
52
+ "pytest>=8",
53
+ # Capped on purpose: CI runs `ruff format --check .`, and a ruff minor can
54
+ # widen what "formatted" means (0.16 started formatting Markdown code blocks).
55
+ # Bump deliberately, reformat in the same commit.
56
+ "ruff>=0.6,<0.17",
57
+ "mypy>=1.10",
58
+ ]
59
+
60
+ [tool.hatch.version]
61
+ path = "src/alphai_haystack/_version.py"
62
+
63
+ [tool.hatch.build.targets.wheel]
64
+ packages = ["src/alphai_haystack"]
65
+
66
+ [tool.hatch.build.targets.sdist]
67
+ include = ["/src", "/tests", "/README.md", "/LICENSE", "/CHANGELOG.md"]
68
+
69
+ [tool.ruff]
70
+ line-length = 100
71
+ target-version = "py310"
72
+ src = ["src", "tests"]
73
+
74
+ [tool.ruff.lint]
75
+ select = ["E", "F", "I", "UP", "B", "SIM", "C4", "PIE", "RUF"]
76
+ ignore = ["B008"]
77
+
78
+ # python_version is 3.12 (not our 3.10 floor) because numpy 2.x stubs use
79
+ # PEP 695 `type` statements, which mypy refuses to parse under 3.10/3.11.
80
+ # 3.10 compatibility is held by ruff (target-version py310) and the CI matrix.
81
+ [tool.mypy]
82
+ python_version = "3.12"
83
+ strict = true
84
+ files = ["src/alphai_haystack"]
85
+
86
+ [tool.pytest.ini_options]
87
+ testpaths = ["tests"]
@@ -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,88 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from datetime import date, datetime, timezone
5
+ from decimal import Decimal
6
+ from typing import Any
7
+
8
+ import pytest
9
+ from alphai.models.news import (
10
+ EnrichedArticle,
11
+ InsiderEvent,
12
+ OriginalArticle,
13
+ RichNewsArticle,
14
+ )
15
+
16
+
17
+ def make_article(
18
+ uid: str = "a1b2c3d4e5f60718",
19
+ title: str = "Nvidia beats on data-center revenue",
20
+ summary: str = "Q2 revenue came in above consensus, driven by data-center demand.",
21
+ tickers: list[str] | None = None,
22
+ relevance_score: int = 8,
23
+ category: str = "earnings",
24
+ insider: InsiderEvent | None = None,
25
+ sources_count: int | None = None,
26
+ ) -> RichNewsArticle:
27
+ return RichNewsArticle(
28
+ original=OriginalArticle(
29
+ uid=uid,
30
+ title=title,
31
+ url=f"https://example.com/{uid}",
32
+ time_published=datetime(2026, 7, 28, 12, 30, tzinfo=timezone.utc),
33
+ summary=summary,
34
+ source="Example Wire",
35
+ source_domain="example.com",
36
+ ),
37
+ enrichment=EnrichedArticle(
38
+ category=category,
39
+ tickers=tickers if tickers is not None else ["NVDA"],
40
+ relevance_score=relevance_score,
41
+ ),
42
+ insider=insider,
43
+ sources_count=sources_count,
44
+ )
45
+
46
+
47
+ def make_insider_event() -> InsiderEvent:
48
+ return InsiderEvent(
49
+ side="sell",
50
+ transaction_code="S",
51
+ shares=Decimal("120000"),
52
+ avg_price_usd=Decimal("171.31"),
53
+ total_value_usd=Decimal("20557200.00"),
54
+ is_10b5_1=True,
55
+ insider_name="Jensen Huang",
56
+ insider_title="CEO",
57
+ is_officer=True,
58
+ transaction_date=date(2026, 7, 24),
59
+ )
60
+
61
+
62
+ class FakeNewsResource:
63
+ """Records call kwargs and yields canned articles."""
64
+
65
+ def __init__(self, articles: list[RichNewsArticle]) -> None:
66
+ self.articles = articles
67
+ self.iter_calls: list[dict[str, Any]] = []
68
+ self.insider_calls: list[dict[str, Any]] = []
69
+
70
+ def iter(self, **kwargs: Any) -> Iterator[RichNewsArticle]:
71
+ self.iter_calls.append(kwargs)
72
+ max_items = kwargs.get("max_items")
73
+ yield from self.articles[:max_items]
74
+
75
+ def insider_iter(self, **kwargs: Any) -> Iterator[RichNewsArticle]:
76
+ self.insider_calls.append(kwargs)
77
+ max_items = kwargs.get("max_items")
78
+ yield from self.articles[:max_items]
79
+
80
+
81
+ class FakeClient:
82
+ def __init__(self, articles: list[RichNewsArticle]) -> None:
83
+ self.news = FakeNewsResource(articles)
84
+
85
+
86
+ @pytest.fixture()
87
+ def articles() -> list[RichNewsArticle]:
88
+ return [make_article(uid=f"uid{i:013d}", title=f"Article {i}") for i in range(25)]
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+ from alphai.models.news import RichNewsArticle
5
+ from haystack import Document
6
+
7
+ from alphai_haystack import AlphaAIInsiderNewsFetcher, AlphaAINewsFetcher
8
+ from alphai_haystack.fetchers import _to_document
9
+ from conftest import FakeClient, make_article, make_insider_event
10
+
11
+
12
+ def news_fetcher_with(articles: list[RichNewsArticle], **kwargs: object) -> AlphaAINewsFetcher:
13
+ fetcher = AlphaAINewsFetcher(**kwargs) # type: ignore[arg-type]
14
+ fetcher._client = FakeClient(articles) # type: ignore[assignment]
15
+ return fetcher
16
+
17
+
18
+ def insider_fetcher_with(
19
+ articles: list[RichNewsArticle], **kwargs: object
20
+ ) -> AlphaAIInsiderNewsFetcher:
21
+ fetcher = AlphaAIInsiderNewsFetcher(**kwargs) # type: ignore[arg-type]
22
+ fetcher._client = FakeClient(articles) # type: ignore[assignment]
23
+ return fetcher
24
+
25
+
26
+ def test_document_mapping() -> None:
27
+ document = _to_document(make_article())
28
+ assert isinstance(document, Document)
29
+ assert document.content is not None
30
+ assert document.content.startswith("Nvidia beats on data-center revenue")
31
+ assert "above consensus" in document.content
32
+ assert document.meta["uid"] == "a1b2c3d4e5f60718"
33
+ assert document.meta["tickers"] == ["NVDA"]
34
+ assert document.meta["category"] == "earnings"
35
+ assert document.meta["relevance_score"] == 8
36
+ assert document.meta["published_at"] == "2026-07-28T12:30:00+00:00"
37
+ assert document.meta["source_domain"] == "example.com"
38
+ assert "insider" not in document.meta
39
+
40
+
41
+ def test_document_mapping_insider_block() -> None:
42
+ document = _to_document(make_article(insider=make_insider_event()))
43
+ insider = document.meta["insider"]
44
+ assert insider["side"] == "sell"
45
+ assert insider["shares"] == "120000"
46
+ assert insider["total_value_usd"] == "20557200.00"
47
+ assert insider["is_10b5_1"] is True
48
+ assert insider["insider_name"] == "Jensen Huang"
49
+ assert insider["transaction_date"] == "2026-07-24"
50
+
51
+
52
+ def test_news_run_returns_top_k_documents(articles: list[RichNewsArticle]) -> None:
53
+ fetcher = news_fetcher_with(articles, top_k=5)
54
+ documents = fetcher.run()["documents"]
55
+ assert len(documents) == 5
56
+ assert documents[0].meta["title"] == "Article 0"
57
+
58
+
59
+ def test_news_run_passes_init_filters(articles: list[RichNewsArticle]) -> None:
60
+ fetcher = news_fetcher_with(
61
+ articles, symbol="NVDA", category="earnings", min_relevance=7, collapse_stories=True
62
+ )
63
+ fetcher.run()
64
+ call = fetcher._client.news.iter_calls[0] # type: ignore[union-attr]
65
+ assert call == {
66
+ "symbol": "NVDA",
67
+ "category": "earnings",
68
+ "min_relevance": 7,
69
+ "collapse_stories": True,
70
+ "max_items": 10,
71
+ }
72
+
73
+
74
+ def test_news_run_overrides_beat_init(articles: list[RichNewsArticle]) -> None:
75
+ fetcher = news_fetcher_with(articles, symbol="NVDA", min_relevance=7, top_k=10)
76
+ fetcher.run(symbol="AMD", min_relevance=9, top_k=3)
77
+ call = fetcher._client.news.iter_calls[0] # type: ignore[union-attr]
78
+ assert call["symbol"] == "AMD"
79
+ assert call["min_relevance"] == 9
80
+ assert call["max_items"] == 3
81
+
82
+
83
+ def test_insider_run_passes_filters(articles: list[RichNewsArticle]) -> None:
84
+ fetcher = insider_fetcher_with(articles, symbol="NVDA", min_relevance=8, top_k=4)
85
+ documents = fetcher.run()["documents"]
86
+ assert len(documents) == 4
87
+ call = fetcher._client.news.insider_calls[0] # type: ignore[union-attr]
88
+ assert call == {"symbol": "NVDA", "min_relevance": 8, "max_items": 4}
89
+
90
+
91
+ def test_top_k_must_be_positive() -> None:
92
+ with pytest.raises(ValueError):
93
+ AlphaAINewsFetcher(top_k=0)
94
+ with pytest.raises(ValueError):
95
+ AlphaAIInsiderNewsFetcher(top_k=-1)
96
+
97
+
98
+ def test_warm_up_uses_resolved_secret(monkeypatch: pytest.MonkeyPatch) -> None:
99
+ created: list[str] = []
100
+
101
+ class RecordingClient:
102
+ def __init__(self, api_key: str) -> None:
103
+ created.append(api_key)
104
+
105
+ monkeypatch.setattr("alphai_haystack.fetchers.Client", RecordingClient)
106
+ monkeypatch.setenv("ALPHAI_API_KEY", "ak_test_123")
107
+ fetcher = AlphaAINewsFetcher()
108
+ fetcher.warm_up()
109
+ fetcher.warm_up() # second call must not create a second client
110
+ assert created == ["ak_test_123"]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from alphai_haystack import AlphaAIInsiderNewsFetcher, AlphaAINewsFetcher
6
+
7
+
8
+ def test_news_fetcher_to_dict_from_dict_roundtrip(monkeypatch: pytest.MonkeyPatch) -> None:
9
+ monkeypatch.setenv("ALPHAI_API_KEY", "ak_test_123")
10
+ fetcher = AlphaAINewsFetcher(
11
+ symbol="NVDA", category="earnings", min_relevance=7, collapse_stories=True, top_k=5
12
+ )
13
+ data = fetcher.to_dict()
14
+
15
+ init = data["init_parameters"]
16
+ assert init["symbol"] == "NVDA"
17
+ assert init["category"] == "earnings"
18
+ assert init["min_relevance"] == 7
19
+ assert init["collapse_stories"] is True
20
+ assert init["top_k"] == 5
21
+ # The secret must serialize as an env-var reference, never as the raw key.
22
+ assert init["api_key"]["type"] == "env_var"
23
+ assert "ak_test_123" not in str(data)
24
+
25
+ restored = AlphaAINewsFetcher.from_dict(data)
26
+ assert restored.symbol == "NVDA"
27
+ assert restored.top_k == 5
28
+ assert restored.api_key.resolve_value() == "ak_test_123"
29
+
30
+
31
+ def test_insider_fetcher_to_dict_from_dict_roundtrip(monkeypatch: pytest.MonkeyPatch) -> None:
32
+ monkeypatch.setenv("ALPHAI_API_KEY", "ak_test_123")
33
+ fetcher = AlphaAIInsiderNewsFetcher(symbol="AAPL", min_relevance=9, top_k=3)
34
+ data = fetcher.to_dict()
35
+
36
+ init = data["init_parameters"]
37
+ assert init["symbol"] == "AAPL"
38
+ assert init["min_relevance"] == 9
39
+ assert init["top_k"] == 3
40
+ assert init["api_key"]["type"] == "env_var"
41
+
42
+ restored = AlphaAIInsiderNewsFetcher.from_dict(data)
43
+ assert restored.symbol == "AAPL"
44
+ assert restored.min_relevance == 9
45
+
46
+
47
+ def test_fetchers_are_pipeline_serializable(monkeypatch: pytest.MonkeyPatch) -> None:
48
+ """The components must survive a full Pipeline YAML round-trip."""
49
+ from haystack import Pipeline
50
+
51
+ monkeypatch.setenv("ALPHAI_API_KEY", "ak_test_123")
52
+ pipeline = Pipeline()
53
+ pipeline.add_component("news", AlphaAINewsFetcher(symbol="NVDA"))
54
+ pipeline.add_component("insider", AlphaAIInsiderNewsFetcher(min_relevance=8))
55
+
56
+ yaml_text = pipeline.dumps()
57
+ assert "ak_test_123" not in yaml_text
58
+
59
+ restored = Pipeline.loads(yaml_text)
60
+ news = restored.get_component("news")
61
+ assert isinstance(news, AlphaAINewsFetcher)
62
+ assert news.symbol == "NVDA"