feat(pilot): Phase 3 — Suggested fix tracking + Resolve preview with state_version cache
Adds the AI-proposed resolution path and the inline preview of the
markdown that will be posted to the customer ticket on Resolve. The
preview is keyed on (session_id, ai_sessions.state_version) so back-to-
back fetches against unchanged state hit an in-process cache instead
of paying for a Sonnet call.
Backend:
- preview_cache: in-process LRU keyed on (kind, session_id, state_version).
No TTL — state_version is the source of truth. Soft-cap 5000 entries.
- unified_chat_service: [SUGGEST_FIX] parser (last-block-wins, JSON
payload, confidence clamped 0-100), supersession persistence (sets
superseded_at on prior active row), atomic state_version bump.
- ResolutionNoteGeneratorService: pulls session, facts, active fix, and
redacted script_generations into a structured input bundle for Sonnet;
produces the four-section markdown (Problem / What we confirmed /
Root cause / Resolution). Sensitive script parameters redacted via
ScriptTemplateEngine.redact_sensitive driven by the template's
parameters_schema.
- /api/v1/ai-sessions/{id}/suggested-fixes/active — 200 with the active
fix or 404.
- /api/v1/ai-sessions/{id}/suggested-fixes/{fix_id}/decision — records
one_off / draft_template / build_template / dismissed; dismiss
supersedes; bumps state_version. 409 on dismissing an already-
superseded fix.
- /api/v1/ai-sessions/{id}/resolution-note/preview — generates or returns
cached markdown; from_cache flag in payload signals cache hit.
- scripts.py POST /generate now bumps state_version on the linked
ai_session_id when present (third source of preview-cache invalidation
per Section 5.5).
- ASSISTANT_SYSTEM_PROMPT documents [SUGGEST_FIX] (when to/not to emit,
format, supersession semantics).
- 12 tests covering the parser (well-formed, last-wins, malformed,
confidence clamping), supersession + state_version invariant, all
decision branches, preview cache hit-on-no-change + miss-after-write.
Frontend:
- src/components/pilot/sections/SuggestedFix.tsx — amber-accented card
with confidence badge; dismiss action wired to the decision endpoint.
- src/components/pilot/ResolutionNotePreview.tsx — popover with refresh,
loading state, cached/fresh indicator, ticket-ref display.
- src/api/sessionSuggestedFixes.ts — typed client; getActive normalizes
404 to null so callers don't have to special-case.
- TaskLane gains suggestedFixSlot + bottomSlot props (rendered after
Diagnostic Checks; bottomSlot anchors the Resolve action).
- AssistantChatPage: refreshSessionDerived helper batches fact + fix
refresh; fact mutations and chat sends both schedule a 500ms-debounced
preview refresh per the Section 5.5 spec.
Verified end-to-end against the dev stack with a real Sonnet call:
- /active 404 → fact create → preview generates four-section markdown
grounded only in provided facts → second preview call hits cache
(from_cache=true, no LLM call) → fact write 2 → cache miss, regenerates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
84
frontend/src/api/sessionSuggestedFixes.ts
Normal file
84
frontend/src/api/sessionSuggestedFixes.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Session suggested-fix + resolution-note preview API (Phase 3).
|
||||
*
|
||||
* Mirrors backend endpoints under /api/v1/ai-sessions/{id}/...
|
||||
* See FLOWPILOT-MIGRATION.md Sections 5.2 + 5.4.
|
||||
*/
|
||||
import apiClient from './client'
|
||||
|
||||
export type UserDecision = 'one_off' | 'draft_template' | 'build_template' | 'dismissed'
|
||||
|
||||
export interface SessionSuggestedFix {
|
||||
id: string
|
||||
session_id: string
|
||||
title: string
|
||||
description: string
|
||||
confidence_pct: number
|
||||
script_template_id: string | null
|
||||
ai_drafted_script: string | null
|
||||
ai_drafted_parameters: Record<string, unknown> | null
|
||||
user_decision: UserDecision | null
|
||||
superseded_at: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface DecisionResponse {
|
||||
id: string
|
||||
user_decision: UserDecision
|
||||
rendered_script: string | null
|
||||
redirect_path: string | null
|
||||
}
|
||||
|
||||
export interface ResolutionNotePreview {
|
||||
markdown: string
|
||||
target_ticket_ref: string | null
|
||||
state_version: number
|
||||
from_cache: boolean
|
||||
}
|
||||
|
||||
export const sessionSuggestedFixesApi = {
|
||||
/**
|
||||
* Returns the active suggested fix for a session, or `null` if there isn't one.
|
||||
* The endpoint returns 404 in the no-fix case, which is normal — we coerce
|
||||
* to null so callers don't have to distinguish "no fix" from "request failed".
|
||||
*/
|
||||
async getActive(sessionId: string): Promise<SessionSuggestedFix | null> {
|
||||
try {
|
||||
const r = await apiClient.get<SessionSuggestedFix>(
|
||||
`/ai-sessions/${sessionId}/suggested-fixes/active`,
|
||||
)
|
||||
return r.data
|
||||
} catch (err) {
|
||||
const status = (err as { response?: { status?: number } })?.response?.status
|
||||
if (status === 404) return null
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
async recordDecision(
|
||||
sessionId: string,
|
||||
fixId: string,
|
||||
decision: UserDecision,
|
||||
): Promise<DecisionResponse> {
|
||||
const r = await apiClient.post<DecisionResponse>(
|
||||
`/ai-sessions/${sessionId}/suggested-fixes/${fixId}/decision`,
|
||||
{ decision },
|
||||
)
|
||||
return r.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch (or get cached) draft markdown for the Resolve note. Backend cache
|
||||
* is keyed on state_version, so calling this back-to-back without intervening
|
||||
* fact / suggested-fix / script-generation writes returns the same payload
|
||||
* cheaply (no Sonnet call).
|
||||
*/
|
||||
async getResolutionNotePreview(sessionId: string): Promise<ResolutionNotePreview> {
|
||||
const r = await apiClient.post<ResolutionNotePreview>(
|
||||
`/ai-sessions/${sessionId}/resolution-note/preview`,
|
||||
)
|
||||
return r.data
|
||||
},
|
||||
}
|
||||
|
||||
export default sessionSuggestedFixesApi
|
||||
Reference in New Issue
Block a user