Adds FixStatus literal (5 values matching the DB check constraint), extends SessionSuggestedFixResponse with outcome fields, and introduces SessionSuggestedFixOutcomeRequest for the PATCH /outcome endpoint coming in Task 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
154 lines
6.0 KiB
Python
154 lines
6.0 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"]
|
|
|
|
# "dismissed" here is the outcome dimension — orthogonal to UserDecision's
|
|
# "dismissed" (script-path choice), though the migration backfill aligns
|
|
# them for pre-existing rows.
|
|
FixStatus = Literal[
|
|
"proposed",
|
|
"applied_success",
|
|
"applied_failed",
|
|
"applied_partial",
|
|
"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
|
|
status: FixStatus
|
|
applied_at: datetime | None
|
|
verified_at: datetime | None
|
|
partial_notes: str | None
|
|
failure_reason: str | None
|
|
ai_outcome_proposal: dict[str, Any] | None
|
|
|
|
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)",
|
|
)
|
|
|
|
|
|
# Subset of FixStatus that the engineer can set via the outcome endpoint —
|
|
# `proposed` is excluded because you can't un-decide a fix back to "proposed".
|
|
FixOutcome = Literal[
|
|
"applied_success", "applied_failed", "applied_partial", "dismissed"
|
|
]
|
|
|
|
|
|
class SessionSuggestedFixOutcomeRequest(BaseModel):
|
|
"""Engineer-reported outcome of applying a suggested fix.
|
|
|
|
Writes to session_suggested_fixes.status and companion columns. This is
|
|
orthogonal to `user_decision` (which records which script-path the
|
|
engineer took); outcome captures whether the fix actually worked.
|
|
|
|
Allowed transitions:
|
|
- from `proposed`: any of applied_success | applied_failed | applied_partial | dismissed
|
|
- from `applied_partial`: applied_success | applied_failed (partial is not terminal)
|
|
- from any terminal outcome: no change (server returns 409)
|
|
"""
|
|
outcome: FixOutcome
|
|
# Required for applied_partial, optional for applied_failed, ignored otherwise.
|
|
notes: str | None = Field(None, max_length=500)
|
|
|
|
|
|
# ── 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
|