From 7f5f31db30f31147ffe9f27b76ac96aaf83b27b3 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Mon, 16 Mar 2026 22:55:50 +0100 Subject: [PATCH] concepts filesystem and mobile mic --- concepts/Agent-Toolbox-Dateisystem-Konzept.md | 594 ++++++++++++++++++ ...coach-Voice-Recording-Streaming-Konzept.md | 334 ++++++++++ deployment/poweron_sec.kdbx | Bin 18222 -> 18878 bytes 3 files changed, 928 insertions(+) create mode 100644 concepts/Agent-Toolbox-Dateisystem-Konzept.md create mode 100644 concepts/Commcoach-Voice-Recording-Streaming-Konzept.md diff --git a/concepts/Agent-Toolbox-Dateisystem-Konzept.md b/concepts/Agent-Toolbox-Dateisystem-Konzept.md new file mode 100644 index 0000000..fdbd8e6 --- /dev/null +++ b/concepts/Agent-Toolbox-Dateisystem-Konzept.md @@ -0,0 +1,594 @@ +# Konzept: Agent Toolbox & Dateisystem-Erweiterung + +> Poweron Platform -- Erweiterung der Agent-Fähigkeiten und des Dateisystems +> Status: Konzept / Version 1.2 / März 2026 + +--- + +## 1. Ausgangslage (IST-Zustand) + +### 1.1 Datenmodelle + +Die Grundstruktur für ein hierarchisches Dateisystem existiert bereits, wird aber kaum genutzt. + +**FileItem** (`gateway/modules/datamodels/datamodelFiles.py`): + +| Feld | Typ | Beschreibung | +|------|-----|-------------| +| `id` | str | Primary Key (UUID) | +| `fileName` | str | Dateiname | +| `mimeType` | str | MIME-Typ | +| `fileSize` | int | Grösse in Bytes | +| `folderId` | Optional[str] | Verweis auf übergeordneten Folder | +| `featureInstanceId` | Optional[str] | Feature-Instanz-Zuordnung | +| `tags` | Optional[List[str]] | Tags | +| `status` | Optional[str] | Verarbeitungsstatus | +| `description` | Optional[str] | Beschreibung | + +**FileFolder** (`gateway/modules/datamodels/datamodelFileFolder.py`): + +| Feld | Typ | Beschreibung | +|------|-----|-------------| +| `id` | str | Primary Key (UUID) | +| `name` | str | Ordnername | +| `parentId` | Optional[str] | Übergeordneter Folder (None = Root) | +| `mandateId` | Optional[str] | Mandant | +| `featureInstanceId` | Optional[str] | Feature-Instanz-Zuordnung | + +### 1.2 Backend-Operationen + +``` +Vorhanden Fehlend +───────────────────── ────────────────────── +Files: Files: + createFile ✓ copyFile ✗ + deleteFile ✓ + updateFile ✓ (Metadaten) + createFileData ✓ (Update-Pfad: Logik aus Codeeditor applyEdit kopieren) + getAllFiles ✓ + +Folders: Folders: + createFolder ✓ deleteFolder ✗ + listFolders ✓ renameFolder ✗ + moveFolder ✗ + getFolder ✗ +``` + +### 1.3 Agent-Tools (27 registriert) + +Dateibezogene Tools im aktuellen Agent: + +| Tool | Funktion | Status | +|------|----------|--------| +| `readFile` | Datei lesen | Vorhanden | +| `listFiles` | Dateien auflisten | Vorhanden | +| `searchFiles` | Dateien suchen | Vorhanden | +| `writeFile` | Neue Datei erstellen | Vorhanden | +| `moveFile` | Datei verschieben (via `updateFile({folderId})`) | Vorhanden | +| `tagFile` | Tags setzen | Vorhanden | +| `createFolder` | Ordner erstellen | Vorhanden | +| `listFolders` | Ordner auflisten | Vorhanden | +| `deleteFile` | Datei löschen | **Fehlt** (Backend existiert) | +| `renameFile` | Datei umbenennen | **Fehlt** (Backend existiert) | +| `editFile` | Dateiinhalt ändern | **Fehlt** (Logik aus Codeeditor `applyEdit` extrahieren) | +| `copyFile` | Datei duplizieren | **Fehlt** (Backend muss gebaut werden) | + +### 1.4 Codeeditor-Feature (wird entfernt -- Logik extrahieren) + +Das bestehende **Codeeditor-Feature** (`gateway/modules/features/codeeditor/`) wird perspektivisch entfernt. Es enthält jedoch bewährte File-Edit-Logik, die vor dem Entfernen in den Workspace-Agent **kopiert** werden muss: + +**Zu extrahierende Backend-Logik:** + +| Datei | Was kopieren | +|-------|-------------| +| `routeFeatureCodeeditor.py` | `applyEdit()`-Logik: File-Write via `createFileData(fileId, contentBytes)`, MIME-Type-Erkennung | +| `datamodelCodeeditor.py` | `FileEditProposal`, `EditStatusEnum` (PENDING → ACCEPTED / REJECTED) | +| `responseParser.py` | `file_edit`-Block-Parsing (`fileName`, `oldContent`, `newContent`) | + +**Zu extrahierende Frontend-Logik:** + +| Datei | Was kopieren | +|-------|-------------| +| `DiffPreviewPanel.tsx` | Diff-View mit Accept/Reject-Buttons → als Shared-Komponente extrahieren | +| `useCodeEditor.ts` | `acceptEdit` / `rejectEdit` Handlers → in Workspace-Hook übernehmen | + +### 1.5 Frontend + +- **FileBrowser** (`frontend_nyla/src/pages/views/workspace/FileBrowser.tsx`): Gruppiert Dateien nach `featureInstanceId`, ignoriert `folderId` und Folder komplett. Kein Folder-Tree. +- **FilesPage** (`frontend_nyla/src/pages/basedata/FilesPage.tsx`): Flache Tabelle mit FormGeneratorTable. Keine Ordner-Navigation. +- **DataSourcePanel**: Externer Datenquellen-Tree existiert als Referenzimplementierung für Lazy-Loading-Tree-UI. + +--- + +## 2. Dateisystem-Erweiterung (Backend) + +### 2.1 Übersicht + +```mermaid +graph TB + subgraph fileSystem [Hierarchisches Dateisystem] + Root["(Global) Root"] + FolderA["Folder: Projekte"] + FolderB["Folder: Analysen"] + FolderC["Folder: Q1"] + FileA["File: Report.pdf"] + FileB["File: Daten.xlsx"] + FileC["File: Summary.md"] + Root --> FolderA + Root --> FolderB + FolderA --> FolderC + FolderA --> FileA + FolderC --> FileB + FolderB --> FileC + end + + subgraph agent [Agent Tools] + CRUD_Files["Files: create, read, edit, copy, delete, rename, move"] + CRUD_Folders["Folders: create, delete, rename, move, list"] + end + + agent -->|"operiert auf"| fileSystem +``` + +### 2.2 Neue/Erweiterte DB-Methoden + +Alle Methoden in `interfaceDbManagement.py` ergänzen: + +**Übergreifende Regeln für alle Folder-Operationen:** +- **Unique-Name-Constraint:** Innerhalb eines Folders darf ein Foldername nur einmal vorkommen (unique pro `parentId`) +- **Geschützter Name:** Der Name `"(Global)"` ist reserviert für den virtuellen Root und darf nicht für echte Folder verwendet werden +- Validation bei `createFolder`, `renameFolder`, `moveFolder` + +**deleteFolder(folderId, recursive=False)** +- Wenn `recursive=True`: Alle Dateien und Unterordner kaskadierend löschen +- Wenn `recursive=False` und Folder nicht leer: Fehler werfen +- RBAC-Check auf Folder-Ebene + +**renameFolder(folderId, newName)** +- Via `recordModify(FileFolder, folderId, {"name": newName})` +- **Unique-Name-Constraint:** Prüfen, dass im gleichen `parentId` kein anderer Folder mit `newName` existiert +- **Geschützter Name:** `"(Global)"` darf nicht als Foldername verwendet werden (reserviert für virtuellen Root) + +**moveFolder(folderId, targetParentId)** +- Validierung: Zirkelverweis verhindern (Folder darf nicht in eigenen Unterbaum verschoben werden) +- **Unique-Name-Constraint:** Prüfen, dass im Ziel-Folder kein Folder mit gleichem Namen existiert +- Via `recordModify(FileFolder, folderId, {"parentId": targetParentId})` + +**copyFile(sourceFileId, targetFolderId=None, newFileName=None)** +- FileItem duplizieren mit neuer ID +- **FileData vollständig duplizieren** (eigenständige Kopie, damit die Kopie unabhängig bearbeitet werden kann) +- Optional: neuer Name, anderer Ziel-Folder + +**updateFileData(fileId, data)** -- Logik aus Codeeditor `applyEdit` kopieren +- `createFileData(fileId, contentBytes)` zum Überschreiben bestehender Dateien +- Falls `createFileData` bei existierenden Daten überspringt: vorher `deleteFileData(fileId)`, dann `createFileData(fileId, newData)` +- FileItem-Metadaten aktualisieren (fileSize, fileHash via `updateFile`) +- Hinweis: Codeeditor wird später entfernt -- relevante Logik vorher in `interfaceDbManagement` oder Agent-Tool extrahieren + +### 2.3 Root-Folder "(Global)" + +Der Root-Folder ist **virtuell** und wird nicht als DB-Eintrag gespeichert: +- Dateien mit `folderId=None` gehören zum Root +- Ordner mit `parentId=None` sind Top-Level-Ordner unter Root +- Anzeige im UI als "(Global)" Label + +### 2.4 Migration: featureInstanceId entfernen + +Die Gruppierung nach `featureInstanceId` wird durch die Ordnerstruktur ersetzt: + +1. **Spalte beibehalten** aber nicht mehr für Gruppierung nutzen +2. `featureInstanceId` als informatives Metadatum ("Erstellt in: Workspace A") in einer zusätzlichen Tabellenspalte anzeigen +3. Bestehende Dateien: `folderId` bleibt `None` (Root), keine Migration nötig +4. Frontend-Gruppierung nach `featureInstanceId` entfernen, stattdessen Folder-Baum nutzen + +--- + +## 3. Neue Agent-Tools + +### 3.1 Phase 1 -- Quick Wins (Backend existiert) + +#### deleteFile + +| Eigenschaft | Wert | +|-------------|------| +| Backend | `interfaceDbManagement.deleteFile(fileId)` | +| Parameter | `fileId` (required) | +| readOnly | false | +| Hinweis | Knowledge-Store-Einträge ebenfalls bereinigen | + +#### renameFile + +| Eigenschaft | Wert | +|-------------|------| +| Backend | `interfaceDbManagement.updateFile(fileId, {"fileName": newName})` | +| Parameter | `fileId` (required), `newName` (required) | +| readOnly | false | + +#### readUrl + +| Eigenschaft | Wert | +|-------------|------| +| Backend | `serviceWeb._performWebCrawl(urls=[url])` | +| Parameter | `url` (required) | +| readOnly | true | +| Hinweis | Ergänzt `webSearch`: Während `webSearch` nach Informationen sucht, liest `readUrl` gezielt eine bekannte URL | + +#### translateText + +| Eigenschaft | Wert | +|-------------|------| +| Backend | `interfaceVoiceObjects.translateText(text, targetLanguage)` + Google Cloud Translation | +| Parameter | `text` (required), `targetLanguage` (required), `sourceLanguage` (optional, auto-detect) | +| readOnly | true | +| Hinweis | Effizienter als AI-basierte Übersetzung für grosse Textmengen | + +### 3.2 Phase 2 -- Dateisystem-Tools + +#### deleteFolder + +| Eigenschaft | Wert | +|-------------|------| +| Backend | Neu: `interfaceDbManagement.deleteFolder()` | +| Parameter | `folderId` (required), `recursive` (optional, default false) | +| readOnly | false | +| Sicherheit | Agent muss vor kaskadierendem Löschen bestätigen (Anzahl betroffener Dateien nennen) | + +#### renameFolder + +| Eigenschaft | Wert | +|-------------|------| +| Backend | Neu: `interfaceDbManagement.renameFolder()` | +| Parameter | `folderId` (required), `newName` (required) | +| readOnly | false | + +#### moveFolder + +| Eigenschaft | Wert | +|-------------|------| +| Backend | Neu: `interfaceDbManagement.moveFolder()` | +| Parameter | `folderId` (required), `targetParentId` (required, null für Root) | +| readOnly | false | +| Validierung | Zirkelverweis-Check: Ziel darf kein Unterordner der Quelle sein | + +#### copyFile + +| Eigenschaft | Wert | +|-------------|------| +| Backend | Neu: `interfaceDbManagement.copyFile()` | +| Parameter | `fileId` (required), `targetFolderId` (optional), `newFileName` (optional) | +| readOnly | false | +| Implementierung | Vollständige Duplikation: FileItem + FileData werden als eigenständige Kopie erstellt (editierbar) | + +#### editFile + +| Eigenschaft | Wert | +|-------------|------| +| Backend | `dbManagement.createFileData(fileId, contentBytes)` (Logik aus Codeeditor kopiert, siehe 5.2) | +| Parameter | `fileId` (required), `content` (required, Text-Inhalt) | +| readOnly | false | +| Einschränkung | Nur für Text-basierte Dateien (text/\*, application/json, etc.) | +| UX-Option | Optional: Approve/Reject-Mode (Agent schlägt Edit als Diff vor, User bestätigt) | + +### 3.3 Phase 3 -- Erweiterte Fähigkeiten + +#### speechToText + +| Eigenschaft | Wert | +|-------------|------| +| Backend | `connectorVoiceGoogle.speechToText()` | +| Parameter | `fileId` (required, Audio-Datei), `language` (optional, auto-detect) | +| readOnly | true | +| Hinweis | Gegenstück zu `textToSpeech`. Akzeptiert Audio-Dateien aus dem Workspace | + +#### detectLanguage + +| Eigenschaft | Wert | +|-------------|------| +| Backend | `interfaceVoiceObjects.detectLanguage()` | +| Parameter | `text` (required) | +| readOnly | true | + +#### searchImages + +| Eigenschaft | Wert | +|-------------|------| +| Backend | Neu: Google Custom Search API (Konzept in `local/pending/doc_enhancement_web_image_actions_pending.md`) | +| Parameter | `query` (required), `maxResults` (optional), `imageType` (optional), `size` (optional) | +| readOnly | true | +| Abhängigkeit | Google Custom Search API Key + Engine ID | +| Hinweis | Bilder werden als URLs zurückgegeben, können via `downloadFromDataSource` oder einem neuen Download-Mechanismus heruntergeladen werden | + +#### neutralizeData + +| Eigenschaft | Wert | +|-------------|------| +| Backend | `serviceNeutralization.processText()` / `processFile()` | +| Parameter | `text` (optional) oder `fileId` (optional, eines von beiden required) | +| readOnly | true (gibt anonymisierten Text zurück, ändert Original nicht) | + +#### executeCode + +| Eigenschaft | Wert | +|-------------|------| +| Backend | Neu: Sandboxed Code Execution | +| Parameter | `code` (required), `language` (required: "python" oder "javascript") | +| readOnly | true | +| Sicherheitskonzept | Siehe Abschnitt 5.1 | + +--- + +## 4. UI-Umbau + +### 4.1 Dateien-Seite (Split-View) + +``` +┌──────────────────────────────────────────────────────────┐ +│ Dateien │ +├───────────────────┬──────────────────────────────────────┤ +│ │ [+ Upload / Drag&Drop] [Filter]│ +│ (Global) │ ─────────────────────────────────── │ +│ ├── Projekte │ Name Typ Grösse Quelle │ +│ │ ├── Q1 │ ─────────────────────────────────── │ +│ │ └── Q2 │ Report.pdf PDF 1.2 MB Workspace│ +│ ├── Analysen │ Daten.xlsx XLSX 450 KB Chat AI │ +│ └── Archiv │ Notes.md MD 12 KB Chat AI │ +│ │ │ +│ [+ Neuer Ordner] │ │ +│ │ │ +├───────────────────┴──────────────────────────────────────┤ +│ ◄═══════════════ Divider (verschiebbar) ═══════════════►│ +└──────────────────────────────────────────────────────────┘ + +Wichtig: Die Action-Buttons (Upload, Filter) sind OBERHALB der Tabelle fixiert, +damit sie bei wechselnder Tabellengrösse nicht springen. +``` + +### 4.2 Komponenten + +**FolderTree (neue Shared-Komponente)** +- Rekursiv verschachtelbarer Baum +- Lazy-Loading der Unterordner (Referenz: `DataSourcePanel.tsx` für Tree-Pattern) +- **Inline-Icons pro Folder-Zeile:** Delete (🗑), Add Subfolder (+), Rename (✏) -- direkt sichtbar, kein Kontextmenü nötig +- **Drag-and-Drop:** Folders und Files können per Drag auf andere Folders verschoben werden +- Visuelles Feedback: Drop-Target-Highlighting beim Hover über gültiges Ziel +- Wiederverwendbar in der Dateien-Seite UND im Workspace-Chat (gleiche Komponente) + +**FileTable (bestehend, erweitert)** +- Zeigt Dateien des aktuell markierten Folders +- Neue Spalte "Quelle" (zeigt `featureInstanceLabel` als Information, nicht als Gruppierung) +- **Drag-and-Drop:** Dateien aus der Tabelle in den Folder-Tree ziehen zum Verschieben +- Kontextmenü pro Datei: Umbenennen, Löschen, Kopieren, Verschieben, Download + +**SplitView / Divider** +- Verschiebbar per Maus-Drag +- Default-Position: 25% links / 75% rechts +- Minimum-Breite pro Seite: 200px +- Position persistent im LocalStorage + +### 4.3 Workspace-Integration + +Im Workspace-Chat (`WorkspacePage.tsx`) wird der bestehende `FileBrowser` umgebaut: +- Statt Gruppierung nach `featureInstanceId`: der gleiche `FolderTree` wie auf der Dateien-Seite +- Kompaktere Darstellung (kein Split-View nötig, Tree + Dateien untereinander) +- Gleiche Drag-and-Drop-Fähigkeiten + +--- + +## 5. Kritische Bewertung + +### 5.1 executeCode -- Sicherheitskonzept + +**Anforderung:** "Hier einfach nur für Analysen und Berechnungen zulassen, nicht für Codeelemente ausserhalb der Umgebung oder Installationen." + +**Bewertung:** Sinnvoll und notwendig für Datenanalyse. Die Einschränkung auf Analysen/Berechnungen ist korrekt. + +**Empfohlene Absicherung:** +- **Isolierte Ausführung:** RestrictedPython (Python) oder vm2/isolated-vm (JS) -- keine eigene Docker-Sandbox nötig für reine Berechnungen +- **Kein Dateisystem-Zugriff:** `open()`, `os`, `subprocess`, `sys` blockieren +- **Kein Netzwerk-Zugriff:** `socket`, `urllib`, `requests` blockieren +- **Keine Installationen:** `pip`, `import` nur für Whitelist (math, statistics, json, csv, re, datetime, collections, itertools, functools, decimal, fractions) +- **Timeout:** Max. 30 Sekunden Ausführungszeit +- **Memory-Limit:** Max. 256 MB +- **Output-Limit:** Max. 50.000 Zeichen Rückgabe + +**Risiko-Bewertung:** Mittel. RestrictedPython ist gut getestet, aber kein perfekter Sandbox. Für den Use Case (Berechnungen, Datenanalyse) ist das Risiko vertretbar, solange die Whitelist strikt eingehalten wird. + +### 5.2 editFile -- Logik aus Codeeditor übernehmen + +**Anforderung:** "Bestehende Datei-Inhalte aktualisieren." + +**Bewertung:** Das Codeeditor-Feature hat eine bewährte File-Edit-Pipeline (`applyEdit`-Route, `createFileData(fileId, contentBytes)`). Da der Codeeditor später entfernt wird, muss die benötigte Logik in den Workspace-Agent **kopiert** werden -- keine Referenzen auf Codeeditor-Module. + +**Zu übernehmende Logik (aus Codeeditor extrahieren):** +- File-Write: `createFileData(fileId, contentBytes)` zum Überschreiben bestehender Dateien +- Falls `createFileData` bei existierenden Daten überspringt: `deleteFileData` → `createFileData` als Fallback +- MIME-Type-Erkennung: `_guessMimeType(fileName)` Logik kopieren +- FileItem-Metadaten (fileSize, fileHash) via `updateFile` aktualisieren +- MIME-Type-Check: Nur Text-basierte Dateien erlauben (text/\*, application/json, etc.) +- Knowledge-Store re-indexieren nach Edit (bestehende Pipeline nutzen) + +**Zu übernehmende UX-Logik (aus Codeeditor extrahieren):** +- `FileEditProposal`-Datenmodell (`EditStatusEnum`: PENDING → ACCEPTED / REJECTED) in Agent-Datenmodelle kopieren +- `file_edit_proposal` SSE-Event-Pattern in den Workspace-Agent übernehmen +- Diff-View-Komponente (`DiffPreviewPanel.tsx`) als eigenständige Shared-Komponente extrahieren + +**Optionaler Approve/Reject-Mode:** +- Agent emittiert `file_edit_proposal` SSE-Event mit Old/New-Content +- Frontend zeigt Diff-View (extrahierte Shared-Komponente) +- User akzeptiert oder lehnt ab +- Konfigurierbar: Auto-Apply für kleine Edits, Approve für Dateien > 1 KB + +**Race-Condition-Risiko:** Gering. Benutzerbezogen, sequenzieller Agent-Zugriff. + +### 5.3 copyFile -- Vollständige Kopie + +**Anforderung:** "Datei im Workspace duplizieren." + +**Bewertung:** Korrekt. Nur File-Level-Kopie sinnvoll. FileData **muss** vollständig dupliziert werden, damit die Kopie unabhängig bearbeitet werden kann (z.B. via `editFile`). + +**Empfehlung:** +- Nur einzelne Dateien kopieren, keine ganzen Ordner +- **Vollständige Duplikation:** FileItem UND FileData werden als eigenständige Einträge mit neuer ID erstellt. Keine Shared-Referenz auf dieselben Daten, da die Kopie editierbar sein muss. +- Ordner-Kopie als eigenes Feature für später (komplexer wegen Rekursion und Namenskonflikten) + +### 5.4 createCalendarEvent -- Nicht implementierbar + +**Anforderung:** "Kalender-Eintrag erstellen." + +**Bewertung: Noch nicht umsetzbar.** Calendar-API (Microsoft Graph `/calendar/events`) ist nicht gebaut. + +**Empfehlung:** Aus dem aktuellen Scope entfernen. Als separates Feature planen: +1. Microsoft Graph Calendar-Endpoints in `connectorMsft.py` implementieren +2. Workflow-Actions für Calendar erstellen (`outlook.createEvent`, `outlook.listEvents`) +3. Dann als Agent-Tool exponieren + +**Bestandsaufnahme Mail-Operationen (für Kontext):** + +| Operation | Microsoft Outlook | Google Gmail | +|-----------|-------------------|--------------| +| Mail-Ordner auflisten | `OutlookAdapter.browse(me/mailFolders)` | `GmailAdapter.browse(labels)` | +| Mails in Ordner lesen | `OutlookAdapter.browse(me/mailFolders/{id}/messages)` | `GmailAdapter.browse(messages?labelIds=)` | +| Mail herunterladen | `OutlookAdapter.download(me/messages/{id})` | **Stub** (gibt `b""` zurück) | +| Mails suchen | `OutlookAdapter.search(me/messages?$search=)` | `GmailAdapter.search(users/me/messages?q=)` | +| Mail senden | `OutlookAdapter.sendMail(me/sendMail)` | **Nicht implementiert** | +| Draft erstellen | `composeAndDraftEmailWithContext` (Workflow) | **Nicht implementiert** | +| Draft senden | `sendDraftEmail` (Workflow) | **Nicht implementiert** | +| Agent-Tool `sendMail` | Implementiert (nur Outlook trotz Doku "Outlook, Gmail") | **Nicht unterstützt** | + +**Fazit Mail:** Microsoft Outlook ist vollständig integriert (lesen, suchen, senden, drafts). Google Gmail hat nur Lesen/Suchen (readonly Scopes). Gmail-Senden, Drafts und Download sind nicht implementiert -- bei Bedarf separates Feature. + +### 5.5 featureInstanceId-Entfernung + +**Anforderung:** "Die Strukturierung der Daten nach deren Creation Feature Instanz entfernen wir. Dazu eine zusätzliche Spalte ergänzen." + +**Bewertung:** Guter Ansatz. Die Ordnerstruktur ersetzt die Feature-Instanz-Gruppierung. + +**Empfehlung:** +- `featureInstanceId` auf `FileItem` beibehalten (nicht aus dem Modell entfernen) +- Nicht mehr für Gruppierung nutzen, nur als informatives Metadatum +- Neue Tabellenspalte "Quelle" zeigt den Feature-Instanz-Namen +- Kein Datenmigrations-Aufwand nötig -- bestehende Dateien bleiben im Root und können manuell in Ordner verschoben werden +- Die Zuordnung `featureInstanceId` bei neuen Dateien weiterhin setzen (Audit-Trail) + +### 5.6 Root "(Global)" -- Multi-Mandanten-Implikation + +**Anforderung:** "Der Root ist der Folder '(Global)'." + +**Bewertung:** Konsistent mit dem bestehenden System. Dateien sind bereits benutzerbezogen (`_createdBy` Filter). Der Root-Scope ist pro User, nicht systemweit. + +**Empfehlung:** +- Root ist virtuell (kein DB-Eintrag), repräsentiert `folderId=None` +- Dateien sind weiterhin pro User isoliert (bestehende RBAC-Filter) +- Label "(Global)" im UI zeigt dem User, dass dies sein persönlicher Root ist +- Für Multi-Mandanten: Dateien haben `mandateId`, Filter bleibt bestehen + +### 5.7 searchImages -- Abhängigkeit und Integration + +**Anforderung:** Aus `doc_enhancement_web_image_actions_pending.md` ein Tool ableiten. + +**Bewertung:** Das bestehende Konzept ist solide (Google Custom Search API, `WEB_SEARCH_MEDIA` Operation Type). Für den Agent-Tool-Kontext reicht eine vereinfachte Version. + +**Empfehlung:** +- Das Agent-Tool `searchImages` als Wrapper um die neue `WEB_SEARCH_MEDIA`-Operation implementieren +- Ergebnisse als Liste von URLs zurückgeben (der Agent kann dann `downloadFromDataSource` oder einen neuen Mechanismus nutzen, um Bilder herunterzuladen) +- Google API Key als Voraussetzung klar dokumentieren +- Fallback: Wenn kein Google API Key konfiguriert, Tool nicht registrieren + +### 5.8 Dokument-Rendering mit Bildern + +**Anforderung:** Können professionelle Berichte (DOCX, XLSX, PPTX, PDF, HTML) mit eingebetteten Bildern generiert werden? + +**Bewertung: Ja, alle 5 Renderer unterstützen Bilder bereits vollständig.** + +| Renderer | Pfad | Bild-Methode | Bibliothek | +|----------|------|-------------|------------| +| DOCX | `renderers/rendererDocx.py` | `_renderJsonImage()` → `doc.add_picture()` | python-docx | +| XLSX | `renderers/rendererXlsx.py` | `_addImageToExcel()` → `sheet.add_image()` | openpyxl | +| PPTX | `renderers/rendererPptx.py` | `_addImagesToSlide()` → `slide.shapes.add_picture()` | python-pptx | +| PDF | `renderers/rendererPdf.py` | `_renderJsonImage()` → `ReportLabImage` | reportlab | +| HTML | `renderers/rendererHtml.py` | `_renderJsonImage()` → `` | native | + +**Gemeinsames Bild-Format (alle Renderer):** +```json +{ + "type": "image", + "content": { + "base64Data": "", + "altText": "Bildbeschreibung", + "caption": "Optionale Bildunterschrift" + } +} +``` + +**Basis-Klasse** (`documentRendererBaseTemplate.py`): Bietet `_validateImageData()`, `_getImageDimensions()`, `_resizeImageIfNeeded()` als Shared-Funktionen. + +**Fazit:** Kein Handlungsbedarf für Bild-Rendering. Der Agent kann Bilder (z.B. via `generateImage` oder `searchImages` + Download) als Base64-Daten in das Dokument-Schema einbetten, und alle Renderer verarbeiten diese korrekt. + +### 5.9 neutralizeData -- Scope + +**Anforderung:** "Text/Datei anonymisieren." + +**Bewertung:** `serviceNeutralization` existiert, ist aber Feature-spezifisch (Neutralization Feature). Als Agent-Tool muss es Feature-unabhängig aufrufbar sein. + +**Empfehlung:** +- Read-Only: Gibt anonymisierten Text zurück, ändert das Original nicht +- Für Dateien: Temporär extrahieren, neutralisieren, Ergebnis als neuen Text zurückgeben +- Feature-Abhängigkeit prüfen: Wenn Neutralization-Feature nicht lizenziert, Tool nicht registrieren + +--- + +## 6. Zusammenfassung und Priorisierung + +### Tool-Übersicht nach Aufwand und Nutzen + +| Tool | Aufwand | Nutzen | Priorität | Phase | +|------|---------|--------|-----------|-------| +| `deleteFile` | Niedrig | Hoch | **Must** | 1 | +| `renameFile` | Niedrig | Hoch | **Must** | 1 | +| `readUrl` | Niedrig | Hoch | **Must** | 1 | +| `translateText` | Niedrig | Mittel | **Should** | 1 | +| `deleteFolder` | Mittel | Hoch | **Must** | 2 | +| `renameFolder` | Niedrig | Mittel | **Should** | 2 | +| `moveFolder` | Mittel | Mittel | **Should** | 2 | +| `copyFile` | Niedrig | Mittel | **Should** | 2 | +| `editFile` | Niedrig (Codeeditor-Logik vorhanden) | Hoch | **Must** | 2 | +| `speechToText` | Niedrig | Mittel | **Should** | 3 | +| `detectLanguage` | Niedrig | Niedrig | **Could** | 3 | +| `searchImages` | Hoch | Mittel | **Should** | 3 | +| `neutralizeData` | Mittel | Niedrig | **Could** | 3 | +| `executeCode` | Hoch | Hoch | **Should** | 3 | +| `createCalendarEvent` | Hoch | Mittel | **Won't** (vorerst) | - | + +### Geschätzter Gesamtaufwand + +| Bereich | Aufwand | +|---------|---------| +| Phase 1: Quick-Win Tools | 2-3 Tage | +| Phase 2: Dateisystem-Backend + Tools | 4-5 Tage | +| Phase 3: Erweiterte Tools | 5-7 Tage | +| UI: Dateien-Seite Split-View | 3-4 Tage | +| UI: Folder-Tree Komponente | 2-3 Tage | +| UI: Workspace-Integration | 1-2 Tage | +| **Gesamt** | **17-24 Tage** | + +--- + +## 7. Referenzen + +| Dokument | Pfad | +|----------|------| +| AI Agent Architecture | `wiki/concepts/AI-Agent-Architecture-Konzept.md` | +| Web Image Search Konzept | `local/pending/doc_enhancement_web_image_actions_pending.md` | +| Codeeditor (Logik extrahieren, Feature wird entfernt) | `gateway/modules/features/codeeditor/routeFeatureCodeeditor.py` | +| Codeeditor Datenmodell (extrahieren) | `gateway/modules/features/codeeditor/datamodelCodeeditor.py` | +| Codeeditor Response Parser (extrahieren) | `gateway/modules/features/codeeditor/responseParser.py` | +| Codeeditor DiffPreview (als Shared-Komponente extrahieren) | `frontend_nyla/src/pages/views/codeeditor/DiffPreviewPanel.tsx` | +| Dokument-Renderer (Bild-Support) | `gateway/modules/serviceCenter/services/serviceGeneration/renderers/` | +| FileItem Datenmodell | `gateway/modules/datamodels/datamodelFiles.py` | +| FileFolder Datenmodell | `gateway/modules/datamodels/datamodelFileFolder.py` | +| DB-Operationen | `gateway/modules/interfaces/interfaceDbManagement.py` | +| Agent Tools | `gateway/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py` | +| Tool Registry | `gateway/modules/serviceCenter/services/serviceAgent/toolRegistry.py` | +| Frontend FileBrowser | `frontend_nyla/src/pages/views/workspace/FileBrowser.tsx` | +| Frontend FilesPage | `frontend_nyla/src/pages/basedata/FilesPage.tsx` | +| DataSourcePanel (Tree-Referenz) | `frontend_nyla/src/pages/views/workspace/DataSourcePanel.tsx` | diff --git a/concepts/Commcoach-Voice-Recording-Streaming-Konzept.md b/concepts/Commcoach-Voice-Recording-Streaming-Konzept.md new file mode 100644 index 0000000..df939f7 --- /dev/null +++ b/concepts/Commcoach-Voice-Recording-Streaming-Konzept.md @@ -0,0 +1,334 @@ +# CommCoach Voice Recording Streaming Konzept + +## Ausgangslage + +Die aktuelle CommCoach-Spracheingabe basiert auf Browser `SpeechRecognition` (`webkitSpeechRecognition`). +Auf Mobile-Browsern wird die Aufnahme typischerweise alle ca. 5-7 Sekunden automatisch beendet und neu gestartet. +Das ist kein sauber behebbarer App-Bug, sondern eine Browser/API-Limitation. + +## Zielbild + +Die Spracheingabe soll auf Mobile und Desktop stabil laufen, ohne erzwungene 5-Sekunden-Resets, ohne Textverlust zwischen Restart-Gaps und ohne MSFT-Abhängigkeiten. + +## Antwort auf die Kernfragen + +### 1) Ist Option 3 ein genereller Umbau oder nur Mobile? + +Beides ist möglich: + +- **Variante A (Mobile-only Umbau):** + Neue Audio-Streaming-Pipeline nur auf Mobile aktivieren, Desktop bleibt vorerst bei Browser STT. +- **Variante B (Genereller Umbau):** + Neue Pipeline für alle Clients (Mobile + Desktop), einheitliches Verhalten und weniger Wartung. + +Empfehlung: **Variante B**, da nur ein Stack gepflegt werden muss und CommCoach-Logik konsistent bleibt. + +### 2) Kann das komplett in unserer Plattform umgesetzt werden? + +Ja. Vollständig in eigener Plattform ist möglich. + +Notwendig sind: + +- Eigene UI- und Gateway-Implementierung (WebSocket Streaming) +- Eigener STT-Service (self-hosted), z. B. `faster-whisper` +- Optional eigene VAD (Voice Activity Detection), z. B. `webrtcvad` + +Nicht zwingend notwendig: + +- Externe SaaS-STT Provider +- MSFT/Azure Speech + +## Zielarchitektur (MSFT-frei) + +### UI (Frontend CommCoach) + +- Mikrofonaufnahme via `MediaStream` + `AudioWorklet` oder `MediaRecorder` +- Chunking (z. B. 100-250 ms) +- Streaming per WebSocket an Gateway +- Lokaler Live-Preview-Text kommt nicht mehr aus Browser-STT, sondern aus Server-Interims +- Bestehende State-Machine bleibt als Steuerlogik erhalten (`idle`, `listening`, `botSpeaking`, `interrupted`, `muted` orthogonal) + +### Gateway + +- Neuer Endpoint, z. B. `ws /api/feature/commcoach/{instanceId}/stt/stream` +- Sessiongebundene Stream-Verarbeitung +- Weiterleitung der Audio-Chunks an STT-Worker +- Rückkanal von Interims + Final-Text zum UI +- Serverseitige Segmentierung (Silence / VAD) statt Browser `onend` + +### STT-Service (self-hosted) + +- `faster-whisper` als Runtime (GPU empfohlen, CPU möglich) +- Sprachen konfigurierbar (z. B. `de`, `en`) +- Ausgabe: + - `interim` Events (optional, je nach Latenzbudget) + - `final` Segmente + - Zeitmarken für Debug und Nachvollziehbarkeit + +### Optional: VAD-Service + +- Entweder im Gateway oder STT-Worker +- Trigger für Segment-Ende statt fester 1s-Timer +- Stabiler als browserseitige `onspeechstart/onspeechend` + +## Ist-Analyse der bestehenden Codebase (UI + Gateway) + +### Frontend (`frontend_nyla`) + +- `src/pages/views/commcoach/useVoiceController.ts` + - Nutzt Browser `SpeechRecognition` direkt. + - Auto-Restart bei `onend` mit `REC_AUTORESTART_DELAY_MS = 300`. + - `SILENCE_TIMEOUT_MS = 1000` finalisiert User-Text. + - Mobile-Problem entsteht hier durch Browser-`onend`-Zwang (alle ~5-7s). + +- `src/pages/views/commcoach/CommcoachDossierView.tsx` + - Bindet Voice-State-Machine an UI/TTS. + - `voice.liveTranscript` wird als Live-User-Bubble angezeigt. + - Debuglog existiert bereits via `window.__dlog`. + +- `src/hooks/useCommcoach.ts` + - Text läuft über `sendMessageStreamApi` (SSE). + - Audio läuft derzeit nur als One-Shot Blob über `sendAudioStreamApi` (SSE). + - Kein echtes bidirektionales Low-Latency Audio-Streaming. + +- `src/api/commcoachApi.ts` + - Bestehende Endpunkte sind HTTP + SSE. + - Es gibt noch keinen WebSocket-Endpunkt für chunkweises Audio. + +### Gateway (`gateway`) + +- `modules/features/commcoach/routeFeatureCommcoach.py` + - `POST .../message/stream` (SSE) für Text. + - `POST .../audio/stream` (SSE) für Audio-One-Shot. + - Audio-Endpoint liest `await request.body()` komplett und startet danach STT. + +- `modules/features/commcoach/serviceCommcoach.py` + - `processAudioMessage(...)`: STT auf gesamtem Blob, danach `processMessage(...)`. + - Keine Segment-/Chunk-Logik für laufende Erkennung. + +- `modules/interfaces/interfaceVoiceObjects.py` + - Kapselt STT/TTS aktuell über Google Connector. + - API ist bereits zentralisiert und damit gut erweiterbar. + +- `modules/connectors/connectorVoiceGoogle.py` + - Verwendet `recognize` auf Einzeldateien (nicht Streaming). + - Damit strukturell ungeeignet für kontinuierliche Mobile-Spracheingabe. + +## Implementierung im bestehenden System (konkret) + +### Ziel: bestehende CommCoach-Pipeline beibehalten, nur STT-Eingang ersetzen + +Unverändert bleiben: +- Session- und Message-Processing in `serviceCommcoach.py` (`processMessage`, Events, TTS). +- UI-State-Machine-Fachlogik (`idle`, `listening`, `botSpeaking`, `interrupted`, `muted`). + +Ersetzt wird: +- Browser-STT (`SpeechRecognition`) durch Streaming-STT Provider. +- One-Shot Audio-Upload durch WebSocket-Audio-Stream. + +### A) Frontend Änderungen + +1. Neue API-Funktion in `src/api/commcoachApi.ts` + - `openSttStreamApi(instanceId, sessionId, handlers, options)` via WebSocket. + - Handler: `onStatus`, `onInterim`, `onFinal`, `onError`, `onClose`. + +2. Neues Hook `src/hooks/useAudioStreamTranscription.ts` + - Mikrofon aufnehmen (`MediaStream` + `AudioWorklet` oder `MediaRecorder`). + - Audio in 100-250ms Chunks an WS senden. + - Event-Rückkanal verarbeiten (interim/final/status/error). + - Reconnect-Mechanismus für Mobile-Netzwechsel. + +3. `src/pages/views/commcoach/useVoiceController.ts` refactor + - Provider-Interface einführen: + - `browserSpeech` (legacy) + - `streamedStt` (neu) + - `transcriptPartsRef` und `liveTranscript` aus Serverevents speisen. + - `SILENCE_TIMEOUT_MS` nur noch als optionales Guard-Rail, nicht als Kernsegmentierung. + +4. `src/pages/views/commcoach/CommcoachDossierView.tsx` + - Keine Verhaltensänderung in Tabs/State nötig. + - Nur Provider initialisieren und vorhandene Debuganzeige um STT-WS Events erweitern. + +### B) Gateway Änderungen + +1. Neue WS-Route in `modules/features/commcoach/routeFeatureCommcoach.py` + - Beispiel: `ws /api/commcoach/{instanceId}/sessions/{sessionId}/stt/stream` + - Ownership-/Session-Checks analog zu `message/stream`. + - WebSocket akzeptieren, Audio-Chunks entgegennehmen, Events zurücksenden. + +2. Neuer Service `modules/features/commcoach/serviceCommcoachSttStream.py` + - Sessiongebundene Streamverwaltung. + - Chunk-Buffer, VAD/Silence-Regeln, Segmentbildung. + - Für jedes finale Segment: `processMessage(sessionId, contextId, finalText, interface)` aufrufen. + +3. Anpassung `modules/features/commcoach/serviceCommcoach.py` + - `processAudioMessage` bleibt für Legacy/Upload kompatibel. + - Neue Streaming-Nutzung erfolgt über den neuen STT-Stream-Service. + +4. Erweiterung `modules/interfaces/interfaceVoiceObjects.py` + - Neue Methoden ergänzen: + - `startSttStream(...)` + - `pushSttAudioChunk(...)` + - `stopSttStream(...)` + - Bestehende Methoden (`speechToText`, `textToSpeech`) unverändert lassen. + +### C) STT Worker (self-hosted) + +1. Neuer Connector (z. B.) `modules/connectors/connectorVoiceWhisper.py` + - `faster-whisper` Integration. + - Modell, Sprache, VAD-Parameter konfigurierbar. + +2. Optional separater Worker-Prozess + - Entkoppelt Gateway-Latenz von STT-Rechenlast. + - Kommunikation intern über Queue/IPC oder internen WS. + +## WS Event Contract (verbindlich) + +### Client -> Server + +- `open` + - `{ "type": "open", "sessionId": "...", "language": "de-DE", "codec": "pcm16" }` +- `audio` + - `{ "type": "audio", "seq": 123, "chunk": "" }` +- `commit` + - `{ "type": "commit", "reason": "silence|manual|stateChange" }` +- `close` + - `{ "type": "close" }` + +### Server -> Client + +- `status` + - `{ "type": "status", "label": "Sprache wird erkannt..." }` +- `interim` + - `{ "type": "interim", "text": "...", "segmentId": "..." }` +- `final` + - `{ "type": "final", "text": "...", "segmentId": "...", "confidence": 0.0 }` +- `ack` + - `{ "type": "ack", "seq": 123 }` +- `error` + - `{ "type": "error", "code": "stt_failed", "message": "..." }` +- `closed` + - `{ "type": "closed", "reason": "client|server|timeout" }` + +## Migrationsstrategie (ohne doppelte Logikfalle) + +1. Feature-Flag `commcoachVoiceProvider` auf Instanzebene + - Werte: `browserSpeech` | `streamedStt`. + +2. Rolloutpfad + - Schritt 1: intern auf Mobile aktivieren. + - Schritt 2: nach Stabilisierung global aktivieren. + - Schritt 3: Browser-STT-Code entfernen. + +3. Expliziter Fehlerpfad + - Kein stiller Fallback auf Browser-STT, wenn `streamedStt` aktiv ist. + - Fehler wird sichtbar im UI (Banner + Debuglog). + +## Konkrete Taskliste pro Repository + +### Repo `frontend_nyla` + +1. `commcoachApi.ts`: `openSttStreamApi` ergänzen. +2. Neues Hook `useAudioStreamTranscription.ts` implementieren. +3. `useVoiceController.ts`: Provider-Abstraktion + streamed Provider integrieren. +4. `CommcoachDossierView.tsx`: Provider-Init + Debug-Events anzeigen. +5. Tests: + - unit: Segment-Assembler + - integration: state transitions bei interim/final/error + +### Repo `gateway` + +1. WS-Route für STT-Stream in `routeFeatureCommcoach.py`. +2. Neuer Stream-Service `serviceCommcoachSttStream.py`. +3. `interfaceVoiceObjects.py` um Streaming-Methoden erweitern. +4. Neuer Whisper-Connector `connectorVoiceWhisper.py`. +5. Telemetrie + Logs: + - stream open/close + - first interim latency + - first final latency + - reconnect count + +## Variante A vs. Variante B + +| Kriterium | Variante A: Nur Mobile | Variante B: Alle Plattformen | +|---|---|---| +| Time-to-first-fix | schneller | mittel | +| Komplexität gesamt | höher langfristig (2 Stacks) | niedriger langfristig (1 Stack) | +| UX-Konsistenz | unterschiedlich je Device | einheitlich | +| Wartung | doppelt | einfach | +| Risiko | mittelhoch (Divergenz) | mittel (einmaliger Umbau) | + +**Empfehlung:** Variante B. + +## Implementierungsplan + +### Phase 1: Infrastruktur und Prototyp + +1. STT-Worker als eigener Service im Gateway-Umfeld bereitstellen +2. Streaming-Protokoll definieren (Events: `audio`, `interim`, `final`, `status`, `error`, `close`) +3. WS-Route im Gateway für CommCoach implementieren +4. Test-Client mit Beispielaudio aufbauen (ohne UI) zur Last-/Latenzprüfung + +### Phase 2: Frontend Integration + +1. Neues Modul `useAudioStreamTranscription` einführen +2. `useVoiceController` auf Provider-Abstraktion umstellen: + - Provider `browserSpeech` (bestehend) + - Provider `streamedStt` (neu) +3. Transcript-Handling auf Serverevents umstellen: + - Interim in `liveTranscript` + - Final in `transcriptParts` +4. State-Transitions unverändert belassen, nur STT-Quelle ersetzen + +### Phase 3: Segmentierung und Qualität + +1. Serverseitige VAD aktivieren +2. Segment-Ende sauber in `onMessage` überführen +3. Doppelungen/Fragmentverluste mit Session-IDs und Segment-Counter verhindern +4. Mobile Netzwechsel/WS-Reconnect robust behandeln + +### Phase 4: Rollout + +1. Feature-Flag: zuerst intern, dann Pilotmandanten +2. Optionale Stufen: + - Stufe 1: Nur Mobile + - Stufe 2: Alle Plattformen +3. Browser-STT nach Stabilitätsphase vollständig entfernen + +## Technische Leitplanken + +- Keine stillen Fallbacks, die Fehler verdecken +- Explizite Fehlerzustände im UI (Mikrofon, Netzwerk, STT nicht verfügbar) +- Klares Telemetrie-Set: + - Time to first interim + - Time to first final + - Segment-Länge + - Abbruchgründe + - WS-Reconnect-Rate + +## Drittkomponenten + +### Minimal erforderlich + +- Python Packages (self-hosted): + - `faster-whisper` + - optional `webrtcvad` + - Audio-Decode (`ffmpeg` Laufzeit) + +### Nicht erforderlich + +- MSFT/Azure Speech +- Externe STT-SaaS + +## Risiken und Gegenmassnahmen + +- **GPU-Kapazität fehlt:** zunächst kleines Modell und Queueing, später GPU-Skalierung +- **Latenz zu hoch:** Chunk-Grösse reduzieren, VAD feinjustieren, Modellwahl anpassen +- **Mobile Netz instabil:** robustes Reconnect-Handling + Segment-ACKs +- **Drift zwischen UI und Backend:** eindeutige Stream-/Segment-IDs und Idempotenzregeln + +## Fazit + +Der Umbau ist vollständig in der eigenen Plattform machbar und löst das Mobile-5-Sekunden-Problem an der Wurzel. +Für Wartbarkeit und konsistentes Verhalten ist ein **genereller Umbau (Variante B)** sinnvoll. +Ein stufenweiser Rollout mit Feature-Flag minimiert Risiko. diff --git a/deployment/poweron_sec.kdbx b/deployment/poweron_sec.kdbx index c1d6c6a45680f1169060323219c89f1005bef301..ea65af58a4fb0ab2e6f439ade9519f9b8fa18df4 100644 GIT binary patch literal 18878 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⪻?YyDu@XWgQpgucJ4B<`3`%sdU@N_x~YoF;yz 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