Nette → NemoReport AI: Structured JSON ingestion spec¶
Status: Draft 1 (2026-05-07). Koncept k diskuzi s Nette tém. Cíl: Nahradit MHTML browser-save ingestion structured JSON, který Nette generuje přímo z svých zdrojových dat (před rendering do HTML).
Proč JSON místo MHTML¶
Problémy s MHTML cestou (current Phase B.10):
- Browser save dropuje JS-rendered content → Intermap mapy = broken/black PNGs
- BS4 + trafilatura parsing produkuje chunks s navigation noise (Prověřeno, Záznamy, individuální vyhodnocení, Oblast) namíchaným s reálnými daty
- Bug C (5.5.2026) ukázal že chunks tohoto typu AI interpretuje jako placeholder — workaround systém prompt instructions, ale clean structured input je lepší fix
- Mistral OCR latence ~30-60 s pro velké MHTML, JSON parsing < 1 s
Výhoda JSON cesty:
- Nette JE zdrojem pravdy (generuje data z katastr / DIBAVOD / CHKO / BPEJ APIs) — má strukturovaná data před rendering do HTML
- Chunks z JSON jdou clean: section_name="Záplavová území", content="Parcela 324/47 leží v zóně Q5 (5letá voda, vysoké riziko). Vzdálenost od aktivní zóny: 0 m." — žádný metadata noise
- Risk scoring + structured citations už máme (Nette interní)
Endpoint spec (backend bude přidat)¶
POST /reports/{report_id}/ingest-json
Headers:
X-Nette-HMAC: <hmac sha256>
X-Nette-ID: <nette numeric report id>
Content-Type: application/json
Body: structured JSON (schema viz níže)
Response 202 Accepted:
{ "report_id": "uuid", "status": "uploaded", "ingestion_job_id": "uuid" }
HMAC nad canonical string:
<report_id>|<nette_id>|json|<sha256(body bytes)>
Endpoint použije stejný HMAC pattern jako existing POST /reports/{id}/attachments/system (Phase B.11). Backend uloží JSON do Storage, zařadí worker job, parse přes nový _parse_via_json(target) cestu (vedle PDF / MHTML / DOCX).
JSON schema (TypeScript-style)¶
interface NemoReportV2Ingestion {
// ─── Top-level metadata ─────────────────────────────────────
report: {
nette_report_id: string; // "2026030310"
generated_at: string; // ISO 8601
locked: boolean; // true = final, false = WIP draft
schema_version: "v2-2026-05"; // version string
client: {
id: string; // Nette client UUID
name: string; // "Jan Novák" / "Realitní Kancelář XYZ"
};
};
// ─── Identifikace nemovitosti ──────────────────────────────
property: {
address: {
street: string; // "Dolanská 120"
city: string; // "Velké Přítočno"
postcode: string; // "273 51"
district?: string; // "Kladno"
region?: string; // "Středočeský kraj"
country: string; // "CZ"
gps?: { lat: number; lon: number };
};
parcels: Array<{
number: string; // "St. 128" / "324/47"
cadastre: string; // "Velké Přítočno"
cadastre_code?: string; // "777862" — Kód KÚ
area_m2: number;
type: string; // "stavební", "ostatní plocha", "orná půda", ...
}>;
list_of_ownership?: string; // "LV 164"
ownership_type?: string; // "individuální", "SJM", "SVJ"
};
// ─── Strukturované sekce reportu ───────────────────────────
sections: Array<ReportSection>;
// ─── Obrázky a mapy ─────────────────────────────────────────
figures: Array<Figure>;
// ─── Přílohy (vyjádření, posudky, fotodokumentace) ─────────
attachments?: Array<AttachmentRef>;
// ─── Souhrnné hodnocení ────────────────────────────────────
risk_summary?: {
overall_score: number; // 0–10 (10 = nejvyšší riziko)
highest_risks: SectionType[]; // např. ["flood_zones", "cadastre"]
recommendation: string; // Czech text, ~200 chars
};
}
Typed sections¶
type SectionType =
| "cadastre" // Katastr nemovitostí (vlastnictví, exekuce, zástavy)
| "land_use_plan" // Územní plán obce
| "flood_zones" // Záplavová území (Q5/Q20/Q100)
| "flood_risk" // Riziko povodní (pojistné riziko, ne hydrologické)
| "radon_index" // Radonový index (zóna 1/2/3)
| "noise" // Hluková zátěž (silnice, železnice, letecký)
| "protected_areas" // Chráněná území (CHKO, NP, EVL, PR)
| "mining_areas" // Poddolovaná území + důlní díla
| "water_bodies" // Vodní toky a plochy
| "civic_amenities" // Občanská vybavenost (školy, MHD, lékař)
| "soil_quality_bpej" // Kvalita zemědělské půdy
| "geology" // Geologické podloží (sesuv rizika, atd.)
| "summary" // Souhrnný přehled
| "other"; // Custom sekce s payloadem v `data`
interface ReportSection {
type: SectionType;
name: string; // user-facing label "Záplavová území"
order: number; // pořadí v reportu (pro figury / citace)
// Klíčové: human-readable summary CZ, 80–500 chars.
// AI ho použije jako primary chunk content (= replacement za parsed_markdown).
summary: string;
risk_level?: "none" | "low" | "medium" | "high" | "critical";
risk_score?: number; // 0–10 numeric (volitelné, kromě risk_level)
// Strukturovaná data sekce — typed per section.type (viz níže).
// Backend chunking bude tato data inlinovat do chunk content jako fact bullets.
data?: Record<string, unknown>;
// Citation source = co stojí za daty (DIBAVOD, ČÚZK, ČHMÚ, atd.)
citations?: Array<{
source: string; // "DIBAVOD", "ČÚZK", "ČHMÚ", "Geoportál CHMI", ...
url?: string; // permalink na zdrojový dataset
fetched_at?: string; // ISO 8601 — kdy Nette stáhla data z API
notes?: string;
}>;
}
Per-section data schemas (vybrané)¶
// flood_zones
interface FloodZonesData {
zones: Array<{
code: "Q5" | "Q20" | "Q100" | "active_zone";
label: string; // "5letá voda"
intersects: boolean; // protíná parcelu?
area_intersected_m2?: number;
}>;
active_zone_distance_m?: number; // 0 = parcela v aktivní zóně
water_flow?: string; // "Oleška"
water_authority?: string; // "Povodí Labe"
}
// radon_index
interface RadonIndexData {
index: 1 | 2 | 3;
category: "nízký" | "střední" | "vysoký";
geological_unit?: string;
source: "Geofond ČR" | "ČGS";
}
// cadastre
interface CadastreData {
ownership_records: Array<{
type:
| "zástavní_právo_smluvní"
| "zástavní_právo_zákonné"
| "zákaz_zcizení_a_zatížení"
| "exekuce"
| "věcné_břemeno"
| "předkupní_právo"
| "nájem"
| "other";
creditor?: string; // "Banka XYZ", "FÚ", "ČSSZ", ...
amount_czk?: number;
case_id?: string; // pro exekuce: spisová značka
notes?: string;
}>;
encumbered: boolean; // true pokud existuje aspoň 1 record
}
// civic_amenities
interface CivicAmenitiesData {
pois: Array<{
type:
| "school" | "kindergarten" | "hospital" | "doctor"
| "pharmacy" | "post_office" | "shop" | "supermarket"
| "public_transport_stop" | "train_station" | "highway_access"
| "restaurant" | "park" | "other";
name: string;
distance_m: number;
walking_time_min?: number;
}>;
}
// soil_quality_bpej (zemědělská půda)
interface BpejData {
primary_class: string; // "I." | "II." | "III." | "IV." | "V."
bpej_codes: string[]; // ["3.13.10", "3.13.40"]
protected: boolean; // I. a II. třída = ZPF chráněný
removal_fee_per_m2_czk?: number;
}
Figures + attachments¶
interface Figure {
id: string; // Nette figure UUID
type:
| "map_flood" | "map_zoning" | "map_radon" | "map_protected"
| "map_cadastre" | "map_noise" | "map_geology"
| "photo_property" | "photo_aerial" | "document_scan"
| "diagram" | "other";
section_type: SectionType; // links to ReportSection.type
url: string; // public Nette URL (HTTPS, signed token OK)
caption?: string; // Czech, ~50–200 chars
legend?: string; // Czech, popis legendy mapy
viewport?: { north: number; south: number; east: number; west: number };
scale?: string; // "1:5000"
alt_text?: string; // accessibility
}
interface AttachmentRef {
id: string;
type:
| "expert_opinion" // posudek
| "utility_statement" // vyjádření ČEZ, CETIN, atd.
| "geometric_plan" // geometrický plán
| "photo_dossier" // foto pozemku set
| "cadastre_excerpt" // výpis z KN
| "other";
filename: string;
url: string; // public Nette URL pro download
content_type: string; // MIME
summary?: string; // Czech, ~80–300 chars (Nette generates)
issued_at?: string; // ISO 8601 (datum vydání)
issuer?: string; // "ČEZ Distribuce, a.s."
}
Sample example (zkrácený)¶
{
"report": {
"nette_report_id": "2026030310",
"generated_at": "2026-05-07T10:00:00Z",
"locked": true,
"schema_version": "v2-2026-05",
"client": { "id": "uuid", "name": "Jan Novák" }
},
"property": {
"address": {
"street": "Dolanská 120",
"city": "Velké Přítočno",
"postcode": "273 51",
"country": "CZ"
},
"parcels": [
{ "number": "St. 128", "cadastre": "Velké Přítočno", "area_m2": 200.57, "type": "stavební" }
]
},
"sections": [
{
"type": "flood_zones",
"name": "Záplavová území",
"order": 3,
"summary": "Parcela St. 128 leží v zónách Q5, Q20 i Q100 toku Oleška. Vzdálenost od aktivní zóny: 0 m. Pojištění může být problém.",
"risk_level": "high",
"risk_score": 8.5,
"data": {
"zones": [
{ "code": "Q5", "label": "5letá voda", "intersects": true, "area_intersected_m2": 200.57 },
{ "code": "Q20", "label": "20letá voda", "intersects": true },
{ "code": "Q100", "label": "100letá voda", "intersects": true }
],
"active_zone_distance_m": 0,
"water_flow": "Oleška",
"water_authority": "Povodí Labe"
},
"citations": [
{ "source": "DIBAVOD", "url": "https://www.dibavod.cz/...", "fetched_at": "2026-05-07T09:42:00Z" }
]
},
{
"type": "radon_index",
"name": "Radonový index",
"order": 5,
"summary": "Střední radonový index (zóna 2). Při novostavbě je nutné protiradonové opatření.",
"risk_level": "medium",
"data": {
"index": 2,
"category": "střední",
"source": "Geofond ČR"
}
}
],
"figures": [
{
"id": "fig-uuid-1",
"type": "map_flood",
"section_type": "flood_zones",
"url": "https://nette.cz/figures/2026030310/flood.png",
"caption": "Mapa povodňových zón pro parcelu St. 128",
"legend": "Modrá Q5, fialová Q20, světle modrá Q100"
}
],
"risk_summary": {
"overall_score": 7.5,
"highest_risks": ["flood_zones"],
"recommendation": "Před nákupem ověřit u banky pojistitelnost domu v záplavové zóně. Bez pojištění hypotéka pravděpodobně nebude schválena."
}
}
Backend mapping (chunks generation)¶
Worker _parse_via_json(target) produkuje chunks takto:
| Chunk | Source | Content template |
|---|---|---|
| 1 chunk per section | sections[].summary + sections[].data flatten |
# {section.name}\n\n{summary}\n\n{flatten data jako bullet list} |
| 1 chunk per figure | figures[] + linked section |
# {figure.caption}\n\nTyp: {figure.type}\nSekce: {section.name}\n{legend} (multimodal embed s downloaded image) |
| 1 chunk per attachment | attachments[].summary |
# {attachment.filename}\n\n{summary}\n\nVydáno: {issued_at} {issuer} |
Klíčové vůči současnému MHTML chunking:
- ✅ Žádný nav noise (Prověřeno, Záznamy)
- ✅ Strukturovaná data inlined jako bullets (e.g. - Zóna Q5 (5letá voda): protíná parcelu, 200.57 m²)
- ✅ Risk level v každém chunku jako prefix → AI okamžitě vidí prioritu
- ✅ Citations v JSON metadata → chunks mají source_label automaticky správně
- ✅ Figures linked s sekcí → AI zná visual context
Migration path¶
- Schema review s Nette tém — feedback na typed sections + per-section schemas (~1 týden async)
- Backend implementuje
POST /reports/{id}/ingest-json+_parse_via_jsonworker stage (~1-2 dny) - Nette tým implementuje JSON generator (data already exists v Nette internal models, je to "render to JSON" exporter) — odhad ~3-5 dnů
- Parallel run — Nette posílá MHTML i JSON pro nové reporty, porovnáme chunks quality (~1 týden parallel)
- Cutover — MHTML cesta deprecated, jen JSON path. Existing 13 MHTML reportů se re-ingest pokud Nette dodá data, jinak zůstanou jako legacy.
Kdy: Po D-stabilizace fázi (Bug C fix + smoke sweep + edge cases). Sample schema design je tento dokument, ready pro handoff.
Open questions pro Nette tým¶
- Locking guarantee: Posílá Nette
report.locked=trueaž po přesměrování draft → finální? Nebo i pro draft? - Re-ingestion strategy: Pokud Nette upraví report (např. přidá novou výsledku z API), pošle nový JSON s stejným
nette_report_id? Backend by pak idempotently re-ingest (DELETE existing chunks + INSERT new). - Figure URL stability: Jsou Nette figure URL trvalé (perma-link)? Nebo se pravidelně obnovují (signed tokens with TTL)?
- Attachment privacy: Jsou attachment URL veřejné (HTTPS) nebo HMAC-protected? Backend potřebuje download access.
- Custom sekce: Pokud Nette přidá novou sekci kterou v
SectionTypeenum nemáme (např.electromagnetic_radiation), použijtetype: "other"+ popisnéname— backend si je naparsne, jen nebudou typed validation. - Versioning:
schema_version: "v2-2026-05"— když budeme měnit schema, posuneme verzi. Zachováme backward compat aspoň 1 verzi (= old reports stále parse-able).
Reference¶
- Existing Phase B.11 HMAC pattern:
app/routers/nette.py+docs/NETTE_INTEGRATION.md - Backend roadmap: viz CLAUDE.md sekce "Cross-cutting — MHTML → structured JSON ingestion path"