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.
Files changed (118) hide show
  1. finance_copilot-0.1.0/.gitignore +24 -0
  2. finance_copilot-0.1.0/PKG-INFO +291 -0
  3. finance_copilot-0.1.0/README.md +258 -0
  4. finance_copilot-0.1.0/deploy/demo/README.md +34 -0
  5. finance_copilot-0.1.0/finance_copilot/__init__.py +1 -0
  6. finance_copilot-0.1.0/finance_copilot/__main__.py +58 -0
  7. finance_copilot-0.1.0/finance_copilot/api/__init__.py +1 -0
  8. finance_copilot-0.1.0/finance_copilot/api/deps.py +16 -0
  9. finance_copilot-0.1.0/finance_copilot/api/routes/__init__.py +1 -0
  10. finance_copilot-0.1.0/finance_copilot/api/routes/annotations.py +712 -0
  11. finance_copilot-0.1.0/finance_copilot/api/routes/categories.py +74 -0
  12. finance_copilot-0.1.0/finance_copilot/api/routes/config.py +126 -0
  13. finance_copilot-0.1.0/finance_copilot/api/routes/data.py +234 -0
  14. finance_copilot-0.1.0/finance_copilot/api/routes/embeddings.py +157 -0
  15. finance_copilot-0.1.0/finance_copilot/api/routes/groups.py +138 -0
  16. finance_copilot-0.1.0/finance_copilot/api/routes/insights.py +19 -0
  17. finance_copilot-0.1.0/finance_copilot/api/routes/local_llm.py +134 -0
  18. finance_copilot-0.1.0/finance_copilot/api/routes/people.py +80 -0
  19. finance_copilot-0.1.0/finance_copilot/api/routes/rules.py +103 -0
  20. finance_copilot-0.1.0/finance_copilot/api/routes/statements.py +196 -0
  21. finance_copilot-0.1.0/finance_copilot/api/routes/transactions.py +163 -0
  22. finance_copilot-0.1.0/finance_copilot/app.py +137 -0
  23. finance_copilot-0.1.0/finance_copilot/cli.py +101 -0
  24. finance_copilot-0.1.0/finance_copilot/config.py +208 -0
  25. finance_copilot-0.1.0/finance_copilot/db/__init__.py +1 -0
  26. finance_copilot-0.1.0/finance_copilot/db/connection.py +145 -0
  27. finance_copilot-0.1.0/finance_copilot/db/lifecycle.py +55 -0
  28. finance_copilot-0.1.0/finance_copilot/db/migrations/001_initial.sql +57 -0
  29. finance_copilot-0.1.0/finance_copilot/db/migrations/002_links.sql +23 -0
  30. finance_copilot-0.1.0/finance_copilot/db/migrations/003_people.sql +13 -0
  31. finance_copilot-0.1.0/finance_copilot/db/migrations/004_member_type.sql +6 -0
  32. finance_copilot-0.1.0/finance_copilot/db/migrations/005_drop_group_type.sql +4 -0
  33. finance_copilot-0.1.0/finance_copilot/db/migrations/006_seed_categories.sql +122 -0
  34. finance_copilot-0.1.0/finance_copilot/db/migrations/007_expand_annotation_source.sql +21 -0
  35. finance_copilot-0.1.0/finance_copilot/db/migrations/008_feedback_stats.sql +12 -0
  36. finance_copilot-0.1.0/finance_copilot/db/migrations/009_add_reimbursement_category.sql +3 -0
  37. finance_copilot-0.1.0/finance_copilot/db/migrations/010_provenance_dedup_indexes.sql +14 -0
  38. finance_copilot-0.1.0/finance_copilot/db/migrations/011_annotation_jobs.sql +14 -0
  39. finance_copilot-0.1.0/finance_copilot/db/migrations/012_statement_period.sql +9 -0
  40. finance_copilot-0.1.0/finance_copilot/db/migrations/013_category_ids.sql +24 -0
  41. finance_copilot-0.1.0/finance_copilot/db/migrations/014_json_list_columns.sql +29 -0
  42. finance_copilot-0.1.0/finance_copilot/db/migrations/015_annotation_reasoning.sql +6 -0
  43. finance_copilot-0.1.0/finance_copilot/db/migrations/016_app_settings.sql +10 -0
  44. finance_copilot-0.1.0/finance_copilot/db/migrations/017_counterparty_key.sql +26 -0
  45. finance_copilot-0.1.0/finance_copilot/db/migrations/018_self_transfers_category.sql +22 -0
  46. finance_copilot-0.1.0/finance_copilot/db/migrations/019_annotation_source_learned_rule_rag_knn.sql +35 -0
  47. finance_copilot-0.1.0/finance_copilot/db/migrations/020_person_relationship_and_learned_rule_suppressions.sql +27 -0
  48. finance_copilot-0.1.0/finance_copilot/db/migrations/021_statement_balances.sql +58 -0
  49. finance_copilot-0.1.0/finance_copilot/db/migrations/022_statement_account_ref.sql +9 -0
  50. finance_copilot-0.1.0/finance_copilot/db/migrations/023_transaction_links.sql +16 -0
  51. finance_copilot-0.1.0/finance_copilot/db/migrations/024_annotation_review_snooze.sql +11 -0
  52. finance_copilot-0.1.0/finance_copilot/db/migrations/025_job_kind.sql +5 -0
  53. finance_copilot-0.1.0/finance_copilot/db/migrations/026_user_rules.sql +56 -0
  54. finance_copilot-0.1.0/finance_copilot/db/queries/__init__.py +1 -0
  55. finance_copilot-0.1.0/finance_copilot/db/queries/annotations.py +159 -0
  56. finance_copilot-0.1.0/finance_copilot/db/queries/app_settings.py +154 -0
  57. finance_copilot-0.1.0/finance_copilot/db/queries/categories.py +144 -0
  58. finance_copilot-0.1.0/finance_copilot/db/queries/common.py +28 -0
  59. finance_copilot-0.1.0/finance_copilot/db/queries/embeddings.py +206 -0
  60. finance_copilot-0.1.0/finance_copilot/db/queries/feedback_stats.py +88 -0
  61. finance_copilot-0.1.0/finance_copilot/db/queries/groups.py +237 -0
  62. finance_copilot-0.1.0/finance_copilot/db/queries/insights.py +772 -0
  63. finance_copilot-0.1.0/finance_copilot/db/queries/learned_rules.py +218 -0
  64. finance_copilot-0.1.0/finance_copilot/db/queries/people.py +179 -0
  65. finance_copilot-0.1.0/finance_copilot/db/queries/transactions.py +298 -0
  66. finance_copilot-0.1.0/finance_copilot/db/queries/user_rules.py +253 -0
  67. finance_copilot-0.1.0/finance_copilot/demo_seed.py +560 -0
  68. finance_copilot-0.1.0/finance_copilot/local_llm.py +356 -0
  69. finance_copilot-0.1.0/finance_copilot/main.py +254 -0
  70. finance_copilot-0.1.0/finance_copilot/models/__init__.py +1 -0
  71. finance_copilot-0.1.0/finance_copilot/models/annotation.py +132 -0
  72. finance_copilot-0.1.0/finance_copilot/models/transaction.py +48 -0
  73. finance_copilot-0.1.0/finance_copilot/parsers/__init__.py +1 -0
  74. finance_copilot-0.1.0/finance_copilot/parsers/banks/__init__.py +1 -0
  75. finance_copilot-0.1.0/finance_copilot/parsers/banks/kotak.py +96 -0
  76. finance_copilot-0.1.0/finance_copilot/parsers/base.py +46 -0
  77. finance_copilot-0.1.0/finance_copilot/parsers/generic.py +401 -0
  78. finance_copilot-0.1.0/finance_copilot/parsers/registry.py +32 -0
  79. finance_copilot-0.1.0/finance_copilot/parsers/upi.py +42 -0
  80. finance_copilot-0.1.0/finance_copilot/pipeline/__init__.py +1 -0
  81. finance_copilot-0.1.0/finance_copilot/pipeline/annotate.py +1036 -0
  82. finance_copilot-0.1.0/finance_copilot/pipeline/calibration.py +58 -0
  83. finance_copilot-0.1.0/finance_copilot/pipeline/counterparty.py +201 -0
  84. finance_copilot-0.1.0/finance_copilot/pipeline/embed.py +299 -0
  85. finance_copilot-0.1.0/finance_copilot/pipeline/ingest.py +236 -0
  86. finance_copilot-0.1.0/finance_copilot/pipeline/llm.py +568 -0
  87. finance_copilot-0.1.0/finance_copilot/pipeline/rules.py +234 -0
  88. finance_copilot-0.1.0/finance_copilot/pipeline/run_summary.py +192 -0
  89. finance_copilot-0.1.0/finance_copilot/pipeline/sources.py +33 -0
  90. finance_copilot-0.1.0/hatch_build.py +28 -0
  91. finance_copilot-0.1.0/pyproject.toml +107 -0
  92. finance_copilot-0.1.0/ui/dist/assets/CategoryPicker-Cm0NSo75.js +1 -0
  93. finance_copilot-0.1.0/ui/dist/assets/ConfirmDialog-OLzxIURy.js +1 -0
  94. finance_copilot-0.1.0/ui/dist/assets/Dashboard-BSVQzNdb.js +21 -0
  95. finance_copilot-0.1.0/ui/dist/assets/EmptyState-DWGf9hQV.js +1 -0
  96. finance_copilot-0.1.0/ui/dist/assets/Groups-CxbdIaHH.js +1 -0
  97. finance_copilot-0.1.0/ui/dist/assets/Insights-CAcvj1Wu.js +1 -0
  98. finance_copilot-0.1.0/ui/dist/assets/People-C9IyajLX.js +1 -0
  99. finance_copilot-0.1.0/ui/dist/assets/PeriodPicker-oXvMQJaq.js +1 -0
  100. finance_copilot-0.1.0/ui/dist/assets/ReasoningPanel-CCk-75_X.js +6 -0
  101. finance_copilot-0.1.0/ui/dist/assets/ReviewQueue-DBBU47Q9.js +26 -0
  102. finance_copilot-0.1.0/ui/dist/assets/Settings-DHQMZoY-.js +56 -0
  103. finance_copilot-0.1.0/ui/dist/assets/Transactions-X_Kspt5O.js +1 -0
  104. finance_copilot-0.1.0/ui/dist/assets/Upload-PHdAA6CH.js +21 -0
  105. finance_copilot-0.1.0/ui/dist/assets/chevron-down-BOTn-jA0.js +6 -0
  106. finance_copilot-0.1.0/ui/dist/assets/chevron-right-1ZTDEAuV.js +6 -0
  107. finance_copilot-0.1.0/ui/dist/assets/circle-check-DVy3w3Qy.js +6 -0
  108. finance_copilot-0.1.0/ui/dist/assets/circle-help-B0D8uify.js +6 -0
  109. finance_copilot-0.1.0/ui/dist/assets/dates-DENdhcfJ.js +1 -0
  110. finance_copilot-0.1.0/ui/dist/assets/index-B86kwi0r.css +1 -0
  111. finance_copilot-0.1.0/ui/dist/assets/index-CttMkFOe.js +175 -0
  112. finance_copilot-0.1.0/ui/dist/assets/index-D94rVQ8L.js +18 -0
  113. finance_copilot-0.1.0/ui/dist/assets/pencil-BdwggxyK.js +11 -0
  114. finance_copilot-0.1.0/ui/dist/assets/plus-DTgrI-ia.js +6 -0
  115. finance_copilot-0.1.0/ui/dist/assets/refresh-cw-BYXFldMU.js +6 -0
  116. finance_copilot-0.1.0/ui/dist/assets/trash-2-Oxw5TKtt.js +6 -0
  117. finance_copilot-0.1.0/ui/dist/assets/zap-C-11zHGY.js +6 -0
  118. finance_copilot-0.1.0/ui/dist/index.html +13 -0
@@ -0,0 +1,24 @@
1
+ plans/
2
+
3
+ __pycache__/
4
+
5
+ .env
6
+
7
+ .DS_Store
8
+
9
+ data/
10
+
11
+ .claude/
12
+
13
+ .pytest_cache/
14
+
15
+ node_modules/
16
+
17
+ ui/dist/
18
+
19
+ .venv/
20
+ findings/
21
+
22
+ eval/
23
+ # Maintainer runbooks stay local
24
+ docs/release.md
@@ -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
+ ![Dashboard](docs/dashboard.png)
261
+ *Transaction overview with auto-annotate trigger and confidence scores*
262
+
263
+ ### Review Queue
264
+ ![Review Queue](docs/reviewqueue.png)
265
+ *Low-confidence annotations surfaced for human correction*
266
+
267
+ ### Upload & Parse
268
+ ![Upload](docs/upload.png)
269
+ *Drag-and-drop PDF upload with bank auto-detection*
270
+
271
+ ### Transactions
272
+ ![Transactions](docs/transaction.png)
273
+ *Full transaction table with category chips, inline editing, and filters*
274
+
275
+ ### Insights
276
+ ![Insights](docs/insights.png)
277
+ *Spending breakdowns and trends*
278
+
279
+ ### Groups
280
+ ![Groups](docs/groups.png)
281
+ *Group shared expenses (trips, events) and track who paid vs. who settled their split*
282
+
283
+ ### People
284
+ ![People](docs/people.png)
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
+ ![Dashboard](docs/dashboard.png)
228
+ *Transaction overview with auto-annotate trigger and confidence scores*
229
+
230
+ ### Review Queue
231
+ ![Review Queue](docs/reviewqueue.png)
232
+ *Low-confidence annotations surfaced for human correction*
233
+
234
+ ### Upload & Parse
235
+ ![Upload](docs/upload.png)
236
+ *Drag-and-drop PDF upload with bank auto-detection*
237
+
238
+ ### Transactions
239
+ ![Transactions](docs/transaction.png)
240
+ *Full transaction table with category chips, inline editing, and filters*
241
+
242
+ ### Insights
243
+ ![Insights](docs/insights.png)
244
+ *Spending breakdowns and trends*
245
+
246
+ ### Groups
247
+ ![Groups](docs/groups.png)
248
+ *Group shared expenses (trips, events) and track who paid vs. who settled their split*
249
+
250
+ ### People
251
+ ![People](docs/people.png)
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()