gateway/docs/real-estate-feature-integration-guide/02-datamodels.md

34 KiB

Schritt 1: Datenmodell erstellen

← Zurück zur Übersicht | Weiter: Interface erstellen →

Real Estate-Datenmodelle

Datei: modules/datamodels/datamodelRealEstate.py

Das Feature arbeitet stateless ohne Session-Management. Die Datenmodelle definieren die Struktur der Real Estate-Entitäten, die über die API verwaltet werden können.

Hinweis: Die Real Estate-Datenmodell-Entitäten (Projekt, Parzelle, Dokument, etc.) werden in datamodelRealEstate.py definiert. Diese werden direkt über CRUD-Operationen verwaltet, ohne zusätzliche Chat-Interface-Modelle.

Warum keine Chat-Interface-Modelle?

Das Feature arbeitet stateless ohne Session-Management. Alle Operationen arbeiten direkt auf den Real Estate-Datenmodellen:

Stateless Design

  • Keine Session-Modelle: Keine RealEstateChatSession notwendig
  • Keine Query-History: Queries werden nicht gespeichert (kann optional später hinzugefügt werden)
  • Direkte CRUD-Operationen: User-Input → AI-Analyse → CRUD → Ergebnis
  • Einfache Architektur: Weniger Komplexität, bessere Performance

Real Estate-Modelle

Die Real Estate-Modelle (Projekt, Parzelle, Dokument, etc.):

  • Repräsentieren die tatsächlichen Geschäftsdaten der Immobilien-Projekte
  • Werden über lange Zeiträume gepflegt und verändert
  • Haben komplexe Beziehungen zueinander (Projekt → Parzellen → Dokumente)
  • Werden direkt über CRUD-Operationen verwaltet

Datenfluss (stateless)

User Input (natürliche Sprache)
    ↓
AI-Analyse (Intent-Erkennung)
    ↓
CRUD-Operation identifizieren
    ↓
Real Estate-Modelle
    ↓
Datenbank-Operation
    ↓
Ergebnis zurückgeben
    (keine Session, keine History)

Stateless vs. Session-basiert

Real Estate Feature (stateless):

  • Direkte CRUD-Operationen auf Real Estate-Modellen
  • Keine Session-Modelle notwendig
  • Keine Query-History
  • Einfacher und schneller

Chat-System (session-basiert):

  • Verwendet ChatWorkflow für komplexe AI-Workflows
  • Verwendet ChatDocument für Datei-Verknüpfungen
  • Session-Management für Multi-Step-Operationen
  • Für komplexe Workflows mit Planung und Review

Unterschied:

  • Real Estate Feature ist für einfache CRUD-Operationen optimiert
  • Chat-System ist für komplexe AI-Workflows optimiert
  • Beide können parallel existieren und für verschiedene Use Cases genutzt werden

Warum nicht das bestehende ChatWorkflow verwenden?

Sie fragen sich vielleicht: Kann ich nicht einfach das bestehende ChatWorkflow aus datamodelChat.py verwenden?

Die kurze Antwort: Für stateless CRUD-Operationen ist ChatWorkflow zu komplex. Das Real Estate Feature arbeitet ohne Session-Management und nutzt direkt die Real Estate-Modelle.

Unterschiedliche Anwendungsfälle

Aspekt ChatWorkflow (bestehend) Real Estate Feature (stateless)
Zweck Komplexe AI-gesteuerte Workflows mit mehreren Tasks/Actions Einfache CRUD-Operationen
Komplexität Hoch: Tasks, Actions, Rounds, Workflow-Modi, Retries Niedrig: Direkte CRUD-Operationen
Session Session-Management für Multi-Step-Workflows Keine Session, stateless
Verarbeitung Multi-Step AI-Workflows mit Planung, Review, Iteration Direkte CRUD: User-Input → AI-Analyse → CRUD → Ergebnis
Ergebnisse ChatMessage mit documents, ActionResult Direkte CRUD-Ergebnisse (Projekt, Parzelle, etc.)

Warum ChatWorkflow nicht passt:

  1. Zu komplex: ChatWorkflow hat viele Felder, die für einfache CRUD-Operationen nicht relevant sind
  2. Session-basiert: ChatWorkflow benötigt Session-Management, das wir nicht brauchen
  3. Falsches Abstraktionsniveau: ChatWorkflow ist für komplexe AI-Workflows, Real Estate braucht einfache CRUD-Operationen

Die richtige Lösung: Direkte CRUD-Operationen

Stattdessen arbeiten wir direkt mit den Real Estate-Modellen:

# Stateless CRUD-Operationen
User Input  AI-Analyse  CRUD-Operation  Ergebnis
# Keine Session, keine History, einfach und schnell

Wann könnte man ChatWorkflow verwenden?

Sie könnten ChatWorkflow verwenden, wenn Sie:

  • Komplexe AI-Workflows für Real Estate implementieren wollen (z.B. "Analysiere alle Projekte und erstelle einen Bericht")
  • Multi-Step-Verarbeitung benötigen (z.B. "Lade Daten → Transformiere → Erstelle Visualisierung")
  • Planung und Review brauchen (z.B. "Prüfe alle Parzellen auf Konformität")

Aber für einfache CRUD-Operationen ist der stateless Ansatz die bessere Wahl.


Real Estate-Datenmodell-Implementierung:

Die Real Estate-Datenmodell-Entitäten müssen separat in modules/datamodels/datamodelRealEstate.py implementiert werden. Siehe ../PEK_datamodel_desc.md für die vollständige Spezifikation aller Felder und Beziehungen (PEK ist ein Beispiel für eine Real Estate-Firma, das Modell ist aber allgemein verwendbar).

Wichtige Hinweise zum Datenmodell

Objektmodell vs. Datenbank-Repräsentation:

Dieses Dokument beschreibt ein Objektmodell für die Arbeit im Code. Es handelt sich NICHT um ein relationales Datenbankmodell mit Junction Tables.

  • Im Code-Modell: Alle Beziehungen werden als Objektreferenzen oder Listen von Objekten dargestellt (z.B. dokumente: list[Dokument], parzellen: list[Parzelle]).
  • Für die Datenbank-Serialisierung: Bei der Persistierung können Junction Tables verwendet werden, um n:m-Beziehungen in der Datenbank abzubilden. Dies ist jedoch ein Implementierungsdetail der Datenbank-Schicht und gehört nicht zum Hauptmodell.

Systemattribute:

Alle Datenobjekte haben automatisch die folgenden Systemattribute:

  • _createdAt: Float (Timestamp UTC)
  • _createdBy: String (User-ID)
  • _modifiedAt: Float (Timestamp UTC)
  • _modifiedBy: String (User-ID)

Timestamps:

  • Alle Timestamps sind im Float-Format UTC im Datenmodell gespeichert.
  • Die Darstellung im UI erfolgt mit der lokalen Zeitzone des Benutzers.

Wichtige Punkte für die Implementierung:

  • Objektbeziehungen wie parzellen: list[Parzelle] werden als JSONB in PostgreSQL gespeichert
  • Einzelne Objektreferenzen wie kontextKanton: Optional[str] werden als String-ID (Foreign Key) gespeichert
  • Administrative Hierarchie: Kanton benötigt id_land (Foreign Key zu Land), Gemeinde benötigt id_kanton (Foreign Key zu Kanton)
  • Alle Entitäten benötigen mandateId für Mandaten-Isolation
  • Systemattribute (_createdAt, _createdBy, etc.) werden automatisch vom DatabaseConnector hinzugefügt

Datenfluss-Diagramm

---
title: Hauptflüsse - Architektur-Planungs-App
---
flowchart LR
    subgraph Admin[Administrative Ebene]
        Land[Land<br/>Schweiz]
        Kanton[Kanton<br/>z.B. Zürich]
        Gemeinde[Gemeinde<br/>z.B. Zürich Stadt]
        Land --> Kanton
        Kanton --> Gemeinde
    end

    subgraph Geo[Geografische Daten]
        GeoPolylinie[GeoPolylinie<br/>Linie/Polygon]
        GeoPunkt[GeoPunkt<br/>Koordinaten]
        GeoPolylinie --> GeoPunkt
    end

    subgraph Core[Kern-Business-Logik]
        Projekt[Projekt<br/>Bauprojekt]
        Parzelle[Parzelle<br/>Grundstück mit<br/>Bauparametern]
        Gemeinde --> Parzelle
        Projekt --> Parzelle
        Projekt --> GeoPolylinie
        Parzelle --> GeoPolylinie
    end

    subgraph Support[Unterstützende Daten]
        Dokument[Dokument<br/>Dateien & URLs]
        Kontext[Kontext<br/>Zusatzinfos]
    end

Übersichtstabelle aller Entitäten

Objekt Typ Beschreibung Hauptfelder
Projekt Kernentität Bauprojekt mit Status und Perimeter id, label, statusProzess, perimeter, baulinie, parzellen
Parzelle Hauptentität Grundstück mit Bauparametern id, label, plz, bauzone, AZ, BZ, perimeter, baulinie, laermschutzzone, hochwasserschutzzone, grundwasserschutzzone
Dokument Unterstützend Dateien und URLs mit Versionierung id, label, dokumentTyp, quelle, mimeType, kategorienTags
Kontext Unterstützend Flexible Zusatzinformationen id, thema, inhalt
GeoPolylinie Hilfsobjekt Geometrische Linie/Polygon id, closed, punkte
Land Admin Nationale Ebene id, label, abk
Kanton Admin Kantonale Ebene mit Baurecht id, label, id_land, abk
Gemeinde Admin Gemeinde-Ebene mit BZO id, label, id_kanton, plz
GeoPunkt Hilfsobjekt 3D-Koordinate koordinatensystem, x, y, z, referenz
GeoTag Enum Geopunkt-Kategorien K1, K2, K3, Geometer
JaNein Enum Drei-wertiger Status "", "Ja", "Nein"
StatusProzess Enum Projektstatus 7 Werte (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
DokumentTyp Enum Dokumenttyp 6 Werte (kantonBaureglementAktuell, kantonBaureglementRevision, etc.)

Beispiel: Vollständige Pydantic-Modelle für Real Estate-Entitäten

Hier ist ein Beispiel, wie die Pydantic-Modelle für die Real Estate-Entitäten aussehen sollten:

"""
Real Estate data models for Architektur-Planungs-App.
Implements a general Swiss architecture planning data model.
(PEK is one example implementation, but the model is general-purpose)
"""

from typing import List, Dict, Any, Optional, ForwardRef
from enum import Enum
from pydantic import BaseModel, Field
from modules.shared.attributeUtils import registerModelLabels
from modules.shared.timeUtils import getUtcTimestamp
import uuid

# ===== Enums =====

class StatusProzess(str, Enum):
    """Project process status"""
    EINGANG = "Eingang"
    ANALYSE = "Analyse"
    STUDIE = "Studie"
    PLANUNG = "Planung"
    BAURECHTSVERFAHREN = "Baurechtsverfahren"
    UMSETZUNG = "Umsetzung"
    ARCHIV = "Archiv"


class DokumentTyp(str, Enum):
    """Document type for categorization"""
    KANTON_BAUREGLEMENT_AKTUELL = "kantonBaureglementAktuell"
    KANTON_BAUREGLEMENT_REVISION = "kantonBaureglementRevision"
    KANTON_BAUVERORDNUNG_AKTUELL = "kantonBauverordnungAktuell"
    KANTON_BAUVERORDNUNG_REVISION = "kantonBauverordnungRevision"
    GEMEINDE_BZO_AKTUELL = "gemeindeBzoAktuell"
    GEMEINDE_BZO_REVISION = "gemeindeBzoRevision"


class JaNein(str, Enum):
    """Three-valued state for optional yes/no questions"""
    UNBEKANNT = ""  # Empty string for unknown/not captured
    JA = "Ja"
    NEIN = "Nein"


class GeoTag(str, Enum):
    """Geopoint categories"""
    K1 = "K1"  # Fixpunkt höchster Genauigkeit
    K2 = "K2"  # Fixpunkt mittlerer Genauigkeit
    K3 = "K3"  # Fixpunkt niedriger Genauigkeit
    GEOMETER = "Geometer"  # Vom Geometer vermessener Punkt


# ===== Helper Models (must be defined before main models) =====

class GeoPunkt(BaseModel):
    """Represents a 3D point with reference."""
    koordinatensystem: str = Field(
        description="Coordinate system (e.g. 'LV95', 'EPSG:2056')",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=True,
    )
    x: float = Field(
        description="East value (E) [m], typically 2'480'000 - 2'840'000",
        frontend_type="number",
        frontend_readonly=False,
        frontend_required=True,
    )
    y: float = Field(
        description="North value (N) [m], typically 1'070'000 - 1'300'000",
        frontend_type="number",
        frontend_readonly=False,
        frontend_required=True,
    )
    z: Optional[float] = Field(
        None,
        description="Height above sea level [m]",
        frontend_type="number",
        frontend_readonly=False,
        frontend_required=False,
    )
    referenz: Optional[GeoTag] = Field(
        None,
        description="Point categorization",
        frontend_type="select",
        frontend_readonly=False,
        frontend_required=False,
    )


class GeoPolylinie(BaseModel):
    """Represents a line or polygon from multiple GeoPunkte."""
    id: str = Field(
        default_factory=lambda: str(uuid.uuid4()),
        description="Primary key",
    )
    closed: bool = Field(
        description="Is the GeoPolylinie closed (polygon)?",
        frontend_type="boolean",
        frontend_readonly=False,
        frontend_required=True,
    )
    punkte: List[GeoPunkt] = Field(
        default_factory=list,
        description="List of GeoPunkte forming the GeoPolylinie",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=True,
    )


class Dokument(BaseModel):
    """Supporting data object for file and URL management with versioning."""
    id: str = Field(
        default_factory=lambda: str(uuid.uuid4()),
        description="Primary key",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    mandateId: str = Field(
        description="ID of the mandate this document belongs to",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    label: str = Field(
        description="Document label",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=True,
    )
    versionsbezeichnung: Optional[str] = Field(
        None,
        description="Version number or designation (e.g. 'v1.0', 'Rev. A')",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    dokumentTyp: Optional[DokumentTyp] = Field(
        None,
        description="Document type",
        frontend_type="select",
        frontend_readonly=False,
        frontend_required=False,
    )
    dokumentReferenz: str = Field(
        description="File path or URL",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=True,
    )
    quelle: Optional[str] = Field(
        None,
        description="Source of the document",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    mimeType: Optional[str] = Field(
        None,
        description="MIME type of the document (e.g. 'application/pdf', 'image/png')",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    kategorienTags: List[str] = Field(
        default_factory=list,
        description="Document categorization tags",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )


# Beispiel-Kategorien für Dokumente (nicht abschließend):
# - "Kataster Objekte" - Amtliche Vermessung
# - "Kataster Werkeleitungen" - Leitungskataster
# - "Kataster Belastete Standorte" - Altlasten
# - "Kataster Bäume" - Baumkataster
# - "Zonenplan" - Zonenpläne
# - "Planungs- und Baugesetz (PGB)" - Kantonale Baugesetze
# - "Bau- und Zonenordnung (BZO)" - Gemeinde BZO
# - "Parkplatzverordnung" - Parkplatzregelungen
# - "Eigentümerauskunft" - Grundbuch-Auszüge Eigentümer
# - "Grundbuchauszug" - Vollständige Grundbuch-Auszüge
# - "Bauherrschaft" - Dokumente von der Bauherrschaft
# - "Planung" - Planungsdokumente
#
# Hinweis: Aktuelle Dokumente (z.B. aktuelle Baureglemente, BZO) können anhand des
# `dokumentTyp`-Attributs identifiziert werden. Die entsprechenden Dokumente finden sich
# in der `dokumente`-Liste der jeweiligen Entität (Kanton, Gemeinde).


class Kontext(BaseModel):
    """Supporting data object for flexible additional information."""
    id: str = Field(
        default_factory=lambda: str(uuid.uuid4()),
        description="Primary key",
    )
    thema: str = Field(
        description="Theme designation",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=True,
    )
    inhalt: str = Field(
        description="Detailed information (text)",
        frontend_type="textarea",
        frontend_readonly=False,
        frontend_required=True,
    )


# Beispielthemen für Kontext (nicht abschließend):
# - "Nutzung" - Vorgaben zur Erdgeschossnutzung (Wohnen erlaubt oder Pflicht für Gewerbe)
# - "Rechte" - Dienstbarkeiten (Wegrechte, Nähebaurechte, etc.)
# - "Parkierung" - Anforderung Parkplätze (Berechnung / Reduktionsfaktoren)
# - "Ausnützung" - Ausnützungsübertragungen
# - "Umwelt" - Schadstoffbelastungen auf Parzellen
# - "Planung" - Aktive Gestaltungspläne
# - "Lärm" - Lärmempfindlichkeitsstufen
# - "Energie" - Mögliche Wärmenutzung (Wärmeverbundnetze; Fernwärme, Anergie)
# - "Natur" - Baumbestand auf privaten Grundstücken
# - "Schutz" - Isos (Ortsbild, Schutzstatus, Denkmalschutz, Weilergebiet, etc.)
# - "Gefahren" - Naturgefahren (z.B. Objektschutzmassnahmen (Hochwasser))
# - "Revision" - Verweis auf aktuell in oder zukünftig in Revision befindlichen Normen/Gesetze
#
# Verwendung: Kontext-Objekte werden als Listen in den jeweiligen Entitäten gespeichert:
# - projekt.kontextInformationen: list[Kontext]
# - parzelle.kontextInformationen: list[Kontext]
# - land.kontextInformationen: list[Kontext]
# - kanton.kontextInformationen: list[Kontext]
# - gemeinde.kontextInformationen: list[Kontext]
#
# Design-Rationale: Das Kontext-Objekt ermöglicht flexibles Hinzufügen von projektspezifischen,
# parzellen-spezifischen oder regionalen Informationen ohne Schemaänderungen.


class Land(BaseModel):
    """National level administrative entity."""
    id: str = Field(
        default_factory=lambda: str(uuid.uuid4()),
        description="Primary key",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    mandateId: str = Field(
        description="ID of the mandate",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    label: str = Field(
        description="Country name (e.g. 'Schweiz')",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=True,
    )
    abk: Optional[str] = Field(
        None,
        description="Abbreviation (e.g. 'CH')",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    dokumente: List[Dokument] = Field(
        default_factory=list,
        description="National laws/documents",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    kontextInformationen: List[Kontext] = Field(
        default_factory=list,
        description="National context information",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )


class Kanton(BaseModel):
    """Cantonal level administrative entity."""
    id: str = Field(
        default_factory=lambda: str(uuid.uuid4()),
        description="Primary key",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    mandateId: str = Field(
        description="ID of the mandate",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    label: str = Field(
        description="Canton name (e.g. 'Zürich')",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=True,
    )
    id_land: Optional[str] = Field(
        None,
        description="Land ID (Foreign Key) - eindeutiger Link zum Land, in welchem Land der Kanton liegt",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    abk: Optional[str] = Field(
        None,
        description="Abbreviation (e.g. 'ZH')",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    dokumente: List[Dokument] = Field(
        default_factory=list,
        description="Cantonal documents",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    kontextInformationen: List[Kontext] = Field(
        default_factory=list,
        description="Canton-specific context information",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )


class Gemeinde(BaseModel):
    """Municipal level administrative entity."""
    id: str = Field(
        default_factory=lambda: str(uuid.uuid4()),
        description="Primary key",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    mandateId: str = Field(
        description="ID of the mandate",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    label: str = Field(
        description="Municipality name (e.g. 'Zürich')",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=True,
    )
    id_kanton: Optional[str] = Field(
        None,
        description="Kanton ID (Foreign Key) - eindeutiger Link zum Kanton, in welchem Kanton die Gemeinde liegt",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    plz: Optional[str] = Field(
        None,
        description="Postal code (for municipalities with multiple PLZ, this can be a main PLZ). Bei Gemeinden mit mehreren Postleitzahlen wird die konkrete PLZ der Parzelle im Attribut `plz` der Parzelle erfasst.",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    dokumente: List[Dokument] = Field(
        default_factory=list,
        description="Municipal documents",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    kontextInformationen: List[Kontext] = Field(
        default_factory=list,
        description="Municipality-specific context information",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )


# ===== Main Models (use ForwardRef for circular references) =====

# Forward references for circular dependencies
ParzelleRef = ForwardRef('Parzelle')


class Parzelle(BaseModel):
    """Represents a plot with all building law properties."""
    id: str = Field(
        default_factory=lambda: str(uuid.uuid4()),
        description="Primary key",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    mandateId: str = Field(
        description="ID of the mandate",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    
    # Grunddaten
    label: str = Field(
        description="Plot designation",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=True,
    )
    parzellenAliasTags: List[str] = Field(
        default_factory=list,
        description="Additional plot names or field names",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    eigentuemerschaft: Optional[str] = Field(
        None,
        description="Owner of the plot",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    strasseNr: Optional[str] = Field(
        None,
        description="Street and house number",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    plz: Optional[str] = Field(
        None,
        description="Postal code of the plot",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    
    # Geografischer Kontext
    perimeter: Optional[GeoPolylinie] = Field(
        None,
        description="Plot boundary as closed GeoPolylinie",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    baulinie: Optional[GeoPolylinie] = Field(
        None,
        description="Building line of the plot",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    kontextLand: Optional[str] = Field(
        None,
        description="Land ID (Foreign Key)",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    kontextKanton: Optional[str] = Field(
        None,
        description="Canton ID (Foreign Key)",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    kontextGemeinde: Optional[str] = Field(
        None,
        description="Municipality ID (Foreign Key)",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    
    # Bebauungsparameter
    bauzone: Optional[str] = Field(
        None,
        description="Building zone designation (e.g. W3, WG2, etc.)",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    az: Optional[float] = Field(
        None,
        description="Ausnützungsziffer",
        frontend_type="number",
        frontend_readonly=False,
        frontend_required=False,
    )
    bz: Optional[float] = Field(
        None,
        description="Bebauungsziffer",
        frontend_type="number",
        frontend_readonly=False,
        frontend_required=False,
    )
    vollgeschossZahl: Optional[int] = Field(
        None,
        description="Number of allowed full floors",
        frontend_type="number",
        frontend_readonly=False,
        frontend_required=False,
    )
    anrechenbarDachgeschoss: Optional[float] = Field(
        None,
        description="Accountable portion of attic (0.0 - 1.0)",
        frontend_type="number",
        frontend_readonly=False,
        frontend_required=False,
    )
    anrechenbarUntergeschoss: Optional[float] = Field(
        None,
        description="Accountable portion of basement (0.0 - 1.0)",
        frontend_type="number",
        frontend_readonly=False,
        frontend_required=False,
    )
    gebaeudehoeheMax: Optional[float] = Field(
        None,
        description="Maximum building height in meters",
        frontend_type="number",
        frontend_readonly=False,
        frontend_required=False,
    )
    
    # Abstandsregelungen
    regelnGrenzabstand: List[str] = Field(
        default_factory=list,
        description="Regulations for boundary distance",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    regelnMehrlaengenzuschlag: List[str] = Field(
        default_factory=list,
        description="Regulations for additional length surcharge",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    regelnMehrhoehenzuschlag: List[str] = Field(
        default_factory=list,
        description="Regulations for additional height surcharge",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    
    # Eigenschaften (Ja/Nein)
    parzelleBebaut: Optional[JaNein] = Field(
        None,
        description="Is the plot built?",
        frontend_type="select",
        frontend_readonly=False,
        frontend_required=False,
    )
    parzelleErschlossen: Optional[JaNein] = Field(
        None,
        description="Is the plot developed?",
        frontend_type="select",
        frontend_readonly=False,
        frontend_required=False,
    )
    parzelleHanglage: Optional[JaNein] = Field(
        None,
        description="Is the plot on a slope?",
        frontend_type="select",
        frontend_readonly=False,
        frontend_required=False,
    )
    
    # Schutzzonen
    laermschutzzone: Optional[str] = Field(
        None,
        description="Noise protection zone (e.g. 'II')",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    hochwasserschutzzone: Optional[str] = Field(
        None,
        description="Flood protection zone (e.g. 'tief')",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    grundwasserschutzzone: Optional[str] = Field(
        None,
        description="Groundwater protection zone",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=False,
    )
    
    # Beziehungen (stored as JSONB in database)
    parzellenNachbarschaft: List[Dict[str, Any]] = Field(
        default_factory=list,
        description="Neighboring plots (stored as list of Parzelle IDs or full objects)",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    dokumente: List[Dokument] = Field(
        default_factory=list,
        description="Plot-specific documents",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    kontextInformationen: List[Kontext] = Field(
        default_factory=list,
        description="Plot-specific context information",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )


class Projekt(BaseModel):
    """Core object representing a construction project."""
    id: str = Field(
        default_factory=lambda: str(uuid.uuid4()),
        description="Primary key",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    mandateId: str = Field(
        description="ID of the mandate",
        frontend_type="text",
        frontend_readonly=True,
        frontend_required=False,
    )
    label: str = Field(
        description="Project designation",
        frontend_type="text",
        frontend_readonly=False,
        frontend_required=True,
    )
    statusProzess: Optional[StatusProzess] = Field(
        None,
        description="Project status",
        frontend_type="select",
        frontend_readonly=False,
        frontend_required=False,
    )
    perimeter: Optional[GeoPolylinie] = Field(
        None,
        description="Envelope of all plots in the project",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    baulinie: Optional[GeoPolylinie] = Field(
        None,
        description="Building line of the project",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    parzellen: List[Parzelle] = Field(
        default_factory=list,
        description="All plots of the project",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    dokumente: List[Dokument] = Field(
        default_factory=list,
        description="Project-specific documents",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )
    kontextInformationen: List[Kontext] = Field(
        default_factory=list,
        description="Project-specific context information",
        frontend_type="json",
        frontend_readonly=False,
        frontend_required=False,
    )


# Resolve forward references
Parzelle.model_rebuild()
Projekt.model_rebuild()


# Register labels for frontend
registerModelLabels(
    "Projekt",
    {"en": "Project", "fr": "Projet", "de": "Projekt"},
    {
        "id": {"en": "ID", "fr": "ID", "de": "ID"},
        "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
        "statusProzess": {"en": "Process Status", "fr": "Statut du processus", "de": "Prozessstatus"},
        # ... more labels
    },
)

# Similar registerModelLabels calls for all other models...

Wichtige Hinweise zur Implementierung:

  1. Forward References: Für zirkuläre Referenzen (z.B. ParzelleparzellenNachbarschaft: list[Parzelle]) verwenden Sie ForwardRef oder speichern Sie nur IDs als Strings.

  2. JSONB-Speicherung: Listen von Objekten (list[Parzelle], list[Dokument]) werden automatisch als JSONB gespeichert. Der DatabaseConnector erkennt List-Typen automatisch.

  3. Foreign Keys: Einzelne Objektreferenzen wie kontextKanton: Optional[str] werden als String-ID gespeichert. Sie können später im Interface die vollständigen Objekte laden.

  4. MandateId: Alle Entitäten benötigen mandateId für Mandaten-Isolation.

  5. Systemattribute: _createdAt, _createdBy, _modifiedAt, _modifiedBy werden automatisch vom DatabaseConnector hinzugefügt - Sie müssen sie nicht im Modell definieren.


WICHTIG: Die obigen Real Estate-Modelle (Projekt, Parzelle, etc.) sind die tatsächlichen Datenmodelle, die Sie implementieren müssen. Diese werden in modules/datamodels/datamodelRealEstate.py erstellt.

Keine Chat-Interface-Modelle notwendig:

  • Das Feature arbeitet stateless ohne Session-Management
  • Alle Operationen arbeiten direkt auf den Real Estate-Modellen
  • Keine RealEstateChatSession, RealEstateQuery oder RealEstateQueryResult notwendig
  • CRUD-Operationen werden direkt ausgeführt und Ergebnisse direkt zurückgegeben

Wichtige Punkte:

  1. UUID als ID: Alle Modelle verwenden uuid.uuid4() für eindeutige IDs
  2. MandateId: Jedes Modell benötigt mandateId für Mandaten-Isolation
  3. Frontend-Metadaten: frontend_type, frontend_readonly, frontend_required für UI-Generierung
  4. registerModelLabels: Registriert Labels für Mehrsprachigkeit
  5. JSONB-Felder: Dict[str, Any] und List[...] werden automatisch als JSONB in PostgreSQL gespeichert
  6. Foreign Keys: Administrative Hierarchie wird über Foreign Keys abgebildet:
    • Kanton.id_landLand.id
    • Gemeinde.id_kantonKanton.id
    • Parzelle.kontextLandLand.id (Optional)
    • Parzelle.kontextKantonKanton.id (Optional)
    • Parzelle.kontextGemeindeGemeinde.id (Optional)

Q & A - Häufige Fragen

  1. Versionierung: Sollen Änderungen an Parzellen historisiert werden?
    → Vorerst nicht

  2. Mehrsprachigkeit: Labels in DE/FR/IT?
    → Wird im Pydantic Model über registerModelLabels umgesetzt

  3. Benutzer & Rollen: Wer darf was bearbeiten?
    → In der App über Roles und Permissions gesteuert (UAM-System)

  4. Workflow-Engine: Für Statusübergänge und Genehmigungen?
    → In der App über Workflow-Engine gesteuert (optional, kann später integriert werden)

  5. Integration: Anbindung an amtliche Geodaten (z.B. Swisstopo API)?
    → In der App über Integrationen gesteuert (optional)

  6. Berechnungen: Sollen Ausnützungsberechnungen automatisiert werden?
    → In der App über Berechnungen gesteuert (optional)


← Zurück zur Übersicht | Weiter: Interface erstellen →