- Adding a Tenant Type

- Allow filtering on Tenant Types & searching for parts of Tenant names
- Implement health checks
- Start Prometheus monitoring (needs to be finalized)
- Refine audio_processor and srt_processor to reduce duplicate code and support for larger files
- Introduce repopack to reason in LLMs about the code
This commit is contained in:
Josako
2024-09-13 15:43:40 +02:00
parent 9e14824249
commit 6cf660e622
41 changed files with 687 additions and 579 deletions

View File

@@ -7,7 +7,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
import logging.config
from common.extensions import (db, migrate, bootstrap, security, mail, login_manager, cors, csrf, session,
minio_client, simple_encryption)
minio_client, simple_encryption, metrics)
from common.models.user import User, Role, Tenant, TenantDomain
import common.models.interaction
from common.utils.nginx_utils import prefixed_url_for
@@ -114,10 +114,10 @@ def register_extensions(app):
csrf.init_app(app)
login_manager.init_app(app)
cors.init_app(app)
# kms_client.init_app(app)
simple_encryption.init_app(app)
session.init_app(app)
minio_client.init_app(app)
metrics.init_app(app)
# Register Blueprints
@@ -132,3 +132,7 @@ def register_blueprints(app):
app.register_blueprint(security_bp)
from .views.interaction_views import interaction_bp
app.register_blueprint(interaction_bp)
from .views.healthz_views import healthz_bp, init_healtz
app.register_blueprint(healthz_bp)
init_healtz(app)

View File

@@ -1,16 +1,16 @@
{% macro render_field(field, disabled_fields=[], exclude_fields=[]) %}
{% macro render_field(field, disabled_fields=[], exclude_fields=[], class='') %}
{% set disabled = field.name in disabled_fields %}
{% set exclude_fields = exclude_fields + ['csrf_token', 'submit'] %}
{% if field.name not in exclude_fields %}
{% if field.type == 'BooleanField' %}
<div class="form-check">
{{ field(class="form-check-input", type="checkbox", id="flexSwitchCheckDefault") }}
{{ field(class="form-check-input " + class, type="checkbox", id="flexSwitchCheckDefault") }}
{{ field.label(class="form-check-label", for="flexSwitchCheckDefault", disabled=disabled) }}
</div>
{% else %}
<div class="form-group">
{{ field.label(class="form-label") }}
{{ field(class="form-control", disabled=disabled) }}
{{ field(class="form-control " + class, disabled=disabled) }}
{% if field.errors %}
<div class="invalid-feedback">
{% for error in field.errors %}

View File

@@ -13,3 +13,5 @@
<script src="{{url_for('static', filename='assets/js/plugins/anime.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/material-kit-pro.min.js')}}?v=3.0.4 type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js"></script>

View File

@@ -1,22 +1,52 @@
{% extends 'base.html' %}
{% from "macros.html" import render_selectable_table, render_pagination %}
{% from "macros.html" import render_selectable_table, render_pagination, render_field %}
{% block title %}Tenant Selection{% endblock %}
{% block content_title %}Select a Tenant{% endblock %}
{% block content_description %}Select the active tenant for the current session{% endblock %}
{% block content %}
<!-- Filter Form -->
<form method="POST" action="{{ url_for('user_bp.select_tenant') }}" class="mb-4">
{{ filter_form.hidden_tag() }}
<div class="row">
<div class="col-md-4">
{{ render_field(filter_form.types, class="select2") }}
</div>
<div class="col-md-4">
{{ render_field(filter_form.search) }}
</div>
<div class="col-md-4">
{{ filter_form.submit(class="btn btn-primary") }}
</div>
</div>
</form>
<!-- Tenant Selection Form -->
<form method="POST" action="{{ url_for('user_bp.handle_tenant_selection') }}">
{{ render_selectable_table(headers=["Tenant ID", "Tenant Name", "Website"], rows=rows, selectable=True, id="tenantsTable") }}
{{ render_selectable_table(headers=["Tenant ID", "Tenant Name", "Website", "Type"], rows=rows, selectable=True, id="tenantsTable") }}
<div class="form-group mt-3">
<button type="submit" name="action" value="select_tenant" class="btn btn-primary">Set Session Tenant</button>
<button type="submit" name="action" value="edit_tenant" class="btn btn-secondary">Edit Tenant</button>
</div>
</form>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'user_bp.select_tenant') }}
{{ render_pagination(pagination, 'user_bp.select_tenant') }}
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
$('.select2').select2({
placeholder: "Select tenant types",
allowClear: true,
minimumResultsForSearch: Infinity, // Hides the search box
dropdownCssClass: 'select2-dropdown-hidden', // Custom class for dropdown
containerCssClass: 'select2-container-hidden' // Custom class for container
});
});
</script>
{% endblock %}

View File

@@ -10,7 +10,7 @@
<form method="post">
{{ form.hidden_tag() }}
<!-- Main Tenant Information -->
{% set main_fields = ['name', 'website', 'default_language', 'allowed_languages'] %}
{% set main_fields = ['name', 'website', 'default_language', 'allowed_languages', 'rag_context', 'type'] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=main_fields, include_fields=main_fields) }}
{% endfor %}

View File

@@ -30,7 +30,6 @@ class AddDocumentForm(FlaskForm):
user_context = TextAreaField('User Context', validators=[Optional()])
valid_from = DateField('Valid from', id='form-control datepicker', validators=[Optional()])
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])
submit = SubmitField('Submit')
@@ -38,7 +37,8 @@ class AddDocumentForm(FlaskForm):
super().__init__()
self.language.choices = [(language, language) for language in
session.get('tenant').get('allowed_languages')]
self.language.data = session.get('tenant').get('default_language')
if not self.language.data:
self.language.data = session.get('tenant').get('default_language')
class AddURLForm(FlaskForm):
@@ -48,7 +48,6 @@ class AddURLForm(FlaskForm):
user_context = TextAreaField('User Context', validators=[Optional()])
valid_from = DateField('Valid from', id='form-control datepicker', validators=[Optional()])
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])
submit = SubmitField('Submit')
@@ -56,7 +55,8 @@ class AddURLForm(FlaskForm):
super().__init__()
self.language.choices = [(language, language) for language in
session.get('tenant').get('allowed_languages')]
self.language.data = session.get('tenant').get('default_language')
if not self.language.data:
self.language.data = session.get('tenant').get('default_language')
class AddURLsForm(FlaskForm):
@@ -72,7 +72,8 @@ class AddURLsForm(FlaskForm):
super().__init__()
self.language.choices = [(language, language) for language in
session.get('tenant').get('allowed_languages')]
self.language.data = session.get('tenant').get('default_language')
if not self.language.data:
self.language.data = session.get('tenant').get('default_language')
class EditDocumentForm(FlaskForm):

View File

@@ -56,11 +56,9 @@ def before_request():
@roles_accepted('Super User', 'Tenant Admin')
def add_document():
form = AddDocumentForm()
current_app.logger.debug('Adding document')
if form.validate_on_submit():
try:
current_app.logger.debug('Validating file type')
tenant_id = session['tenant']['id']
file = form.file.data
filename = secure_filename(file.filename)
@@ -68,15 +66,15 @@ def add_document():
validate_file_type(extension)
current_app.logger.debug(f'Language on form: {form.language.data}')
api_input = {
'name': form.name.data,
'language': form.language.data,
'user_context': form.user_context.data,
'valid_from': form.valid_from.data,
'user_metadata': json.loads(form.user_metadata.data) if form.user_metadata.data else None,
'system_metadata': json.loads(form.system_metadata.data) if form.system_metadata.data else None
}
current_app.logger.debug(f'Creating document stack with input {api_input}')
new_doc, new_doc_vers = create_document_stack(api_input, file, filename, extension, tenant_id)
task_id = start_embedding_task(tenant_id, new_doc_vers.id)
@@ -113,7 +111,6 @@ def add_url():
'user_context': form.user_context.data,
'valid_from': form.valid_from.data,
'user_metadata': json.loads(form.user_metadata.data) if form.user_metadata.data else None,
'system_metadata': json.loads(form.system_metadata.data) if form.system_metadata.data else None
}
new_doc, new_doc_vers = create_document_stack(api_input, file_content, filename, extension, tenant_id)

View File

@@ -0,0 +1,100 @@
from flask import Blueprint, current_app, request
from flask_healthz import HealthError
from sqlalchemy.exc import SQLAlchemyError
from celery.exceptions import TimeoutError as CeleryTimeoutError
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
import time
from common.extensions import db, metrics, minio_client
from common.utils.celery_utils import current_celery
healthz_bp = Blueprint('healthz', __name__, url_prefix='/_healthz')
# Define Prometheus metrics
api_request_counter = Counter('api_request_count', 'API Request Count', ['method', 'endpoint'])
api_request_latency = Histogram('api_request_latency_seconds', 'API Request latency')
def liveness():
try:
# Basic check to see if the app is running
return True
except Exception:
raise HealthError("Liveness check failed")
def readiness():
checks = {
"database": check_database(),
"celery": check_celery(),
"minio": check_minio(),
# Add more checks as needed
}
if not all(checks.values()):
raise HealthError("Readiness check failed")
def check_database():
try:
# Perform a simple database query
db.session.execute("SELECT 1")
return True
except SQLAlchemyError:
current_app.logger.error("Database check failed", exc_info=True)
return False
def check_celery():
try:
# Send a simple task to Celery
result = current_celery.send_task('tasks.ping', queue='embeddings')
response = result.get(timeout=10) # Wait for up to 10 seconds for a response
return response == 'pong'
except CeleryTimeoutError:
current_app.logger.error("Celery check timed out", exc_info=True)
return False
except Exception as e:
current_app.logger.error(f"Celery check failed: {str(e)}", exc_info=True)
return False
def check_minio():
try:
# List buckets to check if MinIO is accessible
minio_client.list_buckets()
return True
except Exception as e:
current_app.logger.error(f"MinIO check failed: {str(e)}", exc_info=True)
return False
@healthz_bp.route('/metrics')
@metrics.do_not_track()
def prometheus_metrics():
return generate_latest(), 200, {'Content-Type': CONTENT_TYPE_LATEST}
# Custom metrics example
@healthz_bp.before_app_request
def before_request():
request.start_time = time.time()
api_request_counter.labels(
method=request.method, endpoint=request.endpoint
).inc()
@healthz_bp.after_app_request
def after_request(response):
request_duration = time.time() - request.start_time
api_request_latency.observe(request_duration)
return response
def init_healtz(app):
app.config.update(
HEALTHZ={
"live": "healthz_views.liveness",
"ready": "healthz_views.readiness",
}
)

View File

@@ -2,7 +2,7 @@ from flask import current_app
from flask_wtf import FlaskForm
from wtforms import (StringField, PasswordField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
SelectField, SelectMultipleField, FieldList, FormField, FloatField, TextAreaField)
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, ValidationError
import pytz
from common.models.user import Role
@@ -18,6 +18,8 @@ class TenantForm(FlaskForm):
timezone = SelectField('Timezone', choices=[], validators=[DataRequired()])
# RAG context
rag_context = TextAreaField('RAG Context', validators=[Optional()])
# Tenant Type
type = SelectField('Tenant Type', validators=[Optional()], default='Active')
# LLM fields
embedding_model = SelectField('Embedding Model', choices=[], validators=[DataRequired()])
llm_model = SelectField('Large Language Model', choices=[], validators=[DataRequired()])
@@ -65,6 +67,7 @@ class TenantForm(FlaskForm):
# Initialize fallback algorithms
self.fallback_algorithms.choices = \
[(algorithm, algorithm.lower()) for algorithm in current_app.config['FALLBACK_ALGORITHMS']]
self.type.choices = [('', 'Select Type')] + [(t, t) for t in current_app.config['TENANT_TYPES']]
class BaseUserForm(FlaskForm):
@@ -107,4 +110,14 @@ class TenantDomainForm(FlaskForm):
submit = SubmitField('Add Domain')
class TenantSelectionForm(FlaskForm):
types = SelectMultipleField('Tenant Types', choices=[], validators=[Optional()])
search = StringField('Search', validators=[Optional()])
submit = SubmitField('Filter')
def __init__(self, *args, **kwargs):
super(TenantSelectionForm, self).__init__(*args, **kwargs)
self.types.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]

View File

@@ -10,7 +10,7 @@ import ast
from common.models.user import User, Tenant, Role, TenantDomain
from common.extensions import db, security, minio_client, simple_encryption
from common.utils.security_utils import send_confirmation_email, send_reset_email
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm
from common.utils.database import Database
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
from common.utils.simple_encryption import generate_api_key
@@ -245,20 +245,29 @@ def edit_user(user_id):
return render_template('user/edit_user.html', form=form, user_id=user_id)
@user_bp.route('/select_tenant')
@user_bp.route('/select_tenant', methods=['GET', 'POST'])
@roles_required('Super User')
def select_tenant():
filter_form = TenantSelectionForm(request.form)
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
query = Tenant.query.order_by(Tenant.name) # Fetch all tenants from the database
query = Tenant.query
pagination = query.paginate(page=page, per_page=per_page)
if filter_form.validate_on_submit():
if filter_form.types.data:
query = query.filter(Tenant.type.in_(filter_form.types.data))
if filter_form.search.data:
search = f"%{filter_form.search.data}%"
query = query.filter(Tenant.name.ilike(search))
query = query.order_by(Tenant.name)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
tenants = pagination.items
rows = prepare_table_for_macro(tenants, [('id', ''), ('name', ''), ('website', '')])
rows = prepare_table_for_macro(tenants, [('id', ''), ('name', ''), ('website', ''), ('type', '')])
return render_template('user/select_tenant.html', rows=rows, pagination=pagination)
return render_template('user/select_tenant.html', rows=rows, pagination=pagination, filter_form=filter_form)
@user_bp.route('/handle_tenant_selection', methods=['POST'])