feat: Unified Messaging System — kommunikation plugin, AI/Proactive/System participants, MessageSidebar, Rich Content Renderer
Phase 1: Backend plugin kommunikation (13 files, 10 tables, REST API, WebSocket, RBAC, DMS Bridge, Participant Registry, Mini-App Registry, Search Provider) Phase 2: AI plugins as participants (ai_assistant + ai_proactive dock as participants, heartbeat job) Phase 3: system_notif plugin (system events → chat messages, pinned System room) Phase 4: Frontend MessageSidebar (replaces AISidebar, same design, comm API client, WebSocket hook, commStore) Phase 5: Rich Content Block Renderer (11 components: Markdown, HTML, Image, Audio, Video, File, ActionCard, ContactCard, MiniApp, BlockRenderer) BasePlugin: added services property + _container in on_activate
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""Participant Registry — Andockpunkt für Plugins als Teilnehmer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ParticipantHandler(ABC):
|
||||
"""Interface that plugins implement to act as a conversation participant."""
|
||||
|
||||
@abstractmethod
|
||||
async def on_message_received(
|
||||
self,
|
||||
conversation_id: Any,
|
||||
message: dict[str, Any],
|
||||
conversation: dict[str, Any],
|
||||
mentions: list[str],
|
||||
context: dict[str, Any],
|
||||
) -> list[dict[str, Any]] | None:
|
||||
"""Called when a new message arrives in a conversation this participant is part of.
|
||||
|
||||
Args:
|
||||
conversation_id: UUID of the conversation.
|
||||
message: The new message dict.
|
||||
conversation: Full conversation dict with participants.
|
||||
mentions: Parsed @mention participant types.
|
||||
context: Tenant, user, and other context.
|
||||
|
||||
Returns:
|
||||
Optional list of new message dicts (e.g. AI response).
|
||||
For passive readers (system) → return None.
|
||||
For reactive participants (ai) → return [message_dict, ...].
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_participant_info(self) -> dict[str, Any]:
|
||||
"""Return metadata: display_name, avatar_url, capabilities, description."""
|
||||
pass
|
||||
|
||||
|
||||
class ParticipantRegistry:
|
||||
"""Global registry for plugin participants."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._handlers: dict[str, ParticipantHandler] = {}
|
||||
|
||||
def register(self, participant_type: str, handler: ParticipantHandler) -> None:
|
||||
"""Register a plugin as a participant type."""
|
||||
self._handlers[participant_type] = handler
|
||||
logger.info(f"Participant registered: {participant_type}")
|
||||
|
||||
def unregister(self, participant_type: str) -> None:
|
||||
"""Unregister a participant type."""
|
||||
handler = self._handlers.pop(participant_type, None)
|
||||
if handler:
|
||||
logger.info(f"Participant unregistered: {participant_type}")
|
||||
|
||||
def get_handler(self, participant_type: str) -> ParticipantHandler | None:
|
||||
"""Get the handler for a participant type."""
|
||||
return self._handlers.get(participant_type)
|
||||
|
||||
def list_types(self) -> list[str]:
|
||||
"""List all registered participant types."""
|
||||
return list(self._handlers.keys())
|
||||
|
||||
def get_all_info(self) -> dict[str, dict[str, Any]]:
|
||||
"""Get info for all registered participants."""
|
||||
return {ptype: handler.get_participant_info() for ptype, handler in self._handlers.items()}
|
||||
|
||||
|
||||
# Global instance
|
||||
_registry = ParticipantRegistry()
|
||||
|
||||
|
||||
def get_participant_registry() -> ParticipantRegistry:
|
||||
"""Get the global participant registry."""
|
||||
return _registry
|
||||
|
||||
|
||||
def reset_participant_registry_for_testing() -> ParticipantRegistry:
|
||||
"""Create a fresh registry for testing."""
|
||||
global _registry
|
||||
_registry = ParticipantRegistry()
|
||||
return _registry
|
||||
Reference in New Issue
Block a user