psypredict-backend / app /schemas.py
therandomuser03's picture
Add application file
f0f84fb
"""
schemas.py — PsyPredict Pydantic Data Models
All request/response bodies are validated via these schemas.
No unstructured dicts pass through the API layer.
"""
from __future__ import annotations
from typing import List, Optional, Any, Dict
from enum import Enum
from pydantic import BaseModel, Field, field_validator
import re
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class RiskLevel(str, Enum):
MINIMAL = "MINIMAL"
LOW = "LOW"
MODERATE = "MODERATE"
HIGH = "HIGH"
CRITICAL = "CRITICAL"
class MessageRole(str, Enum):
USER = "user"
ASSISTANT = "assistant"
# ---------------------------------------------------------------------------
# Shared Sub-models
# ---------------------------------------------------------------------------
class ConversationMessage(BaseModel):
role: MessageRole
content: str
class EmotionLabel(BaseModel):
label: str
score: float = Field(ge=0.0, le=1.0)
class CrisisResource(BaseModel):
name: str
contact: str
available: str = "24/7"
# ---------------------------------------------------------------------------
# PsychReport — Core Structured Output
# ---------------------------------------------------------------------------
class PsychReport(BaseModel):
"""
Structured psychological assessment output.
Produced by the LLM layer and validated against this schema.
"""
risk_classification: RiskLevel = Field(
description="Overall risk level based on text + multimodal fusion"
)
emotional_state_summary: str = Field(
description="Concise summary of detected emotional state (1-2 sentences)"
)
behavioral_inference: str = Field(
description="Inferred behavioral patterns from the conversation"
)
cognitive_distortions: List[str] = Field(
default_factory=list,
description="List of detected cognitive distortions (e.g. catastrophizing, black-and-white thinking)"
)
suggested_interventions: List[str] = Field(
default_factory=list,
description="Clinical-style intervention suggestions"
)
confidence_score: float = Field(
ge=0.0, le=1.0,
description="Aggregate confidence of this assessment (0.0–1.0)"
)
crisis_triggered: bool = Field(
default=False,
description="True if crisis override layer activated"
)
crisis_resources: Optional[List[CrisisResource]] = Field(
default=None,
description="Emergency resources, populated only when crisis_triggered=True"
)
service_degraded: bool = Field(
default=False,
description="True if Ollama was unreachable and fallback was used"
)
# ---------------------------------------------------------------------------
# Fallback Report (used when Ollama is unavailable)
# ---------------------------------------------------------------------------
def fallback_report() -> PsychReport:
return PsychReport(
risk_classification=RiskLevel.MINIMAL,
emotional_state_summary="Assessment unavailable — inference service is currently offline.",
behavioral_inference="Unable to infer behavioral patterns at this time.",
cognitive_distortions=[],
suggested_interventions=["Please try again shortly."],
confidence_score=0.0,
crisis_triggered=False,
service_degraded=True,
)
# ---------------------------------------------------------------------------
# Remedy Endpoint (must be defined BEFORE ChatResponse which references it)
# ---------------------------------------------------------------------------
class RemedyResponse(BaseModel):
condition: str
symptoms: str
treatments: str
medications: str
dosage: str
gita_remedy: str
# ---------------------------------------------------------------------------
# Facial / Emotion Endpoint
# ---------------------------------------------------------------------------
class EmotionResponse(BaseModel):
emotion: Optional[str] = None
confidence: Optional[float] = None
face_box: Optional[List[int]] = None
message: Optional[str] = None
error: Optional[str] = None
# ---------------------------------------------------------------------------
# Chat Endpoint
# ---------------------------------------------------------------------------
class ChatRequest(BaseModel):
message: str = Field(min_length=1, max_length=2000)
emotion: Optional[str] = Field(default="neutral", description="Face emotion from webcam")
history: List[ConversationMessage] = Field(default_factory=list)
stream: bool = Field(default=False, description="Enable streaming response")
@field_validator("message")
@classmethod
def sanitize_message(cls, v: str) -> str:
# Strip HTML tags
v = re.sub(r"<[^>]+>", "", v)
# Collapse whitespace
v = " ".join(v.split())
return v.strip()
@field_validator("emotion")
@classmethod
def normalize_emotion(cls, v: str) -> str:
return v.lower().strip() if v else "neutral"
class ChatResponse(BaseModel):
response: str = Field(description="Conversational reply text")
report: PsychReport
text_emotion: Optional[List[EmotionLabel]] = None
fusion_risk_score: Optional[float] = None
remedy: Optional[RemedyResponse] = None # CSV-based remedy data, populated automatically
# ---------------------------------------------------------------------------
# Text Analysis Endpoint
# ---------------------------------------------------------------------------
class TextAnalysisRequest(BaseModel):
text: str = Field(min_length=1, max_length=2000)
@field_validator("text")
@classmethod
def sanitize_text(cls, v: str) -> str:
v = re.sub(r"<[^>]+>", "", v)
return " ".join(v.split()).strip()
class TextAnalysisResponse(BaseModel):
emotions: List[EmotionLabel]
dominant: str
crisis_risk: float = Field(ge=0.0, le=1.0)
crisis_triggered: bool
# ---------------------------------------------------------------------------
# Health Endpoint
# ---------------------------------------------------------------------------
class HealthResponse(BaseModel):
status: str
ollama_reachable: bool
ollama_model: str
distilbert_loaded: bool
version: str = "2.0.0"