- 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

@@ -15,7 +15,7 @@ import common.models.document
from common.utils.startup_eveai import perform_startup_actions
from config.logging_config import LOGGING
from common.utils.security import set_tenant_session_data
from .errors import register_error_handlers
from common.utils.errors import register_error_handlers
from common.utils.celery_utils import make_celery, init_celery
from common.utils.template_filters import register_filters
from config.config import get_config

View File

@@ -1,119 +0,0 @@
import traceback
import jinja2
from flask import render_template, request, jsonify, redirect, current_app, flash
from flask_login import current_user
from common.utils.eveai_exceptions import EveAINoSessionTenant
from common.utils.nginx_utils import prefixed_url_for
def not_found_error(error):
if not current_user.is_authenticated:
return redirect(prefixed_url_for('security.login'))
current_app.logger.error(f"Not Found Error: {error}")
return render_template('error/404.html'), 404
def internal_server_error(error):
if not current_user.is_authenticated:
return redirect(prefixed_url_for('security.login'))
current_app.logger.error(f"Internal Server Error: {error}")
return render_template('error/500.html'), 500
def not_authorised_error(error):
if not current_user.is_authenticated:
return redirect(prefixed_url_for('security.login'))
current_app.logger.error(f"Not Authorised Error: {error}")
return render_template('error/401.html')
def access_forbidden(error):
if not current_user.is_authenticated:
return redirect(prefixed_url_for('security.login'))
current_app.logger.error(f"Access Forbidden: {error}")
return render_template('error/403.html')
def key_error_handler(error):
# Check if the KeyError is specifically for 'tenant'
if str(error) == "'tenant'":
return redirect(prefixed_url_for('security.login'))
# For other KeyErrors, you might want to log the error and return a generic error page
current_app.logger.error(f"Key Error: {error}")
return render_template('error/generic.html', error_message="An unexpected error occurred"), 500
def attribute_error_handler(error):
"""Handle AttributeError exceptions.
Specifically catches SQLAlchemy relationship errors when string IDs
are used instead of model instances.
"""
error_msg = str(error)
current_app.logger.error(f"AttributeError: {error_msg}")
current_app.logger.error(traceback.format_exc())
# Handle the SQLAlchemy relationship error specifically
if "'str' object has no attribute '_sa_instance_state'" in error_msg:
flash('Database relationship error. Please check your form inputs and try again.', 'error')
return render_template('error/500.html',
error_type="Relationship Error",
error_details="A string value was provided where a database object was expected."), 500
# Handle other AttributeErrors
flash('An application error occurred. The technical team has been notified.', 'error')
return render_template('error/500.html',
error_type="Attribute Error",
error_details=error_msg), 500
def no_tenant_selected_error(error):
"""Handle errors when no tenant is selected in the current session.
This typically happens when a session expires or becomes invalid after
a long period of inactivity. The user will be redirected to the login page.
"""
current_app.logger.error(f"No Session Tenant Error: {error}")
flash('Your session expired. You will have to re-enter your credentials', 'warning')
# Perform logout if user is authenticated
if current_user.is_authenticated:
from flask_security.utils import logout_user
logout_user()
# Redirect to login page
return redirect(prefixed_url_for('security.login'))
def general_exception(e):
current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True)
flash('An application error occurred. The technical team has been notified.', 'error')
return render_template('error/500.html',
error_type=type(e).__name__,
error_details=str(e)), 500
def register_error_handlers(app):
app.register_error_handler(404, not_found_error)
app.register_error_handler(500, internal_server_error)
app.register_error_handler(401, not_authorised_error)
app.register_error_handler(403, not_authorised_error)
app.register_error_handler(EveAINoSessionTenant, no_tenant_selected_error)
app.register_error_handler(KeyError, key_error_handler)
app.register_error_handler(AttributeError, attribute_error_handler)
app.register_error_handler(Exception, general_exception)
@app.errorhandler(jinja2.TemplateNotFound)
def template_not_found(error):
app.logger.error(f'Template not found: {error.name}')
app.logger.error(f'Search Paths: {app.jinja_loader.list_templates()}')
return f'Template not found: {error.name}. Check logs for details.', 404
@app.errorhandler(jinja2.TemplateSyntaxError)
def template_syntax_error(error):
app.logger.error(f'Template syntax error: {error.message}')
app.logger.error(f'In template {error.filename}, line {error.lineno}')
return f'Template syntax error: {error.message}', 500

View File

@@ -19,17 +19,17 @@
<div class="nav-wrapper position-relative end-0">
<ul class="nav nav-pills nav-fill p-1" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link mb-0 px-0 py-1 active" data-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
Storage
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
Embedding
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
Interaction
</a>
</li>

View File

@@ -19,17 +19,17 @@
<div class="nav-wrapper position-relative end-0">
<ul class="nav nav-pills nav-fill p-1" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link mb-0 px-0 py-1 active" data-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
Storage
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
Embedding
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
Interaction
</a>
</li>

View File

@@ -107,17 +107,17 @@
<!-- Nav Tabs -->
<ul class="nav nav-tabs" id="periodTabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="status-tab" data-toggle="tab" href="#status" role="tab">
<a class="nav-link active" id="status-tab" data-bs-toggle="tab" href="#status" role="tab">
Status & Timeline
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="usage-tab" data-toggle="tab" href="#usage" role="tab">
<a class="nav-link" id="usage-tab" data-bs-toggle="tab" href="#usage" role="tab">
Usage
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="financial-tab" data-toggle="tab" href="#financial" role="tab">
<a class="nav-link" id="financial-tab" data-bs-toggle="tab" href="#financial" role="tab">
Financial
</a>
</li>

View File

@@ -1,3 +1,4 @@
{% extends 'base.html' %}
{% from "macros.html" import render_field, render_included_field %}
@@ -19,17 +20,17 @@
<div class="nav-wrapper position-relative end-0">
<ul class="nav nav-pills nav-fill p-1" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link mb-0 px-0 py-1 active" data-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#storage-tab" role="tab" aria-controls="storage-tab" aria-selected="true">
Storage
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#embedding-tab" role="tab" aria-controls="embedding-tab" aria-selected="false">
Embedding
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#interaction-tab" role="tab" aria-controls="interaction-tab" aria-selected="false">
Interaction
</a>
</li>
@@ -68,4 +69,4 @@
{% block content_footer %}
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% from "macros.html" import render_field %}
{% block title %}Edit Specialist Magic Link{% endblock %}
{% block content_title %}Edit Specialist Magic Link{% endblock %}
{% block content_description %}Edit a Specialist Magic Link{% endblock %}
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = ['magic_link_code'] %}
{% set exclude_fields = [] %}
<!-- Render Static Fields -->
{% for field in form.get_static_fields() %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
<!-- Render Dynamic Fields -->
{% for collection_name, fields in form.get_dynamic_fields().items() %}
{% if fields|length > 0 %}
<h4 class="mt-4">{{ collection_name }}</h4>
{% endif %}
{% for field in fields %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
{% endfor %}
<button type="submit" class="btn btn-primary">Save Specialist Magic Link</button>
</form>
{% endblock %}
{% block content_footer %}
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% from "macros.html" import render_field %}
{% block title %}Specialist Magic Link{% endblock %}
{% block content_title %}Register Specialist Magic Link{% endblock %}
{% block content_description %}Define a new specialist magic link{% endblock %}
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = [] %}
{% set exclude_fields = [] %}
{% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
<button type="submit" class="btn btn-primary">Register Specialist Magic Link</button>
</form>
{% endblock %}
{% block content_footer %}
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Specialist Magic Links{% endblock %}
{% block content_title %}Specialist Magic Links{% endblock %}
{% block content_description %}View Specialists Magic Links{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<div class="container">
<form method="POST" action="{{ url_for('interaction_bp.handle_specialist_magic_link_selection') }}" id="specialistMagicLinksForm">
{{ render_selectable_table(headers=["Specialist ML ID", "Name", "Magic Link Code"], rows=rows, selectable=True, id="specialistMagicLinksTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_specialist_magic_link" class="btn btn-primary" onclick="return validateTableSelection('specialistMagicLinksForm')">Edit Specialist Magic Link</button>
</div>
<button type="submit" name="action" value="create_specialist_magic_link" class="btn btn-success">Register Specialist Magic Link</button>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'interaction_bp.specialist_magic_links') }}
{% endblock %}

View File

@@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Retrievers{% endblock %}
{% block title %}Specialists{% endblock %}
{% block content_title %}Specialists{% endblock %}
{% block content_description %}View Specialists for Tenant{% endblock %}

View File

@@ -138,7 +138,7 @@
{% elif cell.type == 'badge' %}
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
{% elif cell.type == 'link' %}
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
{% else %}
{{ cell.value }}
{% endif %}
@@ -192,7 +192,7 @@
{% elif cell.type == 'badge' %}
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
{% elif cell.type == 'link' %}
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
{% else %}
{{ cell.value }}
{% endif %}
@@ -357,7 +357,7 @@
{% elif cell.type == 'badge' %}
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
{% elif cell.type == 'link' %}
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
{% else %}
{{ cell.value }}
{% endif %}
@@ -450,3 +450,10 @@
</div>
</div>
{% endmacro %}
{% macro debug_to_console(var_name, var_value) %}
<script>
console.log('{{ var_name }}:', {{ var_value|tojson }});
</script>
{% endmacro %}

View File

@@ -106,6 +106,7 @@
{% if current_user.is_authenticated %}
{{ dropdown('Interactions', 'hub', [
{'name': 'Specialists', 'url': '/interaction/specialists', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Specialist Magic Links', 'url': '/interaction/specialist_magic_links', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Chat Sessions', 'url': '/interaction/chat_sessions', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
]) }}
{% endif %}

View File

@@ -1,9 +1,9 @@
{% extends "base.html" %}
{% from "macros.html" import render_field %}
{% block title %}Register Partner Service{% endblock %}
{% from "macros.html" import render_field, debug_to_console %}
{% block title %}Edit Partner Service{% endblock %}
{% block content_title %}Register Partner Service{% endblock %}
{% block content_description %}Register Partner Service{% endblock %}
{% block content_title %}Edit Partner Service{% endblock %}
{% block content_description %}Edit Partner Service{% endblock %}
{% block content %}
<form method="post">
@@ -16,6 +16,8 @@
{% endfor %}
<!-- Render Dynamic Fields -->
{% for collection_name, fields in form.get_dynamic_fields().items() %}
{{ debug_to_console('collection_name', collection_name) }}
{{ debug_to_console('fields', fields) }}
{% if fields|length > 0 %}
<h4 class="mt-4">{{ collection_name }}</h4>
{% endif %}
@@ -23,6 +25,6 @@
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
{% endfor %}
<button type="submit" class="btn btn-primary">Register Partner Service</button>
<button type="submit" class="btn btn-primary">Save Partner Service</button>
</form>
{% endblock %}

View File

@@ -19,3 +19,37 @@
{% endblock %}
{% block content_footer %} {% endblock %}
{% block scripts %}
<script>
// JavaScript om de gebruiker's timezone te detecteren
document.addEventListener('DOMContentLoaded', (event) => {
// Detect timezone
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Send timezone to the server via a POST request
fetch('/set_user_timezone', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ timezone: userTimezone })
}).then(response => {
if (response.ok) {
console.log('Timezone sent to server successfully');
} else {
console.error('Failed to send timezone to server');
}
});
// Initialiseer Select2 voor timezone selectie
$('#timezone').select2({
placeholder: 'Selecteer een timezone...',
allowClear: true,
maximumSelectionLength: 10,
theme: 'bootstrap',
width: '100%'
});
});
</script>
{% endblock %}

View File

@@ -26,7 +26,7 @@
{% block scripts %}
<script>
// JavaScript to detect user's timezone
// JavaScript om de gebruiker's timezone te detecteren
document.addEventListener('DOMContentLoaded', (event) => {
// Detect timezone
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -45,6 +45,31 @@
console.error('Failed to send timezone to server');
}
});
$('#timezone').select2({
placeholder: 'Selecteer een timezone...',
allowClear: true,
theme: 'bootstrap',
width: '100%',
dropdownAutoWidth: true,
dropdownCssClass: 'timezone-dropdown', // Een custom class voor specifieke styling
scrollAfterSelect: false,
// Verbeterd scroll gedrag
dropdownParent: $('body')
});
// Stel de huidige waarde in als de dropdown wordt geopend
$('#timezone').on('select2:open', function() {
if ($(this).val()) {
setTimeout(function() {
let selectedOption = $('.select2-results__option[aria-selected=true]');
if (selectedOption.length) {
selectedOption[0].scrollIntoView({ behavior: 'auto', block: 'center' });
}
}, 0);
}
});
});
</script>
{% endblock %}

View File

@@ -21,7 +21,7 @@
<div class="nav-wrapper position-relative end-0">
<ul class="nav nav-pills nav-fill p-1" role="tablist">
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#license-info-tab" role="tab" aria-controls="license-info" aria-selected="false">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#license-info-tab" role="tab" aria-controls="license-info" aria-selected="false">
License Information
</a>
</li>

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