diff --git a/AppImage/scripts/notification_manager.py b/AppImage/scripts/notification_manager.py index 8a0422a8..df448e6f 100644 --- a/AppImage/scripts/notification_manager.py +++ b/AppImage/scripts/notification_manager.py @@ -36,7 +36,7 @@ if BASE_DIR not in sys.path: from notification_channels import create_channel, CHANNEL_TYPES from notification_templates import ( - render_template, format_with_ai, enrich_with_emojis, TEMPLATES, + render_template, format_with_ai, format_with_ai_full, enrich_with_emojis, TEMPLATES, EVENT_GROUPS, get_event_types_by_group, get_default_enabled_events ) from notification_events import ( @@ -743,12 +743,14 @@ class NotificationManager: # ── Per-channel AI enhancement ── # Apply AI with channel-specific detail level and emoji setting # If AI is enabled AND rich_format is on, AI will include emojis directly - ch_body = format_with_ai( + ai_result = format_with_ai_full( ch_title, ch_body, severity, ai_config, detail_level=detail_level, journal_context=journal_context, use_emojis=use_rich_format ) + ch_title = ai_result.get('title', ch_title) + ch_body = ai_result.get('body', ch_body) # Fallback emoji enrichment only if AI is disabled but rich_format is on # (If AI processed the message with emojis, this is skipped) @@ -1055,17 +1057,19 @@ class NotificationManager: rich_key = f'{ch_name}.rich_format' use_rich_format = self._config.get(rich_key, 'false') == 'true' - ch_message = format_with_ai( + ai_result = format_with_ai_full( title, message, severity, ai_config, detail_level=detail_level, use_emojis=use_rich_format ) + ch_title = ai_result.get('title', title) + ch_message = ai_result.get('body', message) - result = channel.send(title, ch_message, severity, data) + result = channel.send(ch_title, ch_message, severity, data) results[ch_name] = result self._record_history( - event_type, ch_name, title, ch_message, severity, + event_type, ch_name, ch_title, ch_message, severity, result.get('success', False), result.get('error', ''), source @@ -1138,7 +1142,7 @@ class NotificationManager: } # ProxMenux logo for welcome message - logo_url = 'https://macrimi.github.io/ProxMenux/logo.png' + logo_url = 'https://proxmenux.com/telegram.png' for ch_name, channel in targets.items(): try: @@ -1150,14 +1154,16 @@ class NotificationManager: use_rich_format = self._config.get(rich_key, 'false') == 'true' # Apply AI enhancement (translates to configured language) - enhanced_message = format_with_ai( + ai_result = format_with_ai_full( base_title, base_message, 'INFO', ai_config, detail_level=detail_level, use_emojis=use_rich_format ) + enhanced_title = ai_result.get('title', base_title) + enhanced_message = ai_result.get('body', base_message) # Send message - send_result = channel.send(base_title, enhanced_message, 'INFO') + send_result = channel.send(enhanced_title, enhanced_message, 'INFO') success = send_result.get('success', False) error = send_result.get('error', '') @@ -1168,7 +1174,7 @@ class NotificationManager: results[ch_name] = {'success': success, 'error': error} self._record_history( - 'test', ch_name, base_title, + 'test', ch_name, enhanced_title, enhanced_message[:500], 'INFO', success, error, 'api' ) diff --git a/AppImage/scripts/notification_templates.py b/AppImage/scripts/notification_templates.py index cc587845..495197e8 100644 --- a/AppImage/scripts/notification_templates.py +++ b/AppImage/scripts/notification_templates.py @@ -1244,13 +1244,14 @@ AI_SYSTEM_PROMPT = """You are a technical assistant for ProxMenux Monitor, a Pro Your task is to translate and format system alerts to {language}. STRICT RULES: -1. Translate the message to the requested language -2. Maintain an INFORMATIVE and OBJECTIVE tone -3. DO NOT use formal introductions ("Dear...", "Esteemed...") -4. DO NOT give recommendations or action suggestions -5. DO NOT interpret data subjectively -6. Present only FACTS and TECHNICAL DATA -7. Respect the requested detail level: {detail_level} +1. Translate BOTH the title and message body to {language} +2. DO NOT use markdown formatting like **bold** or *italic* +3. Use plain text only - no special formatting syntax +4. Maintain an INFORMATIVE and OBJECTIVE tone +5. DO NOT use formal introductions ("Dear...", "Esteemed...") +6. DO NOT give recommendations or action suggestions +7. Present only FACTS and TECHNICAL DATA +8. Respect the requested detail level: {detail_level} {emoji_instructions} DETAIL LEVELS: @@ -1262,30 +1263,71 @@ MESSAGE TYPES: - Some messages come from Proxmox VE webhooks with raw system data (backup logs, update lists, SMART errors) - Parse and present this data clearly, extracting key information (VM IDs, sizes, durations, errors) - For backup messages: highlight status (OK/ERROR), VM names, sizes, and duration -- For update messages: list package names and counts +- For update messages: list package names and counts clearly formatted - For disk/SMART errors: highlight affected device and error type +OUTPUT FORMAT (VERY IMPORTANT): +You MUST return the response in this exact format with these exact markers: +[TITLE] +Translated title here +[BODY] +Translated message body here + +- The [TITLE] section should contain ONLY the translated title (short, one line) +- The [BODY] section contains the translated and formatted message +- Do NOT include the markers [TITLE] or [BODY] as part of the content +- Start body content directly (emoji if enabled, then text) + If journal log context is provided, use it for more precise event information.""" # Emoji instructions for rich format channels AI_EMOJI_INSTRUCTIONS = """ -8. ENRICH with contextual emojis and icons: - - Use appropriate emojis at the START of the title/message to indicate severity and type - - Severity indicators: Use a colored circle at the start (info=blue, warning=yellow, critical=red) - - Add relevant technical emojis: disk, server, network, security, backup, etc. - - Keep emojis contextual and professional, not decorative - - Examples of appropriate emojis: - * Disk/Storage: disk, folder, file - * Network: globe, signal, connection - * Security: shield, lock, key, warning - * System: gear, server, computer - * Status: checkmark, cross, warning, info - * Backup: save, sync, cloud - * Performance: chart, speedometer""" +10. ENRICH with contextual emojis: + - Start with a severity indicator circle: (blue=info), (yellow=warning), (red=critical) + - Add specific emojis for each data item, not just at the start + - Use emojis that match the content type precisely: + + UPDATES/PACKAGES: + - Total updates count + - Security updates + - Proxmox updates + - Kernel updates + - Package list items (bullet points) + + BACKUP/STORAGE: + - Backup status + - Storage/disk + - Sync/transfer + - Folder/directory + - Size/capacity + + SYSTEM/HARDWARE: + - Server/host + - Container/VM + - CPU/processor + - Memory/RAM + - Temperature + + NETWORK: + - Network/connection + - Speed/bandwidth + - Globe/internet + + SECURITY/ALERTS: + - Warning/alert + - Security/shield + - Error/problem + - Lock/authentication + + STATUS: + - Success/OK + - Failed/error + - Running/active + - Stopped/inactive""" # No emoji instructions for email/plain channels AI_NO_EMOJI_INSTRUCTIONS = """ -8. DO NOT use emojis or special icons - plain text only for email compatibility""" +10. DO NOT use emojis or special icons - plain text only for email compatibility""" class AIEnhancer: @@ -1343,7 +1385,7 @@ class AIEnhancer: def enhance(self, title: str, body: str, severity: str, detail_level: str = 'standard', journal_context: str = '', - use_emojis: bool = False) -> Optional[str]: + use_emojis: bool = False) -> Optional[Dict[str, str]]: """Enhance/translate notification with AI. Args: @@ -1355,7 +1397,7 @@ class AIEnhancer: use_emojis: Whether to include emojis in the response (for push channels) Returns: - Enhanced/translated text or None if failed + Dict with 'title' and 'body' keys, or None if failed """ if not self._provider: return None @@ -1384,11 +1426,49 @@ class AIEnhancer: try: result = self._provider.generate(system_prompt, user_msg, max_tokens) - return result + return self._parse_ai_response(result, title, body) except Exception as e: print(f"[AIEnhancer] Enhancement failed: {e}") return None + def _parse_ai_response(self, response: str, original_title: str, original_body: str) -> Dict[str, str]: + """Parse AI response to extract title and body. + + Args: + response: Raw AI response text + original_title: Original title as fallback + original_body: Original body as fallback + + Returns: + Dict with 'title' and 'body' keys + """ + if not response: + return {'title': original_title, 'body': original_body} + + # Try to parse [TITLE] and [BODY] markers + title_marker = '[TITLE]' + body_marker = '[BODY]' + + title_start = response.find(title_marker) + body_start = response.find(body_marker) + + if title_start != -1 and body_start != -1: + # Extract title (between [TITLE] and [BODY]) + title_content = response[title_start + len(title_marker):body_start].strip() + # Extract body (after [BODY]) + body_content = response[body_start + len(body_marker):].strip() + + return { + 'title': title_content if title_content else original_title, + 'body': body_content if body_content else original_body + } + + # Fallback: if markers not found, use whole response as body + return { + 'title': original_title, + 'body': response.strip() + } + def test_connection(self) -> Dict[str, Any]: """Test the AI provider connection. @@ -1426,22 +1506,47 @@ def format_with_ai(title: str, body: str, severity: str, Returns: Enhanced body string or original if AI fails """ + result = format_with_ai_full(title, body, severity, ai_config, detail_level, journal_context, use_emojis) + return result.get('body', body) + + +def format_with_ai_full(title: str, body: str, severity: str, + ai_config: Dict[str, Any], + detail_level: str = 'standard', + journal_context: str = '', + use_emojis: bool = False) -> Dict[str, str]: + """Format a message with AI enhancement/translation, returning both title and body. + + Args: + title: Notification title + body: Notification body + severity: Severity level + ai_config: Configuration dictionary with AI settings + detail_level: Level of detail (brief, standard, detailed) + journal_context: Optional journal log context + use_emojis: Whether to include emojis (for push channels like Telegram/Discord) + + Returns: + Dict with 'title' and 'body' keys (translated/enhanced) + """ + default_result = {'title': title, 'body': body} + # Check if AI is enabled ai_enabled = ai_config.get('ai_enabled') if isinstance(ai_enabled, str): ai_enabled = ai_enabled.lower() == 'true' if not ai_enabled: - return body + return default_result # Check for API key (not required for Ollama) provider = ai_config.get('ai_provider', 'groq') if provider != 'ollama' and not ai_config.get('ai_api_key'): - return body + return default_result # For Ollama, check URL is configured if provider == 'ollama' and not ai_config.get('ai_ollama_url'): - return body + return default_result # Create enhancer and process enhancer = AIEnhancer(ai_config) @@ -1452,15 +1557,19 @@ def format_with_ai(title: str, body: str, severity: str, use_emojis=use_emojis ) - # Return enhanced text if successful, otherwise original - if enhanced: + # Return enhanced result if successful, otherwise original + if enhanced and isinstance(enhanced, dict): + result_title = enhanced.get('title', title) + result_body = enhanced.get('body', body) + # For detailed level (email), append original message for reference # This ensures full technical data is available even after AI processing if detail_level == 'detailed' and body and len(body) > 50: # Only append if original has substantial content - enhanced += "\n\n" + "-" * 40 + "\n" - enhanced += "Original message:\n" - enhanced += body - return enhanced + result_body += "\n\n" + "-" * 40 + "\n" + result_body += "Original message:\n" + result_body += body + + return {'title': result_title, 'body': result_body} - return body + return default_result