feat: real estate integration
This commit is contained in:
parent
dc93890f46
commit
7e54f4be88
21 changed files with 633 additions and 3046 deletions
|
|
@ -1,976 +0,0 @@
|
|||
# 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)
|
||||
|
||||
|
|
@ -39,9 +39,7 @@ Da das Feature **stateless** arbeitet, benötigen wir nur **ein Interface** für
|
|||
- `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
|
||||
**Hinweis:** Das Haupt-Interface enthält auch `executeQuery()` für direkte SQL-Queries (stateless)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -55,8 +53,9 @@ Da das Feature **stateless** arbeitet, benötigen wir nur **ein Interface** für
|
|||
- Ein Interface für alle CRUD-Operationen
|
||||
- Weniger Komplexität, bessere Wartbarkeit
|
||||
|
||||
3. **Optionales Query-Interface**:
|
||||
- Nur für direkte SQL-Queries (stateless)
|
||||
3. **Query-Funktionalität**:
|
||||
- `executeQuery()` ist direkt im Haupt-Interface verfügbar
|
||||
- Für direkte SQL-Queries (stateless)
|
||||
- Keine Session-Management-Funktionen
|
||||
|
||||
---
|
||||
|
|
@ -92,27 +91,6 @@ Da das Feature **stateless** arbeitet, benötigen wir nur **ein Interface** für
|
|||
|
||||
---
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
|
|
@ -153,13 +131,9 @@ Da das Feature **stateless** arbeitet, benötigen wir nur **ein Interface** für
|
|||
│ └── getInterface() │
|
||||
│ └── nutzt RealEstateAccess │
|
||||
│ │
|
||||
│ QUERY-INTERFACE (OPTIONAL - nur für direkte SQL) │
|
||||
│ │
|
||||
│ interfaceDbRealEstateChatObjects.py │
|
||||
│ ├── RealEstateChatObjects │
|
||||
│ │ └── executeQuery() # Direkte SQL-Ausführung │
|
||||
│ └── getInterface() │
|
||||
│ └── Keine Access-Klasse (stateless) │
|
||||
│ HINWEIS: executeQuery() ist im Haupt-Interface verfügbar │
|
||||
│ (kein separates Chat-Interface notwendig) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
|
@ -177,7 +151,7 @@ Jedes Interface besteht aus **zwei Klassen**:
|
|||
- `uam()` - Filtert Daten basierend auf Benutzerprivilegien
|
||||
- `canModify()` - Prüft, ob Benutzer ändern darf
|
||||
|
||||
**Beispiel:** `RealEstateChatAccess`
|
||||
**Beispiel:** `RealEstateAccess`
|
||||
|
||||
### 2. `*Objects` Klasse (Haupt-Interface)
|
||||
|
||||
|
|
@ -185,13 +159,15 @@ Jedes Interface besteht aus **zwei Klassen**:
|
|||
|
||||
**Methoden:**
|
||||
- `create*()` - Erstellt neue Einträge
|
||||
- `get*()` - Lädt Einträge
|
||||
- `get*()` - Lädt einzelne Einträge nach ID
|
||||
- `get*()` (Plural) - Lädt Listen von Einträgen mit optionalen Filtern
|
||||
- `update*()` - Aktualisiert Einträge
|
||||
- `delete*()` - Löscht Einträge
|
||||
- `executeQuery()` - Führt direkte SQL-Queries aus (stateless)
|
||||
|
||||
**Nutzt:** `*Access` für Zugriffskontrolle
|
||||
|
||||
**Beispiel:** `RealEstateChatObjects`
|
||||
**Beispiel:** `RealEstateObjects`
|
||||
|
||||
**Warum getrennt?**
|
||||
- Separation of Concerns: Zugriffskontrolle ist separate Verantwortlichkeit
|
||||
|
|
@ -208,100 +184,14 @@ Das Real Estate CRUD-Interface besteht aus **zwei separaten Dateien**, genau wie
|
|||
|
||||
**Datei:** `modules/interfaces/interfaceDbRealEstateAccess.py`
|
||||
|
||||
```python
|
||||
"""
|
||||
Access control for Real Estate interface.
|
||||
Handles user access management and permission checks.
|
||||
"""
|
||||
**Enthält:**
|
||||
- `RealEstateAccess` Klasse
|
||||
- Methoden: `uam()`, `canModify()`
|
||||
|
||||
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
|
||||
```
|
||||
**Funktionalität:**
|
||||
- **`uam()`**: Filtert Datensätze basierend auf Benutzerprivilegien (SYSADMIN sieht alles, ADMIN sieht Mandat, User sieht nur eigene)
|
||||
- **`canModify()`**: Prüft, ob Benutzer Datensätze ändern/löschen darf
|
||||
- Fügt Zugriffskontroll-Attribute hinzu: `_hideView`, `_hideEdit`, `_hideDelete`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -309,267 +199,27 @@ class RealEstateAccess:
|
|||
|
||||
**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.).
|
||||
"""
|
||||
**Enthält:**
|
||||
- `RealEstateObjects` Klasse (Haupt-Interface)
|
||||
- `getInterface()` Factory-Funktion (Singleton-Pattern)
|
||||
|
||||
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
|
||||
**Datenbank-Konfiguration:**
|
||||
- Verwendet `DB_REALESTATE_*` Umgebungsvariablen (nicht `DB_APP_*`)
|
||||
- Variablen: `DB_REALESTATE_HOST`, `DB_REALESTATE_DATABASE`, `DB_REALESTATE_USER`, `DB_REALESTATE_PASSWORD_SECRET`, `DB_REALESTATE_PORT`
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
**CRUD-Methoden für alle Entitäten:**
|
||||
- **Projekt**: `createProjekt()`, `getProjekt()`, `getProjekte()`, `updateProjekt()`, `deleteProjekt()`
|
||||
- **Parzelle**: `createParzelle()`, `getParzelle()`, `getParzellen()`, `updateParzelle()`, `deleteParzelle()`
|
||||
- **Dokument**: `createDokument()`, `getDokument()`, `getDokumente()`, `updateDokument()`, `deleteDokument()`
|
||||
- **Gemeinde**: `createGemeinde()`, `getGemeinde()`, `getGemeinden()`, `updateGemeinde()`, `deleteGemeinde()`
|
||||
- **Kanton**: `createKanton()`, `getKanton()`, `getKantone()`, `updateKanton()`, `deleteKanton()`
|
||||
- **Land**: `createLand()`, `getLand()`, `getLaender()`, `updateLand()`, `deleteLand()`
|
||||
|
||||
# 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]
|
||||
```
|
||||
**Zusätzliche Funktionalität:**
|
||||
- **List-Methoden**: Alle Entitäten haben `get*()` (Plural) Methoden für Listen mit optionalen Filtern
|
||||
- **Location-Resolution**: `getParzellen()` löst automatisch Gemeinde-Namen zu IDs auf
|
||||
- **Query-Ausführung**: `executeQuery()` für direkte SQL-Queries (stateless)
|
||||
- **Supporting Tables**: Automatische Erstellung von Land, Kanton, Gemeinde, Dokument Tabellen bei Initialisierung
|
||||
|
||||
## Wichtige Punkte:
|
||||
|
||||
|
|
@ -578,241 +228,34 @@ def getInterface(currentUser: User) -> RealEstateObjects:
|
|||
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
|
||||
6. **List-Methoden**: Alle Entitäten haben `get*()` (Plural) Methoden für Listen mit optionalen Filtern
|
||||
7. **Location-Resolution**: Parzelle-Filter können Gemeinde-Namen enthalten, die automatisch zu IDs aufgelöst werden
|
||||
8. **Query-Ausführung**: `executeQuery()` ist direkt im Haupt-Interface verfügbar (kein separates Chat-Interface notwendig)
|
||||
9. **Datenbank-Initialisierung**: Unterstützende Tabellen (Land, Kanton, Gemeinde, Dokument) werden automatisch erstellt
|
||||
|
||||
---
|
||||
|
||||
## Schritt 2b: Query-Interface (OPTIONAL - nur für direkte SQL-Queries)
|
||||
## Query-Ausführung: executeQuery()
|
||||
|
||||
### Wann benötigt?
|
||||
Das Haupt-Interface `RealEstateObjects` enthält die Methode `executeQuery()` für direkte SQL-Queries. **Kein separates Chat-Interface ist notwendig.**
|
||||
|
||||
**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.
|
||||
### Verwendung
|
||||
|
||||
#### Szenario 1: Alles über CRUD-Interface (EMPFOHLEN)
|
||||
**Für CRUD-Operationen (EMPFOHLEN):**
|
||||
- Verwenden Sie die strukturierten CRUD-Methoden (`createProjekt()`, `getProjekte()`, etc.)
|
||||
- Vorteile: Validierung, Zugriffskontrolle, Typsicherheit, keine SQL-Injection-Risiken
|
||||
|
||||
**Strukturiert und sicher:**
|
||||
**Für komplexe SELECT-Queries (OPTIONAL):**
|
||||
- Verwenden Sie `executeQuery()` direkt im Haupt-Interface
|
||||
- **Warnung**: Nur für SELECT-Queries, immer Parameterisierung verwenden, Queries validieren
|
||||
|
||||
```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
|
||||
### Hinweise zur Query-Ausführung
|
||||
|
||||
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
|
||||
|
|
@ -830,12 +273,12 @@ results = chatInterface.executeQuery(
|
|||
- Nutzt `interfaceDbRealEstateAccess.py`
|
||||
- Haupt-Interface für alle CRUD-Operationen
|
||||
|
||||
### Optional (für direkte SQL-Queries):
|
||||
### Hinweis zu Query-Ausführung:
|
||||
|
||||
4. ⚠️ `modules/interfaces/interfaceDbRealEstateChatObjects.py`
|
||||
- Query-Interface für direkte SQL-Ausführung (RealEstateChatObjects)
|
||||
4. ✅ `executeQuery()` ist bereits im Haupt-Interface verfügbar
|
||||
- Kein separates Chat-Interface notwendig
|
||||
- Direkt in `RealEstateObjects` verfügbar
|
||||
- Stateless, keine Session-Management
|
||||
- Nur wenn Sie komplexe SELECT-Queries benötigen
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ 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"))`
|
||||
- Feature-Logik ruft auf → Interface-Methode `createProjekt()` mit extrahierten Parametern
|
||||
- Ergebnis wird direkt zurückgegeben (keine Session, keine History)
|
||||
|
||||
## AI-Integration: Services initialisieren
|
||||
|
|
@ -38,17 +38,9 @@ Um AI zu verwenden, müssen Sie die **Services** initialisieren. Services sind e
|
|||
|
||||
### 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.
|
||||
**Wichtig:**
|
||||
- Services werden normalerweise im Feature-Logik-Modul initialisiert und an Funktionen weitergegeben
|
||||
- Für Query-Ausführung wird `getRealEstateInterface()` verwendet, nicht `getChatInterface()`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -65,449 +57,6 @@ Die AI analysiert User-Input und identifiziert:
|
|||
|
||||
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
|
||||
|
|
@ -530,60 +79,37 @@ Die AI analysiert User-Input und gibt zurück:
|
|||
|
||||
### 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()`
|
||||
Basierend auf der Intent-Analyse werden folgende Operationen unterstützt:
|
||||
|
||||
### 5. Natural Language to SQL
|
||||
**Unterstützte Entities:**
|
||||
- Projekt, Parzelle, Gemeinde, Kanton, Land, Dokument
|
||||
|
||||
- AI übersetzt natürliche Sprache in SQL-Queries
|
||||
- Automatische Validierung und Sanitization empfohlen
|
||||
- MandateId-Filter wird automatisch hinzugefügt
|
||||
**CREATE** → `interface.createProjekt()`, `interface.createParzelle()`, `interface.createGemeinde()`, `interface.createKanton()`, `interface.createLand()`, `interface.createDokument()`
|
||||
|
||||
**READ** →
|
||||
- Einzelne Entität: `interface.getProjekt(id)`, `interface.getParzelle(id)`, etc.
|
||||
- Liste mit Filtern: `interface.getProjekte(recordFilter)`, `interface.getParzellen(recordFilter)`, etc.
|
||||
- **Wichtig:** READ-Operationen validieren Filter-Felder gegen das Datenmodell
|
||||
|
||||
**UPDATE** → `interface.updateProjekt(id, updateData)`, `interface.updateParzelle(id, updateData)`, etc.
|
||||
|
||||
**DELETE** → `interface.deleteProjekt(id)`, `interface.deleteParzelle(id)`, etc.
|
||||
|
||||
**QUERY** → `interface.executeQuery(queryText, parameters)` für direkte SQL-Ausführung
|
||||
|
||||
### 5. Field Validation
|
||||
|
||||
- **READ-Operationen** validieren Filter-Felder gegen das Datenmodell
|
||||
- Ungültige Felder werden ignoriert und geloggt
|
||||
- **Wichtig:** Location-Queries sollten Parzelle-Entity verwenden, nicht Projekt direkt
|
||||
|
||||
### 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?"`
|
||||
- JSON-Parsing mit Fallback (extrahiert JSON aus Markdown-Code-Blöcken)
|
||||
- Validierung der AI-Response-Struktur
|
||||
- Logging für Debugging mit `exc_info=True` für vollständige Stack-Traces
|
||||
- Entity-Validierung: Prüft ob Entity existiert vor Update/Delete
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -597,32 +123,26 @@ async def execute_query(
|
|||
Body: {"userInput": "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"}
|
||||
|
||||
2. Route ruft Feature-Logik auf
|
||||
→ processNaturalLanguageCommand(currentUser, userInput)
|
||||
# Keine Session-ID notwendig!
|
||||
→ processNaturalLanguageCommand() wird aufgerufen
|
||||
→ Keine Session-ID notwendig!
|
||||
|
||||
3. Feature-Logik initialisiert Services
|
||||
→ services = getServices(currentUser, workflow=None)
|
||||
→ aiService = services.ai
|
||||
→ Services werden für den aktuellen User initialisiert
|
||||
→ AI-Service wird aus Services abgerufen
|
||||
|
||||
4. AI analysiert User-Input
|
||||
→ analyzeUserIntent(aiService, userInput)
|
||||
→ AI gibt zurück:
|
||||
{
|
||||
"intent": "CREATE",
|
||||
"entity": "Projekt",
|
||||
"parameters": {"label": "Hauptstrasse 42"},
|
||||
"confidence": 0.95
|
||||
}
|
||||
→ analyzeUserIntent() wird aufgerufen
|
||||
→ AI gibt zurück: Intent "CREATE", Entity "Projekt", Parameter {"label": "Hauptstrasse 42"}
|
||||
|
||||
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)
|
||||
→ executeIntentBasedOperation() wird mit Intent und Parametern aufgerufen
|
||||
→ Real Estate Interface wird initialisiert
|
||||
→ Projekt-Objekt wird aus Parametern erstellt
|
||||
→ createProjekt() wird aufgerufen
|
||||
|
||||
6. Interface speichert in Datenbank
|
||||
→ DatabaseConnector.recordCreate(Projekt, projekt.model_dump())
|
||||
→ PostgreSQL INSERT INTO Projekt ...
|
||||
→ Datenbank-Operation wird über Interface ausgeführt
|
||||
→ PostgreSQL INSERT wird durchgeführt
|
||||
|
||||
7. Ergebnis wird direkt zurückgegeben
|
||||
→ Route gibt HTTP Response zurück
|
||||
|
|
@ -638,15 +158,6 @@ async def execute_query(
|
|||
|
||||
**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
|
||||
|
|
@ -656,27 +167,13 @@ intentData = json.loads(response)
|
|||
|
||||
**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
|
||||
```
|
||||
**Hinweis:** Response ist direkt ein Text-String (kein JSON-Parsing nötig)
|
||||
|
||||
### `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
|
||||
)
|
||||
```
|
||||
**Hinweis:** Unterstützt optionales `outputFormat` Parameter für strukturierte Ausgaben (z.B. "json")
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -718,59 +215,18 @@ response = await aiService.callAiDocuments(
|
|||
|
||||
### 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}"
|
||||
...
|
||||
"""
|
||||
```
|
||||
Sie können das Datenbank-Schema in Prompts einbinden, um der AI besseren Kontext zu geben. Laden Sie Schema-Informationen dynamisch und fügen Sie diese in den Prompt ein.
|
||||
|
||||
### 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}"
|
||||
...
|
||||
"""
|
||||
```
|
||||
Falls Sie später Kontext zwischen Queries benötigen, können Sie optional eine Session verwenden. Für stateless Operationen ist dies normalerweise nicht notwendig. Falls gewünscht, können Sie vorherige Queries aus einer Session laden und als Kontext in den Prompt einbinden.
|
||||
|
||||
### 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(...)
|
||||
```
|
||||
1. **Intent-Analyse**: Zuerst den User-Intent analysieren
|
||||
2. **Parameter-Validierung**: Optional Parameter mit AI validieren
|
||||
3. **CRUD-Operation**: Die eigentliche Operation ausführen
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -10,136 +10,11 @@ Die Routen definieren die REST-API-Endpunkte für das Feature. Das Feature arbei
|
|||
|
||||
```
|
||||
/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)
|
||||
)
|
||||
├── POST /command → Natürliche Sprache → CRUD-Operation
|
||||
├── POST /query → Direkte SQL-Query
|
||||
├── GET /tables → Liste aller verfügbaren Tabellen
|
||||
├── GET /table/{table} → Daten aus einer Tabelle (mit optionaler Pagination)
|
||||
└── POST /table/{table} → Neuen Datensatz in einer Tabelle erstellen
|
||||
```
|
||||
|
||||
## Wichtige Punkte:
|
||||
|
|
@ -157,118 +32,74 @@ async def execute_query(
|
|||
- Nutzt AI für Intent-Analyse
|
||||
- Führt CRUD-Operationen aus
|
||||
- Gibt Ergebnis direkt zurück
|
||||
- **CSRF-Token erforderlich** (X-CSRF-Token Header)
|
||||
|
||||
**`POST /api/realestate/query`**
|
||||
- Führt direkte SQL-Queries aus
|
||||
- Request Body: `{"queryText": "...", "parameters": {...}}`
|
||||
- Keine Session notwendig
|
||||
- Gibt Query-Ergebnis direkt zurück
|
||||
- **CSRF-Token erforderlich** (X-CSRF-Token Header)
|
||||
|
||||
**`GET /api/realestate/tables`**
|
||||
- Gibt Liste aller verfügbaren Tabellen zurück
|
||||
- Enthält Tabellennamen, Beschreibungen und Model-Namen
|
||||
- **CSRF-Token erforderlich** (X-CSRF-Token Header)
|
||||
|
||||
**`GET /api/realestate/table/{table}`**
|
||||
- Gibt alle Daten aus einer spezifischen Tabelle zurück
|
||||
- Unterstützt optionale Pagination über Query-Parameter
|
||||
- Sortierung und Filterung möglich
|
||||
- Leere Tabellen geben ein leeres Modell-Instanz zurück (für Schema-Extraktion)
|
||||
- **CSRF-Token erforderlich** (X-CSRF-Token Header)
|
||||
|
||||
**`POST /api/realestate/table/{table}`**
|
||||
- Erstellt einen neuen Datensatz in einer spezifischen Tabelle
|
||||
- Request Body enthält die Datensatz-Daten
|
||||
- mandateId wird automatisch gesetzt falls nicht vorhanden
|
||||
- **CSRF-Token erforderlich** (X-CSRF-Token Header)
|
||||
|
||||
### 3. Sicherheit
|
||||
|
||||
- **Rate Limiting**: `@limiter.limit("120/minute")` für API-Schutz
|
||||
- **Authentication**: `Depends(getCurrentUser)` für alle Endpunkte
|
||||
- **Rate Limiting**: 120 Requests pro Minute für alle Endpunkte
|
||||
- **Authentication**: Alle Endpunkte erfordern authentifizierte User
|
||||
- **CSRF-Token-Validierung**: Alle Endpunkte validieren CSRF-Token im X-CSRF-Token Header
|
||||
- Token muss hexadezimaler String sein
|
||||
- Token-Länge: 16-64 Zeichen
|
||||
- Fehlende oder ungültige Token führen zu 403 Forbidden
|
||||
- **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)
|
||||
- Unterschiedliche Status-Codes:
|
||||
- **400 Bad Request**: Validierungsfehler (z.B. fehlende Parameter, ungültige Daten)
|
||||
- **403 Forbidden**: CSRF-Token fehlt oder ist ungültig
|
||||
- **404 Not Found**: Ressource nicht gefunden
|
||||
- **500 Internal Server Error**: Server-Fehler
|
||||
- Detaillierte Fehlermeldungen für Debugging
|
||||
- Logging mit vollständigen Stack-Traces (`exc_info=True`)
|
||||
|
||||
### 5. Response-Struktur
|
||||
|
||||
**Command-Endpunkt:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"intent": "CREATE",
|
||||
"entity": "Projekt",
|
||||
"result": {
|
||||
"operation": "CREATE",
|
||||
"entity": "Projekt",
|
||||
"result": {
|
||||
"id": "projekt_123",
|
||||
"label": "Hauptstrasse 42",
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- Erfolgreiche Response enthält: `success`, `intent`, `entity`, `result`
|
||||
- Result enthält die Operation-Details und das erstellte/aktualisierte/gelesene Objekt
|
||||
|
||||
**Query-Endpunkt:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"rows": [
|
||||
{"id": "...", "label": "...", ...}
|
||||
],
|
||||
"columns": ["id", "label", ...],
|
||||
"rowCount": 15,
|
||||
"executionTime": 0.123
|
||||
}
|
||||
```
|
||||
- Response enthält: `status`, `rows`, `columns`, `rowCount`, `executionTime`
|
||||
|
||||
---
|
||||
**Tables-Endpunkt:**
|
||||
- Response enthält: `tables` (Array mit Tabellen-Informationen), `count`
|
||||
|
||||
## Beispiel-Requests
|
||||
**Table GET-Endpunkt:**
|
||||
- Response ist eine `PaginatedResponse` mit `items` und `pagination` Metadata
|
||||
- Ohne Pagination: Alle Items werden zurückgegeben
|
||||
- Mit Pagination: Enthält `currentPage`, `pageSize`, `totalItems`, `totalPages`, `sort`, `filters`
|
||||
|
||||
### 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"}
|
||||
}
|
||||
```
|
||||
**Table POST-Endpunkt:**
|
||||
- Response ist das erstellte Objekt als Dictionary (model_dump())
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -279,6 +110,8 @@ POST /api/realestate/query
|
|||
```
|
||||
POST /api/realestate/command
|
||||
↓
|
||||
CSRF-Token-Validierung
|
||||
↓
|
||||
routeRealEstate.process_command()
|
||||
↓
|
||||
getCurrentUser() # Auth
|
||||
|
|
@ -297,25 +130,85 @@ return Dict mit Ergebnis
|
|||
```
|
||||
POST /api/realestate/query
|
||||
↓
|
||||
CSRF-Token-Validierung
|
||||
↓
|
||||
routeRealEstate.execute_query()
|
||||
↓
|
||||
getCurrentUser() # Auth
|
||||
↓
|
||||
Body-Parsing (queryText, parameters)
|
||||
↓
|
||||
executeDirectQuery(currentUser, queryText, parameters)
|
||||
↓
|
||||
mainRealEstate.executeDirectQuery()
|
||||
↓
|
||||
getChatInterface(currentUser)
|
||||
getRealEstateInterface(currentUser)
|
||||
↓
|
||||
RealEstateChatObjects.executeQuery(queryText)
|
||||
↓
|
||||
DatabaseConnector.executeQuery(sql)
|
||||
Interface.executeQuery(queryText, parameters)
|
||||
↓
|
||||
return Dict mit rows, columns, rowCount
|
||||
```
|
||||
|
||||
### Table-Endpunkte Flow
|
||||
|
||||
**GET /api/realestate/table/{table}:**
|
||||
```
|
||||
GET /api/realestate/table/{table}
|
||||
↓
|
||||
CSRF-Token-Validierung
|
||||
↓
|
||||
getCurrentUser() # Auth
|
||||
↓
|
||||
Tabellen-Name validieren
|
||||
↓
|
||||
getRealEstateInterface(currentUser)
|
||||
↓
|
||||
Interface.getProjekte() / getParzellen() / etc.
|
||||
↓
|
||||
Pagination anwenden (falls angegeben)
|
||||
↓
|
||||
return PaginatedResponse
|
||||
```
|
||||
|
||||
**POST /api/realestate/table/{table}:**
|
||||
```
|
||||
POST /api/realestate/table/{table}
|
||||
↓
|
||||
CSRF-Token-Validierung
|
||||
↓
|
||||
getCurrentUser() # Auth
|
||||
↓
|
||||
Tabellen-Name validieren
|
||||
↓
|
||||
Model-Instanz aus Body-Daten erstellen
|
||||
↓
|
||||
getRealEstateInterface(currentUser)
|
||||
↓
|
||||
Interface.createProjekt() / createParzelle() / etc.
|
||||
↓
|
||||
return erstelltes Objekt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verfügbare Tabellen
|
||||
|
||||
Die folgenden Tabellen sind verfügbar:
|
||||
- **Projekt**: Real estate projects
|
||||
- **Parzelle**: Plots/parcels
|
||||
- **Dokument**: Documents
|
||||
- **Gemeinde**: Municipalities
|
||||
- **Kanton**: Cantons
|
||||
- **Land**: Countries
|
||||
|
||||
## Pagination
|
||||
|
||||
Der `GET /api/realestate/table/{table}` Endpunkt unterstützt Pagination über einen Query-Parameter:
|
||||
|
||||
- **Parameter**: `pagination` (JSON-encoded string)
|
||||
- **Format**: `{"page": 1, "pageSize": 10, "sort": [{"field": "label", "direction": "asc"}], "filters": []}`
|
||||
- **Ohne Pagination**: Alle Datensätze werden zurückgegeben
|
||||
|
||||
## Vorteile des stateless Ansatzes
|
||||
|
||||
- **Einfachheit**: Kein Session-Management notwendig
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
# 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)
|
||||
|
||||
|
||||
|
||||
|
|
@ -48,3 +48,5 @@ def _initializeDatabase(self):
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
# 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)
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
# 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)
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
# 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)
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
# 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)
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# 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)
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
# 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)
|
||||
|
||||
|
||||
|
||||
|
|
@ -40,3 +40,6 @@ Die Architektur folgt dem Muster bestehender Features wie `chatPlayground`:
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
|
||||
Analyze the following user command and extract the intent, entity, and parameters.
|
||||
|
||||
User Command: "Wie viele Parzellen 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",
|
||||
"aggregation": "count"
|
||||
},
|
||||
"confidence": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
- The user is asking "How many plots are there in Zürich?" which is a READ/query operation
|
||||
- The entity is "Parzelle" (plot/parcel) since the user explicitly asks about "Parzellen"
|
||||
- The location "Zürich" maps to the `kontextGemeinde` field (municipality)
|
||||
- The phrase "Wie viele" (how many) indicates a count aggregation is needed
|
||||
- High confidence (0.95) because the intent and entity are clearly stated in German
|
||||
|
|
@ -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/municipality) is stored in **Parzelle** via the `kontextGemeinde` field, not directly in Projekt, we need to query Parzelle entities
|
||||
- "Zürich" maps to the `kontextGemeinde` parameter
|
||||
- The system will need to find all Parzellen in Zürich and then count the associated unique projects
|
||||
- Confidence is 0.9 because the intent is clear, though the user asks about "Projekte" while we're querying via "Parzelle" (which is the correct approach based on the data model)
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
|
||||
Analyze the following user command and extract the intent, entity, and parameters.
|
||||
|
||||
User Command: "Welche Gemeinden gibt es in der datenbank?"
|
||||
|
||||
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,10 @@
|
|||
```json
|
||||
{
|
||||
"intent": "READ",
|
||||
"entity": "Gemeinde",
|
||||
"parameters": {},
|
||||
"confidence": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
The user is asking "Which municipalities are there in the database?" in German. This is a straightforward READ query for all Gemeinde (municipality) entities without any filter criteria.
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
|
||||
Analyze the following user command and extract the intent, entity, and parameters.
|
||||
|
||||
User Command: "Erstell ein neues Projekt"
|
||||
|
||||
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,10 @@
|
|||
```json
|
||||
{
|
||||
"intent": "CREATE",
|
||||
"entity": "Projekt",
|
||||
"parameters": {},
|
||||
"confidence": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
The user command "Erstell ein neues Projekt" (Create a new project) clearly indicates a CREATE intent for a Projekt entity. No specific parameters like project name (label) or other fields are mentioned in this command, so the parameters object is empty. The confidence is high (0.95) because the intent and entity are unambiguous.
|
||||
Loading…
Reference in a new issue