Introduction of Partner Model, adding code to Tenant model

This commit is contained in:
Josako
2025-04-03 14:07:23 +02:00
parent 1762b930bc
commit 9ad7c1aee9
93 changed files with 823 additions and 22 deletions

View File

@@ -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 '<User %r>' % self.user_name
@@ -176,3 +177,91 @@ class TenantProject(db.Model):
def __repr__(self):
return f"<TenantProject {self.id}: {self.name}>"
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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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')

57
common/utils/log_utils.py Normal file
View File

@@ -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)}"

View File

@@ -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
pass