mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-22 03:02:35 +00:00
Suppress empty metric values on state-alert Assistant handoffs
The Pulse Assistant briefing and prompt for a powered-off alert
rendered "Current value 0.0%; threshold 0.0%" because the backend
sends value=0 and threshold=0 for state alerts (which have no
metric semantics). That line is misleading to the operator and
gives the LLM no useful signal.
Adds isMetricAlertType / isStateAlertType helpers to
frontend-modern/src/utils/alerts.ts naming the state-alert set
(powered-off, unreachable, offline, host-offline, connectivity,
docker-container-state, docker-container-health,
docker-host-offline). State alerts represent binary or enumerated
conditions, not metric threshold crossings.
The alert handoff builder routes through that helper:
- Briefing detailLines omit the value/threshold line when the
alert is a state alert.
- Prompt omits the **Current Value:** and **Threshold:** lines.
- Prompt now includes **Message:** so the actual signal is
surfaced (was previously dropped from the prompt).
- Prompt step 2 swaps "Check related metrics" for "Check what
changed recently for this resource (state events, recent
commands, related alerts)" — the right question for a
binary-state alert.
Two new tests cover the state-alert and metric-alert branches.
This commit is contained in:
parent
4dff26f728
commit
68e2100955
3 changed files with 112 additions and 17 deletions
|
|
@ -87,4 +87,51 @@ describe('alertAssistantHandoffModel', () => {
|
|||
|
||||
expect(handoff.context.targetType).toBe('app-container');
|
||||
});
|
||||
|
||||
it('suppresses current-value and threshold for state alerts (powered-off, etc.)', () => {
|
||||
// State alerts are binary/enumerated conditions. The backend sends
|
||||
// value=0 and threshold=0 for those; rendering "current 0.0% / threshold
|
||||
// 0.0%" is meaningless. The briefing and prompt must omit those lines
|
||||
// and use the alert message instead to convey what's wrong.
|
||||
const handoff = buildAlertAssistantHandoff({
|
||||
alert: makeAlert({
|
||||
type: 'powered-off',
|
||||
value: 0,
|
||||
threshold: 0,
|
||||
message: "VM 'docker' is powered off",
|
||||
}),
|
||||
now: new Date('2026-05-07T10:05:00.000Z'),
|
||||
});
|
||||
|
||||
expect(handoff.prompt).not.toContain('**Current Value:**');
|
||||
expect(handoff.prompt).not.toContain('**Threshold:**');
|
||||
expect(handoff.prompt).not.toContain('0.0%');
|
||||
expect(handoff.prompt).toContain('**Alert Type:** powered-off');
|
||||
expect(handoff.prompt).toContain("**Message:** VM 'docker' is powered off");
|
||||
|
||||
const briefing = handoff.context.briefing as { detailLines: string[] };
|
||||
for (const line of briefing.detailLines) {
|
||||
expect(line).not.toContain('Current value');
|
||||
expect(line).not.toContain('threshold');
|
||||
}
|
||||
// Message line is still present so the operator and the LLM both have
|
||||
// the actual signal.
|
||||
expect(briefing.detailLines).toContain("Message: VM 'docker' is powered off");
|
||||
});
|
||||
|
||||
it('keeps current-value and threshold for metric alerts (cpu, memory, etc.)', () => {
|
||||
const handoff = buildAlertAssistantHandoff({
|
||||
alert: makeAlert({
|
||||
type: 'cpu',
|
||||
value: 92.5,
|
||||
threshold: 80,
|
||||
}),
|
||||
now: new Date('2026-05-07T10:05:00.000Z'),
|
||||
});
|
||||
|
||||
expect(handoff.prompt).toContain('**Current Value:** 92.5%');
|
||||
expect(handoff.prompt).toContain('**Threshold:** 80.0%');
|
||||
const briefing = handoff.context.briefing as { detailLines: string[] };
|
||||
expect(briefing.detailLines).toContain('Current value 92.5%; threshold 80.0%');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { Alert } from '@/types/api';
|
|||
import type { AIChatContext } from '@/stores/aiChat';
|
||||
import { getCanonicalAlertId } from '@/features/alerts/identity';
|
||||
import { formatAlertValue } from '@/utils/alertFormatters';
|
||||
import { isMetricAlertType } from '@/utils/alerts';
|
||||
import { resolveAlertTargetType } from '@/utils/alertTargetTypes';
|
||||
|
||||
interface BuildAlertAssistantHandoffInput {
|
||||
|
|
@ -24,8 +25,15 @@ export function buildAlertAssistantHandoff({
|
|||
}: BuildAlertAssistantHandoffInput): AlertAssistantHandoff {
|
||||
const alertIdentifier = getCanonicalAlertId(alert);
|
||||
const durationText = formatAlertDuration(alert.startTime, now);
|
||||
const currentValue = formatAlertValue(alert.value, alert.type);
|
||||
const thresholdValue = formatAlertValue(alert.threshold, alert.type);
|
||||
// State alerts (powered-off, unreachable, container-state, etc.) are
|
||||
// binary or enumerated conditions, not threshold crossings. Backend
|
||||
// sends value=0 and threshold=0 for those; rendering "current 0.0% /
|
||||
// threshold 0.0%" in operator-facing copy is misleading default-zero
|
||||
// noise. Suppress those fields and rely on alert.type + alert.message
|
||||
// to convey what's wrong.
|
||||
const hasMetricValues = isMetricAlertType(alert.type);
|
||||
const currentValue = hasMetricValues ? formatAlertValue(alert.value, alert.type) : '';
|
||||
const thresholdValue = hasMetricValues ? formatAlertValue(alert.threshold, alert.type) : '';
|
||||
const nodeLabel = alert.node ? alert.nodeDisplayName || alert.node : '';
|
||||
const levelLabel = formatAlertLevel(alert.level);
|
||||
const targetType = resolveAlertTargetType({
|
||||
|
|
@ -47,20 +55,32 @@ export function buildAlertAssistantHandoff({
|
|||
levelLabel,
|
||||
});
|
||||
|
||||
const prompt = `Investigate this ${alert.level.toUpperCase()} alert:
|
||||
|
||||
**Resource:** ${alert.resourceName}
|
||||
**Alert Type:** ${alert.type}
|
||||
**Current Value:** ${currentValue}
|
||||
**Threshold:** ${thresholdValue}
|
||||
**Duration:** ${durationText}
|
||||
${nodeLabel ? `**Node:** ${nodeLabel}` : ''}
|
||||
|
||||
Please:
|
||||
1. Identify the root cause
|
||||
2. Check related metrics
|
||||
3. Suggest specific remediation steps
|
||||
4. Ask for operator approval before running any diagnostic command or change`;
|
||||
const promptLines = [
|
||||
`Investigate this ${alert.level.toUpperCase()} alert:`,
|
||||
``,
|
||||
`**Resource:** ${alert.resourceName}`,
|
||||
`**Alert Type:** ${alert.type}`,
|
||||
];
|
||||
if (hasMetricValues) {
|
||||
promptLines.push(`**Current Value:** ${currentValue}`);
|
||||
promptLines.push(`**Threshold:** ${thresholdValue}`);
|
||||
}
|
||||
promptLines.push(`**Duration:** ${durationText}`);
|
||||
if (nodeLabel) promptLines.push(`**Node:** ${nodeLabel}`);
|
||||
if (alert.message) promptLines.push(`**Message:** ${alert.message}`);
|
||||
promptLines.push(``);
|
||||
promptLines.push(`Please:`);
|
||||
promptLines.push(`1. Identify the root cause`);
|
||||
if (hasMetricValues) {
|
||||
promptLines.push(`2. Check related metrics`);
|
||||
promptLines.push(`3. Suggest specific remediation steps`);
|
||||
promptLines.push(`4. Ask for operator approval before running any diagnostic command or change`);
|
||||
} else {
|
||||
promptLines.push(`2. Check what changed recently for this resource (state events, recent commands, related alerts)`);
|
||||
promptLines.push(`3. Suggest specific remediation steps`);
|
||||
promptLines.push(`4. Ask for operator approval before running any diagnostic command or change`);
|
||||
}
|
||||
const prompt = promptLines.join('\n');
|
||||
|
||||
return {
|
||||
prompt,
|
||||
|
|
@ -83,7 +103,9 @@ Please:
|
|||
subject: `${levelLabel} ${alert.type} on ${alert.resourceName}`,
|
||||
statusLabel: `${levelLabel} alert · Active ${durationText}`,
|
||||
detailLines: [
|
||||
`Current value ${currentValue}; threshold ${thresholdValue}`,
|
||||
hasMetricValues
|
||||
? `Current value ${currentValue}; threshold ${thresholdValue}`
|
||||
: undefined,
|
||||
nodeLabel ? `Node: ${nodeLabel}` : undefined,
|
||||
alert.message ? `Message: ${alert.message}` : undefined,
|
||||
].filter((line): line is string => Boolean(line)),
|
||||
|
|
|
|||
|
|
@ -108,3 +108,29 @@ export const getAlertStyles = (
|
|||
hasAcknowledgedOnlyAlert: !hasUnacknowledgedAlert && acknowledgedCount > 0,
|
||||
};
|
||||
};
|
||||
|
||||
// Alert types representing binary or enumerated state conditions rather
|
||||
// than a metric crossing a threshold. For these, "current value vs
|
||||
// threshold" is meaningless (both come through as 0 from the backend) and
|
||||
// surfacing those fields in operator-facing copy is misleading. The
|
||||
// Assistant briefing and prompt builders omit the value/threshold lines
|
||||
// when the alert type is one of these.
|
||||
const STATE_ALERT_TYPES: ReadonlySet<string> = new Set([
|
||||
'powered-off',
|
||||
'unreachable',
|
||||
'offline',
|
||||
'host-offline',
|
||||
'connectivity',
|
||||
'docker-host-offline',
|
||||
'docker-container-state',
|
||||
'docker-container-health',
|
||||
]);
|
||||
|
||||
export function isStateAlertType(alertType: string | undefined): boolean {
|
||||
if (!alertType) return false;
|
||||
return STATE_ALERT_TYPES.has(alertType);
|
||||
}
|
||||
|
||||
export function isMetricAlertType(alertType: string | undefined): boolean {
|
||||
return !isStateAlertType(alertType);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue