diff --git a/common/models/user.py b/common/models/user.py index 561cec9..9a2b78c 100644 --- a/common/models/user.py +++ b/common/models/user.py @@ -20,6 +20,7 @@ class Tenant(db.Model): # company Information id = db.Column(db.Integer, primary_key=True) + code = db.Column(db.String(50), unique=True, nullable=True) name = db.Column(db.String(80), unique=True, nullable=False) website = db.Column(db.String(255), nullable=True) timezone = db.Column(db.String(50), nullable=True, default='UTC') @@ -99,6 +100,7 @@ class User(db.Model, UserMixin): # User Information id = db.Column(db.Integer, primary_key=True) + tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) user_name = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(255), unique=True, nullable=False) password = db.Column(db.String(255), nullable=True) @@ -120,7 +122,6 @@ class User(db.Model, UserMixin): # Relations roles = db.relationship('Role', secondary=RolesUsers.__table__, backref=db.backref('users', lazy='dynamic')) - tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) def __repr__(self): return '' % self.user_name @@ -176,3 +177,91 @@ class TenantProject(db.Model): def __repr__(self): return f"" + + +class Partner(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, unique=True) + code = db.Column(db.String(50), unique=True, nullable=False) + + # Basic information + logo_url = db.Column(db.String(255), nullable=True) + active = db.Column(db.Boolean, default=True) + + # Versioning Information + created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) + created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True) + updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now()) + updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True) + + # Relationships + services = db.relationship('PartnerService', back_populates='partner') + tenant = db.relationship('Tenant', backref=db.backref('partner', uselist=False)) + + def to_dict(self): + return { + 'id': self.id, + 'tenant_id': self.tenant_id, + 'code': self.code, + 'logo_url': self.logo_url, + 'active': self.active, + 'name': self.tenant.name + } + + +class PartnerService(db.Model): + __bind_key__ = 'public' + __table_args__ = {'schema': 'public'} + + id = db.Column(db.Integer, primary_key=True) + partner_id = db.Column(db.Integer, db.ForeignKey('public.partner.id'), nullable=False) + + # Basic info + name = db.Column(db.String(50), nullable=False) + description = db.Column(db.Text, nullable=True) + + # Service type with versioning (similar to your specialist/retriever pattern) + type = db.Column(db.String(50), nullable=False) # REFERRAL, KNOWLEDGE, SPECIALIST, IMPLEMENTATION, WHITE_LABEL + type_version = db.Column(db.String(20), nullable=False, default="1.0.0") + + # Status + active = db.Column(db.Boolean, default=True) + + # Dynamic configuration specific to this service - using JSONB like your other models + configuration = db.Column(db.JSON, nullable=True) + + # For services that need to track shared resources + 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) + + +class PartnerTenant(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) + 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) + + # Tracking + 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) diff --git a/common/utils/cache/config_cache.py b/common/utils/cache/config_cache.py index 6db4c08..2756c1f 100644 --- a/common/utils/cache/config_cache.py +++ b/common/utils/cache/config_cache.py @@ -7,7 +7,7 @@ from flask import current_app from common.utils.cache.base import CacheHandler, CacheKey from config.type_defs import agent_types, task_types, tool_types, specialist_types, retriever_types, prompt_types, \ - catalog_types + catalog_types, partner_service_types def is_major_minor(version: str) -> bool: @@ -422,7 +422,6 @@ PromptConfigCacheHandler, PromptConfigVersionTreeCacheHandler, PromptConfigTypes config_type='prompts', config_dir='config/prompts', types_module=prompt_types.PROMPT_TYPES - )) CatalogConfigCacheHandler, CatalogConfigVersionTreeCacheHandler, CatalogConfigTypesCacheHandler = ( @@ -430,7 +429,14 @@ CatalogConfigCacheHandler, CatalogConfigVersionTreeCacheHandler, CatalogConfigTy config_type='catalogs', config_dir='config/catalogs', types_module=catalog_types.CATALOG_TYPES + )) +# Add to common/utils/cache/config_cache.py +PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, PartnerServiceConfigTypesCacheHandler = ( + create_config_cache_handlers( + config_type='partner_services', + config_dir='config/partner_services', + types_module=partner_service_types.PARTNER_SERVICE_TYPES )) @@ -459,6 +465,9 @@ def register_config_cache_handlers(cache_manager) -> None: cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config') cache_manager.register_handler(AgentConfigTypesCacheHandler, 'eveai_config') cache_manager.register_handler(AgentConfigVersionTreeCacheHandler, 'eveai_config') + cache_manager.register_handler(PartnerServiceConfigCacheHandler, 'eveai_config') + cache_manager.register_handler(PartnerServiceConfigTypesCacheHandler, 'eveai_config') + cache_manager.register_handler(PartnerServiceConfigVersionTreeCacheHandler, 'eveai_config') cache_manager.agents_config_cache.set_version_tree_cache(cache_manager.agents_version_tree_cache) cache_manager.tasks_config_cache.set_version_tree_cache(cache_manager.tasks_version_tree_cache) @@ -466,3 +475,4 @@ def register_config_cache_handlers(cache_manager) -> None: cache_manager.specialists_config_cache.set_version_tree_cache(cache_manager.specialists_version_tree_cache) cache_manager.retrievers_config_cache.set_version_tree_cache(cache_manager.retrievers_version_tree_cache) cache_manager.prompts_config_cache.set_version_tree_cache(cache_manager.prompts_version_tree_cache) + cache_manager.partner_services_config_cache.set_version_tree_cache(cache_manager.partner_services_version_tree_cache) diff --git a/common/utils/eveai_exceptions.py b/common/utils/eveai_exceptions.py index 66db901..f24f42c 100644 --- a/common/utils/eveai_exceptions.py +++ b/common/utils/eveai_exceptions.py @@ -136,3 +136,14 @@ class EveAIInvalidEmbeddingModel(EveAIException): # Construct the message dynamically message = f"Tenant with ID '{tenant_id}' has no or an invalid embedding model in Catalog {catalog_id}." super().__init__(message, status_code, payload) + + +class EveAIDoublePartner(EveAIException): + """Raised when there is already a partner defined for a given tenant (while registering a partner)""" + + def __init__(self, tenant_id, status_code=400, payload=None): + self.tenant_id = tenant_id + # Construct the message dynamically + message = f"Tenant with ID '{tenant_id}' is already defined as a Partner." + super().__init__(message, status_code, payload) + diff --git a/common/utils/form_assistants.py b/common/utils/form_assistants.py new file mode 100644 index 0000000..85064b5 --- /dev/null +++ b/common/utils/form_assistants.py @@ -0,0 +1,11 @@ +import json + +from wtforms.validators import ValidationError + + +def validate_json(form, field): + if field.data: + try: + json.loads(field.data) + except json.JSONDecodeError: + raise ValidationError('Invalid JSON format') \ No newline at end of file diff --git a/common/utils/log_utils.py b/common/utils/log_utils.py new file mode 100644 index 0000000..ccec50d --- /dev/null +++ b/common/utils/log_utils.py @@ -0,0 +1,57 @@ +import pandas as pd +from sqlalchemy import inspect +from typing import Any, List, Union, Optional + + +def format_query_results(query_results: Any) -> str: + """ + Format query results as a readable string using pandas + + Args: + query_results: SQLAlchemy query, query results, or model instance(s) + + Returns: + Formatted string representation of the query results + """ + try: + # If it's a query object, execute it + if hasattr(query_results, 'all'): + results = query_results.all() + elif not isinstance(query_results, list): + results = [query_results] + else: + results = query_results + + # Handle different types of results + if results and hasattr(results[0], '__table__'): + # SQLAlchemy ORM objects + data = [] + for item in results: + row = {} + for column in inspect(item).mapper.column_attrs: + row[column.key] = getattr(item, column.key) + data.append(row) + df = pd.DataFrame(data) + elif results and isinstance(results[0], tuple): + # Join query results (tuples) + if hasattr(results[0], '_fields'): # Named tuples + df = pd.DataFrame(results) + else: + # Regular tuples - try to get column names from query + if hasattr(query_results, 'statement'): + columns = query_results.statement.columns.keys() + df = pd.DataFrame(results, columns=columns) + else: + df = pd.DataFrame(results) + else: + # Fallback for other types + df = pd.DataFrame(results) + + # Format the output with pandas + with pd.option_context('display.max_rows', 20, 'display.max_columns', None, + 'display.width', 1000): + formatted_output = f"Query returned {len(df)} results:\n{df}" + + return formatted_output + except Exception as e: + return f"Error formatting query results: {str(e)}" diff --git a/common/utils/startup_eveai.py b/common/utils/startup_eveai.py index f333424..77107be 100644 --- a/common/utils/startup_eveai.py +++ b/common/utils/startup_eveai.py @@ -44,4 +44,5 @@ def perform_startup_invalidation(app): except Exception as e: app.logger.error(f"Error during startup invalidation: {e}") # In case of error, we don't want to block the application startup - pass \ No newline at end of file + pass + diff --git a/config/partner_services/MANAGEMENT_SERVICE/1.0.0.yaml b/config/partner_services/MANAGEMENT_SERVICE/1.0.0.yaml new file mode 100644 index 0000000..186004b --- /dev/null +++ b/config/partner_services/MANAGEMENT_SERVICE/1.0.0.yaml @@ -0,0 +1,15 @@ +version: "1.0.0" +name: "Management Service" +configuration: + admin_roles: + name: "Administrative Roles" + type: "enum" + description: "Administrative Roles that can be assigned to the partner" + required: true + allowed_values: ["Partner Admin"] + default: "Partner Admin" +metadata: + author: "Josako" + date_added: "2025-04-02" + changes: "Initial version" + description: "Initial definition of the management service" diff --git a/config/type_defs/partner_service_types.py b/config/type_defs/partner_service_types.py new file mode 100644 index 0000000..47e2b9f --- /dev/null +++ b/config/type_defs/partner_service_types.py @@ -0,0 +1,24 @@ +# config/type_defs/partner_service_types.py +PARTNER_SERVICE_TYPES = { + "REFERRAL_SERVICE": { + "name": "Referral Service", + "description": "Partner referring new customers", + }, + "KNOWLEDGE_SERVICE": { + "name": "Knowledge Service", + "description": "Partner providing catalog content", + }, + "SPECIALIST_SERVICE": { + "name": "Specialist Service", + "description": "Partner providing specialist solutions", + }, + "MANAGEMENT_SERVICE": { + "name": "Management Service", + "description": "Partner managing customer instances", + }, + "WHITE_LABEL_SERVICE": { + "name": "White Label Service", + "description": "Partner reselling under their own brand", + } +} + diff --git a/eveai_app/templates/administration/edit_partner.html b/eveai_app/templates/administration/edit_partner.html new file mode 100644 index 0000000..41e0522 --- /dev/null +++ b/eveai_app/templates/administration/edit_partner.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% from "macros.html" import render_field %} +{% block title %}Update Partner{% endblock %} + +{% block content_title %}Update Partner{% endblock %} +{% block content_description %}Update partner{% endblock %} + +{% block content %} +
+ {{ form.hidden_tag() }} + {% set disabled_fields = ['tenant', 'code'] %} + {% set exclude_fields = [] %} + {% for field in form %} + {{ render_field(field, disabled_fields, exclude_fields) }} + {% endfor %} + +
+{% endblock %} diff --git a/eveai_app/templates/administration/edit_partner_service.html b/eveai_app/templates/administration/edit_partner_service.html new file mode 100644 index 0000000..04207fe --- /dev/null +++ b/eveai_app/templates/administration/edit_partner_service.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% from "macros.html" import render_field %} +{% block title %}Register Partner Service{% endblock %} + +{% block content_title %}Register Partner Service{% endblock %} +{% block content_description %}Register Partner Service{% endblock %} + +{% block content %} +
+ {{ form.hidden_tag() }} + {% set disabled_fields = ['type'] %} + {% set exclude_fields = [] %} + {% for field in form %} + {{ render_field(field, disabled_fields, exclude_fields) }} + {% endfor %} + +
+{% endblock %} diff --git a/eveai_app/templates/administration/partner_service.html b/eveai_app/templates/administration/partner_service.html new file mode 100644 index 0000000..022a94e --- /dev/null +++ b/eveai_app/templates/administration/partner_service.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% from "macros.html" import render_field %} +{% block title %}Register Partner Service{% endblock %} + +{% block content_title %}Register Partner Service{% endblock %} +{% block content_description %}Register Partner Service{% endblock %} + +{% block content %} +
+ {{ form.hidden_tag() }} + {% set disabled_fields = [] %} + {% set exclude_fields = [] %} + {% for field in form %} + {{ render_field(field, disabled_fields, exclude_fields) }} + {% endfor %} + +
+{% endblock %} diff --git a/eveai_app/templates/administration/partner_services.html b/eveai_app/templates/administration/partner_services.html new file mode 100644 index 0000000..a7b77e8 --- /dev/null +++ b/eveai_app/templates/administration/partner_services.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% from 'macros.html' import render_selectable_table, render_pagination %} + +{% block title %}Partner Services{% endblock %} + +{% block content_title %}Partner Services{% endblock %} +{% block content_description %}View Partner Services for active Partner{% endblock %} +{% block content_class %}
{% endblock %} + +{% block content %} +
+
+ {{ render_selectable_table(headers=["Partner Service ID", "Name", "Type"], rows=rows, selectable=True, id="retrieversTable") }} +
+
+ +
+ +
+
+
+{% endblock %} + +{% block content_footer %} + {{ render_pagination(pagination, 'document_bp.retrievers') }} +{% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/administration/partners.html b/eveai_app/templates/administration/partners.html new file mode 100644 index 0000000..e9d1c94 --- /dev/null +++ b/eveai_app/templates/administration/partners.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% from 'macros.html' import render_selectable_table, render_pagination %} + +{% block title %}Partners{% endblock %} + +{% block content_title %}Partners{% endblock %} +{% block content_description %}View Partners{% endblock %} +{% block content_class %}
{% endblock %} + +{% block content %} +
+
+ {{ render_selectable_table(headers=["Partner ID", "Name"], rows=rows, selectable=True, id="partnersTable") }} +
+
+ + +
+
+
+
+{% endblock %} + +{% block content_footer %} + {{ render_pagination(pagination, 'document_bp.retrievers') }} +{% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/administration/trigger_actions.html b/eveai_app/templates/administration/trigger_actions.html index 8e41619..5829012 100644 --- a/eveai_app/templates/administration/trigger_actions.html +++ b/eveai_app/templates/administration/trigger_actions.html @@ -8,6 +8,7 @@
+
diff --git a/eveai_app/templates/basic/session_defaults.html b/eveai_app/templates/basic/session_defaults.html index 5e94509..2e364f5 100644 --- a/eveai_app/templates/basic/session_defaults.html +++ b/eveai_app/templates/basic/session_defaults.html @@ -9,7 +9,7 @@ {% block content %}
{{ form.hidden_tag() }} - {% set disabled_fields = ['user_name', 'user_email', 'tenant_name'] %} + {% set disabled_fields = ['user_name', 'user_email', 'tenant_name', 'partner_name'] %} {% set exclude_fields = [] %} {% for field in form %} {{ render_field(field, disabled_fields, exclude_fields) }} diff --git a/eveai_app/templates/navbar.html b/eveai_app/templates/navbar.html index b9a6b6c..7b2ef81 100644 --- a/eveai_app/templates/navbar.html +++ b/eveai_app/templates/navbar.html @@ -101,6 +101,8 @@ {'name': 'Trigger Actions', 'url': '/administration/trigger_actions', 'roles': ['Super User']}, {'name': 'Licenses', 'url': '/entitlements/view_licenses', 'roles': ['Super User', 'Tenant Admin']}, {'name': 'Usage', 'url': '/entitlements/view_usages', 'roles': ['Super User', 'Tenant Admin']}, + {'name': 'Partners', 'url': '/administration/partners', 'roles': ['Super User']}, + {'name': 'Partner Services', 'url': '/administration/partner_services', 'roles': ['Super User']}, ]) }} {% endif %} {% if current_user.is_authenticated %} @@ -123,6 +125,13 @@ {% endif %} + {% if current_user.has_roles('Super User') and 'partner' in session %} + + {% endif %} {% endif %} diff --git a/eveai_app/templates/user/edit_tenant.html b/eveai_app/templates/user/edit_tenant.html index 095c4fd..b7bc825 100644 --- a/eveai_app/templates/user/edit_tenant.html +++ b/eveai_app/templates/user/edit_tenant.html @@ -9,7 +9,7 @@ {% block content %} {{ form.hidden_tag() }} - {% set disabled_fields = ['name', 'llm_model'] %} + {% set disabled_fields = ['name', 'code', 'llm_model'] %} {% set exclude_fields = [] %} {% for field in form %} {{ render_field(field, disabled_fields, exclude_fields) }} diff --git a/eveai_app/templates/user/tenant.html b/eveai_app/templates/user/tenant.html index 77601a1..5f80c59 100644 --- a/eveai_app/templates/user/tenant.html +++ b/eveai_app/templates/user/tenant.html @@ -9,7 +9,7 @@ {% block content %} {{ form.hidden_tag() }} - {% set disabled_fields = [] %} + {% set disabled_fields = ['code'] %} {% set exclude_fields = [] %} {% for field in form %} {{ render_field(field, disabled_fields, exclude_fields) }} diff --git a/eveai_app/views/administration_forms.py b/eveai_app/views/administration_forms.py index bf58bc8..1f572ae 100644 --- a/eveai_app/views/administration_forms.py +++ b/eveai_app/views/administration_forms.py @@ -1,7 +1,46 @@ from flask import current_app from flask_wtf import FlaskForm -from wtforms.fields.simple import SubmitField +from wtforms.fields.choices import SelectField +from wtforms.fields.simple import SubmitField, StringField, BooleanField, TextAreaField +from wtforms.validators import DataRequired, Optional, Length + +from common.extensions import cache_manager +from common.utils.form_assistants import validate_json +from eveai_app.views.dynamic_form_base import DynamicFormBase class TriggerActionForm(FlaskForm): submit = SubmitField('Submit') + + +class EditPartnerForm(FlaskForm): + tenant = StringField('Tenant', validators=[DataRequired()], render_kw={'readonly': True}) + code = StringField('Code', description="Referral Code", validators=[DataRequired()], render_kw={'readonly': True}) + logo_url = StringField('Logo URL', validators=[Optional(), Length(max=255)]) + active = BooleanField('Active', validators=[DataRequired()], default=True) + + +class PartnerServiceForm(FlaskForm): + name = StringField('Name', validators=[DataRequired(), Length(max=50)]) + description = TextAreaField('Description', validators=[Optional()]) + type = SelectField('Partner Service Type', validators=[DataRequired()]) + active = BooleanField('Active', validators=[DataRequired()], default=True) + + user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json]) + system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json]) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Dynamically populate the 'type' field + types_dict = cache_manager.partner_services_types_cache.get_types() + self.type.choices = [(key, value['name']) for key, value in types_dict.items()] + + +class EditPartnerServiceForm(DynamicFormBase): + name = StringField('Name', validators=[DataRequired(), Length(max=50)]) + description = TextAreaField('Description', validators=[Optional()]) + type = StringField('Partner Service Type', validators=[DataRequired()], render_kw={'readonly': True}) + active = BooleanField('Active', validators=[DataRequired()], default=True) + + user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json]) + system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json]) diff --git a/eveai_app/views/administration_views.py b/eveai_app/views/administration_views.py index add2960..2f19487 100644 --- a/eveai_app/views/administration_views.py +++ b/eveai_app/views/administration_views.py @@ -1,3 +1,4 @@ +import ast import uuid from datetime import datetime as dt, timezone as tz from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify @@ -5,10 +6,15 @@ from flask_security import hash_password, roles_required, roles_accepted, curren from itsdangerous import URLSafeTimedSerializer from sqlalchemy.exc import SQLAlchemyError +from common.extensions import db, cache_manager +from common.models.user import Partner, Tenant, PartnerService from common.utils.celery_utils import current_celery +from common.utils.eveai_exceptions import EveAIException +from common.utils.log_utils import format_query_results +from common.utils.model_logging_utils import update_logging_information, set_logging_information from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed from common.utils.nginx_utils import prefixed_url_for -from .administration_forms import TriggerActionForm +from .administration_forms import TriggerActionForm, EditPartnerForm, PartnerServiceForm, EditPartnerServiceForm administration_bp = Blueprint('administration_bp', __name__, url_prefix='/administration') @@ -35,5 +41,215 @@ def handle_trigger_action(): except Exception as e: current_app.logger.error(f"Failed to trigger usage update task: {str(e)}") flash(f'Failed to trigger usage update: {str(e)}', 'danger') + case 'register_partner': + try: + partner_id = register_partner_from_tenant(session['tenant']['id']) + return redirect(prefixed_url_for('administration_bp.edit_partner', partner_id=partner_id, )) + except EveAIException as e: + current_app.logger.error(f'Error registering partner for tenant {session['tenant']['id']}: {str(e)}') + flash('Error Registering Partner for Selected Tenant', 'danger') + return redirect(prefixed_url_for('user_bp.select_tenant')) return redirect(prefixed_url_for('administration_bp.trigger_actions')) + + +@administration_bp.route('/partner/', methods=['GET', 'POST']) +@roles_accepted('Super User') +def edit_partner(partner_id): + partner = Partner.query.get_or_404(partner_id) # This will return a 404 if no partner is found + tenant = Tenant.query.get_or_404(partner.tenant_id) + form = EditPartnerForm(obj=partner) + form.tenant.data = tenant.name + + if form.validate_on_submit(): + # Populate the user with form data + form.populate_obj(partner) + update_logging_information(partner, dt.now(tz.utc)) + db.session.commit() + flash('Partner updated successfully.', 'success') + return redirect( + prefixed_url_for('administration_bp.edit_partner', + partner_id=partner.id)) # Assuming there's a user profile view to redirect to + else: + form_validation_failed(request, form) + + return render_template('administration/edit_partner.html', form=form, partner_id=partner_id) + + +@administration_bp.route('/partners', methods=['GET', 'POST']) +@roles_accepted('Super User') +def partners(): + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 10, type=int) + + query = (db.session.query( + Partner.id, + Partner.code, + Partner.active, + Partner.logo_url, + # Include all needed Partner columns here + Tenant.name.label('name') + ).join(Tenant, Partner.tenant_id == Tenant.id).order_by(Partner.id)) + + current_app.logger.debug(f'{format_query_results(query)}') + + pagination = query.paginate(page=page, per_page=per_page) + the_partners = pagination.items + + # prepare table data + rows = prepare_table_for_macro(the_partners, [('id', ''), ('name', '')]) + + # Render the catalogs in a template + return render_template('administration/partners.html', rows=rows, pagination=pagination) + + +@administration_bp.route('/handle_partner_selection', methods=['POST']) +@roles_accepted('Super User') +def handle_partner_selection(): + action = request.form['action'] + partner_identification = request.form.get('selected_row') + partner_id = ast.literal_eval(partner_identification).get('value') + partner = Partner.query.get_or_404(partner_id) + + if action == 'set_session_partner': + current_app.logger.info(f"Setting session partner: {partner.id}") + session['partner'] = partner.to_dict() + elif action == 'edit_partner': + return redirect(prefixed_url_for('administration_bp.edit_partner', partner_id=partner_id)) + + return redirect(prefixed_url_for('administration_bp.partners')) + + +@administration_bp.route('/partner_service', methods=['GET', 'POST']) +@roles_accepted('Super User') +def partner_service(): + form = PartnerServiceForm() + + if form.validate_on_submit(): + partner_id = session.get('partner_id', None) + if not partner_id: + flash('No partner has been selected. Set partner before adding services.', 'warning') + return redirect(prefixed_url_for('administration_bp.partners')) + new_partner_service = PartnerService() + form.populate_obj(new_partner_service) + set_logging_information(new_partner_service, dt.now(tz.utc)) + new_partner_service.partner_id = partner_id + + try: + db.session.add(new_partner_service) + db.session.commit() + flash('Partner Service successfully added!', 'success') + current_app.logger.info(f"Partner Service {new_partner_service.name} added successfully for {partner_id}") + # Step 2 of the creation process (depending on type) + return redirect(prefixed_url_for('administration_bp.partner_service', + partner_service_id=new_partner_service.id)) + except SQLAlchemyError as e: + db.session.rollback() + flash(f'Failed to add Partner Service: {str(e)}', 'danger') + current_app.logger.error(f"Failed to add Partner Service {new_partner_service.name} " + f"for partner {partner_id}. Error: {str(e)}") + return render_template('administration/partner_service.html', form=form) + + +@administration_bp.route('/partner_service/', methods=['GET', 'POST']) +@roles_accepted('Super User') +def edit_partner_service(partner_service_id): + partner_service = PartnerService.query.get_or_404(partner_service_id) + partner_id = session.get('partner_id', None) + + form = EditPartnerServiceForm(obj=partner_service) + partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type, + partner_service.type_version) + configuration_config = partner_service_config.get('configuration') + current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: " + f"{configuration_config}") + form.add_dynamic_fields("configuration", configuration_config, partner_service.configuration) + + if form.validate_on_submit(): + form.populate_obj(partner_service) + partner_service.configuration = form.get_dynamic_data('configuration') + + # update partner relationship + partner_service.partner_id = partner_id + + update_logging_information(partner_service, dt.now(tz.utc)) + + try: + db.session.add(partner_service) + db.session.commit() + flash('Partner Service updated successfully.', 'success') + current_app.logger.info(f"Partner Service {partner_service.name} updated successfully! ") + except SQLAlchemyError as e: + db.session.rollback() + flash(f'Failed to update Partner Service: {str(e)}', 'danger') + current_app.logger.error(f"Failed to update Partner Service {partner_service.id} for partner {partner_id}. " + f"Error: {str(e)} ") + return render_template('administration/edit_partner_service.html', form=form, + partner_service_id=partner_service_id) + + return redirect(prefixed_url_for('administration_bp.partner_services')) + else: + form_validation_failed(request, form) + + return render_template('administration/edit_partner_service.html', form=form, + partner_service_id=partner_service_id) + + +@administration_bp.route('/partner_services', methods=['GET', 'POST']) +@roles_accepted('Super User') +def partner_services(): + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 10, type=int) + partner_id = session.get('partner_id', None) + if not partner_id: + flash('No partner has been selected. Set partner before adding services.', 'warning') + return redirect(prefixed_url_for('administration_bp.partners')) + + query = PartnerService.query.filter(PartnerService.partner_id == partner_id) + + pagination = query.paginate(page=page, per_page=per_page) + the_partner_services = pagination.items + + # prepare table data + rows = prepare_table_for_macro(the_partner_services, [('id', ''), ('name', ''), ('type', '')]) + + return render_template('administration/partner_services.html', rows=rows, pagination=pagination) + + +@administration_bp.route('/handle_partner_service_selection', methods=['POST']) +@roles_accepted('Super User') +def handle_partner_service_selection(): + action = request.form['action'] + if action == 'create_partner_service': + return redirect(prefixed_url_for('administration_bp.partner_service')) + + partner_service_identification = request.form.get('selected_row') + partner_service_id = ast.literal_eval(partner_service_identification).get('value') + + if action == 'edit_partner_service': + return redirect(prefixed_url_for('administration_bp.edit_partner_service', + partner_service_id=partner_service_id)) + + return redirect(prefixed_url_for('administration_bp.partner_services')) + + +def register_partner_from_tenant(tenant_id): + # check if there is already a partner defined for the tenant + partner = Partner.query.filter_by(tenant_id=tenant_id).first() + if partner: + return partner.id + + try: + partner = Partner( + tenant_id=tenant_id, + code=f"PART-{str(uuid.uuid4())}", + ) + set_logging_information(partner, dt.now(tz.utc)) + db.session.add(partner) + db.session.commit() + return partner.id + except SQLAlchemyError as e: + db.session.rollback() + raise EveAIException(f"Failed to register partner for tenant {tenant_id}. Error: {str(e)}") + + diff --git a/eveai_app/views/basic_forms.py b/eveai_app/views/basic_forms.py index 62c467e..47ec19f 100644 --- a/eveai_app/views/basic_forms.py +++ b/eveai_app/views/basic_forms.py @@ -18,6 +18,9 @@ class SessionDefaultsForm(FlaskForm): tenant_name = StringField('Tenant Name', validators=[DataRequired()]) default_language = SelectField('Default Language', choices=[], validators=[DataRequired()]) + # Partner Defaults + partner_name = StringField('Partner Name', validators=[DataRequired()]) + # Default Catalog - initialize as a regular SelectField catalog = SelectField('Catalog', choices=[], validators=[Optional()]) @@ -28,6 +31,10 @@ class SessionDefaultsForm(FlaskForm): self.user_name.data = current_user.user_name self.user_email.data = current_user.email self.tenant_name.data = session.get('tenant').get('name') + if session.get('partner', None): + self.partner_name.data = session.get('partner').get('name') + else: + self.partner_name.data = "" self.default_language.choices = [(lang, lang.lower()) for lang in session.get('tenant').get('allowed_languages')] self.default_language.data = session.get('default_language') diff --git a/eveai_app/views/document_forms.py b/eveai_app/views/document_forms.py index 0392c0f..f120514 100644 --- a/eveai_app/views/document_forms.py +++ b/eveai_app/views/document_forms.py @@ -10,21 +10,13 @@ from wtforms_sqlalchemy.fields import QuerySelectField from common.extensions import cache_manager from common.models.document import Catalog +from common.utils.form_assistants import validate_json from config.type_defs.catalog_types import CATALOG_TYPES from config.type_defs.processor_types import PROCESSOR_TYPES -from config.type_defs.retriever_types import RETRIEVER_TYPES from .dynamic_form_base import DynamicFormBase -def validate_json(form, field): - if field.data: - try: - json.loads(field.data) - except json.JSONDecodeError: - raise ValidationError('Invalid JSON format') - - class CatalogForm(FlaskForm): name = StringField('Name', validators=[DataRequired(), Length(max=50)]) description = TextAreaField('Description', validators=[Optional()]) diff --git a/eveai_app/views/document_views.py b/eveai_app/views/document_views.py index ce676a1..f1e89ef 100644 --- a/eveai_app/views/document_views.py +++ b/eveai_app/views/document_views.py @@ -29,7 +29,6 @@ from common.utils.view_assistants import form_validation_failed, prepare_table_f from .document_list_view import DocumentListView from .document_version_list_view import DocumentVersionListView from config.type_defs.catalog_types import CATALOG_TYPES -from config.type_defs.retriever_types import RETRIEVER_TYPES document_bp = Blueprint('document_bp', __name__, url_prefix='/document') diff --git a/eveai_app/views/dynamic_form_base.py b/eveai_app/views/dynamic_form_base.py index e1078bd..32fda28 100644 --- a/eveai_app/views/dynamic_form_base.py +++ b/eveai_app/views/dynamic_form_base.py @@ -63,7 +63,7 @@ class DynamicFormBase(FlaskForm): # Required validator if field_def.get('required', False): - validators_list.append(validators.InputRequired()) + validators_list.append(validators.DataRequired()) else: validators_list.append(validators.Optional()) diff --git a/eveai_app/views/user_forms.py b/eveai_app/views/user_forms.py index 8396302..44c780a 100644 --- a/eveai_app/views/user_forms.py +++ b/eveai_app/views/user_forms.py @@ -11,6 +11,7 @@ from config.type_defs.service_types import SERVICE_TYPES class TenantForm(FlaskForm): name = StringField('Name', validators=[DataRequired(), Length(max=80)]) + code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True}) type = SelectField('Tenant Type', validators=[Optional()], default='Active') website = StringField('Website', validators=[DataRequired(), Length(max=255)]) # language fields @@ -125,3 +126,6 @@ class EditTenantProjectForm(FlaskForm): super().__init__(*args, **kwargs) # Initialize choices for the services field self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()] + + + diff --git a/eveai_app/views/user_views.py b/eveai_app/views/user_views.py index ae8fd3f..45295b7 100644 --- a/eveai_app/views/user_views.py +++ b/eveai_app/views/user_views.py @@ -8,7 +8,7 @@ from itsdangerous import URLSafeTimedSerializer from sqlalchemy.exc import SQLAlchemyError import ast -from common.models.user import User, Tenant, Role, TenantDomain, TenantProject +from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, Partner from common.extensions import db, security, minio_client, simple_encryption from common.utils.security_utils import send_confirmation_email, send_reset_email from config.type_defs.service_types import SERVICE_TYPES @@ -18,6 +18,8 @@ from common.utils.database import Database from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed from common.utils.simple_encryption import generate_api_key from common.utils.nginx_utils import prefixed_url_for +from common.utils.eveai_exceptions import EveAIDoublePartner, EveAIException +from common.utils.document_utils import set_logging_information, update_logging_information user_bp = Blueprint('user_bp', __name__, url_prefix='/user') @@ -36,6 +38,10 @@ def log_after_request(response): @roles_required('Super User') def tenant(): form = TenantForm() + if request.method == 'GET': + code = f"TENANT-{str(uuid.uuid4())}" + form.code.data = code + if form.validate_on_submit(): # Handle the required attributes new_tenant = Tenant() @@ -244,6 +250,7 @@ def handle_tenant_selection(): return redirect(prefixed_url_for('user_bp.tenant_overview')) case 'new_tenant': return redirect(prefixed_url_for('user_bp.tenant')) + # Add more conditions for other actions return redirect(prefixed_url_for('select_tenant')) diff --git a/migrations/.DS_Store b/migrations/.DS_Store deleted file mode 100644 index 7c27f81..0000000 Binary files a/migrations/.DS_Store and /dev/null differ diff --git a/migrations/public/.DS_Store b/migrations/public/.DS_Store deleted file mode 100644 index 6b5d62d..0000000 Binary files a/migrations/public/.DS_Store and /dev/null differ diff --git a/migrations/public/__pycache__/env.cpython-312.pyc b/migrations/public/__pycache__/env.cpython-312.pyc deleted file mode 100644 index 845ac97..0000000 Binary files a/migrations/public/__pycache__/env.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/867deef0888b_initialize_empty_tenant_codes.py b/migrations/public/versions/867deef0888b_initialize_empty_tenant_codes.py new file mode 100644 index 0000000..8654230 --- /dev/null +++ b/migrations/public/versions/867deef0888b_initialize_empty_tenant_codes.py @@ -0,0 +1,82 @@ +"""Initialize empty Tenant codes + +Revision ID: 867deef0888b +Revises: cab899dbb213 +Create Date: 2025-04-03 09:01:20.446536 + +""" +from alembic import op +import sqlalchemy as sa +import uuid +from sqlalchemy.sql import table, column, select, or_ +from sqlalchemy.orm import Session + + +# revision identifiers, used by Alembic. +revision = '867deef0888b' +down_revision = 'cab899dbb213' +branch_labels = None +depends_on = None + + +def upgrade(): + # Create a reference to the tenant table + tenant_table = table('tenant', + column('id', sa.Integer), + column('code', sa.String(50)), + schema='public' # Assuming the table is in the 'public' schema + ) + + # Get a connection + connection = op.get_bind() + session = Session(bind=connection) + + try: + # Find all tenants with empty or null code + # Note the updated select syntax for SQLAlchemy 2.0 + query = select(tenant_table.c.id).where( + or_( + tenant_table.c.code == None, + tenant_table.c.code == '' + ) + ) + + results = connection.execute(query) + + # Update each tenant with a UUID-based code + for row in results: + tenant_id = row[0] + code = f"TENANT-{str(uuid.uuid4())}" + + # Update the tenant record + update_stmt = tenant_table.update().where( + tenant_table.c.id == tenant_id + ).values( + code=code + ) + + connection.execute(update_stmt) + + # Commit changes + session.commit() + + # Log how many records were updated + count_query = select(sa.func.count()).select_from(tenant_table).where( + tenant_table.c.code.like('TENANT-%') + ) + + updated_count = connection.execute(count_query).scalar() + + print(f"Updated {updated_count} tenant records with UUID-based codes") + + except Exception as e: + session.rollback() + print(f"Error updating tenant codes: {str(e)}") + raise e + finally: + session.close() + + +def downgrade(): + # No downgrade needed for this data migration + pass \ No newline at end of file diff --git a/migrations/public/versions/98adf66ce189_add_partner_models.py b/migrations/public/versions/98adf66ce189_add_partner_models.py new file mode 100644 index 0000000..1d42be3 --- /dev/null +++ b/migrations/public/versions/98adf66ce189_add_partner_models.py @@ -0,0 +1,86 @@ +"""Add Partner Models + +Revision ID: 98adf66ce189 +Revises: 03a1e7633c01 +Create Date: 2025-03-31 14:43:06.833648 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '98adf66ce189' +down_revision = '03a1e7633c01' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('partner', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('code', sa.String(length=50), nullable=False), + sa.Column('logo_url', sa.String(length=255), nullable=True), + sa.Column('active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['public.user.id'], ), + sa.ForeignKeyConstraint(['tenant_id'], ['public.tenant.id'], ), + sa.ForeignKeyConstraint(['updated_by'], ['public.user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('code'), + sa.UniqueConstraint('tenant_id'), + schema='public' + ) + op.create_table('partner_service', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('partner_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('type', sa.String(length=50), nullable=False), + sa.Column('type_version', sa.String(length=20), nullable=False), + sa.Column('active', sa.Boolean(), nullable=True), + sa.Column('configuration', sa.JSON(), nullable=True), + sa.Column('system_metadata', sa.JSON(), nullable=True), + sa.Column('user_metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['public.user.id'], ), + sa.ForeignKeyConstraint(['partner_id'], ['public.partner.id'], ), + sa.ForeignKeyConstraint(['updated_by'], ['public.user.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + op.create_table('partner_tenant', + sa.Column('partner_service_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('relationship_type', sa.String(length=20), nullable=False), + sa.Column('configuration', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['public.user.id'], ), + sa.ForeignKeyConstraint(['partner_service_id'], ['public.partner_service.id'], ), + sa.ForeignKeyConstraint(['tenant_id'], ['public.tenant.id'], ), + sa.ForeignKeyConstraint(['updated_by'], ['public.user.id'], ), + sa.PrimaryKeyConstraint('partner_service_id', 'tenant_id'), + schema='public' + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + op.drop_table('partner_tenant', schema='public') + op.drop_table('partner_service', schema='public') + op.drop_table('partner', schema='public') + # ### end Alembic commands ### diff --git a/migrations/public/versions/__pycache__/07c7128c166e_refactor_to_flask_security.cpython-312.pyc b/migrations/public/versions/__pycache__/07c7128c166e_refactor_to_flask_security.cpython-312.pyc deleted file mode 100644 index 5408f19..0000000 Binary files a/migrations/public/versions/__pycache__/07c7128c166e_refactor_to_flask_security.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/08e334d64cab_flask_security_active_attribute.cpython-312.pyc b/migrations/public/versions/__pycache__/08e334d64cab_flask_security_active_attribute.cpython-312.pyc deleted file mode 100644 index 49a283c..0000000 Binary files a/migrations/public/versions/__pycache__/08e334d64cab_flask_security_active_attribute.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/090e8ec0fcad_added_user_to_public_schema.cpython-312.pyc b/migrations/public/versions/__pycache__/090e8ec0fcad_added_user_to_public_schema.cpython-312.pyc deleted file mode 100644 index 98424bc..0000000 Binary files a/migrations/public/versions/__pycache__/090e8ec0fcad_added_user_to_public_schema.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/0cab60b47683_adding_timezone_to_tenant.cpython-312.pyc b/migrations/public/versions/__pycache__/0cab60b47683_adding_timezone_to_tenant.cpython-312.pyc deleted file mode 100644 index 551e05a..0000000 Binary files a/migrations/public/versions/__pycache__/0cab60b47683_adding_timezone_to_tenant.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/167d821471ed_2024_07_04t09_42_15z.cpython-312.pyc b/migrations/public/versions/__pycache__/167d821471ed_2024_07_04t09_42_15z.cpython-312.pyc deleted file mode 100644 index 9d4a7fe..0000000 Binary files a/migrations/public/versions/__pycache__/167d821471ed_2024_07_04t09_42_15z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/1716099b62f0_enable_chunk_size_definition_at_tenant_.cpython-312.pyc b/migrations/public/versions/__pycache__/1716099b62f0_enable_chunk_size_definition_at_tenant_.cpython-312.pyc deleted file mode 100644 index 2951f33..0000000 Binary files a/migrations/public/versions/__pycache__/1716099b62f0_enable_chunk_size_definition_at_tenant_.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/1751a5318d34_2024_07_04t09_21_49z.cpython-312.pyc b/migrations/public/versions/__pycache__/1751a5318d34_2024_07_04t09_21_49z.cpython-312.pyc deleted file mode 100644 index 9abcaed..0000000 Binary files a/migrations/public/versions/__pycache__/1751a5318d34_2024_07_04t09_21_49z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/29c9d981a10b_security_additions_to_tenants_cors.cpython-312.pyc b/migrations/public/versions/__pycache__/29c9d981a10b_security_additions_to_tenants_cors.cpython-312.pyc deleted file mode 100644 index 93e5d9e..0000000 Binary files a/migrations/public/versions/__pycache__/29c9d981a10b_security_additions_to_tenants_cors.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/2a18dab7d1a4_2024_07_04t09_43_14z.cpython-312.pyc b/migrations/public/versions/__pycache__/2a18dab7d1a4_2024_07_04t09_43_14z.cpython-312.pyc deleted file mode 100644 index e66e314..0000000 Binary files a/migrations/public/versions/__pycache__/2a18dab7d1a4_2024_07_04t09_43_14z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/358bb5f8ebf1_adding_rag_context_and_fallback_.cpython-312.pyc b/migrations/public/versions/__pycache__/358bb5f8ebf1_adding_rag_context_and_fallback_.cpython-312.pyc deleted file mode 100644 index 844ee1d..0000000 Binary files a/migrations/public/versions/__pycache__/358bb5f8ebf1_adding_rag_context_and_fallback_.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/38b9e73d847f_2024_07_04t09_38_14z.cpython-312.pyc b/migrations/public/versions/__pycache__/38b9e73d847f_2024_07_04t09_38_14z.cpython-312.pyc deleted file mode 100644 index 1e66ede..0000000 Binary files a/migrations/public/versions/__pycache__/38b9e73d847f_2024_07_04t09_38_14z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/3ebec5c5b065_flask_security_trackable.cpython-312.pyc b/migrations/public/versions/__pycache__/3ebec5c5b065_flask_security_trackable.cpython-312.pyc deleted file mode 100644 index 8245e29..0000000 Binary files a/migrations/public/versions/__pycache__/3ebec5c5b065_flask_security_trackable.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/445db8db7cbc_defined_interaction_domain.cpython-312.pyc b/migrations/public/versions/__pycache__/445db8db7cbc_defined_interaction_domain.cpython-312.pyc deleted file mode 100644 index bf6300b..0000000 Binary files a/migrations/public/versions/__pycache__/445db8db7cbc_defined_interaction_domain.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/4701b1283750_remove_unneeded_is_active_as_it_is_.cpython-312.pyc b/migrations/public/versions/__pycache__/4701b1283750_remove_unneeded_is_active_as_it_is_.cpython-312.pyc deleted file mode 100644 index 190d476..0000000 Binary files a/migrations/public/versions/__pycache__/4701b1283750_remove_unneeded_is_active_as_it_is_.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/66739292fa56_allow_for_more_than_1_api_key.cpython-312.pyc b/migrations/public/versions/__pycache__/66739292fa56_allow_for_more_than_1_api_key.cpython-312.pyc deleted file mode 100644 index 00a4c7f..0000000 Binary files a/migrations/public/versions/__pycache__/66739292fa56_allow_for_more_than_1_api_key.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/668e4a1000cb_2024_07_04t12_47_41z.cpython-312.pyc b/migrations/public/versions/__pycache__/668e4a1000cb_2024_07_04t12_47_41z.cpython-312.pyc deleted file mode 100644 index b25884c..0000000 Binary files a/migrations/public/versions/__pycache__/668e4a1000cb_2024_07_04t12_47_41z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/6a34aecee578_added_validation_date_to_tenantdomain.cpython-312.pyc b/migrations/public/versions/__pycache__/6a34aecee578_added_validation_date_to_tenantdomain.cpython-312.pyc deleted file mode 100644 index 9f1eea0..0000000 Binary files a/migrations/public/versions/__pycache__/6a34aecee578_added_validation_date_to_tenantdomain.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/884ac7420cc5_add_temperature_variables_defaults.cpython-312.pyc b/migrations/public/versions/__pycache__/884ac7420cc5_add_temperature_variables_defaults.cpython-312.pyc deleted file mode 100644 index b897278..0000000 Binary files a/migrations/public/versions/__pycache__/884ac7420cc5_add_temperature_variables_defaults.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/89a287bbd574_2024_07_04t12_28_00z.cpython-312.pyc b/migrations/public/versions/__pycache__/89a287bbd574_2024_07_04t12_28_00z.cpython-312.pyc deleted file mode 100644 index 57d503c..0000000 Binary files a/migrations/public/versions/__pycache__/89a287bbd574_2024_07_04t12_28_00z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/8ab72fe23ee4_2024_07_04t12_51_36z.cpython-312.pyc b/migrations/public/versions/__pycache__/8ab72fe23ee4_2024_07_04t12_51_36z.cpython-312.pyc deleted file mode 100644 index 2c217d7..0000000 Binary files a/migrations/public/versions/__pycache__/8ab72fe23ee4_2024_07_04t12_51_36z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/92cdb9c6f6b7_added_versioning_information_to_.cpython-312.pyc b/migrations/public/versions/__pycache__/92cdb9c6f6b7_added_versioning_information_to_.cpython-312.pyc deleted file mode 100644 index 902b87f..0000000 Binary files a/migrations/public/versions/__pycache__/92cdb9c6f6b7_added_versioning_information_to_.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/92f0bd8ddd69_initial_public_schema.cpython-312.pyc b/migrations/public/versions/__pycache__/92f0bd8ddd69_initial_public_schema.cpython-312.pyc deleted file mode 100644 index e98fabc..0000000 Binary files a/migrations/public/versions/__pycache__/92f0bd8ddd69_initial_public_schema.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/a8c1c8e9a31a_security_additions_to_tenants.cpython-312.pyc b/migrations/public/versions/__pycache__/a8c1c8e9a31a_security_additions_to_tenants.cpython-312.pyc deleted file mode 100644 index 248a5ed..0000000 Binary files a/migrations/public/versions/__pycache__/a8c1c8e9a31a_security_additions_to_tenants.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/ac7753aa8fa3_add_tags_for_html_embeddings_and_.cpython-312.pyc b/migrations/public/versions/__pycache__/ac7753aa8fa3_add_tags_for_html_embeddings_and_.cpython-312.pyc deleted file mode 100644 index 7f7dc08..0000000 Binary files a/migrations/public/versions/__pycache__/ac7753aa8fa3_add_tags_for_html_embeddings_and_.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/b0ac53ab9d12_changed_user_to_include_is_super.cpython-312.pyc b/migrations/public/versions/__pycache__/b0ac53ab9d12_changed_user_to_include_is_super.cpython-312.pyc deleted file mode 100644 index 146fb51..0000000 Binary files a/migrations/public/versions/__pycache__/b0ac53ab9d12_changed_user_to_include_is_super.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/b7258a741c2f_add_embedding_search_parameters.cpython-312.pyc b/migrations/public/versions/__pycache__/b7258a741c2f_add_embedding_search_parameters.cpython-312.pyc deleted file mode 100644 index 6d034d8..0000000 Binary files a/migrations/public/versions/__pycache__/b7258a741c2f_add_embedding_search_parameters.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/b79748d3aaae_security_additions_to_tenants_cors_part_.cpython-312.pyc b/migrations/public/versions/__pycache__/b79748d3aaae_security_additions_to_tenants_cors_part_.cpython-312.pyc deleted file mode 100644 index 6e6755a..0000000 Binary files a/migrations/public/versions/__pycache__/b79748d3aaae_security_additions_to_tenants_cors_part_.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/b853a06f4bf4_language_llm_fields_added.cpython-312.pyc b/migrations/public/versions/__pycache__/b853a06f4bf4_language_llm_fields_added.cpython-312.pyc deleted file mode 100644 index 5250467..0000000 Binary files a/migrations/public/versions/__pycache__/b853a06f4bf4_language_llm_fields_added.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/cda254aa11ec_2024_07_04t09_59_04z.cpython-312.pyc b/migrations/public/versions/__pycache__/cda254aa11ec_2024_07_04t09_59_04z.cpython-312.pyc deleted file mode 100644 index 5ffa348..0000000 Binary files a/migrations/public/versions/__pycache__/cda254aa11ec_2024_07_04t09_59_04z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/ce68bccd110e_tuning_parameters_added_to_tenant.cpython-312.pyc b/migrations/public/versions/__pycache__/ce68bccd110e_tuning_parameters_added_to_tenant.cpython-312.pyc deleted file mode 100644 index c1b24cb..0000000 Binary files a/migrations/public/versions/__pycache__/ce68bccd110e_tuning_parameters_added_to_tenant.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/d4a45bf36bfc_add_temperature_variables.cpython-312.pyc b/migrations/public/versions/__pycache__/d4a45bf36bfc_add_temperature_variables.cpython-312.pyc deleted file mode 100644 index 158f147..0000000 Binary files a/migrations/public/versions/__pycache__/d4a45bf36bfc_add_temperature_variables.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/d93eab8ce138_2024_07_04t09_36_42z.cpython-312.pyc b/migrations/public/versions/__pycache__/d93eab8ce138_2024_07_04t09_36_42z.cpython-312.pyc deleted file mode 100644 index fb1f6aa..0000000 Binary files a/migrations/public/versions/__pycache__/d93eab8ce138_2024_07_04t09_36_42z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/dde18fb96c17_tenant_domains_do_not_need_to_be_unique.cpython-312.pyc b/migrations/public/versions/__pycache__/dde18fb96c17_tenant_domains_do_not_need_to_be_unique.cpython-312.pyc deleted file mode 100644 index 99d91ca..0000000 Binary files a/migrations/public/versions/__pycache__/dde18fb96c17_tenant_domains_do_not_need_to_be_unique.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/df7bd57b7ae8_2024_07_04t09_08_45z.cpython-312.pyc b/migrations/public/versions/__pycache__/df7bd57b7ae8_2024_07_04t09_08_45z.cpython-312.pyc deleted file mode 100644 index 3bde0d5..0000000 Binary files a/migrations/public/versions/__pycache__/df7bd57b7ae8_2024_07_04t09_08_45z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/e76f189a6489_2024_07_04t09_40_45z.cpython-312.pyc b/migrations/public/versions/__pycache__/e76f189a6489_2024_07_04t09_40_45z.cpython-312.pyc deleted file mode 100644 index 57fc05f..0000000 Binary files a/migrations/public/versions/__pycache__/e76f189a6489_2024_07_04t09_40_45z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/ecf1215f61da_add_tags_for_html_embeddings.cpython-312.pyc b/migrations/public/versions/__pycache__/ecf1215f61da_add_tags_for_html_embeddings.cpython-312.pyc deleted file mode 100644 index ff8404e..0000000 Binary files a/migrations/public/versions/__pycache__/ecf1215f61da_add_tags_for_html_embeddings.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/edb5953e3438_2024_07_04t08_18_50z.cpython-312.pyc b/migrations/public/versions/__pycache__/edb5953e3438_2024_07_04t08_18_50z.cpython-312.pyc deleted file mode 100644 index 2926945..0000000 Binary files a/migrations/public/versions/__pycache__/edb5953e3438_2024_07_04t08_18_50z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/fdc9ac232c49_2024_07_04t07_39_16z.cpython-312.pyc b/migrations/public/versions/__pycache__/fdc9ac232c49_2024_07_04t07_39_16z.cpython-312.pyc deleted file mode 100644 index 74ca188..0000000 Binary files a/migrations/public/versions/__pycache__/fdc9ac232c49_2024_07_04t07_39_16z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/__pycache__/ff206102492b_2024_07_04t09_28_42z.cpython-312.pyc b/migrations/public/versions/__pycache__/ff206102492b_2024_07_04t09_28_42z.cpython-312.pyc deleted file mode 100644 index bef915e..0000000 Binary files a/migrations/public/versions/__pycache__/ff206102492b_2024_07_04t09_28_42z.cpython-312.pyc and /dev/null differ diff --git a/migrations/public/versions/cab899dbb213_add_code_to_tenant.py b/migrations/public/versions/cab899dbb213_add_code_to_tenant.py new file mode 100644 index 0000000..277f5b1 --- /dev/null +++ b/migrations/public/versions/cab899dbb213_add_code_to_tenant.py @@ -0,0 +1,34 @@ +"""Add code to Tenant + +Revision ID: cab899dbb213 +Revises: 98adf66ce189 +Create Date: 2025-04-02 16:08:06.597183 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'cab899dbb213' +down_revision = '98adf66ce189' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tenant', schema=None) as batch_op: + batch_op.add_column(sa.Column('code', sa.String(length=50), nullable=True)) + batch_op.create_unique_constraint(None, ['code']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tenant', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_column('code') + + # ### end Alembic commands ### diff --git a/migrations/tenant/.DS_Store b/migrations/tenant/.DS_Store deleted file mode 100644 index 2cb5b56..0000000 Binary files a/migrations/tenant/.DS_Store and /dev/null differ diff --git a/migrations/tenant/__pycache__/env.cpython-312.pyc b/migrations/tenant/__pycache__/env.cpython-312.pyc deleted file mode 100644 index fef0463..0000000 Binary files a/migrations/tenant/__pycache__/env.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/.DS_Store b/migrations/tenant/versions/.DS_Store deleted file mode 100644 index e793af5..0000000 Binary files a/migrations/tenant/versions/.DS_Store and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/217938792642_removing_documentlanguage_user_context_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/217938792642_removing_documentlanguage_user_context_.cpython-312.pyc deleted file mode 100644 index ddc62de..0000000 Binary files a/migrations/tenant/versions/__pycache__/217938792642_removing_documentlanguage_user_context_.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/324ed734ed9b_chunking_information_added_to_document_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/324ed734ed9b_chunking_information_added_to_document_.cpython-312.pyc deleted file mode 100644 index 9e7d126..0000000 Binary files a/migrations/tenant/versions/__pycache__/324ed734ed9b_chunking_information_added_to_document_.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/3bc7bbed1ec6_ensure_latest_version_id_is_nullable.cpython-312.pyc b/migrations/tenant/versions/__pycache__/3bc7bbed1ec6_ensure_latest_version_id_is_nullable.cpython-312.pyc deleted file mode 100644 index ce37c6f..0000000 Binary files a/migrations/tenant/versions/__pycache__/3bc7bbed1ec6_ensure_latest_version_id_is_nullable.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/45620002ae0d_additional_processing_fields_added_to_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/45620002ae0d_additional_processing_fields_added_to_.cpython-312.pyc deleted file mode 100644 index b23bc19..0000000 Binary files a/migrations/tenant/versions/__pycache__/45620002ae0d_additional_processing_fields_added_to_.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/4f22dd6260e3_embeddings_for_openai.cpython-312.pyc b/migrations/tenant/versions/__pycache__/4f22dd6260e3_embeddings_for_openai.cpython-312.pyc deleted file mode 100644 index 4923fd9..0000000 Binary files a/migrations/tenant/versions/__pycache__/4f22dd6260e3_embeddings_for_openai.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/505b61314bba_defined_interaction_domain.cpython-312.pyc b/migrations/tenant/versions/__pycache__/505b61314bba_defined_interaction_domain.cpython-312.pyc deleted file mode 100644 index d01f0fe..0000000 Binary files a/migrations/tenant/versions/__pycache__/505b61314bba_defined_interaction_domain.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/53fee8f13cdb_defined_interaction_domain_2.cpython-312.pyc b/migrations/tenant/versions/__pycache__/53fee8f13cdb_defined_interaction_domain_2.cpython-312.pyc deleted file mode 100644 index 7a7c284..0000000 Binary files a/migrations/tenant/versions/__pycache__/53fee8f13cdb_defined_interaction_domain_2.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/5c67abd3b371_add_caching_for_latest_version_in_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/5c67abd3b371_add_caching_for_latest_version_in_.cpython-312.pyc deleted file mode 100644 index 12e8790..0000000 Binary files a/migrations/tenant/versions/__pycache__/5c67abd3b371_add_caching_for_latest_version_in_.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/5d5437d81041_add_embedding_model_for_openai_large_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/5d5437d81041_add_embedding_model_for_openai_large_.cpython-312.pyc deleted file mode 100644 index 8f822fb..0000000 Binary files a/migrations/tenant/versions/__pycache__/5d5437d81041_add_embedding_model_for_openai_large_.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/6fbceab656a8_adding_algorithm_information_in_the_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/6fbceab656a8_adding_algorithm_information_in_the_.cpython-312.pyc deleted file mode 100644 index eea460b..0000000 Binary files a/migrations/tenant/versions/__pycache__/6fbceab656a8_adding_algorithm_information_in_the_.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/83b4a2aedbf1_initial_document_models_added.cpython-312.pyc b/migrations/tenant/versions/__pycache__/83b4a2aedbf1_initial_document_models_added.cpython-312.pyc deleted file mode 100644 index 920338c..0000000 Binary files a/migrations/tenant/versions/__pycache__/83b4a2aedbf1_initial_document_models_added.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/a3f3f5eaa3de_adding_detailed_question_information_to_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/a3f3f5eaa3de_adding_detailed_question_information_to_.cpython-312.pyc deleted file mode 100644 index a37271f..0000000 Binary files a/migrations/tenant/versions/__pycache__/a3f3f5eaa3de_adding_detailed_question_information_to_.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/cb01c9192dc1_interaction_appreciation_should_be_null_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/cb01c9192dc1_interaction_appreciation_should_be_null_.cpython-312.pyc deleted file mode 100644 index 4ee418c..0000000 Binary files a/migrations/tenant/versions/__pycache__/cb01c9192dc1_interaction_appreciation_should_be_null_.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/d173cea8d204_added_timezone_information_of_chat_user_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/d173cea8d204_added_timezone_information_of_chat_user_.cpython-312.pyc deleted file mode 100644 index a0f5e48..0000000 Binary files a/migrations/tenant/versions/__pycache__/d173cea8d204_added_timezone_information_of_chat_user_.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/e2735d216d3c_defined_interaction_domain.cpython-312.pyc b/migrations/tenant/versions/__pycache__/e2735d216d3c_defined_interaction_domain.cpython-312.pyc deleted file mode 100644 index da04751..0000000 Binary files a/migrations/tenant/versions/__pycache__/e2735d216d3c_defined_interaction_domain.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/f5bbaed3a9ee_embeddings_according_to_pgvector_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/f5bbaed3a9ee_embeddings_according_to_pgvector_.cpython-312.pyc deleted file mode 100644 index 2935c41..0000000 Binary files a/migrations/tenant/versions/__pycache__/f5bbaed3a9ee_embeddings_according_to_pgvector_.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/f6ecc306055a_adding_session_id_to_chatsession.cpython-312.pyc b/migrations/tenant/versions/__pycache__/f6ecc306055a_adding_session_id_to_chatsession.cpython-312.pyc deleted file mode 100644 index 1ccec0d..0000000 Binary files a/migrations/tenant/versions/__pycache__/f6ecc306055a_adding_session_id_to_chatsession.cpython-312.pyc and /dev/null differ diff --git a/migrations/tenant/versions/__pycache__/f88854b2ac59_full_support_for_multiple_embedding_.cpython-312.pyc b/migrations/tenant/versions/__pycache__/f88854b2ac59_full_support_for_multiple_embedding_.cpython-312.pyc deleted file mode 100644 index 770d81d..0000000 Binary files a/migrations/tenant/versions/__pycache__/f88854b2ac59_full_support_for_multiple_embedding_.cpython-312.pyc and /dev/null differ diff --git a/requirements.txt b/requirements.txt index 3f1708e..eb4d55f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -89,4 +89,5 @@ sseclient~=0.0.27 termcolor~=2.5.0 mistral-common~=1.5.3 mistralai~=1.5.0 -contextvars~=2.4 \ No newline at end of file +contextvars~=2.4 +pandas~=2.2.3 \ No newline at end of file