"""AI Proactive Participant Handler — bridges the kommunikation plugin with the proactive AI. When a message is received in a conversation that includes the 'ai_proactive' participant, this handler generates a proactive suggestion based on the message context. """ from __future__ import annotations import logging import uuid from typing import Any from app.core.db import create_db_session from app.plugins.builtins.kommunikation.participant_registry import ParticipantHandler logger = logging.getLogger(__name__) class AIProactiveParticipantHandler(ParticipantHandler): """Handles proactive AI suggestions as a participant in kommunikation conversations.""" 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: """Generate a proactive suggestion when ai_proactive is a participant. Checks if 'ai_proactive' is in the conversation participants as a participant_type. If yes, generates a suggestion based on the message. Returns a list with one message dict containing the suggestion as an action card, or None on error. """ # Check if 'ai_proactive' is a participant in this conversation participants = conversation.get("participants", []) ai_proactive_is_participant = any( p.get("participant_type") == "ai_proactive" for p in participants ) if not ai_proactive_is_participant: return None # Don't respond to our own messages if message.get("sender_type") == "ai_proactive": return None # Get tenant_id and user_id from context tenant_id_str = context.get("tenant_id") or message.get("tenant_id") user_id_str = context.get("user_id") if not tenant_id_str: logger.warning("AIProactiveParticipantHandler: missing tenant_id in context") return None try: tenant_id = uuid.UUID(str(tenant_id_str)) user_id = uuid.UUID(str(user_id_str)) if user_id_str else None except (ValueError, TypeError): logger.warning("AIProactiveParticipantHandler: invalid UUID in context") return None # Generate suggestion using the existing generate_suggestion function try: from app.plugins.builtins.ai_proactive.services import ( generate_suggestion, get_user_settings, ) async with create_db_session(tenant_id) as db: # Get user settings for the proactive AI if user_id: settings = await get_user_settings(db, tenant_id, user_id) else: # Create minimal default settings if no user_id from app.plugins.builtins.ai_proactive.models import ProactiveSettings settings = ProactiveSettings( tenant_id=tenant_id, user_id=user_id or uuid.uuid4(), enabled=True, suggestion_categories=["mail", "tasks", "contacts", "companies", "insights"], confidence_threshold=0.5, rate_limit_seconds=10, model="ollama/deepseek-v4-flash", ) if not settings.enabled: return None # Build context data from the message context_data: dict[str, Any] = { "entity_type": "message", "message": message.get("content", ""), "conversation_id": str(conversation_id), "sender_type": message.get("sender_type", "user"), } # Generate suggestion via LLM suggestion = await generate_suggestion( context_data, settings, db=db, tenant_id=tenant_id ) if not suggestion: return None # Build the response message with an action card block suggestion_text = suggestion.get("content", "") title = suggestion.get("title", "KI Vorschlag") suggestion_type = suggestion.get("suggestion_type", "info") confidence = suggestion.get("confidence", 0.5) actions = suggestion.get("actions", []) return [ { "content": suggestion_text, "content_format": "markdown", "blocks": [ { "block_type": "action_card", "block_data": { "title": title, "suggestion_type": suggestion_type, "confidence": confidence, "actions": actions, }, } ], } ] except Exception: logger.exception("AIProactiveParticipantHandler: error generating suggestion") return None async def handle_event(self, payload: dict[str, Any]) -> None: """Handle a message.received event from the event bus. Extracts conversation_id from the payload, loads the conversation, and calls on_message_received. If a response is generated, sends it back to the conversation. """ conversation_id_str = payload.get("conversation_id") tenant_id_str = payload.get("tenant_id") message_content = payload.get("content", "") message_id = payload.get("message_id") sender_type = payload.get("sender_type", "user") sender_id_str = payload.get("sender_id") if not conversation_id_str or not tenant_id_str: logger.warning("AIProactiveParticipantHandler.handle_event: missing conversation_id or tenant_id") return try: tenant_id = uuid.UUID(str(tenant_id_str)) conversation_id = uuid.UUID(str(conversation_id_str)) except (ValueError, TypeError): logger.warning("AIProactiveParticipantHandler.handle_event: invalid UUID in payload") return # Build the message dict message: dict[str, Any] = { "id": message_id, "content": message_content, "sender_type": sender_type, "tenant_id": tenant_id_str, } # Load conversation try: from app.plugins.builtins.kommunikation.services import get_conversation async with create_db_session(tenant_id) as db: if not sender_id_str: logger.warning("AIProactiveParticipantHandler.handle_event: missing sender_id") return user_id = uuid.UUID(str(sender_id_str)) conversation = await get_conversation(db, tenant_id, conversation_id, user_id) if not conversation: logger.warning("AIProactiveParticipantHandler.handle_event: conversation not found") return # Parse mentions from message content from app.plugins.builtins.kommunikation.services import parse_mentions mentions = parse_mentions(message_content) context: dict[str, Any] = { "tenant_id": tenant_id_str, "user_id": sender_id_str, } # Call on_message_received response_messages = await self.on_message_received( conversation_id, message, conversation, mentions, context ) # If we got a response, send it to the conversation if response_messages: from app.plugins.builtins.kommunikation.services import send_message for resp_msg in response_messages: await send_message( db=db, tenant_id=tenant_id, conversation_id=conversation_id, sender_id=None, sender_type="ai_proactive", content=resp_msg.get("content", ""), content_format=resp_msg.get("content_format", "text"), blocks=resp_msg.get("blocks"), ) await db.commit() except Exception: logger.exception("AIProactiveParticipantHandler.handle_event: error processing event") def get_participant_info(self) -> dict[str, Any]: """Return metadata about this participant.""" return { "display_name": "Live KI", "capabilities": ["proactive", "context_aware", "heartbeat"], "description": "Proaktive KI mit Kontextbewusstsein und Heartbeat", }