mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +00:00
fix(notifications): preserve VM and CT identities in AI diagnoses
This commit is contained in:
@@ -2308,6 +2308,12 @@ def render_template(event_type: str, data: Dict[str, Any],
|
||||
'diagnostics': '\n'.join(dict.fromkeys(line for line in analysis_lines if line.strip())),
|
||||
'failed_ids': [str(guest['vmid']) for guest in (parsed_backup or {}).get('vms', [])
|
||||
if str(guest.get('status', '')).lower() == 'error'],
|
||||
'guest_types': {
|
||||
str(guest['vmid']): 'VM' if guest['type'] == 'qemu' else 'CT'
|
||||
for guest in (parsed_backup or {}).get('vms', [])
|
||||
if str(guest.get('status', '')).lower() == 'error'
|
||||
and guest.get('type') in ('qemu', 'lxc')
|
||||
},
|
||||
}
|
||||
|
||||
if backup_diagnostics:
|
||||
@@ -3390,6 +3396,8 @@ and one brief, actionable check when supported by the evidence. {emoji_rule}
|
||||
Do not reproduce shell commands, credentials, timestamps, raw logs, or a backup inventory.
|
||||
Never invent successes, failed guest IDs, causes, available space or quota amounts.
|
||||
Keep the scope of each error: a cause recorded for one guest is not proof for other guests.
|
||||
Use the authoritative Guest identities from the context, with VM/CT labels exactly
|
||||
as given. Proxmox's generic "Backup of VM" log wording can also refer to a CT.
|
||||
"Disk quota exceeded" means a quota prevented a write, not proof that the Proxmox
|
||||
host disk is full. If the error explicitly concerns a PBS connection/write, explain
|
||||
the destination quota and suggest checking that account/datastore quota.
|
||||
@@ -3464,6 +3472,11 @@ def format_backup_diagnostics_with_ai(title: str, body: str, severity: str,
|
||||
context = '\n'.join(lines)
|
||||
if len(context) > 12000:
|
||||
context = context[:5999] + '\n…\n' + context[-5998:]
|
||||
guest_types = {str(vmid): kind for vmid, kind in analysis.get('guest_types', {}).items()
|
||||
if kind in ('VM', 'CT') and re.fullmatch(r'\d+', str(vmid))}
|
||||
if guest_types:
|
||||
context = 'Guest identities: ' + ', '.join(
|
||||
f'{guest_types[vmid]} {vmid}' for vmid in sorted(guest_types, key=int)) + '\n' + context
|
||||
language = AI_LANGUAGES.get(ai_config.get('ai_language', 'en'), 'English')
|
||||
config = {
|
||||
**ai_config, 'channel_type': '', 'ai_prompt_mode': 'custom',
|
||||
@@ -3477,7 +3490,19 @@ def format_backup_diagnostics_with_ai(title: str, body: str, severity: str,
|
||||
except Exception:
|
||||
result = {}
|
||||
explanation = str(result.get('body') or '').strip()
|
||||
if not explanation or explanation == context or len(explanation) > 2000:
|
||||
invalid = not explanation or explanation == context or len(explanation) > 2000
|
||||
if not invalid:
|
||||
references = re.compile(r'\b(?:VM|CT)\s*#?\s*(\d+)\b', re.IGNORECASE)
|
||||
failed_ids = {str(vmid) for vmid in analysis.get('failed_ids', [])}
|
||||
if failed_ids and set(references.findall(explanation)) - failed_ids:
|
||||
invalid = True
|
||||
else:
|
||||
# A fluent diagnosis must not turn a VM into a CT (or vice versa).
|
||||
# Unknown types stay neutral rather than inheriting the model's guess.
|
||||
explanation = references.sub(
|
||||
lambda match: f"{guest_types.get(match.group(1), 'ID')} {match.group(1)}",
|
||||
explanation)
|
||||
if invalid:
|
||||
explanation = _backup_quota_explanation(
|
||||
diagnostics, analysis, data, ai_config.get('ai_language', 'en'), use_emojis)
|
||||
if not explanation:
|
||||
|
||||
@@ -41,6 +41,9 @@ def report(count=11):
|
||||
'null' if failed else f'ct/{vmid}/2026-11-01T06:00:00Z'))
|
||||
return ('Proxmox vzdump report\n' + '\n'.join(rows)
|
||||
+ '\nTotal running time: 00:14:47\nTotal size: 110.487 GiB\n'
|
||||
+ '108: 2026-11-01 07:10:47 INFO: Starting Backup of VM 108 (lxc)\n'
|
||||
+ '110: 2026-11-01 07:14:47 INFO: Starting Backup of VM 110 (qemu)\n'
|
||||
+ '9000: 2026-11-01 07:14:48 INFO: Starting Backup of VM 9000 (lxc)\n'
|
||||
+ "108: 2026-11-01 07:14:47 ERROR: Backup of VM 108 failed - command '/usr/bin/proxmox-backup-client backup root.pxar:/mnt/vzsnap0' failed: exit code 255\n"
|
||||
+ "110: 2026-11-01 07:14:48 ERROR: VM 110 qmp command 'backup' failed - backup connect failed: command error: Disk quota exceeded (os error 122)\n"
|
||||
+ "9000: 2026-11-01 07:14:53 ERROR: Backup of VM 9000 failed - command 'lxc-usernsexec -- proxmox-backup-client backup' failed: exit code 255\n")
|
||||
@@ -183,6 +186,33 @@ class BackupAIDiagnosticsTests(unittest.TestCase):
|
||||
{'title': rendered['title'], 'body': rendered['body']})
|
||||
self.assertEqual(self.prompts, [])
|
||||
|
||||
def test_ai_guest_type_is_corrected_using_authoritative_inventory(self):
|
||||
data, rendered = render()
|
||||
self.assertEqual(data['_backup_analysis']['guest_types'],
|
||||
{'108': 'CT', '110': 'VM', '9000': 'CT'})
|
||||
with patch.object(self.provider, 'generate', return_value=(
|
||||
'[TITLE]\nIgnored\n[BODY]\nCT 110: cuota excedida. VM 108: falta causa.')) as generate:
|
||||
result = self.explain(data, rendered)
|
||||
explanation = result['body'][len(data['_backup_analysis']['inventory_body']):]
|
||||
self.assertIn('VM 110: cuota excedida', explanation)
|
||||
self.assertIn('CT 108: falta causa', explanation)
|
||||
self.assertIn('Guest identities: CT 108, VM 110, CT 9000', generate.call_args.args[1])
|
||||
|
||||
def test_ai_cannot_introduce_a_failed_guest_outside_the_report(self):
|
||||
data, rendered = render()
|
||||
with patch.object(self.provider, 'generate', return_value=(
|
||||
'[TITLE]\nIgnored\n[BODY]\nVM 999: backup failed.')):
|
||||
result = self.explain(data, rendered)
|
||||
self.assertNotIn('999', result['body'])
|
||||
self.assertIn('Copias 110:', result['body'])
|
||||
|
||||
def test_unknown_guest_type_is_neutral_in_ai_explanation(self):
|
||||
data, rendered = render()
|
||||
data['_backup_analysis']['guest_types'].pop('110')
|
||||
result = self.explain(data, rendered)
|
||||
explanation = result['body'][len(data['_backup_analysis']['inventory_body']):]
|
||||
self.assertIn('ID 110:', explanation)
|
||||
|
||||
def test_missing_credentials_use_evidenced_quota_fallback(self):
|
||||
data, rendered = render()
|
||||
result = self.explain(data, rendered, ai_provider='openai', ai_api_key='')
|
||||
|
||||
Reference in New Issue
Block a user