Source code for maeser.admin_portal._flask_admin_portal

# SPDX-License-Identifier: LGPL-3.0-or-later

from flask import Flask, render_template, request, redirect, url_for, session
from maeser.config import VEC_STORE_PATH as UPLOAD_ROOT, OPENAI_API_KEY
import os
import functools
import secrets
from werkzeug.utils import secure_filename
from maeser.admin_portal.design_model import (
    get_model_config,
    save_model,
    delete_datasets,
    remove_class_model,
    load_rules,
    load_datasets,
)

admin_flask_app: Flask = Flask(__name__)

# All code below is for the FLASK INTERFACE
# Demo User
# The values of "username" and "password" will be replaced when run_admin_portal() is called
USER = {"username": "adam", "password": "ajosiahs", "is_admin": True}

# Global Variables
model_name = ""
host_address = ""
rules = []
contexts = []


# decorator for login checking
def require_login(func):
    @functools.wraps(
        func
    )  # updates metadata so that check_login.__name__ == func.__name__
    def check_login(*args, **kwargs):
        if "user" in session:
            return func(*args, **kwargs)
        else:
            return redirect(url_for("login"))

    return check_login


@admin_flask_app.route("/")
@require_login
def home():
    return render_template("admin_portal.html", username=session["user"])


@admin_flask_app.route("/login", methods=["GET", "POST"])
def login():
    error = None
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]
        if username == USER["username"] and password == USER["password"]:
            session["user"] = username
            return redirect(url_for("home"))
        else:
            error = "Invalid Credentials. Please try again."
    return render_template("login.html", error=error)


@admin_flask_app.route("/design_model", methods=["GET", "POST"])
@require_login
def design_model():
    if request.method == "POST":
        # Get model config
        try:
            model_config: tuple = get_model_config(UPLOAD_ROOT)
        except AttributeError as e:
            print(f"Unable to use model config: {e}")
            return redirect(url_for("design_model"))

        # Save model
        try:
            save_model(UPLOAD_ROOT, *model_config)
        except Exception as e:
            print(f"Unable to generate model: {e}")
            return redirect(url_for("design_model"))

        print(
            "Model generation complete. Please check the terminal output to ensure no errors were thrown."
        )
        return redirect(url_for("manage_models"))

    return render_template("design_model.html", username=session["user"])


@admin_flask_app.route("/manage_models", methods=["GET", "POST"])
@require_login
def manage_models():
    if request.method == "POST":
        course_id = request.form.get("course_id")
        if not course_id:
            print("Error: Course ID is required.")
            return redirect(url_for("design_model"))
        else:
            remove_class_model(UPLOAD_ROOT, course_id)
        return redirect(url_for("manage_models"))

    # List all course_id folders inside UPLOAD_ROOT
    models = []
    for item in os.listdir(UPLOAD_ROOT):
        model_path = os.path.join(UPLOAD_ROOT, item)
        if os.path.isdir(model_path):
            models.append(item)  # item is the course_id

    return render_template(
        "manage_models.html", username=session["user"], models=models
    )


@admin_flask_app.route("/edit_model/<course_id>", methods=["GET", "POST"])
@require_login
def edit_model(course_id):
    # Get important file paths
    model_dir = os.path.join(UPLOAD_ROOT, secure_filename(course_id))
    bot_path = os.path.join(model_dir, "bot.txt")

    if request.method == "POST":
        # Get model config
        try:
            model_config: tuple = get_model_config(UPLOAD_ROOT)
        except AttributeError as e:
            print(f"Unable to use model config: {e}")
            return redirect(url_for("edit_model", course_id=course_id))

        # Delete selected datasets
        delete_datasets(model_dir)

        # Save model
        try:
            save_model(UPLOAD_ROOT, *model_config)
        except Exception as e:
            print(f"Unable to generate model: {e}")
            return redirect(url_for("edit_model", course_id=course_id))
        
        print(
            "Model generation complete. Please check the terminal output to ensure no errors were thrown."
        )
        return redirect(url_for("manage_models"))

    # Get rules and datasets
    rules = load_rules(bot_path)
    current_datasets = load_datasets(model_dir)

    return render_template(
        "edit_model.html",
        course_id=course_id,
        rules=rules,
        current_datasets=current_datasets,
        username=session["user"],
    )


@admin_flask_app.route("/logout")
def logout():
    session.pop("user", None)
    return redirect(url_for("login"))


def _check_config_variables():
    """Checks to see if any necessary config values are unassigned and raises a LookupError
    accordingly.

    Necessary config values are pulled from `config.yaml` using **maeser.config** and are as follows:

    - **UPLOAD_ROOT** (*str*): The path where courses are accessed and managed by the admin portal. Defined in the "vectorstore:vec_store_path" field in the config.
    - **OPENAI_API_KEY** (*str*): The API key for the OpenAI LLM. Defined in the "api_keys:openai_api_key" field in the config.
    
    Raises:
        LookupError: If any of the necessary config values are unassigned.
    """
    if UPLOAD_ROOT == "" or UPLOAD_ROOT == "...":
        raise LookupError(f'vec_store_path ("{UPLOAD_ROOT}") cannot be unassigned.')

    if OPENAI_API_KEY == "" or OPENAI_API_KEY == "...":
        raise LookupError(f'openai_api_key ("{OPENAI_API_KEY}") cannot be unassigned.')


[docs] def run_admin_portal( username: str = "adam", password: str = "ajosiahs", secret_key: str | None = None, host: str | None = None, port: int | None = None, debug: bool | None = None, load_dotenv: bool = True, **options, ): """Runs the admin portal flask application. Allows a username, password, and secret key to be declared as well as all standard Flask.run parameters. See the Flask documentation for more information on parameters and options. In production, **username**, **password**, and **secret_key** should always be assigned. Args: username (str, optional): The username for logging in to the admin portal. Defaults to "adam". password (str, optional): The password for logging in to the admin portal. Defaults to "ajosiahs". secret_key (str, optional): The secret key to assign to the application. Creates a random key if none is provided. host (str | None, optional): The web server hostname. port (int | None, optional): The web server port. debug (bool | None, optional): If True, enables debug mode. load_dotenv (bool, optional): Loads .env and .flaskenv files to initialize environment variables. Defaults to True. options: Werkzeug server options. For more information, see ``werkzeug.serving.run_simple``. """ _check_config_variables() # Assign parameters specific to Admin Portal USER["username"] = username USER["password"] = password admin_flask_app.secret_key = secret_key if secret_key else secrets.token_hex() # Run Flask with parameters admin_flask_app.run( host=host, port=port, debug=debug, load_dotenv=load_dotenv, **options, )
if __name__ == "__main__": run_admin_portal()