222 lines
8.6 KiB
Python
222 lines
8.6 KiB
Python
"""
|
|
Prompt Registry - Centralized prompt management with override hierarchy
|
|
Supports: task-level overrides → DB prompts → GlobalAIPrompt (REQUIRED)
|
|
"""
|
|
import logging
|
|
from typing import Dict, Any, Optional
|
|
from django.db import models
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PromptRegistry:
|
|
"""
|
|
Centralized prompt registry with hierarchical resolution:
|
|
1. Task-level prompt_override (if exists)
|
|
2. DB prompt for (account, function)
|
|
3. GlobalAIPrompt (REQUIRED - no hardcoded fallbacks)
|
|
"""
|
|
|
|
# Removed ALL hardcoded prompts - GlobalAIPrompt is now the ONLY source of default prompts
|
|
# To add/modify prompts, use Django admin: /admin/system/globalaiprompt/
|
|
|
|
# Mapping from function names to prompt types
|
|
FUNCTION_TO_PROMPT_TYPE = {
|
|
'auto_cluster': 'clustering',
|
|
'generate_ideas': 'ideas',
|
|
'generate_content': 'content_generation',
|
|
'generate_images': 'image_prompt_extraction',
|
|
'extract_image_prompts': 'image_prompt_extraction',
|
|
'generate_image_prompts': 'image_prompt_extraction',
|
|
'generate_site_structure': 'site_structure_generation',
|
|
'optimize_content': 'optimize_content',
|
|
# Phase 8: Universal Content Types
|
|
'generate_product_content': 'product_generation',
|
|
'generate_service_page': 'service_generation',
|
|
'generate_taxonomy': 'taxonomy_generation',
|
|
}
|
|
|
|
@classmethod
|
|
def get_prompt(
|
|
cls,
|
|
function_name: str,
|
|
account: Optional[Any] = None,
|
|
task: Optional[Any] = None,
|
|
context: Optional[Dict[str, Any]] = None
|
|
) -> str:
|
|
"""
|
|
Get prompt for a function with hierarchical resolution.
|
|
|
|
Priority:
|
|
1. task.prompt_override (if task provided and has override)
|
|
2. DB prompt for (account, function)
|
|
3. GlobalAIPrompt (REQUIRED - no hardcoded fallbacks)
|
|
|
|
Args:
|
|
function_name: AI function name (e.g., 'auto_cluster', 'generate_ideas')
|
|
account: Account object (optional)
|
|
task: Task object with optional prompt_override (optional)
|
|
context: Additional context for prompt rendering (optional)
|
|
|
|
Returns:
|
|
Prompt string ready for formatting
|
|
"""
|
|
# Step 1: Check task-level override
|
|
if task and hasattr(task, 'prompt_override') and task.prompt_override:
|
|
logger.info(f"Using task-level prompt override for {function_name}")
|
|
prompt = task.prompt_override
|
|
return cls._render_prompt(prompt, context or {})
|
|
|
|
# Step 2: Get prompt type
|
|
prompt_type = cls.FUNCTION_TO_PROMPT_TYPE.get(function_name, function_name)
|
|
|
|
# Step 3: Try DB prompt (account-specific)
|
|
if account:
|
|
try:
|
|
from igny8_core.modules.system.models import AIPrompt
|
|
db_prompt = AIPrompt.objects.get(
|
|
account=account,
|
|
prompt_type=prompt_type,
|
|
is_active=True
|
|
)
|
|
logger.info(f"Using account-specific prompt for {function_name} (account {account.id})")
|
|
prompt = db_prompt.prompt_value
|
|
return cls._render_prompt(prompt, context or {})
|
|
except Exception as e:
|
|
logger.debug(f"No account-specific prompt found for {function_name}: {e}")
|
|
|
|
# Step 4: Try GlobalAIPrompt (platform-wide default) - REQUIRED
|
|
try:
|
|
from igny8_core.modules.system.global_settings_models import GlobalAIPrompt
|
|
global_prompt = GlobalAIPrompt.objects.get(
|
|
prompt_type=prompt_type,
|
|
is_active=True
|
|
)
|
|
logger.info(f"Using global default prompt for {function_name} from GlobalAIPrompt")
|
|
prompt = global_prompt.prompt_value
|
|
return cls._render_prompt(prompt, context or {})
|
|
except Exception as e:
|
|
error_msg = (
|
|
f"ERROR: Global prompt '{prompt_type}' not found for function '{function_name}'. "
|
|
f"Please configure it in Django admin at: /admin/system/globalaiprompt/. "
|
|
f"Error: {e}"
|
|
)
|
|
logger.error(error_msg)
|
|
raise ValueError(error_msg)
|
|
|
|
@classmethod
|
|
def _render_prompt(cls, prompt_template: str, context: Dict[str, Any]) -> str:
|
|
"""
|
|
Render prompt template with context variables.
|
|
Supports both .format() style ({variable}) and placeholder replacement ([IGNY8_*]).
|
|
|
|
Args:
|
|
prompt_template: Prompt template string
|
|
context: Context variables for rendering
|
|
|
|
Returns:
|
|
Rendered prompt string
|
|
"""
|
|
if not context:
|
|
return prompt_template
|
|
|
|
rendered = prompt_template
|
|
|
|
# Step 1: Replace [IGNY8_*] placeholders first (always do this)
|
|
for key, value in context.items():
|
|
placeholder = f'[IGNY8_{key.upper()}]'
|
|
if placeholder in rendered:
|
|
rendered = rendered.replace(placeholder, str(value))
|
|
logger.debug(f"Replaced placeholder {placeholder} with {len(str(value))} characters")
|
|
|
|
# Step 2: Try .format() style for {variable} placeholders (if any remain)
|
|
# Normalize context keys - convert UPPER to lowercase for .format()
|
|
normalized_context = {}
|
|
for key, value in context.items():
|
|
# Try both original key and lowercase version
|
|
normalized_context[key] = value
|
|
normalized_context[key.lower()] = value
|
|
|
|
# Only try .format() if there are {variable} placeholders
|
|
if '{' in rendered and '}' in rendered:
|
|
try:
|
|
rendered = rendered.format(**normalized_context)
|
|
except (KeyError, ValueError, IndexError) as e:
|
|
# If .format() fails, log warning but keep the [IGNY8_*] replacements
|
|
logger.warning(f"Failed to format prompt with .format(): {e}. Using [IGNY8_*] replacements only.")
|
|
|
|
return rendered
|
|
|
|
@classmethod
|
|
def get_image_prompt_template(cls, account: Optional[Any] = None) -> str:
|
|
"""
|
|
Get image prompt template.
|
|
Returns template string (not rendered) - caller should format with .format()
|
|
"""
|
|
prompt_type = 'image_prompt_template'
|
|
|
|
# Try DB prompt
|
|
if account:
|
|
try:
|
|
from igny8_core.modules.system.models import AIPrompt
|
|
db_prompt = AIPrompt.objects.get(
|
|
account=account,
|
|
prompt_type=prompt_type,
|
|
is_active=True
|
|
)
|
|
return db_prompt.prompt_value
|
|
except Exception:
|
|
pass
|
|
|
|
# Try GlobalAIPrompt
|
|
try:
|
|
from igny8_core.modules.system.global_settings_models import GlobalAIPrompt
|
|
global_prompt = GlobalAIPrompt.objects.get(
|
|
prompt_type=prompt_type,
|
|
is_active=True
|
|
)
|
|
return global_prompt.prompt_value
|
|
except Exception:
|
|
# Fallback for image_prompt_template
|
|
return '{image_type} image for blog post titled "{post_title}": {image_prompt}'
|
|
|
|
@classmethod
|
|
def get_negative_prompt(cls, account: Optional[Any] = None) -> str:
|
|
"""
|
|
Get negative prompt.
|
|
Returns template string (not rendered).
|
|
"""
|
|
prompt_type = 'negative_prompt'
|
|
|
|
# Try DB prompt
|
|
if account:
|
|
try:
|
|
from igny8_core.modules.system.models import AIPrompt
|
|
db_prompt = AIPrompt.objects.get(
|
|
account=account,
|
|
prompt_type=prompt_type,
|
|
is_active=True
|
|
)
|
|
return db_prompt.prompt_value
|
|
except Exception:
|
|
pass
|
|
|
|
# Try GlobalAIPrompt
|
|
try:
|
|
from igny8_core.modules.system.global_settings_models import GlobalAIPrompt
|
|
global_prompt = GlobalAIPrompt.objects.get(
|
|
prompt_type=prompt_type,
|
|
is_active=True
|
|
)
|
|
return global_prompt.prompt_value
|
|
except Exception:
|
|
# Fallback for negative_prompt
|
|
return 'text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title'
|
|
|
|
|
|
# Convenience function for backward compatibility
|
|
def get_prompt(function_name: str, account=None, task=None, context=None) -> str:
|
|
"""Get prompt using registry"""
|
|
return PromptRegistry.get_prompt(function_name, account=account, task=task, context=context)
|
|
|