Files
WiseGrade/backend/wisegrade/models.py
T

370 lines
16 KiB
Python

from datetime import datetime, timezone
from pgvector.sqlalchemy import Vector
from sqlalchemy import JSON, Boolean, Column, DateTime, Float, ForeignKey, Integer, LargeBinary, String, Text, UniqueConstraint
from sqlalchemy.types import TypeDecorator
from .db import Base
def now():
return datetime.now(timezone.utc)
class Embedding(TypeDecorator):
impl = JSON
cache_ok = True
def load_dialect_impl(self, dialect):
return dialect.type_descriptor(Vector(1024) if dialect.name == "postgresql" else JSON())
class Course(Base):
__tablename__ = "courses"
id = Column(Integer, primary_key=True)
name = Column(String(200), unique=True, nullable=False)
code = Column(String(100), unique=True, nullable=False)
created_at = Column(DateTime(timezone=True), default=now)
class CourseVersion(Base):
__tablename__ = "course_versions"
id = Column(Integer, primary_key=True)
course_id = Column(Integer, ForeignKey("courses.id"), nullable=False)
version = Column(Integer, nullable=False)
title = Column(String(200), nullable=False)
objectives = Column(JSON, default=list, nullable=False)
indicators = Column(JSON, default=list, nullable=False)
directions = Column(JSON, default=list, nullable=False)
prompts = Column(JSON, default=dict, nullable=False)
templates = Column(JSON, default=list, nullable=False)
published = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), default=now)
__table_args__ = (UniqueConstraint("course_id", "version"),)
class CourseObjective(Base):
__tablename__ = "course_objectives"
id = Column(Integer, primary_key=True)
course_version_id = Column(Integer, ForeignKey("course_versions.id"), nullable=False)
code = Column(String(50), nullable=False)
name = Column(String(300), nullable=False)
description = Column(Text, default="")
indicator_codes = Column(JSON, default=list)
__table_args__ = (UniqueConstraint("course_version_id", "code"),)
class GraduationIndicator(Base):
__tablename__ = "graduation_indicators"
id = Column(Integer, primary_key=True)
course_version_id = Column(Integer, ForeignKey("course_versions.id"), nullable=False)
code = Column(String(50), nullable=False)
description = Column(Text, nullable=False)
__table_args__ = (UniqueConstraint("course_version_id", "code"),)
class TopicDirection(Base):
__tablename__ = "topic_directions"
id = Column(Integer, primary_key=True)
course_version_id = Column(Integer, ForeignKey("course_versions.id"), nullable=False)
name = Column(String(200), nullable=False)
examples = Column(JSON, default=list)
__table_args__ = (UniqueConstraint("course_version_id", "name"),)
class RubricVersion(Base):
__tablename__ = "rubric_versions"
id = Column(Integer, primary_key=True)
course_version_id = Column(Integer, ForeignKey("course_versions.id"), nullable=False)
version = Column(Integer, nullable=False)
criteria = Column(JSON, default=list, nullable=False)
weights = Column(JSON, default=dict, nullable=False)
published = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), default=now)
__table_args__ = (UniqueConstraint("course_version_id", "version"),)
class RubricCriterion(Base):
__tablename__ = "rubric_criteria"
id = Column(Integer, primary_key=True)
rubric_version_id = Column(Integer, ForeignKey("rubric_versions.id"), nullable=False)
criterion_id = Column(String(100), nullable=False)
category = Column(String(50), nullable=False)
name = Column(String(300), nullable=False)
description = Column(Text, nullable=False)
max_score = Column(Float, nullable=False)
linked_objectives = Column(JSON, default=list)
linked_outcomes = Column(JSON, default=list)
required_evidence = Column(JSON, default=list)
__table_args__ = (UniqueConstraint("rubric_version_id", "criterion_id"),)
class RubricScoreLevel(Base):
__tablename__ = "rubric_score_levels"
id = Column(Integer, primary_key=True)
rubric_criterion_id = Column(Integer, ForeignKey("rubric_criteria.id"), nullable=False)
min_score = Column(Float, nullable=False)
max_score = Column(Float, nullable=False)
description = Column(Text, nullable=False)
class Student(Base):
__tablename__ = "students"
id = Column(Integer, primary_key=True)
student_no = Column(String(80), unique=True, nullable=False)
name = Column(String(100), nullable=False)
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True)
course_version_id = Column(Integer, ForeignKey("course_versions.id"), nullable=False)
student_id = Column(Integer, ForeignKey("students.id"), nullable=False)
topic = Column(String(500), nullable=False)
direction = Column(String(200), default="")
source_dir = Column(String(1000), nullable=False)
status = Column(String(40), default="imported")
facts = Column(JSON, default=dict, nullable=False)
created_at = Column(DateTime(timezone=True), default=now)
__table_args__ = (UniqueConstraint("course_version_id", "source_dir"),)
class Submission(Base):
__tablename__ = "submissions"
id = Column(Integer, primary_key=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
category = Column(String(50), nullable=False)
status = Column(String(40), nullable=False)
__table_args__ = (UniqueConstraint("project_id", "category"),)
class SubmissionFile(Base):
__tablename__ = "submission_files"
id = Column(Integer, primary_key=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
submission_id = Column(Integer, ForeignKey("submissions.id"), nullable=True)
category = Column(String(40), nullable=False)
relative_path = Column(String(1200), nullable=False)
mime_type = Column(String(100), nullable=False)
size = Column(Integer, nullable=False)
mtime = Column(Float, nullable=False)
sha256 = Column(String(64), nullable=False)
recognition_status = Column(String(40), nullable=False)
parse_status = Column(String(40), default="pending")
parse_error = Column(Text, default="")
parsed = Column(JSON, default=dict, nullable=False)
__table_args__ = (UniqueConstraint("project_id", "relative_path"),)
class KnowledgeDocument(Base):
__tablename__ = "knowledge_documents"
id = Column(Integer, primary_key=True)
course_version_id = Column(Integer, ForeignKey("course_versions.id"), nullable=False)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=True)
submission_file_id = Column(Integer, ForeignKey("submission_files.id"), nullable=True)
title = Column(String(500), nullable=False)
document_type = Column(String(80), nullable=False)
source_hash = Column(String(64), nullable=False)
index_version = Column(Integer, nullable=False, default=2, server_default="1")
status = Column(String(40), default="ready")
__table_args__ = (UniqueConstraint("course_version_id", "project_id", "submission_file_id", "source_hash"),)
class KnowledgePage(Base):
__tablename__ = "knowledge_pages"
document_id = Column(Integer, ForeignKey("knowledge_documents.id", ondelete="CASCADE"), primary_key=True)
page = Column(Integer, primary_key=True)
text = Column(Text, nullable=False, default="")
image_path = Column(String(1200), nullable=True) # Relative to processed_path.
# Page-level classification: cover, abstract, toc, body, references or unknown.
page_type = Column(String(20), nullable=False, default="unknown", server_default="unknown")
mixed = Column(Boolean, nullable=False, default=False, server_default="false")
section = Column(String(500), default="")
chapter = Column(String(500), default="")
confidence = Column(Float, nullable=True)
# Bounded classifier evidence: which signals decided the type, kept for audit.
classification = Column(JSON, default=dict, nullable=False)
class KnowledgeChunk(Base):
__tablename__ = "knowledge_chunks"
id = Column(Integer, primary_key=True)
document_id = Column(Integer, ForeignKey("knowledge_documents.id"), nullable=False)
course_version_id = Column(Integer, nullable=False)
project_id = Column(Integer, nullable=True)
document_type = Column(String(80), nullable=False)
page = Column(Integer, nullable=False)
section = Column(String(500), default="")
chapter = Column(String(500), default="")
page_type = Column(String(20), nullable=False, default="unknown", server_default="unknown")
block_type = Column(String(20), nullable=False, default="body", server_default="body")
text = Column(Text, nullable=False)
embedding = Column(Embedding, nullable=True)
class ScoringBatch(Base):
__tablename__ = "scoring_batches"
id = Column(Integer, primary_key=True)
course_version_id = Column(Integer, ForeignKey("course_versions.id"), nullable=False)
rubric_version_id = Column(Integer, ForeignKey("rubric_versions.id"), nullable=False)
course_snapshot = Column(JSON, nullable=False)
rubric_snapshot = Column(JSON, nullable=False)
model_config_snapshot = Column(JSON, nullable=False)
embedding_model_name = Column(String(200), nullable=False)
embedding_model_version = Column(String(100), nullable=False)
status = Column(String(40), default="created")
project_ids = Column(JSON, nullable=True) # NULL identifies a legacy batch with unknown original membership.
progress_done = Column(Integer, default=0)
progress_total = Column(Integer, default=0)
error = Column(Text, default="")
created_at = Column(DateTime(timezone=True), default=now)
finished_at = Column(DateTime(timezone=True), nullable=True)
class CriterionScore(Base):
__tablename__ = "criterion_scores"
id = Column(Integer, primary_key=True)
batch_id = Column(Integer, ForeignKey("scoring_batches.id"), nullable=False)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
criterion_id = Column(String(100), nullable=False)
model_score = Column(Float, nullable=True)
teacher_score = Column(Float, nullable=True)
model_comment = Column(Text, default="")
teacher_comment = Column(Text, nullable=True)
confidence = Column(Float, nullable=True)
missing_evidence = Column(JSON, default=list)
suggestion = Column(Text, default="")
status = Column(String(40), default="pending")
error = Column(Text, default="")
#: Content targets that still lacked body evidence, with the search that was run.
#: Kept after teacher review so the original retrieval problem stays auditable.
evidence_gaps = Column(JSON, default=list)
__table_args__ = (UniqueConstraint("batch_id", "project_id", "criterion_id"),)
@property
def final_score(self):
return self.teacher_score if self.teacher_score is not None else self.model_score
class ScoreEvidence(Base):
__tablename__ = "score_evidences"
id = Column(Integer, primary_key=True)
score_id = Column(Integer, ForeignKey("criterion_scores.id"), nullable=False)
document_id = Column(Integer, ForeignKey("knowledge_documents.id"), nullable=True)
submission_file_id = Column(Integer, ForeignKey("submission_files.id"), nullable=True)
page = Column(Integer, nullable=False)
quote = Column(Text, nullable=False)
image_path = Column(String(1200), nullable=True)
# Which required-evidence target this quotation is offered to support.
target_id = Column(String(50), default="", server_default="")
target_label = Column(String(300), default="", server_default="")
# Page type of the source passage, and its role in the judgement.
page_type = Column(String(20), default="unknown", server_default="unknown")
source_role = Column(String(20), default="content", server_default="content")
# True when the quotation was written before this retrieval policy existed.
historical = Column(Boolean, default=False, nullable=False, server_default="false")
class TeacherRevision(Base):
__tablename__ = "teacher_revisions"
id = Column(Integer, primary_key=True)
score_id = Column(Integer, ForeignKey("criterion_scores.id"), nullable=False)
old_score = Column(Float, nullable=True)
new_score = Column(Float, nullable=True)
old_comment = Column(Text, nullable=True)
new_comment = Column(Text, nullable=True)
reason = Column(Text, default="")
created_at = Column(DateTime(timezone=True), default=now)
class ExportTask(Base):
__tablename__ = "export_tasks"
id = Column(Integer, primary_key=True)
batch_id = Column(Integer, ForeignKey("scoring_batches.id"), nullable=False)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=True)
kind = Column(String(20), nullable=False)
path = Column(String(1200), nullable=True)
status = Column(String(40), default="queued")
error = Column(Text, default="")
created_at = Column(DateTime(timezone=True), default=now)
class AppSetting(Base):
__tablename__ = "app_settings"
key = Column(String(100), primary_key=True)
encrypted_value = Column(LargeBinary, nullable=False)
updated_at = Column(DateTime(timezone=True), default=now)
class AuditLog(Base):
__tablename__ = "audit_logs"
id = Column(Integer, primary_key=True)
action = Column(String(100), nullable=False)
entity_type = Column(String(100), nullable=False)
entity_id = Column(Integer, nullable=True)
details = Column(JSON, default=dict)
created_at = Column(DateTime(timezone=True), default=now)
class IndexTask(Base):
__tablename__ = "index_tasks"
id = Column(Integer, primary_key=True)
course_version_id = Column(Integer, ForeignKey("course_versions.id"), nullable=False)
submission_file_id = Column(Integer, ForeignKey("submission_files.id"), nullable=True)
kind = Column(String(30), nullable=False)
status = Column(String(30), nullable=False, default="queued")
result = Column(JSON, nullable=False, default=dict)
error = Column(Text, default="")
created_at = Column(DateTime(timezone=True), default=now)
updated_at = Column(DateTime(timezone=True), default=now, onupdate=now)
class Conversation(Base):
__tablename__ = "conversations"
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False, default="新会话")
first_request_id = Column(String(36), nullable=True, unique=True)
course_version_id = Column(Integer, ForeignKey("course_versions.id"), nullable=True)
batch_id = Column(Integer, ForeignKey("scoring_batches.id"), nullable=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=True)
created_at = Column(DateTime(timezone=True), default=now)
updated_at = Column(DateTime(timezone=True), default=now, onupdate=now)
class ChatMessage(Base):
__tablename__ = "chat_messages"
id = Column(Integer, primary_key=True)
conversation_id = Column(Integer, ForeignKey("conversations.id"), nullable=False)
role = Column(String(20), nullable=False)
content = Column(Text, nullable=False, default="")
citations = Column(JSON, nullable=False, default=list)
tool_calls = Column(JSON, nullable=False, default=list)
created_at = Column(DateTime(timezone=True), default=now)
class AgentRun(Base):
__tablename__ = "agent_runs"
id = Column(Integer, primary_key=True)
conversation_id = Column(Integer, ForeignKey("conversations.id"), nullable=False)
status = Column(String(30), nullable=False, default="running")
error = Column(Text, default="")
tool_calls = Column(JSON, nullable=False, default=list)
events = Column(JSON, nullable=False, default=list)
created_at = Column(DateTime(timezone=True), default=now)
finished_at = Column(DateTime(timezone=True), nullable=True)
class PendingAction(Base):
__tablename__ = "pending_actions"
id = Column(Integer, primary_key=True)
conversation_id = Column(Integer, ForeignKey("conversations.id"), nullable=False)
kind = Column(String(40), nullable=False)
payload = Column(JSON, nullable=False)
preview = Column(JSON, nullable=False)
expected = Column(JSON, nullable=False)
idempotency_key = Column(String(64), nullable=False, unique=True)
status = Column(String(30), nullable=False, default="pending")
result = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), default=now)
expires_at = Column(DateTime(timezone=True), nullable=False)