diff --git a/app.py b/app.py
index 8788b4ec..484a8b23 100644
--- a/app.py
+++ b/app.py
@@ -413,6 +413,9 @@ app.include_router(workflowRouter)
from modules.routes.routeChatPlayground import router as chatPlaygroundRouter
app.include_router(chatPlaygroundRouter)
+from modules.routes.routeRealEstate import router as realEstateRouter
+app.include_router(realEstateRouter)
+
from modules.routes.routeSecurityLocal import router as localRouter
app.include_router(localRouter)
diff --git a/docs/PEK_datamodel_desc.md b/docs/PEK_datamodel_desc.md
new file mode 100644
index 00000000..d2b94e5e
--- /dev/null
+++ b/docs/PEK_datamodel_desc.md
@@ -0,0 +1,418 @@
+# Datenmodell Architektur-Planungs-App
+
+## Übersicht
+
+Dieses Datenmodell bildet die Grundlage für eine Schweizer Architektur-Planungs-App zur Verwaltung von Bauprojekten, Parzellen, Dokumenten und regulatorischen Kontextinformationen.
+
+## 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.
+
+## Datenfluss-Diagramm
+
+```mermaid
+---
+title: Hauptflüsse - Architektur-Planungs-App
+---
+flowchart LR
+ subgraph Admin[Administrative Ebene]
+ Land[Land
Schweiz]
+ Kanton[Kanton
z.B. Zürich]
+ Gemeinde[Gemeinde
z.B. Zürich Stadt]
+ Land --> Kanton
+ Kanton --> Gemeinde
+ end
+
+ subgraph Geo[Geografische Daten]
+ GeoPolylinie[GeoPolylinie
Linie/Polygon]
+ GeoPunkt[GeoPunkt
Koordinaten]
+ GeoPolylinie --> GeoPunkt
+ end
+
+ subgraph Core[Kern-Business-Logik]
+ Projekt[Projekt
Bauprojekt]
+ Parzelle[Parzelle
Grundstück mit
Bauparametern]
+ Gemeinde --> Parzelle
+ Projekt --> Parzelle
+ Projekt --> GeoPolylinie
+ Parzelle --> GeoPolylinie
+ end
+
+ subgraph Support[Unterstützende Daten]
+ Dokument[Dokument
Dateien & URLs]
+ Kontext[Kontext
Zusatzinfos]
+ end
+
+ style Land fill:#50C878,stroke:#2D7A4A,stroke-width:2px,color:#fff
+ style Kanton fill:#50C878,stroke:#2D7A4A,stroke-width:2px,color:#fff
+ style Gemeinde fill:#50C878,stroke:#2D7A4A,stroke-width:2px,color:#fff
+ style Parzelle fill:#FF6B6B,stroke:#C92A2A,stroke-width:4px,color:#fff
+ style Projekt fill:#FF6B6B,stroke:#C92A2A,stroke-width:4px,color:#fff
+ style Dokument fill:#F5A623,stroke:#C17D11,stroke-width:2px,color:#fff
+ style GeoPolylinie fill:#F5A623,stroke:#C17D11,stroke-width:2px,color:#fff
+ style GeoPunkt fill:#F5A623,stroke:#C17D11,stroke-width:2px,color:#fff
+ style Kontext fill:#F5A623,stroke:#C17D11,stroke-width:2px,color:#fff
+```
+
+---
+
+## Alle Datenobjekte als Tabellen
+
+### Übersichtstabelle
+
+| 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, abk, Baureglement |
+| **Gemeinde** | Admin | Gemeinde-Ebene mit BZO | id, label, plz, BZO |
+| **GeoPunkt** | Hilfsobjekt | 3D-Koordinate | koordinatensystem, x, y, z, referenz |
+| **GeoTag** | Enum | Geopunkt-Kategorien | - |
+| **JaNein** | Enum | Drei-wertiger Status | "", "Ja", "Nein" |
+| **StatusProzess** | Enum | Projektstatus | 7 Werte |
+| **DokumentTyp** | Enum | Dokumenttyp | 6 Werte |
+
+---
+
+## Zentrale Entitäten
+
+### 1. Projekt
+**Das Kernobjekt, das ein Bauprojekt repräsentiert.**
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `id` | UUID | ✓ | Eindeutiger Identifier |
+| `label` | String | ✓ | Projektbezeichnung |
+| `statusProzess` | Enum[StatusProzess] | - | Projektstatus: Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv |
+| `perimeter` | GeoPolylinie | - | Umhüllende aller Parzellen des Projektes |
+| `baulinie` | GeoPolylinie | - | Baulinie des Projektes |
+| `parzellen` | list[Parzelle] | - | Alle Parzellen des Projektes |
+| `dokumente` | list[Dokument] | - | Projektspezifische Dokumente |
+| `kontextInformationen` | list[Kontext] | - | Projektspezifische Kontextinfos |
+
+---
+
+### 2. Parzelle
+**Repräsentiert ein Grundstück mit allen baurechtlichen Eigenschaften als ein einheitliches Objekt.**
+
+#### Grunddaten
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `id` | UUID | ✓ | Eindeutiger Identifier |
+| `label` | String | ✓ | Parzellenbezeichnung |
+| `parzellenAliasTags` | list[String] | - | Weitere Parzellennamen oder Flurnamen |
+| `eigentuemerschaft` | String | - | Eigentümer der Parzelle |
+| `strasseNr` | String | - | Straße und Hausnummer |
+| `plz` | String | - | Postleitzahl der Parzelle (insbesondere bei Gemeinden mit mehreren PLZ) |
+
+#### Geografischer Kontext
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `perimeter` | GeoPolylinie | - | Parzellengrenze als geschlossene GeoPolylinie |
+| `baulinie` | GeoPolylinie | - | Baulinie der Parzelle |
+| `kontextGemeinde` | Gemeinde | - | Gemeinde der Parzelle |
+
+#### Bebauungsparameter
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `bauzone` | String | - | Bauzonenbezeichnung (z.B. W3, WG2, etc.) |
+| `az` | Float | - | Ausnützungsziffer |
+| `bz` | Float | - | Bebauungsziffer |
+| `vollgeschossZahl` | Integer | - | Anzahl zulässiger Vollgeschosse |
+| `anrechenbarDachgeschoss` | Float | - | Anrechenbarer Anteil Dachgeschoss (0.0 - 1.0) |
+| `anrechenbarUntergeschoss` | Float | - | Anrechenbarer Anteil Untergeschoss (0.0 - 1.0) |
+| `gebaeudehoeheMax` | Float | - | Maximale Gebäudehöhe in Metern |
+
+#### Abstandsregelungen
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `regelnGrenzabstand` | list[String] | - | Regelungen zum Grenzabstand |
+| `regelnMehrlaengenzuschlag` | list[String] | - | Regelungen zum Mehrlängenzuschlag |
+| `regelnMehrhoehenzuschlag` | list[String] | - | Regelungen zum Mehrhöhenzuschlag |
+
+#### Eigenschaften (Ja/Nein)
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `parzelleBebaut` | JaNein | - | Ist die Parzelle bebaut? ("", "Ja", "Nein") |
+| `parzelleErschlossen` | JaNein | - | Ist die Parzelle erschlossen? ("", "Ja", "Nein") |
+| `parzelleHanglage` | JaNein | - | Liegt die Parzelle in Hanglage? ("", "Ja", "Nein") |
+
+#### Schutzzonen
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `laermschutzzone` | String | - | Lärmschutzzone (z.B. "II") |
+| `hochwasserschutzzone` | String | - | Hochwasserschutzzone (z.B. "tief") |
+| `grundwasserschutzzone` | String | - | Grundwasserschutzzone |
+
+#### Beziehungen
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `parzellenNachbarschaft` | list[Parzelle] | - | Nachbarparzellen |
+| `dokumente` | list[Dokument] | - | Parzellenspezifische Dokumente |
+| `kontextInformationen` | list[Kontext] | - | Parzellenspezifische Kontextinfos |
+
+---
+
+### 3. Dokument
+**Unterstützendes Datenobjekt zur Verwaltung von Dateien und URLs mit Versionierung.**
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `id` | UUID | ✓ | Eindeutiger Identifier |
+| `label` | String | ✓ | Dokumentbezeichnung |
+| `versionsbezeichnung` | String | - | Versionsnummer oder -bezeichnung (z.B. "v1.0", "Rev. A") |
+| `dokumentTyp` | Enum[DokumentTyp] | - | Typ des Dokuments (siehe DokumentTyp-Enum) |
+| `dokumentReferenz` | String | ✓ | Dateipfad oder URL |
+| `quelle` | String | - | Quelle des Dokuments |
+| `mimeType` | String | - | MIME-Type des Dokuments (z.B. "application/pdf", "image/png") |
+| `kategorienTags` | list[String] | - | Kategorisierung des Dokuments |
+
+**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).
+
+#### Beispiel-Kategorien (nicht abschliessend)
+
+| Kategorie | Beschreibung |
+|-----------|--------------|
+| `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 |
+
+---
+
+### 4. Geografische Entitäten
+
+#### GeoPolylinie
+**Repräsentiert eine Linie oder ein Polygon aus mehreren GeoPunkten.**
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `id` | UUID | ✓ | Eindeutiger Identifier |
+| `closed` | Boolean | ✓ | Ist die GeoPolylinie geschlossen (Polygon)? |
+| `punkte` | list[GeoPunkt] | ✓ | Liste der GeoPunkte, die die GeoPolylinie bilden |
+
+**Verwendung:**
+- Parzellenperimeter (geschlossene GeoPolylinie)
+- Baulinie (offene oder geschlossene GeoPolylinie)
+- Projektperimeter (geschlossene GeoPolylinie)
+
+#### GeoPunkt
+**Repräsentiert einen 3D-Punkt mit Referenzangabe.**
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `koordinatensystem` | String | ✓ | Koordinatensystem (z.B. "LV95", "EPSG:2056") |
+| `x` | Float | ✓ | Ostwert (E) [m], typisch 2'480'000 - 2'840'000 |
+| `y` | Float | ✓ | Nordwert (N) [m], typisch 1'070'000 - 1'300'000 |
+| `z` | Float | - | Höhe über Meer [m] |
+| `referenz` | Enum[GeoTag] | - | Kategorisierung des Punktes |
+
+**Verwendung:**
+- Als Teil einer GeoPolylinie (Parzellenperimeter, Baulinie)
+- Einzelne Referenzpunkte
+
+#### GeoTag (Enum)
+
+| Kategorie | Beschreibung |
+|-----------|--------------|
+| `K1` | Fixpunkt höchster Genauigkeit |
+| `K2` | Fixpunkt mittlerer Genauigkeit |
+| `K3` | Fixpunkt niedriger Genauigkeit |
+| `Geometer` | Vom Geometer vermessener Punkt |
+
+**Beispiel (Zürich Hauptbahnhof):**
+- `koordinatensystem`: "LV95" oder "EPSG:2056"
+- `x`: 2'683'140 [m]
+- `y`: 1'247'850 [m]
+- `z`: 408 [m]
+- `referenz`: "K1" (oder ein anderer GeoTag-Wert)
+
+---
+
+### 5. Administrative Hierarchie
+
+#### Land
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `id` | UUID | ✓ | Eindeutiger Identifier |
+| `label` | String | ✓ | Landesname (z.B. "Schweiz") |
+| `abk` | String | - | Abkürzung (z.B. "CH") |
+| `dokumente` | list[Dokument] | - | Nationale Gesetze |
+| `kontextInformationen` | list[Kontext] | - | Nationale Kontextinformationen |
+
+---
+
+#### Kanton
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `id` | UUID | ✓ | Eindeutiger Identifier |
+| `label` | String | ✓ | Kantonsname (z.B. "Zürich") |
+| `id_land` | [land] | eindeutiger Link zum land, also in welchem land kanton liegt |
+| `abk` | String | - | Abkürzung (z.B. "ZH") |
+| `dokumente` | list[Dokument] | - | Kantonale Dokumente |
+| `kontextInformationen` | list[Kontext] | - | Kantonsspezifische Kontextinfos |
+
+---
+
+#### Gemeinde
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `id` | UUID | ✓ | Eindeutiger Identifier |
+| `label` | String | ✓ | Gemeindename (z.B. "Zürich") |
+| `id_kanton` | [kanton] | eindeutiger Link zur gemeinde, also im welchem kanton gemeinde liegt |
+| `plz` | String | - | Postleitzahl (bei Gemeinden mit mehreren PLZ kann dies eine Haupt-PLZ sein) |
+| `dokumente` | list[Dokument] | - | Gemeindedokumente |
+| `kontextInformationen` | list[Kontext] | - | Gemeindespezifische Kontextinfos |
+
+**Hinweis:**
+Bei Gemeinden mit mehreren Postleitzahlen (z.B. Zürich, Bern) wird die konkrete PLZ der Parzelle im Attribut `plz` der Parzelle erfasst.
+
+---
+
+### 6. Kontext
+**Unterstützendes Datenobjekt für flexible Zusatzinformationen und Hinweise.**
+
+| Feld | Datentyp | Pflicht | Beschreibung |
+|------|----------|---------|--------------|
+| `id` | UUID | ✓ | Eindeutiger Identifier |
+| `thema` | String | ✓ | Bezeichnung des Themas |
+| `inhalt` | String | ✓ | Detaillierte Information (Text) |
+
+**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]`
+
+#### Beispielthemen (nicht abschliessend)
+
+| Themenbereich | Beispiele |
+|---------------|-----------|
+| **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 (z.B. Revision PBG mit aktuell negativer Vorwirkung) |
+
+**Design-Rationale:**
+Das Kontext-Objekt ermöglicht flexibles Hinzufügen von projektspezifischen, parzellen-spezifischen oder regionalen Informationen ohne Schemaänderungen.
+
+---
+
+### 7. Hilfsentitäten & Enumerationen
+
+#### JaNein (Enum)
+**Drei-wertiger Zustand für optionale Ja/Nein-Fragen.**
+
+| Wert | Bedeutung |
+|------|-----------|
+| `""` (leer) | Unbekannt / Nicht erfasst |
+| `"Ja"` | Ja / Zutreffend |
+| `"Nein"` | Nein / Nicht zutreffend |
+
+**Verwendung:**
+- `parzelleBebaut`: Ist die Parzelle bebaut?
+- `parzelleErschlossen`: Ist die Parzelle erschlossen?
+- `parzelleHanglage`: Liegt die Parzelle in Hanglage?
+
+---
+
+#### StatusProzess (Enum)
+**Projektstatus zur Nachverfolgung des Projektfortschritts.**
+
+| Wert | Beschreibung |
+|------|--------------|
+| `Eingang` | Projekt wurde eingereicht |
+| `Analyse` | Projekt wird analysiert |
+| `Studie` | Machbarkeitsstudie läuft |
+| `Planung` | Planungsphase |
+| `Baurechtsverfahren` | Baubewilligung läuft |
+| `Umsetzung` | Bauprojekt in Umsetzung |
+| `Archiv` | Projekt abgeschlossen |
+
+---
+
+#### DokumentTyp (Enum)
+**Dokumenttyp zur Kategorisierung von Dokumenten, insbesondere für kantonale und kommunale Dokumente.**
+
+| Wert | Beschreibung |
+|------|--------------|
+| `kantonBaureglementAktuell` | Aktuelles Baureglement (Kanton) |
+| `kantonBaureglementRevision` | Baureglement in Revision (Kanton) |
+| `kantonBauverordnungAktuell` | Aktuelle Bauverordnung (Kanton) |
+| `kantonBauverordnungRevision` | Bauverordnung in Revision (Kanton) |
+| `gemeindeBzoAktuell` | Aktuelle Bau- und Zonenordnung (BZO) (Gemeinde) |
+| `gemeindeBzoRevision` | BZO in Revision (Gemeinde) |
+
+**Verwendung:**
+- Dokumente in der `dokumente`-Liste von Kanton oder Gemeinde können über `dokumentTyp` identifiziert werden
+- Ermöglicht die Suche nach aktuellen oder in Revision befindlichen Dokumenten
+
+---
+
+## Q & A
+
+1. **Versionierung**: Sollen Änderungen an Parzellen historisiert werden? --> Vorerst nicht
+2. **Mehrsprachigkeit**: Labels in DE/FR/IT? --> Wir im Pydantic Model später umgesetzt
+3. **Benutzer & Rollen**: Wer darf was bearbeiten? --> In der App über Roles und Permissions gesteuert
+4. **Workflow-Engine**: Für Statusübergänge und Genehmigungen? --> In der App über Workflow-Engine gesteuert
+5. **Integration**: Anbindung an amtliche Geodaten (z.B. Swisstopo API)? --> In der App über Integrationen gesteuert
+6. **Berechnungen**: Sollen Ausnützungsberechnungen automatisiert werden? --> In der App über Berechnungen gesteuert
+
+---
+
+## Nächste Schritte
+
+1. **Validierung**: Review mit PEK
+2. **Prototyp**: Implementierung der Datenmodell-Klassen
+3. **GIS-Integration**: PostGIS aufsetzen, Test-Geodaten importieren
+4. **API-Design**: RESTful API (FastAPI) mit OpenAPI-Dokumentation
diff --git a/docs/real-estate-feature-integration-guide/01-overview.md b/docs/real-estate-feature-integration-guide/01-overview.md
new file mode 100644
index 00000000..f0691689
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/01-overview.md
@@ -0,0 +1,169 @@
+# Überblick und Projektstruktur
+
+[← Zurück zum Inhaltsverzeichnis](README.md) | [Weiter: Datenmodell erstellen →](02-datamodels.md)
+
+## Überblick
+
+Das Feature "realEstate" bietet eine **stateless API** für Real Estate-Datenbankoperationen mit AI-basierter natürlicher Sprachverarbeitung:
+
+### Hauptfunktionalität
+
+- **Natürliche Sprache → CRUD-Operationen**: User-Input wird mit AI analysiert und in entsprechende Datenbankoperationen übersetzt
+- **Direkte Datenbankabfragen**: SQL-Queries können direkt ausgeführt werden
+- **CRUD-Operationen**: Erstellen, Lesen, Aktualisieren und Löschen von Real Estate-Entitäten
+- **PostgreSQL-Datenbankzugriff**: Über den bestehenden DatabaseConnector
+- **RESTful API-Endpunkte**: Einfache, stateless Endpunkte ohne Session-Management
+- **Benutzerauthentifizierung und Zugriffskontrolle**: Integriert in bestehende UAM-Struktur
+
+### Architektur-Ansatz
+
+**Stateless Design:**
+- Keine Chat-Sessions notwendig (optional für zukünftige Erweiterungen)
+- Jeder Request ist unabhängig
+- Direkter Flow: User-Input → AI-Analyse → CRUD-Operation → Ergebnis
+- Keine Query-History (kann optional später hinzugefügt werden)
+
+**AI-Integration:**
+- Nutzt `serviceAi` für Intent-Erkennung
+- Übersetzt natürliche Sprache in CRUD-Operationen
+- Unterstützt CREATE, READ, UPDATE, DELETE, QUERY-Intents
+
+**Real Estate-Datenmodell-Entitäten:**
+- **Kernentitäten**: Projekt, Parzelle
+- **Unterstützend**: Dokument, Kontext
+- **Geografisch**: GeoPolylinie, GeoPunkt
+- **Administrativ**: Land, Kanton, Gemeinde
+
+### Architektur-Komponenten
+
+Die Architektur folgt dem Muster bestehender Features:
+- **Routes** (`modules/routes/`) - API-Endpunkte (stateless)
+- **Features** (`modules/features/`) - Geschäftslogik mit AI-Integration
+- **Interfaces** (`modules/interfaces/`) - Datenbankzugriff (CRUD-Operationen)
+- **DataModels** (`modules/datamodels/`) - Pydantic-Modelle für Real Estate-Entitäten
+- **Services** (`modules/services/`) - AI-Service für Intent-Analyse
+
+---
+
+## Projektstruktur
+
+```
+gateway/
+├── modules/
+│ ├── routes/
+│ │ └── routeRealEstate.py # NEU: Stateless API-Endpunkte
+│ │ ├── POST /api/realestate/command # Natürliche Sprache → CRUD
+│ │ └── POST /api/realestate/query # Direkte SQL-Query
+│ │
+│ ├── features/
+│ │ └── realEstate/
+│ │ └── mainRealEstate.py # NEU: Feature-Logik mit AI-Integration
+│ │ ├── processNaturalLanguageCommand() # Hauptfunktion
+│ │ ├── analyzeUserIntent() # AI-basierte Intent-Analyse
+│ │ └── executeIntentBasedOperation() # CRUD-Ausführung
+│ │
+│ ├── interfaces/
+│ │ ├── interfaceDbRealEstateAccess.py # NEU: Zugriffskontrolle
+│ │ ├── interfaceDbRealEstateObjects.py # NEU: CRUD-Interface
+│ │ └── interfaceDbRealEstateChatObjects.py # OPTIONAL: Für Session-Support
+│ │
+│ ├── datamodels/
+│ │ ├── datamodelRealEstate.py # NEU: Real Estate Datenmodelle
+│ │ │ ├── Projekt, Parzelle, Dokument, etc.
+│ │ └── datamodelRealEstateChat.py # OPTIONAL: Für Session-Support
+│ │
+│ ├── services/
+│ │ └── serviceAi/ # BEREITS VORHANDEN
+│ │ └── mainServiceAi.py # Wird für Intent-Analyse genutzt
+│ │
+│ └── connectors/
+│ └── connectorDbPostgre.py # BEREITS VORHANDEN
+│
+├── app.py # Router-Registrierung
+├── env_dev.env # Environment-Konfiguration
+└── modules/features/featuresLifecycle.py # Feature-Lifecycle
+```
+
+### Wichtige Dateien
+
+**Erforderlich:**
+- `routeRealEstate.py` - API-Endpunkte (stateless)
+- `mainRealEstate.py` - Feature-Logik mit AI-Integration
+- `interfaceDbRealEstateAccess.py` - Zugriffskontrolle
+- `interfaceDbRealEstateObjects.py` - CRUD-Interface
+- `datamodelRealEstate.py` - Datenmodelle
+
+**Optional (für zukünftige Session-Unterstützung):**
+- `interfaceDbRealEstateChatObjects.py` - Session-Management
+- `datamodelRealEstateChat.py` - Session-Modelle
+
+---
+
+## Datenfluss: User-Input → Ergebnis
+
+### Flow: Natürliche Sprache ohne Session
+
+```
+1. User sendet Request
+ POST /api/realestate/command
+ Body: { "userInput": "Erstelle ein neues Projekt namens 'Hauptstrasse 42'" }
+
+2. Route empfängt Request
+ routeRealEstate.process_command()
+ → Auth: getCurrentUser()
+
+3. Feature-Logik verarbeitet
+ mainRealEstate.processNaturalLanguageCommand()
+ → Services initialisieren: getServices(currentUser, workflow=None)
+
+4. AI analysiert Intent
+ analyzeUserIntent(services.ai, userInput)
+ → services.ai.callAiPlanning(intentPrompt)
+ → AI gibt zurück: { "intent": "CREATE", "entity": "Projekt", "parameters": {...} }
+
+5. CRUD-Operation ausführen
+ executeIntentBasedOperation(intent, entity, parameters)
+ → getRealEstateInterface(currentUser)
+ → RealEstateObjects.createProjekt(projekt)
+ → DatabaseConnector.recordCreate(Projekt)
+
+6. Ergebnis zurückgeben
+ HTTP 200 OK
+ { "success": true, "intent": "CREATE", "result": {...} }
+```
+
+### Flow: Direkte SQL-Query
+
+```
+1. User sendet Request
+ POST /api/realestate/query
+ Body: { "queryText": "SELECT * FROM Projekt WHERE plz = '8000'" }
+
+2. Route empfängt Request
+ routeRealEstate.execute_direct_query()
+ → Auth: getCurrentUser()
+
+3. Query ausführen
+ getChatInterface(currentUser)
+ → RealEstateChatObjects.executeQuery(queryText)
+ → DatabaseConnector.executeQuery(sql)
+
+4. Ergebnis zurückgeben
+ HTTP 200 OK
+ { "rows": [...], "columns": [...], "rowCount": 15 }
+```
+
+---
+
+## Vorteile des stateless Ansatzes
+
+- **Einfachheit**: Kein Session-Management notwendig
+- **Performance**: Weniger Datenbank-Operationen pro Request
+- **Skalierbarkeit**: Stateless Requests sind einfacher zu skalieren
+- **Flexibilität**: Jeder Request ist unabhängig
+- **Erweiterbarkeit**: Session-Support kann später optional hinzugefügt werden
+
+---
+
+**Nächster Schritt:** [02-datamodels.md](02-datamodels.md)
+
diff --git a/docs/real-estate-feature-integration-guide/02-datamodels.md b/docs/real-estate-feature-integration-guide/02-datamodels.md
new file mode 100644
index 00000000..376f5121
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/02-datamodels.md
@@ -0,0 +1,976 @@
+# Schritt 1: Datenmodell erstellen
+
+[← Zurück zur Übersicht](README.md) | [Weiter: Interface erstellen →](03-interfaces.md)
+
+## 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:
+
+```python
+# 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
+
+```mermaid
+---
+title: Hauptflüsse - Architektur-Planungs-App
+---
+flowchart LR
+ subgraph Admin[Administrative Ebene]
+ Land[Land
Schweiz]
+ Kanton[Kanton
z.B. Zürich]
+ Gemeinde[Gemeinde
z.B. Zürich Stadt]
+ Land --> Kanton
+ Kanton --> Gemeinde
+ end
+
+ subgraph Geo[Geografische Daten]
+ GeoPolylinie[GeoPolylinie
Linie/Polygon]
+ GeoPunkt[GeoPunkt
Koordinaten]
+ GeoPolylinie --> GeoPunkt
+ end
+
+ subgraph Core[Kern-Business-Logik]
+ Projekt[Projekt
Bauprojekt]
+ Parzelle[Parzelle
Grundstück mit
Bauparametern]
+ Gemeinde --> Parzelle
+ Projekt --> Parzelle
+ Projekt --> GeoPolylinie
+ Parzelle --> GeoPolylinie
+ end
+
+ subgraph Support[Unterstützende Daten]
+ Dokument[Dokument
Dateien & URLs]
+ Kontext[Kontext
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:
+
+```python
+"""
+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. `Parzelle` → `parzellenNachbarschaft: 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_land` → `Land.id`
+ - `Gemeinde.id_kanton` → `Kanton.id`
+ - `Parzelle.kontextLand` → `Land.id` (Optional)
+ - `Parzelle.kontextKanton` → `Kanton.id` (Optional)
+ - `Parzelle.kontextGemeinde` → `Gemeinde.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](README.md) | [Weiter: Interface erstellen →](03-interfaces.md)
+
diff --git a/docs/real-estate-feature-integration-guide/03-interfaces.md b/docs/real-estate-feature-integration-guide/03-interfaces.md
new file mode 100644
index 00000000..a90c31b9
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/03-interfaces.md
@@ -0,0 +1,845 @@
+# Schritt 2: Interface erstellen
+
+[← Zurück: Datenmodell erstellen](02-datamodels.md) | [Weiter: Feature-Logik implementieren →](04-feature-logic.md)
+
+## Übersicht: Was sind Interfaces?
+
+**Interfaces** sind aktive Klassen, die den **Datenbankzugriff** implementieren. Sie unterscheiden sich von **Datamodels** (die nur die Datenstruktur definieren):
+
+| **Aspekt** | **Datamodels** | **Interfaces** |
+|------------|----------------|----------------|
+| **Zweck** | Definiert **WAS** (Datenstruktur) | Implementiert **WIE** (Datenzugriff) |
+| **Inhalt** | Pydantic-Modelle mit Feldern und Validierung | Klassen mit CRUD-Methoden (`create`, `get`, `update`, `delete`) |
+| **Beispiel** | `class Projekt(BaseModel): ...` | `def createProjekt(...) -> Projekt: ...` |
+| **Aktivität** | Passiv (nur Struktur) | Aktiv (führt Operationen aus) |
+
+**Analogie:**
+- **Datamodel** = Bauplan (beschreibt das Haus)
+- **Interface** = Bauunternehmer (baut das Haus)
+
+---
+
+## Struktur: Real Estate CRUD-Interface
+
+Da das Feature **stateless** arbeitet, benötigen wir nur **ein Interface** für CRUD-Operationen auf Real Estate-Entitäten:
+
+### Real Estate-Datenmodelle → Real Estate CRUD-Interface
+
+**Datamodel:** `datamodelRealEstate.py`
+- `Projekt`
+- `Parzelle`
+- `Dokument`
+- `Kanton`, `Gemeinde`, `Land`
+- `GeoPolylinie`, `GeoPunkt`
+- `Kontext`
+- etc.
+
+**Interface:** `interfaceDbRealEstateObjects.py`
+- `RealEstateObjects` (Haupt-Interface)
+- `RealEstateAccess` (Zugriffskontrolle)
+- Methoden: `createProjekt()`, `getParzelle()`, `updateDokument()`, etc.
+
+**Optional:** `interfaceDbRealEstateChatObjects.py` (nur für direkte SQL-Queries)
+- `RealEstateChatObjects` - Für direkte Query-Ausführung ohne Session
+- Methoden: `executeQuery()` - Führt SQL direkt aus
+
+---
+
+## Warum nur ein Haupt-Interface?
+
+1. **Stateless Design**:
+ - Keine Session-Verwaltung notwendig
+ - Direkte CRUD-Operationen auf Real Estate-Modellen
+
+2. **Einfache Architektur**:
+ - Ein Interface für alle CRUD-Operationen
+ - Weniger Komplexität, bessere Wartbarkeit
+
+3. **Optionales Query-Interface**:
+ - Nur für direkte SQL-Queries (stateless)
+ - Keine Session-Management-Funktionen
+
+---
+
+## Zu erstellende Dateien
+
+### Schritt 2a: Real Estate CRUD-Interface (ERFORDERLICH)
+
+**Zwei separate Dateien** (wie bei anderen Features):
+
+#### Datei 1: `modules/interfaces/interfaceDbRealEstateAccess.py`
+
+**Enthält:**
+- `RealEstateAccess` - Zugriffskontrolle für Real Estate-Entitäten
+- Methoden: `uam()`, `canModify()`
+
+**Zweck:** Prüft Zugriffsrechte und filtert Daten basierend auf Benutzerprivilegien
+
+#### Datei 2: `modules/interfaces/interfaceDbRealEstateObjects.py`
+
+**Enthält:**
+- `RealEstateObjects` - Haupt-Interface für CRUD-Operationen
+- `getInterface()` - Factory-Funktion
+- Nutzt `RealEstateAccess` aus der Access-Datei
+
+**Zweck:** Verwaltet Real Estate-Entitäten (Projekt, Parzelle, Dokument, etc.)
+
+**Nutzt:**
+- `datamodelRealEstate.py` (Projekt, Parzelle, Dokument, etc.)
+- `interfaceDbRealEstateAccess.py` (für Zugriffskontrolle)
+
+**Wann benötigt:** Für alle CRUD-Operationen auf Real Estate-Entitäten (z.B. Projekte erstellen/bearbeiten, Parzellen verwalten). Dies ist das Haupt-Interface für das Feature.
+
+---
+
+### Schritt 2b: Query-Interface (OPTIONAL - nur für direkte SQL-Queries)
+
+**Eine Datei** für stateless Query-Ausführung:
+
+#### Datei: `modules/interfaces/interfaceDbRealEstateChatObjects.py`
+
+**Enthält:**
+- `RealEstateChatObjects` - Interface für direkte SQL-Query-Ausführung
+- `getInterface()` - Factory-Funktion
+- Methoden: `executeQuery()` - Führt SQL direkt aus (stateless)
+
+**Zweck:** Direkte SQL-Query-Ausführung ohne Session-Management
+
+**Nutzt:**
+- `connectorDbPostgre.DatabaseConnector` für direkte SQL-Ausführung
+- Keine Chat-Modelle (stateless)
+
+**Wann benötigt:** Nur wenn Sie direkte SQL-Queries ausführen möchten (z.B. für komplexe SELECT-Queries). Für CRUD-Operationen verwenden Sie das Real Estate CRUD-Interface.
+
+---
+
+## Übersicht: Dateien und ihre Beziehungen
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ DATAMODELS (Struktur) │
+├─────────────────────────────────────────────────────────────┤
+│ datamodelRealEstate.py │
+│ ├── Projekt │
+│ ├── Parzelle │
+│ ├── Dokument │
+│ ├── Kanton, Gemeinde, Land │
+│ ├── GeoPolylinie, GeoPunkt │
+│ ├── Kontext │
+│ └── ... │
+└─────────────────────────────────────────────────────────────┘
+ │
+ │ nutzt
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ INTERFACES (Zugriff) │
+├─────────────────────────────────────────────────────────────┤
+│ REAL ESTATE CRUD-INTERFACE (ERFORDERLICH) │
+│ │
+│ interfaceDbRealEstateAccess.py │
+│ └── RealEstateAccess │
+│ ├── uam() │
+│ └── canModify() │
+│ │
+│ interfaceDbRealEstateObjects.py │
+│ ├── RealEstateObjects │
+│ │ ├── createProjekt() │
+│ │ ├── getProjekt() │
+│ │ ├── updateProjekt() │
+│ │ ├── deleteProjekt() │
+│ │ ├── createParzelle() │
+│ │ ├── getParzelle() │
+│ │ └── ... (CRUD für alle Entitäten) │
+│ └── getInterface() │
+│ └── nutzt RealEstateAccess │
+│ │
+│ QUERY-INTERFACE (OPTIONAL - nur für direkte SQL) │
+│ │
+│ interfaceDbRealEstateChatObjects.py │
+│ ├── RealEstateChatObjects │
+│ │ └── executeQuery() # Direkte SQL-Ausführung │
+│ └── getInterface() │
+│ └── Keine Access-Klasse (stateless) │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Interface-Struktur: Access vs. Objects
+
+Jedes Interface besteht aus **zwei Klassen**:
+
+### 1. `*Access` Klasse (Zugriffskontrolle)
+
+**Zweck:** Prüft, wer was sehen/dürfen darf
+
+**Methoden:**
+- `uam()` - Filtert Daten basierend auf Benutzerprivilegien
+- `canModify()` - Prüft, ob Benutzer ändern darf
+
+**Beispiel:** `RealEstateChatAccess`
+
+### 2. `*Objects` Klasse (Haupt-Interface)
+
+**Zweck:** Führt CRUD-Operationen aus
+
+**Methoden:**
+- `create*()` - Erstellt neue Einträge
+- `get*()` - Lädt Einträge
+- `update*()` - Aktualisiert Einträge
+- `delete*()` - Löscht Einträge
+
+**Nutzt:** `*Access` für Zugriffskontrolle
+
+**Beispiel:** `RealEstateChatObjects`
+
+**Warum getrennt?**
+- Separation of Concerns: Zugriffskontrolle ist separate Verantwortlichkeit
+- Wiederverwendbarkeit: Access-Klasse kann von mehreren Interfaces genutzt werden
+- Testbarkeit: Zugriffskontrolle kann unabhängig getestet werden
+
+---
+
+## Implementierung: Real Estate CRUD-Interface
+
+Das Real Estate CRUD-Interface besteht aus **zwei separaten Dateien**, genau wie bei anderen Features (`interfaceDbAppObjects.py` + `interfaceDbAppAccess.py`).
+
+### Datei 1: Access-Implementierung
+
+**Datei:** `modules/interfaces/interfaceDbRealEstateAccess.py`
+
+```python
+"""
+Access control for Real Estate interface.
+Handles user access management and permission checks.
+"""
+
+import logging
+from typing import Dict, Any, List, Optional
+from modules.datamodels.datamodelUam import User, UserPrivilege
+
+logger = logging.getLogger(__name__)
+
+
+class RealEstateAccess:
+ """
+ Access control class for Real Estate interface.
+ Handles user access management and permission checks.
+ """
+
+ def __init__(self, currentUser: User, db):
+ """Initialize with user context."""
+ self.currentUser = currentUser
+ self.mandateId = currentUser.mandateId
+ self.userId = currentUser.id
+
+ if not self.mandateId or not self.userId:
+ raise ValueError("Invalid user context: mandateId and userId are required")
+
+ self.db = db
+
+ def uam(self, model_class: type, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """
+ Unified user access management function that filters data based on user privileges.
+
+ Args:
+ model_class: Pydantic model class for the table
+ recordset: Recordset to filter based on access rules
+
+ Returns:
+ Filtered recordset with access control attributes
+ """
+ from modules.datamodels.datamodelUam import UserPrivilege
+
+ userPrivilege = self.currentUser.privilege
+ filtered_records = []
+
+ # System admins see all records
+ if userPrivilege == UserPrivilege.SYSADMIN:
+ filtered_records = recordset
+ # Admins see records in their mandate
+ elif userPrivilege == UserPrivilege.ADMIN:
+ filtered_records = [r for r in recordset if r.get("mandateId", "-") == self.mandateId]
+ # Regular users see only their records
+ else:
+ filtered_records = [
+ r for r in recordset
+ if r.get("mandateId", "-") == self.mandateId and r.get("_createdBy") == self.userId
+ ]
+
+ # Add access control attributes
+ for record in filtered_records:
+ record["_hideView"] = False
+ record["_hideEdit"] = not self.canModify(model_class, record.get("id"))
+ record["_hideDelete"] = not self.canModify(model_class, record.get("id"))
+
+ return filtered_records
+
+ def canModify(self, model_class: type, recordId: Optional[str] = None) -> bool:
+ """Checks if the current user can modify records."""
+ from modules.datamodels.datamodelUam import UserPrivilege
+
+ userPrivilege = self.currentUser.privilege
+
+ if userPrivilege == UserPrivilege.SYSADMIN:
+ return True
+
+ if recordId is not None:
+ records = self.db.getRecordset(model_class, recordFilter={"id": recordId})
+ if not records:
+ return False
+
+ record = records[0]
+
+ if userPrivilege == UserPrivilege.ADMIN and record.get("mandateId", "-") == self.mandateId:
+ return True
+
+ if (record.get("mandateId", "-") == self.mandateId and
+ record.get("_createdBy") == self.userId):
+ return True
+
+ return False
+ else:
+ return True # Regular users can create records
+```
+
+---
+
+### Datei 2: Objects-Implementierung
+
+**Datei:** `modules/interfaces/interfaceDbRealEstateObjects.py`
+
+```python
+"""
+Interface to Real Estate database objects.
+Uses PostgreSQL connector for data access with user/mandate filtering.
+Handles CRUD operations on Real Estate entities (Projekt, Parzelle, etc.).
+"""
+
+import logging
+from typing import Dict, Any, List, Optional
+from modules.datamodels.datamodelRealEstate import (
+ Projekt,
+ Parzelle,
+ Dokument,
+ Kanton,
+ Gemeinde,
+ Land,
+ GeoPolylinie,
+ GeoPunkt,
+ Kontext,
+ StatusProzess,
+)
+from modules.datamodels.datamodelUam import User
+from modules.connectors.connectorDbPostgre import DatabaseConnector
+from modules.shared.configuration import APP_CONFIG
+# Import Access-Klasse aus separater Datei
+from modules.interfaces.interfaceDbRealEstateAccess import RealEstateAccess
+
+logger = logging.getLogger(__name__)
+
+# Singleton factory for Real Estate interfaces
+_realEstateInterfaces = {}
+
+
+class RealEstateObjects:
+ """
+ Interface to Real Estate database objects.
+ Uses PostgreSQL connector for data access with user/mandate filtering.
+ Handles CRUD operations on Real Estate entities.
+ """
+
+ def __init__(self, currentUser: Optional[User] = None):
+ """Initializes the Real Estate Interface."""
+ self.currentUser = currentUser
+ self.userId = currentUser.id if currentUser else None
+ self.mandateId = currentUser.mandateId if currentUser else None
+ self.access = None
+
+ # Initialize database
+ self._initializeDatabase()
+
+ # Set user context if provided
+ if currentUser:
+ self.setUserContext(currentUser)
+
+ def _initializeDatabase(self):
+ """Initialize PostgreSQL database connection."""
+ try:
+ # Get database configuration from environment
+ dbHost = APP_CONFIG.get("DB_APP_HOST", "localhost")
+ dbDatabase = APP_CONFIG.get("DB_APP_DATABASE", "poweron_app")
+ dbUser = APP_CONFIG.get("DB_APP_USER")
+ dbPassword = APP_CONFIG.get("DB_APP_PASSWORD_SECRET")
+ dbPort = int(APP_CONFIG.get("DB_APP_PORT", 5432))
+
+ # Initialize database connector
+ self.db = DatabaseConnector(
+ dbHost=dbHost,
+ dbDatabase=dbDatabase,
+ dbUser=dbUser,
+ dbPassword=dbPassword,
+ dbPort=dbPort,
+ userId=self.userId if self.userId else None,
+ )
+
+ logger.info(f"Real Estate database connector initialized for database: {dbDatabase}")
+ except Exception as e:
+ logger.error(f"Error initializing Real Estate database: {e}")
+ raise
+
+ def setUserContext(self, currentUser: User):
+ """Sets the user context for the interface."""
+ self.currentUser = currentUser
+ self.userId = currentUser.id
+ self.mandateId = currentUser.mandateId
+
+ if not self.userId or not self.mandateId:
+ raise ValueError("Invalid user context: id and mandateId are required")
+
+ # Initialize access control
+ self.access = RealEstateAccess(self.currentUser, self.db)
+
+ # Update database context
+ self.db.updateContext(self.userId)
+
+ # ===== Projekt Methods =====
+
+ def createProjekt(self, projekt: Projekt) -> Projekt:
+ """Create a new project."""
+ # Ensure mandateId is set
+ if not projekt.mandateId:
+ projekt.mandateId = self.mandateId
+
+ # Apply access control
+ self.access.uam(Projekt, [])
+
+ # Save to database
+ self.db.recordCreate(Projekt, projekt.model_dump())
+
+ return projekt
+
+ def getProjekt(self, projektId: str) -> Optional[Projekt]:
+ """Get a project by ID."""
+ records = self.db.getRecordset(
+ Projekt,
+ recordFilter={"id": projektId}
+ )
+
+ if not records:
+ return None
+
+ # Apply access control
+ filtered = self.access.uam(Projekt, records)
+
+ if not filtered:
+ return None
+
+ return Projekt(**filtered[0])
+
+ def updateProjekt(self, projektId: str, updateData: Dict[str, Any]) -> Optional[Projekt]:
+ """Update a project."""
+ projekt = self.getProjekt(projektId)
+ if not projekt:
+ return None
+
+ # Check if user can modify
+ if not self.access.canModify(Projekt, projektId):
+ raise PermissionError(f"User {self.userId} cannot modify project {projektId}")
+
+ # Update fields
+ for key, value in updateData.items():
+ if hasattr(projekt, key):
+ setattr(projekt, key, value)
+
+ # Save to database
+ self.db.recordModify(Projekt, projektId, projekt.model_dump())
+
+ return projekt
+
+ def deleteProjekt(self, projektId: str) -> bool:
+ """Delete a project."""
+ projekt = self.getProjekt(projektId)
+ if not projekt:
+ return False
+
+ # Check if user can modify
+ if not self.access.canModify(Projekt, projektId):
+ raise PermissionError(f"User {self.userId} cannot delete project {projektId}")
+
+ return self.db.recordDelete(Projekt, projektId)
+
+ # ===== Parzelle Methods =====
+
+ def createParzelle(self, parzelle: Parzelle) -> Parzelle:
+ """Create a new plot."""
+ if not parzelle.mandateId:
+ parzelle.mandateId = self.mandateId
+
+ self.access.uam(Parzelle, [])
+ self.db.recordCreate(Parzelle, parzelle.model_dump())
+
+ return parzelle
+
+ def getParzelle(self, parzelleId: str) -> Optional[Parzelle]:
+ """Get a plot by ID."""
+ records = self.db.getRecordset(
+ Parzelle,
+ recordFilter={"id": parzelleId}
+ )
+
+ if not records:
+ return None
+
+ filtered = self.access.uam(Parzelle, records)
+
+ if not filtered:
+ return None
+
+ return Parzelle(**filtered[0])
+
+ def updateParzelle(self, parzelleId: str, updateData: Dict[str, Any]) -> Optional[Parzelle]:
+ """Update a plot."""
+ parzelle = self.getParzelle(parzelleId)
+ if not parzelle:
+ return None
+
+ if not self.access.canModify(Parzelle, parzelleId):
+ raise PermissionError(f"User {self.userId} cannot modify plot {parzelleId}")
+
+ for key, value in updateData.items():
+ if hasattr(parzelle, key):
+ setattr(parzelle, key, value)
+
+ self.db.recordModify(Parzelle, parzelleId, parzelle.model_dump())
+
+ return parzelle
+
+ def deleteParzelle(self, parzelleId: str) -> bool:
+ """Delete a plot."""
+ parzelle = self.getParzelle(parzelleId)
+ if not parzelle:
+ return False
+
+ if not self.access.canModify(Parzelle, parzelleId):
+ raise PermissionError(f"User {self.userId} cannot delete plot {parzelleId}")
+
+ return self.db.recordDelete(Parzelle, parzelleId)
+
+ # ===== Dokument Methods =====
+
+ def createDokument(self, dokument: Dokument) -> Dokument:
+ """Create a new document."""
+ if not dokument.mandateId:
+ dokument.mandateId = self.mandateId
+
+ self.access.uam(Dokument, [])
+ self.db.recordCreate(Dokument, dokument.model_dump())
+
+ return dokument
+
+ def getDokument(self, dokumentId: str) -> Optional[Dokument]:
+ """Get a document by ID."""
+ records = self.db.getRecordset(
+ Dokument,
+ recordFilter={"id": dokumentId}
+ )
+
+ if not records:
+ return None
+
+ filtered = self.access.uam(Dokument, records)
+
+ if not filtered:
+ return None
+
+ return Dokument(**filtered[0])
+
+ # ... weitere CRUD-Methoden für andere Entitäten (Kanton, Gemeinde, Land, etc.)
+
+
+def getInterface(currentUser: User) -> RealEstateObjects:
+ """
+ Factory function to get or create a Real Estate interface instance for a user.
+ Uses singleton pattern per user.
+ """
+ userKey = f"{currentUser.id}_{currentUser.mandateId}"
+
+ if userKey not in _realEstateInterfaces:
+ _realEstateInterfaces[userKey] = RealEstateObjects(currentUser)
+
+ return _realEstateInterfaces[userKey]
+```
+
+## Wichtige Punkte:
+
+1. **DatabaseConnector**: Nutzt `connectorDbPostgre.DatabaseConnector` für Datenbankzugriff
+2. **Access Control**: `RealEstateAccess` implementiert Benutzer- und Mandaten-Filterung
+3. **Singleton Pattern**: `getInterface()` erstellt pro User eine Instanz
+4. **CRUD-Operationen**: `recordCreate`, `recordModify`, `recordDelete`, `getRecordset` vom Connector
+5. **MandateId**: Wird automatisch gesetzt, wenn nicht vorhanden
+
+---
+
+## Schritt 2b: Query-Interface (OPTIONAL - nur für direkte SQL-Queries)
+
+### Wann benötigt?
+
+**Kurze Antwort:** Nur wenn Sie **direkte SQL-Queries** ausführen möchten (z.B. für komplexe SELECT-Queries). Für CRUD-Operationen verwenden Sie das Real Estate CRUD-Interface.
+
+#### Szenario 1: Alles über CRUD-Interface (EMPFOHLEN)
+
+**Strukturiert und sicher:**
+
+```python
+# User schreibt: "Erstelle Projekt 'Test'"
+# Feature-Logik nutzt CRUD-Interface:
+realEstateInterface = getRealEstateInterface(currentUser)
+projekt = realEstateInterface.createProjekt(Projekt(
+ mandateId=currentUser.mandateId, # Automatisch gesetzt
+ label="Test" # Validierung durch Pydantic
+))
+```
+
+**Vorteile:**
+- ✅ **Validierung**: Pydantic-Modelle prüfen alle Felder automatisch
+- ✅ **Zugriffskontrolle**: `RealEstateAccess` prüft Berechtigungen
+- ✅ **Sicherheit**: Kein SQL-Injection-Risiko
+- ✅ **Geschäftslogik**: Automatisches Setzen von Systemfeldern (`_createdBy`, `_createdAt`)
+- ✅ **Typsicherheit**: Fehler werden zur Entwicklungszeit erkannt
+- ✅ **Wartbarkeit**: Zentrale CRUD-Methoden, einfach zu testen
+
+#### Szenario 2: Direkte SQL-Queries (OPTIONAL)
+
+**Nur für komplexe SELECT-Queries:**
+
+```python
+# Für komplexe Queries, die nicht über CRUD-Methoden abgedeckt sind
+chatInterface = getChatInterface(currentUser)
+results = chatInterface.executeQuery(
+ "SELECT p.*, COUNT(parz.id) as parzellen_count FROM Projekt p LEFT JOIN Parzelle parz ON parz.projektId = p.id GROUP BY p.id"
+)
+```
+
+**Warnung:**
+- ⚠️ **Nur für SELECT-Queries**: Keine INSERT/UPDATE/DELETE über direkte SQL-Queries
+- ⚠️ **Validierung erforderlich**: Queries sollten validiert werden
+- ⚠️ **SQL-Injection-Risiko**: Immer Parameterisierung verwenden
+
+#### Empfehlung
+
+**Sie benötigen das Query-Interface, wenn Sie:**
+
+- ✅ **Komplexe SELECT-Queries** benötigen (z.B. JOINs, Aggregationen)
+- ✅ **Flexible Query-Ausführung** benötigen (nicht über CRUD-Methoden abgedeckt)
+
+**Sie benötigen es NICHT, wenn Sie:**
+
+- ✅ **Nur CRUD-Operationen** benötigen (verwenden Sie das CRUD-Interface)
+- ✅ **Einfache Queries** haben (können über CRUD-Methoden abgedeckt werden)
+
+**Für Production-Systeme:**
+- **CRUD-Operationen**: Immer über CRUD-Interface
+- **Komplexe Queries**: Optional über Query-Interface (nur SELECT)
+
+### Struktur: Query-Interface (stateless)
+
+**Eine Datei** für direkte SQL-Query-Ausführung:
+
+#### Datei: `modules/interfaces/interfaceDbRealEstateChatObjects.py`
+
+```python
+"""
+Interface for direct SQL query execution (stateless).
+Uses PostgreSQL connector for direct query execution without session management.
+"""
+
+import logging
+from typing import Dict, Any, Optional
+from modules.datamodels.datamodelUam import User
+from modules.connectors.connectorDbPostgre import DatabaseConnector
+from modules.shared.configuration import APP_CONFIG
+
+logger = logging.getLogger(__name__)
+
+# Singleton factory
+_realEstateChatInterfaces = {}
+
+
+class RealEstateChatObjects:
+ """Interface for direct SQL query execution (stateless)."""
+
+ def __init__(self, currentUser: Optional[User] = None):
+ """Initialize the Query Interface."""
+ self.currentUser = currentUser
+ self.userId = currentUser.id if currentUser else None
+ self.mandateId = currentUser.mandateId if currentUser else None
+
+ # Initialize database
+ self._initializeDatabase()
+
+ # Set user context if provided
+ if currentUser:
+ self.setUserContext(currentUser)
+
+ def _initializeDatabase(self):
+ """Initialize PostgreSQL database connection."""
+ try:
+ dbHost = APP_CONFIG.get("DB_APP_HOST", "localhost")
+ dbDatabase = APP_CONFIG.get("DB_APP_DATABASE", "poweron_app")
+ dbUser = APP_CONFIG.get("DB_APP_USER")
+ dbPassword = APP_CONFIG.get("DB_APP_PASSWORD_SECRET")
+ dbPort = int(APP_CONFIG.get("DB_APP_PORT", 5432))
+
+ self.db = DatabaseConnector(
+ dbHost=dbHost,
+ dbDatabase=dbDatabase,
+ dbUser=dbUser,
+ dbPassword=dbPassword,
+ dbPort=dbPort,
+ userId=self.userId if self.userId else None,
+ )
+
+ logger.info(f"Real Estate Query database connector initialized for database: {dbDatabase}")
+ except Exception as e:
+ logger.error(f"Error initializing Real Estate Query database: {e}")
+ raise
+
+ def setUserContext(self, currentUser: User):
+ """Sets the user context for the interface."""
+ self.currentUser = currentUser
+ self.userId = currentUser.id
+ self.mandateId = currentUser.mandateId
+
+ if not self.userId or not self.mandateId:
+ raise ValueError("Invalid user context: id and mandateId are required")
+
+ # Update database context
+ self.db.updateContext(self.userId)
+
+ # ===== Database Query Execution =====
+
+ def executeQuery(self, queryText: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
+ """
+ Execute a SQL query directly on the database (stateless).
+
+ WARNING: This method executes raw SQL. Ensure proper validation and sanitization
+ before calling this method. Consider implementing query whitelisting or
+ only allowing SELECT statements for production use.
+
+ Args:
+ queryText: SQL query string (preferably SELECT only)
+ parameters: Optional parameters for parameterized queries
+
+ Returns:
+ Dictionary with 'rows' (list of dicts), 'columns' (list of column names),
+ 'rowCount' (int), and 'executionTime' (float)
+ """
+ import time
+
+ try:
+ start_time = time.time()
+
+ # Ensure connection is alive
+ self.db._ensure_connection()
+
+ with self.db.connection.cursor() as cursor:
+ # Execute query
+ if parameters:
+ # Use parameterized query for safety
+ cursor.execute(queryText, parameters)
+ else:
+ cursor.execute(queryText)
+
+ # Fetch results
+ rows = cursor.fetchall()
+
+ # Convert to list of dictionaries
+ result_rows = [dict(row) for row in rows]
+
+ # Get column names
+ columns = [desc[0] for desc in cursor.description] if cursor.description else []
+
+ execution_time = time.time() - start_time
+
+ return {
+ "rows": result_rows,
+ "columns": columns,
+ "rowCount": len(result_rows),
+ "executionTime": execution_time,
+ }
+ except Exception as e:
+ logger.error(f"Error executing query: {e}")
+ raise
+
+
+def getInterface(currentUser: User) -> RealEstateChatObjects:
+ """
+ Factory function to get or create a Query interface instance for a user.
+ Uses singleton pattern per user.
+ """
+ userKey = f"{currentUser.id}_{currentUser.mandateId}"
+
+ if userKey not in _realEstateChatInterfaces:
+ _realEstateChatInterfaces[userKey] = RealEstateChatObjects(currentUser)
+
+ return _realEstateChatInterfaces[userKey]
+```
+
+### Hinweise zur Implementierung
+
+1. **Stateless**: Keine Session-Management-Funktionen
+2. **Nur für Queries**: Primär für SELECT-Queries gedacht
+3. **Sicherheit**: Immer Parameterisierung verwenden
+4. **Validierung**: Queries sollten validiert werden (z.B. nur SELECT erlauben)
+
+### Beispiel: Beide Interfaces zusammen nutzen
+
+```python
+from modules.interfaces.interfaceDbRealEstateChatObjects import getInterface as getChatInterface
+from modules.interfaces.interfaceDbRealEstateObjects import getInterface as getRealEstateInterface
+
+# CRUD-Interface für strukturierte Operationen
+realEstateInterface = getRealEstateInterface(currentUser)
+projekt = realEstateInterface.createProjekt(Projekt(
+ mandateId=currentUser.mandateId,
+ label="Neues Projekt"
+))
+
+# Query-Interface für komplexe SELECT-Queries (optional)
+chatInterface = getChatInterface(currentUser)
+results = chatInterface.executeQuery(
+ "SELECT p.*, COUNT(parz.id) as parzellen_count FROM Projekt p LEFT JOIN Parzelle parz ON parz.projektId = p.id GROUP BY p.id"
+)
+```
+
+---
+
+## Zusammenfassung: Benötigte Dateien
+
+### Erforderlich (für CRUD-Operationen):
+
+1. ✅ `modules/datamodels/datamodelRealEstate.py`
+ - Real Estate-Datenmodelle (Projekt, Parzelle, Dokument, etc.)
+
+2. ✅ `modules/interfaces/interfaceDbRealEstateAccess.py`
+ - Zugriffskontrolle für Real Estate-Entitäten (RealEstateAccess)
+
+3. ✅ `modules/interfaces/interfaceDbRealEstateObjects.py`
+ - Real Estate CRUD-Interface (RealEstateObjects)
+ - Nutzt `interfaceDbRealEstateAccess.py`
+ - Haupt-Interface für alle CRUD-Operationen
+
+### Optional (für direkte SQL-Queries):
+
+4. ⚠️ `modules/interfaces/interfaceDbRealEstateChatObjects.py`
+ - Query-Interface für direkte SQL-Ausführung (RealEstateChatObjects)
+ - Stateless, keine Session-Management
+ - Nur wenn Sie komplexe SELECT-Queries benötigen
+
+---
+
+[← Zurück: Datenmodell erstellen](02-datamodels.md) | [Weiter: Feature-Logik implementieren →](04-feature-logic.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/04-feature-logic.md b/docs/real-estate-feature-integration-guide/04-feature-logic.md
new file mode 100644
index 00000000..cd0b1758
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/04-feature-logic.md
@@ -0,0 +1,780 @@
+# Schritt 3: Feature-Logik implementieren
+
+[← Zurück: Interface erstellen](03-interfaces.md) | [Weiter: Routen erstellen →](05-routes.md)
+
+**Datei:** `modules/features/realEstate/mainRealEstate.py`
+
+Die Feature-Logik enthält die Geschäftslogik für das Feature. Sie wird von den Routen aufgerufen und arbeitet **stateless** ohne Session-Management.
+
+## Übersicht: Stateless Feature-Logik mit AI-Integration
+
+Die Feature-Logik verwendet **AI**, um natürliche Sprache direkt in CRUD-Operationen zu übersetzen - ohne Session-Management:
+
+```
+User Input (natürliche Sprache)
+ ↓
+AI-Analyse (Intent-Erkennung)
+ ↓
+CRUD-Operation identifizieren
+ ↓
+Parameter extrahieren
+ ↓
+Interface CRUD-Methode aufrufen
+ ↓
+Datenbank-Operation ausführen
+ ↓
+Ergebnis zurückgeben (keine Session-Speicherung)
+```
+
+**Beispiel:**
+- User: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+- AI analysiert → Intent: CREATE, Entity: Projekt, Parameter: {label: "Hauptstrasse 42"}
+- Feature-Logik ruft auf → `interface.createProjekt(Projekt(label="Hauptstrasse 42"))`
+- Ergebnis wird direkt zurückgegeben (keine Session, keine History)
+
+## AI-Integration: Services initialisieren
+
+Um AI zu verwenden, müssen Sie die **Services** initialisieren. Services sind eine zentrale Schnittstelle zu verschiedenen Systemkomponenten (AI, Chat, Database, etc.).
+
+### Services-Initialisierung
+
+```python
+from modules.services import getInterface as getServices
+
+# Services für einen User erhalten
+services = getServices(currentUser, workflow=None)
+
+# AI-Service verfügbar über:
+aiService = services.ai # Für AI-Aufrufe
+```
+
+**Wichtig:** Services werden normalerweise im Feature-Logik-Modul initialisiert und an Funktionen weitergegeben.
+
+---
+
+## AI-basierte Intent-Erkennung und CRUD-Operationen
+
+### Schritt 1: Intent-Analyse mit AI
+
+Die AI analysiert User-Input und identifiziert:
+- **Intent**: CREATE, READ, UPDATE, DELETE, QUERY
+- **Entity**: Projekt, Parzelle, Dokument, etc.
+- **Parameter**: Extrahierte Werte aus dem User-Input
+
+### Schritt 2: CRUD-Operation ausführen
+
+Basierend auf der AI-Analyse wird die entsprechende Interface-Methode aufgerufen.
+
+---
+
+## Beispiel-Implementierung:
+
+```python
+"""
+Real Estate feature main logic.
+Handles chat interface for database queries with AI-powered natural language processing.
+"""
+
+import logging
+import json
+from typing import Optional, Dict, Any, List
+from modules.datamodels.datamodelUam import User
+from modules.datamodels.datamodelRealEstate import (
+ Projekt,
+ Parzelle,
+ StatusProzess,
+)
+from modules.interfaces.interfaceDbRealEstateChatObjects import getInterface as getChatInterface
+from modules.interfaces.interfaceDbRealEstateObjects import getInterface as getRealEstateInterface
+from modules.services import getInterface as getServices
+
+logger = logging.getLogger(__name__)
+
+
+# ===== Direkte Query-Ausführung (stateless) =====
+
+async def executeDirectQuery(
+ currentUser: User,
+ queryText: str,
+ parameters: Optional[Dict[str, Any]] = None,
+) -> Dict[str, Any]:
+ """
+ Execute a database query directly without session management.
+
+ Args:
+ currentUser: Current authenticated user
+ queryText: SQL query text
+ parameters: Optional parameters for parameterized queries
+
+ Returns:
+ Dictionary containing query result (rows, columns, rowCount)
+
+ Note:
+ - No session or query history is saved
+ - Query is executed directly and result is returned
+ - For production, validate and sanitize queries before execution
+ """
+ try:
+ chatInterface = getChatInterface(currentUser)
+
+ # Execute query directly (no session tracking)
+ result = chatInterface.executeQuery(queryText, parameters)
+
+ logger.info(
+ f"Query executed successfully: {result['rowCount']} rows in {result.get('executionTime', 0):.3f}s"
+ )
+
+ return {
+ "status": "success",
+ "rows": result["rows"],
+ "columns": result["columns"],
+ "rowCount": result["rowCount"],
+ "executionTime": result.get("executionTime", 0),
+ }
+
+ except Exception as e:
+ logger.error(f"Error executing query: {str(e)}")
+ raise
+
+
+# ===== AI-basierte Intent-Erkennung und CRUD-Operationen =====
+
+async def processNaturalLanguageCommand(
+ currentUser: User,
+ userInput: str,
+) -> Dict[str, Any]:
+ """
+ Process natural language user input and execute corresponding CRUD operations.
+
+ Uses AI to analyze user intent and extract parameters, then executes the appropriate
+ CRUD operation through the interface. Works stateless without session management.
+
+ Args:
+ currentUser: Current authenticated user
+ userInput: Natural language command from user
+
+ Returns:
+ Dictionary containing operation result and metadata
+
+ Example user inputs:
+ - "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ - "Zeige mir alle Projekte in Zürich"
+ - "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ - "Lösche Parzelle ABC"
+ - "SELECT * FROM Projekt WHERE plz = '8000'"
+ """
+ try:
+ # Initialize services for AI access
+ services = getServices(currentUser, workflow=None)
+ aiService = services.ai
+
+ # Step 1: Analyze user intent with AI
+ intentAnalysis = await analyzeUserIntent(aiService, userInput)
+
+ logger.info(f"Intent analysis result: {intentAnalysis}")
+
+ # Step 2: Execute CRUD operation based on intent
+ result = await executeIntentBasedOperation(
+ currentUser=currentUser,
+ intent=intentAnalysis["intent"],
+ entity=intentAnalysis["entity"],
+ parameters=intentAnalysis["parameters"],
+ )
+
+ return {
+ "success": True,
+ "intent": intentAnalysis["intent"],
+ "entity": intentAnalysis["entity"],
+ "result": result,
+ }
+
+ except Exception as e:
+ logger.error(f"Error processing natural language command: {str(e)}")
+ raise
+
+
+async def analyzeUserIntent(
+ aiService,
+ userInput: str
+) -> Dict[str, Any]:
+ """
+ Use AI to analyze user input and extract intent, entity, and parameters.
+
+ Args:
+ aiService: AI service instance
+ userInput: Natural language user input
+
+ Returns:
+ Dictionary with 'intent', 'entity', and 'parameters'
+ """
+ # Create a structured prompt for intent analysis
+ intentPrompt = f"""
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "{userInput}"
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query
+
+Available entities:
+- Projekt: Real estate project
+- Parzelle: Plot/parcel
+- Dokument: Document
+- Kanton: Canton
+- Gemeinde: Municipality
+
+Return a JSON object with the following structure:
+{{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {{
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields
+ // For READ: include filter criteria
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include query text or natural language query
+ }},
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {{"intent": "CREATE", "entity": "Projekt", "parameters": {{"label": "Hauptstrasse 42"}}, "confidence": 0.95}}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {{"intent": "READ", "entity": "Projekt", "parameters": {{}}, "confidence": 0.9}}
+
+- Input: "SELECT * FROM Projekt WHERE plz = '8000'"
+ Output: {{"intent": "QUERY", "entity": null, "parameters": {{"queryText": "SELECT * FROM Projekt WHERE plz = '8000'", "queryType": "sql"}}, "confidence": 1.0}}
+"""
+
+ try:
+ # Use AI planning call for structured JSON response
+ response = await aiService.callAiPlanning(
+ prompt=intentPrompt,
+ debugType="intentanalysis"
+ )
+
+ # Parse JSON response
+ intentData = json.loads(response)
+
+ # Validate response structure
+ if "intent" not in intentData or "entity" not in intentData:
+ raise ValueError("Invalid intent analysis response structure")
+
+ return intentData
+
+ except json.JSONDecodeError as e:
+ logger.error(f"Failed to parse AI intent analysis response: {e}")
+ logger.error(f"Raw response: {response}")
+ raise ValueError(f"AI returned invalid JSON: {str(e)}")
+ except Exception as e:
+ logger.error(f"Error analyzing user intent: {str(e)}")
+ raise
+
+
+async def executeIntentBasedOperation(
+ currentUser: User,
+ intent: str,
+ entity: Optional[str],
+ parameters: Dict[str, Any],
+) -> Dict[str, Any]:
+ """
+ Execute CRUD operation based on analyzed intent.
+
+ Args:
+ currentUser: Current authenticated user
+ intent: Intent from AI analysis (CREATE, READ, UPDATE, DELETE, QUERY)
+ entity: Entity type from AI analysis
+ parameters: Extracted parameters from AI analysis
+
+ Returns:
+ Operation result
+ """
+ try:
+ if intent == "QUERY":
+ # Execute database query directly (stateless)
+ queryText = parameters.get("queryText", "")
+
+ result = await executeDirectQuery(
+ currentUser=currentUser,
+ queryText=queryText,
+ parameters=parameters.get("queryParameters"),
+ )
+ return result
+
+ elif intent == "CREATE":
+ # Create new entity
+ realEstateInterface = getRealEstateInterface(currentUser)
+
+ if entity == "Projekt":
+ projekt = Projekt(
+ mandateId=currentUser.mandateId,
+ label=parameters.get("label", ""),
+ statusProzess=StatusProzess(parameters.get("statusProzess", "EINGANG")) if parameters.get("statusProzess") else None,
+ )
+ created = realEstateInterface.createProjekt(projekt)
+ return {"operation": "CREATE", "entity": "Projekt", "result": created.model_dump()}
+
+ elif entity == "Parzelle":
+ parzelle = Parzelle(
+ mandateId=currentUser.mandateId,
+ label=parameters.get("label", ""),
+ # ... weitere Parameter
+ )
+ created = realEstateInterface.createParzelle(parzelle)
+ return {"operation": "CREATE", "entity": "Parzelle", "result": created.model_dump()}
+
+ else:
+ raise ValueError(f"CREATE operation not supported for entity: {entity}")
+
+ elif intent == "READ":
+ # Read entities
+ realEstateInterface = getRealEstateInterface(currentUser)
+
+ if entity == "Projekt":
+ # Apply filters from parameters
+ projektId = parameters.get("id")
+ if projektId:
+ projekt = realEstateInterface.getProjekt(projektId)
+ return {"operation": "READ", "entity": "Projekt", "result": projekt.model_dump() if projekt else None}
+ else:
+ # List all projects (with optional filters)
+ # Note: You may need to implement getProjekte() method
+ raise NotImplementedError("List operation needs to be implemented")
+
+ else:
+ raise ValueError(f"READ operation not supported for entity: {entity}")
+
+ elif intent == "UPDATE":
+ # Update existing entity
+ realEstateInterface = getRealEstateInterface(currentUser)
+
+ if entity == "Projekt":
+ projektId = parameters.get("id")
+ if not projektId:
+ raise ValueError("UPDATE operation requires entity ID")
+
+ # Get existing projekt
+ projekt = realEstateInterface.getProjekt(projektId)
+ if not projekt:
+ raise ValueError(f"Projekt {projektId} not found")
+
+ # Update fields
+ updateData = {k: v for k, v in parameters.items() if k != "id"}
+ updated = realEstateInterface.updateProjekt(projektId, updateData)
+ return {"operation": "UPDATE", "entity": "Projekt", "result": updated.model_dump()}
+
+ else:
+ raise ValueError(f"UPDATE operation not supported for entity: {entity}")
+
+ elif intent == "DELETE":
+ # Delete entity
+ realEstateInterface = getRealEstateInterface(currentUser)
+
+ if entity == "Projekt":
+ projektId = parameters.get("id")
+ if not projektId:
+ raise ValueError("DELETE operation requires entity ID")
+
+ success = realEstateInterface.deleteProjekt(projektId)
+ return {"operation": "DELETE", "entity": "Projekt", "success": success}
+
+ else:
+ raise ValueError(f"DELETE operation not supported for entity: {entity}")
+
+ else:
+ raise ValueError(f"Unknown intent: {intent}")
+
+ except Exception as e:
+ logger.error(f"Error executing intent-based operation: {str(e)}")
+ raise
+
+
+# ===== Erweiterte Query-Funktion mit AI-Unterstützung =====
+
+async def executeNaturalLanguageQuery(
+ currentUser: User,
+ naturalLanguageQuery: str,
+) -> Dict[str, Any]:
+ """
+ Execute a natural language query by translating it to SQL using AI.
+
+ Args:
+ currentUser: Current authenticated user
+ naturalLanguageQuery: Natural language query (e.g., "Zeige mir alle Projekte in Zürich")
+
+ Returns:
+ Query result with metadata (stateless, no session)
+ """
+ try:
+ services = getServices(currentUser, workflow=None)
+ aiService = services.ai
+
+ # Step 1: Translate natural language to SQL using AI
+ sqlQuery = await translateNaturalLanguageToSQL(aiService, naturalLanguageQuery, currentUser.mandateId)
+
+ logger.info(f"Translated '{naturalLanguageQuery}' to SQL: {sqlQuery}")
+
+ # Step 2: Execute the SQL query directly (stateless)
+ result = await executeDirectQuery(
+ currentUser=currentUser,
+ queryText=sqlQuery,
+ )
+
+ return result
+
+ except Exception as e:
+ logger.error(f"Error executing natural language query: {str(e)}")
+ raise
+
+
+async def translateNaturalLanguageToSQL(
+ aiService,
+ naturalLanguageQuery: str,
+ mandateId: str
+) -> str:
+ """
+ Use AI to translate natural language query to SQL.
+
+ Args:
+ aiService: AI service instance
+ naturalLanguageQuery: Natural language query
+ mandateId: User's mandate ID for filtering
+
+ Returns:
+ SQL query string with mandateId filter applied
+ """
+ translationPrompt = f"""
+Translate the following natural language query into a valid PostgreSQL SQL SELECT statement.
+
+Natural Language Query: "{naturalLanguageQuery}"
+
+Available tables and their fields:
+- Projekt: id, mandateId, label, statusProzess, perimeter, baulinie, parzellen (JSONB), dokumente (JSONB)
+- Parzelle: id, mandateId, label, strasseNr, plz, bauzone, az, bz, kontextKanton, kontextGemeinde
+- Dokument: id, mandateId, label, dokumentTyp, dokumentReferenz, mimeType
+- Kanton: id, mandateId, label, abk
+- Gemeinde: id, mandateId, label, plz
+
+Rules:
+1. Always include 'mandateId' filter based on user context (use placeholder {{mandateId}})
+2. Only use SELECT statements (no INSERT, UPDATE, DELETE)
+3. Return ONLY the SQL query, no explanations
+4. Use proper PostgreSQL syntax
+5. For text searches, use ILIKE for case-insensitive matching
+
+Examples:
+- Input: "Zeige mir alle Projekte"
+ Output: SELECT * FROM Projekt WHERE mandateId = '{{mandateId}}'
+
+- Input: "Zeige mir alle Parzellen in Zürich"
+ Output: SELECT p.* FROM Parzelle p JOIN Gemeinde g ON p.kontextGemeinde = g.id WHERE g.label ILIKE '%Zürich%' AND p.mandateId = '{{mandateId}}'
+
+- Input: "Wie viele Projekte haben Status 'Planung'?"
+ Output: SELECT COUNT(*) as count FROM Projekt WHERE statusProzess = 'Planung' AND mandateId = '{{mandateId}}'
+
+Now translate this query:
+"""
+
+ try:
+ # Use AI planning call for SQL generation
+ response = await aiService.callAiPlanning(
+ prompt=translationPrompt,
+ debugType="sqltranslation"
+ )
+
+ # Clean response (remove markdown code blocks if present)
+ sqlQuery = response.strip()
+ if sqlQuery.startswith("```sql"):
+ sqlQuery = sqlQuery[6:]
+ if sqlQuery.startswith("```"):
+ sqlQuery = sqlQuery[3:]
+ if sqlQuery.endswith("```"):
+ sqlQuery = sqlQuery[:-3]
+ sqlQuery = sqlQuery.strip()
+
+ # Replace placeholder with actual mandateId
+ sqlQuery = sqlQuery.replace("{{mandateId}}", mandateId)
+
+ return sqlQuery
+
+ except Exception as e:
+ logger.error(f"Error translating natural language to SQL: {str(e)}")
+ raise ValueError(f"Failed to translate query: {str(e)}")
+```
+
+## Wichtige Punkte:
+
+### 1. Services-Initialisierung
+
+- **`getServices(currentUser, workflow=None)`** - Initialisiert Services für AI-Zugriff
+- **`services.ai`** - Zugriff auf AI-Service für AI-Aufrufe
+
+### 2. AI-Aufrufe
+
+- **`callAiPlanning()`** - Für strukturierte JSON-Antworten (Intent-Analyse, SQL-Übersetzung)
+- **`callAiText()`** - Für einfache Text-Generierung
+- **`callAiDocuments()`** - Für Dokumenten-Verarbeitung
+
+### 3. Intent-Analyse
+
+Die AI analysiert User-Input und gibt zurück:
+- **Intent**: CREATE, READ, UPDATE, DELETE, QUERY
+- **Entity**: Projekt, Parzelle, Dokument, etc.
+- **Parameters**: Extrahierte Werte aus dem Input
+
+### 4. CRUD-Operationen
+
+Basierend auf der Intent-Analyse:
+- **CREATE** → `interface.createProjekt()`, `interface.createParzelle()`, etc.
+- **READ** → `interface.getProjekt()`, `interface.getParzelle()`, etc.
+- **UPDATE** → `interface.updateProjekt()`, etc.
+- **DELETE** → `interface.deleteProjekt()`, etc.
+- **QUERY** → `interface.executeQuery()` oder `executeDatabaseQuery()`
+
+### 5. Natural Language to SQL
+
+- AI übersetzt natürliche Sprache in SQL-Queries
+- Automatische Validierung und Sanitization empfohlen
+- MandateId-Filter wird automatisch hinzugefügt
+
+### 6. Error Handling
+
+- Umfassendes Error Handling für AI-Aufrufe
+- JSON-Parsing mit Fallback
+- Logging für Debugging
+
+---
+
+## Beispiel-Verwendung:
+
+```python
+# In einer Route (stateless):
+@router.post("/command")
+async def process_command(
+ userInput: str = Body(...),
+ currentUser: User = Depends(getCurrentUser)
+):
+ result = await processNaturalLanguageCommand(
+ currentUser=currentUser,
+ userInput=userInput
+ )
+ return result
+
+# Direkte Query (stateless):
+@router.post("/query")
+async def execute_query(
+ queryText: str = Body(...),
+ currentUser: User = Depends(getCurrentUser)
+):
+ result = await executeDirectQuery(
+ currentUser=currentUser,
+ queryText=queryText
+ )
+ return result
+```
+
+**User-Input-Beispiele:**
+- `"Erstelle ein neues Projekt namens 'Hauptstrasse 42'"`
+- `"Zeige mir alle Projekte in Zürich"`
+- `"Aktualisiere Projekt XYZ mit Status 'Planung'"`
+- `"Wie viele Parzellen haben Bauzone W3?"`
+
+---
+
+## Vollständiger Flow: User-Input → CRUD-Operation (stateless)
+
+### Beispiel: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+
+```
+1. User sendet HTTP POST Request
+ POST /api/realestate/command
+ Body: {"userInput": "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"}
+
+2. Route ruft Feature-Logik auf
+ → processNaturalLanguageCommand(currentUser, userInput)
+ # Keine Session-ID notwendig!
+
+3. Feature-Logik initialisiert Services
+ → services = getServices(currentUser, workflow=None)
+ → aiService = services.ai
+
+4. AI analysiert User-Input
+ → analyzeUserIntent(aiService, userInput)
+ → AI gibt zurück:
+ {
+ "intent": "CREATE",
+ "entity": "Projekt",
+ "parameters": {"label": "Hauptstrasse 42"},
+ "confidence": 0.95
+ }
+
+5. Feature-Logik führt CRUD-Operation aus
+ → executeIntentBasedOperation(intent="CREATE", entity="Projekt", ...)
+ → realEstateInterface = getRealEstateInterface(currentUser)
+ → projekt = Projekt(mandateId=..., label="Hauptstrasse 42")
+ → created = realEstateInterface.createProjekt(projekt)
+
+6. Interface speichert in Datenbank
+ → DatabaseConnector.recordCreate(Projekt, projekt.model_dump())
+ → PostgreSQL INSERT INTO Projekt ...
+
+7. Ergebnis wird direkt zurückgegeben
+ → Route gibt HTTP Response zurück
+ → Keine Session-Speicherung, keine History
+ → Frontend zeigt Erfolg
+```
+
+---
+
+## AI-Service Methoden im Detail
+
+### `callAiPlanning()` - Für strukturierte Antworten
+
+**Verwendung:** Intent-Analyse, SQL-Übersetzung, strukturierte Daten-Extraktion
+
+```python
+response = await aiService.callAiPlanning(
+ prompt=intentPrompt,
+ debugType="intentanalysis" # Optional: für Debug-Dateien
+)
+# Response ist JSON-String, muss geparst werden
+intentData = json.loads(response)
+```
+
+**Vorteile:**
+- Optimiert für strukturierte JSON-Antworten
+- Verwendet beste Modelle für Planungs-Aufgaben
+- Automatisches Debug-File-Writing
+
+### `callAiText()` - Für einfache Text-Generierung
+
+**Verwendung:** Text-Generierung, Zusammenfassungen, Erklärungen
+
+```python
+response = await aiService.callAiText(
+ prompt="Erkläre mir...",
+ documents=None, # Optional: Dokumente für Kontext
+ options=AiCallOptions(...)
+)
+# Response ist direkt Text-String
+```
+
+### `callAiDocuments()` - Für Dokumenten-Verarbeitung
+
+**Verwendung:** Dokumenten-Analyse, Extraktion, Generierung mit Dokumenten-Kontext
+
+```python
+response = await aiService.callAiDocuments(
+ prompt="Analysiere diese Dokumente...",
+ documents=[ChatDocument(...), ...],
+ options=AiCallOptions(...),
+ outputFormat="json" # Optional: Format für Output
+)
+```
+
+---
+
+## Best Practices für AI-Integration
+
+### 1. Prompt-Engineering
+
+- **Klare Struktur**: Definieren Sie genau, welche Antwort Sie erwarten
+- **Beispiele**: Geben Sie Beispiele für bessere Ergebnisse
+- **Format**: Spezifizieren Sie das erwartete Format (JSON, SQL, etc.)
+
+### 2. Error Handling
+
+- **JSON-Parsing**: Immer try/except für JSON-Parsing
+- **Fallback**: Planen Sie Fallback-Strategien bei AI-Fehlern
+- **Validierung**: Validieren Sie AI-Antworten vor Verwendung
+
+### 3. Sicherheit
+
+- **Query-Validierung**: Validieren Sie SQL-Queries vor Ausführung
+- **Parameter-Sanitization**: Sanitizen Sie alle Parameter
+- **MandateId-Filter**: Stellen Sie sicher, dass MandateId immer gefiltert wird
+
+### 4. Performance
+
+- **Caching**: Cache häufige AI-Antworten wenn möglich
+- **Model-Auswahl**: Lassen Sie das System automatisch das beste Modell wählen
+- **Async**: Nutzen Sie async/await für nicht-blockierende Operationen
+
+### 5. Debugging
+
+- **Debug-Files**: Nutzen Sie `debugType` Parameter für Debug-Dateien
+- **Logging**: Loggen Sie alle AI-Aufrufe und Antworten
+- **Confidence-Scores**: Nutzen Sie Confidence-Scores für Fehlerbehandlung
+
+---
+
+## Erweiterte Features
+
+### Schema-Aware Prompting
+
+Sie können das Datenbank-Schema in Prompts einbinden:
+
+```python
+# Lade Schema-Informationen
+schemaInfo = getDatabaseSchema() # Ihre Funktion
+
+prompt = f"""
+Available database schema:
+{schemaInfo}
+
+User query: "{userInput}"
+...
+"""
+```
+
+### Context-Aware Operations (Optional)
+
+Falls Sie später Kontext zwischen Queries benötigen, können Sie optional eine Session verwenden:
+
+```python
+# Optional: Session für Kontext (nur wenn nötig)
+# Für stateless Operationen nicht notwendig
+
+# Falls Session gewünscht:
+sessionId = parameters.get("sessionId") # Optional
+if sessionId:
+ previousQueries = interface.getQueries(sessionId=sessionId)
+ context = "\n".join([q.queryText for q in previousQueries[-5:]])
+else:
+ context = "" # Kein Kontext bei stateless Operationen
+
+prompt = f"""
+{context if context else ""}
+User query: "{userInput}"
+...
+"""
+```
+
+### Multi-Step Operations
+
+Für komplexe Operationen können Sie mehrere AI-Calls machen:
+
+```python
+# Schritt 1: Intent-Analyse
+intent = await analyzeUserIntent(aiService, userInput)
+
+# Schritt 2: Parameter-Validierung
+if intent["intent"] == "CREATE":
+ validatedParams = await validateParameters(aiService, intent["parameters"])
+
+# Schritt 3: CRUD-Operation
+result = await executeIntentBasedOperation(...)
+```
+
+---
+
+[← Zurück: Interface erstellen](03-interfaces.md) | [Weiter: Routen erstellen →](05-routes.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/05-routes.md b/docs/real-estate-feature-integration-guide/05-routes.md
new file mode 100644
index 00000000..04908fd7
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/05-routes.md
@@ -0,0 +1,332 @@
+# Schritt 4: Routen erstellen
+
+[← Zurück: Feature-Logik implementieren](04-feature-logic.md) | [Weiter: Router registrieren →](06-router-registration.md)
+
+**Datei:** `modules/routes/routeRealEstate.py`
+
+Die Routen definieren die REST-API-Endpunkte für das Feature. Das Feature arbeitet **stateless** ohne Session-Management.
+
+## Route-Struktur
+
+```
+/api/realestate/
+ ├── POST /command → Natürliche Sprache → CRUD-Operation
+ └── POST /query → Direkte SQL-Query
+```
+
+## Beispiel-Implementierung:
+
+```python
+"""
+Real Estate routes for the backend API.
+Implements stateless endpoints for real estate database operations with AI-powered natural language processing.
+"""
+
+import logging
+from typing import Optional, Dict, Any
+from fastapi import APIRouter, HTTPException, Depends, Body, Request
+from modules.security.auth import limiter, getCurrentUser
+from modules.datamodels.datamodelUam import User
+from modules.features.realEstate.mainRealEstate import (
+ processNaturalLanguageCommand,
+ executeDirectQuery,
+)
+
+# Configure logger
+logger = logging.getLogger(__name__)
+
+# Create router for real estate endpoints
+router = APIRouter(
+ prefix="/api/realestate",
+ tags=["Real Estate"],
+ responses={404: {"description": "Not found"}}
+)
+
+
+# ===== Stateless Command Endpoint =====
+
+@router.post("/command", response_model=Dict[str, Any])
+@limiter.limit("120/minute")
+async def process_command(
+ request: Request,
+ userInput: str = Body(..., embed=True, description="Natural language command"),
+ currentUser: User = Depends(getCurrentUser)
+) -> Dict[str, Any]:
+ """
+ Process natural language command and execute corresponding CRUD operation.
+
+ Uses AI to analyze user intent and extract parameters, then executes the appropriate
+ CRUD operation. Works stateless without session management.
+
+ Example user inputs:
+ - "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ - "Zeige mir alle Projekte in Zürich"
+ - "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ - "Lösche Parzelle ABC"
+ - "SELECT * FROM Projekt WHERE plz = '8000'"
+
+ Returns:
+ {
+ "success": true,
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|...|null",
+ "result": {...}
+ }
+ """
+ try:
+ result = await processNaturalLanguageCommand(
+ currentUser=currentUser,
+ userInput=userInput
+ )
+ return result
+ except ValueError as e:
+ logger.error(f"Validation error: {str(e)}")
+ raise HTTPException(
+ status_code=400,
+ detail=str(e)
+ )
+ except Exception as e:
+ logger.error(f"Error processing command: {str(e)}")
+ raise HTTPException(
+ status_code=500,
+ detail=str(e)
+ )
+
+
+# ===== Stateless Query Endpoint =====
+
+@router.post("/query", response_model=Dict[str, Any])
+@limiter.limit("120/minute")
+async def execute_query(
+ request: Request,
+ queryText: str = Body(..., embed=True, description="SQL query text"),
+ parameters: Optional[Dict[str, Any]] = Body(None, embed=True, description="Optional query parameters for parameterized queries"),
+ currentUser: User = Depends(getCurrentUser)
+) -> Dict[str, Any]:
+ """
+ Execute a direct SQL query without session management.
+
+ Executes the query directly and returns the result. No query history is saved.
+
+ WARNING: This endpoint executes raw SQL queries. Ensure proper validation
+ and sanitization on the frontend. Consider implementing query whitelisting
+ or only allowing SELECT statements for production use.
+
+ Returns:
+ {
+ "status": "success",
+ "rows": [...],
+ "columns": [...],
+ "rowCount": 15,
+ "executionTime": 0.123
+ }
+ """
+ try:
+ result = await executeDirectQuery(
+ currentUser=currentUser,
+ queryText=queryText,
+ parameters=parameters,
+ )
+ return result
+ except ValueError as e:
+ logger.error(f"Validation error: {str(e)}")
+ raise HTTPException(
+ status_code=400,
+ detail=str(e)
+ )
+ except Exception as e:
+ logger.error(f"Error executing query: {str(e)}")
+ raise HTTPException(
+ status_code=500,
+ detail=str(e)
+ )
+```
+
+## Wichtige Punkte:
+
+### 1. Stateless Design
+
+- **Keine Session-Management**: Alle Endpunkte arbeiten stateless
+- **Direkte Verarbeitung**: User-Input wird direkt verarbeitet und Ergebnis zurückgegeben
+- **Keine History**: Queries werden nicht gespeichert (kann optional später hinzugefügt werden)
+
+### 2. API-Endpunkte
+
+**`POST /api/realestate/command`**
+- Verarbeitet natürliche Sprache
+- Nutzt AI für Intent-Analyse
+- Führt CRUD-Operationen aus
+- Gibt Ergebnis direkt zurück
+
+**`POST /api/realestate/query`**
+- Führt direkte SQL-Queries aus
+- Keine Session notwendig
+- Gibt Query-Ergebnis direkt zurück
+
+### 3. Sicherheit
+
+- **Rate Limiting**: `@limiter.limit("120/minute")` für API-Schutz
+- **Authentication**: `Depends(getCurrentUser)` für alle Endpunkte
+- **Query-Validierung**: WICHTIG - Validieren Sie SQL-Queries vor Ausführung
+- **MandateId-Filter**: Wird automatisch durch Interfaces angewendet
+
+### 4. Error Handling
+
+- Umfassendes Error Handling mit HTTPException
+- Unterschiedliche Status-Codes: 400 (Validation), 404 (Not Found), 500 (Server Error)
+- Detaillierte Fehlermeldungen für Debugging
+
+### 5. Response-Struktur
+
+**Command-Endpunkt:**
+```json
+{
+ "success": true,
+ "intent": "CREATE",
+ "entity": "Projekt",
+ "result": {
+ "operation": "CREATE",
+ "entity": "Projekt",
+ "result": {
+ "id": "projekt_123",
+ "label": "Hauptstrasse 42",
+ ...
+ }
+ }
+}
+```
+
+**Query-Endpunkt:**
+```json
+{
+ "status": "success",
+ "rows": [
+ {"id": "...", "label": "...", ...}
+ ],
+ "columns": ["id", "label", ...],
+ "rowCount": 15,
+ "executionTime": 0.123
+}
+```
+
+---
+
+## Beispiel-Requests
+
+### Command-Endpunkt
+
+```bash
+# CREATE Operation
+POST /api/realestate/command
+Content-Type: application/json
+Authorization: Bearer
+
+{
+ "userInput": "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+}
+
+# READ Operation
+POST /api/realestate/command
+{
+ "userInput": "Zeige mir alle Projekte in Zürich"
+}
+
+# UPDATE Operation
+POST /api/realestate/command
+{
+ "userInput": "Aktualisiere Projekt XYZ mit Status 'Planung'"
+}
+
+# DELETE Operation
+POST /api/realestate/command
+{
+ "userInput": "Lösche Parzelle ABC"
+}
+
+# QUERY Operation (SQL wird erkannt)
+POST /api/realestate/command
+{
+ "userInput": "SELECT * FROM Projekt WHERE plz = '8000'"
+}
+```
+
+### Query-Endpunkt
+
+```bash
+# Direkte SQL-Query
+POST /api/realestate/query
+Content-Type: application/json
+Authorization: Bearer
+
+{
+ "queryText": "SELECT * FROM Projekt WHERE plz = '8000'"
+}
+
+# Parameterized Query
+POST /api/realestate/query
+{
+ "queryText": "SELECT * FROM Projekt WHERE plz = $1",
+ "parameters": {"$1": "8000"}
+}
+```
+
+---
+
+## Flow: Route → Feature-Logik
+
+### Command-Endpunkt Flow
+
+```
+POST /api/realestate/command
+ ↓
+routeRealEstate.process_command()
+ ↓
+getCurrentUser() # Auth
+ ↓
+processNaturalLanguageCommand(currentUser, userInput)
+ ↓
+mainRealEstate.processNaturalLanguageCommand()
+ ↓
+analyzeUserIntent() → executeIntentBasedOperation()
+ ↓
+return Dict mit Ergebnis
+```
+
+### Query-Endpunkt Flow
+
+```
+POST /api/realestate/query
+ ↓
+routeRealEstate.execute_query()
+ ↓
+getCurrentUser() # Auth
+ ↓
+executeDirectQuery(currentUser, queryText, parameters)
+ ↓
+mainRealEstate.executeDirectQuery()
+ ↓
+getChatInterface(currentUser)
+ ↓
+RealEstateChatObjects.executeQuery(queryText)
+ ↓
+DatabaseConnector.executeQuery(sql)
+ ↓
+return Dict mit rows, columns, rowCount
+```
+
+---
+
+## Vorteile des stateless Ansatzes
+
+- **Einfachheit**: Kein Session-Management notwendig
+- **Performance**: Weniger Datenbank-Operationen pro Request
+- **Skalierbarkeit**: Stateless Requests sind einfacher zu skalieren
+- **Flexibilität**: Jeder Request ist unabhängig
+- **Schnell**: Direkte Verarbeitung ohne Overhead
+
+---
+
+[← Zurück: Feature-Logik implementieren](04-feature-logic.md) | [Weiter: Router registrieren →](06-router-registration.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/06-router-registration.md b/docs/real-estate-feature-integration-guide/06-router-registration.md
new file mode 100644
index 00000000..430c88b2
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/06-router-registration.md
@@ -0,0 +1,45 @@
+# Schritt 5: Router registrieren
+
+[← Zurück: Routen erstellen](05-routes.md) | [Weiter: Environment-Konfiguration →](07-environment.md)
+
+**Datei:** `app.py`
+
+Der Router muss in der Hauptanwendung registriert werden.
+
+## Änderung in app.py:
+
+```python
+# ... existing imports ...
+
+# Include all routers
+
+from modules.routes.routeAdmin import router as generalRouter
+app.include_router(generalRouter)
+
+# ... existing routers ...
+
+from modules.routes.routeChatPlayground import router as chatPlaygroundRouter
+app.include_router(chatPlaygroundRouter)
+
+# NEU: Real Estate Router hinzufügen (Chat-Interface)
+from modules.routes.routeRealEstate import router as realEstateRouter
+app.include_router(realEstateRouter)
+
+# NEU: Real Estate Data Router hinzufügen (falls CRUD-API gewünscht)
+# from modules.routes.routeRealEstateData import router as realEstateDataRouter
+# app.include_router(realEstateDataRouter)
+
+from modules.routes.routeSecurityLocal import router as localRouter
+app.include_router(localRouter)
+
+# ... rest of routers ...
+```
+
+**Wichtig**: Die Reihenfolge der Router-Registrierung kann wichtig sein, wenn es Überschneidungen in den Pfaden gibt. Allgemeinere Routen sollten nach spezifischeren Routen kommen.
+
+---
+
+[← Zurück: Routen erstellen](05-routes.md) | [Weiter: Environment-Konfiguration →](07-environment.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/07-environment.md b/docs/real-estate-feature-integration-guide/07-environment.md
new file mode 100644
index 00000000..73765e61
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/07-environment.md
@@ -0,0 +1,50 @@
+# Schritt 6: Environment-Konfiguration
+
+[← Zurück: Router registrieren](06-router-registration.md) | [Weiter: Feature Lifecycle →](08-lifecycle.md)
+
+**Datei:** `env_dev.env`
+
+Für das realEstate-Feature benötigen wir keine zusätzlichen Environment-Variablen, da es die bereits vorhandenen PostgreSQL-Konfigurationen nutzt:
+
+```env
+# PostgreSQL Storage (bereits vorhanden)
+DB_APP_HOST=localhost
+DB_APP_DATABASE=poweron_app
+DB_APP_USER=poweron_dev
+DB_APP_PASSWORD_SECRET = DEV_ENC:...
+DB_APP_PORT=5432
+```
+
+**Optional**: Falls Sie eine separate Datenbank für Real Estate verwenden möchten, können Sie zusätzliche Variablen hinzufügen:
+
+```env
+# Optional: Separate Real Estate Database
+DB_REALESTATE_HOST=localhost
+DB_REALESTATE_DATABASE=poweron_realestate
+DB_REALESTATE_USER=poweron_dev
+DB_REALESTATE_PASSWORD_SECRET = DEV_ENC:...
+DB_REALESTATE_PORT=5432
+```
+
+In diesem Fall müssten Sie die `_initializeDatabase()` Methode im Interface anpassen:
+
+```python
+def _initializeDatabase(self):
+ """Initialize PostgreSQL database connection."""
+ try:
+ # Use Real Estate specific config if available, otherwise fall back to APP config
+ dbHost = APP_CONFIG.get("DB_REALESTATE_HOST") or APP_CONFIG.get("DB_APP_HOST", "localhost")
+ dbDatabase = APP_CONFIG.get("DB_REALESTATE_DATABASE") or APP_CONFIG.get("DB_APP_DATABASE", "poweron_app")
+ dbUser = APP_CONFIG.get("DB_REALESTATE_USER") or APP_CONFIG.get("DB_APP_USER")
+ dbPassword = APP_CONFIG.get("DB_REALESTATE_PASSWORD_SECRET") or APP_CONFIG.get("DB_APP_PASSWORD_SECRET")
+ dbPort = int(APP_CONFIG.get("DB_REALESTATE_PORT") or APP_CONFIG.get("DB_APP_PORT", 5432))
+
+ # ... rest of initialization ...
+```
+
+---
+
+[← Zurück: Router registrieren](06-router-registration.md) | [Weiter: Feature Lifecycle →](08-lifecycle.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/08-lifecycle.md b/docs/real-estate-feature-integration-guide/08-lifecycle.md
new file mode 100644
index 00000000..0946e331
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/08-lifecycle.md
@@ -0,0 +1,43 @@
+# Schritt 7: Feature Lifecycle (optional)
+
+[← Zurück: Environment-Konfiguration](07-environment.md) | [Weiter: Datenbank-Schema →](09-database-schema.md)
+
+**Datei:** `modules/features/featuresLifecycle.py`
+
+Falls Ihr Feature Hintergrundprozesse oder Initialisierung beim Start benötigt, können Sie diese hier hinzufügen:
+
+```python
+async def start() -> None:
+ """ Start feature triggers and background managers """
+
+ # Provide Event User
+ rootInterface = getRootInterface()
+ eventUser = rootInterface.getUserByUsername("event")
+
+ # ... existing features ...
+
+ # Feature RealEstate (optional)
+ # from modules.features.realEstate import mainRealEstate
+ # mainRealEstate.initializeFeature(eventUser)
+ # logger.info("Real Estate feature initialized")
+
+ return True
+
+
+async def stop() -> None:
+ """ Stop feature triggers and background managers """
+
+ # Feature RealEstate cleanup (optional)
+ # from modules.features.realEstate import mainRealEstate
+ # mainRealEstate.cleanupFeature()
+ # logger.info("Real Estate feature cleaned up")
+
+ return True
+```
+
+---
+
+[← Zurück: Environment-Konfiguration](07-environment.md) | [Weiter: Datenbank-Schema →](09-database-schema.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/09-database-schema.md b/docs/real-estate-feature-integration-guide/09-database-schema.md
new file mode 100644
index 00000000..bd094003
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/09-database-schema.md
@@ -0,0 +1,247 @@
+# Datenbank-Schema
+
+[← Zurück: Feature Lifecycle](08-lifecycle.md) | [Weiter: Sicherheitshinweise →](10-security.md)
+
+Die Datenbank-Tabellen werden automatisch vom `DatabaseConnector` erstellt, basierend auf den Pydantic-Modellen:
+
+## Chat-Interface Tabellen:
+
+- **RealEstateQuery**: Speichert Abfragen
+- **RealEstateQueryResult**: Speichert Abfrageergebnisse (mit JSONB für `rowData`)
+- **RealEstateChatSession**: Speichert Chat-Sessions
+
+## Real Estate-Datenmodell Tabellen:
+
+Die folgenden Tabellen werden basierend auf den Real Estate-Datenmodell-Entitäten erstellt:
+
+- **Projekt**: Bauprojekte (mit `parzellen`, `dokumente`, `kontextInformationen` als JSONB)
+- **Parzelle**: Grundstücke mit Bauparametern (mit `parzellenNachbarschaft`, `dokumente`, `kontextInformationen` als JSONB)
+- **Dokument**: Dateien und URLs
+- **Kontext**: Zusatzinformationen
+- **GeoPolylinie**: Geometrische Linien/Polygone (mit `punkte` als JSONB)
+- **GeoPunkt**: 3D-Koordinaten
+- **Land**: Nationale Ebene (mit `dokumente`, `kontextInformationen` als JSONB)
+- **Kanton**: Kantonale Ebene (mit `dokumente`, `kontextInformationen` als JSONB)
+- **Gemeinde**: Gemeinde-Ebene (mit `dokumente`, `kontextInformationen` als JSONB)
+
+---
+
+## Automatische Tabellenerstellung
+
+### Wie funktioniert die automatische Tabellenerstellung?
+
+Der `DatabaseConnector` erstellt Tabellen **automatisch beim ersten Zugriff** auf ein Pydantic-Modell. Sie müssen keine SQL-CREATE-TABLE-Statements manuell schreiben.
+
+#### 1. Ablauf der Tabellenerstellung:
+
+```
+1. Code ruft z.B. `db.recordCreate(Projekt, projekt_data)` auf
+ ↓
+2. DatabaseConnector ruft `_ensureTableExists(Projekt)` auf
+ ↓
+3. Prüft ob Tabelle "Projekt" existiert (über information_schema)
+ ↓
+4. Wenn NICHT vorhanden:
+ → Ruft `_create_table_from_model()` auf
+ → Extrahiert Felder aus Pydantic-Modell mit `_get_model_fields()`
+ → Mappt Python-Typen zu SQL-Typen
+ → Erstellt CREATE TABLE Statement
+ → Führt SQL aus
+ → Erstellt Indexes für Foreign Keys
+```
+
+#### 2. Typ-Mapping (Python → PostgreSQL):
+
+Der `DatabaseConnector` mappt automatisch Pydantic-Feldtypen zu PostgreSQL-Datentypen:
+
+| Python/Pydantic Typ | PostgreSQL Typ | Beispiel |
+|---------------------|----------------|----------|
+| `str` oder `Optional[str]` | `TEXT` | `label: str` → `"label" TEXT` |
+| `int` | `INTEGER` | `vollgeschossZahl: int` → `"vollgeschossZahl" INTEGER` |
+| `float` | `DOUBLE PRECISION` | `az: float` → `"az" DOUBLE PRECISION` |
+| `bool` | `BOOLEAN` | `closed: bool` → `"closed" BOOLEAN` |
+| `Dict[str, Any]` oder `dict` | `JSONB` | `parameters: Dict[str, Any]` → `"parameters" JSONB` |
+| `List[...]` oder `list` | `JSONB` | `parzellen: List[Parzelle]` → `"parzellen" JSONB` |
+| `Optional[Enum]` | `TEXT` | `statusProzess: StatusProzess` → `"statusProzess" TEXT` |
+
+**Spezielle Felder:**
+- Felder mit Namen `*Id` (z.B. `kontextKantonId`) erhalten automatisch einen Index
+- Systemfelder werden automatisch hinzugefügt: `_createdAt`, `_createdBy`, `_modifiedAt`, `_modifiedBy`
+
+#### 3. Beispiel: CREATE TABLE Statement
+
+Für das `Projekt`-Modell würde automatisch folgendes SQL erstellt:
+
+```sql
+CREATE TABLE IF NOT EXISTS "Projekt" (
+ "id" VARCHAR(255) PRIMARY KEY,
+ "mandateId" TEXT,
+ "label" TEXT,
+ "statusProzess" TEXT,
+ "perimeter" JSONB,
+ "baulinie" JSONB,
+ "parzellen" JSONB,
+ "dokumente" JSONB,
+ "kontextInformationen" JSONB,
+ "_createdAt" DOUBLE PRECISION,
+ "_modifiedAt" DOUBLE PRECISION,
+ "_createdBy" VARCHAR(255),
+ "_modifiedBy" VARCHAR(255)
+);
+
+-- Automatisch erstellte Indexes für Foreign Keys:
+CREATE INDEX IF NOT EXISTS "idx_Projekt_mandateId" ON "Projekt" ("mandateId");
+```
+
+#### 4. Automatische Schema-Migrationen
+
+**Wichtig:** Der Connector unterstützt **additive Migrationen**:
+
+- Wenn eine Tabelle bereits existiert, werden **fehlende Spalten automatisch hinzugefügt**
+- **Bestehende Spalten werden NICHT gelöscht oder geändert**
+- Wenn Sie ein neues Feld zum Pydantic-Modell hinzufügen, wird es beim nächsten Zugriff automatisch als Spalte hinzugefügt
+
+**Beispiel:**
+```python
+# Ursprüngliches Modell
+class Projekt(BaseModel):
+ id: str
+ label: str
+ statusProzess: Optional[StatusProzess]
+
+# Später: Neues Feld hinzugefügt
+class Projekt(BaseModel):
+ id: str
+ label: str
+ statusProzess: Optional[StatusProzess]
+ beschreibung: Optional[str] # NEU
+
+# Beim nächsten recordCreate() wird automatisch ausgeführt:
+# ALTER TABLE "Projekt" ADD COLUMN "beschreibung" TEXT
+```
+
+#### 5. Wann werden Tabellen erstellt?
+
+Tabellen werden erstellt, wenn Sie **zum ersten Mal** eine der folgenden Operationen ausführen:
+
+- `db.recordCreate(model_class, data)` - Erstellt Record
+- `db.recordUpdate(model_class, recordId, data)` - Aktualisiert Record
+- `db.getRecordset(model_class)` - Lädt Records
+- `db.getRecord(model_class, recordId)` - Lädt einen Record
+
+**Beispiel:**
+```python
+# Beim ersten Aufruf wird die Tabelle "Projekt" automatisch erstellt
+interface = getInterface(currentUser)
+projekt = interface.createProjekt(label="Mein Projekt")
+# → Tabelle "Projekt" wird jetzt in PostgreSQL erstellt
+```
+
+#### 6. Manuelle Tabellenerstellung (optional)
+
+Falls Sie Tabellen manuell erstellen möchten (z.B. für Initialisierung), können Sie:
+
+```python
+from modules.connectors.connectorDbPostgre import DatabaseConnector
+from modules.datamodels.datamodelRealEstate import Projekt, Parzelle
+
+# Connector initialisieren
+db = DatabaseConnector(
+ dbHost="localhost",
+ dbDatabase="poweron_app",
+ dbUser="poweron_dev",
+ dbPassword="...",
+ dbPort=5432
+)
+
+# Tabellen explizit erstellen
+db._ensureTableExists(Projekt)
+db._ensureTableExists(Parzelle)
+# ... weitere Modelle
+```
+
+#### 7. Wichtige Hinweise:
+
+✅ **Automatisch:**
+- Tabellenerstellung beim ersten Zugriff
+- Spalten-Erstellung basierend auf Pydantic-Feldern
+- Index-Erstellung für Foreign Keys (`*Id` Felder)
+- Systemfelder (`_createdAt`, etc.) werden automatisch hinzugefügt
+
+❌ **NICHT automatisch:**
+- Foreign Key Constraints (werden nicht erstellt - Sie müssen sie manuell hinzufügen falls gewünscht)
+- Unique Constraints (außer PRIMARY KEY auf `id`)
+- Check Constraints
+- Trigger oder Stored Procedures
+
+⚠️ **Einschränkungen:**
+- **Keine Schema-Änderungen**: Wenn Sie einen Feldtyp ändern (z.B. `str` → `int`), wird die Spalte NICHT automatisch geändert
+- **Keine Spalten-Löschung**: Gelöschte Felder im Modell werden nicht aus der Datenbank entfernt
+- **Case-Sensitive**: Tabellennamen werden exakt wie der Klassenname verwendet (z.B. `Projekt`, nicht `projekt`)
+
+#### 8. Beispiel: Vollständiger Ablauf
+
+```python
+# 1. Pydantic-Modell definieren
+class Projekt(BaseModel):
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
+ mandateId: str
+ label: str
+ statusProzess: Optional[StatusProzess]
+ parzellen: List[Parzelle] = Field(default_factory=list)
+
+# 2. Interface initialisieren (erstellt noch keine Tabellen)
+interface = getInterface(currentUser)
+
+# 3. Ersten Record erstellen (erstellt jetzt die Tabelle!)
+projekt = interface.createProjekt(
+ label="Mein erstes Projekt",
+ statusProzess=StatusProzess.PLANUNG
+)
+# → Intern wird ausgeführt:
+# 1. _ensureTableExists(Projekt) aufgerufen
+# 2. Tabelle "Projekt" existiert nicht → wird erstellt
+# 3. CREATE TABLE "Projekt" (...) wird ausgeführt
+# 4. Record wird eingefügt
+
+# 4. Weitere Records können jetzt ohne Tabellenerstellung erstellt werden
+projekt2 = interface.createProjekt(label="Zweites Projekt")
+# → Tabelle existiert bereits, nur INSERT wird ausgeführt
+```
+
+---
+
+## Zusammenfassung:
+
+- ✅ **Tabellenname** = Klassenname des Pydantic-Modells (z.B. `Projekt`)
+- ✅ **Spalten** = Alle Felder aus dem Pydantic-Modell
+- ✅ **Typen** = Automatisch gemappt (str→TEXT, List→JSONB, etc.)
+- ✅ **Systemfelder** = Automatisch hinzugefügt (`_createdAt`, `_createdBy`, etc.)
+- ✅ **Indexes** = Automatisch für Felder mit `*Id` Suffix
+- ✅ **Migrationen** = Additive Migrationen (neue Spalten werden hinzugefügt)
+- ⚠️ **Keine Constraints** = Foreign Keys, Unique, Check müssen manuell erstellt werden
+
+## Beispiel-Abfragen auf Real Estate-Datenmodell:
+
+```sql
+-- Alle Parzellen in einer bestimmten Gemeinde
+SELECT * FROM Parzelle WHERE plz = '8000' ORDER BY label;
+
+-- Projekte mit Status "Planung"
+SELECT * FROM Projekt WHERE "statusProzess" = 'Planung';
+
+-- Parzellen mit bestimmter Bauzone
+SELECT label, az, bz, gebaeudehoeheMax FROM Parzelle WHERE bauzone = 'W3';
+
+-- Dokumente eines Projekts
+SELECT * FROM Dokument WHERE id IN (
+ SELECT unnest(dokumente::jsonb->>'id') FROM Projekt WHERE id = '...'
+);
+```
+
+---
+
+[← Zurück: Feature Lifecycle](08-lifecycle.md) | [Weiter: Sicherheitshinweise →](10-security.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/10-security.md b/docs/real-estate-feature-integration-guide/10-security.md
new file mode 100644
index 00000000..f5ab6b2e
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/10-security.md
@@ -0,0 +1,90 @@
+# Sicherheitshinweise
+
+[← Zurück: Datenbank-Schema](09-database-schema.md) | [Weiter: Testing →](11-testing.md)
+
+## ⚠️ WICHTIG: Query-Validierung
+
+Die aktuelle Implementierung erlaubt die Ausführung von **rohen SQL-Queries**. Für Produktion sollten Sie:
+
+1. **Query-Whitelisting**: Nur erlaubte Queries zulassen
+2. **Nur SELECT**: Nur SELECT-Statements erlauben (keine INSERT/UPDATE/DELETE)
+3. **Parameterized Queries**: Immer Parameterized Queries verwenden
+4. **Query-Parsing**: SQL-Parser verwenden zur Validierung
+5. **Rate Limiting**: Strikte Rate Limits setzen (bereits implementiert)
+
+## Beispiel für Query-Validierung:
+
+```python
+def validateQuery(queryText: str) -> bool:
+ """
+ Validate that query is safe to execute.
+ Only allows SELECT statements on Real Estate data model tables.
+ """
+ query_lower = queryText.strip().lower()
+
+ # Only allow SELECT statements
+ if not query_lower.startswith('select'):
+ return False
+
+ # Block dangerous keywords
+ dangerous_keywords = [
+ 'drop', 'delete', 'insert', 'update', 'alter', 'create',
+ 'truncate', 'grant', 'revoke', 'exec', 'execute', 'call'
+ ]
+ for keyword in dangerous_keywords:
+ if keyword in query_lower:
+ return False
+
+ # Only allow queries on Real Estate data model tables
+ allowed_tables = [
+ 'projekt', 'parzelle', 'dokument', 'kontext',
+ 'geopolylinie', 'geopunkt', 'land', 'kanton', 'gemeinde'
+ ]
+
+ # Check if query references allowed tables
+ # Simple check - in production, use SQL parser
+ query_contains_allowed_table = any(
+ f'from {table}' in query_lower or f'join {table}' in query_lower
+ for table in allowed_tables
+ )
+
+ if not query_contains_allowed_table:
+ # Allow queries that don't specify table explicitly (might be subqueries)
+ # But log for review
+ logger.warning(f"Query does not reference known Real Estate tables: {queryText[:100]}")
+
+ return True
+```
+
+## Erweiterte Validierung mit SQL-Parser:
+
+Für Produktion sollten Sie einen SQL-Parser verwenden:
+
+```python
+from sqlparse import parse, tokens
+
+def validateQueryAdvanced(queryText: str) -> bool:
+ """Advanced query validation using SQL parser."""
+ try:
+ parsed = parse(queryText)[0]
+
+ # Check statement type
+ if parsed.get_type() != 'SELECT':
+ return False
+
+ # Extract table names and validate
+ # Implementation depends on SQL parser library
+ # ...
+
+ return True
+ except Exception as e:
+ logger.error(f"Query parsing failed: {e}")
+ return False
+```
+
+---
+
+[← Zurück: Datenbank-Schema](09-database-schema.md) | [Weiter: Testing →](11-testing.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/11-testing.md b/docs/real-estate-feature-integration-guide/11-testing.md
new file mode 100644
index 00000000..2ec33c3a
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/11-testing.md
@@ -0,0 +1,51 @@
+# Testing
+
+[← Zurück: Sicherheitshinweise](10-security.md) | [Weiter: Troubleshooting →](12-troubleshooting.md)
+
+## Manuelle API-Tests mit curl:
+
+```bash
+# 1. Login (erhalten Sie Token)
+curl -X POST "http://localhost:8000/api/local/auth/login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "username=youruser&password=yourpass"
+
+# 2. Session erstellen
+curl -X POST "http://localhost:8000/api/realestate/sessions" \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"title": "Parzellen-Analyse Zürich"}'
+
+# 3. Query ausführen - Beispiel: Alle Parzellen in Zürich
+curl -X POST "http://localhost:8000/api/realestate/sessions/SESSION_ID/queries" \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "queryText": "SELECT label, plz, bauzone, az, bz, gebaeudehoeheMax FROM Parzelle WHERE plz = ''8000'' ORDER BY label LIMIT 20",
+ "queryType": "sql"
+ }'
+
+# 4. Query ausführen - Beispiel: Projekte mit Status "Planung"
+curl -X POST "http://localhost:8000/api/realestate/sessions/SESSION_ID/queries" \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "queryText": "SELECT id, label, \"statusProzess\" FROM Projekt WHERE \"statusProzess\" = ''Planung''",
+ "queryType": "sql"
+ }'
+
+# 5. Queries abrufen
+curl -X GET "http://localhost:8000/api/realestate/sessions/SESSION_ID/queries" \
+ -H "Authorization: Bearer YOUR_TOKEN"
+```
+
+## Swagger UI:
+
+Nach dem Start der Anwendung können Sie die API unter `http://localhost:8000/docs` testen.
+
+---
+
+[← Zurück: Sicherheitshinweise](10-security.md) | [Weiter: Troubleshooting →](12-troubleshooting.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/12-troubleshooting.md b/docs/real-estate-feature-integration-guide/12-troubleshooting.md
new file mode 100644
index 00000000..9c7b2475
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/12-troubleshooting.md
@@ -0,0 +1,33 @@
+# Troubleshooting
+
+[← Zurück: Testing](11-testing.md) | [Weiter: Zusammenfassung →](13-summary.md)
+
+## Problem: Datenbankverbindung schlägt fehl
+
+**Lösung**: Überprüfen Sie die Environment-Variablen in `env_dev.env`:
+- `DB_APP_HOST`
+- `DB_APP_DATABASE`
+- `DB_APP_USER`
+- `DB_APP_PASSWORD_SECRET`
+- `DB_APP_PORT`
+
+## Problem: Tabellen werden nicht erstellt
+
+**Lösung**: Der Connector erstellt Tabellen beim ersten Zugriff. Stellen Sie sicher, dass:
+- Die Datenbank existiert
+- Der Benutzer CREATE-Rechte hat
+- Die Verbindung erfolgreich ist
+
+## Problem: Access Denied Fehler
+
+**Lösung**: Überprüfen Sie:
+- User hat gültiges `mandateId`
+- User hat entsprechende Privilegien
+- Access Control Logik im Interface
+
+---
+
+[← Zurück: Testing](11-testing.md) | [Weiter: Zusammenfassung →](13-summary.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/13-summary.md b/docs/real-estate-feature-integration-guide/13-summary.md
new file mode 100644
index 00000000..240af342
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/13-summary.md
@@ -0,0 +1,136 @@
+# Zusammenfassung
+
+[← Zurück: Troubleshooting](12-troubleshooting.md) | [← Zurück zur Übersicht](README.md)
+
+## Dateinamen-Konvention:
+
+**Wichtig:** Die Dateien sind nach Funktionalität benannt:
+
+| Datei | Zweck | Enthält |
+|-------|-------|---------|
+| `datamodelRealEstateChat.py` | Chat-Interface Modelle | `RealEstateQuery`, `RealEstateQueryResult`, `RealEstateChatSession` |
+| `datamodelRealEstate.py` | Real Estate-Datenmodelle | `Projekt`, `Parzelle`, `Dokument`, etc. (allgemein verwendbar) |
+| `interfaceDbRealEstateChatObjects.py` | Chat-Interface Interface | Methoden für Sessions und Queries |
+| `interfaceDbRealEstateObjects.py` | Real Estate CRUD Interface | Methoden für Projekt, Parzelle, etc. (optional) |
+
+**Hinweis:** Das Modell ist allgemein für alle Real Estate-Firmen verwendbar. PEK ist nur ein Beispiel.
+
+---
+
+## Zu erstellende Dateien:
+
+1. **`modules/datamodels/datamodelRealEstateChat.py`** (Chat-Interface Modelle)
+ - Pydantic-Modelle: `RealEstateQuery`, `RealEstateQueryResult`, `RealEstateChatSession`
+ - Enums: `QueryStatusEnum`
+
+2. **`modules/datamodels/datamodelRealEstate.py`** (Real Estate-Datenmodell)
+ - Pydantic-Modelle: `Projekt`, `Parzelle`, `Dokument`, `Kontext`, `GeoPolylinie`, `GeoPunkt`, `Land`, `Kanton`, `Gemeinde`
+ - Enums: `StatusProzess`, `DokumentTyp`, `JaNein`, `GeoTag`
+ - Siehe `../PEK_datamodel_desc.md` für vollständige Spezifikation (PEK ist ein Beispiel, das Modell ist allgemein verwendbar)
+
+3. **`modules/interfaces/interfaceDbRealEstateChatObjects.py`** (Chat-Interface)
+ - `RealEstateChatObjects` Klasse für Datenbankzugriff (Chat-Sessions, Queries)
+ - `RealEstateChatAccess` Klasse für Zugriffskontrolle
+ - `getInterface()` Factory-Funktion
+
+4. **`modules/interfaces/interfaceDbRealEstateObjects.py`** (NEU - für Real Estate-Datenmodell CRUD)
+ - `RealEstateObjects` Klasse für CRUD-Operationen auf Real Estate-Entitäten (Projekt, Parzelle, etc.)
+ - `RealEstateAccess` Klasse für Zugriffskontrolle
+ - Methoden für Projekt, Parzelle, Dokument, etc.
+ - **Hinweis:** Diese Datei ist für CRUD-Operationen auf die Real Estate-Entitäten. Das Chat-Interface nutzt `interfaceDbRealEstateChatObjects.py` (siehe Punkt 3).
+ - **Optional:** Falls Sie eine separate CRUD-API benötigen (das Chat-Interface kann auch direkt SQL-Queries verwenden)
+
+5. **`modules/features/realEstate/mainRealEstate.py`**
+ - Feature-Logik-Funktionen: `createSession`, `executeDatabaseQuery`, etc.
+
+6. **`modules/routes/routeRealEstate.py`**
+ - FastAPI Router mit allen Endpunkten für Chat-Interface
+
+7. **`modules/routes/routeRealEstateData.py`** (NEU - für Real Estate-Datenmodell)
+ - FastAPI Router für CRUD-Operationen auf Real Estate-Entitäten
+ - Endpunkte für Projekt, Parzelle, Dokument, etc.
+ - **Optional:** Falls Sie eine separate CRUD-API benötigen (das Chat-Interface kann auch direkt SQL-Queries verwenden)
+
+## Zu modifizierende Dateien:
+
+1. **`app.py`**
+ - Router-Registrierung für `routeRealEstate` hinzufügen (Chat-Interface)
+ - Router-Registrierung für `routeRealEstateData` hinzufügen (falls CRUD-API gewünscht)
+
+2. **`env_dev.env`** (optional)
+ - Separate Datenbank-Konfiguration falls gewünscht
+ - PostGIS-Konfiguration falls geografische Abfragen benötigt werden
+
+3. **`modules/features/featuresLifecycle.py`** (optional)
+ - Feature-Initialisierung falls benötigt
+ - Initialisierung von Standard-Daten (z.B. Land "Schweiz", Kantone, Gemeinden)
+
+## Datenmodell-Implementierung:
+
+**Wichtig:** Bevor Sie das Chat-Interface nutzen können, müssen Sie die Real Estate-Datenmodell-Entitäten implementieren:
+
+1. **Erstellen Sie `modules/datamodels/datamodelRealEstate.py`** mit allen Entitäten aus `../PEK_datamodel_desc.md`
+ - **Hinweis:** PEK ist ein Beispiel für eine Real Estate-Firma, aber das Modell ist allgemein verwendbar für alle Real Estate-Firmen
+2. **Beachten Sie die Objektbeziehungen**:
+ - `parzellen: list[Parzelle]` wird als JSONB gespeichert
+ - `kontextKanton: Kanton` wird als String-ID gespeichert (Foreign Key)
+3. **Implementieren Sie die Enums** entsprechend der Spezifikation
+4. **Testen Sie die Tabellenerstellung** durch den DatabaseConnector
+
+---
+
+## Nächste Schritte
+
+1. **Real Estate-Datenmodell-Implementierung**:
+ - Erstellen Sie die Pydantic-Modelle für alle Real Estate-Entitäten (`Projekt`, `Parzelle`, `Dokument`, `Kontext`, `GeoPolylinie`, `GeoPunkt`, `Land`, `Kanton`, `Gemeinde`)
+ - Implementieren Sie die Enums (`StatusProzess`, `DokumentTyp`, `JaNein`, `GeoTag`)
+ - Siehe `../PEK_datamodel_desc.md` für vollständige Spezifikation (PEK ist ein Beispiel, das Modell ist allgemein verwendbar)
+
+2. **Query-Validierung implementieren**: Siehe [Sicherheitshinweise](10-security.md)
+ - Besonders wichtig für Real Estate-Datenmodell: Nur SELECT-Statements erlauben
+ - Whitelist für erlaubte Tabellen (Projekt, Parzelle, etc.)
+
+3. **Natural Language Processing**:
+ - Implementieren Sie NLP für `queryType="natural"`
+ - Beispiele: "Zeige mir alle Parzellen in Zürich" → SQL-Query
+ - Nutzen Sie AI-Modelle zur SQL-Generierung aus natürlicher Sprache
+
+4. **Geografische Abfragen**:
+ - PostGIS-Integration für räumliche Abfragen
+ - Beispiel: "Zeige alle Parzellen innerhalb eines bestimmten Perimeters"
+ - Nutzung von GeoPolylinie und GeoPunkt für GIS-Funktionen
+
+5. **Query-History**: Erweiterte Historie-Funktionen
+ - Speichern häufig verwendeter Queries
+ - Query-Templates für häufige Abfragen (z.B. "Parzellen nach Bauzone")
+
+6. **Export-Funktionen**: CSV/Excel-Export von Ergebnissen
+ - Export von Parzellen-Listen
+ - Export von Projekt-Übersichten
+
+7. **Caching**: Query-Ergebnisse cachen für wiederholte Abfragen
+ - Besonders für administrative Daten (Land, Kanton, Gemeinde)
+
+8. **Permissions**: Erweiterte Berechtigungen für bestimmte Tabellen
+ - Mandaten-basierte Filterung für Projekte und Parzellen
+ - Rollen-basierte Zugriffe (z.B. nur Leserechte für bestimmte Benutzer)
+
+---
+
+## Architektur-Zusammenfassung
+
+Dieses Feature folgt dem etablierten Muster des Projekts:
+- **Separation of Concerns**: Routes → Features → Interfaces → Connectors
+- **Dependency Injection**: Interfaces werden über Factory-Funktionen erstellt
+- **Access Control**: Mandaten- und Benutzer-basierte Filterung
+- **Type Safety**: Pydantic-Modelle für Validierung
+- **Async Support**: Asynchrone Verarbeitung für Skalierbarkeit
+
+Die Implementierung ist modular und erweiterbar. Sie können weitere Funktionen hinzufügen, ohne die bestehende Struktur zu ändern.
+
+---
+
+[← Zurück: Troubleshooting](12-troubleshooting.md) | [← Zurück zur Übersicht](README.md)
+
+
+
diff --git a/docs/real-estate-feature-integration-guide/README.md b/docs/real-estate-feature-integration-guide/README.md
new file mode 100644
index 00000000..95b0346d
--- /dev/null
+++ b/docs/real-estate-feature-integration-guide/README.md
@@ -0,0 +1,42 @@
+# Feature Integration Guide: realEstate
+
+Diese Dokumentation erklärt Schritt für Schritt, wie Sie ein neues Feature "realEstate" in das Gateway-Projekt integrieren. Das Feature ermöglicht es, über ein Chat-Interface Datenbankabfragen auf Real Estate-Daten (Architektur-Planungs-App) durchzuführen.
+
+**Referenz:** Das zugrundeliegende Datenmodell ist in `../PEK_datamodel_desc.md` beschrieben (PEK ist ein Beispiel für eine Real Estate-Firma, das Modell ist aber allgemein verwendbar).
+
+## Inhaltsverzeichnis
+
+1. [Überblick und Projektstruktur](01-overview.md)
+2. [Schritt 1: Datenmodell erstellen](02-datamodels.md)
+3. [Schritt 2: Interface erstellen](03-interfaces.md)
+4. [Schritt 3: Feature-Logik implementieren](04-feature-logic.md)
+5. [Schritt 4: Routen erstellen](05-routes.md)
+6. [Schritt 5: Router registrieren](06-router-registration.md)
+7. [Schritt 6: Environment-Konfiguration](07-environment.md)
+8. [Schritt 7: Feature Lifecycle (optional)](08-lifecycle.md)
+9. [Datenbank-Schema und Tabellenerstellung](09-database-schema.md)
+10. [Sicherheitshinweise](10-security.md)
+11. [Testing](11-testing.md)
+12. [Troubleshooting](12-troubleshooting.md)
+13. [Zusammenfassung](13-summary.md)
+
+---
+
+## Schnellstart
+
+Für eine schnelle Übersicht über alle zu erstellenden Dateien, siehe [Zusammenfassung](13-summary.md).
+
+## Architektur-Überblick
+
+Die Architektur folgt dem Muster bestehender Features wie `chatPlayground`:
+- **Routes** (`modules/routes/`) - API-Endpunkte
+- **Features** (`modules/features/`) - Geschäftslogik
+- **Interfaces** (`modules/interfaces/`) - Datenbankzugriff
+- **DataModels** (`modules/datamodels/`) - Pydantic-Modelle
+
+---
+
+**Nächster Schritt:** [01-overview.md](01-overview.md)
+
+
+
diff --git a/env_dev.env b/env_dev.env
index 95b2b91e..c20be4e2 100644
--- a/env_dev.env
+++ b/env_dev.env
@@ -29,6 +29,13 @@ DB_MANAGEMENT_USER=poweron_dev
DB_MANAGEMENT_PASSWORD_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEUldqSTVpUnFqdGhITDYzT3RScGlMYVdTMmZhOXdudDRCc3dhdllOd3l6MS1vWHY2MjVsTUF1Sk9saEJOSk9ONUlBZjQwb2c2T1gtWWJhcXFzVVVXd01xc0U0b0lJX0JyVDRxaDhNS01JcWs9
DB_MANAGEMENT_PORT=5432
+# PostgreSQL Storage (new)
+DB_REALESTATE_HOST=localhost
+DB_REALESTATE_DATABASE=poweron_realestate
+DB_REALESTATE_USER=poweron_dev
+DB_REALESTATE_PASSWORD_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEcUIxNEFfQ2xnS0RrSC1KNnUxTlVvTGZoMHgzaEI4Z3NlVzVROTVLak5Ubi1vaEZubFZaMTFKMGd6MXAxekN2d2NvMy1hRjg2UVhybktlcFA5anZ1WjFlQmZhcXdwaGhWdzRDc3ExeUhzWTg9
+DB_REALESTATE_PORT=5432
+
# Security Configuration
APP_JWT_KEY_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpERjlrSktmZHVuQnJ1VVJDdndLaUcxZGJsT2ZlUFRlcFdOZ001RnlzM2FhLWhRV2tjWWFhaWQwQ3hkcUFvbThMcndxSjFpYTdfRV9OZGhTcksxbXFTZWg5MDZvOHpCVXBHcDJYaHlJM0tyNWRZckZsVHpQcmxTZHJoZUs1M3lfU2ljRnJaTmNSQ0w0X085OXI0QW80M2xfQnJqZmZ6VEh3TUltX0xzeE42SGtZPQ==
APP_TOKEN_EXPIRY=300
diff --git a/logs/debug/prompts/20251119-100038-001-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-100038-001-intentanalysis_prompt.txt
new file mode 100644
index 00000000..7f329236
--- /dev/null
+++ b/logs/debug/prompts/20251119-100038-001-intentanalysis_prompt.txt
@@ -0,0 +1,48 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities:
+- Projekt: Real estate project
+- Parzelle: Plot/parcel
+- Dokument: Document
+- Kanton: Canton
+- Gemeinde: Municipality
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields (label, statusProzess, etc.)
+ // For READ: include filter criteria (id, label, plz, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE plz = '8000'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE plz = '8000'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-100041-002-intentanalysis_response.txt b/logs/debug/prompts/20251119-100041-002-intentanalysis_response.txt
new file mode 100644
index 00000000..202b02aa
--- /dev/null
+++ b/logs/debug/prompts/20251119-100041-002-intentanalysis_response.txt
@@ -0,0 +1,10 @@
+```json
+{
+ "intent": "CREATE",
+ "entity": "Projekt",
+ "parameters": {
+ "label": "Hauptstrasse 42"
+ },
+ "confidence": 0.95
+}
+```
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-103736-003-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-103736-003-intentanalysis_prompt.txt
new file mode 100644
index 00000000..69cfe85c
--- /dev/null
+++ b/logs/debug/prompts/20251119-103736-003-intentanalysis_prompt.txt
@@ -0,0 +1,48 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Zeige mir alle Projekte in Zürich."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities:
+- Projekt: Real estate project
+- Parzelle: Plot/parcel
+- Dokument: Document
+- Kanton: Canton
+- Gemeinde: Municipality
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields (label, statusProzess, etc.)
+ // For READ: include filter criteria (id, label, plz, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE plz = '8000'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE plz = '8000'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-103742-004-intentanalysis_response.txt b/logs/debug/prompts/20251119-103742-004-intentanalysis_response.txt
new file mode 100644
index 00000000..12bd99bc
--- /dev/null
+++ b/logs/debug/prompts/20251119-103742-004-intentanalysis_response.txt
@@ -0,0 +1,16 @@
+```json
+{
+ "intent": "READ",
+ "entity": "Projekt",
+ "parameters": {
+ "location": "Zürich"
+ },
+ "confidence": 0.95
+}
+```
+
+**Analysis:**
+- **Intent**: READ - The command "Zeige mir" (Show me) clearly indicates a query/read operation
+- **Entity**: Projekt - The user explicitly asks for "Projekte" (projects)
+- **Parameters**: The location filter "in Zürich" is extracted as a search criterion. This could refer to a municipality (Gemeinde) named Zürich or projects located in the Zürich area
+- **Confidence**: 0.95 - High confidence as the intent and entity are clearly stated, with an explicit location filter
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-103802-005-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-103802-005-intentanalysis_prompt.txt
new file mode 100644
index 00000000..69cfe85c
--- /dev/null
+++ b/logs/debug/prompts/20251119-103802-005-intentanalysis_prompt.txt
@@ -0,0 +1,48 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Zeige mir alle Projekte in Zürich."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities:
+- Projekt: Real estate project
+- Parzelle: Plot/parcel
+- Dokument: Document
+- Kanton: Canton
+- Gemeinde: Municipality
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields (label, statusProzess, etc.)
+ // For READ: include filter criteria (id, label, plz, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE plz = '8000'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE plz = '8000'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-103808-006-intentanalysis_response.txt b/logs/debug/prompts/20251119-103808-006-intentanalysis_response.txt
new file mode 100644
index 00000000..62d89d2a
--- /dev/null
+++ b/logs/debug/prompts/20251119-103808-006-intentanalysis_response.txt
@@ -0,0 +1,16 @@
+```json
+{
+ "intent": "READ",
+ "entity": "Projekt",
+ "parameters": {
+ "location": "Zürich"
+ },
+ "confidence": 0.95
+}
+```
+
+**Analysis:**
+- **Intent**: READ - The command "Zeige mir" (Show me) clearly indicates a query/read operation
+- **Entity**: Projekt - The user explicitly asks for "Projekte" (projects)
+- **Parameters**: The location filter "in Zürich" is extracted as a search criterion. This could refer to a municipality (Gemeinde) named Zürich or projects located in the Zürich area
+- **Confidence**: 0.95 - High confidence as the intent and entity are clearly stated in the command
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-104317-007-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-104317-007-intentanalysis_prompt.txt
new file mode 100644
index 00000000..bb7b48bc
--- /dev/null
+++ b/logs/debug/prompts/20251119-104317-007-intentanalysis_prompt.txt
@@ -0,0 +1,88 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Zeige mir alle Projekte in Zürich."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key)
+- kontextKanton: string (canton ID, Foreign Key)
+- kontextLand: string (country ID, Foreign Key)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Location queries (city, postal code) should use Parzelle fields (plz, kontextGemeinde), NOT Projekt fields
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-104324-008-intentanalysis_response.txt b/logs/debug/prompts/20251119-104324-008-intentanalysis_response.txt
new file mode 100644
index 00000000..037f72d0
--- /dev/null
+++ b/logs/debug/prompts/20251119-104324-008-intentanalysis_response.txt
@@ -0,0 +1,18 @@
+```json
+{
+ "intent": "READ",
+ "entity": "Parzelle",
+ "parameters": {
+ "kontextGemeinde": "Zürich"
+ },
+ "confidence": 0.9
+}
+```
+
+**Reasoning:**
+- The user wants to see/display ("Zeige mir") projects, which indicates a READ intent
+- The location filter "in Zürich" refers to a city/municipality
+- According to the schema, **Projekt does NOT have location fields directly** - location information is stored in the associated **Parzelle** entity through the `kontextGemeinde` field
+- Therefore, to find projects in Zürich, we need to query **Parzelle** with the filter `kontextGemeinde: "Zürich"`
+- The system can then retrieve the associated projects through the relationship (Projekte contain Parzellen)
+- Confidence is 0.9 because the intent is clear, though the user said "Projekte" while we're technically querying "Parzelle" first to filter by location
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-104747-009-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-104747-009-intentanalysis_prompt.txt
new file mode 100644
index 00000000..bb7b48bc
--- /dev/null
+++ b/logs/debug/prompts/20251119-104747-009-intentanalysis_prompt.txt
@@ -0,0 +1,88 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Zeige mir alle Projekte in Zürich."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key)
+- kontextKanton: string (canton ID, Foreign Key)
+- kontextLand: string (country ID, Foreign Key)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Location queries (city, postal code) should use Parzelle fields (plz, kontextGemeinde), NOT Projekt fields
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-104754-010-intentanalysis_response.txt b/logs/debug/prompts/20251119-104754-010-intentanalysis_response.txt
new file mode 100644
index 00000000..9df10c17
--- /dev/null
+++ b/logs/debug/prompts/20251119-104754-010-intentanalysis_response.txt
@@ -0,0 +1,17 @@
+```json
+{
+ "intent": "READ",
+ "entity": "Parzelle",
+ "parameters": {
+ "kontextGemeinde": "Zürich"
+ },
+ "confidence": 0.9
+}
+```
+
+**Reasoning:**
+- The user wants to see/display ("Zeige mir") projects, which indicates a READ intent
+- The location filter "in Zürich" refers to a city/municipality
+- According to the schema, **Projekt does NOT have location fields directly** - location information is stored in the associated **Parzelle** entity through the `kontextGemeinde` field
+- Therefore, to find projects in Zürich, we need to query **Parzelle** entities with `kontextGemeinde: "Zürich"`, which will give us the plots (and their associated projects) in that municipality
+- The confidence is 0.9 because the intent and location are clear, though the user said "Projekte" but we need to query via "Parzelle" due to the data model structure
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-104856-011-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-104856-011-intentanalysis_prompt.txt
new file mode 100644
index 00000000..bbfb0711
--- /dev/null
+++ b/logs/debug/prompts/20251119-104856-011-intentanalysis_prompt.txt
@@ -0,0 +1,88 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Erstell eine neue Parzelle in Zürich."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key)
+- kontextKanton: string (canton ID, Foreign Key)
+- kontextLand: string (country ID, Foreign Key)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Location queries (city, postal code) should use Parzelle fields (plz, kontextGemeinde), NOT Projekt fields
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-104902-012-intentanalysis_response.txt b/logs/debug/prompts/20251119-104902-012-intentanalysis_response.txt
new file mode 100644
index 00000000..f18102ca
--- /dev/null
+++ b/logs/debug/prompts/20251119-104902-012-intentanalysis_response.txt
@@ -0,0 +1,17 @@
+```json
+{
+ "intent": "CREATE",
+ "entity": "Parzelle",
+ "parameters": {
+ "kontextGemeinde": "Zürich"
+ },
+ "confidence": 0.9
+}
+```
+
+**Analysis:**
+- **Intent**: CREATE - The user explicitly wants to create ("Erstell") a new entity
+- **Entity**: Parzelle - The user specifies "eine neue Parzelle" (a new plot/parcel)
+- **Parameters**:
+ - `kontextGemeinde`: "Zürich" - The location (municipality) where the plot should be created
+- **Confidence**: 0.9 - High confidence as the intent and entity are clearly stated, though only minimal information (location) is provided for the creation
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-105033-013-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-105033-013-intentanalysis_prompt.txt
new file mode 100644
index 00000000..f5c8b8af
--- /dev/null
+++ b/logs/debug/prompts/20251119-105033-013-intentanalysis_prompt.txt
@@ -0,0 +1,88 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Zeige mir alle Parzellen in Zürich."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key)
+- kontextKanton: string (canton ID, Foreign Key)
+- kontextLand: string (country ID, Foreign Key)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Location queries (city, postal code) should use Parzelle fields (plz, kontextGemeinde), NOT Projekt fields
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-105039-014-intentanalysis_response.txt b/logs/debug/prompts/20251119-105039-014-intentanalysis_response.txt
new file mode 100644
index 00000000..0e156b67
--- /dev/null
+++ b/logs/debug/prompts/20251119-105039-014-intentanalysis_response.txt
@@ -0,0 +1,17 @@
+```json
+{
+ "intent": "READ",
+ "entity": "Parzelle",
+ "parameters": {
+ "kontextGemeinde": "Zürich"
+ },
+ "confidence": 0.95
+}
+```
+
+**Reasoning:**
+- The user command "Zeige mir alle Parzellen in Zürich" translates to "Show me all plots in Zurich"
+- **Intent**: READ - The user wants to query/retrieve existing entities
+- **Entity**: Parzelle - The user explicitly asks for "Parzellen" (plots/parcels)
+- **Parameters**: The location filter "in Zürich" maps to the `kontextGemeinde` field of the Parzelle entity, which stores the municipality information
+- **Confidence**: 0.95 - High confidence because the intent is clear (show/display), the entity is explicitly mentioned (Parzellen), and the location parameter (Zürich) clearly maps to the kontextGemeinde field
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-110433-015-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-110433-015-intentanalysis_prompt.txt
new file mode 100644
index 00000000..6c53a768
--- /dev/null
+++ b/logs/debug/prompts/20251119-110433-015-intentanalysis_prompt.txt
@@ -0,0 +1,89 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Zeige mir alle Parzellen in Zürich."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-110440-016-intentanalysis_response.txt b/logs/debug/prompts/20251119-110440-016-intentanalysis_response.txt
new file mode 100644
index 00000000..331d501b
--- /dev/null
+++ b/logs/debug/prompts/20251119-110440-016-intentanalysis_response.txt
@@ -0,0 +1,17 @@
+```json
+{
+ "intent": "READ",
+ "entity": "Parzelle",
+ "parameters": {
+ "kontextGemeinde": "Zürich"
+ },
+ "confidence": 0.95
+}
+```
+
+**Explanation:**
+- **Intent**: READ - The user wants to query/view existing data ("Zeige mir" = "Show me")
+- **Entity**: Parzelle - The user explicitly asks for "Parzellen" (plots/parcels)
+- **Parameters**:
+ - `kontextGemeinde`: "Zürich" - The location filter is specified as Zürich, which is a municipality (Gemeinde). According to the schema, location information for Parzellen is stored in the `kontextGemeinde` field
+- **Confidence**: 0.95 - High confidence as the command is clear and unambiguous, directly requesting plots in a specific location
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-125234-017-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-125234-017-intentanalysis_prompt.txt
new file mode 100644
index 00000000..41ca3352
--- /dev/null
+++ b/logs/debug/prompts/20251119-125234-017-intentanalysis_prompt.txt
@@ -0,0 +1,89 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Füge die Gemeinde Zürich in das System hinzu."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-125239-018-intentanalysis_response.txt b/logs/debug/prompts/20251119-125239-018-intentanalysis_response.txt
new file mode 100644
index 00000000..e278914c
--- /dev/null
+++ b/logs/debug/prompts/20251119-125239-018-intentanalysis_response.txt
@@ -0,0 +1,16 @@
+```json
+{
+ "intent": "CREATE",
+ "entity": "Gemeinde",
+ "parameters": {
+ "label": "Zürich"
+ },
+ "confidence": 0.95
+}
+```
+
+**Analysis:**
+- **Intent**: CREATE - The verb "Füge...hinzu" (add) clearly indicates the user wants to create a new entity
+- **Entity**: Gemeinde - The user explicitly mentions "die Gemeinde Zürich" (the municipality Zürich)
+- **Parameters**: The municipality name "Zürich" is extracted as the label/identifier
+- **Confidence**: 0.95 - High confidence due to clear intent and explicit entity mention
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-125657-019-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-125657-019-intentanalysis_prompt.txt
new file mode 100644
index 00000000..41ca3352
--- /dev/null
+++ b/logs/debug/prompts/20251119-125657-019-intentanalysis_prompt.txt
@@ -0,0 +1,89 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Füge die Gemeinde Zürich in das System hinzu."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-125702-020-intentanalysis_response.txt b/logs/debug/prompts/20251119-125702-020-intentanalysis_response.txt
new file mode 100644
index 00000000..069e404e
--- /dev/null
+++ b/logs/debug/prompts/20251119-125702-020-intentanalysis_response.txt
@@ -0,0 +1,16 @@
+```json
+{
+ "intent": "CREATE",
+ "entity": "Gemeinde",
+ "parameters": {
+ "label": "Zürich"
+ },
+ "confidence": 0.95
+}
+```
+
+**Analysis:**
+- **Intent**: CREATE - The user explicitly wants to add ("Füge...hinzu") a new entity to the system
+- **Entity**: Gemeinde - The user specifically mentions "die Gemeinde Zürich" (the municipality Zürich)
+- **Parameters**: The municipality name "Zürich" is extracted as the label/identifier
+- **Confidence**: 0.95 - High confidence as the command is clear and unambiguous with explicit CREATE intent and entity type
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-125750-021-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-125750-021-intentanalysis_prompt.txt
new file mode 100644
index 00000000..853df98b
--- /dev/null
+++ b/logs/debug/prompts/20251119-125750-021-intentanalysis_prompt.txt
@@ -0,0 +1,89 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Füge ein neues Projekt mit Parzelle in Zürich hinzu."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-125756-022-intentanalysis_response.txt b/logs/debug/prompts/20251119-125756-022-intentanalysis_response.txt
new file mode 100644
index 00000000..85c1db10
--- /dev/null
+++ b/logs/debug/prompts/20251119-125756-022-intentanalysis_response.txt
@@ -0,0 +1,20 @@
+```json
+{
+ "intent": "CREATE",
+ "entity": "Projekt",
+ "parameters": {
+ "parzellen": [
+ {
+ "kontextGemeinde": "Zürich"
+ }
+ ]
+ },
+ "confidence": 0.85
+}
+```
+
+**Reasoning:**
+- The user wants to create ("Füge hinzu") a new project ("neues Projekt")
+- The project should include a plot ("mit Parzelle") in Zürich
+- Since Projekt contains Parzellen as a nested list, and the location information (Zürich) belongs to Parzelle (via kontextGemeinde field), I've structured the parameters to reflect this relationship
+- The confidence is 0.85 because while the intent is clear, the user hasn't provided other typical project details like a label/name for the project itself
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-141840-023-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-141840-023-intentanalysis_prompt.txt
new file mode 100644
index 00000000..69983369
--- /dev/null
+++ b/logs/debug/prompts/20251119-141840-023-intentanalysis_prompt.txt
@@ -0,0 +1,89 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Füge die Gemeinde Winterthur hinzu"
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-141848-024-intentanalysis_response.txt b/logs/debug/prompts/20251119-141848-024-intentanalysis_response.txt
new file mode 100644
index 00000000..5b3aa3d5
--- /dev/null
+++ b/logs/debug/prompts/20251119-141848-024-intentanalysis_response.txt
@@ -0,0 +1,16 @@
+```json
+{
+ "intent": "CREATE",
+ "entity": "Gemeinde",
+ "parameters": {
+ "label": "Winterthur"
+ },
+ "confidence": 0.95
+}
+```
+
+**Analysis:**
+- **Intent**: CREATE - The verb "Füge...hinzu" (add) clearly indicates the user wants to create a new entity
+- **Entity**: Gemeinde - The user explicitly mentions "die Gemeinde Winterthur" (the municipality Winterthur)
+- **Parameters**: The name/label of the municipality is "Winterthur"
+- **Confidence**: 0.95 - High confidence as the command is clear and unambiguous
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-144901-025-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-144901-025-intentanalysis_prompt.txt
new file mode 100644
index 00000000..b13d0efe
--- /dev/null
+++ b/logs/debug/prompts/20251119-144901-025-intentanalysis_prompt.txt
@@ -0,0 +1,89 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Wie viele Projekte in Zürich gibt es?"
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-144909-026-intentanalysis_response.txt b/logs/debug/prompts/20251119-144909-026-intentanalysis_response.txt
new file mode 100644
index 00000000..59197959
--- /dev/null
+++ b/logs/debug/prompts/20251119-144909-026-intentanalysis_response.txt
@@ -0,0 +1,17 @@
+```json
+{
+ "intent": "READ",
+ "entity": "Parzelle",
+ "parameters": {
+ "kontextGemeinde": "Zürich"
+ },
+ "confidence": 0.9
+}
+```
+
+**Reasoning:**
+- The user asks "How many projects in Zürich are there?" which is a READ/query operation
+- Since location information (city) is stored in Parzelle via `kontextGemeinde`, not directly in Projekt, we need to query Parzelle entities
+- The parameter `kontextGemeinde` is set to "Zürich" to filter plots by municipality
+- To get projects in Zürich, the system will need to find all Parzellen with kontextGemeinde="Zürich" and then identify the associated projects
+- Confidence is 0.9 because the intent is clear, though the user asks about "projects" while we're querying "Parzelle" (which is the correct approach given the data model)
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-145121-027-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-145121-027-intentanalysis_prompt.txt
new file mode 100644
index 00000000..6f20254d
--- /dev/null
+++ b/logs/debug/prompts/20251119-145121-027-intentanalysis_prompt.txt
@@ -0,0 +1,89 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "welche projekte in Zürich gibt es?"
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-145129-028-intentanalysis_response.txt b/logs/debug/prompts/20251119-145129-028-intentanalysis_response.txt
new file mode 100644
index 00000000..b6c6eaae
--- /dev/null
+++ b/logs/debug/prompts/20251119-145129-028-intentanalysis_response.txt
@@ -0,0 +1,17 @@
+```json
+{
+ "intent": "READ",
+ "entity": "Parzelle",
+ "parameters": {
+ "kontextGemeinde": "Zürich"
+ },
+ "confidence": 0.9
+}
+```
+
+**Reasoning:**
+- The user is asking "which projects in Zürich exist?" which is a READ/query operation
+- Since location information (city/municipality) is stored in Parzelle (via `kontextGemeinde`), not directly in Projekt, we need to query Parzelle entities
+- "Zürich" is the municipality name that maps to the `kontextGemeinde` field
+- The system will need to find all Parzellen in Zürich and then retrieve their associated Projekte
+- High confidence (0.9) as the intent and location are clearly stated
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-145151-029-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-145151-029-intentanalysis_prompt.txt
new file mode 100644
index 00000000..c2b228d9
--- /dev/null
+++ b/logs/debug/prompts/20251119-145151-029-intentanalysis_prompt.txt
@@ -0,0 +1,89 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Welche Projekte in Zürich gibt es?"
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-145202-030-intentanalysis_response.txt b/logs/debug/prompts/20251119-145202-030-intentanalysis_response.txt
new file mode 100644
index 00000000..4cc5d8b0
--- /dev/null
+++ b/logs/debug/prompts/20251119-145202-030-intentanalysis_response.txt
@@ -0,0 +1,18 @@
+```json
+{
+ "intent": "READ",
+ "entity": "Parzelle",
+ "parameters": {
+ "kontextGemeinde": "Zürich"
+ },
+ "confidence": 0.9
+}
+```
+
+**Reasoning:**
+- The user is asking "Which projects in Zürich exist?" (Welche Projekte in Zürich gibt es?)
+- This is a READ operation (querying for existing data)
+- Since location information (city/municipality) is stored in **Parzelle** via the `kontextGemeinde` field, not directly in Projekt, we need to query Parzelle entities
+- The parameter `kontextGemeinde` is set to "Zürich" to filter plots by municipality
+- To get projects in Zürich, the system will need to find Parzellen with kontextGemeinde="Zürich" and then retrieve their associated Projekte
+- Confidence is 0.9 because the intent is clear, though the user asked about "Projekte" while we're technically querying via "Parzelle" due to the data model structure
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-145222-031-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-145222-031-intentanalysis_prompt.txt
new file mode 100644
index 00000000..d67478d1
--- /dev/null
+++ b/logs/debug/prompts/20251119-145222-031-intentanalysis_prompt.txt
@@ -0,0 +1,89 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Welche Gemeinden haben wir bereits hinzugefügt?"
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-145229-032-intentanalysis_response.txt b/logs/debug/prompts/20251119-145229-032-intentanalysis_response.txt
new file mode 100644
index 00000000..4e0657b7
--- /dev/null
+++ b/logs/debug/prompts/20251119-145229-032-intentanalysis_response.txt
@@ -0,0 +1,14 @@
+```json
+{
+ "intent": "READ",
+ "entity": "Gemeinde",
+ "parameters": {},
+ "confidence": 0.95
+}
+```
+
+**Reasoning:**
+- The user is asking "Which municipalities have we already added?" (Welche Gemeinden haben wir bereits hinzugefügt?)
+- This is clearly a READ operation to query existing Gemeinde entities
+- No specific filters are mentioned, so the user wants to see all Gemeinde records
+- The confidence is high (0.95) because the intent and entity are explicitly stated in the question
\ No newline at end of file
diff --git a/logs/debug/prompts/20251119-145449-033-intentanalysis_prompt.txt b/logs/debug/prompts/20251119-145449-033-intentanalysis_prompt.txt
new file mode 100644
index 00000000..26aa68b0
--- /dev/null
+++ b/logs/debug/prompts/20251119-145449-033-intentanalysis_prompt.txt
@@ -0,0 +1,89 @@
+
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "Füge eine neue Parzelle hinzu die in Zürich liegt und die Adresse Ueberlandstrasse 11 8050 hat."
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ },
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {"intent": "CREATE", "entity": "Projekt", "parameters": {"label": "Hauptstrasse 42"}, "confidence": 0.95}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {"intent": "READ", "entity": "Projekt", "parameters": {}, "confidence": 0.9}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"kontextGemeinde": "Zürich"}, "confidence": 0.9}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {"intent": "READ", "entity": "Parzelle", "parameters": {"plz": "8000"}, "confidence": 0.95}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {"intent": "UPDATE", "entity": "Projekt", "parameters": {"id": "XYZ", "statusProzess": "Planung"}, "confidence": 0.85}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {"intent": "QUERY", "entity": null, "parameters": {"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}, "confidence": 1.0}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {"intent": "DELETE", "entity": "Parzelle", "parameters": {"id": "ABC"}, "confidence": 0.9}
diff --git a/logs/debug/prompts/20251119-145452-034-intentanalysis_response.txt b/logs/debug/prompts/20251119-145452-034-intentanalysis_response.txt
new file mode 100644
index 00000000..379e6909
--- /dev/null
+++ b/logs/debug/prompts/20251119-145452-034-intentanalysis_response.txt
@@ -0,0 +1,12 @@
+```json
+{
+ "intent": "CREATE",
+ "entity": "Parzelle",
+ "parameters": {
+ "kontextGemeinde": "Zürich",
+ "strasseNr": "Ueberlandstrasse 11",
+ "plz": "8050"
+ },
+ "confidence": 0.95
+}
+```
\ No newline at end of file
diff --git a/modules/datamodels/datamodelRealEstate.py b/modules/datamodels/datamodelRealEstate.py
new file mode 100644
index 00000000..fa9e717e
--- /dev/null
+++ b/modules/datamodels/datamodelRealEstate.py
@@ -0,0 +1,667 @@
+"""
+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,
+ )
+
+
+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,
+ )
+
+
+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,
+ )
+
+ 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"},
+ "mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
+ },
+)
+
+registerModelLabels(
+ "Parzelle",
+ {"en": "Plot", "fr": "Parcelle", "de": "Parzelle"},
+ {
+ "id": {"en": "ID", "fr": "ID", "de": "ID"},
+ "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
+ "mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
+ },
+)
+
+registerModelLabels(
+ "Dokument",
+ {"en": "Document", "fr": "Document", "de": "Dokument"},
+ {
+ "id": {"en": "ID", "fr": "ID", "de": "ID"},
+ "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
+ },
+)
+
diff --git a/modules/features/realEstate/__init__.py b/modules/features/realEstate/__init__.py
new file mode 100644
index 00000000..48368b52
--- /dev/null
+++ b/modules/features/realEstate/__init__.py
@@ -0,0 +1,4 @@
+"""
+Real Estate feature module.
+"""
+
diff --git a/modules/features/realEstate/mainRealEstate.py b/modules/features/realEstate/mainRealEstate.py
new file mode 100644
index 00000000..5396a20a
--- /dev/null
+++ b/modules/features/realEstate/mainRealEstate.py
@@ -0,0 +1,769 @@
+"""
+Real Estate feature main logic.
+Handles database operations with AI-powered natural language processing.
+Stateless implementation without session management.
+"""
+
+import logging
+import json
+from typing import Optional, Dict, Any
+from modules.datamodels.datamodelUam import User
+from modules.datamodels.datamodelRealEstate import (
+ Projekt,
+ Parzelle,
+ StatusProzess,
+)
+from modules.services import getInterface as getServices
+from modules.interfaces.interfaceDbRealEstateObjects import getInterface as getRealEstateInterface
+
+logger = logging.getLogger(__name__)
+
+
+# ===== Direkte Query-Ausführung (stateless) =====
+
+async def executeDirectQuery(
+ currentUser: User,
+ queryText: str,
+ parameters: Optional[Dict[str, Any]] = None,
+) -> Dict[str, Any]:
+ """
+ Execute a database query directly without session management.
+
+ Args:
+ currentUser: Current authenticated user
+ queryText: SQL query text
+ parameters: Optional parameters for parameterized queries
+
+ Returns:
+ Dictionary containing query result (rows, columns, rowCount)
+
+ Note:
+ - No session or query history is saved
+ - Query is executed directly and result is returned
+ - For production, validate and sanitize queries before execution
+ - TODO: Implement actual database query execution via interface
+ """
+ try:
+ logger.info(f"Executing direct query for user {currentUser.id} (mandate: {currentUser.mandateId})")
+ logger.debug(f"Query text: {queryText}")
+ if parameters:
+ logger.debug(f"Query parameters: {parameters}")
+
+ # Execute query via Real Estate interface (stateless)
+ realEstateInterface = getRealEstateInterface(currentUser)
+ result = realEstateInterface.executeQuery(queryText, parameters)
+
+ logger.info(
+ f"Query executed successfully: {result['rowCount']} rows in {result.get('executionTime', 0):.3f}s"
+ )
+
+ return {
+ "status": "success",
+ "rows": result["rows"],
+ "columns": result["columns"],
+ "rowCount": result["rowCount"],
+ "executionTime": result.get("executionTime", 0),
+ }
+
+ except Exception as e:
+ logger.error(f"Error executing query: {str(e)}", exc_info=True)
+ raise
+
+
+# ===== AI-basierte Intent-Erkennung und CRUD-Operationen =====
+
+async def processNaturalLanguageCommand(
+ currentUser: User,
+ userInput: str,
+) -> Dict[str, Any]:
+ """
+ Process natural language user input and execute corresponding CRUD operations.
+
+ Uses AI to analyze user intent and extract parameters, then executes the appropriate
+ CRUD operation through the interface. Works stateless without session management.
+
+ Args:
+ currentUser: Current authenticated user
+ userInput: Natural language command from user
+
+ Returns:
+ Dictionary containing operation result and metadata
+
+ Example user inputs:
+ - "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ - "Zeige mir alle Projekte in Zürich"
+ - "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ - "Lösche Parzelle ABC"
+ - "SELECT * FROM Projekt WHERE plz = '8000'"
+ """
+ try:
+ logger.info(f"Processing natural language command for user {currentUser.id} (mandate: {currentUser.mandateId})")
+ logger.debug(f"User input: {userInput}")
+
+ # Initialize services for AI access
+ services = getServices(currentUser, workflow=None)
+ aiService = services.ai
+
+ # Step 1: Analyze user intent with AI
+ intentAnalysis = await analyzeUserIntent(aiService, userInput)
+
+ logger.info(f"Intent analysis result: intent={intentAnalysis.get('intent')}, entity={intentAnalysis.get('entity')}")
+
+ # Step 2: Execute CRUD operation based on intent
+ result = await executeIntentBasedOperation(
+ currentUser=currentUser,
+ intent=intentAnalysis["intent"],
+ entity=intentAnalysis.get("entity"),
+ parameters=intentAnalysis.get("parameters", {}),
+ )
+
+ return {
+ "success": True,
+ "intent": intentAnalysis["intent"],
+ "entity": intentAnalysis.get("entity"),
+ "result": result,
+ }
+
+ except Exception as e:
+ logger.error(f"Error processing natural language command: {str(e)}", exc_info=True)
+ raise
+
+
+async def analyzeUserIntent(
+ aiService,
+ userInput: str
+) -> Dict[str, Any]:
+ """
+ Use AI to analyze user input and extract intent, entity, and parameters.
+
+ Args:
+ aiService: AI service instance
+ userInput: Natural language user input
+
+ Returns:
+ Dictionary with 'intent', 'entity', and 'parameters'
+ """
+ # Create a structured prompt for intent analysis with accurate field information
+ intentPrompt = f"""
+Analyze the following user command and extract the intent, entity, and parameters.
+
+User Command: "{userInput}"
+
+Available intents:
+- CREATE: User wants to create a new entity
+- READ: User wants to read/query entities
+- UPDATE: User wants to update an existing entity
+- DELETE: User wants to delete an entity
+- QUERY: User wants to execute a database query (SQL statements)
+
+Available entities and their fields:
+
+**Projekt** (Real estate project):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (project designation/name)
+- statusProzess: string enum (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv)
+- perimeter: GeoPolylinie (geographic boundary, JSONB)
+- baulinie: GeoPolylinie (building line, JSONB)
+- parzellen: List[Parzelle] (plots belonging to project, JSONB)
+- dokumente: List[Dokument] (documents, JSONB)
+- kontextInformationen: List[Kontext] (context info, JSONB)
+
+**Parzelle** (Plot/parcel):
+- id: string (primary key)
+- mandateId: string (mandate ID)
+- label: string (plot designation)
+- strasseNr: string (street and house number)
+- plz: string (postal code)
+- kontextGemeinde: string (municipality ID, Foreign Key to Gemeinde table)
+- bauzone: string (building zone, e.g. W3, WG2)
+- az: float (Ausnützungsziffer)
+- bz: float (Bebauungsziffer)
+- vollgeschossZahl: int (number of allowed full floors)
+- gebaeudehoeheMax: float (maximum building height in meters)
+- laermschutzzone: string (noise protection zone)
+- hochwasserschutzzone: string (flood protection zone)
+- grundwasserschutzzone: string (groundwater protection zone)
+- parzelleBebaut: JaNein enum (is plot built)
+- parzelleErschlossen: JaNein enum (is plot developed)
+- parzelleHanglage: JaNein enum (is plot on slope)
+
+**Important relationships:**
+- Projekte contain Parzellen (projects have plots)
+- Parzelle links to Gemeinde (via kontextGemeinde)
+- Gemeinde links to Kanton (via id_kanton)
+- Kanton links to Land (via id_land)
+- Location queries (city, postal code) should use Parzelle.kontextGemeinde (municipality name will be resolved to ID)
+- Projekt does NOT have location fields directly - location is stored in associated Parzellen
+
+Return a JSON object with the following structure:
+{{
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|Dokument|Kanton|Gemeinde|null",
+ "parameters": {{
+ // Extracted parameters from user input
+ // For CREATE/UPDATE: include all relevant fields using EXACT field names from above
+ // For READ: include filter criteria using EXACT field names (id, label, plz, kontextGemeinde, etc.)
+ // For DELETE: include entity ID if mentioned
+ // For QUERY: include queryText if SQL is detected
+ // IMPORTANT: Use only field names that exist in the entity definition above
+ }},
+ "confidence": 0.0-1.0 // Confidence score for the analysis
+}}
+
+Examples:
+- Input: "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ Output: {{"intent": "CREATE", "entity": "Projekt", "parameters": {{"label": "Hauptstrasse 42"}}, "confidence": 0.95}}
+
+- Input: "Zeige mir alle Projekte"
+ Output: {{"intent": "READ", "entity": "Projekt", "parameters": {{}}, "confidence": 0.9}}
+
+- Input: "Zeige mir Projekte in Zürich"
+ Output: {{"intent": "READ", "entity": "Parzelle", "parameters": {{"kontextGemeinde": "Zürich"}}, "confidence": 0.9}}
+ Note: Location queries should query Parzelle, not Projekt directly
+
+- Input: "Zeige mir Parzellen mit PLZ 8000"
+ Output: {{"intent": "READ", "entity": "Parzelle", "parameters": {{"plz": "8000"}}, "confidence": 0.95}}
+
+- Input: "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ Output: {{"intent": "UPDATE", "entity": "Projekt", "parameters": {{"id": "XYZ", "statusProzess": "Planung"}}, "confidence": 0.85}}
+
+- Input: "SELECT * FROM Projekt WHERE label = 'Test'"
+ Output: {{"intent": "QUERY", "entity": null, "parameters": {{"queryText": "SELECT * FROM Projekt WHERE label = 'Test'", "queryType": "sql"}}, "confidence": 1.0}}
+
+- Input: "Lösche Parzelle ABC"
+ Output: {{"intent": "DELETE", "entity": "Parzelle", "parameters": {{"id": "ABC"}}, "confidence": 0.9}}
+"""
+
+ try:
+ # Use AI planning call for structured JSON response
+ response = await aiService.callAiPlanning(
+ prompt=intentPrompt,
+ debugType="intentanalysis"
+ )
+
+ # Extract JSON from response (handles markdown code blocks)
+ jsonStart = response.find('{')
+ jsonEnd = response.rfind('}') + 1
+
+ if jsonStart == -1 or jsonEnd == 0:
+ raise ValueError("No JSON found in AI response")
+
+ jsonStr = response[jsonStart:jsonEnd]
+
+ # Parse JSON response
+ intentData = json.loads(jsonStr)
+
+ # Validate response structure
+ if "intent" not in intentData:
+ raise ValueError("Invalid intent analysis response: missing 'intent' field")
+
+ # Ensure parameters exists
+ if "parameters" not in intentData:
+ intentData["parameters"] = {}
+
+ logger.debug(f"Parsed intent analysis: {intentData}")
+
+ return intentData
+
+ except json.JSONDecodeError as e:
+ logger.error(f"Failed to parse AI intent analysis response: {e}")
+ logger.error(f"Raw response: {response}")
+ raise ValueError(f"AI returned invalid JSON: {str(e)}")
+ except Exception as e:
+ logger.error(f"Error analyzing user intent: {str(e)}", exc_info=True)
+ raise
+
+
+async def executeIntentBasedOperation(
+ currentUser: User,
+ intent: str,
+ entity: Optional[str],
+ parameters: Dict[str, Any],
+) -> Dict[str, Any]:
+ """
+ Execute CRUD operation based on analyzed intent.
+
+ Args:
+ currentUser: Current authenticated user
+ intent: Intent from AI analysis (CREATE, READ, UPDATE, DELETE, QUERY)
+ entity: Entity type from AI analysis
+ parameters: Extracted parameters from AI analysis
+
+ Returns:
+ Operation result
+
+ Note:
+ - TODO: Implement actual interface calls once datamodels are ready
+ - Currently returns test responses showing what would be executed
+ """
+ try:
+ logger.info(f"Executing intent-based operation: intent={intent}, entity={entity}")
+ logger.debug(f"Parameters: {parameters}")
+
+ if intent == "QUERY":
+ # Execute database query directly (stateless)
+ queryText = parameters.get("queryText", "")
+
+ if not queryText:
+ raise ValueError("QUERY intent requires queryText in parameters")
+
+ result = await executeDirectQuery(
+ currentUser=currentUser,
+ queryText=queryText,
+ parameters=parameters.get("queryParameters"),
+ )
+ return result
+
+ elif intent == "CREATE":
+ # Create new entity
+ realEstateInterface = getRealEstateInterface(currentUser)
+
+ if entity == "Projekt":
+ # Create Projekt from parameters
+ projekt = Projekt(
+ mandateId=currentUser.mandateId,
+ label=parameters.get("label", ""),
+ statusProzess=StatusProzess(parameters.get("statusProzess", "EINGANG")) if parameters.get("statusProzess") else None,
+ )
+ created = realEstateInterface.createProjekt(projekt)
+ return {
+ "operation": "CREATE",
+ "entity": "Projekt",
+ "result": created.model_dump()
+ }
+
+ elif entity == "Parzelle":
+ # Create Parzelle from parameters
+ parzelle = Parzelle(
+ mandateId=currentUser.mandateId,
+ label=parameters.get("label", ""),
+ strasseNr=parameters.get("strasseNr"),
+ plz=parameters.get("plz"),
+ bauzone=parameters.get("bauzone"),
+ kontextGemeinde=parameters.get("kontextGemeinde"),
+ )
+ created = realEstateInterface.createParzelle(parzelle)
+ return {
+ "operation": "CREATE",
+ "entity": "Parzelle",
+ "result": created.model_dump()
+ }
+ elif entity == "Gemeinde":
+ # Create Gemeinde from parameters
+ from modules.datamodels.datamodelRealEstate import Gemeinde
+ gemeinde = Gemeinde(
+ mandateId=currentUser.mandateId,
+ label=parameters.get("label", ""),
+ id_kanton=parameters.get("id_kanton"),
+ plz=parameters.get("plz"),
+ )
+ created = realEstateInterface.createGemeinde(gemeinde)
+ return {
+ "operation": "CREATE",
+ "entity": "Gemeinde",
+ "result": created.model_dump()
+ }
+ elif entity == "Kanton":
+ # Create Kanton from parameters
+ from modules.datamodels.datamodelRealEstate import Kanton
+ kanton = Kanton(
+ mandateId=currentUser.mandateId,
+ label=parameters.get("label", ""),
+ id_land=parameters.get("id_land"),
+ abk=parameters.get("abk"),
+ )
+ created = realEstateInterface.createKanton(kanton)
+ return {
+ "operation": "CREATE",
+ "entity": "Kanton",
+ "result": created.model_dump()
+ }
+ elif entity == "Land":
+ # Create Land from parameters
+ from modules.datamodels.datamodelRealEstate import Land
+ land = Land(
+ mandateId=currentUser.mandateId,
+ label=parameters.get("label", ""),
+ abk=parameters.get("abk"),
+ )
+ created = realEstateInterface.createLand(land)
+ return {
+ "operation": "CREATE",
+ "entity": "Land",
+ "result": created.model_dump()
+ }
+ elif entity == "Dokument":
+ # Create Dokument from parameters
+ from modules.datamodels.datamodelRealEstate import Dokument
+ dokument = Dokument(
+ mandateId=currentUser.mandateId,
+ label=parameters.get("label", ""),
+ dokumentReferenz=parameters.get("dokumentReferenz", ""),
+ versionsbezeichnung=parameters.get("versionsbezeichnung"),
+ dokumentTyp=parameters.get("dokumentTyp"),
+ quelle=parameters.get("quelle"),
+ mimeType=parameters.get("mimeType"),
+ )
+ created = realEstateInterface.createDokument(dokument)
+ return {
+ "operation": "CREATE",
+ "entity": "Dokument",
+ "result": created.model_dump()
+ }
+ else:
+ raise ValueError(f"CREATE operation not supported for entity: {entity}")
+
+ elif intent == "READ":
+ # Read entities
+ realEstateInterface = getRealEstateInterface(currentUser)
+
+ if entity == "Projekt":
+ projektId = parameters.get("id")
+ if projektId:
+ # Get single Projekt by ID
+ projekt = realEstateInterface.getProjekt(projektId)
+ if not projekt:
+ raise ValueError(f"Projekt {projektId} not found")
+ return {
+ "operation": "READ",
+ "entity": "Projekt",
+ "result": projekt.model_dump()
+ }
+ else:
+ # List all Projekte (with optional filters)
+ # Validate filter fields against Projekt model
+ validProjektFields = {"id", "mandateId", "label", "statusProzess"}
+ recordFilter = {
+ k: v for k, v in parameters.items()
+ if k != "id" and k in validProjektFields
+ }
+ # Warn about invalid fields
+ invalidFields = {k: v for k, v in parameters.items() if k not in validProjektFields and k != "id"}
+ if invalidFields:
+ logger.warning(f"Invalid filter fields for Projekt ignored: {list(invalidFields.keys())}")
+ logger.info("Note: Location queries should use Parzelle entity, not Projekt")
+
+ projekte = realEstateInterface.getProjekte(recordFilter=recordFilter if recordFilter else None)
+ return {
+ "operation": "READ",
+ "entity": "Projekt",
+ "result": [p.model_dump() for p in projekte]
+ }
+ elif entity == "Parzelle":
+ parzelleId = parameters.get("id")
+ if parzelleId:
+ # Get single Parzelle by ID
+ parzelle = realEstateInterface.getParzelle(parzelleId)
+ if not parzelle:
+ raise ValueError(f"Parzelle {parzelleId} not found")
+ return {
+ "operation": "READ",
+ "entity": "Parzelle",
+ "result": parzelle.model_dump()
+ }
+ else:
+ # List all Parzellen (with optional filters)
+ # Validate filter fields against Parzelle model
+ # Note: kontextKanton and kontextLand are NOT direct fields on Parzelle
+ # Parzelle links to Gemeinde, Gemeinde links to Kanton, Kanton links to Land
+ validParzelleFields = {
+ "id", "mandateId", "label", "strasseNr", "plz",
+ "kontextGemeinde", # Only direct link - Gemeinde → Kanton → Land
+ "bauzone", "az", "bz", "vollgeschossZahl", "gebaeudehoeheMax",
+ "laermschutzzone", "hochwasserschutzzone", "grundwasserschutzzone",
+ "parzelleBebaut", "parzelleErschlossen", "parzelleHanglage"
+ }
+ recordFilter = {
+ k: v for k, v in parameters.items()
+ if k != "id" and k in validParzelleFields
+ }
+ # Warn about invalid fields
+ invalidFields = {k: v for k, v in parameters.items() if k not in validParzelleFields and k != "id"}
+ if invalidFields:
+ logger.warning(f"Invalid filter fields for Parzelle ignored: {list(invalidFields.keys())}")
+
+ parzellen = realEstateInterface.getParzellen(recordFilter=recordFilter if recordFilter else None)
+ return {
+ "operation": "READ",
+ "entity": "Parzelle",
+ "result": [p.model_dump() for p in parzellen]
+ }
+ elif entity == "Gemeinde":
+ from modules.datamodels.datamodelRealEstate import Gemeinde
+ gemeindeId = parameters.get("id")
+ if gemeindeId:
+ gemeinde = realEstateInterface.getGemeinde(gemeindeId)
+ if not gemeinde:
+ raise ValueError(f"Gemeinde {gemeindeId} not found")
+ return {
+ "operation": "READ",
+ "entity": "Gemeinde",
+ "result": gemeinde.model_dump()
+ }
+ else:
+ recordFilter = {k: v for k, v in parameters.items() if k != "id"}
+ gemeinden = realEstateInterface.getGemeinden(recordFilter=recordFilter if recordFilter else None)
+ return {
+ "operation": "READ",
+ "entity": "Gemeinde",
+ "result": [g.model_dump() for g in gemeinden]
+ }
+ elif entity == "Kanton":
+ from modules.datamodels.datamodelRealEstate import Kanton
+ kantonId = parameters.get("id")
+ if kantonId:
+ kanton = realEstateInterface.getKanton(kantonId)
+ if not kanton:
+ raise ValueError(f"Kanton {kantonId} not found")
+ return {
+ "operation": "READ",
+ "entity": "Kanton",
+ "result": kanton.model_dump()
+ }
+ else:
+ recordFilter = {k: v for k, v in parameters.items() if k != "id"}
+ kantone = realEstateInterface.getKantone(recordFilter=recordFilter if recordFilter else None)
+ return {
+ "operation": "READ",
+ "entity": "Kanton",
+ "result": [k.model_dump() for k in kantone]
+ }
+ elif entity == "Land":
+ from modules.datamodels.datamodelRealEstate import Land
+ landId = parameters.get("id")
+ if landId:
+ land = realEstateInterface.getLand(landId)
+ if not land:
+ raise ValueError(f"Land {landId} not found")
+ return {
+ "operation": "READ",
+ "entity": "Land",
+ "result": land.model_dump()
+ }
+ else:
+ recordFilter = {k: v for k, v in parameters.items() if k != "id"}
+ laender = realEstateInterface.getLaender(recordFilter=recordFilter if recordFilter else None)
+ return {
+ "operation": "READ",
+ "entity": "Land",
+ "result": [l.model_dump() for l in laender]
+ }
+ elif entity == "Dokument":
+ from modules.datamodels.datamodelRealEstate import Dokument
+ dokumentId = parameters.get("id")
+ if dokumentId:
+ dokument = realEstateInterface.getDokument(dokumentId)
+ if not dokument:
+ raise ValueError(f"Dokument {dokumentId} not found")
+ return {
+ "operation": "READ",
+ "entity": "Dokument",
+ "result": dokument.model_dump()
+ }
+ else:
+ recordFilter = {k: v for k, v in parameters.items() if k != "id"}
+ dokumente = realEstateInterface.getDokumente(recordFilter=recordFilter if recordFilter else None)
+ return {
+ "operation": "READ",
+ "entity": "Dokument",
+ "result": [d.model_dump() for d in dokumente]
+ }
+ else:
+ raise ValueError(f"READ operation not supported for entity: {entity}")
+
+ elif intent == "UPDATE":
+ # Update existing entity
+ realEstateInterface = getRealEstateInterface(currentUser)
+
+ if entity == "Projekt":
+ projektId = parameters.get("id")
+ if not projektId:
+ raise ValueError("UPDATE operation requires entity ID")
+
+ # Get existing projekt
+ projekt = realEstateInterface.getProjekt(projektId)
+ if not projekt:
+ raise ValueError(f"Projekt {projektId} not found")
+
+ # Update fields
+ updateData = {k: v for k, v in parameters.items() if k != "id"}
+ updated = realEstateInterface.updateProjekt(projektId, updateData)
+ return {
+ "operation": "UPDATE",
+ "entity": "Projekt",
+ "result": updated.model_dump()
+ }
+ elif entity == "Parzelle":
+ parzelleId = parameters.get("id")
+ if not parzelleId:
+ raise ValueError("UPDATE operation requires entity ID")
+
+ # Get existing parzelle
+ parzelle = realEstateInterface.getParzelle(parzelleId)
+ if not parzelle:
+ raise ValueError(f"Parzelle {parzelleId} not found")
+
+ # Update fields
+ updateData = {k: v for k, v in parameters.items() if k != "id"}
+ updated = realEstateInterface.updateParzelle(parzelleId, updateData)
+ return {
+ "operation": "UPDATE",
+ "entity": "Parzelle",
+ "result": updated.model_dump()
+ }
+ elif entity == "Gemeinde":
+ from modules.datamodels.datamodelRealEstate import Gemeinde
+ gemeindeId = parameters.get("id")
+ if not gemeindeId:
+ raise ValueError("UPDATE operation requires entity ID")
+
+ gemeinde = realEstateInterface.getGemeinde(gemeindeId)
+ if not gemeinde:
+ raise ValueError(f"Gemeinde {gemeindeId} not found")
+
+ updateData = {k: v for k, v in parameters.items() if k != "id"}
+ updated = realEstateInterface.updateGemeinde(gemeindeId, updateData)
+ return {
+ "operation": "UPDATE",
+ "entity": "Gemeinde",
+ "result": updated.model_dump()
+ }
+ elif entity == "Kanton":
+ from modules.datamodels.datamodelRealEstate import Kanton
+ kantonId = parameters.get("id")
+ if not kantonId:
+ raise ValueError("UPDATE operation requires entity ID")
+
+ kanton = realEstateInterface.getKanton(kantonId)
+ if not kanton:
+ raise ValueError(f"Kanton {kantonId} not found")
+
+ updateData = {k: v for k, v in parameters.items() if k != "id"}
+ updated = realEstateInterface.updateKanton(kantonId, updateData)
+ return {
+ "operation": "UPDATE",
+ "entity": "Kanton",
+ "result": updated.model_dump()
+ }
+ elif entity == "Land":
+ from modules.datamodels.datamodelRealEstate import Land
+ landId = parameters.get("id")
+ if not landId:
+ raise ValueError("UPDATE operation requires entity ID")
+
+ land = realEstateInterface.getLand(landId)
+ if not land:
+ raise ValueError(f"Land {landId} not found")
+
+ updateData = {k: v for k, v in parameters.items() if k != "id"}
+ updated = realEstateInterface.updateLand(landId, updateData)
+ return {
+ "operation": "UPDATE",
+ "entity": "Land",
+ "result": updated.model_dump()
+ }
+ elif entity == "Dokument":
+ from modules.datamodels.datamodelRealEstate import Dokument
+ dokumentId = parameters.get("id")
+ if not dokumentId:
+ raise ValueError("UPDATE operation requires entity ID")
+
+ dokument = realEstateInterface.getDokument(dokumentId)
+ if not dokument:
+ raise ValueError(f"Dokument {dokumentId} not found")
+
+ updateData = {k: v for k, v in parameters.items() if k != "id"}
+ updated = realEstateInterface.updateDokument(dokumentId, updateData)
+ return {
+ "operation": "UPDATE",
+ "entity": "Dokument",
+ "result": updated.model_dump()
+ }
+ else:
+ raise ValueError(f"UPDATE operation not supported for entity: {entity}")
+
+ elif intent == "DELETE":
+ # Delete entity
+ realEstateInterface = getRealEstateInterface(currentUser)
+
+ if entity == "Projekt":
+ projektId = parameters.get("id")
+ if not projektId:
+ raise ValueError("DELETE operation requires entity ID")
+
+ success = realEstateInterface.deleteProjekt(projektId)
+ return {
+ "operation": "DELETE",
+ "entity": "Projekt",
+ "success": success
+ }
+ elif entity == "Parzelle":
+ parzelleId = parameters.get("id")
+ if not parzelleId:
+ raise ValueError("DELETE operation requires entity ID")
+
+ success = realEstateInterface.deleteParzelle(parzelleId)
+ return {
+ "operation": "DELETE",
+ "entity": "Parzelle",
+ "success": success
+ }
+ elif entity == "Gemeinde":
+ from modules.datamodels.datamodelRealEstate import Gemeinde
+ gemeindeId = parameters.get("id")
+ if not gemeindeId:
+ raise ValueError("DELETE operation requires entity ID")
+
+ success = realEstateInterface.deleteGemeinde(gemeindeId)
+ return {
+ "operation": "DELETE",
+ "entity": "Gemeinde",
+ "success": success
+ }
+ elif entity == "Kanton":
+ from modules.datamodels.datamodelRealEstate import Kanton
+ kantonId = parameters.get("id")
+ if not kantonId:
+ raise ValueError("DELETE operation requires entity ID")
+
+ success = realEstateInterface.deleteKanton(kantonId)
+ return {
+ "operation": "DELETE",
+ "entity": "Kanton",
+ "success": success
+ }
+ elif entity == "Land":
+ from modules.datamodels.datamodelRealEstate import Land
+ landId = parameters.get("id")
+ if not landId:
+ raise ValueError("DELETE operation requires entity ID")
+
+ success = realEstateInterface.deleteLand(landId)
+ return {
+ "operation": "DELETE",
+ "entity": "Land",
+ "success": success
+ }
+ elif entity == "Dokument":
+ from modules.datamodels.datamodelRealEstate import Dokument
+ dokumentId = parameters.get("id")
+ if not dokumentId:
+ raise ValueError("DELETE operation requires entity ID")
+
+ success = realEstateInterface.deleteDokument(dokumentId)
+ return {
+ "operation": "DELETE",
+ "entity": "Dokument",
+ "success": success
+ }
+ else:
+ raise ValueError(f"DELETE operation not supported for entity: {entity}")
+
+ else:
+ raise ValueError(f"Unknown intent: {intent}")
+
+ except Exception as e:
+ logger.error(f"Error executing intent-based operation: {str(e)}", exc_info=True)
+ raise
+
diff --git a/modules/interfaces/interfaceDbRealEstateAccess.py b/modules/interfaces/interfaceDbRealEstateAccess.py
new file mode 100644
index 00000000..9a25293f
--- /dev/null
+++ b/modules/interfaces/interfaceDbRealEstateAccess.py
@@ -0,0 +1,87 @@
+"""
+Access control for Real Estate interface.
+Handles user access management and permission checks.
+"""
+
+import logging
+from typing import Dict, Any, List, Optional
+from modules.datamodels.datamodelUam import User, UserPrivilege
+
+logger = logging.getLogger(__name__)
+
+
+class RealEstateAccess:
+ """
+ Access control class for Real Estate interface.
+ Handles user access management and permission checks.
+ """
+
+ def __init__(self, currentUser: User, db):
+ """Initialize with user context."""
+ self.currentUser = currentUser
+ self.mandateId = currentUser.mandateId
+ self.userId = currentUser.id
+ self.privilege = currentUser.privilege
+
+ if not self.mandateId or not self.userId:
+ raise ValueError("Invalid user context: mandateId and userId are required")
+
+ self.db = db
+
+ def uam(self, model_class: type, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """
+ Unified user access management function that filters data based on user privileges.
+
+ Args:
+ model_class: Pydantic model class for the table
+ recordset: Recordset to filter based on access rules
+
+ Returns:
+ Filtered recordset with access control attributes
+ """
+ filtered_records = []
+
+ # System admins see all records
+ if self.privilege == UserPrivilege.SYSADMIN:
+ filtered_records = recordset
+ # Admins see records in their mandate
+ elif self.privilege == UserPrivilege.ADMIN:
+ filtered_records = [r for r in recordset if r.get("mandateId", "-") == self.mandateId]
+ # Regular users see only their records
+ else:
+ filtered_records = [
+ r for r in recordset
+ if r.get("mandateId", "-") == self.mandateId and r.get("_createdBy") == self.userId
+ ]
+
+ # Add access control attributes
+ for record in filtered_records:
+ record["_hideView"] = False
+ record["_hideEdit"] = not self.canModify(model_class, record.get("id"))
+ record["_hideDelete"] = not self.canModify(model_class, record.get("id"))
+
+ return filtered_records
+
+ def canModify(self, model_class: type, recordId: Optional[str] = None) -> bool:
+ """Checks if the current user can modify records."""
+ if self.privilege == UserPrivilege.SYSADMIN:
+ return True
+
+ if recordId is not None:
+ records = self.db.getRecordset(model_class, recordFilter={"id": recordId})
+ if not records:
+ return False
+
+ record = records[0]
+
+ if self.privilege == UserPrivilege.ADMIN and record.get("mandateId", "-") == self.mandateId:
+ return True
+
+ if (record.get("mandateId", "-") == self.mandateId and
+ record.get("_createdBy") == self.userId):
+ return True
+
+ return False
+ else:
+ return True # Regular users can create records
+
diff --git a/modules/interfaces/interfaceDbRealEstateObjects.py b/modules/interfaces/interfaceDbRealEstateObjects.py
new file mode 100644
index 00000000..2c6d1554
--- /dev/null
+++ b/modules/interfaces/interfaceDbRealEstateObjects.py
@@ -0,0 +1,713 @@
+"""
+Interface to Real Estate database objects.
+Uses PostgreSQL connector for data access with user/mandate filtering.
+Handles CRUD operations on Real Estate entities (Projekt, Parzelle, etc.).
+"""
+
+import logging
+from typing import Dict, Any, List, Optional
+from modules.datamodels.datamodelRealEstate import (
+ Projekt,
+ Parzelle,
+ Dokument,
+ Kanton,
+ Gemeinde,
+ Land,
+ GeoPolylinie,
+ GeoPunkt,
+ Kontext,
+ StatusProzess,
+)
+from modules.datamodels.datamodelUam import User
+from modules.connectors.connectorDbPostgre import DatabaseConnector
+from modules.shared.configuration import APP_CONFIG
+# Import Access-Klasse aus separater Datei
+from modules.interfaces.interfaceDbRealEstateAccess import RealEstateAccess
+
+logger = logging.getLogger(__name__)
+
+# Singleton factory for Real Estate interfaces
+_realEstateInterfaces = {}
+
+
+class RealEstateObjects:
+ """
+ Interface to Real Estate database objects.
+ Uses PostgreSQL connector for data access with user/mandate filtering.
+ Handles CRUD operations on Real Estate entities.
+ """
+
+ def __init__(self, currentUser: Optional[User] = None):
+ """Initializes the Real Estate Interface."""
+ self.currentUser = currentUser
+ self.userId = currentUser.id if currentUser else None
+ self.mandateId = currentUser.mandateId if currentUser else None
+ self.access = None
+
+ # Initialize database
+ self._initializeDatabase()
+
+ # Set user context if provided
+ if currentUser:
+ self.setUserContext(currentUser)
+
+ def _initializeDatabase(self):
+ """Initialize PostgreSQL database connection."""
+ try:
+ # Get database configuration from environment
+ dbHost = APP_CONFIG.get("DB_REALESTATE_HOST", "localhost")
+ dbDatabase = APP_CONFIG.get("DB_REALESTATE_DATABASE", "poweron_realestate")
+ dbUser = APP_CONFIG.get("DB_REALESTATE_USER")
+ dbPassword = APP_CONFIG.get("DB_REALESTATE_PASSWORD_SECRET")
+ dbPort = int(APP_CONFIG.get("DB_REALESTATE_PORT", 5432))
+
+ # Initialize database connector
+ self.db = DatabaseConnector(
+ dbHost=dbHost,
+ dbDatabase=dbDatabase,
+ dbUser=dbUser,
+ dbPassword=dbPassword,
+ dbPort=dbPort,
+ userId=self.userId if self.userId else None,
+ )
+
+ # Initialize database system (creates database and system table if needed)
+ # Note: This is also called in DatabaseConnector.__init__, but we call it explicitly
+ # for consistency with other interfaces and to ensure proper initialization
+ self.db.initDbSystem()
+
+ # Ensure all supporting tables are created (Land, Kanton, Gemeinde, Dokument)
+ # These tables are needed for foreign key relationships
+ self._ensureSupportingTablesExist()
+
+ logger.info(f"Real Estate database connector initialized for database: {dbDatabase}")
+ except Exception as e:
+ logger.error(f"Error initializing Real Estate database: {e}")
+ raise
+
+ def _ensureSupportingTablesExist(self):
+ """Ensure all supporting tables (Land, Kanton, Gemeinde, Dokument) are created."""
+ try:
+ # These tables are created on-demand when first accessed, but we ensure they exist here
+ # to avoid errors when resolving location names to IDs
+ self.db._ensureTableExists(Land)
+ self.db._ensureTableExists(Kanton)
+ self.db._ensureTableExists(Gemeinde)
+ self.db._ensureTableExists(Dokument)
+ logger.debug("Supporting tables (Land, Kanton, Gemeinde, Dokument) verified/created")
+ except Exception as e:
+ logger.warning(f"Error ensuring supporting tables exist: {e}")
+ # Don't raise - tables will be created on-demand anyway
+
+ def setUserContext(self, currentUser: User):
+ """Sets the user context for the interface."""
+ self.currentUser = currentUser
+ self.userId = currentUser.id
+ self.mandateId = currentUser.mandateId
+
+ if not self.userId or not self.mandateId:
+ raise ValueError("Invalid user context: id and mandateId are required")
+
+ # Initialize access control
+ self.access = RealEstateAccess(self.currentUser, self.db)
+
+ # Update database context
+ self.db.updateContext(self.userId)
+
+ # ===== Projekt Methods =====
+
+ def createProjekt(self, projekt: Projekt) -> Projekt:
+ """Create a new project."""
+ # Ensure mandateId is set
+ if not projekt.mandateId:
+ projekt.mandateId = self.mandateId
+
+ # Apply access control
+ self.access.uam(Projekt, [])
+
+ # Save to database
+ self.db.recordCreate(Projekt, projekt.model_dump())
+
+ return projekt
+
+ def getProjekt(self, projektId: str) -> Optional[Projekt]:
+ """Get a project by ID."""
+ records = self.db.getRecordset(
+ Projekt,
+ recordFilter={"id": projektId}
+ )
+
+ if not records:
+ return None
+
+ # Apply access control
+ filtered = self.access.uam(Projekt, records)
+
+ if not filtered:
+ return None
+
+ return Projekt(**filtered[0])
+
+ def getProjekte(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Projekt]:
+ """Get all projects matching the filter."""
+ records = self.db.getRecordset(Projekt, recordFilter=recordFilter or {})
+
+ # Apply access control
+ filtered = self.access.uam(Projekt, records)
+
+ return [Projekt(**r) for r in filtered]
+
+ def updateProjekt(self, projektId: str, updateData: Dict[str, Any]) -> Optional[Projekt]:
+ """Update a project."""
+ projekt = self.getProjekt(projektId)
+ if not projekt:
+ return None
+
+ # Check if user can modify
+ if not self.access.canModify(Projekt, projektId):
+ raise PermissionError(f"User {self.userId} cannot modify project {projektId}")
+
+ # Update fields
+ for key, value in updateData.items():
+ if hasattr(projekt, key):
+ setattr(projekt, key, value)
+
+ # Save to database
+ self.db.recordModify(Projekt, projektId, projekt.model_dump())
+
+ return projekt
+
+ def deleteProjekt(self, projektId: str) -> bool:
+ """Delete a project."""
+ projekt = self.getProjekt(projektId)
+ if not projekt:
+ return False
+
+ # Check if user can modify
+ if not self.access.canModify(Projekt, projektId):
+ raise PermissionError(f"User {self.userId} cannot delete project {projektId}")
+
+ return self.db.recordDelete(Projekt, projektId)
+
+ # ===== Parzelle Methods =====
+
+ def createParzelle(self, parzelle: Parzelle) -> Parzelle:
+ """Create a new plot."""
+ if not parzelle.mandateId:
+ parzelle.mandateId = self.mandateId
+
+ self.access.uam(Parzelle, [])
+ self.db.recordCreate(Parzelle, parzelle.model_dump())
+
+ return parzelle
+
+ def getParzelle(self, parzelleId: str) -> Optional[Parzelle]:
+ """Get a plot by ID."""
+ records = self.db.getRecordset(
+ Parzelle,
+ recordFilter={"id": parzelleId}
+ )
+
+ if not records:
+ return None
+
+ filtered = self.access.uam(Parzelle, records)
+
+ if not filtered:
+ return None
+
+ return Parzelle(**filtered[0])
+
+ def getParzellen(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Parzelle]:
+ """Get all plots matching the filter."""
+ # Resolve location names to IDs if needed
+ if recordFilter:
+ recordFilter = self._resolveLocationFilters(recordFilter)
+
+ records = self.db.getRecordset(Parzelle, recordFilter=recordFilter or {})
+
+ # Apply access control
+ filtered = self.access.uam(Parzelle, records)
+
+ return [Parzelle(**r) for r in filtered]
+
+ def _resolveLocationFilters(self, recordFilter: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ Resolve location names to IDs for foreign key fields.
+ Only handles kontextGemeinde (Parzelle → Gemeinde).
+ Note: Parzelle does NOT have direct links to Kanton or Land.
+ The relationship is: Parzelle → Gemeinde → Kanton → Land
+ """
+ resolvedFilter = recordFilter.copy()
+
+ # Resolve Gemeinde name to ID
+ # This is the only direct location link on Parzelle
+ if "kontextGemeinde" in resolvedFilter:
+ gemeindeValue = resolvedFilter["kontextGemeinde"]
+ # Check if it's a name (not a UUID-like string)
+ if not self._isUUID(gemeindeValue):
+ gemeindeId = self._resolveGemeindeByName(gemeindeValue)
+ if gemeindeId:
+ resolvedFilter["kontextGemeinde"] = gemeindeId
+ logger.debug(f"Resolved Gemeinde name '{gemeindeValue}' to ID '{gemeindeId}'")
+ else:
+ logger.warning(f"Gemeinde '{gemeindeValue}' not found, filter may return no results")
+ # Keep the original value - query will return empty if not found
+
+ # Note: kontextKanton and kontextLand are NOT fields on Parzelle
+ # If they appear in the filter, they will be filtered out by the validation in mainRealEstate.py
+
+ return resolvedFilter
+
+ def _isUUID(self, value: str) -> bool:
+ """Check if a string looks like a UUID."""
+ import re
+ uuid_pattern = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.IGNORECASE)
+ return bool(uuid_pattern.match(value))
+
+ def _resolveGemeindeByName(self, name: str) -> Optional[str]:
+ """Resolve Gemeinde name to ID by looking up in Gemeinde table."""
+ try:
+ # First try exact match
+ gemeinden = self.db.getRecordset(
+ Gemeinde,
+ recordFilter={"label": name}
+ )
+ if gemeinden:
+ gemeindeId = gemeinden[0].get("id")
+ logger.debug(f"Found Gemeinde '{name}' with ID '{gemeindeId}'")
+ return gemeindeId
+
+ # If no exact match, try case-insensitive search via SQL query
+ # This handles cases where the name might have different casing
+ self.db._ensure_connection()
+ with self.db.connection.cursor() as cursor:
+ cursor.execute(
+ 'SELECT "id" FROM "Gemeinde" WHERE LOWER("label") = LOWER(%s) LIMIT 1',
+ (name,)
+ )
+ result = cursor.fetchone()
+ if result:
+ # psycopg2 returns tuples, so result[0] is the id
+ gemeindeId = result[0]
+ logger.debug(f"Found Gemeinde '{name}' (case-insensitive) with ID '{gemeindeId}'")
+ return gemeindeId
+
+ logger.warning(f"Gemeinde '{name}' not found in database")
+ return None
+ except Exception as e:
+ logger.error(f"Error resolving Gemeinde by name '{name}': {e}", exc_info=True)
+ return None
+
+ def _resolveKantonByName(self, name: str) -> Optional[str]:
+ """Resolve Kanton name to ID by looking up in Kanton table."""
+ try:
+ # First try exact match
+ kantone = self.db.getRecordset(
+ Kanton,
+ recordFilter={"label": name}
+ )
+ if kantone:
+ kantonId = kantone[0].get("id")
+ logger.debug(f"Found Kanton '{name}' with ID '{kantonId}'")
+ return kantonId
+
+ # Try case-insensitive search
+ self.db._ensure_connection()
+ with self.db.connection.cursor() as cursor:
+ cursor.execute(
+ 'SELECT "id" FROM "Kanton" WHERE LOWER("label") = LOWER(%s) LIMIT 1',
+ (name,)
+ )
+ result = cursor.fetchone()
+ if result:
+ # psycopg2 returns tuples, so result[0] is the id
+ kantonId = result[0]
+ logger.debug(f"Found Kanton '{name}' (case-insensitive) with ID '{kantonId}'")
+ return kantonId
+
+ logger.warning(f"Kanton '{name}' not found in database")
+ return None
+ except Exception as e:
+ logger.error(f"Error resolving Kanton by name '{name}': {e}", exc_info=True)
+ return None
+
+ def _resolveLandByName(self, name: str) -> Optional[str]:
+ """Resolve Land name to ID by looking up in Land table."""
+ try:
+ # First try exact match
+ laender = self.db.getRecordset(
+ Land,
+ recordFilter={"label": name}
+ )
+ if laender:
+ landId = laender[0].get("id")
+ logger.debug(f"Found Land '{name}' with ID '{landId}'")
+ return landId
+
+ # Try case-insensitive search
+ self.db._ensure_connection()
+ with self.db.connection.cursor() as cursor:
+ cursor.execute(
+ 'SELECT "id" FROM "Land" WHERE LOWER("label") = LOWER(%s) LIMIT 1',
+ (name,)
+ )
+ result = cursor.fetchone()
+ if result:
+ # psycopg2 returns tuples, so result[0] is the id
+ landId = result[0]
+ logger.debug(f"Found Land '{name}' (case-insensitive) with ID '{landId}'")
+ return landId
+
+ logger.warning(f"Land '{name}' not found in database")
+ return None
+ except Exception as e:
+ logger.error(f"Error resolving Land by name '{name}': {e}", exc_info=True)
+ return None
+
+ def updateParzelle(self, parzelleId: str, updateData: Dict[str, Any]) -> Optional[Parzelle]:
+ """Update a plot."""
+ parzelle = self.getParzelle(parzelleId)
+ if not parzelle:
+ return None
+
+ if not self.access.canModify(Parzelle, parzelleId):
+ raise PermissionError(f"User {self.userId} cannot modify plot {parzelleId}")
+
+ for key, value in updateData.items():
+ if hasattr(parzelle, key):
+ setattr(parzelle, key, value)
+
+ self.db.recordModify(Parzelle, parzelleId, parzelle.model_dump())
+
+ return parzelle
+
+ def deleteParzelle(self, parzelleId: str) -> bool:
+ """Delete a plot."""
+ parzelle = self.getParzelle(parzelleId)
+ if not parzelle:
+ return False
+
+ if not self.access.canModify(Parzelle, parzelleId):
+ raise PermissionError(f"User {self.userId} cannot delete plot {parzelleId}")
+
+ return self.db.recordDelete(Parzelle, parzelleId)
+
+ # ===== Dokument Methods =====
+
+ def createDokument(self, dokument: Dokument) -> Dokument:
+ """Create a new document."""
+ if not dokument.mandateId:
+ dokument.mandateId = self.mandateId
+
+ self.access.uam(Dokument, [])
+ self.db.recordCreate(Dokument, dokument.model_dump())
+
+ return dokument
+
+ def getDokument(self, dokumentId: str) -> Optional[Dokument]:
+ """Get a document by ID."""
+ records = self.db.getRecordset(
+ Dokument,
+ recordFilter={"id": dokumentId}
+ )
+
+ if not records:
+ return None
+
+ filtered = self.access.uam(Dokument, records)
+
+ if not filtered:
+ return None
+
+ return Dokument(**filtered[0])
+
+ def getDokumente(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Dokument]:
+ """Get all documents matching the filter."""
+ records = self.db.getRecordset(Dokument, recordFilter=recordFilter or {})
+ filtered = self.access.uam(Dokument, records)
+ return [Dokument(**r) for r in filtered]
+
+ def updateDokument(self, dokumentId: str, updateData: Dict[str, Any]) -> Optional[Dokument]:
+ """Update a document."""
+ dokument = self.getDokument(dokumentId)
+ if not dokument:
+ return None
+
+ if not self.access.canModify(Dokument, dokumentId):
+ raise PermissionError(f"User {self.userId} cannot modify document {dokumentId}")
+
+ for key, value in updateData.items():
+ if hasattr(dokument, key):
+ setattr(dokument, key, value)
+
+ self.db.recordModify(Dokument, dokumentId, dokument.model_dump())
+ return dokument
+
+ def deleteDokument(self, dokumentId: str) -> bool:
+ """Delete a document."""
+ dokument = self.getDokument(dokumentId)
+ if not dokument:
+ return False
+
+ if not self.access.canModify(Dokument, dokumentId):
+ raise PermissionError(f"User {self.userId} cannot delete document {dokumentId}")
+
+ return self.db.recordDelete(Dokument, dokumentId)
+
+ # ===== Gemeinde Methods =====
+
+ def createGemeinde(self, gemeinde: Gemeinde) -> Gemeinde:
+ """Create a new municipality."""
+ if not gemeinde.mandateId:
+ gemeinde.mandateId = self.mandateId
+
+ self.access.uam(Gemeinde, [])
+ self.db.recordCreate(Gemeinde, gemeinde.model_dump())
+
+ return gemeinde
+
+ def getGemeinde(self, gemeindeId: str) -> Optional[Gemeinde]:
+ """Get a municipality by ID."""
+ records = self.db.getRecordset(
+ Gemeinde,
+ recordFilter={"id": gemeindeId}
+ )
+
+ if not records:
+ return None
+
+ filtered = self.access.uam(Gemeinde, records)
+
+ if not filtered:
+ return None
+
+ return Gemeinde(**filtered[0])
+
+ def getGemeinden(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Gemeinde]:
+ """Get all municipalities matching the filter."""
+ records = self.db.getRecordset(Gemeinde, recordFilter=recordFilter or {})
+ filtered = self.access.uam(Gemeinde, records)
+ return [Gemeinde(**r) for r in filtered]
+
+ def updateGemeinde(self, gemeindeId: str, updateData: Dict[str, Any]) -> Optional[Gemeinde]:
+ """Update a municipality."""
+ gemeinde = self.getGemeinde(gemeindeId)
+ if not gemeinde:
+ return None
+
+ if not self.access.canModify(Gemeinde, gemeindeId):
+ raise PermissionError(f"User {self.userId} cannot modify municipality {gemeindeId}")
+
+ for key, value in updateData.items():
+ if hasattr(gemeinde, key):
+ setattr(gemeinde, key, value)
+
+ self.db.recordModify(Gemeinde, gemeindeId, gemeinde.model_dump())
+ return gemeinde
+
+ def deleteGemeinde(self, gemeindeId: str) -> bool:
+ """Delete a municipality."""
+ gemeinde = self.getGemeinde(gemeindeId)
+ if not gemeinde:
+ return False
+
+ if not self.access.canModify(Gemeinde, gemeindeId):
+ raise PermissionError(f"User {self.userId} cannot delete municipality {gemeindeId}")
+
+ return self.db.recordDelete(Gemeinde, gemeindeId)
+
+ # ===== Kanton Methods =====
+
+ def createKanton(self, kanton: Kanton) -> Kanton:
+ """Create a new canton."""
+ if not kanton.mandateId:
+ kanton.mandateId = self.mandateId
+
+ self.access.uam(Kanton, [])
+ self.db.recordCreate(Kanton, kanton.model_dump())
+
+ return kanton
+
+ def getKanton(self, kantonId: str) -> Optional[Kanton]:
+ """Get a canton by ID."""
+ records = self.db.getRecordset(
+ Kanton,
+ recordFilter={"id": kantonId}
+ )
+
+ if not records:
+ return None
+
+ filtered = self.access.uam(Kanton, records)
+
+ if not filtered:
+ return None
+
+ return Kanton(**filtered[0])
+
+ def getKantone(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Kanton]:
+ """Get all cantons matching the filter."""
+ records = self.db.getRecordset(Kanton, recordFilter=recordFilter or {})
+ filtered = self.access.uam(Kanton, records)
+ return [Kanton(**r) for r in filtered]
+
+ def updateKanton(self, kantonId: str, updateData: Dict[str, Any]) -> Optional[Kanton]:
+ """Update a canton."""
+ kanton = self.getKanton(kantonId)
+ if not kanton:
+ return None
+
+ if not self.access.canModify(Kanton, kantonId):
+ raise PermissionError(f"User {self.userId} cannot modify canton {kantonId}")
+
+ for key, value in updateData.items():
+ if hasattr(kanton, key):
+ setattr(kanton, key, value)
+
+ self.db.recordModify(Kanton, kantonId, kanton.model_dump())
+ return kanton
+
+ def deleteKanton(self, kantonId: str) -> bool:
+ """Delete a canton."""
+ kanton = self.getKanton(kantonId)
+ if not kanton:
+ return False
+
+ if not self.access.canModify(Kanton, kantonId):
+ raise PermissionError(f"User {self.userId} cannot delete canton {kantonId}")
+
+ return self.db.recordDelete(Kanton, kantonId)
+
+ # ===== Land Methods =====
+
+ def createLand(self, land: Land) -> Land:
+ """Create a new country."""
+ if not land.mandateId:
+ land.mandateId = self.mandateId
+
+ self.access.uam(Land, [])
+ self.db.recordCreate(Land, land.model_dump())
+
+ return land
+
+ def getLand(self, landId: str) -> Optional[Land]:
+ """Get a country by ID."""
+ records = self.db.getRecordset(
+ Land,
+ recordFilter={"id": landId}
+ )
+
+ if not records:
+ return None
+
+ filtered = self.access.uam(Land, records)
+
+ if not filtered:
+ return None
+
+ return Land(**filtered[0])
+
+ def getLaender(self, recordFilter: Optional[Dict[str, Any]] = None) -> List[Land]:
+ """Get all countries matching the filter."""
+ records = self.db.getRecordset(Land, recordFilter=recordFilter or {})
+ filtered = self.access.uam(Land, records)
+ return [Land(**r) for r in filtered]
+
+ def updateLand(self, landId: str, updateData: Dict[str, Any]) -> Optional[Land]:
+ """Update a country."""
+ land = self.getLand(landId)
+ if not land:
+ return None
+
+ if not self.access.canModify(Land, landId):
+ raise PermissionError(f"User {self.userId} cannot modify country {landId}")
+
+ for key, value in updateData.items():
+ if hasattr(land, key):
+ setattr(land, key, value)
+
+ self.db.recordModify(Land, landId, land.model_dump())
+ return land
+
+ def deleteLand(self, landId: str) -> bool:
+ """Delete a country."""
+ land = self.getLand(landId)
+ if not land:
+ return False
+
+ if not self.access.canModify(Land, landId):
+ raise PermissionError(f"User {self.userId} cannot delete country {landId}")
+
+ return self.db.recordDelete(Land, landId)
+
+ # ===== Direct Query Execution (stateless) =====
+
+ def executeQuery(self, queryText: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
+ """
+ Execute a SQL query directly on the database (stateless).
+
+ WARNING: This method executes raw SQL. Ensure proper validation and sanitization
+ before calling this method. Consider implementing query whitelisting or
+ only allowing SELECT statements for production use.
+
+ Args:
+ queryText: SQL query string (preferably SELECT only)
+ parameters: Optional parameters for parameterized queries
+
+ Returns:
+ Dictionary with 'rows' (list of dicts), 'columns' (list of column names),
+ 'rowCount' (int), and 'executionTime' (float)
+ """
+ import time
+
+ try:
+ start_time = time.time()
+
+ # Ensure connection is alive
+ self.db._ensure_connection()
+
+ with self.db.connection.cursor() as cursor:
+ # Execute query
+ if parameters:
+ # Use parameterized query for safety
+ cursor.execute(queryText, parameters)
+ else:
+ cursor.execute(queryText)
+
+ # Fetch results
+ rows = cursor.fetchall()
+
+ # Convert to list of dictionaries
+ result_rows = []
+ if rows:
+ columns = [desc[0] for desc in cursor.description] if cursor.description else []
+ result_rows = [dict(zip(columns, row)) for row in rows]
+ else:
+ columns = []
+
+ execution_time = time.time() - start_time
+
+ return {
+ "rows": result_rows,
+ "columns": columns,
+ "rowCount": len(result_rows),
+ "executionTime": execution_time,
+ }
+ except Exception as e:
+ logger.error(f"Error executing query: {e}", exc_info=True)
+ raise
+
+
+def getInterface(currentUser: User) -> RealEstateObjects:
+ """
+ Factory function to get or create a Real Estate interface instance for a user.
+ Uses singleton pattern per user.
+ """
+ userKey = f"{currentUser.id}_{currentUser.mandateId}"
+
+ if userKey not in _realEstateInterfaces:
+ _realEstateInterfaces[userKey] = RealEstateObjects(currentUser)
+
+ return _realEstateInterfaces[userKey]
+
diff --git a/modules/routes/routeRealEstate.py b/modules/routes/routeRealEstate.py
new file mode 100644
index 00000000..b5e34ef3
--- /dev/null
+++ b/modules/routes/routeRealEstate.py
@@ -0,0 +1,637 @@
+"""
+Real Estate routes for the backend API.
+Implements stateless endpoints for real estate database operations with AI-powered natural language processing.
+"""
+
+import logging
+import json
+from typing import Optional, Dict, Any, List, Union
+from fastapi import APIRouter, HTTPException, Depends, Body, Request, Query, Path, status
+
+# Import auth modules
+from modules.security.auth import limiter, getCurrentUser
+
+# Import models
+from modules.datamodels.datamodelUam import User
+from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
+from modules.datamodels.datamodelRealEstate import (
+ Projekt,
+ Parzelle,
+ Dokument,
+ Gemeinde,
+ Kanton,
+ Land,
+)
+
+# Import interfaces
+from modules.interfaces.interfaceDbRealEstateObjects import getInterface as getRealEstateInterface
+
+# Import feature logic
+from modules.features.realEstate.mainRealEstate import (
+ processNaturalLanguageCommand,
+ executeDirectQuery,
+)
+
+# Import attribute utilities for model schema
+from modules.shared.attributeUtils import getModelAttributeDefinitions
+
+# Configure logger
+logger = logging.getLogger(__name__)
+
+# Create router for real estate endpoints
+router = APIRouter(
+ prefix="/api/realestate",
+ tags=["Real Estate"],
+ responses={
+ 404: {"description": "Not found"},
+ 400: {"description": "Bad request"},
+ 401: {"description": "Unauthorized"},
+ 403: {"description": "Forbidden"},
+ 500: {"description": "Internal server error"}
+ }
+)
+
+
+@router.post("/command", response_model=Dict[str, Any])
+@limiter.limit("120/minute")
+async def process_command(
+ request: Request,
+ userInput: str = Body(..., embed=True, description="Natural language command"),
+ currentUser: User = Depends(getCurrentUser)
+) -> Dict[str, Any]:
+ """
+ Process natural language command and execute corresponding CRUD operation.
+
+ Uses AI to analyze user intent and extract parameters, then executes the appropriate
+ CRUD operation. Works stateless without session management.
+
+ Example user inputs:
+ - "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
+ - "Zeige mir alle Projekte in Zürich"
+ - "Aktualisiere Projekt XYZ mit Status 'Planung'"
+ - "Lösche Parzelle ABC"
+ - "SELECT * FROM Projekt WHERE plz = '8000'"
+
+ Headers:
+ - X-CSRF-Token: CSRF token (required for security)
+
+ Returns:
+ {
+ "success": true,
+ "intent": "CREATE|READ|UPDATE|DELETE|QUERY",
+ "entity": "Projekt|Parzelle|...|null",
+ "result": {...}
+ }
+ """
+ try:
+ # Validate CSRF token (middleware also checks, but explicit validation for better error messages)
+ csrf_token = request.headers.get("X-CSRF-Token") or request.headers.get("x-csrf-token")
+ if not csrf_token:
+ logger.warning(f"CSRF token missing for POST /api/realestate/command from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="CSRF token missing. Please include X-CSRF-Token header."
+ )
+
+ # Basic CSRF token format validation
+ if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
+ logger.warning(f"Invalid CSRF token format for POST /api/realestate/command from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid CSRF token format"
+ )
+
+ # Validate token is hex string
+ try:
+ int(csrf_token, 16)
+ except ValueError:
+ logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/command from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid CSRF token format"
+ )
+
+ logger.info(f"Processing command request from user {currentUser.id} (mandate: {currentUser.mandateId})")
+ logger.debug(f"User input: {userInput}")
+
+ # Process natural language command with AI
+ result = await processNaturalLanguageCommand(
+ currentUser=currentUser,
+ userInput=userInput
+ )
+
+ return result
+
+ except ValueError as e:
+ logger.error(f"Validation error in process_command: {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Validation error: {str(e)}"
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error processing command: {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error processing command: {str(e)}"
+ )
+
+@router.post("/query", response_model=Dict[str, Any])
+@limiter.limit("120/minute")
+async def execute_query(
+ request: Request,
+ body: Dict[str, Any] = Body(...),
+ currentUser: User = Depends(getCurrentUser)
+) -> Dict[str, Any]:
+ """
+ Execute a direct SQL query without session management.
+
+ Executes the query directly and returns the result. No query history is saved.
+
+ Request body:
+ {
+ "queryText": "SELECT * FROM Projekt WHERE plz = '8000'",
+ "parameters": { // Optional
+ "$1": "8000"
+ }
+ }
+
+ Headers:
+ - X-CSRF-Token: CSRF token (required for security)
+
+ WARNING: This endpoint executes raw SQL queries. Ensure proper validation
+ and sanitization on the frontend. Consider implementing query whitelisting
+ or only allowing SELECT statements for production use.
+
+ Returns:
+ {
+ "status": "success",
+ "rows": [...],
+ "columns": [...],
+ "rowCount": 15,
+ "executionTime": 0.123
+ }
+ """
+ try:
+ # Validate CSRF token (middleware also checks, but explicit validation for better error messages)
+ csrf_token = request.headers.get("X-CSRF-Token") or request.headers.get("x-csrf-token")
+ if not csrf_token:
+ logger.warning(f"CSRF token missing for POST /api/realestate/query from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="CSRF token missing. Please include X-CSRF-Token header."
+ )
+
+ # Basic CSRF token format validation
+ if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
+ logger.warning(f"Invalid CSRF token format for POST /api/realestate/query from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid CSRF token format"
+ )
+
+ # Validate token is hex string
+ try:
+ int(csrf_token, 16)
+ except ValueError:
+ logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/query from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid CSRF token format"
+ )
+
+ # Extract fields from body
+ queryText = body.get("queryText")
+ if not queryText:
+ raise ValueError("queryText is required")
+
+ parameters = body.get("parameters")
+
+ logger.info(f"Processing query request from user {currentUser.id} (mandate: {currentUser.mandateId})")
+ logger.debug(f"Query text: {queryText}")
+ if parameters:
+ logger.debug(f"Query parameters: {parameters}")
+
+ # Execute direct query
+ result = await executeDirectQuery(
+ currentUser=currentUser,
+ queryText=queryText,
+ parameters=parameters,
+ )
+
+ return result
+
+ except ValueError as e:
+ logger.error(f"Validation error in execute_query: {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Validation error: {str(e)}"
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error executing query: {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error executing query: {str(e)}"
+ )
+
+
+@router.get("/tables", response_model=Dict[str, Any])
+@limiter.limit("120/minute")
+async def get_available_tables(
+ request: Request,
+ currentUser: User = Depends(getCurrentUser)
+) -> Dict[str, Any]:
+ """
+ Get all available real estate tables.
+
+ Returns a list of available table names with their descriptions.
+
+ Headers:
+ - X-CSRF-Token: CSRF token (required for security)
+
+ Example:
+ - GET /api/realestate/tables
+ """
+ try:
+ # Validate CSRF token if provided
+ csrf_token = request.headers.get("X-CSRF-Token") or request.headers.get("x-csrf-token")
+ if not csrf_token:
+ logger.warning(f"CSRF token missing for GET /api/realestate/tables from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="CSRF token missing. Please include X-CSRF-Token header."
+ )
+
+ # Basic CSRF token format validation
+ if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
+ logger.warning(f"Invalid CSRF token format for GET /api/realestate/tables from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid CSRF token format"
+ )
+
+ # Validate token is hex string
+ try:
+ int(csrf_token, 16)
+ except ValueError:
+ logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/tables from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid CSRF token format"
+ )
+
+ logger.info(f"Getting available tables for user {currentUser.id} (mandate: {currentUser.mandateId})")
+
+ # Define available tables with descriptions
+ tables = [
+ {
+ "name": "Projekt",
+ "description": "Real estate projects",
+ "model": "Projekt"
+ },
+ {
+ "name": "Parzelle",
+ "description": "Plots/parcels",
+ "model": "Parzelle"
+ },
+ {
+ "name": "Dokument",
+ "description": "Documents",
+ "model": "Dokument"
+ },
+ {
+ "name": "Gemeinde",
+ "description": "Municipalities",
+ "model": "Gemeinde"
+ },
+ {
+ "name": "Kanton",
+ "description": "Cantons",
+ "model": "Kanton"
+ },
+ {
+ "name": "Land",
+ "description": "Countries",
+ "model": "Land"
+ },
+ ]
+
+ return {
+ "tables": tables,
+ "count": len(tables)
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error getting available tables: {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error getting available tables: {str(e)}"
+ )
+
+
+@router.get("/table/{table}", response_model=PaginatedResponse[Any])
+@limiter.limit("120/minute")
+async def get_table_data(
+ request: Request,
+ table: str = Path(..., description="Table name (Projekt, Parzelle, Dokument, Gemeinde, Kanton, Land)"),
+ pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
+ currentUser: User = Depends(getCurrentUser)
+) -> PaginatedResponse[Dict[str, Any]]:
+ """
+ Get all data from a specific real estate table with optional pagination.
+
+ Available tables:
+ - Projekt: Real estate projects
+ - Parzelle: Plots/parcels
+ - Dokument: Documents
+ - Gemeinde: Municipalities
+ - Kanton: Cantons
+ - Land: Countries
+
+ Query Parameters:
+ - pagination: JSON-encoded PaginationParams object, or None for no pagination
+
+ Headers:
+ - X-CSRF-Token: CSRF token (required for security)
+
+ Examples:
+ - GET /api/realestate/table/Projekt (no pagination - returns all items)
+ - GET /api/realestate/table/Parzelle?pagination={"page":1,"pageSize":10,"sort":[]}
+ - GET /api/realestate/table/Gemeinde?pagination={"page":2,"pageSize":20,"sort":[{"field":"label","direction":"asc"}]}
+ """
+ try:
+ # Validate CSRF token if provided
+ csrf_token = request.headers.get("X-CSRF-Token") or request.headers.get("x-csrf-token")
+ if not csrf_token:
+ logger.warning(f"CSRF token missing for GET /api/realestate/table/{table} from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="CSRF token missing. Please include X-CSRF-Token header."
+ )
+
+ # Basic CSRF token format validation
+ if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
+ logger.warning(f"Invalid CSRF token format for GET /api/realestate/table/{table} from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid CSRF token format"
+ )
+
+ # Validate token is hex string
+ try:
+ int(csrf_token, 16)
+ except ValueError:
+ logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/table/{table} from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid CSRF token format"
+ )
+
+ logger.info(f"Getting table data for '{table}' from user {currentUser.id} (mandate: {currentUser.mandateId})")
+
+ # Map table names to model classes and getter methods
+ table_mapping = {
+ "Projekt": (Projekt, "getProjekte"),
+ "Parzelle": (Parzelle, "getParzellen"),
+ "Dokument": (Dokument, "getDokumente"),
+ "Gemeinde": (Gemeinde, "getGemeinden"),
+ "Kanton": (Kanton, "getKantone"),
+ "Land": (Land, "getLaender"),
+ }
+
+ # Validate table name
+ if table not in table_mapping:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Invalid table name '{table}'. Available tables: {', '.join(table_mapping.keys())}"
+ )
+
+ # Get interface and fetch data
+ realEstateInterface = getRealEstateInterface(currentUser)
+ model_class, method_name = table_mapping[table]
+ getter_method = getattr(realEstateInterface, method_name)
+
+ # Fetch all records (no filter for now)
+ records = getter_method(recordFilter=None)
+
+ # Keep records as model instances (like routeDataFiles does with FileItem)
+ # FastAPI will automatically serialize Pydantic models to JSON
+ items = records
+
+ # If table is empty, create an empty instance with all fields set to None/empty
+ # This allows the frontend to extract column structure from the response
+ # All fields will be None/empty - no IDs or other values generated
+ if not items:
+ try:
+ # Get all model fields
+ model_fields = model_class.model_fields
+ empty_values = {}
+
+ # Set all fields to None - explicitly set every field to None
+ # This ensures no default_factory is called and no IDs are generated
+ for field_name in model_fields.keys():
+ empty_values[field_name] = None
+
+ # Create instance with all None values
+ # Use model_validate with allow_none=True or construct directly
+ empty_instance = model_class.model_construct(**empty_values)
+ items = [empty_instance]
+ logger.debug(f"Created empty instance for {table} with all fields set to None")
+ except Exception as e:
+ logger.warning(f"Could not create empty instance for {table}: {str(e)}. Returning empty list.")
+ items = []
+
+ # Parse pagination parameter
+ paginationParams = None
+ if pagination:
+ try:
+ paginationDict = json.loads(pagination)
+ paginationParams = PaginationParams(**paginationDict) if paginationDict else None
+ except (json.JSONDecodeError, ValueError) as e:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Invalid pagination parameter: {str(e)}"
+ )
+
+ # Apply pagination if requested
+ if paginationParams:
+ # Apply sorting if specified
+ if paginationParams.sort:
+ for sort_field in reversed(paginationParams.sort): # Reverse to apply in priority order
+ field_name = sort_field.field
+ direction = sort_field.direction.lower()
+
+ def sort_key(item):
+ # Access attribute from model instance
+ value = getattr(item, field_name, None)
+ # Handle None values - put them at the end for asc, at the start for desc
+ if value is None:
+ return (1, None) # Use tuple to ensure None values sort consistently
+ return (0, value)
+
+ items.sort(key=sort_key, reverse=(direction == "desc"))
+
+ # Apply pagination
+ total_items = len(items)
+ total_pages = (total_items + paginationParams.pageSize - 1) // paginationParams.pageSize # Ceiling division
+ start_idx = (paginationParams.page - 1) * paginationParams.pageSize
+ end_idx = start_idx + paginationParams.pageSize
+ paginated_items = items[start_idx:end_idx]
+
+ return PaginatedResponse(
+ items=paginated_items,
+ pagination=PaginationMetadata(
+ currentPage=paginationParams.page,
+ pageSize=paginationParams.pageSize,
+ totalItems=total_items,
+ totalPages=total_pages,
+ sort=paginationParams.sort,
+ filters=paginationParams.filters
+ )
+ )
+ else:
+ # No pagination - return all items (as model instances, like routeDataFiles)
+ return PaginatedResponse(
+ items=items,
+ pagination=None
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error getting table data for '{table}': {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error getting table data: {str(e)}"
+ )
+
+
+@router.post("/table/{table}", response_model=Dict[str, Any])
+@limiter.limit("120/minute")
+async def create_table_record(
+ request: Request,
+ table: str = Path(..., description="Table name (Projekt, Parzelle, Dokument, Gemeinde, Kanton, Land)"),
+ data: Dict[str, Any] = Body(..., description="Record data to create"),
+ currentUser: User = Depends(getCurrentUser)
+) -> Dict[str, Any]:
+ """
+ Create a new record in a specific real estate table.
+
+ Available tables:
+ - Projekt: Real estate projects
+ - Parzelle: Plots/parcels
+ - Dokument: Documents
+ - Gemeinde: Municipalities
+ - Kanton: Cantons
+ - Land: Countries
+
+ Request Body:
+ - JSON object with fields matching the table's data model
+
+ Headers:
+ - X-CSRF-Token: CSRF token (required for security)
+
+ Examples:
+ - POST /api/realestate/table/Projekt
+ Body: {"label": "Hauptstrasse 42", "statusProzess": "Eingang"}
+ - POST /api/realestate/table/Parzelle
+ Body: {"label": "Parzelle 1", "strasseNr": "Hauptstrasse 42", "plz": "8000", "bauzone": "W3"}
+ """
+ try:
+ # Validate CSRF token
+ csrf_token = request.headers.get("X-CSRF-Token") or request.headers.get("x-csrf-token")
+ if not csrf_token:
+ logger.warning(f"CSRF token missing for POST /api/realestate/table/{table} from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="CSRF token missing. Please include X-CSRF-Token header."
+ )
+
+ # Basic CSRF token format validation
+ if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
+ logger.warning(f"Invalid CSRF token format for POST /api/realestate/table/{table} from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid CSRF token format"
+ )
+
+ # Validate token is hex string
+ try:
+ int(csrf_token, 16)
+ except ValueError:
+ logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/table/{table} from user {currentUser.id}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid CSRF token format"
+ )
+
+ logger.info(f"Creating record in table '{table}' for user {currentUser.id} (mandate: {currentUser.mandateId})")
+ logger.debug(f"Record data: {data}")
+
+ # Map table names to model classes and create methods
+ table_mapping = {
+ "Projekt": (Projekt, "createProjekt"),
+ "Parzelle": (Parzelle, "createParzelle"),
+ "Dokument": (Dokument, "createDokument"),
+ "Gemeinde": (Gemeinde, "createGemeinde"),
+ "Kanton": (Kanton, "createKanton"),
+ "Land": (Land, "createLand"),
+ }
+
+ # Validate table name
+ if table not in table_mapping:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Invalid table name '{table}'. Available tables: {', '.join(table_mapping.keys())}"
+ )
+
+ # Get interface
+ realEstateInterface = getRealEstateInterface(currentUser)
+ model_class, method_name = table_mapping[table]
+ create_method = getattr(realEstateInterface, method_name)
+
+ # Ensure mandateId is set (will be set by interface if missing)
+ if "mandateId" not in data:
+ data["mandateId"] = currentUser.mandateId
+
+ # Create model instance from data
+ try:
+ model_instance = model_class(**data)
+ except Exception as e:
+ logger.error(f"Error creating {table} model instance: {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Invalid data for {table}: {str(e)}"
+ )
+
+ # Create record
+ try:
+ created_record = create_method(model_instance)
+
+ # Convert to dictionary for response
+ if hasattr(created_record, 'model_dump'):
+ return created_record.model_dump()
+ else:
+ return created_record
+
+ except Exception as e:
+ logger.error(f"Error creating {table} record: {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error creating {table} record: {str(e)}"
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error creating record in table '{table}': {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error creating record: {str(e)}"
+ )
+