import json import uuid from flask import Blueprint, render_template, request, session, current_app, jsonify, Response, stream_with_context, \ url_for from sqlalchemy.exc import SQLAlchemyError from common.extensions import db, content_manager from common.models.user import Tenant, SpecialistMagicLinkTenant, TenantMake from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction from common.services.interaction.specialist_services import SpecialistServices from common.utils.business_event import BusinessEvent from common.utils.business_event_context import current_event from common.utils.database import Database from common.utils.chat_utils import get_default_chat_customisation from common.utils.execution_progress import ExecutionProgressTracker from common.extensions import cache_manager chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat') @chat_bp.before_request def log_before_request(): current_app.logger.debug(f'Before request: {request.path} =====================================') @chat_bp.after_request def log_after_request(response): return response # @chat_bp.before_request # def before_request(): # try: # mw_before_request() # except Exception as e: # current_app.logger.error(f'Error switching schema in Document Blueprint: {e}') # raise @chat_bp.route('/') def index(): return render_template('error/404.html'), 404 @chat_bp.route('/') def chat(magic_link_code): """ Main chat interface accessed via magic link """ try: # Find the tenant using the magic link code magic_link_tenant = SpecialistMagicLinkTenant.query.filter_by(magic_link_code=magic_link_code).first() if not magic_link_tenant: current_app.logger.error(f"Invalid magic link code: {magic_link_code}") return render_template('error/404.html'), 404 # Get tenant information tenant_id = magic_link_tenant.tenant_id tenant = Tenant.query.get(tenant_id) if not tenant: current_app.logger.error(f"Tenant not found for ID: {tenant_id}") return render_template('error/404.html'), 404 # Switch to tenant schema Database(tenant_id).switch_schema() # Get specialist magic link details from tenant schema specialist_ml = SpecialistMagicLink.query.filter_by(magic_link_code=magic_link_code).first() if not specialist_ml: current_app.logger.error(f"Specialist magic link not found in tenant schema: {tenant_id}") return render_template('error/404.html'), 404 # Get relevant TenantMake tenant_make = TenantMake.query.get(specialist_ml.tenant_make_id) if not tenant_make: current_app.logger.error(f"Tenant make not found: {specialist_ml.tenant_make_id}") return render_template('error/500.html'), 500 # Get specialist details specialist = Specialist.query.get(specialist_ml.specialist_id) if not specialist: current_app.logger.error(f"Specialist not found: {specialist_ml.specialist_id}") return render_template('error/404.html'), 404 # Store necessary information in session session['tenant'] = tenant.to_dict() session['specialist'] = specialist.to_dict() session['magic_link'] = specialist_ml.to_dict() session['tenant_make'] = tenant_make.to_dict() session['chat_session_id'] = SpecialistServices.start_session() # Get customisation options with defaults current_app.logger.debug(f"Make Customisation Options: {tenant_make.chat_customisation_options}") try: customisation = get_default_chat_customisation(tenant_make.chat_customisation_options) current_app.logger.debug(f"Customisation Options: {customisation}") except Exception as e: current_app.logger.error(f"Error processing customisation options: {str(e)}") # Fallback to default customisation customisation = get_default_chat_customisation(None) # Start a new chat session session['chat_session_id'] = SpecialistServices.start_session() # Define settings for the client settings = { "max_message_length": 2000, "auto_scroll": True } specialist_config = specialist.configuration if isinstance(specialist_config, str): specialist_config = json.loads(specialist_config) static_url = current_app.config.get('STATIC_URL') current_app.logger.debug(f"STATIC_URL: {static_url}") if not static_url: static_url = url_for('static', filename='') return render_template('chat.html', tenant=tenant, tenant_make=tenant_make, specialist=specialist, customisation=customisation, messages=[], settings=settings, config=current_app.config, static_url=static_url ) except Exception as e: current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True) return render_template('error/500.html'), 500 @chat_bp.route('/api/send_message', methods=['POST']) def send_message(): """ API endpoint to send a message to the specialist """ current_app.logger.debug(f"Sending message to specialist: {session.get('specialist', {}).get('id', 'UNKNOWN')}\n" f"with data: {request.json} \n" f"BEFORE TRY") try: current_app.logger.debug( f"Sending message to specialist: {session.get('specialist', {}).get('id', 'UNKNOWN')}\n" f"with data: {request.json} \n" f"AFTER TRY") # Voeg meer debug logging toe om elk stap te traceren current_app.logger.debug("Step 1: Getting request data") data = request.json message = data.get('message', '') form_values = data.get('form_values', {}) current_app.logger.debug(f"Step 2: Parsed message='{message}', form_values={form_values}") # Controleer of er ofwel een bericht of formuliergegevens zijn if not message and not form_values: current_app.logger.debug("Step 3: No message or form data - returning error") return jsonify({'error': 'No message or form data provided'}), 400 current_app.logger.debug("Step 4: Getting session data") # Veiliger session toegang met fallbacks tenant_data = session.get('tenant', {}) specialist_data = session.get('specialist', {}) tenant_id = tenant_data.get('id') specialist_id = specialist_data.get('id') chat_session_id = session.get('chat_session_id') specialist_args = session.get('magic_link', {}).get('specialist_args', {}) current_app.logger.debug( f"Step 5: Session data - tenant_id={tenant_id}, specialist_id={specialist_id}, chat_session_id={chat_session_id}") if not all([tenant_id, specialist_id, chat_session_id]): current_app.logger.error( f"Missing session data: tenant_id={tenant_id}, specialist_id={specialist_id}, chat_session_id={chat_session_id}") return jsonify({'error': 'Session expired or invalid'}), 400 current_app.logger.debug("Step 6: Switching to tenant schema") # Switch to tenant schema Database(tenant_id).switch_schema() current_app.logger.debug("Step 7: Preparing specialist arguments") # Add user message to specialist arguments if message: specialist_args['question'] = message # Add form values to specialist arguments if present if form_values: specialist_args['form_values'] = form_values # Add language to specialist arguments if present user_language = data.get('language') if user_language: specialist_args['language'] = user_language current_app.logger.debug(f"Step 8: About to execute specialist with args: {specialist_args}") current_app.logger.debug(f"Sending message to specialist: {specialist_id} for tenant {tenant_id}\n" f" with args: {specialist_args}\n" f"with session ID: {chat_session_id}") # Execute specialist current_app.logger.debug("Step 9: Calling SpecialistServices.execute_specialist") result = SpecialistServices.execute_specialist( tenant_id=tenant_id, specialist_id=specialist_id, specialist_arguments=specialist_args, session_id=chat_session_id, user_timezone=data.get('timezone', 'UTC') ) current_app.logger.debug(f"Step 10: Specialist execution result: {result}") # Store the task ID for polling current_app.logger.debug("Step 11: Storing task ID in session") session['current_task_id'] = result['task_id'] current_app.logger.debug("Step 12: Preparing response") response_data = { 'status': 'processing', 'task_id': result['task_id'], 'content': 'Verwerking gestart...', 'type': 'text' } current_app.logger.debug(f"Step 13: Returning response: {response_data}") return jsonify(response_data) except Exception as e: current_app.logger.error(f"Error sending message: {str(e)}", exc_info=True) return jsonify({'error': str(e)}), 500 @chat_bp.route('/api/check_status', methods=['GET']) def check_status(): """ API endpoint to check the status of a task """ try: task_id = request.args.get('task_id') or session.get('current_task_id') if not task_id: return jsonify({'error': 'No task ID provided'}), 400 tenant_id = session.get('tenant_id') if not tenant_id: return jsonify({'error': 'Session expired or invalid'}), 400 # Switch to tenant schema Database(tenant_id).switch_schema() # Check task status using Celery task_result = current_app.celery.AsyncResult(task_id) if task_result.state == 'PENDING': return jsonify({'status': 'pending'}) elif task_result.state == 'SUCCESS': result = task_result.result # Format the response specialist_result = result.get('result', {}) response = { 'status': 'success', 'answer': specialist_result.get('answer', ''), 'citations': specialist_result.get('citations', []), 'insufficient_info': specialist_result.get('insufficient_info', False), 'interaction_id': result.get('interaction_id') } return jsonify(response) else: return jsonify({ 'status': 'error', 'message': str(task_result.info) }) except Exception as e: current_app.logger.error(f"Error checking status: {str(e)}", exc_info=True) return jsonify({'error': str(e)}), 500 @chat_bp.route('/api/task_progress/') def task_progress_stream(task_id): """ Server-Sent Events endpoint voor realtime voortgangsupdates """ current_app.logger.debug(f"Streaming updates for task ID: {task_id}") try: tracker = ExecutionProgressTracker() def generate(): try: for update in tracker.get_updates(task_id): current_app.logger.debug(f"Progress update: {update}") yield update except Exception as e: current_app.logger.error(f"Progress stream error: {str(e)}") yield f"data: {{'error': '{str(e)}'}}\n\n" return Response( stream_with_context(generate()), mimetype='text/event-stream', headers={ 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no', 'Connection': 'keep-alive' } ) except Exception as e: current_app.logger.error(f"Failed to start progress stream: {str(e)}") return jsonify({'error': str(e)}), 500 @chat_bp.route('/api/translate', methods=['POST']) def translate(): """ API endpoint om tekst te vertalen naar een doeltaal Parameters (JSON): - text: de tekst die moet worden vertaald - target_lang: de ISO 639-1 taalcode waarnaar moet worden vertaald - source_lang: (optioneel) de ISO 639-1 taalcode van de brontaal - context: (optioneel) context voor de vertaling Returns: JSON met vertaalde tekst """ try: tenant_id = session.get('tenant', {}).get('id') with BusinessEvent('Client Translation Service', tenant_id): with current_event.create_span('Front-End Translation'): data = request.json # Valideer vereiste parameters if not data or 'text' not in data or 'target_lang' not in data: return jsonify({ 'success': False, 'error': 'Required parameters missing: text and/or target_lang' }), 400 text = data.get('text') target_lang = data.get('target_lang') source_lang = data.get('source_lang') context = data.get('context') # Controleer of tekst niet leeg is if not text.strip(): return jsonify({ 'success': False, 'error': 'Text to translate cannot be empty' }), 400 # Haal tenant_id uit sessie tenant_id = session.get('tenant', {}).get('id') if not tenant_id: current_app.logger.error("No tenant ID found in session") # Fallback naar huidige app tenant_id tenant_id = getattr(current_app, 'tenant_id', None) # Haal vertaling op (uit cache of genereer nieuw) translation = cache_manager.translation_cache.get_translation( text=text, target_lang=target_lang, source_lang=source_lang, context=context ) if not translation: return jsonify({ 'success': False, 'error': 'No translation found in cache. Please try again later.' }), 500 # Retourneer het resultaat return jsonify({ 'success': True, 'translated_text': translation.translated_text, 'source_language': translation.source_language, 'target_language': translation.target_language }) except Exception as e: current_app.logger.error(f"Error translating: {str(e)}", exc_info=True) return jsonify({ 'success': False, 'error': f"Error translating: {str(e)}" }), 500 @chat_bp.route('/dpa', methods=['GET']) def privacy_statement(): """ Public AJAX endpoint for dpa statement content Returns JSON response suitable for modal display """ try: # Use content_manager to get the latest dpa content content_data = content_manager.read_content('dpa') if not content_data: current_app.logger.error("Data Privacy Agreement content not found") return jsonify({ 'error': 'Data Privacy Agreement not available', 'message': 'The Data Pdpa Agreement could not be loaded at this time.' }), 404 current_app.logger.debug(f"Content data: {content_data}") # Return JSON response for AJAX consumption return jsonify({ 'title': 'Data Privacy Agreement', 'content': content_data['content'], 'version': content_data['version'], 'content_type': content_data['content_type'] }), 200 except Exception as e: current_app.logger.error(f"Error loading Data Privacy Agreement: {str(e)}") return jsonify({ 'error': 'Server error', 'message': 'An error occurred while loading the Data Privacy Agreement.' }), 500 @chat_bp.route('/terms', methods=['GET']) def terms_conditions(): """ Public AJAX endpoint for terms & conditions content Returns JSON response suitable for modal display """ try: # Use content_manager to get the latest terms content content_data = content_manager.read_content('terms') if not content_data: current_app.logger.error("Terms & conditions content not found") return jsonify({ 'error': 'Terms & conditions not available', 'message': 'The terms & conditions could not be loaded at this time.' }), 404 # Return JSON response for AJAX consumption return jsonify({ 'title': 'Terms & Conditions', 'content': content_data['content'], 'version': content_data['version'], 'content_type': content_data['content_type'] }), 200 except Exception as e: current_app.logger.error(f"Error loading terms & conditions: {str(e)}") return jsonify({ 'error': 'Server error', 'message': 'An error occurred while loading the terms & conditions.' }), 500