All checks were successful
Mirror to GitHub / mirror (push) Successful in 11s
Closes the loop on the Phase 5 "Run now, templatize after resolve" path.
After a session resolves, drafts queued by the three-option dialog surface
as a modal that lets the engineer review the AI-proposed parameterization
and either save as a reusable team template or skip. A "don't ask again"
toggle writes to account_settings.preferences so the next resolve won't
pop the modal.
Backend:
- /api/v1/draft-templates:
* GET — list account drafts (pending_only default true; pass false for
audit view including accepted/rejected)
* GET /{id} — single draft
* POST /{id}/accept — promotes to a new script_templates row with
source_session_id / source_user_id / source_ticket_ref populated
(drives the Script Library "generated from CW #X · resolved by Y"
provenance chip). Draft flips to status=accepted,
promoted_template_id set, resolved_at stamped. 409 on re-accept /
already-rejected. 400 on unknown category_id.
* POST /{id}/reject — flips to status=rejected. 409 on re-reject.
- /api/v1/accounts/me/preferences (GET/PATCH) — thin wrapper over
AccountSettings.get_setting/set_setting. PATCH merges keys into the
JSONB column, preserving existing keys the client didn't touch.
Used by the "Don't ask again for this team" checkbox
(templatize_prompt_enabled=false) and, forward-looking, by
cw_resolved_status_id / cw_escalated_status_id from Phase 4.
- 13 tests: list filter, accept with/without edited_body, provenance
copy-through, reject, 409 on re-accept / re-reject, 400 on unknown
category, prefs round-trip with merge semantics.
Frontend:
- src/components/pilot/script/TemplatizePrompt.tsx — modal showing the
drafted script with proposed parameters in the Phase 5
ParameterizationPreview, editable name/category/description, an
individual-parameter remove button, and the "don't ask again" opt-out.
Accept posts to /draft-templates/{id}/accept + optionally PATCHes
preferences. Skip posts /reject.
- src/api/draftTemplates.ts — typed client plus accountPreferencesApi.
- AssistantChatPage: after a successful Resolve (external OR local),
fetches preferences + pending drafts for the session and queues the
modal one draft at a time. Escalate does not trigger this flow.
- Sidebar: Scripts nav shows the pending-draft count as a badge. Fetched
independently of the main sidebar stats so endpoint flakes don't
break the rest of the sidebar.
Verified live 2026-04-22: seed two drafts → GET sees both pending →
accept draft A (template created, provenance CW #99123 populated) →
reject draft B → pending count drops → PATCH opt-out → GET confirms
persistence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
69 lines
2.1 KiB
Python
69 lines
2.1 KiB
Python
"""Pydantic schemas for FlowPilot Phase 6 draft templates.
|
|
|
|
A draft is the engineer's "Run now, templatize after resolve" path output:
|
|
the script ran for the ticket, and the AI proposed a parameterization.
|
|
Post-resolve, the engineer accepts (promotes to a real template) or rejects.
|
|
|
|
See FLOWPILOT-MIGRATION.md Section 5.3.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Literal
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
DraftStatus = Literal["pending", "accepted", "rejected"]
|
|
|
|
|
|
class DraftTemplateResponse(BaseModel):
|
|
id: UUID
|
|
account_id: UUID
|
|
source_session_id: UUID
|
|
source_user_id: UUID
|
|
script_body: str
|
|
proposed_parameters: dict[str, Any]
|
|
proposed_name: str | None
|
|
proposed_category_id: UUID | None
|
|
status: DraftStatus
|
|
resolved_at: datetime | None
|
|
promoted_template_id: UUID | None
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class DraftTemplateListResponse(BaseModel):
|
|
drafts: list[DraftTemplateResponse]
|
|
|
|
|
|
class DraftTemplateAcceptRequest(BaseModel):
|
|
"""Engineer's confirmation that this draft should become a real template.
|
|
|
|
Engineer may override the AI's proposed name / category and edit the
|
|
parameter schema before promotion. Body and parameters_schema are
|
|
persisted to the new `script_templates` row.
|
|
"""
|
|
name: str = Field(..., min_length=1, max_length=200)
|
|
category_id: UUID
|
|
description: str | None = Field(None, max_length=2000)
|
|
# Final parameter schema in the Script Generator's standard shape.
|
|
# See ScriptTemplate.parameters_schema for the contract.
|
|
parameters_schema: dict[str, Any]
|
|
# Optional last-minute edits to the script body. Defaults to the draft's
|
|
# `script_body` (which TemplateExtractionService produced as the templated
|
|
# form with `{{ key }}` placeholders).
|
|
edited_body: str | None = Field(None, min_length=1, max_length=50_000)
|
|
|
|
|
|
class DraftTemplateAcceptResponse(BaseModel):
|
|
draft_id: UUID
|
|
promoted_template_id: UUID
|
|
template_slug: str
|
|
|
|
|
|
class DraftTemplateRejectResponse(BaseModel):
|
|
draft_id: UUID
|
|
status: Literal["rejected"]
|