This commit is contained in:
Ida Dittrich 2026-02-16 09:30:41 +01:00
commit 3aaa4790c2
4 changed files with 117 additions and 15 deletions

View file

@ -78,6 +78,7 @@ class TeamsbotSession(BaseModel):
startedByUserId: str = Field(description="User ID who started the session")
bridgeSessionId: Optional[str] = Field(default=None, description="Session ID on the .NET Media Bridge")
meetingChatId: Optional[str] = Field(default=None, description="Teams meeting chat ID for Graph API messages")
sessionContext: Optional[str] = Field(default=None, description="Custom context/knowledge provided by the user for this session")
summary: Optional[str] = Field(default=None, description="AI-generated meeting summary (after session ends)")
errorMessage: Optional[str] = Field(default=None, description="Error message if status is ERROR")
transcriptSegmentCount: int = Field(default=0, description="Number of transcript segments in this session")
@ -200,6 +201,7 @@ class TeamsbotStartSessionRequest(BaseModel):
backgroundImageUrl: Optional[str] = Field(default=None, description="Override background image for this session")
connectionId: Optional[str] = Field(default=None, description="Microsoft connection ID for Graph API access")
joinMode: Optional[TeamsbotJoinMode] = Field(default=None, description="How the bot joins: systemBot, anonymous, or userAccount. Defaults to systemBot if credentials configured, else anonymous.")
sessionContext: Optional[str] = Field(default=None, description="Custom context/knowledge to provide to the bot for this session (e.g. meeting agenda, documents, background info)")
class TeamsbotSessionResponse(BaseModel):

View file

@ -178,6 +178,7 @@ async def startSession(
meetingLink=cleanMeetingUrl,
botName=body.botName or config.botName,
backgroundImageUrl=body.backgroundImageUrl or config.backgroundImageUrl,
sessionContext=body.sessionContext,
status=TeamsbotSessionStatus.PENDING,
startedByUserId=str(context.user.id),
).model_dump()

View file

@ -77,6 +77,8 @@ class TeamsbotService:
# State
self._lastAiCallTime: float = 0.0
self._contextBuffer: List[Dict[str, Any]] = []
self._sessionContext: Optional[str] = None # User-provided background context
self._contextSummary: Optional[str] = None # AI-generated summary of long context
# =========================================================================
# Session Lifecycle
@ -210,6 +212,13 @@ class TeamsbotService:
interface = interfaceDb.getInterface(self.currentUser, self.mandateId, self.instanceId)
voiceInterface = getVoiceInterface(self.currentUser, self.mandateId)
# Load session context (user-provided background knowledge)
session = interface.getSession(sessionId)
if session:
self._sessionContext = session.get("sessionContext")
if self._sessionContext:
logger.info(f"Session {sessionId}: Loaded session context ({len(self._sessionContext)} chars)")
logger.info(f"[WS-DEBUG] WebSocket handler started for session {sessionId}")
try:
@ -323,6 +332,23 @@ class TeamsbotService:
if not text:
return
# Check for STOP command: "<botname> stop" or "<botname> STOP"
# This immediately stops the bot from speaking and clears the audio queue.
botNameLower = self.config.botName.lower()
textLower = text.lower()
if botNameLower in textLower and "stop" in textLower:
logger.info(f"Session {sessionId}: STOP command detected: [{speaker}] {text[:60]}")
if websocket:
try:
await websocket.send_text(json.dumps({
"type": "stopAudio",
"sessionId": sessionId,
}))
except Exception as stopErr:
logger.warning(f"Failed to send stop command: {stopErr}")
# Don't trigger AI analysis for stop commands
return
# Filter out the bot's own speech from AI triggering.
# The bot hears itself via captions — these should be stored in the
# transcript for the record, but must NOT trigger AI analysis (feedback loop).
@ -351,6 +377,10 @@ class TeamsbotService:
# Keep only last N segments
maxSegments = self.config.contextWindowSegments
if len(self._contextBuffer) > maxSegments:
# When buffer overflows, summarize the older half to preserve context
# without losing information. The summary replaces the old segments.
if not self._contextSummary and len(self._contextBuffer) > maxSegments * 1.5:
asyncio.create_task(self._summarizeContextBuffer(sessionId))
self._contextBuffer = self._contextBuffer[-maxSegments:]
# Emit SSE event for live transcript
@ -473,7 +503,17 @@ class TeamsbotService:
else:
contextLines.append(f"[{speaker}]: {text}")
transcriptContext = f"BOT_NAME:{self.config.botName}\n" + "\n".join(contextLines)
# Include session context if provided by the user at session start
sessionContextStr = ""
if self._sessionContext:
sessionContextStr = f"\nSESSION_CONTEXT (background knowledge provided by the user):\n{self._sessionContext}\n"
# Include summary of earlier conversation if available
summaryStr = ""
if self._contextSummary:
summaryStr = f"\nEARLIER_CONVERSATION_SUMMARY:\n{self._contextSummary}\n"
transcriptContext = f"BOT_NAME:{self.config.botName}{sessionContextStr}{summaryStr}\nRECENT_TRANSCRIPT:\n" + "\n".join(contextLines)
# Call SPEECH_TEAMS
try:
@ -633,6 +673,56 @@ class TeamsbotService:
logger.error(f"SPEECH_TEAMS analysis failed for session {sessionId}: {type(e).__name__}: {e}", exc_info=True)
await _emitSessionEvent(sessionId, "error", {"message": f"AI analysis failed: {type(e).__name__}: {str(e)}"})
# =========================================================================
# Context Summarization (for long sessions)
# =========================================================================
async def _summarizeContextBuffer(self, sessionId: str):
"""Summarize the older part of the context buffer to preserve information
without exceeding the context window. This runs in the background."""
try:
if self._contextSummary:
return # Already summarized recently
# Take the older half of the buffer for summarization
halfPoint = len(self._contextBuffer) // 2
oldSegments = self._contextBuffer[:halfPoint]
if len(oldSegments) < 10:
return # Not enough to summarize
# Build text to summarize
lines = []
for seg in oldSegments:
speaker = seg.get("speaker", "Unknown")
text = seg.get("text", "")
lines.append(f"[{speaker}]: {text}")
textToSummarize = "\n".join(lines)
from modules.services.serviceAi.mainServiceAi import AiService
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum
serviceContext = _ServiceContext(self.currentUser, self.mandateId, self.instanceId)
aiService = AiService(serviceCenter=serviceContext)
await aiService.ensureAiObjectsInitialized()
request = AiCallRequest(
prompt="Fasse das folgende Meeting-Transkript in 3-5 Saetzen zusammen. Nenne die wichtigsten Themen, Entscheidungen und offene Fragen. Antworte NUR mit der Zusammenfassung, keine Erklaerungen.",
context=textToSummarize,
options=AiCallOptions(
operationType=OperationTypeEnum.DATA_ANALYSE,
priority=PriorityEnum.SPEED,
)
)
response = await aiService.callAi(request)
if response and response.errorCount == 0:
self._contextSummary = response.content.strip()
logger.info(f"Session {sessionId}: Context summarized ({len(oldSegments)} segments -> {len(self._contextSummary)} chars)")
except Exception as e:
logger.warning(f"Context summarization failed for session {sessionId}: {e}")
# =========================================================================
# Meeting Summary
# =========================================================================

View file

@ -331,24 +331,33 @@ class AiService:
basePrompt = f"""Du bist "{botName}", ein AI-Teilnehmer in einem Microsoft Teams Meeting.
Analysiere das folgende Transkript und entscheide, ob du antworten sollst.
WICHTIG: Das Transkript kann in verschiedenen Sprachen sein (Deutsch, Englisch, etc.) weil die Spracherkennung die Sprache nicht immer korrekt erkennt. Antworte immer in der Sprache, in der du angesprochen wirst.
SPRACHE: Das Transkript kann in verschiedenen Sprachen sein. Antworte immer in der Sprache des letzten Sprechers der dich angesprochen hat. Wenn jemand sagt "let's talk German" oder "sprich deutsch", wechsle die Sprache entsprechend.
REGEL 1 (HOECHSTE PRIORITAET - IMMER ANTWORTEN):
Wenn dein Name "{botName}" oder eine aehnliche Anrede (Shelly, shelly, SHELLY, etc.) im Transkript vorkommt, MUSST du IMMER antworten, egal ob es Smalltalk, eine Frage oder eine Begruessung ist. Du bist ein freundlicher Meeting-Teilnehmer.
WANN ANTWORTEN:
REGEL 2 (ANTWORTEN wenn sinnvoll):
- Eine explizite Frage an die Runde gestellt wird, die du sachlich beantworten kannst
- Du einen wertvollen Beitrag zur Diskussion hast
REGEL 1 (HOECHSTE PRIORITAET - NUR wenn direkt angesprochen):
Antworte NUR wenn dein Name "{botName}" (oder Varianten wie Shelly, shelly) DIREKT im aktuellsten Transkript-Segment vorkommt.
Beispiele wo du antworten MUSST: "Shelly, was denkst du?", "Hey Shelly", "Shelly please introduce yourself"
Beispiele wo du NICHT antworten darfst: Jemand spricht ueber ein Thema ohne dich zu adressieren.
REGEL 3 (NICHT ANTWORTEN):
- Die Teilnehmer normal miteinander sprechen OHNE deinen Namen zu nennen
- Die Diskussion keinen Input von dir benoetigt und du nicht angesprochen wirst
REGEL 2 (NUR bei direkter Frage an dich):
Wenn jemand eine Frage DIREKT AN DICH stellt (mit deinem Namen), beantworte sie.
Antworte NICHT auf allgemeine Fragen in der Runde, die nicht an dich gerichtet sind.
ANTWORT-STIL:
- Halte dich kurz und praezise (max 2-3 Saetze)
- Sei freundlich, professionell und natuerlich
- Bei Begruessung: Antworte herzlich zurueck
- Bei Fragen: Beantworte sachlich und konkret"""
REGEL 3 (NICHT ANTWORTEN - sehr wichtig):
- Wenn Teilnehmer miteinander sprechen ohne dich zu adressieren: NICHT antworten
- Wenn die Konversation nicht an dich gerichtet ist: NICHT antworten
- Wenn du bereits auf dieselbe Frage geantwortet hast: NICHT nochmal antworten
- Wenn du nicht sicher bist ob du gemeint bist: NICHT antworten
- Im Zweifel: shouldRespond = false
ANTWORT-STIL (wenn du antwortest):
- Direkt und konkret antworten, KEINE Floskeln
- NICHT mit "Hallo [Name]" anfangen wenn du bereits begruessst hast
- NICHT "Ich bin {botName} und ich bin hier um zu helfen" wiederholen
- NICHT frueheres wiederholen das du schon gesagt hast
- Max 1-2 Saetze, praezise auf den Punkt
- Sieh dir an was du (markiert als [YOU]) bereits gesagt hast und wiederhole es NICHT"""
# Append user-configured instructions if provided
if userSystemPrompt and userSystemPrompt.strip():