secfeed-langchain 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_langchain-0.1.0/LICENSE +21 -0
- secfeed_langchain-0.1.0/PKG-INFO +75 -0
- secfeed_langchain-0.1.0/README.md +60 -0
- secfeed_langchain-0.1.0/pyproject.toml +20 -0
- secfeed_langchain-0.1.0/secfeed_langchain/__init__.py +8 -0
- secfeed_langchain-0.1.0/secfeed_langchain/core.py +172 -0
- secfeed_langchain-0.1.0/secfeed_langchain/tools.py +77 -0
- secfeed_langchain-0.1.0/secfeed_langchain.egg-info/PKG-INFO +75 -0
- secfeed_langchain-0.1.0/secfeed_langchain.egg-info/SOURCES.txt +12 -0
- secfeed_langchain-0.1.0/secfeed_langchain.egg-info/dependency_links.txt +1 -0
- secfeed_langchain-0.1.0/secfeed_langchain.egg-info/requires.txt +2 -0
- secfeed_langchain-0.1.0/secfeed_langchain.egg-info/top_level.txt +1 -0
- secfeed_langchain-0.1.0/setup.cfg +4 -0
- secfeed_langchain-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,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: secfeed-langchain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SEC EDGAR tools for LangChain and LangGraph 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-langchain
|
|
8
|
+
Keywords: langchain,langgraph,sec,edgar,10-k,insider trading,financial statements,agents,tools
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: secfeed>=0.1.3
|
|
13
|
+
Requires-Dist: langchain-core>=0.3
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# SEC filing tools for LangChain
|
|
17
|
+
|
|
18
|
+
Give your LangChain and LangGraph 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 or embeddings needed: 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-langchain
|
|
24
|
+
export SECFEED_API_KEY=sf_... # free key: https://secfeedapi.com/signup/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from langchain_openai import ChatOpenAI
|
|
29
|
+
from langchain.agents import create_agent
|
|
30
|
+
from secfeed_langchain import all_tools
|
|
31
|
+
|
|
32
|
+
agent = create_agent(ChatOpenAI(model="gpt-4o-mini"), all_tools())
|
|
33
|
+
reply = agent.invoke({"messages": [("user", "What are NVDA's biggest risks?")]})
|
|
34
|
+
print(reply["messages"][-1].content)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each tool is a normal LangChain `StructuredTool`, so it also works with `llm.bind_tools(...)`, LangChain agents and any framework that accepts LangChain tools.
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | What the agent gets | API calls |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `search_10k` | 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
|
+
| `search_10q` | Same for the latest 10-Q | 2 |
|
|
45
|
+
| `financial_statements` | Income statement, balance sheet and cash flow from XBRL, last periods side by side | 1 |
|
|
46
|
+
| `insider_trades` | Latest Form 4 trades: who, role, buy or sell, shares, price, value, 10b5-1 flag | 1 |
|
|
47
|
+
| `recent_8k` | Latest 8-K reports and the items each one covers, with links | 2 |
|
|
48
|
+
|
|
49
|
+
Repeat questions about the same filing are cached for the life of the process, so they cost no extra calls.
|
|
50
|
+
|
|
51
|
+
You can call a tool directly to see what the model sees:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from secfeed_langchain import insider_trades
|
|
55
|
+
print(insider_trades.invoke({"ticker": "MSFT", "code": "S", "limit": 3}))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Example agent
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install secfeed-langchain langchain langchain-openai
|
|
62
|
+
python examples/stock_agent.py "Is AMD's growth funded by its own cash flow, and are insiders selling?"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Any OpenAI compatible endpoint works: set `OPENAI_BASE_URL` and `MODEL`.
|
|
66
|
+
|
|
67
|
+
## Also available
|
|
68
|
+
|
|
69
|
+
- [secfeed-crewai](https://github.com/secfeedapi/secfeed-crewai): the same tools for CrewAI, with a three agent stock research crew
|
|
70
|
+
- [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)
|
|
71
|
+
- [SecFeed MCP server](https://secfeedapi.com/mcp/) for Claude, Cursor and other MCP clients
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# SEC filing tools for LangChain
|
|
2
|
+
|
|
3
|
+
Give your LangChain and LangGraph 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 or embeddings needed: 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-langchain
|
|
9
|
+
export SECFEED_API_KEY=sf_... # free key: https://secfeedapi.com/signup/
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from langchain_openai import ChatOpenAI
|
|
14
|
+
from langchain.agents import create_agent
|
|
15
|
+
from secfeed_langchain import all_tools
|
|
16
|
+
|
|
17
|
+
agent = create_agent(ChatOpenAI(model="gpt-4o-mini"), all_tools())
|
|
18
|
+
reply = agent.invoke({"messages": [("user", "What are NVDA's biggest risks?")]})
|
|
19
|
+
print(reply["messages"][-1].content)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Each tool is a normal LangChain `StructuredTool`, so it also works with `llm.bind_tools(...)`, LangChain agents and any framework that accepts LangChain tools.
|
|
23
|
+
|
|
24
|
+
## Tools
|
|
25
|
+
|
|
26
|
+
| Tool | What the agent gets | API calls |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `search_10k` | 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
|
+
| `search_10q` | Same for the latest 10-Q | 2 |
|
|
30
|
+
| `financial_statements` | Income statement, balance sheet and cash flow from XBRL, last periods side by side | 1 |
|
|
31
|
+
| `insider_trades` | Latest Form 4 trades: who, role, buy or sell, shares, price, value, 10b5-1 flag | 1 |
|
|
32
|
+
| `recent_8k` | Latest 8-K reports and the items each one covers, with links | 2 |
|
|
33
|
+
|
|
34
|
+
Repeat questions about the same filing are cached for the life of the process, so they cost no extra calls.
|
|
35
|
+
|
|
36
|
+
You can call a tool directly to see what the model sees:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from secfeed_langchain import insider_trades
|
|
40
|
+
print(insider_trades.invoke({"ticker": "MSFT", "code": "S", "limit": 3}))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Example agent
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install secfeed-langchain langchain langchain-openai
|
|
47
|
+
python examples/stock_agent.py "Is AMD's growth funded by its own cash flow, and are insiders selling?"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Any OpenAI compatible endpoint works: set `OPENAI_BASE_URL` and `MODEL`.
|
|
51
|
+
|
|
52
|
+
## Also available
|
|
53
|
+
|
|
54
|
+
- [secfeed-crewai](https://github.com/secfeedapi/secfeed-crewai): the same tools for CrewAI, with a three agent stock research crew
|
|
55
|
+
- [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)
|
|
56
|
+
- [SecFeed MCP server](https://secfeedapi.com/mcp/) for Claude, Cursor and other MCP clients
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "secfeed-langchain"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SEC EDGAR tools for LangChain and LangGraph agents: 10-K/10-Q sections, financial statements, insider trades, 8-Ks"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = ["secfeed>=0.1.3", "langchain-core>=0.3"]
|
|
13
|
+
keywords = ["langchain", "langgraph", "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-langchain"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools]
|
|
20
|
+
packages = ["secfeed_langchain"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""SEC EDGAR tools for LangChain and LangGraph agents, powered by the SecFeed API (https://secfeedapi.com)."""
|
|
2
|
+
__version__ = "0.1.0"
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from .tools import (all_tools, financial_statements, insider_trades, recent_8k, search_10k, # noqa: F401
|
|
6
|
+
search_10q)
|
|
7
|
+
except ImportError: # langchain-core not installed: the plain functions in secfeed_langchain.core still work
|
|
8
|
+
pass
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Plain functions behind the LangChain tools (same code as secfeed-crewai). No LangChain import here.
|
|
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,77 @@
|
|
|
1
|
+
"""LangChain tools for SEC EDGAR data via the SecFeed API. Set SECFEED_API_KEY in the environment."""
|
|
2
|
+
from typing import List, Literal, Optional
|
|
3
|
+
|
|
4
|
+
from langchain_core.tools import BaseTool, StructuredTool
|
|
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 FinancialsInput(BaseModel):
|
|
18
|
+
ticker: str = Field(..., description="Stock ticker, e.g. NVDA")
|
|
19
|
+
form: Literal["10-K", "10-Q"] = Field("10-K", description="10-K for annual figures, 10-Q for the latest quarter")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InsiderInput(BaseModel):
|
|
23
|
+
ticker: str = Field(..., description="Stock ticker, e.g. NVDA")
|
|
24
|
+
code: Optional[str] = Field(None, description="P for open market buys only, S for sales only, empty for all")
|
|
25
|
+
limit: int = Field(20, description="How many transactions, up to 50")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EightKInput(BaseModel):
|
|
29
|
+
ticker: str = Field(..., description="Stock ticker, e.g. NVDA")
|
|
30
|
+
limit: int = Field(10, description="How many reports, up to 50")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _10k(ticker: str, question: str, section: Optional[str] = None) -> str:
|
|
34
|
+
return core.search_filing(ticker, question, "10-K", section or None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _10q(ticker: str, question: str, section: Optional[str] = None) -> str:
|
|
38
|
+
return core.search_filing(ticker, question, "10-Q", section or None)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _fin(ticker: str, form: str = "10-K") -> str:
|
|
42
|
+
return core.financials(ticker, form)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _insider(ticker: str, code: Optional[str] = None, limit: int = 20) -> str:
|
|
46
|
+
code = (code or "").strip().upper()[:1] or None
|
|
47
|
+
return core.insider_trades(ticker, code if code in ("P", "S") else None, limit)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _8k(ticker: str, limit: int = 10) -> str:
|
|
51
|
+
return core.recent_8k(ticker, limit)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
search_10k = StructuredTool.from_function(
|
|
55
|
+
_10k, name="search_latest_10k", args_schema=FilingQuestion,
|
|
56
|
+
description=("Reads the company's latest annual report (10-K) and returns the passages most relevant to the question, "
|
|
57
|
+
"with a link to the filing. Good for business model, risk factors and full-year results."))
|
|
58
|
+
search_10q = StructuredTool.from_function(
|
|
59
|
+
_10q, name="search_latest_10q", args_schema=FilingQuestion,
|
|
60
|
+
description=("Reads the company's latest quarterly report (10-Q) and returns the passages most relevant to the "
|
|
61
|
+
"question, with a link to the filing. Good for the most recent quarter's results and outlook."))
|
|
62
|
+
financial_statements = StructuredTool.from_function(
|
|
63
|
+
_fin, name="financial_statements", args_schema=FinancialsInput,
|
|
64
|
+
description=("Income statement, balance sheet and cash flow figures from the latest 10-K or 10-Q, taken from the "
|
|
65
|
+
"filing's XBRL data, with the last periods side by side."))
|
|
66
|
+
insider_trades = StructuredTool.from_function(
|
|
67
|
+
_insider, name="insider_trades", args_schema=InsiderInput,
|
|
68
|
+
description=("Latest Form 4 insider transactions for a company: who traded, their role, buy or sell, shares, price, "
|
|
69
|
+
"value and whether it was under a 10b5-1 plan."))
|
|
70
|
+
recent_8k = StructuredTool.from_function(
|
|
71
|
+
_8k, name="recent_8k_reports", args_schema=EightKInput,
|
|
72
|
+
description=("The company's latest 8-K current reports and what each one covers (earnings releases, material "
|
|
73
|
+
"agreements, leadership changes, other events), with links."))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def all_tools() -> List[BaseTool]:
|
|
77
|
+
return [search_10k, search_10q, financial_statements, insider_trades, recent_8k]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: secfeed-langchain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SEC EDGAR tools for LangChain and LangGraph 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-langchain
|
|
8
|
+
Keywords: langchain,langgraph,sec,edgar,10-k,insider trading,financial statements,agents,tools
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: secfeed>=0.1.3
|
|
13
|
+
Requires-Dist: langchain-core>=0.3
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# SEC filing tools for LangChain
|
|
17
|
+
|
|
18
|
+
Give your LangChain and LangGraph 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 or embeddings needed: 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-langchain
|
|
24
|
+
export SECFEED_API_KEY=sf_... # free key: https://secfeedapi.com/signup/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from langchain_openai import ChatOpenAI
|
|
29
|
+
from langchain.agents import create_agent
|
|
30
|
+
from secfeed_langchain import all_tools
|
|
31
|
+
|
|
32
|
+
agent = create_agent(ChatOpenAI(model="gpt-4o-mini"), all_tools())
|
|
33
|
+
reply = agent.invoke({"messages": [("user", "What are NVDA's biggest risks?")]})
|
|
34
|
+
print(reply["messages"][-1].content)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each tool is a normal LangChain `StructuredTool`, so it also works with `llm.bind_tools(...)`, LangChain agents and any framework that accepts LangChain tools.
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | What the agent gets | API calls |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `search_10k` | 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
|
+
| `search_10q` | Same for the latest 10-Q | 2 |
|
|
45
|
+
| `financial_statements` | Income statement, balance sheet and cash flow from XBRL, last periods side by side | 1 |
|
|
46
|
+
| `insider_trades` | Latest Form 4 trades: who, role, buy or sell, shares, price, value, 10b5-1 flag | 1 |
|
|
47
|
+
| `recent_8k` | Latest 8-K reports and the items each one covers, with links | 2 |
|
|
48
|
+
|
|
49
|
+
Repeat questions about the same filing are cached for the life of the process, so they cost no extra calls.
|
|
50
|
+
|
|
51
|
+
You can call a tool directly to see what the model sees:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from secfeed_langchain import insider_trades
|
|
55
|
+
print(insider_trades.invoke({"ticker": "MSFT", "code": "S", "limit": 3}))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Example agent
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install secfeed-langchain langchain langchain-openai
|
|
62
|
+
python examples/stock_agent.py "Is AMD's growth funded by its own cash flow, and are insiders selling?"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Any OpenAI compatible endpoint works: set `OPENAI_BASE_URL` and `MODEL`.
|
|
66
|
+
|
|
67
|
+
## Also available
|
|
68
|
+
|
|
69
|
+
- [secfeed-crewai](https://github.com/secfeedapi/secfeed-crewai): the same tools for CrewAI, with a three agent stock research crew
|
|
70
|
+
- [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)
|
|
71
|
+
- [SecFeed MCP server](https://secfeedapi.com/mcp/) for Claude, Cursor and other MCP clients
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
secfeed_langchain/__init__.py
|
|
5
|
+
secfeed_langchain/core.py
|
|
6
|
+
secfeed_langchain/tools.py
|
|
7
|
+
secfeed_langchain.egg-info/PKG-INFO
|
|
8
|
+
secfeed_langchain.egg-info/SOURCES.txt
|
|
9
|
+
secfeed_langchain.egg-info/dependency_links.txt
|
|
10
|
+
secfeed_langchain.egg-info/requires.txt
|
|
11
|
+
secfeed_langchain.egg-info/top_level.txt
|
|
12
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
secfeed_langchain
|
|
@@ -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_langchain 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()
|