# SPDX-License-Identifier: LGPL-3.0-or-later
"""
Blueprint definitions for the Maeser application.
This module sets up the **Flask blueprint** and associated routes for the Maeser
application. It includes route handlers for chat interfaces, user management,
feedback, and training functionalities.
"""
from flask import Blueprint, Flask
from flask_login import login_required, current_user, LoginManager
import os
from datetime import datetime
import threading
import time
from maeser.chat.chat_session_manager import ChatSessionManager
from maeser.controllers.common.decorators import admin_required, rate_limited
from maeser.user_manager import UserManager
from maeser.controllers import (
chat_api,
chat_interface,
chat_logs_overview,
display_chat_log,
feedback_api,
feedback_form_get,
feedback_form_post,
login_api,
logout,
new_session_api,
training,
training_post,
conversation_history_api,
remaining_requests_api,
manage_users_view,
user_management_api,
)
[docs]
class AppManager:
"""
Manages the Maeser App and its configurations.
Takes in configuration for the Maeser application and applies the configuration when
adding the Flask blueprint.
Running **add_flask_blueprint()** after initializing an AppManager object will return
a Flask object with the blueprint added.
Args:
app (Flask): The Flask application instance.
app_name (str | None, optional): The name of the application.
flask_secret_key (str): The secret key for the Flask application.
chat_session_manager (ChatSessionManager): The chat session manager instance.
user_manager (UserManager | None, optional): The user manager instance. Defaults to None.
main_logo_login (str | None, optional): URL or path to the login logo. Defaults to None.
main_logo_chat (str | None, optional): URL or path to the chat logo. Defaults to None.
chat_head (str | None, optional): URL or path to the chat header image. Defaults to None.
favicon (str | None, optional): URL or path to the favicon. Defaults to None.
"""
def __init__(
self,
# app and app name
app: Flask,
app_name: str,
# flask secret
flask_secret_key: str,
# managers
chat_session_manager: ChatSessionManager,
user_manager: UserManager | None = None,
# images
main_logo_login: str | None = None,
main_logo_chat: str | None = None,
chat_head: str | None = None,
favicon: str | None = None,
):
self.app = app
self.app_name = app_name
self.flask_secret_key = flask_secret_key
self.chat_session_manager = chat_session_manager
self.user_manager = user_manager
self.main_logo_login = main_logo_login
self.main_logo_chat = main_logo_chat
self.chat_head = chat_head
self.favicon = favicon
# NOTE: The commented-out variables below are unimplemented.
# self.login_text = login_text # TODO: Not implemented
# self.changelog = changelog # TODO: Not implemented
# self.chat_greeting = chat_greeting # TODO: Not implemented
# self.branch_response = branch_response # TODO: Not implemented
# self.animation = animation # TODO: Not implemented
# self.primary_color = primary_color # TODO: Not implemented
# self.secondary_color = secondary_color # TODO: Not implemented
# self.button_color = button_color # TODO: Not implemented
# self.button_color_active = button_color_active # TODO: Not implemented
# self.button_color_inactive = button_color_inactive # TODO: Not implemented
# self.fafa_button = fafa_button # TODO: Not implemented
# self.logout_button = logout_button # TODO: Not implemented
# self.new_chat_button = new_chat_button # TODO: Not implemented
# self.help_train_button = help_train_button # TODO: Not implemented
self.current_dir = os.path.dirname(os.path.abspath(__file__ + "/."))
# The following functions with no code are work in progress and will be added soon
# def template_styles_css(self):
# """
# Template the styles.css file using Jinja2 templating
# """
# def template_chat_interface(self):
# """
# Template the chat_interface.html file using Jinja2 templating
# """
# def template_login(self):
# """
# Template the login.html file using Jinja2 templating
# """
# def template_training(self):
# """
# Template the training.html file using Jinja2 templating
# """
# def template_feedback(self):
# """
# Template the feedback_form.html file using Jinja2 templating
# """
[docs]
def add_flask_blueprint(self) -> Flask:
"""
Adds the Maeser blueprint to the Flask application.
This function does not mutate the AppManager instance itself but rather returns
a new Flask instance with the blueprint added.
Returns:
Flask: The Flask application instance with the blueprint registered.
"""
maeser_blueprint = Blueprint(
"maeser",
__name__,
template_folder=os.path.join(self.current_dir, "data/templates"),
static_folder=os.path.join(self.current_dir, "data/static"),
static_url_path="/maeser/static",
)
@maeser_blueprint.app_template_filter("datetimeformat")
def datetimeformat(value, format="%Y-%m-%d %H:%M:%S"):
"""Format a timestamp as a datetime string."""
return datetime.fromtimestamp(value).strftime(format)
if self.user_manager:
def refresh_requests():
"""Refresh user requests at regular intervals."""
while True:
time.sleep(self.user_manager.rate_limit_interval)
self.user_manager.refresh_requests()
threading.Thread(target=refresh_requests, daemon=True).start()
self.app.secret_key = self.flask_secret_key
login_manager = LoginManager(self.app)
login_manager.init_app(self.app)
login_manager.login_view = "maeser.login" # type: ignore
login_manager.session_protection = "strong"
@login_manager.user_loader
def load_user(user_full_id: str):
"""Load a user by their ID."""
auth_method, user_id = user_full_id.split(".", 1)
return self.user_manager.get_user(auth_method, user_id)
@maeser_blueprint.route("/")
@login_required
def chat_interface_route():
"""Route for the chat interface."""
return chat_interface.controller(
self.chat_session_manager,
self.user_manager.max_requests,
self.user_manager.rate_limit_interval,
current_user,
main_logo_login=self.main_logo_login,
main_logo_chat=self.main_logo_chat,
favicon=self.favicon,
chat_head=self.chat_head,
app_name=self.app_name,
)
@maeser_blueprint.route("/login", methods=["GET", "POST"])
def login():
"""Route for the login interface."""
return login_api.login_controller(
self.user_manager,
main_logo_login=self.main_logo_login,
main_logo_chat=self.main_logo_chat,
favicon=self.favicon,
app_name=self.app_name,
)
@maeser_blueprint.route("/login/github", methods=["GET"])
def github_authorize():
"""Route for GitHub authorization."""
return login_api.github_authorize_controller(
current_user, self.user_manager.authenticators["github"]
)
@maeser_blueprint.route("/login/github_callback")
def github_auth_callback():
"""Route for GitHub authorization callback."""
return login_api.github_auth_callback_controller(
current_user,
self.user_manager,
)
@maeser_blueprint.route("/logout")
@login_required
def logout_route():
"""Route for logging out."""
return logout.controller()
@maeser_blueprint.route("/users")
@login_required
@admin_required(current_user)
def manage_users():
"""Route for managing users."""
return manage_users_view.controller(
self.user_manager,
main_logo_chat=self.main_logo_chat,
favicon=self.favicon,
app_name=self.app_name,
)
@maeser_blueprint.route("/users/api", methods=["POST"])
@login_required
@admin_required(current_user)
def manage_users_api():
"""API route for managing users."""
return user_management_api.controller(self.user_manager)
@maeser_blueprint.route("/req_session", methods=["POST"])
@login_required
def sess_handler():
"""Route for handling session requests."""
return new_session_api.controller(self.chat_session_manager, True)
@maeser_blueprint.route("/msg/<chat_session>", methods=["POST"])
@login_required
@rate_limited(self.user_manager, current_user)
def msg_api(chat_session):
"""API route for handling chat messages."""
return chat_api.controller(self.chat_session_manager, chat_session)
@maeser_blueprint.route("/feedback", methods=["POST"])
@login_required
def feedback():
"""Route for submitting feedback."""
return feedback_api.controller(self.chat_session_manager)
@self.app.route("/get_requests_remaining", methods=["GET"])
@login_required
def get_requests_remaining():
"""Route for getting the remaining requests."""
return remaining_requests_api.controller(
self.user_manager, current_user
)
else:
@maeser_blueprint.route("/")
def chat_interface_route():
"""Route for the chat interface."""
return chat_interface.controller(
self.chat_session_manager,
main_logo_login=self.main_logo_login,
main_logo_chat=self.main_logo_chat,
favicon=self.favicon,
chat_head=self.chat_head,
app_name=self.app_name,
)
@maeser_blueprint.route("/req_session", methods=["POST"])
def sess_handler():
"""Route for handling session requests."""
return new_session_api.controller(self.chat_session_manager)
@maeser_blueprint.route("/msg/<chat_session>", methods=["POST"])
def msg_api(chat_session):
"""API route for handling chat messages."""
return chat_api.controller(self.chat_session_manager, chat_session)
@maeser_blueprint.route("/feedback", methods=["POST"])
def feedback():
"""Route for submitting feedback."""
return feedback_api.controller(self.chat_session_manager)
if self.chat_session_manager.chat_logs_manager:
@maeser_blueprint.route("/train")
@login_required if self.user_manager else lambda x: x
def train():
"""Route for training."""
return training.controller(
main_logo_chat=self.main_logo_chat,
favicon=self.favicon,
app_name=self.app_name,
)
@maeser_blueprint.route("/submit_train", methods=["POST"])
@login_required if self.user_manager else lambda x: x
def submit_train():
"""Route for submitting training data."""
return training_post.controller(self.chat_session_manager)
@maeser_blueprint.route("/feedback_form")
def feedback_form():
"""Route for getting the feedback form."""
return feedback_form_get.controller(
main_logo_chat=self.main_logo_chat,
favicon=self.favicon,
app_name=self.app_name,
)
@maeser_blueprint.route("/submit_feedback", methods=["POST"])
def submit_feedback():
"""Route for submitting feedback."""
return feedback_form_post.controller(self.chat_session_manager)
@maeser_blueprint.route("/logs", methods=["GET"])
@login_required if self.user_manager else lambda x: x
@admin_required(current_user) if self.user_manager else lambda x: x
def logs():
"""Route for viewing chat logs."""
return chat_logs_overview.controller(
self.chat_session_manager,
main_logo_chat=self.main_logo_chat,
favicon=self.favicon,
app_name=self.app_name,
)
@maeser_blueprint.route("/logs/<branch>/<filename>")
@login_required if self.user_manager else lambda x: x
@admin_required(current_user) if self.user_manager else lambda x: x
def display_log(branch, filename):
"""Route for displaying a specific chat log."""
return display_chat_log.controller(
self.chat_session_manager, branch, filename, app_name=self.app_name
)
@maeser_blueprint.route("/conversation_history", methods=["POST"])
@login_required if self.user_manager else lambda x: x
def conversation_history():
"""Route for getting conversation history."""
return conversation_history_api.controller(self.chat_session_manager)
self.app.register_blueprint(maeser_blueprint)
return self.app