From c13c7ba626d19b110578beb29629031b40633110 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Wed, 18 Mar 2026 09:36:05 +0100 Subject: [PATCH] Update notification service --- AppImage/components/notification-settings.tsx | 37 +++++++++ .../scripts/ai_providers/openai_provider.py | 30 ++++++- AppImage/scripts/notification_manager.py | 4 +- AppImage/scripts/notification_templates.py | 78 +++++++++++++++---- 4 files changed, 127 insertions(+), 22 deletions(-) diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index 7763e084..36b3f41f 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -61,6 +61,7 @@ interface NotificationConfig { ai_model: string ai_language: string ai_ollama_url: string + ai_openai_base_url: string channel_ai_detail: Record hostname: string webhook_secret: string @@ -211,6 +212,7 @@ const DEFAULT_CONFIG: NotificationConfig = { ai_model: "", ai_language: "en", ai_ollama_url: "http://localhost:11434", + ai_openai_base_url: "", channel_ai_detail: { telegram: "brief", gotify: "brief", @@ -1390,6 +1392,26 @@ export function NotificationSettings() { )} + {/* Custom Base URL for OpenAI-compatible APIs */} + {config.ai_provider === "openai" && ( +
+
+ + (optional) +
+ updateConfig(p => ({ ...p, ai_openai_base_url: e.target.value }))} + disabled={!editMode} + /> +

+ For OpenAI-compatible APIs: BytePlus, LocalAI, LM Studio, vLLM, etc. +

+
+ )} + {/* API Key (not shown for Ollama) */} {config.ai_provider !== "ollama" && (
@@ -1584,6 +1606,21 @@ export function NotificationSettings() {

{provider.description}

+ {/* OpenAI compatibility note */} + {provider.value === "openai" && ( +
+

OpenAI-Compatible APIs

+

+ You can use any OpenAI-compatible API by setting a custom Base URL. Compatible services include: +

+
    +
  • BytePlus/ByteDance (Kimi K2.5)
  • +
  • LocalAI, LM Studio, vLLM
  • +
  • Together AI, Fireworks AI
  • +
  • Any service using OpenAI format
  • +
+
+ )}
))} diff --git a/AppImage/scripts/ai_providers/openai_provider.py b/AppImage/scripts/ai_providers/openai_provider.py index 440e14e9..5211a467 100644 --- a/AppImage/scripts/ai_providers/openai_provider.py +++ b/AppImage/scripts/ai_providers/openai_provider.py @@ -8,16 +8,37 @@ from .base import AIProvider, AIProviderError class OpenAIProvider(AIProvider): - """OpenAI provider using their Chat Completions API.""" + """OpenAI provider using their Chat Completions API. + + Also compatible with OpenAI-compatible APIs like: + - BytePlus/ByteDance (Kimi K2.5) + - LocalAI + - LM Studio + - vLLM + - Together AI + - Any OpenAI-compatible endpoint + """ NAME = "openai" DEFAULT_MODEL = "gpt-4o-mini" REQUIRES_API_KEY = True - API_URL = "https://api.openai.com/v1/chat/completions" + DEFAULT_API_URL = "https://api.openai.com/v1/chat/completions" + + def _get_api_url(self) -> str: + """Get the API URL, using custom base_url if provided.""" + if self.base_url: + # Ensure the URL ends with the correct path + base = self.base_url.rstrip('/') + if not base.endswith('/chat/completions'): + if not base.endswith('/v1'): + base = f"{base}/v1" + base = f"{base}/chat/completions" + return base + return self.DEFAULT_API_URL def generate(self, system_prompt: str, user_message: str, max_tokens: int = 200) -> Optional[str]: - """Generate a response using OpenAI's API. + """Generate a response using OpenAI's API or compatible endpoint. Args: system_prompt: System instructions @@ -48,7 +69,8 @@ class OpenAIProvider(AIProvider): 'Authorization': f'Bearer {self.api_key}', } - result = self._make_request(self.API_URL, payload, headers) + api_url = self._get_api_url() + result = self._make_request(api_url, payload, headers) try: return result['choices'][0]['message']['content'].strip() diff --git a/AppImage/scripts/notification_manager.py b/AppImage/scripts/notification_manager.py index e199bdb7..ae549e8a 100644 --- a/AppImage/scripts/notification_manager.py +++ b/AppImage/scripts/notification_manager.py @@ -1162,9 +1162,11 @@ class NotificationManager: ai_status = f'AI: enabled ({ai_provider} / {ai_language})' if ai_enabled else 'AI: disabled' # Base test message β€” shows current channel config + # NOTE: narrative lines are intentionally unlabeled so the AI + # does not prepend "Message:" or other spurious field labels. base_title = 'ProxMenux Test' base_message = ( - 'Welcome to ProxMenux Monitor!\n' + 'Welcome to ProxMenux Monitor!\n\n' 'This is a test message to verify your notification channel is working correctly.\n\n' 'Channel configuration:\n' f'{icon_status}\n' diff --git a/AppImage/scripts/notification_templates.py b/AppImage/scripts/notification_templates.py index 9fd8f521..16520a22 100644 --- a/AppImage/scripts/notification_templates.py +++ b/AppImage/scripts/notification_templates.py @@ -1064,9 +1064,9 @@ EVENT_EMOJI = { 'replication_fail': '\u274C', 'replication_complete': '\u2705', # Backups - 'backup_start': '\U0001F4E6', # package - 'backup_complete': '\u2705', - 'backup_fail': '\u274C', + 'backup_start': '\U0001F4BE\U0001F680', # πŸ’ΎπŸš€ floppy + rocket + 'backup_complete': '\U0001F4BE\u2705', # πŸ’Ύβœ… floppy + check + 'backup_fail': '\U0001F4BE\u274C', # πŸ’ΎβŒ floppy + cross 'snapshot_complete': '\U0001F4F8', # camera with flash 'snapshot_fail': '\u274C', # Resources @@ -1240,7 +1240,7 @@ AI_LANGUAGES = { AI_DETAIL_TOKENS = { 'brief': 100, # 2-3 lines, essential only 'standard': 200, # Concise paragraph with context - 'detailed': 400, # Complete technical details + 'detailed': 700, # Complete technical details (raised: multi-VM backups can be long) } # System prompt template - informative, no recommendations @@ -1255,7 +1255,9 @@ Your task is to translate and reformat incoming server alert messages into {lang 4. Tone: factual, concise, technical. No greetings, no closings, no apologies 5. DO NOT add recommendations, action items, or suggestions ("you should…", "consider…") 6. Present ONLY the facts already in the input β€” do not invent or assume information -7. Detail level to apply: {detail_level} +7. PLAIN NARRATIVE LINES β€” if a line in the input is a complete sentence (not a "Label: value" + pair), translate it as-is. Never prepend "Message:", "Note:", or any other label to a sentence. +8. Detail level to apply: {detail_level} - brief β†’ 2-3 lines, essential data only (status + key metric) - standard β†’ short paragraph covering who/what/where and the key value - detailed β†’ full technical breakdown of all available fields @@ -1271,13 +1273,22 @@ Your task is to translate and reformat incoming server alert messages into {lang BACKUP (backup_complete / backup_fail / backup_start): Input contains: VM/CT names, IDs, size, duration, storage location, status per VM - Output body must list each VM on its own line: name, ID, status (ok/error), size, duration - End with a summary line: total VMs, total size, total time + Output body: first line is plain text (no emoji) describing the event briefly. + Then list each VM/CT with its fields. End with a summary line. + PARTIAL FAILURE RULE: if some VMs succeeded and at least one failed, use a combined title + like "Backup partially failed" / "Copia de seguridad parcialmente fallida" β€” never say + "backup failed" when there are also successful VMs in the same job. + NEVER omit the storage/archive line or the summary line β€” always include them even for long jobs. UPDATES (update_summary / pve_update): Input contains: total count, security count, proxmox count, kernel count, package list - Output body must show each count on its own line with its label - List important packages below, one per line + Output body must show each count on its own line with its label. + For the package list: use "β€’ " (bullet + space) before each package name, NOT the πŸ“‹ emoji. + The πŸ“‹ emoji goes only on the "Important packages:" header line. + Example packages block: + πŸ“‹ Important packages: + β€’ pve-manager (9.1.4 -> 9.1.6) + β€’ qemu-server (9.1.3 -> 9.1.4) DISK / SMART ERRORS (disk_io_error / storage_unavailable): Input contains: device name, error type, SMART values or I/O error codes @@ -1403,37 +1414,61 @@ AI_EMOJI_INSTRUCTIONS = """ βš™οΈ Kernel updates: 0 πŸ“‹ Important packages: - πŸ“‹ none + β€’ none EXAMPLE β€” updates message (with important packages): [TITLE] πŸ“¦ amd: Updates available [BODY] - πŸ“¦ Total updates: 55 - πŸ”’ Security updates: 3 - πŸ”„ Proxmox updates: 2 + πŸ“¦ Total updates: 90 + πŸ”’ Security updates: 6 + πŸ”„ Proxmox updates: 14 βš™οΈ Kernel updates: 1 πŸ“‹ Important packages: - πŸ“‹ pve-manager - πŸ“‹ libssl3 + β€’ pve-manager (9.1.4 -> 9.1.6) + β€’ qemu-server (9.1.3 -> 9.1.4) + β€’ pve-container (6.0.18 -> 6.1.2) EXAMPLE β€” backup complete with multiple VMs: [TITLE] - βœ… pve01: Backup complete + πŸ’Ύβœ… pve01: Backup complete [BODY] + Backup job finished on storage local-bak. + 🏷️ VM web01 (ID: 100) βœ… Status: ok πŸ“ Size: 12.3 GiB ⏱️ Duration: 00:04:21 + πŸ—„οΈ Storage: vm/100/2026-03-17T22:00:08Z 🏷️ CT db (ID: 101) βœ… Status: ok πŸ“ Size: 4.1 GiB ⏱️ Duration: 00:01:10 + πŸ—„οΈ Storage: ct/101/2026-03-17T22:04:29Z πŸ“Š Total: 2 backups | 16.4 GiB | ⏱️ 00:05:31 + EXAMPLE β€” backup partially failed (some ok, some failed): + [TITLE] + πŸ’ΎβŒ pve01: Backup partially failed + [BODY] + Backup job finished with errors on storage PBS2. + + 🏷️ VM web01 (ID: 100) + βœ… Status: ok + πŸ“ Size: 12.3 GiB + ⏱️ Duration: 00:04:21 + πŸ—„οΈ Storage: vm/100/2026-03-17T22:00:08Z + + 🏷️ VM broken (ID: 102) + ❌ Status: error + πŸ“ Size: 0 B + ⏱️ Duration: 00:00:37 + + πŸ“Š Total: 2 backups | ❌ 1 failed | 12.3 GiB | ⏱️ 00:04:58 + EXAMPLE β€” disk I/O health warning: [TITLE] πŸ’₯ amd: Health warning β€” Disk I/O errors @@ -1493,11 +1528,20 @@ class AIEnhancer: from ai_providers import get_provider provider_name = self.config.get('ai_provider', 'groq') + + # Determine base_url based on provider + if provider_name == 'ollama': + base_url = self.config.get('ai_ollama_url', '') + elif provider_name == 'openai': + base_url = self.config.get('ai_openai_base_url', '') + else: + base_url = '' + self._provider = get_provider( provider_name, api_key=self.config.get('ai_api_key', ''), model=self.config.get('ai_model', ''), - base_url=self.config.get('ai_ollama_url', ''), + base_url=base_url, ) except Exception as e: print(f"[AIEnhancer] Failed to initialize provider: {e}")