All checks were successful
Mirror to GitHub / mirror (push) Successful in 10s
Wires the SuggestedFix card to an inline panel that handles both cases:
template-matched fixes open the Script Library generator with parameters
pre-filled from session context; un-matched fixes open the three-option
dialog (one_off / draft_template / build_template). The decision endpoint
records the path choice with side effects: draft_template persists a
draft_templates row via a Sonnet-driven TemplateExtractionService;
build_template returns a redirect to the Script Builder; one_off just
records the choice.
Backend:
- TemplateExtractionService: drafts a parameter schema from a concrete
rendered script. Conservative by default ("prefer fewer parameters").
Round-trip-validates that templated_body only references declared
parameters; missing-key mismatch falls back to the original script
with no params. LLM/parse failures fall back identically — the
engineer can still create a draft and refine in the post-resolve
prompt (Phase 6).
- /suggested-fixes/{fix_id}/decision side effects:
* one_off → returns rendered_script (engineer's edited version or the
fix's ai_drafted_script verbatim)
* draft_template → same + creates draft_templates row with extracted
params, returns draft_template_id
* build_template → returns redirect_path=/scripts/builder?from_session=
&fix= so the frontend can navigate to the builder pre-loaded
- 400 when a non-template fix has no ai_drafted_script (template-matched
fixes take the dedicated /scripts/generate path, not this endpoint).
- 12 tests: TemplateExtractionService parse + fallback paths, all four
decision branches, edited_script override, missing-script 400.
Frontend:
- src/components/pilot/script/{TemplateMatchPanel, NoTemplateDialog,
ParameterizationPreview}.tsx — inline panels rendered in the task
lane's bottom slot when the engineer clicks a SuggestedFix card.
- TemplateMatchPanel: loads template via /scripts/templates/{id},
pre-fills params from fix.ai_drafted_parameters with cyan "from
session" tags, generates via existing /scripts/generate (already
bumps state_version on ai_session_id from Phase 3). 404 falls back
with a clear message instead of erroring.
- NoTemplateDialog: shows the AI-drafted script with proposed parameter
values highlighted in amber via ParameterizationPreview; three option
cards with the middle (draft_template) flagged Recommended; inline
edit on the script body before deciding.
- SuggestedFix card now clickable: onActivate toggles the inline panel.
- AssistantChatPage: scriptPanelOpen state + handleScriptDecision that
navigates on build_template and toasts on the other paths. Active fix
changes auto-close the panel so engineers don't act on stale state.
- Cmd+K → "Open inline Script Generator" palette entry surfaces only on
/pilot/:id routes; fires a window event the chat page subscribes to.
No Resolve shortcut added per Section 14 decision (browser ⌘R conflict).
Verified 2026-04-22 against the dev stack:
- one_off / draft_template / build_template all return the right shape
with real Sonnet TemplateExtractionService for the draft path.
- Conservative extraction confirmed: cmdkey + Restart-Process script
yielded zero proposed parameters as intended.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
113 lines
4.5 KiB
Python
113 lines
4.5 KiB
Python
"""Pydantic schemas for session suggested fixes (Phase 3).
|
|
|
|
See FLOWPILOT-MIGRATION.md Section 5.2.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Literal
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
UserDecision = Literal["one_off", "draft_template", "build_template", "dismissed"]
|
|
|
|
|
|
class SessionSuggestedFixResponse(BaseModel):
|
|
id: UUID
|
|
session_id: UUID
|
|
title: str
|
|
description: str
|
|
confidence_pct: int
|
|
script_template_id: UUID | None
|
|
ai_drafted_script: str | None
|
|
ai_drafted_parameters: dict[str, Any] | None
|
|
user_decision: UserDecision | None
|
|
superseded_at: datetime | None
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class SessionSuggestedFixDecisionRequest(BaseModel):
|
|
"""Engineer's path choice on a suggested fix.
|
|
|
|
Server-side side effects per Section 5.2:
|
|
- one_off: record decision, return the rendered (AI-drafted or
|
|
engineer-edited) script. No persistent library artifact created.
|
|
- draft_template: same as one_off, plus TemplateExtractionService
|
|
proposes a parameterization and a draft_templates row is created.
|
|
- build_template: return a redirect payload pointing at the Script
|
|
Builder page, pre-loaded with the drafted script body.
|
|
- dismissed: mark the fix superseded.
|
|
|
|
For one_off / draft_template, the engineer may have edited the drafted
|
|
script or its parameters in the dialog. The final versions are sent
|
|
back here so we persist what will actually run.
|
|
"""
|
|
decision: UserDecision
|
|
# Present for one_off / draft_template — the engineer's final version of
|
|
# the drafted script after any inline edits. Omit to use the fix's
|
|
# `ai_drafted_script` verbatim.
|
|
edited_script: str | None = Field(None, min_length=1, max_length=50_000)
|
|
# Parameter values used when rendering (informational, stored on the
|
|
# draft_template row so a reviewer can see what the first run used).
|
|
parameters_used: dict[str, Any] | None = None
|
|
|
|
|
|
class SessionSuggestedFixDecisionResponse(BaseModel):
|
|
"""Returned after recording a decision."""
|
|
id: UUID
|
|
user_decision: UserDecision
|
|
# Populated for one_off / draft_template — the script to display/run.
|
|
rendered_script: str | None = None
|
|
# Populated for draft_template — the ID of the draft_templates row so
|
|
# the post-resolve TemplatizePrompt can fetch it in Phase 6.
|
|
draft_template_id: UUID | None = None
|
|
# Populated for build_template — where to send the engineer next.
|
|
redirect_path: str | None = Field(
|
|
None,
|
|
description="Where to send the engineer next (e.g. /scripts/builder?... for build_template)",
|
|
)
|
|
|
|
|
|
# ── Resolution note preview ────────────────────────────────────────────────
|
|
|
|
class ResolutionNotePreviewResponse(BaseModel):
|
|
markdown: str
|
|
target_ticket_ref: str | None
|
|
state_version: int
|
|
from_cache: bool
|
|
|
|
|
|
# ── Phase 4: Resolve + Escalate post ───────────────────────────────────────
|
|
|
|
class ResolutionNotePostRequest(BaseModel):
|
|
"""Engineer-edited resolution markdown. Server posts to PSA + marks resolved."""
|
|
markdown: str = Field(..., min_length=1, max_length=20_000)
|
|
# Optional override for resolution summary shown on the session listing;
|
|
# defaults to the first line of the markdown if omitted.
|
|
resolution_summary: str | None = Field(None, max_length=500)
|
|
|
|
|
|
class EscalationPackagePostRequest(BaseModel):
|
|
markdown: str = Field(..., min_length=1, max_length=20_000)
|
|
# Free-text reason shown in session listings and escalation queue.
|
|
escalation_reason: str | None = Field(None, max_length=500)
|
|
|
|
|
|
class ResolutionPostResponse(BaseModel):
|
|
"""Response shape for both Resolve/Escalate POST endpoints."""
|
|
# "resolved" / "escalated" / "resolved_local" / "escalated_local"
|
|
# The _local variants indicate the session has no linked PSA ticket —
|
|
# markdown is stored, session state is updated, nothing was posted externally.
|
|
outcome: str
|
|
session_status: str
|
|
external_id: str | None = None
|
|
posted_at: datetime | None = None
|
|
# Populated when a status transition was attempted and verified. None
|
|
# when no target status is configured in account_settings.preferences.
|
|
verified_status_id: int | None = None
|
|
verified_status_name: str | None = None
|
|
status_transition_skipped_reason: str | None = None
|