teams bridge
This commit is contained in:
parent
edecfb002c
commit
b7e4efb3a3
8 changed files with 88 additions and 52 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
Loading…
Reference in a new issue