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:
Agent Zero
2026-07-22 01:22:15 +02:00
parent 5980d38c66
commit cc3ac9a43d
45 changed files with 8154 additions and 113 deletions
@@ -0,0 +1,46 @@
"""System participant handler — passive participant that only reads."""
from __future__ import annotations
import logging
from typing import Any
from app.plugins.builtins.kommunikation.participant_registry import ParticipantHandler
logger = logging.getLogger(__name__)
class SystemParticipantHandler(ParticipantHandler):
"""System participant — passive, does not respond to messages.
The system participant only sends messages triggered by system events
(handled in the plugin's event handlers). It does not respond to
user messages in the chat.
"""
def __init__(self, container: Any) -> None:
self._container = container
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:
"""System is passive — does not respond to messages.
Users can interact with action_card buttons (open, archive) which
are handled via the frontend, not via new messages.
"""
return None
def get_participant_info(self) -> dict[str, Any]:
"""Return participant metadata."""
return {
"display_name": "System",
"avatar_url": None,
"capabilities": ["notifications", "action_cards"],
"description": "System-Benachrichtigungen",
}
+223
View File
@@ -0,0 +1,223 @@
"""System Notification plugin — converts system events into chat messages."""
from __future__ import annotations
import logging
from typing import Any
from app.plugins.base import BasePlugin
from app.plugins.manifest import PluginManifest
logger = logging.getLogger(__name__)
class SystemNotifPlugin(BasePlugin):
"""System notifications as a participant in the kommunikation plugin."""
manifest = PluginManifest(
name="system_notif",
version="1.0.0",
display_name="System Benachrichtigungen",
description=(
"Wandelt System-Events in Chat-Nachrichten um. "
"Registriert sich als 'system' Teilnehmer am kommunikation Plugin."
),
dependencies=["kommunikation"],
routes=[],
events=[
"lead.created",
"contact.created",
"contact.updated",
"task.overdue",
"task.created",
"mail.received",
"user.created",
"workflow.completed",
"notification.created",
],
migrations=[],
permissions=["system_notif:read"],
is_core=False,
)
def __init__(self) -> None:
super().__init__()
self._system_handler = None
async def on_activate(self, db, service_container, event_bus) -> None:
"""Register as system participant and subscribe to events."""
await super().on_activate(db, service_container, event_bus)
from app.plugins.builtins.system_notif.participant_handler import SystemParticipantHandler
from app.plugins.builtins.kommunikation.participant_registry import get_participant_registry
self._system_handler = SystemParticipantHandler(service_container)
registry = get_participant_registry()
registry.register("system", self._system_handler)
logger.info("System notification plugin activated — registered as 'system' participant")
async def on_deactivate(self, db, service_container, event_bus) -> None:
"""Unregister participant."""
from app.plugins.builtins.kommunikation.participant_registry import get_participant_registry
get_participant_registry().unregister("system")
self._system_handler = None
await super().on_deactivate(db, service_container, event_bus)
logger.info("System notification plugin deactivated")
# ─── Event Handlers ───
async def on_lead_created(self, payload: dict[str, Any]) -> None:
"""Handle lead.created event → system message."""
await self._create_system_notification(payload, event_type="lead.created")
async def on_contact_created(self, payload: dict[str, Any]) -> None:
"""Handle contact.created event → system message."""
await self._create_system_notification(payload, event_type="contact.created")
async def on_contact_updated(self, payload: dict[str, Any]) -> None:
"""Handle contact.updated event → system message."""
await self._create_system_notification(payload, event_type="contact.updated")
async def on_task_overdue(self, payload: dict[str, Any]) -> None:
"""Handle task.overdue event → system message (warning)."""
await self._create_system_notification(payload, event_type="task.overdue", severity="warning")
async def on_task_created(self, payload: dict[str, Any]) -> None:
"""Handle task.created event → system message."""
await self._create_system_notification(payload, event_type="task.created")
async def on_mail_received(self, payload: dict[str, Any]) -> None:
"""Handle mail.received event → system message."""
await self._create_system_notification(payload, event_type="mail.received")
async def on_user_created(self, payload: dict[str, Any]) -> None:
"""Handle user.created event → system message."""
await self._create_system_notification(payload, event_type="user.created")
async def on_workflow_completed(self, payload: dict[str, Any]) -> None:
"""Handle workflow.completed event → system message."""
await self._create_system_notification(payload, event_type="workflow.completed")
async def on_notification_created(self, payload: dict[str, Any]) -> None:
"""Handle notification.created event → system message.
This is the bridge from the core notification system to the kommunikation plugin.
Core code publishes 'notification.created' on the EventBus, this plugin
converts it into a comm_message in the user's System room.
"""
await self._create_system_notification(payload, event_type="notification.created")
async def _create_system_notification(
self,
payload: dict[str, Any],
event_type: str,
severity: str = "info",
) -> None:
"""Create a system notification message in the user's System room."""
import uuid
from app.core.db import create_db_session
from app.plugins.builtins.kommunikation.services import create_plugin_room, send_message
tenant_id_str = payload.get("tenant_id")
user_id_str = payload.get("user_id")
if not tenant_id_str or not user_id_str:
logger.warning("System notification missing tenant_id or user_id in payload: %s", payload)
return
try:
tenant_id = uuid.UUID(tenant_id_str)
user_id = uuid.UUID(user_id_str)
except (ValueError, TypeError):
logger.warning("System notification invalid UUIDs: tenant=%s user=%s", tenant_id_str, user_id_str)
return
# Build notification content from payload
title = payload.get("title", "")
body = payload.get("body", "")
action_url = payload.get("action_url", "")
if not title:
# Generate title from event type
event_titles = {
"lead.created": "Neuer Lead",
"contact.created": "Neuer Kontakt",
"contact.updated": "Kontakt aktualisiert",
"task.overdue": "Aufgabe überfällig",
"task.created": "Neue Aufgabe",
"mail.received": "Neue E-Mail",
"user.created": "Neuer Benutzer",
"workflow.completed": "Workflow abgeschlossen",
"notification.created": "Benachrichtigung",
}
title = event_titles.get(event_type, event_type)
content = f"**{title}**"
if body:
content += f"\n{body}"
# Build action_card block if action_url is present
blocks = []
if action_url:
blocks.append({
"block_type": "action_card",
"block_data": {
"title": title,
"body": body or "",
"actions": [
{"label": "Öffnen", "action": action_url, "type": "primary"},
{"label": "Archivieren", "action": "dismiss", "type": "secondary"},
],
},
})
async with create_db_session(tenant_id) as db:
# Ensure System room exists for this user
await create_plugin_room(
db, tenant_id, user_id,
plugin_name="system_notif",
title="System",
participant_type="system",
user_role="reader",
)
# Find the System room conversation
from sqlalchemy import select
from app.plugins.builtins.kommunikation.models import CommConversation, CommParticipant
result = await db.execute(
select(CommConversation).where(
CommConversation.tenant_id == tenant_id,
CommConversation.title == "System",
CommConversation.is_locked == True,
CommConversation.locked_by == "system_notif",
CommConversation.deleted_at.is_(None),
).join(CommParticipant, CommParticipant.conversation_id == CommConversation.id).where(
CommParticipant.participant_id == user_id,
CommParticipant.participant_type == "user",
CommParticipant.left_at.is_(None),
)
)
conv = result.scalar_one_or_none()
if conv is None:
logger.warning("Could not find System room for user %s", user_id)
return
# Send the system message
await send_message(
db, tenant_id, conv.id,
sender_id=None,
sender_type="system",
content=content,
content_format="markdown",
blocks=blocks if blocks else None,
metadata={"event_type": event_type, "severity": severity},
)
logger.info("System notification created: %s for user %s", event_type, user_id)
def get_notification_types(self) -> list[dict[str, Any]]:
return []