Files
ALwrity/backend/services/seo_tools/meta_description_service.py

420 lines
16 KiB
Python

"""
Meta Description Generation Service
AI-powered SEO meta description generator that creates compelling,
optimized descriptions for content creators and digital marketers.
"""
from typing import Dict, Any, List, Optional
from datetime import datetime
from loguru import logger
from ..llm_providers.main_text_generation import llm_text_gen
from middleware.logging_middleware import seo_logger
class MetaDescriptionService:
"""Service for generating AI-powered SEO meta descriptions"""
def __init__(self):
"""Initialize the meta description service"""
self.service_name = "meta_description_generator"
logger.info(f"Initialized {self.service_name}")
async def generate_meta_description(
self,
keywords: List[str],
tone: str = "General",
search_intent: str = "Informational Intent",
language: str = "English",
custom_prompt: Optional[str] = None
) -> Dict[str, Any]:
"""
Generate AI-powered meta descriptions based on keywords and parameters
Args:
keywords: List of target keywords
tone: Desired tone (General, Informative, Engaging, etc.)
search_intent: Type of search intent
language: Target language for generation
custom_prompt: Optional custom prompt override
Returns:
Dictionary containing generated meta descriptions and analysis
"""
try:
start_time = datetime.utcnow()
# Input validation
if not keywords or len(keywords) == 0:
raise ValueError("At least one keyword is required")
# Prepare keywords string
keywords_str = ", ".join(keywords[:10]) # Limit to 10 keywords
# Build the generation prompt
if custom_prompt:
prompt = custom_prompt
else:
prompt = self._build_meta_description_prompt(
keywords_str, tone, search_intent, language
)
# Generate meta descriptions using AI
logger.info(f"Generating meta descriptions for keywords: {keywords_str}")
ai_response = llm_text_gen(
prompt=prompt,
system_prompt=self._get_system_prompt(language)
)
# Parse and structure the response
meta_descriptions = self._parse_ai_response(ai_response)
# Analyze generated descriptions
analysis = self._analyze_meta_descriptions(meta_descriptions, keywords)
execution_time = (datetime.utcnow() - start_time).total_seconds()
result = {
"meta_descriptions": meta_descriptions,
"analysis": analysis,
"generation_params": {
"keywords": keywords,
"tone": tone,
"search_intent": search_intent,
"language": language,
"keywords_count": len(keywords)
},
"ai_model_info": {
"provider": "gemini",
"model": "gemini-2.0-flash-001",
"prompt_length": len(prompt),
"response_length": len(ai_response)
},
"execution_time": execution_time,
"timestamp": datetime.utcnow().isoformat()
}
# Log the operation
await seo_logger.log_tool_usage(
tool_name=self.service_name,
input_data={
"keywords": keywords,
"tone": tone,
"search_intent": search_intent,
"language": language
},
output_data=result,
success=True
)
await seo_logger.log_ai_analysis(
tool_name=self.service_name,
prompt=prompt,
response=ai_response,
model_used="gemini-2.0-flash-001"
)
logger.info(f"Successfully generated {len(meta_descriptions)} meta descriptions")
return result
except Exception as e:
logger.error(f"Error generating meta descriptions: {e}")
# Log the error
await seo_logger.log_tool_usage(
tool_name=self.service_name,
input_data={
"keywords": keywords,
"tone": tone,
"search_intent": search_intent,
"language": language
},
output_data={"error": str(e)},
success=False
)
raise
def _build_meta_description_prompt(
self,
keywords: str,
tone: str,
search_intent: str,
language: str
) -> str:
"""Build the AI prompt for meta description generation"""
intent_guidance = {
"Informational Intent": "Focus on providing value and answering questions",
"Commercial Intent": "Emphasize benefits and competitive advantages",
"Transactional Intent": "Include strong calls-to-action and urgency",
"Navigational Intent": "Highlight brand recognition and specific page content"
}
tone_guidance = {
"General": "balanced and professional",
"Informative": "educational and authoritative",
"Engaging": "compelling and conversational",
"Humorous": "light-hearted and memorable",
"Intriguing": "mysterious and curiosity-driven",
"Playful": "fun and energetic"
}
prompt = f"""
Create 5 compelling SEO meta descriptions for content targeting these keywords: {keywords}
Requirements:
- Length: 150-160 characters (optimal for search results)
- Language: {language}
- Tone: {tone_guidance.get(tone, tone)}
- Search Intent: {search_intent} - {intent_guidance.get(search_intent, "")}
- Include primary keywords naturally
- Create urgency or curiosity where appropriate
- Ensure each description is unique and actionable
Guidelines for effective meta descriptions:
1. Start with action words or emotional triggers
2. Include primary keyword in first 120 characters
3. Add value proposition or benefit
4. Use active voice
5. Consider including numbers or specific details
6. End with compelling reason to click
Please provide 5 different meta descriptions, each on a new line, numbered 1-5.
Focus on creating descriptions that will improve click-through rates for content creators and digital marketers.
"""
return prompt
def _get_system_prompt(self, language: str) -> str:
"""Get system prompt for meta description generation"""
return f"""You are an expert SEO copywriter specializing in meta descriptions that drive high click-through rates.
You understand search engine optimization, user psychology, and compelling copywriting.
Your goal is to create meta descriptions that:
- Accurately represent the content
- Entice users to click
- Include target keywords naturally
- Comply with search engine best practices
- Appeal to the target audience
Language: {language}
Always provide exactly 5 unique meta descriptions as requested, numbered 1-5.
"""
def _parse_ai_response(self, ai_response: str) -> List[Dict[str, Any]]:
"""Parse AI response into structured meta descriptions"""
descriptions = []
lines = ai_response.strip().split('\n')
current_desc = ""
for line in lines:
line = line.strip()
if not line:
continue
# Check if line starts with a number (1., 2., etc.)
if line and (line[0].isdigit() or line.startswith(('1.', '2.', '3.', '4.', '5.'))):
if current_desc:
# Process previous description
cleaned_desc = self._clean_description(current_desc)
if cleaned_desc:
descriptions.append(self._analyze_single_description(cleaned_desc))
# Start new description
current_desc = line
else:
# Continue current description
if current_desc:
current_desc += " " + line
# Process last description
if current_desc:
cleaned_desc = self._clean_description(current_desc)
if cleaned_desc:
descriptions.append(self._analyze_single_description(cleaned_desc))
# If parsing failed, create fallback descriptions
if not descriptions:
descriptions = self._create_fallback_descriptions(ai_response)
return descriptions[:5] # Ensure max 5 descriptions
def _clean_description(self, description: str) -> str:
"""Clean and format a meta description"""
# Remove numbering
cleaned = description
if cleaned and cleaned[0].isdigit():
# Remove "1. ", "2. ", etc.
cleaned = cleaned.split('.', 1)[-1].strip()
# Remove extra whitespace
cleaned = ' '.join(cleaned.split())
# Remove quotes if present
if cleaned.startswith('"') and cleaned.endswith('"'):
cleaned = cleaned[1:-1]
return cleaned
def _analyze_single_description(self, description: str) -> Dict[str, Any]:
"""Analyze a single meta description"""
char_count = len(description)
word_count = len(description.split())
# Check if length is optimal
length_status = "optimal" if 150 <= char_count <= 160 else \
"short" if char_count < 150 else "long"
return {
"text": description,
"character_count": char_count,
"word_count": word_count,
"length_status": length_status,
"seo_score": self._calculate_seo_score(description, char_count),
"recommendations": self._generate_recommendations(description, char_count)
}
def _calculate_seo_score(self, description: str, char_count: int) -> int:
"""Calculate SEO score for a meta description"""
score = 0
# Length scoring (40 points max)
if 150 <= char_count <= 160:
score += 40
elif 140 <= char_count <= 170:
score += 30
elif 130 <= char_count <= 180:
score += 20
else:
score += 10
# Action words (20 points max)
action_words = ['discover', 'learn', 'get', 'find', 'explore', 'unlock', 'master', 'boost', 'improve', 'achieve']
if any(word.lower() in description.lower() for word in action_words):
score += 20
# Numbers or specifics (15 points max)
if any(char.isdigit() for char in description):
score += 15
# Emotional triggers (15 points max)
emotional_words = ['amazing', 'incredible', 'proven', 'secret', 'ultimate', 'essential', 'exclusive', 'free']
if any(word.lower() in description.lower() for word in emotional_words):
score += 15
# Call to action (10 points max)
cta_phrases = ['click', 'read more', 'learn more', 'discover', 'find out', 'see how']
if any(phrase.lower() in description.lower() for phrase in cta_phrases):
score += 10
return min(score, 100) # Cap at 100
def _generate_recommendations(self, description: str, char_count: int) -> List[str]:
"""Generate recommendations for improving meta description"""
recommendations = []
if char_count < 150:
recommendations.append("Consider adding more detail to reach optimal length (150-160 characters)")
elif char_count > 160:
recommendations.append("Shorten description to fit within optimal length (150-160 characters)")
if not any(char.isdigit() for char in description):
recommendations.append("Consider adding specific numbers or statistics for better appeal")
action_words = ['discover', 'learn', 'get', 'find', 'explore', 'unlock', 'master', 'boost', 'improve', 'achieve']
if not any(word.lower() in description.lower() for word in action_words):
recommendations.append("Add action words to create urgency and encourage clicks")
if description.count(',') > 2:
recommendations.append("Simplify sentence structure for better readability")
return recommendations
def _analyze_meta_descriptions(self, descriptions: List[Dict[str, Any]], keywords: List[str]) -> Dict[str, Any]:
"""Analyze all generated meta descriptions"""
if not descriptions:
return {"error": "No descriptions generated"}
# Calculate overall statistics
avg_length = sum(desc["character_count"] for desc in descriptions) / len(descriptions)
avg_score = sum(desc["seo_score"] for desc in descriptions) / len(descriptions)
# Find best description
best_desc = max(descriptions, key=lambda x: x["seo_score"])
# Keyword coverage analysis
keyword_coverage = self._analyze_keyword_coverage(descriptions, keywords)
return {
"total_descriptions": len(descriptions),
"average_length": round(avg_length, 1),
"average_seo_score": round(avg_score, 1),
"best_description": best_desc,
"keyword_coverage": keyword_coverage,
"length_distribution": {
"optimal": len([d for d in descriptions if d["length_status"] == "optimal"]),
"short": len([d for d in descriptions if d["length_status"] == "short"]),
"long": len([d for d in descriptions if d["length_status"] == "long"])
}
}
def _analyze_keyword_coverage(self, descriptions: List[Dict[str, Any]], keywords: List[str]) -> Dict[str, Any]:
"""Analyze how well keywords are covered in descriptions"""
coverage_stats = {}
for keyword in keywords:
coverage_count = sum(
1 for desc in descriptions
if keyword.lower() in desc["text"].lower()
)
coverage_stats[keyword] = {
"covered_count": coverage_count,
"coverage_percentage": (coverage_count / len(descriptions)) * 100
}
return coverage_stats
def _create_fallback_descriptions(self, ai_response: str) -> List[Dict[str, Any]]:
"""Create fallback descriptions if parsing fails"""
# Split response into sentences and use first few as descriptions
sentences = ai_response.split('. ')
descriptions = []
for i, sentence in enumerate(sentences[:5]):
if len(sentence.strip()) > 50: # Minimum length check
desc_text = sentence.strip()
if not desc_text.endswith('.'):
desc_text += '.'
descriptions.append(self._analyze_single_description(desc_text))
return descriptions
async def health_check(self) -> Dict[str, Any]:
"""Health check for the meta description service"""
try:
# Test basic functionality
test_result = await self.generate_meta_description(
keywords=["test"],
tone="General",
search_intent="Informational Intent",
language="English"
)
return {
"status": "operational",
"service": self.service_name,
"test_passed": bool(test_result.get("meta_descriptions")),
"last_check": datetime.utcnow().isoformat()
}
except Exception as e:
return {
"status": "error",
"service": self.service_name,
"error": str(e),
"last_check": datetime.utcnow().isoformat()
}