Update notification service

This commit is contained in:
MacRimi
2026-03-17 19:22:12 +01:00
parent ff6904d436
commit 71505362b4
2 changed files with 159 additions and 44 deletions

View File

@@ -36,7 +36,7 @@ if BASE_DIR not in sys.path:
from notification_channels import create_channel, CHANNEL_TYPES from notification_channels import create_channel, CHANNEL_TYPES
from notification_templates import ( 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 EVENT_GROUPS, get_event_types_by_group, get_default_enabled_events
) )
from notification_events import ( from notification_events import (
@@ -743,12 +743,14 @@ class NotificationManager:
# ── Per-channel AI enhancement ── # ── Per-channel AI enhancement ──
# Apply AI with channel-specific detail level and emoji setting # Apply AI with channel-specific detail level and emoji setting
# If AI is enabled AND rich_format is on, AI will include emojis directly # 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, ch_title, ch_body, severity, ai_config,
detail_level=detail_level, detail_level=detail_level,
journal_context=journal_context, journal_context=journal_context,
use_emojis=use_rich_format 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 # Fallback emoji enrichment only if AI is disabled but rich_format is on
# (If AI processed the message with emojis, this is skipped) # (If AI processed the message with emojis, this is skipped)
@@ -1055,17 +1057,19 @@ class NotificationManager:
rich_key = f'{ch_name}.rich_format' rich_key = f'{ch_name}.rich_format'
use_rich_format = self._config.get(rich_key, 'false') == 'true' 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, title, message, severity, ai_config,
detail_level=detail_level, detail_level=detail_level,
use_emojis=use_rich_format 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 results[ch_name] = result
self._record_history( 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('success', False),
result.get('error', ''), result.get('error', ''),
source source
@@ -1138,7 +1142,7 @@ class NotificationManager:
} }
# ProxMenux logo for welcome message # 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(): for ch_name, channel in targets.items():
try: try:
@@ -1150,14 +1154,16 @@ class NotificationManager:
use_rich_format = self._config.get(rich_key, 'false') == 'true' use_rich_format = self._config.get(rich_key, 'false') == 'true'
# Apply AI enhancement (translates to configured language) # 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, base_title, base_message, 'INFO', ai_config,
detail_level=detail_level, detail_level=detail_level,
use_emojis=use_rich_format use_emojis=use_rich_format
) )
enhanced_title = ai_result.get('title', base_title)
enhanced_message = ai_result.get('body', base_message)
# Send 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) success = send_result.get('success', False)
error = send_result.get('error', '') error = send_result.get('error', '')
@@ -1168,7 +1174,7 @@ class NotificationManager:
results[ch_name] = {'success': success, 'error': error} results[ch_name] = {'success': success, 'error': error}
self._record_history( self._record_history(
'test', ch_name, base_title, 'test', ch_name, enhanced_title,
enhanced_message[:500], 'INFO', enhanced_message[:500], 'INFO',
success, error, 'api' success, error, 'api'
) )

View File

@@ -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}. Your task is to translate and format system alerts to {language}.
STRICT RULES: STRICT RULES:
1. Translate the message to the requested language 1. Translate BOTH the title and message body to {language}
2. Maintain an INFORMATIVE and OBJECTIVE tone 2. DO NOT use markdown formatting like **bold** or *italic*
3. DO NOT use formal introductions ("Dear...", "Esteemed...") 3. Use plain text only - no special formatting syntax
4. DO NOT give recommendations or action suggestions 4. Maintain an INFORMATIVE and OBJECTIVE tone
5. DO NOT interpret data subjectively 5. DO NOT use formal introductions ("Dear...", "Esteemed...")
6. Present only FACTS and TECHNICAL DATA 6. DO NOT give recommendations or action suggestions
7. Respect the requested detail level: {detail_level} 7. Present only FACTS and TECHNICAL DATA
8. Respect the requested detail level: {detail_level}
{emoji_instructions} {emoji_instructions}
DETAIL LEVELS: 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) - 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) - 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 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 - 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.""" If journal log context is provided, use it for more precise event information."""
# Emoji instructions for rich format channels # Emoji instructions for rich format channels
AI_EMOJI_INSTRUCTIONS = """ AI_EMOJI_INSTRUCTIONS = """
8. ENRICH with contextual emojis and icons: 10. ENRICH with contextual emojis:
- Use appropriate emojis at the START of the title/message to indicate severity and type - Start with a severity indicator circle: (blue=info), (yellow=warning), (red=critical)
- Severity indicators: Use a colored circle at the start (info=blue, warning=yellow, critical=red) - Add specific emojis for each data item, not just at the start
- Add relevant technical emojis: disk, server, network, security, backup, etc. - Use emojis that match the content type precisely:
- Keep emojis contextual and professional, not decorative
- Examples of appropriate emojis: UPDATES/PACKAGES:
* Disk/Storage: disk, folder, file - Total updates count
* Network: globe, signal, connection - Security updates
* Security: shield, lock, key, warning - Proxmox updates
* System: gear, server, computer - Kernel updates
* Status: checkmark, cross, warning, info - Package list items (bullet points)
* Backup: save, sync, cloud
* Performance: chart, speedometer""" 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 # No emoji instructions for email/plain channels
AI_NO_EMOJI_INSTRUCTIONS = """ 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: class AIEnhancer:
@@ -1343,7 +1385,7 @@ class AIEnhancer:
def enhance(self, title: str, body: str, severity: str, def enhance(self, title: str, body: str, severity: str,
detail_level: str = 'standard', detail_level: str = 'standard',
journal_context: str = '', journal_context: str = '',
use_emojis: bool = False) -> Optional[str]: use_emojis: bool = False) -> Optional[Dict[str, str]]:
"""Enhance/translate notification with AI. """Enhance/translate notification with AI.
Args: Args:
@@ -1355,7 +1397,7 @@ class AIEnhancer:
use_emojis: Whether to include emojis in the response (for push channels) use_emojis: Whether to include emojis in the response (for push channels)
Returns: Returns:
Enhanced/translated text or None if failed Dict with 'title' and 'body' keys, or None if failed
""" """
if not self._provider: if not self._provider:
return None return None
@@ -1384,11 +1426,49 @@ class AIEnhancer:
try: try:
result = self._provider.generate(system_prompt, user_msg, max_tokens) result = self._provider.generate(system_prompt, user_msg, max_tokens)
return result return self._parse_ai_response(result, title, body)
except Exception as e: except Exception as e:
print(f"[AIEnhancer] Enhancement failed: {e}") print(f"[AIEnhancer] Enhancement failed: {e}")
return None 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]: def test_connection(self) -> Dict[str, Any]:
"""Test the AI provider connection. """Test the AI provider connection.
@@ -1426,22 +1506,47 @@ def format_with_ai(title: str, body: str, severity: str,
Returns: Returns:
Enhanced body string or original if AI fails 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 # Check if AI is enabled
ai_enabled = ai_config.get('ai_enabled') ai_enabled = ai_config.get('ai_enabled')
if isinstance(ai_enabled, str): if isinstance(ai_enabled, str):
ai_enabled = ai_enabled.lower() == 'true' ai_enabled = ai_enabled.lower() == 'true'
if not ai_enabled: if not ai_enabled:
return body return default_result
# Check for API key (not required for Ollama) # Check for API key (not required for Ollama)
provider = ai_config.get('ai_provider', 'groq') provider = ai_config.get('ai_provider', 'groq')
if provider != 'ollama' and not ai_config.get('ai_api_key'): if provider != 'ollama' and not ai_config.get('ai_api_key'):
return body return default_result
# For Ollama, check URL is configured # For Ollama, check URL is configured
if provider == 'ollama' and not ai_config.get('ai_ollama_url'): if provider == 'ollama' and not ai_config.get('ai_ollama_url'):
return body return default_result
# Create enhancer and process # Create enhancer and process
enhancer = AIEnhancer(ai_config) enhancer = AIEnhancer(ai_config)
@@ -1452,15 +1557,19 @@ def format_with_ai(title: str, body: str, severity: str,
use_emojis=use_emojis use_emojis=use_emojis
) )
# Return enhanced text if successful, otherwise original # Return enhanced result if successful, otherwise original
if enhanced: 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 # For detailed level (email), append original message for reference
# This ensures full technical data is available even after AI processing # This ensures full technical data is available even after AI processing
if detail_level == 'detailed' and body and len(body) > 50: if detail_level == 'detailed' and body and len(body) > 50:
# Only append if original has substantial content # Only append if original has substantial content
enhanced += "\n\n" + "-" * 40 + "\n" result_body += "\n\n" + "-" * 40 + "\n"
enhanced += "Original message:\n" result_body += "Original message:\n"
enhanced += body result_body += body
return enhanced
return body return {'title': result_title, 'body': result_body}
return default_result