Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| 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) | |
| 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 | |
| def sanitize_comment(cls, comment: str): | |
| """Remove HTML tags to prevent XSS""" | |
| return nh3.clean(comment) | |
| 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) | |
| 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 | |
| ) | |
| 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"] | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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 | |