Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 9,294 Bytes
cbfe36d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | import nh3
from constants import (
MAX_COMMENT_LENGTH,
MAX_FILE_NAME_LENGTH,
MAX_ID_LENGTH,
MAX_MESSAGE_LENGTH,
MAX_RESPONSE_LENGTH,
)
from pydantic import BaseModel, Field, field_validator
from typing import Literal, Set
from uuid import UUID
class IdentifierBase(BaseModel):
user_id: str = Field(
pattern="^[a-zA-Z0-9_-]+$", min_length=1, max_length=MAX_ID_LENGTH
)
# Participant ID could be in ProfileBase instead. It doesn't really matter.
participant_id: str = Field(
pattern="^[a-zA-Z0-9_-]+$", min_length=1, max_length=MAX_ID_LENGTH
)
session_id: str = Field(
pattern="^[a-zA-Z0-9_-]+$", min_length=1, max_length=MAX_ID_LENGTH
)
class ProfileBase(BaseModel):
consent: bool
age_group: Literal["0-18", "18-24", "25-34", "35-44", "45-54", "55-64", "65+"]
gender: Literal["M", "F"]
roles: Set[
Literal["patient", "clinician", "computer-scientist", "researcher", "other"]
] = Field(min_length=1, max_length=5)
class ChatRequest(IdentifierBase, ProfileBase):
conversation_id: str = Field(
pattern="^[a-zA-Z0-9_-]+$", min_length=1, max_length=MAX_ID_LENGTH
)
model_type: Literal[
"champ",
"openai",
"google-conservative",
"google-creative",
"qwen",
"fake",
"skills_wiki",
"skills_wiki_short",
]
lang: Literal["en", "fr"]
human_message: str = Field(min_length=1, max_length=MAX_MESSAGE_LENGTH)
@field_validator("human_message")
def sanitize_human_message(cls, human_message: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(human_message)
class FeedbackRequest(IdentifierBase, ProfileBase):
message_index: int = Field(ge=0, le=10_000)
rating: Literal["like", "dislike", "mixed"]
comment: str = Field(min_length=0, max_length=MAX_COMMENT_LENGTH)
reply_content: str = Field(min_length=1, max_length=MAX_RESPONSE_LENGTH)
reply_id: UUID
@field_validator("comment")
def sanitize_comment(cls, comment: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(comment)
@field_validator("reply_content")
def sanitize_reply_content(cls, reply_content: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(reply_content)
class CommentRequest(IdentifierBase, ProfileBase):
comment: str = Field(min_length=1, max_length=MAX_COMMENT_LENGTH)
@field_validator("comment")
def sanitize_comment(cls, comment: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(comment)
class DeleteFileRequest(IdentifierBase, ProfileBase):
file_name: str = Field(
# Pattern: Allows letters, numbers, -, _, spaces, and dots (but no double dots or starting dots or spaces)
pattern=r"^[a-zA-Z0-9_()-][a-zA-Z0-9\s_()-]*(\.[a-zA-Z0-9\s_-]+)*$",
min_length=1,
max_length=MAX_FILE_NAME_LENGTH,
)
class ReviewRequestBase(BaseModel):
"""Requests from the critique (/review) and evaluation (/evaluate) pages.
These pages have no consent/profile flow, so they only carry the
machine-generated user and session identifiers.
"""
user_id: str = Field(
pattern="^[a-zA-Z0-9_-]+$", min_length=1, max_length=MAX_ID_LENGTH
)
session_id: str = Field(
pattern="^[a-zA-Z0-9_-]+$", min_length=1, max_length=MAX_ID_LENGTH
)
@field_validator("user_id", mode="before")
def strip_user_id(cls, user_id: str):
"""Trim whitespace before the pattern check, so a stray leading/
trailing space doesn't turn a persona login into a mismatched id."""
return user_id.strip() if isinstance(user_id, str) else user_id
class ReviewAskRequest(ReviewRequestBase):
"""Ask ChatGPT the selected question."""
question_id: int = Field(ge=1, le=12709)
class CritiqueHighlight(BaseModel):
"""One highlighted excerpt of the answer, rated good/bad/mixed.
The rating alone is meaningful, so the comment may be empty.
"""
highlighted_text: str = Field(min_length=1, max_length=MAX_RESPONSE_LENGTH)
comment: str = Field(min_length=0, max_length=MAX_COMMENT_LENGTH)
rating: Literal["good", "bad", "mixed"]
@field_validator("highlighted_text", "comment")
def sanitize(cls, value: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(value)
class CritiqueSubmitRequest(ReviewRequestBase):
"""A complete critique of one LLM answer, saved as a single record."""
question_id: int = Field(ge=1, le=12709)
answer_id: UUID
answer: str = Field(min_length=1, max_length=MAX_RESPONSE_LENGTH)
# The reviewer's grade of the LLM answer, out of 5.
answer_grade: int = Field(ge=1, le=5)
general_comment: str = Field(min_length=0, max_length=MAX_COMMENT_LENGTH)
highlights: list[CritiqueHighlight] = Field(default_factory=list, max_length=200)
@field_validator("answer", "general_comment")
def sanitize(cls, value: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(value)
class EvaluationSubmitRequest(ReviewRequestBase):
"""A supervisor's evaluation of a submitted critique."""
critique_id: UUID
# The supervisor's grade of the critique, out of 5.
critique_grade: int = Field(ge=1, le=5)
comment: str = Field(min_length=0, max_length=MAX_COMMENT_LENGTH)
@field_validator("comment")
def sanitize_comment(cls, comment: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(comment)
class TraineeAnswerRequest(ReviewRequestBase):
"""A trainee's free-text answer to a practice scenario, plus confidence."""
question_id: int = Field(ge=1, le=12709)
answer: str = Field(min_length=1, max_length=MAX_RESPONSE_LENGTH)
# How confident the trainee is in their answer, 1 (low) to 5 (high).
confidence: int = Field(ge=1, le=5)
@field_validator("answer")
def sanitize_answer(cls, answer: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(answer)
class TraineeGuidelineAnswerRequest(ReviewRequestBase):
"""A trainee's free-text answer to a guideline question, plus confidence."""
# Guideline question ids are strings namespaced by document,
# e.g. "anaphylaxis-3" (see guideline_questions.py).
question_id: str = Field(min_length=1, max_length=64)
answer: str = Field(min_length=1, max_length=MAX_RESPONSE_LENGTH)
# How confident the trainee is in their answer, 1 (low) to 5 (high).
confidence: int = Field(ge=1, le=5)
@field_validator("answer")
def sanitize_answer(cls, answer: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(answer)
class TraineeFeedbackReviewRequest(ReviewRequestBase):
"""A trainee's review of the model feedback (highlights + general comment)."""
# int for the CSV bank, str for guideline questions ("anaphylaxis-3").
question_id: int | str
feedback_id: UUID
# Whether the trainee agrees with the LLM feedback. Required — the client
# blocks submission until answered, and this enforces it server-side too.
agree: bool
general_comment: str = Field(min_length=0, max_length=MAX_COMMENT_LENGTH)
highlights: list[CritiqueHighlight] = Field(default_factory=list, max_length=200)
@field_validator("general_comment")
def sanitize_general_comment(cls, general_comment: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(general_comment)
class TraineeCommentRequest(ReviewRequestBase):
"""A free-form comment left from the trainee footer (about anything)."""
comment: str = Field(min_length=1, max_length=MAX_COMMENT_LENGTH)
@field_validator("comment")
def sanitize_comment(cls, comment: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(comment)
class SupervisorReviewRequest(BaseModel):
"""A supervisor's evaluation of one trainee attempt's LLM feedback.
Not a ReviewRequestBase: there's no trainee session here, just the
supervisor's own (unauthenticated, demo-only) identity.
"""
supervisor_id: str = Field(
pattern="^[a-zA-Z0-9_-]+$", min_length=1, max_length=MAX_ID_LENGTH
)
feedback_id: UUID
# The supervisor's grade of the LLM feedback, out of 5.
feedback_grade: int = Field(ge=1, le=5)
# The supervisor's grade of the trainee's answer itself, out of 5.
answer_grade: int = Field(ge=1, le=5)
# Comment on the LLM feedback's quality.
feedback_comment: str = Field(min_length=0, max_length=MAX_COMMENT_LENGTH)
# Comment on the trainee's answer itself.
answer_comment: str = Field(min_length=0, max_length=MAX_COMMENT_LENGTH)
@field_validator("feedback_comment", "answer_comment")
def sanitize_comment(cls, comment: str):
"""Remove HTML tags to prevent XSS"""
return nh3.clean(comment)
class ClearConversationRequest(BaseModel):
old_session_id: str = Field(
pattern="^[a-zA-Z0-9_-]+$", min_length=1, max_length=MAX_ID_LENGTH
)
new_session_id: str = Field(
pattern="^[a-zA-Z0-9_-]+$", min_length=1, max_length=MAX_ID_LENGTH
)
class ChatMessage(BaseModel):
role: Literal["user", "assistant", "system"]
content: str
|