- Modal display of privacy statement & Terms & Conditions - Consent-flag ==> check of privacy and Terms & Conditions - customisation option added to show or hide DynamicForm titles
447 lines
18 KiB
Python
447 lines
18 KiB
Python
import json
|
|
import uuid
|
|
from flask import Blueprint, render_template, request, session, current_app, jsonify, Response, stream_with_context
|
|
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():
|
|
customisation = get_default_chat_customisation()
|
|
return render_template('error.html', message="Please use a valid magic link to access the chat.",
|
|
customisation=customisation)
|
|
|
|
|
|
@chat_bp.route('/<magic_link_code>')
|
|
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.html', message="Invalid magic link code.")
|
|
|
|
# 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.html', message="Tenant not found.")
|
|
# 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.html', message="Specialist configuration not found.")
|
|
|
|
# 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.html', message="Tenant make not found.")
|
|
|
|
# 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.html', message="Specialist not found.")
|
|
|
|
# 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)
|
|
|
|
return render_template('chat.html',
|
|
tenant=tenant,
|
|
tenant_make=tenant_make,
|
|
specialist=specialist,
|
|
customisation=customisation,
|
|
messages=[],
|
|
settings=settings,
|
|
config=current_app.config
|
|
)
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)
|
|
return render_template('error.html', message="An error occurred while setting up the chat.")
|
|
|
|
|
|
@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/<task_id>')
|
|
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'
|
|
}
|
|
)
|
|
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('/privacy', methods=['GET'])
|
|
def privacy_statement():
|
|
"""
|
|
Public AJAX endpoint for privacy statement content
|
|
Returns JSON response suitable for modal display
|
|
"""
|
|
try:
|
|
# Use content_manager to get the latest privacy content
|
|
content_data = content_manager.read_content('privacy')
|
|
|
|
if not content_data:
|
|
current_app.logger.error("Privacy statement content not found")
|
|
return jsonify({
|
|
'error': 'Privacy statement not available',
|
|
'message': 'The privacy statement 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': 'Privacy Statement',
|
|
'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 privacy statement: {str(e)}")
|
|
return jsonify({
|
|
'error': 'Server error',
|
|
'message': 'An error occurred while loading the privacy statement.'
|
|
}), 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 |