mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-05 20:03:48 +00:00
Update notification service
This commit is contained in:
@@ -9,13 +9,13 @@ import { Label } from "./ui/label"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Button } from "./ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog"
|
||||
import { fetchApi } from "../lib/api-config"
|
||||
import {
|
||||
Bell, BellOff, Send, CheckCircle2, XCircle, Loader2,
|
||||
AlertTriangle, Info, Settings2, Zap, Eye, EyeOff,
|
||||
Trash2, ChevronDown, ChevronUp, ChevronRight, TestTube2, Mail, Webhook,
|
||||
Copy, Server, Shield, ExternalLink, RefreshCw
|
||||
Copy, Server, Shield, ExternalLink, RefreshCw, Download, Upload
|
||||
} from "lucide-react"
|
||||
|
||||
interface ChannelConfig {
|
||||
@@ -63,6 +63,8 @@ interface NotificationConfig {
|
||||
ai_language: string
|
||||
ai_ollama_url: string
|
||||
ai_openai_base_url: string
|
||||
ai_prompt_mode: string // 'default' or 'custom'
|
||||
ai_custom_prompt: string // User's custom prompt
|
||||
channel_ai_detail: Record<string, string>
|
||||
hostname: string
|
||||
webhook_secret: string
|
||||
@@ -180,6 +182,23 @@ const AI_DETAIL_LEVELS = [
|
||||
{ value: "detailed", label: "Detailed", desc: "Complete technical details" },
|
||||
]
|
||||
|
||||
// Example custom prompt for users to adapt
|
||||
const EXAMPLE_CUSTOM_PROMPT = `You are a notification formatter for ProxMenux Monitor.
|
||||
|
||||
Your task is to translate and format server notifications.
|
||||
|
||||
RULES:
|
||||
1. Translate to the user's language
|
||||
2. Use plain text only (no markdown)
|
||||
3. Be concise and factual
|
||||
4. Do not add recommendations
|
||||
|
||||
OUTPUT FORMAT:
|
||||
[TITLE]
|
||||
your title here
|
||||
[BODY]
|
||||
your message here`
|
||||
|
||||
const DEFAULT_CONFIG: NotificationConfig = {
|
||||
enabled: false,
|
||||
channels: {
|
||||
@@ -222,6 +241,8 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
||||
ai_language: "en",
|
||||
ai_ollama_url: "http://localhost:11434",
|
||||
ai_openai_base_url: "",
|
||||
ai_prompt_mode: "default",
|
||||
ai_custom_prompt: "",
|
||||
channel_ai_detail: {
|
||||
telegram: "brief",
|
||||
gotify: "brief",
|
||||
@@ -259,6 +280,7 @@ export function NotificationSettings() {
|
||||
const [aiTestResult, setAiTestResult] = useState<{ success: boolean; message: string; model?: string } | null>(null)
|
||||
const [providerModels, setProviderModels] = useState<string[]>([])
|
||||
const [loadingProviderModels, setLoadingProviderModels] = useState(false)
|
||||
const [showCustomPromptInfo, setShowCustomPromptInfo] = useState(false)
|
||||
const [webhookSetup, setWebhookSetup] = useState<{
|
||||
status: "idle" | "running" | "success" | "failed"
|
||||
fallback_commands: string[]
|
||||
@@ -269,7 +291,7 @@ export function NotificationSettings() {
|
||||
try {
|
||||
const data = await fetchApi<{ success: boolean; config: NotificationConfig }>("/api/notifications/settings")
|
||||
if (data.success && data.config) {
|
||||
// Ensure ai_api_keys and ai_models objects exist (fallback for older configs)
|
||||
// Ensure ai_api_keys, ai_models, and prompt settings exist (fallback for older configs)
|
||||
const configWithDefaults = {
|
||||
...data.config,
|
||||
ai_api_keys: data.config.ai_api_keys || {
|
||||
@@ -287,7 +309,9 @@ export function NotificationSettings() {
|
||||
anthropic: "",
|
||||
openai: "",
|
||||
openrouter: "",
|
||||
}
|
||||
},
|
||||
ai_prompt_mode: data.config.ai_prompt_mode || "default",
|
||||
ai_custom_prompt: data.config.ai_custom_prompt || "",
|
||||
}
|
||||
// If ai_model exists but ai_models doesn't have it, save it
|
||||
if (configWithDefaults.ai_model && !configWithDefaults.ai_models[configWithDefaults.ai_provider]) {
|
||||
@@ -1669,80 +1693,173 @@ export function NotificationSettings() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Test Connection button */}
|
||||
<button
|
||||
onClick={handleTestAI}
|
||||
disabled={
|
||||
!editMode ||
|
||||
testingAI ||
|
||||
!config.ai_model ||
|
||||
(config.ai_provider !== "ollama" && !config.ai_api_keys?.[config.ai_provider])
|
||||
}
|
||||
className="w-full h-9 flex items-center justify-center gap-2 rounded-md text-sm font-medium bg-purple-600 hover:bg-purple-700 text-white disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{testingAI ? (
|
||||
<><Loader2 className="h-4 w-4 animate-spin" /> Testing...</>
|
||||
) : (
|
||||
<><Zap className="h-4 w-4" /> Test Connection</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Test result */}
|
||||
{aiTestResult && (
|
||||
<div className={`flex items-start gap-2 p-3 rounded-md ${
|
||||
aiTestResult.success
|
||||
? "bg-green-500/10 border border-green-500/20"
|
||||
: "bg-red-500/10 border border-red-500/20"
|
||||
}`}>
|
||||
{aiTestResult.success
|
||||
? <CheckCircle2 className="h-4 w-4 text-green-400 shrink-0 mt-0.5" />
|
||||
: <XCircle className="h-4 w-4 text-red-400 shrink-0 mt-0.5" />
|
||||
}
|
||||
<p className={`text-xs sm:text-sm leading-relaxed ${
|
||||
aiTestResult.success ? "text-green-400/90" : "text-red-400/90"
|
||||
}`}>
|
||||
{aiTestResult.message}
|
||||
{aiTestResult.model && ` (${aiTestResult.model})`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Per-channel detail level */}
|
||||
{/* Prompt Mode section */}
|
||||
<div className="space-y-3 pt-3 border-t border-border/50">
|
||||
<Label className="text-xs sm:text-sm text-foreground/80">Detail Level per Channel</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{CHANNEL_TYPES.map(ch => (
|
||||
<div key={ch} className="flex items-center justify-between gap-2 px-3 py-2 rounded bg-muted/30">
|
||||
<span className="text-xs sm:text-sm text-foreground/70 capitalize">{ch}</span>
|
||||
<Select
|
||||
value={config.channel_ai_detail?.[ch] || "standard"}
|
||||
onValueChange={v => updateConfig(p => ({
|
||||
...p,
|
||||
channel_ai_detail: { ...p.channel_ai_detail, [ch]: v }
|
||||
}))}
|
||||
disabled={!editMode}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-[90px] text-xs px-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AI_DETAIL_LEVELS.map(l => (
|
||||
<SelectItem key={l.value} value={l.value} className="text-xs">
|
||||
{l.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-foreground/80">Prompt Mode</Label>
|
||||
<Select
|
||||
value={config.ai_prompt_mode || "default"}
|
||||
onValueChange={v => {
|
||||
updateConfig(p => ({ ...p, ai_prompt_mode: v }))
|
||||
// Show info modal when switching to custom for the first time
|
||||
if (v === "custom" && !config.ai_custom_prompt) {
|
||||
setShowCustomPromptInfo(true)
|
||||
}
|
||||
}}
|
||||
disabled={!editMode}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default Prompt</SelectItem>
|
||||
<SelectItem value="custom">Custom Prompt</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Default mode: Detail Level per Channel */}
|
||||
{(config.ai_prompt_mode || "default") === "default" && (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-xs sm:text-sm text-foreground/80">Detail Level per Channel</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{CHANNEL_TYPES.map(ch => (
|
||||
<div key={ch} className="flex items-center justify-between gap-2 px-3 py-2 rounded bg-muted/30">
|
||||
<span className="text-xs sm:text-sm text-foreground/70 capitalize">{ch}</span>
|
||||
<Select
|
||||
value={config.channel_ai_detail?.[ch] || "standard"}
|
||||
onValueChange={v => updateConfig(p => ({
|
||||
...p,
|
||||
channel_ai_detail: { ...p.channel_ai_detail, [ch]: v }
|
||||
}))}
|
||||
disabled={!editMode}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-[90px] text-xs px-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AI_DETAIL_LEVELS.map(l => (
|
||||
<SelectItem key={l.value} value={l.value} className="text-xs">
|
||||
{l.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-start gap-2 p-3 rounded-md bg-purple-500/10 border border-purple-500/20">
|
||||
<Info className="h-4 w-4 text-purple-400 shrink-0 mt-0.5" />
|
||||
<p className="text-xs sm:text-sm text-purple-400/90 leading-relaxed">
|
||||
AI translates and formats notifications to your selected language. Each channel can have different detail levels.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom mode: Editable prompt textarea */}
|
||||
{config.ai_prompt_mode === "custom" && (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-foreground/80">Custom Prompt</Label>
|
||||
<textarea
|
||||
value={config.ai_custom_prompt || ""}
|
||||
onChange={e => updateConfig(p => ({ ...p, ai_custom_prompt: e.target.value }))}
|
||||
disabled={!editMode}
|
||||
placeholder="Enter your custom prompt instructions for the AI..."
|
||||
className="w-full h-48 px-3 py-2 text-sm rounded-md border border-border bg-background resize-y focus:outline-none focus:ring-2 focus:ring-purple-500/50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!editMode}
|
||||
onClick={() => {
|
||||
const blob = new Blob([config.ai_custom_prompt || ""], { type: "text/plain" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "proxmenux_custom_prompt.txt"
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!editMode}
|
||||
onClick={() => {
|
||||
const input = document.createElement("input")
|
||||
input.type = "file"
|
||||
input.accept = ".txt,.md"
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (file) {
|
||||
const text = await file.text()
|
||||
updateConfig(p => ({ ...p, ai_custom_prompt: text }))
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 p-3 rounded-md bg-purple-500/10 border border-purple-500/20">
|
||||
<Info className="h-4 w-4 text-purple-400 shrink-0 mt-0.5" />
|
||||
<p className="text-xs sm:text-sm text-purple-400/90 leading-relaxed">
|
||||
Define your own prompt rules and format. You control the detail level and style of all notifications. Export to share with others or import prompts from the community.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 p-3 rounded-md bg-purple-500/10 border border-purple-500/20">
|
||||
<Info className="h-4 w-4 text-purple-400 shrink-0 mt-0.5" />
|
||||
<p className="text-xs sm:text-sm text-purple-400/90 leading-relaxed">
|
||||
AI enhancement translates and formats notifications to your selected language. Each channel can have different detail levels. If the AI service is unavailable, standard templates are used as fallback.
|
||||
</p>
|
||||
{/* Test Connection button - moved to end */}
|
||||
<div className="space-y-3 pt-3 border-t border-border/50">
|
||||
<button
|
||||
onClick={handleTestAI}
|
||||
disabled={
|
||||
!editMode ||
|
||||
testingAI ||
|
||||
!config.ai_model ||
|
||||
(config.ai_provider !== "ollama" && !config.ai_api_keys?.[config.ai_provider])
|
||||
}
|
||||
className="w-full h-9 flex items-center justify-center gap-2 rounded-md text-sm font-medium bg-purple-600 hover:bg-purple-700 text-white disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{testingAI ? (
|
||||
<><Loader2 className="h-4 w-4 animate-spin" /> Testing...</>
|
||||
) : (
|
||||
<><Zap className="h-4 w-4" /> Test Connection</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Test result */}
|
||||
{aiTestResult && (
|
||||
<div className={`flex items-start gap-2 p-3 rounded-md ${
|
||||
aiTestResult.success
|
||||
? "bg-green-500/10 border border-green-500/20"
|
||||
: "bg-red-500/10 border border-red-500/20"
|
||||
}`}>
|
||||
{aiTestResult.success
|
||||
? <CheckCircle2 className="h-4 w-4 text-green-400 shrink-0 mt-0.5" />
|
||||
: <XCircle className="h-4 w-4 text-red-400 shrink-0 mt-0.5" />
|
||||
}
|
||||
<p className={`text-xs sm:text-sm leading-relaxed ${
|
||||
aiTestResult.success ? "text-green-400/90" : "text-red-400/90"
|
||||
}`}>
|
||||
{aiTestResult.message}
|
||||
{aiTestResult.model && ` (${aiTestResult.model})`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -1903,6 +2020,77 @@ export function NotificationSettings() {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Custom Prompt Info Modal */}
|
||||
<Dialog open={showCustomPromptInfo} onOpenChange={setShowCustomPromptInfo}>
|
||||
<DialogContent className="max-w-[90vw] sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
<Settings2 className="h-5 w-5 text-purple-400" />
|
||||
Custom Prompt Mode
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Create your own AI prompt for ProxMenux Monitor notifications
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-foreground/90">What is a custom prompt?</h4>
|
||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||
The prompt defines how the AI formats your notifications. With a custom prompt, you control the style, detail level, and format of all messages.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-foreground/90">Important requirements</h4>
|
||||
<ul className="text-muted-foreground text-xs space-y-1.5">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-purple-400 mt-0.5">1.</span>
|
||||
<span>Your prompt must output in this format:<br/>
|
||||
<code className="bg-muted px-1.5 py-0.5 rounded text-[11px]">[TITLE]</code> followed by the title, then <code className="bg-muted px-1.5 py-0.5 rounded text-[11px]">[BODY]</code> followed by the message
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-purple-400 mt-0.5">2.</span>
|
||||
<span>Use plain text only (no markdown) for compatibility with all channels</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-purple-400 mt-0.5">3.</span>
|
||||
<span>The prompt receives raw Proxmox event data as input</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-foreground/90">Getting started</h4>
|
||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||
We have added an example prompt to get you started. You can adapt it, export it to share with others, or import prompts from the community.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
updateConfig(p => ({ ...p, ai_custom_prompt: EXAMPLE_CUSTOM_PROMPT }))
|
||||
setShowCustomPromptInfo(false)
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
Load Example
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setShowCustomPromptInfo(false)}
|
||||
className="flex-1 bg-purple-600 hover:bg-purple-700 text-white"
|
||||
>
|
||||
Got it
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
38
AppImage/config/default_prompt.txt
Normal file
38
AppImage/config/default_prompt.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
You are a system notification formatter for ProxMenux Monitor, a Proxmox VE monitoring tool.
|
||||
|
||||
Your task is to translate and reformat incoming server alert messages into {language}.
|
||||
|
||||
═══ ABSOLUTE RULES ═══
|
||||
1. Translate BOTH title and body to {language}. Every word, label, and unit must be in {language}.
|
||||
2. NO markdown: no **bold**, no *italic*, no `code`, no headers (#), no bullet lists (- or *)
|
||||
3. Plain text only — the output is sent to chat apps and email which handle their own formatting
|
||||
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. OUTPUT ONLY THE FINAL RESULT — never include both original and processed versions.
|
||||
Do NOT append "Original message:", "Original:", "Source:", or any before/after comparison.
|
||||
Return ONLY the single, final formatted message in {language}.
|
||||
8. 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.
|
||||
9. 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
|
||||
10. Keep the "hostname: " prefix in the title. Translate only the descriptive part.
|
||||
Example: "pve01: Updates available" → "pve01: Actualizaciones disponibles"
|
||||
11. EMPTY LIST VALUES — if a list field is empty, "none", or "0":
|
||||
Always write the translated word for "none" on the line after the label, never leave it blank.
|
||||
12. DEDUPLICATION — input may contain redundant or repeated information from multiple monitoring sources:
|
||||
- Identify and merge duplicate facts (same device, same error, same metric mentioned twice)
|
||||
- Present each unique fact exactly once in a clear, consolidated form
|
||||
- If the same data appears in different formats, choose the most informative version
|
||||
|
||||
{emoji_instructions}
|
||||
|
||||
═══ OUTPUT FORMAT ═══
|
||||
TITLE: <translated title>
|
||||
BODY:
|
||||
<translated body>
|
||||
|
||||
IMPORTANT:
|
||||
- Do NOT include the literal words TITLE or BODY anywhere in the translated content
|
||||
@@ -110,12 +110,13 @@ else
|
||||
echo "⚠️ ai_providers directory not found"
|
||||
fi
|
||||
|
||||
# Copy config files (verified AI models, etc.)
|
||||
# Copy config files (verified AI models, prompts, etc.)
|
||||
echo "📋 Copying config files..."
|
||||
CONFIG_DIR="$APPIMAGE_ROOT/config"
|
||||
if [ -d "$CONFIG_DIR" ]; then
|
||||
mkdir -p "$APP_DIR/usr/bin/config"
|
||||
cp "$CONFIG_DIR/"*.json "$APP_DIR/usr/bin/config/" 2>/dev/null || true
|
||||
cp "$CONFIG_DIR/"*.txt "$APP_DIR/usr/bin/config/" 2>/dev/null || true
|
||||
echo "✅ Config files copied"
|
||||
else
|
||||
echo "⚠️ config directory not found"
|
||||
|
||||
@@ -739,6 +739,8 @@ class NotificationManager:
|
||||
'ai_model': self._config.get('ai_model', ''),
|
||||
'ai_language': self._config.get('ai_language', 'en'),
|
||||
'ai_ollama_url': self._config.get('ai_ollama_url', ''),
|
||||
'ai_prompt_mode': self._config.get('ai_prompt_mode', 'default'),
|
||||
'ai_custom_prompt': self._config.get('ai_custom_prompt', ''),
|
||||
}
|
||||
|
||||
# Get journal context if available
|
||||
@@ -1070,6 +1072,8 @@ class NotificationManager:
|
||||
'ai_model': self._config.get('ai_model', ''),
|
||||
'ai_language': self._config.get('ai_language', 'en'),
|
||||
'ai_ollama_url': self._config.get('ai_ollama_url', ''),
|
||||
'ai_prompt_mode': self._config.get('ai_prompt_mode', 'default'),
|
||||
'ai_custom_prompt': self._config.get('ai_custom_prompt', ''),
|
||||
}
|
||||
|
||||
results = {}
|
||||
@@ -1166,6 +1170,8 @@ class NotificationManager:
|
||||
'ai_model': self._config.get('ai_model', ''),
|
||||
'ai_language': self._config.get('ai_language', 'en'),
|
||||
'ai_ollama_url': self._config.get('ai_ollama_url', ''),
|
||||
'ai_prompt_mode': self._config.get('ai_prompt_mode', 'default'),
|
||||
'ai_custom_prompt': self._config.get('ai_custom_prompt', ''),
|
||||
}
|
||||
|
||||
ai_enabled = self._config.get('ai_enabled', 'false')
|
||||
@@ -1559,6 +1565,8 @@ class NotificationManager:
|
||||
'ai_language': self._config.get('ai_language', 'en'),
|
||||
'ai_ollama_url': self._config.get('ai_ollama_url', 'http://localhost:11434'),
|
||||
'ai_openai_base_url': self._config.get('ai_openai_base_url', ''),
|
||||
'ai_prompt_mode': self._config.get('ai_prompt_mode', 'default'),
|
||||
'ai_custom_prompt': self._config.get('ai_custom_prompt', ''),
|
||||
'ai_detail_levels': ai_detail_levels,
|
||||
'hostname': self._config.get('hostname', ''),
|
||||
'webhook_secret': self._config.get('webhook_secret', ''),
|
||||
|
||||
@@ -1682,18 +1682,23 @@ class AIEnhancer:
|
||||
language_code = self.config.get('ai_language', 'en')
|
||||
language_name = AI_LANGUAGES.get(language_code, 'English')
|
||||
|
||||
# Get token limit for detail level
|
||||
max_tokens = AI_DETAIL_TOKENS.get(detail_level, 200)
|
||||
# Check for custom prompt mode
|
||||
prompt_mode = self.config.get('ai_prompt_mode', 'default')
|
||||
custom_prompt = self.config.get('ai_custom_prompt', '')
|
||||
|
||||
# Select emoji instructions based on channel type
|
||||
emoji_instructions = AI_EMOJI_INSTRUCTIONS if use_emojis else AI_NO_EMOJI_INSTRUCTIONS
|
||||
|
||||
# Build system prompt with emoji instructions
|
||||
system_prompt = AI_SYSTEM_PROMPT.format(
|
||||
language=language_name,
|
||||
detail_level=detail_level,
|
||||
emoji_instructions=emoji_instructions
|
||||
)
|
||||
if prompt_mode == 'custom' and custom_prompt.strip():
|
||||
# Custom prompt: user controls everything, use higher token limit
|
||||
system_prompt = custom_prompt
|
||||
max_tokens = 500 # Allow more tokens for custom prompts
|
||||
else:
|
||||
# Default prompt: use detail level and emoji settings
|
||||
max_tokens = AI_DETAIL_TOKENS.get(detail_level, 200)
|
||||
emoji_instructions = AI_EMOJI_INSTRUCTIONS if use_emojis else AI_NO_EMOJI_INSTRUCTIONS
|
||||
system_prompt = AI_SYSTEM_PROMPT.format(
|
||||
language=language_name,
|
||||
detail_level=detail_level,
|
||||
emoji_instructions=emoji_instructions
|
||||
)
|
||||
|
||||
# Build user message
|
||||
user_msg = f"Severity: {severity}\nTitle: {title}\nMessage:\n{body}"
|
||||
|
||||
Reference in New Issue
Block a user