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", )