unified failsafe neutralization architecture

This commit is contained in:
ValueOn AG 2026-03-29 21:55:05 +02:00
parent 7ec8abe314
commit 70649218f1
3 changed files with 488 additions and 129 deletions

View file

@ -0,0 +1,200 @@
# Neutralisierung
**Stand:** 2026-03-29
---
## 1. Grundidee
Neutralisierung passiert dort, wo **Content entsteht oder ins System einfliesst** — nicht als nachgelagerter Filter vor dem Modell-Call. Wenn Content einmal neutralisiert ist (z. B. im RAG), bleibt er neutralisiert. Kein doppeltes Verarbeiten.
---
## 2. Wer fordert Neutralisierung?
Drei Quellen, von breit nach spezifisch:
| Quelle | Flag im Code | Bedeutung |
|--------|-------------|-----------|
| **Feature-Instanz** | `DataNeutraliserConfig.enabled` (hat `featureInstanceId` + `mandateId`) | Alle Daten in dieser Feature-Instanz werden neutralisiert. Betrifft jeden Content-Einstieg innerhalb dieser Instanz. |
| **Chat-Workflow / Session** | `ServiceCenterContext.requireNeutralization` (gesetzt z. B. vom AI-Workspace oder Automation) | Dieser Workflow/Turn: jeder Content, der hier verarbeitet wird, wird neutralisiert. |
| **Dokument oder Quelle** | `FileItem.neutralize`, `DataSource.neutralize`, `FeatureDataSource.neutralize` | Dieses konkrete Objekt: Content daraus wird neutralisiert, egal ob die Feature-Instanz oder der Workflow es sonst fordern würden. |
**Auswertung:** Irgendeine Quelle sagt `True` → neutralisieren. Keine Quelle kann eine andere aufheben. Es gibt kein `False`-Override, das ein `True` von woanders aushebelt.
---
## 3. Wo passiert die Neutralisierung?
Am **Punkt der Content-Einspeisung** — dort wo Rohdaten zu verarbeitbarem Content werden:
| Content-Einstieg | Was passiert | Wer prüft das Flag |
|------------------|-------------|---------------------|
| **Datei-Indexierung** (RAG) | Text-Chunks werden über `processText` neutralisiert, bevor sie ins Embedding gehen. Medien: nur internes Modell oder nicht indexieren. Ergebnis: RAG enthält nur neutralisierten Content. | `mainServiceKnowledge.indexFile` liest `FileItem.neutralize` |
| **Content-Extraktion** (Dokumente für KI-Verarbeitung) | Extrahierter Text/Tabellen werden neutralisiert. PDF, DOCX, XLSX, PPTX über `processFile` (Extract → neutralisieren → zurückschreiben). | Caller kennt die Quelle und deren Flag |
| **User-Prompt** (Chat-Eingabe) | Prompt-Text wird durch `processText` neutralisiert, wenn Feature-Instanz oder Workflow es fordern. | `mainServiceAi` prüft Context-Flag / Config |
| **DataSource-Download** | Flag wird auf erstellte `FileItem`s vererbt → bei Extraktion/Indexierung greift das File-Flag automatisch. | `mainServiceAgent._resolveDataSource` / `_downloadFromDataSource` |
| **FeatureDataSource-Abfrage** | Wenn eine `FeatureDataSource.neutralize=True` hat → Content aus diesem Sub-Agent-Call wird neutralisiert. | `mainServiceAgent._queryFeatureInstance` setzt `requireNeutralization=True` |
| **Workflow-Aktion `neutralizeData`** | Expliziter Neutralisierungs-Schritt auf bereits extrahierten ContentParts. | Workflow-Config + `NeutralizationConfig.enabled` |
**Was NICHT nochmal neutralisiert werden muss:**
- RAG-Chunks, die bereits neutralisiert indexiert sind — die sind schon sauber.
- Web-Suchergebnisse — enthalten keine Mandantendaten, brauchen keine Neutralisierung.
**Flag-Änderung:** Ändert sich `FileItem.neutralize` (Toggle) → Index löschen → Re-Indexierung triggern → neuer Index ist im richtigen Zustand.
---
## 4. Engine: `NeutralizationService`
Eine Engine, drei Einstiege, aufgerufen an den Content-Einstiegspunkten aus Abschnitt 3.
| API | Eingabe | Status |
|-----|---------|--------|
| `processText(text)` | Rohtext (Prompt, Message, Text-ContentPart, Text-Chunk) | **Ist** |
| `processFile(fileId)` | Datei aus DB — wird nach MIME geroutet | **Ist** |
| `processBinaryBytes` / `Async` | Bytes + Dateiname + MIME | **Ist** |
| **`processImage(imageBytes, fileName)`** | Bild-Datei oder Bild-ContentPart (image/png, image/jpeg, …) | **Soll — fehlt heute** |
**Ist-Zustand Bilder:** `_processBinaryFile` überspringt Parts mit `typeGroup == 'image'` (unverändert durchgereicht). Eigenständige Bild-Dateien (`image/*` MIME) werden in `_isBinaryMimeType` als Skip behandelt. **Es gibt keinen Bild-Neutralisierungs-Pfad.**
**Soll:** `processImage` als eigener Einstieg. Ruft internes Vision-Modell (`NEUTRALIZATION_IMAGE` → Private-LLM) auf, um sensible Inhalte in Bildern zu erkennen/entfernen. Kein externer Provider. Modell nicht verfügbar → Bild blockieren (nicht unbehandelt weiterreichen).
**Ablauf nach MIME:**
| MIME | Pfad |
|------|------|
| PDF, DOCX, XLSX, PPTX | `runExtraction` → Text/Table-Parts durch `_neutralizeText`, Bild-Parts durch `processImage` → PDF in-place oder Office-Renderer |
| Text, JSON, CSV, XML | `_neutralizeText` direkt (TextProcessor, ListProcessor, BinaryProcessor je nach Inhalt) |
| Bilder (image/*) | **`processImage`** → internes Vision-Modell (`NEUTRALIZATION_IMAGE`). Nicht verfügbar → blockieren. |
| Video, Audio | Nur internes Modell oder blockieren (analog Bilder, aber niedrigere Priorität) |
| Anderes Binary | Skip (nicht unterstützt) |
**Mapping:** Platzhalter `[typ.uuid]` → Attribute in DB. `resolveText(text)` für Rückübersetzung.
---
## 5. Modellauswahl
| Operation Type | Modelle | Zweck |
|----------------|---------|-------|
| `NEUTRALIZATION_TEXT` | `poweron-text-general` (Rating 9) | Falls Engine ein LLM für Text braucht |
| `NEUTRALIZATION_IMAGE` | `poweron-vision-general` (9), `poweron-vision-deep` (9) | Medien bei Neutralisierungspflicht: nur intern |
Registriert in `aicorePluginPrivateLlm.py`. Externe Provider haben keine `NEUTRALIZATION_*`-Ratings → werden nie für Neutralisierung gewählt.
---
## 6. Fail-safe
| Situation | Verhalten |
|-----------|-----------|
| Neutralisierung gefordert, Engine nicht verfügbar | Content **nicht** weiterverarbeiten (blockieren) |
| Neutralisierung eines Teils schlägt fehl | Teil entfernen, nicht im Rohzustand weiterleiten |
| Kein internes Modell für Medien verfügbar | Medien-Part entfernen |
---
## 7. Code-Karte
| Bereich | Pfad |
|---------|------|
| Engine | `features/neutralization/serviceNeutralization/mainServiceNeutralization.py` + `subProcess*.py` |
| Config + Attribute DB | `features/neutralization/datamodelFeatureNeutralizer.py`, `interfaceFeatureNeutralizer.py` |
| Index (Data-at-rest) | `serviceCenter/services/serviceKnowledge/mainServiceKnowledge.py` |
| Prompt-Neutralisierung | `serviceCenter/services/serviceAi/mainServiceAi.py` (`_shouldNeutralize`, `_neutralizeRequest`) |
| Agent / DataSource / Feature | `serviceCenter/services/serviceAgent/mainServiceAgent.py` |
| Workflow-Aktion | `workflows/methods/methodContext/actions/neutralizeData.py` |
| Modelle / Enums | `datamodels/datamodelAi.py`, `aicore/aicorePluginPrivateLlm.py` |
| Flags | `datamodelFiles.py` (`FileItem.neutralize`), `datamodelDataSource.py`, `datamodelFeatureDataSource.py` |
| Context | `serviceCenter/context.py` (`ServiceCenterContext.requireNeutralization`) |
---
## 8. Umsetzungsplan
### Schritt 1: `processImage` in Engine bauen
**Datei:** `mainServiceNeutralization.py`
**Was:** Neue Methode `processImage(imageBytes: bytes, fileName: str, mimeType: str) -> Dict` und `processImageAsync`.
**Wie:** Internes Vision-Modell aufrufen (`NEUTRALIZATION_IMAGE` via Model Selector / `aiObjects`). Prompt: sensible Inhalte im Bild erkennen und beschreiben, damit Caller entscheiden kann ob Bild blockiert wird. Kein internes Modell verfügbar → `{'status': 'blocked'}` zurückgeben.
**Abhängigkeit:** `OperationTypeEnum.NEUTRALIZATION_IMAGE` + Modell-Ratings in `aicorePluginPrivateLlm.py`**bereits erledigt**.
### Schritt 2: `_shouldNeutralize` vereinfachen
**Datei:** `mainServiceAi.py` (Zeile ~560)
**Ist:** Prüft `request.requireNeutralization``context.requireNeutralization``NeutralizationConfig.enabled` (Mandant-Level Config). `request.requireNeutralization=False` bricht sofort ab.
**Soll:** Drei Quellen gemäss Abschnitt 2, kein `False`-Override:
1. Feature-Instanz: `NeutralizationConfig.enabled` (hat `featureInstanceId`)
2. Workflow/Session: `context.requireNeutralization`
3. Request: `request.requireNeutralization`
Irgendein `True` → neutralisieren. `False` in einer Quelle hebt `True` in einer anderen **nicht** auf.
**Änderung:** `if request.requireNeutralization is False: return False` **entfernen**. Stattdessen OR-Verknüpfung aller drei Quellen.
### Schritt 3: `_neutralizeRequest` auf Prompt/Messages beschränken
**Datei:** `mainServiceAi.py` (Zeile ~592)
**Ist:** Neutralisiert `prompt`, `context`, `messages` (String-Content).
**Soll:** Zusätzlich Text in `contentParts` und Text-Teile in multimodalen Messages (content-Liste mit `type==text`). Bild-Teile in Messages: bei Neutralisierungspflicht über `processImage` prüfen oder entfernen.
**Wichtig:** Hier geht es nur um Prompt-/Sessiondaten. File-Content aus RAG ist bereits neutralisiert (Schritt 5), muss hier nicht nochmal durch.
### Schritt 4: `indexFile` — Bild-Chunks behandeln
**Datei:** `mainServiceKnowledge.py` (Zeile ~146 ff.)
**Ist:** Nur `textObjects` (contentType == "text") werden bei `_shouldNeutralize` durch `processText` geschickt. Bild-Chunks (`contentType == "image"`) werden unverändert indexiert.
**Soll:** Bild-Chunks bei `_shouldNeutralize` über `processImage` (Schritt 1) prüfen. Ergebnis `blocked` → Bild-Chunk nicht indexieren. Ergebnis OK → Bild-Chunk indexieren (internes Modell hat es gesehen, kein externer Provider nötig).
**Abhängigkeit:** Schritt 1.
### Schritt 5: Agent-Tool `readFile` — Content bei Extraktion neutralisieren
**Datei:** `mainServiceAgent.py`, Tool `_readFile` (Zeile ~540 ff.)
**Ist:** Liest aus Knowledge Store (bereits indexiert) oder extrahiert on-demand. Kein Neutralisierungs-Check.
**Soll:**
- **Knowledge Store Pfad (Zeile ~548):** RAG-Chunks sind bereits neutralisiert wenn `FileItem.neutralize=True` war (Schritt 4) → nichts zu tun.
- **On-Demand Extraktion (Zeile ~630 ff.):** Nach `runExtraction` und vor Rückgabe der Text-Objekte: `FileItem.neutralize` laden. Wenn `True` → Text-Objekte durch `processText`, Bild-Objekte durch `processImage`.
### Schritt 6: Agent-Tool `summarizeContent` — File-Flag prüfen
**Datei:** `mainServiceAgent.py`, Tool `_summarizeContent` (Zeile ~1921 ff.)
**Ist:** Liest Content-Objects aus Knowledge Store, baut `combinedText` und ruft `callAi` auf. Kein Neutralisierungs-Check.
**Soll:** Content aus Knowledge Store ist bereits neutralisiert (Schritt 4) → nichts zu tun, **sofern** File indexiert war. Wenn nicht indexiert: File-Flag prüfen, Text vor `callAi` durch `processText`.
**Hinweis:** Durch Schritt 4 ist der Regelfall abgedeckt.
### Schritt 7: Agent-Tool `describeImage` — File-Flag prüfen
**Datei:** `mainServiceAgent.py`, Tool `_describeImage` (Zeile ~2015 ff.)
**Ist:** Lädt Bild-Chunk oder Rohdaten, sendet per `callAi` mit `IMAGE_ANALYSE` an **beliebigen** Provider (inkl. extern). Kein Neutralisierungs-Check.
**Soll:** `FileItem.neutralize` laden (fileId ist bekannt). Wenn `True`:
- `operationType` auf `NEUTRALIZATION_IMAGE` statt `IMAGE_ANALYSE` → Model Selector wählt nur internes Modell.
- Kein internes Modell → Tool gibt Fehler zurück statt Bild an externen Provider zu senden.
### Schritt 8: `_processBinaryFile` — Bild-Parts nicht mehr überspringen
**Datei:** `mainServiceNeutralization.py` (Zeile ~298)
**Ist:** `if type_group in ('binary', 'image'): neutralized_parts.append(part); continue` — Bild-Parts werden übersprungen.
**Soll:** `binary` weiterhin skip. `image` → durch `processImage` (Schritt 1). Ergebnis `blocked` → Part entfernen. Ergebnis OK → Part behalten.
**Abhängigkeit:** Schritt 1.
### Schritt 9: Workflow-Aktion `neutralizeData` — Bild-Parts
**Datei:** `neutralizeData.py` (Zeile ~130 ff.)
**Ist:** Iteriert über ContentParts, neutralisiert nur Parts mit `part.data` (Text). `typeGroup != text/table` → unverändert durchgereicht.
**Soll:** Bild-Parts durch `processImage`. Ergebnis `blocked` → Part entfernen.
**Abhängigkeit:** Schritt 1.
### Schritt 10: Tests
| Test | Was |
|------|-----|
| T1 | `FileItem.neutralize=True``indexFile` → Text-Chunks neutralisiert, Bild-Chunks geprüft/blockiert |
| T2 | `readFile` on-demand auf File mit `neutralize=True` → Text neutralisiert |
| T3 | `describeImage` auf File mit `neutralize=True` → nur internes Modell |
| T4 | Feature-Instanz Config `enabled=True` → Prompt in `callAi` neutralisiert |
| T5 | Workflow `requireNeutralization=True` → Prompt neutralisiert |
| T6 | `processFile` auf PDF mit Bildern → Text neutralisiert, Bild-Parts durch `processImage` |
| T7 | Flag-Toggle → Index gelöscht → Re-Index im richtigen Zustand |
### Schritt 11: Logging
**Alle Stellen (Schritte 29):** Bei Neutralisierung loggen: welche Quelle (Feature-Instanz / Workflow / File-Flag), welche `fileId` falls vorhanden, Ergebnis (OK / Part entfernt / blockiert). **Kein Klartext** in Logs.

View file

@ -1,129 +0,0 @@
Kontext: Wir arbeiten an einer Multi-Tenant-Applikation mit Feature-Store-Modell. Ich möchte UI und Gateway austesten und offene Themen weiterführen. Hier ist der vollständige architektonische Kontext:
---
## Architektur-Überblick
- **Backend (Gateway):** `@poweron/gateway` — FastAPI (Python), PostgreSQL
- **Frontend:** `@poweron/frontend_nyla` — React/TypeScript (Vite)
- **Konzeptdokument:** `@poweron/wiki/concepts/Multi-Mandate-Onboarding-und-Store-Konzept.md`
- **Umsetzungsbericht (103 Punkte OK):** `@poweron/wiki/concepts/Multi-Mandate-Umsetzungsbericht.md`
---
## RBAC-Rollenmodell (KRITISCH — nie verwechseln!)
Es gibt ZWEI vollständig getrennte Rollensysteme. Siehe auch: `@poweron/.cursor/rules/rbac-role-separation.mdc`
### 1. Mandantenrollen (Mandate Roles)
- Labels: `admin`, `user`, `viewer` (generisch)
- Gespeichert mit: `featureCode=NULL`, `featureInstanceId=NULL`, `mandateId=<id>`
- Templates: `isSystemRole=True`, `mandateId=NULL`, `featureCode=NULL`
- Verknüpfung: `UserMandateRole` Junction Table
- Logik: Zuordnung User → Mandant erfolgt via `createUserMandate()` in `interfaceDbApp.py`, welche IMMER mindestens eine Mandate-Rolle verlangt (ValueError wenn leer)
### 2. Feature-Instanz-Rollen (Feature Instance Roles)
- Labels: `workspace-admin`, `workspace-user`, `workspace-viewer`, `automation-admin`, etc. (Feature-Prefix!)
- Gespeichert mit: `featureCode=<code>`, `featureInstanceId=<id>`, `mandateId=<id>`
- Templates: `isSystemRole=False`, `mandateId=NULL`, `featureInstanceId=NULL`, `featureCode=<code>` (globale Vorlagen)
- Verknüpfung: `FeatureAccessRole` Junction Table
- Logik: Zuordnung User → Feature-Instanz erfolgt via `createFeatureAccess()` in `interfaceDbApp.py`, welche IMMER mindestens eine Instanz-Rolle verlangt (ValueError wenn leer)
- Jedes Feature-Modul definiert seine `TEMPLATE_ROLES` in `mainXxx.py`
### Strikte Regeln:
1. NIE eine Mandantenrolle (`admin`) einer `FeatureAccessRole` zuweisen
2. NIE eine Feature-Rolle (`workspace-admin`) einer `UserMandateRole` zuweisen
3. Admin-Lookup für Feature-Instanzen: `roleLabel.endswith("-admin")`, NICHT `"admin" in roleLabel`
4. `_copyTemplateRoles` sucht: `featureCode=X, mandateId=NULL, featureInstanceId=NULL` — nie `isSystemRole=True`
5. Alle Pfade, die `FeatureAccess` erstellen, MÜSSEN mindestens eine instanz-scoped roleId mitgeben
### Datenmodelle:
- `Role`: `@poweron/gateway/modules/datamodels/datamodelRbac.py` — Felder: id, roleLabel, description, mandateId, featureInstanceId, featureCode, isSystemRole
- `UserMandate`: `@poweron/gateway/modules/datamodels/datamodelMembership.py` — User-Mitgliedschaft in Mandant
- `FeatureAccess`: `@poweron/gateway/modules/datamodels/datamodelMembership.py` — User-Zugriff auf Feature-Instanz
- `FeatureInstance`: `@poweron/gateway/modules/datamodels/datamodelFeatures.py` — Instanz eines Features in einem Mandanten
- `Mandate`: `@poweron/gateway/modules/datamodels/datamodelUam.py` — Mandantenmodell mit isSystem (Root-Schutz), deletedAt. `mandateType` wurde entfernt (keine Geschäftslogik).
---
## Feature-Store-Konzept ("Own Instance Pattern")
- **UI:** `@poweron/frontend_nyla/src/pages/Store.tsx` — Pro Mandant ein "Aktivieren für [Mandant]" Button
- **Backend:** `@poweron/gateway/modules/routes/routeStore.py``POST /api/store/activate`
- **Flow:** User wählt Feature → wählt Mandant → Backend erstellt `FeatureInstance` mit `copyTemplateRoles=True` → findet Admin-Rolle (`endswith("-admin")`) → weist User via `createFeatureAccess(userId, instanceId, roleIds=[adminRoleId])` zu
- **Auto-Provisioning:** `GET /api/store/mandates` erstellt automatisch "Home {username}"-Mandate, wenn User keine Admin-Mandate hat
- **Home-Mandant:** Jeder User hat ab erstem Login ein Home-Mandat ("Home {username}"). Wird in Login, Register, OAuth-Onboarding und Store einheitlich sichergestellt.
- **Template-Rollen-Kopie:** `@poweron/gateway/modules/interfaces/interfaceFeatures.py``_copyTemplateRoles()` und `syncRolesFromTemplate()`
- **Orphan Control:** Letzer FeatureAccess entfernt → FeatureInstance wird gelöscht
---
## Schlüssel-Backend-Dateien
| Datei | Zweck |
|-------|-------|
| `@poweron/gateway/modules/interfaces/interfaceDbApp.py` | Kern-DB-Interface: createUserMandate, createFeatureAccess, _provisionMandateForUser; Domain-Models nutzen `PowerOnModel` / `sys*`-Metadaten |
| `@poweron/gateway/modules/interfaces/interfaceFeatures.py` | Feature-Instanz-Verwaltung: _copyTemplateRoles, syncRolesFromTemplate |
| `@poweron/gateway/modules/routes/routeStore.py` | Store-API: activate, mandates-list |
| `@poweron/gateway/modules/routes/routeAdminFeatures.py` | Admin-API: add_user_to_feature_instance (Rollen-Validierung: nur Instanz-Rollen!), rename instance |
| `@poweron/gateway/modules/routes/routeSystem.py` | `/api/navigation` — liefert isAdmin pro FeatureInstance |
| `@poweron/gateway/modules/routes/routeSecurityLocal.py` | Login, Register, _provisionMandateForUser |
| `@poweron/gateway/modules/shared/attributeUtils.py` | Generiert Attribute aus Pydantic-Models für FormGenerator |
| `@poweron/gateway/modules/connectors/connectorDbPostgre.py` | DB-Connector: `sysCreatedAt` / `sysCreatedBy` / `sysModifiedAt` / `sysModifiedBy`; Migration alter `_createdAt`-Spalten via `migrateLegacyUnderscoreSysColumns` (Bootstrap) |
| `@poweron/gateway/modules/migration/migrateRootUsers.py` | Root→Personal-Mandate-Migration |
---
## Schlüssel-Frontend-Dateien
| Datei | Zweck |
|-------|-------|
| `@poweron/frontend_nyla/src/pages/Store.tsx` | Feature-Store mit Mandate-Kontext |
| `@poweron/frontend_nyla/src/components/UnifiedDataBar/UnifiedDataBar.tsx` | UDB: Chats, Files, Sources Tabs |
| `@poweron/frontend_nyla/src/components/UnifiedDataBar/ChatsTab.tsx` | Chats: Volltext-Suche, Feature-Code-Gruppierung, Flat-Mode nach lastMessageAt |
| `@poweron/frontend_nyla/src/components/UnifiedDataBar/FilesTab.tsx` | Files: Scope/Neutralize Icons |
| `@poweron/frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx` | Sources: Scope/Neutralize auf Active Sources |
| `@poweron/frontend_nyla/src/components/Navigation/MandateNavigation.tsx` | Nav-Tree mit Rename-Icon für Instanz-Admins |
| `@poweron/frontend_nyla/src/components/Navigation/TreeNavigation/TreeNavigation.tsx` | Generischer Tree mit actions-Slot |
| `@poweron/frontend_nyla/src/components/OnboardingAssistant.tsx` | Globaler Onboarding-Assistent (Dashboard, dismissible, reaktivierbar via UserSection) |
| `@poweron/frontend_nyla/src/components/Navigation/UserSection.tsx` | User-Kontextmenü: Guthaben, Einstellungen, Onboarding |
| `@poweron/frontend_nyla/src/pages/Settings.tsx` | User-Settings: profile, appearance, voice, neutralization, privacy |
| `@poweron/frontend_nyla/src/components/FormGenerator/` | Dynamische Formulare aus Backend-Attribut-Definitionen |
| `@poweron/frontend_nyla/src/hooks/useNavigation.ts` | Navigation-Hook mit FeatureInstance.isAdmin |
| `@poweron/frontend_nyla/src/pages/views/workspace/WorkspacePage.tsx` | Workspace-Feature-Seite mit Chat-Drag&Drop + RAG-Resolve |
| `@poweron/frontend_nyla/src/pages/views/commcoach/CommcoachDossierView.tsx` | CommCoach mit UDB (`hideTabs: chats`), gleicher `instanceId`/`mandateId`-Kontext wie Workspace |
| `@poweron/gateway/modules/datamodels/datamodelBase.py` | `PowerOnModel`: `sysCreatedAt`, `sysCreatedBy`, `sysModifiedAt`, `sysModifiedBy` (camelCase, UI-readonly vorbereitet) |
---
## System-Felder (DB-Metadaten)
- **Spalten / Modelle:** `sysCreatedAt`, `sysCreatedBy`, `sysModifiedAt`, `sysModifiedBy` — gepflegt durch den Connector (`connectorDbPostgre.py`); viele Domain-Models erben von `PowerOnModel` in `datamodelBase.py`.
- **Legacy:** Frühere `_createdAt`-Spalten werden bei Bootstrap durch `migrateLegacyUnderscoreSysColumns` / `migrateLegacyUnderscoreSysColumnsAllPoweronDatabases` in die `sys*`-Felder übernommen.
- **UI:** FormGenerator unterstützt `readonly`-Attribute; wo Metadaten in Listen/Forms explizit gezeigt werden sollen, Felder als readonly markieren (schrittweise je Screen).
---
## Produkt-Themen (Stand 2026-03-28)
1. **CommCoach UDB + Voice:** UDB in `CommcoachDossierView`. Voice-Controller bezieht STT-Sprache dynamisch aus zentralen Preferences.
2. **Neutralisierung End-to-End:** `requireNeutralization` vom UI-Toggle bis `AiCallRequest` durchgereicht via ServiceCenterContext. Mandanten-RAG in CommCoach via `searchSessionsByTopicRag`.
3. **Settings (5 Tabs):** Profil, Darstellung, Stimme, Datenneutralisierung, Datenschutz.
4. **Billing: nur PREPAY_MANDATE:** `PREPAY_USER` komplett entfernt. `budgetAiCHF` in Subscriptions (Trial: 5 CHF, Standard: 10 CHF/Monat). Auto-Recharge-Felder in BillingSettings.
5. **mandateType entfernt:** Enum, Feld, Validator in 13+ Dateien gelöscht. `isSystem` als einziger Root-Schutz.
6. **Home-Mandant:** "Home {username}" bei erstem Login (Register/OAuth/Invitation) via `_ensureHomeMandate()`.
7. **UDB ChatsTab:** Server-seitige Volltext-Suche (`searchWorkflowsByContent`), Flat-Mode nach `lastMessageAt`, zweistufige Baumansicht (Feature-Code-Sektionen > Feature-Instanzen).
8. **Chat Drag & Drop + RAG:** Chat-Items aus ChatsTab auf Workspace droppbar. Backend `/resolve-rag` liefert Zusammenfassung.
9. **Datenvolumen:** `assertCapacity("dataVolumeMB")` prüft RAG-Index-Grösse (`FileContentIndex.totalSize`). API `/api/subscription/data-volume/{mandateId}` liefert `ragIndexMB`, `filesMB`, `percentUsed`, `warning` (>=80%).
10. **Mandate-Cascade:** `deleteMandate(force=True)` löscht alle abhängigen Tabellen: ContentChunk, FileContentIndex, DataNeutralizerAttributes, DataSource, FeatureDataSource, FileItem, ChatMessage/Log/Workflow, FeatureAccessRole, FeatureAccess, FeatureInstance, UserMandateRole, UserMandate, Stripe-Subscriptions, BillingTransaction/Account/Settings, AccessRule, Role, Mandate. Frontend: Namensbestätigungs-Dialog.
---
## Coding-Konventionen
- Alle internen Funktionen beginnen mit `_` Prefix (z.B. `_copyTemplateRoles`)
- camelCase für alle Variablen und Funktionsnamen (kein snake_case)
- Keine unnötigen Fallbacks — Fehler müssen propagiert werden
- Keine Deprecations — alte Logik löschen statt deprecaten
- Keine Backwards-Compatibility-Workarounds
- Pydantic-Models sind die einzige Quelle für UI-Feld-Definitionen (via `attributeUtils.py`)

View file

@ -0,0 +1,288 @@
# PowerOn Applikations-Kontext (Stand 2026-03-29)
Multi-Tenant SaaS-Applikation mit Feature-Store-Modell, AI-Agent-Workspace und mandantenweiter Datenneutralisierung.
---
## 1. Technologie-Stack
| Layer | Technologie | Pfad |
|-------|------------|------|
| Backend (Gateway) | FastAPI (Python), PostgreSQL | `@poweron/gateway` |
| Frontend | React/TypeScript (Vite) | `@poweron/frontend_nyla` |
| AI Core | Multi-Provider (Anthropic, OpenAI, Mistral, Perplexity, Tavily, PrivateLLM) | `@poweron/gateway/modules/aicore` |
| DB-Connector | PostgreSQL mit pgvector (Embeddings) | `@poweron/gateway/modules/connectors/connectorDbPostgre.py` |
---
## 2. Gateway-Architektur (Backend)
### Modulstruktur (`gateway/modules/`)
| Modul | Zweck |
|-------|-------|
| `aicore/` | Model-Registry, Model-Selector, Provider-Plugins (Anthropic, OpenAI, Mistral, Perplexity, Tavily, PrivateLLM) |
| `auth/` | Authentifizierung |
| `connectors/` | DB-Connector (PostgreSQL), externe Konnektoren |
| `datamodels/` | Pydantic-Datenmodelle (30+ Dateien: Ai, Billing, Chat, Content, Files, Knowledge, Rbac, Subscription, Workflow...) |
| `features/` | Feature-Module (autonome Domänen): workspace, automation, automation2, chatbot, commcoach, neutralization, realEstate, trustee, teamsbot |
| `interfaces/` | DB-Interfaces (App, Billing, Chat, Knowledge, Management, Subscription), AI-Objects, RBAC, Features, Messaging |
| `migration/` | Daten-Migrationen |
| `routes/` | REST-API-Routen (30+ Dateien: Admin, Billing, DataFiles, DataSources, Security, Store, System...) |
| `security/` | Sicherheits-Middleware |
| `serviceCenter/` | Zentrale Service-Orchestrierung (siehe Abschnitt 3) |
| `serviceHub/` | Service-Registry und Dependency Injection |
| `shared/` | Gemeinsame Utilities (attributeUtils, etc.) |
| `system/` | System-Konfiguration |
| `workflows/` | Workflow-Engine mit Methoden und Aktionen (siehe Abschnitt 4) |
### Interfaces (DB-Schicht)
Die Interfaces kapseln alle Datenbankzugriffe:
| Interface | Verantwortlich für |
|-----------|--------------------|
| `interfaceDbApp.py` | User, Mandate, FeatureAccess, UserConnections, Preferences |
| `interfaceDbBilling.py` | BillingAccount, BillingTransaction, Subscriptions |
| `interfaceDbChat.py` | ChatWorkflow, ChatMessage, ChatDocument |
| `interfaceDbKnowledge.py` | FileContentIndex, ContentChunk, RoundMemory (RAG/Knowledge Store) |
| `interfaceDbManagement.py` | FileItem, Folder, Prompt, DataSource (mandantenweite Stammdaten) |
| `interfaceDbSubscription.py` | Subscription-Verwaltung |
| `interfaceAiObjects.py` | AI-Call-Abstraction (Text, Embedding, Vision, Streaming) |
| `interfaceFeatures.py` | Feature-Instanz-Lifecycle, Template-Rollen-Kopie |
| `interfaceRbac.py` | RBAC-Regelauswertung |
---
## 3. ServiceCenter (Kern-Orchestrierung)
Das ServiceCenter ist die zentrale Schicht, die alle Services verbindet. Es wird pro Request/Session erstellt und propagiert einen **ServiceCenterContext** (`context.py`).
### Services (`serviceCenter/services/`)
| Service | Pfad | Zweck |
|---------|------|-------|
| `serviceAgent` | `mainServiceAgent.py` (3400+ Zeilen) | AI-Agent mit 30+ Tools (readFile, writeFile, searchInFileContent, browseContainer, summarizeContent, readContentObjects, webSearch, sendMail, etc.) |
| `serviceAi` | `mainServiceAi.py` (1700+ Zeilen) | AI-Call-Gateway: Neutralisierung, Billing-Preflight, Provider-Selection, Streaming, Extraction, Generation |
| `serviceKnowledge` | `mainServiceKnowledge.py` | Knowledge Store: Indexierung, Semantic Search, RAG-Kontext-Aufbau (`buildAgentContext`) |
| `serviceBilling` | | Billing-Checks, Transaktionen |
| `serviceChat` | | Chat-Persistence |
| `serviceExtraction` | | Dokument-Extraktion (PDF, DOCX, XLSX...) |
| `serviceGeneration` | | Dokument-Generierung |
| `serviceMessaging` | | E-Mail, Notifications |
| `serviceSubscription` | | Subscription-Verwaltung |
| `serviceWeb` | | Web-Scraping |
### ServiceCenterContext (`context.py`)
Propagiert pro Request:
- `userId`, `mandateId`, `featureInstanceId`
- `requireNeutralization: Optional[bool]` — Chat-Level Neutralisierungsflag
- Provider-Restrictions, Billing-Context
### Agent-Tools (in `mainServiceAgent.py`)
Der Agent hat 30+ registrierte Tools. Tools liefern direkt Rohdaten — Neutralisierung erfolgt zentral im AI-Service (`mainServiceAi.py`) bevor Daten an ein AI-Modell gesendet werden.
Datenschutz-relevante DataSource-Tools:
- `_resolveDataSource()` — liest `neutralize`-Flag der DataSource als 4. Tuple-Element
- `_downloadFromDataSource()` — vererbt `neutralize`-Flag auf erstellte FileItems
- `_queryFeatureInstance()` — setzt `requireNeutralization=True` auf Sub-Agent AI-Calls wenn FeatureDataSource `neutralize=True`
---
## 4. Workflow-Engine
### WorkflowManager (`workflows/workflowManager.py`, 1400+ Zeilen)
Zentrale Workflow-Steuerung für Automation und dynamische Workflows.
### Methoden (`workflows/methods/`)
| Methode | Aktionen |
|---------|----------|
| `methodContext` | `extractContent`, `neutralizeData`, `saveContent`, `transformContent` |
| `methodAi` | AI-Analyse, Summarization, Prompt-Processing |
| `methodChatbot` | Chatbot-spezifische Aktionen |
| `methodSharepoint` | SharePoint-Integration |
| `methodOutlook` | Outlook/E-Mail-Integration |
| `methodJira` | Jira-Integration |
| `methodTrustee` | Trustee-Feature-Aktionen |
---
## 5. Neutralisierungs-System (KRITISCH)
**Architektur, Prozess und Code-Karte (ein Dokument):** [wiki/compliance/Neutralisierung.md](../compliance/Neutralisierung.md)
### Prinzip: Zentrales Gate — ALLE Daten, die an ein AI-Modell gehen, werden AUSSCHLIESSLICH in `mainServiceAi.py` neutralisiert.
### Architektur: Einzige zentrale Stelle
Neutralisierung findet nur an **einer einzigen Stelle** statt: `_shouldNeutralize()` und `_neutralizeRequest()` in `mainServiceAi.py`. Kein anderer Service, kein Tool und kein Workflow prüft oder neutralisiert eigenständig. Failsafe: Wenn Neutralisierung erforderlich ist (`requireNeutralization=True`) und fehlschlägt, wird der AI-Call blockiert (`RuntimeError`).
### Neutralisierungs-Trigger (Prioritätsreihenfolge)
1. **Per-Request:** `AiCallRequest.requireNeutralization = True/False` — expliziter Override
2. **Chat-Level:** `ServiceCenterContext.requireNeutralization = True` — Session-Level-Flag
3. **Mandate-Config:** `NeutralizationConfig.enabled` — mandantenweite Konfiguration
### Was neutralisiert wird (in `_neutralizeRequest`)
- `request.prompt` — Benutzer-Prompt
- `request.context` — RAG-Kontext (externe Dokumente)
- `request.messages` — Chat-Nachrichten (OpenAI-Style)
### Schlüssel-Dateien
| Datei | Rolle |
|-------|-------|
| `features/neutralization/serviceNeutralization/mainServiceNeutralization.py` | `processText(text)``{neutralized_text, mappings}`, `resolveText(text)` → Rehydrierung |
| `routes/routeDataFiles.py` | `PATCH /{fileId}/neutralize` — Toggle löscht Index synchron, re-indexiert im Background |
| `serviceAi/mainServiceAi.py` | **Zentrales Gate:** `_shouldNeutralize()` prüft Request → Context → Config; `_neutralizeRequest()` neutralisiert prompt, context, messages. Hard-Mode bei `requireNeutralization=True` |
| `serviceKnowledge/mainServiceKnowledge.py` | `indexFile()` neutralisiert bei `FileItem.neutralize=True` (Data-at-rest-Schutz). RAG-Chunks werden neutral gespeichert |
| `serviceAgent/mainServiceAgent.py` | `_resolveDataSource()` liest `neutralize`-Flag der DataSource. `_downloadFromDataSource()` vererbt Flag auf erstellte FileItems. `_queryFeatureInstance()` setzt `requireNeutralization=True` wenn FeatureDataSource neutralize-Flag gesetzt |
| `workflows/methods/methodContext/actions/neutralizeData.py` | Explizite Neutralisierungs-Aktion in Workflows (prüft nur Config, nicht Context-Flag) |
### Failsafe-Kette
1. **Toggle ON:** Index + Chunks werden synchron gelöscht → kein Leak bei Background-Re-Index-Fehler
2. **Indexierung:** `indexFile()` neutralisiert Chunks bei `FileItem.neutralize=True` → Data-at-rest ist sicher
3. **DataSource-Download:** neutralize-Flag wird auf erstellte FileItems vererbt
4. **FeatureDataSource:** `_queryFeatureInstance()` setzt `requireNeutralization=True` auf Sub-Agent AI-Calls
5. **AI-Calls (Zentrales Gate):** Hard-Mode (`requireNeutralization=True`) → Prompt-/Context-Fehler = `RuntimeError` (AI-Call blockiert), Message-Fehler = Message entfernt
6. **`_rehydrateResponse()`** bleibt als Utility-Methode verfügbar, wird aber nicht mehr automatisch aufgerufen
### NeutralizationPanel (Frontend)
`frontend_nyla/src/pages/views/workspace/NeutralizationPanel.tsx` — zeigt alle Files mit `neutralize`-Flag, deren Status und Platzhalter-Mappings. API: `GET /api/workspace/{instanceId}/files``{files: [...]}`.
---
## 6. Frontend-Architektur
### Struktur (`frontend_nyla/src/`)
| Ordner | Inhalt |
|--------|--------|
| `pages/` | Seiten: admin/, basedata/, billing/, settings/, views/ (workspace, commcoach, chatbot, trustee, automation) |
| `components/` | Wiederverwendbar: FormGenerator, FolderTree, Navigation, UnifiedDataBar, Automation2FlowEditor, OnboardingAssistant |
| `hooks/` | useApi, useFiles, useNavigation, useConfirm, usePrompt, useResizablePanels, etc. |
| `contexts/` | FileContext, PekContext, ToastContext, WorkflowSelectionContext |
| `api/` | API-Client (api.ts) und Feature-spezifische API-Module |
| `core/` | PageManager |
| `layouts/` | Layout-Komponenten |
| `locales/` | i18n |
| `types/` | TypeScript-Typen |
| `utils/` | Utility-Funktionen |
### Wichtige UI-Komponenten
| Komponente | Zweck |
|------------|-------|
| `UnifiedDataBar (UDB)` | Multi-Tab-Panel: Chats, Files, Sources — wird in Workspace und CommCoach genutzt |
| `FormGenerator` | Dynamische Formulare/Tabellen aus Backend-Attribut-Definitionen |
| `FolderTree` | Rekursiver Ordnerbaum mit Drag&Drop, Multi-Selection, Inline-Editing |
| `MandateNavigation` | Feature-Baum-Navigation mit Mandanten-Kontext |
| `Automation2FlowEditor` | n8n-style Flow-Builder für Automation2 |
| `WorkspacePage` | AI-Workspace mit Chat, UDB, Agent-Streaming |
### UI-Regeln
- **Keine Browser-Dialoge** (alert/confirm/prompt) — stattdessen `useConfirm()` und `usePrompt()` Hooks
- Alle internen Funktionen mit `_` Prefix
- camelCase für Variablen und Funktionen
---
## 7. RBAC-System (Zwei getrennte Rollensysteme!)
### Mandantenrollen
- Labels: `admin`, `user`, `viewer`
- Scope: `mandateId`, kein `featureCode`/`featureInstanceId`
- Junction: `UserMandateRole`
### Feature-Instanz-Rollen
- Labels: `workspace-admin`, `workspace-user`, `workspace-viewer`, etc.
- Scope: `mandateId` + `featureCode` + `featureInstanceId`
- Junction: `FeatureAccessRole`
**Strikte Regel:** NIE eine Mandantenrolle einer FeatureAccessRole zuweisen oder umgekehrt.
---
## 8. Billing & Subscriptions
- **Modell:** PREPAY_MANDATE (kein PREPAY_USER)
- **Billing-Check:** Vor jedem AI-Call via `_preflightBillingCheck` und `_checkBillingBeforeAiCall`
- **Datenvolumen:** `assertCapacity("dataVolumeMB")` prüft RAG-Index-Grösse
- **Trial:** 5 CHF, Standard: 10 CHF/Monat
---
## 9. AI-Core (Provider-Abstraction)
### Model-Registry (`aicoreModelRegistry.py`)
Registriert alle Provider-Plugins und deren Modelle dynamisch.
### Model-Selector (`aicoreModelSelector.py`)
Wählt optimal basierend auf: Operation Type, Prompt-Grösse, Provider-Restrictions, Score-Ranking.
### Provider-Plugins
- `aicorePluginAnthropic.py` — Claude Sonnet/Haiku/Opus
- `aicorePluginOpenai.py` — GPT-4o, Embeddings (text-embedding-3-small/large), DALL-E
- `aicorePluginMistral.py` — Mistral Large/Small, Mistral Embed
- `aicorePluginPerplexity.py` — Sonar/Sonar-Pro
- `aicorePluginTavily.py` — Web-Search
- `aicorePluginPrivateLlm.py` — Private LLM
- `aicorePluginInternal.py` — Interne Extraktoren/Generatoren/Renderer
### Operation Types
`dataExtract`, `dataAnalyse`, `agent`, `embedding`, `imageGeneration`, `vision`, `webSearch`
---
## 10. Knowledge Store (RAG)
### Datenmodelle (`datamodelKnowledge.py`)
- `FileContentIndex` — Pro File: Struktur, Metadaten, `isNeutralized`, `neutralizationStatus`, `totalObjects`, `totalSize`
- `ContentChunk` — Semantische Chunks mit Embedding-Vektor, `fileId`, `data`, `metadata`
- `RoundMemory` — Kontext aus früheren Runden (file_ref, etc.)
### Indexierung (`serviceKnowledge.indexFile()`)
1. Extraktion via `serviceExtraction`
2. Optional: Neutralisierung via `serviceNeutralization.processText()`
3. Chunking + Embedding
4. Speicherung in `FileContentIndex` + `ContentChunk`
### RAG-Kontext (`serviceKnowledge.buildAgentContext()`)
1. Semantic Search via pgvector
2. Context-Budget: Priorisierte Layer (File-Refs, Instance Docs, Mandate Docs)
3. Neutralisierung erfolgt zentral im AI-Service wenn der RAG-Kontext als `request.context` an ein AI-Modell geht
---
## 11. Feature-Module
| Feature | Beschreibung |
|---------|-------------|
| `workspace` | AI-Agent-Workspace mit Chat, Tools, RAG, Streaming |
| `automation` | Workflow-Automatisierung (v1) |
| `automation2` | Flow-Editor n8n-Style (v2) |
| `chatbot` | Chatbot-Feature |
| `commcoach` | Kommunikations-Coach mit Voice, Dossier, UDB |
| `neutralization` | Datenneutralisierungs-Service und Config |
| `trustee` | Trustee/Treuhand-Feature (Buchhaltung, Positionen) |
| `realEstate` | Immobilien-Feature |
| `teamsbot` | MS Teams Bot |
---
## 12. Coding-Konventionen
- Alle internen Funktionen beginnen mit `_` Prefix
- camelCase für Variablen und Funktionsnamen (kein snake_case)
- Keine Browser-Dialoge — `useConfirm()` / `usePrompt()` Hooks
- Fehler propagieren — keine stillen Fallbacks bei kritischen Pfaden
- Pydantic-Models als einzige Quelle für UI-Feld-Definitionen
- `PowerOnModel` als Basis mit `sysCreatedAt`, `sysCreatedBy`, `sysModifiedAt`, `sysModifiedBy`