- Move global config files to globals iso global folder, as the name global conflicts with python language

- Creation of Traicie Vancancy Definition specialist
- Allow to invoke non-interaction specialists from withing Evie's mgmt interface (eveai_app)
- Improvements to crewai specialized classes
- Introduction to json editor for showing specialists arguments and results in a better way
- Introduction of more complex pagination (adding extra arguments) by adding a global 'get_pagination_html'
- Allow follow-up of ChatSession / Specialist execution
- Improvement in logging of Specialists (but needs to be finished)
This commit is contained in:
Josako
2025-05-26 11:26:03 +02:00
parent d789e431ca
commit 1fdbd2ff45
94 changed files with 1657 additions and 443 deletions

View File

@@ -81,6 +81,8 @@ class DynamicFormBase(FlaskForm):
message=f"Value must be between {min_value or '-∞'} and {max_value or ''}"
)
)
elif field_type in ['string', 'str']:
validators_list.append(self._validate_string_not_empty)
elif field_type == 'tagging_fields':
validators_list.append(self._validate_tagging_fields)
elif field_type == 'tagging_fields_filter':
@@ -130,6 +132,11 @@ class DynamicFormBase(FlaskForm):
except Exception as e:
raise ValidationError(f"Invalid filter definition: {str(e)}")
def _validate_string_not_empty(self, form, field):
"""Validator om te controleren of een StringField niet leeg is"""
if not field.data or field.data.strip() == "":
raise ValidationError("This field cannot be empty")
def _validate_filter_condition(self, condition):
"""Recursively validate a filter condition structure"""
# Check if this is a logical condition (AND/OR/NOT)
@@ -264,6 +271,7 @@ class DynamicFormBase(FlaskForm):
'float': FloatField,
'boolean': BooleanField,
'string': StringField,
'str': StringField,
'text': TextAreaField,
'date': DateField,
'file': FileField,
@@ -368,6 +376,28 @@ class DynamicFormBase(FlaskForm):
data[original_field_name] = field.data
return data
# def validate_on_submit(self):
# """Aangepaste validate_on_submit die dynamische velden correct verwerkt"""
# if request.method == 'POST':
# # Update formdata met de huidige request data
# self.formdata = request.form
# self.raw_formdata = request.form.to_dict()
#
# # Verwerk alle dynamische velden opnieuw met de actuele formuliergegevens
# for collection_name, field_names in self.dynamic_fields.items():
# for full_field_name in field_names:
# field = self._fields.get(full_field_name)
# if field:
# # Log voor debug
# current_app.logger.debug(
# f"Re-processing dynamic field {full_field_name} with current form data")
# # Verwerk het veld opnieuw met de huidige request data
# field.process(self.formdata)
#
# # Nu voeren we de standaard validatie uit
# return super().validate()
# return False
def validate_tagging_fields(form, field):
"""Validate the tagging fields structure"""

View File

@@ -1,6 +1,7 @@
from flask_wtf import FlaskForm
from wtforms import (StringField, BooleanField, SelectField, TextAreaField)
from wtforms.fields.datetime import DateField
from wtforms.fields.numeric import IntegerField
from wtforms.validators import DataRequired, Length, Optional
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
@@ -124,3 +125,11 @@ class EditEveAIAssetVersionForm(DynamicFormBase):
asset_type_version = StringField('Asset Type Version', validators=[DataRequired()], render_kw={'readonly': True})
bucket_name = StringField('Bucket Name', validators=[DataRequired()], render_kw={'readonly': True})
class ExecuteSpecialistForm(DynamicFormBase):
id = IntegerField('Specialist ID', validators=[DataRequired()], render_kw={'readonly': True})
name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True})
description = TextAreaField('Specialist Description', validators=[Optional()], render_kw={'readonly': True})

View File

@@ -1,5 +1,7 @@
import ast
import json
from datetime import datetime as dt, timezone as tz
import time
from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify, url_for
from flask_security import roles_accepted
@@ -14,7 +16,9 @@ from common.models.interaction import (ChatSession, Interaction, InteractionEmbe
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion)
from common.extensions import db, cache_manager
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
from common.utils.model_logging_utils import set_logging_information, update_logging_information
from common.utils.middleware import mw_before_request
@@ -25,7 +29,7 @@ from common.utils.specialist_utils import initialize_specialist
from config.type_defs.specialist_types import SPECIALIST_TYPES
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm)
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm)
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
@@ -72,10 +76,13 @@ def handle_chat_session_selection():
cs_id = ast.literal_eval(chat_session_identification).get('value')
action = request.form['action']
current_app.logger.debug(f'Handle Chat Session Selection Action: {action}')
match action:
case 'view_chat_session':
return redirect(prefixed_url_for('interaction_bp.view_chat_session', chat_session_id=cs_id))
case 'chat_session_interactions':
return redirect(prefixed_url_for('interaction_bp.session_interactions', chat_session_id=cs_id))
# Add more conditions for other actions
return redirect(prefixed_url_for('interaction_bp.chat_sessions'))
@@ -124,8 +131,14 @@ def view_chat_session(chat_session_id):
@interaction_bp.route('/view_chat_session_by_session_id/<session_id>', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_chat_session_by_session_id(session_id):
"""
Deze route accepteert een session_id (string) en stuurt door naar view_chat_session met het juiste chat_session_id (integer)
"""
# Vind de chat session op basis van session_id
chat_session = ChatSession.query.filter_by(session_id=session_id).first_or_404()
show_chat_session(chat_session)
# Nu we het chat_session object hebben, kunnen we de bestaande functie hergebruiken
return view_chat_session(chat_session.id)
def show_chat_session(chat_session):
@@ -303,6 +316,8 @@ def handle_specialist_selection():
if action == "edit_specialist":
return redirect(prefixed_url_for('interaction_bp.edit_specialist', specialist_id=specialist_id))
elif action == "execute_specialist":
return redirect(prefixed_url_for('interaction_bp.execute_specialist', specialist_id=specialist_id))
return redirect(prefixed_url_for('interaction_bp.specialists'))
@@ -391,9 +406,8 @@ def edit_tool(tool_id):
form = EditEveAIToolForm(obj=tool)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return render_template('interaction/components/edit_tool.html',
form=form,
tool=tool)
return render_template('interaction/components/edit_tool.html', form=form, tool=tool)
return None
@interaction_bp.route('/tool/<int:tool_id>/save', methods=['POST'])
@@ -546,3 +560,106 @@ def edit_asset_version(asset_version_id):
return render_template('interaction/edit_asset_version.html', form=form)
@interaction_bp.route('/execute_specialist/<int:specialist_id>', methods=['GET', 'POST'])
def execute_specialist(specialist_id):
specialist = Specialist.query.get_or_404(specialist_id)
specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
if specialist_config.get('chat', True):
flash("Only specialists that don't require interactions can be executed this way!", 'error')
return redirect(prefixed_url_for('interaction_bp.specialists'))
form = ExecuteSpecialistForm(request.form, obj=specialist)
arguments_config = specialist_config.get('arguments', None)
if arguments_config:
form.add_dynamic_fields('arguments', arguments_config)
if form.validate_on_submit():
# We're only interested in gathering the dynamic arguments
arguments = form.get_dynamic_data("arguments")
current_app.logger.debug(f"Executing specialist {specialist.id} with arguments: {arguments}")
session_id = SpecialistServices.start_session()
execution_task = SpecialistServices.execute_specialist(
tenant_id=session.get('tenant').get('id'),
specialist_id=specialist_id,
specialist_arguments=arguments,
session_id=session_id,
user_timezone=session.get('tenant').get('timezone')
)
current_app.logger.debug(f"Execution task for specialist {specialist.id} created: {execution_task}")
return redirect(prefixed_url_for('interaction_bp.session_interactions_by_session_id', session_id=session_id))
return render_template('interaction/execute_specialist.html', form=form)
@interaction_bp.route('/session_interactions_by_session_id/<session_id>', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def session_interactions_by_session_id(session_id):
"""
This route shows all interactions for a given session_id (string).
If the chat_session doesn't exist yet, it will wait for up to 10 seconds
(with 1 second intervals) until it becomes available.
"""
waiting_message = request.args.get('waiting', 'false') == 'true'
# Try up to 10 times with 1 second pause
max_tries = 10
current_try = 1
while current_try <= max_tries:
chat_session = ChatSession.query.filter_by(session_id=session_id).first()
if chat_session:
# Session found, display the interactions
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
query = Interaction.query.filter_by(chat_session_id=chat_session.id).order_by(Interaction.question_at)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
interactions = pagination.items
rows = prepare_table_for_macro(interactions, [('id', ''), ('question_at', ''), ('detailed_question_at', ''),
('answer_at', ''), ('processing_error', '')])
# Define a callback to make a URL for a given page and the same session_id
def make_page_url(page_num):
return prefixed_url_for('interaction_bp.session_interactions_by_session_id', session_id=session_id,
page=page_num)
return render_template('interaction/session_interactions.html',
chat_session=chat_session, rows=rows, pagination=pagination,
make_page_url=make_page_url)
# Session not found, wait and try again
if current_try < max_tries:
current_try += 1
time.sleep(1)
else:
# Maximum number of attempts reached
break
# If we're here, the session wasn't found after the maximum number of attempts
flash(f'The chat session with ID {session_id} could not be found after {max_tries} attempts. '
f'The session may still be in the process of being created or the ID might be incorrect.', 'warning')
# Show a waiting page with auto-refresh if we haven't shown a waiting message yet
if not waiting_message:
return render_template('interaction/waiting_for_session.html',
session_id=session_id,
refresh_url=prefixed_url_for('interaction_bp.session_interactions_by_session_id',
session_id=session_id,
waiting='true'))
# If we've already shown a waiting message and still don't have a session, go back to the specialists page
return redirect(prefixed_url_for('interaction_bp.specialists'))
@interaction_bp.route('/session_interactions/<chat_session_id>', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def session_interactions(chat_session_id):
"""
This route shows all interactions for a given chat_session_id (int).
"""
chat_session = ChatSession.query.get_or_404(chat_session_id)
return session_interactions_by_session_id(chat_session.session_id)

View File

@@ -163,22 +163,24 @@ def edit_partner_service(partner_service_id):
partner_id = session['partner']['id']
form = EditPartnerServiceForm(obj=partner_service)
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)
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)
if form.validate_on_submit():
if request.method == 'POST':
current_app.logger.debug(f"Form returned: {form.data}")
raw_form_data = request.form.to_dict()
current_app.logger.debug(f"Raw form data: {raw_form_data}")
if form.validate_on_submit():
form.populate_obj(partner_service)
partner_service.configuration = form.get_dynamic_data('configuration')
partner_service.permissions = form.get_dynamic_data('permissions')

View File

@@ -37,8 +37,6 @@ class TenantForm(FlaskForm):
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]
# initialise LLM fields
self.llm_model.choices = [(model, model) for model in current_app.config['SUPPORTED_LLMS']]
# 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

View File

@@ -302,7 +302,6 @@ def handle_tenant_selection():
# set tenant information in the session
session['tenant'] = the_tenant.to_dict()
session['default_language'] = the_tenant.default_language
session['llm_model'] = the_tenant.llm_model
# remove catalog-related items from the session
session.pop('catalog_id', None)
session.pop('catalog_name', None)