import ast import json import uuid from datetime import datetime as dt, timezone as tz import time from common.extensions import minio_client from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify, url_for from flask_security import roles_accepted, current_user from langchain.agents import Agent from sqlalchemy import desc from sqlalchemy.exc import SQLAlchemyError from werkzeug.datastructures import FileStorage 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, EveAIAsset, SpecialistMagicLink, EveAIDataCapsule) from common.extensions import db, cache_manager from common.models.user import SpecialistMagicLinkTenant from common.services.interaction.specialist_services import SpecialistServices from common.utils.execution_progress import ExecutionProgressTracker from common.utils.minio_utils import MIB_CONVERTOR from common.utils.model_logging_utils import set_logging_information, update_logging_information from common.utils.middleware import mw_before_request 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, ExecuteSpecialistForm, SpecialistMagicLinkForm, EditSpecialistMagicLinkForm, ViewSpecialistMagicLinkURLsForm) from eveai_app.views.list_views.interaction_list_views import (get_specialists_list_view, get_assets_list_view, get_magic_links_list_view, get_chat_sessions_list_view, get_eveai_data_capsules_list_view) from eveai_app.views.list_views.list_view_utils import render_list_view interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction') @interaction_bp.before_request def log_before_request(): current_app.logger.debug(f'Before request: {request.path} =====================================') @interaction_bp.after_request def log_after_request(response): return response # Routes for Chat Session Management -------------------------------------------------------------- @interaction_bp.before_request def before_request(): try: mw_before_request() except Exception as e: current_app.logger.error(f'Error switching schema in Interaction Blueprint: {e}') raise @interaction_bp.route('/chat_sessions', methods=['GET', 'POST']) def chat_sessions(): # Get configuration and render the list view config = get_chat_sessions_list_view() return render_list_view('list_view.html', **config) @interaction_bp.route('/handle_chat_session_selection', methods=['POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def handle_chat_session_selection(): chat_session_identification = request.form['selected_row'] cs_id = ast.literal_eval(chat_session_identification).get('value') action = request.form['action'] match action: case 'view_chat_session': return redirect(prefixed_url_for('interaction_bp.view_chat_session', chat_session_id=cs_id, for_redirect=True)) case 'chat_session_interactions': return redirect(prefixed_url_for('interaction_bp.session_interactions', chat_session_id=cs_id, for_redirect=True)) # Add more conditions for other actions return redirect(prefixed_url_for('interaction_bp.chat_sessions', for_redirect=True)) @interaction_bp.route('/view_chat_session/', methods=['GET']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def view_chat_session(chat_session_id): # Get chat session with user info chat_session = ChatSession.query.get_or_404(chat_session_id) # Get interactions with specialist info interactions = (Interaction.query .filter_by(chat_session_id=chat_session.id) .join(Specialist, Interaction.specialist_id == Specialist.id, isouter=True) .add_columns( Interaction.id, Interaction.question_at, Interaction.specialist_arguments, Interaction.specialist_results, Specialist.name.label('specialist_name'), Specialist.type.label('specialist_type') ).order_by(Interaction.question_at).all()) # Fetch all related embeddings for the interactions in this session embedding_query = (db.session.query(InteractionEmbedding.interaction_id, DocumentVersion.url, DocumentVersion.object_name) .join(Embedding, InteractionEmbedding.embedding_id == Embedding.id) .join(DocumentVersion, Embedding.doc_vers_id == DocumentVersion.id) .filter(InteractionEmbedding.interaction_id.in_([i.id for i, *_ in interactions]))) # Create a dictionary to store embeddings for each interaction embeddings_dict = {} for interaction_id, url, object_name in embedding_query: if interaction_id not in embeddings_dict: embeddings_dict[interaction_id] = [] embeddings_dict[interaction_id].append({'url': url, 'object_name': object_name}) return render_template('interaction/view_chat_session.html', chat_session=chat_session, interactions=interactions, embeddings_dict=embeddings_dict) @interaction_bp.route('/view_chat_session_by_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() # 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): interactions = Interaction.query.filter_by(chat_session_id=chat_session.id).all() return render_template('interaction/view_chat_session.html', chat_session=chat_session, interactions=interactions) # Routes for Specialist Management ---------------------------------------------------------------- @interaction_bp.route('/specialist', methods=['GET', 'POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def specialist(): form = SpecialistForm() if form.validate_on_submit(): tenant_id = session.get('tenant').get('id') try: new_specialist = Specialist() # Populate fields individually instead of using populate_obj (gives problem with QueryMultipleSelectField) new_specialist.name = form.name.data new_specialist.type = form.type.data new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version( new_specialist.type) new_specialist.active = form.active.data new_specialist.tuning = form.tuning.data set_logging_information(new_specialist, dt.now(tz.utc)) db.session.add(new_specialist) db.session.flush() # This assigns the ID to the specialist without committing the transaction # Create the retriever associations selected_retrievers = form.retrievers.data for retriever in selected_retrievers: specialist_retriever = SpecialistRetriever( specialist_id=new_specialist.id, retriever_id=retriever.id ) db.session.add(specialist_retriever) # Commit everything in one transaction db.session.commit() flash('Specialist successfully added!', 'success') current_app.logger.info(f'Specialist {new_specialist.name} successfully added for tenant {tenant_id}!') # Initialize the newly create specialist SpecialistServices.initialize_specialist(new_specialist.id, new_specialist.type, new_specialist.type_version) return redirect(prefixed_url_for('interaction_bp.edit_specialist', specialist_id=new_specialist.id, for_redirect=True)) except Exception as e: db.session.rollback() current_app.logger.error(f'Failed to add specialist. Error: {str(e)}', exc_info=True) flash(f'Failed to add specialist. Error: {str(e)}', 'danger') return render_template('interaction/specialist.html', form=form) return render_template('interaction/specialist.html', form=form) @interaction_bp.route('/specialist/', methods=['GET', 'POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def edit_specialist(specialist_id): specialist = Specialist.query.get_or_404(specialist_id) form = EditSpecialistForm(request.form, obj=specialist) specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version) form.add_dynamic_fields("configuration", specialist_config, specialist.configuration) agent_rows = prepare_table_for_macro(specialist.agents, [('id', ''), ('name', ''), ('type', ''), ('type_version', '')]) task_rows = prepare_table_for_macro(specialist.tasks, [('id', ''), ('name', ''), ('type', ''), ('type_version', '')]) tool_rows = prepare_table_for_macro(specialist.tools, [('id', ''), ('name', ''), ('type', ''), ('type_version', '')]) # Construct the SVG overview path svg_filename = f"{specialist.type}_{specialist.type_version}_overview.svg" svg_path = url_for('static', filename=f'assets/specialists/{svg_filename}') if request.method == 'GET': # Get the actual Retriever objects for the associated retriever_ids retriever_objects = Retriever.query.filter( Retriever.id.in_([sr.retriever_id for sr in specialist.retrievers]) ).all() form.retrievers.data = retriever_objects if specialist.description is None: specialist.description = specialist_config.get('metadata').get('description', '') if form.validate_on_submit(): # Update the basic fields specialist.name = form.name.data specialist.description = form.description.data specialist.tuning = form.tuning.data specialist.active = form.active.data # Update the configuration dynamic fields specialist.configuration = form.get_dynamic_data("configuration") # Get current and selected retrievers current_retrievers = {sr.retriever_id: sr for sr in specialist.retrievers} selected_retrievers = {r.id: r for r in form.retrievers.data} # Remove unselected retrievers for retriever_id in set(current_retrievers.keys()) - set(selected_retrievers.keys()): specialist_retriever = current_retrievers[retriever_id] db.session.delete(specialist_retriever) # Add new retrievers for retriever_id in set(selected_retrievers.keys()) - set(current_retrievers.keys()): specialist_retriever = SpecialistRetriever( specialist_id=specialist.id, retriever_id=retriever_id ) db.session.add(specialist_retriever) # Update logging information update_logging_information(specialist, dt.now(tz.utc)) try: db.session.commit() flash('Specialist updated successfully!', 'success') current_app.logger.info(f'Specialist {specialist.id} updated successfully') return redirect(prefixed_url_for('interaction_bp.specialists', for_redirect=True)) except SQLAlchemyError as e: db.session.rollback() flash(f'Failed to update specialist. Error: {str(e)}', 'danger') current_app.logger.error(f'Failed to update specialist {specialist_id}. Error: {str(e)}') # On error, re-render with components list config from eveai_app.views.list_views.interaction_list_views import get_specialist_components_list_view components_config = get_specialist_components_list_view(specialist) return render_template('interaction/edit_specialist.html', form=form, specialist_id=specialist_id, components_title=components_config.get('title'), components_data=components_config.get('data'), components_columns=components_config.get('columns'), components_actions=components_config.get('actions'), components_initial_sort=components_config.get('initial_sort'), components_table_id=components_config.get('table_id'), components_table_height=components_config.get('table_height'), components_description=components_config.get('description'), components_index=components_config.get('index'), prefixed_url_for=prefixed_url_for, svg_path=svg_path, ) else: form_validation_failed(request, form) # Build combined components list view config for embedding from eveai_app.views.list_views.interaction_list_views import get_specialist_components_list_view components_config = get_specialist_components_list_view(specialist) return render_template('interaction/edit_specialist.html', form=form, specialist_id=specialist_id, components_title=components_config.get('title'), components_data=components_config.get('data'), components_columns=components_config.get('columns'), components_actions=components_config.get('actions'), components_initial_sort=components_config.get('initial_sort'), components_table_id=components_config.get('table_id'), components_table_height=components_config.get('table_height'), components_description=components_config.get('description'), components_index=components_config.get('index'), prefixed_url_for=prefixed_url_for, svg_path=svg_path, ) @interaction_bp.route('/specialists', methods=['GET', 'POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def specialists(): # Get configuration and render the list view config = get_specialists_list_view() return render_list_view('list_view.html', **config) @interaction_bp.route('/handle_specialist_selection', methods=['POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def handle_specialist_selection(): action = request.form.get('action') if action == 'create_specialist': return redirect(prefixed_url_for('interaction_bp.specialist', for_redirect=True)) specialist_identification = request.form.get('selected_row') specialist_id = ast.literal_eval(specialist_identification).get('value') if action == "edit_specialist": return redirect(prefixed_url_for('interaction_bp.edit_specialist', specialist_id=specialist_id, for_redirect=True)) elif action == "execute_specialist": return redirect(prefixed_url_for('interaction_bp.execute_specialist', specialist_id=specialist_id, for_redirect=True)) return redirect(prefixed_url_for('interaction_bp.specialists', for_redirect=True)) @interaction_bp.route('/specialist//components_data', methods=['GET']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def specialist_components_data(specialist_id): """Return JSON data for the specialist's combined components list (agents, tasks, tools).""" specialist = Specialist.query.get_or_404(specialist_id) from eveai_app.views.list_views.interaction_list_views import get_specialist_components_list_view config = get_specialist_components_list_view(specialist) return jsonify({'data': config.get('data', [])}) # Routes for Agent management --------------------------------------------------------------------- @interaction_bp.route('/agent//edit', methods=['GET']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def edit_agent(agent_id): agent = EveAIAgent.query.get_or_404(agent_id) form = EditEveAIAgentForm(obj=agent) if request.headers.get('X-Requested-With') == 'XMLHttpRequest': # Determine defaults for reset button if available enable_reset_defaults = False model_default = '' temperature_default = '' if getattr(form, '_agent_config', None): model_default = form._agent_config.get('full_model_name', 'mistral.mistral-medium-latest') temperature_default = form._agent_config.get('temperature', 0.7) enable_reset_defaults = True # Return just the form portion for AJAX requests return render_template('interaction/components/edit_agent.html', form=form, agent=agent, title="Edit Agent", description="Configure the agent with company-specific details if required", submit_text="Save Agent", enable_reset_defaults=enable_reset_defaults, model_default=model_default, temperature_default=temperature_default) return None @interaction_bp.route('/agent//save', methods=['POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def save_agent(agent_id): current_app.logger.info(f'Trying to save agent {agent_id} -------------------------------------------') agent = EveAIAgent.query.get_or_404(agent_id) if agent_id else EveAIAgent() tenant_id = session.get('tenant').get('id') form = EditEveAIAgentForm(formdata=request.form, obj=agent) if form.validate_on_submit(): try: form.populate_obj(agent) update_logging_information(agent, dt.now(tz.utc)) if not agent_id: # New agent db.session.add(agent) db.session.commit() return jsonify({'success': True, 'message': 'Agent saved successfully'}) except Exception as e: db.session.rollback() current_app.logger.error(f'Failed to save agent {agent_id} for tenant {tenant_id}. Error: {str(e)}') return jsonify({'success': False, 'message': f"Failed to save agent {agent_id}: {str(e)}"}) else: # On validation errors, return the editor partial HTML so the frontend can display inline errors in the modal form_validation_failed(request, form) enable_reset_defaults = False model_default = '' temperature_default = '' if getattr(form, '_agent_config', None): model_default = form._agent_config.get('full_model_name', 'mistral.mistral-medium-latest') temperature_default = form._agent_config.get('temperature', 0.7) enable_reset_defaults = True return render_template('interaction/components/edit_agent.html', form=form, agent=agent, title="Edit Agent", description="Configure the agent with company-specific details if required", submit_text="Save Agent", enable_reset_defaults=enable_reset_defaults, model_default=model_default, temperature_default=temperature_default), 400 # Routes for Task management ---------------------------------------------------------------------- @interaction_bp.route('/task//edit', methods=['GET']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def edit_task(task_id): task = EveAITask.query.get_or_404(task_id) form = EditEveAITaskForm(obj=task) if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return render_template('interaction/components/edit_task.html', form=form, task=task, title="Edit Task", description="Configure the task with company-specific details if required", submit_text="Save Task" ) return None @interaction_bp.route('/task//save', methods=['POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def save_task(task_id): task = EveAITask.query.get_or_404(task_id) if task_id else EveAITask() tenant_id = session.get('tenant').get('id') form = EditEveAITaskForm(formdata=request.form, obj=task) # Bind explicit formdata if form.validate_on_submit(): try: form.populate_obj(task) update_logging_information(task, dt.now(tz.utc)) if not task_id: # New task db.session.add(task) db.session.commit() return jsonify({'success': True, 'message': 'Task saved successfully'}) except Exception as e: db.session.rollback() current_app.logger.error(f'Failed to save task {task_id} for tenant {tenant_id}. Error: {str(e)}') return jsonify({'success': False, 'message': f"Failed to save task {task_id}: {str(e)}"}) # On validation errors, return the editor partial HTML (400) so frontend can show inline errors form_validation_failed(request, form) return render_template('interaction/components/edit_task.html', form=form, task=task, title="Edit Task", description="Configure the task with company-specific details if required", submit_text="Save Task"), 400 # Routes for Tool management ---------------------------------------------------------------------- @interaction_bp.route('/tool//edit', methods=['GET']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def edit_tool(tool_id): tool = EveAITool.query.get_or_404(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, title="Edit Tool", description="Configure the tool with company-specific details if required", submit_text="Save Tool" ) return None @interaction_bp.route('/tool//save', methods=['POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def save_tool(tool_id): tool = EveAITool.query.get_or_404(tool_id) if tool_id else EveAITool() tenant_id = session.get('tenant').get('id') form = EditEveAIToolForm(formdata=request.form, obj=tool) if form.validate_on_submit(): try: form.populate_obj(tool) update_logging_information(tool, dt.now(tz.utc)) if not tool_id: # New tool db.session.add(tool) db.session.commit() return jsonify({'success': True, 'message': 'Tool saved successfully'}) except Exception as e: db.session.rollback() current_app.logger.error(f'Failed to save tool {tool_id} for tenant {tenant_id}. Error: {str(e)}') return jsonify({'success': False, 'message': f"Failed to save tool {tool_id}: {str(e)}"}) # On validation errors, return the editor partial HTML (400) form_validation_failed(request, form) return render_template('interaction/components/edit_tool.html', form=form, tool=tool, title="Edit Tool", description="Configure the tool with company-specific details if required", submit_text="Save Tool"), 400 # Component selection handlers -------------------------------------------------------------------- @interaction_bp.route('/handle_agent_selection', methods=['POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def handle_agent_selection(): agent_identification = request.form['selected_row'] agent_id = ast.literal_eval(agent_identification).get('value') action = request.form.get('action') if action == "edit_agent": return redirect(prefixed_url_for('interaction_bp.edit_agent', agent_id=agent_id, for_redirect=True)) return redirect(prefixed_url_for('interaction_bp.edit_specialist', for_redirect=True)) @interaction_bp.route('/handle_task_selection', methods=['POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def handle_task_selection(): task_identification = request.form['selected_row'] task_id = ast.literal_eval(task_identification).get('value') action = request.form.get('action') if action == "edit_task": return redirect(prefixed_url_for('interaction_bp.edit_task', task_id=task_id, for_redirect=True)) return redirect(prefixed_url_for('interaction_bp.edit_specialist', for_redirect=True)) @interaction_bp.route('/handle_tool_selection', methods=['POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def handle_tool_selection(): tool_identification = request.form['selected_row'] tool_id = ast.literal_eval(tool_identification).get('value') action = request.form.get('action') if action == "edit_tool": return redirect(prefixed_url_for('interaction_bp.edit_tool', tool_id=tool_id, for_redirect=True)) return redirect(prefixed_url_for('interaction_bp.edit_specialist', for_redirect=True)) # Specialist Execution ---------------------------------------------------------------------------- @interaction_bp.route('/execute_specialist/', 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', for_redirect=True)) form = ExecuteSpecialistForm(request.form, obj=specialist) if 'arguments' in specialist_config: form.add_dynamic_fields('arguments', specialist_config) if form.validate_on_submit(): # We're only interested in gathering the dynamic arguments arguments = form.get_dynamic_data("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') ) return redirect(prefixed_url_for('interaction_bp.session_interactions_by_session_id', session_id=session_id, for_redirect=True)) return render_template('interaction/execute_specialist.html', form=form) # Interaction Mgmt -------------------------------------------------------------------------------- @interaction_bp.route('/session_interactions_by_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', for_redirect=True)) @interaction_bp.route('/session_interactions/', 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) # 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 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 # Define the make valid for this magic link specialist = Specialist.query.get(new_specialist_magic_link.specialist_id) make_id = specialist.configuration.get('make', None) if make_id: new_specialist_magic_link.tenant_make_id = make_id elif session.get('tenant').get('default_tenant_make_id'): new_specialist_magic_link.tenant_make_id = session.get('tenant').get('default_tenant_make_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, for_redirect=True)) 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/', 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) # Set the tenant_make_id default value if request.method == 'GET': if specialist_ml.tenant_make_id is None: form.tenant_make_id.data = 0 else: form.tenant_make_id.data = specialist_ml.tenant_make_id 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") # Handle the tenant_make_id special case (0 = None) if form.tenant_make_id.data == 0: specialist_ml.tenant_make_id = None # 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', for_redirect=True)) 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('/view_specialist_magic_link_urls/', methods=['GET']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def view_specialist_magic_link_urls(specialist_magic_link_id): specialist_ml = SpecialistMagicLink.query.get_or_404(specialist_magic_link_id) form = ViewSpecialistMagicLinkURLsForm(obj=specialist_ml) # Set the chat client URL tenant_id = session.get('tenant').get('id') chat_client_prefix = current_app.config.get('CHAT_CLIENT_PREFIX', 'chat_client/chat/') base_url = request.url_root magic_link_code = specialist_ml.magic_link_code # Parse the URL om poortinformatie te behouden als deze afwijkt van de standaard url_parts = request.url.split('/') host_port = url_parts[2] # Dit bevat zowel hostname als poort indien aanwezig # Generate the full URL for chat client with magic link code chat_client_url = f"{request.scheme}://{host_port}/{chat_client_prefix}{magic_link_code}" form.chat_client_url.data = chat_client_url # Generate QR code as data URI for direct embedding in HTML try: import qrcode import io import base64 # Generate QR code as PNG for better compatibility qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4 ) qr.add_data(chat_client_url) qr.make(fit=True) # Generate PNG image in memory img = qr.make_image(fill_color="black", back_color="white") buffer = io.BytesIO() img.save(buffer, format='PNG') img_data = buffer.getvalue() # Create data URI for direct embedding in HTML img_base64 = base64.b64encode(img_data).decode('utf-8') data_uri = f"data:image/png;base64,{img_base64}" # Store the data URI in the form data form.qr_code_url.data = data_uri except Exception as e: current_app.logger.error(f"Failed to generate QR code: {str(e)}") form.qr_code_url.data = "Error generating QR code" return render_template('interaction/view_specialist_magic_link_urls.html', form=form) @interaction_bp.route('/specialist_magic_links', methods=['GET', 'POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def specialist_magic_links(): # Get configuration and render the list view config = get_magic_links_list_view() return render_list_view('list_view.html', **config) @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', for_redirect=True)) 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, for_redirect=True)) if action == "view_specialist_magic_link_urls": return redirect(prefixed_url_for('interaction_bp.view_specialist_magic_link_urls', specialist_magic_link_id=specialist_ml_id, for_redirect=True)) return redirect(prefixed_url_for('interaction_bp.specialists', for_redirect=True)) # Routes for Asset Management --------------------------------------------------------------------- @interaction_bp.route('/eveai_data_capsules', methods=['GET', 'POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def eveai_data_capsules(): # Get configuration and render the list view config = get_eveai_data_capsules_list_view() return render_list_view('list_view.html', **config) @interaction_bp.route('/assets', methods=['GET', 'POST']) def assets(): config = get_assets_list_view() return render_list_view('list_view.html', **config) @interaction_bp.route('/handle_data_capsule_selection', methods=['POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def handle_data_capsule_selection(): data_capsule_identification = request.form['selected_row'] capsule_id = ast.literal_eval(data_capsule_identification).get('value') action = request.form['action'] # Get the data capsule data_capsule = EveAIDataCapsule.query.get_or_404(capsule_id) match action: case 'view_data_capsule': # For now, we'll just redirect to view_data_capsule return redirect(prefixed_url_for('interaction_bp.view_data_capsule', data_capsule_id=capsule_id, for_redirect=True)) case 'view_chat_session': # Redirect to the chat session return redirect(prefixed_url_for('interaction_bp.view_chat_session', chat_session_id=data_capsule.chat_session_id, for_redirect=True)) # Default redirect back to the data capsules list return redirect(prefixed_url_for('interaction_bp.eveai_data_capsules', for_redirect=True)) @interaction_bp.route('/handle_asset_selection', methods=['POST']) def handle_asset_selection(): action = request.form.get('action') asset_identification = request.form.get('selected_row') asset_id = ast.literal_eval(asset_identification).get('value') if action == 'edit_asset': return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id, for_redirect=True)) return redirect(prefixed_url_for('interaction_bp.assets', for_redirect=True)) @interaction_bp.route('/view_data_capsule/', methods=['GET']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def view_data_capsule(data_capsule_id): # Get the data capsule data_capsule = EveAIDataCapsule.query.get_or_404(data_capsule_id) # Get the related chat session chat_session = ChatSession.query.get_or_404(data_capsule.chat_session_id) return render_template('interaction/view_data_capsule.html', data_capsule=data_capsule, chat_session=chat_session) @interaction_bp.route('/edit_asset/', methods=['GET', 'POST']) def edit_asset(asset_id): asset = EveAIAsset.query.get_or_404(asset_id) tenant_id = session.get('tenant', {}).get('id') if not tenant_id: flash('Geen tenant geselecteerd', 'error') return redirect(url_for('interaction_bp.assets', for_redirect=True)) # Controleer of het bestandstype wordt ondersteund if asset.file_type != 'json': flash( f'Bestandstype "{asset.file_type}" wordt momenteel niet ondersteund voor bewerking. Alleen JSON-bestanden kunnen worden bewerkt.', 'warning') return redirect(url_for('interaction_bp.assets', for_redirect=True)) if request.method == 'GET': try: # Haal het bestand op uit MinIO file_data = minio_client.download_asset_file( tenant_id, asset.bucket_name, asset.object_name ) # Decodeer JSON data json_content = json.loads(file_data.decode('utf-8')) context = { 'asset': asset, 'json_content': json.dumps(json_content, indent=2), 'asset_id': asset_id } return render_template('interaction/edit_asset.html', **context) except json.JSONDecodeError: flash('Fout bij het laden van het JSON-bestand: ongeldig JSON-formaat', 'error') return redirect(prefixed_url_for('interaction_bp.assets', for_redirect=True)) except Exception as e: current_app.logger.error(f"Error loading asset {asset_id}: {str(e)}") flash(f'Fout bij het laden van het asset: {str(e)}', 'error') return redirect(prefixed_url_for('interaction_bp.assets', for_redirect=True)) elif request.method == 'POST': try: # Haal de bewerkte JSON data op uit het formulier json_data = request.form.get('json_content') current_app.logger.debug(f"Raw JSON data received for asset {asset_id}: {json_data[:200] if json_data else 'None'}...") current_app.logger.debug(f"JSON data length: {len(json_data) if json_data else 0}") if not json_data: current_app.logger.error(f"No JSON data received for asset {asset_id}") flash('Geen JSON data ontvangen', 'error') return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id, for_redirect=True)) # Valideer JSON formaat try: parsed_json = json.loads(json_data) except json.JSONDecodeError as e: flash(f'Ongeldig JSON-formaat: {str(e)}', 'error') return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id, for_redirect=True)) # Upload de bijgewerkte JSON naar MinIO bucket_name, object_name, file_size = minio_client.upload_asset_file( tenant_id, asset.id, asset.type, asset.file_type, json_data ) # Update asset metadata asset.file_size = file_size / MIB_CONVERTOR asset.updated_at = dt.now(tz.utc) asset.updated_by = current_user.id asset.last_used_at = dt.now(tz.utc) db.session.commit() flash('Asset succesvol bijgewerkt', 'success') return redirect(prefixed_url_for('interaction_bp.assets', for_redirect=True)) except Exception as e: db.session.rollback() current_app.logger.error(f"Error saving asset {asset_id}: {str(e)}") flash(f'Fout bij het opslaan van het asset: {str(e)}', 'error') return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id, for_redirect=True))