All checks were successful
Mirror to GitHub / mirror (push) Successful in 11s
Wires the preview popover's Confirm & post action to ConnectWise (and,
via the provider pattern, any future PSA). Adds the parallel Escalate
flow with the handoff-oriented five-section markdown. Sessions without a
linked PSA ticket resolve/escalate locally — markdown stored, status
flipped, nothing posted externally.
Backend:
- EscalationPackageGeneratorService: Sonnet, five sections (Problem /
What we've confirmed / What we've tried / Current hypothesis /
Suggested next steps). Shares the preview_cache with a separate KIND
so Resolve and Escalate previews for the same state coexist.
- PSAWritebackService: post_resolution_note (RESOLUTION note type,
customer-visible), post_escalation_package (INTERNAL_ANALYSIS,
handoff for the next engineer only), transition_ticket_status with
mandatory re-fetch verification. PSAStatusVerificationError surfaces
loudly when CW silently rejects a status change — the
ConnectWise anti-pattern CLAUDE.md flags.
- Endpoints:
* POST /ai-sessions/{id}/escalation-package/preview
* POST /ai-sessions/{id}/resolution-note/post
* POST /ai-sessions/{id}/escalation-package/post
Outcomes: "resolved" / "escalated" with external_id + verified status,
"resolved_local" / "escalated_local" when no PSA linked.
- Target CW status IDs live in account_settings.preferences
(cw_resolved_status_id, cw_escalated_status_id). When unset, the post
proceeds without a status transition — response includes a
status_transition_skipped_reason rather than silently erroring.
- 7 tests: local-only path, PSA happy path with verified transition,
status verification failure → 502, skipped transition when
unconfigured, 409 on already-resolved re-post, escalate parallel path,
internal-analysis note type enforced.
Frontend:
- ResolutionNotePreview now kind-parameterized ('resolve' | 'escalate')
with inline edit + Confirm & post. Preview loads from the matching
backend endpoint; posting calls the matching endpoint; outcome toast
surfaces the verified CW status or the local-only result.
- AssistantChatPage: previewKind state replaces previewOpen; two toggle
buttons (Preview Resolve note / Escalate instead) in the lane's bottom
slot. handleConfirmPost dispatches by kind.
Verified 2026-04-22:
- Local-only Resolve + Escalate round-trip against the dev stack.
- Live Sonnet escalation-package preview; cache hit on repeat call
with no state change (separate cache kind from resolution-note).
- PSA post + status-verification paths covered by mocked-provider pytest
cases. Live CW round-trip pending a test CW instance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
3.6 KiB
Python
96 lines
3.6 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: render the script (Phase 5), no template created.
|
|
- draft_template: render + queue a draft_templates row (Phase 5/6).
|
|
- build_template: redirect to full template creation (Phase 5).
|
|
- dismissed: mark the fix superseded so a fresh suggestion can take over.
|
|
"""
|
|
decision: UserDecision
|
|
|
|
|
|
class SessionSuggestedFixDecisionResponse(BaseModel):
|
|
"""Returned after recording a decision; richer payloads land in Phase 5."""
|
|
id: UUID
|
|
user_decision: UserDecision
|
|
# Set when the decision triggered side effects (e.g. a script generation).
|
|
# Phase 3 only records the choice; this stays None until Phase 5 wires it.
|
|
rendered_script: str | None = None
|
|
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
|