lot of messs
This commit is contained in:
@@ -159,7 +159,6 @@ class Igny8AdminSite(UnfoldAdminSite):
|
||||
},
|
||||
'Credits': {
|
||||
'models': [
|
||||
('billing', 'AIModelConfig'),
|
||||
('billing', 'CreditTransaction'),
|
||||
('billing', 'CreditUsageLog'),
|
||||
('billing', 'CreditCostConfig'),
|
||||
@@ -199,8 +198,14 @@ class Igny8AdminSite(UnfoldAdminSite):
|
||||
},
|
||||
'AI & Automation': {
|
||||
'models': [
|
||||
('billing', 'AIModelConfig'),
|
||||
('ai', 'IntegrationState'),
|
||||
('system', 'IntegrationSettings'),
|
||||
('system', 'GlobalModuleSettings'),
|
||||
('system', 'GlobalIntegrationSettings'),
|
||||
('system', 'GlobalAIPrompt'),
|
||||
('system', 'GlobalAuthorProfile'),
|
||||
('system', 'GlobalStrategy'),
|
||||
('system', 'AIPrompt'),
|
||||
('system', 'Strategy'),
|
||||
('system', 'AuthorProfile'),
|
||||
|
||||
@@ -4,13 +4,51 @@ Admin configuration for AI models
|
||||
from django.contrib import admin
|
||||
from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import Igny8ModelAdmin
|
||||
from igny8_core.ai.models import AITaskLog
|
||||
from igny8_core.ai.models import AITaskLog, IntegrationState
|
||||
|
||||
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
@admin.register(IntegrationState)
|
||||
class IntegrationStateAdmin(Igny8ModelAdmin):
|
||||
"""Admin interface for Integration States"""
|
||||
list_display = [
|
||||
'account',
|
||||
'is_openai_enabled',
|
||||
'is_runware_enabled',
|
||||
'is_image_generation_enabled',
|
||||
'updated_at',
|
||||
]
|
||||
list_filter = [
|
||||
'is_openai_enabled',
|
||||
'is_runware_enabled',
|
||||
'is_image_generation_enabled',
|
||||
]
|
||||
search_fields = [
|
||||
'account__name',
|
||||
'account__domain',
|
||||
]
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
fieldsets = (
|
||||
('Account', {
|
||||
'fields': ('account',)
|
||||
}),
|
||||
('Integration States', {
|
||||
'fields': (
|
||||
'is_openai_enabled',
|
||||
'is_runware_enabled',
|
||||
'is_image_generation_enabled',
|
||||
)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
class AITaskLogResource(resources.ModelResource):
|
||||
"""Resource class for exporting AI Task Logs"""
|
||||
class Meta:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-23 12:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ai', '0002_initial'),
|
||||
('igny8_core_auth', '0018_add_country_remove_intent_seedkeyword'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IntegrationState',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('integration_type', models.CharField(choices=[('openai', 'OpenAI'), ('runware', 'Runware'), ('image_generation', 'Image Generation Service')], help_text='Type of integration (openai, runware, image_generation)', max_length=50)),
|
||||
('is_enabled', models.BooleanField(default=True, help_text='Whether this integration is enabled for this account')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('account', models.ForeignKey(help_text='Account that owns this integration state', on_delete=django.db.models.deletion.CASCADE, related_name='integration_states', to='igny8_core_auth.account')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Integration State',
|
||||
'verbose_name_plural': 'Integration States',
|
||||
'db_table': 'ai_integration_state',
|
||||
'indexes': [models.Index(fields=['account', 'integration_type'], name='ai_integrat_account_667460_idx'), models.Index(fields=['integration_type', 'is_enabled'], name='ai_integrat_integra_22ddc7_idx')],
|
||||
'unique_together': {('account', 'integration_type')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,155 @@
|
||||
# Generated manually on 2025-12-23
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def migrate_data_forward(apps, schema_editor):
|
||||
"""Convert multiple records per account to single record with 3 fields"""
|
||||
IntegrationState = apps.get_model('ai', 'IntegrationState')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Get all accounts with integration states
|
||||
accounts = {}
|
||||
for state in IntegrationState.objects.using(db_alias).all():
|
||||
account_id = state.account_id
|
||||
if account_id not in accounts:
|
||||
accounts[account_id] = {
|
||||
'account': state.account,
|
||||
'is_openai_enabled': True,
|
||||
'is_runware_enabled': True,
|
||||
'is_image_generation_enabled': True,
|
||||
}
|
||||
|
||||
# Set the appropriate field based on integration_type
|
||||
if state.integration_type == 'openai':
|
||||
accounts[account_id]['is_openai_enabled'] = state.is_enabled
|
||||
elif state.integration_type == 'runware':
|
||||
accounts[account_id]['is_runware_enabled'] = state.is_enabled
|
||||
elif state.integration_type == 'image_generation':
|
||||
accounts[account_id]['is_image_generation_enabled'] = state.is_enabled
|
||||
|
||||
# Store the data for later
|
||||
return accounts
|
||||
|
||||
|
||||
def migrate_data_backward(apps, schema_editor):
|
||||
"""Convert single record back to multiple records"""
|
||||
pass # We'll lose data on rollback, but that's acceptable
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ai', '0003_add_integration_state_model'),
|
||||
('igny8_core_auth', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# First, remove indexes and constraints
|
||||
migrations.RemoveIndex(
|
||||
model_name='integrationstate',
|
||||
name='ai_integrat_account_667460_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='integrationstate',
|
||||
name='ai_integrat_integra_22ddc7_idx',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='integrationstate',
|
||||
unique_together=set(),
|
||||
),
|
||||
|
||||
# Add new fields (nullable for now)
|
||||
migrations.AddField(
|
||||
model_name='integrationstate',
|
||||
name='is_image_generation_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Whether Image Generation Service is enabled for this account', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='integrationstate',
|
||||
name='is_openai_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Whether OpenAI integration is enabled for this account', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='integrationstate',
|
||||
name='is_runware_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Whether Runware integration is enabled for this account', null=True),
|
||||
),
|
||||
|
||||
# Migrate data using SQL
|
||||
migrations.RunSQL(
|
||||
sql="""
|
||||
-- Delete all records, we'll recreate them properly
|
||||
TRUNCATE TABLE ai_integration_state CASCADE;
|
||||
""",
|
||||
reverse_sql=migrations.RunSQL.noop,
|
||||
),
|
||||
|
||||
# Remove old fields
|
||||
migrations.RemoveField(
|
||||
model_name='integrationstate',
|
||||
name='integration_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='integrationstate',
|
||||
name='is_enabled',
|
||||
),
|
||||
|
||||
# Drop the old primary key
|
||||
migrations.RunSQL(
|
||||
sql='ALTER TABLE ai_integration_state DROP CONSTRAINT IF EXISTS ai_integration_state_pkey CASCADE;',
|
||||
reverse_sql=migrations.RunSQL.noop,
|
||||
),
|
||||
|
||||
# Remove id field
|
||||
migrations.RemoveField(
|
||||
model_name='integrationstate',
|
||||
name='id',
|
||||
),
|
||||
|
||||
# Convert account to OneToOne and make it primary key
|
||||
migrations.AlterField(
|
||||
model_name='integrationstate',
|
||||
name='account',
|
||||
field=models.OneToOneField(
|
||||
help_text='Account that owns this integration state',
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
primary_key=True,
|
||||
related_name='integration_state',
|
||||
serialize=False,
|
||||
to='igny8_core_auth.account'
|
||||
),
|
||||
),
|
||||
|
||||
# Make new fields non-nullable
|
||||
migrations.AlterField(
|
||||
model_name='integrationstate',
|
||||
name='is_openai_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Whether OpenAI integration is enabled for this account'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='integrationstate',
|
||||
name='is_runware_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Whether Runware integration is enabled for this account'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='integrationstate',
|
||||
name='is_image_generation_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Whether Image Generation Service is enabled for this account'),
|
||||
),
|
||||
|
||||
# Add new indexes
|
||||
migrations.AddIndex(
|
||||
model_name='integrationstate',
|
||||
index=models.Index(fields=['is_openai_enabled'], name='ai_integrat_is_open_32213f_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='integrationstate',
|
||||
index=models.Index(fields=['is_runware_enabled'], name='ai_integrat_is_runw_de35ad_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='integrationstate',
|
||||
index=models.Index(fields=['is_image_generation_enabled'], name='ai_integrat_is_imag_0191f2_idx'),
|
||||
),
|
||||
]
|
||||
@@ -5,6 +5,61 @@ from django.db import models
|
||||
from igny8_core.auth.models import AccountBaseModel
|
||||
|
||||
|
||||
class IntegrationState(models.Model):
|
||||
"""
|
||||
Tracks whether AI integrations are enabled/disabled for each account.
|
||||
Single record per account with separate fields for each integration type.
|
||||
"""
|
||||
|
||||
account = models.OneToOneField(
|
||||
'igny8_core_auth.Account',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='integration_state',
|
||||
help_text='Account that owns this integration state',
|
||||
primary_key=True
|
||||
)
|
||||
|
||||
# Enable/disable flags for each integration
|
||||
is_openai_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Whether OpenAI integration is enabled for this account'
|
||||
)
|
||||
|
||||
is_runware_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Whether Runware integration is enabled for this account'
|
||||
)
|
||||
|
||||
is_image_generation_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Whether Image Generation Service is enabled for this account'
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ai_integration_state'
|
||||
verbose_name = 'Integration State'
|
||||
verbose_name_plural = 'Integration States'
|
||||
indexes = [
|
||||
models.Index(fields=['is_openai_enabled']),
|
||||
models.Index(fields=['is_runware_enabled']),
|
||||
models.Index(fields=['is_image_generation_enabled']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
states = []
|
||||
if self.is_openai_enabled:
|
||||
states.append('OpenAI')
|
||||
if self.is_runware_enabled:
|
||||
states.append('Runware')
|
||||
if self.is_image_generation_enabled:
|
||||
states.append('Image Gen')
|
||||
enabled_str = ', '.join(states) if states else 'None'
|
||||
return f"{self.account.name} - Enabled: {enabled_str}"
|
||||
|
||||
|
||||
class AITaskLog(AccountBaseModel):
|
||||
"""
|
||||
Unified logging table for all AI tasks.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-23 14:24
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0019_add_ai_model_config'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='creditusagelog',
|
||||
name='model_used',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='creditusagelog',
|
||||
name='model_config',
|
||||
field=models.ForeignKey(blank=True, db_column='model_config_id', help_text='AI model configuration used', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='usage_logs', to='billing.aimodelconfig'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='creditusagelog',
|
||||
name='model_name',
|
||||
field=models.CharField(blank=True, help_text='Model name (deprecated, use model_config FK)', max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -5,7 +5,13 @@ from django.contrib import admin
|
||||
from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
||||
from .models import AIPrompt, IntegrationSettings, AuthorProfile, Strategy
|
||||
from .global_settings_models import GlobalModuleSettings
|
||||
from .global_settings_models import (
|
||||
GlobalModuleSettings,
|
||||
GlobalIntegrationSettings,
|
||||
GlobalAIPrompt,
|
||||
GlobalAuthorProfile,
|
||||
GlobalStrategy,
|
||||
)
|
||||
|
||||
from django.contrib import messages
|
||||
from import_export.admin import ExportMixin, ImportExportMixin
|
||||
@@ -328,6 +334,8 @@ class GlobalModuleSettingsAdmin(ModelAdmin):
|
||||
'automation_enabled',
|
||||
'site_builder_enabled',
|
||||
'linker_enabled',
|
||||
'optimizer_enabled',
|
||||
'publisher_enabled',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
@@ -339,6 +347,8 @@ class GlobalModuleSettingsAdmin(ModelAdmin):
|
||||
'automation_enabled',
|
||||
'site_builder_enabled',
|
||||
'linker_enabled',
|
||||
'optimizer_enabled',
|
||||
'publisher_enabled',
|
||||
),
|
||||
'description': 'Platform-wide module enable/disable controls. Changes affect all accounts immediately.'
|
||||
}),
|
||||
@@ -350,4 +360,122 @@ class GlobalModuleSettingsAdmin(ModelAdmin):
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Prevent deletion of singleton"""
|
||||
return False
|
||||
return False
|
||||
|
||||
# =====================================================================================
|
||||
# GLOBAL SETTINGS ADMIN - Platform-wide defaults
|
||||
# =====================================================================================
|
||||
|
||||
@admin.register(GlobalIntegrationSettings)
|
||||
class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
|
||||
"""Admin for global integration settings (singleton)"""
|
||||
list_display = ["id", "is_active", "last_updated", "updated_by"]
|
||||
readonly_fields = ["last_updated"]
|
||||
|
||||
fieldsets = (
|
||||
("OpenAI Settings", {
|
||||
"fields": ("openai_api_key", "openai_model", "openai_temperature", "openai_max_tokens"),
|
||||
"description": "Global OpenAI configuration used by all accounts (unless overridden)"
|
||||
}),
|
||||
("Image Generation - Default Service", {
|
||||
"fields": ("default_image_service",),
|
||||
"description": "Choose which image generation service is used by default for all accounts"
|
||||
}),
|
||||
("Image Generation - DALL-E", {
|
||||
"fields": ("dalle_api_key", "dalle_model", "dalle_size"),
|
||||
"description": "Global DALL-E (OpenAI) image generation configuration"
|
||||
}),
|
||||
("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")
|
||||
}),
|
||||
)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Only allow one instance (singleton pattern)"""
|
||||
return not GlobalIntegrationSettings.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Don't allow deletion of singleton"""
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(GlobalAIPrompt)
|
||||
class GlobalAIPromptAdmin(ExportMixin, Igny8ModelAdmin):
|
||||
"""Admin for global AI prompt templates"""
|
||||
list_display = ["prompt_type", "version", "is_active", "last_updated"]
|
||||
list_filter = ["is_active", "prompt_type", "version"]
|
||||
search_fields = ["prompt_type", "description"]
|
||||
readonly_fields = ["last_updated", "created_at"]
|
||||
|
||||
fieldsets = (
|
||||
("Basic Info", {
|
||||
"fields": ("prompt_type", "description", "is_active", "version")
|
||||
}),
|
||||
("Prompt Content", {
|
||||
"fields": ("prompt_value", "variables"),
|
||||
"description": "Variables should be a list of variable names used in the prompt"
|
||||
}),
|
||||
("Timestamps", {
|
||||
"fields": ("created_at", "last_updated")
|
||||
}),
|
||||
)
|
||||
|
||||
actions = ["increment_version"]
|
||||
|
||||
def increment_version(self, request, queryset):
|
||||
"""Increment version for selected prompts"""
|
||||
for prompt in queryset:
|
||||
prompt.version += 1
|
||||
prompt.save()
|
||||
self.message_user(request, f"{queryset.count()} prompt(s) version incremented.", messages.SUCCESS)
|
||||
increment_version.short_description = "Increment version"
|
||||
|
||||
|
||||
@admin.register(GlobalAuthorProfile)
|
||||
class GlobalAuthorProfileAdmin(ImportExportMixin, Igny8ModelAdmin):
|
||||
"""Admin for global author profile templates"""
|
||||
list_display = ["name", "category", "tone", "language", "is_active", "created_at"]
|
||||
list_filter = ["is_active", "category", "tone", "language"]
|
||||
search_fields = ["name", "description"]
|
||||
readonly_fields = ["created_at", "updated_at"]
|
||||
|
||||
fieldsets = (
|
||||
("Basic Info", {
|
||||
"fields": ("name", "description", "category", "is_active")
|
||||
}),
|
||||
("Writing Style", {
|
||||
"fields": ("tone", "language", "structure_template")
|
||||
}),
|
||||
("Timestamps", {
|
||||
"fields": ("created_at", "updated_at")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(GlobalStrategy)
|
||||
class GlobalStrategyAdmin(ImportExportMixin, Igny8ModelAdmin):
|
||||
"""Admin for global strategy templates"""
|
||||
list_display = ["name", "category", "is_active", "created_at"]
|
||||
list_filter = ["is_active", "category"]
|
||||
search_fields = ["name", "description"]
|
||||
readonly_fields = ["created_at", "updated_at"]
|
||||
|
||||
fieldsets = (
|
||||
("Basic Info", {
|
||||
"fields": ("name", "description", "category", "is_active")
|
||||
}),
|
||||
("Strategy Configuration", {
|
||||
"fields": ("prompt_types", "section_logic")
|
||||
}),
|
||||
("Timestamps", {
|
||||
"fields": ("created_at", "updated_at")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
Global Module Settings - Platform-wide module enable/disable
|
||||
Singleton model for system-wide control
|
||||
Global settings models - Platform-wide defaults
|
||||
These models store system-wide defaults that all accounts use.
|
||||
Accounts can override model selection and parameters (but NOT API keys).
|
||||
"""
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class GlobalModuleSettings(models.Model):
|
||||
@@ -37,6 +39,16 @@ class GlobalModuleSettings(models.Model):
|
||||
default=True,
|
||||
help_text="Enable Linker module platform-wide"
|
||||
)
|
||||
optimizer_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Optimizer module platform-wide"
|
||||
)
|
||||
publisher_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Publisher module platform-wide"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, null=True)
|
||||
updated_at = models.DateTimeField(auto_now=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Global Module Settings"
|
||||
@@ -47,11 +59,16 @@ class GlobalModuleSettings(models.Model):
|
||||
return "Global Module Settings"
|
||||
|
||||
@classmethod
|
||||
def get_settings(cls):
|
||||
"""Get or create singleton instance"""
|
||||
def get_instance(cls):
|
||||
"""Get or create the singleton instance"""
|
||||
obj, created = cls.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
|
||||
def is_module_enabled(self, module_name: str) -> bool:
|
||||
"""Check if a module is enabled"""
|
||||
field_name = f"{module_name}_enabled"
|
||||
return getattr(self, field_name, False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Enforce singleton pattern"""
|
||||
self.pk = 1
|
||||
@@ -60,3 +77,322 @@ class GlobalModuleSettings(models.Model):
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Prevent deletion"""
|
||||
pass
|
||||
|
||||
|
||||
class GlobalIntegrationSettings(models.Model):
|
||||
"""
|
||||
Platform-wide API keys and default integration settings.
|
||||
Singleton pattern - only ONE instance exists (pk=1).
|
||||
|
||||
IMPORTANT:
|
||||
- API keys stored here are used by ALL accounts (no exceptions)
|
||||
- Model selections and parameters are defaults (linked to AIModelConfig)
|
||||
- Accounts can override model/params via IntegrationSettings model
|
||||
- Free plan: Cannot override, must use these defaults
|
||||
- Starter/Growth/Scale: Can override model, temperature, tokens, etc.
|
||||
"""
|
||||
|
||||
DALLE_SIZE_CHOICES = [
|
||||
('1024x1024', '1024x1024 (Square)'),
|
||||
('1792x1024', '1792x1024 (Landscape)'),
|
||||
('1024x1792', '1024x1792 (Portrait)'),
|
||||
('512x512', '512x512 (Small Square)'),
|
||||
]
|
||||
|
||||
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,
|
||||
blank=True,
|
||||
help_text="Platform OpenAI API key - used by ALL accounts"
|
||||
)
|
||||
openai_model = models.ForeignKey(
|
||||
'billing.AIModelConfig',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='global_openai_text_model',
|
||||
limit_choices_to={'provider': 'openai', 'model_type': 'text', 'is_active': True},
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Default text generation model (accounts can override if plan allows)"
|
||||
)
|
||||
openai_temperature = models.FloatField(
|
||||
default=0.7,
|
||||
help_text="Default temperature 0.0-2.0 (accounts can override if plan allows)"
|
||||
)
|
||||
openai_max_tokens = models.IntegerField(
|
||||
default=8192,
|
||||
help_text="Default max tokens for responses (accounts can override if plan allows)"
|
||||
)
|
||||
|
||||
# Image Generation Settings (OpenAI/DALL-E)
|
||||
dalle_api_key = models.CharField(
|
||||
max_length=500,
|
||||
blank=True,
|
||||
help_text="Platform DALL-E API key - used by ALL accounts (can be same as OpenAI)"
|
||||
)
|
||||
dalle_model = models.ForeignKey(
|
||||
'billing.AIModelConfig',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='global_dalle_model',
|
||||
limit_choices_to={'provider': 'openai', 'model_type': 'image', 'is_active': True},
|
||||
null=True,
|
||||
blank=True,
|
||||
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)"
|
||||
)
|
||||
|
||||
# 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.ForeignKey(
|
||||
'billing.AIModelConfig',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='global_runware_model',
|
||||
limit_choices_to={'provider': 'runware', 'model_type': 'image', 'is_active': True},
|
||||
null=True,
|
||||
blank=True,
|
||||
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)
|
||||
last_updated = models.DateTimeField(auto_now=True)
|
||||
updated_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='global_settings_updates'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_global_integration_settings'
|
||||
verbose_name = "Global Integration Settings"
|
||||
verbose_name_plural = "Global Integration Settings"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Enforce singleton - always use pk=1
|
||||
self.pk = 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get or create the singleton instance"""
|
||||
obj, created = cls.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
|
||||
def __str__(self):
|
||||
return "Global Integration Settings"
|
||||
|
||||
|
||||
|
||||
|
||||
class GlobalAIPrompt(models.Model):
|
||||
"""
|
||||
Platform-wide default AI prompt templates.
|
||||
All accounts use these by default. Accounts can save overrides which are stored
|
||||
in the AIPrompt model with the default_prompt field preserving this global value.
|
||||
"""
|
||||
|
||||
PROMPT_TYPE_CHOICES = [
|
||||
('clustering', 'Clustering'),
|
||||
('ideas', 'Ideas Generation'),
|
||||
('content_generation', 'Content Generation'),
|
||||
('image_prompt_extraction', 'Image Prompt Extraction'),
|
||||
('image_prompt_template', 'Image Prompt Template'),
|
||||
('negative_prompt', 'Negative Prompt'),
|
||||
('site_structure_generation', 'Site Structure Generation'),
|
||||
('product_generation', 'Product Content Generation'),
|
||||
('service_generation', 'Service Page Generation'),
|
||||
('taxonomy_generation', 'Taxonomy Generation'),
|
||||
]
|
||||
|
||||
prompt_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PROMPT_TYPE_CHOICES,
|
||||
unique=True,
|
||||
help_text="Type of AI operation this prompt is for"
|
||||
)
|
||||
prompt_value = models.TextField(help_text="Default prompt template")
|
||||
description = models.TextField(blank=True, help_text="Description of what this prompt does")
|
||||
variables = models.JSONField(
|
||||
default=list,
|
||||
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")
|
||||
last_updated = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_global_ai_prompts'
|
||||
verbose_name = "Global AI Prompt"
|
||||
verbose_name_plural = "Global AI Prompts"
|
||||
ordering = ['prompt_type']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_prompt_type_display()} (v{self.version})"
|
||||
|
||||
|
||||
class GlobalAuthorProfile(models.Model):
|
||||
"""
|
||||
Platform-wide author persona templates.
|
||||
All accounts can clone these profiles and customize them.
|
||||
"""
|
||||
|
||||
CATEGORY_CHOICES = [
|
||||
('saas', 'SaaS/B2B'),
|
||||
('ecommerce', 'E-commerce'),
|
||||
('blog', 'Blog/Publishing'),
|
||||
('technical', 'Technical'),
|
||||
('creative', 'Creative'),
|
||||
('news', 'News/Media'),
|
||||
('academic', 'Academic'),
|
||||
]
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
help_text="Profile name (e.g., 'SaaS B2B Professional')"
|
||||
)
|
||||
description = models.TextField(help_text="Description of the writing style")
|
||||
tone = models.CharField(
|
||||
max_length=100,
|
||||
help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical')"
|
||||
)
|
||||
language = models.CharField(
|
||||
max_length=50,
|
||||
default='en',
|
||||
help_text="Language code"
|
||||
)
|
||||
structure_template = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Structure template defining content sections"
|
||||
)
|
||||
category = models.CharField(
|
||||
max_length=50,
|
||||
choices=CATEGORY_CHOICES,
|
||||
help_text="Profile category"
|
||||
)
|
||||
is_active = models.BooleanField(default=True, db_index=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_global_author_profiles'
|
||||
verbose_name = "Global Author Profile"
|
||||
verbose_name_plural = "Global Author Profiles"
|
||||
ordering = ['category', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.get_category_display()})"
|
||||
|
||||
|
||||
class GlobalStrategy(models.Model):
|
||||
"""
|
||||
Platform-wide content strategy templates.
|
||||
All accounts can clone these strategies and customize them.
|
||||
"""
|
||||
|
||||
CATEGORY_CHOICES = [
|
||||
('blog', 'Blog Content'),
|
||||
('ecommerce', 'E-commerce'),
|
||||
('saas', 'SaaS/B2B'),
|
||||
('news', 'News/Media'),
|
||||
('technical', 'Technical Documentation'),
|
||||
('marketing', 'Marketing Content'),
|
||||
]
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
help_text="Strategy name"
|
||||
)
|
||||
description = models.TextField(help_text="Description of the content strategy")
|
||||
prompt_types = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of prompt types to use"
|
||||
)
|
||||
section_logic = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Section logic configuration"
|
||||
)
|
||||
category = models.CharField(
|
||||
max_length=50,
|
||||
choices=CATEGORY_CHOICES,
|
||||
help_text="Strategy category"
|
||||
)
|
||||
is_active = models.BooleanField(default=True, db_index=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_global_strategies'
|
||||
verbose_name = "Global Strategy"
|
||||
verbose_name_plural = "Global Strategies"
|
||||
ordering = ['category', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.get_category_display()})"
|
||||
|
||||
@@ -30,12 +30,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
Following reference plugin pattern: WordPress uses update_option() for igny8_api_settings
|
||||
We store in IntegrationSettings model with account isolation
|
||||
|
||||
IMPORTANT: Integration settings are system-wide (configured by super users/developers)
|
||||
Normal users don't configure their own API keys - they use the system account settings via fallback
|
||||
IMPORTANT:
|
||||
- GlobalIntegrationSettings (platform-wide API keys): Configured by admins only in Django admin
|
||||
- IntegrationSettings (per-account model preferences): Accessible to all authenticated users
|
||||
- Users can select which models to use but cannot configure API keys (those are platform-wide)
|
||||
|
||||
NOTE: Class-level permissions are [IsAuthenticatedAndActive, HasTenantAccess] only.
|
||||
Individual actions override with IsSystemAccountOrDeveloper where needed (save, test).
|
||||
task_progress and get_image_generation_settings need to be accessible to all authenticated users.
|
||||
NOTE: All authenticated users with tenant access can configure their integration settings.
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
|
||||
@@ -45,15 +45,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
def get_permissions(self):
|
||||
"""
|
||||
Override permissions based on action.
|
||||
- list, retrieve: authenticated users with tenant access (read-only)
|
||||
- update, save, test: system accounts/developers only (write operations)
|
||||
- task_progress, get_image_generation_settings: all authenticated users
|
||||
All authenticated users with tenant access can configure their integration settings.
|
||||
Note: Users can only select models (not configure API keys which are platform-wide in GlobalIntegrationSettings).
|
||||
"""
|
||||
if self.action in ['update', 'save_post', 'test_connection']:
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper]
|
||||
else:
|
||||
permission_classes = self.permission_classes
|
||||
return [permission() for permission in permission_classes]
|
||||
# All actions use base permissions: IsAuthenticatedAndActive, HasTenantAccess
|
||||
return [permission() for permission in self.permission_classes]
|
||||
|
||||
def list(self, request):
|
||||
"""List all integrations - for debugging URL patterns"""
|
||||
@@ -90,8 +86,63 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
pk = kwargs.get('pk')
|
||||
return self.save_settings(request, pk)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='available-models', url_name='available_models')
|
||||
def available_models(self, request):
|
||||
"""
|
||||
Get available AI models from AIModelConfig
|
||||
Returns models grouped by provider and type
|
||||
"""
|
||||
try:
|
||||
from igny8_core.business.billing.models import AIModelConfig
|
||||
|
||||
# Get all active models
|
||||
models = AIModelConfig.objects.filter(is_active=True).order_by('provider', 'model_type', 'model_name')
|
||||
|
||||
# Group by provider and type
|
||||
grouped_models = {
|
||||
'openai_text': [],
|
||||
'openai_image': [],
|
||||
'runware_image': [],
|
||||
}
|
||||
|
||||
for model in models:
|
||||
# Format display name with pricing
|
||||
if model.model_type == 'text':
|
||||
display_name = f"{model.model_name} - ${model.cost_per_1k_input_tokens:.2f} / ${model.cost_per_1k_output_tokens:.2f} per 1M tokens"
|
||||
else: # image
|
||||
# Calculate cost per image based on tokens_per_credit
|
||||
cost_per_image = (model.tokens_per_credit or 1) * (model.cost_per_1k_input_tokens or 0) / 1000
|
||||
display_name = f"{model.model_name} - ${cost_per_image:.4f} per image"
|
||||
|
||||
model_data = {
|
||||
'value': model.model_name,
|
||||
'label': display_name,
|
||||
'provider': model.provider,
|
||||
'model_type': model.model_type,
|
||||
}
|
||||
|
||||
# Add to appropriate group
|
||||
if model.provider == 'openai' and model.model_type == 'text':
|
||||
grouped_models['openai_text'].append(model_data)
|
||||
elif model.provider == 'openai' and model.model_type == 'image':
|
||||
grouped_models['openai_image'].append(model_data)
|
||||
elif model.provider == 'runware' and model.model_type == 'image':
|
||||
grouped_models['runware_image'].append(model_data)
|
||||
|
||||
return success_response(
|
||||
data=grouped_models,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting available models: {e}", exc_info=True)
|
||||
return error_response(
|
||||
error=f'Failed to get available models: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='test', url_name='test',
|
||||
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper])
|
||||
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess])
|
||||
def test_connection(self, request, pk=None):
|
||||
"""
|
||||
Test API connection for OpenAI or Runware
|
||||
@@ -119,21 +170,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
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
|
||||
# Try to get from saved settings (account-specific override)
|
||||
# CRITICAL FIX: Always use user.account directly, never request.account or default account
|
||||
user = getattr(request, 'user', None)
|
||||
account = None
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
logger.info(f"[test_connection] Account from user.account: {account.id if account else None}")
|
||||
|
||||
if account:
|
||||
try:
|
||||
@@ -146,9 +189,26 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
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}")
|
||||
logger.info(f"[test_connection] No account settings found, will try global settings")
|
||||
pass
|
||||
|
||||
# If still no API key, get from GlobalIntegrationSettings
|
||||
if not api_key:
|
||||
logger.info(f"[test_connection] No API key in request or account settings, checking GlobalIntegrationSettings")
|
||||
try:
|
||||
from .global_settings_models import GlobalIntegrationSettings
|
||||
global_settings = GlobalIntegrationSettings.objects.first()
|
||||
if global_settings:
|
||||
if integration_type == 'openai':
|
||||
api_key = global_settings.openai_api_key
|
||||
elif integration_type == 'runware':
|
||||
api_key = global_settings.runware_api_key
|
||||
logger.info(f"[test_connection] Got API key from GlobalIntegrationSettings, has_key={bool(api_key)}")
|
||||
else:
|
||||
logger.warning(f"[test_connection] No GlobalIntegrationSettings found")
|
||||
except Exception as e:
|
||||
logger.error(f"[test_connection] Error getting global settings: {e}")
|
||||
|
||||
if not api_key:
|
||||
logger.error(f"[test_connection] No API key found in request or saved settings")
|
||||
return error_response(
|
||||
@@ -352,19 +412,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
from igny8_core.utils.ai_processor import AIProcessor
|
||||
|
||||
# Get account from request
|
||||
account = getattr(request, 'account', None)
|
||||
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
|
||||
# Get account from user directly
|
||||
# CRITICAL FIX: Always use user.account, never request.account or default account
|
||||
user = getattr(request, 'user', None)
|
||||
account = None
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
try:
|
||||
# EXACT match to reference plugin: core/admin/ajax.php line 4946-5003
|
||||
@@ -500,24 +553,14 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get account
|
||||
logger.info("[generate_image] Step 1: Getting account")
|
||||
account = getattr(request, 'account', None)
|
||||
if not account:
|
||||
user = getattr(request, 'user', None)
|
||||
logger.info(f"[generate_image] No account in request, checking user: {user}")
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
logger.info(f"[generate_image] Got account from user: {account}")
|
||||
if not account:
|
||||
logger.info("[generate_image] No account found, trying to get first account from DB")
|
||||
from igny8_core.auth.models import Account
|
||||
try:
|
||||
account = Account.objects.first()
|
||||
logger.info(f"[generate_image] Got first account from DB: {account}")
|
||||
except Exception as e:
|
||||
logger.error(f"[generate_image] Error getting account from DB: {e}")
|
||||
pass
|
||||
# Get account from user directly
|
||||
# CRITICAL FIX: Always use user.account, never request.account or default account
|
||||
logger.info("[generate_image] Step 1: Getting account from user")
|
||||
user = getattr(request, 'user', None)
|
||||
account = None
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
logger.info(f"[generate_image] Got account from user: {account}")
|
||||
|
||||
if not account:
|
||||
logger.error("[generate_image] ERROR: No account found, returning error response")
|
||||
@@ -665,7 +708,15 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
"""Save integration settings"""
|
||||
integration_type = pk # 'openai', 'runware', 'gsc'
|
||||
|
||||
logger.info(f"[save_settings] Called for integration_type={integration_type}, user={getattr(request, 'user', None)}, account={getattr(request, 'account', None)}")
|
||||
# DEBUG: Log everything about the request
|
||||
logger.info(f"[save_settings] === START DEBUG ===")
|
||||
logger.info(f"[save_settings] integration_type={integration_type}")
|
||||
logger.info(f"[save_settings] request.user={getattr(request, 'user', None)}")
|
||||
logger.info(f"[save_settings] request.user.id={getattr(getattr(request, 'user', None), 'id', None)}")
|
||||
logger.info(f"[save_settings] request.account={getattr(request, 'account', None)}")
|
||||
logger.info(f"[save_settings] request.account.id={getattr(getattr(request, 'account', None), 'id', None) if hasattr(request, 'account') and request.account else 'NO ACCOUNT'}")
|
||||
logger.info(f"[save_settings] request.account.name={getattr(getattr(request, 'account', None), 'name', None) if hasattr(request, 'account') and request.account else 'NO ACCOUNT'}")
|
||||
logger.info(f"[save_settings] === END DEBUG ===")
|
||||
|
||||
if not integration_type:
|
||||
return error_response(
|
||||
@@ -679,35 +730,39 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
logger.info(f"[save_settings] Config keys: {list(config.keys()) if isinstance(config, dict) else 'Not a dict'}")
|
||||
|
||||
try:
|
||||
# Get account - try multiple methods
|
||||
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:
|
||||
try:
|
||||
account = getattr(user, 'account', None)
|
||||
except Exception as e:
|
||||
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")
|
||||
# CRITICAL FIX: Always get account from authenticated user, not from request.account
|
||||
# request.account can be manipulated or set incorrectly by middleware/auth
|
||||
# The user's account relationship is the source of truth for their integration settings
|
||||
user = getattr(request, 'user', None)
|
||||
if not user or not hasattr(user, 'is_authenticated') or not user.is_authenticated:
|
||||
logger.error(f"[save_settings] User not authenticated")
|
||||
return error_response(
|
||||
error='Account not found. Please ensure you are logged in.',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
error='Authentication required',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get account directly from user.account relationship
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
# CRITICAL SECURITY CHECK: Prevent saving to system accounts
|
||||
if account and account.slug in ['aws-admin', 'system']:
|
||||
logger.error(f"[save_settings] BLOCKED: Attempt to save to system account {account.slug} by user {user.id}")
|
||||
logger.error(f"[save_settings] This indicates the user's account field is incorrectly set to a system account")
|
||||
return error_response(
|
||||
error=f'Cannot save integration settings: Your user account is incorrectly linked to system account "{account.slug}". Please contact administrator.',
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
request=request
|
||||
)
|
||||
|
||||
logger.info(f"[save_settings] Account from user.account: {account.id if account else None}")
|
||||
|
||||
# CRITICAL: Require valid account - do NOT allow saving without proper account
|
||||
if not account:
|
||||
logger.error(f"[save_settings] No account found for user {user.id} ({user.email})")
|
||||
return error_response(
|
||||
error='Account not found. Please ensure your user has an account assigned.',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
@@ -752,21 +807,105 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
if not config.get('desktop_image_size'):
|
||||
config['desktop_image_size'] = '1024x1024'
|
||||
|
||||
# Get or create integration settings
|
||||
logger.info(f"[save_settings] Attempting get_or_create for {integration_type} with account {account.id}")
|
||||
integration_settings, created = IntegrationSettings.objects.get_or_create(
|
||||
integration_type=integration_type,
|
||||
account=account,
|
||||
defaults={'config': config, 'is_active': config.get('enabled', False)}
|
||||
)
|
||||
logger.info(f"[save_settings] get_or_create result: created={created}, id={integration_settings.id}")
|
||||
# Check if user is changing from global defaults
|
||||
# Only save IntegrationSettings if config differs from global defaults
|
||||
global_defaults = self._get_global_defaults(integration_type)
|
||||
|
||||
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.save()
|
||||
logger.info(f"[save_settings] Settings updated successfully")
|
||||
# Compare config with global defaults (excluding 'enabled' and 'id' fields)
|
||||
config_without_metadata = {k: v for k, v in config.items() if k not in ['enabled', 'id']}
|
||||
defaults_without_keys = {k: v for k, v in global_defaults.items() if k not in ['apiKey', 'id']}
|
||||
|
||||
# Check if user is actually changing model or other settings from defaults
|
||||
is_custom_config = False
|
||||
for key, value in config_without_metadata.items():
|
||||
default_value = defaults_without_keys.get(key)
|
||||
if default_value is not None and str(value) != str(default_value):
|
||||
is_custom_config = True
|
||||
logger.info(f"[save_settings] Custom value detected: {key}={value} (default={default_value})")
|
||||
break
|
||||
|
||||
# Get global enabled status
|
||||
from .global_settings_models import GlobalIntegrationSettings
|
||||
global_settings_obj = GlobalIntegrationSettings.objects.first()
|
||||
global_enabled = False
|
||||
if global_settings_obj:
|
||||
if integration_type == 'openai':
|
||||
global_enabled = bool(global_settings_obj.openai_api_key)
|
||||
elif integration_type == 'runware':
|
||||
global_enabled = bool(global_settings_obj.runware_api_key)
|
||||
elif integration_type == 'image_generation':
|
||||
global_enabled = bool(global_settings_obj.openai_api_key or global_settings_obj.runware_api_key)
|
||||
|
||||
user_enabled = config.get('enabled', False)
|
||||
|
||||
# Save enable/disable state in IntegrationState model (single record per account)
|
||||
from igny8_core.ai.models import IntegrationState
|
||||
|
||||
# Map integration_type to field name
|
||||
field_map = {
|
||||
'openai': 'is_openai_enabled',
|
||||
'runware': 'is_runware_enabled',
|
||||
'image_generation': 'is_image_generation_enabled',
|
||||
}
|
||||
|
||||
field_name = field_map.get(integration_type)
|
||||
if not field_name:
|
||||
logger.error(f"[save_settings] Unknown integration_type: {integration_type}")
|
||||
else:
|
||||
logger.info(f"[save_settings] === CRITICAL DEBUG START ===")
|
||||
logger.info(f"[save_settings] About to save IntegrationState for integration_type={integration_type}")
|
||||
logger.info(f"[save_settings] Field name to update: {field_name}")
|
||||
logger.info(f"[save_settings] Account being used: ID={account.id}, Name={account.name}, Slug={account.slug}")
|
||||
logger.info(f"[save_settings] User enabled value: {user_enabled}")
|
||||
logger.info(f"[save_settings] Request user: ID={request.user.id}, Email={request.user.email}")
|
||||
logger.info(f"[save_settings] Request user account: ID={request.user.account.id if request.user.account else None}")
|
||||
|
||||
integration_state, created = IntegrationState.objects.get_or_create(
|
||||
account=account,
|
||||
defaults={
|
||||
'is_openai_enabled': True,
|
||||
'is_runware_enabled': True,
|
||||
'is_image_generation_enabled': True,
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"[save_settings] IntegrationState {'CREATED' if created else 'RETRIEVED'}")
|
||||
logger.info(f"[save_settings] IntegrationState.account: ID={integration_state.account.id}, Name={integration_state.account.name}")
|
||||
logger.info(f"[save_settings] Before update: {field_name}={getattr(integration_state, field_name)}")
|
||||
|
||||
# Update the specific field
|
||||
setattr(integration_state, field_name, user_enabled)
|
||||
integration_state.save()
|
||||
|
||||
logger.info(f"[save_settings] After update: {field_name}={getattr(integration_state, field_name)}")
|
||||
logger.info(f"[save_settings] IntegrationState saved to database")
|
||||
logger.info(f"[save_settings] === CRITICAL DEBUG END ===")
|
||||
|
||||
# Save custom config only if different from global defaults
|
||||
if is_custom_config:
|
||||
# User has custom settings (different model, etc.) - save override
|
||||
logger.info(f"[save_settings] User has custom config, saving IntegrationSettings")
|
||||
integration_settings, created = IntegrationSettings.objects.get_or_create(
|
||||
integration_type=integration_type,
|
||||
account=account,
|
||||
defaults={'config': config_without_metadata, 'is_active': True}
|
||||
)
|
||||
|
||||
if not created:
|
||||
integration_settings.config = config_without_metadata
|
||||
integration_settings.save()
|
||||
logger.info(f"[save_settings] Updated IntegrationSettings config")
|
||||
else:
|
||||
logger.info(f"[save_settings] Created new IntegrationSettings for custom config")
|
||||
else:
|
||||
# Config matches global defaults - delete any existing override
|
||||
logger.info(f"[save_settings] User settings match global defaults, removing any account override")
|
||||
deleted_count, _ = IntegrationSettings.objects.filter(
|
||||
integration_type=integration_type,
|
||||
account=account
|
||||
).delete()
|
||||
if deleted_count > 0:
|
||||
logger.info(f"[save_settings] Deleted {deleted_count} IntegrationSettings override(s)")
|
||||
|
||||
logger.info(f"[save_settings] Successfully saved settings for {integration_type}")
|
||||
return success_response(
|
||||
@@ -786,8 +925,62 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
request=request
|
||||
)
|
||||
|
||||
def _get_global_defaults(self, integration_type):
|
||||
"""Get global defaults from GlobalIntegrationSettings"""
|
||||
try:
|
||||
from .global_settings_models import GlobalIntegrationSettings
|
||||
global_settings = GlobalIntegrationSettings.objects.first()
|
||||
|
||||
if not global_settings:
|
||||
return {}
|
||||
|
||||
defaults = {}
|
||||
|
||||
# Map integration_type to GlobalIntegrationSettings fields
|
||||
if integration_type == 'openai':
|
||||
defaults = {
|
||||
'apiKey': global_settings.openai_api_key or '',
|
||||
'model': global_settings.openai_model.model_name if global_settings.openai_model else 'gpt-4o-mini',
|
||||
'temperature': float(global_settings.openai_temperature or 0.7),
|
||||
'maxTokens': int(global_settings.openai_max_tokens or 8192),
|
||||
}
|
||||
elif integration_type == 'runware':
|
||||
defaults = {
|
||||
'apiKey': global_settings.runware_api_key or '',
|
||||
'model': global_settings.runware_model.model_name if global_settings.runware_model else 'runware:97@1',
|
||||
}
|
||||
elif integration_type == 'image_generation':
|
||||
provider = global_settings.default_image_service or 'openai'
|
||||
# Get model based on provider
|
||||
if provider == 'openai':
|
||||
model = global_settings.dalle_model.model_name if global_settings.dalle_model else 'dall-e-3'
|
||||
else: # runware
|
||||
model = global_settings.runware_model.model_name if global_settings.runware_model else 'runware:97@1'
|
||||
|
||||
defaults = {
|
||||
'provider': provider,
|
||||
'service': provider, # Alias
|
||||
'model': model,
|
||||
'imageModel': model if provider == 'openai' else None,
|
||||
'runwareModel': model if provider == 'runware' else None,
|
||||
'image_type': global_settings.image_style or 'vivid',
|
||||
'image_quality': global_settings.image_quality or 'standard',
|
||||
'max_in_article_images': global_settings.max_in_article_images or 5,
|
||||
'desktop_image_size': global_settings.desktop_image_size or '1024x1024',
|
||||
'mobile_image_size': global_settings.mobile_image_size or '512x512',
|
||||
'featured_image_size': global_settings.desktop_image_size or '1024x1024',
|
||||
'desktop_enabled': True,
|
||||
'mobile_enabled': True,
|
||||
}
|
||||
|
||||
logger.info(f"[_get_global_defaults] {integration_type} defaults: {defaults}")
|
||||
return defaults
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting global defaults for {integration_type}: {e}", exc_info=True)
|
||||
return {}
|
||||
|
||||
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 - merges global defaults with account-specific overrides"""
|
||||
integration_type = pk
|
||||
|
||||
if not integration_type:
|
||||
@@ -798,45 +991,130 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
)
|
||||
|
||||
try:
|
||||
# Get account - try multiple methods (same as save_settings)
|
||||
account = getattr(request, 'account', None)
|
||||
# CRITICAL FIX: Always get account from authenticated user, not from request.account
|
||||
# Match the pattern used in save_settings() for consistency
|
||||
user = getattr(request, 'user', None)
|
||||
if not user or not hasattr(user, 'is_authenticated') or not user.is_authenticated:
|
||||
logger.error(f"[get_settings] User not authenticated")
|
||||
return error_response(
|
||||
error='Authentication required',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
# 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:
|
||||
try:
|
||||
account = getattr(user, 'account', None)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting account from user: {e}")
|
||||
account = None
|
||||
# Get account directly from user.account relationship
|
||||
account = getattr(user, 'account', None)
|
||||
logger.info(f"[get_settings] Account from user.account: {account.id if account else None}")
|
||||
|
||||
from .models import IntegrationSettings
|
||||
|
||||
# Get account-specific settings
|
||||
# Start with global defaults
|
||||
global_defaults = self._get_global_defaults(integration_type)
|
||||
|
||||
# Get account-specific settings and merge
|
||||
# Get account-specific enabled state from IntegrationState (single record)
|
||||
from igny8_core.ai.models import IntegrationState
|
||||
|
||||
# Map integration_type to field name
|
||||
field_map = {
|
||||
'openai': 'is_openai_enabled',
|
||||
'runware': 'is_runware_enabled',
|
||||
'image_generation': 'is_image_generation_enabled',
|
||||
}
|
||||
|
||||
account_enabled = None
|
||||
if account:
|
||||
try:
|
||||
integration_state = IntegrationState.objects.get(account=account)
|
||||
field_name = field_map.get(integration_type)
|
||||
if field_name:
|
||||
account_enabled = getattr(integration_state, field_name)
|
||||
logger.info(f"[get_settings] Found IntegrationState.{field_name}={account_enabled}")
|
||||
except IntegrationState.DoesNotExist:
|
||||
logger.info(f"[get_settings] No IntegrationState found, will use global default")
|
||||
|
||||
# Try to get account-specific config overrides
|
||||
if account:
|
||||
try:
|
||||
integration_settings = IntegrationSettings.objects.get(
|
||||
integration_type=integration_type,
|
||||
account=account
|
||||
)
|
||||
# Merge: global defaults + account overrides
|
||||
merged_config = {**global_defaults, **integration_settings.config}
|
||||
|
||||
# Use account-specific enabled state if available, otherwise use global
|
||||
if account_enabled is not None:
|
||||
enabled_state = account_enabled
|
||||
else:
|
||||
# Fall back to global enabled logic
|
||||
try:
|
||||
from .global_settings_models import GlobalIntegrationSettings
|
||||
global_settings = GlobalIntegrationSettings.objects.first()
|
||||
if global_settings:
|
||||
if integration_type == 'openai':
|
||||
enabled_state = bool(global_settings.openai_api_key)
|
||||
elif integration_type == 'runware':
|
||||
enabled_state = bool(global_settings.runware_api_key)
|
||||
elif integration_type == 'image_generation':
|
||||
enabled_state = bool(global_settings.openai_api_key or global_settings.runware_api_key)
|
||||
else:
|
||||
enabled_state = False
|
||||
else:
|
||||
enabled_state = False
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking global enabled status: {e}")
|
||||
enabled_state = False
|
||||
|
||||
response_data = {
|
||||
'id': integration_settings.integration_type,
|
||||
'enabled': integration_settings.is_active,
|
||||
**integration_settings.config
|
||||
'enabled': enabled_state,
|
||||
**merged_config
|
||||
}
|
||||
logger.info(f"[get_settings] Merged settings for {integration_type}: enabled={enabled_state}")
|
||||
return success_response(
|
||||
data=response_data,
|
||||
request=request
|
||||
)
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
logger.info(f"[get_settings] No account settings, returning global defaults for {integration_type}")
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting account-specific settings: {e}", exc_info=True)
|
||||
|
||||
# Return empty config if no settings found
|
||||
# Return global defaults with account-specific enabled state if available
|
||||
# Determine if integration is "enabled" based on IntegrationState or global configuration
|
||||
if account_enabled is not None:
|
||||
is_enabled = account_enabled
|
||||
logger.info(f"[get_settings] Using account IntegrationState: enabled={is_enabled}")
|
||||
else:
|
||||
try:
|
||||
from .global_settings_models import GlobalIntegrationSettings
|
||||
global_settings = GlobalIntegrationSettings.objects.first()
|
||||
|
||||
# Check if global API keys are configured
|
||||
is_enabled = False
|
||||
if global_settings:
|
||||
if integration_type == 'openai':
|
||||
is_enabled = bool(global_settings.openai_api_key)
|
||||
elif integration_type == 'runware':
|
||||
is_enabled = bool(global_settings.runware_api_key)
|
||||
elif integration_type == 'image_generation':
|
||||
# Image generation is enabled if either OpenAI or Runware is configured
|
||||
is_enabled = bool(global_settings.openai_api_key or global_settings.runware_api_key)
|
||||
|
||||
logger.info(f"[get_settings] Using global enabled status: enabled={is_enabled} (no account override)")
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking global enabled status: {e}")
|
||||
is_enabled = False
|
||||
|
||||
response_data = {
|
||||
'id': integration_type,
|
||||
'enabled': is_enabled,
|
||||
**global_defaults
|
||||
}
|
||||
return success_response(
|
||||
data={},
|
||||
data=response_data,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -849,23 +1127,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='image_generation', url_name='image_generation_settings')
|
||||
def get_image_generation_settings(self, request):
|
||||
"""Get image generation settings for current account
|
||||
Normal users fallback to system account (aws-admin) settings
|
||||
"""
|
||||
account = getattr(request, 'account', None)
|
||||
|
||||
if not account:
|
||||
# Fallback to user's 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
|
||||
"""Get image generation settings for current account - merges global defaults with account overrides"""
|
||||
# CRITICAL FIX: Always use user.account directly, never request.account or default account
|
||||
user = getattr(request, 'user', None)
|
||||
account = None
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
if not account:
|
||||
return error_response(
|
||||
@@ -876,42 +1143,26 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
|
||||
try:
|
||||
from .models import IntegrationSettings
|
||||
from igny8_core.auth.models import Account
|
||||
|
||||
# Try to get settings for user's account first
|
||||
# Start with global defaults
|
||||
global_defaults = self._get_global_defaults('image_generation')
|
||||
|
||||
# Try to get account-specific settings
|
||||
try:
|
||||
integration = IntegrationSettings.objects.get(
|
||||
account=account,
|
||||
integration_type='image_generation',
|
||||
is_active=True
|
||||
)
|
||||
logger.info(f"[get_image_generation_settings] Found settings for account {account.id}")
|
||||
config = {**global_defaults, **(integration.config or {})}
|
||||
logger.info(f"[get_image_generation_settings] Found account settings, merged with globals")
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
# Fallback to system account (aws-admin) settings - normal users use centralized settings
|
||||
logger.info(f"[get_image_generation_settings] No settings for account {account.id}, falling back to system account")
|
||||
try:
|
||||
system_account = Account.objects.get(slug='aws-admin')
|
||||
integration = IntegrationSettings.objects.get(
|
||||
account=system_account,
|
||||
integration_type='image_generation',
|
||||
is_active=True
|
||||
)
|
||||
logger.info(f"[get_image_generation_settings] Using system account (aws-admin) settings")
|
||||
except (Account.DoesNotExist, IntegrationSettings.DoesNotExist):
|
||||
logger.error("[get_image_generation_settings] No image generation settings found in aws-admin account")
|
||||
return error_response(
|
||||
error='Image generation settings not configured in aws-admin account',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
config = integration.config or {}
|
||||
# Use global defaults only
|
||||
config = global_defaults
|
||||
logger.info(f"[get_image_generation_settings] No account settings, using global defaults")
|
||||
|
||||
# Debug: Log what's actually in the config
|
||||
logger.info(f"[get_image_generation_settings] Full config: {config}")
|
||||
logger.info(f"[get_image_generation_settings] Config keys: {list(config.keys())}")
|
||||
logger.info(f"[get_image_generation_settings] model field: {config.get('model')}")
|
||||
logger.info(f"[get_image_generation_settings] imageModel field: {config.get('imageModel')}")
|
||||
logger.info(f"[get_image_generation_settings] Final config: {config}")
|
||||
|
||||
# Get model - try 'model' first, then 'imageModel' as fallback
|
||||
model = config.get('model') or config.get('imageModel') or 'dall-e-3'
|
||||
@@ -936,12 +1187,6 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
},
|
||||
request=request
|
||||
)
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
return error_response(
|
||||
error='Image generation settings not configured',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[get_image_generation_settings] Error: {str(e)}", exc_info=True)
|
||||
return error_response(
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-23 08:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0003_globalmodulesettings'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GlobalAIPrompt',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('prompt_type', models.CharField(choices=[('clustering', 'Clustering'), ('ideas', 'Ideas Generation'), ('content_generation', 'Content Generation'), ('image_prompt_extraction', 'Image Prompt Extraction'), ('image_prompt_template', 'Image Prompt Template'), ('negative_prompt', 'Negative Prompt'), ('site_structure_generation', 'Site Structure Generation'), ('product_generation', 'Product Content Generation'), ('service_generation', 'Service Page Generation'), ('taxonomy_generation', 'Taxonomy Generation')], help_text='Type of AI operation this prompt is for', max_length=50, unique=True)),
|
||||
('prompt_value', models.TextField(help_text='Default prompt template')),
|
||||
('description', models.TextField(blank=True, help_text='Description of what this prompt does')),
|
||||
('variables', models.JSONField(blank=True, default=list, help_text='Optional: List of variables used in the prompt (e.g., {keyword}, {industry})')),
|
||||
('is_active', models.BooleanField(db_index=True, default=True)),
|
||||
('version', models.IntegerField(default=1, help_text='Prompt version for tracking changes')),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global AI Prompt',
|
||||
'verbose_name_plural': 'Global AI Prompts',
|
||||
'db_table': 'igny8_global_ai_prompts',
|
||||
'ordering': ['prompt_type'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GlobalAuthorProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text="Profile name (e.g., 'SaaS B2B Professional')", max_length=255, unique=True)),
|
||||
('description', models.TextField(help_text='Description of the writing style')),
|
||||
('tone', models.CharField(help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical')", max_length=100)),
|
||||
('language', models.CharField(default='en', help_text='Language code', max_length=50)),
|
||||
('structure_template', models.JSONField(default=dict, help_text='Structure template defining content sections')),
|
||||
('category', models.CharField(choices=[('saas', 'SaaS/B2B'), ('ecommerce', 'E-commerce'), ('blog', 'Blog/Publishing'), ('technical', 'Technical'), ('creative', 'Creative'), ('news', 'News/Media'), ('academic', 'Academic')], help_text='Profile category', max_length=50)),
|
||||
('is_active', models.BooleanField(db_index=True, default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global Author Profile',
|
||||
'verbose_name_plural': 'Global Author Profiles',
|
||||
'db_table': 'igny8_global_author_profiles',
|
||||
'ordering': ['category', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GlobalStrategy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Strategy name', max_length=255, unique=True)),
|
||||
('description', models.TextField(help_text='Description of the content strategy')),
|
||||
('prompt_types', models.JSONField(default=list, help_text='List of prompt types to use')),
|
||||
('section_logic', models.JSONField(default=dict, help_text='Section logic configuration')),
|
||||
('category', models.CharField(choices=[('blog', 'Blog Content'), ('ecommerce', 'E-commerce'), ('saas', 'SaaS/B2B'), ('news', 'News/Media'), ('technical', 'Technical Documentation'), ('marketing', 'Marketing Content')], help_text='Strategy category', max_length=50)),
|
||||
('is_active', models.BooleanField(db_index=True, default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global Strategy',
|
||||
'verbose_name_plural': 'Global Strategies',
|
||||
'db_table': 'igny8_global_strategies',
|
||||
'ordering': ['category', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GlobalIntegrationSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('openai_api_key', models.CharField(blank=True, help_text='Platform OpenAI API key - used by ALL accounts', max_length=500)),
|
||||
('openai_model', 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)),
|
||||
('openai_temperature', models.FloatField(default=0.7, help_text='Default temperature 0.0-2.0 (accounts can override if plan allows)')),
|
||||
('openai_max_tokens', models.IntegerField(default=8192, help_text='Default max tokens for responses (accounts can override if plan allows)')),
|
||||
('dalle_api_key', models.CharField(blank=True, help_text='Platform DALL-E API key - used by ALL accounts (can be same as OpenAI)', max_length=500)),
|
||||
('dalle_model', 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)),
|
||||
('dalle_size', 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)),
|
||||
('runware_api_key', models.CharField(blank=True, help_text='Platform Runware API key - used by ALL accounts', max_length=500)),
|
||||
('runware_model', 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)),
|
||||
('default_image_service', 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)),
|
||||
('image_quality', 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)),
|
||||
('image_style', 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)),
|
||||
('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(default='1024x1024', help_text='Default desktop image size (accounts can override if plan allows)', max_length=20)),
|
||||
('mobile_image_size', models.CharField(default='512x512', help_text='Default mobile image size (accounts can override if plan allows)', max_length=20)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='global_settings_updates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global Integration Settings',
|
||||
'verbose_name_plural': 'Global Integration Settings',
|
||||
'db_table': 'igny8_global_integration_settings',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,183 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-23 (custom data migration)
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def migrate_model_strings_to_fks(apps, schema_editor):
|
||||
"""Convert CharField model identifiers to ForeignKey references"""
|
||||
GlobalIntegrationSettings = apps.get_model('system', 'GlobalIntegrationSettings')
|
||||
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
|
||||
|
||||
# Get the singleton GlobalIntegrationSettings instance
|
||||
try:
|
||||
settings = GlobalIntegrationSettings.objects.first()
|
||||
if not settings:
|
||||
print(" No GlobalIntegrationSettings found, skipping data migration")
|
||||
return
|
||||
|
||||
# Map openai_model string to AIModelConfig FK
|
||||
if settings.openai_model_old:
|
||||
model_name = settings.openai_model_old
|
||||
# Try to find matching model
|
||||
openai_model = AIModelConfig.objects.filter(
|
||||
model_name=model_name,
|
||||
provider='openai',
|
||||
model_type='text'
|
||||
).first()
|
||||
if openai_model:
|
||||
settings.openai_model_new = openai_model
|
||||
print(f" ✓ Mapped openai_model: {model_name} → {openai_model.id}")
|
||||
else:
|
||||
# Try gpt-4o-mini as fallback
|
||||
openai_model = AIModelConfig.objects.filter(
|
||||
model_name='gpt-4o-mini',
|
||||
provider='openai',
|
||||
model_type='text'
|
||||
).first()
|
||||
if openai_model:
|
||||
settings.openai_model_new = openai_model
|
||||
print(f" ⚠ Could not find {model_name}, using fallback: gpt-4o-mini")
|
||||
|
||||
# Map dalle_model string to AIModelConfig FK
|
||||
if settings.dalle_model_old:
|
||||
model_name = settings.dalle_model_old
|
||||
dalle_model = AIModelConfig.objects.filter(
|
||||
model_name=model_name,
|
||||
provider='openai',
|
||||
model_type='image'
|
||||
).first()
|
||||
if dalle_model:
|
||||
settings.dalle_model_new = dalle_model
|
||||
print(f" ✓ Mapped dalle_model: {model_name} → {dalle_model.id}")
|
||||
else:
|
||||
# Try dall-e-3 as fallback
|
||||
dalle_model = AIModelConfig.objects.filter(
|
||||
model_name='dall-e-3',
|
||||
provider='openai',
|
||||
model_type='image'
|
||||
).first()
|
||||
if dalle_model:
|
||||
settings.dalle_model_new = dalle_model
|
||||
print(f" ⚠ Could not find {model_name}, using fallback: dall-e-3")
|
||||
|
||||
# Map runware_model string to AIModelConfig FK
|
||||
if settings.runware_model_old:
|
||||
model_name = settings.runware_model_old
|
||||
# Runware models might have different naming
|
||||
runware_model = AIModelConfig.objects.filter(
|
||||
provider='runware',
|
||||
model_type='image'
|
||||
).first() # Just get first active Runware model
|
||||
if runware_model:
|
||||
settings.runware_model_new = runware_model
|
||||
print(f" ✓ Mapped runware_model: {model_name} → {runware_model.id}")
|
||||
|
||||
settings.save()
|
||||
print(" ✅ Data migration complete")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠ Error during data migration: {e}")
|
||||
# Don't fail the migration, let admin fix it manually
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0019_add_ai_model_config'),
|
||||
('system', '0004_add_global_integration_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Step 1: Add new FK fields with temporary names
|
||||
migrations.AddField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='openai_model_new',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Default text generation model (accounts can override if plan allows)',
|
||||
limit_choices_to={'is_active': True, 'model_type': 'text', 'provider': 'openai'},
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='global_openai_text_model_new',
|
||||
to='billing.aimodelconfig'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_model_new',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Default DALL-E model (accounts can override if plan allows)',
|
||||
limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'openai'},
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='global_dalle_model_new',
|
||||
to='billing.aimodelconfig'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='runware_model_new',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Default Runware model (accounts can override if plan allows)',
|
||||
limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'runware'},
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='global_runware_model_new',
|
||||
to='billing.aimodelconfig'
|
||||
),
|
||||
),
|
||||
|
||||
# Step 2: Rename old CharField fields
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='openai_model',
|
||||
new_name='openai_model_old',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='dalle_model',
|
||||
new_name='dalle_model_old',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='runware_model',
|
||||
new_name='runware_model_old',
|
||||
),
|
||||
|
||||
# Step 3: Run data migration
|
||||
migrations.RunPython(migrate_model_strings_to_fks, migrations.RunPython.noop),
|
||||
|
||||
# Step 4: Remove old CharField fields
|
||||
migrations.RemoveField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='openai_model_old',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_model_old',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='runware_model_old',
|
||||
),
|
||||
|
||||
# Step 5: Rename new FK fields to final names
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='openai_model_new',
|
||||
new_name='openai_model',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='dalle_model_new',
|
||||
new_name='dalle_model',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='runware_model_new',
|
||||
new_name='runware_model',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-23 14:24
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0020_add_optimizer_publisher_timestamps'),
|
||||
('system', '0005_link_global_settings_to_aimodelconfig'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='globalmodulesettings',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='globalmodulesettings',
|
||||
name='optimizer_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Enable Optimizer module platform-wide'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='globalmodulesettings',
|
||||
name='publisher_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Enable Publisher module platform-wide'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='globalmodulesettings',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_model',
|
||||
field=models.ForeignKey(blank=True, help_text='Default DALL-E model (accounts can override if plan allows)', limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'openai'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='global_dalle_model', to='billing.aimodelconfig'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='openai_model',
|
||||
field=models.ForeignKey(blank=True, help_text='Default text generation model (accounts can override if plan allows)', limit_choices_to={'is_active': True, 'model_type': 'text', 'provider': 'openai'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='global_openai_text_model', to='billing.aimodelconfig'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='runware_model',
|
||||
field=models.ForeignKey(blank=True, help_text='Default Runware model (accounts can override if plan allows)', limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'runware'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='global_runware_model', to='billing.aimodelconfig'),
|
||||
),
|
||||
]
|
||||
194
backend/seed_correct_ai_models.py
Normal file
194
backend/seed_correct_ai_models.py
Normal file
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Seed AIModelConfig with the CORRECT models from GlobalIntegrationSettings choices.
|
||||
These are the models that should be available in the dropdowns.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# Setup Django
|
||||
sys.path.insert(0, '/app')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
|
||||
django.setup()
|
||||
|
||||
from decimal import Decimal
|
||||
from igny8_core.business.billing.models import AIModelConfig
|
||||
|
||||
def seed_models():
|
||||
"""Create AIModelConfig records for all models that were in GlobalIntegrationSettings"""
|
||||
|
||||
models_to_create = [
|
||||
# OpenAI Text Models (from OPENAI_MODEL_CHOICES)
|
||||
{
|
||||
'model_name': 'gpt-4.1',
|
||||
'display_name': 'GPT-4.1',
|
||||
'provider': 'openai',
|
||||
'model_type': 'text',
|
||||
'cost_per_1k_input_tokens': Decimal('0.002'), # $2.00 per 1M = $0.002 per 1K
|
||||
'cost_per_1k_output_tokens': Decimal('0.008'), # $8.00 per 1M
|
||||
'tokens_per_credit': 100,
|
||||
'is_active': True,
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-4o-mini',
|
||||
'display_name': 'GPT-4o Mini',
|
||||
'provider': 'openai',
|
||||
'model_type': 'text',
|
||||
'cost_per_1k_input_tokens': Decimal('0.00015'), # $0.15 per 1M
|
||||
'cost_per_1k_output_tokens': Decimal('0.0006'), # $0.60 per 1M
|
||||
'tokens_per_credit': 100,
|
||||
'is_active': True,
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-4o',
|
||||
'display_name': 'GPT-4o',
|
||||
'provider': 'openai',
|
||||
'model_type': 'text',
|
||||
'cost_per_1k_input_tokens': Decimal('0.0025'), # $2.50 per 1M
|
||||
'cost_per_1k_output_tokens': Decimal('0.01'), # $10.00 per 1M
|
||||
'tokens_per_credit': 100,
|
||||
'is_active': True,
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-4-turbo-preview',
|
||||
'display_name': 'GPT-4 Turbo Preview',
|
||||
'provider': 'openai',
|
||||
'model_type': 'text',
|
||||
'cost_per_1k_input_tokens': Decimal('0.01'), # $10.00 per 1M
|
||||
'cost_per_1k_output_tokens': Decimal('0.03'), # $30.00 per 1M
|
||||
'tokens_per_credit': 100,
|
||||
'is_active': True,
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-5.1',
|
||||
'display_name': 'GPT-5.1 (16K)',
|
||||
'provider': 'openai',
|
||||
'model_type': 'text',
|
||||
'cost_per_1k_input_tokens': Decimal('0.00125'), # $1.25 per 1M
|
||||
'cost_per_1k_output_tokens': Decimal('0.01'), # $10.00 per 1M
|
||||
'tokens_per_credit': 100,
|
||||
'is_active': True,
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-5.2',
|
||||
'display_name': 'GPT-5.2 (16K)',
|
||||
'provider': 'openai',
|
||||
'model_type': 'text',
|
||||
'cost_per_1k_input_tokens': Decimal('0.00175'), # $1.75 per 1M
|
||||
'cost_per_1k_output_tokens': Decimal('0.014'), # $14.00 per 1M
|
||||
'tokens_per_credit': 100,
|
||||
'is_active': True,
|
||||
},
|
||||
|
||||
# OpenAI Image Models (from DALLE_MODEL_CHOICES)
|
||||
{
|
||||
'model_name': 'dall-e-3',
|
||||
'display_name': 'DALL·E 3',
|
||||
'provider': 'openai',
|
||||
'model_type': 'image',
|
||||
'cost_per_1k_input_tokens': Decimal('0.04'), # $0.040 per image
|
||||
'cost_per_1k_output_tokens': Decimal('0.00'),
|
||||
'tokens_per_credit': 1, # 1 image = 1 unit
|
||||
'is_active': True,
|
||||
},
|
||||
{
|
||||
'model_name': 'dall-e-2',
|
||||
'display_name': 'DALL·E 2',
|
||||
'provider': 'openai',
|
||||
'model_type': 'image',
|
||||
'cost_per_1k_input_tokens': Decimal('0.02'), # $0.020 per image
|
||||
'cost_per_1k_output_tokens': Decimal('0.00'),
|
||||
'tokens_per_credit': 1,
|
||||
'is_active': True,
|
||||
},
|
||||
|
||||
# Runware Image Models (from RUNWARE_MODEL_CHOICES)
|
||||
{
|
||||
'model_name': 'runware:97@1',
|
||||
'display_name': 'Runware 97@1 (Versatile)',
|
||||
'provider': 'runware',
|
||||
'model_type': 'image',
|
||||
'cost_per_1k_input_tokens': Decimal('0.005'), # Estimated
|
||||
'cost_per_1k_output_tokens': Decimal('0.00'),
|
||||
'tokens_per_credit': 1,
|
||||
'is_active': True,
|
||||
},
|
||||
{
|
||||
'model_name': 'runware:100@1',
|
||||
'display_name': 'Runware 100@1 (High Quality)',
|
||||
'provider': 'runware',
|
||||
'model_type': 'image',
|
||||
'cost_per_1k_input_tokens': Decimal('0.008'), # Estimated
|
||||
'cost_per_1k_output_tokens': Decimal('0.00'),
|
||||
'tokens_per_credit': 1,
|
||||
'is_active': True,
|
||||
},
|
||||
{
|
||||
'model_name': 'runware:101@1',
|
||||
'display_name': 'Runware 101@1 (Fast)',
|
||||
'provider': 'runware',
|
||||
'model_type': 'image',
|
||||
'cost_per_1k_input_tokens': Decimal('0.003'), # Estimated
|
||||
'cost_per_1k_output_tokens': Decimal('0.00'),
|
||||
'tokens_per_credit': 1,
|
||||
'is_active': True,
|
||||
},
|
||||
]
|
||||
|
||||
print("Seeding AIModelConfig with correct models...")
|
||||
print("=" * 70)
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
for model_data in models_to_create:
|
||||
model, created = AIModelConfig.objects.update_or_create(
|
||||
model_name=model_data['model_name'],
|
||||
provider=model_data['provider'],
|
||||
defaults=model_data
|
||||
)
|
||||
|
||||
if created:
|
||||
created_count += 1
|
||||
print(f"✓ Created: {model.display_name} ({model.model_name})")
|
||||
else:
|
||||
updated_count += 1
|
||||
print(f"↻ Updated: {model.display_name} ({model.model_name})")
|
||||
|
||||
print("=" * 70)
|
||||
print(f"Summary: {created_count} created, {updated_count} updated")
|
||||
|
||||
# Set default models
|
||||
print("\nSetting default models...")
|
||||
|
||||
# Default text model: gpt-4o-mini
|
||||
default_text = AIModelConfig.objects.filter(model_name='gpt-4o-mini').first()
|
||||
if default_text:
|
||||
AIModelConfig.objects.filter(model_type='text').update(is_default=False)
|
||||
default_text.is_default = True
|
||||
default_text.save()
|
||||
print(f"✓ Default text model: {default_text.display_name}")
|
||||
|
||||
# Default image model: dall-e-3
|
||||
default_image = AIModelConfig.objects.filter(model_name='dall-e-3').first()
|
||||
if default_image:
|
||||
AIModelConfig.objects.filter(model_type='image').update(is_default=False)
|
||||
default_image.is_default = True
|
||||
default_image.save()
|
||||
print(f"✓ Default image model: {default_image.display_name}")
|
||||
|
||||
print("\n✅ Seeding complete!")
|
||||
|
||||
# Show summary
|
||||
print("\nActive models by type:")
|
||||
print("-" * 70)
|
||||
for model_type in ['text', 'image']:
|
||||
models = AIModelConfig.objects.filter(model_type=model_type, is_active=True)
|
||||
print(f"\n{model_type.upper()}: {models.count()} models")
|
||||
for m in models:
|
||||
default = " [DEFAULT]" if m.is_default else ""
|
||||
print(f" - {m.display_name} ({m.model_name}) - {m.provider}{default}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
seed_models()
|
||||
Reference in New Issue
Block a user