From 6380f14ebe0b5736db4b9be0503d7f277001ddda Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Tue, 12 May 2026 19:16:34 +0200
Subject: [PATCH] teamsbot anonymous bot working
---
.../features/teamsbot/browserBotConnector.py | 8 +++-
.../features/teamsbot/datamodelTeamsbot.py | 9 ++++
.../teamsbot/interfaceFeatureTeamsbot.py | 3 ++
.../features/teamsbot/routeFeatureTeamsbot.py | 10 ++--
modules/features/teamsbot/service.py | 46 ++++++++++++++++++-
5 files changed, 69 insertions(+), 7 deletions(-)
diff --git a/modules/features/teamsbot/browserBotConnector.py b/modules/features/teamsbot/browserBotConnector.py
index 2e76d039..d99fe829 100644
--- a/modules/features/teamsbot/browserBotConnector.py
+++ b/modules/features/teamsbot/browserBotConnector.py
@@ -40,6 +40,8 @@ class BrowserBotConnector:
botAccountPassword: Optional[str] = None,
transferMode: str = "auto",
debugMode: bool = False,
+ avatarMediaData: Optional[str] = None,
+ avatarMediaType: Optional[str] = None,
) -> Dict[str, Any]:
"""
Send join command to the Browser Bot service.
@@ -79,12 +81,16 @@ class BrowserBotConnector:
"debugMode": debugMode,
}
- # Add authenticated join credentials if configured
if botAccountEmail and botAccountPassword:
payload["botAccountEmail"] = botAccountEmail
payload["botAccountPassword"] = botAccountPassword
logger.info(f"Bot will join authenticated as {botAccountEmail}")
+ if avatarMediaData and avatarMediaType:
+ payload["avatarMediaData"] = avatarMediaData
+ payload["avatarMediaType"] = avatarMediaType
+ logger.info(f"Avatar media attached: {avatarMediaType}, {len(avatarMediaData)} chars")
+
try:
async with aiohttp.ClientSession(timeout=_BOT_TIMEOUT) as session:
async with session.post(f"{self.botUrl}/api/bot", json=payload) as resp:
diff --git a/modules/features/teamsbot/datamodelTeamsbot.py b/modules/features/teamsbot/datamodelTeamsbot.py
index 076b0eda..18904525 100644
--- a/modules/features/teamsbot/datamodelTeamsbot.py
+++ b/modules/features/teamsbot/datamodelTeamsbot.py
@@ -119,6 +119,10 @@ class TeamsbotMeetingModule(PowerOnModel):
default=None,
description="Default display name for the bot when starting a session from this module",
)
+ defaultAvatarFileId: Optional[str] = Field(
+ default=None,
+ description="FileItem ID for the default avatar image/video shown in the meeting",
+ )
status: TeamsbotModuleStatus = Field(default=TeamsbotModuleStatus.ACTIVE)
@@ -225,6 +229,7 @@ class TeamsbotUserSettings(PowerOnModel):
triggerCooldownSeconds: Optional[int] = Field(default=None, description="Trigger cooldown override")
contextWindowSegments: Optional[int] = Field(default=None, description="Context window override")
debugMode: Optional[bool] = Field(default=None, description="Debug mode override")
+ avatarFileId: Optional[str] = Field(default=None, description="FileItem ID for bot avatar image/video override")
# ============================================================================
@@ -248,6 +253,7 @@ class TeamsbotConfig(BaseModel):
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")
+ avatarFileId: Optional[str] = Field(default=None, description="FileItem ID for bot avatar image/video shown in the meeting")
def _getEffectiveBrowserBotUrl(self) -> Optional[str]:
"""Resolve the effective browser bot URL: per-instance config takes priority, then env variable."""
@@ -288,6 +294,7 @@ class CreateMeetingModuleRequest(BaseModel):
kpiTargets: Optional[str] = None
defaultMeetingLink: Optional[str] = None
defaultBotName: Optional[str] = None
+ defaultAvatarFileId: Optional[str] = None
class UpdateMeetingModuleRequest(BaseModel):
@@ -300,6 +307,7 @@ class UpdateMeetingModuleRequest(BaseModel):
kpiTargets: Optional[str] = None
defaultMeetingLink: Optional[str] = None
defaultBotName: Optional[str] = None
+ defaultAvatarFileId: Optional[str] = None
status: Optional[TeamsbotModuleStatus] = None
@@ -317,6 +325,7 @@ class TeamsbotConfigUpdateRequest(BaseModel):
triggerCooldownSeconds: Optional[int] = None
contextWindowSegments: Optional[int] = None
debugMode: Optional[bool] = None
+ avatarFileId: Optional[str] = None
# ============================================================================
diff --git a/modules/features/teamsbot/interfaceFeatureTeamsbot.py b/modules/features/teamsbot/interfaceFeatureTeamsbot.py
index 8491b3b9..2bfe77ff 100644
--- a/modules/features/teamsbot/interfaceFeatureTeamsbot.py
+++ b/modules/features/teamsbot/interfaceFeatureTeamsbot.py
@@ -25,6 +25,7 @@ from .datamodelTeamsbot import (
TeamsbotDirectorPromptStatus,
TeamsbotDirectorPromptMode,
TeamsbotMeetingModule,
+ TeamsbotModuleStatus,
)
logger = logging.getLogger(__name__)
@@ -338,6 +339,8 @@ class TeamsbotObjects:
def getModules(self, instanceId: str) -> List[Dict[str, Any]]:
"""Get all meeting modules for a feature instance."""
records = self.db.getRecordset(TeamsbotMeetingModule, recordFilter={"instanceId": instanceId})
+ for r in records:
+ r.setdefault("status", TeamsbotModuleStatus.ACTIVE.value)
records.sort(key=lambda r: r.get("sysCreatedAt") or "", reverse=True)
return records
diff --git a/modules/features/teamsbot/routeFeatureTeamsbot.py b/modules/features/teamsbot/routeFeatureTeamsbot.py
index b3088f8e..f07c98c5 100644
--- a/modules/features/teamsbot/routeFeatureTeamsbot.py
+++ b/modules/features/teamsbot/routeFeatureTeamsbot.py
@@ -40,6 +40,7 @@ from .datamodelTeamsbot import (
TeamsbotDirectorPromptMode,
TeamsbotDirectorPromptStatus,
TeamsbotMeetingModule,
+ TeamsbotModuleStatus,
CreateMeetingModuleRequest,
UpdateMeetingModuleRequest,
DIRECTOR_PROMPT_FILE_LIMIT,
@@ -203,6 +204,7 @@ async def createModule(
data["instanceId"] = instanceId
data["mandateId"] = mandateId
data["ownerUserId"] = str(context.user.id)
+ data.setdefault("status", TeamsbotModuleStatus.ACTIVE.value)
module = interface.createModule(data)
return {"module": module}
@@ -688,12 +690,10 @@ def _getEffectiveConfig(instanceId: str, userId: str, interface) -> TeamsbotConf
if not userSettings:
return baseConfig
- # Merge: user settings override instance defaults (only non-None values)
+ # Merge: user settings override instance defaults (only non-None values).
+ # Derive mergeable fields from TeamsbotConfig so new fields are picked up automatically.
overrides = {}
- for field in ["botName", "aiSystemPrompt", "responseMode",
- "responseChannel", "transferMode", "language", "voiceId",
- "triggerIntervalSeconds", "triggerCooldownSeconds", "contextWindowSegments",
- "debugMode"]:
+ for field in TeamsbotConfig.model_fields:
value = userSettings.get(field)
if value is not None:
overrides[field] = value
diff --git a/modules/features/teamsbot/service.py b/modules/features/teamsbot/service.py
index 8017e6dc..2136d8e0 100644
--- a/modules/features/teamsbot/service.py
+++ b/modules/features/teamsbot/service.py
@@ -732,6 +732,12 @@ class TeamsbotService:
hasAuth = bool(botAccountEmail and botAccountPassword)
logger.info(f"Joining meeting for session {sessionId}: auth={hasAuth}, email={botAccountEmail or 'N/A'}, transferMode={self.config.transferMode}")
+ avatarMediaData = None
+ avatarMediaType = None
+ avatarFileId = self._resolveAvatarFileId(session, interface)
+ if avatarFileId:
+ avatarMediaData, avatarMediaType = self._loadAvatarFileData(avatarFileId, interface)
+
result = await self.browserBotConnector.joinMeeting(
sessionId=sessionId,
meetingUrl=meetingLink,
@@ -743,6 +749,8 @@ class TeamsbotService:
botAccountPassword=botAccountPassword,
transferMode=self.config.transferMode if hasattr(self.config, 'transferMode') else "auto",
debugMode=self.config.debugMode if hasattr(self.config, 'debugMode') else False,
+ avatarMediaData=avatarMediaData,
+ avatarMediaType=avatarMediaType,
)
if result.get("success"):
@@ -767,6 +775,37 @@ class TeamsbotService:
})
await _emitSessionEvent(sessionId, "statusChange", {"status": "error", "errorMessage": str(e)})
+ def _resolveAvatarFileId(self, session, interface):
+ """Resolve avatarFileId: module override > config default."""
+ moduleId = session.get("moduleId")
+ if moduleId:
+ module = interface.getModule(moduleId)
+ if module and module.get("defaultAvatarFileId"):
+ return module["defaultAvatarFileId"]
+ return getattr(self.config, "avatarFileId", None)
+
+ def _loadAvatarFileData(self, fileId, _teamsbotInterface):
+ """Load avatar file as base64 data + mime type. Returns (data, mimeType) or (None, None)."""
+ import base64
+ from modules.interfaces import interfaceDbManagement
+ try:
+ mgmt = interfaceDbManagement.getInterface(self.currentUser, self.mandateId)
+ fileRecord = mgmt.getFile(fileId)
+ if not fileRecord:
+ logger.warning(f"Avatar file {fileId} not found")
+ return None, None
+ mimeType = getattr(fileRecord, "mimeType", None) or "image/png"
+ rawBytes = mgmt.getFileData(fileId)
+ if not rawBytes:
+ logger.warning(f"Avatar file {fileId} has no data")
+ return None, None
+ b64 = base64.b64encode(rawBytes).decode("ascii")
+ logger.info(f"Avatar file loaded: {fileId}, {mimeType}, {len(b64)} chars base64")
+ return b64, mimeType
+ except Exception as e:
+ logger.error(f"Failed to load avatar file {fileId}: {e}")
+ return None, None
+
async def leaveMeeting(self, sessionId: str):
"""Send leave command to the Browser Bot service."""
from . import interfaceFeatureTeamsbot as interfaceDb
@@ -1217,6 +1256,12 @@ class TeamsbotService:
if self.config.botName:
phraseHints.append(self.config.botName)
+ # Sprache kommt ausschliesslich aus der Session/Instance-Konfig
+ # (TeamsbotUserSettings.language ueberschreibt
+ # TeamsbotConfig.language, Fallback de-DE im Schema).
+ # KEIN hardcodierter Alternative-Sprachen-Pool — der hat dafuer
+ # gesorgt, dass Google STT bei verrauschter Audio auf en-US
+ # gesprungen ist und englisches Kauderwelsch geliefert hat.
sttResult = await voiceInterface.speechToText(
audioContent=audioBytes,
language=self.config.language or "de-DE",
@@ -1224,7 +1269,6 @@ class TeamsbotService:
channels=1,
skipFallbacks=True,
phraseHints=phraseHints if phraseHints else None,
- alternativeLanguages=["en-US"],
audioFormat="linear16",
)