Compare commits
6 Commits
v2.3.3-alf
...
v2.3.5-alf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67ceb57b79 | ||
|
|
23b49516cb | ||
|
|
9cc266b97f | ||
|
|
3f77871c4f | ||
|
|
199cf94cf2 | ||
|
|
c4dcd6a0d3 |
@@ -1,7 +1,7 @@
|
|||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
from ..extensions import db
|
from ..extensions import db
|
||||||
from .user import User, Tenant
|
from .user import User, Tenant, TenantMake
|
||||||
from .document import Embedding, Retriever
|
from .document import Embedding, Retriever
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ class Specialist(db.Model):
|
|||||||
tuning = db.Column(db.Boolean, nullable=True, default=False)
|
tuning = db.Column(db.Boolean, nullable=True, default=False)
|
||||||
configuration = db.Column(JSONB, nullable=True)
|
configuration = db.Column(JSONB, nullable=True)
|
||||||
arguments = db.Column(JSONB, nullable=True)
|
arguments = db.Column(JSONB, nullable=True)
|
||||||
|
active = db.Column(db.Boolean, nullable=True, default=True)
|
||||||
|
|
||||||
# Relationship to retrievers through the association table
|
# Relationship to retrievers through the association table
|
||||||
retrievers = db.relationship('SpecialistRetriever', backref='specialist', lazy=True,
|
retrievers = db.relationship('SpecialistRetriever', backref='specialist', lazy=True,
|
||||||
@@ -44,6 +45,21 @@ class Specialist(db.Model):
|
|||||||
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
||||||
updated_by = db.Column(db.Integer, db.ForeignKey(User.id))
|
updated_by = db.Column(db.Integer, db.ForeignKey(User.id))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Specialist {self.id}: {self.name}>"
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description,
|
||||||
|
'type': self.type,
|
||||||
|
'type_version': self.type_version,
|
||||||
|
'configuration': self.configuration,
|
||||||
|
'arguments': self.arguments,
|
||||||
|
'active': self.active,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class EveAIAsset(db.Model):
|
class EveAIAsset(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -222,6 +238,7 @@ class SpecialistMagicLink(db.Model):
|
|||||||
name = db.Column(db.String(50), nullable=False)
|
name = db.Column(db.String(50), nullable=False)
|
||||||
description = db.Column(db.Text, nullable=True)
|
description = db.Column(db.Text, nullable=True)
|
||||||
specialist_id = db.Column(db.Integer, db.ForeignKey(Specialist.id, ondelete='CASCADE'), nullable=False)
|
specialist_id = db.Column(db.Integer, db.ForeignKey(Specialist.id, ondelete='CASCADE'), nullable=False)
|
||||||
|
tenant_make_id = db.Column(db.Integer, db.ForeignKey(TenantMake.id, ondelete='CASCADE'), nullable=True)
|
||||||
magic_link_code = db.Column(db.String(55), nullable=False, unique=True)
|
magic_link_code = db.Column(db.String(55), nullable=False, unique=True)
|
||||||
|
|
||||||
valid_from = db.Column(db.DateTime, nullable=True)
|
valid_from = db.Column(db.DateTime, nullable=True)
|
||||||
@@ -236,3 +253,14 @@ class SpecialistMagicLink(db.Model):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<SpecialistMagicLink {self.specialist_id} {self.magic_link_code}>"
|
return f"<SpecialistMagicLink {self.specialist_id} {self.magic_link_code}>"
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description,
|
||||||
|
'magic_link_code': self.magic_link_code,
|
||||||
|
'valid_from': self.valid_from,
|
||||||
|
'valid_to': self.valid_to,
|
||||||
|
'specialist_args': self.specialist_args,
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,17 +28,19 @@ class Tenant(db.Model):
|
|||||||
|
|
||||||
# language information
|
# language information
|
||||||
default_language = db.Column(db.String(2), nullable=True)
|
default_language = db.Column(db.String(2), nullable=True)
|
||||||
allowed_languages = db.Column(ARRAY(sa.String(2)), nullable=True)
|
|
||||||
|
|
||||||
# Entitlements
|
# Entitlements
|
||||||
currency = db.Column(db.String(20), nullable=True)
|
currency = db.Column(db.String(20), nullable=True)
|
||||||
storage_dirty = db.Column(db.Boolean, nullable=True, default=False)
|
storage_dirty = db.Column(db.Boolean, nullable=True, default=False)
|
||||||
|
default_tenant_make_id = db.Column(db.Integer, db.ForeignKey('public.tenant_make.id'), nullable=True)
|
||||||
|
|
||||||
# Relations
|
# Relations
|
||||||
users = db.relationship('User', backref='tenant')
|
users = db.relationship('User', backref='tenant')
|
||||||
domains = db.relationship('TenantDomain', backref='tenant')
|
domains = db.relationship('TenantDomain', backref='tenant')
|
||||||
licenses = db.relationship('License', back_populates='tenant')
|
licenses = db.relationship('License', back_populates='tenant')
|
||||||
license_usages = db.relationship('LicenseUsage', backref='tenant')
|
license_usages = db.relationship('LicenseUsage', backref='tenant')
|
||||||
|
tenant_makes = db.relationship('TenantMake', backref='tenant', foreign_keys='TenantMake.tenant_id')
|
||||||
|
default_tenant_make = db.relationship('TenantMake', foreign_keys=[default_tenant_make_id], uselist=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_license(self):
|
def current_license(self):
|
||||||
@@ -60,8 +62,8 @@ class Tenant(db.Model):
|
|||||||
'timezone': self.timezone,
|
'timezone': self.timezone,
|
||||||
'type': self.type,
|
'type': self.type,
|
||||||
'default_language': self.default_language,
|
'default_language': self.default_language,
|
||||||
'allowed_languages': self.allowed_languages,
|
|
||||||
'currency': self.currency,
|
'currency': self.currency,
|
||||||
|
'default_tenant_make_id': self.default_tenant_make_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -179,7 +181,7 @@ class TenantMake(db.Model):
|
|||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
|
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
|
||||||
name = db.Column(db.String(50), nullable=False)
|
name = db.Column(db.String(50), nullable=False, unique=True)
|
||||||
description = db.Column(db.Text, nullable=True)
|
description = db.Column(db.Text, nullable=True)
|
||||||
active = db.Column(db.Boolean, nullable=False, default=True)
|
active = db.Column(db.Boolean, nullable=False, default=True)
|
||||||
website = db.Column(db.String(255), nullable=True)
|
website = db.Column(db.String(255), nullable=True)
|
||||||
@@ -194,6 +196,20 @@ class TenantMake(db.Model):
|
|||||||
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
||||||
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
|
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<TenantMake {self.id} for tenant {self.tenant_id}: {self.name}>"
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description,
|
||||||
|
'active': self.active,
|
||||||
|
'website': self.website,
|
||||||
|
'logo_url': self.logo_url,
|
||||||
|
'chat_customisation_options': self.chat_customisation_options,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Partner(db.Model):
|
class Partner(db.Model):
|
||||||
__bind_key__ = 'public'
|
__bind_key__ = 'public'
|
||||||
|
|||||||
@@ -220,3 +220,18 @@ class SpecialistServices:
|
|||||||
db.session.add(tool)
|
db.session.add(tool)
|
||||||
current_app.logger.info(f"Created tool {tool.id} of type {tool_type}")
|
current_app.logger.info(f"Created tool {tool.id} of type {tool_type}")
|
||||||
return tool
|
return tool
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_specialist_system_field(specialist_id, config_name, system_name):
|
||||||
|
"""Get the value of a system field in a specialist's configuration. Returns the actual value, or None."""
|
||||||
|
specialist = Specialist.query.get(specialist_id)
|
||||||
|
if not specialist:
|
||||||
|
raise ValueError(f"Specialist with ID {specialist_id} not found")
|
||||||
|
config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
|
||||||
|
if not config:
|
||||||
|
raise ValueError(f"No configuration found for {specialist.type} version {specialist.version}")
|
||||||
|
potential_field = config.get(config_name, None)
|
||||||
|
if potential_field:
|
||||||
|
if potential_field.type == 'system' and potential_field.system_name == system_name:
|
||||||
|
return specialist.configuration.get(config_name, None)
|
||||||
|
return None
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ Utility functions for chat customization.
|
|||||||
def get_default_chat_customisation(tenant_customisation=None):
|
def get_default_chat_customisation(tenant_customisation=None):
|
||||||
"""
|
"""
|
||||||
Get chat customization options with default values for missing options.
|
Get chat customization options with default values for missing options.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tenant_customization (dict, optional): The tenant's customization options.
|
tenant_customization (dict, optional): The tenant's customization options.
|
||||||
Defaults to None.
|
Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing all customization options with default values
|
dict: A dictionary containing all customization options with default values
|
||||||
for any missing options.
|
for any missing options.
|
||||||
@@ -21,22 +21,25 @@ def get_default_chat_customisation(tenant_customisation=None):
|
|||||||
'background_color': '#ffffff',
|
'background_color': '#ffffff',
|
||||||
'text_color': '#212529',
|
'text_color': '#212529',
|
||||||
'sidebar_color': '#f8f9fa',
|
'sidebar_color': '#f8f9fa',
|
||||||
'logo_url': None,
|
'sidebar_background': '#2c3e50',
|
||||||
'sidebar_text': None,
|
'gradient_start_color': '#f5f7fa',
|
||||||
|
'gradient_end_color': '#c3cfe2',
|
||||||
|
'markdown_background_color': 'transparent',
|
||||||
|
'markdown_text_color': '#ffffff',
|
||||||
|
'sidebar_markdown': '',
|
||||||
'welcome_message': 'Hello! How can I help you today?',
|
'welcome_message': 'Hello! How can I help you today?',
|
||||||
'team_info': []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# If no tenant customization is provided, return the defaults
|
# If no tenant customization is provided, return the defaults
|
||||||
if tenant_customisation is None:
|
if tenant_customisation is None:
|
||||||
return default_customisation
|
return default_customisation
|
||||||
|
|
||||||
# Start with the default customization
|
# Start with the default customization
|
||||||
customisation = default_customisation.copy()
|
customisation = default_customisation.copy()
|
||||||
|
|
||||||
# Update with tenant customization
|
# Update with tenant customization
|
||||||
for key, value in tenant_customisation.items():
|
for key, value in tenant_customisation.items():
|
||||||
if key in customisation:
|
if key in customisation:
|
||||||
customisation[key] = value
|
customisation[key] = value
|
||||||
|
|
||||||
return customisation
|
return customisation
|
||||||
|
|||||||
@@ -26,9 +26,34 @@ configuration:
|
|||||||
description: "Sidebar Color"
|
description: "Sidebar Color"
|
||||||
type: "color"
|
type: "color"
|
||||||
required: false
|
required: false
|
||||||
"sidebar_text":
|
"sidebar_background":
|
||||||
name: "Sidebar Text"
|
name: "Sidebar Background"
|
||||||
description: "Text to be shown in the sidebar"
|
description: "Sidebar Background Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"markdown_background_color":
|
||||||
|
name: "Markdown Background"
|
||||||
|
description: "Markdown Background Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"markdown_text_color":
|
||||||
|
name: "Markdown Text"
|
||||||
|
description: "Markdown Text Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"gradient_start_color":
|
||||||
|
name: "Gradient Start Color"
|
||||||
|
description: "Start Color for the gradient in the Chat Area"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"gradient_end_color":
|
||||||
|
name: "Gradient End Color"
|
||||||
|
description: "End Color for the gradient in the Chat Area"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"sidebar_markdown":
|
||||||
|
name: "Sidebar Markdown"
|
||||||
|
description: "Sidebar Markdown-formatted Text"
|
||||||
type: "text"
|
type: "text"
|
||||||
required: false
|
required: false
|
||||||
"welcome_message":
|
"welcome_message":
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: "1.1.0"
|
version: "1.2.0"
|
||||||
name: "Traicie Role Definition Specialist"
|
name: "Traicie Role Definition Specialist"
|
||||||
framework: "crewai"
|
framework: "crewai"
|
||||||
partner: "traicie"
|
partner: "traicie"
|
||||||
@@ -11,9 +11,9 @@ arguments:
|
|||||||
type: "str"
|
type: "str"
|
||||||
required: true
|
required: true
|
||||||
specialist_name:
|
specialist_name:
|
||||||
name: "Specialist Name"
|
name: "Chatbot Name"
|
||||||
description: "The name the specialist will be called upon"
|
description: "The name of the chatbot."
|
||||||
type: str
|
type: "str"
|
||||||
required: true
|
required: true
|
||||||
role_reference:
|
role_reference:
|
||||||
name: "Role Reference"
|
name: "Role Reference"
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -5,6 +5,31 @@ All notable changes to EveAI will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.3.5-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Chat Client Initialisation (based on SpecialistMagicLink code)
|
||||||
|
- Definition of framework for the chat_client (using vue.js)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Remove AllowedLanguages from Tenant
|
||||||
|
- Remove Tenant URL (now in Make)
|
||||||
|
- Adapt chat client customisation options
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Several Bugfixes to administrative app
|
||||||
|
|
||||||
|
## [2.3.4-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Introduction of Tenant Make
|
||||||
|
- Introduction of 'system' type for dynamic attributes
|
||||||
|
- Introduce Tenant Make to Traicie Specialists
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Enable Specialist 'activation' / 'deactivation'
|
||||||
|
- Unique constraints introduced for Catalog Name (tenant level) and make name (public level)
|
||||||
|
|
||||||
## [2.3.3-alfa]
|
## [2.3.3-alfa]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -8,7 +8,19 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_field_content(field, disabled=False, readonly=False, class='') %}
|
{% macro render_field_content(field, disabled=False, readonly=False, class='') %}
|
||||||
{% if field.type == 'BooleanField' %}
|
{# Check if this is a hidden input field, if so, render only the field without label #}
|
||||||
|
{{ debug_to_console("Field Class: ", field.widget.__class__.__name__) }}
|
||||||
|
{% if field.widget.__class__.__name__ == 'HiddenInput' %}
|
||||||
|
{{ debug_to_console("Hidden Field: ", "Detected") }}
|
||||||
|
{{ field(class="form-control " + class, disabled=disabled, readonly=readonly) }}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in field.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% elif field.type == 'BooleanField' %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
{{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }}
|
{{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from wtforms.validators import DataRequired, Length, Optional, URL, ValidationEr
|
|||||||
from flask_wtf.file import FileField, FileRequired
|
from flask_wtf.file import FileField, FileRequired
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from wtforms.widgets.core import HiddenInput
|
||||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||||
|
|
||||||
from common.extensions import cache_manager
|
from common.extensions import cache_manager
|
||||||
@@ -20,7 +21,7 @@ from .dynamic_form_base import DynamicFormBase
|
|||||||
def validate_catalog_name(form, field):
|
def validate_catalog_name(form, field):
|
||||||
# Controleer of een catalog met deze naam al bestaat
|
# Controleer of een catalog met deze naam al bestaat
|
||||||
existing_catalog = Catalog.query.filter_by(name=field.data).first()
|
existing_catalog = Catalog.query.filter_by(name=field.data).first()
|
||||||
if existing_catalog:
|
if existing_catalog and (not hasattr(form, 'id') or form.id.data != existing_catalog.id):
|
||||||
raise ValidationError(f'A Catalog with name "{field.data}" already exists. Choose another name.')
|
raise ValidationError(f'A Catalog with name "{field.data}" already exists. Choose another name.')
|
||||||
|
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ class CatalogForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class EditCatalogForm(DynamicFormBase):
|
class EditCatalogForm(DynamicFormBase):
|
||||||
|
id = IntegerField('ID', widget=HiddenInput())
|
||||||
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name])
|
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name])
|
||||||
description = TextAreaField('Description', validators=[Optional()])
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
|
||||||
@@ -188,7 +190,7 @@ class AddDocumentForm(DynamicFormBase):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.language.choices = [(language, language) for language in
|
self.language.choices = [(language, language) for language in
|
||||||
session.get('tenant').get('allowed_languages')]
|
current_app.config['SUPPORTED_LANGUAGES']]
|
||||||
if not self.language.data:
|
if not self.language.data:
|
||||||
self.language.data = session.get('tenant').get('default_language')
|
self.language.data = session.get('tenant').get('default_language')
|
||||||
|
|
||||||
@@ -208,7 +210,7 @@ class AddURLForm(DynamicFormBase):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.language.choices = [(language, language) for language in
|
self.language.choices = [(language, language) for language in
|
||||||
session.get('tenant').get('allowed_languages')]
|
current_app.config['SUPPORTED_LANGUAGES']]
|
||||||
if not self.language.data:
|
if not self.language.data:
|
||||||
self.language.data = session.get('tenant').get('default_language')
|
self.language.data = session.get('tenant').get('default_language')
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ from datetime import date
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
|
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
|
||||||
validators, ValidationError)
|
validators, ValidationError)
|
||||||
from flask import current_app, request
|
from flask import current_app, request, session
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from wtforms.fields.choices import SelectField
|
from wtforms.fields.choices import SelectField
|
||||||
from wtforms.fields.datetime import DateField
|
from wtforms.fields.datetime import DateField
|
||||||
from wtforms.fields.simple import ColorField
|
from wtforms.fields.simple import ColorField
|
||||||
|
|
||||||
|
from common.models.user import TenantMake
|
||||||
from common.utils.config_field_types import TaggingFields, json_to_patterns, patterns_to_json
|
from common.utils.config_field_types import TaggingFields, json_to_patterns, patterns_to_json
|
||||||
|
|
||||||
|
|
||||||
@@ -300,6 +302,22 @@ class DynamicFormBase(FlaskForm):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValidationError(f"Invalid ordered list: {str(e)}")
|
raise ValidationError(f"Invalid ordered list: {str(e)}")
|
||||||
|
|
||||||
|
def _get_system_field(self, system_name):
|
||||||
|
"""Get the field class and kwargs for a system field. Add system field cases as you need them."""
|
||||||
|
field_class = None
|
||||||
|
extra_classes = ''
|
||||||
|
field_kwargs = {}
|
||||||
|
match system_name:
|
||||||
|
case 'tenant_make':
|
||||||
|
field_class = SelectField
|
||||||
|
tenant_id = session.get('tenant').get('id')
|
||||||
|
makes = TenantMake.query.filter_by(tenant_id=tenant_id).all()
|
||||||
|
choices = [(make.name, make.name) for make in makes]
|
||||||
|
extra_classes = ''
|
||||||
|
field_kwargs = {'choices': choices}
|
||||||
|
|
||||||
|
return field_class, extra_classes, field_kwargs
|
||||||
|
|
||||||
def add_dynamic_fields(self, collection_name, config, initial_data=None):
|
def add_dynamic_fields(self, collection_name, config, initial_data=None):
|
||||||
"""Add dynamic fields to the form based on the configuration.
|
"""Add dynamic fields to the form based on the configuration.
|
||||||
|
|
||||||
@@ -357,11 +375,12 @@ class DynamicFormBase(FlaskForm):
|
|||||||
extra_classes = ['monospace-text', 'pattern-input']
|
extra_classes = ['monospace-text', 'pattern-input']
|
||||||
field_kwargs = {}
|
field_kwargs = {}
|
||||||
elif field_type == 'ordered_list':
|
elif field_type == 'ordered_list':
|
||||||
current_app.logger.debug(f"Adding ordered list field for {full_field_name}")
|
|
||||||
field_class = OrderedListField
|
field_class = OrderedListField
|
||||||
extra_classes = ''
|
extra_classes = ''
|
||||||
list_type = field_def.get('list_type', '')
|
list_type = field_def.get('list_type', '')
|
||||||
field_kwargs = {'list_type': list_type}
|
field_kwargs = {'list_type': list_type}
|
||||||
|
elif field_type == 'system':
|
||||||
|
field_class, extra_classes, field_kwargs = self._get_system_field(field_def.get('system_name', ''))
|
||||||
else:
|
else:
|
||||||
extra_classes = ''
|
extra_classes = ''
|
||||||
field_class = {
|
field_class = {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from wtforms_sqlalchemy.fields import QuerySelectMultipleField
|
|||||||
|
|
||||||
from common.models.document import Retriever
|
from common.models.document import Retriever
|
||||||
from common.models.interaction import EveAITool, Specialist
|
from common.models.interaction import EveAITool, Specialist
|
||||||
|
from common.models.user import TenantMake
|
||||||
from common.extensions import cache_manager
|
from common.extensions import cache_manager
|
||||||
from common.utils.form_assistants import validate_json
|
from common.utils.form_assistants import validate_json
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ def get_tools():
|
|||||||
|
|
||||||
class SpecialistForm(FlaskForm):
|
class SpecialistForm(FlaskForm):
|
||||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||||
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
|
||||||
retrievers = QuerySelectMultipleField(
|
retrievers = QuerySelectMultipleField(
|
||||||
'Retrievers',
|
'Retrievers',
|
||||||
@@ -34,7 +36,7 @@ class SpecialistForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
type = SelectField('Specialist Type', validators=[DataRequired()])
|
type = SelectField('Specialist Type', validators=[DataRequired()])
|
||||||
|
active = BooleanField('Active', validators=[Optional()], default=True)
|
||||||
tuning = BooleanField('Enable Specialist Tuning', default=False)
|
tuning = BooleanField('Enable Specialist Tuning', default=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -47,6 +49,7 @@ class SpecialistForm(FlaskForm):
|
|||||||
class EditSpecialistForm(DynamicFormBase):
|
class EditSpecialistForm(DynamicFormBase):
|
||||||
name = StringField('Name', validators=[DataRequired()])
|
name = StringField('Name', validators=[DataRequired()])
|
||||||
description = TextAreaField('Description', validators=[Optional()])
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
active = BooleanField('Active', validators=[Optional()], default=True)
|
||||||
|
|
||||||
retrievers = QuerySelectMultipleField(
|
retrievers = QuerySelectMultipleField(
|
||||||
'Retrievers',
|
'Retrievers',
|
||||||
@@ -148,7 +151,7 @@ class SpecialistMagicLinkForm(FlaskForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
specialists = Specialist.query.all()
|
specialists = Specialist.query.all()
|
||||||
# Dynamically populate the 'type' field using the constructor
|
# Dynamically populate the specialist field
|
||||||
self.specialist_id.choices = [(specialist.id, specialist.name) for specialist in specialists]
|
self.specialist_id.choices = [(specialist.id, specialist.name) for specialist in specialists]
|
||||||
|
|
||||||
|
|
||||||
@@ -159,6 +162,7 @@ class EditSpecialistMagicLinkForm(DynamicFormBase):
|
|||||||
render_kw={'readonly': True})
|
render_kw={'readonly': True})
|
||||||
specialist_id = IntegerField('Specialist', validators=[DataRequired()], render_kw={'readonly': True})
|
specialist_id = IntegerField('Specialist', validators=[DataRequired()], render_kw={'readonly': True})
|
||||||
specialist_name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True})
|
specialist_name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True})
|
||||||
|
tenant_make_id = SelectField('Tenant Make', validators=[Optional()], coerce=int)
|
||||||
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
|
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
|
||||||
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
|
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
|
||||||
|
|
||||||
@@ -174,5 +178,10 @@ class EditSpecialistMagicLinkForm(DynamicFormBase):
|
|||||||
else:
|
else:
|
||||||
self.specialist_name.data = ''
|
self.specialist_name.data = ''
|
||||||
|
|
||||||
|
# Dynamically populate the tenant_make field with None as first option
|
||||||
|
tenant_makes = TenantMake.query.all()
|
||||||
|
self.tenant_make_id.choices = [(0, 'None')] + [(make.id, make.name) for make in tenant_makes]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ def specialist():
|
|||||||
new_specialist.type = form.type.data
|
new_specialist.type = form.type.data
|
||||||
new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version(
|
new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version(
|
||||||
new_specialist.type)
|
new_specialist.type)
|
||||||
|
new_specialist.active = form.active.data
|
||||||
new_specialist.tuning = form.tuning.data
|
new_specialist.tuning = form.tuning.data
|
||||||
|
|
||||||
set_logging_information(new_specialist, dt.now(tz.utc))
|
set_logging_information(new_specialist, dt.now(tz.utc))
|
||||||
@@ -231,6 +232,7 @@ def edit_specialist(specialist_id):
|
|||||||
specialist.name = form.name.data
|
specialist.name = form.name.data
|
||||||
specialist.description = form.description.data
|
specialist.description = form.description.data
|
||||||
specialist.tuning = form.tuning.data
|
specialist.tuning = form.tuning.data
|
||||||
|
specialist.active = form.active.data
|
||||||
# Update the configuration dynamic fields
|
# Update the configuration dynamic fields
|
||||||
specialist.configuration = form.get_dynamic_data("configuration")
|
specialist.configuration = form.get_dynamic_data("configuration")
|
||||||
|
|
||||||
@@ -297,7 +299,7 @@ def specialists():
|
|||||||
|
|
||||||
# prepare table data
|
# prepare table data
|
||||||
rows = prepare_table_for_macro(the_specialists,
|
rows = prepare_table_for_macro(the_specialists,
|
||||||
[('id', ''), ('name', ''), ('type', '')])
|
[('id', ''), ('name', ''), ('type', ''), ('type_version', ''), ('active', ''),])
|
||||||
|
|
||||||
# Render the catalogs in a template
|
# Render the catalogs in a template
|
||||||
return render_template('interaction/specialists.html', rows=rows, pagination=pagination)
|
return render_template('interaction/specialists.html', rows=rows, pagination=pagination)
|
||||||
@@ -689,7 +691,7 @@ def specialist_magic_link():
|
|||||||
try:
|
try:
|
||||||
new_specialist_magic_link = SpecialistMagicLink()
|
new_specialist_magic_link = SpecialistMagicLink()
|
||||||
|
|
||||||
# Populate fields individually instead of using populate_obj (gives problem with QueryMultipleSelectField)
|
# Populate fields individually instead of using populate_obj
|
||||||
form.populate_obj(new_specialist_magic_link)
|
form.populate_obj(new_specialist_magic_link)
|
||||||
|
|
||||||
set_logging_information(new_specialist_magic_link, dt.now(tz.utc))
|
set_logging_information(new_specialist_magic_link, dt.now(tz.utc))
|
||||||
@@ -699,6 +701,14 @@ def specialist_magic_link():
|
|||||||
new_spec_ml_tenant.magic_link_code = new_specialist_magic_link.magic_link_code
|
new_spec_ml_tenant.magic_link_code = new_specialist_magic_link.magic_link_code
|
||||||
new_spec_ml_tenant.tenant_id = tenant_id
|
new_spec_ml_tenant.tenant_id = tenant_id
|
||||||
|
|
||||||
|
# Define the make valid for this magic link
|
||||||
|
make_id = SpecialistServices.get_specialist_system_field(new_specialist_magic_link.specialist_id,
|
||||||
|
"make", "tenant_make")
|
||||||
|
if make_id:
|
||||||
|
new_spec_ml_tenant.tenant_make_id = make_id
|
||||||
|
elif session.get('tenant').get('default_tenant_make_id'):
|
||||||
|
new_spec_ml_tenant.tenant_make_id = session.get('tenant').get('default_tenant_make_id')
|
||||||
|
|
||||||
db.session.add(new_specialist_magic_link)
|
db.session.add(new_specialist_magic_link)
|
||||||
db.session.add(new_spec_ml_tenant)
|
db.session.add(new_spec_ml_tenant)
|
||||||
|
|
||||||
@@ -731,12 +741,23 @@ def edit_specialist_magic_link(specialist_magic_link_id):
|
|||||||
|
|
||||||
form.add_dynamic_fields("arguments", specialist_config, specialist_ml.specialist_args)
|
form.add_dynamic_fields("arguments", specialist_config, specialist_ml.specialist_args)
|
||||||
|
|
||||||
|
# Set the tenant_make_id default value
|
||||||
|
if request.method == 'GET':
|
||||||
|
if specialist_ml.tenant_make_id is None:
|
||||||
|
form.tenant_make_id.data = 0
|
||||||
|
else:
|
||||||
|
form.tenant_make_id.data = specialist_ml.tenant_make_id
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Update the basic fields
|
# Update the basic fields
|
||||||
form.populate_obj(specialist_ml)
|
form.populate_obj(specialist_ml)
|
||||||
# Update the arguments dynamic fields
|
# Update the arguments dynamic fields
|
||||||
specialist_ml.specialist_args = form.get_dynamic_data("arguments")
|
specialist_ml.specialist_args = form.get_dynamic_data("arguments")
|
||||||
|
|
||||||
|
# Handle the tenant_make_id special case (0 = None)
|
||||||
|
if form.tenant_make_id.data == 0:
|
||||||
|
specialist_ml.tenant_make_id = None
|
||||||
|
|
||||||
# Update logging information
|
# Update logging information
|
||||||
update_logging_information(specialist_ml, dt.now(tz.utc))
|
update_logging_information(specialist_ml, dt.now(tz.utc))
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ from flask import current_app, session
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import (StringField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
|
from wtforms import (StringField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
|
||||||
SelectField, SelectMultipleField, FieldList, FormField, TextAreaField)
|
SelectField, SelectMultipleField, FieldList, FormField, TextAreaField)
|
||||||
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional
|
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, ValidationError
|
||||||
import pytz
|
import pytz
|
||||||
from flask_security import current_user
|
from flask_security import current_user
|
||||||
|
from wtforms.widgets.core import HiddenInput
|
||||||
|
|
||||||
|
from common.models.user import TenantMake
|
||||||
from common.services.user import UserServices
|
from common.services.user import UserServices
|
||||||
from config.type_defs.service_types import SERVICE_TYPES
|
from config.type_defs.service_types import SERVICE_TYPES
|
||||||
from eveai_app.views.dynamic_form_base import DynamicFormBase
|
from eveai_app.views.dynamic_form_base import DynamicFormBase
|
||||||
@@ -18,7 +20,6 @@ class TenantForm(FlaskForm):
|
|||||||
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
||||||
# language fields
|
# language fields
|
||||||
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
|
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
|
||||||
allowed_languages = SelectMultipleField('Allowed Languages', choices=[], validators=[DataRequired()])
|
|
||||||
# invoicing fields
|
# invoicing fields
|
||||||
currency = SelectField('Currency', choices=[], validators=[DataRequired()])
|
currency = SelectField('Currency', choices=[], validators=[DataRequired()])
|
||||||
# Timezone
|
# Timezone
|
||||||
@@ -33,13 +34,56 @@ class TenantForm(FlaskForm):
|
|||||||
super(TenantForm, self).__init__(*args, **kwargs)
|
super(TenantForm, self).__init__(*args, **kwargs)
|
||||||
# initialise language fields
|
# initialise language fields
|
||||||
self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
|
self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
|
||||||
self.allowed_languages.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
|
|
||||||
# initialise currency field
|
# initialise currency field
|
||||||
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
|
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
|
||||||
# initialise timezone
|
# initialise timezone
|
||||||
self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones]
|
self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones]
|
||||||
# Initialize fallback algorithms
|
# Initialize fallback algorithms
|
||||||
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
|
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
|
||||||
|
# Initialize default tenant make choices
|
||||||
|
tenant_id = session.get('tenant', {}).get('id') if 'tenant' in session else None
|
||||||
|
# Show field only for Super Users with partner in session
|
||||||
|
if not current_user.has_roles('Super User') or 'partner' not in session:
|
||||||
|
self._fields.pop('assign_to_partner', None)
|
||||||
|
|
||||||
|
|
||||||
|
class EditTenantForm(FlaskForm):
|
||||||
|
id = IntegerField('ID', widget=HiddenInput())
|
||||||
|
name = StringField('Name', validators=[DataRequired(), Length(max=80)])
|
||||||
|
code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True})
|
||||||
|
type = SelectField('Tenant Type', validators=[Optional()], default='Active')
|
||||||
|
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
||||||
|
# language fields
|
||||||
|
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
|
||||||
|
# invoicing fields
|
||||||
|
currency = SelectField('Currency', choices=[], validators=[DataRequired()])
|
||||||
|
# Timezone
|
||||||
|
timezone = SelectField('Timezone', choices=[], validators=[DataRequired()])
|
||||||
|
# Default tenant make
|
||||||
|
default_tenant_make_id = SelectField('Default Tenant Make', choices=[], validators=[Optional()])
|
||||||
|
|
||||||
|
# For Super Users only - Allow to assign the tenant to the partner
|
||||||
|
assign_to_partner = BooleanField('Assign to Partner', default=False)
|
||||||
|
# Embedding variables
|
||||||
|
submit = SubmitField('Submit')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(EditTenantForm, self).__init__(*args, **kwargs)
|
||||||
|
# initialise language fields
|
||||||
|
self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
|
||||||
|
# initialise currency field
|
||||||
|
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
|
||||||
|
# initialise timezone
|
||||||
|
self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones]
|
||||||
|
# Initialize fallback algorithms
|
||||||
|
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
|
||||||
|
# Initialize default tenant make choices
|
||||||
|
tenant_id = self.id.data
|
||||||
|
if tenant_id:
|
||||||
|
tenant_makes = TenantMake.query.filter_by(tenant_id=tenant_id, active=True).all()
|
||||||
|
self.default_tenant_make_id.choices = [(str(make.id), make.name) for make in tenant_makes]
|
||||||
|
# Add empty choice
|
||||||
|
self.default_tenant_make_id.choices.insert(0, ('', 'Geen'))
|
||||||
# Show field only for Super Users with partner in session
|
# Show field only for Super Users with partner in session
|
||||||
if not current_user.has_roles('Super User') or 'partner' not in session:
|
if not current_user.has_roles('Super User') or 'partner' not in session:
|
||||||
self._fields.pop('assign_to_partner', None)
|
self._fields.pop('assign_to_partner', None)
|
||||||
@@ -132,8 +176,30 @@ class EditTenantProjectForm(FlaskForm):
|
|||||||
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]
|
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_make_name(form, field):
|
||||||
|
# Check if tenant_make already exists in the database
|
||||||
|
existing_make = TenantMake.query.filter_by(name=field.data).first()
|
||||||
|
|
||||||
|
if existing_make:
|
||||||
|
current_app.logger.debug(f'Existing make: {existing_make.id}')
|
||||||
|
current_app.logger.debug(f'Form has id: {hasattr(form, 'id')}')
|
||||||
|
if hasattr(form, 'id'):
|
||||||
|
current_app.logger.debug(f'Form has id: {form.id.data}')
|
||||||
|
if existing_make:
|
||||||
|
if not hasattr(form, 'id') or form.id.data != existing_make.id:
|
||||||
|
raise ValidationError(f'A Make with name "{field.data}" already exists. Choose another name.')
|
||||||
|
|
||||||
|
|
||||||
class TenantMakeForm(DynamicFormBase):
|
class TenantMakeForm(DynamicFormBase):
|
||||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name])
|
||||||
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
active = BooleanField('Active', validators=[Optional()], default=True)
|
||||||
|
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
||||||
|
logo_url = StringField('Logo URL', validators=[Optional(), Length(max=255)])
|
||||||
|
|
||||||
|
class EditTenantMakeForm(DynamicFormBase):
|
||||||
|
id = IntegerField('ID', widget=HiddenInput())
|
||||||
|
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name])
|
||||||
description = TextAreaField('Description', validators=[Optional()])
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
active = BooleanField('Active', validators=[Optional()], default=True)
|
active = BooleanField('Active', validators=[Optional()], default=True)
|
||||||
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime as dt, timezone as tz
|
from datetime import datetime as dt, timezone as tz
|
||||||
from flask import request, redirect, flash, render_template, Blueprint, session, current_app
|
from flask import request, redirect, flash, render_template, Blueprint, session, current_app
|
||||||
@@ -11,7 +12,7 @@ from common.utils.dynamic_field_utils import create_default_config_from_type_con
|
|||||||
from common.utils.security_utils import send_confirmation_email, send_reset_email
|
from common.utils.security_utils import send_confirmation_email, send_reset_email
|
||||||
from config.type_defs.service_types import SERVICE_TYPES
|
from config.type_defs.service_types import SERVICE_TYPES
|
||||||
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
|
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
|
||||||
TenantProjectForm, EditTenantProjectForm, TenantMakeForm
|
TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm, EditTenantMakeForm
|
||||||
from common.utils.database import Database
|
from common.utils.database import Database
|
||||||
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
|
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
|
||||||
from common.utils.simple_encryption import generate_api_key
|
from common.utils.simple_encryption import generate_api_key
|
||||||
@@ -111,12 +112,18 @@ def tenant():
|
|||||||
@roles_accepted('Super User', 'Partner Admin')
|
@roles_accepted('Super User', 'Partner Admin')
|
||||||
def edit_tenant(tenant_id):
|
def edit_tenant(tenant_id):
|
||||||
tenant = Tenant.query.get_or_404(tenant_id) # This will return a 404 if no tenant is found
|
tenant = Tenant.query.get_or_404(tenant_id) # This will return a 404 if no tenant is found
|
||||||
form = TenantForm(obj=tenant)
|
form = EditTenantForm(obj=tenant)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Populate the tenant with form data
|
# Populate the tenant with form data
|
||||||
form.populate_obj(tenant)
|
form.populate_obj(tenant)
|
||||||
|
|
||||||
|
# Convert default_tenant_make_id to integer if not empty
|
||||||
|
if form.default_tenant_make_id.data:
|
||||||
|
tenant.default_tenant_make_id = int(form.default_tenant_make_id.data)
|
||||||
|
else:
|
||||||
|
tenant.default_tenant_make_id = None
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Tenant updated successfully.', 'success')
|
flash('Tenant updated successfully.', 'success')
|
||||||
if session.get('tenant'):
|
if session.get('tenant'):
|
||||||
@@ -460,8 +467,18 @@ def edit_tenant_domain(tenant_domain_id):
|
|||||||
def tenant_overview():
|
def tenant_overview():
|
||||||
tenant_id = session['tenant']['id']
|
tenant_id = session['tenant']['id']
|
||||||
tenant = Tenant.query.get_or_404(tenant_id)
|
tenant = Tenant.query.get_or_404(tenant_id)
|
||||||
form = TenantForm(obj=tenant)
|
form = EditTenantForm(obj=tenant)
|
||||||
return render_template('user/tenant_overview.html', form=form)
|
|
||||||
|
# Zet de waarde van default_tenant_make_id
|
||||||
|
if tenant.default_tenant_make_id:
|
||||||
|
form.default_tenant_make_id.data = str(tenant.default_tenant_make_id)
|
||||||
|
|
||||||
|
# Haal de naam van de default make op als deze bestaat
|
||||||
|
default_make_name = None
|
||||||
|
if tenant.default_tenant_make:
|
||||||
|
default_make_name = tenant.default_tenant_make.name
|
||||||
|
|
||||||
|
return render_template('user/tenant_overview.html', form=form, default_make_name=default_make_name)
|
||||||
|
|
||||||
|
|
||||||
@user_bp.route('/tenant_project', methods=['GET', 'POST'])
|
@user_bp.route('/tenant_project', methods=['GET', 'POST'])
|
||||||
@@ -627,15 +644,17 @@ def delete_tenant_project(tenant_project_id):
|
|||||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
def tenant_make():
|
def tenant_make():
|
||||||
form = TenantMakeForm()
|
form = TenantMakeForm()
|
||||||
|
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
|
||||||
|
default_customisation_options = create_default_config_from_type_config(customisation_config["configuration"])
|
||||||
|
form.add_dynamic_fields("configuration", customisation_config, default_customisation_options)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
tenant_id = session['tenant']['id']
|
tenant_id = session['tenant']['id']
|
||||||
new_tenant_make = TenantMake()
|
new_tenant_make = TenantMake()
|
||||||
form.populate_obj(new_tenant_make)
|
form.populate_obj(new_tenant_make)
|
||||||
new_tenant_make.tenant_id = tenant_id
|
new_tenant_make.tenant_id = tenant_id
|
||||||
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
|
customisation_options = form.get_dynamic_data("configuration")
|
||||||
new_tenant_make.chat_customisation_options = create_default_config_from_type_config(
|
new_tenant_make.chat_customisation_options = json.dumps(customisation_options)
|
||||||
customisation_config["configuration"])
|
|
||||||
form.add_dynamic_fields("configuration", customisation_config, new_tenant_make.chat_customisation_options)
|
|
||||||
set_logging_information(new_tenant_make, dt.now(tz.utc))
|
set_logging_information(new_tenant_make, dt.now(tz.utc))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -660,7 +679,8 @@ def tenant_makes():
|
|||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
per_page = request.args.get('per_page', 10, type=int)
|
per_page = request.args.get('per_page', 10, type=int)
|
||||||
|
|
||||||
query = TenantMake.query.order_by(TenantMake.id)
|
tenant_id = session['tenant']['id']
|
||||||
|
query = TenantMake.query.filter_by(tenant_id=tenant_id).order_by(TenantMake.id)
|
||||||
|
|
||||||
pagination = query.paginate(page=page, per_page=per_page)
|
pagination = query.paginate(page=page, per_page=per_page)
|
||||||
tenant_makes = pagination.items
|
tenant_makes = pagination.items
|
||||||
@@ -681,7 +701,7 @@ def edit_tenant_make(tenant_make_id):
|
|||||||
tenant_make = TenantMake.query.get_or_404(tenant_make_id)
|
tenant_make = TenantMake.query.get_or_404(tenant_make_id)
|
||||||
|
|
||||||
# Create form instance with the tenant make
|
# Create form instance with the tenant make
|
||||||
form = TenantMakeForm(request.form, obj=tenant_make)
|
form = EditTenantMakeForm(request.form, obj=tenant_make)
|
||||||
|
|
||||||
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
|
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
|
||||||
form.add_dynamic_fields("configuration", customisation_config, tenant_make.chat_customisation_options)
|
form.add_dynamic_fields("configuration", customisation_config, tenant_make.chat_customisation_options)
|
||||||
@@ -724,6 +744,28 @@ def handle_tenant_make_selection():
|
|||||||
|
|
||||||
if action == 'edit_tenant_make':
|
if action == 'edit_tenant_make':
|
||||||
return redirect(prefixed_url_for('user_bp.edit_tenant_make', tenant_make_id=tenant_make_id))
|
return redirect(prefixed_url_for('user_bp.edit_tenant_make', tenant_make_id=tenant_make_id))
|
||||||
|
elif action == 'set_as_default':
|
||||||
|
# Set this make as the default for the tenant
|
||||||
|
tenant_id = session['tenant']['id']
|
||||||
|
tenant = Tenant.query.get(tenant_id)
|
||||||
|
tenant.default_tenant_make_id = tenant_make_id
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
flash(f'Default tenant make updated successfully.', 'success')
|
||||||
|
# Update session data if necessary
|
||||||
|
if 'tenant' in session:
|
||||||
|
session['tenant'] = tenant.to_dict()
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f'Failed to update default tenant make. Error: {str(e)}', 'danger')
|
||||||
|
current_app.logger.error(f'Failed to update default tenant make. Error: {str(e)}')
|
||||||
|
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_makes'))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def reset_uniquifier(user):
|
def reset_uniquifier(user):
|
||||||
security.datastore.set_uniquifier(user)
|
security.datastore.set_uniquifier(user)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from flask import Flask, jsonify
|
|
||||||
|
from flask import Flask, jsonify, request
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
import logging.config
|
import logging.config
|
||||||
|
|
||||||
@@ -74,6 +75,13 @@ def create_app(config_file=None):
|
|||||||
|
|
||||||
app.logger.info(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})")
|
app.logger.info(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})")
|
||||||
app.logger.info("-------------------------------------------------------------------------------------------------")
|
app.logger.info("-------------------------------------------------------------------------------------------------")
|
||||||
|
|
||||||
|
# @app.before_request
|
||||||
|
# def app_before_request():
|
||||||
|
# app.logger.debug(f'App before request: {request.path} ===== Method: {request.method} =====')
|
||||||
|
# app.logger.debug(f'Full URL: {request.url}')
|
||||||
|
# app.logger.debug(f'Endpoint: {request.endpoint}')
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,28 +4,153 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}EveAI Chat{% endblock %}</title>
|
<title>{% block title %}EveAI Chat{% endblock %}</title>
|
||||||
|
|
||||||
<!-- CSS -->
|
<!-- CSS -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}">
|
||||||
|
|
||||||
|
<!-- Vue.js -->
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
|
|
||||||
|
<!-- Markdown parser for explanation text -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
|
||||||
<!-- Custom theme colors from tenant settings -->
|
<!-- Custom theme colors from tenant settings -->
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary-color: {{ customization.primary_color|default('#007bff') }};
|
--primary-color: {{ customisation.primary_color|default('#007bff') }};
|
||||||
--secondary-color: {{ customization.secondary_color|default('#6c757d') }};
|
--secondary-color: {{ customisation.secondary_color|default('#6c757d') }};
|
||||||
--background-color: {{ customization.background_color|default('#ffffff') }};
|
--background-color: {{ customisation.background_color|default('#ffffff') }};
|
||||||
--text-color: {{ customization.text_color|default('#212529') }};
|
--text-color: {{ customisation.text_color|default('#212529') }};
|
||||||
--sidebar-color: {{ customization.sidebar_color|default('#f8f9fa') }};
|
--sidebar-color: {{ customisation.sidebar_color|default('#f8f9fa') }};
|
||||||
|
--sidebar-background: {{ customisation.sidebar_background|default('#2c3e50') }};
|
||||||
|
--gradient-start-color: {{ customisation.gradient_start_color|default('#f5f7fa') }};
|
||||||
|
--gradient-end-color: {{ customisation.gradient_end_color|default('#c3cfe2') }};
|
||||||
|
--markdown-background-color: {{ customisation.markdown_background_color|default('transparent') }};
|
||||||
|
--markdown-text-color: {{ customisation.markdown_text_color|default('#ffffff') }};
|
||||||
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 300px;
|
||||||
|
background-color: var(--sidebar-background);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-make-name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-explanation {
|
||||||
|
margin-top: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--markdown-background-color);
|
||||||
|
color: var(--markdown-text-color);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure all elements in the markdown content inherit the text color */
|
||||||
|
.sidebar-explanation * {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style links in the markdown content */
|
||||||
|
.sidebar-explanation a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start-color), var(--gradient-end-color));
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div id="app" class="app-container">
|
||||||
{% block content %}{% endblock %}
|
<!-- Left sidebar - never changes -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<img src="{{ tenant_make.logo_url|default('') }}" alt="{{ tenant_make.name|default('Logo') }}">
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-make-name">
|
||||||
|
{{ tenant_make.name|default('') }}
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-explanation" v-html="compiledExplanation"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right content area - contains the chat client -->
|
||||||
|
<div class="content-area">
|
||||||
|
<div class="chat-container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
Vue.createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
explanation: `{{ customisation.sidebar_markdown|default('') }}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
compiledExplanation: function() {
|
||||||
|
// Handle different versions of the marked library
|
||||||
|
if (typeof marked === 'function') {
|
||||||
|
return marked(this.explanation);
|
||||||
|
} else if (marked && typeof marked.parse === 'function') {
|
||||||
|
return marked.parse(this.explanation);
|
||||||
|
} else {
|
||||||
|
console.error('Marked library not properly loaded');
|
||||||
|
return this.explanation; // Fallback to raw text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#app');
|
||||||
|
</script>
|
||||||
|
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,13 +3,32 @@ from flask import Blueprint, render_template, request, session, current_app, jso
|
|||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from common.extensions import db
|
from common.extensions import db
|
||||||
from common.models.user import Tenant, SpecialistMagicLinkTenant
|
from common.models.user import Tenant, SpecialistMagicLinkTenant, TenantMake
|
||||||
from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction
|
from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction
|
||||||
from common.services.interaction.specialist_services import SpecialistServices
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
from common.utils.database import Database
|
from common.utils.database import Database
|
||||||
from common.utils.chat_utils import get_default_chat_customisation
|
from common.utils.chat_utils import get_default_chat_customisation
|
||||||
|
|
||||||
chat_bp = Blueprint('chat', __name__)
|
chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat')
|
||||||
|
|
||||||
|
@chat_bp.before_request
|
||||||
|
def log_before_request():
|
||||||
|
current_app.logger.debug(f'Before request: {request.path} =====================================')
|
||||||
|
|
||||||
|
|
||||||
|
@chat_bp.after_request
|
||||||
|
def log_after_request(response):
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# @chat_bp.before_request
|
||||||
|
# def before_request():
|
||||||
|
# try:
|
||||||
|
# mw_before_request()
|
||||||
|
# except Exception as e:
|
||||||
|
# current_app.logger.error(f'Error switching schema in Document Blueprint: {e}')
|
||||||
|
# raise
|
||||||
|
|
||||||
|
|
||||||
@chat_bp.route('/')
|
@chat_bp.route('/')
|
||||||
def index():
|
def index():
|
||||||
@@ -31,14 +50,12 @@ def chat(magic_link_code):
|
|||||||
current_app.logger.error(f"Invalid magic link code: {magic_link_code}")
|
current_app.logger.error(f"Invalid magic link code: {magic_link_code}")
|
||||||
return render_template('error.html', message="Invalid magic link code.")
|
return render_template('error.html', message="Invalid magic link code.")
|
||||||
|
|
||||||
tenant_id = magic_link_tenant.tenant_id
|
|
||||||
|
|
||||||
# Get tenant information
|
# Get tenant information
|
||||||
|
tenant_id = magic_link_tenant.tenant_id
|
||||||
tenant = Tenant.query.get(tenant_id)
|
tenant = Tenant.query.get(tenant_id)
|
||||||
if not tenant:
|
if not tenant:
|
||||||
current_app.logger.error(f"Tenant not found for ID: {tenant_id}")
|
current_app.logger.error(f"Tenant not found for ID: {tenant_id}")
|
||||||
return render_template('error.html', message="Tenant not found.")
|
return render_template('error.html', message="Tenant not found.")
|
||||||
|
|
||||||
# Switch to tenant schema
|
# Switch to tenant schema
|
||||||
Database(tenant_id).switch_schema()
|
Database(tenant_id).switch_schema()
|
||||||
|
|
||||||
@@ -48,6 +65,12 @@ def chat(magic_link_code):
|
|||||||
current_app.logger.error(f"Specialist magic link not found in tenant schema: {tenant_id}")
|
current_app.logger.error(f"Specialist magic link not found in tenant schema: {tenant_id}")
|
||||||
return render_template('error.html', message="Specialist configuration not found.")
|
return render_template('error.html', message="Specialist configuration not found.")
|
||||||
|
|
||||||
|
# Get relevant TenantMake
|
||||||
|
tenant_make = TenantMake.query.get(specialist_ml.tenant_make_id)
|
||||||
|
if not tenant_make:
|
||||||
|
current_app.logger.error(f"Tenant make not found: {specialist_ml.tenant_make_id}")
|
||||||
|
return render_template('error.html', message="Tenant make not found.")
|
||||||
|
|
||||||
# Get specialist details
|
# Get specialist details
|
||||||
specialist = Specialist.query.get(specialist_ml.specialist_id)
|
specialist = Specialist.query.get(specialist_ml.specialist_id)
|
||||||
if not specialist:
|
if not specialist:
|
||||||
@@ -55,21 +78,22 @@ def chat(magic_link_code):
|
|||||||
return render_template('error.html', message="Specialist not found.")
|
return render_template('error.html', message="Specialist not found.")
|
||||||
|
|
||||||
# Store necessary information in session
|
# Store necessary information in session
|
||||||
session['tenant_id'] = tenant_id
|
session['tenant'] = tenant.to_dict()
|
||||||
session['specialist_id'] = specialist_ml.specialist_id
|
session['specialist'] = specialist.to_dict()
|
||||||
session['specialist_args'] = specialist_ml.specialist_args or {}
|
session['magic_link'] = specialist_ml.to_dict()
|
||||||
session['magic_link_code'] = magic_link_code
|
session['tenant_make'] = tenant_make.to_dict()
|
||||||
|
|
||||||
# Get customisation options with defaults
|
# Get customisation options with defaults
|
||||||
customisation = get_default_chat_customisation(tenant.chat_customisation_options)
|
customisation = get_default_chat_customisation(tenant_make.chat_customisation_options)
|
||||||
|
|
||||||
# Start a new chat session
|
# Start a new chat session
|
||||||
session['chat_session_id'] = SpecialistServices.start_session()
|
session['chat_session_id'] = SpecialistServices.start_session()
|
||||||
|
|
||||||
return render_template('chat.html',
|
return render_template('chat.html',
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
specialist=specialist,
|
tenant_make=tenant_make,
|
||||||
customisation=customisation)
|
specialist=specialist,
|
||||||
|
customisation=customisation)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)
|
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from os import wait
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from crewai.flow.flow import start, listen, and_
|
||||||
|
from flask import current_app
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from common.extensions import db
|
||||||
|
from common.models.user import Tenant
|
||||||
|
from common.models.interaction import Specialist
|
||||||
|
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
|
||||||
|
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
|
||||||
|
from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments
|
||||||
|
from eveai_chat_workers.outputs.traicie.competencies.competencies_v1_1 import Competencies
|
||||||
|
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
|
||||||
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
||||||
|
"""
|
||||||
|
type: TRAICIE_ROLE_DEFINITION_SPECIALIST
|
||||||
|
type_version: 1.0
|
||||||
|
Traicie Role Definition Specialist Executor class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs):
|
||||||
|
self.role_definition_crew = None
|
||||||
|
|
||||||
|
super().__init__(tenant_id, specialist_id, session_id, task_id)
|
||||||
|
|
||||||
|
# Load the Tenant & set language
|
||||||
|
self.tenant = Tenant.query.get_or_404(tenant_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> str:
|
||||||
|
return "TRAICIE_ROLE_DEFINITION_SPECIALIST"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type_version(self) -> str:
|
||||||
|
return "1.3"
|
||||||
|
|
||||||
|
def _config_task_agents(self):
|
||||||
|
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
|
||||||
|
|
||||||
|
def _config_pydantic_outputs(self):
|
||||||
|
self._add_pydantic_output("traicie_get_competencies_task", Competencies, "competencies")
|
||||||
|
|
||||||
|
def _instantiate_specialist(self):
|
||||||
|
verbose = self.tuning
|
||||||
|
|
||||||
|
role_definition_agents = [self.traicie_hr_bp_agent]
|
||||||
|
role_definition_tasks = [self.traicie_get_competencies_task]
|
||||||
|
self.role_definition_crew = EveAICrewAICrew(
|
||||||
|
self,
|
||||||
|
"Role Definition Crew",
|
||||||
|
agents=role_definition_agents,
|
||||||
|
tasks=role_definition_tasks,
|
||||||
|
verbose=verbose,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.flow = RoleDefinitionFlow(
|
||||||
|
self,
|
||||||
|
self.role_definition_crew
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
|
||||||
|
self.log_tuning("Traicie Role Definition Specialist execution started", {})
|
||||||
|
|
||||||
|
flow_inputs = {
|
||||||
|
"vacancy_text": arguments.vacancy_text,
|
||||||
|
"role_name": arguments.role_name,
|
||||||
|
'role_reference': arguments.role_reference,
|
||||||
|
}
|
||||||
|
|
||||||
|
flow_results = self.flow.kickoff(inputs=flow_inputs)
|
||||||
|
|
||||||
|
flow_state = self.flow.state
|
||||||
|
|
||||||
|
results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
|
||||||
|
if flow_state.competencies:
|
||||||
|
results.competencies = flow_state.competencies
|
||||||
|
|
||||||
|
self.create_selection_specialist(arguments, flow_state.competencies)
|
||||||
|
|
||||||
|
self.log_tuning(f"Traicie Role Definition Specialist execution ended", {"Results": results.model_dump()})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def create_selection_specialist(self, arguments: SpecialistArguments, competencies: List[ListItem]):
|
||||||
|
"""This method creates a new TRAICIE_SELECTION_SPECIALIST specialist with the given competencies."""
|
||||||
|
current_app.logger.info(f"Creating selection with arguments: {arguments.model_dump()}")
|
||||||
|
selection_comptencies = []
|
||||||
|
for competency in competencies:
|
||||||
|
selection_competency = {
|
||||||
|
"title": competency.title,
|
||||||
|
"description": competency.description,
|
||||||
|
"assess": True,
|
||||||
|
"is_knockout": False,
|
||||||
|
}
|
||||||
|
selection_comptencies.append(selection_competency)
|
||||||
|
|
||||||
|
selection_config = {
|
||||||
|
"name": arguments.specialist_name,
|
||||||
|
"competencies": selection_comptencies,
|
||||||
|
"tone_of_voice": "Professional & Neutral",
|
||||||
|
"language_level": "Standard",
|
||||||
|
"role_reference": arguments.role_reference,
|
||||||
|
"make": arguments.make,
|
||||||
|
}
|
||||||
|
name = arguments.role_name
|
||||||
|
if len(name) > 50:
|
||||||
|
name = name[:47] + "..."
|
||||||
|
|
||||||
|
new_specialist = Specialist(
|
||||||
|
name=name,
|
||||||
|
description=f"Specialist for {arguments.role_name} role",
|
||||||
|
type="TRAICIE_SELECTION_SPECIALIST",
|
||||||
|
type_version="1.1",
|
||||||
|
tuning=False,
|
||||||
|
configuration=selection_config,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
db.session.add(new_specialist)
|
||||||
|
db.session.commit()
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"Error creating selection specialist: {str(e)}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
SpecialistServices.initialize_specialist(new_specialist.id, "TRAICIE_SELECTION_SPECIALIST", "1.0")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionSpecialistInput(BaseModel):
|
||||||
|
role_name: str = Field(..., alias="role_name")
|
||||||
|
role_reference: Optional[str] = Field(..., alias="role_reference")
|
||||||
|
vacancy_text: Optional[str] = Field(None, alias="vacancy_text")
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionSpecialistResult(SpecialistResult):
|
||||||
|
competencies: Optional[List[ListItem]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefFlowState(EveAIFlowState):
|
||||||
|
"""Flow state for Traicie Role Definition specialist that automatically updates from task outputs"""
|
||||||
|
input: Optional[RoleDefinitionSpecialistInput] = None
|
||||||
|
competencies: Optional[List[ListItem]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionFlow(EveAICrewAIFlow[RoleDefFlowState]):
|
||||||
|
def __init__(self,
|
||||||
|
specialist_executor: CrewAIBaseSpecialistExecutor,
|
||||||
|
role_definitiion_crew: EveAICrewAICrew,
|
||||||
|
**kwargs):
|
||||||
|
super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs)
|
||||||
|
self.specialist_executor = specialist_executor
|
||||||
|
self.role_definition_crew = role_definitiion_crew
|
||||||
|
self.exception_raised = False
|
||||||
|
|
||||||
|
@start()
|
||||||
|
def process_inputs(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@listen(process_inputs)
|
||||||
|
async def execute_role_definition (self):
|
||||||
|
inputs = self.state.input.model_dump()
|
||||||
|
try:
|
||||||
|
current_app.logger.debug("In execute_role_definition")
|
||||||
|
crew_output = await self.role_definition_crew.kickoff_async(inputs=inputs)
|
||||||
|
# Unfortunately, crew_output will only contain the output of the latest task.
|
||||||
|
# As we will only take into account the flow state, we need to ensure both competencies and criteria
|
||||||
|
# are copies to the flow state.
|
||||||
|
update = {}
|
||||||
|
for task in self.role_definition_crew.tasks:
|
||||||
|
current_app.logger.debug(f"Task {task.name} output:\n{task.output}")
|
||||||
|
if task.name == "traicie_get_competencies_task":
|
||||||
|
# update["competencies"] = task.output.pydantic.competencies
|
||||||
|
self.state.competencies = task.output.pydantic.competencies
|
||||||
|
# crew_output.pydantic = crew_output.pydantic.model_copy(update=update)
|
||||||
|
current_app.logger.debug(f"State after execute_role_definition: {self.state}")
|
||||||
|
current_app.logger.debug(f"State dump after execute_role_definition: {self.state.model_dump()}")
|
||||||
|
return crew_output
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"CREW execute_role_definition Kickoff Error: {str(e)}")
|
||||||
|
self.exception_raised = True
|
||||||
|
raise e
|
||||||
|
|
||||||
|
async def kickoff_async(self, inputs=None):
|
||||||
|
current_app.logger.debug(f"Async kickoff {self.name}")
|
||||||
|
current_app.logger.debug(f"Inputs: {inputs}")
|
||||||
|
self.state.input = RoleDefinitionSpecialistInput.model_validate(inputs)
|
||||||
|
current_app.logger.debug(f"State: {self.state}")
|
||||||
|
result = await super().kickoff_async(inputs)
|
||||||
|
return self.state
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from os import wait
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from crewai.flow.flow import start, listen, and_
|
||||||
|
from flask import current_app
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from common.extensions import db
|
||||||
|
from common.models.user import Tenant
|
||||||
|
from common.models.interaction import Specialist
|
||||||
|
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
|
||||||
|
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
|
||||||
|
from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments
|
||||||
|
from eveai_chat_workers.outputs.traicie.competencies.competencies_v1_1 import Competencies
|
||||||
|
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
|
||||||
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
||||||
|
"""
|
||||||
|
type: TRAICIE_SELECTION_SPECIALIST
|
||||||
|
type_version: 1.0
|
||||||
|
Traicie Selection Specialist Executor class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs):
|
||||||
|
self.role_definition_crew = None
|
||||||
|
|
||||||
|
super().__init__(tenant_id, specialist_id, session_id, task_id)
|
||||||
|
|
||||||
|
# Load the Tenant & set language
|
||||||
|
self.tenant = Tenant.query.get_or_404(tenant_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> str:
|
||||||
|
return "TRAICIE_SELECTION_SPECIALIST"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type_version(self) -> str:
|
||||||
|
return "1.0"
|
||||||
|
|
||||||
|
def _config_task_agents(self):
|
||||||
|
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
|
||||||
|
|
||||||
|
def _config_pydantic_outputs(self):
|
||||||
|
self._add_pydantic_output("traicie_get_competencies_task", Competencies, "competencies")
|
||||||
|
|
||||||
|
def _instantiate_specialist(self):
|
||||||
|
verbose = self.tuning
|
||||||
|
|
||||||
|
role_definition_agents = [self.traicie_hr_bp_agent]
|
||||||
|
role_definition_tasks = [self.traicie_get_competencies_task]
|
||||||
|
self.role_definition_crew = EveAICrewAICrew(
|
||||||
|
self,
|
||||||
|
"Role Definition Crew",
|
||||||
|
agents=role_definition_agents,
|
||||||
|
tasks=role_definition_tasks,
|
||||||
|
verbose=verbose,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.flow = RoleDefinitionFlow(
|
||||||
|
self,
|
||||||
|
self.role_definition_crew
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
|
||||||
|
self.log_tuning("Traicie Role Definition Specialist execution started", {})
|
||||||
|
|
||||||
|
flow_inputs = {
|
||||||
|
"vacancy_text": arguments.vacancy_text,
|
||||||
|
"role_name": arguments.role_name,
|
||||||
|
'role_reference': arguments.role_reference,
|
||||||
|
}
|
||||||
|
|
||||||
|
flow_results = self.flow.kickoff(inputs=flow_inputs)
|
||||||
|
|
||||||
|
flow_state = self.flow.state
|
||||||
|
|
||||||
|
results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
|
||||||
|
if flow_state.competencies:
|
||||||
|
results.competencies = flow_state.competencies
|
||||||
|
|
||||||
|
self.create_selection_specialist(arguments, flow_state.competencies)
|
||||||
|
|
||||||
|
self.log_tuning(f"Traicie Role Definition Specialist execution ended", {"Results": results.model_dump()})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def create_selection_specialist(self, arguments: SpecialistArguments, competencies: List[ListItem]):
|
||||||
|
"""This method creates a new TRAICIE_SELECTION_SPECIALIST specialist with the given competencies."""
|
||||||
|
current_app.logger.info(f"Creating selection with arguments: {arguments.model_dump()}")
|
||||||
|
selection_comptencies = []
|
||||||
|
for competency in competencies:
|
||||||
|
selection_competency = {
|
||||||
|
"title": competency.title,
|
||||||
|
"description": competency.description,
|
||||||
|
"assess": True,
|
||||||
|
"is_knockout": False,
|
||||||
|
}
|
||||||
|
selection_comptencies.append(selection_competency)
|
||||||
|
|
||||||
|
selection_config = {
|
||||||
|
"name": arguments.specialist_name,
|
||||||
|
"competencies": selection_comptencies,
|
||||||
|
"tone_of_voice": "Professional & Neutral",
|
||||||
|
"language_level": "Standard",
|
||||||
|
"role_reference": arguments.role_reference,
|
||||||
|
}
|
||||||
|
name = arguments.role_name
|
||||||
|
if len(name) > 50:
|
||||||
|
name = name[:47] + "..."
|
||||||
|
|
||||||
|
new_specialist = Specialist(
|
||||||
|
name=name,
|
||||||
|
description=f"Specialist for {arguments.role_name} role",
|
||||||
|
type="TRAICIE_SELECTION_SPECIALIST",
|
||||||
|
type_version="1.0",
|
||||||
|
tuning=False,
|
||||||
|
configuration=selection_config,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
db.session.add(new_specialist)
|
||||||
|
db.session.commit()
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"Error creating selection specialist: {str(e)}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
SpecialistServices.initialize_specialist(new_specialist.id, "TRAICIE_SELECTION_SPECIALIST", "1.0")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionSpecialistInput(BaseModel):
|
||||||
|
role_name: str = Field(..., alias="role_name")
|
||||||
|
role_reference: Optional[str] = Field(..., alias="role_reference")
|
||||||
|
vacancy_text: Optional[str] = Field(None, alias="vacancy_text")
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionSpecialistResult(SpecialistResult):
|
||||||
|
competencies: Optional[List[ListItem]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefFlowState(EveAIFlowState):
|
||||||
|
"""Flow state for Traicie Role Definition specialist that automatically updates from task outputs"""
|
||||||
|
input: Optional[RoleDefinitionSpecialistInput] = None
|
||||||
|
competencies: Optional[List[ListItem]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionFlow(EveAICrewAIFlow[RoleDefFlowState]):
|
||||||
|
def __init__(self,
|
||||||
|
specialist_executor: CrewAIBaseSpecialistExecutor,
|
||||||
|
role_definitiion_crew: EveAICrewAICrew,
|
||||||
|
**kwargs):
|
||||||
|
super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs)
|
||||||
|
self.specialist_executor = specialist_executor
|
||||||
|
self.role_definition_crew = role_definitiion_crew
|
||||||
|
self.exception_raised = False
|
||||||
|
|
||||||
|
@start()
|
||||||
|
def process_inputs(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@listen(process_inputs)
|
||||||
|
async def execute_role_definition (self):
|
||||||
|
inputs = self.state.input.model_dump()
|
||||||
|
try:
|
||||||
|
current_app.logger.debug("In execute_role_definition")
|
||||||
|
crew_output = await self.role_definition_crew.kickoff_async(inputs=inputs)
|
||||||
|
# Unfortunately, crew_output will only contain the output of the latest task.
|
||||||
|
# As we will only take into account the flow state, we need to ensure both competencies and criteria
|
||||||
|
# are copies to the flow state.
|
||||||
|
update = {}
|
||||||
|
for task in self.role_definition_crew.tasks:
|
||||||
|
current_app.logger.debug(f"Task {task.name} output:\n{task.output}")
|
||||||
|
if task.name == "traicie_get_competencies_task":
|
||||||
|
# update["competencies"] = task.output.pydantic.competencies
|
||||||
|
self.state.competencies = task.output.pydantic.competencies
|
||||||
|
# crew_output.pydantic = crew_output.pydantic.model_copy(update=update)
|
||||||
|
current_app.logger.debug(f"State after execute_role_definition: {self.state}")
|
||||||
|
current_app.logger.debug(f"State dump after execute_role_definition: {self.state.model_dump()}")
|
||||||
|
return crew_output
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"CREW execute_role_definition Kickoff Error: {str(e)}")
|
||||||
|
self.exception_raised = True
|
||||||
|
raise e
|
||||||
|
|
||||||
|
async def kickoff_async(self, inputs=None):
|
||||||
|
current_app.logger.debug(f"Async kickoff {self.name}")
|
||||||
|
current_app.logger.debug(f"Inputs: {inputs}")
|
||||||
|
self.state.input = RoleDefinitionSpecialistInput.model_validate(inputs)
|
||||||
|
current_app.logger.debug(f"State: {self.state}")
|
||||||
|
result = await super().kickoff_async(inputs)
|
||||||
|
return self.state
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""Add default TenantMake to Tenant model
|
||||||
|
|
||||||
|
Revision ID: 83d4e90f87c6
|
||||||
|
Revises: f40d16a0965a
|
||||||
|
Create Date: 2025-06-09 15:42:51.503696
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '83d4e90f87c6'
|
||||||
|
down_revision = 'f40d16a0965a'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('default_tenant_make_id', sa.Integer(), nullable=True))
|
||||||
|
batch_op.create_foreign_key(None, 'tenant_make', ['default_tenant_make_id'], ['id'], referent_schema='public')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.drop_column('default_tenant_make_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Make TenantMake name unique
|
||||||
|
|
||||||
|
Revision ID: f40d16a0965a
|
||||||
|
Revises: 200bda7f5251
|
||||||
|
Create Date: 2025-06-09 06:15:56.791634
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f40d16a0965a'
|
||||||
|
down_revision = '200bda7f5251'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant_make', schema=None) as batch_op:
|
||||||
|
batch_op.create_unique_constraint(None, ['name'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant_make', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(None, type_='unique')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Add tenant_make reference to SpecialistMagicLink
|
||||||
|
|
||||||
|
Revision ID: 2b6ae6cc923e
|
||||||
|
Revises: a179785e5362
|
||||||
|
Create Date: 2025-06-09 15:59:39.157066
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import pgvector
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2b6ae6cc923e'
|
||||||
|
down_revision = 'a179785e5362'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('specialist_magic_link', sa.Column('tenant_make_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'specialist_magic_link', 'tenant_make', ['tenant_make_id'], ['id'], referent_schema='public', ondelete='CASCADE')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Add active field to Specialist
|
||||||
|
|
||||||
|
Revision ID: a179785e5362
|
||||||
|
Revises: c71facc0ce7e
|
||||||
|
Create Date: 2025-06-09 08:30:18.532600
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import pgvector
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a179785e5362'
|
||||||
|
down_revision = 'c71facc0ce7e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('specialist', sa.Column('active', sa.Boolean(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('specialist', 'active')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -82,7 +82,6 @@ def initialize_default_tenant():
|
|||||||
'website': 'https://www.askeveai.com',
|
'website': 'https://www.askeveai.com',
|
||||||
'timezone': 'UTC',
|
'timezone': 'UTC',
|
||||||
'default_language': 'en',
|
'default_language': 'en',
|
||||||
'allowed_languages': ['en', 'fr', 'nl', 'de', 'es'],
|
|
||||||
'type': 'Active',
|
'type': 'Active',
|
||||||
'currency': '€',
|
'currency': '€',
|
||||||
'created_at': dt.now(tz.utc),
|
'created_at': dt.now(tz.utc),
|
||||||
|
|||||||
Reference in New Issue
Block a user