- Initialisation of the EveAI Chat Client.
- Introduction of Tenant Makes
This commit is contained in:
106
eveai_chat_client/__init__.py
Normal file
106
eveai_chat_client/__init__.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import logging
|
||||
import os
|
||||
from flask import Flask, jsonify
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
import logging.config
|
||||
|
||||
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 common.utils.startup_eveai import perform_startup_actions
|
||||
from config.logging_config import LOGGING
|
||||
from eveai_chat_client.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
|
||||
|
||||
|
||||
def create_app(config_file=None):
|
||||
app = Flask(__name__, static_url_path='/static')
|
||||
|
||||
# Ensure all necessary headers are handled
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
||||
|
||||
environment = os.getenv('FLASK_ENV', 'development')
|
||||
|
||||
match environment:
|
||||
case 'development':
|
||||
app.config.from_object(get_config('dev'))
|
||||
case 'production':
|
||||
app.config.from_object(get_config('prod'))
|
||||
case _:
|
||||
app.config.from_object(get_config('dev'))
|
||||
|
||||
app.config['SESSION_KEY_PREFIX'] = 'eveai_chat_client_'
|
||||
|
||||
try:
|
||||
os.makedirs(app.instance_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
logging.config.dictConfig(LOGGING)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info("eveai_chat_client starting up")
|
||||
|
||||
# Register extensions
|
||||
register_extensions(app)
|
||||
|
||||
# Configure CSRF protection
|
||||
app.config['WTF_CSRF_CHECK_DEFAULT'] = False # Disable global CSRF protection
|
||||
app.config['WTF_CSRF_TIME_LIMIT'] = None # Remove time limit for CSRF tokens
|
||||
|
||||
app.celery = make_celery(app.name, app.config)
|
||||
init_celery(app.celery, app)
|
||||
|
||||
# Register Blueprints
|
||||
register_blueprints(app)
|
||||
|
||||
# Register Error Handlers
|
||||
register_error_handlers(app)
|
||||
|
||||
# Register Cache Handlers
|
||||
register_cache_handlers(app)
|
||||
|
||||
# Debugging settings
|
||||
if app.config['DEBUG'] is True:
|
||||
app.logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Register template filters
|
||||
register_filters(app)
|
||||
|
||||
# Perform startup actions such as cache invalidation
|
||||
perform_startup_actions(app)
|
||||
|
||||
app.logger.info(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})")
|
||||
app.logger.info("-------------------------------------------------------------------------------------------------")
|
||||
return app
|
||||
|
||||
|
||||
def register_extensions(app):
|
||||
db.init_app(app)
|
||||
bootstrap.init_app(app)
|
||||
csrf.init_app(app)
|
||||
cors.init_app(app)
|
||||
simple_encryption.init_app(app)
|
||||
session.init_app(app)
|
||||
minio_client.init_app(app)
|
||||
cache_manager.init_app(app)
|
||||
metrics.init_app(app)
|
||||
content_manager.init_app(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)
|
||||
from .views.healthz_views import healthz_bp
|
||||
app.register_blueprint(healthz_bp)
|
||||
|
||||
|
||||
def register_cache_handlers(app):
|
||||
from common.utils.cache.config_cache import register_config_cache_handlers
|
||||
register_config_cache_handlers(cache_manager)
|
||||
from common.utils.cache.crewai_processed_config_cache import register_specialist_cache_handlers
|
||||
register_specialist_cache_handlers(cache_manager)
|
||||
244
eveai_chat_client/static/css/chat.css
Normal file
244
eveai_chat_client/static/css/chat.css
Normal file
@@ -0,0 +1,244 @@
|
||||
/* Base styles */
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
--secondary-color: #6c757d;
|
||||
--background-color: #ffffff;
|
||||
--text-color: #212529;
|
||||
--sidebar-color: #f8f9fa;
|
||||
--message-user-bg: #e9f5ff;
|
||||
--message-bot-bg: #f8f9fa;
|
||||
--border-radius: 8px;
|
||||
--spacing: 16px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Chat layout */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background-color: var(--sidebar-color);
|
||||
border-right: 1px solid rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: var(--spacing);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
max-width: 100%;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-text {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.team-info {
|
||||
margin-top: auto;
|
||||
padding-top: var(--spacing);
|
||||
border-top: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.team-member {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.team-member img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: var(--spacing);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: var(--spacing);
|
||||
max-width: 80%;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.bot-message {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--border-radius);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.user-message .message-content {
|
||||
background-color: var(--message-user-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.bot-message .message-content {
|
||||
background-color: var(--message-bot-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: var(--spacing);
|
||||
border-top: 1px solid rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#chat-input {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(0,0,0,0.2);
|
||||
border-radius: var(--border-radius);
|
||||
resize: none;
|
||||
height: 60px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#send-button {
|
||||
padding: 0 24px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Loading indicator */
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.typing-indicator span {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
animation: typing 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.5); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Error page styles */
|
||||
.error-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.error-box {
|
||||
background-color: white;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: 1rem 0;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-block;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 30%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
31
eveai_chat_client/templates/base.html
Normal file
31
eveai_chat_client/templates/base.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}EveAI Chat{% endblock %}</title>
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}">
|
||||
|
||||
<!-- Custom theme colors from tenant settings -->
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: {{ customization.primary_color|default('#007bff') }};
|
||||
--secondary-color: {{ customization.secondary_color|default('#6c757d') }};
|
||||
--background-color: {{ customization.background_color|default('#ffffff') }};
|
||||
--text-color: {{ customization.text_color|default('#212529') }};
|
||||
--sidebar-color: {{ customization.sidebar_color|default('#f8f9fa') }};
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
214
eveai_chat_client/templates/chat.html
Normal file
214
eveai_chat_client/templates/chat.html
Normal file
@@ -0,0 +1,214 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Chat{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="chat-container">
|
||||
<!-- Left sidebar with customizable content -->
|
||||
<div class="sidebar">
|
||||
{% if customisation.logo_url %}
|
||||
<div class="logo">
|
||||
<img src="{{ customisation.logo_url }}" alt="{{ tenant.name }} Logo">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="sidebar-content">
|
||||
{% if customisation.sidebar_text %}
|
||||
<div class="sidebar-text">
|
||||
{{ customisation.sidebar_text|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if customisation.team_info %}
|
||||
<div class="team-info">
|
||||
<h3>Team</h3>
|
||||
<div class="team-members">
|
||||
{% for member in customisation.team_info %}
|
||||
<div class="team-member">
|
||||
{% if member.avatar %}
|
||||
<img src="{{ member.avatar }}" alt="{{ member.name }}">
|
||||
{% endif %}
|
||||
<div class="member-info">
|
||||
<h4>{{ member.name }}</h4>
|
||||
<p>{{ member.role }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main chat area -->
|
||||
<div class="chat-main">
|
||||
<div class="chat-header">
|
||||
<h1>{{ specialist.name }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
<!-- Messages will be added here dynamically -->
|
||||
{% if customisation.welcome_message %}
|
||||
<div class="message bot-message">
|
||||
<div class="message-content">{{ customisation.welcome_message|safe }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="message bot-message">
|
||||
<div class="message-content">Hello! How can I help you today?</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="chat-input-container">
|
||||
<textarea id="chat-input" placeholder="Type your message here..."></textarea>
|
||||
<button id="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Store session information
|
||||
const sessionInfo = {
|
||||
tenantId: {{ tenant.id }},
|
||||
specialistId: {{ specialist.id }},
|
||||
chatSessionId: "{{ session.chat_session_id }}"
|
||||
};
|
||||
|
||||
// Chat functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const sendButton = document.getElementById('send-button');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
let currentTaskId = null;
|
||||
let pollingInterval = null;
|
||||
|
||||
// Function to add a message to the chat
|
||||
function addMessage(message, isUser = false) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = isUser ? 'message user-message' : 'message bot-message';
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'message-content';
|
||||
contentDiv.innerHTML = message;
|
||||
|
||||
messageDiv.appendChild(contentDiv);
|
||||
chatMessages.appendChild(messageDiv);
|
||||
|
||||
// Scroll to bottom
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
// Function to send a message
|
||||
function sendMessage() {
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// Add user message to chat
|
||||
addMessage(message, true);
|
||||
|
||||
// Clear input
|
||||
chatInput.value = '';
|
||||
|
||||
// Add loading indicator
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.className = 'message bot-message loading';
|
||||
loadingDiv.innerHTML = '<div class="message-content"><div class="typing-indicator"><span></span><span></span><span></span></div></div>';
|
||||
chatMessages.appendChild(loadingDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
|
||||
// Send message to server
|
||||
fetch('/api/send_message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'processing') {
|
||||
currentTaskId = data.task_id;
|
||||
|
||||
// Start polling for results
|
||||
if (pollingInterval) clearInterval(pollingInterval);
|
||||
pollingInterval = setInterval(checkTaskStatus, 1000);
|
||||
} else {
|
||||
// Remove loading indicator
|
||||
chatMessages.removeChild(loadingDiv);
|
||||
|
||||
// Show error if any
|
||||
if (data.error) {
|
||||
addMessage(`Error: ${data.error}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Remove loading indicator
|
||||
chatMessages.removeChild(loadingDiv);
|
||||
addMessage(`Error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to check task status
|
||||
function checkTaskStatus() {
|
||||
if (!currentTaskId) return;
|
||||
|
||||
fetch(`/api/check_status?task_id=${currentTaskId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Remove loading indicator
|
||||
const loadingDiv = document.querySelector('.loading');
|
||||
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||
|
||||
// Add bot response
|
||||
addMessage(data.answer);
|
||||
|
||||
// Clear polling
|
||||
clearInterval(pollingInterval);
|
||||
currentTaskId = null;
|
||||
} else if (data.status === 'error') {
|
||||
// Remove loading indicator
|
||||
const loadingDiv = document.querySelector('.loading');
|
||||
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||
|
||||
// Show error
|
||||
addMessage(`Error: ${data.message}`);
|
||||
|
||||
// Clear polling
|
||||
clearInterval(pollingInterval);
|
||||
currentTaskId = null;
|
||||
}
|
||||
// If status is 'pending', continue polling
|
||||
})
|
||||
.catch(error => {
|
||||
// Remove loading indicator
|
||||
const loadingDiv = document.querySelector('.loading');
|
||||
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||
|
||||
addMessage(`Error checking status: ${error.message}`);
|
||||
|
||||
// Clear polling
|
||||
clearInterval(pollingInterval);
|
||||
currentTaskId = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
sendButton.addEventListener('click', sendMessage);
|
||||
|
||||
chatInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
15
eveai_chat_client/templates/error.html
Normal file
15
eveai_chat_client/templates/error.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Error{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="error-container">
|
||||
<div class="error-box">
|
||||
<h1>Oops! Something went wrong</h1>
|
||||
<p class="error-message">{{ message }}</p>
|
||||
<div class="error-actions">
|
||||
<a href="/" class="btn-primary">Go to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
1
eveai_chat_client/utils/__init__.py
Normal file
1
eveai_chat_client/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Utils package for eveai_chat_client
|
||||
85
eveai_chat_client/utils/errors.py
Normal file
85
eveai_chat_client/utils/errors.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import traceback
|
||||
|
||||
import jinja2
|
||||
from flask import render_template, request, jsonify, redirect, current_app, flash
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def attribute_error_handler(error):
|
||||
"""Handle AttributeError exceptions."""
|
||||
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
|
||||
|
||||
|
||||
def no_tenant_selected_error(error):
|
||||
"""Handle errors when no tenant is selected in the current session."""
|
||||
current_app.logger.error(f"No Session Tenant Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="Session expired. Please use a valid magic link."), 401
|
||||
|
||||
|
||||
def general_exception(e):
|
||||
current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True)
|
||||
return render_template('error.html', message="An application error occurred."), 500
|
||||
|
||||
|
||||
def template_not_found_error(error):
|
||||
"""Handle Jinja2 TemplateNotFound exceptions."""
|
||||
current_app.logger.error(f'Template not found: {error.name}')
|
||||
current_app.logger.error(f'Search Paths: {current_app.jinja_loader.list_templates()}')
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="Template not found."), 404
|
||||
|
||||
|
||||
def template_syntax_error(error):
|
||||
"""Handle Jinja2 TemplateSyntaxError exceptions."""
|
||||
current_app.logger.error(f'Template syntax error: {error.message}')
|
||||
current_app.logger.error(f'In template {error.filename}, line {error.lineno}')
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="Template syntax error."), 500
|
||||
|
||||
|
||||
def register_error_handlers(app):
|
||||
app.register_error_handler(404, not_found_error)
|
||||
app.register_error_handler(500, internal_server_error)
|
||||
app.register_error_handler(401, not_authorised_error)
|
||||
app.register_error_handler(403, not_authorised_error)
|
||||
app.register_error_handler(EveAINoSessionTenant, no_tenant_selected_error)
|
||||
app.register_error_handler(KeyError, key_error_handler)
|
||||
app.register_error_handler(AttributeError, attribute_error_handler)
|
||||
app.register_error_handler(jinja2.TemplateNotFound, template_not_found_error)
|
||||
app.register_error_handler(jinja2.TemplateSyntaxError, template_syntax_error)
|
||||
app.register_error_handler(Exception, general_exception)
|
||||
1
eveai_chat_client/views/__init__.py
Normal file
1
eveai_chat_client/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Views package for eveai_chat_client
|
||||
170
eveai_chat_client/views/chat_views.py
Normal file
170
eveai_chat_client/views/chat_views.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import uuid
|
||||
from flask import Blueprint, render_template, request, session, current_app, jsonify, abort
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from common.extensions import db
|
||||
from common.models.user import Tenant, SpecialistMagicLinkTenant
|
||||
from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction
|
||||
from common.services.interaction.specialist_services import SpecialistServices
|
||||
from common.utils.database import Database
|
||||
from common.utils.chat_utils import get_default_chat_customisation
|
||||
|
||||
chat_bp = Blueprint('chat', __name__)
|
||||
|
||||
@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.")
|
||||
|
||||
tenant_id = magic_link_tenant.tenant_id
|
||||
|
||||
# Get tenant information
|
||||
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 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_id'] = tenant_id
|
||||
session['specialist_id'] = specialist_ml.specialist_id
|
||||
session['specialist_args'] = specialist_ml.specialist_args or {}
|
||||
session['magic_link_code'] = magic_link_code
|
||||
|
||||
# Get customisation options with defaults
|
||||
customisation = get_default_chat_customisation(tenant.chat_customisation_options)
|
||||
|
||||
# Start a new chat session
|
||||
session['chat_session_id'] = SpecialistServices.start_session()
|
||||
|
||||
return render_template('chat.html',
|
||||
tenant=tenant,
|
||||
specialist=specialist,
|
||||
customisation=customisation)
|
||||
|
||||
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
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
message = data.get('message')
|
||||
|
||||
if not message:
|
||||
return jsonify({'error': 'No message provided'}), 400
|
||||
|
||||
tenant_id = session.get('tenant_id')
|
||||
specialist_id = session.get('specialist_id')
|
||||
chat_session_id = session.get('chat_session_id')
|
||||
specialist_args = session.get('specialist_args', {})
|
||||
|
||||
if not all([tenant_id, specialist_id, chat_session_id]):
|
||||
return jsonify({'error': 'Session expired or invalid'}), 400
|
||||
|
||||
# Switch to tenant schema
|
||||
Database(tenant_id).switch_schema()
|
||||
|
||||
# Add user message to specialist arguments
|
||||
specialist_args['user_message'] = message
|
||||
|
||||
# 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')
|
||||
)
|
||||
|
||||
# Store the task ID for polling
|
||||
session['current_task_id'] = result['task_id']
|
||||
|
||||
return jsonify({
|
||||
'status': 'processing',
|
||||
'task_id': result['task_id']
|
||||
})
|
||||
|
||||
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
|
||||
24
eveai_chat_client/views/error_views.py
Normal file
24
eveai_chat_client/views/error_views.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
error_bp = Blueprint('error', __name__)
|
||||
|
||||
@error_bp.route('/error')
|
||||
def error_page():
|
||||
"""
|
||||
Generic error page
|
||||
"""
|
||||
return render_template('error.html', message="An error occurred.")
|
||||
|
||||
@error_bp.app_errorhandler(404)
|
||||
def page_not_found(e):
|
||||
"""
|
||||
Handle 404 errors
|
||||
"""
|
||||
return render_template('error.html', message="Page not found."), 404
|
||||
|
||||
@error_bp.app_errorhandler(500)
|
||||
def internal_server_error(e):
|
||||
"""
|
||||
Handle 500 errors
|
||||
"""
|
||||
return render_template('error.html', message="Internal server error."), 500
|
||||
17
eveai_chat_client/views/healthz_views.py
Normal file
17
eveai_chat_client/views/healthz_views.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from flask import Blueprint, jsonify
|
||||
|
||||
healthz_bp = Blueprint('healthz', __name__)
|
||||
|
||||
@healthz_bp.route('/healthz/ready')
|
||||
def ready():
|
||||
"""
|
||||
Health check endpoint for readiness probe
|
||||
"""
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
@healthz_bp.route('/healthz/live')
|
||||
def live():
|
||||
"""
|
||||
Health check endpoint for liveness probe
|
||||
"""
|
||||
return jsonify({"status": "ok"})
|
||||
Reference in New Issue
Block a user