teamsbot anonymous bot working

This commit is contained in:
ValueOn AG 2026-05-12 19:16:34 +02:00
parent c130f49cf9
commit 6380f14ebe
5 changed files with 69 additions and 7 deletions

View file

@ -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:

View file

@ -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
# ============================================================================

View file

@ -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

View file

@ -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

View file

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