Files
resolutionflow/backend/app/schemas/session.py
Michael Chihlas 416bb230e3 feat: close sessions from history page with inline popover
Add ability to close active sessions directly from the Session History
page via an inline popover with outcome selection and optional notes.
Adds two new outcomes: cancelled and resolved_externally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 01:59:12 -04:00

143 lines
5.0 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
class Config:
from_attributes = True
class SessionExport(BaseModel):
format: str = Field(default="markdown", pattern="^(text|markdown|html|psa)$")
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 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