wiki/b-reference/teams-bot/architecture.md
2026-05-12 19:16:21 +02:00

242 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: canonical -->
<!-- lastReviewed: 2026-05-12 -->
<!-- verifiedAgainst: service-teams-browser-bot (statisches Avatar-Video + anonymer Join via Chrome-Channel + WebRTC-Wrapper-Gating 2026-05-12); service-teams-browser-bot (documentation review 2026-02-18); gateway/modules/features/teamsbot/{service,routeFeatureTeamsbot,interfaceFeatureTeamsbot,datamodelTeamsbot}.py + STT single-language (2026-05-12) + `GET /api/teamsbot/{instanceId}/dashboard/stream` (2026-05-11); gateway/tests/unit/teamsbot/test_directorPrompts.py (Director Prompts 2026-04-24); gateway Voice STT batch + linear16 (2026-05-10); frontend_nyla TeamsbotDashboardView / TeamsbotModulesView / TeamsbotSessionView (IA + SSE 2026-05-11) -->
# Teams Meeting Bot -- Architektur
## Überblick
AI-gesteuerter Meeting-Bot für Microsoft Teams. Tritt Meetings als regulärer Teilnehmer bei (Browser-Automation via Playwright/Chromium), erfasst Live-Transkripte, reagiert per Sprache (TTS) und/oder Chat. Kein Teams-Graph-SDK nötig -- funktioniert mandantenübergreifend ohne Admin-Approval.
## System-Architektur
```
┌────────────┐ SSE ┌──────────────┐ WebSocket ┌─────────────┐
│ Frontend │◄──────────────│ Gateway │◄───────────────│ Bot Service │
│ (Nyla UI) │ │ (AI, TTS, │ HTTP (join/ │ (Playwright │
│ │ │ Sessions) │ leave) │ Chromium) │
└────────────┘ └──────────────┘ └─────────────┘
```
| Verbindung | Protokoll | Zweck |
|------------|----------|-------|
| Gateway ↔ Bot | WebSocket | Echtzeit: Transkripte, Chat, Audio, Status |
| Gateway → Bot | HTTP | Session-Steuerung (join, leave, status) |
| Frontend ← Gateway | SSE | Live-Transkript-Stream für UI; Dashboard-Push (`dashboard/stream`) |
## Nyla UI (MeetingModule-IA)
Die Feature-Oberfläche ist in **fünf Tabs** strukturiert (siehe auch `wiki/c-work/2-build/2026-04-teamsbot-greenfield-ia-and-live-update.md`):
| Tab | Route-Segment | Zweck |
|-----|----------------|--------|
| Dashboard | `dashboard` | KPIs, Modul-Aktivität, Quick-Actions; Daten per **SSE** `GET /api/teamsbot/{instanceId}/dashboard/stream` (Snapshots `sessions` + `modules`, Intervall 3 s bei aktiver eigener Sitzung, sonst 20 s) |
| Assistent | `assistant` | Wizard: Modul wählen/anlegen, Meeting-Link, Join-Modus (Systembot / Gast / Mein Account), optional Sitzungskontext → startet Session und navigiert zur Live-Ansicht |
| Module | `modules` | CRUD `TeamsbotMeetingModule`, aufklappbare Session-Liste pro Modul; **Deep-Link** `?moduleId=<uuid>` klappt das Modul auf und scrollt es ins Sichtfeld |
| Live-Session | `sessions` | Regie-Panel, UDB, Transkript, SSE `sessions/{id}/stream`; **MFA** (`mfaChallenge` / `mfaResolved`) wird hier über dieselbe SSE-Verbindung abgewickelt |
| Einstellungen | `settings` | Bot-Stimme, User-Settings, System-Bots (SysAdmin) |
**Datenmodell:** `TeamsbotMeetingModule` gruppiert Reihen; `TeamsbotSession.moduleId` verknüpft optional (Adhoc ohne Modul). Standard-Meeting-Link und Standard-Bot-Name pro Modul für Prefill im Assistenten.
## Kernfähigkeiten
- **Live Transcription:** Erfasst Untertitel mit Sprecher-Zuordnung, streamt via SSE
- **AI-Analyse:** Transkript-Segmente werden durch AI-Modell (GPT-4o-mini / Claude) analysiert
- **Voice Response:** TTS-Audio wird über den Mikrofon-Kanal ins Meeting gespielt
- **Chat Response:** Bot kann Chat-Nachrichten ins Meeting schreiben
- **Multi-Session:** Mehrere Bot-Instanzen parallel in verschiedenen Meetings
## Use Cases
| UC | Beschreibung |
|----|-------------|
| AI Meeting Assistant | Bot nimmt teil, hört zu, antwortet auf Ansprache ("Hey Nyla, ...") |
| Live Transcription | Echtzeit-Transkript-Stream für Teilnehmer ausserhalb des Meetings |
| Meeting Summary | AI-generierte Zusammenfassung nach Meeting-Ende |
| Multi-Bot | Mehrere parallele Sessions in verschiedenen Meetings |
| Director Prompts | Operator gibt dem laufenden Bot **private** Regieanweisungen (One-Shot oder Persistent), Antwort wird ins Meeting eingespielt |
| Hybrid Agent Escalation | SPEECH_TEAMS-Pfad kann komplexe Anfragen via `needsAgent=true` an den vollen Agent (`agentService.runAgent`) eskalieren |
## Integration mit Gateway
Der Gateway (Feature `teamsbot`) verwaltet Sessions und stellt die AI-Pipeline bereit:
- Session-Lifecycle: erstellen, starten, stoppen
- WebSocket-Verbindung pro Session
- AI-Analyse der Transkript-Segmente via `serviceAi`
- TTS-Generierung für Voice-Responses
- **Dashboard-SSE:** `GET /api/teamsbot/{instanceId}/dashboard/stream` — wiederholte JSON-Events `{ "type": "dashboardState", "sessions": [...], "modules": [...] }` mit gleicher Sichtbarkeit wie `GET /sessions` (eigene Sessions, ausser Platform-Admin sieht alle).
## Schlüssel-Dateien
| Datei / Bereich | Rolle |
|-----------------|-------|
| `gateway/modules/features/teamsbot/` | Gateway-seitiges Feature-Modul (inkl. `dashboard/stream`, Session-SSE, Module-CRUD) |
| `frontend_nyla/src/pages/views/teamsbot/` | Dashboard (SSE), Assistent, Module, Live-Session, Einstellungen |
| `frontend_nyla/src/api/teamsbotApi.ts` | u.a. `createDashboardStream`, `createSessionStream`, Module-API |
| `service-teams-browser-bot/` | Eigenständiger Bot-Service (separates Repository) |
## Regeln / Invarianten
- Bot tritt als **regulärer Web-Teilnehmer** bei (Browser-Automation), nicht via Graph Communications SDK
- Jede Session läuft in einer **eigenen Browser-Instanz** (Isolation)
- Authentifizierter Join (mit Microsoft-Account) oder Anonymous Guest -- je nach Konfiguration
- Gateway ist die **einzige** Schnittstelle für AI-Aufrufe und TTS -- der Bot-Service selbst hat keine AI-Logik
**STT auf dem Gateway:** Meeting-Audio-Chunks (WebSocket `audioChunk`, PCM) werden pro Chunk mit `VoiceObjects.speechToText` transkribiert (Batch `recognize`, gemeinsamer Connector mit CommCoach). Konfiguration u. a. `audioFormat=linear16`, `skipFallbacks=True`; Details und Modell-Defaults: [voice-google.md](../gateway/voice-google.md).
**Sprache ist single-language.** Die STT-Sprache ist die Sessionsprache (`TeamsbotUserSettings.language` überschreibt `TeamsbotConfig.language`, Schema-Default `de-DE`). Es gibt **keine** hardcodierten Alternativ-Sprachen — frühere `alternative_language_codes=["en-US"]`-Kombi liess Google STT bei verrauschter Audio (z.B. nach langem Bot-TTS-Playback mit minimalem akustischem Loopback) auf en-US springen und englisches Kauderwelsch zurückgeben (Confidences 0.30.5), während saubere Sprache korrekt als de-DE mit ~0.9+ erkannt wurde. Falls mehrsprachige Meetings gebraucht werden: `connectorVoiceGoogle.speechToText` akzeptiert `alternativeLanguages: list[str]`; eine entsprechende Konfig-Spalte (z.B. `TeamsbotConfig.alternativeLanguages`) müsste explizit eingeführt werden.
### Audio-Capture: WebRTC-Wrapper-Gating
Der Bot installiert einen `RTCPeerConnection`-Wrapper per `addInitScript` (Browser-Start, vor jeder Teams-Navigation), damit später keine PC unbeobachtet bleibt. **Während Pre-Join, Lobby und SDP-Renegotiation darf der Wrapper aber NICHTS am Audio-Stream anfassen** — kein `clone()`, kein `createMediaStreamSource()`, kein `AudioContext`. Jeder Eingriff in dieser Phase löst in Teams' aktuellem `light-meetings`-Bundle den Renderer-Crash `Cannot read properties of null (reading 'rejectMediaDescriptionsUpdateAsync')` aus und der anonyme Bot landet entweder dauerhaft in der Lobby oder wird wieder zur Pre-Join-Seite geworfen.
Mechanik:
1. Wrapper-Init setzt `window.__audioCaptureEnabled = false` und legt einen `track`-Listener pro PC an, der bei `false` ausschließlich Diagnostik loggt.
2. Erst nach `orchestrator._setState('in_meeting')` ruft `_enableTranscriptCapture``audioCaptureProcedure.startCapture()` auf.
3. `startCapture()` flippt `__audioCaptureEnabled = true` und iteriert `getReceivers()` aller PCs mit `connectionState === 'connected'`. Für jede live Audio-Spur wird `__audioCaptureAttachTrack(pc, track)` ausgeführt (AudioContext + `track.clone()` + `MediaStreamSource` + AudioWorklet/ScriptProcessor → PCM16 16 kHz Mono).
4. Spätere `track`-Events bauen ihren Audio-Graph automatisch, sobald die zugehörige PC `connected` ist.
Es wird **keine Label-Filterung** angewendet — die Tracks haben je nach Session/Layout entweder `mainAudio-<n>` oder UUID-Labels. Der einzige saubere Trigger ist der Bot-State.
Refs: `service-teams-browser-bot/src/bot/audioCaptureProcedure.ts` (`__audioCaptureEnabled`, `__audioCaptureAttachTrack`, `startCapture`); Aufrufer `orchestrator.ts` (`_attemptJoin` STEP 4 → `_setState('in_meeting')``_enableTranscriptCapture`).
### Chat-Panel-Toggle-Auswahl im Auth-Layout
Im authentifizierten Full-Teams-Layout existieren ZWEI sichtbare Buttons mit "Chat" im aria-label:
1. der **echte Toggle**: UUID-id, `aria-label="Chat (Ctrl+Shift+2)"`, **`aria-pressed="true|false"`** — toggelt das Meeting-Chat-Side-Panel.
2. ein **Schein-Toggle**: `id="chat-button"`, `aria-label="Chat"`, **kein `aria-pressed`** — vermutlich der Side-Nav-Eintrag der Chat-App; klicken hat keinerlei Wirkung auf das Meeting-Chat-Panel.
In light-meetings (anon) existiert nur `#chat-button` und der IST der echte Toggle (light-meetings nutzt menu-button-Semantik ohne `aria-pressed`).
`_openChatPanel()` löst das so:
- Sammelt alle sichtbaren `button` / `[role=button]` / `[role=menuitem]` deren `id`/`data-tid`/`aria-label`/`title` einen der Hints `chat`/`unterhalt`/`besprechung`/`conversation` enthält (sprachunabhängig).
- **Bevorzugt Kandidaten mit `aria-pressed` ∈ {`true`,`false`}** (echte Toggle-Buttons), Fallback ist der erste passende Nicht-Toggle.
- Trackt geklickte Buttons per `id|data-tid|aria-label`-Key und überspringt sie in den nächsten Runden — verhindert die Endlosschleife "klicke 12× denselben falschen Button" wenn der erste Pick nichts bewirkt.
Refs: `service-teams-browser-bot/src/bot/chatProcedure.ts._openChatPanel`.
### Chat-Panel-Detection
`_isChatPanelOpen()` macht **zwei** Checks am `[data-tid="calling-right-side-panel"]`-Container — keine Fallbacks:
1. **Existenz + echte Visibility** des Side-Pane-Containers: `offsetWidth/Height > 0 && offsetParent !== null`. Wenn das Panel zu ist, ist der Container entweder unmounted oder vom Parent auf 0×0 kollabiert.
2. **Mode-Disambiguation per chat-spezifischen Child-Tids** innerhalb des sichtbaren Containers: `message-pane-layout`, `message-pane-body`, `chat-pane-compose-message-footer`, `message-pane-footer`, `#chat-pane-list`, `[data-app-name="chats"]`. Trennt Chat sauber von People / Info / Captions ohne Text-Lookup.
Frühere Iterationen hatten zwei zusätzliche Schichten (aria-pressed-Toggle + Compose-Box-Sichtbarkeit) als "Fallback für ältere Auth-Layouts". Diese sind 2026-05-12 entfernt worden — sowohl anon als auch auth nutzen denselben `calling-right-side-panel`-Container, die Fallbacks hätten nie gefeuert (oder wären redundant gewesen wenn doch). Konform zur Coding-Regel "Do not add fallback code, if not necessary".
#### Trapdoor: `vdi-occlusion`
Die CSS-Klasse `vdi-occlusion` taucht in der neuen Calling-Layout-Version **als Permanent-Klasse** auf `calling-right-side-panel` UND `message-pane-layout` auf — auch wenn das Panel sichtbar geöffnet ist. Sie ist KEIN Visibility-Indikator. Ein älterer Detection-Code, der `vdi-occlusion → "Panel zu"` ableitete, lieferte deshalb **immer** false-negative bei offenem Panel → periodischer Scan triggerte `_openChatPanel()` → klickte den Toggle → schloss das Panel echt → klickte weitere chat-aria Buttons (Side-Nav-Apps "Chats"/"Meeting chats", Tab-Items `tab-item-com.microsoft.chattabs.*`) → Endlos-Toggle-Loop mit `UPR`-Page-Errors von Teams. Visibility wird ausschließlich über `offsetWidth/Height > 0 && (offsetParent !== null || position === fixed)` geprüft.
#### Toggle-Button selbst trägt KEINE State-Information
Im neuen Calling-Layout ist der `<button id="chat-button" aria-label="Chat">` byte-identisch in beiden Zuständen — kein `aria-pressed`, kein `data-state`, identische CSS-Klassen. Die optische "blaue Linie unten" beim offenen Zustand kommt aus einem CSS-`:has()`-Selektor der vom Vorhandensein des offenen Side-Panels abhängt, nicht vom Button-State. Detection muss daher zwingend über den Container, nicht den Button laufen.
Refs: `service-teams-browser-bot/src/bot/chatProcedure.ts._isChatPanelOpen`.
### Browser-Channel: echtes Chrome für anonyme Joins
Anonyme Joins **müssen** mit dem lokal installierten echten Chrome laufen (`channel: 'chrome'`), nicht mit Playwrights gebündeltem Chromium. Teams' `light-meetings`-Pfad fingerprintet den Browser (vermutlich `Sec-CH-UA` Client-Hints + Canvas/WebGL-Signale + fehlende Chrome-Marken-Identität); das Bundled-Chromium fällt durch und Teams reagiert mit zwei Konsequenzen:
1. Der anonyme Bot wird **trotz "Jeder umgeht die Lobby"-Setting** in die Lobby gezwungen.
2. Während dieser Lobby-Phase läuft ein Preheating-PC-Codepfad, der mit `Cannot read properties of null (reading 'rejectMediaDescriptionsUpdateAsync')` crasht und den Bot auf die "Sorry, we couldn't connect you"-Seite wirft.
Der **manuelle** Inkognito-Test im echten Chrome derselben Maschine geht beide Symptome nicht — er joint ohne Lobby. Das beweist, dass es kein generelles Teams-Problem für anonyme Gäste ist, sondern den Playwright-Chromium-Fingerprint trifft.
Der **authentifizierte Pfad** funktioniert auch mit Bundled-Chromium, weil eine echte MS-Login-Session den Anti-Bot-Check überspringt.
Konfiguration: `BOT_BROWSER_CHANNEL=chrome` in `.env`, gelesen in `src/config.ts` als `botBrowserChannel`, an `chromium.launch({ ..., channel })` durchgereicht in `src/bot/orchestrator.ts._launchBrowser()`. Leer = Bundled Chromium (Default, geeignet für Auth-Joins / Tests). `chrome` und `msedge` setzen den lokalen Real-Browser ein; auf den Bot-Hosts muss Chrome bzw. Edge installiert sein (auf den Standard-Chrome-Pfaden, die Playwright kennt).
**Container-Deployment**: Das Microsoft Playwright Base-Image (`mcr.microsoft.com/playwright:v1.50.0-jammy`) liefert nur das gebündelte Chromium mit. Das `Dockerfile` installiert deshalb explizit das echte Google Chrome via `npx playwright install --with-deps chrome` (Playwright legt Chrome an einen ihm bekannten Pfad ab + apt-installiert alle System-Deps); `docker-compose.yml` setzt `BOT_BROWSER_CHANNEL=chrome` als Default-Env. Auf reinen VMs reicht ein OS-natives `apt-get install -y google-chrome-stable` aus dem Google-Repo, sofern die Chrome-Binary unter `/usr/bin/google-chrome[-stable]` landet.
Bisect-Historie 2026-05-12 (Diagnose-Bypässe bleiben als undokumentierte Debug-Schalter im Code, falls erneut nötig):
- `BOT_DISABLE_MEDIA_WRAPPERS=true` → Crash blieb. ⇒ Wrapper unschuldig.
- `BOT_ANON_USE_AUTH_BROWSER_SETUP=true` (= minimale Chromium-Args + kein Stealth-Init) → Crash blieb. ⇒ weder Anon-Args noch Stealth-Properties sind die Ursache.
- `BOT_BROWSER_CHANNEL=chrome` → Lobby weg, Crash weg, Audio-Capture läuft sauber. ⇒ root cause ist der Browser-Fingerprint.
### Statisches Avatar-Video (ersetzt grünen Lade-Spinner)
Wenn der Bot **kein** Video sendet, rendert Teams für andere Teilnehmer einen eigenen Platzhalter — eine grüne Fläche mit Initialen + Lade-Spinner, die nie zur Ruhe kommt. Mit `BOT_USE_CANVAS_VIDEO=true` schiebt der Bot stattdessen einen ruhigen, statischen Canvas-Stream als Video-Track in die WebRTC-Sender — eine einfarbige Fläche mit dem Anzeigenamen mittig, ohne Animation, ohne Branding.
Konfiguration (alle drei Env-Variablen optional, alles in `src/config.ts`):
| Env | Default | Bedeutung |
|---|---|---|
| `BOT_USE_CANVAS_VIDEO` | `false` | Schaltet das statische Avatar-Video ein. Empfohlen für den anonymen Bot. |
| `BOT_AVATAR_BG_COLOR` | `#a8d4f0` (hellblau) | CSS-Farbe der Hintergrundfläche. |
| `BOT_AVATAR_TEXT_COLOR` | `#1a3552` (dunkelblau) | CSS-Farbe des zentrierten Anzeigenamens. |
Implementierung in `src/bot/mediaGetUserMediaPatch.ts`:
- Canvas wird einmalig erzeugt (640×360), `draw()` zeichnet bei jedem Tick **identische** Pixel — keine Animation, kein Pulse, kein Gradient.
- Tick-Rate auf 2 fps gesenkt (vorher 15 fps Animation). Der Tick bleibt nötig, weil `captureStream()` in headless Chromium den Track sonst pausiert; Inhalt ist aber konstant, daher entstehen keine Frame-Diffs auf dem Encoder.
- Video-Track-`contentHint` von `'motion'` auf `'detail'` umgestellt — sagt dem WebRTC-Encoder "statisch", er reduziert Bitrate und behält Textschärfe.
- Farben werden vom `audioProcedure.ts` an den Init-Script-Payload durchgereicht und in `_startBotAvatarStream()` als `fillStyle` für Hintergrund + Label verwendet.
**Benutzerdefiniertes Avatar-Bild / Video:** Benutzer können in den Bot-Einstellungen (`TeamsbotConfig.avatarFileId`) oder pro Modul (`TeamsbotMeetingModule.defaultAvatarFileId`) eine Datei aus dem Dateisystem auswählen (Bilder: PNG, JPG, etc.; Videos: MP4, WebM, etc.). Modul-Einstellung überschreibt Instanz-Default. Beim Session-Start löst das Gateway die Datei auf, lädt sie als Base64 und sendet sie im Join-Payload (`avatarMediaData` + `avatarMediaType`) an den Bot. Der Bot rendert:
- **Bilder**: `drawImage()` auf den Canvas (statisch, 2 fps).
- **Videos**: Unsichtbares `<video>`-Element mit Loop, Frames werden per `drawImage()` auf den Canvas gezeichnet (15 fps, `contentHint='motion'`).
- **Kein File gesetzt**: Fallback auf statische Farbfläche + Bot-Name (bisheriges Verhalten).
Der Auth-Bot ist von der Änderung nur betroffen, wenn `BOT_USE_CANVAS_VIDEO=true` gesetzt ist; sonst sendet er weiterhin gar kein Video.
## Hybrid-Routing: SPEECH_TEAMS + Agent
Der Teamsbot läuft auf zwei kooperierenden Pfaden:
```mermaid
flowchart LR
Audio["Meeting Audio"] --> STT["STT"]
STT --> ST["SPEECH_TEAMS<br/>(fast, low-latency)"]
ST -->|shouldRespond=true| TTS["TTS + Chat"]
ST -->|needsAgent=true| Agent["agentService.runAgent<br/>(toolSet=core, web=on,<br/>maxRounds=5, maxCostCHF=0.10)"]
Agent -->|FINAL text| TTS
UI["Operator UI<br/>(Regie-Panel + UDB)"] -->|POST directorPrompt| Route["routeFeatureTeamsbot"]
Route -->|submitDirectorPrompt| Svc["TeamsbotService<br/>(_activeServices)"]
Svc -->|asyncio.create_task| Agent
TTS --> Meeting
Svc -->|SSE 'directorPrompt'| UI
```
- **`SPEECH_TEAMS`** bleibt der Default-Pfad mit niedrigster Latenz. Der dazugehörige System-Prompt erlaubt dem Modell explizit, `needsAgent=true` + `agentReason` zu setzen, wenn die Anfrage Web-Recherche, Mail oder Multi-Step-Tools erfordert.
- **Director Prompts** umgehen `SPEECH_TEAMS` komplett und feuern direkt einen `runAgent`-Lauf, dessen `FINAL`-Event-Text wieder über die bestehenden TTS-/Chat-Kanäle ins Meeting geliefert wird.
## Director Prompts (private Operator-Anweisungen)
Operator-Prompts sind **privat** (nur per SSE an den Session-Owner sichtbar) und werden in PostgreSQL gespeichert (`poweron_teamsbot.TeamsbotDirectorPrompt`).
| Modus | Verhalten |
|---|---|
| `oneShot` | Einmaliger Agent-Lauf, danach Status `consumed` |
| `persistent` | Agent-Lauf wird ausgeführt **und** der Text wird als `OPERATOR_DIRECTIVES`-Block in jeden folgenden `SPEECH_TEAMS`-Trigger eingemischt, bis der Operator den Prompt löscht |
| Lifecycle-Status | Bedeutung |
|---|---|
| `queued` | Eingereicht, Agent noch nicht gestartet |
| `running` | Agent läuft |
| `succeeded` | Agent fertig, persistent bleibt aktiv |
| `consumed` | One-Shot abgeschlossen oder persistent gelöscht |
| `failed` | Agent-Lauf fehlgeschlagen, persistent wird automatisch aus aktiven Direktiven entfernt |
**Persistenz beim Reconnect:** Bei jedem WebSocket-Reconnect ruft der Service `interface.getActivePersistentPrompts(sessionId)` auf und füllt `_activePersistentPrompts` neu, damit Direktiven Network-Drops überleben.
**Limits:** `DIRECTOR_PROMPT_TEXT_LIMIT = 8000` Zeichen, `DIRECTOR_PROMPT_FILE_LIMIT = 10` UDB-Dateien (Pflicht-Prüfung in der Route, validiert auch von `TeamsbotDirectorPromptCreateRequest`).
**RBAC:** Routes prüfen `_validateInstanceAccess` + `_validateSessionOwnership`. Ein Nicht-Owner sieht 404, niemals 403, um die Existenz der Session nicht preiszugeben.
**Rate-Limit:** `30/minute` pro Operator (slowapi).
## Schicht-Trennung (Plan #5 abgeschlossen 2026-04-24)
| Verantwortung | Datei |
|---|---|
| Persistenz + Lifecycle | `interfaceFeatureTeamsbot.py` (`createDirectorPrompt`, `getActivePersistentPrompts`, `updateDirectorPrompt`, `deleteDirectorPrompt`) |
| Orchestrierung + Agent-Lauf + SSE | `service.py` (`submitDirectorPrompt`, `_processDirectorPrompt`, `_runAgentForMeeting`, `_buildPersistentDirectorContext`, `removePersistentPrompt`) |
| HTTP + RBAC + Limits | `routeFeatureTeamsbot.py` (POST/GET/DELETE `/sessions/{id}/directorPrompts`) |
| Frontend Regie-Panel + UDB-Sidebar + SSE-Listener | `frontend_nyla/src/pages/views/teamsbot/TeamsbotSessionView.tsx` + `Teamsbot.module.css` |
| Frontend API-Wrapper | `frontend_nyla/src/api/teamsbotApi.ts` (`submitDirectorPrompt`, `listDirectorPrompts`, `deleteDirectorPrompt`) |
| Tests | `gateway/tests/unit/teamsbot/test_directorPrompts.py` (26 Tests, AC 5 + 6 abgedeckt; AC 14 manuell live) |