Compare commits
12 Commits
v2.3.1-alf
...
v2.3.5-alf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67ceb57b79 | ||
|
|
23b49516cb | ||
|
|
9cc266b97f | ||
|
|
3f77871c4f | ||
|
|
199cf94cf2 | ||
|
|
c4dcd6a0d3 | ||
|
|
43ee9139d6 | ||
|
|
8f45005713 | ||
|
|
bc1626c4ff | ||
|
|
57c0e7a1ba | ||
|
|
0d05499d2b | ||
|
|
b4e58659a8 |
@@ -11,6 +11,7 @@ from flask_restx import Api
|
||||
from prometheus_flask_exporter import PrometheusMetrics
|
||||
|
||||
from .utils.cache.eveai_cache_manager import EveAICacheManager
|
||||
from .utils.content_utils import ContentManager
|
||||
from .utils.simple_encryption import SimpleEncryption
|
||||
from .utils.minio_utils import MinioClient
|
||||
|
||||
@@ -30,4 +31,5 @@ simple_encryption = SimpleEncryption()
|
||||
minio_client = MinioClient()
|
||||
metrics = PrometheusMetrics.for_app_factory()
|
||||
cache_manager = EveAICacheManager()
|
||||
content_manager = ContentManager()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import sqlalchemy as sa
|
||||
|
||||
class Catalog(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
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)
|
||||
type = db.Column(db.String(50), nullable=False, default="STANDARD_CATALOG")
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
from ..extensions import db
|
||||
from .user import User, Tenant
|
||||
from .user import User, Tenant, TenantMake
|
||||
from .document import Embedding, Retriever
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class Specialist(db.Model):
|
||||
tuning = db.Column(db.Boolean, nullable=True, default=False)
|
||||
configuration = 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
|
||||
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_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):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -215,3 +231,36 @@ class SpecialistDispatcher(db.Model):
|
||||
dispatcher_id = db.Column(db.Integer, db.ForeignKey(Dispatcher.id, ondelete='CASCADE'), primary_key=True)
|
||||
|
||||
dispatcher = db.relationship("Dispatcher", backref="specialist_dispatchers")
|
||||
|
||||
|
||||
class SpecialistMagicLink(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
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)
|
||||
|
||||
valid_from = db.Column(db.DateTime, nullable=True)
|
||||
valid_to = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
specialist_args = db.Column(JSONB, nullable=True)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||
created_by = db.Column(db.Integer, db.ForeignKey(User.id), nullable=True)
|
||||
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"<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,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import date
|
||||
|
||||
from common.extensions import db
|
||||
from flask_security import UserMixin, RoleMixin
|
||||
from sqlalchemy.dialects.postgresql import ARRAY
|
||||
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||
import sqlalchemy as sa
|
||||
|
||||
from common.models.entitlements import License
|
||||
@@ -28,17 +28,19 @@ 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)
|
||||
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
|
||||
users = db.relationship('User', backref='tenant')
|
||||
domains = db.relationship('TenantDomain', backref='tenant')
|
||||
licenses = db.relationship('License', back_populates='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
|
||||
def current_license(self):
|
||||
@@ -60,8 +62,8 @@ 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,
|
||||
}
|
||||
|
||||
|
||||
@@ -173,6 +175,42 @@ class TenantProject(db.Model):
|
||||
return f"<TenantProject {self.id}: {self.name}>"
|
||||
|
||||
|
||||
class TenantMake(db.Model):
|
||||
__bind_key__ = 'public'
|
||||
__table_args__ = {'schema': 'public'}
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
|
||||
name = db.Column(db.String(50), nullable=False, unique=True)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
active = db.Column(db.Boolean, nullable=False, default=True)
|
||||
website = db.Column(db.String(255), nullable=True)
|
||||
logo_url = db.Column(db.String(255), nullable=True)
|
||||
|
||||
# Chat customisation options
|
||||
chat_customisation_options = db.Column(JSONB, nullable=True)
|
||||
|
||||
# Versioning Information
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||
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"<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):
|
||||
__bind_key__ = 'public'
|
||||
__table_args__ = {'schema': 'public'}
|
||||
@@ -271,3 +309,11 @@ class PartnerTenant(db.Model):
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||
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'), nullable=True)
|
||||
|
||||
|
||||
class SpecialistMagicLinkTenant(db.Model):
|
||||
__bind_key__ = 'public'
|
||||
__table_args__ = {'schema': 'public'}
|
||||
|
||||
magic_link_code = db.Column(db.String(55), primary_key=True)
|
||||
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from common.extensions import db
|
||||
from common.models.entitlements import PartnerServiceLicenseTier
|
||||
from common.models.user import Partner
|
||||
from common.utils.eveai_exceptions import EveAINoManagementPartnerService
|
||||
from common.utils.eveai_exceptions import EveAINoManagementPartnerService, EveAINoSessionPartner
|
||||
from common.utils.model_logging_utils import set_logging_information
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class LicenseTierServices:
|
||||
# Get partner service (MANAGEMENT_SERVICE type)
|
||||
partner = Partner.query.get(partner_id)
|
||||
if not partner:
|
||||
return
|
||||
raise EveAINoSessionPartner()
|
||||
|
||||
# Find a management service for this partner
|
||||
management_service = next((service for service in session['partner']['services']
|
||||
|
||||
@@ -220,3 +220,18 @@ 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):
|
||||
"""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
|
||||
|
||||
@@ -28,7 +28,7 @@ class TenantServices:
|
||||
if service.get('type') == 'MANAGEMENT_SERVICE'), None)
|
||||
|
||||
if not management_service:
|
||||
current_app.logger.error(f"No Management Service defined for partner {partner_id}"
|
||||
current_app.logger.error(f"No Management Service defined for partner {partner_id} "
|
||||
f"while associating tenant {tenant_id} with partner.")
|
||||
raise EveAINoManagementPartnerService()
|
||||
|
||||
|
||||
15
common/utils/cache/config_cache.py
vendored
15
common/utils/cache/config_cache.py
vendored
@@ -7,7 +7,7 @@ from flask import current_app
|
||||
|
||||
from common.utils.cache.base import CacheHandler, CacheKey
|
||||
from config.type_defs import agent_types, task_types, tool_types, specialist_types, retriever_types, prompt_types, \
|
||||
catalog_types, partner_service_types, processor_types
|
||||
catalog_types, partner_service_types, processor_types, customisation_types
|
||||
|
||||
|
||||
def is_major_minor(version: str) -> bool:
|
||||
@@ -463,7 +463,6 @@ ProcessorConfigCacheHandler, ProcessorConfigVersionTreeCacheHandler, ProcessorCo
|
||||
types_module=processor_types.PROCESSOR_TYPES
|
||||
))
|
||||
|
||||
# Add to common/utils/cache/config_cache.py
|
||||
PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, PartnerServiceConfigTypesCacheHandler = (
|
||||
create_config_cache_handlers(
|
||||
config_type='partner_services',
|
||||
@@ -471,6 +470,14 @@ PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, P
|
||||
types_module=partner_service_types.PARTNER_SERVICE_TYPES
|
||||
))
|
||||
|
||||
CustomisationConfigCacheHandler, CustomisationConfigVersionTreeCacheHandler, CustomisationConfigTypesCacheHandler = (
|
||||
create_config_cache_handlers(
|
||||
config_type='customisations',
|
||||
config_dir='config/customisations',
|
||||
types_module=customisation_types.CUSTOMISATION_TYPES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def register_config_cache_handlers(cache_manager) -> None:
|
||||
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
||||
@@ -503,6 +510,9 @@ def register_config_cache_handlers(cache_manager) -> None:
|
||||
cache_manager.register_handler(PartnerServiceConfigCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(PartnerServiceConfigTypesCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(PartnerServiceConfigVersionTreeCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(CustomisationConfigCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(CustomisationConfigTypesCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(CustomisationConfigVersionTreeCacheHandler, 'eveai_config')
|
||||
|
||||
cache_manager.agents_config_cache.set_version_tree_cache(cache_manager.agents_version_tree_cache)
|
||||
cache_manager.tasks_config_cache.set_version_tree_cache(cache_manager.tasks_version_tree_cache)
|
||||
@@ -513,3 +523,4 @@ def register_config_cache_handlers(cache_manager) -> None:
|
||||
cache_manager.catalogs_config_cache.set_version_tree_cache(cache_manager.catalogs_version_tree_cache)
|
||||
cache_manager.processors_config_cache.set_version_tree_cache(cache_manager.processors_version_tree_cache)
|
||||
cache_manager.partner_services_config_cache.set_version_tree_cache(cache_manager.partner_services_version_tree_cache)
|
||||
cache_manager.customisations_config_cache.set_version_tree_cache(cache_manager.customisations_version_tree_cache)
|
||||
|
||||
45
common/utils/chat_utils.py
Normal file
45
common/utils/chat_utils.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Utility functions for chat customization.
|
||||
"""
|
||||
|
||||
def get_default_chat_customisation(tenant_customisation=None):
|
||||
"""
|
||||
Get chat customization options with default values for missing options.
|
||||
|
||||
Args:
|
||||
tenant_customization (dict, optional): The tenant's customization options.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing all customization options with default values
|
||||
for any missing options.
|
||||
"""
|
||||
# Default customization options
|
||||
default_customisation = {
|
||||
'primary_color': '#007bff',
|
||||
'secondary_color': '#6c757d',
|
||||
'background_color': '#ffffff',
|
||||
'text_color': '#212529',
|
||||
'sidebar_color': '#f8f9fa',
|
||||
'sidebar_background': '#2c3e50',
|
||||
'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?',
|
||||
}
|
||||
|
||||
# If no tenant customization is provided, return the defaults
|
||||
if tenant_customisation is None:
|
||||
return default_customisation
|
||||
|
||||
# Start with the default customization
|
||||
customisation = default_customisation.copy()
|
||||
|
||||
# Update with tenant customization
|
||||
for key, value in tenant_customisation.items():
|
||||
if key in customisation:
|
||||
customisation[key] = value
|
||||
|
||||
return customisation
|
||||
@@ -21,7 +21,7 @@ class TaggingField(BaseModel):
|
||||
@field_validator('type', mode='before')
|
||||
@classmethod
|
||||
def validate_type(cls, v: str) -> str:
|
||||
valid_types = ['string', 'integer', 'float', 'date', 'enum']
|
||||
valid_types = ['string', 'integer', 'float', 'date', 'enum', 'color']
|
||||
if v not in valid_types:
|
||||
raise ValueError(f'type must be one of {valid_types}')
|
||||
return v
|
||||
@@ -243,7 +243,7 @@ class ArgumentDefinition(BaseModel):
|
||||
@field_validator('type')
|
||||
@classmethod
|
||||
def validate_type(cls, v: str) -> str:
|
||||
valid_types = ['string', 'integer', 'float', 'date', 'enum']
|
||||
valid_types = ['string', 'integer', 'float', 'date', 'enum', 'color']
|
||||
if v not in valid_types:
|
||||
raise ValueError(f'type must be one of {valid_types}')
|
||||
return v
|
||||
@@ -256,7 +256,8 @@ class ArgumentDefinition(BaseModel):
|
||||
'integer': NumericConstraint,
|
||||
'float': NumericConstraint,
|
||||
'date': DateConstraint,
|
||||
'enum': EnumConstraint
|
||||
'enum': EnumConstraint,
|
||||
'color': StringConstraint
|
||||
}
|
||||
|
||||
expected_type = expected_constraint_types.get(self.type)
|
||||
|
||||
215
common/utils/content_utils.py
Normal file
215
common/utils/content_utils.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from packaging import version
|
||||
from flask import current_app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentManager:
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
if app:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
self.app = app
|
||||
|
||||
# Controleer of het pad bestaat
|
||||
if not os.path.exists(app.config['CONTENT_DIR']):
|
||||
logger.warning(f"Content directory not found at: {app.config['CONTENT_DIR']}")
|
||||
else:
|
||||
logger.info(f"Content directory configured at: {app.config['CONTENT_DIR']}")
|
||||
|
||||
def get_content_path(self, content_type, major_minor=None, patch=None):
|
||||
"""
|
||||
Geef het volledige pad naar een contentbestand
|
||||
|
||||
Args:
|
||||
content_type (str): Type content (bv. 'changelog', 'terms')
|
||||
major_minor (str, optional): Major.Minor versie (bv. '1.0')
|
||||
patch (str, optional): Patchnummer (bv. '5')
|
||||
|
||||
Returns:
|
||||
str: Volledige pad naar de content map of bestand
|
||||
"""
|
||||
content_path = os.path.join(self.app.config['CONTENT_DIR'], content_type)
|
||||
|
||||
if major_minor:
|
||||
content_path = os.path.join(content_path, major_minor)
|
||||
|
||||
if patch:
|
||||
content_path = os.path.join(content_path, f"{major_minor}.{patch}.md")
|
||||
|
||||
return content_path
|
||||
|
||||
def _parse_version(self, filename):
|
||||
"""Parse een versienummer uit een bestandsnaam"""
|
||||
match = re.match(r'(\d+\.\d+)\.(\d+)\.md', filename)
|
||||
if match:
|
||||
return match.group(1), match.group(2)
|
||||
return None, None
|
||||
|
||||
def get_latest_version(self, content_type, major_minor=None):
|
||||
"""
|
||||
Verkrijg de laatste versie van een bepaald contenttype
|
||||
|
||||
Args:
|
||||
content_type (str): Type content (bv. 'changelog', 'terms')
|
||||
major_minor (str, optional): Specifieke major.minor versie, anders de hoogste
|
||||
|
||||
Returns:
|
||||
tuple: (major_minor, patch, full_version) of None als niet gevonden
|
||||
"""
|
||||
try:
|
||||
# Basispad voor dit contenttype
|
||||
content_path = os.path.join(self.app.config['CONTENT_DIR'], content_type)
|
||||
|
||||
if not os.path.exists(content_path):
|
||||
logger.error(f"Content path does not exist: {content_path}")
|
||||
return None
|
||||
|
||||
# Als geen major_minor opgegeven, vind de hoogste
|
||||
if not major_minor:
|
||||
available_versions = os.listdir(content_path)
|
||||
if not available_versions:
|
||||
return None
|
||||
|
||||
# Sorteer op versienummer (major.minor)
|
||||
available_versions.sort(key=lambda v: version.parse(v))
|
||||
major_minor = available_versions[-1]
|
||||
|
||||
# Nu we major_minor hebben, zoek de hoogste patch
|
||||
major_minor_path = os.path.join(content_path, major_minor)
|
||||
|
||||
if not os.path.exists(major_minor_path):
|
||||
logger.error(f"Version path does not exist: {major_minor_path}")
|
||||
return None
|
||||
|
||||
files = os.listdir(major_minor_path)
|
||||
version_files = []
|
||||
|
||||
for file in files:
|
||||
mm, p = self._parse_version(file)
|
||||
if mm == major_minor and p:
|
||||
version_files.append((mm, p, f"{mm}.{p}"))
|
||||
|
||||
if not version_files:
|
||||
return None
|
||||
|
||||
# Sorteer op patch nummer
|
||||
version_files.sort(key=lambda v: int(v[1]))
|
||||
return version_files[-1]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding latest version for {content_type}: {str(e)}")
|
||||
return None
|
||||
|
||||
def read_content(self, content_type, major_minor=None, patch=None):
|
||||
"""
|
||||
Lees content met versieondersteuning
|
||||
|
||||
Als major_minor en patch niet zijn opgegeven, wordt de laatste versie gebruikt.
|
||||
Als alleen major_minor is opgegeven, wordt de laatste patch van die versie gebruikt.
|
||||
|
||||
Args:
|
||||
content_type (str): Type content (bv. 'changelog', 'terms')
|
||||
major_minor (str, optional): Major.Minor versie (bv. '1.0')
|
||||
patch (str, optional): Patchnummer (bv. '5')
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'content': str,
|
||||
'version': str,
|
||||
'content_type': str
|
||||
} of None bij fout
|
||||
"""
|
||||
try:
|
||||
# Als geen versie opgegeven, vind de laatste
|
||||
if not major_minor:
|
||||
version_info = self.get_latest_version(content_type)
|
||||
if not version_info:
|
||||
logger.error(f"No versions found for {content_type}")
|
||||
return None
|
||||
|
||||
major_minor, patch, full_version = version_info
|
||||
|
||||
# Als geen patch opgegeven, vind de laatste patch voor deze major_minor
|
||||
elif not patch:
|
||||
version_info = self.get_latest_version(content_type, major_minor)
|
||||
if not version_info:
|
||||
logger.error(f"No versions found for {content_type} {major_minor}")
|
||||
return None
|
||||
|
||||
major_minor, patch, full_version = version_info
|
||||
else:
|
||||
full_version = f"{major_minor}.{patch}"
|
||||
|
||||
# Nu hebben we major_minor en patch, lees het bestand
|
||||
file_path = self.get_content_path(content_type, major_minor, patch)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"Content file does not exist: {file_path}")
|
||||
return None
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
content = file.read()
|
||||
|
||||
return {
|
||||
'content': content,
|
||||
'version': full_version,
|
||||
'content_type': content_type
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading content {content_type} {major_minor}.{patch}: {str(e)}")
|
||||
return None
|
||||
|
||||
def list_content_types(self):
|
||||
"""Lijst alle beschikbare contenttypes op"""
|
||||
try:
|
||||
return [d for d in os.listdir(self.app.config['CONTENT_DIR'])
|
||||
if os.path.isdir(os.path.join(self.app.config['CONTENT_DIR'], d))]
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing content types: {str(e)}")
|
||||
return []
|
||||
|
||||
def list_versions(self, content_type):
|
||||
"""
|
||||
Lijst alle beschikbare versies voor een contenttype
|
||||
|
||||
Returns:
|
||||
list: Lijst van dicts met versie-informatie
|
||||
[{'version': '1.0.0', 'path': '/path/to/file', 'date_modified': datetime}]
|
||||
"""
|
||||
versions = []
|
||||
try:
|
||||
content_path = os.path.join(self.app.config['CONTENT_DIR'], content_type)
|
||||
|
||||
if not os.path.exists(content_path):
|
||||
return []
|
||||
|
||||
for major_minor in os.listdir(content_path):
|
||||
major_minor_path = os.path.join(content_path, major_minor)
|
||||
|
||||
if not os.path.isdir(major_minor_path):
|
||||
continue
|
||||
|
||||
for file in os.listdir(major_minor_path):
|
||||
mm, p = self._parse_version(file)
|
||||
if mm and p:
|
||||
file_path = os.path.join(major_minor_path, file)
|
||||
mod_time = os.path.getmtime(file_path)
|
||||
versions.append({
|
||||
'version': f"{mm}.{p}",
|
||||
'path': file_path,
|
||||
'date_modified': mod_time
|
||||
})
|
||||
|
||||
# Sorteer op versienummer
|
||||
versions.sort(key=lambda v: version.parse(v['version']))
|
||||
return versions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing versions for {content_type}: {str(e)}")
|
||||
return []
|
||||
@@ -38,6 +38,8 @@ def create_default_config_from_type_config(type_config):
|
||||
default_config[field_name] = 0
|
||||
elif field_type == "boolean":
|
||||
default_config[field_name] = False
|
||||
elif field_type == "color":
|
||||
default_config[field_name] = "#000000"
|
||||
else:
|
||||
default_config[field_name] = ""
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import traceback
|
||||
|
||||
import jinja2
|
||||
@@ -12,6 +13,7 @@ def not_found_error(error):
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(prefixed_url_for('security.login'))
|
||||
current_app.logger.error(f"Not Found Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error/404.html'), 404
|
||||
|
||||
|
||||
@@ -19,6 +21,7 @@ def internal_server_error(error):
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(prefixed_url_for('security.login'))
|
||||
current_app.logger.error(f"Internal Server Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error/500.html'), 500
|
||||
|
||||
|
||||
@@ -26,6 +29,7 @@ def not_authorised_error(error):
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(prefixed_url_for('security.login'))
|
||||
current_app.logger.error(f"Not Authorised Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error/401.html')
|
||||
|
||||
|
||||
@@ -33,6 +37,7 @@ def access_forbidden(error):
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(prefixed_url_for('security.login'))
|
||||
current_app.logger.error(f"Access Forbidden: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error/403.html')
|
||||
|
||||
|
||||
@@ -42,6 +47,7 @@ def key_error_handler(error):
|
||||
return redirect(prefixed_url_for('security.login'))
|
||||
# For other KeyErrors, you might want to log the error and return a generic error page
|
||||
current_app.logger.error(f"Key Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error/generic.html', error_message="An unexpected error occurred"), 500
|
||||
|
||||
|
||||
@@ -76,6 +82,7 @@ def no_tenant_selected_error(error):
|
||||
a long period of inactivity. The user will be redirected to the login page.
|
||||
"""
|
||||
current_app.logger.error(f"No Session Tenant Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
flash('Your session expired. You will have to re-enter your credentials', 'warning')
|
||||
|
||||
# Perform logout if user is authenticated
|
||||
@@ -95,6 +102,26 @@ def general_exception(e):
|
||||
error_details=str(e)), 500
|
||||
|
||||
|
||||
def template_not_found_error(error):
|
||||
"""Handle Jinja2 TemplateNotFound exceptions."""
|
||||
current_app.logger.error(f'Template not found: {error.name}')
|
||||
current_app.logger.error(f'Search Paths: {current_app.jinja_loader.list_templates()}')
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error/500.html',
|
||||
error_type="Template Not Found",
|
||||
error_details=f"Template '{error.name}' could not be found."), 404
|
||||
|
||||
|
||||
def template_syntax_error(error):
|
||||
"""Handle Jinja2 TemplateSyntaxError exceptions."""
|
||||
current_app.logger.error(f'Template syntax error: {error.message}')
|
||||
current_app.logger.error(f'In template {error.filename}, line {error.lineno}')
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error/500.html',
|
||||
error_type="Template Syntax Error",
|
||||
error_details=f"Error in template '{error.filename}' at line {error.lineno}: {error.message}"), 500
|
||||
|
||||
|
||||
def register_error_handlers(app):
|
||||
app.register_error_handler(404, not_found_error)
|
||||
app.register_error_handler(500, internal_server_error)
|
||||
@@ -103,17 +130,6 @@ def register_error_handlers(app):
|
||||
app.register_error_handler(EveAINoSessionTenant, no_tenant_selected_error)
|
||||
app.register_error_handler(KeyError, key_error_handler)
|
||||
app.register_error_handler(AttributeError, attribute_error_handler)
|
||||
app.register_error_handler(Exception, general_exception)
|
||||
|
||||
@app.errorhandler(jinja2.TemplateNotFound)
|
||||
def template_not_found(error):
|
||||
app.logger.error(f'Template not found: {error.name}')
|
||||
app.logger.error(f'Search Paths: {app.jinja_loader.list_templates()}')
|
||||
return f'Template not found: {error.name}. Check logs for details.', 404
|
||||
|
||||
@app.errorhandler(jinja2.TemplateSyntaxError)
|
||||
def template_syntax_error(error):
|
||||
app.logger.error(f'Template syntax error: {error.message}')
|
||||
app.logger.error(f'In template {error.filename}, line {error.lineno}')
|
||||
return f'Template syntax error: {error.message}', 500
|
||||
|
||||
app.register_error_handler(jinja2.TemplateNotFound, template_not_found_error)
|
||||
app.register_error_handler(jinja2.TemplateSyntaxError, template_syntax_error)
|
||||
app.register_error_handler(Exception, general_exception)
|
||||
25
config/agents/traicie/TRAICIE_RECRUITER/1.0.0.yaml
Normal file
25
config/agents/traicie/TRAICIE_RECRUITER/1.0.0.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
version: "1.0.0"
|
||||
name: "Traicie HR BP "
|
||||
role: >
|
||||
You are an Expert Recruiter working for {tenant_name}
|
||||
{custom_role}
|
||||
goal: >
|
||||
As an expert recruiter, you identify, attract, and secure top talent by building genuine relationships, deeply
|
||||
understanding business needs, and ensuring optimal alignment between candidate potential and organizational goals
|
||||
, while championing diversity, culture fit, and long-term retention.
|
||||
{custom_goal}
|
||||
backstory: >
|
||||
You started your career in a high-pressure agency setting, where you quickly learned the art of fast-paced hiring and
|
||||
relationship building. Over the years, you moved in-house, partnering closely with business leaders to shape
|
||||
recruitment strategies that go beyond filling roles—you focus on finding the right people to drive growth and culture.
|
||||
With a strong grasp of both tech and non-tech profiles, you’ve adapted to changing trends, from remote work to
|
||||
AI-driven sourcing. You’re more than a recruiter—you’re a trusted advisor, a brand ambassador, and a connector of
|
||||
people and purpose.
|
||||
{custom_backstory}
|
||||
full_model_name: "mistral.mistral-medium-latest"
|
||||
temperature: 0.3
|
||||
metadata:
|
||||
author: "Josako"
|
||||
date_added: "2025-05-21"
|
||||
description: "HR BP Agent."
|
||||
changes: "Initial version"
|
||||
@@ -172,6 +172,9 @@ class Config(object):
|
||||
# Entitlement Constants
|
||||
ENTITLEMENTS_MAX_PENDING_DAYS = 5 # Defines the maximum number of days a pending entitlement can be active
|
||||
|
||||
# Content Directory for static content like the changelog, terms & conditions, privacy statement, ...
|
||||
CONTENT_DIR = '/app/content'
|
||||
|
||||
|
||||
class DevConfig(Config):
|
||||
DEVELOPMENT = True
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
version: "1.0.0"
|
||||
name: "Chat Client Customisation"
|
||||
configuration:
|
||||
"primary_color":
|
||||
name: "Primary Color"
|
||||
description: "Primary Color"
|
||||
type: "color"
|
||||
required: false
|
||||
"secondary_color":
|
||||
name: "Secondary Color"
|
||||
description: "Secondary Color"
|
||||
type: "color"
|
||||
required: false
|
||||
"background_color":
|
||||
name: "Background Color"
|
||||
description: "Background Color"
|
||||
type: "color"
|
||||
required: false
|
||||
"text_color":
|
||||
name: "Text Color"
|
||||
description: "Text Color"
|
||||
type: "color"
|
||||
required: false
|
||||
"sidebar_color":
|
||||
name: "Sidebar Color"
|
||||
description: "Sidebar Color"
|
||||
type: "color"
|
||||
required: false
|
||||
"sidebar_background":
|
||||
name: "Sidebar Background"
|
||||
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"
|
||||
required: false
|
||||
"welcome_message":
|
||||
name: "Welcome Message"
|
||||
description: "Text to be shown as Welcome"
|
||||
type: "text"
|
||||
required: false
|
||||
metadata:
|
||||
author: "Josako"
|
||||
date_added: "2024-06-06"
|
||||
changes: "Initial version"
|
||||
description: "Parameters allowing to customise the chat client"
|
||||
@@ -303,10 +303,10 @@ LOGGING = {
|
||||
'backupCount': 2,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
'file_chat': {
|
||||
'file_chat_client': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/eveai_chat.log',
|
||||
'filename': 'logs/eveai_chat_client.log',
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 2,
|
||||
'formatter': 'standard',
|
||||
@@ -432,8 +432,8 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'propagate': False
|
||||
},
|
||||
'eveai_chat': { # logger for the eveai_chat
|
||||
'handlers': ['file_chat', 'graylog', ] if env == 'production' else ['file_chat', ],
|
||||
'eveai_chat_client': { # logger for the eveai_chat
|
||||
'handlers': ['file_chat_client', 'graylog', ] if env == 'production' else ['file_chat_client', ],
|
||||
'level': 'DEBUG',
|
||||
'propagate': False
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: "1.1.0"
|
||||
version: "1.2.0"
|
||||
name: "Traicie Role Definition Specialist"
|
||||
framework: "crewai"
|
||||
partner: "traicie"
|
||||
@@ -11,9 +11,9 @@ arguments:
|
||||
type: "str"
|
||||
required: true
|
||||
specialist_name:
|
||||
name: "Specialist Name"
|
||||
description: "The name the specialist will be called upon"
|
||||
type: str
|
||||
name: "Chatbot Name"
|
||||
description: "The name of the chatbot."
|
||||
type: "str"
|
||||
required: true
|
||||
role_reference:
|
||||
name: "Role Reference"
|
||||
|
||||
@@ -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"
|
||||
@@ -68,10 +68,32 @@ competency_details:
|
||||
required: true
|
||||
default: true
|
||||
arguments:
|
||||
vacancy_text:
|
||||
name: "vacancy_text"
|
||||
type: "text"
|
||||
description: "The Vacancy Text"
|
||||
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:
|
||||
|
||||
@@ -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"
|
||||
7
config/type_defs/customisation_types.py
Normal file
7
config/type_defs/customisation_types.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# Catalog Types
|
||||
CUSTOMISATION_TYPES = {
|
||||
"CHAT_CLIENT_CUSTOMISATION": {
|
||||
"name": "Chat Client Customisation",
|
||||
"description": "Parameters allowing to customise the chat client",
|
||||
},
|
||||
}
|
||||
@@ -5,6 +5,57 @@ 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.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]
|
||||
|
||||
### Added
|
||||
- Add Tenant Make
|
||||
- Add Chat Client customisation options to Tenant Make
|
||||
|
||||
### Changed
|
||||
- Catalog name must be unique to avoid mistakes
|
||||
|
||||
### Fixed
|
||||
- Ensure document version is selected in UI before trying to view it.
|
||||
- Remove obsolete tab from tenant overview
|
||||
|
||||
## [2.3.2-alfa]
|
||||
|
||||
### Added
|
||||
- Changelog display
|
||||
- Introduction of Specialist Magic Links
|
||||
|
||||
### Fixed
|
||||
- dynamic fields for adding documents / urls to dossier catalog
|
||||
- tabs in latest bootstrap version no longer functional
|
||||
- partner association of license tier not working when no partner selected
|
||||
- data-type dynamic field needs conversion to isoformat
|
||||
- Add public tables to env.py of tenant schema
|
||||
|
||||
## [2.3.1-alfa]
|
||||
|
||||
### Added
|
||||
@@ -16,18 +67,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Role Definition Specialist creates Selection Specialist from generated competencies
|
||||
- Improvements to Selection Specialist (Agent definition to be started)
|
||||
|
||||
### Deprecated
|
||||
- For soon-to-be removed features.
|
||||
|
||||
### Removed
|
||||
- For now removed features.
|
||||
|
||||
### Fixed
|
||||
- For any bug fixes.
|
||||
|
||||
### Security
|
||||
- In case of vulnerabilities.
|
||||
|
||||
## [2.3.0-alfa]
|
||||
|
||||
### Added
|
||||
@@ -47,7 +86,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Introduction of ChatSession (Specialist Execution) follow-up in administrative interface
|
||||
- Introduce npm for javascript libraries usage and optimisations
|
||||
- Introduction of new top bar in administrative interface to show session defaults (removing old navbar buttons)
|
||||
-
|
||||
|
||||
### Changed
|
||||
- Add 'Register'-button to list views, replacing register menu-items
|
||||
@@ -105,9 +143,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixed
|
||||
- Set default language when registering Documents or URLs.
|
||||
|
||||
### Security
|
||||
- In case of vulnerabilities.
|
||||
|
||||
## [2.1.0-alfa]
|
||||
|
||||
### Added
|
||||
37
content/privacy/1.0/1.0.0.md
Normal file
37
content/privacy/1.0/1.0.0.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Privacy Policy
|
||||
|
||||
## Version 1.0.0
|
||||
|
||||
*Effective Date: 2025-06-03*
|
||||
|
||||
### 1. Introduction
|
||||
|
||||
This Privacy Policy describes how EveAI collects, uses, and discloses your information when you use our services.
|
||||
|
||||
### 2. Information We Collect
|
||||
|
||||
We collect information you provide directly to us, such as account information, content you process through our services, and communication data.
|
||||
|
||||
### 3. How We Use Your Information
|
||||
|
||||
We use your information to provide, maintain, and improve our services, process transactions, send communications, and comply with legal obligations.
|
||||
|
||||
### 4. Data Security
|
||||
|
||||
We implement appropriate security measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction.
|
||||
|
||||
### 5. International Data Transfers
|
||||
|
||||
Your information may be transferred to and processed in countries other than the country you reside in, where data protection laws may differ.
|
||||
|
||||
### 6. Your Rights
|
||||
|
||||
Depending on your location, you may have certain rights regarding your personal information, such as access, correction, deletion, or restriction of processing.
|
||||
|
||||
### 7. Changes to This Policy
|
||||
|
||||
We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.
|
||||
|
||||
### 8. Contact Us
|
||||
|
||||
If you have any questions about this Privacy Policy, please contact us at privacy@askeveai.be.
|
||||
37
content/terms/1.0/1.0.0.md
Normal file
37
content/terms/1.0/1.0.0.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Terms of Service
|
||||
|
||||
## Version 1.0.0
|
||||
|
||||
*Effective Date: 2025-06-03*
|
||||
|
||||
### 1. Introduction
|
||||
|
||||
Welcome to EveAI. By accessing or using our services, you agree to be bound by these Terms of Service.
|
||||
|
||||
### 2. Service Description
|
||||
|
||||
EveAI provides AI-powered solutions for businesses to optimize their operations through intelligent document processing and specialist execution.
|
||||
|
||||
### 3. User Accounts
|
||||
|
||||
To access certain features of the Service, you must register for an account. You are responsible for maintaining the confidentiality of your account information.
|
||||
|
||||
### 4. Privacy
|
||||
|
||||
Your use of the Service is also governed by our Privacy Policy, which can be found [here](/content/privacy).
|
||||
|
||||
### 5. Intellectual Property
|
||||
|
||||
All content, features, and functionality of the Service are owned by EveAI and are protected by international copyright, trademark, and other intellectual property laws.
|
||||
|
||||
### 6. Limitation of Liability
|
||||
|
||||
In no event shall EveAI be liable for any indirect, incidental, special, consequential or punitive damages.
|
||||
|
||||
### 7. Changes to Terms
|
||||
|
||||
We reserve the right to modify these Terms at any time. Your continued use of the Service after such modifications will constitute your acceptance of the new Terms.
|
||||
|
||||
### 8. Governing Law
|
||||
|
||||
These Terms shall be governed by the laws of Belgium.
|
||||
@@ -70,6 +70,7 @@ services:
|
||||
depends_on:
|
||||
- eveai_app
|
||||
- eveai_api
|
||||
- eveai_chat_client
|
||||
networks:
|
||||
- eveai-network
|
||||
|
||||
@@ -91,6 +92,7 @@ services:
|
||||
volumes:
|
||||
- ../eveai_app:/app/eveai_app
|
||||
- ../common:/app/common
|
||||
- ../content:/app/content
|
||||
- ../config:/app/config
|
||||
- ../migrations:/app/migrations
|
||||
- ../scripts:/app/scripts
|
||||
@@ -176,6 +178,44 @@ services:
|
||||
# networks:
|
||||
# - eveai-network
|
||||
|
||||
eveai_chat_client:
|
||||
image: josakola/eveai_chat_client:latest
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ./docker/eveai_chat_client/Dockerfile
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
ports:
|
||||
- 5004:5004
|
||||
expose:
|
||||
- 8000
|
||||
environment:
|
||||
<<: *common-variables
|
||||
COMPONENT_NAME: eveai_chat_client
|
||||
volumes:
|
||||
- ../eveai_chat_client:/app/eveai_chat_client
|
||||
- ../common:/app/common
|
||||
- ../config:/app/config
|
||||
- ../scripts:/app/scripts
|
||||
- ../patched_packages:/app/patched_packages
|
||||
- ./eveai_logs:/app/logs
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5004/healthz/ready"]
|
||||
interval: 30s
|
||||
timeout: 1s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- eveai-network
|
||||
|
||||
eveai_chat_workers:
|
||||
image: josakola/eveai_chat_workers:latest
|
||||
build:
|
||||
@@ -440,4 +480,3 @@ volumes:
|
||||
#secrets:
|
||||
# db-password:
|
||||
# file: ./db/password.txt
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ services:
|
||||
depends_on:
|
||||
- eveai_app
|
||||
- eveai_api
|
||||
- eveai_chat_client
|
||||
networks:
|
||||
- eveai-network
|
||||
restart: "no"
|
||||
@@ -106,6 +107,33 @@ services:
|
||||
- eveai-network
|
||||
restart: "no"
|
||||
|
||||
eveai_chat_client:
|
||||
image: josakola/eveai_chat_client:${EVEAI_VERSION:-latest}
|
||||
ports:
|
||||
- 5004:5004
|
||||
expose:
|
||||
- 8000
|
||||
environment:
|
||||
<<: *common-variables
|
||||
COMPONENT_NAME: eveai_chat_client
|
||||
volumes:
|
||||
- eveai_logs:/app/logs
|
||||
- crewai_storage:/app/crewai_storage
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5004/healthz/ready"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- eveai-network
|
||||
restart: "no"
|
||||
|
||||
eveai_chat_workers:
|
||||
image: josakola/eveai_chat_workers:${EVEAI_VERSION:-latest}
|
||||
expose:
|
||||
|
||||
@@ -56,6 +56,7 @@ COPY config /app/config
|
||||
COPY migrations /app/migrations
|
||||
COPY scripts /app/scripts
|
||||
COPY patched_packages /app/patched_packages
|
||||
COPY content /app/content
|
||||
|
||||
# Set permissions for entrypoint script
|
||||
RUN chmod 777 /app/scripts/entrypoint.sh
|
||||
|
||||
72
docker/eveai_chat_client/Dockerfile
Normal file
72
docker/eveai_chat_client/Dockerfile
Normal file
@@ -0,0 +1,72 @@
|
||||
ARG PYTHON_VERSION=3.12.7
|
||||
FROM python:${PYTHON_VERSION}-slim as base
|
||||
|
||||
# Prevents Python from writing pyc files.
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Keeps Python from buffering stdout and stderr to avoid situations where
|
||||
# the application crashes without emitting any logs due to buffering.
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Create directory for patched packages and set permissions
|
||||
RUN mkdir -p /app/patched_packages && \
|
||||
chmod 777 /app/patched_packages
|
||||
|
||||
# Ensure patches are applied to the application.
|
||||
ENV PYTHONPATH=/app/patched_packages:$PYTHONPATH
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Create a non-privileged user that the app will run under.
|
||||
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
||||
ARG UID=10001
|
||||
RUN adduser \
|
||||
--disabled-password \
|
||||
--gecos "" \
|
||||
--home "/nonexistent" \
|
||||
--shell "/bin/bash" \
|
||||
--no-create-home \
|
||||
--uid "${UID}" \
|
||||
appuser
|
||||
|
||||
# Install necessary packages and build tools
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
gcc \
|
||||
postgresql-client \
|
||||
curl \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create logs directory and set permissions
|
||||
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
|
||||
|
||||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||||
# Leverage a bind mount to requirements.txt to avoid having to copy them into
|
||||
# into this layer.
|
||||
|
||||
COPY requirements.txt /app/
|
||||
RUN python -m pip install -r /app/requirements.txt
|
||||
|
||||
# Copy the source code into the container.
|
||||
COPY eveai_chat_client /app/eveai_chat_client
|
||||
COPY common /app/common
|
||||
COPY config /app/config
|
||||
COPY scripts /app/scripts
|
||||
COPY patched_packages /app/patched_packages
|
||||
COPY content /app/content
|
||||
|
||||
# Set permissions for scripts
|
||||
RUN chmod 777 /app/scripts/entrypoint.sh && \
|
||||
chmod 777 /app/scripts/start_eveai_chat_client.sh
|
||||
|
||||
# Set ownership of the application directory to the non-privileged user
|
||||
RUN chown -R appuser:appuser /app
|
||||
|
||||
# Expose the port that the application listens on.
|
||||
EXPOSE 5004
|
||||
|
||||
# Set entrypoint and command
|
||||
ENTRYPOINT ["/app/scripts/entrypoint.sh"]
|
||||
CMD ["/app/scripts/start_eveai_chat_client.sh"]
|
||||
516
documentation/Eveai Chat Client Developer Documentation.md
Normal file
516
documentation/Eveai Chat Client Developer Documentation.md
Normal file
@@ -0,0 +1,516 @@
|
||||
# Evie Chat Client - Developer Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Evie Chat Client is a modern, customizable chat interface for interacting with eveai specialists. It supports both anonymous and authenticated modes, with initial focus on anonymous mode. The client provides real-time interaction with AI specialists, customizable tenant branding, European-compliant analytics tracking, and secure QR code access.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Anonymous Mode**: Public access with tenant UUID and API key authentication
|
||||
- **QR Code Access**: Secure pre-authenticated landing pages for QR code integration
|
||||
- **Real-time Communication**: Server-Sent Events (SSE) for live updates and intermediate states
|
||||
- **Tenant Customization**: Simple CSS variable-based theming with visual editor
|
||||
- **Multiple Choice Options**: Dynamic button/dropdown responses from specialists
|
||||
- **Chat History**: Persistent ChatSession and Interaction storage
|
||||
- **File Upload Support**: Planned for future implementation
|
||||
- **European Analytics**: Umami integration for GDPR-compliant tracking
|
||||
|
||||
## Architecture
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
evie-project/
|
||||
├── common/ # Shared code across components
|
||||
│ ├── services/ # Reusable business logic
|
||||
│ │ ├── chat_service.py # Chat session management
|
||||
│ │ ├── specialist_service.py # Specialist interaction wrapper
|
||||
│ │ ├── tenant_service.py # Tenant config & theming
|
||||
│ │ └── qr_service.py # QR code session management
|
||||
│ └── utils/ # Utility functions
|
||||
│ ├── auth.py # API key validation
|
||||
│ ├── tracking.py # Umami analytics integration
|
||||
│ └── qr_utils.py # QR code generation utilities
|
||||
├── eveai_chat_client/ # Chat client component
|
||||
│ ├── app.py # Flask app entry point
|
||||
│ ├── routes/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── chat_routes.py # Main chat interface routes
|
||||
│ │ ├── api_routes.py # SSE/API endpoints
|
||||
│ │ └── qr_routes.py # QR code landing pages
|
||||
│ └── templates/
|
||||
│ ├── base.html # Base template
|
||||
│ ├── chat.html # Main chat interface
|
||||
│ ├── qr_expired.html # QR code error page
|
||||
│ └── components/
|
||||
│ ├── message.html # Individual message component
|
||||
│ ├── options.html # Multiple choice options
|
||||
│ └── thinking.html # Intermediate states display
|
||||
└── eveai_app/ # Admin interface (existing)
|
||||
└── qr_management/ # QR code creation interface
|
||||
├── create_qr.py
|
||||
└── qr_templates.html
|
||||
```
|
||||
|
||||
### Integration Approach
|
||||
|
||||
- **Services Layer**: Direct integration with common/services for better performance
|
||||
- **Database**: Utilizes existing ChatSession and Interaction models
|
||||
- **Caching**: Leverages existing Redis setup
|
||||
- **Static Files**: Uses existing nginx/static structure
|
||||
|
||||
## QR Code Access Flow
|
||||
|
||||
### QR Code System Architecture
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Admin as Admin (eveai_app)
|
||||
participant QRService as QR Service (common)
|
||||
participant PublicDB as Public Schema
|
||||
participant TenantDB as Tenant Schema
|
||||
participant User as End User
|
||||
participant ChatClient as Chat Client
|
||||
participant ChatSession as Chat Session
|
||||
|
||||
%% QR Code Creation Flow
|
||||
Admin->>QRService: Create QR code with specialist config
|
||||
QRService->>PublicDB: Store qr_lookup (qr_id → tenant_code)
|
||||
QRService->>TenantDB: Store qr_sessions (full config + args)
|
||||
QRService->>Admin: Return QR code image with /qr/{qr_id}
|
||||
|
||||
%% QR Code Usage Flow
|
||||
User->>ChatClient: Scan QR → GET /qr/{qr_id}
|
||||
ChatClient->>PublicDB: Lookup tenant_code by qr_id
|
||||
ChatClient->>TenantDB: Get full QR session data
|
||||
ChatClient->>ChatSession: Create ChatSession with pre-filled args
|
||||
ChatClient->>User: Set temp auth + redirect to chat interface
|
||||
User->>ChatClient: Access chat with pre-authenticated session
|
||||
```
|
||||
|
||||
### QR Code Data Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Admin Creates QR Code] --> B[Generate UUID for QR Session]
|
||||
B --> C[Store Lookup in Public Schema]
|
||||
C --> D[Store Full Data in Tenant Schema]
|
||||
D --> E[Generate QR Code Image]
|
||||
|
||||
F[User Scans QR Code] --> G[Extract QR Session ID from URL]
|
||||
G --> H[Lookup Tenant Code in Public Schema]
|
||||
H --> I[Retrieve Full QR Data from Tenant Schema]
|
||||
I --> J{QR Valid & Not Expired?}
|
||||
J -->|No| K[Show Error Page]
|
||||
J -->|Yes| L[Create ChatSession with Pre-filled Args]
|
||||
L --> M[Set Temporary Browser Authentication]
|
||||
M --> N[Redirect to Chat Interface]
|
||||
N --> O[Start Chat with Specialist]
|
||||
```
|
||||
|
||||
## URL Structure & Parameters
|
||||
|
||||
### Main Chat Interface
|
||||
```
|
||||
GET /chat/{tenant_code}/{specialist_id}
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `api_key` (required for direct access): Tenant API key for authentication
|
||||
- `session` (optional): Existing chat session ID
|
||||
- `utm_source`, `utm_campaign`, `utm_medium` (optional): Analytics tracking
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
# Direct access
|
||||
/chat/550e8400-e29b-41d4-a716-446655440000/document-analyzer?api_key=xxx&utm_source=email
|
||||
|
||||
# QR code access (after redirect)
|
||||
/chat/550e8400-e29b-41d4-a716-446655440000/document-analyzer?session=abc123-def456
|
||||
```
|
||||
|
||||
### QR Code Landing Pages
|
||||
```
|
||||
GET /qr/{qr_session_id} # QR code entry point (redirects, no HTML page)
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
```
|
||||
POST /api/chat/{tenant_code}/interact # Send message to specialist
|
||||
GET /api/chat/{tenant_code}/status/{session_id} # SSE endpoint for updates
|
||||
```
|
||||
|
||||
## Authentication & Security
|
||||
|
||||
### Anonymous Mode Access Methods
|
||||
|
||||
1. **Direct Access**: URL with API key parameter
|
||||
2. **QR Code Access**: Pre-authenticated via secure landing page
|
||||
|
||||
### QR Code Security Model
|
||||
- **QR Code Contains**: Only a UUID session identifier
|
||||
- **Sensitive Data**: Stored securely in tenant database schema
|
||||
- **Usage Control**: Configurable expiration and usage limits
|
||||
- **Audit Trail**: Track QR code creation and usage
|
||||
|
||||
### Security Considerations
|
||||
- Use tenant UUIDs to prevent enumeration attacks
|
||||
- Validate API keys against tenant database
|
||||
- Implement CORS policies for cross-origin requests
|
||||
- Sanitize all user messages and file uploads
|
||||
- QR sessions have configurable expiration and usage limits
|
||||
|
||||
## QR Code Management
|
||||
|
||||
### Database Schema
|
||||
|
||||
#### Public Schema (Routing Only)
|
||||
```sql
|
||||
CREATE TABLE qr_lookup (
|
||||
qr_session_id UUID PRIMARY KEY,
|
||||
tenant_code UUID NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
INDEX idx_tenant_code (tenant_code)
|
||||
);
|
||||
```
|
||||
|
||||
#### Tenant Schema (Full QR Data)
|
||||
```sql
|
||||
CREATE TABLE qr_sessions (
|
||||
id UUID PRIMARY KEY,
|
||||
specialist_id UUID NOT NULL,
|
||||
api_key VARCHAR(255) NOT NULL,
|
||||
specialist_args JSONB,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
expires_at TIMESTAMP,
|
||||
usage_count INTEGER DEFAULT 0,
|
||||
usage_limit INTEGER,
|
||||
created_by_user_id UUID
|
||||
);
|
||||
```
|
||||
|
||||
### QR Code Creation (eveai_app)
|
||||
```python
|
||||
# In eveai_app admin interface
|
||||
from common.services.qr_service import QRService
|
||||
|
||||
def create_specialist_qr_code():
|
||||
qr_data = {
|
||||
'tenant_code': current_tenant.code,
|
||||
'specialist_id': selected_specialist.id,
|
||||
'api_key': current_tenant.api_key,
|
||||
'specialist_args': {
|
||||
'department': 'sales',
|
||||
'language': 'en',
|
||||
'context': 'product_inquiry'
|
||||
},
|
||||
'metadata': {
|
||||
'name': 'Sales Support QR - Product Brochure',
|
||||
'usage_limit': 500,
|
||||
'expires_days': 90
|
||||
}
|
||||
}
|
||||
|
||||
qr_service = QRService()
|
||||
qr_session_id, qr_image = qr_service.create_qr_session(qr_data)
|
||||
return qr_image
|
||||
```
|
||||
|
||||
### QR Code Processing (eveai_chat_client)
|
||||
```python
|
||||
# In eveai_chat_client routes
|
||||
from common.services.qr_service import QRService
|
||||
from common.services.chat_service import ChatService
|
||||
|
||||
@app.route('/qr/<qr_session_id>')
|
||||
def handle_qr_code(qr_session_id):
|
||||
qr_service = QRService()
|
||||
qr_data = qr_service.get_and_validate_qr_session(qr_session_id)
|
||||
|
||||
if not qr_data:
|
||||
return render_template('qr_expired.html'), 410
|
||||
|
||||
# Create ChatSession with pre-filled arguments
|
||||
chat_service = ChatService()
|
||||
chat_session = chat_service.create_session(
|
||||
tenant_code=qr_data['tenant_code'],
|
||||
specialist_id=qr_data['specialist_id'],
|
||||
initial_args=qr_data['specialist_args'],
|
||||
source='qr_code'
|
||||
)
|
||||
|
||||
# Set temporary authentication
|
||||
flask_session['qr_auth'] = {
|
||||
'tenant_code': qr_data['tenant_code'],
|
||||
'api_key': qr_data['api_key'],
|
||||
'chat_session_id': chat_session.id,
|
||||
'expires_at': datetime.utcnow() + timedelta(hours=24)
|
||||
}
|
||||
|
||||
# Redirect to chat interface
|
||||
return redirect(f"/chat/{qr_data['tenant_code']}/{qr_data['specialist_id']}?session={chat_session.id}")
|
||||
```
|
||||
|
||||
## Real-time Communication
|
||||
|
||||
### Server-Sent Events (SSE)
|
||||
- **Connection**: Long-lived SSE connection per chat session
|
||||
- **Message Types**:
|
||||
- `message`: Complete specialist response
|
||||
- `thinking`: Intermediate processing states
|
||||
- `options`: Multiple choice response options
|
||||
- `error`: Error messages
|
||||
- `complete`: Interaction completion
|
||||
|
||||
### SSE Message Format
|
||||
```json
|
||||
{
|
||||
"type": "thinking",
|
||||
"data": {
|
||||
"message": "Analyzing your request...",
|
||||
"step": 1,
|
||||
"total_steps": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tenant Customization
|
||||
|
||||
### Theme Configuration
|
||||
Stored in tenant table as JSONB column:
|
||||
```sql
|
||||
ALTER TABLE tenants ADD COLUMN theme_config JSONB;
|
||||
```
|
||||
|
||||
### CSS Variables Approach
|
||||
Inline CSS variables in chat template:
|
||||
```css
|
||||
:root {
|
||||
/* Brand Colors */
|
||||
--primary-color: {{ tenant.theme_config.primary_color or '#007bff' }};
|
||||
--secondary-color: {{ tenant.theme_config.secondary_color or '#6c757d' }};
|
||||
--accent-color: {{ tenant.theme_config.accent_color or '#28a745' }};
|
||||
|
||||
/* Chat Interface */
|
||||
--user-message-bg: {{ tenant.theme_config.user_message_bg or 'var(--primary-color)' }};
|
||||
--bot-message-bg: {{ tenant.theme_config.bot_message_bg or '#f8f9fa' }};
|
||||
--chat-bg: {{ tenant.theme_config.chat_bg or '#ffffff' }};
|
||||
|
||||
/* Typography */
|
||||
--font-family: {{ tenant.theme_config.font_family or 'system-ui, -apple-system, sans-serif' }};
|
||||
--font-size-base: {{ tenant.theme_config.font_size or '16px' }};
|
||||
|
||||
/* Branding */
|
||||
--logo-url: url('/api/tenant/{{ tenant.code }}/logo');
|
||||
--header-bg: {{ tenant.theme_config.header_bg or 'var(--primary-color)' }};
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Editor (eveai_app)
|
||||
Simple form interface with:
|
||||
- Color pickers for brand colors
|
||||
- Font selection dropdown
|
||||
- Logo upload functionality
|
||||
- Live preview of chat interface
|
||||
- Reset to defaults option
|
||||
|
||||
## Multiple Choice Options
|
||||
|
||||
### Dynamic Rendering Logic
|
||||
```python
|
||||
def render_options(options_list):
|
||||
if len(options_list) <= 3:
|
||||
return render_template('components/options.html',
|
||||
display_type='buttons',
|
||||
options=options_list)
|
||||
else:
|
||||
return render_template('components/options.html',
|
||||
display_type='dropdown',
|
||||
options=options_list)
|
||||
```
|
||||
|
||||
### Option Data Structure
|
||||
```json
|
||||
{
|
||||
"type": "options",
|
||||
"data": {
|
||||
"question": "How would you like to proceed?",
|
||||
"options": [
|
||||
{"id": "option1", "text": "Continue analysis", "value": "continue"},
|
||||
{"id": "option2", "text": "Generate report", "value": "report"},
|
||||
{"id": "option3", "text": "Start over", "value": "restart"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Analytics Integration
|
||||
|
||||
### Umami Setup
|
||||
- **European Hosting**: Self-hosted Umami instance
|
||||
- **Privacy Compliant**: No cookies, GDPR compliant by design
|
||||
- **Tracking Events**:
|
||||
- Chat session start (including QR code source)
|
||||
- Message sent
|
||||
- Option selected
|
||||
- Session duration
|
||||
- Specialist interaction completion
|
||||
- QR code usage
|
||||
|
||||
### Tracking Implementation
|
||||
```javascript
|
||||
// Track chat events
|
||||
function trackEvent(eventName, eventData) {
|
||||
if (window.umami) {
|
||||
umami.track(eventName, eventData);
|
||||
}
|
||||
}
|
||||
|
||||
// Track QR code usage
|
||||
function trackQRUsage(qrSessionId, tenantCode) {
|
||||
trackEvent('qr_code_used', {
|
||||
qr_session_id: qrSessionId,
|
||||
tenant_code: tenantCode
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## File Upload Support (Future)
|
||||
|
||||
### Planned Implementation
|
||||
- **Multipart Upload**: Standard HTML5 file upload
|
||||
- **File Types**: Documents, images, spreadsheets
|
||||
- **Storage**: Tenant-specific S3 buckets
|
||||
- **Processing**: Integration with existing document processing pipeline
|
||||
- **UI**: Drag-and-drop interface with progress indicators
|
||||
|
||||
### Security Considerations
|
||||
- File type validation
|
||||
- Size limits per tenant
|
||||
- Virus scanning integration
|
||||
- Temporary file cleanup
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Organization
|
||||
- **Services**: Place reusable business logic in `common/services/`
|
||||
- **Utils**: Place utility functions in `common/utils/`
|
||||
- **Multi-tenant**: Maintain data isolation using existing patterns
|
||||
- **Error Handling**: Implement proper error handling and logging
|
||||
|
||||
### Service Layer Examples
|
||||
```python
|
||||
# common/services/qr_service.py
|
||||
class QRService:
|
||||
def create_qr_session(self, qr_data):
|
||||
# Create QR session with hybrid storage approach
|
||||
pass
|
||||
|
||||
def get_and_validate_qr_session(self, qr_session_id):
|
||||
# Validate and retrieve QR session data
|
||||
pass
|
||||
|
||||
# common/services/chat_service.py
|
||||
class ChatService:
|
||||
def create_session(self, tenant_code, specialist_id, initial_args=None, source='direct'):
|
||||
# Create chat session with optional pre-filled arguments
|
||||
pass
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
- Unit tests for services and utilities in `common/`
|
||||
- Integration tests for chat flow including QR code access
|
||||
- UI tests for theme customization
|
||||
- Load testing for SSE connections
|
||||
- Cross-browser compatibility testing
|
||||
|
||||
### Performance Considerations
|
||||
- Cache tenant configurations in Redis
|
||||
- Cache QR session lookups in Redis
|
||||
- Optimize SSE connection management
|
||||
- Implement connection pooling for database
|
||||
- Use CDN for static assets
|
||||
- Monitor real-time connection limits
|
||||
|
||||
## Deployment
|
||||
|
||||
### Container Configuration
|
||||
- New `eveai_chat_client` container
|
||||
- Integration with existing docker setup
|
||||
- Environment configuration for tenant isolation
|
||||
- Load balancer configuration for SSE connections
|
||||
|
||||
### Dependencies
|
||||
- Flask and Flask-restx (existing)
|
||||
- Celery integration (existing)
|
||||
- PostgreSQL and Redis (existing)
|
||||
- Umami analytics client library
|
||||
- QR code generation library (qrcode)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Authenticated Mode
|
||||
- User login integration
|
||||
- Session persistence across devices
|
||||
- Advanced specialist access controls
|
||||
- User-specific chat history
|
||||
|
||||
### Advanced Features
|
||||
- Voice message support
|
||||
- Screen sharing capabilities
|
||||
- Collaborative chat sessions
|
||||
- Advanced analytics dashboard
|
||||
- Mobile app integration
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
CHAT_CLIENT_PORT=5000
|
||||
TENANT_API_VALIDATION_CACHE_TTL=3600
|
||||
SSE_CONNECTION_TIMEOUT=300
|
||||
QR_SESSION_DEFAULT_EXPIRY_DAYS=30
|
||||
QR_SESSION_MAX_USAGE_LIMIT=1000
|
||||
UMAMI_WEBSITE_ID=your-website-id
|
||||
UMAMI_SCRIPT_URL=https://your-umami.domain/script.js
|
||||
```
|
||||
|
||||
### Sample Theme Configuration
|
||||
```json
|
||||
{
|
||||
"primary_color": "#2563eb",
|
||||
"secondary_color": "#64748b",
|
||||
"accent_color": "#059669",
|
||||
"user_message_bg": "#2563eb",
|
||||
"bot_message_bg": "#f1f5f9",
|
||||
"chat_bg": "#ffffff",
|
||||
"font_family": "Inter, system-ui, sans-serif",
|
||||
"font_size": "16px",
|
||||
"header_bg": "#1e40af"
|
||||
}
|
||||
```
|
||||
|
||||
### Sample QR Session Data
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"tenant_code": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"specialist_id": "789e0123-e45f-67g8-h901-234567890123",
|
||||
"api_key": "tenant_api_key_here",
|
||||
"specialist_args": {
|
||||
"department": "technical_support",
|
||||
"product_category": "software",
|
||||
"priority": "high",
|
||||
"language": "en"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "Technical Support QR - Software Issues",
|
||||
"created_by": "admin_user_id",
|
||||
"usage_limit": 100,
|
||||
"expires_at": "2025-09-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This documentation provides a comprehensive foundation for developing the Evie Chat Client with secure QR code integration while maintaining consistency with the existing eveai multi-tenant architecture.
|
||||
@@ -7,7 +7,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
import logging.config
|
||||
|
||||
from common.extensions import (db, migrate, bootstrap, security, login_manager, cors, csrf, session,
|
||||
minio_client, simple_encryption, metrics, cache_manager)
|
||||
minio_client, simple_encryption, metrics, cache_manager, content_manager)
|
||||
from common.models.user import User, Role, Tenant, TenantDomain
|
||||
import common.models.interaction
|
||||
import common.models.entitlements
|
||||
@@ -15,7 +15,7 @@ import common.models.document
|
||||
from common.utils.startup_eveai import perform_startup_actions
|
||||
from config.logging_config import LOGGING
|
||||
from common.utils.security import set_tenant_session_data
|
||||
from .errors import register_error_handlers
|
||||
from common.utils.errors import register_error_handlers
|
||||
from common.utils.celery_utils import make_celery, init_celery
|
||||
from common.utils.template_filters import register_filters
|
||||
from config.config import get_config
|
||||
@@ -124,6 +124,7 @@ def register_extensions(app):
|
||||
minio_client.init_app(app)
|
||||
cache_manager.init_app(app)
|
||||
metrics.init_app(app)
|
||||
content_manager.init_app(app)
|
||||
|
||||
|
||||
def register_blueprints(app):
|
||||
|
||||
102
eveai_app/templates/basic/view_markdown.html
Normal file
102
eveai_app/templates/basic/view_markdown.html
Normal file
@@ -0,0 +1,102 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content_title %}{{ title }}{% endblock %}
|
||||
{% block content_description %}{{ description }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="card">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="showRaw">Show Raw</button>
|
||||
<button class="btn btn-sm btn-outline-primary active" id="showRendered">Show Rendered</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Raw markdown view (hidden by default) -->
|
||||
<div id="rawMarkdown" class="code-wrapper" style="display: none;">
|
||||
<pre><code class="language-markdown">{{ markdown_content }}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- Rendered markdown view -->
|
||||
<div id="renderedMarkdown" class="markdown-body">
|
||||
{{ markdown_content | markdown }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@4.0.0/github-markdown.min.css">
|
||||
<style>
|
||||
pre, code {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
white-space: pre-wrap !important;
|
||||
word-wrap: break-word !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 1rem !important;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
.code-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
padding: 1rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Dark mode styling (optional) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.markdown-body {
|
||||
color: #c9d1d9;
|
||||
background-color: #0d1117;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize syntax highlighting
|
||||
document.querySelectorAll('pre code').forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
|
||||
// Toggle buttons for display
|
||||
const showRawBtn = document.getElementById('showRaw');
|
||||
const showRenderedBtn = document.getElementById('showRendered');
|
||||
const rawMarkdown = document.getElementById('rawMarkdown');
|
||||
const renderedMarkdown = document.getElementById('renderedMarkdown');
|
||||
|
||||
showRawBtn.addEventListener('click', function() {
|
||||
rawMarkdown.style.display = 'block';
|
||||
renderedMarkdown.style.display = 'none';
|
||||
showRawBtn.classList.add('active');
|
||||
showRenderedBtn.classList.remove('active');
|
||||
});
|
||||
|
||||
showRenderedBtn.addEventListener('click', function() {
|
||||
rawMarkdown.style.display = 'none';
|
||||
renderedMarkdown.style.display = 'block';
|
||||
showRawBtn.classList.remove('active');
|
||||
showRenderedBtn.classList.add('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -5,16 +5,18 @@
|
||||
|
||||
{% block content_title %}Document Versions{% endblock %}
|
||||
{% block content_description %}View Versions for {{ document }}{% endblock %}
|
||||
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto">{% endblock %}
|
||||
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<form method="POST" action="{{ url_for('document_bp.handle_document_version_selection') }}">
|
||||
<form method="POST" action="{{ url_for('document_bp.handle_document_version_selection') }}" id="documentVersionsForm">
|
||||
{{ render_selectable_table(headers=["ID", "URL", "Object Name", "File Type", "Process.", "Proces. Start", "Proces. Finish", "Proces. Error"], rows=rows, selectable=True, id="versionsTable") }}
|
||||
<div class="form-group mt-3">
|
||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary">Edit Document Version</button>
|
||||
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger">View Processed Document</button>
|
||||
<button type="submit" name="action" value="process_document_version" class="btn btn-danger">Process Document Version</button>
|
||||
<div class="form-group mt-3 d-flex justify-content-between">
|
||||
<div>
|
||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
|
||||
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">View Processed Document</button>
|
||||
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<div class="form-group mt-3 d-flex justify-content-between">
|
||||
<div>
|
||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
|
||||
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger">View Processed Document</button>
|
||||
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">View Processed Document</button>
|
||||
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
<div class="nav-wrapper position-relative end-0">
|
||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link mb-0 px-0 py-1 active" data-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
|
||||
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
|
||||
Storage
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
||||
Embedding
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
||||
Interaction
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
<div class="nav-wrapper position-relative end-0">
|
||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link mb-0 px-0 py-1 active" data-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
|
||||
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
|
||||
Storage
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
||||
Embedding
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
||||
Interaction
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -107,17 +107,17 @@
|
||||
<!-- Nav Tabs -->
|
||||
<ul class="nav nav-tabs" id="periodTabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="status-tab" data-toggle="tab" href="#status" role="tab">
|
||||
<a class="nav-link active" id="status-tab" data-bs-toggle="tab" href="#status" role="tab">
|
||||
Status & Timeline
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="usage-tab" data-toggle="tab" href="#usage" role="tab">
|
||||
<a class="nav-link" id="usage-tab" data-bs-toggle="tab" href="#usage" role="tab">
|
||||
Usage
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="financial-tab" data-toggle="tab" href="#financial" role="tab">
|
||||
<a class="nav-link" id="financial-tab" data-bs-toggle="tab" href="#financial" role="tab">
|
||||
Financial
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field, render_included_field %}
|
||||
|
||||
@@ -19,17 +20,17 @@
|
||||
<div class="nav-wrapper position-relative end-0">
|
||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link mb-0 px-0 py-1 active" data-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
|
||||
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#storage-tab" role="tab" aria-controls="storage-tab" aria-selected="true">
|
||||
Storage
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#embedding-tab" role="tab" aria-controls="embedding-tab" aria-selected="false">
|
||||
Embedding
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#interaction-tab" role="tab" aria-controls="interaction-tab" aria-selected="false">
|
||||
Interaction
|
||||
</a>
|
||||
</li>
|
||||
@@ -68,4 +69,4 @@
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field %}
|
||||
|
||||
{% block title %}Edit Specialist Magic Link{% endblock %}
|
||||
|
||||
{% block content_title %}Edit Specialist Magic Link{% endblock %}
|
||||
{% block content_description %}Edit a Specialist Magic Link{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = ['magic_link_code'] %}
|
||||
{% set exclude_fields = [] %}
|
||||
<!-- Render Static Fields -->
|
||||
{% for field in form.get_static_fields() %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
<!-- Render Dynamic Fields -->
|
||||
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||
{% if fields|length > 0 %}
|
||||
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||
{% endif %}
|
||||
{% for field in fields %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Save Specialist Magic Link</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
23
eveai_app/templates/interaction/specialist_magic_link.html
Normal file
23
eveai_app/templates/interaction/specialist_magic_link.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field %}
|
||||
|
||||
{% block title %}Specialist Magic Link{% endblock %}
|
||||
|
||||
{% block content_title %}Register Specialist Magic Link{% endblock %}
|
||||
{% block content_description %}Define a new specialist magic link{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Register Specialist Magic Link</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
26
eveai_app/templates/interaction/specialist_magic_links.html
Normal file
26
eveai_app/templates/interaction/specialist_magic_links.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||
|
||||
{% block title %}Specialist Magic Links{% endblock %}
|
||||
|
||||
{% block content_title %}Specialist Magic Links{% endblock %}
|
||||
{% block content_description %}View Specialists Magic Links{% endblock %}
|
||||
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<form method="POST" action="{{ url_for('interaction_bp.handle_specialist_magic_link_selection') }}" id="specialistMagicLinksForm">
|
||||
{{ render_selectable_table(headers=["Specialist ML ID", "Name", "Magic Link Code"], rows=rows, selectable=True, id="specialistMagicLinksTable") }}
|
||||
<div class="form-group mt-3 d-flex justify-content-between">
|
||||
<div>
|
||||
<button type="submit" name="action" value="edit_specialist_magic_link" class="btn btn-primary" onclick="return validateTableSelection('specialistMagicLinksForm')">Edit Specialist Magic Link</button>
|
||||
</div>
|
||||
<button type="submit" name="action" value="create_specialist_magic_link" class="btn btn-success">Register Specialist Magic Link</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
{{ render_pagination(pagination, 'interaction_bp.specialist_magic_links') }}
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||
|
||||
{% block title %}Retrievers{% endblock %}
|
||||
{% block title %}Specialists{% endblock %}
|
||||
|
||||
{% block content_title %}Specialists{% endblock %}
|
||||
{% block content_description %}View Specialists for Tenant{% endblock %}
|
||||
@@ -10,7 +10,7 @@
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<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>
|
||||
<button type="submit" name="action" value="edit_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Edit Specialist</button>
|
||||
|
||||
@@ -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 %}
|
||||
<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-check form-switch">
|
||||
{{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }}
|
||||
@@ -138,7 +150,7 @@
|
||||
{% elif cell.type == 'badge' %}
|
||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||
{% elif cell.type == 'link' %}
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
{% else %}
|
||||
{{ cell.value }}
|
||||
{% endif %}
|
||||
@@ -192,7 +204,7 @@
|
||||
{% elif cell.type == 'badge' %}
|
||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||
{% elif cell.type == 'link' %}
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
{% else %}
|
||||
{{ cell.value }}
|
||||
{% endif %}
|
||||
@@ -357,7 +369,7 @@
|
||||
{% elif cell.type == 'badge' %}
|
||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||
{% elif cell.type == 'link' %}
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
{% else %}
|
||||
{{ cell.value }}
|
||||
{% endif %}
|
||||
@@ -450,3 +462,10 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro debug_to_console(var_name, var_value) %}
|
||||
<script>
|
||||
console.log('{{ var_name }}:', {{ var_value|tojson }});
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
{'name': 'Tenant Overview', 'url': '/user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Edit Tenant', 'url': '/user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Tenant Domains', 'url': '/user/view_tenant_domains', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Tenant Makes', 'url': '/user/tenant_makes', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Tenant Projects', 'url': '/user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Users', 'url': '/user/view_users', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
]) }}
|
||||
@@ -106,6 +107,7 @@
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ dropdown('Interactions', 'hub', [
|
||||
{'name': 'Specialists', 'url': '/interaction/specialists', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Specialist Magic Links', 'url': '/interaction/specialist_magic_links', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Chat Sessions', 'url': '/interaction/chat_sessions', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
]) }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros.html" import render_field %}
|
||||
{% block title %}Register Partner Service{% endblock %}
|
||||
{% from "macros.html" import render_field, debug_to_console %}
|
||||
{% block title %}Edit Partner Service{% endblock %}
|
||||
|
||||
{% block content_title %}Register Partner Service{% endblock %}
|
||||
{% block content_description %}Register Partner Service{% endblock %}
|
||||
{% block content_title %}Edit Partner Service{% endblock %}
|
||||
{% block content_description %}Edit Partner Service{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
@@ -16,6 +16,8 @@
|
||||
{% endfor %}
|
||||
<!-- Render Dynamic Fields -->
|
||||
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||
{{ debug_to_console('collection_name', collection_name) }}
|
||||
{{ debug_to_console('fields', fields) }}
|
||||
{% if fields|length > 0 %}
|
||||
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||
{% endif %}
|
||||
@@ -23,6 +25,6 @@
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Register Partner Service</button>
|
||||
<button type="submit" class="btn btn-primary">Save Partner Service</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -19,3 +19,37 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %} {% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// JavaScript om de gebruiker's timezone te detecteren
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
// Detect timezone
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Send timezone to the server via a POST request
|
||||
fetch('/set_user_timezone', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ timezone: userTimezone })
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
console.log('Timezone sent to server successfully');
|
||||
} else {
|
||||
console.error('Failed to send timezone to server');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialiseer Select2 voor timezone selectie
|
||||
$('#timezone').select2({
|
||||
placeholder: 'Selecteer een timezone...',
|
||||
allowClear: true,
|
||||
maximumSelectionLength: 10,
|
||||
theme: 'bootstrap',
|
||||
width: '100%'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
33
eveai_app/templates/user/edit_tenant_make.html
Normal file
33
eveai_app/templates/user/edit_tenant_make.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field %}
|
||||
|
||||
{% block title %}Edit Tenant Make{% endblock %}
|
||||
|
||||
{% block content_title %}Edit Tenant Make{% endblock %}
|
||||
{% block content_description %}Edit a Tenant Make.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
<!-- Render Static Fields -->
|
||||
{% for field in form.get_static_fields() %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
<!-- Render Dynamic Fields -->
|
||||
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||
{% if fields|length > 0 %}
|
||||
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||
{% endif %}
|
||||
{% for field in fields %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Save Tenant Make</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// JavaScript to detect user's timezone
|
||||
// JavaScript om de gebruiker's timezone te detecteren
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
// Detect timezone
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
@@ -45,6 +45,31 @@
|
||||
console.error('Failed to send timezone to server');
|
||||
}
|
||||
});
|
||||
|
||||
$('#timezone').select2({
|
||||
placeholder: 'Selecteer een timezone...',
|
||||
allowClear: true,
|
||||
theme: 'bootstrap',
|
||||
width: '100%',
|
||||
dropdownAutoWidth: true,
|
||||
dropdownCssClass: 'timezone-dropdown', // Een custom class voor specifieke styling
|
||||
scrollAfterSelect: false,
|
||||
// Verbeterd scroll gedrag
|
||||
dropdownParent: $('body')
|
||||
});
|
||||
|
||||
// Stel de huidige waarde in als de dropdown wordt geopend
|
||||
$('#timezone').on('select2:open', function() {
|
||||
if ($(this).val()) {
|
||||
setTimeout(function() {
|
||||
let selectedOption = $('.select2-results__option[aria-selected=true]');
|
||||
if (selectedOption.length) {
|
||||
selectedOption[0].scrollIntoView({ behavior: 'auto', block: 'center' });
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
32
eveai_app/templates/user/tenant_make.html
Normal file
32
eveai_app/templates/user/tenant_make.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field %}
|
||||
|
||||
{% block title %}Tenant Make Registration{% endblock %}
|
||||
|
||||
{% block content_title %}Register Tenant Make{% endblock %}
|
||||
{% block content_description %}Define a new tenant make{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form.get_static_fields() %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
<!-- Render Dynamic Fields -->
|
||||
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||
{% if fields|length > 0 %}
|
||||
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||
{% endif %}
|
||||
{% for field in fields %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Register Tenant Make</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
26
eveai_app/templates/user/tenant_makes.html
Normal file
26
eveai_app/templates/user/tenant_makes.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||
|
||||
{% block title %}Tenant Makes{% endblock %}
|
||||
|
||||
{% block content_title %}Tenant Makes{% endblock %}
|
||||
{% block content_description %}View Tenant Makes for Tenant{% endblock %}
|
||||
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<form method="POST" action="{{ url_for('user_bp.handle_tenant_make_selection') }}" id="tenantMakesForm">
|
||||
{{ render_selectable_table(headers=["Tenant Make ID", "Name", "Website", "Active"], rows=rows, selectable=True, id="tenantMakesTable") }}
|
||||
<div class="form-group mt-3 d-flex justify-content-between">
|
||||
<div>
|
||||
<button type="submit" name="action" value="edit_tenant_make" class="btn btn-primary" onclick="return validateTableSelection('tenantMakesForm')">Edit Tenant Make</button>
|
||||
</div>
|
||||
<button type="submit" name="action" value="create_tenant_make" class="btn btn-success">Register Tenant Make</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
{{ render_pagination(pagination, "user_bp.tenant_makes") }}
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field, render_included_field %}
|
||||
{% from "macros.html" import render_field, render_included_field, debug_to_console %}
|
||||
|
||||
{% block title %}Tenant Overview{% endblock %}
|
||||
|
||||
@@ -9,162 +9,23 @@
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<!-- Main Tenant Information -->
|
||||
{% set main_fields = ['name', 'code', 'website', 'default_language', 'allowed_languages', 'type'] %}
|
||||
{% set disabled_fields = [] %}
|
||||
{% for field in form %}
|
||||
{{ render_included_field(field, disabled_fields=main_fields, include_fields=main_fields) }}
|
||||
{{ debug_to_console('field to disable', field.name) }}
|
||||
{{ debug_to_console('field type to disable', field.type) }}
|
||||
{% if field.name != 'csrf_token' and field.type != 'HiddenField' %}
|
||||
{% set disabled_fields = disabled_fields + [field.name] %}
|
||||
{{ debug_to_console('disable', '!') }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ debug_to_console('disabled_fields', disabled_fields) }}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Nav Tabs -->
|
||||
<div class="row mt-5">
|
||||
<div class="col-lg-12">
|
||||
<div class="nav-wrapper position-relative end-0">
|
||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#license-info-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
||||
License Information
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content tab-space">
|
||||
<!-- License Information Tab -->
|
||||
<div class="tab-pane fade" id="license-info-tab" role="tabpanel">
|
||||
{% set license_fields = ['currency', 'usage_email', ] %}
|
||||
{% for field in form %}
|
||||
{{ render_included_field(field, disabled_fields=license_fields, include_fields=license_fields) }}
|
||||
{% endfor %}
|
||||
<!-- Register API Key Button -->
|
||||
<button type="button" class="btn btn-primary" onclick="generateNewChatApiKey()">Register Chat API Key</button>
|
||||
<button type="button" class="btn btn-primary" onclick="generateNewApiKey()">Register API Key</button>
|
||||
<!-- API Key Display Field -->
|
||||
<div id="chat-api-key-field" style="display:none;">
|
||||
<label for="chat-api-key">Chat API Key:</label>
|
||||
<input type="text" id="chat-api-key" class="form-control" readonly>
|
||||
<button type="button" id="copy-chat-button" class="btn btn-primary">Copy to Clipboard</button>
|
||||
<p id="copy-chat-message" style="display:none;color:green;">Chat API key copied to clipboard</p>
|
||||
</div>
|
||||
<div id="api-key-field" style="display:none;">
|
||||
<label for="api-key">API Key:</label>
|
||||
<input type="text" id="api-key" class="form-control" readonly>
|
||||
<button type="button" id="copy-api-button" class="btn btn-primary">Copy to Clipboard</button>
|
||||
<p id="copy-message" style="display:none;color:green;">API key copied to clipboard</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Function to generate a new Chat API Key
|
||||
function generateNewChatApiKey() {
|
||||
generateApiKey('/admin/user/generate_chat_api_key', '#chat-api-key', '#chat-api-key-field');
|
||||
}
|
||||
|
||||
// Function to generate a new general API Key
|
||||
function generateNewApiKey() {
|
||||
generateApiKey('/admin/user/generate_api_api_key', '#api-key', '#api-key-field');
|
||||
}
|
||||
|
||||
// Reusable function to handle API key generation
|
||||
function generateApiKey(url, inputSelector, fieldSelector) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
success: function(response) {
|
||||
$(inputSelector).val(response.api_key);
|
||||
$(fieldSelector).show();
|
||||
},
|
||||
error: function(error) {
|
||||
alert('Error generating new API key: ' + error.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to copy text to clipboard
|
||||
function copyToClipboard(selector, messageSelector) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
const text = element.value;
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
showCopyMessage(messageSelector);
|
||||
}).catch(function(error) {
|
||||
alert('Failed to copy text: ' + error);
|
||||
});
|
||||
} else {
|
||||
fallbackCopyToClipboard(text, messageSelector);
|
||||
}
|
||||
} else {
|
||||
console.error('Element not found for selector:', selector);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback method for copying text to clipboard
|
||||
function fallbackCopyToClipboard(text, messageSelector) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
showCopyMessage(messageSelector);
|
||||
} catch (err) {
|
||||
alert('Fallback: Oops, unable to copy', err);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Function to show copy confirmation message
|
||||
function showCopyMessage(messageSelector) {
|
||||
const message = document.querySelector(messageSelector);
|
||||
if (message) {
|
||||
message.style.display = 'block';
|
||||
setTimeout(function() {
|
||||
message.style.display = 'none';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for copy buttons
|
||||
document.getElementById('copy-chat-button').addEventListener('click', function() {
|
||||
copyToClipboard('#chat-api-key', '#copy-chat-message');
|
||||
});
|
||||
|
||||
document.getElementById('copy-api-button').addEventListener('click', function() {
|
||||
copyToClipboard('#api-key', '#copy-message');
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// JavaScript to detect user's timezone
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
// Detect timezone
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Send timezone to the server via a POST request
|
||||
fetch('/set_user_timezone', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ timezone: userTimezone })
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
console.log('Timezone sent to server successfully');
|
||||
} else {
|
||||
console.error('Failed to send timezone to server');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import session
|
||||
from flask import session, current_app
|
||||
from flask_security import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SelectField
|
||||
@@ -36,7 +36,7 @@ class SessionDefaultsForm(FlaskForm):
|
||||
else:
|
||||
self.partner_name.data = ""
|
||||
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')
|
||||
|
||||
# Get a new session for catalog queries
|
||||
|
||||
@@ -9,9 +9,17 @@ from common.models.user import Tenant
|
||||
from common.utils.database import Database
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
from .basic_forms import SessionDefaultsForm
|
||||
from common.extensions import content_manager
|
||||
|
||||
import markdown
|
||||
|
||||
basic_bp = Blueprint('basic_bp', __name__)
|
||||
|
||||
# Markdown filter toevoegen aan Jinja2
|
||||
@basic_bp.app_template_filter('markdown')
|
||||
def render_markdown(text):
|
||||
return markdown.markdown(text, extensions=['tables', 'fenced_code'])
|
||||
|
||||
|
||||
@basic_bp.before_request
|
||||
def log_before_request():
|
||||
@@ -104,28 +112,57 @@ def check_csrf():
|
||||
})
|
||||
|
||||
|
||||
@basic_bp.route('/release_notes', methods=['GET'])
|
||||
@basic_bp.route('/content/<content_type>', methods=['GET'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def release_notes():
|
||||
"""Display the CHANGELOG.md file."""
|
||||
def view_content(content_type):
|
||||
"""
|
||||
Show content like release notes, terms of use, etc.
|
||||
|
||||
Args:
|
||||
content_type (str): Type content (eg. 'changelog', 'terms', 'privacy')
|
||||
"""
|
||||
try:
|
||||
# Construct the URL to the CHANGELOG.md file in the static directory
|
||||
static_url = url_for('static', filename='docs/CHANGELOG.md', _external=True)
|
||||
current_app.logger.debug(f"Showing content {content_type}")
|
||||
major_minor = request.args.get('version')
|
||||
patch = request.args.get('patch')
|
||||
|
||||
# Make a request to get the content of the CHANGELOG.md file
|
||||
response = requests.get(static_url)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
# Gebruik de ContentManager om de content op te halen
|
||||
content_data = content_manager.read_content(content_type, major_minor, patch)
|
||||
|
||||
# Get the content of the response
|
||||
markdown_content = response.text
|
||||
if not content_data:
|
||||
flash(f'Content van type {content_type} werd niet gevonden.', 'danger')
|
||||
return redirect(prefixed_url_for('basic_bp.index'))
|
||||
|
||||
# Titels en beschrijvingen per contenttype
|
||||
titles = {
|
||||
'changelog': 'Release Notes',
|
||||
'terms': 'Terms & Conditions',
|
||||
'privacy': 'Privacy Statement',
|
||||
# Voeg andere types toe indien nodig
|
||||
}
|
||||
|
||||
descriptions = {
|
||||
'changelog': 'EveAI Release Notes',
|
||||
'terms': "Terms & Conditions for using AskEveAI's Evie",
|
||||
'privacy': "Privacy Statement for AskEveAI's Evie",
|
||||
# Voeg andere types toe indien nodig
|
||||
}
|
||||
|
||||
return render_template(
|
||||
'basic/view_markdown.html',
|
||||
title='Release Notes',
|
||||
description='EveAI Release Notes and Change History',
|
||||
markdown_content=markdown_content
|
||||
title=titles.get(content_type, content_type.capitalize()),
|
||||
description=descriptions.get(content_type, ''),
|
||||
markdown_content=content_data['content'],
|
||||
version=content_data['version']
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error displaying release notes: {str(e)}")
|
||||
flash(f'Error displaying release notes: {str(e)}', 'danger')
|
||||
current_app.logger.error(f"Error displaying content {content_type}: {str(e)}")
|
||||
flash(f'Error displaying content: {str(e)}', 'danger')
|
||||
return redirect(prefixed_url_for('basic_bp.index'))
|
||||
|
||||
@basic_bp.route('/release_notes', methods=['GET'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def release_notes():
|
||||
"""Doorverwijzen naar de nieuwe content view voor changelog"""
|
||||
current_app.logger.debug(f"Redirecting to content viewer")
|
||||
return redirect(prefixed_url_for('basic_bp.view_content', content_type='changelog'))
|
||||
|
||||
@@ -6,6 +6,7 @@ from wtforms.validators import DataRequired, Length, Optional, URL, ValidationEr
|
||||
from flask_wtf.file import FileField, FileRequired
|
||||
import json
|
||||
|
||||
from wtforms.widgets.core import HiddenInput
|
||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||
|
||||
from common.extensions import cache_manager
|
||||
@@ -17,8 +18,15 @@ from config.type_defs.processor_types import PROCESSOR_TYPES
|
||||
from .dynamic_form_base import DynamicFormBase
|
||||
|
||||
|
||||
def validate_catalog_name(form, field):
|
||||
# Controleer of een catalog met deze naam al bestaat
|
||||
existing_catalog = Catalog.query.filter_by(name=field.data).first()
|
||||
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.')
|
||||
|
||||
|
||||
class CatalogForm(FlaskForm):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
|
||||
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
||||
@@ -41,7 +49,8 @@ class CatalogForm(FlaskForm):
|
||||
|
||||
|
||||
class EditCatalogForm(DynamicFormBase):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||
id = IntegerField('ID', widget=HiddenInput())
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
|
||||
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
||||
@@ -181,7 +190,7 @@ class AddDocumentForm(DynamicFormBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.language.choices = [(language, language) for language in
|
||||
session.get('tenant').get('allowed_languages')]
|
||||
current_app.config['SUPPORTED_LANGUAGES']]
|
||||
if not self.language.data:
|
||||
self.language.data = session.get('tenant').get('default_language')
|
||||
|
||||
@@ -201,7 +210,7 @@ class AddURLForm(DynamicFormBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.language.choices = [(language, language) for language in
|
||||
session.get('tenant').get('allowed_languages')]
|
||||
current_app.config['SUPPORTED_LANGUAGES']]
|
||||
if not self.language.data:
|
||||
self.language.data = session.get('tenant').get('default_language')
|
||||
|
||||
|
||||
@@ -389,10 +389,7 @@ def add_document():
|
||||
|
||||
catalog = Catalog.query.get_or_404(catalog_id)
|
||||
if catalog.configuration and len(catalog.configuration) > 0:
|
||||
full_config = cache_manager.catalogs_config_cache.get_config(catalog.type)
|
||||
document_version_configurations = full_config['document_version_configurations']
|
||||
for config in document_version_configurations:
|
||||
form.add_dynamic_fields(config, full_config, catalog.configuration[config])
|
||||
form.add_dynamic_fields("tagging_fields", catalog.configuration)
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
@@ -402,11 +399,8 @@ def add_document():
|
||||
sub_file_type = form.sub_file_type.data
|
||||
filename = secure_filename(file.filename)
|
||||
extension = filename.rsplit('.', 1)[1].lower()
|
||||
catalog_properties = {}
|
||||
full_config = cache_manager.catalogs_config_cache.get_config(catalog.type)
|
||||
document_version_configurations = full_config['document_version_configurations']
|
||||
for config in document_version_configurations:
|
||||
catalog_properties[config] = form.get_dynamic_data(config)
|
||||
|
||||
catalog_properties = form.get_dynamic_data("tagging_fields")
|
||||
|
||||
api_input = {
|
||||
'catalog_id': catalog_id,
|
||||
@@ -446,10 +440,7 @@ def add_url():
|
||||
|
||||
catalog = Catalog.query.get_or_404(catalog_id)
|
||||
if catalog.configuration and len(catalog.configuration) > 0:
|
||||
full_config = cache_manager.catalogs_config_cache.get_config(catalog.type)
|
||||
document_version_configurations = full_config['document_version_configurations']
|
||||
for config in document_version_configurations:
|
||||
form.add_dynamic_fields(config, full_config, catalog.configuration[config])
|
||||
form.add_dynamic_fields("tagging_fields", catalog.configuration)
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
from datetime import date
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
|
||||
validators, ValidationError)
|
||||
from flask import current_app, request
|
||||
from flask import current_app, request, session
|
||||
import json
|
||||
|
||||
from wtforms.fields.choices import SelectField
|
||||
from wtforms.fields.datetime import DateField
|
||||
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
|
||||
|
||||
|
||||
@@ -297,6 +302,22 @@ class DynamicFormBase(FlaskForm):
|
||||
except Exception as 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):
|
||||
"""Add dynamic fields to the form based on the configuration.
|
||||
|
||||
@@ -354,11 +375,12 @@ class DynamicFormBase(FlaskForm):
|
||||
extra_classes = ['monospace-text', 'pattern-input']
|
||||
field_kwargs = {}
|
||||
elif field_type == 'ordered_list':
|
||||
current_app.logger.debug(f"Adding ordered list field for {full_field_name}")
|
||||
field_class = OrderedListField
|
||||
extra_classes = ''
|
||||
list_type = field_def.get('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:
|
||||
extra_classes = ''
|
||||
field_class = {
|
||||
@@ -370,6 +392,7 @@ class DynamicFormBase(FlaskForm):
|
||||
'text': TextAreaField,
|
||||
'date': DateField,
|
||||
'file': FileField,
|
||||
'color': ColorField,
|
||||
}.get(field_type, StringField)
|
||||
field_kwargs = {}
|
||||
|
||||
@@ -396,6 +419,12 @@ class DynamicFormBase(FlaskForm):
|
||||
except (TypeError, ValueError) as e:
|
||||
current_app.logger.error(f"Error converting initial data to a list of patterns: {e}")
|
||||
field_data = {}
|
||||
elif field_type == 'date' and isinstance(field_data, str):
|
||||
try:
|
||||
field_data = date.fromisoformat(field_data)
|
||||
except ValueError:
|
||||
current_app.logger.error(f"Error converting ISO date string '{field_data}' to date object")
|
||||
field_data = None
|
||||
elif default is not None:
|
||||
field_data = default
|
||||
|
||||
@@ -406,6 +435,14 @@ class DynamicFormBase(FlaskForm):
|
||||
render_kw['data-bs-toggle'] = 'tooltip'
|
||||
render_kw['data-bs-placement'] = 'right'
|
||||
|
||||
# Add special styling for color fields to make them more compact and visible
|
||||
if field_type == 'color':
|
||||
render_kw['style'] = 'width: 100px; height: 40px;'
|
||||
if 'class' in render_kw:
|
||||
render_kw['class'] = f"{render_kw['class']} color-field"
|
||||
else:
|
||||
render_kw['class'] = 'color-field'
|
||||
|
||||
|
||||
current_app.logger.debug(f"render_kw for {full_field_name}: {render_kw}")
|
||||
|
||||
@@ -543,6 +580,8 @@ class DynamicFormBase(FlaskForm):
|
||||
data[original_field_name] = patterns_to_json(field.data)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error converting initial data to patterns: {e}")
|
||||
elif isinstance(field, DateField):
|
||||
data[original_field_name] = field.data.isoformat()
|
||||
else:
|
||||
data[original_field_name] = field.data
|
||||
return data
|
||||
@@ -593,7 +632,7 @@ def validate_tagging_fields(form, field):
|
||||
raise ValidationError(f"Field {field_name} missing required 'type' property")
|
||||
|
||||
# Validate type
|
||||
if field_def['type'] not in ['string', 'integer', 'float', 'date', 'enum']:
|
||||
if field_def['type'] not in ['string', 'integer', 'float', 'date', 'enum', 'color']:
|
||||
raise ValidationError(f"Field {field_name} has invalid type: {field_def['type']}")
|
||||
|
||||
# Validate enum fields have allowed_values
|
||||
|
||||
@@ -7,8 +7,10 @@ from wtforms.validators import DataRequired, Length, Optional
|
||||
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
|
||||
|
||||
from common.models.document import Retriever
|
||||
from common.models.interaction import EveAITool
|
||||
from common.models.interaction import EveAITool, Specialist
|
||||
from common.models.user import TenantMake
|
||||
from common.extensions import cache_manager
|
||||
from common.utils.form_assistants import validate_json
|
||||
|
||||
from .dynamic_form_base import DynamicFormBase
|
||||
|
||||
@@ -23,6 +25,7 @@ def get_tools():
|
||||
|
||||
class SpecialistForm(FlaskForm):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
|
||||
retrievers = QuerySelectMultipleField(
|
||||
'Retrievers',
|
||||
@@ -33,7 +36,7 @@ class SpecialistForm(FlaskForm):
|
||||
)
|
||||
|
||||
type = SelectField('Specialist Type', validators=[DataRequired()])
|
||||
|
||||
active = BooleanField('Active', validators=[Optional()], default=True)
|
||||
tuning = BooleanField('Enable Specialist Tuning', default=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -46,6 +49,7 @@ class SpecialistForm(FlaskForm):
|
||||
class EditSpecialistForm(DynamicFormBase):
|
||||
name = StringField('Name', validators=[DataRequired()])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
active = BooleanField('Active', validators=[Optional()], default=True)
|
||||
|
||||
retrievers = QuerySelectMultipleField(
|
||||
'Retrievers',
|
||||
@@ -132,4 +136,52 @@ class ExecuteSpecialistForm(DynamicFormBase):
|
||||
description = TextAreaField('Specialist Description', validators=[Optional()], render_kw={'readonly': True})
|
||||
|
||||
|
||||
class SpecialistMagicLinkForm(FlaskForm):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||
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()])
|
||||
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
|
||||
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
|
||||
|
||||
# Metadata fields
|
||||
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
|
||||
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
specialists = Specialist.query.all()
|
||||
# Dynamically populate the specialist field
|
||||
self.specialist_id.choices = [(specialist.id, specialist.name) for specialist in specialists]
|
||||
|
||||
|
||||
class EditSpecialistMagicLinkForm(DynamicFormBase):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
magic_link_code = StringField('Magic Link Code', validators=[DataRequired(), Length(max=55)],
|
||||
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})
|
||||
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()])
|
||||
|
||||
# Metadata fields
|
||||
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
|
||||
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
specialist = Specialist.query.get(kwargs['specialist_id'])
|
||||
if specialist:
|
||||
self.specialist_name.data = specialist.name
|
||||
else:
|
||||
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]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ast
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime as dt, timezone as tz
|
||||
import time
|
||||
|
||||
@@ -13,9 +14,10 @@ from werkzeug.utils import secure_filename
|
||||
|
||||
from common.models.document import Embedding, DocumentVersion, Retriever
|
||||
from common.models.interaction import (ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever,
|
||||
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion)
|
||||
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion, SpecialistMagicLink)
|
||||
|
||||
from common.extensions import db, cache_manager
|
||||
from common.models.user import SpecialistMagicLinkTenant
|
||||
from common.services.interaction.specialist_services import SpecialistServices
|
||||
from common.utils.asset_utils import create_asset_stack, add_asset_version_file
|
||||
from common.utils.execution_progress import ExecutionProgressTracker
|
||||
@@ -26,7 +28,8 @@ from common.utils.nginx_utils import prefixed_url_for
|
||||
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
||||
|
||||
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
|
||||
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm)
|
||||
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm,
|
||||
SpecialistMagicLinkForm, EditSpecialistMagicLinkForm)
|
||||
|
||||
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
|
||||
|
||||
@@ -159,6 +162,7 @@ def specialist():
|
||||
new_specialist.type = form.type.data
|
||||
new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version(
|
||||
new_specialist.type)
|
||||
new_specialist.active = form.active.data
|
||||
new_specialist.tuning = form.tuning.data
|
||||
|
||||
set_logging_information(new_specialist, dt.now(tz.utc))
|
||||
@@ -228,6 +232,7 @@ def edit_specialist(specialist_id):
|
||||
specialist.name = form.name.data
|
||||
specialist.description = form.description.data
|
||||
specialist.tuning = form.tuning.data
|
||||
specialist.active = form.active.data
|
||||
# Update the configuration dynamic fields
|
||||
specialist.configuration = form.get_dynamic_data("configuration")
|
||||
|
||||
@@ -294,7 +299,7 @@ def specialists():
|
||||
|
||||
# prepare table data
|
||||
rows = prepare_table_for_macro(the_specialists,
|
||||
[('id', ''), ('name', ''), ('type', '')])
|
||||
[('id', ''), ('name', ''), ('type', ''), ('type_version', ''), ('active', ''),])
|
||||
|
||||
# Render the catalogs in a template
|
||||
return render_template('interaction/specialists.html', rows=rows, pagination=pagination)
|
||||
@@ -669,3 +674,138 @@ def session_interactions(chat_session_id):
|
||||
"""
|
||||
chat_session = ChatSession.query.get_or_404(chat_session_id)
|
||||
return session_interactions_by_session_id(chat_session.session_id)
|
||||
|
||||
|
||||
# Routes for SpecialistMagicLink Management -------------------------------------------------------
|
||||
@interaction_bp.route('/specialist_magic_link', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def specialist_magic_link():
|
||||
form = SpecialistMagicLinkForm()
|
||||
|
||||
if request.method == 'GET':
|
||||
magic_link_code = f"SPECIALIST_ML-{str(uuid.uuid4())}"
|
||||
form.magic_link_code.data = magic_link_code
|
||||
|
||||
if form.validate_on_submit():
|
||||
tenant_id = session.get('tenant').get('id')
|
||||
try:
|
||||
new_specialist_magic_link = SpecialistMagicLink()
|
||||
|
||||
# Populate fields individually instead of using populate_obj
|
||||
form.populate_obj(new_specialist_magic_link)
|
||||
|
||||
set_logging_information(new_specialist_magic_link, dt.now(tz.utc))
|
||||
|
||||
# Create 'public' SpecialistMagicLinkTenant
|
||||
new_spec_ml_tenant = SpecialistMagicLinkTenant()
|
||||
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.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_spec_ml_tenant)
|
||||
|
||||
db.session.commit()
|
||||
flash('Specialist Magic Link successfully added!', 'success')
|
||||
current_app.logger.info(f'Specialist {new_specialist_magic_link.name} successfully added for '
|
||||
f'tenant {tenant_id}!')
|
||||
|
||||
return redirect(prefixed_url_for('interaction_bp.edit_specialist_magic_link',
|
||||
specialist_magic_link_id=new_specialist_magic_link.id))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f'Failed to add specialist magic link. Error: {str(e)}', exc_info=True)
|
||||
flash(f'Failed to add specialist magic link. Error: {str(e)}', 'danger')
|
||||
|
||||
return render_template('interaction/specialist_magic_link.html', form=form)
|
||||
|
||||
|
||||
@interaction_bp.route('/specialist_magic_link/<int:specialist_magic_link_id>', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def edit_specialist_magic_link(specialist_magic_link_id):
|
||||
specialist_ml = SpecialistMagicLink.query.get_or_404(specialist_magic_link_id)
|
||||
# We need to pass along the extra kwarg specialist_id, as this id is required to initialize the form
|
||||
form = EditSpecialistMagicLinkForm(request.form, obj=specialist_ml, specialist_id=specialist_ml.specialist_id)
|
||||
|
||||
# Find the Specialist type and type_version to enable to retrieve the arguments
|
||||
specialist = Specialist.query.get_or_404(specialist_ml.specialist_id)
|
||||
specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
|
||||
|
||||
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():
|
||||
# Update the basic fields
|
||||
form.populate_obj(specialist_ml)
|
||||
# Update the arguments dynamic fields
|
||||
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(specialist_ml, dt.now(tz.utc))
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
flash('Specialist Magic Link updated successfully!', 'success')
|
||||
current_app.logger.info(f'Specialist Magic Link {specialist_ml.id} updated successfully')
|
||||
return redirect(prefixed_url_for('interaction_bp.specialist_magic_links'))
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to update specialist Magic Link. Error: {str(e)}', 'danger')
|
||||
current_app.logger.error(f'Failed to update specialist Magic Link {specialist_ml.id}. Error: {str(e)}')
|
||||
else:
|
||||
form_validation_failed(request, form)
|
||||
|
||||
return render_template('interaction/edit_specialist_magic_link.html', form=form)
|
||||
|
||||
|
||||
@interaction_bp.route('/specialist_magic_links', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def specialist_magic_links():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
query = SpecialistMagicLink.query.order_by(SpecialistMagicLink.id)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page)
|
||||
the_specialist_magic_links = pagination.items
|
||||
|
||||
# prepare table data
|
||||
rows = prepare_table_for_macro(the_specialist_magic_links, [('id', ''), ('name', ''), ('magic_link_code', ''),])
|
||||
|
||||
# Render the catalogs in a template
|
||||
return render_template('interaction/specialist_magic_links.html', rows=rows, pagination=pagination)
|
||||
|
||||
|
||||
@interaction_bp.route('/handle_specialist_magic_link_selection', methods=['POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def handle_specialist_magic_link_selection():
|
||||
action = request.form.get('action')
|
||||
if action == 'create_specialist_magic_link':
|
||||
return redirect(prefixed_url_for('interaction_bp.specialist_magic_link'))
|
||||
|
||||
specialist_ml_identification = request.form.get('selected_row')
|
||||
specialist_ml_id = ast.literal_eval(specialist_ml_identification).get('value')
|
||||
|
||||
if action == "edit_specialist_magic_link":
|
||||
return redirect(prefixed_url_for('interaction_bp.edit_specialist_magic_link',
|
||||
specialist_magic_link_id=specialist_ml_id))
|
||||
|
||||
return redirect(prefixed_url_for('interaction_bp.specialists'))
|
||||
|
||||
@@ -161,19 +161,19 @@ def edit_partner_service(partner_service_id):
|
||||
partner_service = PartnerService.query.get_or_404(partner_service_id)
|
||||
partner = session.get('partner', None)
|
||||
partner_id = session['partner']['id']
|
||||
current_app.logger.debug(f"Request Type: {request.method}")
|
||||
|
||||
form = EditPartnerServiceForm(obj=partner_service)
|
||||
if request.method == 'GET':
|
||||
partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
|
||||
partner_service.type_version)
|
||||
configuration_config = partner_service_config.get('configuration')
|
||||
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
|
||||
f"{configuration_config}")
|
||||
form.add_dynamic_fields("configuration", configuration_config, partner_service.configuration)
|
||||
permissions_config = partner_service_config.get('permissions')
|
||||
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
|
||||
f"{permissions_config}")
|
||||
form.add_dynamic_fields("permissions", permissions_config, partner_service.permissions)
|
||||
partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
|
||||
partner_service.type_version)
|
||||
configuration_config = partner_service_config.get('configuration')
|
||||
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
|
||||
f"{configuration_config}")
|
||||
form.add_dynamic_fields("configuration", partner_service_config, partner_service.configuration)
|
||||
permissions_config = partner_service_config.get('permissions')
|
||||
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
|
||||
f"{permissions_config}")
|
||||
form.add_dynamic_fields("permissions", partner_service_config, partner_service.permissions)
|
||||
|
||||
if request.method == 'POST':
|
||||
current_app.logger.debug(f"Form returned: {form.data}")
|
||||
|
||||
@@ -2,12 +2,15 @@ from flask import current_app, session
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (StringField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
|
||||
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
|
||||
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 config.type_defs.service_types import SERVICE_TYPES
|
||||
from eveai_app.views.dynamic_form_base import DynamicFormBase
|
||||
|
||||
|
||||
class TenantForm(FlaskForm):
|
||||
@@ -17,7 +20,6 @@ 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
|
||||
@@ -32,13 +34,56 @@ class TenantForm(FlaskForm):
|
||||
super(TenantForm, 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
|
||||
self.timezone.choices = [(tz, tz) for tz in pytz.all_timezones]
|
||||
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
|
||||
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
|
||||
if not current_user.has_roles('Super User') or 'partner' not in session:
|
||||
self._fields.pop('assign_to_partner', None)
|
||||
@@ -131,4 +176,35 @@ class EditTenantProjectForm(FlaskForm):
|
||||
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):
|
||||
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()])
|
||||
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)])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime as dt, timezone as tz
|
||||
from flask import request, redirect, flash, render_template, Blueprint, session, current_app
|
||||
@@ -5,12 +6,13 @@ from flask_security import roles_accepted, current_user
|
||||
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
||||
import ast
|
||||
|
||||
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant
|
||||
from common.extensions import db, security, minio_client, simple_encryption
|
||||
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake
|
||||
from common.extensions import db, security, minio_client, simple_encryption, cache_manager
|
||||
from common.utils.dynamic_field_utils import create_default_config_from_type_config
|
||||
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
|
||||
TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm, EditTenantMakeForm
|
||||
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
|
||||
@@ -110,12 +112,18 @@ 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
|
||||
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()
|
||||
flash('Tenant updated successfully.', 'success')
|
||||
if session.get('tenant'):
|
||||
@@ -459,8 +467,18 @@ 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)
|
||||
return render_template('user/tenant_overview.html', form=form)
|
||||
form = EditTenantForm(obj=tenant)
|
||||
|
||||
# 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'])
|
||||
@@ -622,6 +640,133 @@ def delete_tenant_project(tenant_project_id):
|
||||
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
||||
|
||||
|
||||
@user_bp.route('/tenant_make', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def tenant_make():
|
||||
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():
|
||||
tenant_id = session['tenant']['id']
|
||||
new_tenant_make = TenantMake()
|
||||
form.populate_obj(new_tenant_make)
|
||||
new_tenant_make.tenant_id = tenant_id
|
||||
customisation_options = form.get_dynamic_data("configuration")
|
||||
new_tenant_make.chat_customisation_options = json.dumps(customisation_options)
|
||||
set_logging_information(new_tenant_make, dt.now(tz.utc))
|
||||
|
||||
try:
|
||||
db.session.add(new_tenant_make)
|
||||
db.session.commit()
|
||||
flash('Tenant Make successfully added!', 'success')
|
||||
current_app.logger.info(f'Tenant Make {new_tenant_make.name} successfully added for tenant {tenant_id}!')
|
||||
# Enable step 2 of creation of retriever - add configuration of the retriever (dependent on type)
|
||||
return redirect(prefixed_url_for('user_bp.tenant_makes', tenant_make_id=new_tenant_make.id))
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to add Tenant Make. Error: {e}', 'danger')
|
||||
current_app.logger.error(f'Failed to add Tenant Make {new_tenant_make.name}'
|
||||
f'for tenant {tenant_id}. Error: {str(e)}')
|
||||
|
||||
return render_template('user/tenant_make.html', form=form)
|
||||
|
||||
|
||||
@user_bp.route('/tenant_makes', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def tenant_makes():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
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
|
||||
|
||||
# prepare table data
|
||||
rows = prepare_table_for_macro(tenant_makes,
|
||||
[('id', ''), ('name', ''), ('website', ''), ('active', '')])
|
||||
|
||||
# Render the tenant makes in a template
|
||||
return render_template('user/tenant_makes.html', rows=rows, pagination=pagination)
|
||||
|
||||
|
||||
@user_bp.route('/tenant_make/<int:tenant_make_id>', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def edit_tenant_make(tenant_make_id):
|
||||
"""Edit an existing tenant make configuration."""
|
||||
# Get the tenant make or return 404
|
||||
tenant_make = TenantMake.query.get_or_404(tenant_make_id)
|
||||
|
||||
# Create form instance with the tenant make
|
||||
form = EditTenantMakeForm(request.form, obj=tenant_make)
|
||||
|
||||
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
|
||||
form.add_dynamic_fields("configuration", customisation_config, tenant_make.chat_customisation_options)
|
||||
|
||||
if form.validate_on_submit():
|
||||
# Update basic fields
|
||||
form.populate_obj(tenant_make)
|
||||
tenant_make.chat_customisation_options = form.get_dynamic_data("configuration")
|
||||
|
||||
# Update logging information
|
||||
update_logging_information(tenant_make, dt.now(tz.utc))
|
||||
|
||||
# Save changes to database
|
||||
try:
|
||||
db.session.add(tenant_make)
|
||||
db.session.commit()
|
||||
flash('Tenant Make updated successfully!', 'success')
|
||||
current_app.logger.info(f'Tenant Make {tenant_make.id} updated successfully')
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to update tenant make. Error: {str(e)}', 'danger')
|
||||
current_app.logger.error(f'Failed to update tenant make {tenant_make_id}. Error: {str(e)}')
|
||||
return render_template('user/edit_tenant_make.html', form=form, tenant_make_id=tenant_make_id)
|
||||
|
||||
return redirect(prefixed_url_for('user_bp.tenant_makes'))
|
||||
else:
|
||||
form_validation_failed(request, form)
|
||||
|
||||
return render_template('user/edit_tenant_make.html', form=form, tenant_make_id=tenant_make_id)
|
||||
|
||||
|
||||
@user_bp.route('/handle_tenant_make_selection', methods=['POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def handle_tenant_make_selection():
|
||||
action = request.form['action']
|
||||
if action == 'create_tenant_make':
|
||||
return redirect(prefixed_url_for('user_bp.tenant_make'))
|
||||
tenant_make_identification = request.form.get('selected_row')
|
||||
tenant_make_id = ast.literal_eval(tenant_make_identification).get('value')
|
||||
|
||||
if action == 'edit_tenant_make':
|
||||
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):
|
||||
security.datastore.set_uniquifier(user)
|
||||
db.session.add(user)
|
||||
|
||||
114
eveai_chat_client/__init__.py
Normal file
114
eveai_chat_client/__init__.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from flask import Flask, jsonify, request
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
import logging.config
|
||||
|
||||
from common.extensions import (db, bootstrap, cors, csrf, session,
|
||||
minio_client, simple_encryption, metrics, cache_manager, content_manager)
|
||||
from common.models.user import Tenant, SpecialistMagicLinkTenant
|
||||
from common.utils.startup_eveai import perform_startup_actions
|
||||
from config.logging_config import LOGGING
|
||||
from eveai_chat_client.utils.errors import register_error_handlers
|
||||
from common.utils.celery_utils import make_celery, init_celery
|
||||
from common.utils.template_filters import register_filters
|
||||
from config.config import get_config
|
||||
|
||||
|
||||
def create_app(config_file=None):
|
||||
app = Flask(__name__, static_url_path='/static')
|
||||
|
||||
# Ensure all necessary headers are handled
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
||||
|
||||
environment = os.getenv('FLASK_ENV', 'development')
|
||||
|
||||
match environment:
|
||||
case 'development':
|
||||
app.config.from_object(get_config('dev'))
|
||||
case 'production':
|
||||
app.config.from_object(get_config('prod'))
|
||||
case _:
|
||||
app.config.from_object(get_config('dev'))
|
||||
|
||||
app.config['SESSION_KEY_PREFIX'] = 'eveai_chat_client_'
|
||||
|
||||
try:
|
||||
os.makedirs(app.instance_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
logging.config.dictConfig(LOGGING)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info("eveai_chat_client starting up")
|
||||
|
||||
# Register extensions
|
||||
register_extensions(app)
|
||||
|
||||
# Configure CSRF protection
|
||||
app.config['WTF_CSRF_CHECK_DEFAULT'] = False # Disable global CSRF protection
|
||||
app.config['WTF_CSRF_TIME_LIMIT'] = None # Remove time limit for CSRF tokens
|
||||
|
||||
app.celery = make_celery(app.name, app.config)
|
||||
init_celery(app.celery, app)
|
||||
|
||||
# Register Blueprints
|
||||
register_blueprints(app)
|
||||
|
||||
# Register Error Handlers
|
||||
register_error_handlers(app)
|
||||
|
||||
# Register Cache Handlers
|
||||
register_cache_handlers(app)
|
||||
|
||||
# Debugging settings
|
||||
if app.config['DEBUG'] is True:
|
||||
app.logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Register template filters
|
||||
register_filters(app)
|
||||
|
||||
# Perform startup actions such as cache invalidation
|
||||
perform_startup_actions(app)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def register_extensions(app):
|
||||
db.init_app(app)
|
||||
bootstrap.init_app(app)
|
||||
csrf.init_app(app)
|
||||
cors.init_app(app)
|
||||
simple_encryption.init_app(app)
|
||||
session.init_app(app)
|
||||
minio_client.init_app(app)
|
||||
cache_manager.init_app(app)
|
||||
metrics.init_app(app)
|
||||
content_manager.init_app(app)
|
||||
|
||||
|
||||
def register_blueprints(app):
|
||||
from .views.chat_views import chat_bp
|
||||
app.register_blueprint(chat_bp)
|
||||
from .views.error_views import error_bp
|
||||
app.register_blueprint(error_bp)
|
||||
from .views.healthz_views import healthz_bp
|
||||
app.register_blueprint(healthz_bp)
|
||||
|
||||
|
||||
def register_cache_handlers(app):
|
||||
from common.utils.cache.config_cache import register_config_cache_handlers
|
||||
register_config_cache_handlers(cache_manager)
|
||||
from common.utils.cache.crewai_processed_config_cache import register_specialist_cache_handlers
|
||||
register_specialist_cache_handlers(cache_manager)
|
||||
244
eveai_chat_client/static/css/chat.css
Normal file
244
eveai_chat_client/static/css/chat.css
Normal file
@@ -0,0 +1,244 @@
|
||||
/* Base styles */
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
--secondary-color: #6c757d;
|
||||
--background-color: #ffffff;
|
||||
--text-color: #212529;
|
||||
--sidebar-color: #f8f9fa;
|
||||
--message-user-bg: #e9f5ff;
|
||||
--message-bot-bg: #f8f9fa;
|
||||
--border-radius: 8px;
|
||||
--spacing: 16px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Chat layout */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background-color: var(--sidebar-color);
|
||||
border-right: 1px solid rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: var(--spacing);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
max-width: 100%;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-text {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.team-info {
|
||||
margin-top: auto;
|
||||
padding-top: var(--spacing);
|
||||
border-top: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.team-member {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.team-member img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: var(--spacing);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: var(--spacing);
|
||||
max-width: 80%;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.bot-message {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--border-radius);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.user-message .message-content {
|
||||
background-color: var(--message-user-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.bot-message .message-content {
|
||||
background-color: var(--message-bot-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: var(--spacing);
|
||||
border-top: 1px solid rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#chat-input {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(0,0,0,0.2);
|
||||
border-radius: var(--border-radius);
|
||||
resize: none;
|
||||
height: 60px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#send-button {
|
||||
padding: 0 24px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Loading indicator */
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.typing-indicator span {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
animation: typing 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.5); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Error page styles */
|
||||
.error-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.error-box {
|
||||
background-color: white;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: 1rem 0;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-block;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 30%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
156
eveai_chat_client/templates/base.html
Normal file
156
eveai_chat_client/templates/base.html
Normal file
@@ -0,0 +1,156 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}EveAI Chat{% endblock %}</title>
|
||||
|
||||
<!-- 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 -->
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: {{ customisation.primary_color|default('#007bff') }};
|
||||
--secondary-color: {{ customisation.secondary_color|default('#6c757d') }};
|
||||
--background-color: {{ customisation.background_color|default('#ffffff') }};
|
||||
--text-color: {{ customisation.text_color|default('#212529') }};
|
||||
--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>
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="app-container">
|
||||
<!-- 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>
|
||||
|
||||
<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 %}
|
||||
</body>
|
||||
</html>
|
||||
214
eveai_chat_client/templates/chat.html
Normal file
214
eveai_chat_client/templates/chat.html
Normal file
@@ -0,0 +1,214 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Chat{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="chat-container">
|
||||
<!-- Left sidebar with customizable content -->
|
||||
<div class="sidebar">
|
||||
{% if customisation.logo_url %}
|
||||
<div class="logo">
|
||||
<img src="{{ customisation.logo_url }}" alt="{{ tenant.name }} Logo">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="sidebar-content">
|
||||
{% if customisation.sidebar_text %}
|
||||
<div class="sidebar-text">
|
||||
{{ customisation.sidebar_text|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if customisation.team_info %}
|
||||
<div class="team-info">
|
||||
<h3>Team</h3>
|
||||
<div class="team-members">
|
||||
{% for member in customisation.team_info %}
|
||||
<div class="team-member">
|
||||
{% if member.avatar %}
|
||||
<img src="{{ member.avatar }}" alt="{{ member.name }}">
|
||||
{% endif %}
|
||||
<div class="member-info">
|
||||
<h4>{{ member.name }}</h4>
|
||||
<p>{{ member.role }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main chat area -->
|
||||
<div class="chat-main">
|
||||
<div class="chat-header">
|
||||
<h1>{{ specialist.name }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
<!-- Messages will be added here dynamically -->
|
||||
{% if customisation.welcome_message %}
|
||||
<div class="message bot-message">
|
||||
<div class="message-content">{{ customisation.welcome_message|safe }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="message bot-message">
|
||||
<div class="message-content">Hello! How can I help you today?</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="chat-input-container">
|
||||
<textarea id="chat-input" placeholder="Type your message here..."></textarea>
|
||||
<button id="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Store session information
|
||||
const sessionInfo = {
|
||||
tenantId: {{ tenant.id }},
|
||||
specialistId: {{ specialist.id }},
|
||||
chatSessionId: "{{ session.chat_session_id }}"
|
||||
};
|
||||
|
||||
// Chat functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const sendButton = document.getElementById('send-button');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
let currentTaskId = null;
|
||||
let pollingInterval = null;
|
||||
|
||||
// Function to add a message to the chat
|
||||
function addMessage(message, isUser = false) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = isUser ? 'message user-message' : 'message bot-message';
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'message-content';
|
||||
contentDiv.innerHTML = message;
|
||||
|
||||
messageDiv.appendChild(contentDiv);
|
||||
chatMessages.appendChild(messageDiv);
|
||||
|
||||
// Scroll to bottom
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
// Function to send a message
|
||||
function sendMessage() {
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// Add user message to chat
|
||||
addMessage(message, true);
|
||||
|
||||
// Clear input
|
||||
chatInput.value = '';
|
||||
|
||||
// Add loading indicator
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.className = 'message bot-message loading';
|
||||
loadingDiv.innerHTML = '<div class="message-content"><div class="typing-indicator"><span></span><span></span><span></span></div></div>';
|
||||
chatMessages.appendChild(loadingDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
|
||||
// Send message to server
|
||||
fetch('/api/send_message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'processing') {
|
||||
currentTaskId = data.task_id;
|
||||
|
||||
// Start polling for results
|
||||
if (pollingInterval) clearInterval(pollingInterval);
|
||||
pollingInterval = setInterval(checkTaskStatus, 1000);
|
||||
} else {
|
||||
// Remove loading indicator
|
||||
chatMessages.removeChild(loadingDiv);
|
||||
|
||||
// Show error if any
|
||||
if (data.error) {
|
||||
addMessage(`Error: ${data.error}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Remove loading indicator
|
||||
chatMessages.removeChild(loadingDiv);
|
||||
addMessage(`Error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to check task status
|
||||
function checkTaskStatus() {
|
||||
if (!currentTaskId) return;
|
||||
|
||||
fetch(`/api/check_status?task_id=${currentTaskId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Remove loading indicator
|
||||
const loadingDiv = document.querySelector('.loading');
|
||||
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||
|
||||
// Add bot response
|
||||
addMessage(data.answer);
|
||||
|
||||
// Clear polling
|
||||
clearInterval(pollingInterval);
|
||||
currentTaskId = null;
|
||||
} else if (data.status === 'error') {
|
||||
// Remove loading indicator
|
||||
const loadingDiv = document.querySelector('.loading');
|
||||
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||
|
||||
// Show error
|
||||
addMessage(`Error: ${data.message}`);
|
||||
|
||||
// Clear polling
|
||||
clearInterval(pollingInterval);
|
||||
currentTaskId = null;
|
||||
}
|
||||
// If status is 'pending', continue polling
|
||||
})
|
||||
.catch(error => {
|
||||
// Remove loading indicator
|
||||
const loadingDiv = document.querySelector('.loading');
|
||||
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||
|
||||
addMessage(`Error checking status: ${error.message}`);
|
||||
|
||||
// Clear polling
|
||||
clearInterval(pollingInterval);
|
||||
currentTaskId = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
sendButton.addEventListener('click', sendMessage);
|
||||
|
||||
chatInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
15
eveai_chat_client/templates/error.html
Normal file
15
eveai_chat_client/templates/error.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Error{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="error-container">
|
||||
<div class="error-box">
|
||||
<h1>Oops! Something went wrong</h1>
|
||||
<p class="error-message">{{ message }}</p>
|
||||
<div class="error-actions">
|
||||
<a href="/" class="btn-primary">Go to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
1
eveai_chat_client/utils/__init__.py
Normal file
1
eveai_chat_client/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Utils package for eveai_chat_client
|
||||
85
eveai_chat_client/utils/errors.py
Normal file
85
eveai_chat_client/utils/errors.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import traceback
|
||||
|
||||
import jinja2
|
||||
from flask import render_template, request, jsonify, redirect, current_app, flash
|
||||
|
||||
from common.utils.eveai_exceptions import EveAINoSessionTenant
|
||||
|
||||
|
||||
def not_found_error(error):
|
||||
current_app.logger.error(f"Not Found Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="Page not found."), 404
|
||||
|
||||
|
||||
def internal_server_error(error):
|
||||
current_app.logger.error(f"Internal Server Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="Internal server error."), 500
|
||||
|
||||
|
||||
def not_authorised_error(error):
|
||||
current_app.logger.error(f"Not Authorised Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="Not authorized."), 401
|
||||
|
||||
|
||||
def access_forbidden(error):
|
||||
current_app.logger.error(f"Access Forbidden: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="Access forbidden."), 403
|
||||
|
||||
|
||||
def key_error_handler(error):
|
||||
current_app.logger.error(f"Key Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="An unexpected error occurred."), 500
|
||||
|
||||
|
||||
def attribute_error_handler(error):
|
||||
"""Handle AttributeError exceptions."""
|
||||
error_msg = str(error)
|
||||
current_app.logger.error(f"AttributeError: {error_msg}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="An application error occurred."), 500
|
||||
|
||||
|
||||
def no_tenant_selected_error(error):
|
||||
"""Handle errors when no tenant is selected in the current session."""
|
||||
current_app.logger.error(f"No Session Tenant Error: {error}")
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="Session expired. Please use a valid magic link."), 401
|
||||
|
||||
|
||||
def general_exception(e):
|
||||
current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True)
|
||||
return render_template('error.html', message="An application error occurred."), 500
|
||||
|
||||
|
||||
def template_not_found_error(error):
|
||||
"""Handle Jinja2 TemplateNotFound exceptions."""
|
||||
current_app.logger.error(f'Template not found: {error.name}')
|
||||
current_app.logger.error(f'Search Paths: {current_app.jinja_loader.list_templates()}')
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="Template not found."), 404
|
||||
|
||||
|
||||
def template_syntax_error(error):
|
||||
"""Handle Jinja2 TemplateSyntaxError exceptions."""
|
||||
current_app.logger.error(f'Template syntax error: {error.message}')
|
||||
current_app.logger.error(f'In template {error.filename}, line {error.lineno}')
|
||||
current_app.logger.error(traceback.format_exc())
|
||||
return render_template('error.html', message="Template syntax error."), 500
|
||||
|
||||
|
||||
def register_error_handlers(app):
|
||||
app.register_error_handler(404, not_found_error)
|
||||
app.register_error_handler(500, internal_server_error)
|
||||
app.register_error_handler(401, not_authorised_error)
|
||||
app.register_error_handler(403, not_authorised_error)
|
||||
app.register_error_handler(EveAINoSessionTenant, no_tenant_selected_error)
|
||||
app.register_error_handler(KeyError, key_error_handler)
|
||||
app.register_error_handler(AttributeError, attribute_error_handler)
|
||||
app.register_error_handler(jinja2.TemplateNotFound, template_not_found_error)
|
||||
app.register_error_handler(jinja2.TemplateSyntaxError, template_syntax_error)
|
||||
app.register_error_handler(Exception, general_exception)
|
||||
1
eveai_chat_client/views/__init__.py
Normal file
1
eveai_chat_client/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Views package for eveai_chat_client
|
||||
194
eveai_chat_client/views/chat_views.py
Normal file
194
eveai_chat_client/views/chat_views.py
Normal file
@@ -0,0 +1,194 @@
|
||||
import uuid
|
||||
from flask import Blueprint, render_template, request, session, current_app, jsonify, abort
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from common.extensions import db
|
||||
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_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():
|
||||
customisation = get_default_chat_customisation()
|
||||
return render_template('error.html', message="Please use a valid magic link to access the chat.",
|
||||
customisation=customisation)
|
||||
|
||||
|
||||
@chat_bp.route('/<magic_link_code>')
|
||||
def chat(magic_link_code):
|
||||
"""
|
||||
Main chat interface accessed via magic link
|
||||
"""
|
||||
try:
|
||||
# Find the tenant using the magic link code
|
||||
magic_link_tenant = SpecialistMagicLinkTenant.query.filter_by(magic_link_code=magic_link_code).first()
|
||||
|
||||
if not magic_link_tenant:
|
||||
current_app.logger.error(f"Invalid magic link code: {magic_link_code}")
|
||||
return render_template('error.html', message="Invalid magic link code.")
|
||||
|
||||
# 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()
|
||||
|
||||
# Get specialist magic link details from tenant schema
|
||||
specialist_ml = SpecialistMagicLink.query.filter_by(magic_link_code=magic_link_code).first()
|
||||
if not specialist_ml:
|
||||
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:
|
||||
current_app.logger.error(f"Specialist not found: {specialist_ml.specialist_id}")
|
||||
return render_template('error.html', message="Specialist not found.")
|
||||
|
||||
# Store necessary information in session
|
||||
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_make.chat_customisation_options)
|
||||
|
||||
# Start a new chat session
|
||||
session['chat_session_id'] = SpecialistServices.start_session()
|
||||
|
||||
return render_template('chat.html',
|
||||
tenant=tenant,
|
||||
tenant_make=tenant_make,
|
||||
specialist=specialist,
|
||||
customisation=customisation)
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)
|
||||
return render_template('error.html', message="An error occurred while setting up the chat.")
|
||||
|
||||
@chat_bp.route('/api/send_message', methods=['POST'])
|
||||
def send_message():
|
||||
"""
|
||||
API endpoint to send a message to the specialist
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
message = data.get('message')
|
||||
|
||||
if not message:
|
||||
return jsonify({'error': 'No message provided'}), 400
|
||||
|
||||
tenant_id = session.get('tenant_id')
|
||||
specialist_id = session.get('specialist_id')
|
||||
chat_session_id = session.get('chat_session_id')
|
||||
specialist_args = session.get('specialist_args', {})
|
||||
|
||||
if not all([tenant_id, specialist_id, chat_session_id]):
|
||||
return jsonify({'error': 'Session expired or invalid'}), 400
|
||||
|
||||
# Switch to tenant schema
|
||||
Database(tenant_id).switch_schema()
|
||||
|
||||
# Add user message to specialist arguments
|
||||
specialist_args['user_message'] = message
|
||||
|
||||
# Execute specialist
|
||||
result = SpecialistServices.execute_specialist(
|
||||
tenant_id=tenant_id,
|
||||
specialist_id=specialist_id,
|
||||
specialist_arguments=specialist_args,
|
||||
session_id=chat_session_id,
|
||||
user_timezone=data.get('timezone', 'UTC')
|
||||
)
|
||||
|
||||
# Store the task ID for polling
|
||||
session['current_task_id'] = result['task_id']
|
||||
|
||||
return jsonify({
|
||||
'status': 'processing',
|
||||
'task_id': result['task_id']
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error sending message: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@chat_bp.route('/api/check_status', methods=['GET'])
|
||||
def check_status():
|
||||
"""
|
||||
API endpoint to check the status of a task
|
||||
"""
|
||||
try:
|
||||
task_id = request.args.get('task_id') or session.get('current_task_id')
|
||||
|
||||
if not task_id:
|
||||
return jsonify({'error': 'No task ID provided'}), 400
|
||||
|
||||
tenant_id = session.get('tenant_id')
|
||||
if not tenant_id:
|
||||
return jsonify({'error': 'Session expired or invalid'}), 400
|
||||
|
||||
# Switch to tenant schema
|
||||
Database(tenant_id).switch_schema()
|
||||
|
||||
# Check task status using Celery
|
||||
task_result = current_app.celery.AsyncResult(task_id)
|
||||
|
||||
if task_result.state == 'PENDING':
|
||||
return jsonify({'status': 'pending'})
|
||||
elif task_result.state == 'SUCCESS':
|
||||
result = task_result.result
|
||||
|
||||
# Format the response
|
||||
specialist_result = result.get('result', {})
|
||||
response = {
|
||||
'status': 'success',
|
||||
'answer': specialist_result.get('answer', ''),
|
||||
'citations': specialist_result.get('citations', []),
|
||||
'insufficient_info': specialist_result.get('insufficient_info', False),
|
||||
'interaction_id': result.get('interaction_id')
|
||||
}
|
||||
|
||||
return jsonify(response)
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(task_result.info)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error checking status: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
24
eveai_chat_client/views/error_views.py
Normal file
24
eveai_chat_client/views/error_views.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
error_bp = Blueprint('error', __name__)
|
||||
|
||||
@error_bp.route('/error')
|
||||
def error_page():
|
||||
"""
|
||||
Generic error page
|
||||
"""
|
||||
return render_template('error.html', message="An error occurred.")
|
||||
|
||||
@error_bp.app_errorhandler(404)
|
||||
def page_not_found(e):
|
||||
"""
|
||||
Handle 404 errors
|
||||
"""
|
||||
return render_template('error.html', message="Page not found."), 404
|
||||
|
||||
@error_bp.app_errorhandler(500)
|
||||
def internal_server_error(e):
|
||||
"""
|
||||
Handle 500 errors
|
||||
"""
|
||||
return render_template('error.html', message="Internal server error."), 500
|
||||
17
eveai_chat_client/views/healthz_views.py
Normal file
17
eveai_chat_client/views/healthz_views.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from flask import Blueprint, jsonify
|
||||
|
||||
healthz_bp = Blueprint('healthz', __name__)
|
||||
|
||||
@healthz_bp.route('/healthz/ready')
|
||||
def ready():
|
||||
"""
|
||||
Health check endpoint for readiness probe
|
||||
"""
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
@healthz_bp.route('/healthz/live')
|
||||
def live():
|
||||
"""
|
||||
Health check endpoint for liveness probe
|
||||
"""
|
||||
return jsonify({"status": "ok"})
|
||||
@@ -40,7 +40,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
||||
|
||||
@property
|
||||
def type_version(self) -> str:
|
||||
return "1.1"
|
||||
return "1.2"
|
||||
|
||||
def _config_task_agents(self):
|
||||
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -121,7 +121,7 @@
|
||||
{% elif cell.type == 'badge' %}
|
||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||
{% elif cell.type == 'link' %}
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
{% else %}
|
||||
{{ cell.value }}
|
||||
{% endif %}
|
||||
@@ -177,7 +177,7 @@
|
||||
{% elif cell.type == 'badge' %}
|
||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||
{% elif cell.type == 'link' %}
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
{% else %}
|
||||
{{ cell.value }}
|
||||
{% endif %}
|
||||
@@ -342,7 +342,7 @@
|
||||
{% elif cell.type == 'badge' %}
|
||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||
{% elif cell.type == 'link' %}
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||
{% else %}
|
||||
{{ cell.value }}
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Add TenantMake model
|
||||
|
||||
Revision ID: 200bda7f5251
|
||||
Revises: b6146237f298
|
||||
Create Date: 2025-06-06 13:48:40.208711
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '200bda7f5251'
|
||||
down_revision = 'b6146237f298'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('tenant_make',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('active', sa.Boolean(), nullable=False),
|
||||
sa.Column('website', sa.String(length=255), nullable=True),
|
||||
sa.Column('logo_url', sa.String(length=255), nullable=True),
|
||||
sa.Column('chat_customisation_options', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_by', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['public.user.id'], ),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['public.tenant.id'], ),
|
||||
sa.ForeignKeyConstraint(['updated_by'], ['public.user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='public'
|
||||
)
|
||||
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||
batch_op.drop_column('chat_customisation_options')
|
||||
|
||||
# ### 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.add_column(sa.Column('chat_customisation_options', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True))
|
||||
|
||||
op.drop_table('tenant_make', schema='public')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Add SpecialistMagicLinkTenant model
|
||||
|
||||
Revision ID: 2b4cb553530e
|
||||
Revises: 7d3c6f48735c
|
||||
Create Date: 2025-06-03 20:26:36.423880
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2b4cb553530e'
|
||||
down_revision = '7d3c6f48735c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('specialist_magic_link_tenant',
|
||||
sa.Column('magic_link_code', sa.String(length=55), nullable=False),
|
||||
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['public.tenant.id'], ),
|
||||
sa.PrimaryKeyConstraint('magic_link_code'),
|
||||
schema='public'
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('specialist_magic_link_tenant', schema='public')
|
||||
# ### end Alembic commands ###
|
||||
@@ -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 ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Add Chat Configuration Options to Tenant model
|
||||
|
||||
Revision ID: b6146237f298
|
||||
Revises: 2b4cb553530e
|
||||
Create Date: 2025-06-06 03:45:24.264045
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b6146237f298'
|
||||
down_revision = '2b4cb553530e'
|
||||
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('chat_customisation_options', postgresql.JSONB(astext_type=sa.Text()), nullable=True))
|
||||
|
||||
# ### 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_column('chat_customisation_options')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -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 ###
|
||||
@@ -71,8 +71,8 @@ target_db = current_app.extensions['migrate'].db
|
||||
def get_public_table_names():
|
||||
# TODO: This function should include the necessary functionality to automatically retrieve table names
|
||||
return ['role', 'roles_users', 'tenant', 'user', 'tenant_domain','license_tier', 'license', 'license_usage',
|
||||
'business_event_log', 'tenant_project']
|
||||
|
||||
'business_event_log', 'tenant_project', 'partner', 'partner_service', 'invoice', 'license_period',
|
||||
'license_change_log', 'partner_service_license_tier', 'payment', 'partner_tenant']
|
||||
|
||||
PUBLIC_TABLES = get_public_table_names()
|
||||
logger.info(f"Public tables: {PUBLIC_TABLES}")
|
||||
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Make Catalog Name Unique
|
||||
|
||||
Revision ID: c71facc0ce7e
|
||||
Revises: d69520ec540d
|
||||
Create Date: 2025-06-07 08:38:23.759681
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import pgvector
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c71facc0ce7e'
|
||||
down_revision = 'd69520ec540d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_unique_constraint(None, 'catalog', ['name'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Add SpecialistMagicLink model
|
||||
|
||||
Revision ID: d69520ec540d
|
||||
Revises: 55c696c4a687
|
||||
Create Date: 2025-06-03 20:25:51.129869
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import pgvector
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd69520ec540d'
|
||||
down_revision = '55c696c4a687'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('specialist_magic_link',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('specialist_id', sa.Integer(), nullable=False),
|
||||
sa.Column('magic_link_code', sa.String(length=55), nullable=False),
|
||||
sa.Column('valid_from', sa.DateTime(), nullable=True),
|
||||
sa.Column('valid_to', sa.DateTime(), nullable=True),
|
||||
sa.Column('specialist_args', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_by', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['public.user.id'], ),
|
||||
sa.ForeignKeyConstraint(['specialist_id'], ['specialist.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['updated_by'], ['public.user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('magic_link_code')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('specialist_magic_link')
|
||||
# ### end Alembic commands ###
|
||||
@@ -18,6 +18,11 @@ http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Define upstream servers
|
||||
upstream eveai_chat_client {
|
||||
server eveai_chat_client:5004;
|
||||
}
|
||||
|
||||
log_format custom_log_format '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
@@ -93,6 +98,26 @@ http {
|
||||
# add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
# }
|
||||
|
||||
location /chat-client/ {
|
||||
proxy_pass http://eveai_chat_client/;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Prefix /chat-client;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
|
||||
# Add CORS headers
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
}
|
||||
|
||||
location /admin/ {
|
||||
# include uwsgi_params;
|
||||
# uwsgi_pass 127.0.0.1:5001;
|
||||
|
||||
@@ -1192,5 +1192,27 @@ select.select2[multiple] {
|
||||
border: 1px solid var(--bs-primary) !important; /* Duidelijke rand toevoegen */
|
||||
}
|
||||
|
||||
/* Select2 settings ---------------------------------------------------------------------------- */
|
||||
|
||||
.select2-container--default .select2-results > .select2-results__options {
|
||||
max-height: 200px !important; /* Pas deze waarde aan naar wens */
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* Zorg voor een consistente breedte */
|
||||
.select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Voorkom dat de dropdown de pagina uitbreidt */
|
||||
.select2-dropdown {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.timezone-dropdown {
|
||||
max-height: 300px;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
0
nginx/static/css/eveai-chat-style.css
Normal file
0
nginx/static/css/eveai-chat-style.css
Normal file
@@ -82,8 +82,6 @@ def initialize_default_tenant():
|
||||
'website': 'https://www.askeveai.com',
|
||||
'timezone': 'UTC',
|
||||
'default_language': 'en',
|
||||
'allowed_languages': ['en', 'fr', 'nl', 'de', 'es'],
|
||||
'llm_model': 'mistral.mistral-large-latest',
|
||||
'type': 'Active',
|
||||
'currency': '€',
|
||||
'created_at': dt.now(tz.utc),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from gevent import monkey
|
||||
monkey.patch_all()
|
||||
|
||||
from eveai_chat import create_app
|
||||
from eveai_chat_client import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "/app/" || exit 1
|
||||
export PROJECT_DIR="/app"
|
||||
export PYTHONPATH="$PROJECT_DIR/patched_packages:$PYTHONPATH:$PROJECT_DIR" # Include the app directory in the Python path & patched packages
|
||||
|
||||
# Ensure we can write the logs
|
||||
chown -R appuser:appuser /app/logs
|
||||
|
||||
# Set flask environment variables
|
||||
#export FLASK_ENV=development # Use 'production' as appropriate
|
||||
#export FLASK_DEBUG=1 # Use 0 for production
|
||||
echo "Starting EveAI Chat"
|
||||
|
||||
# Start Flask app
|
||||
gunicorn -w 1 -k gevent -b 0.0.0.0:5002 --worker-connections 100 scripts.run_eveai_chat:app
|
||||
23
scripts/start_eveai_chat_client.sh
Executable file
23
scripts/start_eveai_chat_client.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "/app" || exit 1
|
||||
export PYTHONPATH="$PYTHONPATH:/app/"
|
||||
|
||||
# Ensure we can write the logs
|
||||
chown -R appuser:appuser /app/logs
|
||||
|
||||
# Wait for the database to be ready
|
||||
echo "Waiting for database to be ready"
|
||||
until pg_isready -h $DB_HOST -p $DB_PORT; do
|
||||
echo "Postgres is unavailable - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
echo "Postgres is up - executing commands"
|
||||
|
||||
# Set FLASK_APP environment variables
|
||||
PROJECT_DIR="/app"
|
||||
export FLASK_APP=${PROJECT_DIR}/scripts/run_eveai_chat_client.py
|
||||
export PYTHONPATH="$PROJECT_DIR/patched_packages:$PYTHONPATH:$PROJECT_DIR"
|
||||
|
||||
# Start Flask app with Gunicorn
|
||||
gunicorn -w 1 -k gevent -b 0.0.0.0:5004 --worker-connections 100 scripts.run_eveai_chat_client:app
|
||||
Reference in New Issue
Block a user