246 lines
22 KiB
Markdown
246 lines
22 KiB
Markdown
<!-- 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); platform-core/modules/features/teamsbot/{service,routeFeatureTeamsbot,interfaceFeatureTeamsbot,datamodelTeamsbot}.py + STT single-language (2026-05-12) + `GET /api/teamsbot/{instanceId}/dashboard/stream` (2026-05-11); platform-core/tests/unit/teamsbot/test_directorPrompts.py (Director Prompts 2026-04-24); gateway Voice STT batch + linear16 (2026-05-10); ui-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 |
|
||
|-----------------|-------|
|
||
| `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-<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 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 `<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 | `ui-nyla/src/pages/views/teamsbot/TeamsbotSessionView.tsx` + `Teamsbot.module.css` |
|
||
| Frontend API-Wrapper | `ui-nyla/src/api/teamsbotApi.ts` (`submitDirectorPrompt`, `listDirectorPrompts`, `deleteDirectorPrompt`) |
|
||
| Tests | `platform-core/tests/unit/teamsbot/test_directorPrompts.py` (26 Tests, AC 5 + 6 abgedeckt; AC 1–4 manuell live) |
|