Přeskočit obsah

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

  1. Schema review s Nette tém — feedback na typed sections + per-section schemas (~1 týden async)
  2. Backend implementuje POST /reports/{id}/ingest-json + _parse_via_json worker stage (~1-2 dny)
  3. Nette tým implementuje JSON generator (data already exists v Nette internal models, je to "render to JSON" exporter) — odhad ~3-5 dnů
  4. Parallel run — Nette posílá MHTML i JSON pro nové reporty, porovnáme chunks quality (~1 týden parallel)
  5. 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

  1. Locking guarantee: Posílá Nette report.locked=true až po přesměrování draft → finální? Nebo i pro draft?
  2. 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).
  3. Figure URL stability: Jsou Nette figure URL trvalé (perma-link)? Nebo se pravidelně obnovují (signed tokens with TTL)?
  4. Attachment privacy: Jsou attachment URL veřejné (HTTPS) nebo HMAC-protected? Backend potřebuje download access.
  5. Custom sekce: Pokud Nette přidá novou sekci kterou v SectionType enum nemáme (např. electromagnetic_radiation), použijte type: "other" + popisné name — backend si je naparsne, jen nebudou typed validation.
  6. 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"