Files
igny8/backend/igny8_core/ai/prompts.py
2025-11-09 19:34:54 +05:00

271 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Prompt Registry - Centralized prompt management with override hierarchy
Supports: task-level overrides → DB prompts → default fallbacks
"""
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. Default fallback from registry
"""
# Default prompts stored in registry
DEFAULT_PROMPTS = {
'clustering': """Analyze the following keywords and group them into topic clusters.
Each cluster should include:
- "name": A clear, descriptive topic name
- "description": A brief explanation of what the cluster covers
- "keywords": A list of related keywords that belong to this cluster
Format the output as a JSON object with a "clusters" array.
IMPORTANT: In the "keywords" array, you MUST use the EXACT keyword strings from the input list below. Do not modify, paraphrase, or create variations of the keywords. Only use the exact keywords as they appear in the input list.
Clustering rules:
- Group keywords based on strong semantic or topical relationships (intent, use-case, function, audience, etc.)
- Clusters should reflect how people actually search — problem ➝ solution, general ➝ specific, product ➝ benefit, etc.
- Avoid grouping keywords just because they share similar words — focus on meaning
- Include 310 keywords per cluster where appropriate
- Skip unrelated or outlier keywords that don't fit a clear theme
- CRITICAL: Only return keywords that exactly match the input keywords (case-insensitive matching is acceptable)
Keywords to process:
[IGNY8_KEYWORDS]""",
'ideas': """Generate SEO-optimized, high-quality content ideas and detailed outlines for each of the following keyword clusters.
Clusters to analyze:
[IGNY8_CLUSTERS]
Keywords in each cluster:
[IGNY8_CLUSTER_KEYWORDS]
Return your response as JSON with an "ideas" array.
For each cluster, generate 1-3 content ideas.
Each idea must include:
- "title": compelling blog/article title that naturally includes a primary keyword
- "description": detailed content outline with H2/H3 structure (as plain text or structured JSON)
- "content_type": the type of content (blog_post, article, guide, tutorial)
- "content_structure": the editorial structure (cluster_hub, landing_page, pillar_page, supporting_page)
- "estimated_word_count": estimated total word count (1500-2200 words)
- "target_keywords": comma-separated list of keywords that will be covered (or "covered_keywords")
- "cluster_name": name of the cluster this idea belongs to (REQUIRED)
- "cluster_id": ID of the cluster this idea belongs to (REQUIRED - use the exact cluster ID from the input)
IMPORTANT: You MUST include the exact "cluster_id" from the cluster data provided. Match the cluster name to find the correct cluster_id.
Return only valid JSON with an "ideas" array.""",
'content_generation': """You are an editorial content strategist. Generate a complete blog post/article based on the provided content idea.
CONTENT IDEA DETAILS:
[IGNY8_IDEA]
KEYWORD CLUSTER:
[IGNY8_CLUSTER]
ASSOCIATED KEYWORDS:
[IGNY8_KEYWORDS]
Generate well-structured, SEO-optimized content with:
- Engaging introduction
- 5-8 H2 sections with H3 subsections
- Natural keyword integration
- 1500-2000 words total
- Proper HTML formatting (h2, h3, p, ul, ol, table tags)
Return the content as plain text with HTML tags.""",
'image_prompt_extraction': """Extract image prompts from the following article content.
ARTICLE TITLE: {title}
ARTICLE CONTENT:
{content}
Extract image prompts for:
1. Featured Image: One main image that represents the article topic
2. In-Article Images: Up to {max_images} images that would be useful within the article content
Return a JSON object with this structure:
{{
"featured_prompt": "Detailed description of the featured image",
"in_article_prompts": [
"Description of first in-article image",
"Description of second in-article image",
...
]
}}
Make sure each prompt is detailed enough for image generation, describing the visual elements, style, mood, and composition.""",
'image_prompt_template': 'Create a high-quality {image_type} image to use as a featured photo for a blog post titled "{post_title}". The image should visually represent the theme, mood, and subject implied by the image prompt: {image_prompt}. Focus on a realistic, well-composed scene that naturally communicates the topic without text or logos. Use balanced lighting, pleasing composition, and photographic detail suitable for lifestyle or editorial web content. Avoid adding any visible or readable text, brand names, or illustrative effects. **And make sure image is not blurry.**',
'negative_prompt': 'text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title',
}
# 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',
}
@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. Default fallback from registry
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
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 DB 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 DB prompt found for {function_name}: {e}")
# Step 4: Use default fallback
prompt = cls.DEFAULT_PROMPTS.get(prompt_type, '')
if not prompt:
logger.warning(f"No default prompt found for {prompt_type}, using empty string")
return cls._render_prompt(prompt, context or {})
@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
# 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
# Try .format() style first (for {variable} placeholders)
try:
return prompt_template.format(**normalized_context)
except (KeyError, ValueError):
# Fall back to simple string replacement for [IGNY8_*] placeholders
rendered = prompt_template
for key, value in context.items():
placeholder = f'[IGNY8_{key.upper()}]'
if placeholder in rendered:
rendered = rendered.replace(placeholder, str(value))
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
# Use default
return cls.DEFAULT_PROMPTS.get(prompt_type, '')
@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
# Use default
return cls.DEFAULT_PROMPTS.get(prompt_type, '')
# 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)