mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 18:53:37 +00:00
Add Explain contextual button to findings
Adds a contextual "Explain" entry point next to the existing
"Discuss with Assistant" button on every finding card. The new button
opens Assistant with the same handoff context (investigation record,
operational memory, pending approval, proposed fix) but seeds a
different leading sentence: "Explain this Patrol finding... Walk me
through what we know, why it matters for the affected workloads, how
confident the analysis is, and whether the recommended action is the
right next step." This routes the LLM toward an explanatory framing
rather than open-ended discussion, matching the user's vision of
specific contextual entry points instead of a single generic chat
button.
Plumbing changes:
- New PatrolAssistantFindingIntent type ('discuss' | 'explain')
- Optional intent on PatrolAssistantFindingPromptInput and
PatrolAssistantFindingHandoffInput
- buildPatrolAssistantFindingPrompt switches the leading sentence on
intent; downstream context attachment is identical so trust signals
(impact, confidence, previous resolved fix, etc.) flow through both
paths uniformly.
- FindingsPanel extracts the shared handoff into a small
openFindingInAssistant(finding, intent) helper used by both
handleDiscussWithAssistant and the new handleExplainFinding.
Adds two tests: explain-intent prompt has explanation framing while
discuss-intent keeps the existing wording, and the FindingsPanel
source-text test pins the new button + handler wiring. Updates the
patrol-intelligence, frontend-primitives, and api-contracts contracts
to pin the contextual-intent rule and the uniform-context-attachment
invariant.
This commit is contained in:
@@ -966,6 +966,14 @@ the canonical monitored-system blocked payload.
|
||||
persisted findings created by older binaries must adopt the
|
||||
freshly-classified impact text on next re-detection rather than
|
||||
preserving the empty value.
|
||||
The Patrol Assistant handoff carries an optional
|
||||
`intent` field of type `PatrolAssistantFindingIntent`
|
||||
(`'discuss' | 'explain'`) so contextual entry points on the finding
|
||||
surface can route through the same handoff builder while seeding
|
||||
different leading sentences. The structured handoff context
|
||||
(investigation record, operational memory, pending approval,
|
||||
proposed fix, next-step action) must be attached identically across
|
||||
intents; only the seed prompt's framing varies.
|
||||
`unified.UnifiedFinding` also carries a
|
||||
`previous_resolved_fix_summary` operational-memory field captured at
|
||||
regression time from
|
||||
|
||||
@@ -942,9 +942,13 @@ prompt explain the same operator-facing priority.
|
||||
investigation-record framing must derive that prompt copy through
|
||||
`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`
|
||||
so shared drawer primitives stay shell-owned rather than becoming a
|
||||
Patrol-specific prompt formatter. That feature-owned presentation
|
||||
helper is the single emitter for investigation-record `impact` and
|
||||
`rollback` fields: when an investigation record exists but those fields
|
||||
Patrol-specific prompt formatter. The Patrol-owned helper exposes a
|
||||
`PatrolAssistantFindingIntent` parameter ('discuss' | 'explain') so
|
||||
contextual entry points can vary the seeded leading sentence without
|
||||
duplicating the structured-context attachment; shared drawer
|
||||
primitives must not branch on intent themselves. That feature-owned
|
||||
presentation helper is the single emitter for investigation-record
|
||||
`impact` and `rollback` fields: when an investigation record exists but those fields
|
||||
are empty, the helper emits explicit `Impact not assessed` and
|
||||
`Rollback not specified` lines into the model-only Patrol finding
|
||||
prompt context so the operator-visible gap is surfaced to Assistant
|
||||
|
||||
@@ -258,7 +258,17 @@ Patrol-specific presentation helpers.
|
||||
renders an `Impact:` line between `Description` and `Recommendation`.
|
||||
The same panel also surfaces `investigation_record.confidence` as a
|
||||
badge in the collapsed finding row (next to the investigation outcome
|
||||
badge) so operators can scan trust without expanding every card. The
|
||||
badge) so operators can scan trust without expanding every card.
|
||||
Findings carry contextual Assistant entry points keyed on intent: the
|
||||
default "Discuss with Assistant" button opens an open-ended chat
|
||||
handoff, and a parallel "Explain" button opens the same handoff with
|
||||
a `PatrolAssistantFindingIntent='explain'` seed that asks the LLM to
|
||||
walk through what we know, why it matters, how confident the
|
||||
analysis is, and whether the recommended action is appropriate. Both
|
||||
buttons must route through `buildPatrolAssistantFindingHandoff` so
|
||||
the structured context (investigation record, operational memory,
|
||||
pending approval, proposed fix, next-step action) is attached
|
||||
uniformly; only the leading sentence differs by intent. The
|
||||
badge palette is provided by `getInvestigationConfidenceBadgeClasses`
|
||||
in `frontend-modern/src/utils/aiFindingPresentation.ts`: high is
|
||||
reassuringly emphasized, medium is neutral, low is a soft amber.
|
||||
|
||||
@@ -498,8 +498,10 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscussWithAssistant = async (finding: UnifiedFinding, e: Event) => {
|
||||
e.stopPropagation();
|
||||
const openFindingInAssistant = async (
|
||||
finding: UnifiedFinding,
|
||||
intent: 'discuss' | 'explain',
|
||||
) => {
|
||||
await aiIntelligenceStore.loadPendingApprovals();
|
||||
const subject = getFindingSubjectPresentation(finding).label;
|
||||
const title = getFindingTitlePresentation(finding).label;
|
||||
@@ -537,10 +539,21 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
proposedFix,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
nextStepAction,
|
||||
intent,
|
||||
});
|
||||
aiChatStore.openWithPrompt(handoff.prompt, handoff.context);
|
||||
};
|
||||
|
||||
const handleDiscussWithAssistant = async (finding: UnifiedFinding, e: Event) => {
|
||||
e.stopPropagation();
|
||||
await openFindingInAssistant(finding, 'discuss');
|
||||
};
|
||||
|
||||
const handleExplainFinding = async (finding: UnifiedFinding, e: Event) => {
|
||||
e.stopPropagation();
|
||||
await openFindingInAssistant(finding, 'explain');
|
||||
};
|
||||
|
||||
const handleOpenPlanInAssistant = (finding: UnifiedFinding, plan: RemediationPlan, e: Event) => {
|
||||
e.stopPropagation();
|
||||
const subject = getFindingSubjectPresentation(finding).label;
|
||||
@@ -1022,6 +1035,22 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
Add Note
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleExplainFinding(finding, e)}
|
||||
class="px-2 py-1 rounded border border-border hover:bg-surface-hover flex items-center gap-1"
|
||||
title="Open Assistant with an explanation prompt"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
Explain
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleDiscussWithAssistant(finding, e)}
|
||||
|
||||
@@ -76,6 +76,17 @@ describe('FindingsPanel assistant handoff', () => {
|
||||
expect(findingsPanelSource).not.toContain('Rollback: `');
|
||||
});
|
||||
|
||||
it('exposes an Explain entry point that seeds Assistant with an explanation prompt', () => {
|
||||
// The Explain button is a contextual entry point parallel to "Discuss with
|
||||
// Assistant" but with an intent that produces explanation framing rather
|
||||
// than open-ended discussion. Both flow through the same handoff builder
|
||||
// so context attachment stays uniform.
|
||||
expect(findingsPanelSource).toContain('handleExplainFinding');
|
||||
expect(findingsPanelSource).toContain("openFindingInAssistant(finding, 'explain')");
|
||||
expect(findingsPanelSource).toMatch(/>\s*Explain\s*</);
|
||||
expect(findingsPanelSource).toContain("openFindingInAssistant(finding, 'discuss')");
|
||||
});
|
||||
|
||||
it('surfaces investigation_record.confidence as a badge in the collapsed row', () => {
|
||||
// The seven-question schema's confidence answer should be visible to
|
||||
// operators without expanding the card. The badge sits next to the
|
||||
|
||||
@@ -876,6 +876,27 @@ describe('patrolInvestigationContextModel', () => {
|
||||
expect(presentation.impact).toBe('');
|
||||
});
|
||||
|
||||
it('seeds an explain-intent prompt with explanation framing rather than discussion framing', () => {
|
||||
const discussPrompt = buildPatrolAssistantFindingPrompt({
|
||||
title: 'Backup job failing',
|
||||
subject: 'vm-101',
|
||||
description: 'Datastore quota exhausted',
|
||||
});
|
||||
const explainPrompt = buildPatrolAssistantFindingPrompt({
|
||||
title: 'Backup job failing',
|
||||
subject: 'vm-101',
|
||||
description: 'Datastore quota exhausted',
|
||||
intent: 'explain',
|
||||
});
|
||||
|
||||
expect(discussPrompt).toContain("I'd like to discuss");
|
||||
expect(explainPrompt).toContain('Explain this Patrol finding');
|
||||
expect(explainPrompt.toLowerCase()).toContain('walk me through what we know');
|
||||
// Both prompts include the title and subject so the seed reads naturally.
|
||||
expect(explainPrompt).toContain('Backup job failing');
|
||||
expect(explainPrompt).toContain('vm-101');
|
||||
});
|
||||
|
||||
it('surfaces investigation impact and rollback when the backend record carries them', () => {
|
||||
const presentation = buildPatrolInvestigationRecordPresentation({
|
||||
id: 'record-2',
|
||||
|
||||
@@ -73,6 +73,8 @@ export interface PatrolInvestigationRecordPresentation {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type PatrolAssistantFindingIntent = 'discuss' | 'explain';
|
||||
|
||||
export interface PatrolAssistantFindingPromptInput {
|
||||
title: string;
|
||||
subject: string;
|
||||
@@ -83,6 +85,7 @@ export interface PatrolAssistantFindingPromptInput {
|
||||
proposedFix?: PatrolAssistantProposedFixBriefingInput | null;
|
||||
investigationRecord?: InvestigationRecord | null;
|
||||
nextStepAction?: PatrolAssistantNextStepInput | null;
|
||||
intent?: PatrolAssistantFindingIntent;
|
||||
}
|
||||
|
||||
export interface PatrolAssistantApprovalBriefingInput {
|
||||
@@ -167,6 +170,7 @@ export interface PatrolAssistantFindingHandoffInput {
|
||||
proposedFix?: PatrolAssistantProposedFixBriefingInput | null;
|
||||
investigationRecord?: InvestigationRecord | null;
|
||||
nextStepAction?: PatrolAssistantNextStepInput | null;
|
||||
intent?: PatrolAssistantFindingIntent;
|
||||
}
|
||||
|
||||
export interface PatrolAssistantFindingModeInput {
|
||||
@@ -502,8 +506,17 @@ export function buildPatrolAssistantFindingPrompt(
|
||||
const actionInstruction = buildPatrolAssistantFindingActionPromptInstruction(input);
|
||||
const nextStepAction = normalizePatrolAssistantNextStepAction(input.nextStepAction);
|
||||
|
||||
let prompt = `I'd like to discuss this Patrol finding: "${title}" on ${subject}.`;
|
||||
if (hasRecord) {
|
||||
let prompt: string;
|
||||
if (input.intent === 'explain') {
|
||||
prompt =
|
||||
`Explain this Patrol finding: "${title}" on ${subject}. ` +
|
||||
'Walk me through what we know, why it matters for the affected workloads, ' +
|
||||
'how confident the analysis is, and whether the recommended action is the right next step. ' +
|
||||
'Use the structured investigation record and operational memory in the attached context as the primary source.';
|
||||
} else {
|
||||
prompt = `I'd like to discuss this Patrol finding: "${title}" on ${subject}.`;
|
||||
}
|
||||
if (hasRecord && input.intent !== 'explain') {
|
||||
prompt +=
|
||||
'\n\nPulse Patrol has a structured investigation record for this finding. Use that record as the main context before suggesting next actions.';
|
||||
}
|
||||
@@ -648,6 +661,7 @@ export function buildPatrolAssistantFindingHandoff(
|
||||
proposedFix: input.proposedFix,
|
||||
investigationRecord: input.investigationRecord,
|
||||
nextStepAction: input.nextStepAction,
|
||||
intent: input.intent,
|
||||
}),
|
||||
context: {
|
||||
targetType: resource?.type,
|
||||
|
||||
Reference in New Issue
Block a user