feat: add debugMode to config, filter bot own captions to prevent repeats

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
patrick-motsch 2026-02-18 23:52:51 +01:00
parent b616688411
commit 7778325e5e
6 changed files with 283 additions and 148 deletions

View file

@ -39,6 +39,7 @@ class BrowserBotConnector:
botAccountEmail: Optional[str] = None,
botAccountPassword: Optional[str] = None,
transferMode: str = "auto",
debugMode: bool = False,
) -> Dict[str, Any]:
"""
Send join command to the Browser Bot service.
@ -75,6 +76,7 @@ class BrowserBotConnector:
"gatewayWsUrl": gatewayWsUrl,
"language": language,
"transferMode": transferMode,
"debugMode": debugMode,
}
# Add authenticated join credentials if configured

View file

@ -186,6 +186,7 @@ class TeamsbotConfig(BaseModel):
triggerIntervalSeconds: int = Field(default=10, ge=3, le=60, description="Seconds between periodic AI analysis triggers")
triggerCooldownSeconds: int = Field(default=3, ge=1, le=30, description="Minimum seconds between AI calls")
contextWindowSegments: int = Field(default=20, ge=5, le=100, description="Number of transcript segments to include in AI context")
debugMode: bool = Field(default=False, description="Enable debug mode: screenshots at every join step for diagnostics")
def _getEffectiveBrowserBotUrl(self) -> Optional[str]:
"""Resolve the effective browser bot URL: per-instance config takes priority, then env variable."""
@ -228,6 +229,7 @@ class TeamsbotConfigUpdateRequest(BaseModel):
triggerIntervalSeconds: Optional[int] = None
triggerCooldownSeconds: Optional[int] = None
contextWindowSegments: Optional[int] = None
debugMode: Optional[bool] = None
# ============================================================================

View file

@ -146,6 +146,7 @@ class TeamsbotService:
botAccountEmail=botAccountEmail,
botAccountPassword=botAccountPassword,
transferMode=self.config.transferMode if hasattr(self.config, 'transferMode') else "auto",
debugMode=self.config.debugMode if hasattr(self.config, 'debugMode') else False,
)
if result.get("success"):
@ -458,8 +459,13 @@ class TeamsbotService:
if not text:
return
# Filter out the bot's own speech from AI triggering.
# Filter out the bot's own speech entirely — captions of the bot's
# own voice come back as garbled text (e.g. German TTS → English caption)
# which pollutes the context buffer and confuses AI analysis.
isBotSpeaker = self._isBotSpeaker(speaker)
if isBotSpeaker:
logger.debug(f"Session {sessionId}: Ignoring own bot caption from: [{speaker}] {text[:80]}...")
return
# Differential transcript writing:
# If the same speaker is still talking and the new text is a
@ -538,11 +544,6 @@ class TeamsbotService:
"isContinuation": isContinuation,
})
# Skip AI analysis for bot's own speech (prevents feedback loop)
if isBotSpeaker:
logger.debug(f"Session {sessionId}: Skipping AI trigger for bot's own speech: [{speaker}] {text[:60]}...")
return
# Check if AI analysis should be triggered (only for final transcripts)
if not isFinal:
return

View file

@ -531,6 +531,34 @@ def getEffectivePermissions(
try:
interface = getRootInterface()
# MandateAdmin: verify the requested user shares at least one admin mandate
if not context.hasSysAdminRole:
adminMandateIds = []
adminUserMandates = interface.getUserMandates(str(context.user.id))
for um in adminUserMandates:
umId = getattr(um, 'id', None)
mid = getattr(um, 'mandateId', None)
if not umId or not mid:
continue
roleIds = interface.getRoleIdsForUserMandate(str(umId))
for roleId in roleIds:
role = interface.getRole(roleId)
if role and role.roleLabel == "admin" and not role.featureInstanceId:
adminMandateIds.append(str(mid))
break
if not adminMandateIds:
raise HTTPException(status_code=403, detail="Insufficient permissions")
userInAdminMandate = False
for mid in adminMandateIds:
if _isUserInMandate(interface, userId, mid):
userInAdminMandate = True
break
if not userInAdminMandate:
raise HTTPException(status_code=403, detail="Benutzer gehört nicht zu Ihrem Mandate")
# Get user
user = interface.getUser(userId)
if not user:

View file

@ -336,6 +336,33 @@ def _getInstanceViewPermissions(
return permissions # Fail-safe: no permissions on error
def _filterItems(
items: List[Dict[str, Any]],
language: str,
isSysAdmin: bool,
roleIds: List[str],
hasGlobalPermission: bool
) -> List[Dict[str, Any]]:
"""Filter and format navigation items based on permissions."""
filteredItems = []
for item in items:
if item.get("adminOnly") and not isSysAdmin:
if not hasGlobalPermission and not _checkUiPermission(roleIds, item["objectKey"]):
continue
if item.get("sysAdminOnly") and not isSysAdmin:
continue
if item.get("public"):
filteredItems.append(_formatBlockItem(item, language))
continue
if isSysAdmin:
filteredItems.append(_formatBlockItem(item, language))
continue
if hasGlobalPermission or _checkUiPermission(roleIds, item["objectKey"]):
filteredItems.append(_formatBlockItem(item, language))
filteredItems.sort(key=lambda i: i["order"])
return filteredItems
def _buildStaticBlocks(
language: str,
isSysAdmin: bool,
@ -346,40 +373,54 @@ def _buildStaticBlocks(
Build static navigation blocks from NAVIGATION_SECTIONS.
Returns list of blocks with items filtered by permissions.
Supports subgroups within sections.
"""
blocks = []
for section in NAVIGATION_SECTIONS:
# Filter items based on UI AccessRules (ui.admin.*, ui.billing.*, etc.)
filteredItems = []
for item in section.get("items", []):
# Public items are always visible
if item.get("public"):
filteredItems.append(_formatBlockItem(item, language))
continue
# SysAdmin-only items (e.g. automation-events) require isSysAdmin
if item.get("sysAdminOnly") and not isSysAdmin:
continue
# Check UI AccessRule for this objectKey
# Roles with item=None rule (e.g. sysadmin) get access to everything
# Roles with specific ui.admin.* rules get access to those items
if hasGlobalPermission or _checkUiPermission(roleIds, item["objectKey"]):
filteredItems.append(_formatBlockItem(item, language))
if section.get("adminOnly") and not isSysAdmin:
continue
# Only include section if it has visible items
if filteredItems:
# Sort items by order
filteredItems.sort(key=lambda i: i["order"])
# Handle sections with subgroups
if "subgroups" in section:
filteredSubgroups = []
for subgroup in section["subgroups"]:
subItems = _filterItems(
subgroup.get("items", []), language, isSysAdmin, roleIds, hasGlobalPermission
)
if subItems:
filteredSubgroups.append({
"id": subgroup["id"],
"title": subgroup["title"].get(language, subgroup["title"].get("en", subgroup["id"])),
"order": subgroup.get("order", 50),
"items": subItems,
})
blocks.append({
"type": "static",
"id": section["id"],
"title": section["title"].get(language, section["title"].get("en", section["id"])),
"order": section.get("order", 50),
"items": filteredItems,
})
filteredSubgroups.sort(key=lambda s: s["order"])
if filteredSubgroups:
blocks.append({
"type": "static",
"id": section["id"],
"title": section["title"].get(language, section["title"].get("en", section["id"])),
"order": section.get("order", 50),
"items": [],
"subgroups": filteredSubgroups,
})
else:
# Standard flat section
filteredItems = _filterItems(
section.get("items", []), language, isSysAdmin, roleIds, hasGlobalPermission
)
if filteredItems:
blocks.append({
"type": "static",
"id": section["id"],
"title": section["title"].get(language, section["title"].get("en", section["id"])),
"order": section.get("order", 50),
"items": filteredItems,
})
return blocks

View file

@ -106,132 +106,176 @@ NAVIGATION_SECTIONS = [
},
],
},
# ─── Administration (with subgroups) ───
# Access control is at item level, NOT section level.
# Groups auto-hide if 0 visible pages for the user.
{
"id": "admin",
"title": {"en": "ADMINISTRATION", "de": "ADMINISTRATION", "fr": "ADMINISTRATION"},
"order": 200,
"adminOnly": True,
"items": [
"subgroups": [
# ── Wizards ──
{
"id": "admin-users",
"objectKey": "ui.admin.users",
"label": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"},
"icon": "FaUsers",
"path": "/admin/users",
"id": "admin-wizards",
"title": {"en": "Wizards", "de": "Wizards", "fr": "Assistants"},
"order": 10,
"adminOnly": True,
"items": [
{
"id": "admin-mandate-wizard",
"objectKey": "ui.admin.mandateWizard",
"label": {"en": "Mandate Wizard", "de": "Mandanten-Wizard", "fr": "Assistant mandat"},
"icon": "FaMagic",
"path": "/admin/mandate-wizard",
"order": 10,
"adminOnly": True,
},
{
"id": "admin-invitation-wizard",
"objectKey": "ui.admin.invitationWizard",
"label": {"en": "Invitation Wizard", "de": "Einladungs-Wizard", "fr": "Assistant d'invitation"},
"icon": "FaEnvelopeOpenText",
"path": "/admin/invitation-wizard",
"order": 20,
"adminOnly": True,
},
],
},
# ── Users ──
{
"id": "admin-invitations",
"objectKey": "ui.admin.invitations",
"label": {"en": "User Invitations", "de": "Benutzer-Einladungen", "fr": "Invitations utilisateurs"},
"icon": "FaEnvelopeOpenText",
"path": "/admin/invitations",
"order": 12,
"adminOnly": True,
},
{
"id": "admin-user-access-overview",
"objectKey": "ui.admin.userAccessOverview",
"label": {"en": "User Access Overview", "de": "Benutzer-Zugriffsübersicht", "fr": "Aperçu des accès utilisateur"},
"icon": "FaClipboardList",
"path": "/admin/user-access-overview",
"order": 14,
"adminOnly": True,
},
{
"id": "admin-mandates",
"objectKey": "ui.admin.mandates",
"label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"},
"icon": "FaBuilding",
"path": "/admin/mandates",
"id": "admin-users-group",
"title": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"},
"order": 20,
"adminOnly": True,
"items": [
{
"id": "admin-users",
"objectKey": "ui.admin.users",
"label": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"},
"icon": "FaUsers",
"path": "/admin/users",
"order": 10,
"adminOnly": True,
},
{
"id": "admin-invitations",
"objectKey": "ui.admin.invitations",
"label": {"en": "User Invitations", "de": "Benutzer-Einladungen", "fr": "Invitations utilisateurs"},
"icon": "FaEnvelopeOpenText",
"path": "/admin/invitations",
"order": 20,
"adminOnly": True,
},
{
"id": "admin-user-access-overview",
"objectKey": "ui.admin.userAccessOverview",
"label": {"en": "User Access Overview", "de": "Benutzer-Zugriffsübersicht", "fr": "Aperçu des accès utilisateur"},
"icon": "FaClipboardList",
"path": "/admin/user-access-overview",
"order": 30,
"adminOnly": True,
},
{
"id": "admin-billing",
"objectKey": "ui.admin.billing",
"label": {"en": "Billing Administration", "de": "Billing-Verwaltung", "fr": "Administration de facturation"},
"icon": "FaMoneyBillAlt",
"path": "/admin/billing",
"order": 40,
"adminOnly": True,
"sysAdminOnly": True,
},
],
},
# ── System ──
{
"id": "admin-user-mandates",
"objectKey": "ui.admin.userMandates",
"label": {"en": "Mandate Members", "de": "Mandanten-Mitglieder", "fr": "Membres du mandat"},
"icon": "FaUserFriends",
"path": "/admin/user-mandates",
"order": 25,
"adminOnly": True,
},
{
"id": "admin-access",
"objectKey": "ui.admin.access",
"label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"},
"icon": "FaBuilding",
"path": "/admin/access",
"id": "admin-system-group",
"title": {"en": "System", "de": "System", "fr": "Système"},
"order": 30,
"adminOnly": True,
},
{
"id": "admin-roles",
"objectKey": "ui.admin.roles",
"label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
"icon": "FaUserTag",
"path": "/admin/mandate-roles",
"order": 40,
"adminOnly": True,
},
{
"id": "admin-mandate-role-permissions",
"objectKey": "ui.admin.mandateRolePermissions",
"label": {"en": "Role Permissions", "de": "Rollen-Berechtigungen", "fr": "Permissions des rôles"},
"icon": "FaKey",
"path": "/admin/mandate-role-permissions",
"order": 45,
"adminOnly": True,
},
{
"id": "admin-feature-instances",
"objectKey": "ui.admin.featureInstances",
"label": {"en": "Feature Instances", "de": "Feature-Instanzen", "fr": "Instances de features"},
"icon": "FaCubes",
"path": "/admin/feature-instances",
"order": 48,
"adminOnly": True,
},
{
"id": "admin-feature-roles",
"objectKey": "ui.admin.featureRoles",
"label": {"en": "Feature Role Templates", "de": "Features Rollen-Vorlagen", "fr": "Modèles de rôles features"},
"icon": "FaShieldAlt",
"path": "/admin/feature-roles",
"order": 50,
"adminOnly": True,
"sysAdminOnly": True,
},
{
"id": "admin-billing",
"objectKey": "ui.admin.billing",
"label": {"en": "Billing Administration", "de": "Billing-Verwaltung", "fr": "Administration de facturation"},
"icon": "FaMoneyBillAlt",
"path": "/admin/billing",
"order": 60,
"adminOnly": True,
"sysAdminOnly": True,
},
{
"id": "admin-automation-events",
"objectKey": "ui.admin.automationEvents",
"label": {"en": "Automation Events", "de": "Automation Events", "fr": "Événements d'automatisation"},
"icon": "FaClock",
"path": "/admin/automation-events",
"order": 65,
"adminOnly": True,
"sysAdminOnly": True,
},
{
"id": "admin-logs",
"objectKey": "ui.admin.logs",
"label": {"en": "Logs", "de": "Logs", "fr": "Logs"},
"icon": "FaFileAlt",
"path": "/admin/logs",
"order": 70,
"adminOnly": True,
"sysAdminOnly": True,
"items": [
{
"id": "admin-roles",
"objectKey": "ui.admin.roles",
"label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
"icon": "FaUserTag",
"path": "/admin/mandate-roles",
"order": 10,
"adminOnly": True,
},
{
"id": "admin-mandate-role-permissions",
"objectKey": "ui.admin.mandateRolePermissions",
"label": {"en": "Role Permissions", "de": "Rollen-Berechtigungen", "fr": "Permissions des rôles"},
"icon": "FaKey",
"path": "/admin/mandate-role-permissions",
"order": 20,
"adminOnly": True,
},
{
"id": "admin-mandates",
"objectKey": "ui.admin.mandates",
"label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"},
"icon": "FaBuilding",
"path": "/admin/mandates",
"order": 30,
"adminOnly": True,
},
{
"id": "admin-user-mandates",
"objectKey": "ui.admin.userMandates",
"label": {"en": "Mandate Members", "de": "Mandanten-Mitglieder", "fr": "Membres du mandat"},
"icon": "FaUserFriends",
"path": "/admin/user-mandates",
"order": 40,
"adminOnly": True,
},
{
"id": "admin-access",
"objectKey": "ui.admin.access",
"label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"},
"icon": "FaBuilding",
"path": "/admin/access",
"order": 50,
"adminOnly": True,
},
{
"id": "admin-feature-instances",
"objectKey": "ui.admin.featureInstances",
"label": {"en": "Feature Instances", "de": "Feature-Instanzen", "fr": "Instances de features"},
"icon": "FaCubes",
"path": "/admin/feature-instances",
"order": 60,
"adminOnly": True,
},
{
"id": "admin-feature-roles",
"objectKey": "ui.admin.featureRoles",
"label": {"en": "Feature Role Templates", "de": "Features Rollen-Vorlagen", "fr": "Modèles de rôles features"},
"icon": "FaShieldAlt",
"path": "/admin/feature-roles",
"order": 70,
"adminOnly": True,
"sysAdminOnly": True,
},
{
"id": "admin-automation-events",
"objectKey": "ui.admin.automationEvents",
"label": {"en": "Automation Events", "de": "Automation Events", "fr": "Événements d'automatisation"},
"icon": "FaClock",
"path": "/admin/automation-events",
"order": 80,
"adminOnly": True,
"sysAdminOnly": True,
},
{
"id": "admin-logs",
"objectKey": "ui.admin.logs",
"label": {"en": "Logs", "de": "Logs", "fr": "Logs"},
"icon": "FaFileAlt",
"path": "/admin/logs",
"order": 90,
"adminOnly": True,
"sysAdminOnly": True,
},
],
},
],
},
@ -255,6 +299,7 @@ def _buildUiObjectsFromNavigation() -> List[Dict[str, Any]]:
"""Build UI_OBJECTS list from NAVIGATION_SECTIONS for RBAC registration."""
uiObjects = []
for section in NAVIGATION_SECTIONS:
# Process direct items
for item in section.get("items", []):
uiObjects.append({
"objectKey": item["objectKey"],
@ -268,6 +313,22 @@ def _buildUiObjectsFromNavigation() -> List[Dict[str, Any]]:
"icon": item["icon"],
}
})
# Process subgroups (nested items within section)
for subgroup in section.get("subgroups", []):
for item in subgroup.get("items", []):
uiObjects.append({
"objectKey": item["objectKey"],
"label": item["label"],
"meta": {
"area": section["id"],
"subgroup": subgroup["id"],
"public": item.get("public", False),
"adminOnly": item.get("adminOnly", False),
"deprecated": item.get("deprecated", False),
"path": item["path"],
"icon": item["icon"],
}
})
return uiObjects