From b7e4efb3a38c0580cf0082a5b1bcb05a0f1a9df6 Mon Sep 17 00:00:00 2001
From: patrick-motsch
Date: Fri, 13 Feb 2026 07:27:33 +0100
Subject: [PATCH] teams bridge
---
env_dev.env | 3 +
env_int.env | 3 +
env_prod.env | 3 +
.../features/teamsbot/datamodelTeamsbot.py | 9 ++-
.../teamsbot/interfaceFeatureTeamsbot.py | 78 +++++++++++--------
.../features/teamsbot/routeFeatureTeamsbot.py | 39 ++++++----
modules/features/teamsbot/service.py | 2 +-
modules/routes/routeSystem.py | 3 +
8 files changed, 88 insertions(+), 52 deletions(-)
diff --git a/env_dev.env b/env_dev.env
index 5339bbaf..da6f931f 100644
--- a/env_dev.env
+++ b/env_dev.env
@@ -57,6 +57,9 @@ Connector_GoogleSpeech_API_KEY_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpETk5FWWM3Q0JKMzhI
# Feature SyncDelta JIRA configuration
Feature_SyncDelta_JIRA_DELTA_TOKEN_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEbm0yRUJ6VUJKbUwyRW5kMnRaNW4wM2YxMkJUTXVXZUdmdVRCaUZIVHU2TTV2RWZLRmUtZkcwZE4yRUNlNDQ0aUJWYjNfdVg5YjV5c2JwMHhoUUYxZWdkeS11bXR0eGxRLWRVaVU3cUVQZWJlNDRtY1lWUDdqeDVFSlpXS0VFX21WajlRS3lHQjc0bS11akkybWV3QUFlR2hNWUNYLUdiRjZuN2dQODdDSExXWG1Dd2ZGclI2aUhlSWhETVZuY3hYdnhkb2c2LU1JTFBvWFpTNmZtMkNVOTZTejJwbDI2eGE0OS1xUlIwQnlCSmFxRFNCeVJNVzlOMDhTR1VUamx4RDRyV3p6Tk9qVHBrWWdySUM3TVRaYjd3N0JHMFhpdzFhZTNDLTFkRVQ2RVE4U19COXRhRWtNc0NVOHRqUS1CRDFpZ19xQmtFLU9YSDU3TXBZQXpVcld3PT0=
+# Teamsbot Media Bridge
+TEAMSBOT_BRIDGE_URL = https://media.poweron.swiss:9440
+
# Debug Configuration
APP_DEBUG_CHAT_WORKFLOW_ENABLED = True
APP_DEBUG_CHAT_WORKFLOW_DIR = D:/Athi/Local/Web/poweron/local/debug
diff --git a/env_int.env b/env_int.env
index 5534cbdf..87cb8117 100644
--- a/env_int.env
+++ b/env_int.env
@@ -57,6 +57,9 @@ Connector_GoogleSpeech_API_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRkNmVXZ1pWcHcydTF2
# Feature SyncDelta JIRA configuration
Feature_SyncDelta_JIRA_DELTA_TOKEN_SECRET = INT_ENC:Z0FBQUFBQm8xSVRkTUNsWm4wX0p6eXFDZmJ4dFdHNEs1MV9MUzdrb3RzeC1jVWVYZ0REWHRyZkFiaGZLcUQtTXFBZzZkNzRmQ0gxbEhGbUNlVVFfR1JEQTc0aldkZkgyWnBOcjdlUlZxR0tDTEdKRExULXAyUEtsVmNTMkRKU1BJNnFiM0hlMXo4YndMcHlRMExtZDQ3Zm9vNFhMcEZCcHpBPT0=
+# Teamsbot Media Bridge
+TEAMSBOT_BRIDGE_URL = https://media.poweron.swiss:9440
+
# Debug Configuration
APP_DEBUG_CHAT_WORKFLOW_ENABLED = FALSE
APP_DEBUG_CHAT_WORKFLOW_DIR = ./test-chat
diff --git a/env_prod.env b/env_prod.env
index a7b4512c..8c85fbb6 100644
--- a/env_prod.env
+++ b/env_prod.env
@@ -57,6 +57,9 @@ Connector_GoogleSpeech_API_KEY_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z4NFQxaF9uN3h1cVB
# Feature SyncDelta JIRA configuration
Feature_SyncDelta_JIRA_DELTA_TOKEN_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z4d3Z4d2x6N1FhUktMU0RKbkxfY2pTQkRzXzJ6UXVEbDNCaFM3UHMtQVFGYzNmYWs4N0lMM1R2SFJuZTVFVmx6MGVEbXc5U3NOTnY1TWN0ZDNaamlHQWloalM3VldmREJNSHQ1TlVkSVFJMTVhQWVGSVRMTGw4UTBqNGlQZFVuaHp4WUlKemR5UnBXZlh0REJFLXJ4ejR3PT0=
+# Teamsbot Media Bridge
+TEAMSBOT_BRIDGE_URL = https://media.poweron.swiss:9440
+
# Debug Configuration
APP_DEBUG_CHAT_WORKFLOW_ENABLED = FALSE
APP_DEBUG_CHAT_WORKFLOW_DIR = ./test-chat
diff --git a/modules/features/teamsbot/datamodelTeamsbot.py b/modules/features/teamsbot/datamodelTeamsbot.py
index febdce9e..8e3dc60c 100644
--- a/modules/features/teamsbot/datamodelTeamsbot.py
+++ b/modules/features/teamsbot/datamodelTeamsbot.py
@@ -116,11 +116,18 @@ class TeamsbotConfig(BaseModel):
responseMode: TeamsbotResponseMode = Field(default=TeamsbotResponseMode.AUTO, description="How the bot responds")
language: str = Field(default="de-DE", description="Primary language for STT/TTS")
voiceId: Optional[str] = Field(default=None, description="Google TTS voice ID (e.g., de-DE-Standard-A)")
- bridgeUrl: Optional[str] = Field(default=None, description="URL of the .NET Media Bridge service")
+ bridgeUrl: Optional[str] = Field(default=None, description="URL of the .NET Media Bridge service. Falls back to TEAMSBOT_BRIDGE_URL env variable if not set per-instance.")
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")
+ def _getEffectiveBridgeUrl(self) -> Optional[str]:
+ """Resolve the effective bridge URL: per-instance config takes priority, then env variable."""
+ if self.bridgeUrl:
+ return self.bridgeUrl
+ from modules.shared.configuration import APP_CONFIG
+ return APP_CONFIG.get("TEAMSBOT_BRIDGE_URL")
+
# ============================================================================
# API Request/Response Models
diff --git a/modules/features/teamsbot/interfaceFeatureTeamsbot.py b/modules/features/teamsbot/interfaceFeatureTeamsbot.py
index da5a8a62..1aff28cc 100644
--- a/modules/features/teamsbot/interfaceFeatureTeamsbot.py
+++ b/modules/features/teamsbot/interfaceFeatureTeamsbot.py
@@ -45,7 +45,22 @@ class TeamsbotObjects:
self.currentUser = currentUser
self.mandateId = mandateId
self.featureInstanceId = featureInstanceId
- self.db = DatabaseConnector()
+ self.userId = str(currentUser.id) if currentUser else "system"
+
+ dbHost = APP_CONFIG.get("DB_HOST", "_no_config_default_data")
+ dbDatabase = "poweron_teamsbot"
+ dbUser = APP_CONFIG.get("DB_USER")
+ dbPassword = APP_CONFIG.get("DB_PASSWORD_SECRET")
+ dbPort = int(APP_CONFIG.get("DB_PORT", 5432))
+
+ self.db = DatabaseConnector(
+ dbHost=dbHost,
+ dbDatabase=dbDatabase,
+ dbUser=dbUser,
+ dbPassword=dbPassword,
+ dbPort=dbPort,
+ userId=self.userId,
+ )
# =========================================================================
# Sessions
@@ -53,35 +68,32 @@ class TeamsbotObjects:
def getSessions(self, instanceId: str, includeEnded: bool = True) -> List[Dict[str, Any]]:
"""Get all sessions for a feature instance."""
- filters = {"instanceId": instanceId}
- if not includeEnded:
- filters["status__ne"] = TeamsbotSessionStatus.ENDED.value
-
records = self.db.getRecordset(
TeamsbotSession,
- filters=filters,
- orderBy=[("startedAt", "DESC")]
+ recordFilter={"instanceId": instanceId},
)
+ if not includeEnded:
+ records = [r for r in records if r.get("status") != TeamsbotSessionStatus.ENDED.value]
+ # Sort by startedAt descending
+ records.sort(key=lambda r: r.get("startedAt") or "", reverse=True)
return records
def getActiveSessions(self, instanceId: str) -> List[Dict[str, Any]]:
"""Get only active (non-ended, non-error) sessions."""
records = self.db.getRecordset(
TeamsbotSession,
- filters={
- "instanceId": instanceId,
- "status__in": [
- TeamsbotSessionStatus.PENDING.value,
- TeamsbotSessionStatus.JOINING.value,
- TeamsbotSessionStatus.ACTIVE.value,
- ]
- }
+ recordFilter={"instanceId": instanceId},
)
- return records
+ activeStatuses = {
+ TeamsbotSessionStatus.PENDING.value,
+ TeamsbotSessionStatus.JOINING.value,
+ TeamsbotSessionStatus.ACTIVE.value,
+ }
+ return [r for r in records if r.get("status") in activeStatuses]
def getSession(self, sessionId: str) -> Optional[Dict[str, Any]]:
"""Get a single session by ID."""
- records = self.db.getRecordset(TeamsbotSession, filters={"id": sessionId})
+ records = self.db.getRecordset(TeamsbotSession, recordFilter={"id": sessionId})
return records[0] if records else None
def createSession(self, sessionData: Dict[str, Any]) -> Dict[str, Any]:
@@ -109,27 +121,25 @@ class TeamsbotObjects:
def getTranscripts(self, sessionId: str, limit: int = None, offset: int = None) -> List[Dict[str, Any]]:
"""Get transcript segments for a session, ordered by timestamp."""
- filters = {"sessionId": sessionId}
records = self.db.getRecordset(
TeamsbotTranscript,
- filters=filters,
- orderBy=[("timestamp", "ASC")],
- limit=limit,
- offset=offset
+ recordFilter={"sessionId": sessionId},
)
+ records.sort(key=lambda r: r.get("timestamp") or "")
+ if offset:
+ records = records[offset:]
+ if limit:
+ records = records[:limit]
return records
def getRecentTranscripts(self, sessionId: str, count: int = 20) -> List[Dict[str, Any]]:
"""Get the most recent N transcript segments for context building."""
records = self.db.getRecordset(
TeamsbotTranscript,
- filters={"sessionId": sessionId},
- orderBy=[("timestamp", "DESC")],
- limit=count
+ recordFilter={"sessionId": sessionId},
)
- # Reverse to get chronological order
- records.reverse()
- return records
+ records.sort(key=lambda r: r.get("timestamp") or "")
+ return records[-count:]
def createTranscript(self, transcriptData: Dict[str, Any]) -> Dict[str, Any]:
"""Create a new transcript segment."""
@@ -138,7 +148,7 @@ class TeamsbotObjects:
def _deleteTranscriptsBySession(self, sessionId: str) -> int:
"""Delete all transcripts for a session."""
- records = self.db.getRecordset(TeamsbotTranscript, filters={"sessionId": sessionId})
+ records = self.db.getRecordset(TeamsbotTranscript, recordFilter={"sessionId": sessionId})
count = 0
for record in records:
self.db.recordDelete(TeamsbotTranscript, record.get("id"))
@@ -153,9 +163,9 @@ class TeamsbotObjects:
"""Get all bot responses for a session."""
records = self.db.getRecordset(
TeamsbotBotResponse,
- filters={"sessionId": sessionId},
- orderBy=[("timestamp", "ASC")]
+ recordFilter={"sessionId": sessionId},
)
+ records.sort(key=lambda r: r.get("timestamp") or "")
return records
def createBotResponse(self, responseData: Dict[str, Any]) -> Dict[str, Any]:
@@ -165,7 +175,7 @@ class TeamsbotObjects:
def _deleteResponsesBySession(self, sessionId: str) -> int:
"""Delete all bot responses for a session."""
- records = self.db.getRecordset(TeamsbotBotResponse, filters={"sessionId": sessionId})
+ records = self.db.getRecordset(TeamsbotBotResponse, recordFilter={"sessionId": sessionId})
count = 0
for record in records:
self.db.recordDelete(TeamsbotBotResponse, record.get("id"))
@@ -178,8 +188,8 @@ class TeamsbotObjects:
def getSessionStats(self, sessionId: str) -> Dict[str, Any]:
"""Get aggregated statistics for a session."""
- transcripts = self.db.getRecordset(TeamsbotTranscript, filters={"sessionId": sessionId})
- responses = self.db.getRecordset(TeamsbotBotResponse, filters={"sessionId": sessionId})
+ transcripts = self.db.getRecordset(TeamsbotTranscript, recordFilter={"sessionId": sessionId})
+ responses = self.db.getRecordset(TeamsbotBotResponse, recordFilter={"sessionId": sessionId})
totalCost = sum(r.get("priceCHF", 0) for r in responses)
totalProcessingTime = sum(r.get("processingTime", 0) for r in responses)
diff --git a/modules/features/teamsbot/routeFeatureTeamsbot.py b/modules/features/teamsbot/routeFeatureTeamsbot.py
index e4d390d2..4c6a7877 100644
--- a/modules/features/teamsbot/routeFeatureTeamsbot.py
+++ b/modules/features/teamsbot/routeFeatureTeamsbot.py
@@ -98,9 +98,9 @@ def _getInstanceConfig(instanceId: str) -> TeamsbotConfig:
@router.post("/{instanceId}/sessions")
@limiter.limit("10/minute")
async def startSession(
+ request: Request,
instanceId: str,
- request: TeamsbotStartSessionRequest,
- httpRequest: Request,
+ body: TeamsbotStartSessionRequest,
context: RequestContext = Depends(getRequestContext),
):
"""Start a new Teams Bot session -- bot joins the specified meeting."""
@@ -112,9 +112,9 @@ async def startSession(
sessionData = TeamsbotSession(
instanceId=instanceId,
mandateId=mandateId,
- meetingLink=request.meetingLink,
- botName=request.botName or config.botName,
- backgroundImageUrl=request.backgroundImageUrl or config.backgroundImageUrl,
+ meetingLink=body.meetingLink,
+ botName=body.botName or config.botName,
+ backgroundImageUrl=body.backgroundImageUrl or config.backgroundImageUrl,
status=TeamsbotSessionStatus.PENDING,
startedByUserId=str(context.user.id),
).model_dump()
@@ -124,12 +124,12 @@ async def startSession(
# Derive gateway base URL from the incoming request so the bridge
# can build full callback/WS URLs targeting this specific gateway instance.
- gatewayBaseUrl = str(httpRequest.base_url).rstrip("/")
+ gatewayBaseUrl = str(request.base_url).rstrip("/")
# Start the bot in background (join meeting via bridge)
service = TeamsbotService(context.user, mandateId, instanceId, config)
asyncio.create_task(
- service.joinMeeting(sessionId, request.meetingLink, request.connectionId, gatewayBaseUrl)
+ service.joinMeeting(sessionId, body.meetingLink, body.connectionId, gatewayBaseUrl)
)
logger.info(f"Teamsbot session {sessionId} created for instance {instanceId}")
@@ -139,9 +139,10 @@ async def startSession(
@router.get("/{instanceId}/sessions")
@limiter.limit("30/minute")
async def listSessions(
+ request: Request,
instanceId: str,
includeEnded: bool = Query(True, description="Include ended sessions"),
- context: RequestContext = Depends(getRequestContext)
+ context: RequestContext = Depends(getRequestContext),
):
"""List all sessions for a feature instance."""
_validateInstanceAccess(instanceId, context)
@@ -153,11 +154,12 @@ async def listSessions(
@router.get("/{instanceId}/sessions/{sessionId}")
@limiter.limit("30/minute")
async def getSession(
+ request: Request,
instanceId: str,
sessionId: str,
includeTranscripts: bool = Query(True),
includeResponses: bool = Query(True),
- context: RequestContext = Depends(getRequestContext)
+ context: RequestContext = Depends(getRequestContext),
):
"""Get session details with optional transcripts and bot responses."""
_validateInstanceAccess(instanceId, context)
@@ -182,9 +184,10 @@ async def getSession(
@router.get("/{instanceId}/sessions/{sessionId}/stream")
@limiter.limit("10/minute")
async def streamSession(
+ request: Request,
instanceId: str,
sessionId: str,
- context: RequestContext = Depends(getRequestContext)
+ context: RequestContext = Depends(getRequestContext),
):
"""
SSE live stream for a session.
@@ -240,9 +243,10 @@ async def streamSession(
@router.post("/{instanceId}/sessions/{sessionId}/stop")
@limiter.limit("10/minute")
async def stopSession(
+ request: Request,
instanceId: str,
sessionId: str,
- context: RequestContext = Depends(getRequestContext)
+ context: RequestContext = Depends(getRequestContext),
):
"""Stop an active session -- bot leaves the meeting."""
mandateId = _validateInstanceAccess(instanceId, context)
@@ -268,9 +272,10 @@ async def stopSession(
@router.delete("/{instanceId}/sessions/{sessionId}")
@limiter.limit("10/minute")
async def deleteSession(
+ request: Request,
instanceId: str,
sessionId: str,
- context: RequestContext = Depends(getRequestContext)
+ context: RequestContext = Depends(getRequestContext),
):
"""Delete a session and all related data."""
_validateInstanceAccess(instanceId, context)
@@ -297,8 +302,9 @@ async def deleteSession(
@router.get("/{instanceId}/config")
@limiter.limit("30/minute")
async def getConfig(
+ request: Request,
instanceId: str,
- context: RequestContext = Depends(getRequestContext)
+ context: RequestContext = Depends(getRequestContext),
):
"""Get the teamsbot configuration for a feature instance."""
_validateInstanceAccess(instanceId, context)
@@ -309,16 +315,17 @@ async def getConfig(
@router.put("/{instanceId}/config")
@limiter.limit("10/minute")
async def updateConfig(
+ request: Request,
instanceId: str,
- request: TeamsbotConfigUpdateRequest,
- context: RequestContext = Depends(getRequestContext)
+ configUpdate: TeamsbotConfigUpdateRequest,
+ context: RequestContext = Depends(getRequestContext),
):
"""Update the teamsbot configuration."""
mandateId = _validateInstanceAccess(instanceId, context)
# Load current config and merge updates
currentConfig = _getInstanceConfig(instanceId)
- updateDict = request.model_dump(exclude_none=True)
+ updateDict = configUpdate.model_dump(exclude_none=True)
mergedConfig = currentConfig.model_copy(update=updateDict)
# Save to FeatureInstance.config
diff --git a/modules/features/teamsbot/service.py b/modules/features/teamsbot/service.py
index d7707e05..af3f0686 100644
--- a/modules/features/teamsbot/service.py
+++ b/modules/features/teamsbot/service.py
@@ -55,7 +55,7 @@ class TeamsbotService:
self.mandateId = mandateId
self.instanceId = instanceId
self.config = config
- self.bridgeConnector = BridgeConnector(config.bridgeUrl)
+ self.bridgeConnector = BridgeConnector(config._getEffectiveBridgeUrl())
# State
self._lastAiCallTime: float = 0.0
diff --git a/modules/routes/routeSystem.py b/modules/routes/routeSystem.py
index 25c99749..6b01ad21 100644
--- a/modules/routes/routeSystem.py
+++ b/modules/routes/routeSystem.py
@@ -108,6 +108,9 @@ def _getFeatureUiObjects(featureCode: str) -> List[Dict[str, Any]]:
elif featureCode == "automation":
from modules.features.automation.mainAutomation import UI_OBJECTS
return UI_OBJECTS
+ elif featureCode == "teamsbot":
+ from modules.features.teamsbot.mainTeamsbot import UI_OBJECTS
+ return UI_OBJECTS
else:
logger.warning(f"Unknown feature code: {featureCode}")
return []