dahutapea commited on
Commit
6ba3ef3
·
1 Parent(s): 0fa006f

FinChat: RAG chatbot over SEC 10-K filings

Browse files
Files changed (15) hide show
  1. .env.example +3 -0
  2. .gitignore +30 -0
  3. .streamlit/config.toml +11 -0
  4. DEPLOY.md +112 -0
  5. README.md +155 -13
  6. app.py +65 -0
  7. eval/__init__.py +0 -0
  8. eval/gold_set.py +137 -0
  9. eval/results.md +207 -0
  10. eval/run_eval.py +111 -0
  11. requirements.txt +27 -3
  12. src/__init__.py +0 -0
  13. src/config.py +53 -0
  14. src/ingest.py +196 -0
  15. src/rag.py +172 -0
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Copy this file to ".env" and paste your own key.
2
+ # Get a free key (no credit card) at https://console.groq.com
3
+ GROQ_API_KEY=your_groq_api_key_here
.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Secrets (NEVER commit) ---
2
+ .env
3
+
4
+ # --- Python ---
5
+ __pycache__/
6
+ *.py[cod]
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ *.log
10
+
11
+ # --- Virtual environment ---
12
+ venv/
13
+ .venv/
14
+ env/
15
+
16
+ # --- Generated data & vector store (rebuild with ingest.py) ---
17
+ data/
18
+ vectorstore/
19
+
20
+ # --- Model / HF cache (can be large) ---
21
+ .cache/
22
+
23
+ # --- Streamlit ---
24
+ .streamlit/secrets.toml
25
+
26
+ # --- OS / editor ---
27
+ .DS_Store
28
+ Thumbs.db
29
+ .vscode/
30
+ .idea/
.streamlit/config.toml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Streamlit configuration for FinChat
2
+ [server]
3
+ headless = true
4
+ # Disable the source file-watcher. It walks every imported module (including
5
+ # transformers' lazy image processors that import torchvision, which we don't
6
+ # install), producing noisy tracebacks and slow startup. We don't need
7
+ # hot-reload for a deployed app.
8
+ fileWatcherType = "none"
9
+
10
+ [browser]
11
+ gatherUsageStats = false
DEPLOY.md ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploying FinChat to Hugging Face Spaces
2
+
3
+ A free, step-by-step guide to putting FinChat online with a public link.
4
+
5
+ ---
6
+
7
+ ## 0. Before you start
8
+
9
+ 1. **Rotate your Groq API key.** Go to <https://console.groq.com> → *API Keys* → create a new
10
+ key. You'll paste the new key into the Space as a secret (never commit it).
11
+ 2. Create a free **Hugging Face account**: <https://huggingface.co/join>.
12
+ 3. Make sure **git** is installed (it is on this machine).
13
+
14
+ ---
15
+
16
+ ## 1. Create the Space
17
+
18
+ 1. Go to <https://huggingface.co/new-space>.
19
+ 2. **Owner:** you. **Space name:** `finchat`.
20
+ 3. **SDK:** choose **Streamlit**.
21
+ 4. **Hardware:** *CPU basic* (free). **Visibility:** *Public*.
22
+ 5. Click **Create Space**. You now have an empty Space repo.
23
+
24
+ ---
25
+
26
+ ## 2. Add your API key as a secret
27
+
28
+ In your Space: **Settings → Variables and secrets → New secret**
29
+ - **Name:** `GROQ_API_KEY`
30
+ - **Value:** your Groq key
31
+
32
+ The app reads this automatically (`os.getenv("GROQ_API_KEY")`), so no `.env` is
33
+ needed on the Space.
34
+
35
+ ---
36
+
37
+ ## 3. Push the code
38
+
39
+ From the project root:
40
+
41
+ ```bash
42
+ git init
43
+ git add .
44
+ git commit -m "FinChat: RAG chatbot over SEC 10-K filings"
45
+
46
+ # Connect to your Space (replace <username>)
47
+ git remote add space https://huggingface.co/spaces/<username>/finchat
48
+ git push space main
49
+ ```
50
+
51
+ > `.gitignore` already excludes `.env`, `venv/`, `data/`, and `vectorstore/`,
52
+ > so your key and large files stay out of the repo.
53
+
54
+ The Space will build (install `requirements.txt`) and start. **On first load,
55
+ the app builds the vector store itself** (downloads the dataset + embeds the
56
+ chunks). On free CPU this takes roughly **5–8 minutes** the first time — the UI
57
+ shows *"Preparing the knowledge base…"*. After that it's fast.
58
+
59
+ ---
60
+
61
+ ## 4. (Optional) Instant cold starts — commit the prebuilt index
62
+
63
+ Free Spaces sleep after inactivity and rebuild the index on wake (~5–8 min),
64
+ which is slow for someone clicking your link cold. To make cold starts
65
+ **instant**, commit the prebuilt vector store using **git-lfs** (it's ~66 MB).
66
+
67
+ ```bash
68
+ # One-time: install git-lfs from https://git-lfs.com then:
69
+ git lfs install
70
+
71
+ # Copy the prebuilt store into the repo
72
+ mkdir vectorstore
73
+ cp -r ~/.finchat/vectorstore/* vectorstore/ # Windows: xcopy /E /I %USERPROFILE%\.finchat\vectorstore vectorstore
74
+
75
+ # Track the large binary files with LFS
76
+ git lfs track "vectorstore/**"
77
+ git add .gitattributes
78
+
79
+ # Stop ignoring the committed store, then commit it
80
+ # -> remove the "vectorstore/" line from .gitignore first
81
+ git add vectorstore .gitignore
82
+ git commit -m "Add prebuilt vector store for instant startup"
83
+ git push space main
84
+ ```
85
+
86
+ Then, in the Space **Settings → Variables and secrets**, add a **variable**
87
+ (not a secret):
88
+ - **Name:** `FINCHAT_VECTORSTORE`
89
+ - **Value:** `vectorstore`
90
+
91
+ Now `ensure_index()` finds the committed store and skips the rebuild entirely.
92
+
93
+ ---
94
+
95
+ ## 5. Verify
96
+
97
+ 1. Open your Space URL.
98
+ 2. Wait for the first load (see the spinner if it's building).
99
+ 3. Ask: *"What are AMD's main business risks?"* — you should get a grounded
100
+ answer with a *Sources* panel and a routing badge.
101
+ 4. Add the live link to the top of `README.md` and to your portfolio.
102
+
103
+ ---
104
+
105
+ ## Troubleshooting
106
+
107
+ | Symptom | Fix |
108
+ |---|---|
109
+ | `GROQ_API_KEY is not set` | Add the secret (Step 2) and *Restart* the Space. |
110
+ | Build error on `datasets` | Confirm `requirements.txt` pins `datasets<3.0`. |
111
+ | Stuck on "Preparing the knowledge base" | First build is slow on free CPU; wait, or use Step 4. |
112
+ | Sidebar hidden | Click the **›** at the top-left to expand it. |
README.md CHANGED
@@ -1,19 +1,161 @@
1
  ---
2
- title: Finchat
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
  pinned: false
11
- short_description: ChatBot that provides Annual Reports
12
  ---
13
 
14
- # Welcome to Streamlit!
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
 
 
 
 
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: FinChat
3
+ emoji: 💬
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: streamlit
7
+ sdk_version: 1.40.0
8
+ app_file: app.py
 
9
  pinned: false
 
10
  ---
11
 
12
+ # 💬 FinChat — Chat with SEC 10-K Filings
13
 
14
+ FinChat is a **Retrieval-Augmented Generation (RAG)** chatbot that answers
15
+ questions about public companies using their **SEC 10-K annual filings**.
16
+ Ask *"What are AMD's main business risks?"* and FinChat finds the relevant
17
+ passages in the filings and answers — **grounded in the source, with
18
+ citations** — instead of making things up.
19
 
20
+ > Portfolio project · Retrieval-Augmented Generation over financial documents.
21
+
22
+ <!-- After deploying, add your live link here:
23
+ **🔗 Live demo:** https://huggingface.co/spaces/<your-username>/finchat -->
24
+
25
+ ---
26
+
27
+ ## ✨ Features
28
+
29
+ - **Grounded answers with citations** — every response is backed by excerpts
30
+ from real 10-K filings, shown in an expandable *Sources* panel.
31
+ - **Query routing ("knows where to look")** — FinChat detects which company a
32
+ question is about and searches *only* that company's filings via metadata
33
+ filtering, with graceful semantic fallback when the company is ambiguous.
34
+ - **Refuses to hallucinate** — if the answer isn't in the filings, it says so.
35
+ - **Benchmarked** — an LLM-as-judge evaluation scores answers against a
36
+ curated gold set (see [Evaluation](#-evaluation)).
37
+ - **100% free stack** — local embeddings + a free LLM API. No paid keys.
38
+
39
+ ---
40
+
41
+ ## 🏗️ Architecture
42
+
43
+ ```
44
+ INGESTION (once)
45
+ 10-K sentences ──► reassemble into sections ──► split into chunks
46
+ ──► embed (local model) ──► store vectors + metadata in ChromaDB
47
+
48
+ QUERY (per question)
49
+ question ──► detect company ──► embed ──► search (filtered by company)
50
+ ──► top-k chunks ──► LLM ──► grounded answer + citations
51
+ ```
52
+
53
+ | Layer | Tool |
54
+ |---------------|---------------------------------------------------|
55
+ | Orchestration | LangChain |
56
+ | Embeddings | `BAAI/bge-small-en-v1.5` (local, free) |
57
+ | Vector store | ChromaDB (persisted locally) |
58
+ | LLM | Llama 3.3 70B via Groq (free) |
59
+ | UI | Streamlit |
60
+ | Data | `JanosAudran/financial-reports-sec` (10-K text) |
61
+ | Evaluation | Curated gold set + LLM-as-judge |
62
+
63
+ **Companies in the demo corpus (2017–2020 filings):** AMD, Abbott (ABT),
64
+ Air Products (APD), AAR Corp (AIR), Matson (MATX). Swap them in
65
+ [`src/config.py`](src/config.py).
66
+
67
+ ---
68
+
69
+ ## 🚀 Setup
70
+
71
+ ```bash
72
+ # 1. Create & activate a virtual environment
73
+ python -m venv venv
74
+ venv\Scripts\activate # Windows
75
+ # source venv/bin/activate # macOS / Linux
76
+
77
+ # 2. Install dependencies
78
+ pip install -r requirements.txt
79
+
80
+ # 3. Add your Groq API key: copy .env.example to .env and paste your key
81
+ # Get a free key at https://console.groq.com
82
+ ```
83
+
84
+ ## 🛠️ Usage
85
+
86
+ ```bash
87
+ python -m src.ingest --list # see which companies are available
88
+ python -m src.ingest # build the vector store (run once)
89
+ streamlit run app.py # launch the chatbot
90
+ python -m eval.run_eval # run the evaluation
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 📊 Evaluation
96
+
97
+ FinChat is graded by an **LLM-as-judge** against a **12-question curated gold
98
+ set** whose reference answers are written from the 2017–2020 filings in the
99
+ corpus (see [`eval/gold_set.py`](eval/gold_set.py)). Full report:
100
+ [`eval/results.md`](eval/results.md).
101
+
102
+ | Metric | top-5 (baseline) | **top-8 (tuned)** |
103
+ |---|---|---|
104
+ | CORRECT / PARTIAL / INCORRECT | 7 / 4 / 1 | **10 / 2 / 0** |
105
+ | **Accuracy** (CORRECT=1, PARTIAL=0.5) | 75% | **92%** |
106
+
107
+ Increasing retrieval depth from **top-5 to top-8 chunks** lifted accuracy from
108
+ **75% → 92%** and eliminated the incorrect answer, with **no regressions** — the
109
+ earlier misses were passages that existed in the filings but fell just outside
110
+ the top-5.
111
+
112
+ **Error analysis (remaining misses)**
113
+ - ✅ Structural questions (segments, services, products, primary business) are
114
+ answered correctly across all companies.
115
+ - ⚠️ The 2 remaining PARTIALs are exhaustive-list / framing recall gaps: the
116
+ risk-factor answer omits "competition," and the Matson answer omits the
117
+ "China" expedited service — the relevant passage is worded differently from
118
+ the question.
119
+
120
+ *A corpus-aligned gold set was used because the dataset ends in 2020 while the
121
+ FinanceBench benchmark targets 2018–2023 filings, so direct benchmark overlap
122
+ is thin.*
123
+
124
+ ---
125
+
126
+ ## ☁️ Deployment
127
+
128
+ Deploy to Hugging Face Spaces (free) — see **[DEPLOY.md](DEPLOY.md)** for
129
+ step-by-step instructions. The app **self-builds** the vector store on first
130
+ load, so no prebuilt index is required.
131
+
132
+ ---
133
+
134
+ ## ⚠️ Limitations
135
+
136
+ - Answers are only as good as the retrieved passages; **numeric/table**
137
+ questions are the hardest part of financial RAG.
138
+ - The corpus is scoped to 5 companies / recent years to stay laptop-friendly.
139
+ - Not financial advice — a portfolio/educational project.
140
+
141
+ ---
142
+
143
+ ## 📁 Project structure
144
+
145
+ ```
146
+ .
147
+ ├── app.py # Streamlit chat UI
148
+ ├── src/
149
+ │ ├── config.py # all tunable settings
150
+ │ ├── ingest.py # build the vector store (load→section→chunk→embed→store)
151
+ │ └── rag.py # retrieval + generation + query routing
152
+ ├── eval/
153
+ │ ├── gold_set.py # curated questions + reference answers
154
+ │ ├── run_eval.py # LLM-as-judge evaluation harness
155
+ │ └── results.md # latest evaluation report
156
+ ├── .streamlit/config.toml # Streamlit settings
157
+ ├── requirements.txt
158
+ ├── .env.example # template for your API key
159
+ ├── DEPLOY.md # Hugging Face Spaces deploy guide
160
+ └── README.md
161
+ ```
app.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FinChat - Streamlit chat UI.
2
+
3
+ Run from the project root:
4
+ streamlit run app.py
5
+ """
6
+ import streamlit as st
7
+
8
+ from src.rag import answer, available_companies, ensure_index
9
+
10
+ st.set_page_config(
11
+ page_title="FinChat",
12
+ page_icon="💬",
13
+ layout="centered",
14
+ initial_sidebar_state="expanded",
15
+ )
16
+
17
+ st.title("💬 FinChat")
18
+ st.caption(
19
+ "Ask questions about companies' SEC 10-K filings. "
20
+ "Every answer is grounded in the filings, with sources you can inspect."
21
+ )
22
+
23
+ # On a fresh deployment (e.g. Hugging Face Spaces) the vector store won't exist
24
+ # yet -- build it once on first load. On later runs this is a fast no-op.
25
+ with st.spinner("Preparing the knowledge base (first run only, please wait)…"):
26
+ ensure_index()
27
+
28
+ # --- sidebar: which companies are available ---------------------------------
29
+ with st.sidebar:
30
+ st.header("📚 Companies loaded")
31
+ for ticker, name in available_companies():
32
+ st.markdown(f"- **{ticker}** — {name}")
33
+ st.caption("Source: SEC 10-K filings, 2017–2020.")
34
+
35
+ # --- chat history -----------------------------------------------------------
36
+ if "messages" not in st.session_state:
37
+ st.session_state.messages = []
38
+
39
+ for msg in st.session_state.messages:
40
+ with st.chat_message(msg["role"]):
41
+ st.markdown(msg["content"])
42
+
43
+ # --- new question -----------------------------------------------------------
44
+ if prompt := st.chat_input("e.g. What were AMD's main risk factors?"):
45
+ st.session_state.messages.append({"role": "user", "content": prompt})
46
+ with st.chat_message("user"):
47
+ st.markdown(prompt)
48
+
49
+ with st.chat_message("assistant"):
50
+ with st.spinner("Searching the filings..."):
51
+ result = answer(prompt)
52
+
53
+ st.markdown(result["answer"])
54
+
55
+ if result["routed_to"]:
56
+ st.caption(f"🔎 Routed retrieval to: **{result['routed_to']}**")
57
+
58
+ with st.expander(f"📄 Sources ({len(result['sources'])})"):
59
+ for i, doc in enumerate(result["sources"], 1):
60
+ st.markdown(f"**[{i}] {doc.metadata.get('source', '')}**")
61
+ st.write(doc.page_content[:500] + "…")
62
+
63
+ st.session_state.messages.append(
64
+ {"role": "assistant", "content": result["answer"]}
65
+ )
eval/__init__.py ADDED
File without changes
eval/gold_set.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Curated evaluation set for FinChat.
2
+
3
+ Each item is a question plus a concise REFERENCE answer written from the actual
4
+ 2017-2020 10-K filings in the corpus (verified against retrieved passages).
5
+ run_eval.py runs FinChat on each question and grades its answer against the
6
+ reference with an LLM-as-judge.
7
+
8
+ This exists because the corpus ends in 2020, while the FinanceBench benchmark
9
+ targets 2018-2023 filings -- so a corpus-aligned gold set gives a fair,
10
+ meaningful accuracy number.
11
+ """
12
+
13
+ GOLD_SET = [
14
+ {
15
+ "company": "AMD",
16
+ "question": "What products does AMD design and sell?",
17
+ "reference": (
18
+ "AMD is a global semiconductor company. Its products include x86 "
19
+ "microprocessors (CPUs), accelerated processing units (APUs) that "
20
+ "integrate CPUs with graphics, discrete graphics processing units "
21
+ "(GPUs), and semi-custom System-on-Chip (SoC) products. Brands "
22
+ "include Ryzen and Threadripper CPUs, EPYC server processors, and "
23
+ "Radeon graphics."
24
+ ),
25
+ },
26
+ {
27
+ "company": "AMD",
28
+ "question": "How are AMD's products manufactured?",
29
+ "reference": (
30
+ "AMD is a fabless semiconductor company and does not own the "
31
+ "foundries that make its chips. It relies on third-party foundries, "
32
+ "notably GlobalFoundries (with which it has a wafer supply "
33
+ "agreement) and TSMC, to manufacture its microprocessor, APU, and "
34
+ "GPU products."
35
+ ),
36
+ },
37
+ {
38
+ "company": "AMD",
39
+ "question": "What are some key risk factors AMD identifies?",
40
+ "reference": (
41
+ "Risks include intense competition (such as Intel in CPUs and "
42
+ "Nvidia in GPUs), dependence on third-party foundries like "
43
+ "GlobalFoundries and TSMC for manufacturing, the need to launch "
44
+ "competitive products on time, reliance on a limited number of "
45
+ "customers and third-party products, and general economic and "
46
+ "market conditions."
47
+ ),
48
+ },
49
+ {
50
+ "company": "AMD",
51
+ "question": "Does AMD make chips for game consoles or other semi-custom customers?",
52
+ "reference": (
53
+ "Yes. AMD designs and sells semi-custom SoC products, including "
54
+ "chips used in game consoles, and earns semi-custom revenue from "
55
+ "third-party customers."
56
+ ),
57
+ },
58
+ {
59
+ "company": "ABT",
60
+ "question": "What are Abbott's main business segments?",
61
+ "reference": (
62
+ "Abbott operates through four reportable segments: Established "
63
+ "Pharmaceutical Products, Diagnostic Products, Nutritional "
64
+ "Products, and Medical Devices."
65
+ ),
66
+ },
67
+ {
68
+ "company": "ABT",
69
+ "question": "What does Abbott's diagnostics business provide?",
70
+ "reference": (
71
+ "Abbott's diagnostics business provides in vitro diagnostic systems "
72
+ "and tests, including core laboratory diagnostics (immunoassay, "
73
+ "clinical chemistry, hematology, and blood screening), molecular "
74
+ "diagnostics, point-of-care, and rapid diagnostics, including its "
75
+ "Alinity family of instruments."
76
+ ),
77
+ },
78
+ {
79
+ "company": "APD",
80
+ "question": "What is Air Products' primary business?",
81
+ "reference": (
82
+ "Air Products is a world-leading industrial gases company. It "
83
+ "produces and sells atmospheric gases (such as oxygen, nitrogen, "
84
+ "and argon), process and specialty gases, related equipment, and "
85
+ "services."
86
+ ),
87
+ },
88
+ {
89
+ "company": "APD",
90
+ "question": "Which industries or end markets does Air Products serve?",
91
+ "reference": (
92
+ "Air Products serves customers globally across the energy, "
93
+ "electronics, chemicals, metals, and manufacturing markets."
94
+ ),
95
+ },
96
+ {
97
+ "company": "AIR",
98
+ "question": "What business segments does AAR Corp operate?",
99
+ "reference": (
100
+ "AAR operates two reportable segments: Aviation Services and "
101
+ "Expeditionary Services. It is a diversified provider of products "
102
+ "and services to the worldwide commercial aviation and government "
103
+ "and defense markets."
104
+ ),
105
+ },
106
+ {
107
+ "company": "AIR",
108
+ "question": "What services does AAR provide to the aviation industry?",
109
+ "reference": (
110
+ "AAR provides aftermarket aviation support: it sells and leases "
111
+ "new, overhauled, and repaired engine and airframe parts and "
112
+ "components; provides maintenance, repair and overhaul (MRO) and "
113
+ "component inventory and repair programs; and offers supply-chain "
114
+ "and expeditionary/airlift services to commercial and "
115
+ "government/defense customers."
116
+ ),
117
+ },
118
+ {
119
+ "company": "MATX",
120
+ "question": "What geographic markets does Matson's ocean transportation serve?",
121
+ "reference": (
122
+ "Matson's ocean transportation serves the domestic non-contiguous "
123
+ "U.S. economies of Hawaii, Alaska, and Guam, other island economies "
124
+ "in Micronesia, and provides an expedited service from China."
125
+ ),
126
+ },
127
+ {
128
+ "company": "MATX",
129
+ "question": "What does Matson's logistics business do?",
130
+ "reference": (
131
+ "Matson Logistics provides transportation brokerage, intermodal "
132
+ "rail and highway services, less-than-container-load consolidation, "
133
+ "freight forwarding, warehousing and distribution, and supply-chain "
134
+ "management services."
135
+ ),
136
+ },
137
+ ]
eval/results.md ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FinChat Evaluation — Curated Gold Set
2
+
3
+ FinChat is graded by an LLM-as-judge against reference answers written from the 2017-2020 10-K filings in the corpus.
4
+
5
+ - **Questions:** 12
6
+ - **CORRECT:** 10 **PARTIAL:** 2 **INCORRECT:** 0
7
+ - **Score:** 11.0 / 12
8
+ - **Accuracy (CORRECT=1.0, PARTIAL=0.5):** 92%
9
+
10
+ | # | Company | Verdict | Question |
11
+ |---|---------|---------|----------|
12
+ | 1 | AMD | CORRECT | What products does AMD design and sell? |
13
+ | 2 | AMD | CORRECT | How are AMD's products manufactured? |
14
+ | 3 | AMD | PARTIAL | What are some key risk factors AMD identifies? |
15
+ | 4 | AMD | CORRECT | Does AMD make chips for game consoles or other semi-custom customers? |
16
+ | 5 | ABT | CORRECT | What are Abbott's main business segments? |
17
+ | 6 | ABT | CORRECT | What does Abbott's diagnostics business provide? |
18
+ | 7 | APD | CORRECT | What is Air Products' primary business? |
19
+ | 8 | APD | CORRECT | Which industries or end markets does Air Products serve? |
20
+ | 9 | AIR | CORRECT | What business segments does AAR Corp operate? |
21
+ | 10 | AIR | CORRECT | What services does AAR provide to the aviation industry? |
22
+ | 11 | MATX | PARTIAL | What geographic markets does Matson's ocean transportation serve? |
23
+ | 12 | MATX | CORRECT | What does Matson's logistics business do? |
24
+
25
+ ---
26
+
27
+ ## Detailed results
28
+
29
+ ### 1. What products does AMD design and sell?
30
+ - **Company:** AMD | **Routed to:** AMD | **Verdict:** CORRECT
31
+ - **Judge:** The system answer accurately conveys the key products designed and sold by AMD, with minor omissions of specific brand names.
32
+ - **Reference:** AMD is a global semiconductor company. Its products include x86 microprocessors (CPUs), accelerated processing units (APUs) that integrate CPUs with graphics, discrete graphics processing units (GPUs), and semi-custom System-on-Chip (SoC) products. Brands include Ryzen and Threadripper CPUs, EPYC server processors, and Radeon graphics.
33
+ - **FinChat:** AMD designs and sells x86 microprocessors (CPUs), accelerated processing units (APUs), discrete graphics processing units (GPUs), semi-custom System-on-Chip (SOC) products, and chipsets for the PC, gaming, datacenter, and embedded markets.
34
+ Advanced Micro Devices Inc (2019, 2020)
35
+
36
+ Sources:
37
+ [2] ADVANCED MICRO DEVICES INC 10-K (2019) - Section 8
38
+ [6] ADVANCED MICRO DEVICES INC 10-K (2020) - Section 10
39
+
40
+ ### 2. How are AMD's products manufactured?
41
+ - **Company:** AMD | **Routed to:** AMD | **Verdict:** CORRECT
42
+ - **Judge:** The system answer conveys the key fact that AMD relies on third-party foundries, including GlobalFoundries, to manufacture its products.
43
+ - **Reference:** AMD is a fabless semiconductor company and does not own the foundries that make its chips. It relies on third-party foundries, notably GlobalFoundries (with which it has a wafer supply agreement) and TSMC, to manufacture its microprocessor, APU, and GPU products.
44
+ - **FinChat:** AMD relies on third parties to manufacture its products. The company has a wafer supply agreement with GF (GlobalFoundries) to purchase all of its microprocessor and APU product requirements, and a certain portion of its GPU product requirements, manufactured at process nodes larger than 7 nanometer (nm) from GF with limited exceptions.
45
+
46
+ Sources:
47
+ [1] ADVANCED MICRO DEVICES INC 10-K (2018) - Section 0
48
+ [2] ADVANCED MICRO DEVICES INC 10-K (2017) - Section 0
49
+ [3] ADVANCED MICRO DEVICES INC 10-K (2018) - Section 1
50
+
51
+ ### 3. What are some key risk factors AMD identifies?
52
+ - **Company:** AMD | **Routed to:** AMD | **Verdict:** PARTIAL
53
+ - **Judge:** The system answer captures some key risk factors, such as dependence on third-party manufacturers and the need to launch competitive products on time, but misses others like general economic and market conditions and reliance on a limited number of customers.
54
+ - **Reference:** Risks include intense competition (such as Intel in CPUs and Nvidia in GPUs), dependence on third-party foundries like GlobalFoundries and TSMC for manufacturing, the need to launch competitive products on time, reliance on a limited number of customers and third-party products, and general economic and market conditions.
55
+ - **FinChat:** AMD identifies several key risk factors, including:
56
+ 1. Intel Corporation's dominance of the microprocessor market and its aggressive business practices (ADVANCED MICRO DEVICES INC 10-K (2017) - Section 1, ADVANCED MICRO DEVICES INC 10-K (2018) - Section 1)
57
+ 2. Cyber-attacks, hacking, and phishing attacks (ADVANCED MICRO DEVICES INC 10-K (2018) - Section 0, ADVANCED MICRO DEVICES INC 10-K (2020) - Section 1)
58
+ 3. Failure to develop and introduce new products on a timely basis (ADVANCED MICRO DEVICES INC 10-K (2018) - Section 1)
59
+ 4. Dependence on third-party manufacturers and suppliers (ADVANCED MICRO DEVICES INC 10-K (2017) - Section 0)
60
+ 5. Loss of a significant customer (ADVANCED MICRO DEVICES INC 10-K (2018) - Section 0)
61
+
62
+ Sources:
63
+ [1] ADVANCED MICRO DEVICES INC 10-K (2017) - Section 1
64
+ [2] ADVANCED MICRO DEVICES INC 10-K (2018) - Section 0
65
+ [3] ADVANCED MICRO DEVICES INC 10-K (2018) - Section 1
66
+ [4] ADVANCED MICRO DEVICES INC 10-K (2020) - Section 1
67
+ [5] ADVANCED MICRO DEVICES INC 10-K (2017) - Section 0
68
+ [6] ADVANCED MICRO DEVICES INC 10-K (2018) - Section 0
69
+
70
+ ### 4. Does AMD make chips for game consoles or other semi-custom customers?
71
+ - **Company:** AMD | **Routed to:** AMD | **Verdict:** CORRECT
72
+ - **Judge:** The system answer conveys the key fact that AMD designs and sells semi-custom SoC products for game consoles, matching the reference answer.
73
+ - **Reference:** Yes. AMD designs and sells semi-custom SoC products, including chips used in game consoles, and earns semi-custom revenue from third-party customers.
74
+ - **FinChat:** Yes, AMD makes semi-custom System-on-Chip (SoC) products for game consoles. The company's success in this area is dependent on securing customers for its semi-custom design pipeline and consumer market conditions, including the success of the Sony PlayStation and Microsoft Xbox game console systems.
75
+ Sources:
76
+ [6] ADVANCED MICRO DEVICES INC 10-K (2018) - Section 1
77
+ [7] ADVANCED MICRO DEVICES INC 10-K (2017) - Section 8
78
+
79
+ ### 5. What are Abbott's main business segments?
80
+ - **Company:** ABT | **Routed to:** ABT | **Verdict:** CORRECT
81
+ - **Judge:** The system answer accurately lists Abbott's four main business segments, matching the reference answer.
82
+ - **Reference:** Abbott operates through four reportable segments: Established Pharmaceutical Products, Diagnostic Products, Nutritional Products, and Medical Devices.
83
+ - **FinChat:** Abbott's main business segments are:
84
+ 1. Established Pharmaceutical Products,
85
+ 2. Diagnostic Products,
86
+ 3. Nutritional Products, and
87
+ 4. Medical Devices (previously known as Cardiovascular and Neuromodulation Products).
88
+ Abbott Laboratories (2020)
89
+
90
+ Sources:
91
+ [1] ABBOTT LABORATORIES 10-K (2020) - Section 10
92
+ [3] ABBOTT LABORATORIES 10-K (2020) - Section 10
93
+ [5] ABBOTT LABORATORIES 10-K (2017) - Section 0
94
+ [6] ABBOTT LABORATORIES 10-K (2018) - Section 10
95
+
96
+ ### 6. What does Abbott's diagnostics business provide?
97
+ - **Company:** ABT | **Routed to:** ABT | **Verdict:** CORRECT
98
+ - **Judge:** The system answer conveys the key facts of Abbott's diagnostics business, including the types of diagnostic systems and tests provided, with only minor omissions.
99
+ - **Reference:** Abbott's diagnostics business provides in vitro diagnostic systems and tests, including core laboratory diagnostics (immunoassay, clinical chemistry, hematology, and blood screening), molecular diagnostics, point-of-care, and rapid diagnostics, including its Alinity family of instruments.
100
+ - **FinChat:** Abbott's diagnostics business provides various products, including:
101
+ - Core laboratory systems for immunoassay, clinical chemistry, hematology, and transfusion medicine,
102
+ - Molecular diagnostics systems,
103
+ - Rapid diagnostics products for cardiometabolic disease, infectious disease, and toxicology,
104
+ - Informatics and automation solutions to increase efficiency in laboratories.
105
+
106
+ Abbott Laboratories (2017, 2018, 2019, 2020)
107
+ Sources:
108
+ [1] ABBOTT LABORATORIES 10-K (2019) - Section 8
109
+ [3] ABBOTT LABORATORIES 10-K (2017) - Section 0
110
+ [6] ABBOTT LABORATORIES 10-K (2018) - Section 8
111
+ [8] ABBOTT LABORATORIES 10-K (2020) - Section 0
112
+
113
+ ### 7. What is Air Products' primary business?
114
+ - **Company:** APD | **Routed to:** None | **Verdict:** CORRECT
115
+ - **Judge:** The system answer accurately conveys Air Products' primary business, matching the key facts of the reference answer.
116
+ - **Reference:** Air Products is a world-leading industrial gases company. It produces and sells atmospheric gases (such as oxygen, nitrogen, and argon), process and specialty gases, related equipment, and services.
117
+ - **FinChat:** Air Products' primary business is as a world-leading Industrial Gases company, providing atmospheric, process, and specialty gases, and related equipment to various industries.
118
+ Air Products & Chemicals, Inc. (2018)
119
+
120
+ Sources:
121
+ [2] Air Products & Chemicals, Inc. 10-K (2018) - Section 8
122
+ [3] Air Products & Chemicals, Inc. 10-K (2018) - Section 0
123
+
124
+ ### 8. Which industries or end markets does Air Products serve?
125
+ - **Company:** APD | **Routed to:** None | **Verdict:** CORRECT
126
+ - **Judge:** The system answer conveys the key facts of the reference, listing similar industries and end markets served by Air Products, with minor additions of detail.
127
+ - **Reference:** Air Products serves customers globally across the energy, electronics, chemicals, metals, and manufacturing markets.
128
+ - **FinChat:** Air Products serves various industries, including:
129
+ - Refining
130
+ - Chemical
131
+ - Gasification
132
+ - Metals
133
+ - Electronics
134
+ - Manufacturing
135
+ - Food and beverage.
136
+
137
+ In later years, the company also serves energy, environment, and emerging markets.
138
+
139
+ Air Products & Chemicals, Inc. (2018, 2017, 2020)
140
+ Sources:
141
+ [1] Air Products & Chemicals, Inc. 10-K (2018) - Section 8
142
+ [2] Air Products & Chemicals, Inc. 10-K (2017) - Section 0
143
+ [6] Air Products & Chemicals, Inc. 10-K (2020) - Section 8
144
+
145
+ ### 9. What business segments does AAR Corp operate?
146
+ - **Company:** AIR | **Routed to:** None | **Verdict:** CORRECT
147
+ - **Judge:** The system answer accurately identifies the two business segments of AAR Corp.
148
+ - **Reference:** AAR operates two reportable segments: Aviation Services and Expeditionary Services. It is a diversified provider of products and services to the worldwide commercial aviation and government and defense markets.
149
+ - **FinChat:** AAR CORP operates two business segments: Aviation Services and Expeditionary Services.
150
+ Sources:
151
+ [3] AAR CORP 10-K (2018) - Section 0
152
+ [4] AAR CORP 10-K (2017) - Section 0
153
+
154
+ ### 10. What services does AAR provide to the aviation industry?
155
+ - **Company:** AIR | **Routed to:** None | **Verdict:** CORRECT
156
+ - **Judge:** The system answer comprehensively covers the key services provided by AAR to the aviation industry, matching the reference answer's scope.
157
+ - **Reference:** AAR provides aftermarket aviation support: it sells and leases new, overhauled, and repaired engine and airframe parts and components; provides maintenance, repair and overhaul (MRO) and component inventory and repair programs; and offers supply-chain and expeditionary/airlift services to commercial and government/defense customers.
158
+ - **FinChat:** AAR CORP provides various services to the aviation industry, including:
159
+ - Inventory management and distribution services
160
+ - Maintenance, repair and overhaul ("MRO")
161
+ - Engineering services
162
+ - Customized flight hour component inventory and repair programs
163
+ - Warranty claim management
164
+ - Outsourcing programs for engine and airframe parts and components
165
+ - Material planning, sourcing, logistics, information and program management, and parts and component repair and overhaul
166
+ - Aircraft modifications
167
+ - Aircraft and engine sales and leasing.
168
+
169
+ Sources:
170
+ [1] AAR CORP 10-K (2019) - Section 0
171
+ [5] AAR CORP 10-K (2019) - Section 0
172
+ [7] AAR CORP 10-K (2019) - Section 10
173
+ [8] AAR CORP 10-K (2020) - Section 0
174
+
175
+ ### 11. What geographic markets does Matson's ocean transportation serve?
176
+ - **Company:** MATX | **Routed to:** MATX | **Verdict:** PARTIAL
177
+ - **Judge:** The system answer includes the key markets mentioned in the reference, but also adds several unrelated or unsupported markets.
178
+ - **Reference:** Matson's ocean transportation serves the domestic non-contiguous U.S. economies of Hawaii, Alaska, and Guam, other island economies in Micronesia, and provides an expedited service from China.
179
+ - **FinChat:** Matson's Ocean Transportation segment serves the following geographic markets:
180
+ - Hawaii
181
+ - Alaska
182
+ - Guam
183
+ - Micronesia
184
+ - China
185
+ - South Pacific islands, including Fiji, Samoa, American Samoa, the Cook Islands, Tonga, Niue, Nauru, and the Solomon Islands
186
+ - U.S. West Coast, including California and Washington
187
+ - Australia (east coast ports)
188
+ - New Zealand
189
+ - North America (through logistics services).
190
+
191
+ Matson, Inc. (2019, 2020)
192
+
193
+ ### 12. What does Matson's logistics business do?
194
+ - **Company:** MATX | **Routed to:** MATX | **Verdict:** CORRECT
195
+ - **Judge:** The system answer comprehensively covers the key services provided by Matson's logistics business, matching the reference answer.
196
+ - **Reference:** Matson Logistics provides transportation brokerage, intermodal rail and highway services, less-than-container-load consolidation, freight forwarding, warehousing and distribution, and supply-chain management services.
197
+ - **FinChat:** Matson's Logistics business, conducted through Matson Logistics, Inc., provides a variety of logistics services to its customers, including:
198
+ 1. Multimodal transportation brokerage of domestic and international rail intermodal services, long-haul and regional highway trucking services, specialized hauling, flat-bed and project services, less-than-truckload services, and expedited freight services (collectively, “Transportation Brokerage” services);
199
+ 2. Less-than-container load (“LCL”) consolidation and freight forwarding services (collectively, “Freight Forwarding” services);
200
+ 3. Warehousing and distribution services; and
201
+ 4. Supply chain management, non-vessel operating common carrier (“NVOCC”) freight forwarding and other services.
202
+ Matson, Inc. (2020)
203
+
204
+ Sources:
205
+ [1] Matson, Inc. 10-K (2020) - Section 10
206
+ [3] Matson, Inc. 10-K (2020) - Section 0
207
+ [7] Matson, Inc. 10-K (2020) - Section 10
eval/run_eval.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluate FinChat on the curated gold set with an LLM-as-judge score.
2
+
3
+ For each question, FinChat produces an answer, then a separate LLM "judge"
4
+ grades that answer against the reference: CORRECT (1.0), PARTIAL (0.5), or
5
+ INCORRECT (0.0). Results and the overall accuracy are written to
6
+ eval/results.md.
7
+
8
+ Run from the project root:
9
+ python -m eval.run_eval
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+
18
+ # Make `src` and `eval` importable no matter how this is launched.
19
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
20
+
21
+ from langchain_core.prompts import ChatPromptTemplate
22
+
23
+ from src.rag import answer, get_llm
24
+ from eval.gold_set import GOLD_SET
25
+
26
+ JUDGE_PROMPT = ChatPromptTemplate.from_messages(
27
+ [
28
+ (
29
+ "system",
30
+ "You grade a financial question-answering system against a reference "
31
+ "answer.\n"
32
+ "Grade CORRECT if the system answer conveys the key facts of the "
33
+ "reference (minor omissions or extra detail are fine), PARTIAL if it "
34
+ "captures some but misses important parts, and INCORRECT if it is "
35
+ "wrong, empty, or unsupported.\n"
36
+ "Respond in EXACTLY this format:\n"
37
+ "VERDICT: <CORRECT|PARTIAL|INCORRECT>\n"
38
+ "REASON: <one short sentence>",
39
+ ),
40
+ (
41
+ "human",
42
+ "QUESTION:\n{question}\n\nREFERENCE ANSWER:\n{reference}\n\n"
43
+ "SYSTEM ANSWER:\n{system}",
44
+ ),
45
+ ]
46
+ )
47
+
48
+ SCORE = {"CORRECT": 1.0, "PARTIAL": 0.5, "INCORRECT": 0.0}
49
+
50
+
51
+ def judge(question: str, reference: str, system: str) -> tuple[str, str]:
52
+ text = (JUDGE_PROMPT | get_llm()).invoke(
53
+ {"question": question, "reference": reference, "system": system}
54
+ ).content
55
+ v = re.search(r"VERDICT:\s*(CORRECT|PARTIAL|INCORRECT)", text, re.I)
56
+ r = re.search(r"REASON:\s*(.+)", text, re.I)
57
+ verdict = v.group(1).upper() if v else "INCORRECT"
58
+ reason = r.group(1).strip() if r else "(unparsed)"
59
+ return verdict, reason
60
+
61
+
62
+ def main() -> None:
63
+ results = []
64
+ total = 0.0
65
+
66
+ for i, item in enumerate(GOLD_SET, 1):
67
+ res = answer(item["question"])
68
+ verdict, reason = judge(item["question"], item["reference"], res["answer"])
69
+ total += SCORE[verdict]
70
+ results.append((item, res, verdict, reason))
71
+ print(f"[{verdict:9}] {item['company']:5} {item['question']}")
72
+ time.sleep(1.0) # be gentle with the free-tier rate limit
73
+
74
+ n = len(GOLD_SET)
75
+ accuracy = total / n if n else 0.0
76
+ correct = sum(1 for _, _, v, _ in results if v == "CORRECT")
77
+ partial = sum(1 for _, _, v, _ in results if v == "PARTIAL")
78
+
79
+ lines = [
80
+ "# FinChat Evaluation — Curated Gold Set\n",
81
+ "FinChat is graded by an LLM-as-judge against reference answers written "
82
+ "from the 2017-2020 10-K filings in the corpus.\n",
83
+ f"- **Questions:** {n}",
84
+ f"- **CORRECT:** {correct} **PARTIAL:** {partial} "
85
+ f"**INCORRECT:** {n - correct - partial}",
86
+ f"- **Score:** {total:.1f} / {n}",
87
+ f"- **Accuracy (CORRECT=1.0, PARTIAL=0.5):** {accuracy:.0%}\n",
88
+ "| # | Company | Verdict | Question |",
89
+ "|---|---------|---------|----------|",
90
+ ]
91
+ for i, (item, _res, verdict, _reason) in enumerate(results, 1):
92
+ lines.append(f"| {i} | {item['company']} | {verdict} | {item['question']} |")
93
+
94
+ lines += ["\n---\n", "## Detailed results\n"]
95
+ for i, (item, res, verdict, reason) in enumerate(results, 1):
96
+ lines += [
97
+ f"### {i}. {item['question']}",
98
+ f"- **Company:** {item['company']} | "
99
+ f"**Routed to:** {res['routed_to']} | **Verdict:** {verdict}",
100
+ f"- **Judge:** {reason}",
101
+ f"- **Reference:** {item['reference']}",
102
+ f"- **FinChat:** {res['answer']}\n",
103
+ ]
104
+
105
+ out_path = Path(__file__).resolve().parent / "results.md"
106
+ out_path.write_text("\n".join(lines), encoding="utf-8")
107
+ print(f"\nAccuracy: {accuracy:.0%} ({total:.1f}/{n}) -> wrote {out_path}")
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
requirements.txt CHANGED
@@ -1,3 +1,27 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FinChat — dependencies
2
+ # Install with: pip install -r requirements.txt
3
+
4
+ # --- RAG orchestration ---
5
+ langchain>=0.3.0
6
+ langchain-community>=0.3.0
7
+ langchain-huggingface>=0.1.0 # local embedding models
8
+ langchain-groq>=0.2.0 # free, fast LLM inference
9
+ langchain-chroma>=0.1.4 # vector store integration
10
+
11
+ # --- Vector store ---
12
+ chromadb>=0.5.0
13
+
14
+ # --- Embedding model runtime (pulls in torch, CPU is fine) ---
15
+ sentence-transformers>=3.0.0
16
+
17
+ # --- Data loading ---
18
+ # Pinned <3.0: this dataset ships a loader script, and datasets>=3.0 removed
19
+ # support for script-based datasets.
20
+ datasets>=2.19.0,<3.0.0
21
+ pandas>=2.0.0
22
+
23
+ # --- Web UI ---
24
+ streamlit>=1.38.0
25
+
26
+ # --- Config / secrets ---
27
+ python-dotenv>=1.0.0
src/__init__.py ADDED
File without changes
src/config.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Central configuration for FinChat.
2
+
3
+ Everything you might want to tune lives here, so you don't have to hunt
4
+ through the code. Edit values, then re-run ingestion.
5
+ """
6
+ import os
7
+ from pathlib import Path
8
+
9
+ # --- Paths ------------------------------------------------------------------
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
11
+ DATA_DIR = PROJECT_ROOT / "data"
12
+
13
+ # Keep the vector store OUTSIDE OneDrive. OneDrive syncs files as they're
14
+ # written, which locks ChromaDB's SQLite database mid-write on Windows
15
+ # (PermissionError / possible corruption). The store is disposable -- it's
16
+ # rebuilt by ingest.py -- so its location doesn't matter to the repo.
17
+ # Override with the FINCHAT_VECTORSTORE env var (e.g. on Hugging Face Spaces).
18
+ VECTORSTORE_DIR = Path(
19
+ os.getenv("FINCHAT_VECTORSTORE", str(Path.home() / ".finchat" / "vectorstore"))
20
+ )
21
+
22
+ # --- Dataset (Hugging Face) -------------------------------------------------
23
+ # Each row is ONE sentence from a 10-K filing. ingest.py reassembles the
24
+ # sentences into section text before chunking. "small_full" (~240k sentences)
25
+ # keeps the download light enough for a laptop.
26
+ HF_DATASET = "JanosAudran/financial-reports-sec"
27
+ HF_CONFIG = "small_full"
28
+ HF_SPLIT = "train"
29
+
30
+ # --- Corpus scope -----------------------------------------------------------
31
+ # Which companies to ingest. Leave TARGET_TICKERS = [] to auto-pick the
32
+ # TOP_N_COMPANIES with the most content (handy before you know what's in the
33
+ # data -- run `python -m src.ingest --list` to see the options).
34
+ # Recognizable companies available in the "small_full" config, recent years.
35
+ # Swap freely: also available -> CECE, BKTI, ACU, AE, WDDD (all end at 2020).
36
+ TARGET_TICKERS: list[str] = ["AMD", "ABT", "APD", "AIR", "MATX"]
37
+ TARGET_YEARS: list[int] = [2017, 2018, 2019, 2020]
38
+ TOP_N_COMPANIES = 8 # used only when TARGET_TICKERS is empty
39
+
40
+ # --- Chunking ---------------------------------------------------------------
41
+ CHUNK_SIZE = 900 # characters per chunk
42
+ CHUNK_OVERLAP = 150 # overlap keeps sentences from being cut off
43
+
44
+ # --- Models -----------------------------------------------------------------
45
+ EMBEDDING_MODEL = "BAAI/bge-small-en-v1.5" # local, free, ~130 MB on first run
46
+ LLM_MODEL = "llama-3.3-70b-versatile" # Groq free tier
47
+ LLM_TEMPERATURE = 0.0 # 0 = factual, deterministic
48
+
49
+ # --- Retrieval --------------------------------------------------------------
50
+ TOP_K = 8 # how many chunks to feed the LLM
51
+
52
+ # --- Vector store -----------------------------------------------------------
53
+ CHROMA_COLLECTION = "finchat_10k"
src/ingest.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build the FinChat vector store from SEC 10-K filings.
2
+
3
+ Pipeline:
4
+ load sentences -> group into section text -> split into chunks
5
+ -> embed locally -> store in Chroma (persisted to ./vectorstore)
6
+
7
+ Run it once (from the project root):
8
+ python -m src.ingest # build the vector store
9
+ python -m src.ingest --list # just list available companies, then exit
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import re
15
+ import shutil
16
+ import sys
17
+ import time
18
+ from pathlib import Path
19
+
20
+ # Allow running as either `python -m src.ingest` or `python src/ingest.py`.
21
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
22
+
23
+ import pandas as pd
24
+ from datasets import load_dataset
25
+ from langchain_core.documents import Document
26
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
27
+ from langchain_huggingface import HuggingFaceEmbeddings
28
+ from langchain_chroma import Chroma
29
+
30
+ from src import config
31
+
32
+
33
+ def pretty_section(label: str) -> str:
34
+ """Turn a raw section label into a readable 'Section N' for display."""
35
+ nums = re.findall(r"\d+", str(label))
36
+ if nums:
37
+ return f"Section {nums[-1]}"
38
+ return str(label).replace("_", " ").strip().title() or "Filing"
39
+
40
+
41
+ def load_dataframe() -> pd.DataFrame:
42
+ """Download the dataset split and return it as a tidy DataFrame."""
43
+ print(f"Loading {config.HF_DATASET} [{config.HF_CONFIG}] ... (first run downloads it)")
44
+ ds = load_dataset(
45
+ config.HF_DATASET, config.HF_CONFIG, split=config.HF_SPLIT,
46
+ trust_remote_code=True,
47
+ )
48
+ df = ds.to_pandas()
49
+
50
+ # `tickers` is a list per row -> take the first as the primary ticker.
51
+ df["ticker"] = df["tickers"].apply(
52
+ lambda t: t[0] if hasattr(t, "__len__") and len(t) else None
53
+ )
54
+ # reportDate looks like "2020-09-26"; the year is the first 4 chars.
55
+ df["year"] = df["reportDate"].astype(str).str.slice(0, 4)
56
+ return df
57
+
58
+
59
+ def list_companies(df: pd.DataFrame, top: int = 30) -> None:
60
+ counts = (
61
+ df.dropna(subset=["ticker"])
62
+ .groupby(["ticker", "name"])
63
+ .size()
64
+ .sort_values(ascending=False)
65
+ .head(top)
66
+ )
67
+ print("\nTop companies available (ticker | name | #sentences):")
68
+ for (ticker, name), n in counts.items():
69
+ print(f" {ticker:<8} {str(name):<42} {n}")
70
+ print("\nCopy the tickers you want into TARGET_TICKERS in src/config.py.")
71
+
72
+
73
+ def select_rows(df: pd.DataFrame) -> pd.DataFrame:
74
+ df = df.dropna(subset=["ticker"]).copy()
75
+
76
+ if config.TARGET_YEARS:
77
+ years = {str(y) for y in config.TARGET_YEARS}
78
+ df = df[df["year"].isin(years)]
79
+
80
+ if config.TARGET_TICKERS:
81
+ wanted = {t.upper() for t in config.TARGET_TICKERS}
82
+ df = df[df["ticker"].str.upper().isin(wanted)]
83
+ else:
84
+ # No explicit list -> keep the TOP_N companies by content volume.
85
+ top = (
86
+ df.groupby("ticker").size()
87
+ .sort_values(ascending=False)
88
+ .head(config.TOP_N_COMPANIES)
89
+ .index
90
+ )
91
+ df = df[df["ticker"].isin(top)]
92
+
93
+ return df
94
+
95
+
96
+ def build_documents(df: pd.DataFrame) -> list[Document]:
97
+ """Reassemble sentences into section text, then split into chunks."""
98
+ # One "raw document" per (filing, section). docID identifies one filing.
99
+ grouped = df.sort_values("sentenceCount").groupby(["docID", "section"])
100
+
101
+ raw_docs: list[Document] = []
102
+ for (doc_id, section), rows in grouped:
103
+ text = " ".join(str(s) for s in rows["sentence"].tolist()).strip()
104
+ if len(text) < 50: # skip near-empty sections
105
+ continue
106
+ head = rows.iloc[0]
107
+ company = str(head["name"])
108
+ year = str(head["year"])
109
+ raw_docs.append(
110
+ Document(
111
+ page_content=text,
112
+ metadata={
113
+ "ticker": str(head["ticker"]),
114
+ "company": company,
115
+ "year": year,
116
+ "section": str(section),
117
+ "cik": str(head["cik"]),
118
+ "docID": str(doc_id),
119
+ "source": f"{company} 10-K ({year}) - {pretty_section(section)}",
120
+ },
121
+ )
122
+ )
123
+
124
+ splitter = RecursiveCharacterTextSplitter(
125
+ chunk_size=config.CHUNK_SIZE,
126
+ chunk_overlap=config.CHUNK_OVERLAP,
127
+ )
128
+ chunks = splitter.split_documents(raw_docs)
129
+ print(f"Reassembled {len(raw_docs)} sections -> {len(chunks)} chunks.")
130
+ return chunks
131
+
132
+
133
+ def _safe_rmtree(path: Path, retries: int = 3) -> None:
134
+ """Delete a directory, retrying briefly through transient file locks."""
135
+ for _ in range(retries):
136
+ try:
137
+ shutil.rmtree(path)
138
+ return
139
+ except (PermissionError, OSError):
140
+ time.sleep(1.0)
141
+ shutil.rmtree(path) # last try -- let the error surface if it still fails
142
+
143
+
144
+ def build_vectorstore(chunks: list[Document]) -> None:
145
+ if config.VECTORSTORE_DIR.exists():
146
+ print("Removing existing vector store ...")
147
+ _safe_rmtree(config.VECTORSTORE_DIR)
148
+ config.VECTORSTORE_DIR.parent.mkdir(parents=True, exist_ok=True)
149
+
150
+ print(f"Loading embedding model {config.EMBEDDING_MODEL} ...")
151
+ embeddings = HuggingFaceEmbeddings(model_name=config.EMBEDDING_MODEL)
152
+
153
+ print("Embedding & storing chunks (this can take a few minutes) ...")
154
+ Chroma.from_documents(
155
+ documents=chunks,
156
+ embedding=embeddings,
157
+ collection_name=config.CHROMA_COLLECTION,
158
+ persist_directory=str(config.VECTORSTORE_DIR),
159
+ )
160
+ print(f"Done. Vector store saved to: {config.VECTORSTORE_DIR}")
161
+
162
+
163
+ def build_index() -> None:
164
+ """Run the full ingestion pipeline: load -> select -> chunk -> embed -> store.
165
+
166
+ Importable so the app can bootstrap the vector store on first run
167
+ (e.g. on a fresh Hugging Face Space).
168
+ """
169
+ df = load_dataframe()
170
+ selected = select_rows(df)
171
+ if selected.empty:
172
+ raise SystemExit(
173
+ "\nNo rows matched TARGET_TICKERS / TARGET_YEARS in src/config.py.\n"
174
+ "Run `python -m src.ingest --list` to see what's available."
175
+ )
176
+ companies = sorted(selected["ticker"].unique())
177
+ print(f"Ingesting {len(companies)} companies: {', '.join(companies)}")
178
+ chunks = build_documents(selected)
179
+ build_vectorstore(chunks)
180
+
181
+
182
+ def main() -> None:
183
+ parser = argparse.ArgumentParser(description="Build the FinChat vector store.")
184
+ parser.add_argument("--list", action="store_true",
185
+ help="List available companies and exit.")
186
+ args = parser.parse_args()
187
+
188
+ if args.list:
189
+ list_companies(load_dataframe())
190
+ return
191
+
192
+ build_index()
193
+
194
+
195
+ if __name__ == "__main__":
196
+ main()
src/rag.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FinChat retrieval-augmented generation (RAG) chain.
2
+
3
+ Public entry point: answer(question) -> {"answer", "routed_to", "sources"}
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import re
9
+ from collections import defaultdict
10
+ from functools import lru_cache
11
+
12
+ from dotenv import load_dotenv
13
+ from langchain_chroma import Chroma
14
+ from langchain_huggingface import HuggingFaceEmbeddings
15
+ from langchain_groq import ChatGroq
16
+ from langchain_core.prompts import ChatPromptTemplate
17
+
18
+ from src import config
19
+
20
+ # Load GROQ_API_KEY from the project's .env by explicit path (robust no matter
21
+ # where the process is launched). On Hugging Face Spaces there is no .env and
22
+ # this is a harmless no-op -- the key comes from Space secrets instead.
23
+ load_dotenv(config.PROJECT_ROOT / ".env")
24
+
25
+ SYSTEM_PROMPT = """You are FinChat, a financial analyst assistant. Answer the \
26
+ question using ONLY the excerpts from SEC 10-K filings provided below.
27
+
28
+ Rules:
29
+ - Use ONLY the provided context. Do NOT rely on outside knowledge.
30
+ - If the answer is not in the context, reply exactly:
31
+ "I couldn't find that in the filings I have."
32
+ - Be concise and precise with numbers. State the company and fiscal year.
33
+ - End with a short "Sources:" list referencing the excerpts you used.
34
+
35
+ Context:
36
+ {context}
37
+ """
38
+
39
+ PROMPT = ChatPromptTemplate.from_messages(
40
+ [("system", SYSTEM_PROMPT), ("human", "{question}")]
41
+ )
42
+
43
+
44
+ @lru_cache(maxsize=1)
45
+ def get_vectorstore() -> Chroma:
46
+ embeddings = HuggingFaceEmbeddings(model_name=config.EMBEDDING_MODEL)
47
+ return Chroma(
48
+ collection_name=config.CHROMA_COLLECTION,
49
+ embedding_function=embeddings,
50
+ persist_directory=str(config.VECTORSTORE_DIR),
51
+ )
52
+
53
+
54
+ def ensure_index() -> None:
55
+ """Build the vector store on first run if it's empty or missing.
56
+
57
+ Lets the app bootstrap itself on a fresh deployment (e.g. Hugging Face
58
+ Spaces). On normal runs where the store already exists, this is a fast
59
+ no-op.
60
+ """
61
+ try:
62
+ has_docs = len(get_vectorstore().get(limit=1).get("ids", [])) > 0
63
+ except Exception:
64
+ has_docs = False
65
+ if not has_docs:
66
+ get_vectorstore.cache_clear() # release the empty store handle
67
+ from src.ingest import build_index
68
+ build_index()
69
+ get_vectorstore.cache_clear() # reopen the freshly built store
70
+
71
+
72
+ @lru_cache(maxsize=1)
73
+ def get_llm() -> ChatGroq:
74
+ if not os.getenv("GROQ_API_KEY"):
75
+ raise RuntimeError("GROQ_API_KEY is not set. Add it to your .env file.")
76
+ return ChatGroq(
77
+ model=config.LLM_MODEL,
78
+ temperature=config.LLM_TEMPERATURE,
79
+ max_retries=5, # back off through Groq free-tier rate limits
80
+ )
81
+
82
+
83
+ # Words that shouldn't count as a company alias on their own.
84
+ _STOPWORDS = {
85
+ "inc", "corp", "corporation", "company", "ltd", "llc", "plc", "the",
86
+ "and", "group", "holdings", "international", "industries", "products",
87
+ "resources", "energy", "technologies", "systems",
88
+ }
89
+
90
+
91
+ @lru_cache(maxsize=1)
92
+ def company_aliases() -> dict[str, str]:
93
+ """Map each recognizable alias (ticker or distinctive name word) -> ticker.
94
+
95
+ This lets FinChat route a question to the right company before retrieving
96
+ ("knows exactly where to look"). Any alias shared by more than one company
97
+ is dropped, so we never route to the wrong filing.
98
+ """
99
+ store = get_vectorstore()
100
+ metadatas = store.get(include=["metadatas"]).get("metadatas", [])
101
+
102
+ ticker_to_name: dict[str, str] = {}
103
+ for m in metadatas:
104
+ ticker = (m.get("ticker") or "").upper()
105
+ if ticker:
106
+ ticker_to_name[ticker] = m.get("company") or ""
107
+
108
+ alias_to_tickers: dict[str, set] = defaultdict(set)
109
+ for ticker, name in ticker_to_name.items():
110
+ alias_to_tickers[ticker.lower()].add(ticker) # the ticker itself
111
+ for word in re.findall(r"[a-z]+", name.lower()):
112
+ if len(word) >= 4 and word not in _STOPWORDS:
113
+ alias_to_tickers[word].add(ticker)
114
+
115
+ # Keep only unambiguous aliases (mapping to exactly one company).
116
+ return {a: next(iter(ts)) for a, ts in alias_to_tickers.items() if len(ts) == 1}
117
+
118
+
119
+ @lru_cache(maxsize=1)
120
+ def available_companies() -> list[tuple[str, str]]:
121
+ """Return sorted (ticker, company name) pairs present in the vector store."""
122
+ store = get_vectorstore()
123
+ metadatas = store.get(include=["metadatas"]).get("metadatas", [])
124
+ seen: dict[str, str] = {}
125
+ for m in metadatas:
126
+ ticker = (m.get("ticker") or "").upper()
127
+ if ticker and ticker not in seen:
128
+ seen[ticker] = m.get("company") or ticker
129
+ return sorted(seen.items())
130
+
131
+
132
+ def detect_ticker(question: str) -> str | None:
133
+ """Figure out which company the question is about."""
134
+ aliases = company_aliases()
135
+
136
+ # 1) An explicit ticker written in capitals, e.g. "AMD" or "ABT".
137
+ for token in re.findall(r"\b[A-Z]{2,6}\b", question):
138
+ if token.lower() in aliases:
139
+ return aliases[token.lower()]
140
+
141
+ # 2) A distinctive company-name word, e.g. "abbott" or "matson".
142
+ for word in re.findall(r"[a-z]+", question.lower()):
143
+ if len(word) >= 4 and word in aliases:
144
+ return aliases[word]
145
+
146
+ return None
147
+
148
+
149
+ def retrieve(question: str, ticker: str | None):
150
+ store = get_vectorstore()
151
+ search_kwargs: dict = {"k": config.TOP_K}
152
+ if ticker:
153
+ # Metadata filter = search ONLY that company's filings.
154
+ search_kwargs["filter"] = {"ticker": ticker}
155
+ return store.as_retriever(search_kwargs=search_kwargs).invoke(question)
156
+
157
+
158
+ def format_context(docs) -> str:
159
+ return "\n\n".join(
160
+ f"[{i}] {d.metadata.get('source', 'source')}\n{d.page_content}"
161
+ for i, d in enumerate(docs, 1)
162
+ )
163
+
164
+
165
+ def answer(question: str) -> dict:
166
+ """Route -> retrieve -> generate. Returns answer, routing info, sources."""
167
+ ticker = detect_ticker(question)
168
+ docs = retrieve(question, ticker)
169
+ context = format_context(docs) if docs else "(no relevant excerpts found)"
170
+ chain = PROMPT | get_llm()
171
+ response = chain.invoke({"context": context, "question": question})
172
+ return {"answer": response.content, "routed_to": ticker, "sources": docs}