Compare commits
3 Commits
v2.3.2-alf
...
v2.3.3-alf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43ee9139d6 | ||
|
|
8f45005713 | ||
|
|
bc1626c4ff |
@@ -8,7 +8,7 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
class Catalog(db.Model):
|
class Catalog(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(50), nullable=False)
|
name = db.Column(db.String(50), nullable=False, unique=True)
|
||||||
description = db.Column(db.Text, nullable=True)
|
description = db.Column(db.Text, nullable=True)
|
||||||
type = db.Column(db.String(50), nullable=False, default="STANDARD_CATALOG")
|
type = db.Column(db.String(50), nullable=False, default="STANDARD_CATALOG")
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import date
|
|||||||
|
|
||||||
from common.extensions import db
|
from common.extensions import db
|
||||||
from flask_security import UserMixin, RoleMixin
|
from flask_security import UserMixin, RoleMixin
|
||||||
from sqlalchemy.dialects.postgresql import ARRAY
|
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from common.models.entitlements import License
|
from common.models.entitlements import License
|
||||||
@@ -173,6 +173,28 @@ class TenantProject(db.Model):
|
|||||||
return f"<TenantProject {self.id}: {self.name}>"
|
return f"<TenantProject {self.id}: {self.name}>"
|
||||||
|
|
||||||
|
|
||||||
|
class TenantMake(db.Model):
|
||||||
|
__bind_key__ = 'public'
|
||||||
|
__table_args__ = {'schema': 'public'}
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
|
||||||
|
name = db.Column(db.String(50), nullable=False)
|
||||||
|
description = db.Column(db.Text, nullable=True)
|
||||||
|
active = db.Column(db.Boolean, nullable=False, default=True)
|
||||||
|
website = db.Column(db.String(255), nullable=True)
|
||||||
|
logo_url = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
|
# Chat customisation options
|
||||||
|
chat_customisation_options = db.Column(JSONB, nullable=True)
|
||||||
|
|
||||||
|
# Versioning Information
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||||
|
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||||
|
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
||||||
|
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
|
||||||
|
|
||||||
|
|
||||||
class Partner(db.Model):
|
class Partner(db.Model):
|
||||||
__bind_key__ = 'public'
|
__bind_key__ = 'public'
|
||||||
__table_args__ = {'schema': 'public'}
|
__table_args__ = {'schema': 'public'}
|
||||||
@@ -279,5 +301,3 @@ class SpecialistMagicLinkTenant(db.Model):
|
|||||||
|
|
||||||
magic_link_code = db.Column(db.String(55), primary_key=True)
|
magic_link_code = db.Column(db.String(55), 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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
15
common/utils/cache/config_cache.py
vendored
15
common/utils/cache/config_cache.py
vendored
@@ -7,7 +7,7 @@ from flask import current_app
|
|||||||
|
|
||||||
from common.utils.cache.base import CacheHandler, CacheKey
|
from common.utils.cache.base import CacheHandler, CacheKey
|
||||||
from config.type_defs import agent_types, task_types, tool_types, specialist_types, retriever_types, prompt_types, \
|
from config.type_defs import agent_types, task_types, tool_types, specialist_types, retriever_types, prompt_types, \
|
||||||
catalog_types, partner_service_types, processor_types
|
catalog_types, partner_service_types, processor_types, customisation_types
|
||||||
|
|
||||||
|
|
||||||
def is_major_minor(version: str) -> bool:
|
def is_major_minor(version: str) -> bool:
|
||||||
@@ -463,7 +463,6 @@ ProcessorConfigCacheHandler, ProcessorConfigVersionTreeCacheHandler, ProcessorCo
|
|||||||
types_module=processor_types.PROCESSOR_TYPES
|
types_module=processor_types.PROCESSOR_TYPES
|
||||||
))
|
))
|
||||||
|
|
||||||
# Add to common/utils/cache/config_cache.py
|
|
||||||
PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, PartnerServiceConfigTypesCacheHandler = (
|
PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, PartnerServiceConfigTypesCacheHandler = (
|
||||||
create_config_cache_handlers(
|
create_config_cache_handlers(
|
||||||
config_type='partner_services',
|
config_type='partner_services',
|
||||||
@@ -471,6 +470,14 @@ PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, P
|
|||||||
types_module=partner_service_types.PARTNER_SERVICE_TYPES
|
types_module=partner_service_types.PARTNER_SERVICE_TYPES
|
||||||
))
|
))
|
||||||
|
|
||||||
|
CustomisationConfigCacheHandler, CustomisationConfigVersionTreeCacheHandler, CustomisationConfigTypesCacheHandler = (
|
||||||
|
create_config_cache_handlers(
|
||||||
|
config_type='customisations',
|
||||||
|
config_dir='config/customisations',
|
||||||
|
types_module=customisation_types.CUSTOMISATION_TYPES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_config_cache_handlers(cache_manager) -> None:
|
def register_config_cache_handlers(cache_manager) -> None:
|
||||||
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
||||||
@@ -503,6 +510,9 @@ def register_config_cache_handlers(cache_manager) -> None:
|
|||||||
cache_manager.register_handler(PartnerServiceConfigCacheHandler, 'eveai_config')
|
cache_manager.register_handler(PartnerServiceConfigCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(PartnerServiceConfigTypesCacheHandler, 'eveai_config')
|
cache_manager.register_handler(PartnerServiceConfigTypesCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(PartnerServiceConfigVersionTreeCacheHandler, 'eveai_config')
|
cache_manager.register_handler(PartnerServiceConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(CustomisationConfigCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(CustomisationConfigTypesCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(CustomisationConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
|
|
||||||
cache_manager.agents_config_cache.set_version_tree_cache(cache_manager.agents_version_tree_cache)
|
cache_manager.agents_config_cache.set_version_tree_cache(cache_manager.agents_version_tree_cache)
|
||||||
cache_manager.tasks_config_cache.set_version_tree_cache(cache_manager.tasks_version_tree_cache)
|
cache_manager.tasks_config_cache.set_version_tree_cache(cache_manager.tasks_version_tree_cache)
|
||||||
@@ -513,3 +523,4 @@ def register_config_cache_handlers(cache_manager) -> None:
|
|||||||
cache_manager.catalogs_config_cache.set_version_tree_cache(cache_manager.catalogs_version_tree_cache)
|
cache_manager.catalogs_config_cache.set_version_tree_cache(cache_manager.catalogs_version_tree_cache)
|
||||||
cache_manager.processors_config_cache.set_version_tree_cache(cache_manager.processors_version_tree_cache)
|
cache_manager.processors_config_cache.set_version_tree_cache(cache_manager.processors_version_tree_cache)
|
||||||
cache_manager.partner_services_config_cache.set_version_tree_cache(cache_manager.partner_services_version_tree_cache)
|
cache_manager.partner_services_config_cache.set_version_tree_cache(cache_manager.partner_services_version_tree_cache)
|
||||||
|
cache_manager.customisations_config_cache.set_version_tree_cache(cache_manager.customisations_version_tree_cache)
|
||||||
|
|||||||
42
common/utils/chat_utils.py
Normal file
42
common/utils/chat_utils.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Utility functions for chat customization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_default_chat_customisation(tenant_customisation=None):
|
||||||
|
"""
|
||||||
|
Get chat customization options with default values for missing options.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tenant_customization (dict, optional): The tenant's customization options.
|
||||||
|
Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary containing all customization options with default values
|
||||||
|
for any missing options.
|
||||||
|
"""
|
||||||
|
# Default customization options
|
||||||
|
default_customisation = {
|
||||||
|
'primary_color': '#007bff',
|
||||||
|
'secondary_color': '#6c757d',
|
||||||
|
'background_color': '#ffffff',
|
||||||
|
'text_color': '#212529',
|
||||||
|
'sidebar_color': '#f8f9fa',
|
||||||
|
'logo_url': None,
|
||||||
|
'sidebar_text': None,
|
||||||
|
'welcome_message': 'Hello! How can I help you today?',
|
||||||
|
'team_info': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# If no tenant customization is provided, return the defaults
|
||||||
|
if tenant_customisation is None:
|
||||||
|
return default_customisation
|
||||||
|
|
||||||
|
# Start with the default customization
|
||||||
|
customisation = default_customisation.copy()
|
||||||
|
|
||||||
|
# Update with tenant customization
|
||||||
|
for key, value in tenant_customisation.items():
|
||||||
|
if key in customisation:
|
||||||
|
customisation[key] = value
|
||||||
|
|
||||||
|
return customisation
|
||||||
@@ -21,7 +21,7 @@ class TaggingField(BaseModel):
|
|||||||
@field_validator('type', mode='before')
|
@field_validator('type', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_type(cls, v: str) -> str:
|
def validate_type(cls, v: str) -> str:
|
||||||
valid_types = ['string', 'integer', 'float', 'date', 'enum']
|
valid_types = ['string', 'integer', 'float', 'date', 'enum', 'color']
|
||||||
if v not in valid_types:
|
if v not in valid_types:
|
||||||
raise ValueError(f'type must be one of {valid_types}')
|
raise ValueError(f'type must be one of {valid_types}')
|
||||||
return v
|
return v
|
||||||
@@ -243,7 +243,7 @@ class ArgumentDefinition(BaseModel):
|
|||||||
@field_validator('type')
|
@field_validator('type')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_type(cls, v: str) -> str:
|
def validate_type(cls, v: str) -> str:
|
||||||
valid_types = ['string', 'integer', 'float', 'date', 'enum']
|
valid_types = ['string', 'integer', 'float', 'date', 'enum', 'color']
|
||||||
if v not in valid_types:
|
if v not in valid_types:
|
||||||
raise ValueError(f'type must be one of {valid_types}')
|
raise ValueError(f'type must be one of {valid_types}')
|
||||||
return v
|
return v
|
||||||
@@ -256,7 +256,8 @@ class ArgumentDefinition(BaseModel):
|
|||||||
'integer': NumericConstraint,
|
'integer': NumericConstraint,
|
||||||
'float': NumericConstraint,
|
'float': NumericConstraint,
|
||||||
'date': DateConstraint,
|
'date': DateConstraint,
|
||||||
'enum': EnumConstraint
|
'enum': EnumConstraint,
|
||||||
|
'color': StringConstraint
|
||||||
}
|
}
|
||||||
|
|
||||||
expected_type = expected_constraint_types.get(self.type)
|
expected_type = expected_constraint_types.get(self.type)
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ def create_default_config_from_type_config(type_config):
|
|||||||
default_config[field_name] = 0
|
default_config[field_name] = 0
|
||||||
elif field_type == "boolean":
|
elif field_type == "boolean":
|
||||||
default_config[field_name] = False
|
default_config[field_name] = False
|
||||||
|
elif field_type == "color":
|
||||||
|
default_config[field_name] = "#000000"
|
||||||
else:
|
else:
|
||||||
default_config[field_name] = ""
|
default_config[field_name] = ""
|
||||||
|
|
||||||
|
|||||||
25
config/agents/traicie/TRAICIE_RECRUITER/1.0.0.yaml
Normal file
25
config/agents/traicie/TRAICIE_RECRUITER/1.0.0.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Traicie HR BP "
|
||||||
|
role: >
|
||||||
|
You are an Expert Recruiter working for {tenant_name}
|
||||||
|
{custom_role}
|
||||||
|
goal: >
|
||||||
|
As an expert recruiter, you identify, attract, and secure top talent by building genuine relationships, deeply
|
||||||
|
understanding business needs, and ensuring optimal alignment between candidate potential and organizational goals
|
||||||
|
, while championing diversity, culture fit, and long-term retention.
|
||||||
|
{custom_goal}
|
||||||
|
backstory: >
|
||||||
|
You started your career in a high-pressure agency setting, where you quickly learned the art of fast-paced hiring and
|
||||||
|
relationship building. Over the years, you moved in-house, partnering closely with business leaders to shape
|
||||||
|
recruitment strategies that go beyond filling roles—you focus on finding the right people to drive growth and culture.
|
||||||
|
With a strong grasp of both tech and non-tech profiles, you’ve adapted to changing trends, from remote work to
|
||||||
|
AI-driven sourcing. You’re more than a recruiter—you’re a trusted advisor, a brand ambassador, and a connector of
|
||||||
|
people and purpose.
|
||||||
|
{custom_backstory}
|
||||||
|
full_model_name: "mistral.mistral-medium-latest"
|
||||||
|
temperature: 0.3
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2025-05-21"
|
||||||
|
description: "HR BP Agent."
|
||||||
|
changes: "Initial version"
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Chat Client Customisation"
|
||||||
|
configuration:
|
||||||
|
"primary_color":
|
||||||
|
name: "Primary Color"
|
||||||
|
description: "Primary Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"secondary_color":
|
||||||
|
name: "Secondary Color"
|
||||||
|
description: "Secondary Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"background_color":
|
||||||
|
name: "Background Color"
|
||||||
|
description: "Background Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"text_color":
|
||||||
|
name: "Text Color"
|
||||||
|
description: "Text Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"sidebar_color":
|
||||||
|
name: "Sidebar Color"
|
||||||
|
description: "Sidebar Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"sidebar_text":
|
||||||
|
name: "Sidebar Text"
|
||||||
|
description: "Text to be shown in the sidebar"
|
||||||
|
type: "text"
|
||||||
|
required: false
|
||||||
|
"welcome_message":
|
||||||
|
name: "Welcome Message"
|
||||||
|
description: "Text to be shown as Welcome"
|
||||||
|
type: "text"
|
||||||
|
required: false
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2024-06-06"
|
||||||
|
changes: "Initial version"
|
||||||
|
description: "Parameters allowing to customise the chat client"
|
||||||
@@ -303,10 +303,10 @@ LOGGING = {
|
|||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
},
|
},
|
||||||
'file_chat': {
|
'file_chat_client': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/eveai_chat.log',
|
'filename': 'logs/eveai_chat_client.log',
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -432,8 +432,8 @@ LOGGING = {
|
|||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'eveai_chat': { # logger for the eveai_chat
|
'eveai_chat_client': { # logger for the eveai_chat
|
||||||
'handlers': ['file_chat', 'graylog', ] if env == 'production' else ['file_chat', ],
|
'handlers': ['file_chat_client', 'graylog', ] if env == 'production' else ['file_chat_client', ],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -88,7 +88,13 @@ arguments:
|
|||||||
type: "str"
|
type: "str"
|
||||||
description: "The language (2-letter code) used to start the conversation"
|
description: "The language (2-letter code) used to start the conversation"
|
||||||
required: true
|
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:
|
results:
|
||||||
competencies:
|
competencies:
|
||||||
name: "competencies"
|
name: "competencies"
|
||||||
|
|||||||
7
config/type_defs/customisation_types.py
Normal file
7
config/type_defs/customisation_types.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Catalog Types
|
||||||
|
CUSTOMISATION_TYPES = {
|
||||||
|
"CHAT_CLIENT_CUSTOMISATION": {
|
||||||
|
"name": "Chat Client Customisation",
|
||||||
|
"description": "Parameters allowing to customise the chat client",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -5,6 +5,19 @@ 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.3-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add Tenant Make
|
||||||
|
- Add Chat Client customisation options to Tenant Make
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Catalog name must be unique to avoid mistakes
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Ensure document version is selected in UI before trying to view it.
|
||||||
|
- Remove obsolete tab from tenant overview
|
||||||
|
|
||||||
## [2.3.2-alfa]
|
## [2.3.2-alfa]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -29,18 +42,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Role Definition Specialist creates Selection Specialist from generated competencies
|
- Role Definition Specialist creates Selection Specialist from generated competencies
|
||||||
- Improvements to Selection Specialist (Agent definition to be started)
|
- Improvements to Selection Specialist (Agent definition to be started)
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
- For soon-to-be removed features.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- For now removed features.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- For any bug fixes.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- In case of vulnerabilities.
|
|
||||||
|
|
||||||
## [2.3.0-alfa]
|
## [2.3.0-alfa]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -60,7 +61,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Introduction of ChatSession (Specialist Execution) follow-up in administrative interface
|
- Introduction of ChatSession (Specialist Execution) follow-up in administrative interface
|
||||||
- Introduce npm for javascript libraries usage and optimisations
|
- Introduce npm for javascript libraries usage and optimisations
|
||||||
- Introduction of new top bar in administrative interface to show session defaults (removing old navbar buttons)
|
- Introduction of new top bar in administrative interface to show session defaults (removing old navbar buttons)
|
||||||
-
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Add 'Register'-button to list views, replacing register menu-items
|
- Add 'Register'-button to list views, replacing register menu-items
|
||||||
@@ -118,9 +118,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Fixed
|
### Fixed
|
||||||
- Set default language when registering Documents or URLs.
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
### Security
|
|
||||||
- In case of vulnerabilities.
|
|
||||||
|
|
||||||
## [2.1.0-alfa]
|
## [2.1.0-alfa]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- eveai_app
|
- eveai_app
|
||||||
- eveai_api
|
- eveai_api
|
||||||
|
- eveai_chat_client
|
||||||
networks:
|
networks:
|
||||||
- eveai-network
|
- eveai-network
|
||||||
|
|
||||||
@@ -177,6 +178,44 @@ services:
|
|||||||
# networks:
|
# networks:
|
||||||
# - eveai-network
|
# - eveai-network
|
||||||
|
|
||||||
|
eveai_chat_client:
|
||||||
|
image: josakola/eveai_chat_client:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: ./docker/eveai_chat_client/Dockerfile
|
||||||
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
ports:
|
||||||
|
- 5004:5004
|
||||||
|
expose:
|
||||||
|
- 8000
|
||||||
|
environment:
|
||||||
|
<<: *common-variables
|
||||||
|
COMPONENT_NAME: eveai_chat_client
|
||||||
|
volumes:
|
||||||
|
- ../eveai_chat_client:/app/eveai_chat_client
|
||||||
|
- ../common:/app/common
|
||||||
|
- ../config:/app/config
|
||||||
|
- ../scripts:/app/scripts
|
||||||
|
- ../patched_packages:/app/patched_packages
|
||||||
|
- ./eveai_logs:/app/logs
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5004/healthz/ready"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 1s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- eveai-network
|
||||||
|
|
||||||
eveai_chat_workers:
|
eveai_chat_workers:
|
||||||
image: josakola/eveai_chat_workers:latest
|
image: josakola/eveai_chat_workers:latest
|
||||||
build:
|
build:
|
||||||
@@ -441,4 +480,3 @@ volumes:
|
|||||||
#secrets:
|
#secrets:
|
||||||
# db-password:
|
# db-password:
|
||||||
# file: ./db/password.txt
|
# file: ./db/password.txt
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- eveai_app
|
- eveai_app
|
||||||
- eveai_api
|
- eveai_api
|
||||||
|
- eveai_chat_client
|
||||||
networks:
|
networks:
|
||||||
- eveai-network
|
- eveai-network
|
||||||
restart: "no"
|
restart: "no"
|
||||||
@@ -106,6 +107,33 @@ services:
|
|||||||
- eveai-network
|
- eveai-network
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|
||||||
|
eveai_chat_client:
|
||||||
|
image: josakola/eveai_chat_client:${EVEAI_VERSION:-latest}
|
||||||
|
ports:
|
||||||
|
- 5004:5004
|
||||||
|
expose:
|
||||||
|
- 8000
|
||||||
|
environment:
|
||||||
|
<<: *common-variables
|
||||||
|
COMPONENT_NAME: eveai_chat_client
|
||||||
|
volumes:
|
||||||
|
- eveai_logs:/app/logs
|
||||||
|
- crewai_storage:/app/crewai_storage
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5004/healthz/ready"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- eveai-network
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
eveai_chat_workers:
|
eveai_chat_workers:
|
||||||
image: josakola/eveai_chat_workers:${EVEAI_VERSION:-latest}
|
image: josakola/eveai_chat_workers:${EVEAI_VERSION:-latest}
|
||||||
expose:
|
expose:
|
||||||
|
|||||||
72
docker/eveai_chat_client/Dockerfile
Normal file
72
docker/eveai_chat_client/Dockerfile
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
ARG PYTHON_VERSION=3.12.7
|
||||||
|
FROM python:${PYTHON_VERSION}-slim as base
|
||||||
|
|
||||||
|
# Prevents Python from writing pyc files.
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# Keeps Python from buffering stdout and stderr to avoid situations where
|
||||||
|
# the application crashes without emitting any logs due to buffering.
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Create directory for patched packages and set permissions
|
||||||
|
RUN mkdir -p /app/patched_packages && \
|
||||||
|
chmod 777 /app/patched_packages
|
||||||
|
|
||||||
|
# Ensure patches are applied to the application.
|
||||||
|
ENV PYTHONPATH=/app/patched_packages:$PYTHONPATH
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create a non-privileged user that the app will run under.
|
||||||
|
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
||||||
|
ARG UID=10001
|
||||||
|
RUN adduser \
|
||||||
|
--disabled-password \
|
||||||
|
--gecos "" \
|
||||||
|
--home "/nonexistent" \
|
||||||
|
--shell "/bin/bash" \
|
||||||
|
--no-create-home \
|
||||||
|
--uid "${UID}" \
|
||||||
|
appuser
|
||||||
|
|
||||||
|
# Install necessary packages and build tools
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
gcc \
|
||||||
|
postgresql-client \
|
||||||
|
curl \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create logs directory and set permissions
|
||||||
|
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
|
||||||
|
|
||||||
|
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||||||
|
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||||||
|
# Leverage a bind mount to requirements.txt to avoid having to copy them into
|
||||||
|
# into this layer.
|
||||||
|
|
||||||
|
COPY requirements.txt /app/
|
||||||
|
RUN python -m pip install -r /app/requirements.txt
|
||||||
|
|
||||||
|
# Copy the source code into the container.
|
||||||
|
COPY eveai_chat_client /app/eveai_chat_client
|
||||||
|
COPY common /app/common
|
||||||
|
COPY config /app/config
|
||||||
|
COPY scripts /app/scripts
|
||||||
|
COPY patched_packages /app/patched_packages
|
||||||
|
COPY content /app/content
|
||||||
|
|
||||||
|
# Set permissions for scripts
|
||||||
|
RUN chmod 777 /app/scripts/entrypoint.sh && \
|
||||||
|
chmod 777 /app/scripts/start_eveai_chat_client.sh
|
||||||
|
|
||||||
|
# Set ownership of the application directory to the non-privileged user
|
||||||
|
RUN chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
# Expose the port that the application listens on.
|
||||||
|
EXPOSE 5004
|
||||||
|
|
||||||
|
# Set entrypoint and command
|
||||||
|
ENTRYPOINT ["/app/scripts/entrypoint.sh"]
|
||||||
|
CMD ["/app/scripts/start_eveai_chat_client.sh"]
|
||||||
@@ -5,16 +5,18 @@
|
|||||||
|
|
||||||
{% block content_title %}Document Versions{% endblock %}
|
{% block content_title %}Document Versions{% endblock %}
|
||||||
{% block content_description %}View Versions for {{ document }}{% endblock %}
|
{% block content_description %}View Versions for {{ document }}{% endblock %}
|
||||||
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto">{% endblock %}
|
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<form method="POST" action="{{ url_for('document_bp.handle_document_version_selection') }}">
|
<form method="POST" action="{{ url_for('document_bp.handle_document_version_selection') }}" id="documentVersionsForm">
|
||||||
{{ render_selectable_table(headers=["ID", "URL", "Object Name", "File Type", "Process.", "Proces. Start", "Proces. Finish", "Proces. Error"], rows=rows, selectable=True, id="versionsTable") }}
|
{{ render_selectable_table(headers=["ID", "URL", "Object Name", "File Type", "Process.", "Proces. Start", "Proces. Finish", "Proces. Error"], rows=rows, selectable=True, id="versionsTable") }}
|
||||||
<div class="form-group mt-3">
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary">Edit Document Version</button>
|
<div>
|
||||||
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger">View Processed Document</button>
|
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
|
||||||
<button type="submit" name="action" value="process_document_version" class="btn btn-danger">Process Document Version</button>
|
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">View Processed Document</button>
|
||||||
|
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="form-group mt-3 d-flex justify-content-between">
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
|
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
|
||||||
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger">View Processed Document</button>
|
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">View Processed Document</button>
|
||||||
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
|
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
{'name': 'Tenant Overview', 'url': '/user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Tenant Overview', 'url': '/user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Edit Tenant', 'url': '/user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Edit Tenant', 'url': '/user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Tenant Domains', 'url': '/user/view_tenant_domains', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Tenant Domains', 'url': '/user/view_tenant_domains', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
|
{'name': 'Tenant Makes', 'url': '/user/tenant_makes', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Tenant Projects', 'url': '/user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Tenant Projects', 'url': '/user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Users', 'url': '/user/view_users', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Users', 'url': '/user/view_users', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
]) }}
|
]) }}
|
||||||
|
|||||||
33
eveai_app/templates/user/edit_tenant_make.html
Normal file
33
eveai_app/templates/user/edit_tenant_make.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from "macros.html" import render_field %}
|
||||||
|
|
||||||
|
{% block title %}Edit Tenant Make{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}Edit Tenant Make{% endblock %}
|
||||||
|
{% block content_description %}Edit a Tenant Make.{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% set disabled_fields = [] %}
|
||||||
|
{% set exclude_fields = [] %}
|
||||||
|
<!-- Render Static Fields -->
|
||||||
|
{% for field in form.get_static_fields() %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
<!-- Render Dynamic Fields -->
|
||||||
|
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||||
|
{% if fields|length > 0 %}
|
||||||
|
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||||
|
{% endif %}
|
||||||
|
{% for field in fields %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-primary">Save Tenant Make</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_footer %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
32
eveai_app/templates/user/tenant_make.html
Normal file
32
eveai_app/templates/user/tenant_make.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from "macros.html" import render_field %}
|
||||||
|
|
||||||
|
{% block title %}Tenant Make Registration{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}Register Tenant Make{% endblock %}
|
||||||
|
{% block content_description %}Define a new tenant make{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% set disabled_fields = [] %}
|
||||||
|
{% set exclude_fields = [] %}
|
||||||
|
{% for field in form.get_static_fields() %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
<!-- Render Dynamic Fields -->
|
||||||
|
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||||
|
{% if fields|length > 0 %}
|
||||||
|
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||||
|
{% endif %}
|
||||||
|
{% for field in fields %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-primary">Register Tenant Make</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_footer %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
26
eveai_app/templates/user/tenant_makes.html
Normal file
26
eveai_app/templates/user/tenant_makes.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||||
|
|
||||||
|
{% block title %}Tenant Makes{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}Tenant Makes{% endblock %}
|
||||||
|
{% block content_description %}View Tenant Makes for Tenant{% endblock %}
|
||||||
|
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<form method="POST" action="{{ url_for('user_bp.handle_tenant_make_selection') }}" id="tenantMakesForm">
|
||||||
|
{{ render_selectable_table(headers=["Tenant Make ID", "Name", "Website", "Active"], rows=rows, selectable=True, id="tenantMakesTable") }}
|
||||||
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<button type="submit" name="action" value="edit_tenant_make" class="btn btn-primary" onclick="return validateTableSelection('tenantMakesForm')">Edit Tenant Make</button>
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="action" value="create_tenant_make" class="btn btn-success">Register Tenant Make</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_footer %}
|
||||||
|
{{ render_pagination(pagination, "user_bp.tenant_makes") }}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% from "macros.html" import render_field, render_included_field %}
|
{% from "macros.html" import render_field, render_included_field, debug_to_console %}
|
||||||
|
|
||||||
{% block title %}Tenant Overview{% endblock %}
|
{% block title %}Tenant Overview{% endblock %}
|
||||||
|
|
||||||
@@ -9,162 +9,23 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<!-- Main Tenant Information -->
|
{% set disabled_fields = [] %}
|
||||||
{% set main_fields = ['name', 'code', 'website', 'default_language', 'allowed_languages', 'type'] %}
|
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
{{ render_included_field(field, disabled_fields=main_fields, include_fields=main_fields) }}
|
{{ debug_to_console('field to disable', field.name) }}
|
||||||
|
{{ debug_to_console('field type to disable', field.type) }}
|
||||||
|
{% if field.name != 'csrf_token' and field.type != 'HiddenField' %}
|
||||||
|
{% set disabled_fields = disabled_fields + [field.name] %}
|
||||||
|
{{ debug_to_console('disable', '!') }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{{ debug_to_console('disabled_fields', disabled_fields) }}
|
||||||
|
{% set exclude_fields = [] %}
|
||||||
|
{% for field in form %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<!-- Nav Tabs -->
|
|
||||||
<div class="row mt-5">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<div class="nav-wrapper position-relative end-0">
|
|
||||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#license-info-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
|
||||||
License Information
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="tab-content tab-space">
|
|
||||||
<!-- License Information Tab -->
|
|
||||||
<div class="tab-pane fade" id="license-info-tab" role="tabpanel">
|
|
||||||
{% set license_fields = ['currency', 'usage_email', ] %}
|
|
||||||
{% for field in form %}
|
|
||||||
{{ render_included_field(field, disabled_fields=license_fields, include_fields=license_fields) }}
|
|
||||||
{% endfor %}
|
|
||||||
<!-- Register API Key Button -->
|
|
||||||
<button type="button" class="btn btn-primary" onclick="generateNewChatApiKey()">Register Chat API Key</button>
|
|
||||||
<button type="button" class="btn btn-primary" onclick="generateNewApiKey()">Register API Key</button>
|
|
||||||
<!-- API Key Display Field -->
|
|
||||||
<div id="chat-api-key-field" style="display:none;">
|
|
||||||
<label for="chat-api-key">Chat API Key:</label>
|
|
||||||
<input type="text" id="chat-api-key" class="form-control" readonly>
|
|
||||||
<button type="button" id="copy-chat-button" class="btn btn-primary">Copy to Clipboard</button>
|
|
||||||
<p id="copy-chat-message" style="display:none;color:green;">Chat API key copied to clipboard</p>
|
|
||||||
</div>
|
|
||||||
<div id="api-key-field" style="display:none;">
|
|
||||||
<label for="api-key">API Key:</label>
|
|
||||||
<input type="text" id="api-key" class="form-control" readonly>
|
|
||||||
<button type="button" id="copy-api-button" class="btn btn-primary">Copy to Clipboard</button>
|
|
||||||
<p id="copy-message" style="display:none;color:green;">API key copied to clipboard</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content_footer %}
|
{% block content_footer %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
// Function to generate a new Chat API Key
|
|
||||||
function generateNewChatApiKey() {
|
|
||||||
generateApiKey('/admin/user/generate_chat_api_key', '#chat-api-key', '#chat-api-key-field');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to generate a new general API Key
|
|
||||||
function generateNewApiKey() {
|
|
||||||
generateApiKey('/admin/user/generate_api_api_key', '#api-key', '#api-key-field');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reusable function to handle API key generation
|
|
||||||
function generateApiKey(url, inputSelector, fieldSelector) {
|
|
||||||
$.ajax({
|
|
||||||
url: url,
|
|
||||||
type: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
success: function(response) {
|
|
||||||
$(inputSelector).val(response.api_key);
|
|
||||||
$(fieldSelector).show();
|
|
||||||
},
|
|
||||||
error: function(error) {
|
|
||||||
alert('Error generating new API key: ' + error.responseText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to copy text to clipboard
|
|
||||||
function copyToClipboard(selector, messageSelector) {
|
|
||||||
const element = document.querySelector(selector);
|
|
||||||
if (element) {
|
|
||||||
const text = element.value;
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
showCopyMessage(messageSelector);
|
|
||||||
}).catch(function(error) {
|
|
||||||
alert('Failed to copy text: ' + error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
fallbackCopyToClipboard(text, messageSelector);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Element not found for selector:', selector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback method for copying text to clipboard
|
|
||||||
function fallbackCopyToClipboard(text, messageSelector) {
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.value = text;
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
try {
|
|
||||||
document.execCommand('copy');
|
|
||||||
showCopyMessage(messageSelector);
|
|
||||||
} catch (err) {
|
|
||||||
alert('Fallback: Oops, unable to copy', err);
|
|
||||||
}
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to show copy confirmation message
|
|
||||||
function showCopyMessage(messageSelector) {
|
|
||||||
const message = document.querySelector(messageSelector);
|
|
||||||
if (message) {
|
|
||||||
message.style.display = 'block';
|
|
||||||
setTimeout(function() {
|
|
||||||
message.style.display = 'none';
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event listeners for copy buttons
|
|
||||||
document.getElementById('copy-chat-button').addEventListener('click', function() {
|
|
||||||
copyToClipboard('#chat-api-key', '#copy-chat-message');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('copy-api-button').addEventListener('click', function() {
|
|
||||||
copyToClipboard('#api-key', '#copy-message');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
// JavaScript to detect user's timezone
|
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
|
||||||
// Detect timezone
|
|
||||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
|
|
||||||
// Send timezone to the server via a POST request
|
|
||||||
fetch('/set_user_timezone', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ timezone: userTimezone })
|
|
||||||
}).then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
console.log('Timezone sent to server successfully');
|
|
||||||
} else {
|
|
||||||
console.error('Failed to send timezone to server');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -17,8 +17,15 @@ from config.type_defs.processor_types import PROCESSOR_TYPES
|
|||||||
from .dynamic_form_base import DynamicFormBase
|
from .dynamic_form_base import DynamicFormBase
|
||||||
|
|
||||||
|
|
||||||
|
def validate_catalog_name(form, field):
|
||||||
|
# Controleer of een catalog met deze naam al bestaat
|
||||||
|
existing_catalog = Catalog.query.filter_by(name=field.data).first()
|
||||||
|
if existing_catalog:
|
||||||
|
raise ValidationError(f'A Catalog with name "{field.data}" already exists. Choose another name.')
|
||||||
|
|
||||||
|
|
||||||
class CatalogForm(FlaskForm):
|
class CatalogForm(FlaskForm):
|
||||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name])
|
||||||
description = TextAreaField('Description', validators=[Optional()])
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
|
||||||
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
||||||
@@ -41,7 +48,7 @@ class CatalogForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class EditCatalogForm(DynamicFormBase):
|
class EditCatalogForm(DynamicFormBase):
|
||||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name])
|
||||||
description = TextAreaField('Description', validators=[Optional()])
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
|
||||||
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import json
|
|||||||
|
|
||||||
from wtforms.fields.choices import SelectField
|
from wtforms.fields.choices import SelectField
|
||||||
from wtforms.fields.datetime import DateField
|
from wtforms.fields.datetime import DateField
|
||||||
|
from wtforms.fields.simple import ColorField
|
||||||
from common.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
|
||||||
|
|
||||||
|
|
||||||
@@ -372,6 +373,7 @@ class DynamicFormBase(FlaskForm):
|
|||||||
'text': TextAreaField,
|
'text': TextAreaField,
|
||||||
'date': DateField,
|
'date': DateField,
|
||||||
'file': FileField,
|
'file': FileField,
|
||||||
|
'color': ColorField,
|
||||||
}.get(field_type, StringField)
|
}.get(field_type, StringField)
|
||||||
field_kwargs = {}
|
field_kwargs = {}
|
||||||
|
|
||||||
@@ -414,6 +416,14 @@ class DynamicFormBase(FlaskForm):
|
|||||||
render_kw['data-bs-toggle'] = 'tooltip'
|
render_kw['data-bs-toggle'] = 'tooltip'
|
||||||
render_kw['data-bs-placement'] = 'right'
|
render_kw['data-bs-placement'] = 'right'
|
||||||
|
|
||||||
|
# Add special styling for color fields to make them more compact and visible
|
||||||
|
if field_type == 'color':
|
||||||
|
render_kw['style'] = 'width: 100px; height: 40px;'
|
||||||
|
if 'class' in render_kw:
|
||||||
|
render_kw['class'] = f"{render_kw['class']} color-field"
|
||||||
|
else:
|
||||||
|
render_kw['class'] = 'color-field'
|
||||||
|
|
||||||
|
|
||||||
current_app.logger.debug(f"render_kw for {full_field_name}: {render_kw}")
|
current_app.logger.debug(f"render_kw for {full_field_name}: {render_kw}")
|
||||||
|
|
||||||
@@ -603,7 +613,7 @@ def validate_tagging_fields(form, field):
|
|||||||
raise ValidationError(f"Field {field_name} missing required 'type' property")
|
raise ValidationError(f"Field {field_name} missing required 'type' property")
|
||||||
|
|
||||||
# Validate type
|
# Validate type
|
||||||
if field_def['type'] not in ['string', 'integer', 'float', 'date', 'enum']:
|
if field_def['type'] not in ['string', 'integer', 'float', 'date', 'enum', 'color']:
|
||||||
raise ValidationError(f"Field {field_name} has invalid type: {field_def['type']}")
|
raise ValidationError(f"Field {field_name} has invalid type: {field_def['type']}")
|
||||||
|
|
||||||
# Validate enum fields have allowed_values
|
# Validate enum fields have allowed_values
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from flask_security import current_user
|
|||||||
|
|
||||||
from common.services.user import UserServices
|
from common.services.user import UserServices
|
||||||
from config.type_defs.service_types import SERVICE_TYPES
|
from config.type_defs.service_types import SERVICE_TYPES
|
||||||
|
from eveai_app.views.dynamic_form_base import DynamicFormBase
|
||||||
|
|
||||||
|
|
||||||
class TenantForm(FlaskForm):
|
class TenantForm(FlaskForm):
|
||||||
@@ -131,4 +132,13 @@ 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()]
|
||||||
|
|
||||||
|
|
||||||
|
class TenantMakeForm(DynamicFormBase):
|
||||||
|
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||||
|
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)])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ from flask_security import roles_accepted, current_user
|
|||||||
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
||||||
import ast
|
import ast
|
||||||
|
|
||||||
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant
|
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake
|
||||||
from common.extensions import db, security, minio_client, simple_encryption
|
from common.extensions import db, security, minio_client, simple_encryption, cache_manager
|
||||||
|
from common.utils.dynamic_field_utils import create_default_config_from_type_config
|
||||||
from common.utils.security_utils import send_confirmation_email, send_reset_email
|
from common.utils.security_utils import send_confirmation_email, send_reset_email
|
||||||
from config.type_defs.service_types import SERVICE_TYPES
|
from config.type_defs.service_types import SERVICE_TYPES
|
||||||
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
|
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
|
||||||
TenantProjectForm, EditTenantProjectForm
|
TenantProjectForm, EditTenantProjectForm, TenantMakeForm
|
||||||
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
|
||||||
@@ -622,6 +623,108 @@ def delete_tenant_project(tenant_project_id):
|
|||||||
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/tenant_make', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def tenant_make():
|
||||||
|
form = TenantMakeForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
tenant_id = session['tenant']['id']
|
||||||
|
new_tenant_make = TenantMake()
|
||||||
|
form.populate_obj(new_tenant_make)
|
||||||
|
new_tenant_make.tenant_id = tenant_id
|
||||||
|
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
|
||||||
|
new_tenant_make.chat_customisation_options = create_default_config_from_type_config(
|
||||||
|
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))
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(new_tenant_make)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Tenant Make successfully added!', 'success')
|
||||||
|
current_app.logger.info(f'Tenant Make {new_tenant_make.name} successfully added for tenant {tenant_id}!')
|
||||||
|
# Enable step 2 of creation of retriever - add configuration of the retriever (dependent on type)
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_makes', tenant_make_id=new_tenant_make.id))
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f'Failed to add Tenant Make. Error: {e}', 'danger')
|
||||||
|
current_app.logger.error(f'Failed to add Tenant Make {new_tenant_make.name}'
|
||||||
|
f'for tenant {tenant_id}. Error: {str(e)}')
|
||||||
|
|
||||||
|
return render_template('user/tenant_make.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/tenant_makes', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def tenant_makes():
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
per_page = request.args.get('per_page', 10, type=int)
|
||||||
|
|
||||||
|
query = TenantMake.query.order_by(TenantMake.id)
|
||||||
|
|
||||||
|
pagination = query.paginate(page=page, per_page=per_page)
|
||||||
|
tenant_makes = pagination.items
|
||||||
|
|
||||||
|
# prepare table data
|
||||||
|
rows = prepare_table_for_macro(tenant_makes,
|
||||||
|
[('id', ''), ('name', ''), ('website', ''), ('active', '')])
|
||||||
|
|
||||||
|
# Render the tenant makes in a template
|
||||||
|
return render_template('user/tenant_makes.html', rows=rows, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/tenant_make/<int:tenant_make_id>', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def edit_tenant_make(tenant_make_id):
|
||||||
|
"""Edit an existing tenant make configuration."""
|
||||||
|
# Get the tenant make or return 404
|
||||||
|
tenant_make = TenantMake.query.get_or_404(tenant_make_id)
|
||||||
|
|
||||||
|
# Create form instance with the tenant make
|
||||||
|
form = TenantMakeForm(request.form, obj=tenant_make)
|
||||||
|
|
||||||
|
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
|
||||||
|
form.add_dynamic_fields("configuration", customisation_config, tenant_make.chat_customisation_options)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
# Update basic fields
|
||||||
|
form.populate_obj(tenant_make)
|
||||||
|
tenant_make.chat_customisation_options = form.get_dynamic_data("configuration")
|
||||||
|
|
||||||
|
# Update logging information
|
||||||
|
update_logging_information(tenant_make, dt.now(tz.utc))
|
||||||
|
|
||||||
|
# Save changes to database
|
||||||
|
try:
|
||||||
|
db.session.add(tenant_make)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Tenant Make updated successfully!', 'success')
|
||||||
|
current_app.logger.info(f'Tenant Make {tenant_make.id} updated successfully')
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f'Failed to update tenant make. Error: {str(e)}', 'danger')
|
||||||
|
current_app.logger.error(f'Failed to update tenant make {tenant_make_id}. Error: {str(e)}')
|
||||||
|
return render_template('user/edit_tenant_make.html', form=form, tenant_make_id=tenant_make_id)
|
||||||
|
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_makes'))
|
||||||
|
else:
|
||||||
|
form_validation_failed(request, form)
|
||||||
|
|
||||||
|
return render_template('user/edit_tenant_make.html', form=form, tenant_make_id=tenant_make_id)
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/handle_tenant_make_selection', methods=['POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def handle_tenant_make_selection():
|
||||||
|
action = request.form['action']
|
||||||
|
if action == 'create_tenant_make':
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_make'))
|
||||||
|
tenant_make_identification = request.form.get('selected_row')
|
||||||
|
tenant_make_id = ast.literal_eval(tenant_make_identification).get('value')
|
||||||
|
|
||||||
|
if action == 'edit_tenant_make':
|
||||||
|
return redirect(prefixed_url_for('user_bp.edit_tenant_make', tenant_make_id=tenant_make_id))
|
||||||
|
|
||||||
def reset_uniquifier(user):
|
def reset_uniquifier(user):
|
||||||
security.datastore.set_uniquifier(user)
|
security.datastore.set_uniquifier(user)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
|
|||||||
106
eveai_chat_client/__init__.py
Normal file
106
eveai_chat_client/__init__.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from flask import Flask, jsonify
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
import logging.config
|
||||||
|
|
||||||
|
from common.extensions import (db, bootstrap, cors, csrf, session,
|
||||||
|
minio_client, simple_encryption, metrics, cache_manager, content_manager)
|
||||||
|
from common.models.user import Tenant, SpecialistMagicLinkTenant
|
||||||
|
from common.utils.startup_eveai import perform_startup_actions
|
||||||
|
from config.logging_config import LOGGING
|
||||||
|
from eveai_chat_client.utils.errors import register_error_handlers
|
||||||
|
from common.utils.celery_utils import make_celery, init_celery
|
||||||
|
from common.utils.template_filters import register_filters
|
||||||
|
from config.config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(config_file=None):
|
||||||
|
app = Flask(__name__, static_url_path='/static')
|
||||||
|
|
||||||
|
# Ensure all necessary headers are handled
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
||||||
|
|
||||||
|
environment = os.getenv('FLASK_ENV', 'development')
|
||||||
|
|
||||||
|
match environment:
|
||||||
|
case 'development':
|
||||||
|
app.config.from_object(get_config('dev'))
|
||||||
|
case 'production':
|
||||||
|
app.config.from_object(get_config('prod'))
|
||||||
|
case _:
|
||||||
|
app.config.from_object(get_config('dev'))
|
||||||
|
|
||||||
|
app.config['SESSION_KEY_PREFIX'] = 'eveai_chat_client_'
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(app.instance_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logging.config.dictConfig(LOGGING)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.info("eveai_chat_client starting up")
|
||||||
|
|
||||||
|
# Register extensions
|
||||||
|
register_extensions(app)
|
||||||
|
|
||||||
|
# Configure CSRF protection
|
||||||
|
app.config['WTF_CSRF_CHECK_DEFAULT'] = False # Disable global CSRF protection
|
||||||
|
app.config['WTF_CSRF_TIME_LIMIT'] = None # Remove time limit for CSRF tokens
|
||||||
|
|
||||||
|
app.celery = make_celery(app.name, app.config)
|
||||||
|
init_celery(app.celery, app)
|
||||||
|
|
||||||
|
# Register Blueprints
|
||||||
|
register_blueprints(app)
|
||||||
|
|
||||||
|
# Register Error Handlers
|
||||||
|
register_error_handlers(app)
|
||||||
|
|
||||||
|
# Register Cache Handlers
|
||||||
|
register_cache_handlers(app)
|
||||||
|
|
||||||
|
# Debugging settings
|
||||||
|
if app.config['DEBUG'] is True:
|
||||||
|
app.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Register template filters
|
||||||
|
register_filters(app)
|
||||||
|
|
||||||
|
# Perform startup actions such as cache invalidation
|
||||||
|
perform_startup_actions(app)
|
||||||
|
|
||||||
|
app.logger.info(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})")
|
||||||
|
app.logger.info("-------------------------------------------------------------------------------------------------")
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def register_extensions(app):
|
||||||
|
db.init_app(app)
|
||||||
|
bootstrap.init_app(app)
|
||||||
|
csrf.init_app(app)
|
||||||
|
cors.init_app(app)
|
||||||
|
simple_encryption.init_app(app)
|
||||||
|
session.init_app(app)
|
||||||
|
minio_client.init_app(app)
|
||||||
|
cache_manager.init_app(app)
|
||||||
|
metrics.init_app(app)
|
||||||
|
content_manager.init_app(app)
|
||||||
|
|
||||||
|
|
||||||
|
def register_blueprints(app):
|
||||||
|
from .views.chat_views import chat_bp
|
||||||
|
app.register_blueprint(chat_bp)
|
||||||
|
from .views.error_views import error_bp
|
||||||
|
app.register_blueprint(error_bp)
|
||||||
|
from .views.healthz_views import healthz_bp
|
||||||
|
app.register_blueprint(healthz_bp)
|
||||||
|
|
||||||
|
|
||||||
|
def register_cache_handlers(app):
|
||||||
|
from common.utils.cache.config_cache import register_config_cache_handlers
|
||||||
|
register_config_cache_handlers(cache_manager)
|
||||||
|
from common.utils.cache.crewai_processed_config_cache import register_specialist_cache_handlers
|
||||||
|
register_specialist_cache_handlers(cache_manager)
|
||||||
244
eveai_chat_client/static/css/chat.css
Normal file
244
eveai_chat_client/static/css/chat.css
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/* Base styles */
|
||||||
|
:root {
|
||||||
|
--primary-color: #007bff;
|
||||||
|
--secondary-color: #6c757d;
|
||||||
|
--background-color: #ffffff;
|
||||||
|
--text-color: #212529;
|
||||||
|
--sidebar-color: #f8f9fa;
|
||||||
|
--message-user-bg: #e9f5ff;
|
||||||
|
--message-bot-bg: #f8f9fa;
|
||||||
|
--border-radius: 8px;
|
||||||
|
--spacing: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat layout */
|
||||||
|
.chat-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background-color: var(--sidebar-color);
|
||||||
|
border-right: 1px solid rgba(0,0,0,0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--spacing);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
margin-bottom: var(--spacing);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-text {
|
||||||
|
margin-bottom: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-info {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: var(--spacing);
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-member {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-member img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
padding: var(--spacing);
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: var(--spacing);
|
||||||
|
max-width: 80%;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-message {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message .message-content {
|
||||||
|
background-color: var(--message-user-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-message .message-content {
|
||||||
|
background-color: var(--message-bot-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-container {
|
||||||
|
padding: var(--spacing);
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.1);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.2);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
resize: none;
|
||||||
|
height: 60px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#send-button {
|
||||||
|
padding: 0 24px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading indicator */
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
background-color: rgba(0,0,0,0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
animation: typing 1.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.5); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error page styles */
|
||||||
|
.error-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-actions {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chat-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 30%;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
eveai_chat_client/templates/base.html
Normal file
31
eveai_chat_client/templates/base.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}EveAI Chat{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}">
|
||||||
|
|
||||||
|
<!-- Custom theme colors from tenant settings -->
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: {{ customization.primary_color|default('#007bff') }};
|
||||||
|
--secondary-color: {{ customization.secondary_color|default('#6c757d') }};
|
||||||
|
--background-color: {{ customization.background_color|default('#ffffff') }};
|
||||||
|
--text-color: {{ customization.text_color|default('#212529') }};
|
||||||
|
--sidebar-color: {{ customization.sidebar_color|default('#f8f9fa') }};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
214
eveai_chat_client/templates/chat.html
Normal file
214
eveai_chat_client/templates/chat.html
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Chat{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="chat-container">
|
||||||
|
<!-- Left sidebar with customizable content -->
|
||||||
|
<div class="sidebar">
|
||||||
|
{% if customisation.logo_url %}
|
||||||
|
<div class="logo">
|
||||||
|
<img src="{{ customisation.logo_url }}" alt="{{ tenant.name }} Logo">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="sidebar-content">
|
||||||
|
{% if customisation.sidebar_text %}
|
||||||
|
<div class="sidebar-text">
|
||||||
|
{{ customisation.sidebar_text|safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if customisation.team_info %}
|
||||||
|
<div class="team-info">
|
||||||
|
<h3>Team</h3>
|
||||||
|
<div class="team-members">
|
||||||
|
{% for member in customisation.team_info %}
|
||||||
|
<div class="team-member">
|
||||||
|
{% if member.avatar %}
|
||||||
|
<img src="{{ member.avatar }}" alt="{{ member.name }}">
|
||||||
|
{% endif %}
|
||||||
|
<div class="member-info">
|
||||||
|
<h4>{{ member.name }}</h4>
|
||||||
|
<p>{{ member.role }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main chat area -->
|
||||||
|
<div class="chat-main">
|
||||||
|
<div class="chat-header">
|
||||||
|
<h1>{{ specialist.name }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-messages" id="chat-messages">
|
||||||
|
<!-- Messages will be added here dynamically -->
|
||||||
|
{% if customisation.welcome_message %}
|
||||||
|
<div class="message bot-message">
|
||||||
|
<div class="message-content">{{ customisation.welcome_message|safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="message bot-message">
|
||||||
|
<div class="message-content">Hello! How can I help you today?</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-input-container">
|
||||||
|
<textarea id="chat-input" placeholder="Type your message here..."></textarea>
|
||||||
|
<button id="send-button">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Store session information
|
||||||
|
const sessionInfo = {
|
||||||
|
tenantId: {{ tenant.id }},
|
||||||
|
specialistId: {{ specialist.id }},
|
||||||
|
chatSessionId: "{{ session.chat_session_id }}"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chat functionality
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
const sendButton = document.getElementById('send-button');
|
||||||
|
const chatMessages = document.getElementById('chat-messages');
|
||||||
|
|
||||||
|
let currentTaskId = null;
|
||||||
|
let pollingInterval = null;
|
||||||
|
|
||||||
|
// Function to add a message to the chat
|
||||||
|
function addMessage(message, isUser = false) {
|
||||||
|
const messageDiv = document.createElement('div');
|
||||||
|
messageDiv.className = isUser ? 'message user-message' : 'message bot-message';
|
||||||
|
|
||||||
|
const contentDiv = document.createElement('div');
|
||||||
|
contentDiv.className = 'message-content';
|
||||||
|
contentDiv.innerHTML = message;
|
||||||
|
|
||||||
|
messageDiv.appendChild(contentDiv);
|
||||||
|
chatMessages.appendChild(messageDiv);
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to send a message
|
||||||
|
function sendMessage() {
|
||||||
|
const message = chatInput.value.trim();
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
// Add user message to chat
|
||||||
|
addMessage(message, true);
|
||||||
|
|
||||||
|
// Clear input
|
||||||
|
chatInput.value = '';
|
||||||
|
|
||||||
|
// Add loading indicator
|
||||||
|
const loadingDiv = document.createElement('div');
|
||||||
|
loadingDiv.className = 'message bot-message loading';
|
||||||
|
loadingDiv.innerHTML = '<div class="message-content"><div class="typing-indicator"><span></span><span></span><span></span></div></div>';
|
||||||
|
chatMessages.appendChild(loadingDiv);
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
|
||||||
|
// Send message to server
|
||||||
|
fetch('/api/send_message', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: message,
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'processing') {
|
||||||
|
currentTaskId = data.task_id;
|
||||||
|
|
||||||
|
// Start polling for results
|
||||||
|
if (pollingInterval) clearInterval(pollingInterval);
|
||||||
|
pollingInterval = setInterval(checkTaskStatus, 1000);
|
||||||
|
} else {
|
||||||
|
// Remove loading indicator
|
||||||
|
chatMessages.removeChild(loadingDiv);
|
||||||
|
|
||||||
|
// Show error if any
|
||||||
|
if (data.error) {
|
||||||
|
addMessage(`Error: ${data.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Remove loading indicator
|
||||||
|
chatMessages.removeChild(loadingDiv);
|
||||||
|
addMessage(`Error: ${error.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check task status
|
||||||
|
function checkTaskStatus() {
|
||||||
|
if (!currentTaskId) return;
|
||||||
|
|
||||||
|
fetch(`/api/check_status?task_id=${currentTaskId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Remove loading indicator
|
||||||
|
const loadingDiv = document.querySelector('.loading');
|
||||||
|
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||||
|
|
||||||
|
// Add bot response
|
||||||
|
addMessage(data.answer);
|
||||||
|
|
||||||
|
// Clear polling
|
||||||
|
clearInterval(pollingInterval);
|
||||||
|
currentTaskId = null;
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
// Remove loading indicator
|
||||||
|
const loadingDiv = document.querySelector('.loading');
|
||||||
|
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||||
|
|
||||||
|
// Show error
|
||||||
|
addMessage(`Error: ${data.message}`);
|
||||||
|
|
||||||
|
// Clear polling
|
||||||
|
clearInterval(pollingInterval);
|
||||||
|
currentTaskId = null;
|
||||||
|
}
|
||||||
|
// If status is 'pending', continue polling
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Remove loading indicator
|
||||||
|
const loadingDiv = document.querySelector('.loading');
|
||||||
|
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||||
|
|
||||||
|
addMessage(`Error checking status: ${error.message}`);
|
||||||
|
|
||||||
|
// Clear polling
|
||||||
|
clearInterval(pollingInterval);
|
||||||
|
currentTaskId = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
sendButton.addEventListener('click', sendMessage);
|
||||||
|
|
||||||
|
chatInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
15
eveai_chat_client/templates/error.html
Normal file
15
eveai_chat_client/templates/error.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Error{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="error-box">
|
||||||
|
<h1>Oops! Something went wrong</h1>
|
||||||
|
<p class="error-message">{{ message }}</p>
|
||||||
|
<div class="error-actions">
|
||||||
|
<a href="/" class="btn-primary">Go to Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
1
eveai_chat_client/utils/__init__.py
Normal file
1
eveai_chat_client/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Utils package for eveai_chat_client
|
||||||
85
eveai_chat_client/utils/errors.py
Normal file
85
eveai_chat_client/utils/errors.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
import jinja2
|
||||||
|
from flask import render_template, request, jsonify, redirect, current_app, flash
|
||||||
|
|
||||||
|
from common.utils.eveai_exceptions import EveAINoSessionTenant
|
||||||
|
|
||||||
|
|
||||||
|
def not_found_error(error):
|
||||||
|
current_app.logger.error(f"Not Found Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Page not found."), 404
|
||||||
|
|
||||||
|
|
||||||
|
def internal_server_error(error):
|
||||||
|
current_app.logger.error(f"Internal Server Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Internal server error."), 500
|
||||||
|
|
||||||
|
|
||||||
|
def not_authorised_error(error):
|
||||||
|
current_app.logger.error(f"Not Authorised Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Not authorized."), 401
|
||||||
|
|
||||||
|
|
||||||
|
def access_forbidden(error):
|
||||||
|
current_app.logger.error(f"Access Forbidden: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Access forbidden."), 403
|
||||||
|
|
||||||
|
|
||||||
|
def key_error_handler(error):
|
||||||
|
current_app.logger.error(f"Key Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="An unexpected error occurred."), 500
|
||||||
|
|
||||||
|
|
||||||
|
def attribute_error_handler(error):
|
||||||
|
"""Handle AttributeError exceptions."""
|
||||||
|
error_msg = str(error)
|
||||||
|
current_app.logger.error(f"AttributeError: {error_msg}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="An application error occurred."), 500
|
||||||
|
|
||||||
|
|
||||||
|
def no_tenant_selected_error(error):
|
||||||
|
"""Handle errors when no tenant is selected in the current session."""
|
||||||
|
current_app.logger.error(f"No Session Tenant Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Session expired. Please use a valid magic link."), 401
|
||||||
|
|
||||||
|
|
||||||
|
def general_exception(e):
|
||||||
|
current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True)
|
||||||
|
return render_template('error.html', message="An application error occurred."), 500
|
||||||
|
|
||||||
|
|
||||||
|
def template_not_found_error(error):
|
||||||
|
"""Handle Jinja2 TemplateNotFound exceptions."""
|
||||||
|
current_app.logger.error(f'Template not found: {error.name}')
|
||||||
|
current_app.logger.error(f'Search Paths: {current_app.jinja_loader.list_templates()}')
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Template not found."), 404
|
||||||
|
|
||||||
|
|
||||||
|
def template_syntax_error(error):
|
||||||
|
"""Handle Jinja2 TemplateSyntaxError exceptions."""
|
||||||
|
current_app.logger.error(f'Template syntax error: {error.message}')
|
||||||
|
current_app.logger.error(f'In template {error.filename}, line {error.lineno}')
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Template syntax error."), 500
|
||||||
|
|
||||||
|
|
||||||
|
def register_error_handlers(app):
|
||||||
|
app.register_error_handler(404, not_found_error)
|
||||||
|
app.register_error_handler(500, internal_server_error)
|
||||||
|
app.register_error_handler(401, not_authorised_error)
|
||||||
|
app.register_error_handler(403, not_authorised_error)
|
||||||
|
app.register_error_handler(EveAINoSessionTenant, no_tenant_selected_error)
|
||||||
|
app.register_error_handler(KeyError, key_error_handler)
|
||||||
|
app.register_error_handler(AttributeError, attribute_error_handler)
|
||||||
|
app.register_error_handler(jinja2.TemplateNotFound, template_not_found_error)
|
||||||
|
app.register_error_handler(jinja2.TemplateSyntaxError, template_syntax_error)
|
||||||
|
app.register_error_handler(Exception, general_exception)
|
||||||
1
eveai_chat_client/views/__init__.py
Normal file
1
eveai_chat_client/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Views package for eveai_chat_client
|
||||||
170
eveai_chat_client/views/chat_views.py
Normal file
170
eveai_chat_client/views/chat_views.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import uuid
|
||||||
|
from flask import Blueprint, render_template, request, session, current_app, jsonify, abort
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from common.extensions import db
|
||||||
|
from common.models.user import Tenant, SpecialistMagicLinkTenant
|
||||||
|
from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction
|
||||||
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
|
from common.utils.database import Database
|
||||||
|
from common.utils.chat_utils import get_default_chat_customisation
|
||||||
|
|
||||||
|
chat_bp = Blueprint('chat', __name__)
|
||||||
|
|
||||||
|
@chat_bp.route('/')
|
||||||
|
def index():
|
||||||
|
customisation = get_default_chat_customisation()
|
||||||
|
return render_template('error.html', message="Please use a valid magic link to access the chat.",
|
||||||
|
customisation=customisation)
|
||||||
|
|
||||||
|
|
||||||
|
@chat_bp.route('/<magic_link_code>')
|
||||||
|
def chat(magic_link_code):
|
||||||
|
"""
|
||||||
|
Main chat interface accessed via magic link
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Find the tenant using the magic link code
|
||||||
|
magic_link_tenant = SpecialistMagicLinkTenant.query.filter_by(magic_link_code=magic_link_code).first()
|
||||||
|
|
||||||
|
if not magic_link_tenant:
|
||||||
|
current_app.logger.error(f"Invalid magic link code: {magic_link_code}")
|
||||||
|
return render_template('error.html', message="Invalid magic link code.")
|
||||||
|
|
||||||
|
tenant_id = magic_link_tenant.tenant_id
|
||||||
|
|
||||||
|
# Get tenant information
|
||||||
|
tenant = Tenant.query.get(tenant_id)
|
||||||
|
if not tenant:
|
||||||
|
current_app.logger.error(f"Tenant not found for ID: {tenant_id}")
|
||||||
|
return render_template('error.html', message="Tenant not found.")
|
||||||
|
|
||||||
|
# Switch to tenant schema
|
||||||
|
Database(tenant_id).switch_schema()
|
||||||
|
|
||||||
|
# Get specialist magic link details from tenant schema
|
||||||
|
specialist_ml = SpecialistMagicLink.query.filter_by(magic_link_code=magic_link_code).first()
|
||||||
|
if not specialist_ml:
|
||||||
|
current_app.logger.error(f"Specialist magic link not found in tenant schema: {tenant_id}")
|
||||||
|
return render_template('error.html', message="Specialist configuration not found.")
|
||||||
|
|
||||||
|
# Get specialist details
|
||||||
|
specialist = Specialist.query.get(specialist_ml.specialist_id)
|
||||||
|
if not specialist:
|
||||||
|
current_app.logger.error(f"Specialist not found: {specialist_ml.specialist_id}")
|
||||||
|
return render_template('error.html', message="Specialist not found.")
|
||||||
|
|
||||||
|
# Store necessary information in session
|
||||||
|
session['tenant_id'] = tenant_id
|
||||||
|
session['specialist_id'] = specialist_ml.specialist_id
|
||||||
|
session['specialist_args'] = specialist_ml.specialist_args or {}
|
||||||
|
session['magic_link_code'] = magic_link_code
|
||||||
|
|
||||||
|
# Get customisation options with defaults
|
||||||
|
customisation = get_default_chat_customisation(tenant.chat_customisation_options)
|
||||||
|
|
||||||
|
# Start a new chat session
|
||||||
|
session['chat_session_id'] = SpecialistServices.start_session()
|
||||||
|
|
||||||
|
return render_template('chat.html',
|
||||||
|
tenant=tenant,
|
||||||
|
specialist=specialist,
|
||||||
|
customisation=customisation)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)
|
||||||
|
return render_template('error.html', message="An error occurred while setting up the chat.")
|
||||||
|
|
||||||
|
@chat_bp.route('/api/send_message', methods=['POST'])
|
||||||
|
def send_message():
|
||||||
|
"""
|
||||||
|
API endpoint to send a message to the specialist
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
message = data.get('message')
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
return jsonify({'error': 'No message provided'}), 400
|
||||||
|
|
||||||
|
tenant_id = session.get('tenant_id')
|
||||||
|
specialist_id = session.get('specialist_id')
|
||||||
|
chat_session_id = session.get('chat_session_id')
|
||||||
|
specialist_args = session.get('specialist_args', {})
|
||||||
|
|
||||||
|
if not all([tenant_id, specialist_id, chat_session_id]):
|
||||||
|
return jsonify({'error': 'Session expired or invalid'}), 400
|
||||||
|
|
||||||
|
# Switch to tenant schema
|
||||||
|
Database(tenant_id).switch_schema()
|
||||||
|
|
||||||
|
# Add user message to specialist arguments
|
||||||
|
specialist_args['user_message'] = message
|
||||||
|
|
||||||
|
# Execute specialist
|
||||||
|
result = SpecialistServices.execute_specialist(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
specialist_id=specialist_id,
|
||||||
|
specialist_arguments=specialist_args,
|
||||||
|
session_id=chat_session_id,
|
||||||
|
user_timezone=data.get('timezone', 'UTC')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store the task ID for polling
|
||||||
|
session['current_task_id'] = result['task_id']
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'processing',
|
||||||
|
'task_id': result['task_id']
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error sending message: {str(e)}", exc_info=True)
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@chat_bp.route('/api/check_status', methods=['GET'])
|
||||||
|
def check_status():
|
||||||
|
"""
|
||||||
|
API endpoint to check the status of a task
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
task_id = request.args.get('task_id') or session.get('current_task_id')
|
||||||
|
|
||||||
|
if not task_id:
|
||||||
|
return jsonify({'error': 'No task ID provided'}), 400
|
||||||
|
|
||||||
|
tenant_id = session.get('tenant_id')
|
||||||
|
if not tenant_id:
|
||||||
|
return jsonify({'error': 'Session expired or invalid'}), 400
|
||||||
|
|
||||||
|
# Switch to tenant schema
|
||||||
|
Database(tenant_id).switch_schema()
|
||||||
|
|
||||||
|
# Check task status using Celery
|
||||||
|
task_result = current_app.celery.AsyncResult(task_id)
|
||||||
|
|
||||||
|
if task_result.state == 'PENDING':
|
||||||
|
return jsonify({'status': 'pending'})
|
||||||
|
elif task_result.state == 'SUCCESS':
|
||||||
|
result = task_result.result
|
||||||
|
|
||||||
|
# Format the response
|
||||||
|
specialist_result = result.get('result', {})
|
||||||
|
response = {
|
||||||
|
'status': 'success',
|
||||||
|
'answer': specialist_result.get('answer', ''),
|
||||||
|
'citations': specialist_result.get('citations', []),
|
||||||
|
'insufficient_info': specialist_result.get('insufficient_info', False),
|
||||||
|
'interaction_id': result.get('interaction_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(response)
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': str(task_result.info)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error checking status: {str(e)}", exc_info=True)
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
24
eveai_chat_client/views/error_views.py
Normal file
24
eveai_chat_client/views/error_views.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
error_bp = Blueprint('error', __name__)
|
||||||
|
|
||||||
|
@error_bp.route('/error')
|
||||||
|
def error_page():
|
||||||
|
"""
|
||||||
|
Generic error page
|
||||||
|
"""
|
||||||
|
return render_template('error.html', message="An error occurred.")
|
||||||
|
|
||||||
|
@error_bp.app_errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
"""
|
||||||
|
Handle 404 errors
|
||||||
|
"""
|
||||||
|
return render_template('error.html', message="Page not found."), 404
|
||||||
|
|
||||||
|
@error_bp.app_errorhandler(500)
|
||||||
|
def internal_server_error(e):
|
||||||
|
"""
|
||||||
|
Handle 500 errors
|
||||||
|
"""
|
||||||
|
return render_template('error.html', message="Internal server error."), 500
|
||||||
17
eveai_chat_client/views/healthz_views.py
Normal file
17
eveai_chat_client/views/healthz_views.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
|
||||||
|
healthz_bp = Blueprint('healthz', __name__)
|
||||||
|
|
||||||
|
@healthz_bp.route('/healthz/ready')
|
||||||
|
def ready():
|
||||||
|
"""
|
||||||
|
Health check endpoint for readiness probe
|
||||||
|
"""
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
@healthz_bp.route('/healthz/live')
|
||||||
|
def live():
|
||||||
|
"""
|
||||||
|
Health check endpoint for liveness probe
|
||||||
|
"""
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
@@ -40,7 +40,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def type_version(self) -> str:
|
def type_version(self) -> str:
|
||||||
return "1.1"
|
return "1.2"
|
||||||
|
|
||||||
def _config_task_agents(self):
|
def _config_task_agents(self):
|
||||||
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
|
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
|
||||||
|
|||||||
@@ -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,52 @@
|
|||||||
|
"""Add TenantMake model
|
||||||
|
|
||||||
|
Revision ID: 200bda7f5251
|
||||||
|
Revises: b6146237f298
|
||||||
|
Create Date: 2025-06-06 13:48:40.208711
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '200bda7f5251'
|
||||||
|
down_revision = 'b6146237f298'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('tenant_make',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('active', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('website', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('logo_url', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('chat_customisation_options', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_by', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['created_by'], ['public.user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['public.tenant.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['updated_by'], ['public.user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
schema='public'
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('chat_customisation_options')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('chat_customisation_options', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True))
|
||||||
|
|
||||||
|
op.drop_table('tenant_make', schema='public')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Add Chat Configuration Options to Tenant model
|
||||||
|
|
||||||
|
Revision ID: b6146237f298
|
||||||
|
Revises: 2b4cb553530e
|
||||||
|
Create Date: 2025-06-06 03:45:24.264045
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b6146237f298'
|
||||||
|
down_revision = '2b4cb553530e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('chat_customisation_options', postgresql.JSONB(astext_type=sa.Text()), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('chat_customisation_options')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Make Catalog Name Unique
|
||||||
|
|
||||||
|
Revision ID: c71facc0ce7e
|
||||||
|
Revises: d69520ec540d
|
||||||
|
Create Date: 2025-06-07 08:38:23.759681
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import pgvector
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c71facc0ce7e'
|
||||||
|
down_revision = 'd69520ec540d'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_unique_constraint(None, 'catalog', ['name'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -18,6 +18,11 @@ http {
|
|||||||
include mime.types;
|
include mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Define upstream servers
|
||||||
|
upstream eveai_chat_client {
|
||||||
|
server eveai_chat_client:5004;
|
||||||
|
}
|
||||||
|
|
||||||
log_format custom_log_format '$remote_addr - $remote_user [$time_local] "$request" '
|
log_format custom_log_format '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||||
@@ -93,6 +98,26 @@ http {
|
|||||||
# add_header 'Access-Control-Allow-Credentials' 'true' always;
|
# add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
location /chat-client/ {
|
||||||
|
proxy_pass http://eveai_chat_client/;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /chat-client;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_buffering off;
|
||||||
|
|
||||||
|
# Add CORS headers
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||||
|
}
|
||||||
|
|
||||||
location /admin/ {
|
location /admin/ {
|
||||||
# include uwsgi_params;
|
# include uwsgi_params;
|
||||||
# uwsgi_pass 127.0.0.1:5001;
|
# uwsgi_pass 127.0.0.1:5001;
|
||||||
|
|||||||
0
nginx/static/css/eveai-chat-style.css
Normal file
0
nginx/static/css/eveai-chat-style.css
Normal file
@@ -1,7 +1,7 @@
|
|||||||
from gevent import monkey
|
from gevent import monkey
|
||||||
monkey.patch_all()
|
monkey.patch_all()
|
||||||
|
|
||||||
from eveai_chat import create_app
|
from eveai_chat_client import create_app
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
cd "/app/" || exit 1
|
|
||||||
export PROJECT_DIR="/app"
|
|
||||||
export PYTHONPATH="$PROJECT_DIR/patched_packages:$PYTHONPATH:$PROJECT_DIR" # Include the app directory in the Python path & patched packages
|
|
||||||
|
|
||||||
# Ensure we can write the logs
|
|
||||||
chown -R appuser:appuser /app/logs
|
|
||||||
|
|
||||||
# Set flask environment variables
|
|
||||||
#export FLASK_ENV=development # Use 'production' as appropriate
|
|
||||||
#export FLASK_DEBUG=1 # Use 0 for production
|
|
||||||
echo "Starting EveAI Chat"
|
|
||||||
|
|
||||||
# Start Flask app
|
|
||||||
gunicorn -w 1 -k gevent -b 0.0.0.0:5002 --worker-connections 100 scripts.run_eveai_chat:app
|
|
||||||
23
scripts/start_eveai_chat_client.sh
Executable file
23
scripts/start_eveai_chat_client.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "/app" || exit 1
|
||||||
|
export PYTHONPATH="$PYTHONPATH:/app/"
|
||||||
|
|
||||||
|
# Ensure we can write the logs
|
||||||
|
chown -R appuser:appuser /app/logs
|
||||||
|
|
||||||
|
# Wait for the database to be ready
|
||||||
|
echo "Waiting for database to be ready"
|
||||||
|
until pg_isready -h $DB_HOST -p $DB_PORT; do
|
||||||
|
echo "Postgres is unavailable - sleeping"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "Postgres is up - executing commands"
|
||||||
|
|
||||||
|
# Set FLASK_APP environment variables
|
||||||
|
PROJECT_DIR="/app"
|
||||||
|
export FLASK_APP=${PROJECT_DIR}/scripts/run_eveai_chat_client.py
|
||||||
|
export PYTHONPATH="$PROJECT_DIR/patched_packages:$PYTHONPATH:$PROJECT_DIR"
|
||||||
|
|
||||||
|
# Start Flask app with Gunicorn
|
||||||
|
gunicorn -w 1 -k gevent -b 0.0.0.0:5004 --worker-connections 100 scripts.run_eveai_chat_client:app
|
||||||
Reference in New Issue
Block a user