Files
WiseGrade/backend/wisegrade/models.py
T
aerospaceboy ebe33392ca fix: improve import, evidence indexing and material-scoped grading
Preserve document revisions and evidence, harden indexing and export behavior, add explicit material selection and direct-folder import, validate model JSON schemas with safe error messages, and retain student selection while loading scoring data.
2026-10-14 13:52:56 +08:00

372 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=5, server_default="5")
revision = Column(Integer, nullable=False, default=1, server_default="1")
status = Column(String(40), default="ready")
__table_args__ = (UniqueConstraint("course_version_id", "project_id", "submission_file_id", "source_hash", "revision",
name="uq_knowledge_document_revision"),)
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)