finance-copilot 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.
- finance_copilot-0.1.0/.gitignore +24 -0
- finance_copilot-0.1.0/PKG-INFO +291 -0
- finance_copilot-0.1.0/README.md +258 -0
- finance_copilot-0.1.0/deploy/demo/README.md +34 -0
- finance_copilot-0.1.0/finance_copilot/__init__.py +1 -0
- finance_copilot-0.1.0/finance_copilot/__main__.py +58 -0
- finance_copilot-0.1.0/finance_copilot/api/__init__.py +1 -0
- finance_copilot-0.1.0/finance_copilot/api/deps.py +16 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/__init__.py +1 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/annotations.py +712 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/categories.py +74 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/config.py +126 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/data.py +234 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/embeddings.py +157 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/groups.py +138 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/insights.py +19 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/local_llm.py +134 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/people.py +80 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/rules.py +103 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/statements.py +196 -0
- finance_copilot-0.1.0/finance_copilot/api/routes/transactions.py +163 -0
- finance_copilot-0.1.0/finance_copilot/app.py +137 -0
- finance_copilot-0.1.0/finance_copilot/cli.py +101 -0
- finance_copilot-0.1.0/finance_copilot/config.py +208 -0
- finance_copilot-0.1.0/finance_copilot/db/__init__.py +1 -0
- finance_copilot-0.1.0/finance_copilot/db/connection.py +145 -0
- finance_copilot-0.1.0/finance_copilot/db/lifecycle.py +55 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/001_initial.sql +57 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/002_links.sql +23 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/003_people.sql +13 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/004_member_type.sql +6 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/005_drop_group_type.sql +4 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/006_seed_categories.sql +122 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/007_expand_annotation_source.sql +21 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/008_feedback_stats.sql +12 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/009_add_reimbursement_category.sql +3 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/010_provenance_dedup_indexes.sql +14 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/011_annotation_jobs.sql +14 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/012_statement_period.sql +9 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/013_category_ids.sql +24 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/014_json_list_columns.sql +29 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/015_annotation_reasoning.sql +6 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/016_app_settings.sql +10 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/017_counterparty_key.sql +26 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/018_self_transfers_category.sql +22 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/019_annotation_source_learned_rule_rag_knn.sql +35 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/020_person_relationship_and_learned_rule_suppressions.sql +27 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/021_statement_balances.sql +58 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/022_statement_account_ref.sql +9 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/023_transaction_links.sql +16 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/024_annotation_review_snooze.sql +11 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/025_job_kind.sql +5 -0
- finance_copilot-0.1.0/finance_copilot/db/migrations/026_user_rules.sql +56 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/__init__.py +1 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/annotations.py +159 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/app_settings.py +154 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/categories.py +144 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/common.py +28 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/embeddings.py +206 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/feedback_stats.py +88 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/groups.py +237 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/insights.py +772 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/learned_rules.py +218 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/people.py +179 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/transactions.py +298 -0
- finance_copilot-0.1.0/finance_copilot/db/queries/user_rules.py +253 -0
- finance_copilot-0.1.0/finance_copilot/demo_seed.py +560 -0
- finance_copilot-0.1.0/finance_copilot/local_llm.py +356 -0
- finance_copilot-0.1.0/finance_copilot/main.py +254 -0
- finance_copilot-0.1.0/finance_copilot/models/__init__.py +1 -0
- finance_copilot-0.1.0/finance_copilot/models/annotation.py +132 -0
- finance_copilot-0.1.0/finance_copilot/models/transaction.py +48 -0
- finance_copilot-0.1.0/finance_copilot/parsers/__init__.py +1 -0
- finance_copilot-0.1.0/finance_copilot/parsers/banks/__init__.py +1 -0
- finance_copilot-0.1.0/finance_copilot/parsers/banks/kotak.py +96 -0
- finance_copilot-0.1.0/finance_copilot/parsers/base.py +46 -0
- finance_copilot-0.1.0/finance_copilot/parsers/generic.py +401 -0
- finance_copilot-0.1.0/finance_copilot/parsers/registry.py +32 -0
- finance_copilot-0.1.0/finance_copilot/parsers/upi.py +42 -0
- finance_copilot-0.1.0/finance_copilot/pipeline/__init__.py +1 -0
- finance_copilot-0.1.0/finance_copilot/pipeline/annotate.py +1036 -0
- finance_copilot-0.1.0/finance_copilot/pipeline/calibration.py +58 -0
- finance_copilot-0.1.0/finance_copilot/pipeline/counterparty.py +201 -0
- finance_copilot-0.1.0/finance_copilot/pipeline/embed.py +299 -0
- finance_copilot-0.1.0/finance_copilot/pipeline/ingest.py +236 -0
- finance_copilot-0.1.0/finance_copilot/pipeline/llm.py +568 -0
- finance_copilot-0.1.0/finance_copilot/pipeline/rules.py +234 -0
- finance_copilot-0.1.0/finance_copilot/pipeline/run_summary.py +192 -0
- finance_copilot-0.1.0/finance_copilot/pipeline/sources.py +33 -0
- finance_copilot-0.1.0/hatch_build.py +28 -0
- finance_copilot-0.1.0/pyproject.toml +107 -0
- finance_copilot-0.1.0/ui/dist/assets/CategoryPicker-Cm0NSo75.js +1 -0
- finance_copilot-0.1.0/ui/dist/assets/ConfirmDialog-OLzxIURy.js +1 -0
- finance_copilot-0.1.0/ui/dist/assets/Dashboard-BSVQzNdb.js +21 -0
- finance_copilot-0.1.0/ui/dist/assets/EmptyState-DWGf9hQV.js +1 -0
- finance_copilot-0.1.0/ui/dist/assets/Groups-CxbdIaHH.js +1 -0
- finance_copilot-0.1.0/ui/dist/assets/Insights-CAcvj1Wu.js +1 -0
- finance_copilot-0.1.0/ui/dist/assets/People-C9IyajLX.js +1 -0
- finance_copilot-0.1.0/ui/dist/assets/PeriodPicker-oXvMQJaq.js +1 -0
- finance_copilot-0.1.0/ui/dist/assets/ReasoningPanel-CCk-75_X.js +6 -0
- finance_copilot-0.1.0/ui/dist/assets/ReviewQueue-DBBU47Q9.js +26 -0
- finance_copilot-0.1.0/ui/dist/assets/Settings-DHQMZoY-.js +56 -0
- finance_copilot-0.1.0/ui/dist/assets/Transactions-X_Kspt5O.js +1 -0
- finance_copilot-0.1.0/ui/dist/assets/Upload-PHdAA6CH.js +21 -0
- finance_copilot-0.1.0/ui/dist/assets/chevron-down-BOTn-jA0.js +6 -0
- finance_copilot-0.1.0/ui/dist/assets/chevron-right-1ZTDEAuV.js +6 -0
- finance_copilot-0.1.0/ui/dist/assets/circle-check-DVy3w3Qy.js +6 -0
- finance_copilot-0.1.0/ui/dist/assets/circle-help-B0D8uify.js +6 -0
- finance_copilot-0.1.0/ui/dist/assets/dates-DENdhcfJ.js +1 -0
- finance_copilot-0.1.0/ui/dist/assets/index-B86kwi0r.css +1 -0
- finance_copilot-0.1.0/ui/dist/assets/index-CttMkFOe.js +175 -0
- finance_copilot-0.1.0/ui/dist/assets/index-D94rVQ8L.js +18 -0
- finance_copilot-0.1.0/ui/dist/assets/pencil-BdwggxyK.js +11 -0
- finance_copilot-0.1.0/ui/dist/assets/plus-DTgrI-ia.js +6 -0
- finance_copilot-0.1.0/ui/dist/assets/refresh-cw-BYXFldMU.js +6 -0
- finance_copilot-0.1.0/ui/dist/assets/trash-2-Oxw5TKtt.js +6 -0
- finance_copilot-0.1.0/ui/dist/assets/zap-C-11zHGY.js +6 -0
- finance_copilot-0.1.0/ui/dist/index.html +13 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: finance-copilot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first personal finance tool: parses bank statement PDFs and auto-categorizes transactions with rules, RAG, and a local LLM.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Abhilash-3107/fin-copilot
|
|
6
|
+
Project-URL: Source, https://github.com/Abhilash-3107/fin-copilot
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: bank-statements,finance,local-first,personal-finance
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: cryptography>=43
|
|
17
|
+
Requires-Dist: fastapi>=0.115
|
|
18
|
+
Requires-Dist: fastembed>=0.4; python_version < '3.15'
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: pdfplumber>=0.11
|
|
21
|
+
Requires-Dist: platformdirs>=4
|
|
22
|
+
Requires-Dist: pydantic-settings>=2.3
|
|
23
|
+
Requires-Dist: pydantic>=2.7
|
|
24
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
25
|
+
Requires-Dist: python-ulid>=2.6
|
|
26
|
+
Requires-Dist: rich>=13
|
|
27
|
+
Requires-Dist: sqlite-vec>=0.1.6
|
|
28
|
+
Requires-Dist: typer>=0.12
|
|
29
|
+
Requires-Dist: uvicorn>=0.30
|
|
30
|
+
Provides-Extra: local-llm
|
|
31
|
+
Requires-Dist: llama-cpp-python>=0.3; extra == 'local-llm'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Finance Copilot
|
|
35
|
+
|
|
36
|
+
Finance Copilot is a local-first personal finance application for parsing bank statements, organising transactions and analysing expenses.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uvx finance-copilot
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
That's it.
|
|
43
|
+
The app installs, starts on localhost, and opens in your browser.
|
|
44
|
+
Drop in a bank statement PDF (or click "Load demo data") and watch every transaction get parsed, balance-checked, and categorized.
|
|
45
|
+
No account, no cloud, no data leaving your computer.
|
|
46
|
+
|
|
47
|
+
> Requires [uv](https://docs.astral.sh/uv/getting-started/installation/) (one installer command, no Python needed). Prefer pipx? `pipx run finance-copilot` works too. First run takes a minute while dependencies download.
|
|
48
|
+
>
|
|
49
|
+
> On Python 3.15 the app runs with built-in embeddings off, because their runtime has no 3.15 build yet. For the full experience run `uvx --python 3.14 finance-copilot`.
|
|
50
|
+
|
|
51
|
+
Out of the box you get statement parsing with running-balance verification, rule-based and history-based categorization, expense groups, people tracking, and insights.
|
|
52
|
+
Add AI when you want it: point Settings › AI & Model at [Ollama](https://ollama.com) or paste any OpenAI-compatible API key.
|
|
53
|
+
|
|
54
|
+
## Highlights
|
|
55
|
+
|
|
56
|
+
- **100% local** - SQLite database + Ollama for inference, everything stays on your machine
|
|
57
|
+
- **Multi-stage annotation pipeline** - Your rules > built-in rules > learned merchants > RAG Direct > RAG Prompted > Plain LLM, with graceful fallbacks
|
|
58
|
+
- **Your own rules** - Settings > Rules: a word or phrase and a category, with a live preview of what it matches before you save
|
|
59
|
+
- **Human-in-the-loop** - Low-confidence predictions surface in an intuitive and fast review queue for correction
|
|
60
|
+
- **Bank statement parsing** - Dedicated Kotak parser plus a generic parser that handles most banks' tabular statement PDFs, with every row verified against the running balance; UPI metadata extracted automatically
|
|
61
|
+
- **Vector similarity search** - sqlite-vec powers few-shot retrieval for better categorization over time
|
|
62
|
+
- **React dashboard** - Transaction management, annotation review, expense groups, and insights/charts
|
|
63
|
+
|
|
64
|
+
## How the Pipeline Works
|
|
65
|
+
|
|
66
|
+
When you trigger auto-annotation, each transaction flows through four stages. The first match wins:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Transaction
|
|
70
|
+
|
|
|
71
|
+
v
|
|
72
|
+
[Stage 1: Rules] - Known-person match (UPI handle from people table),
|
|
73
|
+
| confidence: 0.95 then merchant keywords & UPI notes (~70 built-in rules)
|
|
74
|
+
| no match? ↓
|
|
75
|
+
v
|
|
76
|
+
[Stage 2: RAG Direct] - Find top-5 similar annotated transactions via embeddings
|
|
77
|
+
| confidence: dynamic If best match similarity >= 0.92 and donor is trusted,
|
|
78
|
+
| copy annotation (cosine × agreement × margin factors)
|
|
79
|
+
| no match? ↓
|
|
80
|
+
v
|
|
81
|
+
[Stage 3: RAG Prompted] - Few-shot LLM call with retrieved examples as context
|
|
82
|
+
| confidence: dynamic llm_conf × calibrated dampening (base 0.92, adapts with feedback)
|
|
83
|
+
| no match? ↓
|
|
84
|
+
v
|
|
85
|
+
[Stage 4: Plain LLM] - Cold LLM call without examples
|
|
86
|
+
| confidence: dynamic llm_conf × calibrated dampening (base 0.85, adapts with feedback)
|
|
87
|
+
v
|
|
88
|
+
Annotation saved → below threshold (0.85)? → Review Queue
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Confidence dampening for Stages 3 and 4 uses Bayesian calibration, starting from static base values but dynamically adjusting per `(source, category)` as human feedback (confirmations, corrections) accumulates.
|
|
92
|
+
|
|
93
|
+
See [`docs/annotation-pipeline.md`](docs/annotation-pipeline.md) for the full Mermaid flowchart.
|
|
94
|
+
|
|
95
|
+
## Tech Stack
|
|
96
|
+
|
|
97
|
+
| Layer | Technology |
|
|
98
|
+
|-------|-----------|
|
|
99
|
+
| Backend | FastAPI (async Python), Pydantic v2 |
|
|
100
|
+
| Frontend | React 18, React Router, Vite |
|
|
101
|
+
| Styling | Tailwind CSS, Lucide React icons |
|
|
102
|
+
| Database | SQLite + sqlite-vec (vectors in the same DB file) |
|
|
103
|
+
| LLM | Ollama — qwen3.5:4b (inference), nomic-embed-text (embeddings) |
|
|
104
|
+
| PDF Parsing | pdfplumber |
|
|
105
|
+
| Charts | Chart.js + react-chartjs-2 |
|
|
106
|
+
| CLI | Typer + Rich (terminal review queue) |
|
|
107
|
+
| IDs | ULID (sortable, unique) |
|
|
108
|
+
|
|
109
|
+
## Developing
|
|
110
|
+
|
|
111
|
+
### Prerequisites
|
|
112
|
+
|
|
113
|
+
- Python 3.10+
|
|
114
|
+
- Node.js 18+
|
|
115
|
+
- [Ollama](https://ollama.com) installed and running
|
|
116
|
+
|
|
117
|
+
Pull the required models:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
ollama pull qwen3.5:4b
|
|
121
|
+
ollama pull nomic-embed-text
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Backend
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Install dependencies (creates .venv from pyproject.toml + uv.lock)
|
|
128
|
+
uv sync
|
|
129
|
+
|
|
130
|
+
# Start the API server (runs migrations automatically on startup)
|
|
131
|
+
# Binds to 127.0.0.1 by default — loopback only, so your statements are not
|
|
132
|
+
# exposed on the network. Set RELOAD=1 for auto-reload during development.
|
|
133
|
+
RELOAD=1 uv run python -m finance_copilot
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
> The server binds to `127.0.0.1` by default. It has no authentication yet, so
|
|
137
|
+
> only expose it on your LAN deliberately: set `HOST=0.0.0.0` (you will get a
|
|
138
|
+
> startup warning). `PORT` defaults to `8000`.
|
|
139
|
+
|
|
140
|
+
### Frontend
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
cd ui
|
|
144
|
+
npm install
|
|
145
|
+
npm run dev # Dev server at http://localhost:5173 (proxies API to :8000)
|
|
146
|
+
npm run build # Production build → ui/dist/ (served by FastAPI)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Environment Variables
|
|
150
|
+
|
|
151
|
+
Create a `.env` file in the project root:
|
|
152
|
+
|
|
153
|
+
```env
|
|
154
|
+
OLLAMA_URL=http://localhost:11434
|
|
155
|
+
OLLAMA_MODEL=qwen3.5:4b
|
|
156
|
+
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
|
|
157
|
+
CONFIDENCE_THRESHOLD=0.85
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For password-protected statement PDFs, enter the password in the upload form — it is sent with the upload request, not stored.
|
|
161
|
+
|
|
162
|
+
All values have sensible defaults in `finance_copilot/config.py` — the `.env` file is optional unless you need to override them.
|
|
163
|
+
|
|
164
|
+
### CLI Review Queue
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
uv run python -m finance_copilot.cli review
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Interactive terminal UI for reviewing and correcting low-confidence annotations.
|
|
171
|
+
|
|
172
|
+
## Project Structure
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
finance_copilot/
|
|
176
|
+
├── api/routes/ # FastAPI endpoints (statements, transactions, annotations, ...)
|
|
177
|
+
├── db/
|
|
178
|
+
│ ├── migrations/ # Versioned SQL migrations (applied once, tracked in schema_migrations)
|
|
179
|
+
│ └── queries/ # SQL query builders
|
|
180
|
+
├── models/ # Pydantic schemas
|
|
181
|
+
├── parsers/
|
|
182
|
+
│ └── banks/ # Bank-specific PDF parsers (kotak.py)
|
|
183
|
+
├── pipeline/ # Annotation engine (rules, embed, annotate, llm, calibration)
|
|
184
|
+
├── config.py # All settings (Pydantic BaseSettings, reads .env)
|
|
185
|
+
├── cli.py # Typer CLI for terminal review
|
|
186
|
+
└── main.py # FastAPI app entry point
|
|
187
|
+
|
|
188
|
+
ui/
|
|
189
|
+
├── finance_copilot/
|
|
190
|
+
│ ├── pages/ # Dashboard, ReviewQueue, Transactions, Upload, Groups, People, Insights
|
|
191
|
+
│ ├── components/ # AnnotationPanel, CategoryPicker, TransactionTable, TagInput, ...
|
|
192
|
+
│ ├── contexts/ # StatementContext, ToastContext
|
|
193
|
+
│ └── lib/api.js # HTTP client wrapper
|
|
194
|
+
└── vite.config.js
|
|
195
|
+
|
|
196
|
+
data/ # SQLite database (gitignored)
|
|
197
|
+
docs/ # Pipeline documentation
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Your Data
|
|
201
|
+
|
|
202
|
+
Everything lives in one SQLite file, `finance.db`, in the app's data folder:
|
|
203
|
+
`~/Library/Application Support/FinanceCopilot` on macOS, `%LOCALAPPDATA%\FinanceCopilot` on Windows, and `~/.local/share/FinanceCopilot` on Linux.
|
|
204
|
+
Pass `--data-dir` to keep it somewhere else.
|
|
205
|
+
|
|
206
|
+
Settings › Data downloads a backup of the whole ledger.
|
|
207
|
+
The file leaves out your AI provider's API key, so it is safe to share or attach to a bug report.
|
|
208
|
+
Restore from the same tab: the app checks the file, saves your current data as `pre-restore-<timestamp>.db` next to `finance.db`, then swaps the backup in.
|
|
209
|
+
|
|
210
|
+
To restore by hand instead, quit the app, replace `finance.db` in the data folder with the backup file, and start it again.
|
|
211
|
+
Delete any `finance.db-wal` and `finance.db-shm` files next to it first, or SQLite may replay them over the restored file.
|
|
212
|
+
|
|
213
|
+
## Demo Data
|
|
214
|
+
|
|
215
|
+
Click "Load demo data" on an empty dashboard to explore with four months of fictional transactions, people, and an expense group.
|
|
216
|
+
To build a separate demo database instead (it never touches your real ledger):
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
uv run python scripts/seed_demo_data.py # writes data/demo.db
|
|
220
|
+
uv run python scripts/seed_demo_data.py --wipe # remove demo rows
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The demo data includes:
|
|
224
|
+
- Pre-annotated transactions from all pipeline stages (rule, rag_direct, rag_prompted, llm, manual)
|
|
225
|
+
- Low-confidence annotations that appear in the review queue
|
|
226
|
+
- Unannotated transactions ready for auto-annotate
|
|
227
|
+
- Synthetic people for known-person matching, and a settled Goa trip group
|
|
228
|
+
|
|
229
|
+
After seeding, trigger auto-annotate on the remaining unannotated transactions to see the full pipeline in action.
|
|
230
|
+
|
|
231
|
+
## Bank Statement Parsing
|
|
232
|
+
|
|
233
|
+
Uploads are auto-detected by the parser registry (`finance_copilot/parsers/registry.py`):
|
|
234
|
+
|
|
235
|
+
1. **Dedicated parsers** (currently Kotak) are tried first.
|
|
236
|
+
2. The **generic parser** (`finance_copilot/parsers/generic.py`) handles everything else: it
|
|
237
|
+
extracts tables from the PDF, infers which columns are date / description /
|
|
238
|
+
debit / credit / balance from the header row, and then **verifies every row
|
|
239
|
+
arithmetically against the running balance** (`balance[i] == balance[i-1] ± amount`).
|
|
240
|
+
It only claims a statement when ≥80% of those checks pass, so it cannot
|
|
241
|
+
silently ingest wrong amounts — unsupported layouts are rejected with an error
|
|
242
|
+
instead.
|
|
243
|
+
|
|
244
|
+
Limitations of the generic parser: it needs a text-native PDF (no scans/OCR), a
|
|
245
|
+
tabular layout with a header row, and a running-balance column (so most savings
|
|
246
|
+
account statements work; credit-card statements generally won't yet).
|
|
247
|
+
|
|
248
|
+
### Adding a Dedicated Bank Parser
|
|
249
|
+
|
|
250
|
+
Only needed when the generic parser can't handle a bank's layout:
|
|
251
|
+
|
|
252
|
+
1. Create a new file under `finance_copilot/parsers/banks/` (e.g., `sbi.py`)
|
|
253
|
+
2. Subclass `StatementParser` from `finance_copilot/parsers/base.py`
|
|
254
|
+
3. Implement the `parse()` method to extract transactions from the PDF
|
|
255
|
+
4. Register it in `finance_copilot/parsers/registry.py` **before** `GenericStatementParser`
|
|
256
|
+
|
|
257
|
+
## Demo Screenshots
|
|
258
|
+
|
|
259
|
+
### Dashboard
|
|
260
|
+

|
|
261
|
+
*Transaction overview with auto-annotate trigger and confidence scores*
|
|
262
|
+
|
|
263
|
+
### Review Queue
|
|
264
|
+

|
|
265
|
+
*Low-confidence annotations surfaced for human correction*
|
|
266
|
+
|
|
267
|
+
### Upload & Parse
|
|
268
|
+

|
|
269
|
+
*Drag-and-drop PDF upload with bank auto-detection*
|
|
270
|
+
|
|
271
|
+
### Transactions
|
|
272
|
+

|
|
273
|
+
*Full transaction table with category chips, inline editing, and filters*
|
|
274
|
+
|
|
275
|
+
### Insights
|
|
276
|
+

|
|
277
|
+
*Spending breakdowns and trends*
|
|
278
|
+
|
|
279
|
+
### Groups
|
|
280
|
+

|
|
281
|
+
*Group shared expenses (trips, events) and track who paid vs. who settled their split*
|
|
282
|
+
|
|
283
|
+
### People
|
|
284
|
+

|
|
285
|
+
*Known counterparties with UPI handles and relationships, so transfers get categorized correctly*
|
|
286
|
+
|
|
287
|
+
## Testing
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
uv run pytest finance_copilot/tests/
|
|
291
|
+
```
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Finance Copilot
|
|
2
|
+
|
|
3
|
+
Finance Copilot is a local-first personal finance application for parsing bank statements, organising transactions and analysing expenses.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uvx finance-copilot
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
That's it.
|
|
10
|
+
The app installs, starts on localhost, and opens in your browser.
|
|
11
|
+
Drop in a bank statement PDF (or click "Load demo data") and watch every transaction get parsed, balance-checked, and categorized.
|
|
12
|
+
No account, no cloud, no data leaving your computer.
|
|
13
|
+
|
|
14
|
+
> Requires [uv](https://docs.astral.sh/uv/getting-started/installation/) (one installer command, no Python needed). Prefer pipx? `pipx run finance-copilot` works too. First run takes a minute while dependencies download.
|
|
15
|
+
>
|
|
16
|
+
> On Python 3.15 the app runs with built-in embeddings off, because their runtime has no 3.15 build yet. For the full experience run `uvx --python 3.14 finance-copilot`.
|
|
17
|
+
|
|
18
|
+
Out of the box you get statement parsing with running-balance verification, rule-based and history-based categorization, expense groups, people tracking, and insights.
|
|
19
|
+
Add AI when you want it: point Settings › AI & Model at [Ollama](https://ollama.com) or paste any OpenAI-compatible API key.
|
|
20
|
+
|
|
21
|
+
## Highlights
|
|
22
|
+
|
|
23
|
+
- **100% local** - SQLite database + Ollama for inference, everything stays on your machine
|
|
24
|
+
- **Multi-stage annotation pipeline** - Your rules > built-in rules > learned merchants > RAG Direct > RAG Prompted > Plain LLM, with graceful fallbacks
|
|
25
|
+
- **Your own rules** - Settings > Rules: a word or phrase and a category, with a live preview of what it matches before you save
|
|
26
|
+
- **Human-in-the-loop** - Low-confidence predictions surface in an intuitive and fast review queue for correction
|
|
27
|
+
- **Bank statement parsing** - Dedicated Kotak parser plus a generic parser that handles most banks' tabular statement PDFs, with every row verified against the running balance; UPI metadata extracted automatically
|
|
28
|
+
- **Vector similarity search** - sqlite-vec powers few-shot retrieval for better categorization over time
|
|
29
|
+
- **React dashboard** - Transaction management, annotation review, expense groups, and insights/charts
|
|
30
|
+
|
|
31
|
+
## How the Pipeline Works
|
|
32
|
+
|
|
33
|
+
When you trigger auto-annotation, each transaction flows through four stages. The first match wins:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Transaction
|
|
37
|
+
|
|
|
38
|
+
v
|
|
39
|
+
[Stage 1: Rules] - Known-person match (UPI handle from people table),
|
|
40
|
+
| confidence: 0.95 then merchant keywords & UPI notes (~70 built-in rules)
|
|
41
|
+
| no match? ↓
|
|
42
|
+
v
|
|
43
|
+
[Stage 2: RAG Direct] - Find top-5 similar annotated transactions via embeddings
|
|
44
|
+
| confidence: dynamic If best match similarity >= 0.92 and donor is trusted,
|
|
45
|
+
| copy annotation (cosine × agreement × margin factors)
|
|
46
|
+
| no match? ↓
|
|
47
|
+
v
|
|
48
|
+
[Stage 3: RAG Prompted] - Few-shot LLM call with retrieved examples as context
|
|
49
|
+
| confidence: dynamic llm_conf × calibrated dampening (base 0.92, adapts with feedback)
|
|
50
|
+
| no match? ↓
|
|
51
|
+
v
|
|
52
|
+
[Stage 4: Plain LLM] - Cold LLM call without examples
|
|
53
|
+
| confidence: dynamic llm_conf × calibrated dampening (base 0.85, adapts with feedback)
|
|
54
|
+
v
|
|
55
|
+
Annotation saved → below threshold (0.85)? → Review Queue
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Confidence dampening for Stages 3 and 4 uses Bayesian calibration, starting from static base values but dynamically adjusting per `(source, category)` as human feedback (confirmations, corrections) accumulates.
|
|
59
|
+
|
|
60
|
+
See [`docs/annotation-pipeline.md`](docs/annotation-pipeline.md) for the full Mermaid flowchart.
|
|
61
|
+
|
|
62
|
+
## Tech Stack
|
|
63
|
+
|
|
64
|
+
| Layer | Technology |
|
|
65
|
+
|-------|-----------|
|
|
66
|
+
| Backend | FastAPI (async Python), Pydantic v2 |
|
|
67
|
+
| Frontend | React 18, React Router, Vite |
|
|
68
|
+
| Styling | Tailwind CSS, Lucide React icons |
|
|
69
|
+
| Database | SQLite + sqlite-vec (vectors in the same DB file) |
|
|
70
|
+
| LLM | Ollama — qwen3.5:4b (inference), nomic-embed-text (embeddings) |
|
|
71
|
+
| PDF Parsing | pdfplumber |
|
|
72
|
+
| Charts | Chart.js + react-chartjs-2 |
|
|
73
|
+
| CLI | Typer + Rich (terminal review queue) |
|
|
74
|
+
| IDs | ULID (sortable, unique) |
|
|
75
|
+
|
|
76
|
+
## Developing
|
|
77
|
+
|
|
78
|
+
### Prerequisites
|
|
79
|
+
|
|
80
|
+
- Python 3.10+
|
|
81
|
+
- Node.js 18+
|
|
82
|
+
- [Ollama](https://ollama.com) installed and running
|
|
83
|
+
|
|
84
|
+
Pull the required models:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
ollama pull qwen3.5:4b
|
|
88
|
+
ollama pull nomic-embed-text
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Backend
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Install dependencies (creates .venv from pyproject.toml + uv.lock)
|
|
95
|
+
uv sync
|
|
96
|
+
|
|
97
|
+
# Start the API server (runs migrations automatically on startup)
|
|
98
|
+
# Binds to 127.0.0.1 by default — loopback only, so your statements are not
|
|
99
|
+
# exposed on the network. Set RELOAD=1 for auto-reload during development.
|
|
100
|
+
RELOAD=1 uv run python -m finance_copilot
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
> The server binds to `127.0.0.1` by default. It has no authentication yet, so
|
|
104
|
+
> only expose it on your LAN deliberately: set `HOST=0.0.0.0` (you will get a
|
|
105
|
+
> startup warning). `PORT` defaults to `8000`.
|
|
106
|
+
|
|
107
|
+
### Frontend
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cd ui
|
|
111
|
+
npm install
|
|
112
|
+
npm run dev # Dev server at http://localhost:5173 (proxies API to :8000)
|
|
113
|
+
npm run build # Production build → ui/dist/ (served by FastAPI)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Environment Variables
|
|
117
|
+
|
|
118
|
+
Create a `.env` file in the project root:
|
|
119
|
+
|
|
120
|
+
```env
|
|
121
|
+
OLLAMA_URL=http://localhost:11434
|
|
122
|
+
OLLAMA_MODEL=qwen3.5:4b
|
|
123
|
+
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
|
|
124
|
+
CONFIDENCE_THRESHOLD=0.85
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For password-protected statement PDFs, enter the password in the upload form — it is sent with the upload request, not stored.
|
|
128
|
+
|
|
129
|
+
All values have sensible defaults in `finance_copilot/config.py` — the `.env` file is optional unless you need to override them.
|
|
130
|
+
|
|
131
|
+
### CLI Review Queue
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
uv run python -m finance_copilot.cli review
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Interactive terminal UI for reviewing and correcting low-confidence annotations.
|
|
138
|
+
|
|
139
|
+
## Project Structure
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
finance_copilot/
|
|
143
|
+
├── api/routes/ # FastAPI endpoints (statements, transactions, annotations, ...)
|
|
144
|
+
├── db/
|
|
145
|
+
│ ├── migrations/ # Versioned SQL migrations (applied once, tracked in schema_migrations)
|
|
146
|
+
│ └── queries/ # SQL query builders
|
|
147
|
+
├── models/ # Pydantic schemas
|
|
148
|
+
├── parsers/
|
|
149
|
+
│ └── banks/ # Bank-specific PDF parsers (kotak.py)
|
|
150
|
+
├── pipeline/ # Annotation engine (rules, embed, annotate, llm, calibration)
|
|
151
|
+
├── config.py # All settings (Pydantic BaseSettings, reads .env)
|
|
152
|
+
├── cli.py # Typer CLI for terminal review
|
|
153
|
+
└── main.py # FastAPI app entry point
|
|
154
|
+
|
|
155
|
+
ui/
|
|
156
|
+
├── finance_copilot/
|
|
157
|
+
│ ├── pages/ # Dashboard, ReviewQueue, Transactions, Upload, Groups, People, Insights
|
|
158
|
+
│ ├── components/ # AnnotationPanel, CategoryPicker, TransactionTable, TagInput, ...
|
|
159
|
+
│ ├── contexts/ # StatementContext, ToastContext
|
|
160
|
+
│ └── lib/api.js # HTTP client wrapper
|
|
161
|
+
└── vite.config.js
|
|
162
|
+
|
|
163
|
+
data/ # SQLite database (gitignored)
|
|
164
|
+
docs/ # Pipeline documentation
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Your Data
|
|
168
|
+
|
|
169
|
+
Everything lives in one SQLite file, `finance.db`, in the app's data folder:
|
|
170
|
+
`~/Library/Application Support/FinanceCopilot` on macOS, `%LOCALAPPDATA%\FinanceCopilot` on Windows, and `~/.local/share/FinanceCopilot` on Linux.
|
|
171
|
+
Pass `--data-dir` to keep it somewhere else.
|
|
172
|
+
|
|
173
|
+
Settings › Data downloads a backup of the whole ledger.
|
|
174
|
+
The file leaves out your AI provider's API key, so it is safe to share or attach to a bug report.
|
|
175
|
+
Restore from the same tab: the app checks the file, saves your current data as `pre-restore-<timestamp>.db` next to `finance.db`, then swaps the backup in.
|
|
176
|
+
|
|
177
|
+
To restore by hand instead, quit the app, replace `finance.db` in the data folder with the backup file, and start it again.
|
|
178
|
+
Delete any `finance.db-wal` and `finance.db-shm` files next to it first, or SQLite may replay them over the restored file.
|
|
179
|
+
|
|
180
|
+
## Demo Data
|
|
181
|
+
|
|
182
|
+
Click "Load demo data" on an empty dashboard to explore with four months of fictional transactions, people, and an expense group.
|
|
183
|
+
To build a separate demo database instead (it never touches your real ledger):
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
uv run python scripts/seed_demo_data.py # writes data/demo.db
|
|
187
|
+
uv run python scripts/seed_demo_data.py --wipe # remove demo rows
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The demo data includes:
|
|
191
|
+
- Pre-annotated transactions from all pipeline stages (rule, rag_direct, rag_prompted, llm, manual)
|
|
192
|
+
- Low-confidence annotations that appear in the review queue
|
|
193
|
+
- Unannotated transactions ready for auto-annotate
|
|
194
|
+
- Synthetic people for known-person matching, and a settled Goa trip group
|
|
195
|
+
|
|
196
|
+
After seeding, trigger auto-annotate on the remaining unannotated transactions to see the full pipeline in action.
|
|
197
|
+
|
|
198
|
+
## Bank Statement Parsing
|
|
199
|
+
|
|
200
|
+
Uploads are auto-detected by the parser registry (`finance_copilot/parsers/registry.py`):
|
|
201
|
+
|
|
202
|
+
1. **Dedicated parsers** (currently Kotak) are tried first.
|
|
203
|
+
2. The **generic parser** (`finance_copilot/parsers/generic.py`) handles everything else: it
|
|
204
|
+
extracts tables from the PDF, infers which columns are date / description /
|
|
205
|
+
debit / credit / balance from the header row, and then **verifies every row
|
|
206
|
+
arithmetically against the running balance** (`balance[i] == balance[i-1] ± amount`).
|
|
207
|
+
It only claims a statement when ≥80% of those checks pass, so it cannot
|
|
208
|
+
silently ingest wrong amounts — unsupported layouts are rejected with an error
|
|
209
|
+
instead.
|
|
210
|
+
|
|
211
|
+
Limitations of the generic parser: it needs a text-native PDF (no scans/OCR), a
|
|
212
|
+
tabular layout with a header row, and a running-balance column (so most savings
|
|
213
|
+
account statements work; credit-card statements generally won't yet).
|
|
214
|
+
|
|
215
|
+
### Adding a Dedicated Bank Parser
|
|
216
|
+
|
|
217
|
+
Only needed when the generic parser can't handle a bank's layout:
|
|
218
|
+
|
|
219
|
+
1. Create a new file under `finance_copilot/parsers/banks/` (e.g., `sbi.py`)
|
|
220
|
+
2. Subclass `StatementParser` from `finance_copilot/parsers/base.py`
|
|
221
|
+
3. Implement the `parse()` method to extract transactions from the PDF
|
|
222
|
+
4. Register it in `finance_copilot/parsers/registry.py` **before** `GenericStatementParser`
|
|
223
|
+
|
|
224
|
+
## Demo Screenshots
|
|
225
|
+
|
|
226
|
+
### Dashboard
|
|
227
|
+

|
|
228
|
+
*Transaction overview with auto-annotate trigger and confidence scores*
|
|
229
|
+
|
|
230
|
+
### Review Queue
|
|
231
|
+

|
|
232
|
+
*Low-confidence annotations surfaced for human correction*
|
|
233
|
+
|
|
234
|
+
### Upload & Parse
|
|
235
|
+

|
|
236
|
+
*Drag-and-drop PDF upload with bank auto-detection*
|
|
237
|
+
|
|
238
|
+
### Transactions
|
|
239
|
+

|
|
240
|
+
*Full transaction table with category chips, inline editing, and filters*
|
|
241
|
+
|
|
242
|
+
### Insights
|
|
243
|
+

|
|
244
|
+
*Spending breakdowns and trends*
|
|
245
|
+
|
|
246
|
+
### Groups
|
|
247
|
+

|
|
248
|
+
*Group shared expenses (trips, events) and track who paid vs. who settled their split*
|
|
249
|
+
|
|
250
|
+
### People
|
|
251
|
+

|
|
252
|
+
*Known counterparties with UPI handles and relationships, so transfers get categorized correctly*
|
|
253
|
+
|
|
254
|
+
## Testing
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
uv run pytest finance_copilot/tests/
|
|
258
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Demo instance deployment
|
|
2
|
+
|
|
3
|
+
One small VPS (~$5), one container, seeded sample data only.
|
|
4
|
+
The API refuses statement uploads, backups, delete-everything, and AI-config writes when `FC_DEMO_MODE=1` - the box is structurally incapable of holding real user data.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# On the VPS (Docker + Caddy installed):
|
|
10
|
+
docker build -t fc-demo deploy/demo/
|
|
11
|
+
docker run -d --name fc-demo --restart unless-stopped -p 127.0.0.1:8080:8080 fc-demo
|
|
12
|
+
|
|
13
|
+
# Caddy: copy Caddyfile to /etc/caddy/Caddyfile (edit the domain), then
|
|
14
|
+
systemctl reload caddy
|
|
15
|
+
|
|
16
|
+
# Cron (crontab -e):
|
|
17
|
+
0 * * * * CONTAINER=fc-demo /srv/finance-copilot/reset.sh
|
|
18
|
+
0 4 * * 1 CONTAINER=fc-demo FC_DEMO_RESEED=1 /srv/finance-copilot/reset.sh
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Upgrading
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
docker build --no-cache -t fc-demo deploy/demo/ # pulls the latest PyPI release
|
|
25
|
+
docker rm -f fc-demo && docker run -d --name fc-demo --restart unless-stopped -p 127.0.0.1:8080:8080 fc-demo
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Notes
|
|
29
|
+
|
|
30
|
+
- LLM and embedding providers are `none` on this box: no API key to leak, no Ollama to run.
|
|
31
|
+
The seeded dataset ships pre-annotated with realistic sources and confidences, so the review queue, Money Map, and dev-mode reasoning all demo well.
|
|
32
|
+
- No accounts, no cookies beyond what the app already uses, no analytics.
|
|
33
|
+
Count visits and upload-attempt 403s from the Caddy/app logs; the 403s are the conversion signal.
|
|
34
|
+
- `rate_limit` requires the Caddy rate-limit plugin (`xcaddy build --with github.com/mholt/caddy-ratelimit`); drop that block if you use stock Caddy.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Personal finance budgeting automation — local-first ingestion, embeddings, and RAG annotation."""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Run the API server: `python -m src`.
|
|
2
|
+
|
|
3
|
+
Owns the host default (127.0.0.1) so the API - and every bank statement it
|
|
4
|
+
serves - is loopback-only unless the user deliberately opts into LAN exposure.
|
|
5
|
+
There is no authentication layer yet (single-user local product), so a
|
|
6
|
+
non-loopback bind is a real disclosure and gets a loud warning, never a silent
|
|
7
|
+
copy-pasted default.
|
|
8
|
+
|
|
9
|
+
Config via environment: HOST (default 127.0.0.1), PORT (default 8000),
|
|
10
|
+
RELOAD (set to 1/true for auto-reload during development).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ipaddress
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
import uvicorn
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("finance_copilot")
|
|
21
|
+
|
|
22
|
+
# Names ip_address() cannot parse but which are still loopback in practice.
|
|
23
|
+
_LOOPBACK_NAMES = {"localhost", "localhost.localdomain"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_loopback(host: str) -> bool:
|
|
27
|
+
if host in _LOOPBACK_NAMES:
|
|
28
|
+
return True
|
|
29
|
+
try:
|
|
30
|
+
return ipaddress.ip_address(host).is_loopback
|
|
31
|
+
except ValueError:
|
|
32
|
+
# A hostname we can't resolve to a literal — treat as non-loopback so we
|
|
33
|
+
# err toward warning rather than staying silent.
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def warn_if_exposed(host: str) -> None:
|
|
38
|
+
"""Warn when binding somewhere other than loopback (0.0.0.0, a LAN IP, etc.)."""
|
|
39
|
+
if _is_loopback(host):
|
|
40
|
+
return
|
|
41
|
+
logger.warning(
|
|
42
|
+
"Binding to %s exposes the API - and every bank statement it serves - to "
|
|
43
|
+
"your whole network with NO authentication. Bind to 127.0.0.1 (the default) "
|
|
44
|
+
"unless you specifically intend LAN access.",
|
|
45
|
+
host,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> None:
|
|
50
|
+
host = os.getenv("HOST", "127.0.0.1")
|
|
51
|
+
port = int(os.getenv("PORT", "8000"))
|
|
52
|
+
reload = os.getenv("RELOAD", "").lower() in ("1", "true", "yes")
|
|
53
|
+
warn_if_exposed(host)
|
|
54
|
+
uvicorn.run("finance_copilot.main:app", host=host, port=port, reload=reload)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastAPI application wiring: routes and shared dependencies."""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Shared FastAPI dependencies (e.g. database connection or session accessors)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sqlite3
|
|
5
|
+
from collections.abc import Generator
|
|
6
|
+
|
|
7
|
+
from finance_copilot.db.connection import get_connection
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_db() -> Generator[sqlite3.Connection, None, None]:
|
|
11
|
+
"""Per-request connection. Migrations run once at startup (lifespan), not here."""
|
|
12
|
+
conn = get_connection()
|
|
13
|
+
try:
|
|
14
|
+
yield conn
|
|
15
|
+
finally:
|
|
16
|
+
conn.close()
|