Files
chihlasm f16a686fb4 feat: add onboarding, branding, and supporting data models, migrations, and schemas
Add onboarding_dismissed and branding columns (logo_data, logo_content_type,
company_display_name) to users and teams models. Create SessionSupportingData
model for attaching text snippets and screenshots to sessions. Add Pydantic
schemas for onboarding status, branding responses, and supporting data CRUD.
Update SessionExport to accept pdf format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 23:51:42 -04:00

183 lines
6.2 KiB
Python

from datetime import datetime
from typing import Optional, Any, Literal
from uuid import UUID
from pydantic import BaseModel, Field, validator
SessionOutcome = Literal["resolved", "escalated", "workaround", "unresolved", "cancelled", "resolved_externally"]
class CustomStepSchema(BaseModel):
"""Enhanced custom step with source tracking.
Backward compatible: old sessions without new fields load with defaults.
"""
type: str # "decision" | "action" | "solution"
content: str
notes: Optional[str] = None
# Source tracking (new fields, optional for backward compatibility)
source: Literal["ad-hoc", "step-library", "forked-tree"] = "ad-hoc"
source_step_id: Optional[UUID] = None
inserted_at: Optional[datetime] = None
inserted_after_node_id: Optional[str] = None
class DecisionRecord(BaseModel):
node_id: str
question: Optional[str] = None
answer: Optional[str] = None
action_performed: Optional[str] = None
notes: Optional[str] = None
command_output: Optional[str] = Field(None, max_length=10000)
automation_used: Optional[bool] = False
timestamp: datetime
entered_at: Optional[datetime] = None
exited_at: Optional[datetime] = None
duration_seconds: Optional[int] = Field(None, ge=0)
attachments: list[str] = Field(default_factory=list)
class SessionCreate(BaseModel):
tree_id: UUID
ticket_number: Optional[str] = Field(None, max_length=100)
client_name: Optional[str] = Field(None, max_length=255)
session_variables: Optional[dict[str, str]] = Field(None, description="Intake form values for procedural flows")
class SessionUpdate(BaseModel):
path_taken: Optional[list[str]] = None
decisions: Optional[list[DecisionRecord]] = None
custom_steps: Optional[list[CustomStepSchema]] = None
ticket_number: Optional[str] = Field(None, max_length=100)
client_name: Optional[str] = Field(None, max_length=255)
scratchpad: Optional[str] = None
next_steps: Optional[str] = None
session_variables: Optional[dict[str, str]] = None
class PrepareSessionRequest(BaseModel):
"""Create a prepared session with pre-filled variables and optional assignee."""
tree_id: UUID
session_variables: Optional[dict[str, str]] = Field(None, description="Pre-filled intake form values")
assigned_to_id: Optional[UUID] = Field(None, description="User ID of the engineer to assign this session to")
ticket_number: Optional[str] = Field(None, max_length=100)
client_name: Optional[str] = Field(None, max_length=255)
class SessionResponse(BaseModel):
id: UUID
tree_id: UUID
user_id: UUID
tree_snapshot: dict[str, Any]
path_taken: list[str]
decisions: list[dict[str, Any]]
custom_steps: list[dict[str, Any]] = Field(default_factory=list)
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
outcome: Optional[SessionOutcome] = None
outcome_notes: Optional[str] = None
next_steps: str = ""
ticket_number: Optional[str] = None
client_name: Optional[str] = None
exported: bool
scratchpad: str = ""
session_variables: dict[str, str] = Field(default_factory=dict)
# Prepared session fields
prepared_by_id: Optional[UUID] = None
assigned_to_id: Optional[UUID] = None
@validator('scratchpad', 'next_steps', pre=True, always=True)
def normalize_text_fields(cls, v):
return v or ""
batch_id: Optional[UUID] = None
target_label: Optional[str] = None
# PSA ticket link
psa_ticket_id: Optional[str] = None
psa_connection_id: Optional[UUID] = None
# Fallback step decisions
fallback_decisions: list[dict[str, Any]] = Field(default_factory=list)
class Config:
from_attributes = True
class SessionExport(BaseModel):
format: str = Field(default="markdown", pattern="^(text|markdown|html|psa|pdf)$")
include_timestamps: bool = True
include_tree_info: bool = True
# Phase A
include_outcome_notes: bool = True
include_next_steps: bool = True
max_step_index: Optional[int] = Field(None, ge=1, description="1-based inclusive step cutoff")
# Phase B
include_summary: bool = False
detail_level: Literal["standard", "full"] = "standard"
# Phase C
redaction_mode: Literal["none", "mask"] = "none"
class SessionComplete(BaseModel):
outcome: SessionOutcome
outcome_notes: Optional[str] = None
next_steps: Optional[str] = None
class FallbackStepRecord(BaseModel):
parent_step_id: str
fallback_step_id: str
completed_at: str | None = None
notes: str | None = None
outcome: Literal['resolved', 'not_resolved', 'skipped']
class SessionVariablesUpdate(BaseModel):
"""Partial update to session variables (dict merge)."""
variables: dict[str, str] = Field(..., description="Key-value pairs to merge into session_variables")
class ScratchpadUpdate(BaseModel):
scratchpad: str
class SaveAsTreeRequest(BaseModel):
"""Request to save a session as a tree."""
tree_name: Optional[str] = Field(None, max_length=255, description="Custom name for the saved tree (auto-generated if not provided)")
description: Optional[str] = Field(None, description="Description for the saved tree")
status: Literal["draft", "published"] = Field("draft", description="Status of the saved tree")
class SaveAsTreeResponse(BaseModel):
"""Response after saving a session as a tree."""
tree_id: UUID
tree_name: str
message: str
# ── PSA ticket link ──────────────────────────────────────────────────
class TicketLinkRequest(BaseModel):
"""Link or unlink a PSA ticket to a session."""
psa_ticket_id: Optional[str] = None # null to unlink
class PSATicketResponse(BaseModel):
"""PSA ticket details returned when linking."""
id: str
summary: str
company_name: Optional[str] = None
board_name: Optional[str] = None
status_name: Optional[str] = None
priority_name: Optional[str] = None
class TicketLinkResponse(BaseModel):
"""Response after linking/unlinking a ticket."""
session_id: str
psa_ticket_id: Optional[str] = None
ticket: Optional[PSATicketResponse] = None