From 364da812baa7fbfc9c62481195f8d6229d8f2332 Mon Sep 17 00:00:00 2001 From: Josako Date: Wed, 22 May 2024 21:32:09 +0200 Subject: [PATCH] API key working, CORS working, SocketIO working (but no JWT), Chat client v1, Session implemented (server side) --- common/extensions.py | 7 +- common/models/interaction.py | 12 +- common/utils/cors_utils.py | 75 +++++++++ common/utils/key_encryption.py | 58 +++++-- config/config.py | 17 ++ config/gc_sa_eveai.json | 13 ++ eveai_app/__init__.py | 10 +- eveai_app/templates/user/select_tenant.html | 39 +++-- eveai_chat/__init__.py | 35 +++- eveai_chat/socket_handlers/chat_handler.py | 52 ++++++ eveai_chat/static/eve_ai_chat.html | 66 ++++++++ eveai_chat/static/eveai_chat.html | 20 +++ eveai_chat/views/chat_views.py | 75 +++++++-- external/nginx.conf | 1 + public/chat.html | 25 +++ public/index.html | 11 ++ scripts/run_eveai_chat.py | 34 +++- scripts/start_eveai_chat.sh | 4 +- static/css/eveai-chat-style.css | 77 +++++++++ static/js/eveai-chat-widget.js | 172 ++++++++++++++++++++ static/js/eveai-sdk.js | 29 ++++ 21 files changed, 763 insertions(+), 69 deletions(-) create mode 100644 common/utils/cors_utils.py create mode 100644 config/gc_sa_eveai.json create mode 100644 eveai_chat/socket_handlers/chat_handler.py create mode 100644 eveai_chat/static/eve_ai_chat.html create mode 100644 eveai_chat/static/eveai_chat.html create mode 120000 external/nginx.conf create mode 100644 public/chat.html create mode 100644 public/index.html create mode 100644 static/css/eveai-chat-style.css create mode 100644 static/js/eveai-chat-widget.js create mode 100644 static/js/eveai-sdk.js diff --git a/common/extensions.py b/common/extensions.py index f617f3f..95bb32d 100644 --- a/common/extensions.py +++ b/common/extensions.py @@ -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') diff --git a/common/models/interaction.py b/common/models/interaction.py index dd10559..156eb03 100644 --- a/common/models/interaction.py +++ b/common/models/interaction.py @@ -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"" @@ -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) diff --git a/common/utils/cors_utils.py b/common/utils/cors_utils.py new file mode 100644 index 0000000..77304c4 --- /dev/null +++ b/common/utils/cors_utils.py @@ -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 diff --git a/common/utils/key_encryption.py b/common/utils/key_encryption.py index 5513cba..e5c852f 100644 --- a/common/utils/key_encryption.py +++ b/common/utils/key_encryption.py @@ -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 - decrypt_response = self.decrypt( - request={'name': key_version, 'ciphertext': encrypted_dek} - ) - dek = decrypt_response.plaintext + try: + decrypt_response = self.decrypt( + 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 diff --git a/config/config.py b/config/config.py index 7911bba..7fff6f5 100644 --- a/config/config.py +++ b/config/config.py @@ -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 diff --git a/config/gc_sa_eveai.json b/config/gc_sa_eveai.json new file mode 100644 index 0000000..9919cab --- /dev/null +++ b/config/gc_sa_eveai.json @@ -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" +} diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py index 66da3be..c06066f 100644 --- a/eveai_app/__init__.py +++ b/eveai_app/__init__.py @@ -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 diff --git a/eveai_app/templates/user/select_tenant.html b/eveai_app/templates/user/select_tenant.html index f4ca473..196c13b 100644 --- a/eveai_app/templates/user/select_tenant.html +++ b/eveai_app/templates/user/select_tenant.html @@ -3,43 +3,42 @@ {% 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 %}
- {{ 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") }}
-
{% endblock %} + {% block scripts %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/eveai_chat/__init__.py b/eveai_chat/__init__.py index de147f4..f39aad7 100644 --- a/eveai_chat/__init__.py +++ b/eveai_chat/__init__.py @@ -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) diff --git a/eveai_chat/socket_handlers/chat_handler.py b/eveai_chat/socket_handlers/chat_handler.py new file mode 100644 index 0000000..7943107 --- /dev/null +++ b/eveai_chat/socket_handlers/chat_handler.py @@ -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() + diff --git a/eveai_chat/static/eve_ai_chat.html b/eveai_chat/static/eve_ai_chat.html new file mode 100644 index 0000000..515941b --- /dev/null +++ b/eveai_chat/static/eve_ai_chat.html @@ -0,0 +1,66 @@ + + + + Chat Client + + + +

Chat Client

+ +
+ + +
+
+ + + + diff --git a/eveai_chat/static/eveai_chat.html b/eveai_chat/static/eveai_chat.html new file mode 100644 index 0000000..b464287 --- /dev/null +++ b/eveai_chat/static/eveai_chat.html @@ -0,0 +1,20 @@ + + + + + + Chat Client + + + + + +
+ + + diff --git a/eveai_chat/views/chat_views.py b/eveai_chat/views/chat_views.py index 4e9d549..eaf7d63 100644 --- a/eveai_chat/views/chat_views.py +++ b/eveai_chat/views/chat_views.py @@ -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 diff --git a/external/nginx.conf b/external/nginx.conf new file mode 120000 index 0000000..a1a0866 --- /dev/null +++ b/external/nginx.conf @@ -0,0 +1 @@ +/opt/homebrew/etc/nginx/nginx.conf \ No newline at end of file diff --git a/public/chat.html b/public/chat.html new file mode 100644 index 0000000..895491e --- /dev/null +++ b/public/chat.html @@ -0,0 +1,25 @@ + + + + + + Chat Client + + + + + + +
+ + + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..866d827 --- /dev/null +++ b/public/index.html @@ -0,0 +1,11 @@ + + + + + EveAI + + +

EveAI

+

Ja ja, ge zijt gearriveerd ;-)

+ + \ No newline at end of file diff --git a/scripts/run_eveai_chat.py b/scripts/run_eveai_chat.py index c7a0a71..302965a 100644 --- a/scripts/run_eveai_chat.py +++ b/scripts/run_eveai_chat.py @@ -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") - http_server = WSGIServer(('0.0.0.0', 5001), app, handler_class=WebSocketHandler) - http_server.serve_forever() # Continuously listens for incoming requests - - + 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() +logging.info("Application started") diff --git a/scripts/start_eveai_chat.sh b/scripts/start_eveai_chat.sh index ea2dc88..b32ce36 100755 --- a/scripts/start_eveai_chat.sh +++ b/scripts/start_eveai_chat.sh @@ -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 \ No newline at end of file diff --git a/static/css/eveai-chat-style.css b/static/css/eveai-chat-style.css new file mode 100644 index 0000000..956d68d --- /dev/null +++ b/static/css/eveai-chat-style.css @@ -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; +} diff --git a/static/js/eveai-chat-widget.js b/static/js/eveai-chat-widget.js new file mode 100644 index 0000000..1bf032b --- /dev/null +++ b/static/js/eveai-chat-widget.js @@ -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 ` +
+
+
+ + +
+
+ `; + } + + addMessage(text, type = 'user', id = null, algorithm = 'default') { + const message = document.createElement('div'); + message.classList.add('message', type); + message.innerHTML = ` +

${text}

+ ${type === 'bot' ? ` +
+ + thumb_up + thumb_down +
` : ''} + `; + 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 +} \ No newline at end of file diff --git a/static/js/eveai-sdk.js b/static/js/eveai-sdk.js new file mode 100644 index 0000000..21d4b24 --- /dev/null +++ b/static/js/eveai-sdk.js @@ -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 = ''; + 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'); + } + } +} \ No newline at end of file