Files
resolutionflow/backend/app/schemas/step_library.py
chihlasm 7803dc4522 Add step library foundation and user preferences (#24)
## Summary
Implements Phase 2.5 Step Library Foundation:

### Issues Completed
- #3 User Preferences - export format default setting
- #5 Step Categories - database table and seed data  
- #6 Step Library - database schema and migrations
- #7 Step Library - CRUD API endpoints
- #8 Step Library - rating and review system

### Changes
**Backend:**
- Migration 007: step_categories table with 10 seeded global categories
- Migration 008: step_library, step_ratings, step_usage_log tables
- Full CRUD API for step categories (/api/v1/step-categories)
- Full CRUD API for step library (/api/v1/steps) with search, filters, ratings
- CORS support for Railway PR environments (ALLOW_RAILWAY_ORIGINS)

**Frontend:**
- User preferences store (Zustand + localStorage)
- Settings page at /settings with export format dropdown
- Default export format applied in SessionDetailPage

### Testing
- Tested in Railway PR environment
- Database seeded with 7 MSP troubleshooting trees
- All API endpoints verified working
2026-02-03 02:07:46 -05:00

136 lines
3.7 KiB
Python

from datetime import datetime
from decimal import Decimal
from typing import Optional, Literal
from uuid import UUID
from pydantic import BaseModel, Field
class StepCommand(BaseModel):
"""A command that can be run as part of a step."""
label: str
command: str
command_type: Optional[str] = None # e.g., 'powershell', 'cmd', 'bash'
class StepContent(BaseModel):
"""Content structure for step library entries (stored as JSONB)."""
instructions: str = Field(..., min_length=1)
help_text: Optional[str] = None
commands: Optional[list[StepCommand]] = None
# Base schemas
class StepLibraryBase(BaseModel):
title: str = Field(..., min_length=1, max_length=255)
step_type: Literal['decision', 'action', 'solution']
content: StepContent
category_id: Optional[UUID] = None
tags: list[str] = Field(default_factory=list)
visibility: Literal['private', 'team', 'public'] = 'private'
class StepLibraryCreate(StepLibraryBase):
team_id: Optional[UUID] = None
class StepLibraryUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=255)
step_type: Optional[Literal['decision', 'action', 'solution']] = None
content: Optional[StepContent] = None
category_id: Optional[UUID] = None
tags: Optional[list[str]] = None
visibility: Optional[Literal['private', 'team', 'public']] = None
class StepLibraryResponse(StepLibraryBase):
id: UUID
created_by: UUID
team_id: Optional[UUID] = None
usage_count: int
rating_average: Decimal
rating_count: int
helpful_yes: int
helpful_no: int
is_featured: bool
is_verified: bool
is_active: bool
created_at: datetime
updated_at: datetime
# Computed fields (populated by API)
category_name: Optional[str] = None
author_name: Optional[str] = None
class Config:
from_attributes = True
class StepLibraryListResponse(BaseModel):
id: UUID
title: str
step_type: str
visibility: str
category_id: Optional[UUID] = None
category_name: Optional[str] = None
tags: list[str]
usage_count: int
rating_average: Decimal
rating_count: int
is_featured: bool
created_by: UUID
author_name: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
# Rating schemas
class StepRatingBase(BaseModel):
rating: int = Field(..., ge=1, le=5)
was_helpful: Optional[bool] = None
review_text: Optional[str] = Field(None, max_length=500)
class StepRatingCreate(StepRatingBase):
session_id: Optional[UUID] = None # For verified use tracking
class StepRatingUpdate(BaseModel):
rating: Optional[int] = Field(None, ge=1, le=5)
was_helpful: Optional[bool] = None
review_text: Optional[str] = Field(None, max_length=500)
class StepRatingResponse(StepRatingBase):
id: UUID
step_id: UUID
user_id: UUID
is_verified_use: bool
session_id: Optional[UUID] = None
is_visible: bool
created_at: datetime
updated_at: datetime
# Computed
user_name: Optional[str] = None
class Config:
from_attributes = True
# Search and filter schemas
class StepSearchParams(BaseModel):
q: Optional[str] = None # Full-text search query
category_id: Optional[UUID] = None
tags: Optional[list[str]] = None
min_rating: Optional[float] = Field(None, ge=0, le=5)
step_type: Optional[Literal['decision', 'action', 'solution']] = None
visibility: Optional[Literal['private', 'team', 'public']] = None
sort_by: Literal['recent', 'popular', 'highest_rated', 'most_used'] = 'recent'
limit: int = Field(20, ge=1, le=100)
offset: int = Field(0, ge=0)
class PopularTagResponse(BaseModel):
tag: str
count: int