mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-01 03:46:22 +00:00
Update notification service
This commit is contained in:
@@ -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'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user