diff --git a/common/models/interaction.py b/common/models/interaction.py index 7b10e8f..4e656af 100644 --- a/common/models/interaction.py +++ b/common/models/interaction.py @@ -45,6 +45,21 @@ class Specialist(db.Model): updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now()) updated_by = db.Column(db.Integer, db.ForeignKey(User.id)) + def __repr__(self): + return f"" + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'type': self.type, + 'type_version': self.type_version, + 'configuration': self.configuration, + 'arguments': self.arguments, + 'active': self.active, + } + class EveAIAsset(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -238,3 +253,14 @@ class SpecialistMagicLink(db.Model): def __repr__(self): return f"" + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'magic_link_code': self.magic_link_code, + 'valid_from': self.valid_from, + 'valid_to': self.valid_to, + 'specialist_args': self.specialist_args, + } diff --git a/common/models/user.py b/common/models/user.py index 32b16a0..75bcb23 100644 --- a/common/models/user.py +++ b/common/models/user.py @@ -28,7 +28,6 @@ class Tenant(db.Model): # language information default_language = db.Column(db.String(2), nullable=True) - allowed_languages = db.Column(ARRAY(sa.String(2)), nullable=True) # Entitlements currency = db.Column(db.String(20), nullable=True) @@ -63,7 +62,6 @@ class Tenant(db.Model): 'timezone': self.timezone, 'type': self.type, 'default_language': self.default_language, - 'allowed_languages': self.allowed_languages, 'currency': self.currency, 'default_tenant_make_id': self.default_tenant_make_id, } @@ -198,6 +196,20 @@ class TenantMake(db.Model): updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now()) updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) + def __repr__(self): + return f"" + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'active': self.active, + 'website': self.website, + 'logo_url': self.logo_url, + 'chat_customisation_options': self.chat_customisation_options, + } + class Partner(db.Model): __bind_key__ = 'public' diff --git a/common/services/interaction/specialist_services.py b/common/services/interaction/specialist_services.py index d81aee5..cc2d549 100644 --- a/common/services/interaction/specialist_services.py +++ b/common/services/interaction/specialist_services.py @@ -220,3 +220,17 @@ class SpecialistServices: db.session.add(tool) current_app.logger.info(f"Created tool {tool.id} of type {tool_type}") return tool + + @staticmethod + def get_specialist_system_field(specialist_id, config_name, system_name): + specialist = Specialist.query.get(specialist_id) + if not specialist: + raise ValueError(f"Specialist with ID {specialist_id} not found") + config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.version) + if not config: + raise ValueError(f"No configuration found for {specialist.type} version {specialist.version}") + potential_field = config.get(config_name, None) + if potential_field: + if potential_field.type == 'system' and potential_field.system_name == system_name: + return specialist.configuration.get(config_name, None) + return None diff --git a/content/changelog/1.0/1.0.0.md b/content/changelog/1.0/1.0.0.md index 72b1073..915714a 100644 --- a/content/changelog/1.0/1.0.0.md +++ b/content/changelog/1.0/1.0.0.md @@ -5,6 +5,17 @@ All notable changes to EveAI will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.4-alfa] + +### Added +- Introduction of Tenant Make +- Introduction of 'system' type for dynamic attributes +- Introduce Tenant Make to Traicie Specialists + +### Changed +- Enable Specialist 'activation' / 'deactivation' +- Unique constraints introduced for Catalog Name (tenant level) and make name (public level) + ## [2.3.3-alfa] ### Added diff --git a/eveai_app/templates/macros.html b/eveai_app/templates/macros.html index dd10f21..a84a6c3 100644 --- a/eveai_app/templates/macros.html +++ b/eveai_app/templates/macros.html @@ -8,7 +8,19 @@ {% endmacro %} {% macro render_field_content(field, disabled=False, readonly=False, class='') %} - {% if field.type == 'BooleanField' %} + {# Check if this is a hidden input field, if so, render only the field without label #} + {{ debug_to_console("Field Class: ", field.widget.__class__.__name__) }} + {% if field.widget.__class__.__name__ == 'HiddenInput' %} + {{ debug_to_console("Hidden Field: ", "Detected") }} + {{ field(class="form-control " + class, disabled=disabled, readonly=readonly) }} + {% if field.errors %} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + {% elif field.type == 'BooleanField' %}
{{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }} diff --git a/eveai_app/views/document_forms.py b/eveai_app/views/document_forms.py index 27eb73d..9b2cfaf 100644 --- a/eveai_app/views/document_forms.py +++ b/eveai_app/views/document_forms.py @@ -26,7 +26,6 @@ def validate_catalog_name(form, field): class CatalogForm(FlaskForm): - id = IntegerField('ID', widget=HiddenInput()) name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name]) description = TextAreaField('Description', validators=[Optional()]) diff --git a/eveai_app/views/interaction_forms.py b/eveai_app/views/interaction_forms.py index f153f31..2e48a0e 100644 --- a/eveai_app/views/interaction_forms.py +++ b/eveai_app/views/interaction_forms.py @@ -141,7 +141,6 @@ class SpecialistMagicLinkForm(FlaskForm): description = TextAreaField('Description', validators=[Optional()]) magic_link_code = StringField('Magic Link Code', validators=[DataRequired(), Length(max=55)], render_kw={'readonly': True}) specialist_id = SelectField('Specialist', validators=[DataRequired()]) - tenant_make_id = SelectField('Tenant Make', validators=[Optional()], coerce=int) valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()]) valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()]) @@ -155,10 +154,6 @@ class SpecialistMagicLinkForm(FlaskForm): # Dynamically populate the specialist field self.specialist_id.choices = [(specialist.id, specialist.name) for specialist in specialists] - # Dynamically populate the tenant_make field with None as first option - tenant_makes = TenantMake.query.all() - self.tenant_make_id.choices = [(0, 'None')] + [(make.id, make.name) for make in tenant_makes] - class EditSpecialistMagicLinkForm(DynamicFormBase): name = StringField('Name', validators=[DataRequired(), Length(max=50)]) @@ -168,7 +163,6 @@ class EditSpecialistMagicLinkForm(DynamicFormBase): specialist_id = IntegerField('Specialist', validators=[DataRequired()], render_kw={'readonly': True}) specialist_name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True}) tenant_make_id = SelectField('Tenant Make', validators=[Optional()], coerce=int) - tenant_make_name = StringField('Tenant Make Name', validators=[Optional()], render_kw={'readonly': True}) valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()]) valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()]) diff --git a/eveai_app/views/interaction_views.py b/eveai_app/views/interaction_views.py index 64fc2cf..cb3754c 100644 --- a/eveai_app/views/interaction_views.py +++ b/eveai_app/views/interaction_views.py @@ -705,6 +705,14 @@ def specialist_magic_link(): new_spec_ml_tenant.magic_link_code = new_specialist_magic_link.magic_link_code new_spec_ml_tenant.tenant_id = tenant_id + # Define the make valid for this magic link + make_id = SpecialistServices.get_specialist_system_field(new_specialist_magic_link.id, + "make", "tenant_make") + if make_id: + new_spec_ml_tenant.tenant_make_id = make_id + elif session.get('tenant').get('default_tenant_make_id'): + new_spec_ml_tenant.tenant_make_id = session.get('tenant').get('default_tenant_make_id') + db.session.add(new_specialist_magic_link) db.session.add(new_spec_ml_tenant) diff --git a/eveai_app/views/user_forms.py b/eveai_app/views/user_forms.py index bc7bc47..593939b 100644 --- a/eveai_app/views/user_forms.py +++ b/eveai_app/views/user_forms.py @@ -20,7 +20,41 @@ class TenantForm(FlaskForm): website = StringField('Website', validators=[DataRequired(), Length(max=255)]) # language fields default_language = SelectField('Default Language', choices=[], validators=[DataRequired()]) - allowed_languages = SelectMultipleField('Allowed Languages', choices=[], validators=[DataRequired()]) + # invoicing fields + currency = SelectField('Currency', choices=[], validators=[DataRequired()]) + # Timezone + timezone = SelectField('Timezone', choices=[], validators=[DataRequired()]) + + # For Super Users only - Allow to assign the tenant to the partner + assign_to_partner = BooleanField('Assign to Partner', default=False) + # Embedding variables + submit = SubmitField('Submit') + + def __init__(self, *args, **kwargs): + super(TenantForm, self).__init__(*args, **kwargs) + # initialise language fields + self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']] + # initialise currency field + self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']] + # initialise timezone + self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones] + # Initialize fallback algorithms + self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']] + # Initialize default tenant make choices + tenant_id = session.get('tenant', {}).get('id') if 'tenant' in session else None + # Show field only for Super Users with partner in session + if not current_user.has_roles('Super User') or 'partner' not in session: + self._fields.pop('assign_to_partner', None) + + +class EditTenantForm(FlaskForm): + id = IntegerField('ID', widget=HiddenInput()) + name = StringField('Name', validators=[DataRequired(), Length(max=80)]) + code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True}) + type = SelectField('Tenant Type', validators=[Optional()], default='Active') + website = StringField('Website', validators=[DataRequired(), Length(max=255)]) + # language fields + default_language = SelectField('Default Language', choices=[], validators=[DataRequired()]) # invoicing fields currency = SelectField('Currency', choices=[], validators=[DataRequired()]) # Timezone @@ -34,10 +68,9 @@ class TenantForm(FlaskForm): submit = SubmitField('Submit') def __init__(self, *args, **kwargs): - super(TenantForm, self).__init__(*args, **kwargs) + super(EditTenantForm, self).__init__(*args, **kwargs) # initialise language fields self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']] - self.allowed_languages.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']] # initialise currency field self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']] # initialise timezone @@ -45,7 +78,7 @@ class TenantForm(FlaskForm): # Initialize fallback algorithms self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']] # Initialize default tenant make choices - tenant_id = session.get('tenant', {}).get('id') if 'tenant' in session else None + tenant_id = self.id.data if tenant_id: tenant_makes = TenantMake.query.filter_by(tenant_id=tenant_id, active=True).all() self.default_tenant_make_id.choices = [(str(make.id), make.name) for make in tenant_makes] @@ -154,6 +187,13 @@ def validate_make_name(form, field): class TenantMakeForm(DynamicFormBase): + name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name]) + description = TextAreaField('Description', validators=[Optional()]) + active = BooleanField('Active', validators=[Optional()], default=True) + website = StringField('Website', validators=[DataRequired(), Length(max=255)]) + logo_url = StringField('Logo URL', validators=[Optional(), Length(max=255)]) + +class EditTenantMakeForm(DynamicFormBase): id = IntegerField('ID', widget=HiddenInput()) name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name]) description = TextAreaField('Description', validators=[Optional()]) diff --git a/eveai_app/views/user_views.py b/eveai_app/views/user_views.py index 52ab133..31d4060 100644 --- a/eveai_app/views/user_views.py +++ b/eveai_app/views/user_views.py @@ -12,7 +12,7 @@ from common.utils.dynamic_field_utils import create_default_config_from_type_con from common.utils.security_utils import send_confirmation_email, send_reset_email from config.type_defs.service_types import SERVICE_TYPES from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \ - TenantProjectForm, EditTenantProjectForm, TenantMakeForm + TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm from common.utils.database import Database from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed from common.utils.simple_encryption import generate_api_key @@ -53,10 +53,6 @@ def tenant(): new_tenant = Tenant() form.populate_obj(new_tenant) - # Convert default_tenant_make_id to integer if not empty - if form.default_tenant_make_id.data: - new_tenant.default_tenant_make_id = int(form.default_tenant_make_id.data) - timestamp = dt.now(tz.utc) new_tenant.created_at = timestamp new_tenant.updated_at = timestamp @@ -116,7 +112,7 @@ def tenant(): @roles_accepted('Super User', 'Partner Admin') def edit_tenant(tenant_id): tenant = Tenant.query.get_or_404(tenant_id) # This will return a 404 if no tenant is found - form = TenantForm(obj=tenant) + form = EditTenantForm(obj=tenant) if form.validate_on_submit(): # Populate the tenant with form data @@ -471,7 +467,7 @@ def edit_tenant_domain(tenant_domain_id): def tenant_overview(): tenant_id = session['tenant']['id'] tenant = Tenant.query.get_or_404(tenant_id) - form = TenantForm(obj=tenant) + form = EditTenantForm(obj=tenant) # Zet de waarde van default_tenant_make_id if tenant.default_tenant_make_id: @@ -683,7 +679,8 @@ def tenant_makes(): page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 10, type=int) - query = TenantMake.query.order_by(TenantMake.id) + tenant_id = session['tenant']['id'] + query = TenantMake.query.filter_by(tenant_id=tenant_id).order_by(TenantMake.id) pagination = query.paginate(page=page, per_page=per_page) tenant_makes = pagination.items diff --git a/eveai_chat_client/__init__.py b/eveai_chat_client/__init__.py index c1d9ca5..1f263ee 100644 --- a/eveai_chat_client/__init__.py +++ b/eveai_chat_client/__init__.py @@ -1,6 +1,7 @@ import logging import os -from flask import Flask, jsonify + +from flask import Flask, jsonify, request from werkzeug.middleware.proxy_fix import ProxyFix import logging.config @@ -74,6 +75,13 @@ def create_app(config_file=None): app.logger.info(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})") app.logger.info("-------------------------------------------------------------------------------------------------") + + # @app.before_request + # def app_before_request(): + # app.logger.debug(f'App before request: {request.path} ===== Method: {request.method} =====') + # app.logger.debug(f'Full URL: {request.url}') + # app.logger.debug(f'Endpoint: {request.endpoint}') + return app diff --git a/eveai_chat_client/templates/base.html b/eveai_chat_client/templates/base.html index 16c1c22..e8877a1 100644 --- a/eveai_chat_client/templates/base.html +++ b/eveai_chat_client/templates/base.html @@ -11,11 +11,11 @@ diff --git a/eveai_chat_client/views/chat_views.py b/eveai_chat_client/views/chat_views.py index 1b7f369..70d4374 100644 --- a/eveai_chat_client/views/chat_views.py +++ b/eveai_chat_client/views/chat_views.py @@ -3,13 +3,32 @@ from flask import Blueprint, render_template, request, session, current_app, jso from sqlalchemy.exc import SQLAlchemyError from common.extensions import db -from common.models.user import Tenant, SpecialistMagicLinkTenant +from common.models.user import Tenant, SpecialistMagicLinkTenant, TenantMake from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction from common.services.interaction.specialist_services import SpecialistServices from common.utils.database import Database from common.utils.chat_utils import get_default_chat_customisation -chat_bp = Blueprint('chat', __name__) +chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat') + +@chat_bp.before_request +def log_before_request(): + current_app.logger.debug(f'Before request: {request.path} =====================================') + + +@chat_bp.after_request +def log_after_request(response): + return response + + +# @chat_bp.before_request +# def before_request(): +# try: +# mw_before_request() +# except Exception as e: +# current_app.logger.error(f'Error switching schema in Document Blueprint: {e}') +# raise + @chat_bp.route('/') def index(): @@ -31,14 +50,12 @@ def chat(magic_link_code): current_app.logger.error(f"Invalid magic link code: {magic_link_code}") return render_template('error.html', message="Invalid magic link code.") - tenant_id = magic_link_tenant.tenant_id - # Get tenant information + tenant_id = magic_link_tenant.tenant_id tenant = Tenant.query.get(tenant_id) if not tenant: current_app.logger.error(f"Tenant not found for ID: {tenant_id}") return render_template('error.html', message="Tenant not found.") - # Switch to tenant schema Database(tenant_id).switch_schema() @@ -48,6 +65,12 @@ def chat(magic_link_code): current_app.logger.error(f"Specialist magic link not found in tenant schema: {tenant_id}") return render_template('error.html', message="Specialist configuration not found.") + # Get relevant TenantMake + tenant_make = TenantMake.query.get(specialist_ml.tenant_make_id) + if not tenant_make: + current_app.logger.error(f"Tenant make not found: {specialist_ml.tenant_make_id}") + return render_template('error.html', message="Tenant make not found.") + # Get specialist details specialist = Specialist.query.get(specialist_ml.specialist_id) if not specialist: @@ -55,13 +78,13 @@ def chat(magic_link_code): return render_template('error.html', message="Specialist not found.") # Store necessary information in session - session['tenant_id'] = tenant_id - session['specialist_id'] = specialist_ml.specialist_id - session['specialist_args'] = specialist_ml.specialist_args or {} - session['magic_link_code'] = magic_link_code + session['tenant'] = tenant.to_dict() + session['specialist'] = specialist.to_dict() + session['magic_link'] = specialist_ml.to_dict() + session['tenant_make'] = tenant_make.to_dict() # Get customisation options with defaults - customisation = get_default_chat_customisation(tenant.chat_customisation_options) + customisation = get_default_chat_customisation(tenant_make.chat_customisation_options) # Start a new chat session session['chat_session_id'] = SpecialistServices.start_session()