API key working, CORS working, SocketIO working (but no JWT), Chat client v1, Session implemented (server side)

This commit is contained in:
Josako
2024-05-22 21:32:09 +02:00
parent 883988dbab
commit 364da812ba
21 changed files with 763 additions and 69 deletions

View File

@@ -6,6 +6,8 @@ from flask_mailman import Mail
from flask_login import LoginManager
from flask_cors import CORS
from flask_socketio import SocketIO
from flask_jwt_extended import JWTManager
from flask_session import Session
from .utils.key_encryption import JosKMSClient
@@ -18,4 +20,7 @@ mail = Mail()
login_manager = LoginManager()
cors = CORS()
socketio = SocketIO()
kms_client = JosKMSClient()
jwt = JWTManager()
session = Session()
kms_client = JosKMSClient.from_service_account_json('config/gc_sa_eveai.json')

View File

@@ -1,16 +1,16 @@
from ..extensions import db
from .user import User, Tenant
from .document import Embedding
class ChatSession(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
user_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=True)
session_start = db.Column(db.DateTime, nullable=False)
session_end = db.Column(db.DateTime, nullable=True)
# Relations
chat_interactions = db.relationship('Interaction', backref='chat_session', lazy=True)
user = db.relationship('User', backref='chat_sessions', lazy=True)
interactions = db.relationship('Interaction', backref='chat_session', lazy=True)
def __repr__(self):
return f"<ChatSession {self.id} by {self.user_id}>"
@@ -18,7 +18,7 @@ class ChatSession(db.Model):
class Interaction(db.Model):
id = db.Column(db.Integer, primary_key=True)
chat_session_id = db.Column(db.Integer, db.ForeignKey('public.chat_session.id'), nullable=False)
chat_session_id = db.Column(db.Integer, db.ForeignKey(ChatSession.id), nullable=False)
question = db.Column(db.Text, nullable=False)
answer = db.Column(db.Text, nullable=True)
language = db.Column(db.String(2), nullable=False)
@@ -33,5 +33,5 @@ class Interaction(db.Model):
class InteractionEmbedding(db.Model):
interaction_id = db.Column(db.Integer, db.ForeignKey('interaction.id', ondelete='CASCADE'), primary_key=True)
embedding_id = db.Column(db.Integer, db.ForeignKey('embedding.id', ondelete='CASCADE'), primary_key=True)
interaction_id = db.Column(db.Integer, db.ForeignKey(Interaction.id, ondelete='CASCADE'), primary_key=True)
embedding_id = db.Column(db.Integer, db.ForeignKey(Embedding.id, ondelete='CASCADE'), primary_key=True)

View File

@@ -0,0 +1,75 @@
from flask import request, current_app, session
from common.models.user import Tenant, TenantDomain
def get_allowed_origins(tenant_id):
session_key = f"allowed_origins_{tenant_id}"
if session_key in session:
current_app.logger.debug(f"Fetching allowed origins for tenant {tenant_id} from session")
return session[session_key]
current_app.logger.debug(f"Fetching allowed origins for tenant {tenant_id} from database")
tenant_domains = TenantDomain.query.filter_by(tenant_id=int(tenant_id)).all()
allowed_origins = [domain.domain for domain in tenant_domains]
# Cache the result in the session
session[session_key] = allowed_origins
return allowed_origins
def cors_after_request(response, prefix):
current_app.logger.debug(f'CORS after request: {request.path}, prefix: {prefix}')
current_app.logger.debug(f'request.headers: {request.headers}')
current_app.logger.debug(f'request.args: {request.args}')
current_app.logger.debug(f'request is json?: {request.is_json}')
tenant_id = None
allowed_origins = []
# Try to get tenant_id from JSON payload
json_data = request.get_json(silent=True)
current_app.logger.debug(f'request.get_json(silent=True): {json_data}')
if json_data and 'tenant_id' in json_data:
tenant_id = json_data['tenant_id']
else:
# Fallback to get tenant_id from query parameters or headers if JSON is not available
tenant_id = request.args.get('tenant_id') or request.args.get('tenantId') or request.headers.get('X-Tenant-ID')
current_app.logger.debug(f'Identified tenant_id: {tenant_id}')
if tenant_id:
allowed_origins = get_allowed_origins(tenant_id)
current_app.logger.debug(f'Allowed origins for tenant {tenant_id}: {allowed_origins}')
else:
current_app.logger.warning('tenant_id not found in request')
origin = request.headers.get('Origin')
current_app.logger.debug(f'Origin: {origin}')
if origin in allowed_origins:
response.headers.add('Access-Control-Allow-Origin', origin)
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
response.headers.add('Access-Control-Allow-Credentials', 'true')
current_app.logger.debug(f'CORS headers set for origin: {origin}')
else:
current_app.logger.warning(f'Origin {origin} not allowed')
return response
def create_cors_after_request(prefix):
def wrapped_cors_after_request(response):
return cors_after_request(response, prefix)
return wrapped_cors_after_request
def create_multiple_cors_after_requests(prefixes):
def wrapped_cors_after_requests(response):
for prefix, cors_function in prefixes:
response = cors_function(response)
return response
return wrapped_cors_after_requests

View File

@@ -1,9 +1,11 @@
from google.cloud import kms
from google.cloud import kms_v1
from base64 import b64encode, b64decode
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import random
import time
from flask import Flask
import os
def generate_api_key(prefix="EveAI-Chat"):
@@ -11,7 +13,7 @@ def generate_api_key(prefix="EveAI-Chat"):
return f"{prefix}-{'-'.join(parts)}"
class JosKMSClient(kms.KeyManagementServiceClient):
class JosKMSClient(kms_v1.KeyManagementServiceClient):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.key_name = None
@@ -26,18 +28,36 @@ class JosKMSClient(kms.KeyManagementServiceClient):
self.key_ring = app.config.get('GC_KEY_RING')
self.crypto_key = app.config.get('GC_CRYPTO_KEY')
self.key_name = self.crypto_key_path(self.project_id, self.location, self.key_ring, self.crypto_key)
app.logger.info(f'Project ID: {self.project_id}')
app.logger.info(f'Location: {self.location}')
app.logger.info(f'Key Ring: {self.key_ring}')
app.logger.info(f'Crypto Key: {self.crypto_key}')
app.logger.info(f'Key Name: {self.key_name}')
app.logger.info(f'Service Account Key Path: {os.getenv('GOOGLE_APPLICATION_CREDENTIALS')}')
os.environ["GOOGLE_CLOUD_PROJECT"] = self.project_id
def encrypt_api_key(self, api_key):
"""Encrypts the API key using the latest version of the KEK."""
dek = get_random_bytes(32) # AES 256-bit key
cipher = AES.new(dek, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(api_key.encode())
# print(f'Dek: {dek}')
# Encrypt the DEK using the latest version of the Google Cloud KMS key
encrypt_response = self.encrypt(
request={'name': self.key_name, 'plaintext': dek}
)
encrypted_dek = encrypt_response.ciphertext
# print(f"Encrypted DEK: {encrypted_dek}")
#
# # Check
# decrypt_response = self.decrypt(
# request={'name': self.key_name, 'ciphertext': encrypted_dek}
# )
# decrypted_dek = decrypt_response.plaintext
# print(f"Decrypted DEK: {decrypted_dek}")
# Store the version of the key used
key_version = encrypt_response.name
@@ -53,17 +73,35 @@ class JosKMSClient(kms.KeyManagementServiceClient):
def decrypt_api_key(self, encrypted_data):
"""Decrypts the API key using the specified key version."""
key_version = encrypted_data['key_version']
encrypted_dek = b64decode(encrypted_data['encrypted_dek'])
nonce = b64decode(encrypted_data['nonce'])
tag = b64decode(encrypted_data['tag'])
ciphertext = b64decode(encrypted_data['ciphertext'])
key_name = self.key_name
encrypted_dek = b64decode(encrypted_data['encrypted_dek'].encode('utf-8'))
nonce = b64decode(encrypted_data['nonce'].encode('utf-8'))
tag = b64decode(encrypted_data['tag'].encode('utf-8'))
ciphertext = b64decode(encrypted_data['ciphertext'].encode('utf-8'))
# Decrypt the DEK using the specified version of the Google Cloud KMS key
try:
decrypt_response = self.decrypt(
request={'name': key_version, 'ciphertext': encrypted_dek}
request={'name': key_name, 'ciphertext': encrypted_dek}
)
dek = decrypt_response.plaintext
except Exception as e:
print(f"Failed to decrypt DEK: {e}")
return None
cipher = AES.new(dek, AES.MODE_GCM, nonce=nonce)
api_key = cipher.decrypt_and_verify(ciphertext, tag)
return api_key.decode()
def check_kms_access_and_latency(self):
# key_name = self.crypto_key_path(self.project_id, self.location, self.key_ring, self.crypto_key)
#
# start_time = time.time()
# try:
# response = self.get_crypto_key(name=key_name)
# end_time = time.time()
# print(f"Response Time: {end_time - start_time} seconds")
# print("Access to KMS is successful.")
# except Exception as e:
# print(f"Failed to access KMS: {e}")
pass

View File

@@ -64,8 +64,15 @@ class Config(object):
```{text}```"""
# SocketIO settings
# SOCKETIO_ASYNC_MODE = 'threading'
SOCKETIO_ASYNC_MODE = 'gevent'
# Session Settings
SESSION_TYPE = 'redis'
SESSION_PERMANENT = False
SESSION_USE_SIGNER = True
SESSION_KEY_PREFIX = 'eveai_chat_'
class DevConfig(Config):
DEVELOPMENT = True
@@ -107,6 +114,16 @@ class DevConfig(Config):
GC_KEY_RING = 'eveai-chat'
GC_CRYPTO_KEY = 'envelope-encryption-key'
# JWT settings
JWT_SECRET_KEY = 'bsdMkmQ8ObfMD52yAFg4trrvjgjMhuIqg2fjDpD/JqvgY0ccCcmlsEnVFmR79WPiLKEA3i8a5zmejwLZKl4v9Q=='
# Session settings
SESSION_REDIS = {
'host': 'localhost', # Redis server hostname or IP address
'port': 6379, # Redis server port
'db': 2, # Redis database number (optional)
'password': None # Redis password (optional)
}
class ProdConfig(Config):
DEVELOPMENT = False

13
config/gc_sa_eveai.json Normal file
View File

@@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "eveai-420711",
"private_key_id": "e666408e75793321a6134243628346722a71b3a6",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCaGTXCWpq08YD1\nOW4z+gncOlB7T/EIiEwsZgMp6pyUrNioGfiI9YN+uVR0nsUSmFf1YyerRgX7RqD5\nRc7T/OuX8iIvmloK3g7CaFezcVrjnBKcg/QsjDAt/OO3DTk4vykDlh/Kqxx73Jdv\nFH9YSV2H7ToWqIE8CTDnqe8vQS7Bq995c9fPlues31MgndRFg3CFkH0ldfZ4aGm3\n1RnBDyC+9SPQW9e7CJgNN9PWTmOT51Zyy5IRuV5OWePMQaGLVmCo5zNc/EHZEVRu\n1hxJPHL3NNmkYDY8tye8uHgjsAkv8QuwIuUSqnqjoo1/Yg+P0+9GCpePOAJRNxJS\n0YpDFWc5AgMBAAECggEACIU4/hG+bh97BD7JriFhfDDT6bg7g+pCs/hsAlxQ42jv\nOH7pyWuHJXGf5Cwx31usZAq4fcrgYnVpnyl8odIL628y9AjdI66wMuWhZnBFGJgK\nRhHcZWjW8nlXf0lBjwwFe4edzbn1AuWT5fYZ2HWDW2mthY/e8sUwqWPcWsjdifhz\nNR7V+Ia47McKXYgEKjyEObSP1NUOW24zH0DgxS52YPMwa1FoHn6+9Pr8P3TsTSO6\nh6f8tnd81DGl1UH4F5Bj/MHsQXyAMJbu44S4+rZ4Qlk+5xPp9hfCNpxWaHLIkJCg\nYXnC8UAjjyXiqyK0U0RjJf8TS1FxUI4iPepLNqp/pQKBgQDTicZnWFXmCFTnycWp\n66P3Yx0yvlKdUdfnoD/n9NdmUA3TZUlEVfb0IOm7ZFubF/zDTH87XrRiD/NVDbr8\n6bdhA1DXzraxhbfD36Hca6K74Ba4aYJsSWWwI0hL3FDSsv8c7qAIaUF2iwuHb7Y0\nRDcvZqowtQobcQC8cHLc/bI/ZwKBgQC6fMeGaU+lP6jhp9Nb/3Gz5Z1zzCu34IOo\nlgpTNZsowRKYLtjHifrEFi3XRxPKz5thMuJFniof5U4WoMYtRXy+PbgySvBpCia2\nXty05XssnLLMvLpYU5sbQvmOTe20zaIzLohRvvmqrydYIKu62NTubNeuD1L+Zr0q\nz1P5/wUgXwKBgQCW9MrRFQi3j1qHzkVwbOglsmUzwP3TpoQclw8DyIWuTZKQOMeA\nLJh+vr4NLCDzHLsT45MoGv0+vYM4PwQhV+e1I1idqLZXGMV60iv/0A/hYpjUIPch\nr38RoxwEhsRml7XWP7OUTQiaP7+Kdv3fbo6zFOB+wbLkwk90KgrOCX0aIQKBgFeK\n7esmErJjMPdFXk3om0q09nX+mWNHLOb+EDjBiGXYRM9V5oO9PQ/BzaEqh5sEXE+D\noH7H4cR5U3AB5yYnYYi41ngdf7//eO7Rl1AADhOCN9kum1eNX9mrVhU8deMTSRo3\ntNyTBwbeFF0lcRhUY5jNVW4rWW19cz3ed/B6i8CHAoGBAJ/l5rkV74Z5hg6BWNfQ\nYAg/4PLZmjnXIy5QdnWc/PYgbhn5+iVUcL9fSofFzJM1rjFnNcs3S90MGeOmfmo4\nM1WtcQFQbsCGt6+G5uEL/nf74mKUGpOqEM/XSkZ3inweWiDk3LK3iYfXCMBFouIr\n80IlzI1yMf7MVmWn3e1zPjCA\n-----END PRIVATE KEY-----\n",
"client_email": "eveai-349@eveai-420711.iam.gserviceaccount.com",
"client_id": "109927035346319712442",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/eveai-349%40eveai-420711.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@@ -8,6 +8,7 @@ import logging.config
from common.extensions import db, migrate, bootstrap, security, mail, login_manager, cors, kms_client
from common.models.user import User, Role, Tenant, TenantDomain
import common.models.interaction
from config.logging_config import LOGGING
from common.utils.security import set_tenant_session_data
from .errors import register_error_handlers
@@ -29,7 +30,6 @@ def create_app(config_file=None):
pass
logging.config.dictConfig(LOGGING)
print(__name__)
logger = logging.getLogger(__name__)
logger.info("eveai_app starting up")
@@ -38,12 +38,12 @@ def create_app(config_file=None):
register_extensions(app)
# Check GCloud availability
kms_client.check_kms_access_and_latency()
app.celery = make_celery(app.name, app.config)
init_celery(app.celery, app)
print(app.celery.conf.broker_url)
print(app.celery.conf.result_backend)
# Setup Flask-Security-Too
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security.init_app(app, user_datastore)
@@ -69,6 +69,8 @@ def create_app(config_file=None):
# Register API
register_api(app)
app.logger.info("EveAI App Server Started Successfully")
app.logger.info("-------------------------------------------------------------------------------------------------")
return app

View File

@@ -8,27 +8,25 @@
{% block content %}
<form method="POST" action="{{ url_for('user_bp.handle_tenant_selection') }}">
{{ render_selectable_table(headers=["Tenant ID", "Tenant Name","Website"], rows=tenants, selectable=True, id="tenantsTable") }}
{{ render_selectable_table(headers=["Tenant ID", "Tenant Name", "Website"], rows=tenants, selectable=True, id="tenantsTable") }}
<div class="form-group mt-3">
<button type="submit" name="action" value="select_tenant" class="btn btn-primary">Set Session Tenant</button>
<button type="submit" name="action" value="view_users" class="btn btn-secondary">View Users</button>
<button type="submit" name="action" value="edit_tenant" class="btn btn-secondary">Edit Tenant</button>
<!-- Add more buttons as needed -->
</div>
</form>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
$('#tenantsTable').DataTable({
'columnDefs': [{
'columnDefs': [
{
'targets': 0,
'searchable': false,
'orderable': false,
'className': 'dt-body-center',
'render': function (data, type, full, meta){
return '<input type="radio" name="user_id" value="' + $('<div/>').text(data).html() + '">';
}
},
{
'targets': 1,
@@ -37,7 +35,8 @@ $(document).ready(function() {
{
'targets': 2,
'orderable': true
}],
}
],
'order': [[1, 'asc']]
});
});

View File

@@ -1,10 +1,12 @@
import logging
import logging.config
from flask import Flask
from flask_socketio import emit
from redis import Redis
from common.extensions import db, socketio
from common.extensions import db, socketio, jwt, kms_client, cors, session
from config.logging_config import LOGGING
from eveai_chat.socket_handlers import chat_handler
from common.utils.cors_utils import create_cors_after_request
def create_app(config_file=None):
@@ -18,6 +20,15 @@ def create_app(config_file=None):
logging.config.dictConfig(LOGGING)
register_extensions(app)
# Register Blueprints
register_blueprints(app)
@app.route('/ping')
def ping():
return 'pong'
app.logger.info("EveAI Chat Server Started Successfully")
app.logger.info("-------------------------------------------------------------------------------------------------")
return app
@@ -29,5 +40,25 @@ def register_extensions(app):
async_mode=app.config.get('SOCKETIO_ASYNC_MODE'),
logger=app.config.get('SOCKETIO_LOGGER'),
engineio_logger=app.config.get('SOCKETIO_ENGINEIO_LOGGER'),
path='/socket.io'
)
jwt.init_app(app)
kms_client.init_app(app)
# Cors setup
cors.init_app(app, resources={r"/chat/*": {"origins": "*"}})
app.after_request(create_cors_after_request('/chat'))
# Session setup
# redis_config = app.config['SESSION_REDIS']
# redis_client = Redis(host=redis_config['host'],
# port=redis_config['port'],
# db=redis_config['db'],
# password=redis_config['password']
# )
session.init_app(app)
def register_blueprints(app):
from .views.chat_views import chat_bp
app.register_blueprint(chat_bp)

View File

@@ -0,0 +1,52 @@
from flask_jwt_extended import verify_jwt_in_request, get_jwt_identity, verify_jwt_in_request, decode_token
from flask_socketio import emit, disconnect
from flask import current_app, request
from common.extensions import socketio
@socketio.on('connect')
def handle_connect():
try:
# Extract token from the auth object
token = request.args.get('token')
if not token:
raise Exception("Missing Authorization Token")
current_app.logger.debug(f'SocketIO: Received token: {token}')
# Verify token
decoded_token = decode_token(token.split(" ")[1]) # Split to remove "Bearer " prefix
tenant_id = decoded_token["identity"]["tenant_id"]
current_app.logger.info(f'SocketIO: Tenant {decoded_token["identity"]["tenant_id"]} connected')
# communicate connection to client
emit('connect', {'status': 'Connected', 'tenant_id': tenant_id})
except Exception as e:
current_app.logger.error(f'SocketIO: Connection failed: {e}')
# communicate connection problem to client
emit('connect', {'status': 'Connection Failed'})
disconnect()
@socketio.on('disconnect')
def handle_disconnect():
current_app.logger.debug('SocketIO: Client disconnected')
@socketio.on('user_message')
def handle_message(data):
try:
current_app.logger.debug(f"SocketIO: Received message from tenant {data['tenantId']}: {data['message']}")
verify_jwt_in_request()
current_tenant = get_jwt_identity()
print(f'Tenant {current_tenant["tenant_id"]} sent a message: {data}')
# Store interaction in the database
response = {
'tenantId': data['tenantId'],
'message': 'This is a bot response. Actual implementation still required.',
'messageId': 'bot-message-id',
'algorithm': 'alg1'
}
current_app.logger.debug(f"SocketIO: Bot response: {response}")
emit('bot_response', response, broadcast=True)
except Exception as e:
current_app.logger.error(f'SocketIO: Message handling failed: {e}')
disconnect()

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<title>Chat Client</title>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
</head>
<body>
<h1>Chat Client</h1>
<button onclick="registerClient()">Register Client</button>
<div>
<input type="text" id="message" placeholder="Enter your message">
<button onclick="sendMessage()">Send Message</button>
</div>
<div id="messages"></div>
<script>
let socket;
async function registerClient() {
const tenantId = '1';
const apiKey = 'EveAI-CHAT-8553-7987-2800-9115-6454';
const response = await fetch('http://127.0.0.1:5001/chat/register_client', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tenant_id: tenantId, api_key: apiKey })
});
if (response.ok) {
const data = await response.json();
const token = data.token;
socket = io('http://127.0.0.1:5001/chat', {
auth: {
token: `Bearer ${token}`
}
});
socket.on('connect', () => {
console.log('Connected to server');
});
socket.on('response', (msg) => {
const messagesDiv = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.innerText = `Response: ${msg.data}`;
messagesDiv.appendChild(messageElement);
});
} else {
console.error('Registration failed');
}
}
function sendMessage() {
const message = document.getElementById('message').value;
if (socket) {
socket.emit('message', { content: message });
} else {
console.error('Socket not connected');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Client</title>
<link rel="stylesheet" href="css/eveai-chat-style.css">
<script src="js/eveai-sdk.js" defer></script>
<script src="js/eveai-chat-widget.js" defer></script>
</head>
<body>
<div id="chat-container"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const eveAI = new EveAI('tenant-id', 'EVEAI-CHAT-xxxx-xxxx-xxxx-xxxx-xxxx');
eveAI.initializeChat('chat-container');
});
</script>
</body>
</html>

View File

@@ -1,34 +1,75 @@
# from . import user_bp
import uuid
from datetime import datetime as dt, timezone as tz
from flask import request, redirect, url_for, flash, render_template, Blueprint, session, current_app
from flask import request, redirect, url_for, render_template, Blueprint, session, current_app, jsonify
from flask_security import hash_password, roles_required, roles_accepted
from sqlalchemy.exc import SQLAlchemyError
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
from flask_socketio import emit, join_room, leave_room
import ast
from common.models.user import User, Tenant
from common.models.interaction import ChatSession, Interaction, InteractionEmbedding
from common.models.document import Embedding
from common.extensions import db, socketio
from common.extensions import db, socketio, kms_client
from common.utils.database import Database
chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat')
@chat_bp.route('/', methods=['GET', 'POST'])
def chat():
return render_template('chat.html')
@chat_bp.route('/register_client', methods=['POST'])
def register_client():
tenant_id = request.json.get('tenant_id')
api_key = request.json.get('api_key')
# Validate tenant_id and api_key here (e.g., check against the database)
if validate_tenant(tenant_id, api_key):
access_token = create_access_token(identity={'tenant_id': tenant_id, 'api_key': api_key})
return jsonify({'token': access_token}), 200
else:
return jsonify({'message': 'Invalid credentials'}), 401
@chat.record_once
def on_register(state):
# TODO: write initialisation code when the blueprint is registered (only once)
# socketio.init_app(state.app)
pass
@socketio.on('connect', namespace='/chat')
@jwt_required()
def handle_connect():
current_tenant = get_jwt_identity()
print(f'Tenant {current_tenant["tenant_id"]} connected')
@socketio.on('message', namespace='/chat')
def handle_message(message):
# TODO: write message handling code to actually realise chat
# print('Received message:', message)
# socketio.emit('response', {'data': message}, namespace='/chat')
pass
@jwt_required()
def handle_message(data):
current_tenant = get_jwt_identity()
print(f'Tenant {current_tenant["tenant_id"]} sent a message: {data}')
# Store interaction in the database
emit('response', {'data': 'Message received'}, broadcast=True)
def validate_tenant(tenant_id, api_key):
tenant = Tenant.query.get_or_404(tenant_id)
encrypted_api_key = ast.literal_eval(tenant.encrypted_chat_api_key)
decrypted_api_key = kms_client.decrypt_api_key(encrypted_api_key)
return decrypted_api_key == api_key
# @chat_bp.route('/', methods=['GET', 'POST'])
# def chat():
# return render_template('chat.html')
#
#
# @chat.record_once
# def on_register(state):
# # TODO: write initialisation code when the blueprint is registered (only once)
# # socketio.init_app(state.app)
# pass
#
#
# @socketio.on('message', namespace='/chat')
# def handle_message(message):
# # TODO: write message handling code to actually realise chat
# # print('Received message:', message)
# # socketio.emit('response', {'data': message}, namespace='/chat')
# pass

1
external/nginx.conf vendored Symbolic link
View File

@@ -0,0 +1 @@
/opt/homebrew/etc/nginx/nginx.conf

25
public/chat.html Normal file
View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Client</title>
<link rel="stylesheet" href="/static/css/eveai-chat-style.css">
<script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
<script src="/static/js/eveai-sdk.js" defer></script>
<script src="/static/js/eveai-chat-widget.js" defer></script>
</head>
<body>
<div id="chat-container"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const eveAI = new EveAI(
'1',
'EveAI-CHAT-8553-7987-2800-9115-6454',
'http://macstudio.ask-eve-ai-local.com'
);
eveAI.initializeChat('chat-container');
});
</script>
</body>
</html>

11
public/index.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>EveAI</title>
</head>
<body>
<h1 style="text-align: center;">EveAI</h1>
<p style="text-align: center;">Ja ja, ge zijt gearriveerd ;-)</p>
</body>
</html>

View File

@@ -1,14 +1,32 @@
from eveai_chat import create_app
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler
import os
# Determine if we are in debug mode
debug_mode = os.environ.get('CHAT_DEBUG', 'True').lower() == 'true'
# Only monkey patch if not in debug mode
if not debug_mode:
from gevent import monkey
monkey.patch_all()
from eveai_chat import create_app
from common.extensions import socketio
import logging
logging.basicConfig(level=logging.DEBUG)
app = create_app()
if __name__ == '__main__':
print("Server starting on port 5001")
if debug_mode:
logging.info("Starting Flask application in debug mode")
app.config['DEBUG'] = True # Enable debug mode in Flask
app.config['ENV'] = 'development'
socketio.run(app, debug=True, host='0.0.0.0', port=5001, allow_unsafe_werkzeug=True) # Use Flask's built-in server for debugging
else:
logging.info("Starting Flask application with gevent WSGI server")
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler
http_server = WSGIServer(('0.0.0.0', 5001), app, handler_class=WebSocketHandler)
http_server.serve_forever() # Continuously listens for incoming requests
http_server.serve_forever()
logging.info("Application started")

View File

@@ -10,6 +10,8 @@ export FLASK_ENV=development # Use 'production' as appropriate
export FLASK_DEBUG=1 # Use 0 for production
# Start Flask app
python scripts/run_eveai_chat.py
gunicorn --workers 4 --worker-class gevent -b 0.0.0.0:5001 scripts.run_eveai_chat:app &
wait
deactivate

View File

@@ -0,0 +1,77 @@
/* eveai_chat.css */
:root {
--user-message-bg: #d1e7dd; /* Default user message background color */
--bot-message-bg: #ffffff; /* Default bot message background color */
--chat-bg: #f8f9fa; /* Default chat background color */
--algorithm-color-default: #ccc; /* Default algorithm indicator color */
--algorithm-color-alg1: #f00; /* Algorithm 1 color */
--algorithm-color-alg2: #0f0; /* Algorithm 2 color */
--algorithm-color-alg3: #00f; /* Algorithm 3 color */
}
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 600px;
margin: auto;
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
background-color: var(--chat-bg);
}
.messages-area {
flex: 1;
overflow-y: auto;
padding: 10px;
background-color: var(--bot-message-bg);
}
.message {
max-width: 90%;
margin-bottom: 10px;
padding: 10px;
border-radius: 15px;
}
.message.user {
margin-left: auto;
background-color: var(--user-message-bg);
}
.message.bot {
background-color: var(--bot-message-bg);
}
.message-icons {
display: flex;
align-items: center;
}
.message-icons i {
margin-left: 5px;
cursor: pointer;
}
.question-area {
padding: 10px;
display: flex;
align-items: center;
background-color: var(--user-message-bg);
}
.question-area input {
flex: 1;
border: none;
padding: 10px;
border-radius: 15px;
margin-right: 10px;
}
.question-area button {
background: none;
border: none;
cursor: pointer;
color: #007bff;
}

View File

@@ -0,0 +1,172 @@
// static/js/eveai-chat-widget.js
class EveAIChatWidget extends HTMLElement {
static get observedAttributes() {
return ['tenant-id', 'api-key', 'domain'];
}
constructor() {
super();
this.socket = null; // Initialize socket to null
this.attributesSet = false; // Flag to check if all attributes are set
console.log('EveAIChatWidget constructor called');
}
connectedCallback() {
console.log('connectedCallback called');
this.innerHTML = this.getTemplate();
this.messagesArea = this.querySelector('.messages-area');
this.questionInput = this.querySelector('.question-area input');
this.querySelector('.question-area button').addEventListener('click', () => this.handleSendMessage());
if (this.areAllAttributesSet() && !this.socket) {
console.log('Attributes already set in connectedCallback, initializing socket');
this.initializeSocket();
}
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`attributeChangedCallback called: ${name} changed from ${oldValue} to ${newValue}`);
this.updateAttributes();
console.log('Current attributes:', {
tenantId: this.getAttribute('tenant-id'),
apiKey: this.getAttribute('api-key'),
domain: this.getAttribute('domain')
});
if (this.areAllAttributesSet() && !this.socket) {
console.log('All attributes set in attributeChangedCallback, initializing socket');
this.attributesSet = true;
this.initializeSocket();
}
}
updateAttributes() {
console.log('Updating attributes:');
this.tenantId = this.getAttribute('tenant-id');
this.apiKey = this.getAttribute('api-key');
this.domain = this.getAttribute('domain');
console.log('Updated attributes:', {
tenantId: this.tenantId,
apiKey: this.apiKey,
domain: this.domain
});
}
areAllAttributesSet() {
const tenantId = this.getAttribute('tenant-id');
const apiKey = this.getAttribute('api-key');
const domain = this.getAttribute('domain');
console.log('Checking if all attributes are set:', {
tenantId,
apiKey,
domain
});
return tenantId && apiKey && domain;
}
initializeSocket() {
if (this.socket) {
console.log('Socket already initialized');
return;
}
if (!this.domain || this.domain === 'null') {
console.error('Domain attribute is missing or invalid');
return;
}
console.log(`Initializing socket connection to ${this.domain}`);
const token = 'Bearer ' + this.apiKey
// Include tenantId in query parameters
this.socket = io(this.domain, {
path: '/chat/socket.io/',
transports: ['websocket', 'polling'],
auth: {
token: token // Add the token to the authentication object
},
query: {
tenantId: this.tenantId,
// apiKey: this.apiKey
},
});
this.socket.on('connect', () => {
console.log('Socket connected');
});
this.socket.on('connect_error', (err) => {
console.error('Socket connection error:', err);
});
this.socket.on('connect_timeout', () => {
console.error('Socket connection timeout')
});
this.socket.on('disconnect', () => {
console.log('Socket disconnected');
});
this.socket.on('bot_response', (data) => {
if (data.tenantId === this.tenantId) {
this.addMessage(data.message, 'bot', data.messageId, data.algorithm);
}
});
}
getTemplate() {
return `
<div class="chat-container">
<div class="messages-area"></div>
<div class="question-area">
<input type="text" placeholder="Type your message here..." />
<button>Send</button>
</div>
</div>
`;
}
addMessage(text, type = 'user', id = null, algorithm = 'default') {
const message = document.createElement('div');
message.classList.add('message', type);
message.innerHTML = `
<p>${text}</p>
${type === 'bot' ? `
<div class="message-icons">
<span class="algorithm-indicator" style="background-color: var(--algorithm-color-${algorithm});"></span>
<i class="material-icons" onclick="handleFeedback('${id}', 'up')">thumb_up</i>
<i class="material-icons" onclick="handleFeedback('${id}', 'down')">thumb_down</i>
</div>` : ''}
`;
this.messagesArea.appendChild(message);
this.messagesArea.scrollTop = this.messagesArea.scrollHeight;
}
handleSendMessage() {
console.log('handleSendMessage called');
const message = this.questionInput.value.trim();
if (message) {
this.addMessage(message, 'user');
this.questionInput.value = '';
this.sendMessageToBackend(message);
}
}
sendMessageToBackend(message) {
console.log('sendMessageToBackend called');
if (!this.socket) {
console.error('Socket is not initialized');
return;
}
console.log('Sending message to backend');
this.socket.emit('user_message', { tenantId: this.tenantId, apiKey: this.apiKey, message });
}
}
customElements.define('eveai-chat-widget', EveAIChatWidget);
function handleFeedback(messageId, feedback) {
// Send feedback to the backend
console.log(`Feedback for ${messageId}: ${feedback}`);
// Implement the actual feedback mechanism
}

29
static/js/eveai-sdk.js Normal file
View File

@@ -0,0 +1,29 @@
// static/js/eveai-sdk.js
class EveAI {
constructor(tenantId, apiKey, domain) {
this.tenantId = tenantId;
this.apiKey = apiKey;
this.domain = domain;
console.log('EveAI constructor:', { tenantId, apiKey, domain });
}
initializeChat(containerId) {
const container = document.getElementById(containerId);
if (container) {
container.innerHTML = '<eveai-chat-widget></eveai-chat-widget>';
customElements.whenDefined('eveai-chat-widget').then(() => {
const chatWidget = container.querySelector('eveai-chat-widget');
chatWidget.setAttribute('tenant-id', this.tenantId);
chatWidget.setAttribute('api-key', this.apiKey);
chatWidget.setAttribute('domain', this.domain);
console.log('Attributes set in chat widget:', {
tenantId: chatWidget.getAttribute('tenant-id'),
apiKey: chatWidget.getAttribute('api-key'),
domain: chatWidget.getAttribute('domain')
});
});
} else {
console.error('Container not found');
}
}
}