Merge branch 'main' of https://git.igny8.com/salman/igny8
This commit is contained in:
@@ -43,33 +43,21 @@ class AICore:
|
||||
self._load_account_settings()
|
||||
|
||||
def _load_account_settings(self):
|
||||
"""Load API keys from IntegrationSettings for account only - no fallbacks"""
|
||||
def get_integration_key(integration_type: str, account):
|
||||
if not account:
|
||||
return None
|
||||
try:
|
||||
from igny8_core.modules.system.models import IntegrationSettings
|
||||
settings_obj = IntegrationSettings.objects.filter(
|
||||
integration_type=integration_type,
|
||||
account=account,
|
||||
is_active=True
|
||||
).first()
|
||||
if settings_obj and settings_obj.config:
|
||||
return settings_obj.config.get('apiKey')
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load {integration_type} settings for account {getattr(account, 'id', None)}: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
# Load account-specific keys only - configure via Django admin
|
||||
if self.account:
|
||||
self._openai_api_key = get_integration_key('openai', self.account)
|
||||
self._runware_api_key = get_integration_key('runware', self.account)
|
||||
|
||||
# Fallback to Django settings as last resort
|
||||
if not self._openai_api_key:
|
||||
self._openai_api_key = getattr(settings, 'OPENAI_API_KEY', None)
|
||||
if not self._runware_api_key:
|
||||
self._runware_api_key = getattr(settings, 'RUNWARE_API_KEY', None)
|
||||
"""Load API keys from GlobalIntegrationSettings (platform-wide, used by ALL accounts)"""
|
||||
try:
|
||||
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
|
||||
|
||||
# Get global settings - single instance used by ALL accounts
|
||||
global_settings = GlobalIntegrationSettings.get_instance()
|
||||
|
||||
# Load API keys from global settings (platform-wide)
|
||||
self._openai_api_key = global_settings.openai_api_key
|
||||
self._runware_api_key = global_settings.runware_api_key
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Could not load GlobalIntegrationSettings: {e}", exc_info=True)
|
||||
self._openai_api_key = None
|
||||
self._runware_api_key = None
|
||||
|
||||
def get_api_key(self, integration_type: str = 'openai') -> Optional[str]:
|
||||
"""Get API key for integration type"""
|
||||
|
||||
@@ -197,12 +197,12 @@ class GenerateImagePromptsFunction(BaseAIFunction):
|
||||
prompt_text = str(prompt_data)
|
||||
caption_text = ''
|
||||
|
||||
heading = h2_headings[idx] if idx < len(h2_headings) else f"Section {idx + 1}"
|
||||
heading = h2_headings[idx] if idx < len(h2_headings) else f"Section {idx}"
|
||||
|
||||
Images.objects.update_or_create(
|
||||
content=content,
|
||||
image_type='in_article',
|
||||
position=idx + 1,
|
||||
position=idx, # 0-based position matching section array indices
|
||||
defaults={
|
||||
'prompt': prompt_text,
|
||||
'caption': caption_text,
|
||||
@@ -218,27 +218,33 @@ class GenerateImagePromptsFunction(BaseAIFunction):
|
||||
|
||||
# Helper methods
|
||||
def _get_max_in_article_images(self, account) -> int:
|
||||
"""Get max_in_article_images from AWS account IntegrationSettings only"""
|
||||
"""
|
||||
Get max_in_article_images from settings.
|
||||
Uses account's IntegrationSettings override, or GlobalIntegrationSettings.
|
||||
"""
|
||||
from igny8_core.modules.system.models import IntegrationSettings
|
||||
from igny8_core.auth.models import Account
|
||||
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
|
||||
|
||||
# Only use system account (aws-admin) settings
|
||||
system_account = Account.objects.get(slug='aws-admin')
|
||||
settings = IntegrationSettings.objects.get(
|
||||
account=system_account,
|
||||
integration_type='image_generation',
|
||||
is_active=True
|
||||
)
|
||||
max_images = settings.config.get('max_in_article_images')
|
||||
|
||||
if max_images is None:
|
||||
raise ValueError(
|
||||
"max_in_article_images not configured in aws-admin image_generation settings. "
|
||||
"Please set this value in the Integration Settings page."
|
||||
# Try account-specific override first
|
||||
try:
|
||||
settings = IntegrationSettings.objects.get(
|
||||
account=account,
|
||||
integration_type='image_generation',
|
||||
is_active=True
|
||||
)
|
||||
max_images = settings.config.get('max_in_article_images')
|
||||
|
||||
if max_images is not None:
|
||||
max_images = int(max_images)
|
||||
logger.info(f"Using max_in_article_images={max_images} from account {account.id} IntegrationSettings override")
|
||||
return max_images
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
logger.debug(f"No IntegrationSettings override for account {account.id}, using GlobalIntegrationSettings")
|
||||
|
||||
max_images = int(max_images)
|
||||
logger.info(f"Using max_in_article_images={max_images} from aws-admin account")
|
||||
# Use GlobalIntegrationSettings default
|
||||
global_settings = GlobalIntegrationSettings.get_instance()
|
||||
max_images = global_settings.max_in_article_images
|
||||
logger.info(f"Using max_in_article_images={max_images} from GlobalIntegrationSettings (account {account.id})")
|
||||
return max_images
|
||||
|
||||
def _extract_content_elements(self, content: Content, max_images: int) -> Dict:
|
||||
|
||||
@@ -67,32 +67,40 @@ class GenerateImagesFunction(BaseAIFunction):
|
||||
if not tasks:
|
||||
raise ValueError("No tasks found")
|
||||
|
||||
# Get image generation settings from aws-admin account only (global settings)
|
||||
# Get image generation settings
|
||||
# Try account-specific override, otherwise use GlobalIntegrationSettings
|
||||
from igny8_core.modules.system.models import IntegrationSettings
|
||||
from igny8_core.auth.models import Account
|
||||
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
|
||||
|
||||
system_account = Account.objects.get(slug='aws-admin')
|
||||
integration = IntegrationSettings.objects.get(
|
||||
account=system_account,
|
||||
integration_type='image_generation',
|
||||
is_active=True
|
||||
)
|
||||
image_settings = integration.config or {}
|
||||
image_settings = {}
|
||||
try:
|
||||
integration = IntegrationSettings.objects.get(
|
||||
account=account,
|
||||
integration_type='image_generation',
|
||||
is_active=True
|
||||
)
|
||||
image_settings = integration.config or {}
|
||||
logger.info(f"Using image settings from account {account.id} IntegrationSettings override")
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
logger.info(f"No IntegrationSettings override for account {account.id}, using GlobalIntegrationSettings")
|
||||
|
||||
# Extract settings with defaults
|
||||
provider = image_settings.get('provider') or image_settings.get('service', 'openai')
|
||||
# Use GlobalIntegrationSettings for missing values
|
||||
global_settings = GlobalIntegrationSettings.get_instance()
|
||||
|
||||
# Extract settings with defaults from global settings
|
||||
provider = image_settings.get('provider') or image_settings.get('service') or global_settings.default_image_service
|
||||
if provider == 'runware':
|
||||
model = image_settings.get('model') or image_settings.get('runwareModel', 'runware:97@1')
|
||||
model = image_settings.get('model') or image_settings.get('runwareModel') or global_settings.runware_model
|
||||
else:
|
||||
model = image_settings.get('model', 'dall-e-3')
|
||||
model = image_settings.get('model') or global_settings.dalle_model
|
||||
|
||||
return {
|
||||
'tasks': tasks,
|
||||
'account': account,
|
||||
'provider': provider,
|
||||
'model': model,
|
||||
'image_type': image_settings.get('image_type', 'realistic'),
|
||||
'max_in_article_images': int(image_settings.get('max_in_article_images')),
|
||||
'image_type': image_settings.get('image_type') or global_settings.image_style,
|
||||
'max_in_article_images': int(image_settings.get('max_in_article_images') or global_settings.max_in_article_images),
|
||||
'desktop_enabled': image_settings.get('desktop_enabled', True),
|
||||
'mobile_enabled': image_settings.get('mobile_enabled', True),
|
||||
}
|
||||
|
||||
@@ -181,41 +181,47 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
||||
failed = 0
|
||||
results = []
|
||||
|
||||
# Get image generation settings from IntegrationSettings
|
||||
# Always use system account settings (aws-admin) for global configuration
|
||||
logger.info("[process_image_generation_queue] Step 1: Loading image generation settings from aws-admin")
|
||||
from igny8_core.auth.models import Account
|
||||
# Get image generation settings
|
||||
# Try account-specific override, otherwise use GlobalIntegrationSettings
|
||||
logger.info("[process_image_generation_queue] Step 1: Loading image generation settings")
|
||||
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
|
||||
|
||||
config = {}
|
||||
try:
|
||||
system_account = Account.objects.get(slug='aws-admin')
|
||||
image_settings = IntegrationSettings.objects.get(
|
||||
account=system_account,
|
||||
account=account,
|
||||
integration_type='image_generation',
|
||||
is_active=True
|
||||
)
|
||||
logger.info(f"[process_image_generation_queue] Using system account (aws-admin) settings")
|
||||
logger.info(f"[process_image_generation_queue] Using account {account.id} IntegrationSettings override")
|
||||
config = image_settings.config or {}
|
||||
except (Account.DoesNotExist, IntegrationSettings.DoesNotExist):
|
||||
logger.error("[process_image_generation_queue] ERROR: Image generation settings not found in aws-admin account")
|
||||
return {'success': False, 'error': 'Image generation settings not found in aws-admin account'}
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
logger.info(f"[process_image_generation_queue] No IntegrationSettings override for account {account.id}, using GlobalIntegrationSettings")
|
||||
except Exception as e:
|
||||
logger.error(f"[process_image_generation_queue] ERROR loading image generation settings: {e}", exc_info=True)
|
||||
return {'success': False, 'error': f'Error loading image generation settings: {str(e)}'}
|
||||
|
||||
# Use GlobalIntegrationSettings for missing values
|
||||
global_settings = GlobalIntegrationSettings.get_instance()
|
||||
|
||||
logger.info(f"[process_image_generation_queue] Image generation settings loaded. Config keys: {list(config.keys())}")
|
||||
logger.info(f"[process_image_generation_queue] Full config: {config}")
|
||||
|
||||
# Get provider and model from config (respect user settings)
|
||||
provider = config.get('provider', 'openai')
|
||||
# Get model - try 'model' first, then 'imageModel' as fallback
|
||||
model = config.get('model') or config.get('imageModel') or 'dall-e-3'
|
||||
# Get provider and model from config with global fallbacks
|
||||
provider = config.get('provider') or global_settings.default_image_service
|
||||
if provider == 'runware':
|
||||
model = config.get('model') or config.get('imageModel') or global_settings.runware_model
|
||||
else:
|
||||
model = config.get('model') or config.get('imageModel') or global_settings.dalle_model
|
||||
|
||||
logger.info(f"[process_image_generation_queue] Using PROVIDER: {provider}, MODEL: {model} from settings")
|
||||
image_type = config.get('image_type', 'realistic')
|
||||
image_type = config.get('image_type') or global_settings.image_style
|
||||
image_format = config.get('image_format', 'webp')
|
||||
desktop_enabled = config.get('desktop_enabled', True)
|
||||
mobile_enabled = config.get('mobile_enabled', True)
|
||||
# Get image sizes from config, with fallback defaults
|
||||
featured_image_size = config.get('featured_image_size') or ('1280x832' if provider == 'runware' else '1024x1024')
|
||||
desktop_image_size = config.get('desktop_image_size') or '1024x1024'
|
||||
desktop_image_size = config.get('desktop_image_size') or global_settings.desktop_image_size
|
||||
in_article_image_size = config.get('in_article_image_size') or '512x512' # Default to 512x512
|
||||
|
||||
logger.info(f"[process_image_generation_queue] Settings loaded:")
|
||||
@@ -226,44 +232,22 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
||||
logger.info(f" - Desktop enabled: {desktop_enabled}")
|
||||
logger.info(f" - Mobile enabled: {mobile_enabled}")
|
||||
|
||||
# Get provider API key (using same approach as test image generation)
|
||||
# Note: API key is stored as 'apiKey' (camelCase) in IntegrationSettings.config
|
||||
# Normal users use system account settings (aws-admin) via fallback
|
||||
logger.info(f"[process_image_generation_queue] Step 2: Loading {provider.upper()} API key")
|
||||
try:
|
||||
provider_settings = IntegrationSettings.objects.get(
|
||||
account=account,
|
||||
integration_type=provider, # Use the provider from settings
|
||||
is_active=True
|
||||
)
|
||||
logger.info(f"[process_image_generation_queue] {provider.upper()} integration settings found for account {account.id}")
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
# Fallback to system account (aws-admin) settings
|
||||
logger.info(f"[process_image_generation_queue] No {provider.upper()} settings for account {account.id}, falling back to system account")
|
||||
from igny8_core.auth.models import Account
|
||||
try:
|
||||
system_account = Account.objects.get(slug='aws-admin')
|
||||
provider_settings = IntegrationSettings.objects.get(
|
||||
account=system_account,
|
||||
integration_type=provider,
|
||||
is_active=True
|
||||
)
|
||||
logger.info(f"[process_image_generation_queue] Using system account (aws-admin) {provider.upper()} settings")
|
||||
except (Account.DoesNotExist, IntegrationSettings.DoesNotExist):
|
||||
logger.error(f"[process_image_generation_queue] ERROR: {provider.upper()} integration settings not found in system account either")
|
||||
return {'success': False, 'error': f'{provider.upper()} integration not found or not active'}
|
||||
except Exception as e:
|
||||
logger.error(f"[process_image_generation_queue] ERROR getting {provider.upper()} API key: {e}", exc_info=True)
|
||||
return {'success': False, 'error': f'Error retrieving {provider.upper()} API key: {str(e)}'}
|
||||
# Get provider API key
|
||||
# API keys are ALWAYS from GlobalIntegrationSettings (accounts cannot override API keys)
|
||||
# Account IntegrationSettings only store provider preference, NOT API keys
|
||||
logger.info(f"[process_image_generation_queue] Step 2: Loading {provider.upper()} API key from GlobalIntegrationSettings")
|
||||
|
||||
# Extract API key from provider settings
|
||||
logger.info(f"[process_image_generation_queue] {provider.upper()} config keys: {list(provider_settings.config.keys()) if provider_settings.config else 'None'}")
|
||||
# Get API key from GlobalIntegrationSettings
|
||||
if provider == 'runware':
|
||||
api_key = global_settings.runware_api_key
|
||||
elif provider == 'openai':
|
||||
api_key = global_settings.dalle_api_key or global_settings.openai_api_key
|
||||
else:
|
||||
api_key = None
|
||||
|
||||
api_key = provider_settings.config.get('apiKey') if provider_settings.config else None
|
||||
if not api_key:
|
||||
logger.error(f"[process_image_generation_queue] {provider.upper()} API key not found in config")
|
||||
logger.error(f"[process_image_generation_queue] {provider.upper()} config: {provider_settings.config}")
|
||||
return {'success': False, 'error': f'{provider.upper()} API key not configured'}
|
||||
logger.error(f"[process_image_generation_queue] {provider.upper()} API key not configured in GlobalIntegrationSettings")
|
||||
return {'success': False, 'error': f'{provider.upper()} API key not configured in GlobalIntegrationSettings'}
|
||||
|
||||
# Log API key presence (but not the actual key for security)
|
||||
api_key_preview = f"{api_key[:10]}...{api_key[-4:]}" if len(api_key) > 14 else "***"
|
||||
|
||||
@@ -5,6 +5,7 @@ import time
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional, Callable
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from igny8_core.ai.constants import DEBUG_MODE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -195,24 +196,35 @@ class CostTracker:
|
||||
"""Tracks API costs and token usage"""
|
||||
|
||||
def __init__(self):
|
||||
self.total_cost = 0.0
|
||||
self.total_cost = Decimal('0.0')
|
||||
self.total_tokens = 0
|
||||
self.operations = []
|
||||
|
||||
def record(self, function_name: str, cost: float, tokens: int, model: str = None):
|
||||
"""Record an API call cost"""
|
||||
def record(self, function_name: str, cost, tokens: int, model: str = None):
|
||||
"""Record an API call cost
|
||||
|
||||
Args:
|
||||
function_name: Name of the AI function
|
||||
cost: Cost value (can be float or Decimal)
|
||||
tokens: Number of tokens used
|
||||
model: Model name
|
||||
"""
|
||||
# Convert cost to Decimal if it's a float to avoid type mixing
|
||||
if not isinstance(cost, Decimal):
|
||||
cost = Decimal(str(cost))
|
||||
|
||||
self.total_cost += cost
|
||||
self.total_tokens += tokens
|
||||
self.operations.append({
|
||||
'function': function_name,
|
||||
'cost': cost,
|
||||
'cost': float(cost), # Store as float for JSON serialization
|
||||
'tokens': tokens,
|
||||
'model': model
|
||||
})
|
||||
|
||||
def get_total(self) -> float:
|
||||
"""Get total cost"""
|
||||
return self.total_cost
|
||||
def get_total(self):
|
||||
"""Get total cost (returns float for JSON serialization)"""
|
||||
return float(self.total_cost)
|
||||
|
||||
def get_total_tokens(self) -> int:
|
||||
"""Get total tokens"""
|
||||
|
||||
@@ -135,7 +135,7 @@ def validate_api_key(api_key: Optional[str], integration_type: str = 'openai') -
|
||||
|
||||
def validate_model(model: str, model_type: str = 'text') -> Dict[str, Any]:
|
||||
"""
|
||||
Validate that model is in supported list.
|
||||
Validate that model is in supported list using database.
|
||||
|
||||
Args:
|
||||
model: Model name to validate
|
||||
@@ -144,27 +144,59 @@ def validate_model(model: str, model_type: str = 'text') -> Dict[str, Any]:
|
||||
Returns:
|
||||
Dict with 'valid' (bool) and optional 'error' (str)
|
||||
"""
|
||||
from .constants import MODEL_RATES, VALID_OPENAI_IMAGE_MODELS
|
||||
|
||||
if model_type == 'text':
|
||||
if model not in MODEL_RATES:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Model "{model}" is not in supported models list'
|
||||
}
|
||||
elif model_type == 'image':
|
||||
if model not in VALID_OPENAI_IMAGE_MODELS:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Model "{model}" is not valid for OpenAI image generation. Only {", ".join(VALID_OPENAI_IMAGE_MODELS)} are supported.'
|
||||
}
|
||||
|
||||
return {'valid': True}
|
||||
try:
|
||||
# Try database first
|
||||
from igny8_core.business.billing.models import AIModelConfig
|
||||
|
||||
exists = AIModelConfig.objects.filter(
|
||||
model_name=model,
|
||||
model_type=model_type,
|
||||
is_active=True
|
||||
).exists()
|
||||
|
||||
if not exists:
|
||||
# Get available models for better error message
|
||||
available = list(AIModelConfig.objects.filter(
|
||||
model_type=model_type,
|
||||
is_active=True
|
||||
).values_list('model_name', flat=True))
|
||||
|
||||
if available:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Model "{model}" is not active or not found. Available {model_type} models: {", ".join(available)}'
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Model "{model}" is not found in database'
|
||||
}
|
||||
|
||||
return {'valid': True}
|
||||
|
||||
except Exception:
|
||||
# Fallback to constants if database fails
|
||||
from .constants import MODEL_RATES, VALID_OPENAI_IMAGE_MODELS
|
||||
|
||||
if model_type == 'text':
|
||||
if model not in MODEL_RATES:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Model "{model}" is not in supported models list'
|
||||
}
|
||||
elif model_type == 'image':
|
||||
if model not in VALID_OPENAI_IMAGE_MODELS:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Model "{model}" is not valid for OpenAI image generation. Only {", ".join(VALID_OPENAI_IMAGE_MODELS)} are supported.'
|
||||
}
|
||||
|
||||
return {'valid': True}
|
||||
|
||||
|
||||
def validate_image_size(size: str, model: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate that image size is valid for the selected model.
|
||||
Validate that image size is valid for the selected model using database.
|
||||
|
||||
Args:
|
||||
size: Image size (e.g., '1024x1024')
|
||||
@@ -173,14 +205,40 @@ def validate_image_size(size: str, model: str) -> Dict[str, Any]:
|
||||
Returns:
|
||||
Dict with 'valid' (bool) and optional 'error' (str)
|
||||
"""
|
||||
from .constants import VALID_SIZES_BY_MODEL
|
||||
|
||||
valid_sizes = VALID_SIZES_BY_MODEL.get(model, [])
|
||||
if size not in valid_sizes:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Image size "{size}" is not valid for model "{model}". Valid sizes are: {", ".join(valid_sizes)}'
|
||||
}
|
||||
|
||||
return {'valid': True}
|
||||
try:
|
||||
# Try database first
|
||||
from igny8_core.business.billing.models import AIModelConfig
|
||||
|
||||
model_config = AIModelConfig.objects.filter(
|
||||
model_name=model,
|
||||
model_type='image',
|
||||
is_active=True
|
||||
).first()
|
||||
|
||||
if model_config:
|
||||
if not model_config.validate_size(size):
|
||||
valid_sizes = model_config.valid_sizes or []
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Image size "{size}" is not valid for model "{model}". Valid sizes are: {", ".join(valid_sizes)}'
|
||||
}
|
||||
return {'valid': True}
|
||||
else:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Image model "{model}" not found in database'
|
||||
}
|
||||
|
||||
except Exception:
|
||||
# Fallback to constants if database fails
|
||||
from .constants import VALID_SIZES_BY_MODEL
|
||||
|
||||
valid_sizes = VALID_SIZES_BY_MODEL.get(model, [])
|
||||
if size not in valid_sizes:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Image size "{size}" is not valid for model "{model}". Valid sizes are: {", ".join(valid_sizes)}'
|
||||
}
|
||||
|
||||
return {'valid': True}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user