# 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=` 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 | |-----------------|-------| | `platform-core/modules/features/teamsbot/` | Gateway-seitiges Feature-Modul (inkl. `dashboard/stream`, Session-SSE, Module-CRUD) | | `ui-nyla/src/pages/views/teamsbot/` | Dashboard (SSE), Assistent, Module, Live-Session, Einstellungen | | `ui-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](../platform-core/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.3–0.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-` 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 MEHRERE sichtbare Buttons mit "Chat" im aria-label. Kritisch ist die Unterscheidung: 1. der **echte Meeting-Chat-Toggle**: UUID-id, `aria-label="Chat (Ctrl+Shift+2)"`, **`aria-pressed="true|false"`** — toggelt das Meeting-Chat-Side-Panel. 2. **Sidebar-Navigation "Chats"**: `aria-label="Chats "` (mit Leerzeichen), **hat `aria-pressed`** — klickt man diesen, navigiert der Bot **weg vom Meeting** zur Teams-Chat-Sektion. Das Meeting wird als PiP minimiert, alle PeerConnections schließen → Video/Audio bricht ab. 3. Weitere Buttons: `More chat options` (Menü), `tab-item-com.microsoft.chattabs.*` (Tabs im Chat-Panel), `chat-join-button`, `chat-header-participant-count`. 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). - **Filtert gefährliche Buttons aus** (`isDangerousNavButton`): Sidebar-Navigation (`aria="Chats"`/`"Unterhaltungen"`), Tab-Items (`tab-item-*`), Submenu-Triggers (`More chat options`), Participant-Count, Chat-Join-Button, App-Bar-Kinder. - **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. - **Nach Toggle-Klick**: wenn `_isChatPanelOpen()` DOM-basiert fehlschlägt, prüft Fallback ob `aria-pressed` des geklickten Toggles jetzt `"true"` ist → Panel gilt als offen (auth-mode Fallback). Refs: `service-teams-browser-bot/src/bot/chatProcedure.ts._openChatPanel`. ### Chat-Panel-Detection `_isChatPanelOpen()` macht zwei Strategie-Checks: **Strategie 1 (light-meetings + standard)**: Check am `[data-tid="calling-right-side-panel"]`-Container: 1. **Existenz + echte Visibility**: `offsetWidth/Height > 0 && offsetParent !== null`. 2. **Mode-Disambiguation per chat-spezifischen Child-Tids**: `message-pane-layout`, `message-pane-body`, `chat-pane-compose-message-footer`, `message-pane-footer`, `#chat-pane-list`, `[data-app-name="chats"]`. **Strategie 2 (auth full-Teams Fallback)**: Im vollen Teams-Web-Client existiert `calling-right-side-panel` möglicherweise nicht oder hat ein anderes Layout. Stattdessen wird geprüft ob ein Toggle-Button mit Keyboard-Shortcut im Label (`Chat (Ctrl+...)` / `Chat (Strg+...)`) `aria-pressed="true"` hat. Dies erkennt den geöffneten Zustand zuverlässig auch wenn der DOM-Container anders strukturiert ist. #### 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 `