diff --git a/.gitignore b/.gitignore index 347cb00..c6cbd0d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ __pycache__ **/__pycache__ /.idea *.pyc -*.pyc common/.DS_Store common/__pycache__/__init__.cpython-312.pyc common/__pycache__/extensions.cpython-312.pyc diff --git a/common/.DS_Store b/common/.DS_Store deleted file mode 100644 index 59df3c2..0000000 Binary files a/common/.DS_Store and /dev/null differ diff --git a/common/__pycache__/__init__.cpython-312.pyc b/common/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 0852227..0000000 Binary files a/common/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/common/__pycache__/extensions.cpython-312.pyc b/common/__pycache__/extensions.cpython-312.pyc deleted file mode 100644 index 29e10e5..0000000 Binary files a/common/__pycache__/extensions.cpython-312.pyc and /dev/null differ diff --git a/common/extensions.py b/common/extensions.py index df4fc8d..ee43bfd 100644 --- a/common/extensions.py +++ b/common/extensions.py @@ -2,7 +2,6 @@ from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_bootstrap import Bootstrap from flask_security import Security -from flask_mailman import Mail from flask_login import LoginManager from flask_cors import CORS from flask_jwt_extended import JWTManager @@ -23,7 +22,6 @@ migrate = Migrate() bootstrap = Bootstrap() csrf = CSRFProtect() security = Security() -mail = Mail() login_manager = LoginManager() cors = CORS() jwt = JWTManager() diff --git a/common/langchain/__pycache__/EveAIHistoryRetriever.cpython-312.pyc b/common/langchain/__pycache__/EveAIHistoryRetriever.cpython-312.pyc deleted file mode 100644 index 6eb8d74..0000000 Binary files a/common/langchain/__pycache__/EveAIHistoryRetriever.cpython-312.pyc and /dev/null differ diff --git a/common/langchain/__pycache__/EveAIRetriever.cpython-312.pyc b/common/langchain/__pycache__/EveAIRetriever.cpython-312.pyc deleted file mode 100644 index 974cf86..0000000 Binary files a/common/langchain/__pycache__/EveAIRetriever.cpython-312.pyc and /dev/null differ diff --git a/common/models/.DS_Store b/common/models/.DS_Store deleted file mode 100644 index f3a10b2..0000000 Binary files a/common/models/.DS_Store and /dev/null differ diff --git a/common/models/__pycache__/__init__.cpython-312.pyc b/common/models/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 11892cf..0000000 Binary files a/common/models/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/common/models/__pycache__/document.cpython-312.pyc b/common/models/__pycache__/document.cpython-312.pyc deleted file mode 100644 index 471fdd2..0000000 Binary files a/common/models/__pycache__/document.cpython-312.pyc and /dev/null differ diff --git a/common/models/__pycache__/interaction.cpython-312.pyc b/common/models/__pycache__/interaction.cpython-312.pyc deleted file mode 100644 index 70d8fa6..0000000 Binary files a/common/models/__pycache__/interaction.cpython-312.pyc and /dev/null differ diff --git a/common/models/__pycache__/user.cpython-312.pyc b/common/models/__pycache__/user.cpython-312.pyc deleted file mode 100644 index 0428f31..0000000 Binary files a/common/models/__pycache__/user.cpython-312.pyc and /dev/null differ diff --git a/common/models/entitlements.py b/common/models/entitlements.py index b317725..24d6955 100644 --- a/common/models/entitlements.py +++ b/common/models/entitlements.py @@ -1,4 +1,13 @@ +from sqlalchemy.sql.expression import text + from common.extensions import db +from datetime import datetime as dt, timezone as tz +from enum import Enum +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.ext.hybrid import hybrid_property +from dateutil.relativedelta import relativedelta + +from common.utils.database import Database class BusinessEventLog(db.Model): @@ -41,7 +50,7 @@ class License(db.Model): tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) tier_id = db.Column(db.Integer, db.ForeignKey('public.license_tier.id'),nullable=False) # 'small', 'medium', 'custom' start_date = db.Column(db.Date, nullable=False) - end_date = db.Column(db.Date, nullable=True) + nr_of_periods = db.Column(db.Integer, nullable=False) currency = db.Column(db.String(20), nullable=False) yearly_payment = db.Column(db.Boolean, nullable=False, default=False) basic_fee = db.Column(db.Float, nullable=False) @@ -56,6 +65,9 @@ class License(db.Model): additional_interaction_bucket = db.Column(db.Integer, nullable=False) overage_embedding = db.Column(db.Float, nullable=False, default=0) overage_interaction = db.Column(db.Float, nullable=False, default=0) + additional_storage_allowed = db.Column(db.Boolean, nullable=False, default=False) + additional_embedding_allowed = db.Column(db.Boolean, nullable=False, default=False) + additional_interaction_allowed = db.Column(db.Boolean, nullable=False, default=False) # Versioning Information created_at = db.Column(db.DateTime, nullable=True, server_default=db.func.now()) @@ -65,7 +77,58 @@ class License(db.Model): 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') + periods = db.relationship('LicensePeriod', back_populates='license', + order_by='LicensePeriod.period_number', + cascade='all, delete-orphan') + + @hybrid_property + def end_date(self): + """ + Berekent de einddatum van de licentie op basis van start_date en nr_of_periods. + Elke periode is 1 maand, dus einddatum = startdatum + nr_of_periods maanden - 1 dag + """ + if self.start_date and self.nr_of_periods: + return self.start_date + relativedelta(months=self.nr_of_periods) - relativedelta(days=1) + return None + + @end_date.expression + def end_date(cls): + """ + SQL expressie versie van de end_date property voor gebruik in queries + """ + return db.func.date_add( + db.func.date_add( + cls.start_date, + db.text(f'INTERVAL cls.nr_of_periods MONTH') + ), + db.text('INTERVAL -1 DAY') + ) + + def update_configuration(self, **changes): + """ + Update license configuration + These changes will only apply to future periods, not existing ones + + Args: + **changes: Dictionary of changes to apply to the license + + Returns: + None + """ + allowed_fields = [ + 'tier_id', 'currency', 'basic_fee', 'max_storage_mb', + 'additional_storage_price', 'additional_storage_bucket', + 'included_embedding_mb', 'additional_embedding_price', 'additional_embedding_bucket', + 'included_interaction_tokens', 'additional_interaction_token_price', + 'additional_interaction_bucket', 'overage_embedding', 'overage_interaction', + 'additional_storage_allowed', 'additional_embedding_allowed', + 'additional_interaction_allowed' + ] + + # Apply only allowed changes + for key, value in changes.items(): + if key in allowed_fields: + setattr(self, key, value) class LicenseTier(db.Model): @@ -123,13 +186,198 @@ class PartnerServiceLicenseTier(db.Model): partner_service = db.relationship('PartnerService', back_populates='license_tiers') -class LicenseUsage(db.Model): +class PeriodStatus(Enum): + UPCOMING = "UPCOMING" # The period is still in the future + PENDING = "PENDING" # The period is active, but prepaid is not yet received + ACTIVE = "ACTIVE" # The period is active and prepaid has been received + COMPLETED = "COMPLETED" # The period has been completed, but not yet invoiced + INVOICED = "INVOICED" # The period has been completed and invoiced, but overage payment still pending + CLOSED = "CLOSED" # The period has been closed, invoiced and fully paid + + +class LicensePeriod(db.Model): __bind_key__ = 'public' __table_args__ = {'schema': 'public'} id = db.Column(db.Integer, primary_key=True) license_id = db.Column(db.Integer, db.ForeignKey('public.license.id'), nullable=False) tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) + + # Period identification + period_number = db.Column(db.Integer, nullable=False) + period_start = db.Column(db.Date, nullable=False) + period_end = db.Column(db.Date, nullable=False) + + # License configuration snapshot - copied from license when period is created + currency = db.Column(db.String(20), nullable=False) + basic_fee = db.Column(db.Float, nullable=False) + max_storage_mb = db.Column(db.Integer, nullable=False) + additional_storage_price = db.Column(db.Float, nullable=False) + additional_storage_bucket = db.Column(db.Integer, nullable=False) + included_embedding_mb = db.Column(db.Integer, nullable=False) + additional_embedding_price = db.Column(db.Numeric(10, 4), nullable=False) + additional_embedding_bucket = db.Column(db.Integer, nullable=False) + included_interaction_tokens = db.Column(db.Integer, nullable=False) + additional_interaction_token_price = db.Column(db.Numeric(10, 4), nullable=False) + additional_interaction_bucket = db.Column(db.Integer, nullable=False) + + # Allowance flags - can be changed from False to True within a period + additional_storage_allowed = db.Column(db.Boolean, nullable=False, default=False) + additional_embedding_allowed = db.Column(db.Boolean, nullable=False, default=False) + additional_interaction_allowed = db.Column(db.Boolean, nullable=False, default=False) + + # Status tracking + status = db.Column(db.Enum(PeriodStatus), nullable=False, default=PeriodStatus.UPCOMING) + + # State transition timestamps + upcoming_at = db.Column(db.DateTime, nullable=True) + pending_at = db.Column(db.DateTime, nullable=True) + active_at = db.Column(db.DateTime, nullable=True) + completed_at = db.Column(db.DateTime, nullable=True) + invoiced_at = db.Column(db.DateTime, nullable=True) + closed_at = db.Column(db.DateTime, nullable=True) + + # Standard audit fields + created_at = db.Column(db.DateTime, server_default=db.func.now()) + updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now()) + created_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) + updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) + + # Relationships + license = db.relationship('License', back_populates='periods') + license_usage = db.relationship('LicenseUsage', + uselist=False, # This makes it one-to-one + back_populates='license_period', + cascade='all, delete-orphan') + payments = db.relationship('Payment', back_populates='license_period') + invoices = db.relationship('Invoice', back_populates='license_period', + cascade='all, delete-orphan') + + def update_allowance(self, allowance_type, allow_value, user_id=None): + """ + Update an allowance flag within a period + Only allows transitioning from False to True + + Args: + allowance_type: One of 'storage', 'embedding', or 'interaction' + allow_value: The new value (must be True) + user_id: User ID performing the update + + Raises: + ValueError: If trying to change from True to False, or invalid allowance type + """ + field_name = f"additional_{allowance_type}_allowed" + + # Verify valid field + if not hasattr(self, field_name): + raise ValueError(f"Invalid allowance type: {allowance_type}") + + # Get current value + current_value = getattr(self, field_name) + + # Only allow False -> True transition + if current_value is True and allow_value is True: + # Already True, no change needed + return + elif allow_value is False: + raise ValueError(f"Cannot change {field_name} from {current_value} to False") + + # Update the field + setattr(self, field_name, True) + self.updated_at = dt.now(tz.utc) + if user_id: + self.updated_by = user_id + + @property + def prepaid_invoice(self): + """Get the prepaid invoice for this period""" + return Invoice.query.filter_by( + license_period_id=self.id, + invoice_type=PaymentType.PREPAID + ).first() + + @property + def overage_invoice(self): + """Get the overage invoice for this period""" + return Invoice.query.filter_by( + license_period_id=self.id, + invoice_type=PaymentType.POSTPAID + ).first() + + @property + def prepaid_payment(self): + """Get the prepaid payment for this period""" + return Payment.query.filter_by( + license_period_id=self.id, + payment_type=PaymentType.PREPAID + ).first() + + @property + def overage_payment(self): + """Get the overage payment for this period""" + return Payment.query.filter_by( + license_period_id=self.id, + payment_type=PaymentType.POSTPAID + ).first() + + @property + def all_invoices(self): + """Get all invoices for this period""" + return self.invoices + + @property + def all_payments(self): + """Get all payments for this period""" + return self.payments + + def transition_status(self, new_status: PeriodStatus, user_id: int = None): + """Transition to a new status with proper validation and logging""" + if not self.can_transition_to(new_status): + raise ValueError(f"Invalid status transition from {self.status} to {new_status}") + + self.status = new_status + self.updated_at = dt.now(tz.utc) + if user_id: + self.updated_by = user_id + + # Set appropriate timestamps + if new_status == PeriodStatus.ACTIVE and not self.prepaid_received_at: + self.prepaid_received_at = dt.now(tz.utc) + elif new_status == PeriodStatus.COMPLETED: + self.completed_at = dt.now(tz.utc) + elif new_status == PeriodStatus.INVOICED: + self.invoiced_at = dt.now(tz.utc) + elif new_status == PeriodStatus.CLOSED: + self.closed_at = dt.now(tz.utc) + + @property + def is_overdue(self): + """Check if a prepaid payment is overdue""" + return (self.status == PeriodStatus.PENDING and + self.period_start <= dt.now(tz.utc).date()) + + def can_transition_to(self, new_status: PeriodStatus) -> bool: + """Check if a status transition is valid""" + valid_transitions = { + PeriodStatus.UPCOMING: [PeriodStatus.ACTIVE, PeriodStatus.PENDING], + PeriodStatus.PENDING: [PeriodStatus.ACTIVE], + PeriodStatus.ACTIVE: [PeriodStatus.COMPLETED], + PeriodStatus.COMPLETED: [PeriodStatus.INVOICED, PeriodStatus.CLOSED], + PeriodStatus.INVOICED: [PeriodStatus.CLOSED], + PeriodStatus.CLOSED: [] + } + return new_status in valid_transitions.get(self.status, []) + + def __repr__(self): + return f'' + + +class LicenseUsage(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) storage_mb_used = db.Column(db.Float, default=0) embedding_mb_used = db.Column(db.Float, default=0) embedding_prompt_tokens_used = db.Column(db.Integer, default=0) @@ -138,9 +386,170 @@ class LicenseUsage(db.Model): interaction_prompt_tokens_used = db.Column(db.Integer, default=0) interaction_completion_tokens_used = db.Column(db.Integer, default=0) interaction_total_tokens_used = db.Column(db.Integer, default=0) - period_start_date = db.Column(db.Date, nullable=False) - period_end_date = db.Column(db.Date, nullable=False) + license_period_id = db.Column(db.Integer, db.ForeignKey('public.license_period.id'), nullable=False) + + # Standard audit fields + created_at = db.Column(db.DateTime, server_default=db.func.now()) + updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now()) + created_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) + updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) + + license_period = db.relationship('LicensePeriod', back_populates='license_usage') + + def recalculate_storage(self): + Database(self.tenant_id).switch_schema() + # Perform a SUM operation to get the total file size from document_versions + total_storage = db.session.execute(text(f""" + SELECT SUM(file_size) + FROM document_version + """)).scalar() + + self.storage_mb_used = total_storage + + +class PaymentType(Enum): + PREPAID = "PREPAID" + POSTPAID = "POSTPAID" + + +class PaymentStatus(Enum): + PENDING = "PENDING" + PAID = "PAID" + FAILED = "FAILED" + CANCELLED = "CANCELLED" + + +class Payment(db.Model): + __bind_key__ = 'public' + __table_args__ = {'schema': 'public'} + + id = db.Column(db.Integer, primary_key=True) + license_period_id = db.Column(db.Integer, db.ForeignKey('public.license_period.id'), nullable=True) + tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) + + # Payment details + payment_type = db.Column(db.Enum(PaymentType), nullable=False) + amount = db.Column(db.Numeric(10, 2), nullable=False) + currency = db.Column(db.String(3), nullable=False) + description = db.Column(db.Text, nullable=True) + + # Status tracking + status = db.Column(db.Enum(PaymentStatus), nullable=False, default=PaymentStatus.PENDING) + + # External provider information + external_payment_id = db.Column(db.String(255), nullable=True) + payment_method = db.Column(db.String(50), nullable=True) # credit_card, bank_transfer, etc. + provider_data = db.Column(JSONB, nullable=True) # Provider-specific data + + # Payment information + paid_at = db.Column(db.DateTime, nullable=True) + + # Standard audit fields + created_at = db.Column(db.DateTime, server_default=db.func.now()) + created_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) + updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now()) + updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) + + # Relationships + license_period = db.relationship('LicensePeriod', back_populates='payments') + invoice = db.relationship('Invoice', back_populates='payment', uselist=False) + + @property + def is_overdue(self): + """Check if payment is overdue""" + if self.status != PaymentStatus.PENDING: + return False + + # For prepaid payments, check if period start has passed + if (self.payment_type == PaymentType.PREPAID and + self.license_period_id): + return self.license_period.period_start <= dt.now(tz.utc).date() + + # For postpaid, check against due date (would be on invoice) + return False + + def __repr__(self): + return f'' + + +class InvoiceStatus(Enum): + DRAFT = "DRAFT" + SENT = "SENT" + PAID = "PAID" + OVERDUE = "OVERDUE" + CANCELLED = "CANCELLED" + + +class Invoice(db.Model): + __bind_key__ = 'public' + __table_args__ = {'schema': 'public'} + + id = db.Column(db.Integer, primary_key=True) + license_period_id = db.Column(db.Integer, db.ForeignKey('public.license_period.id'), nullable=False) + payment_id = db.Column(db.Integer, db.ForeignKey('public.payment.id'), nullable=True) + tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) + + # Invoice details + invoice_type = db.Column(db.Enum(PaymentType), nullable=False) + invoice_number = db.Column(db.String(50), unique=True, nullable=False) + invoice_date = db.Column(db.Date, nullable=False) + due_date = db.Column(db.Date, nullable=False) + + # Financial details + amount = db.Column(db.Numeric(10, 2), nullable=False) + currency = db.Column(db.String(3), nullable=False) + tax_amount = db.Column(db.Numeric(10, 2), default=0) + + # Descriptive fields + description = db.Column(db.Text, nullable=True) + status = db.Column(db.Enum(InvoiceStatus), nullable=False, default=InvoiceStatus.DRAFT) + + # Timestamps + sent_at = db.Column(db.DateTime, nullable=True) + paid_at = db.Column(db.DateTime, nullable=True) + + # Standard audit fields + created_at = db.Column(db.DateTime, server_default=db.func.now()) + created_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) + updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now()) + updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) + + # Relationships + license_period = db.relationship('LicensePeriod', back_populates='invoices') + payment = db.relationship('Payment', back_populates='invoice') + + def __repr__(self): + return f'' + + +class LicenseChangeLog(db.Model): + """ + Log of changes to license configurations + Used for auditing and tracking when/why license details changed + """ + __bind_key__ = 'public' + __table_args__ = {'schema': 'public'} + + id = db.Column(db.Integer, primary_key=True) + license_id = db.Column(db.Integer, db.ForeignKey('public.license.id'), nullable=False) + changed_at = db.Column(db.DateTime, nullable=False, default=lambda: dt.now(tz.utc)) + + # What changed + field_name = db.Column(db.String(100), nullable=False) + old_value = db.Column(db.String(255), nullable=True) + new_value = db.Column(db.String(255), nullable=False) + + # Why it changed + reason = db.Column(db.Text, nullable=True) + + # Standard audit fields + created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True) + + # Relationships + license = db.relationship('License', backref=db.backref('change_logs', order_by='LicenseChangeLog.changed_at')) + + def __repr__(self): + return f' {self.new_value}>' - license = db.relationship('License', back_populates='usages') diff --git a/common/services/entitlements/__init__.py b/common/services/entitlements/__init__.py new file mode 100644 index 0000000..80cd62c --- /dev/null +++ b/common/services/entitlements/__init__.py @@ -0,0 +1,9 @@ +from common.services.entitlements.license_period_services import LicensePeriodServices +from common.services.entitlements.license_usage_services import LicenseUsageServices +from common.services.entitlements.license_tier_services import LicenseTierServices + +__all__ = [ + 'LicensePeriodServices', + 'LicenseUsageServices', + 'LicenseTierServices' +] diff --git a/common/services/entitlements/license_period_services.py b/common/services/entitlements/license_period_services.py new file mode 100644 index 0000000..ef5dc0c --- /dev/null +++ b/common/services/entitlements/license_period_services.py @@ -0,0 +1,223 @@ +from dateutil.relativedelta import relativedelta +from datetime import datetime as dt, timezone as tz, timedelta +from flask import current_app + +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.sql.expression import and_ + +from common.extensions import db +from common.models.entitlements import LicensePeriod, License, PeriodStatus, LicenseUsage +from common.utils.eveai_exceptions import EveAILicensePeriodsExceeded, EveAIPendingLicensePeriod, EveAINoActiveLicense +from common.utils.model_logging_utils import set_logging_information, update_logging_information + + +class LicensePeriodServices: + @staticmethod + def find_current_license_period_for_usage(tenant_id: int) -> LicensePeriod: + """ + Find the current license period for a tenant. It ensures the status of the different license periods are adapted + when required, and a LicenseUsage object is created if required. + + Args: + tenant_id: The ID of the tenant to find the license period for + + Raises: + EveAIException: and derived classes + """ + current_date = dt.now(tz.utc).date() + license_period = (db.session.query(LicensePeriod) + .filter_by(tenant_id=tenant_id) + .filter(and_(LicensePeriod.period_start_date <= current_date, + LicensePeriod.period_end_date >= current_date)) + .first()) + if not license_period: + license_period = LicensePeriodServices._create_next_license_period_for_usage(tenant_id) + if license_period: + match license_period.status: + case PeriodStatus.UPCOMING: + LicensePeriodServices._complete_last_license_period() + LicensePeriodServices._activate_license_period(license_period) + if not license_period.license_usage: + new_license_usage = LicenseUsage() + new_license_usage.license_period = license_period + try: + db.session.add(new_license_usage) + db.session.commit() + except SQLAlchemyError as e: + db.session.rollback() + current_app.logger.error( + f"Error creating new license usage for license period {license_period.id}: {str(e)}") + raise e + if license_period.status == PeriodStatus.ACTIVE: + return license_period + else: + # Status is PENDING, so no prepaid payment received. There is no license period we can use. + # We allow for a delay of 5 days before raising an exception. + current_date = dt.now(tz.utc).date() + delta = abs(current_date - license_period.period_start_date) + if delta > timedelta(days=current_app.config.get('ENTITLEMENTS_MAX_PENDING_DAYS', 5)): + raise EveAIPendingLicensePeriod() + case PeriodStatus.ACTIVE: + return license_period + case PeriodStatus.PENDING: + return license_period + else: + raise EveAILicensePeriodsExceeded(license_id=None) + + @staticmethod + def _create_next_license_period_for_usage(tenant_id) -> LicensePeriod: + """ + Create a new period for this license using the current license configuration + + Args: + tenant_id: The ID of the tenant to create the period for + + Returns: + LicensePeriod: The newly created license period + """ + current_date = dt.now(tz.utc).date() + + # Zoek de actieve licentie voor deze tenant op de huidige datum + the_license = (db.session.query(License) + .filter_by(tenant_id=tenant_id) + .filter(License.start_date <= current_date) + .filter(License.end_date >= current_date) + .first()) + + if not the_license: + current_app.logger.error(f"No active license found for tenant {tenant_id} on date {current_date}") + raise EveAINoActiveLicense(tenant_id=tenant_id) + + next_period_number = 1 + if the_license.periods: + # If there are existing periods, get the next sequential number + next_period_number = max(p.period_number for p in the_license.periods) + 1 + + if next_period_number > the_license.max_periods: + raise EveAILicensePeriodsExceeded(license_id=the_license.id) + + new_license_period = LicensePeriod( + license_id=the_license.id, + tenant_id=tenant_id, + period_number=next_period_number, + period_start=the_license.start_date + relativedelta(months=next_period_number-1), + period_end=the_license.end_date + relativedelta(months=next_period_number, days=-1), + status=PeriodStatus.UPCOMING, + ) + set_logging_information(new_license_period, dt.now(tz.utc)) + new_license_usage = LicenseUsage( + license_period=new_license_period, + tenant_id=tenant_id, + ) + set_logging_information(new_license_usage, dt.now(tz.utc)) + + try: + db.session.add(new_license_period) + db.session.add(new_license_usage) + db.session.commit() + return new_license_period + except SQLAlchemyError as e: + db.session.rollback() + current_app.logger.error(f"Error creating next license period for tenant {tenant_id}: {str(e)}") + raise e + + @staticmethod + def _activate_license_period(license_period_id: int = None, license_period: LicensePeriod = None) -> LicensePeriod: + """ + Activate a license period + + Args: + license_period_id: The ID of the license period to activate (optional if license_period is provided) + license_period: The LicensePeriod object to activate (optional if license_period_id is provided) + + Returns: + LicensePeriod: The activated license period object + + Raises: + ValueError: If neither license_period_id nor license_period is provided + """ + if license_period is None and license_period_id is None: + raise ValueError("Either license_period_id or license_period must be provided") + + # Get a license period object if only ID was provided + if license_period is None: + license_period = LicensePeriod.query.get_or_404(license_period_id) + + if license_period.upcoming_at is not None: + license_period.pending_at.upcoming_at = dt.now(tz.utc) + license_period.status = PeriodStatus.PENDING + if license_period.prepaid_payment: + # There is a payment received for the given period + license_period.active_at = dt.now(tz.utc) + license_period.status = PeriodStatus.ACTIVE + + # Copy snapshot fields from the license to the period + the_license = License.query.get_or_404(license_period.license_id) + license_period.currency = the_license.currency + license_period.basic_fee = the_license.basic_fee + license_period.max_storage_mb = the_license.max_storage_mb + license_period.additional_storage_price = the_license.additional_storage_price + license_period.additional_storage_bucket = the_license.additional_storage_bucket + license_period.included_embedding_mb = the_license.included_embedding_mb + license_period.additional_embedding_price = the_license.additional_embedding_price + license_period.additional_embedding_bucket = the_license.additional_embedding_bucket + license_period.included_interaction_tokens = the_license.included_interaction_tokens + license_period.additional_interaction_token_price = the_license.additional_interaction_token_price + license_period.additional_interaction_bucket = the_license.additional_interaction_bucket + license_period.additional_storage_allowed = the_license.additional_storage_allowed + license_period.additional_embedding_allowed = the_license.additional_embedding_allowed + license_period.additional_interaction_allowed = the_license.additional_interaction_allowed + + update_logging_information(license_period, dt.now(tz.utc)) + + if not license_period.license_usage: + license_period.license_usage = LicenseUsage( + tenant_id=license_period.tenant_id, + license_period=license_period, + ) + + license_period.license_usage.recalculate_storage() + + try: + db.session.add(license_period) + db.session.add(license_period.license_usage) + db.session.commit() + except SQLAlchemyError as e: + db.session.rollback() + current_app.logger.error(f"Error activating license period {license_period_id}: {str(e)}") + raise e + + return license_period + + @staticmethod + def _complete_last_license_period(tenant_id) -> None: + """ + Complete the active or pending license period for a tenant. This is done by setting the status to COMPLETED. + + Args: + tenant_id: De ID van de tenant + """ + # Zoek de licenseperiode voor deze tenant met status ACTIVE of PENDING + active_period = ( + db.session.query(LicensePeriod) + .filter_by(tenant_id=tenant_id) + .filter(LicensePeriod.status.in_([PeriodStatus.ACTIVE, PeriodStatus.PENDING])) + .first() + ) + + # Als er geen actieve periode gevonden is, hoeven we niets te doen + if not active_period: + return + + # Zet de gevonden periode op COMPLETED + active_period.status = PeriodStatus.COMPLETED + active_period.completed_at = dt.now(tz.utc) + update_logging_information(active_period, dt.now(tz.utc)) + + try: + db.session.add(active_period) + db.session.commit() + except SQLAlchemyError as e: + db.session.rollback() + current_app.logger.error(f"Error completing period {active_period.id} for {tenant_id}: {str(e)}") + raise e diff --git a/common/services/entitlement_services.py b/common/services/entitlements/license_tier_services.py similarity index 94% rename from common/services/entitlement_services.py rename to common/services/entitlements/license_tier_services.py index 88312f3..d83ca3f 100644 --- a/common/services/entitlement_services.py +++ b/common/services/entitlements/license_tier_services.py @@ -1,17 +1,16 @@ -from flask import session, current_app, flash +from flask import session, flash, current_app +from datetime import datetime as dt, timezone as tz + 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.models.user import Partner 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: +class LicenseTierServices: @staticmethod def associate_license_tier_with_partner(license_tier_id): """Associate a license tier with a partner""" diff --git a/common/services/entitlements/license_usage_services.py b/common/services/entitlements/license_usage_services.py new file mode 100644 index 0000000..2859c83 --- /dev/null +++ b/common/services/entitlements/license_usage_services.py @@ -0,0 +1,143 @@ +from dateutil.relativedelta import relativedelta +from flask import session, current_app, flash +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.sql.expression import text + +from common.extensions import db, cache_manager +from common.models.entitlements import PartnerServiceLicenseTier, License, LicenseUsage, LicensePeriod, PeriodStatus +from common.models.user import Partner, PartnerTenant +from common.services.entitlements import LicensePeriodServices +from common.utils.database import Database +from common.utils.eveai_exceptions import EveAINoManagementPartnerService, EveAINoActiveLicense, \ + EveAIStorageQuotaExceeded, EveAIEmbeddingQuotaExceeded, EveAIInteractionQuotaExceeded, EveAILicensePeriodsExceeded, \ + EveAIException +from common.utils.model_logging_utils import set_logging_information, update_logging_information +from datetime import datetime as dt, timezone as tz + +from common.utils.security_utils import current_user_has_role + + +class LicenseUsageServices: + @staticmethod + def check_storage_and_embedding_quota(tenant_id: int, file_size_mb: float) -> None: + """ + Check if a tenant can add a new document without exceeding storage and embedding quotas + + Args: + tenant_id: ID of the tenant + file_size_mb: Size of the file in MB + + Raises: + EveAIStorageQuotaExceeded: If storage quota would be exceeded + EveAIEmbeddingQuotaExceeded: If embedding quota would be exceeded + EveAINoActiveLicense: If no active license is found + EveAIException: For other errors + """ + # Get active license period + license_period = LicensePeriodServices.find_current_license_period_for_usage(tenant_id) + # Early return if both overruns are allowed - no need to check usage at all + if license_period.additional_storage_allowed and license_period.additional_embedding_allowed: + return + + # Check storage quota only if overruns are not allowed + if not license_period.additional_storage_allowed: + LicenseUsageServices._validate_storage_quota(license_period, file_size_mb) + + # Check embedding quota only if overruns are not allowed + if not license_period.additional_embedding_allowed: + LicenseUsageServices._validate_embedding_quota(license_period, file_size_mb) + + @staticmethod + def check_embedding_quota(tenant_id: int, file_size_mb: float) -> None: + """ + Check if a tenant can re-embed a document without exceeding embedding quota + + Args: + tenant_id: ID of the tenant + file_size_mb: Size of the file in MB + + Raises: + EveAIEmbeddingQuotaExceeded: If embedding quota would be exceeded + EveAINoActiveLicense: If no active license is found + EveAIException: For other errors + """ + # Get active license period + license_period = LicensePeriodServices.find_current_license_period_for_usage(tenant_id) + # Early return if both overruns are allowed - no need to check usage at all + if license_period.additional_embedding_allowed: + return + + # Check embedding quota + LicenseUsageServices._validate_embedding_quota(license_period, file_size_mb) + + @staticmethod + def check_interaction_quota(tenant_id: int) -> None: + """ + Check if a tenant can execute a specialist without exceeding interaction quota. As it is impossible to estimate + the number of interaction tokens, we only check if the interaction quota are exceeded. So we might have a + limited overrun. + + Args: + tenant_id: ID of the tenant + + Raises: + EveAIInteractionQuotaExceeded: If interaction quota would be exceeded + EveAINoActiveLicense: If no active license is found + EveAIException: For other errors + """ + # Get active license period + license_period = LicensePeriodServices.find_current_license_period_for_usage(tenant_id) + # Early return if both overruns are allowed - no need to check usage at all + if license_period.additional_interaction_allowed: + return + + # Convert tokens to M tokens and check interaction quota + LicenseUsageServices._validate_interaction_quota(license_period) + + @staticmethod + def _validate_storage_quota(license_period: LicensePeriod, additional_mb: float) -> None: + """Check storage quota and raise exception if exceeded""" + current_storage = license_period.license_usage.storage_mb_used or 0 + projected_storage = current_storage + additional_mb + max_storage = license_period.max_storage_mb + + # Hard limit check (we only get here if overruns are NOT allowed) + if projected_storage > max_storage: + raise EveAIStorageQuotaExceeded( + current_usage=current_storage, + limit=max_storage, + additional=additional_mb + ) + + @staticmethod + def _validate_embedding_quota(license_period: LicensePeriod, additional_mb: float) -> None: + """Check embedding quota and raise exception if exceeded""" + current_embedding = license_period.license_usage.embedding_mb_used or 0 + projected_embedding = current_embedding + additional_mb + max_embedding = license_period.included_embedding_mb + + # Hard limit check (we only get here if overruns are NOT allowed) + if projected_embedding > max_embedding: + raise EveAIEmbeddingQuotaExceeded( + current_usage=current_embedding, + limit=max_embedding, + additional=additional_mb + ) + + @staticmethod + def _validate_interaction_quota(license_period) -> None: + """Check interaction quota and raise exception if exceeded (tokens in millions). We might have an overrun!""" + current_tokens = license_period.license_usage.interaction_total_tokens_used / 1_000_000 or 0 + max_tokens = license_period.included_interaction_tokens + + # Hard limit check (we only get here if overruns are NOT allowed) + if current_tokens > max_tokens: + raise EveAIInteractionQuotaExceeded( + current_usage=current_tokens, + limit=max_tokens + ) + + + + + diff --git a/common/services/user/__init__.py b/common/services/user/__init__.py new file mode 100644 index 0000000..adac945 --- /dev/null +++ b/common/services/user/__init__.py @@ -0,0 +1,5 @@ +from common.services.user.user_services import UserServices +from common.services.user.partner_services import PartnerServices +from common.services.user.tenant_services import TenantServices + +__all__ = ['UserServices', 'PartnerServices', 'TenantServices'] \ No newline at end of file diff --git a/common/services/partner_services.py b/common/services/user/partner_services.py similarity index 100% rename from common/services/partner_services.py rename to common/services/user/partner_services.py diff --git a/common/services/tenant_services.py b/common/services/user/tenant_services.py similarity index 100% rename from common/services/tenant_services.py rename to common/services/user/tenant_services.py diff --git a/common/services/user_services.py b/common/services/user/user_services.py similarity index 100% rename from common/services/user_services.py rename to common/services/user/user_services.py diff --git a/common/utils/.DS_Store b/common/utils/.DS_Store deleted file mode 100644 index 86dbb5e..0000000 Binary files a/common/utils/.DS_Store and /dev/null differ diff --git a/common/utils/__pycache__/__init__.cpython-312.pyc b/common/utils/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index db92efb..0000000 Binary files a/common/utils/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/celery_utils.cpython-312.pyc b/common/utils/__pycache__/celery_utils.cpython-312.pyc deleted file mode 100644 index 0ec5442..0000000 Binary files a/common/utils/__pycache__/celery_utils.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/cors_utils.cpython-312.pyc b/common/utils/__pycache__/cors_utils.cpython-312.pyc deleted file mode 100644 index ef730b6..0000000 Binary files a/common/utils/__pycache__/cors_utils.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/database.cpython-312.pyc b/common/utils/__pycache__/database.cpython-312.pyc deleted file mode 100644 index 93870c4..0000000 Binary files a/common/utils/__pycache__/database.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/datetime_utils.cpython-312.pyc b/common/utils/__pycache__/datetime_utils.cpython-312.pyc deleted file mode 100644 index d792341..0000000 Binary files a/common/utils/__pycache__/datetime_utils.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/debug_utils.cpython-312.pyc b/common/utils/__pycache__/debug_utils.cpython-312.pyc deleted file mode 100644 index 85c5fe7..0000000 Binary files a/common/utils/__pycache__/debug_utils.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/key_encryption.cpython-312.pyc b/common/utils/__pycache__/key_encryption.cpython-312.pyc deleted file mode 100644 index a81d7e7..0000000 Binary files a/common/utils/__pycache__/key_encryption.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/middleware.cpython-312.pyc b/common/utils/__pycache__/middleware.cpython-312.pyc deleted file mode 100644 index 815e2c1..0000000 Binary files a/common/utils/__pycache__/middleware.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/model_utils.cpython-312.pyc b/common/utils/__pycache__/model_utils.cpython-312.pyc deleted file mode 100644 index 15d6528..0000000 Binary files a/common/utils/__pycache__/model_utils.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/nginx_utils.cpython-312.pyc b/common/utils/__pycache__/nginx_utils.cpython-312.pyc deleted file mode 100644 index b5d239f..0000000 Binary files a/common/utils/__pycache__/nginx_utils.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/security.cpython-312.pyc b/common/utils/__pycache__/security.cpython-312.pyc deleted file mode 100644 index a95c177..0000000 Binary files a/common/utils/__pycache__/security.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/security_utils.cpython-312.pyc b/common/utils/__pycache__/security_utils.cpython-312.pyc deleted file mode 100644 index 842a2e9..0000000 Binary files a/common/utils/__pycache__/security_utils.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/simple_encryption.cpython-312.pyc b/common/utils/__pycache__/simple_encryption.cpython-312.pyc deleted file mode 100644 index bca19c9..0000000 Binary files a/common/utils/__pycache__/simple_encryption.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/template_filters.cpython-312.pyc b/common/utils/__pycache__/template_filters.cpython-312.pyc deleted file mode 100644 index 6aeaba8..0000000 Binary files a/common/utils/__pycache__/template_filters.cpython-312.pyc and /dev/null differ diff --git a/common/utils/__pycache__/view_assistants.cpython-312.pyc b/common/utils/__pycache__/view_assistants.cpython-312.pyc deleted file mode 100644 index 6b1ae74..0000000 Binary files a/common/utils/__pycache__/view_assistants.cpython-312.pyc and /dev/null differ diff --git a/common/utils/asset_utils.py b/common/utils/asset_utils.py index 5eb3815..9b600bd 100644 --- a/common/utils/asset_utils.py +++ b/common/utils/asset_utils.py @@ -5,7 +5,6 @@ from sqlalchemy.exc import SQLAlchemyError from common.extensions import cache_manager, minio_client, db from common.models.interaction import EveAIAsset, EveAIAssetVersion -from common.utils.document_utils import mark_tenant_storage_dirty from common.utils.model_logging_utils import set_logging_information @@ -55,7 +54,8 @@ def create_version_for_asset(asset, tenant_id): def add_asset_version_file(asset_version, field_name, file, tenant_id): object_name, file_size = minio_client.upload_file(asset_version.bucket_name, asset_version.id, field_name, file.content_type) - mark_tenant_storage_dirty(tenant_id) + # mark_tenant_storage_dirty(tenant_id) + # TODO - zorg ervoor dat de herberekening van storage onmiddellijk gebeurt! return object_name diff --git a/common/utils/cache/license_cache.py b/common/utils/cache/license_cache.py new file mode 100644 index 0000000..9c60db1 --- /dev/null +++ b/common/utils/cache/license_cache.py @@ -0,0 +1,102 @@ +# common/utils/cache/license_cache.py +from typing import Dict, Any, Optional +from datetime import datetime as dt, timezone as tz + +from flask import current_app +from sqlalchemy import and_ +from sqlalchemy.inspection import inspect + +from common.utils.cache.base import CacheHandler +from common.models.entitlements import License + + +class LicenseCacheHandler(CacheHandler[License]): + """Handles caching of active licenses for tenants""" + handler_name = 'license_cache' + + def __init__(self, region): + super().__init__(region, 'active_license') + self.configure_keys('tenant_id') + + def _to_cache_data(self, instance: License) -> Dict[str, Any]: + """Convert License instance to cache data using SQLAlchemy inspection""" + if not instance: + return {} + + # Get all column attributes from the SQLAlchemy model + mapper = inspect(License) + data = {} + + for column in mapper.columns: + value = getattr(instance, column.name) + + # Handle date serialization + if isinstance(value, dt): + data[column.name] = value.isoformat() + else: + data[column.name] = value + + return data + + def _from_cache_data(self, data: Dict[str, Any], **kwargs) -> License: + """Create License instance from cache data using SQLAlchemy inspection""" + if not data: + return None + + # Create a new License instance + license = License() + mapper = inspect(License) + + # Set all attributes dynamically + for column in mapper.columns: + if column.name in data: + value = data[column.name] + + # Handle date deserialization + if column.name.endswith('_date') and value: + if isinstance(value, str): + value = dt.fromisoformat(value).date() + + setattr(license, column.name, value) + + return license + + def _should_cache(self, value: License) -> bool: + """Validate if the license should be cached""" + return value is not None and value.id is not None + + def get_active_license(self, tenant_id: int) -> Optional[License]: + """ + Get the currently active license for a tenant + + Args: + tenant_id: ID of the tenant + + Returns: + License instance if found, None otherwise + """ + + def creator_func(tenant_id: int) -> Optional[License]: + from common.extensions import db + current_date = dt.now(tz=tz.utc).date() + + # TODO --> Active License via active Period? + + return (db.session.query(License) + .filter_by(tenant_id=tenant_id) + .filter(License.start_date <= current_date) + .last()) + + return self.get(creator_func, tenant_id=tenant_id) + + def invalidate_tenant_license(self, tenant_id: int): + """Invalidate cached license for specific tenant""" + self.invalidate(tenant_id=tenant_id) + + +def register_license_cache_handlers(cache_manager) -> None: + """Register license cache handlers with cache manager""" + cache_manager.register_handler( + LicenseCacheHandler, + 'eveai_model' # Use existing eveai_model region + ) diff --git a/common/utils/document_utils.py b/common/utils/document_utils.py index edf1551..7668bfd 100644 --- a/common/utils/document_utils.py +++ b/common/utils/document_utils.py @@ -16,9 +16,15 @@ from .eveai_exceptions import (EveAIInvalidLanguageException, EveAIDoubleURLExce EveAIInvalidCatalog, EveAIInvalidDocument, EveAIInvalidDocumentVersion, EveAIException) from ..models.user import Tenant from common.utils.model_logging_utils import set_logging_information, update_logging_information +from common.services.entitlements import LicenseUsageServices + +MB_CONVERTOR = 1_048_576 def create_document_stack(api_input, file, filename, extension, tenant_id): + # Precheck if we can add a document to the stack + LicenseUsageServices.check_storage_and_embedding_quota(tenant_id, len(file)/MB_CONVERTOR) + # Create the Document catalog_id = int(api_input.get('catalog_id')) catalog = Catalog.query.get(catalog_id) @@ -102,8 +108,6 @@ def create_version_for_document(document, tenant_id, url, sub_file_type, langua set_logging_information(new_doc_vers, dt.now(tz.utc)) - mark_tenant_storage_dirty(tenant_id) - return new_doc_vers @@ -124,7 +128,7 @@ def upload_file_for_version(doc_vers, file, extension, tenant_id): ) doc_vers.bucket_name = bn doc_vers.object_name = on - doc_vers.file_size = size / 1048576 # Convert bytes to MB + doc_vers.file_size = size / MB_CONVERTOR # Convert bytes to MB db.session.commit() current_app.logger.info(f'Successfully saved document to MinIO for tenant {tenant_id} for ' @@ -274,6 +278,9 @@ def refresh_document_with_info(doc_id, tenant_id, api_input): if not old_doc_vers.url: return None, "This document has no URL. Only documents with a URL can be refreshed." + # Precheck if we have enough quota for the new version + LicenseUsageServices.check_storage_and_embedding_quota(tenant_id, old_doc_vers.file_size) + new_doc_vers = create_version_for_document( doc, tenant_id, old_doc_vers.url, @@ -330,6 +337,9 @@ def refresh_document_with_content(doc_id: int, tenant_id: int, file_content: byt old_doc_vers = DocumentVersion.query.filter_by(doc_id=doc_id).order_by(desc(DocumentVersion.id)).first() + # Precheck if we have enough quota for the new version + LicenseUsageServices.check_storage_and_embedding_quota(tenant_id, len(file_content) / MB_CONVERTOR) + # Create new version with same file type as original extension = old_doc_vers.file_type @@ -377,13 +387,6 @@ def refresh_document(doc_id, tenant_id): return refresh_document_with_info(doc_id, tenant_id, api_input) -# Function triggered when a document_version is created or updated -def mark_tenant_storage_dirty(tenant_id): - tenant = db.session.query(Tenant).filter_by(id=int(tenant_id)).first() - tenant.storage_dirty = True - db.session.commit() - - def cope_with_local_url(url): parsed_url = urlparse(url) # Check if this is an internal WordPress URL (TESTING) and rewrite it diff --git a/common/utils/dynamic_field_utils.py b/common/utils/dynamic_field_utils.py new file mode 100644 index 0000000..fd10d70 --- /dev/null +++ b/common/utils/dynamic_field_utils.py @@ -0,0 +1,44 @@ +def create_default_config_from_type_config(type_config): + """ + Creëert een dictionary met standaardwaarden gebaseerd op een typedefinitie configuratie. + + Args: + type_config (dict): Het configuration-veld van een typedefinitie (bijv. uit processor_types). + + Returns: + dict: Een dictionary met de naam van ieder veld als sleutel en de standaardwaarde als waarde. + Alleen velden met een standaardwaarde of die verplicht zijn, worden opgenomen. + + Voorbeeld: + >>> config = PROCESSOR_TYPES["HTML_PROCESSOR"]["configuration"] + >>> create_default_config_from_type_def(config) + {'html_tags': 'p, h1, h2, h3, h4, h5, h6, li, table, thead, tbody, tr, td', + 'html_end_tags': 'p, li, table', + 'html_excluded_classes': '', + 'html_excluded_elements': 'header, footer, nav, script', + 'html_included_elements': 'article, main', + 'chunking_heading_level': 2} + """ + if not type_config: + return {} + + default_config = {} + + for field_name, field_def in type_config.items(): + # Als het veld een standaardwaarde heeft, voeg deze toe + if "default" in field_def: + default_config[field_name] = field_def["default"] + # Als het veld verplicht is maar geen standaardwaarde heeft, voeg een lege string toe + elif field_def.get("required", False): + # Kies een geschikte "lege" waarde op basis van het type + field_type = field_def.get("type", "string") + if field_type == "string": + default_config[field_name] = "" + elif field_type == "integer": + default_config[field_name] = 0 + elif field_type == "boolean": + default_config[field_name] = False + else: + default_config[field_name] = "" + + return default_config diff --git a/common/utils/eveai_exceptions.py b/common/utils/eveai_exceptions.py index 9622661..0c912c3 100644 --- a/common/utils/eveai_exceptions.py +++ b/common/utils/eveai_exceptions.py @@ -186,3 +186,65 @@ class EveAINoManagementPartnerForTenant(EveAIException): super().__init__(message, status_code, payload) +class EveAIQuotaExceeded(EveAIException): + """Base exception for quota-related errors""" + + def __init__(self, message, quota_type, current_usage, limit, additional=0, status_code=400, payload=None): + super().__init__(message, status_code, payload) + self.quota_type = quota_type + self.current_usage = current_usage + self.limit = limit + self.additional = additional + + +class EveAIStorageQuotaExceeded(EveAIQuotaExceeded): + """Raised when storage quota is exceeded""" + + def __init__(self, current_usage, limit, additional, status_code=400, payload=None): + message = (f"Storage quota exceeded. Current: {current_usage:.1f}MB, " + f"Additional: {additional:.1f}MB, Limit: {limit}MB") + super().__init__(message, "storage", current_usage, limit, additional, status_code, payload) + + +class EveAIEmbeddingQuotaExceeded(EveAIQuotaExceeded): + """Raised when embedding quota is exceeded""" + + def __init__(self, current_usage, limit, additional, status_code=400, payload=None): + message = (f"Embedding quota exceeded. Current: {current_usage:.1f}MB, " + f"Additional: {additional:.1f}MB, Limit: {limit}MB") + super().__init__(message, "embedding", current_usage, limit, additional, status_code, payload) + + +class EveAIInteractionQuotaExceeded(EveAIQuotaExceeded): + """Raised when the interaction token quota is exceeded""" + + def __init__(self, current_usage, limit, status_code=400, payload=None): + message = (f"Interaction token quota exceeded. Current: {current_usage:.2f}M tokens, " + f"Limit: {limit:.2f}M tokens") + super().__init__(message, "interaction", current_usage, limit, 0, status_code, payload) + + +class EveAIQuotaWarning(EveAIException): + """Warning when approaching quota limits (not blocking)""" + + def __init__(self, message, quota_type, usage_percentage, status_code=200, payload=None): + super().__init__(message, status_code, payload) + self.quota_type = quota_type + self.usage_percentage = usage_percentage + + +class EveAILicensePeriodsExceeded(EveAIException): + """Raised when no more license periods can be created for a given license""" + + def __init__(self, license_id, status_code=400, payload=None): + message = f"No more license periods can be created for license with ID {license_id}. " + super().__init__(message, status_code, payload) + + +class EveAIPendingLicensePeriod(EveAIException): + """Raised when a license period is pending""" + + def __init__(self, status_code=400, payload=None): + message = f"Basic Fee Payment has not been received yet. Please ensure payment has been made, and please wait for payment to be processed." + super().__init__(message, status_code, payload) + diff --git a/common/utils/mail_utils.py b/common/utils/mail_utils.py new file mode 100644 index 0000000..ff40848 --- /dev/null +++ b/common/utils/mail_utils.py @@ -0,0 +1,46 @@ +from scaleway import Client +from scaleway.tem.v1alpha1.api import TemV1Alpha1API +from scaleway.tem.v1alpha1.types import CreateEmailRequestAddress +from html2text import HTML2Text +from flask import current_app + + +def send_email(to_email, to_name, subject, html): + current_app.logger.debug(f"Sending email to {to_email} with subject {subject}") + access_key = current_app.config['SW_EMAIL_ACCESS_KEY'] + secret_key = current_app.config['SW_EMAIL_SECRET_KEY'] + default_project_id = current_app.config['SW_PROJECT'] + default_region = "fr-par" + current_app.logger.debug(f"Access Key: {access_key}\nSecret Key: {secret_key}\n" + f"Default Project ID: {default_project_id}\nDefault Region: {default_region}") + client = Client( + access_key=access_key, + secret_key=secret_key, + default_project_id=default_project_id, + default_region=default_region + ) + current_app.logger.debug(f"Scaleway Client Initialized") + tem = TemV1Alpha1API(client) + current_app.logger.debug(f"Tem Initialized") + from_ = CreateEmailRequestAddress(email=current_app.config['SW_EMAIL_SENDER'], + name=current_app.config['SW_EMAIL_NAME']) + to_ = CreateEmailRequestAddress(email=to_email, name=to_name) + + email = tem.create_email( + from_=from_, + to=[to_], + subject=subject, + text=html_to_text(html), + html=html, + project_id=default_project_id, + ) + current_app.logger.debug(f"Email sent to {to_email}") + + +def html_to_text(html_content): + """Convert HTML to plain text using html2text""" + h = HTML2Text() + h.ignore_images = True + h.ignore_emphasis = False + h.body_width = 0 # No wrapping + return h.handle(html_content) diff --git a/common/utils/middleware.py b/common/utils/middleware.py index 0301e27..f80e60f 100644 --- a/common/utils/middleware.py +++ b/common/utils/middleware.py @@ -4,11 +4,11 @@ for handling tenant requests """ from flask_security import current_user -from flask import session, current_app, redirect +from flask import session from .database import Database from .eveai_exceptions import EveAINoSessionTenant, EveAINoSessionPartner, EveAINoManagementPartnerService, \ EveAINoManagementPartnerForTenant -from ..services.user_services import UserServices +from common.services.user import UserServices def mw_before_request(): diff --git a/common/utils/model_logging_utils.py b/common/utils/model_logging_utils.py index 6578d51..94740c9 100644 --- a/common/utils/model_logging_utils.py +++ b/common/utils/model_logging_utils.py @@ -10,7 +10,6 @@ def set_logging_information(obj, timestamp): obj.created_by = user_id obj.updated_by = user_id - def update_logging_information(obj, timestamp): obj.updated_at = timestamp diff --git a/common/utils/security.py b/common/utils/security.py index 6d4c723..fd23676 100644 --- a/common/utils/security.py +++ b/common/utils/security.py @@ -39,11 +39,12 @@ def is_valid_tenant(tenant_id): raise EveAITenantInvalid(tenant_id) else: current_date = dt.now(tz=tz.utc).date() - active_license = (License.query.filter_by(tenant_id=tenant_id) - .filter(and_(License.start_date <= current_date, - License.end_date >= current_date)) - .one_or_none()) - if not active_license: - raise EveAINoActiveLicense(tenant_id) + # TODO -> Check vervangen door Active License Period! + # active_license = (License.query.filter_by(tenant_id=tenant_id) + # .filter(and_(License.start_date <= current_date, + # License.end_date >= current_date)) + # .one_or_none()) + # if not active_license: + # raise EveAINoActiveLicense(tenant_id) return True diff --git a/common/utils/security_utils.py b/common/utils/security_utils.py index 46f90cb..eae67ce 100644 --- a/common/utils/security_utils.py +++ b/common/utils/security_utils.py @@ -1,11 +1,10 @@ from flask import current_app, render_template from flask_security import current_user -from flask_mailman import EmailMessage from itsdangerous import URLSafeTimedSerializer -import socket from common.models.user import Role from common.utils.nginx_utils import prefixed_url_for +from common.utils.mail_utils import send_email def confirm_token(token, expiration=3600): @@ -18,14 +17,6 @@ def confirm_token(token, expiration=3600): return email -def send_email(to, subject, template): - msg = EmailMessage(subject=subject, - body=template, - to=[to]) - msg.content_subtype = "html" - msg.send() - - def generate_reset_token(email): serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) return serializer.dumps(email, salt=current_app.config['SECURITY_PASSWORD_SALT']) @@ -37,9 +28,6 @@ def generate_confirmation_token(email): def send_confirmation_email(user): - if not test_smtp_connection(): - raise Exception("Failed to connect to SMTP server") - token = generate_confirmation_token(user.email) confirm_url = prefixed_url_for('security_bp.confirm_email', token=token, _external=True) @@ -47,7 +35,7 @@ def send_confirmation_email(user): subject = "Please confirm your email" try: - send_email(user.email, "Confirm your email", html) + send_email(user.email, f"{user.first_name} {user.last_name}", "Confirm your email", html) current_app.logger.info(f'Confirmation email sent to {user.email}') except Exception as e: current_app.logger.error(f'Failed to send confirmation email to {user.email}. Error: {str(e)}') @@ -62,41 +50,13 @@ def send_reset_email(user): subject = "Reset Your Password" try: - send_email(user.email, "Reset Your Password", html) + send_email(user.email, f"{user.first_name} {user.last_name}", subject, html) current_app.logger.info(f'Reset email sent to {user.email}') except Exception as e: current_app.logger.error(f'Failed to send reset email to {user.email}. Error: {str(e)}') raise -def test_smtp_connection(): - try: - current_app.logger.info(f"Attempting to resolve google.com...") - google_ip = socket.gethostbyname('google.com') - current_app.logger.info(f"Successfully resolved google.com to {google_ip}") - except Exception as e: - current_app.logger.error(f"Failed to resolve google.com: {str(e)}") - - try: - smtp_server = current_app.config['MAIL_SERVER'] - current_app.logger.info(f"Attempting to resolve {smtp_server}...") - smtp_ip = socket.gethostbyname(smtp_server) - current_app.logger.info(f"Successfully resolved {smtp_server} to {smtp_ip}") - except Exception as e: - current_app.logger.error(f"Failed to resolve {smtp_server}: {str(e)}") - - try: - smtp_server = current_app.config['MAIL_SERVER'] - smtp_port = current_app.config['MAIL_PORT'] - sock = socket.create_connection((smtp_server, smtp_port), timeout=10) - sock.close() - current_app.logger.info(f"Successfully connected to SMTP server {smtp_server}:{smtp_port}") - return True - except Exception as e: - current_app.logger.error(f"Failed to connect to SMTP server: {str(e)}") - return False - - def get_current_user_roles(): """Get the roles of the currently authenticated user. diff --git a/common/utils/template_filters.py b/common/utils/template_filters.py index a14a2b2..f17ed3e 100644 --- a/common/utils/template_filters.py +++ b/common/utils/template_filters.py @@ -29,9 +29,23 @@ def time_difference(start_dt, end_dt): return "Ongoing" +def status_color(status_name): + """Return Bootstrap color class for status""" + colors = { + 'UPCOMING': 'secondary', + 'PENDING': 'warning', + 'ACTIVE': 'success', + 'COMPLETED': 'info', + 'INVOICED': 'primary', + 'CLOSED': 'dark' + } + return colors.get(status_name, 'secondary') + + def register_filters(app): """ Registers custom filters with the Flask app. """ app.jinja_env.filters['to_local_time'] = to_local_time app.jinja_env.filters['time_difference'] = time_difference + app.jinja_env.filters['status_color'] = status_color diff --git a/config/.DS_Store b/config/.DS_Store deleted file mode 100644 index 19a305d..0000000 Binary files a/config/.DS_Store and /dev/null differ diff --git a/config/__pycache__/__init__.cpython-312.pyc b/config/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 381a0b7..0000000 Binary files a/config/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/config/__pycache__/config.cpython-312.pyc b/config/__pycache__/config.cpython-312.pyc deleted file mode 100644 index 44122e4..0000000 Binary files a/config/__pycache__/config.cpython-312.pyc and /dev/null differ diff --git a/config/__pycache__/logging_config.cpython-312.pyc b/config/__pycache__/logging_config.cpython-312.pyc deleted file mode 100644 index 8e971ca..0000000 Binary files a/config/__pycache__/logging_config.cpython-312.pyc and /dev/null differ diff --git a/docker/.python-version b/docker/.python-version new file mode 100644 index 0000000..56bb660 --- /dev/null +++ b/docker/.python-version @@ -0,0 +1 @@ +3.12.7 diff --git a/eveai_app/.DS_Store b/eveai_app/.DS_Store deleted file mode 100644 index a049e9c..0000000 Binary files a/eveai_app/.DS_Store and /dev/null differ diff --git a/eveai_app/__pycache__/__init__.cpython-312.pyc b/eveai_app/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 9e1c516..0000000 Binary files a/eveai_app/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/__pycache__/errors.cpython-312.pyc b/eveai_app/__pycache__/errors.cpython-312.pyc deleted file mode 100644 index ea652c7..0000000 Binary files a/eveai_app/__pycache__/errors.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/templates/.DS_Store b/eveai_app/templates/.DS_Store deleted file mode 100644 index bd2e771..0000000 Binary files a/eveai_app/templates/.DS_Store and /dev/null differ diff --git a/eveai_app/views/.DS_Store b/eveai_app/views/.DS_Store deleted file mode 100644 index 0be32ea..0000000 Binary files a/eveai_app/views/.DS_Store and /dev/null differ diff --git a/eveai_app/views/__pycache__/__init__.cpython-312.pyc b/eveai_app/views/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 95ac753..0000000 Binary files a/eveai_app/views/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/views/__pycache__/basic_forms.cpython-312.pyc b/eveai_app/views/__pycache__/basic_forms.cpython-312.pyc deleted file mode 100644 index c57710c..0000000 Binary files a/eveai_app/views/__pycache__/basic_forms.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/views/__pycache__/basic_views.cpython-312.pyc b/eveai_app/views/__pycache__/basic_views.cpython-312.pyc deleted file mode 100644 index fbf1ed7..0000000 Binary files a/eveai_app/views/__pycache__/basic_views.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/views/__pycache__/document_forms.cpython-312.pyc b/eveai_app/views/__pycache__/document_forms.cpython-312.pyc deleted file mode 100644 index 3238be9..0000000 Binary files a/eveai_app/views/__pycache__/document_forms.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/views/__pycache__/document_views.cpython-312.pyc b/eveai_app/views/__pycache__/document_views.cpython-312.pyc deleted file mode 100644 index 9c290fd..0000000 Binary files a/eveai_app/views/__pycache__/document_views.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/views/__pycache__/interaction_views.cpython-312.pyc b/eveai_app/views/__pycache__/interaction_views.cpython-312.pyc deleted file mode 100644 index fe83f8c..0000000 Binary files a/eveai_app/views/__pycache__/interaction_views.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/views/__pycache__/security_forms.cpython-312.pyc b/eveai_app/views/__pycache__/security_forms.cpython-312.pyc deleted file mode 100644 index f1ea398..0000000 Binary files a/eveai_app/views/__pycache__/security_forms.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/views/__pycache__/security_views.cpython-312.pyc b/eveai_app/views/__pycache__/security_views.cpython-312.pyc deleted file mode 100644 index e59c7bf..0000000 Binary files a/eveai_app/views/__pycache__/security_views.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/views/__pycache__/user_forms.cpython-312.pyc b/eveai_app/views/__pycache__/user_forms.cpython-312.pyc deleted file mode 100644 index 34c7115..0000000 Binary files a/eveai_app/views/__pycache__/user_forms.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/views/__pycache__/user_views.cpython-312.pyc b/eveai_app/views/__pycache__/user_views.cpython-312.pyc deleted file mode 100644 index 3859279..0000000 Binary files a/eveai_app/views/__pycache__/user_views.cpython-312.pyc and /dev/null differ diff --git a/eveai_app/views/document_views.py b/eveai_app/views/document_views.py index 422c494..2f8fcf3 100644 --- a/eveai_app/views/document_views.py +++ b/eveai_app/views/document_views.py @@ -17,6 +17,7 @@ from common.models.interaction import Specialist, SpecialistRetriever from common.utils.document_utils import create_document_stack, start_embedding_task, process_url, \ edit_document, \ edit_document_version, refresh_document, clean_url +from common.utils.dynamic_field_utils import create_default_config_from_type_config from common.utils.eveai_exceptions import EveAIInvalidLanguageException, EveAIUnsupportedFileType, \ EveAIDoubleURLException, EveAIException from config.type_defs.processor_types import PROCESSOR_TYPES @@ -159,6 +160,8 @@ def processor(): new_processor = Processor() form.populate_obj(new_processor) new_processor.catalog_id = form.catalog.data.id + new_processor.configuration = create_default_config_from_type_config( + PROCESSOR_TYPES[new_processor.type]["configuration"]) set_logging_information(new_processor, dt.now(tz.utc)) @@ -181,7 +184,7 @@ def processor(): @document_bp.route('/processor/', methods=['GET', 'POST']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def edit_processor(processor_id): - """Edit an existing processorr configuration.""" + """Edit an existing processor configuration.""" # Get the processor or return 404 processor = Processor.query.get_or_404(processor_id) diff --git a/eveai_chat/.DS_Store b/eveai_chat/.DS_Store deleted file mode 100644 index e306a2f..0000000 Binary files a/eveai_chat/.DS_Store and /dev/null differ diff --git a/eveai_chat/__pycache__/__init__.cpython-312.pyc b/eveai_chat/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 0b3f736..0000000 Binary files a/eveai_chat/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/eveai_chat/socket_handlers/__pycache__/chat_handler.cpython-312.pyc b/eveai_chat/socket_handlers/__pycache__/chat_handler.cpython-312.pyc deleted file mode 100644 index b5c1803..0000000 Binary files a/eveai_chat/socket_handlers/__pycache__/chat_handler.cpython-312.pyc and /dev/null differ diff --git a/eveai_chat/views/.DS_Store b/eveai_chat/views/.DS_Store deleted file mode 100644 index 2e89e7e..0000000 Binary files a/eveai_chat/views/.DS_Store and /dev/null differ diff --git a/eveai_chat/views/__pycache__/chat_views.cpython-312.pyc b/eveai_chat/views/__pycache__/chat_views.cpython-312.pyc deleted file mode 100644 index c0d2e28..0000000 Binary files a/eveai_chat/views/__pycache__/chat_views.cpython-312.pyc and /dev/null differ diff --git a/eveai_chat_workers/__pycache__/__init__.cpython-312.pyc b/eveai_chat_workers/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 2cee1d2..0000000 Binary files a/eveai_chat_workers/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/eveai_chat_workers/__pycache__/tasks.cpython-312.pyc b/eveai_chat_workers/__pycache__/tasks.cpython-312.pyc deleted file mode 100644 index 2756a1c..0000000 Binary files a/eveai_chat_workers/__pycache__/tasks.cpython-312.pyc and /dev/null differ diff --git a/eveai_workers/__pycache__/__init__.cpython-312.pyc b/eveai_workers/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 8ae3244..0000000 Binary files a/eveai_workers/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/eveai_workers/__pycache__/tasks.cpython-312.pyc b/eveai_workers/__pycache__/tasks.cpython-312.pyc deleted file mode 100644 index ae58524..0000000 Binary files a/eveai_workers/__pycache__/tasks.cpython-312.pyc and /dev/null differ diff --git a/integrations/Zapier/eveai_integration/.gitignore b/integrations/Zapier/eveai_integration/.gitignore new file mode 100644 index 0000000..ae6ff4e --- /dev/null +++ b/integrations/Zapier/eveai_integration/.gitignore @@ -0,0 +1,63 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/ +dist/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# environment variables file +.env +.environment + +# next.js build output +.next diff --git a/integrations/Zapier/eveai_integration/test/creates/add_document.test.js b/integrations/Zapier/eveai_integration/test/creates/add_document.test.js new file mode 100644 index 0000000..67e465a --- /dev/null +++ b/integrations/Zapier/eveai_integration/test/creates/add_document.test.js @@ -0,0 +1,20 @@ +const zapier = require('zapier-platform-core'); + +// Use this to make test calls into your app: +const App = require('../../index'); +const appTester = zapier.createAppTester(App); +// read the `.env` file into the environment, if available +zapier.tools.env.inject(); + +describe('creates.add_document', () => { + it('should run', async () => { + const bundle = { inputData: {} }; + + const results = await appTester( + App.creates['add_document'].operation.perform, + bundle + ); + expect(results).toBeDefined(); + // TODO: add more assertions + }); +}); diff --git a/integrations/Zapier/eveai_integration/test/example.test.js b/integrations/Zapier/eveai_integration/test/example.test.js new file mode 100644 index 0000000..3e8b74d --- /dev/null +++ b/integrations/Zapier/eveai_integration/test/example.test.js @@ -0,0 +1,7 @@ +/* globals describe, it, expect */ + +describe('addition ', () => { + it('should work', () => { + expect(1 + 1).toEqual(2); + }); +}); diff --git a/nginx/static/.DS_Store b/nginx/static/.DS_Store deleted file mode 100644 index 6b60b01..0000000 Binary files a/nginx/static/.DS_Store and /dev/null differ diff --git a/nginx/static/assets/.DS_Store b/nginx/static/assets/.DS_Store deleted file mode 100644 index 3b894de..0000000 Binary files a/nginx/static/assets/.DS_Store and /dev/null differ diff --git a/nginx/static/assets/img/.DS_Store b/nginx/static/assets/img/.DS_Store deleted file mode 100644 index 711d5d8..0000000 Binary files a/nginx/static/assets/img/.DS_Store and /dev/null differ diff --git a/nginx/static/assets/js/.DS_Store b/nginx/static/assets/js/.DS_Store deleted file mode 100644 index ff05f2e..0000000 Binary files a/nginx/static/assets/js/.DS_Store and /dev/null differ diff --git a/nginx/static/assets/specialists/SPIN_SPECIALIST_1.0.0_overview.svg b/nginx/static/assets/specialists/SPIN_SPECIALIST_1.0.0_overview.svg new file mode 100644 index 0000000..c374ee4 --- /dev/null +++ b/nginx/static/assets/specialists/SPIN_SPECIALIST_1.0.0_overview.svg @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + + + EveAI Agent + + + + + + + + + + + + + + + + + + + + Identification Agent + + + + + + + + + + + + + + + + SPIN Sales Assistant + + + + + + + + + + + + + + + + SPIN Sales Specialist + + + + + + + + + + + + + + + + + + + + Identification Agent + + + + + + + + + + + + + + + + Consolidation Agent + + + + + + + + + + + + + + + + RAG Agent + + + + + + + + + + + + + + + + + EveAI Task + + + + + + + + + + + + + RAG Task + + + + + + + + + + + + + + + SPIN Detection + + + + + + + + + + + + + + + + + + + Identification Gathering + + + + + + + + + + + + + + + + + + + Identification Questions + + + + + + + + + + + + + + + Consolidate Q&A + + + + + + + + + + + + + + + SPIN Questions + + + + + + + + + + + + + EveAI Tool + + + + + + + + + + + + + + + + RAG Task + + + + + + + + + + + + + Retrieval + + + + + + + Q&A Processing + + + + + + + + + + + + + + + + + diff --git a/nginx/static/assets/specialists/TRAICIE_VACATURE_SPECIALIST_1.0.0_overview.svg b/nginx/static/assets/specialists/TRAICIE_VACATURE_SPECIALIST_1.0.0_overview.svg new file mode 100644 index 0000000..53f0227 --- /dev/null +++ b/nginx/static/assets/specialists/TRAICIE_VACATURE_SPECIALIST_1.0.0_overview.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + EveAI Agent + + + + + + + + + + + + + + + + + EveAI Task + + + + + + + + + + + + + EveAI Tool + + + + + + + + + + + + + + + + Retrieval + + + + + + + + + + + + + + + + + diff --git a/nginx/static/js/eveai-token-manager.js b/nginx/static/js/eveai-token-manager.js new file mode 100755 index 0000000..e69de29 diff --git a/repopack stuff (deprecated)/.repopackignore b/repopack stuff (deprecated)/.repopackignore new file mode 100644 index 0000000..639b115 --- /dev/null +++ b/repopack stuff (deprecated)/.repopackignore @@ -0,0 +1,55 @@ +# Add patterns to ignore here, one per line +# Example: +# *.log +# tmp/ +db_backups/ +logs/ +nginx/static/assets/fonts/ +nginx/static/assets/img/ +nginx/static/assets/js/ +nginx/static/scss/ +patched_packages/ +migrations/ +*material* +*nucleo* +*package* +*.svg +nginx/mime.types +*.gitignore* +.python-version +.repopackignore* +repopack.config.json +*repo.txt +temp_requirements/ +tests/ +docker/eveai_logs/ +docker/logs/ +patched_packages/docker/ +eveai_api/ +eveai_beat/ +eveai_chat/ +eveai_chat_workers/ +eveai_entitlements/ +eveai_workers/ +eveai_client/ +instance/ +integrations/ +migrations/ +nginx/ +scripts/ +common/models/entitlements.py +common/models/interaction.py +common/models/document.py +config/agents/ +config/prompts/ +config/specialists/ +config/tasks/ +config/tools/ +eveai_app/templates/administration/ +eveai_app/templates/entitlements/ +eveai_app/templates/interaction/ +eveai_app/templates/document/ +eveai_app/views/administration* +eveai_app/views/entitlements* +eveai_app/views/interaction* +eveai_app/views/document* \ No newline at end of file diff --git a/scripts/.DS_Store b/scripts/.DS_Store deleted file mode 100644 index 0392ff2..0000000 Binary files a/scripts/.DS_Store and /dev/null differ diff --git a/scripts/__pycache__/run_eveai_app.cpython-312.pyc b/scripts/__pycache__/run_eveai_app.cpython-312.pyc deleted file mode 100644 index ba6168c..0000000 Binary files a/scripts/__pycache__/run_eveai_app.cpython-312.pyc and /dev/null differ diff --git a/scripts/__pycache__/run_eveai_chat.cpython-312.pyc b/scripts/__pycache__/run_eveai_chat.cpython-312.pyc deleted file mode 100644 index f619f7f..0000000 Binary files a/scripts/__pycache__/run_eveai_chat.cpython-312.pyc and /dev/null differ diff --git a/scripts/__pycache__/run_eveai_workers.cpython-312.pyc b/scripts/__pycache__/run_eveai_workers.cpython-312.pyc deleted file mode 100644 index fbcde7d..0000000 Binary files a/scripts/__pycache__/run_eveai_workers.cpython-312.pyc and /dev/null differ