Compare commits
33 Commits
v2.3.0-alf
...
v2.3.7-alf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
043cea45f2 | ||
|
|
7b87880045 | ||
|
|
5b2c04501c | ||
|
|
babcd6ec04 | ||
|
|
71adf64668 | ||
|
|
dbea41451a | ||
|
|
82e25b356c | ||
|
|
3c7460f741 | ||
|
|
2835486599 | ||
|
|
f1c60f9574 | ||
|
|
b326c0c6f2 | ||
|
|
5f1a5711f6 | ||
|
|
67ceb57b79 | ||
|
|
23b49516cb | ||
|
|
9cc266b97f | ||
|
|
3f77871c4f | ||
|
|
199cf94cf2 | ||
|
|
c4dcd6a0d3 | ||
|
|
43ee9139d6 | ||
|
|
8f45005713 | ||
|
|
bc1626c4ff | ||
|
|
57c0e7a1ba | ||
|
|
0d05499d2b | ||
|
|
b4e58659a8 | ||
|
|
67078ce925 | ||
|
|
ebdb836448 | ||
|
|
81e754317a | ||
|
|
578981c745 | ||
|
|
8fb2ad43c5 | ||
|
|
49f9077a7b | ||
|
|
d290b46a0c | ||
|
|
73647e4795 | ||
|
|
25e169dbea |
19
.aiignore
Normal file
19
.aiignore
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# An .aiignore file follows the same syntax as a .gitignore file.
|
||||||
|
# .gitignore documentation: https://git-scm.com/docs/gitignore
|
||||||
|
|
||||||
|
# you can ignore files
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# or folders
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
nginx/node_modules/
|
||||||
|
nginx/static/
|
||||||
|
db_backups/
|
||||||
|
docker/eveai_logs/
|
||||||
|
docker/logs/
|
||||||
|
docker/minio/
|
||||||
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -53,3 +53,7 @@ scripts/__pycache__/run_eveai_app.cpython-312.pyc
|
|||||||
/docker/grafana/data/
|
/docker/grafana/data/
|
||||||
/temp_requirements/
|
/temp_requirements/
|
||||||
/nginx/node_modules/
|
/nginx/node_modules/
|
||||||
|
/nginx/static/assets/css/chat.css
|
||||||
|
/nginx/static/assets/css/chat-components.css
|
||||||
|
/nginx/static/assets/js/components/
|
||||||
|
/nginx/static/assets/js/chat-app.js
|
||||||
|
|||||||
67
README.md.k8s-logging
Normal file
67
README.md.k8s-logging
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Kubernetes Logging Upgrade
|
||||||
|
|
||||||
|
## Overzicht
|
||||||
|
Deze instructies beschrijven hoe je alle services moet bijwerken om de nieuwe logging configuratie te gebruiken die zowel compatibel is met traditionele bestandsgebaseerde logging (voor ontwikkeling/test) als met Kubernetes (voor productie).
|
||||||
|
|
||||||
|
## Stappen voor elke service
|
||||||
|
|
||||||
|
Pas de volgende wijzigingen toe in elk van de volgende services:
|
||||||
|
|
||||||
|
- eveai_app
|
||||||
|
- eveai_workers
|
||||||
|
- eveai_api
|
||||||
|
- eveai_chat_client
|
||||||
|
- eveai_chat_workers
|
||||||
|
- eveai_beat
|
||||||
|
- eveai_entitlements
|
||||||
|
|
||||||
|
### 1. Update de imports
|
||||||
|
|
||||||
|
Verander:
|
||||||
|
```python
|
||||||
|
from config.logging_config import LOGGING
|
||||||
|
```
|
||||||
|
|
||||||
|
Naar:
|
||||||
|
```python
|
||||||
|
from config.logging_config import configure_logging
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Update de logging configuratie
|
||||||
|
|
||||||
|
Verander:
|
||||||
|
```python
|
||||||
|
logging.config.dictConfig(LOGGING)
|
||||||
|
```
|
||||||
|
|
||||||
|
Naar:
|
||||||
|
```python
|
||||||
|
configure_logging()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dockerfile Aanpassingen
|
||||||
|
|
||||||
|
Voeg de volgende regels toe aan je Dockerfile voor elke service om de Kubernetes-specifieke logging afhankelijkheden te installeren (alleen voor productie):
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Alleen voor productie (Kubernetes) builds
|
||||||
|
COPY requirements-k8s.txt /app/
|
||||||
|
RUN if [ "$ENVIRONMENT" = "production" ]; then pip install -r requirements-k8s.txt; fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kubernetes Deployment
|
||||||
|
|
||||||
|
Zorg ervoor dat je Kubernetes deployment manifests de volgende omgevingsvariabele bevatten:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
- name: FLASK_ENV
|
||||||
|
value: "production"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Voordelen
|
||||||
|
|
||||||
|
1. De code detecteert automatisch of deze in Kubernetes draait
|
||||||
|
2. In ontwikkeling/test omgevingen blijft alles naar bestanden schrijven
|
||||||
|
3. In Kubernetes gaan logs naar stdout/stderr in JSON-formaat
|
||||||
|
4. Geen wijzigingen nodig in bestaande logger code in de applicatie
|
||||||
6
app/eveai_chat_client/templates/chat.html
Normal file
6
app/eveai_chat_client/templates/chat.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- chat.html -->
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ tenant_make.name|default('EveAI') }} - AI Chat{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
@@ -11,6 +11,7 @@ from flask_restx import Api
|
|||||||
from prometheus_flask_exporter import PrometheusMetrics
|
from prometheus_flask_exporter import PrometheusMetrics
|
||||||
|
|
||||||
from .utils.cache.eveai_cache_manager import EveAICacheManager
|
from .utils.cache.eveai_cache_manager import EveAICacheManager
|
||||||
|
from .utils.content_utils import ContentManager
|
||||||
from .utils.simple_encryption import SimpleEncryption
|
from .utils.simple_encryption import SimpleEncryption
|
||||||
from .utils.minio_utils import MinioClient
|
from .utils.minio_utils import MinioClient
|
||||||
|
|
||||||
@@ -30,4 +31,5 @@ simple_encryption = SimpleEncryption()
|
|||||||
minio_client = MinioClient()
|
minio_client = MinioClient()
|
||||||
metrics = PrometheusMetrics.for_app_factory()
|
metrics = PrometheusMetrics.for_app_factory()
|
||||||
cache_manager = EveAICacheManager()
|
cache_manager = EveAICacheManager()
|
||||||
|
content_manager = ContentManager()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
class Catalog(db.Model):
|
class Catalog(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
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)
|
description = db.Column(db.Text, nullable=True)
|
||||||
type = db.Column(db.String(50), nullable=False, default="STANDARD_CATALOG")
|
type = db.Column(db.String(50), nullable=False, default="STANDARD_CATALOG")
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
from ..extensions import db
|
from ..extensions import db
|
||||||
from .user import User, Tenant
|
from .user import User, Tenant, TenantMake
|
||||||
from .document import Embedding, Retriever
|
from .document import Embedding, Retriever
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ class Specialist(db.Model):
|
|||||||
tuning = db.Column(db.Boolean, nullable=True, default=False)
|
tuning = db.Column(db.Boolean, nullable=True, default=False)
|
||||||
configuration = db.Column(JSONB, nullable=True)
|
configuration = db.Column(JSONB, nullable=True)
|
||||||
arguments = db.Column(JSONB, nullable=True)
|
arguments = db.Column(JSONB, nullable=True)
|
||||||
|
active = db.Column(db.Boolean, nullable=True, default=True)
|
||||||
|
|
||||||
# Relationship to retrievers through the association table
|
# Relationship to retrievers through the association table
|
||||||
retrievers = db.relationship('SpecialistRetriever', backref='specialist', lazy=True,
|
retrievers = db.relationship('SpecialistRetriever', backref='specialist', lazy=True,
|
||||||
@@ -44,6 +45,21 @@ class Specialist(db.Model):
|
|||||||
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
||||||
updated_by = db.Column(db.Integer, db.ForeignKey(User.id))
|
updated_by = db.Column(db.Integer, db.ForeignKey(User.id))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Specialist {self.id}: {self.name}>"
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description,
|
||||||
|
'type': self.type,
|
||||||
|
'type_version': self.type_version,
|
||||||
|
'configuration': self.configuration,
|
||||||
|
'arguments': self.arguments,
|
||||||
|
'active': self.active,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class EveAIAsset(db.Model):
|
class EveAIAsset(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -215,3 +231,36 @@ class SpecialistDispatcher(db.Model):
|
|||||||
dispatcher_id = db.Column(db.Integer, db.ForeignKey(Dispatcher.id, ondelete='CASCADE'), primary_key=True)
|
dispatcher_id = db.Column(db.Integer, db.ForeignKey(Dispatcher.id, ondelete='CASCADE'), primary_key=True)
|
||||||
|
|
||||||
dispatcher = db.relationship("Dispatcher", backref="specialist_dispatchers")
|
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 common.extensions import db
|
||||||
from flask_security import UserMixin, RoleMixin
|
from flask_security import UserMixin, RoleMixin
|
||||||
from sqlalchemy.dialects.postgresql import ARRAY
|
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from common.models.entitlements import License
|
from common.models.entitlements import License
|
||||||
@@ -28,17 +28,19 @@ class Tenant(db.Model):
|
|||||||
|
|
||||||
# language information
|
# language information
|
||||||
default_language = db.Column(db.String(2), nullable=True)
|
default_language = db.Column(db.String(2), nullable=True)
|
||||||
allowed_languages = db.Column(ARRAY(sa.String(2)), nullable=True)
|
|
||||||
|
|
||||||
# Entitlements
|
# Entitlements
|
||||||
currency = db.Column(db.String(20), nullable=True)
|
currency = db.Column(db.String(20), nullable=True)
|
||||||
storage_dirty = db.Column(db.Boolean, nullable=True, default=False)
|
storage_dirty = db.Column(db.Boolean, nullable=True, default=False)
|
||||||
|
default_tenant_make_id = db.Column(db.Integer, db.ForeignKey('public.tenant_make.id'), nullable=True)
|
||||||
|
|
||||||
# Relations
|
# Relations
|
||||||
users = db.relationship('User', backref='tenant')
|
users = db.relationship('User', backref='tenant')
|
||||||
domains = db.relationship('TenantDomain', backref='tenant')
|
domains = db.relationship('TenantDomain', backref='tenant')
|
||||||
licenses = db.relationship('License', back_populates='tenant')
|
licenses = db.relationship('License', back_populates='tenant')
|
||||||
license_usages = db.relationship('LicenseUsage', backref='tenant')
|
license_usages = db.relationship('LicenseUsage', backref='tenant')
|
||||||
|
tenant_makes = db.relationship('TenantMake', backref='tenant', foreign_keys='TenantMake.tenant_id')
|
||||||
|
default_tenant_make = db.relationship('TenantMake', foreign_keys=[default_tenant_make_id], uselist=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_license(self):
|
def current_license(self):
|
||||||
@@ -60,8 +62,8 @@ class Tenant(db.Model):
|
|||||||
'timezone': self.timezone,
|
'timezone': self.timezone,
|
||||||
'type': self.type,
|
'type': self.type,
|
||||||
'default_language': self.default_language,
|
'default_language': self.default_language,
|
||||||
'allowed_languages': self.allowed_languages,
|
|
||||||
'currency': self.currency,
|
'currency': self.currency,
|
||||||
|
'default_tenant_make_id': self.default_tenant_make_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -173,6 +175,42 @@ class TenantProject(db.Model):
|
|||||||
return f"<TenantProject {self.id}: {self.name}>"
|
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):
|
class Partner(db.Model):
|
||||||
__bind_key__ = 'public'
|
__bind_key__ = 'public'
|
||||||
__table_args__ = {'schema': '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)
|
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_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)
|
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.extensions import db
|
||||||
from common.models.entitlements import PartnerServiceLicenseTier
|
from common.models.entitlements import PartnerServiceLicenseTier
|
||||||
from common.models.user import Partner
|
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
|
from common.utils.model_logging_utils import set_logging_information
|
||||||
|
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ class LicenseTierServices:
|
|||||||
# Get partner service (MANAGEMENT_SERVICE type)
|
# Get partner service (MANAGEMENT_SERVICE type)
|
||||||
partner = Partner.query.get(partner_id)
|
partner = Partner.query.get(partner_id)
|
||||||
if not partner:
|
if not partner:
|
||||||
return
|
raise EveAINoSessionPartner()
|
||||||
|
|
||||||
# Find a management service for this partner
|
# Find a management service for this partner
|
||||||
management_service = next((service for service in session['partner']['services']
|
management_service = next((service for service in session['partner']['services']
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, Any, Tuple
|
from datetime import datetime as dt, timezone as tz
|
||||||
|
from typing import Dict, Any, Tuple, Optional
|
||||||
|
from flask import current_app
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from common.extensions import db, cache_manager
|
||||||
|
from common.models.interaction import (
|
||||||
|
Specialist, EveAIAgent, EveAITask, EveAITool
|
||||||
|
)
|
||||||
from common.utils.celery_utils import current_celery
|
from common.utils.celery_utils import current_celery
|
||||||
|
from common.utils.model_logging_utils import set_logging_information, update_logging_information
|
||||||
|
|
||||||
|
|
||||||
class SpecialistServices:
|
class SpecialistServices:
|
||||||
@@ -27,4 +35,203 @@ class SpecialistServices:
|
|||||||
'status': 'queued',
|
'status': 'queued',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initialize_specialist(specialist_id: int, specialist_type: str, specialist_version: str):
|
||||||
|
"""
|
||||||
|
Initialize an agentic specialist by creating all its components based on configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
specialist_id: ID of the specialist to initialize
|
||||||
|
specialist_type: Type of the specialist
|
||||||
|
specialist_version: Version of the specialist type to use
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If specialist not found or invalid configuration
|
||||||
|
SQLAlchemyError: If database operations fail
|
||||||
|
"""
|
||||||
|
config = cache_manager.specialists_config_cache.get_config(specialist_type, specialist_version)
|
||||||
|
if not config:
|
||||||
|
raise ValueError(f"No configuration found for {specialist_type} version {specialist_version}")
|
||||||
|
if config['framework'] == 'langchain':
|
||||||
|
pass # Langchain does not require additional items to be initialized. All configuration is in the specialist.
|
||||||
|
|
||||||
|
specialist = Specialist.query.get(specialist_id)
|
||||||
|
if not specialist:
|
||||||
|
raise ValueError(f"Specialist with ID {specialist_id} not found")
|
||||||
|
|
||||||
|
if config['framework'] == 'crewai':
|
||||||
|
SpecialistServices.initialize_crewai_specialist(specialist, config)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initialize_crewai_specialist(specialist: Specialist, config: Dict[str, Any]):
|
||||||
|
timestamp = dt.now(tz=tz.utc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Initialize agents
|
||||||
|
if 'agents' in config:
|
||||||
|
for agent_config in config['agents']:
|
||||||
|
SpecialistServices._create_agent(
|
||||||
|
specialist_id=specialist.id,
|
||||||
|
agent_type=agent_config['type'],
|
||||||
|
agent_version=agent_config['version'],
|
||||||
|
name=agent_config.get('name'),
|
||||||
|
description=agent_config.get('description'),
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize tasks
|
||||||
|
if 'tasks' in config:
|
||||||
|
for task_config in config['tasks']:
|
||||||
|
SpecialistServices._create_task(
|
||||||
|
specialist_id=specialist.id,
|
||||||
|
task_type=task_config['type'],
|
||||||
|
task_version=task_config['version'],
|
||||||
|
name=task_config.get('name'),
|
||||||
|
description=task_config.get('description'),
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize tools
|
||||||
|
if 'tools' in config:
|
||||||
|
for tool_config in config['tools']:
|
||||||
|
SpecialistServices._create_tool(
|
||||||
|
specialist_id=specialist.id,
|
||||||
|
tool_type=tool_config['type'],
|
||||||
|
tool_version=tool_config['version'],
|
||||||
|
name=tool_config.get('name'),
|
||||||
|
description=tool_config.get('description'),
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
current_app.logger.info(f"Successfully initialized crewai specialist {specialist.id}")
|
||||||
|
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"Database error initializing crewai specialist {specialist.id}: {str(e)}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"Error initializing crewai specialist {specialist.id}: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_agent(
|
||||||
|
specialist_id: int,
|
||||||
|
agent_type: str,
|
||||||
|
agent_version: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
timestamp: Optional[dt] = None
|
||||||
|
) -> EveAIAgent:
|
||||||
|
"""Create an agent with the given configuration."""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = dt.now(tz=tz.utc)
|
||||||
|
|
||||||
|
# Get agent configuration from cache
|
||||||
|
agent_config = cache_manager.agents_config_cache.get_config(agent_type, agent_version)
|
||||||
|
|
||||||
|
agent = EveAIAgent(
|
||||||
|
specialist_id=specialist_id,
|
||||||
|
name=name or agent_config.get('name', agent_type),
|
||||||
|
description=description or agent_config.get('metadata').get('description', ''),
|
||||||
|
type=agent_type,
|
||||||
|
type_version=agent_version,
|
||||||
|
role=None,
|
||||||
|
goal=None,
|
||||||
|
backstory=None,
|
||||||
|
tuning=False,
|
||||||
|
configuration=None,
|
||||||
|
arguments=None
|
||||||
|
)
|
||||||
|
|
||||||
|
set_logging_information(agent, timestamp)
|
||||||
|
|
||||||
|
db.session.add(agent)
|
||||||
|
current_app.logger.info(f"Created agent {agent.id} of type {agent_type}")
|
||||||
|
return agent
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_task(
|
||||||
|
specialist_id: int,
|
||||||
|
task_type: str,
|
||||||
|
task_version: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
timestamp: Optional[dt] = None
|
||||||
|
) -> EveAITask:
|
||||||
|
"""Create a task with the given configuration."""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = dt.now(tz=tz.utc)
|
||||||
|
|
||||||
|
# Get task configuration from cache
|
||||||
|
task_config = cache_manager.tasks_config_cache.get_config(task_type, task_version)
|
||||||
|
|
||||||
|
task = EveAITask(
|
||||||
|
specialist_id=specialist_id,
|
||||||
|
name=name or task_config.get('name', task_type),
|
||||||
|
description=description or task_config.get('metadata').get('description', ''),
|
||||||
|
type=task_type,
|
||||||
|
type_version=task_version,
|
||||||
|
task_description=None,
|
||||||
|
expected_output=None,
|
||||||
|
tuning=False,
|
||||||
|
configuration=None,
|
||||||
|
arguments=None,
|
||||||
|
context=None,
|
||||||
|
asynchronous=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
set_logging_information(task, timestamp)
|
||||||
|
|
||||||
|
db.session.add(task)
|
||||||
|
current_app.logger.info(f"Created task {task.id} of type {task_type}")
|
||||||
|
return task
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_tool(
|
||||||
|
specialist_id: int,
|
||||||
|
tool_type: str,
|
||||||
|
tool_version: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
timestamp: Optional[dt] = None
|
||||||
|
) -> EveAITool:
|
||||||
|
"""Create a tool with the given configuration."""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = dt.now(tz=tz.utc)
|
||||||
|
|
||||||
|
# Get tool configuration from cache
|
||||||
|
tool_config = cache_manager.tools_config_cache.get_config(tool_type, tool_version)
|
||||||
|
|
||||||
|
tool = EveAITool(
|
||||||
|
specialist_id=specialist_id,
|
||||||
|
name=name or tool_config.get('name', tool_type),
|
||||||
|
description=description or tool_config.get('metadata').get('description', ''),
|
||||||
|
type=tool_type,
|
||||||
|
type_version=tool_version,
|
||||||
|
tuning=False,
|
||||||
|
configuration=None,
|
||||||
|
arguments=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
set_logging_information(tool, timestamp)
|
||||||
|
|
||||||
|
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 service.get('type') == 'MANAGEMENT_SERVICE'), None)
|
||||||
|
|
||||||
if not management_service:
|
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.")
|
f"while associating tenant {tenant_id} with partner.")
|
||||||
raise EveAINoManagementPartnerService()
|
raise EveAINoManagementPartnerService()
|
||||||
|
|
||||||
|
|||||||
41
common/utils/cache/config_cache.py
vendored
41
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 common.utils.cache.base import CacheHandler, CacheKey
|
||||||
from config.type_defs import agent_types, task_types, tool_types, specialist_types, retriever_types, prompt_types, \
|
from config.type_defs import agent_types, task_types, tool_types, specialist_types, retriever_types, prompt_types, \
|
||||||
catalog_types, partner_service_types
|
catalog_types, partner_service_types, processor_types, customisation_types, specialist_form_types
|
||||||
|
|
||||||
|
|
||||||
def is_major_minor(version: str) -> bool:
|
def is_major_minor(version: str) -> bool:
|
||||||
@@ -59,7 +59,7 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
|
|||||||
"""Set the version tree cache dependency."""
|
"""Set the version tree cache dependency."""
|
||||||
self.version_tree_cache = cache
|
self.version_tree_cache = cache
|
||||||
|
|
||||||
def _load_specific_config(self, type_name: str, version_str: str) -> Dict[str, Any]:
|
def _load_specific_config(self, type_name: str, version_str: str = 'latest') -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Load a specific configuration version
|
Load a specific configuration version
|
||||||
Automatically handles global vs partner-specific configs
|
Automatically handles global vs partner-specific configs
|
||||||
@@ -456,7 +456,13 @@ CatalogConfigCacheHandler, CatalogConfigVersionTreeCacheHandler, CatalogConfigTy
|
|||||||
types_module=catalog_types.CATALOG_TYPES
|
types_module=catalog_types.CATALOG_TYPES
|
||||||
))
|
))
|
||||||
|
|
||||||
# Add to common/utils/cache/config_cache.py
|
ProcessorConfigCacheHandler, ProcessorConfigVersionTreeCacheHandler, ProcessorConfigTypesCacheHandler = (
|
||||||
|
create_config_cache_handlers(
|
||||||
|
config_type='processors',
|
||||||
|
config_dir='config/processors',
|
||||||
|
types_module=processor_types.PROCESSOR_TYPES
|
||||||
|
))
|
||||||
|
|
||||||
PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, PartnerServiceConfigTypesCacheHandler = (
|
PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, PartnerServiceConfigTypesCacheHandler = (
|
||||||
create_config_cache_handlers(
|
create_config_cache_handlers(
|
||||||
config_type='partner_services',
|
config_type='partner_services',
|
||||||
@@ -464,6 +470,22 @@ PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, P
|
|||||||
types_module=partner_service_types.PARTNER_SERVICE_TYPES
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
SpecialistFormConfigCacheHandler, SpecialistFormConfigVersionTreeCacheHandler, SpecialistFormConfigTypesCacheHandler = (
|
||||||
|
create_config_cache_handlers(
|
||||||
|
config_type='specialist_forms',
|
||||||
|
config_dir='config/specialist_forms',
|
||||||
|
types_module=specialist_form_types.SPECIALIST_FORM_TYPES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_config_cache_handlers(cache_manager) -> None:
|
def register_config_cache_handlers(cache_manager) -> None:
|
||||||
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
||||||
@@ -487,12 +509,21 @@ def register_config_cache_handlers(cache_manager) -> None:
|
|||||||
cache_manager.register_handler(CatalogConfigCacheHandler, 'eveai_config')
|
cache_manager.register_handler(CatalogConfigCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(CatalogConfigTypesCacheHandler, 'eveai_config')
|
cache_manager.register_handler(CatalogConfigTypesCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(CatalogConfigVersionTreeCacheHandler, 'eveai_config')
|
cache_manager.register_handler(CatalogConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(ProcessorConfigCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(ProcessorConfigTypesCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(ProcessorConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(AgentConfigTypesCacheHandler, 'eveai_config')
|
cache_manager.register_handler(AgentConfigTypesCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(AgentConfigVersionTreeCacheHandler, 'eveai_config')
|
cache_manager.register_handler(AgentConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(PartnerServiceConfigCacheHandler, 'eveai_config')
|
cache_manager.register_handler(PartnerServiceConfigCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(PartnerServiceConfigTypesCacheHandler, 'eveai_config')
|
cache_manager.register_handler(PartnerServiceConfigTypesCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(PartnerServiceConfigVersionTreeCacheHandler, '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.register_handler(SpecialistFormConfigCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(SpecialistFormConfigTypesCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(SpecialistFormConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
|
|
||||||
cache_manager.agents_config_cache.set_version_tree_cache(cache_manager.agents_version_tree_cache)
|
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)
|
cache_manager.tasks_config_cache.set_version_tree_cache(cache_manager.tasks_version_tree_cache)
|
||||||
@@ -500,4 +531,8 @@ def register_config_cache_handlers(cache_manager) -> None:
|
|||||||
cache_manager.specialists_config_cache.set_version_tree_cache(cache_manager.specialists_version_tree_cache)
|
cache_manager.specialists_config_cache.set_version_tree_cache(cache_manager.specialists_version_tree_cache)
|
||||||
cache_manager.retrievers_config_cache.set_version_tree_cache(cache_manager.retrievers_version_tree_cache)
|
cache_manager.retrievers_config_cache.set_version_tree_cache(cache_manager.retrievers_version_tree_cache)
|
||||||
cache_manager.prompts_config_cache.set_version_tree_cache(cache_manager.prompts_version_tree_cache)
|
cache_manager.prompts_config_cache.set_version_tree_cache(cache_manager.prompts_version_tree_cache)
|
||||||
|
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.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)
|
||||||
|
cache_manager.specialist_forms_config_cache.set_version_tree_cache(cache_manager.specialist_forms_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')
|
@field_validator('type', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_type(cls, v: str) -> str:
|
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:
|
if v not in valid_types:
|
||||||
raise ValueError(f'type must be one of {valid_types}')
|
raise ValueError(f'type must be one of {valid_types}')
|
||||||
return v
|
return v
|
||||||
@@ -243,7 +243,7 @@ class ArgumentDefinition(BaseModel):
|
|||||||
@field_validator('type')
|
@field_validator('type')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_type(cls, v: str) -> str:
|
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:
|
if v not in valid_types:
|
||||||
raise ValueError(f'type must be one of {valid_types}')
|
raise ValueError(f'type must be one of {valid_types}')
|
||||||
return v
|
return v
|
||||||
@@ -256,7 +256,8 @@ class ArgumentDefinition(BaseModel):
|
|||||||
'integer': NumericConstraint,
|
'integer': NumericConstraint,
|
||||||
'float': NumericConstraint,
|
'float': NumericConstraint,
|
||||||
'date': DateConstraint,
|
'date': DateConstraint,
|
||||||
'enum': EnumConstraint
|
'enum': EnumConstraint,
|
||||||
|
'color': StringConstraint
|
||||||
}
|
}
|
||||||
|
|
||||||
expected_type = expected_constraint_types.get(self.type)
|
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
|
default_config[field_name] = 0
|
||||||
elif field_type == "boolean":
|
elif field_type == "boolean":
|
||||||
default_config[field_name] = False
|
default_config[field_name] = False
|
||||||
|
elif field_type == "color":
|
||||||
|
default_config[field_name] = "#000000"
|
||||||
else:
|
else:
|
||||||
default_config[field_name] = ""
|
default_config[field_name] = ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
@@ -12,6 +13,7 @@ def not_found_error(error):
|
|||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(prefixed_url_for('security.login'))
|
return redirect(prefixed_url_for('security.login'))
|
||||||
current_app.logger.error(f"Not Found Error: {error}")
|
current_app.logger.error(f"Not Found Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
return render_template('error/404.html'), 404
|
return render_template('error/404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
@@ -19,6 +21,7 @@ def internal_server_error(error):
|
|||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(prefixed_url_for('security.login'))
|
return redirect(prefixed_url_for('security.login'))
|
||||||
current_app.logger.error(f"Internal Server Error: {error}")
|
current_app.logger.error(f"Internal Server Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
return render_template('error/500.html'), 500
|
return render_template('error/500.html'), 500
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +29,7 @@ def not_authorised_error(error):
|
|||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(prefixed_url_for('security.login'))
|
return redirect(prefixed_url_for('security.login'))
|
||||||
current_app.logger.error(f"Not Authorised Error: {error}")
|
current_app.logger.error(f"Not Authorised Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
return render_template('error/401.html')
|
return render_template('error/401.html')
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +37,7 @@ def access_forbidden(error):
|
|||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(prefixed_url_for('security.login'))
|
return redirect(prefixed_url_for('security.login'))
|
||||||
current_app.logger.error(f"Access Forbidden: {error}")
|
current_app.logger.error(f"Access Forbidden: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
return render_template('error/403.html')
|
return render_template('error/403.html')
|
||||||
|
|
||||||
|
|
||||||
@@ -42,6 +47,7 @@ def key_error_handler(error):
|
|||||||
return redirect(prefixed_url_for('security.login'))
|
return redirect(prefixed_url_for('security.login'))
|
||||||
# For other KeyErrors, you might want to log the error and return a generic error page
|
# 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(f"Key Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
return render_template('error/generic.html', error_message="An unexpected error occurred"), 500
|
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.
|
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(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')
|
flash('Your session expired. You will have to re-enter your credentials', 'warning')
|
||||||
|
|
||||||
# Perform logout if user is authenticated
|
# Perform logout if user is authenticated
|
||||||
@@ -95,6 +102,26 @@ def general_exception(e):
|
|||||||
error_details=str(e)), 500
|
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):
|
def register_error_handlers(app):
|
||||||
app.register_error_handler(404, not_found_error)
|
app.register_error_handler(404, not_found_error)
|
||||||
app.register_error_handler(500, internal_server_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(EveAINoSessionTenant, no_tenant_selected_error)
|
||||||
app.register_error_handler(KeyError, key_error_handler)
|
app.register_error_handler(KeyError, key_error_handler)
|
||||||
app.register_error_handler(AttributeError, attribute_error_handler)
|
app.register_error_handler(AttributeError, attribute_error_handler)
|
||||||
app.register_error_handler(Exception, general_exception)
|
app.register_error_handler(jinja2.TemplateNotFound, template_not_found_error)
|
||||||
|
app.register_error_handler(jinja2.TemplateSyntaxError, template_syntax_error)
|
||||||
@app.errorhandler(jinja2.TemplateNotFound)
|
app.register_error_handler(Exception, general_exception)
|
||||||
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
|
|
||||||
|
|
||||||
@@ -248,3 +248,14 @@ class EveAIPendingLicensePeriod(EveAIException):
|
|||||||
message = f"Basic Fee Payment has not been received yet. Please ensure payment has been made, and please wait for payment to be processed."
|
message = f"Basic Fee Payment has not been received yet. Please ensure payment has been made, and please wait for payment to be processed."
|
||||||
super().__init__(message, status_code, payload)
|
super().__init__(message, status_code, payload)
|
||||||
|
|
||||||
|
|
||||||
|
class EveAISpecialistExecutionError(EveAIException):
|
||||||
|
"""Raised when an error occurs during specialist execution"""
|
||||||
|
|
||||||
|
def __init__(self, tenant_id, specialist_id, session_id, details, status_code=400, payload=None):
|
||||||
|
message = (f"Error during specialist {specialist_id} execution \n"
|
||||||
|
f"with Session ID {session_id} \n"
|
||||||
|
f"for Tenant {tenant_id}. \n"
|
||||||
|
f"Details: {details} \n"
|
||||||
|
f"The System Administrator has been notified. Please try again later.")
|
||||||
|
super().__init__(message, status_code, payload)
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
from datetime import datetime as dt, timezone as tz
|
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
from flask import current_app
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
|
||||||
|
|
||||||
from common.extensions import db, cache_manager
|
|
||||||
from common.models.interaction import (
|
|
||||||
Specialist, EveAIAgent, EveAITask, EveAITool
|
|
||||||
)
|
|
||||||
from common.utils.model_logging_utils import set_logging_information, update_logging_information
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_specialist(specialist_id: int, specialist_type: str, specialist_version: str):
|
|
||||||
"""
|
|
||||||
Initialize an agentic specialist by creating all its components based on configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
specialist_id: ID of the specialist to initialize
|
|
||||||
specialist_type: Type of the specialist
|
|
||||||
specialist_version: Version of the specialist type to use
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If specialist not found or invalid configuration
|
|
||||||
SQLAlchemyError: If database operations fail
|
|
||||||
"""
|
|
||||||
config = cache_manager.specialists_config_cache.get_config(specialist_type, specialist_version)
|
|
||||||
if not config:
|
|
||||||
raise ValueError(f"No configuration found for {specialist_type} version {specialist_version}")
|
|
||||||
if config['framework'] == 'langchain':
|
|
||||||
pass # Langchain does not require additional items to be initialized. All configuration is in the specialist.
|
|
||||||
|
|
||||||
specialist = Specialist.query.get(specialist_id)
|
|
||||||
if not specialist:
|
|
||||||
raise ValueError(f"Specialist with ID {specialist_id} not found")
|
|
||||||
|
|
||||||
if config['framework'] == 'crewai':
|
|
||||||
initialize_crewai_specialist(specialist, config)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_crewai_specialist(specialist: Specialist, config: Dict[str, Any]):
|
|
||||||
timestamp = dt.now(tz=tz.utc)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Initialize agents
|
|
||||||
if 'agents' in config:
|
|
||||||
for agent_config in config['agents']:
|
|
||||||
_create_agent(
|
|
||||||
specialist_id=specialist.id,
|
|
||||||
agent_type=agent_config['type'],
|
|
||||||
agent_version=agent_config['version'],
|
|
||||||
name=agent_config.get('name'),
|
|
||||||
description=agent_config.get('description'),
|
|
||||||
timestamp=timestamp
|
|
||||||
)
|
|
||||||
|
|
||||||
# Initialize tasks
|
|
||||||
if 'tasks' in config:
|
|
||||||
for task_config in config['tasks']:
|
|
||||||
_create_task(
|
|
||||||
specialist_id=specialist.id,
|
|
||||||
task_type=task_config['type'],
|
|
||||||
task_version=task_config['version'],
|
|
||||||
name=task_config.get('name'),
|
|
||||||
description=task_config.get('description'),
|
|
||||||
timestamp=timestamp
|
|
||||||
)
|
|
||||||
|
|
||||||
# Initialize tools
|
|
||||||
if 'tools' in config:
|
|
||||||
for tool_config in config['tools']:
|
|
||||||
_create_tool(
|
|
||||||
specialist_id=specialist.id,
|
|
||||||
tool_type=tool_config['type'],
|
|
||||||
tool_version=tool_config['version'],
|
|
||||||
name=tool_config.get('name'),
|
|
||||||
description=tool_config.get('description'),
|
|
||||||
timestamp=timestamp
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
current_app.logger.info(f"Successfully initialized crewai specialist {specialist.id}")
|
|
||||||
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
db.session.rollback()
|
|
||||||
current_app.logger.error(f"Database error initializing crewai specialist {specialist.id}: {str(e)}")
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
db.session.rollback()
|
|
||||||
current_app.logger.error(f"Error initializing crewai specialist {specialist.id}: {str(e)}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def _create_agent(
|
|
||||||
specialist_id: int,
|
|
||||||
agent_type: str,
|
|
||||||
agent_version: str,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
description: Optional[str] = None,
|
|
||||||
timestamp: Optional[dt] = None
|
|
||||||
) -> EveAIAgent:
|
|
||||||
"""Create an agent with the given configuration."""
|
|
||||||
if timestamp is None:
|
|
||||||
timestamp = dt.now(tz=tz.utc)
|
|
||||||
|
|
||||||
# Get agent configuration from cache
|
|
||||||
agent_config = cache_manager.agents_config_cache.get_config(agent_type, agent_version)
|
|
||||||
|
|
||||||
agent = EveAIAgent(
|
|
||||||
specialist_id=specialist_id,
|
|
||||||
name=name or agent_config.get('name', agent_type),
|
|
||||||
description=description or agent_config.get('metadata').get('description', ''),
|
|
||||||
type=agent_type,
|
|
||||||
type_version=agent_version,
|
|
||||||
role=None,
|
|
||||||
goal=None,
|
|
||||||
backstory=None,
|
|
||||||
tuning=False,
|
|
||||||
configuration=None,
|
|
||||||
arguments=None
|
|
||||||
)
|
|
||||||
|
|
||||||
set_logging_information(agent, timestamp)
|
|
||||||
|
|
||||||
db.session.add(agent)
|
|
||||||
current_app.logger.info(f"Created agent {agent.id} of type {agent_type}")
|
|
||||||
return agent
|
|
||||||
|
|
||||||
|
|
||||||
def _create_task(
|
|
||||||
specialist_id: int,
|
|
||||||
task_type: str,
|
|
||||||
task_version: str,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
description: Optional[str] = None,
|
|
||||||
timestamp: Optional[dt] = None
|
|
||||||
) -> EveAITask:
|
|
||||||
"""Create a task with the given configuration."""
|
|
||||||
if timestamp is None:
|
|
||||||
timestamp = dt.now(tz=tz.utc)
|
|
||||||
|
|
||||||
# Get task configuration from cache
|
|
||||||
task_config = cache_manager.tasks_config_cache.get_config(task_type, task_version)
|
|
||||||
|
|
||||||
task = EveAITask(
|
|
||||||
specialist_id=specialist_id,
|
|
||||||
name=name or task_config.get('name', task_type),
|
|
||||||
description=description or task_config.get('metadata').get('description', ''),
|
|
||||||
type=task_type,
|
|
||||||
type_version=task_version,
|
|
||||||
task_description=None,
|
|
||||||
expected_output=None,
|
|
||||||
tuning=False,
|
|
||||||
configuration=None,
|
|
||||||
arguments=None,
|
|
||||||
context=None,
|
|
||||||
asynchronous=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
set_logging_information(task, timestamp)
|
|
||||||
|
|
||||||
db.session.add(task)
|
|
||||||
current_app.logger.info(f"Created task {task.id} of type {task_type}")
|
|
||||||
return task
|
|
||||||
|
|
||||||
|
|
||||||
def _create_tool(
|
|
||||||
specialist_id: int,
|
|
||||||
tool_type: str,
|
|
||||||
tool_version: str,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
description: Optional[str] = None,
|
|
||||||
timestamp: Optional[dt] = None
|
|
||||||
) -> EveAITool:
|
|
||||||
"""Create a tool with the given configuration."""
|
|
||||||
if timestamp is None:
|
|
||||||
timestamp = dt.now(tz=tz.utc)
|
|
||||||
|
|
||||||
# Get tool configuration from cache
|
|
||||||
tool_config = cache_manager.tools_config_cache.get_config(tool_type, tool_version)
|
|
||||||
|
|
||||||
tool = EveAITool(
|
|
||||||
specialist_id=specialist_id,
|
|
||||||
name=name or tool_config.get('name', tool_type),
|
|
||||||
description=description or tool_config.get('metadata').get('description', ''),
|
|
||||||
type=tool_type,
|
|
||||||
type_version=tool_version,
|
|
||||||
tuning=False,
|
|
||||||
configuration=None,
|
|
||||||
arguments=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
set_logging_information(tool, timestamp)
|
|
||||||
|
|
||||||
db.session.add(tool)
|
|
||||||
current_app.logger.info(f"Created tool {tool.id} of type {tool_type}")
|
|
||||||
return tool
|
|
||||||
25
config/agents/traicie/TRAICIE_RECRUITER_AGENT/1.0.0.yaml
Normal file
25
config/agents/traicie/TRAICIE_RECRUITER_AGENT/1.0.0.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Traicie Recruiter"
|
||||||
|
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.magistral-medium-latest"
|
||||||
|
temperature: 0.3
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2025-06-18"
|
||||||
|
description: "Traicie Recruiter Agent"
|
||||||
|
changes: "Initial version"
|
||||||
21
config/catalogs/globals/DOSSIER_CATALOG/1.0.0.yaml
Normal file
21
config/catalogs/globals/DOSSIER_CATALOG/1.0.0.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Dossier Catalog"
|
||||||
|
description: "A Catalog with information in Evie's Library in which several Dossiers can be stored"
|
||||||
|
configuration:
|
||||||
|
tagging_fields:
|
||||||
|
name: "Tagging Fields"
|
||||||
|
type: "tagging_fields"
|
||||||
|
description: "Define the metadata fields that will be used for tagging documents.
|
||||||
|
Each field must have:
|
||||||
|
- type: one of 'string', 'integer', 'float', 'date', 'enum'
|
||||||
|
- required: boolean indicating if the field is mandatory
|
||||||
|
- description: field description
|
||||||
|
- allowed_values: list of values (for enum type only)
|
||||||
|
- min_value/max_value: range limits (for numeric types only)"
|
||||||
|
required: true
|
||||||
|
default: {}
|
||||||
|
document_version_configurations: ["tagging_fields"]
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A Catalog with information in Evie's Library in which several Dossiers can be stored"
|
||||||
9
config/catalogs/globals/STANDARD_CATALOG/1.0.0.yaml
Normal file
9
config/catalogs/globals/STANDARD_CATALOG/1.0.0.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Standard Catalog"
|
||||||
|
description: "A Catalog with information in Evie's Library, to be considered as a whole"
|
||||||
|
configuration: {}
|
||||||
|
document_version_configurations: []
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A Catalog with information in Evie's Library, to be considered as a whole"
|
||||||
106
config/config.py
106
config/config.py
@@ -12,10 +12,7 @@ class Config(object):
|
|||||||
DEBUG = False
|
DEBUG = False
|
||||||
DEVELOPMENT = False
|
DEVELOPMENT = False
|
||||||
SECRET_KEY = environ.get('SECRET_KEY')
|
SECRET_KEY = environ.get('SECRET_KEY')
|
||||||
SESSION_COOKIE_SECURE = False
|
|
||||||
SESSION_COOKIE_HTTPONLY = True
|
|
||||||
COMPONENT_NAME = environ.get('COMPONENT_NAME')
|
COMPONENT_NAME = environ.get('COMPONENT_NAME')
|
||||||
SESSION_KEY_PREFIX = f'{COMPONENT_NAME}_'
|
|
||||||
|
|
||||||
# Database Settings
|
# Database Settings
|
||||||
DB_HOST = environ.get('DB_HOST')
|
DB_HOST = environ.get('DB_HOST')
|
||||||
@@ -44,8 +41,6 @@ class Config(object):
|
|||||||
# SECURITY_POST_CHANGE_VIEW = '/admin/login'
|
# SECURITY_POST_CHANGE_VIEW = '/admin/login'
|
||||||
# SECURITY_BLUEPRINT_NAME = 'security_bp'
|
# SECURITY_BLUEPRINT_NAME = 'security_bp'
|
||||||
SECURITY_PASSWORD_SALT = environ.get('SECURITY_PASSWORD_SALT')
|
SECURITY_PASSWORD_SALT = environ.get('SECURITY_PASSWORD_SALT')
|
||||||
REMEMBER_COOKIE_SAMESITE = 'strict'
|
|
||||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
|
||||||
SECURITY_CONFIRMABLE = True
|
SECURITY_CONFIRMABLE = True
|
||||||
SECURITY_TRACKABLE = True
|
SECURITY_TRACKABLE = True
|
||||||
SECURITY_PASSWORD_COMPLEXITY_CHECKER = 'zxcvbn'
|
SECURITY_PASSWORD_COMPLEXITY_CHECKER = 'zxcvbn'
|
||||||
@@ -56,6 +51,10 @@ class Config(object):
|
|||||||
SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE = 'Your Password Has Been Reset'
|
SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE = 'Your Password Has Been Reset'
|
||||||
SECURITY_EMAIL_PLAINTEXT = False
|
SECURITY_EMAIL_PLAINTEXT = False
|
||||||
SECURITY_EMAIL_HTML = True
|
SECURITY_EMAIL_HTML = True
|
||||||
|
SECURITY_SESSION_PROTECTION = 'basic' # of 'basic' als 'strong' problemen geeft
|
||||||
|
SECURITY_REMEMBER_TOKEN_VALIDITY = timedelta(minutes=60) # Zelfde als session lifetime
|
||||||
|
SECURITY_AUTO_LOGIN_AFTER_CONFIRM = True
|
||||||
|
SECURITY_AUTO_LOGIN_AFTER_RESET = True
|
||||||
|
|
||||||
# Ensure Flask-Security-Too is handling CSRF tokens when behind a proxy
|
# Ensure Flask-Security-Too is handling CSRF tokens when behind a proxy
|
||||||
SECURITY_CSRF_PROTECT_MECHANISMS = ['session']
|
SECURITY_CSRF_PROTECT_MECHANISMS = ['session']
|
||||||
@@ -67,7 +66,89 @@ class Config(object):
|
|||||||
MAX_CONTENT_LENGTH = 50 * 1024 * 1024
|
MAX_CONTENT_LENGTH = 50 * 1024 * 1024
|
||||||
|
|
||||||
# supported languages
|
# supported languages
|
||||||
SUPPORTED_LANGUAGES = ['en', 'fr', 'nl', 'de', 'es']
|
SUPPORTED_LANGUAGES = ['en', 'fr', 'nl', 'de', 'es', 'it', 'pt', 'ru', 'zh', 'ja', 'ko', 'ar', 'hi']
|
||||||
|
SUPPORTED_LANGUAGE_DETAILS = {
|
||||||
|
"English": {
|
||||||
|
"iso 639-1": "en",
|
||||||
|
"iso 639-2": "eng",
|
||||||
|
"iso 639-3": "eng",
|
||||||
|
"flag": "🇬🇧"
|
||||||
|
},
|
||||||
|
"French": {
|
||||||
|
"iso 639-1": "fr",
|
||||||
|
"iso 639-2": "fre", # of 'fra'
|
||||||
|
"iso 639-3": "fra",
|
||||||
|
"flag": "🇫🇷"
|
||||||
|
},
|
||||||
|
"German": {
|
||||||
|
"iso 639-1": "de",
|
||||||
|
"iso 639-2": "ger", # of 'deu'
|
||||||
|
"iso 639-3": "deu",
|
||||||
|
"flag": "🇩🇪"
|
||||||
|
},
|
||||||
|
"Spanish": {
|
||||||
|
"iso 639-1": "es",
|
||||||
|
"iso 639-2": "spa",
|
||||||
|
"iso 639-3": "spa",
|
||||||
|
"flag": "🇪🇸"
|
||||||
|
},
|
||||||
|
"Italian": {
|
||||||
|
"iso 639-1": "it",
|
||||||
|
"iso 639-2": "ita",
|
||||||
|
"iso 639-3": "ita",
|
||||||
|
"flag": "🇮🇹"
|
||||||
|
},
|
||||||
|
"Portuguese": {
|
||||||
|
"iso 639-1": "pt",
|
||||||
|
"iso 639-2": "por",
|
||||||
|
"iso 639-3": "por",
|
||||||
|
"flag": "🇵🇹"
|
||||||
|
},
|
||||||
|
"Dutch": {
|
||||||
|
"iso 639-1": "nl",
|
||||||
|
"iso 639-2": "dut", # of 'nld'
|
||||||
|
"iso 639-3": "nld",
|
||||||
|
"flag": "🇳🇱"
|
||||||
|
},
|
||||||
|
"Russian": {
|
||||||
|
"iso 639-1": "ru",
|
||||||
|
"iso 639-2": "rus",
|
||||||
|
"iso 639-3": "rus",
|
||||||
|
"flag": "🇷🇺"
|
||||||
|
},
|
||||||
|
"Chinese": {
|
||||||
|
"iso 639-1": "zh",
|
||||||
|
"iso 639-2": "chi", # of 'zho'
|
||||||
|
"iso 639-3": "zho",
|
||||||
|
"flag": "🇨🇳"
|
||||||
|
},
|
||||||
|
"Japanese": {
|
||||||
|
"iso 639-1": "ja",
|
||||||
|
"iso 639-2": "jpn",
|
||||||
|
"iso 639-3": "jpn",
|
||||||
|
"flag": "🇯🇵"
|
||||||
|
},
|
||||||
|
"Korean": {
|
||||||
|
"iso 639-1": "ko",
|
||||||
|
"iso 639-2": "kor",
|
||||||
|
"iso 639-3": "kor",
|
||||||
|
"flag": "🇰🇷"
|
||||||
|
},
|
||||||
|
"Arabic": {
|
||||||
|
"iso 639-1": "ar",
|
||||||
|
"iso 639-2": "ara",
|
||||||
|
"iso 639-3": "ara",
|
||||||
|
"flag": "🇸🇦"
|
||||||
|
},
|
||||||
|
"Hindi": {
|
||||||
|
"iso 639-1": "hi",
|
||||||
|
"iso 639-2": "hin",
|
||||||
|
"iso 639-3": "hin",
|
||||||
|
"flag": "🇮🇳"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
SUPPORTED_LANGUAGES_Full = list(SUPPORTED_LANGUAGE_DETAILS.keys())
|
||||||
|
|
||||||
# supported currencies
|
# supported currencies
|
||||||
SUPPORTED_CURRENCIES = ['€', '$']
|
SUPPORTED_CURRENCIES = ['€', '$']
|
||||||
@@ -107,6 +188,15 @@ class Config(object):
|
|||||||
PERMANENT_SESSION_LIFETIME = timedelta(minutes=60)
|
PERMANENT_SESSION_LIFETIME = timedelta(minutes=60)
|
||||||
SESSION_REFRESH_EACH_REQUEST = True
|
SESSION_REFRESH_EACH_REQUEST = True
|
||||||
|
|
||||||
|
SESSION_COOKIE_NAME = f'{COMPONENT_NAME}_session'
|
||||||
|
SESSION_COOKIE_DOMAIN = None # Laat Flask dit automatisch bepalen
|
||||||
|
SESSION_COOKIE_PATH = '/'
|
||||||
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
SESSION_COOKIE_SECURE = False # True voor production met HTTPS
|
||||||
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
|
REMEMBER_COOKIE_SAMESITE = 'strict'
|
||||||
|
SESSION_KEY_PREFIX = f'{COMPONENT_NAME}_'
|
||||||
|
|
||||||
# JWT settings
|
# JWT settings
|
||||||
JWT_SECRET_KEY = environ.get('JWT_SECRET_KEY')
|
JWT_SECRET_KEY = environ.get('JWT_SECRET_KEY')
|
||||||
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1) # Set token expiry to 1 hour
|
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1) # Set token expiry to 1 hour
|
||||||
@@ -172,6 +262,9 @@ class Config(object):
|
|||||||
# Entitlement Constants
|
# Entitlement Constants
|
||||||
ENTITLEMENTS_MAX_PENDING_DAYS = 5 # Defines the maximum number of days a pending entitlement can be active
|
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):
|
class DevConfig(Config):
|
||||||
DEVELOPMENT = True
|
DEVELOPMENT = True
|
||||||
@@ -182,6 +275,7 @@ class DevConfig(Config):
|
|||||||
# Define the nginx prefix used for the specific apps
|
# Define the nginx prefix used for the specific apps
|
||||||
EVEAI_APP_LOCATION_PREFIX = '/admin'
|
EVEAI_APP_LOCATION_PREFIX = '/admin'
|
||||||
EVEAI_CHAT_LOCATION_PREFIX = '/chat'
|
EVEAI_CHAT_LOCATION_PREFIX = '/chat'
|
||||||
|
CHAT_CLIENT_PREFIX = 'chat-client/chat/'
|
||||||
|
|
||||||
# file upload settings
|
# file upload settings
|
||||||
# UPLOAD_FOLDER = '/app/tenant_files'
|
# UPLOAD_FOLDER = '/app/tenant_files'
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from datetime import datetime as dt, timezone as tz
|
from datetime import datetime as dt, timezone as tz
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from graypy import GELFUDPHandler
|
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
|
|
||||||
# Graylog configuration
|
|
||||||
GRAYLOG_HOST = os.environ.get('GRAYLOG_HOST', 'localhost')
|
|
||||||
GRAYLOG_PORT = int(os.environ.get('GRAYLOG_PORT', 12201))
|
|
||||||
env = os.environ.get('FLASK_ENV', 'development')
|
env = os.environ.get('FLASK_ENV', 'development')
|
||||||
|
|
||||||
|
|
||||||
@@ -144,23 +142,6 @@ class TuningFormatter(logging.Formatter):
|
|||||||
return formatted_msg
|
return formatted_msg
|
||||||
|
|
||||||
|
|
||||||
class GraylogFormatter(logging.Formatter):
|
|
||||||
"""Maintains existing Graylog formatting while adding tuning fields"""
|
|
||||||
|
|
||||||
def format(self, record):
|
|
||||||
if getattr(record, 'is_tuning_log', False):
|
|
||||||
# Add tuning-specific fields to Graylog
|
|
||||||
record.tuning_fields = {
|
|
||||||
'is_tuning_log': True,
|
|
||||||
'tuning_type': record.tuning_type,
|
|
||||||
'tenant_id': record.tenant_id,
|
|
||||||
'catalog_id': record.catalog_id,
|
|
||||||
'specialist_id': record.specialist_id,
|
|
||||||
'retriever_id': record.retriever_id,
|
|
||||||
'processor_id': record.processor_id,
|
|
||||||
'session_id': record.session_id,
|
|
||||||
}
|
|
||||||
return super().format(record)
|
|
||||||
|
|
||||||
class TuningLogger:
|
class TuningLogger:
|
||||||
"""Helper class to manage tuning logs with consistent structure"""
|
"""Helper class to manage tuning logs with consistent structure"""
|
||||||
@@ -177,10 +158,10 @@ class TuningLogger:
|
|||||||
specialist_id: Optional specialist ID for context
|
specialist_id: Optional specialist ID for context
|
||||||
retriever_id: Optional retriever ID for context
|
retriever_id: Optional retriever ID for context
|
||||||
processor_id: Optional processor ID for context
|
processor_id: Optional processor ID for context
|
||||||
session_id: Optional session ID for context and log file naming
|
session_id: Optional session ID for context
|
||||||
log_file: Optional custom log file name to use
|
log_file: Optional custom log file name (ignored - all logs go to tuning.log)
|
||||||
"""
|
"""
|
||||||
|
# Always use the standard tuning logger
|
||||||
self.logger = logging.getLogger(logger_name)
|
self.logger = logging.getLogger(logger_name)
|
||||||
self.tenant_id = tenant_id
|
self.tenant_id = tenant_id
|
||||||
self.catalog_id = catalog_id
|
self.catalog_id = catalog_id
|
||||||
@@ -188,63 +169,8 @@ class TuningLogger:
|
|||||||
self.retriever_id = retriever_id
|
self.retriever_id = retriever_id
|
||||||
self.processor_id = processor_id
|
self.processor_id = processor_id
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
self.log_file = log_file
|
|
||||||
# Determine whether to use a session-specific logger
|
|
||||||
if session_id:
|
|
||||||
# Create a unique logger name for this session
|
|
||||||
session_logger_name = f"{logger_name}_{session_id}"
|
|
||||||
self.logger = logging.getLogger(session_logger_name)
|
|
||||||
|
|
||||||
# If this logger doesn't have handlers yet, configure it
|
def log_tuning(self, tuning_type: str, message: str, data=None, level=logging.DEBUG):
|
||||||
if not self.logger.handlers:
|
|
||||||
# Determine log file path
|
|
||||||
if not log_file and session_id:
|
|
||||||
log_file = f"logs/tuning_{session_id}.log"
|
|
||||||
elif not log_file:
|
|
||||||
log_file = "logs/tuning.log"
|
|
||||||
|
|
||||||
# Configure the logger
|
|
||||||
self._configure_session_logger(log_file)
|
|
||||||
else:
|
|
||||||
# Use the standard tuning logger
|
|
||||||
self.logger = logging.getLogger(logger_name)
|
|
||||||
|
|
||||||
def _configure_session_logger(self, log_file):
|
|
||||||
"""Configure a new session-specific logger with appropriate handlers"""
|
|
||||||
# Create and configure a file handler
|
|
||||||
file_handler = logging.handlers.RotatingFileHandler(
|
|
||||||
filename=log_file,
|
|
||||||
maxBytes=1024 * 1024 * 3, # 3MB
|
|
||||||
backupCount=3
|
|
||||||
)
|
|
||||||
file_handler.setFormatter(TuningFormatter())
|
|
||||||
file_handler.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
# Add the file handler to the logger
|
|
||||||
self.logger.addHandler(file_handler)
|
|
||||||
|
|
||||||
# Add Graylog handler in production
|
|
||||||
env = os.environ.get('FLASK_ENV', 'development')
|
|
||||||
if env == 'production':
|
|
||||||
try:
|
|
||||||
graylog_handler = GELFUDPHandler(
|
|
||||||
host=GRAYLOG_HOST,
|
|
||||||
port=GRAYLOG_PORT,
|
|
||||||
debugging_fields=True
|
|
||||||
)
|
|
||||||
graylog_handler.setFormatter(GraylogFormatter())
|
|
||||||
self.logger.addHandler(graylog_handler)
|
|
||||||
except Exception as e:
|
|
||||||
# Fall back to just file logging if Graylog setup fails
|
|
||||||
fallback_logger = logging.getLogger('eveai_app')
|
|
||||||
fallback_logger.warning(f"Failed to set up Graylog handler: {str(e)}")
|
|
||||||
|
|
||||||
# Set logger level and disable propagation
|
|
||||||
self.logger.setLevel(logging.DEBUG)
|
|
||||||
self.logger.propagate = False
|
|
||||||
|
|
||||||
|
|
||||||
def log_tuning(self, tuning_type: str, message: str, data=None, level=logging.DEBUG):
|
|
||||||
"""Log a tuning event with structured data"""
|
"""Log a tuning event with structured data"""
|
||||||
try:
|
try:
|
||||||
# Create a standard LogRecord for tuning
|
# Create a standard LogRecord for tuning
|
||||||
@@ -275,13 +201,82 @@ def log_tuning(self, tuning_type: str, message: str, data=None, level=logging.DE
|
|||||||
self.logger.handle(record)
|
self.logger.handle(record)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
fallback_logger = logging.getLogger('eveai_workers')
|
print(f"Failed to log tuning message: {str(e)}")
|
||||||
fallback_logger.exception(f"Failed to log tuning message: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
# Set the custom log record factory
|
# Set the custom log record factory
|
||||||
logging.setLogRecordFactory(TuningLogRecord)
|
logging.setLogRecordFactory(TuningLogRecord)
|
||||||
|
|
||||||
|
def configure_logging():
|
||||||
|
"""Configure logging based on environment
|
||||||
|
|
||||||
|
When running in Kubernetes, directs logs to stdout in JSON format
|
||||||
|
Otherwise uses file-based logging for development/testing
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Verkrijg het absolute pad naar de logs directory
|
||||||
|
base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
logs_dir = os.path.join(base_dir, 'logs')
|
||||||
|
|
||||||
|
# Zorg ervoor dat de logs directory bestaat met de juiste permissies
|
||||||
|
if not os.path.exists(logs_dir):
|
||||||
|
try:
|
||||||
|
os.makedirs(logs_dir, exist_ok=True)
|
||||||
|
print(f"Logs directory aangemaakt op: {logs_dir}")
|
||||||
|
except (IOError, PermissionError) as e:
|
||||||
|
print(f"WAARSCHUWING: Kan logs directory niet aanmaken: {e}")
|
||||||
|
print(f"Logs worden mogelijk niet correct geschreven!")
|
||||||
|
|
||||||
|
# Check if running in Kubernetes
|
||||||
|
in_kubernetes = os.environ.get('KUBERNETES_SERVICE_HOST') is not None
|
||||||
|
|
||||||
|
# Controleer of de pythonjsonlogger pakket beschikbaar is als we in Kubernetes zijn
|
||||||
|
if in_kubernetes:
|
||||||
|
try:
|
||||||
|
import pythonjsonlogger.jsonlogger
|
||||||
|
has_json_logger = True
|
||||||
|
except ImportError:
|
||||||
|
print("WAARSCHUWING: python-json-logger pakket is niet geïnstalleerd.")
|
||||||
|
print("Voer 'pip install python-json-logger>=2.0.7' uit om JSON logging in te schakelen.")
|
||||||
|
print("Terugvallen op standaard logging formaat.")
|
||||||
|
has_json_logger = False
|
||||||
|
in_kubernetes = False # Fall back to standard logging
|
||||||
|
else:
|
||||||
|
has_json_logger = False
|
||||||
|
|
||||||
|
# Apply the configuration
|
||||||
|
logging_config = dict(LOGGING)
|
||||||
|
|
||||||
|
# Wijzig de json_console handler om terug te vallen op console als pythonjsonlogger niet beschikbaar is
|
||||||
|
if not has_json_logger and 'json_console' in logging_config['handlers']:
|
||||||
|
# Vervang json_console handler door een console handler met standaard formatter
|
||||||
|
logging_config['handlers']['json_console']['formatter'] = 'standard'
|
||||||
|
|
||||||
|
# In Kubernetes, conditionally modify specific loggers to use JSON console output
|
||||||
|
# This preserves the same logger names but changes where/how they log
|
||||||
|
if in_kubernetes:
|
||||||
|
for logger_name in logging_config['loggers']:
|
||||||
|
if logger_name: # Skip the root logger
|
||||||
|
logging_config['loggers'][logger_name]['handlers'] = ['json_console']
|
||||||
|
|
||||||
|
# Controleer of de logs directory schrijfbaar is voordat we de configuratie toepassen
|
||||||
|
logs_dir = os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs')
|
||||||
|
if os.path.exists(logs_dir) and not os.access(logs_dir, os.W_OK):
|
||||||
|
print(f"WAARSCHUWING: Logs directory bestaat maar is niet schrijfbaar: {logs_dir}")
|
||||||
|
print("Logs worden mogelijk niet correct geschreven!")
|
||||||
|
|
||||||
|
logging.config.dictConfig(logging_config)
|
||||||
|
logging.info(f"Logging configured. Environment: {'Kubernetes' if in_kubernetes else 'Development/Testing'}")
|
||||||
|
logging.info(f"Logs directory: {logs_dir}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error configuring logging: {str(e)}")
|
||||||
|
print("Gedetailleerde foutinformatie:")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
# Fall back to basic configuration
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
@@ -290,7 +285,7 @@ LOGGING = {
|
|||||||
'file_app': {
|
'file_app': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/eveai_app.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'eveai_app.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -298,15 +293,15 @@ LOGGING = {
|
|||||||
'file_workers': {
|
'file_workers': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/eveai_workers.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'eveai_workers.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
},
|
},
|
||||||
'file_chat': {
|
'file_chat_client': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/eveai_chat.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'eveai_chat_client.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -314,7 +309,7 @@ LOGGING = {
|
|||||||
'file_chat_workers': {
|
'file_chat_workers': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/eveai_chat_workers.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'eveai_chat_workers.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -322,7 +317,7 @@ LOGGING = {
|
|||||||
'file_api': {
|
'file_api': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/eveai_api.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'eveai_api.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -330,7 +325,7 @@ LOGGING = {
|
|||||||
'file_beat': {
|
'file_beat': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/eveai_beat.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'eveai_beat.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -338,7 +333,7 @@ LOGGING = {
|
|||||||
'file_entitlements': {
|
'file_entitlements': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/eveai_entitlements.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'eveai_entitlements.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -346,7 +341,7 @@ LOGGING = {
|
|||||||
'file_sqlalchemy': {
|
'file_sqlalchemy': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/sqlalchemy.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'sqlalchemy.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -354,7 +349,7 @@ LOGGING = {
|
|||||||
'file_security': {
|
'file_security': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/security.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'security.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -362,7 +357,7 @@ LOGGING = {
|
|||||||
'file_rag_tuning': {
|
'file_rag_tuning': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/rag_tuning.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'rag_tuning.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -370,7 +365,7 @@ LOGGING = {
|
|||||||
'file_embed_tuning': {
|
'file_embed_tuning': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/embed_tuning.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'embed_tuning.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -378,7 +373,7 @@ LOGGING = {
|
|||||||
'file_business_events': {
|
'file_business_events': {
|
||||||
'level': 'INFO',
|
'level': 'INFO',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/business_events.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'business_events.log'),
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -388,100 +383,104 @@ LOGGING = {
|
|||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
},
|
},
|
||||||
|
'json_console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'level': 'INFO',
|
||||||
|
'formatter': 'json',
|
||||||
|
'stream': 'ext://sys.stdout',
|
||||||
|
},
|
||||||
'tuning_file': {
|
'tuning_file': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/tuning.log',
|
'filename': os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'logs', 'tuning.log'),
|
||||||
'maxBytes': 1024 * 1024 * 3, # 3MB
|
'maxBytes': 1024 * 1024 * 3, # 3MB
|
||||||
'backupCount': 3,
|
'backupCount': 3,
|
||||||
'formatter': 'tuning',
|
'formatter': 'tuning',
|
||||||
},
|
},
|
||||||
'graylog': {
|
|
||||||
'level': 'DEBUG',
|
|
||||||
'class': 'graypy.GELFUDPHandler',
|
|
||||||
'host': GRAYLOG_HOST,
|
|
||||||
'port': GRAYLOG_PORT,
|
|
||||||
'debugging_fields': True,
|
|
||||||
'formatter': 'graylog'
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'formatters': {
|
'formatters': {
|
||||||
'standard': {
|
'standard': {
|
||||||
'format': '%(asctime)s [%(levelname)s] %(name)s (%(component)s) [%(module)s:%(lineno)d]: %(message)s',
|
'format': '%(asctime)s [%(levelname)s] %(name)s (%(component)s) [%(module)s:%(lineno)d]: %(message)s',
|
||||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||||
},
|
},
|
||||||
'graylog': {
|
|
||||||
'format': '[%(levelname)s] %(name)s (%(component)s) [%(module)s:%(lineno)d in %(funcName)s] '
|
|
||||||
'[Thread: %(threadName)s]: %(message)s',
|
|
||||||
'datefmt': '%Y-%m-%d %H:%M:%S',
|
|
||||||
'()': GraylogFormatter
|
|
||||||
},
|
|
||||||
'tuning': {
|
'tuning': {
|
||||||
'()': TuningFormatter,
|
'()': TuningFormatter,
|
||||||
'datefmt': '%Y-%m-%d %H:%M:%S UTC'
|
'datefmt': '%Y-%m-%d %H:%M:%S UTC'
|
||||||
|
},
|
||||||
|
'json': {
|
||||||
|
'format': '%(message)s',
|
||||||
|
'class': 'logging.Formatter' if not 'pythonjsonlogger' in sys.modules else 'pythonjsonlogger.jsonlogger.JsonFormatter',
|
||||||
|
'json_default': lambda obj: str(obj) if isinstance(obj, (dt, Exception)) else None,
|
||||||
|
'json_ensure_ascii': False,
|
||||||
|
'rename_fields': {
|
||||||
|
'asctime': 'timestamp',
|
||||||
|
'levelname': 'severity'
|
||||||
|
},
|
||||||
|
'timestamp': True,
|
||||||
|
'datefmt': '%Y-%m-%dT%H:%M:%S.%fZ'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'loggers': {
|
'loggers': {
|
||||||
'eveai_app': { # logger for the eveai_app
|
'eveai_app': { # logger for the eveai_app
|
||||||
'handlers': ['file_app', 'graylog', ] if env == 'production' else ['file_app', ],
|
'handlers': ['file_app'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'eveai_workers': { # logger for the eveai_workers
|
'eveai_workers': { # logger for the eveai_workers
|
||||||
'handlers': ['file_workers', 'graylog', ] if env == 'production' else ['file_workers', ],
|
'handlers': ['file_workers'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'eveai_chat': { # logger for the eveai_chat
|
'eveai_chat_client': { # logger for the eveai_chat
|
||||||
'handlers': ['file_chat', 'graylog', ] if env == 'production' else ['file_chat', ],
|
'handlers': ['file_chat_client'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'eveai_chat_workers': { # logger for the eveai_chat_workers
|
'eveai_chat_workers': { # logger for the eveai_chat_workers
|
||||||
'handlers': ['file_chat_workers', 'graylog', ] if env == 'production' else ['file_chat_workers', ],
|
'handlers': ['file_chat_workers'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'eveai_api': { # logger for the eveai_chat_workers
|
'eveai_api': { # logger for the eveai_api
|
||||||
'handlers': ['file_api', 'graylog', ] if env == 'production' else ['file_api', ],
|
'handlers': ['file_api'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'eveai_beat': { # logger for the eveai_beat
|
'eveai_beat': { # logger for the eveai_beat
|
||||||
'handlers': ['file_beat', 'graylog', ] if env == 'production' else ['file_beat', ],
|
'handlers': ['file_beat'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'eveai_entitlements': { # logger for the eveai_entitlements
|
'eveai_entitlements': { # logger for the eveai_entitlements
|
||||||
'handlers': ['file_entitlements', 'graylog', ] if env == 'production' else ['file_entitlements', ],
|
'handlers': ['file_entitlements'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'sqlalchemy.engine': { # logger for the sqlalchemy
|
'sqlalchemy.engine': { # logger for the sqlalchemy
|
||||||
'handlers': ['file_sqlalchemy', 'graylog', ] if env == 'production' else ['file_sqlalchemy', ],
|
'handlers': ['file_sqlalchemy'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'security': { # logger for the security
|
'security': { # logger for the security
|
||||||
'handlers': ['file_security', 'graylog', ] if env == 'production' else ['file_security', ],
|
'handlers': ['file_security'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'business_events': {
|
'business_events': {
|
||||||
'handlers': ['file_business_events', 'graylog'],
|
'handlers': ['file_business_events'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
# Single tuning logger
|
# Single tuning logger
|
||||||
'tuning': {
|
'tuning': {
|
||||||
'handlers': ['tuning_file', 'graylog'] if env == 'production' else ['tuning_file'],
|
'handlers': ['tuning_file'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False,
|
'propagate': False,
|
||||||
},
|
},
|
||||||
'': { # root logger
|
'': { # root logger
|
||||||
'handlers': ['console'],
|
'handlers': ['console'] if os.environ.get('KUBERNETES_SERVICE_HOST') is None else ['json_console'],
|
||||||
'level': 'WARNING', # Set higher level for root to minimize noise
|
'level': 'WARNING', # Set higher level for root to minimize noise
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
9
config/processors/globals/AUDIO_PROCESSOR/1.0.0.yaml
Normal file
9
config/processors/globals/AUDIO_PROCESSOR/1.0.0.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "AUDIO Processor"
|
||||||
|
file_types: "mp3, mp4, ogg"
|
||||||
|
description: "A Processor for audio files"
|
||||||
|
configuration: {}
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A Processor for audio files"
|
||||||
59
config/processors/globals/DOCX_PROCESSOR/1.0.0.yaml
Normal file
59
config/processors/globals/DOCX_PROCESSOR/1.0.0.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "DOCX Processor"
|
||||||
|
file_types: "docx"
|
||||||
|
description: "A processor for DOCX files"
|
||||||
|
configuration:
|
||||||
|
chunking_patterns:
|
||||||
|
name: "Chunking Patterns"
|
||||||
|
description: "A list of Patterns used to chunk files into logical pieces"
|
||||||
|
type: "chunking_patterns"
|
||||||
|
required: false
|
||||||
|
chunking_heading_level:
|
||||||
|
name: "Chunking Heading Level"
|
||||||
|
type: "integer"
|
||||||
|
description: "Maximum heading level to consider for chunking (1-6)"
|
||||||
|
required: false
|
||||||
|
default: 2
|
||||||
|
extract_comments:
|
||||||
|
name: "Extract Comments"
|
||||||
|
type: "boolean"
|
||||||
|
description: "Whether to include document comments in the markdown"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
extract_headers_footers:
|
||||||
|
name: "Extract Headers/Footers"
|
||||||
|
type: "boolean"
|
||||||
|
description: "Whether to include headers and footers in the markdown"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
preserve_formatting:
|
||||||
|
name: "Preserve Formatting"
|
||||||
|
type: "boolean"
|
||||||
|
description: "Whether to preserve bold, italic, and other text formatting"
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
list_style:
|
||||||
|
name: "List Style"
|
||||||
|
type: "enum"
|
||||||
|
description: "How to format lists in markdown"
|
||||||
|
required: false
|
||||||
|
default: "dash"
|
||||||
|
allowed_values: ["dash", "asterisk", "plus"]
|
||||||
|
image_handling:
|
||||||
|
name: "Image Handling"
|
||||||
|
type: "enum"
|
||||||
|
description: "How to handle embedded images"
|
||||||
|
required: false
|
||||||
|
default: "skip"
|
||||||
|
allowed_values: ["skip", "extract", "placeholder"]
|
||||||
|
table_alignment:
|
||||||
|
name: "Table Alignment"
|
||||||
|
type: "enum"
|
||||||
|
description: "How to align table contents"
|
||||||
|
required: false
|
||||||
|
default: "left"
|
||||||
|
allowed_values: ["left", "center", "preserve"]
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A processor for DOCX files"
|
||||||
49
config/processors/globals/HTML_PROCESSOR/1.0.0.yaml
Normal file
49
config/processors/globals/HTML_PROCESSOR/1.0.0.yaml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "HTML Processor"
|
||||||
|
file_types: "html"
|
||||||
|
description: "A processor for HTML files"
|
||||||
|
configuration:
|
||||||
|
chunking_patterns:
|
||||||
|
name: "Chunking Patterns"
|
||||||
|
description: "A list of Patterns used to chunk files into logical pieces"
|
||||||
|
type: "chunking_patterns"
|
||||||
|
required: false
|
||||||
|
chunking_heading_level:
|
||||||
|
name: "Chunking Heading Level"
|
||||||
|
type: "integer"
|
||||||
|
description: "Maximum heading level to consider for chunking (1-6)"
|
||||||
|
required: false
|
||||||
|
default: 2
|
||||||
|
html_tags:
|
||||||
|
name: "HTML Tags"
|
||||||
|
type: "string"
|
||||||
|
description: "A comma-separated list of HTML tags"
|
||||||
|
required: true
|
||||||
|
default: "p, h1, h2, h3, h4, h5, h6, li, table, thead, tbody, tr, td"
|
||||||
|
html_end_tags:
|
||||||
|
name: "HTML End Tags"
|
||||||
|
type: "string"
|
||||||
|
description: "A comma-separated list of HTML end tags (where can the chunk end)"
|
||||||
|
required: true
|
||||||
|
default: "p, li, table"
|
||||||
|
html_included_elements:
|
||||||
|
name: "HTML Included Elements"
|
||||||
|
type: "string"
|
||||||
|
description: "A comma-separated list of elements to be included"
|
||||||
|
required: true
|
||||||
|
default: "article, main"
|
||||||
|
html_excluded_elements:
|
||||||
|
name: "HTML Excluded Elements"
|
||||||
|
type: "string"
|
||||||
|
description: "A comma-separated list of elements to be excluded"
|
||||||
|
required: false
|
||||||
|
default: "header, footer, nav, script"
|
||||||
|
html_excluded_classes:
|
||||||
|
name: "HTML Excluded Classes"
|
||||||
|
type: "string"
|
||||||
|
description: "A comma-separated list of classes to be excluded"
|
||||||
|
required: false
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A processor for HTML files"
|
||||||
20
config/processors/globals/MARKDOWN_PROCESSOR/1.0.0.yaml
Normal file
20
config/processors/globals/MARKDOWN_PROCESSOR/1.0.0.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Markdown Processor"
|
||||||
|
file_types: "md"
|
||||||
|
description: "A Processor for markdown files"
|
||||||
|
configuration:
|
||||||
|
chunking_patterns:
|
||||||
|
name: "Chunking Patterns"
|
||||||
|
description: "A list of Patterns used to chunk files into logical pieces"
|
||||||
|
type: "chunking_patterns"
|
||||||
|
required: false
|
||||||
|
chunking_heading_level:
|
||||||
|
name: "Chunking Heading Level"
|
||||||
|
type: "integer"
|
||||||
|
description: "Maximum heading level to consider for chunking (1-6)"
|
||||||
|
required: false
|
||||||
|
default: 2
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A Processor for markdown files"
|
||||||
20
config/processors/globals/PDF_PROCESSOR/1.0.0.yaml
Normal file
20
config/processors/globals/PDF_PROCESSOR/1.0.0.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "PDF Processor"
|
||||||
|
file_types: "pdf"
|
||||||
|
description: "A Processor for PDF files"
|
||||||
|
configuration:
|
||||||
|
chunking_patterns:
|
||||||
|
name: "Chunking Patterns"
|
||||||
|
description: "A list of Patterns used to chunk files into logical pieces"
|
||||||
|
type: "chunking_patterns"
|
||||||
|
required: false
|
||||||
|
chunking_heading_level:
|
||||||
|
name: "Chunking Heading Level"
|
||||||
|
type: "integer"
|
||||||
|
description: "Maximum heading level to consider for chunking (1-6)"
|
||||||
|
required: false
|
||||||
|
default: 2
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A Processor for PDF files"
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
type: "PERSONAL_CONTACT_FORM"
|
||||||
|
version: "1.0.0"
|
||||||
|
name: "Personal Contact Form"
|
||||||
|
icon: "person"
|
||||||
|
fields:
|
||||||
|
name:
|
||||||
|
name: "Name"
|
||||||
|
description: "Your name"
|
||||||
|
type: "str"
|
||||||
|
required: true
|
||||||
|
email:
|
||||||
|
name: "Email"
|
||||||
|
type: "str"
|
||||||
|
description: "Your Name"
|
||||||
|
required: true
|
||||||
|
phone:
|
||||||
|
name: "Phone Number"
|
||||||
|
type: "str"
|
||||||
|
description: "Your Phone Number"
|
||||||
|
context: "Een kleine test om te zien of we context kunnen doorgeven en tonen"
|
||||||
|
required: true
|
||||||
|
address:
|
||||||
|
name: "Address"
|
||||||
|
type: "string"
|
||||||
|
description: "Your Address"
|
||||||
|
required: false
|
||||||
|
zip:
|
||||||
|
name: "Postal Code"
|
||||||
|
type: "string"
|
||||||
|
description: "Postal Code"
|
||||||
|
required: false
|
||||||
|
city:
|
||||||
|
name: "City"
|
||||||
|
type: "string"
|
||||||
|
description: "City"
|
||||||
|
required: false
|
||||||
|
country:
|
||||||
|
name: "Country"
|
||||||
|
type: "string"
|
||||||
|
description: "Country"
|
||||||
|
required: false
|
||||||
|
consent:
|
||||||
|
name: "Consent"
|
||||||
|
type: "boolean"
|
||||||
|
description: "Consent"
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
type: "PROFESSIONAL_CONTACT_FORM"
|
||||||
|
version: "1.0.0"
|
||||||
|
name: "Professional Contact Form"
|
||||||
|
icon: "account_circle"
|
||||||
|
fields:
|
||||||
|
name:
|
||||||
|
name: "Name"
|
||||||
|
description: "Your name"
|
||||||
|
type: "str"
|
||||||
|
required: true
|
||||||
|
email:
|
||||||
|
name: "Email"
|
||||||
|
type: "str"
|
||||||
|
description: "Your Name"
|
||||||
|
required: true
|
||||||
|
phone:
|
||||||
|
name: "Phone Number"
|
||||||
|
type: "str"
|
||||||
|
description: "Your Phone Number"
|
||||||
|
required: true
|
||||||
|
company:
|
||||||
|
name: "Company Name"
|
||||||
|
type: "str"
|
||||||
|
description: "Company Name"
|
||||||
|
required: true
|
||||||
|
job_title:
|
||||||
|
name: "Job Title"
|
||||||
|
type: "str"
|
||||||
|
description: "Job Title"
|
||||||
|
required: false
|
||||||
|
address:
|
||||||
|
name: "Address"
|
||||||
|
type: "str"
|
||||||
|
description: "Your Address"
|
||||||
|
required: false
|
||||||
|
zip:
|
||||||
|
name: "Postal Code"
|
||||||
|
type: "str"
|
||||||
|
description: "Postal Code"
|
||||||
|
required: false
|
||||||
|
city:
|
||||||
|
name: "City"
|
||||||
|
type: "str"
|
||||||
|
description: "City"
|
||||||
|
required: false
|
||||||
|
country:
|
||||||
|
name: "Country"
|
||||||
|
type: "str"
|
||||||
|
description: "Country"
|
||||||
|
required: false
|
||||||
|
consent:
|
||||||
|
name: "Consent"
|
||||||
|
type: "bool"
|
||||||
|
description: "Consent"
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
version: "1.2.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
|
||||||
|
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: "Updated for unified competencies and ko criteria"
|
||||||
|
description: "Assistant to create a new Vacancy based on Vacancy Text"
|
||||||
@@ -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"
|
||||||
@@ -5,12 +5,17 @@ partner: "traicie"
|
|||||||
chat: false
|
chat: false
|
||||||
configuration:
|
configuration:
|
||||||
name:
|
name:
|
||||||
name: "name"
|
name: "Name"
|
||||||
description: "The name the specialist is called upon."
|
description: "The name the specialist is called upon."
|
||||||
type: "str"
|
type: "str"
|
||||||
required: true
|
required: true
|
||||||
|
role_reference:
|
||||||
|
name: "Role Reference"
|
||||||
|
description: "A customer reference to the role"
|
||||||
|
type: "str"
|
||||||
|
required: false
|
||||||
competencies:
|
competencies:
|
||||||
name: "competencies"
|
name: "Competencies"
|
||||||
description: "An ordered list of competencies."
|
description: "An ordered list of competencies."
|
||||||
type: "ordered_list"
|
type: "ordered_list"
|
||||||
list_type: "competency_details"
|
list_type: "competency_details"
|
||||||
@@ -41,17 +46,17 @@ configuration:
|
|||||||
required: false
|
required: false
|
||||||
competency_details:
|
competency_details:
|
||||||
title:
|
title:
|
||||||
name: "title"
|
name: "Title"
|
||||||
description: "Competency Title"
|
description: "Competency Title"
|
||||||
type: "str"
|
type: "str"
|
||||||
required: true
|
required: true
|
||||||
description:
|
description:
|
||||||
name: "description"
|
name: "Description"
|
||||||
description: "Description (in context of the role) of the competency"
|
description: "Description (in context of the role) of the competency"
|
||||||
type: "text"
|
type: "text"
|
||||||
required: true
|
required: true
|
||||||
is_knockout:
|
is_knockout:
|
||||||
name: "Is Knockout"
|
name: "KO"
|
||||||
description: "Defines if the competency is a knock-out criterium"
|
description: "Defines if the competency is a knock-out criterium"
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
required: true
|
required: true
|
||||||
@@ -63,10 +68,32 @@ competency_details:
|
|||||||
required: true
|
required: true
|
||||||
default: true
|
default: true
|
||||||
arguments:
|
arguments:
|
||||||
vacancy_text:
|
region:
|
||||||
name: "vacancy_text"
|
name: "Region"
|
||||||
type: "text"
|
type: "str"
|
||||||
description: "The Vacancy Text"
|
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
|
required: true
|
||||||
results:
|
results:
|
||||||
competencies:
|
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"
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
version: "1.3.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_RECRUITER"
|
||||||
|
version: "1.0"
|
||||||
|
tasks:
|
||||||
|
- type: "TRAICIE_KO_CRITERIA_INTERVIEW_DEFINITION"
|
||||||
|
version: "1.0"
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2025-06-16"
|
||||||
|
changes: "Realising the actual interaction with the LLM"
|
||||||
|
description: "Assistant to create a new Vacancy based on Vacancy Text"
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
version: "1.3.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_RECRUITER_AGENT"
|
||||||
|
version: "1.0"
|
||||||
|
tasks:
|
||||||
|
- type: "TRAICIE_KO_CRITERIA_INTERVIEW_DEFINITION_TASK"
|
||||||
|
version: "1.0"
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2025-06-18"
|
||||||
|
changes: "Add make to the selection specialist"
|
||||||
|
description: "Assistant to create a new Vacancy based on Vacancy Text"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "KO Criteria Interview Definition"
|
||||||
|
task_description: >
|
||||||
|
In context of a vacancy in your company {tenant_name}, you are provided with a set of competencies. (both description
|
||||||
|
and title). The competencies are in between triple backquotes. You need to prepare for the interviews,
|
||||||
|
and are to provide for each of these ko criteria:
|
||||||
|
|
||||||
|
- A question to ask the recruitment candidate describing the context of the competency. Use your experience to not
|
||||||
|
just ask a closed question, but a question from which you can indirectly derive a positive or negative qualification of
|
||||||
|
the competency based on the answer of the candidate.
|
||||||
|
|
||||||
|
Apply the following tone of voice in both questions and answers: {tone_of_voice}
|
||||||
|
Apply the following language level in both questions and answers: {language_level}
|
||||||
|
Use {language} as language for both questions and answers.
|
||||||
|
|
||||||
|
```{competencies}```
|
||||||
|
|
||||||
|
{custom_description}
|
||||||
|
|
||||||
|
expected_output: >
|
||||||
|
For each of the ko criteria, you provide:
|
||||||
|
- the exact title in the original language
|
||||||
|
- the question
|
||||||
|
- a set of answers, with for each answer an indication if it is the correct answer, or a false response.
|
||||||
|
{custom_expected_output}
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2025-06-15"
|
||||||
|
description: "A Task to define interview Q&A from given KO Criteria"
|
||||||
|
changes: "Initial Version"
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "KO Criteria Interview Definition"
|
||||||
|
task_description: >
|
||||||
|
In context of a vacancy in your company {tenant_name}, you are provided with a set of knock-out criteria
|
||||||
|
(both description and title). The criteria are in between triple backquotes.You need to prepare for the interviews,
|
||||||
|
and are to provide for each of these ko criteria:
|
||||||
|
|
||||||
|
- A short question to ask the recruitment candidate describing the context of the ko criterium. Use your experience to
|
||||||
|
ask a question that enables us to verify compliancy to the criterium.
|
||||||
|
- A set of 2 short answers to that question, from the candidates perspective. One of the answers will result in a
|
||||||
|
positive evaluation of the criterium, the other one in a negative evaluation. Mark each of the answers as positive
|
||||||
|
or negative.
|
||||||
|
Describe the answers from the perspective of the candidate. Be sure to include all necessary aspects in you answers.
|
||||||
|
|
||||||
|
Apply the following tone of voice in both questions and answers: {tone_of_voice}
|
||||||
|
Use the following description to understand tone of voice:
|
||||||
|
|
||||||
|
{tone_of_voice_context}
|
||||||
|
|
||||||
|
Apply the following language level in both questions and answers: {language_level}
|
||||||
|
|
||||||
|
Use {language} as language for both questions and answers.
|
||||||
|
Use the following description to understand language_level:
|
||||||
|
|
||||||
|
{language_level_context}
|
||||||
|
|
||||||
|
```{ko_criteria}```
|
||||||
|
|
||||||
|
{custom_description}
|
||||||
|
|
||||||
|
expected_output: >
|
||||||
|
For each of the ko criteria, you provide:
|
||||||
|
- the exact title as specified in the original language
|
||||||
|
- the question in {language}
|
||||||
|
- a positive answer, resulting in a positive evaluation of the criterium. In {language}.
|
||||||
|
- a negative answer, resulting in a negative evaluation of the criterium. In {language}.
|
||||||
|
{custom_expected_output}
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2025-06-15"
|
||||||
|
description: "A Task to define interview Q&A from given KO Criteria"
|
||||||
|
changes: "Initial Version"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "KO Criteria Interview Definition"
|
||||||
|
task_description: >
|
||||||
|
In context of a vacancy in your company {tenant_name}, you are provided with a set of competencies
|
||||||
|
(both description and title). The competencies are in between triple backquotes. The competencies provided should be
|
||||||
|
handled as knock-out criteria.
|
||||||
|
For each of the knock-out criteria, you need to define
|
||||||
|
|
||||||
|
- A short (1 sentence), closed-ended question (Yes / No) to ask the recruitment candidate. Use your experience to ask a question that
|
||||||
|
enables us to verify compliancy to the criterium.
|
||||||
|
- A set of 2 short answers (1 small sentence each) to that question (positive answer / negative answer), from the
|
||||||
|
candidates perspective.
|
||||||
|
The positive answer will result in a positive evaluation of the criterium, the negative answer in a negative evaluation
|
||||||
|
of the criterium. Try to avoid just using Yes / No as positive and negative answers.
|
||||||
|
|
||||||
|
Apply the following tone of voice in both questions and answers: {tone_of_voice}, i.e. {tone_of_voice_context}
|
||||||
|
|
||||||
|
Apply the following language level in both questions and answers: {language_level}, i.e. {language_level_context}
|
||||||
|
|
||||||
|
Use {language} as language for both questions and answers.
|
||||||
|
|
||||||
|
```{ko_criteria}```
|
||||||
|
|
||||||
|
{custom_description}
|
||||||
|
|
||||||
|
expected_output: >
|
||||||
|
For each of the ko criteria, you provide:
|
||||||
|
- the exact title as specified in the original language
|
||||||
|
- the question in {language}
|
||||||
|
- a positive answer, resulting in a positive evaluation of the criterium. In {language}.
|
||||||
|
- a negative answer, resulting in a negative evaluation of the criterium. In {language}.
|
||||||
|
{custom_expected_output}
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2025-06-20"
|
||||||
|
description: "A Task to define interview Q&A from given KO Criteria"
|
||||||
|
changes: "Improvement to ensure closed-ended questions and short descriptions"
|
||||||
@@ -32,5 +32,10 @@ AGENT_TYPES = {
|
|||||||
"name": "Traicie HR BP Agent",
|
"name": "Traicie HR BP Agent",
|
||||||
"description": "An HR Business Partner Agent",
|
"description": "An HR Business Partner Agent",
|
||||||
"partner": "traicie"
|
"partner": "traicie"
|
||||||
}
|
},
|
||||||
|
"TRAICIE_RECRUITER_AGENT": {
|
||||||
|
"name": "Traicie Recruiter Agent",
|
||||||
|
"description": "An Senior Recruiter Agent",
|
||||||
|
"partner": "traicie"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,28 +2,10 @@
|
|||||||
CATALOG_TYPES = {
|
CATALOG_TYPES = {
|
||||||
"STANDARD_CATALOG": {
|
"STANDARD_CATALOG": {
|
||||||
"name": "Standard Catalog",
|
"name": "Standard Catalog",
|
||||||
"Description": "A Catalog with information in Evie's Library, to be considered as a whole",
|
"description": "A Catalog with information in Evie's Library, to be considered as a whole",
|
||||||
"configuration": {},
|
|
||||||
"document_version_configurations": []
|
|
||||||
},
|
},
|
||||||
"DOSSIER_CATALOG": {
|
"DOSSIER_CATALOG": {
|
||||||
"name": "Dossier Catalog",
|
"name": "Dossier Catalog",
|
||||||
"Description": "A Catalog with information in Evie's Library in which several Dossiers can be stored",
|
"description": "A Catalog with information in Evie's Library in which several Dossiers can be stored",
|
||||||
"configuration": {
|
|
||||||
"tagging_fields": {
|
|
||||||
"name": "Tagging Fields",
|
|
||||||
"type": "tagging_fields",
|
|
||||||
"description": """Define the metadata fields that will be used for tagging documents.
|
|
||||||
Each field must have:
|
|
||||||
- type: one of 'string', 'integer', 'float', 'date', 'enum'
|
|
||||||
- required: boolean indicating if the field is mandatory
|
|
||||||
- description: field description
|
|
||||||
- allowed_values: list of values (for enum type only)
|
|
||||||
- min_value/max_value: range limits (for numeric types only)""",
|
|
||||||
"required": True,
|
|
||||||
"default": {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"document_version_configurations": ["tagging_fields"]
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,168 +1,28 @@
|
|||||||
# Catalog Types
|
# Processor Types
|
||||||
PROCESSOR_TYPES = {
|
PROCESSOR_TYPES = {
|
||||||
"HTML_PROCESSOR": {
|
"HTML_PROCESSOR": {
|
||||||
"name": "HTML Processor",
|
"name": "HTML Processor",
|
||||||
|
"description": "A processor for HTML files",
|
||||||
"file_types": "html",
|
"file_types": "html",
|
||||||
"Description": "A processor for HTML files",
|
|
||||||
"configuration": {
|
|
||||||
"chunking_patterns": {
|
|
||||||
"name": "Chunking Patterns",
|
|
||||||
"description": "A list of Patterns used to chunk files into logical pieces",
|
|
||||||
"type": "chunking_patterns",
|
|
||||||
"required": False
|
|
||||||
},
|
|
||||||
"chunking_heading_level": {
|
|
||||||
"name": "Chunking Heading Level",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Maximum heading level to consider for chunking (1-6)",
|
|
||||||
"required": False,
|
|
||||||
"default": 2
|
|
||||||
},
|
|
||||||
"html_tags": {
|
|
||||||
"name": "HTML Tags",
|
|
||||||
"type": "string",
|
|
||||||
"description": "A comma-separated list of HTML tags",
|
|
||||||
"required": True,
|
|
||||||
"default": "p, h1, h2, h3, h4, h5, h6, li, table, thead, tbody, tr, td"
|
|
||||||
},
|
|
||||||
"html_end_tags": {
|
|
||||||
"name": "HTML End Tags",
|
|
||||||
"type": "string",
|
|
||||||
"description": "A comma-separated list of HTML end tags (where can the chunk end)",
|
|
||||||
"required": True,
|
|
||||||
"default": "p, li, table"
|
|
||||||
},
|
|
||||||
"html_included_elements": {
|
|
||||||
"name": "HTML Included Elements",
|
|
||||||
"type": "string",
|
|
||||||
"description": "A comma-separated list of elements to be included",
|
|
||||||
"required": True,
|
|
||||||
"default": "article, main"
|
|
||||||
},
|
|
||||||
"html_excluded_elements": {
|
|
||||||
"name": "HTML Excluded Elements",
|
|
||||||
"type": "string",
|
|
||||||
"description": "A comma-separated list of elements to be excluded",
|
|
||||||
"required": False,
|
|
||||||
"default": "header, footer, nav, script"
|
|
||||||
},
|
|
||||||
"html_excluded_classes": {
|
|
||||||
"name": "HTML Excluded Classes",
|
|
||||||
"type": "string",
|
|
||||||
"description": "A comma-separated list of classes to be excluded",
|
|
||||||
"required": False,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"PDF_PROCESSOR": {
|
"PDF_PROCESSOR": {
|
||||||
"name": "PDF Processor",
|
"name": "PDF Processor",
|
||||||
|
"description": "A Processor for PDF files",
|
||||||
"file_types": "pdf",
|
"file_types": "pdf",
|
||||||
"Description": "A Processor for PDF files",
|
|
||||||
"configuration": {
|
|
||||||
"chunking_patterns": {
|
|
||||||
"name": "Chunking Patterns",
|
|
||||||
"description": "A list of Patterns used to chunk files into logical pieces",
|
|
||||||
"type": "chunking_patterns",
|
|
||||||
"required": False
|
|
||||||
},
|
|
||||||
"chunking_heading_level": {
|
|
||||||
"name": "Chunking Heading Level",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Maximum heading level to consider for chunking (1-6)",
|
|
||||||
"required": False,
|
|
||||||
"default": 2
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"AUDIO_PROCESSOR": {
|
"AUDIO_PROCESSOR": {
|
||||||
"name": "AUDIO Processor",
|
"name": "AUDIO Processor",
|
||||||
|
"description": "A Processor for audio files",
|
||||||
"file_types": "mp3, mp4, ogg",
|
"file_types": "mp3, mp4, ogg",
|
||||||
"Description": "A Processor for audio files",
|
|
||||||
"configuration": {}
|
|
||||||
},
|
},
|
||||||
"MARKDOWN_PROCESSOR": {
|
"MARKDOWN_PROCESSOR": {
|
||||||
"name": "Markdown Processor",
|
"name": "Markdown Processor",
|
||||||
|
"description": "A Processor for markdown files",
|
||||||
"file_types": "md",
|
"file_types": "md",
|
||||||
"Description": "A Processor for markdown files",
|
|
||||||
"configuration": {
|
|
||||||
"chunking_patterns": {
|
|
||||||
"name": "Chunking Patterns",
|
|
||||||
"description": "A list of Patterns used to chunk files into logical pieces",
|
|
||||||
"type": "chunking_patterns",
|
|
||||||
"required": False
|
|
||||||
},
|
|
||||||
"chunking_heading_level": {
|
|
||||||
"name": "Chunking Heading Level",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Maximum heading level to consider for chunking (1-6)",
|
|
||||||
"required": False,
|
|
||||||
"default": 2
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"DOCX_PROCESSOR": {
|
"DOCX_PROCESSOR": {
|
||||||
"name": "DOCX Processor",
|
"name": "DOCX Processor",
|
||||||
|
"description": "A processor for DOCX files",
|
||||||
"file_types": "docx",
|
"file_types": "docx",
|
||||||
"Description": "A processor for DOCX files",
|
|
||||||
"configuration": {
|
|
||||||
"chunking_patterns": {
|
|
||||||
"name": "Chunking Patterns",
|
|
||||||
"description": "A list of Patterns used to chunk files into logical pieces",
|
|
||||||
"type": "chunking_patterns",
|
|
||||||
"required": False
|
|
||||||
},
|
|
||||||
"chunking_heading_level": {
|
|
||||||
"name": "Chunking Heading Level",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Maximum heading level to consider for chunking (1-6)",
|
|
||||||
"required": False,
|
|
||||||
"default": 2
|
|
||||||
},
|
|
||||||
"extract_comments": {
|
|
||||||
"name": "Extract Comments",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether to include document comments in the markdown",
|
|
||||||
"required": False,
|
|
||||||
"default": False
|
|
||||||
},
|
|
||||||
"extract_headers_footers": {
|
|
||||||
"name": "Extract Headers/Footers",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether to include headers and footers in the markdown",
|
|
||||||
"required": False,
|
|
||||||
"default": False
|
|
||||||
},
|
|
||||||
"preserve_formatting": {
|
|
||||||
"name": "Preserve Formatting",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether to preserve bold, italic, and other text formatting",
|
|
||||||
"required": False,
|
|
||||||
"default": True
|
|
||||||
},
|
|
||||||
"list_style": {
|
|
||||||
"name": "List Style",
|
|
||||||
"type": "enum",
|
|
||||||
"description": "How to format lists in markdown",
|
|
||||||
"required": False,
|
|
||||||
"default": "dash",
|
|
||||||
"allowed_values": ["dash", "asterisk", "plus"]
|
|
||||||
},
|
|
||||||
"image_handling": {
|
|
||||||
"name": "Image Handling",
|
|
||||||
"type": "enum",
|
|
||||||
"description": "How to handle embedded images",
|
|
||||||
"required": False,
|
|
||||||
"default": "skip",
|
|
||||||
"allowed_values": ["skip", "extract", "placeholder"]
|
|
||||||
},
|
|
||||||
"table_alignment": {
|
|
||||||
"name": "Table Alignment",
|
|
||||||
"type": "enum",
|
|
||||||
"description": "How to align table contents",
|
|
||||||
"required": False,
|
|
||||||
"default": "left",
|
|
||||||
"allowed_values": ["left", "center", "preserve"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
config/type_defs/specialist_form_types.py
Normal file
11
config/type_defs/specialist_form_types.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Specialist Form Types
|
||||||
|
SPECIALIST_FORM_TYPES = {
|
||||||
|
"PERSONAL_CONTACT_FORM": {
|
||||||
|
"name": "Personal Contact Form",
|
||||||
|
"description": "A form for entering your personal contact details",
|
||||||
|
},
|
||||||
|
"PROFESSIONAL_CONTACT_FORM": {
|
||||||
|
"name": "Professional Contact Form",
|
||||||
|
"description": "A form for entering your professional contact details",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -16,5 +16,9 @@ SPECIALIST_TYPES = {
|
|||||||
"name": "Traicie Role Definition Specialist",
|
"name": "Traicie Role Definition Specialist",
|
||||||
"description": "Assistant Defining Competencies and KO Criteria",
|
"description": "Assistant Defining Competencies and KO Criteria",
|
||||||
"partner": "traicie"
|
"partner": "traicie"
|
||||||
|
},
|
||||||
|
"TRAICIE_SELECTION_SPECIALIST": {
|
||||||
|
"name": "Traicie Selection Specialist",
|
||||||
|
"description": "Recruitment Selection Assistant",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,5 +41,10 @@ TASK_TYPES = {
|
|||||||
"name": "Traicie Get KO Criteria",
|
"name": "Traicie Get KO Criteria",
|
||||||
"description": "A Task to get KO Criteria from a Vacancy Text",
|
"description": "A Task to get KO Criteria from a Vacancy Text",
|
||||||
"partner": "traicie"
|
"partner": "traicie"
|
||||||
|
},
|
||||||
|
"TRAICIE_KO_CRITERIA_INTERVIEW_DEFINITION_TASK": {
|
||||||
|
"name": "Traicie KO Criteria Interview Definition",
|
||||||
|
"description": "A Task to define KO Criteria questions to be used during the interview",
|
||||||
|
"partner": "traicie"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
416
content/changelog/1.0/1.0.0.md
Normal file
416
content/changelog/1.0/1.0.0.md
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
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.7-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Basic Base Specialist additions for handling phases and transferring data between state and output
|
||||||
|
- Introduction of URL and QR-code for MagicLink
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Logging improvement & simplification (remove Graylog)
|
||||||
|
- Traicie Selection Specialist v1.3 - full roundtrip & full process
|
||||||
|
|
||||||
|
## [2.3.6-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Full Chat Client functionality, including Forms, ESS, theming
|
||||||
|
- First Demo version of Traicie Selection Specialist
|
||||||
|
|
||||||
|
## [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
|
||||||
|
- Introduction of ordered_list dynamic field type (using tabulator)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Bring configuration of PROCESSOR_TYPES & CATALOG_TYPES to new config standard
|
||||||
|
- Specialist Editor: move general information in tab
|
||||||
|
- Role Definition Specialist creates Selection Specialist from generated competencies
|
||||||
|
- Improvements to Selection Specialist (Agent definition to be started)
|
||||||
|
|
||||||
|
## [2.3.0-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Introduction of Push Gateway for Prometheus
|
||||||
|
- Introduction of Partner Models
|
||||||
|
- Introduction of Tenant and Partner codes for more security
|
||||||
|
- Introduction of 'Management Partner' type and additional 'Partner Admin'-role
|
||||||
|
- Introduction of a technical services layer
|
||||||
|
- Introduction of partner-specific configurations
|
||||||
|
- Introduction of additional test environment
|
||||||
|
- Introduction of strict no-overage usage
|
||||||
|
- Introduction of LicensePeriod, Payments & Invoices
|
||||||
|
- Introduction of Processed File Viewer
|
||||||
|
- Introduction of Traicie Role Definition Specialist
|
||||||
|
- Allow invocation of non-interactive specialists in administrative interface (eveai_app)
|
||||||
|
- Introduction of advanced JSON editor
|
||||||
|
- 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
|
||||||
|
- Add additional environment capabilities in docker
|
||||||
|
- PDF Processor now uses Mistral OCR
|
||||||
|
- Allow additional chunking mechanisms for very long chunks (in case of very large documents)
|
||||||
|
- Allow for TrackedMistralAIEmbedding batching to allow for processing long documents
|
||||||
|
- RAG & SPIN Specialist improvements
|
||||||
|
- Move mail messaging from standard SMTP to Scaleway TEM mails
|
||||||
|
- Improve mail layouts
|
||||||
|
- Add functionality to add a default dictionary for dynamic forms
|
||||||
|
- AI model choices defined by Ask Eve AI iso Tenant (replaces ModelVariables completely)
|
||||||
|
- Improve HTML Processing
|
||||||
|
- Pagination improvements
|
||||||
|
- Update Material Kit Pro to latest version
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Repopack implementation ==> Using PyCharm's new AI capabilities instead
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Synchronous vs Asynchronous behaviour in crewAI type specialists
|
||||||
|
- Nasty dynamic boolean fields bug corrected
|
||||||
|
- Several smaller bugfixes
|
||||||
|
- Tasks & Tools editors finished
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- In case of vulnerabilities.
|
||||||
|
|
||||||
|
## [2.2.0-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Mistral AI as main provider for embeddings, chains and specialists
|
||||||
|
- Usage measuring for specialists
|
||||||
|
- RAG from chain to specialist technology
|
||||||
|
- Dossier catalog management possibilities added to eveai_app
|
||||||
|
- Asset definition (Paused - other priorities)
|
||||||
|
- Prometheus and Grafana
|
||||||
|
- Add prometheus monitoring to business events
|
||||||
|
- Asynchronous execution of specialists
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Moved choice for AI providers / models to specialists and prompts
|
||||||
|
- Improve RAG to not repeat historic answers
|
||||||
|
- Fixed embedding model, no more choices allowed
|
||||||
|
- clean url (of tracking parameters) before adding it to a catalog
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- For soon-to-be removed features.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Add Multiple URLs removed from menu
|
||||||
|
- Old Specialist items removed from interaction menu
|
||||||
|
-
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
|
## [2.1.0-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Zapier Refresh Document
|
||||||
|
- SPIN Specialist definition - from start to finish
|
||||||
|
- Introduction of startup scripts in eveai_app
|
||||||
|
- Caching for all configurations added
|
||||||
|
- Caching for processed specialist configurations
|
||||||
|
- Caching for specialist history
|
||||||
|
- Augmented Specialist Editor, including Specialist graphic presentation
|
||||||
|
- Introduction of specialist_execution_api, introducting SSE
|
||||||
|
- Introduction of crewai framework for specialist implementation
|
||||||
|
- Test app for testing specialists - also serves as a sample client application for SSE
|
||||||
|
-
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improvement of startup of applications using gevent, and better handling and scaling of multiple connections
|
||||||
|
- STANDARD_RAG Specialist improvement
|
||||||
|
-
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- eveai_chat - using sockets - will be replaced with new specialist_execution_api and SSE
|
||||||
|
|
||||||
|
## [2.0.1-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Zapîer Integration (partial - only adding files).
|
||||||
|
- Addition of general chunking parameters (chunking_heading_level and chunking_patterns)
|
||||||
|
- Addition of DocX and markdown Processor Types
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- For changes in existing functionality.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- For soon-to-be removed features.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- For now removed features.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Ensure the RAG Specialist is using the detailed_question
|
||||||
|
- Wordpress Chat Plugin: languages dropdown filled again
|
||||||
|
- OpenAI update - proxies no longer supported
|
||||||
|
- Build & Release script for Wordpress Plugins (including end user download folder)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- In case of vulnerabilities.
|
||||||
|
|
||||||
|
## [2.0.0-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Introduction of dynamic Retrievers & Specialists
|
||||||
|
- Introduction of dynamic Processors
|
||||||
|
- Introduction of caching system
|
||||||
|
- Introduction of a better template manager
|
||||||
|
- Modernisation of external API/Socket authentication using projects
|
||||||
|
- Creation of new eveai_chat WordPress plugin to support specialists
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Update of eveai_sync WordPress plugin
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Security improvements to Docker images
|
||||||
|
|
||||||
|
## [1.0.14-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New release script added to tag images with release number
|
||||||
|
- Allow the addition of multiple types of Catalogs
|
||||||
|
- Generic functionality to enable dynamic fields
|
||||||
|
- Addition of Retrievers to allow for smart collection of information in Catalogs
|
||||||
|
- Add dynamic fields to Catalog / Retriever / DocumentVersion
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Processing parameters defined at Catalog level iso Tenant level
|
||||||
|
- Reroute 'blank' paths to 'admin'
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- For soon-to-be removed features.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- For now removed features.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- In case of vulnerabilities.
|
||||||
|
|
||||||
|
## [1.0.13-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Finished Catalog introduction
|
||||||
|
- Reinitialization of WordPress site for syncing
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Modification of WordPress Sync Component
|
||||||
|
- Cleanup of attributes in Tenant
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Overall bugfixes as result from the Catalog introduction
|
||||||
|
|
||||||
|
## [1.0.12-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added Catalog functionality
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- For changes in existing functionality.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- For soon-to-be removed features.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- For now removed features.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- In case of vulnerabilities.
|
||||||
|
|
||||||
|
## [1.0.11-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- License Usage Calculation realised
|
||||||
|
- View License Usages
|
||||||
|
- Celery Beat container added
|
||||||
|
- First schedule in Celery Beat for calculating usage (hourly)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- repopack can now split for different components
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Various fixes as consequence of changing file_location / file_name ==> bucket_name / object_name
|
||||||
|
- Celery Routing / Queuing updated
|
||||||
|
|
||||||
|
## [1.0.10-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- BusinessEventLog monitoring using Langchain native code
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Allow longer audio files (or video) to be uploaded and processed
|
||||||
|
- Storage and Embedding usage now expressed in MiB iso tokens (more logical)
|
||||||
|
- Views for License / LicenseTier
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Portkey removed for monitoring usage
|
||||||
|
|
||||||
|
## [1.0.9-alfa] - 2024/10/01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Business Event tracing (eveai_workers & eveai_chat_workers)
|
||||||
|
- Flower Container added for monitoring
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Healthcheck improvements
|
||||||
|
- model_utils turned into a class with lazy loading
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- For soon-to-be removed features.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- For now removed features.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
|
## [1.0.8-alfa] - 2024-09-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Tenant type defined to allow for active, inactive, demo ... tenants
|
||||||
|
- Search and filtering functionality on Tenants
|
||||||
|
- Implementation of health checks (1st version)
|
||||||
|
- Provision for Prometheus monitoring (no implementation yet)
|
||||||
|
- Refine audio_processor and srt_processor to reduce duplicate code and support larger files
|
||||||
|
- Introduction of repopack to reason in LLMs about the code
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Refine audio_processor and srt_processor to reduce duplicate code and support larger files
|
||||||
|
|
||||||
|
## [1.0.7-alfa] - 2024-09-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Full Document API allowing for creation, updating and invalidation of documents.
|
||||||
|
- Metadata fields (JSON) added to DocumentVersion, allowing end-users to add structured information
|
||||||
|
- Wordpress plugin eveai_sync to synchronize Wordpress content with EveAI
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Maximal deduplication of code between views and api in document_utils.py
|
||||||
|
|
||||||
|
## [1.0.6-alfa] - 2024-09-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Problems with tenant scheme migrations - may have to be revisited
|
||||||
|
- Correction of default language settings when uploading docs or URLs
|
||||||
|
- Addition of a CHANGELOG.md file
|
||||||
|
|
||||||
|
## [1.0.5-alfa] - 2024-09-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Allow chatwidget to connect to multiple servers (e.g. development and production)
|
||||||
|
- Start implementation of API
|
||||||
|
- Add API-key functionality to tenants
|
||||||
|
- Deduplication of API and Document view code
|
||||||
|
- Allow URL addition to accept all types of files, not just HTML
|
||||||
|
- Allow new file types upload: srt, mp3, ogg, mp4
|
||||||
|
- Improve processing of different file types using Processor classes
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Removed direct upload of Youtube URLs, due to continuous changes in Youtube website
|
||||||
|
|
||||||
|
## [1.0.4-alfa] - 2024-08-27
|
||||||
|
Skipped
|
||||||
|
|
||||||
|
## [1.0.3-alfa] - 2024-08-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Refinement of HTML processing - allow for excluded classes and elements.
|
||||||
|
- Allow for multiple instances of Evie on 1 website (pure + Wordpress plugin)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- PDF Processing extracted in new PDF Processor class.
|
||||||
|
- Allow for longer and more complex PDFs to be uploaded.
|
||||||
|
|
||||||
|
## [1.0.2-alfa] - 2024-08-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Bugfix for ResetPasswordForm in config.py
|
||||||
|
|
||||||
|
## [1.0.1-alfa] - 2024-08-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Full Document Version Overview
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improvements to user creation and registration, renewal of passwords, ...
|
||||||
|
|
||||||
|
## [1.0.0-alfa] - 2024-08-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release of the project.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- None
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- None
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/username/repo/compare/v1.0.0...HEAD
|
||||||
|
[1.0.0]: https://github.com/username/repo/releases/tag/v1.0.0
|
||||||
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:
|
depends_on:
|
||||||
- eveai_app
|
- eveai_app
|
||||||
- eveai_api
|
- eveai_api
|
||||||
|
- eveai_chat_client
|
||||||
networks:
|
networks:
|
||||||
- eveai-network
|
- eveai-network
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ../eveai_app:/app/eveai_app
|
- ../eveai_app:/app/eveai_app
|
||||||
- ../common:/app/common
|
- ../common:/app/common
|
||||||
|
- ../content:/app/content
|
||||||
- ../config:/app/config
|
- ../config:/app/config
|
||||||
- ../migrations:/app/migrations
|
- ../migrations:/app/migrations
|
||||||
- ../scripts:/app/scripts
|
- ../scripts:/app/scripts
|
||||||
@@ -176,6 +178,44 @@ services:
|
|||||||
# networks:
|
# networks:
|
||||||
# - eveai-network
|
# - 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:
|
eveai_chat_workers:
|
||||||
image: josakola/eveai_chat_workers:latest
|
image: josakola/eveai_chat_workers:latest
|
||||||
build:
|
build:
|
||||||
@@ -440,4 +480,3 @@ volumes:
|
|||||||
#secrets:
|
#secrets:
|
||||||
# db-password:
|
# db-password:
|
||||||
# file: ./db/password.txt
|
# file: ./db/password.txt
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- eveai_app
|
- eveai_app
|
||||||
- eveai_api
|
- eveai_api
|
||||||
|
- eveai_chat_client
|
||||||
networks:
|
networks:
|
||||||
- eveai-network
|
- eveai-network
|
||||||
restart: "no"
|
restart: "no"
|
||||||
@@ -106,6 +107,33 @@ services:
|
|||||||
- eveai-network
|
- eveai-network
|
||||||
restart: "no"
|
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:
|
eveai_chat_workers:
|
||||||
image: josakola/eveai_chat_workers:${EVEAI_VERSION:-latest}
|
image: josakola/eveai_chat_workers:${EVEAI_VERSION:-latest}
|
||||||
expose:
|
expose:
|
||||||
|
|||||||
@@ -115,15 +115,41 @@ echo "Set COMPOSE_FILE to $COMPOSE_FILE"
|
|||||||
echo "Set EVEAI_VERSION to $VERSION"
|
echo "Set EVEAI_VERSION to $VERSION"
|
||||||
echo "Set DOCKER_ACCOUNT to $DOCKER_ACCOUNT"
|
echo "Set DOCKER_ACCOUNT to $DOCKER_ACCOUNT"
|
||||||
|
|
||||||
# Define aliases for common Docker commands
|
docker-compose() {
|
||||||
alias docker-compose="docker compose -f $COMPOSE_FILE"
|
docker compose -f $COMPOSE_FILE "$@"
|
||||||
alias dc="docker compose -f $COMPOSE_FILE"
|
}
|
||||||
alias dcup="docker compose -f $COMPOSE_FILE up -d --remove-orphans"
|
|
||||||
alias dcdown="docker compose -f $COMPOSE_FILE down"
|
dc() {
|
||||||
alias dcps="docker compose -f $COMPOSE_FILE ps"
|
docker compose -f $COMPOSE_FILE "$@"
|
||||||
alias dclogs="docker compose -f $COMPOSE_FILE logs"
|
}
|
||||||
alias dcpull="docker compose -f $COMPOSE_FILE pull"
|
|
||||||
alias dcrefresh="docker compose -f $COMPOSE_FILE pull && docker compose -f $COMPOSE_FILE up -d --remove-orphans"
|
dcup() {
|
||||||
|
docker compose -f $COMPOSE_FILE up -d --remove-orphans "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
dcdown() {
|
||||||
|
docker compose -f $COMPOSE_FILE down "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
dcps() {
|
||||||
|
docker compose -f $COMPOSE_FILE ps "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
dclogs() {
|
||||||
|
docker compose -f $COMPOSE_FILE logs "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
dcpull() {
|
||||||
|
docker compose -f $COMPOSE_FILE pull "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
dcrefresh() {
|
||||||
|
docker compose -f $COMPOSE_FILE pull && docker compose -f $COMPOSE_FILE up -d --remove-orphans "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Exporteer de functies zodat ze beschikbaar zijn in andere scripts
|
||||||
|
export -f docker-compose dc dcup dcdown dcps dclogs dcpull dcrefresh
|
||||||
|
|
||||||
|
|
||||||
echo "Docker environment switched to $ENVIRONMENT with version $VERSION"
|
echo "Docker environment switched to $ENVIRONMENT with version $VERSION"
|
||||||
echo "You can now use 'docker-compose', 'dc', 'dcup', 'dcdown', 'dcps', 'dclogs', 'dcpull' or 'dcrefresh' commands"
|
echo "You can now use 'docker-compose', 'dc', 'dcup', 'dcdown', 'dcps', 'dclogs', 'dcpull' or 'dcrefresh' commands"
|
||||||
@@ -56,6 +56,7 @@ COPY config /app/config
|
|||||||
COPY migrations /app/migrations
|
COPY migrations /app/migrations
|
||||||
COPY scripts /app/scripts
|
COPY scripts /app/scripts
|
||||||
COPY patched_packages /app/patched_packages
|
COPY patched_packages /app/patched_packages
|
||||||
|
COPY content /app/content
|
||||||
|
|
||||||
# Set permissions for entrypoint script
|
# Set permissions for entrypoint script
|
||||||
RUN chmod 777 /app/scripts/entrypoint.sh
|
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"]
|
||||||
9
docker/rebuild_chat_client.sh
Executable file
9
docker/rebuild_chat_client.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
source ./docker_env_switch.sh dev
|
||||||
|
source .env
|
||||||
|
|
||||||
|
dcdown eveai_chat_client nginx
|
||||||
|
./update_chat_client_statics.sh
|
||||||
|
./build_and_push_eveai.sh -b nginx
|
||||||
|
dcup eveai_chat_client nginx
|
||||||
49
docker/update_chat_client_statics.sh
Executable file
49
docker/update_chat_client_statics.sh
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to copy eveai_chat_client/static files to nginx/static
|
||||||
|
# without overwriting existing files
|
||||||
|
|
||||||
|
SRC_DIR="../eveai_chat_client/static/assets"
|
||||||
|
DEST_DIR="../nginx/static/assets"
|
||||||
|
|
||||||
|
# Check if source directory exists
|
||||||
|
if [ ! -d "$SRC_DIR" ]; then
|
||||||
|
echo "Error: Source directory $SRC_DIR does not exist!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create destination directory if it doesn't exist
|
||||||
|
if [ ! -d "$DEST_DIR" ]; then
|
||||||
|
echo "Destination directory $DEST_DIR does not exist. Creating it..."
|
||||||
|
mkdir -p "$DEST_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to recursively copy files without overwriting
|
||||||
|
copy_without_overwrite() {
|
||||||
|
local src=$1
|
||||||
|
local dest=$2
|
||||||
|
|
||||||
|
# Loop through all items in source directory
|
||||||
|
for item in "$src"/*; do
|
||||||
|
# Get the filename from the path
|
||||||
|
base_name=$(basename "$item")
|
||||||
|
|
||||||
|
# If it's a directory, create it in the destination and recurse
|
||||||
|
if [ -d "$item" ]; then
|
||||||
|
if [ ! -d "$dest/$base_name" ]; then
|
||||||
|
echo "Creating directory: $dest/$base_name"
|
||||||
|
mkdir -p "$dest/$base_name"
|
||||||
|
fi
|
||||||
|
copy_without_overwrite "$item" "$dest/$base_name"
|
||||||
|
else
|
||||||
|
# If it's a file and doesn't exist in the destination, copy it
|
||||||
|
cp "$item" "$dest/$base_name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start the copy process
|
||||||
|
echo "Starting to copy files from $SRC_DIR to $DEST_DIR..."
|
||||||
|
copy_without_overwrite "$SRC_DIR" "$DEST_DIR"
|
||||||
|
|
||||||
|
echo "Copy completed!"
|
||||||
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.
|
||||||
@@ -12,7 +12,7 @@ import logging.config
|
|||||||
from common.models.user import TenantDomain
|
from common.models.user import TenantDomain
|
||||||
from common.utils.cors_utils import get_allowed_origins
|
from common.utils.cors_utils import get_allowed_origins
|
||||||
from common.utils.database import Database
|
from common.utils.database import Database
|
||||||
from config.logging_config import LOGGING
|
from config.logging_config import configure_logging
|
||||||
from .api.document_api import document_ns
|
from .api.document_api import document_ns
|
||||||
from .api.auth import auth_ns
|
from .api.auth import auth_ns
|
||||||
from .api.specialist_execution_api import specialist_execution_ns
|
from .api.specialist_execution_api import specialist_execution_ns
|
||||||
@@ -40,7 +40,7 @@ def create_app(config_file=None):
|
|||||||
app.celery = make_celery(app.name, app.config)
|
app.celery = make_celery(app.name, app.config)
|
||||||
init_celery(app.celery, app)
|
init_celery(app.celery, app)
|
||||||
|
|
||||||
logging.config.dictConfig(LOGGING)
|
configure_logging()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
logger.info("eveai_api starting up")
|
logger.info("eveai_api starting up")
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
|||||||
import logging.config
|
import logging.config
|
||||||
|
|
||||||
from common.extensions import (db, migrate, bootstrap, security, login_manager, cors, csrf, session,
|
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
|
from common.models.user import User, Role, Tenant, TenantDomain
|
||||||
import common.models.interaction
|
import common.models.interaction
|
||||||
import common.models.entitlements
|
import common.models.entitlements
|
||||||
import common.models.document
|
import common.models.document
|
||||||
from common.utils.startup_eveai import perform_startup_actions
|
from common.utils.startup_eveai import perform_startup_actions
|
||||||
from config.logging_config import LOGGING
|
from config.logging_config import configure_logging
|
||||||
from common.utils.security import set_tenant_session_data
|
from common.utils.security import set_tenant_session_data
|
||||||
from .errors import register_error_handlers
|
from common.utils.errors import register_error_handlers
|
||||||
from common.utils.celery_utils import make_celery, init_celery
|
from common.utils.celery_utils import make_celery, init_celery
|
||||||
from common.utils.template_filters import register_filters
|
from common.utils.template_filters import register_filters
|
||||||
from config.config import get_config
|
from config.config import get_config
|
||||||
@@ -47,8 +47,16 @@ def create_app(config_file=None):
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
logging.config.dictConfig(LOGGING)
|
# Configureer logging op basis van de omgeving (K8s of traditioneel)
|
||||||
logger = logging.getLogger(__name__)
|
try:
|
||||||
|
configure_logging()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
# Test dat logging werkt
|
||||||
|
logger.debug("Logging test in eveai_app")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Critical Error Initialising Error: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
logger.info("eveai_app starting up")
|
logger.info("eveai_app starting up")
|
||||||
|
|
||||||
@@ -92,6 +100,45 @@ def create_app(config_file=None):
|
|||||||
# app.logger.debug(f"Before request - Session data: {session}")
|
# app.logger.debug(f"Before request - Session data: {session}")
|
||||||
# app.logger.debug(f"Before request - Request headers: {request.headers}")
|
# app.logger.debug(f"Before request - Request headers: {request.headers}")
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def before_request():
|
||||||
|
from flask import session, request
|
||||||
|
from flask_login import current_user
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
app.logger.debug(f"Before request - URL: {request.url}")
|
||||||
|
app.logger.debug(f"Before request - Session permanent: {session.permanent}")
|
||||||
|
|
||||||
|
# Log session expiry tijd als deze bestaat
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
# Controleer of sessie permanent is (nodig voor PERMANENT_SESSION_LIFETIME)
|
||||||
|
if not session.permanent:
|
||||||
|
session.permanent = True
|
||||||
|
app.logger.debug("Session marked as permanent (enables 60min timeout)")
|
||||||
|
|
||||||
|
# Log wanneer sessie zou verlopen
|
||||||
|
if '_permanent' in session:
|
||||||
|
expires_at = datetime.datetime.now() + app.permanent_session_lifetime
|
||||||
|
app.logger.debug(f"Session will expire at: {expires_at} (60 min from now)")
|
||||||
|
|
||||||
|
@app.route('/debug/session')
|
||||||
|
def debug_session():
|
||||||
|
from flask import session
|
||||||
|
from flask_security import current_user
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
info = {
|
||||||
|
'session_permanent': session.permanent,
|
||||||
|
'session_lifetime_minutes': app.permanent_session_lifetime.total_seconds() / 60,
|
||||||
|
'session_refresh_enabled': app.config.get('SESSION_REFRESH_EACH_REQUEST'),
|
||||||
|
'current_time': datetime.datetime.now().isoformat(),
|
||||||
|
'session_data_keys': list(session.keys())
|
||||||
|
}
|
||||||
|
return jsonify(info)
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Not authenticated'})
|
||||||
|
|
||||||
# Register template filters
|
# Register template filters
|
||||||
register_filters(app)
|
register_filters(app)
|
||||||
|
|
||||||
@@ -124,6 +171,7 @@ def register_extensions(app):
|
|||||||
minio_client.init_app(app)
|
minio_client.init_app(app)
|
||||||
cache_manager.init_app(app)
|
cache_manager.init_app(app)
|
||||||
metrics.init_app(app)
|
metrics.init_app(app)
|
||||||
|
content_manager.init_app(app)
|
||||||
|
|
||||||
|
|
||||||
def register_blueprints(app):
|
def register_blueprints(app):
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
{% include 'scripts.html' %}
|
{% include 'scripts.html' %}
|
||||||
|
{% include 'ordered_list_configs.html' %}
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
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_title %}Document Versions{% endblock %}
|
||||||
{% block content_description %}View Versions for {{ document }}{% 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 %}
|
{% block content %}
|
||||||
<div class="container">
|
<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") }}
|
{{ 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">
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary">Edit Document Version</button>
|
<div>
|
||||||
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger">View Processed Document</button>
|
<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="process_document_version" class="btn btn-danger">Process 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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="form-group mt-3 d-flex justify-content-between">
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
|
<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>
|
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,17 +19,17 @@
|
|||||||
<div class="nav-wrapper position-relative end-0">
|
<div class="nav-wrapper position-relative end-0">
|
||||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<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
|
Storage
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<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
|
Embedding
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<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
|
Interaction
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -19,17 +19,17 @@
|
|||||||
<div class="nav-wrapper position-relative end-0">
|
<div class="nav-wrapper position-relative end-0">
|
||||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<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
|
Storage
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<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
|
Embedding
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<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
|
Interaction
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -107,17 +107,17 @@
|
|||||||
<!-- Nav Tabs -->
|
<!-- Nav Tabs -->
|
||||||
<ul class="nav nav-tabs" id="periodTabs" role="tablist">
|
<ul class="nav nav-tabs" id="periodTabs" role="tablist">
|
||||||
<li class="nav-item">
|
<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
|
Status & Timeline
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<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
|
Usage
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<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
|
Financial
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% from "macros.html" import render_field, render_included_field %}
|
{% from "macros.html" import render_field, render_included_field %}
|
||||||
|
|
||||||
@@ -19,17 +20,17 @@
|
|||||||
<div class="nav-wrapper position-relative end-0">
|
<div class="nav-wrapper position-relative end-0">
|
||||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<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
|
Storage
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<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
|
Embedding
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<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
|
Interaction
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -68,4 +69,4 @@
|
|||||||
|
|
||||||
{% block content_footer %}
|
{% block content_footer %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
364
eveai_app/templates/eveai_ordered_list_editor.html
Normal file
364
eveai_app/templates/eveai_ordered_list_editor.html
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
<script type="module">
|
||||||
|
window.EveAI = window.EveAI || {};
|
||||||
|
window.EveAI.OrderedListEditors = {
|
||||||
|
instances: {},
|
||||||
|
initialize: function(containerId, data, listType, options = {}) {
|
||||||
|
console.log('Initializing OrderedListEditor for', containerId, 'with data', data, 'and listType', listType);
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (!container || typeof container !== 'object' || !('classList' in container)) {
|
||||||
|
console.error(`Container with ID ${containerId} not found or not a valid element:`, container);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.instances[containerId]) return this.instances[containerId];
|
||||||
|
|
||||||
|
if (typeof window.Tabulator !== 'function') {
|
||||||
|
console.error('Tabulator not loaded (window.Tabulator missing).');
|
||||||
|
container.innerHTML = `<div class="alert alert-danger p-3">
|
||||||
|
<strong>Error:</strong> Tabulator not loaded
|
||||||
|
</div>`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the list type configuration
|
||||||
|
const listTypeConfig = this._getListTypeConfig(listType);
|
||||||
|
if (!listTypeConfig) {
|
||||||
|
console.error(`List type configuration for ${listType} not found.`);
|
||||||
|
container.innerHTML = `<div class="alert alert-danger p-3">
|
||||||
|
<strong>Error:</strong> List type configuration for ${listType} not found
|
||||||
|
</div>`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create column definitions from list type
|
||||||
|
const columns = this._createColumnsFromListType(listTypeConfig);
|
||||||
|
|
||||||
|
// Debug log for data and columns
|
||||||
|
console.log('Data for Tabulator:', data);
|
||||||
|
console.log('Columns for Tabulator:', columns);
|
||||||
|
|
||||||
|
// Debug log for column titles
|
||||||
|
console.log('Column titles:', columns.map(col => col.title || ''));
|
||||||
|
|
||||||
|
// Initialize Tabulator
|
||||||
|
try {
|
||||||
|
console.log('Creating Tabulator for', containerId);
|
||||||
|
const table = new Tabulator(container, {
|
||||||
|
data: data || [],
|
||||||
|
columns: columns,
|
||||||
|
layout: "fitColumns", // Changed to fitColumns to ensure columns display horizontally
|
||||||
|
movableRows: true,
|
||||||
|
movableRowsPlaceholder: false, // Don't use placeholder, show actual row content
|
||||||
|
movableRowsSender: "table", // Keep a copy of the row in the table while dragging
|
||||||
|
rowHeader: {headerSort:false, resizable: false, minWidth:30, width:30, rowHandle:true, formatter:"handle"},
|
||||||
|
maxHeight: "50%", // Auto height to display all rows
|
||||||
|
placeholder: "No Data Available",
|
||||||
|
autoResize: false,
|
||||||
|
resizableColumnFit: true,
|
||||||
|
responsiveLayout: false,
|
||||||
|
tooltips: true, // Enable tooltips
|
||||||
|
tooltipsHeader: true,
|
||||||
|
selectable: false, // Disable row selection to prevent jumping
|
||||||
|
selectableRangeMode: "click", // Only select on click, not on drag
|
||||||
|
selectableRollingSelection: false, // Disable rolling selection
|
||||||
|
scrollToRowIfVisible: false, // Don't scroll to row even if it's already visible
|
||||||
|
scrollToRowPosition: "nearest",
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Tabulator created for', containerId);
|
||||||
|
container.classList.add('tabulator-initialized');
|
||||||
|
|
||||||
|
// Debug: Log table structure
|
||||||
|
console.log('Table structure:', {
|
||||||
|
tableElement: container,
|
||||||
|
tableData: table.getData(),
|
||||||
|
tableColumns: table.getColumnDefinitions()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add row button
|
||||||
|
const addRowBtn = document.createElement('button');
|
||||||
|
addRowBtn.className = 'btn btn-sm btn-primary mt-2';
|
||||||
|
addRowBtn.innerHTML = 'Add Row';
|
||||||
|
addRowBtn.addEventListener('click', () => {
|
||||||
|
const newRow = {};
|
||||||
|
// Create empty row with default values
|
||||||
|
Object.entries(listTypeConfig).forEach(([key, field]) => {
|
||||||
|
if (field.type === 'boolean') {
|
||||||
|
newRow[key] = field.default === true;
|
||||||
|
} else {
|
||||||
|
newRow[key] = field.default !== undefined ? field.default : '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
table.addRow(newRow);
|
||||||
|
this._updateTextarea(containerId, table);
|
||||||
|
});
|
||||||
|
container.parentNode.insertBefore(addRowBtn, container.nextSibling);
|
||||||
|
|
||||||
|
// Add explode button for fullscreen mode
|
||||||
|
const explodeBtn = document.createElement('button');
|
||||||
|
explodeBtn.className = 'btn btn-sm btn-secondary mt-2 ms-2';
|
||||||
|
explodeBtn.innerHTML = '<i class="material-icons">fullscreen</i> Expand';
|
||||||
|
explodeBtn.addEventListener('click', () => {
|
||||||
|
container.classList.toggle('fullscreen-mode');
|
||||||
|
|
||||||
|
// Update button text based on current state
|
||||||
|
if (container.classList.contains('fullscreen-mode')) {
|
||||||
|
explodeBtn.innerHTML = '<i class="material-icons">fullscreen_exit</i> Collapse';
|
||||||
|
} else {
|
||||||
|
explodeBtn.innerHTML = '<i class="material-icons">fullscreen</i> Expand';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redraw table to adjust to new size
|
||||||
|
table.redraw(true);
|
||||||
|
});
|
||||||
|
container.parentNode.insertBefore(explodeBtn, addRowBtn.nextSibling);
|
||||||
|
|
||||||
|
// Store instance
|
||||||
|
this.instances[containerId] = {
|
||||||
|
table: table,
|
||||||
|
textarea: document.getElementById(containerId.replace('-editor', ''))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent scrolling when clicking on cells
|
||||||
|
container.addEventListener('click', function(e) {
|
||||||
|
// Prevent the default behavior which might cause scrolling
|
||||||
|
if (e.target.closest('.tabulator-cell')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
// Update textarea on various events that change data
|
||||||
|
table.on("dataChanged", () => {
|
||||||
|
console.log("dataChanged event triggered");
|
||||||
|
this._updateTextarea(containerId, table);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for row movement
|
||||||
|
table.on("rowMoved", () => {
|
||||||
|
console.log("rowMoved event triggered");
|
||||||
|
this._updateTextarea(containerId, table);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for cell edits
|
||||||
|
table.on("cellEdited", () => {
|
||||||
|
console.log("cellEdited event triggered");
|
||||||
|
this._updateTextarea(containerId, table);
|
||||||
|
});
|
||||||
|
|
||||||
|
return table;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error initializing Tabulator for ${containerId}:`, e);
|
||||||
|
container.innerHTML = `<div class="alert alert-danger p-3">
|
||||||
|
<strong>Error initializing Tabulator:</strong><br>${e.message}
|
||||||
|
</div>`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateTextarea: function(containerId, table) {
|
||||||
|
const instance = this.instances[containerId];
|
||||||
|
if (instance && instance.textarea) {
|
||||||
|
const data = table.getData();
|
||||||
|
console.log('Updating textarea with data:', data);
|
||||||
|
instance.textarea.value = JSON.stringify(data);
|
||||||
|
console.log('Textarea value updated:', instance.textarea.value);
|
||||||
|
|
||||||
|
// Trigger change event on textarea to ensure form validation recognizes the change
|
||||||
|
const event = new Event('change', { bubbles: true });
|
||||||
|
instance.textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
// Also trigger input event for any listeners that might be using that
|
||||||
|
const inputEvent = new Event('input', { bubbles: true });
|
||||||
|
instance.textarea.dispatchEvent(inputEvent);
|
||||||
|
} else {
|
||||||
|
console.error('Cannot update textarea: instance or textarea not found for', containerId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getListTypeConfig: function(listType) {
|
||||||
|
// Try to get the list type configuration from window.listTypeConfigs
|
||||||
|
if (window.listTypeConfigs && window.listTypeConfigs[listType]) {
|
||||||
|
return window.listTypeConfigs[listType];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, log a warning and return a default configuration
|
||||||
|
console.warn(`List type configuration for ${listType} not found in window.listTypeConfigs. Using a default configuration.`);
|
||||||
|
return {
|
||||||
|
title: {
|
||||||
|
name: "Title",
|
||||||
|
description: "Title",
|
||||||
|
type: "str",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
name: "Description",
|
||||||
|
description: "Description",
|
||||||
|
type: "text",
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Custom formatter for text columns to truncate text in normal mode
|
||||||
|
_truncateFormatter: function(cell, formatterParams, onRendered) {
|
||||||
|
const value = cell.getValue();
|
||||||
|
const maxLength = formatterParams.maxLength || 100;
|
||||||
|
|
||||||
|
if (value && value.length > maxLength) {
|
||||||
|
// Create a truncated version with "..." and show more indicator
|
||||||
|
const truncated = value.substring(0, maxLength) + "...";
|
||||||
|
|
||||||
|
// Return HTML with truncated text and a "show more" button
|
||||||
|
return `<div class="truncated-cell">
|
||||||
|
<div class="truncated-content">${truncated}</div>
|
||||||
|
<div class="show-more" title="Click to edit and see full text">
|
||||||
|
<i class="material-icons">more_horiz</i>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
_createColumnsFromListType: function(listTypeConfig) {
|
||||||
|
const columns = [];
|
||||||
|
|
||||||
|
// Add columns for each field in the list type
|
||||||
|
Object.entries(listTypeConfig).forEach(([key, field]) => {
|
||||||
|
const column = {
|
||||||
|
title: field.name || key,
|
||||||
|
field: key,
|
||||||
|
headerTooltip: field.description,
|
||||||
|
headerSort: false,
|
||||||
|
visible: true,
|
||||||
|
resizable: "header",
|
||||||
|
};
|
||||||
|
console.log("Column ", field.name, " type: ", field.type)
|
||||||
|
// Set width based on field type
|
||||||
|
if (field.type === 'boolean') {
|
||||||
|
column.minWidth = 50;
|
||||||
|
column.maxWidth = 80; // Limit maximum width
|
||||||
|
column.widthGrow = 0; // Don't allow boolean columns to grow
|
||||||
|
} else if (field.type === 'text') {
|
||||||
|
column.width = 400; // Much larger width for text columns (especially description)
|
||||||
|
column.minWidth = 300; // Ensure text columns have adequate minimum width
|
||||||
|
column.widthGrow = 2; // Allow text columns to grow significantly more
|
||||||
|
} else {
|
||||||
|
column.width = 150; // Default width for other columns
|
||||||
|
column.minWidth = 100;
|
||||||
|
column.widthGrow = 1; // Allow some growth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure consistent width calculation
|
||||||
|
column.widthShrink = 0; // Don't allow shrinking below minWidth
|
||||||
|
|
||||||
|
// Set editor based on field type
|
||||||
|
if (field.type === 'boolean') {
|
||||||
|
column.formatter = 'tickCross';
|
||||||
|
column.editor = 'tickCross';
|
||||||
|
column.hozAlign = 'center';
|
||||||
|
column.headerHozAlign = 'center';
|
||||||
|
column.formatterParams = {
|
||||||
|
allowEmpty: true,
|
||||||
|
allowTruthy: true,
|
||||||
|
tickElement: "<i class='material-icons'>check_circle</i>",
|
||||||
|
crossElement: "<i class='material-icons'>cancel</i>"
|
||||||
|
};
|
||||||
|
} else if (field.type === 'enum' && field.allowed_values) {
|
||||||
|
column.editor = 'select';
|
||||||
|
column.editorParams = {
|
||||||
|
values: field.allowed_values
|
||||||
|
};
|
||||||
|
column.hozAlign = 'left';
|
||||||
|
column.headerHozAlign = 'left';
|
||||||
|
} else if (field.type === 'text') {
|
||||||
|
column.editor = 'textarea';
|
||||||
|
column.formatter = this._truncateFormatter; // Use custom formatter to truncate text
|
||||||
|
column.variableHeight = true;
|
||||||
|
// Configure formatter parameters
|
||||||
|
column.formatterParams = {
|
||||||
|
maxLength: 50,
|
||||||
|
autoResize: true
|
||||||
|
};
|
||||||
|
// Prevent scrolling when editing text cells
|
||||||
|
column.editorParams = {
|
||||||
|
elementAttributes: {
|
||||||
|
preventScroll: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
column.hozAlign = 'left';
|
||||||
|
column.headerHozAlign = 'left';
|
||||||
|
} else {
|
||||||
|
column.editor = 'input';
|
||||||
|
column.hozAlign = 'left';
|
||||||
|
column.headerHozAlign = 'left';
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(column);
|
||||||
|
});
|
||||||
|
|
||||||
|
// We don't add a delete button column as per requirements
|
||||||
|
// to prevent users from deleting rows
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
},
|
||||||
|
|
||||||
|
get: function(containerId) {
|
||||||
|
return this.instances[containerId] || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function(containerId) {
|
||||||
|
if (this.instances[containerId]) {
|
||||||
|
if (this.instances[containerId].table && typeof this.instances[containerId].table.destroy === 'function') {
|
||||||
|
this.instances[containerId].table.destroy();
|
||||||
|
}
|
||||||
|
delete this.instances[containerId];
|
||||||
|
}
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (container) {
|
||||||
|
container.classList.remove('tabulator-initialized');
|
||||||
|
container.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize list type configurations
|
||||||
|
window.listTypeConfigs = window.listTypeConfigs || {};
|
||||||
|
|
||||||
|
// Initialize ordered list editors
|
||||||
|
document.querySelectorAll('.ordered-list-field').forEach(function(textarea) {
|
||||||
|
const containerId = textarea.id + '-editor';
|
||||||
|
console.log('Initializing ordered list editor for', containerId);
|
||||||
|
|
||||||
|
// Create container if it doesn't exist
|
||||||
|
let container = document.getElementById(containerId);
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.id = containerId;
|
||||||
|
container.className = 'ordered-list-editor';
|
||||||
|
textarea.parentNode.insertBefore(container, textarea.nextSibling);
|
||||||
|
textarea.classList.add('d-none'); // Hide the textarea
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = textarea.value ? JSON.parse(textarea.value) : [];
|
||||||
|
const listType = textarea.getAttribute('data-list-type');
|
||||||
|
|
||||||
|
// Check if we have the list type configuration
|
||||||
|
if (listType && !window.listTypeConfigs[listType]) {
|
||||||
|
console.warn(`List type configuration for ${listType} not found. Using default configuration.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.EveAI.OrderedListEditors.initialize(containerId, data, listType);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error initializing ordered list editor:', e);
|
||||||
|
container.innerHTML = `<div class="alert alert-danger p-3">
|
||||||
|
<strong>Error initializing ordered list editor:</strong><br>${e.message}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -15,29 +15,17 @@
|
|||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{% set disabled_fields = ['type', 'type_version'] %}
|
{% set disabled_fields = ['type', 'type_version'] %}
|
||||||
{% set exclude_fields = [] %}
|
{% set exclude_fields = [] %}
|
||||||
<!-- Render Static Fields -->
|
|
||||||
{% for field in form.get_static_fields() %}
|
|
||||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Overview Section -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="specialist-overview" id="specialist-svg">
|
|
||||||
<img src="{{ svg_path }}" alt="Specialist Overview" class="w-100">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nav Tabs -->
|
<!-- Nav Tabs -->
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<div class="nav-wrapper position-relative end-0">
|
<div class="nav-wrapper position-relative end-0">
|
||||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
<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-bs-toggle="tab" href="#general-tab" role="tab">
|
||||||
|
General
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#configuration-tab" role="tab">
|
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#configuration-tab" role="tab">
|
||||||
Configuration
|
Configuration
|
||||||
@@ -67,6 +55,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content tab-space">
|
<div class="tab-content tab-space">
|
||||||
|
<!-- General Tab -->
|
||||||
|
<div class="tab-pane fade" id="general-tab" role="tabpanel">
|
||||||
|
<!-- Render Static Fields -->
|
||||||
|
{% for field in form.get_static_fields() %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Overview Section -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="specialist-overview" id="specialist-svg">
|
||||||
|
<img src="{{ svg_path }}" alt="Specialist Overview" class="w-100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Configuration Tab -->
|
<!-- Configuration Tab -->
|
||||||
<div class="tab-pane fade show active" id="configuration-tab" role="tabpanel">
|
<div class="tab-pane fade show active" id="configuration-tab" role="tabpanel">
|
||||||
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||||
@@ -420,6 +429,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
color: #344767 !important; /* Default dark color */
|
color: #344767 !important; /* Default dark color */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Style for active tabs */
|
||||||
|
.nav-link.active {
|
||||||
|
background-color: #5e72e4 !important; /* Primary blue color */
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
/* Style for disabled tabs */
|
/* Style for disabled tabs */
|
||||||
.nav-link.disabled {
|
.nav-link.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@@ -476,4 +493,3 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{% 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', 'chat_client_url', 'qr_code_url'] %}
|
||||||
|
{% set exclude_fields = [] %}
|
||||||
|
<!-- Render Static Fields -->
|
||||||
|
{% for field in form.get_static_fields() %}
|
||||||
|
{% if field.name == 'qr_code_url' and field.data %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ field.id }}">{{ field.label.text }}</label>
|
||||||
|
<div style="max-width: 200px;">
|
||||||
|
<img src="{{ field.data }}" alt="QR Code" class="img-fluid">
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="{{ field.name }}" value="{{ field.data|e }}">
|
||||||
|
</div>
|
||||||
|
{% elif field.name == 'chat_client_url' %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ field.id }}" class="form-label">{{ field.label.text }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="{{ field.data }}" id="{{ field.id }}" readonly>
|
||||||
|
<a href="{{ field.data }}" class="btn btn-primary" target="_blank">Open link</a>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="{{ field.name }}" value="{{ field.data|e }}">
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endif %}
|
||||||
|
{% 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' %}
|
{% extends 'base.html' %}
|
||||||
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||||
|
|
||||||
{% block title %}Retrievers{% endblock %}
|
{% block title %}Specialists{% endblock %}
|
||||||
|
|
||||||
{% block content_title %}Specialists{% endblock %}
|
{% block content_title %}Specialists{% endblock %}
|
||||||
{% block content_description %}View Specialists for Tenant{% endblock %}
|
{% block content_description %}View Specialists for Tenant{% endblock %}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<form method="POST" action="{{ url_for('interaction_bp.handle_specialist_selection') }}" id="specialistsForm">
|
<form method="POST" action="{{ url_for('interaction_bp.handle_specialist_selection') }}" id="specialistsForm">
|
||||||
{{ render_selectable_table(headers=["Specialist ID", "Name", "Type"], rows=rows, selectable=True, id="specialistsTable") }}
|
{{ render_selectable_table(headers=["Specialist ID", "Name", "Type", "Type Version", "Active"], rows=rows, selectable=True, id="specialistsTable") }}
|
||||||
<div class="form-group mt-3 d-flex justify-content-between">
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" name="action" value="edit_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Edit Specialist</button>
|
<button type="submit" name="action" value="edit_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Edit Specialist</button>
|
||||||
|
|||||||
@@ -1,5 +1,26 @@
|
|||||||
|
{# Helper functie om veilig de class van een veld te krijgen #}
|
||||||
|
{% macro get_field_class(field, default='') %}
|
||||||
|
{% if field.render_kw is not none and field.render_kw.get('class') is not none %}
|
||||||
|
{{ field.render_kw.get('class') }}
|
||||||
|
{% else %}
|
||||||
|
{{ default }}
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_field_content(field, disabled=False, readonly=False, class='') %}
|
{% macro render_field_content(field, disabled=False, readonly=False, class='') %}
|
||||||
{% if field.type == 'BooleanField' %}
|
{# Check if this is a hidden input field, if so, render only the field without label #}
|
||||||
|
{{ debug_to_console("Field Class: ", field.widget.__class__.__name__) }}
|
||||||
|
{% if field.widget.__class__.__name__ == 'HiddenInput' %}
|
||||||
|
{{ debug_to_console("Hidden Field: ", "Detected") }}
|
||||||
|
{{ field(class="form-control " + class, disabled=disabled, readonly=readonly) }}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in field.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% elif field.type == 'BooleanField' %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
{{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }}
|
{{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }}
|
||||||
@@ -55,9 +76,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% set field_class = get_field_class(field) %}
|
||||||
{% if field.type == 'TextAreaField' and 'json-editor' in class %}
|
{% if field.type == 'TextAreaField' and 'json-editor' in class %}
|
||||||
<div id="{{ field.id }}-editor" class="json-editor-container"></div>
|
<div id="{{ field.id }}-editor" class="json-editor-container"></div>
|
||||||
{{ field(class="form-control d-none " + class, disabled=disabled, readonly=readonly) }}
|
{{ field(class="form-control d-none " + class, disabled=disabled, readonly=readonly) }}
|
||||||
|
{% elif field.type == 'OrderedListField' or 'ordered-list-field' in field_class %}
|
||||||
|
{# Create container for ordered list editor and hide the textarea #}
|
||||||
|
<div id="{{ field.id }}-editor" class="ordered-list-editor"></div>
|
||||||
|
{{ field(class="form-control d-none " + field_class|trim, disabled=disabled, readonly=readonly) }}
|
||||||
{% elif field.type == 'SelectField' %}
|
{% elif field.type == 'SelectField' %}
|
||||||
{{ field(class="form-control form-select " + class, disabled=disabled, readonly=readonly) }}
|
{{ field(class="form-control form-select " + class, disabled=disabled, readonly=readonly) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -76,6 +102,7 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% macro render_field(field, disabled_fields=[], readonly_fields=[], exclude_fields=[], class='') %}
|
{% macro render_field(field, disabled_fields=[], readonly_fields=[], exclude_fields=[], class='') %}
|
||||||
<!-- Debug info -->
|
<!-- Debug info -->
|
||||||
<!-- Field name: {{ field.name }}, Field type: {{ field.__class__.__name__ }} -->
|
<!-- Field name: {{ field.name }}, Field type: {{ field.__class__.__name__ }} -->
|
||||||
@@ -123,7 +150,7 @@
|
|||||||
{% elif cell.type == 'badge' %}
|
{% elif cell.type == 'badge' %}
|
||||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||||
{% elif cell.type == 'link' %}
|
{% 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 %}
|
{% else %}
|
||||||
{{ cell.value }}
|
{{ cell.value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -177,7 +204,7 @@
|
|||||||
{% elif cell.type == 'badge' %}
|
{% elif cell.type == 'badge' %}
|
||||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||||
{% elif cell.type == 'link' %}
|
{% 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 %}
|
{% else %}
|
||||||
{{ cell.value }}
|
{{ cell.value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -342,7 +369,7 @@
|
|||||||
{% elif cell.type == 'badge' %}
|
{% elif cell.type == 'badge' %}
|
||||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||||
{% elif cell.type == 'link' %}
|
{% 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 %}
|
{% else %}
|
||||||
{{ cell.value }}
|
{{ cell.value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -436,3 +463,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% 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': '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': '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 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': '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']},
|
{'name': 'Users', 'url': '/user/view_users', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
]) }}
|
]) }}
|
||||||
@@ -106,6 +107,7 @@
|
|||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
{{ dropdown('Interactions', 'hub', [
|
{{ dropdown('Interactions', 'hub', [
|
||||||
{'name': 'Specialists', 'url': '/interaction/specialists', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'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']},
|
{'name': 'Chat Sessions', 'url': '/interaction/chat_sessions', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
]) }}
|
]) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -120,6 +122,7 @@
|
|||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
{{ dropdown(current_user.user_name, 'person', [
|
{{ dropdown(current_user.user_name, 'person', [
|
||||||
{'name': 'Session Defaults', 'url': '/session_defaults', 'roles': ['Super User', 'Tenant Admin']},
|
{'name': 'Session Defaults', 'url': '/session_defaults', 'roles': ['Super User', 'Tenant Admin']},
|
||||||
|
{'name': 'Release Notes', 'url': '/release_notes', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Logout', 'url': '/logout'}
|
{'name': 'Logout', 'url': '/logout'}
|
||||||
]) }}
|
]) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -133,4 +136,4 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
eveai_app/templates/ordered_list_configs.html
Normal file
7
eveai_app/templates/ordered_list_configs.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{# Include this template in any page that uses ordered_list fields #}
|
||||||
|
{# Usage: {% include 'ordered_list_configs.html' %} #}
|
||||||
|
{# The form must be available in the template context as 'form' #}
|
||||||
|
|
||||||
|
{% if form and form.get_list_type_configs_js %}
|
||||||
|
{{ form.get_list_type_configs_js()|safe }}
|
||||||
|
{% endif %}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% from "macros.html" import render_field %}
|
{% from "macros.html" import render_field, debug_to_console %}
|
||||||
{% block title %}Register Partner Service{% endblock %}
|
{% block title %}Edit Partner Service{% endblock %}
|
||||||
|
|
||||||
{% block content_title %}Register Partner Service{% endblock %}
|
{% block content_title %}Edit Partner Service{% endblock %}
|
||||||
{% block content_description %}Register Partner Service{% endblock %}
|
{% block content_description %}Edit Partner Service{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<!-- Render Dynamic Fields -->
|
<!-- Render Dynamic Fields -->
|
||||||
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
{% 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 %}
|
{% if fields|length > 0 %}
|
||||||
<h4 class="mt-4">{{ collection_name }}</h4>
|
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -23,6 +25,6 @@
|
|||||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% 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>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -9,14 +9,19 @@
|
|||||||
<script src="{{url_for('static', filename='assets/js/material-kit-pro.min.js')}}"></script>
|
<script src="{{url_for('static', filename='assets/js/material-kit-pro.min.js')}}"></script>
|
||||||
|
|
||||||
{% include 'eveai_json_editor.html' %}
|
{% include 'eveai_json_editor.html' %}
|
||||||
|
{% include 'eveai_ordered_list_editor.html' %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize tooltips
|
// Initialize tooltips if bootstrap is available
|
||||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
if (typeof bootstrap !== 'undefined') {
|
||||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
});
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Bootstrap is not defined. Tooltips will not be initialized.');
|
||||||
|
}
|
||||||
|
|
||||||
// De JSON editor initialisatie is hierboven al samengevoegd.
|
// De JSON editor initialisatie is hierboven al samengevoegd.
|
||||||
// Deze dubbele DOMContentLoaded listener en .json-editor initialisatie kan verwijderd worden.
|
// Deze dubbele DOMContentLoaded listener en .json-editor initialisatie kan verwijderd worden.
|
||||||
@@ -46,9 +51,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Find and click the corresponding tab button
|
// Find and click the corresponding tab button
|
||||||
const tabButton = document.querySelector(`[data-bs-toggle="tab"][data-bs-target="#${tabId}"]`);
|
const tabButton = document.querySelector(`[data-bs-toggle="tab"][data-bs-target="#${tabId}"]`);
|
||||||
if (tabButton) {
|
if (tabButton && typeof bootstrap !== 'undefined') {
|
||||||
const tab = new bootstrap.Tab(tabButton);
|
const tab = new bootstrap.Tab(tabButton);
|
||||||
tab.show();
|
tab.show();
|
||||||
|
} else if (tabButton) {
|
||||||
|
// Fallback if bootstrap is not available
|
||||||
|
tabButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll the invalid field into view and focus it
|
// Scroll the invalid field into view and focus it
|
||||||
|
|||||||
@@ -19,3 +19,37 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_footer %} {% 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 %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
// JavaScript to detect user's timezone
|
// JavaScript om de gebruiker's timezone te detecteren
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
document.addEventListener('DOMContentLoaded', (event) => {
|
||||||
// Detect timezone
|
// Detect timezone
|
||||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
@@ -45,6 +45,31 @@
|
|||||||
console.error('Failed to send timezone to server');
|
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>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% 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' %}
|
{% 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 %}
|
{% block title %}Tenant Overview{% endblock %}
|
||||||
|
|
||||||
@@ -9,162 +9,23 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<!-- Main Tenant Information -->
|
{% set disabled_fields = [] %}
|
||||||
{% set main_fields = ['name', 'code', 'website', 'default_language', 'allowed_languages', 'type'] %}
|
|
||||||
{% for field in form %}
|
{% 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 %}
|
{% 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>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content_footer %}
|
{% block content_footer %}
|
||||||
|
|
||||||
{% endblock %}
|
{% 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_security import current_user
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, SelectField
|
from wtforms import StringField, SelectField
|
||||||
@@ -36,7 +36,7 @@ class SessionDefaultsForm(FlaskForm):
|
|||||||
else:
|
else:
|
||||||
self.partner_name.data = ""
|
self.partner_name.data = ""
|
||||||
self.default_language.choices = [(lang, lang.lower()) for lang in
|
self.default_language.choices = [(lang, lang.lower()) for lang in
|
||||||
session.get('tenant').get('allowed_languages')]
|
current_app.config['SUPPORTED_LANGUAGES']]
|
||||||
self.default_language.data = session.get('default_language')
|
self.default_language.data = session.get('default_language')
|
||||||
|
|
||||||
# Get a new session for catalog queries
|
# Get a new session for catalog queries
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
from flask import request, render_template, Blueprint, session, current_app, jsonify, flash, redirect
|
from flask import request, render_template, Blueprint, session, current_app, jsonify, flash, redirect, url_for
|
||||||
from flask_security import roles_required, roles_accepted
|
from flask_security import roles_required, roles_accepted
|
||||||
from flask_wtf.csrf import generate_csrf
|
from flask_wtf.csrf import generate_csrf
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
from common.models.document import Catalog
|
from common.models.document import Catalog
|
||||||
from common.models.user import Tenant
|
from common.models.user import Tenant
|
||||||
from common.utils.database import Database
|
from common.utils.database import Database
|
||||||
from common.utils.nginx_utils import prefixed_url_for
|
from common.utils.nginx_utils import prefixed_url_for
|
||||||
from .basic_forms import SessionDefaultsForm
|
from .basic_forms import SessionDefaultsForm
|
||||||
|
from common.extensions import content_manager
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
|
||||||
basic_bp = Blueprint('basic_bp', __name__)
|
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
|
@basic_bp.before_request
|
||||||
def log_before_request():
|
def log_before_request():
|
||||||
@@ -101,3 +111,58 @@ def check_csrf():
|
|||||||
'session_data': dict(session)
|
'session_data': dict(session)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@basic_bp.route('/content/<content_type>', methods=['GET'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
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:
|
||||||
|
current_app.logger.debug(f"Showing content {content_type}")
|
||||||
|
major_minor = request.args.get('version')
|
||||||
|
patch = request.args.get('patch')
|
||||||
|
|
||||||
|
# Gebruik de ContentManager om de content op te halen
|
||||||
|
content_data = content_manager.read_content(content_type, major_minor, patch)
|
||||||
|
|
||||||
|
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=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 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
|
from flask_wtf.file import FileField, FileRequired
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from wtforms.widgets.core import HiddenInput
|
||||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||||
|
|
||||||
from common.extensions import cache_manager
|
from common.extensions import cache_manager
|
||||||
@@ -17,8 +18,15 @@ from config.type_defs.processor_types import PROCESSOR_TYPES
|
|||||||
from .dynamic_form_base import DynamicFormBase
|
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):
|
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()])
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
|
||||||
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
||||||
@@ -41,7 +49,8 @@ class CatalogForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class EditCatalogForm(DynamicFormBase):
|
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()])
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
|
||||||
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
||||||
@@ -181,7 +190,7 @@ class AddDocumentForm(DynamicFormBase):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.language.choices = [(language, language) for language in
|
self.language.choices = [(language, language) for language in
|
||||||
session.get('tenant').get('allowed_languages')]
|
current_app.config['SUPPORTED_LANGUAGES']]
|
||||||
if not self.language.data:
|
if not self.language.data:
|
||||||
self.language.data = session.get('tenant').get('default_language')
|
self.language.data = session.get('tenant').get('default_language')
|
||||||
|
|
||||||
@@ -201,7 +210,7 @@ class AddURLForm(DynamicFormBase):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.language.choices = [(language, language) for language in
|
self.language.choices = [(language, language) for language in
|
||||||
session.get('tenant').get('allowed_languages')]
|
current_app.config['SUPPORTED_LANGUAGES']]
|
||||||
if not self.language.data:
|
if not self.language.data:
|
||||||
self.language.data = session.get('tenant').get('default_language')
|
self.language.data = session.get('tenant').get('default_language')
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from common.utils.document_utils import create_document_stack, start_embedding_t
|
|||||||
from common.utils.dynamic_field_utils import create_default_config_from_type_config
|
from common.utils.dynamic_field_utils import create_default_config_from_type_config
|
||||||
from common.utils.eveai_exceptions import EveAIInvalidLanguageException, EveAIUnsupportedFileType, \
|
from common.utils.eveai_exceptions import EveAIInvalidLanguageException, EveAIUnsupportedFileType, \
|
||||||
EveAIDoubleURLException, EveAIException
|
EveAIDoubleURLException, EveAIException
|
||||||
from config.type_defs.processor_types import PROCESSOR_TYPES
|
|
||||||
from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm, \
|
from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm, \
|
||||||
CatalogForm, EditCatalogForm, RetrieverForm, EditRetrieverForm, ProcessorForm, EditProcessorForm
|
CatalogForm, EditCatalogForm, RetrieverForm, EditRetrieverForm, ProcessorForm, EditProcessorForm
|
||||||
from common.utils.middleware import mw_before_request
|
from common.utils.middleware import mw_before_request
|
||||||
@@ -29,7 +28,6 @@ from common.utils.nginx_utils import prefixed_url_for
|
|||||||
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
||||||
from .document_list_view import DocumentListView
|
from .document_list_view import DocumentListView
|
||||||
from .document_version_list_view import DocumentVersionListView
|
from .document_version_list_view import DocumentVersionListView
|
||||||
from config.type_defs.catalog_types import CATALOG_TYPES
|
|
||||||
|
|
||||||
document_bp = Blueprint('document_bp', __name__, url_prefix='/document')
|
document_bp = Blueprint('document_bp', __name__, url_prefix='/document')
|
||||||
|
|
||||||
@@ -126,8 +124,8 @@ def edit_catalog(catalog_id):
|
|||||||
tenant_id = session.get('tenant').get('id')
|
tenant_id = session.get('tenant').get('id')
|
||||||
|
|
||||||
form = EditCatalogForm(request.form, obj=catalog)
|
form = EditCatalogForm(request.form, obj=catalog)
|
||||||
configuration_config = CATALOG_TYPES[catalog.type]["configuration"]
|
full_config = cache_manager.catalogs_config_cache.get_config(catalog.type)
|
||||||
form.add_dynamic_fields("configuration", configuration_config, catalog.configuration)
|
form.add_dynamic_fields("configuration", full_config, catalog.configuration)
|
||||||
|
|
||||||
if request.method == 'POST' and form.validate_on_submit():
|
if request.method == 'POST' and form.validate_on_submit():
|
||||||
form.populate_obj(catalog)
|
form.populate_obj(catalog)
|
||||||
@@ -160,8 +158,9 @@ def processor():
|
|||||||
new_processor = Processor()
|
new_processor = Processor()
|
||||||
form.populate_obj(new_processor)
|
form.populate_obj(new_processor)
|
||||||
new_processor.catalog_id = form.catalog.data.id
|
new_processor.catalog_id = form.catalog.data.id
|
||||||
|
processor_config = cache_manager.processors_config_cache.get_config(new_processor.type)
|
||||||
new_processor.configuration = create_default_config_from_type_config(
|
new_processor.configuration = create_default_config_from_type_config(
|
||||||
PROCESSOR_TYPES[new_processor.type]["configuration"])
|
processor_config["configuration"])
|
||||||
|
|
||||||
set_logging_information(new_processor, dt.now(tz.utc))
|
set_logging_information(new_processor, dt.now(tz.utc))
|
||||||
|
|
||||||
@@ -197,8 +196,8 @@ def edit_processor(processor_id):
|
|||||||
# Create form instance with the processor
|
# Create form instance with the processor
|
||||||
form = EditProcessorForm(request.form, obj=processor)
|
form = EditProcessorForm(request.form, obj=processor)
|
||||||
|
|
||||||
configuration_config = PROCESSOR_TYPES[processor.type]["configuration"]
|
full_config = cache_manager.processors_config_cache.get_config(processor.type)
|
||||||
form.add_dynamic_fields("configuration", configuration_config, processor.configuration)
|
form.add_dynamic_fields("configuration", full_config, processor.configuration)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Update basic fields
|
# Update basic fields
|
||||||
@@ -390,9 +389,7 @@ def add_document():
|
|||||||
|
|
||||||
catalog = Catalog.query.get_or_404(catalog_id)
|
catalog = Catalog.query.get_or_404(catalog_id)
|
||||||
if catalog.configuration and len(catalog.configuration) > 0:
|
if catalog.configuration and len(catalog.configuration) > 0:
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
form.add_dynamic_fields("tagging_fields", catalog.configuration)
|
||||||
for config in document_version_configurations:
|
|
||||||
form.add_dynamic_fields(config, catalog.configuration[config])
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
@@ -402,10 +399,8 @@ def add_document():
|
|||||||
sub_file_type = form.sub_file_type.data
|
sub_file_type = form.sub_file_type.data
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
extension = filename.rsplit('.', 1)[1].lower()
|
extension = filename.rsplit('.', 1)[1].lower()
|
||||||
catalog_properties = {}
|
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
catalog_properties = form.get_dynamic_data("tagging_fields")
|
||||||
for config in document_version_configurations:
|
|
||||||
catalog_properties[config] = form.get_dynamic_data(config)
|
|
||||||
|
|
||||||
api_input = {
|
api_input = {
|
||||||
'catalog_id': catalog_id,
|
'catalog_id': catalog_id,
|
||||||
@@ -445,9 +440,7 @@ def add_url():
|
|||||||
|
|
||||||
catalog = Catalog.query.get_or_404(catalog_id)
|
catalog = Catalog.query.get_or_404(catalog_id)
|
||||||
if catalog.configuration and len(catalog.configuration) > 0:
|
if catalog.configuration and len(catalog.configuration) > 0:
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
form.add_dynamic_fields("tagging_fields", catalog.configuration)
|
||||||
for config in document_version_configurations:
|
|
||||||
form.add_dynamic_fields(config, catalog.configuration[config])
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
@@ -459,7 +452,8 @@ def add_url():
|
|||||||
file_content, filename, extension = process_url(url, tenant_id)
|
file_content, filename, extension = process_url(url, tenant_id)
|
||||||
|
|
||||||
catalog_properties = {}
|
catalog_properties = {}
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
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:
|
for config in document_version_configurations:
|
||||||
catalog_properties[config] = form.get_dynamic_data(config)
|
catalog_properties[config] = form.get_dynamic_data(config)
|
||||||
|
|
||||||
@@ -582,13 +576,14 @@ def edit_document_version_view(document_version_id):
|
|||||||
|
|
||||||
catalog = Catalog.query.get_or_404(catalog_id)
|
catalog = Catalog.query.get_or_404(catalog_id)
|
||||||
if catalog.configuration and len(catalog.configuration) > 0:
|
if catalog.configuration and len(catalog.configuration) > 0:
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
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:
|
for config in document_version_configurations:
|
||||||
form.add_dynamic_fields(config, catalog.configuration[config], doc_vers.catalog_properties[config])
|
form.add_dynamic_fields(config, full_config, doc_vers.catalog_properties[config])
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
catalog_properties = {}
|
catalog_properties = {}
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
# Use the full_config variable we already defined
|
||||||
for config in document_version_configurations:
|
for config in document_version_configurations:
|
||||||
catalog_properties[config] = form.get_dynamic_data(config)
|
catalog_properties[config] = form.get_dynamic_data(config)
|
||||||
|
|
||||||
@@ -897,4 +892,3 @@ def clean_markdown(markdown):
|
|||||||
if markdown.endswith("```"):
|
if markdown.endswith("```"):
|
||||||
markdown = markdown[:-3].strip()
|
markdown = markdown[:-3].strip()
|
||||||
return markdown
|
return markdown
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
from datetime import date
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
|
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
|
||||||
validators, ValidationError)
|
validators, ValidationError)
|
||||||
from flask import current_app, request
|
from flask import current_app, request, session
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from wtforms.fields.choices import SelectField
|
from wtforms.fields.choices import SelectField
|
||||||
from wtforms.fields.datetime import DateField
|
from wtforms.fields.datetime import DateField
|
||||||
|
from wtforms.fields.simple import ColorField
|
||||||
|
|
||||||
|
from common.models.user import TenantMake
|
||||||
from common.utils.config_field_types import TaggingFields, json_to_patterns, patterns_to_json
|
from common.utils.config_field_types import TaggingFields, json_to_patterns, patterns_to_json
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +54,51 @@ class ChunkingPatternsField(TextAreaField):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderedListField(TextAreaField):
|
||||||
|
"""Field for ordered list data that will be rendered as a Tabulator table"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
list_type = kwargs.pop('list_type', '')
|
||||||
|
|
||||||
|
# Behoud bestaande render_kw attributen als die er zijn
|
||||||
|
if 'render_kw' in kwargs:
|
||||||
|
existing_render_kw = kwargs['render_kw']
|
||||||
|
else:
|
||||||
|
existing_render_kw = {}
|
||||||
|
|
||||||
|
current_app.logger.debug(f"incomming render_kw for ordered list field: {existing_render_kw}")
|
||||||
|
|
||||||
|
# Stel nieuwe render_kw samen
|
||||||
|
new_render_kw = {
|
||||||
|
'data-list-type': list_type,
|
||||||
|
'data-handle-enter': 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Voeg klasse toe en behoud bestaande klassen
|
||||||
|
if 'class' in existing_render_kw:
|
||||||
|
existing_classes = existing_render_kw['class']
|
||||||
|
if isinstance(existing_classes, list):
|
||||||
|
existing_classes += ' ordered-list-field'
|
||||||
|
new_render_kw['class'] = existing_classes
|
||||||
|
else:
|
||||||
|
# String classes samenvoegen
|
||||||
|
new_render_kw['class'] = f"{existing_classes} ordered-list-field"
|
||||||
|
else:
|
||||||
|
new_render_kw['class'] = 'ordered-list-field'
|
||||||
|
|
||||||
|
# Voeg alle bestaande attributen toe aan nieuwe render_kw
|
||||||
|
for key, value in existing_render_kw.items():
|
||||||
|
if key != 'class': # Klassen hebben we al verwerkt
|
||||||
|
new_render_kw[key] = value
|
||||||
|
|
||||||
|
current_app.logger.debug(f"final render_kw for ordered list field: {new_render_kw}")
|
||||||
|
|
||||||
|
# Update kwargs met de nieuwe gecombineerde render_kw
|
||||||
|
kwargs['render_kw'] = new_render_kw
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class DynamicFormBase(FlaskForm):
|
class DynamicFormBase(FlaskForm):
|
||||||
def __init__(self, formdata=None, *args, **kwargs):
|
def __init__(self, formdata=None, *args, **kwargs):
|
||||||
super(DynamicFormBase, self).__init__(*args, **kwargs)
|
super(DynamicFormBase, self).__init__(*args, **kwargs)
|
||||||
@@ -89,6 +139,8 @@ class DynamicFormBase(FlaskForm):
|
|||||||
validators_list.append(self._validate_tagging_fields_filter)
|
validators_list.append(self._validate_tagging_fields_filter)
|
||||||
elif field_type == 'dynamic_arguments':
|
elif field_type == 'dynamic_arguments':
|
||||||
validators_list.append(self._validate_dynamic_arguments)
|
validators_list.append(self._validate_dynamic_arguments)
|
||||||
|
elif field_type == 'ordered_list':
|
||||||
|
validators_list.append(self._validate_ordered_list)
|
||||||
|
|
||||||
return validators_list
|
return validators_list
|
||||||
|
|
||||||
@@ -227,10 +279,68 @@ class DynamicFormBase(FlaskForm):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValidationError(f"Invalid argument definition: {str(e)}")
|
raise ValidationError(f"Invalid argument definition: {str(e)}")
|
||||||
|
|
||||||
|
def _validate_ordered_list(self, form, field):
|
||||||
|
"""Validate the ordered list structure"""
|
||||||
|
if not field.data:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse JSON data
|
||||||
|
list_data = json.loads(field.data)
|
||||||
|
|
||||||
|
# Validate it's a list
|
||||||
|
if not isinstance(list_data, list):
|
||||||
|
raise ValidationError("Ordered list must be a list")
|
||||||
|
|
||||||
|
# Validate each item in the list is a dictionary
|
||||||
|
for i, item in enumerate(list_data):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ValidationError(f"Item {i} in ordered list must be an object")
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise ValidationError("Invalid JSON format")
|
||||||
|
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):
|
def add_dynamic_fields(self, collection_name, config, initial_data=None):
|
||||||
"""Add dynamic fields to the form based on the configuration."""
|
"""Add dynamic fields to the form based on the configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
collection_name: The name of the collection of fields to add
|
||||||
|
config: The full configuration object, which should contain the field definitions
|
||||||
|
for the collection_name and may also contain list_type definitions
|
||||||
|
initial_data: Optional initial data for the fields
|
||||||
|
"""
|
||||||
|
current_app.logger.debug(f"Adding dynamic fields for collection {collection_name} with config: {config}")
|
||||||
|
# Store the full configuration for later use in get_list_type_configs_js
|
||||||
|
if not hasattr(self, '_full_configs'):
|
||||||
|
self._full_configs = {}
|
||||||
|
self._full_configs[collection_name] = config
|
||||||
|
|
||||||
|
# Get the specific field configuration for this collection
|
||||||
|
field_config = config.get(collection_name, {})
|
||||||
|
if not field_config:
|
||||||
|
# Handle the case where config is already the specific field configuration
|
||||||
|
return
|
||||||
|
|
||||||
self.dynamic_fields[collection_name] = []
|
self.dynamic_fields[collection_name] = []
|
||||||
for field_name, field_def in config.items():
|
for field_name, field_def in field_config.items():
|
||||||
# Prefix the field name with the collection name
|
# Prefix the field name with the collection name
|
||||||
full_field_name = f"{collection_name}_{field_name}"
|
full_field_name = f"{collection_name}_{field_name}"
|
||||||
label = field_def.get('name', field_name)
|
label = field_def.get('name', field_name)
|
||||||
@@ -264,6 +374,13 @@ class DynamicFormBase(FlaskForm):
|
|||||||
field_class = ChunkingPatternsField
|
field_class = ChunkingPatternsField
|
||||||
extra_classes = ['monospace-text', 'pattern-input']
|
extra_classes = ['monospace-text', 'pattern-input']
|
||||||
field_kwargs = {}
|
field_kwargs = {}
|
||||||
|
elif field_type == 'ordered_list':
|
||||||
|
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:
|
else:
|
||||||
extra_classes = ''
|
extra_classes = ''
|
||||||
field_class = {
|
field_class = {
|
||||||
@@ -275,6 +392,7 @@ class DynamicFormBase(FlaskForm):
|
|||||||
'text': TextAreaField,
|
'text': TextAreaField,
|
||||||
'date': DateField,
|
'date': DateField,
|
||||||
'file': FileField,
|
'file': FileField,
|
||||||
|
'color': ColorField,
|
||||||
}.get(field_type, StringField)
|
}.get(field_type, StringField)
|
||||||
field_kwargs = {}
|
field_kwargs = {}
|
||||||
|
|
||||||
@@ -289,12 +407,24 @@ class DynamicFormBase(FlaskForm):
|
|||||||
except (TypeError, ValueError) as e:
|
except (TypeError, ValueError) as e:
|
||||||
current_app.logger.error(f"Error converting initial data to JSON: {e}")
|
current_app.logger.error(f"Error converting initial data to JSON: {e}")
|
||||||
field_data = "{}"
|
field_data = "{}"
|
||||||
|
elif field_type == 'ordered_list' and isinstance(field_data, list):
|
||||||
|
try:
|
||||||
|
field_data = json.dumps(field_data)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
current_app.logger.error(f"Error converting ordered list data to JSON: {e}")
|
||||||
|
field_data = "[]"
|
||||||
elif field_type == 'chunking_patterns':
|
elif field_type == 'chunking_patterns':
|
||||||
try:
|
try:
|
||||||
field_data = json_to_patterns(field_data)
|
field_data = json_to_patterns(field_data)
|
||||||
except (TypeError, ValueError) as e:
|
except (TypeError, ValueError) as e:
|
||||||
current_app.logger.error(f"Error converting initial data to a list of patterns: {e}")
|
current_app.logger.error(f"Error converting initial data to a list of patterns: {e}")
|
||||||
field_data = {}
|
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:
|
elif default is not None:
|
||||||
field_data = default
|
field_data = default
|
||||||
|
|
||||||
@@ -305,6 +435,17 @@ class DynamicFormBase(FlaskForm):
|
|||||||
render_kw['data-bs-toggle'] = 'tooltip'
|
render_kw['data-bs-toggle'] = 'tooltip'
|
||||||
render_kw['data-bs-placement'] = 'right'
|
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}")
|
||||||
|
|
||||||
# Create the field
|
# Create the field
|
||||||
field_kwargs.update({
|
field_kwargs.update({
|
||||||
'label': label,
|
'label': label,
|
||||||
@@ -340,6 +481,73 @@ class DynamicFormBase(FlaskForm):
|
|||||||
# Return all fields that are not dynamic
|
# Return all fields that are not dynamic
|
||||||
return [field for name, field in self._fields.items() if name not in dynamic_field_names]
|
return [field for name, field in self._fields.items() if name not in dynamic_field_names]
|
||||||
|
|
||||||
|
def get_list_type_configs_js(self):
|
||||||
|
"""Generate JavaScript code for list type configurations used by ordered_list fields."""
|
||||||
|
from common.extensions import cache_manager
|
||||||
|
|
||||||
|
list_types = {}
|
||||||
|
|
||||||
|
# First check if we have any full configurations stored
|
||||||
|
if hasattr(self, '_full_configs'):
|
||||||
|
# Look for list types in the stored full configurations
|
||||||
|
for config_name, config in self._full_configs.items():
|
||||||
|
for key, value in config.items():
|
||||||
|
# Check if this is a list type definition (not a field definition)
|
||||||
|
if isinstance(value, dict) and all(isinstance(v, dict) for v in value.values()):
|
||||||
|
# This looks like a list type definition
|
||||||
|
list_types[key] = value
|
||||||
|
|
||||||
|
# Collect all list types used in ordered_list fields
|
||||||
|
for collection_name, field_names in self.dynamic_fields.items():
|
||||||
|
for full_field_name in field_names:
|
||||||
|
field = getattr(self, full_field_name)
|
||||||
|
if isinstance(field, OrderedListField):
|
||||||
|
list_type = field.render_kw.get('data-list-type')
|
||||||
|
if list_type and list_type not in list_types:
|
||||||
|
# First try to get from current_app.config
|
||||||
|
list_type_config = current_app.config.get('LIST_TYPES', {}).get(list_type)
|
||||||
|
if list_type_config:
|
||||||
|
list_types[list_type] = list_type_config
|
||||||
|
else:
|
||||||
|
# Try to find the list type in specialist configurations using the cache
|
||||||
|
try:
|
||||||
|
# Get all specialist types
|
||||||
|
specialist_types = cache_manager.specialists_types_cache.get_types()
|
||||||
|
|
||||||
|
# For each specialist type, check if it has the list type we're looking for
|
||||||
|
for specialist_type in specialist_types:
|
||||||
|
try:
|
||||||
|
# Get the latest version for this specialist type
|
||||||
|
latest_version = cache_manager.specialists_version_tree_cache.get_latest_version(specialist_type)
|
||||||
|
|
||||||
|
# Get the configuration for this specialist type and version
|
||||||
|
specialist_config = cache_manager.specialists_config_cache.get_config(specialist_type, latest_version)
|
||||||
|
|
||||||
|
# Check if this specialist has the list type we're looking for
|
||||||
|
if list_type in specialist_config:
|
||||||
|
list_types[list_type] = specialist_config[list_type]
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.debug(f"Error checking specialist {specialist_type}: {e}")
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error retrieving specialist configurations: {e}")
|
||||||
|
|
||||||
|
# If no list types found, return empty script
|
||||||
|
if not list_types:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Generate JavaScript code
|
||||||
|
js_code = "<script>\n"
|
||||||
|
js_code += "window.listTypeConfigs = window.listTypeConfigs || {};\n"
|
||||||
|
|
||||||
|
for list_type, config in list_types.items():
|
||||||
|
js_code += f"window.listTypeConfigs['{list_type}'] = {json.dumps(config, indent=2)};\n"
|
||||||
|
|
||||||
|
js_code += "</script>\n"
|
||||||
|
|
||||||
|
return js_code
|
||||||
|
|
||||||
def get_dynamic_fields(self):
|
def get_dynamic_fields(self):
|
||||||
"""Return a dictionary of dynamic fields per collection."""
|
"""Return a dictionary of dynamic fields per collection."""
|
||||||
result = {}
|
result = {}
|
||||||
@@ -361,7 +569,7 @@ class DynamicFormBase(FlaskForm):
|
|||||||
if field.type == 'BooleanField':
|
if field.type == 'BooleanField':
|
||||||
data[original_field_name] = full_field_name in self.raw_formdata
|
data[original_field_name] = full_field_name in self.raw_formdata
|
||||||
current_app.logger.debug(f"Value for {original_field_name} is {data[original_field_name]}")
|
current_app.logger.debug(f"Value for {original_field_name} is {data[original_field_name]}")
|
||||||
elif isinstance(field, (TaggingFieldsField, TaggingFieldsFilterField, DynamicArgumentsField)) and field.data:
|
elif isinstance(field, (TaggingFieldsField, TaggingFieldsFilterField, DynamicArgumentsField, OrderedListField)) and field.data:
|
||||||
try:
|
try:
|
||||||
data[original_field_name] = json.loads(field.data)
|
data[original_field_name] = json.loads(field.data)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
@@ -372,6 +580,8 @@ class DynamicFormBase(FlaskForm):
|
|||||||
data[original_field_name] = patterns_to_json(field.data)
|
data[original_field_name] = patterns_to_json(field.data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error converting initial data to patterns: {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:
|
else:
|
||||||
data[original_field_name] = field.data
|
data[original_field_name] = field.data
|
||||||
return data
|
return data
|
||||||
@@ -422,7 +632,7 @@ def validate_tagging_fields(form, field):
|
|||||||
raise ValidationError(f"Field {field_name} missing required 'type' property")
|
raise ValidationError(f"Field {field_name} missing required 'type' property")
|
||||||
|
|
||||||
# Validate type
|
# 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']}")
|
raise ValidationError(f"Field {field_name} has invalid type: {field_def['type']}")
|
||||||
|
|
||||||
# Validate enum fields have allowed_values
|
# Validate enum fields have allowed_values
|
||||||
@@ -443,4 +653,4 @@ def validate_tagging_fields(form, field):
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise ValidationError("Invalid JSON format")
|
raise ValidationError("Invalid JSON format")
|
||||||
except (TypeError, ValueError) as e:
|
except (TypeError, ValueError) as e:
|
||||||
raise ValidationError(f"Invalid field definition: {str(e)}")
|
raise ValidationError(f"Invalid field definition: {str(e)}")
|
||||||
|
|||||||
@@ -259,6 +259,10 @@ def view_usages():
|
|||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
per_page = request.args.get('per_page', 10, type=int)
|
per_page = request.args.get('per_page', 10, type=int)
|
||||||
|
|
||||||
|
if not session.get('tenant', None):
|
||||||
|
flash('You can only view usage for a Tenant. Select a Tenant to continue!', 'danger')
|
||||||
|
return redirect(prefixed_url_for('user_bp.select_tenant'))
|
||||||
|
|
||||||
tenant_id = session.get('tenant').get('id')
|
tenant_id = session.get('tenant').get('id')
|
||||||
query = LicenseUsage.query.filter_by(tenant_id=tenant_id).order_by(desc(LicenseUsage.id))
|
query = LicenseUsage.query.filter_by(tenant_id=tenant_id).order_by(desc(LicenseUsage.id))
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ from wtforms.validators import DataRequired, Length, Optional
|
|||||||
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
|
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
|
||||||
|
|
||||||
from common.models.document import Retriever
|
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.extensions import cache_manager
|
||||||
|
from common.utils.form_assistants import validate_json
|
||||||
|
|
||||||
from .dynamic_form_base import DynamicFormBase
|
from .dynamic_form_base import DynamicFormBase
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ def get_tools():
|
|||||||
|
|
||||||
class SpecialistForm(FlaskForm):
|
class SpecialistForm(FlaskForm):
|
||||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||||
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
|
||||||
retrievers = QuerySelectMultipleField(
|
retrievers = QuerySelectMultipleField(
|
||||||
'Retrievers',
|
'Retrievers',
|
||||||
@@ -33,7 +36,7 @@ class SpecialistForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
type = SelectField('Specialist Type', validators=[DataRequired()])
|
type = SelectField('Specialist Type', validators=[DataRequired()])
|
||||||
|
active = BooleanField('Active', validators=[Optional()], default=True)
|
||||||
tuning = BooleanField('Enable Specialist Tuning', default=False)
|
tuning = BooleanField('Enable Specialist Tuning', default=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -46,6 +49,7 @@ class SpecialistForm(FlaskForm):
|
|||||||
class EditSpecialistForm(DynamicFormBase):
|
class EditSpecialistForm(DynamicFormBase):
|
||||||
name = StringField('Name', validators=[DataRequired()])
|
name = StringField('Name', validators=[DataRequired()])
|
||||||
description = TextAreaField('Description', validators=[Optional()])
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
active = BooleanField('Active', validators=[Optional()], default=True)
|
||||||
|
|
||||||
retrievers = QuerySelectMultipleField(
|
retrievers = QuerySelectMultipleField(
|
||||||
'Retrievers',
|
'Retrievers',
|
||||||
@@ -132,4 +136,54 @@ class ExecuteSpecialistForm(DynamicFormBase):
|
|||||||
description = TextAreaField('Specialist Description', validators=[Optional()], render_kw={'readonly': True})
|
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})
|
||||||
|
chat_client_url = StringField('Chat Client URL', validators=[Optional()], render_kw={'readonly': True})
|
||||||
|
qr_code_url = StringField('QR Code', validators=[Optional()], 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 ast
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
from datetime import datetime as dt, timezone as tz
|
from datetime import datetime as dt, timezone as tz
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -13,9 +14,10 @@ from werkzeug.utils import secure_filename
|
|||||||
|
|
||||||
from common.models.document import Embedding, DocumentVersion, Retriever
|
from common.models.document import Embedding, DocumentVersion, Retriever
|
||||||
from common.models.interaction import (ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever,
|
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.extensions import db, cache_manager
|
||||||
|
from common.models.user import SpecialistMagicLinkTenant
|
||||||
from common.services.interaction.specialist_services import SpecialistServices
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
from common.utils.asset_utils import create_asset_stack, add_asset_version_file
|
from common.utils.asset_utils import create_asset_stack, add_asset_version_file
|
||||||
from common.utils.execution_progress import ExecutionProgressTracker
|
from common.utils.execution_progress import ExecutionProgressTracker
|
||||||
@@ -24,12 +26,10 @@ from common.utils.model_logging_utils import set_logging_information, update_log
|
|||||||
from common.utils.middleware import mw_before_request
|
from common.utils.middleware import mw_before_request
|
||||||
from common.utils.nginx_utils import prefixed_url_for
|
from common.utils.nginx_utils import prefixed_url_for
|
||||||
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
||||||
from common.utils.specialist_utils import initialize_specialist
|
|
||||||
|
|
||||||
from config.type_defs.specialist_types import SPECIALIST_TYPES
|
|
||||||
|
|
||||||
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
|
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')
|
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
|
||||||
|
|
||||||
@@ -162,6 +162,7 @@ def specialist():
|
|||||||
new_specialist.type = form.type.data
|
new_specialist.type = form.type.data
|
||||||
new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version(
|
new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version(
|
||||||
new_specialist.type)
|
new_specialist.type)
|
||||||
|
new_specialist.active = form.active.data
|
||||||
new_specialist.tuning = form.tuning.data
|
new_specialist.tuning = form.tuning.data
|
||||||
|
|
||||||
set_logging_information(new_specialist, dt.now(tz.utc))
|
set_logging_information(new_specialist, dt.now(tz.utc))
|
||||||
@@ -184,7 +185,7 @@ def specialist():
|
|||||||
current_app.logger.info(f'Specialist {new_specialist.name} successfully added for tenant {tenant_id}!')
|
current_app.logger.info(f'Specialist {new_specialist.name} successfully added for tenant {tenant_id}!')
|
||||||
|
|
||||||
# Initialize the newly create specialist
|
# Initialize the newly create specialist
|
||||||
initialize_specialist(new_specialist.id, new_specialist.type, new_specialist.type_version)
|
SpecialistServices.initialize_specialist(new_specialist.id, new_specialist.type, new_specialist.type_version)
|
||||||
|
|
||||||
return redirect(prefixed_url_for('interaction_bp.edit_specialist', specialist_id=new_specialist.id))
|
return redirect(prefixed_url_for('interaction_bp.edit_specialist', specialist_id=new_specialist.id))
|
||||||
|
|
||||||
@@ -204,8 +205,7 @@ def edit_specialist(specialist_id):
|
|||||||
form = EditSpecialistForm(request.form, obj=specialist)
|
form = EditSpecialistForm(request.form, obj=specialist)
|
||||||
|
|
||||||
specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
|
specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
|
||||||
configuration_config = specialist_config.get('configuration')
|
form.add_dynamic_fields("configuration", specialist_config, specialist.configuration)
|
||||||
form.add_dynamic_fields("configuration", configuration_config, specialist.configuration)
|
|
||||||
|
|
||||||
agent_rows = prepare_table_for_macro(specialist.agents,
|
agent_rows = prepare_table_for_macro(specialist.agents,
|
||||||
[('id', ''), ('name', ''), ('type', ''), ('type_version', '')])
|
[('id', ''), ('name', ''), ('type', ''), ('type_version', '')])
|
||||||
@@ -232,6 +232,7 @@ def edit_specialist(specialist_id):
|
|||||||
specialist.name = form.name.data
|
specialist.name = form.name.data
|
||||||
specialist.description = form.description.data
|
specialist.description = form.description.data
|
||||||
specialist.tuning = form.tuning.data
|
specialist.tuning = form.tuning.data
|
||||||
|
specialist.active = form.active.data
|
||||||
# Update the configuration dynamic fields
|
# Update the configuration dynamic fields
|
||||||
specialist.configuration = form.get_dynamic_data("configuration")
|
specialist.configuration = form.get_dynamic_data("configuration")
|
||||||
|
|
||||||
@@ -298,7 +299,7 @@ def specialists():
|
|||||||
|
|
||||||
# prepare table data
|
# prepare table data
|
||||||
rows = prepare_table_for_macro(the_specialists,
|
rows = prepare_table_for_macro(the_specialists,
|
||||||
[('id', ''), ('name', ''), ('type', '')])
|
[('id', ''), ('name', ''), ('type', ''), ('type_version', ''), ('active', ''),])
|
||||||
|
|
||||||
# Render the catalogs in a template
|
# Render the catalogs in a template
|
||||||
return render_template('interaction/specialists.html', rows=rows, pagination=pagination)
|
return render_template('interaction/specialists.html', rows=rows, pagination=pagination)
|
||||||
@@ -521,8 +522,7 @@ def edit_asset_version(asset_version_id):
|
|||||||
form = EditEveAIAssetVersionForm(asset_version)
|
form = EditEveAIAssetVersionForm(asset_version)
|
||||||
asset_config = cache_manager.assets_config_cache.get_config(asset_version.asset.type,
|
asset_config = cache_manager.assets_config_cache.get_config(asset_version.asset.type,
|
||||||
asset_version.asset.type_version)
|
asset_version.asset.type_version)
|
||||||
configuration_config = asset_config.get('configuration')
|
form.add_dynamic_fields("configuration", asset_config, asset_version.configuration)
|
||||||
form.add_dynamic_fields("configuration", configuration_config, asset_version.configuration)
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Update the configuration dynamic fields
|
# Update the configuration dynamic fields
|
||||||
@@ -582,9 +582,8 @@ def execute_specialist(specialist_id):
|
|||||||
return redirect(prefixed_url_for('interaction_bp.specialists'))
|
return redirect(prefixed_url_for('interaction_bp.specialists'))
|
||||||
|
|
||||||
form = ExecuteSpecialistForm(request.form, obj=specialist)
|
form = ExecuteSpecialistForm(request.form, obj=specialist)
|
||||||
arguments_config = specialist_config.get('arguments', None)
|
if 'arguments' in specialist_config:
|
||||||
if arguments_config:
|
form.add_dynamic_fields('arguments', specialist_config)
|
||||||
form.add_dynamic_fields('arguments', arguments_config)
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# We're only interested in gathering the dynamic arguments
|
# We're only interested in gathering the dynamic arguments
|
||||||
@@ -674,4 +673,189 @@ def session_interactions(chat_session_id):
|
|||||||
This route shows all interactions for a given chat_session_id (int).
|
This route shows all interactions for a given chat_session_id (int).
|
||||||
"""
|
"""
|
||||||
chat_session = ChatSession.query.get_or_404(chat_session_id)
|
chat_session = ChatSession.query.get_or_404(chat_session_id)
|
||||||
return session_interactions_by_session_id(chat_session.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
|
||||||
|
|
||||||
|
# Set the chat client URL
|
||||||
|
tenant_id = session.get('tenant').get('id')
|
||||||
|
chat_client_prefix = current_app.config.get('CHAT_CLIENT_PREFIX', 'chat_client/chat/')
|
||||||
|
base_url = request.url_root
|
||||||
|
magic_link_code = specialist_ml.magic_link_code
|
||||||
|
|
||||||
|
# Parse the URL om poortinformatie te behouden als deze afwijkt van de standaard
|
||||||
|
url_parts = request.url.split('/')
|
||||||
|
host_port = url_parts[2] # Dit bevat zowel hostname als poort indien aanwezig
|
||||||
|
|
||||||
|
# Generate the full URL for chat client with magic link code
|
||||||
|
chat_client_url = f"{request.scheme}://{host_port}/{chat_client_prefix}{magic_link_code}"
|
||||||
|
form.chat_client_url.data = chat_client_url
|
||||||
|
|
||||||
|
# Generate QR code as data URI for direct embedding in HTML
|
||||||
|
try:
|
||||||
|
import qrcode
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# Generate QR code as PNG for better compatibility
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=1,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||||
|
box_size=10,
|
||||||
|
border=4
|
||||||
|
)
|
||||||
|
qr.add_data(chat_client_url)
|
||||||
|
qr.make(fit=True)
|
||||||
|
|
||||||
|
# Generate PNG image in memory
|
||||||
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
img.save(buffer, format='PNG')
|
||||||
|
img_data = buffer.getvalue()
|
||||||
|
|
||||||
|
# Create data URI for direct embedding in HTML
|
||||||
|
img_base64 = base64.b64encode(img_data).decode('utf-8')
|
||||||
|
data_uri = f"data:image/png;base64,{img_base64}"
|
||||||
|
|
||||||
|
# Store the data URI in the form data
|
||||||
|
form.qr_code_url.data = data_uri
|
||||||
|
|
||||||
|
current_app.logger.debug(f"QR code generated successfully for {magic_link_code}")
|
||||||
|
current_app.logger.debug(f"QR code data URI starts with: {data_uri[:50]}...")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Failed to generate QR code: {str(e)}")
|
||||||
|
form.qr_code_url.data = "Error generating QR code"
|
||||||
|
|
||||||
|
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_service = PartnerService.query.get_or_404(partner_service_id)
|
||||||
partner = session.get('partner', None)
|
partner = session.get('partner', None)
|
||||||
partner_id = session['partner']['id']
|
partner_id = session['partner']['id']
|
||||||
|
current_app.logger.debug(f"Request Type: {request.method}")
|
||||||
|
|
||||||
form = EditPartnerServiceForm(obj=partner_service)
|
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_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
|
partner_service.type_version)
|
||||||
partner_service.type_version)
|
configuration_config = partner_service_config.get('configuration')
|
||||||
configuration_config = partner_service_config.get('configuration')
|
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
|
||||||
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
|
f"{configuration_config}")
|
||||||
f"{configuration_config}")
|
form.add_dynamic_fields("configuration", partner_service_config, partner_service.configuration)
|
||||||
form.add_dynamic_fields("configuration", configuration_config, partner_service.configuration)
|
permissions_config = partner_service_config.get('permissions')
|
||||||
permissions_config = partner_service_config.get('permissions')
|
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
|
||||||
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
|
f"{permissions_config}")
|
||||||
f"{permissions_config}")
|
form.add_dynamic_fields("permissions", partner_service_config, partner_service.permissions)
|
||||||
form.add_dynamic_fields("permissions", permissions_config, partner_service.permissions)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
current_app.logger.debug(f"Form returned: {form.data}")
|
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 flask_wtf import FlaskForm
|
||||||
from wtforms import (StringField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
|
from wtforms import (StringField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
|
||||||
SelectField, SelectMultipleField, FieldList, FormField, TextAreaField)
|
SelectField, SelectMultipleField, FieldList, FormField, TextAreaField)
|
||||||
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional
|
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, ValidationError
|
||||||
import pytz
|
import pytz
|
||||||
from flask_security import current_user
|
from flask_security import current_user
|
||||||
|
from wtforms.widgets.core import HiddenInput
|
||||||
|
|
||||||
|
from common.models.user import TenantMake
|
||||||
from common.services.user import UserServices
|
from common.services.user import UserServices
|
||||||
from config.type_defs.service_types import SERVICE_TYPES
|
from config.type_defs.service_types import SERVICE_TYPES
|
||||||
|
from eveai_app.views.dynamic_form_base import DynamicFormBase
|
||||||
|
|
||||||
|
|
||||||
class TenantForm(FlaskForm):
|
class TenantForm(FlaskForm):
|
||||||
@@ -17,7 +20,6 @@ class TenantForm(FlaskForm):
|
|||||||
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
||||||
# language fields
|
# language fields
|
||||||
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
|
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
|
||||||
allowed_languages = SelectMultipleField('Allowed Languages', choices=[], validators=[DataRequired()])
|
|
||||||
# invoicing fields
|
# invoicing fields
|
||||||
currency = SelectField('Currency', choices=[], validators=[DataRequired()])
|
currency = SelectField('Currency', choices=[], validators=[DataRequired()])
|
||||||
# Timezone
|
# Timezone
|
||||||
@@ -32,13 +34,56 @@ class TenantForm(FlaskForm):
|
|||||||
super(TenantForm, self).__init__(*args, **kwargs)
|
super(TenantForm, self).__init__(*args, **kwargs)
|
||||||
# initialise language fields
|
# initialise language fields
|
||||||
self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
|
self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
|
||||||
self.allowed_languages.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
|
|
||||||
# initialise currency field
|
# initialise currency field
|
||||||
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
|
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
|
||||||
# initialise timezone
|
# initialise timezone
|
||||||
self.timezone.choices = [(tz, tz) for tz in pytz.all_timezones]
|
self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones]
|
||||||
# Initialize fallback algorithms
|
# Initialize fallback algorithms
|
||||||
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
|
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
|
||||||
|
# Initialize default tenant make choices
|
||||||
|
tenant_id = session.get('tenant', {}).get('id') if 'tenant' in session else None
|
||||||
|
# Show field only for Super Users with partner in session
|
||||||
|
if not current_user.has_roles('Super User') or 'partner' not in session:
|
||||||
|
self._fields.pop('assign_to_partner', None)
|
||||||
|
|
||||||
|
|
||||||
|
class EditTenantForm(FlaskForm):
|
||||||
|
id = IntegerField('ID', widget=HiddenInput())
|
||||||
|
name = StringField('Name', validators=[DataRequired(), Length(max=80)])
|
||||||
|
code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True})
|
||||||
|
type = SelectField('Tenant Type', validators=[Optional()], default='Active')
|
||||||
|
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
||||||
|
# language fields
|
||||||
|
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
|
||||||
|
# invoicing fields
|
||||||
|
currency = SelectField('Currency', choices=[], validators=[DataRequired()])
|
||||||
|
# Timezone
|
||||||
|
timezone = SelectField('Timezone', choices=[], validators=[DataRequired()])
|
||||||
|
# Default tenant make
|
||||||
|
default_tenant_make_id = SelectField('Default Tenant Make', choices=[], validators=[Optional()])
|
||||||
|
|
||||||
|
# For Super Users only - Allow to assign the tenant to the partner
|
||||||
|
assign_to_partner = BooleanField('Assign to Partner', default=False)
|
||||||
|
# Embedding variables
|
||||||
|
submit = SubmitField('Submit')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(EditTenantForm, self).__init__(*args, **kwargs)
|
||||||
|
# initialise language fields
|
||||||
|
self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
|
||||||
|
# initialise currency field
|
||||||
|
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
|
||||||
|
# initialise timezone
|
||||||
|
self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones]
|
||||||
|
# Initialize fallback algorithms
|
||||||
|
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
|
||||||
|
# Initialize default tenant make choices
|
||||||
|
tenant_id = self.id.data
|
||||||
|
if tenant_id:
|
||||||
|
tenant_makes = TenantMake.query.filter_by(tenant_id=tenant_id, active=True).all()
|
||||||
|
self.default_tenant_make_id.choices = [(str(make.id), make.name) for make in tenant_makes]
|
||||||
|
# Add empty choice
|
||||||
|
self.default_tenant_make_id.choices.insert(0, ('', 'Geen'))
|
||||||
# Show field only for Super Users with partner in session
|
# Show field only for Super Users with partner in session
|
||||||
if not current_user.has_roles('Super User') or 'partner' not in session:
|
if not current_user.has_roles('Super User') or 'partner' not in session:
|
||||||
self._fields.pop('assign_to_partner', None)
|
self._fields.pop('assign_to_partner', None)
|
||||||
@@ -131,4 +176,35 @@ class EditTenantProjectForm(FlaskForm):
|
|||||||
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]
|
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_make_name(form, field):
|
||||||
|
# Check if tenant_make already exists in the database
|
||||||
|
existing_make = TenantMake.query.filter_by(name=field.data).first()
|
||||||
|
|
||||||
|
if existing_make:
|
||||||
|
current_app.logger.debug(f'Existing make: {existing_make.id}')
|
||||||
|
current_app.logger.debug(f'Form has id: {hasattr(form, 'id')}')
|
||||||
|
if hasattr(form, 'id'):
|
||||||
|
current_app.logger.debug(f'Form has id: {form.id.data}')
|
||||||
|
if existing_make:
|
||||||
|
if not hasattr(form, 'id') or form.id.data != existing_make.id:
|
||||||
|
raise ValidationError(f'A Make with name "{field.data}" already exists. Choose another name.')
|
||||||
|
|
||||||
|
|
||||||
|
class TenantMakeForm(DynamicFormBase):
|
||||||
|
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
|
import uuid
|
||||||
from datetime import datetime as dt, timezone as tz
|
from datetime import datetime as dt, timezone as tz
|
||||||
from flask import request, redirect, flash, render_template, Blueprint, session, current_app
|
from flask import request, redirect, flash, render_template, Blueprint, session, current_app
|
||||||
@@ -5,12 +6,13 @@ from flask_security import roles_accepted, current_user
|
|||||||
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
||||||
import ast
|
import ast
|
||||||
|
|
||||||
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant
|
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake
|
||||||
from common.extensions import db, security, minio_client, simple_encryption
|
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 common.utils.security_utils import send_confirmation_email, send_reset_email
|
||||||
from config.type_defs.service_types import SERVICE_TYPES
|
from config.type_defs.service_types import SERVICE_TYPES
|
||||||
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
|
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
|
||||||
TenantProjectForm, EditTenantProjectForm
|
TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm, EditTenantMakeForm
|
||||||
from common.utils.database import Database
|
from common.utils.database import Database
|
||||||
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
|
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
|
||||||
from common.utils.simple_encryption import generate_api_key
|
from common.utils.simple_encryption import generate_api_key
|
||||||
@@ -110,12 +112,18 @@ def tenant():
|
|||||||
@roles_accepted('Super User', 'Partner Admin')
|
@roles_accepted('Super User', 'Partner Admin')
|
||||||
def edit_tenant(tenant_id):
|
def edit_tenant(tenant_id):
|
||||||
tenant = Tenant.query.get_or_404(tenant_id) # This will return a 404 if no tenant is found
|
tenant = Tenant.query.get_or_404(tenant_id) # This will return a 404 if no tenant is found
|
||||||
form = TenantForm(obj=tenant)
|
form = EditTenantForm(obj=tenant)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Populate the tenant with form data
|
# Populate the tenant with form data
|
||||||
form.populate_obj(tenant)
|
form.populate_obj(tenant)
|
||||||
|
|
||||||
|
# Convert default_tenant_make_id to integer if not empty
|
||||||
|
if form.default_tenant_make_id.data:
|
||||||
|
tenant.default_tenant_make_id = int(form.default_tenant_make_id.data)
|
||||||
|
else:
|
||||||
|
tenant.default_tenant_make_id = None
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Tenant updated successfully.', 'success')
|
flash('Tenant updated successfully.', 'success')
|
||||||
if session.get('tenant'):
|
if session.get('tenant'):
|
||||||
@@ -459,8 +467,18 @@ def edit_tenant_domain(tenant_domain_id):
|
|||||||
def tenant_overview():
|
def tenant_overview():
|
||||||
tenant_id = session['tenant']['id']
|
tenant_id = session['tenant']['id']
|
||||||
tenant = Tenant.query.get_or_404(tenant_id)
|
tenant = Tenant.query.get_or_404(tenant_id)
|
||||||
form = TenantForm(obj=tenant)
|
form = EditTenantForm(obj=tenant)
|
||||||
return render_template('user/tenant_overview.html', form=form)
|
|
||||||
|
# Zet de waarde van default_tenant_make_id
|
||||||
|
if tenant.default_tenant_make_id:
|
||||||
|
form.default_tenant_make_id.data = str(tenant.default_tenant_make_id)
|
||||||
|
|
||||||
|
# Haal de naam van de default make op als deze bestaat
|
||||||
|
default_make_name = None
|
||||||
|
if tenant.default_tenant_make:
|
||||||
|
default_make_name = tenant.default_tenant_make.name
|
||||||
|
|
||||||
|
return render_template('user/tenant_overview.html', form=form, default_make_name=default_make_name)
|
||||||
|
|
||||||
|
|
||||||
@user_bp.route('/tenant_project', methods=['GET', 'POST'])
|
@user_bp.route('/tenant_project', methods=['GET', 'POST'])
|
||||||
@@ -622,6 +640,133 @@ def delete_tenant_project(tenant_project_id):
|
|||||||
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
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):
|
def reset_uniquifier(user):
|
||||||
security.datastore.set_uniquifier(user)
|
security.datastore.set_uniquifier(user)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from flask import Flask
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from common.utils.celery_utils import make_celery, init_celery
|
from common.utils.celery_utils import make_celery, init_celery
|
||||||
from config.logging_config import LOGGING
|
from config.logging_config import configure_logging
|
||||||
from config.config import get_config
|
from config.config import get_config
|
||||||
|
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ def create_app(config_file=None):
|
|||||||
case _:
|
case _:
|
||||||
app.config.from_object(get_config('dev'))
|
app.config.from_object(get_config('dev'))
|
||||||
|
|
||||||
logging.config.dictConfig(LOGGING)
|
configure_logging()
|
||||||
|
|
||||||
register_extensions(app)
|
register_extensions(app)
|
||||||
|
|
||||||
|
|||||||
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 configure_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
|
||||||
|
|
||||||
|
configure_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)
|
||||||
825
eveai_chat_client/static/assets/css/chat-components.css
Normal file
825
eveai_chat_client/static/assets/css/chat-components.css
Normal file
@@ -0,0 +1,825 @@
|
|||||||
|
|
||||||
|
/* Chat App Container Layout */
|
||||||
|
.chat-app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0; /* Belangrijk voor flexbox overflow */
|
||||||
|
padding: 20px; /* Algemene padding voor alle kanten */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gemeenschappelijke container voor consistente breedte */
|
||||||
|
.chat-component-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px; /* Optimale breedte */
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1; /* Neemt beschikbare verticale ruimte in */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Area - neemt alle beschikbare ruimte */
|
||||||
|
.chat-messages-area {
|
||||||
|
flex: 1; /* Neemt alle beschikbare ruimte */
|
||||||
|
overflow: hidden; /* Voorkomt dat het groter wordt dan container */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0; /* Belangrijk voor nested flexbox */
|
||||||
|
margin-bottom: 20px; /* Ruimte tussen messages en input */
|
||||||
|
border-radius: 15px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px; /* Optimale breedte */
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto; /* Horizontaal centreren */
|
||||||
|
align-self: center; /* Extra centrering in flexbox context */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Input - altijd onderaan */
|
||||||
|
.chat-input-area {
|
||||||
|
flex: none; /* Neemt alleen benodigde ruimte */
|
||||||
|
border-radius: 15px;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||||
|
z-index: 10;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px; /* Optimale breedte */
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto; /* Horizontaal centreren */
|
||||||
|
align-self: center; /* Extra centrering in flexbox context */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zorg dat de MessageHistory container ook flexbox gebruikt */
|
||||||
|
.message-history-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 20px; /* Interne padding voor MessageHistory */
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px; /* Optimale breedte */
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto; /* Horizontaal centreren */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 10px; /* Ruimte voor scrollbar */
|
||||||
|
margin-right: -10px; /* Compenseer voor scrollbar */
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Input styling */
|
||||||
|
.chat-input-container {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
padding: 20px; /* Interne padding voor ChatInput */
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 1000px; /* Optimale breedte */
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto; /* Horizontaal centreren */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
||||||
|
border: 1px solid rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-main {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 45px;
|
||||||
|
max-height: 120px;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 25px;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input.over-limit {
|
||||||
|
border-color: #dc3545;
|
||||||
|
background-color: rgba(220, 53, 69, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.input-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn:hover:not(:disabled) {
|
||||||
|
background: var(--secondary-color);
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Character counter */
|
||||||
|
.character-counter {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -25px;
|
||||||
|
right: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: rgba(255,255,255,0.9);
|
||||||
|
border-radius: 10px;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-counter.over-limit {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: bold;
|
||||||
|
background: rgba(220, 53, 69, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.loading-spinner {
|
||||||
|
font-size: 16px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Mobile responsiveness */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chat-app-container {
|
||||||
|
padding: 10px; /* Kleinere padding op mobiel */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages-area {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
max-width: 100%; /* Op mobiel volledige breedte gebruiken */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area {
|
||||||
|
max-width: 100%; /* Op mobiel volledige breedte gebruiken */
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-history-container {
|
||||||
|
padding: 15px;
|
||||||
|
max-width: 100%; /* Op mobiel volledige breedte gebruiken */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-container {
|
||||||
|
padding: 15px;
|
||||||
|
max-width: 100%; /* Op mobiel volledige breedte gebruiken */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
padding: 15px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
font-size: 16px; /* Voorkomt zoom op iOS */
|
||||||
|
padding: 10px 15px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-component-container {
|
||||||
|
max-width: 100%; /* Op mobiel volledige breedte gebruiken */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra small screens */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.chat-app-container {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages-area {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-history-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading states */
|
||||||
|
.chat-input.loading .message-input {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input.loading .action-btn {
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling voor webkit browsers */
|
||||||
|
.chat-messages::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0,0,0,0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verberg lege message bubbles tot er inhoud is */
|
||||||
|
.message-text:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tracker .status-icon.error {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tracker.error .progress-header {
|
||||||
|
background-color: rgba(244, 67, 54, 0.1);
|
||||||
|
border-color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zorg dat de progress tracker goed wordt weergegeven in een lege message bubble */
|
||||||
|
.message-content:has(.message-text:empty) .message-progress {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verberg de message content container als er geen inhoud is en de verwerking bezig is */
|
||||||
|
.message-content:has(.message-text:empty):not(:has(.message-progress.completed)):not(:has(.message-progress.error)) {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus binnen ChatInput voor toegankelijkheid */
|
||||||
|
.chat-input:focus-within {
|
||||||
|
box-shadow: 0 2px 20px rgba(0, 123, 255, 0.2);
|
||||||
|
border-color: rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions */
|
||||||
|
.chat-messages-area,
|
||||||
|
.chat-input-area {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages-area:hover,
|
||||||
|
.chat-input-area:hover {
|
||||||
|
box-shadow: 0 6px 25px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Message Bubbles Styling - Aangepast voor werkelijke template structuur */
|
||||||
|
|
||||||
|
/* Basis message container */
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 0 20px;
|
||||||
|
animation: messageSlideIn 0.3s ease-out;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User message alignment - rechts uitgelijnd */
|
||||||
|
.message.user {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AI/Bot message alignment - links uitgelijnd */
|
||||||
|
.message.ai,
|
||||||
|
.message.bot {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message content wrapper - dit wordt de bubble */
|
||||||
|
.message-content {
|
||||||
|
max-width: 70%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User message bubble styling */
|
||||||
|
.message.user .message-content {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
color: white;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AI/Bot message bubble styling */
|
||||||
|
.message.ai .message-content,
|
||||||
|
.message.bot .message-content {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #212529;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
margin-right: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message text content */
|
||||||
|
.message-text {
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text p + p {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #545b62;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special message types */
|
||||||
|
|
||||||
|
/* Form messages */
|
||||||
|
.form-message {
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-message .message-content {
|
||||||
|
max-width: 90%;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* System messages */
|
||||||
|
.system-message {
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(108, 117, 125, 0.1);
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 10px auto;
|
||||||
|
max-width: 80%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error messages */
|
||||||
|
.error-message {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 10px auto;
|
||||||
|
max-width: 80%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message reactions */
|
||||||
|
.message-reactions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction {
|
||||||
|
background: rgba(0,0,0,0.05);
|
||||||
|
border: 1px solid rgba(0,0,0,0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction:hover {
|
||||||
|
background: rgba(0,0,0,0.1);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image and file messages */
|
||||||
|
.message-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-image:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-caption {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-attachment {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0,0,0,0.03);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-download {
|
||||||
|
font-size: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-download:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects voor message bubbles */
|
||||||
|
.message-content:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bestaande animation en date-separator blijven hetzelfde */
|
||||||
|
@keyframes messageSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Empty state styling - blijft hetzelfde */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-subtext {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.message {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 85%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-content {
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai .message-content,
|
||||||
|
.message.bot .message-content {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.message {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 90%;
|
||||||
|
margin-left: 20px !important;
|
||||||
|
margin-right: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Tracker Styling */
|
||||||
|
.progress-tracker {
|
||||||
|
margin: 8px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tracker.expanded {
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tracker.completed {
|
||||||
|
background: rgba(155, 255, 155, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(0,0,0,0.02);
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header:hover {
|
||||||
|
background: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tracker.expanded .progress-header {
|
||||||
|
border-bottom-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.completed {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
font-size: 8px;
|
||||||
|
line-height: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.in-progress {
|
||||||
|
background: #007bff;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid #f3f3f3;
|
||||||
|
border-top: 2px solid #007bff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-toggle {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tracker.expanded .progress-toggle {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border-top: 1px solid #f5c6cb;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-content {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tracker.expanded .progress-content {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-content.single-line {
|
||||||
|
max-height: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-line {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||||
|
color: #6c757d;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-line:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animaties */
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Integratie met message bubbles */
|
||||||
|
.message.ai .progress-tracker,
|
||||||
|
.message.bot .progress-tracker {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.progress-tracker {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-line {
|
||||||
|
padding: 3px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-content.single-line {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user