"""
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.
© 2024 Blaine Freestone, Carson Bush, Gohaun Manley
This file is part of Maeser.
Maeser is free software: you can redistribute it and/or modify it under the terms of
the GNU Lesser General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
Maeser is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with
Maeser. If not, see <https://www.gnu.org/licenses/>.
"""
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 .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 App_Manager:
"""
Manages the Maeser App and its configurations.
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.
login_text (str | None, optional): Text to display on the login page. Defaults to None.
changelog (str | None, optional): Text to display as the changelog. Defaults to None.
chat_greeting (str, optional): Greeting message to display in the chat. Defaults to "Hello, how can I help you today?".
branch_response (str, optional): Response message to display when a branch is selected. Defaults to "Okay, I'll help you with ${action}!".
animation (bool, optional): Whether to enable toggle animations. Defaults to False.
primary_color (str, optional): Primary color of the application. Defaults to "#333".
secondary_color (str, optional): Secondary color of the application. Defaults to "#ccc".
button_color (str, optional): Color of the buttons. Defaults to "#0084ff".
button_color_active (str, optional): Color of the active buttons. Defaults to "#009e15".
button_color_inactive (str, optional): Color of the inactive buttons. Defaults to "#ddd".
fafa_button (str, optional): Color of the fafa buttons. Defaults to "#eee".
logout_button (str, optional): Color of the logout button. Defaults to "#333".
new_chat_button (str, optional): Color of the new chat button. Defaults to "#333".
help_train_button (str, optional): Color of the help train button. Defaults to "#eee".
"""
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,
# text
login_text: str | None = None,
changelog: str | None = None,
chat_greeting: str = "Hello, how can I help you today?",
branch_response: str = "Okay, I'll help you with ${action}!",
# toggle animations
animation: bool = False,
# colors
primary_color: str = "#f5f5f5",
secondary_color: str = "#ccc",
button_color: str = "#0084ff",
button_color_active: str = "#009e15",
button_color_inactive: str = "#e9e9e9",
fafa_button: str = "#b8b8b8",
logout_button: str = "#000000",
new_chat_button: str = "#000000",
help_train_button: str = "#a8a8a8",
):
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
self.login_text = login_text
self.changelog = changelog
self.chat_greeting = chat_greeting
self.branch_response = branch_response
self.animation = animation
self.primary_color = primary_color
self.secondary_color = secondary_color
self.button_color = button_color
self.button_color_active = button_color_active
self.button_color_inactive = button_color_inactive
self.fafa_button = fafa_button
self.logout_button = logout_button
self.new_chat_button = new_chat_button
self.help_train_button = help_train_button
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
[docs]
def template_styles_css(self):
"""
Template the styles.css file using Jinja2 templating
"""
[docs]
def template_chat_interface(self):
"""
Template the chat_interface.html file using Jinja2 templating
"""
[docs]
def template_login(self):
"""
Template the login.html file using Jinja2 templating
"""
[docs]
def template_training(self):
"""
Template the training.html file using Jinja2 templating
"""
[docs]
def template_feedback(self):
"""
Template the feedback_form.html file using Jinja2 templating
"""
[docs]
def add_flask_blueprint(self) -> Flask:
"""
Add the Maeser blueprint to the Flask application.
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,
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("/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_login=self.main_logo_login,
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_login=self.main_logo_login,
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_login=self.main_logo_login,
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,
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