feat(migrations): Rename indexes and update global integration settings fields for improved clarity and functionality

feat(admin): Add API monitoring, debug console, and system health templates for enhanced admin interface

docs: Add AI system cleanup summary and audit report detailing architecture, token management, and recommendations

docs: Introduce credits and tokens system guide outlining configuration, data flow, and monitoring strategies
This commit is contained in:
IGNY8 VPS (Salman)
2025-12-20 12:55:05 +00:00
parent eb6cba7920
commit 3283a83b42
51 changed files with 3578 additions and 5434 deletions

View File

@@ -1,3 +1,19 @@
"""
IGNY8 System Module
"""
# Avoid circular imports - don't import models at module level
# Models are automatically discovered by Django
__all__ = [
# Account-based models
'AIPrompt',
'IntegrationSettings',
'AuthorProfile',
'Strategy',
# Global settings models
'GlobalIntegrationSettings',
'AccountIntegrationOverride',
'GlobalAIPrompt',
'GlobalAuthorProfile',
'GlobalStrategy',
]

View File

@@ -5,6 +5,12 @@ 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 (
GlobalIntegrationSettings,
GlobalAIPrompt,
GlobalAuthorProfile,
GlobalStrategy,
)
from django.contrib import messages
from import_export.admin import ExportMixin, ImportExportMixin
@@ -52,8 +58,8 @@ except ImportError:
@admin.register(AIPrompt)
class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
resource_class = AIPromptResource
list_display = ['id', 'prompt_type', 'account', 'is_active', 'updated_at']
list_filter = ['prompt_type', 'is_active', 'account']
list_display = ['id', 'prompt_type', 'account', 'is_customized', 'is_active', 'updated_at']
list_filter = ['prompt_type', 'is_active', 'is_customized', 'account']
search_fields = ['prompt_type']
readonly_fields = ['created_at', 'updated_at', 'default_prompt']
actions = [
@@ -64,10 +70,11 @@ class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
fieldsets = (
('Basic Info', {
'fields': ('account', 'prompt_type', 'is_active')
'fields': ('account', 'prompt_type', 'is_active', 'is_customized')
}),
('Prompt Content', {
'fields': ('prompt_value', 'default_prompt')
'fields': ('prompt_value', 'default_prompt'),
'description': 'Customize prompt_value or reset to default_prompt'
}),
('Timestamps', {
'fields': ('created_at', 'updated_at')
@@ -94,14 +101,14 @@ class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
bulk_deactivate.short_description = 'Deactivate selected prompts'
def bulk_reset_to_default(self, request, queryset):
"""Reset selected prompts to their global defaults"""
count = 0
for prompt in queryset:
if prompt.default_prompt:
prompt.prompt_value = prompt.default_prompt
prompt.save()
prompt.reset_to_default()
count += 1
self.message_user(request, f'{count} AI prompt(s) reset to default values.', messages.SUCCESS)
bulk_reset_to_default.short_description = 'Reset to default values'
self.message_user(request, f'{count} prompt(s) reset to default.', messages.SUCCESS)
bulk_reset_to_default.short_description = 'Reset selected prompts to global default'
class IntegrationSettingsResource(resources.ModelResource):
@@ -114,36 +121,42 @@ class IntegrationSettingsResource(resources.ModelResource):
@admin.register(IntegrationSettings)
class IntegrationSettingsAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
"""
Admin for per-account integration setting overrides.
IMPORTANT: This stores ONLY model selection and parameter overrides.
API keys come from GlobalIntegrationSettings and cannot be overridden.
Free plan users cannot create these - they must use global defaults.
"""
resource_class = IntegrationSettingsResource
list_display = ['id', 'integration_type', 'account', 'is_active', 'updated_at']
list_filter = ['integration_type', 'is_active', 'account']
search_fields = ['integration_type']
search_fields = ['integration_type', 'account__name']
readonly_fields = ['created_at', 'updated_at']
actions = [
'bulk_activate',
'bulk_deactivate',
'bulk_test_connection',
]
fieldsets = (
('Basic Info', {
'fields': ('account', 'integration_type', 'is_active')
}),
('Configuration', {
('Configuration Overrides', {
'fields': ('config',),
'description': 'JSON configuration containing API keys and settings. Example: {"apiKey": "sk-...", "model": "gpt-4.1", "enabled": true}'
'description': (
'JSON overrides for model/parameter selection. '
'Fields: model, temperature, max_tokens, image_size, image_quality, etc. '
'Leave null to use global defaults. '
'Example: {"model": "gpt-4", "temperature": 0.8, "max_tokens": 4000} '
'WARNING: NEVER store API keys here - they come from GlobalIntegrationSettings'
)
}),
('Timestamps', {
'fields': ('created_at', 'updated_at')
}),
)
def get_readonly_fields(self, request, obj=None):
"""Make config readonly when viewing to prevent accidental exposure"""
if obj: # Editing existing object
return self.readonly_fields + ['config']
return self.readonly_fields
def get_account_display(self, obj):
"""Safely get account name"""
try:
@@ -312,4 +325,119 @@ class StrategyAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
strategy_copy.save()
count += 1
self.message_user(request, f'{count} strategy/strategies cloned.', messages.SUCCESS)
bulk_clone.short_description = 'Clone selected strategies'
bulk_clone.short_description = 'Clone selected strategies'
# =============================================================================
# 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)"
}),
("DALL-E Settings", {
"fields": ("dalle_api_key", "dalle_model", "dalle_size", "dalle_quality", "dalle_style"),
"description": "Global DALL-E image generation configuration"
}),
("Anthropic Settings", {
"fields": ("anthropic_api_key", "anthropic_model"),
"description": "Global Anthropic Claude configuration"
}),
("Runware Settings", {
"fields": ("runware_api_key",),
"description": "Global Runware image generation configuration"
}),
("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):
"""Dont 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")
}),
)

View File

@@ -0,0 +1,270 @@
"""
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 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
- 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.
"""
# 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.CharField(
max_length=100,
default='gpt-4-turbo-preview',
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)"
)
# DALL-E Settings (for image generation)
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.CharField(
max_length=100,
default='dall-e-3',
help_text="Default DALL-E model (accounts can override if plan allows)"
)
dalle_size = models.CharField(
max_length=20,
default='1024x1024',
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)
runware_api_key = models.CharField(
max_length=500,
blank=True,
help_text="Platform Runware API key - used by ALL accounts"
)
# 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,
help_text="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()})"

View File

@@ -10,7 +10,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view
from igny8_core.api.base import AccountModelViewSet
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
from django.conf import settings
logger = logging.getLogger(__name__)
@@ -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
Integration settings configured through Django admin interface.
Normal users can view settings but only Admin/Owner roles can modify.
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.
Individual actions override with IsAdminOrOwner where needed (save, test).
task_progress and get_image_generation_settings accessible to all authenticated users.
"""
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
@@ -46,11 +46,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
"""
Override permissions based on action.
- list, retrieve: authenticated users with tenant access (read-only)
- update, save, test: system accounts/developers only (write operations)
- update, save, test: Admin/Owner roles only (write operations)
- task_progress, get_image_generation_settings: all authenticated users
"""
if self.action in ['update', 'save_post', 'test_connection']:
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper]
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
else:
permission_classes = self.permission_classes
return [permission() for permission in permission_classes]
@@ -91,7 +91,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
return self.save_settings(request, pk)
@action(detail=True, methods=['post'], url_path='test', url_name='test',
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper])
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner])
def test_connection(self, request, pk=None):
"""
Test API connection for OpenAI or Runware

View File

@@ -0,0 +1,186 @@
# Generated migration for global settings models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('system', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('igny8_core_auth', '0001_initial'),
]
operations = [
# Create GlobalIntegrationSettings
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='Global OpenAI API key used by all accounts (unless overridden)', max_length=500)),
('openai_model', models.CharField(default='gpt-4-turbo-preview', help_text='Default OpenAI model for text generation', max_length=100)),
('openai_temperature', models.FloatField(default=0.7, help_text='Temperature for OpenAI text generation (0.0 to 2.0)')),
('openai_max_tokens', models.IntegerField(default=4000, help_text='Maximum tokens for OpenAI responses')),
('dalle_api_key', models.CharField(blank=True, help_text='Global DALL-E API key (can be same as OpenAI key)', max_length=500)),
('dalle_model', models.CharField(default='dall-e-3', help_text='DALL-E model version', max_length=100)),
('dalle_size', models.CharField(default='1024x1024', help_text='Default image size for DALL-E', max_length=20)),
('dalle_quality', models.CharField(choices=[('standard', 'Standard'), ('hd', 'HD')], default='standard', help_text='Image quality for DALL-E 3', max_length=20)),
('dalle_style', models.CharField(choices=[('vivid', 'Vivid'), ('natural', 'Natural')], default='vivid', help_text='Image style for DALL-E 3', max_length=20)),
('anthropic_api_key', models.CharField(blank=True, help_text='Global Anthropic Claude API key', max_length=500)),
('anthropic_model', models.CharField(default='claude-3-sonnet-20240229', help_text='Default Anthropic Claude model', max_length=100)),
('runware_api_key', models.CharField(blank=True, help_text='Global Runware API key for image generation', max_length=500)),
('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',
},
),
# Create AccountIntegrationOverride
migrations.CreateModel(
name='AccountIntegrationOverride',
fields=[
('account', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='integration_override', serialize=False, to='igny8_core_auth.account')),
('use_own_keys', models.BooleanField(default=False, help_text="Use account's own API keys instead of global settings")),
('openai_api_key', models.CharField(blank=True, max_length=500, null=True)),
('openai_model', models.CharField(blank=True, max_length=100, null=True)),
('openai_temperature', models.FloatField(blank=True, null=True)),
('openai_max_tokens', models.IntegerField(blank=True, null=True)),
('dalle_api_key', models.CharField(blank=True, max_length=500, null=True)),
('dalle_model', models.CharField(blank=True, max_length=100, null=True)),
('dalle_size', models.CharField(blank=True, max_length=20, null=True)),
('dalle_quality', models.CharField(blank=True, max_length=20, null=True)),
('dalle_style', models.CharField(blank=True, max_length=20, null=True)),
('anthropic_api_key', models.CharField(blank=True, max_length=500, null=True)),
('anthropic_model', models.CharField(blank=True, max_length=100, null=True)),
('runware_api_key', models.CharField(blank=True, max_length=500, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Account Integration Override',
'verbose_name_plural': 'Account Integration Overrides',
'db_table': 'igny8_account_integration_override',
},
),
# Create GlobalAIPrompt
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(default=list, help_text='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'],
},
),
# Create GlobalAuthorProfile
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'],
},
),
# Create GlobalStrategy
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'],
},
),
# Update AIPrompt model - remove default_prompt, add is_customized
migrations.RemoveField(
model_name='aiprompt',
name='default_prompt',
),
migrations.AddField(
model_name='aiprompt',
name='is_customized',
field=models.BooleanField(default=False, help_text='True if account customized the prompt, False if using global default'),
),
migrations.AddIndex(
model_name='aiprompt',
index=models.Index(fields=['is_customized'], name='igny8_ai_pr_is_cust_idx'),
),
# Update AuthorProfile - add is_custom and cloned_from
migrations.AddField(
model_name='authorprofile',
name='is_custom',
field=models.BooleanField(default=False, help_text='True if created by account, False if cloned from global template'),
),
migrations.AddField(
model_name='authorprofile',
name='cloned_from',
field=models.ForeignKey(blank=True, help_text='Reference to the global template this was cloned from', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cloned_instances', to='system.globalauthorprofile'),
),
migrations.AddIndex(
model_name='authorprofile',
index=models.Index(fields=['is_custom'], name='igny8_autho_is_cust_idx'),
),
# Update Strategy - add is_custom and cloned_from
migrations.AddField(
model_name='strategy',
name='is_custom',
field=models.BooleanField(default=False, help_text='True if created by account, False if cloned from global template'),
),
migrations.AddField(
model_name='strategy',
name='cloned_from',
field=models.ForeignKey(blank=True, help_text='Reference to the global template this was cloned from', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cloned_instances', to='system.globalstrategy'),
),
migrations.AddIndex(
model_name='strategy',
index=models.Index(fields=['is_custom'], name='igny8_strat_is_cust_idx'),
),
]

View File

@@ -0,0 +1,108 @@
# Generated by Django 5.2.9 on 2025-12-20 12:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0003_fix_global_settings_architecture'),
]
operations = [
migrations.RenameIndex(
model_name='aiprompt',
new_name='igny8_ai_pr_is_cust_5d7a72_idx',
old_name='igny8_ai_pr_is_cust_idx',
),
migrations.RenameIndex(
model_name='authorprofile',
new_name='igny8_autho_is_cust_d163e6_idx',
old_name='igny8_autho_is_cust_idx',
),
migrations.RenameIndex(
model_name='strategy',
new_name='igny8_strat_is_cust_4b3c4b_idx',
old_name='igny8_strat_is_cust_idx',
),
migrations.AlterField(
model_name='aiprompt',
name='default_prompt',
field=models.TextField(blank=True, help_text='Global default prompt - used for reset to default'),
),
migrations.AlterField(
model_name='aiprompt',
name='prompt_value',
field=models.TextField(help_text='Current prompt text (customized or default)'),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='anthropic_api_key',
field=models.CharField(blank=True, help_text='Platform Anthropic API key - used by ALL accounts', max_length=500),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='anthropic_model',
field=models.CharField(default='claude-3-sonnet-20240229', help_text='Default Anthropic model (accounts can override if plan allows)', max_length=100),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='dalle_api_key',
field=models.CharField(blank=True, help_text='Platform DALL-E API key - used by ALL accounts (can be same as OpenAI)', max_length=500),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='dalle_model',
field=models.CharField(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_quality',
field=models.CharField(choices=[('standard', 'Standard'), ('hd', 'HD')], default='standard', help_text='Default image quality (accounts can override if plan allows)', max_length=20),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='dalle_size',
field=models.CharField(default='1024x1024', help_text='Default image size (accounts can override if plan allows)', max_length=20),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='dalle_style',
field=models.CharField(choices=[('vivid', 'Vivid'), ('natural', 'Natural')], default='vivid', help_text='Default image style (accounts can override if plan allows)', max_length=20),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='openai_api_key',
field=models.CharField(blank=True, help_text='Platform OpenAI API key - used by ALL accounts', max_length=500),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='openai_max_tokens',
field=models.IntegerField(default=8192, help_text='Default max tokens for responses (accounts can override if plan allows)'),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='openai_model',
field=models.CharField(default='gpt-4-turbo-preview', help_text='Default text generation model (accounts can override if plan allows)', max_length=100),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='openai_temperature',
field=models.FloatField(default=0.7, help_text='Default temperature 0.0-2.0 (accounts can override if plan allows)'),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='runware_api_key',
field=models.CharField(blank=True, help_text='Platform Runware API key - used by ALL accounts', max_length=500),
),
migrations.AlterField(
model_name='integrationsettings',
name='config',
field=models.JSONField(default=dict, help_text='Model and parameter overrides only. Fields: model, temperature, max_tokens, image_size, image_quality, etc. NULL = use global default. NEVER store API keys here.'),
),
migrations.AlterField(
model_name='integrationsettings',
name='integration_type',
field=models.CharField(choices=[('openai', 'OpenAI'), ('dalle', 'DALL-E'), ('anthropic', 'Anthropic'), ('runware', 'Runware')], db_index=True, max_length=50),
),
]

View File

@@ -11,7 +11,12 @@ from .settings_models import (
class AIPrompt(AccountBaseModel):
"""AI Prompt templates for various AI operations"""
"""
Account-specific AI Prompt templates.
Stores global default in default_prompt, current value in prompt_value.
When user saves an override, prompt_value changes but default_prompt stays.
Reset copies default_prompt back to prompt_value.
"""
PROMPT_TYPE_CHOICES = [
('clustering', 'Clustering'),
@@ -28,8 +33,15 @@ class AIPrompt(AccountBaseModel):
]
prompt_type = models.CharField(max_length=50, choices=PROMPT_TYPE_CHOICES, db_index=True)
prompt_value = models.TextField(help_text="The prompt template text")
default_prompt = models.TextField(help_text="Default prompt value (for reset)")
prompt_value = models.TextField(help_text="Current prompt text (customized or default)")
default_prompt = models.TextField(
blank=True,
help_text="Global default prompt - used for reset to default"
)
is_customized = models.BooleanField(
default=False,
help_text="True if account customized the prompt, False if using global default"
)
is_active = models.BooleanField(default=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
@@ -41,24 +53,77 @@ class AIPrompt(AccountBaseModel):
indexes = [
models.Index(fields=['prompt_type']),
models.Index(fields=['account', 'prompt_type']),
models.Index(fields=['is_customized']),
]
@classmethod
def get_effective_prompt(cls, account, prompt_type):
"""
Get the effective prompt for an account.
Returns account-specific prompt if exists and customized.
Otherwise returns global default.
"""
from .global_settings_models import GlobalAIPrompt
# Try to get account-specific prompt
try:
account_prompt = cls.objects.get(account=account, prompt_type=prompt_type, is_active=True)
# If customized, use account's version
if account_prompt.is_customized:
return account_prompt.prompt_value
# If not customized, use default_prompt from account record or global
return account_prompt.default_prompt or account_prompt.prompt_value
except cls.DoesNotExist:
pass
# Fallback to global prompt
try:
global_prompt = GlobalAIPrompt.objects.get(prompt_type=prompt_type, is_active=True)
return global_prompt.prompt_value
except GlobalAIPrompt.DoesNotExist:
return None
def reset_to_default(self):
"""Reset prompt to global default"""
if self.default_prompt:
self.prompt_value = self.default_prompt
self.is_customized = False
self.save()
def __str__(self):
return f"{self.get_prompt_type_display()}"
status = "Custom" if self.is_customized else "Default"
return f"{self.get_prompt_type_display()} ({status})"
class IntegrationSettings(AccountBaseModel):
"""Integration settings for OpenAI, Runware, GSC, etc."""
"""
Per-account integration settings overrides.
IMPORTANT: This model stores ONLY model selection and parameter overrides.
API keys are NEVER stored here - they come from GlobalIntegrationSettings.
Free plan: Cannot create overrides, must use global defaults
Starter/Growth/Scale plans: Can override model, temperature, tokens, image settings
NULL values in config mean "use global default"
"""
INTEGRATION_TYPE_CHOICES = [
('openai', 'OpenAI'),
('dalle', 'DALL-E'),
('anthropic', 'Anthropic'),
('runware', 'Runware'),
('gsc', 'Google Search Console'),
('image_generation', 'Image Generation Service'),
]
integration_type = models.CharField(max_length=50, choices=INTEGRATION_TYPE_CHOICES, db_index=True)
config = models.JSONField(default=dict, help_text="Integration configuration (API keys, settings, etc.)")
config = models.JSONField(
default=dict,
help_text=(
"Model and parameter overrides only. Fields: model, temperature, max_tokens, "
"image_size, image_quality, etc. NULL = use global default. "
"NEVER store API keys here."
)
)
is_active = models.BooleanField(default=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
@@ -80,6 +145,7 @@ class IntegrationSettings(AccountBaseModel):
class AuthorProfile(AccountBaseModel):
"""
Writing style profiles - tone, language, structure templates.
Can be cloned from global templates or created from scratch.
Examples: "SaaS B2B Informative", "E-commerce Product Descriptions", etc.
"""
name = models.CharField(max_length=255, help_text="Profile name (e.g., 'SaaS B2B Informative')")
@@ -93,6 +159,18 @@ class AuthorProfile(AccountBaseModel):
default=dict,
help_text="Structure template defining content sections and their order"
)
is_custom = models.BooleanField(
default=False,
help_text="True if created by account, False if cloned from global template"
)
cloned_from = models.ForeignKey(
'system.GlobalAuthorProfile',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='cloned_instances',
help_text="Reference to the global template this was cloned from"
)
is_active = models.BooleanField(default=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
@@ -105,16 +183,19 @@ class AuthorProfile(AccountBaseModel):
indexes = [
models.Index(fields=['account', 'is_active']),
models.Index(fields=['name']),
models.Index(fields=['is_custom']),
]
def __str__(self):
account = getattr(self, 'account', None)
return f"{self.name} ({account.name if account else 'No Account'})"
status = "Custom" if self.is_custom else "Template"
return f"{self.name} ({status}) - {account.name if account else 'No Account'}"
class Strategy(AccountBaseModel):
"""
Defined content strategies per sector, integrating prompt types, section logic, etc.
Can be cloned from global templates or created from scratch.
Links together prompts, author profiles, and sector-specific content strategies.
"""
name = models.CharField(max_length=255, help_text="Strategy name")
@@ -135,6 +216,18 @@ class Strategy(AccountBaseModel):
default=dict,
help_text="Section logic configuration defining content structure and flow"
)
is_custom = models.BooleanField(
default=False,
help_text="True if created by account, False if cloned from global template"
)
cloned_from = models.ForeignKey(
'system.GlobalStrategy',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='cloned_instances',
help_text="Reference to the global template this was cloned from"
)
is_active = models.BooleanField(default=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
@@ -148,8 +241,10 @@ class Strategy(AccountBaseModel):
models.Index(fields=['account', 'is_active']),
models.Index(fields=['account', 'sector']),
models.Index(fields=['name']),
models.Index(fields=['is_custom']),
]
def __str__(self):
sector_name = self.sector.name if self.sector else 'Global'
return f"{self.name} ({sector_name})"
status = "Custom" if self.is_custom else "Template"
return f"{self.name} ({status}) - {sector_name}"