Files
resolutionflow/backend/app/schemas/draft_template.py
Michael Chihlas 4aaf57adb5
All checks were successful
Mirror to GitHub / mirror (push) Successful in 11s
feat(pilot): Phase 6 — post-resolve templatize prompt + draft accept/reject
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>
2026-04-22 02:37:49 -04:00

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"]