An open API service indexing awesome lists of open source software.

https://github.com/drake69/spendif-ai

🏦 Personal finance ledger β€” aggregates bank statements (CSV/XLSX) into a single ledger with hybrid deterministic + LLM categorization. Offline-first, privacy-safe.
https://github.com/drake69/spendif-ai

bank-statements budgeting finance llm ollama personal-finance python self-hosted sqlite streamlit

Last synced: 7 days ago
JSON representation

🏦 Personal finance ledger β€” aggregates bank statements (CSV/XLSX) into a single ledger with hybrid deterministic + LLM categorization. Offline-first, privacy-safe.

Awesome Lists containing this project

README

          

# Spendif.ai v3.0

[![CI](https://github.com/drake69/spendif-ai/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/drake69/spendif-ai/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/drake69/spendif-ai/graph/badge.svg)](https://codecov.io/gh/drake69/spendif-ai)
[![Python 3.13](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)](https://www.python.org/downloads/)
[![License: PolyForm NC](https://img.shields.io/badge/license-PolyForm%20Noncommercial-orange)](LICENSE)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Streamlit](https://img.shields.io/badge/UI-Streamlit-ff4b4b?logo=streamlit&logoColor=white)](https://streamlit.io)
[![Issues](https://img.shields.io/github/issues/drake69/spendif-ai)](https://github.com/drake69/spendif-ai/issues)
[![Last commit](https://img.shields.io/github/last-commit/drake69/spendif-ai)](https://github.com/drake69/spendif-ai/commits/main)
[![Supporta su Patreon](https://img.shields.io/badge/Patreon-offrimi%20un%20caffΓ¨%20β˜•-F96854?logo=patreon&logoColor=white)](https://patreon.com/drake69)

> πŸ‡¬πŸ‡§ [Read in English](README.md)

Registro finanziario personale unificato con pipeline ibrida deterministica + LLM. Aggrega esportazioni bancarie eterogenee (conti correnti, carte di credito/debito/prepagate, conti deposito) in un unico ledger cronologico, eliminando i doppi conteggi causati da addebiti carta ricorrenti e da giroconti interni. Offline-first; backend LLM remoti opt-in, con sanitizzazione PII obbligatoria.

---

> πŸ‘‹ **Utente finale β€” vuoi installare Spendif.ai?**
> Vai alla [pagina Primo avvio](https://drake69.github.io/spendif-ai/getting-started.html) per la guida illustrata all'installazione e al primo avvio. Questo README Γ¨ dedicato a sviluppatori e contributor.

---

## Cos'Γ¨ (tecnicamente)

- **Python 3.13 Β· Streamlit Β· SQLAlchemy Β· Pydantic Β· Pandas**
- **Pipeline ibrida**: normalizer deterministico + classifier LLM + categorizer a cascata
- **LLM multi-backend** con circuit breaker: llama.cpp (default per desktop), Ollama, OpenAI, Claude β€” interazione diretta tramite SDK, senza LangChain
- **Launcher desktop nativo**: pywebview + PyInstaller β†’ DMG / MSIX / .deb / .rpm
- **UI Streamlit articolata in una quindicina di pagine**, i18n IT+EN completo (760+ chiavi di traduzione)

## Cosa Γ¨ implementato

- **Pipeline ibrida (deterministica + LLM)** β€” `core/normalizer.py` analizza qualunque esportazione bancaria tabellare; `core/classifier.py` deduce `DocumentSchema` tramite LLM; `core/categorizer.py` esegue una cascata in 4 fasi (regole utente β†’ regex β†’ LLM β†’ fallback)
- **LLM multi-backend con circuit breaker** β€” factory in `core/llm_backends.py`: llama.cpp, Ollama, OpenAI, Claude. Fallback automatico e quarantena in caso di fallimento
- **Sanitizzazione PII (RF-10)** β€” redazione di IBAN / PAN / codice fiscale / nomi titolari in `core/sanitizer.py`, obbligatoria prima di qualsiasi chiamata remota (`assert_sanitized()` Γ¨ una precondizione, non un'opzione best-effort)
- **Tassonomia multilingua** β€” 2 livelli in DB, 5 lingue (it/en/fr/de/es), configurabile dall'UI Streamlit
- **Riconciliazione carta-conto (RF-03, beta)** β€” algoritmo in 3 fasi in `core/normalizer.py`: abbina gli addebiti carta con le spese sottostanti per eliminare i doppi conteggi *(casi limite ancora in fase di affinamento)*
- **Rilevazione giroconti interni (RF-04, beta)** β€” abbinamento per importo simbolico e finestra di Β±7 giorni, con permutazioni dei nomi del titolare per intercettare esportazioni del tipo Β«Cognome NomeΒ» *(casi limite ancora in fase di affinamento)*

## Cosa arriva dopo

Spendif.ai Γ¨ in fase di alpha test. Le funzioni che aggiungeremo le decidiamo insieme a chi usa l'app oggi: nessun lavoro speculativo, nessuna roadmap calata dall'alto.

Funzioni in valutazione in base ai riscontri degli alpha tester:

- **Registrazione del contante** β€” registrare manualmente le spese in contanti, senza estratto bancario
- **Andamento degli investimenti** β€” vedere a colpo d'occhio come si comportano gli strumenti in portafoglio
- **App mobile compagna** β€” registrare al volo le spese in contanti dal telefono e sincronizzarle con il desktop

Una di queste ti servirebbe? Segnalacelo su [GitHub Discussions](https://github.com/drake69/spendif-ai/discussions). Quando arrivano abbastanza richieste sulla stessa funzione, sale in cima alla coda.

## πŸ‘©β€πŸ’» Sviluppo locale

```bash
git clone https://github.com/drake69/spendif-ai.git
cd spendify
uv sync --extra desktop

# LLM locale (scelta dello sviluppatore β€” l'installer desktop gestisce
# questo automaticamente):
# β†’ se hai giΓ  Ollama attivo: ollama pull gemma3:12b
# β†’ altrimenti: `uv sync` installa llama-cpp-python e il launcher
# scarica automaticamente un modello GGUF al primo avvio

./start.sh # oppure: streamlit run app.py
```

Prerequisiti: **Python 3.13+**, **[uv](https://github.com/astral-sh/uv)** e, in alternativa, Ollama (oppure nulla: llama.cpp Γ¨ giΓ  incluso). Configurazione completa β†’ [CONTRIBUTING.md](CONTRIBUTING.md).

### Eseguire come app desktop nativa da sorgente

```bash
uv run python -m desktop.launcher
```

Apre una finestra pywebview, scarica un modello AI al primo avvio e avvia Streamlit all'interno della stessa finestra. L'esperienza Γ¨ identica a quella dei bundle DMG/MSIX.

## Eseguire i test

```bash
uv run pytest -v # suite completa (no mock LLM)
uv run pytest --cov=. --cov-report=term-missing # con coverage (target β‰₯ 90%)
uv run pytest -k "architecture" # gate separazione layer
uv run pytest -k "security" # forbidden pattern + SQL injection
```

I test architetturali e di sicurezza sono gate CI obbligatori e devono restare verdi sul branch `main`.

## Architettura

```
ui/
↓
services/ ─┬──→ core/ (pipeline: normalizer, classifier, categorizer,
β”‚ sanitizer, llm_backends, orchestrator)
β”‚
└──→ db/ (models + repository CRUD) β†’ SQLite
```

**Layer:**

- **`ui/`** β€” Pagine Streamlit (onboarding, upload, review, history, analysis,
report, budget, budget vs actual, bulk edit, chat, checklist, home, registry,
rules, settings, taxonomy, llm_models) piΓΉ `i18n/`, `widgets/`, `components/`
e `sidebar.py`. Sola presentazione: importa **esclusivamente** da `services/`.

- **`services/`** β€” Facade fra UI e logica interna. I servizi
(`transaction_service`, `import_service`, `review_service`, `llm_service`,
`budget_service`, `category_service`, `rule_service`, `settings_service`,
`nsi_taxonomy_service`) orchestrano la pipeline di `core/` e persistono lo
stato via `db.repository` (pattern Repository). Riesportano i tipi di dominio
(`DocumentSchema`, …) per evitare che l'UI tocchi i layer inferiori.

- **`core/`** β€” Pipeline di dominio, indipendente da Streamlit:
normalizzazione deterministica (`normalizer`, RF-02/03/04/06),
classificazione documenti via LLM (`classifier`, RF-01), categorizzazione
a cascata (`categorizer`, RF-05), sanitizzazione PII obbligatoria prima
di chiamate LLM remote (`sanitizer`, RF-10), factory dei backend LLM
(`llm_backends`), orchestratore Flow 1 / Flow 2 (`orchestrator`, RF-01β†’RF-07)
e motore di auto-learning storico (`history_engine`). Funzioni pure dove
possibile, unit-testabili senza mock di Streamlit.

- **`db/`** β€” Persistenza SQLAlchemy: `models.py` definisce le tabelle ORM
(Transaction, ImportBatch, DocumentSchemaModel, ReconciliationLink,
InternalTransferLink, CategoryRule, UserSettings, BudgetTarget,
LlmUsageLog, …, RF-07); `repository.py` espone CRUD idempotenti upsert-style
(RF-06/07); `taxonomy_defaults.py` contiene i template di tassonomia per
lingua.

**Regola verificata in CI dal coupling gate:** `ui/` non puΓ² importare da
`core/` nΓ© da `db/` β€” passa solo per `services/`. `tools/coupling_check.py
--strict` blocca le PR che la violano.

**Debito noto:** `db/repository.py` importa al top-level alcuni tipi da
`core/` (`DocumentSchema`, `CategoryRule`); `core/` a sua volta importa da
`db/` in alcuni punti con import inline dentro le funzioni per evitare cicli.
L'estrazione di un layer `schemas/` ignorante che spezzi il ciclo Γ¨ tracciata
in backlog come **AI-153**.

Diagramma completo e Flusso 1 vs Flusso 2 β†’ [docs/architecture.it.md](docs/architecture.it.md).

## Struttura del repository

```
sw_artifacts/
β”œβ”€β”€ app.py # Entry point Streamlit (onboarding gate + ~15 pagine)
β”œβ”€β”€ core/ # Pipeline: orchestrator, normalizer, classifier, categorizer, sanitizer, llm_backends
β”œβ”€β”€ services/ # Facade layer per l'UI; async runner; settings; import
β”œβ”€β”€ ui/ # Pagine Streamlit + i18n + widgets
β”œβ”€β”€ db/ # SQLAlchemy ORM, repository pattern, schema con auto-hash migrations
β”œβ”€β”€ api/ # Endpoint REST FastAPI (opzionale)
β”œβ”€β”€ desktop/ # Launcher nativo (pywebview) + splash
β”œβ”€β”€ packaging/ # Build script: macos/, windows/, linux/, homebrew/, winget/
β”œβ”€β”€ docker/ # Containerizzazione
β”œβ”€β”€ prompts/ # Template prompt LLM (JSON versionato)
β”œβ”€β”€ reports/ # Export HTML + CSV + XLSX
β”œβ”€β”€ tests/ # Suite pytest (target coverage β‰₯ 90%)
β”œβ”€β”€ benchmark/ # Suite benchmark LLM (multi-provider)
└── docs/ # Documentazione utente e sviluppatore
```

Dettagli β†’ [docs/developer_guide.md](docs/developer_guide.md).

## πŸ“š Documentazione

| Argomento | Lingue |
|---|---|
| Installazione e primo avvio | [EN](docs/installazione.en.md) Β· [IT](docs/installazione.md) |
| Guida utente (ogni pagina) | [EN](docs/guida_utente.en.md) Β· [IT](docs/guida_utente.md) |
| Reference guide (pipeline, tassonomia, RF-03/04) | [EN](docs/reference_guide.en.md) Β· [IT](docs/reference_guide.md) |
| Architettura | [EN](docs/architecture.md) Β· [IT](docs/architecture.it.md) |
| Design decisions | [EN](docs/design_decisions.md) Β· [IT](docs/design_decisions.it.md) |
| Configurazione | [EN](docs/configurazione.en.md) Β· [IT](docs/configurazione.md) |
| Developer guide | [EN](docs/developer_guide.en.md) Β· [IT](docs/developer_guide.md) |
| Guida alla categorizzazione | [EN](docs/guida_classificazione.en.md) Β· [IT](docs/guida_classificazione.md) |
| Schema database | [EN](docs/database.en.md) Β· [IT](docs/database.md) |
| Deployment | [EN](docs/deployment.en.md) Β· [IT](docs/deployment.md) |
| Processo di rilascio | [EN](docs/release_process.md) Β· [IT](docs/release_process.it.md) |
| Loop di build e test desktop | [EN](docs/desktop_build_and_test.md) Β· [IT](docs/desktop_build_and_test.it.md) |
| Contribuire | [EN](CONTRIBUTING.en.md) Β· [IT](CONTRIBUTING.md) |
| Politica di sicurezza | [EN](SECURITY.md) Β· [IT](SECURITY.it.md) |
| Changelog | [EN](CHANGELOG.md) Β· [IT](CHANGELOG.it.md) |

## Contribuire

Segnalazioni di bug, proposte di funzionalitΓ  e PR sono benvenute. Vedi [CONTRIBUTING.md](CONTRIBUTING.md) per workflow, regole di branching, framework delle prioritΓ  e gate CI.

## Licenza

**PolyForm Noncommercial 1.0.0** β€” vedi [LICENSE](LICENSE). Uso personale libero; l'uso commerciale richiede una licenza separata.

---

### Cosa lascia la macchina β€” trasparenza

Tutti i dati finanziari sono memorizzati localmente in `~/.spendifai/ledger.db`.

**Backend LLM locale (default β€” llama.cpp, Ollama)**: nulla lascia la macchina.

**Backend LLM remoto (opt-in β€” OpenAI, Claude)**: il payload contiene
descrizioni sanitizzate insieme a **importi**, **date** e **metadati
delle colonne**.

#### Esempio di redazione β€” categorizer (`core/categorizer.py:303`)

Riga grezza della transazione dal CSV:

```
date: 2026-03-15
description: "BONIFICO da MARIO ROSSI IT60X0542811101000000123456 CAU 12345 STIPENDIO MENSILE"
amount: 1500.00
```

Cosa l'LLM remoto riceve effettivamente:

```json
{
"amount": "1500.00",
"description": "BONIFICO da Carlo Brambilla STIPENDIO MENSILE"
}
```

Cosa Γ¨ cambiato:
- `MARIO ROSSI` (nome titolare configurato) β†’ `Carlo Brambilla` (nome fittizio dal pool italiano, ripristinato dopo la risposta LLM)
- `IT60X...` (IBAN) β†’ ``
- `CAU 12345` (codice transazione bancaria) β†’ ``
- `amount` e metadati di data: **inviati in chiaro**

Il prompt del categorizer istruisce il modello a "basare la decisione su
descrizione, importo e contesto" (`prompts/categorizer.json`). L'effettivo
impatto dell'importo sull'accuratezza, in pratica, non Γ¨ stato misurato
contro una baseline senza importi: il comportamento predefinito,
conservativo, lo lascia nel payload finchΓ© la misurazione non sarΓ  disponibile.

#### Roadmap

Le modalitΓ  di redazione di importi e date per i backend remoti
(`none` / `buckets` / `strip`) sono in roadmap β€” elemento di backlog **AI-55**.