""" 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)