This commit is contained in:
IGNY8 VPS (Salman)
2025-12-20 19:49:57 +00:00
parent 3283a83b42
commit 9e8ff4fbb1
18 changed files with 797 additions and 828 deletions

View File

@@ -343,18 +343,22 @@ class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
"fields": ("openai_api_key", "openai_model", "openai_temperature", "openai_max_tokens"),
"description": "Global OpenAI configuration used by all accounts (unless overridden)"
}),
("DALL-E Settings", {
"fields": ("dalle_api_key", "dalle_model", "dalle_size", "dalle_quality", "dalle_style"),
"description": "Global DALL-E image generation configuration"
("Image Generation - Default Service", {
"fields": ("default_image_service",),
"description": "Choose which image generation service is used by default for all accounts"
}),
("Anthropic Settings", {
"fields": ("anthropic_api_key", "anthropic_model"),
"description": "Global Anthropic Claude configuration"
("Image Generation - DALL-E", {
"fields": ("dalle_api_key", "dalle_model", "dalle_size"),
"description": "Global DALL-E (OpenAI) image generation configuration"
}),
("Runware Settings", {
"fields": ("runware_api_key",),
("Image Generation - Runware", {
"fields": ("runware_api_key", "runware_model"),
"description": "Global Runware image generation configuration"
}),
("Universal Image Settings", {
"fields": ("image_quality", "image_style", "max_in_article_images", "desktop_image_size", "mobile_image_size"),
"description": "Image quality, style, and sizing settings that apply to ALL providers (DALL-E, Runware, etc.)"
}),
("Status", {
"fields": ("is_active", "last_updated", "updated_by")
}),

View File

@@ -20,6 +20,61 @@ class GlobalIntegrationSettings(models.Model):
- Starter/Growth/Scale: Can override model, temperature, tokens, etc.
"""
OPENAI_MODEL_CHOICES = [
('gpt-4.1', 'GPT-4.1 - $2.00 / $8.00 per 1M tokens'),
('gpt-4o-mini', 'GPT-4o mini - $0.15 / $0.60 per 1M tokens'),
('gpt-4o', 'GPT-4o - $2.50 / $10.00 per 1M tokens'),
('gpt-4-turbo-preview', 'GPT-4 Turbo Preview - $10.00 / $30.00 per 1M tokens'),
('gpt-5.1', 'GPT-5.1 - $1.25 / $10.00 per 1M tokens (16K)'),
('gpt-5.2', 'GPT-5.2 - $1.75 / $14.00 per 1M tokens (16K)'),
]
DALLE_MODEL_CHOICES = [
('dall-e-3', 'DALL·E 3 - $0.040 per image'),
('dall-e-2', 'DALL·E 2 - $0.020 per image'),
]
DALLE_SIZE_CHOICES = [
('1024x1024', '1024x1024 (Square)'),
('1792x1024', '1792x1024 (Landscape)'),
('1024x1792', '1024x1792 (Portrait)'),
('512x512', '512x512 (Small Square)'),
]
DALLE_QUALITY_CHOICES = [
('standard', 'Standard'),
('hd', 'HD'),
]
DALLE_STYLE_CHOICES = [
('vivid', 'Vivid'),
('natural', 'Natural'),
]
RUNWARE_MODEL_CHOICES = [
('runware:97@1', 'Runware 97@1 - Versatile Model'),
('runware:100@1', 'Runware 100@1 - High Quality'),
('runware:101@1', 'Runware 101@1 - Fast Generation'),
]
IMAGE_QUALITY_CHOICES = [
('standard', 'Standard'),
('hd', 'HD'),
]
IMAGE_STYLE_CHOICES = [
('vivid', 'Vivid'),
('natural', 'Natural'),
('realistic', 'Realistic'),
('artistic', 'Artistic'),
('cartoon', 'Cartoon'),
]
IMAGE_SERVICE_CHOICES = [
('openai', 'OpenAI DALL-E'),
('runware', 'Runware'),
]
# OpenAI Settings (for text generation)
openai_api_key = models.CharField(
max_length=500,
@@ -28,7 +83,8 @@ class GlobalIntegrationSettings(models.Model):
)
openai_model = models.CharField(
max_length=100,
default='gpt-4-turbo-preview',
default='gpt-4o-mini',
choices=OPENAI_MODEL_CHOICES,
help_text="Default text generation model (accounts can override if plan allows)"
)
openai_temperature = models.FloatField(
@@ -40,7 +96,7 @@ class GlobalIntegrationSettings(models.Model):
help_text="Default max tokens for responses (accounts can override if plan allows)"
)
# DALL-E Settings (for image generation)
# Image Generation Settings (OpenAI/DALL-E)
dalle_api_key = models.CharField(
max_length=500,
blank=True,
@@ -49,44 +105,64 @@ class GlobalIntegrationSettings(models.Model):
dalle_model = models.CharField(
max_length=100,
default='dall-e-3',
choices=DALLE_MODEL_CHOICES,
help_text="Default DALL-E model (accounts can override if plan allows)"
)
dalle_size = models.CharField(
max_length=20,
default='1024x1024',
choices=DALLE_SIZE_CHOICES,
help_text="Default image size (accounts can override if plan allows)"
)
dalle_quality = models.CharField(
max_length=20,
default='standard',
choices=[('standard', 'Standard'), ('hd', 'HD')],
help_text="Default image quality (accounts can override if plan allows)"
)
dalle_style = models.CharField(
max_length=20,
default='vivid',
choices=[('vivid', 'Vivid'), ('natural', 'Natural')],
help_text="Default image style (accounts can override if plan allows)"
)
# Anthropic Settings (for Claude)
anthropic_api_key = models.CharField(
max_length=500,
blank=True,
help_text="Platform Anthropic API key - used by ALL accounts"
)
anthropic_model = models.CharField(
max_length=100,
default='claude-3-sonnet-20240229',
help_text="Default Anthropic model (accounts can override if plan allows)"
)
# Runware Settings (alternative image generation)
# Image Generation Settings (Runware)
runware_api_key = models.CharField(
max_length=500,
blank=True,
help_text="Platform Runware API key - used by ALL accounts"
)
runware_model = models.CharField(
max_length=100,
default='runware:97@1',
choices=RUNWARE_MODEL_CHOICES,
help_text="Default Runware model (accounts can override if plan allows)"
)
# Default Image Generation Service
default_image_service = models.CharField(
max_length=20,
default='openai',
choices=IMAGE_SERVICE_CHOICES,
help_text="Default image generation service for all accounts (openai=DALL-E, runware=Runware)"
)
# Universal Image Generation Settings (applies to ALL providers)
image_quality = models.CharField(
max_length=20,
default='standard',
choices=IMAGE_QUALITY_CHOICES,
help_text="Default image quality for all providers (accounts can override if plan allows)"
)
image_style = models.CharField(
max_length=20,
default='realistic',
choices=IMAGE_STYLE_CHOICES,
help_text="Default image style for all providers (accounts can override if plan allows)"
)
max_in_article_images = models.IntegerField(
default=2,
help_text="Default maximum images to generate per article (1-5, accounts can override if plan allows)"
)
desktop_image_size = models.CharField(
max_length=20,
default='1024x1024',
help_text="Default desktop image size (accounts can override if plan allows)"
)
mobile_image_size = models.CharField(
max_length=20,
default='512x512',
help_text="Default mobile image size (accounts can override if plan allows)"
)
# Metadata
is_active = models.BooleanField(default=True)
@@ -151,7 +227,8 @@ class GlobalAIPrompt(models.Model):
description = models.TextField(blank=True, help_text="Description of what this prompt does")
variables = models.JSONField(
default=list,
help_text="List of variables used in the prompt (e.g., {keyword}, {industry})"
blank=True,
help_text="Optional: List of variables used in the prompt (e.g., {keyword}, {industry})"
)
is_active = models.BooleanField(default=True, db_index=True)
version = models.IntegerField(default=1, help_text="Prompt version for tracking changes")

View File

@@ -94,14 +94,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner])
def test_connection(self, request, pk=None):
"""
Test API connection for OpenAI or Runware
Supports two modes:
- with_response=false: Simple connection test (GET /v1/models)
- with_response=true: Full response test with ping message
Test API connection using platform API keys.
Tests OpenAI or Runware with current model selection.
"""
integration_type = pk # 'openai', 'runware'
logger.info(f"[test_connection] Called for integration_type={integration_type}, user={getattr(request, 'user', None)}, account={getattr(request, 'account', None)}")
logger.info(f"[test_connection] Called for integration_type={integration_type}")
if not integration_type:
return error_response(
@@ -110,70 +108,43 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
request=request
)
# Get API key and config from request or saved settings
config = request.data.get('config', {}) if isinstance(request.data.get('config'), dict) else {}
api_key = request.data.get('apiKey') or config.get('apiKey')
# Merge request.data with config if config is a dict
if not isinstance(config, dict):
config = {}
if not api_key:
# Try to get from saved settings
account = getattr(request, 'account', None)
logger.info(f"[test_connection] Account from request: {account.id if account else None}")
# Fallback to user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
# Fallback to default account
if not account:
from igny8_core.auth.models import Account
try:
account = Account.objects.first()
except Exception:
pass
if account:
try:
from .models import IntegrationSettings
logger.info(f"[test_connection] Looking for saved settings for account {account.id}")
saved_settings = IntegrationSettings.objects.get(
integration_type=integration_type,
account=account
)
api_key = saved_settings.config.get('apiKey')
logger.info(f"[test_connection] Found saved settings, has_apiKey={bool(api_key)}")
except IntegrationSettings.DoesNotExist:
logger.warning(f"[test_connection] No saved settings found for {integration_type} and account {account.id}")
pass
if not api_key:
logger.error(f"[test_connection] No API key found in request or saved settings")
return error_response(
error='API key is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
logger.info(f"[test_connection] Testing {integration_type} connection with API key (length={len(api_key) if api_key else 0})")
try:
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
# Get platform API keys
global_settings = GlobalIntegrationSettings.get_instance()
# Get config from request (model selection)
config = request.data.get('config', {}) if isinstance(request.data.get('config'), dict) else {}
if integration_type == 'openai':
api_key = global_settings.openai_api_key
if not api_key:
return error_response(
error='Platform OpenAI API key not configured. Please contact administrator.',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
return self._test_openai(api_key, config, request)
elif integration_type == 'runware':
api_key = global_settings.runware_api_key
if not api_key:
return error_response(
error='Platform Runware API key not configured. Please contact administrator.',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
return self._test_runware(api_key, request)
else:
return error_response(
error=f'Validation not supported for {integration_type}',
error=f'Testing not supported for {integration_type}',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
except Exception as e:
logger.error(f"Error testing {integration_type} connection: {str(e)}", exc_info=True)
import traceback
error_trace = traceback.format_exc()
logger.error(f"Full traceback: {error_trace}")
return error_response(
error=str(e),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -662,8 +633,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
return self.save_settings(request, integration_type)
def save_settings(self, request, pk=None):
"""Save integration settings"""
integration_type = pk # 'openai', 'runware', 'gsc'
"""
Save integration settings (account overrides only).
- Saves model/parameter overrides to IntegrationSettings
- NEVER saves API keys (those are platform-wide)
- Free plan: Should be blocked at frontend level
"""
integration_type = pk
logger.info(f"[save_settings] Called for integration_type={integration_type}, user={getattr(request, 'user', None)}, account={getattr(request, 'account', None)}")
@@ -678,12 +654,19 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
config = dict(request.data) if hasattr(request.data, 'dict') else (request.data if isinstance(request.data, dict) else {})
logger.info(f"[save_settings] Config keys: {list(config.keys()) if isinstance(config, dict) else 'Not a dict'}")
# Remove any API keys from config (security - they shouldn't be sent but just in case)
config.pop('apiKey', None)
config.pop('api_key', None)
config.pop('openai_api_key', None)
config.pop('dalle_api_key', None)
config.pop('runware_api_key', None)
config.pop('anthropic_api_key', None)
try:
# Get account - try multiple methods
# Get account
account = getattr(request, 'account', None)
logger.info(f"[save_settings] Account from request: {account.id if account else None}")
# Fallback 1: Get from authenticated user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
@@ -693,93 +676,81 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
logger.warning(f"Error getting account from user: {e}")
account = None
# Fallback 2: If still no account, get default account (for development)
if not account:
from igny8_core.auth.models import Account
try:
# Get the first account as fallback (development only)
account = Account.objects.first()
except Exception as e:
logger.warning(f"Error getting default account: {e}")
account = None
if not account:
logger.error(f"[save_settings] No account found after all fallbacks")
logger.error(f"[save_settings] No account found")
return error_response(
error='Account not found. Please ensure you are logged in.',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
logger.info(f"[save_settings] Using account: {account.id} ({account.name}, slug={account.slug}, status={account.status})")
logger.info(f"[save_settings] Using account: {account.id} ({account.name})")
# Store integration settings in a simple model or settings table
# For now, we'll use a simple approach - store in IntegrationSettings model
# or use Django settings/database
# TODO: Check if Free plan - they shouldn't be able to save overrides
# This should be blocked at frontend level, but add backend check too
# Import IntegrationSettings model
from .models import IntegrationSettings
# For image_generation, ensure provider is set correctly
if integration_type == 'image_generation':
# Build clean config with only allowed overrides
clean_config = {}
if integration_type == 'openai':
# Only allow model, temperature, max_tokens overrides
if 'model' in config:
clean_config['model'] = config['model']
if 'temperature' in config:
clean_config['temperature'] = config['temperature']
if 'max_tokens' in config:
clean_config['max_tokens'] = config['max_tokens']
elif integration_type == 'image_generation':
# Map service to provider if service is provided
if 'service' in config and 'provider' not in config:
config['provider'] = config['service']
# Ensure provider is set
if 'provider' not in config:
config['provider'] = config.get('service', 'openai')
# Set model based on provider
if config.get('provider') == 'openai' and 'model' not in config:
config['model'] = config.get('imageModel', 'dall-e-3')
elif config.get('provider') == 'runware' and 'model' not in config:
config['model'] = config.get('runwareModel', 'runware:97@1')
# Ensure all image settings have defaults (except max_in_article_images which must be explicitly set)
config.setdefault('image_type', 'realistic')
config.setdefault('image_format', 'webp')
config.setdefault('desktop_enabled', True)
config.setdefault('mobile_enabled', True)
# Set default image sizes based on provider/model
provider = config.get('provider', 'openai')
model = config.get('model', 'dall-e-3')
if not config.get('featured_image_size'):
if provider == 'runware':
config['featured_image_size'] = '1280x832'
else: # openai
config['featured_image_size'] = '1024x1024'
if not config.get('desktop_image_size'):
config['desktop_image_size'] = '1024x1024'
if 'service' in config:
clean_config['service'] = config['service']
clean_config['provider'] = config['service']
if 'provider' in config:
clean_config['provider'] = config['provider']
clean_config['service'] = config['provider']
# Model selection (service-specific)
if 'model' in config:
clean_config['model'] = config['model']
if 'imageModel' in config:
clean_config['imageModel'] = config['imageModel']
clean_config['model'] = config['imageModel'] # Also store in 'model' for consistency
if 'runwareModel' in config:
clean_config['runwareModel'] = config['runwareModel']
# Universal image settings (applies to all providers)
for key in ['image_type', 'image_quality', 'image_style', 'max_in_article_images', 'image_format',
'desktop_enabled', 'mobile_enabled', 'featured_image_size', 'desktop_image_size']:
if key in config:
clean_config[key] = config[key]
# Get or create integration settings
logger.info(f"[save_settings] Attempting get_or_create for {integration_type} with account {account.id}")
logger.info(f"[save_settings] Saving clean config: {clean_config}")
integration_settings, created = IntegrationSettings.objects.get_or_create(
integration_type=integration_type,
account=account,
defaults={'config': config, 'is_active': config.get('enabled', False)}
defaults={'config': clean_config, 'is_active': True}
)
logger.info(f"[save_settings] get_or_create result: created={created}, id={integration_settings.id}")
logger.info(f"[save_settings] Result: created={created}, id={integration_settings.id}")
if not created:
logger.info(f"[save_settings] Updating existing settings (id={integration_settings.id})")
integration_settings.config = config
integration_settings.is_active = config.get('enabled', False)
integration_settings.config = clean_config
integration_settings.is_active = True
integration_settings.save()
logger.info(f"[save_settings] Settings updated successfully")
logger.info(f"[save_settings] Updated existing settings")
logger.info(f"[save_settings] Successfully saved settings for {integration_type}")
logger.info(f"[save_settings] Successfully saved overrides for {integration_type}")
return success_response(
data={'config': config},
data={'config': clean_config},
message=f'{integration_type.upper()} settings saved successfully',
request=request
)
except Exception as e:
logger.error(f"Error saving integration settings for {integration_type}: {str(e)}", exc_info=True)
import traceback
error_trace = traceback.format_exc()
logger.error(f"Full traceback: {error_trace}")
return error_response(
error=f'Failed to save settings: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -787,7 +758,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
)
def get_settings(self, request, pk=None):
"""Get integration settings - defaults to AWS-admin settings if account doesn't have its own"""
"""
Get integration settings for frontend.
Returns:
- Global defaults (model, temperature, etc.)
- Account overrides if they exist
- NO API keys (platform-wide only)
"""
integration_type = pk
if not integration_type:
@@ -798,10 +775,9 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
)
try:
# Get account - try multiple methods (same as save_settings)
# Get account
account = getattr(request, 'account', None)
# Fallback 1: Get from authenticated user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
@@ -812,31 +788,116 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
account = None
from .models import IntegrationSettings
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
# Get account-specific settings
if account:
try:
integration_settings = IntegrationSettings.objects.get(
integration_type=integration_type,
account=account
)
response_data = {
'id': integration_settings.integration_type,
'enabled': integration_settings.is_active,
**integration_settings.config
}
return success_response(
data=response_data,
request=request
)
except IntegrationSettings.DoesNotExist:
pass
except Exception as e:
logger.error(f"Error getting account-specific settings: {e}", exc_info=True)
# Get global defaults
global_settings = GlobalIntegrationSettings.get_instance()
# Build response with global defaults
if integration_type == 'openai':
response_data = {
'id': 'openai',
'enabled': True, # Always enabled (platform-wide)
'model': global_settings.openai_model,
'temperature': global_settings.openai_temperature,
'max_tokens': global_settings.openai_max_tokens,
'using_global': True, # Flag to show it's using global
}
# Check for account overrides
if account:
try:
integration_settings = IntegrationSettings.objects.get(
integration_type=integration_type,
account=account,
is_active=True
)
config = integration_settings.config or {}
if config.get('model'):
response_data['model'] = config['model']
response_data['using_global'] = False
if config.get('temperature') is not None:
response_data['temperature'] = config['temperature']
if config.get('max_tokens'):
response_data['max_tokens'] = config['max_tokens']
except IntegrationSettings.DoesNotExist:
pass
elif integration_type == 'runware':
response_data = {
'id': 'runware',
'enabled': True, # Always enabled (platform-wide)
'using_global': True,
}
elif integration_type == 'image_generation':
# Get default service and model based on global settings
default_service = global_settings.default_image_service
default_model = global_settings.dalle_model if default_service == 'openai' else global_settings.runware_model
response_data = {
'id': 'image_generation',
'enabled': True,
'service': default_service, # From global settings
'provider': default_service, # Alias for service
'model': default_model, # Service-specific default model
'imageModel': global_settings.dalle_model, # OpenAI model
'runwareModel': global_settings.runware_model, # Runware model
'image_type': global_settings.image_style, # Use image_style as default
'image_quality': global_settings.image_quality, # Universal quality
'image_style': global_settings.image_style, # Universal style
'max_in_article_images': global_settings.max_in_article_images,
'image_format': 'webp',
'desktop_enabled': True,
'mobile_enabled': True,
'featured_image_size': global_settings.dalle_size,
'desktop_image_size': global_settings.desktop_image_size,
'mobile_image_size': global_settings.mobile_image_size,
'using_global': True,
}
# Check for account overrides
if account:
try:
integration_settings = IntegrationSettings.objects.get(
integration_type=integration_type,
account=account,
is_active=True
)
config = integration_settings.config or {}
# Override with account settings
if config:
response_data['using_global'] = False
# Service/provider
if 'service' in config:
response_data['service'] = config['service']
response_data['provider'] = config['service']
if 'provider' in config:
response_data['provider'] = config['provider']
response_data['service'] = config['provider']
# Models
if 'model' in config:
response_data['model'] = config['model']
if 'imageModel' in config:
response_data['imageModel'] = config['imageModel']
if 'runwareModel' in config:
response_data['runwareModel'] = config['runwareModel']
# Universal image settings
for key in ['image_type', 'image_quality', 'image_style', 'max_in_article_images', 'image_format',
'desktop_enabled', 'mobile_enabled', 'featured_image_size', 'desktop_image_size']:
if key in config:
response_data[key] = config[key]
except IntegrationSettings.DoesNotExist:
pass
else:
# Other integration types - return empty
response_data = {
'id': integration_type,
'enabled': False,
}
# Return empty config if no settings found
return success_response(
data={},
data=response_data,
request=request
)
except Exception as e:

View File

@@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0003_fix_global_settings_architecture'),
('system', '0002_add_global_settings_models'),
]
operations = [

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.9 on 2025-12-20 14:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0004_fix_global_settings_remove_override'),
]
operations = [
migrations.AlterField(
model_name='globalintegrationsettings',
name='anthropic_model',
field=models.CharField(choices=[('claude-3-5-sonnet-20241022', 'Claude 3.5 Sonnet (Oct 2024) - $3.00 / $15.00 per 1M tokens'), ('claude-3-5-sonnet-20240620', 'Claude 3.5 Sonnet (Jun 2024) - $3.00 / $15.00 per 1M tokens'), ('claude-3-opus-20240229', 'Claude 3 Opus - $15.00 / $75.00 per 1M tokens'), ('claude-3-sonnet-20240229', 'Claude 3 Sonnet - $3.00 / $15.00 per 1M tokens'), ('claude-3-haiku-20240307', 'Claude 3 Haiku - $0.25 / $1.25 per 1M tokens')], default='claude-3-5-sonnet-20241022', help_text='Default Anthropic model (accounts can override if plan allows)', max_length=100),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='dalle_model',
field=models.CharField(choices=[('dall-e-3', 'DALL·E 3 - $0.040 per image'), ('dall-e-2', 'DALL·E 2 - $0.020 per image')], default='dall-e-3', help_text='Default DALL-E model (accounts can override if plan allows)', max_length=100),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='dalle_size',
field=models.CharField(choices=[('1024x1024', '1024x1024 (Square)'), ('1792x1024', '1792x1024 (Landscape)'), ('1024x1792', '1024x1792 (Portrait)'), ('512x512', '512x512 (Small Square)')], default='1024x1024', help_text='Default image size (accounts can override if plan allows)', max_length=20),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='openai_model',
field=models.CharField(choices=[('gpt-4.1', 'GPT-4.1 - $2.00 / $8.00 per 1M tokens'), ('gpt-4o-mini', 'GPT-4o mini - $0.15 / $0.60 per 1M tokens'), ('gpt-4o', 'GPT-4o - $2.50 / $10.00 per 1M tokens'), ('gpt-4-turbo-preview', 'GPT-4 Turbo Preview - $10.00 / $30.00 per 1M tokens'), ('gpt-5.1', 'GPT-5.1 - $1.25 / $10.00 per 1M tokens (16K)'), ('gpt-5.2', 'GPT-5.2 - $1.75 / $14.00 per 1M tokens (16K)')], default='gpt-4o-mini', help_text='Default text generation model (accounts can override if plan allows)', max_length=100),
),
]

View File

@@ -0,0 +1,44 @@
# Generated by Django 5.2.9 on 2025-12-20 14:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0005_add_model_choices'),
]
operations = [
migrations.RemoveField(
model_name='globalintegrationsettings',
name='anthropic_api_key',
),
migrations.RemoveField(
model_name='globalintegrationsettings',
name='anthropic_model',
),
migrations.RemoveField(
model_name='globalintegrationsettings',
name='dalle_quality',
),
migrations.RemoveField(
model_name='globalintegrationsettings',
name='dalle_style',
),
migrations.AddField(
model_name='globalintegrationsettings',
name='image_quality',
field=models.CharField(choices=[('standard', 'Standard'), ('hd', 'HD')], default='standard', help_text='Default image quality for all providers (accounts can override if plan allows)', max_length=20),
),
migrations.AddField(
model_name='globalintegrationsettings',
name='image_style',
field=models.CharField(choices=[('vivid', 'Vivid'), ('natural', 'Natural'), ('realistic', 'Realistic'), ('artistic', 'Artistic'), ('cartoon', 'Cartoon')], default='realistic', help_text='Default image style for all providers (accounts can override if plan allows)', max_length=20),
),
migrations.AddField(
model_name='globalintegrationsettings',
name='runware_model',
field=models.CharField(choices=[('runware:97@1', 'Runware 97@1 - Versatile Model'), ('runware:100@1', 'Runware 100@1 - High Quality'), ('runware:101@1', 'Runware 101@1 - Fast Generation')], default='runware:97@1', help_text='Default Runware model (accounts can override if plan allows)', max_length=100),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.9 on 2025-12-20 15:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0006_fix_image_settings'),
]
operations = [
migrations.AddField(
model_name='globalintegrationsettings',
name='desktop_image_size',
field=models.CharField(default='1024x1024', help_text='Default desktop image size (accounts can override if plan allows)', max_length=20),
),
migrations.AddField(
model_name='globalintegrationsettings',
name='max_in_article_images',
field=models.IntegerField(default=2, help_text='Default maximum images to generate per article (1-5, accounts can override if plan allows)'),
),
migrations.AddField(
model_name='globalintegrationsettings',
name='mobile_image_size',
field=models.CharField(default='512x512', help_text='Default mobile image size (accounts can override if plan allows)', max_length=20),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.9 on 2025-12-20 15:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0007_add_image_defaults'),
]
operations = [
migrations.AddField(
model_name='globalintegrationsettings',
name='default_image_service',
field=models.CharField(choices=[('openai', 'OpenAI DALL-E'), ('runware', 'Runware')], default='openai', help_text='Default image generation service for all accounts (openai=DALL-E, runware=Runware)', max_length=20),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.9 on 2025-12-20 19:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0008_add_default_image_service'),
]
operations = [
migrations.AlterField(
model_name='globalaiprompt',
name='variables',
field=models.JSONField(blank=True, default=list, help_text='Optional: List of variables used in the prompt (e.g., {keyword}, {industry})'),
),
]

View File

@@ -84,11 +84,23 @@ class AIPrompt(AccountBaseModel):
return None
def reset_to_default(self):
"""Reset prompt to global default"""
if self.default_prompt:
self.prompt_value = self.default_prompt
"""Reset prompt to global default from GlobalAIPrompt"""
from .global_settings_models import GlobalAIPrompt
try:
global_prompt = GlobalAIPrompt.objects.get(
prompt_type=self.prompt_type,
is_active=True
)
self.prompt_value = global_prompt.prompt_value
self.default_prompt = global_prompt.prompt_value
self.is_customized = False
self.save()
except GlobalAIPrompt.DoesNotExist:
raise ValueError(
f"Cannot reset: Global prompt '{self.prompt_type}' not found. "
f"Please configure it in Django admin at: /admin/system/globalaiprompt/"
)
def __str__(self):
status = "Custom" if self.is_customized else "Default"

View File

@@ -5,8 +5,14 @@ from typing import Optional
def get_default_prompt(prompt_type: str) -> str:
"""Get default prompt value by type"""
defaults = {
"""Get default prompt value from GlobalAIPrompt ONLY - single source of truth"""
from .global_settings_models import GlobalAIPrompt
try:
global_prompt = GlobalAIPrompt.objects.get(prompt_type=prompt_type, is_active=True)
return global_prompt.prompt_value
except GlobalAIPrompt.DoesNotExist:
return f"ERROR: Global prompt '{prompt_type}' not configured in admin. Please configure it at: admin/system/globalaiprompt/"
'clustering': """You are a semantic strategist and SEO architecture engine. Your task is to analyze the provided keyword list and group them into meaningful, intent-driven topic clusters that reflect how real users search, think, and act online.
Return a single JSON object with a "clusters" array. Each cluster must follow this structure: