diff --git a/common/models/entitlements.py b/common/models/entitlements.py index d33f98b..b317725 100644 --- a/common/models/entitlements.py +++ b/common/models/entitlements.py @@ -57,6 +57,12 @@ class License(db.Model): overage_embedding = db.Column(db.Float, nullable=False, default=0) overage_interaction = db.Column(db.Float, nullable=False, default=0) + # Versioning Information + created_at = db.Column(db.DateTime, nullable=True, 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=True, server_default=db.func.now(), onupdate=db.func.now()) + updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True) + tenant = db.relationship('Tenant', back_populates='licenses') license_tier = db.relationship('LicenseTier', back_populates='licenses') usages = db.relationship('LicenseUsage', order_by='LicenseUsage.period_start_date', back_populates='license') @@ -88,7 +94,33 @@ class LicenseTier(db.Model): standard_overage_embedding = db.Column(db.Float, nullable=False, default=0) standard_overage_interaction = db.Column(db.Float, nullable=False, default=0) + # Versioning Information + created_at = db.Column(db.DateTime, nullable=True, 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=True, server_default=db.func.now(), onupdate=db.func.now()) + updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True) + licenses = db.relationship('License', back_populates='license_tier') + partner_services = db.relationship('PartnerServiceLicenseTier', back_populates='license_tier') + + +class PartnerServiceLicenseTier(db.Model): + __bind_key__ = 'public' + __table_args__ = {'schema': 'public'} + + partner_service_id = db.Column(db.Integer, db.ForeignKey('public.partner_service.id'), primary_key=True, + nullable=False) + license_tier_id = db.Column(db.Integer, db.ForeignKey('public.license_tier.id'), primary_key=True, + nullable=False) + + # Versioning Information + created_at = db.Column(db.DateTime, nullable=True, 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=True, server_default=db.func.now(), onupdate=db.func.now()) + updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True) + + license_tier = db.relationship('LicenseTier', back_populates='partner_services') + partner_service = db.relationship('PartnerService', back_populates='license_tiers') class LicenseUsage(db.Model): diff --git a/common/models/user.py b/common/models/user.py index d96eeff..d15ca22 100644 --- a/common/models/user.py +++ b/common/models/user.py @@ -211,7 +211,8 @@ class Partner(db.Model): 'type': service.type, 'type_version': service.type_version, 'active': service.active, - 'configuration': service.configuration + 'configuration': service.configuration, + 'permissions': service.permissions, }) return { 'id': self.id, @@ -250,15 +251,16 @@ class PartnerService(db.Model): system_metadata = db.Column(db.JSON, nullable=True) user_metadata = db.Column(db.JSON, nullable=True) - # Relationships - partner = db.relationship('Partner', back_populates='services') - # 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'), nullable=True) + # Relationships + partner = db.relationship('Partner', back_populates='services') + license_tiers = db.relationship('PartnerServiceLicenseTier', back_populates='partner_service') + class PartnerTenant(db.Model): __bind_key__ = 'public' @@ -267,9 +269,6 @@ class PartnerTenant(db.Model): partner_service_id = db.Column(db.Integer, db.ForeignKey('public.partner_service.id'), primary_key=True) tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), primary_key=True) - # Relationship type - relationship_type = db.Column(db.String(20), nullable=False) # REFERRED, MANAGED, WHITE_LABEL - # JSONB for flexible configuration specific to this relationship configuration = db.Column(db.JSON, nullable=True) diff --git a/common/services/entitlement_services.py b/common/services/entitlement_services.py new file mode 100644 index 0000000..88312f3 --- /dev/null +++ b/common/services/entitlement_services.py @@ -0,0 +1,68 @@ +from flask import session, current_app, flash +from sqlalchemy.exc import SQLAlchemyError + +from common.extensions import db +from common.models.entitlements import PartnerServiceLicenseTier +from common.models.user import Partner, PartnerTenant +from common.utils.eveai_exceptions import EveAINoManagementPartnerService +from common.utils.model_logging_utils import set_logging_information +from datetime import datetime as dt, timezone as tz + +from common.utils.security_utils import current_user_has_role + + +class EntitlementServices: + @staticmethod + def associate_license_tier_with_partner(license_tier_id): + """Associate a license tier with a partner""" + try: + partner_id = session['partner']['id'] + # Get partner service (MANAGEMENT_SERVICE type) + partner = Partner.query.get(partner_id) + if not partner: + return + + # Find a management service for this partner + management_service = next((service for service in session['partner']['services'] + if service.get('type') == 'MANAGEMENT_SERVICE'), None) + + if not management_service: + flash("Cannot associate license tier with partner. No management service defined for partner", "danger") + current_app.logger.error(f"No Management Service defined for partner {partner_id}" + f"trying to associate license tier {license_tier_id}.") + raise EveAINoManagementPartnerService() + # Check if the association already exists + existing_association = PartnerServiceLicenseTier.query.filter_by( + partner_service_id=management_service['id'], + license_tier_id=license_tier_id + ).first() + + if existing_association: + # Association already exists, nothing to do + flash("License tier was already associated with partner", "info") + current_app.logger.info(f"Association between partner service {management_service['id']} and " + f"license tier {license_tier_id} already exists.") + return + + # Create the association + association = PartnerServiceLicenseTier( + partner_service_id=management_service['id'], + license_tier_id=license_tier_id + ) + set_logging_information(association, dt.now(tz.utc)) + + db.session.add(association) + db.session.commit() + + flash("Successfully associated license tier to partner", "success") + current_app.logger.info(f"Successfully associated license tier {license_tier_id} with " + f"partner service {management_service['id']}") + + return True + + except SQLAlchemyError as e: + db.session.rollback() + flash("Failed to associated license tier with partner service due to an internal error. " + "Please contact the System Administrator", "danger") + current_app.logger.error(f"Error associating license tier {license_tier_id} with partner: {str(e)}") + raise e diff --git a/common/services/partner_services.py b/common/services/partner_services.py new file mode 100644 index 0000000..d81deb6 --- /dev/null +++ b/common/services/partner_services.py @@ -0,0 +1,47 @@ +from typing import List + +from flask import session +from sqlalchemy.exc import SQLAlchemyError + +from common.models.entitlements import PartnerServiceLicenseTier +from common.utils.eveai_exceptions import EveAINoManagementPartnerService, EveAINoSessionPartner + +from common.utils.security_utils import current_user_has_role + + +class PartnerServices: + @staticmethod + def get_allowed_license_tier_ids() -> List[int]: + """ + Retrieve IDs of all License Tiers associated with the partner's management service + + Returns: + List of license tier IDs + + Raises: + EveAINoSessionPartner: If no partner is in the session + EveAINoManagementPartnerService: If partner has no management service + """ + partner = session.get("partner", None) + if not partner: + raise EveAINoSessionPartner() + + # Find a management service for this partner + management_service = next((service for service in session['partner']['services'] + if service.get('type') == 'MANAGEMENT_SERVICE'), None) + if not management_service: + raise EveAINoManagementPartnerService() + management_service_id = management_service['id'] + + # Query for all license tiers associated with this management service + associations = PartnerServiceLicenseTier.query.filter_by( + partner_service_id=management_service_id + ).all() + + # Extract the license tier IDs + license_tier_ids = [assoc.license_tier_id for assoc in associations] + + return license_tier_ids + + + diff --git a/common/services/tenant_service.py b/common/services/tenant_service.py deleted file mode 100644 index a925b60..0000000 --- a/common/services/tenant_service.py +++ /dev/null @@ -1,71 +0,0 @@ -from flask import session, current_app -from sqlalchemy.exc import SQLAlchemyError - -from common.extensions import db -from common.models.user import Partner, PartnerTenant -from common.utils.eveai_exceptions import EveAINoManagementPartnerService -from common.utils.model_logging_utils import set_logging_information -from datetime import datetime as dt, timezone as tz - -from common.utils.security_utils import current_user_has_role - - -class TenantService: - @staticmethod - def associate_tenant_with_partner(tenant_id): - """Associate a tenant with a partner""" - try: - partner_id = session['partner']['id'] - # Get partner service (MANAGEMENT_SERVICE type) - partner = Partner.query.get(partner_id) - if not partner: - return - - # Find a management service for this partner - management_service = next((service for service in session['partner']['services'] - if service.get('type') == 'MANAGEMENT_SERVICE'), None) - - if not management_service: - current_app.logger.error(f"No Management Service defined for partner {partner_id}" - f"while associating tenant {tenant_id} with partner.") - raise EveAINoManagementPartnerService() - - # Create the association - tenant_partner = PartnerTenant( - partner_service_id=management_service['id'], - tenant_id=tenant_id, - relationship_type='MANAGED', - ) - set_logging_information(tenant_partner, dt.now(tz.utc)) - - db.session.add(tenant_partner) - db.session.commit() - - except SQLAlchemyError as e: - db.session.rollback() - current_app.logger.error(f"Error associating tenant {tenant_id} with partner: {str(e)}") - raise e - - @staticmethod - def can_user_edit_tenant(tenant_id) -> bool: - if current_user_has_role('Super User'): - return True - elif current_user_has_role('Partner Admin'): - partner_id = session['partner']['id'] - partner_service = next((service for service in session['partner']['services'] - if service.get('type') == 'MANAGEMENT_SERVICE'), None) - if not partner_service: - return False - else: - partner_tenant = PartnerTenant.query.filter( - PartnerTenant.tenant_id == tenant_id, - PartnerTenant.partner_service_id == partner_service['id'], - ).first() - if partner_tenant: - return True - else: - return False - else: - return False - - diff --git a/common/services/tenant_services.py b/common/services/tenant_services.py new file mode 100644 index 0000000..5920d2a --- /dev/null +++ b/common/services/tenant_services.py @@ -0,0 +1,176 @@ +from typing import Dict, List + +from flask import session, current_app +from sqlalchemy.exc import SQLAlchemyError + +from common.extensions import db, cache_manager +from common.models.user import Partner, PartnerTenant, PartnerService, Tenant +from common.utils.eveai_exceptions import EveAINoManagementPartnerService +from common.utils.model_logging_utils import set_logging_information +from datetime import datetime as dt, timezone as tz + +from common.utils.security_utils import current_user_has_role + + +class TenantServices: + @staticmethod + def associate_tenant_with_partner(tenant_id): + """Associate a tenant with a partner""" + try: + partner_id = session['partner']['id'] + # Get partner service (MANAGEMENT_SERVICE type) + partner = Partner.query.get(partner_id) + if not partner: + return + + # Find a management service for this partner + management_service = next((service for service in session['partner']['services'] + if service.get('type') == 'MANAGEMENT_SERVICE'), None) + + if not management_service: + current_app.logger.error(f"No Management Service defined for partner {partner_id}" + f"while associating tenant {tenant_id} with partner.") + raise EveAINoManagementPartnerService() + + # Create the association + tenant_partner = PartnerTenant( + partner_service_id=management_service['id'], + tenant_id=tenant_id, + relationship_type='MANAGED', + ) + set_logging_information(tenant_partner, dt.now(tz.utc)) + + db.session.add(tenant_partner) + db.session.commit() + + except SQLAlchemyError as e: + db.session.rollback() + current_app.logger.error(f"Error associating tenant {tenant_id} with partner: {str(e)}") + raise e + + @staticmethod + def get_available_types_for_tenant(tenant_id: int, config_type: str) -> Dict[str, Dict[str, str]]: + """ + Get available configuration types for a tenant based on partner relationships + + Args: + tenant_id: The tenant ID + config_type: The configuration type ('specialists', 'agents', 'tasks', etc.) + + Returns: + Dictionary of available types for the tenant + """ + # Get the appropriate cache handler based on config_type + cache_handler = None + if config_type == 'specialists': + cache_handler = cache_manager.specialists_types_cache + elif config_type == 'agents': + cache_handler = cache_manager.agents_types_cache + elif config_type == 'tasks': + cache_handler = cache_manager.tasks_types_cache + elif config_type == 'tools': + cache_handler = cache_manager.tools_types_cache + else: + raise ValueError(f"Unsupported config type: {config_type}") + + # Get all types with their metadata (including partner info) + all_types = cache_handler.get_types() + + # Filter to include: + # 1. Types with no partner (global) + # 2. Types with partners that have a SPECIALIST_SERVICE relationship with this tenant + available_partners = TenantServices.get_tenant_partner_names(tenant_id) + + available_types = { + type_id: info for type_id, info in all_types.items() + if info.get('partner') is None or info.get('partner') in available_partners + } + + return available_types + + @staticmethod + def get_tenant_partner_names(tenant_id: int) -> List[str]: + """ + Get names of partners that have a SPECIALIST_SERVICE relationship with this tenant + + Args: + tenant_id: The tenant ID + + Returns: + List of partner names (tenant names) + """ + # Find all PartnerTenant relationships for this tenant + partner_names = [] + try: + # Get all partner services of type SPECIALIST_SERVICE + specialist_services = ( + PartnerService.query + .filter_by(type='SPECIALIST_SERVICE') + .all() + ) + + if not specialist_services: + return [] + + # Find tenant relationships with these services + partner_tenants = ( + PartnerTenant.query + .filter_by(tenant_id=tenant_id) + .filter(PartnerTenant.partner_service_id.in_([svc.id for svc in specialist_services])) + .all() + ) + + # Get the partner names (their tenant names) + for pt in partner_tenants: + partner_service = ( + PartnerService.query + .filter_by(id=pt.partner_service_id) + .first() + ) + + if partner_service: + partner = Partner.query.get(partner_service.partner_id) + if partner: + # Get the tenant associated with this partner + partner_tenant = Tenant.query.get(partner.tenant_id) + if partner_tenant: + partner_names.append(partner_tenant.name) + + except SQLAlchemyError as e: + current_app.logger.error(f"Database error retrieving partner names: {str(e)}") + + return partner_names + + @staticmethod + def can_use_specialist_type(tenant_id: int, specialist_type: str) -> bool: + """ + Check if a tenant can use a specific specialist type + + Args: + tenant_id: The tenant ID + specialist_type: The specialist type ID + + Returns: + True if the tenant can use the specialist type, False otherwise + """ + # Get the specialist type definition + try: + specialist_types = cache_manager.specialists_types_cache.get_types() + specialist_def = specialist_types.get(specialist_type) + + if not specialist_def: + return False + + # If it's a global specialist, anyone can use it + if specialist_def.get('partner') is None: + return True + + # If it's a partner-specific specialist, check if tenant has access + partner_name = specialist_def.get('partner') + available_partners = TenantServices.get_tenant_partner_names(tenant_id) + + return partner_name in available_partners + + except Exception as e: + current_app.logger.error(f"Error checking specialist type access: {str(e)}") + return False \ No newline at end of file diff --git a/common/services/user_service.py b/common/services/user_service.py deleted file mode 100644 index be666ff..0000000 --- a/common/services/user_service.py +++ /dev/null @@ -1,39 +0,0 @@ -from flask import session - -from common.models.user import Partner, Role - -# common/services/user_service.py -from common.utils.eveai_exceptions import EveAIRoleAssignmentException -from common.utils.security_utils import current_user_has_role, all_user_roles - - -class UserService: - @staticmethod - def get_assignable_roles(): - """Retrieves roles that can be assigned to a user depending on the current user logged in, - and the active tenant for the session""" - current_tenant_id = session.get('tenant').get('id', None) - effective_role_names = [] - if current_tenant_id == 1: - if current_user_has_role("Super User"): - effective_role_names.append("Super User") - elif current_tenant_id: - if current_user_has_role("Tenant Admin"): - effective_role_names.append("Tenant Admin") - if current_user_has_role("Partner Admin") or current_user_has_role("Super User"): - effective_role_names.append("Tenant Admin") - if session.get('partner'): - if session.get('partner').get('tenant_id') == current_tenant_id: - effective_role_names.append("Partner Admin") - effective_role_names = list(set(effective_role_names)) - effective_roles = [(role.id, role.name) for role in - Role.query.filter(Role.name.in_(effective_role_names)).all()] - return effective_roles - - @staticmethod - def validate_role_assignments(role_ids): - """Validate a set of role assignments, raising exception for first invalid role""" - assignable_roles = UserService.get_assignable_roles() - assignable_role_ids = {role[0] for role in assignable_roles} - role_id_set = set(role_ids) - return role_id_set.issubset(assignable_role_ids) diff --git a/common/services/user_services.py b/common/services/user_services.py new file mode 100644 index 0000000..7e22ba6 --- /dev/null +++ b/common/services/user_services.py @@ -0,0 +1,95 @@ +from flask import session + +from common.models.user import Partner, Role, PartnerTenant + +from common.utils.eveai_exceptions import EveAIRoleAssignmentException +from common.utils.security_utils import current_user_has_role + + +class UserServices: + @staticmethod + def get_assignable_roles(): + """Retrieves roles that can be assigned to a user depending on the current user logged in, + and the active tenant for the session""" + current_tenant_id = session.get('tenant').get('id', None) + effective_role_names = [] + if current_tenant_id == 1: + if current_user_has_role("Super User"): + effective_role_names.append("Super User") + elif current_tenant_id: + if current_user_has_role("Tenant Admin"): + effective_role_names.append("Tenant Admin") + if current_user_has_role("Partner Admin") or current_user_has_role("Super User"): + effective_role_names.append("Tenant Admin") + if session.get('partner'): + if session.get('partner').get('tenant_id') == current_tenant_id: + effective_role_names.append("Partner Admin") + effective_role_names = list(set(effective_role_names)) + effective_roles = [(role.id, role.name) for role in + Role.query.filter(Role.name.in_(effective_role_names)).all()] + return effective_roles + + @staticmethod + def validate_role_assignments(role_ids): + """Validate a set of role assignments, raising exception for first invalid role""" + assignable_roles = UserServices.get_assignable_roles() + assignable_role_ids = {role[0] for role in assignable_roles} + role_id_set = set(role_ids) + return role_id_set.issubset(assignable_role_ids) + + @staticmethod + def can_user_edit_tenant(tenant_id) -> bool: + if current_user_has_role('Super User'): + return True + elif current_user_has_role('Partner Admin'): + partner = session.get('partner', None) + if partner and partner["tenant_id"] == tenant_id: + return True + partner_service = next((service for service in session['partner']['services'] + if service.get('type') == 'MANAGEMENT_SERVICE'), None) + if not partner_service: + return False + else: + partner_tenant = PartnerTenant.query.filter( + PartnerTenant.tenant_id == tenant_id, + PartnerTenant.partner_service_id == partner_service['id'], + ).first() + if partner_tenant: + return True + else: + return False + else: + return False + + @staticmethod + def can_user_create_tenant() -> bool: + if current_user_has_role('Super User'): + return True + elif current_user_has_role('Partner Admin'): + partner_id = session['partner']['id'] + partner_service = next((service for service in session['partner']['services'] + if service.get('type') == 'MANAGEMENT_SERVICE'), None) + if not partner_service: + return False + else: + partner_permissions = partner_service.get('permissions', None) + return partner_permissions.get('can_create_tenant', False) + else: + return False + + @staticmethod + def can_user_assign_license() -> bool: + if current_user_has_role('Super User'): + return True + elif current_user_has_role('Partner Admin'): + partner_id = session['partner']['id'] + partner_service = next((service for service in session['partner']['services'] + if service.get('type') == 'MANAGEMENT_SERVICE'), None) + if not partner_service: + return False + else: + partner_permissions = partner_service.get('permissions', None) + return partner_permissions.get('can_assign_license', False) + else: + return False + diff --git a/common/utils/cache/config_cache.py b/common/utils/cache/config_cache.py index 2756c1f..e101a89 100644 --- a/common/utils/cache/config_cache.py +++ b/common/utils/cache/config_cache.py @@ -62,13 +62,7 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]): def _load_specific_config(self, type_name: str, version_str: str) -> Dict[str, Any]: """ Load a specific configuration version - - Args: - type_name: Type name - version_str: Version string - - Returns: - Configuration data + Automatically handles global vs partner-specific configs """ version_tree = self.version_tree_cache.get_versions(type_name) versions = version_tree['versions'] @@ -79,11 +73,16 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]): if version_str not in versions: raise ValueError(f"Version {version_str} not found for {type_name}") - file_path = versions[version_str]['file_path'] + version_info = versions[version_str] + file_path = version_info['file_path'] + partner = version_info.get('partner') try: with open(file_path) as f: config = yaml.safe_load(f) + # Add partner information to the config + if partner: + config['partner'] = partner return config except Exception as e: raise ValueError(f"Error loading config from {file_path}: {e}") @@ -133,20 +132,37 @@ class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]): def _load_version_tree(self, type_name: str) -> Dict[str, Any]: """ Load version tree for a specific type without loading full configurations - - Args: - type_name: Name of configuration type - - Returns: - Dict containing available versions and their metadata + Checks both global and partner-specific directories """ - type_path = Path(self._config_dir) / type_name - if not type_path.exists(): + # First check the global path + global_path = Path(self._config_dir) / "global" / type_name + + # If global path doesn't exist, check if the type exists directly in the root + # (for backward compatibility) + if not global_path.exists(): + global_path = Path(self._config_dir) / type_name + + if not global_path.exists(): + # Check if it exists in any partner subdirectories + partner_dirs = [d for d in Path(self._config_dir).iterdir() + if d.is_dir() and d.name != "global"] + + for partner_dir in partner_dirs: + partner_type_path = partner_dir / type_name + if partner_type_path.exists(): + # Found in partner directory + return self._load_versions_from_path(partner_type_path) + + # If we get here, the type wasn't found anywhere raise ValueError(f"No configuration found for type {type_name}") - version_files = list(type_path.glob('*.yaml')) + return self._load_versions_from_path(global_path) + + def _load_versions_from_path(self, path: Path) -> Dict[str, Any]: + """Load all versions from a specific path""" + version_files = list(path.glob('*.yaml')) if not version_files: - raise ValueError(f"No versions found for type {type_name}") + raise ValueError(f"No versions found in {path}") versions = {} latest_version = None @@ -160,9 +176,17 @@ class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]): with open(file_path) as f: yaml_data = yaml.safe_load(f) metadata = yaml_data.get('metadata', {}) + # Add partner information if available + partner = None + if "global" not in str(file_path): + # Extract partner name from path + # Path format: config_dir/partner_name/type_name/version.yaml + partner = file_path.parent.parent.name + versions[ver] = { 'metadata': metadata, - 'file_path': str(file_path) + 'file_path': str(file_path), + 'partner': partner } # Track latest version @@ -316,7 +340,8 @@ class BaseConfigTypesCacheHandler(CacheHandler[Dict[str, Any]]): type_definitions = { type_id: { 'name': info['name'], - 'description': info['description'] + 'description': info['description'], + 'partner': info.get('partner') # Include partner info if available } for type_id, info in self._types_module.items() } diff --git a/common/utils/middleware.py b/common/utils/middleware.py index 5e2b67b..0301e27 100644 --- a/common/utils/middleware.py +++ b/common/utils/middleware.py @@ -8,7 +8,7 @@ from flask import session, current_app, redirect from .database import Database from .eveai_exceptions import EveAINoSessionTenant, EveAINoSessionPartner, EveAINoManagementPartnerService, \ EveAINoManagementPartnerForTenant -from ..services.tenant_service import TenantService +from ..services.user_services import UserServices def mw_before_request(): @@ -36,7 +36,7 @@ def mw_before_request(): if service.get('type') == 'MANAGEMENT_SERVICE'), None) if not management_service: raise EveAINoManagementPartnerService() - if not TenantService.can_user_edit_tenant(tenant_id): + if not UserServices.can_user_edit_tenant(tenant_id): raise EveAINoManagementPartnerForTenant() Database(tenant_id).switch_schema() diff --git a/config/agents/EMAIL_CONTENT_AGENT/1.0.0.yaml b/config/agents/global/EMAIL_CONTENT_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/EMAIL_CONTENT_AGENT/1.0.0.yaml rename to config/agents/global/EMAIL_CONTENT_AGENT/1.0.0.yaml diff --git a/config/agents/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml b/config/agents/global/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml rename to config/agents/global/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml diff --git a/config/agents/IDENTIFICATION_AGENT/1.0.0.yaml b/config/agents/global/IDENTIFICATION_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/IDENTIFICATION_AGENT/1.0.0.yaml rename to config/agents/global/IDENTIFICATION_AGENT/1.0.0.yaml diff --git a/config/agents/RAG_AGENT/1.0.0.yaml b/config/agents/global/RAG_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/RAG_AGENT/1.0.0.yaml rename to config/agents/global/RAG_AGENT/1.0.0.yaml diff --git a/config/agents/RAG_COMMUNICATION_AGENT/1.0.0.yaml b/config/agents/global/RAG_COMMUNICATION_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/RAG_COMMUNICATION_AGENT/1.0.0.yaml rename to config/agents/global/RAG_COMMUNICATION_AGENT/1.0.0.yaml diff --git a/config/agents/SPIN_DETECTION_AGENT/1.0.0.yaml b/config/agents/global/SPIN_DETECTION_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/SPIN_DETECTION_AGENT/1.0.0.yaml rename to config/agents/global/SPIN_DETECTION_AGENT/1.0.0.yaml diff --git a/config/agents/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml b/config/agents/global/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml rename to config/agents/global/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml diff --git a/config/assets/DOCUMENT_TEMPLATE/1.0.0.yaml b/config/assets/global/DOCUMENT_TEMPLATE/1.0.0.yaml similarity index 100% rename from config/assets/DOCUMENT_TEMPLATE/1.0.0.yaml rename to config/assets/global/DOCUMENT_TEMPLATE/1.0.0.yaml diff --git a/config/partner_services/MANAGEMENT_SERVICE/1.0.0.yaml b/config/partner_services/global/MANAGEMENT_SERVICE/1.0.0.yaml similarity index 100% rename from config/partner_services/MANAGEMENT_SERVICE/1.0.0.yaml rename to config/partner_services/global/MANAGEMENT_SERVICE/1.0.0.yaml diff --git a/config/partner_services/global/SPECIALIST_SERVICE/1.0.0.yaml b/config/partner_services/global/SPECIALIST_SERVICE/1.0.0.yaml new file mode 100644 index 0000000..e881527 --- /dev/null +++ b/config/partner_services/global/SPECIALIST_SERVICE/1.0.0.yaml @@ -0,0 +1,9 @@ +version: "1.0.0" +name: "Management Service" +configuration: {} +permissions: {} +metadata: + author: "Josako" + date_added: "2025-04-02" + changes: "Initial version" + description: "Initial definition of the management service" diff --git a/config/prompts/encyclopedia/1.0.0.yaml b/config/prompts/global/encyclopedia/1.0.0.yaml similarity index 100% rename from config/prompts/encyclopedia/1.0.0.yaml rename to config/prompts/global/encyclopedia/1.0.0.yaml diff --git a/config/prompts/history/1.0.0.yaml b/config/prompts/global/history/1.0.0.yaml similarity index 100% rename from config/prompts/history/1.0.0.yaml rename to config/prompts/global/history/1.0.0.yaml diff --git a/config/prompts/html_parse/1.0.0.yaml b/config/prompts/global/html_parse/1.0.0.yaml similarity index 100% rename from config/prompts/html_parse/1.0.0.yaml rename to config/prompts/global/html_parse/1.0.0.yaml diff --git a/config/prompts/openai/gpt-4o/encyclopedia/1.0.0.yaml b/config/prompts/global/openai/gpt-4o/encyclopedia/1.0.0.yaml similarity index 100% rename from config/prompts/openai/gpt-4o/encyclopedia/1.0.0.yaml rename to config/prompts/global/openai/gpt-4o/encyclopedia/1.0.0.yaml diff --git a/config/prompts/openai/gpt-4o/history/1.0.0.yaml b/config/prompts/global/openai/gpt-4o/history/1.0.0.yaml similarity index 100% rename from config/prompts/openai/gpt-4o/history/1.0.0.yaml rename to config/prompts/global/openai/gpt-4o/history/1.0.0.yaml diff --git a/config/prompts/openai/gpt-4o/html_parse/1.0.0.yaml b/config/prompts/global/openai/gpt-4o/html_parse/1.0.0.yaml similarity index 100% rename from config/prompts/openai/gpt-4o/html_parse/1.0.0.yaml rename to config/prompts/global/openai/gpt-4o/html_parse/1.0.0.yaml diff --git a/config/prompts/openai/gpt-4o/pdf_parse/1.0.0.yaml b/config/prompts/global/openai/gpt-4o/pdf_parse/1.0.0.yaml similarity index 100% rename from config/prompts/openai/gpt-4o/pdf_parse/1.0.0.yaml rename to config/prompts/global/openai/gpt-4o/pdf_parse/1.0.0.yaml diff --git a/config/prompts/openai/gpt-4o/rag/1.0.0.yaml b/config/prompts/global/openai/gpt-4o/rag/1.0.0.yaml similarity index 100% rename from config/prompts/openai/gpt-4o/rag/1.0.0.yaml rename to config/prompts/global/openai/gpt-4o/rag/1.0.0.yaml diff --git a/config/prompts/openai/gpt-4o/summary/1.0.0.yaml b/config/prompts/global/openai/gpt-4o/summary/1.0.0.yaml similarity index 100% rename from config/prompts/openai/gpt-4o/summary/1.0.0.yaml rename to config/prompts/global/openai/gpt-4o/summary/1.0.0.yaml diff --git a/config/prompts/openai/gpt-4o/transcript/1.0.0.yaml b/config/prompts/global/openai/gpt-4o/transcript/1.0.0.yaml similarity index 100% rename from config/prompts/openai/gpt-4o/transcript/1.0.0.yaml rename to config/prompts/global/openai/gpt-4o/transcript/1.0.0.yaml diff --git a/config/prompts/pdf_parse/1.0.0.yaml b/config/prompts/global/pdf_parse/1.0.0.yaml similarity index 100% rename from config/prompts/pdf_parse/1.0.0.yaml rename to config/prompts/global/pdf_parse/1.0.0.yaml diff --git a/config/prompts/rag/1.0.0.yaml b/config/prompts/global/rag/1.0.0.yaml similarity index 100% rename from config/prompts/rag/1.0.0.yaml rename to config/prompts/global/rag/1.0.0.yaml diff --git a/config/prompts/summary/1.0.0.yaml b/config/prompts/global/summary/1.0.0.yaml similarity index 100% rename from config/prompts/summary/1.0.0.yaml rename to config/prompts/global/summary/1.0.0.yaml diff --git a/config/prompts/transcript/1.0.0.yaml b/config/prompts/global/transcript/1.0.0.yaml similarity index 100% rename from config/prompts/transcript/1.0.0.yaml rename to config/prompts/global/transcript/1.0.0.yaml diff --git a/config/retrievers/DOSSIER_RETRIEVER/1.0.0.yaml b/config/retrievers/global/DOSSIER_RETRIEVER/1.0.0.yaml similarity index 100% rename from config/retrievers/DOSSIER_RETRIEVER/1.0.0.yaml rename to config/retrievers/global/DOSSIER_RETRIEVER/1.0.0.yaml diff --git a/config/retrievers/STANDARD_RAG/1.0.0.yaml b/config/retrievers/global/STANDARD_RAG/1.0.0.yaml similarity index 100% rename from config/retrievers/STANDARD_RAG/1.0.0.yaml rename to config/retrievers/global/STANDARD_RAG/1.0.0.yaml diff --git a/config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0.yaml b/config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0.yaml new file mode 100644 index 0000000..e362c12 --- /dev/null +++ b/config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0.yaml @@ -0,0 +1,163 @@ +version: "1.0.0" +name: "Traicie Vacature Specialist" +framework: "crewai" +configuration: + ko_criteria: + name: "Knock-out criteria" + type: "text" + description: "The knock-out criteria (1 per line)" + required: true + hard_skills: + name: "Hard Skills" + type: "text" + description: "The hard skills to be checked with the applicant (1 per line)" + required: false + soft_skills: + name: "Soft Skills" + type: "text" + description: "The soft skills required for the job (1 per line)" + required: false + tone_of_voice: + name: "Tone of Voice" + type: "enum" + description: "Tone of voice to be used in communicating with the applicant" + required: false + default: "formal" + allowed_values: [ "formal", "informal", "dynamic" ] + vacancy_text: + name: "Vacancy Text" + type: "text" + description: "The vacancy for this specialist" +arguments: + language: + name: "Language" + type: "str" + description: "Language code to be used for receiving questions and giving answers" + required: true +results: + rag_output: + answer: + name: "answer" + type: "str" + description: "Answer to the query" + required: true + citations: + name: "citations" + type: "List[str]" + description: "List of citations" + required: false + insufficient_info: + name: "insufficient_info" + type: "bool" + description: "Whether or not the query is insufficient info" + required: true + spin: + situation: + name: "situation" + type: "str" + description: "A description of the customer's current situation / context" + required: false + problem: + name: "problem" + type: "str" + description: "The current problems the customer is facing, for which he/she seeks a solution" + required: false + implication: + name: "implication" + type: "str" + description: "A list of implications" + required: false + needs: + name: "needs" + type: "str" + description: "A list of needs" + required: false + additional_info: + name: "additional_info" + type: "str" + description: "Additional information that may be commercially interesting" + required: false + lead_info: + lead_personal_info: + name: + name: "name" + type: "str" + description: "name of the lead" + required: "true" + job_title: + name: "job_title" + type: "str" + description: "job title" + required: false + email: + name: "email" + type: "str" + description: "lead email" + required: "false" + phone: + name: "phone" + type: "str" + description: "lead phone" + required: false + additional_info: + name: "additional_info" + type: "str" + description: "additional info on the lead" + required: false + lead_company_info: + company_name: + name: "company_name" + type: "str" + description: "Name of the lead company" + required: false + industry: + name: "industry" + type: "str" + description: "The industry of the company" + required: false + company_size: + name: "company_size" + type: "int" + description: "The size of the company" + required: false + company_website: + name: "company_website" + type: "str" + description: "The main website for the company" + required: false + additional_info: + name: "additional_info" + type: "str" + description: "Additional information that may be commercially interesting" + required: false +agents: + - type: "RAG_AGENT" + version: "1.0" + - type: "RAG_COMMUNICATION_AGENT" + version: "1.0" + - type: "SPIN_DETECTION_AGENT" + version: "1.0" + - type: "SPIN_SALES_SPECIALIST_AGENT" + version: "1.0" + - type: "IDENTIFICATION_AGENT" + version: "1.0" + - type: "RAG_COMMUNICATION_AGENT" + version: "1.0" +tasks: + - type: "RAG_TASK" + version: "1.0" + - type: "SPIN_DETECT_TASK" + version: "1.0" + - type: "SPIN_QUESTIONS_TASK" + version: "1.0" + - type: "IDENTIFICATION_DETECTION_TASK" + version: "1.0" + - type: "IDENTIFICATION_QUESTIONS_TASK" + version: "1.0" + - type: "RAG_CONSOLIDATION_TASK" + version: "1.0" +metadata: + author: "Josako" + date_added: "2025-01-08" + changes: "Initial version" + description: "A Specialist that performs both Q&A as SPIN (Sales Process) activities" \ No newline at end of file diff --git a/config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0_overview.svg b/config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0_overview.svg new file mode 100644 index 0000000..53f0227 --- /dev/null +++ b/config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0_overview.svg @@ -0,0 +1,76 @@ + + + diff --git a/config/specialists/RAG_SPECIALIST/1.0.0.yaml b/config/specialists/global/RAG_SPECIALIST/1.0.0.yaml similarity index 100% rename from config/specialists/RAG_SPECIALIST/1.0.0.yaml rename to config/specialists/global/RAG_SPECIALIST/1.0.0.yaml diff --git a/config/specialists/SPIN_SPECIALIST/1.0.0.yaml b/config/specialists/global/SPIN_SPECIALIST/1.0.0.yaml similarity index 100% rename from config/specialists/SPIN_SPECIALIST/1.0.0.yaml rename to config/specialists/global/SPIN_SPECIALIST/1.0.0.yaml diff --git a/config/specialists/global/SPIN_SPECIALIST/1.0.0_overview.svg b/config/specialists/global/SPIN_SPECIALIST/1.0.0_overview.svg new file mode 100644 index 0000000..c374ee4 --- /dev/null +++ b/config/specialists/global/SPIN_SPECIALIST/1.0.0_overview.svg @@ -0,0 +1,299 @@ + + + diff --git a/config/specialists/STANDARD_RAG_SPECIALIST/1.0.0.yaml b/config/specialists/global/STANDARD_RAG_SPECIALIST/1.0.0.yaml similarity index 100% rename from config/specialists/STANDARD_RAG_SPECIALIST/1.0.0.yaml rename to config/specialists/global/STANDARD_RAG_SPECIALIST/1.0.0.yaml diff --git a/config/tasks/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml b/config/tasks/global/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml rename to config/tasks/global/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml diff --git a/config/tasks/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml b/config/tasks/global/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml rename to config/tasks/global/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml diff --git a/config/tasks/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml b/config/tasks/global/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml rename to config/tasks/global/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml diff --git a/config/tasks/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml b/config/tasks/global/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml rename to config/tasks/global/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml diff --git a/config/tasks/RAG_CONSOLIDATION_TASK/1.0.0.yaml b/config/tasks/global/RAG_CONSOLIDATION_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/RAG_CONSOLIDATION_TASK/1.0.0.yaml rename to config/tasks/global/RAG_CONSOLIDATION_TASK/1.0.0.yaml diff --git a/config/tasks/RAG_TASK/1.0.0.yaml b/config/tasks/global/RAG_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/RAG_TASK/1.0.0.yaml rename to config/tasks/global/RAG_TASK/1.0.0.yaml diff --git a/config/tasks/SPIN_DETECT_TASK/1.0.0.yaml b/config/tasks/global/SPIN_DETECT_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/SPIN_DETECT_TASK/1.0.0.yaml rename to config/tasks/global/SPIN_DETECT_TASK/1.0.0.yaml diff --git a/config/tasks/SPIN_QUESTIONS_TASK/1.0.0.yaml b/config/tasks/global/SPIN_QUESTIONS_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/SPIN_QUESTIONS_TASK/1.0.0.yaml rename to config/tasks/global/SPIN_QUESTIONS_TASK/1.0.0.yaml diff --git a/config/type_defs/specialist_types.py b/config/type_defs/specialist_types.py index 1970197..4d002f2 100644 --- a/config/type_defs/specialist_types.py +++ b/config/type_defs/specialist_types.py @@ -11,5 +11,10 @@ SPECIALIST_TYPES = { "SPIN_SPECIALIST": { "name": "Spin Sales Specialist", "description": "A specialist that allows to answer user queries, try to get SPIN-information and Identification", + }, + "TRAICIE_VACATURE_SPECIALIST": { + "name": "Traicie Vacature Specialist", + "description": "Specialist configureerbaar voor een specifieke vacature", + "partner": "Traicie" } } \ No newline at end of file diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py index 0a65eac..1770c97 100644 --- a/eveai_app/__init__.py +++ b/eveai_app/__init__.py @@ -141,8 +141,8 @@ def register_blueprints(app): app.register_blueprint(interaction_bp) from .views.entitlements_views import entitlements_bp app.register_blueprint(entitlements_bp) - from .views.administration_views import administration_bp - app.register_blueprint(administration_bp) + from .views.partner_views import partner_bp + app.register_blueprint(partner_bp) from .views.healthz_views import healthz_bp, init_healtz app.register_blueprint(healthz_bp) init_healtz(app) diff --git a/eveai_app/errors.py b/eveai_app/errors.py index e2e77f4..4e8ae53 100644 --- a/eveai_app/errors.py +++ b/eveai_app/errors.py @@ -56,13 +56,13 @@ def attribute_error_handler(error): # Handle the SQLAlchemy relationship error specifically if "'str' object has no attribute '_sa_instance_state'" in error_msg: flash('Database relationship error. Please check your form inputs and try again.', 'error') - return render_template('errors/500.html', + return render_template('error/500.html', error_type="Relationship Error", error_details="A string value was provided where a database object was expected."), 500 # Handle other AttributeErrors flash('An application error occurred. The technical team has been notified.', 'error') - return render_template('errors/500.html', + return render_template('error/500.html', error_type="Attribute Error", error_details=error_msg), 500 @@ -70,7 +70,7 @@ def attribute_error_handler(error): def general_exception(e): current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True) flash('An application error occurred. The technical team has been notified.', 'error') - return render_template('errors/500.html', + return render_template('error/500.html', error_type=type(e).__name__, error_details=str(e)), 500 diff --git a/eveai_app/templates/entitlements/edit_license.html b/eveai_app/templates/entitlements/edit_license.html index 462b023..735b70a 100644 --- a/eveai_app/templates/entitlements/edit_license.html +++ b/eveai_app/templates/entitlements/edit_license.html @@ -11,7 +11,7 @@ {{ form.hidden_tag() }} {% set main_fields = ['start_date', 'end_date', 'currency', 'yearly_payment', 'basic_fee'] %} {% for field in form %} - {{ render_included_field(field, disabled_fields=['currency'], include_fields=main_fields) }} + {{ render_included_field(field, readonly_fields=ext_readonly_fields + ['currency'], include_fields=main_fields) }} {% endfor %}