- Add Specialist Magic Links

- correction of some bugs:
  - dynamic fields for adding documents / urls to dossier catalog
  - tabs in latest bootstrap version no longer functional
  - partner association of license tier not working when no partner selected
  - data-type dynamic field needs conversion to isoformat
  - Add public tables to env.py of tenant schema
This commit is contained in:
Josako
2025-06-04 11:53:35 +02:00
parent b4e58659a8
commit 0d05499d2b
34 changed files with 822 additions and 121 deletions

View File

@@ -389,10 +389,7 @@ def add_document():
catalog = Catalog.query.get_or_404(catalog_id)
if catalog.configuration and len(catalog.configuration) > 0:
full_config = cache_manager.catalogs_config_cache.get_config(catalog.type)
document_version_configurations = full_config['document_version_configurations']
for config in document_version_configurations:
form.add_dynamic_fields(config, full_config, catalog.configuration[config])
form.add_dynamic_fields("tagging_fields", catalog.configuration)
if form.validate_on_submit():
try:
@@ -402,11 +399,8 @@ def add_document():
sub_file_type = form.sub_file_type.data
filename = secure_filename(file.filename)
extension = filename.rsplit('.', 1)[1].lower()
catalog_properties = {}
full_config = cache_manager.catalogs_config_cache.get_config(catalog.type)
document_version_configurations = full_config['document_version_configurations']
for config in document_version_configurations:
catalog_properties[config] = form.get_dynamic_data(config)
catalog_properties = form.get_dynamic_data("tagging_fields")
api_input = {
'catalog_id': catalog_id,
@@ -446,10 +440,7 @@ def add_url():
catalog = Catalog.query.get_or_404(catalog_id)
if catalog.configuration and len(catalog.configuration) > 0:
full_config = cache_manager.catalogs_config_cache.get_config(catalog.type)
document_version_configurations = full_config['document_version_configurations']
for config in document_version_configurations:
form.add_dynamic_fields(config, full_config, catalog.configuration[config])
form.add_dynamic_fields("tagging_fields", catalog.configuration)
if form.validate_on_submit():
try:

View File

@@ -1,3 +1,5 @@
from datetime import date
from flask_wtf import FlaskForm
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
validators, ValidationError)
@@ -396,6 +398,12 @@ class DynamicFormBase(FlaskForm):
except (TypeError, ValueError) as e:
current_app.logger.error(f"Error converting initial data to a list of patterns: {e}")
field_data = {}
elif field_type == 'date' and isinstance(field_data, str):
try:
field_data = date.fromisoformat(field_data)
except ValueError:
current_app.logger.error(f"Error converting ISO date string '{field_data}' to date object")
field_data = None
elif default is not None:
field_data = default
@@ -543,6 +551,8 @@ class DynamicFormBase(FlaskForm):
data[original_field_name] = patterns_to_json(field.data)
except Exception as e:
current_app.logger.error(f"Error converting initial data to patterns: {e}")
elif isinstance(field, DateField):
data[original_field_name] = field.data.isoformat()
else:
data[original_field_name] = field.data
return data

View File

@@ -7,8 +7,9 @@ from wtforms.validators import DataRequired, Length, Optional
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
from common.models.document import Retriever
from common.models.interaction import EveAITool
from common.models.interaction import EveAITool, Specialist
from common.extensions import cache_manager
from common.utils.form_assistants import validate_json
from .dynamic_form_base import DynamicFormBase
@@ -132,4 +133,46 @@ class ExecuteSpecialistForm(DynamicFormBase):
description = TextAreaField('Specialist Description', validators=[Optional()], render_kw={'readonly': True})
class SpecialistMagicLinkForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()])
magic_link_code = StringField('Magic Link Code', validators=[DataRequired(), Length(max=55)], render_kw={'readonly': True})
specialist_id = SelectField('Specialist', validators=[DataRequired()])
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
# Metadata fields
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
specialists = Specialist.query.all()
# Dynamically populate the 'type' field using the constructor
self.specialist_id.choices = [(specialist.id, specialist.name) for specialist in specialists]
class EditSpecialistMagicLinkForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()])
magic_link_code = StringField('Magic Link Code', validators=[DataRequired(), Length(max=55)],
render_kw={'readonly': True})
specialist_id = IntegerField('Specialist', validators=[DataRequired()], render_kw={'readonly': True})
specialist_name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True})
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
# Metadata fields
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
specialist = Specialist.query.get(kwargs['specialist_id'])
if specialist:
self.specialist_name.data = specialist.name
else:
self.specialist_name.data = ''

View File

@@ -1,5 +1,6 @@
import ast
import json
import uuid
from datetime import datetime as dt, timezone as tz
import time
@@ -13,9 +14,10 @@ from werkzeug.utils import secure_filename
from common.models.document import Embedding, DocumentVersion, Retriever
from common.models.interaction import (ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever,
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion)
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion, SpecialistMagicLink)
from common.extensions import db, cache_manager
from common.models.user import SpecialistMagicLinkTenant
from common.services.interaction.specialist_services import SpecialistServices
from common.utils.asset_utils import create_asset_stack, add_asset_version_file
from common.utils.execution_progress import ExecutionProgressTracker
@@ -26,7 +28,8 @@ from common.utils.nginx_utils import prefixed_url_for
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm)
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm,
SpecialistMagicLinkForm, EditSpecialistMagicLinkForm)
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
@@ -669,3 +672,119 @@ def session_interactions(chat_session_id):
"""
chat_session = ChatSession.query.get_or_404(chat_session_id)
return session_interactions_by_session_id(chat_session.session_id)
# Routes for SpecialistMagicLink Management -------------------------------------------------------
@interaction_bp.route('/specialist_magic_link', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def specialist_magic_link():
form = SpecialistMagicLinkForm()
if request.method == 'GET':
magic_link_code = f"SPECIALIST_ML-{str(uuid.uuid4())}"
form.magic_link_code.data = magic_link_code
if form.validate_on_submit():
tenant_id = session.get('tenant').get('id')
try:
new_specialist_magic_link = SpecialistMagicLink()
# Populate fields individually instead of using populate_obj (gives problem with QueryMultipleSelectField)
form.populate_obj(new_specialist_magic_link)
set_logging_information(new_specialist_magic_link, dt.now(tz.utc))
# Create 'public' SpecialistMagicLinkTenant
new_spec_ml_tenant = SpecialistMagicLinkTenant()
new_spec_ml_tenant.magic_link_code = new_specialist_magic_link.magic_link_code
new_spec_ml_tenant.tenant_id = tenant_id
db.session.add(new_specialist_magic_link)
db.session.add(new_spec_ml_tenant)
db.session.commit()
flash('Specialist Magic Link successfully added!', 'success')
current_app.logger.info(f'Specialist {new_specialist_magic_link.name} successfully added for '
f'tenant {tenant_id}!')
return redirect(prefixed_url_for('interaction_bp.edit_specialist_magic_link',
specialist_magic_link_id=new_specialist_magic_link.id))
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Failed to add specialist magic link. Error: {str(e)}', exc_info=True)
flash(f'Failed to add specialist magic link. Error: {str(e)}', 'danger')
return render_template('interaction/specialist_magic_link.html', form=form)
@interaction_bp.route('/specialist_magic_link/<int:specialist_magic_link_id>', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_specialist_magic_link(specialist_magic_link_id):
specialist_ml = SpecialistMagicLink.query.get_or_404(specialist_magic_link_id)
# We need to pass along the extra kwarg specialist_id, as this id is required to initialize the form
form = EditSpecialistMagicLinkForm(request.form, obj=specialist_ml, specialist_id=specialist_ml.specialist_id)
# Find the Specialist type and type_version to enable to retrieve the arguments
specialist = Specialist.query.get_or_404(specialist_ml.specialist_id)
specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
form.add_dynamic_fields("arguments", specialist_config, specialist_ml.specialist_args)
if form.validate_on_submit():
# Update the basic fields
form.populate_obj(specialist_ml)
# Update the arguments dynamic fields
specialist_ml.specialist_args = form.get_dynamic_data("arguments")
# Update logging information
update_logging_information(specialist_ml, dt.now(tz.utc))
try:
db.session.commit()
flash('Specialist Magic Link updated successfully!', 'success')
current_app.logger.info(f'Specialist Magic Link {specialist_ml.id} updated successfully')
return redirect(prefixed_url_for('interaction_bp.specialist_magic_links'))
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Failed to update specialist Magic Link. Error: {str(e)}', 'danger')
current_app.logger.error(f'Failed to update specialist Magic Link {specialist_ml.id}. Error: {str(e)}')
else:
form_validation_failed(request, form)
return render_template('interaction/edit_specialist_magic_link.html', form=form)
@interaction_bp.route('/specialist_magic_links', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def specialist_magic_links():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
query = SpecialistMagicLink.query.order_by(SpecialistMagicLink.id)
pagination = query.paginate(page=page, per_page=per_page)
the_specialist_magic_links = pagination.items
# prepare table data
rows = prepare_table_for_macro(the_specialist_magic_links, [('id', ''), ('name', ''), ('magic_link_code', ''),])
# Render the catalogs in a template
return render_template('interaction/specialist_magic_links.html', rows=rows, pagination=pagination)
@interaction_bp.route('/handle_specialist_magic_link_selection', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_specialist_magic_link_selection():
action = request.form.get('action')
if action == 'create_specialist_magic_link':
return redirect(prefixed_url_for('interaction_bp.specialist_magic_link'))
specialist_ml_identification = request.form.get('selected_row')
specialist_ml_id = ast.literal_eval(specialist_ml_identification).get('value')
if action == "edit_specialist_magic_link":
return redirect(prefixed_url_for('interaction_bp.edit_specialist_magic_link',
specialist_magic_link_id=specialist_ml_id))
return redirect(prefixed_url_for('interaction_bp.specialists'))

View File

@@ -161,19 +161,19 @@ def edit_partner_service(partner_service_id):
partner_service = PartnerService.query.get_or_404(partner_service_id)
partner = session.get('partner', None)
partner_id = session['partner']['id']
current_app.logger.debug(f"Request Type: {request.method}")
form = EditPartnerServiceForm(obj=partner_service)
if request.method == 'GET':
partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
partner_service.type_version)
configuration_config = partner_service_config.get('configuration')
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
f"{configuration_config}")
form.add_dynamic_fields("configuration", configuration_config, partner_service.configuration)
permissions_config = partner_service_config.get('permissions')
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
f"{permissions_config}")
form.add_dynamic_fields("permissions", permissions_config, partner_service.permissions)
partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
partner_service.type_version)
configuration_config = partner_service_config.get('configuration')
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
f"{configuration_config}")
form.add_dynamic_fields("configuration", partner_service_config, partner_service.configuration)
permissions_config = partner_service_config.get('permissions')
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
f"{permissions_config}")
form.add_dynamic_fields("permissions", partner_service_config, partner_service.permissions)
if request.method == 'POST':
current_app.logger.debug(f"Form returned: {form.data}")

View File

@@ -36,7 +36,7 @@ class TenantForm(FlaskForm):
# initialise currency field
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
# initialise timezone
self.timezone.choices = [(tz, tz) for tz in pytz.all_timezones]
self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones]
# Initialize fallback algorithms
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
# Show field only for Super Users with partner in session