teamsbot anonymous bot working
This commit is contained in:
parent
c130f49cf9
commit
6380f14ebe
5 changed files with 69 additions and 7 deletions
|
|
@ -40,6 +40,8 @@ class BrowserBotConnector:
|
||||||
botAccountPassword: Optional[str] = None,
|
botAccountPassword: Optional[str] = None,
|
||||||
transferMode: str = "auto",
|
transferMode: str = "auto",
|
||||||
debugMode: bool = False,
|
debugMode: bool = False,
|
||||||
|
avatarMediaData: Optional[str] = None,
|
||||||
|
avatarMediaType: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Send join command to the Browser Bot service.
|
Send join command to the Browser Bot service.
|
||||||
|
|
@ -79,12 +81,16 @@ class BrowserBotConnector:
|
||||||
"debugMode": debugMode,
|
"debugMode": debugMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add authenticated join credentials if configured
|
|
||||||
if botAccountEmail and botAccountPassword:
|
if botAccountEmail and botAccountPassword:
|
||||||
payload["botAccountEmail"] = botAccountEmail
|
payload["botAccountEmail"] = botAccountEmail
|
||||||
payload["botAccountPassword"] = botAccountPassword
|
payload["botAccountPassword"] = botAccountPassword
|
||||||
logger.info(f"Bot will join authenticated as {botAccountEmail}")
|
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:
|
try:
|
||||||
async with aiohttp.ClientSession(timeout=_BOT_TIMEOUT) as session:
|
async with aiohttp.ClientSession(timeout=_BOT_TIMEOUT) as session:
|
||||||
async with session.post(f"{self.botUrl}/api/bot", json=payload) as resp:
|
async with session.post(f"{self.botUrl}/api/bot", json=payload) as resp:
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,10 @@ class TeamsbotMeetingModule(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Default display name for the bot when starting a session from this module",
|
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)
|
status: TeamsbotModuleStatus = Field(default=TeamsbotModuleStatus.ACTIVE)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -225,6 +229,7 @@ class TeamsbotUserSettings(PowerOnModel):
|
||||||
triggerCooldownSeconds: Optional[int] = Field(default=None, description="Trigger cooldown override")
|
triggerCooldownSeconds: Optional[int] = Field(default=None, description="Trigger cooldown override")
|
||||||
contextWindowSegments: Optional[int] = Field(default=None, description="Context window override")
|
contextWindowSegments: Optional[int] = Field(default=None, description="Context window override")
|
||||||
debugMode: Optional[bool] = Field(default=None, description="Debug mode 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")
|
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")
|
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")
|
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]:
|
def _getEffectiveBrowserBotUrl(self) -> Optional[str]:
|
||||||
"""Resolve the effective browser bot URL: per-instance config takes priority, then env variable."""
|
"""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
|
kpiTargets: Optional[str] = None
|
||||||
defaultMeetingLink: Optional[str] = None
|
defaultMeetingLink: Optional[str] = None
|
||||||
defaultBotName: Optional[str] = None
|
defaultBotName: Optional[str] = None
|
||||||
|
defaultAvatarFileId: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateMeetingModuleRequest(BaseModel):
|
class UpdateMeetingModuleRequest(BaseModel):
|
||||||
|
|
@ -300,6 +307,7 @@ class UpdateMeetingModuleRequest(BaseModel):
|
||||||
kpiTargets: Optional[str] = None
|
kpiTargets: Optional[str] = None
|
||||||
defaultMeetingLink: Optional[str] = None
|
defaultMeetingLink: Optional[str] = None
|
||||||
defaultBotName: Optional[str] = None
|
defaultBotName: Optional[str] = None
|
||||||
|
defaultAvatarFileId: Optional[str] = None
|
||||||
status: Optional[TeamsbotModuleStatus] = None
|
status: Optional[TeamsbotModuleStatus] = None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -317,6 +325,7 @@ class TeamsbotConfigUpdateRequest(BaseModel):
|
||||||
triggerCooldownSeconds: Optional[int] = None
|
triggerCooldownSeconds: Optional[int] = None
|
||||||
contextWindowSegments: Optional[int] = None
|
contextWindowSegments: Optional[int] = None
|
||||||
debugMode: Optional[bool] = None
|
debugMode: Optional[bool] = None
|
||||||
|
avatarFileId: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from .datamodelTeamsbot import (
|
||||||
TeamsbotDirectorPromptStatus,
|
TeamsbotDirectorPromptStatus,
|
||||||
TeamsbotDirectorPromptMode,
|
TeamsbotDirectorPromptMode,
|
||||||
TeamsbotMeetingModule,
|
TeamsbotMeetingModule,
|
||||||
|
TeamsbotModuleStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -338,6 +339,8 @@ class TeamsbotObjects:
|
||||||
def getModules(self, instanceId: str) -> List[Dict[str, Any]]:
|
def getModules(self, instanceId: str) -> List[Dict[str, Any]]:
|
||||||
"""Get all meeting modules for a feature instance."""
|
"""Get all meeting modules for a feature instance."""
|
||||||
records = self.db.getRecordset(TeamsbotMeetingModule, recordFilter={"instanceId": instanceId})
|
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)
|
records.sort(key=lambda r: r.get("sysCreatedAt") or "", reverse=True)
|
||||||
return records
|
return records
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ from .datamodelTeamsbot import (
|
||||||
TeamsbotDirectorPromptMode,
|
TeamsbotDirectorPromptMode,
|
||||||
TeamsbotDirectorPromptStatus,
|
TeamsbotDirectorPromptStatus,
|
||||||
TeamsbotMeetingModule,
|
TeamsbotMeetingModule,
|
||||||
|
TeamsbotModuleStatus,
|
||||||
CreateMeetingModuleRequest,
|
CreateMeetingModuleRequest,
|
||||||
UpdateMeetingModuleRequest,
|
UpdateMeetingModuleRequest,
|
||||||
DIRECTOR_PROMPT_FILE_LIMIT,
|
DIRECTOR_PROMPT_FILE_LIMIT,
|
||||||
|
|
@ -203,6 +204,7 @@ async def createModule(
|
||||||
data["instanceId"] = instanceId
|
data["instanceId"] = instanceId
|
||||||
data["mandateId"] = mandateId
|
data["mandateId"] = mandateId
|
||||||
data["ownerUserId"] = str(context.user.id)
|
data["ownerUserId"] = str(context.user.id)
|
||||||
|
data.setdefault("status", TeamsbotModuleStatus.ACTIVE.value)
|
||||||
module = interface.createModule(data)
|
module = interface.createModule(data)
|
||||||
return {"module": module}
|
return {"module": module}
|
||||||
|
|
||||||
|
|
@ -688,12 +690,10 @@ def _getEffectiveConfig(instanceId: str, userId: str, interface) -> TeamsbotConf
|
||||||
if not userSettings:
|
if not userSettings:
|
||||||
return baseConfig
|
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 = {}
|
overrides = {}
|
||||||
for field in ["botName", "aiSystemPrompt", "responseMode",
|
for field in TeamsbotConfig.model_fields:
|
||||||
"responseChannel", "transferMode", "language", "voiceId",
|
|
||||||
"triggerIntervalSeconds", "triggerCooldownSeconds", "contextWindowSegments",
|
|
||||||
"debugMode"]:
|
|
||||||
value = userSettings.get(field)
|
value = userSettings.get(field)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
overrides[field] = value
|
overrides[field] = value
|
||||||
|
|
|
||||||
|
|
@ -732,6 +732,12 @@ class TeamsbotService:
|
||||||
hasAuth = bool(botAccountEmail and botAccountPassword)
|
hasAuth = bool(botAccountEmail and botAccountPassword)
|
||||||
logger.info(f"Joining meeting for session {sessionId}: auth={hasAuth}, email={botAccountEmail or 'N/A'}, transferMode={self.config.transferMode}")
|
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(
|
result = await self.browserBotConnector.joinMeeting(
|
||||||
sessionId=sessionId,
|
sessionId=sessionId,
|
||||||
meetingUrl=meetingLink,
|
meetingUrl=meetingLink,
|
||||||
|
|
@ -743,6 +749,8 @@ class TeamsbotService:
|
||||||
botAccountPassword=botAccountPassword,
|
botAccountPassword=botAccountPassword,
|
||||||
transferMode=self.config.transferMode if hasattr(self.config, 'transferMode') else "auto",
|
transferMode=self.config.transferMode if hasattr(self.config, 'transferMode') else "auto",
|
||||||
debugMode=self.config.debugMode if hasattr(self.config, 'debugMode') else False,
|
debugMode=self.config.debugMode if hasattr(self.config, 'debugMode') else False,
|
||||||
|
avatarMediaData=avatarMediaData,
|
||||||
|
avatarMediaType=avatarMediaType,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
|
|
@ -767,6 +775,37 @@ class TeamsbotService:
|
||||||
})
|
})
|
||||||
await _emitSessionEvent(sessionId, "statusChange", {"status": "error", "errorMessage": str(e)})
|
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):
|
async def leaveMeeting(self, sessionId: str):
|
||||||
"""Send leave command to the Browser Bot service."""
|
"""Send leave command to the Browser Bot service."""
|
||||||
from . import interfaceFeatureTeamsbot as interfaceDb
|
from . import interfaceFeatureTeamsbot as interfaceDb
|
||||||
|
|
@ -1217,6 +1256,12 @@ class TeamsbotService:
|
||||||
if self.config.botName:
|
if self.config.botName:
|
||||||
phraseHints.append(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(
|
sttResult = await voiceInterface.speechToText(
|
||||||
audioContent=audioBytes,
|
audioContent=audioBytes,
|
||||||
language=self.config.language or "de-DE",
|
language=self.config.language or "de-DE",
|
||||||
|
|
@ -1224,7 +1269,6 @@ class TeamsbotService:
|
||||||
channels=1,
|
channels=1,
|
||||||
skipFallbacks=True,
|
skipFallbacks=True,
|
||||||
phraseHints=phraseHints if phraseHints else None,
|
phraseHints=phraseHints if phraseHints else None,
|
||||||
alternativeLanguages=["en-US"],
|
|
||||||
audioFormat="linear16",
|
audioFormat="linear16",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue