mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-21 03:05:44 +00:00
Add Investigate button on Patrol findings
Pulse Intelligence direction named seven contextual Assistant entry points: Explain, Investigate, Prepare fix, Verify fix, Why did this happen, Remember this is expected, Create rule from this. Explain was the only first-class one. Investigate was rolled into "Discuss with Assistant" with a generic open-ended prompt. Splits Investigate off as its own action. Where Explain says "tell me what we already know," Investigate says "go find out what's true right now": - Widens PatrolAssistantFindingIntent to 'discuss' | 'explain' | 'investigate'. - buildPatrolAssistantFindingPrompt gains an investigate branch: the prompt explicitly instructs the LLM to use its Pulse tools (metrics, alerts, resource state, recent changes, correlations) to gather fresh evidence, then synthesize root cause + confidence + safe next step + whether the recommended action still holds. Any command-running step must route through governed approval, not the LLM's own judgment. - FindingsPanel adds an Investigate button between Explain and Copy summary, and a handleInvestigateFinding handler that routes through openFindingInAssistant with the new intent. - Both Explain and Investigate now set autoSendInitialPrompt: true (single condition update); Discuss stays false. Test: a new investigate-intent prompt-builder case asserts the prompt mentions active tool use, the synthesis dimensions (root cause, confidence, safe next step), and the governed- approval safety boundary, and isn't the Discuss seed.
This commit is contained in:
@@ -518,7 +518,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
|
||||
const openFindingInAssistant = async (
|
||||
finding: UnifiedFinding,
|
||||
intent: 'discuss' | 'explain',
|
||||
intent: 'discuss' | 'explain' | 'investigate',
|
||||
) => {
|
||||
await aiIntelligenceStore.loadPendingApprovals();
|
||||
const subject = getFindingSubjectPresentation(finding).label;
|
||||
@@ -559,15 +559,15 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
nextStepAction,
|
||||
intent,
|
||||
});
|
||||
// Explain is an action-style entry point — clicking the button is the
|
||||
// operator asking "do the explaining for me," not "open chat so I can
|
||||
// type a question." Auto-submit the handoff prompt so the analysis is
|
||||
// already in flight when the drawer is on screen. Discuss with
|
||||
// Assistant keeps the previous behaviour (pre-fill, operator drives)
|
||||
// because that entry is open-ended by intent.
|
||||
// Explain and Investigate are action-style entry points — clicking
|
||||
// the button is the operator asking "do this for me," not "open chat
|
||||
// so I can type a question." Auto-submit the handoff prompt so the
|
||||
// work is already in flight when the drawer is on screen. Discuss
|
||||
// with Assistant keeps the previous behaviour (pre-fill, operator
|
||||
// drives) because that entry is open-ended by intent.
|
||||
aiChatStore.openWithPrompt(handoff.prompt, {
|
||||
...handoff.context,
|
||||
autoSendInitialPrompt: intent === 'explain',
|
||||
autoSendInitialPrompt: intent === 'explain' || intent === 'investigate',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -581,6 +581,17 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
await openFindingInAssistant(finding, 'explain');
|
||||
};
|
||||
|
||||
// Investigate is the active counterpart to Explain. Explain says "tell me
|
||||
// what we already know"; Investigate says "go find out what's true right
|
||||
// now." The prompt instructs the LLM to use its Pulse tools (metrics,
|
||||
// alerts, state, recent changes, correlations) rather than just narrate
|
||||
// the attached context, and the surface auto-sends so the work is in
|
||||
// flight when the operator lands.
|
||||
const handleInvestigateFinding = async (finding: UnifiedFinding, e: Event) => {
|
||||
e.stopPropagation();
|
||||
await openFindingInAssistant(finding, 'investigate');
|
||||
};
|
||||
|
||||
// Copy a Markdown summary of the finding to the clipboard so the operator
|
||||
// can paste it into a chat, ticket, or incident channel. The shape mirrors
|
||||
// the seven-question schema (title + impact + recommendation + trust
|
||||
@@ -1173,7 +1184,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
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"
|
||||
title="Ask Pulse Assistant to explain what we know about this finding using the attached evidence"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -1185,6 +1196,22 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
</svg>
|
||||
Explain
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleInvestigateFinding(finding, e)}
|
||||
class="px-2 py-1 rounded border border-border hover:bg-surface-hover flex items-center gap-1"
|
||||
title="Ask Pulse Assistant to actively investigate using metrics, alerts, and state — not just summarize"
|
||||
>
|
||||
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
Investigate
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleCopyFindingSummary(finding, e)}
|
||||
|
||||
@@ -951,6 +951,36 @@ describe('patrolInvestigationContextModel', () => {
|
||||
expect(explainPrompt).toContain('vm-101');
|
||||
});
|
||||
|
||||
it('seeds an investigate-intent prompt that instructs active tool use, not just narration', () => {
|
||||
// Investigate is the action counterpart to Explain. Where Explain says
|
||||
// "tell me what we know," Investigate says "go find out what's true
|
||||
// right now" — the prompt must explicitly direct the LLM toward its
|
||||
// Pulse tools (metrics, alerts, state) rather than just summarizing
|
||||
// the attached briefing.
|
||||
const prompt = buildPatrolAssistantFindingPrompt({
|
||||
title: 'Backup job failing',
|
||||
subject: 'vm-101',
|
||||
description: 'Datastore quota exhausted',
|
||||
intent: 'investigate',
|
||||
});
|
||||
|
||||
expect(prompt).toContain('Investigate this Patrol finding now');
|
||||
expect(prompt).toContain('Backup job failing');
|
||||
expect(prompt).toContain('vm-101');
|
||||
// Active-tool-use instruction — the differentiator vs Explain.
|
||||
expect(prompt.toLowerCase()).toContain('actively use the pulse tools');
|
||||
expect(prompt.toLowerCase()).toMatch(/metrics|alerts|resource state|recent changes/);
|
||||
// Synthesis instruction — root cause + confidence + safe next step.
|
||||
expect(prompt.toLowerCase()).toContain('root cause');
|
||||
expect(prompt.toLowerCase()).toContain('confidence');
|
||||
expect(prompt.toLowerCase()).toContain('safe next step');
|
||||
// Safety: any command-running must route through governed approval,
|
||||
// not the LLM's own judgment.
|
||||
expect(prompt.toLowerCase()).toContain('governed approval');
|
||||
// Must not be the open-ended Discuss prompt.
|
||||
expect(prompt).not.toContain("I'd like to discuss");
|
||||
});
|
||||
|
||||
it('surfaces investigation impact and rollback when the backend record carries them', () => {
|
||||
const presentation = buildPatrolInvestigationRecordPresentation({
|
||||
id: 'record-2',
|
||||
|
||||
@@ -86,7 +86,14 @@ export interface PatrolInvestigationRecordPresentation {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type PatrolAssistantFindingIntent = 'discuss' | 'explain';
|
||||
// 'discuss' = open-ended chat entry, operator drives.
|
||||
// 'explain' = action-style "do the explaining," auto-sent.
|
||||
// 'investigate' = action-style "run the investigation now using tools" —
|
||||
// the prompt explicitly invites the LLM to query metrics/alerts/state
|
||||
// rather than just narrate the attached context, and the surface
|
||||
// auto-sends the prompt so the work is already in flight on the screen
|
||||
// the operator lands on.
|
||||
export type PatrolAssistantFindingIntent = 'discuss' | 'explain' | 'investigate';
|
||||
|
||||
export interface PatrolAssistantFindingPromptInput {
|
||||
title: string;
|
||||
@@ -526,10 +533,19 @@ export function buildPatrolAssistantFindingPrompt(
|
||||
'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 if (input.intent === 'investigate') {
|
||||
prompt =
|
||||
`Investigate this Patrol finding now: "${title}" on ${subject}. ` +
|
||||
'Do not just summarize the attached context — actively use the Pulse tools available to you ' +
|
||||
'(metrics, alerts, resource state, recent changes, correlations) to gather fresh evidence ' +
|
||||
'about the current state of the affected resource and any related resources. ' +
|
||||
'Then synthesize: what is the root cause given the current evidence, ' +
|
||||
'what is your confidence, what is the safe next step, and is the recommended action still right? ' +
|
||||
'If any command-running step is involved, surface it for governed approval rather than executing on your own judgment.';
|
||||
} else {
|
||||
prompt = `I'd like to discuss this Patrol finding: "${title}" on ${subject}.`;
|
||||
}
|
||||
if (hasRecord && input.intent !== 'explain') {
|
||||
if (hasRecord && input.intent !== 'explain' && input.intent !== 'investigate') {
|
||||
prompt +=
|
||||
'\n\nPulse Patrol has a structured investigation record for this finding. Use that record as the main context before suggesting next actions.';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user