AI Blog Writer - Implement modular architecture with research, outline, and core services
This commit is contained in:
21
backend/services/blog_writer/outline/__init__.py
Normal file
21
backend/services/blog_writer/outline/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Outline module for AI Blog Writer.
|
||||
|
||||
This module handles all outline-related functionality including:
|
||||
- AI-powered outline generation
|
||||
- Outline refinement and optimization
|
||||
- Section enhancement and rebalancing
|
||||
- Strategic content planning
|
||||
"""
|
||||
|
||||
from .outline_service import OutlineService
|
||||
from .outline_generator import OutlineGenerator
|
||||
from .outline_optimizer import OutlineOptimizer
|
||||
from .section_enhancer import SectionEnhancer
|
||||
|
||||
__all__ = [
|
||||
'OutlineService',
|
||||
'OutlineGenerator',
|
||||
'OutlineOptimizer',
|
||||
'SectionEnhancer'
|
||||
]
|
||||
351
backend/services/blog_writer/outline/outline_generator.py
Normal file
351
backend/services/blog_writer/outline/outline_generator.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
Outline Generator - AI-powered outline generation from research data.
|
||||
|
||||
Generates comprehensive, SEO-optimized outlines using research intelligence.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
import asyncio
|
||||
from loguru import logger
|
||||
|
||||
from models.blog_models import (
|
||||
BlogOutlineRequest,
|
||||
BlogOutlineResponse,
|
||||
BlogOutlineSection,
|
||||
)
|
||||
|
||||
|
||||
class OutlineGenerator:
|
||||
"""Generates AI-powered outlines from research data."""
|
||||
|
||||
async def generate(self, request: BlogOutlineRequest) -> BlogOutlineResponse:
|
||||
"""
|
||||
Generate AI-powered outline using research results
|
||||
"""
|
||||
# Extract research insights
|
||||
research = request.research
|
||||
primary_keywords = research.keyword_analysis.get('primary', [])
|
||||
secondary_keywords = research.keyword_analysis.get('secondary', [])
|
||||
content_angles = research.suggested_angles
|
||||
sources = research.sources
|
||||
search_intent = research.keyword_analysis.get('search_intent', 'informational')
|
||||
|
||||
# Check for custom instructions
|
||||
custom_instructions = getattr(request, 'custom_instructions', None)
|
||||
|
||||
# Build comprehensive outline generation prompt with rich research data
|
||||
outline_prompt = self._build_outline_prompt(
|
||||
primary_keywords, secondary_keywords, content_angles, sources,
|
||||
search_intent, request, custom_instructions
|
||||
)
|
||||
|
||||
logger.info("Generating AI-powered outline using research results")
|
||||
|
||||
# Define schema with proper property ordering (critical for Gemini API)
|
||||
outline_schema = self._get_outline_schema()
|
||||
|
||||
# Generate outline using structured JSON response with retry logic
|
||||
outline_data = await self._generate_with_retry(outline_prompt, outline_schema)
|
||||
|
||||
# Convert to BlogOutlineSection objects
|
||||
outline_sections = self._convert_to_sections(outline_data, sources)
|
||||
|
||||
# Extract title options
|
||||
title_options = outline_data.get('title_options', [])
|
||||
if not title_options:
|
||||
title_options = self._generate_fallback_titles(primary_keywords)
|
||||
|
||||
logger.info(f"Generated outline with {len(outline_sections)} sections and {len(title_options)} title options")
|
||||
|
||||
return BlogOutlineResponse(
|
||||
success=True,
|
||||
title_options=title_options,
|
||||
outline=outline_sections
|
||||
)
|
||||
|
||||
async def generate_with_progress(self, request: BlogOutlineRequest, task_id: str) -> BlogOutlineResponse:
|
||||
"""
|
||||
Outline generation method with progress updates for real-time feedback.
|
||||
"""
|
||||
from api.blog_writer.router import _update_progress
|
||||
|
||||
# Extract research insights
|
||||
research = request.research
|
||||
primary_keywords = research.keyword_analysis.get('primary', [])
|
||||
secondary_keywords = research.keyword_analysis.get('secondary', [])
|
||||
content_angles = research.suggested_angles
|
||||
sources = research.sources
|
||||
search_intent = research.keyword_analysis.get('search_intent', 'informational')
|
||||
|
||||
# Check for custom instructions
|
||||
custom_instructions = getattr(request, 'custom_instructions', None)
|
||||
|
||||
await _update_progress(task_id, "📊 Analyzing research data and building content strategy...")
|
||||
|
||||
# Build comprehensive outline generation prompt with rich research data
|
||||
outline_prompt = self._build_outline_prompt(
|
||||
primary_keywords, secondary_keywords, content_angles, sources,
|
||||
search_intent, request, custom_instructions
|
||||
)
|
||||
|
||||
await _update_progress(task_id, "🤖 Generating AI-powered outline with research insights...")
|
||||
|
||||
# Define schema with proper property ordering (critical for Gemini API)
|
||||
outline_schema = self._get_outline_schema()
|
||||
|
||||
await _update_progress(task_id, "🔄 Making AI request to generate structured outline...")
|
||||
|
||||
# Generate outline using structured JSON response with retry logic
|
||||
outline_data = await self._generate_with_retry(outline_prompt, outline_schema, task_id)
|
||||
|
||||
await _update_progress(task_id, "📝 Processing outline structure and validating sections...")
|
||||
|
||||
# Convert to BlogOutlineSection objects
|
||||
outline_sections = self._convert_to_sections(outline_data, sources)
|
||||
|
||||
# Extract title options
|
||||
title_options = outline_data.get('title_options', [])
|
||||
if not title_options:
|
||||
title_options = self._generate_fallback_titles(primary_keywords)
|
||||
|
||||
await _update_progress(task_id, "✅ Outline generation completed successfully!")
|
||||
|
||||
return BlogOutlineResponse(
|
||||
success=True,
|
||||
title_options=title_options,
|
||||
outline=outline_sections
|
||||
)
|
||||
|
||||
def _build_outline_prompt(self, primary_keywords: List[str], secondary_keywords: List[str],
|
||||
content_angles: List[str], sources: List, search_intent: str,
|
||||
request: BlogOutlineRequest, custom_instructions: str = None) -> str:
|
||||
"""Build the comprehensive outline generation prompt."""
|
||||
return f"""
|
||||
You are a world-class content strategist and SEO expert with 15+ years of experience creating viral, high-converting blog content. Your outlines have generated millions of views and driven significant business results.
|
||||
|
||||
CONTENT STRATEGY BRIEF:
|
||||
Topic: {', '.join(primary_keywords)}
|
||||
Search Intent: {search_intent}
|
||||
Target Word Count: {request.word_count or 1500} words
|
||||
Industry Context: {getattr(request.persona, 'industry', 'General') if request.persona else 'General'}
|
||||
Audience: {getattr(request.persona, 'target_audience', 'General') if request.persona else 'General'}
|
||||
|
||||
{f"CUSTOM USER INSTRUCTIONS: {custom_instructions}" if custom_instructions else ""}
|
||||
|
||||
RESEARCH INTELLIGENCE:
|
||||
Primary Keywords: {', '.join(primary_keywords)}
|
||||
Secondary Keywords: {', '.join(secondary_keywords)}
|
||||
Long-tail Opportunities: {', '.join(request.research.keyword_analysis.get('long_tail', [])[:5])}
|
||||
Semantic Keywords: {', '.join(request.research.keyword_analysis.get('semantic_keywords', [])[:5])}
|
||||
Trending Terms: {', '.join(request.research.keyword_analysis.get('trending_terms', [])[:3])}
|
||||
Keyword Difficulty: {request.research.keyword_analysis.get('difficulty', 6)}/10
|
||||
Content Gaps: {', '.join(request.research.keyword_analysis.get('content_gaps', [])[:3])}
|
||||
|
||||
Content Angles Discovered:
|
||||
{chr(10).join([f"• {angle}" for angle in content_angles[:6]])}
|
||||
|
||||
Competitive Intelligence:
|
||||
Top Competitors: {', '.join(request.research.competitor_analysis.get('top_competitors', [])[:3])}
|
||||
Market Opportunities: {', '.join(request.research.competitor_analysis.get('opportunities', [])[:3])}
|
||||
Competitive Advantages: {', '.join(request.research.competitor_analysis.get('competitive_advantages', [])[:3])}
|
||||
Market Positioning: {request.research.competitor_analysis.get('market_positioning', 'Standard positioning')}
|
||||
|
||||
Research Sources Available: {len(sources)} authoritative sources with current data
|
||||
Key Statistics Available: Multiple data points, percentages, and expert quotes from credible sources
|
||||
|
||||
STRATEGIC OUTLINE REQUIREMENTS:
|
||||
|
||||
{f"CUSTOM REQUIREMENTS: {custom_instructions}" if custom_instructions else ""}
|
||||
|
||||
1. CONTENT ARCHITECTURE:
|
||||
- Create a logical, engaging narrative arc that guides readers from problem to solution
|
||||
- Structure content to build authority and trust progressively
|
||||
- Include data-driven insights and expert opinions from research
|
||||
- Ensure each section adds unique value and builds upon previous sections
|
||||
|
||||
2. SEO OPTIMIZATION:
|
||||
- Naturally integrate primary keywords in headings and content
|
||||
- Use secondary keywords strategically throughout sections
|
||||
- Include long-tail keywords in subheadings and key points
|
||||
- Optimize for featured snippets and voice search
|
||||
|
||||
3. READER ENGAGEMENT:
|
||||
- Start with compelling hooks and pain points
|
||||
- Use storytelling elements and real-world examples
|
||||
- Include actionable insights and practical takeaways
|
||||
- End with clear next steps and calls-to-action
|
||||
|
||||
4. CONTENT DEPTH:
|
||||
- Provide comprehensive coverage of the topic
|
||||
- Include multiple perspectives and expert insights
|
||||
- Address common questions and objections
|
||||
- Offer unique angles not covered by competitors
|
||||
|
||||
5. WORD COUNT DISTRIBUTION:
|
||||
- Introduction: 12% of total word count
|
||||
- Main content sections: 76% of total word count
|
||||
- Conclusion: 12% of total word count
|
||||
- Ensure balanced section lengths for optimal readability
|
||||
|
||||
6. COMPETITIVE ADVANTAGE:
|
||||
- Leverage content gaps identified in research
|
||||
- Include unique data points and statistics
|
||||
- Provide fresh perspectives on trending topics
|
||||
- Address underserved audience segments
|
||||
|
||||
TITLE STRATEGY:
|
||||
Create 5 compelling title options that:
|
||||
- Include primary keywords naturally
|
||||
- Promise clear value and outcomes
|
||||
- Appeal to the target audience's pain points
|
||||
- Stand out from competitor content
|
||||
- Optimize for click-through rates
|
||||
|
||||
Generate a comprehensive outline with the following structure:
|
||||
{{
|
||||
"title_options": [
|
||||
"Title 1 with primary keyword",
|
||||
"Title 2 with emotional hook",
|
||||
"Title 3 with benefit-focused approach",
|
||||
"Title 4 with question format",
|
||||
"Title 5 with urgency/trending angle"
|
||||
],
|
||||
"outline": [
|
||||
{{
|
||||
"heading": "Section heading with primary keyword",
|
||||
"subheadings": ["Subheading 1", "Subheading 2", "Subheading 3"],
|
||||
"key_points": ["Key point 1", "Key point 2", "Key point 3"],
|
||||
"word_count": 300,
|
||||
"keywords": ["primary keyword", "secondary keyword"]
|
||||
}}
|
||||
]
|
||||
}}
|
||||
"""
|
||||
|
||||
def _get_outline_schema(self) -> Dict[str, Any]:
|
||||
"""Get the structured JSON schema for outline generation."""
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title_options": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
},
|
||||
"outline": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heading": {"type": "string"},
|
||||
"subheadings": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
},
|
||||
"key_points": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
},
|
||||
"word_count": {"type": "integer"},
|
||||
"keywords": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"required": ["heading", "subheadings", "key_points", "word_count", "keywords"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["title_options", "outline"],
|
||||
"propertyOrdering": ["title_options", "outline"]
|
||||
}
|
||||
|
||||
async def _generate_with_retry(self, prompt: str, schema: Dict[str, Any], task_id: str = None) -> Dict[str, Any]:
|
||||
"""Generate outline with retry logic for API failures."""
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
from api.blog_writer.router import _update_progress
|
||||
|
||||
max_retries = 2 # Conservative retry for expensive API calls
|
||||
retry_delay = 5 # 5 second delay between retries
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
if task_id:
|
||||
await _update_progress(task_id, f"🤖 Calling Gemini API for outline generation (attempt {attempt + 1}/{max_retries + 1})...")
|
||||
|
||||
outline_data = gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=schema,
|
||||
temperature=0.3,
|
||||
max_tokens=4000 # Increased to avoid MAX_TOKENS truncation
|
||||
)
|
||||
|
||||
# Log response for debugging
|
||||
logger.info(f"Gemini response received: {type(outline_data)}")
|
||||
|
||||
# Check for errors in the response
|
||||
if isinstance(outline_data, dict) and 'error' in outline_data:
|
||||
error_msg = str(outline_data['error'])
|
||||
if "503" in error_msg and "overloaded" in error_msg and attempt < max_retries:
|
||||
if task_id:
|
||||
await _update_progress(task_id, f"⚠️ AI service overloaded, retrying in {retry_delay} seconds...")
|
||||
logger.warning(f"Gemini API overloaded, retrying in {retry_delay} seconds (attempt {attempt + 1}/{max_retries + 1})")
|
||||
await asyncio.sleep(retry_delay)
|
||||
continue
|
||||
else:
|
||||
logger.error(f"Gemini structured response error: {outline_data['error']}")
|
||||
raise ValueError(f"AI outline generation failed: {outline_data['error']}")
|
||||
|
||||
# Validate required fields
|
||||
if not isinstance(outline_data, dict) or 'outline' not in outline_data or not isinstance(outline_data['outline'], list):
|
||||
if attempt < max_retries:
|
||||
if task_id:
|
||||
await _update_progress(task_id, f"⚠️ Invalid response structure, retrying in {retry_delay} seconds...")
|
||||
logger.warning(f"Invalid response structure, retrying in {retry_delay} seconds (attempt {attempt + 1}/{max_retries + 1})")
|
||||
await asyncio.sleep(retry_delay)
|
||||
continue
|
||||
else:
|
||||
raise ValueError("Invalid outline structure in Gemini response")
|
||||
|
||||
# If we get here, the response is valid
|
||||
return outline_data
|
||||
|
||||
except Exception as e:
|
||||
error_str = str(e)
|
||||
if ("503" in error_str or "overloaded" in error_str) and attempt < max_retries:
|
||||
if task_id:
|
||||
await _update_progress(task_id, f"⚠️ AI service error, retrying in {retry_delay} seconds...")
|
||||
logger.warning(f"Gemini API error, retrying in {retry_delay} seconds (attempt {attempt + 1}/{max_retries + 1}): {error_str}")
|
||||
await asyncio.sleep(retry_delay)
|
||||
continue
|
||||
else:
|
||||
logger.error(f"Outline generation failed after {attempt + 1} attempts: {error_str}")
|
||||
raise ValueError(f"AI outline generation failed: {error_str}")
|
||||
|
||||
def _convert_to_sections(self, outline_data: Dict[str, Any], sources: List) -> List[BlogOutlineSection]:
|
||||
"""Convert outline data to BlogOutlineSection objects."""
|
||||
outline_sections = []
|
||||
for i, section_data in enumerate(outline_data.get('outline', [])):
|
||||
if not isinstance(section_data, dict) or 'heading' not in section_data:
|
||||
continue
|
||||
|
||||
section = BlogOutlineSection(
|
||||
id=f"s{i+1}",
|
||||
heading=section_data.get('heading', f'Section {i+1}'),
|
||||
subheadings=section_data.get('subheadings', []),
|
||||
key_points=section_data.get('key_points', []),
|
||||
references=sources[:3], # Use first 3 sources as references
|
||||
target_words=section_data.get('word_count', 200),
|
||||
keywords=section_data.get('keywords', [])
|
||||
)
|
||||
outline_sections.append(section)
|
||||
|
||||
return outline_sections
|
||||
|
||||
def _generate_fallback_titles(self, primary_keywords: List[str]) -> List[str]:
|
||||
"""Generate fallback titles when AI generation fails."""
|
||||
primary_keyword = primary_keywords[0] if primary_keywords else "Topic"
|
||||
return [
|
||||
f"The Complete Guide to {primary_keyword}",
|
||||
f"{primary_keyword}: Everything You Need to Know",
|
||||
f"How to Master {primary_keyword} in 2024"
|
||||
]
|
||||
114
backend/services/blog_writer/outline/outline_optimizer.py
Normal file
114
backend/services/blog_writer/outline/outline_optimizer.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Outline Optimizer - AI-powered outline optimization and rebalancing.
|
||||
|
||||
Optimizes outlines for better flow, SEO, and engagement.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
|
||||
from models.blog_models import BlogOutlineSection
|
||||
|
||||
|
||||
class OutlineOptimizer:
|
||||
"""Optimizes outlines for better flow, SEO, and engagement."""
|
||||
|
||||
async def optimize(self, outline: List[BlogOutlineSection], focus: str = "general optimization") -> List[BlogOutlineSection]:
|
||||
"""Optimize entire outline for better flow, SEO, and engagement."""
|
||||
outline_text = "\n".join([f"{i+1}. {s.heading}" for i, s in enumerate(outline)])
|
||||
|
||||
optimization_prompt = f"""
|
||||
Optimize this blog outline for better flow, engagement, and SEO:
|
||||
|
||||
Current Outline:
|
||||
{outline_text}
|
||||
|
||||
Optimization Focus: {focus}
|
||||
|
||||
Optimization Goals:
|
||||
- Improve narrative flow and logical progression
|
||||
- Enhance SEO with better keyword distribution
|
||||
- Increase engagement with compelling headings
|
||||
- Ensure comprehensive coverage of the topic
|
||||
- Optimize for featured snippets and voice search
|
||||
|
||||
Respond with JSON array of optimized sections:
|
||||
[
|
||||
{{
|
||||
"heading": "Optimized heading",
|
||||
"subheadings": ["subheading 1", "subheading 2"],
|
||||
"key_points": ["point 1", "point 2"],
|
||||
"target_words": 300,
|
||||
"keywords": ["keyword1", "keyword2"]
|
||||
}}
|
||||
]
|
||||
"""
|
||||
|
||||
try:
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
optimization_schema = {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heading": {"type": "string"},
|
||||
"subheadings": {"type": "array", "items": {"type": "string"}},
|
||||
"key_points": {"type": "array", "items": {"type": "string"}},
|
||||
"target_words": {"type": "integer"},
|
||||
"keywords": {"type": "array", "items": {"type": "string"}}
|
||||
},
|
||||
"required": ["heading", "subheadings", "key_points", "target_words", "keywords"]
|
||||
}
|
||||
}
|
||||
|
||||
optimized_data = gemini_structured_json_response(
|
||||
prompt=optimization_prompt,
|
||||
schema=optimization_schema,
|
||||
temperature=0.3,
|
||||
max_tokens=2000
|
||||
)
|
||||
|
||||
if isinstance(optimized_data, list):
|
||||
optimized_sections = []
|
||||
for i, section_data in enumerate(optimized_data):
|
||||
section = BlogOutlineSection(
|
||||
id=f"s{i+1}",
|
||||
heading=section_data.get('heading', f'Section {i+1}'),
|
||||
subheadings=section_data.get('subheadings', []),
|
||||
key_points=section_data.get('key_points', []),
|
||||
references=outline[i].references if i < len(outline) else [],
|
||||
target_words=section_data.get('target_words', 300),
|
||||
keywords=section_data.get('keywords', [])
|
||||
)
|
||||
optimized_sections.append(section)
|
||||
return optimized_sections
|
||||
except Exception as e:
|
||||
logger.warning(f"AI outline optimization failed: {e}")
|
||||
|
||||
return outline
|
||||
|
||||
def rebalance_word_counts(self, outline: List[BlogOutlineSection], target_words: int) -> List[BlogOutlineSection]:
|
||||
"""Rebalance word count distribution across sections."""
|
||||
total_sections = len(outline)
|
||||
if total_sections == 0:
|
||||
return outline
|
||||
|
||||
# Calculate target distribution
|
||||
intro_words = int(target_words * 0.12) # 12% for intro
|
||||
conclusion_words = int(target_words * 0.12) # 12% for conclusion
|
||||
main_content_words = target_words - intro_words - conclusion_words
|
||||
|
||||
# Distribute main content words across sections
|
||||
words_per_section = main_content_words // total_sections
|
||||
remainder = main_content_words % total_sections
|
||||
|
||||
for i, section in enumerate(outline):
|
||||
if i == 0: # First section (intro)
|
||||
section.target_words = intro_words
|
||||
elif i == total_sections - 1: # Last section (conclusion)
|
||||
section.target_words = conclusion_words
|
||||
else: # Main content sections
|
||||
section.target_words = words_per_section + (1 if i < remainder else 0)
|
||||
|
||||
return outline
|
||||
154
backend/services/blog_writer/outline/outline_service.py
Normal file
154
backend/services/blog_writer/outline/outline_service.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Outline Service - Core outline generation and management functionality.
|
||||
|
||||
Handles AI-powered outline generation, refinement, and optimization.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
import asyncio
|
||||
from loguru import logger
|
||||
|
||||
from models.blog_models import (
|
||||
BlogOutlineRequest,
|
||||
BlogOutlineResponse,
|
||||
BlogOutlineRefineRequest,
|
||||
BlogOutlineSection,
|
||||
)
|
||||
|
||||
from .outline_generator import OutlineGenerator
|
||||
from .outline_optimizer import OutlineOptimizer
|
||||
from .section_enhancer import SectionEnhancer
|
||||
|
||||
|
||||
class OutlineService:
|
||||
"""Service for generating and managing blog outlines using AI."""
|
||||
|
||||
def __init__(self):
|
||||
self.outline_generator = OutlineGenerator()
|
||||
self.outline_optimizer = OutlineOptimizer()
|
||||
self.section_enhancer = SectionEnhancer()
|
||||
|
||||
async def generate_outline(self, request: BlogOutlineRequest) -> BlogOutlineResponse:
|
||||
"""
|
||||
Stage 2: Content Planning with AI-generated outline using research results
|
||||
Uses Gemini with research data to create comprehensive, SEO-optimized outline
|
||||
"""
|
||||
return await self.outline_generator.generate(request)
|
||||
|
||||
async def generate_outline_with_progress(self, request: BlogOutlineRequest, task_id: str) -> BlogOutlineResponse:
|
||||
"""
|
||||
Outline generation method with progress updates for real-time feedback.
|
||||
"""
|
||||
return await self.outline_generator.generate_with_progress(request, task_id)
|
||||
|
||||
async def refine_outline(self, request: BlogOutlineRefineRequest) -> BlogOutlineResponse:
|
||||
"""
|
||||
Refine outline with HITL (Human-in-the-Loop) operations
|
||||
Supports add, remove, move, merge, rename operations
|
||||
"""
|
||||
outline = request.outline.copy()
|
||||
operation = request.operation.lower()
|
||||
section_id = request.section_id
|
||||
payload = request.payload or {}
|
||||
|
||||
try:
|
||||
if operation == 'add':
|
||||
# Add new section
|
||||
new_section = BlogOutlineSection(
|
||||
id=f"s{len(outline) + 1}",
|
||||
heading=payload.get('heading', 'New Section'),
|
||||
subheadings=payload.get('subheadings', []),
|
||||
key_points=payload.get('key_points', []),
|
||||
references=[],
|
||||
target_words=payload.get('target_words', 300)
|
||||
)
|
||||
outline.append(new_section)
|
||||
logger.info(f"Added new section: {new_section.heading}")
|
||||
|
||||
elif operation == 'remove' and section_id:
|
||||
# Remove section
|
||||
outline = [s for s in outline if s.id != section_id]
|
||||
logger.info(f"Removed section: {section_id}")
|
||||
|
||||
elif operation == 'rename' and section_id:
|
||||
# Rename section
|
||||
for section in outline:
|
||||
if section.id == section_id:
|
||||
section.heading = payload.get('heading', section.heading)
|
||||
break
|
||||
logger.info(f"Renamed section {section_id} to: {payload.get('heading')}")
|
||||
|
||||
elif operation == 'move' and section_id:
|
||||
# Move section (reorder)
|
||||
direction = payload.get('direction', 'down') # 'up' or 'down'
|
||||
current_index = next((i for i, s in enumerate(outline) if s.id == section_id), -1)
|
||||
|
||||
if current_index != -1:
|
||||
if direction == 'up' and current_index > 0:
|
||||
outline[current_index], outline[current_index - 1] = outline[current_index - 1], outline[current_index]
|
||||
elif direction == 'down' and current_index < len(outline) - 1:
|
||||
outline[current_index], outline[current_index + 1] = outline[current_index + 1], outline[current_index]
|
||||
logger.info(f"Moved section {section_id} {direction}")
|
||||
|
||||
elif operation == 'merge' and section_id:
|
||||
# Merge with next section
|
||||
current_index = next((i for i, s in enumerate(outline) if s.id == section_id), -1)
|
||||
if current_index != -1 and current_index < len(outline) - 1:
|
||||
current_section = outline[current_index]
|
||||
next_section = outline[current_index + 1]
|
||||
|
||||
# Merge sections
|
||||
current_section.heading = f"{current_section.heading} & {next_section.heading}"
|
||||
current_section.subheadings.extend(next_section.subheadings)
|
||||
current_section.key_points.extend(next_section.key_points)
|
||||
current_section.references.extend(next_section.references)
|
||||
current_section.target_words = (current_section.target_words or 0) + (next_section.target_words or 0)
|
||||
|
||||
# Remove the next section
|
||||
outline.pop(current_index + 1)
|
||||
logger.info(f"Merged section {section_id} with next section")
|
||||
|
||||
elif operation == 'update' and section_id:
|
||||
# Update section details
|
||||
for section in outline:
|
||||
if section.id == section_id:
|
||||
if 'heading' in payload:
|
||||
section.heading = payload['heading']
|
||||
if 'subheadings' in payload:
|
||||
section.subheadings = payload['subheadings']
|
||||
if 'key_points' in payload:
|
||||
section.key_points = payload['key_points']
|
||||
if 'target_words' in payload:
|
||||
section.target_words = payload['target_words']
|
||||
break
|
||||
logger.info(f"Updated section {section_id}")
|
||||
|
||||
# Reassign IDs to maintain order
|
||||
for i, section in enumerate(outline):
|
||||
section.id = f"s{i+1}"
|
||||
|
||||
return BlogOutlineResponse(
|
||||
success=True,
|
||||
title_options=["Refined Outline"],
|
||||
outline=outline
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Outline refinement failed: {e}")
|
||||
return BlogOutlineResponse(
|
||||
success=False,
|
||||
title_options=["Error"],
|
||||
outline=request.outline
|
||||
)
|
||||
|
||||
async def enhance_section_with_ai(self, section: BlogOutlineSection, focus: str = "general improvement") -> BlogOutlineSection:
|
||||
"""Enhance a section using AI with research context."""
|
||||
return await self.section_enhancer.enhance(section, focus)
|
||||
|
||||
async def optimize_outline_with_ai(self, outline: List[BlogOutlineSection], focus: str = "general optimization") -> List[BlogOutlineSection]:
|
||||
"""Optimize entire outline for better flow, SEO, and engagement."""
|
||||
return await self.outline_optimizer.optimize(outline, focus)
|
||||
|
||||
def rebalance_word_counts(self, outline: List[BlogOutlineSection], target_words: int) -> List[BlogOutlineSection]:
|
||||
"""Rebalance word count distribution across sections."""
|
||||
return self.outline_optimizer.rebalance_word_counts(outline, target_words)
|
||||
81
backend/services/blog_writer/outline/section_enhancer.py
Normal file
81
backend/services/blog_writer/outline/section_enhancer.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Section Enhancer - AI-powered section enhancement and improvement.
|
||||
|
||||
Enhances individual outline sections for better engagement and value.
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from models.blog_models import BlogOutlineSection
|
||||
|
||||
|
||||
class SectionEnhancer:
|
||||
"""Enhances individual outline sections using AI."""
|
||||
|
||||
async def enhance(self, section: BlogOutlineSection, focus: str = "general improvement") -> BlogOutlineSection:
|
||||
"""Enhance a section using AI with research context."""
|
||||
enhancement_prompt = f"""
|
||||
Enhance the following blog section to make it more engaging, comprehensive, and valuable:
|
||||
|
||||
Current Section:
|
||||
Heading: {section.heading}
|
||||
Subheadings: {', '.join(section.subheadings)}
|
||||
Key Points: {', '.join(section.key_points)}
|
||||
Target Words: {section.target_words}
|
||||
Keywords: {', '.join(section.keywords)}
|
||||
|
||||
Enhancement Focus: {focus}
|
||||
|
||||
Improve:
|
||||
1. Make subheadings more specific and actionable
|
||||
2. Add more comprehensive key points with data/insights
|
||||
3. Include practical examples and case studies
|
||||
4. Address common questions and objections
|
||||
5. Optimize for SEO with better keyword integration
|
||||
|
||||
Respond with JSON:
|
||||
{{
|
||||
"heading": "Enhanced heading",
|
||||
"subheadings": ["enhanced subheading 1", "enhanced subheading 2"],
|
||||
"key_points": ["enhanced point 1", "enhanced point 2"],
|
||||
"target_words": 400,
|
||||
"keywords": ["keyword1", "keyword2"]
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
enhancement_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heading": {"type": "string"},
|
||||
"subheadings": {"type": "array", "items": {"type": "string"}},
|
||||
"key_points": {"type": "array", "items": {"type": "string"}},
|
||||
"target_words": {"type": "integer"},
|
||||
"keywords": {"type": "array", "items": {"type": "string"}}
|
||||
},
|
||||
"required": ["heading", "subheadings", "key_points", "target_words", "keywords"]
|
||||
}
|
||||
|
||||
enhanced_data = gemini_structured_json_response(
|
||||
prompt=enhancement_prompt,
|
||||
schema=enhancement_schema,
|
||||
temperature=0.4,
|
||||
max_tokens=1000
|
||||
)
|
||||
|
||||
if isinstance(enhanced_data, dict) and 'error' not in enhanced_data:
|
||||
return BlogOutlineSection(
|
||||
id=section.id,
|
||||
heading=enhanced_data.get('heading', section.heading),
|
||||
subheadings=enhanced_data.get('subheadings', section.subheadings),
|
||||
key_points=enhanced_data.get('key_points', section.key_points),
|
||||
references=section.references,
|
||||
target_words=enhanced_data.get('target_words', section.target_words),
|
||||
keywords=enhanced_data.get('keywords', section.keywords)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"AI section enhancement failed: {e}")
|
||||
|
||||
return section
|
||||
Reference in New Issue
Block a user