From a325fa508495af4c419796a82bd805b48ef886e8 Mon Sep 17 00:00:00 2001 From: Josako Date: Thu, 11 Sep 2025 14:46:28 +0200 Subject: [PATCH] - error handling now uses a more comprehensive error communication system. --- common/templates/error/401.html | 22 ++++++++++ common/templates/error/403.html | 22 ++++++++++ common/templates/error/404.html | 22 ++++++++++ common/templates/error/500.html | 22 ++++++++++ common/templates/error/generic.html | 22 ++++++++++ common/utils/errors.py | 61 ++++++++++++++++++--------- config/config.py | 13 ++---- eveai_chat_client/__init__.py | 38 ++++++++++++++--- eveai_chat_client/utils/errors.py | 12 +++--- eveai_chat_client/views/chat_views.py | 16 +++---- eveai_chat_workers/__init__.py | 9 ++-- eveai_chat_workers/tasks.py | 9 +++- eveai_workers/tasks.py | 7 ++- 13 files changed, 216 insertions(+), 59 deletions(-) create mode 100644 common/templates/error/401.html create mode 100644 common/templates/error/403.html create mode 100644 common/templates/error/404.html create mode 100644 common/templates/error/500.html create mode 100644 common/templates/error/generic.html diff --git a/common/templates/error/401.html b/common/templates/error/401.html new file mode 100644 index 0000000..f9b8f21 --- /dev/null +++ b/common/templates/error/401.html @@ -0,0 +1,22 @@ + + + + + + Unauthorized + + + +
+

Not authorized

+

Your session may have expired or this action is not permitted.

+

Go to home

+
+ + diff --git a/common/templates/error/403.html b/common/templates/error/403.html new file mode 100644 index 0000000..3e40bbb --- /dev/null +++ b/common/templates/error/403.html @@ -0,0 +1,22 @@ + + + + + + Forbidden + + + +
+

Access forbidden

+

You don't have permission to access this resource.

+

Go to home

+
+ + diff --git a/common/templates/error/404.html b/common/templates/error/404.html new file mode 100644 index 0000000..def72a8 --- /dev/null +++ b/common/templates/error/404.html @@ -0,0 +1,22 @@ + + + + + + Page not found + + + +
+

Page not found

+

The page you are looking for doesn’t exist or has been moved.

+

Go to home

+
+ + diff --git a/common/templates/error/500.html b/common/templates/error/500.html new file mode 100644 index 0000000..0e58f10 --- /dev/null +++ b/common/templates/error/500.html @@ -0,0 +1,22 @@ + + + + + + Something went wrong + + + +
+

We’re sorry — something went wrong

+

Please try again later. If the issue persists, contact support.

+

Go to home

+
+ + diff --git a/common/templates/error/generic.html b/common/templates/error/generic.html new file mode 100644 index 0000000..a9c2f32 --- /dev/null +++ b/common/templates/error/generic.html @@ -0,0 +1,22 @@ + + + + + + Error + + + +
+

Oops! Something went wrong

+

Please try again. If the issue persists, contact support.

+

Go to home

+
+ + diff --git a/common/utils/errors.py b/common/utils/errors.py index 8578236..7f6d76c 100644 --- a/common/utils/errors.py +++ b/common/utils/errors.py @@ -10,41 +10,54 @@ from common.utils.nginx_utils import prefixed_url_for def not_found_error(error): - if not current_user.is_authenticated: - return redirect(prefixed_url_for('security.login')) + profile = current_app.config.get('ERRORS_PROFILE', 'web_app') + if profile == 'web_app': + if not current_user.is_authenticated: + return redirect(prefixed_url_for('security.login', for_redirect=True)) current_app.logger.error(f"Not Found Error: {error}") current_app.logger.error(traceback.format_exc()) return render_template('error/404.html'), 404 def internal_server_error(error): - if not current_user.is_authenticated: - return redirect(prefixed_url_for('security.login')) + profile = current_app.config.get('ERRORS_PROFILE', 'web_app') + if profile == 'web_app': + if not current_user.is_authenticated: + return redirect(prefixed_url_for('security.login', for_redirect=True)) current_app.logger.error(f"Internal Server Error: {error}") current_app.logger.error(traceback.format_exc()) return render_template('error/500.html'), 500 def not_authorised_error(error): - if not current_user.is_authenticated: - return redirect(prefixed_url_for('security.login')) + profile = current_app.config.get('ERRORS_PROFILE', 'web_app') + if profile == 'web_app': + if not current_user.is_authenticated: + return redirect(prefixed_url_for('security.login', for_redirect=True)) current_app.logger.error(f"Not Authorised Error: {error}") current_app.logger.error(traceback.format_exc()) - return render_template('error/401.html') + return render_template('error/401.html'), 401 def access_forbidden(error): - if not current_user.is_authenticated: - return redirect(prefixed_url_for('security.login')) + profile = current_app.config.get('ERRORS_PROFILE', 'web_app') + if profile == 'web_app': + if not current_user.is_authenticated: + return redirect(prefixed_url_for('security.login', for_redirect=True)) current_app.logger.error(f"Access Forbidden: {error}") current_app.logger.error(traceback.format_exc()) - return render_template('error/403.html') + return render_template('error/403.html'), 403 def key_error_handler(error): + profile = current_app.config.get('ERRORS_PROFILE', 'web_app') # Check if the KeyError is specifically for 'tenant' if str(error) == "'tenant'": - return redirect(prefixed_url_for('security.login')) + if profile == 'web_app': + return redirect(prefixed_url_for('security.login', for_redirect=True)) + else: + current_app.logger.warning("Session tenant missing in chat_client context") + return render_template('error/401.html'), 401 # For other KeyErrors, you might want to log the error and return a generic error page current_app.logger.error(f"Key Error: {error}") current_app.logger.error(traceback.format_exc()) @@ -79,19 +92,24 @@ def no_tenant_selected_error(error): """Handle errors when no tenant is selected in the current session. This typically happens when a session expires or becomes invalid after - a long period of inactivity. The user will be redirected to the login page. + a long period of inactivity. The user will be redirected to the login page (web_app) + or shown an error page (chat_client). """ + profile = current_app.config.get('ERRORS_PROFILE', 'web_app') current_app.logger.error(f"No Session Tenant Error: {error}") current_app.logger.error(traceback.format_exc()) flash('Your session expired. You will have to re-enter your credentials', 'warning') - # Perform logout if user is authenticated - if current_user.is_authenticated: - from flask_security.utils import logout_user - logout_user() - - # Redirect to login page - return redirect(prefixed_url_for('security.login')) + if profile == 'web_app': + # Perform logout if user is authenticated + if current_user.is_authenticated: + from flask_security.utils import logout_user + logout_user() + # Redirect to login page + return redirect(prefixed_url_for('security.login', for_redirect=True)) + else: + # chat_client: render 401 page + return render_template('error/401.html'), 401 def general_exception(e): @@ -122,7 +140,10 @@ def template_syntax_error(error): error_details=f"Error in template '{error.filename}' at line {error.lineno}: {error.message}"), 500 -def register_error_handlers(app): +def register_error_handlers(app, profile: str = 'web_app'): + # Store profile in app config to drive handler behavior + app.config['ERRORS_PROFILE'] = profile + app.register_error_handler(404, not_found_error) app.register_error_handler(500, internal_server_error) app.register_error_handler(401, not_authorised_error) diff --git a/config/config.py b/config/config.py index 3c786af..ea646c4 100644 --- a/config/config.py +++ b/config/config.py @@ -363,8 +363,6 @@ class DevConfig(Config): EXPLAIN_TEMPLATE_LOADING = False # Define the nginx prefix used for the specific apps - EVEAI_APP_LOCATION_PREFIX = '' - EVEAI_CHAT_LOCATION_PREFIX = '/chat' CHAT_CLIENT_PREFIX = 'chat-client/chat/' # Define the static path @@ -391,8 +389,6 @@ class TestConfig(Config): EXPLAIN_TEMPLATE_LOADING = False # Define the nginx prefix used for the specific apps - EVEAI_APP_LOCATION_PREFIX = '' - EVEAI_CHAT_LOCATION_PREFIX = '/chat' CHAT_CLIENT_PREFIX = 'chat-client/chat/' # Define the static path @@ -419,9 +415,7 @@ class StagingConfig(Config): EXPLAIN_TEMPLATE_LOADING = False # Define the nginx prefix used for the specific apps - EVEAI_APP_LOCATION_PREFIX = '' - EVEAI_CHAT_LOCATION_PREFIX = '' - CHAT_CLIENT_PREFIX = '' + CHAT_CLIENT_PREFIX = 'chat-client/chat/' # Define the static path STATIC_URL = 'https://evie-staging-static.askeveai.com' @@ -452,11 +446,10 @@ class ProdConfig(Config): WTF_CSRF_SSL_STRICT = True # Set to True if using HTTPS # Define the nginx prefix used for the specific apps - EVEAI_APP_LOCATION_PREFIX = '' - EVEAI_CHAT_LOCATION_PREFIX = '' + EVEAI_CHAT_LOCATION_PREFIX = 'EVEAI_APP_LOCATION_PREFIX' # Define the static path - STATIC_URL = 'https://evie-staging-static.askeveai.com' + STATIC_URL = 'https://evie-prod-static.askeveai.com' # PATH settings ffmpeg_path = '/usr/bin/ffmpeg' diff --git a/eveai_chat_client/__init__.py b/eveai_chat_client/__init__.py index db933e9..5968ded 100644 --- a/eveai_chat_client/__init__.py +++ b/eveai_chat_client/__init__.py @@ -1,18 +1,20 @@ import logging import os -from flask import Flask, jsonify, request, url_for +from flask import Flask, jsonify, request, url_for, session as flask_session from werkzeug.middleware.proxy_fix import ProxyFix import logging.config +from jinja2 import ChoiceLoader, FileSystemLoader from common.extensions import (db, bootstrap, cors, csrf, session, minio_client, simple_encryption, metrics, cache_manager, content_manager) from common.models.user import Tenant, SpecialistMagicLinkTenant from config.logging_config import configure_logging -from eveai_chat_client.utils.errors import register_error_handlers +from common.utils.errors import register_error_handlers from common.utils.celery_utils import make_celery, init_celery from common.utils.template_filters import register_filters from config.config import get_config +from common.utils.chat_utils import get_default_chat_customisation def create_app(config_file=None): @@ -57,11 +59,23 @@ def create_app(config_file=None): app.celery = make_celery(app.name, app.config) init_celery(app.celery, app) + # Configure template loader with fallback to common/templates + try: + import os as _os + common_templates_path = _os.path.normpath(_os.path.join(app.root_path, '..', 'common', 'templates')) + app.jinja_loader = ChoiceLoader([ + app.jinja_loader, + FileSystemLoader(common_templates_path), + ]) + app.logger.debug(f"Added common templates path: {common_templates_path}") + except Exception as e: + app.logger.error(f"Failed to configure ChoiceLoader for common templates: {e}") + # Register Blueprints register_blueprints(app) - # Register Error Handlers - register_error_handlers(app) + # Register Error Handlers (shared, profile-aware) + register_error_handlers(app, profile='chat_client') # Register Cache Handlers register_cache_handlers(app) @@ -85,6 +99,19 @@ def create_app(config_file=None): # Register template filters register_filters(app) + # Always inject chat customisation for templates (safe defaults) + @app.context_processor + def inject_customisation(): + try: + tm = flask_session.get('tenant_make') + options = None + if tm and isinstance(tm, dict): + options = tm.get('chat_customisation_options') + customisation = get_default_chat_customisation(options) + except Exception: + customisation = get_default_chat_customisation(None) + return {'customisation': customisation} + app.logger.info(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})") app.logger.info("-------------------------------------------------------------------------------------------------") @@ -115,8 +142,7 @@ def register_extensions(app): def register_blueprints(app): from .views.chat_views import chat_bp app.register_blueprint(chat_bp) - from .views.error_views import error_bp - app.register_blueprint(error_bp) + # Do not register local error blueprint; use shared error handlers from common from .views.healthz_views import healthz_bp app.register_blueprint(healthz_bp) diff --git a/eveai_chat_client/utils/errors.py b/eveai_chat_client/utils/errors.py index 73035dc..8c2915e 100644 --- a/eveai_chat_client/utils/errors.py +++ b/eveai_chat_client/utils/errors.py @@ -9,31 +9,31 @@ from common.utils.eveai_exceptions import EveAINoSessionTenant def not_found_error(error): current_app.logger.error(f"Not Found Error: {error}") current_app.logger.error(traceback.format_exc()) - return render_template('error.html', message="Page not found."), 404 + return render_template('error/404.html'), 404 def internal_server_error(error): current_app.logger.error(f"Internal Server Error: {error}") current_app.logger.error(traceback.format_exc()) - return render_template('error.html', message="Internal server error."), 500 + return render_template('error/500.html'), 500 def not_authorised_error(error): current_app.logger.error(f"Not Authorised Error: {error}") current_app.logger.error(traceback.format_exc()) - return render_template('error.html', message="Not authorized."), 401 + return render_template('error/401.html'), 401 def access_forbidden(error): current_app.logger.error(f"Access Forbidden: {error}") current_app.logger.error(traceback.format_exc()) - return render_template('error.html', message="Access forbidden."), 403 + return render_template('error/403.html'), 403 def key_error_handler(error): current_app.logger.error(f"Key Error: {error}") current_app.logger.error(traceback.format_exc()) - return render_template('error.html', message="An unexpected error occurred."), 500 + return render_template('error/500.html'), 500 def attribute_error_handler(error): @@ -41,7 +41,7 @@ def attribute_error_handler(error): error_msg = str(error) current_app.logger.error(f"AttributeError: {error_msg}") current_app.logger.error(traceback.format_exc()) - return render_template('error.html', message="An application error occurred."), 500 + return render_template('error/500.html'), 500 def no_tenant_selected_error(error): diff --git a/eveai_chat_client/views/chat_views.py b/eveai_chat_client/views/chat_views.py index e14013e..39abd02 100644 --- a/eveai_chat_client/views/chat_views.py +++ b/eveai_chat_client/views/chat_views.py @@ -37,9 +37,7 @@ def log_after_request(response): @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) + return render_template('error/404.html'), 404 @chat_bp.route('/') @@ -53,14 +51,14 @@ def chat(magic_link_code): 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.") + 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.html', message="Tenant not found.") + return render_template('error/404.html'), 404 # Switch to tenant schema Database(tenant_id).switch_schema() @@ -68,19 +66,19 @@ def chat(magic_link_code): 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.") + 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.html', message="Tenant make not found.") + 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.html', message="Specialist not found.") + return render_template('error/404.html'), 404 # Store necessary information in session session['tenant'] = tenant.to_dict() @@ -124,7 +122,7 @@ def chat(magic_link_code): 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.") + return render_template('error/500.html'), 500 @chat_bp.route('/api/send_message', methods=['POST']) diff --git a/eveai_chat_workers/__init__.py b/eveai_chat_workers/__init__.py index e2dd4fc..154db5c 100644 --- a/eveai_chat_workers/__init__.py +++ b/eveai_chat_workers/__init__.py @@ -28,18 +28,19 @@ def create_app(config_file=None): configure_logging() - app.logger.info('Starting up eveai_chat_workers...') register_extensions(app) + register_cache_handlers(app) + from . import specialists, retrievers celery = make_celery(app.name, app.config) init_celery(celery, app) - register_cache_handlers(app) + from . import tasks - from eveai_chat_workers import tasks - print(tasks.tasks_ping()) + app.logger.info("EveAI Worker Server Started Successfully") + app.logger.info("-------------------------------------------------------------------------------------------------") return app, celery diff --git a/eveai_chat_workers/tasks.py b/eveai_chat_workers/tasks.py index 3140101..b3411ea 100644 --- a/eveai_chat_workers/tasks.py +++ b/eveai_chat_workers/tasks.py @@ -5,6 +5,7 @@ import traceback from flask import current_app from celery import states from sqlalchemy.exc import SQLAlchemyError, InterfaceError, OperationalError +from redis.exceptions import ConnectionError as RedisConnectionError, TimeoutError as RedisTimeoutError from common.utils.config_field_types import TaggingFields from common.utils.database import Database @@ -21,7 +22,9 @@ from common.utils.execution_progress import ExecutionProgressTracker # Healthcheck task -@current_celery.task(name='ping', queue='llm_interactions') +@current_celery.task(bind=True, name='ping', queue='llm_interactions', + autoretry_for=(InterfaceError, OperationalError, RedisConnectionError, RedisTimeoutError, OSError), + retry_backoff=True, retry_jitter=True, max_retries=5) def ping(): return 'pong' @@ -215,7 +218,9 @@ def prepare_arguments(specialist: Any, arguments: Dict[str, Any]) -> Dict[str, A raise ArgumentPreparationError(str(e)) -@current_celery.task(bind=True, name='execute_specialist', queue='llm_interactions', autoretry_for=(InterfaceError, OperationalError), retry_backoff=True, retry_jitter=True, max_retries=5) +@current_celery.task(bind=True, name='execute_specialist', queue='llm_interactions', + autoretry_for=(InterfaceError, OperationalError), + retry_backoff=True, retry_jitter=True, max_retries=5) def execute_specialist(self, tenant_id: int, specialist_id: int, arguments: Dict[str, Any], session_id: str, user_timezone: str) -> dict: """ diff --git a/eveai_workers/tasks.py b/eveai_workers/tasks.py index 3ec8bd1..bddb477 100644 --- a/eveai_workers/tasks.py +++ b/eveai_workers/tasks.py @@ -11,6 +11,7 @@ from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from sqlalchemy import or_ from sqlalchemy.exc import SQLAlchemyError, InterfaceError, OperationalError +from redis.exceptions import ConnectionError as RedisConnectionError, TimeoutError as RedisTimeoutError import traceback from common.extensions import db, cache_manager @@ -32,13 +33,15 @@ from common.utils.config_field_types import json_to_pattern_list # Healthcheck task -@current_celery.task(name='ping', queue='embeddings') +@current_celery.task(bind=True, name='ping', queue='embeddings', + autoretry_for=(InterfaceError, OperationalError, RedisConnectionError, RedisTimeoutError, OSError), + retry_backoff=True, retry_jitter=True, max_retries=5) def ping(): return 'pong' @current_celery.task(bind=True, name='create_embeddings', queue='embeddings', - autoretry_for=(InterfaceError, OperationalError), + autoretry_for=(InterfaceError, OperationalError, RedisConnectionError, RedisTimeoutError, OSError), retry_backoff=True, retry_jitter=True, max_retries=5) def create_embeddings(self, tenant_id, document_version_id): document_version = None