secfeed-crewai 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.
- secfeed_crewai-0.1.0/LICENSE +21 -0
- secfeed_crewai-0.1.0/PKG-INFO +94 -0
- secfeed_crewai-0.1.0/README.md +79 -0
- secfeed_crewai-0.1.0/pyproject.toml +20 -0
- secfeed_crewai-0.1.0/secfeed_crewai/__init__.py +8 -0
- secfeed_crewai-0.1.0/secfeed_crewai/core.py +172 -0
- secfeed_crewai-0.1.0/secfeed_crewai/tools.py +85 -0
- secfeed_crewai-0.1.0/secfeed_crewai.egg-info/PKG-INFO +94 -0
- secfeed_crewai-0.1.0/secfeed_crewai.egg-info/SOURCES.txt +12 -0
- secfeed_crewai-0.1.0/secfeed_crewai.egg-info/dependency_links.txt +1 -0
- secfeed_crewai-0.1.0/secfeed_crewai.egg-info/requires.txt +2 -0
- secfeed_crewai-0.1.0/secfeed_crewai.egg-info/top_level.txt +1 -0
- secfeed_crewai-0.1.0/setup.cfg +4 -0
- secfeed_crewai-0.1.0/tests/test_core.py +32 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SecFeed
|
|
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,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: secfeed-crewai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SEC EDGAR tools for CrewAI agents: 10-K/10-Q sections, financial statements, insider trades, 8-Ks
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://secfeedapi.com
|
|
7
|
+
Project-URL: Source, https://github.com/secfeedapi/secfeed-crewai
|
|
8
|
+
Keywords: crewai,sec,edgar,10-k,insider trading,financial statements,agents,tools
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: secfeed>=0.1.3
|
|
13
|
+
Requires-Dist: crewai>=0.80
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# SEC filing tools for CrewAI
|
|
17
|
+
|
|
18
|
+
Give your CrewAI agents SEC EDGAR data: 10-K and 10-Q sections, XBRL financial statements, Form 4 insider trades and 8-K reports. Powered by the [SecFeed API](https://secfeedapi.com).
|
|
19
|
+
|
|
20
|
+
No vector store, no embeddings and no OpenAI key needed for the tools: SecFeed returns the exact filing section (Risk Factors, MD&A, Business), and the tool hands your agent the paragraphs that match its question.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install "secfeed-crewai @ git+https://github.com/secfeedapi/secfeed-crewai"
|
|
24
|
+
export SECFEED_API_KEY=sf_... # free key: https://secfeedapi.com/signup/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from crewai import Agent
|
|
29
|
+
from secfeed_crewai import SearchTenKTool, FinancialStatementsTool, InsiderTradesTool
|
|
30
|
+
|
|
31
|
+
analyst = Agent(
|
|
32
|
+
role="Equity analyst",
|
|
33
|
+
goal="Explain NVDA's risks and financial health from its filings",
|
|
34
|
+
backstory="You only trust primary sources.",
|
|
35
|
+
tools=[SearchTenKTool(), FinancialStatementsTool(), InsiderTradesTool()],
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | What the agent gets | API calls |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `SearchTenKTool` | The paragraphs of the latest 10-K that answer a question, from the right section (Business, Risk Factors, MD&A, Market Risk), with the filing link | 2 |
|
|
44
|
+
| `SearchTenQTool` | Same for the latest 10-Q | 2 |
|
|
45
|
+
| `FinancialStatementsTool` | Income statement, balance sheet and cash flow from XBRL, last periods side by side | 1 |
|
|
46
|
+
| `InsiderTradesTool` | Latest Form 4 trades: who, role, buy or sell, shares, price, value, 10b5-1 flag | 1 |
|
|
47
|
+
| `RecentEightKTool` | Latest 8-K reports and the items each one covers, with links | 2 |
|
|
48
|
+
|
|
49
|
+
`all_tools()` returns all five. The plain functions behind them live in `secfeed_crewai.core` and work without CrewAI (LangChain, LlamaIndex, plain scripts).
|
|
50
|
+
|
|
51
|
+
Example output of `InsiderTradesTool` for MSFT:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
MSFT insider transactions (3817 on file, latest 3):
|
|
55
|
+
- 2026-11-10 ... (EVP, Chief Marketing Officer): open market sale 5.00K sh @ 531.0 = $2.65M
|
|
56
|
+
- 2026-11-09 ... (EVP, Chief Financial Officer): open market sale 5.91K sh @ 544.2359 = $3.22M, 10b5-1 plan
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Example: stock research crew
|
|
60
|
+
|
|
61
|
+
`examples/stock_analysis/main.py` runs three agents (filings researcher, financial analyst, memo writer) and writes `memo-<TICKER>.md`:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
export OPENAI_API_KEY=... # or any model CrewAI supports:
|
|
65
|
+
export MODEL=gpt-4o-mini # e.g. MODEL=anthropic/<model> or MODEL=openrouter/<model>
|
|
66
|
+
python examples/stock_analysis/main.py NVDA
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A full run makes about a dozen SecFeed calls (tested: 11 for AMD), so the free trial covers it.
|
|
70
|
+
|
|
71
|
+
## Coming from the CrewAI stock analysis example?
|
|
72
|
+
|
|
73
|
+
The classic CrewAI stock analysis example looks up the latest filing, downloads the whole HTML page, splits it into chunks and embeds them with OpenAI to answer a question. These tools replace that whole file:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# before
|
|
77
|
+
from tools.sec_tools import SECTools
|
|
78
|
+
tools = [SECTools.search_10q, SECTools.search_10k]
|
|
79
|
+
|
|
80
|
+
# after
|
|
81
|
+
from secfeed_crewai import SearchTenQTool, SearchTenKTool
|
|
82
|
+
tools = [SearchTenQTool(), SearchTenKTool()]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
You drop `unstructured`, `faiss` and the embeddings call, and the agent reads the actual section instead of random chunks of the page.
|
|
86
|
+
|
|
87
|
+
## Also available
|
|
88
|
+
|
|
89
|
+
- [secfeed](https://pypi.org/project/secfeed/) Python and Node SDKs for every SecFeed endpoint (13F holdings, Form D, full text search, XBRL frames and more)
|
|
90
|
+
- [SecFeed MCP server](https://secfeedapi.com/mcp/) for Claude, Cursor and other MCP clients
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# SEC filing tools for CrewAI
|
|
2
|
+
|
|
3
|
+
Give your CrewAI agents SEC EDGAR data: 10-K and 10-Q sections, XBRL financial statements, Form 4 insider trades and 8-K reports. Powered by the [SecFeed API](https://secfeedapi.com).
|
|
4
|
+
|
|
5
|
+
No vector store, no embeddings and no OpenAI key needed for the tools: SecFeed returns the exact filing section (Risk Factors, MD&A, Business), and the tool hands your agent the paragraphs that match its question.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install "secfeed-crewai @ git+https://github.com/secfeedapi/secfeed-crewai"
|
|
9
|
+
export SECFEED_API_KEY=sf_... # free key: https://secfeedapi.com/signup/
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from crewai import Agent
|
|
14
|
+
from secfeed_crewai import SearchTenKTool, FinancialStatementsTool, InsiderTradesTool
|
|
15
|
+
|
|
16
|
+
analyst = Agent(
|
|
17
|
+
role="Equity analyst",
|
|
18
|
+
goal="Explain NVDA's risks and financial health from its filings",
|
|
19
|
+
backstory="You only trust primary sources.",
|
|
20
|
+
tools=[SearchTenKTool(), FinancialStatementsTool(), InsiderTradesTool()],
|
|
21
|
+
)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Tools
|
|
25
|
+
|
|
26
|
+
| Tool | What the agent gets | API calls |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `SearchTenKTool` | The paragraphs of the latest 10-K that answer a question, from the right section (Business, Risk Factors, MD&A, Market Risk), with the filing link | 2 |
|
|
29
|
+
| `SearchTenQTool` | Same for the latest 10-Q | 2 |
|
|
30
|
+
| `FinancialStatementsTool` | Income statement, balance sheet and cash flow from XBRL, last periods side by side | 1 |
|
|
31
|
+
| `InsiderTradesTool` | Latest Form 4 trades: who, role, buy or sell, shares, price, value, 10b5-1 flag | 1 |
|
|
32
|
+
| `RecentEightKTool` | Latest 8-K reports and the items each one covers, with links | 2 |
|
|
33
|
+
|
|
34
|
+
`all_tools()` returns all five. The plain functions behind them live in `secfeed_crewai.core` and work without CrewAI (LangChain, LlamaIndex, plain scripts).
|
|
35
|
+
|
|
36
|
+
Example output of `InsiderTradesTool` for MSFT:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
MSFT insider transactions (3817 on file, latest 3):
|
|
40
|
+
- 2026-11-10 ... (EVP, Chief Marketing Officer): open market sale 5.00K sh @ 531.0 = $2.65M
|
|
41
|
+
- 2026-11-09 ... (EVP, Chief Financial Officer): open market sale 5.91K sh @ 544.2359 = $3.22M, 10b5-1 plan
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Example: stock research crew
|
|
45
|
+
|
|
46
|
+
`examples/stock_analysis/main.py` runs three agents (filings researcher, financial analyst, memo writer) and writes `memo-<TICKER>.md`:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export OPENAI_API_KEY=... # or any model CrewAI supports:
|
|
50
|
+
export MODEL=gpt-4o-mini # e.g. MODEL=anthropic/<model> or MODEL=openrouter/<model>
|
|
51
|
+
python examples/stock_analysis/main.py NVDA
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A full run makes about a dozen SecFeed calls (tested: 11 for AMD), so the free trial covers it.
|
|
55
|
+
|
|
56
|
+
## Coming from the CrewAI stock analysis example?
|
|
57
|
+
|
|
58
|
+
The classic CrewAI stock analysis example looks up the latest filing, downloads the whole HTML page, splits it into chunks and embeds them with OpenAI to answer a question. These tools replace that whole file:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# before
|
|
62
|
+
from tools.sec_tools import SECTools
|
|
63
|
+
tools = [SECTools.search_10q, SECTools.search_10k]
|
|
64
|
+
|
|
65
|
+
# after
|
|
66
|
+
from secfeed_crewai import SearchTenQTool, SearchTenKTool
|
|
67
|
+
tools = [SearchTenQTool(), SearchTenKTool()]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
You drop `unstructured`, `faiss` and the embeddings call, and the agent reads the actual section instead of random chunks of the page.
|
|
71
|
+
|
|
72
|
+
## Also available
|
|
73
|
+
|
|
74
|
+
- [secfeed](https://pypi.org/project/secfeed/) Python and Node SDKs for every SecFeed endpoint (13F holdings, Form D, full text search, XBRL frames and more)
|
|
75
|
+
- [SecFeed MCP server](https://secfeedapi.com/mcp/) for Claude, Cursor and other MCP clients
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "secfeed-crewai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SEC EDGAR tools for CrewAI agents: 10-K/10-Q sections, financial statements, insider trades, 8-Ks"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = ["secfeed>=0.1.3", "crewai>=0.80"]
|
|
13
|
+
keywords = ["crewai", "sec", "edgar", "10-k", "insider trading", "financial statements", "agents", "tools"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://secfeedapi.com"
|
|
17
|
+
Source = "https://github.com/secfeedapi/secfeed-crewai"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools]
|
|
20
|
+
packages = ["secfeed_crewai"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""SEC EDGAR tools for CrewAI agents, powered by the SecFeed API (https://secfeedapi.com)."""
|
|
2
|
+
__version__ = "0.1.0"
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from .tools import (FinancialStatementsTool, InsiderTradesTool, RecentEightKTool, SearchTenKTool, # noqa: F401
|
|
6
|
+
SearchTenQTool, all_tools)
|
|
7
|
+
except ImportError: # crewai not installed: the plain functions in secfeed_crewai.core still work
|
|
8
|
+
pass
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Plain functions behind the CrewAI tools. No CrewAI import here, so they work in any agent framework (or none).
|
|
2
|
+
|
|
3
|
+
Every function returns compact text sized for an LLM context window, and costs one or two SecFeed API calls.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from secfeed import Client, SecFeedError
|
|
9
|
+
|
|
10
|
+
# Which part of a filing answers which kind of question. 10-K items: 1 Business, 1A Risk Factors, 7 MD&A, 7A market risk.
|
|
11
|
+
# 10-Q items: part1item2 MD&A, part2item1a risk factor updates.
|
|
12
|
+
SECTIONS = {
|
|
13
|
+
"10-K": {"business": "1", "risk": "1A", "mdna": "7", "market_risk": "7A"},
|
|
14
|
+
"10-Q": {"business": "part1item2", "risk": "part2item1a", "mdna": "part1item2", "market_risk": "part1item3"},
|
|
15
|
+
}
|
|
16
|
+
ROUTES = [ # (words in the question, section) checked in order; MD&A is the default
|
|
17
|
+
(("risk", "threat", "uncertain", "litigation", "regulat", "lawsuit"), "risk"),
|
|
18
|
+
(("interest rate", "currency", "foreign exchange", "hedg", "market risk"), "market_risk"),
|
|
19
|
+
(("product", "segment", "customer", "competit", "employee", "strategy", "business model", "what does"), "business"),
|
|
20
|
+
]
|
|
21
|
+
SECTION_NAMES = {"business": "Business", "risk": "Risk Factors", "mdna": "Management's Discussion and Analysis",
|
|
22
|
+
"market_risk": "Market Risk"}
|
|
23
|
+
STOP = set("the a an of to in and or for on is are was were be by with what how did does do its it this that from as at "
|
|
24
|
+
"which who their they has have had last year quarter company".split())
|
|
25
|
+
MAX_CHARS = 6000
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def client(api_key=None):
|
|
29
|
+
key = api_key or os.environ.get("SECFEED_API_KEY")
|
|
30
|
+
if not key:
|
|
31
|
+
raise SecFeedError("Set SECFEED_API_KEY (free key at https://secfeedapi.com/signup/)")
|
|
32
|
+
return Client(key)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _section_for(question):
|
|
36
|
+
q = question.lower()
|
|
37
|
+
for words, section in ROUTES:
|
|
38
|
+
if any(w in q for w in words):
|
|
39
|
+
return section
|
|
40
|
+
return "mdna"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _terms(text):
|
|
44
|
+
return {w for w in re.findall(r"[a-z][a-z0-9&\-]+", text.lower()) if w not in STOP and len(w) > 2}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def best_passages(text, question, max_chars=MAX_CHARS):
|
|
48
|
+
"""Paragraphs of `text` ranked by how many of the question's words they contain, kept in document order.
|
|
49
|
+
Simple keyword ranking: no embeddings, no vector store, no extra API key."""
|
|
50
|
+
paras = [p.strip() for p in re.split(r"\n\s*\n", text) if len(p.strip()) > 80]
|
|
51
|
+
terms = _terms(question)
|
|
52
|
+
if not paras:
|
|
53
|
+
return text[:max_chars]
|
|
54
|
+
scored = sorted(range(len(paras)), key=lambda i: -len(terms & _terms(paras[i])))
|
|
55
|
+
keep, size = set(), 0
|
|
56
|
+
for i in scored:
|
|
57
|
+
if size + len(paras[i]) > max_chars and keep:
|
|
58
|
+
continue
|
|
59
|
+
keep.add(i)
|
|
60
|
+
size += len(paras[i])
|
|
61
|
+
if size >= max_chars:
|
|
62
|
+
break
|
|
63
|
+
return "\n\n".join(paras[i] for i in sorted(keep))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_FILINGS, _SECTIONS = {}, {} # per-process caches: agents often ask several questions about the same filing
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def latest_filing(sf, ticker, form):
|
|
70
|
+
k = (ticker.upper(), form)
|
|
71
|
+
if k not in _FILINGS:
|
|
72
|
+
res = sf.filings(f'ticker:{ticker.upper()} AND formType:"{form}"', size=1)
|
|
73
|
+
_FILINGS[k] = (res.get("filings") or [None])[0]
|
|
74
|
+
return _FILINGS[k]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _section_text(sf, url, item):
|
|
78
|
+
if (url, item) not in _SECTIONS:
|
|
79
|
+
_SECTIONS[(url, item)] = sf.extract(url, item)
|
|
80
|
+
return _SECTIONS[(url, item)]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def search_filing(ticker, question, form="10-K", section=None, api_key=None):
|
|
84
|
+
"""Answer material for `question` from the company's latest 10-K or 10-Q: picks the right section (or `section`:
|
|
85
|
+
business, risk, mdna, market_risk), then returns the most relevant paragraphs."""
|
|
86
|
+
sf = client(api_key)
|
|
87
|
+
form = form.upper()
|
|
88
|
+
if form not in SECTIONS:
|
|
89
|
+
return "form must be 10-K or 10-Q"
|
|
90
|
+
f = latest_filing(sf, ticker, form)
|
|
91
|
+
if not f:
|
|
92
|
+
return f"No {form} found for {ticker}. Check the ticker."
|
|
93
|
+
section = section if section in SECTIONS[form] else _section_for(question)
|
|
94
|
+
try:
|
|
95
|
+
text = _section_text(sf, f["linkToFilingDetails"], SECTIONS[form][section])
|
|
96
|
+
except SecFeedError as e:
|
|
97
|
+
return f"Could not read {SECTION_NAMES[section]} of {f['companyName']} {form} filed {f['filedAt']}: {e}"
|
|
98
|
+
head = (f"{f['companyName']} ({ticker.upper()}) {form} filed {f['filedAt']}, section: {SECTION_NAMES[section]}\n"
|
|
99
|
+
f"Source: {f['linkToFilingDetails']}\n\n")
|
|
100
|
+
return head + best_passages(text, question)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _money(v):
|
|
104
|
+
try:
|
|
105
|
+
v = float(v)
|
|
106
|
+
except (TypeError, ValueError):
|
|
107
|
+
return str(v)
|
|
108
|
+
for div, unit in ((1e9, "B"), (1e6, "M"), (1e3, "K")):
|
|
109
|
+
if abs(v) >= div:
|
|
110
|
+
return f"{v / div:,.2f}{unit}"
|
|
111
|
+
return f"{v:,.2f}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def financials(ticker, form="10-K", api_key=None):
|
|
115
|
+
"""Income statement, balance sheet and cash flow from the latest 10-K or 10-Q (XBRL), newest periods first."""
|
|
116
|
+
sf = client(api_key)
|
|
117
|
+
rows = sf.financial_statements(ticker.upper(), form_type=form.upper(), n=1)
|
|
118
|
+
if not rows:
|
|
119
|
+
return f"No XBRL financial statements found for {ticker}."
|
|
120
|
+
data = rows[0]
|
|
121
|
+
out = [f"{ticker.upper()} {form.upper()} financial statements (accession {data.get('accessionNo')}), USD unless noted"]
|
|
122
|
+
for name, items in data.items():
|
|
123
|
+
if not isinstance(items, dict):
|
|
124
|
+
continue
|
|
125
|
+
out.append(f"\n{name}")
|
|
126
|
+
for concept, facts in list(items.items())[:25]:
|
|
127
|
+
if not isinstance(facts, list):
|
|
128
|
+
continue
|
|
129
|
+
vals = []
|
|
130
|
+
for x in [x for x in facts if not x.get("segment")][:2]:
|
|
131
|
+
p = x.get("period") or {}
|
|
132
|
+
when = p.get("endDate") or p.get("instant") or ""
|
|
133
|
+
vals.append(f"{when}: {_money(x.get('value'))}")
|
|
134
|
+
if vals:
|
|
135
|
+
out.append(f" {concept}: " + "; ".join(vals))
|
|
136
|
+
return "\n".join(out)[:12000]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
CODES = {"P": "open market buy", "S": "open market sale", "A": "grant", "M": "option exercise", "F": "tax withholding",
|
|
140
|
+
"G": "gift"}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def insider_trades(ticker, code=None, limit=20, api_key=None):
|
|
144
|
+
"""Latest Form 4 insider transactions. code: P (buys), S (sales) or None for all."""
|
|
145
|
+
sf = client(api_key)
|
|
146
|
+
res = sf.insider_trades(ticker=ticker.upper(), code=code, size=min(int(limit), 50))
|
|
147
|
+
tx = res.get("transactions") or []
|
|
148
|
+
if not tx:
|
|
149
|
+
return f"No insider transactions found for {ticker}."
|
|
150
|
+
out = [f"{ticker.upper()} insider transactions ({res.get('total')} on file, latest {len(tx)}):"]
|
|
151
|
+
for t in tx:
|
|
152
|
+
o = (t.get("owners") or [{}])[0]
|
|
153
|
+
role = o.get("officerTitle") or ("Director" if o.get("isDirector") else "10% owner" if o.get("isTenPercentOwner") else "")
|
|
154
|
+
val = f" = ${_money(t['shares'] * t['price'])}" if t.get("shares") and t.get("price") else ""
|
|
155
|
+
plan = ", 10b5-1 plan" if t.get("aff10b5one") else ""
|
|
156
|
+
out.append(f"- {t.get('tx_date')} {o.get('name')} ({role}): {CODES.get(t.get('code'), t.get('code'))} "
|
|
157
|
+
f"{_money(t.get('shares'))} sh @ {t.get('price')}{val}{plan}")
|
|
158
|
+
return "\n".join(out)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def recent_8k(ticker, limit=10, api_key=None):
|
|
162
|
+
"""Latest 8-K current reports with the items each one covers (earnings, deals, leadership, etc.)."""
|
|
163
|
+
sf = client(api_key)
|
|
164
|
+
f = latest_filing(sf, ticker, "8-K")
|
|
165
|
+
if not f:
|
|
166
|
+
return f"No 8-K found for {ticker}."
|
|
167
|
+
res = sf.events_8k(cik=f["cik"], size=min(int(limit), 50))
|
|
168
|
+
out = [f"{f['companyName']} ({ticker.upper()}) latest 8-K reports:"]
|
|
169
|
+
for e in res.get("filings") or []:
|
|
170
|
+
items = "; ".join(f"{i['item']} {i['title']}" for i in e.get("items") or [])
|
|
171
|
+
out.append(f"- {e.get('filed')}: {items} {e.get('url')}")
|
|
172
|
+
return "\n".join(out)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""CrewAI tools for SEC EDGAR data via the SecFeed API. Set SECFEED_API_KEY in the environment."""
|
|
2
|
+
from typing import Literal, Optional, Type
|
|
3
|
+
|
|
4
|
+
from crewai.tools import BaseTool
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from . import core
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FilingQuestion(BaseModel):
|
|
11
|
+
ticker: str = Field(..., description="Stock ticker, e.g. NVDA")
|
|
12
|
+
question: str = Field(..., description="What you want to find out from the filing")
|
|
13
|
+
section: Optional[str] = Field(
|
|
14
|
+
None, description="Optional: business, risk, mdna or market_risk. Picked from the question when left empty.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SearchTenKTool(BaseTool):
|
|
18
|
+
name: str = "Search latest 10-K"
|
|
19
|
+
description: str = ("Reads the company's latest annual report (10-K) and returns the passages most relevant to the "
|
|
20
|
+
"question, with a link to the filing. Good for business model, risk factors and full-year results.")
|
|
21
|
+
args_schema: Type[BaseModel] = FilingQuestion
|
|
22
|
+
|
|
23
|
+
def _run(self, ticker: str, question: str, section: Optional[str] = None) -> str:
|
|
24
|
+
return core.search_filing(ticker, question, "10-K", section or None)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SearchTenQTool(BaseTool):
|
|
28
|
+
name: str = "Search latest 10-Q"
|
|
29
|
+
description: str = ("Reads the company's latest quarterly report (10-Q) and returns the passages most relevant to the "
|
|
30
|
+
"question, with a link to the filing. Good for the most recent quarter's results and outlook.")
|
|
31
|
+
args_schema: Type[BaseModel] = FilingQuestion
|
|
32
|
+
|
|
33
|
+
def _run(self, ticker: str, question: str, section: Optional[str] = None) -> str:
|
|
34
|
+
return core.search_filing(ticker, question, "10-Q", section or None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FinancialsInput(BaseModel):
|
|
38
|
+
ticker: str = Field(..., description="Stock ticker, e.g. NVDA")
|
|
39
|
+
form: Literal["10-K", "10-Q"] = Field("10-K", description="10-K for annual figures, 10-Q for the latest quarter")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FinancialStatementsTool(BaseTool):
|
|
43
|
+
name: str = "Financial statements"
|
|
44
|
+
description: str = ("Income statement, balance sheet and cash flow figures from the latest 10-K or 10-Q, taken from "
|
|
45
|
+
"the filing's XBRL data, with the last few periods side by side.")
|
|
46
|
+
args_schema: Type[BaseModel] = FinancialsInput
|
|
47
|
+
|
|
48
|
+
def _run(self, ticker: str, form: str = "10-K") -> str:
|
|
49
|
+
return core.financials(ticker, form)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InsiderInput(BaseModel):
|
|
53
|
+
ticker: str = Field(..., description="Stock ticker, e.g. NVDA")
|
|
54
|
+
code: Optional[str] = Field(None, description="P for open market buys only, S for sales only, empty for all")
|
|
55
|
+
limit: int = Field(20, description="How many transactions, up to 50")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class InsiderTradesTool(BaseTool):
|
|
59
|
+
name: str = "Insider trades"
|
|
60
|
+
description: str = ("Latest Form 4 insider transactions for a company: who traded, their role, buy or sell, shares, "
|
|
61
|
+
"price, value and whether it was under a 10b5-1 plan.")
|
|
62
|
+
args_schema: Type[BaseModel] = InsiderInput
|
|
63
|
+
|
|
64
|
+
def _run(self, ticker: str, code: Optional[str] = None, limit: int = 20) -> str:
|
|
65
|
+
code = (code or "").strip().upper()[:1] or None
|
|
66
|
+
return core.insider_trades(ticker, code if code in ("P", "S") else None, limit)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EightKInput(BaseModel):
|
|
70
|
+
ticker: str = Field(..., description="Stock ticker, e.g. NVDA")
|
|
71
|
+
limit: int = Field(10, description="How many reports, up to 50")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RecentEightKTool(BaseTool):
|
|
75
|
+
name: str = "Recent 8-K reports"
|
|
76
|
+
description: str = ("The company's latest 8-K current reports and what each one covers (earnings releases, material "
|
|
77
|
+
"agreements, leadership changes, other events), with links.")
|
|
78
|
+
args_schema: Type[BaseModel] = EightKInput
|
|
79
|
+
|
|
80
|
+
def _run(self, ticker: str, limit: int = 10) -> str:
|
|
81
|
+
return core.recent_8k(ticker, limit)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def all_tools():
|
|
85
|
+
return [SearchTenKTool(), SearchTenQTool(), FinancialStatementsTool(), InsiderTradesTool(), RecentEightKTool()]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: secfeed-crewai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SEC EDGAR tools for CrewAI agents: 10-K/10-Q sections, financial statements, insider trades, 8-Ks
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://secfeedapi.com
|
|
7
|
+
Project-URL: Source, https://github.com/secfeedapi/secfeed-crewai
|
|
8
|
+
Keywords: crewai,sec,edgar,10-k,insider trading,financial statements,agents,tools
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: secfeed>=0.1.3
|
|
13
|
+
Requires-Dist: crewai>=0.80
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# SEC filing tools for CrewAI
|
|
17
|
+
|
|
18
|
+
Give your CrewAI agents SEC EDGAR data: 10-K and 10-Q sections, XBRL financial statements, Form 4 insider trades and 8-K reports. Powered by the [SecFeed API](https://secfeedapi.com).
|
|
19
|
+
|
|
20
|
+
No vector store, no embeddings and no OpenAI key needed for the tools: SecFeed returns the exact filing section (Risk Factors, MD&A, Business), and the tool hands your agent the paragraphs that match its question.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install "secfeed-crewai @ git+https://github.com/secfeedapi/secfeed-crewai"
|
|
24
|
+
export SECFEED_API_KEY=sf_... # free key: https://secfeedapi.com/signup/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from crewai import Agent
|
|
29
|
+
from secfeed_crewai import SearchTenKTool, FinancialStatementsTool, InsiderTradesTool
|
|
30
|
+
|
|
31
|
+
analyst = Agent(
|
|
32
|
+
role="Equity analyst",
|
|
33
|
+
goal="Explain NVDA's risks and financial health from its filings",
|
|
34
|
+
backstory="You only trust primary sources.",
|
|
35
|
+
tools=[SearchTenKTool(), FinancialStatementsTool(), InsiderTradesTool()],
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | What the agent gets | API calls |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `SearchTenKTool` | The paragraphs of the latest 10-K that answer a question, from the right section (Business, Risk Factors, MD&A, Market Risk), with the filing link | 2 |
|
|
44
|
+
| `SearchTenQTool` | Same for the latest 10-Q | 2 |
|
|
45
|
+
| `FinancialStatementsTool` | Income statement, balance sheet and cash flow from XBRL, last periods side by side | 1 |
|
|
46
|
+
| `InsiderTradesTool` | Latest Form 4 trades: who, role, buy or sell, shares, price, value, 10b5-1 flag | 1 |
|
|
47
|
+
| `RecentEightKTool` | Latest 8-K reports and the items each one covers, with links | 2 |
|
|
48
|
+
|
|
49
|
+
`all_tools()` returns all five. The plain functions behind them live in `secfeed_crewai.core` and work without CrewAI (LangChain, LlamaIndex, plain scripts).
|
|
50
|
+
|
|
51
|
+
Example output of `InsiderTradesTool` for MSFT:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
MSFT insider transactions (3817 on file, latest 3):
|
|
55
|
+
- 2026-11-10 ... (EVP, Chief Marketing Officer): open market sale 5.00K sh @ 531.0 = $2.65M
|
|
56
|
+
- 2026-11-09 ... (EVP, Chief Financial Officer): open market sale 5.91K sh @ 544.2359 = $3.22M, 10b5-1 plan
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Example: stock research crew
|
|
60
|
+
|
|
61
|
+
`examples/stock_analysis/main.py` runs three agents (filings researcher, financial analyst, memo writer) and writes `memo-<TICKER>.md`:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
export OPENAI_API_KEY=... # or any model CrewAI supports:
|
|
65
|
+
export MODEL=gpt-4o-mini # e.g. MODEL=anthropic/<model> or MODEL=openrouter/<model>
|
|
66
|
+
python examples/stock_analysis/main.py NVDA
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A full run makes about a dozen SecFeed calls (tested: 11 for AMD), so the free trial covers it.
|
|
70
|
+
|
|
71
|
+
## Coming from the CrewAI stock analysis example?
|
|
72
|
+
|
|
73
|
+
The classic CrewAI stock analysis example looks up the latest filing, downloads the whole HTML page, splits it into chunks and embeds them with OpenAI to answer a question. These tools replace that whole file:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# before
|
|
77
|
+
from tools.sec_tools import SECTools
|
|
78
|
+
tools = [SECTools.search_10q, SECTools.search_10k]
|
|
79
|
+
|
|
80
|
+
# after
|
|
81
|
+
from secfeed_crewai import SearchTenQTool, SearchTenKTool
|
|
82
|
+
tools = [SearchTenQTool(), SearchTenKTool()]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
You drop `unstructured`, `faiss` and the embeddings call, and the agent reads the actual section instead of random chunks of the page.
|
|
86
|
+
|
|
87
|
+
## Also available
|
|
88
|
+
|
|
89
|
+
- [secfeed](https://pypi.org/project/secfeed/) Python and Node SDKs for every SecFeed endpoint (13F holdings, Form D, full text search, XBRL frames and more)
|
|
90
|
+
- [SecFeed MCP server](https://secfeedapi.com/mcp/) for Claude, Cursor and other MCP clients
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
secfeed_crewai/__init__.py
|
|
5
|
+
secfeed_crewai/core.py
|
|
6
|
+
secfeed_crewai/tools.py
|
|
7
|
+
secfeed_crewai.egg-info/PKG-INFO
|
|
8
|
+
secfeed_crewai.egg-info/SOURCES.txt
|
|
9
|
+
secfeed_crewai.egg-info/dependency_links.txt
|
|
10
|
+
secfeed_crewai.egg-info/requires.txt
|
|
11
|
+
secfeed_crewai.egg-info/top_level.txt
|
|
12
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
secfeed_crewai
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Offline tests: section routing and passage ranking. Run: python -m unittest discover -s tests"""
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from secfeed_crewai import core
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Routing(unittest.TestCase):
|
|
8
|
+
def test_sections(self):
|
|
9
|
+
self.assertEqual(core._section_for("What are the biggest risks?"), "risk")
|
|
10
|
+
self.assertEqual(core._section_for("Who are its main competitors?"), "business")
|
|
11
|
+
self.assertEqual(core._section_for("How exposed is it to interest rate changes?"), "market_risk")
|
|
12
|
+
self.assertEqual(core._section_for("How did revenue grow?"), "mdna")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Passages(unittest.TestCase):
|
|
16
|
+
def test_keeps_matching_paragraphs_in_order(self):
|
|
17
|
+
filler = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore. " * 3
|
|
18
|
+
text = "\n\n".join([filler, "Export controls on China reduced our data center revenue in the quarter. " * 2,
|
|
19
|
+
filler, "Tariffs and export controls may change again and hurt China sales. " * 2])
|
|
20
|
+
out = core.best_passages(text, "export controls China", max_chars=400)
|
|
21
|
+
self.assertIn("reduced our data center", out)
|
|
22
|
+
self.assertIn("Tariffs", out)
|
|
23
|
+
self.assertLess(out.index("reduced"), out.index("Tariffs"))
|
|
24
|
+
self.assertNotIn("Lorem", out)
|
|
25
|
+
|
|
26
|
+
def test_money(self):
|
|
27
|
+
self.assertEqual(core._money("215938000000"), "215.94B")
|
|
28
|
+
self.assertEqual(core._money(5000), "5.00K")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
unittest.main()
|