Skip to content

Commit a308344

Browse files
aborrusoclaude
andauthored
feat: formato --format compact (NDJSON) per agenti AI (#7)
* feat: formato --format compact (NDJSON) per agenti AI Quarto formato di output per `search`: una riga JSON per record con i soli campi ad alto segnale (id, title, org, type, category, updated, resources), per scremare molti risultati a basso consumo di token prima del `get`. - libreria: compact_results(payload) in search.py (org da apiso_OrganizationName_txt, category da apiso_TopicCategory_s, resources dai links con dedup+sort) - output.py: mode compact + emit NDJSON; get --format compact rifiutato - test: +6 (4 libreria, 2 CLI), 37/37 verdi - doc: README, SKILL.md, docs/future-ideas.md (spunti Copernicus) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): commenti PR #7 + reference OGC services Fix dai commenti review (Copilot + Greptile): - SKILL: aggiunto 'updated' tra i campi del formato compact - cli: help --format chiarisce che compact è solo per search - output: docstring emit() csv allineata (output vuoto, non errore) - search: _topic_category cerca la categoria ISO sia in keywords_s sia in categories (prima saltava il fallback se keywords_s era popolato ma privo di valori ISO) + test - future-ideas: sezione compact marcata come implementata Inoltre: nuova reference skills/rndt-explorer/references/ogc-services.md (esplorazione servizi OGC WMS/WFS/WCS/WMTS del catalogo RNDT con GDAL/OGR JSON), linkata dalla Fase 4 della SKILL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 700716a commit a308344

10 files changed

Lines changed: 375 additions & 15 deletions

File tree

LOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# LOG
22

3+
## 2026-06-11
4+
5+
- **Nuovo formato `--format compact` (NDJSON per agenti).** Quarto formato di output per `search`: una riga JSON per record con i soli campi ad alto segnale (`id`, `title`, `org`, `type`, `category`, `updated`, `resources`). Pensato per far scremare molti risultati a basso consumo di token prima del `get`. Spunto dal progetto Copernicus-Services-Products-Metadata (rendering sintetico dei risultati; lì *prima* della ricerca perché catalogo locale, qui *dopo* perché catalogo remoto).
6+
- Libreria: `compact_results(payload)` in `search.py``org` da `apiso_OrganizationName_txt` (fallback `author.name`), `category` da `apiso_TopicCategory_s` (fallback keyword ISO), `resources` = dctype dei `links` (escluse rappresentazioni del metadato), dedup+sort.
7+
- `output.py`: aggiunto mode `compact` + branch NDJSON. `get --format compact` rifiutato come csv (dettaglio non tabellare).
8+
- Test: +6 (4 libreria, 2 CLI). 37/37 verdi, ruff pulito.
9+
- Doc: README (formati + Per agenti AI), SKILL.md, `docs/future-ideas.md` con gli altri spunti Copernicus (rerank semantico, snapshot/cronologia CI, Parquet).
10+
- **Verifica `--bbox`** (challenge utente): il filtro funziona (semantica overlaps; 485→21 record in Sicilia, 0 in oceano). Il rumore "Toscana sotto bbox Sicilia" è dovuto a record con bbox dichiarato errato (tutta Italia `6.6,35.5,18.5,47.1`) — problema di qualità nei metadati sorgente, non del filtro.
11+
- **Fix review PR #7** (Copilot + Greptile): SKILL elenca anche `updated` tra i campi compact; help `--format` chiarisce che `compact` è solo per `search`; docstring `emit()` csv allineata (output vuoto, non errore); `_topic_category` ora cerca la categoria ISO sia in `keywords_s` sia in `categories` (prima saltava il fallback se `keywords_s` era popolato ma senza valori ISO) +1 test; `docs/future-ideas.md` marca il compact come implementato. 38/38 verdi.
12+
- **Nuova reference skill `references/ogc-services.md`**: guida all'esplorazione dei servizi OGC (WMS/WFS/WCS/WMTS) linkati nel catalogo RNDT con GDAL/OGR a output JSON. Punti verificati: `gdalinfo`/`ogrinfo -json` danno solo Nome+Titolo (no `queryable`/abstract → solo nel GetCapabilities, che ha tutto), `gdallocationinfo` per GetFeatureInfo, `ogr2ogr` per download vettoriale. Esempio catasto AdE (layer `fabbricati`, non `BU.Building`; WFS senza fabbricati; 3 layer queryable). Linkata da Fase 4 della SKILL.
13+
314
## 2026-06-10 (continua)
415

516
- **README: sezione "Esempi di conversazione con un'AI"** per utenti GIS desktop (non CLI). 7 scenari conversation-first (domanda in linguaggio naturale → comando openrndt leggibile → URL WMS/WFS da incollare in QGIS), tutti con risultati RNDT reali e verificati live: uso suolo Emilia-Romagna (WMS getCapabilities testato 200), catasto Piemonte, ortofoto (Sardegna/Lodi/Piemonte), reticolo idrografico WFS (ISPRA/ARPA Veneto/Basilicata), 430 dataset Regione Lombardia, bbox area Bologna (40, framing onesto "sovrapposizione"), 259 frane open data. Niente jq mostrato, niente link con IP interni (bug issue #2).

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ openrndt discover
5656
```
5757

5858
Tutti i comandi accettano `--format json` (default), `--format table`, `--format csv`.
59+
Per `search` c'è anche `--format compact`: una riga NDJSON per record con i soli
60+
campi ad alto segnale (`id`, `title`, `org`, `type`, `category`, `updated`,
61+
`resources`), pensata per agenti AI e pipe a basso consumo di token.
62+
63+
```bash
64+
openrndt --format compact search --q "catasto" --num 3
65+
# {"id":"…","title":"…","org":"Comune di Torino","type":"service","category":null,"updated":"…","resources":["WMS"]}
66+
```
5967

6068
## Esempi di conversazione con un'AI
6169

@@ -231,7 +239,8 @@ passo passo. Da qui i principi di design (sul modello di
231239
[opensdmx](https://github.com/aborruso/opensdmx)):
232240

233241
- **Output strutturato, mai oggetti Python.** Default JSON su `stdout`; `--format
234-
table` per la lettura umana, `--format csv` per i risultati tabellari.
242+
table` per la lettura umana, `--format csv` per i risultati tabellari, `--format
243+
compact` (NDJSON, una riga per record) per scremare molti risultati a basso costo.
235244
- **In modalità JSON, `stdout` contiene solo JSON.** Errori e avvisi vanno su
236245
`stderr`: si può fare pipe diretta in `jq`.
237246
- **Errori leggibili e self-contained: mai stack trace.** Un errore di rete o HTTP

docs/future-ideas.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Idee future
2+
3+
Spunti raccolti per evoluzioni di openrndt. Non sono impegni: vanno valutati
4+
caso per caso rispetto al design (CLI snella, read-only, niente cache locale).
5+
6+
## Spunti da Copernicus-Services-Products-Metadata
7+
8+
Riferimento: <https://github.com/do-me/Copernicus-Services-Products-Metadata>
9+
10+
Quel progetto fa l'opposto di openrndt — snapshot statico settimanale del
11+
catalogo committato nel repo (Parquet/CSV/Excel/JSON) + discovery locale
12+
**retrieve-then-rerank** con cross-encoder. openrndt invece è live e read-only.
13+
Le idee trasferibili:
14+
15+
### Rerank semantico sui risultati (priorità alta)
16+
17+
La ricerca lessicale RNDT (Solr/Lucene) premia chi ripete le parole esatte
18+
della query. Un reranker leggero — es. `cross-encoder/ettin-reranker-17m-v1`
19+
(~68 MB, gira su CPU) — potrebbe riordinare i top-N di `openrndt search` per
20+
*intento* anziché per match testuale.
21+
22+
- Flag opzionale `--rerank` su `search`: prende i top-K, costruisce
23+
"product documents" compatti (titolo + abstract + keyword) e li riordina.
24+
- Resta opzionale → non rompe il design read-only.
25+
- Dipendenza pesante: valutare extra `pip install openrndt[rerank]` oppure
26+
tenerla fuori dalla CLI e solo dentro la skill.
27+
- Da verificare: qualità del reranker in italiano (serve un PoC su query reale).
28+
29+
### Snapshot periodico + cronologia (GitHub Actions)
30+
31+
Workflow CI separato (NON nella CLI) che fa un dump periodico del catalogo RNDT.
32+
Abilita: ricerca offline veloce, rerank senza N chiamate di rete, e soprattutto
33+
il **diff temporale** ("cosa è stato aggiunto/modificato nel catalogo questo
34+
mese"). Da tenere come artefatto di repo, fuori dal design no-cache della CLI.
35+
36+
### Output Parquet per analisi
37+
38+
Aggiungere `--format parquet` (preserva i tipi, ottimo per duckdb/pandas).
39+
Coerente con l'uso analitico, basso costo. Unico formato extra che aggiunge
40+
valore reale rispetto a json/table/csv già presenti.
41+
42+
### Output compatto per agenti AI — ✅ implementato
43+
44+
Realizzato come `--format compact` (NDJSON, una riga per record). Vedi PR #7.
45+
Nota di design: in Copernicus il "product document" sintetico è costruito
46+
*prima* di cercare (catalogo locale); in openrndt il catalogo è remoto, quindi
47+
il formato compatto è solo un rendering dei risultati *dopo* la `search`.
48+
49+
## Da NON fare
50+
51+
- Snapshot dentro la CLI: violerebbe il design read-only/no-cache. Va come
52+
workflow separato.
53+
- Replicare i 6 formati di output di Copernicus: ridondante. Solo Parquet
54+
aggiunge valore reale.

skills/rndt-explorer/SKILL.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,16 @@ catalogo ufficiale italiano dei metadati geografici (ISO 19115/19139).
2929
L'opzione **globale** `--format` (sempre PRIMA del comando) sceglie l'output:
3030

3131
```bash
32-
openrndt --format json search … # default, per parsing
33-
openrndt --format table search … # Rich, per umani
34-
openrndt --format csv search … # per fogli di calcolo
32+
openrndt --format json search … # default, per parsing
33+
openrndt --format table search … # Rich, per umani
34+
openrndt --format csv search … # per fogli di calcolo
35+
openrndt --format compact search … # NDJSON: 1 riga/record, per scremare a basso costo
3536
```
3637

38+
Il formato `compact` (solo per `search`) emette una riga JSON per record con i
39+
campi ad alto segnale — `id`, `title`, `org`, `type`, `category`, `updated`, `resources`
40+
ideale per individuare il record giusto prima di chiedere il dettaglio con `get`.
41+
3742
Tutti i comandi hanno `--help`. La skill segue 4 fasi.
3843

3944
---
@@ -167,6 +172,12 @@ openrndt --format json search --q "catasto" --num 50 \
167172
Tabella `rel`/`dctype` completa in
168173
[`references/result-structure.md`](./references/result-structure.md).
169174

175+
Una volta ottenuto l'endpoint di un servizio OGC (WMS/WFS/WCS/WMTS),
176+
esploralo con GDAL/OGR a output JSON (`gdalinfo -json "WMS:…"`,
177+
`ogrinfo -json "WFS:…"`): nomi dei layer, feature type, quali layer sono
178+
interrogabili con GetFeatureInfo, download vettoriale con `ogr2ogr`. Guida in
179+
[`references/ogc-services.md`](./references/ogc-services.md).
180+
170181
---
171182

172183
## Workflow pronti
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Esplorare i servizi OGC del catalogo RNDT
2+
3+
Gran parte dei metadati RNDT linka **servizi OGC** — WMS, WFS, WCS, WMTS
4+
(campo `links`, `dctype`). Questo riferimento è di servizio all'esplorazione di
5+
quei servizi: una volta ottenuto l'endpoint con `openrndt get`, ispezionalo per
6+
capire cosa offre (layer, feature type, interrogabilità, dati scaricabili)
7+
prima di usarlo.
8+
9+
Strumento di elezione: **GDAL/OGR con output JSON**, perché dà info strutturate
10+
e affidabili. Regola generale valida per qualunque servizio OGC, non solo quelli
11+
del RNDT: **non andare a memoria sui nomi dei layer, interroga il servizio**.
12+
13+
Parsa sempre con `jq` e azzera lo stderr (`2>/dev/null`): i warning GDAL
14+
sporcano lo stdout JSON.
15+
16+
## Il GetCapabilities ha TUTTO
17+
18+
Ogni servizio OGC si auto-descrive con il documento **GetCapabilities**: è la
19+
fonte autorevole e completa. Contiene tutto ciò che serve a usare il servizio:
20+
21+
- elenco dei layer / feature type / coverage, con `Name`, `Title`, `Abstract`;
22+
- per i WMS: l'attributo `queryable="0|1"` (interrogabilità con GetFeatureInfo),
23+
gli **stili** disponibili, i **formati** immagine e i formati di
24+
GetFeatureInfo, i limiti di **scala** (`MinScaleDenominator`/`Max…`);
25+
- i **CRS/SRS** supportati e i **bounding box** per ciascun layer;
26+
- per i WFS: i formati di output (`outputFormat`), le operazioni supportate.
27+
28+
Richiesta (sostituisci `SERVICE`/`VERSION` per WFS/WCS/WMTS):
29+
30+
```bash
31+
curl -s "<endpoint>?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities" -o caps.xml
32+
```
33+
34+
GDAL/OGR ne leggono solo un **sottoinsieme comodo** (nomi + titoli, come layer
35+
raster/vettoriali pronti all'uso) e scartano il resto. Quindi: usa GDAL per la
36+
via rapida, ma quando ti serve un'informazione che GDAL non espone
37+
(queryability, stili, scale, formati) vai **sempre** al GetCapabilities nativo.
38+
39+
## Esplorare un WMS — `gdalinfo -json`
40+
41+
Elenca i layer come *subdataset* (parsing del GetCapabilities):
42+
43+
```bash
44+
gdalinfo -json "WMS:<endpoint>?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities" 2>/dev/null \
45+
| jq -r '.metadata.SUBDATASETS | to_entries[] | select(.key|endswith("_DESC")) | .value'
46+
```
47+
48+
Ogni subdataset ha solo due chiavi:
49+
50+
- `SUBDATASET_N_NAME` → un URL **GetMap** (è GDAL che traduce ogni layer in
51+
una richiesta GetMap; per questo il subdataset non è un GetCapabilities).
52+
- `SUBDATASET_N_DESC` → il **titolo** del layer.
53+
54+
## Esplorare un WFS — `ogrinfo -json`
55+
56+
Elenca i *feature type* (layer vettoriali):
57+
58+
```bash
59+
ogrinfo -json "WFS:<endpoint>" 2>/dev/null | jq '[.layers[].name]'
60+
```
61+
62+
## Cosa GDAL NON dà: `queryable` e abstract
63+
64+
`gdalinfo -json` espone **solo** Nome (come GetMap) e Titolo. **Non** porta i
65+
flag di capability del WMS (`queryable`, `opaque`, stili) né l'abstract: per
66+
design li scarta. Verificato e confermato dalla doc GDAL (driver WMS) e dalla
67+
community: *"the NAME is used as `_NAME`, the TITLE for `_DESC`… user need to
68+
read the [resto] directly from the native GetCapabilities response"*.
69+
70+
### Quali layer sono interrogabili con GetFeatureInfo?
71+
72+
Sta **solo** nel GetCapabilities XML, attributo `<Layer queryable="0|1">`.
73+
GDAL non lo espone. Lettura diretta:
74+
75+
```bash
76+
curl -s "<endpoint>?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities" -o caps.xml
77+
grep -n -A2 '<Layer queryable=' caps.xml | grep -E 'queryable=|<Name>'
78+
```
79+
80+
Il primo `<Name>` dopo ogni `<Layer queryable="…">` è il nome del layer (lo
81+
`<Name>` con `default` che segue appartiene allo `<Style>`, ignoralo).
82+
83+
## Interrogare un punto (GetFeatureInfo) — `gdallocationinfo`
84+
85+
GDAL *sa* fare una GetFeatureInfo, ma con un altro strumento: interroga e
86+
basta, non ti dice prima se il layer è queryable.
87+
88+
```bash
89+
gdallocationinfo "WMS:<url GetMap del layer>" -wgs84 <lon> <lat>
90+
```
91+
92+
## Scaricare vettoriale da un WFS — `ogr2ogr`
93+
94+
```bash
95+
ogr2ogr -f GPKG out.gpkg "WFS:<endpoint>" <feature_type> \
96+
-spat <xmin> <ymin> <xmax> <ymax> # ritaglio per bbox
97+
```
98+
99+
## Altri servizi OGC
100+
101+
- **WCS** (coverage raster): `gdalinfo -json "WCS:<endpoint>"` → subdataset
102+
delle coverage; poi `gdal_translate "WCS:…"` per scaricare.
103+
- **WMTS** (tile): `gdalinfo -json "WMTS:<endpoint>"` → subdataset per
104+
layer/tile-matrix-set.
105+
106+
In tutti i casi vale la stessa regola: GDAL elenca i layer/coverage ma **non**
107+
porta i flag di capability — per dettagli (queryability, formati, stili) leggi
108+
il GetCapabilities nativo.
109+
110+
## Esempio reale: catasto Agenzia delle Entrate
111+
112+
Servizio nazionale (copre tutta Italia, isole comprese). Scheda RNDT:
113+
`openrndt get age:consultazione_catasto_wms`.
114+
115+
- **WMS**: `https://wms.cartografia.agenziaentrate.gov.it/inspire/wms/ows01.php`
116+
— 11 layer; gli edifici sono il layer **`fabbricati`** (titolo "Fabbricati"),
117+
NON `BU.Building`. Particelle = `CP.CadastralParcel`, mappe/zone =
118+
`CP.CadastralZoning`.
119+
- **WFS**: `https://wfs.cartografia.agenziaentrate.gov.it/inspire/wfs/owfs01.php`
120+
— solo 2 feature type: `CP:CadastralParcel` e `CP:CadastralZoning`. **I
121+
fabbricati NON sono nel WFS**: edifici disponibili solo come WMS (raster).
122+
- **Interrogabili (GetFeatureInfo)**: solo `Cartografia_Catastale`,
123+
`CP.CadastralZoning`, `CP.CadastralParcel`. `fabbricati` **non** è queryable.

src/openrndt/cli.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from openrndt import codelists, config, output
1212
from openrndt.item import ItemNotFoundError, get_item, get_item_html, get_item_xml
13+
from openrndt.search import compact_results
1314
from openrndt.search import search as do_search
1415

1516
app = typer.Typer(
@@ -35,7 +36,7 @@ def _root(
3536
"json",
3637
"--format",
3738
"-F",
38-
help="Formato di output: json (default), table, csv.",
39+
help="Formato di output: json (default), table, csv, compact (NDJSON per agenti, solo per search).",
3940
case_sensitive=False,
4041
),
4142
) -> None:
@@ -131,15 +132,23 @@ def search(
131132
if not isinstance(payload, dict):
132133
typer.echo("Risposta RNDT inattesa (non è un oggetto JSON).", err=True)
133134
raise typer.Exit(1)
134-
if output.get_mode() == "json":
135+
mode = output.get_mode()
136+
if mode == "json":
135137
output.emit(payload)
136-
else:
137-
rows = _result_rows(payload)
138+
return
139+
if mode == "compact":
140+
rows = compact_results(payload)
138141
if not rows:
139142
typer.echo("Nessun risultato per la ricerca.", err=True)
140143
return
141-
title = f"RNDT — {payload.get('num', len(rows))} di {payload.get('total', '?')}"
142-
output.emit(payload, table_rows=rows, table_title=title)
144+
output.emit(payload, table_rows=rows)
145+
return
146+
rows = _result_rows(payload)
147+
if not rows:
148+
typer.echo("Nessun risultato per la ricerca.", err=True)
149+
return
150+
title = f"RNDT — {payload.get('num', len(rows))} di {payload.get('total', '?')}"
151+
output.emit(payload, table_rows=rows, table_title=title)
143152

144153

145154
@app.command()
@@ -151,7 +160,7 @@ def get(
151160
"""Recupera il dettaglio di un singolo metadato."""
152161
if as_xml and as_html:
153162
raise typer.BadParameter("Specifica --xml oppure --html, non entrambi.")
154-
if not (as_xml or as_html) and output.get_mode() == "csv":
163+
if not (as_xml or as_html) and output.get_mode() in {"csv", "compact"}:
155164
typer.echo(
156165
"Il dettaglio di un metadato non è tabellare: usa --format json (default) o table.",
157166
err=True,

src/openrndt/output.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Output dispatcher: json | table | csv."""
1+
"""Output dispatcher: json | table | csv | compact."""
22

33
from __future__ import annotations
44

@@ -17,7 +17,7 @@
1717

1818
def set_mode(mode: str) -> None:
1919
global _output_mode
20-
if mode not in {"json", "table", "csv"}:
20+
if mode not in {"json", "table", "csv", "compact"}:
2121
raise ValueError(f"Formato non supportato: {mode}")
2222
_output_mode = mode
2323

@@ -31,12 +31,19 @@ def emit(data: Any, *, table_rows: Iterable[dict[str, Any]] | None = None, table
3131
3232
- `json`: serializza `data` con indentazione.
3333
- `table`: usa `table_rows` (lista di dict piatti) se fornita, altrimenti pretty-print del JSON.
34-
- `csv`: scrive `table_rows` se fornita, altrimenti errore.
34+
- `csv`: scrive `table_rows` se fornita, altrimenti output vuoto.
35+
- `compact`: scrive `table_rows` come NDJSON (una riga JSON per record).
3536
"""
3637
if _output_mode == "json":
3738
sys.stdout.write(json.dumps(data, ensure_ascii=False, indent=2) + "\n")
3839
return
3940

41+
if _output_mode == "compact":
42+
rows = list(table_rows) if table_rows is not None else []
43+
for row in rows:
44+
sys.stdout.write(json.dumps(row, ensure_ascii=False) + "\n")
45+
return
46+
4047
if _output_mode == "table":
4148
if table_rows is None:
4249
_console.print_json(data=data)

0 commit comments

Comments
 (0)