integrated PEK data modell and according routes to get data
This commit is contained in:
parent
b603399690
commit
a20c44aadf
57 changed files with 8804 additions and 0 deletions
3
app.py
3
app.py
|
|
@ -406,6 +406,9 @@ app.include_router(workflowRouter)
|
||||||
from modules.routes.routeChatPlayground import router as chatPlaygroundRouter
|
from modules.routes.routeChatPlayground import router as chatPlaygroundRouter
|
||||||
app.include_router(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
|
from modules.routes.routeSecurityLocal import router as localRouter
|
||||||
app.include_router(localRouter)
|
app.include_router(localRouter)
|
||||||
|
|
||||||
|
|
|
||||||
418
docs/PEK_datamodel_desc.md
Normal file
418
docs/PEK_datamodel_desc.md
Normal file
|
|
@ -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<br/>Schweiz]
|
||||||
|
Kanton[Kanton<br/>z.B. Zürich]
|
||||||
|
Gemeinde[Gemeinde<br/>z.B. Zürich Stadt]
|
||||||
|
Land --> Kanton
|
||||||
|
Kanton --> Gemeinde
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Geo[Geografische Daten]
|
||||||
|
GeoPolylinie[GeoPolylinie<br/>Linie/Polygon]
|
||||||
|
GeoPunkt[GeoPunkt<br/>Koordinaten]
|
||||||
|
GeoPolylinie --> GeoPunkt
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Core[Kern-Business-Logik]
|
||||||
|
Projekt[Projekt<br/>Bauprojekt]
|
||||||
|
Parzelle[Parzelle<br/>Grundstück mit<br/>Bauparametern]
|
||||||
|
Gemeinde --> Parzelle
|
||||||
|
Projekt --> Parzelle
|
||||||
|
Projekt --> GeoPolylinie
|
||||||
|
Parzelle --> GeoPolylinie
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Support[Unterstützende Daten]
|
||||||
|
Dokument[Dokument<br/>Dateien & URLs]
|
||||||
|
Kontext[Kontext<br/>Zusatzinfos]
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
169
docs/real-estate-feature-integration-guide/01-overview.md
Normal file
169
docs/real-estate-feature-integration-guide/01-overview.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
976
docs/real-estate-feature-integration-guide/02-datamodels.md
Normal file
976
docs/real-estate-feature-integration-guide/02-datamodels.md
Normal file
|
|
@ -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<br/>Schweiz]
|
||||||
|
Kanton[Kanton<br/>z.B. Zürich]
|
||||||
|
Gemeinde[Gemeinde<br/>z.B. Zürich Stadt]
|
||||||
|
Land --> Kanton
|
||||||
|
Kanton --> Gemeinde
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Geo[Geografische Daten]
|
||||||
|
GeoPolylinie[GeoPolylinie<br/>Linie/Polygon]
|
||||||
|
GeoPunkt[GeoPunkt<br/>Koordinaten]
|
||||||
|
GeoPolylinie --> GeoPunkt
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Core[Kern-Business-Logik]
|
||||||
|
Projekt[Projekt<br/>Bauprojekt]
|
||||||
|
Parzelle[Parzelle<br/>Grundstück mit<br/>Bauparametern]
|
||||||
|
Gemeinde --> Parzelle
|
||||||
|
Projekt --> Parzelle
|
||||||
|
Projekt --> GeoPolylinie
|
||||||
|
Parzelle --> GeoPolylinie
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Support[Unterstützende Daten]
|
||||||
|
Dokument[Dokument<br/>Dateien & URLs]
|
||||||
|
Kontext[Kontext<br/>Zusatzinfos]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Übersichtstabelle aller Entitäten
|
||||||
|
|
||||||
|
| Objekt | Typ | Beschreibung | Hauptfelder |
|
||||||
|
|--------|-----|--------------|-------------|
|
||||||
|
| **Projekt** | Kernentität | Bauprojekt mit Status und Perimeter | id, label, statusProzess, perimeter, baulinie, parzellen |
|
||||||
|
| **Parzelle** | Hauptentität | Grundstück mit Bauparametern | id, label, plz, bauzone, AZ, BZ, perimeter, baulinie, laermschutzzone, hochwasserschutzzone, grundwasserschutzzone |
|
||||||
|
| **Dokument** | Unterstützend | Dateien und URLs mit Versionierung | id, label, dokumentTyp, quelle, mimeType, kategorienTags |
|
||||||
|
| **Kontext** | Unterstützend | Flexible Zusatzinformationen | id, thema, inhalt |
|
||||||
|
| **GeoPolylinie** | Hilfsobjekt | Geometrische Linie/Polygon | id, closed, punkte |
|
||||||
|
| **Land** | Admin | Nationale Ebene | id, label, abk |
|
||||||
|
| **Kanton** | Admin | Kantonale Ebene mit Baurecht | id, label, id_land, abk |
|
||||||
|
| **Gemeinde** | Admin | Gemeinde-Ebene mit BZO | id, label, id_kanton, plz |
|
||||||
|
| **GeoPunkt** | Hilfsobjekt | 3D-Koordinate | koordinatensystem, x, y, z, referenz |
|
||||||
|
| **GeoTag** | Enum | Geopunkt-Kategorien | K1, K2, K3, Geometer |
|
||||||
|
| **JaNein** | Enum | Drei-wertiger Status | "", "Ja", "Nein" |
|
||||||
|
| **StatusProzess** | Enum | Projektstatus | 7 Werte (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv) |
|
||||||
|
| **DokumentTyp** | Enum | Dokumenttyp | 6 Werte (kantonBaureglementAktuell, kantonBaureglementRevision, etc.) |
|
||||||
|
|
||||||
|
### Beispiel: Vollständige Pydantic-Modelle für Real Estate-Entitäten
|
||||||
|
|
||||||
|
Hier ist ein Beispiel, wie die Pydantic-Modelle für die Real Estate-Entitäten aussehen sollten:
|
||||||
|
|
||||||
|
```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)
|
||||||
|
|
||||||
845
docs/real-estate-feature-integration-guide/03-interfaces.md
Normal file
845
docs/real-estate-feature-integration-guide/03-interfaces.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
780
docs/real-estate-feature-integration-guide/04-feature-logic.md
Normal file
780
docs/real-estate-feature-integration-guide/04-feature-logic.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
332
docs/real-estate-feature-integration-guide/05-routes.md
Normal file
332
docs/real-estate-feature-integration-guide/05-routes.md
Normal file
|
|
@ -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 <token>
|
||||||
|
|
||||||
|
{
|
||||||
|
"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 <token>
|
||||||
|
|
||||||
|
{
|
||||||
|
"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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
50
docs/real-estate-feature-integration-guide/07-environment.md
Normal file
50
docs/real-estate-feature-integration-guide/07-environment.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
43
docs/real-estate-feature-integration-guide/08-lifecycle.md
Normal file
43
docs/real-estate-feature-integration-guide/08-lifecycle.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
247
docs/real-estate-feature-integration-guide/09-database-schema.md
Normal file
247
docs/real-estate-feature-integration-guide/09-database-schema.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
90
docs/real-estate-feature-integration-guide/10-security.md
Normal file
90
docs/real-estate-feature-integration-guide/10-security.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
51
docs/real-estate-feature-integration-guide/11-testing.md
Normal file
51
docs/real-estate-feature-integration-guide/11-testing.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
136
docs/real-estate-feature-integration-guide/13-summary.md
Normal file
136
docs/real-estate-feature-integration-guide/13-summary.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
42
docs/real-estate-feature-integration-guide/README.md
Normal file
42
docs/real-estate-feature-integration-guide/README.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,6 +29,13 @@ DB_MANAGEMENT_USER=poweron_dev
|
||||||
DB_MANAGEMENT_PASSWORD_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEUldqSTVpUnFqdGhITDYzT3RScGlMYVdTMmZhOXdudDRCc3dhdllOd3l6MS1vWHY2MjVsTUF1Sk9saEJOSk9ONUlBZjQwb2c2T1gtWWJhcXFzVVVXd01xc0U0b0lJX0JyVDRxaDhNS01JcWs9
|
DB_MANAGEMENT_PASSWORD_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEUldqSTVpUnFqdGhITDYzT3RScGlMYVdTMmZhOXdudDRCc3dhdllOd3l6MS1vWHY2MjVsTUF1Sk9saEJOSk9ONUlBZjQwb2c2T1gtWWJhcXFzVVVXd01xc0U0b0lJX0JyVDRxaDhNS01JcWs9
|
||||||
DB_MANAGEMENT_PORT=5432
|
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
|
# Security Configuration
|
||||||
APP_JWT_KEY_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpERjlrSktmZHVuQnJ1VVJDdndLaUcxZGJsT2ZlUFRlcFdOZ001RnlzM2FhLWhRV2tjWWFhaWQwQ3hkcUFvbThMcndxSjFpYTdfRV9OZGhTcksxbXFTZWg5MDZvOHpCVXBHcDJYaHlJM0tyNWRZckZsVHpQcmxTZHJoZUs1M3lfU2ljRnJaTmNSQ0w0X085OXI0QW80M2xfQnJqZmZ6VEh3TUltX0xzeE42SGtZPQ==
|
APP_JWT_KEY_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpERjlrSktmZHVuQnJ1VVJDdndLaUcxZGJsT2ZlUFRlcFdOZ001RnlzM2FhLWhRV2tjWWFhaWQwQ3hkcUFvbThMcndxSjFpYTdfRV9OZGhTcksxbXFTZWg5MDZvOHpCVXBHcDJYaHlJM0tyNWRZckZsVHpQcmxTZHJoZUs1M3lfU2ljRnJaTmNSQ0w0X085OXI0QW80M2xfQnJqZmZ6VEh3TUltX0xzeE42SGtZPQ==
|
||||||
APP_TOKEN_EXPIRY=300
|
APP_TOKEN_EXPIRY=300
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"intent": "CREATE",
|
||||||
|
"entity": "Projekt",
|
||||||
|
"parameters": {
|
||||||
|
"label": "Hauptstrasse 42"
|
||||||
|
},
|
||||||
|
"confidence": 0.95
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"intent": "CREATE",
|
||||||
|
"entity": "Parzelle",
|
||||||
|
"parameters": {
|
||||||
|
"kontextGemeinde": "Zürich",
|
||||||
|
"strasseNr": "Ueberlandstrasse 11",
|
||||||
|
"plz": "8050"
|
||||||
|
},
|
||||||
|
"confidence": 0.95
|
||||||
|
}
|
||||||
|
```
|
||||||
667
modules/datamodels/datamodelRealEstate.py
Normal file
667
modules/datamodels/datamodelRealEstate.py
Normal file
|
|
@ -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"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
4
modules/features/realEstate/__init__.py
Normal file
4
modules/features/realEstate/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""
|
||||||
|
Real Estate feature module.
|
||||||
|
"""
|
||||||
|
|
||||||
769
modules/features/realEstate/mainRealEstate.py
Normal file
769
modules/features/realEstate/mainRealEstate.py
Normal file
|
|
@ -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
|
||||||
|
|
||||||
87
modules/interfaces/interfaceDbRealEstateAccess.py
Normal file
87
modules/interfaces/interfaceDbRealEstateAccess.py
Normal file
|
|
@ -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
|
||||||
|
|
||||||
713
modules/interfaces/interfaceDbRealEstateObjects.py
Normal file
713
modules/interfaces/interfaceDbRealEstateObjects.py
Normal file
|
|
@ -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]
|
||||||
|
|
||||||
637
modules/routes/routeRealEstate.py
Normal file
637
modules/routes/routeRealEstate.py
Normal file
|
|
@ -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)}"
|
||||||
|
)
|
||||||
|
|
||||||
Loading…
Reference in a new issue