Monitoraggio AI multi-fattore del rischio frana e inondazione per il territorio italiano. Copertura nazionale — tutte le 20 regioni ISTAT su griglia 1 km² (~312k celle); validato sul pilota Puglia + Basilicata.
Limen ("soglia" in latino) unisce morfologia, geologia, umidità del suolo, piogge, sismicità, incendi e archivi storici in un punteggio di rischio frana e inondazione per ogni cella di un'area italiana. Il rischio idraulico parte dalla pericolosità geografica ISPRA (Mosaicatura Idraulica / PGRA — reticolo fluviale, aree costiere e lacustri) e la solleva quando è prevista un'inondazione: pioggia prevista (pluviale), portata dei fiumi da GloFAS (fluviale) e mareggiate (costiera). Stack: Python 3.12 + FastAPI + PostgreSQL 16 + PostGIS, frontend Vite + React + MapLibre, notifiche multi-canale. Il sistema è costruito attorno a un Multi-Agent Framework (MAF) che orchestra ingestione → scoring → spiegazione, con un data layer portabile tra Docker locale, Neon (serverless) e qualsiasi PostgreSQL gestito.
git clone https://github.com/agent-engineering-studio/limen.git
cd limen
uv sync --all-groups
make up # Postgres+PostGIS + GeoServer (mcp-geoserver) + frontend
make init # migrate → seed 20 regioni → ITALICA → bootstrap → calibrate
uv run limen serve # FastAPI su http://localhost:8080/docs
uv run limen report build # report HTML statico zone a rischio → site/index.html
( cd frontend && npm ci && npm run dev ) # mappa su http://localhost:5173make init è idempotente e ricostruisce tutti i dati su una macchina
nuova: applica le migrazioni, semina le 20 regioni ISTAT (griglia 1 km²),
scarica il catalogo eventi frana e-ITALICA da Zenodo (truth set del
backtest §2.5), popola i fattori statici per cella (IFFI + PAI dal
PostGIS di GeoServer + slope da DTM) e calibra s_static.
Su Neon (dev/test serverless): impostare DB__CONNECTION_STRING con
?sslmode=require e SCHEDULER__CACHE_CLEANUP=apscheduler, nient'altro
— pg_cron viene saltato e l'APScheduler in-process prende in carico i
job periodici. In produzione (host Docker self-hosted, nessun cloud
provider): docker compose -f infra/docker/docker-compose.demo.yml up -d --build. Provider LLM risolto per precedenza LLM__PROVIDER >
ANTHROPIC_API_KEY > OPENAI_API_KEY > credenziali Foundry > Ollama;
in locale/produzione si usa Ollama (host, modello qwen).
Approfondimenti: docs/demo.md (demo locale su un AOI piccolo),
docs/architecture.md,
docs/openclaw.md,
docs/api.md,
docs/deployment.md,
docs/scoring-model.md,
docs/runbook.md.
Il motore V1 è una combinazione lineare pesata pura e interpretabile
(§2.4 del project doc) che legge ogni peso, soglia e cutoff di classe da
src/limen/config/regional_thresholds.yaml.
Nessuna costante cablata nel codice di scoring. Nessun LLM. Nessun I/O.
La stessa interfaccia CellFeatureBundle accetta anche il motore ML V2.
| Sorgente | Cosa ingeriamo | Cadenza | Implementazione |
|---|---|---|---|
| Open-Meteo | precip oraria, umidità del suolo 0–7 / 7–28 cm, neve (forecast); precip cumulata (archivio) | live, cache 30 min | integrations/openmeteo/ + CachedOpenMeteoClient |
| ISPRA (PostGIS di GeoServer) | inventario IFFI (frane/aree/dgpv, tutte le regioni), mosaico PAI frana; il mosaico idraulica alimenta il componente H |
all'init / settimanale | integrations/geoserver_source/ legge il PostGIS di mcp-geoserver |
| INGV | eventi FDSN (mag ≥ 3.5, ultimi 7 g, bbox AOI); griglia ShakeMap | poll event-driven | integrations/ingv/ + seismic_repo + ObjectStore |
| EFFIS | perimetri aree bruciate; fallback bulk Shapefile | batch settimanale | integrations/effis/ |
| Bootstrap statico | per cella: iffi_density_500 (entro 500 m dalla cella), distance_to_iffi_m, pai_class_norm, slope_deg (DTM) — SQL PostGIS set-based |
one-shot CLI | integrations/static_bootstrap/ + limen bootstrap-static |
| Motore di scoring (V1) | soglia Caine I/D (ri-tarata su ITALICA), API sigmoide, finestra post-incendio, decadimento sismico, aggregatore pesato + 5 classi | puro (no I/O) | core/scoring/ + MultiFactorScoringEngine |
| Calibrate | stat di normalizzazione per-AOI; precompute s_static |
one-shot | limen calibrate + reports/calibrate_<aoi>.md |
| Ingest eventi | catalogo e-ITALICA (frane innescate da pioggia, datate, tutta Italia) — truth set del backtest | one-shot, auto-download Zenodo | limen ingest-events |
| Backtest | replay di una finestra storica con pioggia antecedente CERRA (5.5 km) + truth set e-ITALICA → hit rate / FAR / lead time vs target §2.5 | one-shot | limen backtest + reports/backtest_*.md |
| Workflow MAF (V1) | AreaResolver → StaticFactors → MeteoFetch → SeismicCheck → FireCheck → [SensorFetch?] → RiskScoring → EscalationGate → RiskAnalyst → Briefing → PersistResult → AlertDispatch | one-shot CLI | agents/ + limen monitor-once |
| Provider LLM | precedenza LLM__PROVIDER > Anthropic > OpenAI > Foundry > Ollama; il resolver salta i provider cloud senza SDK e cade su Ollama (solo httpx). Briefing in italiano; RiskAnalyst restituisce JSON tipizzato. |
risolto all'avvio | agents/llm_factory/resolve_llm_factory |
| API HTTP | /health + /ready, POST /api/monitor/{aoi}, GET /api/aoi/{id}/risk/latest, GET /api/cell/{id}/breakdown, GET /api/aoi, GET /api/alerts, /api/tiles/..., OpenAPI su /docs e /redoc |
FastAPI / uvicorn | api/ + limen serve |
| Job periodici | workflow MAF orario (con shadow ML), sweep previsionale ogni 6 h, nowcast radar DPC ogni 15 min, report nazionale giornaliero, sync ISPRA settimanale, drift monitor ML (PSI/KS training-vs-live sulle feature canoniche che lo shadow persiste), cache cleanup + retention di model_runs (default 30 gg) |
APScheduler in-process | api/jobs/ |
| Radar DPC (nowcast) | SRI nazionale 1 km / 5 min (piattaforma radar DPC, CC-BY-SA): pioggia ≥ NOWCAST__MIN_INTENSITY_MMH su una regione ⇒ il workflow di quella AOI parte subito invece di aspettare il tick orario (cooldown 45 min; alert dal percorso operativo normale) |
poll ogni NOWCAST__INTERVAL_MINUTES |
integrations/dpc/ + api/jobs/nowcast_monitoring.py |
| Forecast previsionale | scoring a now+H ore con pioggia prevista Open-Meteo (osservata+prevista nella stessa finestra); champion + challenger ML sulle stesse celle; report on-demand o alert "PREVISIONE" schedulato con dedup (AOI, orizzonte) |
limen forecast / job ogni FORECAST__INTERVAL_HOURS |
agents/workflows/forecast.py + api/jobs/forecast_monitoring.py |
MCP limen-ops |
tool_risk_summary, tool_top_risk_cells, tool_cell_breakdown, tool_recent_alerts, tool_national_report + admin (tool_run_monitor, tool_build_report, tool_forecast_history, fail-closed su MCP_ADMIN_TOKEN) per gateway agentici (OpenClaw, Claude Desktop) |
servizio compose mcp, HTTP 127.0.0.1:8766/mcp |
mcp/ + limen mcp-serve |
| A2A (Agent2Agent) | interoperabilità JSON-RPC 2.0 con altri agent: Agent Card su /.well-known/agent-card.json, endpoint /a2a con message/send, message/stream (SSE), tasks/get/cancel, push notifications; skill = le query di sola lettura di limen-ops; task persistiti (a2a_tasks) |
montato sull'API, read-only | a2a/ + api/endpoints/a2a.py + pagina UI #/integrazioni |
| Report nazionale giornaliero | quadro aggregato 20 regioni (riepilogo per regione, top celle nazionali, top ML shadow, conteggi alert 24h) reso in italiano deterministico e spedito ogni mattina sui canali di notifica | cron REPORT__HOUR_UTC (default 06 UTC) |
api/jobs/daily_report.py + tool_national_report |
| Report HTML statico | pagina delle zone a maggior rischio: cluster di celle contigue (PostGIS ST_ClusterDBSCAN), mini-mappa interattiva Leaflet per zona (zoom/pan reali su basemap OSM/Carto, celle in GeoJSON, coordinate geografiche in evidenza), motivo deterministico S/M/E/F/H + verdetto, palette YlOrRd. Nessun LLM, nessuna immagine raster. Prova la soglia di allerta (High) e, se nessuna zona la supera, scende (Moderate→Low) mostrando comunque le aree relativamente più a rischio con un banner informativo (nessun allarme) — il report non è mai vuoto. Archivio immutabile versionato (site/archive/<ts>/ + manifest.json) per fact-checking futuro; idempotente (salta il rebuild se l'assessment non cambia); pubblicabile via GitHub Pages (REPORT__HTML_PUBLISH) |
al boot + ogni REPORT__HTML_INTERVAL_HOURS (default 1h) o on-demand |
site/ + limen report build + api/jobs/html_report.py |
| Vector tiles | matview mv_latest_risk (grid_cells ⨝ ultimo risk_assessment per cella), rinfrescata da refresh_mv_latest_risk(); servita da pg_tileserv |
per ciclo di monitoraggio | migrazione 007_map_views.sql |
| Rollup per comune | aggregazione amministrativa mv_comune_risk (regione già coperta da v_region_tiles): classe della peggior cella + profilo (conteggi per classe, esposti) + classifica per esposizione. Superfici: mappa (choropleth comune + badge celle-in-allerta sui High+, drill-down), sidebar, classifica, REST /api/comuni + /api/comune/{istat}, tool MCP/A2A top_comuni/comune_risk, sezione report, comune negli alert. Confini ISTAT importati con limen seed-comuni; refresh agganciato a refresh_mv_latest_risk() |
one-shot seed-comuni + per ciclo |
migrazione 026_comuni.sql + data/repos/comune_risk.py |
| Frontend | SPA Vite + TS + React + MapLibre GL JS: RiskMap (vector tiles, palette 5 classi ColorBrewer YlOrRd), LegendPanel (etichette + range, non solo colore), AlertList, CellPopup, TimelineSlider; overlay PMTiles PAI/IFFI opt-in |
pubblico, read-only | frontend/ |
| Notifiche | Protocol NotificationChannel + Telegram / MQTT / Email / Webhook (POST JSON a gateway agentici, es. OpenClaw /hooks con bearer token); dispatcher in parallelo con isolamento eccezioni per canale; dedup su alert_dispatches |
per tick del workflow | notifications/ |
| Decisione | Motivazione |
|---|---|
| PostgreSQL 16 + PostGIS engine-agnostic (no Supabase, no BaaS, no ORM) | Stesso SQL e stesso codice su Docker locale, Neon o self-hosted. Cambia solo DB__CONNECTION_STRING. |
asyncpg + codec PostGIS custom |
Le geometrie viaggiano come oggetti Shapely, niente boilerplate WKB, niente lock-in di sessione ORM. |
pg_cron opzionale |
Neon non lo supporta. L'APScheduler in-process esegue gli stessi job periodici quando l'estensione manca. |
Object storage dietro Protocol (filesystem / s3) |
I byte raster non vanno mai nel DB. PostGIS memorizza solo riferimenti (path + bbox + CRS + checksum). Il backend s3 punta a qualsiasi endpoint S3-compatibile (MinIO, R2, B2) via OBJECT_STORE__ENDPOINT_URL — mai SDK cloud. |
| Migrazioni SQL semplici | Niente Alembic, niente ORM. Un runner con tabella schema_migrations + checksum. Comportamento identico su ogni Postgres. |
Pydantic v2 + structlog |
Configurazione tipizzata e log strutturati senza reinventare. |
uv + layout src/ |
Gestione dipendenze lockfile-first; il pacchetto non può importare per errore il proprio codice di test. |
| GeoServer come sorgente dati generica | mcp-geoserver pubblica gli opendata ISPRA nel suo PostGIS; la semantica ISPRA vive solo nel loader Limen, non nell'MCP (che resta generico). |
Il ciclo di test formale ha tarato il motore sui dati reali:
- Soglia Caine I/D ri-derivata dal catalogo e-ITALICA (5974 coppie intensità-durata misurate da pluviometri), inviluppo inferiore T5 per macroregione. La soglia storica lasciava il 36% delle frane reali sotto soglia; ora ~95% sono sopra soglia.
- Sorgente pioggia: CERRA (5.5 km) al posto di ERA5 (~28 km), che non risolve la pioggia convettiva locale.
- Densità IFFI contata entro 500 m dalla cella (non dal centroide).
- Saturazione densità in YAML (
static.iffi_density_saturation), bilanciata su ITALICA (recall vs precisione).
Validazione su ground-truth (pioggia pluviometro reale): ~63–77% delle frane reali raggiungono ≥Moderate; backtest end-to-end su una finestra scatenante con hit-rate e lead-time entro i target §2.5. Il FAR resta limitato dall'incompletezza del catalogo eventi, non dal motore.
Caricata da variabili d'ambiente (e .env opzionale) via
limen.config.settings.Settings. I campi annidati usano __ come
delimitatore.
| Variabile | Default | Note |
|---|---|---|
DB__CONNECTION_STRING |
postgresql://limen:limen@localhost:5432/limen |
DSN PostgreSQL. Aggiungi ?sslmode=require per Neon. |
OBJECT_STORE__BACKEND |
filesystem |
filesystem o s3. |
OBJECT_STORE__ENDPOINT_URL |
vuoto | Endpoint S3-compatibile (MinIO, R2, B2). |
SCHEDULER__CACHE_CLEANUP |
apscheduler |
pg_cron o apscheduler. Usa APScheduler su Neon. |
LLM__PROVIDER |
vuoto | Override: anthropic / openai / foundry / ollama. |
LLM__OLLAMA_BASE_URL |
http://localhost:11434 |
Ollama host (da container: host.docker.internal). |
LLM__OLLAMA_MODEL |
qwen3.6:latest |
Modello Ollama unico per tutti i ruoli agente. |
SCORING__MODE |
champion_only |
shadow fa girare il challenger ML in parallelo (scrive solo model_runs). |
FORECAST__ENABLED |
true |
Sweep previsionale schedulato; FORECAST__HORIZON_HOURS (48), __INTERVAL_HOURS (6), __MIN_LEVEL (High). |
REPORT__HTML_ENABLED |
true |
Report HTML statico; __HTML_INTERVAL_HOURS (1), __HTML_RUN_AT_STARTUP (true), __HTML_OUTPUT_DIR (site), __HTML_MIN_LEVEL (High), __HTML_MAX_CLUSTERS (50), __HTML_ARCHIVE_KEEP (240), __HTML_PUBLISH (false). |
NOTIFICATIONS__WEBHOOK__URL |
vuoto | Gateway agentico (OpenClaw /hooks); __TOKEN per il bearer. |
MCP_ADMIN_TOKEN |
vuoto | Abilita tool_run_monitor sull'MCP; assente = disabilitato (fail-closed). |
GEOSERVER_SOURCE__DB_DSN |
vuoto | DSN del PostGIS di mcp-geoserver (IFFI + PAI). |
LIMEN_DEM_RASTER |
vuoto | GeoTIFF DTM per lo slope (opt-in). |
LIMEN_ITALICA_CSV |
vuoto | CSV e-ITALICA locale; se assente, auto-download da Zenodo. |
LOG_LEVEL / LOG_JSON |
INFO / false |
Livello + output JSON dei log structlog. |
Vedi .env.example per l'elenco completo con esempi.
aoi, grid_cells, cell_static_factors, iffi_landslides,
pai_hazard, landslide_events (catalogo eventi datati),
risk_assessments, model_runs (predizioni del challenger in shadow),
training_samples (feature store ML), norm_stats, raster_refs,
app_cache, alert_dispatches, forecast_dispatches (dedup alert
previsionali), seismic_events, fire_perimeters, tabelle sensori,
schema_migrations. Tutte le geometrie in EPSG:4326; distanze/aree
calcolate in EPSG:3035 (LAEA Europe). Migrazioni SQL immutabili in
src/limen/data/migrations/NNN_*.sql.
make test # unit + integration (testcontainers)
make test-unit # veloce, senza Docker
make check # lint + typecheck + test
make lint # ruff check
make format # ruff format
make typecheck # mypy --strict su src/ (esegui dopo `uv sync --all-groups`)Gate prima di ogni commit: ruff check + ruff format puliti,
mypy --strict pulito, pytest verde. Su Apple Silicon i test di
integrazione usano automaticamente imresamu/postgis-arm64
(override con LIMEN_TEST_POSTGIS_IMAGE).
Completati di recente:
-
Copertura nazionale (20 regioni ISTAT) con soglia Caine ri-tarata su e-ITALICA e pioggia di backtest CERRA.
-
Componente H (idraulica / inondazione) attivo dal mosaico idraulica ISPRA via GeoServer (~132k celle in zona di pericolosità idraulica).
-
Inondazione dinamica (issue #8, attiva di default): la componente H riceve un uplift quando è prevista un'inondazione — pioggia prevista (pluviale), portata dei fiumi via Open-Meteo Flood/GloFAS (fluviale) e mareggiate via Open-Meteo Marine (costiera), il tutto scalato dalla pericolosità geografica ISPRA. Deterministico, puro, degrada in modo neutro (segnale non applicabile ⇒ 0); disattivabile con
ENABLE_FLOOD_FORECAST=false. -
Autenticazione su database (self-hosted, PA-compliant — Clerk rimosso): registrazione con nome/cognome/email + verifica via codice, login con password (hash scrypt), sessioni server-side in cookie httpOnly (revocabili), ruoli
admin/ml-ops/operatore/viewer, CLIlimen create-admin. Endpoint/api/auth/*,AuthProvider/useAuthsulla SPA. SPID via seam OIDC (in arrivo). Vedi issue #49 +src/limen/auth/. -
Sfidante ML addestrato su 37k campioni (frane e-ITALICA + pioggia antecedente CERRA per campione): AUC-PR 0.60 vs 0.28 del motore deterministico sulla stessa partizione spaziale e la stessa pioggia (
limen train; registro MLflow; promozione solo manuale). Backup/ripristino del dataset e del modello:make dump-training/make restore-training. -
Shadow mode attivo: il challenger ML gira in parallelo al champion a ogni ciclo orario (
SCORING__MODE=shadow) e scrive le probabilità inmodel_runs; il motore deterministico resta l'unico autoritativo. -
Area di diagnostica ML shadow (issue #26): endpoint
/api/shadow/summary(accordo/divergenza per regione + eventi reali) con aggregazione condivisa col CLIlimen shadow-report; un pannellino operatore in linguaggio piano nella dashboard; e una rotta riservata#/diagnostica-ml(ruoloml-ops) con recall sugli eventi reali, accordo per regione e mappa della divergenza per cella (palette neutra, non quella del rischio; vista tilev_shadow_divergence_tilesvia pg_tileserv). Non autoritativa — il V1 guida sempre le allerte; il testo è pensato per operatori non tecnici. -
limen forecast— run previsionale anow+Hore: la finestra Open-Meteo miscela pioggia osservata e prevista, champion e ML valutano le stesse celle, report in./reports/senza toccare lo stato operativo (LIMEN_FORECAST_AOI/_HOURS/_CELL_LIMIT). -
MCP
limen-ops(limen mcp-serve): rischio, allerte e run del workflow esposti come tool per gateway agentici (OpenClaw, Claude Desktop). Nel compose è il serviziomcp(immaginelimen/mcp:0.1, HTTP su127.0.0.1:8766;LIMEN_MCP_BIND=0.0.0.0solo dietro TLS quando il gateway sta su un'altra macchina). Aggancio OpenClaw:openclaw mcp set limen-ops '{"url":"http://127.0.0.1:8766/mcp","transport":"streamable-http"}'. -
Canale di notifica
webhook: gli alert POSTano il payload JSON al gateway agentico (es. OpenClaw/hooks, bearer token) — pull via MCP per le domande, push via webhook per gli eventi. Config:NOTIFICATIONS__ENABLED_CHANNELS=["webhook"]+NOTIFICATIONS__WEBHOOK__URL/TOKEN. -
Pagina divulgativa «Come funziona» nel frontend (
#/come-funziona) con simulatore che usa la formula reale di produzione. -
Cartella
llm-training/: dataset (assessment → briefing) in formato Alpaca + guida passo-passo per l'eventuale fine-tuning con LLaMA-Factory. -
Pipeline event-driven (attiva di default, opt-out
FORECAST__ENABLED=false): sweep previsionale schedulato anow+48hcon pioggia prevista; celle a livello ≥FORECAST__MIN_LEVELfanno partire un alert previsionale sugli stessi canali (webhook/OpenClaw, Telegram…), etichettato "PREVISIONE", riassunto deterministico, dedup per (AOI, orizzonte) separata dagli alert operativi. -
Nowcast radar DPC (attivo di default): l'SRI a 1 km della rete radar nazionale fa da innesco a orizzonte breve — pioggia intensa vista dal radar ⇒ scoring immediato della regione colpita.
Prossimi passi:
- Verdetto shadow challenger ML vs champion (issue #4). Tooling pronto:
uv run limen shadow-reportleggemodel_runssulla finestra post-fix (LIMEN_SHADOW_SINCEdefault2026-07-06T13:00Z,LIMEN_SHADOW_AOIopzionale) e scrive divergenza media/p95/max, correlazione, accordo classi e gli eventi reali ITALICA inreports/shadow_report_*.md. Il giudizio va dato entro la retention di 30 gg dimodel_runs— finestra 2026-07-20 → 2026-08-03. Se il challenger convince, promozione manuale (mlflow models transition-stage, mai automatica). L'area di diagnostica ML è già realizzata (vedi sopra); resta solo il reliability plot, gated su abbastanza eventi reali (issue #30).
Limen usa i seguenti dataset aperti — l'attribuzione è obbligatoria quando la mappa / i briefing vengono pubblicati:
- ISPRA IdroGEO (inventario IFFI, mosaici PAI frana e idraulica) — © ISPRA / Autorità italiane, CC-BY 4.0. https://idrogeo.isprambiente.it
- e-ITALICA (catalogo frane innescate da pioggia, CNR-IRPI) — CC-BY 4.0, Zenodo DOI 10.5281/zenodo.14204473.
- Copernicus (Open-Meteo, ERA5, CERRA reanalisi regionale) — licenza Copernicus, uso libero con attribuzione. https://open-meteo.com
- INGV (servizio eventi FDSN, ShakeMap) — CC-BY 4.0. https://terremoti.ingv.it
- EFFIS (perimetri aree bruciate) — termini Copernicus EFFIS.
- CORINE Land Cover 2018 Italia (SINAnet/ISPRA) — CC-BY. https://groupware.sinanet.isprambiente.it
- Carta Geolitologica d'Italia 1:500k (Geoportale Nazionale PCN/MASE, Servizio Geologico d'Italia) — CC-BY 4.0.
- ISTAT (confini amministrativi 2023) — CC-BY 4.0.
- OpenStreetMap (basemap) — ODbL.
CONTRIBUTING.md— setup dev, stile commit, gate.SECURITY.md— canale di disclosure privato.CODE_OF_CONDUCT.md— regole di comportamento della community.ROADMAP.md— direzione e prossimi passi.docs/OSS_IMPACT.md— impatto open-source e componenti riusabili.CHANGELOG.md— storico versionato (Keep a Changelog).
Limen è uno strumento di supporto alle decisioni: i suoi output sono indicatori modellati, non allerte ufficiali di Protezione Civile.
Apache-2.0 — vedi LICENSE.
