Files
eveAI/eveai_chat_client/views/chat_views.py
Josako 5465dae52f - Optimisation and streamlining of messages in ExecutionProgressTracker (ept)
- Adaptation of ProgressTracker to handle these optimised messages
- Hardening SSE-streaming in eveai_chat_client
2025-10-03 08:58:44 +02:00

453 lines
18 KiB
Python

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('/<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/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/<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',
'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('/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