feat: refactor message streaming + add streaming service

This commit is contained in:
Ida Dittrich 2026-03-04 08:10:50 +01:00
parent 1f529568f5
commit f1231c4b86
12 changed files with 152 additions and 149 deletions

View file

@ -27,8 +27,7 @@ from modules.features.chatbot.bridges.tools import (
create_tavily_search_tool, create_tavily_search_tool,
create_send_streaming_message_tool, create_send_streaming_message_tool,
) )
from modules.features.chatbot.streaming.helpers import ChatStreamingHelper from modules.services.serviceStreaming import ChatStreamingHelper
from modules.features.chatbot.streaming.events import get_event_manager
from modules.datamodels.datamodelUam import User from modules.datamodels.datamodelUam import User
if TYPE_CHECKING: if TYPE_CHECKING:
@ -179,6 +178,7 @@ class Chatbot:
system_prompt: str = "You are a helpful assistant." system_prompt: str = "You are a helpful assistant."
workflow_id: str = "default" workflow_id: str = "default"
config: Optional["ChatbotConfig"] = None config: Optional["ChatbotConfig"] = None
_event_manager: Any = None
@classmethod @classmethod
async def create( async def create(
@ -188,6 +188,7 @@ class Chatbot:
system_prompt: str, system_prompt: str,
workflow_id: str = "default", workflow_id: str = "default",
config: Optional["ChatbotConfig"] = None, config: Optional["ChatbotConfig"] = None,
event_manager=None,
) -> "Chatbot": ) -> "Chatbot":
"""Factory method to create and configure a Chatbot instance. """Factory method to create and configure a Chatbot instance.
@ -197,6 +198,7 @@ class Chatbot:
system_prompt: The system prompt to initialize the chatbot. system_prompt: The system prompt to initialize the chatbot.
workflow_id: The workflow ID (maps to thread_id). workflow_id: The workflow ID (maps to thread_id).
config: Optional chatbot configuration for dynamic tool enablement. config: Optional chatbot configuration for dynamic tool enablement.
event_manager: Optional event manager for streaming (passed from route).
Returns: Returns:
A configured Chatbot instance. A configured Chatbot instance.
@ -207,6 +209,7 @@ class Chatbot:
system_prompt=system_prompt, system_prompt=system_prompt,
workflow_id=workflow_id, workflow_id=workflow_id,
config=config, config=config,
_event_manager=event_manager,
) )
configured_tools = await instance._configure_tools() configured_tools = await instance._configure_tools()
instance.app = instance._build_app(memory, configured_tools) instance.app = instance._build_app(memory, configured_tools)
@ -247,10 +250,9 @@ class Chatbot:
tools.append(tavily_tool) tools.append(tavily_tool)
logger.debug("Added Tavily search tool") logger.debug("Added Tavily search tool")
# Streaming status tool (if enabled) # Streaming status tool (if enabled and event_manager available)
if streaming_enabled: if streaming_enabled and self._event_manager:
event_manager = get_event_manager() send_streaming_message = create_send_streaming_message_tool(self._event_manager)
send_streaming_message = create_send_streaming_message_tool(event_manager)
tools.append(send_streaming_message) tools.append(send_streaming_message)
logger.debug("Added streaming status tool") logger.debug("Added streaming status tool")

View file

@ -1027,7 +1027,7 @@ class ChatObjects:
totalPages=totalPages totalPages=totalPages
) )
def createMessage(self, messageData: Dict[str, Any]) -> ChatbotMessage: def createMessage(self, messageData: Dict[str, Any], event_manager=None) -> ChatbotMessage:
"""Creates a message for a conversation if user has access. Accepts workflowId (from bridge) or conversationId.""" """Creates a message for a conversation if user has access. Accepts workflowId (from bridge) or conversationId."""
try: try:
if "id" not in messageData or not messageData["id"]: if "id" not in messageData or not messageData["id"]:
@ -1131,23 +1131,21 @@ class ChatObjects:
actionName=createdMessage.get("actionName") actionName=createdMessage.get("actionName")
) )
try: if event_manager:
from modules.features.chatbot.streaming.events import get_event_manager try:
event_manager = get_event_manager() message_timestamp = parseTimestamp(chat_message.publishedAt, default=getUtcTimestamp())
message_timestamp = parseTimestamp(chat_message.publishedAt, default=getUtcTimestamp()) asyncio.create_task(event_manager.emit_event(
asyncio.create_task(event_manager.emit_event( context_id=conversationId,
context_id=conversationId, event_type="chatdata",
event_type="chatdata", data={
data={ "type": "message",
"type": "message", "createdAt": message_timestamp,
"createdAt": message_timestamp, "item": chat_message.model_dump()
"item": chat_message.model_dump() },
}, event_category="chat"
event_category="chat" ))
)) except Exception as e:
except Exception as e: logger.debug(f"Could not emit message event: {e}")
# Event manager not available or error - continue without emitting
logger.debug(f"Could not emit message event: {e}")
# Debug: Store message and documents for debugging - only if debug enabled # Debug: Store message and documents for debugging - only if debug enabled
storeDebugMessageAndDocuments(chat_message, self.currentUser, mandateId=self.mandateId, featureInstanceId=self.featureInstanceId) storeDebugMessageAndDocuments(chat_message, self.currentUser, mandateId=self.mandateId, featureInstanceId=self.featureInstanceId)
@ -1387,7 +1385,7 @@ class ChatObjects:
totalPages=totalPages totalPages=totalPages
) )
def createLog(self, logData: Dict[str, Any]) -> Optional[ChatbotLog]: def createLog(self, logData: Dict[str, Any], event_manager=None) -> Optional[ChatbotLog]:
"""Creates a log entry for a conversation if user has access. Accepts workflowId for backward compat.""" """Creates a log entry for a conversation if user has access. Accepts workflowId for backward compat."""
conversationId = logData.get("conversationId") or logData.get("workflowId") conversationId = logData.get("conversationId") or logData.get("workflowId")
if not conversationId: if not conversationId:
@ -1420,19 +1418,18 @@ class ChatObjects:
if not createdLog: if not createdLog:
return None return None
try: if event_manager:
from modules.features.chatbot.streaming.events import get_event_manager try:
event_manager = get_event_manager() log_timestamp = parseTimestamp(createdLog.get("timestamp"), default=getUtcTimestamp())
log_timestamp = parseTimestamp(createdLog.get("timestamp"), default=getUtcTimestamp()) asyncio.create_task(event_manager.emit_event(
asyncio.create_task(event_manager.emit_event( context_id=conversationId,
context_id=conversationId, event_type="chatdata",
event_type="chatdata", data={"type": "log", "createdAt": log_timestamp, "item": ChatbotLog(**createdLog).model_dump()},
data={"type": "log", "createdAt": log_timestamp, "item": ChatbotLog(**createdLog).model_dump()}, event_category="log",
event_category="log", message="New log"
message="New log" ))
)) except Exception as e:
except Exception as e: logger.debug(f"Could not emit log event: {e}")
logger.debug(f"Could not emit log event: {e}")
return ChatbotLog(**createdLog) return ChatbotLog(**createdLog)

View file

@ -31,7 +31,7 @@ from modules.features.chatbot.interfaceFeatureChatbot import ChatbotConversation
# Import chatbot feature # Import chatbot feature
from modules.features.chatbot import chatProcess from modules.features.chatbot import chatProcess
from modules.features.chatbot.streaming.events import get_event_manager from modules.services.serviceStreaming import get_event_manager
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -244,8 +244,11 @@ async def stream_chatbot_start(
final_workflow_id = workflowId or userInput.workflowId final_workflow_id = workflowId or userInput.workflowId
# Start background processing (this will create the workflow and event queue) # Start background processing (this will create the workflow and event queue)
# Pass featureInstanceId to chatProcess # Pass featureInstanceId and event_manager to chatProcess
workflow = await chatProcess(context.user, mandateId, userInput, final_workflow_id, featureInstanceId=instanceId) workflow = await chatProcess(
context.user, mandateId, userInput, final_workflow_id,
featureInstanceId=instanceId, event_manager=event_manager
)
# Check if workflow was created successfully # Check if workflow was created successfully
if not workflow: if not workflow:
@ -464,7 +467,8 @@ async def stop_chatbot(
"lastActivity": getUtcTimestamp() "lastActivity": getUtcTimestamp()
}) })
# Store log entry event_manager = get_event_manager()
# Store log entry (createLog emits when event_manager is provided)
interfaceDbChat.createLog({ interfaceDbChat.createLog({
"id": f"log_{uuid.uuid4()}", "id": f"log_{uuid.uuid4()}",
"workflowId": workflowId, "workflowId": workflowId,
@ -473,13 +477,12 @@ async def stop_chatbot(
"status": "stopped", "status": "stopped",
"timestamp": getUtcTimestamp(), "timestamp": getUtcTimestamp(),
"roundNumber": workflow.currentRound if workflow else 1 "roundNumber": workflow.currentRound if workflow else 1
}) }, event_manager=event_manager)
# Reload workflow to return updated version # Reload workflow to return updated version
workflow = interfaceDbChat.getWorkflow(workflowId) workflow = interfaceDbChat.getWorkflow(workflowId)
# Emit stopped event to active streams # Emit stopped event to active streams
event_manager = get_event_manager()
await event_manager.emit_event( await event_manager.emit_event(
context_id=workflowId, context_id=workflowId,
event_type="stopped", event_type="stopped",

View file

@ -20,7 +20,6 @@ from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, Operati
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentItemReference from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentItemReference
from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp
from modules.services import getInterface as getServices from modules.services import getInterface as getServices
from modules.features.chatbot.streaming.events import get_event_manager
from modules.features.chatbot.chatbot import Chatbot from modules.features.chatbot.chatbot import Chatbot
from modules.features.chatbot.bridges.ai import AICenterChatModel, clear_workflow_allowed_providers from modules.features.chatbot.bridges.ai import AICenterChatModel, clear_workflow_allowed_providers
from modules.features.chatbot.bridges.memory import DatabaseCheckpointer from modules.features.chatbot.bridges.memory import DatabaseCheckpointer
@ -69,7 +68,8 @@ async def chatProcess(
mandateId: Optional[str], mandateId: Optional[str],
userInput: UserInputRequest, userInput: UserInputRequest,
workflowId: Optional[str] = None, workflowId: Optional[str] = None,
featureInstanceId: Optional[str] = None featureInstanceId: Optional[str] = None,
event_manager=None # Required when called from streaming route
) -> ChatbotConversation: ) -> ChatbotConversation:
""" """
Simple chatbot processing - analyze user input and generate queries. Simple chatbot processing - analyze user input and generate queries.
@ -104,10 +104,7 @@ async def chatProcess(
from modules.features.chatbot.interfaceFeatureChatbot import getInterface as getChatbotInterface from modules.features.chatbot.interfaceFeatureChatbot import getInterface as getChatbotInterface
interfaceDbChat = getChatbotInterface(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId) interfaceDbChat = getChatbotInterface(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId)
# Get event manager and create queue if needed # Create or load workflow (event_manager passed from route)
event_manager = get_event_manager()
# Create or load workflow
if workflowId: if workflowId:
workflow = interfaceDbChat.getWorkflow(workflowId) workflow = interfaceDbChat.getWorkflow(workflowId)
if not workflow: if not workflow:
@ -221,22 +218,11 @@ async def chatProcess(
} }
if user_documents: if user_documents:
userMessageData["documents"] = [d.model_dump() for d in user_documents] userMessageData["documents"] = [d.model_dump() for d in user_documents]
userMessage = interfaceDbChat.createMessage(userMessageData) # Don't pass event_manager: event_stream sends initial chatData from DB (includes user msg).
# Emitting here would duplicate it (initial chatData + queue event).
userMessage = interfaceDbChat.createMessage(userMessageData, event_manager=None)
logger.info(f"Stored user message: {userMessage.id} with {len(user_documents)} document(s)") logger.info(f"Stored user message: {userMessage.id} with {len(user_documents)} document(s)")
# Emit message event for streaming (exact chatData format)
message_timestamp = parseTimestamp(userMessage.publishedAt, default=getUtcTimestamp())
await event_manager.emit_event(
context_id=workflow.id,
event_type="chatdata",
data={
"type": "message",
"createdAt": message_timestamp,
"item": userMessage.model_dump()
},
event_category="chat"
)
# Update workflow status # Update workflow status
interfaceDbChat.updateWorkflow(workflow.id, { interfaceDbChat.updateWorkflow(workflow.id, {
"status": "running", "status": "running",
@ -255,7 +241,8 @@ async def chatProcess(
userInput, userInput,
userMessage.id, userMessage.id,
featureInstanceId=featureInstanceId, featureInstanceId=featureInstanceId,
config=chatbot_config config=chatbot_config,
event_manager=event_manager
)) ))
# Reload workflow to include new message # Reload workflow to include new message
@ -407,34 +394,8 @@ async def _emit_log_and_event(
"status": status, "status": status,
"roundNumber": round_number "roundNumber": round_number
} }
# Store log in database # Store log in database (createLog emits when event_manager is provided)
created_log = interfaceDbChat.createLog(log_data) created_log = interfaceDbChat.createLog(log_data, event_manager=event_manager)
# Emit event directly for streaming (using correct signature)
if created_log and event_manager:
try:
# Convert to dict if it's a Pydantic model (ChatbotLog from createLog)
if hasattr(created_log, "model_dump"):
log_dict = created_log.model_dump()
elif hasattr(created_log, "dict"):
log_dict = created_log.dict()
else:
log_dict = log_data
await event_manager.emit_event(
context_id=workflowId,
event_type="chatdata",
data={
"type": "log",
"createdAt": log_timestamp,
"item": log_dict
},
event_category="chat",
message="New log",
step="log"
)
except Exception as emit_error:
logger.warning(f"Error emitting log event: {emit_error}")
except Exception as e: except Exception as e:
logger.error(f"Error storing log: {e}", exc_info=True) logger.error(f"Error storing log: {e}", exc_info=True)
@ -1019,7 +980,7 @@ async def _bridge_chatbot_events(
} }
try: try:
assistant_msg = interface_db_chat.createMessage(message_data) assistant_msg = interface_db_chat.createMessage(message_data, event_manager=event_manager)
final_message_stored = True final_message_stored = True
# Emit message event # Emit message event
@ -1104,7 +1065,7 @@ async def _bridge_chatbot_events(
"success": False "success": False
} }
error_msg = interface_db_chat.createMessage(error_message_data) error_msg = interface_db_chat.createMessage(error_message_data, event_manager=event_manager)
# Emit message event # Emit message event
message_timestamp = parseTimestamp(error_msg.publishedAt, default=getUtcTimestamp()) message_timestamp = parseTimestamp(error_msg.publishedAt, default=getUtcTimestamp())
@ -1272,7 +1233,8 @@ async def _processChatbotMessageLangGraph(
userInput: UserInputRequest, userInput: UserInputRequest,
userMessageId: str, userMessageId: str,
featureInstanceId: Optional[str] = None, featureInstanceId: Optional[str] = None,
config: Optional[ChatbotConfig] = None config: Optional[ChatbotConfig] = None,
event_manager=None
): ):
""" """
Process chatbot message using LangGraph. Process chatbot message using LangGraph.
@ -1285,9 +1247,8 @@ async def _processChatbotMessageLangGraph(
userInput: User input request userInput: User input request
userMessageId: User message ID userMessageId: User message ID
featureInstanceId: Optional feature instance ID for loading instance-specific config featureInstanceId: Optional feature instance ID for loading instance-specific config
event_manager: Event manager for streaming (passed from chatProcess)
""" """
event_manager = get_event_manager()
try: try:
from modules.features.chatbot.interfaceFeatureChatbot import getInterface as getChatbotInterface from modules.features.chatbot.interfaceFeatureChatbot import getInterface as getChatbotInterface
interfaceDbChat = getChatbotInterface(currentUser, mandateId=services.mandateId, featureInstanceId=featureInstanceId) interfaceDbChat = getChatbotInterface(currentUser, mandateId=services.mandateId, featureInstanceId=featureInstanceId)
@ -1376,7 +1337,8 @@ async def _processChatbotMessageLangGraph(
memory=memory, memory=memory,
system_prompt=system_prompt, system_prompt=system_prompt,
workflow_id=workflowId, workflow_id=workflowId,
config=config config=config,
event_manager=event_manager
) )
# Emit synthetic status for real-time UI feedback # Emit synthetic status for real-time UI feedback
@ -1436,7 +1398,7 @@ async def _processChatbotMessageLangGraph(
"taskNumber": 0, "taskNumber": 0,
"actionNumber": 0 "actionNumber": 0
} }
errorMessage = interfaceDbChat.createMessage(errorMessageData) errorMessage = interfaceDbChat.createMessage(errorMessageData, event_manager=event_manager)
# Emit message event # Emit message event
message_timestamp = parseTimestamp(errorMessage.publishedAt, default=getUtcTimestamp()) message_timestamp = parseTimestamp(errorMessage.publishedAt, default=getUtcTimestamp())

View file

@ -1,3 +0,0 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Streaming infrastructure for chatbot events."""

View file

@ -17,7 +17,7 @@ from modules.auth import limiter, getRequestContext, RequestContext
from modules.interfaces import interfaceDbChat, interfaceDbManagement from modules.interfaces import interfaceDbChat, interfaceDbManagement
from modules.interfaces.interfaceAiObjects import AiObjects from modules.interfaces.interfaceAiObjects import AiObjects
from modules.datamodels.datamodelChat import UserInputRequest from modules.datamodels.datamodelChat import UserInputRequest
from modules.features.chatbot.streaming.events import get_event_manager from modules.services.serviceStreaming import get_event_manager
from modules.features.codeeditor import codeEditorProcessor, fileContextManager from modules.features.codeeditor import codeEditorProcessor, fileContextManager
from modules.features.codeeditor.datamodelCodeeditor import FileEditProposal, EditStatusEnum from modules.features.codeeditor.datamodelCodeeditor import FileEditProposal, EditStatusEnum

View file

@ -1003,7 +1003,7 @@ class ChatObjects:
totalPages=totalPages totalPages=totalPages
) )
def createMessage(self, messageData: Dict[str, Any]) -> ChatMessage: def createMessage(self, messageData: Dict[str, Any], event_manager=None) -> ChatMessage:
"""Creates a message for a workflow if user has access.""" """Creates a message for a workflow if user has access."""
try: try:
# Ensure ID is present # Ensure ID is present
@ -1121,25 +1121,23 @@ class ChatObjects:
actionName=createdMessage.get("actionName") actionName=createdMessage.get("actionName")
) )
# Emit message event for streaming (if event manager is available) # Emit message event for streaming (if event manager is provided)
try: if event_manager:
from modules.features.chatbot.streaming.events import get_event_manager try:
event_manager = get_event_manager() message_timestamp = parseTimestamp(chat_message.publishedAt, default=getUtcTimestamp())
message_timestamp = parseTimestamp(chat_message.publishedAt, default=getUtcTimestamp()) # Emit message event in exact chatData format: {type, createdAt, item}
# Emit message event in exact chatData format: {type, createdAt, item} asyncio.create_task(event_manager.emit_event(
asyncio.create_task(event_manager.emit_event( context_id=workflowId,
context_id=workflowId, event_type="chatdata",
event_type="chatdata", data={
data={ "type": "message",
"type": "message", "createdAt": message_timestamp,
"createdAt": message_timestamp, "item": chat_message.dict()
"item": chat_message.dict() },
}, event_category="chat"
event_category="chat" ))
)) except Exception as e:
except Exception as e: logger.debug(f"Could not emit message event: {e}")
# Event manager not available or error - continue without emitting
logger.debug(f"Could not emit message event: {e}")
# Debug: Store message and documents for debugging - only if debug enabled # Debug: Store message and documents for debugging - only if debug enabled
storeDebugMessageAndDocuments(chat_message, self.currentUser, mandateId=self.mandateId, featureInstanceId=self.featureInstanceId) storeDebugMessageAndDocuments(chat_message, self.currentUser, mandateId=self.mandateId, featureInstanceId=self.featureInstanceId)
@ -1447,7 +1445,7 @@ class ChatObjects:
totalPages=totalPages totalPages=totalPages
) )
def createLog(self, logData: Dict[str, Any]) -> ChatLog: def createLog(self, logData: Dict[str, Any], event_manager=None) -> ChatLog:
"""Creates a log entry for a workflow if user has access.""" """Creates a log entry for a workflow if user has access."""
# Check workflow access # Check workflow access
workflowId = logData.get("workflowId") workflowId = logData.get("workflowId")
@ -1498,25 +1496,23 @@ class ChatObjects:
# Create log in normalized table # Create log in normalized table
createdLog = self.db.recordCreate(ChatLog, log_model) createdLog = self.db.recordCreate(ChatLog, log_model)
# Emit log event for streaming (if event manager is available) # Emit log event for streaming (if event manager is provided)
try: if event_manager:
from modules.features.chatbot.streaming.events import get_event_manager try:
event_manager = get_event_manager() log_timestamp = parseTimestamp(createdLog.get("timestamp"), default=getUtcTimestamp())
log_timestamp = parseTimestamp(createdLog.get("timestamp"), default=getUtcTimestamp()) # Emit log event in exact chatData format: {type, createdAt, item}
# Emit log event in exact chatData format: {type, createdAt, item} asyncio.create_task(event_manager.emit_event(
asyncio.create_task(event_manager.emit_event( context_id=workflowId,
context_id=workflowId, event_type="chatdata",
event_type="chatdata", data={
data={ "type": "log",
"type": "log", "createdAt": log_timestamp,
"createdAt": log_timestamp, "item": ChatLog(**createdLog).dict()
"item": ChatLog(**createdLog).dict() },
}, event_category="chat"
event_category="chat" ))
)) except Exception as e:
except Exception as e: logger.debug(f"Could not emit log event: {e}")
# Event manager not available or error - continue without emitting
logger.debug(f"Could not emit log event: {e}")
# Return validated ChatLog instance # Return validated ChatLog instance
return ChatLog(**createdLog) return ChatLog(**createdLog)

View file

@ -107,6 +107,9 @@ class Services:
from .serviceMessaging.mainServiceMessaging import MessagingService from .serviceMessaging.mainServiceMessaging import MessagingService
self.messaging = PublicService(MessagingService(self)) self.messaging = PublicService(MessagingService(self))
from .serviceStreaming.mainServiceStreaming import StreamingService
self.streaming = PublicService(StreamingService(self))
# ============================================================ # ============================================================
# AI SERVICES (from modules/services/) # AI SERVICES (from modules/services/)
# ============================================================ # ============================================================

View file

@ -0,0 +1,8 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Streaming service for SSE event management across features."""
from .eventManager import EventManager, get_event_manager
from .helpers import ChatStreamingHelper
__all__ = ["EventManager", "get_event_manager", "ChatStreamingHelper"]

View file

@ -1,21 +1,20 @@
# Copyright (c) 2025 Patrick Motsch # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
""" """
Event manager for chatbot streaming. Event manager for SSE streaming.
Manages event queues for Server-Sent Events (SSE) streaming. Manages event queues for Server-Sent Events (SSE) streaming across features.
""" """
import logging import logging
import asyncio import asyncio
from typing import Dict, Optional, Any from typing import Dict, Optional, Any
from collections import defaultdict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class EventManager: class EventManager:
""" """
Manages event queues for chatbot streaming. Manages event queues for SSE streaming.
Each workflow has its own async queue for events. Each workflow has its own async queue for events.
""" """

View file

@ -0,0 +1,36 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Streaming service for SSE event management.
Provides access to the global event manager for workflow streaming.
"""
import logging
from typing import Any
from modules.services.serviceStreaming.eventManager import EventManager, get_event_manager
logger = logging.getLogger(__name__)
class StreamingService:
"""
Streaming service providing access to SSE event infrastructure.
"""
def __init__(self, services: Any):
"""Initialize streaming service with service center access.
Args:
services: Service center instance providing access to interfaces
"""
self.services = services
def getEventManager(self) -> EventManager:
"""
Get the global event manager instance for SSE streaming.
Returns:
EventManager instance
"""
return get_event_manager()