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
RealEstateChatSessionnotwendig - 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
ChatWorkflowfür komplexe AI-Workflows - Verwendet
ChatDocumentfü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:
- Zu komplex:
ChatWorkflowhat viele Felder, die für einfache CRUD-Operationen nicht relevant sind - Session-basiert:
ChatWorkflowbenötigt Session-Management, das wir nicht brauchen - Falsches Abstraktionsniveau:
ChatWorkflowist 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:
Kantonbenötigtid_land(Foreign Key zu Land),Gemeindebenötigtid_kanton(Foreign Key zu Kanton) - Alle Entitäten benötigen
mandateIdfü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:
-
Forward References: Für zirkuläre Referenzen (z.B.
Parzelle→parzellenNachbarschaft: list[Parzelle]) verwenden SieForwardRefoder speichern Sie nur IDs als Strings. -
JSONB-Speicherung: Listen von Objekten (
list[Parzelle],list[Dokument]) werden automatisch als JSONB gespeichert. Der DatabaseConnector erkenntList-Typen automatisch. -
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. -
MandateId: Alle Entitäten benötigen
mandateIdfür Mandaten-Isolation. -
Systemattribute:
_createdAt,_createdBy,_modifiedAt,_modifiedBywerden 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,RealEstateQueryoderRealEstateQueryResultnotwendig - CRUD-Operationen werden direkt ausgeführt und Ergebnisse direkt zurückgegeben
Wichtige Punkte:
- UUID als ID: Alle Modelle verwenden
uuid.uuid4()für eindeutige IDs - MandateId: Jedes Modell benötigt
mandateIdfür Mandaten-Isolation - Frontend-Metadaten:
frontend_type,frontend_readonly,frontend_requiredfür UI-Generierung - registerModelLabels: Registriert Labels für Mehrsprachigkeit
- JSONB-Felder:
Dict[str, Any]undList[...]werden automatisch als JSONB in PostgreSQL gespeichert - Foreign Keys: Administrative Hierarchie wird über Foreign Keys abgebildet:
Kanton.id_land→Land.idGemeinde.id_kanton→Kanton.idParzelle.kontextLand→Land.id(Optional)Parzelle.kontextKanton→Kanton.id(Optional)Parzelle.kontextGemeinde→Gemeinde.id(Optional)
Q & A - Häufige Fragen
-
Versionierung: Sollen Änderungen an Parzellen historisiert werden?
→ Vorerst nicht -
Mehrsprachigkeit: Labels in DE/FR/IT?
→ Wird im Pydantic Model überregisterModelLabelsumgesetzt -
Benutzer & Rollen: Wer darf was bearbeiten?
→ In der App über Roles und Permissions gesteuert (UAM-System) -
Workflow-Engine: Für Statusübergänge und Genehmigungen?
→ In der App über Workflow-Engine gesteuert (optional, kann später integriert werden) -
Integration: Anbindung an amtliche Geodaten (z.B. Swisstopo API)?
→ In der App über Integrationen gesteuert (optional) -
Berechnungen: Sollen Ausnützungsberechnungen automatisiert werden?
→ In der App über Berechnungen gesteuert (optional)