| """ |
| Database models and initialization for LLM Deployment System. |
| |
| This module defines the database schema using SQLAlchemy and provides |
| utilities for database operations. |
| """ |
|
|
| from datetime import datetime, timezone |
| from typing import List, Optional, Dict, Any |
| import json |
| import uuid |
|
|
| from sqlalchemy import ( |
| create_engine, Column, Integer, String, Text, DateTime, |
| Boolean, Float, ForeignKey, JSON, Enum, UniqueConstraint |
| ) |
| from sqlalchemy.ext.declarative import declarative_base |
| from sqlalchemy.orm import sessionmaker, relationship, Session |
| from sqlalchemy.dialects.postgresql import UUID as PGUUID |
|
|
| from utils.config import config |
| from utils.logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
| Base = declarative_base() |
|
|
|
|
| class TaskStatus(str, Enum): |
| """Enumeration of task statuses.""" |
| PENDING = "pending" |
| SENT = "sent" |
| RECEIVED = "received" |
| EVALUATING = "evaluating" |
| COMPLETED = "completed" |
| FAILED = "failed" |
|
|
|
|
| class EvaluationStatus(str, Enum): |
| """Enumeration of evaluation statuses.""" |
| PENDING = "pending" |
| RUNNING = "running" |
| PASSED = "passed" |
| FAILED = "failed" |
| ERROR = "error" |
|
|
|
|
| class Submission(Base): |
| """Model for storing student submissions and API endpoints.""" |
|
|
| __tablename__ = "submissions" |
|
|
| id = Column(Integer, primary_key=True, index=True) |
| email = Column(String(255), nullable=False, index=True) |
| endpoint = Column(String(500), nullable=False) |
| secret = Column(String(255), nullable=False) |
| github_username = Column(String(255)) |
| github_repo_url = Column(String(500)) |
| created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) |
| updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) |
|
|
| |
| tasks = relationship("Task", back_populates="submission", cascade="all, delete-orphan") |
|
|
| __table_args__ = ( |
| UniqueConstraint('email', 'endpoint', name='unique_submission'), |
| ) |
|
|
|
|
| class Task(Base): |
| """Model for storing tasks sent to students.""" |
|
|
| __tablename__ = "tasks" |
|
|
| id = Column(Integer, primary_key=True, index=True) |
| submission_id = Column(Integer, ForeignKey("submissions.id"), nullable=False) |
|
|
| |
| task_id = Column(String(255), nullable=False, unique=True, index=True) |
| round = Column(Integer, nullable=False) |
| nonce = Column(String(255), nullable=False, unique=True) |
|
|
| |
| brief = Column(Text, nullable=False) |
| checks = Column(JSON, nullable=False) |
| attachments = Column(JSON, default=list) |
|
|
| |
| status = Column(String(50), default="pending") |
| sent_at = Column(DateTime) |
| received_at = Column(DateTime) |
|
|
| |
| status_code = Column(Integer) |
| error_message = Column(Text) |
|
|
| created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) |
| updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) |
|
|
| |
| submission = relationship("Submission", back_populates="tasks") |
| repos = relationship("Repository", back_populates="task", cascade="all, delete-orphan") |
|
|
|
|
| class Repository(Base): |
| """Model for storing repository information submitted by students.""" |
|
|
| __tablename__ = "repositories" |
|
|
| id = Column(Integer, primary_key=True, index=True) |
| task_id = Column(Integer, ForeignKey("tasks.id"), nullable=False) |
|
|
| |
| repo_url = Column(String(500), nullable=False) |
| commit_sha = Column(String(40), nullable=False) |
| pages_url = Column(String(500)) |
|
|
| |
| submitted_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) |
|
|
| |
| task = relationship("Task", back_populates="repos") |
| evaluations = relationship("Evaluation", back_populates="repository", cascade="all, delete-orphan") |
|
|
|
|
| class Evaluation(Base): |
| """Model for storing evaluation results.""" |
|
|
| __tablename__ = "evaluations" |
|
|
| id = Column(Integer, primary_key=True, index=True) |
| repository_id = Column(Integer, ForeignKey("repositories.id"), nullable=False) |
|
|
| |
| check_name = Column(String(255), nullable=False) |
| status = Column(String(50), nullable=False) |
| score = Column(Float) |
| reason = Column(Text) |
| logs = Column(JSON) |
|
|
| |
| evaluated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) |
| duration_seconds = Column(Float) |
|
|
| |
| repository = relationship("Repository", back_populates="evaluations") |
|
|
|
|
| class TaskTemplate(Base): |
| """Model for storing task templates used to generate tasks.""" |
|
|
| __tablename__ = "task_templates" |
|
|
| id = Column(Integer, primary_key=True, index=True) |
| template_id = Column(String(100), nullable=False, unique=True, index=True) |
|
|
| |
| name = Column(String(255), nullable=False) |
| description = Column(Text) |
|
|
| |
| brief_template = Column(Text, nullable=False) |
| checks_template = Column(JSON, nullable=False) |
| attachments_template = Column(JSON, default=list) |
|
|
| |
| round2_brief_template = Column(Text) |
| round2_checks_template = Column(JSON) |
| round2_attachments_template = Column(JSON) |
|
|
| |
| is_active = Column(Boolean, default=True) |
| created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) |
| updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) |
|
|
| |
| seed_config = Column(JSON, default=dict) |
|
|
|
|
| class SystemConfig(Base): |
| """Model for storing system-wide configuration.""" |
|
|
| __tablename__ = "system_config" |
|
|
| id = Column(Integer, primary_key=True) |
| key = Column(String(255), nullable=False, unique=True, index=True) |
| value = Column(JSON, nullable=False) |
| description = Column(Text) |
| updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) |
|
|
|
|
| class DatabaseManager: |
| """Database manager class for handling connections and operations.""" |
|
|
| def __init__(self): |
| self.engine = None |
| self.SessionLocal = None |
| self._setup_database() |
|
|
| def _setup_database(self): |
| """Setup database connection and create tables.""" |
| try: |
| |
| self.engine = create_engine( |
| config.get_database_url(), |
| connect_args={"check_same_thread": False} if "sqlite" in config.DATABASE_URL else {}, |
| pool_pre_ping=True, |
| echo=config.FLASK_DEBUG |
| ) |
|
|
| |
| Base.metadata.create_all(bind=self.engine) |
|
|
| |
| self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine) |
|
|
| logger.info(f"Database initialized: {config.DATABASE_URL}") |
|
|
| except Exception as e: |
| logger.error(f"Failed to initialize database: {e}") |
| raise |
|
|
| def get_session(self) -> Session: |
| """Get a database session.""" |
| return self.SessionLocal() |
|
|
| def create_submission(self, email: str, endpoint: str, secret: str, **kwargs) -> Submission: |
| """Create a new submission record.""" |
| with self.get_session() as session: |
| submission = Submission( |
| email=email, |
| endpoint=endpoint, |
| secret=secret, |
| **kwargs |
| ) |
| session.add(submission) |
| session.commit() |
| session.refresh(submission) |
| return submission |
|
|
| def get_submission_by_email(self, email: str) -> Optional[Submission]: |
| """Get submission by email.""" |
| with self.get_session() as session: |
| return session.query(Submission).filter(Submission.email == email).first() |
|
|
| def create_task(self, submission_id: int, task_data: Dict[str, Any]) -> Task: |
| """Create a new task record.""" |
| with self.get_session() as session: |
| task = Task(submission_id=submission_id, **task_data) |
| session.add(task) |
| session.commit() |
| session.refresh(task) |
| return task |
|
|
| def get_task_by_id(self, task_id: str) -> Optional[Task]: |
| """Get task by task_id.""" |
| with self.get_session() as session: |
| return session.query(Task).filter(Task.task_id == task_id).first() |
|
|
| def get_tasks_by_submission(self, submission_id: int, round: Optional[int] = None) -> List[Task]: |
| """Get tasks for a submission, optionally filtered by round.""" |
| with self.get_session() as session: |
| query = session.query(Task).filter(Task.submission_id == submission_id) |
| if round is not None: |
| query = query.filter(Task.round == round) |
| return query.all() |
|
|
| def update_task_status(self, task_id: str, status: str, **kwargs) -> bool: |
| """Update task status and other fields.""" |
| with self.get_session() as session: |
| task = session.query(Task).filter(Task.task_id == task_id).first() |
| if not task: |
| return False |
|
|
| task.status = status |
| if 'status_code' in kwargs: |
| task.status_code = kwargs['status_code'] |
| if 'error_message' in kwargs: |
| task.error_message = kwargs['error_message'] |
| if status == "sent" and not task.sent_at: |
| task.sent_at = datetime.now(timezone.utc) |
| if status == "received" and not task.received_at: |
| task.received_at = datetime.now(timezone.utc) |
|
|
| session.commit() |
| return True |
|
|
| def create_repository(self, task_id: int, repo_data: Dict[str, Any]) -> Repository: |
| """Create a new repository record.""" |
| with self.get_session() as session: |
| repo = Repository(task_id=task_id, **repo_data) |
| session.add(repo) |
| session.commit() |
| session.refresh(repo) |
| return repo |
|
|
| def get_repositories_by_task(self, task_id: str) -> List[Repository]: |
| """Get repositories for a task.""" |
| with self.get_session() as session: |
| task = session.query(Task).filter(Task.task_id == task_id).first() |
| if not task: |
| return [] |
| return task.repos |
|
|
| def add_evaluation(self, repository_id: int, evaluation_data: Dict[str, Any]) -> Evaluation: |
| """Add an evaluation result.""" |
| with self.get_session() as session: |
| evaluation = Evaluation(repository_id=repository_id, **evaluation_data) |
| session.add(evaluation) |
| session.commit() |
| session.refresh(evaluation) |
| return evaluation |
|
|
| def get_evaluations_by_repository(self, repository_id: int) -> List[Evaluation]: |
| """Get evaluations for a repository.""" |
| with self.get_session() as session: |
| return session.query(Evaluation).filter(Evaluation.repository_id == repository_id).all() |
|
|
| def get_task_templates(self) -> List[TaskTemplate]: |
| """Get all active task templates.""" |
| with self.get_session() as session: |
| return session.query(TaskTemplate).filter(TaskTemplate.is_active == True).all() |
|
|
| def create_task_template(self, template_data: Dict[str, Any]) -> TaskTemplate: |
| """Create a new task template.""" |
| with self.get_session() as session: |
| template = TaskTemplate(**template_data) |
| session.add(template) |
| session.commit() |
| session.refresh(template) |
| return template |
|
|
|
|
| |
| db_manager = DatabaseManager() |
|
|
|
|
| def get_db() -> DatabaseManager: |
| """Get the global database manager instance.""" |
| return db_manager |
|
|
|
|
| def init_database(): |
| """Initialize the database with default data.""" |
| logger.info("Initializing database with default data...") |
|
|
| |
| templates_data = [ |
| { |
| 'template_id': 'sum-of-sales', |
| 'name': 'Sales Summary Application', |
| 'description': 'Create an app that processes CSV data and displays sales summaries', |
| 'brief_template': 'Publish a single-page site that fetches data.csv from attachments, sums its sales column, sets the title to "Sales Summary {seed}", displays the total inside #total-sales, and loads Bootstrap 5 from jsdelivr.', |
| 'checks_template': [ |
| 'js: document.title === `Sales Summary {seed}`', |
| 'js: !!document.querySelector("link[href*=\'bootstrap\']")', |
| 'js: Math.abs(parseFloat(document.querySelector("#total-sales").textContent) - {result}) < 0.01' |
| ], |
| 'attachments_template': [ |
| { |
| 'name': 'data.csv', |
| 'url': 'data:text/csv;base64,{seed}' |
| } |
| ], |
| 'round2_brief_template': 'Add a Bootstrap table #product-sales that lists each product with its total sales and keeps #total-sales accurate after render.', |
| 'round2_checks_template': [ |
| 'js: document.querySelectorAll("#product-sales tbody tr").length >= 1', |
| 'js: (() => { const rows = [...document.querySelectorAll("#product-sales tbody tr td:last-child")]; const sum = rows.reduce((acc, cell) => acc + parseFloat(cell.textContent), 0); return Math.abs(sum - {result}) < 0.01; })()' |
| ] |
| }, |
| { |
| 'template_id': 'markdown-to-html', |
| 'name': 'Markdown to HTML Converter', |
| 'description': 'Create an app that converts Markdown to HTML with syntax highlighting', |
| 'brief_template': 'Publish a static page that converts input.md from attachments to HTML with marked, renders it inside #markdown-output, and loads highlight.js for code blocks.', |
| 'checks_template': [ |
| 'js: !!document.querySelector("script[src*=\'marked\']")', |
| 'js: !!document.querySelector("script[src*=\'highlight.js\']")', |
| 'js: document.querySelector("#markdown-output").innerHTML.includes("<h")' |
| ], |
| 'attachments_template': [ |
| { |
| 'name': 'input.md', |
| 'url': 'data:text/markdown;base64,{seed}' |
| } |
| ], |
| 'round2_brief_template': 'Add tabs #markdown-tabs that switch between rendered HTML in #markdown-output and the original Markdown in #markdown-source while keeping content in sync.', |
| 'round2_checks_template': [ |
| 'js: document.querySelectorAll("#markdown-tabs button").length >= 2', |
| 'js: document.querySelector("#markdown-source").textContent.trim().length > 0' |
| ] |
| }, |
| { |
| 'template_id': 'github-user-created', |
| 'name': 'GitHub User Information', |
| 'description': 'Create an app that fetches and displays GitHub user information', |
| 'brief_template': 'Publish a Bootstrap page with form id="github-user-{seed}" that fetches a GitHub username, optionally uses ?token=, and displays the account creation date in YYYY-MM-DD UTC inside #github-created-at.', |
| 'checks_template': [ |
| 'js: document.querySelector("#github-user-{seed}").tagName === "FORM"', |
| 'js: document.querySelector("#github-created-at").textContent.includes("20")', |
| 'js: !!document.querySelector("script").textContent.includes("https://api.github.com/users/")' |
| ], |
| 'round2_brief_template': 'Show an aria-live alert #github-status that reports when a lookup starts, succeeds, or fails.', |
| 'round2_checks_template': [ |
| 'js: document.querySelector("#github-status").getAttribute("aria-live") === "polite"', |
| 'js: !!document.querySelector("script").textContent.includes("github-status")' |
| ] |
| } |
| ] |
|
|
| with db_manager.get_session() as session: |
| for template_data in templates_data: |
| |
| existing = session.query(TaskTemplate).filter( |
| TaskTemplate.template_id == template_data['template_id'] |
| ).first() |
|
|
| if not existing: |
| template = TaskTemplate(**template_data) |
| session.add(template) |
| logger.info(f"Created task template: {template_data['template_id']}") |
|
|
| session.commit() |
|
|
| logger.info("Database initialization completed") |
|
|