6 Commits

Author SHA1 Message Date
Josako
67ceb57b79 - Changelog to 2.3.5-alfa 2025-06-10 20:57:07 +02:00
Josako
23b49516cb - Create framework for chat-client, including logo, explanatory text, color settings, ...
- remove allowed_langages from tenant
- Correct bugs in Tenant, TenantMake, SpecialistMagicLink
- Change chat client customisation elements
2025-06-10 20:52:01 +02:00
Josako
9cc266b97f - Corrections to tenant, catalog, and tenant_make
- Clean-up of tenant elements
- ensure the chat_client get's it's initial call rifht.
2025-06-10 16:10:08 +02:00
Josako
3f77871c4f - Add a default make to the tenant
- Add a make to the SpecialistMagicLink
2025-06-09 18:13:38 +02:00
Josako
199cf94cf2 - Changed label for specialist_name to chatbot name ==> more logical
- Bug in unique name for catalogs
2025-06-09 16:06:41 +02:00
Josako
c4dcd6a0d3 - Add a new 'system' type to dynamic forms, first one defined = 'tenant_make'
- Add active field to Specialist model
- Improve Specialists view
- Propagate make for Role Definition Specialist to Selection Specialist (make is defined at the role level)
- Ensure a make with a given name can only be defined once
2025-06-09 11:06:36 +02:00
28 changed files with 1205 additions and 76 deletions

View File

@@ -1,7 +1,7 @@
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from ..extensions import db from ..extensions import db
from .user import User, Tenant from .user import User, Tenant, TenantMake
from .document import Embedding, Retriever from .document import Embedding, Retriever
@@ -29,6 +29,7 @@ class Specialist(db.Model):
tuning = db.Column(db.Boolean, nullable=True, default=False) tuning = db.Column(db.Boolean, nullable=True, default=False)
configuration = db.Column(JSONB, nullable=True) configuration = db.Column(JSONB, nullable=True)
arguments = db.Column(JSONB, nullable=True) arguments = db.Column(JSONB, nullable=True)
active = db.Column(db.Boolean, nullable=True, default=True)
# Relationship to retrievers through the association table # Relationship to retrievers through the association table
retrievers = db.relationship('SpecialistRetriever', backref='specialist', lazy=True, retrievers = db.relationship('SpecialistRetriever', backref='specialist', lazy=True,
@@ -44,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_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)) updated_by = db.Column(db.Integer, db.ForeignKey(User.id))
def __repr__(self):
return f"<Specialist {self.id}: {self.name}>"
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): class EveAIAsset(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@@ -222,6 +238,7 @@ class SpecialistMagicLink(db.Model):
name = db.Column(db.String(50), nullable=False) name = db.Column(db.String(50), nullable=False)
description = db.Column(db.Text, nullable=True) description = db.Column(db.Text, nullable=True)
specialist_id = db.Column(db.Integer, db.ForeignKey(Specialist.id, ondelete='CASCADE'), nullable=False) specialist_id = db.Column(db.Integer, db.ForeignKey(Specialist.id, ondelete='CASCADE'), nullable=False)
tenant_make_id = db.Column(db.Integer, db.ForeignKey(TenantMake.id, ondelete='CASCADE'), nullable=True)
magic_link_code = db.Column(db.String(55), nullable=False, unique=True) magic_link_code = db.Column(db.String(55), nullable=False, unique=True)
valid_from = db.Column(db.DateTime, nullable=True) valid_from = db.Column(db.DateTime, nullable=True)
@@ -236,3 +253,14 @@ class SpecialistMagicLink(db.Model):
def __repr__(self): def __repr__(self):
return f"<SpecialistMagicLink {self.specialist_id} {self.magic_link_code}>" return f"<SpecialistMagicLink {self.specialist_id} {self.magic_link_code}>"
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,
}

View File

@@ -28,17 +28,19 @@ class Tenant(db.Model):
# language information # language information
default_language = db.Column(db.String(2), nullable=True) default_language = db.Column(db.String(2), nullable=True)
allowed_languages = db.Column(ARRAY(sa.String(2)), nullable=True)
# Entitlements # Entitlements
currency = db.Column(db.String(20), nullable=True) currency = db.Column(db.String(20), nullable=True)
storage_dirty = db.Column(db.Boolean, nullable=True, default=False) storage_dirty = db.Column(db.Boolean, nullable=True, default=False)
default_tenant_make_id = db.Column(db.Integer, db.ForeignKey('public.tenant_make.id'), nullable=True)
# Relations # Relations
users = db.relationship('User', backref='tenant') users = db.relationship('User', backref='tenant')
domains = db.relationship('TenantDomain', backref='tenant') domains = db.relationship('TenantDomain', backref='tenant')
licenses = db.relationship('License', back_populates='tenant') licenses = db.relationship('License', back_populates='tenant')
license_usages = db.relationship('LicenseUsage', backref='tenant') license_usages = db.relationship('LicenseUsage', backref='tenant')
tenant_makes = db.relationship('TenantMake', backref='tenant', foreign_keys='TenantMake.tenant_id')
default_tenant_make = db.relationship('TenantMake', foreign_keys=[default_tenant_make_id], uselist=False)
@property @property
def current_license(self): def current_license(self):
@@ -60,8 +62,8 @@ class Tenant(db.Model):
'timezone': self.timezone, 'timezone': self.timezone,
'type': self.type, 'type': self.type,
'default_language': self.default_language, 'default_language': self.default_language,
'allowed_languages': self.allowed_languages,
'currency': self.currency, 'currency': self.currency,
'default_tenant_make_id': self.default_tenant_make_id,
} }
@@ -179,7 +181,7 @@ class TenantMake(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
name = db.Column(db.String(50), nullable=False) name = db.Column(db.String(50), nullable=False, unique=True)
description = db.Column(db.Text, nullable=True) description = db.Column(db.Text, nullable=True)
active = db.Column(db.Boolean, nullable=False, default=True) active = db.Column(db.Boolean, nullable=False, default=True)
website = db.Column(db.String(255), nullable=True) website = db.Column(db.String(255), nullable=True)
@@ -194,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_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')) updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
def __repr__(self):
return f"<TenantMake {self.id} for tenant {self.tenant_id}: {self.name}>"
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): class Partner(db.Model):
__bind_key__ = 'public' __bind_key__ = 'public'

View File

@@ -220,3 +220,18 @@ class SpecialistServices:
db.session.add(tool) db.session.add(tool)
current_app.logger.info(f"Created tool {tool.id} of type {tool_type}") current_app.logger.info(f"Created tool {tool.id} of type {tool_type}")
return tool return tool
@staticmethod
def get_specialist_system_field(specialist_id, config_name, system_name):
"""Get the value of a system field in a specialist's configuration. Returns the actual value, or None."""
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.type_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

View File

@@ -5,11 +5,11 @@ Utility functions for chat customization.
def get_default_chat_customisation(tenant_customisation=None): def get_default_chat_customisation(tenant_customisation=None):
""" """
Get chat customization options with default values for missing options. Get chat customization options with default values for missing options.
Args: Args:
tenant_customization (dict, optional): The tenant's customization options. tenant_customization (dict, optional): The tenant's customization options.
Defaults to None. Defaults to None.
Returns: Returns:
dict: A dictionary containing all customization options with default values dict: A dictionary containing all customization options with default values
for any missing options. for any missing options.
@@ -21,22 +21,25 @@ def get_default_chat_customisation(tenant_customisation=None):
'background_color': '#ffffff', 'background_color': '#ffffff',
'text_color': '#212529', 'text_color': '#212529',
'sidebar_color': '#f8f9fa', 'sidebar_color': '#f8f9fa',
'logo_url': None, 'sidebar_background': '#2c3e50',
'sidebar_text': None, 'gradient_start_color': '#f5f7fa',
'gradient_end_color': '#c3cfe2',
'markdown_background_color': 'transparent',
'markdown_text_color': '#ffffff',
'sidebar_markdown': '',
'welcome_message': 'Hello! How can I help you today?', 'welcome_message': 'Hello! How can I help you today?',
'team_info': []
} }
# If no tenant customization is provided, return the defaults # If no tenant customization is provided, return the defaults
if tenant_customisation is None: if tenant_customisation is None:
return default_customisation return default_customisation
# Start with the default customization # Start with the default customization
customisation = default_customisation.copy() customisation = default_customisation.copy()
# Update with tenant customization # Update with tenant customization
for key, value in tenant_customisation.items(): for key, value in tenant_customisation.items():
if key in customisation: if key in customisation:
customisation[key] = value customisation[key] = value
return customisation return customisation

View File

@@ -26,9 +26,34 @@ configuration:
description: "Sidebar Color" description: "Sidebar Color"
type: "color" type: "color"
required: false required: false
"sidebar_text": "sidebar_background":
name: "Sidebar Text" name: "Sidebar Background"
description: "Text to be shown in the sidebar" description: "Sidebar Background Color"
type: "color"
required: false
"markdown_background_color":
name: "Markdown Background"
description: "Markdown Background Color"
type: "color"
required: false
"markdown_text_color":
name: "Markdown Text"
description: "Markdown Text Color"
type: "color"
required: false
"gradient_start_color":
name: "Gradient Start Color"
description: "Start Color for the gradient in the Chat Area"
type: "color"
required: false
"gradient_end_color":
name: "Gradient End Color"
description: "End Color for the gradient in the Chat Area"
type: "color"
required: false
"sidebar_markdown":
name: "Sidebar Markdown"
description: "Sidebar Markdown-formatted Text"
type: "text" type: "text"
required: false required: false
"welcome_message": "welcome_message":

View File

@@ -1,4 +1,4 @@
version: "1.1.0" version: "1.2.0"
name: "Traicie Role Definition Specialist" name: "Traicie Role Definition Specialist"
framework: "crewai" framework: "crewai"
partner: "traicie" partner: "traicie"
@@ -11,9 +11,9 @@ arguments:
type: "str" type: "str"
required: true required: true
specialist_name: specialist_name:
name: "Specialist Name" name: "Chatbot Name"
description: "The name the specialist will be called upon" description: "The name of the chatbot."
type: str type: "str"
required: true required: true
role_reference: role_reference:
name: "Role Reference" name: "Role Reference"

View File

@@ -0,0 +1,50 @@
version: "1.3.0"
name: "Traicie Role Definition Specialist"
framework: "crewai"
partner: "traicie"
chat: false
configuration: {}
arguments:
role_name:
name: "Role Name"
description: "The name of the role that is being processed. Will be used to create a selection specialist"
type: "str"
required: true
specialist_name:
name: "Chatbot Name"
description: "The name of the chatbot."
type: "str"
required: true
make:
name: "Make"
description: "The make for which the role is defined and the selection specialist is created"
type: "system"
system_name: "tenant_make"
required: true
role_reference:
name: "Role Reference"
description: "A customer reference to the role"
type: "str"
required: false
vacancy_text:
name: "vacancy_text"
type: "text"
description: "The Vacancy Text"
required: true
results:
competencies:
name: "competencies"
type: "List[str, str]"
description: "List of vacancy competencies and their descriptions"
required: false
agents:
- type: "TRAICIE_HR_BP_AGENT"
version: "1.0"
tasks:
- type: "TRAICIE_GET_COMPETENCIES_TASK"
version: "1.1"
metadata:
author: "Josako"
date_added: "2025-05-27"
changes: "Added a make to be specified (a selection specialist now is created in context of a make"
description: "Assistant to create a new Vacancy based on Vacancy Text"

View File

@@ -0,0 +1,120 @@
version: "1.1.0"
name: "Traicie Selection Specialist"
framework: "crewai"
partner: "traicie"
chat: false
configuration:
name:
name: "Name"
description: "The name the specialist is called upon."
type: "str"
required: true
role_reference:
name: "Role Reference"
description: "A customer reference to the role"
type: "str"
required: false
make:
name: "Make"
description: "The make for which the role is defined and the selection specialist is created"
type: "system"
system_name: "tenant_make"
required: true
competencies:
name: "Competencies"
description: "An ordered list of competencies."
type: "ordered_list"
list_type: "competency_details"
required: true
tone_of_voice:
name: "Tone of Voice"
description: "The tone of voice the specialist uses to communicate"
type: "enum"
allowed_values: ["Professional & Neutral", "Warm & Empathetic", "Energetic & Enthusiastic", "Accessible & Informal", "Expert & Trustworthy", "No-nonsense & Goal-driven"]
default: "Professional & Neutral"
required: true
language_level:
name: "Language Level"
description: "Language level to be used when communicating, relating to CEFR levels"
type: "enum"
allowed_values: ["Basic", "Standard", "Professional"]
default: "Standard"
required: true
welcome_message:
name: "Welcome Message"
description: "Introductory text given by the specialist - but translated according to Tone of Voice, Language Level and Starting Language"
type: "text"
required: false
closing_message:
name: "Closing Message"
description: "Closing message given by the specialist - but translated according to Tone of Voice, Language Level and Starting Language"
type: "text"
required: false
competency_details:
title:
name: "Title"
description: "Competency Title"
type: "str"
required: true
description:
name: "Description"
description: "Description (in context of the role) of the competency"
type: "text"
required: true
is_knockout:
name: "KO"
description: "Defines if the competency is a knock-out criterium"
type: "boolean"
required: true
default: false
assess:
name: "Assess"
description: "Indication if this competency is to be assessed"
type: "boolean"
required: true
default: true
arguments:
region:
name: "Region"
type: "str"
description: "The region of the specific vacancy"
required: false
working_schedule:
name: "Work Schedule"
type: "str"
description: "The work schedule or employment type of the specific vacancy"
required: false
start_date:
name: "Start Date"
type: "date"
description: "The start date of the specific vacancy"
required: false
language:
name: "Language"
type: "str"
description: "The language (2-letter code) used to start the conversation"
required: true
interaction_mode:
name: "Interaction Mode"
type: "enum"
description: "The interaction mode the specialist will start working in."
allowed_values: ["Job Application", "Seduction"]
default: "Job Application"
required: true
results:
competencies:
name: "competencies"
type: "List[str, str]"
description: "List of vacancy competencies and their descriptions"
required: false
agents:
- type: "TRAICIE_HR_BP_AGENT"
version: "1.0"
tasks:
- type: "TRAICIE_GET_COMPETENCIES_TASK"
version: "1.1"
metadata:
author: "Josako"
date_added: "2025-05-27"
changes: "Add make to the selection specialist"
description: "Assistant to create a new Vacancy based on Vacancy Text"

View File

@@ -5,6 +5,31 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.3.5-alfa]
### Added
- Chat Client Initialisation (based on SpecialistMagicLink code)
- Definition of framework for the chat_client (using vue.js)
### Changed
- Remove AllowedLanguages from Tenant
- Remove Tenant URL (now in Make)
- Adapt chat client customisation options
### Fixed
- Several Bugfixes to administrative app
## [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] ## [2.3.3-alfa]
### Added ### Added

View File

@@ -10,7 +10,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<form method="POST" action="{{ url_for('interaction_bp.handle_specialist_selection') }}" id="specialistsForm"> <form method="POST" action="{{ url_for('interaction_bp.handle_specialist_selection') }}" id="specialistsForm">
{{ render_selectable_table(headers=["Specialist ID", "Name", "Type"], rows=rows, selectable=True, id="specialistsTable") }} {{ render_selectable_table(headers=["Specialist ID", "Name", "Type", "Type Version", "Active"], rows=rows, selectable=True, id="specialistsTable") }}
<div class="form-group mt-3 d-flex justify-content-between"> <div class="form-group mt-3 d-flex justify-content-between">
<div> <div>
<button type="submit" name="action" value="edit_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Edit Specialist</button> <button type="submit" name="action" value="edit_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Edit Specialist</button>

View File

@@ -8,7 +8,19 @@
{% endmacro %} {% endmacro %}
{% macro render_field_content(field, disabled=False, readonly=False, class='') %} {% 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 %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
{% elif field.type == 'BooleanField' %}
<div class="form-group"> <div class="form-group">
<div class="form-check form-switch"> <div class="form-check form-switch">
{{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }} {{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }}

View File

@@ -1,4 +1,4 @@
from flask import session from flask import session, current_app
from flask_security import current_user from flask_security import current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, SelectField from wtforms import StringField, SelectField
@@ -36,7 +36,7 @@ class SessionDefaultsForm(FlaskForm):
else: else:
self.partner_name.data = "" self.partner_name.data = ""
self.default_language.choices = [(lang, lang.lower()) for lang in self.default_language.choices = [(lang, lang.lower()) for lang in
session.get('tenant').get('allowed_languages')] current_app.config['SUPPORTED_LANGUAGES']]
self.default_language.data = session.get('default_language') self.default_language.data = session.get('default_language')
# Get a new session for catalog queries # Get a new session for catalog queries

View File

@@ -6,6 +6,7 @@ from wtforms.validators import DataRequired, Length, Optional, URL, ValidationEr
from flask_wtf.file import FileField, FileRequired from flask_wtf.file import FileField, FileRequired
import json import json
from wtforms.widgets.core import HiddenInput
from wtforms_sqlalchemy.fields import QuerySelectField from wtforms_sqlalchemy.fields import QuerySelectField
from common.extensions import cache_manager from common.extensions import cache_manager
@@ -20,7 +21,7 @@ from .dynamic_form_base import DynamicFormBase
def validate_catalog_name(form, field): def validate_catalog_name(form, field):
# Controleer of een catalog met deze naam al bestaat # Controleer of een catalog met deze naam al bestaat
existing_catalog = Catalog.query.filter_by(name=field.data).first() existing_catalog = Catalog.query.filter_by(name=field.data).first()
if existing_catalog: if existing_catalog and (not hasattr(form, 'id') or form.id.data != existing_catalog.id):
raise ValidationError(f'A Catalog with name "{field.data}" already exists. Choose another name.') raise ValidationError(f'A Catalog with name "{field.data}" already exists. Choose another name.')
@@ -48,6 +49,7 @@ class CatalogForm(FlaskForm):
class EditCatalogForm(DynamicFormBase): class EditCatalogForm(DynamicFormBase):
id = IntegerField('ID', widget=HiddenInput())
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name]) name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name])
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
@@ -188,7 +190,7 @@ class AddDocumentForm(DynamicFormBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.language.choices = [(language, language) for language in self.language.choices = [(language, language) for language in
session.get('tenant').get('allowed_languages')] current_app.config['SUPPORTED_LANGUAGES']]
if not self.language.data: if not self.language.data:
self.language.data = session.get('tenant').get('default_language') self.language.data = session.get('tenant').get('default_language')
@@ -208,7 +210,7 @@ class AddURLForm(DynamicFormBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.language.choices = [(language, language) for language in self.language.choices = [(language, language) for language in
session.get('tenant').get('allowed_languages')] current_app.config['SUPPORTED_LANGUAGES']]
if not self.language.data: if not self.language.data:
self.language.data = session.get('tenant').get('default_language') self.language.data = session.get('tenant').get('default_language')

View File

@@ -3,12 +3,14 @@ from datetime import date
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField, from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
validators, ValidationError) validators, ValidationError)
from flask import current_app, request from flask import current_app, request, session
import json import json
from wtforms.fields.choices import SelectField from wtforms.fields.choices import SelectField
from wtforms.fields.datetime import DateField from wtforms.fields.datetime import DateField
from wtforms.fields.simple import ColorField from wtforms.fields.simple import ColorField
from common.models.user import TenantMake
from common.utils.config_field_types import TaggingFields, json_to_patterns, patterns_to_json from common.utils.config_field_types import TaggingFields, json_to_patterns, patterns_to_json
@@ -300,6 +302,22 @@ class DynamicFormBase(FlaskForm):
except Exception as e: except Exception as e:
raise ValidationError(f"Invalid ordered list: {str(e)}") raise ValidationError(f"Invalid ordered list: {str(e)}")
def _get_system_field(self, system_name):
"""Get the field class and kwargs for a system field. Add system field cases as you need them."""
field_class = None
extra_classes = ''
field_kwargs = {}
match system_name:
case 'tenant_make':
field_class = SelectField
tenant_id = session.get('tenant').get('id')
makes = TenantMake.query.filter_by(tenant_id=tenant_id).all()
choices = [(make.name, make.name) for make in makes]
extra_classes = ''
field_kwargs = {'choices': choices}
return field_class, extra_classes, field_kwargs
def add_dynamic_fields(self, collection_name, config, initial_data=None): def add_dynamic_fields(self, collection_name, config, initial_data=None):
"""Add dynamic fields to the form based on the configuration. """Add dynamic fields to the form based on the configuration.
@@ -357,11 +375,12 @@ class DynamicFormBase(FlaskForm):
extra_classes = ['monospace-text', 'pattern-input'] extra_classes = ['monospace-text', 'pattern-input']
field_kwargs = {} field_kwargs = {}
elif field_type == 'ordered_list': elif field_type == 'ordered_list':
current_app.logger.debug(f"Adding ordered list field for {full_field_name}")
field_class = OrderedListField field_class = OrderedListField
extra_classes = '' extra_classes = ''
list_type = field_def.get('list_type', '') list_type = field_def.get('list_type', '')
field_kwargs = {'list_type': list_type} field_kwargs = {'list_type': list_type}
elif field_type == 'system':
field_class, extra_classes, field_kwargs = self._get_system_field(field_def.get('system_name', ''))
else: else:
extra_classes = '' extra_classes = ''
field_class = { field_class = {

View File

@@ -8,6 +8,7 @@ from wtforms_sqlalchemy.fields import QuerySelectMultipleField
from common.models.document import Retriever from common.models.document import Retriever
from common.models.interaction import EveAITool, Specialist from common.models.interaction import EveAITool, Specialist
from common.models.user import TenantMake
from common.extensions import cache_manager from common.extensions import cache_manager
from common.utils.form_assistants import validate_json from common.utils.form_assistants import validate_json
@@ -24,6 +25,7 @@ def get_tools():
class SpecialistForm(FlaskForm): class SpecialistForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)]) name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()])
retrievers = QuerySelectMultipleField( retrievers = QuerySelectMultipleField(
'Retrievers', 'Retrievers',
@@ -34,7 +36,7 @@ class SpecialistForm(FlaskForm):
) )
type = SelectField('Specialist Type', validators=[DataRequired()]) type = SelectField('Specialist Type', validators=[DataRequired()])
active = BooleanField('Active', validators=[Optional()], default=True)
tuning = BooleanField('Enable Specialist Tuning', default=False) tuning = BooleanField('Enable Specialist Tuning', default=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -47,6 +49,7 @@ class SpecialistForm(FlaskForm):
class EditSpecialistForm(DynamicFormBase): class EditSpecialistForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired()]) name = StringField('Name', validators=[DataRequired()])
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
active = BooleanField('Active', validators=[Optional()], default=True)
retrievers = QuerySelectMultipleField( retrievers = QuerySelectMultipleField(
'Retrievers', 'Retrievers',
@@ -148,7 +151,7 @@ class SpecialistMagicLinkForm(FlaskForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
specialists = Specialist.query.all() specialists = Specialist.query.all()
# Dynamically populate the 'type' field using the constructor # Dynamically populate the specialist field
self.specialist_id.choices = [(specialist.id, specialist.name) for specialist in specialists] self.specialist_id.choices = [(specialist.id, specialist.name) for specialist in specialists]
@@ -159,6 +162,7 @@ class EditSpecialistMagicLinkForm(DynamicFormBase):
render_kw={'readonly': True}) render_kw={'readonly': True})
specialist_id = IntegerField('Specialist', validators=[DataRequired()], render_kw={'readonly': True}) specialist_id = IntegerField('Specialist', validators=[DataRequired()], render_kw={'readonly': True})
specialist_name = StringField('Specialist Name', 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)
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()]) valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()]) valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
@@ -174,5 +178,10 @@ class EditSpecialistMagicLinkForm(DynamicFormBase):
else: else:
self.specialist_name.data = '' self.specialist_name.data = ''
# 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]

View File

@@ -162,6 +162,7 @@ def specialist():
new_specialist.type = form.type.data new_specialist.type = form.type.data
new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version( new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version(
new_specialist.type) new_specialist.type)
new_specialist.active = form.active.data
new_specialist.tuning = form.tuning.data new_specialist.tuning = form.tuning.data
set_logging_information(new_specialist, dt.now(tz.utc)) set_logging_information(new_specialist, dt.now(tz.utc))
@@ -231,6 +232,7 @@ def edit_specialist(specialist_id):
specialist.name = form.name.data specialist.name = form.name.data
specialist.description = form.description.data specialist.description = form.description.data
specialist.tuning = form.tuning.data specialist.tuning = form.tuning.data
specialist.active = form.active.data
# Update the configuration dynamic fields # Update the configuration dynamic fields
specialist.configuration = form.get_dynamic_data("configuration") specialist.configuration = form.get_dynamic_data("configuration")
@@ -297,7 +299,7 @@ def specialists():
# prepare table data # prepare table data
rows = prepare_table_for_macro(the_specialists, rows = prepare_table_for_macro(the_specialists,
[('id', ''), ('name', ''), ('type', '')]) [('id', ''), ('name', ''), ('type', ''), ('type_version', ''), ('active', ''),])
# Render the catalogs in a template # Render the catalogs in a template
return render_template('interaction/specialists.html', rows=rows, pagination=pagination) return render_template('interaction/specialists.html', rows=rows, pagination=pagination)
@@ -689,7 +691,7 @@ def specialist_magic_link():
try: try:
new_specialist_magic_link = SpecialistMagicLink() new_specialist_magic_link = SpecialistMagicLink()
# Populate fields individually instead of using populate_obj (gives problem with QueryMultipleSelectField) # Populate fields individually instead of using populate_obj
form.populate_obj(new_specialist_magic_link) form.populate_obj(new_specialist_magic_link)
set_logging_information(new_specialist_magic_link, dt.now(tz.utc)) set_logging_information(new_specialist_magic_link, dt.now(tz.utc))
@@ -699,6 +701,14 @@ def specialist_magic_link():
new_spec_ml_tenant.magic_link_code = new_specialist_magic_link.magic_link_code new_spec_ml_tenant.magic_link_code = new_specialist_magic_link.magic_link_code
new_spec_ml_tenant.tenant_id = tenant_id 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.specialist_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_specialist_magic_link)
db.session.add(new_spec_ml_tenant) db.session.add(new_spec_ml_tenant)
@@ -731,12 +741,23 @@ def edit_specialist_magic_link(specialist_magic_link_id):
form.add_dynamic_fields("arguments", specialist_config, specialist_ml.specialist_args) form.add_dynamic_fields("arguments", specialist_config, specialist_ml.specialist_args)
# Set the tenant_make_id default value
if request.method == 'GET':
if specialist_ml.tenant_make_id is None:
form.tenant_make_id.data = 0
else:
form.tenant_make_id.data = specialist_ml.tenant_make_id
if form.validate_on_submit(): if form.validate_on_submit():
# Update the basic fields # Update the basic fields
form.populate_obj(specialist_ml) form.populate_obj(specialist_ml)
# Update the arguments dynamic fields # Update the arguments dynamic fields
specialist_ml.specialist_args = form.get_dynamic_data("arguments") specialist_ml.specialist_args = form.get_dynamic_data("arguments")
# Handle the tenant_make_id special case (0 = None)
if form.tenant_make_id.data == 0:
specialist_ml.tenant_make_id = None
# Update logging information # Update logging information
update_logging_information(specialist_ml, dt.now(tz.utc)) update_logging_information(specialist_ml, dt.now(tz.utc))

View File

@@ -2,10 +2,12 @@ from flask import current_app, session
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import (StringField, BooleanField, SubmitField, EmailField, IntegerField, DateField, from wtforms import (StringField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
SelectField, SelectMultipleField, FieldList, FormField, TextAreaField) SelectField, SelectMultipleField, FieldList, FormField, TextAreaField)
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, ValidationError
import pytz import pytz
from flask_security import current_user from flask_security import current_user
from wtforms.widgets.core import HiddenInput
from common.models.user import TenantMake
from common.services.user import UserServices from common.services.user import UserServices
from config.type_defs.service_types import SERVICE_TYPES from config.type_defs.service_types import SERVICE_TYPES
from eveai_app.views.dynamic_form_base import DynamicFormBase from eveai_app.views.dynamic_form_base import DynamicFormBase
@@ -18,7 +20,6 @@ class TenantForm(FlaskForm):
website = StringField('Website', validators=[DataRequired(), Length(max=255)]) website = StringField('Website', validators=[DataRequired(), Length(max=255)])
# language fields # language fields
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()]) default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
allowed_languages = SelectMultipleField('Allowed Languages', choices=[], validators=[DataRequired()])
# invoicing fields # invoicing fields
currency = SelectField('Currency', choices=[], validators=[DataRequired()]) currency = SelectField('Currency', choices=[], validators=[DataRequired()])
# Timezone # Timezone
@@ -33,13 +34,56 @@ class TenantForm(FlaskForm):
super(TenantForm, self).__init__(*args, **kwargs) super(TenantForm, self).__init__(*args, **kwargs)
# initialise language fields # initialise language fields
self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']] 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 # initialise currency field
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']] self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
# initialise timezone # initialise timezone
self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones] self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones]
# Initialize fallback algorithms # Initialize fallback algorithms
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']] 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
timezone = SelectField('Timezone', choices=[], validators=[DataRequired()])
# Default tenant make
default_tenant_make_id = SelectField('Default Tenant Make', choices=[], validators=[Optional()])
# 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(EditTenantForm, 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 = 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]
# Add empty choice
self.default_tenant_make_id.choices.insert(0, ('', 'Geen'))
# Show field only for Super Users with partner in session # Show field only for Super Users with partner in session
if not current_user.has_roles('Super User') or 'partner' not in session: if not current_user.has_roles('Super User') or 'partner' not in session:
self._fields.pop('assign_to_partner', None) self._fields.pop('assign_to_partner', None)
@@ -132,8 +176,30 @@ class EditTenantProjectForm(FlaskForm):
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()] self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]
def validate_make_name(form, field):
# Check if tenant_make already exists in the database
existing_make = TenantMake.query.filter_by(name=field.data).first()
if existing_make:
current_app.logger.debug(f'Existing make: {existing_make.id}')
current_app.logger.debug(f'Form has id: {hasattr(form, 'id')}')
if hasattr(form, 'id'):
current_app.logger.debug(f'Form has id: {form.id.data}')
if existing_make:
if not hasattr(form, 'id') or form.id.data != existing_make.id:
raise ValidationError(f'A Make with name "{field.data}" already exists. Choose another name.')
class TenantMakeForm(DynamicFormBase): class TenantMakeForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired(), Length(max=50)]) 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()]) description = TextAreaField('Description', validators=[Optional()])
active = BooleanField('Active', validators=[Optional()], default=True) active = BooleanField('Active', validators=[Optional()], default=True)
website = StringField('Website', validators=[DataRequired(), Length(max=255)]) website = StringField('Website', validators=[DataRequired(), Length(max=255)])

View File

@@ -1,3 +1,4 @@
import json
import uuid 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, flash, render_template, Blueprint, session, current_app from flask import request, redirect, flash, render_template, Blueprint, session, current_app
@@ -11,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 common.utils.security_utils import send_confirmation_email, send_reset_email
from config.type_defs.service_types import SERVICE_TYPES from config.type_defs.service_types import SERVICE_TYPES
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \ from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
TenantProjectForm, EditTenantProjectForm, TenantMakeForm TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm, EditTenantMakeForm
from common.utils.database import Database from common.utils.database import Database
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
from common.utils.simple_encryption import generate_api_key from common.utils.simple_encryption import generate_api_key
@@ -111,12 +112,18 @@ def tenant():
@roles_accepted('Super User', 'Partner Admin') @roles_accepted('Super User', 'Partner Admin')
def edit_tenant(tenant_id): def edit_tenant(tenant_id):
tenant = Tenant.query.get_or_404(tenant_id) # This will return a 404 if no tenant is found 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(): if form.validate_on_submit():
# Populate the tenant with form data # Populate the tenant with form data
form.populate_obj(tenant) form.populate_obj(tenant)
# Convert default_tenant_make_id to integer if not empty
if form.default_tenant_make_id.data:
tenant.default_tenant_make_id = int(form.default_tenant_make_id.data)
else:
tenant.default_tenant_make_id = None
db.session.commit() db.session.commit()
flash('Tenant updated successfully.', 'success') flash('Tenant updated successfully.', 'success')
if session.get('tenant'): if session.get('tenant'):
@@ -460,8 +467,18 @@ def edit_tenant_domain(tenant_domain_id):
def tenant_overview(): def tenant_overview():
tenant_id = session['tenant']['id'] tenant_id = session['tenant']['id']
tenant = Tenant.query.get_or_404(tenant_id) tenant = Tenant.query.get_or_404(tenant_id)
form = TenantForm(obj=tenant) form = EditTenantForm(obj=tenant)
return render_template('user/tenant_overview.html', form=form)
# Zet de waarde van default_tenant_make_id
if tenant.default_tenant_make_id:
form.default_tenant_make_id.data = str(tenant.default_tenant_make_id)
# Haal de naam van de default make op als deze bestaat
default_make_name = None
if tenant.default_tenant_make:
default_make_name = tenant.default_tenant_make.name
return render_template('user/tenant_overview.html', form=form, default_make_name=default_make_name)
@user_bp.route('/tenant_project', methods=['GET', 'POST']) @user_bp.route('/tenant_project', methods=['GET', 'POST'])
@@ -627,15 +644,17 @@ def delete_tenant_project(tenant_project_id):
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def tenant_make(): def tenant_make():
form = TenantMakeForm() form = TenantMakeForm()
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
default_customisation_options = create_default_config_from_type_config(customisation_config["configuration"])
form.add_dynamic_fields("configuration", customisation_config, default_customisation_options)
if form.validate_on_submit(): if form.validate_on_submit():
tenant_id = session['tenant']['id'] tenant_id = session['tenant']['id']
new_tenant_make = TenantMake() new_tenant_make = TenantMake()
form.populate_obj(new_tenant_make) form.populate_obj(new_tenant_make)
new_tenant_make.tenant_id = tenant_id new_tenant_make.tenant_id = tenant_id
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION") customisation_options = form.get_dynamic_data("configuration")
new_tenant_make.chat_customisation_options = create_default_config_from_type_config( new_tenant_make.chat_customisation_options = json.dumps(customisation_options)
customisation_config["configuration"])
form.add_dynamic_fields("configuration", customisation_config, new_tenant_make.chat_customisation_options)
set_logging_information(new_tenant_make, dt.now(tz.utc)) set_logging_information(new_tenant_make, dt.now(tz.utc))
try: try:
@@ -660,7 +679,8 @@ def tenant_makes():
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, 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) pagination = query.paginate(page=page, per_page=per_page)
tenant_makes = pagination.items tenant_makes = pagination.items
@@ -681,7 +701,7 @@ def edit_tenant_make(tenant_make_id):
tenant_make = TenantMake.query.get_or_404(tenant_make_id) tenant_make = TenantMake.query.get_or_404(tenant_make_id)
# Create form instance with the tenant make # Create form instance with the tenant make
form = TenantMakeForm(request.form, obj=tenant_make) form = EditTenantMakeForm(request.form, obj=tenant_make)
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION") customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
form.add_dynamic_fields("configuration", customisation_config, tenant_make.chat_customisation_options) form.add_dynamic_fields("configuration", customisation_config, tenant_make.chat_customisation_options)
@@ -724,6 +744,28 @@ def handle_tenant_make_selection():
if action == 'edit_tenant_make': if action == 'edit_tenant_make':
return redirect(prefixed_url_for('user_bp.edit_tenant_make', tenant_make_id=tenant_make_id)) return redirect(prefixed_url_for('user_bp.edit_tenant_make', tenant_make_id=tenant_make_id))
elif action == 'set_as_default':
# Set this make as the default for the tenant
tenant_id = session['tenant']['id']
tenant = Tenant.query.get(tenant_id)
tenant.default_tenant_make_id = tenant_make_id
try:
db.session.commit()
flash(f'Default tenant make updated successfully.', 'success')
# Update session data if necessary
if 'tenant' in session:
session['tenant'] = tenant.to_dict()
return None
return None
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Failed to update default tenant make. Error: {str(e)}', 'danger')
current_app.logger.error(f'Failed to update default tenant make. Error: {str(e)}')
return redirect(prefixed_url_for('user_bp.tenant_makes'))
return None
def reset_uniquifier(user): def reset_uniquifier(user):
security.datastore.set_uniquifier(user) security.datastore.set_uniquifier(user)

View File

@@ -1,6 +1,7 @@
import logging import logging
import os import os
from flask import Flask, jsonify
from flask import Flask, jsonify, request
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
import logging.config 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(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})")
app.logger.info("-------------------------------------------------------------------------------------------------") 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 return app

View File

@@ -4,28 +4,153 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}EveAI Chat{% endblock %}</title> <title>{% block title %}EveAI Chat{% endblock %}</title>
<!-- CSS --> <!-- CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}">
<!-- Vue.js -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Markdown parser for explanation text -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Custom theme colors from tenant settings --> <!-- Custom theme colors from tenant settings -->
<style> <style>
:root { :root {
--primary-color: {{ customization.primary_color|default('#007bff') }}; --primary-color: {{ customisation.primary_color|default('#007bff') }};
--secondary-color: {{ customization.secondary_color|default('#6c757d') }}; --secondary-color: {{ customisation.secondary_color|default('#6c757d') }};
--background-color: {{ customization.background_color|default('#ffffff') }}; --background-color: {{ customisation.background_color|default('#ffffff') }};
--text-color: {{ customization.text_color|default('#212529') }}; --text-color: {{ customisation.text_color|default('#212529') }};
--sidebar-color: {{ customization.sidebar_color|default('#f8f9fa') }}; --sidebar-color: {{ customisation.sidebar_color|default('#f8f9fa') }};
--sidebar-background: {{ customisation.sidebar_background|default('#2c3e50') }};
--gradient-start-color: {{ customisation.gradient_start_color|default('#f5f7fa') }};
--gradient-end-color: {{ customisation.gradient_end_color|default('#c3cfe2') }};
--markdown-background-color: {{ customisation.markdown_background_color|default('transparent') }};
--markdown-text-color: {{ customisation.markdown_text_color|default('#ffffff') }};
}
body, html {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial, sans-serif;
}
.app-container {
display: flex;
height: 100vh;
width: 100%;
}
.sidebar {
width: 300px;
background-color: var(--sidebar-background);
color: white;
padding: 20px;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.sidebar-logo {
text-align: center;
margin-bottom: 20px;
}
.sidebar-logo img {
max-width: 100%;
max-height: 100px;
}
.sidebar-make-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.sidebar-explanation {
margin-top: 20px;
overflow-y: auto;
background-color: var(--markdown-background-color);
color: var(--markdown-text-color);
padding: 10px;
border-radius: 5px;
}
/* Ensure all elements in the markdown content inherit the text color */
.sidebar-explanation * {
color: inherit;
}
/* Style links in the markdown content */
.sidebar-explanation a {
color: var(--primary-color);
text-decoration: underline;
}
.content-area {
flex: 1;
background: linear-gradient(135deg, var(--gradient-start-color), var(--gradient-end-color));
overflow-y: auto;
display: flex;
flex-direction: column;
}
.chat-container {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
} }
</style> </style>
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<div class="container"> <div id="app" class="app-container">
{% block content %}{% endblock %} <!-- Left sidebar - never changes -->
<div class="sidebar">
<div class="sidebar-logo">
<img src="{{ tenant_make.logo_url|default('') }}" alt="{{ tenant_make.name|default('Logo') }}">
</div>
<div class="sidebar-make-name">
{{ tenant_make.name|default('') }}
</div>
<div class="sidebar-explanation" v-html="compiledExplanation"></div>
</div>
<!-- Right content area - contains the chat client -->
<div class="content-area">
<div class="chat-container">
{% block content %}{% endblock %}
</div>
</div>
</div> </div>
<script>
Vue.createApp({
data() {
return {
explanation: `{{ customisation.sidebar_markdown|default('') }}`
}
},
computed: {
compiledExplanation: function() {
// Handle different versions of the marked library
if (typeof marked === 'function') {
return marked(this.explanation);
} else if (marked && typeof marked.parse === 'function') {
return marked.parse(this.explanation);
} else {
console.error('Marked library not properly loaded');
return this.explanation; // Fallback to raw text
}
}
}
}).mount('#app');
</script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -3,13 +3,32 @@ from flask import Blueprint, render_template, request, session, current_app, jso
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db 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.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction
from common.services.interaction.specialist_services import SpecialistServices from common.services.interaction.specialist_services import SpecialistServices
from common.utils.database import Database from common.utils.database import Database
from common.utils.chat_utils import get_default_chat_customisation 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('/') @chat_bp.route('/')
def index(): def index():
@@ -31,14 +50,12 @@ def chat(magic_link_code):
current_app.logger.error(f"Invalid magic link code: {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.") return render_template('error.html', message="Invalid magic link code.")
tenant_id = magic_link_tenant.tenant_id
# Get tenant information # Get tenant information
tenant_id = magic_link_tenant.tenant_id
tenant = Tenant.query.get(tenant_id) tenant = Tenant.query.get(tenant_id)
if not tenant: if not tenant:
current_app.logger.error(f"Tenant not found for ID: {tenant_id}") current_app.logger.error(f"Tenant not found for ID: {tenant_id}")
return render_template('error.html', message="Tenant not found.") return render_template('error.html', message="Tenant not found.")
# Switch to tenant schema # Switch to tenant schema
Database(tenant_id).switch_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}") 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.") 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 # Get specialist details
specialist = Specialist.query.get(specialist_ml.specialist_id) specialist = Specialist.query.get(specialist_ml.specialist_id)
if not specialist: if not specialist:
@@ -55,21 +78,22 @@ def chat(magic_link_code):
return render_template('error.html', message="Specialist not found.") return render_template('error.html', message="Specialist not found.")
# Store necessary information in session # Store necessary information in session
session['tenant_id'] = tenant_id session['tenant'] = tenant.to_dict()
session['specialist_id'] = specialist_ml.specialist_id session['specialist'] = specialist.to_dict()
session['specialist_args'] = specialist_ml.specialist_args or {} session['magic_link'] = specialist_ml.to_dict()
session['magic_link_code'] = magic_link_code session['tenant_make'] = tenant_make.to_dict()
# Get customisation options with defaults # 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 # Start a new chat session
session['chat_session_id'] = SpecialistServices.start_session() session['chat_session_id'] = SpecialistServices.start_session()
return render_template('chat.html', return render_template('chat.html',
tenant=tenant, tenant=tenant,
specialist=specialist, tenant_make=tenant_make,
customisation=customisation) specialist=specialist,
customisation=customisation)
except Exception as e: except Exception as e:
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True) current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)

View File

@@ -0,0 +1,198 @@
import asyncio
import json
from os import wait
from typing import Optional, List
from crewai.flow.flow import start, listen, and_
from flask import current_app
from pydantic import BaseModel, Field
from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db
from common.models.user import Tenant
from common.models.interaction import Specialist
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments
from eveai_chat_workers.outputs.traicie.competencies.competencies_v1_1 import Competencies
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
from common.services.interaction.specialist_services import SpecialistServices
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
"""
type: TRAICIE_ROLE_DEFINITION_SPECIALIST
type_version: 1.0
Traicie Role Definition Specialist Executor class
"""
def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs):
self.role_definition_crew = None
super().__init__(tenant_id, specialist_id, session_id, task_id)
# Load the Tenant & set language
self.tenant = Tenant.query.get_or_404(tenant_id)
@property
def type(self) -> str:
return "TRAICIE_ROLE_DEFINITION_SPECIALIST"
@property
def type_version(self) -> str:
return "1.3"
def _config_task_agents(self):
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
def _config_pydantic_outputs(self):
self._add_pydantic_output("traicie_get_competencies_task", Competencies, "competencies")
def _instantiate_specialist(self):
verbose = self.tuning
role_definition_agents = [self.traicie_hr_bp_agent]
role_definition_tasks = [self.traicie_get_competencies_task]
self.role_definition_crew = EveAICrewAICrew(
self,
"Role Definition Crew",
agents=role_definition_agents,
tasks=role_definition_tasks,
verbose=verbose,
)
self.flow = RoleDefinitionFlow(
self,
self.role_definition_crew
)
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
self.log_tuning("Traicie Role Definition Specialist execution started", {})
flow_inputs = {
"vacancy_text": arguments.vacancy_text,
"role_name": arguments.role_name,
'role_reference': arguments.role_reference,
}
flow_results = self.flow.kickoff(inputs=flow_inputs)
flow_state = self.flow.state
results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
if flow_state.competencies:
results.competencies = flow_state.competencies
self.create_selection_specialist(arguments, flow_state.competencies)
self.log_tuning(f"Traicie Role Definition Specialist execution ended", {"Results": results.model_dump()})
return results
def create_selection_specialist(self, arguments: SpecialistArguments, competencies: List[ListItem]):
"""This method creates a new TRAICIE_SELECTION_SPECIALIST specialist with the given competencies."""
current_app.logger.info(f"Creating selection with arguments: {arguments.model_dump()}")
selection_comptencies = []
for competency in competencies:
selection_competency = {
"title": competency.title,
"description": competency.description,
"assess": True,
"is_knockout": False,
}
selection_comptencies.append(selection_competency)
selection_config = {
"name": arguments.specialist_name,
"competencies": selection_comptencies,
"tone_of_voice": "Professional & Neutral",
"language_level": "Standard",
"role_reference": arguments.role_reference,
"make": arguments.make,
}
name = arguments.role_name
if len(name) > 50:
name = name[:47] + "..."
new_specialist = Specialist(
name=name,
description=f"Specialist for {arguments.role_name} role",
type="TRAICIE_SELECTION_SPECIALIST",
type_version="1.1",
tuning=False,
configuration=selection_config,
)
try:
db.session.add(new_specialist)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(f"Error creating selection specialist: {str(e)}")
raise e
SpecialistServices.initialize_specialist(new_specialist.id, "TRAICIE_SELECTION_SPECIALIST", "1.0")
class RoleDefinitionSpecialistInput(BaseModel):
role_name: str = Field(..., alias="role_name")
role_reference: Optional[str] = Field(..., alias="role_reference")
vacancy_text: Optional[str] = Field(None, alias="vacancy_text")
class RoleDefinitionSpecialistResult(SpecialistResult):
competencies: Optional[List[ListItem]] = None
class RoleDefFlowState(EveAIFlowState):
"""Flow state for Traicie Role Definition specialist that automatically updates from task outputs"""
input: Optional[RoleDefinitionSpecialistInput] = None
competencies: Optional[List[ListItem]] = None
class RoleDefinitionFlow(EveAICrewAIFlow[RoleDefFlowState]):
def __init__(self,
specialist_executor: CrewAIBaseSpecialistExecutor,
role_definitiion_crew: EveAICrewAICrew,
**kwargs):
super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs)
self.specialist_executor = specialist_executor
self.role_definition_crew = role_definitiion_crew
self.exception_raised = False
@start()
def process_inputs(self):
return ""
@listen(process_inputs)
async def execute_role_definition (self):
inputs = self.state.input.model_dump()
try:
current_app.logger.debug("In execute_role_definition")
crew_output = await self.role_definition_crew.kickoff_async(inputs=inputs)
# Unfortunately, crew_output will only contain the output of the latest task.
# As we will only take into account the flow state, we need to ensure both competencies and criteria
# are copies to the flow state.
update = {}
for task in self.role_definition_crew.tasks:
current_app.logger.debug(f"Task {task.name} output:\n{task.output}")
if task.name == "traicie_get_competencies_task":
# update["competencies"] = task.output.pydantic.competencies
self.state.competencies = task.output.pydantic.competencies
# crew_output.pydantic = crew_output.pydantic.model_copy(update=update)
current_app.logger.debug(f"State after execute_role_definition: {self.state}")
current_app.logger.debug(f"State dump after execute_role_definition: {self.state.model_dump()}")
return crew_output
except Exception as e:
current_app.logger.error(f"CREW execute_role_definition Kickoff Error: {str(e)}")
self.exception_raised = True
raise e
async def kickoff_async(self, inputs=None):
current_app.logger.debug(f"Async kickoff {self.name}")
current_app.logger.debug(f"Inputs: {inputs}")
self.state.input = RoleDefinitionSpecialistInput.model_validate(inputs)
current_app.logger.debug(f"State: {self.state}")
result = await super().kickoff_async(inputs)
return self.state

View File

@@ -0,0 +1,197 @@
import asyncio
import json
from os import wait
from typing import Optional, List
from crewai.flow.flow import start, listen, and_
from flask import current_app
from pydantic import BaseModel, Field
from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db
from common.models.user import Tenant
from common.models.interaction import Specialist
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments
from eveai_chat_workers.outputs.traicie.competencies.competencies_v1_1 import Competencies
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
from common.services.interaction.specialist_services import SpecialistServices
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
"""
type: TRAICIE_SELECTION_SPECIALIST
type_version: 1.0
Traicie Selection Specialist Executor class
"""
def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs):
self.role_definition_crew = None
super().__init__(tenant_id, specialist_id, session_id, task_id)
# Load the Tenant & set language
self.tenant = Tenant.query.get_or_404(tenant_id)
@property
def type(self) -> str:
return "TRAICIE_SELECTION_SPECIALIST"
@property
def type_version(self) -> str:
return "1.0"
def _config_task_agents(self):
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
def _config_pydantic_outputs(self):
self._add_pydantic_output("traicie_get_competencies_task", Competencies, "competencies")
def _instantiate_specialist(self):
verbose = self.tuning
role_definition_agents = [self.traicie_hr_bp_agent]
role_definition_tasks = [self.traicie_get_competencies_task]
self.role_definition_crew = EveAICrewAICrew(
self,
"Role Definition Crew",
agents=role_definition_agents,
tasks=role_definition_tasks,
verbose=verbose,
)
self.flow = RoleDefinitionFlow(
self,
self.role_definition_crew
)
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
self.log_tuning("Traicie Role Definition Specialist execution started", {})
flow_inputs = {
"vacancy_text": arguments.vacancy_text,
"role_name": arguments.role_name,
'role_reference': arguments.role_reference,
}
flow_results = self.flow.kickoff(inputs=flow_inputs)
flow_state = self.flow.state
results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
if flow_state.competencies:
results.competencies = flow_state.competencies
self.create_selection_specialist(arguments, flow_state.competencies)
self.log_tuning(f"Traicie Role Definition Specialist execution ended", {"Results": results.model_dump()})
return results
def create_selection_specialist(self, arguments: SpecialistArguments, competencies: List[ListItem]):
"""This method creates a new TRAICIE_SELECTION_SPECIALIST specialist with the given competencies."""
current_app.logger.info(f"Creating selection with arguments: {arguments.model_dump()}")
selection_comptencies = []
for competency in competencies:
selection_competency = {
"title": competency.title,
"description": competency.description,
"assess": True,
"is_knockout": False,
}
selection_comptencies.append(selection_competency)
selection_config = {
"name": arguments.specialist_name,
"competencies": selection_comptencies,
"tone_of_voice": "Professional & Neutral",
"language_level": "Standard",
"role_reference": arguments.role_reference,
}
name = arguments.role_name
if len(name) > 50:
name = name[:47] + "..."
new_specialist = Specialist(
name=name,
description=f"Specialist for {arguments.role_name} role",
type="TRAICIE_SELECTION_SPECIALIST",
type_version="1.0",
tuning=False,
configuration=selection_config,
)
try:
db.session.add(new_specialist)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(f"Error creating selection specialist: {str(e)}")
raise e
SpecialistServices.initialize_specialist(new_specialist.id, "TRAICIE_SELECTION_SPECIALIST", "1.0")
class RoleDefinitionSpecialistInput(BaseModel):
role_name: str = Field(..., alias="role_name")
role_reference: Optional[str] = Field(..., alias="role_reference")
vacancy_text: Optional[str] = Field(None, alias="vacancy_text")
class RoleDefinitionSpecialistResult(SpecialistResult):
competencies: Optional[List[ListItem]] = None
class RoleDefFlowState(EveAIFlowState):
"""Flow state for Traicie Role Definition specialist that automatically updates from task outputs"""
input: Optional[RoleDefinitionSpecialistInput] = None
competencies: Optional[List[ListItem]] = None
class RoleDefinitionFlow(EveAICrewAIFlow[RoleDefFlowState]):
def __init__(self,
specialist_executor: CrewAIBaseSpecialistExecutor,
role_definitiion_crew: EveAICrewAICrew,
**kwargs):
super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs)
self.specialist_executor = specialist_executor
self.role_definition_crew = role_definitiion_crew
self.exception_raised = False
@start()
def process_inputs(self):
return ""
@listen(process_inputs)
async def execute_role_definition (self):
inputs = self.state.input.model_dump()
try:
current_app.logger.debug("In execute_role_definition")
crew_output = await self.role_definition_crew.kickoff_async(inputs=inputs)
# Unfortunately, crew_output will only contain the output of the latest task.
# As we will only take into account the flow state, we need to ensure both competencies and criteria
# are copies to the flow state.
update = {}
for task in self.role_definition_crew.tasks:
current_app.logger.debug(f"Task {task.name} output:\n{task.output}")
if task.name == "traicie_get_competencies_task":
# update["competencies"] = task.output.pydantic.competencies
self.state.competencies = task.output.pydantic.competencies
# crew_output.pydantic = crew_output.pydantic.model_copy(update=update)
current_app.logger.debug(f"State after execute_role_definition: {self.state}")
current_app.logger.debug(f"State dump after execute_role_definition: {self.state.model_dump()}")
return crew_output
except Exception as e:
current_app.logger.error(f"CREW execute_role_definition Kickoff Error: {str(e)}")
self.exception_raised = True
raise e
async def kickoff_async(self, inputs=None):
current_app.logger.debug(f"Async kickoff {self.name}")
current_app.logger.debug(f"Inputs: {inputs}")
self.state.input = RoleDefinitionSpecialistInput.model_validate(inputs)
current_app.logger.debug(f"State: {self.state}")
result = await super().kickoff_async(inputs)
return self.state

View File

@@ -0,0 +1,35 @@
"""Add default TenantMake to Tenant model
Revision ID: 83d4e90f87c6
Revises: f40d16a0965a
Create Date: 2025-06-09 15:42:51.503696
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '83d4e90f87c6'
down_revision = 'f40d16a0965a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenant', schema=None) as batch_op:
batch_op.add_column(sa.Column('default_tenant_make_id', sa.Integer(), nullable=True))
batch_op.create_foreign_key(None, 'tenant_make', ['default_tenant_make_id'], ['id'], referent_schema='public')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenant', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.drop_column('default_tenant_make_id')
# ### end Alembic commands ###

View File

@@ -0,0 +1,31 @@
"""Make TenantMake name unique
Revision ID: f40d16a0965a
Revises: 200bda7f5251
Create Date: 2025-06-09 06:15:56.791634
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f40d16a0965a'
down_revision = '200bda7f5251'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenant_make', schema=None) as batch_op:
batch_op.create_unique_constraint(None, ['name'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenant_make', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
# ### end Alembic commands ###

View File

@@ -0,0 +1,30 @@
"""Add tenant_make reference to SpecialistMagicLink
Revision ID: 2b6ae6cc923e
Revises: a179785e5362
Create Date: 2025-06-09 15:59:39.157066
"""
from alembic import op
import sqlalchemy as sa
import pgvector
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '2b6ae6cc923e'
down_revision = 'a179785e5362'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('specialist_magic_link', sa.Column('tenant_make_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'specialist_magic_link', 'tenant_make', ['tenant_make_id'], ['id'], referent_schema='public', ondelete='CASCADE')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@@ -0,0 +1,29 @@
"""Add active field to Specialist
Revision ID: a179785e5362
Revises: c71facc0ce7e
Create Date: 2025-06-09 08:30:18.532600
"""
from alembic import op
import sqlalchemy as sa
import pgvector
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'a179785e5362'
down_revision = 'c71facc0ce7e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('specialist', sa.Column('active', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('specialist', 'active')
# ### end Alembic commands ###

View File

@@ -82,7 +82,6 @@ def initialize_default_tenant():
'website': 'https://www.askeveai.com', 'website': 'https://www.askeveai.com',
'timezone': 'UTC', 'timezone': 'UTC',
'default_language': 'en', 'default_language': 'en',
'allowed_languages': ['en', 'fr', 'nl', 'de', 'es'],
'type': 'Active', 'type': 'Active',
'currency': '', 'currency': '',
'created_at': dt.now(tz.utc), 'created_at': dt.now(tz.utc),