Files
igny8/backend/igny8_core/ai/prompts.py
IGNY8 VPS (Salman) abeede5f04 image prompt issues
2025-12-25 01:17:41 +00:00

226 lines
8.9 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 - provide both original case, lowercase, and uppercase
normalized_context = {}
for key, value in context.items():
# Add original key
normalized_context[key] = value
# Add lowercase version
normalized_context[key.lower()] = value
# Add uppercase version
normalized_context[key.upper()] = value
# Only try .format() if there are {variable} placeholders
if '{' in rendered and '}' in rendered:
try:
rendered = rendered.format(**normalized_context)
logger.debug(f"Successfully formatted prompt with context keys: {list(context.keys())}")
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}. Context keys: {list(context.keys())}. 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)