diff --git a/src/gened/base.py b/src/gened/base.py index 90120f4..4038c04 100644 --- a/src/gened/base.py +++ b/src/gened/base.py @@ -22,6 +22,7 @@ db, demo, docs, + experiments, # noqa: F401 (import registers routes even though unused here) filters, instructor, lti, diff --git a/src/gened/experiments.py b/src/gened/experiments.py new file mode 100644 index 0000000..9c6ba4b --- /dev/null +++ b/src/gened/experiments.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: 2024 Mark Liffiton +# +# SPDX-License-Identifier: AGPL-3.0-only + +from flask import ( + flash, + redirect, + render_template, + request, + url_for, +) +from werkzeug.wrappers.response import Response + +from .admin import bp as bp_admin +from .admin import register_admin_link +from .db import get_db + +# ### Admin routes ### +# Auth requirements covered by admin.before_request() + +@register_admin_link("Experiments") +@bp_admin.route("/experiments/") +def experiments_view() -> str: + db = get_db() + experiments = db.execute("SELECT *, (SELECT COUNT(*) FROM experiment_class WHERE experiment_id=id) AS count FROM experiments").fetchall() + return render_template("admin_experiments.html", experiments=experiments) + +@bp_admin.route("/experiment/new") +def experiment_new() -> str: + return render_template("experiment_form.html") + +@bp_admin.route("/experiment/") +def experiment_form(id: int) -> str: + db = get_db() + experiment = db.execute("SELECT * FROM experiments WHERE id=?", [id]).fetchone() + classes = db.execute("SELECT id, name FROM classes ORDER BY name").fetchall() + classes = [dict(row) for row in classes] # so we can tojson it in the template + assigned_classes = db.execute("SELECT class_id AS id, classes.name FROM experiment_class JOIN classes ON experiment_class.class_id=classes.id WHERE experiment_id=? ORDER BY name", [id]).fetchall() + assigned_classes = [dict(row) for row in assigned_classes] + return render_template("experiment_form.html", experiment=experiment, classes=classes, assigned_classes=assigned_classes) + +@bp_admin.route("/experiment/update", methods=['POST']) +def experiment_update() -> Response: + db = get_db() + + exp_id = request.form.get("exp_id", type=int) + + if exp_id is None: + # Adding a new experiment + cur = db.execute("INSERT INTO experiments (name, description) VALUES (?, ?)", + [request.form['name'], request.form['description']]) + exp_id = cur.lastrowid + db.commit() + flash(f"Experiment {request.form['name']} created.") + else: + # Updating + db.execute("UPDATE experiments SET name=?, description=? WHERE id=?", + [request.form['name'], request.form['description'], exp_id]) + db.commit() + flash("Experiment updated.") + + # Update assigned classes + db.execute("DELETE FROM experiment_class WHERE experiment_id=?", [exp_id]) + for class_id in request.form.getlist('assigned_classes'): + db.execute("INSERT INTO experiment_class (experiment_id, class_id) VALUES (?, ?)", + [exp_id, class_id]) + db.commit() + + return redirect(url_for(".experiment_form", id=exp_id)) + +@bp_admin.route("/experiment/delete/", methods=['POST']) +def experiment_delete(exp_id: int) -> Response: + db = get_db() + + # Delete the experiment and its class assignments + db.execute("DELETE FROM experiment_class WHERE experiment_id=?", [exp_id]) + db.execute("DELETE FROM experiments WHERE id=?", [exp_id]) + db.commit() + + flash("Experiment deleted.") + + return redirect(url_for(".experiments_view")) diff --git a/src/gened/migrations/20240624--add_experiment_tables.sql b/src/gened/migrations/20240624--add_experiment_tables.sql new file mode 100644 index 0000000..53d5f7c --- /dev/null +++ b/src/gened/migrations/20240624--add_experiment_tables.sql @@ -0,0 +1,27 @@ +-- SPDX-FileCopyrightText: 2024 Mark Liffiton +-- +-- SPDX-License-Identifier: AGPL-3.0-only + +BEGIN; + +-- Create experiments table +CREATE TABLE experiments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT +); + +-- Create experiment_class table +CREATE TABLE experiment_class ( + experiment_id INTEGER NOT NULL, + class_id INTEGER NOT NULL, + PRIMARY KEY (experiment_id, class_id), + FOREIGN KEY (experiment_id) REFERENCES experiments (id), + FOREIGN KEY (class_id) REFERENCES classes (id) +); + +-- Create indexes for experiment_class table +CREATE INDEX exp_crs_experiment_idx ON experiment_class(experiment_id); +CREATE INDEX exp_crs_class_idx ON experiment_class(class_id); + +COMMIT; diff --git a/src/gened/schema_common.sql b/src/gened/schema_common.sql index bfe56ea..633d533 100644 --- a/src/gened/schema_common.sql +++ b/src/gened/schema_common.sql @@ -157,10 +157,29 @@ CREATE TABLE models ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, shortname TEXT NOT NULL UNIQUE, - model TEXT + model TEXT NOT NULL ); INSERT INTO models(name, shortname, model) VALUES ('OpenAI GPT-3.5 Turbo', 'GPT-3.5', 'gpt-3.5-turbo-0125'), ('OpenAI GPT-4o', 'GPT-4', 'gpt-4o') ; + +-- Experiments (like feature flags) +CREATE TABLE experiments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT +); + +CREATE TABLE experiment_class ( + experiment_id INTEGER NOT NULL, + class_id INTEGER NOT NULL, + PRIMARY KEY (experiment_id, class_id), + FOREIGN KEY (experiment_id) REFERENCES experiments (id), + FOREIGN KEY (class_id) REFERENCES classes (id) +); +DROP INDEX IF EXISTS exp_crs_experiment_idx; +CREATE INDEX exp_crs_experiment_idx ON experiment_class(experiment_id); +DROP INDEX IF EXISTS exp_crs_class_idx; +CREATE INDEX exp_crs_class_idx ON experiment_class(class_id); diff --git a/src/gened/templates/admin_experiments.html b/src/gened/templates/admin_experiments.html new file mode 100644 index 0000000..533fb50 --- /dev/null +++ b/src/gened/templates/admin_experiments.html @@ -0,0 +1,20 @@ +{# +SPDX-FileCopyrightText: 2024 Mark Liffiton + +SPDX-License-Identifier: AGPL-3.0-only +#} + +{% extends "admin_main.html" %} +{% from "tables.html" import datatable %} + +{% block admin_body %} +

Experiments Create New

+
+ {{ datatable( + 'experiments', + [('id', 'id'), ('name', 'name'), ('description', 'description'), ('classes', 'count', 'r')], + experiments, + edit_handler="admin.experiment_form", + ) }} +
+{% endblock %} diff --git a/src/gened/templates/experiment_form.html b/src/gened/templates/experiment_form.html new file mode 100644 index 0000000..65dc212 --- /dev/null +++ b/src/gened/templates/experiment_form.html @@ -0,0 +1,130 @@ +{# +SPDX-FileCopyrightText: 2024 Mark Liffiton + +SPDX-License-Identifier: AGPL-3.0-only +#} + +{% extends "admin_main.html" %} + +{% block admin_body %} +
+ +
+ {% if experiment %} + {# We're editing an existing experiment. Provide its ID. #} +

Edit Experiment

+ + {% else %} +

New Experiment

+ {% endif %} + +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + {% if experiment %} +
+
+ +
+
+
+ +
+
+ +
+
+
+ + + {% endif %} + +
+
+
+
+
+ + {% if experiment %} + + {% endif %} +
+
+
+
+ +
+{% endblock %}