Files
ALwrity/backend/services/blog_writer/content/introduction_generator.py

187 lines
7.6 KiB
Python

"""
Introduction Generator - Generates varied blog introductions based on content and research.
Generates 3 different introduction options for the user to choose from.
"""
from typing import Dict, Any, List
from loguru import logger
from models.blog_models import BlogResearchResponse, BlogOutlineSection
class IntroductionGenerator:
"""Generates blog introductions using research and content data."""
def __init__(self):
"""Initialize the introduction generator."""
pass
def build_introduction_prompt(
self,
blog_title: str,
research: BlogResearchResponse,
outline: List[BlogOutlineSection],
sections_content: Dict[str, str],
primary_keywords: List[str],
search_intent: str
) -> str:
"""Build a prompt for generating blog introductions."""
# Extract key research insights
keyword_analysis = research.keyword_analysis or {}
content_angles = research.suggested_angles or []
# Get a summary of the first few sections for context
section_summaries = []
for i, section in enumerate(outline[:3], 1):
section_id = section.id
content = sections_content.get(section_id, '')
if content:
# Take first 200 chars as summary
summary = content[:200] + '...' if len(content) > 200 else content
section_summaries.append(f"{i}. {section.heading}: {summary}")
sections_text = '\n'.join(section_summaries) if section_summaries else "Content sections are being generated."
primary_kw_text = ', '.join(primary_keywords) if primary_keywords else "the topic"
content_angle_text = ', '.join(content_angles[:3]) if content_angles else "General insights"
return f"""Generate exactly 3 varied blog introductions for the following blog post.
BLOG TITLE: {blog_title}
PRIMARY KEYWORDS: {primary_kw_text}
SEARCH INTENT: {search_intent}
CONTENT ANGLES: {content_angle_text}
BLOG CONTENT SUMMARY:
{sections_text}
REQUIREMENTS FOR EACH INTRODUCTION:
- 80-120 words in length
- Hook the reader immediately with a compelling opening
- Clearly state the value proposition and what readers will learn
- Include the primary keyword naturally within the first 2 sentences
- Each introduction should have a different angle/approach:
1. First: Problem-focused (highlight the challenge readers face)
2. Second: Benefit-focused (emphasize the value and outcomes)
3. Third: Story/statistic-focused (use a compelling fact or narrative hook)
- Maintain a professional yet engaging tone
- Avoid generic phrases - be specific and benefit-driven
Return ONLY a JSON array of exactly 3 introductions:
[
"First introduction (80-120 words, problem-focused)",
"Second introduction (80-120 words, benefit-focused)",
"Third introduction (80-120 words, story/statistic-focused)"
]"""
def get_introduction_schema(self) -> Dict[str, Any]:
"""Get the JSON schema for introduction generation."""
return {
"type": "array",
"items": {
"type": "string",
"minLength": 80,
"maxLength": 150
},
"minItems": 3,
"maxItems": 3
}
async def generate_introductions(
self,
blog_title: str,
research: BlogResearchResponse,
outline: List[BlogOutlineSection],
sections_content: Dict[str, str],
primary_keywords: List[str],
search_intent: str,
user_id: str
) -> List[str]:
"""Generate 3 varied blog introductions.
Args:
blog_title: The blog post title
research: Research data with keywords and insights
outline: Blog outline sections
sections_content: Dictionary mapping section IDs to their content
primary_keywords: Primary keywords for the blog
search_intent: Search intent (informational, commercial, etc.)
user_id: User ID for API calls
Returns:
List of 3 introduction options
"""
from services.llm_providers.main_text_generation import llm_text_gen
if not user_id:
raise ValueError("user_id is required for introduction generation")
# Build prompt
prompt = self.build_introduction_prompt(
blog_title=blog_title,
research=research,
outline=outline,
sections_content=sections_content,
primary_keywords=primary_keywords,
search_intent=search_intent
)
# Get schema
schema = self.get_introduction_schema()
logger.info(f"Generating blog introductions for user {user_id}")
try:
# Generate introductions using structured JSON response
result = llm_text_gen(
prompt=prompt,
json_struct=schema,
system_prompt="You are an expert content writer specializing in creating compelling blog introductions that hook readers and clearly communicate value.",
user_id=user_id
)
# Handle response - could be array directly or wrapped in dict
if isinstance(result, list):
introductions = result
elif isinstance(result, dict):
# Try common keys
introductions = result.get('introductions', result.get('options', result.get('intros', [])))
if not introductions and isinstance(result.get('response'), list):
introductions = result['response']
else:
logger.warning(f"Unexpected introduction generation result type: {type(result)}")
introductions = []
# Validate and clean introductions
cleaned_introductions = []
for intro in introductions:
if isinstance(intro, str) and len(intro.strip()) >= 50: # Minimum reasonable length
cleaned = intro.strip()
# Ensure it's within reasonable bounds
if len(cleaned) <= 200: # Allow slight overflow for quality
cleaned_introductions.append(cleaned)
# Ensure we have exactly 3 introductions
if len(cleaned_introductions) < 3:
logger.warning(f"Generated only {len(cleaned_introductions)} introductions, expected 3")
# Pad with placeholder if needed
while len(cleaned_introductions) < 3:
cleaned_introductions.append(f"{blog_title} - A comprehensive guide covering essential insights and practical strategies.")
# Return exactly 3 introductions
return cleaned_introductions[:3]
except Exception as e:
logger.error(f"Failed to generate introductions: {e}")
# Fallback: generate simple introductions
fallback_introductions = [
f"In this comprehensive guide, we'll explore {primary_keywords[0] if primary_keywords else 'essential insights'} and provide actionable strategies.",
f"Discover everything you need to know about {primary_keywords[0] if primary_keywords else 'this topic'} and how it can transform your approach.",
f"Whether you're new to {primary_keywords[0] if primary_keywords else 'this topic'} or looking to deepen your understanding, this guide has you covered."
]
return fallback_introductions