globals
This commit is contained in:
@@ -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")
|
||||
}),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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})'),
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user