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_login import LoginManager
from flask_cors import CORS from flask_cors import CORS
from flask_socketio import SocketIO from flask_socketio import SocketIO
from flask_jwt_extended import JWTManager
from flask_session import Session
from .utils.key_encryption import JosKMSClient from .utils.key_encryption import JosKMSClient
@@ -18,4 +20,7 @@ mail = Mail()
login_manager = LoginManager() login_manager = LoginManager()
cors = CORS() cors = CORS()
socketio = SocketIO() 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 ..extensions import db
from .user import User, Tenant from .user import User, Tenant
from .document import Embedding
class ChatSession(db.Model): class ChatSession(db.Model):
id = db.Column(db.Integer, primary_key=True) 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_start = db.Column(db.DateTime, nullable=False)
session_end = db.Column(db.DateTime, nullable=True) session_end = db.Column(db.DateTime, nullable=True)
# Relations # Relations
chat_interactions = db.relationship('Interaction', backref='chat_session', lazy=True) interactions = db.relationship('Interaction', backref='chat_session', lazy=True)
user = db.relationship('User', backref='chat_sessions', lazy=True)
def __repr__(self): def __repr__(self):
return f"<ChatSession {self.id} by {self.user_id}>" return f"<ChatSession {self.id} by {self.user_id}>"
@@ -18,7 +18,7 @@ class ChatSession(db.Model):
class Interaction(db.Model): class Interaction(db.Model):
id = db.Column(db.Integer, primary_key=True) 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) question = db.Column(db.Text, nullable=False)
answer = db.Column(db.Text, nullable=True) answer = db.Column(db.Text, nullable=True)
language = db.Column(db.String(2), nullable=False) language = db.Column(db.String(2), nullable=False)
@@ -33,5 +33,5 @@ class Interaction(db.Model):
class InteractionEmbedding(db.Model): class InteractionEmbedding(db.Model):
interaction_id = db.Column(db.Integer, db.ForeignKey('interaction.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) 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 base64 import b64encode, b64decode
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes from Crypto.Random import get_random_bytes
import random import random
import time
from flask import Flask from flask import Flask
import os
def generate_api_key(prefix="EveAI-Chat"): def generate_api_key(prefix="EveAI-Chat"):
@@ -11,7 +13,7 @@ def generate_api_key(prefix="EveAI-Chat"):
return f"{prefix}-{'-'.join(parts)}" return f"{prefix}-{'-'.join(parts)}"
class JosKMSClient(kms.KeyManagementServiceClient): class JosKMSClient(kms_v1.KeyManagementServiceClient):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.key_name = None self.key_name = None
@@ -26,18 +28,36 @@ class JosKMSClient(kms.KeyManagementServiceClient):
self.key_ring = app.config.get('GC_KEY_RING') self.key_ring = app.config.get('GC_KEY_RING')
self.crypto_key = app.config.get('GC_CRYPTO_KEY') 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) 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): def encrypt_api_key(self, api_key):
"""Encrypts the API key using the latest version of the KEK.""" """Encrypts the API key using the latest version of the KEK."""
dek = get_random_bytes(32) # AES 256-bit key dek = get_random_bytes(32) # AES 256-bit key
cipher = AES.new(dek, AES.MODE_GCM) cipher = AES.new(dek, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(api_key.encode()) 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 the DEK using the latest version of the Google Cloud KMS key
encrypt_response = self.encrypt( encrypt_response = self.encrypt(
request={'name': self.key_name, 'plaintext': dek} request={'name': self.key_name, 'plaintext': dek}
) )
encrypted_dek = encrypt_response.ciphertext 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 # Store the version of the key used
key_version = encrypt_response.name key_version = encrypt_response.name
@@ -53,17 +73,35 @@ class JosKMSClient(kms.KeyManagementServiceClient):
def decrypt_api_key(self, encrypted_data): def decrypt_api_key(self, encrypted_data):
"""Decrypts the API key using the specified key version.""" """Decrypts the API key using the specified key version."""
key_version = encrypted_data['key_version'] key_version = encrypted_data['key_version']
encrypted_dek = b64decode(encrypted_data['encrypted_dek']) key_name = self.key_name
nonce = b64decode(encrypted_data['nonce']) encrypted_dek = b64decode(encrypted_data['encrypted_dek'].encode('utf-8'))
tag = b64decode(encrypted_data['tag']) nonce = b64decode(encrypted_data['nonce'].encode('utf-8'))
ciphertext = b64decode(encrypted_data['ciphertext']) 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 # Decrypt the DEK using the specified version of the Google Cloud KMS key
decrypt_response = self.decrypt( try:
request={'name': key_version, 'ciphertext': encrypted_dek} decrypt_response = self.decrypt(
) request={'name': key_name, 'ciphertext': encrypted_dek}
dek = decrypt_response.plaintext )
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) cipher = AES.new(dek, AES.MODE_GCM, nonce=nonce)
api_key = cipher.decrypt_and_verify(ciphertext, tag) api_key = cipher.decrypt_and_verify(ciphertext, tag)
return api_key.decode() 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}```""" ```{text}```"""
# SocketIO settings # SocketIO settings
# SOCKETIO_ASYNC_MODE = 'threading'
SOCKETIO_ASYNC_MODE = 'gevent' SOCKETIO_ASYNC_MODE = 'gevent'
# Session Settings
SESSION_TYPE = 'redis'
SESSION_PERMANENT = False
SESSION_USE_SIGNER = True
SESSION_KEY_PREFIX = 'eveai_chat_'
class DevConfig(Config): class DevConfig(Config):
DEVELOPMENT = True DEVELOPMENT = True
@@ -107,6 +114,16 @@ class DevConfig(Config):
GC_KEY_RING = 'eveai-chat' GC_KEY_RING = 'eveai-chat'
GC_CRYPTO_KEY = 'envelope-encryption-key' 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): class ProdConfig(Config):
DEVELOPMENT = False 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.extensions import db, migrate, bootstrap, security, mail, login_manager, cors, kms_client
from common.models.user import User, Role, Tenant, TenantDomain from common.models.user import User, Role, Tenant, TenantDomain
import common.models.interaction
from config.logging_config import LOGGING from config.logging_config import LOGGING
from common.utils.security import set_tenant_session_data from common.utils.security import set_tenant_session_data
from .errors import register_error_handlers from .errors import register_error_handlers
@@ -29,7 +30,6 @@ def create_app(config_file=None):
pass pass
logging.config.dictConfig(LOGGING) logging.config.dictConfig(LOGGING)
print(__name__)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info("eveai_app starting up") logger.info("eveai_app starting up")
@@ -38,12 +38,12 @@ def create_app(config_file=None):
register_extensions(app) register_extensions(app)
# Check GCloud availability
kms_client.check_kms_access_and_latency()
app.celery = make_celery(app.name, app.config) app.celery = make_celery(app.name, app.config)
init_celery(app.celery, app) init_celery(app.celery, app)
print(app.celery.conf.broker_url)
print(app.celery.conf.result_backend)
# Setup Flask-Security-Too # Setup Flask-Security-Too
user_datastore = SQLAlchemyUserDatastore(db, User, Role) user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security.init_app(app, user_datastore) security.init_app(app, user_datastore)
@@ -69,6 +69,8 @@ def create_app(config_file=None):
# Register API # Register API
register_api(app) register_api(app)
app.logger.info("EveAI App Server Started Successfully")
app.logger.info("-------------------------------------------------------------------------------------------------")
return app return app

View File

@@ -3,43 +3,42 @@
{% block title %}Tenant Selection{% endblock %} {% block title %}Tenant Selection{% endblock %}
{% block content_title %}Select a Tenant{% endblock %} {% block content_title %}Select a Tenant{% endblock %}
{% block content_description %}Select the active tenant for the current session{% endblock %} {% block content_description %}Select the active tenant for the current session{% endblock %}
{% block content %} {% block content %}
<form method="POST" action="{{ url_for('user_bp.handle_tenant_selection') }}"> <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"> <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="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="view_users" class="btn btn-secondary">View Users</button>
<button type="submit" name="action" value="edit_tenant" class="btn btn-secondary">Edit Tenant</button> <button type="submit" name="action" value="edit_tenant" class="btn btn-secondary">Edit Tenant</button>
<!-- Add more buttons as needed -->
</div> </div>
</form> </form>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script> <script>
$(document).ready(function() { $(document).ready(function() {
$('#tenantsTable').DataTable({ $('#tenantsTable').DataTable({
'columnDefs': [{ 'columnDefs': [
'targets': 0, {
'searchable': false, 'targets': 0,
'orderable': false, 'searchable': false,
'className': 'dt-body-center', 'orderable': false,
'render': function (data, type, full, meta){ 'className': 'dt-body-center',
return '<input type="radio" name="user_id" value="' + $('<div/>').text(data).html() + '">'; },
{
'targets': 1,
'orderable': true
},
{
'targets': 2,
'orderable': true
} }
}, ],
{
'targets': 1,
'orderable': true
},
{
'targets': 2,
'orderable': true
}],
'order': [[1, 'asc']] 'order': [[1, 'asc']]
}); });
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,10 +1,12 @@
import logging import logging
import logging.config import logging.config
from flask import Flask 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 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): def create_app(config_file=None):
@@ -18,6 +20,15 @@ def create_app(config_file=None):
logging.config.dictConfig(LOGGING) logging.config.dictConfig(LOGGING)
register_extensions(app) 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 return app
@@ -29,5 +40,25 @@ def register_extensions(app):
async_mode=app.config.get('SOCKETIO_ASYNC_MODE'), async_mode=app.config.get('SOCKETIO_ASYNC_MODE'),
logger=app.config.get('SOCKETIO_LOGGER'), logger=app.config.get('SOCKETIO_LOGGER'),
engineio_logger=app.config.get('SOCKETIO_ENGINEIO_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 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 flask_security import hash_password, roles_required, roles_accepted
from sqlalchemy.exc import SQLAlchemyError 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.user import User, Tenant
from common.models.interaction import ChatSession, Interaction, InteractionEmbedding from common.models.interaction import ChatSession, Interaction, InteractionEmbedding
from common.models.document import Embedding 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 from common.utils.database import Database
chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat') chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat')
@chat_bp.route('/', methods=['GET', 'POST']) @chat_bp.route('/register_client', methods=['POST'])
def chat(): def register_client():
return render_template('chat.html') 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 @socketio.on('connect', namespace='/chat')
def on_register(state): @jwt_required()
# TODO: write initialisation code when the blueprint is registered (only once) def handle_connect():
# socketio.init_app(state.app) current_tenant = get_jwt_identity()
pass print(f'Tenant {current_tenant["tenant_id"]} connected')
@socketio.on('message', namespace='/chat') @socketio.on('message', namespace='/chat')
def handle_message(message): @jwt_required()
# TODO: write message handling code to actually realise chat def handle_message(data):
# print('Received message:', message) current_tenant = get_jwt_identity()
# socketio.emit('response', {'data': message}, namespace='/chat') print(f'Tenant {current_tenant["tenant_id"]} sent a message: {data}')
pass # 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 import os
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler
# 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() app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
print("Server starting on port 5001") if debug_mode:
http_server = WSGIServer(('0.0.0.0', 5001), app, handler_class=WebSocketHandler) logging.info("Starting Flask application in debug mode")
http_server.serve_forever() # Continuously listens for incoming requests 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()
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 export FLASK_DEBUG=1 # Use 0 for production
# Start Flask app # 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 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');
}
}
}